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,327 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from solverpilot.runtime import SolveResult
|
|
7
|
+
from solverpilot.validate import PublicStatus
|
|
8
|
+
|
|
9
|
+
from .model import ClaimDisposition, ClaimKind, ExplanationClaim, ExplanationReport
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
_STATUS_EXPLANATIONS: dict[PublicStatus, str] = {
|
|
13
|
+
PublicStatus.VALID_OPTIMAL: (
|
|
14
|
+
"The backend reported optimality and the returned primal candidate passed SolverPilot's canonical validation. "
|
|
15
|
+
"This status alone is not an independent proof of optimality; consult the optimality evidence in this report."
|
|
16
|
+
),
|
|
17
|
+
PublicStatus.VALID_FEASIBLE: (
|
|
18
|
+
"The returned candidate passed SolverPilot's canonical feasibility validation. "
|
|
19
|
+
"Feasibility does not establish optimality."
|
|
20
|
+
),
|
|
21
|
+
PublicStatus.FEASIBLE_LIMIT: (
|
|
22
|
+
"A candidate passed canonical validation, but the backend terminated at a configured or solver limit. "
|
|
23
|
+
"The result must not be presented as proven optimal."
|
|
24
|
+
),
|
|
25
|
+
PublicStatus.INFEASIBLE: (
|
|
26
|
+
"The backend reported the model infeasible. Additional SolverPilot diagnostics, when present, are reported separately; "
|
|
27
|
+
"the public status alone is not an independently reconstructed universal infeasibility proof."
|
|
28
|
+
),
|
|
29
|
+
PublicStatus.UNBOUNDED: (
|
|
30
|
+
"The backend reported the model unbounded. SolverPilot does not infer an independent unboundedness proof from this status alone."
|
|
31
|
+
),
|
|
32
|
+
PublicStatus.INFEASIBLE_OR_UNBOUNDED: (
|
|
33
|
+
"The backend could not distinguish infeasibility from unboundedness. Neither conclusion should be stated separately without more evidence."
|
|
34
|
+
),
|
|
35
|
+
PublicStatus.INVALID_SOLUTION: (
|
|
36
|
+
"The backend returned a candidate that failed SolverPilot's independent canonical validation. "
|
|
37
|
+
"The candidate must not be presented as a valid solution."
|
|
38
|
+
),
|
|
39
|
+
PublicStatus.ERROR: (
|
|
40
|
+
"The solve did not produce a validated normal result. No feasibility, optimality, infeasibility, or superiority claim follows from this status."
|
|
41
|
+
),
|
|
42
|
+
PublicStatus.UNKNOWN: (
|
|
43
|
+
"The outcome could not be mapped to a stronger public status. Additional evidence is required before making a solve-quality claim."
|
|
44
|
+
),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def explain_status(status: PublicStatus | str) -> str:
|
|
49
|
+
if isinstance(status, str):
|
|
50
|
+
status = PublicStatus(status)
|
|
51
|
+
return _STATUS_EXPLANATIONS[status]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _validation_payload(result: SolveResult) -> dict[str, Any] | None:
|
|
55
|
+
report = result.validation
|
|
56
|
+
if report is None:
|
|
57
|
+
return None
|
|
58
|
+
return {
|
|
59
|
+
"valid": report.valid,
|
|
60
|
+
"max_bound_violation": report.max_bound_violation,
|
|
61
|
+
"max_constraint_violation": report.max_constraint_violation,
|
|
62
|
+
"max_integrality_violation": report.max_integrality_violation,
|
|
63
|
+
"objective_recomputed": report.objective_recomputed,
|
|
64
|
+
"objective_reported": report.objective_reported,
|
|
65
|
+
"objective_difference": report.objective_difference,
|
|
66
|
+
"objective_consistent": report.objective_consistent,
|
|
67
|
+
"warnings": tuple(report.warnings),
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _optimality_payload(result: SolveResult) -> dict[str, bool]:
|
|
72
|
+
evidence = result.optimality_evidence
|
|
73
|
+
return {
|
|
74
|
+
"backend_reported_optimal": evidence.backend_reported_optimal,
|
|
75
|
+
"primal_validated": evidence.primal_validated,
|
|
76
|
+
"dual_verified": evidence.dual_verified,
|
|
77
|
+
"gap_verified": evidence.gap_verified,
|
|
78
|
+
"certificate_verified": evidence.certificate_verified,
|
|
79
|
+
"independently_verified_optimal": evidence.independently_verified_optimal,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _planner_payload(result: SolveResult) -> dict[str, Any] | None:
|
|
84
|
+
plan = result.plan
|
|
85
|
+
if plan is None:
|
|
86
|
+
return None
|
|
87
|
+
return {
|
|
88
|
+
"selected_backend": plan.selected_backend,
|
|
89
|
+
"intent": plan.intent.value,
|
|
90
|
+
"strategy": plan.strategy,
|
|
91
|
+
"evidence_level": plan.evidence_level,
|
|
92
|
+
"health_policy": plan.health_policy.value,
|
|
93
|
+
"rationale": tuple(plan.rationale),
|
|
94
|
+
"candidate_count": len(plan.candidates),
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _runtime_payload(result: SolveResult) -> dict[str, Any]:
|
|
99
|
+
timings = result.trace.timings
|
|
100
|
+
return {
|
|
101
|
+
"reuse_applied": result.trace.reuse_applied,
|
|
102
|
+
"reuse_mode": result.trace.reuse_mode,
|
|
103
|
+
"total_s": timings.total_s,
|
|
104
|
+
"solve_s": timings.solve_s,
|
|
105
|
+
"validate_s": timings.validate_s,
|
|
106
|
+
"diagnose_s": timings.diagnose_s,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _diagnostics_payload(result: SolveResult) -> dict[str, Any] | None:
|
|
111
|
+
diagnostics = result.diagnostics
|
|
112
|
+
if diagnostics is None:
|
|
113
|
+
return None
|
|
114
|
+
return {
|
|
115
|
+
"confirmed_infeasible": diagnostics.confirmed_infeasible,
|
|
116
|
+
"static_issue_count": len(diagnostics.static_issues),
|
|
117
|
+
"iis_available": diagnostics.iis is not None,
|
|
118
|
+
"iis_valid": bool(diagnostics.iis is not None and diagnostics.iis.valid),
|
|
119
|
+
"elastic_available": diagnostics.elastic is not None,
|
|
120
|
+
"conflict_available": diagnostics.conflict is not None,
|
|
121
|
+
"conflict_irreducible": None if diagnostics.conflict is None else diagnostics.conflict.irreducible,
|
|
122
|
+
"warnings": tuple(diagnostics.warnings),
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _claims(result: SolveResult) -> tuple[ExplanationClaim, ...]:
|
|
127
|
+
claims: list[ExplanationClaim] = []
|
|
128
|
+
validation_valid = bool(result.validation is not None and result.validation.valid and result.x is not None)
|
|
129
|
+
if validation_valid:
|
|
130
|
+
claims.append(
|
|
131
|
+
ExplanationClaim(
|
|
132
|
+
claim_id="solution.feasibility",
|
|
133
|
+
kind=ClaimKind.FEASIBILITY,
|
|
134
|
+
disposition=ClaimDisposition.SUPPORTED,
|
|
135
|
+
statement="The returned candidate passed SolverPilot's canonical validation.",
|
|
136
|
+
rationale="A primal candidate exists and ValidationReport.valid is true.",
|
|
137
|
+
evidence_refs=("result.validation",),
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
else:
|
|
141
|
+
claims.append(
|
|
142
|
+
ExplanationClaim(
|
|
143
|
+
claim_id="solution.feasibility",
|
|
144
|
+
kind=ClaimKind.FEASIBILITY,
|
|
145
|
+
disposition=ClaimDisposition.BLOCKED,
|
|
146
|
+
statement="A validated feasible candidate cannot be claimed from this result.",
|
|
147
|
+
rationale="No primal candidate passed SolverPilot's canonical validation.",
|
|
148
|
+
evidence_refs=("result.validation",),
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
evidence = result.optimality_evidence
|
|
153
|
+
if evidence.independently_verified_optimal:
|
|
154
|
+
claims.append(
|
|
155
|
+
ExplanationClaim(
|
|
156
|
+
claim_id="solution.optimality",
|
|
157
|
+
kind=ClaimKind.OPTIMALITY,
|
|
158
|
+
disposition=ClaimDisposition.SUPPORTED,
|
|
159
|
+
statement="Optimality satisfied SolverPilot's available independent optimality checks.",
|
|
160
|
+
rationale="Primal validation, dual verification, and a verified gap or certificate are all present.",
|
|
161
|
+
evidence_refs=("result.optimality_evidence",),
|
|
162
|
+
qualifiers=("The scope of verification is limited to the checks represented by OptimalityEvidence.",),
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
elif result.status is PublicStatus.VALID_OPTIMAL and evidence.backend_reported_optimal and evidence.primal_validated:
|
|
166
|
+
claims.append(
|
|
167
|
+
ExplanationClaim(
|
|
168
|
+
claim_id="solution.optimality",
|
|
169
|
+
kind=ClaimKind.OPTIMALITY,
|
|
170
|
+
disposition=ClaimDisposition.QUALIFIED,
|
|
171
|
+
statement="The backend reported optimality and the primal candidate passed canonical validation.",
|
|
172
|
+
rationale="SolverPilot did not independently reconstruct all optimality conditions required by OptimalityEvidence.",
|
|
173
|
+
evidence_refs=("result.optimality_evidence", "result.validation"),
|
|
174
|
+
qualifiers=("Do not describe this as independently proven optimal.",),
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
else:
|
|
178
|
+
claims.append(
|
|
179
|
+
ExplanationClaim(
|
|
180
|
+
claim_id="solution.optimality",
|
|
181
|
+
kind=ClaimKind.OPTIMALITY,
|
|
182
|
+
disposition=ClaimDisposition.BLOCKED,
|
|
183
|
+
statement="Optimality is not established by this result.",
|
|
184
|
+
rationale="The result does not carry sufficient optimality evidence.",
|
|
185
|
+
evidence_refs=("result.optimality_evidence",),
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
if result.status is PublicStatus.INFEASIBLE:
|
|
190
|
+
confirmed = bool(result.diagnostics is not None and result.diagnostics.confirmed_infeasible)
|
|
191
|
+
claims.append(
|
|
192
|
+
ExplanationClaim(
|
|
193
|
+
claim_id="solution.infeasibility",
|
|
194
|
+
kind=ClaimKind.INFEASIBILITY,
|
|
195
|
+
disposition=ClaimDisposition.SUPPORTED if confirmed else ClaimDisposition.QUALIFIED,
|
|
196
|
+
statement=(
|
|
197
|
+
"SolverPilot diagnostics additionally confirmed infeasibility for the supplied model."
|
|
198
|
+
if confirmed
|
|
199
|
+
else "The backend reported the supplied model infeasible."
|
|
200
|
+
),
|
|
201
|
+
rationale=(
|
|
202
|
+
"InfeasibilityReport.confirmed_infeasible is true."
|
|
203
|
+
if confirmed
|
|
204
|
+
else "No independent SolverPilot diagnostic confirmation is attached to this SolveResult."
|
|
205
|
+
),
|
|
206
|
+
evidence_refs=("result.diagnostics", "result.backend_status") if confirmed else ("result.backend_status",),
|
|
207
|
+
qualifiers=(
|
|
208
|
+
"The claim is limited to the supplied mathematical model and data; it is not a statement about an external real-world process.",
|
|
209
|
+
),
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
else:
|
|
213
|
+
claims.append(
|
|
214
|
+
ExplanationClaim(
|
|
215
|
+
claim_id="solution.infeasibility",
|
|
216
|
+
kind=ClaimKind.INFEASIBILITY,
|
|
217
|
+
disposition=ClaimDisposition.BLOCKED,
|
|
218
|
+
statement="Infeasibility is not established by this result.",
|
|
219
|
+
rationale=f"Public status is {result.status.value!r}, not 'infeasible'.",
|
|
220
|
+
evidence_refs=("result.status",),
|
|
221
|
+
)
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
if result.status is PublicStatus.UNBOUNDED:
|
|
225
|
+
claims.append(
|
|
226
|
+
ExplanationClaim(
|
|
227
|
+
claim_id="solution.unboundedness",
|
|
228
|
+
kind=ClaimKind.UNBOUNDEDNESS,
|
|
229
|
+
disposition=ClaimDisposition.QUALIFIED,
|
|
230
|
+
statement="The backend reported the supplied model unbounded.",
|
|
231
|
+
rationale="SolverPilot currently exposes no general independent unboundedness-proof field on SolveResult.",
|
|
232
|
+
evidence_refs=("result.backend_status",),
|
|
233
|
+
qualifiers=("Do not describe this as independently proven unbounded from SolveResult alone.",),
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
else:
|
|
237
|
+
claims.append(
|
|
238
|
+
ExplanationClaim(
|
|
239
|
+
claim_id="solution.unboundedness",
|
|
240
|
+
kind=ClaimKind.UNBOUNDEDNESS,
|
|
241
|
+
disposition=ClaimDisposition.BLOCKED,
|
|
242
|
+
statement="Unboundedness is not established by this result.",
|
|
243
|
+
rationale=f"Public status is {result.status.value!r}, not 'unbounded'.",
|
|
244
|
+
evidence_refs=("result.status",),
|
|
245
|
+
)
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
if result.plan is not None:
|
|
249
|
+
claims.append(
|
|
250
|
+
ExplanationClaim(
|
|
251
|
+
claim_id="routing.selection",
|
|
252
|
+
kind=ClaimKind.PLANNER_SELECTION,
|
|
253
|
+
disposition=ClaimDisposition.SUPPORTED,
|
|
254
|
+
statement=f"SolverPilot's planner selected backend {result.plan.selected_backend!r}.",
|
|
255
|
+
rationale="The selection is recorded in SolvePlan.",
|
|
256
|
+
evidence_refs=("result.plan",),
|
|
257
|
+
qualifiers=("Planner selection is not, by itself, evidence that this backend is globally fastest or superior.",),
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
if result.trace.reuse_applied is True:
|
|
262
|
+
claims.append(
|
|
263
|
+
ExplanationClaim(
|
|
264
|
+
claim_id="runtime.reuse",
|
|
265
|
+
kind=ClaimKind.REOPTIMIZATION,
|
|
266
|
+
disposition=ClaimDisposition.SUPPORTED,
|
|
267
|
+
statement="The recorded solve reused prior solver state.",
|
|
268
|
+
rationale="SolveTrace.reuse_applied is true.",
|
|
269
|
+
evidence_refs=("result.trace",),
|
|
270
|
+
qualifiers=("State reuse does not by itself establish a performance improvement.",),
|
|
271
|
+
)
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
return tuple(claims)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _warnings(result: SolveResult) -> tuple[str, ...]:
|
|
278
|
+
values: list[str] = []
|
|
279
|
+
if result.validation is not None:
|
|
280
|
+
values.extend(str(x) for x in result.validation.warnings)
|
|
281
|
+
values.extend(str(x) for x in result.trace.warnings)
|
|
282
|
+
if result.diagnostics is not None:
|
|
283
|
+
values.extend(str(x) for x in result.diagnostics.warnings)
|
|
284
|
+
# Preserve order while removing duplicates.
|
|
285
|
+
return tuple(dict.fromkeys(values))
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def explain_result(result: SolveResult) -> ExplanationReport:
|
|
289
|
+
if not isinstance(result, SolveResult):
|
|
290
|
+
raise TypeError("explain_result expects a SolveResult")
|
|
291
|
+
optimality = _optimality_payload(result)
|
|
292
|
+
independently_verified = optimality["independently_verified_optimal"]
|
|
293
|
+
if result.status is PublicStatus.VALID_OPTIMAL:
|
|
294
|
+
summary = (
|
|
295
|
+
"A validated candidate was returned and optimality passed SolverPilot's available independent checks."
|
|
296
|
+
if independently_verified
|
|
297
|
+
else "A validated candidate was returned; the backend reported optimality, but complete independent optimality verification is not available."
|
|
298
|
+
)
|
|
299
|
+
elif result.status in {PublicStatus.VALID_FEASIBLE, PublicStatus.FEASIBLE_LIMIT}:
|
|
300
|
+
summary = "A validated feasible candidate was returned without an independent optimality claim."
|
|
301
|
+
elif result.status is PublicStatus.INFEASIBLE:
|
|
302
|
+
summary = (
|
|
303
|
+
"The model was reported infeasible and SolverPilot diagnostics additionally confirmed infeasibility."
|
|
304
|
+
if result.diagnostics is not None and result.diagnostics.confirmed_infeasible
|
|
305
|
+
else "The backend reported the supplied model infeasible; no additional SolverPilot confirmation is attached."
|
|
306
|
+
)
|
|
307
|
+
elif result.status is PublicStatus.INVALID_SOLUTION:
|
|
308
|
+
summary = "The backend returned a candidate that failed SolverPilot's canonical validation."
|
|
309
|
+
else:
|
|
310
|
+
summary = f"The solve terminated with public status {result.status.value!r}."
|
|
311
|
+
|
|
312
|
+
return ExplanationReport(
|
|
313
|
+
status=result.status.value,
|
|
314
|
+
summary=summary,
|
|
315
|
+
status_explanation=explain_status(result.status),
|
|
316
|
+
problem_class=result.trace.problem_class,
|
|
317
|
+
backend=result.trace.backend,
|
|
318
|
+
backend_version=result.trace.backend_version,
|
|
319
|
+
objective=result.objective,
|
|
320
|
+
validation=_validation_payload(result),
|
|
321
|
+
optimality=optimality,
|
|
322
|
+
planner=_planner_payload(result),
|
|
323
|
+
runtime=_runtime_payload(result),
|
|
324
|
+
diagnostics=_diagnostics_payload(result),
|
|
325
|
+
claims=_claims(result),
|
|
326
|
+
warnings=_warnings(result),
|
|
327
|
+
)
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping, Sequence
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from solverpilot.io.hashing import deep_freeze_json, deep_thaw_json
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
REPORT_SCHEMA_VERSION = "0.1"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ClaimKind(str, Enum):
|
|
15
|
+
FEASIBILITY = "feasibility"
|
|
16
|
+
OPTIMALITY = "optimality"
|
|
17
|
+
INFEASIBILITY = "infeasibility"
|
|
18
|
+
UNBOUNDEDNESS = "unboundedness"
|
|
19
|
+
PLANNER_SELECTION = "planner_selection"
|
|
20
|
+
REOPTIMIZATION = "reoptimization"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ClaimDisposition(str, Enum):
|
|
24
|
+
SUPPORTED = "supported"
|
|
25
|
+
QUALIFIED = "qualified"
|
|
26
|
+
BLOCKED = "blocked"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _nonempty_text(value: object, field_name: str) -> str:
|
|
30
|
+
if not isinstance(value, str) or not value.strip():
|
|
31
|
+
raise ValueError(f"{field_name} must be a non-empty string")
|
|
32
|
+
return value.strip()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _tuple_text(values: Sequence[str], field_name: str) -> tuple[str, ...]:
|
|
36
|
+
if isinstance(values, (str, bytes, bytearray)):
|
|
37
|
+
raise ValueError(f"{field_name} must be a sequence of strings, not a string")
|
|
38
|
+
try:
|
|
39
|
+
normalized = tuple(_nonempty_text(value, field_name) for value in values)
|
|
40
|
+
except TypeError as exc:
|
|
41
|
+
raise ValueError(f"{field_name} must be a sequence of strings") from exc
|
|
42
|
+
return normalized
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _freeze_mapping(value: Mapping[str, Any] | None, field_name: str) -> Mapping[str, Any] | None:
|
|
46
|
+
if value is None:
|
|
47
|
+
return None
|
|
48
|
+
if not isinstance(value, Mapping):
|
|
49
|
+
raise ValueError(f"{field_name} must be a mapping or None")
|
|
50
|
+
try:
|
|
51
|
+
frozen = deep_freeze_json(value)
|
|
52
|
+
except Exception as exc:
|
|
53
|
+
if "finite" in str(exc).lower():
|
|
54
|
+
raise ValueError(f"report contains NaN or infinity at $.{field_name}") from exc
|
|
55
|
+
raise ValueError(f"report is not JSON-safe at $.{field_name}") from exc
|
|
56
|
+
if not isinstance(frozen, Mapping):
|
|
57
|
+
raise ValueError(f"{field_name} must form an object")
|
|
58
|
+
return frozen
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True, slots=True)
|
|
62
|
+
class ExplanationClaim:
|
|
63
|
+
claim_id: str
|
|
64
|
+
kind: ClaimKind
|
|
65
|
+
disposition: ClaimDisposition
|
|
66
|
+
statement: str
|
|
67
|
+
rationale: str
|
|
68
|
+
evidence_refs: tuple[str, ...] = ()
|
|
69
|
+
qualifiers: tuple[str, ...] = ()
|
|
70
|
+
|
|
71
|
+
def __post_init__(self) -> None:
|
|
72
|
+
claim_id = _nonempty_text(self.claim_id, "claim_id")
|
|
73
|
+
statement = _nonempty_text(self.statement, "statement")
|
|
74
|
+
rationale = _nonempty_text(self.rationale, "rationale")
|
|
75
|
+
try:
|
|
76
|
+
kind = self.kind if isinstance(self.kind, ClaimKind) else ClaimKind(str(self.kind).strip().lower())
|
|
77
|
+
except ValueError as exc:
|
|
78
|
+
raise ValueError("kind must be a supported ClaimKind") from exc
|
|
79
|
+
try:
|
|
80
|
+
disposition = (
|
|
81
|
+
self.disposition
|
|
82
|
+
if isinstance(self.disposition, ClaimDisposition)
|
|
83
|
+
else ClaimDisposition(str(self.disposition).strip().lower())
|
|
84
|
+
)
|
|
85
|
+
except ValueError as exc:
|
|
86
|
+
raise ValueError("disposition must be a supported ClaimDisposition") from exc
|
|
87
|
+
evidence_refs = _tuple_text(self.evidence_refs, "evidence_refs")
|
|
88
|
+
qualifiers = _tuple_text(self.qualifiers, "qualifiers")
|
|
89
|
+
if len(set(evidence_refs)) != len(evidence_refs):
|
|
90
|
+
raise ValueError("evidence_refs must not contain duplicates")
|
|
91
|
+
object.__setattr__(self, "claim_id", claim_id)
|
|
92
|
+
object.__setattr__(self, "kind", kind)
|
|
93
|
+
object.__setattr__(self, "disposition", disposition)
|
|
94
|
+
object.__setattr__(self, "statement", statement)
|
|
95
|
+
object.__setattr__(self, "rationale", rationale)
|
|
96
|
+
object.__setattr__(self, "evidence_refs", evidence_refs)
|
|
97
|
+
object.__setattr__(self, "qualifiers", qualifiers)
|
|
98
|
+
|
|
99
|
+
def to_dict(self) -> dict[str, Any]:
|
|
100
|
+
return {
|
|
101
|
+
"claim_id": self.claim_id,
|
|
102
|
+
"kind": self.kind.value,
|
|
103
|
+
"disposition": self.disposition.value,
|
|
104
|
+
"statement": self.statement,
|
|
105
|
+
"rationale": self.rationale,
|
|
106
|
+
"evidence_refs": list(self.evidence_refs),
|
|
107
|
+
"qualifiers": list(self.qualifiers),
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass(frozen=True, slots=True)
|
|
112
|
+
class ExplanationReport:
|
|
113
|
+
status: str
|
|
114
|
+
summary: str
|
|
115
|
+
status_explanation: str
|
|
116
|
+
problem_class: str
|
|
117
|
+
backend: str | None
|
|
118
|
+
backend_version: str | None
|
|
119
|
+
objective: float | None
|
|
120
|
+
validation: Mapping[str, Any] | None
|
|
121
|
+
optimality: Mapping[str, Any]
|
|
122
|
+
planner: Mapping[str, Any] | None
|
|
123
|
+
runtime: Mapping[str, Any]
|
|
124
|
+
diagnostics: Mapping[str, Any] | None
|
|
125
|
+
claims: tuple[ExplanationClaim, ...]
|
|
126
|
+
warnings: tuple[str, ...] = ()
|
|
127
|
+
schema_version: str = REPORT_SCHEMA_VERSION
|
|
128
|
+
|
|
129
|
+
def __post_init__(self) -> None:
|
|
130
|
+
status = _nonempty_text(self.status, "status")
|
|
131
|
+
summary = _nonempty_text(self.summary, "summary")
|
|
132
|
+
status_explanation = _nonempty_text(self.status_explanation, "status_explanation")
|
|
133
|
+
problem_class = _nonempty_text(self.problem_class, "problem_class")
|
|
134
|
+
backend = None if self.backend is None else _nonempty_text(self.backend, "backend")
|
|
135
|
+
backend_version = None if self.backend_version is None else _nonempty_text(self.backend_version, "backend_version")
|
|
136
|
+
if self.schema_version != REPORT_SCHEMA_VERSION:
|
|
137
|
+
raise ValueError(f"schema_version must be {REPORT_SCHEMA_VERSION!r}")
|
|
138
|
+
if self.objective is not None:
|
|
139
|
+
import math
|
|
140
|
+
|
|
141
|
+
if isinstance(self.objective, bool) or type(self.objective) not in (int, float) or not math.isfinite(float(self.objective)):
|
|
142
|
+
raise ValueError("report contains NaN or infinity at $.objective")
|
|
143
|
+
objective: float | None = float(self.objective)
|
|
144
|
+
else:
|
|
145
|
+
objective = None
|
|
146
|
+
validation = _freeze_mapping(self.validation, "validation")
|
|
147
|
+
optimality = _freeze_mapping(self.optimality, "optimality")
|
|
148
|
+
planner = _freeze_mapping(self.planner, "planner")
|
|
149
|
+
runtime = _freeze_mapping(self.runtime, "runtime")
|
|
150
|
+
diagnostics = _freeze_mapping(self.diagnostics, "diagnostics")
|
|
151
|
+
if optimality is None or runtime is None:
|
|
152
|
+
raise ValueError("optimality and runtime must be mappings")
|
|
153
|
+
claims = tuple(self.claims)
|
|
154
|
+
if any(not isinstance(claim, ExplanationClaim) for claim in claims):
|
|
155
|
+
raise ValueError("claims must contain ExplanationClaim objects")
|
|
156
|
+
warnings = _tuple_text(self.warnings, "warnings")
|
|
157
|
+
ids = [claim.claim_id for claim in claims]
|
|
158
|
+
if len(ids) != len(set(ids)):
|
|
159
|
+
raise ValueError("claim IDs must be unique")
|
|
160
|
+
object.__setattr__(self, "status", status)
|
|
161
|
+
object.__setattr__(self, "summary", summary)
|
|
162
|
+
object.__setattr__(self, "status_explanation", status_explanation)
|
|
163
|
+
object.__setattr__(self, "problem_class", problem_class)
|
|
164
|
+
object.__setattr__(self, "backend", backend)
|
|
165
|
+
object.__setattr__(self, "backend_version", backend_version)
|
|
166
|
+
object.__setattr__(self, "objective", objective)
|
|
167
|
+
object.__setattr__(self, "validation", validation)
|
|
168
|
+
object.__setattr__(self, "optimality", optimality)
|
|
169
|
+
object.__setattr__(self, "planner", planner)
|
|
170
|
+
object.__setattr__(self, "runtime", runtime)
|
|
171
|
+
object.__setattr__(self, "diagnostics", diagnostics)
|
|
172
|
+
object.__setattr__(self, "claims", claims)
|
|
173
|
+
object.__setattr__(self, "warnings", warnings)
|
|
174
|
+
_validate_claim_evidence_refs(claims)
|
|
175
|
+
if not self.claim_safe:
|
|
176
|
+
raise ValueError("ExplanationReport contains a supported claim that is not authorized by structured evidence")
|
|
177
|
+
_assert_json_safe(self.to_dict())
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def claim_safe(self) -> bool:
|
|
181
|
+
for claim in self.claims:
|
|
182
|
+
if claim.disposition is not ClaimDisposition.SUPPORTED:
|
|
183
|
+
continue
|
|
184
|
+
if claim.kind is ClaimKind.FEASIBILITY:
|
|
185
|
+
if not (isinstance(self.validation, Mapping) and self.validation.get("valid") is True):
|
|
186
|
+
return False
|
|
187
|
+
elif claim.kind is ClaimKind.OPTIMALITY:
|
|
188
|
+
if self.optimality.get("independently_verified_optimal") is not True:
|
|
189
|
+
return False
|
|
190
|
+
elif claim.kind is ClaimKind.INFEASIBILITY:
|
|
191
|
+
if not (isinstance(self.diagnostics, Mapping) and self.diagnostics.get("confirmed_infeasible") is True):
|
|
192
|
+
return False
|
|
193
|
+
elif claim.kind is ClaimKind.UNBOUNDEDNESS:
|
|
194
|
+
# S5/S10 has no generic independent unboundedness-proof field.
|
|
195
|
+
return False
|
|
196
|
+
elif claim.kind is ClaimKind.PLANNER_SELECTION:
|
|
197
|
+
if self.planner is None:
|
|
198
|
+
return False
|
|
199
|
+
elif claim.kind is ClaimKind.REOPTIMIZATION:
|
|
200
|
+
if self.runtime.get("reuse_applied") is not True:
|
|
201
|
+
return False
|
|
202
|
+
return True
|
|
203
|
+
|
|
204
|
+
def to_dict(self) -> dict[str, Any]:
|
|
205
|
+
payload = {
|
|
206
|
+
"status": self.status,
|
|
207
|
+
"summary": self.summary,
|
|
208
|
+
"status_explanation": self.status_explanation,
|
|
209
|
+
"problem_class": self.problem_class,
|
|
210
|
+
"backend": self.backend,
|
|
211
|
+
"backend_version": self.backend_version,
|
|
212
|
+
"objective": self.objective,
|
|
213
|
+
"validation": None if self.validation is None else deep_thaw_json(self.validation),
|
|
214
|
+
"optimality": deep_thaw_json(self.optimality),
|
|
215
|
+
"planner": None if self.planner is None else deep_thaw_json(self.planner),
|
|
216
|
+
"runtime": deep_thaw_json(self.runtime),
|
|
217
|
+
"diagnostics": None if self.diagnostics is None else deep_thaw_json(self.diagnostics),
|
|
218
|
+
"claims": [claim.to_dict() for claim in self.claims],
|
|
219
|
+
"warnings": list(self.warnings),
|
|
220
|
+
"schema_version": self.schema_version,
|
|
221
|
+
}
|
|
222
|
+
_assert_json_safe(payload)
|
|
223
|
+
if not self.claim_safe:
|
|
224
|
+
raise ValueError("ExplanationReport no longer satisfies claim-safety invariants")
|
|
225
|
+
return payload
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _assert_json_safe(value: Any, *, path: str = "$") -> None:
|
|
229
|
+
import math
|
|
230
|
+
|
|
231
|
+
if value is None or isinstance(value, (str, bool, int)):
|
|
232
|
+
return
|
|
233
|
+
if isinstance(value, float):
|
|
234
|
+
if not math.isfinite(value):
|
|
235
|
+
raise ValueError(f"report contains NaN or infinity at {path}")
|
|
236
|
+
return
|
|
237
|
+
if isinstance(value, (tuple, list)):
|
|
238
|
+
for index, item in enumerate(value):
|
|
239
|
+
_assert_json_safe(item, path=f"{path}[{index}]")
|
|
240
|
+
return
|
|
241
|
+
if isinstance(value, Mapping):
|
|
242
|
+
for key, item in value.items():
|
|
243
|
+
if not isinstance(key, str):
|
|
244
|
+
raise ValueError(f"report mapping keys must be strings at {path}")
|
|
245
|
+
_assert_json_safe(item, path=f"{path}.{key}")
|
|
246
|
+
return
|
|
247
|
+
raise ValueError(f"report is not JSON-safe at {path}: {type(value).__name__}")
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
_KNOWN_EVIDENCE_REFS = frozenset({
|
|
251
|
+
"result.status",
|
|
252
|
+
"result.validation",
|
|
253
|
+
"result.optimality_evidence",
|
|
254
|
+
"result.backend_status",
|
|
255
|
+
"result.diagnostics",
|
|
256
|
+
"result.plan",
|
|
257
|
+
"result.trace",
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _validate_claim_evidence_refs(claims: tuple[ExplanationClaim, ...]) -> None:
|
|
262
|
+
unknown = sorted({ref for claim in claims for ref in claim.evidence_refs if ref not in _KNOWN_EVIDENCE_REFS})
|
|
263
|
+
if unknown:
|
|
264
|
+
raise ValueError(f"claims contain unknown evidence references: {unknown}")
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import html
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
from .model import ExplanationReport
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def render_json(report: ExplanationReport, *, indent: int | None = 2) -> str:
|
|
10
|
+
if not isinstance(report, ExplanationReport):
|
|
11
|
+
raise TypeError("render_json expects an ExplanationReport")
|
|
12
|
+
if not report.claim_safe:
|
|
13
|
+
raise ValueError("refusing to render an explanation report that violates claim-safety invariants")
|
|
14
|
+
return json.dumps(report.to_dict(), ensure_ascii=False, sort_keys=True, indent=indent, allow_nan=False)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _md(value: object) -> str:
|
|
18
|
+
text = html.escape(str(value), quote=False)
|
|
19
|
+
return text.replace("|", "\\|").replace("\r", " ").replace("\n", " ")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def render_markdown(report: ExplanationReport) -> str:
|
|
23
|
+
if not isinstance(report, ExplanationReport):
|
|
24
|
+
raise TypeError("render_markdown expects an ExplanationReport")
|
|
25
|
+
if not report.claim_safe:
|
|
26
|
+
raise ValueError("refusing to render an explanation report that violates claim-safety invariants")
|
|
27
|
+
lines = [
|
|
28
|
+
"# SolverPilot explanation",
|
|
29
|
+
"",
|
|
30
|
+
f"**Status:** `{_md(report.status)}`",
|
|
31
|
+
f"**Problem class:** `{_md(report.problem_class)}`",
|
|
32
|
+
f"**Backend:** `{_md(report.backend if report.backend is not None else 'unknown')}`",
|
|
33
|
+
f"**Objective:** `{_md(report.objective if report.objective is not None else 'not available')}`",
|
|
34
|
+
"",
|
|
35
|
+
"## Summary",
|
|
36
|
+
_md(report.summary),
|
|
37
|
+
"",
|
|
38
|
+
"## Status interpretation",
|
|
39
|
+
_md(report.status_explanation),
|
|
40
|
+
"",
|
|
41
|
+
"## Claims",
|
|
42
|
+
"| Claim | Disposition | Statement |",
|
|
43
|
+
"| --- | --- | --- |",
|
|
44
|
+
]
|
|
45
|
+
for claim in report.claims:
|
|
46
|
+
lines.append(
|
|
47
|
+
f"| `{_md(claim.claim_id)}` | `{_md(claim.disposition.value)}` | {_md(claim.statement)} |"
|
|
48
|
+
)
|
|
49
|
+
if claim.qualifiers:
|
|
50
|
+
for qualifier in claim.qualifiers:
|
|
51
|
+
lines.append(f" - **Qualifier:** {_md(qualifier)}")
|
|
52
|
+
if report.warnings:
|
|
53
|
+
lines.extend(["", "## Warnings"])
|
|
54
|
+
lines.extend(f"- {_md(warning)}" for warning in report.warnings)
|
|
55
|
+
return "\n".join(lines) + "\n"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .auto import builtin_backend_candidates, default_registry, solve, solve_production
|
|
2
|
+
from .budgeting import apply_budget
|
|
3
|
+
from .executor import execute
|
|
4
|
+
from .portfolio import PortfolioAttempt, PortfolioSolveResult, execute_portfolio
|
|
5
|
+
from .result import OptimalityEvidence, SolveResult
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"PortfolioAttempt",
|
|
9
|
+
"PortfolioSolveResult",
|
|
10
|
+
"SolveResult",
|
|
11
|
+
"OptimalityEvidence",
|
|
12
|
+
"apply_budget",
|
|
13
|
+
"builtin_backend_candidates",
|
|
14
|
+
"default_registry",
|
|
15
|
+
"execute",
|
|
16
|
+
"execute_portfolio",
|
|
17
|
+
"solve",
|
|
18
|
+
"solve_production",
|
|
19
|
+
]
|