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.
Files changed (201) hide show
  1. solverpilot/__init__.py +250 -0
  2. solverpilot/_immutability.py +27 -0
  3. solverpilot/_version.py +3 -0
  4. solverpilot/applications/__init__.py +3 -0
  5. solverpilot/applications/routing/__init__.py +19 -0
  6. solverpilot/applications/routing/compile_milp.py +321 -0
  7. solverpilot/applications/routing/diagnostics.py +78 -0
  8. solverpilot/applications/routing/errors.py +14 -0
  9. solverpilot/applications/routing/heuristics.py +167 -0
  10. solverpilot/applications/routing/model.py +259 -0
  11. solverpilot/applications/routing/reference.py +81 -0
  12. solverpilot/applications/routing/solution.py +89 -0
  13. solverpilot/applications/routing/timeline.py +79 -0
  14. solverpilot/applications/routing/validation.py +169 -0
  15. solverpilot/applications/tsp/__init__.py +41 -0
  16. solverpilot/applications/tsp/compile_cp.py +121 -0
  17. solverpilot/applications/tsp/compile_milp.py +139 -0
  18. solverpilot/applications/tsp/distance.py +60 -0
  19. solverpilot/applications/tsp/errors.py +17 -0
  20. solverpilot/applications/tsp/heuristics.py +112 -0
  21. solverpilot/applications/tsp/model.py +175 -0
  22. solverpilot/applications/tsp/reference.py +121 -0
  23. solverpilot/applications/tsp/solution.py +65 -0
  24. solverpilot/applications/tsp/validation.py +92 -0
  25. solverpilot/backends/__init__.py +51 -0
  26. solverpilot/backends/_casadi_highs_worker.py +92 -0
  27. solverpilot/backends/base.py +28 -0
  28. solverpilot/backends/bundled_capi.py +910 -0
  29. solverpilot/backends/casadi_conic.py +303 -0
  30. solverpilot/backends/health.py +240 -0
  31. solverpilot/backends/highspy_native.py +217 -0
  32. solverpilot/backends/nlopt_native.py +237 -0
  33. solverpilot/backends/osqp_native.py +297 -0
  34. solverpilot/backends/protocol_v2.py +119 -0
  35. solverpilot/backends/pyscipopt_native.py +200 -0
  36. solverpilot/backends/registry.py +38 -0
  37. solverpilot/backends/scipy_highs.py +138 -0
  38. solverpilot/backends/scipy_highs_lp.py +195 -0
  39. solverpilot/backends/scipy_slsqp_qp.py +133 -0
  40. solverpilot/backends/scipy_vendored_highs_dev.py +363 -0
  41. solverpilot/benchmark/__init__.py +23 -0
  42. solverpilot/benchmark/acquire.py +199 -0
  43. solverpilot/benchmark/baselines.py +70 -0
  44. solverpilot/benchmark/bundle.py +37 -0
  45. solverpilot/benchmark/campaign.py +150 -0
  46. solverpilot/benchmark/environment.py +115 -0
  47. solverpilot/benchmark/integrity.py +46 -0
  48. solverpilot/benchmark/model.py +146 -0
  49. solverpilot/benchmark/policy.py +182 -0
  50. solverpilot/benchmark/registry.py +38 -0
  51. solverpilot/benchmark/report.py +44 -0
  52. solverpilot/benchmark/runner.py +516 -0
  53. solverpilot/benchmark/slurm.py +55 -0
  54. solverpilot/benchmark/specs.py +75 -0
  55. solverpilot/benchmark/splits.py +181 -0
  56. solverpilot/benchmark/summary.py +384 -0
  57. solverpilot/benchmark/verify.py +142 -0
  58. solverpilot/benchmark/worker.py +71 -0
  59. solverpilot/bridges/__init__.py +19 -0
  60. solverpilot/bridges/engine.py +44 -0
  61. solverpilot/bridges/indicator.py +232 -0
  62. solverpilot/bridges/registry.py +78 -0
  63. solverpilot/bridges/transformation-tape.schema.json +53 -0
  64. solverpilot/bridges/types.py +149 -0
  65. solverpilot/bridges/validation.py +110 -0
  66. solverpilot/capabilities/__init__.py +32 -0
  67. solverpilot/capabilities/backend-capability-v2.schema.json +21 -0
  68. solverpilot/capabilities/conformance.py +151 -0
  69. solverpilot/capabilities/enums.py +27 -0
  70. solverpilot/capabilities/manifest.py +19 -0
  71. solverpilot/capabilities/ontology.py +73 -0
  72. solverpilot/capabilities/requirements.py +37 -0
  73. solverpilot/capabilities/resolve.py +30 -0
  74. solverpilot/capabilities/v2.py +711 -0
  75. solverpilot/cli/__init__.py +1 -0
  76. solverpilot/cli/backend_health.py +77 -0
  77. solverpilot/cli/benchmark.py +331 -0
  78. solverpilot/cli/benchmark_worker.py +33 -0
  79. solverpilot/cli/capabilities.py +65 -0
  80. solverpilot/conic/__init__.py +11 -0
  81. solverpilot/conic/backend.py +263 -0
  82. solverpilot/conic/compiler.py +310 -0
  83. solverpilot/conic/conformance.py +60 -0
  84. solverpilot/conic/conic-ir-v1.schema.json +24 -0
  85. solverpilot/conic/ir.py +299 -0
  86. solverpilot/conic/validation.py +149 -0
  87. solverpilot/cp/__init__.py +7 -0
  88. solverpilot/cp/conformance.py +32 -0
  89. solverpilot/cp/cp-ir-v1.schema.json +13 -0
  90. solverpilot/cp/ir.py +170 -0
  91. solverpilot/cp/model.py +143 -0
  92. solverpilot/cp/ortools_backend.py +423 -0
  93. solverpilot/cp/ortools_worker.py +75 -0
  94. solverpilot/cp/reference.py +51 -0
  95. solverpilot/cp/serialization.py +117 -0
  96. solverpilot/cp/validate.py +83 -0
  97. solverpilot/diagnose/__init__.py +33 -0
  98. solverpilot/diagnose/infeasibility.py +463 -0
  99. solverpilot/diagnose/model.py +87 -0
  100. solverpilot/evaluation/__init__.py +53 -0
  101. solverpilot/evaluation/metrics.py +150 -0
  102. solverpilot/evaluation/oracle.py +288 -0
  103. solverpilot/evaluation/proof.py +53 -0
  104. solverpilot/evaluation/references.py +73 -0
  105. solverpilot/evaluation/regret.py +281 -0
  106. solverpilot/exceptions.py +34 -0
  107. solverpilot/experimental/__init__.py +32 -0
  108. solverpilot/extensions/__init__.py +56 -0
  109. solverpilot/extensions/entrypoints.py +34 -0
  110. solverpilot/extensions/errors.py +35 -0
  111. solverpilot/extensions/manager.py +651 -0
  112. solverpilot/extensions/manifest.py +232 -0
  113. solverpilot/extensions/model.py +101 -0
  114. solverpilot/extensions/versioning.py +136 -0
  115. solverpilot/history/__init__.py +22 -0
  116. solverpilot/history/errors.py +15 -0
  117. solverpilot/history/model.py +129 -0
  118. solverpilot/history/sqlite.py +651 -0
  119. solverpilot/inspect/__init__.py +3 -0
  120. solverpilot/inspect/fingerprint.py +199 -0
  121. solverpilot/intelligence/__init__.py +36 -0
  122. solverpilot/intelligence/errors.py +16 -0
  123. solverpilot/intelligence/fingerprint.py +108 -0
  124. solverpilot/intelligence/leakage.py +69 -0
  125. solverpilot/intelligence/schema.py +314 -0
  126. solverpilot/intelligence/shift.py +208 -0
  127. solverpilot/io/__init__.py +41 -0
  128. solverpilot/io/binary64.py +49 -0
  129. solverpilot/io/csv.py +230 -0
  130. solverpilot/io/errors.py +25 -0
  131. solverpilot/io/hashing.py +111 -0
  132. solverpilot/io/json.py +90 -0
  133. solverpilot/io/mapping.py +195 -0
  134. solverpilot/io/model.py +407 -0
  135. solverpilot/io/numeric.py +108 -0
  136. solverpilot/io/provenance.py +114 -0
  137. solverpilot/io/result.py +45 -0
  138. solverpilot/io/source.py +104 -0
  139. solverpilot/io/strict_json.py +133 -0
  140. solverpilot/minlp/__init__.py +6 -0
  141. solverpilot/minlp/compiler.py +66 -0
  142. solverpilot/minlp/conformance.py +34 -0
  143. solverpilot/minlp/convexity.py +85 -0
  144. solverpilot/minlp/ir.py +35 -0
  145. solverpilot/minlp/minlp-ir-v1.schema.json +1 -0
  146. solverpilot/minlp/orchestrator.py +220 -0
  147. solverpilot/minlp/validation.py +28 -0
  148. solverpilot/model/__init__.py +27 -0
  149. solverpilot/model/compiler.py +1263 -0
  150. solverpilot/model/errors.py +22 -0
  151. solverpilot/model/expression.py +373 -0
  152. solverpilot/model/model.py +716 -0
  153. solverpilot/model/sets.py +60 -0
  154. solverpilot/model/types.py +36 -0
  155. solverpilot/nlp/__init__.py +7 -0
  156. solverpilot/nlp/ad.py +166 -0
  157. solverpilot/nlp/backend.py +113 -0
  158. solverpilot/nlp/compiler.py +116 -0
  159. solverpilot/nlp/conformance.py +58 -0
  160. solverpilot/nlp/ir.py +70 -0
  161. solverpilot/nlp/nlp-ir-v1.schema.json +1 -0
  162. solverpilot/nlp/validation.py +60 -0
  163. solverpilot/plan/__init__.py +33 -0
  164. solverpilot/plan/model.py +79 -0
  165. solverpilot/plan/planner.py +201 -0
  166. solverpilot/plan/production.py +627 -0
  167. solverpilot/plan/selective_lp.py +65 -0
  168. solverpilot/problem/__init__.py +16 -0
  169. solverpilot/problem/enums.py +20 -0
  170. solverpilot/problem/hashing.py +88 -0
  171. solverpilot/problem/linear.py +196 -0
  172. solverpilot/problem/mps.py +447 -0
  173. solverpilot/problem/quadratic.py +192 -0
  174. solverpilot/reporting/__init__.py +27 -0
  175. solverpilot/reporting/explain.py +327 -0
  176. solverpilot/reporting/model.py +264 -0
  177. solverpilot/reporting/render.py +55 -0
  178. solverpilot/runtime/__init__.py +19 -0
  179. solverpilot/runtime/auto.py +193 -0
  180. solverpilot/runtime/budgeting.py +43 -0
  181. solverpilot/runtime/executor.py +126 -0
  182. solverpilot/runtime/portfolio.py +197 -0
  183. solverpilot/runtime/result.py +72 -0
  184. solverpilot/session/__init__.py +29 -0
  185. solverpilot/session/conformance_v2.py +239 -0
  186. solverpilot/session/mutations.py +115 -0
  187. solverpilot/session/persistent-session-trace.schema.json +45 -0
  188. solverpilot/session/persistent.py +491 -0
  189. solverpilot/session/reuse.py +81 -0
  190. solverpilot/session/session.py +236 -0
  191. solverpilot/trace/__init__.py +3 -0
  192. solverpilot/trace/schema.py +47 -0
  193. solverpilot/validate/__init__.py +10 -0
  194. solverpilot/validate/core.py +179 -0
  195. solverpilot/validate/result.py +55 -0
  196. solverpilot-0.1.dist-info/METADATA +554 -0
  197. solverpilot-0.1.dist-info/RECORD +201 -0
  198. solverpilot-0.1.dist-info/WHEEL +5 -0
  199. solverpilot-0.1.dist-info/entry_points.txt +4 -0
  200. solverpilot-0.1.dist-info/licenses/LICENSE +202 -0
  201. 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
+ }