@ohtaman/washi 0.6.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 (59) hide show
  1. package/README.ja.md +103 -421
  2. package/README.md +103 -421
  3. package/README.zh-CN.md +103 -421
  4. package/THIRD_PARTY_LICENSES.md +5 -3
  5. package/dist/browser/solvers/glpk.js +2 -2
  6. package/dist/browser/solvers/glpk.min.js +1 -1
  7. package/dist/browser/washi.js +777 -261
  8. package/dist/browser/washi.min.js +3 -3
  9. package/dist/lib/{registry-C20sxTwm.js → registry-D7TZ3Wh_.js} +88 -18
  10. package/dist/lib/solvers/glpk.js +3 -3
  11. package/dist/lib/solvers/z3.js +1 -1
  12. package/dist/lib/washi.js +864 -421
  13. package/dist/types/index.d.ts +1 -1
  14. package/dist/types/index.d.ts.map +1 -1
  15. package/dist/types/modeler/expression.d.ts +47 -26
  16. package/dist/types/modeler/expression.d.ts.map +1 -1
  17. package/dist/types/modeler/index.d.ts +2 -2
  18. package/dist/types/modeler/index.d.ts.map +1 -1
  19. package/dist/types/modeler/model.d.ts +12 -6
  20. package/dist/types/modeler/model.d.ts.map +1 -1
  21. package/dist/types/modeler/parser.d.ts +25 -5
  22. package/dist/types/modeler/parser.d.ts.map +1 -1
  23. package/dist/types/modeler/solvers/glpk/solver.d.ts.map +1 -1
  24. package/dist/types/modeler/solvers/highs/solver.d.ts.map +1 -1
  25. package/dist/types/modeler/solvers/highs/worker-client.d.ts.map +1 -1
  26. package/dist/types/modeler/solvers/types.d.ts +2 -0
  27. package/dist/types/modeler/solvers/types.d.ts.map +1 -1
  28. package/docs/examples/diet-sensitivity.md +15 -14
  29. package/docs/examples/ecommerce-coupons.md +15 -22
  30. package/docs/examples/index.md +14 -12
  31. package/docs/examples/knapsack.md +8 -7
  32. package/docs/examples/portfolio-qp.md +11 -4
  33. package/docs/examples/production.md +4 -4
  34. package/docs/examples/shift-scheduling.md +20 -7
  35. package/docs/examples/smt-security.md +16 -4
  36. package/docs/examples/transportation.md +46 -19
  37. package/docs/examples/traveling-salesperson.md +28 -44
  38. package/docs/examples/unsat-core.md +15 -5
  39. package/docs/examples/vehicle-routing.md +10 -8
  40. package/docs/getting-started/ai-agents.md +25 -14
  41. package/docs/getting-started/cross-origin.md +56 -28
  42. package/docs/getting-started/index.md +40 -69
  43. package/docs/getting-started/runtimes.md +66 -61
  44. package/docs/guide/constraints.md +46 -40
  45. package/docs/guide/decomposition.md +127 -259
  46. package/docs/guide/expressions.md +90 -39
  47. package/docs/guide/formulation-tips.md +212 -0
  48. package/docs/guide/objectives.md +7 -4
  49. package/docs/guide/unsat-core.md +18 -6
  50. package/docs/guide/variables.md +33 -38
  51. package/docs/ja/getting-started/ai-agents.md +24 -14
  52. package/docs/solvers/highs.md +27 -6
  53. package/docs/solvers/index.md +1 -1
  54. package/docs/solvers/z3.md +3 -0
  55. package/docs/zh/getting-started/ai-agents.md +24 -14
  56. package/llms-full.txt +929 -687
  57. package/llms.txt +21 -19
  58. package/package.json +3 -3
  59. package/skills/washi/SKILL.md +14 -10
@@ -1,162 +1,163 @@
1
- # Advanced Decomposition Methods & Best Practices
1
+ # Decomposition Methods (`solveColumnGeneration` & `solveBenders`)
2
2
 
3
- Mixed-Integer Programming (MIP) formulations can become slow when symmetry or coupling constraints cause branch-and-bound trees to grow large.
4
- WaSHI provides decomposition orchestrators (`master.solveColumnGeneration` and `master.solveBenders`) along with lower-level modeling primitives for dual inspection and dynamic model modification.
3
+ When monolithic Mixed-Integer Programming (MIP) models have exponentially many candidate patterns or large multi-stage subproblems, WaSHI provides two high-level decomposition orchestrators—`master.solveColumnGeneration()` and `master.solveBenders()`—along with low-level primitives for dual inspection and dynamic model updates.
5
4
 
