@ohtaman/washi 0.2.0 → 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 (38) hide show
  1. package/README.ja.md +131 -115
  2. package/README.md +131 -115
  3. package/README.zh-CN.md +133 -117
  4. package/dist/browser/solvers/glpk.js +10 -2
  5. package/dist/browser/solvers/glpk.min.js +1 -1
  6. package/dist/browser/solvers/z3.js +34 -12
  7. package/dist/browser/solvers/z3.min.js +2 -2
  8. package/dist/browser/washi.js +368 -306
  9. package/dist/browser/washi.min.js +4 -4
  10. package/dist/lib/{constraint-XWdn0ID9.js → constraint-BRuB3ooC.js} +0 -3
  11. package/dist/lib/solvers/glpk.js +10 -2
  12. package/dist/lib/solvers/z3.js +35 -10
  13. package/dist/lib/washi.js +368 -321
  14. package/dist/types/index.d.ts +5 -7
  15. package/dist/types/index.d.ts.map +1 -1
  16. package/dist/types/modeler/constraint.d.ts +0 -3
  17. package/dist/types/modeler/constraint.d.ts.map +1 -1
  18. package/dist/types/modeler/expression.d.ts +16 -10
  19. package/dist/types/modeler/expression.d.ts.map +1 -1
  20. package/dist/types/modeler/index.d.ts +2 -4
  21. package/dist/types/modeler/index.d.ts.map +1 -1
  22. package/dist/types/modeler/model.d.ts +90 -11
  23. package/dist/types/modeler/model.d.ts.map +1 -1
  24. package/dist/types/modeler/solvers/glpk/solver.d.ts.map +1 -1
  25. package/dist/types/modeler/solvers/highs/solver.d.ts +1 -1
  26. package/dist/types/modeler/solvers/highs/solver.d.ts.map +1 -1
  27. package/dist/types/modeler/solvers/types.d.ts +1 -1
  28. package/dist/types/modeler/solvers/z3/loader.d.ts +12 -0
  29. package/dist/types/modeler/solvers/z3/loader.d.ts.map +1 -1
  30. package/dist/types/modeler/types.d.ts +8 -3
  31. package/dist/types/modeler/types.d.ts.map +1 -1
  32. package/dist/types/modeler/variable.d.ts +1 -6
  33. package/dist/types/modeler/variable.d.ts.map +1 -1
  34. package/package.json +10 -2
  35. package/dist/types/modeler/mip-compat.d.ts +0 -43
  36. package/dist/types/modeler/mip-compat.d.ts.map +0 -1
  37. package/dist/types/modeler/pulp-compat.d.ts +0 -67
  38. package/dist/types/modeler/pulp-compat.d.ts.map +0 -1
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
51
+
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.
49
56
 
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.
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,7 @@ 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";
203
206
  import "@ohtaman/washi/solvers/z3";
204
207
 
205
208
  const m = new Model({ name: "SmartSecurityRules" });
@@ -208,16 +211,16 @@ const isNight = m.addBoolVar("isNight");
208
211
  const motion = m.addBoolVar("motion");
209
212
  const alarm = m.addBoolVar("alarm");
210
213
  const siren = m.addBoolVar("siren");
211
- const lux = m.addVar({ name: "lux", type: "Integer", lb: 0 });
214
+ const lux = m.integer("lux", { lb: 0 });
212
215
 
213
216
  // Logical rules
214
- m.addConstraint(lux.le(50).implies(isNight), "dark_implies_night");
215
- 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");
216
219
 
217
220
  // Current sensor observations
218
- m.addConstraint(lux.le(10), "sensor_dark");
219
- m.addConstraint(motion, "sensor_motion");
220
- 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");
221
224
 
222
225
  // Check satisfiability and deduce state without an objective function
223
226
  const res = await m.check({ solver: "z3" });
@@ -233,28 +236,28 @@ console.log(`Siren active? ${m.values.get("siren")}`); // true
233
236
  When business rules or constraints contradict each other, extract a **conflicting constraint subset** (unsatisfiable core; minimality is not guaranteed) for troubleshooting (requires Z3):
234
237
 
235
238
  ```ts
236
- import { Model } from "@ohtaman/washi";
239
+ import { Model, expr } from "@ohtaman/washi";
237
240
  import "@ohtaman/washi/solvers/z3";
238
241
 
239
242
  const m = new Model({ name: "ConflictDiagnosis", solver: "z3" });
240
- const engineers = m.addVar({ name: "engineers", type: "Integer", lb: 0, ub: 20 });
241
- 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 });
242
245
  const rushDelivery = m.addBoolVar("rushDelivery");
243
246
 
244
247
  // Rule 1: Each engineer costs $8,000
245
- m.addConstraint(engineers.mul(8000).le(budget), "budget_limit");
248
+ m.constrain(expr`8000 * ${engineers} <= ${budget}`, "budget_limit");
246
249
 
247
250
  // Rule 2: Customer requires rush delivery
248
- m.addConstraint(rushDelivery, "rush_required");
251
+ m.constrain(rushDelivery, "rush_required");
249
252
 
250
253
  // Rule 3: Rush delivery requires at least 8 engineers
251
- m.addConstraint(rushDelivery.implies(engineers.ge(8)), "rush_staffing_rule");
254
+ m.constrain(rushDelivery.implies(engineers.ge(8)), "rush_staffing_rule");
252
255
 
253
256
  // Rule 4: Hard board budget cap of $50,000 (Conflict: 8 * $8000 = $64,000 > $50,000!)
254
- m.addConstraint(budget.le(50000), "strict_board_budget_cap");
257
+ m.constrain(expr`${budget} <= 50000`, "strict_board_budget_cap");
255
258
 
256
259
  // Rule 5: Innocent bystander rule
257
- m.addConstraint(engineers.ge(1), "minimum_one_employee");
260
+ m.constrain(expr`${engineers} >= 1`, "minimum_one_employee");
258
261
 
259
262
  const res = await m.check({ solver: "z3" });
260
263
 
@@ -271,53 +274,49 @@ console.log("Conflicting constraints:", res.unsatCore);
271
274
  To use the GNU Linear Programming Kit (GLPK) solver plugin:
272
275
 
273
276
  ```ts
