@ohtaman/washi 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.ja.md +103 -386
  2. package/README.md +104 -387
  3. package/README.zh-CN.md +103 -386
  4. package/THIRD_PARTY_LICENSES.md +5 -4
  5. package/dist/browser/_washi-node-only-BvRk9kiK.js +0 -0
  6. package/dist/browser/browser-runtime-TLCY0hkc.js +1793 -0
  7. package/dist/browser/highs-worker.js +1 -1
  8. package/dist/browser/highs.wasm +0 -0
  9. package/dist/browser/highs.wasm.LICENSE +1 -2
  10. package/dist/browser/loader-DewlSho8.js +643 -0
  11. package/dist/browser/solvers/glpk.js +3 -3
  12. package/dist/browser/solvers/glpk.min.js +1 -1
  13. package/dist/browser/solvers/z3.js +7 -1
  14. package/dist/browser/solvers/z3.min.js +1 -1
  15. package/dist/browser/washi.js +1708 -540
  16. package/dist/browser/washi.min.js +3 -4
  17. package/dist/lib/_washi-node-only-BvRk9kiK.js +0 -0
  18. package/dist/lib/highs-browser-runtime.js +2120 -4124
  19. package/dist/lib/highs-node-runtime.js +24 -1
  20. package/dist/lib/highs-runtime.js +2 -0
  21. package/dist/lib/highs-worker.js +1 -1
  22. package/dist/lib/highs.wasm +0 -0
  23. package/dist/lib/highs.wasm.LICENSE +1 -2
  24. package/dist/lib/loader-BZUQNvh4.js +649 -0
  25. package/dist/lib/{registry-BEfvpZW3.js → registry-D7TZ3Wh_.js} +142 -59
  26. package/dist/lib/solvers/glpk.js +4 -4
  27. package/dist/lib/solvers/z3.js +1 -1
  28. package/dist/lib/washi.js +1576 -465
  29. package/dist/types/decomposition/benders.d.ts +32 -0
  30. package/dist/types/decomposition/benders.d.ts.map +1 -0
  31. package/dist/types/decomposition/column-generation.d.ts +34 -0
  32. package/dist/types/decomposition/column-generation.d.ts.map +1 -0
  33. package/dist/types/decomposition/index.d.ts +4 -0
  34. package/dist/types/decomposition/index.d.ts.map +1 -0
  35. package/dist/types/decomposition/types.d.ts +230 -0
  36. package/dist/types/decomposition/types.d.ts.map +1 -0
  37. package/dist/types/index.d.ts +4 -3
  38. package/dist/types/index.d.ts.map +1 -1
  39. package/dist/types/modeler/boolean-expr.d.ts.map +1 -1
  40. package/dist/types/modeler/constraint.d.ts +11 -0
  41. package/dist/types/modeler/constraint.d.ts.map +1 -1
  42. package/dist/types/modeler/expression.d.ts +57 -26
  43. package/dist/types/modeler/expression.d.ts.map +1 -1
  44. package/dist/types/modeler/index.d.ts +4 -3
  45. package/dist/types/modeler/index.d.ts.map +1 -1
  46. package/dist/types/modeler/linearizer.d.ts +15 -0
  47. package/dist/types/modeler/linearizer.d.ts.map +1 -0
  48. package/dist/types/modeler/lp-writer.d.ts +3 -1
  49. package/dist/types/modeler/lp-writer.d.ts.map +1 -1
  50. package/dist/types/modeler/model.d.ts +159 -8
  51. package/dist/types/modeler/model.d.ts.map +1 -1
  52. package/dist/types/modeler/parser.d.ts +25 -5
  53. package/dist/types/modeler/parser.d.ts.map +1 -1
  54. package/dist/types/modeler/solvers/glpk/solver.d.ts.map +1 -1
  55. package/dist/types/modeler/solvers/highs/browser-runtime.d.ts +2 -1
  56. package/dist/types/modeler/solvers/highs/browser-runtime.d.ts.map +1 -1
  57. package/dist/types/modeler/solvers/highs/driver.d.ts +232 -0
  58. package/dist/types/modeler/solvers/highs/driver.d.ts.map +1 -0
  59. package/dist/types/modeler/solvers/highs/index.d.ts +1 -0
  60. package/dist/types/modeler/solvers/highs/index.d.ts.map +1 -1
  61. package/dist/types/modeler/solvers/highs/loader.d.ts +4 -4
  62. package/dist/types/modeler/solvers/highs/loader.d.ts.map +1 -1
  63. package/dist/types/modeler/solvers/highs/node-runtime.d.ts +2 -1
  64. package/dist/types/modeler/solvers/highs/node-runtime.d.ts.map +1 -1
  65. package/dist/types/modeler/solvers/highs/solver.d.ts +1 -1
  66. package/dist/types/modeler/solvers/highs/solver.d.ts.map +1 -1
  67. package/dist/types/modeler/solvers/highs/worker-client.d.ts.map +1 -1
  68. package/dist/types/modeler/solvers/types.d.ts +4 -0
  69. package/dist/types/modeler/solvers/types.d.ts.map +1 -1
  70. package/dist/types/modeler/types.d.ts +27 -0
  71. package/dist/types/modeler/types.d.ts.map +1 -1
  72. package/docs/examples/diet-sensitivity.md +15 -14
  73. package/docs/examples/ecommerce-coupons.md +15 -22
  74. package/docs/examples/index.md +18 -12
  75. package/docs/examples/knapsack.md +8 -7
  76. package/docs/examples/portfolio-qp.md +11 -4
  77. package/docs/examples/production.md +4 -4
  78. package/docs/examples/shift-scheduling.md +20 -7
  79. package/docs/examples/smt-security.md +16 -4
  80. package/docs/examples/transportation.md +46 -19
  81. package/docs/examples/traveling-salesperson.md +157 -0
  82. package/docs/examples/unsat-core.md +15 -5
  83. package/docs/examples/vehicle-routing.md +208 -0
  84. package/docs/getting-started/ai-agents.md +33 -18
  85. package/docs/getting-started/cross-origin.md +56 -28
  86. package/docs/getting-started/index.md +40 -69
  87. package/docs/getting-started/runtimes.md +66 -61
  88. package/docs/guide/constraints.md +47 -41
  89. package/docs/guide/decomposition.md +267 -0
  90. package/docs/guide/expressions.md +90 -39
  91. package/docs/guide/formulation-tips.md +212 -0
  92. package/docs/guide/objectives.md +7 -4
  93. package/docs/guide/unsat-core.md +28 -9
  94. package/docs/guide/variables.md +33 -38
  95. package/docs/ja/getting-started/ai-agents.md +32 -18
  96. package/docs/solvers/custom-solver.md +1 -1
  97. package/docs/solvers/highs.md +48 -5
  98. package/docs/solvers/index.md +4 -4
  99. package/docs/solvers/z3.md +3 -0
  100. package/docs/zh/getting-started/ai-agents.md +32 -18
  101. package/llms-full.txt +1536 -478
  102. package/llms.txt +28 -22
  103. package/package.json +19 -16
  104. package/skills/washi/SKILL.md +21 -17
  105. package/dist/browser/browser-runtime-D13gHOT5.js +0 -4189
  106. package/dist/browser/loader-CtaDHef_.js +0 -273
  107. package/dist/lib/loader-DmNC_UNk.js +0 -279
