@ohtaman/washi 0.4.0 → 0.5.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 (97) hide show
  1. package/README.ja.md +100 -107
  2. package/README.md +98 -105
  3. package/README.zh-CN.md +101 -108
  4. package/dist/browser/highs-worker.js +21 -3
  5. package/dist/browser/loader-CtaDHef_.js +273 -0
  6. package/dist/browser/solvers/glpk.js +433 -286
  7. package/dist/browser/solvers/glpk.min.js +1 -1
  8. package/dist/browser/solvers/z3.js +1002 -981
  9. package/dist/browser/solvers/z3.min.js +2 -2
  10. package/dist/browser/washi.js +2129 -676
  11. package/dist/browser/washi.min.js +4 -4
  12. package/dist/lib/highs-worker.js +21 -3
  13. package/dist/lib/loader-DmNC_UNk.js +279 -0
  14. package/dist/lib/registry-BEfvpZW3.js +951 -0
  15. package/dist/lib/solvers/glpk.js +201 -90
  16. package/dist/lib/solvers/z3.js +399 -259
  17. package/dist/lib/washi.js +1627 -988
  18. package/dist/types/index.d.ts +4 -4
  19. package/dist/types/index.d.ts.map +1 -1
  20. package/dist/types/modeler/big-m.d.ts +75 -0
  21. package/dist/types/modeler/big-m.d.ts.map +1 -0
  22. package/dist/types/modeler/boolean-expr.d.ts +103 -14
  23. package/dist/types/modeler/boolean-expr.d.ts.map +1 -1
  24. package/dist/types/modeler/constraint.d.ts +36 -9
  25. package/dist/types/modeler/constraint.d.ts.map +1 -1
  26. package/dist/types/modeler/expression.d.ts +113 -28
  27. package/dist/types/modeler/expression.d.ts.map +1 -1
  28. package/dist/types/modeler/index.d.ts +10 -6
  29. package/dist/types/modeler/index.d.ts.map +1 -1
  30. package/dist/types/modeler/lp-writer.d.ts +27 -2
  31. package/dist/types/modeler/lp-writer.d.ts.map +1 -1
  32. package/dist/types/modeler/model.d.ts +478 -131
  33. package/dist/types/modeler/model.d.ts.map +1 -1
  34. package/dist/types/modeler/objective.d.ts +59 -0
  35. package/dist/types/modeler/objective.d.ts.map +1 -0
  36. package/dist/types/modeler/parser.d.ts +34 -10
  37. package/dist/types/modeler/parser.d.ts.map +1 -1
  38. package/dist/types/modeler/quad-expr.d.ts +26 -7
  39. package/dist/types/modeler/quad-expr.d.ts.map +1 -1
  40. package/dist/types/modeler/solution-validator.d.ts +93 -0
  41. package/dist/types/modeler/solution-validator.d.ts.map +1 -0
  42. package/dist/types/modeler/solvers/glpk/solver.d.ts +26 -2
  43. package/dist/types/modeler/solvers/glpk/solver.d.ts.map +1 -1
  44. package/dist/types/modeler/solvers/highs/loader.d.ts +36 -11
  45. package/dist/types/modeler/solvers/highs/loader.d.ts.map +1 -1
  46. package/dist/types/modeler/solvers/highs/solver.d.ts +11 -4
  47. package/dist/types/modeler/solvers/highs/solver.d.ts.map +1 -1
  48. package/dist/types/modeler/solvers/highs/worker-client.d.ts.map +1 -1
  49. package/dist/types/modeler/solvers/mock.d.ts +14 -5
  50. package/dist/types/modeler/solvers/mock.d.ts.map +1 -1
  51. package/dist/types/modeler/solvers/types.d.ts +71 -11
  52. package/dist/types/modeler/solvers/types.d.ts.map +1 -1
  53. package/dist/types/modeler/solvers/z3/loader.d.ts +2 -3
  54. package/dist/types/modeler/solvers/z3/loader.d.ts.map +1 -1
  55. package/dist/types/modeler/solvers/z3/solver.d.ts +15 -5
  56. package/dist/types/modeler/solvers/z3/solver.d.ts.map +1 -1
  57. package/dist/types/modeler/types.d.ts +59 -1
  58. package/dist/types/modeler/types.d.ts.map +1 -1
  59. package/dist/types/modeler/validation.d.ts +56 -0
  60. package/dist/types/modeler/validation.d.ts.map +1 -0
  61. package/dist/types/modeler/variable.d.ts +48 -14
  62. package/dist/types/modeler/variable.d.ts.map +1 -1
  63. package/docs/examples/diet-sensitivity.md +90 -0
  64. package/docs/examples/ecommerce-coupons.md +106 -0
  65. package/docs/examples/index.md +53 -0
  66. package/docs/examples/knapsack.md +60 -0
  67. package/docs/examples/portfolio-qp.md +83 -0
  68. package/docs/examples/production.md +51 -0
  69. package/docs/examples/shift-scheduling.md +175 -0
  70. package/docs/examples/smt-security.md +74 -0
  71. package/docs/examples/transportation.md +90 -0
  72. package/docs/examples/unsat-core.md +64 -0
  73. package/docs/getting-started/ai-agents.md +267 -0
  74. package/docs/getting-started/cross-origin.md +87 -0
  75. package/docs/getting-started/index.md +116 -0
  76. package/docs/getting-started/runtimes.md +227 -0
  77. package/docs/guide/constraints.md +177 -0
  78. package/docs/guide/expressions.md +204 -0
  79. package/docs/guide/objectives.md +170 -0
  80. package/docs/guide/unsat-core.md +147 -0
  81. package/docs/guide/variables.md +188 -0
  82. package/docs/ja/getting-started/ai-agents.md +266 -0
  83. package/docs/solvers/custom-solver.md +525 -0
  84. package/docs/solvers/glpk.md +66 -0
  85. package/docs/solvers/highs.md +93 -0
  86. package/docs/solvers/index.md +42 -0
  87. package/docs/solvers/z3.md +78 -0
  88. package/docs/zh/getting-started/ai-agents.md +266 -0
  89. package/llms-full.txt +3249 -0
  90. package/llms.txt +197 -0
  91. package/package.json +18 -5
  92. package/skills/washi/SKILL.md +197 -0
  93. package/dist/browser/loader-CVt4CloI.js +0 -58
  94. package/dist/lib/async-mutex-Duo4jwzn.js +0 -195
  95. package/dist/lib/constraint-BRuB3ooC.js +0 -90
  96. package/dist/lib/loader-Dti0v9yC.js +0 -50
  97. package/dist/lib/registry-DksqCSjI.js +0 -32
