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,447 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, replace
|
|
4
|
+
import gzip
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Iterable, TextIO
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from scipy import sparse
|
|
10
|
+
|
|
11
|
+
from .enums import ObjectiveSense, VariableDomain
|
|
12
|
+
from .linear import LinearProblem
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MPSParseError(ValueError):
|
|
16
|
+
"""Raised when an MPS input is malformed or semantically ambiguous."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MPSUnsupportedFeatureError(MPSParseError):
|
|
20
|
+
"""Raised when an MPS construct cannot be represented by the v0.1 IR."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_SECTION_NAMES = {
|
|
24
|
+
"NAME",
|
|
25
|
+
"OBJSENSE",
|
|
26
|
+
"OBJNAME",
|
|
27
|
+
"ROWS",
|
|
28
|
+
"COLUMNS",
|
|
29
|
+
"RHS",
|
|
30
|
+
"RANGES",
|
|
31
|
+
"BOUNDS",
|
|
32
|
+
"ENDATA",
|
|
33
|
+
}
|
|
34
|
+
_UNSUPPORTED_SECTIONS = {
|
|
35
|
+
"QMATRIX",
|
|
36
|
+
"QUADOBJ",
|
|
37
|
+
"QSECTION",
|
|
38
|
+
"DMATRIX",
|
|
39
|
+
"SOS",
|
|
40
|
+
"INDICATORS",
|
|
41
|
+
"GENCONS",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _number(token: str, *, line_no: int) -> float:
|
|
46
|
+
# Fortran-style D exponents are still common in old MPS corpora.
|
|
47
|
+
token = token.replace("D", "E").replace("d", "e")
|
|
48
|
+
try:
|
|
49
|
+
value = float(token)
|
|
50
|
+
except ValueError as exc:
|
|
51
|
+
raise MPSParseError(f"line {line_no}: invalid numeric value {token!r}") from exc
|
|
52
|
+
if not np.isfinite(value):
|
|
53
|
+
raise MPSParseError(f"line {line_no}: non-finite numeric value {token!r}")
|
|
54
|
+
return value
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _clean_marker(token: str) -> str:
|
|
58
|
+
return token.strip().strip("'\"").upper()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(slots=True)
|
|
62
|
+
class _MPSState:
|
|
63
|
+
name: str | None = None
|
|
64
|
+
objective_sense: ObjectiveSense = ObjectiveSense.MINIMIZE
|
|
65
|
+
objective_row: str | None = None
|
|
66
|
+
explicit_objective_row: str | None = None
|
|
67
|
+
row_types: dict[str, str] = None # type: ignore[assignment]
|
|
68
|
+
row_order: list[str] = None # type: ignore[assignment]
|
|
69
|
+
variable_order: list[str] = None # type: ignore[assignment]
|
|
70
|
+
variable_index: dict[str, int] = None # type: ignore[assignment]
|
|
71
|
+
entries: dict[tuple[str, str], float] = None # type: ignore[assignment]
|
|
72
|
+
objective: dict[str, float] = None # type: ignore[assignment]
|
|
73
|
+
rhs: dict[str, float] = None # type: ignore[assignment]
|
|
74
|
+
ranges: dict[str, float] = None # type: ignore[assignment]
|
|
75
|
+
bounds_ops: list[tuple[str, str, float | None]] = None # type: ignore[assignment]
|
|
76
|
+
integer_marked: set[str] = None # type: ignore[assignment]
|
|
77
|
+
explicit_binary: set[str] = None # type: ignore[assignment]
|
|
78
|
+
explicit_integer: set[str] = None # type: ignore[assignment]
|
|
79
|
+
active_rhs_name: str | None = None
|
|
80
|
+
active_range_name: str | None = None
|
|
81
|
+
active_bound_name: str | None = None
|
|
82
|
+
integer_mode: bool = False
|
|
83
|
+
|
|
84
|
+
def __post_init__(self) -> None:
|
|
85
|
+
self.row_types = {}
|
|
86
|
+
self.row_order = []
|
|
87
|
+
self.variable_order = []
|
|
88
|
+
self.variable_index = {}
|
|
89
|
+
self.entries = {}
|
|
90
|
+
self.objective = {}
|
|
91
|
+
self.rhs = {}
|
|
92
|
+
self.ranges = {}
|
|
93
|
+
self.bounds_ops = []
|
|
94
|
+
self.integer_marked = set()
|
|
95
|
+
self.explicit_binary = set()
|
|
96
|
+
self.explicit_integer = set()
|
|
97
|
+
|
|
98
|
+
def ensure_variable(self, name: str) -> None:
|
|
99
|
+
if name not in self.variable_index:
|
|
100
|
+
self.variable_index[name] = len(self.variable_order)
|
|
101
|
+
self.variable_order.append(name)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _iter_lines(source: Iterable[str]):
|
|
105
|
+
for line_no, raw in enumerate(source, start=1):
|
|
106
|
+
# MPS comments are indicated by '*' in column 1. Blank lines are harmless.
|
|
107
|
+
if not raw.strip() or raw.startswith("*"):
|
|
108
|
+
continue
|
|
109
|
+
yield line_no, raw.rstrip("\r\n")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _parse_stream(source: Iterable[str]) -> LinearProblem:
|
|
113
|
+
state = _MPSState()
|
|
114
|
+
section: str | None = None
|
|
115
|
+
|
|
116
|
+
for line_no, raw in _iter_lines(source):
|
|
117
|
+
stripped = raw.strip()
|
|
118
|
+
tokens = stripped.split()
|
|
119
|
+
if not tokens:
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
first_upper = tokens[0].upper()
|
|
123
|
+
# Section cards begin in column 1 in fixed MPS. Accept free-MPS section cards too.
|
|
124
|
+
is_section_card = (not raw[:1].isspace()) and (
|
|
125
|
+
first_upper in _SECTION_NAMES or first_upper in _UNSUPPORTED_SECTIONS
|
|
126
|
+
)
|
|
127
|
+
if is_section_card:
|
|
128
|
+
if first_upper in _UNSUPPORTED_SECTIONS:
|
|
129
|
+
raise MPSUnsupportedFeatureError(
|
|
130
|
+
f"line {line_no}: section {first_upper} is outside the v0.1 linear/MILP MPS reader"
|
|
131
|
+
)
|
|
132
|
+
section = first_upper
|
|
133
|
+
if section == "NAME":
|
|
134
|
+
if len(tokens) >= 2:
|
|
135
|
+
state.name = tokens[1]
|
|
136
|
+
continue
|
|
137
|
+
if section == "ENDATA":
|
|
138
|
+
break
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
if section is None:
|
|
142
|
+
raise MPSParseError(f"line {line_no}: data before first MPS section")
|
|
143
|
+
|
|
144
|
+
if section == "OBJSENSE":
|
|
145
|
+
sense = first_upper
|
|
146
|
+
if sense in {"MIN", "MINIMIZE", "MINIMUM"}:
|
|
147
|
+
state.objective_sense = ObjectiveSense.MINIMIZE
|
|
148
|
+
elif sense in {"MAX", "MAXIMIZE", "MAXIMUM"}:
|
|
149
|
+
state.objective_sense = ObjectiveSense.MAXIMIZE
|
|
150
|
+
else:
|
|
151
|
+
raise MPSParseError(f"line {line_no}: unknown OBJSENSE value {tokens[0]!r}")
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
if section == "OBJNAME":
|
|
155
|
+
state.explicit_objective_row = tokens[0]
|
|
156
|
+
continue
|
|
157
|
+
|
|
158
|
+
if section == "ROWS":
|
|
159
|
+
if len(tokens) < 2:
|
|
160
|
+
raise MPSParseError(f"line {line_no}: ROWS entry requires type and name")
|
|
161
|
+
row_type, row_name = tokens[0].upper(), tokens[1]
|
|
162
|
+
if row_type not in {"N", "E", "L", "G"}:
|
|
163
|
+
raise MPSParseError(f"line {line_no}: unsupported row type {row_type!r}")
|
|
164
|
+
if row_name in state.row_types:
|
|
165
|
+
raise MPSParseError(f"line {line_no}: duplicate row {row_name!r}")
|
|
166
|
+
state.row_types[row_name] = row_type
|
|
167
|
+
state.row_order.append(row_name)
|
|
168
|
+
if row_type == "N" and state.objective_row is None:
|
|
169
|
+
state.objective_row = row_name
|
|
170
|
+
continue
|
|
171
|
+
|
|
172
|
+
if section == "COLUMNS":
|
|
173
|
+
# Marker cards tokenize as: MARK0000 'MARKER' 'INTORG' (or INTEND).
|
|
174
|
+
if len(tokens) >= 3 and _clean_marker(tokens[1]) == "MARKER":
|
|
175
|
+
marker = _clean_marker(tokens[-1])
|
|
176
|
+
if marker == "INTORG":
|
|
177
|
+
state.integer_mode = True
|
|
178
|
+
elif marker == "INTEND":
|
|
179
|
+
state.integer_mode = False
|
|
180
|
+
else:
|
|
181
|
+
raise MPSParseError(f"line {line_no}: unknown integer marker {tokens[-1]!r}")
|
|
182
|
+
continue
|
|
183
|
+
|
|
184
|
+
if len(tokens) not in {3, 5}:
|
|
185
|
+
raise MPSParseError(
|
|
186
|
+
f"line {line_no}: COLUMNS entry must contain one or two row/value pairs"
|
|
187
|
+
)
|
|
188
|
+
var = tokens[0]
|
|
189
|
+
state.ensure_variable(var)
|
|
190
|
+
if state.integer_mode:
|
|
191
|
+
state.integer_marked.add(var)
|
|
192
|
+
pairs = [(tokens[1], tokens[2])]
|
|
193
|
+
if len(tokens) == 5:
|
|
194
|
+
pairs.append((tokens[3], tokens[4]))
|
|
195
|
+
objective_row = state.explicit_objective_row or state.objective_row
|
|
196
|
+
for row, raw_value in pairs:
|
|
197
|
+
if row not in state.row_types:
|
|
198
|
+
raise MPSParseError(f"line {line_no}: unknown row {row!r} in COLUMNS")
|
|
199
|
+
value = _number(raw_value, line_no=line_no)
|
|
200
|
+
if row == objective_row:
|
|
201
|
+
state.objective[var] = state.objective.get(var, 0.0) + value
|
|
202
|
+
elif state.row_types[row] == "N":
|
|
203
|
+
# Additional N rows are free rows; they are not constraints in the canonical IR.
|
|
204
|
+
continue
|
|
205
|
+
else:
|
|
206
|
+
key = (row, var)
|
|
207
|
+
state.entries[key] = state.entries.get(key, 0.0) + value
|
|
208
|
+
continue
|
|
209
|
+
|
|
210
|
+
if section in {"RHS", "RANGES"}:
|
|
211
|
+
if len(tokens) not in {3, 5}:
|
|
212
|
+
raise MPSParseError(
|
|
213
|
+
f"line {line_no}: {section} entry must contain one or two row/value pairs"
|
|
214
|
+
)
|
|
215
|
+
vector_name = tokens[0]
|
|
216
|
+
active_attr = "active_rhs_name" if section == "RHS" else "active_range_name"
|
|
217
|
+
active = getattr(state, active_attr)
|
|
218
|
+
if active is None:
|
|
219
|
+
setattr(state, active_attr, vector_name)
|
|
220
|
+
active = vector_name
|
|
221
|
+
if vector_name != active:
|
|
222
|
+
raise MPSUnsupportedFeatureError(
|
|
223
|
+
f"line {line_no}: multiple {section} vectors are ambiguous in canonical v0.1; "
|
|
224
|
+
f"active={active!r}, encountered={vector_name!r}"
|
|
225
|
+
)
|
|
226
|
+
pairs = [(tokens[1], tokens[2])]
|
|
227
|
+
if len(tokens) == 5:
|
|
228
|
+
pairs.append((tokens[3], tokens[4]))
|
|
229
|
+
target = state.rhs if section == "RHS" else state.ranges
|
|
230
|
+
for row, raw_value in pairs:
|
|
231
|
+
if row not in state.row_types:
|
|
232
|
+
raise MPSParseError(f"line {line_no}: unknown row {row!r} in {section}")
|
|
233
|
+
target[row] = _number(raw_value, line_no=line_no)
|
|
234
|
+
continue
|
|
235
|
+
|
|
236
|
+
if section == "BOUNDS":
|
|
237
|
+
if len(tokens) < 3:
|
|
238
|
+
raise MPSParseError(f"line {line_no}: malformed BOUNDS entry")
|
|
239
|
+
bound_type = tokens[0].upper()
|
|
240
|
+
vector_name = tokens[1]
|
|
241
|
+
var = tokens[2]
|
|
242
|
+
if state.active_bound_name is None:
|
|
243
|
+
state.active_bound_name = vector_name
|
|
244
|
+
if vector_name != state.active_bound_name:
|
|
245
|
+
raise MPSUnsupportedFeatureError(
|
|
246
|
+
f"line {line_no}: multiple BOUNDS vectors are ambiguous in canonical v0.1; "
|
|
247
|
+
f"active={state.active_bound_name!r}, encountered={vector_name!r}"
|
|
248
|
+
)
|
|
249
|
+
state.ensure_variable(var)
|
|
250
|
+
value = _number(tokens[3], line_no=line_no) if len(tokens) >= 4 else None
|
|
251
|
+
if bound_type in {"SC", "SI"}:
|
|
252
|
+
raise MPSUnsupportedFeatureError(
|
|
253
|
+
f"line {line_no}: semi-continuous/semi-integer bound {bound_type} is unsupported"
|
|
254
|
+
)
|
|
255
|
+
if bound_type not in {"LO", "UP", "FX", "FR", "MI", "PL", "BV", "LI", "UI"}:
|
|
256
|
+
raise MPSUnsupportedFeatureError(
|
|
257
|
+
f"line {line_no}: unsupported bound type {bound_type!r}"
|
|
258
|
+
)
|
|
259
|
+
if bound_type in {"LO", "UP", "FX", "LI", "UI"} and value is None:
|
|
260
|
+
raise MPSParseError(f"line {line_no}: bound type {bound_type} requires a value")
|
|
261
|
+
if bound_type == "BV":
|
|
262
|
+
state.explicit_binary.add(var)
|
|
263
|
+
if bound_type in {"LI", "UI"}:
|
|
264
|
+
state.explicit_integer.add(var)
|
|
265
|
+
state.bounds_ops.append((bound_type, var, value))
|
|
266
|
+
continue
|
|
267
|
+
|
|
268
|
+
# NAME was already handled as a section card; ENDATA exits above.
|
|
269
|
+
if section not in {"NAME"}:
|
|
270
|
+
raise MPSParseError(f"line {line_no}: unexpected data in section {section}")
|
|
271
|
+
|
|
272
|
+
if not state.row_types:
|
|
273
|
+
raise MPSParseError("MPS input contains no ROWS entries")
|
|
274
|
+
if not state.variable_order:
|
|
275
|
+
raise MPSParseError("MPS input contains no variables")
|
|
276
|
+
|
|
277
|
+
objective_row = state.explicit_objective_row or state.objective_row
|
|
278
|
+
if state.explicit_objective_row is not None:
|
|
279
|
+
if state.explicit_objective_row not in state.row_types:
|
|
280
|
+
raise MPSParseError(f"OBJNAME references unknown row {state.explicit_objective_row!r}")
|
|
281
|
+
if state.row_types[state.explicit_objective_row] != "N":
|
|
282
|
+
raise MPSParseError("OBJNAME must reference an N row")
|
|
283
|
+
|
|
284
|
+
constraint_rows = [r for r in state.row_order if state.row_types[r] != "N"]
|
|
285
|
+
row_index = {name: i for i, name in enumerate(constraint_rows)}
|
|
286
|
+
n = len(state.variable_order)
|
|
287
|
+
m = len(constraint_rows)
|
|
288
|
+
|
|
289
|
+
rr: list[int] = []
|
|
290
|
+
cc: list[int] = []
|
|
291
|
+
vv: list[float] = []
|
|
292
|
+
for (row, var), value in state.entries.items():
|
|
293
|
+
if value == 0.0:
|
|
294
|
+
continue
|
|
295
|
+
rr.append(row_index[row])
|
|
296
|
+
cc.append(state.variable_index[var])
|
|
297
|
+
vv.append(value)
|
|
298
|
+
A = sparse.coo_matrix((vv, (rr, cc)), shape=(m, n), dtype=np.float64).tocsr()
|
|
299
|
+
|
|
300
|
+
c = np.zeros(n, dtype=np.float64)
|
|
301
|
+
for var, value in state.objective.items():
|
|
302
|
+
c[state.variable_index[var]] += value
|
|
303
|
+
|
|
304
|
+
rhs = np.zeros(m, dtype=np.float64)
|
|
305
|
+
for row, value in state.rhs.items():
|
|
306
|
+
if row in row_index:
|
|
307
|
+
rhs[row_index[row]] = value
|
|
308
|
+
|
|
309
|
+
lower = np.full(m, -np.inf, dtype=np.float64)
|
|
310
|
+
upper = np.full(m, np.inf, dtype=np.float64)
|
|
311
|
+
for row, i in row_index.items():
|
|
312
|
+
b = rhs[i]
|
|
313
|
+
row_type = state.row_types[row]
|
|
314
|
+
if row_type == "E":
|
|
315
|
+
lower[i] = upper[i] = b
|
|
316
|
+
elif row_type == "L":
|
|
317
|
+
upper[i] = b
|
|
318
|
+
elif row_type == "G":
|
|
319
|
+
lower[i] = b
|
|
320
|
+
|
|
321
|
+
if row in state.ranges:
|
|
322
|
+
r = state.ranges[row]
|
|
323
|
+
width = abs(r)
|
|
324
|
+
if row_type == "G":
|
|
325
|
+
lower[i], upper[i] = b, b + width
|
|
326
|
+
elif row_type == "L":
|
|
327
|
+
lower[i], upper[i] = b - width, b
|
|
328
|
+
elif row_type == "E":
|
|
329
|
+
if r >= 0:
|
|
330
|
+
lower[i], upper[i] = b, b + width
|
|
331
|
+
else:
|
|
332
|
+
lower[i], upper[i] = b - width, b
|
|
333
|
+
|
|
334
|
+
# Standard MPS integer markers imply default bounds [0, 1] for marked variables.
|
|
335
|
+
integer_vars = state.integer_marked | state.explicit_integer | state.explicit_binary
|
|
336
|
+
vl = np.zeros(n, dtype=np.float64)
|
|
337
|
+
vu = np.full(n, np.inf, dtype=np.float64)
|
|
338
|
+
for var in state.integer_marked:
|
|
339
|
+
vu[state.variable_index[var]] = 1.0
|
|
340
|
+
|
|
341
|
+
explicit_upper = np.zeros(n, dtype=bool)
|
|
342
|
+
for bound_type, var, value in state.bounds_ops:
|
|
343
|
+
j = state.variable_index[var]
|
|
344
|
+
if bound_type == "LO":
|
|
345
|
+
if value is None:
|
|
346
|
+
raise MPSParseError("LO bound requires a numeric value")
|
|
347
|
+
vl[j] = value
|
|
348
|
+
elif bound_type == "UP":
|
|
349
|
+
if value is None:
|
|
350
|
+
raise MPSParseError("UP bound requires a numeric value")
|
|
351
|
+
vu[j] = value
|
|
352
|
+
explicit_upper[j] = True
|
|
353
|
+
# Widely used MPS interpretation: a negative UP releases the default 0 lower bound.
|
|
354
|
+
if value < 0.0 and vl[j] == 0.0:
|
|
355
|
+
vl[j] = -np.inf
|
|
356
|
+
elif bound_type == "FX":
|
|
357
|
+
if value is None:
|
|
358
|
+
raise MPSParseError("FX bound requires a numeric value")
|
|
359
|
+
vl[j] = vu[j] = value
|
|
360
|
+
explicit_upper[j] = True
|
|
361
|
+
elif bound_type == "FR":
|
|
362
|
+
vl[j], vu[j] = -np.inf, np.inf
|
|
363
|
+
explicit_upper[j] = True
|
|
364
|
+
elif bound_type == "MI":
|
|
365
|
+
vl[j] = -np.inf
|
|
366
|
+
elif bound_type == "PL":
|
|
367
|
+
vu[j] = np.inf
|
|
368
|
+
explicit_upper[j] = True
|
|
369
|
+
elif bound_type == "BV":
|
|
370
|
+
vl[j], vu[j] = 0.0, 1.0
|
|
371
|
+
explicit_upper[j] = True
|
|
372
|
+
elif bound_type == "LI":
|
|
373
|
+
if value is None:
|
|
374
|
+
raise MPSParseError("LI bound requires a numeric value")
|
|
375
|
+
vl[j] = value
|
|
376
|
+
# LI explicitly declares a general integer. If no upper bound was already
|
|
377
|
+
# supplied, it overrides the INTORG marker's implicit binary upper bound.
|
|
378
|
+
if not explicit_upper[j]:
|
|
379
|
+
vu[j] = np.inf
|
|
380
|
+
elif bound_type == "UI":
|
|
381
|
+
if value is None:
|
|
382
|
+
raise MPSParseError("UI bound requires a numeric value")
|
|
383
|
+
vu[j] = value
|
|
384
|
+
explicit_upper[j] = True
|
|
385
|
+
|
|
386
|
+
domains: list[VariableDomain] = []
|
|
387
|
+
for var in state.variable_order:
|
|
388
|
+
j = state.variable_index[var]
|
|
389
|
+
if var in state.explicit_binary or (
|
|
390
|
+
var in integer_vars and vl[j] == 0.0 and vu[j] == 1.0
|
|
391
|
+
):
|
|
392
|
+
domains.append(VariableDomain.BINARY)
|
|
393
|
+
elif var in integer_vars:
|
|
394
|
+
domains.append(VariableDomain.INTEGER)
|
|
395
|
+
else:
|
|
396
|
+
domains.append(VariableDomain.CONTINUOUS)
|
|
397
|
+
|
|
398
|
+
objective_offset = 0.0
|
|
399
|
+
if objective_row is not None and objective_row in state.rhs:
|
|
400
|
+
# In MPS the RHS value attached to the objective N-row is -c0.
|
|
401
|
+
objective_offset = -float(state.rhs[objective_row])
|
|
402
|
+
|
|
403
|
+
return LinearProblem.from_data(
|
|
404
|
+
A=A,
|
|
405
|
+
c=c,
|
|
406
|
+
variable_lower=vl,
|
|
407
|
+
variable_upper=vu,
|
|
408
|
+
constraint_lower=lower,
|
|
409
|
+
constraint_upper=upper,
|
|
410
|
+
domains=domains,
|
|
411
|
+
objective_sense=state.objective_sense,
|
|
412
|
+
objective_offset=objective_offset,
|
|
413
|
+
name=state.name,
|
|
414
|
+
metadata={
|
|
415
|
+
"source_format": "mps",
|
|
416
|
+
"variable_names": tuple(state.variable_order),
|
|
417
|
+
"constraint_names": tuple(constraint_rows),
|
|
418
|
+
"objective_row": objective_row,
|
|
419
|
+
"rhs_name": state.active_rhs_name,
|
|
420
|
+
"ranges_name": state.active_range_name,
|
|
421
|
+
"bounds_name": state.active_bound_name,
|
|
422
|
+
},
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def parse_mps(text: str) -> LinearProblem:
|
|
427
|
+
"""Parse a linear/MILP MPS document into the canonical sparse IR.
|
|
428
|
+
|
|
429
|
+
Supported v0.1 constructs: NAME, OBJSENSE, OBJNAME, ROWS, COLUMNS,
|
|
430
|
+
integer MARKER cards, RHS, RANGES, BOUNDS (LO/UP/FX/FR/MI/PL/BV/LI/UI),
|
|
431
|
+
and ENDATA. Quadratic, SOS, indicator and semi-continuous constructs are rejected
|
|
432
|
+
rather than silently lowered.
|
|
433
|
+
"""
|
|
434
|
+
|
|
435
|
+
return _parse_stream(text.splitlines())
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def read_mps(path: str | Path) -> LinearProblem:
|
|
439
|
+
"""Read ``.mps`` or ``.mps.gz`` into the canonical sparse IR."""
|
|
440
|
+
|
|
441
|
+
p = Path(path)
|
|
442
|
+
opener = gzip.open if p.suffix.lower() == ".gz" else open
|
|
443
|
+
with opener(p, "rt", encoding="ascii", errors="strict") as f: # type: ignore[arg-type]
|
|
444
|
+
problem = _parse_stream(f)
|
|
445
|
+
metadata = dict(problem.metadata)
|
|
446
|
+
metadata["source_path"] = str(p)
|
|
447
|
+
return replace(problem, metadata=metadata)
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Sequence
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from scipy import sparse
|
|
8
|
+
from scipy.sparse.linalg import ArpackError, ArpackNoConvergence, eigsh
|
|
9
|
+
|
|
10
|
+
from .enums import ConvexityStatus, ObjectiveSense, VariableDomain
|
|
11
|
+
from .hashing import _hash_parts, hash_sparse_matrix
|
|
12
|
+
from .linear import LinearProblem, _readonly_csr
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _canonicalize_hessian(
|
|
16
|
+
P: sparse.csr_matrix,
|
|
17
|
+
*,
|
|
18
|
+
atol: float = 1e-12,
|
|
19
|
+
rtol: float = 1e-12,
|
|
20
|
+
) -> sparse.csr_matrix:
|
|
21
|
+
"""Return the single symmetric Hessian used by every SolverPilot component.
|
|
22
|
+
|
|
23
|
+
Symmetry is checked *pairwise*. An unrelated large coefficient is therefore
|
|
24
|
+
unable to relax the tolerance for a small asymmetric pair. Once accepted,
|
|
25
|
+
the Hessian is explicitly symmetrized so hash/objective/backend semantics all
|
|
26
|
+
consume the same matrix.
|
|
27
|
+
"""
|
|
28
|
+
diff = (P - P.T).tocoo()
|
|
29
|
+
if diff.nnz == 0:
|
|
30
|
+
return _readonly_csr(P)
|
|
31
|
+
if diff.nnz:
|
|
32
|
+
rows = diff.row
|
|
33
|
+
cols = diff.col
|
|
34
|
+
pij = np.asarray(P[rows, cols]).reshape(-1)
|
|
35
|
+
pji = np.asarray(P[cols, rows]).reshape(-1)
|
|
36
|
+
scale = np.maximum.reduce(
|
|
37
|
+
[np.ones(diff.nnz, dtype=np.float64), np.abs(pij), np.abs(pji)]
|
|
38
|
+
)
|
|
39
|
+
allowed = float(atol) + float(rtol) * scale
|
|
40
|
+
if np.any(np.abs(diff.data) > allowed):
|
|
41
|
+
raise ValueError("P must be symmetric within pairwise numerical tolerance")
|
|
42
|
+
return _readonly_csr((P * 0.5 + P.T * 0.5).tocsr())
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _scaled_psd_matrix(P: sparse.csr_matrix) -> sparse.csr_matrix:
|
|
46
|
+
"""Diagonal congruence scaling that preserves positive semidefiniteness.
|
|
47
|
+
|
|
48
|
+
Scaling each row/column by its own magnitude prevents a huge, unrelated
|
|
49
|
+
coefficient from masking a materially negative eigenvalue elsewhere.
|
|
50
|
+
"""
|
|
51
|
+
if P.shape[0] == 0:
|
|
52
|
+
return P
|
|
53
|
+
row_max = np.asarray(abs(P).max(axis=1).toarray(), dtype=np.float64).reshape(-1)
|
|
54
|
+
row_max = np.maximum(1.0, row_max)
|
|
55
|
+
inv_sqrt = 1.0 / np.sqrt(row_max)
|
|
56
|
+
D = sparse.diags(inv_sqrt, offsets=0, format="csr")
|
|
57
|
+
scaled = (D @ P @ D).tocsr()
|
|
58
|
+
scaled.sum_duplicates()
|
|
59
|
+
scaled.eliminate_zeros()
|
|
60
|
+
scaled.sort_indices()
|
|
61
|
+
return scaled
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _check_convexity(
|
|
65
|
+
P: sparse.csr_matrix,
|
|
66
|
+
*,
|
|
67
|
+
atol: float = 1e-10,
|
|
68
|
+
rtol: float = 1e-9,
|
|
69
|
+
) -> ConvexityStatus:
|
|
70
|
+
n = P.shape[0]
|
|
71
|
+
if n == 0 or P.nnz == 0:
|
|
72
|
+
return ConvexityStatus.CONFIRMED
|
|
73
|
+
try:
|
|
74
|
+
sym = _canonicalize_hessian(P)
|
|
75
|
+
except ValueError:
|
|
76
|
+
return ConvexityStatus.REJECTED
|
|
77
|
+
scaled = _scaled_psd_matrix(sym)
|
|
78
|
+
row_sums = np.asarray(abs(scaled).sum(axis=1), dtype=np.float64).reshape(-1)
|
|
79
|
+
scale = max(1.0, float(np.max(row_sums)) if row_sums.size else 0.0)
|
|
80
|
+
# The matrix has already undergone a PSD-preserving congruence scaling. The
|
|
81
|
+
# remaining tolerance is therefore about the local normalized matrix, not the
|
|
82
|
+
# largest raw coefficient in the original problem.
|
|
83
|
+
tol = float(atol) + float(rtol) * scale
|
|
84
|
+
try:
|
|
85
|
+
if n <= 256:
|
|
86
|
+
lam_min = float(np.linalg.eigvalsh(scaled.toarray())[0])
|
|
87
|
+
else:
|
|
88
|
+
lam_min = float(eigsh(scaled, k=1, which="SA", return_eigenvectors=False)[0])
|
|
89
|
+
except (np.linalg.LinAlgError, ArpackError, ArpackNoConvergence):
|
|
90
|
+
return ConvexityStatus.UNKNOWN
|
|
91
|
+
return ConvexityStatus.CONFIRMED if lam_min >= -tol else ConvexityStatus.REJECTED
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def require_confirmed_convexity(problem: "QuadraticProblem") -> None:
|
|
95
|
+
"""Fail closed before any backend consumes a quadratic problem directly."""
|
|
96
|
+
if problem.convexity_status is not ConvexityStatus.CONFIRMED:
|
|
97
|
+
raise ValueError("unverified quadratic problem cannot be solved as convex_qp")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(frozen=True, slots=True)
|
|
101
|
+
class QuadraticProblem:
|
|
102
|
+
linear: LinearProblem
|
|
103
|
+
P: sparse.csr_matrix
|
|
104
|
+
convexity_status: ConvexityStatus = ConvexityStatus.UNKNOWN
|
|
105
|
+
metadata: dict[str, object] = field(default_factory=dict, compare=False, repr=False)
|
|
106
|
+
structural_hash: str = field(init=False)
|
|
107
|
+
data_hash: str = field(init=False)
|
|
108
|
+
|
|
109
|
+
def __post_init__(self) -> None:
|
|
110
|
+
if self.linear.objective_sense is not ObjectiveSense.MINIMIZE:
|
|
111
|
+
raise ValueError("v0.1 QuadraticProblem currently supports convex minimization only")
|
|
112
|
+
if self.linear.has_integer_variables:
|
|
113
|
+
raise ValueError("v0.1 QuadraticProblem supports continuous convex QP only")
|
|
114
|
+
P_raw = _readonly_csr(self.P)
|
|
115
|
+
n = self.linear.n_variables
|
|
116
|
+
if P_raw.shape != (n, n):
|
|
117
|
+
raise ValueError(f"P must have shape ({n}, {n}), got {P_raw.shape}")
|
|
118
|
+
P = _canonicalize_hessian(P_raw)
|
|
119
|
+
status = ConvexityStatus(self.convexity_status)
|
|
120
|
+
if status is ConvexityStatus.REJECTED:
|
|
121
|
+
raise ValueError("nonconvex quadratic objectives are out of scope for v0.1")
|
|
122
|
+
if status is ConvexityStatus.CONFIRMED:
|
|
123
|
+
checked = _check_convexity(P)
|
|
124
|
+
if checked is not ConvexityStatus.CONFIRMED:
|
|
125
|
+
raise ValueError("P is not independently confirmed positive semidefinite")
|
|
126
|
+
object.__setattr__(self, "P", P)
|
|
127
|
+
object.__setattr__(self, "convexity_status", status)
|
|
128
|
+
object.__setattr__(self, "metadata", dict(self.metadata))
|
|
129
|
+
object.__setattr__(
|
|
130
|
+
self,
|
|
131
|
+
"structural_hash",
|
|
132
|
+
_hash_parts([self.linear.structural_hash.encode(), hash_sparse_matrix(P.astype(bool)).encode()]),
|
|
133
|
+
)
|
|
134
|
+
object.__setattr__(
|
|
135
|
+
self,
|
|
136
|
+
"data_hash",
|
|
137
|
+
_hash_parts([self.linear.data_hash.encode(), hash_sparse_matrix(P).encode()]),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def n_variables(self) -> int:
|
|
142
|
+
return self.linear.n_variables
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def n_constraints(self) -> int:
|
|
146
|
+
return self.linear.n_constraints
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def objective_sense(self) -> ObjectiveSense:
|
|
150
|
+
"""Expose the common problem-level objective sense used by runtime code."""
|
|
151
|
+
return self.linear.objective_sense
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def objective_offset(self) -> float:
|
|
155
|
+
return float(self.linear.objective_offset)
|
|
156
|
+
|
|
157
|
+
@classmethod
|
|
158
|
+
def from_data(
|
|
159
|
+
cls,
|
|
160
|
+
*,
|
|
161
|
+
P: sparse.spmatrix | np.ndarray,
|
|
162
|
+
A: sparse.spmatrix | np.ndarray,
|
|
163
|
+
q: Sequence[float] | np.ndarray,
|
|
164
|
+
variable_lower: Sequence[float] | np.ndarray,
|
|
165
|
+
variable_upper: Sequence[float] | np.ndarray,
|
|
166
|
+
constraint_lower: Sequence[float] | np.ndarray,
|
|
167
|
+
constraint_upper: Sequence[float] | np.ndarray,
|
|
168
|
+
objective_sense: ObjectiveSense | str = ObjectiveSense.MINIMIZE,
|
|
169
|
+
objective_offset: float = 0.0,
|
|
170
|
+
verify_convexity: bool = True,
|
|
171
|
+
metadata: dict[str, object] | None = None,
|
|
172
|
+
) -> "QuadraticProblem":
|
|
173
|
+
sense = ObjectiveSense(objective_sense)
|
|
174
|
+
if sense is ObjectiveSense.MAXIMIZE:
|
|
175
|
+
raise ValueError("v0.1 QuadraticProblem currently supports convex minimization only")
|
|
176
|
+
linear = LinearProblem.from_data(
|
|
177
|
+
A=A,
|
|
178
|
+
c=q,
|
|
179
|
+
variable_lower=variable_lower,
|
|
180
|
+
variable_upper=variable_upper,
|
|
181
|
+
constraint_lower=constraint_lower,
|
|
182
|
+
constraint_upper=constraint_upper,
|
|
183
|
+
domains=[VariableDomain.CONTINUOUS] * sparse.csr_matrix(A).shape[1],
|
|
184
|
+
objective_sense=sense,
|
|
185
|
+
objective_offset=objective_offset,
|
|
186
|
+
metadata={} if metadata is None else metadata,
|
|
187
|
+
)
|
|
188
|
+
P_csr = _canonicalize_hessian(_readonly_csr(P))
|
|
189
|
+
status = _check_convexity(P_csr) if verify_convexity else ConvexityStatus.UNKNOWN
|
|
190
|
+
if status is ConvexityStatus.REJECTED:
|
|
191
|
+
raise ValueError("P is not positive semidefinite within scale-aware tolerance")
|
|
192
|
+
return cls(linear=linear, P=P_csr, convexity_status=status, metadata={} if metadata is None else metadata)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Structured, claim-safe explanations for SolverPilot solve results.
|
|
2
|
+
|
|
3
|
+
This package is intentionally not re-exported from the frozen top-level ``solverpilot``
|
|
4
|
+
namespace. It explains existing ``SolveResult`` evidence without changing solve behavior.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .explain import explain_result, explain_status
|
|
8
|
+
from .model import (
|
|
9
|
+
REPORT_SCHEMA_VERSION,
|
|
10
|
+
ClaimDisposition,
|
|
11
|
+
ClaimKind,
|
|
12
|
+
ExplanationClaim,
|
|
13
|
+
ExplanationReport,
|
|
14
|
+
)
|
|
15
|
+
from .render import render_json, render_markdown
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"REPORT_SCHEMA_VERSION",
|
|
19
|
+
"ClaimDisposition",
|
|
20
|
+
"ClaimKind",
|
|
21
|
+
"ExplanationClaim",
|
|
22
|
+
"ExplanationReport",
|
|
23
|
+
"explain_result",
|
|
24
|
+
"explain_status",
|
|
25
|
+
"render_json",
|
|
26
|
+
"render_markdown",
|
|
27
|
+
]
|