@ohtaman/washi 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +103 -421
- package/README.md +103 -421
- package/README.zh-CN.md +103 -421
- package/THIRD_PARTY_LICENSES.md +5 -3
- package/dist/browser/solvers/glpk.js +2 -2
- package/dist/browser/solvers/glpk.min.js +1 -1
- package/dist/browser/washi.js +777 -261
- package/dist/browser/washi.min.js +3 -3
- package/dist/lib/{registry-C20sxTwm.js → registry-D7TZ3Wh_.js} +88 -18
- package/dist/lib/solvers/glpk.js +3 -3
- package/dist/lib/solvers/z3.js +1 -1
- package/dist/lib/washi.js +864 -421
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/modeler/expression.d.ts +47 -26
- package/dist/types/modeler/expression.d.ts.map +1 -1
- package/dist/types/modeler/index.d.ts +2 -2
- package/dist/types/modeler/index.d.ts.map +1 -1
- package/dist/types/modeler/model.d.ts +12 -6
- package/dist/types/modeler/model.d.ts.map +1 -1
- package/dist/types/modeler/parser.d.ts +25 -5
- package/dist/types/modeler/parser.d.ts.map +1 -1
- package/dist/types/modeler/solvers/glpk/solver.d.ts.map +1 -1
- package/dist/types/modeler/solvers/highs/solver.d.ts.map +1 -1
- package/dist/types/modeler/solvers/highs/worker-client.d.ts.map +1 -1
- package/dist/types/modeler/solvers/types.d.ts +2 -0
- package/dist/types/modeler/solvers/types.d.ts.map +1 -1
- package/docs/examples/diet-sensitivity.md +15 -14
- package/docs/examples/ecommerce-coupons.md +15 -22
- package/docs/examples/index.md +14 -12
- package/docs/examples/knapsack.md +8 -7
- package/docs/examples/portfolio-qp.md +11 -4
- package/docs/examples/production.md +4 -4
- package/docs/examples/shift-scheduling.md +20 -7
- package/docs/examples/smt-security.md +16 -4
- package/docs/examples/transportation.md +46 -19
- package/docs/examples/traveling-salesperson.md +28 -44
- package/docs/examples/unsat-core.md +15 -5
- package/docs/examples/vehicle-routing.md +10 -8
- package/docs/getting-started/ai-agents.md +25 -14
- package/docs/getting-started/cross-origin.md +56 -28
- package/docs/getting-started/index.md +40 -69
- package/docs/getting-started/runtimes.md +66 -61
- package/docs/guide/constraints.md +46 -40
- package/docs/guide/decomposition.md +127 -259
- package/docs/guide/expressions.md +90 -39
- package/docs/guide/formulation-tips.md +212 -0
- package/docs/guide/objectives.md +7 -4
- package/docs/guide/unsat-core.md +18 -6
- package/docs/guide/variables.md +33 -38
- package/docs/ja/getting-started/ai-agents.md +24 -14
- package/docs/solvers/highs.md +27 -6
- package/docs/solvers/index.md +1 -1
- package/docs/solvers/z3.md +3 -0
- package/docs/zh/getting-started/ai-agents.md +24 -14
- package/llms-full.txt +929 -687
- package/llms.txt +21 -19
- package/package.json +3 -3
- package/skills/washi/SKILL.md +14 -10
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
| Problem Class | Solver | Key APIs | Links |
|
|
4
4
|
| :--- | :--- | :--- | :--- |
|
|
5
|
-
| Combinatorial OR / Dantzig-Wolfe | HiGHS (default) | `m.solveColumnGeneration()`, `pricing`, `
|
|
5
|
+
| Combinatorial OR / Dantzig-Wolfe | HiGHS (default) | `m.solveColumnGeneration()`, `pricing`, `m.binary()`, `m.constraint()`, `expr` (`sum`) | <ul><li><a href="https://github.com/ohtaman/washi/blob/main/examples/node/13-vehicle-routing-column-generation.ts">Source Code</a></li><li><a href="/guide/decomposition">Decomposition Guide</a></li></ul> |
|
|
6
6
|
|
|
7
7
|
The **Capacitated Vehicle Routing Problem (CVRP)** determines the minimum number of delivery vehicles needed to visit a set of customers, subject to vehicle cargo capacity, maximum customer stops per shift, and route duration limits.
|
|
8
8
|
|
|
@@ -24,7 +24,7 @@ We consider a distribution network with a central depot at $(0, 0)$ and **50 cus
|
|
|
24
24
|
<CodeRunner>
|
|
25
25
|
|
|
26
26
|
```typescript
|
|
27
|
-
import { Model, expr, minimize
|
|
27
|
+
import { Model, expr, minimize } from "@ohtaman/washi";
|
|
28
28
|
|
|
29
29
|
// --- Extracted Routing Helpers ---
|
|
30
30
|
|
|
@@ -141,13 +141,12 @@ async function runCVRP() {
|
|
|
141
141
|
|
|
142
142
|
// Initial naive routes: 1 dedicated vehicle per customer
|
|
143
143
|
const initialRoutes = customers.map((c) => master.binary(`route_init_${c.id}`));
|
|
144
|
-
master.objective(minimize(sum(initialRoutes)));
|
|
144
|
+
master.objective(minimize(expr`sum(${initialRoutes})`));
|
|
145
145
|
|
|
146
146
|
// Each customer must be visited by exactly 1 vehicle
|
|
147
|
-
|
|
148
|
-
master.constraint(expr`${initialRoutes[i]} == 1`, `visit_${
|
|
149
|
-
|
|
150
|
-
const visitConstrs = master.constraints;
|
|
147
|
+
const visitConstrs = customers.map((c, i) =>
|
|
148
|
+
master.constraint(expr`${initialRoutes[i]} == 1`, `visit_${c.id}`)
|
|
149
|
+
);
|
|
151
150
|
|
|
152
151
|
console.log(`Initial state: 50 naive single-customer routes (50 vehicles).`);
|
|
153
152
|
console.log("Solving via Column Generation with dual-directed heuristic pricing...");
|
|
@@ -187,7 +186,7 @@ async function runCVRP() {
|
|
|
187
186
|
console.log(`Total Columns Generated: ${result.columnsAdded}`);
|
|
188
187
|
|
|
189
188
|
console.log("\nSample routes selected in fleet:");
|
|
190
|
-
const activeRoutes = result.generatedColumns.filter((c) =>
|
|
189
|
+
const activeRoutes = result.generatedColumns.filter((c) => c.variable.value === 1);
|
|
191
190
|
for (const r of activeRoutes.slice(0, 5)) {
|
|
192
191
|
const meta = r.proposal.meta;
|
|
193
192
|
console.log(` ${meta?.tour.join(" -> ")} (Transit: ${meta?.travelTime}m, Total: ${meta?.totalTime}m)`);
|
|
@@ -204,3 +203,6 @@ runCVRP();
|
|
|
204
203
|
- **Master Problem and Pricing Separation**: The master problem solves the LP relaxation and final set-partitioning MIP, while the `pricing` callback searches for negative-reduced-cost routes using cheapest insertion and 2-opt local search.
|
|
205
204
|
- **Dual Value Access**: Dual values from the master LP relaxation are read from `visitConstrs[i].dual` to prioritize customers with higher shadow prices during route construction.
|
|
206
205
|
- **Route Metadata**: Each proposed column attaches `{ tour, travelTime, totalTime }` under `meta`, allowing the ordered stop sequence to be inspected from `result.generatedColumns` after the solve completes.
|
|
206
|
+
- **Set Partitioning (`== 1`) vs. Set Covering (`>= 1`)**: This example uses `== 1` so that the final MIP never covers a customer twice. In large-scale column generation, formulating coverage as `>= 1` restricts the dual prices to $\pi_i \ge 0$, avoiding negative dual oscillations; if the final MIP selects overlapping routes, duplicate visits can be removed by post-processing whenever distances satisfy the triangle inequality.
|
|
207
|
+
- **Heuristic Pricing vs. Exact ESPPRC**: Because `findImprovingRoute` uses a fast greedy heuristic (cheapest insertion + 2-opt) instead of an exact Elementary Shortest Path Problem with Resource Constraints (ESPPRC) solver, returning `null` means no further improving columns were found by the heuristic rather than proving global LP optimality.
|
|
208
|
+
- **Price-and-Branch (`finalizeAsMIP: true`)**: Solving the Restricted Master Problem as a MIP over the columns generated at the root node is known as the *Restricted Master Heuristic* (Price-and-Branch). It produces near-optimal integer fleets quickly without requiring full Branch-and-Price tree search.
|
|
@@ -57,7 +57,7 @@ import "@ohtaman/washi/solvers/glpk";
|
|
|
57
57
|
1. **Bundled WASM by default (no implicit CDN fallback)**:
|
|
58
58
|
- In Node.js and modern bundlers, WebAssembly assets resolve relative to the module automatically.
|
|
59
59
|
- 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.
|
|
60
|
-
- 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
|
|
60
|
+
- 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 WaSHI's bundled local `highs.wasm` runtime).
|
|
61
61
|
2. **Z3 requires a Secure Context, Cross-Origin Isolation, and CORS in browsers**:
|
|
62
62
|
- 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:
|
|
63
63
|
- `Cross-Origin-Opener-Policy: same-origin` (on the document)
|
|
@@ -66,7 +66,7 @@ import "@ohtaman/washi/solvers/glpk";
|
|
|
66
66
|
- 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.
|
|
67
67
|
3. **Web Workers & Cancellation (`AbortSignal`)**:
|
|
68
68
|
- Pre-solve cancellation (`signal.aborted` before execution starts) is supported across all solvers.
|
|
69
|
-
- 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).
|
|
69
|
+
- In browsers where `Worker` is available on a non-opaque effective origin (`window.origin !== "null"`), HiGHS runs in a dedicated Web Worker by default (`useWorker: true`). On opaque effective origins (`window.origin === "null"`, such as `file:` URLs or `sandbox="allow-scripts"` `<iframe>` embeds without `allow-same-origin`), HiGHS defaults to main-thread execution (`useWorker: false`). Passing `{ signal }` to `m.solve({ signal })` preemptively terminates an in-flight Worker solve (or cancels queued Worker requests).
|
|
70
70
|
- 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.
|
|
71
71
|
|
|
72
72
|
---
|
|
@@ -82,6 +82,7 @@ const m = new Model("ExampleModel");
|
|
|
82
82
|
|
|
83
83
|
// Scalar variables
|
|
84
84
|
const x = m.continuous("x", { lb: 0, ub: 100 });
|
|
85
|
+
const y = m.continuous("y", { lb: 0, ub: 100 });
|
|
85
86
|
const n = m.integer("n", { lb: 0, ub: 10 });
|
|
86
87
|
const b = m.binary("b");
|
|
87
88
|
const flag = m.bool("flag"); // Propositional boolean (for Z3 / logical constraints)
|
|
@@ -103,12 +104,12 @@ const mixed = m.binary(["A", "B"] as const, 3); // Record<"A" | "B", Var[]>
|
|
|
103
104
|
### Step 2: Build Expressions (`expr`, `sum`, `dot`)
|
|
104
105
|
|
|
105
106
|
- **Unified `expr` template tag**:
|
|
106
|
-
- Linear expression (`LinExpr`): `` expr`30 * ${x} + 50 * ${n} - 10` ``
|
|
107
|
-
- Quadratic expression (`QuadExpr`): `` expr`${x}^2 + 2 * ${x} * ${
|
|
108
|
-
- Linear constraint (`Constraint`): `` expr`${x} + 2 * ${n} <= 40` `` (supports `<=`, `>=`, `==`, and `=`).
|
|
109
|
-
- **Aggregations**:
|
|
110
|
-
- `sum(
|
|
111
|
-
- `dot(a, b)` computes the inner product of matching 1D arrays, 2D arrays (`number[][]` / `Var[][]
|
|
107
|
+
- Linear expression (`LinExpr`): `` expr`30 * ${x} + 50 * ${n} - 10` ``, `` expr`dot(${profit}, ${byItem}) - 20 * ${b}` ``, `` expr`sum((${price} - ${cost}) * ${byItem})` ``
|
|
108
|
+
- Quadratic expression (`QuadExpr`): `` expr`${x}^2 + 2 * ${x} * ${y} + ${y}^2` ``, `` expr`sum((${vec} - ${target})^2)` `` (supported for convex QP objectives on purely continuous models using `m.continuous`; built-in HiGHS does not support MIQP models containing integer, binary, or boolean variables).
|
|
109
|
+
- Linear constraint (`Constraint`): `` expr`${x} + 2 * ${n} <= 40` ``, `` expr`sum(${byItem}) <= 100` `` (supports `<=`, `>=`, `==`, and `=`).
|
|
110
|
+
- **Aggregations & Element-Wise Container Arithmetic**:
|
|
111
|
+
- `sum(containerOrItems, mapper?)` (usable inside `` expr`sum(...)` `` or standalone `sum(...)`) sums a 1D/2D `Array`, `Iterable`, or plain `Record` of `Var` / `LinExpr` / `number` (without needing `Object.values(record)`), or maps over items/counts via `sum(items, (item, i) => ...)`. Inside `` expr`sum(...)` ``, element-wise container arithmetic (`+`, `-`, `*`, `/`, `^2`) is also supported.
|
|
112
|
+
- `dot(a, b)` (usable inside `` expr`dot(${a}, ${b})` `` or standalone `dot(a, b)`) computes the inner product of matching 1D arrays, 2D arrays (`number[][]` / `Var[][]`, computing the Frobenius inner product $\sum_{i,j} A_{i,j} B_{i,j}$), plain records (`Record<string, ...>`), or mixed Array/Record containers in either operand order.
|
|
112
113
|
- **Fluent methods**:
|
|
113
114
|
- Arithmetic (on `LinExpr` and `QuadExpr`; `Var` supports `.add()`, `.sub()`, `.mul()`, `.neg()`): `.add(other)`, `.sub(other)`, `.mul(other)`, `.div(scalar)`, `.neg()`
|
|
114
115
|
- Comparisons (on `Var` and `LinExpr`): `.le(rhs, name?)`, `.ge(rhs, name?)`, `.eq(rhs, name?)`
|
|
@@ -122,8 +123,8 @@ Constructing an `expr` or `.le()` object does **not** attach it to the model unt
|
|
|
122
123
|
// Set objective using maximize() or minimize()
|
|
123
124
|
m.objective(maximize(expr`30 * ${x} + 50 * ${n}`));
|
|
124
125
|
|
|
125
|
-
// Register constraints on the model
|
|
126
|
-
m.constraint(expr`${x} + 2 * ${n} <= 40`, "resource_limit");
|
|
126
|
+
// Register constraints on the model (returns the registered Constraint / BoolExpr or array)
|
|
127
|
+
const limit = m.constraint(expr`${x} + 2 * ${n} <= 40`, "resource_limit");
|
|
127
128
|
|
|
128
129
|
// Indicator constraint (MIP Big-M reformulation: b === 1 => x <= 50)
|
|
129
130
|
m.indicator(b, expr`${x} <= 50`, { activeValue: 1, name: "cap_when_b" });
|
|
@@ -161,10 +162,13 @@ export async function runMipExample() {
|
|
|
161
162
|
const qty = m.integer(products, { lb: 0, ub: 100 });
|
|
162
163
|
const setup = m.binary("setup");
|
|
163
164
|
|
|
164
|
-
m.objective(maximize(expr
|
|
165
|
-
m.constraint(expr
|
|
166
|
-
m.constraint(expr
|
|
167
|
-
m.
|
|
165
|
+
m.objective(maximize(expr`dot(${profit}, ${qty}) - 20 * ${setup}`));
|
|
166
|
+
m.constraint(expr`dot(${woodPerUnit}, ${qty}) <= 40`, "wood_cap");
|
|
167
|
+
m.constraint(expr`dot(${laborPerUnit}, ${qty}) <= 50`, "labor_cap");
|
|
168
|
+
m.indicator(setup, expr`sum(${qty}) <= 0`, {
|
|
169
|
+
activeValue: 0,
|
|
170
|
+
name: "require_setup",
|
|
171
|
+
});
|
|
168
172
|
|
|
169
173
|
const status = await m.solve();
|
|
170
174
|
const report = m.validateSolution();
|
|
@@ -269,3 +273,10 @@ export function verifyNegativePatterns() {
|
|
|
269
273
|
}
|
|
270
274
|
```
|
|
271
275
|
|
|
276
|
+
### Practical OR Modeling Guardrails
|
|
277
|
+
|
|
278
|
+
- **Prefer inequalities (`>=` / `<=`) over exact equalities (`==`)** for demand coverage and resource limits when objective costs are positive; reserve `==` for physical conservation laws (e.g., flow balance, portfolio weights summing to `1`).
|
|
279
|
+
- **Always provide finite `lb` and `ub` bounds** on variables referenced inside `m.indicator(...)` and omit `M` so WaSHI automatically derives the tightest valid Big-M instead of falling back to `1e6`.
|
|
280
|
+
- **Use soft constraints (elastic slack variables)** for non-rigid business targets (e.g., preferred staffing, safety stock, day-off requests) by adding a non-negative slack variable (`lb: 0`) with a tiered objective penalty, reserving hard constraints for physical or legal invariants.
|
|
281
|
+
|
|
282
|
+
|
|
@@ -1,47 +1,70 @@
|
|
|
1
1
|
# Cross-Origin Isolation (Z3 & Browsers)
|
|
2
2
|
|
|
3
|
-
When running the **Z3** solver
|
|
3
|
+
When running the **Z3** solver inside a web browser, Cross-Origin Isolation HTTP headers are required so the browser enables `SharedArrayBuffer`.
|
|
4
|
+
|
|
5
|
+
> [!TIP]
|
|
6
|
+
> **Not needed for HiGHS, GLPK, or Node.js**: The built-in **HiGHS** solver and **GLPK** plugin run in standard browser contexts without Cross-Origin Isolation or `SharedArrayBuffer`. Running Z3 in **Node.js** also requires no HTTP headers. You only need this page when using `@ohtaman/washi/solvers/z3` in the browser.
|
|
4
7
|
|
|
5
8
|
---
|
|
6
9
|
|
|
7
10
|
## Why is Cross-Origin Isolation Required?
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
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**.
|
|
12
|
+
Z3's WebAssembly build uses multithreading (pthreads) backed by `SharedArrayBuffer` and Web Workers. Modern browsers (Chrome, Firefox, Safari, Edge) disable `SharedArrayBuffer` by default to mitigate Spectre-style side-channel attacks unless the top-level document opts into **Cross-Origin Isolation** (`window.crossOriginIsolated === true`).
|
|
12
13
|
|
|
13
14
|
---
|
|
14
15
|
|
|
15
16
|
## Required HTTP Response Headers
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
Serve the top-level HTML document with:
|
|
18
19
|
|
|
19
20
|
```http
|
|
20
21
|
Cross-Origin-Opener-Policy: same-origin
|
|
21
22
|
Cross-Origin-Embedder-Policy: require-corp
|
|
22
23
|
```
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
If your page loads cross-origin subresources (such as scripts, images, or WASM files from another domain), those responses must also include `Cross-Origin-Resource-Policy: cross-origin` (or CORS headers).
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
Cross-Origin-
|
|
28
|
-
```
|
|
27
|
+
> [!NOTE]
|
|
28
|
+
> Chromium and Firefox also support `Cross-Origin-Embedder-Policy: credentialless`, which relaxes subresource CORP requirements, **but Safari (WebKit) does not support `credentialless`**. Use `require-corp` if you support Safari / iOS.
|
|
29
29
|
|
|
30
30
|
---
|
|
31
31
|
|
|
32
|
-
##
|
|
32
|
+
## Setup by Environment
|
|
33
33
|
|
|
34
|
-
### 1.
|
|
34
|
+
### 1. Vite (`vite.config.ts`)
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
Configure both the local dev server (`vite`) and production preview server (`vite preview`):
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { defineConfig } from "vite";
|
|
40
|
+
|
|
41
|
+
export default defineConfig({
|
|
42
|
+
server: {
|
|
43
|
+
headers: {
|
|
44
|
+
"Cross-Origin-Opener-Policy": "same-origin",
|
|
45
|
+
"Cross-Origin-Embedder-Policy": "require-corp",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
preview: {
|
|
49
|
+
headers: {
|
|
50
|
+
"Cross-Origin-Opener-Policy": "same-origin",
|
|
51
|
+
"Cross-Origin-Embedder-Policy": "require-corp",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
41
54
|
});
|
|
42
55
|
```
|
|
43
56
|
|
|
44
|
-
### 2.
|
|
57
|
+
### 2. Cloudflare Pages / Netlify (`public/_headers`)
|
|
58
|
+
|
|
59
|
+
Place a `_headers` file in `public/` so it is copied to the root of your build output:
|
|
60
|
+
|
|
61
|
+
```text
|
|
62
|
+
/*
|
|
63
|
+
Cross-Origin-Opener-Policy: same-origin
|
|
64
|
+
Cross-Origin-Embedder-Policy: require-corp
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 3. Next.js (`next.config.js`)
|
|
45
68
|
|
|
46
69
|
```javascript
|
|
47
70
|
/** @type {import('next').NextConfig} */
|
|
@@ -62,26 +85,31 @@ const nextConfig = {
|
|
|
62
85
|
module.exports = nextConfig;
|
|
63
86
|
```
|
|
64
87
|
|
|
65
|
-
###
|
|
88
|
+
### 4. Custom Node.js / Express Server
|
|
89
|
+
|
|
90
|
+
```javascript
|
|
91
|
+
app.use((req, res, next) => {
|
|
92
|
+
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
|
|
93
|
+
res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
|
|
94
|
+
next();
|
|
95
|
+
});
|
|
96
|
+
```
|
|
66
97
|
|
|
67
|
-
|
|
98
|
+
### 5. Static Hosting without Header Control (e.g., GitHub Pages)
|
|
68
99
|
|
|
69
|
-
|
|
100
|
+
Platforms like GitHub Pages do not allow customizing HTTP response headers directly. You can use **`coi-serviceworker`** to inject the isolation headers via a client-side Service Worker:
|
|
70
101
|
|
|
71
|
-
1. Place `coi-serviceworker.js`
|
|
72
|
-
2.
|
|
102
|
+
1. Place `coi-serviceworker.js` at your site root.
|
|
103
|
+
2. Load it in your HTML `<head>`:
|
|
73
104
|
|
|
74
105
|
```html
|
|
75
106
|
<script src="/coi-serviceworker.js"></script>
|
|
76
107
|
```
|
|
77
108
|
|
|
78
|
-
|
|
109
|
+
On the first visit, the Service Worker registers and reloads the page once to establish `window.crossOriginIsolated === true`.
|
|
79
110
|
|
|
80
111
|
---
|
|
81
112
|
|
|
82
|
-
##
|
|
83
|
-
|
|
84
|
-
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.
|
|
113
|
+
## Runtime Check in WaSHI
|
|
85
114
|
|
|
86
|
-
|
|
87
|
-
> The default **HiGHS** solver engine does not require cross-origin isolation or `SharedArrayBuffer`. It works out of the box in standard browser contexts.
|
|
115
|
+
WaSHI's Z3 adapter checks `crossOriginIsolated` before initializing Z3 in the browser. If Cross-Origin Isolation is missing, `solve()` / `check()` immediately rejects with an actionable error explaining that COOP/COEP headers or a Service Worker are required, rather than hanging silently.
|
|
@@ -1,19 +1,35 @@
|
|
|
1
1
|
# Introduction & Installation
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**WaSHI** (WebAssembly-backed, Solver-independent, High-level modeling Interface) brings the algebraic modeling style of Python's **PuLP** to the TypeScript and JavaScript ecosystem. Write expressions naturally with tagged templates (`expr`) and solve mathematical optimization problems directly in **Node.js and the browser** using WebAssembly solvers (**HiGHS**, **Z3**, and **GLPK**).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
```typescript
|
|
6
|
+
import { Model, expr, maximize } from "@ohtaman/washi";
|
|
7
|
+
|
|
8
|
+
const m = new Model("production");
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
// 1. Define decision variables with realistic finite bounds (lb / ub)
|
|
11
|
+
const desks = m.integer("desks", { lb: 0, ub: 100 });
|
|
12
|
+
const chairs = m.integer("chairs", { lb: 0, ub: 100 });
|
|
8
13
|
|
|
9
|
-
|
|
10
|
-
|
|
14
|
+
// 2. Set the objective function and constraints
|
|
15
|
+
m.objective(maximize(expr`30 * ${desks} + 50 * ${chairs}`));
|
|
16
|
+
m.constraint(expr`${desks} + 2 * ${chairs} <= 40`, "wood_capacity");
|
|
17
|
+
m.constraint(expr`2 * ${desks} + ${chairs} <= 50`, "labor_capacity");
|
|
18
|
+
|
|
19
|
+
// 3. Solve (built-in HiGHS WebAssembly) and validate the solution
|
|
20
|
+
const status = await m.solve();
|
|
21
|
+
if (m.hasPrimalSolution) {
|
|
22
|
+
const report = m.validateSolution();
|
|
23
|
+
console.log(status, report.valid); // "Optimal", true
|
|
24
|
+
console.log(`Profit: ${m.objectiveValue} (desks=${desks.value}, chairs=${chairs.value})`); // 1100 (desks=20, chairs=10)
|
|
25
|
+
}
|
|
26
|
+
```
|
|
11
27
|
|
|
12
28
|
---
|
|
13
29
|
|
|
14
30
|
## 📦 Installation
|
|
15
31
|
|
|
16
|
-
|
|
32
|
+
The default **HiGHS** solver (LP / MIP / Convex QP) is bundled and works out of the box:
|
|
17
33
|
|
|
18
34
|
::: code-group
|
|
19
35
|
|
|
@@ -35,82 +51,37 @@ bun add @ohtaman/washi
|
|
|
35
51
|
|
|
36
52
|
:::
|
|
37
53
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
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:
|
|
54
|
+
Install optional peer dependencies only if you need **Z3** (SMT logic & Unsat Core diagnosis) or **GLPK**:
|
|
41
55
|
|
|
42
56
|
```bash
|
|
43
|
-
# For SMT logic and Unsat Core conflict diagnosis
|
|
44
|
-
npm install
|
|
45
|
-
|
|
46
|
-
# For GLPK solver (GPL-3.0)
|
|
47
|
-
npm install glpk.js
|
|
57
|
+
npm install z3-solver # For SMT logic and Unsat Core conflict diagnosis
|
|
58
|
+
npm install glpk.js # For GLPK solver (GPL-3.0)
|
|
48
59
|
```
|
|
49
60
|
|
|
50
61
|
---
|
|
51
62
|
|
|
52
|
-
##
|
|
53
|
-
|
|
54
|
-
Here is a step-by-step example of solving a production allocation problem.
|
|
55
|
-
|
|
56
|
-
### 1. Import the Core Classes
|
|
57
|
-
|
|
58
|
-
```typescript
|
|
59
|
-
import { Model, expr, maximize } from "@ohtaman/washi";
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
### 2. Instantiate a Model
|
|
63
|
-
|
|
64
|
-
```typescript
|
|
65
|
-
const model = new Model("WoodworkingProfit");
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
### 3. Add Decision Variables
|
|
63
|
+
## 💡 Why TypeScript & WebAssembly?
|
|
69
64
|
|
|
70
|
-
|
|
65
|
+
- **Zero native setup across Node.js and the browser**: No platform-specific C/C++ builds or separate Python runtimes to manage. A single `npm install` runs the same modeling code across **Node.js APIs, CLIs, and scripts** as well as in the **browser**.
|
|
66
|
+
- **Lightweight embedding with standard Web APIs & UIs**: Go beyond local analysis scripts by shipping **interactive Web tools, mini-games, or Chrome extensions** that anyone can open via a URL with zero backend hosting cost. You can wire mathematical optimization directly into drag-and-drop UIs or **standard browser capabilities**—such as screen/camera capture and speech recognition (Web Speech API)—with no network round-trips or external data transfer.
|
|
67
|
+
- **End-to-end type safety from app data to model**: Prevent common indexing and dictionary-key mistakes with TypeScript's type system. Typed variable dictionaries (`Record<K, Var>`) and variable-reference interpolation (`` expr`30 * ${qty.desk} <= 40` ``) connect directly to your application schemas, preserving compile-time variable/key checks, IDE autocompletion, and symbol renaming.
|
|
68
|
+
- **Strong synergy with AI development & LLM workflows**: Keeping your application and optimization model in a **single TypeScript codebase** lets you validate everything in one test runner—without crossing a Python service boundary—and deploy easily as static assets or a single service. That tight test-and-deploy loop fits AI-assisted coding naturally, while at runtime you can pair flexible LLM extraction with exact solver feasibility and conflict diagnosis (`unsatCore`) in the same runtime.
|
|
71
69
|
|
|
72
|
-
|
|
73
|
-
const x = model.integer("x", { lb: 0 });
|
|
74
|
-
const y = model.integer("y", { lb: 0 });
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
### 4. Set the Objective Function
|
|
78
|
-
|
|
79
|
-
Use the `expr` tagged template literal along with `maximize(...)` or `minimize(...)` to define the goal:
|
|
80
|
-
|
|
81
|
-
```typescript
|
|
82
|
-
// Maximize 30*x + 50*y
|
|
83
|
-
model.objective(maximize(expr`30 * ${x} + 50 * ${y}`));
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
### 5. Add Constraints
|
|
87
|
-
|
|
88
|
-
Use `model.constraint()` with the `expr` tagged template to formulate equality and inequality constraints:
|
|
89
|
-
|
|
90
|
-
```typescript
|
|
91
|
-
model.constraint(expr`${x} + 2 * ${y} <= 40`, "wood_capacity");
|
|
92
|
-
model.constraint(expr`2 * ${x} + ${y} <= 50`, "labor_capacity");
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
### 6. Solve and Extract Results
|
|
70
|
+
---
|
|
96
71
|
|
|
97
|
-
|
|
72
|
+
## 🔍 Core Workflow Essentials
|
|
98
73
|
|
|
99
|
-
|
|
100
|
-
const status = await model.solve();
|
|
74
|
+
Three practical habits cover the essentials shown in the snippet above:
|
|
101
75
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
```
|
|
76
|
+
1. **Set realistic finite bounds (`lb`, `ub`)**: When defining variables with `m.continuous`, `m.integer`, or `m.binary`, finite bounds strengthen MIP presolve and enable automatic tight Big-M derivation for logical constraints.
|
|
77
|
+
2. **Formulate objectives and constraints with `expr`**: Use `` m.objective(maximize(expr`...`)) `` and `` m.constraint(expr`... <= ...`, "name") ``. Naming constraints lets you pinpoint conflicting rules immediately if a model becomes infeasible.
|
|
78
|
+
3. **Check `hasPrimalSolution` and call `validateSolution()`**: After `await m.solve()`, check `m.hasPrimalSolution` before reading `.value` on variables, and call `m.validateSolution()` to independently verify that bounds, integrality, and constraints hold within tolerance.
|
|
106
79
|
|
|
107
80
|
---
|
|
108
81
|
|
|
109
82
|
## 🧭 Next Steps
|
|
110
83
|
|
|
111
|
-
-
|
|
112
|
-
-
|
|
113
|
-
-
|
|
114
|
-
-
|
|
115
|
-
- Browse 9 runnable optimization and SMT recipes in [Examples & API Finder](../examples/).
|
|
116
|
-
|
|
84
|
+
- **[Modeling Guide](../guide/variables.md)**: Array/dictionary variables, aggregations (`sum`, `dot`), logical constraints, and Unsat Core diagnosis
|
|
85
|
+
- **[Examples & API Finder](../examples/)**: 11 runnable optimization and SMT recipes—from shift scheduling and portfolio QP to vehicle routing
|
|
86
|
+
- **[Solvers Overview](../solvers/)**: Compare built-in HiGHS with optional Z3 and GLPK adapters
|
|
87
|
+
- **[Runtime Environments](./runtimes.md)**: Setup details for Node.js, Vite / Next.js bundlers, CDNs, and static hosting
|