@ohtaman/washi 0.5.0 → 0.7.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 (107) hide show
  1. package/README.ja.md +103 -386
  2. package/README.md +104 -387
  3. package/README.zh-CN.md +103 -386
  4. package/THIRD_PARTY_LICENSES.md +5 -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 +1 -1
  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 +3 -3
  12. package/dist/browser/solvers/glpk.min.js +1 -1
  13. package/dist/browser/solvers/z3.js +7 -1
  14. package/dist/browser/solvers/z3.min.js +1 -1
  15. package/dist/browser/washi.js +1708 -540
  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 +1 -1
  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-BEfvpZW3.js → registry-D7TZ3Wh_.js} +142 -59
  26. package/dist/lib/solvers/glpk.js +4 -4
  27. package/dist/lib/solvers/z3.js +1 -1
  28. package/dist/lib/washi.js +1576 -465
  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 +4 -3
  38. package/dist/types/index.d.ts.map +1 -1
  39. package/dist/types/modeler/boolean-expr.d.ts.map +1 -1
  40. package/dist/types/modeler/constraint.d.ts +11 -0
  41. package/dist/types/modeler/constraint.d.ts.map +1 -1
  42. package/dist/types/modeler/expression.d.ts +57 -26
  43. package/dist/types/modeler/expression.d.ts.map +1 -1
  44. package/dist/types/modeler/index.d.ts +4 -3
  45. package/dist/types/modeler/index.d.ts.map +1 -1
  46. package/dist/types/modeler/linearizer.d.ts +15 -0
  47. package/dist/types/modeler/linearizer.d.ts.map +1 -0
  48. package/dist/types/modeler/lp-writer.d.ts +3 -1
  49. package/dist/types/modeler/lp-writer.d.ts.map +1 -1
  50. package/dist/types/modeler/model.d.ts +159 -8
  51. package/dist/types/modeler/model.d.ts.map +1 -1
  52. package/dist/types/modeler/parser.d.ts +25 -5
  53. package/dist/types/modeler/parser.d.ts.map +1 -1
  54. package/dist/types/modeler/solvers/glpk/solver.d.ts.map +1 -1
  55. package/dist/types/modeler/solvers/highs/browser-runtime.d.ts +2 -1
  56. package/dist/types/modeler/solvers/highs/browser-runtime.d.ts.map +1 -1
  57. package/dist/types/modeler/solvers/highs/driver.d.ts +232 -0
  58. package/dist/types/modeler/solvers/highs/driver.d.ts.map +1 -0
  59. package/dist/types/modeler/solvers/highs/index.d.ts +1 -0
  60. package/dist/types/modeler/solvers/highs/index.d.ts.map +1 -1
  61. package/dist/types/modeler/solvers/highs/loader.d.ts +4 -4
  62. package/dist/types/modeler/solvers/highs/loader.d.ts.map +1 -1
  63. package/dist/types/modeler/solvers/highs/node-runtime.d.ts +2 -1
  64. package/dist/types/modeler/solvers/highs/node-runtime.d.ts.map +1 -1
  65. package/dist/types/modeler/solvers/highs/solver.d.ts +1 -1
  66. package/dist/types/modeler/solvers/highs/solver.d.ts.map +1 -1
  67. package/dist/types/modeler/solvers/highs/worker-client.d.ts.map +1 -1
  68. package/dist/types/modeler/solvers/types.d.ts +4 -0
  69. package/dist/types/modeler/solvers/types.d.ts.map +1 -1
  70. package/dist/types/modeler/types.d.ts +27 -0
  71. package/dist/types/modeler/types.d.ts.map +1 -1
  72. package/docs/examples/diet-sensitivity.md +15 -14
  73. package/docs/examples/ecommerce-coupons.md +15 -22
  74. package/docs/examples/index.md +18 -12
  75. package/docs/examples/knapsack.md +8 -7
  76. package/docs/examples/portfolio-qp.md +11 -4
  77. package/docs/examples/production.md +4 -4
  78. package/docs/examples/shift-scheduling.md +20 -7
  79. package/docs/examples/smt-security.md +16 -4
  80. package/docs/examples/transportation.md +46 -19
  81. package/docs/examples/traveling-salesperson.md +157 -0
  82. package/docs/examples/unsat-core.md +15 -5
  83. package/docs/examples/vehicle-routing.md +208 -0
  84. package/docs/getting-started/ai-agents.md +33 -18
  85. package/docs/getting-started/cross-origin.md +56 -28
  86. package/docs/getting-started/index.md +40 -69
  87. package/docs/getting-started/runtimes.md +66 -61
  88. package/docs/guide/constraints.md +47 -41
  89. package/docs/guide/decomposition.md +267 -0
  90. package/docs/guide/expressions.md +90 -39
  91. package/docs/guide/formulation-tips.md +212 -0
  92. package/docs/guide/objectives.md +7 -4
  93. package/docs/guide/unsat-core.md +28 -9
  94. package/docs/guide/variables.md +33 -38
  95. package/docs/ja/getting-started/ai-agents.md +32 -18
  96. package/docs/solvers/custom-solver.md +1 -1
  97. package/docs/solvers/highs.md +48 -5
  98. package/docs/solvers/index.md +4 -4
  99. package/docs/solvers/z3.md +3 -0
  100. package/docs/zh/getting-started/ai-agents.md +32 -18
  101. package/llms-full.txt +1536 -478
  102. package/llms.txt +28 -22
  103. package/package.json +19 -16
  104. package/skills/washi/SKILL.md +21 -17
  105. package/dist/browser/browser-runtime-D13gHOT5.js +0 -4189
  106. package/dist/browser/loader-CtaDHef_.js +0 -273
  107. package/dist/lib/loader-DmNC_UNk.js +0 -279
