@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.
Files changed (130) hide show
  1. package/README.ja.md +149 -121
  2. package/README.md +149 -121
  3. package/README.zh-CN.md +150 -122
  4. package/THIRD_PARTY_LICENSES.md +3 -4
  5. package/dist/browser/_washi-node-only-BvRk9kiK.js +0 -0
  6. package/dist/browser/browser-runtime-TLCY0hkc.js +1793 -0
  7. package/dist/browser/highs-worker.js +21 -3
  8. package/dist/browser/highs.wasm +0 -0
  9. package/dist/browser/highs.wasm.LICENSE +1 -2
  10. package/dist/browser/loader-DewlSho8.js +643 -0
  11. package/dist/browser/solvers/glpk.js +433 -286
  12. package/dist/browser/solvers/glpk.min.js +1 -1
  13. package/dist/browser/solvers/z3.js +1008 -981
  14. package/dist/browser/solvers/z3.min.js +2 -2
  15. package/dist/browser/washi.js +2778 -673
  16. package/dist/browser/washi.min.js +3 -4
  17. package/dist/lib/_washi-node-only-BvRk9kiK.js +0 -0
  18. package/dist/lib/highs-browser-runtime.js +2120 -4124
  19. package/dist/lib/highs-node-runtime.js +24 -1
  20. package/dist/lib/highs-runtime.js +2 -0
  21. package/dist/lib/highs-worker.js +21 -3
  22. package/dist/lib/highs.wasm +0 -0
  23. package/dist/lib/highs.wasm.LICENSE +1 -2
  24. package/dist/lib/loader-BZUQNvh4.js +649 -0
  25. package/dist/lib/registry-C20sxTwm.js +964 -0
  26. package/dist/lib/solvers/glpk.js +201 -90
  27. package/dist/lib/solvers/z3.js +399 -259
  28. package/dist/lib/washi.js +2363 -1056
  29. package/dist/types/decomposition/benders.d.ts +32 -0
  30. package/dist/types/decomposition/benders.d.ts.map +1 -0
  31. package/dist/types/decomposition/column-generation.d.ts +34 -0
  32. package/dist/types/decomposition/column-generation.d.ts.map +1 -0
  33. package/dist/types/decomposition/index.d.ts +4 -0
  34. package/dist/types/decomposition/index.d.ts.map +1 -0
  35. package/dist/types/decomposition/types.d.ts +230 -0
  36. package/dist/types/decomposition/types.d.ts.map +1 -0
  37. package/dist/types/index.d.ts +5 -4
  38. package/dist/types/index.d.ts.map +1 -1
  39. package/dist/types/modeler/big-m.d.ts +75 -0
  40. package/dist/types/modeler/big-m.d.ts.map +1 -0
  41. package/dist/types/modeler/boolean-expr.d.ts +103 -14
  42. package/dist/types/modeler/boolean-expr.d.ts.map +1 -1
  43. package/dist/types/modeler/constraint.d.ts +47 -9
  44. package/dist/types/modeler/constraint.d.ts.map +1 -1
  45. package/dist/types/modeler/expression.d.ts +123 -28
  46. package/dist/types/modeler/expression.d.ts.map +1 -1
  47. package/dist/types/modeler/index.d.ts +11 -6
  48. package/dist/types/modeler/index.d.ts.map +1 -1
  49. package/dist/types/modeler/linearizer.d.ts +15 -0
  50. package/dist/types/modeler/linearizer.d.ts.map +1 -0
  51. package/dist/types/modeler/lp-writer.d.ts +30 -3
  52. package/dist/types/modeler/lp-writer.d.ts.map +1 -1
  53. package/dist/types/modeler/model.d.ts +623 -131
  54. package/dist/types/modeler/model.d.ts.map +1 -1
  55. package/dist/types/modeler/objective.d.ts +59 -0
  56. package/dist/types/modeler/objective.d.ts.map +1 -0
  57. package/dist/types/modeler/parser.d.ts +34 -10
  58. package/dist/types/modeler/parser.d.ts.map +1 -1
  59. package/dist/types/modeler/quad-expr.d.ts +26 -7
  60. package/dist/types/modeler/quad-expr.d.ts.map +1 -1
  61. package/dist/types/modeler/solution-validator.d.ts +93 -0
  62. package/dist/types/modeler/solution-validator.d.ts.map +1 -0
  63. package/dist/types/modeler/solvers/glpk/solver.d.ts +26 -2
  64. package/dist/types/modeler/solvers/glpk/solver.d.ts.map +1 -1
  65. package/dist/types/modeler/solvers/highs/browser-runtime.d.ts +2 -1
  66. package/dist/types/modeler/solvers/highs/browser-runtime.d.ts.map +1 -1
  67. package/dist/types/modeler/solvers/highs/driver.d.ts +232 -0
  68. package/dist/types/modeler/solvers/highs/driver.d.ts.map +1 -0
  69. package/dist/types/modeler/solvers/highs/index.d.ts +1 -0
  70. package/dist/types/modeler/solvers/highs/index.d.ts.map +1 -1
  71. package/dist/types/modeler/solvers/highs/loader.d.ts +38 -13
  72. package/dist/types/modeler/solvers/highs/loader.d.ts.map +1 -1
  73. package/dist/types/modeler/solvers/highs/node-runtime.d.ts +2 -1
  74. package/dist/types/modeler/solvers/highs/node-runtime.d.ts.map +1 -1
  75. package/dist/types/modeler/solvers/highs/solver.d.ts +12 -5
  76. package/dist/types/modeler/solvers/highs/solver.d.ts.map +1 -1
  77. package/dist/types/modeler/solvers/highs/worker-client.d.ts.map +1 -1
  78. package/dist/types/modeler/solvers/mock.d.ts +14 -5
  79. package/dist/types/modeler/solvers/mock.d.ts.map +1 -1
  80. package/dist/types/modeler/solvers/types.d.ts +73 -11
  81. package/dist/types/modeler/solvers/types.d.ts.map +1 -1
  82. package/dist/types/modeler/solvers/z3/loader.d.ts +2 -3
  83. package/dist/types/modeler/solvers/z3/loader.d.ts.map +1 -1
  84. package/dist/types/modeler/solvers/z3/solver.d.ts +15 -5
  85. package/dist/types/modeler/solvers/z3/solver.d.ts.map +1 -1
  86. package/dist/types/modeler/types.d.ts +86 -1
  87. package/dist/types/modeler/types.d.ts.map +1 -1
  88. package/dist/types/modeler/validation.d.ts +56 -0
  89. package/dist/types/modeler/validation.d.ts.map +1 -0
  90. package/dist/types/modeler/variable.d.ts +48 -14
  91. package/dist/types/modeler/variable.d.ts.map +1 -1
  92. package/docs/examples/diet-sensitivity.md +90 -0
  93. package/docs/examples/ecommerce-coupons.md +106 -0
  94. package/docs/examples/index.md +57 -0
  95. package/docs/examples/knapsack.md +60 -0
  96. package/docs/examples/portfolio-qp.md +83 -0
  97. package/docs/examples/production.md +51 -0
  98. package/docs/examples/shift-scheduling.md +175 -0
  99. package/docs/examples/smt-security.md +74 -0
  100. package/docs/examples/transportation.md +90 -0
  101. package/docs/examples/traveling-salesperson.md +173 -0
  102. package/docs/examples/unsat-core.md +64 -0
  103. package/docs/examples/vehicle-routing.md +206 -0
  104. package/docs/getting-started/ai-agents.md +271 -0
  105. package/docs/getting-started/cross-origin.md +87 -0
  106. package/docs/getting-started/index.md +116 -0
  107. package/docs/getting-started/runtimes.md +227 -0
  108. package/docs/guide/constraints.md +177 -0
  109. package/docs/guide/decomposition.md +399 -0
  110. package/docs/guide/expressions.md +204 -0
  111. package/docs/guide/objectives.md +170 -0
  112. package/docs/guide/unsat-core.md +154 -0
  113. package/docs/guide/variables.md +188 -0
  114. package/docs/ja/getting-started/ai-agents.md +270 -0
  115. package/docs/solvers/custom-solver.md +525 -0
  116. package/docs/solvers/glpk.md +66 -0
  117. package/docs/solvers/highs.md +115 -0
  118. package/docs/solvers/index.md +42 -0
  119. package/docs/solvers/z3.md +78 -0
  120. package/docs/zh/getting-started/ai-agents.md +270 -0
  121. package/llms-full.txt +4065 -0
  122. package/llms.txt +201 -0
  123. package/package.json +32 -16
  124. package/skills/washi/SKILL.md +197 -0
  125. package/dist/browser/browser-runtime-D13gHOT5.js +0 -4189
  126. package/dist/browser/loader-CVt4CloI.js +0 -58
  127. package/dist/lib/async-mutex-Duo4jwzn.js +0 -195
  128. package/dist/lib/constraint-BRuB3ooC.js +0 -90
  129. package/dist/lib/loader-Dti0v9yC.js +0 -50
  130. 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
+