@ohtaman/washi 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.ja.md +103 -421
  2. package/README.md +103 -421
  3. package/README.zh-CN.md +103 -421
  4. package/THIRD_PARTY_LICENSES.md +5 -3
  5. package/dist/browser/solvers/glpk.js +2 -2
  6. package/dist/browser/solvers/glpk.min.js +1 -1
  7. package/dist/browser/washi.js +777 -261
  8. package/dist/browser/washi.min.js +3 -3
  9. package/dist/lib/{registry-C20sxTwm.js → registry-D7TZ3Wh_.js} +88 -18
  10. package/dist/lib/solvers/glpk.js +3 -3
  11. package/dist/lib/solvers/z3.js +1 -1
  12. package/dist/lib/washi.js +864 -421
  13. package/dist/types/index.d.ts +1 -1
  14. package/dist/types/index.d.ts.map +1 -1
  15. package/dist/types/modeler/expression.d.ts +47 -26
  16. package/dist/types/modeler/expression.d.ts.map +1 -1
  17. package/dist/types/modeler/index.d.ts +2 -2
  18. package/dist/types/modeler/index.d.ts.map +1 -1
  19. package/dist/types/modeler/model.d.ts +12 -6
  20. package/dist/types/modeler/model.d.ts.map +1 -1
  21. package/dist/types/modeler/parser.d.ts +25 -5
  22. package/dist/types/modeler/parser.d.ts.map +1 -1
  23. package/dist/types/modeler/solvers/glpk/solver.d.ts.map +1 -1
  24. package/dist/types/modeler/solvers/highs/solver.d.ts.map +1 -1
  25. package/dist/types/modeler/solvers/highs/worker-client.d.ts.map +1 -1
  26. package/dist/types/modeler/solvers/types.d.ts +2 -0
  27. package/dist/types/modeler/solvers/types.d.ts.map +1 -1
  28. package/docs/examples/diet-sensitivity.md +15 -14
  29. package/docs/examples/ecommerce-coupons.md +15 -22
  30. package/docs/examples/index.md +14 -12
  31. package/docs/examples/knapsack.md +8 -7
  32. package/docs/examples/portfolio-qp.md +11 -4
  33. package/docs/examples/production.md +4 -4
  34. package/docs/examples/shift-scheduling.md +20 -7
  35. package/docs/examples/smt-security.md +16 -4
  36. package/docs/examples/transportation.md +46 -19
  37. package/docs/examples/traveling-salesperson.md +28 -44
  38. package/docs/examples/unsat-core.md +15 -5
  39. package/docs/examples/vehicle-routing.md +10 -8
  40. package/docs/getting-started/ai-agents.md +25 -14
  41. package/docs/getting-started/cross-origin.md +56 -28
  42. package/docs/getting-started/index.md +40 -69
  43. package/docs/getting-started/runtimes.md +66 -61
  44. package/docs/guide/constraints.md +46 -40
  45. package/docs/guide/decomposition.md +127 -259
  46. package/docs/guide/expressions.md +90 -39
  47. package/docs/guide/formulation-tips.md +212 -0
  48. package/docs/guide/objectives.md +7 -4
  49. package/docs/guide/unsat-core.md +18 -6
  50. package/docs/guide/variables.md +33 -38
  51. package/docs/ja/getting-started/ai-agents.md +24 -14
  52. package/docs/solvers/highs.md +27 -6
  53. package/docs/solvers/index.md +1 -1
  54. package/docs/solvers/z3.md +3 -0
  55. package/docs/zh/getting-started/ai-agents.md +24 -14
  56. package/llms-full.txt +929 -687
  57. package/llms.txt +21 -19
  58. package/package.json +3 -3
  59. package/skills/washi/SKILL.md +14 -10
@@ -2,7 +2,7 @@
2
2
 
3
3
  Mathematical expressions are linear or quadratic combinations of decision variables and numeric coefficients.
4
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.
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 with full static type safety.
6
6
 
7
7
  | Approach | Example | Infix readability | Type safety & IDE rename | Indexed variables (`flow[i][j]`) |
8
8
  | :--- | :--- | :--- | :--- | :--- |
@@ -12,9 +12,9 @@ In JavaScript and TypeScript—which do not support operator overloading—WaSHI
12
12
 
13
13
  ---
14
14
 
15
- ## ✍️ Tagged Template Literal (`expr`)
15
+ ## Tagged Template Literal (`expr`)
16
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.
17
+ Similar to parameterized `sql` tagged templates, `expr` receives static operator text (`+`, `-`, `*`, `^2`, `<=`, `>=`, `==`) separately from 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
18
 
19
19
  Use `expr` for both arithmetic expressions (`LinExpr | QuadExpr`) and relational constraints (`Constraint`):
20
20
 
@@ -37,44 +37,55 @@ const balanceConstraint = expr`${x} + 2 * ${y} <= 40`;
37
37
 
38
38
  ### Supported Syntax in `expr`
39
39
  - **Addition & Subtraction**: `${x} + ${y} - 5`