package/llms-full.txt ADDED
@@ -0,0 +1,3249 @@
1
+ # AI Coding Agent Quick-Reference Guide
2
+
3
+ > **Scope Note**: This document is the **consumer modeling quick-reference** for AI coding agents and developers building optimization or satisfiability models with `@ohtaman/washi`.
4
+ > - **Portable Agent Skill (`SKILL.md`)**: For an actionable end-to-end modeling, `toLpString()` inspection, `validateSolution()` verification, and Z3 `unsatCore` conflict-diagnosis workflow skill (compatible with Claude Code, Antigravity, Cursor, Codex, and other `SKILL.md`-aware agents), use [`skills/washi/SKILL.md`](https://github.com/ohtaman/washi/blob/main/skills/washi/SKILL.md) (also shipped in the npm package at `node_modules/@ohtaman/washi/skills/washi/SKILL.md`):
5
+ > ```bash
6
+ > # Link or copy the packaged skill into your project's agent skills directory
7
+ > mkdir -p .agents/skills/washi
8
+ > cp node_modules/@ohtaman/washi/skills/washi/SKILL.md .agents/skills/washi/SKILL.md
9
+ > ```
10
+ > - **Repository Review Guidelines**: For repository contribution and pull-request review rules, see [`AGENTS.md`](https://github.com/ohtaman/washi/blob/main/AGENTS.md) in the repository root.
11
+
12
+ ---
13
+
14
+ ## 1. Package Imports & Deployment
15
+
16
+ ### Node.js / Bundlers (Vite, Webpack, Next.js, Bun)
17
+
18
+ ```ts
19
+ import {
20
+ Model,
21
+ expr,
22
+ sum,
23
+ dot,
24
+ maximize,
25
+ minimize,
26
+ and,
27
+ or,
28
+ not,
29
+ implies,
30
+ xor,
31
+ setHighsWasmUrl,
32
+ } from "@ohtaman/washi";
33
+
34
+ // Optional solver plugins (require installing peer dependencies first):
35
+ // npm install z3-solver
36
+ import "@ohtaman/washi/solvers/z3";
37
+ // npm install glpk.js
38
+ import "@ohtaman/washi/solvers/glpk";
39
+ ```
40
+
41
+ ### Browser ESM & Classic Script Tags
42
+
43
+ - **Browser ESM**:
44
+ - Core (HiGHS built-in): `import { Model, expr, maximize } from "./washi/washi.js";`
45
+ - GLPK plugin: `import "./washi/solvers/glpk.js";`
46
+ - Z3 plugin: `import "./washi/solvers/z3.js";`
47
+ - **Classic `<script>` / CodePen (IIFE)**:
48
+ - Core: `<script src="./washi/washi.min.js"></script>` (exposes global `Washi`)
49
+ - GLPK plugin: `<script src="./washi/solvers/glpk.min.js"></script>` (registers `"glpk"`, exposes `WashiGLPK`)
50
+ - Z3 plugin: `<script src="./washi/solvers/z3.min.js"></script>` (registers `"z3"`, exposes `WashiZ3`)
51
+
52
+ ### WebAssembly Asset & Runtime Rules
53
+
54
+ 1. **Bundled WASM by default (no implicit CDN fallback)**:
55
+ - In Node.js and modern bundlers, WebAssembly assets resolve relative to the module automatically.
56
+ - In browser deployments, deploy the full `dist/browser/` directory (JS, Worker, and `.wasm` files) together. If a local WASM URL is missing or returns an HTML SPA fallback, initialization fails immediately and reports the attempted URL—it **never** silently switches to a public CDN.
57
+ - In browser runtimes, to load `highs.wasm` from a custom location, call `setHighsWasmUrl("https://.../highs.wasm")` or pass `await m.solve({ wasmUrl: "https://.../highs.wasm" })` (ignored in Node.js, which always loads the local Node `highs` package runtime).
58
+ 2. **Z3 requires a Secure Context, Cross-Origin Isolation, and CORS in browsers**:
59
+ - Z3 WebAssembly uses `SharedArrayBuffer` multithreading. Browser pages running Z3 must run in a secure context (`window.isSecureContext === true`, i.e., HTTPS or `localhost`) and be cross-origin isolated (`window.crossOriginIsolated === true`) by serving:
60
+ - `Cross-Origin-Opener-Policy: same-origin` (on the document)
61
+ - `Cross-Origin-Embedder-Policy: require-corp` (on the document)
62
+ - `Cross-Origin-Resource-Policy: cross-origin` and `Access-Control-Allow-Origin` (on cross-origin script/WASM asset responses)
63
+ - Importing `@ohtaman/washi` never auto-injects a Service Worker. When a secure context or cross-origin isolation is absent in a browser, Z3 rejects immediately with a descriptive error.
64
+ 3. **Web Workers & Cancellation (`AbortSignal`)**:
65
+ - Pre-solve cancellation (`signal.aborted` before execution starts) is supported across all solvers.
66
+ - In browsers where `Worker` is available, HiGHS runs in a dedicated Web Worker by default (`useWorker: true`). Passing `{ signal }` to `m.solve({ signal })` preemptively terminates an in-flight Worker solve (or cancels queued Worker requests).
67
+ - On main-thread HiGHS (`useWorker: false` or in Node.js), cooperative mid-solve cancellation is supported when `abort()` is triggered synchronously inside an `onProgress` callback at a solver interrupt checkpoint; arbitrary asynchronous aborts from outside cannot preempt a blocked synchronous WASM thread until a callback checkpoint or completion. Single-threaded WASM solvers without interrupt hooks (GLPK, Z3) check `signal` before execution starts.
68
+
69
+ ---
70
+
71
+ ## 2. Canonical Modeling Workflow
72
+
73
+ Formulating and solving a model consists of four distinct steps:
74
+
75
+ ### Step 1: Create Model & Decision Variables
76
+
77
+ ```ts
78
+ const m = new Model("ExampleModel");
79
+
80
+ // Scalar variables
81
+ const x = m.continuous("x", { lb: 0, ub: 100 });
82
+ const n = m.integer("n", { lb: 0, ub: 10 });
83
+ const b = m.binary("b");
84
+ const flag = m.bool("flag"); // Propositional boolean (for Z3 / logical constraints)
85
+
86
+ // Indexed variable containers:
87
+ // - Numeric dimension -> Array
88
+ // - Key-array dimension -> Record
89
+ const vec = m.continuous(4, { lb: 0, prefix: "v" }); // Var[] (length 4)
90
+ const grid = m.binary(2, 3, { prefix: "g" }); // Var[][] (2x3)
91
+ const byItem = m.integer(["desk", "chair"] as const, { lb: 0 }); // Record<"desk" | "chair", Var>
92
+ const mixed = m.binary(["A", "B"] as const, 3); // Record<"A" | "B", Var[]>
93
+ ```
94
+
95
+ - **Optional Primal Initial Values (`initialValue?: number`)**:
96
+ - Numeric variable constructors (`m.continuous`, `m.integer`, `m.binary`, and `new Var`) and the mutable `v.initialValue` property accept an optional finite primal hint (validated with tolerance `1e-6` against bounds `[lb, ub]` and integer/binary domains), kept distinct from the solved `v.value`.
97
+ - A start may be complete or partial (e.g., transferred by stable variable names when rebuilding a MIP model). Supplying `initialValue` is a hint only—it does **not** fix the variable, add a hard constraint, or guarantee solver acceptance or a runtime speedup.
98
+ - Supported by **HiGHS** (`supportsWarmStart: true`); solvers without warm-start support (**GLPK**, **Z3**) throw an error if any variable has `initialValue !== undefined`.
99
+
100
+ ### Step 2: Build Expressions (`expr`, `sum`, `dot`)
101
+
102
+ - **Unified `expr` template tag**:
103
+ - Linear expression (`LinExpr`): `` expr`30 * ${x} + 50 * ${n} - 10` ``
104
+ - Quadratic expression (`QuadExpr`): `` expr`${x}^2 + 2 * ${x} * ${n} + ${n}^2` `` (variable-by-variable products and squares are supported for convex QP objectives).
105
+ - Linear constraint (`Constraint`): `` expr`${x} + 2 * ${n} <= 40` `` (supports `<=`, `>=`, `==`, and `=`).
106
+ - **Aggregations**:
107
+ - `sum(iterable, mapper?)` sums an `Iterable` of `Var` / `LinExpr` / `number` (e.g., `Var[]`, `grid.flat()`, or `Object.values(record)`).
108
+ - `dot(a, b)` computes the inner product of matching 1D arrays, 2D arrays (`number[][]` / `Var[][]`), plain records (`Record<string, ...>`), or mixed Array/Record containers in either operand order.
109
+ - **Fluent methods**:
110
+ - Arithmetic (on `LinExpr` and `QuadExpr`; `Var` supports `.add()`, `.sub()`, `.mul()`, `.neg()`): `.add(other)`, `.sub(other)`, `.mul(other)`, `.div(scalar)`, `.neg()`
111
+ - Comparisons (on `Var` and `LinExpr`): `.le(rhs, name?)`, `.ge(rhs, name?)`, `.eq(rhs, name?)`
112
+ - Boolean logic (on `BoolVar`, `BoolExpr`, and `Constraint` for Z3): standalone `and(...)`, `or(...)`, `not(...)`, `implies(a, b)`, `xor(a, b)` and fluent `.and(...)`, `.or(...)`, `.not()`, `.implies(...)`, `.xor(...)`
113
+
114
+ ### Step 3: Register Objective & Constraints on the Model
115
+
116
+ Constructing an `expr` or `.le()` object does **not** attach it to the model until you pass it to `m.constraint(...)` or `m.objective(...)`:
117
+
118
+ ```ts
119
+ // Set objective using maximize() or minimize()
120
+ m.objective(maximize(expr`30 * ${x} + 50 * ${n}`));
121
+
122
+ // Register constraints on the model
123
+ m.constraint(expr`${x} + 2 * ${n} <= 40`, "resource_limit");
124
+
125
+ // Indicator constraint (MIP Big-M reformulation: b === 1 => x <= 50)
126
+ m.indicator(b, expr`${x} <= 50`, { activeValue: 1, name: "cap_when_b" });
127
+ ```
128
+
129
+ ### Step 4: Execute (`solve` or `check`) & Verify (`validateSolution`)
130
+
131
+ - **Optimization (`await m.solve(options?)`)**:
132
+ - Returns `SolverStatus` (`"Optimal"`, `"Feasible"`, `"Infeasible"`, `"Unbounded"`, `"TimeLimit"`, `"IterationLimit"`, `"Error"`, `"Unknown"`, `"NotSolved"`, `"Sat"`, or `"Unsat"`). Built-in optimization solvers return the optimization statuses (`"Optimal"`, `"Feasible"`, `"Infeasible"`, `"Unbounded"`, `"TimeLimit"`, `"IterationLimit"`, `"Error"`, `"Unknown"`, `"NotSolved"`), where a time-, iteration-, or solution-limited run (or an interrupted solve) that found a valid primal incumbent returns `"Feasible"`; `"Sat"` and `"Unsat"` are included in `SolverStatus` for satisfiability/custom solver adapters (such as `MockSolver`).
133
+ - Read values via `x.value`, `expr`...`.value`, `m.getValues(x)`, `m.getValues(container)`, `m.objectiveValue`, `constr.dual`, `constr.slack`, and `x.reducedCost`.
134
+ - Call `m.validateSolution()` after solving to independently verify variable finiteness, variable bounds (`lb`/`ub`), integer/binary/boolean domains, linear constraint residuals, and logical constraints (`SolutionValidationReport`: `{ valid, complete, maxViolation, violations, ... }`). Note that `validateSolution()` checks primal feasibility of variables and constraints; it does not recompute or compare `m.objectiveValue`.
135
+ - **Satisfiability & Unsat Core (`await m.check(options?)`)**:
136
+ - Returns `CheckResult` (`{ status: "Sat" | "Unsat" | "Unknown", values: Record<string, number | boolean>, conflicts?: readonly string[], unsatCore?: readonly string[], runTime?: number }`), where `conflicts` and `unsatCore` are optional and populated only when an unsat-core-capable solver (such as Z3) returns `"Unsat"`.
137
+ - For models containing only linear arithmetic constraints, `m.check()` works with any solver (including the default HiGHS and GLPK) by falling back to a feasibility `solve()`. Logical constraints (`BoolVar`, `and`, `or`, `implies`, etc.) and unsatisfiable-core extraction (`result.conflicts` / `result.unsatCore`) require an SMT-capable solver (`{ solver: "z3" }`). Because Z3 asserts variable bounds (`lb`/`ub`) and binary `{0, 1}` domains directly while tracking registered constraints, `conflicts` / `unsatCore` reports a subset of registered constraint names that conflicts *together with the model's fixed variable bounds/domains* (and is not guaranteed minimal).
138
+ - **No concurrent mutation during async solve/check**:
139
+ - Mutating variables, constraints, or the objective while `await m.solve()` or `await m.check()` is pending invalidates the in-flight execution and rejects the stale promise.
140
+
141
+ ---
142
+
143
+ ## 3. Executable Examples
144
+
145
+ ### Example A: Mixed-Integer Linear Programming (MIP) with Indexed Containers & Solution Validation
146
+
147
+ ```ts
148
+ import { Model, expr, dot, sum, maximize } from "@ohtaman/washi";
149
+
150
+ export async function runMipExample() {
151
+ const m = new Model("ProductionMIP");
152
+ const products = ["desk", "chair"] as const;
153
+ const profit = { desk: 30, chair: 50 };
154
+ const woodPerUnit = { desk: 1, chair: 2 };
155
+ const laborPerUnit = { desk: 2, chair: 1 };
156
+
157
+ const qty = m.integer(products, { lb: 0, ub: 100 });
158
+ const setup = m.binary("setup");
159
+
160
+ m.objective(maximize(expr`${dot(profit, qty)} - 20 * ${setup}`));
161
+ m.constraint(expr`${dot(woodPerUnit, qty)} <= 40`, "wood_cap");
162
+ m.constraint(expr`${dot(laborPerUnit, qty)} <= 50`, "labor_cap");
163
+ m.constraint(expr`${sum(Object.values(qty))} <= 30 * ${setup}`, "require_setup");
164
+
165
+ const status = await m.solve();
166
+ const report = m.validateSolution();
167
+ const values = m.getValues(qty);
168
+
169
+ return {
170
+ status,
171
+ objectiveValue: m.objectiveValue,
172
+ values,
173
+ setup: setup.value,
174
+ valid: report.valid,
175
+ maxViolation: report.maxViolation,
176
+ };
177
+ }
178
+ ```
179
+
180
+ ### Example B: Convex Quadratic Programming (QP)
181
+
182
+ Variable-by-variable products (`${x} * ${y}`) and squares (`${x}^2`) are supported in `expr` for quadratic objectives:
183
+
184
+ ```ts
185
+ import { Model, expr, minimize } from "@ohtaman/washi";
186
+
187
+ export async function runQpExample() {
188
+ const m = new Model("PortfolioQP");
189
+ const x = m.continuous("x", { lb: 0 });
190
+ const y = m.continuous("y", { lb: 0 });
191
+
192
+ // Minimize x^2 + y^2 subject to x + y >= 4
193
+ m.objective(minimize(expr`${x}^2 + ${y}^2`));
194
+ m.constraint(expr`${x} + ${y} >= 4`, "budget");
195
+
196
+ const status = await m.solve();
197
+ const report = m.validateSolution();
198
+
199
+ return {
200
+ status,
201
+ objectiveValue: m.objectiveValue,
202
+ x: x.value,
203
+ y: y.value,
204
+ valid: report.valid,
205
+ };
206
+ }
207
+ ```
208
+
209
+ ### Example C: Z3 Feasibility Check & Unsat Core Diagnosis
210
+
211
+ ```ts
212
+ import { Model, expr, implies } from "@ohtaman/washi";
213
+ import "@ohtaman/washi/solvers/z3";
214
+
215
+ export async function runZ3Example() {
216
+ const m = new Model("PolicyCheck", { solver: "z3" });
217
+ const vip = m.bool("vip");
218
+ const discount = m.integer("discount", { lb: 0, ub: 50 });
219
+
220
+ m.constraint(vip, "customer_is_vip");
221
+ m.constraint(implies(vip, expr`${discount} >= 20`), "vip_min_discount");
222
+ m.constraint(expr`${discount} <= 10`, "campaign_cap");
223
+
224
+ const unsatResult = await m.check();
225
+ return {
226
+ status: unsatResult.status,
227
+ conflicts: unsatResult.conflicts,
228
+ };
229
+ }
230
+ ```
231
+
232
+ ---
233
+
234
+ ## 4. Negative Examples & Common Pitfalls
235
+
236
+ Avoid these invalid patterns and retired aliases:
237
+
238
+ ```ts
239
+ import { Model, expr, maximize } from "@ohtaman/washi";
240
+
241
+ export function verifyNegativePatterns() {
242
+ const m = new Model("NegativeExamples");
243
+ const x = m.continuous("x", { lb: 0 });
244
+ const y = m.continuous("y", { lb: 0 });
245
+
246
+ // 1. DO NOT pass a Constraint to m.objective() or arithmetic methods:
247
+ // WRONG: m.objective(maximize(expr`${x} <= 10` as any)); -> throws Error
248
+ // WRONG: expr`${x} + 1`.add(expr`${y} <= 5` as any); -> throws Error
249
+
250
+ // 2. DO NOT pass an arithmetic expression without (<=, >=, ==) to m.constraint():
251
+ // WRONG: m.constraint(expr`${x} + ${y}` as any); -> throws Error
252
+
253
+ // 3. DO NOT use quadratic terms inside linear constraints:
254
+ // WRONG: expr`${x}^2 <= 10`; -> throws Error ("Quadratic constraints are not supported")
255
+
256
+ // 4. DO NOT use retired aliases (removed in #59 / #81):
257
+ // - Use `expr` instead of retired `c` template tag
258
+ // - Use `m.constraint(...)` instead of `m.constrain(...)` or `m.add(...)`
259
+ // - Use `m.objective(maximize(...))` / `m.objective(minimize(...))` instead of `m.maximize(...)` / `m.minimize(...)` / `m.setObjective(...)`
260
+ // - Use `await m.solve()` instead of `await m.optimize()`
261
+ // - Use `.le()`, `.ge()`, `.eq()` instead of `.lessThanOrEqual()`, `.greaterThanOrEqual()`, `.equalTo()`
262
+ // - Use `.value` or `m.getValues(...)` instead of `expr.getValue()`
263
+ // - Use `m.variables` / `m.constraints` instead of `m.vars` / `m.constrs`
264
+ return { m, x, y, maximize };
265
+ }
266
+ ```
267
+
268
+ ---
269
+
270
+ # Introduction & Installation
271
+
272
+ Welcome to **WaSHI** (WebAssembly-backed, Solver-independent, High-level modeling Interface).
273
+
274
+ WaSHI brings the algebraic modeling style of Python's **PuLP** and Julia's **JuMP** to the TypeScript and JavaScript ecosystem. You can formulate mathematical optimization problems using algebraic expressions and solve them locally using WebAssembly solvers (**HiGHS**, **Z3**, and **GLPK**)—without a separate Python service or native C/C++ build steps.
275
+
276
+ ### Why TypeScript & WebAssembly?
277
+
278
+ - **Removing the deployment bottleneck**: In traditional optimization workflows (e.g., Python with PuLP or Pyomo), turning a model into an interactive application requires provisioning a backend service and managing native C/C++ solver binaries. As AI-assisted development accelerates UI and application prototyping, backend infrastructure often becomes the primary bottleneck to shipping. Running both the modeler and WebAssembly solvers on standard Web technologies allows an optimization tool to be deployed as static assets (e.g., on GitHub Pages or Cloudflare Pages, serving the generated JS, Worker, and WASM files; with COOP/COEP headers when using Z3)—and makes it straightforward to pair mathematical optimization with in-browser LLMs (e.g., WebGPU / WebLLM) entirely on the client.
279
+ - **Algebraic syntax without operator overloading (`expr`)**: Because JavaScript lacks operator overloading, libraries usually trade off between verbose method chaining (`x.mul(30).add(y.mul(50))`) and untyped strings (`"30 * x + 50 * y"`). WaSHI uses tagged template literals (`` expr`30 * ${x} + 50 * ${y} <= 40` ``) similar to parameterized SQL queries—parsing infix operators while binding `${x}` directly as typed TypeScript object references for static type checking, IDE symbol renaming, and multi-dimensional indexing (`${flow[i][j]}`).
280
+
281
+ ---
282
+
283
+ ## 📦 Installation
284
+
285
+ Install WaSHI using your preferred package manager:
286
+
287
+ ```bash [npm]
288
+ npm install @ohtaman/washi
289
+ ```
290
+
291
+ ```bash [pnpm]
292
+ pnpm add @ohtaman/washi
293
+ ```
294
+
295
+ ```bash [yarn]
296
+ yarn add @ohtaman/washi
297
+ ```
298
+
299
+ ```bash [bun]
300
+ bun add @ohtaman/washi
301
+ ```
302
+
303
+ ### Optional Solver Dependencies
304
+
305
+ WaSHI ships with **HiGHS** built-in with zero configuration. If you need SMT logic / Unsat Core diagnosis via **Z3**, or compatibility with **GLPK**, install their respective peer dependencies:
306
+
307
+ ```bash
308
+ # For SMT logic and Unsat Core conflict diagnosis
309
+ npm install z3-solver
310
+
311
+ # For GLPK solver (GPL-3.0)
312
+ npm install glpk.js
313
+ ```
314
+
315
+ ---
316
+
317
+ ## 🚀 Quickstart Walkthrough
318
+
319
+ Here is a step-by-step example of solving a production allocation problem.
320
+
321
+ ### 1. Import the Core Classes
322
+
323
+ ```typescript
324
+ import { Model, expr, maximize } from "@ohtaman/washi";
325
+ ```
326
+
327
+ ### 2. Instantiate a Model
328
+
329
+ ```typescript
330
+ const model = new Model("WoodworkingProfit");
331
+ ```
332
+
333
+ ### 3. Add Decision Variables
334
+
335
+ Variables represent the unknowns you want to determine. Factory methods (`m.continuous`, `m.integer`, `m.binary`) clearly define their domain:
336
+
337
+ ```typescript
338
+ const x = model.integer("x", { lb: 0 });
339
+ const y = model.integer("y", { lb: 0 });
340
+ ```
341
+
342
+ ### 4. Set the Objective Function
343
+
344
+ Use the `expr` tagged template literal along with `maximize(...)` or `minimize(...)` to define the goal:
345
+
346
+ ```typescript
347
+ // Maximize 30*x + 50*y
348
+ model.objective(maximize(expr`30 * ${x} + 50 * ${y}`));
349
+ ```
350
+
351
+ ### 5. Add Constraints
352
+
353
+ Use `model.constraint()` with the `expr` tagged template to formulate equality and inequality constraints:
354
+
355
+ ```typescript
356
+ model.constraint(expr`${x} + 2 * ${y} <= 40`, "wood_capacity");
357
+ model.constraint(expr`2 * ${x} + ${y} <= 50`, "labor_capacity");
358
+ ```
359
+
360
+ ### 6. Solve and Extract Results
361
+
362
+ Solve the model asynchronously via WebAssembly (HiGHS by default):
363
+
364
+ ```typescript
365
+ const status = await model.solve();
366
+
367
+ console.log("Status:", status); // "Optimal"
368
+ console.log("Optimal Objective Value:", model.objectiveValue); // 1100
369
+ console.log(`Solution: x = ${x.value}, y = ${y.value}`); // x = 20, y = 10
370
+ ```
371
+
372
+ ---
373
+
374
+ ## 🧭 Next Steps
375
+
376
+ - Give AI coding agents a concise API & deployment reference with the [AI Coding Agent Quick-Reference Guide](docs/getting-started/ai-agents.md).
377
+ - Learn how to run WaSHI across different environments in [Runtime Environments](docs/getting-started/runtimes.md).
378
+ - Understand how to formulate complex expressions in the [Modeling Guide](docs/guide/expressions.md).
379
+ - Explore built-in and pluggable solvers in the [Solvers Matrix](docs/solvers/index.md).
380
+ - Browse 9 runnable optimization and SMT recipes in [Examples & API Finder](docs/examples/index.md).
381
+
382
+ ---
383
+
384
+ # Runtime Environments & Deployment
385
+
386
+ WaSHI runs across JavaScript and TypeScript runtimes—from Node.js, Bun, and Deno to modern bundlers, vanilla browser `<script>` tags, and air-gapped intranets—without native C/C++ compilation.
387
+
388
+ ---
389
+
390
+ ## 🟢 Node.js
391
+
392
+ WaSHI supports Node.js (v18+) out of the box with standard ECMAScript Modules (ESM). The built-in HiGHS WebAssembly binary is loaded directly from the installed package.
393
+
394
+ ```typescript
395
+ import { Model, expr, maximize } from "@ohtaman/washi";
396
+
397
+ const m = new Model("NodeExample");
398
+ const x = m.continuous("x", { lb: 0, ub: 10 });
399
+ const y = m.continuous("y", { lb: 0, ub: 20 });
400
+
401
+ m.objective(maximize(expr`2 * ${x} + 3 * ${y}`));
402
+ m.constraint(expr`${x} + ${y} <= 15`, "capacity");
403
+
404
+ await m.solve();
405
+ console.log(`Status: ${m.status}, Objective: ${m.objectiveValue}`);
406
+ console.log(`x = ${x.value}, y = ${y.value}`);
407
+ ```
408
+
409
+ Run directly with `tsx` or `node`:
410
+
411
+ ```bash
412
+ npx tsx app.ts
413
+ ```
414
+
415
+ ---
416
+
417
+ ## 🌐 Bundlers (Vite, Webpack, Rollup, Next.js)
418
+
419
+ ### Modern Bundlers (Vite / Rollup / Webpack)
420
+
421
+ When importing `@ohtaman/washi` in a browser bundler, the `browser` export condition resolves automatically:
422
+
423
+ ```typescript
424
+ import { Model, expr, maximize } from "@ohtaman/washi";
425
+
426
+ export async function runOptimization() {
427
+ const m = new Model({ solver: "highs" });
428
+ const x = m.continuous("x", { lb: 0, ub: 100 });
429
+ m.objective(maximize(x));
430
+ await m.solve();
431
+ return x.value;
432
+ }
433
+ ```
434
+
435
+ #### Background Web Worker Execution (Default)
436
+ In browsers, HiGHS automatically runs inside a dedicated **Web Worker** so long-running LP/MIP/QP solves never freeze the main UI thread, animations, or user input, while enabling real-time progress events (`onProgress`) and cancellation (`AbortSignal`).
437
+
438
+ If your environment prohibits Web Workers or you explicitly want synchronous main-thread execution, pass `useWorker: false`:
439
+
440
+ ```typescript
441
+ await m.solve({ useWorker: false });
442
+ ```
443
+
444
+ #### Customizing the HiGHS WASM Asset URL
445
+ By default, WaSHI resolves `highs.wasm` relative to the bundle (`new URL("./highs.wasm", import.meta.url)`). It **never** makes implicit requests to external CDNs (`jsdelivr`, `unpkg`, etc.).
446
+
447
+ If your bundler or static server hosts `.wasm` files under a custom path, configure the URL globally or per solve:
448
+
449
+ ```typescript
450
+ import { setHighsWasmUrl } from "@ohtaman/washi";
451
+
452
+ // Option A: Configure globally before solving
453
+ setHighsWasmUrl("/assets/wasm/highs.wasm");
454
+
455
+ // Option B: Configure per solve call
456
+ await m.solve({ wasmUrl: "/assets/wasm/highs.wasm" });
457
+ ```
458
+
459
+ ### Next.js & Server-Side Rendering (SSR)
460
+
461
+ In Next.js App Router or Pages Router, optimization models can run either in Server Actions / API Routes (Node.js runtime) or on the client inside `"use client"` components:
462
+
463
+ ```tsx
464
+ "use client";
465
+
466
+ import { useState } from "react";
467
+ import { Model, maximize } from "@ohtaman/washi";
468
+
469
+ export default function OptimizerComponent() {
470
+ const [val, setVal] = useState<number | null>(null);
471
+
472
+ const handleSolve = async () => {
473
+ const m = new Model();
474
+ const x = m.continuous("x", { lb: 0, ub: 50 });
475
+ m.objective(maximize(x));
476
+ await m.solve();
477
+ setVal(x.value);
478
+ };
479
+
480
+ return (
481
+ <div>
482
+ <button onClick={handleSolve}>Run Optimization</button>
483
+ {val !== null && <p>Result: {val}</p>}
484
+ </div>
485
+ );
486
+ }
487
+ ```
488
+
489
+ ---
490
+
491
+ ## 📦 Browser ESM (`<script type="module">`)
492
+
493
+ You can import the pre-bundled browser ESM modules directly from a local static server (or from an npm CDN once a compatible release is published) without a build step:
494
+
495
+ ```html
496
+ <script type="module">
497
+ // Serve from your local static server / dist:
498
+ import { Model, expr, maximize } from "./dist/browser/washi.js";
499
+ // Or from an npm CDN once a compatible release is published:
500
+ // import { Model, expr, maximize } from "https://cdn.jsdelivr.net/npm/@ohtaman/washi@<version>/dist/browser/washi.js";
501
+ // Optional solver plugins:
502
+ // import "./dist/browser/solvers/glpk.js";
503
+ // import "./dist/browser/solvers/z3.js";
504
+
505
+ const m = new Model("BrowserESM");
506
+ const x = m.integer("x", { lb: 0 });
507
+ const y = m.integer("y", { lb: 0 });
508
+
509
+ m.objective(maximize(expr`30 * ${x} + 50 * ${y}`));
510
+ m.constraint(expr`${x} + 2 * ${y} <= 40`, "wood");
511
+ m.constraint(expr`2 * ${x} + ${y} <= 50`, "labor");
512
+
513
+ await m.solve();
514
+ console.log("Optimal Objective:", m.objectiveValue);
515
+ </script>
516
+ ```
517
+
518
+ ---
519
+
520
+ ## 🟨 Classic `<script>` Tags (IIFE / Standalone Build / CDN)
521
+
522
+ For static HTML pages, CodePen, or JSFiddle, load `washi.min.js` via a standard `<script>` tag. All exports are attached to the global `window.Washi` object:
523
+
524
+ ```html
525
+ <!DOCTYPE html>
526
+ <html>
527
+ <head>
528
+ <meta charset="utf-8" />
529
+ <title>WaSHI Browser Demo</title>
530
+ </head>
531
+ <body>
532
+ <h1>WaSHI Browser Optimization</h1>
533
+ <pre id="output">Solving...</pre>
534
+
535
+ <!-- Core WaSHI + HiGHS (MIT) from local static build / assets -->
536
+ <script src="./dist/browser/washi.min.js"></script>
537
+ <!-- Or via CDN once a compatible release is published: -->
538
+ <!-- <script src="https://cdn.jsdelivr.net/npm/@ohtaman/washi@<version>/dist/browser/washi.min.js"></script> -->
539
+ <!-- Optional plugins (auto-register on window.Washi): -->
540
+ <!-- <script src="./dist/browser/solvers/glpk.min.js"></script> -->
541
+ <!-- <script src="./dist/browser/solvers/z3.min.js"></script> -->
542
+
543
+ <script>
544
+ (async () => {
545
+ const { Model, expr, maximize } = Washi;
546
+
547
+ const m = new Model("BrowserDemo");
548
+ const x = m.continuous("x", { lb: 0, ub: 10 });
549
+ const y = m.continuous("y", { lb: 0, ub: 20 });
550
+
551
+ m.objective(maximize(expr`2 * ${x} + 3 * ${y}`));
552
+ m.constraint(expr`${x} + ${y} <= 15`);
553
+
554
+ await m.solve();
555
+
556
+ document.getElementById("output").textContent =
557
+ `Status: ${m.status}\n` +
558
+ `Objective: ${m.objectiveValue}\n` +
559
+ `x = ${x.value}, y = ${y.value}`;
560
+ })();
561
+ </script>
562
+ </body>
563
+ </html>
564
+ ```
565
+
566
+ > [!NOTE]
567
+ > When loading `washi.min.js` from a cross-origin CDN (such as jsDelivr or unpkg), browsers block direct `new Worker("https://cdn...")` construction due to the same-origin policy. WaSHI automatically bridges cross-origin workers via a lightweight `Blob` wrapper using `importScripts()`, so Web Worker execution works out of the box from CDNs.
568
+
569
+ ---
570
+
571
+ ## 🔒 Offline, Intranet & Strict CSP Environments
572
+
573
+ WaSHI never phones home or falls back to public CDNs. To self-host in an air-gapped intranet or under a strict Content Security Policy (CSP):
574
+
575
+ 1. **Copy Static Assets**: Serve the files from `node_modules/@ohtaman/washi/dist/browser/` on your own origin:
576
+ - `washi.js` / `washi.min.js`, `highs.wasm`, and `highs-worker.js`
577
+ - Optional GLPK: `solvers/glpk.js` / `solvers/glpk.min.js`
578
+ - Optional Z3: `solvers/z3.js` / `solvers/z3.min.js`, `solvers/z3-built.js`, `solvers/z3-built.wasm`
579
+ 2. **Content Security Policy (CSP) Headers**:
580
+ - Allow WebAssembly compilation (`'wasm-unsafe-eval'` in `script-src`).
581
+ - If using the default Web Worker or cross-origin Blob worker fallback, allow `worker-src 'self' blob:;`.
582
+ - If `blob:` workers are prohibited by policy, either serve `highs-worker.js` from `'self'` or run on the main thread with `await m.solve({ useWorker: false })`.
583
+ 3. **Cross-Origin Isolation (Z3 Only)**:
584
+ - HiGHS and GLPK run in standard browser contexts without special headers.
585
+ - Z3 WASM uses pthreads + `SharedArrayBuffer` and requires `Cross-Origin-Opener-Policy: same-origin` and `Cross-Origin-Embedder-Policy: require-corp` (or `credentialless`). See the [Cross-Origin Isolation Guide](docs/getting-started/cross-origin.md) for setup details.
586
+
587
+ ---
588
+
589
+ ## ⚡ Bun & Deno
590
+
591
+ ### Bun
592
+
593
+ ```bash
594
+ bun add @ohtaman/washi
595
+ bun run app.ts
596
+ ```
597
+
598
+ ### Deno
599
+
600
+ In Deno (v1.30+ with `npm:` specifiers):
601
+
602
+ ```typescript
603
+ import { Model, maximize } from "npm:@ohtaman/washi";
604
+
605
+ const m = new Model();
606
+ const x = m.continuous("x", { lb: 0, ub: 10 });
607
+ m.objective(maximize(x));
608
+ await m.solve();
609
+ console.log("x =", x.value);
610
+ ```
611
+
612
+ ---
613
+
614
+ # Cross-Origin Isolation (Z3 & Browsers)
615
+
616
+ When running the **Z3** solver engine inside a web browser, special HTTP headers are required due to browser security models.
617
+
618
+ ---
619
+
620
+ ## Why is Cross-Origin Isolation Required?
621
+
622
+ The Z3 WebAssembly engine utilizes multithreading backed by `SharedArrayBuffer` and Web Workers.
623
+
624
+ In modern browsers (Chrome, Firefox, Safari, Edge), `SharedArrayBuffer` is disabled by default to mitigate Spectre-style timing side-channel attacks. To re-enable it, the hosting page must be **Cross-Origin Isolated**.
625
+
626
+ ---
627
+
628
+ ## Required HTTP Response Headers
629
+
630
+ To establish cross-origin isolation, your web server must serve the top-level HTML document with the following HTTP headers:
631
+
632
+ ```http
633
+ Cross-Origin-Opener-Policy: same-origin
634
+ Cross-Origin-Embedder-Policy: require-corp
635
+ ```
636
+
637
+ In addition, any cross-origin assets (e.g. scripts or WASM files loaded from external domains) must send:
638
+
639
+ ```http
640
+ Cross-Origin-Resource-Policy: cross-origin
641
+ ```
642
+
643
+ ---
644
+
645
+ ## Solutions by Hosting Environment
646
+
647
+ ### 1. Custom Node.js / Express Server
648
+
649
+ ```javascript
650
+ app.use((req, res, next) => {
651
+ res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
652
+ res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
653
+ next();
654
+ });
655
+ ```
656
+
657
+ ### 2. Next.js (`next.config.js`)
658
+
659
+ ```javascript
660
+ /** @type {import('next').NextConfig} */
661
+ const nextConfig = {
662
+ async headers() {
663
+ return [
664
+ {
665
+ source: "/(.*)",
666
+ headers: [
667
+ { key: "Cross-Origin-Opener-Policy", value: "same-origin" },
668
+ { key: "Cross-Origin-Embedder-Policy", value: "require-corp" },
669
+ ],
670
+ },
671
+ ];
672
+ },
673
+ };
674
+
675
+ module.exports = nextConfig;
676
+ ```
677
+
678
+ ### 3. Static Hosting without Header Control (e.g., GitHub Pages)
679
+
680
+ Platforms like GitHub Pages or preview environments do not allow customizing response headers directly.
681
+
682
+ For these environments, you can use **`coi-serviceworker`** (a lightweight Service Worker that injects isolation headers on the client side):
683
+
684
+ 1. Place `coi-serviceworker.js` in your website root.
685
+ 2. In your HTML `<head>`, include:
686
+
687
+ ```html
688
+ <script src="/coi-serviceworker.js"></script>
689
+ ```
690
+
691
+ When a user visits for the first time, the service worker registers and reloads the page once to establish `window.crossOriginIsolated === true`.
692
+
693
+ ---
694
+
695
+ ## Graceful Degradation in WaSHI
696
+
697
+ WaSHI's Z3 solver adapter automatically inspects the environment at runtime. If Z3 is invoked in a browser lacking cross-origin isolation, it immediately rejects with a clear, informative error message explaining that COOP/COEP headers or a service worker are required, rather than hanging silently.
698
+
699
+ > [!TIP]
700
+ > The default **HiGHS** solver engine does not require cross-origin isolation or `SharedArrayBuffer`. It works out of the box in standard browser contexts.
701
+
702
+ ---
703
+
704
+ # Variables & Domains
705
+
706
+ Variables represent the mathematical decision variables in your optimization model.
707
+ WaSHI provides domain-specific factory methods on the `Model` instance: `m.continuous()`, `m.integer()`, `m.binary()`, and `m.bool()`.
708
+
709
+ ---
710
+
711
+ ## Decision Variable Types
712
+
713
+ ### 1. Continuous Variables (`m.continuous`)
714
+ Can take any real number value within `[lb, ub]`. Ideal for physical measurements, flow rates, budgets, and weights:
715
+
716
+ ```typescript
717
+ import { Model } from "@ohtaman/washi";
718
+
719
+ const m = new Model();
720
+
721
+ // Single continuous variable bounded in [0, 1000]
722
+ const fuel = m.continuous("fuel", { lb: 0, ub: 1000 });
723
+ ```
724
+
725
+ ### 2. Integer Variables (`m.integer`)
726
+ Restricted to integers. Ideal for countable assets (machines, trucks, staff count):
727
+
728
+ ```typescript
729
+ // Single integer variable bounded in [0, 15]
730
+ const trucks = m.integer("trucks", { lb: 0, ub: 15 });
731
+ ```
732
+
733
+ ### 3. Binary Variables (`m.binary`)
734
+ Restricted to $\{0, 1\}$ as mathematical 0-1 values. Ideal for MIP selection, yes/no decisions, and facility selections:
735
+
736
+ ```typescript
737
+ // Single binary variable
738
+ const openWarehouse = m.binary("open_warehouse");
739
+ // lb is automatically 0, ub is automatically 1
740
+ ```
741
+
742
+ ### 4. Propositional Boolean Variables (`m.bool`)
743
+ Pure boolean logic propositions for SMT / SAT modeling with Z3:
744
+
745
+ ```typescript
746
+ // Single propositional boolean variable
747
+ const isNight = m.bool("isNight");
748
+ const alarmTriggered = m.bool("alarmTriggered");
749
+
750
+ // First-class boolean operators: and, or, not, implies, xor
751
+ m.constraint(alarmTriggered.implies(isNight), "alarm_policy");
752
+ ```
753
+
754
+ ### 💡 Modeling Semi-Continuous Decisions
755
+ Semi-continuous variables take values in $\{0\} \cup [L, U]$ (either zero or between a positive lower and upper bound, useful for minimum production batches or operational thresholds).
756
+
757
+ While not a primitive `VarType` in `Model`, semi-continuous behavior is cleanly modeled by combining a continuous variable with a binary indicator variable and linking constraints ($L \cdot b \le x \le U \cdot b$):
758
+
759
+ ```typescript
760
+ import { Model, expr } from "@ohtaman/washi";
761
+
762
+ const m = new Model();
763
+ const batch = m.continuous("batch", { lb: 0, ub: 100 });
764
+ const isActive = m.binary("is_active");
765
+
766
+ // If isActive === 0, batch is forced to 0; if 1, batch is within [10, 100]
767
+ m.constraint(expr`${batch} >= 10 * ${isActive}`, "min_batch_run");
768
+ m.constraint(expr`${batch} <= 100 * ${isActive}`, "max_batch_run");
769
+ ```
770
+
771
+ ---
772
+
773
+ ## Multi-Dimensional & Array Creation
774
+
775
+ The domain factory methods (`continuous`, `integer`, `binary`, `bool`) support count, arrays of keys, and row/column keys to create 1D arrays, typed dictionaries, and 2D matrices in a single call.
776
+
777
+ ### By Count
778
+ Pass a number to create an array of variables:
779
+
780
+ ```typescript
781
+ const x = m.continuous(5, { lb: 0 }); // Var[] (x_0, ..., x_4)
782
+ const flags = m.bool(3); // BoolVar[] (p_0, p_1, p_2)
783
+ ```
784
+
785
+ ### 1D Variable Dictionaries
786
+ Pass an array of keys to get a typed dictionary `Record<string, Var>` or `Record<string, BoolVar>`:
787
+
788
+ ```typescript
789
+ const items = ["laptop", "phone", "tablet"];
790
+
791
+ // Record<"laptop" | "phone" | "tablet", Var>
792
+ const selected = m.binary(items);
793
+
794
+ const perms = m.bool(["read", "write", "execute"]);
795
+ // perms.read, perms.write, perms.execute
796
+ ```
797
+
798
+ ### 2D Variable Matrices & Grids
799
+ Each dimension follows a consistent rule: passing a numeric count (`number`) produces an `Array`, and passing a collection of keys produces a `Record`:
800
+ - `(rowsCount, colsCount)` $\rightarrow$ `Var[][]` (or `BoolVar[][]`)
801
+ - `(rowKeys, colKeys)` $\rightarrow$ `Record<R, Record<C, Var>>` (or `Record<R, Record<C, BoolVar>>`)
802
+ - `(rowsCount, colKeys)` $\rightarrow$ `Array<Record<C, Var>>`
803
+ - `(rowKeys, colsCount)` $\rightarrow$ `Record<R, Var[]>`
804
+
805
+ ```typescript
806
+ const factories = ["Tokyo", "Osaka"];
807
+ const stores = ["StoreA", "StoreB", "StoreC"];
808
+
809
+ // 1. Named keys x Named keys -> Record<string, Record<string, Var>>
810
+ const ship = m.continuous(factories, stores, { lb: 0 });
811
+ // ship.Tokyo.StoreA
812
+
813
+ // 2. Numeric count x Numeric count -> Var[][] (2D Array)
814
+ const board = m.binary(8, 8, { prefix: "queen" });
815
+ // board[0][0], board.map(...)
816
+
817
+ // 3. Mixed dimensions: Named keys x Numeric count -> Record<string, Var[]>
818
+ const schedule = m.binary(factories, 7, { prefix: "open_day" });
819
+ // schedule.Tokyo[0]
820
+ ```
821
+
822
+ ---
823
+
824
+ ## Variable Options
825
+
826
+ | Option | Type | Default | Description |
827
+ | :--- | :--- | :--- | :--- |
828
+ | `lb` | `number` | `0` | Lower bound. Defaults to 0 (non-negative). |
829
+ | `ub` | `number` | `Infinity` (or `1` for binary) | Upper bound. |
830
+ | `type` | `"Continuous"` \| `"Integer"` \| `"Binary"` | Inferred from method | Underlying variable domain category. |
831
+ | `initialValue` | `number` | `undefined` | Optional primal solution hint (MIP start), kept distinct from solved `variable.value`. |
832
+
833
+ ---
834
+
835
+ ## Primal Initial Values (Warm-Start Hints)
836
+
837
+ When rebuilding a model across iterations with stable variable names, you can supply optional primal initial values (`initialValue`) on a subset or all numeric variables (`m.continuous`, `m.integer`, `m.binary`, or `v.initialValue = ...`):
838
+
839
+ ```typescript
840
+ const prevValues: Record<string, number | null> = { trucks: 8, open_warehouse: 1 };
841
+
842
+ const m2 = new Model("RebuiltMIP");
843
+ const trucks2 = m2.integer("trucks", {
844
+ lb: 0,
845
+ ub: 15,
846
+ initialValue: prevValues.trucks ?? undefined,
847
+ });
848
+ const openWarehouse2 = m2.binary("open_warehouse", {
849
+ initialValue: prevValues.open_warehouse ?? undefined,
850
+ });
851
+ const extraCapacity = m2.continuous("extra_capacity", { lb: 0 }); // Partial start: no initialValue needed
852
+ ```
853
+
854
+ - **Hint vs. Hard Constraint**: `initialValue` is strictly an optional solver hint (passed to HiGHS via `setSolution`). It does **not** fix the variable, add a constraint, or guarantee either solver acceptance or a runtime speedup. It remains separate from `variable.value` (which is `null` until solved and populated with the solver's primal solution).
855
+ - **Validation & Tolerance (`1e-6`)**: Supplied values must be finite numbers within `[lb - 1e-6, ub + 1e-6]` (and within `1e-6` of an integer inside bounds for `"Integer"` and `"Binary"` variables). Partial starts are not required to satisfy every model constraint.
856
+ - **Solver Support**: Supported by **HiGHS** (`capabilities.supportsWarmStart === true`). Solvers without warm-start support (**GLPK**, **Z3**, or custom adapters without `supportsWarmStart: true`) reject `solve()` and `check()` with a descriptive error rather than silently ignoring `initialValue`.
857
+
858
+ ---
859
+
860
+ ## Accessing Solution Values & Reduced Costs
861
+
862
+ After calling `await m.solve()` or `await m.check()`, assigned variable values (optimal or incumbent primal values from `solve()`, or a satisfying assignment from `check()`) are accessible via `variable.value` (returns `number | null` for `Var`, or `boolean | null` for `BoolVar`) or the bulk accessor `m.getValues()`:
863
+
864
+ ```typescript
865
+ await m.solve();
866
+
867
+ // Single variable primal value
868
+ console.log(`trucks: ${trucks.value}`);
869
+
870
+ // Reduced cost (LP sensitivity analysis — how much an objective coefficient must improve before a variable at its bound enters the basis)
871
+ console.log(`fuel reduced cost: ${fuel.reducedCost}`);
872
+
873
+ // Bulk extraction for 1D or 2D collections (works for both Var and BoolVar collections)
874
+ const selectedValues = m.getValues(selected);
875
+ // { laptop: 1, phone: 0, tablet: 1 }
876
+
877
+ const flowValues = m.getValues(ship);
878
+ // { Tokyo: { StoreA: 50, ... }, Osaka: { ... } }
879
+ ```
880
+
881
+ ---
882
+
883
+ ## Looking Up Variables by Name
884
+
885
+ You can retrieve registered decision variables by name or inspect the total variable count on the model:
886
+
887
+ ```typescript
888
+ const v = m.getVarByName("trucks"); // Var | undefined
889
+ const b = m.getBoolVarByName("isNight"); // BoolVar | undefined
890
+ console.log("Total numeric variables:", m.numVars);
891
+ ```
892
+
893
+ ---
894
+
895
+ # Expressions (`expr`, `sum`, `dot`)
896
+
897
+ Mathematical expressions are linear or quadratic combinations of decision variables and numeric coefficients.
898
+
899
+ 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.
900
+
901
+ | Approach | Example | Infix readability | Type safety & IDE rename | Indexed variables (`flow[i][j]`) |
902
+ | :--- | :--- | :--- | :--- | :--- |
903
+ | Method chaining | `x.mul(30).add(y.mul(50)).le(40)` | Low | Yes | Yes |
904
+ | Raw string parsing | `parseConstraint("30 * x + 50 * y <= 40", resolver)` | High | No (runtime string lookup) | Fragile (string concatenation) |
905
+ | **Tagged template (`expr`)** | `` expr`30 * ${x} + 50 * ${y} <= 40` `` | **High** | **Yes (actual TS references)** | **Yes (bound by object reference)** |
906
+
907
+ ---
908
+
909
+ ## ✍️ Tagged Template Literal (`expr`)
910
+
911
+ 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.
912
+
913
+ Use `expr` for both arithmetic expressions (`LinExpr | QuadExpr`) and relational constraints (`Constraint`):
914
+
915
+ ```typescript
916
+ import { Model, expr } from "@ohtaman/washi";
917
+
918
+ const m = new Model();
919
+ const x = m.continuous("x", { lb: 0 });
920
+ const y = m.continuous("y", { lb: 0 });
921
+
922
+ // 1. Linear combination: 3*x + 5*y - 10
923
+ const linearExpr = expr`3 * ${x} + 5 * ${y} - 10`;
924
+
925
+ // 2. Quadratic expression: x * y + 2 * x^2
926
+ const quadExpr = expr`${x} * ${y} + 2 * ${x} * ${x}`;
927
+
928
+ // 3. Relational constraint:
929
+ const balanceConstraint = expr`${x} + 2 * ${y} <= 40`;
930
+ ```
931
+
932
+ ### Supported Syntax in `expr`
933
+ - **Addition & Subtraction**: `${x} + ${y} - 5`
934
+ - **Scalar Multiplication**: `4 * ${x}`, `${x} * 4`
935
+ - **Variable Multiplication (Quadratic terms)**: `${x} * ${y}`, `2 * ${x} * ${x}`
936
+ - **Powers & Exponentiation**: `${x}^2`, `${x}**2`
937
+ - **Unary Negation**: `-${x} + 10`
938
+ - **Parentheses**: `2 * (${x} + ${y})`
939
+ - **Nested Expressions**: `expr` seamlessly interpolates variables, numbers, `sum()` results, and nested expressions.
940
+
941
+ > [!NOTE]
942
+ > For relational comparisons (`<=`, `>=`, `==`), `expr` (e.g., ``expr`${x} + 2 * ${y} <= 40` ``) returns a first-class `Constraint`.
943
+ > 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.
944
+
945
+ ---
946
+
947
+ ## ⚡ High-Performance Summation (`sum`)
948
+
949
+ For iterating over large collections of variables or indices:
950
+
951
+ ```typescript
952
+ import { sum } from "@ohtaman/washi";
953
+
954
+ const items = ["A", "B", "C"];
955
+ const weights = { A: 10, B: 20, C: 30 };
956
+ const x = m.binary(items);
957
+
958
+ // 1. Transform elements with a mapping callback:
959
+ const totalWeight = sum(items, (item) => x[item].mul(weights[item]));
960
+
961
+ // 2. Direct array summation:
962
+ const count = sum(Object.values(x));
963
+ ```
964
+
965
+ `sum()` runs in $O(N)$ linear time without AST overhead.
966
+
967
+ ---
968
+
969
+ ## 🎯 Dot Product (`dot`)
970
+
971
+ 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)`).
972
+
973
+ Supported 1D and 2D container shapes:
974
+ - **1D Arrays**: `number[]` $\times$ `(Var | LinExpr)[]`
975
+ - **1D Records**: `Record<K, number>` $\times$ `Record<K, Var | LinExpr>`
976
+ - **2D Arrays**: `number[][]` $\times$ `(Var | LinExpr)[][]`
977
+ - **2D Records**: `Record<R, Record<C, number>>` $\times$ `Record<R, Record<C, Var | LinExpr>>`
978
+ - **2D Mixed**: `Array<Record<C, ...>>` or `Record<R, Array<...>>`
979
+
980
+ Mismatched array lengths, missing dictionary keys, or non-finite/non-linear leaves throw an explicit error indicating the offending path (e.g., `at '[0].StoreB'`).
981
+
982
+ ### 1D Vector Dot Product
983
+ ```typescript
984
+ import { dot } from "@ohtaman/washi";
985
+
986
+ const profits = { desk: 30, chair: 50 };
987
+ const vars = m.integer(["desk", "chair"]);
988
+
989
+ // Computes 30 * vars.desk + 50 * vars.chair
990
+ const totalProfit = dot(profits, vars);
991
+ ```
992
+
993
+ ### 2D Matrix Dot Product
994
+ Works directly with 2D arrays (`m.continuous(rows, cols)`), nested dictionaries (`m.continuous(rowKeys, colKeys)`), or mixed 2D containers:
995
+
996
+ ```typescript
997
+ const factories = ["Tokyo", "Osaka"];
998
+ const stores = ["StoreA", "StoreB"];
999
+
1000
+ const costs = {
1001
+ Tokyo: { StoreA: 4, StoreB: 7 },
1002
+ Osaka: { StoreA: 8, StoreB: 3 },
1003
+ };
1004
+
1005
+ const ship = m.continuous(factories, stores);
1006
+
1007
+ // Total shipping cost in one line:
1008
+ m.objective(minimize(dot(costs, ship)));
1009
+ ```
1010
+
1011
+ ---
1012
+
1013
+ ## 🧮 Quadratic Expressions (`QuadExpr` & `expr`)
1014
+
1015
+ 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()`:
1016
+
1017
+ ### 1. Using `expr` (Recommended)
1018
+ You can write quadratic formulas naturally with `*`, `^2`, or `**2`:
1019
+
1020
+ ```typescript
1021
+ import { Model, expr, minimize } from "@ohtaman/washi";
1022
+
1023
+ const m = new Model();
1024
+ const x = m.continuous("x", { lb: 0 });
1025
+ const y = m.continuous("y", { lb: 0 });
1026
+
1027
+ // Quadratic objective using expr:
1028
+ m.objective(minimize(expr`3 * ${x} + ${x}^2 + ${y}^2`));
1029
+
1030
+ // Linear constraint using expr:
1031
+ m.constraint(expr`${x} + ${y} >= 4`);
1032
+ ```
1033
+
1034
+ ### 2. Method Chaining with `.mul()` or `QuadExpr`
1035
+ Alternatively, you can construct quadratic expressions programmatically:
1036
+
1037
+ ```typescript
1038
+ import { Model, QuadExpr, minimize } from "@ohtaman/washi";
1039
+
1040
+ const m = new Model();
1041
+ const x = m.continuous("x", { lb: 0 });
1042
+ const y = m.continuous("y", { lb: 0 });
1043
+
1044
+ // 1. Construct quadratic terms with .mul()
1045
+ // Represents x * y + 2 * x^2
1046
+ const quad = x.mul(y).add(x.mul(x).mul(2));
1047
+
1048
+ // 2. Build portfolio risk objective: w^T * Sigma * w
1049
+ const assets = ["Tech", "Bonds"];
1050
+ const w = m.continuous(assets, { lb: 0, ub: 1 });
1051
+
1052
+ let risk = new QuadExpr();
1053
+ risk = risk.add(w.Tech.mul(w.Tech).mul(0.04));
1054
+ risk = risk.add(w.Tech.mul(w.Bonds).mul(0.005));
1055
+ risk = risk.add(w.Bonds.mul(w.Bonds).mul(0.01));
1056
+
1057
+ m.objective(minimize(risk));
1058
+ ```
1059
+
1060
+ ---
1061
+
1062
+ ## 📊 Evaluating Expressions After Solving (`.value`)
1063
+
1064
+ 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:
1065
+
1066
+ ```typescript
1067
+ const revenue = expr`30 * ${x} + 50 * ${y}`;
1068
+ const laborUsed = expr`2 * ${x} + ${y}`;
1069
+
1070
+ m.objective(maximize(revenue));
1071
+ m.constraint(expr`${x} + 2 * ${y} <= 40`, "wood_cap");
1072
+ m.constraint(expr`${laborUsed} <= 50`, "labor_cap");
1073
+
1074
+ await m.solve();
1075
+
1076
+ console.log("Total Revenue:", revenue.value); // 1100 (x = 20, y = 10)
1077
+ console.log("Labor Used:", laborUsed.value); // 50
1078
+ ```
1079
+
1080
+ If the model has not been solved (or any variable in the expression has `value === null`), `.value` returns `null`.
1081
+
1082
+ ---
1083
+
1084
+ ## 🔗 Fluent Arithmetic & Relational Methods
1085
+
1086
+ In addition to `expr`, `Var`, `LinExpr`, and `QuadExpr` provide immutable fluent methods for programmatic expression building:
1087
+
1088
+ | Method | Supported On | Return Type | Description |
1089
+ | :--- | :--- | :--- | :--- |
1090
+ | `.add(other)` | `Var`, `LinExpr`, `QuadExpr` | `LinExpr` or `QuadExpr` | Adds a number, `Var`, `LinExpr`, or `QuadExpr`. |
1091
+ | `.sub(other)` | `Var`, `LinExpr`, `QuadExpr` | `LinExpr` or `QuadExpr` | Subtracts a number, `Var`, `LinExpr`, or `QuadExpr`. |
1092
+ | `.mul(other)` | `Var`, `LinExpr`, `QuadExpr` | `LinExpr` or `QuadExpr` | Multiplies by a scalar `number`, or multiplies two linear terms into a `QuadExpr`. |
1093
+ | `.div(divisor)` | `LinExpr`, `QuadExpr` | `LinExpr` or `QuadExpr` | Divides by a non-zero finite scalar `number`. |
1094
+ | `.neg()` | `Var`, `LinExpr`, `QuadExpr` | `LinExpr` or `QuadExpr` | Negates all terms ($-e$). |
1095
+ | `.le(rhs)` | `Var`, `LinExpr` | `Constraint` | Builds a $\le$ constraint (`lhs <= rhs`). |
1096
+ | `.ge(rhs)` | `Var`, `LinExpr` | `Constraint` | Builds a $\ge$ constraint (`lhs >= rhs`). |
1097
+ | `.eq(rhs)` | `Var`, `LinExpr` | `Constraint` | Builds an $==$ equality constraint (`lhs == rhs`). |
1098
+
1099
+ ---
1100
+
1101
+ # Constraints (`m.constraint` & `m.indicator`)
1102
+
1103
+ Constraints restrict the feasible solution space of your mathematical or logical model.
1104
+ In WaSHI, both linear arithmetic constraints and propositional SMT formulas are registered using **`m.constraint()`**:
1105
+
1106
+ ```typescript
1107
+ m.constraint(expr`${desks} + 2 * ${chairs} <= 40`, "wood_limit");
1108
+ ```
1109
+
1110
+ ---
1111
+
1112
+ ## ✍️ Algebraic Linear Constraints (`expr`)
1113
+
1114
+ The `expr` tagged template literal syntax allows writing equations and inequalities with natural mathematical notation:
1115
+
1116
+ ```typescript
1117
+ import { Model, expr } from "@ohtaman/washi";
1118
+
1119
+ const m = new Model();
1120
+ const x = m.continuous("x", { lb: 0 });
1121
+ const y = m.continuous("y", { lb: 0 });
1122
+
1123
+ // Upper bound inequality (<=)
1124
+ m.constraint(expr`${x} + 2 * ${y} <= 40`, "wood_limit");
1125
+
1126
+ // Lower bound inequality (>=)
1127
+ m.constraint(expr`3 * ${x} + ${y} >= 20`, "min_demand");
1128
+
1129
+ // Exact equality (==)
1130
+ m.constraint(expr`${x} + ${y} == 30`, "exact_quota");
1131
+ ```
1132
+
1133
+ ### Supported Comparison Operators
1134
+ - `<=`: Upper bound constraint
1135
+ - `>=`: Lower bound constraint
1136
+ - `==` or `=`: Exact equality constraint
1137
+
1138
+ ### Batch Registration
1139
+ You can pass an array of constraints to `m.constraint([...])` to register them transactionally in a single call:
1140
+
1141
+ ```typescript
1142
+ m.constraint([expr`${x} <= 10`, expr`${y} <= 20`]);
1143
+ ```
1144
+
1145
+ ---
1146
+
1147
+ ## 🏷️ Naming Constraints
1148
+
1149
+ Every constraint can be given a unique, descriptive name as the second argument of `m.constraint(c, name)`:
1150
+
1151
+ ```typescript
1152
+ m.constraint(expr`${x} + ${y} <= 100`, "budget_cap");
1153
+ ```
1154
+
1155
+ ### Why Name Constraints?
1156
+ 1. **Model Inspection (`m.toLpString()`)**: Exported CPLEX LP strings retain these descriptive names (sanitized if needed) so you can inspect the exact generated formulation.
1157
+ 2. **Conflict Diagnosis (`conflicts` / `unsatCore`)**: When constraints contradict each other, Z3 `m.check()` returns the exact constraint names forming the conflicting subset.
1158
+ 3. **Direct Lookup (`m.getConstraintByName()`)**: Retrieve a constraint by name after solving to inspect its `dual` or `slack` value.
1159
+
1160
+ ---
1161
+
1162
+ ## 🔁 Adding Constraints over Iterables & Sums
1163
+
1164
+ Use `sum()` or `dot()` inside `expr` to express linear aggregations cleanly:
1165
+
1166
+ ```typescript
1167
+ import { Model, sum, expr } from "@ohtaman/washi";
1168
+
1169
+ const items = ["A", "B", "C"];
1170
+ const weights = { A: 5, B: 10, C: 15 };
1171
+ const selected = m.binary(items);
1172
+
1173
+ // Total weight cannot exceed capacity 25
1174
+ m.constraint(
1175
+ expr`${sum(items, (i) => selected[i].mul(weights[i]))} <= 25`,
1176
+ "capacity_limit"
1177
+ );
1178
+ ```
1179
+
1180
+ ---
1181
+
1182
+ ## 🎯 Conditional / Indicator Constraints (`m.indicator`)
1183
+
1184
+ When a linear constraint should only take effect when a binary decision variable is active (`1` or `0`), use `m.indicator()`:
1185
+
1186
+ ```typescript
1187
+ const useDiscount = m.binary("use_discount");
1188
+ const cartTotal = m.continuous("cart_total", { lb: 0, ub: 10_000 });
1189
+
1190
+ // If useDiscount == 1, then cartTotal >= 100
1191
+ // WaSHI automatically derives a safe, tight Big-M from the variable bounds
1192
+ m.indicator(useDiscount, expr`${cartTotal} >= 100`, {
1193
+ activeValue: 1,
1194
+ name: "min_spend_for_discount",
1195
+ });
1196
+ ```
1197
+
1198
+ ### Safe Automatic Big-M Derivation & Bounds
1199
+
1200
+ When `options.M` is omitted, WaSHI automatically derives a safe, minimal Big-M value from the finite variable bounds:
1201
+ - For `<=` constraints, variables with positive coefficients require finite upper bounds (`ub`), and negative coefficients require finite lower bounds (`lb`).
1202
+ - For `>=` constraints, variables with positive coefficients require finite lower bounds (`lb`), and negative coefficients require finite upper bounds (`ub`).
1203
+ - For `==` constraints, safe bounds are derived separately for both the `<=` and `>=` directions (generating `name_le` and `name_ge`).
1204
+
1205
+ If any referenced variable lacks the required finite bounds, `m.indicator()` throws an actionable error prompting for finite bounds or an explicit `options.M`.
1206
+
1207
+ ### Explicit Override & Numerical Stability
1208
+
1209
+ You can override the derived Big-M by providing `{ M: number }`:
1210
+
1211
+ ```typescript
1212
+ m.indicator(useDiscount, expr`${cartTotal} >= 100`, {
1213
+ activeValue: 1,
1214
+ M: 10_000, // Explicit override
1215
+ });
1216
+ ```
1217
+
1218
+ > [!TIP]
1219
+ > **Numerical Stability vs. Correctness**:
1220
+ > 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.
1221
+ > By setting explicit, realistic variable bounds (`lb` and `ub`), WaSHI derives the tightest possible safe Big-M automatically.
1222
+
1223
+ ---
1224
+
1225
+ ## 🧠 Propositional Logic & SMT Constraints (`BoolVar`, `and`, `or`, `not`, `implies`, `xor`)
1226
+
1227
+ When solving with the **Z3** SMT engine (`@ohtaman/washi/solvers/z3`), you can combine propositional boolean variables (`m.bool()`) and linear constraints (`Constraint`) using first-class logical operators:
1228
+
1229
+ ```typescript
1230
+ import { Model, expr, and, or, not, implies, xor } from "@ohtaman/washi";
1231
+ import "@ohtaman/washi/solvers/z3";
1232
+
1233
+ const m = new Model({ solver: "z3" });
1234
+ const isAdmin = m.bool("isAdmin");
1235
+ const hasMfa = m.bool("hasMfa");
1236
+ const Clearance = m.integer("clearance", { lb: 0, ub: 5 });
1237
+
1238
+ // Method syntax on BoolVar / BoolOp:
1239
+ m.constraint(isAdmin.implies(hasMfa), "admin_requires_mfa");
1240
+
1241
+ // Mixing boolean propositions with linear arithmetic comparisons:
1242
+ m.constraint(
1243
+ implies(isAdmin, expr`${Clearance} >= 4`),
1244
+ "admin_clearance_level"
1245
+ );
1246
+
1247
+ // Top-level helper functions (and, or, not, implies, xor):
1248
+ m.constraint(and(or(isAdmin, hasMfa), not(xor(isAdmin, hasMfa))), "policy");
1249
+ ```
1250
+
1251
+ ### Choosing Between `m.indicator()` and `implies()`
1252
+
1253
+ | Feature | `m.indicator(binVar, constraint, opts)` | `implies(a, b)` / `a.implies(b)` |
1254
+ | :--- | :--- | :--- |
1255
+ | **Trigger variable** | Numeric `Binary` variable (`m.binary()`) | `BoolVar` (`m.bool()`), `BoolOp`, or `Constraint` |
1256
+ | **Consequent** | Linear `Constraint` (`<=`, `>=`, `==`) | `BoolVar`, `BoolOp`, or linear `Constraint` |
1257
+ | **Target Solvers** | **All MIP/LP solvers** (HiGHS, GLPK, Z3) via Big-M linearization | **Z3 SMT solver** (`capabilities.supportsSMT: true`) |
1258
+ | **Best For** | Operations research & MIP optimization with an objective (`m.solve()`) | Rule engines, policy verification, and SMT feasibility (`m.check()`) |
1259
+
1260
+ ---
1261
+
1262
+ ## 📐 Dual Values, Shadow Prices & Lookups
1263
+
1264
+ For continuous Linear Programming (LP) problems, query the economic shadow price (`dual`) and surplus (`slack`) on each constraint:
1265
+
1266
+ ```typescript
1267
+ m.constraint(expr`${x} + 2 * ${y} <= 40`, "wood");
1268
+
1269
+ await m.solve();
1270
+
1271
+ const woodConstraint = m.getConstraintByName("wood");
1272
+ console.log("Shadow price (dual):", woodConstraint?.dual);
1273
+ console.log("Slack surplus:", woodConstraint?.slack);
1274
+ console.log("Total linear constraints:", m.numConstrs);
1275
+ ```
1276
+
1277
+ For logical constraints registered with a name, use `m.getLogicalConstraintByName(name)` to look up the registered `LogicalConstraintItem`.
1278
+
1279
+ ---
1280
+
1281
+ # Objectives & Solving (`m.solve` & `m.check`)
1282
+
1283
+ The objective function defines the goal of mathematical optimization—whether to maximize profits, minimize costs, or minimize quadratic risk variance. When you only need to verify constraint feasibility or solve pure SMT logic, you can also run `m.check()` without an objective.
1284
+
1285
+ ---
1286
+
1287
+ ## Setting the Objective Function
1288
+
1289
+ Set the objective direction and formula declaratively using `m.objective(maximize(...))` or `m.objective(minimize(...))`:
1290
+
1291
+ ```typescript
1292
+ import { Model, expr, maximize, minimize } from "@ohtaman/washi";
1293
+
1294
+ const m = new Model("ProductionRevenue");
1295
+ const x = m.integer("x", { lb: 0, ub: 40 });
1296
+ const y = m.integer("y", { lb: 0, ub: 40 });
1297
+
1298
+ // Maximize revenue: 50*x + 40*y subject to resource capacity
1299
+ m.objective(maximize(expr`50 * ${x} + 40 * ${y}`));
1300
+ m.constraint(expr`2 * ${x} + 3 * ${y} <= 100`, "resource_limit");
1301
+
1302
+ // Or minimize cost:
1303
+ // m.objective(minimize(expr`10 * ${x} + 15 * ${y}`));
1304
+ ```
1305
+
1306
+ 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`.
1307
+
1308
+ ---
1309
+
1310
+ ## 🚀 Solving the Model (`m.solve()` vs. `m.check()`)
1311
+
1312
+ WaSHI provides two asynchronous execution entrypoints:
1313
+
1314
+ | Method | Return Value | Purpose |
1315
+ | :--- | :--- | :--- |
1316
+ | **`await m.solve(options?)`** | `Promise<SolverStatus>` | Optimizes the objective (`Minimize` or `Maximize`) subject to all constraints. Populates primal values, objective value, MIP bounds/gaps, and LP duals/slacks. |
1317
+ | **`await m.check(options?)`** | `Promise<CheckResult>` | Checks constraint satisfiability without an objective. Returns `{ status, values, conflicts?, unsatCore?, runTime? }` where `values: Record<string, number \| boolean>` contains both numeric and Boolean variable assignments. Ideal for SMT/SAT verification and Z3 `unsatCore` extraction. |
1318
+
1319
+ ```typescript
1320
+ const status = await m.solve({
1321
+ timeLimit: 10.0, // Timeout in seconds
1322
+ mipGap: 0.01, // Stop within 1% of global optimum
1323
+ outputFlag: false, // Enable/disable solver console logging (HiGHS: outputFlag, GLPK: verbose)
1324
+ });
1325
+ ```
1326
+
1327
+ ### Solve Options (`SolveOptions`)
1328
+
1329
+ | Option | Type | Default | Description |
1330
+ | :--- | :--- | :--- | :--- |
1331
+ | `solver` | `string` \| `Solver` | Model's solver (`"highs"`) | Registered solver name (e.g. `"highs"`, `"z3"`, `"glpk"`, or custom) or `Solver` instance for this run. |
1332
+ | `timeLimit` | `number` | `undefined` | Maximum solve duration in seconds. |
1333
+ | `mipGap` / `relGap` | `number` | `undefined` | Relative MIP optimality gap tolerance (e.g. `0.01` for 1%). |
1334
+ | `absGap` | `number` | `undefined` | Absolute MIP optimality gap tolerance (HiGHS `mip_abs_gap`). |
1335
+ | `presolve` | `"on"` \| `"off"` \| `"choose"` | `"choose"` | Controls solver presolve reduction prior to optimization. |
1336
+ | `outputFlag` / `verbose` | `boolean` | `false` | Emits internal solver iteration logs (`outputFlag` for HiGHS, `verbose` for GLPK). |
1337
+ | `useWorker` | `boolean` | `true` (browser) | Offloads HiGHS to a dedicated Web Worker in browsers. Set `false` for main-thread execution. |
1338
+ | `wasmUrl` | `string` | Auto-resolved | Custom URL to load `highs.wasm` in browser deployments. |
1339
+ | `signal` | `AbortSignal` | `undefined` | Cooperative cancellation signal (`AbortController.signal`). |
1340
+ | `onProgress` | `(e: SolveProgressEvent) => void` | `undefined` | Callback receiving live solver progress snapshots. |
1341
+
1342
+ You can also pass a solver name directly as the first argument: `await m.solve("glpk", { timeLimit: 5 })`.
1343
+
1344
+ ---
1345
+
1346
+ ## ⏱️ Real-Time Progress (`onProgress`) & Cancellation (`AbortSignal`)
1347
+
1348
+ 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`:
1349
+
1350
+ ```typescript
1351
+ const controller = new AbortController();
1352
+
1353
+ // Example: cancel if the user clicks a "Stop" button or after a custom condition
1354
+ const status = await m.solve({
1355
+ signal: controller.signal,
1356
+ onProgress: (event) => {
1357
+ console.log(
1358
+ `[${event.elapsedTime.toFixed(1)}s] ` +
1359
+ `Best: ${event.objectiveValue ?? "—"}, ` +
1360
+ `Bound: ${event.objectiveBound ?? "—"}, ` +
1361
+ `Gap: ${event.mipGap !== undefined ? (event.mipGap * 100).toFixed(2) + "%" : "—"}`
1362
+ );
1363
+
1364
+ // Optional: stop early as soon as a good-enough incumbent is found
1365
+ if (event.objectiveValue !== undefined && event.objectiveValue >= 1000) {
1366
+ controller.abort();
1367
+ }
1368
+ },
1369
+ });
1370
+
1371
+ console.log("Status:", m.status); // "Feasible" (if an incumbent was found before abort) or "Unknown"
1372
+ console.log("Termination:", m.terminationStatus); // "Interrupted"
1373
+ console.log("Has incumbent:", m.hasPrimalSolution); // true if an incumbent was available
1374
+ ```
1375
+
1376
+ ### `SolveProgressEvent` Fields
1377
+
1378
+ | Field | Type | Description |
1379
+ | :--- | :--- | :--- |
1380
+ | `elapsedTime` | `number` | Elapsed solver time in seconds (optional alias: `elapsedSeconds`). |
1381
+ | `phase` | `"simplex" \| "ipm" \| "mip" \| undefined` | Current solver phase when known. |
1382
+ | `objectiveValue` | `number \| undefined` | Current best feasible incumbent objective value (MIP). |
1383
+ | `objectiveBound` | `number \| undefined` | Current best proven dual / objective bound (MIP). |
1384
+ | `relGap` / `mipGap` | `number \| undefined` | Current relative MIP optimality gap (`0.05` = 5%). |
1385
+ | `absGap` | `number \| undefined` | Current absolute MIP optimality gap `\|objectiveValue - objectiveBound\|`. |
1386
+ | `nodes` | `number \| undefined` | Branch-and-bound nodes explored so far. |
1387
+ | `iterations` | `number \| undefined` | Solver iteration count (simplex, IPM, or MIP LP iterations). |
1388
+ | `message` | `string \| undefined` | Raw progress line emitted by the solver. |
1389
+
1390
+ ---
1391
+
1392
+ ## 📊 Reading Results, Bounds & Solver Status
1393
+
1394
+ After solving, query the status, optimal values, and MIP optimality bounds on the `Model`:
1395
+
1396
+ ```typescript
1397
+ const status = await m.solve();
1398
+
1399
+ console.log("Status:", m.status); // e.g. "Optimal" or "Feasible"
1400
+ console.log("Termination Reason:", m.terminationStatus); // e.g. "Optimal", "TimeLimit", "Interrupted"
1401
+ console.log("Has Primal Solution:", m.hasPrimalSolution); // true when x.value / m.objectiveValue are populated
1402
+
1403
+ // Objective & MIP Gap Metrics
1404
+ console.log("Objective Value:", m.objectiveValue); // Best primal objective (or null)
1405
+ console.log("Objective Bound:", m.objectiveBound); // Best dual bound (or undefined)
1406
+ console.log("Relative Gap:", m.relGap); // Relative MIP gap (alias: m.mipGap)
1407
+ console.log("Absolute Gap:", m.absGap); // |objectiveValue - objectiveBound|
1408
+
1409
+ // Individual & Bulk Variable Values
1410
+ console.log("x =", x.value, "y =", y.value);
1411
+ console.log(m.getValues({ x, y })); // { x: ..., y: ... }
1412
+ ```
1413
+
1414
+ ### Understanding `m.status` vs. `m.terminationStatus`
1415
+
1416
+ When a MIP solver stops early due to a time limit, iteration limit, or `AbortSignal`, two distinct questions matter:
1417
+ 1. **Is there a usable solution?** (`m.hasPrimalSolution` / `m.status`)
1418
+ 2. **Why did the solver stop?** (`m.terminationStatus`)
1419
+
1420
+ If an integer-feasible incumbent was found before hitting `timeLimit` or `signal.abort()`, WaSHI sets `m.status = "Feasible"` (so you know `x.value` and `m.objectiveValue` hold a valid solution) and sets `m.terminationStatus = "TimeLimit"` (or `"Interrupted"`). If no incumbent was found before stopping, `m.hasPrimalSolution` is `false`, `m.terminationStatus` records why the solver stopped (`"TimeLimit"`, `"IterationLimit"`, or `"Interrupted"`), and `m.status` is `"TimeLimit"`, `"IterationLimit"`, or `"Unknown"` (when aborted via `AbortSignal` without an incumbent).
1421
+
1422
+ ### All `SolverStatus` Values
1423
+
1424
+ | Status | `hasPrimalSolution` | Meaning |
1425
+ | :--- | :--- | :--- |
1426
+ | `"Optimal"` | `true` | Global optimal solution proven (within tolerances). |
1427
+ | `"Feasible"` | `true` | Valid primal solution (incumbent) found, but optimality was not proven before stopping (`terminationStatus` indicates why: `"TimeLimit"`, `"IterationLimit"`, `"Interrupted"`, etc.). |
1428
+ | `"Sat"` | `true` | Constraints are satisfiable (`m.check()`). |
1429
+ | `"Infeasible"` | `false` | Proven mathematically infeasible; no solution satisfies all constraints. |
1430
+ | `"Unsat"` | `false` | Logical/arithmetic constraints are unsatisfiable (`m.check()`). |
1431
+ | `"Unbounded"` | `false` | Objective can improve indefinitely without violating constraints. |
1432
+ | `"TimeLimit"` | `false` | Time limit reached before finding any feasible primal solution. |
1433
+ | `"IterationLimit"` | `false` | Iteration limit reached before finding any feasible primal solution. |
1434
+ | `"Unknown"` | `false` | Solver stopped inconclusively without a valid primal solution (including when cancelled via `AbortSignal` before finding an incumbent, where `terminationStatus === "Interrupted"`). |
1435
+ | `"Error"` | `false` | Solver encountered an internal execution error. |
1436
+ | `"NotSolved"` | `false` | Initial state before solving, or after the model has been mutated. |
1437
+
1438
+ ---
1439
+
1440
+ ## 🔄 Model Mutations & Execution Validity
1441
+
1442
+ A `Model` instance keeps its solution state synchronized across mutations made through `Model` methods and `Model` / `Var` / `Constraint` property setters:
1443
+
1444
+ 1. **Post-Solve Mutations Reset Solution State**: Adding variables or constraints, updating the objective (`m.objective(...)`, `m.objectiveExpr = ...`, `m.sense = ...`), or modifying variable bounds/types (`v.lb`, `v.ub`, `v.type`) or constraint properties (`c.lhs`, `c.sense`, `c.rhs`) immediately resets `m.status` to `"NotSolved"`, clears `m.solution` and `m.objectiveValue`, and resets variable/constraint primal and dual values to `null`.
1445
+ 2. **Mid-Flight Mutations Discard Stale Runs**: If the model is modified via these APIs while an asynchronous `m.solve()` or `m.check()` call is in progress, the stale solver result is discarded, the model remains in `"NotSolved"`, and the in-flight promise rejects with an explicit error.
1446
+ 3. **Overlapping Executions**: Starting a new `m.solve()` or `m.check()` call supersedes any earlier in-flight call on the same model so out-of-order completions can never overwrite the latest run's state.
1447
+ 4. **Transactional Batch Edits**: If an invalid edit is rejected (including batch array calls like `m.constraint([...])`), the model rolls back the failed edit without leaving partial entities registered or invalidating an existing valid solution.
1448
+
1449
+ > [!NOTE]
1450
+ > Renaming already-registered variables or constraints (e.g., reassigning `c.name` after registration) or mutating internal backing collections and ASTs directly (such as `c.lhs.terms`, `m.constraints.splice(...)`, or `BoolOp.children`) is unsupported and will be rejected by model integrity validation on the next solve/check/export call.
1451
+
1452
+ ---
1453
+
1454
+ # Conflict Diagnosis (`unsatCore`) & Solution Validation
1455
+
1456
+ When building real-world optimization and configuration systems, two verification questions arise constantly:
1457
+ 1. **Why is my model infeasible?** Which specific business rules contradict each other?
1458
+ 2. **Does the solver's solution genuinely satisfy every constraint?** How do I verify tolerances, integrality, and formulations?
1459
+
1460
+ 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`).
1461
+
1462
+ ---
1463
+
1464
+ ## 🔍 Conflict Diagnosis (`unsatCore` / `conflicts`)
1465
+
1466
+ When a model is **Infeasible**, traditional MIP solvers simply report that no solution exists without indicating *which* constraints are contradictory.
1467
+ 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:
1468
+
1469
+ ```typescript
1470
+ import { Model, expr } from "@ohtaman/washi";
1471
+ // Register the Z3 solver plugin
1472
+ import "@ohtaman/washi/solvers/z3";
1473
+
1474
+ const m = new Model();
1475
+
1476
+ const x = m.continuous("reserve", { lb: 0 });
1477
+
1478
+ // Add named business requirements
1479
+ m.constraint(expr`${x} >= 100`, "minimum_regulatory_reserve");
1480
+ m.constraint(expr`${x} <= 50`, "budget_ceiling");
1481
+ m.constraint(expr`${x} >= 10`, "routine_operating_cost");
1482
+
1483
+ const result = await m.check({ solver: "z3" });
1484
+
1485
+ if (result.status === "Unsat") {
1486
+ console.log("Contradictory constraints identified:");
1487
+ console.log(m.conflicts); // or result.conflicts / result.unsatCore / m.unsatCore
1488
+ // Output: ["minimum_regulatory_reserve", "budget_ceiling"]
1489
+ }
1490
+ ```
1491
+
1492
+ Notice that `routine_operating_cost` is **not** in the core, because only `minimum_regulatory_reserve` ($x \ge 100$) and `budget_ceiling` ($x \le 50$) directly contradict each other.
1493
+
1494
+ > [!IMPORTANT]
1495
+ > - **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`).
1496
+ > - **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")`.
1497
+
1498
+ ### Workflow: Optimize with HiGHS, Diagnose with Z3
1499
+
1500
+ Because WaSHI models are solver-independent, you can solve your optimization model with **HiGHS** first and automatically fall back to **Z3** `m.check()` if HiGHS reports `"Infeasible"`:
1501
+
1502
+ ```typescript
1503
+ const status = await m.solve(); // Fast MIP solve with default HiGHS
1504
+
1505
+ if (status === "Infeasible") {
1506
+ const diag = await m.check({ solver: "z3" });
1507
+ console.error("Model is infeasible due to conflicting rules:", diag.conflicts);
1508
+ }
1509
+ ```
1510
+
1511
+ ---
1512
+
1513
+ ## ✅ Independent Solution Validation (`m.validateSolution()`)
1514
+
1515
+ 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:
1516
+
1517
+ ```typescript
1518
+ const status = await m.solve();
1519
+
1520
+ if (m.hasPrimalSolution) {
1521
+ const report = m.validateSolution({
1522
+ feasibilityTolerance: 1e-6, // Max allowed violation for bounds and linear constraints
1523
+ integralityTolerance: 1e-6, // Max distance from nearest integer for Integer/Binary vars
1524
+ evaluateLogicalConstraints: true,
1525
+ });
1526
+
1527
+ console.log("Valid:", report.valid);
1528
+ console.log("Complete:", report.complete);
1529
+ console.log("Max violation:", report.maxViolation);
1530
+ console.log("Summary:", report.summary);
1531
+
1532
+ if (!report.valid) {
1533
+ console.warn("Violations detected:", report.violations);
1534
+ }
1535
+ }
1536
+ ```
1537
+
1538
+ ### `SolutionValidationReport` Summary
1539
+
1540
+ | Property | Type | Description |
1541
+ | :--- | :--- | :--- |
1542
+ | `valid` | `boolean` | `true` iff `complete === true`, `uncheckedConstraints.length === 0`, and `violations.length === 0`. |
1543
+ | `complete` | `boolean` | `true` when all variables, Boolean variables, linear constraints, and logical constraints were evaluated. |
1544
+ | `maxViolation` | `number` | Largest numeric violation magnitude across all evaluated bounds, integrality checks, and constraints. |
1545
+ | `violations` | `SolutionViolation[]` | Flat list of violations (`"missing_value"`, `"non_finite_value"`, `"invalid_boolean_value"`, `"lower_bound"`, `"upper_bound"`, `"integrality"`, `"linear_constraint"`, `"logical_constraint"`, `"unchecked_constraint"`). |
1546
+ | `violationsByName` | `Record<string, SolutionViolation[]>` | Violations grouped by variable or constraint name. |
1547
+ | `uncheckedConstraints` | `string[]` | Names of constraints that could not be evaluated. |
1548
+ | `missingVariables` | `string[]` | Names of numeric or Boolean variables whose values were `null` or `undefined`. |
1549
+ | `summary` | `SolutionValidationSummary` | Counts of checked vs. total variables, Boolean variables, linear constraints, and logical constraints. |
1550
+
1551
+ ---
1552
+
1553
+ ## 📄 Inspecting the LP Formulation (`m.toLpString()`)
1554
+
1555
+ When debugging coefficients, signs, or Big-M indicator expansions, call `m.toLpString()` to export the model in standard CPLEX/HiGHS `.lp` format:
1556
+
1557
+ ```typescript
1558
+ const m = new Model("DebugModel");
1559
+ const x = m.integer("x", { lb: 0, ub: 10 });
1560
+ m.objective(maximize(expr`5 * ${x}`));
1561
+ m.constraint(expr`2 * ${x} <= 15`, "resource_limit");
1562
+
1563
+ console.log(m.toLpString());
1564
+ /*
1565
+ Maximize
1566
+ obj: 5 x
1567
+ Subject To
1568
+ resource_limit: 2 x <= 15
1569
+ Bounds
1570
+ 0 <= x <= 10
1571
+ Generals
1572
+ x
1573
+ End
1574
+ */
1575
+ ```
1576
+
1577
+ ---
1578
+
1579
+ ## 🧪 Unit Testing Application Logic with `MockSolver`
1580
+
1581
+ 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:
1582
+
1583
+ ```typescript
1584
+ import { Model, MockSolver, expr, maximize } from "@ohtaman/washi";
1585
+
1586
+ const mock = new MockSolver({
1587
+ status: "Optimal",
1588
+ objectiveValue: 420,
1589
+ values: { x: 10, y: 20 },
1590
+ });
1591
+
1592
+ const m = new Model({ solver: mock });
1593
+ const x = m.integer("x");
1594
+ const y = m.integer("y");
1595
+ m.objective(maximize(expr`10 * ${x} + 16 * ${y}`));
1596
+
1597
+ await m.solve();
1598
+ console.log(m.objectiveValue); // 420
1599
+ console.log(x.value, y.value); // 10, 20
1600
+ ```
1601
+
1602
+ ---
1603
+
1604
+ # Solvers Overview
1605
+
1606
+ WaSHI is designed around a pluggable, decoupled solver architecture. You formulate mathematical models using a unified algebraic API, then solve them with the engine best suited for your problem class.
1607
+
1608
+ ---
1609
+
1610
+ ## ⚡ Solvers Matrix & Comparison
1611
+
1612
+ | Solver | Engine License | Problem Classes | Best For | Status in WaSHI |
1613
+ | :--- | :--- | :--- | :--- | :--- |
1614
+ | **HiGHS** | **MIT** | **LP, MIP, Convex QP** | Industrial-scale optimization, operations research, finance | **Built-in default** (zero configuration) |
1615
+ | **Z3** | **MIT** | **SMT, SAT, Logic, MIP** | Propositional logic, conflict diagnosis (`unsatCore`), rule engines | Optional plugin (`@ohtaman/washi/solvers/z3`) |
1616
+ | **GLPK** | **GPL-3.0** | **LP, MIP** | Academic benchmarks, legacy GLPK compatibility | Optional plugin (`@ohtaman/washi/solvers/glpk`) |
1617
+ | **Mock** | **MIT** | *Any* | Ultra-fast unit testing in CI without loading WebAssembly | Built-in |
1618
+
1619
+ ---
1620
+
1621
+ ## Selecting a Solver
1622
+
1623
+ Specify the solver by name when creating your `Model`:
1624
+
1625
+ ```typescript
1626
+ // 1. Built-in HiGHS (default)
1627
+ const m1 = new Model({ solver: "highs" });
1628
+
1629
+ // 2. Z3 SMT solver
1630
+ import "@ohtaman/washi/solvers/z3";
1631
+ const m2 = new Model({ solver: "z3" });
1632
+
1633
+ // 3. GLPK solver
1634
+ import "@ohtaman/washi/solvers/glpk";
1635
+ const m3 = new Model({ solver: "glpk" });
1636
+ ```
1637
+
1638
+ ---
1639
+
1640
+ ## Solver Guides
1641
+
1642
+ - [HiGHS Guide](docs/solvers/highs.md): Web Worker multithreading, MIP tolerances, and QP solving.
1643
+ - [Z3 Guide](docs/solvers/z3.md): SMT assertions, logic modeling, and Unsat Core analysis.
1644
+ - [GLPK Guide](docs/solvers/glpk.md): GLPK adapter, GPL-3.0 licensing considerations, and browser usage.
1645
+ - [Custom Solvers](docs/solvers/custom-solver.md): Step-by-step developer tutorial for integrating proprietary or cloud REST solvers.
1646
+
1647
+ ---
1648
+
1649
+ # HiGHS Solver Engine
1650
+
1651
+ **HiGHS** is high-performance mathematical optimization software developed at the University of Edinburgh. It is widely recognized as the state-of-the-art open-source solver for Linear Programming (LP), Mixed-Integer Programming (MIP), and Convex Quadratic Programming (QP).
1652
+
1653
+ ---
1654
+
1655
+ ## Key Features in WaSHI
1656
+
1657
+ - **Built-in & Zero-Config**: Bundled directly with `@ohtaman/washi`. No separate peer dependency or compilation required.
1658
+ - **Permissive MIT License**: 100% commercially friendly.
1659
+ - **Web Worker Multithreading**: Automatically offloads intensive solving to background Web Workers in browser environments to avoid freezing the UI thread.
1660
+ - **Warm-Start Support (`initialValue`)**: Supports passing variable primal initial value hints (`initialValue`) to HiGHS `setSolution` for warm starting.
1661
+ - **Fast WASM Execution**: Emscripten-compiled 32-bit WASM runs at near-native speed.
1662
+
1663
+ ---
1664
+
1665
+ ## Supported Problem Classes
1666
+
1667
+ ### 1. Linear Programming (LP)
1668
+ Solves continuous problems via Primal and Dual Simplex algorithms or Interior Point (IPM) methods. After solving, you can inspect constraint shadow prices (`c.dual`), slacks (`c.slack`), and variable reduced costs (`v.reducedCost`):
1669
+
1670
+ ```typescript
1671
+ import { Model, expr, maximize } from "@ohtaman/washi";
1672
+
1673
+ const model = new Model({ solver: "highs" });
1674
+ const x = model.continuous("x", { lb: 0 });
1675
+ const y = model.continuous("y", { lb: 0 });
1676
+ model.objective(maximize(expr`10 * ${x} + 6 * ${y}`));
1677
+ model.constraint(expr`${x} + ${y} <= 100`, "cap");
1678
+ await model.solve();
1679
+ ```
1680
+
1681
+ ### 2. Mixed-Integer Linear Programming (MIP)
1682
+ Solves discrete problems using Branch-and-Bound and cutting plane techniques:
1683
+
1684
+ ```typescript
1685
+ const count = model.integer("count", { lb: 0, ub: 50 });
1686
+ ```
1687
+
1688
+ ### 3. Convex Quadratic Programming (QP)
1689
+ Solves quadratic objective functions subject to linear constraints:
1690
+
1691
+ ```typescript
1692
+ // Minimize risk/variance uniformly using expr:
1693
+ model.objective(minimize(expr`0.5 * ${x}^2 + 0.5 * ${y}^2`));
1694
+
1695
+ // Or programmatically with .mul():
1696
+ // const quadObj = x.mul(x).mul(0.5).add(y.mul(y).mul(0.5));
1697
+ // model.objective(minimize(quadObj));
1698
+ ```
1699
+
1700
+ ---
1701
+
1702
+ ## Solver Options
1703
+
1704
+ Pass solver options into `model.solve()`:
1705
+
1706
+ ```typescript
1707
+ const controller = new AbortController();
1708
+
1709
+ await model.solve({
1710
+ timeLimit: 60.0, // Maximum solve time in seconds
1711
+ mipGap: 0.001, // Terminate when within 0.1% of global optimality (alias: relGap)
1712
+ absGap: 1.0, // Absolute MIP optimality gap tolerance
1713
+ presolve: "on", // Presolve control ("on" | "off" | "choose")
1714
+ outputFlag: true, // Output internal simplex / branch-and-bound logs (output_flag)
1715
+ signal: controller.signal,
1716
+ onProgress: (ev) => {
1717
+ console.log(`Elapsed: ${ev.elapsedTime.toFixed(1)}s, Best: ${ev.objectiveValue}`);
1718
+ },
1719
+ });
1720
+ ```
1721
+
1722
+ ---
1723
+
1724
+ ## Worker & WASM Configuration in Browsers
1725
+
1726
+ By default in browsers, WaSHI spawns a dedicated Web Worker to execute HiGHS in the background so UI rendering stays responsive and `onProgress` / `AbortSignal` work seamlessly.
1727
+
1728
+ If you need to run synchronously on the main thread (or in environments where Web Workers are prohibited), pass `useWorker: false` to `model.solve()` or instantiate `HighsSolver`:
1729
+
1730
+ ```typescript
1731
+ import { Model, HighsSolver } from "@ohtaman/washi";
1732
+
1733
+ // Option 1: Per solve call
1734
+ await model.solve({ useWorker: false });
1735
+
1736
+ // Option 2: When instantiating the solver
1737
+ const syncModel = new Model({
1738
+ solver: new HighsSolver({ useWorker: false }),
1739
+ });
1740
+ ```
1741
+
1742
+ ---
1743
+
1744
+ # Z3 SMT Solver Engine
1745
+
1746
+ **Z3** is a theorem prover and Satisfiability Modulo Theories (SMT) solver developed by Microsoft Research. In WaSHI, Z3 provides advanced propositional logic reasoning and conflict diagnosis.
1747
+
1748
+ ---
1749
+
1750
+ ## Installation
1751
+
1752
+ Z3 is provided as an optional peer dependency:
1753
+
1754
+ ```bash
1755
+ npm install z3-solver
1756
+ ```
1757
+
1758
+ ---
1759
+
1760
+ ## When to Use Z3?
1761
+
1762
+ 1. **Unsat Core Conflict Diagnosis**:
1763
+ When your MIP model is infeasible and you need to isolate which constraints are contradictory.
1764
+ 2. **Boolean / Logical Formulations**:
1765
+ When solving problems containing complex logical relations (implications, XOR, nested Boolean expressions).
1766
+ 3. **Exact Mathematical Guarantees**:
1767
+ Z3 can perform exact rational arithmetic without floating-point inaccuracies.
1768
+
1769
+ ---
1770
+
1771
+ ## Basic Usage
1772
+
1773
+ ```typescript
1774
+ import { Model, expr } from "@ohtaman/washi";
1775
+ import "@ohtaman/washi/solvers/z3";
1776
+
1777
+ const model = new Model({ solver: "z3" });
1778
+
1779
+ const x = model.integer("x", { lb: 0, ub: 10 });
1780
+ const y = model.integer("y", { lb: 0, ub: 10 });
1781
+
1782
+ model.constraint(expr`${x} + ${y} == 15`, "sum_15");
1783
+ model.constraint(expr`${x} >= 8`, "x_at_least_8");
1784
+
1785
+ await model.solve();
1786
+ console.log(`x = ${x.value}, y = ${y.value}`);
1787
+ ```
1788
+
1789
+ ---
1790
+
1791
+ ## Unsat Core Extraction (`model.check()`)
1792
+
1793
+ Use `await model.check()` with Z3 to verify satisfiability and extract the conflicting subset of named constraints (`conflicts` / `unsatCore`) when a model is unsatisfiable:
1794
+
1795
+ ```typescript
1796
+ const model = new Model({ solver: "z3" });
1797
+ const a = model.continuous("a");
1798
+
1799
+ model.constraint(expr`${a} >= 10`, "req_A");
1800
+ model.constraint(expr`${a} <= 5`, "req_B");
1801
+
1802
+ const result = await model.check();
1803
+
1804
+ if (result.status === "Unsat") {
1805
+ console.log("Conflicting constraints:", result.conflicts); // or model.conflicts / model.unsatCore
1806
+ // ["req_A", "req_B"]
1807
+ }
1808
+ ```
1809
+
1810
+ ---
1811
+
1812
+ ## Browser Security Note
1813
+
1814
+ In browser environments, the Z3 WASM engine relies on pthreads and `SharedArrayBuffer`, which require cross-origin isolation headers (`COOP` / `COEP`). See the [Cross-Origin Isolation Guide](docs/getting-started/cross-origin.md) for detailed setup.
1815
+
1816
+ ## Runtime Failures and Thread Cleanup
1817
+
1818
+ If a Z3 worker encounters a fatal runtime error, pending operations on that instance reject with an error and the broken runtime is invalidated; subsequent `solve()` or `check()` calls automatically initialize a fresh Z3 runtime rather than misclassifying failures as `Sat` or `Unsat`.
1819
+
1820
+ To explicitly terminate Z3 pthread workers when tearing down a browser view or worker pool, call `killZ3Threads()` from `@ohtaman/washi/solvers/z3`.
1821
+
1822
+ ---
1823
+
1824
+ # GLPK Solver Engine
1825
+
1826
+ The **GNU Linear Programming Kit (GLPK)** is a long-standing, robust simplex and interior-point solver package intended for solving large-scale linear programming (LP) and mixed integer programming (MIP).
1827
+
1828
+ ---
1829
+
1830
+ ## Installation
1831
+
1832
+ GLPK is provided as an optional peer dependency via `glpk.js`:
1833
+
1834
+ ```bash
1835
+ npm install glpk.js
1836
+ ```
1837
+
1838
+ ---
1839
+
1840
+ ## Licensing Consideration (GPL-3.0)
1841
+
1842
+ GLPK is licensed under the **GNU General Public License v3.0 (GPL-3.0)**.
1843
+
1844
+ - Because WaSHI is MIT-licensed, `glpk.js` is **never bundled statically** in the core package.
1845
+ - It is dynamically imported only if you explicitly register or invoke `@ohtaman/washi/solvers/glpk`.
1846
+ - Ensure your project's distribution requirements comply with the GPL-3.0 license when shipping products that bundle GLPK.
1847
+
1848
+ ---
1849
+
1850
+ ## Basic Usage
1851
+
1852
+ ```typescript
1853
+ import { Model, expr, maximize } from "@ohtaman/washi";
1854
+ import "@ohtaman/washi/solvers/glpk";
1855
+
1856
+ const model = new Model({ solver: "glpk" });
1857
+
1858
+ const x = model.continuous("x", { lb: 0, ub: 10 });
1859
+ const y = model.continuous("y", { lb: 0, ub: 20 });
1860
+
1861
+ model.objective(maximize(expr`3 * ${x} + 2 * ${y}`));
1862
+ model.constraint(expr`${x} + ${y} <= 15`, "cap");
1863
+
1864
+ await model.solve();
1865
+
1866
+ console.log("GLPK Status:", model.status);
1867
+ console.log("Objective:", model.objectiveValue);
1868
+ ```
1869
+
1870
+ ---
1871
+
1872
+ ## Result Semantics & Status Mapping
1873
+
1874
+ ### Solver Statuses
1875
+ GLPK return codes are mapped strictly to WaSHI `SolverStatus`:
1876
+ - `GLP_OPT` (5) $\to$ `"Optimal"`
1877
+ - `GLP_FEAS` (2) $\to$ `"Feasible"` (feasible MIP incumbent found)
1878
+ - `GLP_INFEAS` (3) / `GLP_NOFEAS` (4) $\to$ `"Infeasible"`
1879
+ - `GLP_UNBND` (6) $\to$ `"Unbounded"`
1880
+ - `GLP_UNDEF` (1) and unrecognized/missing statuses $\to$ `"Unknown"` (never misclassified as `"Optimal"`)
1881
+
1882
+ ### Solution Availability
1883
+ - Variable values (`model.values`, `var.value`) and objective values (`model.objectiveValue`) are exposed **only** when a valid primal solution is available (`"Optimal"` or `"Feasible"`).
1884
+ - For unsuccessful or non-optimal solves (`Infeasible`, `Unbounded`, `Unknown`, `Error`), `model.objectiveValue` is `null` and variable values remain `null` without fabricating fake all-zero solutions.
1885
+
1886
+ ### Differences Between LP and MIP
1887
+ - **Linear Programming (LP)**: Solved using the simplex algorithm. Dual values (shadow prices via `constraint.dual`) and slacks are populated for optimal solutions.
1888
+ - **Mixed-Integer Programming (MIP)**: Solved using the branch-and-cut algorithm. Produces integer-feasible primal variable solutions; dual values are mathematically undefined for discrete optimization and omitted.
1889
+
1890
+ ---
1891
+
1892
+ # WaSHI Custom Solver Developer Guide
1893
+
1894
+ This guide describes how to build, test, and distribute custom solver plugins for **WaSHI** (`@ohtaman/washi`).
1895
+
1896
+ WaSHI features a pluggable, decoupled solver architecture. Core `@ohtaman/washi` bundles only the zero-config HiGHS WASM engine and a Mock solver. Any third-party solver—whether implemented in WebAssembly, native C++, a Web Worker, or an external cloud REST/RPC backend—can be registered and used through the unified `Model` API.
1897
+
1898
+ ---
1899
+
1900
+ ## 1. Pluggable Solvers Architecture Overview
1901
+
1902
+ ```mermaid
1903
+ graph TD
1904
+ A[WaSHI Model] --> B[Solver Registry Map Symbol.for]
1905
+ B --> C[HiGHS WASM Engine Core]
1906
+ B --> D[Mock Solver Core]
1907
+ B --> E[GLPK Plugin @ohtaman/washi/solvers/glpk]
1908
+ B --> F[Z3 SMT Plugin @ohtaman/washi/solvers/z3]
1909
+ B --> G[Your Custom Solver Plugin]
1910
+ ```
1911
+
1912
+ ### Key Architectural Characteristics
1913
+ 1. **Unified Global Registry**: The solver registry is backed by `Symbol.for("washi.solverRegistry")` on `globalThis`. This ensures that even across different ESM chunks, dynamic imports, or `<script>` tags, every bundle accesses the identical solver map.
1914
+ 2. **Behavioral Symmetry**: Node.js and browser environments share identical error messages, solver naming, and capability handling.
1915
+ 3. **Decoupled Bundling**: Core WaSHI does not import optional or custom plugins. A plugin only registers itself when explicitly imported.
1916
+
1917
+ ---
1918
+
1919
+ ## 2. API Contracts & TypeScript Interfaces
1920
+
1921
+ All custom solvers must implement the `Solver` interface defined in `@ohtaman/washi`:
1922
+
1923
+ ```typescript
1924
+ import {
1925
+ generateLpString,
1926
+ buildLpSymbolMaps,
1927
+ isLpExportResult,
1928
+ sanitizeIdentifier,
1929
+ type Solver,
1930
+ type SolverOptions,
1931
+ type SolverResult,
1932
+ type SolverCheckResult,
1933
+ type SolverCapabilities,
1934
+ type SolveModelData,
1935
+ type CheckModelData,
1936
+ type LogicalConstraintItem,
1937
+ type LpModelData,
1938
+ type LpExportResult,
1939
+ type LpSymbolMaps,
1940
+ type SolverStatus,
1941
+ type TerminationStatus,
1942
+ type SolveProgressEvent,
1943
+ } from "@ohtaman/washi";
1944
+ ```
1945
+
1946
+ ### The `Solver` Interface
1947
+
1948
+ ```typescript
1949
+ export interface Solver {
1950
+ /** Unique human-readable name of the solver (e.g. "GLPK", "AcmeCloudSolver") */
1951
+ readonly name: string;
1952
+
1953
+ /** Solver version string (e.g. "1.2.0") */
1954
+ readonly version?: string;
1955
+
1956
+ /** Capability flags declaring which problem classes the solver supports */
1957
+ readonly capabilities?: SolverCapabilities;
1958
+
1959
+ /**
1960
+ * Main solve method for LP, MIP, QP, and SMT optimization models.
1961
+ *
1962
+ * @param modelData Typed in-memory model structure (variables, constraints, objective, sense, boolVars, logicalConstraints)
1963
+ * @param options Runtime execution options (timeLimit, mipGap, verbose, signal, onProgress, etc.)
1964
+ */
1965
+ solve(
1966
+ modelData: SolveModelData,
1967
+ options?: SolverOptions
1968
+ ): Promise<SolverResult>;
1969
+
1970
+ /**
1971
+ * Optional check method for Pure SAT / SMT or feasibility checking without an objective.
1972
+ *
1973
+ * @param modelData Objective-free in-memory model structure (variables, boolVars, constraints, logicalConstraints)
1974
+ * @param options Runtime execution options
1975
+ */
1976
+ check?(
1977
+ modelData: CheckModelData,
1978
+ options?: SolverOptions
1979
+ ): Promise<SolverCheckResult>;
1980
+ }
1981
+ ```
1982
+
1983
+ ### `CheckModelData` & `SolveModelData`
1984
+
1985
+ `Model.solve()` and `Model.check()` pass the structured model directly to your adapter without serializing an LP string upfront:
1986
+
1987
+ ```typescript
1988
+ export interface CheckModelData {
1989
+ /** Model name */
1990
+ name?: string;
1991
+ /** Decision variables registered in the model */
1992
+ variables: Var[];
1993
+ /** Propositional Boolean variables registered in the model */
1994
+ boolVars?: BoolVar[];
1995
+ /** Linear arithmetic constraints registered in the model */
1996
+ constraints: Constraint[];
1997
+ /** Logical / SMT constraints registered in the model */
1998
+ logicalConstraints?: LogicalConstraintItem[];
1999
+ }
2000
+
2001
+ export interface SolveModelData extends CheckModelData {
2002
+ /** Optimization direction ("Minimize" | "Maximize") */
2003
+ sense: OptimizationSense;
2004
+ /** Linear or quadratic objective expression */
2005
+ objective: LinExpr | QuadExpr;
2006
+ }
2007
+ ```
2008
+
2009
+ ### `SolverCapabilities`
2010
+
2011
+ Custom solvers declare their supported features using `capabilities`. Before dispatching `solve()` or `check()`, `Model` validates:
2012
+ - `supportsQP === false` when the objective contains quadratic terms (`QuadExpr`),
2013
+ - `supportsSMT !== true` when the model contains logical constraints (`m.logicalConstraints`), and
2014
+ - `supportsWarmStart !== true` when any variable has a primal `initialValue` set.
2015
+
2016
+ ```typescript
2017
+ export interface SolverCapabilities {
2018
+ /** Supports mixed-integer programming (integer/binary variables) */
2019
+ supportsMIP?: boolean;
2020
+
2021
+ /** Supports quadratic objectives (QP: x^T Q x + c^T x) */
2022
+ supportsQP?: boolean;
2023
+
2024
+ /** Supports second-order cone or quadratic constraints */
2025
+ supportsQCQP?: boolean;
2026
+
2027
+ /** Supports satisfiability modulo theories / boolean logic constraints */
2028
+ supportsSMT?: boolean;
2029
+
2030
+ /** Supports extracting an unsatisfiable core (conflicting constraint subset) */
2031
+ supportsUnsatCore?: boolean;
2032
+
2033
+ /** Supports primal initial values / MIP warm starts (Var.initialValue) */
2034
+ supportsWarmStart?: boolean;
2035
+ }
2036
+ ```
2037
+
2038
+ ### `SolverOptions` & `SolveProgressEvent`
2039
+
2040
+ Runtime options passed to `solve` and `check` include cancellation and progress hooks (note that the routing `solver` field from `SolveOptions` is stripped before options are passed to your adapter):
2041
+
2042
+ ```typescript
2043
+ export interface SolverOptions {
2044
+ /** Time limit in seconds */
2045
+ timeLimit?: number;
2046
+
2047
+ /** Relative MIP optimality gap tolerance */
2048
+ mipGap?: number;
2049
+
2050
+ /** Enable solver console logging */
2051
+ verbose?: boolean;
2052
+
2053
+ /** Cooperative cancellation signal; when aborted, return terminationStatus: "Interrupted" */
2054
+ signal?: AbortSignal;
2055
+
2056
+ /** Progress callback invoked with normalized SolveProgressEvent snapshots */
2057
+ onProgress?: (progress: SolveProgressEvent) => void;
2058
+
2059
+ /** Additional solver-specific options */
2060
+ [key: string]: any;
2061
+ }
2062
+ ```
2063
+
2064
+ ### Optional LP Serialization & Symbol-Mapping Helpers
2065
+
2066
+ Solvers that consume structured JS/WASM data (such as `GLPKSolver` and `Z3Solver`) work directly with `SolveModelData` / `CheckModelData`.
2067
+ If your adapter targets a backend that requires CPLEX/HiGHS LP text or restricted identifier syntax, WaSHI exports opt-in helpers:
2068
+
2069
+ - `generateLpString(modelData): LpExportResult` — serializes `modelData` into a CPLEX LP format string (`lpString`) along with collision-safe symbol lookup maps (`nameToVar`, `nameToConstraint`).
2070
+ - `buildLpSymbolMaps(modelData): LpSymbolMaps` — builds collision-safe sanitized symbol maps (`nameToVar`, `varToName`, `nameToConstraint`, `constraintToName`) without serializing an LP string.
2071
+ - `isLpExportResult(value): value is LpExportResult` — runtime type guard for `LpExportResult` (useful when supporting legacy 3-argument `solve(modelData, lpResult, options)` overloads).
2072
+ - `sanitizeIdentifier(raw, fallbackPrefix = "x", id = 0)` — sanitizes an arbitrary display name into a valid LP identifier.
2073
+
2074
+ ```typescript
2075
+ export interface LpExportResult {
2076
+ /** Standard CPLEX/HiGHS LP formatted string */
2077
+ lpString: string;
2078
+
2079
+ /** Map translating sanitized LP variable identifiers back to WaSHI Var objects */
2080
+ nameToVar: Map<string, Var>;
2081
+
2082
+ /** Map translating sanitized LP constraint identifiers back to WaSHI Constraint objects */
2083
+ nameToConstraint: Map<string, Constraint>;
2084
+ }
2085
+ ```
2086
+
2087
+ ### `SolverResult` Normalization & Identity Decoding
2088
+
2089
+ The `solve` method must return normalized results keyed by the model's canonical variable/constraint names (`v.name`, `bv.name`, `c.name`). Adapters that use `generateLpString(modelData)` or `buildLpSymbolMaps(modelData)` for an LP backend must translate sanitized LP identifiers back to canonical model names using `nameToVar` and `nameToConstraint`:
2090
+
2091
+ ```typescript
2092
+ export interface SolverResult {
2093
+ /** Standardized status string */
2094
+ status: SolverStatus;
2095
+
2096
+ /** Exact termination reason independent of whether a feasible incumbent is available (defaults to status) */
2097
+ terminationStatus?: TerminationStatus;
2098
+
2099
+ /** Whether a feasible primal solution / incumbent is available (inferred from status/values if omitted) */
2100
+ hasPrimalSolution?: boolean;
2101
+
2102
+ /** Optimal or best objective value (null if no primal solution is available) */
2103
+ objectiveValue: number | null;
2104
+
2105
+ /** Best proven dual/objective bound when provided by the solver */
2106
+ objectiveBound?: number;
2107
+
2108
+ /** Relative MIP optimality gap when provided by the solver */
2109
+ relGap?: number;
2110
+
2111
+ /** Alias for relGap: relative MIP optimality gap when provided by the solver */
2112
+ mipGap?: number;
2113
+
2114
+ /** Absolute MIP optimality gap |objectiveValue - objectiveBound| (computed automatically when both are finite) */
2115
+ absGap?: number;
2116
+
2117
+ /** Variable primal values keyed by canonical variable name (non-finite values are ignored) */
2118
+ values: Record<string, number>;
2119
+
2120
+ /** Propositional Boolean variable assignments keyed by BoolVar name (optional, SMT/SAT) */
2121
+ boolValues?: Record<string, boolean>;
2122
+
2123
+ /** Variable reduced costs (optional, LP only) */
2124
+ reducedCosts?: Record<string, number>;
2125
+
2126
+ /** Constraint dual values / shadow prices (optional, LP only) */
2127
+ duals?: Record<string, number>;
2128
+
2129
+ /** Constraint slack values (optional) */
2130
+ slacks?: Record<string, number>;
2131
+
2132
+ /** Solve execution duration in seconds */
2133
+ runTime?: number;
2134
+
2135
+ /** Diagnostic solver logs / text outputs */
2136
+ logs?: string[];
2137
+
2138
+ /** Raw solver response object for inspection */
2139
+ raw?: any;
2140
+ }
2141
+ ```
2142
+
2143
+ #### Standardized `SolverStatus` Values
2144
+ - `"Optimal"`: Global optimal solution found (for MIP within tolerance, for LP proven optimal).
2145
+ - `"Feasible"`: Feasible solution found, but optimality not yet proven (e.g., hit time limit with an incumbent).
2146
+ - `"Infeasible"`: Model is proven mathematically infeasible.
2147
+ - `"Unbounded"`: Model objective is unbounded.
2148
+ - `"TimeLimit"`: Solver terminated due to `timeLimit` without proof of optimality.
2149
+ - `"IterationLimit"`: Solver terminated due to iteration limit.
2150
+ - `"Sat"` / `"Unsat"`: Satisfiable / unsatisfiable status returned by `check()` or SMT solvers.
2151
+ - `"Error"` / `"NotSolved"` / `"Unknown"`: Error, not yet solved, or unrecognized status. **Never convert Unknown/Error to Optimal.**
2152
+
2153
+ ---
2154
+
2155
+ ## 3. Step-by-Step Implementation Tutorials
2156
+
2157
+ ### Tutorial A: In-House Cloud REST Solver Adapter
2158
+
2159
+ Here is a complete, production-ready custom solver connecting to an HTTP microservice that accepts LP format strings on demand via `generateLpString(modelData)`:
2160
+
2161
+ ```typescript
2162
+ import {
2163
+ generateLpString,
2164
+ registerSolver,
2165
+ type Solver,
2166
+ type SolverOptions,
2167
+ type SolverResult,
2168
+ type SolverStatus,
2169
+ type SolveModelData,
2170
+ } from "@ohtaman/washi";
2171
+
2172
+ export interface CloudSolverConfig extends SolverOptions {
2173
+ endpoint?: string;
2174
+ apiKey?: string;
2175
+ }
2176
+
2177
+ export class CloudSolver implements Solver {
2178
+ public readonly name = "CloudSolver";
2179
+ public readonly version = "1.0.0";
2180
+ public readonly capabilities = {
2181
+ supportsMIP: true,
2182
+ supportsQP: false,
2183
+ };
2184
+
2185
+ private endpoint: string;
2186
+ private apiKey: string;
2187
+
2188
+ constructor(config: CloudSolverConfig = {}) {
2189
+ this.endpoint = config.endpoint || "https://solver.mycompany.internal/v1/solve";
2190
+ this.apiKey = config.apiKey || "";
2191
+ }
2192
+
2193
+ async solve(
2194
+ modelData: SolveModelData,
2195
+ options: SolverOptions = {}
2196
+ ): Promise<SolverResult> {
2197
+ const startTime = performance.now();
2198
+ const { lpString, nameToVar, nameToConstraint } = generateLpString(modelData);
2199
+
2200
+ let payload: any;
2201
+ try {
2202
+ const response = await fetch(this.endpoint, {
2203
+ method: "POST",
2204
+ headers: {
2205
+ "Content-Type": "application/json",
2206
+ ...(this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}),
2207
+ },
2208
+ body: JSON.stringify({
2209
+ lp: lpString,
2210
+ timeLimit: options.timeLimit,
2211
+ mipGap: options.mipGap,
2212
+ }),
2213
+ signal: options.signal,
2214
+ });
2215
+
2216
+ if (!response.ok) {
2217
+ throw new Error(`[CloudSolver] Server returned HTTP ${response.status}: ${await response.text()}`);
2218
+ }
2219
+
2220
+ payload = await response.json();
2221
+ } catch (err: any) {
2222
+ if (options.signal?.aborted || err?.name === "AbortError") {
2223
+ return {
2224
+ status: "Unknown",
2225
+ terminationStatus: "Interrupted",
2226
+ objectiveValue: null,
2227
+ values: {},
2228
+ runTime: (performance.now() - startTime) / 1000,
2229
+ };
2230
+ }
2231
+ throw err;
2232
+ }
2233
+
2234
+ const runTime = (performance.now() - startTime) / 1000;
2235
+
2236
+ const values: Record<string, number> = Object.create(null);
2237
+ for (const [k, val] of Object.entries((payload.values || {}) as Record<string, number>)) {
2238
+ values[nameToVar.get(k)?.name ?? k] = val;
2239
+ }
2240
+ const duals: Record<string, number> = Object.create(null);
2241
+ for (const [k, val] of Object.entries((payload.duals || {}) as Record<string, number>)) {
2242
+ duals[nameToConstraint.get(k)?.name ?? k] = val;
2243
+ }
2244
+ const slacks: Record<string, number> = Object.create(null);
2245
+ for (const [k, val] of Object.entries((payload.slacks || {}) as Record<string, number>)) {
2246
+ slacks[nameToConstraint.get(k)?.name ?? k] = val;
2247
+ }
2248
+
2249
+ const statusMap: Record<string, SolverStatus> = {
2250
+ OPTIMAL: "Optimal",
2251
+ FEASIBLE: "Feasible",
2252
+ INFEASIBLE: "Infeasible",
2253
+ UNBOUNDED: "Unbounded",
2254
+ TIME_LIMIT: "TimeLimit",
2255
+ TIMELIMIT: "TimeLimit",
2256
+ ITERATION_LIMIT: "IterationLimit",
2257
+ ERROR: "Error",
2258
+ NOT_SOLVED: "NotSolved",
2259
+ SAT: "Sat",
2260
+ UNSAT: "Unsat",
2261
+ };
2262
+ const rawStatus: SolverStatus = statusMap[String(payload.status).toUpperCase()] ?? "Unknown";
2263
+ const hasIncumbent =
2264
+ (typeof payload.objectiveValue === "number" && Number.isFinite(payload.objectiveValue)) ||
2265
+ Object.values(values).some((val) => typeof val === "number" && Number.isFinite(val));
2266
+ const isLimitStop = rawStatus === "TimeLimit" || rawStatus === "IterationLimit";
2267
+ const status: SolverStatus = isLimitStop && hasIncumbent ? "Feasible" : rawStatus;
2268
+
2269
+ return {
2270
+ status,
2271
+ terminationStatus: rawStatus,
2272
+ objectiveValue: payload.objectiveValue ?? null,
2273
+ values,
2274
+ duals,
2275
+ slacks,
2276
+ runTime: payload.solveDuration || runTime,
2277
+ logs: payload.logs || [],
2278
+ raw: payload,
2279
+ };
2280
+ }
2281
+ }
2282
+
2283
+ // Auto-register so `import "my-cloud-solver"` enables `solver: "cloud"`
2284
+ export function registerCloudSolver(): void {
2285
+ registerSolver("cloud", (opts) => new CloudSolver(opts));
2286
+ }
2287
+ registerCloudSolver();
2288
+ ```
2289
+
2290
+ ---
2291
+
2292
+ ### Tutorial B: Registering and Using the Custom Solver
2293
+
2294
+ Once implemented, the custom solver can be selected in three different ways:
2295
+
2296
+ #### Option 1: By Registered Name String
2297
+ ```typescript
2298
+ import { Model } from "@ohtaman/washi";
2299
+ import "./cloud-solver.js"; // Auto-registers "cloud"
2300
+
2301
+ const model = new Model({ name: "SupplyChain", solver: "cloud" });
2302
+ // Or model.solve("cloud")
2303
+ ```
2304
+
2305
+ #### Option 2: By Passing the Solver Instance Directly
2306
+ ```typescript
2307
+ import { Model } from "@ohtaman/washi";
2308
+ import { CloudSolver } from "./cloud-solver.js";
2309
+
2310
+ const customInstance = new CloudSolver({
2311
+ endpoint: "https://dedicated-gpu-cluster.internal/solve",
2312
+ apiKey: "secret-token",
2313
+ });
2314
+
2315
+ const model = new Model({ name: "FacilityLocation", solver: customInstance });
2316
+ const status = await model.solve();
2317
+ ```
2318
+
2319
+ #### Option 3: Setting Global Default Solver
2320
+ ```typescript
2321
+ import { setDefaultSolver, Model } from "@ohtaman/washi";
2322
+ import { CloudSolver } from "./cloud-solver.js";
2323
+
2324
+ setDefaultSolver(() => new CloudSolver());
2325
+
2326
+ // All subsequent models use CloudSolver by default
2327
+ const model = new Model("DefaultedModel");
2328
+ ```
2329
+
2330
+ ---
2331
+
2332
+ ## 4. Distribution: Packaging as a Plugin
2333
+
2334
+ To publish your solver as an independent npm package (e.g. `@my-org/washi-solver-cplex`):
2335
+
2336
+ ### 1. `package.json` Exports Structure
2337
+ ```json
2338
+ {
2339
+ "name": "@my-org/washi-solver-cplex",
2340
+ "version": "1.0.0",
2341
+ "type": "module",
2342
+ "main": "./dist/lib/index.js",
2343
+ "types": "./dist/types/index.d.ts",
2344
+ "exports": {
2345
+ ".": {
2346
+ "types": "./dist/types/index.d.ts",
2347
+ "import": "./dist/lib/index.js",
2348
+ "default": "./dist/lib/index.js"
2349
+ },
2350
+ "./browser": {
2351
+ "types": "./dist/types/index.d.ts",
2352
+ "import": "./dist/browser/index.js",
2353
+ "default": "./dist/browser/index.js"
2354
+ },
2355
+ "./browser/index.min.js": "./dist/browser/index.min.js"
2356
+ },
2357
+ "peerDependencies": {
2358
+ "@ohtaman/washi": ">=0.1.0"
2359
+ }
2360
+ }
2361
+ ```
2362
+
2363
+ ### 2. Browser IIFE Build
2364
+ When building the browser standalone script (`.min.js`), configure your bundler (Vite or Rollup) to:
2365
+ - Output format: `iife`
2366
+ - Global name: `WashiCplex`
2367
+ - Rely on `Symbol.for("washi.solverRegistry")` so that `<script src="washi.min.js"></script><script src="washi-cplex.min.js"></script>` automatically connects.
2368
+
2369
+ ---
2370
+
2371
+ ## 5. Testing Your Custom Solver
2372
+
2373
+ You can test your solver using Vitest or Jest without bundling:
2374
+
2375
+ ```typescript
2376
+ import { describe, it, expect, vi } from "vitest";
2377
+ import { Model, expr, maximize, registerSolver, listAvailableSolvers } from "@ohtaman/washi";
2378
+ import { CloudSolver } from "./cloud-solver.js";
2379
+
2380
+ describe("Custom Solver Integration", () => {
2381
+ it("registers in available solvers list", () => {
2382
+ registerSolver("cloud-test", () => new CloudSolver());
2383
+ expect(listAvailableSolvers()).toContain("cloud-test");
2384
+ });
2385
+
2386
+ it("propagates primal values and duals into WaSHI Model", async () => {
2387
+ const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(
2388
+ new Response(
2389
+ JSON.stringify({
2390
+ status: "OPTIMAL",
2391
+ objectiveValue: 34,
2392
+ values: { x: 2, y: 10 },
2393
+ duals: { budget: 2 },
2394
+ }),
2395
+ { status: 200, headers: { "Content-Type": "application/json" } }
2396
+ )
2397
+ );
2398
+
2399
+ try {
2400
+ const model = new Model({ solver: new CloudSolver() });
2401
+ const x = model.continuous("x", { lb: 0, ub: 10 });
2402
+ const y = model.continuous("y", { lb: 0, ub: 10 });
2403
+ model.objective(maximize(expr`2 * ${x} + 3 * ${y}`));
2404
+ model.constraint(expr`${x} + ${y} <= 12`, "budget");
2405
+
2406
+ const status = await model.solve();
2407
+ expect(status).toBe("Optimal");
2408
+ expect(x.value).toBe(2);
2409
+ const c1 = model.getConstraintByName("budget");
2410
+ expect(c1?.dual).toBe(2);
2411
+ } finally {
2412
+ fetchSpy.mockRestore();
2413
+ }
2414
+ });
2415
+ });
2416
+ ```
2417
+
2418
+ ---
2419
+
2420
+ # Examples & API Finder
2421
+
2422
+ Explore runnable optimization and logic formulations built with WaSHI. Every example can be executed directly inside its documentation page, or edited in the [Playground](https://ohtaman.github.io/washi/demo/).
2423
+
2424
+ ## Examples
2425
+
2426
+ | Example | Problem Class | Solver | Key WaSHI APIs | Description | Code Editor |
2427
+ | :--- | :--- | :--- | :--- | :--- | :--- |
2428
+ | **[Production Planning](docs/examples/production.md)** | LP / MIP | HiGHS | `m.integer()`, `m.constraint()`, `expr` | Basic variables, tagged template literal equations, and maximizing profit under resource limits. | [Playground](https://ohtaman.github.io/washi/demo/?preset=production) |
2429
+ | **[0-1 Knapsack](docs/examples/knapsack.md)** | Binary MIP | HiGHS | `m.binary(keys)`, `sum()`, `m.constraint()` | 1D dictionary of `Binary` variables and linear aggregation over item arrays. | [Playground](https://ohtaman.github.io/washi/demo/?preset=knapsack) |
2430
+ | **[Transportation Network](docs/examples/transportation.md)** | LP | HiGHS | `m.continuous(rows, cols)`, `dot()`, `m.constraint()` | 2D matrix indexing (`ship[factory][store]`) and row/column flow balance constraints. | [Playground](https://ohtaman.github.io/washi/demo/?preset=transportation) |
2431
+ | **[Diet & Sensitivity Analysis](docs/examples/diet-sensitivity.md)** | LP | HiGHS | `constr.dual`, `constr.slack`, `var.reducedCost` | Reading shadow prices (bottlenecks), constraint surpluses, and reduced costs after solving. | [Playground](https://ohtaman.github.io/washi/demo/?preset=diet_sensitivity) |
2432
+ | **[Staff Shift Scheduling](docs/examples/shift-scheduling.md)** | Binary MIP + Unsat Core | HiGHS + Z3 | `m.binary(staff, days)`, `check()`, `unsatCore` | 5×7 weekly roster planner (coverage, senior duty, max shifts, day-off requests) and diagnosing conflicting vacation requests with Z3 `unsatCore`. | [Playground](https://ohtaman.github.io/washi/demo/?preset=shift_scheduling) |
2433
+ | **[E-Commerce Coupon Optimizer](docs/examples/ecommerce-coupons.md)** | Indicator MIP | HiGHS | `m.indicator()`, `sum()`, `m.constraint()` | Checkout optimizer using `m.indicator()` for minimum spend thresholds, exclusive vs. stackable rules, and smart filler add-on suggestions. | [Playground](https://ohtaman.github.io/washi/demo/?preset=cart_coupon) |
2434
+ | **[Portfolio Optimization](docs/examples/portfolio-qp.md)** | Convex QP | HiGHS | `m.continuous()`, `m.constraint()`, `dot()`, `QuadExpr` | Markowitz mean-variance risk minimization ($w^T \Sigma w$) using `QuadExpr` subject to return targets. | [Playground](https://ohtaman.github.io/washi/demo/?preset=portfolio_qp) |
2435
+ | **[SMT Security Policies](docs/examples/smt-security.md)** | SMT Logic | Z3 | `m.bool()`, `m.constraint()`, `m.check()` | Propositional variables (`BoolVar`) and logical connectives (`implies`, `and`, `not`, `xor`) mixed with integer bounds. | [Playground](https://ohtaman.github.io/washi/demo/?preset=smt_sat_scheduling) |
2436
+ | **[Conflict Diagnosis (`unsatCore`)](docs/examples/unsat-core.md)** | Unsat Core | Z3 | `m.constraint()`, `m.check()`, `m.unsatCore` | Extracting a conflicting constraint subset (`unsatCore`) when staffing and budget rules contradict each other. | [Playground](https://ohtaman.github.io/washi/demo/?preset=conflict_diagnosis_unsat) |
2437
+
2438
+ ## Find by WaSHI API (Reverse Index)
2439
+
2440
+ Looking for a specific modeling method or solver capability? Jump straight to the example that demonstrates it:
2441
+
2442
+ | Feature / API | Description | Example Page | Node.js Source |
2443
+ | :--- | :--- | :--- | :--- |
2444
+ | ``m.constraint(expr`...`)`` | Tagged template literal constraints | [Production Planning](docs/examples/production.md) | [`01-production-planning.ts`](https://github.com/ohtaman/washi/blob/main/examples/node/01-production-planning.ts) |
2445
+ | `m.binary(keys)` / `m.continuous(keys)` | 1D dictionary of indexed variables | [0-1 Knapsack](docs/examples/knapsack.md) | [`02-knapsack-problem.ts`](https://github.com/ohtaman/washi/blob/main/examples/node/02-knapsack-problem.ts) |
2446
+ | `m.continuous(rows, cols)` / `m.binary(rows, cols)` | 2D matrix of indexed variables | [Transportation Network](docs/examples/transportation.md), [Shift Scheduling](docs/examples/shift-scheduling.md) | [`09-transportation-problem.ts`](https://github.com/ohtaman/washi/blob/main/examples/node/09-transportation-problem.ts) |
2447
+ | `dot(matrix, vars2d)` / `dot(vector, vars)` | Fast vector/matrix dot product | [Transportation Network](docs/examples/transportation.md), [Portfolio Optimization](docs/examples/portfolio-qp.md) | [`09-transportation-problem.ts`](https://github.com/ohtaman/washi/blob/main/examples/node/09-transportation-problem.ts) |
2448
+ | `constr.dual` & `constr.slack` | Shadow prices & constraint surpluses | [Diet & Sensitivity Analysis](docs/examples/diet-sensitivity.md) | [`10-diet-sensitivity.ts`](https://github.com/ohtaman/washi/blob/main/examples/node/10-diet-sensitivity.ts) |
2449
+ | `var.reducedCost` | Variable reduced costs in LP | [Diet & Sensitivity Analysis](docs/examples/diet-sensitivity.md) | [`10-diet-sensitivity.ts`](https://github.com/ohtaman/washi/blob/main/examples/node/10-diet-sensitivity.ts) |
2450
+ | `m.indicator()` | Big-M conditional constraints (`binVar => constr`) | [E-Commerce Coupon Optimizer](docs/examples/ecommerce-coupons.md) | [`12-ecommerce-coupon-optimizer.ts`](https://github.com/ohtaman/washi/blob/main/examples/node/12-ecommerce-coupon-optimizer.ts) |
2451
+ | `new QuadExpr()` | Convex quadratic objectives ($x^T Q x$) | [Portfolio Optimization](docs/examples/portfolio-qp.md) | [`03-portfolio-qp.ts`](https://github.com/ohtaman/washi/blob/main/examples/node/03-portfolio-qp.ts) |
2452
+ | `m.bool()` & `.implies()` | Propositional logic & SMT rules | [SMT Security Policies](docs/examples/smt-security.md) | [`04-smt-security-rules.ts`](https://github.com/ohtaman/washi/blob/main/examples/node/04-smt-security-rules.ts) |
2453
+ | `m.check()` & `m.unsatCore` | Feasibility check & conflict diagnosis | [Conflict Diagnosis](docs/examples/unsat-core.md), [Shift Scheduling](docs/examples/shift-scheduling.md) | [`05-unsat-core-diagnosis.ts`](https://github.com/ohtaman/washi/blob/main/examples/node/05-unsat-core-diagnosis.ts) |
2454
+ | `solver: "glpk"` | Solving LP/MIP with GLPK plugin | [GLPK Solver Guide](docs/solvers/glpk.md) | [`08-glpk-solver-plugin.ts`](https://github.com/ohtaman/washi/blob/main/examples/node/08-glpk-solver-plugin.ts) |
2455
+
2456
+ ## Running Examples Locally in Node.js
2457
+
2458
+ All standalone scripts are located in [`examples/node/`](https://github.com/ohtaman/washi/tree/main/examples/node):
2459
+
2460
+ ```bash
2461
+ npx tsx examples/node/01-production-planning.ts
2462
+ npx tsx examples/node/02-knapsack-problem.ts
2463
+ npx tsx examples/node/03-portfolio-qp.ts
2464
+ npx tsx examples/node/04-smt-security-rules.ts
2465
+ npx tsx examples/node/05-unsat-core-diagnosis.ts
2466
+ npx tsx examples/node/07-hybrid-smt-mip.ts
2467
+ npx tsx examples/node/08-glpk-solver-plugin.ts
2468
+ npx tsx examples/node/09-transportation-problem.ts
2469
+ npx tsx examples/node/10-diet-sensitivity.ts
2470
+ npx tsx examples/node/11-shift-scheduling.ts
2471
+ npx tsx examples/node/12-ecommerce-coupon-optimizer.ts
2472
+ ```
2473
+
2474
+ ---
2475
+
2476
+ # Production Planning (LP / MIP)
2477
+
2478
+ | Problem Class | Solver | Key APIs | Links |
2479
+ | :--- | :--- | :--- | :--- |
2480
+ | Linear / Integer Programming (LP / MIP) | HiGHS (default) | `m.integer()`, `m.constraint()`, `expr` | <ul><li><a href="https://ohtaman.github.io/washi/demo/?preset=production">Open in Playground</a></li><li><a href="https://github.com/ohtaman/washi/blob/main/examples/node/01-production-planning.ts">Source Code</a></li></ul> |
2481
+
2482
+ Production planning is a classic Linear Programming problem: determine the optimal quantity of each product to manufacture in order to maximize total profit while respecting resource constraints (raw materials, machine time, labor hours).
2483
+
2484
+ ## Mathematical Formulation
2485
+
2486
+ Let $x$ be the number of chairs and $y$ be the number of tables.
2487
+
2488
+ $$\begin{aligned}
2489
+ \text{Maximize} \quad & 45x + 80y & (\text{Profit}) \\
2490
+ \text{Subject to} \quad & 5x + 20y \le 400 & (\text{Mahogany Wood limit}) \\
2491
+ & 10x + 15y \le 450 & (\text{Carpentry Labor limit}) \\
2492
+ & x, y \ge 0, \quad x, y \in \mathbb{Z} & (\text{Integer Quantities})
2493
+ \end{aligned}$$
2494
+
2495
+ ## Implementation in WaSHI
2496
+
2497
+ ```typescript
2498
+ import { Model, expr, maximize } from "@ohtaman/washi";
2499
+
2500
+ async function runProductionOptimization() {
2501
+ const m = new Model("FactoryProduction");
2502
+
2503
+ // Decision variables: non-negative integer quantities
2504
+ const chairs = m.integer("chairs", { lb: 0 });
2505
+ const tables = m.integer("tables", { lb: 0 });
2506
+
2507
+ // Objective: Maximize total profit ($45 per chair, $80 per table)
2508
+ m.objective(maximize(expr`45 * ${chairs} + 80 * ${tables}`));
2509
+
2510
+ // Resource constraints: declarative formulas via expr and fluent constraint()
2511
+ m.constraint(expr`5 * ${chairs} + 20 * ${tables} <= 400`, "mahogany_limit")
2512
+ .constraint(expr`10 * ${chairs} + 15 * ${tables} <= 450`, "labor_limit");
2513
+
2514
+ const status = await m.solve();
2515
+
2516
+ console.log(`Status: ${status}`);
2517
+ console.log(`Optimal Profit: $${m.objectiveValue}`);
2518
+ console.log(`Chairs: ${chairs.value}, Tables: ${tables.value}`);
2519
+ }
2520
+
2521
+ runProductionOptimization();
2522
+ ```
2523
+
2524
+ ---
2525
+
2526
+ # 0-1 Knapsack Problem (Binary MIP)
2527
+
2528
+ | Problem Class | Solver | Key APIs | Links |
2529
+ | :--- | :--- | :--- | :--- |
2530
+ | 0-1 Binary MIP | HiGHS (default) | `m.binary()`, `m.constraint()`, `expr`, `maximize`, `dot` | <ul><li><a href="https://ohtaman.github.io/washi/demo/?preset=knapsack">Open in Playground</a></li><li><a href="https://github.com/ohtaman/washi/blob/main/examples/node/02-knapsack-problem.ts">Source Code</a></li></ul> |
2531
+
2532
+ The 0-1 Knapsack problem is an essential combinatorial optimization problem: given a set of items, each with a weight and a value, determine which items to include in a collection so that the total weight does not exceed a given limit and the total value is maximized.
2533
+
2534
+ ## Mathematical Formulation
2535
+
2536
+ Let $x_i \in \{0, 1\}$ be binary decision variables indicating whether item $i$ is selected ($x_i = 1$) or not ($x_i = 0$):
2537
+
2538
+ $$\begin{aligned}
2539
+ \text{Maximize} \quad & \sum_{i} v_i x_i & (\text{Total Value}) \\
2540
+ \text{Subject to} \quad & \sum_{i} w_i x_i \le W & (\text{Weight Capacity}) \\
2541
+ & x_i \in \{0, 1\} & (\text{Binary Decisions})
2542
+ \end{aligned}$$
2543
+
2544
+ ## Implementation in WaSHI
2545
+
2546
+ ```typescript
2547
+ import { Model, maximize, dot, expr } from "@ohtaman/washi";
2548
+
2549
+ async function solveKnapsack() {
2550
+ const m = new Model("Knapsack");
2551
+
2552
+ const items = [
2553
+ { name: "Laptop", weight: 3, value: 50 },
2554
+ { name: "Camera", weight: 2, value: 40 },
2555
+ { name: "Drone", weight: 4, value: 65 },
2556
+ { name: "FirstAid", weight: 1, value: 15 },
2557
+ { name: "Tent", weight: 5, value: 70 },
2558
+ ];
2559
+ const maxCapacity = 7; // kg
2560
+
2561
+ // Create binary variables: x[name] (0 or 1)
2562
+ const x = m.binary(items.map((it) => it.name));
2563
+
2564
+ const values = Object.fromEntries(items.map((it) => [it.name, it.value]));
2565
+ const weights = Object.fromEntries(items.map((it) => [it.name, it.weight]));
2566
+
2567
+ // Maximize total value via dot product
2568
+ m.objective(maximize(dot(values, x)));
2569
+
2570
+ // Weight capacity constraint using natural expr and constraint() syntax
2571
+ m.constraint(expr`${dot(weights, x)} <= ${maxCapacity}`, "capacity_limit");
2572
+
2573
+ const status = await m.solve();
2574
+
2575
+ console.log(`Status: ${status}`);
2576
+ console.log(`Max Total Value: $${m.objectiveValue}`);
2577
+ console.log("Selected Items:", m.getValues(x));
2578
+ }
2579
+
2580
+ solveKnapsack();
2581
+ ```
2582
+
2583
+ ---
2584
+
2585
+ # Transportation & Allocation Network (LP)
2586
+
2587
+ | Problem Class | Solver | Key APIs | Links |
2588
+ | :--- | :--- | :--- | :--- |
2589
+ | Linear Programming (LP) | HiGHS (default) | `m.continuous()`, `m.constraint()`, `expr`, `minimize`, `dot` | <ul><li><a href="https://ohtaman.github.io/washi/demo/?preset=transportation">Open in Playground</a></li><li><a href="https://github.com/ohtaman/washi/blob/main/examples/node/09-transportation-problem.ts">Source Code</a></li></ul> |
2590
+
2591
+ The transportation problem is one of the most fundamental network optimization models: determine how many units to ship from each supply origin (factory) to each destination (store) to minimize total shipping cost while respecting factory supply limits and meeting store demands.
2592
+
2593
+ This example demonstrates how to construct a **2D matrix of decision variables** using `m.continuous(factories, stores)` and write aggregated constraints declaratively via `dot()` and `m.constraint(expr`...`)`.
2594
+
2595
+ ## Problem Inputs
2596
+
2597
+ We have **3 factories** (`Tokyo`, `Nagoya`, `Osaka`) shipping to **4 regional stores** (`North`, `East`, `West`, `South`):
2598
+
2599
+ | Factory \ Store | North ($/unit) | East ($/unit) | West ($/unit) | South ($/unit) | Supply Capacity |
2600
+ | :--- | :---: | :---: | :---: | :---: | :---: |
2601
+ | **Tokyo** | $4 | $2 | $7 | $8 | **120** units |
2602
+ | **Nagoya** | $6 | $4 | $3 | $5 | **100** units |
2603
+ | **Osaka** | $9 | $7 | $2 | $3 | **80** units |
2604
+ | **Store Demand** | **70** units | **90** units | **60** units | **60** units | Total: 280 / 300 |
2605
+
2606
+ ## Mathematical Formulation
2607
+
2608
+ Let $x_{f, s} \ge 0$ be the quantity shipped from factory $f \in F$ to store $s \in S$, and $c_{f, s}$ be the unit shipping cost.
2609
+
2610
+ $$\begin{aligned}
2611
+ \text{Minimize} \quad & \sum_{f \in F} \sum_{s \in S} c_{f,s} \, x_{f,s} \\
2612
+ \text{Subject to} \quad & \sum_{s \in S} x_{f,s} \le \text{Supply}_f \quad \forall f \in F \quad (\text{Factory capacity}) \\
2613
+ & \sum_{f \in F} x_{f,s} \ge \text{Demand}_s \quad \forall s \in S \quad (\text{Store demand}) \\
2614
+ & x_{f,s} \ge 0 \quad \forall f \in F, s \in S
2615
+ \end{aligned}$$
2616
+
2617
+ ## Implementation in WaSHI
2618
+
2619
+ ```typescript
2620
+ import { Model, sum, dot, minimize, expr } from "@ohtaman/washi";
2621
+
2622
+ const factories = ["Tokyo", "Nagoya", "Osaka"] as const;
2623
+ const stores = ["North", "East", "West", "South"] as const;
2624
+
2625
+ const shippingCost = {
2626
+ Tokyo: { North: 4, East: 2, West: 7, South: 8 },
2627
+ Nagoya: { North: 6, East: 4, West: 3, South: 5 },
2628
+ Osaka: { North: 9, East: 7, West: 2, South: 3 },
2629
+ };
2630
+
2631
+ const supplies = { Tokyo: 120, Nagoya: 100, Osaka: 80 };
2632
+ const demands = { North: 70, East: 90, West: 60, South: 60 };
2633
+
2634
+ async function runTransportation() {
2635
+ const m = new Model("TransportationNetwork");
2636
+
2637
+ // 1. Create 2D continuous variables: ship[factory][store] >= 0
2638
+ const ship = m.continuous([...factories], [...stores], { lb: 0 });
2639
+
2640
+ // 2. Objective: Minimize total shipping cost across all routes via 2D dot product
2641
+ m.objective(minimize(dot(shippingCost, ship)));
2642
+
2643
+ // 3. Supply constraints (row sums): outgoing from f <= supplies[f]
2644
+ for (const f of factories) {
2645
+ m.constraint(
2646
+ expr`${sum(stores, (s) => ship[f][s])} <= ${supplies[f]}`,
2647
+ `supply_${f}`
2648
+ );
2649
+ }
2650
+
2651
+ // 4. Demand constraints (column sums): incoming to s >= demands[s]
2652
+ for (const s of stores) {
2653
+ m.constraint(
2654
+ expr`${sum(factories, (f) => ship[f][s])} >= ${demands[s]}`,
2655
+ `demand_${s}`
2656
+ );
2657
+ }
2658
+
2659
+ const status = await m.solve();
2660
+ console.log(`Status: ${status}, Total Shipping Cost: $${m.objectiveValue}`);
2661
+ for (const f of factories) {
2662
+ const routes = stores
2663
+ .map((s) => `${s}:${Math.round(ship[f][s].value ?? 0)}`)
2664
+ .join(", ");
2665
+ console.log(` ${f.padEnd(7)} -> ${routes}`);
2666
+ }
2667
+ }
2668
+
2669
+ runTransportation();
2670
+ ```
2671
+
2672
+ ---
2673
+
2674
+ # Diet Problem & Sensitivity Analysis (LP Duals & Slacks)
2675
+
2676
+ | Problem Class | Solver | Key APIs | Links |
2677
+ | :--- | :--- | :--- | :--- |
2678
+ | Linear Programming (LP) | HiGHS (default) | `m.continuous()`, `m.constraint()`, `expr`, `minimize`, `dot` | <ul><li><a href="https://ohtaman.github.io/washi/demo/?preset=diet_sensitivity">Open in Playground</a></li><li><a href="https://github.com/ohtaman/washi/blob/main/examples/node/10-diet-sensitivity.ts">Source Code</a></li></ul> |
2679
+
2680
+ When solving a Linear Programming model, the optimal variable values (`var.value`) are only half the story. In practice, decision-makers need to know:
2681
+
2682
+ 1. **Bottlenecks (`constr.dual` — Shadow Price):** Which minimum requirements are actively driving up the total cost, and by how much per unit?
2683
+ 2. **Surpluses (`constr.slack`):** Which constraints have room to spare?
2684
+ 3. **Reduced Costs (`var.reducedCost`):** For items not selected in the optimal plan, how much cheaper would they need to become before entering the solution?
2685
+
2686
+ ## Problem Inputs
2687
+
2688
+ We choose daily servings ($0 \le x_i \le 4$) among 6 food items to meet 4 daily nutritional minimums (`Calories >= 2000 kcal`, `Protein >= 105 g`, `Calcium >= 1000 mg`, `Iron >= 18 mg`) at minimum cost:
2689
+
2690
+ | Food Item | Cost / Serving | Calories (kcal) | Protein (g) | Calcium (mg) | Iron (mg) | Max Servings |
2691
+ | :--- | :---: | :---: | :---: | :---: | :---: | :---: |
2692
+ | **Oats** | $0.60 | 150 | 5 | 20 | 1.8 | 4.0 |
2693
+ | **Chicken** | $2.40 | 220 | 32 | 15 | 1.2 | 4.0 |
2694
+ | **Eggs** | $0.80 | 140 | 12 | 50 | 1.7 | 4.0 |
2695
+ | **Milk** | $0.90 | 120 | 8 | 280 | 0.1 | 4.0 |
2696
+ | **Spinach** | $1.50 | 40 | 4 | 120 | 3.5 | 4.0 |
2697
+ | **Salmon** | $3.60 | 200 | 25 | 20 | 0.8 | 4.0 |
2698
+
2699
+ ## Implementation in WaSHI
2700
+
2701
+ ```typescript
2702
+ import { Model, minimize, dot, expr } from "@ohtaman/washi";
2703
+
2704
+ const foods = [
2705
+ { name: "Oats", cost: 0.60, calories: 150, protein: 5, calcium: 20, iron: 1.8 },
2706
+ { name: "Chicken", cost: 2.40, calories: 220, protein: 32, calcium: 15, iron: 1.2 },
2707
+ { name: "Eggs", cost: 0.80, calories: 140, protein: 12, calcium: 50, iron: 1.7 },
2708
+ { name: "Milk", cost: 0.90, calories: 120, protein: 8, calcium: 280, iron: 0.1 },
2709
+ { name: "Spinach", cost: 1.50, calories: 40, protein: 4, calcium: 120, iron: 3.5 },
2710
+ { name: "Salmon", cost: 3.60, calories: 200, protein: 25, calcium: 20, iron: 0.8 },
2711
+ ];
2712
+
2713
+ async function runDietSensitivity() {
2714
+ const m = new Model("DietSensitivity");
2715
+
2716
+ // Continuous servings per day (0 to 4 servings max per food)
2717
+ const srv = m.continuous(foods.map((f) => f.name), { lb: 0, ub: 4 });
2718
+
2719
+ const costs = Object.fromEntries(foods.map((f) => [f.name, f.cost]));
2720
+ const cals = Object.fromEntries(foods.map((f) => [f.name, f.calories]));
2721
+ const prots = Object.fromEntries(foods.map((f) => [f.name, f.protein]));
2722
+ const calcs = Object.fromEntries(foods.map((f) => [f.name, f.calcium]));
2723
+ const irons = Object.fromEntries(foods.map((f) => [f.name, f.iron]));
2724
+
2725
+ // Objective: Minimize total daily cost via dot product
2726
+ m.objective(minimize(dot(costs, srv)));
2727
+
2728
+ // Nutritional constraints defined declaratively with expr and constraint()
2729
+ m.constraint(expr`${dot(cals, srv)} >= 2000`, "min_calories")
2730
+ .constraint(expr`${dot(prots, srv)} >= 105`, "min_protein")
2731
+ .constraint(expr`${dot(calcs, srv)} >= 1000`, "min_calcium")
2732
+ .constraint(expr`${dot(irons, srv)} >= 18`, "min_iron");
2733
+
2734
+ const status = await m.solve();
2735
+
2736
+ console.log(`Status: ${status}, Minimum Daily Cost: $${m.objectiveValue?.toFixed(2)}`);
2737
+ console.log("--- Food Servings & Reduced Costs ---");
2738
+ for (const f of foods) {
2739
+ const v = srv[f.name];
2740
+ console.log(` ${f.name.padEnd(8)}: value=${v.value?.toFixed(2)}, reducedCost=${v.reducedCost?.toFixed(4)}`);
2741
+ }
2742
+ console.log("--- Nutritional Constraints (Dual & Slack) ---");
2743
+ for (const name of ["min_calories", "min_protein", "min_calcium", "min_iron"]) {
2744
+ const c = m.getConstraintByName(name);
2745
+ if (c) {
2746
+ console.log(` ${c.name.padEnd(13)}: dual=$${c.dual?.toFixed(4)}, slack=${Math.abs(c.slack ?? 0).toFixed(2)}`);
2747
+ }
2748
+ }
2749
+ }
2750
+
2751
+ runDietSensitivity();
2752
+ ```
2753
+
2754
+ ## Interpretation of Results
2755
+
2756
+ - **Bottleneck constraints (`slack == 0`, `dual > 0`):** Calories (`2000 kcal`) and Iron (`18 mg`) are tight at their lower bounds. Increasing the daily Iron requirement by 1 mg increases the minimum daily diet cost by **$0.3241**.
2757
+ - **Constraints with surplus (`slack > 0`, `dual == 0`):** Protein (`+46.46 g` surplus) and Calcium (`+483.02 mg` surplus) are already exceeded with room to spare, so their shadow prices are `$0`.
2758
+ - **Reduced cost (`Salmon`):** Unselected `Salmon` (`0.00` servings) has a reduced cost of `+1.5125`, meaning its price per serving would need to drop by at least **$1.51** before it enters the optimal daily menu.
2759
+
2760
+ ---
2761
+
2762
+ # Staff Shift Scheduling & Conflict Diagnosis (MIP + Z3)
2763
+
2764
+ | Problem Class | Solver | Key APIs | Links |
2765
+ | :--- | :--- | :--- | :--- |
2766
+ | Binary MIP & SMT Unsat Core | HiGHS + Z3 | `m.binary(staff, days)`, `m.constraint()`, `expr`, `minimize`, `dot`, `m.check()` | <ul><li><a href="https://ohtaman.github.io/washi/demo/?preset=shift_scheduling">Playground (Roster)</a></li><li><a href="https://ohtaman.github.io/washi/demo/?preset=shift_conflict_unsat">Playground (Conflict)</a></li><li><a href="https://github.com/ohtaman/washi/blob/main/examples/node/11-shift-scheduling.ts">Source Code</a></li></ul> |
2767
+
2768
+ Staff shift scheduling is a classic real-world application of Mixed-Integer Programming in SaaS and workforce management tools. However, in real applications, **staff vacation requests frequently collide with minimum coverage or leadership rules**, causing the solver to return `Infeasible`.
2769
+
2770
+ This example demonstrates a practical production workflow:
2771
+ 1. **Try HiGHS MIP Optimization First:** Solve the shift model under the given vacation requests and operational constraints.
2772
+ 2. **Accept Solution if Optimal:** If a feasible roster is found, display the schedule and minimized payroll cost without unnecessary diagnostic overhead.
2773
+ 3. **Diagnose with Z3 `unsatCore` Only When Infeasible:** If HiGHS reports `Infeasible`, diagnose the **exact same model and constraints** with Z3 to isolate the conflicting constraint subset (`unsatCore`), pinpointing which day's coverage rules and which staff requests collided.
2774
+
2775
+ > [!IMPORTANT]
2776
+ > **Vacation Requests are Hard Constraints:**
2777
+ > In this model, vacation requests are enforced as strict hard constraints (`shift[staff][day] == 0`), not soft constraints with penalty relaxation. When requests collide with mandatory staffing requirements, the model is strictly infeasible.
2778
+
2779
+ ## Scenario & Rules
2780
+
2781
+ We schedule **5 staff members** (`Alice`, `Bob`, `Charlie`, `Diana`, `Evan`) across **7 days** (`Mon`–`Sun`):
2782
+
2783
+ - **Daily Headcount:** `Mon–Thu`: 3 staff, `Fri`: 4 staff, `Sat`: 3 staff, `Sun`: 2 staff.
2784
+ - **Senior Duty:** At least 1 senior staff member (`Alice` or `Bob`) must be on duty every day.
2785
+ - **Work Limit:** Each staff member works at most **5 days** per week.
2786
+ - **Weekend Premium:** Weekend shifts (`Sat`, `Sun`) incur an extra `+$30` shift premium on top of base daily wage.
2787
+
2788
+ We evaluate two distinct, named scenarios with different vacation inputs to illustrate the full workflow:
2789
+ - **Scenario A (Balanced Requests — Feasible):** Alice (Sun), Bob (Mon), Charlie (Sat), Diana (Wed). All constraints can be satisfied simultaneously. HiGHS outputs an optimal roster.
2790
+ - **Scenario B (Conflicting Requests — Infeasible & Diagnosis):** Alice (Sat), Bob (Sat), Charlie (Sat), Diana (Wed). Because both senior leaders (Alice and Bob) request Saturday off simultaneously, the rule requiring at least 1 senior on duty every day cannot be satisfied. HiGHS reports `Infeasible`, triggering Z3 conflict diagnosis.
2791
+
2792
+ ## Implementation in WaSHI
2793
+
2794
+ ```typescript
2795
+ import { Model, sum, dot, minimize, expr } from "@ohtaman/washi";
2796
+ import "@ohtaman/washi/solvers/z3";
2797
+
2798
+ const days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] as const;
2799
+ type Day = (typeof days)[number];
2800
+
2801
+ const staffList = [
2802
+ { name: "Alice", isSenior: true, dailyWage: 220, maxShifts: 5 },
2803
+ { name: "Bob", isSenior: true, dailyWage: 210, maxShifts: 5 },
2804
+ { name: "Charlie", isSenior: false, dailyWage: 160, maxShifts: 5 },
2805
+ { name: "Diana", isSenior: false, dailyWage: 160, maxShifts: 5 },
2806
+ { name: "Evan", isSenior: false, dailyWage: 150, maxShifts: 5 },
2807
+ ];
2808
+
2809
+ const requiredCoverage: Record<Day, number> = {
2810
+ Mon: 3, Tue: 3, Wed: 3, Thu: 3, Fri: 4, Sat: 3, Sun: 2,
2811
+ };
2812
+
2813
+ function buildShiftModel(name: string, dayOffRequests: Array<{ staff: string; day: Day }>) {
2814
+ const m = new Model(name);
2815
+
2816
+ // 2D binary matrix: shift[staff][day] ∈ {0, 1}
2817
+ const shift = m.binary(staffList.map((s) => s.name), [...days]);
2818
+
2819
+ // Objective: Minimize weekly payroll cost via 2D dot product
2820
+ const costMatrix = Object.fromEntries(
2821
+ staffList.map((s) => [
2822
+ s.name,
2823
+ Object.fromEntries(
2824
+ days.map((d) => [d, s.dailyWage + (d === "Sat" || d === "Sun" ? 30 : 0)])
2825
+ ),
2826
+ ])
2827
+ );
2828
+ m.objective(minimize(dot(costMatrix, shift)));
2829
+
2830
+ // Rule 1: Daily headcount coverage
2831
+ for (const d of days) {
2832
+ m.constraint(
2833
+ expr`${sum(staffList, (s) => shift[s.name][d])} >= ${requiredCoverage[d]}`,
2834
+ `coverage_${d}`
2835
+ );
2836
+ }
2837
+
2838
+ // Rule 2: At least 1 senior staff member per day
2839
+ const seniors = staffList.filter((s) => s.isSenior);
2840
+ for (const d of days) {
2841
+ m.constraint(
2842
+ expr`${sum(seniors, (s) => shift[s.name][d])} >= 1`,
2843
+ `senior_duty_${d}`
2844
+ );
2845
+ }
2846
+
2847
+ // Rule 3: Max working days per staff member
2848
+ for (const s of staffList) {
2849
+ m.constraint(
2850
+ expr`${sum(days, (d) => shift[s.name][d])} <= ${s.maxShifts}`,
2851
+ `max_shifts_${s.name}`
2852
+ );
2853
+ }
2854
+
2855
+ // Rule 4: Named day-off requests as HARD constraints (shift[staff][day] == 0)
2856
+ for (const req of dayOffRequests) {
2857
+ m.constraint(
2858
+ expr`${shift[req.staff][req.day]} == 0`,
2859
+ `day_off_${req.staff}_${req.day}`
2860
+ );
2861
+ }
2862
+
2863
+ return { model: m, shift };
2864
+ }
2865
+
2866
+ async function runScenario(
2867
+ title: string,
2868
+ modelName: string,
2869
+ dayOffRequests: Array<{ staff: string; day: Day }>
2870
+ ) {
2871
+ console.log(`=== ${title} ===`);
2872
+ console.log(
2873
+ "Hard Vacation Requests: " +
2874
+ dayOffRequests.map((r) => `${r.staff} (${r.day})`).join(", ")
2875
+ );
2876
+
2877
+ const { model, shift } = buildShiftModel(modelName, dayOffRequests);
2878
+
2879
+ // Step 1: Optimize with HiGHS
2880
+ const status = await model.solve();
2881
+ console.log(`HiGHS Optimize Status: ${status}`);
2882
+
2883
+ if (status === "Optimal") {
2884
+ console.log(`Optimal Weekly Payroll: $${model.objectiveValue}`);
2885
+ for (const s of staffList) {
2886
+ const schedule = days.map((d) => (Math.round(shift[s.name][d].value ?? 0) === 1 ? "ON" : " ·"));
2887
+ console.log(` ${s.name.padEnd(8)}: ${schedule.join(" ")}`);
2888
+ }
2889
+ console.log("Schedule resolved successfully (no UNSAT diagnosis needed).\n");
2890
+ } else if (status === "Infeasible") {
2891
+ // Step 2: Diagnose the exact same model and constraints with Z3
2892
+ console.log("HiGHS returned Infeasible! Diagnosing conflict with Z3 unsatCore...");
2893
+ const checkRes = await model.check({ solver: "z3" });
2894
+ console.log(`Z3 Check Status: ${checkRes.status}`);
2895
+ console.log("Conflicting Constraint Subset (unsatCore):", model.unsatCore);
2896
+ console.log(
2897
+ "Note: unsatCore returns a conflicting subset (minimality/order not guaranteed). " +
2898
+ "Independent requests like Diana's Wednesday off are excluded.\n"
2899
+ );
2900
+ }
2901
+ }
2902
+
2903
+ async function main() {
2904
+ // Scenario A: Balanced requests (Solvable)
2905
+ await runScenario(
2906
+ "Scenario A: Balanced Vacation Requests",
2907
+ "FeasibleRoster",
2908
+ [
2909
+ { staff: "Alice", day: "Sun" },
2910
+ { staff: "Bob", day: "Mon" },
2911
+ { staff: "Charlie", day: "Sat" },
2912
+ { staff: "Diana", day: "Wed" },
2913
+ ]
2914
+ );
2915
+
2916
+ // Scenario B: Conflicting Saturday requests (Infeasible)
2917
+ await runScenario(
2918
+ "Scenario B: Conflicting Saturday Requests",
2919
+ "ConflictRoster",
2920
+ [
2921
+ { staff: "Alice", day: "Sat" }, // Senior
2922
+ { staff: "Bob", day: "Sat" }, // Senior
2923
+ { staff: "Charlie", day: "Sat" },
2924
+ { staff: "Diana", day: "Wed" },
2925
+ ]
2926
+ );
2927
+ }
2928
+
2929
+ main();
2930
+ ```
2931
+
2932
+ ---
2933
+
2934
+ # E-Commerce Cart & Coupon Optimizer (Indicator MIP)
2935
+
2936
+ | Problem Class | Solver | Key APIs | Links |
2937
+ | :--- | :--- | :--- | :--- |
2938
+ | Mixed-Integer Programming (MIP) | HiGHS (default) | `m.binary()`, `m.constraint()`, `m.indicator()`, `expr`, `minimize` | <ul><li><a href="https://ohtaman.github.io/washi/demo/?preset=cart_coupon">Open in Playground</a></li><li><a href="https://github.com/ohtaman/washi/blob/main/examples/node/12-ecommerce-coupon-optimizer.ts">Source Code</a></li></ul> |
2939
+
2940
+ Modern e-commerce checkouts often feature complex promotional rules: minimum spend thresholds, category-specific discounts, stackable vs. mutually exclusive coupons, and maximum coupon limits per order. Running an optimizer directly in the browser (or an edge worker) lets the checkout UI automatically apply the best coupon combination—and even recommend a small "filler" item when crossing a spend threshold lowers the customer's net bill.
2941
+
2942
+ This example demonstrates **`m.indicator(binVar, constraint, { activeValue, M? })`**, which automatically linearizes conditional *"if coupon is selected, minimum spend must hold"* constraints using Big-M reformulation for MIP solvers like HiGHS. When `M` is omitted, WaSHI automatically derives safe, tight Big-M values from finite variable bounds.
2943
+
2944
+ ## Scenario & Promotion Rules
2945
+
2946
+ A customer's shopping cart currently contains **$242** of merchandise (**$210** in Tech items and **$32** in Apparel):
2947
+ - **Optional Filler Add-on:** A **$10** Braided USB-C Cable (Tech) can be added to bring the cart total from `$242` to `$252`.
2948
+ - **Available Coupons** (maximum **2 coupons** total per checkout; at most **1 Exclusive** coupon):
2949
+
2950
+ | Coupon Code | Discount | Condition | Stacking Rule |
2951
+ | :--- | :---: | :--- | :--- |
2952
+ | `WELCOME15` | **-$15** | Total cart spend $\ge \$100$ | **Stackable** |
2953
+ | `TECH30` | **-$30** | Tech category spend $\ge \$200$ | **Exclusive** (cannot combine with `MEGA45_250`) |
2954
+ | `MEGA45_250` | **-$45** | Total cart spend $\ge \$250$ | **Exclusive** (cannot combine with `TECH30`) |
2955
+ | `MEMBER10` | **-$10** | Total cart spend $\ge \$150$ | **Stackable** |
2956
+
2957
+ Without adding the `$10` cable, the cart (`$242`) cannot use `MEGA45_250` and pays `$242 - $30 (TECH30) - $15 (WELCOME15) = $197`.
2958
+ By adding the `$10` cable (`$252` gross), the customer unlocks `MEGA45_250` (`-$45`) + `WELCOME15` (`-$15`) for a net total of **$192**—saving an extra **$5** while getting an extra item!
2959
+
2960
+ ## Implementation in WaSHI
2961
+
2962
+ ```typescript
2963
+ import { Model, sum, minimize, expr } from "@ohtaman/washi";
2964
+
2965
+ async function optimizeCheckoutCart() {
2966
+ const m = new Model("CartCouponOptimizer");
2967
+
2968
+ const baseCartSubtotal = 242;
2969
+ const baseTechSubtotal = 210;
2970
+
2971
+ // Binary decision: add optional $10 USB-C cable to unlock $250 tier?
2972
+ const addUsbCable = m.binary("addon_USB_Cable");
2973
+ const usbCablePrice = 10;
2974
+
2975
+ const coupons = [
2976
+ { id: "WELCOME15", discount: 15, minCart: 100, minTech: 0, stackable: true },
2977
+ { id: "TECH30", discount: 30, minCart: 0, minTech: 200, stackable: false },
2978
+ { id: "MEGA45_250", discount: 45, minCart: 250, minTech: 0, stackable: false },
2979
+ { id: "MEMBER10", discount: 10, minCart: 150, minTech: 0, stackable: true },
2980
+ ];
2981
+
2982
+ // Binary decision variables for each coupon
2983
+ const useCoupon = m.binary(coupons.map((c) => c.id));
2984
+
2985
+ const merchSpend = addUsbCable.mul(usbCablePrice).add(baseCartSubtotal);
2986
+ const techSpend = addUsbCable.mul(usbCablePrice).add(baseTechSubtotal);
2987
+
2988
+ // 1. Indicator constraints: If useCoupon[id] == 1, minimum spend threshold must hold
2989
+ for (const cpn of coupons) {
2990
+ if (cpn.minCart > 0) {
2991
+ m.indicator(useCoupon[cpn.id], merchSpend.ge(cpn.minCart), {
2992
+ activeValue: 1,
2993
+ M: 500,
2994
+ name: `min_spend_${cpn.id}`,
2995
+ });
2996
+ }
2997
+ if (cpn.minTech > 0) {
2998
+ m.indicator(useCoupon[cpn.id], techSpend.ge(cpn.minTech), {
2999
+ activeValue: 1,
3000
+ M: 500,
3001
+ name: `min_tech_${cpn.id}`,
3002
+ });
3003
+ }
3004
+ }
3005
+
3006
+ // 2. Mutual exclusion among non-stackable ("Exclusive") coupons
3007
+ const exclusiveCoupons = coupons.filter((c) => !c.stackable);
3008
+ m.constraint(
3009
+ expr`${sum(exclusiveCoupons, (c) => useCoupon[c.id])} <= 1`,
3010
+ "at_most_one_exclusive"
3011
+ );
3012
+
3013
+ // 3. Max 2 coupons per order
3014
+ m.constraint(
3015
+ expr`${sum(coupons, (c) => useCoupon[c.id])} <= 2`,
3016
+ "max_two_coupons"
3017
+ );
3018
+
3019
+ // Objective: Minimize final net payable amount (merchSpend - totalDiscounts)
3020
+ const totalDiscount = sum(coupons, (c) => useCoupon[c.id].mul(c.discount));
3021
+ m.objective(minimize(merchSpend.sub(totalDiscount)));
3022
+
3023
+ const status = await m.solve();
3024
+ console.log(`Status: ${status}, Optimal Net Payable: $${m.objectiveValue}`);
3025
+ console.log(`Recommended Add-on (+$10 USB-C Cable): ${(addUsbCable.value ?? 0) > 0.5 ? "YES" : "NO"}`);
3026
+ for (const cpn of coupons) {
3027
+ const applied = (useCoupon[cpn.id].value ?? 0) > 0.5;
3028
+ console.log(` ${applied ? "✅ [APPLIED]" : " [skipped]"} ${cpn.id.padEnd(11)} (-$${cpn.discount})`);
3029
+ }
3030
+ }
3031
+
3032
+ optimizeCheckoutCart();
3033
+ ```
3034
+
3035
+ ---
3036
+
3037
+ # Portfolio Optimization (QP)
3038
+
3039
+ | Problem Class | Solver | Key APIs | Links |
3040
+ | :--- | :--- | :--- | :--- |
3041
+ | Convex Quadratic Programming (QP) | HiGHS (default) | `m.continuous()`, `m.constraint()`, `expr`, `minimize`, `dot`, `QuadExpr` | <ul><li><a href="https://ohtaman.github.io/washi/demo/?preset=portfolio_qp">Open in Playground</a></li><li><a href="https://github.com/ohtaman/washi/blob/main/examples/node/03-portfolio-qp.ts">Source Code</a></li></ul> |
3042
+
3043
+ The Markowitz mean-variance portfolio optimization problem is the foundation of modern quantitative finance: allocate capital across risky assets to minimize overall portfolio variance (risk) subject to achieving a minimum required expected return.
3044
+
3045
+ ## Mathematical Formulation
3046
+
3047
+ Let $w_i \ge 0$ denote the allocation weight for asset $i$:
3048
+
3049
+ $$\begin{aligned}
3050
+ \text{Minimize} \quad & \mathbf{w}^T \boldsymbol{\Sigma} \mathbf{w} & (\text{Portfolio Variance}) \\
3051
+ \text{Subject to} \quad & \sum_{i=1}^n w_i = 1 & (\text{Full Capital Budget}) \\
3052
+ & \sum_{i=1}^n r_i w_i \ge R_{\text{target}} & (\text{Minimum Target Return}) \\
3053
+ & 0 \le w_i \le 1 & (\text{Long-Only Weights})
3054
+ \end{aligned}$$
3055
+
3056
+ where $\boldsymbol{\Sigma}$ is the positive semi-definite asset return covariance matrix.
3057
+
3058
+ ## Implementation in WaSHI
3059
+
3060
+ ```typescript
3061
+ import { Model, QuadExpr, sum, dot, minimize, expr } from "@ohtaman/washi";
3062
+
3063
+ async function solvePortfolio() {
3064
+ const m = new Model("PortfolioQP");
3065
+
3066
+ const assets = ["Tech", "Bonds", "GlobalEq", "RealEstate", "Gold"];
3067
+ const expectedReturns = [0.12, 0.04, 0.09, 0.07, 0.05];
3068
+ const targetReturn = 0.085; // 8.5% minimum expected return
3069
+
3070
+ // 5x5 Covariance Matrix
3071
+ const cov = [
3072
+ [0.040, 0.002, 0.025, 0.015, 0.005],
3073
+ [0.002, 0.005, 0.003, 0.002, 0.001],
3074
+ [0.025, 0.003, 0.030, 0.018, 0.008],
3075
+ [0.015, 0.002, 0.018, 0.025, 0.004],
3076
+ [0.005, 0.001, 0.008, 0.004, 0.020],
3077
+ ];
3078
+
3079
+ // Allocation weights w_i in [0, 1]
3080
+ const w = m.continuous(assets, { lb: 0, ub: 1 });
3081
+
3082
+ // 1. Budget constraint: sum(w_i) == 1.0 (100%)
3083
+ m.constraint(expr`${sum(assets, (a) => w[a])} == 1.0`, "budget_limit");
3084
+
3085
+ // 2. Expected return constraint: sum(r_i * w_i) >= 8.5%
3086
+ const retMap = Object.fromEntries(assets.map((a, i) => [a, expectedReturns[i]]));
3087
+ m.constraint(expr`${dot(retMap, w)} >= ${targetReturn}`, "target_return");
3088
+
3089
+ // 3. Quadratic Objective: minimize risk w^T * Sigma * w
3090
+ let risk = new QuadExpr();
3091
+ for (let i = 0; i < assets.length; i++) {
3092
+ for (let j = 0; j < assets.length; j++) {
3093
+ if (cov[i][j] !== 0) {
3094
+ risk = risk.add(w[assets[i]].mul(w[assets[j]]).mul(cov[i][j]));
3095
+ }
3096
+ }
3097
+ }
3098
+ m.objective(minimize(risk));
3099
+
3100
+ // Solve natively with HiGHS Convex QP engine
3101
+ const status = await m.solve();
3102
+
3103
+ console.log(`Status: ${status}`);
3104
+ console.log(`Variance: ${m.objectiveValue?.toFixed(6)}`);
3105
+ console.log(`Volatility (Std Dev): ${((Math.sqrt(m.objectiveValue || 0)) * 100).toFixed(2)}%`);
3106
+
3107
+ assets.forEach((name) => {
3108
+ console.log(` ${name.padEnd(10)}: ${((w[name].value ?? 0) * 100).toFixed(2)}%`);
3109
+ });
3110
+ }
3111
+
3112
+ solvePortfolio();
3113
+ ```
3114
+
3115
+ ---
3116
+
3117
+ # SMT Security Policies
3118
+
3119
+ | Problem Class | Solver | Key APIs | Links |
3120
+ | :--- | :--- | :--- | :--- |
3121
+ | Propositional Logic & SMT | Z3 (`@ohtaman/washi/solvers/z3`) | `m.bool()`, `m.integer()`, `m.constraint()`, `expr`, `m.check()` | <ul><li><a href="https://ohtaman.github.io/washi/demo/?preset=smt_sat_scheduling">Open in Playground</a></li><li><a href="https://github.com/ohtaman/washi/blob/main/examples/node/04-smt-security-rules.ts">Source Code</a></li></ul> |
3122
+
3123
+ Satisfiability Modulo Theories (SMT) combines propositional logic (Boolean SAT) with first-order theories (linear integer arithmetic, real inequalities).
3124
+
3125
+ This example demonstrates how to formally verify and deduce system states in an IoT home security system using WaSHI and the Z3 solver engine.
3126
+
3127
+ ## The Scenario
3128
+
3129
+ We model a security rule system with sensor readings and logic rules:
3130
+ 1. When ambient light $\le 50$, it implies night time (`isNight`).
3131
+ 2. When motion is detected at night and the alarm is armed, the siren must sound.
3132
+ 3. If the alarm is armed, the front door must never be unlocked.
3133
+ 4. When motion is detected at night, comfort lights should turn on.
3134
+
3135
+ ## Implementation in WaSHI
3136
+
3137
+ ```typescript
3138
+ import { Model, not, expr } from "@ohtaman/washi";
3139
+ import "@ohtaman/washi/solvers/z3";
3140
+
3141
+ async function verifySecurityRules() {
3142
+ const m = new Model({ name: "SmartHomeSecurity", solver: "z3" });
3143
+
3144
+ // 1. Boolean Propositional Variables
3145
+ const isNight = m.bool("isNight");
3146
+ const motionDetected = m.bool("motionDetected");
3147
+ const alarmArmed = m.bool("alarmArmed");
3148
+ const frontDoorUnlocked = m.bool("frontDoorUnlocked");
3149
+ const sirenActive = m.bool("sirenActive");
3150
+ const lightsOn = m.bool("lightsOn");
3151
+
3152
+ // 2. Arithmetic metric (lux sensor)
3153
+ const luxLevel = m.integer("luxLevel", { lb: 0, ub: 1000 });
3154
+
3155
+ // Rule 1: luxLevel <= 50 implies isNight
3156
+ m.constraint(luxLevel.le(50).implies(isNight), "dark_implies_night");
3157
+
3158
+ // Rule 2: (motionDetected AND isNight AND alarmArmed) => sirenActive
3159
+ m.constraint(
3160
+ motionDetected.and(isNight).and(alarmArmed).implies(sirenActive),
3161
+ "intrusion_rule"
3162
+ );
3163
+
3164
+ // Rule 3: alarmArmed => NOT(frontDoorUnlocked)
3165
+ m.constraint(alarmArmed.implies(not(frontDoorUnlocked)), "lock_safety_rule");
3166
+
3167
+ // Rule 4: (motionDetected AND isNight) => lightsOn
3168
+ m.constraint(motionDetected.and(isNight).implies(lightsOn), "comfort_lights");
3169
+
3170
+ // 3. Observed Context
3171
+ m.constraint(expr`${luxLevel} <= 20`, "sensor_reading_dark");
3172
+ m.constraint(alarmArmed, "user_armed_alarm");
3173
+ m.constraint(motionDetected, "sensor_motion_triggered");
3174
+
3175
+ // Check satisfiability and deduce state
3176
+ const res = await m.check({ solver: "z3" });
3177
+
3178
+ console.log(`Status: ${res.status}`);
3179
+ console.log(`Siren Active : ${sirenActive.value}`);
3180
+ console.log(`Lights On : ${lightsOn.value}`);
3181
+ console.log(`Door Locked : ${frontDoorUnlocked.value ? "No" : "Yes"}`);
3182
+ console.log(`Is Night : ${isNight.value}`);
3183
+ }
3184
+
3185
+ verifySecurityRules();
3186
+ ```
3187
+
3188
+ ---
3189
+
3190
+ # Conflict Diagnosis (Unsat Core)
3191
+
3192
+ | Problem Class | Solver | Key APIs | Links |
3193
+ | :--- | :--- | :--- | :--- |
3194
+ | Infeasibility Diagnosis / Unsat Core | Z3 (`@ohtaman/washi/solvers/z3`) | `m.integer()`, `m.continuous()`, `m.constraint()`, `expr`, `m.check()`, `m.conflicts` | <ul><li><a href="https://ohtaman.github.io/washi/demo/?preset=conflict_diagnosis_unsat">Open in Playground</a></li><li><a href="https://github.com/ohtaman/washi/blob/main/examples/node/05-unsat-core-diagnosis.ts">Source Code</a></li></ul> |
3195
+
3196
+ When business rules, budgets, and operational policies grow complex, constraints inevitably conflict. Traditional solvers simply report "Infeasible", leaving engineers to guess which of hundreds of rules caused the contradiction.
3197
+
3198
+ This example shows how to isolate the conflicting constraint subset using Z3 Unsat Core extraction.
3199
+
3200
+ ## The Conflicting Scenario
3201
+
3202
+ A tech team is staffing a customer project:
3203
+ 1. Each engineer costs $8,000 against the project budget (`budget_limit`).
3204
+ 2. The customer requires rush delivery (`rush_required`).
3205
+ 3. Rush delivery mandates at least 8 engineers (`rush_staffing_rule`).
3206
+ 4. The board has imposed a strict budget cap of $50,000 (`strict_board_budget_cap`).
3207
+ 5. Labor law requires at least 1 employee on staff (`minimum_one_employee`).
3208
+
3209
+ Notice that:
3210
+
3211
+ $$8 \text{ engineers} \times \$8,000 = \$64,000 > \$50,000 \text{ budget!}$$
3212
+
3213
+ ## Implementation in WaSHI
3214
+
3215
+ ```typescript
3216
+ import { Model, expr } from "@ohtaman/washi";
3217
+ import "@ohtaman/washi/solvers/z3";
3218
+
3219
+ async function diagnoseConflicts() {
3220
+ const m = new Model({ name: "ConflictDiagnosis", solver: "z3" });
3221
+
3222
+ const engineers = m.integer("engineers", { lb: 0, ub: 20 });
3223
+ const budget = m.continuous("budget", { lb: 0, ub: 100000 });
3224
+ const rushDelivery = m.bool("rushDelivery");
3225
+
3226
+ // Rule 1: Each engineer costs $8,000
3227
+ m.constraint(expr`8000 * ${engineers} <= ${budget}`, "budget_limit");
3228
+
3229
+ // Rule 2: Customer requires rush delivery
3230
+ m.constraint(rushDelivery, "rush_required");
3231
+
3232
+ // Rule 3: Rush delivery requires at least 8 engineers
3233
+ m.constraint(rushDelivery.implies(engineers.ge(8)), "rush_staffing_rule");
3234
+
3235
+ // Rule 4: Board budget cap of $50,000 (Conflict!)
3236
+ m.constraint(expr`${budget} <= 50000`, "strict_board_budget_cap");
3237
+
3238
+ // Rule 5: Innocent bystander rule
3239
+ m.constraint(expr`${engineers} >= 1`, "minimum_one_employee");
3240
+
3241
+ // Run diagnosis
3242
+ const res = await m.check({ solver: "z3" });
3243
+
3244
+ console.log(`Status: ${res.status}`);
3245
+ console.log("Conflicting Constraints (conflicts):", m.conflicts);
3246
+ }
3247
+
3248
+ diagnoseConflicts();
3249
+ ```