@@ -2,7 +2,7 @@
2
2
 
3
3
  | Problem Class | Solver | Key APIs | Links |
4
4
  | :--- | :--- | :--- | :--- |
5
- | Propositional Logic & SMT | Z3 (`@ohtaman/washi/solvers/z3`) | `m.bool()`, `m.integer()`, `m.constraint()`, `expr`, `m.check()` | <ul><li><a href="https://ohtaman.github.io/washi/demo/?preset=smt_sat_scheduling">Open in Playground</a></li><li><a href="https://github.com/ohtaman/washi/blob/main/examples/node/04-smt-security-rules.ts">Source Code</a></li></ul> |
5
+ | Propositional Logic & SMT | Z3 (`@ohtaman/washi/solvers/z3`) | `m.bool()`, `m.integer()`, `implies()`, `m.constraint()`, `expr`, `m.check()` | <ul><li><a href="https://washi.ohtaman.dev/demo/?preset=smt_security">Open in Playground</a></li><li><a href="https://github.com/ohtaman/washi/blob/main/examples/node/04-smt-security-rules.ts">Source Code</a></li></ul> |
6
6
 
7
7
  Satisfiability Modulo Theories (SMT) combines propositional logic (Boolean SAT) with first-order theories (linear integer arithmetic, real inequalities).
8
8
 
@@ -18,10 +18,10 @@ We model a security rule system with sensor readings and logic rules:
18
18
 
19
19
  ## Implementation in WaSHI
20
20
 
21
- <CodeRunner preset="smt_sat_scheduling">
21
+ <CodeRunner preset="smt_security">
22
22
 
23
23
  ```typescript
24
- import { Model, not, expr } from "@ohtaman/washi";
24
+ import { Model, implies, not, expr } from "@ohtaman/washi";
25
25
  import "@ohtaman/washi/solvers/z3";
26
26
 
27
27
  async function verifySecurityRules() {
@@ -39,7 +39,7 @@ async function verifySecurityRules() {
39
39
  const luxLevel = m.integer("luxLevel", { lb: 0, ub: 1000 });
40
40
 
41
41
  // Rule 1: luxLevel <= 50 implies isNight
42
- m.constraint(luxLevel.le(50).implies(isNight), "dark_implies_night");
42
+ m.constraint(implies(expr`${luxLevel} <= 50`, isNight), "dark_implies_night");
43
43
 
44
44
  // Rule 2: (motionDetected AND isNight AND alarmArmed) => sirenActive
45
45
  m.constraint(
@@ -66,9 +66,21 @@ async function verifySecurityRules() {
66
66
  console.log(`Lights On : ${lightsOn.value}`);
67
67
  console.log(`Door Locked : ${frontDoorUnlocked.value ? "No" : "Yes"}`);
68
68
  console.log(`Is Night : ${isNight.value}`);
69
+
70
+ // 4. Formal Safety Verification (Counterexample Search):
71
+ // Prove that the siren CANNOT remain off under these sensor readings
72
+ m.constraint(not(sirenActive), "counterexample_siren_off");
73
+ const proof = await m.check({ solver: "z3" });
74
+ console.log(`Can siren stay off? ${proof.status} (Proven safe when Unsat)`);
75
+ console.log(`Active proof core : ${(proof.conflicts ?? []).join(", ")}`);
69
76
  }
70
77
 
71
78
  verifySecurityRules();
72
79
  ```