40
- - **Scalar Multiplication**: `4 * ${x}`, `${x} * 4`
40
+ - **Scalar Multiplication & Division**: `4 * ${x}`, `${x} / 2`
41
41
  - **Variable Multiplication (Quadratic terms)**: `${x} * ${y}`, `2 * ${x} * ${x}`
42
42
  - **Powers & Exponentiation**: `${x}^2`, `${x}**2`
43
- - **Unary Negation**: `-${x} + 10`
43
+ - **Unary Plus & Negation**: `-${x} + 10`, `+${x}`
44
44
  - **Parentheses**: `2 * (${x} + ${y})`
45
- - **Nested Expressions**: `expr` seamlessly interpolates variables, numbers, `sum()` results, and nested expressions.
45
+ - **Aggregations (`sum` & `dot`)**: `sum(${xVec}) <= 100`, `dot(${costs}, ${xVec})`, `sum(${rows}, ${(r) => grid[r][col]})`
46
+ - **Element-Wise Container Arithmetic**: `sum((${price} - ${cost}) * ${qty})`, `sum((${xVec} - ${target})^2)`, `dot(${price} - ${cost}, ${qty})`
47
+ - **Nested Expressions**: `expr` interpolates variables, numbers, `Array` / `Record` containers (inside `sum` or `dot`), and nested `LinExpr` / `QuadExpr` objects.
46
48
 
47
49
  > [!NOTE]
48
50
  > 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.
51
+ > 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`. Interpolated `Array` or `Record` containers must be reduced to a scalar via `sum(...)` or `dot(..., ...)`. Polynomial terms of degree $> 2$ (e.g., ``${x} * ${y} * ${z}``, ``${x}^3``) and non-linear constraints (e.g., ``${x} * ${y} <= 10``) throw an error because LP/MIP/QP formulations require linear constraints and at most quadratic objectives.
50
52
 
51
53
  ---
52
54
 
53
- ## ⚡ High-Performance Summation (`sum`)
55
+ ## High-Performance Summation (`sum`)
54
56
 
55
- For iterating over large collections of variables or indices:
57
+ Sum 1D or 2D `Array`, `Iterable`, or `Record` containers directly inside `` expr`sum(...)` `` or via the standalone `sum()` helper:
56
58
 
57
59
  ```typescript
58
- import { sum } from "@ohtaman/washi";
60
+ import { expr, sum } from "@ohtaman/washi";
59
61
 
60
- const items = ["A", "B", "C"];
62
+ const items = ["A", "B", "C"] as const;
61
63
  const weights = { A: 10, B: 20, C: 30 };
62
64
  const x = m.binary(items);
63
65
 
64
- // 1. Transform elements with a mapping callback:
66
+ // 1. Direct Record or Array summation inside expr (no Object.values() needed):
67
+ m.constraint(expr`sum(${x}) <= 2`, "max_selected");
68
+
69
+ // 2. Element-wise arithmetic inside expr`sum(...)`:
70
+ const weightedExpr = expr`sum(${weights} * ${x})`;
71
+
72
+ // 3. Transform elements with a mapping callback (standalone or inside expr):
65
73
  const totalWeight = sum(items, (item) => x[item].mul(weights[item]));
66
74
 
67
- // 2. Direct array summation:
68
- const count = sum(Object.values(x));
75
+ // 4. Standalone 1D/2D Record or Array summation:
76
+ const count = sum(x);
69
77
  ```
70
78
 
71
- `sum()` runs in $O(N)$ linear time without AST overhead.
79
+ `sum()` runs in $O(N)$ linear time without AST parsing overhead.
80
+
81
+ > [!TIP]
82
+ > **Avoid $O(N^2)$ `expr` Re-Assignment Inside Loops**: Because `LinExpr` and `expr` results are immutable, accumulating terms one by one via ``total = expr`${total} + ${x[i]}` `` inside a loop copies all previously accumulated terms on every iteration ($O(N^2)$ time and memory). Pass arrays or records to `sum(x)` or `dot(coeffs, x)` for $O(N)$ construction.
72
83
 
73
84
  ---
74
85
 
75
- ## 🎯 Dot Product (`dot`)
86
+ ## Dot Product (`dot`)
76
87
 
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)`).
88
+ The `dot(a, b)` helper (usable both inside `` expr`dot(${a}, ${b})` `` and as a standalone function `dot(a, b)`) 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
89
 
79
90
  Supported 1D and 2D container shapes:
80
91
  - **1D Arrays**: `number[]` $\times$ `(Var | LinExpr)[]`
@@ -87,41 +98,74 @@ Mismatched array lengths, missing dictionary keys, or non-finite/non-linear leav
87
98
 
88
99
  ### 1D Vector Dot Product
89
100
  ```typescript
90
- import { dot } from "@ohtaman/washi";
101
+ import { expr, dot } from "@ohtaman/washi";
91
102
 
92
103
  const profits = { desk: 30, chair: 50 };
93
- const vars = m.integer(["desk", "chair"]);
104
+ const vars = m.integer(["desk", "chair"] as const);
94
105
 
95
- // Computes 30 * vars.desk + 50 * vars.chair
96
- const totalProfit = dot(profits, vars);
106
+ // Computes 30 * vars.desk + 50 * vars.chair inside expr (or via standalone dot(profits, vars))
107
+ const totalProfit = expr`dot(${profits}, ${vars})`;
97
108
  ```
