solverpilot 0.1.0rc2__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.0rc2.dist-info/METADATA +554 -0
  197. solverpilot-0.1.0rc2.dist-info/RECORD +201 -0
  198. solverpilot-0.1.0rc2.dist-info/WHEEL +5 -0
  199. solverpilot-0.1.0rc2.dist-info/entry_points.txt +4 -0
  200. solverpilot-0.1.0rc2.dist-info/licenses/LICENSE +202 -0
  201. solverpilot-0.1.0rc2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,250 @@
1
+ """SolverPilot public API.
2
+
3
+ M30 freezes ``__all__`` as the supported top-level API for the 1.0 release line.
4
+ Research-only milestone translators and the rejected LP selector remain available
5
+ under :mod:`solverpilot.experimental` and are intentionally excluded from this list.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import warnings
11
+
12
+ from ._version import __version__
13
+ from .capabilities import (
14
+ BackendManifest,
15
+ Capability,
16
+ CapabilityRequirements,
17
+ SupportLevel,
18
+ compatible,
19
+ requirements_for,
20
+ )
21
+ from .backends import (
22
+ Backend,
23
+ BackendHealthReport,
24
+ BackendProbeCheck,
25
+ BackendProbeStatus,
26
+ BackendRegistry,
27
+ BackendSolveResult,
28
+ probe_backend,
29
+ probe_backends,
30
+ )
31
+ from .diagnose import (
32
+ ConflictAtom,
33
+ ConflictSetResult,
34
+ DiagnosticIssue,
35
+ ElasticRelaxationResult,
36
+ EvidenceKind,
37
+ InfeasibilityReport,
38
+ NormalizedIISEvidence,
39
+ RelaxationViolation,
40
+ ViolationKind,
41
+ deletion_filter_conflict,
42
+ diagnose_infeasibility,
43
+ elastic_relaxation,
44
+ find_static_infeasibility,
45
+ )
46
+ from .exceptions import (
47
+ BackendUnavailableError,
48
+ BudgetNotSupportedError,
49
+ CapabilityMismatchError,
50
+ NoCompatibleBackendError,
51
+ SolverPilotError,
52
+ UnknownBackendError,
53
+ )
54
+ from .inspect import DistributionStats, ProblemFingerprint, inspect_problem
55
+ from .plan import (
56
+ CandidatePlan,
57
+ EvidenceClass,
58
+ HealthPolicy,
59
+ PerformancePolicy,
60
+ PlannerContext,
61
+ ProductionDecision,
62
+ ProductionEvidence,
63
+ SolveBudget,
64
+ SolveIntent,
65
+ SolvePlan,
66
+ plan_production_solve,
67
+ plan_solve,
68
+ )
69
+ from .problem import (
70
+ ConvexityStatus,
71
+ LinearProblem,
72
+ MPSParseError,
73
+ MPSUnsupportedFeatureError,
74
+ ObjectiveSense,
75
+ QuadraticProblem,
76
+ VariableDomain,
77
+ parse_mps,
78
+ read_mps,
79
+ )
80
+ from .runtime import (
81
+ PortfolioAttempt,
82
+ PortfolioSolveResult,
83
+ SolveResult,
84
+ builtin_backend_candidates,
85
+ default_registry,
86
+ execute,
87
+ execute_portfolio,
88
+ solve,
89
+ solve_production,
90
+ )
91
+ from .session import MutationKind, MutationRecord, ReuseAssessment, Session
92
+ from .trace import PhaseTimings, SolveTrace, TRACE_SCHEMA_VERSION
93
+ from .validate import (
94
+ CandidateSolution,
95
+ PublicStatus,
96
+ ValidationReport,
97
+ ValidationTolerances,
98
+ validate_solution,
99
+ )
100
+
101
+ __all__ = [
102
+ "Backend",
103
+ "BackendHealthReport",
104
+ "BackendManifest",
105
+ "BackendProbeCheck",
106
+ "BackendProbeStatus",
107
+ "BackendRegistry",
108
+ "BackendSolveResult",
109
+ "BackendUnavailableError",
110
+ "BudgetNotSupportedError",
111
+ "CandidatePlan",
112
+ "CandidateSolution",
113
+ "Capability",
114
+ "CapabilityMismatchError",
115
+ "CapabilityRequirements",
116
+ "ConflictAtom",
117
+ "ConflictSetResult",
118
+ "ConvexityStatus",
119
+ "DiagnosticIssue",
120
+ "DistributionStats",
121
+ "ElasticRelaxationResult",
122
+ "EvidenceClass",
123
+ "EvidenceKind",
124
+ "HealthPolicy",
125
+ "InfeasibilityReport",
126
+ "LinearProblem",
127
+ "MPSParseError",
128
+ "MPSUnsupportedFeatureError",
129
+ "MutationKind",
130
+ "MutationRecord",
131
+ "NormalizedIISEvidence",
132
+ "NoCompatibleBackendError",
133
+ "ObjectiveSense",
134
+ "SolverPilotError",
135
+ "UnknownBackendError",
136
+ "PerformancePolicy",
137
+ "PhaseTimings",
138
+ "PlannerContext",
139
+ "PortfolioAttempt",
140
+ "PortfolioSolveResult",
141
+ "ProblemFingerprint",
142
+ "ProductionDecision",
143
+ "ProductionEvidence",
144
+ "PublicStatus",
145
+ "RelaxationViolation",
146
+ "QuadraticProblem",
147
+ "ReuseAssessment",
148
+ "Session",
149
+ "SolveBudget",
150
+ "SolveIntent",
151
+ "SolvePlan",
152
+ "SolveResult",
153
+ "SolveTrace",
154
+ "SupportLevel",
155
+ "TRACE_SCHEMA_VERSION",
156
+ "ValidationReport",
157
+ "ValidationTolerances",
158
+ "VariableDomain",
159
+ "ViolationKind",
160
+ "builtin_backend_candidates",
161
+ "compatible",
162
+ "default_registry",
163
+ "deletion_filter_conflict",
164
+ "diagnose_infeasibility",
165
+ "elastic_relaxation",
166
+ "execute",
167
+ "execute_portfolio",
168
+ "find_static_infeasibility",
169
+ "inspect_problem",
170
+ "parse_mps",
171
+ "plan_production_solve",
172
+ "plan_solve",
173
+ "probe_backend",
174
+ "probe_backends",
175
+ "read_mps",
176
+ "requirements_for",
177
+ "solve",
178
+ "solve_production",
179
+ "validate_solution",
180
+ ]
181
+
182
+
183
+ # Track P P0-P9 merged feature surface. These aliases are intentionally lazy and
184
+ # excluded from ``__all__`` so the M30-frozen stable top-level API remains unchanged.
185
+ # Canonical imports for new functionality live under solverpilot.model, .conic,
186
+ # .nlp, .minlp, .cp, .capabilities, and .session.
187
+ _TRACK_P_LAZY_EXPORTS = {
188
+ "Model": ("solverpilot.model", "Model"),
189
+ "CPModel": ("solverpilot.cp", "CPModel"),
190
+ "ReferenceCPBackend": ("solverpilot.cp", "ReferenceCPBackend"),
191
+ "ORToolsCPSATBackend": ("solverpilot.cp", "ORToolsCPSATBackend"),
192
+ "validate_cp_solution": ("solverpilot.cp", "validate_cp_solution"),
193
+ "reference_cp_conformance": ("solverpilot.cp", "reference_cp_conformance"),
194
+ "ortools_cp_sat_conformance": ("solverpilot.cp", "ortools_cp_sat_conformance"),
195
+ "ConeKind": ("solverpilot.conic", "ConeKind"),
196
+ "ConicProblem": ("solverpilot.conic", "ConicProblem"),
197
+ "CasadiSuperSCSBackend": ("solverpilot.conic", "CasadiSuperSCSBackend"),
198
+ "validate_conic_solution": ("solverpilot.conic", "validate_conic_solution"),
199
+ "conform_casadi_superscs_backend": ("solverpilot.conic", "conform_casadi_superscs_backend"),
200
+ "conform_minlp_orchestrator": ("solverpilot.minlp", "conform_minlp_orchestrator"),
201
+ "CapabilityKey": ("solverpilot.capabilities", "CapabilityKey"),
202
+ "VerificationLevel": ("solverpilot.capabilities", "VerificationLevel"),
203
+ "compatible_v2": ("solverpilot.capabilities", "compatible_v2"),
204
+ "requirements_v2_for": ("solverpilot.capabilities", "requirements_v2_for"),
205
+ "sum": ("solverpilot.model", "sum"),
206
+ "symbolic_sum": ("solverpilot.model", "sum"),
207
+ "exp": ("solverpilot.model", "exp"),
208
+ "MutationExecutionPath": ("solverpilot.session", "MutationExecutionPath"),
209
+ "PersistentSession": ("solverpilot.session", "PersistentSession"),
210
+ "conform_persistent_backend": ("solverpilot.session", "conform_persistent_backend"),
211
+ }
212
+
213
+ # Pre-M30 compatibility shim. These names are intentionally not in ``__all__`` and
214
+ # must not be treated as stable product APIs. They remain lazily accessible so old
215
+ # research notebooks fail softly while emitting an explicit migration warning.
216
+ _DEPRECATED_EXPERIMENTAL = {
217
+ "M22_OFFICIAL_EVIDENCE",
218
+ "SelectiveLPDecision",
219
+ "decide_selective_lp_backend",
220
+ "production_evidence_from_m22_gate",
221
+ "production_evidence_from_m24_public_ood",
222
+ "production_evidence_from_m25_opportunity",
223
+ "production_evidence_from_m26_validation",
224
+ "production_evidence_from_m27_heldout",
225
+ "production_evidence_from_m28_native_choose",
226
+ "production_evidence_from_m29_value_audit",
227
+ }
228
+
229
+
230
+ def __getattr__(name: str):
231
+ if name in _TRACK_P_LAZY_EXPORTS:
232
+ import importlib
233
+
234
+ module_name, attr_name = _TRACK_P_LAZY_EXPORTS[name]
235
+ return getattr(importlib.import_module(module_name), attr_name)
236
+ if name in _DEPRECATED_EXPERIMENTAL:
237
+ from . import experimental
238
+
239
+ warnings.warn(
240
+ f"solverpilot.{name} is research-only and moved to solverpilot.experimental.{name}; "
241
+ "the top-level compatibility alias will be removed before/at 1.0 if not explicitly retained",
242
+ DeprecationWarning,
243
+ stacklevel=2,
244
+ )
245
+ return getattr(experimental, name)
246
+ raise AttributeError(f"module 'solverpilot' has no attribute {name!r}")
247
+
248
+
249
+ def __dir__() -> list[str]:
250
+ return sorted(set(globals()) | _DEPRECATED_EXPERIMENTAL | set(_TRACK_P_LAZY_EXPORTS))
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from types import MappingProxyType
5
+ from typing import Any
6
+
7
+ import numpy as np
8
+
9
+
10
+ def readonly_array(value: np.ndarray | Any, *, dtype=None) -> np.ndarray:
11
+ """Return an owned, read-only NumPy snapshot."""
12
+ arr = np.array(value, dtype=dtype, copy=True)
13
+ arr.flags.writeable = False
14
+ return arr
15
+
16
+
17
+ def deep_freeze(value: Any) -> Any:
18
+ """Recursively snapshot common mutable containers into immutable forms."""
19
+ if isinstance(value, np.ndarray):
20
+ return readonly_array(value)
21
+ if isinstance(value, Mapping):
22
+ return MappingProxyType({k: deep_freeze(v) for k, v in value.items()})
23
+ if isinstance(value, (list, tuple)):
24
+ return tuple(deep_freeze(v) for v in value)
25
+ if isinstance(value, (set, frozenset)):
26
+ return frozenset(deep_freeze(v) for v in value)
27
+ return value
@@ -0,0 +1,3 @@
1
+ """Single source for the import-time package version."""
2
+
3
+ __version__ = "0.1.0rc2"
@@ -0,0 +1,3 @@
1
+ """Optional application packs built on SolverPilot's canonical optimization core."""
2
+
3
+ __all__: list[str] = []
@@ -0,0 +1,19 @@
1
+ from .errors import VRPCompileError, VRPError, VRPReferenceLimitError, VRPValidationError
2
+ from .model import VRPCustomer, VRPInstance, VRPMatrix, VRPNode, VRPTimeWindow, VRPVehicle
3
+ from .solution import VRPRoute, VRPSolution
4
+ from .timeline import VRPRouteTimeline, VRPVisitTimeline, build_route_timeline
5
+ from .validation import VRPValidationIssue, VRPValidationReport, route_distance, validate_vrp_solution
6
+ from .diagnostics import VRPDiagnosticAction, diagnose_vrp_solution, diagnose_vrp_validation
7
+ from .reference import VRPReferenceResult, solve_vrp_reference
8
+ from .heuristics import clarke_wright_savings, nearest_feasible_insertion
9
+ from .compile_milp import VRPMILPCompilation, VRPMILPSolveResult, compile_vrp_milp, decode_vrp_milp_solution, solve_vrp_milp
10
+
11
+ __all__ = [
12
+ "VRPError", "VRPValidationError", "VRPCompileError", "VRPReferenceLimitError",
13
+ "VRPTimeWindow", "VRPNode", "VRPCustomer", "VRPVehicle", "VRPMatrix", "VRPInstance",
14
+ "VRPRoute", "VRPSolution", "VRPVisitTimeline", "VRPRouteTimeline", "build_route_timeline",
15
+ "VRPValidationIssue", "VRPValidationReport", "route_distance", "validate_vrp_solution",
16
+ "VRPDiagnosticAction", "diagnose_vrp_validation", "diagnose_vrp_solution", "VRPReferenceResult", "solve_vrp_reference",
17
+ "nearest_feasible_insertion", "clarke_wright_savings",
18
+ "VRPMILPCompilation", "VRPMILPSolveResult", "compile_vrp_milp", "decode_vrp_milp_solution", "solve_vrp_milp",
19
+ ]
@@ -0,0 +1,321 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from math import isfinite
5
+ from types import MappingProxyType
6
+ from typing import Mapping
7
+
8
+ import numpy as np
9
+ from scipy import sparse
10
+
11
+ from solverpilot.backends import ScipyHighsBackend
12
+ from solverpilot.problem import LinearProblem, ObjectiveSense, VariableDomain
13
+ from solverpilot.runtime import SolveResult, execute
14
+ from solverpilot.validate import CandidateSolution, validate_solution
15
+
16
+ from .errors import VRPCompileError
17
+ from .model import VRPInstance
18
+ from .solution import VRPRoute, VRPSolution, _INDEPENDENT_PROOF_TOKEN
19
+ from .validation import VRPValidationReport, route_distance, validate_vrp_solution
20
+
21
+ SOURCE = "__source__"
22
+ SINK = "__sink__"
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class VRPMILPCompilation:
27
+ problem: LinearProblem
28
+ arc_variables: Mapping[tuple[str, str, str], int]
29
+ visit_variables: Mapping[tuple[str, str], int]
30
+ order_variables: Mapping[tuple[str, str], int]
31
+ time_variables: Mapping[tuple[str, str], int]
32
+ big_m_time: float | None
33
+
34
+
35
+ @dataclass(frozen=True, slots=True)
36
+ class VRPMILPSolveResult:
37
+ solution: VRPSolution | None
38
+ validation: VRPValidationReport | None
39
+ core_result: SolveResult
40
+
41
+
42
+ def _time_horizon(instance: VRPInstance) -> tuple[float, float]:
43
+ if instance.travel_times is None:
44
+ raise VRPCompileError("time-constrained MILP requires explicit travel times")
45
+ max_edge = max(max(row) for row in instance.travel_times.values)
46
+ max_service = max((c.service_duration for c in instance.customers), default=0.0)
47
+ markers = [0.0]
48
+ for c in instance.customers:
49
+ if c.time_window is not None:
50
+ markers.extend((c.time_window.start, c.time_window.end))
51
+ for v in instance.vehicles:
52
+ markers.append(v.start_time)
53
+ if v.latest_return is not None:
54
+ markers.append(v.latest_return)
55
+ if v.max_route_duration is not None:
56
+ markers.append(v.start_time + v.max_route_duration)
57
+ route_bound = (len(instance.customers) + 1) * max_edge + sum(c.service_duration for c in instance.customers)
58
+ horizon = max(markers) + route_bound + 1.0
59
+ big_m = horizon + max_edge + max_service + 1.0
60
+ if not isfinite(horizon) or not isfinite(big_m):
61
+ raise VRPCompileError("time horizon is not finite")
62
+ return horizon, big_m
63
+
64
+
65
+ def compile_vrp_milp(instance: VRPInstance) -> VRPMILPCompilation:
66
+ customers = tuple(c.id for c in instance.customers)
67
+ n = len(customers)
68
+ arc_vars: dict[tuple[str, str, str], int] = {}
69
+ visit_vars: dict[tuple[str, str], int] = {}
70
+ order_vars: dict[tuple[str, str], int] = {}
71
+ time_vars: dict[tuple[str, str], int] = {}
72
+ domains: list[VariableDomain] = []
73
+ lower: list[float] = []
74
+ upper: list[float] = []
75
+ objective: list[float] = []
76
+
77
+ def add_var(domain: VariableDomain, lo: float, hi: float, cost: float = 0.0) -> int:
78
+ idx = len(domains)
79
+ domains.append(domain); lower.append(float(lo)); upper.append(float(hi)); objective.append(float(cost))
80
+ return idx
81
+
82
+ cmap = instance.customer_map
83
+ # Per-vehicle path variables on a virtual source/sink graph.
84
+ for vehicle in instance.vehicles:
85
+ vid = vehicle.id
86
+ # unused vehicle: source -> sink with zero cost
87
+ arc_vars[(vid, SOURCE, SINK)] = add_var(VariableDomain.BINARY, 0.0, 1.0, 0.0)
88
+ for cid in customers:
89
+ c = cmap[cid]
90
+ arc_vars[(vid, SOURCE, cid)] = add_var(
91
+ VariableDomain.BINARY, 0.0, 1.0, instance.distance(vehicle.start_node_id, c.node_id)
92
+ )
93
+ arc_vars[(vid, cid, SINK)] = add_var(
94
+ VariableDomain.BINARY, 0.0, 1.0, instance.distance(c.node_id, vehicle.end_node_id)
95
+ )
96
+ for i in customers:
97
+ for j in customers:
98
+ if i == j:
99
+ continue
100
+ arc_vars[(vid, i, j)] = add_var(
101
+ VariableDomain.BINARY, 0.0, 1.0, instance.distance(cmap[i].node_id, cmap[j].node_id)
102
+ )
103
+ for cid in customers:
104
+ visit_vars[(vid, cid)] = add_var(VariableDomain.BINARY, 0.0, 1.0)
105
+ order_vars[(vid, cid)] = add_var(VariableDomain.CONTINUOUS, 1.0, float(max(1, n)))
106
+
107
+ horizon: float | None = None
108
+ big_m: float | None = None
109
+ if instance.has_time_constraints:
110
+ horizon, big_m = _time_horizon(instance)
111
+ for vehicle in instance.vehicles:
112
+ for cid in customers:
113
+ time_vars[(vehicle.id, cid)] = add_var(VariableDomain.CONTINUOUS, 0.0, horizon)
114
+
115
+ row_i: list[int] = []
116
+ col_i: list[int] = []
117
+ data: list[float] = []
118
+ cl: list[float] = []
119
+ cu: list[float] = []
120
+ row = 0
121
+
122
+ def add_row(terms: Mapping[int, float], lo: float = -np.inf, hi: float = np.inf) -> None:
123
+ nonlocal row
124
+ for idx, coef in terms.items():
125
+ if coef:
126
+ row_i.append(row); col_i.append(idx); data.append(float(coef))
127
+ cl.append(float(lo)); cu.append(float(hi)); row += 1
128
+
129
+ # Every customer is assigned to exactly one vehicle.
130
+ for cid in customers:
131
+ add_row({visit_vars[(v.id, cid)]: 1.0 for v in instance.vehicles}, 1.0, 1.0)
132
+
133
+ for vehicle in instance.vehicles:
134
+ vid = vehicle.id
135
+ # Exactly one source exit and exactly one sink entry; source->sink means unused.
136
+ source_terms = {arc_vars[(vid, SOURCE, SINK)]: 1.0}
137
+ source_terms.update({arc_vars[(vid, SOURCE, cid)]: 1.0 for cid in customers})
138
+ add_row(source_terms, 1.0, 1.0)
139
+ sink_terms = {arc_vars[(vid, SOURCE, SINK)]: 1.0}
140
+ sink_terms.update({arc_vars[(vid, cid, SINK)]: 1.0 for cid in customers})
141
+ add_row(sink_terms, 1.0, 1.0)
142
+
143
+ for cid in customers:
144
+ incoming = {arc_vars[(vid, SOURCE, cid)]: 1.0, visit_vars[(vid, cid)]: -1.0}
145
+ outgoing = {arc_vars[(vid, cid, SINK)]: 1.0, visit_vars[(vid, cid)]: -1.0}
146
+ for other in customers:
147
+ if other == cid:
148
+ continue
149
+ incoming[arc_vars[(vid, other, cid)]] = 1.0
150
+ outgoing[arc_vars[(vid, cid, other)]] = 1.0
151
+ add_row(incoming, 0.0, 0.0)
152
+ add_row(outgoing, 0.0, 0.0)
153
+
154
+ # Capacity is a route-level resource in CVRP/VRPTW.
155
+ add_row({visit_vars[(vid, c.id)]: c.demand for c in instance.customers}, -np.inf, vehicle.capacity)
156
+
157
+ # MTZ eliminates customer-only subtours disconnected from the virtual source/sink path.
158
+ if n > 1:
159
+ for i in customers:
160
+ for j in customers:
161
+ if i == j:
162
+ continue
163
+ add_row(
164
+ {
165
+ order_vars[(vid, i)]: 1.0,
166
+ order_vars[(vid, j)]: -1.0,
167
+ arc_vars[(vid, i, j)]: float(n),
168
+ },
169
+ -np.inf,
170
+ float(n - 1),
171
+ )
172
+
173
+ if instance.has_time_constraints:
174
+ if big_m is None:
175
+ raise VRPCompileError("time-constrained routing model requires a computed finite big-M")
176
+ start = vehicle.start_time
177
+ for cid in customers:
178
+ customer = cmap[cid]
179
+ t = time_vars[(vid, cid)]
180
+ y = visit_vars[(vid, cid)]
181
+ if customer.time_window is not None:
182
+ # t >= open - M(1-y); t <= close + M(1-y)
183
+ add_row({t: 1.0, y: -big_m}, customer.time_window.start - big_m, np.inf)
184
+ add_row({t: 1.0, y: big_m}, -np.inf, customer.time_window.end + big_m)
185
+ travel = instance.travel_time(vehicle.start_node_id, customer.node_id)
186
+ x = arc_vars[(vid, SOURCE, cid)]
187
+ # t >= start + travel - M(1-x)
188
+ add_row({t: 1.0, x: -big_m}, start + travel - big_m, np.inf)
189
+ for i in customers:
190
+ ci = cmap[i]
191
+ for j in customers:
192
+ if i == j:
193
+ continue
194
+ travel = instance.travel_time(ci.node_id, cmap[j].node_id)
195
+ # t_j >= t_i + service_i + travel - M(1-x_ij)
196
+ add_row(
197
+ {
198
+ time_vars[(vid, j)]: 1.0,
199
+ time_vars[(vid, i)]: -1.0,
200
+ arc_vars[(vid, i, j)]: -big_m,
201
+ },
202
+ ci.service_duration + travel - big_m,
203
+ np.inf,
204
+ )
205
+ deadlines = []
206
+ if vehicle.latest_return is not None:
207
+ deadlines.append(vehicle.latest_return)
208
+ if vehicle.max_route_duration is not None:
209
+ deadlines.append(vehicle.start_time + vehicle.max_route_duration)
210
+ if deadlines:
211
+ deadline = min(deadlines)
212
+ for cid in customers:
213
+ c = cmap[cid]
214
+ travel = instance.travel_time(c.node_id, vehicle.end_node_id)
215
+ # t_c + service + travel <= deadline + M(1-x_cT)
216
+ add_row(
217
+ {time_vars[(vid, cid)]: 1.0, arc_vars[(vid, cid, SINK)]: big_m},
218
+ -np.inf,
219
+ deadline + big_m - c.service_duration - travel,
220
+ )
221
+
222
+ A = sparse.csr_matrix((data, (row_i, col_i)), shape=(row, len(domains)), dtype=np.float64)
223
+ problem = LinearProblem.from_data(
224
+ A=A,
225
+ c=np.asarray(objective, dtype=np.float64),
226
+ variable_lower=np.asarray(lower, dtype=np.float64),
227
+ variable_upper=np.asarray(upper, dtype=np.float64),
228
+ constraint_lower=np.asarray(cl, dtype=np.float64),
229
+ constraint_upper=np.asarray(cu, dtype=np.float64),
230
+ domains=domains,
231
+ objective_sense=ObjectiveSense.MINIMIZE,
232
+ name=f"vrp-arc-flow:{instance.name}",
233
+ metadata={
234
+ "application": "routing",
235
+ "formulation": "multi-vehicle-arc-flow-mtz",
236
+ "n_customers": n,
237
+ "n_vehicles": len(instance.vehicles),
238
+ "time_constraints": instance.has_time_constraints,
239
+ },
240
+ )
241
+ return VRPMILPCompilation(
242
+ problem,
243
+ MappingProxyType(dict(arc_vars)),
244
+ MappingProxyType(dict(visit_vars)),
245
+ MappingProxyType(dict(order_vars)),
246
+ MappingProxyType(dict(time_vars)),
247
+ big_m,
248
+ )
249
+
250
+
251
+ def decode_vrp_milp_solution(instance: VRPInstance, compilation: VRPMILPCompilation, x: np.ndarray) -> tuple[VRPRoute, ...]:
252
+ values = np.asarray(x, dtype=np.float64)
253
+ if values.ndim != 1 or values.shape[0] != compilation.problem.n_variables or not np.isfinite(values).all():
254
+ raise VRPCompileError("MILP candidate vector has invalid shape or non-finite values")
255
+ canonical = validate_solution(compilation.problem, CandidateSolution(x=values))
256
+ if not canonical.valid:
257
+ raise VRPCompileError("MILP candidate failed canonical formulation validation: " + "; ".join(canonical.warnings))
258
+ routes: list[VRPRoute] = []
259
+ all_customers = set(c.id for c in instance.customers)
260
+ decoded_customers: set[str] = set()
261
+ for vehicle in instance.vehicles:
262
+ vid = vehicle.id
263
+ selected = {(tail, head) for (v, tail, head), idx in compilation.arc_variables.items() if v == vid and values[idx] > 0.5}
264
+ outgoing: dict[str, str] = {}
265
+ incoming: dict[str, str] = {}
266
+ for tail, head in selected:
267
+ if tail in outgoing or head in incoming:
268
+ raise VRPCompileError(f"vehicle {vid!r} candidate has duplicate incoming/outgoing arcs")
269
+ outgoing[tail] = head; incoming[head] = tail
270
+ if SOURCE not in outgoing or SINK not in incoming:
271
+ raise VRPCompileError(f"vehicle {vid!r} candidate lacks a source-to-sink path")
272
+ nxt = outgoing[SOURCE]
273
+ if nxt == SINK:
274
+ if len(selected) != 1:
275
+ raise VRPCompileError(f"unused vehicle {vid!r} contains extra selected arcs")
276
+ routes.append(VRPRoute(vid, ()))
277
+ continue
278
+ customer_ids: list[str] = []
279
+ seen: set[str] = set()
280
+ while nxt != SINK:
281
+ if nxt not in all_customers or nxt in seen:
282
+ raise VRPCompileError(f"vehicle {vid!r} candidate contains an invalid customer path")
283
+ seen.add(nxt); customer_ids.append(nxt); decoded_customers.add(nxt)
284
+ if nxt not in outgoing:
285
+ raise VRPCompileError(f"vehicle {vid!r} path terminates before the sink")
286
+ nxt = outgoing[nxt]
287
+ if len(selected) != len(customer_ids) + 1:
288
+ raise VRPCompileError(f"vehicle {vid!r} contains a disconnected subtour")
289
+ routes.append(VRPRoute(vid, customer_ids))
290
+ if decoded_customers != all_customers:
291
+ raise VRPCompileError("decoded MILP routes do not cover every customer exactly once")
292
+ return tuple(routes)
293
+
294
+
295
+ def solve_vrp_milp(instance: VRPInstance, *, backend=None) -> VRPMILPSolveResult:
296
+ compilation = compile_vrp_milp(instance)
297
+ core = execute(compilation.problem, ScipyHighsBackend() if backend is None else backend)
298
+ if core.x is None:
299
+ return VRPMILPSolveResult(None, None, core)
300
+ if core.validation is None or not core.validation.valid:
301
+ raise VRPCompileError("VRP MILP backend returned a candidate that failed SolverPilot core validation")
302
+ routes = decode_vrp_milp_solution(instance, compilation, core.x)
303
+ objective = sum(route_distance(instance, route) for route in routes)
304
+ solution = VRPSolution(
305
+ routes,
306
+ objective,
307
+ method="milp-arc-flow-mtz",
308
+ is_exact=True,
309
+ optimality_proven=core.optimality_evidence.independently_verified_optimal,
310
+ _proof_token=_INDEPENDENT_PROOF_TOKEN,
311
+ metadata={
312
+ "backend": core.trace.backend,
313
+ "backend_status": core.backend_status,
314
+ "backend_reported_optimal": core.optimality_evidence.backend_reported_optimal,
315
+ "independent_optimality_proof": core.optimality_evidence.independently_verified_optimal,
316
+ },
317
+ )
318
+ report = validate_vrp_solution(instance, solution)
319
+ if not report.valid:
320
+ raise VRPCompileError("decoded VRP MILP solution failed independent routing validation: " + "; ".join(report.errors))
321
+ return VRPMILPSolveResult(solution, report, core)