73
80
 
74
81
  </CodeRunner>
82
+
83
+ ## Practical Notes
84
+
85
+ - **Proof by Refutation (Counterexample Search)**: A single `Sat` check shows that *a* valid assignment exists. To formally prove that a safety property $P$ (such as `sirenActive`) **must** hold in *every* valid state, assert its negation `not(P)` and verify that `m.check()` returns `"Unsat"`. If it returns `"Sat"`, the solver's variable values give you a concrete counterexample (security loophole).
86
+ - **One-Way Implication (`implies`) vs. Equivalence**: In classical logic, `` implies(expr`${luxLevel} <= 50`, isNight) `` only constrains `isNight` when `luxLevel <= 50`. During daytime (`luxLevel > 50`), `isNight` is unconstrained ("open-world" assumption) and could be assigned either `true` or `false` unless you also add the converse rule `` implies(expr`${luxLevel} >= 51`, not(isNight)) ``.
@@ -2,7 +2,7 @@
2
2
 
3
3
  | Problem Class | Solver | Key APIs | Links |
4
4
  | :--- | :--- | :--- | :--- |
5
- | Linear Programming (LP) | HiGHS (default) | `m.continuous()`, `m.constraint()`, `expr`, `minimize`, `dot` | <ul><li><a href="https://ohtaman.github.io/washi/demo/?preset=transportation">Open in Playground</a></li><li><a href="https://github.com/ohtaman/washi/blob/main/examples/node/09-transportation-problem.ts">Source Code</a></li></ul> |
5
+ | Linear Programming (LP) | HiGHS (default) | `m.continuous()`, `m.constraint()`, `expr` (`sum`, `dot`), `minimize` | <ul><li><a href="https://washi.ohtaman.dev/demo/?preset=transportation">Open in Playground</a></li><li><a href="https://github.com/ohtaman/washi/blob/main/examples/node/09-transportation-problem.ts">Source Code</a></li></ul> |
6
6
 
7
7
  The transportation problem is one of the most fundamental network optimization models: determine how many units to ship from each supply origin (factory) to each destination (store) to minimize total shipping cost while respecting factory supply limits and meeting store demands.
8
8
 
@@ -35,7 +35,7 @@ $$\begin{aligned}
35
35
  <CodeRunner preset="transportation">
36
36
 