98
109
 
99
- ### 2D Matrix Dot Product
110
+ ### 2D Matrix Inner Product (Frobenius Inner Product)
100
111
  Works directly with 2D arrays (`m.continuous(rows, cols)`), nested dictionaries (`m.continuous(rowKeys, colKeys)`), or mixed 2D containers:
101
112
 
102
113
  ```typescript
103
- const factories = ["Tokyo", "Osaka"];
104
- const stores = ["StoreA", "StoreB"];
114
+ const factories = ["Tokyo", "Osaka"] as const;
115
+ const stores = ["StoreA", "StoreB"] as const;
105
116
 
106
117
  const costs = {
107
118
  Tokyo: { StoreA: 4, StoreB: 7 },
108
119
  Osaka: { StoreA: 8, StoreB: 3 },
109
120
  };
110
121
 
111
- const ship = m.continuous(factories, stores);
122
+ const ship = m.continuous(factories, stores, { lb: 0 });
123
+
124
+ // Total shipping cost across all routes (equivalent to expr`sum(${costs} * ${ship})`):
125
+ const totalShippingCost = expr`dot(${costs}, ${ship})`;
126
+ ```
127
+
128
+ > [!NOTE]
129
+ > **2D `dot(A, B)` is the Frobenius Inner Product**: For 2D matrices or nested records, `dot(A, B)` computes the scalar **Frobenius inner product** $\langle A, B \rangle_F = \sum_{i,j} A_{i,j} B_{i,j}$ (the sum of element-wise products across all rows and columns, equivalent to `` expr`sum(${A} * ${B})` ``), **not** the matrix-matrix product $(AB)_{i,k} = \sum_j A_{i,j} B_{j,k}$.
130
+
131
+ ---
132
+
133
+ ## Composing Aggregations in `expr`
134
+
135
+ ### Separating Operators and Data
112
136
 
113
- // Total shipping cost in one line:
114
- m.objective(minimize(dot(costs, ship)));
137
+ Write mathematical operators, `sum(...)`, `dot(..., ...)`, and comparison symbols (`<=`, `>=`, `==`) in the template string, and pass variables, scalars, and containers (`Array` / `Record`) via `${...}`:
138
+
139
+ ```typescript
140
+ // Nested template calls and Object.values() (verbose)
141
+ const totalProfit1 = expr`${dot(profits, vars)}`;
142
+ const cap1 = expr`${sum(Object.values(vars))} <= 100`;
143
+
144
+ // Direct sum() and dot() inside expr (recommended)
145
+ const totalProfit2 = expr`dot(${profits}, ${vars})`;
146
+ const cap2 = expr`sum(${vars}) <= 100`;
115
147
  ```
116
148
 
149
+ ### Common `expr` Aggregation Syntax
150
+
151
+ | Operation | Mathematical Formula | WaSHI Expression |
152
+ | :--- | :--- | :--- |
153
+ | **Total sum** (1D/2D `Array` or `Record`) | $\sum_i x_i \le C$ | `` m.constraint(expr`sum(${x}) <= ${C}`) `` |
154
+ | **Inner product** (1D vector or 2D Frobenius) | $\sum_i c_i x_i \le B$ | `` m.constraint(expr`dot(${c}, ${x}) <= ${B}`) `` |
155
+ | **Element-wise difference & product** | $\sum_i (p_i - c_i) \cdot x_i$ | `` expr`sum((${price} - ${cost}) * ${x})` `` |
156
+ | **Sum of squared deviations (QP)** | $\sum_i (x_i - t_i)^2$ | `` expr`sum((${x} - ${target})^2)` `` |
157
+ | **Element-wise constraints** ($\forall i \in I$) | $a_i x_i \le b_i \quad (\forall i)$ | `` m.constraint(items.map((i) => expr`${a[i]} * ${x[i]} <= ${b[i]}`)) `` |
158
+ | **2D row sums** (for each row $f$) | $\sum_s X_{f,s} \le S_f \quad (\forall f)$ | `` m.constraint(factories.map((f) => expr`sum(${ship[f]}) <= ${supply[f]}`)) `` |
159
+ | **2D column sums** (for each column $s$) | $\sum_f X_{f,s} \ge D_s \quad (\forall s)$ | `` m.constraint(stores.map((s) => expr`sum(${factories}, ${(f) => ship[f][s]}) >= ${demand[s]}`)) `` |
160
+
117
161
  ---
118
162
 
119
- ## 🧮 Quadratic Expressions (`QuadExpr` & `expr`)
163
+ ## Quadratic Expressions (`QuadExpr` & `expr`)
120
164
 
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()`:
165
+ WaSHI supports Convex Quadratic Programming (QP) objectives via the HiGHS solver. Quadratic expressions can be written using the `expr` template literal or constructed programmatically with `QuadExpr` and `.mul()`.
122
166
 
123
167
  ### 1. Using `expr` (Recommended)
124
- You can write quadratic formulas naturally with `*`, `^2`, or `**2`:
168
+ Write quadratic terms with `*`, `^2`, or `**2`:
125
169
 
126
170
  ```typescript
