@ohtaman/washi 0.1.2 → 0.3.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 (104) hide show
  1. package/README.ja.md +195 -105
  2. package/README.md +195 -105
  3. package/README.zh-CN.md +197 -107
  4. package/dist/browser/highs-worker.js +2 -2
  5. package/dist/browser/{highs-loader-T47YOyos.js → loader-CVt4CloI.js} +2 -2
  6. package/dist/browser/solvers/glpk.js +1065 -0
  7. package/dist/browser/solvers/glpk.min.js +1 -0
  8. package/dist/browser/solvers/z3-built.js +8245 -0
  9. package/dist/browser/solvers/z3-built.wasm +4 -0
  10. package/dist/browser/solvers/z3.js +7550 -0
  11. package/dist/browser/solvers/z3.min.js +5 -0
  12. package/dist/browser/washi.js +370 -642
  13. package/dist/browser/washi.min.js +4 -4
  14. package/dist/lib/async-mutex-Duo4jwzn.js +195 -0
  15. package/dist/lib/constraint-BRuB3ooC.js +90 -0
  16. package/dist/lib/highs-worker.js +2 -2
  17. package/dist/lib/{highs-loader-Blb99785.js → loader-Dti0v9yC.js} +1 -1
  18. package/dist/lib/registry-DksqCSjI.js +32 -0
  19. package/dist/lib/solvers/glpk.js +161 -2
  20. package/dist/lib/solvers/z3-assets.js +1 -0
  21. package/dist/lib/solvers/z3-built.js +8245 -0
  22. package/dist/lib/solvers/z3-built.wasm +4 -0
  23. package/dist/lib/solvers/z3.js +476 -2
  24. package/dist/lib/washi.js +604 -248
  25. package/dist/types/index.d.ts +7 -10
  26. package/dist/types/index.d.ts.map +1 -1
  27. package/dist/types/modeler/constraint.d.ts +0 -3
  28. package/dist/types/modeler/constraint.d.ts.map +1 -1
  29. package/dist/types/modeler/expression.d.ts +16 -10
  30. package/dist/types/modeler/expression.d.ts.map +1 -1
  31. package/dist/types/modeler/index.d.ts +3 -5
  32. package/dist/types/modeler/index.d.ts.map +1 -1
  33. package/dist/types/modeler/model.d.ts +91 -12
  34. package/dist/types/modeler/model.d.ts.map +1 -1
  35. package/dist/types/modeler/solvers/glpk/index.d.ts +3 -0
  36. package/dist/types/modeler/solvers/glpk/index.d.ts.map +1 -0
  37. package/dist/types/modeler/solvers/glpk/loader.d.ts +16 -0
  38. package/dist/types/modeler/solvers/glpk/loader.d.ts.map +1 -0
  39. package/dist/types/modeler/solvers/glpk/solver.d.ts +26 -0
  40. package/dist/types/modeler/solvers/glpk/solver.d.ts.map +1 -0
  41. package/dist/types/modeler/solvers/glpk.d.ts +1 -21
  42. package/dist/types/modeler/solvers/glpk.d.ts.map +1 -1
  43. package/dist/types/{highs-assets.d.ts → modeler/solvers/highs/assets.d.ts} +1 -1
  44. package/dist/types/modeler/solvers/highs/assets.d.ts.map +1 -0
  45. package/dist/types/modeler/solvers/highs/browser-runtime.d.ts +2 -0
  46. package/dist/types/modeler/solvers/highs/browser-runtime.d.ts.map +1 -0
  47. package/dist/types/modeler/solvers/highs/index.d.ts +6 -0
  48. package/dist/types/modeler/solvers/highs/index.d.ts.map +1 -0
  49. package/dist/types/{highs-loader.d.ts → modeler/solvers/highs/loader.d.ts} +2 -2
  50. package/dist/types/modeler/solvers/highs/loader.d.ts.map +1 -0
  51. package/dist/types/{highs-node-runtime.d.ts → modeler/solvers/highs/node-runtime.d.ts} +1 -1
  52. package/dist/types/modeler/solvers/highs/node-runtime.d.ts.map +1 -0
  53. package/dist/types/modeler/solvers/highs/solver.d.ts +33 -0
  54. package/dist/types/modeler/solvers/highs/solver.d.ts.map +1 -0
  55. package/dist/types/modeler/solvers/highs/worker-client.d.ts +13 -0
  56. package/dist/types/modeler/solvers/highs/worker-client.d.ts.map +1 -0
  57. package/dist/types/{highs-worker-factory.d.ts → modeler/solvers/highs/worker-factory.d.ts} +1 -1
  58. package/dist/types/modeler/solvers/highs/worker-factory.d.ts.map +1 -0
  59. package/dist/types/modeler/solvers/highs/worker.d.ts +2 -0
  60. package/dist/types/modeler/solvers/highs/worker.d.ts.map +1 -0
  61. package/dist/types/modeler/solvers/highs.d.ts +1 -31
  62. package/dist/types/modeler/solvers/highs.d.ts.map +1 -1
  63. package/dist/types/modeler/solvers/index.d.ts +0 -2
  64. package/dist/types/modeler/solvers/index.d.ts.map +1 -1
  65. package/dist/types/modeler/solvers/mock.d.ts +1 -0
  66. package/dist/types/modeler/solvers/mock.d.ts.map +1 -1
  67. package/dist/types/modeler/solvers/registry.d.ts +1 -2
  68. package/dist/types/modeler/solvers/registry.d.ts.map +1 -1
  69. package/dist/types/modeler/solvers/types.d.ts +1 -1
  70. package/dist/types/modeler/solvers/z3/assets.d.ts +11 -0
  71. package/dist/types/modeler/solvers/z3/assets.d.ts.map +1 -0
  72. package/dist/types/modeler/solvers/z3/index.d.ts +3 -0
  73. package/dist/types/modeler/solvers/z3/index.d.ts.map +1 -0
  74. package/dist/types/modeler/solvers/z3/loader.d.ts +56 -0
  75. package/dist/types/modeler/solvers/z3/loader.d.ts.map +1 -0
  76. package/dist/types/modeler/solvers/z3/solver.d.ts +34 -0
  77. package/dist/types/modeler/solvers/z3/solver.d.ts.map +1 -0
  78. package/dist/types/modeler/solvers/z3.d.ts +1 -35
  79. package/dist/types/modeler/solvers/z3.d.ts.map +1 -1
  80. package/dist/types/modeler/types.d.ts +8 -3
  81. package/dist/types/modeler/types.d.ts.map +1 -1
  82. package/dist/types/modeler/variable.d.ts +1 -6
  83. package/dist/types/modeler/variable.d.ts.map +1 -1
  84. package/package.json +28 -6
  85. package/dist/browser/_washi-optional-engine-282Ug7Gw.js +0 -7
  86. package/dist/lib/glpk-5cUpjyb9.js +0 -135
  87. package/dist/lib/z3-C6GeIicJ.js +0 -509
  88. package/dist/types/glpk-loader.d.ts +0 -8
  89. package/dist/types/glpk-loader.d.ts.map +0 -1
  90. package/dist/types/highs-assets.d.ts.map +0 -1
  91. package/dist/types/highs-browser-runtime.d.ts +0 -2
  92. package/dist/types/highs-browser-runtime.d.ts.map +0 -1
  93. package/dist/types/highs-loader.d.ts.map +0 -1
  94. package/dist/types/highs-node-runtime.d.ts.map +0 -1
  95. package/dist/types/highs-worker-client.d.ts +0 -12
  96. package/dist/types/highs-worker-client.d.ts.map +0 -1
  97. package/dist/types/highs-worker-factory.d.ts.map +0 -1
  98. package/dist/types/highs-worker.d.ts +0 -2
  99. package/dist/types/highs-worker.d.ts.map +0 -1
  100. package/dist/types/modeler/mip-compat.d.ts +0 -43
  101. package/dist/types/modeler/mip-compat.d.ts.map +0 -1
  102. package/dist/types/modeler/pulp-compat.d.ts +0 -67
  103. package/dist/types/modeler/pulp-compat.d.ts.map +0 -1
  104. /package/dist/browser/{highs-browser-runtime-D13gHOT5.js → browser-runtime-D13gHOT5.js} +0 -0
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  <source media="(prefers-color-scheme: light)" srcset="./assets/washi-header-light.png">
5
5
  <img alt="WaSHI - Algebraic Modeler for TypeScript &amp; JavaScript" src="./assets/washi-header-light.png" width="420" />
