@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,525 @@
|
|
|
1
|
+
# WaSHI Custom Solver Developer Guide
|
|
2
|
+
|
|
3
|
+
This guide describes how to build, test, and distribute custom solver plugins for **WaSHI** (`@ohtaman/washi`).
|
|
4
|
+
|
|
5
|
+
WaSHI features a pluggable, decoupled solver architecture. Core `@ohtaman/washi` bundles only the zero-config HiGHS WASM engine and a Mock solver. Any third-party solver—whether implemented in WebAssembly, native C++, a Web Worker, or an external cloud REST/RPC backend—can be registered and used through the unified `Model` API.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Pluggable Solvers Architecture Overview
|
|
10
|
+
|
|
11
|
+
```mermaid
|
|
12
|
+
graph TD
|
|
13
|
+
A[WaSHI Model] --> B[Solver Registry Map Symbol.for]
|
|
14
|
+
B --> C[HiGHS WASM Engine Core]
|
|
15
|
+
B --> D[Mock Solver Core]
|
|
16
|
+
B --> E[GLPK Plugin @ohtaman/washi/solvers/glpk]
|
|
17
|
+
B --> F[Z3 SMT Plugin @ohtaman/washi/solvers/z3]
|
|
18
|
+
B --> G[Your Custom Solver Plugin]
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Key Architectural Characteristics
|
|
22
|
+
1. **Unified Global Registry**: The solver registry is backed by `Symbol.for("washi.solverRegistry")` on `globalThis`. This ensures that even across different ESM chunks, dynamic imports, or `<script>` tags, every bundle accesses the identical solver map.
|
|
23
|
+
2. **Behavioral Symmetry**: Node.js and browser environments share identical error messages, solver naming, and capability handling.
|
|
24
|
+
3. **Decoupled Bundling**: Core WaSHI does not import optional or custom plugins. A plugin only registers itself when explicitly imported.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 2. API Contracts & TypeScript Interfaces
|
|
29
|
+
|
|
30
|
+
All custom solvers must implement the `Solver` interface defined in `@ohtaman/washi`:
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import {
|
|
34
|
+
generateLpString,
|
|
35
|
+
buildLpSymbolMaps,
|
|
36
|
+
isLpExportResult,
|
|
37
|
+
sanitizeIdentifier,
|
|
38
|
+
type Solver,
|
|
39
|
+
type SolverOptions,
|
|
40
|
+
type SolverResult,
|
|
41
|
+
type SolverCheckResult,
|
|
42
|
+
type SolverCapabilities,
|
|
43
|
+
type SolveModelData,
|
|
44
|
+
type CheckModelData,
|
|
45
|
+
type LogicalConstraintItem,
|
|
46
|
+
type LpModelData,
|
|
47
|
+
type LpExportResult,
|
|
48
|
+
type LpSymbolMaps,
|
|
49
|
+
type SolverStatus,
|
|
50
|
+
type TerminationStatus,
|
|
51
|
+
type SolveProgressEvent,
|
|
52
|
+
} from "@ohtaman/washi";
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### The `Solver` Interface
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
export interface Solver {
|
|
59
|
+
/** Unique human-readable name of the solver (e.g. "GLPK", "AcmeCloudSolver") */
|
|
60
|
+
readonly name: string;
|
|
61
|
+
|
|
62
|
+
/** Solver version string (e.g. "1.2.0") */
|
|
63
|
+
readonly version?: string;
|
|
64
|
+
|
|
65
|
+
/** Capability flags declaring which problem classes the solver supports */
|
|
66
|
+
readonly capabilities?: SolverCapabilities;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Main solve method for LP, MIP, QP, and SMT optimization models.
|
|
70
|
+
*
|
|
71
|
+
* @param modelData Typed in-memory model structure (variables, constraints, objective, sense, boolVars, logicalConstraints)
|
|
72
|
+
* @param options Runtime execution options (timeLimit, mipGap, verbose, signal, onProgress, etc.)
|
|
73
|
+
*/
|
|
74
|
+
solve(
|
|
75
|
+
modelData: SolveModelData,
|
|
76
|
+
options?: SolverOptions
|
|
77
|
+
): Promise<SolverResult>;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Optional check method for Pure SAT / SMT or feasibility checking without an objective.
|
|
81
|
+
*
|
|
82
|
+
* @param modelData Objective-free in-memory model structure (variables, boolVars, constraints, logicalConstraints)
|
|
83
|
+
* @param options Runtime execution options
|
|
84
|
+
*/
|
|
85
|
+
check?(
|
|
86
|
+
modelData: CheckModelData,
|
|
87
|
+
options?: SolverOptions
|
|
88
|
+
): Promise<SolverCheckResult>;
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### `CheckModelData` & `SolveModelData`
|
|
93
|
+
|
|
94
|
+
`Model.solve()` and `Model.check()` pass the structured model directly to your adapter without serializing an LP string upfront:
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
export interface CheckModelData {
|
|
98
|
+
/** Model name */
|
|
99
|
+
name?: string;
|
|
100
|
+
/** Decision variables registered in the model */
|
|
101
|
+
variables: Var[];
|
|
102
|
+
/** Propositional Boolean variables registered in the model */
|
|
103
|
+
boolVars?: BoolVar[];
|
|
104
|
+
/** Linear arithmetic constraints registered in the model */
|
|
105
|
+
constraints: Constraint[];
|
|
106
|
+
/** Logical / SMT constraints registered in the model */
|
|
107
|
+
logicalConstraints?: LogicalConstraintItem[];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface SolveModelData extends CheckModelData {
|
|
111
|
+
/** Optimization direction ("Minimize" | "Maximize") */
|
|
112
|
+
sense: OptimizationSense;
|
|
113
|
+
/** Linear or quadratic objective expression */
|
|
114
|
+
objective: LinExpr | QuadExpr;
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### `SolverCapabilities`
|
|
119
|
+
|
|
120
|
+
Custom solvers declare their supported features using `capabilities`. Before dispatching `solve()` or `check()`, `Model` validates:
|
|
121
|
+
- `supportsQP === false` when the objective contains quadratic terms (`QuadExpr`),
|
|
122
|
+
- `supportsSMT !== true` when the model contains logical constraints (`m.logicalConstraints`), and
|
|
123
|
+
- `supportsWarmStart !== true` when any variable has a primal `initialValue` set.
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
export interface SolverCapabilities {
|
|
127
|
+
/** Supports mixed-integer programming (integer/binary variables) */
|
|
128
|
+
supportsMIP?: boolean;
|
|
129
|
+
|
|
130
|
+
/** Supports quadratic objectives (QP: x^T Q x + c^T x) */
|
|
131
|
+
supportsQP?: boolean;
|
|
132
|
+
|
|
133
|
+
/** Supports second-order cone or quadratic constraints */
|
|
134
|
+
supportsQCQP?: boolean;
|
|
135
|
+
|
|
136
|
+
/** Supports satisfiability modulo theories / boolean logic constraints */
|
|
137
|
+
supportsSMT?: boolean;
|
|
138
|
+
|
|
139
|
+
/** Supports extracting an unsatisfiable core (conflicting constraint subset) */
|
|
140
|
+
supportsUnsatCore?: boolean;
|
|
141
|
+
|
|
142
|
+
/** Supports primal initial values / MIP warm starts (Var.initialValue) */
|
|
143
|
+
supportsWarmStart?: boolean;
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### `SolverOptions` & `SolveProgressEvent`
|
|
148
|
+
|
|
149
|
+
Runtime options passed to `solve` and `check` include cancellation and progress hooks (note that the routing `solver` field from `SolveOptions` is stripped before options are passed to your adapter):
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
export interface SolverOptions {
|
|
153
|
+
/** Time limit in seconds */
|
|
154
|
+
timeLimit?: number;
|
|
155
|
+
|
|
156
|
+
/** Relative MIP optimality gap tolerance */
|
|
157
|
+
mipGap?: number;
|
|
158
|
+
|
|
159
|
+
/** Enable solver console logging */
|
|
160
|
+
verbose?: boolean;
|
|
161
|
+
|
|
162
|
+
/** Cooperative cancellation signal; when aborted, return terminationStatus: "Interrupted" */
|
|
163
|
+
signal?: AbortSignal;
|
|
164
|
+
|
|
165
|
+
/** Progress callback invoked with normalized SolveProgressEvent snapshots */
|
|
166
|
+
onProgress?: (progress: SolveProgressEvent) => void;
|
|
167
|
+
|
|
168
|
+
/** Additional solver-specific options */
|
|
169
|
+
[key: string]: any;
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Optional LP Serialization & Symbol-Mapping Helpers
|
|
174
|
+
|
|
175
|
+
Solvers that consume structured JS/WASM data (such as `GLPKSolver` and `Z3Solver`) work directly with `SolveModelData` / `CheckModelData`.
|
|
176
|
+
If your adapter targets a backend that requires CPLEX/HiGHS LP text or restricted identifier syntax, WaSHI exports opt-in helpers:
|
|
177
|
+
|
|
178
|
+
- `generateLpString(modelData): LpExportResult` — serializes `modelData` into a CPLEX LP format string (`lpString`) along with collision-safe symbol lookup maps (`nameToVar`, `nameToConstraint`).
|
|
179
|
+
- `buildLpSymbolMaps(modelData): LpSymbolMaps` — builds collision-safe sanitized symbol maps (`nameToVar`, `varToName`, `nameToConstraint`, `constraintToName`) without serializing an LP string.
|
|
180
|
+
- `isLpExportResult(value): value is LpExportResult` — runtime type guard for `LpExportResult` (useful when supporting legacy 3-argument `solve(modelData, lpResult, options)` overloads).
|
|
181
|
+
- `sanitizeIdentifier(raw, fallbackPrefix = "x", id = 0)` — sanitizes an arbitrary display name into a valid LP identifier.
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
export interface LpExportResult {
|
|
185
|
+
/** Standard CPLEX/HiGHS LP formatted string */
|
|
186
|
+
lpString: string;
|
|
187
|
+
|
|
188
|
+
/** Map translating sanitized LP variable identifiers back to WaSHI Var objects */
|
|
189
|
+
nameToVar: Map<string, Var>;
|
|
190
|
+
|
|
191
|
+
/** Map translating sanitized LP constraint identifiers back to WaSHI Constraint objects */
|
|
192
|
+
nameToConstraint: Map<string, Constraint>;
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### `SolverResult` Normalization & Identity Decoding
|
|
197
|
+
|
|
198
|
+
The `solve` method must return normalized results keyed by the model's canonical variable/constraint names (`v.name`, `bv.name`, `c.name`). Adapters that use `generateLpString(modelData)` or `buildLpSymbolMaps(modelData)` for an LP backend must translate sanitized LP identifiers back to canonical model names using `nameToVar` and `nameToConstraint`:
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
export interface SolverResult {
|
|
202
|
+
/** Standardized status string */
|
|
203
|
+
status: SolverStatus;
|
|
204
|
+
|
|
205
|
+
/** Exact termination reason independent of whether a feasible incumbent is available (defaults to status) */
|
|
206
|
+
terminationStatus?: TerminationStatus;
|
|
207
|
+
|
|
208
|
+
/** Whether a feasible primal solution / incumbent is available (inferred from status/values if omitted) */
|
|
209
|
+
hasPrimalSolution?: boolean;
|
|
210
|
+
|
|
211
|
+
/** Optimal or best objective value (null if no primal solution is available) */
|
|
212
|
+
objectiveValue: number | null;
|
|
213
|
+
|
|
214
|
+
/** Best proven dual/objective bound when provided by the solver */
|
|
215
|
+
objectiveBound?: number;
|
|
216
|
+
|
|
217
|
+
/** Relative MIP optimality gap when provided by the solver */
|
|
218
|
+
relGap?: number;
|
|
219
|
+
|
|
220
|
+
/** Alias for relGap: relative MIP optimality gap when provided by the solver */
|
|
221
|
+
mipGap?: number;
|
|
222
|
+
|
|
223
|
+
/** Absolute MIP optimality gap |objectiveValue - objectiveBound| (computed automatically when both are finite) */
|
|
224
|
+
absGap?: number;
|
|
225
|
+
|
|
226
|
+
/** Variable primal values keyed by canonical variable name (non-finite values are ignored) */
|
|
227
|
+
values: Record<string, number>;
|
|
228
|
+
|
|
229
|
+
/** Propositional Boolean variable assignments keyed by BoolVar name (optional, SMT/SAT) */
|
|
230
|
+
boolValues?: Record<string, boolean>;
|
|
231
|
+
|
|
232
|
+
/** Variable reduced costs (optional, LP only) */
|
|
233
|
+
reducedCosts?: Record<string, number>;
|
|
234
|
+
|
|
235
|
+
/** Constraint dual values / shadow prices (optional, LP only) */
|
|
236
|
+
duals?: Record<string, number>;
|
|
237
|
+
|
|
238
|
+
/** Constraint slack values (optional) */
|
|
239
|
+
slacks?: Record<string, number>;
|
|
240
|
+
|
|
241
|
+
/** Solve execution duration in seconds */
|
|
242
|
+
runTime?: number;
|
|
243
|
+
|
|
244
|
+
/** Diagnostic solver logs / text outputs */
|
|
245
|
+
logs?: string[];
|
|
246
|
+
|
|
247
|
+
/** Raw solver response object for inspection */
|
|
248
|
+
raw?: any;
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
#### Standardized `SolverStatus` Values
|
|
253
|
+
- `"Optimal"`: Global optimal solution found (for MIP within tolerance, for LP proven optimal).
|
|
254
|
+
- `"Feasible"`: Feasible solution found, but optimality not yet proven (e.g., hit time limit with an incumbent).
|
|
255
|
+
- `"Infeasible"`: Model is proven mathematically infeasible.
|
|
256
|
+
- `"Unbounded"`: Model objective is unbounded.
|
|
257
|
+
- `"TimeLimit"`: Solver terminated due to `timeLimit` without proof of optimality.
|
|
258
|
+
- `"IterationLimit"`: Solver terminated due to iteration limit.
|
|
259
|
+
- `"Sat"` / `"Unsat"`: Satisfiable / unsatisfiable status returned by `check()` or SMT solvers.
|
|
260
|
+
- `"Error"` / `"NotSolved"` / `"Unknown"`: Error, not yet solved, or unrecognized status. **Never convert Unknown/Error to Optimal.**
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## 3. Step-by-Step Implementation Tutorials
|
|
265
|
+
|
|
266
|
+
### Tutorial A: In-House Cloud REST Solver Adapter
|
|
267
|
+
|
|
268
|
+
Here is a complete, production-ready custom solver connecting to an HTTP microservice that accepts LP format strings on demand via `generateLpString(modelData)`:
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
import {
|
|
272
|
+
generateLpString,
|
|
273
|
+
registerSolver,
|
|
274
|
+
type Solver,
|
|
275
|
+
type SolverOptions,
|
|
276
|
+
type SolverResult,
|
|
277
|
+
type SolverStatus,
|
|
278
|
+
type SolveModelData,
|
|
279
|
+
} from "@ohtaman/washi";
|
|
280
|
+
|
|
281
|
+
export interface CloudSolverConfig extends SolverOptions {
|
|
282
|
+
endpoint?: string;
|
|
283
|
+
apiKey?: string;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export class CloudSolver implements Solver {
|
|
287
|
+
public readonly name = "CloudSolver";
|
|
288
|
+
public readonly version = "1.0.0";
|
|
289
|
+
public readonly capabilities = {
|
|
290
|
+
supportsMIP: true,
|
|
291
|
+
supportsQP: false,
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
private endpoint: string;
|
|
295
|
+
private apiKey: string;
|
|
296
|
+
|
|
297
|
+
constructor(config: CloudSolverConfig = {}) {
|
|
298
|
+
this.endpoint = config.endpoint || "https://solver.mycompany.internal/v1/solve";
|
|
299
|
+
this.apiKey = config.apiKey || "";
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async solve(
|
|
303
|
+
modelData: SolveModelData,
|
|
304
|
+
options: SolverOptions = {}
|
|
305
|
+
): Promise<SolverResult> {
|
|
306
|
+
const startTime = performance.now();
|
|
307
|
+
const { lpString, nameToVar, nameToConstraint } = generateLpString(modelData);
|
|
308
|
+
|
|
309
|
+
let payload: any;
|
|
310
|
+
try {
|
|
311
|
+
const response = await fetch(this.endpoint, {
|
|
312
|
+
method: "POST",
|
|
313
|
+
headers: {
|
|
314
|
+
"Content-Type": "application/json",
|
|
315
|
+
...(this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}),
|
|
316
|
+
},
|
|
317
|
+
body: JSON.stringify({
|
|
318
|
+
lp: lpString,
|
|
319
|
+
timeLimit: options.timeLimit,
|
|
320
|
+
mipGap: options.mipGap,
|
|
321
|
+
}),
|
|
322
|
+
signal: options.signal,
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
if (!response.ok) {
|
|
326
|
+
throw new Error(`[CloudSolver] Server returned HTTP ${response.status}: ${await response.text()}`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
payload = await response.json();
|
|
330
|
+
} catch (err: any) {
|
|
331
|
+
if (options.signal?.aborted || err?.name === "AbortError") {
|
|
332
|
+
return {
|
|
333
|
+
status: "Unknown",
|
|
334
|
+
terminationStatus: "Interrupted",
|
|
335
|
+
objectiveValue: null,
|
|
336
|
+
values: {},
|
|
337
|
+
runTime: (performance.now() - startTime) / 1000,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
throw err;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const runTime = (performance.now() - startTime) / 1000;
|
|
344
|
+
|
|
345
|
+
const values: Record<string, number> = Object.create(null);
|
|
346
|
+
for (const [k, val] of Object.entries((payload.values || {}) as Record<string, number>)) {
|
|
347
|
+
values[nameToVar.get(k)?.name ?? k] = val;
|
|
348
|
+
}
|
|
349
|
+
const duals: Record<string, number> = Object.create(null);
|
|
350
|
+
for (const [k, val] of Object.entries((payload.duals || {}) as Record<string, number>)) {
|
|
351
|
+
duals[nameToConstraint.get(k)?.name ?? k] = val;
|
|
352
|
+
}
|
|
353
|
+
const slacks: Record<string, number> = Object.create(null);
|
|
354
|
+
for (const [k, val] of Object.entries((payload.slacks || {}) as Record<string, number>)) {
|
|
355
|
+
slacks[nameToConstraint.get(k)?.name ?? k] = val;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const statusMap: Record<string, SolverStatus> = {
|
|
359
|
+
OPTIMAL: "Optimal",
|
|
360
|
+
FEASIBLE: "Feasible",
|
|
361
|
+
INFEASIBLE: "Infeasible",
|
|
362
|
+
UNBOUNDED: "Unbounded",
|
|
363
|
+
TIME_LIMIT: "TimeLimit",
|
|
364
|
+
TIMELIMIT: "TimeLimit",
|
|
365
|
+
ITERATION_LIMIT: "IterationLimit",
|
|
366
|
+
ERROR: "Error",
|
|
367
|
+
NOT_SOLVED: "NotSolved",
|
|
368
|
+
SAT: "Sat",
|
|
369
|
+
UNSAT: "Unsat",
|
|
370
|
+
};
|
|
371
|
+
const rawStatus: SolverStatus = statusMap[String(payload.status).toUpperCase()] ?? "Unknown";
|
|
372
|
+
const hasIncumbent =
|
|
373
|
+
(typeof payload.objectiveValue === "number" && Number.isFinite(payload.objectiveValue)) ||
|
|
374
|
+
Object.values(values).some((val) => typeof val === "number" && Number.isFinite(val));
|
|
375
|
+
const isLimitStop = rawStatus === "TimeLimit" || rawStatus === "IterationLimit";
|
|
376
|
+
const status: SolverStatus = isLimitStop && hasIncumbent ? "Feasible" : rawStatus;
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
status,
|
|
380
|
+
terminationStatus: rawStatus,
|
|
381
|
+
objectiveValue: payload.objectiveValue ?? null,
|
|
382
|
+
values,
|
|
383
|
+
duals,
|
|
384
|
+
slacks,
|
|
385
|
+
runTime: payload.solveDuration || runTime,
|
|
386
|
+
logs: payload.logs || [],
|
|
387
|
+
raw: payload,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Auto-register so `import "my-cloud-solver"` enables `solver: "cloud"`
|
|
393
|
+
export function registerCloudSolver(): void {
|
|
394
|
+
registerSolver("cloud", (opts) => new CloudSolver(opts));
|
|
395
|
+
}
|
|
396
|
+
registerCloudSolver();
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
### Tutorial B: Registering and Using the Custom Solver
|
|
402
|
+
|
|
403
|
+
Once implemented, the custom solver can be selected in three different ways:
|
|
404
|
+
|
|
405
|
+
#### Option 1: By Registered Name String
|
|
406
|
+
```typescript
|
|
407
|
+
import { Model } from "@ohtaman/washi";
|
|
408
|
+
import "./cloud-solver.js"; // Auto-registers "cloud"
|
|
409
|
+
|
|
410
|
+
const model = new Model({ name: "SupplyChain", solver: "cloud" });
|
|
411
|
+
// Or model.solve("cloud")
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
#### Option 2: By Passing the Solver Instance Directly
|
|
415
|
+
```typescript
|
|
416
|
+
import { Model } from "@ohtaman/washi";
|
|
417
|
+
import { CloudSolver } from "./cloud-solver.js";
|
|
418
|
+
|
|
419
|
+
const customInstance = new CloudSolver({
|
|
420
|
+
endpoint: "https://dedicated-gpu-cluster.internal/solve",
|
|
421
|
+
apiKey: "secret-token",
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
const model = new Model({ name: "FacilityLocation", solver: customInstance });
|
|
425
|
+
const status = await model.solve();
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
#### Option 3: Setting Global Default Solver
|
|
429
|
+
```typescript
|
|
430
|
+
import { setDefaultSolver, Model } from "@ohtaman/washi";
|
|
431
|
+
import { CloudSolver } from "./cloud-solver.js";
|
|
432
|
+
|
|
433
|
+
setDefaultSolver(() => new CloudSolver());
|
|
434
|
+
|
|
435
|
+
// All subsequent models use CloudSolver by default
|
|
436
|
+
const model = new Model("DefaultedModel");
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
## 4. Distribution: Packaging as a Plugin
|
|
442
|
+
|
|
443
|
+
To publish your solver as an independent npm package (e.g. `@my-org/washi-solver-cplex`):
|
|
444
|
+
|
|
445
|
+
### 1. `package.json` Exports Structure
|
|
446
|
+
```json
|
|
447
|
+
{
|
|
448
|
+
"name": "@my-org/washi-solver-cplex",
|
|
449
|
+
"version": "1.0.0",
|
|
450
|
+
"type": "module",
|
|
451
|
+
"main": "./dist/lib/index.js",
|
|
452
|
+
"types": "./dist/types/index.d.ts",
|
|
453
|
+
"exports": {
|
|
454
|
+
".": {
|
|
455
|
+
"types": "./dist/types/index.d.ts",
|
|
456
|
+
"import": "./dist/lib/index.js",
|
|
457
|
+
"default": "./dist/lib/index.js"
|
|
458
|
+
},
|
|
459
|
+
"./browser": {
|
|
460
|
+
"types": "./dist/types/index.d.ts",
|
|
461
|
+
"import": "./dist/browser/index.js",
|
|
462
|
+
"default": "./dist/browser/index.js"
|
|
463
|
+
},
|
|
464
|
+
"./browser/index.min.js": "./dist/browser/index.min.js"
|
|
465
|
+
},
|
|
466
|
+
"peerDependencies": {
|
|
467
|
+
"@ohtaman/washi": ">=0.1.0"
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
### 2. Browser IIFE Build
|
|
473
|
+
When building the browser standalone script (`.min.js`), configure your bundler (Vite or Rollup) to:
|
|
474
|
+
- Output format: `iife`
|
|
475
|
+
- Global name: `WashiCplex`
|
|
476
|
+
- Rely on `Symbol.for("washi.solverRegistry")` so that `<script src="washi.min.js"></script><script src="washi-cplex.min.js"></script>` automatically connects.
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
## 5. Testing Your Custom Solver
|
|
481
|
+
|
|
482
|
+
You can test your solver using Vitest or Jest without bundling:
|
|
483
|
+
|
|
484
|
+
```typescript
|
|
485
|
+
import { describe, it, expect, vi } from "vitest";
|
|
486
|
+
import { Model, expr, maximize, registerSolver, listAvailableSolvers } from "@ohtaman/washi";
|
|
487
|
+
import { CloudSolver } from "./cloud-solver.js";
|
|
488
|
+
|
|
489
|
+
describe("Custom Solver Integration", () => {
|
|
490
|
+
it("registers in available solvers list", () => {
|
|
491
|
+
registerSolver("cloud-test", () => new CloudSolver());
|
|
492
|
+
expect(listAvailableSolvers()).toContain("cloud-test");
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it("propagates primal values and duals into WaSHI Model", async () => {
|
|
496
|
+
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(
|
|
497
|
+
new Response(
|
|
498
|
+
JSON.stringify({
|
|
499
|
+
status: "OPTIMAL",
|
|
500
|
+
objectiveValue: 34,
|
|
501
|
+
values: { x: 2, y: 10 },
|
|
502
|
+
duals: { budget: 2 },
|
|
503
|
+
}),
|
|
504
|
+
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
505
|
+
)
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
try {
|
|
509
|
+
const model = new Model({ solver: new CloudSolver() });
|
|
510
|
+
const x = model.continuous("x", { lb: 0, ub: 10 });
|
|
511
|
+
const y = model.continuous("y", { lb: 0, ub: 10 });
|
|
512
|
+
model.objective(maximize(expr`2 * ${x} + 3 * ${y}`));
|
|
513
|
+
model.constraint(expr`${x} + ${y} <= 12`, "budget");
|
|
514
|
+
|
|
515
|
+
const status = await model.solve();
|
|
516
|
+
expect(status).toBe("Optimal");
|
|
517
|
+
expect(x.value).toBe(2);
|
|
518
|
+
const c1 = model.getConstraintByName("budget");
|
|
519
|
+
expect(c1?.dual).toBe(2);
|
|
520
|
+
} finally {
|
|
521
|
+
fetchSpy.mockRestore();
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
```
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# GLPK Solver Engine
|
|
2
|
+
|
|
3
|
+
The **GNU Linear Programming Kit (GLPK)** is a long-standing, robust simplex and interior-point solver package intended for solving large-scale linear programming (LP) and mixed integer programming (MIP).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
GLPK is provided as an optional peer dependency via `glpk.js`:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install glpk.js
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Licensing Consideration (GPL-3.0)
|
|
18
|
+
|
|
19
|
+
GLPK is licensed under the **GNU General Public License v3.0 (GPL-3.0)**.
|
|
20
|
+
|
|
21
|
+
- Because WaSHI is MIT-licensed, `glpk.js` is **never bundled statically** in the core package.
|
|
22
|
+
- It is dynamically imported only if you explicitly register or invoke `@ohtaman/washi/solvers/glpk`.
|
|
23
|
+
- Ensure your project's distribution requirements comply with the GPL-3.0 license when shipping products that bundle GLPK.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Basic Usage
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { Model, expr, maximize } from "@ohtaman/washi";
|
|
31
|
+
import "@ohtaman/washi/solvers/glpk";
|
|
32
|
+
|
|
33
|
+
const model = new Model({ solver: "glpk" });
|
|
34
|
+
|
|
35
|
+
const x = model.continuous("x", { lb: 0, ub: 10 });
|
|
36
|
+
const y = model.continuous("y", { lb: 0, ub: 20 });
|
|
37
|
+
|
|
38
|
+
model.objective(maximize(expr`3 * ${x} + 2 * ${y}`));
|
|
39
|
+
model.constraint(expr`${x} + ${y} <= 15`, "cap");
|
|
40
|
+
|
|
41
|
+
await model.solve();
|
|
42
|
+
|
|
43
|
+
console.log("GLPK Status:", model.status);
|
|
44
|
+
console.log("Objective:", model.objectiveValue);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Result Semantics & Status Mapping
|
|
50
|
+
|
|
51
|
+
### Solver Statuses
|
|
52
|
+
GLPK return codes are mapped strictly to WaSHI `SolverStatus`:
|
|
53
|
+
- `GLP_OPT` (5) $\to$ `"Optimal"`
|
|
54
|
+
- `GLP_FEAS` (2) $\to$ `"Feasible"` (feasible MIP incumbent found)
|
|
55
|
+
- `GLP_INFEAS` (3) / `GLP_NOFEAS` (4) $\to$ `"Infeasible"`
|
|
56
|
+
- `GLP_UNBND` (6) $\to$ `"Unbounded"`
|
|
57
|
+
- `GLP_UNDEF` (1) and unrecognized/missing statuses $\to$ `"Unknown"` (never misclassified as `"Optimal"`)
|
|
58
|
+
|
|
59
|
+
### Solution Availability
|
|
60
|
+
- Variable values (`model.values`, `var.value`) and objective values (`model.objectiveValue`) are exposed **only** when a valid primal solution is available (`"Optimal"` or `"Feasible"`).
|
|
61
|
+
- For unsuccessful or non-optimal solves (`Infeasible`, `Unbounded`, `Unknown`, `Error`), `model.objectiveValue` is `null` and variable values remain `null` without fabricating fake all-zero solutions.
|
|
62
|
+
|
|
63
|
+
### Differences Between LP and MIP
|
|
64
|
+
- **Linear Programming (LP)**: Solved using the simplex algorithm. Dual values (shadow prices via `constraint.dual`) and slacks are populated for optimal solutions.
|
|
65
|
+
- **Mixed-Integer Programming (MIP)**: Solved using the branch-and-cut algorithm. Produces integer-feasible primal variable solutions; dual values are mathematically undefined for discrete optimization and omitted.
|
|
66
|
+
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# HiGHS Solver Engine
|
|
2
|
+
|
|
3
|
+
**HiGHS** is high-performance mathematical optimization software developed at the University of Edinburgh. It is widely recognized as the state-of-the-art open-source solver for Linear Programming (LP), Mixed-Integer Programming (MIP), and Convex Quadratic Programming (QP).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Key Features in WaSHI
|
|
8
|
+
|
|
9
|
+
- **Built-in & Zero-Config**: Bundled directly with `@ohtaman/washi`. No separate peer dependency or compilation required.
|
|
10
|
+
- **Permissive MIT License**: 100% commercially friendly.
|
|
11
|
+
- **Web Worker Multithreading**: Automatically offloads intensive solving to background Web Workers in browser environments to avoid freezing the UI thread.
|
|
12
|
+
- **Warm-Start Support (`initialValue`)**: Supports passing variable primal initial value hints (`initialValue`) to HiGHS `setSolution` for warm starting.
|
|
13
|
+
- **Fast WASM Execution**: Emscripten-compiled 32-bit WASM runs at near-native speed.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Supported Problem Classes
|
|
18
|
+
|
|
19
|
+
### 1. Linear Programming (LP)
|
|
20
|
+
Solves continuous problems via Primal and Dual Simplex algorithms or Interior Point (IPM) methods. After solving, you can inspect constraint shadow prices (`c.dual`), slacks (`c.slack`), and variable reduced costs (`v.reducedCost`):
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { Model, expr, maximize } from "@ohtaman/washi";
|
|
24
|
+
|
|
25
|
+
const model = new Model({ solver: "highs" });
|
|
26
|
+
const x = model.continuous("x", { lb: 0 });
|
|
27
|
+
const y = model.continuous("y", { lb: 0 });
|
|
28
|
+
model.objective(maximize(expr`10 * ${x} + 6 * ${y}`));
|
|
29
|
+
model.constraint(expr`${x} + ${y} <= 100`, "cap");
|
|
30
|
+
await model.solve();
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 2. Mixed-Integer Linear Programming (MIP)
|
|
34
|
+
Solves discrete problems using Branch-and-Bound and cutting plane techniques:
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
const count = model.integer("count", { lb: 0, ub: 50 });
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 3. Convex Quadratic Programming (QP)
|
|
41
|
+
Solves quadratic objective functions subject to linear constraints:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
// Minimize risk/variance uniformly using expr:
|
|
45
|
+
model.objective(minimize(expr`0.5 * ${x}^2 + 0.5 * ${y}^2`));
|
|
46
|
+
|
|
47
|
+
// Or programmatically with .mul():
|
|
48
|
+
// const quadObj = x.mul(x).mul(0.5).add(y.mul(y).mul(0.5));
|
|
49
|
+
// model.objective(minimize(quadObj));
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Solver Options
|
|
55
|
+
|
|
56
|
+
Pass solver options into `model.solve()`:
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
const controller = new AbortController();
|
|
60
|
+
|
|
61
|
+
await model.solve({
|
|
62
|
+
timeLimit: 60.0, // Maximum solve time in seconds
|
|
63
|
+
mipGap: 0.001, // Terminate when within 0.1% of global optimality (alias: relGap)
|
|
64
|
+
absGap: 1.0, // Absolute MIP optimality gap tolerance
|
|
65
|
+
presolve: "on", // Presolve control ("on" | "off" | "choose")
|
|
66
|
+
outputFlag: true, // Output internal simplex / branch-and-bound logs (output_flag)
|
|
67
|
+
signal: controller.signal,
|
|
68
|
+
onProgress: (ev) => {
|
|
69
|
+
console.log(`Elapsed: ${ev.elapsedTime.toFixed(1)}s, Best: ${ev.objectiveValue}`);
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Worker & WASM Configuration in Browsers
|
|
77
|
+
|
|
78
|
+
By default in browsers, WaSHI spawns a dedicated Web Worker to execute HiGHS in the background so UI rendering stays responsive and `onProgress` / `AbortSignal` work seamlessly.
|
|
79
|
+
|
|
80
|
+
If you need to run synchronously on the main thread (or in environments where Web Workers are prohibited), pass `useWorker: false` to `model.solve()` or instantiate `HighsSolver`:
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import { Model, HighsSolver } from "@ohtaman/washi";
|
|
84
|
+
|
|
85
|
+
// Option 1: Per solve call
|
|
86
|
+
await model.solve({ useWorker: false });
|
|
87
|
+
|
|
88
|
+
// Option 2: When instantiating the solver
|
|
89
|
+
const syncModel = new Model({
|
|
90
|
+
solver: new HighsSolver({ useWorker: false }),
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|