@ohtaman/washi 0.4.0 → 0.6.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 +149 -121
- package/README.md +149 -121
- package/README.zh-CN.md +150 -122
- package/THIRD_PARTY_LICENSES.md +3 -4
- package/dist/browser/_washi-node-only-BvRk9kiK.js +0 -0
- package/dist/browser/browser-runtime-TLCY0hkc.js +1793 -0
- package/dist/browser/highs-worker.js +21 -3
- package/dist/browser/highs.wasm +0 -0
- package/dist/browser/highs.wasm.LICENSE +1 -2
- package/dist/browser/loader-DewlSho8.js +643 -0
- package/dist/browser/solvers/glpk.js +433 -286
- package/dist/browser/solvers/glpk.min.js +1 -1
- package/dist/browser/solvers/z3.js +1008 -981
- package/dist/browser/solvers/z3.min.js +2 -2
- package/dist/browser/washi.js +2778 -673
- package/dist/browser/washi.min.js +3 -4
- package/dist/lib/_washi-node-only-BvRk9kiK.js +0 -0
- package/dist/lib/highs-browser-runtime.js +2120 -4124
- package/dist/lib/highs-node-runtime.js +24 -1
- package/dist/lib/highs-runtime.js +2 -0
- package/dist/lib/highs-worker.js +21 -3
- package/dist/lib/highs.wasm +0 -0
- package/dist/lib/highs.wasm.LICENSE +1 -2
- package/dist/lib/loader-BZUQNvh4.js +649 -0
- package/dist/lib/registry-C20sxTwm.js +964 -0
- package/dist/lib/solvers/glpk.js +201 -90
- package/dist/lib/solvers/z3.js +399 -259
- package/dist/lib/washi.js +2363 -1056
- package/dist/types/decomposition/benders.d.ts +32 -0
- package/dist/types/decomposition/benders.d.ts.map +1 -0
- package/dist/types/decomposition/column-generation.d.ts +34 -0
- package/dist/types/decomposition/column-generation.d.ts.map +1 -0
- package/dist/types/decomposition/index.d.ts +4 -0
- package/dist/types/decomposition/index.d.ts.map +1 -0
- package/dist/types/decomposition/types.d.ts +230 -0
- package/dist/types/decomposition/types.d.ts.map +1 -0
- package/dist/types/index.d.ts +5 -4
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/modeler/big-m.d.ts +75 -0
- package/dist/types/modeler/big-m.d.ts.map +1 -0
- package/dist/types/modeler/boolean-expr.d.ts +103 -14
- package/dist/types/modeler/boolean-expr.d.ts.map +1 -1
- package/dist/types/modeler/constraint.d.ts +47 -9
- package/dist/types/modeler/constraint.d.ts.map +1 -1
- package/dist/types/modeler/expression.d.ts +123 -28
- package/dist/types/modeler/expression.d.ts.map +1 -1
- package/dist/types/modeler/index.d.ts +11 -6
- package/dist/types/modeler/index.d.ts.map +1 -1
- package/dist/types/modeler/linearizer.d.ts +15 -0
- package/dist/types/modeler/linearizer.d.ts.map +1 -0
- package/dist/types/modeler/lp-writer.d.ts +30 -3
- package/dist/types/modeler/lp-writer.d.ts.map +1 -1
- package/dist/types/modeler/model.d.ts +623 -131
- package/dist/types/modeler/model.d.ts.map +1 -1
- package/dist/types/modeler/objective.d.ts +59 -0
- package/dist/types/modeler/objective.d.ts.map +1 -0
- package/dist/types/modeler/parser.d.ts +34 -10
- package/dist/types/modeler/parser.d.ts.map +1 -1
- package/dist/types/modeler/quad-expr.d.ts +26 -7
- package/dist/types/modeler/quad-expr.d.ts.map +1 -1
- package/dist/types/modeler/solution-validator.d.ts +93 -0
- package/dist/types/modeler/solution-validator.d.ts.map +1 -0
- package/dist/types/modeler/solvers/glpk/solver.d.ts +26 -2
- package/dist/types/modeler/solvers/glpk/solver.d.ts.map +1 -1
- package/dist/types/modeler/solvers/highs/browser-runtime.d.ts +2 -1
- package/dist/types/modeler/solvers/highs/browser-runtime.d.ts.map +1 -1
- package/dist/types/modeler/solvers/highs/driver.d.ts +232 -0
- package/dist/types/modeler/solvers/highs/driver.d.ts.map +1 -0
- package/dist/types/modeler/solvers/highs/index.d.ts +1 -0
- package/dist/types/modeler/solvers/highs/index.d.ts.map +1 -1
- package/dist/types/modeler/solvers/highs/loader.d.ts +38 -13
- package/dist/types/modeler/solvers/highs/loader.d.ts.map +1 -1
- package/dist/types/modeler/solvers/highs/node-runtime.d.ts +2 -1
- package/dist/types/modeler/solvers/highs/node-runtime.d.ts.map +1 -1
- package/dist/types/modeler/solvers/highs/solver.d.ts +12 -5
- 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/mock.d.ts +14 -5
- package/dist/types/modeler/solvers/mock.d.ts.map +1 -1
- package/dist/types/modeler/solvers/types.d.ts +73 -11
- package/dist/types/modeler/solvers/types.d.ts.map +1 -1
- package/dist/types/modeler/solvers/z3/loader.d.ts +2 -3
- package/dist/types/modeler/solvers/z3/loader.d.ts.map +1 -1
- package/dist/types/modeler/solvers/z3/solver.d.ts +15 -5
- package/dist/types/modeler/solvers/z3/solver.d.ts.map +1 -1
- package/dist/types/modeler/types.d.ts +86 -1
- package/dist/types/modeler/types.d.ts.map +1 -1
- package/dist/types/modeler/validation.d.ts +56 -0
- package/dist/types/modeler/validation.d.ts.map +1 -0
- package/dist/types/modeler/variable.d.ts +48 -14
- package/dist/types/modeler/variable.d.ts.map +1 -1
- package/docs/examples/diet-sensitivity.md +90 -0
- package/docs/examples/ecommerce-coupons.md +106 -0
- package/docs/examples/index.md +57 -0
- package/docs/examples/knapsack.md +60 -0
- package/docs/examples/portfolio-qp.md +83 -0
- package/docs/examples/production.md +51 -0
- package/docs/examples/shift-scheduling.md +175 -0
- package/docs/examples/smt-security.md +74 -0
- package/docs/examples/transportation.md +90 -0
- package/docs/examples/traveling-salesperson.md +173 -0
- package/docs/examples/unsat-core.md +64 -0
- package/docs/examples/vehicle-routing.md +206 -0
- package/docs/getting-started/ai-agents.md +271 -0
- package/docs/getting-started/cross-origin.md +87 -0
- package/docs/getting-started/index.md +116 -0
- package/docs/getting-started/runtimes.md +227 -0
- package/docs/guide/constraints.md +177 -0
- package/docs/guide/decomposition.md +399 -0
- package/docs/guide/expressions.md +204 -0
- package/docs/guide/objectives.md +170 -0
- package/docs/guide/unsat-core.md +154 -0
- package/docs/guide/variables.md +188 -0
- package/docs/ja/getting-started/ai-agents.md +270 -0
- package/docs/solvers/custom-solver.md +525 -0
- package/docs/solvers/glpk.md +66 -0
- package/docs/solvers/highs.md +115 -0
- package/docs/solvers/index.md +42 -0
- package/docs/solvers/z3.md +78 -0
- package/docs/zh/getting-started/ai-agents.md +270 -0
- package/llms-full.txt +4065 -0
- package/llms.txt +201 -0
- package/package.json +32 -16
- package/skills/washi/SKILL.md +197 -0
- package/dist/browser/browser-runtime-D13gHOT5.js +0 -4189
- package/dist/browser/loader-CVt4CloI.js +0 -58
- package/dist/lib/async-mutex-Duo4jwzn.js +0 -195
- package/dist/lib/constraint-BRuB3ooC.js +0 -90
- package/dist/lib/loader-Dti0v9yC.js +0 -50
- package/dist/lib/registry-DksqCSjI.js +0 -32
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# AI Coding Agent Quick-Reference Guide
|
|
2
|
+
|
|
3
|
+
> **Scope Note**: This document is the **consumer modeling quick-reference** for AI coding agents and developers building optimization or satisfiability models with `@ohtaman/washi`.
|
|
4
|
+
> - **Portable Agent Skill (`SKILL.md`)**: For an actionable end-to-end modeling, `toLpString()` inspection, `validateSolution()` verification, and Z3 `unsatCore` conflict-diagnosis workflow skill (compatible with Claude Code, Antigravity, Cursor, Codex, and other `SKILL.md`-aware agents), use [`skills/washi/SKILL.md`](https://github.com/ohtaman/washi/blob/main/skills/washi/SKILL.md) (also shipped in the npm package at `node_modules/@ohtaman/washi/skills/washi/SKILL.md`):
|
|
5
|
+
> ```bash
|
|
6
|
+
> # Install directly via the skills CLI (skills.sh / vercel-labs/skills)
|
|
7
|
+
> npx skills add ohtaman/washi
|
|
8
|
+
>
|
|
9
|
+
> # Or link/copy the packaged skill into your project's agent skills directory
|
|
10
|
+
> mkdir -p .agents/skills/washi
|
|
11
|
+
> cp node_modules/@ohtaman/washi/skills/washi/SKILL.md .agents/skills/washi/SKILL.md
|
|
12
|
+
> ```
|
|
13
|
+
> - **Repository Review Guidelines**: For repository contribution and pull-request review rules, see [`AGENTS.md`](https://github.com/ohtaman/washi/blob/main/AGENTS.md) in the repository root.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 1. Package Imports & Deployment
|
|
18
|
+
|
|
19
|
+
### Node.js / Bundlers (Vite, Webpack, Next.js, Bun)
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import {
|
|
23
|
+
Model,
|
|
24
|
+
expr,
|
|
25
|
+
sum,
|
|
26
|
+
dot,
|
|
27
|
+
maximize,
|
|
28
|
+
minimize,
|
|
29
|
+
and,
|
|
30
|
+
or,
|
|
31
|
+
not,
|
|
32
|
+
implies,
|
|
33
|
+
xor,
|
|
34
|
+
setHighsWasmUrl,
|
|
35
|
+
} from "@ohtaman/washi";
|
|
36
|
+
|
|
37
|
+
// Optional solver plugins (require installing peer dependencies first):
|
|
38
|
+
// npm install z3-solver
|
|
39
|
+
import "@ohtaman/washi/solvers/z3";
|
|
40
|
+
// npm install glpk.js
|
|
41
|
+
import "@ohtaman/washi/solvers/glpk";
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Browser ESM & Classic Script Tags
|
|
45
|
+
|
|
46
|
+
- **Browser ESM**:
|
|
47
|
+
- Core (HiGHS built-in): `import { Model, expr, maximize } from "./washi/washi.js";`
|
|
48
|
+
- GLPK plugin: `import "./washi/solvers/glpk.js";`
|
|
49
|
+
- Z3 plugin: `import "./washi/solvers/z3.js";`
|
|
50
|
+
- **Classic `<script>` / CodePen (IIFE)**:
|
|
51
|
+
- Core: `<script src="./washi/washi.min.js"></script>` (exposes global `Washi`)
|
|
52
|
+
- GLPK plugin: `<script src="./washi/solvers/glpk.min.js"></script>` (registers `"glpk"`, exposes `WashiGLPK`)
|
|
53
|
+
- Z3 plugin: `<script src="./washi/solvers/z3.min.js"></script>` (registers `"z3"`, exposes `WashiZ3`)
|
|
54
|
+
|
|
55
|
+
### WebAssembly Asset & Runtime Rules
|
|
56
|
+
|
|
57
|
+
1. **Bundled WASM by default (no implicit CDN fallback)**:
|
|
58
|
+
- In Node.js and modern bundlers, WebAssembly assets resolve relative to the module automatically.
|
|
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 the local Node `highs` package runtime).
|
|
61
|
+
2. **Z3 requires a Secure Context, Cross-Origin Isolation, and CORS in browsers**:
|
|
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
|
+
- `Cross-Origin-Opener-Policy: same-origin` (on the document)
|
|
64
|
+
- `Cross-Origin-Embedder-Policy: require-corp` (on the document)
|
|
65
|
+
- `Cross-Origin-Resource-Policy: cross-origin` and `Access-Control-Allow-Origin` (on cross-origin script/WASM asset responses)
|
|
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
|
+
3. **Web Workers & Cancellation (`AbortSignal`)**:
|
|
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).
|
|
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
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## 2. Canonical Modeling Workflow
|
|
75
|
+
|
|
76
|
+
Formulating and solving a model consists of four distinct steps:
|
|
77
|
+
|
|
78
|
+
### Step 1: Create Model & Decision Variables
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
const m = new Model("ExampleModel");
|
|
82
|
+
|
|
83
|
+
// Scalar variables
|
|
84
|
+
const x = m.continuous("x", { lb: 0, ub: 100 });
|
|
85
|
+
const n = m.integer("n", { lb: 0, ub: 10 });
|
|
86
|
+
const b = m.binary("b");
|
|
87
|
+
const flag = m.bool("flag"); // Propositional boolean (for Z3 / logical constraints)
|
|
88
|
+
|
|
89
|
+
// Indexed variable containers:
|
|
90
|
+
// - Numeric dimension -> Array
|
|
91
|
+
// - Key-array dimension -> Record
|
|
92
|
+
const vec = m.continuous(4, { lb: 0, prefix: "v" }); // Var[] (length 4)
|
|
93
|
+
const grid = m.binary(2, 3, { prefix: "g" }); // Var[][] (2x3)
|
|
94
|
+
const byItem = m.integer(["desk", "chair"] as const, { lb: 0 }); // Record<"desk" | "chair", Var>
|
|
95
|
+
const mixed = m.binary(["A", "B"] as const, 3); // Record<"A" | "B", Var[]>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
- **Optional Primal Initial Values (`initialValue?: number`)**:
|
|
99
|
+
- Numeric variable constructors (`m.continuous`, `m.integer`, `m.binary`, and `new Var`) and the mutable `v.initialValue` property accept an optional finite primal hint (validated with tolerance `1e-6` against bounds `[lb, ub]` and integer/binary domains), kept distinct from the solved `v.value`.
|
|
100
|
+
- A start may be complete or partial (e.g., transferred by stable variable names when rebuilding a MIP model). Supplying `initialValue` is a hint only—it does **not** fix the variable, add a hard constraint, or guarantee solver acceptance or a runtime speedup.
|
|
101
|
+
- Supported by **HiGHS** (`supportsWarmStart: true`); solvers without warm-start support (**GLPK**, **Z3**) throw an error if any variable has `initialValue !== undefined`.
|
|
102
|
+
|
|
103
|
+
### Step 2: Build Expressions (`expr`, `sum`, `dot`)
|
|
104
|
+
|
|
105
|
+
- **Unified `expr` template tag**:
|
|
106
|
+
- Linear expression (`LinExpr`): `` expr`30 * ${x} + 50 * ${n} - 10` ``
|
|
107
|
+
- Quadratic expression (`QuadExpr`): `` expr`${x}^2 + 2 * ${x} * ${n} + ${n}^2` `` (variable-by-variable products and squares are supported for convex QP objectives).
|
|
108
|
+
- Linear constraint (`Constraint`): `` expr`${x} + 2 * ${n} <= 40` `` (supports `<=`, `>=`, `==`, and `=`).
|
|
109
|
+
- **Aggregations**:
|
|
110
|
+
- `sum(iterable, mapper?)` sums an `Iterable` of `Var` / `LinExpr` / `number` (e.g., `Var[]`, `grid.flat()`, or `Object.values(record)`).
|
|
111
|
+
- `dot(a, b)` computes the inner product of matching 1D arrays, 2D arrays (`number[][]` / `Var[][]`), plain records (`Record<string, ...>`), or mixed Array/Record containers in either operand order.
|
|
112
|
+
- **Fluent methods**:
|
|
113
|
+
- Arithmetic (on `LinExpr` and `QuadExpr`; `Var` supports `.add()`, `.sub()`, `.mul()`, `.neg()`): `.add(other)`, `.sub(other)`, `.mul(other)`, `.div(scalar)`, `.neg()`
|
|
114
|
+
- Comparisons (on `Var` and `LinExpr`): `.le(rhs, name?)`, `.ge(rhs, name?)`, `.eq(rhs, name?)`
|
|
115
|
+
- Boolean logic (on `BoolVar`, `BoolExpr`, and `Constraint` for Z3): standalone `and(...)`, `or(...)`, `not(...)`, `implies(a, b)`, `xor(a, b)` and fluent `.and(...)`, `.or(...)`, `.not()`, `.implies(...)`, `.xor(...)`
|
|
116
|
+
|
|
117
|
+
### Step 3: Register Objective & Constraints on the Model
|
|
118
|
+
|
|
119
|
+
Constructing an `expr` or `.le()` object does **not** attach it to the model until you pass it to `m.constraint(...)` or `m.objective(...)`:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
// Set objective using maximize() or minimize()
|
|
123
|
+
m.objective(maximize(expr`30 * ${x} + 50 * ${n}`));
|
|
124
|
+
|
|
125
|
+
// Register constraints on the model
|
|
126
|
+
m.constraint(expr`${x} + 2 * ${n} <= 40`, "resource_limit");
|
|
127
|
+
|
|
128
|
+
// Indicator constraint (MIP Big-M reformulation: b === 1 => x <= 50)
|
|
129
|
+
m.indicator(b, expr`${x} <= 50`, { activeValue: 1, name: "cap_when_b" });
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Step 4: Execute (`solve` or `check`) & Verify (`validateSolution`)
|
|
133
|
+
|
|
134
|
+
- **Optimization (`await m.solve(options?)`)**:
|
|
135
|
+
- Returns `SolverStatus` (`"Optimal"`, `"Feasible"`, `"Infeasible"`, `"Unbounded"`, `"TimeLimit"`, `"IterationLimit"`, `"Error"`, `"Unknown"`, `"NotSolved"`, `"Sat"`, or `"Unsat"`). Built-in optimization solvers return the optimization statuses (`"Optimal"`, `"Feasible"`, `"Infeasible"`, `"Unbounded"`, `"TimeLimit"`, `"IterationLimit"`, `"Error"`, `"Unknown"`, `"NotSolved"`), where a time-, iteration-, or solution-limited run (or an interrupted solve) that found a valid primal incumbent returns `"Feasible"`; `"Sat"` and `"Unsat"` are included in `SolverStatus` for satisfiability/custom solver adapters (such as `MockSolver`).
|
|
136
|
+
- Read values via `x.value`, `expr`...`.value`, `m.getValues(x)`, `m.getValues(container)`, `m.objectiveValue`, `constr.dual`, `constr.slack`, and `x.reducedCost`.
|
|
137
|
+
- Call `m.validateSolution()` after solving to independently verify variable finiteness, variable bounds (`lb`/`ub`), integer/binary/boolean domains, linear constraint residuals, and logical constraints (`SolutionValidationReport`: `{ valid, complete, maxViolation, violations, ... }`). Note that `validateSolution()` checks primal feasibility of variables and constraints; it does not recompute or compare `m.objectiveValue`.
|
|
138
|
+
- **Satisfiability & Conflict Diagnosis (`await m.check(options?)`)**:
|
|
139
|
+
- Returns `CheckResult` (`{ status: "Sat" | "Unsat" | "Unknown", values: Record<string, number | boolean>, conflicts?: readonly string[], unsatCore?: readonly string[], runTime?: number }`), where `conflicts` is populated when the model is proven `"Unsat"`.
|
|
140
|
+
- For models containing linear arithmetic constraints or logical constraints (`BoolVar`, `and`, `or`, `implies`, `xor`), `m.check()` works with any solver (including the default HiGHS and GLPK via single-solve elastic slack relaxation, automatically linearizing logical constraints into 0-1 MIP formulation) and diagnoses conflicting constraints as `conflicts` (leaving `unsatCore: undefined` for semantic rigor).
|
|
141
|
+
- Exact SMT unsatisfiable-core extraction (`result.unsatCore` and `result.conflicts`) is provided when using an SMT-capable solver (`{ solver: "z3" }`). Because Z3 asserts variable bounds (`lb`/`ub`) and binary `{0, 1}` domains directly while tracking registered constraints, `unsatCore` reports a subset of registered constraint names that conflicts *together with the model's fixed variable bounds/domains* (and is not guaranteed minimal).
|
|
142
|
+
- **No concurrent mutation during async solve/check**:
|
|
143
|
+
- Mutating variables, constraints, or the objective while `await m.solve()` or `await m.check()` is pending invalidates the in-flight execution and rejects the stale promise.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## 3. Executable Examples
|
|
148
|
+
|
|
149
|
+
### Example A: Mixed-Integer Linear Programming (MIP) with Indexed Containers & Solution Validation
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
import { Model, expr, dot, sum, maximize } from "@ohtaman/washi";
|
|
153
|
+
|
|
154
|
+
export async function runMipExample() {
|
|
155
|
+
const m = new Model("ProductionMIP");
|
|
156
|
+
const products = ["desk", "chair"] as const;
|
|
157
|
+
const profit = { desk: 30, chair: 50 };
|
|
158
|
+
const woodPerUnit = { desk: 1, chair: 2 };
|
|
159
|
+
const laborPerUnit = { desk: 2, chair: 1 };
|
|
160
|
+
|
|
161
|
+
const qty = m.integer(products, { lb: 0, ub: 100 });
|
|
162
|
+
const setup = m.binary("setup");
|
|
163
|
+
|
|
164
|
+
m.objective(maximize(expr`${dot(profit, qty)} - 20 * ${setup}`));
|
|
165
|
+
m.constraint(expr`${dot(woodPerUnit, qty)} <= 40`, "wood_cap");
|
|
166
|
+
m.constraint(expr`${dot(laborPerUnit, qty)} <= 50`, "labor_cap");
|
|
167
|
+
m.constraint(expr`${sum(Object.values(qty))} <= 30 * ${setup}`, "require_setup");
|
|
168
|
+
|
|
169
|
+
const status = await m.solve();
|
|
170
|
+
const report = m.validateSolution();
|
|
171
|
+
const values = m.getValues(qty);
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
status,
|
|
175
|
+
objectiveValue: m.objectiveValue,
|
|
176
|
+
values,
|
|
177
|
+
setup: setup.value,
|
|
178
|
+
valid: report.valid,
|
|
179
|
+
maxViolation: report.maxViolation,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Example B: Convex Quadratic Programming (QP)
|
|
185
|
+
|
|
186
|
+
Variable-by-variable products (`${x} * ${y}`) and squares (`${x}^2`) are supported in `expr` for quadratic objectives:
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
import { Model, expr, minimize } from "@ohtaman/washi";
|
|
190
|
+
|
|
191
|
+
export async function runQpExample() {
|
|
192
|
+
const m = new Model("PortfolioQP");
|
|
193
|
+
const x = m.continuous("x", { lb: 0 });
|
|
194
|
+
const y = m.continuous("y", { lb: 0 });
|
|
195
|
+
|
|
196
|
+
// Minimize x^2 + y^2 subject to x + y >= 4
|
|
197
|
+
m.objective(minimize(expr`${x}^2 + ${y}^2`));
|
|
198
|
+
m.constraint(expr`${x} + ${y} >= 4`, "budget");
|
|
199
|
+
|
|
200
|
+
const status = await m.solve();
|
|
201
|
+
const report = m.validateSolution();
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
status,
|
|
205
|
+
objectiveValue: m.objectiveValue,
|
|
206
|
+
x: x.value,
|
|
207
|
+
y: y.value,
|
|
208
|
+
valid: report.valid,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Example C: Z3 Feasibility Check & Unsat Core Diagnosis
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
import { Model, expr, implies } from "@ohtaman/washi";
|
|
217
|
+
import "@ohtaman/washi/solvers/z3";
|
|
218
|
+
|
|
219
|
+
export async function runZ3Example() {
|
|
220
|
+
const m = new Model("PolicyCheck", { solver: "z3" });
|
|
221
|
+
const vip = m.bool("vip");
|
|
222
|
+
const discount = m.integer("discount", { lb: 0, ub: 50 });
|
|
223
|
+
|
|
224
|
+
m.constraint(vip, "customer_is_vip");
|
|
225
|
+
m.constraint(implies(vip, expr`${discount} >= 20`), "vip_min_discount");
|
|
226
|
+
m.constraint(expr`${discount} <= 10`, "campaign_cap");
|
|
227
|
+
|
|
228
|
+
const unsatResult = await m.check();
|
|
229
|
+
return {
|
|
230
|
+
status: unsatResult.status,
|
|
231
|
+
conflicts: unsatResult.conflicts,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## 4. Negative Examples & Common Pitfalls
|
|
239
|
+
|
|
240
|
+
Avoid these invalid patterns and retired aliases:
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
import { Model, expr, maximize } from "@ohtaman/washi";
|
|
244
|
+
|
|
245
|
+
export function verifyNegativePatterns() {
|
|
246
|
+
const m = new Model("NegativeExamples");
|
|
247
|
+
const x = m.continuous("x", { lb: 0 });
|
|
248
|
+
const y = m.continuous("y", { lb: 0 });
|
|
249
|
+
|
|
250
|
+
// 1. DO NOT pass a Constraint to m.objective() or arithmetic methods:
|
|
251
|
+
// WRONG: m.objective(maximize(expr`${x} <= 10` as any)); -> throws Error
|
|
252
|
+
// WRONG: expr`${x} + 1`.add(expr`${y} <= 5` as any); -> throws Error
|
|
253
|
+
|
|
254
|
+
// 2. DO NOT pass an arithmetic expression without (<=, >=, ==) to m.constraint():
|
|
255
|
+
// WRONG: m.constraint(expr`${x} + ${y}` as any); -> throws Error
|
|
256
|
+
|
|
257
|
+
// 3. DO NOT use quadratic terms inside linear constraints:
|
|
258
|
+
// WRONG: expr`${x}^2 <= 10`; -> throws Error ("Quadratic constraints are not supported")
|
|
259
|
+
|
|
260
|
+
// 4. DO NOT use retired aliases (removed in #59 / #81):
|
|
261
|
+
// - Use `expr` instead of retired `c` template tag
|
|
262
|
+
// - Use `m.constraint(...)` instead of `m.constrain(...)` or `m.add(...)`
|
|
263
|
+
// - Use `m.objective(maximize(...))` / `m.objective(minimize(...))` instead of `m.maximize(...)` / `m.minimize(...)` / `m.setObjective(...)`
|
|
264
|
+
// - Use `await m.solve()` instead of `await m.optimize()`
|
|
265
|
+
// - Use `.le()`, `.ge()`, `.eq()` instead of `.lessThanOrEqual()`, `.greaterThanOrEqual()`, `.equalTo()`
|
|
266
|
+
// - Use `.value` or `m.getValues(...)` instead of `expr.getValue()`
|
|
267
|
+
// - Use `m.variables` / `m.constraints` instead of `m.vars` / `m.constrs`
|
|
268
|
+
return { m, x, y, maximize };
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Cross-Origin Isolation (Z3 & Browsers)
|
|
2
|
+
|
|
3
|
+
When running the **Z3** solver engine inside a web browser, special HTTP headers are required due to browser security models.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Why is Cross-Origin Isolation Required?
|
|
8
|
+
|
|
9
|
+
The Z3 WebAssembly engine utilizes multithreading backed by `SharedArrayBuffer` and Web Workers.
|
|
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
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Required HTTP Response Headers
|
|
16
|
+
|
|
17
|
+
To establish cross-origin isolation, your web server must serve the top-level HTML document with the following HTTP headers:
|
|
18
|
+
|
|
19
|
+
```http
|
|
20
|
+
Cross-Origin-Opener-Policy: same-origin
|
|
21
|
+
Cross-Origin-Embedder-Policy: require-corp
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
In addition, any cross-origin assets (e.g. scripts or WASM files loaded from external domains) must send:
|
|
25
|
+
|
|
26
|
+
```http
|
|
27
|
+
Cross-Origin-Resource-Policy: cross-origin
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Solutions by Hosting Environment
|
|
33
|
+
|
|
34
|
+
### 1. Custom Node.js / Express Server
|
|
35
|
+
|
|
36
|
+
```javascript
|
|
37
|
+
app.use((req, res, next) => {
|
|
38
|
+
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
|
|
39
|
+
res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
|
|
40
|
+
next();
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 2. Next.js (`next.config.js`)
|
|
45
|
+
|
|
46
|
+
```javascript
|
|
47
|
+
/** @type {import('next').NextConfig} */
|
|
48
|
+
const nextConfig = {
|
|
49
|
+
async headers() {
|
|
50
|
+
return [
|
|
51
|
+
{
|
|
52
|
+
source: "/(.*)",
|
|
53
|
+
headers: [
|
|
54
|
+
{ key: "Cross-Origin-Opener-Policy", value: "same-origin" },
|
|
55
|
+
{ key: "Cross-Origin-Embedder-Policy", value: "require-corp" },
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
module.exports = nextConfig;
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### 3. Static Hosting without Header Control (e.g., GitHub Pages)
|
|
66
|
+
|
|
67
|
+
Platforms like GitHub Pages or preview environments do not allow customizing response headers directly.
|
|
68
|
+
|
|
69
|
+
For these environments, you can use **`coi-serviceworker`** (a lightweight Service Worker that injects isolation headers on the client side):
|
|
70
|
+
|
|
71
|
+
1. Place `coi-serviceworker.js` in your website root.
|
|
72
|
+
2. In your HTML `<head>`, include:
|
|
73
|
+
|
|
74
|
+
```html
|
|
75
|
+
<script src="/coi-serviceworker.js"></script>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
When a user visits for the first time, the service worker registers and reloads the page once to establish `window.crossOriginIsolated === true`.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Graceful Degradation in WaSHI
|
|
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.
|
|
85
|
+
|
|
86
|
+
> [!TIP]
|
|
87
|
+
> The default **HiGHS** solver engine does not require cross-origin isolation or `SharedArrayBuffer`. It works out of the box in standard browser contexts.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Introduction & Installation
|
|
2
|
+
|
|
3
|
+
Welcome to **WaSHI** (WebAssembly-backed, Solver-independent, High-level modeling Interface).
|
|
4
|
+
|
|
5
|
+
WaSHI brings the algebraic modeling style of Python's **PuLP** and Julia's **JuMP** to the TypeScript and JavaScript ecosystem. You can formulate mathematical optimization problems using algebraic expressions and solve them locally using WebAssembly solvers (**HiGHS**, **Z3**, and **GLPK**)—without a separate Python service or native C/C++ build steps.
|
|
6
|
+
|
|
7
|
+
### Why TypeScript & WebAssembly?
|
|
8
|
+
|
|
9
|
+
- **Removing the deployment bottleneck**: In traditional optimization workflows (e.g., Python with PuLP or Pyomo), turning a model into an interactive application requires provisioning a backend service and managing native C/C++ solver binaries. As AI-assisted development accelerates UI and application prototyping, backend infrastructure often becomes the primary bottleneck to shipping. Running both the modeler and WebAssembly solvers on standard Web technologies allows an optimization tool to be deployed as static assets (e.g., on GitHub Pages or Cloudflare Pages, serving the generated JS, Worker, and WASM files; with COOP/COEP headers when using Z3)—and makes it straightforward to pair mathematical optimization with in-browser LLMs (e.g., WebGPU / WebLLM) entirely on the client.
|
|
10
|
+
- **Algebraic syntax without operator overloading (`expr`)**: Because JavaScript lacks operator overloading, libraries usually trade off between verbose method chaining (`x.mul(30).add(y.mul(50))`) and untyped strings (`"30 * x + 50 * y"`). WaSHI uses tagged template literals (`` expr`30 * ${x} + 50 * ${y} <= 40` ``) similar to parameterized SQL queries—parsing infix operators while binding `${x}` directly as typed TypeScript object references for static type checking, IDE symbol renaming, and multi-dimensional indexing (`${flow[i][j]}`).
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 📦 Installation
|
|
15
|
+
|
|
16
|
+
Install WaSHI using your preferred package manager:
|
|
17
|
+
|
|
18
|
+
::: code-group
|
|
19
|
+
|
|
20
|
+
```bash [npm]
|
|
21
|
+
npm install @ohtaman/washi
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```bash [pnpm]
|
|
25
|
+
pnpm add @ohtaman/washi
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```bash [yarn]
|
|
29
|
+
yarn add @ohtaman/washi
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```bash [bun]
|
|
33
|
+
bun add @ohtaman/washi
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
:::
|
|
37
|
+
|
|
38
|
+
### Optional Solver Dependencies
|
|
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:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# For SMT logic and Unsat Core conflict diagnosis
|
|
44
|
+
npm install z3-solver
|
|
45
|
+
|
|
46
|
+
# For GLPK solver (GPL-3.0)
|
|
47
|
+
npm install glpk.js
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 🚀 Quickstart Walkthrough
|
|
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
|
|
69
|
+
|
|
70
|
+
Variables represent the unknowns you want to determine. Factory methods (`m.continuous`, `m.integer`, `m.binary`) clearly define their domain:
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
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
|
|
96
|
+
|
|
97
|
+
Solve the model asynchronously via WebAssembly (HiGHS by default):
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
const status = await model.solve();
|
|
101
|
+
|
|
102
|
+
console.log("Status:", status); // "Optimal"
|
|
103
|
+
console.log("Optimal Objective Value:", model.objectiveValue); // 1100
|
|
104
|
+
console.log(`Solution: x = ${x.value}, y = ${y.value}`); // x = 20, y = 10
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## 🧭 Next Steps
|
|
110
|
+
|
|
111
|
+
- Give AI coding agents a concise API & deployment reference with the [AI Coding Agent Quick-Reference Guide](./ai-agents.md).
|
|
112
|
+
- Learn how to run WaSHI across different environments in [Runtime Environments](./runtimes.md).
|
|
113
|
+
- Understand how to formulate complex expressions in the [Modeling Guide](../guide/expressions.md).
|
|
114
|
+
- Explore built-in and pluggable solvers in the [Solvers Matrix](../solvers/).
|
|
115
|
+
- Browse 9 runnable optimization and SMT recipes in [Examples & API Finder](../examples/).
|
|
116
|
+
|