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,142 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, asdict
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from solverpilot.evaluation import parse_miplib_solu, parse_test_manifest
|
|
8
|
+
from .acquire import sha256_file
|
|
9
|
+
|
|
10
|
+
_SCIENTIFIC_PROVENANCE_LEVELS = {"VERIFIED_FROZEN", "PINNED_EXTERNAL"}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _valid_sha256(value: object) -> bool:
|
|
14
|
+
text = str(value or "")
|
|
15
|
+
return len(text) == 64 and all(ch in "0123456789abcdefABCDEF" for ch in text)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True, slots=True)
|
|
19
|
+
class DatasetVerification:
|
|
20
|
+
ok: bool
|
|
21
|
+
target_dir: str
|
|
22
|
+
file_hashes_ok: bool
|
|
23
|
+
lock_integrity_ok: bool
|
|
24
|
+
manifest_entries: int | None
|
|
25
|
+
reference_records: int | None
|
|
26
|
+
instances_present: int | None
|
|
27
|
+
instances_missing: tuple[str, ...]
|
|
28
|
+
provenance_level: str
|
|
29
|
+
all_remote_inputs_pinned: bool
|
|
30
|
+
unpinned_roles: tuple[str, ...]
|
|
31
|
+
scientific_provenance_verified: bool
|
|
32
|
+
errors: tuple[str, ...]
|
|
33
|
+
|
|
34
|
+
def to_dict(self) -> dict:
|
|
35
|
+
return asdict(self)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def verify_acquired_dataset(target_dir: str | Path) -> DatasetVerification:
|
|
39
|
+
target = Path(target_dir)
|
|
40
|
+
lock_path = target / "acquisition-lock.json"
|
|
41
|
+
if not lock_path.exists():
|
|
42
|
+
raise FileNotFoundError(lock_path)
|
|
43
|
+
|
|
44
|
+
errors: list[str] = []
|
|
45
|
+
lock_integrity_ok = True
|
|
46
|
+
lock_sha_path = target / "acquisition-lock.sha256"
|
|
47
|
+
if lock_sha_path.exists():
|
|
48
|
+
parts = lock_sha_path.read_text(encoding="utf-8").strip().split()
|
|
49
|
+
expected_lock_sha = parts[0] if parts else ""
|
|
50
|
+
if not _valid_sha256(expected_lock_sha) or sha256_file(lock_path).lower() != expected_lock_sha.lower():
|
|
51
|
+
lock_integrity_ok = False
|
|
52
|
+
errors.append("acquisition lock checksum mismatch")
|
|
53
|
+
|
|
54
|
+
lock = json.loads(lock_path.read_text(encoding="utf-8"))
|
|
55
|
+
lock_schema_version = str(lock.get("schema_version") or "legacy")
|
|
56
|
+
if lock_schema_version == "2.0" and not lock_sha_path.exists():
|
|
57
|
+
lock_integrity_ok = False
|
|
58
|
+
errors.append("schema 2.0 acquisition lock is missing acquisition-lock.sha256")
|
|
59
|
+
if lock_schema_version == "2.0" and not _valid_sha256(lock.get("spec_fingerprint")):
|
|
60
|
+
errors.append("schema 2.0 acquisition lock has invalid spec_fingerprint")
|
|
61
|
+
|
|
62
|
+
provenance_level = str(lock.get("provenance_level") or "UNSPECIFIED").strip().upper()
|
|
63
|
+
hashes_ok = True
|
|
64
|
+
role_paths: dict[str, Path] = {}
|
|
65
|
+
observed_unpinned: list[str] = []
|
|
66
|
+
files = lock.get("files", [])
|
|
67
|
+
if not isinstance(files, list):
|
|
68
|
+
raise ValueError("acquisition lock files must be a list")
|
|
69
|
+
|
|
70
|
+
for item in files:
|
|
71
|
+
if not isinstance(item, dict):
|
|
72
|
+
hashes_ok = False; errors.append("invalid acquisition file record"); continue
|
|
73
|
+
role = str(item.get("role") or "")
|
|
74
|
+
path = Path(str(item.get("path") or ""))
|
|
75
|
+
if role: role_paths[role] = path
|
|
76
|
+
expected = item.get("expected_sha256")
|
|
77
|
+
if expected is None:
|
|
78
|
+
if role: observed_unpinned.append(role)
|
|
79
|
+
elif not _valid_sha256(expected):
|
|
80
|
+
hashes_ok = False; errors.append(f"invalid expected SHA-256 for role {role!r}")
|
|
81
|
+
if not path.exists():
|
|
82
|
+
hashes_ok = False; errors.append(f"missing acquired file: {path}"); continue
|
|
83
|
+
observed = sha256_file(path); recorded = item.get("sha256")
|
|
84
|
+
if not _valid_sha256(recorded) or observed.lower() != str(recorded).lower():
|
|
85
|
+
hashes_ok = False; errors.append(f"hash mismatch: {path}")
|
|
86
|
+
if expected is not None and observed.lower() != str(expected).lower():
|
|
87
|
+
hashes_ok = False; errors.append(f"expected hash mismatch: {path}")
|
|
88
|
+
if expected is not None and item.get("verified") is not True:
|
|
89
|
+
hashes_ok = False; errors.append(f"pinned role {role!r} was not recorded as verified")
|
|
90
|
+
if expected is None and item.get("verified") is True:
|
|
91
|
+
hashes_ok = False; errors.append(f"unpinned role {role!r} cannot be recorded as verified")
|
|
92
|
+
|
|
93
|
+
observed_unpinned_tuple = tuple(sorted(set(observed_unpinned)))
|
|
94
|
+
computed_all_pinned = bool(files) and not observed_unpinned_tuple
|
|
95
|
+
if lock_schema_version == "2.0":
|
|
96
|
+
recorded_unpinned = tuple(sorted(set(str(x) for x in lock.get("unpinned_roles", ()))))
|
|
97
|
+
recorded_all_pinned = bool(lock.get("all_remote_inputs_pinned", False))
|
|
98
|
+
if recorded_unpinned != observed_unpinned_tuple:
|
|
99
|
+
errors.append(f"acquisition provenance mismatch: recorded unpinned_roles={recorded_unpinned}, observed={observed_unpinned_tuple}")
|
|
100
|
+
if recorded_all_pinned != computed_all_pinned:
|
|
101
|
+
errors.append("acquisition provenance mismatch: all_remote_inputs_pinned does not match file records")
|
|
102
|
+
|
|
103
|
+
manifest_entries = reference_records = present = None
|
|
104
|
+
missing: list[str] = []
|
|
105
|
+
manifest = role_paths.get("manifest")
|
|
106
|
+
if manifest is not None and manifest.exists():
|
|
107
|
+
try:
|
|
108
|
+
filenames = parse_test_manifest(manifest.read_text(encoding="utf-8")); manifest_entries = len(filenames)
|
|
109
|
+
expected = lock.get("metadata", {}).get("benchmark_instances")
|
|
110
|
+
if expected is not None and manifest_entries != int(expected): errors.append(f"manifest count {manifest_entries} != expected {expected}")
|
|
111
|
+
instance_root = target / "instances"
|
|
112
|
+
if instance_root.exists():
|
|
113
|
+
for filename in filenames:
|
|
114
|
+
direct = instance_root / filename
|
|
115
|
+
if direct.exists(): continue
|
|
116
|
+
matches = list(instance_root.rglob(filename))
|
|
117
|
+
if len(matches) != 1: missing.append(filename)
|
|
118
|
+
present = manifest_entries - len(missing)
|
|
119
|
+
except Exception as exc:
|
|
120
|
+
errors.append(f"manifest verification failed: {type(exc).__name__}: {exc}")
|
|
121
|
+
if manifest is None and str(lock.get("spec_kind")) == "qplib":
|
|
122
|
+
instance_root = target / "instances"
|
|
123
|
+
if instance_root.exists():
|
|
124
|
+
present = len(list(instance_root.rglob("*.qplib")))
|
|
125
|
+
metadata = lock.get("metadata", {})
|
|
126
|
+
if metadata.get("continuous_instances") is not None and metadata.get("discrete_instances") is not None:
|
|
127
|
+
expected = int(metadata["continuous_instances"]) + int(metadata["discrete_instances"])
|
|
128
|
+
if present != expected: errors.append(f"QPLIB extracted count {present} != expected {expected}")
|
|
129
|
+
reference = role_paths.get("reference")
|
|
130
|
+
if reference is not None and reference.exists():
|
|
131
|
+
try:
|
|
132
|
+
refs = parse_miplib_solu(reference.read_text(encoding="utf-8")); reference_records = len(refs)
|
|
133
|
+
expected = lock.get("metadata", {}).get("solution_records")
|
|
134
|
+
if expected is not None and reference_records != int(expected): errors.append(f"reference record count {reference_records} != expected {expected}")
|
|
135
|
+
except Exception as exc:
|
|
136
|
+
errors.append(f"reference verification failed: {type(exc).__name__}: {exc}")
|
|
137
|
+
|
|
138
|
+
local_ok = lock_integrity_ok and hashes_ok and not errors and not missing
|
|
139
|
+
scientific = local_ok and computed_all_pinned and provenance_level in _SCIENTIFIC_PROVENANCE_LEVELS
|
|
140
|
+
return DatasetVerification(local_ok, str(target.resolve()), hashes_ok, lock_integrity_ok, manifest_entries,
|
|
141
|
+
reference_records, present, tuple(missing), provenance_level, computed_all_pinned,
|
|
142
|
+
observed_unpinned_tuple, scientific, tuple(errors))
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from time import perf_counter
|
|
6
|
+
|
|
7
|
+
from solverpilot import SolveBudget, read_mps, solve
|
|
8
|
+
|
|
9
|
+
from .registry import benchmark_registry
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _trace_payload(trace) -> dict[str, object]:
|
|
13
|
+
total = float(trace.timings.total_s)
|
|
14
|
+
solve_s = float(trace.timings.solve_s)
|
|
15
|
+
return {
|
|
16
|
+
"backend_version": trace.backend_version,
|
|
17
|
+
"inspect_s": float(trace.timings.inspect_s),
|
|
18
|
+
"plan_s": float(trace.timings.plan_s),
|
|
19
|
+
"backend_build_s": float(trace.timings.backend_build_s),
|
|
20
|
+
"solve_s": solve_s,
|
|
21
|
+
"validate_s": float(trace.timings.validate_s),
|
|
22
|
+
"diagnose_s": float(trace.timings.diagnose_s),
|
|
23
|
+
"total_s": total,
|
|
24
|
+
"runtime_overhead_s": max(0.0, total - solve_s),
|
|
25
|
+
"reuse_applied": trace.reuse_applied,
|
|
26
|
+
"reuse_mode": trace.reuse_mode,
|
|
27
|
+
"planner_selected_backend": trace.planner_selected_backend,
|
|
28
|
+
"planner_evidence_level": trace.planner_evidence_level,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def solve_file_worker(
|
|
33
|
+
*,
|
|
34
|
+
path: str | Path,
|
|
35
|
+
backend: str,
|
|
36
|
+
portfolio: tuple[str, ...],
|
|
37
|
+
time_limit_s: float | None,
|
|
38
|
+
solver_threads: int | None = None,
|
|
39
|
+
) -> dict:
|
|
40
|
+
parse_t0 = perf_counter()
|
|
41
|
+
problem = read_mps(path)
|
|
42
|
+
parse_s = perf_counter() - parse_t0
|
|
43
|
+
registry = benchmark_registry(portfolio)
|
|
44
|
+
solve_t0 = perf_counter()
|
|
45
|
+
budget = None
|
|
46
|
+
if time_limit_s is not None or solver_threads is not None:
|
|
47
|
+
budget = SolveBudget(wall_time_s=time_limit_s, threads=solver_threads)
|
|
48
|
+
result = solve(
|
|
49
|
+
problem,
|
|
50
|
+
registry=registry,
|
|
51
|
+
backend=None if backend == "@auto" else backend,
|
|
52
|
+
budget=budget,
|
|
53
|
+
)
|
|
54
|
+
controller_s = perf_counter() - solve_t0
|
|
55
|
+
evidence = result.optimality_evidence
|
|
56
|
+
return {
|
|
57
|
+
"parse_s": parse_s,
|
|
58
|
+
"worker_solve_wall_s": controller_s,
|
|
59
|
+
"public_status": result.status.value,
|
|
60
|
+
"objective": result.objective,
|
|
61
|
+
"validated": None if result.validation is None else result.validation.valid,
|
|
62
|
+
"optimality_evidence": {
|
|
63
|
+
"backend_reported_optimal": evidence.backend_reported_optimal,
|
|
64
|
+
"primal_validated": evidence.primal_validated,
|
|
65
|
+
"dual_verified": evidence.dual_verified,
|
|
66
|
+
"gap_verified": evidence.gap_verified,
|
|
67
|
+
"certificate_verified": evidence.certificate_verified,
|
|
68
|
+
"independently_verified_optimal": evidence.independently_verified_optimal,
|
|
69
|
+
},
|
|
70
|
+
"trace": _trace_payload(result.trace),
|
|
71
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .engine import BridgePathDecision, choose_indicator_path
|
|
2
|
+
from .indicator import AffineScalar, BridgeUnavailable, IndicatorLowering, lower_indicator_big_m
|
|
3
|
+
from .registry import BridgeCandidate, BridgeRegistry, BridgeSpec, default_bridge_registry
|
|
4
|
+
from .types import (
|
|
5
|
+
BridgePolicy,
|
|
6
|
+
ExactnessClass,
|
|
7
|
+
MappingAvailability,
|
|
8
|
+
PreconditionCertificate,
|
|
9
|
+
TransformationStep,
|
|
10
|
+
TransformationTape,
|
|
11
|
+
)
|
|
12
|
+
from .validation import SemanticPrimalValidation, validate_semantic_primal
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"AffineScalar", "BridgeCandidate", "BridgePathDecision", "BridgePolicy", "BridgeRegistry", "BridgeSpec", "BridgeUnavailable", "ExactnessClass",
|
|
16
|
+
"IndicatorLowering", "MappingAvailability", "PreconditionCertificate", "SemanticPrimalValidation",
|
|
17
|
+
"TransformationStep", "TransformationTape", "choose_indicator_path", "default_bridge_registry", "lower_indicator_big_m",
|
|
18
|
+
"validate_semantic_primal",
|
|
19
|
+
]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from solverpilot.capabilities.v2 import BackendCapabilityManifestV2, CapabilityKey
|
|
6
|
+
|
|
7
|
+
from .registry import default_bridge_registry
|
|
8
|
+
from .types import BridgePolicy, ExactnessClass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class BridgePathDecision:
|
|
13
|
+
semantic_kind: str
|
|
14
|
+
selected_path: str
|
|
15
|
+
native_usable: bool
|
|
16
|
+
considered_paths: tuple[str, ...] = ()
|
|
17
|
+
rejected_paths: tuple[str, ...] = ()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def choose_indicator_path(
|
|
21
|
+
capabilities: BackendCapabilityManifestV2 | None,
|
|
22
|
+
*,
|
|
23
|
+
native_transport_available: bool,
|
|
24
|
+
policy: BridgePolicy,
|
|
25
|
+
) -> BridgePathDecision:
|
|
26
|
+
specs = default_bridge_registry().candidates("indicator", policy=policy)
|
|
27
|
+
considered = tuple(spec.bridge_id for spec in specs)
|
|
28
|
+
native_usable = False
|
|
29
|
+
rejected: list[str] = []
|
|
30
|
+
if capabilities is not None:
|
|
31
|
+
check = capabilities.check(CapabilityKey.CONSTRAINT_INDICATOR)
|
|
32
|
+
native_usable = check.usable
|
|
33
|
+
if check.usable and not native_transport_available:
|
|
34
|
+
rejected.append("native indicator capability verified but current execution IR has no native-indicator transport")
|
|
35
|
+
elif not check.usable:
|
|
36
|
+
rejected.append(f"native indicator path unavailable: {check.reason}")
|
|
37
|
+
else:
|
|
38
|
+
rejected.append("native indicator path unavailable: no backend capability manifest supplied")
|
|
39
|
+
|
|
40
|
+
if native_usable and native_transport_available:
|
|
41
|
+
return BridgePathDecision("indicator", "native", True, considered, tuple(rejected))
|
|
42
|
+
if not policy.permits(ExactnessClass.EXACT_EQUIVALENT):
|
|
43
|
+
raise RuntimeError("bridge policy rejects exact indicator bridge")
|
|
44
|
+
return BridgePathDecision("indicator", "certified_big_m", native_usable, considered, tuple(rejected))
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
import math
|
|
5
|
+
from typing import Mapping
|
|
6
|
+
|
|
7
|
+
from solverpilot.problem import VariableDomain
|
|
8
|
+
|
|
9
|
+
from .types import (
|
|
10
|
+
BridgePolicy,
|
|
11
|
+
ExactnessClass,
|
|
12
|
+
MappingAvailability,
|
|
13
|
+
PreconditionCertificate,
|
|
14
|
+
TransformationStep,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BridgeUnavailable(RuntimeError):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True, slots=True)
|
|
23
|
+
class AffineScalar:
|
|
24
|
+
constant: float
|
|
25
|
+
linear: Mapping[int, float]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True, slots=True)
|
|
29
|
+
class IndicatorLowering:
|
|
30
|
+
rows: tuple[dict[int, float], ...]
|
|
31
|
+
lower: tuple[float, ...]
|
|
32
|
+
upper: tuple[float, ...]
|
|
33
|
+
step: TransformationStep
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _affine_range(form: AffineScalar, lower, upper) -> tuple[float, float, list[dict[str, float | int]]]:
|
|
37
|
+
lo = float(form.constant)
|
|
38
|
+
hi = float(form.constant)
|
|
39
|
+
terms: list[dict[str, float | int]] = []
|
|
40
|
+
for index, coefficient in sorted(form.linear.items()):
|
|
41
|
+
a = float(coefficient)
|
|
42
|
+
if a == 0.0:
|
|
43
|
+
continue
|
|
44
|
+
v_lo = float(lower[index])
|
|
45
|
+
v_hi = float(upper[index])
|
|
46
|
+
need_lo = v_lo if a >= 0 else v_hi
|
|
47
|
+
need_hi = v_hi if a >= 0 else v_lo
|
|
48
|
+
if not math.isfinite(need_lo) or not math.isfinite(need_hi):
|
|
49
|
+
raise BridgeUnavailable(
|
|
50
|
+
f"cannot certify finite Big-M: variable index {index} has an infinite bound needed by affine range"
|
|
51
|
+
)
|
|
52
|
+
lo += a * need_lo
|
|
53
|
+
hi += a * need_hi
|
|
54
|
+
terms.append({"index": int(index), "coefficient": a, "lower": v_lo, "upper": v_hi})
|
|
55
|
+
return lo, hi, terms
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _upper_row(form: AffineScalar, *, rhs: float, z_index: int, active_value: int, M: float):
|
|
59
|
+
coeffs = dict(form.linear)
|
|
60
|
+
if active_value == 1:
|
|
61
|
+
coeffs[z_index] = coeffs.get(z_index, 0.0) + M
|
|
62
|
+
upper = rhs + M - form.constant
|
|
63
|
+
else:
|
|
64
|
+
coeffs[z_index] = coeffs.get(z_index, 0.0) - M
|
|
65
|
+
upper = rhs - form.constant
|
|
66
|
+
return coeffs, -math.inf, float(upper)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _lower_row(form: AffineScalar, *, rhs: float, z_index: int, active_value: int, M: float):
|
|
70
|
+
coeffs = dict(form.linear)
|
|
71
|
+
if active_value == 1:
|
|
72
|
+
coeffs[z_index] = coeffs.get(z_index, 0.0) - M
|
|
73
|
+
lower = rhs - M - form.constant
|
|
74
|
+
else:
|
|
75
|
+
coeffs[z_index] = coeffs.get(z_index, 0.0) + M
|
|
76
|
+
lower = rhs - form.constant
|
|
77
|
+
return coeffs, float(lower), math.inf
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def lower_indicator_big_m(
|
|
81
|
+
*,
|
|
82
|
+
source_id: str,
|
|
83
|
+
indicator_index: int,
|
|
84
|
+
active_value: int,
|
|
85
|
+
indicator_domain: VariableDomain,
|
|
86
|
+
indicator_lower: float,
|
|
87
|
+
indicator_upper: float,
|
|
88
|
+
body: AffineScalar,
|
|
89
|
+
body_set,
|
|
90
|
+
variable_lower,
|
|
91
|
+
variable_upper,
|
|
92
|
+
parameter_dependencies: tuple[str, ...],
|
|
93
|
+
policy: BridgePolicy,
|
|
94
|
+
) -> IndicatorLowering:
|
|
95
|
+
if active_value not in {0, 1}:
|
|
96
|
+
raise BridgeUnavailable("indicator active value must be 0 or 1")
|
|
97
|
+
if indicator_domain is not VariableDomain.BINARY:
|
|
98
|
+
raise BridgeUnavailable("indicator variable must be binary")
|
|
99
|
+
if indicator_lower < 0.0 or indicator_upper > 1.0:
|
|
100
|
+
raise BridgeUnavailable("indicator variable bounds must lie within [0, 1]")
|
|
101
|
+
|
|
102
|
+
# Exact simplification when the premise is fixed by variable bounds.
|
|
103
|
+
fixed = indicator_lower == indicator_upper and indicator_lower in {0.0, 1.0}
|
|
104
|
+
if fixed:
|
|
105
|
+
from solverpilot.model.sets import EqualTo, GreaterThan, Interval, LessThan
|
|
106
|
+
active = int(indicator_lower) == active_value
|
|
107
|
+
if not active:
|
|
108
|
+
cert = PreconditionCertificate(
|
|
109
|
+
"fixed-indicator-inactive",
|
|
110
|
+
{"indicator_index": indicator_index, "fixed_value": int(indicator_lower), "active_value": active_value},
|
|
111
|
+
)
|
|
112
|
+
step = TransformationStep(
|
|
113
|
+
step_id=f"{source_id}:indicator-fixed-inactive",
|
|
114
|
+
bridge_id="solverpilot.bridge.indicator.fixed",
|
|
115
|
+
bridge_version="1.0",
|
|
116
|
+
source_entity_ids=(source_id,),
|
|
117
|
+
generated_target_ids=(),
|
|
118
|
+
exactness=ExactnessClass.EXACT_EQUIVALENT,
|
|
119
|
+
preconditions=(cert,),
|
|
120
|
+
primal_mapping=MappingAvailability.IDENTITY,
|
|
121
|
+
dual_mapping=MappingAvailability.UNAVAILABLE,
|
|
122
|
+
certificate_mapping=MappingAvailability.UNAVAILABLE,
|
|
123
|
+
mutation_compatibility=("parameter_values",),
|
|
124
|
+
size_delta={"variables": 0, "constraints": -1},
|
|
125
|
+
numerical_risk={"class": "none", "reason": "no Big-M introduced"},
|
|
126
|
+
notes=("premise fixed false; semantic indicator is redundant",),
|
|
127
|
+
)
|
|
128
|
+
return IndicatorLowering((), (), (), step)
|
|
129
|
+
|
|
130
|
+
rows: list[dict[int, float]] = []
|
|
131
|
+
lo: list[float] = []
|
|
132
|
+
hi: list[float] = []
|
|
133
|
+
if isinstance(body_set, LessThan):
|
|
134
|
+
rows.append(dict(body.linear)); lo.append(-math.inf); hi.append(body_set.upper - body.constant)
|
|
135
|
+
elif isinstance(body_set, GreaterThan):
|
|
136
|
+
rows.append(dict(body.linear)); lo.append(body_set.lower - body.constant); hi.append(math.inf)
|
|
137
|
+
elif isinstance(body_set, EqualTo):
|
|
138
|
+
rows.append(dict(body.linear)); rhs = body_set.value - body.constant; lo.append(rhs); hi.append(rhs)
|
|
139
|
+
elif isinstance(body_set, Interval):
|
|
140
|
+
rows.append(dict(body.linear)); lo.append(body_set.lower - body.constant); hi.append(body_set.upper - body.constant)
|
|
141
|
+
else:
|
|
142
|
+
raise BridgeUnavailable(f"unsupported indicator body set {type(body_set).__name__}")
|
|
143
|
+
ids = tuple(f"{source_id}:row:{i}" for i in range(len(rows)))
|
|
144
|
+
cert = PreconditionCertificate(
|
|
145
|
+
"fixed-indicator-active",
|
|
146
|
+
{"indicator_index": indicator_index, "fixed_value": int(indicator_lower), "active_value": active_value},
|
|
147
|
+
)
|
|
148
|
+
step = TransformationStep(
|
|
149
|
+
step_id=f"{source_id}:indicator-fixed-active",
|
|
150
|
+
bridge_id="solverpilot.bridge.indicator.fixed",
|
|
151
|
+
bridge_version="1.0",
|
|
152
|
+
source_entity_ids=(source_id,),
|
|
153
|
+
generated_target_ids=ids,
|
|
154
|
+
exactness=ExactnessClass.EXACT_EQUIVALENT,
|
|
155
|
+
preconditions=(cert,),
|
|
156
|
+
primal_mapping=MappingAvailability.IDENTITY,
|
|
157
|
+
dual_mapping=MappingAvailability.UNAVAILABLE,
|
|
158
|
+
certificate_mapping=MappingAvailability.UNAVAILABLE,
|
|
159
|
+
mutation_compatibility=("parameter_values",),
|
|
160
|
+
size_delta={"variables": 0, "constraints": len(rows) - 1},
|
|
161
|
+
numerical_risk={"class": "none", "reason": "fixed premise materializes original affine body"},
|
|
162
|
+
)
|
|
163
|
+
return IndicatorLowering(tuple(rows), tuple(lo), tuple(hi), step)
|
|
164
|
+
|
|
165
|
+
if not policy.permits(ExactnessClass.EXACT_EQUIVALENT):
|
|
166
|
+
raise BridgeUnavailable("bridge policy rejects exact indicator lowering")
|
|
167
|
+
if policy.require_finite_certified_big_m is not True:
|
|
168
|
+
raise BridgeUnavailable("P4 safe mode requires finite certified Big-M")
|
|
169
|
+
|
|
170
|
+
from solverpilot.model.sets import EqualTo, GreaterThan, Interval, LessThan
|
|
171
|
+
body_min, body_max, bound_terms = _affine_range(body, variable_lower, variable_upper)
|
|
172
|
+
rows: list[dict[int, float]] = []
|
|
173
|
+
lo: list[float] = []
|
|
174
|
+
hi: list[float] = []
|
|
175
|
+
m_values: dict[str, float] = {}
|
|
176
|
+
|
|
177
|
+
def add_upper(rhs: float, label: str) -> None:
|
|
178
|
+
M = max(0.0, body_max - float(rhs))
|
|
179
|
+
row, l, u = _upper_row(body, rhs=float(rhs), z_index=indicator_index, active_value=active_value, M=M)
|
|
180
|
+
rows.append(row); lo.append(l); hi.append(u); m_values[label] = M
|
|
181
|
+
|
|
182
|
+
def add_lower(rhs: float, label: str) -> None:
|
|
183
|
+
M = max(0.0, float(rhs) - body_min)
|
|
184
|
+
row, l, u = _lower_row(body, rhs=float(rhs), z_index=indicator_index, active_value=active_value, M=M)
|
|
185
|
+
rows.append(row); lo.append(l); hi.append(u); m_values[label] = M
|
|
186
|
+
|
|
187
|
+
if isinstance(body_set, LessThan):
|
|
188
|
+
add_upper(body_set.upper, "upper")
|
|
189
|
+
elif isinstance(body_set, GreaterThan):
|
|
190
|
+
add_lower(body_set.lower, "lower")
|
|
191
|
+
elif isinstance(body_set, EqualTo):
|
|
192
|
+
add_lower(body_set.value, "lower"); add_upper(body_set.value, "upper")
|
|
193
|
+
elif isinstance(body_set, Interval):
|
|
194
|
+
add_lower(body_set.lower, "lower"); add_upper(body_set.upper, "upper")
|
|
195
|
+
else:
|
|
196
|
+
raise BridgeUnavailable(f"unsupported indicator body set {type(body_set).__name__}")
|
|
197
|
+
|
|
198
|
+
cert = PreconditionCertificate(
|
|
199
|
+
"certified-affine-big-m",
|
|
200
|
+
{
|
|
201
|
+
"source_id": source_id,
|
|
202
|
+
"indicator_index": indicator_index,
|
|
203
|
+
"active_value": active_value,
|
|
204
|
+
"body_min": body_min,
|
|
205
|
+
"body_max": body_max,
|
|
206
|
+
"m_values": m_values,
|
|
207
|
+
"bound_terms": bound_terms,
|
|
208
|
+
"parameter_dependencies": list(parameter_dependencies),
|
|
209
|
+
},
|
|
210
|
+
)
|
|
211
|
+
generated = tuple(f"{source_id}:bigm:{i}" for i in range(len(rows)))
|
|
212
|
+
step = TransformationStep(
|
|
213
|
+
step_id=f"{source_id}:indicator-bigm",
|
|
214
|
+
bridge_id="solverpilot.bridge.indicator.certified-big-m",
|
|
215
|
+
bridge_version="1.0",
|
|
216
|
+
source_entity_ids=(source_id,),
|
|
217
|
+
generated_target_ids=generated,
|
|
218
|
+
exactness=ExactnessClass.EXACT_EQUIVALENT,
|
|
219
|
+
preconditions=(cert,),
|
|
220
|
+
primal_mapping=MappingAvailability.IDENTITY,
|
|
221
|
+
dual_mapping=MappingAvailability.UNAVAILABLE,
|
|
222
|
+
certificate_mapping=MappingAvailability.UNAVAILABLE,
|
|
223
|
+
mutation_compatibility=("parameter_values_revalidate_certificate",),
|
|
224
|
+
size_delta={"variables": 0, "constraints": len(rows) - 1},
|
|
225
|
+
numerical_risk={
|
|
226
|
+
"class": "bound_derived_big_m",
|
|
227
|
+
"max_abs_M": max((abs(v) for v in m_values.values()), default=0.0),
|
|
228
|
+
"all_M_finite": all(math.isfinite(v) for v in m_values.values()),
|
|
229
|
+
},
|
|
230
|
+
notes=("Big-M values are derived from current finite variable bounds; no guessed constant is allowed",),
|
|
231
|
+
)
|
|
232
|
+
return IndicatorLowering(tuple(rows), tuple(lo), tuple(hi), step)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Callable
|
|
5
|
+
|
|
6
|
+
from .types import BridgePolicy, ExactnessClass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True, slots=True)
|
|
10
|
+
class BridgeSpec:
|
|
11
|
+
bridge_id: str
|
|
12
|
+
bridge_version: str
|
|
13
|
+
source_kind: str
|
|
14
|
+
exactness: ExactnessClass
|
|
15
|
+
priority: int
|
|
16
|
+
description: str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, slots=True)
|
|
20
|
+
class BridgeCandidate:
|
|
21
|
+
spec: BridgeSpec
|
|
22
|
+
applicable: bool
|
|
23
|
+
reason: str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BridgeRegistry:
|
|
27
|
+
"""Small deterministic registry for P4 reformulation candidates.
|
|
28
|
+
|
|
29
|
+
The registry is intentionally evidence-free: it orders safe semantic paths by a
|
|
30
|
+
deterministic structural priority. Runtime performance evidence remains Track M's
|
|
31
|
+
responsibility and is not smuggled into P4 compilation decisions.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self) -> None:
|
|
35
|
+
self._specs: dict[str, BridgeSpec] = {}
|
|
36
|
+
|
|
37
|
+
def register(self, spec: BridgeSpec) -> None:
|
|
38
|
+
key = f"{spec.bridge_id}@{spec.bridge_version}"
|
|
39
|
+
if key in self._specs:
|
|
40
|
+
raise ValueError(f"duplicate bridge registration {key}")
|
|
41
|
+
self._specs[key] = spec
|
|
42
|
+
|
|
43
|
+
def candidates(self, source_kind: str, *, policy: BridgePolicy) -> tuple[BridgeSpec, ...]:
|
|
44
|
+
items = [s for s in self._specs.values() if s.source_kind == source_kind and policy.permits(s.exactness)]
|
|
45
|
+
return tuple(sorted(items, key=lambda s: (s.priority, s.bridge_id, s.bridge_version)))
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def specs(self) -> tuple[BridgeSpec, ...]:
|
|
49
|
+
return tuple(sorted(self._specs.values(), key=lambda s: (s.source_kind, s.priority, s.bridge_id, s.bridge_version)))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def default_bridge_registry() -> BridgeRegistry:
|
|
53
|
+
reg = BridgeRegistry()
|
|
54
|
+
reg.register(BridgeSpec(
|
|
55
|
+
bridge_id="solverpilot.bridge.indicator.native",
|
|
56
|
+
bridge_version="1.0",
|
|
57
|
+
source_kind="indicator",
|
|
58
|
+
exactness=ExactnessClass.EXACT_EQUIVALENT,
|
|
59
|
+
priority=0,
|
|
60
|
+
description="Preserve a native indicator constraint when both backend capability and execution transport are verified.",
|
|
61
|
+
))
|
|
62
|
+
reg.register(BridgeSpec(
|
|
63
|
+
bridge_id="solverpilot.bridge.indicator.fixed",
|
|
64
|
+
bridge_version="1.0",
|
|
65
|
+
source_kind="indicator",
|
|
66
|
+
exactness=ExactnessClass.EXACT_EQUIVALENT,
|
|
67
|
+
priority=10,
|
|
68
|
+
description="Eliminate or materialize an indicator whose premise is fixed by certified binary bounds.",
|
|
69
|
+
))
|
|
70
|
+
reg.register(BridgeSpec(
|
|
71
|
+
bridge_id="solverpilot.bridge.indicator.certified-big-m",
|
|
72
|
+
bridge_version="1.0",
|
|
73
|
+
source_kind="indicator",
|
|
74
|
+
exactness=ExactnessClass.EXACT_EQUIVALENT,
|
|
75
|
+
priority=20,
|
|
76
|
+
description="Lower an affine indicator with finite box-bound-derived Big-M certificates.",
|
|
77
|
+
))
|
|
78
|
+
return reg
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://solverpilot.local/schemas/transformation-tape-p4-v1.json",
|
|
4
|
+
"title": "SolverPilot P4 Transformation Tape",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["schema_version", "steps"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"schema_version": {"const": "solverpilot.transformation-tape.p4.v1"},
|
|
9
|
+
"steps": {
|
|
10
|
+
"type": "array",
|
|
11
|
+
"items": {
|
|
12
|
+
"type": "object",
|
|
13
|
+
"required": [
|
|
14
|
+
"step_id", "bridge_id", "bridge_version", "source_entity_ids",
|
|
15
|
+
"generated_target_ids", "exactness", "preconditions", "primal_mapping",
|
|
16
|
+
"dual_mapping", "certificate_mapping", "mutation_compatibility",
|
|
17
|
+
"size_delta", "numerical_risk", "notes"
|
|
18
|
+
],
|
|
19
|
+
"properties": {
|
|
20
|
+
"step_id": {"type": "string", "minLength": 1},
|
|
21
|
+
"bridge_id": {"type": "string", "minLength": 1},
|
|
22
|
+
"bridge_version": {"type": "string", "minLength": 1},
|
|
23
|
+
"source_entity_ids": {"type": "array", "items": {"type": "string"}},
|
|
24
|
+
"generated_target_ids": {"type": "array", "items": {"type": "string"}},
|
|
25
|
+
"exactness": {"enum": ["exact_equivalent", "certified_relaxation", "heuristic"]},
|
|
26
|
+
"preconditions": {
|
|
27
|
+
"type": "array",
|
|
28
|
+
"items": {
|
|
29
|
+
"type": "object",
|
|
30
|
+
"required": ["kind", "payload", "certificate_version", "sha256"],
|
|
31
|
+
"properties": {
|
|
32
|
+
"kind": {"type": "string"},
|
|
33
|
+
"payload": {"type": "object"},
|
|
34
|
+
"certificate_version": {"type": "string"},
|
|
35
|
+
"sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"}
|
|
36
|
+
},
|
|
37
|
+
"additionalProperties": false
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"primal_mapping": {"enum": ["identity", "available", "unavailable"]},
|
|
41
|
+
"dual_mapping": {"enum": ["identity", "available", "unavailable"]},
|
|
42
|
+
"certificate_mapping": {"enum": ["identity", "available", "unavailable"]},
|
|
43
|
+
"mutation_compatibility": {"type": "array", "items": {"type": "string"}},
|
|
44
|
+
"size_delta": {"type": "object", "additionalProperties": {"type": "integer"}},
|
|
45
|
+
"numerical_risk": {"type": "object"},
|
|
46
|
+
"notes": {"type": "array", "items": {"type": "string"}}
|
|
47
|
+
},
|
|
48
|
+
"additionalProperties": false
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"additionalProperties": false
|
|
53
|
+
}
|