127
171
  import { Model, expr, minimize } from "@ohtaman/washi";
@@ -138,7 +182,7 @@ m.constraint(expr`${x} + ${y} >= 4`);
138
182
  ```
139
183
 
140
184
  ### 2. Method Chaining with `.mul()` or `QuadExpr`
141
- Alternatively, you can construct quadratic expressions programmatically:
185
+ You can also construct quadratic expressions programmatically:
142
186
 
143
187
  ```typescript
144
188
  import { Model, QuadExpr, minimize } from "@ohtaman/washi";
@@ -147,12 +191,11 @@ const m = new Model();
147
191
  const x = m.continuous("x", { lb: 0 });
148
192
  const y = m.continuous("y", { lb: 0 });
149
193
 
150
- // 1. Construct quadratic terms with .mul()
151
- // Represents x * y + 2 * x^2
194
+ // 1. Construct quadratic terms with .mul() (x * y + 2 * x^2)
152
195
  const quad = x.mul(y).add(x.mul(x).mul(2));
153
196
 
154
- // 2. Build portfolio risk objective: w^T * Sigma * w
155
- const assets = ["Tech", "Bonds"];
197
+ // 2. Build portfolio variance objective: w^T * Sigma * w
198
+ const assets = ["Tech", "Bonds"] as const;
156
199
  const w = m.continuous(assets, { lb: 0, ub: 1 });
157
200
 
158
201
  let risk = new QuadExpr();
@@ -163,13 +206,22 @@ risk = risk.add(w.Bonds.mul(w.Bonds).mul(0.01));
163
206
  m.objective(minimize(risk));
164
207
  ```
165
208
 
209
+ > [!NOTE]
210
+ > **Convexity Requirement ($\mathbf{Q} \succeq 0$)**: QP solvers (including HiGHS) require the quadratic objective matrix $\mathbf{Q}$ to be **positive semi-definite (PSD)** when minimizing (or negative semi-definite when maximizing). Sum-of-squares terms ($\sum_i (x_i - t_i)^2$) and covariance matrices ($\mathbf{w}^\top \boldsymbol{\Sigma} \mathbf{w}$) are guaranteed PSD, whereas indefinite products over continuous variables (such as minimizing $x \cdot y$) are non-convex and rejected by the solver. *(To linearize the product of two binary variables $x_A \cdot x_B$ in MILP, see [Formulation Patterns & Tips](./formulation-tips.md#product-of-two-binary-variables).)*
211
+
166
212
  ---
167
213
 
168
- ## 📊 Evaluating Expressions After Solving (`.value`)
214
+ ## Evaluating Expressions After Solving (`.value`)
169
215
 
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:
216
+ 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 directly:
171
217
 
172
218
  ```typescript
219
+ import { Model, expr, maximize } from "@ohtaman/washi";
220
+
221
+ const m = new Model();
222
+ const x = m.integer("x", { lb: 0 });
223
+ const y = m.integer("y", { lb: 0 });
224
+
173
225
  const revenue = expr`30 * ${x} + 50 * ${y}`;
174
226
  const laborUsed = expr`2 * ${x} + ${y}`;
175
227
 
