@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.
Files changed (97) hide show
  1. package/README.ja.md +100 -107
  2. package/README.md +98 -105
  3. package/README.zh-CN.md +101 -108
  4. package/dist/browser/highs-worker.js +21 -3
  5. package/dist/browser/loader-CtaDHef_.js +273 -0
  6. package/dist/browser/solvers/glpk.js +433 -286
  7. package/dist/browser/solvers/glpk.min.js +1 -1
  8. package/dist/browser/solvers/z3.js +1002 -981
  9. package/dist/browser/solvers/z3.min.js +2 -2
  10. package/dist/browser/washi.js +2129 -676
  11. package/dist/browser/washi.min.js +4 -4
  12. package/dist/lib/highs-worker.js +21 -3
  13. package/dist/lib/loader-DmNC_UNk.js +279 -0
  14. package/dist/lib/registry-BEfvpZW3.js +951 -0
  15. package/dist/lib/solvers/glpk.js +201 -90
  16. package/dist/lib/solvers/z3.js +399 -259
  17. package/dist/lib/washi.js +1627 -988
  18. package/dist/types/index.d.ts +4 -4
  19. package/dist/types/index.d.ts.map +1 -1
  20. package/dist/types/modeler/big-m.d.ts +75 -0
  21. package/dist/types/modeler/big-m.d.ts.map +1 -0
  22. package/dist/types/modeler/boolean-expr.d.ts +103 -14
  23. package/dist/types/modeler/boolean-expr.d.ts.map +1 -1
  24. package/dist/types/modeler/constraint.d.ts +36 -9
  25. package/dist/types/modeler/constraint.d.ts.map +1 -1
  26. package/dist/types/modeler/expression.d.ts +113 -28
  27. package/dist/types/modeler/expression.d.ts.map +1 -1
  28. package/dist/types/modeler/index.d.ts +10 -6
  29. package/dist/types/modeler/index.d.ts.map +1 -1
  30. package/dist/types/modeler/lp-writer.d.ts +27 -2
  31. package/dist/types/modeler/lp-writer.d.ts.map +1 -1
  32. package/dist/types/modeler/model.d.ts +478 -131
  33. package/dist/types/modeler/model.d.ts.map +1 -1
  34. package/dist/types/modeler/objective.d.ts +59 -0
  35. package/dist/types/modeler/objective.d.ts.map +1 -0
  36. package/dist/types/modeler/parser.d.ts +34 -10
  37. package/dist/types/modeler/parser.d.ts.map +1 -1
  38. package/dist/types/modeler/quad-expr.d.ts +26 -7
  39. package/dist/types/modeler/quad-expr.d.ts.map +1 -1
  40. package/dist/types/modeler/solution-validator.d.ts +93 -0
  41. package/dist/types/modeler/solution-validator.d.ts.map +1 -0
  42. package/dist/types/modeler/solvers/glpk/solver.d.ts +26 -2
  43. package/dist/types/modeler/solvers/glpk/solver.d.ts.map +1 -1
  44. package/dist/types/modeler/solvers/highs/loader.d.ts +36 -11
  45. package/dist/types/modeler/solvers/highs/loader.d.ts.map +1 -1
  46. package/dist/types/modeler/solvers/highs/solver.d.ts +11 -4
  47. package/dist/types/modeler/solvers/highs/solver.d.ts.map +1 -1
  48. package/dist/types/modeler/solvers/highs/worker-client.d.ts.map +1 -1
  49. package/dist/types/modeler/solvers/mock.d.ts +14 -5
  50. package/dist/types/modeler/solvers/mock.d.ts.map +1 -1
  51. package/dist/types/modeler/solvers/types.d.ts +71 -11
  52. package/dist/types/modeler/solvers/types.d.ts.map +1 -1
  53. package/dist/types/modeler/solvers/z3/loader.d.ts +2 -3
  54. package/dist/types/modeler/solvers/z3/loader.d.ts.map +1 -1
  55. package/dist/types/modeler/solvers/z3/solver.d.ts +15 -5
  56. package/dist/types/modeler/solvers/z3/solver.d.ts.map +1 -1
  57. package/dist/types/modeler/types.d.ts +59 -1
  58. package/dist/types/modeler/types.d.ts.map +1 -1
  59. package/dist/types/modeler/validation.d.ts +56 -0
  60. package/dist/types/modeler/validation.d.ts.map +1 -0
  61. package/dist/types/modeler/variable.d.ts +48 -14
  62. package/dist/types/modeler/variable.d.ts.map +1 -1
  63. package/docs/examples/diet-sensitivity.md +90 -0
  64. package/docs/examples/ecommerce-coupons.md +106 -0
  65. package/docs/examples/index.md +53 -0
  66. package/docs/examples/knapsack.md +60 -0
  67. package/docs/examples/portfolio-qp.md +83 -0
  68. package/docs/examples/production.md +51 -0
  69. package/docs/examples/shift-scheduling.md +175 -0
  70. package/docs/examples/smt-security.md +74 -0
  71. package/docs/examples/transportation.md +90 -0
  72. package/docs/examples/unsat-core.md +64 -0
  73. package/docs/getting-started/ai-agents.md +267 -0
  74. package/docs/getting-started/cross-origin.md +87 -0
  75. package/docs/getting-started/index.md +116 -0
  76. package/docs/getting-started/runtimes.md +227 -0
  77. package/docs/guide/constraints.md +177 -0
  78. package/docs/guide/expressions.md +204 -0
  79. package/docs/guide/objectives.md +170 -0
  80. package/docs/guide/unsat-core.md +147 -0
  81. package/docs/guide/variables.md +188 -0
  82. package/docs/ja/getting-started/ai-agents.md +266 -0
  83. package/docs/solvers/custom-solver.md +525 -0
  84. package/docs/solvers/glpk.md +66 -0
  85. package/docs/solvers/highs.md +93 -0
  86. package/docs/solvers/index.md +42 -0
  87. package/docs/solvers/z3.md +78 -0
  88. package/docs/zh/getting-started/ai-agents.md +266 -0
  89. package/llms-full.txt +3249 -0
  90. package/llms.txt +197 -0
  91. package/package.json +18 -5
  92. package/skills/washi/SKILL.md +197 -0
  93. package/dist/browser/loader-CVt4CloI.js +0 -58
  94. package/dist/lib/async-mutex-Duo4jwzn.js +0 -195
  95. package/dist/lib/constraint-BRuB3ooC.js +0 -90
  96. package/dist/lib/loader-Dti0v9yC.js +0 -50
  97. 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
+