@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,170 @@
|
|
|
1
|
+
# Objectives & Solving (`m.solve` & `m.check`)
|
|
2
|
+
|
|
3
|
+
The objective function defines the goal of mathematical optimization—whether to maximize profits, minimize costs, or minimize quadratic risk variance. When you only need to verify constraint feasibility or solve pure SMT logic, you can also run `m.check()` without an objective.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Setting the Objective Function
|
|
8
|
+
|
|
9
|
+
Set the objective direction and formula declaratively using `m.objective(maximize(...))` or `m.objective(minimize(...))`:
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { Model, expr, maximize, minimize } from "@ohtaman/washi";
|
|
13
|
+
|
|
14
|
+
const m = new Model("ProductionRevenue");
|
|
15
|
+
const x = m.integer("x", { lb: 0, ub: 40 });
|
|
16
|
+
const y = m.integer("y", { lb: 0, ub: 40 });
|
|
17
|
+
|
|
18
|
+
// Maximize revenue: 50*x + 40*y subject to resource capacity
|
|
19
|
+
m.objective(maximize(expr`50 * ${x} + 40 * ${y}`));
|
|
20
|
+
m.constraint(expr`2 * ${x} + 3 * ${y} <= 100`, "resource_limit");
|
|
21
|
+
|
|
22
|
+
// Or minimize cost:
|
|
23
|
+
// m.objective(minimize(expr`10 * ${x} + 15 * ${y}`));
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Calling `m.objective()` with no arguments returns the current objective expression (`LinExpr | QuadExpr`). If you omit setting an objective, the model defaults to minimizing `0`.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 🚀 Solving the Model (`m.solve()` vs. `m.check()`)
|
|
31
|
+
|
|
32
|
+
WaSHI provides two asynchronous execution entrypoints:
|
|
33
|
+
|
|
34
|
+
| Method | Return Value | Purpose |
|
|
35
|
+
| :--- | :--- | :--- |
|
|
36
|
+
| **`await m.solve(options?)`** | `Promise<SolverStatus>` | Optimizes the objective (`Minimize` or `Maximize`) subject to all constraints. Populates primal values, objective value, MIP bounds/gaps, and LP duals/slacks. |
|
|
37
|
+
| **`await m.check(options?)`** | `Promise<CheckResult>` | Checks constraint satisfiability without an objective. Returns `{ status, values, conflicts?, unsatCore?, runTime? }` where `values: Record<string, number \| boolean>` contains both numeric and Boolean variable assignments. Ideal for SMT/SAT verification and Z3 `unsatCore` extraction. |
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
const status = await m.solve({
|
|
41
|
+
timeLimit: 10.0, // Timeout in seconds
|
|
42
|
+
mipGap: 0.01, // Stop within 1% of global optimum
|
|
43
|
+
outputFlag: false, // Enable/disable solver console logging (HiGHS: outputFlag, GLPK: verbose)
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Solve Options (`SolveOptions`)
|
|
48
|
+
|
|
49
|
+
| Option | Type | Default | Description |
|
|
50
|
+
| :--- | :--- | :--- | :--- |
|
|
51
|
+
| `solver` | `string` \| `Solver` | Model's solver (`"highs"`) | Registered solver name (e.g. `"highs"`, `"z3"`, `"glpk"`, or custom) or `Solver` instance for this run. |
|
|
52
|
+
| `timeLimit` | `number` | `undefined` | Maximum solve duration in seconds. |
|
|
53
|
+
| `mipGap` / `relGap` | `number` | `undefined` | Relative MIP optimality gap tolerance (e.g. `0.01` for 1%). |
|
|
54
|
+
| `absGap` | `number` | `undefined` | Absolute MIP optimality gap tolerance (HiGHS `mip_abs_gap`). |
|
|
55
|
+
| `presolve` | `"on"` \| `"off"` \| `"choose"` | `"choose"` | Controls solver presolve reduction prior to optimization. |
|
|
56
|
+
| `outputFlag` / `verbose` | `boolean` | `false` | Emits internal solver iteration logs (`outputFlag` for HiGHS, `verbose` for GLPK). |
|
|
57
|
+
| `useWorker` | `boolean` | `true` (browser) | Offloads HiGHS to a dedicated Web Worker in browsers. Set `false` for main-thread execution. |
|
|
58
|
+
| `wasmUrl` | `string` | Auto-resolved | Custom URL to load `highs.wasm` in browser deployments. |
|
|
59
|
+
| `signal` | `AbortSignal` | `undefined` | Cooperative cancellation signal (`AbortController.signal`). |
|
|
60
|
+
| `onProgress` | `(e: SolveProgressEvent) => void` | `undefined` | Callback receiving live solver progress snapshots. |
|
|
61
|
+
|
|
62
|
+
You can also pass a solver name directly as the first argument: `await m.solve("glpk", { timeLimit: 5 })`.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## ⏱️ Real-Time Progress (`onProgress`) & Cancellation (`AbortSignal`)
|
|
67
|
+
|
|
68
|
+
For long-running MIP optimizations in interactive UIs, you can stream live branch-and-bound progress (`SolveProgressEvent`) and allow users to cancel the solve cleanly with `AbortController`:
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
const controller = new AbortController();
|
|
72
|
+
|
|
73
|
+
// Example: cancel if the user clicks a "Stop" button or after a custom condition
|
|
74
|
+
const status = await m.solve({
|
|
75
|
+
signal: controller.signal,
|
|
76
|
+
onProgress: (event) => {
|
|
77
|
+
console.log(
|
|
78
|
+
`[${event.elapsedTime.toFixed(1)}s] ` +
|
|
79
|
+
`Best: ${event.objectiveValue ?? "—"}, ` +
|
|
80
|
+
`Bound: ${event.objectiveBound ?? "—"}, ` +
|
|
81
|
+
`Gap: ${event.mipGap !== undefined ? (event.mipGap * 100).toFixed(2) + "%" : "—"}`
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// Optional: stop early as soon as a good-enough incumbent is found
|
|
85
|
+
if (event.objectiveValue !== undefined && event.objectiveValue >= 1000) {
|
|
86
|
+
controller.abort();
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
console.log("Status:", m.status); // "Feasible" (if an incumbent was found before abort) or "Unknown"
|
|
92
|
+
console.log("Termination:", m.terminationStatus); // "Interrupted"
|
|
93
|
+
console.log("Has incumbent:", m.hasPrimalSolution); // true if an incumbent was available
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### `SolveProgressEvent` Fields
|
|
97
|
+
|
|
98
|
+
| Field | Type | Description |
|
|
99
|
+
| :--- | :--- | :--- |
|
|
100
|
+
| `elapsedTime` | `number` | Elapsed solver time in seconds (optional alias: `elapsedSeconds`). |
|
|
101
|
+
| `phase` | `"simplex" \| "ipm" \| "mip" \| undefined` | Current solver phase when known. |
|
|
102
|
+
| `objectiveValue` | `number \| undefined` | Current best feasible incumbent objective value (MIP). |
|
|
103
|
+
| `objectiveBound` | `number \| undefined` | Current best proven dual / objective bound (MIP). |
|
|
104
|
+
| `relGap` / `mipGap` | `number \| undefined` | Current relative MIP optimality gap (`0.05` = 5%). |
|
|
105
|
+
| `absGap` | `number \| undefined` | Current absolute MIP optimality gap `\|objectiveValue - objectiveBound\|`. |
|
|
106
|
+
| `nodes` | `number \| undefined` | Branch-and-bound nodes explored so far. |
|
|
107
|
+
| `iterations` | `number \| undefined` | Solver iteration count (simplex, IPM, or MIP LP iterations). |
|
|
108
|
+
| `message` | `string \| undefined` | Raw progress line emitted by the solver. |
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 📊 Reading Results, Bounds & Solver Status
|
|
113
|
+
|
|
114
|
+
After solving, query the status, optimal values, and MIP optimality bounds on the `Model`:
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
const status = await m.solve();
|
|
118
|
+
|
|
119
|
+
console.log("Status:", m.status); // e.g. "Optimal" or "Feasible"
|
|
120
|
+
console.log("Termination Reason:", m.terminationStatus); // e.g. "Optimal", "TimeLimit", "Interrupted"
|
|
121
|
+
console.log("Has Primal Solution:", m.hasPrimalSolution); // true when x.value / m.objectiveValue are populated
|
|
122
|
+
|
|
123
|
+
// Objective & MIP Gap Metrics
|
|
124
|
+
console.log("Objective Value:", m.objectiveValue); // Best primal objective (or null)
|
|
125
|
+
console.log("Objective Bound:", m.objectiveBound); // Best dual bound (or undefined)
|
|
126
|
+
console.log("Relative Gap:", m.relGap); // Relative MIP gap (alias: m.mipGap)
|
|
127
|
+
console.log("Absolute Gap:", m.absGap); // |objectiveValue - objectiveBound|
|
|
128
|
+
|
|
129
|
+
// Individual & Bulk Variable Values
|
|
130
|
+
console.log("x =", x.value, "y =", y.value);
|
|
131
|
+
console.log(m.getValues({ x, y })); // { x: ..., y: ... }
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Understanding `m.status` vs. `m.terminationStatus`
|
|
135
|
+
|
|
136
|
+
When a MIP solver stops early due to a time limit, iteration limit, or `AbortSignal`, two distinct questions matter:
|
|
137
|
+
1. **Is there a usable solution?** (`m.hasPrimalSolution` / `m.status`)
|
|
138
|
+
2. **Why did the solver stop?** (`m.terminationStatus`)
|
|
139
|
+
|
|
140
|
+
If an integer-feasible incumbent was found before hitting `timeLimit` or `signal.abort()`, WaSHI sets `m.status = "Feasible"` (so you know `x.value` and `m.objectiveValue` hold a valid solution) and sets `m.terminationStatus = "TimeLimit"` (or `"Interrupted"`). If no incumbent was found before stopping, `m.hasPrimalSolution` is `false`, `m.terminationStatus` records why the solver stopped (`"TimeLimit"`, `"IterationLimit"`, or `"Interrupted"`), and `m.status` is `"TimeLimit"`, `"IterationLimit"`, or `"Unknown"` (when aborted via `AbortSignal` without an incumbent).
|
|
141
|
+
|
|
142
|
+
### All `SolverStatus` Values
|
|
143
|
+
|
|
144
|
+
| Status | `hasPrimalSolution` | Meaning |
|
|
145
|
+
| :--- | :--- | :--- |
|
|
146
|
+
| `"Optimal"` | `true` | Global optimal solution proven (within tolerances). |
|
|
147
|
+
| `"Feasible"` | `true` | Valid primal solution (incumbent) found, but optimality was not proven before stopping (`terminationStatus` indicates why: `"TimeLimit"`, `"IterationLimit"`, `"Interrupted"`, etc.). |
|
|
148
|
+
| `"Sat"` | `true` | Constraints are satisfiable (`m.check()`). |
|
|
149
|
+
| `"Infeasible"` | `false` | Proven mathematically infeasible; no solution satisfies all constraints. |
|
|
150
|
+
| `"Unsat"` | `false` | Logical/arithmetic constraints are unsatisfiable (`m.check()`). |
|
|
151
|
+
| `"Unbounded"` | `false` | Objective can improve indefinitely without violating constraints. |
|
|
152
|
+
| `"TimeLimit"` | `false` | Time limit reached before finding any feasible primal solution. |
|
|
153
|
+
| `"IterationLimit"` | `false` | Iteration limit reached before finding any feasible primal solution. |
|
|
154
|
+
| `"Unknown"` | `false` | Solver stopped inconclusively without a valid primal solution (including when cancelled via `AbortSignal` before finding an incumbent, where `terminationStatus === "Interrupted"`). |
|
|
155
|
+
| `"Error"` | `false` | Solver encountered an internal execution error. |
|
|
156
|
+
| `"NotSolved"` | `false` | Initial state before solving, or after the model has been mutated. |
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## 🔄 Model Mutations & Execution Validity
|
|
161
|
+
|
|
162
|
+
A `Model` instance keeps its solution state synchronized across mutations made through `Model` methods and `Model` / `Var` / `Constraint` property setters:
|
|
163
|
+
|
|
164
|
+
1. **Post-Solve Mutations Reset Solution State**: Adding variables or constraints, updating the objective (`m.objective(...)`, `m.objectiveExpr = ...`, `m.sense = ...`), or modifying variable bounds/types (`v.lb`, `v.ub`, `v.type`) or constraint properties (`c.lhs`, `c.sense`, `c.rhs`) immediately resets `m.status` to `"NotSolved"`, clears `m.solution` and `m.objectiveValue`, and resets variable/constraint primal and dual values to `null`.
|
|
165
|
+
2. **Mid-Flight Mutations Discard Stale Runs**: If the model is modified via these APIs while an asynchronous `m.solve()` or `m.check()` call is in progress, the stale solver result is discarded, the model remains in `"NotSolved"`, and the in-flight promise rejects with an explicit error.
|
|
166
|
+
3. **Overlapping Executions**: Starting a new `m.solve()` or `m.check()` call supersedes any earlier in-flight call on the same model so out-of-order completions can never overwrite the latest run's state.
|
|
167
|
+
4. **Transactional Batch Edits**: If an invalid edit is rejected (including batch array calls like `m.constraint([...])`), the model rolls back the failed edit without leaving partial entities registered or invalidating an existing valid solution.
|
|
168
|
+
|
|
169
|
+
> [!NOTE]
|
|
170
|
+
> Renaming already-registered variables or constraints (e.g., reassigning `c.name` after registration) or mutating internal backing collections and ASTs directly (such as `c.lhs.terms`, `m.constraints.splice(...)`, or `BoolOp.children`) is unsupported and will be rejected by model integrity validation on the next solve/check/export call.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Conflict Diagnosis (`unsatCore`) & Solution Validation
|
|
2
|
+
|
|
3
|
+
When building real-world optimization and configuration systems, two verification questions arise constantly:
|
|
4
|
+
1. **Why is my model infeasible?** Which specific business rules contradict each other?
|
|
5
|
+
2. **Does the solver's solution genuinely satisfy every constraint?** How do I verify tolerances, integrality, and formulations?
|
|
6
|
+
|
|
7
|
+
WaSHI provides built-in tools for both: **Unsat Core conflict diagnosis** via Z3 (`m.check()`), **independent solution validation** (`m.validateSolution()`), **LP formulation export** (`m.toLpString()`), and **fast unit testing** (`MockSolver`).
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 🔍 Conflict Diagnosis (`unsatCore` / `conflicts`)
|
|
12
|
+
|
|
13
|
+
When a model is **Infeasible**, traditional MIP solvers simply report that no solution exists without indicating *which* constraints are contradictory.
|
|
14
|
+
By calling **`await m.check({ solver: "z3" })`**, WaSHI uses Z3's `assertAndTrack` mechanism to compute an **Unsatisfiable Core (`unsatCore`)**—a conflicting subset of named constraints that cannot be satisfied simultaneously:
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import { Model, expr } from "@ohtaman/washi";
|
|
18
|
+
// Register the Z3 solver plugin
|
|
19
|
+
import "@ohtaman/washi/solvers/z3";
|
|
20
|
+
|
|
21
|
+
const m = new Model();
|
|
22
|
+
|
|
23
|
+
const x = m.continuous("reserve", { lb: 0 });
|
|
24
|
+
|
|
25
|
+
// Add named business requirements
|
|
26
|
+
m.constraint(expr`${x} >= 100`, "minimum_regulatory_reserve");
|
|
27
|
+
m.constraint(expr`${x} <= 50`, "budget_ceiling");
|
|
28
|
+
m.constraint(expr`${x} >= 10`, "routine_operating_cost");
|
|
29
|
+
|
|
30
|
+
const result = await m.check({ solver: "z3" });
|
|
31
|
+
|
|
32
|
+
if (result.status === "Unsat") {
|
|
33
|
+
console.log("Contradictory constraints identified:");
|
|
34
|
+
console.log(m.conflicts); // or result.conflicts / result.unsatCore / m.unsatCore
|
|
35
|
+
// Output: ["minimum_regulatory_reserve", "budget_ceiling"]
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Notice that `routine_operating_cost` is **not** in the core, because only `minimum_regulatory_reserve` ($x \ge 100$) and `budget_ceiling` ($x \le 50$) directly contradict each other.
|
|
40
|
+
|
|
41
|
+
> [!IMPORTANT]
|
|
42
|
+
> - **Use `m.check({ solver: "z3" })` for Unsat Core extraction**: `m.solve()` runs optimization (`Z3.Optimize`), whereas `m.check()` runs tracked satisfiability checking (`Z3.Solver.assertAndTrack`) and populates `result.conflicts` / `m.conflicts` (and alias `m.unsatCore`).
|
|
43
|
+
> - **Name your constraints**: Variable bounds (`lb`, `ub`) act as background domain declarations. To include a bound or business rule in the reported `unsatCore` list, register it as a named constraint via `m.constraint(expr`...`, "rule_name")`.
|
|
44
|
+
|
|
45
|
+
### Workflow: Infeasibility Diagnosis with HiGHS or Z3
|
|
46
|
+
|
|
47
|
+
Because WaSHI models are solver-independent, you can diagnose infeasibilities using either solver:
|
|
48
|
+
1. **Built-in HiGHS & GLPK (default)**: `await m.check()` uses a single-solve elastic slack formulation ($\min \sum s_i$) to identify which constraints required relaxation (`diag.conflicts`), requiring no extra dependencies.
|
|
49
|
+
2. **Z3 SMT Solver**: `await m.check({ solver: "z3" })` uses SMT tracking to extract an exact unsatisfiable core (`diag.unsatCore` / `diag.conflicts`).
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
const status = await m.solve(); // Fast MIP solve with default HiGHS
|
|
53
|
+
|
|
54
|
+
if (status === "Infeasible") {
|
|
55
|
+
// Option 1: Built-in single-solve elastic diagnosis with HiGHS
|
|
56
|
+
const diag = await m.check();
|
|
57
|
+
console.error("Model is infeasible due to conflicting rules:", diag.conflicts);
|
|
58
|
+
|
|
59
|
+
// Option 2: Exact SMT unsat core diagnosis with Z3 (requires import "@ohtaman/washi/solvers/z3")
|
|
60
|
+
// const z3Diag = await m.check({ solver: "z3" });
|
|
61
|
+
// console.error("Z3 Unsat Core:", z3Diag.unsatCore);
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## ✅ Independent Solution Validation (`m.validateSolution()`)
|
|
68
|
+
|
|
69
|
+
After solving a model (or supplying a custom candidate map via `values`), you can independently verify that all variable bounds, integer/binary domains, linear constraints, and logical SMT formulas hold against the original model definitions:
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
const status = await m.solve();
|
|
73
|
+
|
|
74
|
+
if (m.hasPrimalSolution) {
|
|
75
|
+
const report = m.validateSolution({
|
|
76
|
+
feasibilityTolerance: 1e-6, // Max allowed violation for bounds and linear constraints
|
|
77
|
+
integralityTolerance: 1e-6, // Max distance from nearest integer for Integer/Binary vars
|
|
78
|
+
evaluateLogicalConstraints: true,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
console.log("Valid:", report.valid);
|
|
82
|
+
console.log("Complete:", report.complete);
|
|
83
|
+
console.log("Max violation:", report.maxViolation);
|
|
84
|
+
console.log("Summary:", report.summary);
|
|
85
|
+
|
|
86
|
+
if (!report.valid) {
|
|
87
|
+
console.warn("Violations detected:", report.violations);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### `SolutionValidationReport` Summary
|
|
93
|
+
|
|
94
|
+
| Property | Type | Description |
|
|
95
|
+
| :--- | :--- | :--- |
|
|
96
|
+
| `valid` | `boolean` | `true` iff `complete === true`, `uncheckedConstraints.length === 0`, and `violations.length === 0`. |
|
|
97
|
+
| `complete` | `boolean` | `true` when all variables, Boolean variables, linear constraints, and logical constraints were evaluated. |
|
|
98
|
+
| `maxViolation` | `number` | Largest numeric violation magnitude across all evaluated bounds, integrality checks, and constraints. |
|
|
99
|
+
| `violations` | `SolutionViolation[]` | Flat list of violations (`"missing_value"`, `"non_finite_value"`, `"invalid_boolean_value"`, `"lower_bound"`, `"upper_bound"`, `"integrality"`, `"linear_constraint"`, `"logical_constraint"`, `"unchecked_constraint"`). |
|
|
100
|
+
| `violationsByName` | `Record<string, SolutionViolation[]>` | Violations grouped by variable or constraint name. |
|
|
101
|
+
| `uncheckedConstraints` | `string[]` | Names of constraints that could not be evaluated. |
|
|
102
|
+
| `missingVariables` | `string[]` | Names of numeric or Boolean variables whose values were `null` or `undefined`. |
|
|
103
|
+
| `summary` | `SolutionValidationSummary` | Counts of checked vs. total variables, Boolean variables, linear constraints, and logical constraints. |
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## 📄 Inspecting the LP Formulation (`m.toLpString()`)
|
|
108
|
+
|
|
109
|
+
When debugging coefficients, signs, or Big-M indicator expansions, call `m.toLpString()` to export the model in standard CPLEX/HiGHS `.lp` format:
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
const m = new Model("DebugModel");
|
|
113
|
+
const x = m.integer("x", { lb: 0, ub: 10 });
|
|
114
|
+
m.objective(maximize(expr`5 * ${x}`));
|
|
115
|
+
m.constraint(expr`2 * ${x} <= 15`, "resource_limit");
|
|
116
|
+
|
|
117
|
+
console.log(m.toLpString());
|
|
118
|
+
/*
|
|
119
|
+
Maximize
|
|
120
|
+
obj: 5 x
|
|
121
|
+
Subject To
|
|
122
|
+
resource_limit: 2 x <= 15
|
|
123
|
+
Bounds
|
|
124
|
+
0 <= x <= 10
|
|
125
|
+
Generals
|
|
126
|
+
x
|
|
127
|
+
End
|
|
128
|
+
*/
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## 🧪 Unit Testing Application Logic with `MockSolver`
|
|
134
|
+
|
|
135
|
+
When running unit tests for application code that builds or consumes a WaSHI `Model`, you can use the built-in `MockSolver` (`solver: "mock"`) to return deterministic results instantaneously without loading WebAssembly:
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
import { Model, MockSolver, expr, maximize } from "@ohtaman/washi";
|
|
139
|
+
|
|
140
|
+
const mock = new MockSolver({
|
|
141
|
+
status: "Optimal",
|
|
142
|
+
objectiveValue: 420,
|
|
143
|
+
values: { x: 10, y: 20 },
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const m = new Model({ solver: mock });
|
|
147
|
+
const x = m.integer("x");
|
|
148
|
+
const y = m.integer("y");
|
|
149
|
+
m.objective(maximize(expr`10 * ${x} + 16 * ${y}`));
|
|
150
|
+
|
|
151
|
+
await m.solve();
|
|
152
|
+
console.log(m.objectiveValue); // 420
|
|
153
|
+
console.log(x.value, y.value); // 10, 20
|
|
154
|
+
```
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# Variables & Domains
|
|
2
|
+
|
|
3
|
+
Variables represent the mathematical decision variables in your optimization model.
|
|
4
|
+
WaSHI provides domain-specific factory methods on the `Model` instance: `m.continuous()`, `m.integer()`, `m.binary()`, and `m.bool()`.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Decision Variable Types
|
|
9
|
+
|
|
10
|
+
### 1. Continuous Variables (`m.continuous`)
|
|
11
|
+
Can take any real number value within `[lb, ub]`. Ideal for physical measurements, flow rates, budgets, and weights:
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { Model } from "@ohtaman/washi";
|
|
15
|
+
|
|
16
|
+
const m = new Model();
|
|
17
|
+
|
|
18
|
+
// Single continuous variable bounded in [0, 1000]
|
|
19
|
+
const fuel = m.continuous("fuel", { lb: 0, ub: 1000 });
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### 2. Integer Variables (`m.integer`)
|
|
23
|
+
Restricted to integers. Ideal for countable assets (machines, trucks, staff count):
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
// Single integer variable bounded in [0, 15]
|
|
27
|
+
const trucks = m.integer("trucks", { lb: 0, ub: 15 });
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### 3. Binary Variables (`m.binary`)
|
|
31
|
+
Restricted to $\{0, 1\}$ as mathematical 0-1 values. Ideal for MIP selection, yes/no decisions, and facility selections:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
// Single binary variable
|
|
35
|
+
const openWarehouse = m.binary("open_warehouse");
|
|
36
|
+
// lb is automatically 0, ub is automatically 1
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 4. Propositional Boolean Variables (`m.bool`)
|
|
40
|
+
Pure boolean logic propositions for SMT / SAT modeling with Z3:
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
// Single propositional boolean variable
|
|
44
|
+
const isNight = m.bool("isNight");
|
|
45
|
+
const alarmTriggered = m.bool("alarmTriggered");
|
|
46
|
+
|
|
47
|
+
// First-class boolean operators: and, or, not, implies, xor
|
|
48
|
+
m.constraint(alarmTriggered.implies(isNight), "alarm_policy");
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 💡 Modeling Semi-Continuous Decisions
|
|
52
|
+
Semi-continuous variables take values in $\{0\} \cup [L, U]$ (either zero or between a positive lower and upper bound, useful for minimum production batches or operational thresholds).
|
|
53
|
+
|
|
54
|
+
While not a primitive `VarType` in `Model`, semi-continuous behavior is cleanly modeled by combining a continuous variable with a binary indicator variable and linking constraints ($L \cdot b \le x \le U \cdot b$):
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { Model, expr } from "@ohtaman/washi";
|
|
58
|
+
|
|
59
|
+
const m = new Model();
|
|
60
|
+
const batch = m.continuous("batch", { lb: 0, ub: 100 });
|
|
61
|
+
const isActive = m.binary("is_active");
|
|
62
|
+
|
|
63
|
+
// If isActive === 0, batch is forced to 0; if 1, batch is within [10, 100]
|
|
64
|
+
m.constraint(expr`${batch} >= 10 * ${isActive}`, "min_batch_run");
|
|
65
|
+
m.constraint(expr`${batch} <= 100 * ${isActive}`, "max_batch_run");
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Multi-Dimensional & Array Creation
|
|
71
|
+
|
|
72
|
+
The domain factory methods (`continuous`, `integer`, `binary`, `bool`) support count, arrays of keys, and row/column keys to create 1D arrays, typed dictionaries, and 2D matrices in a single call.
|
|
73
|
+
|
|
74
|
+
### By Count
|
|
75
|
+
Pass a number to create an array of variables:
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
const x = m.continuous(5, { lb: 0 }); // Var[] (x_0, ..., x_4)
|
|
79
|
+
const flags = m.bool(3); // BoolVar[] (p_0, p_1, p_2)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 1D Variable Dictionaries
|
|
83
|
+
Pass an array of keys to get a typed dictionary `Record<string, Var>` or `Record<string, BoolVar>`:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
const items = ["laptop", "phone", "tablet"];
|
|
87
|
+
|
|
88
|
+
// Record<"laptop" | "phone" | "tablet", Var>
|
|
89
|
+
const selected = m.binary(items);
|
|
90
|
+
|
|
91
|
+
const perms = m.bool(["read", "write", "execute"]);
|
|
92
|
+
// perms.read, perms.write, perms.execute
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 2D Variable Matrices & Grids
|
|
96
|
+
Each dimension follows a consistent rule: passing a numeric count (`number`) produces an `Array`, and passing a collection of keys produces a `Record`:
|
|
97
|
+
- `(rowsCount, colsCount)` $\rightarrow$ `Var[][]` (or `BoolVar[][]`)
|
|
98
|
+
- `(rowKeys, colKeys)` $\rightarrow$ `Record<R, Record<C, Var>>` (or `Record<R, Record<C, BoolVar>>`)
|
|
99
|
+
- `(rowsCount, colKeys)` $\rightarrow$ `Array<Record<C, Var>>`
|
|
100
|
+
- `(rowKeys, colsCount)` $\rightarrow$ `Record<R, Var[]>`
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
const factories = ["Tokyo", "Osaka"];
|
|
104
|
+
const stores = ["StoreA", "StoreB", "StoreC"];
|
|
105
|
+
|
|
106
|
+
// 1. Named keys x Named keys -> Record<string, Record<string, Var>>
|
|
107
|
+
const ship = m.continuous(factories, stores, { lb: 0 });
|
|
108
|
+
// ship.Tokyo.StoreA
|
|
109
|
+
|
|
110
|
+
// 2. Numeric count x Numeric count -> Var[][] (2D Array)
|
|
111
|
+
const board = m.binary(8, 8, { prefix: "queen" });
|
|
112
|
+
// board[0][0], board.map(...)
|
|
113
|
+
|
|
114
|
+
// 3. Mixed dimensions: Named keys x Numeric count -> Record<string, Var[]>
|
|
115
|
+
const schedule = m.binary(factories, 7, { prefix: "open_day" });
|
|
116
|
+
// schedule.Tokyo[0]
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Variable Options
|
|
122
|
+
|
|
123
|
+
| Option | Type | Default | Description |
|
|
124
|
+
| :--- | :--- | :--- | :--- |
|
|
125
|
+
| `lb` | `number` | `0` | Lower bound. Defaults to 0 (non-negative). |
|
|
126
|
+
| `ub` | `number` | `Infinity` (or `1` for binary) | Upper bound. |
|
|
127
|
+
| `type` | `"Continuous"` \| `"Integer"` \| `"Binary"` | Inferred from method | Underlying variable domain category. |
|
|
128
|
+
| `initialValue` | `number` | `undefined` | Optional primal solution hint (MIP start), kept distinct from solved `variable.value`. |
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Primal Initial Values (Warm-Start Hints)
|
|
133
|
+
|
|
134
|
+
When rebuilding a model across iterations with stable variable names, you can supply optional primal initial values (`initialValue`) on a subset or all numeric variables (`m.continuous`, `m.integer`, `m.binary`, or `v.initialValue = ...`):
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
const prevValues: Record<string, number | null> = { trucks: 8, open_warehouse: 1 };
|
|
138
|
+
|
|
139
|
+
const m2 = new Model("RebuiltMIP");
|
|
140
|
+
const trucks2 = m2.integer("trucks", {
|
|
141
|
+
lb: 0,
|
|
142
|
+
ub: 15,
|
|
143
|
+
initialValue: prevValues.trucks ?? undefined,
|
|
144
|
+
});
|
|
145
|
+
const openWarehouse2 = m2.binary("open_warehouse", {
|
|
146
|
+
initialValue: prevValues.open_warehouse ?? undefined,
|
|
147
|
+
});
|
|
148
|
+
const extraCapacity = m2.continuous("extra_capacity", { lb: 0 }); // Partial start: no initialValue needed
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
- **Hint vs. Hard Constraint**: `initialValue` is strictly an optional solver hint (passed to HiGHS via `setSolution`). It does **not** fix the variable, add a constraint, or guarantee either solver acceptance or a runtime speedup. It remains separate from `variable.value` (which is `null` until solved and populated with the solver's primal solution).
|
|
152
|
+
- **Validation & Tolerance (`1e-6`)**: Supplied values must be finite numbers within `[lb - 1e-6, ub + 1e-6]` (and within `1e-6` of an integer inside bounds for `"Integer"` and `"Binary"` variables). Partial starts are not required to satisfy every model constraint.
|
|
153
|
+
- **Solver Support**: Supported by **HiGHS** (`capabilities.supportsWarmStart === true`). Solvers without warm-start support (**GLPK**, **Z3**, or custom adapters without `supportsWarmStart: true`) reject `solve()` and `check()` with a descriptive error rather than silently ignoring `initialValue`.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Accessing Solution Values & Reduced Costs
|
|
158
|
+
|
|
159
|
+
After calling `await m.solve()` or `await m.check()`, assigned variable values (optimal or incumbent primal values from `solve()`, or a satisfying assignment from `check()`) are accessible via `variable.value` (returns `number | null` for `Var`, or `boolean | null` for `BoolVar`) or the bulk accessor `m.getValues()`:
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
await m.solve();
|
|
163
|
+
|
|
164
|
+
// Single variable primal value
|
|
165
|
+
console.log(`trucks: ${trucks.value}`);
|
|
166
|
+
|
|
167
|
+
// Reduced cost (LP sensitivity analysis — how much an objective coefficient must improve before a variable at its bound enters the basis)
|
|
168
|
+
console.log(`fuel reduced cost: ${fuel.reducedCost}`);
|
|
169
|
+
|
|
170
|
+
// Bulk extraction for 1D or 2D collections (works for both Var and BoolVar collections)
|
|
171
|
+
const selectedValues = m.getValues(selected);
|
|
172
|
+
// { laptop: 1, phone: 0, tablet: 1 }
|
|
173
|
+
|
|
174
|
+
const flowValues = m.getValues(ship);
|
|
175
|
+
// { Tokyo: { StoreA: 50, ... }, Osaka: { ... } }
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Looking Up Variables by Name
|
|
181
|
+
|
|
182
|
+
You can retrieve registered decision variables by name or inspect the total variable count on the model:
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
const v = m.getVarByName("trucks"); // Var | undefined
|
|
186
|
+
const b = m.getBoolVarByName("isNight"); // BoolVar | undefined
|
|
187
|
+
console.log("Total numeric variables:", m.numVars);
|
|
188
|
+
```
|