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
solverpilot/conic/ir.py
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import Enum
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
from types import MappingProxyType
|
|
8
|
+
from typing import Any, Mapping
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
from scipy import sparse
|
|
12
|
+
|
|
13
|
+
from solverpilot.problem import ConvexityStatus, ObjectiveSense, QuadraticProblem, VariableDomain
|
|
14
|
+
from solverpilot.problem.linear import _readonly_csr, _readonly_f64, _require_bound_directions, _require_finite
|
|
15
|
+
from solverpilot.problem.quadratic import _canonicalize_hessian, _check_convexity
|
|
16
|
+
from solverpilot.problem.hashing import _hash_parts, hash_sparse_matrix
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ConeKind(str, Enum):
|
|
20
|
+
SECOND_ORDER = "second_order"
|
|
21
|
+
ROTATED_SECOND_ORDER = "rotated_second_order"
|
|
22
|
+
POSITIVE_SEMIDEFINITE = "positive_semidefinite"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _readonly_shape(value: tuple[int, ...]) -> tuple[int, ...]:
|
|
26
|
+
out = tuple(int(x) for x in value)
|
|
27
|
+
if not out or any(x <= 0 for x in out):
|
|
28
|
+
raise ValueError("cone output_shape must contain positive dimensions")
|
|
29
|
+
return out
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _hash_array(arr: np.ndarray) -> str:
|
|
33
|
+
a = np.ascontiguousarray(np.asarray(arr, dtype=np.float64))
|
|
34
|
+
h = hashlib.sha256()
|
|
35
|
+
h.update(str(a.shape).encode())
|
|
36
|
+
h.update(a.tobytes())
|
|
37
|
+
return h.hexdigest()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _json_float(value: float) -> float | str:
|
|
41
|
+
v = float(value)
|
|
42
|
+
if np.isposinf(v): return "+inf"
|
|
43
|
+
if np.isneginf(v): return "-inf"
|
|
44
|
+
if np.isnan(v): raise ValueError("NaN cannot be serialized")
|
|
45
|
+
return v
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _csr_dict(matrix: sparse.csr_matrix) -> dict[str, Any]:
|
|
49
|
+
m = sparse.csr_matrix(matrix)
|
|
50
|
+
return {
|
|
51
|
+
"shape": [int(m.shape[0]), int(m.shape[1])],
|
|
52
|
+
"indptr": [int(x) for x in m.indptr],
|
|
53
|
+
"indices": [int(x) for x in m.indices],
|
|
54
|
+
"data": [float(x) for x in m.data],
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True, slots=True)
|
|
59
|
+
class ConeAffineBlock:
|
|
60
|
+
"""Affine cone membership block ``F x + g in K``.
|
|
61
|
+
|
|
62
|
+
For vector cones, ``output_shape`` is ``(dim,)``. For PSD cones it is
|
|
63
|
+
``(n, n)`` and the flattened affine map is row-major. The block itself is
|
|
64
|
+
solver-independent; backend-specific vectorization/scaling is performed by
|
|
65
|
+
the adapter.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
kind: ConeKind
|
|
69
|
+
F: sparse.csr_matrix
|
|
70
|
+
g: np.ndarray
|
|
71
|
+
output_shape: tuple[int, ...]
|
|
72
|
+
source_id: str | None = None
|
|
73
|
+
metadata: Mapping[str, Any] = field(default_factory=dict, compare=False, repr=False)
|
|
74
|
+
structural_hash: str = field(init=False)
|
|
75
|
+
data_hash: str = field(init=False)
|
|
76
|
+
|
|
77
|
+
def __post_init__(self) -> None:
|
|
78
|
+
kind = ConeKind(self.kind)
|
|
79
|
+
F = _readonly_csr(self.F)
|
|
80
|
+
g = _readonly_f64(self.g, name="cone offset")
|
|
81
|
+
_require_finite(g, name="cone offset")
|
|
82
|
+
shape = _readonly_shape(self.output_shape)
|
|
83
|
+
expected = int(np.prod(shape, dtype=np.int64))
|
|
84
|
+
if F.shape[0] != expected:
|
|
85
|
+
raise ValueError(f"cone F has {F.shape[0]} rows but output_shape requires {expected}")
|
|
86
|
+
if g.shape != (expected,):
|
|
87
|
+
raise ValueError(f"cone g must have shape ({expected},), got {g.shape}")
|
|
88
|
+
if kind is ConeKind.SECOND_ORDER:
|
|
89
|
+
if len(shape) != 1 or shape[0] < 2:
|
|
90
|
+
raise ValueError("second-order cone requires vector dimension >= 2")
|
|
91
|
+
elif kind is ConeKind.ROTATED_SECOND_ORDER:
|
|
92
|
+
if len(shape) != 1 or shape[0] < 3:
|
|
93
|
+
raise ValueError("rotated second-order cone requires vector dimension >= 3")
|
|
94
|
+
elif kind is ConeKind.POSITIVE_SEMIDEFINITE:
|
|
95
|
+
if len(shape) != 2 or shape[0] != shape[1]:
|
|
96
|
+
raise ValueError("PSD cone requires a square matrix output_shape")
|
|
97
|
+
else: # pragma: no cover - Enum guards this
|
|
98
|
+
raise ValueError(kind)
|
|
99
|
+
object.__setattr__(self, "kind", kind)
|
|
100
|
+
object.__setattr__(self, "F", F)
|
|
101
|
+
object.__setattr__(self, "g", g)
|
|
102
|
+
object.__setattr__(self, "output_shape", shape)
|
|
103
|
+
object.__setattr__(self, "metadata", MappingProxyType(dict(self.metadata)))
|
|
104
|
+
structural = _hash_parts([
|
|
105
|
+
kind.value.encode(),
|
|
106
|
+
json.dumps(shape).encode(),
|
|
107
|
+
hash_sparse_matrix(F.astype(bool)).encode(),
|
|
108
|
+
])
|
|
109
|
+
data = _hash_parts([
|
|
110
|
+
structural.encode(),
|
|
111
|
+
hash_sparse_matrix(F).encode(),
|
|
112
|
+
_hash_array(g).encode(),
|
|
113
|
+
])
|
|
114
|
+
object.__setattr__(self, "structural_hash", structural)
|
|
115
|
+
object.__setattr__(self, "data_hash", data)
|
|
116
|
+
|
|
117
|
+
def to_canonical_dict(self) -> dict[str, Any]:
|
|
118
|
+
return {
|
|
119
|
+
"kind": self.kind.value,
|
|
120
|
+
"F": _csr_dict(self.F),
|
|
121
|
+
"g": [float(x) for x in self.g],
|
|
122
|
+
"output_shape": list(self.output_shape),
|
|
123
|
+
"source_id": self.source_id,
|
|
124
|
+
"metadata": dict(self.metadata),
|
|
125
|
+
"structural_hash": self.structural_hash,
|
|
126
|
+
"data_hash": self.data_hash,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def n_variables(self) -> int:
|
|
131
|
+
return int(self.F.shape[1])
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def dimension(self) -> int:
|
|
135
|
+
return int(self.F.shape[0])
|
|
136
|
+
|
|
137
|
+
def value(self, x: np.ndarray) -> np.ndarray:
|
|
138
|
+
vec = np.asarray(self.F @ np.asarray(x, dtype=np.float64).reshape(-1), dtype=np.float64).reshape(-1) + self.g
|
|
139
|
+
return vec.reshape(self.output_shape)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass(frozen=True, slots=True)
|
|
143
|
+
class ConicProblem:
|
|
144
|
+
"""Convex quadratic objective + linear rows + affine cone memberships."""
|
|
145
|
+
|
|
146
|
+
P: sparse.csr_matrix
|
|
147
|
+
q: np.ndarray
|
|
148
|
+
A: sparse.csr_matrix
|
|
149
|
+
variable_lower: np.ndarray
|
|
150
|
+
variable_upper: np.ndarray
|
|
151
|
+
constraint_lower: np.ndarray
|
|
152
|
+
constraint_upper: np.ndarray
|
|
153
|
+
cones: tuple[ConeAffineBlock, ...]
|
|
154
|
+
objective_sense: ObjectiveSense = ObjectiveSense.MINIMIZE
|
|
155
|
+
objective_offset: float = 0.0
|
|
156
|
+
name: str | None = None
|
|
157
|
+
metadata: Mapping[str, Any] = field(default_factory=dict, compare=False, repr=False)
|
|
158
|
+
convexity_status: ConvexityStatus = ConvexityStatus.UNKNOWN
|
|
159
|
+
structural_hash: str = field(init=False)
|
|
160
|
+
data_hash: str = field(init=False)
|
|
161
|
+
|
|
162
|
+
def __post_init__(self) -> None:
|
|
163
|
+
P_raw = _readonly_csr(self.P)
|
|
164
|
+
q = _readonly_f64(self.q, name="q")
|
|
165
|
+
A = _readonly_csr(self.A)
|
|
166
|
+
vl = _readonly_f64(self.variable_lower, name="variable_lower")
|
|
167
|
+
vu = _readonly_f64(self.variable_upper, name="variable_upper")
|
|
168
|
+
cl = _readonly_f64(self.constraint_lower, name="constraint_lower")
|
|
169
|
+
cu = _readonly_f64(self.constraint_upper, name="constraint_upper")
|
|
170
|
+
sense = ObjectiveSense(self.objective_sense)
|
|
171
|
+
if sense is not ObjectiveSense.MINIMIZE:
|
|
172
|
+
raise ValueError("P6 ConicProblem supports convex minimization only")
|
|
173
|
+
m, n = A.shape
|
|
174
|
+
if P_raw.shape != (n, n):
|
|
175
|
+
raise ValueError(f"P must have shape ({n}, {n})")
|
|
176
|
+
P = _canonicalize_hessian(P_raw)
|
|
177
|
+
if q.shape != (n,) or vl.shape != (n,) or vu.shape != (n,):
|
|
178
|
+
raise ValueError("q/variable bounds must match n_variables")
|
|
179
|
+
_require_finite(q, name="q")
|
|
180
|
+
if cl.shape != (m,) or cu.shape != (m,):
|
|
181
|
+
raise ValueError("linear constraint bounds must match A rows")
|
|
182
|
+
if np.any(vl > vu) or np.any(cl > cu):
|
|
183
|
+
raise ValueError("inconsistent bounds")
|
|
184
|
+
_require_bound_directions(vl, vu, label="variable")
|
|
185
|
+
_require_bound_directions(cl, cu, label="constraint")
|
|
186
|
+
if not np.isfinite(float(self.objective_offset)):
|
|
187
|
+
raise ValueError("objective_offset must be finite")
|
|
188
|
+
cones = tuple(self.cones)
|
|
189
|
+
if not cones:
|
|
190
|
+
raise ValueError("ConicProblem requires at least one cone block")
|
|
191
|
+
for block in cones:
|
|
192
|
+
if block.n_variables != n:
|
|
193
|
+
raise ValueError("cone block variable dimension mismatch")
|
|
194
|
+
status = ConvexityStatus(self.convexity_status)
|
|
195
|
+
if status is ConvexityStatus.UNKNOWN:
|
|
196
|
+
status = _check_convexity(P)
|
|
197
|
+
elif status is ConvexityStatus.CONFIRMED and _check_convexity(P) is not ConvexityStatus.CONFIRMED:
|
|
198
|
+
raise ValueError("conic quadratic objective is not independently confirmed convex")
|
|
199
|
+
if status is ConvexityStatus.REJECTED:
|
|
200
|
+
raise ValueError("conic quadratic objective is not convex")
|
|
201
|
+
object.__setattr__(self, "P", P)
|
|
202
|
+
object.__setattr__(self, "q", q)
|
|
203
|
+
object.__setattr__(self, "A", A)
|
|
204
|
+
object.__setattr__(self, "variable_lower", vl)
|
|
205
|
+
object.__setattr__(self, "variable_upper", vu)
|
|
206
|
+
object.__setattr__(self, "constraint_lower", cl)
|
|
207
|
+
object.__setattr__(self, "constraint_upper", cu)
|
|
208
|
+
object.__setattr__(self, "cones", cones)
|
|
209
|
+
object.__setattr__(self, "objective_sense", sense)
|
|
210
|
+
object.__setattr__(self, "metadata", MappingProxyType(dict(self.metadata)))
|
|
211
|
+
object.__setattr__(self, "convexity_status", status)
|
|
212
|
+
structural = _hash_parts([
|
|
213
|
+
b"solverpilot-conic-ir-v1",
|
|
214
|
+
hash_sparse_matrix(P.astype(bool)).encode(),
|
|
215
|
+
hash_sparse_matrix(A.astype(bool)).encode(),
|
|
216
|
+
*(c.structural_hash.encode() for c in cones),
|
|
217
|
+
])
|
|
218
|
+
data = _hash_parts([
|
|
219
|
+
structural.encode(),
|
|
220
|
+
hash_sparse_matrix(P).encode(),
|
|
221
|
+
_hash_array(q).encode(),
|
|
222
|
+
hash_sparse_matrix(A).encode(),
|
|
223
|
+
_hash_array(vl).encode(), _hash_array(vu).encode(),
|
|
224
|
+
_hash_array(cl).encode(), _hash_array(cu).encode(),
|
|
225
|
+
*(c.data_hash.encode() for c in cones),
|
|
226
|
+
sense.value.encode(), float(self.objective_offset).hex().encode(),
|
|
227
|
+
])
|
|
228
|
+
object.__setattr__(self, "structural_hash", structural)
|
|
229
|
+
object.__setattr__(self, "data_hash", data)
|
|
230
|
+
|
|
231
|
+
def to_canonical_dict(self) -> dict[str, Any]:
|
|
232
|
+
return {
|
|
233
|
+
"schema": "solverpilot.conic-ir.v1",
|
|
234
|
+
"P": _csr_dict(self.P),
|
|
235
|
+
"q": [float(x) for x in self.q],
|
|
236
|
+
"A": _csr_dict(self.A),
|
|
237
|
+
"variable_lower": [_json_float(x) for x in self.variable_lower],
|
|
238
|
+
"variable_upper": [_json_float(x) for x in self.variable_upper],
|
|
239
|
+
"constraint_lower": [_json_float(x) for x in self.constraint_lower],
|
|
240
|
+
"constraint_upper": [_json_float(x) for x in self.constraint_upper],
|
|
241
|
+
"cones": [c.to_canonical_dict() for c in self.cones],
|
|
242
|
+
"objective_sense": self.objective_sense.value,
|
|
243
|
+
"objective_offset": float(self.objective_offset),
|
|
244
|
+
"name": self.name,
|
|
245
|
+
"metadata": dict(self.metadata),
|
|
246
|
+
"convexity_status": self.convexity_status.value,
|
|
247
|
+
"structural_hash": self.structural_hash,
|
|
248
|
+
"data_hash": self.data_hash,
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def n_variables(self) -> int:
|
|
253
|
+
return int(self.A.shape[1])
|
|
254
|
+
|
|
255
|
+
@property
|
|
256
|
+
def n_linear_constraints(self) -> int:
|
|
257
|
+
return int(self.A.shape[0])
|
|
258
|
+
|
|
259
|
+
@property
|
|
260
|
+
def n_cone_constraints(self) -> int:
|
|
261
|
+
return len(self.cones)
|
|
262
|
+
|
|
263
|
+
def objective_value(self, x: np.ndarray) -> float:
|
|
264
|
+
x = np.asarray(x, dtype=np.float64).reshape(-1)
|
|
265
|
+
return float(0.5 * x @ (self.P @ x) + self.q @ x + self.objective_offset)
|
|
266
|
+
|
|
267
|
+
@classmethod
|
|
268
|
+
def from_data(
|
|
269
|
+
cls,
|
|
270
|
+
*,
|
|
271
|
+
P: sparse.spmatrix | np.ndarray,
|
|
272
|
+
q: np.ndarray,
|
|
273
|
+
A: sparse.spmatrix | np.ndarray,
|
|
274
|
+
variable_lower: np.ndarray,
|
|
275
|
+
variable_upper: np.ndarray,
|
|
276
|
+
constraint_lower: np.ndarray,
|
|
277
|
+
constraint_upper: np.ndarray,
|
|
278
|
+
cones: tuple[ConeAffineBlock, ...] | list[ConeAffineBlock],
|
|
279
|
+
objective_offset: float = 0.0,
|
|
280
|
+
name: str | None = None,
|
|
281
|
+
metadata: Mapping[str, Any] | None = None,
|
|
282
|
+
verify_convexity: bool = True,
|
|
283
|
+
) -> "ConicProblem":
|
|
284
|
+
P_csr = sparse.csr_matrix(P, dtype=np.float64)
|
|
285
|
+
status = _check_convexity(P_csr) if verify_convexity else ConvexityStatus.UNKNOWN
|
|
286
|
+
return cls(
|
|
287
|
+
P=P_csr,
|
|
288
|
+
q=np.asarray(q, dtype=np.float64),
|
|
289
|
+
A=sparse.csr_matrix(A, dtype=np.float64),
|
|
290
|
+
variable_lower=np.asarray(variable_lower, dtype=np.float64),
|
|
291
|
+
variable_upper=np.asarray(variable_upper, dtype=np.float64),
|
|
292
|
+
constraint_lower=np.asarray(constraint_lower, dtype=np.float64),
|
|
293
|
+
constraint_upper=np.asarray(constraint_upper, dtype=np.float64),
|
|
294
|
+
cones=tuple(cones),
|
|
295
|
+
objective_offset=float(objective_offset),
|
|
296
|
+
name=name,
|
|
297
|
+
metadata={} if metadata is None else metadata,
|
|
298
|
+
convexity_status=status,
|
|
299
|
+
)
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
import math
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from solverpilot.validate.core import _max_violation, _scaled_feasibility
|
|
8
|
+
|
|
9
|
+
from .ir import ConeKind, ConicProblem
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True)
|
|
13
|
+
class ConeCheck:
|
|
14
|
+
source_id: str | None
|
|
15
|
+
kind: ConeKind
|
|
16
|
+
valid: bool
|
|
17
|
+
violation: float
|
|
18
|
+
detail: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, slots=True)
|
|
22
|
+
class ConicValidationReport:
|
|
23
|
+
valid: bool
|
|
24
|
+
max_variable_violation: float
|
|
25
|
+
max_linear_violation: float
|
|
26
|
+
max_cone_violation: float
|
|
27
|
+
cone_checks: tuple[ConeCheck, ...]
|
|
28
|
+
objective: float | None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _scaled_tol(atol: float, rtol: float, magnitude: float) -> float:
|
|
32
|
+
return float(atol + rtol * max(1.0, abs(float(magnitude))))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _componentwise_feasibility(
|
|
36
|
+
values: np.ndarray,
|
|
37
|
+
lower: np.ndarray,
|
|
38
|
+
upper: np.ndarray,
|
|
39
|
+
*,
|
|
40
|
+
atol: float,
|
|
41
|
+
rtol: float,
|
|
42
|
+
extra_scale: np.ndarray | None = None,
|
|
43
|
+
) -> tuple[float, bool]:
|
|
44
|
+
_, ok = _scaled_feasibility(values, lower, upper, atol=atol, rtol=rtol, extra_scale=extra_scale)
|
|
45
|
+
return _max_violation(values, lower, upper), ok
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _psd_check(M: np.ndarray, *, atol: float, rtol: float) -> tuple[bool, float, float, float]:
|
|
49
|
+
"""Check symmetry and PSD after local congruence scaling.
|
|
50
|
+
|
|
51
|
+
The congruence transform preserves inertia, while removing the dangerous
|
|
52
|
+
dependence on an unrelated huge matrix entry that a single global norm creates.
|
|
53
|
+
"""
|
|
54
|
+
if M.size == 0:
|
|
55
|
+
return True, 0.0, 0.0, 0.0
|
|
56
|
+
abs_m = np.abs(M)
|
|
57
|
+
sym_allowed = float(atol) + float(rtol) * np.maximum(1.0, np.maximum(abs_m, abs_m.T))
|
|
58
|
+
asym_matrix = np.abs(M - M.T)
|
|
59
|
+
asym = float(np.max(asym_matrix))
|
|
60
|
+
symmetry_ok = bool(np.all(asym_matrix <= sym_allowed))
|
|
61
|
+
sym = 0.5 * M + 0.5 * M.T
|
|
62
|
+
eigmin = float(np.linalg.eigvalsh(sym)[0])
|
|
63
|
+
|
|
64
|
+
row_scale = np.maximum(1.0, np.max(np.abs(sym), axis=1))
|
|
65
|
+
inv_sqrt = 1.0 / np.sqrt(row_scale)
|
|
66
|
+
scaled = inv_sqrt[:, None] * sym * inv_sqrt[None, :]
|
|
67
|
+
scaled_eigmin = float(np.linalg.eigvalsh(scaled)[0])
|
|
68
|
+
scaled_norm = float(np.linalg.norm(scaled, ord=np.inf))
|
|
69
|
+
psd_tol = float(atol) + float(rtol) * max(1.0, scaled_norm)
|
|
70
|
+
return bool(symmetry_ok and scaled_eigmin >= -psd_tol), asym, eigmin, scaled_eigmin
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _cone_check(kind: ConeKind, value: np.ndarray, *, atol: float, rtol: float, source_id: str | None) -> ConeCheck:
|
|
74
|
+
if not np.isfinite(value).all():
|
|
75
|
+
return ConeCheck(source_id, kind, False, float("inf"), "non-finite cone activity")
|
|
76
|
+
if kind is ConeKind.SECOND_ORDER:
|
|
77
|
+
y = np.asarray(value, dtype=float).reshape(-1)
|
|
78
|
+
t = float(y[0]); norm = math.hypot(*y[1:])
|
|
79
|
+
viol = max(0.0, norm - t)
|
|
80
|
+
tol = _scaled_tol(atol, rtol, max(abs(t), norm))
|
|
81
|
+
return ConeCheck(source_id, kind, math.isfinite(norm) and math.isfinite(tol) and viol <= tol, viol, f"norm={norm:.17g}, t={t:.17g}, tol={tol:.3g}")
|
|
82
|
+
if kind is ConeKind.ROTATED_SECOND_ORDER:
|
|
83
|
+
y = np.asarray(value, dtype=float).reshape(-1)
|
|
84
|
+
u = float(y[0]); v = float(y[1])
|
|
85
|
+
scale = max(1.0, float(np.max(np.abs(y))))
|
|
86
|
+
z = y / scale
|
|
87
|
+
norm2 = float(np.dot(z[2:], z[2:]))
|
|
88
|
+
product = 2.0 * float(z[0]) * float(z[1])
|
|
89
|
+
gap = norm2 - product
|
|
90
|
+
# Compare the squared inequality in normalized coordinates, avoiding
|
|
91
|
+
# inf - inf. Nonnegativity uses separate, unsquared tolerances.
|
|
92
|
+
tol = (atol / scale) / scale + rtol * max((1.0 / scale) / scale, norm2, abs(product))
|
|
93
|
+
nonnegative = u >= -_scaled_tol(atol, rtol, u) and v >= -_scaled_tol(atol, rtol, v)
|
|
94
|
+
viol = max(0.0, -u, -v, (max(0.0, gap) * scale) * scale)
|
|
95
|
+
return ConeCheck(source_id, kind, nonnegative and gap <= tol, viol, f"u={u:.17g}, v={v:.17g}, normalized_gap={gap:.17g}, tol={tol:.3g}")
|
|
96
|
+
if kind is ConeKind.POSITIVE_SEMIDEFINITE:
|
|
97
|
+
M = np.asarray(value, dtype=float)
|
|
98
|
+
ok, asym, eigmin, scaled_eigmin = _psd_check(M, atol=atol, rtol=rtol)
|
|
99
|
+
viol = max(asym, max(0.0, -eigmin))
|
|
100
|
+
return ConeCheck(
|
|
101
|
+
source_id,
|
|
102
|
+
kind,
|
|
103
|
+
ok,
|
|
104
|
+
viol,
|
|
105
|
+
f"asym={asym:.3g}, eigmin={eigmin:.17g}, scaled_eigmin={scaled_eigmin:.17g}",
|
|
106
|
+
)
|
|
107
|
+
raise AssertionError(kind)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def validate_conic_solution(
|
|
111
|
+
problem: ConicProblem,
|
|
112
|
+
x: np.ndarray | None,
|
|
113
|
+
*,
|
|
114
|
+
atol: float = 1e-7,
|
|
115
|
+
rtol: float = 1e-7,
|
|
116
|
+
) -> ConicValidationReport:
|
|
117
|
+
for name, raw in (("atol", atol), ("rtol", rtol)):
|
|
118
|
+
if isinstance(raw, bool) or type(raw) not in (int, float) or not np.isfinite(float(raw)) or float(raw) < 0:
|
|
119
|
+
raise ValueError(f"{name} must be finite and non-negative")
|
|
120
|
+
atol = float(atol)
|
|
121
|
+
rtol = float(rtol)
|
|
122
|
+
if x is None:
|
|
123
|
+
return ConicValidationReport(False, float("inf"), float("inf"), float("inf"), (), None)
|
|
124
|
+
x = np.asarray(x, dtype=np.float64).reshape(-1)
|
|
125
|
+
if x.shape != (problem.n_variables,) or not np.all(np.isfinite(x)):
|
|
126
|
+
return ConicValidationReport(False, float("inf"), float("inf"), float("inf"), (), None)
|
|
127
|
+
|
|
128
|
+
var_viol, vars_ok = _componentwise_feasibility(
|
|
129
|
+
x,
|
|
130
|
+
problem.variable_lower,
|
|
131
|
+
problem.variable_upper,
|
|
132
|
+
atol=atol,
|
|
133
|
+
rtol=rtol,
|
|
134
|
+
)
|
|
135
|
+
ax = np.asarray(problem.A @ x, dtype=float).reshape(-1)
|
|
136
|
+
row_scale = np.asarray(np.abs(problem.A) @ np.abs(x), dtype=np.float64).reshape(-1)
|
|
137
|
+
lin_viol, linear_ok = _componentwise_feasibility(
|
|
138
|
+
ax,
|
|
139
|
+
problem.constraint_lower,
|
|
140
|
+
problem.constraint_upper,
|
|
141
|
+
atol=atol,
|
|
142
|
+
rtol=rtol,
|
|
143
|
+
extra_scale=row_scale,
|
|
144
|
+
)
|
|
145
|
+
checks = tuple(_cone_check(c.kind, c.value(x), atol=atol, rtol=rtol, source_id=c.source_id) for c in problem.cones)
|
|
146
|
+
cone_viol = max((c.violation for c in checks), default=0.0)
|
|
147
|
+
objective = problem.objective_value(x)
|
|
148
|
+
valid = bool(vars_ok and linear_ok and all(c.valid for c in checks) and np.isfinite(objective))
|
|
149
|
+
return ConicValidationReport(valid, var_viol, lin_viol, cone_viol, checks, objective)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from .ir import *
|
|
2
|
+
from .model import CPModel, CPIntVar, CPBoolVar, CPIntervalVar, CPLinearExpr
|
|
3
|
+
from .validate import CPValidationReport, validate_cp_solution
|
|
4
|
+
from .reference import CPSolveResult, ReferenceCPBackend
|
|
5
|
+
from .ortools_backend import ORToolsCPSATBackend, VERIFIED_ORTOOLS_VERSION
|
|
6
|
+
from .conformance import CPConformanceCheck, CPConformanceReport, reference_cp_conformance, ortools_cp_sat_conformance
|
|
7
|
+
__all__=[n for n in globals() if n.startswith('CP') or n in {'ReferenceCPBackend','ORToolsCPSATBackend','VERIFIED_ORTOOLS_VERSION','validate_cp_solution','reference_cp_conformance','ortools_cp_sat_conformance'}]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from .model import CPModel
|
|
4
|
+
from .reference import ReferenceCPBackend
|
|
5
|
+
from .ortools_backend import ORToolsCPSATBackend, VERIFIED_ORTOOLS_VERSION
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True, slots=True)
|
|
8
|
+
class CPConformanceCheck:
|
|
9
|
+
name:str; passed:bool; detail:str
|
|
10
|
+
@dataclass(frozen=True, slots=True)
|
|
11
|
+
class CPConformanceReport:
|
|
12
|
+
backend:str; available:bool; passed:bool; checks:tuple[CPConformanceCheck,...]; verified_version:str|None=None
|
|
13
|
+
|
|
14
|
+
def reference_cp_conformance():
|
|
15
|
+
checks=[]
|
|
16
|
+
# Latin/permutation core.
|
|
17
|
+
m=CPModel(); xs=[m.int_var(1,3,f'x{i}') for i in range(3)]; m.add_all_different(xs); m.add(xs[0]==1); m.minimize(xs[2])
|
|
18
|
+
r=m.solve(ReferenceCPBackend()); checks.append(CPConformanceCheck('all-different-linear',r.optimality_proven and r.validation.valid and r.objective==2,str(r.assignment)))
|
|
19
|
+
# Element/table/exactly-one.
|
|
20
|
+
m=CPModel(); i=m.int_var(0,2,'i'); t=m.int_var(4,9,'t'); bs=[m.bool_var(f'b{k}') for k in range(3)]; m.add_element(i,[4,7,9],t); m.add_allowed_assignments([i,t],[(0,4),(2,9)]); m.add_exactly_one(bs); m.add(i==2); r=m.solve(); checks.append(CPConformanceCheck('element-table-exactly-one',r.validation.valid and r.assignment[t.var_id]==9,str(r.assignment)))
|
|
21
|
+
# Scheduling core.
|
|
22
|
+
m=CPModel(); s1=m.int_var(0,3,'s1'); s2=m.int_var(0,3,'s2'); a=m.interval_var(s1,2,'a'); b=m.interval_var(s2,2,'b'); m.add_no_overlap([a,b]); m.minimize(s1+s2); r=m.solve(); checks.append(CPConformanceCheck('interval-no-overlap',r.optimality_proven and r.validation.valid and r.objective==2,str(r.assignment)))
|
|
23
|
+
return CPConformanceReport('reference-cp-enumeration',True,all(c.passed for c in checks),tuple(checks),'p9-reference-v1')
|
|
24
|
+
|
|
25
|
+
def ortools_cp_sat_conformance():
|
|
26
|
+
b=ORToolsCPSATBackend();
|
|
27
|
+
if not b.is_available():
|
|
28
|
+
return CPConformanceReport(b.name,False,False,(CPConformanceCheck('availability',False,f'OR-Tools {VERIFIED_ORTOOLS_VERSION} unavailable in release runtime'),),VERIFIED_ORTOOLS_VERSION)
|
|
29
|
+
# Same representative model against adapter.
|
|
30
|
+
m=CPModel(); x=[m.int_var(1,3,f'x{i}') for i in range(3)]; m.add_all_different(x); m.add(x[0]==1); m.minimize(x[2]); r=m.solve(b)
|
|
31
|
+
ok=r.optimality_proven and r.validation.valid and r.objective==2
|
|
32
|
+
return CPConformanceReport(b.name,True,ok,(CPConformanceCheck('solve-and-independent-validation',ok,str(r.assignment)),),VERIFIED_ORTOOLS_VERSION)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema":"https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id":"solverpilot.cp-ir.v1",
|
|
4
|
+
"type":"object",
|
|
5
|
+
"required":["schema_version","variables","intervals","constraints","objective_sense"],
|
|
6
|
+
"properties":{
|
|
7
|
+
"schema_version":{"const":"solverpilot.cp-ir.v1"},
|
|
8
|
+
"variables":{"type":"array"},
|
|
9
|
+
"intervals":{"type":"array"},
|
|
10
|
+
"constraints":{"type":"array"},
|
|
11
|
+
"objective_sense":{"enum":["minimize","maximize"]}
|
|
12
|
+
}
|
|
13
|
+
}
|
solverpilot/cp/ir.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from types import MappingProxyType
|
|
5
|
+
from typing import Any, Mapping
|
|
6
|
+
import hashlib, json
|
|
7
|
+
|
|
8
|
+
class CPObjectiveSense(str, Enum):
|
|
9
|
+
MINIMIZE='minimize'
|
|
10
|
+
MAXIMIZE='maximize'
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True)
|
|
13
|
+
class CPIntVarIR:
|
|
14
|
+
var_id:int
|
|
15
|
+
name:str
|
|
16
|
+
domain:tuple[int,...]
|
|
17
|
+
kind:str='int'
|
|
18
|
+
derived_from:tuple[int,int] | None=None # (start_var_id, fixed_size)
|
|
19
|
+
def __post_init__(self):
|
|
20
|
+
if not self.domain: raise ValueError('CP variable domain cannot be empty')
|
|
21
|
+
vals=tuple(sorted(set(int(v) for v in self.domain)))
|
|
22
|
+
object.__setattr__(self,'domain',vals)
|
|
23
|
+
if self.kind not in {'int','bool'}: raise ValueError('invalid CP variable kind')
|
|
24
|
+
if self.kind=='bool' and not set(vals).issubset({0,1}): raise ValueError('bool domain must be subset of {0,1}')
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True, slots=True)
|
|
27
|
+
class CPLinearExprIR:
|
|
28
|
+
terms:tuple[tuple[int,int],...]=()
|
|
29
|
+
constant:int=0
|
|
30
|
+
def __post_init__(self):
|
|
31
|
+
merged={}
|
|
32
|
+
for vid,c in self.terms:
|
|
33
|
+
merged[int(vid)]=merged.get(int(vid),0)+int(c)
|
|
34
|
+
object.__setattr__(self,'terms',tuple(sorted((v,c) for v,c in merged.items() if c)))
|
|
35
|
+
object.__setattr__(self,'constant',int(self.constant))
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True, slots=True)
|
|
38
|
+
class CPIntervalIR:
|
|
39
|
+
interval_id:int
|
|
40
|
+
name:str
|
|
41
|
+
start_var:int
|
|
42
|
+
size:int
|
|
43
|
+
end_var:int
|
|
44
|
+
def __post_init__(self):
|
|
45
|
+
if self.size < 0: raise ValueError('interval size must be nonnegative')
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True, slots=True)
|
|
48
|
+
class CPLinearConstraintIR:
|
|
49
|
+
expr:CPLinearExprIR; lower:int|None=None; upper:int|None=None; kind:str='linear'
|
|
50
|
+
@dataclass(frozen=True, slots=True)
|
|
51
|
+
class CPAllDifferentIR:
|
|
52
|
+
var_ids:tuple[int,...]; kind:str='all_different'
|
|
53
|
+
@dataclass(frozen=True, slots=True)
|
|
54
|
+
class CPExactlyOneIR:
|
|
55
|
+
literal_ids:tuple[int,...]; kind:str='exactly_one'
|
|
56
|
+
@dataclass(frozen=True, slots=True)
|
|
57
|
+
class CPTableIR:
|
|
58
|
+
var_ids:tuple[int,...]; allowed_tuples:tuple[tuple[int,...],...]; kind:str='table'
|
|
59
|
+
@dataclass(frozen=True, slots=True)
|
|
60
|
+
class CPElementIR:
|
|
61
|
+
index_var:int; values:tuple[int,...]; target_var:int; kind:str='element'
|
|
62
|
+
@dataclass(frozen=True, slots=True)
|
|
63
|
+
class CPCircuitIR:
|
|
64
|
+
arcs:tuple[tuple[int,int,int],...]; kind:str='circuit' # tail, head, bool var
|
|
65
|
+
@dataclass(frozen=True, slots=True)
|
|
66
|
+
class CPNoOverlapIR:
|
|
67
|
+
interval_ids:tuple[int,...]; kind:str='no_overlap'
|
|
68
|
+
@dataclass(frozen=True, slots=True)
|
|
69
|
+
class CPCumulativeIR:
|
|
70
|
+
interval_ids:tuple[int,...]; demands:tuple[int,...]; capacity:int; kind:str='cumulative'
|
|
71
|
+
|
|
72
|
+
CPConstraintIR = CPLinearConstraintIR|CPAllDifferentIR|CPExactlyOneIR|CPTableIR|CPElementIR|CPCircuitIR|CPNoOverlapIR|CPCumulativeIR
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True, slots=True)
|
|
75
|
+
class CPProblem:
|
|
76
|
+
variables:tuple[CPIntVarIR,...]
|
|
77
|
+
intervals:tuple[CPIntervalIR,...]
|
|
78
|
+
constraints:tuple[CPConstraintIR,...]
|
|
79
|
+
objective:CPLinearExprIR|None=None
|
|
80
|
+
objective_sense:CPObjectiveSense=CPObjectiveSense.MINIMIZE
|
|
81
|
+
name:str|None=None
|
|
82
|
+
metadata:Mapping[str,Any]=field(default_factory=dict)
|
|
83
|
+
schema_version:str='solverpilot.cp-ir.v1'
|
|
84
|
+
def __post_init__(self):
|
|
85
|
+
ids=[v.var_id for v in self.variables]
|
|
86
|
+
if len(ids)!=len(set(ids)): raise ValueError('duplicate CP variable id')
|
|
87
|
+
iids=[i.interval_id for i in self.intervals]
|
|
88
|
+
if len(iids)!=len(set(iids)): raise ValueError('duplicate interval id')
|
|
89
|
+
var_map={v.var_id:v for v in self.variables}
|
|
90
|
+
interval_map={i.interval_id:i for i in self.intervals}
|
|
91
|
+
|
|
92
|
+
def require_var(vid:int, where:str):
|
|
93
|
+
if int(vid) not in var_map:
|
|
94
|
+
raise ValueError(f'{where} references unknown CP variable id {vid}')
|
|
95
|
+
|
|
96
|
+
def require_bool(vid:int, where:str):
|
|
97
|
+
require_var(vid,where)
|
|
98
|
+
v=var_map[int(vid)]
|
|
99
|
+
if v.kind!='bool' or not set(v.domain).issubset({0,1}):
|
|
100
|
+
raise ValueError(f'{where} requires Boolean literal variable id {vid}')
|
|
101
|
+
|
|
102
|
+
def require_expr(expr:CPLinearExprIR, where:str):
|
|
103
|
+
for vid,_ in expr.terms: require_var(vid,where)
|
|
104
|
+
|
|
105
|
+
for v in self.variables:
|
|
106
|
+
if v.derived_from is not None:
|
|
107
|
+
require_var(v.derived_from[0],f'variable {v.var_id} derived_from')
|
|
108
|
+
for i in self.intervals:
|
|
109
|
+
require_var(i.start_var,f'interval {i.interval_id} start_var')
|
|
110
|
+
require_var(i.end_var,f'interval {i.interval_id} end_var')
|
|
111
|
+
if self.objective is not None: require_expr(self.objective,'objective')
|
|
112
|
+
|
|
113
|
+
for c in self.constraints:
|
|
114
|
+
where=f'{c.kind} constraint'
|
|
115
|
+
if isinstance(c,CPLinearConstraintIR):
|
|
116
|
+
require_expr(c.expr,where)
|
|
117
|
+
elif isinstance(c,CPAllDifferentIR):
|
|
118
|
+
for vid in c.var_ids: require_var(vid,where)
|
|
119
|
+
elif isinstance(c,CPExactlyOneIR):
|
|
120
|
+
if not c.literal_ids: raise ValueError('exactly_one requires at least one literal')
|
|
121
|
+
for vid in c.literal_ids: require_bool(vid,where)
|
|
122
|
+
elif isinstance(c,CPTableIR):
|
|
123
|
+
for vid in c.var_ids: require_var(vid,where)
|
|
124
|
+
if any(len(t)!=len(c.var_ids) for t in c.allowed_tuples):
|
|
125
|
+
raise ValueError('table tuple arity mismatch')
|
|
126
|
+
elif isinstance(c,CPElementIR):
|
|
127
|
+
require_var(c.index_var,where); require_var(c.target_var,where)
|
|
128
|
+
if not c.values: raise ValueError('element values cannot be empty')
|
|
129
|
+
elif isinstance(c,CPCircuitIR):
|
|
130
|
+
for _,_,vid in c.arcs: require_bool(vid,where)
|
|
131
|
+
elif isinstance(c,CPNoOverlapIR):
|
|
132
|
+
for iid in c.interval_ids:
|
|
133
|
+
if iid not in interval_map: raise ValueError(f'{where} references unknown interval id {iid}')
|
|
134
|
+
elif isinstance(c,CPCumulativeIR):
|
|
135
|
+
if len(c.interval_ids)!=len(c.demands): raise ValueError('cumulative demands must align with interval_ids')
|
|
136
|
+
if c.capacity < 0 or any(int(d)<0 for d in c.demands):
|
|
137
|
+
raise ValueError('cumulative demands/capacity must be nonnegative')
|
|
138
|
+
for iid in c.interval_ids:
|
|
139
|
+
if iid not in interval_map: raise ValueError(f'{where} references unknown interval id {iid}')
|
|
140
|
+
object.__setattr__(self,'metadata',MappingProxyType(dict(self.metadata)))
|
|
141
|
+
@property
|
|
142
|
+
def variable_map(self): return {v.var_id:v for v in self.variables}
|
|
143
|
+
@property
|
|
144
|
+
def interval_map(self): return {i.interval_id:i for i in self.intervals}
|
|
145
|
+
@property
|
|
146
|
+
def base_variables(self): return tuple(v for v in self.variables if v.derived_from is None)
|
|
147
|
+
def canonical_dict(self):
|
|
148
|
+
def expr(e): return {'terms':[list(x) for x in e.terms],'constant':e.constant}
|
|
149
|
+
cs=[]
|
|
150
|
+
for c in self.constraints:
|
|
151
|
+
d={'kind':c.kind}
|
|
152
|
+
if isinstance(c,CPLinearConstraintIR): d.update(expr=expr(c.expr),lower=c.lower,upper=c.upper)
|
|
153
|
+
elif isinstance(c,CPAllDifferentIR): d['var_ids']=list(c.var_ids)
|
|
154
|
+
elif isinstance(c,CPExactlyOneIR): d['literal_ids']=list(c.literal_ids)
|
|
155
|
+
elif isinstance(c,CPTableIR): d.update(var_ids=list(c.var_ids),allowed_tuples=[list(t) for t in c.allowed_tuples])
|
|
156
|
+
elif isinstance(c,CPElementIR): d.update(index_var=c.index_var,values=list(c.values),target_var=c.target_var)
|
|
157
|
+
elif isinstance(c,CPCircuitIR): d['arcs']=[list(a) for a in c.arcs]
|
|
158
|
+
elif isinstance(c,CPNoOverlapIR): d['interval_ids']=list(c.interval_ids)
|
|
159
|
+
elif isinstance(c,CPCumulativeIR): d.update(interval_ids=list(c.interval_ids),demands=list(c.demands),capacity=c.capacity)
|
|
160
|
+
cs.append(d)
|
|
161
|
+
return {'schema_version':self.schema_version,'name':self.name,'variables':[{'id':v.var_id,'name':v.name,'domain':list(v.domain),'kind':v.kind,'derived_from':None if v.derived_from is None else list(v.derived_from)} for v in self.variables],'intervals':[{'id':i.interval_id,'name':i.name,'start_var':i.start_var,'size':i.size,'end_var':i.end_var} for i in self.intervals],'constraints':cs,'objective':None if self.objective is None else expr(self.objective),'objective_sense':self.objective_sense.value,'metadata':dict(self.metadata)}
|
|
162
|
+
@property
|
|
163
|
+
def structural_hash(self):
|
|
164
|
+
d=self.canonical_dict();
|
|
165
|
+
for v in d['variables']: v['domain']=['<domain>']
|
|
166
|
+
d['metadata']={}
|
|
167
|
+
return hashlib.sha256(json.dumps(d,sort_keys=True,separators=(',',':')).encode()).hexdigest()
|
|
168
|
+
@property
|
|
169
|
+
def data_hash(self):
|
|
170
|
+
return hashlib.sha256(json.dumps(self.canonical_dict(),sort_keys=True,separators=(',',':')).encode()).hexdigest()
|