@areb0s/scip.js 1.0.5
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.md +276 -0
- package/dist/basic.html +445 -0
- package/dist/index.mjs +73 -0
- package/dist/scip-core.js +15 -0
- package/dist/scip-worker-client.js +167 -0
- package/dist/scip-worker.js +81 -0
- package/dist/scip-wrapper.js +429 -0
- package/dist/scip.js +3223 -0
- package/dist/scip.js.map +7 -0
- package/dist/scip.min.js +43 -0
- package/dist/scip.min.js.map +7 -0
- package/dist/scip.wasm +0 -0
- package/dist/test-browser.html +118 -0
- package/dist/test-worker.html +161 -0
- package/dist/types.d.ts +230 -0
- package/package.json +66 -0
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCIP.js - SCIP Optimization Solver for JavaScript
|
|
3
|
+
* High-level wrapper around SCIP WASM
|
|
4
|
+
*
|
|
5
|
+
* Supports: LP, MIP, MINLP (Mixed Integer Nonlinear Programming)
|
|
6
|
+
*
|
|
7
|
+
* Usage in Worker (like OpenCV):
|
|
8
|
+
* // Set base URL before loading script
|
|
9
|
+
* self.SCIP_BASE_URL = 'https://cdn.jsdelivr.net/gh/user/scip.js@v1.0.0/dist/';
|
|
10
|
+
*
|
|
11
|
+
* // Load and execute script
|
|
12
|
+
* const response = await fetch(SCIP_BASE_URL + 'scip.min.js');
|
|
13
|
+
* new Function(await response.text())();
|
|
14
|
+
*
|
|
15
|
+
* // Wait for initialization
|
|
16
|
+
* await self.SCIP.ready;
|
|
17
|
+
*
|
|
18
|
+
* // Use
|
|
19
|
+
* const result = await self.SCIP.solve(`...`);
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
let scipModule = null;
|
|
23
|
+
let isInitialized = false;
|
|
24
|
+
let initPromise = null;
|
|
25
|
+
let readyResolve = null;
|
|
26
|
+
let readyReject = null;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Ready promise - resolves when SCIP is initialized
|
|
30
|
+
* Usage: await SCIP.ready;
|
|
31
|
+
*/
|
|
32
|
+
export const ready = new Promise((resolve, reject) => {
|
|
33
|
+
readyResolve = resolve;
|
|
34
|
+
readyReject = reject;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Default CDN base URL for WASM files
|
|
39
|
+
*/
|
|
40
|
+
const DEFAULT_CDN_BASE = 'https://cdn.jsdelivr.net/gh/areb0s/scip.js/dist/';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get base URL from global SCIP_BASE_URL or default CDN
|
|
44
|
+
*/
|
|
45
|
+
function getBaseUrl() {
|
|
46
|
+
// Safe check for global scope (works in browser, worker, and SSR)
|
|
47
|
+
const globalScope = (typeof globalThis !== 'undefined' && globalThis) ||
|
|
48
|
+
(typeof self !== 'undefined' && self) ||
|
|
49
|
+
(typeof window !== 'undefined' && window) ||
|
|
50
|
+
{};
|
|
51
|
+
|
|
52
|
+
// Check for explicit SCIP_BASE_URL
|
|
53
|
+
if (globalScope.SCIP_BASE_URL) {
|
|
54
|
+
return globalScope.SCIP_BASE_URL;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check for __importMetaUrl (set by bundler)
|
|
58
|
+
if (typeof __importMetaUrl !== 'undefined' && __importMetaUrl && !__importMetaUrl.startsWith('blob:')) {
|
|
59
|
+
return __importMetaUrl.substring(0, __importMetaUrl.lastIndexOf('/') + 1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Default to CDN
|
|
63
|
+
return DEFAULT_CDN_BASE;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Solution status enum
|
|
68
|
+
*/
|
|
69
|
+
export const Status = {
|
|
70
|
+
OPTIMAL: 'optimal',
|
|
71
|
+
INFEASIBLE: 'infeasible',
|
|
72
|
+
UNBOUNDED: 'unbounded',
|
|
73
|
+
TIME_LIMIT: 'timelimit',
|
|
74
|
+
UNKNOWN: 'unknown',
|
|
75
|
+
ERROR: 'error'
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Parse SCIP status from output
|
|
80
|
+
*/
|
|
81
|
+
function parseStatus(output) {
|
|
82
|
+
if (output.includes('optimal solution found')) return Status.OPTIMAL;
|
|
83
|
+
if (output.includes('problem is infeasible')) return Status.INFEASIBLE;
|
|
84
|
+
if (output.includes('problem is unbounded')) return Status.UNBOUNDED;
|
|
85
|
+
if (output.includes('time limit reached')) return Status.TIME_LIMIT;
|
|
86
|
+
return Status.UNKNOWN;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Parse solution values from SCIP output
|
|
91
|
+
*/
|
|
92
|
+
function parseSolution(output) {
|
|
93
|
+
const variables = {};
|
|
94
|
+
const objective = { value: null, sense: null };
|
|
95
|
+
|
|
96
|
+
// Parse objective value
|
|
97
|
+
const objMatch = output.match(/objective value:\s*([\d.e+-]+)/i);
|
|
98
|
+
if (objMatch) {
|
|
99
|
+
objective.value = parseFloat(objMatch[1]);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Parse variable values from solution display
|
|
103
|
+
// Format: variable_name value (obj:coef)
|
|
104
|
+
const varRegex = /^(\w+)\s+([\d.e+-]+)/gm;
|
|
105
|
+
let match;
|
|
106
|
+
|
|
107
|
+
// Look for solution section
|
|
108
|
+
const solSection = output.split('solution:')[1] || output;
|
|
109
|
+
|
|
110
|
+
while ((match = varRegex.exec(solSection)) !== null) {
|
|
111
|
+
const name = match[1];
|
|
112
|
+
const value = parseFloat(match[2]);
|
|
113
|
+
if (!isNaN(value) && name !== 'objective') {
|
|
114
|
+
variables[name] = value;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { variables, objective };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Parse statistics from SCIP output
|
|
123
|
+
*/
|
|
124
|
+
function parseStatistics(output) {
|
|
125
|
+
const stats = {
|
|
126
|
+
solvingTime: null,
|
|
127
|
+
nodes: null,
|
|
128
|
+
iterations: null,
|
|
129
|
+
gap: null
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const timeMatch = output.match(/Solving Time \(sec\)\s*:\s*([\d.]+)/);
|
|
133
|
+
if (timeMatch) stats.solvingTime = parseFloat(timeMatch[1]);
|
|
134
|
+
|
|
135
|
+
const nodesMatch = output.match(/Nodes\s*:\s*(\d+)/);
|
|
136
|
+
if (nodesMatch) stats.nodes = parseInt(nodesMatch[1]);
|
|
137
|
+
|
|
138
|
+
const iterMatch = output.match(/LP Iterations\s*:\s*(\d+)/);
|
|
139
|
+
if (iterMatch) stats.iterations = parseInt(iterMatch[1]);
|
|
140
|
+
|
|
141
|
+
const gapMatch = output.match(/Gap\s*:\s*([\d.]+)\s*%/);
|
|
142
|
+
if (gapMatch) stats.gap = parseFloat(gapMatch[1]);
|
|
143
|
+
|
|
144
|
+
return stats;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Initialize SCIP WASM module
|
|
149
|
+
* @param {Object} options - Initialization options
|
|
150
|
+
* @param {string} options.wasmPath - Path to scip.wasm file
|
|
151
|
+
* @returns {Promise<void>}
|
|
152
|
+
*/
|
|
153
|
+
export async function init(options = {}) {
|
|
154
|
+
if (isInitialized) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (initPromise) {
|
|
158
|
+
return initPromise;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
initPromise = (async () => {
|
|
162
|
+
try {
|
|
163
|
+
// Auto-detect wasmPath from SCIP_BASE_URL or script location
|
|
164
|
+
const baseUrl = getBaseUrl();
|
|
165
|
+
const wasmPath = options.wasmPath || (baseUrl + 'scip.wasm');
|
|
166
|
+
|
|
167
|
+
// Dynamic import of the Emscripten-generated module
|
|
168
|
+
const createSCIP = (await import('./scip-core.js')).default;
|
|
169
|
+
|
|
170
|
+
scipModule = await createSCIP({
|
|
171
|
+
locateFile: (path) => {
|
|
172
|
+
if (path.endsWith('.wasm')) {
|
|
173
|
+
return wasmPath;
|
|
174
|
+
}
|
|
175
|
+
return path;
|
|
176
|
+
},
|
|
177
|
+
// Capture stdout/stderr from Emscripten
|
|
178
|
+
print: (text) => {
|
|
179
|
+
if (scipModule && scipModule.onStdout) {
|
|
180
|
+
scipModule.onStdout(text);
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
printErr: (text) => {
|
|
184
|
+
if (scipModule && scipModule.onStderr) {
|
|
185
|
+
scipModule.onStderr(text);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Create directories for problems, solutions, settings
|
|
191
|
+
if (scipModule.FS) {
|
|
192
|
+
try { scipModule.FS.mkdir('/problems'); } catch (e) { /* exists */ }
|
|
193
|
+
try { scipModule.FS.mkdir('/solutions'); } catch (e) { /* exists */ }
|
|
194
|
+
try { scipModule.FS.mkdir('/settings'); } catch (e) { /* exists */ }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
isInitialized = true;
|
|
198
|
+
|
|
199
|
+
// Resolve ready promise
|
|
200
|
+
if (readyResolve) {
|
|
201
|
+
readyResolve();
|
|
202
|
+
}
|
|
203
|
+
} catch (error) {
|
|
204
|
+
if (readyReject) {
|
|
205
|
+
readyReject(error);
|
|
206
|
+
}
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
})();
|
|
210
|
+
|
|
211
|
+
return initPromise;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Check if SCIP is initialized
|
|
216
|
+
* @returns {boolean}
|
|
217
|
+
*/
|
|
218
|
+
export function isReady() {
|
|
219
|
+
return isInitialized;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Solve an optimization problem
|
|
224
|
+
*
|
|
225
|
+
* @param {string} problem - Problem in LP, MPS, or ZIMPL format
|
|
226
|
+
* @param {Object} options - Solver options
|
|
227
|
+
* @param {string} options.format - Input format: 'lp', 'mps', 'zpl' (default: 'lp')
|
|
228
|
+
* @param {number} options.timeLimit - Time limit in seconds
|
|
229
|
+
* @param {number} options.gap - Relative gap for MIP (e.g., 0.01 for 1%)
|
|
230
|
+
* @param {boolean} options.verbose - Enable verbose output
|
|
231
|
+
* @param {Object} options.parameters - Additional SCIP parameters
|
|
232
|
+
* @returns {Promise<Object>} Solution object
|
|
233
|
+
*
|
|
234
|
+
* @example
|
|
235
|
+
* const result = await solve(`
|
|
236
|
+
* Minimize obj: x + 2 y
|
|
237
|
+
* Subject To
|
|
238
|
+
* c1: x + y >= 1
|
|
239
|
+
* Bounds
|
|
240
|
+
* 0 <= x <= 10
|
|
241
|
+
* 0 <= y <= 10
|
|
242
|
+
* End
|
|
243
|
+
* `);
|
|
244
|
+
*
|
|
245
|
+
* console.log(result.status); // 'optimal'
|
|
246
|
+
* console.log(result.objective); // 1.0
|
|
247
|
+
* console.log(result.variables); // { x: 1, y: 0 }
|
|
248
|
+
*/
|
|
249
|
+
export async function solve(problem, options = {}) {
|
|
250
|
+
if (!isInitialized) {
|
|
251
|
+
await init(options);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const {
|
|
255
|
+
format = 'lp',
|
|
256
|
+
timeLimit = 3600,
|
|
257
|
+
gap = null,
|
|
258
|
+
verbose = false,
|
|
259
|
+
parameters = {}
|
|
260
|
+
} = options;
|
|
261
|
+
|
|
262
|
+
// Capture output
|
|
263
|
+
let stdout = '';
|
|
264
|
+
let stderr = '';
|
|
265
|
+
|
|
266
|
+
scipModule.onStdout = (text) => {
|
|
267
|
+
stdout += text + '\n';
|
|
268
|
+
if (verbose) console.log('[SCIP]', text);
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
scipModule.onStderr = (text) => {
|
|
272
|
+
stderr += text + '\n';
|
|
273
|
+
if (verbose) console.error('[SCIP Error]', text);
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
// Determine file extension
|
|
278
|
+
const ext = format === 'mps' ? 'mps' : format === 'zpl' ? 'zpl' : 'lp';
|
|
279
|
+
const problemFile = `/problems/problem.${ext}`;
|
|
280
|
+
const solutionFile = '/solutions/solution.sol';
|
|
281
|
+
|
|
282
|
+
// Write problem to virtual filesystem
|
|
283
|
+
scipModule.FS.writeFile(problemFile, problem);
|
|
284
|
+
|
|
285
|
+
// Build SCIP command
|
|
286
|
+
const commands = [];
|
|
287
|
+
|
|
288
|
+
// Set parameters
|
|
289
|
+
commands.push(`set limits time ${timeLimit}`);
|
|
290
|
+
|
|
291
|
+
if (gap !== null) {
|
|
292
|
+
commands.push(`set limits gap ${gap}`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Custom parameters
|
|
296
|
+
for (const [key, value] of Object.entries(parameters)) {
|
|
297
|
+
commands.push(`set ${key} ${value}`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Read and solve
|
|
301
|
+
commands.push(`read ${problemFile}`);
|
|
302
|
+
commands.push('optimize');
|
|
303
|
+
commands.push('display solution');
|
|
304
|
+
commands.push(`write solution ${solutionFile}`);
|
|
305
|
+
commands.push('display statistics');
|
|
306
|
+
commands.push('quit');
|
|
307
|
+
|
|
308
|
+
// Write settings file
|
|
309
|
+
const settingsContent = commands.join('\n');
|
|
310
|
+
scipModule.FS.writeFile('/settings/commands.txt', settingsContent);
|
|
311
|
+
|
|
312
|
+
// Run SCIP with batch mode
|
|
313
|
+
const exitCode = scipModule.callMain(['-b', '/settings/commands.txt']);
|
|
314
|
+
|
|
315
|
+
// Parse results
|
|
316
|
+
const status = parseStatus(stdout);
|
|
317
|
+
const { variables, objective } = parseSolution(stdout);
|
|
318
|
+
const statistics = parseStatistics(stdout);
|
|
319
|
+
|
|
320
|
+
// Try to read solution file
|
|
321
|
+
let rawSolution = null;
|
|
322
|
+
try {
|
|
323
|
+
rawSolution = scipModule.FS.readFile(solutionFile, { encoding: 'utf8' });
|
|
324
|
+
} catch (e) {
|
|
325
|
+
// Solution file may not exist if infeasible
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
status,
|
|
330
|
+
objective: objective.value,
|
|
331
|
+
variables,
|
|
332
|
+
statistics,
|
|
333
|
+
exitCode,
|
|
334
|
+
output: verbose ? stdout : undefined,
|
|
335
|
+
rawSolution
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
} catch (error) {
|
|
339
|
+
return {
|
|
340
|
+
status: Status.ERROR,
|
|
341
|
+
error: error.message,
|
|
342
|
+
output: stdout + stderr
|
|
343
|
+
};
|
|
344
|
+
} finally {
|
|
345
|
+
// Cleanup
|
|
346
|
+
try {
|
|
347
|
+
scipModule.FS.unlink('/problems/problem.lp');
|
|
348
|
+
} catch (e) {}
|
|
349
|
+
try {
|
|
350
|
+
scipModule.FS.unlink('/problems/problem.mps');
|
|
351
|
+
} catch (e) {}
|
|
352
|
+
try {
|
|
353
|
+
scipModule.FS.unlink('/solutions/solution.sol');
|
|
354
|
+
} catch (e) {}
|
|
355
|
+
try {
|
|
356
|
+
scipModule.FS.unlink('/settings/commands.txt');
|
|
357
|
+
} catch (e) {}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Solve a minimization problem
|
|
363
|
+
* Convenience wrapper that ensures minimization
|
|
364
|
+
*/
|
|
365
|
+
export async function minimize(problem, options = {}) {
|
|
366
|
+
// LP format uses "Minimize" keyword, ensure it's present
|
|
367
|
+
if (!problem.toLowerCase().includes('minimize')) {
|
|
368
|
+
problem = 'Minimize\n' + problem;
|
|
369
|
+
}
|
|
370
|
+
return solve(problem, options);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Solve a maximization problem
|
|
375
|
+
* Convenience wrapper that ensures maximization
|
|
376
|
+
*/
|
|
377
|
+
export async function maximize(problem, options = {}) {
|
|
378
|
+
// LP format uses "Maximize" keyword
|
|
379
|
+
if (!problem.toLowerCase().includes('maximize')) {
|
|
380
|
+
problem = 'Maximize\n' + problem;
|
|
381
|
+
}
|
|
382
|
+
return solve(problem, options);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Get SCIP version info
|
|
387
|
+
*/
|
|
388
|
+
export async function version() {
|
|
389
|
+
if (!isInitialized) {
|
|
390
|
+
await init();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
let output = '';
|
|
394
|
+
scipModule.onStdout = (text) => { output += text + '\n'; };
|
|
395
|
+
|
|
396
|
+
scipModule.callMain(['--version']);
|
|
397
|
+
|
|
398
|
+
return output.trim();
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Get available SCIP parameters
|
|
403
|
+
*/
|
|
404
|
+
export async function getParameters() {
|
|
405
|
+
if (!isInitialized) {
|
|
406
|
+
await init();
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
let output = '';
|
|
410
|
+
scipModule.onStdout = (text) => { output += text + '\n'; };
|
|
411
|
+
|
|
412
|
+
scipModule.FS.writeFile('/settings/params.txt', 'set\nquit\n');
|
|
413
|
+
scipModule.callMain(['-b', '/settings/params.txt']);
|
|
414
|
+
|
|
415
|
+
return output;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Default export
|
|
419
|
+
export default {
|
|
420
|
+
init,
|
|
421
|
+
ready,
|
|
422
|
+
isReady,
|
|
423
|
+
solve,
|
|
424
|
+
minimize,
|
|
425
|
+
maximize,
|
|
426
|
+
version,
|
|
427
|
+
getParameters,
|
|
428
|
+
Status
|
|
429
|
+
};
|