@@ -1,17 +1,17 @@
1
1
  # Constraints (`m.constraint` & `m.indicator`)
2
2
 
3
3
  Constraints restrict the feasible solution space of your mathematical or logical model.
4
- In WaSHI, both linear arithmetic constraints and propositional SMT formulas are registered using **`m.constraint()`**:
4
+ In WaSHI, both linear arithmetic constraints and propositional logic formulas are registered using **`m.constraint()`**:
5
5
 
6
6
  ```typescript
7
- m.constraint(expr`${desks} + 2 * ${chairs} <= 40`, "wood_limit");
7
+ m.constraint(expr`${x} + 2 * ${y} <= 40`, "wood_limit");
8
8
  ```
9
9
 
10
10
  ---
11
11
 
12
- ## ✍️ Algebraic Linear Constraints (`expr`)
12
+ ## Algebraic Linear Constraints (`expr`)
13
13
 
14
- The `expr` tagged template literal syntax allows writing equations and inequalities with natural mathematical notation:
14
+ The `expr` tagged template literal allows writing equations and inequalities with standard mathematical notation:
15
15
 
16
16
  ```typescript
17
17
  import { Model, expr } from "@ohtaman/washi";
@@ -30,13 +30,15 @@ m.constraint(expr`3 * ${x} + ${y} >= 20`, "min_demand");
30
30
  m.constraint(expr`${x} + ${y} == 30`, "exact_quota");
31
31
  ```