@@ -187,7 +239,7 @@ If the model has not been solved (or any variable in the expression has `value =
187
239
 
188
240
  ---
189
241
 
190
- ## 🔗 Fluent Arithmetic & Relational Methods
242
+ ## Fluent Arithmetic & Relational Methods
191
243
 
192
244
  In addition to `expr`, `Var`, `LinExpr`, and `QuadExpr` provide immutable fluent methods for programmatic expression building:
193
245
 
@@ -201,4 +253,3 @@ In addition to `expr`, `Var`, `LinExpr`, and `QuadExpr` provide immutable fluent
201
253
  | `.le(rhs)` | `Var`, `LinExpr` | `Constraint` | Builds a $\le$ constraint (`lhs <= rhs`). |
202
254
  | `.ge(rhs)` | `Var`, `LinExpr` | `Constraint` | Builds a $\ge$ constraint (`lhs >= rhs`). |
203
255
  | `.eq(rhs)` | `Var`, `LinExpr` | `Constraint` | Builds an $==$ equality constraint (`lhs == rhs`). |
204
-
@@ -0,0 +1,212 @@
1
+ # Formulation Patterns & Optimization Tips
2
+
3
+ While the preceding Modeling Guide pages cover WaSHI's APIs (`Model`, `expr`, `m.constraint`, `m.solve`, etc.), this page collects standard mathematical optimization formulation patterns and solver tuning techniques frequently used in linear and mixed-integer programming (LP/MIP).
4
+
5
+ ---
6
+
7
+ ## 1. Variables & Domain Modeling
8
+
9
+ ### Semi-Continuous Decisions ($\{0\} \cup [L, U]$)
10
+
11
+ A semi-continuous quantity must be either `0` (inactive) or within a positive interval `[L, U]` (e.g., a production batch with a minimum run size $L = 10$ and capacity $U = 100$).
12
+
13
+ Combine a continuous variable `x` with a binary activation variable `isActive` and linking constraints $L \cdot b \le x \le U \cdot b$:
14
+
15
+ ```typescript
16
+ import { Model, expr } from "@ohtaman/washi";
17
+
18
+ const m = new Model("SemiContinuousBatch");
19
+ const batch = m.continuous("batch", { lb: 0, ub: 100 });
20
+ const isActive = m.binary("is_active");
21
+
22
+ // If isActive === 0, batch is forced to 0; if isActive === 1, batch lies in [10, 100]
23
+ m.constraint(expr`${batch} >= 10 * ${isActive}`, "min_batch_run");
24
+ m.constraint(expr`${batch} <= 100 * ${isActive}`, "max_batch_run");
25
+ ```
26
+
27
+ ### Minimal-Perturbation Rescheduling vs. Warm Starting
28
+
29
+ When re-optimizing an existing schedule after a disruption, passing yesterday's assignment via [`initialValue`](./variables.md#primal-initial-values-warm-start-hints) only gives the MIP solver a starting search point—it does **not** prevent the solver from changing every assignment for a tiny objective gain.
30
+
31
+ To keep a revised binary plan $x_i \in \{0, 1\}$ close to a baseline assignment $x_i^{\text{prev}} \in \{0, 1\}$, penalize the **Hamming distance** (number of flipped decisions) in the objective:
32
+
33
+ $$\text{Changes} = \sum_{i:\, x_i^{\text{prev}} = 0} x_i + \sum_{i:\, x_i^{\text{prev}} = 1} (1 - x_i)$$
34
+
35
+ ```typescript
36
+ import { Model, expr, minimize } from "@ohtaman/washi";
37
+
38
+ const m = new Model("StableReschedule");
39
+ const prevPlan = [1, 0, 1, 0];
40
+ const x = prevPlan.map((prev, i) =>
41
+ m.binary(`slot_${i}`, { initialValue: prev })
42
+ );
43
+
44
+ // Linear terms counting how many 0/1 decisions differ from prevPlan
45
+ const flipTerms = x.map((v, i) => (prevPlan[i] === 1 ? expr`1 - ${v}` : v));
46
+
47
+ // Require at least 3 active slots while penalizing changes from prevPlan
48
+ m.constraint(expr`sum(${x}) >= 3`, "min_active_slots");
49
+ m.objective(minimize(expr`5 * sum(${flipTerms})`));
50
+ ```
51
+
52
+ ---
53
+
54
+ ## 2. Linearizing Non-Linear Expressions
55
+
56
+ ### Product of Two Binary Variables ($x_A \cdot x_B$)
57
+
58
+ In integer programming, if an interaction cost or synergy bonus applies only when two binary decisions $x_A, x_B \in \{0, 1\}$ are **both** equal to `1`, avoid introducing a quadratic term. Instead, introduce an auxiliary variable $z_{AB} \in [0, 1]$ and add the three standard McCormick / Fortet linear inequalities so the model remains a pure MILP:
59
+
60
+ $$z_{AB} \le x_A, \quad z_{AB} \le x_B, \quad z_{AB} \ge x_A + x_B - 1$$
61
+
62
+ ```typescript
63
+ import { Model, expr, maximize } from "@ohtaman/washi";
64
+
65
+ const m = new Model("BinaryProductLinearization");
66
+ const xA = m.binary("xA");
67
+ const xB = m.binary("xB");
68
+ // When xA and xB are binary, continuous [0, 1] bounds suffice to force bothAB into {0, 1}
69
+ const bothAB = m.continuous("both_AB", { lb: 0, ub: 1 });
70
+
71
+ m.constraint(expr`${bothAB} <= ${xA}`, "and_upper_A");
72
+ m.constraint(expr`${bothAB} <= ${xB}`, "and_upper_B");
73
+ m.constraint(expr`${bothAB} >= ${xA} + ${xB} - 1`, "and_lower_AB");
74
+
75
+ m.objective(maximize(expr`10 * ${xA} + 10 * ${xB} + 5 * ${bothAB}`));
76
+ ```
77
+
78
+ ### Min-Max & Max-Min Objectives (Load Balancing)
79
+
80
+ Linear and quadratic objectives cannot call `Math.max(...)` directly. To minimize a bottleneck or peak workload $\min \max_i (\text{load}_i)$, introduce an auxiliary ceiling variable $z$, constrain every individual load by $\text{load}_i \le z$, and minimize $z$:
81
+
82
+ $$\min z \quad \text{subject to} \quad \text{load}_i \le z \quad \forall i$$
83
+
84
+ ```typescript
85
+ import { Model, expr, minimize } from "@ohtaman/washi";
86
+
87
+ const m = new Model("MinMaxLoadBalancing");
88
+ const workers = ["Alice", "Bob", "Carol"] as const;
89
+ const hours = m.continuous(workers, { lb: 0, ub: 40 });
90
+ const maxHours = m.continuous("max_hours", { lb: 0, ub: 40 });
91
+
92
+ // Bound every worker's hours by the shared ceiling maxHours
93
+ m.constraint(workers.map((w) => expr`${hours[w]} <= ${maxHours}`));
94
+ m.constraint(expr`sum(${hours}) >= 60`, "total_required_work");
95
+
96
+ // Minimizing maxHours distributes the 60 hours evenly (20 hours each)
97
+ m.objective(minimize(maxHours));
98
+ ```
99
+
100
+ *(Symmetrically, to **maximize the minimum** level $\max \min_i u_i$, impose $u_i \ge z$ for all $i$ and set `m.objective(maximize(z))`.)*
101
+
102
+ ---
103
+
104
+ ## 3. Constraint Design & Numerical Conditioning
105
+
106
+ ### Hard vs. Soft Constraints (Elastic Slack Variables)
107
+
108
+ Treating every business target as a strict **hard constraint** makes a model `"Infeasible"` as soon as demand exceeds capacity. Keep physical and legal invariants as hard constraints, and model non-rigid targets as **soft constraints** by adding non-negative slack variables ($s \ge 0$) with penalty weights $W$ in the objective:
109
+
110
+ | Target Type | Hard Constraint | Soft Constraint ($s \ge 0$) | Objective Penalty Term (Minimize) |
111
+ | :--- | :--- | :--- | :--- |
112
+ | **Lower bound** | $\text{lhs} \ge b$ | $\text{lhs} + s_{\text{short}} \ge b$ | $+ W \cdot s_{\text{short}}$ |
113
+ | **Upper bound** | $\text{lhs} \le b$ | $\text{lhs} - s_{\text{excess}} \le b$ | $+ W \cdot s_{\text{excess}}$ |
114
+ | **Target equality** | $\text{lhs} = b$ | $\text{lhs} + s_{\text{short}} - s_{\text{excess}} = b$ | $+ W_1 \cdot s_{\text{short}} + W_2 \cdot s_{\text{excess}}$ |
115
+
116
+ ```typescript
117
+ import { Model, expr, minimize } from "@ohtaman/washi";
118
+
119
+ const m = new Model("SoftConstraintCoverage");
120
+ const assignedStaff = m.integer("assigned_staff", { lb: 0, ub: 4 }); // Max 4 staff available
121
+ const shortage = m.continuous("shortage", { lb: 0 }); // Non-negative slack (s >= 0)
122
+
123
+ // Soft target: aim for 5 staff, allowing shortage > 0 if unavailable
124
+ m.constraint(expr`${assignedStaff} + ${shortage} >= 5`, "target_5_staff");
125
+
126
+ // Penalize unmet shortage ($1,000) above regular wage ($100)
127
+ m.objective(minimize(expr`100 * ${assignedStaff} + 1000 * ${shortage}`));
128
+
129
+ await m.solve();
130
+ // assignedStaff.value === 4, shortage.value === 1 (returns best compromise instead of Infeasible)
131
+ ```
132
+
133
+ > [!TIP]
134
+ > **Scaling Penalty Weights ($W$)**: Choose $W$ larger than normal operational marginal costs (typically $10\times$ to $1{,}000\times$ unit cost), and avoid extreme constants such as $10^{12}$. Keeping the ratio between the largest and smallest coefficients within $10^5\text{–}10^6$ preserves floating-point numerical stability in LP/MIP solvers.
135
+
136
+ ### Symmetry Breaking for Identical Resources
137
+
138
+ When a model contains $K$ interchangeable resources—such as identical trucks, bins, or parallel machines—any feasible assignment has up to $K!$ equivalent permutations with the exact same objective value. Without symmetry breaking, a MIP solver explores redundant mirror-image subtrees before proving optimality.
139
+
140
+ Impose a canonical ordering ($u_0 \ge u_1 \ge \dots \ge u_{K-1}$) so identical resources are activated in index order:
141
+
142
+ ```typescript
143
+ import { Model, expr } from "@ohtaman/washi";
144
+
145
+ const m = new Model("SymmetryBreaking");
146
+ const numTrucks = 4;
147
+ const truckUsed = m.binary(numTrucks, { prefix: "truck_used" });
148
+
149
+ // Only allow activating truck k+1 when truck k is also active
150
+ for (let k = 0; k < numTrucks - 1; k++) {
151
+ m.constraint(
152
+ expr`${truckUsed[k]} >= ${truckUsed[k + 1]}`,
153
+ `order_truck_${k}`
154
+ );
155
+ }
156
+ ```
157
+
158
+ ---
159
+
160
+ ## 4. Multi-Objective & MIP Stopping Design
161
+
162
+ ### Weighted Sum vs. Lexicographic Two-Stage Optimization
163
+
164
+ When balancing competing goals—such as minimizing total cost ($f_1$) while also minimizing peak workload ($f_2$)—two approaches are standard:
165
+
166
+ - **Weighted Sum (Single Solve)**: Combine normalized terms into one objective, e.g., ``minimize(expr`${totalCost} + 50 * ${maxHours}`)``.
167
+ - **Lexicographic (Hierarchical) Two-Stage Solve**: When the primary objective has strict priority, solve for $f_1$ first, add a constraint locking $f_1$ within an acceptable tolerance of its optimum $f_1^*$, and re-solve for $f_2$:
168
+
169
+ ```typescript
170
+ import { Model, expr, minimize } from "@ohtaman/washi";
171
+
172
+ const m = new Model("LexicographicTwoStage");
173
+ const workers = ["Alice", "Bob"] as const;
174
+ const wage = { Alice: 20, Bob: 20 };
175
+ const hours = m.continuous(workers, { lb: 0, ub: 40 });
176
+ const maxHours = m.continuous("max_hours", { lb: 0, ub: 40 });
177
+
178
+ const totalCost = expr`dot(${wage}, ${hours})`;
179
+ m.constraint(expr`sum(${hours}) >= 40`, "required_hours");
180
+ m.constraint(workers.map((w) => expr`${hours[w]} <= ${maxHours}`));
181
+
182
+ // Stage 1: Minimize primary objective (total wage cost)
183
+ m.objective(minimize(totalCost));
184
+ await m.solve();
185
+ const minCost = m.objectiveValue!;
186
+
187
+ // Stage 2: Keep cost within 1% of minCost and balance workload (minimize maxHours)
188
+ m.constraint(expr`${totalCost} <= ${minCost * 1.01}`, "near_optimal_cost");
189
+ m.objective(minimize(maxHours));
190
+ await m.solve();
191
+ ```
192
+
193
+ ### MIP Tail-Off & Optimality Gap Tolerances
194
+
195
+ In combinatorial MIP problems, solvers often find a near-optimal feasible solution within seconds, then spend most of the remaining runtime closing the final fraction of a percent in the branch-and-bound tree (**tail-off**).
196
+
197
+ In interactive UIs and batch pipelines, combine `timeLimit` with business-meaningful tolerances in [`m.solve(options)`](./objectives.md#solve-options-solveoptions):
198
+ - **`relGap: 0.01`**: Stop immediately once the incumbent is proven within **1%** of the theoretical dual bound.
199
+ - **`absGap: 100`**: Stop immediately once `|objectiveValue - objectiveBound| <= 100` in the objective's native units.
200
+
201
+ ---
202
+
203
+ ## 5. Decomposition Formulation Tips
204
+
205
+ When using [`solveColumnGeneration` or `solveBenders`](./decomposition.md):
206
+
207
+ 1. **Set Covering (`>= 1`) vs. Set Partitioning (`== 1`) in Column Generation**:
208
+ Formulating coverage constraints as inequalities (`>= 1`) rather than equalities (`== 1`) restricts dual shadow prices to $\pi_i \ge 0$, reducing dual oscillations and accelerating convergence whenever over-coverage can be pruned at no extra cost (e.g., Euclidean routing under the triangle inequality).
209
+ 2. **Price-and-Branch (`finalizeAsMIP: true`) vs. Exact Branch-and-Price**:
210
+ Solving the final Restricted Master Problem as a MIP over columns generated at the root LP relaxation (`finalizeAsMIP: true`) implements the **Price-and-Branch** heuristic—avoiding complex branching inside the pricing subproblem while producing high-quality integer solutions in practice.
211
+ 3. **Multi-Cut Separation in Benders / Lazy Constraints**:
212
+ When a separation callback detects multiple disconnected subtours or violated components in a candidate solution, returning cuts for **all** violated components in one batch reduces the number of master MIP re-solves (see [Traveling Salesperson (`examples/traveling-salesperson`)](../examples/traveling-salesperson.md)).
@@ -25,9 +25,12 @@ m.constraint(expr`2 * ${x} + 3 * ${y} <= 100`, "resource_limit");
25
25
 
26
26
  Calling `m.objective()` with no arguments returns the current objective expression (`LinExpr | QuadExpr`). If you omit setting an objective, the model defaults to minimizing `0`.
27
27
 
28
+ > [!TIP]
29
+ > For objective formulation patterns—such as **Min-Max / Max-Min load balancing**, **weighted-sum vs. lexicographic multi-objective optimization**, and **MIP stopping tolerances (`relGap` / `absGap`)**—see [Formulation Patterns & Optimization Tips](./formulation-tips.md#_4-multi-objective-mip-stopping-design).
30
+
28
31
  ---
29
32
 
30
- ## 🚀 Solving the Model (`m.solve()` vs. `m.check()`)
33
+ ## Solving the Model (`m.solve()` vs. `m.check()`)
31
34
 
32
35
  WaSHI provides two asynchronous execution entrypoints:
33
36
 
@@ -63,7 +66,7 @@ You can also pass a solver name directly as the first argument: `await m.solve("
63
66
 
64
67
  ---
65
68
 
66
- ## ⏱️ Real-Time Progress (`onProgress`) & Cancellation (`AbortSignal`)
69
+ ## Real-Time Progress (`onProgress`) & Cancellation (`AbortSignal`)
67
70
 
68
71
  For long-running MIP optimizations in interactive UIs, you can stream live branch-and-bound progress (`SolveProgressEvent`) and allow users to cancel the solve cleanly with `AbortController`:
69
72
 
@@ -109,7 +112,7 @@ console.log("Has incumbent:", m.hasPrimalSolution); // true if an incumbent was
109
112
 
110
113
  ---
111
114
 
112
- ## 📊 Reading Results, Bounds & Solver Status
115
+ ## Reading Results, Bounds & Solver Status
113
116
 
114
117
  After solving, query the status, optimal values, and MIP optimality bounds on the `Model`:
115
118
 
@@ -157,7 +160,7 @@ If an integer-feasible incumbent was found before hitting `timeLimit` or `signal
157
160
 
158
161
  ---
159
162
 
160
- ## 🔄 Model Mutations & Execution Validity
163
+ ## Model Mutations & Execution Validity
161
164
 
162
165
  A `Model` instance keeps its solution state synchronized across mutations made through `Model` methods and `Model` / `Var` / `Constraint` property setters:
163
166
 
@@ -4,13 +4,13 @@ When building real-world optimization and configuration systems, two verificatio
4
4
  1. **Why is my model infeasible?** Which specific business rules contradict each other?
5
5
  2. **Does the solver's solution genuinely satisfy every constraint?** How do I verify tolerances, integrality, and formulations?
6
6
 
7
- WaSHI provides built-in tools for both: **Unsat Core conflict diagnosis** via Z3 (`m.check()`), **independent solution validation** (`m.validateSolution()`), **LP formulation export** (`m.toLpString()`), and **fast unit testing** (`MockSolver`).
7
+ WaSHI provides built-in tools for both: **conflict diagnosis** via built-in HiGHS elastic relaxation or Z3 Unsat Core (`m.check()`), **independent solution validation** (`m.validateSolution()`), **LP formulation export** (`m.toLpString()`), and **fast unit testing** (`MockSolver`).
8
8
 
9
9
  ---
10
10
 
11
- ## 🔍 Conflict Diagnosis (`unsatCore` / `conflicts`)
11
+ ## Conflict Diagnosis (`unsatCore` / `conflicts`)
12
12
 
13
- When a model is **Infeasible**, traditional MIP solvers simply report that no solution exists without indicating *which* constraints are contradictory.
13
+ When an optimization run (`m.solve()`) returns **`"Infeasible"`**, the status alone tells you that no solution satisfies every constraint—not *which* rules conflict with each other.
14
14
  By calling **`await m.check({ solver: "z3" })`**, WaSHI uses Z3's `assertAndTrack` mechanism to compute an **Unsatisfiable Core (`unsatCore`)**—a conflicting subset of named constraints that cannot be satisfied simultaneously:
15
15
 
16
16
  ```typescript
@@ -62,9 +62,21 @@ if (status === "Infeasible") {
62
62
  }
63
63
  ```
64
64
 
65
+ ### Comparing Z3 `unsatCore` vs. HiGHS Elastic Slack `conflicts`
66
+
67
+ | Property | Z3 (`m.check({ solver: "z3" })`) | HiGHS / GLPK (`m.check()`) |
68
+ | :--- | :--- | :--- |
69
+ | **Algorithm** | SMT assumption tracking (`assertAndTrack`) | Single-solve $L_1$ elastic slack relaxation ($\min \sum s_i$) |
70
+ | **Populated Fields** | Both `result.unsatCore` and `result.conflicts` | `result.conflicts` (`unsatCore` is `undefined`) |
71
+ | **Semantic Meaning** | A **contradictory subset** of rules that cannot all hold together | A **minimum-violation repair set** of constraints whose relaxation restores feasibility |
72
+ | **Unit-Scaling Sensitivity** | **Scale-invariant** (pure logical/algebraic contradiction) | **Scale-sensitive** (minimizing unweighted $\sum s_i$ prefers relaxing a headcount constraint by `2` units over a budget constraint by `10,000` dollars) |
73
+
74
+ > [!TIP]
75
+ > When diagnosing models that mix constraints of vastly different magnitudes (e.g., headcount $\le 8$ alongside budget $\le 50{,}000$), use **`m.check({ solver: "z3" })`** if you want the complete scale-invariant set of mutually contradictory rules, or normalize constraint scales when relying on unweighted $L_1$ slack relaxation.
76
+
65
77
  ---
66
78
 
67
- ## ✅ Independent Solution Validation (`m.validateSolution()`)
79
+ ## Independent Solution Validation (`m.validateSolution()`)
68
80
 
69
81
  After solving a model (or supplying a custom candidate map via `values`), you can independently verify that all variable bounds, integer/binary domains, linear constraints, and logical SMT formulas hold against the original model definitions:
70
82
 
@@ -104,7 +116,7 @@ if (m.hasPrimalSolution) {
104
116
 
105
117
  ---
106
118
 
107
- ## 📄 Inspecting the LP Formulation (`m.toLpString()`)
119
+ ## Inspecting the LP Formulation (`m.toLpString()`)
108
120
 
109
121
  When debugging coefficients, signs, or Big-M indicator expansions, call `m.toLpString()` to export the model in standard CPLEX/HiGHS `.lp` format:
110
122
 
@@ -130,7 +142,7 @@ End
130
142
 
131
143
  ---
132
144
 
133
- ## 🧪 Unit Testing Application Logic with `MockSolver`
145
+ ## Unit Testing Application Logic with `MockSolver`
134
146
 
135
147
  When running unit tests for application code that builds or consumes a WaSHI `Model`, you can use the built-in `MockSolver` (`solver: "mock"`) to return deterministic results instantaneously without loading WebAssembly:
136
148