6
- ---
7
-
8
- ## Why Decomposition? (When Monolithic MIP Stalls)
9
-
10
- There are two common problem structures where monolithic MIP formulations struggle:
11
-
12
- | Problem Structure | Monolithic MIP Bottleneck | Technique | WaSHI API |
13
- | :--- | :--- | :--- | :--- |
14
- | **Symmetry & Loose Continuous Bounds**<br>*(Cutting Stock, Vehicle Routing/VRP, Crew Rostering)* | Symmetric permutations and a weak continuous relaxation enlarge the branch-and-bound tree. | **Column Generation**<br>*(Dantzig-Wolfe Decomposition)* | [`master.solveColumnGeneration()`](#column-generation-solvecolumngeneration) |
15
- | **Complicating Decisions & Multi-Stage Structure**<br>*(Traveling Salesperson/TSP, Facility Location with Routing, Two-Stage Stochastic Planning)* | Coupling master decisions with large scenario or operational subproblems increases the matrix size. | **Benders Decomposition** | [`master.solveBenders()`](#benders-decomposition-solvebenders) |
5
+ | Decomposition Method | WaSHI Orchestrator | Typical Use Case |
6
+ | :--- | :--- | :--- |
7
+ | **Column Generation** *(Dantzig-Wolfe)* | [`master.solveColumnGeneration()`](#column-generation-solvecolumngeneration) | Models with exponentially many patterns, routes, or schedules (e.g., Cutting Stock, Vehicle Routing, Crew Rostering). |
8
+ | **Benders Decomposition** *(and Lazy Cuts)* | [`master.solveBenders()`](#benders-decomposition-solvebenders) | Two-stage or complicating-variable problems (e.g., Facility Location + Dispatch, Stochastic Planning, TSP Subtour Elimination). |
16
9
 
17
10
  ---
18
11
 
19
12
  ## Foundational Primitives
20
13
 
21
- WaSHI provides fine-grained modeling primitives to inspect dual information and dynamically modify models:
14
+ WaSHI exposes four lower-level primitives used internally by the decomposition orchestrators and available directly for custom algorithms:
22
15
 
23
- ### 1. Constraint Duals (`dual`)
24
- Access the dual values (marginal impact on the objective per unit change in RHS) after an LP solve:
16
+ ### 1. Constraint Duals (`c.dual`)
17
+ After solving a continuous linear program (LP), read the shadow price (marginal impact on the objective per unit change in RHS) of each constraint:
25
18
 
26
19
  ```typescript
27
- m.constraint(expr`${x} + ${y} <= 10`, "capacity");
20
+ import { Model, expr, maximize } from "@ohtaman/washi";
21
+
22
+ const m = new Model();
23
+ const x = m.continuous("x", { lb: 0 });
24
+ const y = m.continuous("y", { lb: 0 });
25
+
26
+ m.objective(maximize(expr`3 * ${x} + 2 * ${y}`));
27
+ const c = m.constraint(expr`${x} + ${y} <= 10`, "capacity");
28
28
  await m.solve();
29
29
 
30
- const c = m.getConstraintByName("capacity")!;
31
- console.log(c.dual); // Dual value / shadow price
32
- console.log(m.solution?.duals.capacity);
30
+ console.log(c.dual); // Dual value / shadow price
31
+ console.log(m.solution?.duals.capacity); // Lookup by constraint name
33
32
  ```
34
33
 
35
- ### 2. Continuous LP Relaxation (`model.relax()`)
36
- Decomposition algorithms require solving the continuous LP relaxation to extract dual values. `model.relax()` temporarily relaxes all `Integer` and `Binary` variables to continuous variables within their bounds:
34
+ ### 2. Continuous LP Relaxation (`m.relax()`)
35
+ `m.relax()` temporarily relaxes all `Integer` and `Binary` variables to continuous variables within their `[lb, ub]` bounds and solves the LP relaxation so constraint duals are populated:
37
36
 
38
37
  ```typescript
39
- // Solves LP relaxation and populates shadow prices in m.solution.duals
38
+ // Solves the continuous LP relaxation and populates m.solution.duals
40
39
  const status = await m.relax();
41
- console.log("Theoretical dual bound:", m.objectiveValue);
40
+ console.log("LP relaxation bound:", m.objectiveValue);
42
41
  ```
43
42
 
44
- ### 3. Dynamic Column Injection (`model.addColumn()`)
45
- Inject a new decision variable along with its objective coefficient and constraint contributions in a single call:
43
+ ### 3. Dynamic Column Injection (`m.addColumn()`)
44
+ Create a new decision variable, set its objective coefficient, and attach its non-zero coefficients to existing named constraints in a single call:
46
45
 
47
46
  ```typescript
48
47
  const col = m.addColumn({
49
48
  name: "pattern_1",
50
- type: "Integer", // or "Continuous"
49
+ type: "Integer", // or "Continuous" / "Binary"
51
50
  lb: 0,
52
- cost: 1, // objective coefficient
51
+ cost: 1, // Objective coefficient
53
52
  coefficients: {
54
- demand_itemA: 2,
55
- demand_itemB: 1,
53
+ capacity: 2,
56
54
  },
57
55
  });
58
56
  ```
59
57
 
60
- ### 4. Dynamic Constraint Modification (`addTerm` / `setTerm`)
61
- Update the coefficients of individual variables in an existing constraint:
58
+ ### 4. Dynamic Constraint Term Updates (`c.addTerm()` / `c.setTerm()`)
59
+ Increment or overwrite the coefficient of a variable inside an existing linear constraint:
62
60
 
63
61
  ```typescript
64
- const c = m.getConstraintByName("budget")!;
65
- c.addTerm(newVar, 5); // Increments newVar's coefficient by 5
66
- c.setTerm(newVar, 0); // Setting to 0 cleanly removes the term
62
+ const extra = m.continuous("extra", { lb: 0 });
63
+ const budget = m.constraint(expr`${x} + ${y} <= 20`, "budget");
64
+
65
+ budget.addTerm(extra, 5); // Adds +5 * extra to the LHS
66
+ budget.setTerm(extra, 0); // Setting 0 removes extra from the constraint
67
67
  ```
68
68
 
69
69
  ---
70
70
 
71
71
  ## Column Generation (`solveColumnGeneration`)
72
72
 
73
- Column Generation starts with a **Restricted Master Problem (RMP)** with a small subset of columns. In each iteration:
74
- 1. It solves the RMP's LP relaxation via `master.relax()`.
75
- 2. Evaluates a pricing subproblem using constraint shadow prices.
76
- 3. Finds candidate columns with negative reduced costs ($\bar{c} = \text{cost} - \sum \pi_i a_i < 0$) and dynamically injects them into the master problem.
77
- 4. Repeats until no improving columns exist.
78
- 5. Finally solves the full master model as a Mixed-Integer Program (`finalizeAsMIP`, default: `true`) to obtain optimal integer solutions.
73
+ `master.solveColumnGeneration(options)` starts from a **Restricted Master Problem (RMP)** containing an initial feasible subset of columns and executes the column generation loop:
74
+ 1. Solves the continuous LP relaxation of the master problem via `master.relax()`.
75
+ 2. Invokes your `pricing` callback with the current dual shadow prices (`duals`, `getDual`).
76
+ 3. Evaluates candidate columns (`ColumnProposal` or `ColumnProposal[]`), dynamically injecting those with improving reduced costs ($\bar{c} = \text{cost} - \sum_i \pi_i a_i < 0$ when minimizing).
77
+ 4. Repeats until `pricing` returns `null` / `[]` or no column improves the objective beyond `reducedCostTolerance`.
78
+ 5. Optionally solves the final master model as a MIP (`finalizeAsMIP: true`, the default) to obtain an integer solution over all generated columns.
79
79
 
80
- ### Column Generation with the `pricing` Callback
81
-
82
- Provide a `pricing` callback that receives constraint shadow prices (duals), solves the subproblem (using a WaSHI model or any custom algorithm), and returns new column proposals. WaSHI automatically evaluates reduced costs and injects improving columns:
80
+ ### Pricing with a WaSHI Subproblem Model
83
81
 
84
82
  ```typescript
85
83
  import { Model, expr, minimize, maximize } from "@ohtaman/washi";
86
84
 
87
85
  const rollWidth = 100;
88
- const items = [
89
- { id: "A", width: 45, demand: 10 },
90
- { id: "B", width: 36, demand: 14 },
91
- { id: "C", width: 16, demand: 20 },
92
- ];
93
86
 
94
- // 1. Define Restricted Master Problem (RMP) with naive initial single-cut patterns
87
+ // 1. Restricted Master Problem (RMP) with simple single-item initial patterns
95
88
  const master = new Model("CuttingStockMaster");
96
-
97
- const pA = master.integer("init_A", { lb: 0 }); // 2 of A per roll (45*2 = 90 <= 100)
98
- const pB = master.integer("init_B", { lb: 0 }); // 2 of B per roll (36*2 = 72 <= 100)
99
- const pC = master.integer("init_C", { lb: 0 }); // 6 of C per roll (16*6 = 96 <= 100)
89
+ const pA = master.integer("init_A", { lb: 0 }); // 2 of A (width 45) per roll
90
+ const pB = master.integer("init_B", { lb: 0 }); // 2 of B (width 36) per roll
91
+ const pC = master.integer("init_C", { lb: 0 }); // 6 of C (width 16) per roll
100
92
 
101
93
  master.objective(minimize(expr`${pA} + ${pB} + ${pC}`));
102
- master.constraint(expr`2 * ${pA} >= 10`, "demand_A");
103
- master.constraint(expr`2 * ${pB} >= 14`, "demand_B");
104
- master.constraint(expr`6 * ${pC} >= 20`, "demand_C");
105
- const [demandA, demandB, demandC] = master.constraints;
94
+ const demandA = master.constraint(expr`2 * ${pA} >= 10`, "demand_A");
95
+ const demandB = master.constraint(expr`2 * ${pB} >= 14`, "demand_B");
96
+ const demandC = master.constraint(expr`6 * ${pC} >= 20`, "demand_C");
106
97
 
107
- // 2. Solve with master.solveColumnGeneration()
98
+ // 2. Run Column Generation
108
99
  const result = await master.solveColumnGeneration({
109
100
  finalizeAsMIP: true,
110
101
  pricing: async () => {
111
- // Pricing subproblem: Bounded Knapsack
102
+ // Knapsack pricing subproblem: find a pattern maximizing total dual value
112
103
  const sub = new Model("KnapsackPricing");
113
- const a_A = sub.integer("a_A", { lb: 0, ub: 2 });
114
- const a_B = sub.integer("a_B", { lb: 0, ub: 2 });
115
- const a_C = sub.integer("a_C", { lb: 0, ub: 6 });
104
+ const aA = sub.integer("a_A", { lb: 0, ub: 2 });
105
+ const aB = sub.integer("a_B", { lb: 0, ub: 2 });
106
+ const aC = sub.integer("a_C", { lb: 0, ub: 6 });
116
107
 
117
- // Maximize shadow price collection
118
108
  const dualA = demandA.dual ?? 0;
119
109
  const dualB = demandB.dual ?? 0;
120
110
  const dualC = demandC.dual ?? 0;
121
- sub.objective(maximize(expr`${dualA} * ${a_A} + ${dualB} * ${a_B} + ${dualC} * ${a_C}`));
122
- sub.constraint(expr`45 * ${a_A} + 36 * ${a_B} + 16 * ${a_C} <= ${rollWidth}`);
111
+ sub.objective(maximize(expr`${dualA} * ${aA} + ${dualB} * ${aB} + ${dualC} * ${aC}`));
112
+ sub.constraint(expr`45 * ${aA} + 36 * ${aB} + 16 * ${aC} <= ${rollWidth}`);
123
113
 
124
114
  await sub.solve();
125
115
 
126
- // Return the generated cutting pattern
127
116
  return {
128
117
  cost: 1,
129
118
  type: "Integer",
130
119
  coefficients: {
131
- demand_A: Math.round(a_A.value ?? 0),
132
- demand_B: Math.round(a_B.value ?? 0),
133
- demand_C: Math.round(a_C.value ?? 0),
120
+ demand_A: Math.round(aA.value ?? 0),
121
+ demand_B: Math.round(aB.value ?? 0),
122
+ demand_C: Math.round(aC.value ?? 0),
134
123
  },
135
124
  };
136
125
  },
137
126
  });
138
127
 
139
- console.log("Optimal rolls needed:", result.objectiveValue); // 14
140
- console.log("Iterations executed:", result.iterations);
141
- console.log("Dynamic columns added:", result.columnsAdded);
128
+ console.log("Optimal rolls:", result.objectiveValue); // 14
129
+ console.log("Iterations:", result.iterations);
130
+ console.log("Columns added:", result.columnsAdded);
142
131
  ```
143
132
 
144
- ### Custom Pricing Algorithms (Graph / Dynamic Programming)
133
+ ### Pricing with a Custom Algorithm (`getDual`)
145
134
 
146
- Because `pricing` is a standard callback returning column definitions, you can solve the subproblem using any algorithm (e.g. Dijkstra, Bellman-Ford, or custom heuristics):
135
+ Because `pricing` is a standard callback receiving `PricingContext`, you can also solve the pricing problem directly in TypeScript (e.g., via combinatorial search, dynamic programming, or shortest-path routines) instead of constructing a subproblem `Model`:
147
136
 
148
137
  ```typescript
149
- const result = await master.solveColumnGeneration({
150
- pricing: async ({ getDual }) => {
151
- // Solve subproblem with specialized custom graph algorithm
152
- const route = solveSpecializedShortestPath(getDual);
153
- if (!route) return null;
154
-
155
- return {
156
- cost: route.distance,
157
- type: "Integer",
158
- coefficients: route.visitedCustomers,
159
- };
138
+ const customResult = await master.solveColumnGeneration({
139
+ maxIterations: 100,
140
+ reducedCostTolerance: 1e-6,
141
+ pricing: ({ getDual }) => {
142
+ const dA = getDual("demand_A");
143
+ const dB = getDual("demand_B");
144
+ const dC = getDual("demand_C");
145
+
146
+ // Search feasible patterns (45*aA + 36*aB + 16*aC <= rollWidth) for negative reduced cost (1 - dualSum < -1e-6)
147
+ let bestDualSum = 1 + 1e-6;
148
+ let best: { demand_A: number; demand_B: number; demand_C: number } | null = null;
149
+ for (let aA = 0; 45 * aA <= rollWidth; aA++) {
150
+ for (let aB = 0; 45 * aA + 36 * aB <= rollWidth; aB++) {
151
+ for (let aC = 0; 45 * aA + 36 * aB + 16 * aC <= rollWidth; aC++) {
152
+ const dualSum = dA * aA + dB * aB + dC * aC;
153
+ if (dualSum > bestDualSum) {
154
+ bestDualSum = dualSum;
155
+ best = { demand_A: aA, demand_B: aB, demand_C: aC };
156
+ }
157
+ }
158
+ }
159
+ }
160
+ return best ? { cost: 1, type: "Integer", coefficients: best } : null;
160
161
  },
161
162
  });
162
163
  ```
@@ -165,33 +166,29 @@ const result = await master.solveColumnGeneration({
165
166
 
166
167
  ## Benders Decomposition (`solveBenders`)
167
168
 
168
- Benders Decomposition partitions the problem into:
169
- - **Master Problem**: Contains complicating variables (e.g., 0-1 facility opening decisions $y$) and an auxiliary variable $\theta \ge 0$ representing estimated subproblem costs.
170
- - **Subproblem**: Solves operational decisions given fixed master values $y^*$, feeding back **Optimality Cuts** to the master.
169
+ `master.solveBenders(options)` partitions a problem into a **Master Problem** (holding first-stage or complicating variables) and a **Subproblem** evaluated at each master candidate solution to generate **Optimality Cuts** or **Feasibility Cuts**.
171
170
 
172
- ### Recommended: Automated Cut Generation with `sub.link()`
171
+ ### Automatic Cut Generation (`sub.link()`)
173
172
 
174
- Return the subproblem `Model` from your callback and register linking constraints with `sub.link(...)`. WaSHI automatically manages $\theta$, evaluates master candidate values, extracts duals, and injects Benders optimality cuts:
173
+ Return a continuous LP subproblem `Model` from `subproblem` and declare cross-model constraints via `sub.link(...)`. WaSHI automatically manages the auxiliary cost estimator variable $\theta$, substitutes master candidate values, extracts subproblem duals, and adds Benders cuts until convergence:
175
174
 
176
175
  ```typescript
177
176
  import { Model, expr, minimize } from "@ohtaman/washi";
178
177
 
179
- // 1. Master problem: Facility location decisions
178
+ // 1. Master problem: binary facility opening decisions
180
179
  const master = new Model("FacilityMaster");
181
180
  const y1 = master.binary("open_F1");
182
181
  const y2 = master.binary("open_F2");
183
182
 
184
- // Objective: Fixed facility costs (theta is managed automatically)
183
+ // Fixed opening costs (theta for operational subproblem cost is managed automatically)
185
184
  master.objective(minimize(expr`100 * ${y1} + 120 * ${y2}`));
186
- // Minimal aggregate capacity requirement
187
- master.constraint(expr`50 * ${y1} + 60 * ${y2} >= 75`, "capacity_check");
185
+ master.constraint(expr`50 * ${y1} + 60 * ${y2} >= 75`, "total_capacity_floor");
188
186
 
189
- // 2. Solve automatically with master.solveBenders()
187
+ // 2. Solve with automatic Benders cut generation
190
188
  const result = await master.solveBenders({
191
189
  gapTolerance: 1e-4,
192
190
  subproblem: () => {
193
- // Operational dispatch subproblem
194
- const sub = new Model("TransportSubproblem");
191
+ const sub = new Model("DispatchSubproblem");
195
192
  const x11 = sub.continuous("x_11", { lb: 0 });
196
193
  const x12 = sub.continuous("x_12", { lb: 0 });
197
194
  const x13 = sub.continuous("x_13", { lb: 0 });
@@ -205,195 +202,66 @@ const result = await master.solveBenders({
205
202
  )
206
203
  );
207
204
 
208
- // Customer demand constraints
209
205
  sub.constraint(expr`${x11} + ${x21} >= 20`, "demand_C1");
210
206
  sub.constraint(expr`${x12} + ${x22} >= 25`, "demand_C2");
211
207
  sub.constraint(expr`${x13} + ${x23} >= 30`, "demand_C3");
212
208
 
213
- // Link subproblem flows with master decisions y1, y2 via linking constraints
209
+ // Link subproblem dispatch flows to master binary variables y1, y2
214
210
  sub.link(expr`${x11} + ${x12} + ${x13} <= 50 * ${y1}`);
215
211
  sub.link(expr`${x21} + ${x22} + ${x23} <= 60 * ${y2}`);
216
212
 
217
- return sub; // Returning the model automatically generates cuts and manages theta
213
+ return sub;
218
214
  },
219
215
  });
220
216
 
221
217
  console.log("Optimal total cost:", result.objectiveValue); // 390
222
- console.log("Iterations:", result.iterations);
223
218
  console.log("Cuts added:", result.cutsAdded);
224
- console.log("Facility decisions:", {
225
- F1: result.values.open_F1,
226
- F2: result.values.open_F2,
227
- });
219
+ console.log("Open facilities:", result.values);
228
220
  ```
229
221
 
230
- ### Low-Level Control: Custom `BendersCut` Callback
222
+ > [!NOTE]
223
+ > **`theta` Lower Bound for Negative Subproblem Objectives**: When `sub.link(...)` is used without an explicit `theta` variable, WaSHI injects `__benders_theta` with default `lb: 0` (assuming non-negative subproblem costs). If your subproblem objective can take negative values (e.g., rebate or profit terms), declare `const theta = master.continuous("theta", { lb: -1e6 })`, include `+ ${theta}` in `master.objective(...)`, and pass `{ theta }` in `solveBenders(options)`.
224
+
225
+ ### Manual Cut Generation (`BendersCut` / Lazy Constraints)
231
226
 
232
- You can also construct Benders cuts manually by returning `BendersCut` objects:
227
+ For combinatorial separation (such as lazy subtour elimination) or custom analytical cuts, return one `BendersCut` object, an array `BendersCut[]`, or `null` when converged:
233
228
 
234
229
  ```typescript
230
+ import { Model, expr, minimize } from "@ohtaman/washi";
231
+
232
+ const master = new Model("ManualBenders");
233
+ const y = master.binary("y");
234
+ const theta = master.continuous("theta", { lb: 0 });
235
+ master.objective(minimize(expr`40 * ${y} + ${theta}`));
236
+
235
237
  const result = await master.solveBenders({
238
+ theta,
236
239
  gapTolerance: 1e-4,
237
- subproblem: async () => {
238
- const open1 = y1.value ?? 0;
239
- const open2 = y2.value ?? 0;
240
- const estTheta = theta.value ?? 0;
241
-
242
- const sub = new Model("TransportSubproblem");
243
- // ...
244
- await sub.solve();
240
+ subproblem: ({ getMasterValue }) => {
241
+ const yVal = getMasterValue(y);
242
+ const estTheta = getMasterValue(theta);
245
243
 
246
- const actualCost = sub.objectiveValue!;
247
- if (estTheta < actualCost - 1e-4) {
244
+ // Operational cost is 100 when y = 0, or 30 when y = 1
245
+ const actualSubCost = 100 - 70 * yVal;
246
+ if (estTheta < actualSubCost - 1e-4) {
248
247
  return {
249
- kind: "optimality",
250
- constraint: expr`${theta} - 50 * ${sub.solution!.duals.cap1} * ${y1} - 60 * ${sub.solution!.duals.cap2} * ${y2} >= ${rhs}`,
251
- subproblemObjective: 100 * open1 + 120 * open2 + actualCost,
248
+ kind: "optimality", // or "feasibility" for lazy combinatorial cuts
249
+ constraint: expr`${theta} >= 100 - 70 * ${y}`,
250
+ subproblemObjective: 40 * yVal + actualSubCost,
252
251
  };
253
252
  }
254
253
  return null; // Converged
255
254
  },
256
255
  });
257
- ```
258
256
 
259
- ---
260
-
261
- ## Application Examples
262
-
263
- Two common applications of decomposition methods:
264
-
265
- ### 1. Vehicle Routing Problem (CVRP) $\times$ Column Generation
266
- *(Full executable source: [`examples/node/13-vehicle-routing-column-generation.ts`](https://github.com/ohtaman/washi/blob/main/examples/node/13-vehicle-routing-column-generation.ts))*
267
-
268
- * **Problem Structure**: Enumerating every feasible customer tour upfront produces a large number of variables.
269
- * **Column Generation Formulation**:
270
- * **Master Problem**: Set covering model that selects the minimum number of vehicle routes such that each customer is served.
271
- * **Pricing Subproblem**: Knapsack model that dynamically generates a capacity-feasible route (column) with negative reduced cost based on customer dual shadow prices.
272
-
273
- ```typescript
274
- import { Model, expr, minimize, LinExpr, type Var } from "@ohtaman/washi";
275
-
276
- const vehicleCapacity = 8;
277
- const customers = [
278
- { id: "C1", demand: 4 },
279
- { id: "C2", demand: 3 },
280
- { id: "C3", demand: 5 },
281
- { id: "C4", demand: 2 },
282
- { id: "C5", demand: 4 },
283
- ];
284
-
285
- // 1. Master Problem: Customer coverage
286
- const master = new Model("CVRP_Master");
287
- const initialRoutes: Var[] = [];
288
- for (const cust of customers) {
289
- initialRoutes.push(master.integer(`init_${cust.id}`, { lb: 0, ub: 1 }));
290
- }
291
- master.objective(minimize(initialRoutes.reduce((acc, r) => acc.add(r), new LinExpr())));
292
-
293
- for (let i = 0; i < customers.length; i++) {
294
- master.constraint(expr`${initialRoutes[i]} >= 1`, `visit_${customers[i].id}`);
295
- }
296
- const visitConstrs = master.constraints;
297
-
298
- // 2. Solve via Column Generation
299
- const result = await master.solveColumnGeneration({
300
- finalizeAsMIP: true,
301
- pricing: async () => {
302
- const sub = new Model("RoutePricing");
303
- const a: Record<string, Var> = {};
304
- const obj = new LinExpr();
305
-
306
- for (let i = 0; i < customers.length; i++) {
307
- const cust = customers[i];
308
- a[cust.id] = sub.binary(`v_${cust.id}`);
309
- obj.addTerm(a[cust.id], visitConstrs[i].dual ?? 0);
310
- }
311
-
312
- sub.objective(maximize(obj));
313
-
314
- // Capacity limit: sum(demand_i * a_i) <= vehicleCapacity
315
- const load = customers.reduce((acc, c) => acc.add(a[c.id].mul(c.demand)), new LinExpr());
316
- sub.constraint(expr`${load} <= ${vehicleCapacity}`);
317
-
318
- await sub.solve();
319
-
320
- const coefficients: Record<string, number> = {};
321
- for (const cust of customers) {
322
- coefficients[`visit_${cust.id}`] = Math.round(a[cust.id].value ?? 0);
323
- }
324
-
325
- return {
326
- cost: 1,
327
- type: "Integer",
328
- coefficients,
329
- };
330
- },
331
- });
332
-
333
- console.log("Optimal fleet size:", result.objectiveValue); // 3 vehicles
257
+ console.log(result.objectiveValue); // 70 (y = 1, theta = 30)
334
258
  ```
335
259
 
336
- ### 2. Traveling Salesperson Problem (TSP) $\times$ Benders Decomposition (Lazy Subtour Elimination)
337
- *(Full executable source: [`examples/node/14-traveling-salesperson-benders.ts`](https://github.com/ohtaman/washi/blob/main/examples/node/14-traveling-salesperson-benders.ts))*
338
-
339
- * **Problem Structure**: Preventing isolated sub-loops (subtours) requires Subtour Elimination Constraints (SECs), which grow as $O(2^n)$ constraints for $n$ cities.
340
- * **Benders Decomposition Formulation**:
341
- * **Master Problem**: Solves the relaxed assignment problem (each city has 1 incoming and 1 outgoing edge).
342
- * **Subproblem**: Evaluates the candidate tour graph for disconnected components. If isolated subtours exist, it lazily injects a Benders feasibility cut ($\sum_{i,j \in S} x_{ij} \le |S| - 1$) cutting off that cycle.
343
-
344
- ```typescript
345
- import { Model, expr, minimize, LinExpr, type Var } from "@ohtaman/washi";
346
-
347
- const n = cities.length;
348
- const master = new Model("TSP_Master");
349
- const x: Record<string, Var> = {};
350
- const objExpr = new LinExpr();
351
-
352
- for (let i = 0; i < n; i++) {
353
- for (let j = 0; j < n; j++) {
354
- if (i !== j) {
355
- const v = master.binary(`x_${i}_${j}`);
356
- x[`${i}_${j}`] = v;
357
- objExpr.addTerm(v, dist(i, j));
358
- }
359
- }
360
- }
361
- master.objective(minimize(objExpr));
362
-
363
- // Degree constraints
364
- for (let i = 0; i < n; i++) {
365
- const outE = new LinExpr();
366
- const inE = new LinExpr();
367
- for (let j = 0; j < n; j++) {
368
- if (i !== j) {
369
- outE.addTerm(x[`${i}_${j}`], 1);
370
- inE.addTerm(x[`${j}_${i}`], 1);
371
- }
372
- }
373
- master.constraint(expr`${outE} == 1`, `out_${i}`);
374
- master.constraint(expr`${inE} == 1`, `in_${i}`);
375
- }
260
+ ---
376
261
 
377
- // Solve with Benders lazy subtour elimination
378
- const result = await master.solveBenders({
379
- subproblem: () => {
380
- const cycles = findCycles(n, (i, j) => (x[`${i}_${j}`].value ?? 0) > 0.5);
381
- if (cycles.length === 1 && cycles[0].length === n) {
382
- return null; // Single tour covers all cities
383
- }
262
+ ## Related Examples & Formulation Tips
384
263
 
385
- // Subtour detected: inject Benders feasibility cut for smallest cycle
386
- const S = cycles.reduce((min, c) => (c.length < min.length ? c : min), cycles[0]);
387
- const secExpr = new LinExpr();
388
- for (const i of S) {
389
- for (const j of S) {
390
- if (i !== j) secExpr.addTerm(x[`${i}_${j}`], 1);
391
- }
392
- }
393
- return {
394
- kind: "feasibility",
395
- constraint: expr`${secExpr} <= ${S.length - 1}`,
396
- };
397
- },
398
- });
399
- ```
264
+ - **Full Decomposition Walkthroughs**:
265
+ - [Vehicle Routing Problem (`solveColumnGeneration`)](../examples/vehicle-routing.md)
266
+ - [Traveling Salesperson Problem (`solveBenders` lazy subtour elimination)](../examples/traveling-salesperson.md)
267
+ - **Formulation Tips**: See [Formulation Patterns & Optimization Tips](./formulation-tips.md#_5-decomposition-formulation-tips) for Set Covering (`>= 1`) vs. Set Partitioning (`== 1`), Price-and-Branch (`finalizeAsMIP`), and multi-cut separation.