32
32
 
33
+ `m.constraint()` returns the registered `Constraint` (or `BoolExpr` for logical formulas), or an array when passed a batch array.
34
+
33
35
  ### Supported Comparison Operators
34
36
  - `<=`: Upper bound constraint
35
37
  - `>=`: Lower bound constraint
36
38
  - `==` or `=`: Exact equality constraint
37
39
 
38
40
  ### Batch Registration
39
- You can pass an array of constraints to `m.constraint([...])` to register them transactionally in a single call:
41
+ Pass an array of constraints to `m.constraint([...])` to register them transactionally in a single call:
40
42
 
41
43
  ```typescript
42
44
  m.constraint([expr`${x} <= 10`, expr`${y} <= 20`]);
@@ -44,50 +46,51 @@ m.constraint([expr`${x} <= 10`, expr`${y} <= 20`]);
44
46
 
45
47
  ---
46
48
 
47
- ## 🏷️ Naming Constraints
49
+ ## Naming Constraints
48
50
 
49
- Every constraint can be given a unique, descriptive name as the second argument of `m.constraint(c, name)`:
51
+ Pass a unique, descriptive name as the second argument of `m.constraint(c, name)`:
50
52
 
51
53
  ```typescript
52
54
  m.constraint(expr`${x} + ${y} <= 100`, "budget_cap");
53
55
  ```
54
56
 
55
57
  ### Why Name Constraints?
56
- 1. **Model Inspection (`m.toLpString()`)**: Exported CPLEX LP strings retain these descriptive names (sanitized if needed) so you can inspect the exact generated formulation.
57
- 2. **Conflict Diagnosis (`conflicts` / `unsatCore`)**: When constraints contradict each other, Z3 `m.check()` returns the exact constraint names forming the conflicting subset.
58
+ 1. **Model Inspection (`m.toLpString()`)**: Exported CPLEX LP strings retain these descriptive names (sanitized if needed) so you can inspect the generated formulation.
59
+ 2. **Conflict Diagnosis (`conflicts` / `unsatCore`)**: When constraints contradict each other, `m.check()` returns the conflicting constraint names (via built-in HiGHS elastic relaxation or Z3 `unsatCore`).
58
60
  3. **Direct Lookup (`m.getConstraintByName()`)**: Retrieve a constraint by name after solving to inspect its `dual` or `slack` value.
59
61
 
60
62
  ---
61
63
 
62
- ## 🔁 Adding Constraints over Iterables & Sums
64
+ ## Aggregated Constraints (`sum` & `dot`)
63
65
 
64
- Use `sum()` or `dot()` inside `expr` to express linear aggregations cleanly:
66
+ Use `sum(...)` and `dot(..., ...)` directly inside `expr` to express linear aggregations over arrays and dictionaries cleanly:
65
67
 
66
68
  ```typescript
67
- import { Model, sum, expr } from "@ohtaman/washi";
69
+ import { Model, expr } from "@ohtaman/washi";
68
70
 
69
- const items = ["A", "B", "C"];
71
+ const m = new Model();
72
+ const items = ["A", "B", "C"] as const;
70
73
  const weights = { A: 5, B: 10, C: 15 };
71
74
  const selected = m.binary(items);
72
75
 
76
+ // Select at most 2 items
77
+ m.constraint(expr`sum(${selected}) <= 2`, "max_items");
78
+
73
79
  // Total weight cannot exceed capacity 25
