@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.
- package/README.ja.md +149 -121
- package/README.md +149 -121
- package/README.zh-CN.md +150 -122
- package/THIRD_PARTY_LICENSES.md +3 -4
- package/dist/browser/_washi-node-only-BvRk9kiK.js +0 -0
- package/dist/browser/browser-runtime-TLCY0hkc.js +1793 -0
- package/dist/browser/highs-worker.js +21 -3
- package/dist/browser/highs.wasm +0 -0
- package/dist/browser/highs.wasm.LICENSE +1 -2
- package/dist/browser/loader-DewlSho8.js +643 -0
- package/dist/browser/solvers/glpk.js +433 -286
- package/dist/browser/solvers/glpk.min.js +1 -1
- package/dist/browser/solvers/z3.js +1008 -981
- package/dist/browser/solvers/z3.min.js +2 -2
- package/dist/browser/washi.js +2778 -673
- package/dist/browser/washi.min.js +3 -4
- package/dist/lib/_washi-node-only-BvRk9kiK.js +0 -0
- package/dist/lib/highs-browser-runtime.js +2120 -4124
- package/dist/lib/highs-node-runtime.js +24 -1
- package/dist/lib/highs-runtime.js +2 -0
- package/dist/lib/highs-worker.js +21 -3
- package/dist/lib/highs.wasm +0 -0
- package/dist/lib/highs.wasm.LICENSE +1 -2
- package/dist/lib/loader-BZUQNvh4.js +649 -0
- package/dist/lib/registry-C20sxTwm.js +964 -0
- package/dist/lib/solvers/glpk.js +201 -90
- package/dist/lib/solvers/z3.js +399 -259
- package/dist/lib/washi.js +2363 -1056
- package/dist/types/decomposition/benders.d.ts +32 -0
- package/dist/types/decomposition/benders.d.ts.map +1 -0
- package/dist/types/decomposition/column-generation.d.ts +34 -0
- package/dist/types/decomposition/column-generation.d.ts.map +1 -0
- package/dist/types/decomposition/index.d.ts +4 -0
- package/dist/types/decomposition/index.d.ts.map +1 -0
- package/dist/types/decomposition/types.d.ts +230 -0
- package/dist/types/decomposition/types.d.ts.map +1 -0
- package/dist/types/index.d.ts +5 -4
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/modeler/big-m.d.ts +75 -0
- package/dist/types/modeler/big-m.d.ts.map +1 -0
- package/dist/types/modeler/boolean-expr.d.ts +103 -14
- package/dist/types/modeler/boolean-expr.d.ts.map +1 -1
- package/dist/types/modeler/constraint.d.ts +47 -9
- package/dist/types/modeler/constraint.d.ts.map +1 -1
- package/dist/types/modeler/expression.d.ts +123 -28
- package/dist/types/modeler/expression.d.ts.map +1 -1
- package/dist/types/modeler/index.d.ts +11 -6
- package/dist/types/modeler/index.d.ts.map +1 -1
- package/dist/types/modeler/linearizer.d.ts +15 -0
- package/dist/types/modeler/linearizer.d.ts.map +1 -0
- package/dist/types/modeler/lp-writer.d.ts +30 -3
- package/dist/types/modeler/lp-writer.d.ts.map +1 -1
- package/dist/types/modeler/model.d.ts +623 -131
- package/dist/types/modeler/model.d.ts.map +1 -1
- package/dist/types/modeler/objective.d.ts +59 -0
- package/dist/types/modeler/objective.d.ts.map +1 -0
- package/dist/types/modeler/parser.d.ts +34 -10
- package/dist/types/modeler/parser.d.ts.map +1 -1
- package/dist/types/modeler/quad-expr.d.ts +26 -7
- package/dist/types/modeler/quad-expr.d.ts.map +1 -1
- package/dist/types/modeler/solution-validator.d.ts +93 -0
- package/dist/types/modeler/solution-validator.d.ts.map +1 -0
- package/dist/types/modeler/solvers/glpk/solver.d.ts +26 -2
- package/dist/types/modeler/solvers/glpk/solver.d.ts.map +1 -1
- package/dist/types/modeler/solvers/highs/browser-runtime.d.ts +2 -1
- package/dist/types/modeler/solvers/highs/browser-runtime.d.ts.map +1 -1
- package/dist/types/modeler/solvers/highs/driver.d.ts +232 -0
- package/dist/types/modeler/solvers/highs/driver.d.ts.map +1 -0
- package/dist/types/modeler/solvers/highs/index.d.ts +1 -0
- package/dist/types/modeler/solvers/highs/index.d.ts.map +1 -1
- package/dist/types/modeler/solvers/highs/loader.d.ts +38 -13
- package/dist/types/modeler/solvers/highs/loader.d.ts.map +1 -1
- package/dist/types/modeler/solvers/highs/node-runtime.d.ts +2 -1
- package/dist/types/modeler/solvers/highs/node-runtime.d.ts.map +1 -1
- package/dist/types/modeler/solvers/highs/solver.d.ts +12 -5
- package/dist/types/modeler/solvers/highs/solver.d.ts.map +1 -1
- package/dist/types/modeler/solvers/highs/worker-client.d.ts.map +1 -1
- package/dist/types/modeler/solvers/mock.d.ts +14 -5
- package/dist/types/modeler/solvers/mock.d.ts.map +1 -1
- package/dist/types/modeler/solvers/types.d.ts +73 -11
- package/dist/types/modeler/solvers/types.d.ts.map +1 -1
- package/dist/types/modeler/solvers/z3/loader.d.ts +2 -3
- package/dist/types/modeler/solvers/z3/loader.d.ts.map +1 -1
- package/dist/types/modeler/solvers/z3/solver.d.ts +15 -5
- package/dist/types/modeler/solvers/z3/solver.d.ts.map +1 -1
- package/dist/types/modeler/types.d.ts +86 -1
- package/dist/types/modeler/types.d.ts.map +1 -1
- package/dist/types/modeler/validation.d.ts +56 -0
- package/dist/types/modeler/validation.d.ts.map +1 -0
- package/dist/types/modeler/variable.d.ts +48 -14
- package/dist/types/modeler/variable.d.ts.map +1 -1
- package/docs/examples/diet-sensitivity.md +90 -0
- package/docs/examples/ecommerce-coupons.md +106 -0
- package/docs/examples/index.md +57 -0
- package/docs/examples/knapsack.md +60 -0
- package/docs/examples/portfolio-qp.md +83 -0
- package/docs/examples/production.md +51 -0
- package/docs/examples/shift-scheduling.md +175 -0
- package/docs/examples/smt-security.md +74 -0
- package/docs/examples/transportation.md +90 -0
- package/docs/examples/traveling-salesperson.md +173 -0
- package/docs/examples/unsat-core.md +64 -0
- package/docs/examples/vehicle-routing.md +206 -0
- package/docs/getting-started/ai-agents.md +271 -0
- package/docs/getting-started/cross-origin.md +87 -0
- package/docs/getting-started/index.md +116 -0
- package/docs/getting-started/runtimes.md +227 -0
- package/docs/guide/constraints.md +177 -0
- package/docs/guide/decomposition.md +399 -0
- package/docs/guide/expressions.md +204 -0
- package/docs/guide/objectives.md +170 -0
- package/docs/guide/unsat-core.md +154 -0
- package/docs/guide/variables.md +188 -0
- package/docs/ja/getting-started/ai-agents.md +270 -0
- package/docs/solvers/custom-solver.md +525 -0
- package/docs/solvers/glpk.md +66 -0
- package/docs/solvers/highs.md +115 -0
- package/docs/solvers/index.md +42 -0
- package/docs/solvers/z3.md +78 -0
- package/docs/zh/getting-started/ai-agents.md +270 -0
- package/llms-full.txt +4065 -0
- package/llms.txt +201 -0
- package/package.json +32 -16
- package/skills/washi/SKILL.md +197 -0
- package/dist/browser/browser-runtime-D13gHOT5.js +0 -4189
- package/dist/browser/loader-CVt4CloI.js +0 -58
- package/dist/lib/async-mutex-Duo4jwzn.js +0 -195
- package/dist/lib/constraint-BRuB3ooC.js +0 -90
- package/dist/lib/loader-Dti0v9yC.js +0 -50
- 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.
|