@ohtaman/washi 0.4.0 → 0.6.0

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 (130) hide show
  1. package/README.ja.md +149 -121
  2. package/README.md +149 -121
  3. package/README.zh-CN.md +150 -122
  4. package/THIRD_PARTY_LICENSES.md +3 -4
  5. package/dist/browser/_washi-node-only-BvRk9kiK.js +0 -0
  6. package/dist/browser/browser-runtime-TLCY0hkc.js +1793 -0
  7. package/dist/browser/highs-worker.js +21 -3
  8. package/dist/browser/highs.wasm +0 -0
  9. package/dist/browser/highs.wasm.LICENSE +1 -2
  10. package/dist/browser/loader-DewlSho8.js +643 -0
  11. package/dist/browser/solvers/glpk.js +433 -286
  12. package/dist/browser/solvers/glpk.min.js +1 -1
  13. package/dist/browser/solvers/z3.js +1008 -981
  14. package/dist/browser/solvers/z3.min.js +2 -2
  15. package/dist/browser/washi.js +2778 -673
  16. package/dist/browser/washi.min.js +3 -4
  17. package/dist/lib/_washi-node-only-BvRk9kiK.js +0 -0
  18. package/dist/lib/highs-browser-runtime.js +2120 -4124
  19. package/dist/lib/highs-node-runtime.js +24 -1
  20. package/dist/lib/highs-runtime.js +2 -0
  21. package/dist/lib/highs-worker.js +21 -3
  22. package/dist/lib/highs.wasm +0 -0
  23. package/dist/lib/highs.wasm.LICENSE +1 -2
  24. package/dist/lib/loader-BZUQNvh4.js +649 -0
  25. package/dist/lib/registry-C20sxTwm.js +964 -0
  26. package/dist/lib/solvers/glpk.js +201 -90
  27. package/dist/lib/solvers/z3.js +399 -259
  28. package/dist/lib/washi.js +2363 -1056
  29. package/dist/types/decomposition/benders.d.ts +32 -0
  30. package/dist/types/decomposition/benders.d.ts.map +1 -0
  31. package/dist/types/decomposition/column-generation.d.ts +34 -0
  32. package/dist/types/decomposition/column-generation.d.ts.map +1 -0
  33. package/dist/types/decomposition/index.d.ts +4 -0
  34. package/dist/types/decomposition/index.d.ts.map +1 -0
  35. package/dist/types/decomposition/types.d.ts +230 -0
  36. package/dist/types/decomposition/types.d.ts.map +1 -0
  37. package/dist/types/index.d.ts +5 -4
  38. package/dist/types/index.d.ts.map +1 -1
  39. package/dist/types/modeler/big-m.d.ts +75 -0
  40. package/dist/types/modeler/big-m.d.ts.map +1 -0
  41. package/dist/types/modeler/boolean-expr.d.ts +103 -14
  42. package/dist/types/modeler/boolean-expr.d.ts.map +1 -1
  43. package/dist/types/modeler/constraint.d.ts +47 -9
  44. package/dist/types/modeler/constraint.d.ts.map +1 -1
  45. package/dist/types/modeler/expression.d.ts +123 -28
  46. package/dist/types/modeler/expression.d.ts.map +1 -1
  47. package/dist/types/modeler/index.d.ts +11 -6
  48. package/dist/types/modeler/index.d.ts.map +1 -1
  49. package/dist/types/modeler/linearizer.d.ts +15 -0
  50. package/dist/types/modeler/linearizer.d.ts.map +1 -0
  51. package/dist/types/modeler/lp-writer.d.ts +30 -3
  52. package/dist/types/modeler/lp-writer.d.ts.map +1 -1
  53. package/dist/types/modeler/model.d.ts +623 -131
  54. package/dist/types/modeler/model.d.ts.map +1 -1
  55. package/dist/types/modeler/objective.d.ts +59 -0
  56. package/dist/types/modeler/objective.d.ts.map +1 -0
  57. package/dist/types/modeler/parser.d.ts +34 -10
  58. package/dist/types/modeler/parser.d.ts.map +1 -1
  59. package/dist/types/modeler/quad-expr.d.ts +26 -7
  60. package/dist/types/modeler/quad-expr.d.ts.map +1 -1
  61. package/dist/types/modeler/solution-validator.d.ts +93 -0
  62. package/dist/types/modeler/solution-validator.d.ts.map +1 -0
  63. package/dist/types/modeler/solvers/glpk/solver.d.ts +26 -2
  64. package/dist/types/modeler/solvers/glpk/solver.d.ts.map +1 -1
  65. package/dist/types/modeler/solvers/highs/browser-runtime.d.ts +2 -1
  66. package/dist/types/modeler/solvers/highs/browser-runtime.d.ts.map +1 -1
  67. package/dist/types/modeler/solvers/highs/driver.d.ts +232 -0
  68. package/dist/types/modeler/solvers/highs/driver.d.ts.map +1 -0
  69. package/dist/types/modeler/solvers/highs/index.d.ts +1 -0
  70. package/dist/types/modeler/solvers/highs/index.d.ts.map +1 -1
  71. package/dist/types/modeler/solvers/highs/loader.d.ts +38 -13
  72. package/dist/types/modeler/solvers/highs/loader.d.ts.map +1 -1
  73. package/dist/types/modeler/solvers/highs/node-runtime.d.ts +2 -1
  74. package/dist/types/modeler/solvers/highs/node-runtime.d.ts.map +1 -1
  75. package/dist/types/modeler/solvers/highs/solver.d.ts +12 -5
  76. package/dist/types/modeler/solvers/highs/solver.d.ts.map +1 -1
  77. package/dist/types/modeler/solvers/highs/worker-client.d.ts.map +1 -1
  78. package/dist/types/modeler/solvers/mock.d.ts +14 -5
  79. package/dist/types/modeler/solvers/mock.d.ts.map +1 -1
  80. package/dist/types/modeler/solvers/types.d.ts +73 -11
  81. package/dist/types/modeler/solvers/types.d.ts.map +1 -1
  82. package/dist/types/modeler/solvers/z3/loader.d.ts +2 -3
  83. package/dist/types/modeler/solvers/z3/loader.d.ts.map +1 -1
  84. package/dist/types/modeler/solvers/z3/solver.d.ts +15 -5
  85. package/dist/types/modeler/solvers/z3/solver.d.ts.map +1 -1
  86. package/dist/types/modeler/types.d.ts +86 -1
  87. package/dist/types/modeler/types.d.ts.map +1 -1
  88. package/dist/types/modeler/validation.d.ts +56 -0
  89. package/dist/types/modeler/validation.d.ts.map +1 -0
  90. package/dist/types/modeler/variable.d.ts +48 -14
  91. package/dist/types/modeler/variable.d.ts.map +1 -1
  92. package/docs/examples/diet-sensitivity.md +90 -0
  93. package/docs/examples/ecommerce-coupons.md +106 -0
  94. package/docs/examples/index.md +57 -0
  95. package/docs/examples/knapsack.md +60 -0
  96. package/docs/examples/portfolio-qp.md +83 -0
  97. package/docs/examples/production.md +51 -0
  98. package/docs/examples/shift-scheduling.md +175 -0
  99. package/docs/examples/smt-security.md +74 -0
  100. package/docs/examples/transportation.md +90 -0
  101. package/docs/examples/traveling-salesperson.md +173 -0
  102. package/docs/examples/unsat-core.md +64 -0
  103. package/docs/examples/vehicle-routing.md +206 -0
  104. package/docs/getting-started/ai-agents.md +271 -0
  105. package/docs/getting-started/cross-origin.md +87 -0
  106. package/docs/getting-started/index.md +116 -0
  107. package/docs/getting-started/runtimes.md +227 -0
  108. package/docs/guide/constraints.md +177 -0
  109. package/docs/guide/decomposition.md +399 -0
  110. package/docs/guide/expressions.md +204 -0
  111. package/docs/guide/objectives.md +170 -0
  112. package/docs/guide/unsat-core.md +154 -0
  113. package/docs/guide/variables.md +188 -0
  114. package/docs/ja/getting-started/ai-agents.md +270 -0
  115. package/docs/solvers/custom-solver.md +525 -0
  116. package/docs/solvers/glpk.md +66 -0
  117. package/docs/solvers/highs.md +115 -0
  118. package/docs/solvers/index.md +42 -0
  119. package/docs/solvers/z3.md +78 -0
  120. package/docs/zh/getting-started/ai-agents.md +270 -0
  121. package/llms-full.txt +4065 -0
  122. package/llms.txt +201 -0
  123. package/package.json +32 -16
  124. package/skills/washi/SKILL.md +197 -0
  125. package/dist/browser/browser-runtime-D13gHOT5.js +0 -4189
  126. package/dist/browser/loader-CVt4CloI.js +0 -58
  127. package/dist/lib/async-mutex-Duo4jwzn.js +0 -195
  128. package/dist/lib/constraint-BRuB3ooC.js +0 -90
  129. package/dist/lib/loader-Dti0v9yC.js +0 -50
  130. package/dist/lib/registry-DksqCSjI.js +0 -32