274
- import { Model, expr, c } from "@ohtaman/washi";
277
+ import { Model, expr } from "@ohtaman/washi";
275
278
  import "@ohtaman/washi/solvers/glpk";
276
279
 
277
- const m = new Model({ sense: "Maximize", solver: "glpk" });
278
- const x = m.addVar({ type: "Integer", lb: 0 });
279
- const y = m.addVar({ type: "Integer", lb: 0 });
280
+ const m = new Model({ solver: "glpk" });
281
+ const x = m.integer("x", { lb: 0 });
282
+ const y = m.integer("y", { lb: 0 });
280
283
 
281
284
  m.maximize(expr`30 * ${x} + 50 * ${y}`);
282
- m.addConstraint(c`${x} + 2 * ${y} <= 40`, "wood_limit");
283
- m.addConstraint(c`2 * ${x} + ${y} <= 50`, "labor_limit");
285
+ m.constrain(expr`${x} + 2 * ${y} <= 40`, "wood_limit");
286
+ m.constrain(expr`2 * ${x} + ${y} <= 50`, "labor_limit");
284
287
 
285
- const status = await m.solve();
288
+ const status = await m.optimize();
286
289
  console.log(`Status: ${status}`); // "Optimal"
287
290
  console.log(`Objective: ${m.objectiveValue}`); // 1100
288
291
  ```
289
292
 
290
293
  ---
291
294
 
292
- ### 6. Python PuLP Compatibility Layer
295
+ ### 6. Vector Operations & Fast Dot Product
293
296
 
294
- If you have existing Python PuLP code, port it with near zero friction:
297
+ Cleanly compute 1D/2D linear combinations without nested loops:
295
298
 
296
299
  ```ts
297
- import {
298
- LpProblem,
299
- LpVariable,
300
- LpMaximize,
301
- LpInteger,
302
- HiGHS,
303
- value,
304
- } from "@ohtaman/washi";
305
-
306
- const prob = new LpProblem("PuLPDemo", LpMaximize);
307
-
308
- const x = new LpVariable("x", 0, undefined, LpInteger);
309
- const y = new LpVariable("y", 0, undefined, LpInteger);
310
-
311
- // PuLP += equivalent
312
- prob.add(x.mul(30).add(y.mul(50)));
313
- prob.add(x.add(y.mul(2)).le(40));
314
- prob.add(x.mul(2).add(y).le(50));
315
-
316
- await prob.solve(new HiGHS());
317
-
318
- console.log(`Status: ${prob.status}`);
319
- console.log(`Objective: ${value(prob.objective)}`);
320
- 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);
321
320
  ```
322
321
 
323
322
  ---
@@ -398,13 +397,17 @@ The browser distribution includes pre-bundled, standalone solver plugins under `
398
397
  ```
399
398
 
400
399
  > [!NOTE]
401
- > **Z3 in Browser Environments**:
402
- > Because the Z3 WebAssembly engine uses multithreading (`SharedArrayBuffer`), the hosting web server must send Cross-Origin-Isolation delivery headers on the document response:
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:
403
402
  > ```http
404
403
  > Cross-Origin-Opener-Policy: same-origin
405
404
  > Cross-Origin-Embedder-Policy: require-corp
406
405
  > ```
407
- > And any cross-origin assets (e.g. from a CDN) must serve `Cross-Origin-Resource-Policy: cross-origin`.
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.
408
411
 
409
412
  Complete standalone examples for ESM, script tags, and CodePen are available in `examples/distribution/`.
410
413
 
@@ -437,33 +440,46 @@ WaSHI features a decoupled, pluggable solver architecture. You can easily build
437
440
 
438
441
  ---
439
442
 
440
- ## 💻 CLI & Interactive Demos
443
+ ## 💻 Examples and browser demo
441
444
 
442
- Ready-to-run standalone examples are available in `examples/node/`:
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.
443
446
 
444
- ```bash
445
- npx tsx examples/node/01-production-planning.ts
446
- npx tsx examples/node/02-knapsack-problem.ts
447
- npx tsx examples/node/03-portfolio-qp.ts
448
- npx tsx examples/node/04-smt-security-rules.ts
449
- npx tsx examples/node/05-unsat-core-diagnosis.ts
450
- npx tsx examples/node/06-pulp-compatibility.ts
451
- npx tsx examples/node/08-glpk-solver-plugin.ts
452
- ```
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 |
453
457
 
454
- To run the interactive browser showcase (`examples/web-demo`):
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:
455
461
 
456
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/
457
473
  npm run dev
458
- # Documentation: http://localhost:5173/
459
- # Interactive Demo: http://localhost:5173/demo/
460
474
  ```
461
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
+
462
478
  ---
463
479
 
464
480
  ## 🧪 Testing
465
481
 
466
- 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:
467
483
 
468
484
  ```bash
469
485
  # Run all unit and integration tests