@areb0s/scip.js 1.1.4 → 1.1.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/dist/scip-core.js +313 -3
- package/dist/scip-wrapper.js +18 -15
- package/dist/scip.js +311 -0
- package/dist/scip.min.js +15 -9
- package/package.json +1 -1
package/dist/scip-wrapper.js
CHANGED
|
@@ -88,29 +88,32 @@ function parseStatus(output) {
|
|
|
88
88
|
|
|
89
89
|
/**
|
|
90
90
|
* Parse solution values from SCIP output
|
|
91
|
+
* @param {string} output - stdout from SCIP
|
|
92
|
+
* @param {string} rawSolution - solution file content (more reliable)
|
|
91
93
|
*/
|
|
92
|
-
function parseSolution(output) {
|
|
94
|
+
function parseSolution(output, rawSolution = null) {
|
|
93
95
|
const variables = {};
|
|
94
96
|
const objective = { value: null, sense: null };
|
|
95
97
|
|
|
98
|
+
// Use rawSolution if available (more reliable)
|
|
99
|
+
const solText = rawSolution || output;
|
|
100
|
+
|
|
96
101
|
// Parse objective value
|
|
97
|
-
const objMatch =
|
|
102
|
+
const objMatch = solText.match(/objective value:\s*([\d.e+-]+)/i);
|
|
98
103
|
if (objMatch) {
|
|
99
104
|
objective.value = parseFloat(objMatch[1]);
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
// Parse variable values from solution display
|
|
103
|
-
//
|
|
104
|
-
|
|
108
|
+
// Match ZIMPL-style variable names: x$sun#0, effSum$star#1, b_sun_10, etc.
|
|
109
|
+
// Format: variableName value (obj:coef)
|
|
110
|
+
const varRegex = /^([\w$#]+)\s+([\d.e+-]+)/gm;
|
|
105
111
|
let match;
|
|
106
112
|
|
|
107
|
-
|
|
108
|
-
const solSection = output.split('solution:')[1] || output;
|
|
109
|
-
|
|
110
|
-
while ((match = varRegex.exec(solSection)) !== null) {
|
|
113
|
+
while ((match = varRegex.exec(solText)) !== null) {
|
|
111
114
|
const name = match[1];
|
|
112
115
|
const value = parseFloat(match[2]);
|
|
113
|
-
if (!isNaN(value) && name !== 'objective') {
|
|
116
|
+
if (!isNaN(value) && name !== 'objective' && name !== 'solution') {
|
|
114
117
|
variables[name] = value;
|
|
115
118
|
}
|
|
116
119
|
}
|
|
@@ -329,12 +332,7 @@ export async function solve(problem, options = {}) {
|
|
|
329
332
|
// Run SCIP with batch mode
|
|
330
333
|
const exitCode = scipModule.callMain(['-b', '/settings/commands.txt']);
|
|
331
334
|
|
|
332
|
-
//
|
|
333
|
-
const status = parseStatus(stdout);
|
|
334
|
-
const { variables, objective } = parseSolution(stdout);
|
|
335
|
-
const statistics = parseStatistics(stdout);
|
|
336
|
-
|
|
337
|
-
// Try to read solution file
|
|
335
|
+
// Try to read solution file first (more reliable for parsing)
|
|
338
336
|
let rawSolution = null;
|
|
339
337
|
try {
|
|
340
338
|
rawSolution = scipModule.FS.readFile(solutionFile, { encoding: 'utf8' });
|
|
@@ -342,6 +340,11 @@ export async function solve(problem, options = {}) {
|
|
|
342
340
|
// Solution file may not exist if infeasible
|
|
343
341
|
}
|
|
344
342
|
|
|
343
|
+
// Parse results
|
|
344
|
+
const status = parseStatus(stdout);
|
|
345
|
+
const { variables, objective } = parseSolution(stdout, rawSolution);
|
|
346
|
+
const statistics = parseStatistics(stdout);
|
|
347
|
+
|
|
345
348
|
return {
|
|
346
349
|
status,
|
|
347
350
|
objective: objective.value,
|
package/dist/scip.js
CHANGED
|
@@ -1,4 +1,28 @@
|
|
|
1
1
|
|
|
2
|
+
/**
|
|
3
|
+
* SCIP.js Browser Bundle
|
|
4
|
+
* Supports: LP, MIP, MINLP (Mixed Integer Nonlinear Programming)
|
|
5
|
+
*/
|
|
6
|
+
(function(global) {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
// Script directory detection for WASM loading
|
|
10
|
+
var __SCIP_SCRIPT_DIR__ = (function() {
|
|
11
|
+
// Check for explicit SCIP_BASE_URL
|
|
12
|
+
if (typeof SCIP_BASE_URL !== 'undefined' && SCIP_BASE_URL) {
|
|
13
|
+
return SCIP_BASE_URL + (SCIP_BASE_URL.endsWith('/') ? '' : '/');
|
|
14
|
+
}
|
|
15
|
+
// Try to detect from current script
|
|
16
|
+
if (typeof document !== 'undefined' && document.currentScript && document.currentScript.src) {
|
|
17
|
+
var src = document.currentScript.src;
|
|
18
|
+
return src.substring(0, src.lastIndexOf('/') + 1);
|
|
19
|
+
}
|
|
20
|
+
// Default CDN (npm)
|
|
21
|
+
return 'https://cdn.jsdelivr.net/npm/@areb0s/scip.js@latest/dist/';
|
|
22
|
+
})();
|
|
23
|
+
|
|
24
|
+
// Inline the transformed scip-core.js (createSCIP factory function)
|
|
25
|
+
|
|
2
26
|
/**
|
|
3
27
|
* SCIP.js Browser Bundle
|
|
4
28
|
* Supports: LP, MIP, MINLP (Mixed Integer Nonlinear Programming)
|
|
@@ -322,4 +346,291 @@ var Module=moduleArg;var readyPromiseResolve,readyPromiseReject;Module["ready"]=
|
|
|
322
346
|
console.error('[SCIP.js] Auto-initialization failed:', err.message);
|
|
323
347
|
});
|
|
324
348
|
|
|
349
|
+
})(typeof self !== 'undefined' ? self : typeof window !== 'undefined' ? window : globalThis);
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
// SCIP wrapper implementation
|
|
353
|
+
var scipModule = null;
|
|
354
|
+
var isInitialized = false;
|
|
355
|
+
var initPromise = null;
|
|
356
|
+
var readyResolve = null;
|
|
357
|
+
var readyReject = null;
|
|
358
|
+
|
|
359
|
+
var readyPromise = new Promise(function(resolve, reject) {
|
|
360
|
+
readyResolve = resolve;
|
|
361
|
+
readyReject = reject;
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
var Status = {
|
|
365
|
+
OPTIMAL: 'optimal',
|
|
366
|
+
INFEASIBLE: 'infeasible',
|
|
367
|
+
UNBOUNDED: 'unbounded',
|
|
368
|
+
TIME_LIMIT: 'timelimit',
|
|
369
|
+
UNKNOWN: 'unknown',
|
|
370
|
+
ERROR: 'error'
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
function parseStatus(output) {
|
|
374
|
+
if (output.includes('optimal solution found')) return Status.OPTIMAL;
|
|
375
|
+
if (output.includes('problem is infeasible')) return Status.INFEASIBLE;
|
|
376
|
+
if (output.includes('problem is unbounded')) return Status.UNBOUNDED;
|
|
377
|
+
if (output.includes('time limit reached')) return Status.TIME_LIMIT;
|
|
378
|
+
return Status.UNKNOWN;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function parseSolution(output, rawSolution) {
|
|
382
|
+
var variables = {};
|
|
383
|
+
var objective = { value: null };
|
|
384
|
+
|
|
385
|
+
// Use rawSolution if available (more reliable)
|
|
386
|
+
var solText = rawSolution || output;
|
|
387
|
+
|
|
388
|
+
var objMatch = solText.match(/objective value:\s*([\d.e+-]+)/i);
|
|
389
|
+
if (objMatch) {
|
|
390
|
+
objective.value = parseFloat(objMatch[1]);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Match ZIMPL-style variable names: x$sun#0, effSum$star#1, b_sun_10, etc.
|
|
394
|
+
// Format: variableName value (obj:coef)
|
|
395
|
+
var varRegex = /^([\w$#]+)\s+([\d.e+-]+)/gm;
|
|
396
|
+
var match;
|
|
397
|
+
|
|
398
|
+
while ((match = varRegex.exec(solText)) !== null) {
|
|
399
|
+
var name = match[1];
|
|
400
|
+
var value = parseFloat(match[2]);
|
|
401
|
+
if (!isNaN(value) && name !== 'objective' && name !== 'solution') {
|
|
402
|
+
variables[name] = value;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return { variables: variables, objective: objective };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function parseStatistics(output) {
|
|
410
|
+
var stats = { solvingTime: null, nodes: null, iterations: null, gap: null };
|
|
411
|
+
|
|
412
|
+
var timeMatch = output.match(/Solving Time \(sec\)\s*:\s*([\d.]+)/);
|
|
413
|
+
if (timeMatch) stats.solvingTime = parseFloat(timeMatch[1]);
|
|
414
|
+
|
|
415
|
+
var nodesMatch = output.match(/Nodes\s*:\s*(\d+)/);
|
|
416
|
+
if (nodesMatch) stats.nodes = parseInt(nodesMatch[1]);
|
|
417
|
+
|
|
418
|
+
var iterMatch = output.match(/LP Iterations\s*:\s*(\d+)/);
|
|
419
|
+
if (iterMatch) stats.iterations = parseInt(iterMatch[1]);
|
|
420
|
+
|
|
421
|
+
var gapMatch = output.match(/Gap\s*:\s*([\d.]+)\s*%/);
|
|
422
|
+
if (gapMatch) stats.gap = parseFloat(gapMatch[1]);
|
|
423
|
+
|
|
424
|
+
return stats;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function init(options) {
|
|
428
|
+
options = options || {};
|
|
429
|
+
|
|
430
|
+
if (isInitialized) {
|
|
431
|
+
return Promise.resolve();
|
|
432
|
+
}
|
|
433
|
+
if (initPromise) {
|
|
434
|
+
return initPromise;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
initPromise = new Promise(function(resolve, reject) {
|
|
438
|
+
try {
|
|
439
|
+
var wasmPath = options.wasmPath || (__SCIP_SCRIPT_DIR__ + 'scip.wasm');
|
|
440
|
+
|
|
441
|
+
createSCIP({
|
|
442
|
+
locateFile: function(path) {
|
|
443
|
+
if (path.endsWith('.wasm')) {
|
|
444
|
+
return wasmPath;
|
|
445
|
+
}
|
|
446
|
+
return path;
|
|
447
|
+
},
|
|
448
|
+
print: function(text) {
|
|
449
|
+
if (scipModule && scipModule.onStdout) {
|
|
450
|
+
scipModule.onStdout(text);
|
|
451
|
+
}
|
|
452
|
+
},
|
|
453
|
+
printErr: function(text) {
|
|
454
|
+
if (scipModule && scipModule.onStderr) {
|
|
455
|
+
scipModule.onStderr(text);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}).then(function(module) {
|
|
459
|
+
scipModule = module;
|
|
460
|
+
|
|
461
|
+
if (scipModule.FS) {
|
|
462
|
+
try { scipModule.FS.mkdir('/problems'); } catch (e) {}
|
|
463
|
+
try { scipModule.FS.mkdir('/solutions'); } catch (e) {}
|
|
464
|
+
try { scipModule.FS.mkdir('/settings'); } catch (e) {}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
isInitialized = true;
|
|
468
|
+
if (readyResolve) readyResolve();
|
|
469
|
+
resolve();
|
|
470
|
+
}).catch(function(err) {
|
|
471
|
+
var error = new Error('SCIP WASM loading failed: ' + (err.message || err));
|
|
472
|
+
if (readyReject) readyReject(error);
|
|
473
|
+
reject(error);
|
|
474
|
+
});
|
|
475
|
+
} catch (err) {
|
|
476
|
+
if (readyReject) readyReject(err);
|
|
477
|
+
reject(err);
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
return initPromise;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function solve(problem, options) {
|
|
485
|
+
options = options || {};
|
|
486
|
+
|
|
487
|
+
var doSolve = function() {
|
|
488
|
+
var format = options.format || 'lp';
|
|
489
|
+
var timeLimit = options.timeLimit || 3600;
|
|
490
|
+
var gap = options.gap || null;
|
|
491
|
+
var verbose = options.verbose || false;
|
|
492
|
+
var parameters = options.parameters || {};
|
|
493
|
+
|
|
494
|
+
var stdout = '';
|
|
495
|
+
var stderr = '';
|
|
496
|
+
|
|
497
|
+
scipModule.onStdout = function(text) {
|
|
498
|
+
stdout += text + '\n';
|
|
499
|
+
if (verbose) console.log('[SCIP]', text);
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
scipModule.onStderr = function(text) {
|
|
503
|
+
stderr += text + '\n';
|
|
504
|
+
if (verbose) console.error('[SCIP Error]', text);
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
try {
|
|
508
|
+
var formatExtMap = { mps: 'mps', zpl: 'zpl', cip: 'cip', lp: 'lp' };
|
|
509
|
+
var ext = formatExtMap[format] || 'lp';
|
|
510
|
+
var problemFile = '/problems/problem.' + ext;
|
|
511
|
+
var solutionFile = '/solutions/solution.sol';
|
|
512
|
+
|
|
513
|
+
scipModule.FS.writeFile(problemFile, problem);
|
|
514
|
+
|
|
515
|
+
var commands = [];
|
|
516
|
+
commands.push('set limits time ' + timeLimit);
|
|
517
|
+
|
|
518
|
+
if (gap !== null) {
|
|
519
|
+
commands.push('set limits gap ' + gap);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
for (var key in parameters) {
|
|
523
|
+
if (parameters.hasOwnProperty(key)) {
|
|
524
|
+
commands.push('set ' + key + ' ' + parameters[key]);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
commands.push('read ' + problemFile);
|
|
529
|
+
commands.push('optimize');
|
|
530
|
+
commands.push('display solution');
|
|
531
|
+
commands.push('write solution ' + solutionFile);
|
|
532
|
+
commands.push('display statistics');
|
|
533
|
+
commands.push('quit');
|
|
534
|
+
|
|
535
|
+
var settingsContent = commands.join('\n');
|
|
536
|
+
scipModule.FS.writeFile('/settings/commands.txt', settingsContent);
|
|
537
|
+
|
|
538
|
+
var exitCode = scipModule.callMain(['-b', '/settings/commands.txt']);
|
|
539
|
+
|
|
540
|
+
// Read rawSolution first (more reliable for parsing)
|
|
541
|
+
var rawSolution = null;
|
|
542
|
+
try {
|
|
543
|
+
rawSolution = scipModule.FS.readFile(solutionFile, { encoding: 'utf8' });
|
|
544
|
+
} catch (e) {}
|
|
545
|
+
|
|
546
|
+
var status = parseStatus(stdout);
|
|
547
|
+
var parsed = parseSolution(stdout, rawSolution);
|
|
548
|
+
var statistics = parseStatistics(stdout);
|
|
549
|
+
|
|
550
|
+
// Cleanup
|
|
551
|
+
var cleanupFiles = [
|
|
552
|
+
'/problems/problem.lp', '/problems/problem.mps',
|
|
553
|
+
'/problems/problem.zpl', '/problems/problem.cip',
|
|
554
|
+
'/solutions/solution.sol', '/settings/commands.txt'
|
|
555
|
+
];
|
|
556
|
+
for (var i = 0; i < cleanupFiles.length; i++) {
|
|
557
|
+
try { scipModule.FS.unlink(cleanupFiles[i]); } catch (e) {}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return {
|
|
561
|
+
status: status,
|
|
562
|
+
objective: parsed.objective.value,
|
|
563
|
+
variables: parsed.variables,
|
|
564
|
+
statistics: statistics,
|
|
565
|
+
exitCode: exitCode,
|
|
566
|
+
output: verbose ? stdout : undefined,
|
|
567
|
+
rawSolution: rawSolution
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
} catch (error) {
|
|
571
|
+
return {
|
|
572
|
+
status: Status.ERROR,
|
|
573
|
+
error: error.message,
|
|
574
|
+
output: stdout + stderr
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
if (!isInitialized) {
|
|
580
|
+
return init(options).then(doSolve);
|
|
581
|
+
}
|
|
582
|
+
return Promise.resolve(doSolve());
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function minimize(problem, options) {
|
|
586
|
+
if (!problem.toLowerCase().includes('minimize')) {
|
|
587
|
+
problem = 'Minimize\n' + problem;
|
|
588
|
+
}
|
|
589
|
+
return solve(problem, options);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function maximize(problem, options) {
|
|
593
|
+
if (!problem.toLowerCase().includes('maximize')) {
|
|
594
|
+
problem = 'Maximize\n' + problem;
|
|
595
|
+
}
|
|
596
|
+
return solve(problem, options);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function version() {
|
|
600
|
+
var getVersion = function() {
|
|
601
|
+
var output = '';
|
|
602
|
+
scipModule.onStdout = function(text) { output += text + '\n'; };
|
|
603
|
+
scipModule.callMain(['--version']);
|
|
604
|
+
return output.trim();
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
if (!isInitialized) {
|
|
608
|
+
return init().then(getVersion);
|
|
609
|
+
}
|
|
610
|
+
return Promise.resolve(getVersion());
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function isReady() {
|
|
614
|
+
return isInitialized;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Expose SCIP API
|
|
618
|
+
var SCIP = {
|
|
619
|
+
init: init,
|
|
620
|
+
ready: readyPromise,
|
|
621
|
+
isReady: isReady,
|
|
622
|
+
solve: solve,
|
|
623
|
+
minimize: minimize,
|
|
624
|
+
maximize: maximize,
|
|
625
|
+
version: version,
|
|
626
|
+
Status: Status
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
global.SCIP = SCIP;
|
|
630
|
+
|
|
631
|
+
// Auto-initialize
|
|
632
|
+
init().catch(function(err) {
|
|
633
|
+
console.error('[SCIP.js] Auto-initialization failed:', err.message);
|
|
634
|
+
});
|
|
635
|
+
|
|
325
636
|
})(typeof self !== 'undefined' ? self : typeof window !== 'undefined' ? window : globalThis);
|