6
6
  </picture>
7
- <p><strong>Intuitive Mathematical Optimization &amp; Algebraic Modeler for TypeScript &amp; JavaScript</strong></p>
7
+ <p><strong>Algebraic Modeling for TypeScript &amp; JavaScript</strong></p>
8
8
  <p>
9
9
  <em>Linear Programming (LP) • Mixed-Integer Programming (MIP) • Quadratic Programming (QP) • SMT &amp; Conflict Diagnosis</em>
10
10
  </p>
@@ -14,56 +14,58 @@
14
14
  <a href="./README.zh-CN.md">简体中文</a>
15
15
  </p>
16
16
  <p>
17
+ <a href="https://www.npmjs.com/package/@ohtaman/washi"><img src="https://img.shields.io/npm/v/@ohtaman/washi?color=crimson&label=npm" alt="npm version" /></a>
18
+ <a href="https://github.com/ohtaman/washi/actions/workflows/distribution.yml"><img src="https://github.com/ohtaman/washi/actions/workflows/distribution.yml/badge.svg" alt="CI Status" /></a>
17
19
  <a href="https://ohtaman.github.io/washi/"><img src="https://img.shields.io/badge/docs-online-blue" alt="docs" /></a>
18
20
  <a href="https://ohtaman.github.io/washi/demo/"><img src="https://img.shields.io/badge/demo-playground-brightgreen" alt="live demo" /></a>
19
- <a href="https://www.npmjs.com/package/@ohtaman/washi"><img src="https://img.shields.io/npm/v/@ohtaman/washi?color=crimson&label=npm" alt="npm version" /></a>
20
- <a href="https://bundlephobia.com/package/@ohtaman/washi"><img src="https://img.shields.io/badge/bundle%20size-7.2%20kB%20(gzip)-blue" alt="bundle size" /></a>
21
21
  <a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="license" /></a>
