@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
|
@@ -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
|
|
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
|
-
##
|
|
15
|
+
## Tagged Template Literal (`expr`)
|
|
16
16
|
|
|
17
|
-
Similar to parameterized `sql` tagged templates, `expr` receives
|
|
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}
|
|
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
|
-
- **
|
|
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
|
|
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
|
-
##
|
|
55
|
+
## High-Performance Summation (`sum`)
|
|
54
56
|
|
|
55
|
-
|
|
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.
|
|
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
|
-
//
|
|
68
|
-
const count = sum(
|
|
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
|
-
##
|
|
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
|
|
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
|
-
|
|
114
|
-
|
|
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
|
-
##
|
|
163
|
+
## Quadratic Expressions (`QuadExpr` & `expr`)
|
|
120
164
|
|
|
121
|
-
WaSHI
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
##
|
|
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
|
|
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
|
-
##
|
|
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)).
|
package/docs/guide/objectives.md
CHANGED
|
@@ -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
|
-
##
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
|
package/docs/guide/unsat-core.md
CHANGED
|
@@ -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: **
|
|
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
|
-
##
|
|
11
|
+
## Conflict Diagnosis (`unsatCore` / `conflicts`)
|
|
12
12
|
|
|
13
|
-
When
|
|
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
|
|
@@ -42,22 +42,41 @@ Notice that `routine_operating_cost` is **not** in the core, because only `minim
|
|
|
42
42
|
> - **Use `m.check({ solver: "z3" })` for Unsat Core extraction**: `m.solve()` runs optimization (`Z3.Optimize`), whereas `m.check()` runs tracked satisfiability checking (`Z3.Solver.assertAndTrack`) and populates `result.conflicts` / `m.conflicts` (and alias `m.unsatCore`).
|
|
43
43
|
> - **Name your constraints**: Variable bounds (`lb`, `ub`) act as background domain declarations. To include a bound or business rule in the reported `unsatCore` list, register it as a named constraint via `m.constraint(expr`...`, "rule_name")`.
|
|
44
44
|
|
|
45
|
-
### Workflow:
|
|
45
|
+
### Workflow: Infeasibility Diagnosis with HiGHS or Z3
|
|
46
46
|
|
|
47
|
-
Because WaSHI models are solver-independent, you can
|
|
47
|
+
Because WaSHI models are solver-independent, you can diagnose infeasibilities using either solver:
|
|
48
|
+
1. **Built-in HiGHS & GLPK (default)**: `await m.check()` uses a single-solve elastic slack formulation ($\min \sum s_i$) to identify which constraints required relaxation (`diag.conflicts`), requiring no extra dependencies.
|
|
49
|
+
2. **Z3 SMT Solver**: `await m.check({ solver: "z3" })` uses SMT tracking to extract an exact unsatisfiable core (`diag.unsatCore` / `diag.conflicts`).
|
|
48
50
|
|
|
49
51
|
```typescript
|
|
50
52
|
const status = await m.solve(); // Fast MIP solve with default HiGHS
|
|
51
53
|
|
|
52
54
|
if (status === "Infeasible") {
|
|
53
|
-
|
|
55
|
+
// Option 1: Built-in single-solve elastic diagnosis with HiGHS
|
|
56
|
+
const diag = await m.check();
|
|
54
57
|
console.error("Model is infeasible due to conflicting rules:", diag.conflicts);
|
|
58
|
+
|
|
59
|
+
// Option 2: Exact SMT unsat core diagnosis with Z3 (requires import "@ohtaman/washi/solvers/z3")
|
|
60
|
+
// const z3Diag = await m.check({ solver: "z3" });
|
|
61
|
+
// console.error("Z3 Unsat Core:", z3Diag.unsatCore);
|
|
55
62
|
}
|
|
56
63
|
```
|
|
57
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
|
+
|
|
58
77
|
---
|
|
59
78
|
|
|
60
|
-
##
|
|
79
|
+
## Independent Solution Validation (`m.validateSolution()`)
|
|
61
80
|
|
|
62
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:
|
|
63
82
|
|
|
@@ -97,7 +116,7 @@ if (m.hasPrimalSolution) {
|
|
|
97
116
|
|
|
98
117
|
---
|
|
99
118
|
|
|
100
|
-
##
|
|
119
|
+
## Inspecting the LP Formulation (`m.toLpString()`)
|
|
101
120
|
|
|
102
121
|
When debugging coefficients, signs, or Big-M indicator expansions, call `m.toLpString()` to export the model in standard CPLEX/HiGHS `.lp` format:
|
|
103
122
|
|
|
@@ -123,7 +142,7 @@ End
|
|
|
123
142
|
|
|
124
143
|
---
|
|
125
144
|
|
|
126
|
-
##
|
|
145
|
+
## Unit Testing Application Logic with `MockSolver`
|
|
127
146
|
|
|
128
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:
|
|
129
148
|
|