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,199 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
import math
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from scipy import sparse
|
|
8
|
+
|
|
9
|
+
from solverpilot.problem import ConvexityStatus, LinearProblem, QuadraticProblem, VariableDomain
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True)
|
|
13
|
+
class DistributionStats:
|
|
14
|
+
minimum: float
|
|
15
|
+
q25: float
|
|
16
|
+
median: float
|
|
17
|
+
mean: float
|
|
18
|
+
q75: float
|
|
19
|
+
maximum: float
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True, slots=True)
|
|
23
|
+
class ProblemFingerprint:
|
|
24
|
+
problem_class: str
|
|
25
|
+
n_variables: int
|
|
26
|
+
n_constraints: int
|
|
27
|
+
nnz_a: int
|
|
28
|
+
density_a: float
|
|
29
|
+
binary_fraction: float
|
|
30
|
+
integer_fraction: float
|
|
31
|
+
equality_fraction: float
|
|
32
|
+
fully_bounded_variable_fraction: float
|
|
33
|
+
free_variable_fraction: float
|
|
34
|
+
objective_nnz_fraction: float
|
|
35
|
+
coefficient_dynamic_range_log10: float | None
|
|
36
|
+
objective_dynamic_range_log10: float | None
|
|
37
|
+
rhs_dynamic_range_log10: float | None
|
|
38
|
+
row_nnz: DistributionStats
|
|
39
|
+
col_nnz: DistributionStats
|
|
40
|
+
singleton_rows: int
|
|
41
|
+
singleton_cols: int
|
|
42
|
+
positive_coefficient_fraction: float
|
|
43
|
+
negative_coefficient_fraction: float
|
|
44
|
+
quadratic_nnz: int | None
|
|
45
|
+
quadratic_density: float | None
|
|
46
|
+
convexity_status: str | None
|
|
47
|
+
structural_hash: str
|
|
48
|
+
estimated_numeric_bytes: int
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _distribution(values: np.ndarray) -> DistributionStats:
|
|
52
|
+
values = np.asarray(values, dtype=np.float64)
|
|
53
|
+
if values.size == 0:
|
|
54
|
+
return DistributionStats(0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
|
|
55
|
+
q25, med, q75 = np.quantile(values, [0.25, 0.5, 0.75])
|
|
56
|
+
return DistributionStats(
|
|
57
|
+
minimum=float(np.min(values)),
|
|
58
|
+
q25=float(q25),
|
|
59
|
+
median=float(med),
|
|
60
|
+
mean=float(np.mean(values)),
|
|
61
|
+
q75=float(q75),
|
|
62
|
+
maximum=float(np.max(values)),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _dynamic_range_log10(values: np.ndarray) -> float | None:
|
|
67
|
+
values = np.asarray(values, dtype=np.float64)
|
|
68
|
+
finite_nonzero = np.abs(values[np.isfinite(values) & (values != 0.0)])
|
|
69
|
+
if finite_nonzero.size == 0:
|
|
70
|
+
return None
|
|
71
|
+
lo = float(np.min(finite_nonzero))
|
|
72
|
+
hi = float(np.max(finite_nonzero))
|
|
73
|
+
if lo <= 0.0 or hi <= 0.0:
|
|
74
|
+
return None
|
|
75
|
+
return float(math.log10(hi / lo))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _estimated_bytes_linear(problem: LinearProblem) -> int:
|
|
79
|
+
A = problem.A
|
|
80
|
+
sparse_bytes = int(A.data.nbytes + A.indices.nbytes + A.indptr.nbytes)
|
|
81
|
+
dense_bytes = sum(
|
|
82
|
+
int(arr.nbytes)
|
|
83
|
+
for arr in (
|
|
84
|
+
problem.c,
|
|
85
|
+
problem.variable_lower,
|
|
86
|
+
problem.variable_upper,
|
|
87
|
+
problem.constraint_lower,
|
|
88
|
+
problem.constraint_upper,
|
|
89
|
+
problem.domains,
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
return sparse_bytes + dense_bytes
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def inspect_problem(problem: LinearProblem | QuadraticProblem) -> ProblemFingerprint:
|
|
96
|
+
if isinstance(problem, QuadraticProblem):
|
|
97
|
+
if problem.convexity_status is not ConvexityStatus.CONFIRMED:
|
|
98
|
+
raise ValueError("unverified quadratic problem cannot be inspected/routed as convex_qp")
|
|
99
|
+
linear = problem.linear
|
|
100
|
+
problem_class = "convex_qp"
|
|
101
|
+
quadratic_nnz: int | None = int(problem.P.nnz)
|
|
102
|
+
n = problem.n_variables
|
|
103
|
+
quadratic_density: float | None = (
|
|
104
|
+
float(problem.P.nnz / (n * n)) if n > 0 else 0.0
|
|
105
|
+
)
|
|
106
|
+
convexity_status: str | None = ConvexityStatus(problem.convexity_status).value
|
|
107
|
+
extra_bytes = int(problem.P.data.nbytes + problem.P.indices.nbytes + problem.P.indptr.nbytes)
|
|
108
|
+
elif isinstance(problem, LinearProblem):
|
|
109
|
+
linear = problem
|
|
110
|
+
problem_class = "milp" if problem.has_integer_variables else "lp"
|
|
111
|
+
quadratic_nnz = None
|
|
112
|
+
quadratic_density = None
|
|
113
|
+
convexity_status = None
|
|
114
|
+
extra_bytes = 0
|
|
115
|
+
else:
|
|
116
|
+
raise TypeError(f"unsupported problem type: {type(problem)!r}")
|
|
117
|
+
|
|
118
|
+
m, n = linear.A.shape
|
|
119
|
+
size = m * n
|
|
120
|
+
density = float(linear.A.nnz / size) if size else 0.0
|
|
121
|
+
|
|
122
|
+
domains = linear.domains
|
|
123
|
+
if n:
|
|
124
|
+
binary_fraction = float(np.mean(domains == VariableDomain.BINARY.value))
|
|
125
|
+
integer_fraction = float(
|
|
126
|
+
np.mean(
|
|
127
|
+
np.isin(
|
|
128
|
+
domains,
|
|
129
|
+
[VariableDomain.INTEGER.value, VariableDomain.BINARY.value],
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
fully_bounded = float(
|
|
134
|
+
np.mean(np.isfinite(linear.variable_lower) & np.isfinite(linear.variable_upper))
|
|
135
|
+
)
|
|
136
|
+
free_fraction = float(
|
|
137
|
+
np.mean(np.isneginf(linear.variable_lower) & np.isposinf(linear.variable_upper))
|
|
138
|
+
)
|
|
139
|
+
objective_nnz_fraction = float(np.count_nonzero(linear.c) / n)
|
|
140
|
+
else:
|
|
141
|
+
binary_fraction = integer_fraction = fully_bounded = free_fraction = 0.0
|
|
142
|
+
objective_nnz_fraction = 0.0
|
|
143
|
+
|
|
144
|
+
if m:
|
|
145
|
+
equality_mask = (
|
|
146
|
+
np.isfinite(linear.constraint_lower)
|
|
147
|
+
& np.isfinite(linear.constraint_upper)
|
|
148
|
+
& (linear.constraint_lower == linear.constraint_upper)
|
|
149
|
+
)
|
|
150
|
+
equality_fraction = float(np.mean(equality_mask))
|
|
151
|
+
else:
|
|
152
|
+
equality_fraction = 0.0
|
|
153
|
+
|
|
154
|
+
row_counts = np.diff(linear.A.indptr)
|
|
155
|
+
col_counts = np.diff(linear.A.tocsc().indptr)
|
|
156
|
+
|
|
157
|
+
data = linear.A.data
|
|
158
|
+
if data.size:
|
|
159
|
+
positive_fraction = float(np.mean(data > 0.0))
|
|
160
|
+
negative_fraction = float(np.mean(data < 0.0))
|
|
161
|
+
else:
|
|
162
|
+
positive_fraction = negative_fraction = 0.0
|
|
163
|
+
|
|
164
|
+
finite_rhs = np.concatenate(
|
|
165
|
+
[
|
|
166
|
+
linear.constraint_lower[np.isfinite(linear.constraint_lower)],
|
|
167
|
+
linear.constraint_upper[np.isfinite(linear.constraint_upper)],
|
|
168
|
+
linear.variable_lower[np.isfinite(linear.variable_lower)],
|
|
169
|
+
linear.variable_upper[np.isfinite(linear.variable_upper)],
|
|
170
|
+
]
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
return ProblemFingerprint(
|
|
174
|
+
problem_class=problem_class,
|
|
175
|
+
n_variables=n,
|
|
176
|
+
n_constraints=m,
|
|
177
|
+
nnz_a=int(linear.A.nnz),
|
|
178
|
+
density_a=density,
|
|
179
|
+
binary_fraction=binary_fraction,
|
|
180
|
+
integer_fraction=integer_fraction,
|
|
181
|
+
equality_fraction=equality_fraction,
|
|
182
|
+
fully_bounded_variable_fraction=fully_bounded,
|
|
183
|
+
free_variable_fraction=free_fraction,
|
|
184
|
+
objective_nnz_fraction=objective_nnz_fraction,
|
|
185
|
+
coefficient_dynamic_range_log10=_dynamic_range_log10(linear.A.data),
|
|
186
|
+
objective_dynamic_range_log10=_dynamic_range_log10(linear.c),
|
|
187
|
+
rhs_dynamic_range_log10=_dynamic_range_log10(finite_rhs),
|
|
188
|
+
row_nnz=_distribution(row_counts),
|
|
189
|
+
col_nnz=_distribution(col_counts),
|
|
190
|
+
singleton_rows=int(np.count_nonzero(row_counts == 1)),
|
|
191
|
+
singleton_cols=int(np.count_nonzero(col_counts == 1)),
|
|
192
|
+
positive_coefficient_fraction=positive_fraction,
|
|
193
|
+
negative_coefficient_fraction=negative_fraction,
|
|
194
|
+
quadratic_nnz=quadratic_nnz,
|
|
195
|
+
quadratic_density=quadratic_density,
|
|
196
|
+
convexity_status=convexity_status,
|
|
197
|
+
structural_hash=problem.structural_hash,
|
|
198
|
+
estimated_numeric_bytes=_estimated_bytes_linear(linear) + extra_bytes,
|
|
199
|
+
)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Feature contracts and conservative distribution-shift diagnostics.
|
|
2
|
+
|
|
3
|
+
This package does not select a solver. It only standardizes pre-solve features,
|
|
4
|
+
audits leakage boundaries, and reports simple explainable shift evidence.
|
|
5
|
+
"""
|
|
6
|
+
from .errors import DistributionShiftError, FeatureSchemaError, IntelligenceError, LeakageDetectedError
|
|
7
|
+
from .fingerprint import PROBLEM_FINGERPRINT_FEATURE_SCHEMA, feature_record_from_problem, problem_fingerprint_feature_schema
|
|
8
|
+
from .leakage import LeakageAuditReport, LeakageFinding, audit_feature_boundary
|
|
9
|
+
from .schema import (
|
|
10
|
+
FeatureDataType,
|
|
11
|
+
FeatureDefinition,
|
|
12
|
+
FeatureRecord,
|
|
13
|
+
FeatureSchema,
|
|
14
|
+
MissingnessPolicy,
|
|
15
|
+
build_feature_record,
|
|
16
|
+
build_feature_schema,
|
|
17
|
+
)
|
|
18
|
+
from .shift import (
|
|
19
|
+
DistributionShiftAssessment,
|
|
20
|
+
DistributionShiftProfile,
|
|
21
|
+
ShiftFlag,
|
|
22
|
+
ShiftSeverity,
|
|
23
|
+
assess_distribution_shift,
|
|
24
|
+
fit_distribution_shift_profile,
|
|
25
|
+
fit_training_shift_profile,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"IntelligenceError", "FeatureSchemaError", "LeakageDetectedError", "DistributionShiftError",
|
|
30
|
+
"FeatureDataType", "MissingnessPolicy", "FeatureDefinition", "FeatureSchema", "FeatureRecord",
|
|
31
|
+
"build_feature_schema", "build_feature_record",
|
|
32
|
+
"LeakageFinding", "LeakageAuditReport", "audit_feature_boundary",
|
|
33
|
+
"ShiftSeverity", "ShiftFlag", "DistributionShiftProfile", "DistributionShiftAssessment",
|
|
34
|
+
"fit_distribution_shift_profile", "fit_training_shift_profile", "assess_distribution_shift",
|
|
35
|
+
"PROBLEM_FINGERPRINT_FEATURE_SCHEMA", "problem_fingerprint_feature_schema", "feature_record_from_problem",
|
|
36
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Errors for versioned feature-intelligence contracts."""
|
|
2
|
+
|
|
3
|
+
class IntelligenceError(ValueError):
|
|
4
|
+
"""Base error for SolverPilot feature-intelligence validation."""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class FeatureSchemaError(IntelligenceError):
|
|
8
|
+
"""Raised when a feature schema or record is invalid."""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LeakageDetectedError(IntelligenceError):
|
|
12
|
+
"""Raised when feature inputs cross the pre-solve/outcome boundary."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DistributionShiftError(IntelligenceError):
|
|
16
|
+
"""Raised when a distribution-shift profile cannot be built or compared safely."""
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Versioned feature schema for SolverPilot's existing ProblemFingerprint."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from solverpilot.inspect import ProblemFingerprint, inspect_problem
|
|
5
|
+
from solverpilot.problem import LinearProblem, QuadraticProblem
|
|
6
|
+
|
|
7
|
+
from .schema import FeatureDefinition, FeatureRecord, MissingnessPolicy, build_feature_record, build_feature_schema
|
|
8
|
+
|
|
9
|
+
_EXTRACTOR_ID = "solverpilot.inspect.problem_fingerprint"
|
|
10
|
+
_EXTRACTOR_VERSION = "1.0.0"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _d(name: str, dtype: str, unit: str, source: tuple[str, ...], *, nullable: bool = False, allowed: tuple[str, ...] = (), lower: float | None = None, upper: float | None = None) -> FeatureDefinition:
|
|
14
|
+
return FeatureDefinition(
|
|
15
|
+
name=name,
|
|
16
|
+
data_type=dtype,
|
|
17
|
+
unit=unit,
|
|
18
|
+
missingness=MissingnessPolicy.NULLABLE if nullable else MissingnessPolicy.FORBIDDEN,
|
|
19
|
+
description=f"Pre-solve structural feature {name} derived from the canonical problem representation.",
|
|
20
|
+
source_fields=source,
|
|
21
|
+
lower_bound=lower,
|
|
22
|
+
upper_bound=upper,
|
|
23
|
+
allowed_values=allowed,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def problem_fingerprint_feature_schema():
|
|
28
|
+
defs = [
|
|
29
|
+
_d("problem_class", "categorical", "category", ("problem.type",), allowed=("convex_qp", "lp", "milp")),
|
|
30
|
+
_d("n_variables", "integer", "count", ("problem.variables",), lower=0),
|
|
31
|
+
_d("n_constraints", "integer", "count", ("problem.constraints",), lower=0),
|
|
32
|
+
_d("nnz_a", "integer", "count", ("problem.A",), lower=0),
|
|
33
|
+
_d("density_a", "number", "ratio", ("problem.A",), lower=0, upper=1),
|
|
34
|
+
_d("binary_fraction", "number", "ratio", ("problem.domains",), lower=0, upper=1),
|
|
35
|
+
_d("integer_fraction", "number", "ratio", ("problem.domains",), lower=0, upper=1),
|
|
36
|
+
_d("equality_fraction", "number", "ratio", ("problem.constraint_bounds",), lower=0, upper=1),
|
|
37
|
+
_d("fully_bounded_variable_fraction", "number", "ratio", ("problem.variable_bounds",), lower=0, upper=1),
|
|
38
|
+
_d("free_variable_fraction", "number", "ratio", ("problem.variable_bounds",), lower=0, upper=1),
|
|
39
|
+
_d("objective_nnz_fraction", "number", "ratio", ("problem.objective_coefficients",), lower=0, upper=1),
|
|
40
|
+
_d("coefficient_dynamic_range_log10", "number", "log10_ratio", ("problem.A",), nullable=True, lower=0),
|
|
41
|
+
_d("objective_dynamic_range_log10", "number", "log10_ratio", ("problem.objective_coefficients",), nullable=True, lower=0),
|
|
42
|
+
_d("rhs_dynamic_range_log10", "number", "log10_ratio", ("problem.constraint_bounds", "problem.variable_bounds"), nullable=True, lower=0),
|
|
43
|
+
_d("singleton_rows", "integer", "count", ("problem.A",), lower=0),
|
|
44
|
+
_d("singleton_cols", "integer", "count", ("problem.A",), lower=0),
|
|
45
|
+
_d("positive_coefficient_fraction", "number", "ratio", ("problem.A",), lower=0, upper=1),
|
|
46
|
+
_d("negative_coefficient_fraction", "number", "ratio", ("problem.A",), lower=0, upper=1),
|
|
47
|
+
_d("quadratic_nnz", "integer", "count", ("problem.P",), nullable=True, lower=0),
|
|
48
|
+
_d("quadratic_density", "number", "ratio", ("problem.P",), nullable=True, lower=0, upper=1),
|
|
49
|
+
_d("convexity_status", "categorical", "category", ("problem.convexity_status",), nullable=True, allowed=("confirmed",)),
|
|
50
|
+
_d("estimated_numeric_bytes", "integer", "bytes", ("problem.canonical_arrays",), lower=0),
|
|
51
|
+
]
|
|
52
|
+
for prefix in ("row_nnz", "col_nnz"):
|
|
53
|
+
for suffix in ("minimum", "q25", "median", "mean", "q75", "maximum"):
|
|
54
|
+
defs.append(_d(f"{prefix}_{suffix}", "number", "count", ("problem.A",), lower=0))
|
|
55
|
+
return build_feature_schema(domain="solverpilot_problem_fingerprint", version="1.0.0", definitions=defs)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
PROBLEM_FINGERPRINT_FEATURE_SCHEMA = problem_fingerprint_feature_schema()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _values(f: ProblemFingerprint) -> dict[str, object]:
|
|
62
|
+
values: dict[str, object] = {
|
|
63
|
+
"problem_class": f.problem_class,
|
|
64
|
+
"n_variables": f.n_variables,
|
|
65
|
+
"n_constraints": f.n_constraints,
|
|
66
|
+
"nnz_a": f.nnz_a,
|
|
67
|
+
"density_a": f.density_a,
|
|
68
|
+
"binary_fraction": f.binary_fraction,
|
|
69
|
+
"integer_fraction": f.integer_fraction,
|
|
70
|
+
"equality_fraction": f.equality_fraction,
|
|
71
|
+
"fully_bounded_variable_fraction": f.fully_bounded_variable_fraction,
|
|
72
|
+
"free_variable_fraction": f.free_variable_fraction,
|
|
73
|
+
"objective_nnz_fraction": f.objective_nnz_fraction,
|
|
74
|
+
"coefficient_dynamic_range_log10": f.coefficient_dynamic_range_log10,
|
|
75
|
+
"objective_dynamic_range_log10": f.objective_dynamic_range_log10,
|
|
76
|
+
"rhs_dynamic_range_log10": f.rhs_dynamic_range_log10,
|
|
77
|
+
"singleton_rows": f.singleton_rows,
|
|
78
|
+
"singleton_cols": f.singleton_cols,
|
|
79
|
+
"positive_coefficient_fraction": f.positive_coefficient_fraction,
|
|
80
|
+
"negative_coefficient_fraction": f.negative_coefficient_fraction,
|
|
81
|
+
"quadratic_nnz": f.quadratic_nnz,
|
|
82
|
+
"quadratic_density": f.quadratic_density,
|
|
83
|
+
"convexity_status": f.convexity_status,
|
|
84
|
+
"estimated_numeric_bytes": f.estimated_numeric_bytes,
|
|
85
|
+
}
|
|
86
|
+
for prefix, stats in (("row_nnz", f.row_nnz), ("col_nnz", f.col_nnz)):
|
|
87
|
+
for suffix in ("minimum", "q25", "median", "mean", "q75", "maximum"):
|
|
88
|
+
values[f"{prefix}_{suffix}"] = getattr(stats, suffix)
|
|
89
|
+
return values
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def feature_record_from_problem(
|
|
93
|
+
problem: LinearProblem | QuadraticProblem,
|
|
94
|
+
*,
|
|
95
|
+
instance_id: str | None = None,
|
|
96
|
+
) -> FeatureRecord:
|
|
97
|
+
fingerprint = inspect_problem(problem)
|
|
98
|
+
values = _values(fingerprint)
|
|
99
|
+
reasons = {name: "not applicable to this problem class" for name, value in values.items() if value is None}
|
|
100
|
+
return build_feature_record(
|
|
101
|
+
schema=PROBLEM_FINGERPRINT_FEATURE_SCHEMA,
|
|
102
|
+
instance_id=problem.data_hash if instance_id is None else instance_id,
|
|
103
|
+
values=values,
|
|
104
|
+
missingness_reasons=reasons,
|
|
105
|
+
source_content_sha256=problem.data_hash,
|
|
106
|
+
extractor_id=_EXTRACTOR_ID,
|
|
107
|
+
extractor_version=_EXTRACTOR_VERSION,
|
|
108
|
+
)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Fail-closed auditing for predictive feature boundaries."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
import re
|
|
6
|
+
from typing import Iterable
|
|
7
|
+
|
|
8
|
+
from .errors import LeakageDetectedError
|
|
9
|
+
from .schema import FeatureRecord, FeatureSchema
|
|
10
|
+
|
|
11
|
+
_FORBIDDEN_FRAGMENTS = (
|
|
12
|
+
"solver", "backend_result", "solve_result", "selected_backend", "selected_solver", "winner",
|
|
13
|
+
"runtime", "wall_clock", "cpu_time", "regret", "target", "label", "prediction",
|
|
14
|
+
"benchmark_result", "oracle", "quality_score", "best_known", "timeout", "solve_status",
|
|
15
|
+
"optimality_status", "validation_result", "rank",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _norm(value: str) -> str:
|
|
20
|
+
return re.sub(r"[^a-z0-9]+", "_", str(value).strip().lower()).strip("_")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True, slots=True)
|
|
24
|
+
class LeakageFinding:
|
|
25
|
+
code: str
|
|
26
|
+
location: str
|
|
27
|
+
fragment: str
|
|
28
|
+
message: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True, slots=True)
|
|
32
|
+
class LeakageAuditReport:
|
|
33
|
+
passed: bool
|
|
34
|
+
findings: tuple[LeakageFinding, ...]
|
|
35
|
+
audited_feature_count: int
|
|
36
|
+
audited_record_count: int
|
|
37
|
+
|
|
38
|
+
def require_pass(self) -> None:
|
|
39
|
+
if not self.passed:
|
|
40
|
+
raise LeakageDetectedError(f"feature leakage audit failed with {len(self.findings)} finding(s)")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def audit_feature_boundary(schema: FeatureSchema, records: Iterable[FeatureRecord] = ()) -> LeakageAuditReport:
|
|
44
|
+
rows = tuple(records)
|
|
45
|
+
findings: list[LeakageFinding] = []
|
|
46
|
+
for definition in schema.definitions:
|
|
47
|
+
locations = [(f"definition.{definition.name}.name", definition.name)]
|
|
48
|
+
locations.extend((f"definition.{definition.name}.source_fields", field) for field in definition.source_fields)
|
|
49
|
+
for location, raw in locations:
|
|
50
|
+
normalized = _norm(raw)
|
|
51
|
+
for fragment in _FORBIDDEN_FRAGMENTS:
|
|
52
|
+
if fragment in normalized:
|
|
53
|
+
findings.append(LeakageFinding(
|
|
54
|
+
"FORBIDDEN_PREDICTIVE_INPUT", location, fragment,
|
|
55
|
+
"feature boundary references solver outcome, runtime, target, oracle, or other post-solve evidence",
|
|
56
|
+
))
|
|
57
|
+
for record in rows:
|
|
58
|
+
if record.feature_schema_id != schema.feature_schema_id:
|
|
59
|
+
findings.append(LeakageFinding(
|
|
60
|
+
"SCHEMA_MISMATCH", "record.feature_schema_id", "schema_mismatch",
|
|
61
|
+
"record does not belong to the audited feature schema",
|
|
62
|
+
))
|
|
63
|
+
undeclared = sorted(set(record.values) - set(schema.names))
|
|
64
|
+
for name in undeclared:
|
|
65
|
+
findings.append(LeakageFinding(
|
|
66
|
+
"UNDECLARED_FEATURE", "record.values", name,
|
|
67
|
+
"record contains an undeclared feature",
|
|
68
|
+
))
|
|
69
|
+
return LeakageAuditReport(not findings, tuple(findings), len(schema.definitions), len(rows))
|