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,463 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from scipy import sparse
|
|
8
|
+
from scipy.optimize import Bounds, LinearConstraint, milp
|
|
9
|
+
|
|
10
|
+
from solverpilot.problem import LinearProblem, VariableDomain
|
|
11
|
+
|
|
12
|
+
from .model import (
|
|
13
|
+
ConflictAtom,
|
|
14
|
+
ConflictSetResult,
|
|
15
|
+
DiagnosticIssue,
|
|
16
|
+
ElasticRelaxationResult,
|
|
17
|
+
EvidenceKind,
|
|
18
|
+
InfeasibilityReport,
|
|
19
|
+
NormalizedIISEvidence,
|
|
20
|
+
RelaxationViolation,
|
|
21
|
+
ViolationKind,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def find_static_infeasibility(problem: LinearProblem, *, atol: float = 1e-12) -> tuple[DiagnosticIssue, ...]:
|
|
26
|
+
"""Find contradictions that require no optimization solve.
|
|
27
|
+
|
|
28
|
+
The checks are deliberately proof-producing: every returned issue is independently
|
|
29
|
+
sufficient to prove infeasibility. Heuristic numerical warnings belong elsewhere.
|
|
30
|
+
"""
|
|
31
|
+
issues: list[DiagnosticIssue] = []
|
|
32
|
+
|
|
33
|
+
# A zero row has activity exactly zero. If zero is outside the ranged row bounds,
|
|
34
|
+
# that row alone proves infeasibility.
|
|
35
|
+
row_nnz = np.diff(problem.A.indptr)
|
|
36
|
+
for i in np.flatnonzero(row_nnz == 0):
|
|
37
|
+
lo = float(problem.constraint_lower[i])
|
|
38
|
+
hi = float(problem.constraint_upper[i])
|
|
39
|
+
if lo > atol or hi < -atol:
|
|
40
|
+
distance = lo if lo > 0 else -hi
|
|
41
|
+
issues.append(
|
|
42
|
+
DiagnosticIssue(
|
|
43
|
+
kind=ViolationKind.EMPTY_ROW,
|
|
44
|
+
index=int(i),
|
|
45
|
+
amount=float(distance),
|
|
46
|
+
message=(
|
|
47
|
+
f"constraint row {int(i)} has no nonzero coefficients, so its activity is 0, "
|
|
48
|
+
f"outside [{lo}, {hi}]"
|
|
49
|
+
),
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Continuous feasibility of bounds is enforced by LinearProblem. Integer variables
|
|
54
|
+
# have the additional requirement that [lb, ub] contains at least one integer.
|
|
55
|
+
for j, domain in enumerate(problem.domains):
|
|
56
|
+
if domain not in {VariableDomain.INTEGER.value, VariableDomain.BINARY.value}:
|
|
57
|
+
continue
|
|
58
|
+
lo = float(problem.variable_lower[j])
|
|
59
|
+
hi = float(problem.variable_upper[j])
|
|
60
|
+
if not np.isfinite(lo) or not np.isfinite(hi):
|
|
61
|
+
continue
|
|
62
|
+
ceil_lo = np.ceil(lo - atol)
|
|
63
|
+
floor_hi = np.floor(hi + atol)
|
|
64
|
+
if ceil_lo > floor_hi:
|
|
65
|
+
issues.append(
|
|
66
|
+
DiagnosticIssue(
|
|
67
|
+
kind=ViolationKind.INTEGER_DOMAIN,
|
|
68
|
+
index=j,
|
|
69
|
+
amount=float(ceil_lo - floor_hi),
|
|
70
|
+
message=(
|
|
71
|
+
f"integer variable {j} has bounds [{lo}, {hi}] containing no integer value"
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return tuple(issues)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True, slots=True)
|
|
80
|
+
class _SlackSource:
|
|
81
|
+
kind: ViolationKind
|
|
82
|
+
index: int
|
|
83
|
+
bound_value: float
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def elastic_relaxation(
|
|
87
|
+
problem: LinearProblem,
|
|
88
|
+
*,
|
|
89
|
+
time_limit_s: float | None = None,
|
|
90
|
+
relative_weighting: bool = True,
|
|
91
|
+
violation_tol: float = 1e-9,
|
|
92
|
+
) -> ElasticRelaxationResult:
|
|
93
|
+
"""Solve a phase-I elastic model and report which bounds must move.
|
|
94
|
+
|
|
95
|
+
Every finite row and variable bound gets its own nonnegative slack. Original
|
|
96
|
+
integrality is preserved. This is *not* an IIS: it finds one minimum-weight
|
|
97
|
+
relaxation, which may contain redundant violations and may not be unique.
|
|
98
|
+
"""
|
|
99
|
+
n = problem.n_variables
|
|
100
|
+
sources: list[_SlackSource] = []
|
|
101
|
+
rows: list[sparse.csr_matrix] = []
|
|
102
|
+
lower: list[float] = []
|
|
103
|
+
upper: list[float] = []
|
|
104
|
+
|
|
105
|
+
# We build one inequality per finite side. Slack columns are appended after x.
|
|
106
|
+
row_specs: list[tuple[sparse.csr_matrix, float, float, _SlackSource, float]] = []
|
|
107
|
+
A = problem.A.tocsr()
|
|
108
|
+
for i in range(problem.n_constraints):
|
|
109
|
+
arow = A.getrow(i)
|
|
110
|
+
lo = float(problem.constraint_lower[i])
|
|
111
|
+
hi = float(problem.constraint_upper[i])
|
|
112
|
+
if np.isfinite(lo):
|
|
113
|
+
source = _SlackSource(ViolationKind.ROW_LOWER, i, lo)
|
|
114
|
+
row_specs.append((arow, lo, np.inf, source, +1.0))
|
|
115
|
+
if np.isfinite(hi):
|
|
116
|
+
source = _SlackSource(ViolationKind.ROW_UPPER, i, hi)
|
|
117
|
+
row_specs.append((arow, -np.inf, hi, source, -1.0))
|
|
118
|
+
|
|
119
|
+
eye = sparse.eye(n, format="csr")
|
|
120
|
+
for j in range(n):
|
|
121
|
+
lo = float(problem.variable_lower[j])
|
|
122
|
+
hi = float(problem.variable_upper[j])
|
|
123
|
+
erow = eye.getrow(j)
|
|
124
|
+
if np.isfinite(lo):
|
|
125
|
+
source = _SlackSource(ViolationKind.VARIABLE_LOWER, j, lo)
|
|
126
|
+
row_specs.append((erow, lo, np.inf, source, +1.0))
|
|
127
|
+
if np.isfinite(hi):
|
|
128
|
+
source = _SlackSource(ViolationKind.VARIABLE_UPPER, j, hi)
|
|
129
|
+
row_specs.append((erow, -np.inf, hi, source, -1.0))
|
|
130
|
+
|
|
131
|
+
k = len(row_specs)
|
|
132
|
+
if k == 0:
|
|
133
|
+
# No finite bounds means the model cannot be infeasible from linear constraints.
|
|
134
|
+
return ElasticRelaxationResult(
|
|
135
|
+
feasible=True,
|
|
136
|
+
objective=0.0,
|
|
137
|
+
x=np.zeros(n, dtype=np.float64),
|
|
138
|
+
violations=(),
|
|
139
|
+
solver_status="trivial",
|
|
140
|
+
message="no finite row or variable bounds required relaxation",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Sparse [original variables | one private slack per bound side].
|
|
144
|
+
pieces: list[sparse.csr_matrix] = []
|
|
145
|
+
for slack_idx, (base, lo, hi, source, slack_sign) in enumerate(row_specs):
|
|
146
|
+
slack = sparse.csr_matrix(
|
|
147
|
+
([slack_sign], ([0], [slack_idx])), shape=(1, k), dtype=np.float64
|
|
148
|
+
)
|
|
149
|
+
pieces.append(sparse.hstack([base, slack], format="csr"))
|
|
150
|
+
lower.append(lo)
|
|
151
|
+
upper.append(hi)
|
|
152
|
+
sources.append(source)
|
|
153
|
+
|
|
154
|
+
M = sparse.vstack(pieces, format="csr")
|
|
155
|
+
|
|
156
|
+
weights = np.ones(k, dtype=np.float64)
|
|
157
|
+
if relative_weighting:
|
|
158
|
+
weights = np.asarray(
|
|
159
|
+
[1.0 / max(1.0, abs(src.bound_value)) for src in sources], dtype=np.float64
|
|
160
|
+
)
|
|
161
|
+
objective = np.concatenate([np.zeros(n, dtype=np.float64), weights])
|
|
162
|
+
|
|
163
|
+
integrality = np.zeros(n + k, dtype=np.int32)
|
|
164
|
+
for j, domain in enumerate(problem.domains):
|
|
165
|
+
if domain in {VariableDomain.INTEGER.value, VariableDomain.BINARY.value}:
|
|
166
|
+
integrality[j] = 1
|
|
167
|
+
|
|
168
|
+
# Original variable bounds are represented as elastic rows above, so x itself is free.
|
|
169
|
+
lb = np.concatenate([np.full(n, -np.inf), np.zeros(k)])
|
|
170
|
+
ub = np.full(n + k, np.inf)
|
|
171
|
+
options: dict[str, Any] = {"disp": False}
|
|
172
|
+
if time_limit_s is not None:
|
|
173
|
+
if time_limit_s <= 0:
|
|
174
|
+
raise ValueError("time_limit_s must be positive")
|
|
175
|
+
options["time_limit"] = float(time_limit_s)
|
|
176
|
+
|
|
177
|
+
result = milp(
|
|
178
|
+
c=objective,
|
|
179
|
+
integrality=integrality,
|
|
180
|
+
bounds=Bounds(lb, ub),
|
|
181
|
+
constraints=LinearConstraint(M, np.asarray(lower), np.asarray(upper)),
|
|
182
|
+
options=options,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
if result.x is None or not bool(result.success):
|
|
186
|
+
return ElasticRelaxationResult(
|
|
187
|
+
feasible=False,
|
|
188
|
+
objective=None,
|
|
189
|
+
x=None,
|
|
190
|
+
violations=(),
|
|
191
|
+
solver_status=str(getattr(result, "status", "unknown")),
|
|
192
|
+
message=str(getattr(result, "message", "elastic relaxation failed")),
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
z = np.asarray(result.x, dtype=np.float64)
|
|
196
|
+
x = z[:n].copy()
|
|
197
|
+
slack_values = z[n:]
|
|
198
|
+
violations: list[RelaxationViolation] = []
|
|
199
|
+
for idx, amount in enumerate(slack_values):
|
|
200
|
+
raw = float(max(0.0, amount))
|
|
201
|
+
if raw <= violation_tol:
|
|
202
|
+
continue
|
|
203
|
+
src = sources[idx]
|
|
204
|
+
violations.append(
|
|
205
|
+
RelaxationViolation(
|
|
206
|
+
kind=src.kind,
|
|
207
|
+
index=src.index,
|
|
208
|
+
amount=raw,
|
|
209
|
+
normalized_amount=float(raw * weights[idx]),
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
violations.sort(key=lambda v: (-v.normalized_amount, v.kind.value, v.index))
|
|
213
|
+
return ElasticRelaxationResult(
|
|
214
|
+
feasible=True,
|
|
215
|
+
objective=float(result.fun),
|
|
216
|
+
x=x,
|
|
217
|
+
violations=tuple(violations),
|
|
218
|
+
solver_status=str(getattr(result, "status", "0")),
|
|
219
|
+
message=str(getattr(result, "message", "")),
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _all_bound_atoms(problem: LinearProblem) -> list[ConflictAtom]:
|
|
225
|
+
atoms: list[ConflictAtom] = []
|
|
226
|
+
for i in range(problem.n_constraints):
|
|
227
|
+
if np.isfinite(problem.constraint_lower[i]):
|
|
228
|
+
atoms.append(ConflictAtom(ViolationKind.ROW_LOWER, i))
|
|
229
|
+
if np.isfinite(problem.constraint_upper[i]):
|
|
230
|
+
atoms.append(ConflictAtom(ViolationKind.ROW_UPPER, i))
|
|
231
|
+
for j in range(problem.n_variables):
|
|
232
|
+
if np.isfinite(problem.variable_lower[j]):
|
|
233
|
+
atoms.append(ConflictAtom(ViolationKind.VARIABLE_LOWER, j))
|
|
234
|
+
if np.isfinite(problem.variable_upper[j]):
|
|
235
|
+
atoms.append(ConflictAtom(ViolationKind.VARIABLE_UPPER, j))
|
|
236
|
+
return atoms
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _feasibility_status_for_atoms(
|
|
240
|
+
problem: LinearProblem,
|
|
241
|
+
atoms: list[ConflictAtom],
|
|
242
|
+
*,
|
|
243
|
+
time_limit_s: float | None,
|
|
244
|
+
) -> str:
|
|
245
|
+
n = problem.n_variables
|
|
246
|
+
rows: list[sparse.csr_matrix] = []
|
|
247
|
+
lo: list[float] = []
|
|
248
|
+
hi: list[float] = []
|
|
249
|
+
A = problem.A.tocsr()
|
|
250
|
+
eye = sparse.eye(n, format="csr")
|
|
251
|
+
for atom in atoms:
|
|
252
|
+
if atom.kind is ViolationKind.ROW_LOWER:
|
|
253
|
+
rows.append(A.getrow(atom.index)); lo.append(float(problem.constraint_lower[atom.index])); hi.append(np.inf)
|
|
254
|
+
elif atom.kind is ViolationKind.ROW_UPPER:
|
|
255
|
+
rows.append(A.getrow(atom.index)); lo.append(-np.inf); hi.append(float(problem.constraint_upper[atom.index]))
|
|
256
|
+
elif atom.kind is ViolationKind.VARIABLE_LOWER:
|
|
257
|
+
rows.append(eye.getrow(atom.index)); lo.append(float(problem.variable_lower[atom.index])); hi.append(np.inf)
|
|
258
|
+
elif atom.kind is ViolationKind.VARIABLE_UPPER:
|
|
259
|
+
rows.append(eye.getrow(atom.index)); lo.append(-np.inf); hi.append(float(problem.variable_upper[atom.index]))
|
|
260
|
+
else:
|
|
261
|
+
raise ValueError(f"unsupported conflict atom kind: {atom.kind}")
|
|
262
|
+
|
|
263
|
+
constraints = ()
|
|
264
|
+
if rows:
|
|
265
|
+
M = sparse.vstack(rows, format="csr")
|
|
266
|
+
constraints = LinearConstraint(M, np.asarray(lo), np.asarray(hi))
|
|
267
|
+
|
|
268
|
+
integrality = np.zeros(n, dtype=np.int32)
|
|
269
|
+
for j, domain in enumerate(problem.domains):
|
|
270
|
+
if domain in {VariableDomain.INTEGER.value, VariableDomain.BINARY.value}:
|
|
271
|
+
integrality[j] = 1
|
|
272
|
+
|
|
273
|
+
options: dict[str, Any] = {"disp": False}
|
|
274
|
+
if time_limit_s is not None:
|
|
275
|
+
options["time_limit"] = float(time_limit_s)
|
|
276
|
+
# Variable bounds are represented only through selected atoms. Keep x otherwise free.
|
|
277
|
+
result = milp(
|
|
278
|
+
c=np.zeros(n, dtype=np.float64),
|
|
279
|
+
integrality=integrality,
|
|
280
|
+
bounds=Bounds(np.full(n, -np.inf), np.full(n, np.inf)),
|
|
281
|
+
constraints=constraints,
|
|
282
|
+
options=options,
|
|
283
|
+
)
|
|
284
|
+
status = int(getattr(result, "status", -1))
|
|
285
|
+
if status == 2:
|
|
286
|
+
return "infeasible"
|
|
287
|
+
if status == 0:
|
|
288
|
+
return "feasible"
|
|
289
|
+
if status == 1:
|
|
290
|
+
return "limit"
|
|
291
|
+
if status == 3:
|
|
292
|
+
# With zero objective this normally means the feasibility model is feasible but
|
|
293
|
+
# solver termination classified it unbounded; it does not prove infeasibility.
|
|
294
|
+
return "feasible_or_unbounded"
|
|
295
|
+
return "error"
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def deletion_filter_conflict(
|
|
299
|
+
problem: LinearProblem,
|
|
300
|
+
*,
|
|
301
|
+
max_checks: int = 250,
|
|
302
|
+
time_limit_s_per_check: float | None = None,
|
|
303
|
+
) -> ConflictSetResult:
|
|
304
|
+
"""Find an irreducible set of row/variable bound sides by deletion filtering.
|
|
305
|
+
|
|
306
|
+
This is a solver-agnostic fallback built on SciPy/HiGHS feasibility solves. If the
|
|
307
|
+
check budget is exhausted, ``complete`` and ``irreducible`` are false; callers must
|
|
308
|
+
not present the partial set as an IIS.
|
|
309
|
+
"""
|
|
310
|
+
if max_checks <= 0:
|
|
311
|
+
raise ValueError("max_checks must be positive")
|
|
312
|
+
if time_limit_s_per_check is not None and time_limit_s_per_check <= 0:
|
|
313
|
+
raise ValueError("time_limit_s_per_check must be positive")
|
|
314
|
+
|
|
315
|
+
active = _all_bound_atoms(problem)
|
|
316
|
+
checks = 1
|
|
317
|
+
initial = _feasibility_status_for_atoms(problem, active, time_limit_s=time_limit_s_per_check)
|
|
318
|
+
if initial != "infeasible":
|
|
319
|
+
return ConflictSetResult(
|
|
320
|
+
atoms=(), irreducible=False, complete=True, checks=checks,
|
|
321
|
+
solver_status=initial,
|
|
322
|
+
message="full bound-side model was not proven infeasible",
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
pos = 0
|
|
326
|
+
while pos < len(active):
|
|
327
|
+
if checks >= max_checks:
|
|
328
|
+
return ConflictSetResult(
|
|
329
|
+
atoms=tuple(active), irreducible=False, complete=False, checks=checks,
|
|
330
|
+
solver_status="check_limit",
|
|
331
|
+
message="deletion-filter check budget exhausted before irreducibility proof",
|
|
332
|
+
)
|
|
333
|
+
trial = active[:pos] + active[pos + 1 :]
|
|
334
|
+
status = _feasibility_status_for_atoms(problem, trial, time_limit_s=time_limit_s_per_check)
|
|
335
|
+
checks += 1
|
|
336
|
+
if status == "infeasible":
|
|
337
|
+
# Atom was unnecessary; keep it deleted and test the new atom at this position.
|
|
338
|
+
active = trial
|
|
339
|
+
continue
|
|
340
|
+
if status == "limit":
|
|
341
|
+
return ConflictSetResult(
|
|
342
|
+
atoms=tuple(active), irreducible=False, complete=False, checks=checks,
|
|
343
|
+
solver_status="time_limit",
|
|
344
|
+
message="a feasibility subcheck hit its time limit",
|
|
345
|
+
)
|
|
346
|
+
if status == "error":
|
|
347
|
+
return ConflictSetResult(
|
|
348
|
+
atoms=tuple(active), irreducible=False, complete=False, checks=checks,
|
|
349
|
+
solver_status="solver_error",
|
|
350
|
+
message="a feasibility subcheck failed",
|
|
351
|
+
)
|
|
352
|
+
# Removing this atom made the set feasible, so it is necessary. By monotonicity
|
|
353
|
+
# it will remain necessary after later deletions.
|
|
354
|
+
pos += 1
|
|
355
|
+
|
|
356
|
+
return ConflictSetResult(
|
|
357
|
+
atoms=tuple(active), irreducible=True, complete=True, checks=checks,
|
|
358
|
+
solver_status="infeasible",
|
|
359
|
+
message="every retained atom is necessary for infeasibility",
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
def _normalize_native_iis(backend: Any, native: Any) -> NormalizedIISEvidence:
|
|
363
|
+
backend_name = getattr(getattr(backend, "manifest", None), "name", type(backend).__name__)
|
|
364
|
+
# HiGHS development/public-style result.
|
|
365
|
+
if hasattr(native, "row_indices") and hasattr(native, "col_indices"):
|
|
366
|
+
return NormalizedIISEvidence(
|
|
367
|
+
backend=str(backend_name),
|
|
368
|
+
valid=bool(getattr(native, "valid", True)),
|
|
369
|
+
row_indices=tuple(int(x) for x in getattr(native, "row_indices", ())),
|
|
370
|
+
variable_indices=tuple(int(x) for x in getattr(native, "col_indices", ())),
|
|
371
|
+
raw_type=type(native).__name__,
|
|
372
|
+
)
|
|
373
|
+
# SCIP-style normalized result.
|
|
374
|
+
if hasattr(native, "constraint_names") and hasattr(native, "variable_names"):
|
|
375
|
+
return NormalizedIISEvidence(
|
|
376
|
+
backend=str(backend_name),
|
|
377
|
+
valid=True,
|
|
378
|
+
constraint_names=tuple(str(x) for x in getattr(native, "constraint_names", ())),
|
|
379
|
+
variable_names=tuple(str(x) for x in getattr(native, "variable_names", ())),
|
|
380
|
+
irreducible=bool(getattr(native, "irreducible", True)),
|
|
381
|
+
raw_type=type(native).__name__,
|
|
382
|
+
)
|
|
383
|
+
raise TypeError(f"unsupported native IIS evidence type: {type(native)!r}")
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _try_native_iis(problem: LinearProblem, backend: Any) -> NormalizedIISEvidence | None:
|
|
387
|
+
if hasattr(backend, "compute_iis"):
|
|
388
|
+
return _normalize_native_iis(backend, backend.compute_iis(problem))
|
|
389
|
+
if hasattr(backend, "generate_iis"):
|
|
390
|
+
return _normalize_native_iis(backend, backend.generate_iis(problem))
|
|
391
|
+
return None
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def diagnose_infeasibility(
|
|
395
|
+
problem: LinearProblem,
|
|
396
|
+
*,
|
|
397
|
+
backend: Any | None = None,
|
|
398
|
+
include_elastic: bool = True,
|
|
399
|
+
include_conflict: bool = False,
|
|
400
|
+
conflict_max_checks: int = 250,
|
|
401
|
+
time_limit_s: float | None = None,
|
|
402
|
+
) -> InfeasibilityReport:
|
|
403
|
+
"""Build typed infeasibility evidence without conflating IIS and relaxation.
|
|
404
|
+
|
|
405
|
+
The function does not infer infeasibility merely because an elastic model has a
|
|
406
|
+
positive objective; callers should normally invoke it after a solver has classified
|
|
407
|
+
the original model infeasible. Static contradictions *do* independently prove it.
|
|
408
|
+
"""
|
|
409
|
+
static = find_static_infeasibility(problem)
|
|
410
|
+
order: list[EvidenceKind] = []
|
|
411
|
+
warnings: list[str] = []
|
|
412
|
+
if static:
|
|
413
|
+
order.append(EvidenceKind.STATIC_CONTRADICTION)
|
|
414
|
+
|
|
415
|
+
iis = None
|
|
416
|
+
if backend is not None:
|
|
417
|
+
try:
|
|
418
|
+
iis = _try_native_iis(problem, backend)
|
|
419
|
+
if iis is not None:
|
|
420
|
+
order.append(EvidenceKind.NATIVE_IIS)
|
|
421
|
+
except (NotImplementedError, ValueError) as exc:
|
|
422
|
+
warnings.append(f"native IIS unavailable for this model: {exc}")
|
|
423
|
+
except Exception as exc: # diagnostic failure must not corrupt the solve result
|
|
424
|
+
warnings.append(f"native IIS failed: {type(exc).__name__}: {exc}")
|
|
425
|
+
|
|
426
|
+
elastic = None
|
|
427
|
+
if include_elastic:
|
|
428
|
+
try:
|
|
429
|
+
elastic = elastic_relaxation(problem, time_limit_s=time_limit_s)
|
|
430
|
+
order.append(EvidenceKind.ELASTIC_RELAXATION)
|
|
431
|
+
except Exception as exc:
|
|
432
|
+
warnings.append(f"elastic relaxation failed: {type(exc).__name__}: {exc}")
|
|
433
|
+
|
|
434
|
+
conflict = None
|
|
435
|
+
if include_conflict:
|
|
436
|
+
try:
|
|
437
|
+
conflict = deletion_filter_conflict(
|
|
438
|
+
problem,
|
|
439
|
+
max_checks=conflict_max_checks,
|
|
440
|
+
time_limit_s_per_check=time_limit_s,
|
|
441
|
+
)
|
|
442
|
+
order.append(EvidenceKind.DELETION_FILTER)
|
|
443
|
+
except Exception as exc:
|
|
444
|
+
warnings.append(f"deletion filter failed: {type(exc).__name__}: {exc}")
|
|
445
|
+
|
|
446
|
+
elastic_proves_infeasible = bool(
|
|
447
|
+
elastic is not None
|
|
448
|
+
and elastic.feasible
|
|
449
|
+
and elastic.objective is not None
|
|
450
|
+
and elastic.objective > 1e-9
|
|
451
|
+
)
|
|
452
|
+
iis_proves_infeasible = bool(iis is not None and iis.valid)
|
|
453
|
+
conflict_proves_infeasible = bool(conflict is not None and conflict.complete and conflict.irreducible)
|
|
454
|
+
confirmed = bool(static) or iis_proves_infeasible or elastic_proves_infeasible or conflict_proves_infeasible
|
|
455
|
+
return InfeasibilityReport(
|
|
456
|
+
confirmed_infeasible=confirmed,
|
|
457
|
+
static_issues=static,
|
|
458
|
+
iis=iis,
|
|
459
|
+
elastic=elastic,
|
|
460
|
+
conflict=conflict,
|
|
461
|
+
evidence_order=tuple(order),
|
|
462
|
+
warnings=tuple(warnings),
|
|
463
|
+
)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class EvidenceKind(str, Enum):
|
|
10
|
+
STATIC_CONTRADICTION = "static_contradiction"
|
|
11
|
+
NATIVE_IIS = "native_iis"
|
|
12
|
+
ELASTIC_RELAXATION = "elastic_relaxation"
|
|
13
|
+
DELETION_FILTER = "deletion_filter"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ViolationKind(str, Enum):
|
|
17
|
+
ROW_LOWER = "row_lower"
|
|
18
|
+
ROW_UPPER = "row_upper"
|
|
19
|
+
VARIABLE_LOWER = "variable_lower"
|
|
20
|
+
VARIABLE_UPPER = "variable_upper"
|
|
21
|
+
INTEGER_DOMAIN = "integer_domain"
|
|
22
|
+
EMPTY_ROW = "empty_row"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True, slots=True)
|
|
26
|
+
class DiagnosticIssue:
|
|
27
|
+
kind: ViolationKind
|
|
28
|
+
index: int
|
|
29
|
+
message: str
|
|
30
|
+
amount: float | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True, slots=True)
|
|
34
|
+
class RelaxationViolation:
|
|
35
|
+
kind: ViolationKind
|
|
36
|
+
index: int
|
|
37
|
+
amount: float
|
|
38
|
+
normalized_amount: float
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True, slots=True)
|
|
42
|
+
class ElasticRelaxationResult:
|
|
43
|
+
feasible: bool
|
|
44
|
+
objective: float | None
|
|
45
|
+
x: np.ndarray | None
|
|
46
|
+
violations: tuple[RelaxationViolation, ...]
|
|
47
|
+
solver_status: str
|
|
48
|
+
message: str
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True, slots=True)
|
|
52
|
+
class NormalizedIISEvidence:
|
|
53
|
+
backend: str
|
|
54
|
+
valid: bool
|
|
55
|
+
row_indices: tuple[int, ...] = ()
|
|
56
|
+
variable_indices: tuple[int, ...] = ()
|
|
57
|
+
constraint_names: tuple[str, ...] = ()
|
|
58
|
+
variable_names: tuple[str, ...] = ()
|
|
59
|
+
irreducible: bool | None = None
|
|
60
|
+
raw_type: str | None = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True, slots=True)
|
|
64
|
+
class ConflictAtom:
|
|
65
|
+
kind: ViolationKind
|
|
66
|
+
index: int
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True, slots=True)
|
|
70
|
+
class ConflictSetResult:
|
|
71
|
+
atoms: tuple[ConflictAtom, ...]
|
|
72
|
+
irreducible: bool
|
|
73
|
+
complete: bool
|
|
74
|
+
checks: int
|
|
75
|
+
solver_status: str
|
|
76
|
+
message: str
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True, slots=True)
|
|
80
|
+
class InfeasibilityReport:
|
|
81
|
+
confirmed_infeasible: bool
|
|
82
|
+
static_issues: tuple[DiagnosticIssue, ...]
|
|
83
|
+
iis: NormalizedIISEvidence | None
|
|
84
|
+
elastic: ElasticRelaxationResult | None
|
|
85
|
+
conflict: ConflictSetResult | None
|
|
86
|
+
evidence_order: tuple[EvidenceKind, ...]
|
|
87
|
+
warnings: tuple[str, ...] = ()
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from .metrics import PortfolioMetrics, performance_profile, portfolio_metrics
|
|
2
|
+
from .references import ObjectiveReference, parse_miplib_solu, parse_test_manifest
|
|
3
|
+
from .oracle import (
|
|
4
|
+
CandidateExclusion,
|
|
5
|
+
CandidateExclusionReason,
|
|
6
|
+
ObjectiveSense,
|
|
7
|
+
OracleCandidate,
|
|
8
|
+
OracleEntry,
|
|
9
|
+
OraclePolicy,
|
|
10
|
+
OracleStatus,
|
|
11
|
+
OracleTable,
|
|
12
|
+
build_oracle_table,
|
|
13
|
+
)
|
|
14
|
+
from .regret import (
|
|
15
|
+
MetricSummary,
|
|
16
|
+
RegretStatus,
|
|
17
|
+
RegretVector,
|
|
18
|
+
SelectionDecision,
|
|
19
|
+
SelectorRegretSummary,
|
|
20
|
+
evaluate_selection_decisions,
|
|
21
|
+
summarize_regret_vectors,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"PortfolioMetrics",
|
|
26
|
+
"performance_profile",
|
|
27
|
+
"portfolio_metrics",
|
|
28
|
+
"ObjectiveReference",
|
|
29
|
+
"parse_miplib_solu",
|
|
30
|
+
"parse_test_manifest",
|
|
31
|
+
"CandidateExclusion",
|
|
32
|
+
"CandidateExclusionReason",
|
|
33
|
+
"ObjectiveSense",
|
|
34
|
+
"OracleCandidate",
|
|
35
|
+
"OracleEntry",
|
|
36
|
+
"OraclePolicy",
|
|
37
|
+
"OracleStatus",
|
|
38
|
+
"OracleTable",
|
|
39
|
+
"build_oracle_table",
|
|
40
|
+
"MetricSummary",
|
|
41
|
+
"RegretStatus",
|
|
42
|
+
"RegretVector",
|
|
43
|
+
"SelectionDecision",
|
|
44
|
+
"SelectorRegretSummary",
|
|
45
|
+
"evaluate_selection_decisions",
|
|
46
|
+
"summarize_regret_vectors",
|
|
47
|
+
"ProofAssessment",
|
|
48
|
+
"ProofLevel",
|
|
49
|
+
"assess_benchmark_row",
|
|
50
|
+
"assess_solve_result",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
from .proof import ProofAssessment, ProofLevel, assess_benchmark_row, assess_solve_result
|