@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.
- package/README.ja.md +103 -386
- package/README.md +104 -387
- package/README.zh-CN.md +103 -386
- package/THIRD_PARTY_LICENSES.md +5 -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 +1 -1
- 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 +3 -3
- package/dist/browser/solvers/glpk.min.js +1 -1
- package/dist/browser/solvers/z3.js +7 -1
- package/dist/browser/solvers/z3.min.js +1 -1
- package/dist/browser/washi.js +1708 -540
- 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 +1 -1
- 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-BEfvpZW3.js → registry-D7TZ3Wh_.js} +142 -59
- package/dist/lib/solvers/glpk.js +4 -4
- package/dist/lib/solvers/z3.js +1 -1
- package/dist/lib/washi.js +1576 -465
- 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 +4 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/modeler/boolean-expr.d.ts.map +1 -1
- package/dist/types/modeler/constraint.d.ts +11 -0
- package/dist/types/modeler/constraint.d.ts.map +1 -1
- package/dist/types/modeler/expression.d.ts +57 -26
- package/dist/types/modeler/expression.d.ts.map +1 -1
- package/dist/types/modeler/index.d.ts +4 -3
- 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 +3 -1
- package/dist/types/modeler/lp-writer.d.ts.map +1 -1
- package/dist/types/modeler/model.d.ts +159 -8
- package/dist/types/modeler/model.d.ts.map +1 -1
- package/dist/types/modeler/parser.d.ts +25 -5
- package/dist/types/modeler/parser.d.ts.map +1 -1
- package/dist/types/modeler/solvers/glpk/solver.d.ts.map +1 -1
- package/dist/types/modeler/solvers/highs/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 +4 -4
- 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 +1 -1
- package/dist/types/modeler/solvers/highs/solver.d.ts.map +1 -1
- package/dist/types/modeler/solvers/highs/worker-client.d.ts.map +1 -1
- package/dist/types/modeler/solvers/types.d.ts +4 -0
- package/dist/types/modeler/solvers/types.d.ts.map +1 -1
- package/dist/types/modeler/types.d.ts +27 -0
- package/dist/types/modeler/types.d.ts.map +1 -1
- package/docs/examples/diet-sensitivity.md +15 -14
- package/docs/examples/ecommerce-coupons.md +15 -22
- package/docs/examples/index.md +18 -12
- package/docs/examples/knapsack.md +8 -7
- package/docs/examples/portfolio-qp.md +11 -4
- package/docs/examples/production.md +4 -4
- package/docs/examples/shift-scheduling.md +20 -7
- package/docs/examples/smt-security.md +16 -4
- package/docs/examples/transportation.md +46 -19
- package/docs/examples/traveling-salesperson.md +157 -0
- package/docs/examples/unsat-core.md +15 -5
- package/docs/examples/vehicle-routing.md +208 -0
- package/docs/getting-started/ai-agents.md +33 -18
- package/docs/getting-started/cross-origin.md +56 -28
- package/docs/getting-started/index.md +40 -69
- package/docs/getting-started/runtimes.md +66 -61
- package/docs/guide/constraints.md +47 -41
- package/docs/guide/decomposition.md +267 -0
- package/docs/guide/expressions.md +90 -39
- package/docs/guide/formulation-tips.md +212 -0
- package/docs/guide/objectives.md +7 -4
- package/docs/guide/unsat-core.md +28 -9
- package/docs/guide/variables.md +33 -38
- package/docs/ja/getting-started/ai-agents.md +32 -18
- package/docs/solvers/custom-solver.md +1 -1
- package/docs/solvers/highs.md +48 -5
- package/docs/solvers/index.md +4 -4
- package/docs/solvers/z3.md +3 -0
- package/docs/zh/getting-started/ai-agents.md +32 -18
- package/llms-full.txt +1536 -478
- package/llms.txt +28 -22
- package/package.json +19 -16
- package/skills/washi/SKILL.md +21 -17
- package/dist/browser/browser-runtime-D13gHOT5.js +0 -4189
- package/dist/browser/loader-CtaDHef_.js +0 -273
- 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
|
|
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`${
|
|
7
|
+
m.constraint(expr`${x} + 2 * ${y} <= 40`, "wood_limit");
|
|
8
8
|
```
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
-
##
|
|
12
|
+
## Algebraic Linear Constraints (`expr`)
|
|
13
13
|
|
|
14
|
-
The `expr` tagged template literal
|
|
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
|
-
|
|
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
|
-
##
|
|
49
|
+
## Naming Constraints
|
|
48
50
|
|
|
49
|
-
|
|
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
|
|
57
|
-
2. **Conflict Diagnosis (`conflicts` / `unsatCore`)**: When constraints contradict each other,
|
|
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
|
-
##
|
|
64
|
+
## Aggregated Constraints (`sum` & `dot`)
|
|
63
65
|
|
|
64
|
-
Use `sum()`
|
|
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,
|
|
69
|
+
import { Model, expr } from "@ohtaman/washi";
|
|
68
70
|
|
|
69
|
-
const
|
|
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
|
-
##
|
|
85
|
+
## Conditional / Indicator Constraints (`m.indicator`)
|
|
83
86
|
|
|
84
|
-
When a linear constraint should only take effect when a binary decision variable
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
110
|
+
### Explicit `M` Override
|
|
108
111
|
|
|
109
|
-
You can override the derived Big-M by
|
|
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
|
-
> **
|
|
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
|
-
##
|
|
126
|
+
## Propositional Logic & SMT Constraints (`BoolVar`, `and`, `or`, `not`, `implies`, `xor`)
|
|
126
127
|
|
|
127
|
-
|
|
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
|
|
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`${
|
|
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 | **
|
|
158
|
-
| **
|
|
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
|
-
##
|
|
166
|
+
## Dual Values, Shadow Prices & Lookups
|
|
163
167
|
|
|
164
|
-
For continuous Linear Programming (LP) problems, query the
|
|
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
|
-
|
|
172
|
-
console.log("
|
|
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
|
|
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.
|