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.
- 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.0rc2.dist-info/METADATA +554 -0
- solverpilot-0.1.0rc2.dist-info/RECORD +201 -0
- solverpilot-0.1.0rc2.dist-info/WHEEL +5 -0
- solverpilot-0.1.0rc2.dist-info/entry_points.txt +4 -0
- solverpilot-0.1.0rc2.dist-info/licenses/LICENSE +202 -0
- solverpilot-0.1.0rc2.dist-info/top_level.txt +1 -0
solverpilot/__init__.py
ADDED
|
@@ -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
|
solverpilot/_version.py
ADDED
|
@@ -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)
|