@ohtaman/washi 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +100 -107
- package/README.md +98 -105
- package/README.zh-CN.md +101 -108
- package/dist/browser/highs-worker.js +21 -3
- package/dist/browser/loader-CtaDHef_.js +273 -0
- package/dist/browser/solvers/glpk.js +433 -286
- package/dist/browser/solvers/glpk.min.js +1 -1
- package/dist/browser/solvers/z3.js +1002 -981
- package/dist/browser/solvers/z3.min.js +2 -2
- package/dist/browser/washi.js +2129 -676
- package/dist/browser/washi.min.js +4 -4
- package/dist/lib/highs-worker.js +21 -3
- package/dist/lib/loader-DmNC_UNk.js +279 -0
- package/dist/lib/registry-BEfvpZW3.js +951 -0
- package/dist/lib/solvers/glpk.js +201 -90
- package/dist/lib/solvers/z3.js +399 -259
- package/dist/lib/washi.js +1627 -988
- package/dist/types/index.d.ts +4 -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 +36 -9
- package/dist/types/modeler/constraint.d.ts.map +1 -1
- package/dist/types/modeler/expression.d.ts +113 -28
- package/dist/types/modeler/expression.d.ts.map +1 -1
- package/dist/types/modeler/index.d.ts +10 -6
- package/dist/types/modeler/index.d.ts.map +1 -1
- package/dist/types/modeler/lp-writer.d.ts +27 -2
- package/dist/types/modeler/lp-writer.d.ts.map +1 -1
- package/dist/types/modeler/model.d.ts +478 -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/loader.d.ts +36 -11
- package/dist/types/modeler/solvers/highs/loader.d.ts.map +1 -1
- package/dist/types/modeler/solvers/highs/solver.d.ts +11 -4
- 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 +71 -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 +59 -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 +53 -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/unsat-core.md +64 -0
- package/docs/getting-started/ai-agents.md +267 -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/expressions.md +204 -0
- package/docs/guide/objectives.md +170 -0
- package/docs/guide/unsat-core.md +147 -0
- package/docs/guide/variables.md +188 -0
- package/docs/ja/getting-started/ai-agents.md +266 -0
- package/docs/solvers/custom-solver.md +525 -0
- package/docs/solvers/glpk.md +66 -0
- package/docs/solvers/highs.md +93 -0
- package/docs/solvers/index.md +42 -0
- package/docs/solvers/z3.md +78 -0
- package/docs/zh/getting-started/ai-agents.md +266 -0
- package/llms-full.txt +3249 -0
- package/llms.txt +197 -0
- package/package.json +18 -5
- package/skills/washi/SKILL.md +197 -0
- 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,227 @@
|
|
|
1
|
+
# Runtime Environments & Deployment
|
|
2
|
+
|
|
3
|
+
WaSHI runs across JavaScript and TypeScript runtimes—from Node.js, Bun, and Deno to modern bundlers, vanilla browser `<script>` tags, and air-gapped intranets—without native C/C++ compilation.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🟢 Node.js
|
|
8
|
+
|
|
9
|
+
WaSHI supports Node.js (v18+) out of the box with standard ECMAScript Modules (ESM). The built-in HiGHS WebAssembly binary is loaded directly from the installed package.
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { Model, expr, maximize } from "@ohtaman/washi";
|
|
13
|
+
|
|
14
|
+
const m = new Model("NodeExample");
|
|
15
|
+
const x = m.continuous("x", { lb: 0, ub: 10 });
|
|
16
|
+
const y = m.continuous("y", { lb: 0, ub: 20 });
|
|
17
|
+
|
|
18
|
+
m.objective(maximize(expr`2 * ${x} + 3 * ${y}`));
|
|
19
|
+
m.constraint(expr`${x} + ${y} <= 15`, "capacity");
|
|
20
|
+
|
|
21
|
+
await m.solve();
|
|
22
|
+
console.log(`Status: ${m.status}, Objective: ${m.objectiveValue}`);
|
|
23
|
+
console.log(`x = ${x.value}, y = ${y.value}`);
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Run directly with `tsx` or `node`:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx tsx app.ts
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 🌐 Bundlers (Vite, Webpack, Rollup, Next.js)
|
|
35
|
+
|
|
36
|
+
### Modern Bundlers (Vite / Rollup / Webpack)
|
|
37
|
+
|
|
38
|
+
When importing `@ohtaman/washi` in a browser bundler, the `browser` export condition resolves automatically:
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import { Model, expr, maximize } from "@ohtaman/washi";
|
|
42
|
+
|
|
43
|
+
export async function runOptimization() {
|
|
44
|
+
const m = new Model({ solver: "highs" });
|
|
45
|
+
const x = m.continuous("x", { lb: 0, ub: 100 });
|
|
46
|
+
m.objective(maximize(x));
|
|
47
|
+
await m.solve();
|
|
48
|
+
return x.value;
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
#### Background Web Worker Execution (Default)
|
|
53
|
+
In browsers, HiGHS automatically runs inside a dedicated **Web Worker** so long-running LP/MIP/QP solves never freeze the main UI thread, animations, or user input, while enabling real-time progress events (`onProgress`) and cancellation (`AbortSignal`).
|
|
54
|
+
|
|
55
|
+
If your environment prohibits Web Workers or you explicitly want synchronous main-thread execution, pass `useWorker: false`:
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
await m.solve({ useWorker: false });
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
#### Customizing the HiGHS WASM Asset URL
|
|
62
|
+
By default, WaSHI resolves `highs.wasm` relative to the bundle (`new URL("./highs.wasm", import.meta.url)`). It **never** makes implicit requests to external CDNs (`jsdelivr`, `unpkg`, etc.).
|
|
63
|
+
|
|
64
|
+
If your bundler or static server hosts `.wasm` files under a custom path, configure the URL globally or per solve:
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import { setHighsWasmUrl } from "@ohtaman/washi";
|
|
68
|
+
|
|
69
|
+
// Option A: Configure globally before solving
|
|
70
|
+
setHighsWasmUrl("/assets/wasm/highs.wasm");
|
|
71
|
+
|
|
72
|
+
// Option B: Configure per solve call
|
|
73
|
+
await m.solve({ wasmUrl: "/assets/wasm/highs.wasm" });
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Next.js & Server-Side Rendering (SSR)
|
|
77
|
+
|
|
78
|
+
In Next.js App Router or Pages Router, optimization models can run either in Server Actions / API Routes (Node.js runtime) or on the client inside `"use client"` components:
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
"use client";
|
|
82
|
+
|
|
83
|
+
import { useState } from "react";
|
|
84
|
+
import { Model, maximize } from "@ohtaman/washi";
|
|
85
|
+
|
|
86
|
+
export default function OptimizerComponent() {
|
|
87
|
+
const [val, setVal] = useState<number | null>(null);
|
|
88
|
+
|
|
89
|
+
const handleSolve = async () => {
|
|
90
|
+
const m = new Model();
|
|
91
|
+
const x = m.continuous("x", { lb: 0, ub: 50 });
|
|
92
|
+
m.objective(maximize(x));
|
|
93
|
+
await m.solve();
|
|
94
|
+
setVal(x.value);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<div>
|
|
99
|
+
<button onClick={handleSolve}>Run Optimization</button>
|
|
100
|
+
{val !== null && <p>Result: {val}</p>}
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## 📦 Browser ESM (`<script type="module">`)
|
|
109
|
+
|
|
110
|
+
You can import the pre-bundled browser ESM modules directly from a local static server (or from an npm CDN once a compatible release is published) without a build step:
|
|
111
|
+
|
|
112
|
+
```html
|
|
113
|
+
<script type="module">
|
|
114
|
+
// Serve from your local static server / dist:
|
|
115
|
+
import { Model, expr, maximize } from "./dist/browser/washi.js";
|
|
116
|
+
// Or from an npm CDN once a compatible release is published:
|
|
117
|
+
// import { Model, expr, maximize } from "https://cdn.jsdelivr.net/npm/@ohtaman/washi@<version>/dist/browser/washi.js";
|
|
118
|
+
// Optional solver plugins:
|
|
119
|
+
// import "./dist/browser/solvers/glpk.js";
|
|
120
|
+
// import "./dist/browser/solvers/z3.js";
|
|
121
|
+
|
|
122
|
+
const m = new Model("BrowserESM");
|
|
123
|
+
const x = m.integer("x", { lb: 0 });
|
|
124
|
+
const y = m.integer("y", { lb: 0 });
|
|
125
|
+
|
|
126
|
+
m.objective(maximize(expr`30 * ${x} + 50 * ${y}`));
|
|
127
|
+
m.constraint(expr`${x} + 2 * ${y} <= 40`, "wood");
|
|
128
|
+
m.constraint(expr`2 * ${x} + ${y} <= 50`, "labor");
|
|
129
|
+
|
|
130
|
+
await m.solve();
|
|
131
|
+
console.log("Optimal Objective:", m.objectiveValue);
|
|
132
|
+
</script>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## 🟨 Classic `<script>` Tags (IIFE / Standalone Build / CDN)
|
|
138
|
+
|
|
139
|
+
For static HTML pages, CodePen, or JSFiddle, load `washi.min.js` via a standard `<script>` tag. All exports are attached to the global `window.Washi` object:
|
|
140
|
+
|
|
141
|
+
```html
|
|
142
|
+
<!DOCTYPE html>
|
|
143
|
+
<html>
|
|
144
|
+
<head>
|
|
145
|
+
<meta charset="utf-8" />
|
|
146
|
+
<title>WaSHI Browser Demo</title>
|
|
147
|
+
</head>
|
|
148
|
+
<body>
|
|
149
|
+
<h1>WaSHI Browser Optimization</h1>
|
|
150
|
+
<pre id="output">Solving...</pre>
|
|
151
|
+
|
|
152
|
+
<!-- Core WaSHI + HiGHS (MIT) from local static build / assets -->
|
|
153
|
+
<script src="./dist/browser/washi.min.js"></script>
|
|
154
|
+
<!-- Or via CDN once a compatible release is published: -->
|
|
155
|
+
<!-- <script src="https://cdn.jsdelivr.net/npm/@ohtaman/washi@<version>/dist/browser/washi.min.js"></script> -->
|
|
156
|
+
<!-- Optional plugins (auto-register on window.Washi): -->
|
|
157
|
+
<!-- <script src="./dist/browser/solvers/glpk.min.js"></script> -->
|
|
158
|
+
<!-- <script src="./dist/browser/solvers/z3.min.js"></script> -->
|
|
159
|
+
|
|
160
|
+
<script>
|
|
161
|
+
(async () => {
|
|
162
|
+
const { Model, expr, maximize } = Washi;
|
|
163
|
+
|
|
164
|
+
const m = new Model("BrowserDemo");
|
|
165
|
+
const x = m.continuous("x", { lb: 0, ub: 10 });
|
|
166
|
+
const y = m.continuous("y", { lb: 0, ub: 20 });
|
|
167
|
+
|
|
168
|
+
m.objective(maximize(expr`2 * ${x} + 3 * ${y}`));
|
|
169
|
+
m.constraint(expr`${x} + ${y} <= 15`);
|
|
170
|
+
|
|
171
|
+
await m.solve();
|
|
172
|
+
|
|
173
|
+
document.getElementById("output").textContent =
|
|
174
|
+
`Status: ${m.status}\n` +
|
|
175
|
+
`Objective: ${m.objectiveValue}\n` +
|
|
176
|
+
`x = ${x.value}, y = ${y.value}`;
|
|
177
|
+
})();
|
|
178
|
+
</script>
|
|
179
|
+
</body>
|
|
180
|
+
</html>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
> [!NOTE]
|
|
184
|
+
> When loading `washi.min.js` from a cross-origin CDN (such as jsDelivr or unpkg), browsers block direct `new Worker("https://cdn...")` construction due to the same-origin policy. WaSHI automatically bridges cross-origin workers via a lightweight `Blob` wrapper using `importScripts()`, so Web Worker execution works out of the box from CDNs.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## 🔒 Offline, Intranet & Strict CSP Environments
|
|
189
|
+
|
|
190
|
+
WaSHI never phones home or falls back to public CDNs. To self-host in an air-gapped intranet or under a strict Content Security Policy (CSP):
|
|
191
|
+
|
|
192
|
+
1. **Copy Static Assets**: Serve the files from `node_modules/@ohtaman/washi/dist/browser/` on your own origin:
|
|
193
|
+
- `washi.js` / `washi.min.js`, `highs.wasm`, and `highs-worker.js`
|
|
194
|
+
- Optional GLPK: `solvers/glpk.js` / `solvers/glpk.min.js`
|
|
195
|
+
- Optional Z3: `solvers/z3.js` / `solvers/z3.min.js`, `solvers/z3-built.js`, `solvers/z3-built.wasm`
|
|
196
|
+
2. **Content Security Policy (CSP) Headers**:
|
|
197
|
+
- Allow WebAssembly compilation (`'wasm-unsafe-eval'` in `script-src`).
|
|
198
|
+
- If using the default Web Worker or cross-origin Blob worker fallback, allow `worker-src 'self' blob:;`.
|
|
199
|
+
- If `blob:` workers are prohibited by policy, either serve `highs-worker.js` from `'self'` or run on the main thread with `await m.solve({ useWorker: false })`.
|
|
200
|
+
3. **Cross-Origin Isolation (Z3 Only)**:
|
|
201
|
+
- HiGHS and GLPK run in standard browser contexts without special headers.
|
|
202
|
+
- Z3 WASM uses pthreads + `SharedArrayBuffer` and requires `Cross-Origin-Opener-Policy: same-origin` and `Cross-Origin-Embedder-Policy: require-corp` (or `credentialless`). See the [Cross-Origin Isolation Guide](./cross-origin.md) for setup details.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## ⚡ Bun & Deno
|
|
207
|
+
|
|
208
|
+
### Bun
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
bun add @ohtaman/washi
|
|
212
|
+
bun run app.ts
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Deno
|
|
216
|
+
|
|
217
|
+
In Deno (v1.30+ with `npm:` specifiers):
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
import { Model, maximize } from "npm:@ohtaman/washi";
|
|
221
|
+
|
|
222
|
+
const m = new Model();
|
|
223
|
+
const x = m.continuous("x", { lb: 0, ub: 10 });
|
|
224
|
+
m.objective(maximize(x));
|
|
225
|
+
await m.solve();
|
|
226
|
+
console.log("x =", x.value);
|
|
227
|
+
```
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# Constraints (`m.constraint` & `m.indicator`)
|
|
2
|
+
|
|
3
|
+
Constraints restrict the feasible solution space of your mathematical or logical model.
|
|
4
|
+
In WaSHI, both linear arithmetic constraints and propositional SMT formulas are registered using **`m.constraint()`**:
|
|
5
|
+
|
|
6
|
+
```typescript
|
|
7
|
+
m.constraint(expr`${desks} + 2 * ${chairs} <= 40`, "wood_limit");
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## ✍️ Algebraic Linear Constraints (`expr`)
|
|
13
|
+
|
|
14
|
+
The `expr` tagged template literal syntax allows writing equations and inequalities with natural mathematical notation:
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import { Model, expr } from "@ohtaman/washi";
|
|
18
|
+
|
|
19
|
+
const m = new Model();
|
|
20
|
+
const x = m.continuous("x", { lb: 0 });
|
|
21
|
+
const y = m.continuous("y", { lb: 0 });
|
|
22
|
+
|
|
23
|
+
// Upper bound inequality (<=)
|
|
24
|
+
m.constraint(expr`${x} + 2 * ${y} <= 40`, "wood_limit");
|
|
25
|
+
|
|
26
|
+
// Lower bound inequality (>=)
|
|
27
|
+
m.constraint(expr`3 * ${x} + ${y} >= 20`, "min_demand");
|
|
28
|
+
|
|
29
|
+
// Exact equality (==)
|
|
30
|
+
m.constraint(expr`${x} + ${y} == 30`, "exact_quota");
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Supported Comparison Operators
|
|
34
|
+
- `<=`: Upper bound constraint
|
|
35
|
+
- `>=`: Lower bound constraint
|
|
36
|
+
- `==` or `=`: Exact equality constraint
|
|
37
|
+
|
|
38
|
+
### Batch Registration
|
|
39
|
+
You can pass an array of constraints to `m.constraint([...])` to register them transactionally in a single call:
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
m.constraint([expr`${x} <= 10`, expr`${y} <= 20`]);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 🏷️ Naming Constraints
|
|
48
|
+
|
|
49
|
+
Every constraint can be given a unique, descriptive name as the second argument of `m.constraint(c, name)`:
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
m.constraint(expr`${x} + ${y} <= 100`, "budget_cap");
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Why Name Constraints?
|
|
56
|
+
1. **Model Inspection (`m.toLpString()`)**: Exported CPLEX LP strings retain these descriptive names (sanitized if needed) so you can inspect the exact generated formulation.
|
|
57
|
+
2. **Conflict Diagnosis (`conflicts` / `unsatCore`)**: When constraints contradict each other, Z3 `m.check()` returns the exact constraint names forming the conflicting subset.
|
|
58
|
+
3. **Direct Lookup (`m.getConstraintByName()`)**: Retrieve a constraint by name after solving to inspect its `dual` or `slack` value.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 🔁 Adding Constraints over Iterables & Sums
|
|
63
|
+
|
|
64
|
+
Use `sum()` or `dot()` inside `expr` to express linear aggregations cleanly:
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import { Model, sum, expr } from "@ohtaman/washi";
|
|
68
|
+
|
|
69
|
+
const items = ["A", "B", "C"];
|
|
70
|
+
const weights = { A: 5, B: 10, C: 15 };
|
|
71
|
+
const selected = m.binary(items);
|
|
72
|
+
|
|
73
|
+
// Total weight cannot exceed capacity 25
|
|
74
|
+
m.constraint(
|
|
75
|
+
expr`${sum(items, (i) => selected[i].mul(weights[i]))} <= 25`,
|
|
76
|
+
"capacity_limit"
|
|
77
|
+
);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 🎯 Conditional / Indicator Constraints (`m.indicator`)
|
|
83
|
+
|
|
84
|
+
When a linear constraint should only take effect when a binary decision variable is active (`1` or `0`), use `m.indicator()`:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
const useDiscount = m.binary("use_discount");
|
|
88
|
+
const cartTotal = m.continuous("cart_total", { lb: 0, ub: 10_000 });
|
|
89
|
+
|
|
90
|
+
// If useDiscount == 1, then cartTotal >= 100
|
|
91
|
+
// WaSHI automatically derives a safe, tight Big-M from the variable bounds
|
|
92
|
+
m.indicator(useDiscount, expr`${cartTotal} >= 100`, {
|
|
93
|
+
activeValue: 1,
|
|
94
|
+
name: "min_spend_for_discount",
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Safe Automatic Big-M Derivation & Bounds
|
|
99
|
+
|
|
100
|
+
When `options.M` is omitted, WaSHI automatically derives a safe, minimal Big-M value from the finite variable bounds:
|
|
101
|
+
- For `<=` constraints, variables with positive coefficients require finite upper bounds (`ub`), and negative coefficients require finite lower bounds (`lb`).
|
|
102
|
+
- For `>=` constraints, variables with positive coefficients require finite lower bounds (`lb`), and negative coefficients require finite upper bounds (`ub`).
|
|
103
|
+
- For `==` constraints, safe bounds are derived separately for both the `<=` and `>=` directions (generating `name_le` and `name_ge`).
|
|
104
|
+
|
|
105
|
+
If any referenced variable lacks the required finite bounds, `m.indicator()` throws an actionable error prompting for finite bounds or an explicit `options.M`.
|
|
106
|
+
|
|
107
|
+
### Explicit Override & Numerical Stability
|
|
108
|
+
|
|
109
|
+
You can override the derived Big-M by providing `{ M: number }`:
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
m.indicator(useDiscount, expr`${cartTotal} >= 100`, {
|
|
113
|
+
activeValue: 1,
|
|
114
|
+
M: 10_000, // Explicit override
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
> [!TIP]
|
|
119
|
+
> **Numerical Stability vs. Correctness**:
|
|
120
|
+
> An $M$ value that is too small can inadvertently cut off valid solutions. Conversely, an excessively large $M$ (e.g., $10^9$) causes poor matrix conditioning and numerical instability in MIP solvers.
|
|
121
|
+
> By setting explicit, realistic variable bounds (`lb` and `ub`), WaSHI derives the tightest possible safe Big-M automatically.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## 🧠 Propositional Logic & SMT Constraints (`BoolVar`, `and`, `or`, `not`, `implies`, `xor`)
|
|
126
|
+
|
|
127
|
+
When solving with the **Z3** SMT engine (`@ohtaman/washi/solvers/z3`), you can combine propositional boolean variables (`m.bool()`) and linear constraints (`Constraint`) using first-class logical operators:
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
import { Model, expr, and, or, not, implies, xor } from "@ohtaman/washi";
|
|
131
|
+
import "@ohtaman/washi/solvers/z3";
|
|
132
|
+
|
|
133
|
+
const m = new Model({ solver: "z3" });
|
|
134
|
+
const isAdmin = m.bool("isAdmin");
|
|
135
|
+
const hasMfa = m.bool("hasMfa");
|
|
136
|
+
const Clearance = m.integer("clearance", { lb: 0, ub: 5 });
|
|
137
|
+
|
|
138
|
+
// Method syntax on BoolVar / BoolOp:
|
|
139
|
+
m.constraint(isAdmin.implies(hasMfa), "admin_requires_mfa");
|
|
140
|
+
|
|
141
|
+
// Mixing boolean propositions with linear arithmetic comparisons:
|
|
142
|
+
m.constraint(
|
|
143
|
+
implies(isAdmin, expr`${Clearance} >= 4`),
|
|
144
|
+
"admin_clearance_level"
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// Top-level helper functions (and, or, not, implies, xor):
|
|
148
|
+
m.constraint(and(or(isAdmin, hasMfa), not(xor(isAdmin, hasMfa))), "policy");
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Choosing Between `m.indicator()` and `implies()`
|
|
152
|
+
|
|
153
|
+
| Feature | `m.indicator(binVar, constraint, opts)` | `implies(a, b)` / `a.implies(b)` |
|
|
154
|
+
| :--- | :--- | :--- |
|
|
155
|
+
| **Trigger variable** | Numeric `Binary` variable (`m.binary()`) | `BoolVar` (`m.bool()`), `BoolOp`, or `Constraint` |
|
|
156
|
+
| **Consequent** | Linear `Constraint` (`<=`, `>=`, `==`) | `BoolVar`, `BoolOp`, or linear `Constraint` |
|
|
157
|
+
| **Target Solvers** | **All MIP/LP solvers** (HiGHS, GLPK, Z3) via Big-M linearization | **Z3 SMT solver** (`capabilities.supportsSMT: true`) |
|
|
158
|
+
| **Best For** | Operations research & MIP optimization with an objective (`m.solve()`) | Rule engines, policy verification, and SMT feasibility (`m.check()`) |
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## 📐 Dual Values, Shadow Prices & Lookups
|
|
163
|
+
|
|
164
|
+
For continuous Linear Programming (LP) problems, query the economic shadow price (`dual`) and surplus (`slack`) on each constraint:
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
m.constraint(expr`${x} + 2 * ${y} <= 40`, "wood");
|
|
168
|
+
|
|
169
|
+
await m.solve();
|
|
170
|
+
|
|
171
|
+
const woodConstraint = m.getConstraintByName("wood");
|
|
172
|
+
console.log("Shadow price (dual):", woodConstraint?.dual);
|
|
173
|
+
console.log("Slack surplus:", woodConstraint?.slack);
|
|
174
|
+
console.log("Total linear constraints:", m.numConstrs);
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
For logical constraints registered with a name, use `m.getLogicalConstraintByName(name)` to look up the registered `LogicalConstraintItem`.
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# Expressions (`expr`, `sum`, `dot`)
|
|
2
|
+
|
|
3
|
+
Mathematical expressions are linear or quadratic combinations of decision variables and numeric coefficients.
|
|
4
|
+
|
|
5
|
+
In JavaScript and TypeScript—which do not support operator overloading—WaSHI provides the **unified tagged template literal (`expr`)**, **`sum()`**, and matrix/vector **`dot()`** to construct algebraic expressions without losing static type safety.
|
|
6
|
+
|
|
7
|
+
| Approach | Example | Infix readability | Type safety & IDE rename | Indexed variables (`flow[i][j]`) |
|
|
8
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
9
|
+
| Method chaining | `x.mul(30).add(y.mul(50)).le(40)` | Low | Yes | Yes |
|
|
10
|
+
| Raw string parsing | `parseConstraint("30 * x + 50 * y <= 40", resolver)` | High | No (runtime string lookup) | Fragile (string concatenation) |
|
|
11
|
+
| **Tagged template (`expr`)** | `` expr`30 * ${x} + 50 * ${y} <= 40` `` | **High** | **Yes (actual TS references)** | **Yes (bound by object reference)** |
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## ✍️ Tagged Template Literal (`expr`)
|
|
16
|
+
|
|
17
|
+
Similar to parameterized `sql` tagged templates, `expr` receives the static operator text (`+`, `-`, `*`, `^2`, `<=`, `>=`, `==`) separately from the interpolated `${...}` values (`Var`, `LinExpr`, `QuadExpr`, or finite `number`). Variables are bound by object reference rather than resolved by string name, avoiding naming collisions and preserving TypeScript type checking and IDE refactoring.
|
|
18
|
+
|
|
19
|
+
Use `expr` for both arithmetic expressions (`LinExpr | QuadExpr`) and relational constraints (`Constraint`):
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { Model, expr } from "@ohtaman/washi";
|
|
23
|
+
|
|
24
|
+
const m = new Model();
|
|
25
|
+
const x = m.continuous("x", { lb: 0 });
|
|
26
|
+
const y = m.continuous("y", { lb: 0 });
|
|
27
|
+
|
|
28
|
+
// 1. Linear combination: 3*x + 5*y - 10
|
|
29
|
+
const linearExpr = expr`3 * ${x} + 5 * ${y} - 10`;
|
|
30
|
+
|
|
31
|
+
// 2. Quadratic expression: x * y + 2 * x^2
|
|
32
|
+
const quadExpr = expr`${x} * ${y} + 2 * ${x} * ${x}`;
|
|
33
|
+
|
|
34
|
+
// 3. Relational constraint:
|
|
35
|
+
const balanceConstraint = expr`${x} + 2 * ${y} <= 40`;
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Supported Syntax in `expr`
|
|
39
|
+
- **Addition & Subtraction**: `${x} + ${y} - 5`
|
|
40
|
+
- **Scalar Multiplication**: `4 * ${x}`, `${x} * 4`
|
|
41
|
+
- **Variable Multiplication (Quadratic terms)**: `${x} * ${y}`, `2 * ${x} * ${x}`
|
|
42
|
+
- **Powers & Exponentiation**: `${x}^2`, `${x}**2`
|
|
43
|
+
- **Unary Negation**: `-${x} + 10`
|
|
44
|
+
- **Parentheses**: `2 * (${x} + ${y})`
|
|
45
|
+
- **Nested Expressions**: `expr` seamlessly interpolates variables, numbers, `sum()` results, and nested expressions.
|
|
46
|
+
|
|
47
|
+
> [!NOTE]
|
|
48
|
+
> For relational comparisons (`<=`, `>=`, `==`), `expr` (e.g., ``expr`${x} + 2 * ${y} <= 40` ``) returns a first-class `Constraint`.
|
|
49
|
+
> For arithmetic expressions without comparison operators, `expr` uniformly handles linear and quadratic expressions (up to degree 2): if quadratic terms are present, `expr` returns a `QuadExpr`; otherwise, it returns a `LinExpr`. Polynomial terms of degree $> 2$ (e.g. ``${x} * ${y} * ${z}``, ``${x}^3``) and non-linear constraints (e.g. ``${x} * ${y} <= 10``) are rejected because standard MILP/LP/QP formulations only support linear constraints and quadratic objectives.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## ⚡ High-Performance Summation (`sum`)
|
|
54
|
+
|
|
55
|
+
For iterating over large collections of variables or indices:
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { sum } from "@ohtaman/washi";
|
|
59
|
+
|
|
60
|
+
const items = ["A", "B", "C"];
|
|
61
|
+
const weights = { A: 10, B: 20, C: 30 };
|
|
62
|
+
const x = m.binary(items);
|
|
63
|
+
|
|
64
|
+
// 1. Transform elements with a mapping callback:
|
|
65
|
+
const totalWeight = sum(items, (item) => x[item].mul(weights[item]));
|
|
66
|
+
|
|
67
|
+
// 2. Direct array summation:
|
|
68
|
+
const count = sum(Object.values(x));
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`sum()` runs in $O(N)$ linear time without AST overhead.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 🎯 Dot Product (`dot`)
|
|
76
|
+
|
|
77
|
+
The `dot(a, b)` helper computes linear combinations of numeric coefficients (`number`) and variables or linear expressions (`Var | LinExpr`) in either operand order (`dot(coeffs, vars)` or `dot(vars, coeffs)`).
|
|
78
|
+
|
|
79
|
+
Supported 1D and 2D container shapes:
|
|
80
|
+
- **1D Arrays**: `number[]` $\times$ `(Var | LinExpr)[]`
|
|
81
|
+
- **1D Records**: `Record<K, number>` $\times$ `Record<K, Var | LinExpr>`
|
|
82
|
+
- **2D Arrays**: `number[][]` $\times$ `(Var | LinExpr)[][]`
|
|
83
|
+
- **2D Records**: `Record<R, Record<C, number>>` $\times$ `Record<R, Record<C, Var | LinExpr>>`
|
|
84
|
+
- **2D Mixed**: `Array<Record<C, ...>>` or `Record<R, Array<...>>`
|
|
85
|
+
|
|
86
|
+
Mismatched array lengths, missing dictionary keys, or non-finite/non-linear leaves throw an explicit error indicating the offending path (e.g., `at '[0].StoreB'`).
|
|
87
|
+
|
|
88
|
+
### 1D Vector Dot Product
|
|
89
|
+
```typescript
|
|
90
|
+
import { dot } from "@ohtaman/washi";
|
|
91
|
+
|
|
92
|
+
const profits = { desk: 30, chair: 50 };
|
|
93
|
+
const vars = m.integer(["desk", "chair"]);
|
|
94
|
+
|
|
95
|
+
// Computes 30 * vars.desk + 50 * vars.chair
|
|
96
|
+
const totalProfit = dot(profits, vars);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 2D Matrix Dot Product
|
|
100
|
+
Works directly with 2D arrays (`m.continuous(rows, cols)`), nested dictionaries (`m.continuous(rowKeys, colKeys)`), or mixed 2D containers:
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
const factories = ["Tokyo", "Osaka"];
|
|
104
|
+
const stores = ["StoreA", "StoreB"];
|
|
105
|
+
|
|
106
|
+
const costs = {
|
|
107
|
+
Tokyo: { StoreA: 4, StoreB: 7 },
|
|
108
|
+
Osaka: { StoreA: 8, StoreB: 3 },
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const ship = m.continuous(factories, stores);
|
|
112
|
+
|
|
113
|
+
// Total shipping cost in one line:
|
|
114
|
+
m.objective(minimize(dot(costs, ship)));
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 🧮 Quadratic Expressions (`QuadExpr` & `expr`)
|
|
120
|
+
|
|
121
|
+
WaSHI natively supports Convex Quadratic Programming (QP) with the HiGHS solver. Quadratic objectives can be written uniformly using the `expr` template literal, or algebraically using `QuadExpr` and `.mul()`:
|
|
122
|
+
|
|
123
|
+
### 1. Using `expr` (Recommended)
|
|
124
|
+
You can write quadratic formulas naturally with `*`, `^2`, or `**2`:
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
import { Model, expr, minimize } from "@ohtaman/washi";
|
|
128
|
+
|
|
129
|
+
const m = new Model();
|
|
130
|
+
const x = m.continuous("x", { lb: 0 });
|
|
131
|
+
const y = m.continuous("y", { lb: 0 });
|
|
132
|
+
|
|
133
|
+
// Quadratic objective using expr:
|
|
134
|
+
m.objective(minimize(expr`3 * ${x} + ${x}^2 + ${y}^2`));
|
|
135
|
+
|
|
136
|
+
// Linear constraint using expr:
|
|
137
|
+
m.constraint(expr`${x} + ${y} >= 4`);
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### 2. Method Chaining with `.mul()` or `QuadExpr`
|
|
141
|
+
Alternatively, you can construct quadratic expressions programmatically:
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import { Model, QuadExpr, minimize } from "@ohtaman/washi";
|
|
145
|
+
|
|
146
|
+
const m = new Model();
|
|
147
|
+
const x = m.continuous("x", { lb: 0 });
|
|
148
|
+
const y = m.continuous("y", { lb: 0 });
|
|
149
|
+
|
|
150
|
+
// 1. Construct quadratic terms with .mul()
|
|
151
|
+
// Represents x * y + 2 * x^2
|
|
152
|
+
const quad = x.mul(y).add(x.mul(x).mul(2));
|
|
153
|
+
|
|
154
|
+
// 2. Build portfolio risk objective: w^T * Sigma * w
|
|
155
|
+
const assets = ["Tech", "Bonds"];
|
|
156
|
+
const w = m.continuous(assets, { lb: 0, ub: 1 });
|
|
157
|
+
|
|
158
|
+
let risk = new QuadExpr();
|
|
159
|
+
risk = risk.add(w.Tech.mul(w.Tech).mul(0.04));
|
|
160
|
+
risk = risk.add(w.Tech.mul(w.Bonds).mul(0.005));
|
|
161
|
+
risk = risk.add(w.Bonds.mul(w.Bonds).mul(0.01));
|
|
162
|
+
|
|
163
|
+
m.objective(minimize(risk));
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## 📊 Evaluating Expressions After Solving (`.value`)
|
|
169
|
+
|
|
170
|
+
Both `LinExpr` and `QuadExpr` expose a `.value` getter (`number | null`). Once a model has been solved and all referenced variables have finite primal values, you can evaluate any sub-expression—such as total department cost, portfolio return, or resource utilization—directly without manual arithmetic:
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
const revenue = expr`30 * ${x} + 50 * ${y}`;
|
|
174
|
+
const laborUsed = expr`2 * ${x} + ${y}`;
|
|
175
|
+
|
|
176
|
+
m.objective(maximize(revenue));
|
|
177
|
+
m.constraint(expr`${x} + 2 * ${y} <= 40`, "wood_cap");
|
|
178
|
+
m.constraint(expr`${laborUsed} <= 50`, "labor_cap");
|
|
179
|
+
|
|
180
|
+
await m.solve();
|
|
181
|
+
|
|
182
|
+
console.log("Total Revenue:", revenue.value); // 1100 (x = 20, y = 10)
|
|
183
|
+
console.log("Labor Used:", laborUsed.value); // 50
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
If the model has not been solved (or any variable in the expression has `value === null`), `.value` returns `null`.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## 🔗 Fluent Arithmetic & Relational Methods
|
|
191
|
+
|
|
192
|
+
In addition to `expr`, `Var`, `LinExpr`, and `QuadExpr` provide immutable fluent methods for programmatic expression building:
|
|
193
|
+
|
|
194
|
+
| Method | Supported On | Return Type | Description |
|
|
195
|
+
| :--- | :--- | :--- | :--- |
|
|
196
|
+
| `.add(other)` | `Var`, `LinExpr`, `QuadExpr` | `LinExpr` or `QuadExpr` | Adds a number, `Var`, `LinExpr`, or `QuadExpr`. |
|
|
197
|
+
| `.sub(other)` | `Var`, `LinExpr`, `QuadExpr` | `LinExpr` or `QuadExpr` | Subtracts a number, `Var`, `LinExpr`, or `QuadExpr`. |
|
|
198
|
+
| `.mul(other)` | `Var`, `LinExpr`, `QuadExpr` | `LinExpr` or `QuadExpr` | Multiplies by a scalar `number`, or multiplies two linear terms into a `QuadExpr`. |
|
|
199
|
+
| `.div(divisor)` | `LinExpr`, `QuadExpr` | `LinExpr` or `QuadExpr` | Divides by a non-zero finite scalar `number`. |
|
|
200
|
+
| `.neg()` | `Var`, `LinExpr`, `QuadExpr` | `LinExpr` or `QuadExpr` | Negates all terms ($-e$). |
|
|
201
|
+
| `.le(rhs)` | `Var`, `LinExpr` | `Constraint` | Builds a $\le$ constraint (`lhs <= rhs`). |
|
|
202
|
+
| `.ge(rhs)` | `Var`, `LinExpr` | `Constraint` | Builds a $\ge$ constraint (`lhs >= rhs`). |
|
|
203
|
+
| `.eq(rhs)` | `Var`, `LinExpr` | `Constraint` | Builds an $==$ equality constraint (`lhs == rhs`). |
|
|
204
|
+
|