37
37
  ```typescript
38
- import { Model, sum, dot, minimize, expr } from "@ohtaman/washi";
38
+ import { Model, minimize, expr } from "@ohtaman/washi";
39
39
 
40
40
  const factories = ["Tokyo", "Nagoya", "Osaka"] as const;
41
41
  const stores = ["North", "East", "West", "South"] as const;
@@ -49,42 +49,69 @@ const shippingCost = {
49
49
  const supplies = { Tokyo: 120, Nagoya: 100, Osaka: 80 };
50
50
  const demands = { North: 70, East: 90, West: 60, South: 60 };
51
51
 
52
- async function runTransportation() {
52
+ async function solveScenario(label: string, cap: typeof supplies) {
53
53
  const m = new Model("TransportationNetwork");
54
54
 
55
55
  // 1. Create 2D continuous variables: ship[factory][store] >= 0
56
56
  const ship = m.continuous([...factories], [...stores], { lb: 0 });
57
57
 
58
58
  // 2. Objective: Minimize total shipping cost across all routes via 2D dot product
59
- m.objective(minimize(dot(shippingCost, ship)));
59
+ m.objective(minimize(expr`dot(${shippingCost}, ${ship})`));
60
60
 
61
- // 3. Supply constraints (row sums): outgoing from f <= supplies[f]
62
- for (const f of factories) {
63
- m.constraint(
64
- expr`${sum(stores, (s) => ship[f][s])} <= ${supplies[f]}`,
65
- `supply_${f}`
66
- );
67
- }
61
+ // 3. Supply constraints (row sums): outgoing from f <= cap[f]
62
+ const supplyConstrs = Object.fromEntries(
63
+ factories.map((f) => [
64
+ f,
65
+ m.constraint(expr`sum(${ship[f]}) <= ${cap[f]}`, `supply_${f}`),
66
+ ])
67
+ );
68
68
 
69
69
  // 4. Demand constraints (column sums): incoming to s >= demands[s]
70
- for (const s of stores) {
71
- m.constraint(
72
- expr`${sum(factories, (f) => ship[f][s])} >= ${demands[s]}`,
73
- `demand_${s}`
74
- );
75
- }
70
+ const demandConstrs = Object.fromEntries(
71
+ stores.map((s) => [
72
+ s,
73
+ m.constraint(
74
+ expr`sum(${factories}, ${(f) => ship[f][s]}) >= ${demands[s]}`,
75
+ `demand_${s}`
76
+ ),
77
+ ])
78
+ );
76
79
 
77
80
  const status = await m.solve();
78
- console.log(`Status: ${status}, Total Shipping Cost: $${m.objectiveValue}`);
81
+ console.log(`[${label}] Status: ${status}, Total Shipping Cost: $${m.objectiveValue}`);
79
82
  for (const f of factories) {
83
+ const dual = supplyConstrs[f].dual ?? 0;
80
84
  const routes = stores
81
85
  .map((s) => `${s}:${Math.round(ship[f][s].value ?? 0)}`)
82
86
  .join(", ");
83
- console.log(` ${f.padEnd(7)} -> ${routes}`);
87
+ console.log(` ${f.padEnd(7)} -> ${routes} (supply dual: $${dual}/u)`);
84
88
  }
89
+ const demandDuals = stores
90
+ .map((s) => `${s}:$${demandConstrs[s].dual ?? 0}`)
91
+ .join(", ");
92
+ console.log(` Marginal Delivered Cost (demand duals) -> ${demandDuals}`);
93
+ }
94
+
95
+ async function runTransportation() {
96
+ // Scenario A: Baseline capacities ($920)
97
+ await solveScenario("Scenario A: Baseline", supplies);
98
+
99
+ // Scenario B: Tokyo capacity reduced (120 -> 80) due to maintenance ($1,000)
100
+ await solveScenario("Scenario B: Tokyo Maintenance", {
101
+ Tokyo: 80,
102
+ Nagoya: 120,
103
+ Osaka: 100,
104
+ });
85
105
  }
86
106
 
87
107
  runTransportation();
88
108
  ```
89
109
 
90
110
  </CodeRunner>