74
- m.constraint(
75
- expr`${sum(items, (i) => selected[i].mul(weights[i]))} <= 25`,
76
- "capacity_limit"
77
- );
80
+ m.constraint(expr`dot(${weights}, ${selected}) <= 25`, "capacity_limit");
78
81
  ```
79
82
 
80
83
  ---
81
84
 
82
- ## 🎯 Conditional / Indicator Constraints (`m.indicator`)
85
+ ## Conditional / Indicator Constraints (`m.indicator`)
83
86
 
84
- When a linear constraint should only take effect when a binary decision variable is active (`1` or `0`), use `m.indicator()`:
87
+ When a linear constraint should only take effect when a binary decision variable (`m.binary()`) equals `1` (or `0`), use `m.indicator()`:
85
88
 
86
89
  ```typescript
87
90
  const useDiscount = m.binary("use_discount");
88
91
  const cartTotal = m.continuous("cart_total", { lb: 0, ub: 10_000 });
89
92
 
90
- // If useDiscount == 1, then cartTotal >= 100
93
+ // If useDiscount == 1, enforce cartTotal >= 100
91
94
  // WaSHI automatically derives a safe, tight Big-M from the variable bounds
92
95
  m.indicator(useDiscount, expr`${cartTotal} >= 100`, {
93
96
  activeValue: 1,
@@ -97,16 +100,16 @@ m.indicator(useDiscount, expr`${cartTotal} >= 100`, {
97
100
 
98
101
  ### Safe Automatic Big-M Derivation & Bounds
99
102
 
100
- When `options.M` is omitted, WaSHI automatically derives a safe, minimal Big-M value from the finite variable bounds:
103
+ When `options.M` is omitted, WaSHI automatically derives a minimal valid Big-M value from finite variable bounds (`lb` and `ub`):
101
104
  - For `<=` constraints, variables with positive coefficients require finite upper bounds (`ub`), and negative coefficients require finite lower bounds (`lb`).
102
105
  - For `>=` constraints, variables with positive coefficients require finite lower bounds (`lb`), and negative coefficients require finite upper bounds (`ub`).
103
- - For `==` constraints, safe bounds are derived separately for both the `<=` and `>=` directions (generating `name_le` and `name_ge`).
106
+ - For `==` constraints, bounds are derived separately for both the `<=` and `>=` directions (generating `name_le` and `name_ge`).
104
107
 
105
- If any referenced variable lacks the required finite bounds, `m.indicator()` throws an actionable error prompting for finite bounds or an explicit `options.M`.
108
+ If any referenced variable lacks the required finite bound, `m.indicator()` throws an error prompting for finite bounds or an explicit `options.M`.
106
109
 
107
- ### Explicit Override & Numerical Stability
110
+ ### Explicit `M` Override
108
111
 
109
- You can override the derived Big-M by providing `{ M: number }`:
112
+ You can override the derived Big-M by passing `{ M: number }`:
110
113
 
111
114
  ```typescript
112
115
  m.indicator(useDiscount, expr`${cartTotal} >= 100`, {
@@ -116,15 +119,13 @@ m.indicator(useDiscount, expr`${cartTotal} >= 100`, {
116
119
  ```
117
120
 
118
121
  > [!TIP]
119
- > **Numerical Stability vs. Correctness**:
120
- > An $M$ value that is too small can inadvertently cut off valid solutions. Conversely, an excessively large $M$ (e.g., $10^9$) causes poor matrix conditioning and numerical instability in MIP solvers.
121
- > By setting explicit, realistic variable bounds (`lb` and `ub`), WaSHI derives the tightest possible safe Big-M automatically.
122
+ > **Prefer Finite Variable Bounds Over Arbitrary Large `M`**: An $M$ value that is too small can cut off valid feasible solutions, whereas an excessively large $M$ (e.g., $10^9$) degrades numerical conditioning in MIP solvers. Setting realistic `lb` and `ub` bounds on variables lets WaSHI compute the tightest safe Big-M automatically.
122
123
 
123
124
  ---
124
125
 
125
- ## 🧠 Propositional Logic & SMT Constraints (`BoolVar`, `and`, `or`, `not`, `implies`, `xor`)
126
+ ## Propositional Logic & SMT Constraints (`BoolVar`, `and`, `or`, `not`, `implies`, `xor`)
126
127
 
127
- When solving with the **Z3** SMT engine (`@ohtaman/washi/solvers/z3`), you can combine propositional boolean variables (`m.bool()`) and linear constraints (`Constraint`) using first-class logical operators:
128
+ WaSHI provides first-class logical operators to combine propositional boolean variables (`m.bool()`) and linear constraints (`Constraint`). Logical formulas are solved natively by the **Z3** SMT adapter (`@ohtaman/washi/solvers/z3`) and automatically linearized to 0-1 MIP constraints on **HiGHS** and **GLPK**:
128
129
 
129
130
  ```typescript
130
131
  import { Model, expr, and, or, not, implies, xor } from "@ohtaman/washi";
@@ -133,14 +134,14 @@ import "@ohtaman/washi/solvers/z3";
133
134
  const m = new Model({ solver: "z3" });
134
135
  const isAdmin = m.bool("isAdmin");
135
136
  const hasMfa = m.bool("hasMfa");
136
- const Clearance = m.integer("clearance", { lb: 0, ub: 5 });
137
+ const clearance = m.integer("clearance", { lb: 0, ub: 5 });
137
138
 
138
139
  // Method syntax on BoolVar / BoolOp:
139
140
  m.constraint(isAdmin.implies(hasMfa), "admin_requires_mfa");
140
141
 
141
142
  // Mixing boolean propositions with linear arithmetic comparisons:
142
143
  m.constraint(
143
- implies(isAdmin, expr`${Clearance} >= 4`),
144
+ implies(isAdmin, expr`${clearance} >= 4`),
144
145
  "admin_clearance_level"
145
146
  );
146
147
 
@@ -154,24 +155,29 @@ m.constraint(and(or(isAdmin, hasMfa), not(xor(isAdmin, hasMfa))), "policy");
154
155
  | :--- | :--- | :--- |
155
156
  | **Trigger variable** | Numeric `Binary` variable (`m.binary()`) | `BoolVar` (`m.bool()`), `BoolOp`, or `Constraint` |
156
157
  | **Consequent** | Linear `Constraint` (`<=`, `>=`, `==`) | `BoolVar`, `BoolOp`, or linear `Constraint` |
157
- | **Target Solvers** | **All MIP/LP solvers** (HiGHS, GLPK, Z3) via Big-M linearization | **Z3 SMT solver** (`capabilities.supportsSMT: true`) |
158
- | **Best For** | Operations research & MIP optimization with an objective (`m.solve()`) | Rule engines, policy verification, and SMT feasibility (`m.check()`) |
158
+ | **Target Solvers** | **All MIP/LP solvers** (HiGHS, GLPK, Z3) via Big-M linearization | **All solvers** (HiGHS and GLPK via automatic 0-1 MIP linearization, Z3 natively) |
159
+ | **Primary Use Case** | MIP optimization models with arithmetic binary variables (`m.solve()`) | Logical rule systems and SMT feasibility (`m.check()`) |
160
+
161
+ > [!NOTE]
162
+ > **General Inequality Antecedents in `implies()`**: On **Z3** (`solver: "z3"`), the antecedent `a` in `implies(a, b)` can be any linear inequality—for example, ``implies(expr`${clearance} >= 4`, hasMfa)`` using `clearance` and `hasMfa` above. On MIP solvers (**HiGHS** / **GLPK**), conditional constraints should use a `BoolVar` (`m.bool()`) antecedent or `m.indicator(binVar, constraint)` with a `Binary` trigger variable.
159
163
 
160
164
  ---
161
165
 
162
- ## 📐 Dual Values, Shadow Prices & Lookups
166
+ ## Dual Values, Shadow Prices & Lookups
163
167
 
164
- For continuous Linear Programming (LP) problems, query the economic shadow price (`dual`) and surplus (`slack`) on each constraint:
168
+ For continuous Linear Programming (LP) problems, query the shadow price (`dual`) and surplus (`slack`) on each constraint (returned by `m.constraint()` or looked up via `m.getConstraintByName()`):
165
169
 
166
170
  ```typescript
167
- m.constraint(expr`${x} + 2 * ${y} <= 40`, "wood");
171
+ const woodConstraint = m.constraint(expr`${x} + 2 * ${y} <= 40`, "wood");
168
172
 
169
173
  await m.solve();
170
174
 
171
- const woodConstraint = m.getConstraintByName("wood");
172
- console.log("Shadow price (dual):", woodConstraint?.dual);
173
- console.log("Slack surplus:", woodConstraint?.slack);
175
+ console.log("Shadow price (dual):", woodConstraint.dual);
176
+ console.log("Slack surplus:", woodConstraint.slack);
174
177
  console.log("Total linear constraints:", m.numConstrs);
175
178
  ```
176
179
 
177
- For logical constraints registered with a name, use `m.getLogicalConstraintByName(name)` to look up the registered `LogicalConstraintItem`.
180
+ For named logical constraints, use `m.getLogicalConstraintByName(name)` to look up the registered `LogicalConstraintItem`.
181
+
182
+ > [!TIP]
183
+ > For modeling techniques such as **soft constraints with slack variables** and **symmetry breaking**, see [Formulation Patterns & Optimization Tips](./formulation-tips.md#_3-constraint-design-numerical-conditioning).
@@ -0,0 +1,267 @@
1
+ # Decomposition Methods (`solveColumnGeneration` & `solveBenders`)
2
+
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.
4
+
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). |
9
+
10
+ ---
11
+
12
+ ## Foundational Primitives
13
+
14
+ WaSHI exposes four lower-level primitives used internally by the decomposition orchestrators and available directly for custom algorithms:
15
+
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:
18
+
19
+ ```typescript
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
+ await m.solve();
29
+
30
+ console.log(c.dual); // Dual value / shadow price
31
+ console.log(m.solution?.duals.capacity); // Lookup by constraint name
32
+ ```
33
+
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:
36
+
37
+ ```typescript
38
+ // Solves the continuous LP relaxation and populates m.solution.duals
39
+ const status = await m.relax();
40
+ console.log("LP relaxation bound:", m.objectiveValue);
41
+ ```
42
+
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:
45
+
46
+ ```typescript
47
+ const col = m.addColumn({
48
+ name: "pattern_1",
49
+ type: "Integer", // or "Continuous" / "Binary"
50
+ lb: 0,
51
+ cost: 1, // Objective coefficient
52
+ coefficients: {
53
+ capacity: 2,
54
+ },
55
+ });
56
+ ```
57
+
58
+ ### 4. Dynamic Constraint Term Updates (`c.addTerm()` / `c.setTerm()`)
59
+ Increment or overwrite the coefficient of a variable inside an existing linear constraint:
60
+
61
+ ```typescript
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
+ ```
68
+
69
+ ---
70
+
71
+ ## Column Generation (`solveColumnGeneration`)
72
+
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
+
80
+ ### Pricing with a WaSHI Subproblem Model
81
+
82
+ ```typescript
83
+ import { Model, expr, minimize, maximize } from "@ohtaman/washi";
84
+
85
+ const rollWidth = 100;
86
+
87
+ // 1. Restricted Master Problem (RMP) with simple single-item initial patterns
88
+ const master = new Model("CuttingStockMaster");
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
92
+
93
+ master.objective(minimize(expr`${pA} + ${pB} + ${pC}`));
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");
97
+
98
+ // 2. Run Column Generation
99
+ const result = await master.solveColumnGeneration({
100
+ finalizeAsMIP: true,
101
+ pricing: async () => {
102
+ // Knapsack pricing subproblem: find a pattern maximizing total dual value
103
+ const sub = new Model("KnapsackPricing");
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 });
107
+
108
+ const dualA = demandA.dual ?? 0;
109
+ const dualB = demandB.dual ?? 0;
110
+ const dualC = demandC.dual ?? 0;
111
+ sub.objective(maximize(expr`${dualA} * ${aA} + ${dualB} * ${aB} + ${dualC} * ${aC}`));
112
+ sub.constraint(expr`45 * ${aA} + 36 * ${aB} + 16 * ${aC} <= ${rollWidth}`);
113
+
114
+ await sub.solve();
115
+
116
+ return {
117
+ cost: 1,
118
+ type: "Integer",
119
+ coefficients: {
120
+ demand_A: Math.round(aA.value ?? 0),
121
+ demand_B: Math.round(aB.value ?? 0),
122
+ demand_C: Math.round(aC.value ?? 0),
123
+ },
124
+ };
125
+ },
126
+ });
127
+
128
+ console.log("Optimal rolls:", result.objectiveValue); // 14
129
+ console.log("Iterations:", result.iterations);
130
+ console.log("Columns added:", result.columnsAdded);
131
+ ```
132
+
133
+ ### Pricing with a Custom Algorithm (`getDual`)
134
+
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`:
136
+
137
+ ```typescript
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;
161
+ },
162
+ });
163
+ ```
164
+
165
+ ---
166
+
167
+ ## Benders Decomposition (`solveBenders`)
168
+
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**.
170
+
171
+ ### Automatic Cut Generation (`sub.link()`)
172
+
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:
174
+
175
+ ```typescript
176
+ import { Model, expr, minimize } from "@ohtaman/washi";
177
+
178
+ // 1. Master problem: binary facility opening decisions
179
+ const master = new Model("FacilityMaster");
180
+ const y1 = master.binary("open_F1");
181
+ const y2 = master.binary("open_F2");
182
+
183
+ // Fixed opening costs (theta for operational subproblem cost is managed automatically)
184
+ master.objective(minimize(expr`100 * ${y1} + 120 * ${y2}`));
185
+ master.constraint(expr`50 * ${y1} + 60 * ${y2} >= 75`, "total_capacity_floor");
186
+
187
+ // 2. Solve with automatic Benders cut generation
188
+ const result = await master.solveBenders({
189
+ gapTolerance: 1e-4,
190
+ subproblem: () => {
191
+ const sub = new Model("DispatchSubproblem");
192
+ const x11 = sub.continuous("x_11", { lb: 0 });
193
+ const x12 = sub.continuous("x_12", { lb: 0 });
194
+ const x13 = sub.continuous("x_13", { lb: 0 });
195
+ const x21 = sub.continuous("x_21", { lb: 0 });
196
+ const x22 = sub.continuous("x_22", { lb: 0 });
197
+ const x23 = sub.continuous("x_23", { lb: 0 });
198
+
199
+ sub.objective(
200
+ minimize(
201
+ expr`3 * ${x11} + 4 * ${x12} + 2 * ${x13} + 4 * ${x21} + 2 * ${x22} + 3 * ${x23}`
202
+ )
203
+ );
204
+
205
+ sub.constraint(expr`${x11} + ${x21} >= 20`, "demand_C1");
206
+ sub.constraint(expr`${x12} + ${x22} >= 25`, "demand_C2");
207
+ sub.constraint(expr`${x13} + ${x23} >= 30`, "demand_C3");
208
+
209
+ // Link subproblem dispatch flows to master binary variables y1, y2
210
+ sub.link(expr`${x11} + ${x12} + ${x13} <= 50 * ${y1}`);
211
+ sub.link(expr`${x21} + ${x22} + ${x23} <= 60 * ${y2}`);
212
+
213
+ return sub;
214
+ },
215
+ });
216
+
217
+ console.log("Optimal total cost:", result.objectiveValue); // 390
218
+ console.log("Cuts added:", result.cutsAdded);
219
+ console.log("Open facilities:", result.values);
220
+ ```
221
+
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)
226
+
227
+ For combinatorial separation (such as lazy subtour elimination) or custom analytical cuts, return one `BendersCut` object, an array `BendersCut[]`, or `null` when converged:
228
+
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
+
237
+ const result = await master.solveBenders({
238
+ theta,
239
+ gapTolerance: 1e-4,
240
+ subproblem: ({ getMasterValue }) => {
241
+ const yVal = getMasterValue(y);
242
+ const estTheta = getMasterValue(theta);
243
+
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) {
247
+ return {
248
+ kind: "optimality", // or "feasibility" for lazy combinatorial cuts
249
+ constraint: expr`${theta} >= 100 - 70 * ${y}`,
250
+ subproblemObjective: 40 * yVal + actualSubCost,
251
+ };
252
+ }
253
+ return null; // Converged
254
+ },
255
+ });
256
+
257
+ console.log(result.objectiveValue); // 70 (y = 1, theta = 30)
258
+ ```
259
+
260
+ ---
261
+
262
+ ## Related Examples & Formulation Tips
263
+
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.