solverpilot 0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- solverpilot/__init__.py +250 -0
- solverpilot/_immutability.py +27 -0
- solverpilot/_version.py +3 -0
- solverpilot/applications/__init__.py +3 -0
- solverpilot/applications/routing/__init__.py +19 -0
- solverpilot/applications/routing/compile_milp.py +321 -0
- solverpilot/applications/routing/diagnostics.py +78 -0
- solverpilot/applications/routing/errors.py +14 -0
- solverpilot/applications/routing/heuristics.py +167 -0
- solverpilot/applications/routing/model.py +259 -0
- solverpilot/applications/routing/reference.py +81 -0
- solverpilot/applications/routing/solution.py +89 -0
- solverpilot/applications/routing/timeline.py +79 -0
- solverpilot/applications/routing/validation.py +169 -0
- solverpilot/applications/tsp/__init__.py +41 -0
- solverpilot/applications/tsp/compile_cp.py +121 -0
- solverpilot/applications/tsp/compile_milp.py +139 -0
- solverpilot/applications/tsp/distance.py +60 -0
- solverpilot/applications/tsp/errors.py +17 -0
- solverpilot/applications/tsp/heuristics.py +112 -0
- solverpilot/applications/tsp/model.py +175 -0
- solverpilot/applications/tsp/reference.py +121 -0
- solverpilot/applications/tsp/solution.py +65 -0
- solverpilot/applications/tsp/validation.py +92 -0
- solverpilot/backends/__init__.py +51 -0
- solverpilot/backends/_casadi_highs_worker.py +92 -0
- solverpilot/backends/base.py +28 -0
- solverpilot/backends/bundled_capi.py +910 -0
- solverpilot/backends/casadi_conic.py +303 -0
- solverpilot/backends/health.py +240 -0
- solverpilot/backends/highspy_native.py +217 -0
- solverpilot/backends/nlopt_native.py +237 -0
- solverpilot/backends/osqp_native.py +297 -0
- solverpilot/backends/protocol_v2.py +119 -0
- solverpilot/backends/pyscipopt_native.py +200 -0
- solverpilot/backends/registry.py +38 -0
- solverpilot/backends/scipy_highs.py +138 -0
- solverpilot/backends/scipy_highs_lp.py +195 -0
- solverpilot/backends/scipy_slsqp_qp.py +133 -0
- solverpilot/backends/scipy_vendored_highs_dev.py +363 -0
- solverpilot/benchmark/__init__.py +23 -0
- solverpilot/benchmark/acquire.py +199 -0
- solverpilot/benchmark/baselines.py +70 -0
- solverpilot/benchmark/bundle.py +37 -0
- solverpilot/benchmark/campaign.py +150 -0
- solverpilot/benchmark/environment.py +115 -0
- solverpilot/benchmark/integrity.py +46 -0
- solverpilot/benchmark/model.py +146 -0
- solverpilot/benchmark/policy.py +182 -0
- solverpilot/benchmark/registry.py +38 -0
- solverpilot/benchmark/report.py +44 -0
- solverpilot/benchmark/runner.py +516 -0
- solverpilot/benchmark/slurm.py +55 -0
- solverpilot/benchmark/specs.py +75 -0
- solverpilot/benchmark/splits.py +181 -0
- solverpilot/benchmark/summary.py +384 -0
- solverpilot/benchmark/verify.py +142 -0
- solverpilot/benchmark/worker.py +71 -0
- solverpilot/bridges/__init__.py +19 -0
- solverpilot/bridges/engine.py +44 -0
- solverpilot/bridges/indicator.py +232 -0
- solverpilot/bridges/registry.py +78 -0
- solverpilot/bridges/transformation-tape.schema.json +53 -0
- solverpilot/bridges/types.py +149 -0
- solverpilot/bridges/validation.py +110 -0
- solverpilot/capabilities/__init__.py +32 -0
- solverpilot/capabilities/backend-capability-v2.schema.json +21 -0
- solverpilot/capabilities/conformance.py +151 -0
- solverpilot/capabilities/enums.py +27 -0
- solverpilot/capabilities/manifest.py +19 -0
- solverpilot/capabilities/ontology.py +73 -0
- solverpilot/capabilities/requirements.py +37 -0
- solverpilot/capabilities/resolve.py +30 -0
- solverpilot/capabilities/v2.py +711 -0
- solverpilot/cli/__init__.py +1 -0
- solverpilot/cli/backend_health.py +77 -0
- solverpilot/cli/benchmark.py +331 -0
- solverpilot/cli/benchmark_worker.py +33 -0
- solverpilot/cli/capabilities.py +65 -0
- solverpilot/conic/__init__.py +11 -0
- solverpilot/conic/backend.py +263 -0
- solverpilot/conic/compiler.py +310 -0
- solverpilot/conic/conformance.py +60 -0
- solverpilot/conic/conic-ir-v1.schema.json +24 -0
- solverpilot/conic/ir.py +299 -0
- solverpilot/conic/validation.py +149 -0
- solverpilot/cp/__init__.py +7 -0
- solverpilot/cp/conformance.py +32 -0
- solverpilot/cp/cp-ir-v1.schema.json +13 -0
- solverpilot/cp/ir.py +170 -0
- solverpilot/cp/model.py +143 -0
- solverpilot/cp/ortools_backend.py +423 -0
- solverpilot/cp/ortools_worker.py +75 -0
- solverpilot/cp/reference.py +51 -0
- solverpilot/cp/serialization.py +117 -0
- solverpilot/cp/validate.py +83 -0
- solverpilot/diagnose/__init__.py +33 -0
- solverpilot/diagnose/infeasibility.py +463 -0
- solverpilot/diagnose/model.py +87 -0
- solverpilot/evaluation/__init__.py +53 -0
- solverpilot/evaluation/metrics.py +150 -0
- solverpilot/evaluation/oracle.py +288 -0
- solverpilot/evaluation/proof.py +53 -0
- solverpilot/evaluation/references.py +73 -0
- solverpilot/evaluation/regret.py +281 -0
- solverpilot/exceptions.py +34 -0
- solverpilot/experimental/__init__.py +32 -0
- solverpilot/extensions/__init__.py +56 -0
- solverpilot/extensions/entrypoints.py +34 -0
- solverpilot/extensions/errors.py +35 -0
- solverpilot/extensions/manager.py +651 -0
- solverpilot/extensions/manifest.py +232 -0
- solverpilot/extensions/model.py +101 -0
- solverpilot/extensions/versioning.py +136 -0
- solverpilot/history/__init__.py +22 -0
- solverpilot/history/errors.py +15 -0
- solverpilot/history/model.py +129 -0
- solverpilot/history/sqlite.py +651 -0
- solverpilot/inspect/__init__.py +3 -0
- solverpilot/inspect/fingerprint.py +199 -0
- solverpilot/intelligence/__init__.py +36 -0
- solverpilot/intelligence/errors.py +16 -0
- solverpilot/intelligence/fingerprint.py +108 -0
- solverpilot/intelligence/leakage.py +69 -0
- solverpilot/intelligence/schema.py +314 -0
- solverpilot/intelligence/shift.py +208 -0
- solverpilot/io/__init__.py +41 -0
- solverpilot/io/binary64.py +49 -0
- solverpilot/io/csv.py +230 -0
- solverpilot/io/errors.py +25 -0
- solverpilot/io/hashing.py +111 -0
- solverpilot/io/json.py +90 -0
- solverpilot/io/mapping.py +195 -0
- solverpilot/io/model.py +407 -0
- solverpilot/io/numeric.py +108 -0
- solverpilot/io/provenance.py +114 -0
- solverpilot/io/result.py +45 -0
- solverpilot/io/source.py +104 -0
- solverpilot/io/strict_json.py +133 -0
- solverpilot/minlp/__init__.py +6 -0
- solverpilot/minlp/compiler.py +66 -0
- solverpilot/minlp/conformance.py +34 -0
- solverpilot/minlp/convexity.py +85 -0
- solverpilot/minlp/ir.py +35 -0
- solverpilot/minlp/minlp-ir-v1.schema.json +1 -0
- solverpilot/minlp/orchestrator.py +220 -0
- solverpilot/minlp/validation.py +28 -0
- solverpilot/model/__init__.py +27 -0
- solverpilot/model/compiler.py +1263 -0
- solverpilot/model/errors.py +22 -0
- solverpilot/model/expression.py +373 -0
- solverpilot/model/model.py +716 -0
- solverpilot/model/sets.py +60 -0
- solverpilot/model/types.py +36 -0
- solverpilot/nlp/__init__.py +7 -0
- solverpilot/nlp/ad.py +166 -0
- solverpilot/nlp/backend.py +113 -0
- solverpilot/nlp/compiler.py +116 -0
- solverpilot/nlp/conformance.py +58 -0
- solverpilot/nlp/ir.py +70 -0
- solverpilot/nlp/nlp-ir-v1.schema.json +1 -0
- solverpilot/nlp/validation.py +60 -0
- solverpilot/plan/__init__.py +33 -0
- solverpilot/plan/model.py +79 -0
- solverpilot/plan/planner.py +201 -0
- solverpilot/plan/production.py +627 -0
- solverpilot/plan/selective_lp.py +65 -0
- solverpilot/problem/__init__.py +16 -0
- solverpilot/problem/enums.py +20 -0
- solverpilot/problem/hashing.py +88 -0
- solverpilot/problem/linear.py +196 -0
- solverpilot/problem/mps.py +447 -0
- solverpilot/problem/quadratic.py +192 -0
- solverpilot/reporting/__init__.py +27 -0
- solverpilot/reporting/explain.py +327 -0
- solverpilot/reporting/model.py +264 -0
- solverpilot/reporting/render.py +55 -0
- solverpilot/runtime/__init__.py +19 -0
- solverpilot/runtime/auto.py +193 -0
- solverpilot/runtime/budgeting.py +43 -0
- solverpilot/runtime/executor.py +126 -0
- solverpilot/runtime/portfolio.py +197 -0
- solverpilot/runtime/result.py +72 -0
- solverpilot/session/__init__.py +29 -0
- solverpilot/session/conformance_v2.py +239 -0
- solverpilot/session/mutations.py +115 -0
- solverpilot/session/persistent-session-trace.schema.json +45 -0
- solverpilot/session/persistent.py +491 -0
- solverpilot/session/reuse.py +81 -0
- solverpilot/session/session.py +236 -0
- solverpilot/trace/__init__.py +3 -0
- solverpilot/trace/schema.py +47 -0
- solverpilot/validate/__init__.py +10 -0
- solverpilot/validate/core.py +179 -0
- solverpilot/validate/result.py +55 -0
- solverpilot-0.1.dist-info/METADATA +554 -0
- solverpilot-0.1.dist-info/RECORD +201 -0
- solverpilot-0.1.dist-info/WHEEL +5 -0
- solverpilot-0.1.dist-info/entry_points.txt +4 -0
- solverpilot-0.1.dist-info/licenses/LICENSE +202 -0
- solverpilot-0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from math import isfinite
|
|
5
|
+
|
|
6
|
+
from .model import VRPInstance
|
|
7
|
+
from .solution import VRPSolution
|
|
8
|
+
from .validation import VRPValidationReport, validate_vrp_solution
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class VRPDiagnosticAction:
|
|
13
|
+
code: str
|
|
14
|
+
target: str
|
|
15
|
+
severity: str
|
|
16
|
+
suggestion: str
|
|
17
|
+
evidence_code: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_ACTIONS = {
|
|
21
|
+
"capacity_exceeded": ("blocking", "Split the route, reassign customers, or use a vehicle with more capacity."),
|
|
22
|
+
"time_window_violation": ("blocking", "Resequence the stop earlier, relax its time window, split the route, or reassign it."),
|
|
23
|
+
"vehicle_shift_late_return": ("blocking", "Split or shorten the route, extend the vehicle shift, or reassign late stops."),
|
|
24
|
+
"max_route_duration_exceeded": ("blocking", "Split the route, add a vehicle, or explicitly increase the permitted route duration."),
|
|
25
|
+
"unassigned_customer": ("blocking", "Add capacity/vehicles or relax the blocking routing constraints before retrying assignment."),
|
|
26
|
+
"duplicate_customer_assignment": ("blocking", "Remove duplicate service and recompute the route plan."),
|
|
27
|
+
"objective_mismatch": ("blocking", "Discard the reported objective and recompute it from the route and distance matrix."),
|
|
28
|
+
"unknown_vehicle": ("blocking", "Repair the vehicle reference before using the plan."),
|
|
29
|
+
"unknown_customer": ("blocking", "Repair the customer reference before using the plan."),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def diagnose_vrp_validation(report: VRPValidationReport) -> tuple[VRPDiagnosticAction, ...]:
|
|
34
|
+
actions: list[VRPDiagnosticAction] = []
|
|
35
|
+
seen: set[tuple[str, str]] = set()
|
|
36
|
+
for issue in report.issues:
|
|
37
|
+
severity, suggestion = _ACTIONS.get(issue.code, ("warning", "Review the validation evidence and repair the route before operational use."))
|
|
38
|
+
target = issue.customer_id or issue.vehicle_id or "solution"
|
|
39
|
+
key = (issue.code, target)
|
|
40
|
+
if key in seen:
|
|
41
|
+
continue
|
|
42
|
+
seen.add(key)
|
|
43
|
+
actions.append(VRPDiagnosticAction(issue.code, target, severity, suggestion, issue.code))
|
|
44
|
+
return tuple(actions)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def diagnose_vrp_solution(instance: VRPInstance, solution: VRPSolution, *, waiting_warning_threshold: float = 30.0) -> tuple[VRPDiagnosticAction, ...]:
|
|
48
|
+
"""Build actionable diagnostics without promoting suggestions to proofs.
|
|
49
|
+
|
|
50
|
+
Validation blockers are emitted first. Feasible waiting time is reported as
|
|
51
|
+
informational/warning evidence because it can be operationally useful but is
|
|
52
|
+
not itself an infeasibility.
|
|
53
|
+
"""
|
|
54
|
+
if isinstance(waiting_warning_threshold, bool):
|
|
55
|
+
raise ValueError("waiting_warning_threshold must be numeric")
|
|
56
|
+
threshold = float(waiting_warning_threshold)
|
|
57
|
+
if not isfinite(threshold) or threshold < 0.0:
|
|
58
|
+
raise ValueError("waiting_warning_threshold must be finite and non-negative")
|
|
59
|
+
report = validate_vrp_solution(instance, solution)
|
|
60
|
+
actions = list(diagnose_vrp_validation(report))
|
|
61
|
+
seen = {(a.code, a.target) for a in actions}
|
|
62
|
+
for timeline in report.timelines:
|
|
63
|
+
for visit in timeline.visits:
|
|
64
|
+
if visit.waiting <= 1e-12:
|
|
65
|
+
continue
|
|
66
|
+
key = ("waiting_time_detected", visit.customer_id)
|
|
67
|
+
if key in seen:
|
|
68
|
+
continue
|
|
69
|
+
seen.add(key)
|
|
70
|
+
severity = "warning" if visit.waiting >= threshold else "info"
|
|
71
|
+
actions.append(VRPDiagnosticAction(
|
|
72
|
+
"waiting_time_detected",
|
|
73
|
+
visit.customer_id,
|
|
74
|
+
severity,
|
|
75
|
+
"Accept the wait if it protects the time window, or consider resequencing; this is a suggestion, not an automatic improvement claim.",
|
|
76
|
+
"timeline_waiting",
|
|
77
|
+
))
|
|
78
|
+
return tuple(actions)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class VRPError(Exception):
|
|
2
|
+
"""Base error for the SolverPilot routing application pack."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class VRPValidationError(VRPError, ValueError):
|
|
6
|
+
"""Raised for invalid VRP data or solution objects."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class VRPCompileError(VRPError, RuntimeError):
|
|
10
|
+
"""Raised when a VRP formulation cannot be compiled or decoded safely."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class VRPReferenceLimitError(VRPError, RuntimeError):
|
|
14
|
+
"""Raised when an exact reference solve exceeds its declared safety limit."""
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from time import perf_counter
|
|
4
|
+
|
|
5
|
+
from .model import VRPInstance
|
|
6
|
+
from .solution import VRPRoute, VRPSolution
|
|
7
|
+
from .validation import route_distance, validate_vrp_solution
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _partial_valid(instance: VRPInstance, routes: list[list[str]]) -> bool:
|
|
11
|
+
# Check each route locally without requiring global coverage by constructing only capacity/timeline facts.
|
|
12
|
+
customers = instance.customer_map
|
|
13
|
+
for vehicle, ids in zip(instance.vehicles, routes):
|
|
14
|
+
if sum(customers[c].demand for c in ids) > vehicle.capacity + 1e-9:
|
|
15
|
+
return False
|
|
16
|
+
if instance.has_time_constraints:
|
|
17
|
+
from .timeline import build_route_timeline
|
|
18
|
+
if not build_route_timeline(instance, VRPRoute(vehicle.id, ids)).valid:
|
|
19
|
+
return False
|
|
20
|
+
return True
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def nearest_feasible_insertion(instance: VRPInstance, *, improve: bool = True) -> VRPSolution:
|
|
24
|
+
started = perf_counter()
|
|
25
|
+
routes: list[list[str]] = [[] for _ in instance.vehicles]
|
|
26
|
+
unassigned: list[str] = []
|
|
27
|
+
for customer in instance.customers:
|
|
28
|
+
options: list[tuple[float, int, int, list[list[str]]]] = []
|
|
29
|
+
for vi, vehicle in enumerate(instance.vehicles):
|
|
30
|
+
for pos in range(len(routes[vi]) + 1):
|
|
31
|
+
candidate_routes = [list(r) for r in routes]
|
|
32
|
+
candidate_routes[vi].insert(pos, customer.id)
|
|
33
|
+
if not _partial_valid(instance, candidate_routes):
|
|
34
|
+
continue
|
|
35
|
+
before = route_distance(instance, VRPRoute(vehicle.id, routes[vi]))
|
|
36
|
+
after = route_distance(instance, VRPRoute(vehicle.id, candidate_routes[vi]))
|
|
37
|
+
options.append((after - before, vi, pos, candidate_routes))
|
|
38
|
+
if not options:
|
|
39
|
+
unassigned.append(customer.id)
|
|
40
|
+
continue
|
|
41
|
+
options.sort(key=lambda x: (x[0], x[1], x[2]))
|
|
42
|
+
routes = options[0][3]
|
|
43
|
+
if improve and not unassigned:
|
|
44
|
+
routes = _local_search(instance, routes)
|
|
45
|
+
route_objects = tuple(VRPRoute(v.id, ids) for v, ids in zip(instance.vehicles, routes))
|
|
46
|
+
objective = sum(route_distance(instance, r) for r in route_objects)
|
|
47
|
+
return VRPSolution(
|
|
48
|
+
route_objects,
|
|
49
|
+
objective,
|
|
50
|
+
method="nearest-feasible-insertion+local-search" if improve else "nearest-feasible-insertion",
|
|
51
|
+
is_exact=False,
|
|
52
|
+
optimality_proven=False,
|
|
53
|
+
unassigned_customer_ids=tuple(unassigned),
|
|
54
|
+
runtime_seconds=perf_counter() - started,
|
|
55
|
+
metadata={"heuristic": True},
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _local_search(instance: VRPInstance, routes: list[list[str]], *, max_passes: int = 20) -> list[list[str]]:
|
|
60
|
+
current = [list(r) for r in routes]
|
|
61
|
+
for _ in range(max_passes):
|
|
62
|
+
best = [list(r) for r in current]
|
|
63
|
+
best_obj = sum(route_distance(instance, VRPRoute(v.id, ids)) for v, ids in zip(instance.vehicles, current))
|
|
64
|
+
improved = False
|
|
65
|
+
# Intra-route reversal; full recomputation is used so asymmetric costs are safe.
|
|
66
|
+
for vi, ids in enumerate(current):
|
|
67
|
+
for i in range(len(ids)):
|
|
68
|
+
for j in range(i + 1, len(ids)):
|
|
69
|
+
cand = [list(r) for r in current]
|
|
70
|
+
cand[vi] = ids[:i] + list(reversed(ids[i : j + 1])) + ids[j + 1 :]
|
|
71
|
+
if not _partial_valid(instance, cand):
|
|
72
|
+
continue
|
|
73
|
+
obj = sum(route_distance(instance, VRPRoute(v.id, x)) for v, x in zip(instance.vehicles, cand))
|
|
74
|
+
if obj < best_obj - 1e-12:
|
|
75
|
+
best_obj, best, improved = obj, cand, True
|
|
76
|
+
# Relocate one customer across any two routes (or within a route).
|
|
77
|
+
for src in range(len(current)):
|
|
78
|
+
for idx in range(len(current[src])):
|
|
79
|
+
cid = current[src][idx]
|
|
80
|
+
for dst in range(len(current)):
|
|
81
|
+
base_without = [list(r) for r in current]
|
|
82
|
+
base_without[src].pop(idx)
|
|
83
|
+
for pos in range(len(base_without[dst]) + 1):
|
|
84
|
+
cand = [list(r) for r in base_without]
|
|
85
|
+
cand[dst].insert(pos, cid)
|
|
86
|
+
if not _partial_valid(instance, cand):
|
|
87
|
+
continue
|
|
88
|
+
obj = sum(route_distance(instance, VRPRoute(v.id, x)) for v, x in zip(instance.vehicles, cand))
|
|
89
|
+
if obj < best_obj - 1e-12:
|
|
90
|
+
best_obj, best, improved = obj, cand, True
|
|
91
|
+
current = best
|
|
92
|
+
if not improved:
|
|
93
|
+
break
|
|
94
|
+
return current
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def clarke_wright_savings(instance: VRPInstance) -> VRPSolution:
|
|
98
|
+
"""Classic parallel Clarke-Wright savings heuristic under explicit preconditions.
|
|
99
|
+
|
|
100
|
+
S8 intentionally limits this implementation to the classical symmetric,
|
|
101
|
+
single-depot, homogeneous-capacity CVRP. It fails closed outside those
|
|
102
|
+
assumptions rather than implying a guarantee for heterogeneous/asymmetric or
|
|
103
|
+
time-window routing.
|
|
104
|
+
"""
|
|
105
|
+
from .errors import VRPValidationError
|
|
106
|
+
if instance.has_time_constraints:
|
|
107
|
+
raise VRPValidationError("Clarke-Wright S8 heuristic is qualified only for CVRP without time constraints")
|
|
108
|
+
starts={v.start_node_id for v in instance.vehicles}; ends={v.end_node_id for v in instance.vehicles}
|
|
109
|
+
if len(starts)!=1 or starts!=ends:
|
|
110
|
+
raise VRPValidationError("Clarke-Wright S8 heuristic requires one common start/end depot")
|
|
111
|
+
capacities={v.capacity for v in instance.vehicles}
|
|
112
|
+
if len(capacities)!=1:
|
|
113
|
+
raise VRPValidationError("Clarke-Wright S8 heuristic requires homogeneous vehicle capacities")
|
|
114
|
+
vals=instance.distances.values
|
|
115
|
+
n=len(vals)
|
|
116
|
+
for i in range(n):
|
|
117
|
+
for j in range(i+1,n):
|
|
118
|
+
if abs(vals[i][j]-vals[j][i])>1e-9:
|
|
119
|
+
raise VRPValidationError("Clarke-Wright S8 heuristic requires a symmetric distance matrix")
|
|
120
|
+
depot=next(iter(starts)); cap=next(iter(capacities)); cmap=instance.customer_map
|
|
121
|
+
routes: dict[int,list[str]]={i:[c.id] for i,c in enumerate(instance.customers)}
|
|
122
|
+
loads: dict[int,float]={i:c.demand for i,c in enumerate(instance.customers)}
|
|
123
|
+
owner={c.id:i for i,c in enumerate(instance.customers)}
|
|
124
|
+
savings=[]
|
|
125
|
+
for a in instance.customers:
|
|
126
|
+
for b in instance.customers:
|
|
127
|
+
if a.id>=b.id: continue
|
|
128
|
+
s=instance.distance(depot,a.node_id)+instance.distance(depot,b.node_id)-instance.distance(a.node_id,b.node_id)
|
|
129
|
+
savings.append((-s,a.id,b.id))
|
|
130
|
+
savings.sort()
|
|
131
|
+
for neg_s,a,b in savings:
|
|
132
|
+
# Once the fleet limit is met, do not accept a non-positive merge.
|
|
133
|
+
# Before that point a non-positive merge may still be necessary to
|
|
134
|
+
# reduce the number of routes to the available vehicle count.
|
|
135
|
+
if -neg_s <= 1e-12 and len(routes) <= len(instance.vehicles):
|
|
136
|
+
break
|
|
137
|
+
ra=owner[a]; rb=owner[b]
|
|
138
|
+
if ra==rb or ra not in routes or rb not in routes: continue
|
|
139
|
+
A=routes[ra]; B=routes[rb]
|
|
140
|
+
if loads[ra]+loads[rb] > cap+1e-9: continue
|
|
141
|
+
candidates=[]
|
|
142
|
+
# Classical endpoint merges, including route reversal because costs are symmetric.
|
|
143
|
+
if A[-1]==a and B[0]==b: candidates.append(A+B)
|
|
144
|
+
if A[0]==a and B[-1]==b: candidates.append(B+A)
|
|
145
|
+
if A[0]==a and B[0]==b: candidates.append(list(reversed(A))+B)
|
|
146
|
+
if A[-1]==a and B[-1]==b: candidates.append(A+list(reversed(B)))
|
|
147
|
+
if not candidates: continue
|
|
148
|
+
merged=min(candidates, key=lambda ids: (route_distance(instance, VRPRoute(instance.vehicles[0].id,ids)), tuple(ids)))
|
|
149
|
+
routes[ra]=merged; loads[ra]+=loads[rb]
|
|
150
|
+
del routes[rb]; del loads[rb]
|
|
151
|
+
for cid in merged: owner[cid]=ra
|
|
152
|
+
route_lists=sorted(routes.values(), key=lambda ids: (tuple(ids),len(ids)))
|
|
153
|
+
if len(route_lists)>len(instance.vehicles):
|
|
154
|
+
fallback = nearest_feasible_insertion(instance, improve=True)
|
|
155
|
+
return VRPSolution(
|
|
156
|
+
fallback.routes, fallback.objective, method="clarke-wright-savings:fallback-nearest",
|
|
157
|
+
is_exact=False, optimality_proven=False,
|
|
158
|
+
unassigned_customer_ids=fallback.unassigned_customer_ids,
|
|
159
|
+
runtime_seconds=fallback.runtime_seconds,
|
|
160
|
+
metadata={"qualified_scope":"symmetric-single-depot-homogeneous-cvrp","fallback":"nearest-feasible-insertion"},
|
|
161
|
+
)
|
|
162
|
+
assigned=route_lists; unassigned=()
|
|
163
|
+
route_objects=[]
|
|
164
|
+
for i,v in enumerate(instance.vehicles):
|
|
165
|
+
route_objects.append(VRPRoute(v.id, assigned[i] if i<len(assigned) else ()))
|
|
166
|
+
objective=sum(route_distance(instance,r) for r in route_objects)
|
|
167
|
+
return VRPSolution(route_objects,objective,method="clarke-wright-savings",is_exact=False,optimality_proven=False,unassigned_customer_ids=unassigned,metadata={"qualified_scope":"symmetric-single-depot-homogeneous-cvrp"})
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from math import isfinite
|
|
5
|
+
from types import MappingProxyType
|
|
6
|
+
from typing import Any, Mapping, Sequence
|
|
7
|
+
|
|
8
|
+
from .errors import VRPValidationError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _meta(value: Mapping[str, Any] | None) -> Mapping[str, Any]:
|
|
12
|
+
return MappingProxyType(dict(value or {}))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _clean_id(value: object, label: str) -> str:
|
|
16
|
+
text = str(value).strip()
|
|
17
|
+
if not text:
|
|
18
|
+
raise VRPValidationError(f"{label} must be a non-empty string")
|
|
19
|
+
return text
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _number(value: object, label: str, *, positive: bool = False) -> float:
|
|
23
|
+
if isinstance(value, bool):
|
|
24
|
+
raise VRPValidationError(f"{label} must be numeric, not bool")
|
|
25
|
+
try:
|
|
26
|
+
out = float(value)
|
|
27
|
+
except (TypeError, ValueError) as exc:
|
|
28
|
+
raise VRPValidationError(f"{label} must be numeric") from exc
|
|
29
|
+
if not isfinite(out):
|
|
30
|
+
raise VRPValidationError(f"{label} must be finite")
|
|
31
|
+
if positive and out <= 0.0:
|
|
32
|
+
raise VRPValidationError(f"{label} must be positive")
|
|
33
|
+
if not positive and out < 0.0:
|
|
34
|
+
raise VRPValidationError(f"{label} must be non-negative")
|
|
35
|
+
return out
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True, slots=True)
|
|
39
|
+
class VRPTimeWindow:
|
|
40
|
+
start: float
|
|
41
|
+
end: float
|
|
42
|
+
|
|
43
|
+
def __post_init__(self) -> None:
|
|
44
|
+
start = _number(self.start, "time-window start")
|
|
45
|
+
end = _number(self.end, "time-window end")
|
|
46
|
+
if end < start:
|
|
47
|
+
raise VRPValidationError("time-window end must be >= start")
|
|
48
|
+
object.__setattr__(self, "start", start)
|
|
49
|
+
object.__setattr__(self, "end", end)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True, slots=True)
|
|
53
|
+
class VRPNode:
|
|
54
|
+
id: str
|
|
55
|
+
metadata: Mapping[str, Any] = field(default_factory=dict, compare=False)
|
|
56
|
+
|
|
57
|
+
def __post_init__(self) -> None:
|
|
58
|
+
object.__setattr__(self, "id", _clean_id(self.id, "node id"))
|
|
59
|
+
object.__setattr__(self, "metadata", _meta(self.metadata))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True, slots=True)
|
|
63
|
+
class VRPCustomer:
|
|
64
|
+
id: str
|
|
65
|
+
node_id: str
|
|
66
|
+
demand: float = 0.0
|
|
67
|
+
service_duration: float = 0.0
|
|
68
|
+
time_window: VRPTimeWindow | None = None
|
|
69
|
+
metadata: Mapping[str, Any] = field(default_factory=dict, compare=False)
|
|
70
|
+
|
|
71
|
+
def __post_init__(self) -> None:
|
|
72
|
+
object.__setattr__(self, "id", _clean_id(self.id, "customer id"))
|
|
73
|
+
object.__setattr__(self, "node_id", _clean_id(self.node_id, "customer node_id"))
|
|
74
|
+
object.__setattr__(self, "demand", _number(self.demand, f"customer {self.id} demand"))
|
|
75
|
+
object.__setattr__(self, "service_duration", _number(self.service_duration, f"customer {self.id} service_duration"))
|
|
76
|
+
if self.time_window is not None and not isinstance(self.time_window, VRPTimeWindow):
|
|
77
|
+
raise VRPValidationError("customer time_window must be VRPTimeWindow or None")
|
|
78
|
+
object.__setattr__(self, "metadata", _meta(self.metadata))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True, slots=True)
|
|
82
|
+
class VRPVehicle:
|
|
83
|
+
id: str
|
|
84
|
+
start_node_id: str
|
|
85
|
+
end_node_id: str | None = None
|
|
86
|
+
capacity: float = 1.0
|
|
87
|
+
shift_window: VRPTimeWindow | None = None
|
|
88
|
+
max_route_duration: float | None = None
|
|
89
|
+
metadata: Mapping[str, Any] = field(default_factory=dict, compare=False)
|
|
90
|
+
|
|
91
|
+
def __post_init__(self) -> None:
|
|
92
|
+
vehicle_id = _clean_id(self.id, "vehicle id")
|
|
93
|
+
start = _clean_id(self.start_node_id, f"vehicle {vehicle_id} start_node_id")
|
|
94
|
+
end = start if self.end_node_id is None else _clean_id(self.end_node_id, f"vehicle {vehicle_id} end_node_id")
|
|
95
|
+
capacity = _number(self.capacity, f"vehicle {vehicle_id} capacity", positive=True)
|
|
96
|
+
if self.shift_window is not None and not isinstance(self.shift_window, VRPTimeWindow):
|
|
97
|
+
raise VRPValidationError("vehicle shift_window must be VRPTimeWindow or None")
|
|
98
|
+
duration = None
|
|
99
|
+
if self.max_route_duration is not None:
|
|
100
|
+
duration = _number(self.max_route_duration, f"vehicle {vehicle_id} max_route_duration", positive=True)
|
|
101
|
+
object.__setattr__(self, "id", vehicle_id)
|
|
102
|
+
object.__setattr__(self, "start_node_id", start)
|
|
103
|
+
object.__setattr__(self, "end_node_id", end)
|
|
104
|
+
object.__setattr__(self, "capacity", capacity)
|
|
105
|
+
object.__setattr__(self, "max_route_duration", duration)
|
|
106
|
+
object.__setattr__(self, "metadata", _meta(self.metadata))
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def start_time(self) -> float:
|
|
110
|
+
return 0.0 if self.shift_window is None else self.shift_window.start
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def latest_return(self) -> float | None:
|
|
114
|
+
return None if self.shift_window is None else self.shift_window.end
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass(frozen=True, slots=True)
|
|
118
|
+
class VRPMatrix:
|
|
119
|
+
values: tuple[tuple[float, ...], ...]
|
|
120
|
+
|
|
121
|
+
def __init__(self, values: Sequence[Sequence[float]], *, label: str = "matrix") -> None:
|
|
122
|
+
try:
|
|
123
|
+
raw = tuple(tuple(row) for row in values)
|
|
124
|
+
except TypeError as exc:
|
|
125
|
+
raise VRPValidationError(f"{label} must be a sequence of rows") from exc
|
|
126
|
+
if len(raw) < 2:
|
|
127
|
+
raise VRPValidationError(f"{label} must contain at least two nodes")
|
|
128
|
+
n = len(raw)
|
|
129
|
+
if any(len(row) != n for row in raw):
|
|
130
|
+
raise VRPValidationError(f"{label} must be square")
|
|
131
|
+
rows: list[tuple[float, ...]] = []
|
|
132
|
+
for i, row in enumerate(raw):
|
|
133
|
+
out: list[float] = []
|
|
134
|
+
for j, value in enumerate(row):
|
|
135
|
+
if isinstance(value, bool):
|
|
136
|
+
raise VRPValidationError(f"{label}[{i},{j}] must be numeric, not bool")
|
|
137
|
+
try:
|
|
138
|
+
v = float(value)
|
|
139
|
+
except (TypeError, ValueError) as exc:
|
|
140
|
+
raise VRPValidationError(f"{label}[{i},{j}] must be numeric") from exc
|
|
141
|
+
if not isfinite(v) or v < 0.0:
|
|
142
|
+
raise VRPValidationError(f"{label}[{i},{j}] must be finite and non-negative")
|
|
143
|
+
out.append(v)
|
|
144
|
+
rows.append(tuple(out))
|
|
145
|
+
object.__setattr__(self, "values", tuple(rows))
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def n_nodes(self) -> int:
|
|
149
|
+
return len(self.values)
|
|
150
|
+
|
|
151
|
+
def edge(self, i: int, j: int) -> float:
|
|
152
|
+
if isinstance(i, bool) or isinstance(j, bool) or not isinstance(i, int) or not isinstance(j, int):
|
|
153
|
+
raise VRPValidationError("matrix indices must be integers")
|
|
154
|
+
if not 0 <= i < self.n_nodes or not 0 <= j < self.n_nodes:
|
|
155
|
+
raise VRPValidationError("matrix index out of range")
|
|
156
|
+
return self.values[i][j]
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@dataclass(frozen=True, slots=True)
|
|
160
|
+
class VRPInstance:
|
|
161
|
+
nodes: tuple[VRPNode, ...]
|
|
162
|
+
customers: tuple[VRPCustomer, ...]
|
|
163
|
+
vehicles: tuple[VRPVehicle, ...]
|
|
164
|
+
distances: VRPMatrix
|
|
165
|
+
travel_times: VRPMatrix | None = None
|
|
166
|
+
name: str = "vrp"
|
|
167
|
+
metadata: Mapping[str, Any] = field(default_factory=dict, compare=False)
|
|
168
|
+
|
|
169
|
+
def __init__(
|
|
170
|
+
self,
|
|
171
|
+
nodes: Sequence[VRPNode],
|
|
172
|
+
customers: Sequence[VRPCustomer],
|
|
173
|
+
vehicles: Sequence[VRPVehicle],
|
|
174
|
+
distances: VRPMatrix | Sequence[Sequence[float]],
|
|
175
|
+
*,
|
|
176
|
+
travel_times: VRPMatrix | Sequence[Sequence[float]] | None = None,
|
|
177
|
+
name: str = "vrp",
|
|
178
|
+
metadata: Mapping[str, Any] | None = None,
|
|
179
|
+
) -> None:
|
|
180
|
+
node_tuple = tuple(nodes)
|
|
181
|
+
customer_tuple = tuple(customers)
|
|
182
|
+
vehicle_tuple = tuple(vehicles)
|
|
183
|
+
if len(node_tuple) < 2:
|
|
184
|
+
raise VRPValidationError("VRP requires at least two nodes")
|
|
185
|
+
if not customer_tuple:
|
|
186
|
+
raise VRPValidationError("VRP requires at least one customer")
|
|
187
|
+
if not vehicle_tuple:
|
|
188
|
+
raise VRPValidationError("VRP requires at least one vehicle")
|
|
189
|
+
node_ids = tuple(n.id for n in node_tuple)
|
|
190
|
+
customer_ids = tuple(c.id for c in customer_tuple)
|
|
191
|
+
vehicle_ids = tuple(v.id for v in vehicle_tuple)
|
|
192
|
+
if len(set(node_ids)) != len(node_ids):
|
|
193
|
+
raise VRPValidationError("VRP node ids must be unique")
|
|
194
|
+
if len(set(customer_ids)) != len(customer_ids):
|
|
195
|
+
raise VRPValidationError("VRP customer ids must be unique")
|
|
196
|
+
if len(set(vehicle_ids)) != len(vehicle_ids):
|
|
197
|
+
raise VRPValidationError("VRP vehicle ids must be unique")
|
|
198
|
+
node_set = set(node_ids)
|
|
199
|
+
unknown_customer_nodes = sorted({c.node_id for c in customer_tuple} - node_set)
|
|
200
|
+
unknown_vehicle_nodes = sorted({x for v in vehicle_tuple for x in (v.start_node_id, v.end_node_id)} - node_set)
|
|
201
|
+
if unknown_customer_nodes:
|
|
202
|
+
raise VRPValidationError(f"customers reference unknown nodes: {unknown_customer_nodes}")
|
|
203
|
+
if unknown_vehicle_nodes:
|
|
204
|
+
raise VRPValidationError(f"vehicles reference unknown nodes: {unknown_vehicle_nodes}")
|
|
205
|
+
d = distances if isinstance(distances, VRPMatrix) else VRPMatrix(distances, label="distance matrix")
|
|
206
|
+
if d.n_nodes != len(node_tuple):
|
|
207
|
+
raise VRPValidationError("distance matrix size must match node count")
|
|
208
|
+
tt = None
|
|
209
|
+
if travel_times is not None:
|
|
210
|
+
tt = travel_times if isinstance(travel_times, VRPMatrix) else VRPMatrix(travel_times, label="travel-time matrix")
|
|
211
|
+
if tt.n_nodes != len(node_tuple):
|
|
212
|
+
raise VRPValidationError("travel-time matrix size must match node count")
|
|
213
|
+
requires_time = any(c.time_window is not None for c in customer_tuple) or any(
|
|
214
|
+
v.shift_window is not None or v.max_route_duration is not None for v in vehicle_tuple
|
|
215
|
+
)
|
|
216
|
+
if requires_time and tt is None:
|
|
217
|
+
raise VRPValidationError("time-constrained VRP requires an explicit travel-time matrix; distance is never used as implicit time")
|
|
218
|
+
clean_name = _clean_id(name, "VRP name")
|
|
219
|
+
object.__setattr__(self, "nodes", node_tuple)
|
|
220
|
+
object.__setattr__(self, "customers", customer_tuple)
|
|
221
|
+
object.__setattr__(self, "vehicles", vehicle_tuple)
|
|
222
|
+
object.__setattr__(self, "distances", d)
|
|
223
|
+
object.__setattr__(self, "travel_times", tt)
|
|
224
|
+
object.__setattr__(self, "name", clean_name)
|
|
225
|
+
object.__setattr__(self, "metadata", _meta(metadata))
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def node_index(self) -> Mapping[str, int]:
|
|
229
|
+
return MappingProxyType({node.id: i for i, node in enumerate(self.nodes)})
|
|
230
|
+
|
|
231
|
+
@property
|
|
232
|
+
def customer_map(self) -> Mapping[str, VRPCustomer]:
|
|
233
|
+
return MappingProxyType({c.id: c for c in self.customers})
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def vehicle_map(self) -> Mapping[str, VRPVehicle]:
|
|
237
|
+
return MappingProxyType({v.id: v for v in self.vehicles})
|
|
238
|
+
|
|
239
|
+
@property
|
|
240
|
+
def has_time_constraints(self) -> bool:
|
|
241
|
+
return any(c.time_window is not None for c in self.customers) or any(
|
|
242
|
+
v.shift_window is not None or v.max_route_duration is not None for v in self.vehicles
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
def distance(self, from_node_id: str, to_node_id: str) -> float:
|
|
246
|
+
idx = self.node_index
|
|
247
|
+
try:
|
|
248
|
+
return self.distances.values[idx[from_node_id]][idx[to_node_id]]
|
|
249
|
+
except KeyError as exc:
|
|
250
|
+
raise VRPValidationError(f"unknown node in distance lookup: {exc.args[0]}") from exc
|
|
251
|
+
|
|
252
|
+
def travel_time(self, from_node_id: str, to_node_id: str) -> float:
|
|
253
|
+
if self.travel_times is None:
|
|
254
|
+
raise VRPValidationError("travel-time matrix is unavailable")
|
|
255
|
+
idx = self.node_index
|
|
256
|
+
try:
|
|
257
|
+
return self.travel_times.values[idx[from_node_id]][idx[to_node_id]]
|
|
258
|
+
except KeyError as exc:
|
|
259
|
+
raise VRPValidationError(f"unknown node in travel-time lookup: {exc.args[0]}") from exc
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from itertools import permutations
|
|
5
|
+
from math import comb, factorial
|
|
6
|
+
from time import perf_counter
|
|
7
|
+
|
|
8
|
+
from .errors import VRPReferenceLimitError
|
|
9
|
+
from .model import VRPInstance
|
|
10
|
+
from .solution import VRPRoute, VRPSolution, _INDEPENDENT_PROOF_TOKEN
|
|
11
|
+
from .validation import route_distance, validate_vrp_solution
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True, slots=True)
|
|
15
|
+
class VRPReferenceResult:
|
|
16
|
+
solution: VRPSolution | None
|
|
17
|
+
evaluations: int
|
|
18
|
+
search_space_upper_bound: int
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _compositions(total: int, parts: int):
|
|
22
|
+
if parts == 1:
|
|
23
|
+
yield (total,)
|
|
24
|
+
return
|
|
25
|
+
for first in range(total + 1):
|
|
26
|
+
for rest in _compositions(total - first, parts - 1):
|
|
27
|
+
yield (first,) + rest
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def solve_vrp_reference(
|
|
31
|
+
instance: VRPInstance,
|
|
32
|
+
*,
|
|
33
|
+
max_customers: int = 8,
|
|
34
|
+
max_evaluations: int = 2_000_000,
|
|
35
|
+
) -> VRPReferenceResult:
|
|
36
|
+
n = len(instance.customers)
|
|
37
|
+
m = len(instance.vehicles)
|
|
38
|
+
if isinstance(max_customers, bool) or not isinstance(max_customers, int) or max_customers < 1:
|
|
39
|
+
raise ValueError("max_customers must be a positive integer")
|
|
40
|
+
if isinstance(max_evaluations, bool) or not isinstance(max_evaluations, int) or max_evaluations < 1:
|
|
41
|
+
raise ValueError("max_evaluations must be a positive integer")
|
|
42
|
+
if n > max_customers:
|
|
43
|
+
raise VRPReferenceLimitError(f"reference VRP solver is limited to {max_customers} customers")
|
|
44
|
+
search_space = factorial(n) * comb(n + m - 1, m - 1)
|
|
45
|
+
if search_space > max_evaluations:
|
|
46
|
+
raise VRPReferenceLimitError(f"reference VRP search-space upper bound {search_space} exceeds max_evaluations={max_evaluations}")
|
|
47
|
+
customer_ids = tuple(c.id for c in instance.customers)
|
|
48
|
+
best_routes: tuple[VRPRoute, ...] | None = None
|
|
49
|
+
best_objective = float("inf")
|
|
50
|
+
evaluations = 0
|
|
51
|
+
started = perf_counter()
|
|
52
|
+
for order in permutations(customer_ids):
|
|
53
|
+
for sizes in _compositions(n, m):
|
|
54
|
+
evaluations += 1
|
|
55
|
+
routes: list[VRPRoute] = []
|
|
56
|
+
pos = 0
|
|
57
|
+
for vehicle, size in zip(instance.vehicles, sizes):
|
|
58
|
+
route_ids = order[pos : pos + size]
|
|
59
|
+
pos += size
|
|
60
|
+
routes.append(VRPRoute(vehicle.id, route_ids))
|
|
61
|
+
objective = sum(route_distance(instance, route) for route in routes)
|
|
62
|
+
if objective >= best_objective - 1e-12:
|
|
63
|
+
continue
|
|
64
|
+
candidate = VRPSolution(routes, objective, method="reference-exhaustive", is_exact=True, optimality_proven=True, _proof_token=_INDEPENDENT_PROOF_TOKEN)
|
|
65
|
+
report = validate_vrp_solution(instance, candidate)
|
|
66
|
+
if report.valid:
|
|
67
|
+
best_objective = objective
|
|
68
|
+
best_routes = tuple(routes)
|
|
69
|
+
if best_routes is None:
|
|
70
|
+
return VRPReferenceResult(None, evaluations, search_space)
|
|
71
|
+
solution = VRPSolution(
|
|
72
|
+
best_routes,
|
|
73
|
+
best_objective,
|
|
74
|
+
method="reference-exhaustive",
|
|
75
|
+
is_exact=True,
|
|
76
|
+
optimality_proven=True,
|
|
77
|
+
runtime_seconds=perf_counter() - started,
|
|
78
|
+
_proof_token=_INDEPENDENT_PROOF_TOKEN,
|
|
79
|
+
metadata={"evaluations": evaluations, "search_space_upper_bound": search_space},
|
|
80
|
+
)
|
|
81
|
+
return VRPReferenceResult(solution, evaluations, search_space)
|