111
+
112
+ ## Practical Notes
113
+
114
+ - **Interpreting Supply & Demand Shadow Prices (`.dual`)**:
115
+ - **`demand_s.dual` ($\ge 0$)**: The marginal delivered cost of supplying 1 additional unit to store $s$.
116
+ - **`supply_f.dual` ($\le 0$)**: How much total network shipping cost would decrease if factory $f$'s capacity were expanded by 1 unit. In Scenario B, when Tokyo is constrained at 80 units, `supply_Tokyo.dual` becomes negative (`-$2/u`), indicating that each extra unit of capacity in Tokyo saves \$2 by avoiding rerouting through Nagoya.
117
+ - **Total Supply vs. Total Demand ($\sum \text{Supply}_f \ge \sum \text{Demand}_s$)**: If store demands exceed aggregate factory capacity, the hard demand constraints become `"Infeasible"`. In production logistics systems, add an elastic "spot-carrier / backorder" variable $u_s \ge 0$ to each store's demand constraint ($\sum_f x_{f,s} + u_s \ge \text{Demand}_s$) with a high penalty cost so the solver allocates scarce inventory optimally rather than failing.
@@ -0,0 +1,157 @@
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">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 } 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
+ const distMatrix = {};
56
+
57
+ for (let i = 0; i < n; i++) {
58
+ x[i] = {};
59
+ distMatrix[i] = {};
60
+ for (let j = 0; j < n; j++) {
61
+ if (i !== j) {
62
+ x[i][j] = master.binary(`x_${i}_${j}`);
63
+ distMatrix[i][j] = dist(i, j);
64
+ }
65
+ }
66
+ }
67
+
68
+ // Objective: Minimize total tour distance
69
+ master.objective(minimize(expr`dot(${distMatrix}, ${x})`));
70
+
71
+ // Each city must have 1 outgoing and 1 incoming edge
72
+ for (let i = 0; i < n; i++) {
73
+ master.constraint(expr`sum(${x[i]}) == 1`, `out_${i}`);
74
+ master.constraint(expr`sum(${n}, ${(j) => x[j][i]}) == 1`, `in_${i}`);
75
+ }
76
+
77
+ console.log("Solving TSP with Benders lazy subtour elimination...");
78
+
79
+ // 2. Solve via Benders Decomposition
80
+ let cutCounter = 0;
81
+ const result = await master.solveBenders({
82
+ subproblem: () => {
83
+ // Find active directed edges in the master solution
84
+ const succ = {};
85
+ for (let i = 0; i < n; i++) {
86
+ for (let j = 0; j < n; j++) {
87
+ if (i !== j && x[i][j].value === 1) {
88
+ succ[i] = j;
89
+ }
90
+ }
91
+ }
92
+
93
+ // Trace connected cycles
94
+ const visited = new Set();
95
+ const cycles = [];
96
+ for (let i = 0; i < n; i++) {
97
+ if (!visited.has(i)) {
98
+ const cycle = [];
99
+ let curr = i;
100
+ while (!visited.has(curr)) {
101
+ visited.add(curr);
102
+ cycle.push(curr);
103
+ curr = succ[curr];
104
+ }
105
+ cycles.push(cycle);
106
+ }
107
+ }
108
+
109
+ // If more than 1 cycle, subtours exist: inject Benders feasibility cuts for all cycles
110
+ if (cycles.length > 1) {
111
+ return cycles.map((cycle) => {
112
+ const S = new Set(cycle);
113
+ cutCounter++;
114
+ console.log(`Subtour detected: [${cycle.map(k => cities[k].name).join(" -> ")}]. Adding Benders cut #${cutCounter}.`);
115
+
116
+ const outCities = Array.from({ length: n }, (_, j) => j).filter((j) => !S.has(j));
117
+ const cutEdges = cycle.flatMap((i) => outCities.map((j) => x[i][j]));
118
+
119
+ return {
120
+ kind: "feasibility",
121
+ constraint: expr`sum(${cutEdges}) >= 1`,
122
+ name: `subtour_cut_${cutCounter}`,
123
+ };
124
+ });
125
+ }
126
+
127
+ return null; // Single connected tour found
128
+ },
129
+ });
130
+
131
+ console.log(`\nStatus: ${result.status}`);
132
+ console.log(`Optimal Tour Distance: ${result.objectiveValue?.toFixed(3)}`);
133
+ console.log(`Total Benders Cuts Added: ${result.cutsAdded}`);
134
+ console.log(`Iterations: ${result.iterations}`);
135
+
136
+ console.log("\nOptimal tour edges:");
137
+ for (let i = 0; i < n; i++) {
138
+ for (let j = 0; j < n; j++) {
139
+ if (i !== j && result.values[`x_${i}_${j}`] === 1) {
140
+ console.log(` ${cities[i].name} -> ${cities[j].name} (distance: ${dist(i, j).toFixed(3)})`);
141
+ }
142
+ }
143
+ }
144
+ }
145
+
146
+ runTSP();
147
+ ```
148
+
149
+ </CodeRunner>
150
+
151
+ ## Notes
152
+
153
+ - **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.
154
+ - **Multi-Cut Separation**: Returning an array of cuts (`cycles.map(...)`) from `subproblem` eliminates all disconnected cycles discovered in the current candidate solution simultaneously, reducing the number of master MIP re-solves.
155
+ - **Two Equivalent DFJ Cut Forms**: Because every city has out-degree $\sum_{j \ne i} x_{ij} = 1$, summing over $i \in S$ gives $\sum_{i \in S, j \in S} x_{ij} + \sum_{i \in S, j \notin S} x_{ij} = |S|$. Thus the cut-set inequality $\sum_{i \in S, j \notin S} x_{ij} \ge 1$ used above and the internal-edge inequality $\sum_{i \in S, j \in S} x_{ij} \le |S| - 1$ used in [`14-traveling-salesperson-benders.ts`](https://github.com/ohtaman/washi/blob/main/examples/node/14-traveling-salesperson-benders.ts) are algebraically identical.
156
+ - **Upfront 2-City Subtour Elimination**: In larger TSP instances, mutual 2-cycles ($i \leftrightarrow j$) are the most frequent initial subtours. Adding the $\binom{n}{2}$ pairwise constraints $x_{ij} + x_{ji} \le 1$ directly to the initial master problem often cuts the number of Benders iterations substantially.
157
+ - **Termination Condition**: Returning `null` from `subproblem` indicates that no violated subtour constraints remain.
@@ -2,9 +2,9 @@
2
2
 
3
3
  | Problem Class | Solver | Key APIs | Links |
4
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> |
5
+ | Infeasibility Diagnosis / Unsat Core | Z3 (`@ohtaman/washi/solvers/z3`) | `m.integer()`, `m.continuous()`, `implies()`, `m.constraint()`, `expr`, `m.check()`, `m.conflicts` | <ul><li><a href="https://washi.ohtaman.dev/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
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.
7
+ When business rules, budgets, and operational policies grow complex, constraints can easily conflict. Because a standard optimization call (`m.solve()`) only reports `"Infeasible"`, WaSHI provides `m.check()` to pinpoint conflicting rules without manual trial-and-error.
8
8
 
9
9
  This example shows how to isolate the conflicting constraint subset using Z3 Unsat Core extraction.
10
10
 
@@ -26,7 +26,7 @@ $$8 \text{ engineers} \times \$8,000 = \$64,000 > \$50,000 \text{ budget!}$$
26
26
  <CodeRunner preset="conflict_diagnosis_unsat">
27
27
 
28
28
  ```typescript
