@ohtaman/washi 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +149 -121
- package/README.md +149 -121
- package/README.zh-CN.md +150 -122
- package/THIRD_PARTY_LICENSES.md +3 -4
- package/dist/browser/_washi-node-only-BvRk9kiK.js +0 -0
- package/dist/browser/browser-runtime-TLCY0hkc.js +1793 -0
- package/dist/browser/highs-worker.js +21 -3
- package/dist/browser/highs.wasm +0 -0
- package/dist/browser/highs.wasm.LICENSE +1 -2
- package/dist/browser/loader-DewlSho8.js +643 -0
- package/dist/browser/solvers/glpk.js +433 -286
- package/dist/browser/solvers/glpk.min.js +1 -1
- package/dist/browser/solvers/z3.js +1008 -981
- package/dist/browser/solvers/z3.min.js +2 -2
- package/dist/browser/washi.js +2778 -673
- package/dist/browser/washi.min.js +3 -4
- package/dist/lib/_washi-node-only-BvRk9kiK.js +0 -0
- package/dist/lib/highs-browser-runtime.js +2120 -4124
- package/dist/lib/highs-node-runtime.js +24 -1
- package/dist/lib/highs-runtime.js +2 -0
- package/dist/lib/highs-worker.js +21 -3
- package/dist/lib/highs.wasm +0 -0
- package/dist/lib/highs.wasm.LICENSE +1 -2
- package/dist/lib/loader-BZUQNvh4.js +649 -0
- package/dist/lib/registry-C20sxTwm.js +964 -0
- package/dist/lib/solvers/glpk.js +201 -90
- package/dist/lib/solvers/z3.js +399 -259
- package/dist/lib/washi.js +2363 -1056
- package/dist/types/decomposition/benders.d.ts +32 -0
- package/dist/types/decomposition/benders.d.ts.map +1 -0
- package/dist/types/decomposition/column-generation.d.ts +34 -0
- package/dist/types/decomposition/column-generation.d.ts.map +1 -0
- package/dist/types/decomposition/index.d.ts +4 -0
- package/dist/types/decomposition/index.d.ts.map +1 -0
- package/dist/types/decomposition/types.d.ts +230 -0
- package/dist/types/decomposition/types.d.ts.map +1 -0
- package/dist/types/index.d.ts +5 -4
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/modeler/big-m.d.ts +75 -0
- package/dist/types/modeler/big-m.d.ts.map +1 -0
- package/dist/types/modeler/boolean-expr.d.ts +103 -14
- package/dist/types/modeler/boolean-expr.d.ts.map +1 -1
- package/dist/types/modeler/constraint.d.ts +47 -9
- package/dist/types/modeler/constraint.d.ts.map +1 -1
- package/dist/types/modeler/expression.d.ts +123 -28
- package/dist/types/modeler/expression.d.ts.map +1 -1
- package/dist/types/modeler/index.d.ts +11 -6
- package/dist/types/modeler/index.d.ts.map +1 -1
- package/dist/types/modeler/linearizer.d.ts +15 -0
- package/dist/types/modeler/linearizer.d.ts.map +1 -0
- package/dist/types/modeler/lp-writer.d.ts +30 -3
- package/dist/types/modeler/lp-writer.d.ts.map +1 -1
- package/dist/types/modeler/model.d.ts +623 -131
- package/dist/types/modeler/model.d.ts.map +1 -1
- package/dist/types/modeler/objective.d.ts +59 -0
- package/dist/types/modeler/objective.d.ts.map +1 -0
- package/dist/types/modeler/parser.d.ts +34 -10
- package/dist/types/modeler/parser.d.ts.map +1 -1
- package/dist/types/modeler/quad-expr.d.ts +26 -7
- package/dist/types/modeler/quad-expr.d.ts.map +1 -1
- package/dist/types/modeler/solution-validator.d.ts +93 -0
- package/dist/types/modeler/solution-validator.d.ts.map +1 -0
- package/dist/types/modeler/solvers/glpk/solver.d.ts +26 -2
- package/dist/types/modeler/solvers/glpk/solver.d.ts.map +1 -1
- package/dist/types/modeler/solvers/highs/browser-runtime.d.ts +2 -1
- package/dist/types/modeler/solvers/highs/browser-runtime.d.ts.map +1 -1
- package/dist/types/modeler/solvers/highs/driver.d.ts +232 -0
- package/dist/types/modeler/solvers/highs/driver.d.ts.map +1 -0
- package/dist/types/modeler/solvers/highs/index.d.ts +1 -0
- package/dist/types/modeler/solvers/highs/index.d.ts.map +1 -1
- package/dist/types/modeler/solvers/highs/loader.d.ts +38 -13
- package/dist/types/modeler/solvers/highs/loader.d.ts.map +1 -1
- package/dist/types/modeler/solvers/highs/node-runtime.d.ts +2 -1
- package/dist/types/modeler/solvers/highs/node-runtime.d.ts.map +1 -1
- package/dist/types/modeler/solvers/highs/solver.d.ts +12 -5
- package/dist/types/modeler/solvers/highs/solver.d.ts.map +1 -1
- package/dist/types/modeler/solvers/highs/worker-client.d.ts.map +1 -1
- package/dist/types/modeler/solvers/mock.d.ts +14 -5
- package/dist/types/modeler/solvers/mock.d.ts.map +1 -1
- package/dist/types/modeler/solvers/types.d.ts +73 -11
- package/dist/types/modeler/solvers/types.d.ts.map +1 -1
- package/dist/types/modeler/solvers/z3/loader.d.ts +2 -3
- package/dist/types/modeler/solvers/z3/loader.d.ts.map +1 -1
- package/dist/types/modeler/solvers/z3/solver.d.ts +15 -5
- package/dist/types/modeler/solvers/z3/solver.d.ts.map +1 -1
- package/dist/types/modeler/types.d.ts +86 -1
- package/dist/types/modeler/types.d.ts.map +1 -1
- package/dist/types/modeler/validation.d.ts +56 -0
- package/dist/types/modeler/validation.d.ts.map +1 -0
- package/dist/types/modeler/variable.d.ts +48 -14
- package/dist/types/modeler/variable.d.ts.map +1 -1
- package/docs/examples/diet-sensitivity.md +90 -0
- package/docs/examples/ecommerce-coupons.md +106 -0
- package/docs/examples/index.md +57 -0
- package/docs/examples/knapsack.md +60 -0
- package/docs/examples/portfolio-qp.md +83 -0
- package/docs/examples/production.md +51 -0
- package/docs/examples/shift-scheduling.md +175 -0
- package/docs/examples/smt-security.md +74 -0
- package/docs/examples/transportation.md +90 -0
- package/docs/examples/traveling-salesperson.md +173 -0
- package/docs/examples/unsat-core.md +64 -0
- package/docs/examples/vehicle-routing.md +206 -0
- package/docs/getting-started/ai-agents.md +271 -0
- package/docs/getting-started/cross-origin.md +87 -0
- package/docs/getting-started/index.md +116 -0
- package/docs/getting-started/runtimes.md +227 -0
- package/docs/guide/constraints.md +177 -0
- package/docs/guide/decomposition.md +399 -0
- package/docs/guide/expressions.md +204 -0
- package/docs/guide/objectives.md +170 -0
- package/docs/guide/unsat-core.md +154 -0
- package/docs/guide/variables.md +188 -0
- package/docs/ja/getting-started/ai-agents.md +270 -0
- package/docs/solvers/custom-solver.md +525 -0
- package/docs/solvers/glpk.md +66 -0
- package/docs/solvers/highs.md +115 -0
- package/docs/solvers/index.md +42 -0
- package/docs/solvers/z3.md +78 -0
- package/docs/zh/getting-started/ai-agents.md +270 -0
- package/llms-full.txt +4065 -0
- package/llms.txt +201 -0
- package/package.json +32 -16
- package/skills/washi/SKILL.md +197 -0
- package/dist/browser/browser-runtime-D13gHOT5.js +0 -4189
- package/dist/browser/loader-CVt4CloI.js +0 -58
- package/dist/lib/async-mutex-Duo4jwzn.js +0 -195
- package/dist/lib/constraint-BRuB3ooC.js +0 -90
- package/dist/lib/loader-Dti0v9yC.js +0 -50
- package/dist/lib/registry-DksqCSjI.js +0 -32
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
# Advanced Decomposition Methods & Best Practices
|
|
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.
|
|
5
|
+
|
|
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) |
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Foundational Primitives
|
|
20
|
+
|
|
21
|
+
WaSHI provides fine-grained modeling primitives to inspect dual information and dynamically modify models:
|
|
22
|
+
|
|
23
|
+
### 1. Constraint Duals (`dual`)
|
|
24
|
+
Access the dual values (marginal impact on the objective per unit change in RHS) after an LP solve:
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
m.constraint(expr`${x} + ${y} <= 10`, "capacity");
|
|
28
|
+
await m.solve();
|
|
29
|
+
|
|
30
|
+
const c = m.getConstraintByName("capacity")!;
|
|
31
|
+
console.log(c.dual); // Dual value / shadow price
|
|
32
|
+
console.log(m.solution?.duals.capacity);
|
|
33
|
+
```
|
|
34
|
+
|
|
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:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
// Solves LP relaxation and populates shadow prices in m.solution.duals
|
|
40
|
+
const status = await m.relax();
|
|
41
|
+
console.log("Theoretical dual bound:", m.objectiveValue);
|
|
42
|
+
```
|
|
43
|
+
|
|
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:
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
const col = m.addColumn({
|
|
49
|
+
name: "pattern_1",
|
|
50
|
+
type: "Integer", // or "Continuous"
|
|
51
|
+
lb: 0,
|
|
52
|
+
cost: 1, // objective coefficient
|
|
53
|
+
coefficients: {
|
|
54
|
+
demand_itemA: 2,
|
|
55
|
+
demand_itemB: 1,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 4. Dynamic Constraint Modification (`addTerm` / `setTerm`)
|
|
61
|
+
Update the coefficients of individual variables in an existing constraint:
|
|
62
|
+
|
|
63
|
+
```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
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Column Generation (`solveColumnGeneration`)
|
|
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.
|
|
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:
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import { Model, expr, minimize, maximize } from "@ohtaman/washi";
|
|
86
|
+
|
|
87
|
+
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
|
+
|
|
94
|
+
// 1. Define Restricted Master Problem (RMP) with naive initial single-cut patterns
|
|
95
|
+
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)
|
|
100
|
+
|
|
101
|
+
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;
|
|
106
|
+
|
|
107
|
+
// 2. Solve with master.solveColumnGeneration()
|
|
108
|
+
const result = await master.solveColumnGeneration({
|
|
109
|
+
finalizeAsMIP: true,
|
|
110
|
+
pricing: async () => {
|
|
111
|
+
// Pricing subproblem: Bounded Knapsack
|
|
112
|
+
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 });
|
|
116
|
+
|
|
117
|
+
// Maximize shadow price collection
|
|
118
|
+
const dualA = demandA.dual ?? 0;
|
|
119
|
+
const dualB = demandB.dual ?? 0;
|
|
120
|
+
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}`);
|
|
123
|
+
|
|
124
|
+
await sub.solve();
|
|
125
|
+
|
|
126
|
+
// Return the generated cutting pattern
|
|
127
|
+
return {
|
|
128
|
+
cost: 1,
|
|
129
|
+
type: "Integer",
|
|
130
|
+
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),
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
console.log("Optimal rolls needed:", result.objectiveValue); // 14
|
|
140
|
+
console.log("Iterations executed:", result.iterations);
|
|
141
|
+
console.log("Dynamic columns added:", result.columnsAdded);
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Custom Pricing Algorithms (Graph / Dynamic Programming)
|
|
145
|
+
|
|
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):
|
|
147
|
+
|
|
148
|
+
```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
|
+
};
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Benders Decomposition (`solveBenders`)
|
|
167
|
+
|
|
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.
|
|
171
|
+
|
|
172
|
+
### Recommended: Automated Cut Generation with `sub.link()`
|
|
173
|
+
|
|
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:
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
import { Model, expr, minimize } from "@ohtaman/washi";
|
|
178
|
+
|
|
179
|
+
// 1. Master problem: Facility location decisions
|
|
180
|
+
const master = new Model("FacilityMaster");
|
|
181
|
+
const y1 = master.binary("open_F1");
|
|
182
|
+
const y2 = master.binary("open_F2");
|
|
183
|
+
|
|
184
|
+
// Objective: Fixed facility costs (theta is managed automatically)
|
|
185
|
+
master.objective(minimize(expr`100 * ${y1} + 120 * ${y2}`));
|
|
186
|
+
// Minimal aggregate capacity requirement
|
|
187
|
+
master.constraint(expr`50 * ${y1} + 60 * ${y2} >= 75`, "capacity_check");
|
|
188
|
+
|
|
189
|
+
// 2. Solve automatically with master.solveBenders()
|
|
190
|
+
const result = await master.solveBenders({
|
|
191
|
+
gapTolerance: 1e-4,
|
|
192
|
+
subproblem: () => {
|
|
193
|
+
// Operational dispatch subproblem
|
|
194
|
+
const sub = new Model("TransportSubproblem");
|
|
195
|
+
const x11 = sub.continuous("x_11", { lb: 0 });
|
|
196
|
+
const x12 = sub.continuous("x_12", { lb: 0 });
|
|
197
|
+
const x13 = sub.continuous("x_13", { lb: 0 });
|
|
198
|
+
const x21 = sub.continuous("x_21", { lb: 0 });
|
|
199
|
+
const x22 = sub.continuous("x_22", { lb: 0 });
|
|
200
|
+
const x23 = sub.continuous("x_23", { lb: 0 });
|
|
201
|
+
|
|
202
|
+
sub.objective(
|
|
203
|
+
minimize(
|
|
204
|
+
expr`3 * ${x11} + 4 * ${x12} + 2 * ${x13} + 4 * ${x21} + 2 * ${x22} + 3 * ${x23}`
|
|
205
|
+
)
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// Customer demand constraints
|
|
209
|
+
sub.constraint(expr`${x11} + ${x21} >= 20`, "demand_C1");
|
|
210
|
+
sub.constraint(expr`${x12} + ${x22} >= 25`, "demand_C2");
|
|
211
|
+
sub.constraint(expr`${x13} + ${x23} >= 30`, "demand_C3");
|
|
212
|
+
|
|
213
|
+
// Link subproblem flows with master decisions y1, y2 via linking constraints
|
|
214
|
+
sub.link(expr`${x11} + ${x12} + ${x13} <= 50 * ${y1}`);
|
|
215
|
+
sub.link(expr`${x21} + ${x22} + ${x23} <= 60 * ${y2}`);
|
|
216
|
+
|
|
217
|
+
return sub; // Returning the model automatically generates cuts and manages theta
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
console.log("Optimal total cost:", result.objectiveValue); // 390
|
|
222
|
+
console.log("Iterations:", result.iterations);
|
|
223
|
+
console.log("Cuts added:", result.cutsAdded);
|
|
224
|
+
console.log("Facility decisions:", {
|
|
225
|
+
F1: result.values.open_F1,
|
|
226
|
+
F2: result.values.open_F2,
|
|
227
|
+
});
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Low-Level Control: Custom `BendersCut` Callback
|
|
231
|
+
|
|
232
|
+
You can also construct Benders cuts manually by returning `BendersCut` objects:
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
const result = await master.solveBenders({
|
|
236
|
+
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();
|
|
245
|
+
|
|
246
|
+
const actualCost = sub.objectiveValue!;
|
|
247
|
+
if (estTheta < actualCost - 1e-4) {
|
|
248
|
+
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,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
return null; // Converged
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
```
|
|
258
|
+
|
|
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
|
|
334
|
+
```
|
|
335
|
+
|
|
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
|
+
}
|
|
376
|
+
|
|
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
|
+
}
|
|
384
|
+
|
|
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
|
+
```
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# Expressions (`expr`, `sum`, `dot`)
|
|
2
|
+
|
|
3
|
+
Mathematical expressions are linear or quadratic combinations of decision variables and numeric coefficients.
|
|
4
|
+
|
|
5
|
+
In JavaScript and TypeScript—which do not support operator overloading—WaSHI provides the **unified tagged template literal (`expr`)**, **`sum()`**, and matrix/vector **`dot()`** to construct algebraic expressions without losing static type safety.
|
|
6
|
+
|
|
7
|
+
| Approach | Example | Infix readability | Type safety & IDE rename | Indexed variables (`flow[i][j]`) |
|
|
8
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
9
|
+
| Method chaining | `x.mul(30).add(y.mul(50)).le(40)` | Low | Yes | Yes |
|
|
10
|
+
| Raw string parsing | `parseConstraint("30 * x + 50 * y <= 40", resolver)` | High | No (runtime string lookup) | Fragile (string concatenation) |
|
|
11
|
+
| **Tagged template (`expr`)** | `` expr`30 * ${x} + 50 * ${y} <= 40` `` | **High** | **Yes (actual TS references)** | **Yes (bound by object reference)** |
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## ✍️ Tagged Template Literal (`expr`)
|
|
16
|
+
|
|
17
|
+
Similar to parameterized `sql` tagged templates, `expr` receives the static operator text (`+`, `-`, `*`, `^2`, `<=`, `>=`, `==`) separately from the interpolated `${...}` values (`Var`, `LinExpr`, `QuadExpr`, or finite `number`). Variables are bound by object reference rather than resolved by string name, avoiding naming collisions and preserving TypeScript type checking and IDE refactoring.
|
|
18
|
+
|
|
19
|
+
Use `expr` for both arithmetic expressions (`LinExpr | QuadExpr`) and relational constraints (`Constraint`):
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { Model, expr } from "@ohtaman/washi";
|
|
23
|
+
|
|
24
|
+
const m = new Model();
|
|
25
|
+
const x = m.continuous("x", { lb: 0 });
|
|
26
|
+
const y = m.continuous("y", { lb: 0 });
|
|
27
|
+
|
|
28
|
+
// 1. Linear combination: 3*x + 5*y - 10
|
|
29
|
+
const linearExpr = expr`3 * ${x} + 5 * ${y} - 10`;
|
|
30
|
+
|
|
31
|
+
// 2. Quadratic expression: x * y + 2 * x^2
|
|
32
|
+
const quadExpr = expr`${x} * ${y} + 2 * ${x} * ${x}`;
|
|
33
|
+
|
|
34
|
+
// 3. Relational constraint:
|
|
35
|
+
const balanceConstraint = expr`${x} + 2 * ${y} <= 40`;
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Supported Syntax in `expr`
|
|
39
|
+
- **Addition & Subtraction**: `${x} + ${y} - 5`
|
|
40
|
+
- **Scalar Multiplication**: `4 * ${x}`, `${x} * 4`
|
|
41
|
+
- **Variable Multiplication (Quadratic terms)**: `${x} * ${y}`, `2 * ${x} * ${x}`
|
|
42
|
+
- **Powers & Exponentiation**: `${x}^2`, `${x}**2`
|
|
43
|
+
- **Unary Negation**: `-${x} + 10`
|
|
44
|
+
- **Parentheses**: `2 * (${x} + ${y})`
|
|
45
|
+
- **Nested Expressions**: `expr` seamlessly interpolates variables, numbers, `sum()` results, and nested expressions.
|
|
46
|
+
|
|
47
|
+
> [!NOTE]
|
|
48
|
+
> For relational comparisons (`<=`, `>=`, `==`), `expr` (e.g., ``expr`${x} + 2 * ${y} <= 40` ``) returns a first-class `Constraint`.
|
|
49
|
+
> For arithmetic expressions without comparison operators, `expr` uniformly handles linear and quadratic expressions (up to degree 2): if quadratic terms are present, `expr` returns a `QuadExpr`; otherwise, it returns a `LinExpr`. Polynomial terms of degree $> 2$ (e.g. ``${x} * ${y} * ${z}``, ``${x}^3``) and non-linear constraints (e.g. ``${x} * ${y} <= 10``) are rejected because standard MILP/LP/QP formulations only support linear constraints and quadratic objectives.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## ⚡ High-Performance Summation (`sum`)
|
|
54
|
+
|
|
55
|
+
For iterating over large collections of variables or indices:
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { sum } from "@ohtaman/washi";
|
|
59
|
+
|
|
60
|
+
const items = ["A", "B", "C"];
|
|
61
|
+
const weights = { A: 10, B: 20, C: 30 };
|
|
62
|
+
const x = m.binary(items);
|
|
63
|
+
|
|
64
|
+
// 1. Transform elements with a mapping callback:
|
|
65
|
+
const totalWeight = sum(items, (item) => x[item].mul(weights[item]));
|
|
66
|
+
|
|
67
|
+
// 2. Direct array summation:
|
|
68
|
+
const count = sum(Object.values(x));
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`sum()` runs in $O(N)$ linear time without AST overhead.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 🎯 Dot Product (`dot`)
|
|
76
|
+
|
|
77
|
+
The `dot(a, b)` helper computes linear combinations of numeric coefficients (`number`) and variables or linear expressions (`Var | LinExpr`) in either operand order (`dot(coeffs, vars)` or `dot(vars, coeffs)`).
|
|
78
|
+
|
|
79
|
+
Supported 1D and 2D container shapes:
|
|
80
|
+
- **1D Arrays**: `number[]` $\times$ `(Var | LinExpr)[]`
|
|
81
|
+
- **1D Records**: `Record<K, number>` $\times$ `Record<K, Var | LinExpr>`
|
|
82
|
+
- **2D Arrays**: `number[][]` $\times$ `(Var | LinExpr)[][]`
|
|
83
|
+
- **2D Records**: `Record<R, Record<C, number>>` $\times$ `Record<R, Record<C, Var | LinExpr>>`
|
|
84
|
+
- **2D Mixed**: `Array<Record<C, ...>>` or `Record<R, Array<...>>`
|
|
85
|
+
|
|
86
|
+
Mismatched array lengths, missing dictionary keys, or non-finite/non-linear leaves throw an explicit error indicating the offending path (e.g., `at '[0].StoreB'`).
|
|
87
|
+
|
|
88
|
+
### 1D Vector Dot Product
|
|
89
|
+
```typescript
|
|
90
|
+
import { dot } from "@ohtaman/washi";
|
|
91
|
+
|
|
92
|
+
const profits = { desk: 30, chair: 50 };
|
|
93
|
+
const vars = m.integer(["desk", "chair"]);
|
|
94
|
+
|
|
95
|
+
// Computes 30 * vars.desk + 50 * vars.chair
|
|
96
|
+
const totalProfit = dot(profits, vars);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 2D Matrix Dot Product
|
|
100
|
+
Works directly with 2D arrays (`m.continuous(rows, cols)`), nested dictionaries (`m.continuous(rowKeys, colKeys)`), or mixed 2D containers:
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
const factories = ["Tokyo", "Osaka"];
|
|
104
|
+
const stores = ["StoreA", "StoreB"];
|
|
105
|
+
|
|
106
|
+
const costs = {
|
|
107
|
+
Tokyo: { StoreA: 4, StoreB: 7 },
|
|
108
|
+
Osaka: { StoreA: 8, StoreB: 3 },
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const ship = m.continuous(factories, stores);
|
|
112
|
+
|
|
113
|
+
// Total shipping cost in one line:
|
|
114
|
+
m.objective(minimize(dot(costs, ship)));
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 🧮 Quadratic Expressions (`QuadExpr` & `expr`)
|
|
120
|
+
|
|
121
|
+
WaSHI natively supports Convex Quadratic Programming (QP) with the HiGHS solver. Quadratic objectives can be written uniformly using the `expr` template literal, or algebraically using `QuadExpr` and `.mul()`:
|
|
122
|
+
|
|
123
|
+
### 1. Using `expr` (Recommended)
|
|
124
|
+
You can write quadratic formulas naturally with `*`, `^2`, or `**2`:
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
import { Model, expr, minimize } from "@ohtaman/washi";
|
|
128
|
+
|
|
129
|
+
const m = new Model();
|
|
130
|
+
const x = m.continuous("x", { lb: 0 });
|
|
131
|
+
const y = m.continuous("y", { lb: 0 });
|
|
132
|
+
|
|
133
|
+
// Quadratic objective using expr:
|
|
134
|
+
m.objective(minimize(expr`3 * ${x} + ${x}^2 + ${y}^2`));
|
|
135
|
+
|
|
136
|
+
// Linear constraint using expr:
|
|
137
|
+
m.constraint(expr`${x} + ${y} >= 4`);
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### 2. Method Chaining with `.mul()` or `QuadExpr`
|
|
141
|
+
Alternatively, you can construct quadratic expressions programmatically:
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import { Model, QuadExpr, minimize } from "@ohtaman/washi";
|
|
145
|
+
|
|
146
|
+
const m = new Model();
|
|
147
|
+
const x = m.continuous("x", { lb: 0 });
|
|
148
|
+
const y = m.continuous("y", { lb: 0 });
|
|
149
|
+
|
|
150
|
+
// 1. Construct quadratic terms with .mul()
|
|
151
|
+
// Represents x * y + 2 * x^2
|
|
152
|
+
const quad = x.mul(y).add(x.mul(x).mul(2));
|
|
153
|
+
|
|
154
|
+
// 2. Build portfolio risk objective: w^T * Sigma * w
|
|
155
|
+
const assets = ["Tech", "Bonds"];
|
|
156
|
+
const w = m.continuous(assets, { lb: 0, ub: 1 });
|
|
157
|
+
|
|
158
|
+
let risk = new QuadExpr();
|
|
159
|
+
risk = risk.add(w.Tech.mul(w.Tech).mul(0.04));
|
|
160
|
+
risk = risk.add(w.Tech.mul(w.Bonds).mul(0.005));
|
|
161
|
+
risk = risk.add(w.Bonds.mul(w.Bonds).mul(0.01));
|
|
162
|
+
|
|
163
|
+
m.objective(minimize(risk));
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## 📊 Evaluating Expressions After Solving (`.value`)
|
|
169
|
+
|
|
170
|
+
Both `LinExpr` and `QuadExpr` expose a `.value` getter (`number | null`). Once a model has been solved and all referenced variables have finite primal values, you can evaluate any sub-expression—such as total department cost, portfolio return, or resource utilization—directly without manual arithmetic:
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
const revenue = expr`30 * ${x} + 50 * ${y}`;
|
|
174
|
+
const laborUsed = expr`2 * ${x} + ${y}`;
|
|
175
|
+
|
|
176
|
+
m.objective(maximize(revenue));
|
|
177
|
+
m.constraint(expr`${x} + 2 * ${y} <= 40`, "wood_cap");
|
|
178
|
+
m.constraint(expr`${laborUsed} <= 50`, "labor_cap");
|
|
179
|
+
|
|
180
|
+
await m.solve();
|
|
181
|
+
|
|
182
|
+
console.log("Total Revenue:", revenue.value); // 1100 (x = 20, y = 10)
|
|
183
|
+
console.log("Labor Used:", laborUsed.value); // 50
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
If the model has not been solved (or any variable in the expression has `value === null`), `.value` returns `null`.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## 🔗 Fluent Arithmetic & Relational Methods
|
|
191
|
+
|
|
192
|
+
In addition to `expr`, `Var`, `LinExpr`, and `QuadExpr` provide immutable fluent methods for programmatic expression building:
|
|
193
|
+
|
|
194
|
+
| Method | Supported On | Return Type | Description |
|
|
195
|
+
| :--- | :--- | :--- | :--- |
|
|
196
|
+
| `.add(other)` | `Var`, `LinExpr`, `QuadExpr` | `LinExpr` or `QuadExpr` | Adds a number, `Var`, `LinExpr`, or `QuadExpr`. |
|
|
197
|
+
| `.sub(other)` | `Var`, `LinExpr`, `QuadExpr` | `LinExpr` or `QuadExpr` | Subtracts a number, `Var`, `LinExpr`, or `QuadExpr`. |
|
|
198
|
+
| `.mul(other)` | `Var`, `LinExpr`, `QuadExpr` | `LinExpr` or `QuadExpr` | Multiplies by a scalar `number`, or multiplies two linear terms into a `QuadExpr`. |
|
|
199
|
+
| `.div(divisor)` | `LinExpr`, `QuadExpr` | `LinExpr` or `QuadExpr` | Divides by a non-zero finite scalar `number`. |
|
|
200
|
+
| `.neg()` | `Var`, `LinExpr`, `QuadExpr` | `LinExpr` or `QuadExpr` | Negates all terms ($-e$). |
|
|
201
|
+
| `.le(rhs)` | `Var`, `LinExpr` | `Constraint` | Builds a $\le$ constraint (`lhs <= rhs`). |
|
|
202
|
+
| `.ge(rhs)` | `Var`, `LinExpr` | `Constraint` | Builds a $\ge$ constraint (`lhs >= rhs`). |
|
|
203
|
+
| `.eq(rhs)` | `Var`, `LinExpr` | `Constraint` | Builds an $==$ equality constraint (`lhs == rhs`). |
|
|
204
|
+
|