22
- <img src="https://img.shields.io/badge/types-TypeScript-blue" alt="TypeScript types" />
23
- <img src="https://img.shields.io/badge/tests-120%2B%20passing-brightgreen" alt="tests" />
24
22
  </p>
25
23
  </div>
26
24
 
27
25
  ---
28
26
 
29
- **WaSHI** is a high-level algebraic modeling library that brings the expressive elegance of Python's **PuLP** and Julia's **JuMP** to the modern TypeScript and JavaScript ecosystem. Formulate mathematical models using natural algebraic equations and solve them instantly across Node.js, browsers, Bun, Deno, and Next.js—powered natively by WebAssembly solvers (**HiGHS**, **Z3**, and **GLPK**) with zero native compilation steps.
27
+ **WaSHI** is an algebraic modeler for TypeScript and JavaScript, inspired by Python's **PuLP** and Julia's **JuMP**. Define variables, objectives, and arithmetic or logical constraints in application code. WaSHI constructs the model, translates it for a solver adapter, and maps results back to your variables. **HiGHS**, **Z3**, and **GLPK** perform the optimization or satisfiability checks.
28
+
29
+ HiGHS is the default solver; Z3 and GLPK are optional plugins. The package provides Node.js and browser distributions, including WebAssembly assets. See the [browser integration guide](#-browser--web-worker-usage) for deployment requirements.
30
+
31
+ [Try the browser demo](https://ohtaman.github.io/washi/demo/) · [Browse example source code](https://github.com/ohtaman/washi/blob/main/examples/README.md) · [ESM / script / CodePen examples](https://github.com/ohtaman/washi/blob/main/examples/distribution/README.md)
30
32
 
31
33
  ```ts
32
- import { Model, expr, c } from "@ohtaman/washi";
34
+ import { Model, expr } from "@ohtaman/washi";
33
35
 
34
- const m = new Model({ sense: "Maximize" });
35
- const x = m.addVar({ type: "Integer", lb: 0 });
36
- const y = m.addVar({ type: "Integer", lb: 0 });
36
+ const m = new Model("WoodworkingProfit");
37
+ const x = m.integer("desks", { lb: 0 });
38
+ const y = m.integer("chairs", { lb: 0 });
37
39
 
38
40
  m.maximize(expr`30 * ${x} + 50 * ${y}`);
39
- m.addConstraint(c`${x} + 2 * ${y} <= 40`, "wood_limit");
40
- m.addConstraint(c`2 * ${x} + ${y} <= 50`, "labor_limit");
41
+ m.constrain(expr`${x} + 2 * ${y} <= 40`, "wood_limit");
42
+ m.constrain(expr`2 * ${x} + ${y} <= 50`, "labor_limit");
41
43
 
42
- await m.solve();
44
+ await m.optimize();
43
45
  console.log(`Produce: x=${x.value}, y=${y.value} (Profit: $${m.objectiveValue})`);
44
46
  ```
45
47
 
46
48
  ---
47
49
 
48
- ## 🎯 Why WaSHI?
50
+ ## 🎯 What WaSHI provides
49
51
 
50
- 1. 🌐 **100% Client-Side & Zero Server Cost**: Run industrial-grade optimization entirely inside the user's browser, edge worker, or Electron app. Zero backend server costs, offline-ready, and 100% private (confidential business data never leaves the client device).
51
- 2. 📦 **Frictionless Zero-Dependency Distribution**: No Python runtimes, no C/C++ compilation, and no OS-dependent native solver binaries to bundle. Just `npm install @ohtaman/washi`—everything (including WebAssembly solvers) works instantly out of the box.
52
- 3. ✍️ **Familiar Algebraic Modeling (PuLP & python-mip DX)**: Never struggle with low-level matrix indices. Formulate models naturally using tagged template literals (`c`${x} + 2 * ${y} <= 40``) or drop-in PuLP/python-mip syntax (`LpProblem`, `lpSum`).
53
- 4. 🔄 **Pluggable Solvers & Unified API**: Formulate your mathematical model once; solve with **HiGHS** (industrial MIP/QP), **Z3** (SMT logic & Unsat Core diagnosis), or **GLPK** with a single parameter.
54
- 5. 🔍 **Automatic Conflict Diagnosis (`unsatCore`)**: When constraints are contradictory (Infeasible), diagnose root causes by extracting a conflicting constraint subset (unsatisfiable core) via the Z3 engine.
55
- 6. ⚡ **Industrial Scale Performance in WebAssembly**: Built with an $O(N)$ linear-time expression builder. Formulate 20,000+ variable MIPs in ~100ms and solve them in ~15 seconds right inside WebAssembly via HiGHS.
52
+ 1. **Algebraic model construction**: Formulate optimization models using domain-specific variable constructors (`m.binary`, `m.integer`, `m.continuous`), tagged template literals (`expr`), vector aggregations (`dot`, `sum`), and first-class constraints (`m.constrain`).
53
+ 2. **Solver adapters**: Use a common modeling API with HiGHS, Z3, or GLPK, or [register your own adapter](https://github.com/ohtaman/washi/blob/main/docs/custom-solver-guide.md). Choose an engine that supports the model's problem class; the adapters do not make every solver support every feature.
54
+ 3. **Local execution and WebAssembly distribution**: With the local solver adapters, solving runs in the application without a separate Python service or native C/C++ build. Browser deployments must include the generated JS, Worker, and WASM assets. Offline use also requires those resources to be available locally or cached. Z3 has additional browser isolation requirements.
55
+ 4. **Named constraints and SMT results**: With Z3, explicitly check satisfiability and inspect an unsatisfiable core to investigate conflicting constraints. A returned core is a conflicting subset, not necessarily the smallest one.
56
+
57
+ Model construction and translation are WaSHI's responsibility. Supported problem classes, numerical behavior, and solve time depend on the selected solver, its settings, and the problem. Modeling overhead and solver execution time should be measured separately; a result for one model is not a general performance guarantee.
56
58
 
57
59
  ---
58
60
 
59
- ## ⚡ Solvers Matrix
61
+ ## ⚙️ Solver adapters and capabilities
60
62
 
61
- | Solver | Engine License | Problem Classes | Best For | Status in WaSHI |
63
+ | Adapter | Engine license | Supported problem classes | Engine role | Integration |
62
64
  | :--- | :--- | :--- | :--- | :--- |
63
- | **HiGHS** | **MIT** | **LP, MIP, Convex QP** | Industrial-scale optimization, production planning, finance | **Built-in default** (zero-config) |
64
- | **Z3** | **MIT** | **SMT, SAT, Logic, MIP** | Propositional logic, conflict diagnosis (`unsatCore`), rule engines | Optional plugin (`@ohtaman/washi/solvers/z3`) |
65
- | **GLPK** | **GPL-3.0** | **LP, MIP** | Academic benchmarks, legacy GLPK compatibility | Optional plugin (`@ohtaman/washi/solvers/glpk`) |
66
- | **Mock** | **MIT** | *Any* | Ultra-fast unit testing in CI without loading WebAssembly | Built-in |
65
+ | **HiGHS** | **MIT** | **LP, MIP, convex QP** | Numerical optimization | Built-in default |
66
+ | **Z3** | **MIT** | **SMT, SAT, logic, MIP** | Satisfiability checks, unsatisfiable cores, optimization | Optional plugin (`@ohtaman/washi/solvers/z3`) |
67
+ | **GLPK** | **GPL-3.0** | **LP, MIP** | Linear and mixed-integer optimization | Optional plugin (`@ohtaman/washi/solvers/glpk`) |
68
+ | **Mock** | **MIT** | — | Simulated solver responses for integration tests | Built-in; does not solve the model |
67
69
 
68
70
  ---
69
71
 
@@ -99,49 +101,49 @@ npm install glpk.js # For GLPK solver (GPL-3.0)
99
101
  Formulate production scheduling, resource allocation, and facility location problems:
100
102
 
101
103
  ```ts
102
- import { Model, c, expr } from "@ohtaman/washi";
104
+ import { Model, expr } from "@ohtaman/washi";
103
105
 
104
- const m = new Model({ name: "FactoryPlanning", sense: "Maximize" });
106
+ const m = new Model("FactoryPlanning");
105
107
 
106
108
  // Decision variables (Integer, non-negative)
107
- const chairs = m.addVar({ name: "chairs", type: "Integer", lb: 0 });
108
- const tables = m.addVar({ name: "tables", type: "Integer", lb: 0 });
109
+ const chairs = m.integer("chairs", { lb: 0 });
110
+ const tables = m.integer("tables", { lb: 0 });
109
111
 
110
112
  // Natural formula objective & constraints via Tagged Template Literals
111
113
  m.maximize(expr`30 * ${chairs} + 50 * ${tables}`);
112
- m.addConstraint(c`${chairs} + 2 * ${tables} <= 40`, "wood_limit");
113
- m.addConstraint(c`2 * ${chairs} + ${tables} <= 50`, "labor_limit");
114
+ m.constrain(expr`${chairs} + 2 * ${tables} <= 40`, "wood_limit");
115
+ m.constrain(expr`2 * ${chairs} + ${tables} <= 50`, "labor_limit");
114
116
 
115
117
  // Solve using default HiGHS WASM engine
116
- const status = await m.solve();
118
+ const status = await m.optimize();
117
119
 
118
120
  console.log(`Status: ${status}`); // "Optimal"
119
121
  console.log(`Objective: $${m.objectiveValue}`); // 1100
120
122
  console.log(`Chairs: ${chairs.value}, Tables: ${tables.value}`); // 20 chairs, 10 tables
121
123
  ```
122
124
 
123
- #### 🗂 Multidimensional Variables Grid (`addVars`)
125
+ #### 🗂 Multidimensional Variables Grid
124
126
 
125
- Construct variable arrays or multi-index matrices in one line (similar to PuLP's `LpVariable.dicts`):
127
+ Construct variable arrays or multi-index matrices in one call:
126
128
 
127
129
  ```ts
128
130
  // 1D dictionary of binary variables
129
- const openCity = m.addVars(["Tokyo", "Osaka", "Nagoya"], { type: "Binary" });
131
+ const openCity = m.binary(["Tokyo", "Osaka", "Nagoya"]);
130
132
 
131
133
  // 2D transport flow matrix: [Factory] x [Store]
132
- const flow = m.addVars(["F1", "F2"], ["StoreA", "StoreB"], { lb: 0 });
134
+ const flow = m.continuous(["F1", "F2"], ["StoreA", "StoreB"], { lb: 0 });
133
135
 
134
136
  // 2D coordinate grid (e.g. 8x8 chessboard)
135
- const board = m.addVars(8, 8, { type: "Binary", prefix: "queen" });
137
+ const board = m.binary(8, 8, { prefix: "queen" });
136
138
  ```
137
139
 
138
- #### 🔀 Big-M Indicator Constraints (`addIndicator`)
140
+ #### 🔀 Big-M Indicator Constraints (`m.indicator`)
139
141
 
140
142
  Model conditional activation rules and fixed-charge economics without manual Big-M algebra:
141
143
 
142
144
  ```ts
143
145
  // If factory is closed (openCity['Tokyo'] == 0), its production capacity must be 0
144
- m.addIndicator(openCity["Tokyo"], flow["F1"]["StoreA"].le(0), {
146
+ m.indicator(openCity["Tokyo"], expr`${flow["F1"]["StoreA"]} <= 0`, {
145
147
  activeValue: 0,
146
148
  M: 1000,
147
149
  });
@@ -154,20 +156,21 @@ m.addIndicator(openCity["Tokyo"], flow["F1"]["StoreA"].le(0), {
154
156
  Minimize investment risk based on a covariance matrix subject to return and budget constraints:
155
157
 
156
158
  ```ts
157
- import { Model, QuadExpr, sum } from "@ohtaman/washi";
159
+ import { Model, QuadExpr, sum, dot, expr } from "@ohtaman/washi";
158
160
 
159
- const m = new Model({ name: "MarkowitzPortfolio", sense: "Minimize" });
161
+ const m = new Model("MarkowitzPortfolio");
160
162
  const assets = ["Tech", "Bonds", "RealEstate"];
161
163
  const returns = [0.14, 0.04, 0.08];
162
164
 
163
165
  // Decision variables: portfolio allocation weights (0 to 1)
164
- const w = assets.map((a) => m.addVar({ name: `w_${a}`, lb: 0, ub: 1 }));
166
+ const w = m.continuous(assets, { lb: 0, ub: 1 });
165
167
 
166
168
  // 1. Fully invested budget: sum(w) == 1.0
167
- m.addConstraint(sum(w).eq(1.0), "budget");
169
+ m.constrain(expr`${sum(assets, (a) => w[a])} == 1.0`, "budget");
168
170
 
169
171
  // 2. Minimum target return: portfolio return >= 8.5%
170
- m.addConstraint(sum(w.map((v, i) => v.mul(returns[i]))).ge(0.085), "min_return");
172
+ const retMap = Object.fromEntries(assets.map((a, i) => [a, returns[i]]));
173
+ m.constrain(expr`${dot(retMap, w)} >= 0.085`, "min_return");
171
174
 
172
175
  // 3. Covariance risk matrix
173
176
  const cov = [
@@ -180,15 +183,15 @@ const cov = [
180
183
  let risk = new QuadExpr();
181
184
  for (let i = 0; i < assets.length; i++) {
182
185
  for (let j = 0; j < assets.length; j++) {
183
- risk = risk.add(w[i].mul(w[j]).mul(cov[i][j]));
186
+ risk = risk.add(w[assets[i]].mul(w[assets[j]]).mul(cov[i][j]));
184
187
  }
185
188
  }
186
189
  m.minimize(risk);
187
190
 
188
- await m.solve();
191
+ await m.optimize();
189
192
  console.log(`Minimal Variance: ${m.objectiveValue?.toFixed(6)}`);
190
- assets.forEach((name, i) => {
191
- console.log(` ${name}: ${((w[i].value || 0) * 100).toFixed(2)}%`);
193
+ assets.forEach((name) => {
194
+ console.log(` ${name}: ${((w[name].value || 0) * 100).toFixed(2)}%`);
192
195
  });
193
196
  ```
194
197
 
@@ -199,7 +202,8 @@ assets.forEach((name, i) => {
199
202
  Express boolean variables, propositional connectives (`and`, `or`, `implies`, `not`, `xor`), and mixed arithmetic rules:
200
203
 
201
204
  ```ts
202
- import { Model, not } from "@ohtaman/washi";
205
+ import { Model, not, expr } from "@ohtaman/washi";
206
+ import "@ohtaman/washi/solvers/z3";
203
207
 
204
208
  const m = new Model({ name: "SmartSecurityRules" });
205
209
 
@@ -207,16 +211,16 @@ const isNight = m.addBoolVar("isNight");
207
211
  const motion = m.addBoolVar("motion");
208
212
  const alarm = m.addBoolVar("alarm");
209
213
  const siren = m.addBoolVar("siren");
210
- const lux = m.addVar({ name: "lux", type: "Integer", lb: 0 });
214
+ const lux = m.integer("lux", { lb: 0 });
211
215
 
212
216
  // Logical rules
213
- m.addConstraint(lux.le(50).implies(isNight), "dark_implies_night");
214
- m.addConstraint(motion.and(isNight).and(alarm).implies(siren), "alarm_rule");
217
+ m.constrain(lux.le(50).implies(isNight), "dark_implies_night");
218
+ m.constrain(motion.and(isNight).and(alarm).implies(siren), "alarm_rule");
215
219
 
216
220
  // Current sensor observations
217
- m.addConstraint(lux.le(10), "sensor_dark");
218
- m.addConstraint(motion, "sensor_motion");
219
- m.addConstraint(alarm, "alarm_on");
221
+ m.constrain(expr`${lux} <= 10`, "sensor_dark");
222
+ m.constrain(motion, "sensor_motion");
223
+ m.constrain(alarm, "alarm_on");
220
224
 
221
225
  // Check satisfiability and deduce state without an objective function
222
226
  const res = await m.check({ solver: "z3" });
@@ -232,28 +236,28 @@ console.log(`Siren active? ${m.values.get("siren")}`); // true
232
236
  When business rules or constraints contradict each other, extract a **conflicting constraint subset** (unsatisfiable core; minimality is not guaranteed) for troubleshooting (requires Z3):
233
237
 
234
238
  ```ts
235
- import { Model } from "@ohtaman/washi";
239
+ import { Model, expr } from "@ohtaman/washi";
236
240
  import "@ohtaman/washi/solvers/z3";
237
241
 
238
242
  const m = new Model({ name: "ConflictDiagnosis", solver: "z3" });
239
- const engineers = m.addVar({ name: "engineers", type: "Integer", lb: 0, ub: 20 });
240
- const budget = m.addVar({ name: "budget", type: "Continuous", lb: 0, ub: 100000 });
243
+ const engineers = m.integer("engineers", { lb: 0, ub: 20 });
244
+ const budget = m.continuous("budget", { lb: 0, ub: 100000 });
241
245
  const rushDelivery = m.addBoolVar("rushDelivery");
242
246
 
243
247
  // Rule 1: Each engineer costs $8,000
244
- m.addConstraint(engineers.mul(8000).le(budget), "budget_limit");
248
+ m.constrain(expr`8000 * ${engineers} <= ${budget}`, "budget_limit");
245
249
 
246
250
  // Rule 2: Customer requires rush delivery
247
- m.addConstraint(rushDelivery, "rush_required");
251
+ m.constrain(rushDelivery, "rush_required");
248
252
 
249
253
  // Rule 3: Rush delivery requires at least 8 engineers
250
- m.addConstraint(rushDelivery.implies(engineers.ge(8)), "rush_staffing_rule");
254
+ m.constrain(rushDelivery.implies(engineers.ge(8)), "rush_staffing_rule");
251
255
 
252
256
  // Rule 4: Hard board budget cap of $50,000 (Conflict: 8 * $8000 = $64,000 > $50,000!)
253
- m.addConstraint(budget.le(50000), "strict_board_budget_cap");
257
+ m.constrain(expr`${budget} <= 50000`, "strict_board_budget_cap");
254
258
 
255
259
  // Rule 5: Innocent bystander rule
256
- m.addConstraint(engineers.ge(1), "minimum_one_employee");
260
+ m.constrain(expr`${engineers} >= 1`, "minimum_one_employee");
257
261
 
258
262
  const res = await m.check({ solver: "z3" });
259
263
 
@@ -265,35 +269,54 @@ console.log("Conflicting constraints:", res.unsatCore);
265
269
 
266
270
  ---
267
271
 
268
- ### 5. Python PuLP Compatibility Layer
272
+ ### 5. Solving with GLPK (GPL-3.0)
273
+
274
+ To use the GNU Linear Programming Kit (GLPK) solver plugin:
275
+
276
+ ```ts
277
+ import { Model, expr } from "@ohtaman/washi";
278
+ import "@ohtaman/washi/solvers/glpk";
279
+
280
+ const m = new Model({ solver: "glpk" });
281
+ const x = m.integer("x", { lb: 0 });
282
+ const y = m.integer("y", { lb: 0 });
283
+
284
+ m.maximize(expr`30 * ${x} + 50 * ${y}`);
285
+ m.constrain(expr`${x} + 2 * ${y} <= 40`, "wood_limit");
286
+ m.constrain(expr`2 * ${x} + ${y} <= 50`, "labor_limit");
287
+
288
+ const status = await m.optimize();
289
+ console.log(`Status: ${status}`); // "Optimal"
290
+ console.log(`Objective: ${m.objectiveValue}`); // 1100
291
+ ```
292
+
293
+ ---
294
+
295
+ ### 6. Vector Operations & Fast Dot Product
269
296
 
270
- If you have existing Python PuLP code, port it with near zero friction:
297
+ Cleanly compute 1D/2D linear combinations without nested loops:
271
298
 
272
299
  ```ts
273
- import {
274
- LpProblem,
275
- LpVariable,
276
- LpMaximize,
277
- LpInteger,
278
- HiGHS,
279
- value,
280
- } from "@ohtaman/washi";
281
-
282
- const prob = new LpProblem("PuLPDemo", LpMaximize);
283
-
284
- const x = new LpVariable("x", 0, undefined, LpInteger);
285
- const y = new LpVariable("y", 0, undefined, LpInteger);
286
-
287
- // PuLP += equivalent
288
- prob.add(x.mul(30).add(y.mul(50)));
289
- prob.add(x.add(y.mul(2)).le(40));
290
- prob.add(x.mul(2).add(y).le(50));
291
-
292
- await prob.solve(new HiGHS());
293
-
294
- console.log(`Status: ${prob.status}`);
295
- console.log(`Objective: ${value(prob.objective)}`);
296
- console.log(`x = ${value(x)}, y = ${value(y)}`);
300
+ import { Model, dot } from "@ohtaman/washi";
301
+
302
+ const m = new Model();
303
+ const factories = ["Tokyo", "Osaka"];
304
+ const stores = ["StoreA", "StoreB"];
305
+
306
+ const costs = {
307
+ Tokyo: { StoreA: 4, StoreB: 7 },
308
+ Osaka: { StoreA: 8, StoreB: 3 },
309
+ };
310
+
311
+ const ship = m.continuous(factories, stores);
312
+
313
+ // Total transportation cost in one line:
314
+ m.minimize(dot(costs, ship));
315
+ await m.optimize();
316
+
317
+ // Extract all optimal variable values as a structured object
318
+ const solution = m.getValues(ship);
319
+ console.log(solution);
297
320
  ```
298
321
 
299
322
  ---
@@ -341,7 +364,52 @@ The package also includes `dist/browser/washi.min.js`, which exports the `Washi`
341
364
 
342
365
  For CDN usage, replace the local URL with `https://cdn.jsdelivr.net/npm/@ohtaman/washi@0.1.2/dist/browser/washi.min.js` (or pin your desired release version `>= 0.1.2`). For ESM use the corresponding `dist/browser/washi.js` URL. Selecting the CDN URL is an explicit choice; local copies never switch to a CDN.
343
366
 
344
- The browser builds include the HiGHS engine, not the optional GLPK/Z3 engines. Use the npm package and optional dependencies for those integrations. `examples/distribution/` contains complete ESM and script examples.
367
+ ### Solver Plugins in the Browser (GLPK & Z3)
368
+
369
+ The browser distribution includes pre-bundled, standalone solver plugins under `dist/browser/solvers/` (available from `>= 0.2.0` or `@latest` on CDN):
370
+ - **GLPK**: `dist/browser/solvers/glpk.js` (ESM) and `dist/browser/solvers/glpk.min.js` (IIFE, registers global `WashiGLPK`)
371
+ - **Z3**: `dist/browser/solvers/z3.js` (ESM) and `dist/browser/solvers/z3.min.js` (IIFE, registers global `WashiZ3`), accompanied by `z3-built.js` and `z3-built.wasm`
372
+
373
+ #### Browser ESM with GLPK
374
+ ```html
375
+ <script type="module">
376
+ import { Model } from "./washi/washi.js";
377
+ import "./washi/solvers/glpk.js";
378
+
379
+ const model = new Model({ solver: "glpk" });
380
+ const x = model.addVar({ lb: 0, ub: 10 });
381
+ model.maximize(x);
382
+ await model.solve();
383
+ console.log("GLPK status:", model.status);
384
+ </script>
385
+ ```
386
+
387
+ #### Plain Script with GLPK
388
+ ```html
389
+ <script src="./washi/washi.min.js"></script>
390
+ <script src="./washi/solvers/glpk.min.js"></script>
391
+ <script>
392
+ const model = new Washi.Model({ solver: "glpk" });
393
+ const x = model.addVar({ lb: 0, ub: 10 });
394
+ model.maximize(x);
395
+ model.solve().then(status => console.log("GLPK status:", status));
396
+ </script>
397
+ ```
398
+
399
+ > [!NOTE]
400
+ > **Z3 in Browser Environments & Cross-Origin Isolation**:
401
+ > Because the Z3 WebAssembly engine uses multithreading (`SharedArrayBuffer`), modern browsers require cross-origin isolation. The HTML document response must send:
402
+ > ```http
403
+ > Cross-Origin-Opener-Policy: same-origin
404
+ > Cross-Origin-Embedder-Policy: require-corp
405
+ > ```
406
+ > And any cross-origin assets must serve `Cross-Origin-Resource-Policy: cross-origin`.
407
+ >
408
+ > General static sites without custom header control (e.g. standard GitHub Pages or preview platforms) and third-party iframe embeds (e.g. CodePen / JSFiddle) do not satisfy these requirements without extra setup. Importing `@ohtaman/washi` never automatically injects or registers a Service Worker into consumer applications.
409
+ >
410
+ > To enable Z3 on GitHub Pages, the official WaSHI Web Demo bundles a dedicated `coi-serviceworker.js` strictly scoped to `/washi/`. On fresh browser sessions, it registers the worker and reloads once to establish `window.crossOriginIsolated === true` before running Z3. Environments lacking Service Workers or failing isolation abort Z3 execution with a descriptive explanation rather than hanging.
411
+
412
+ Complete standalone examples for ESM, script tags, and CodePen are available in `examples/distribution/`.
345
413
 
346
414
  ### Offline distribution, Workers and custom hosting
347
415
 
@@ -364,32 +432,54 @@ Failed initialization reports the attempted URL and does not try a different hos
364
432
 
365
433
  ---
366
434
 
367
- ## 💻 CLI & Interactive Demos
435
+ ## 🛠️ Custom Solvers & Extensibility
368
436
 
369
- Ready-to-run standalone examples are available in `examples/node/`:
437
+ WaSHI features a decoupled, pluggable solver architecture. You can easily build and register your own custom solver adapter (e.g., for cloud REST optimizers, WebAssembly engines, or proprietary solvers):
370
438
 
371
- ```bash
372
- npx tsx examples/node/01-production-planning.ts
373
- npx tsx examples/node/02-knapsack-problem.ts
374
- npx tsx examples/node/03-portfolio-qp.ts
375
- npx tsx examples/node/04-smt-security-rules.ts
376
- npx tsx examples/node/05-unsat-core-diagnosis.ts
377
- npx tsx examples/node/06-pulp-compatibility.ts
378
- ```
439
+ - See the complete [Custom Solver Developer Guide](docs/custom-solver-guide.md) for step-by-step tutorials, complete API contracts (`Solver`, `LpModelData`, `SolverResult`), and testing best practices.
440
+
441
+ ---
442
+
443
+ ## 💻 Examples and browser demo
379
444
 
380
- To run the interactive browser showcase (`examples/web-demo`):
445
+ Start with the [example gallery](https://github.com/ohtaman/washi/blob/main/examples/README.md) or the [online Example & Feature Finder](https://ohtaman.github.io/washi/examples/), which organize examples by problem category and by WaSHI API feature.
446
+
447
+ | Category | Example | What it demonstrates | Solver |
448
+ | :--- | :--- | :--- | :--- |
449
+ | **Fundamentals** | [Production planning](https://github.com/ohtaman/washi/blob/main/examples/node/01-production-planning.ts) | Integer variables, a profit objective, and resource constraints (`expr`, `c`) | HiGHS |
450
+ | **Fundamentals** | [Transportation network](https://github.com/ohtaman/washi/blob/main/examples/node/09-transportation-problem.ts) | 2D origin-destination variable matrix (`addVars(rows, cols)`) and flow balance | HiGHS |
451
+ | **Fundamentals** | [Diet & sensitivity analysis](https://github.com/ohtaman/washi/blob/main/examples/node/10-diet-sensitivity.ts) | Shadow prices (`constr.dual`), surpluses (`constr.slack`), and reduced costs (`var.reducedCost`) | HiGHS |
452
+ | **Practical** | [Staff shift scheduling](https://github.com/ohtaman/washi/blob/main/examples/node/11-shift-scheduling.ts) | Weekly roster optimization + diagnosing conflicting Saturday vacation requests | HiGHS + Z3 |
453
+ | **Practical** | [E-commerce coupon optimizer](https://github.com/ohtaman/washi/blob/main/examples/node/12-ecommerce-coupon-optimizer.ts) | Conditional spend thresholds (`addIndicator`), coupon exclusivity, and add-on recommendations | HiGHS |
454
+ | **Practical** | [Portfolio allocation](https://github.com/ohtaman/washi/blob/main/examples/node/03-portfolio-qp.ts) | A convex quadratic objective (`QuadExpr`) with allocation constraints | HiGHS |
455
+ | **Logic / SMT** | [Business rule checks](https://github.com/ohtaman/washi/blob/main/examples/node/04-smt-security-rules.ts) | Arithmetic and Boolean constraints (`addBoolVar`, `implies`) with satisfiability checking | Z3 |
456
+ | **Logic / SMT** | [Conflict diagnosis](https://github.com/ohtaman/washi/blob/main/examples/node/05-unsat-core-diagnosis.ts) | Named constraints and an unsatisfiable core (`unsatCore`) | Z3 |
457
+
458
+ [Open the hosted demo](https://ohtaman.github.io/washi/demo/) to edit and run models in your browser. For browser ESM, classic script tags, and CodePen, see the [distribution examples](https://github.com/ohtaman/washi/blob/main/examples/distribution/README.md). Z3 requires cross-origin isolation; see the browser requirements above.
459
+
460
+ To run examples locally, clone the repository and install its development dependencies first:
381
461
 
382
462
  ```bash
463
+ git clone https://github.com/ohtaman/washi.git
464
+ cd washi
465
+ npm ci
466
+ npx tsx examples/node/01-production-planning.ts
467
+ npx tsx examples/node/09-transportation-problem.ts
468
+ npx tsx examples/node/10-diet-sensitivity.ts
469
+ npx tsx examples/node/11-shift-scheduling.ts
470
+ npx tsx examples/node/12-ecommerce-coupon-optimizer.ts
471
+
472
+ # Local browser demo: http://localhost:5173/demo/
383
473
  npm run dev
384
- # Documentation: http://localhost:5173/
385
- # Interactive Demo: http://localhost:5173/demo/
386
474
  ```
387
475
 
476
+ The example sources import the repository's `src/` modules. In an application using the published package, import `@ohtaman/washi` and the required solver plugin instead.
477
+
388
478
  ---
389
479
 
390
480
  ## 🧪 Testing
391
481
 
392
- WaSHI is developed with strict Test-Driven Development (TDD) across 120+ unit, integration, and large-scale test cases:
482
+ Run the repository's unit and integration tests, and build the distributions:
393
483
 
394
484
  ```bash
395
485
  # Run all unit and integration tests