@@ -0,0 +1,173 @@
1
+ # Traveling Salesperson Problem (TSP via Benders Decomposition)
2
+
3
+ | Problem Class | Solver | Key APIs | Links |
4
+ | :--- | :--- | :--- | :--- |
5
+ | Combinatorial OR / Benders | HiGHS (default) | `m.solveBenders()`, `subproblem`, `m.binary()`, `m.constraint()`, `expr` | <ul><li><a href="https://github.com/ohtaman/washi/blob/main/examples/node/14-traveling-salesperson-benders.ts">Source Code</a></li><li><a href="../guide/decomposition.md">Decomposition Guide</a></li></ul> |
6
+
7
+ The **Traveling Salesperson Problem (TSP)** seeks the shortest closed tour that visits every city in a given set once and returns to the starting city.
8
+
9
+ In the **Dantzig-Fulkerson-Johnson (DFJ)** formulation, preventing disconnected cycles (**subtours**) requires subtour elimination constraints for every non-empty proper subset of cities $S \subset V$:
10
+
11
+ $$\sum_{i \in S, j \notin S} x_{ij} \ge 1 \quad \forall S \subset V, \; 2 \le |S| \le |V| - 1$$
12
+
13
+ Because there are $2^n - 2$ subsets, enumerating all subtour constraints upfront is impractical as $n$ grows. With **Benders Decomposition (Lazy Constraint Separation)**, constraints are added only when violated:
14
+ 1. **Master Problem**: Solves the degree-assignment relaxation (each city has 1 incoming and 1 outgoing edge).
15
+ 2. **Separation Subproblem**: Inspects the candidate solution graph. If disconnected cycles exist, it adds a **Benders Feasibility Cut** eliminating the smallest cycle and resolves until a single connected tour is obtained.
16
+
17
+ ## Problem Inputs
18
+
19
+ We consider **5 cities** on a 2D Euclidean coordinate grid:
20
+
21
+ | City | Coordinates $(x, y)$ |
22
+ | :--- | :---: |
23
+ | **City A (0)** | $(0, 0)$ |
24
+ | **City B (1)** | $(1, 0)$ |
25
+ | **City C (2)** | $(1, 2)$ |
26
+ | **City D (3)** | $(0, 2)$ |
27
+ | **City E (4)** | $(0.5, 3)$ |
28
+
29
+ ## Implementation in WaSHI
30
+
31
+ <CodeRunner>
32
+
33
+ ```typescript
34
+ import { Model, expr, minimize, LinExpr } from "@ohtaman/washi";
35
+
36
+ async function runTSP() {
37
+ const cities = [
38
+ { name: "A", x: 0, y: 0 },
39
+ { name: "B", x: 1, y: 0 },
40
+ { name: "C", x: 1, y: 2 },
41
+ { name: "D", x: 0, y: 2 },
42
+ { name: "E", x: 0.5, y: 3 },
43
+ ];
44
+ const n = cities.length;
45
+
46
+ function dist(i, j) {
47
+ const dx = cities[i].x - cities[j].x;
48
+ const dy = cities[i].y - cities[j].y;
49
+ return Math.hypot(dx, dy);
50
+ }
51
+
52
+ // 1. Master Problem: Degree assignment relaxation
53
+ const master = new Model("TSP_BendersMaster");
54
+ const x = {};
55
+
56
+ for (let i = 0; i < n; i++) {
57
+ x[i] = {};
58
+ for (let j = 0; j < n; j++) {
59
+ if (i !== j) {
60
+ x[i][j] = master.binary(`x_${i}_${j}`);
61
+ }
62
+ }
63
+ }
64
+
65
+ // Objective: Minimize total tour distance
66
+ let totalDistance = new LinExpr();
67
+ for (let i = 0; i < n; i++) {
68
+ for (let j = 0; j < n; j++) {
69
+ if (i !== j) {
70
+ totalDistance = totalDistance.add(x[i][j].mul(dist(i, j)));
71
+ }
72
+ }
73
+ }
74
+ master.objective(minimize(totalDistance));
75
+
76
+ // Each city must have 1 outgoing and 1 incoming edge
77
+ for (let i = 0; i < n; i++) {
78
+ let outExpr = new LinExpr();
79
+ let inExpr = new LinExpr();
80
+ for (let j = 0; j < n; j++) {
81
+ if (i !== j) {
82
+ outExpr = outExpr.add(x[i][j]);
83
+ inExpr = inExpr.add(x[j][i]);
84
+ }
85
+ }
86
+ master.constraint(expr`${outExpr} == 1`, `out_${i}`);
87
+ master.constraint(expr`${inExpr} == 1`, `in_${i}`);
88
+ }
89
+
90
+ console.log("Solving TSP with Benders lazy subtour elimination...");
91
+
92
+ // 2. Solve via Benders Decomposition
93
+ let cutCounter = 0;
94
+ const result = await master.solveBenders({
95
+ subproblem: () => {
96
+ // Find active directed edges in the master solution
97
+ const succ = {};
98
+ for (let i = 0; i < n; i++) {
99
+ for (let j = 0; j < n; j++) {
100
+ if (i !== j && Math.round(x[i][j].value ?? 0) === 1) {
101
+ succ[i] = j;
102
+ }
103
+ }
104
+ }
105
+
106
+ // Trace connected cycles
107
+ const visited = new Set();
108
+ const cycles = [];
109
+ for (let i = 0; i < n; i++) {
110
+ if (!visited.has(i)) {
111
+ const cycle = [];
112
+ let curr = i;
113
+ while (!visited.has(curr)) {
114
+ visited.add(curr);
115
+ cycle.push(curr);
116
+ curr = succ[curr];
117
+ }
118
+ cycles.push(cycle);
119
+ }
120
+ }
121
+
122
+ // If more than 1 cycle, subtours exist: inject Benders feasibility cut
123
+ if (cycles.length > 1) {
124
+ const smallestCycle = cycles.reduce((min, c) => (c.length < min.length ? c : min));
125
+ const S = new Set(smallestCycle);
126
+ cutCounter++;
127
+ console.log(`Subtour detected: [${smallestCycle.map(k => cities[k].name).join(" -> ")}]. Adding Benders cut #${cutCounter}.`);
128
+
129
+ let cutLhs = new LinExpr();
130
+ for (const i of S) {
131
+ for (let j = 0; j < n; j++) {
132
+ if (!S.has(j)) {
133
+ cutLhs = cutLhs.add(x[i][j]);
134
+ }
135
+ }
136
+ }
137
+
138
+ return {
139
+ kind: "feasibility",
140
+ constraint: expr`${cutLhs} >= 1`,
141
+ name: `subtour_cut_${cutCounter}`,
142
+ };
143
+ }
144
+
145
+ return null; // Single connected tour found
146
+ },
147
+ });
148
+
149
+ console.log(`\nStatus: ${result.status}`);
150
+ console.log(`Optimal Tour Distance: ${result.objectiveValue?.toFixed(3)}`);
151
+ console.log(`Total Benders Cuts Added: ${result.cutsAdded}`);
152
+ console.log(`Iterations: ${result.iterations}`);
153
+
154
+ console.log("\nOptimal tour edges:");
155
+ for (let i = 0; i < n; i++) {
156
+ for (let j = 0; j < n; j++) {
157
+ if (i !== j && (result.values[`x_${i}_${j}`] ?? 0) > 0.5) {
158
+ console.log(` ${cities[i].name} -> ${cities[j].name} (distance: ${dist(i, j).toFixed(3)})`);
159
+ }
160
+ }
161
+ }
162
+ }
163
+
164
+ runTSP();
165
+ ```
166
+
167
+ </CodeRunner>
168
+
169
+ ## Notes
170
+
171
+ - **Lazy Subtour Elimination**: The master problem starts with only the $2n$ degree constraints, and subtour elimination cuts are added on demand when disconnected cycles appear in the master solution.
172
+ - **Separation Callback**: The `subproblem` callback reads candidate edge values via `x[i][j].value`, traces connected components in $O(n)$, and returns `{ kind: "feasibility", constraint: ... }` when multiple cycles are present.
173
+ - **Termination Condition**: Returning `null` from `subproblem` indicates that no violated subtour constraints remain.
@@ -0,0 +1,64 @@
1
+ # Conflict Diagnosis (Unsat Core)
2
+
3
+ | Problem Class | Solver | Key APIs | Links |
4
+ | :--- | :--- | :--- | :--- |
5
+ | Infeasibility Diagnosis / Unsat Core | Z3 (`@ohtaman/washi/solvers/z3`) | `m.integer()`, `m.continuous()`, `m.constraint()`, `expr`, `m.check()`, `m.conflicts` | <ul><li><a href="https://ohtaman.github.io/washi/demo/?preset=conflict_diagnosis_unsat">Open in Playground</a></li><li><a href="https://github.com/ohtaman/washi/blob/main/examples/node/05-unsat-core-diagnosis.ts">Source Code</a></li></ul> |
6
+
7
+ When business rules, budgets, and operational policies grow complex, constraints inevitably conflict. Traditional solvers simply report "Infeasible", leaving engineers to guess which of hundreds of rules caused the contradiction.
8
+
9
+ This example shows how to isolate the conflicting constraint subset using Z3 Unsat Core extraction.
10
+
11
+ ## The Conflicting Scenario
12
+
13
+ A tech team is staffing a customer project:
14
+ 1. Each engineer costs $8,000 against the project budget (`budget_limit`).
15
+ 2. The customer requires rush delivery (`rush_required`).
16
+ 3. Rush delivery mandates at least 8 engineers (`rush_staffing_rule`).
17
+ 4. The board has imposed a strict budget cap of $50,000 (`strict_board_budget_cap`).
18
+ 5. Labor law requires at least 1 employee on staff (`minimum_one_employee`).
19
+
20
+ Notice that:
21
+
22
+ $$8 \text{ engineers} \times \$8,000 = \$64,000 > \$50,000 \text{ budget!}$$
23
+
24
+ ## Implementation in WaSHI
25
+
26
+ <CodeRunner preset="conflict_diagnosis_unsat">
27
+
28
+ ```typescript
29
+ import { Model, expr } from "@ohtaman/washi";
30
+ import "@ohtaman/washi/solvers/z3";
31
+
32
+ async function diagnoseConflicts() {
33
+ const m = new Model({ name: "ConflictDiagnosis", solver: "z3" });
34
+
35
+ const engineers = m.integer("engineers", { lb: 0, ub: 20 });
36
+ const budget = m.continuous("budget", { lb: 0, ub: 100000 });
37
+ const rushDelivery = m.bool("rushDelivery");
38
+
39
+ // Rule 1: Each engineer costs $8,000
40
+ m.constraint(expr`8000 * ${engineers} <= ${budget}`, "budget_limit");
41
+
42
+ // Rule 2: Customer requires rush delivery
43
+ m.constraint(rushDelivery, "rush_required");
44
+
45
+ // Rule 3: Rush delivery requires at least 8 engineers
46
+ m.constraint(rushDelivery.implies(engineers.ge(8)), "rush_staffing_rule");
47
+
48
+ // Rule 4: Board budget cap of $50,000 (Conflict!)
49
+ m.constraint(expr`${budget} <= 50000`, "strict_board_budget_cap");
50
+
51
+ // Rule 5: Innocent bystander rule
52
+ m.constraint(expr`${engineers} >= 1`, "minimum_one_employee");
53
+
54
+ // Run diagnosis
55
+ const res = await m.check({ solver: "z3" });
56
+
57
+ console.log(`Status: ${res.status}`);
58
+ console.log("Conflicting Constraints (conflicts):", m.conflicts);
59
+ }
60
+
61
+ diagnoseConflicts();
62
+ ```
63
+
64
+ </CodeRunner>
@@ -0,0 +1,206 @@
1
+ # Capacitated Vehicle Routing Problem (CVRP, 50 Customers via Column Generation)
2
+
3
+ | Problem Class | Solver | Key APIs | Links |
4
+ | :--- | :--- | :--- | :--- |
5
+ | Combinatorial OR / Dantzig-Wolfe | HiGHS (default) | `m.solveColumnGeneration()`, `pricing`, `sum()`, `m.binary()`, `m.constraint()`, `expr` | <ul><li><a href="https://github.com/ohtaman/washi/blob/main/examples/node/13-vehicle-routing-column-generation.ts">Source Code</a></li><li><a href="../guide/decomposition.md">Decomposition Guide</a></li></ul> |
6
+
7
+ The **Capacitated Vehicle Routing Problem (CVRP)** determines the minimum number of delivery vehicles needed to visit a set of customers, subject to vehicle cargo capacity, maximum customer stops per shift, and route duration limits.
8
+
9
+ In a monolithic MIP formulation, enumerating all possible customer combinations and visit sequences leads to $O(2^n)$ binary variables. With **Column Generation (Dantzig-Wolfe Decomposition)**, routes are generated on demand:
10
+ 1. **Restricted Master Problem (RMP)**: Solves a set partitioning problem that selects a subset of candidate routes so that every customer is visited once.
11
+ 2. **Pricing Subproblem**: Uses the dual values (shadow prices) from the master LP relaxation to construct improving routes with negative reduced cost ($\bar{c} = 1 - \sum \pi_i < 0$) via cheapest insertion and 2-opt local search.
12
+
13
+ ## Problem Inputs
14
+
15
+ We consider a distribution network with a central depot at $(0, 0)$ and **50 customer locations**:
16
+
17
+ - **Vehicle Cargo Capacity**: At most **10 units** per vehicle.
18
+ - **Maximum Stops**: At most **4 customers** per route.
19
+ - **Maximum Shift Duration**: At most **200 minutes** (transit time + service time) per route.
20
+ - **Transit & Service Times**: Euclidean distances between nodes (1 unit = 1 minute) plus **10 minutes** of unloading service time per customer.
21
+
22
+ ## Implementation in WaSHI
23
+
24
+ <CodeRunner>
25
+
26
+ ```typescript
27
+ import { Model, expr, minimize, sum } from "@ohtaman/washi";
28
+
29
+ // --- Extracted Routing Helpers ---
30
+
31
+ // 2-opt local search to eliminate crossing paths in a tour
32
+ function optimize2Opt(tour, dist) {
33
+ let best = [...tour];
34
+ let improved = true;
35
+
36
+ while (improved) {
37
+ improved = false;
38
+ for (let i = 1; i < best.length - 2; i++) {
39
+ for (let j = i + 1; j < best.length - 1; j++) {
40
+ const dCurrent = dist(best[i - 1], best[i]) + dist(best[j], best[j + 1]);
41
+ const dSwap = dist(best[i - 1], best[j]) + dist(best[i], best[j + 1]);
42
+ if (dSwap < dCurrent) {
43
+ best = [...best.slice(0, i), ...best.slice(i, j + 1).reverse(), ...best.slice(j + 1)];
44
+ improved = true;
45
+ }
46
+ }
47
+ }
48
+ }
49
+ return best;
50
+ }
51
+
52
+ // Inserts a customer into the tour at the position that minimizes detour distance
53
+ function insertCheapest(tour, id, dist) {
54
+ let bestPos = 1;
55
+ let minCost = Infinity;
56
+
57
+ for (let p = 1; p < tour.length; p++) {
58
+ const cost = dist(tour[p - 1], id) + dist(id, tour[p]) - dist(tour[p - 1], tour[p]);
59
+ if (cost < minCost) {
60
+ minCost = cost;
61
+ bestPos = p;
62
+ }
63
+ }
64
+ return [...tour.slice(0, bestPos), id, ...tour.slice(bestPos)];
65
+ }
66
+
67
+ // Calculates transit time and total duration (transit + service) for a tour
68
+ function calcRouteTime(tour, dist, serviceTimes) {
69
+ let travelTime = 0;
70
+ for (let i = 0; i < tour.length - 1; i++) {
71
+ travelTime += dist(tour[i], tour[i + 1]);
72
+ }
73
+ const serviceTime = tour.slice(1, -1).reduce((acc, id) => acc + (serviceTimes[id] ?? 0), 0);
74
+ return { travelTime, totalTime: travelTime + serviceTime };
75
+ }
76
+
77
+ // Pricing subproblem: Finds an improving route with negative reduced cost (1 - dualSum < 0)
78
+ function findImprovingRoute(candidates, config) {
79
+ let bestRoute = null;
80
+ let minReducedCost = -1e-4;
81
+
82
+ for (const seed of candidates.slice(0, 15)) {
83
+ let tour = ["Depot", seed.id, "Depot"];
84
+ let load = seed.demand;
85
+ let dualSum = seed.dual;
86
+
87
+ for (const next of candidates) {
88
+ if (tour.includes(next.id)) continue;
89
+ if (load + next.demand > config.vehicleCapacity) continue;
90
+ if (tour.length - 2 >= config.maxStops) continue;
91
+
92
+ const candidateTour = optimize2Opt(insertCheapest(tour, next.id, config.dist), config.dist);
93
+ const { totalTime } = calcRouteTime(candidateTour, config.dist, config.serviceTimes);
94
+
95
+ if (totalTime <= config.maxRouteTime) {
96
+ tour = candidateTour;
97
+ load += next.demand;
98
+ dualSum += next.dual;
99
+ }
100
+ }
101
+
102
+ const reducedCost = 1 - dualSum;
103
+ if (reducedCost < minReducedCost) {
104
+ minReducedCost = reducedCost;
105
+ bestRoute = tour;
106
+ }
107
+ }
108
+ return bestRoute;
109
+ }
110
+
111
+ // --- Main CVRP via Column Generation ---
112
+
113
+ async function runCVRP() {
114
+ const vehicleCapacity = 10;
115
+ const maxStops = 4;
116
+ const maxRouteTime = 200; // minutes
117
+
118
+ // Central depot and 50 customer locations
119
+ const depot = { id: "Depot", x: 0, y: 0 };
120
+ const customers = Array.from({ length: 50 }, (_, i) => {
121
+ const angle = i * 2.4;
122
+ const radius = 15 + (i % 5) * 8;
123
+ return {
124
+ id: `C${i + 1}`,
125
+ x: Math.round(Math.cos(angle) * radius),
126
+ y: Math.round(Math.sin(angle) * radius),
127
+ demand: 1 + (i % 4),
128
+ serviceTime: 10,
129
+ };
130
+ });
131
+
132
+ const nodes = Object.fromEntries([depot, ...customers].map((n) => [n.id, n]));
133
+ const dist = (a, b) =>
134
+ Math.round(Math.hypot(nodes[a].x - nodes[b].x, nodes[a].y - nodes[b].y));
135
+ const serviceTimes = Object.fromEntries(customers.map((c) => [c.id, c.serviceTime]));
136
+
137
+ const routeConfig = { dist, serviceTimes, vehicleCapacity, maxStops, maxRouteTime };
138
+
139
+ // 1. Build Restricted Master Problem (RMP)
140
+ const master = new Model("CVRP_FleetMaster");
141
+
142
+ // Initial naive routes: 1 dedicated vehicle per customer
143
+ const initialRoutes = customers.map((c) => master.binary(`route_init_${c.id}`));
144
+ master.objective(minimize(sum(initialRoutes)));
145
+
146
+ // Each customer must be visited by exactly 1 vehicle
147
+ for (let i = 0; i < customers.length; i++) {
148
+ master.constraint(expr`${initialRoutes[i]} == 1`, `visit_${customers[i].id}`);
149
+ }
150
+ const visitConstrs = master.constraints;
151
+
152
+ console.log(`Initial state: 50 naive single-customer routes (50 vehicles).`);
153
+ console.log("Solving via Column Generation with dual-directed heuristic pricing...");
154
+
155
+ // 2. Solve via Column Generation
156
+ const result = await master.solveColumnGeneration({
157
+ finalizeAsMIP: true,
158
+ pricing: async () => {
159
+ // 1. Sort customers by shadow price (dual profit) descending
160
+ const candidates = customers
161
+ .map((c, i) => ({ id: c.id, demand: c.demand, dual: visitConstrs[i].dual ?? 0 }))
162
+ .filter((c) => c.dual > 0)
163
+ .sort((a, b) => b.dual - a.dual);
164
+
165
+ // 2. Solve pricing subproblem: find route with negative reduced cost (dualSum > 1)
166
+ const bestRoute = findImprovingRoute(candidates, routeConfig);
167
+ if (!bestRoute) return null; // No improving column found
168
+
169
+ // 3. Propose new column to Restricted Master Problem
170
+ const { travelTime, totalTime } = calcRouteTime(bestRoute, dist, serviceTimes);
171
+ const coefficients = {};
172
+ for (const id of bestRoute.slice(1, -1)) {
173
+ coefficients[`visit_${id}`] = 1;
174
+ }
175
+
176
+ return {
177
+ cost: 1,
178
+ type: "Binary",
179
+ coefficients,
180
+ meta: { tour: bestRoute, travelTime, totalTime },
181
+ };
182
+ },
183
+ });
184
+
185
+ console.log(`\nStatus: ${result.status}`);
186
+ console.log(`Fleet Size: ${Math.round(result.objectiveValue ?? 0)} vehicles (heuristic column generation)`);
187
+ console.log(`Total Columns Generated: ${result.columnsAdded}`);
188
+
189
+ console.log("\nSample routes selected in fleet:");
190
+ const activeRoutes = result.generatedColumns.filter((c) => (c.variable.value ?? 0) > 0.5);
191
+ for (const r of activeRoutes.slice(0, 5)) {
192
+ const meta = r.proposal.meta;
193
+ console.log(` ${meta?.tour.join(" -> ")} (Transit: ${meta?.travelTime}m, Total: ${meta?.totalTime}m)`);
194
+ }
195
+ }
196
+
197
+ runCVRP();
198
+ ```
199
+
200
+ </CodeRunner>
201
+
202
+ ## Notes
203
+
204
+ - **Master Problem and Pricing Separation**: The master problem solves the LP relaxation and final set-partitioning MIP, while the `pricing` callback searches for negative-reduced-cost routes using cheapest insertion and 2-opt local search.
205
+ - **Dual Value Access**: Dual values from the master LP relaxation are read from `visitConstrs[i].dual` to prioritize customers with higher shadow prices during route construction.
206
+ - **Route Metadata**: Each proposed column attaches `{ tour, travelTime, totalTime }` under `meta`, allowing the ordered stop sequence to be inspected from `result.generatedColumns` after the solve completes.