@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.
- package/README.ja.md +103 -421
- package/README.md +103 -421
- package/README.zh-CN.md +103 -421
- package/THIRD_PARTY_LICENSES.md +5 -3
- package/dist/browser/solvers/glpk.js +2 -2
- package/dist/browser/solvers/glpk.min.js +1 -1
- package/dist/browser/washi.js +777 -261
- package/dist/browser/washi.min.js +3 -3
- package/dist/lib/{registry-C20sxTwm.js → registry-D7TZ3Wh_.js} +88 -18
- package/dist/lib/solvers/glpk.js +3 -3
- package/dist/lib/solvers/z3.js +1 -1
- package/dist/lib/washi.js +864 -421
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/modeler/expression.d.ts +47 -26
- package/dist/types/modeler/expression.d.ts.map +1 -1
- package/dist/types/modeler/index.d.ts +2 -2
- package/dist/types/modeler/index.d.ts.map +1 -1
- package/dist/types/modeler/model.d.ts +12 -6
- package/dist/types/modeler/model.d.ts.map +1 -1
- package/dist/types/modeler/parser.d.ts +25 -5
- package/dist/types/modeler/parser.d.ts.map +1 -1
- package/dist/types/modeler/solvers/glpk/solver.d.ts.map +1 -1
- 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/types.d.ts +2 -0
- package/dist/types/modeler/solvers/types.d.ts.map +1 -1
- package/docs/examples/diet-sensitivity.md +15 -14
- package/docs/examples/ecommerce-coupons.md +15 -22
- package/docs/examples/index.md +14 -12
- package/docs/examples/knapsack.md +8 -7
- package/docs/examples/portfolio-qp.md +11 -4
- package/docs/examples/production.md +4 -4
- package/docs/examples/shift-scheduling.md +20 -7
- package/docs/examples/smt-security.md +16 -4
- package/docs/examples/transportation.md +46 -19
- package/docs/examples/traveling-salesperson.md +28 -44
- package/docs/examples/unsat-core.md +15 -5
- package/docs/examples/vehicle-routing.md +10 -8
- package/docs/getting-started/ai-agents.md +25 -14
- package/docs/getting-started/cross-origin.md +56 -28
- package/docs/getting-started/index.md +40 -69
- package/docs/getting-started/runtimes.md +66 -61
- package/docs/guide/constraints.md +46 -40
- package/docs/guide/decomposition.md +127 -259
- package/docs/guide/expressions.md +90 -39
- package/docs/guide/formulation-tips.md +212 -0
- package/docs/guide/objectives.md +7 -4
- package/docs/guide/unsat-core.md +18 -6
- package/docs/guide/variables.md +33 -38
- package/docs/ja/getting-started/ai-agents.md +24 -14
- package/docs/solvers/highs.md +27 -6
- package/docs/solvers/index.md +1 -1
- package/docs/solvers/z3.md +3 -0
- package/docs/zh/getting-started/ai-agents.md +24 -14
- package/llms-full.txt +929 -687
- package/llms.txt +21 -19
- package/package.json +3 -3
- package/skills/washi/SKILL.md +14 -10
|
@@ -1,162 +1,163 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Decomposition Methods (`solveColumnGeneration` & `solveBenders`)
|
|
2
2
|
|
|
3
|
-
Mixed-Integer Programming (MIP)
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
console.log(
|
|
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 (`
|
|
36
|
-
|
|
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
|
|
38
|
+
// Solves the continuous LP relaxation and populates m.solution.duals
|
|
40
39
|
const status = await m.relax();
|
|
41
|
-
console.log("
|
|
40
|
+
console.log("LP relaxation bound:", m.objectiveValue);
|
|
42
41
|
```
|
|
43
42
|
|
|
44
|
-
### 3. Dynamic Column Injection (`
|
|
45
|
-
|
|
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, //
|
|
51
|
+
cost: 1, // Objective coefficient
|
|
53
52
|
coefficients: {
|
|
54
|
-
|
|
55
|
-
demand_itemB: 1,
|
|
53
|
+
capacity: 2,
|
|
56
54
|
},
|
|
57
55
|
});
|
|
58
56
|
```
|
|
59
57
|
|
|
60
|
-
### 4. Dynamic Constraint
|
|
61
|
-
|
|
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
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
74
|
-
1.
|
|
75
|
-
2.
|
|
76
|
-
3.
|
|
77
|
-
4. Repeats until no
|
|
78
|
-
5.
|
|
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
|
-
###
|
|
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.
|
|
87
|
+
// 1. Restricted Master Problem (RMP) with simple single-item initial patterns
|
|
95
88
|
const master = new Model("CuttingStockMaster");
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
const
|
|
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.
|
|
98
|
+
// 2. Run Column Generation
|
|
108
99
|
const result = await master.solveColumnGeneration({
|
|
109
100
|
finalizeAsMIP: true,
|
|
110
101
|
pricing: async () => {
|
|
111
|
-
//
|
|
102
|
+
// Knapsack pricing subproblem: find a pattern maximizing total dual value
|
|
112
103
|
const sub = new Model("KnapsackPricing");
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
const
|
|
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} * ${
|
|
122
|
-
sub.constraint(expr`45 * ${
|
|
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(
|
|
132
|
-
demand_B: Math.round(
|
|
133
|
-
demand_C: Math.round(
|
|
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
|
|
140
|
-
console.log("Iterations
|
|
141
|
-
console.log("
|
|
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
|
-
###
|
|
133
|
+
### Pricing with a Custom Algorithm (`getDual`)
|
|
145
134
|
|
|
146
|
-
Because `pricing` is a standard callback
|
|
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
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
171
|
+
### Automatic Cut Generation (`sub.link()`)
|
|
173
172
|
|
|
174
|
-
Return
|
|
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:
|
|
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
|
-
//
|
|
183
|
+
// Fixed opening costs (theta for operational subproblem cost is managed automatically)
|
|
185
184
|
master.objective(minimize(expr`100 * ${y1} + 120 * ${y2}`));
|
|
186
|
-
|
|
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
|
|
187
|
+
// 2. Solve with automatic Benders cut generation
|
|
190
188
|
const result = await master.solveBenders({
|
|
191
189
|
gapTolerance: 1e-4,
|
|
192
190
|
subproblem: () => {
|
|
193
|
-
|
|
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
|
|
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;
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
238
|
-
const
|
|
239
|
-
const
|
|
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
|
-
|
|
247
|
-
|
|
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}
|
|
251
|
-
subproblemObjective:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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.
|