29
- import { Model, expr } from "@ohtaman/washi";
29
+ import { Model, implies, expr } from "@ohtaman/washi";
30
30
  import "@ohtaman/washi/solvers/z3";
31
31
 
32
32
  async function diagnoseConflicts() {
@@ -43,7 +43,7 @@ async function diagnoseConflicts() {
43
43
  m.constraint(rushDelivery, "rush_required");
44
44
 
45
45
  // Rule 3: Rush delivery requires at least 8 engineers
46
- m.constraint(rushDelivery.implies(engineers.ge(8)), "rush_staffing_rule");
46
+ m.constraint(implies(rushDelivery, expr`${engineers} >= 8`), "rush_staffing_rule");
47
47
 
48
48
  // Rule 4: Board budget cap of $50,000 (Conflict!)
49
49
  m.constraint(expr`${budget} <= 50000`, "strict_board_budget_cap");
@@ -55,10 +55,20 @@ async function diagnoseConflicts() {
55
55
  const res = await m.check({ solver: "z3" });
56
56
 
57
57
  console.log(`Status: ${res.status}`);
58
- console.log("Conflicting Constraints (conflicts):", m.conflicts);
58
+ console.log("Z3 Unsat Core (scale-invariant):", m.conflicts);
59
+
60
+ // Built-in HiGHS can also diagnose conflicts via single-solve L1 elastic slack relaxation:
61
+ const highsDiag = await m.check({ solver: "highs" });
62
+ console.log("HiGHS Elastic Repair Set:", highsDiag.conflicts);
59
63
  }
60
64
 
61
65
  diagnoseConflicts();
62
66
  ```
63
67
 
64
68
  </CodeRunner>
69
+
70
+ ## Practical Notes
71
+
72
+ - **Z3 `unsatCore` vs. HiGHS Elastic Slack `conflicts`**:
73
+ - **Z3 (`m.check({ solver: "z3" })`)** performs logical assumption tracking and returns all four rules that jointly form the contradiction (`budget_limit`, `rush_required`, `rush_staffing_rule`, `strict_board_budget_cap`), while excluding the innocent `minimum_one_employee` rule.
74
+ - **HiGHS (`m.check({ solver: "highs" })`)** solves a single $L_1$ elastic slack minimization problem ($\min \sum s_i$). Because relaxing `rush_staffing_rule` (or `rush_required`) requires a slack of only `1`–`2` units whereas relaxing `strict_board_budget_cap` requires a slack of `14,000` dollars, unweighted $L_1$ minimization naturally highlights the smallest-magnitude repair (`rush_required`) needed to restore feasibility.
@@ -0,0 +1,208 @@
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`, `m.binary()`, `m.constraint()`, `expr` (`sum`) | <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">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 } 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(expr`sum(${initialRoutes})`));
145
+
146
+ // Each customer must be visited by exactly 1 vehicle
147
+ const visitConstrs = customers.map((c, i) =>
148
+ master.constraint(expr`${initialRoutes[i]} == 1`, `visit_${c.id}`)
149
+ );
150
+
151
+ console.log(`Initial state: 50 naive single-customer routes (50 vehicles).`);
152
+ console.log("Solving via Column Generation with dual-directed heuristic pricing...");
153
+
154
+ // 2. Solve via Column Generation
155
+ const result = await master.solveColumnGeneration({
156
+ finalizeAsMIP: true,
157
+ pricing: async () => {
158
+ // 1. Sort customers by shadow price (dual profit) descending
159
+ const candidates = customers
160
+ .map((c, i) => ({ id: c.id, demand: c.demand, dual: visitConstrs[i].dual ?? 0 }))
161
+ .filter((c) => c.dual > 0)
162
+ .sort((a, b) => b.dual - a.dual);
163
+
164
+ // 2. Solve pricing subproblem: find route with negative reduced cost (dualSum > 1)
165
+ const bestRoute = findImprovingRoute(candidates, routeConfig);
166
+ if (!bestRoute) return null; // No improving column found
167
+
168
+ // 3. Propose new column to Restricted Master Problem
169
+ const { travelTime, totalTime } = calcRouteTime(bestRoute, dist, serviceTimes);
170
+ const coefficients = {};
171
+ for (const id of bestRoute.slice(1, -1)) {
172
+ coefficients[`visit_${id}`] = 1;
173
+ }
174
+
175
+ return {
176
+ cost: 1,
177
+ type: "Binary",
178
+ coefficients,
179
+ meta: { tour: bestRoute, travelTime, totalTime },
180
+ };
181
+ },
182
+ });
183
+
184
+ console.log(`\nStatus: ${result.status}`);
185
+ console.log(`Fleet Size: ${Math.round(result.objectiveValue ?? 0)} vehicles (heuristic column generation)`);
186
+ console.log(`Total Columns Generated: ${result.columnsAdded}`);
187
+
188
+ console.log("\nSample routes selected in fleet:");
189
+ const activeRoutes = result.generatedColumns.filter((c) => c.variable.value === 1);
190
+ for (const r of activeRoutes.slice(0, 5)) {
191
+ const meta = r.proposal.meta;
192
+ console.log(` ${meta?.tour.join(" -> ")} (Transit: ${meta?.travelTime}m, Total: ${meta?.totalTime}m)`);
193
+ }
194
+ }
195
+
196
+ runCVRP();
197
+ ```
198
+
199
+ </CodeRunner>
200
+
201
+ ## Notes
202
+
203
+ - **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.
204
+ - **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.
205
+ - **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.
206
+ - **Set Partitioning (`== 1`) vs. Set Covering (`>= 1`)**: This example uses `== 1` so that the final MIP never covers a customer twice. In large-scale column generation, formulating coverage as `>= 1` restricts the dual prices to $\pi_i \ge 0$, avoiding negative dual oscillations; if the final MIP selects overlapping routes, duplicate visits can be removed by post-processing whenever distances satisfy the triangle inequality.
207
+ - **Heuristic Pricing vs. Exact ESPPRC**: Because `findImprovingRoute` uses a fast greedy heuristic (cheapest insertion + 2-opt) instead of an exact Elementary Shortest Path Problem with Resource Constraints (ESPPRC) solver, returning `null` means no further improving columns were found by the heuristic rather than proving global LP optimality.
208
+ - **Price-and-Branch (`finalizeAsMIP: true`)**: Solving the Restricted Master Problem as a MIP over the columns generated at the root node is known as the *Restricted Master Heuristic* (Price-and-Branch). It produces near-optimal integer fleets quickly without requiring full Branch-and-Price tree search.