@areb0s/scip.js 1.1.3 → 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.
@@ -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 = output.match(/objective value:\s*([\d.e+-]+)/i);
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
- // Format: variable_name value (obj:coef)
104
- const varRegex = /^(\w+)\s+([\d.e+-]+)/gm;
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
- // Look for solution section
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
- // Parse results
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)
@@ -67,23 +91,27 @@ var Module=moduleArg;var readyPromiseResolve,readyPromiseReject;Module["ready"]=
67
91
  return Status.UNKNOWN;
68
92
  }
69
93
 
70
- function parseSolution(output) {
94
+ function parseSolution(output, rawSolution) {
71
95
  var variables = {};
72
96
  var objective = { value: null };
73
97
 
74
- var objMatch = output.match(/objective value:\s*([\d.e+-]+)/i);
98
+ // Use rawSolution if available (more reliable)
99
+ var solText = rawSolution || output;
100
+
101
+ var objMatch = solText.match(/objective value:\s*([\d.e+-]+)/i);
75
102
  if (objMatch) {
76
103
  objective.value = parseFloat(objMatch[1]);
77
104
  }
78
105
 
79
- var varRegex = /^(\w+)\s+([\d.e+-]+)/gm;
106
+ // Match ZIMPL-style variable names: x$sun#0, effSum$star#1, b_sun_10, etc.
107
+ // Format: variableName value (obj:coef)
108
+ var varRegex = /^([\w$#]+)\s+([\d.e+-]+)/gm;
80
109
  var match;
81
- var solSection = output.split('solution:')[1] || output;
82
110
 
83
- while ((match = varRegex.exec(solSection)) !== null) {
111
+ while ((match = varRegex.exec(solText)) !== null) {
84
112
  var name = match[1];
85
113
  var value = parseFloat(match[2]);
86
- if (!isNaN(value) && name !== 'objective') {
114
+ if (!isNaN(value) && name !== 'objective' && name !== 'solution') {
87
115
  variables[name] = value;
88
116
  }
89
117
  }
@@ -122,22 +150,10 @@ var Module=moduleArg;var readyPromiseResolve,readyPromiseReject;Module["ready"]=
122
150
  initPromise = new Promise(function(resolve, reject) {
123
151
  try {
124
152
  var wasmPath = options.wasmPath || (__SCIP_SCRIPT_DIR__ + 'scip.wasm');
125
- console.log('[SCIP.js] __SCIP_SCRIPT_DIR__:', __SCIP_SCRIPT_DIR__);
126
- console.log('[SCIP.js] Loading WASM from:', wasmPath);
127
-
128
- // Verify WASM is accessible
129
- fetch(wasmPath, { method: 'HEAD' })
130
- .then(function(res) {
131
- console.log('[SCIP.js] WASM file check:', res.ok ? 'OK' : 'FAILED', res.status);
132
- })
133
- .catch(function(err) {
134
- console.error('[SCIP.js] WASM file check failed:', err);
135
- });
136
153
 
137
154
  createSCIP({
138
155
  locateFile: function(path) {
139
156
  if (path.endsWith('.wasm')) {
140
- console.log('[SCIP.js] locateFile called for:', path, '-> returning:', wasmPath);
141
157
  return wasmPath;
142
158
  }
143
159
  return path;
@@ -153,7 +169,6 @@ var Module=moduleArg;var readyPromiseResolve,readyPromiseReject;Module["ready"]=
153
169
  }
154
170
  }
155
171
  }).then(function(module) {
156
- console.log('[SCIP.js] WASM loaded successfully');
157
172
  scipModule = module;
158
173
 
159
174
  if (scipModule.FS) {
@@ -166,16 +181,11 @@ var Module=moduleArg;var readyPromiseResolve,readyPromiseReject;Module["ready"]=
166
181
  if (readyResolve) readyResolve();
167
182
  resolve();
168
183
  }).catch(function(err) {
169
- console.error('[SCIP.js] WASM loading failed:', err);
170
- console.error('[SCIP.js] Attempted WASM path:', wasmPath);
171
- console.error('[SCIP.js] Make sure the WASM file is accessible at this URL.');
172
- console.error('[SCIP.js] You can set window.SCIP_BASE_URL before loading this script to specify a custom path.');
173
- var error = new Error('SCIP WASM loading failed: ' + (err.message || err) + '. WASM path: ' + wasmPath);
184
+ var error = new Error('SCIP WASM loading failed: ' + (err.message || err));
174
185
  if (readyReject) readyReject(error);
175
186
  reject(error);
176
187
  });
177
188
  } catch (err) {
178
- console.error('[SCIP.js] Initialization error:', err);
179
189
  if (readyReject) readyReject(err);
180
190
  reject(err);
181
191
  }
@@ -240,15 +250,303 @@ var Module=moduleArg;var readyPromiseResolve,readyPromiseReject;Module["ready"]=
240
250
 
241
251
  var exitCode = scipModule.callMain(['-b', '/settings/commands.txt']);
242
252
 
253
+ // Read rawSolution first (more reliable for parsing)
254
+ var rawSolution = null;
255
+ try {
256
+ rawSolution = scipModule.FS.readFile(solutionFile, { encoding: 'utf8' });
257
+ } catch (e) {}
258
+
243
259
  var status = parseStatus(stdout);
244
- var parsed = parseSolution(stdout);
260
+ var parsed = parseSolution(stdout, rawSolution);
245
261
  var statistics = parseStatistics(stdout);
246
262
 
263
+ // Cleanup
264
+ var cleanupFiles = [
265
+ '/problems/problem.lp', '/problems/problem.mps',
266
+ '/problems/problem.zpl', '/problems/problem.cip',
267
+ '/solutions/solution.sol', '/settings/commands.txt'
268
+ ];
269
+ for (var i = 0; i < cleanupFiles.length; i++) {
270
+ try { scipModule.FS.unlink(cleanupFiles[i]); } catch (e) {}
271
+ }
272
+
273
+ return {
274
+ status: status,
275
+ objective: parsed.objective.value,
276
+ variables: parsed.variables,
277
+ statistics: statistics,
278
+ exitCode: exitCode,
279
+ output: verbose ? stdout : undefined,
280
+ rawSolution: rawSolution
281
+ };
282
+
283
+ } catch (error) {
284
+ return {
285
+ status: Status.ERROR,
286
+ error: error.message,
287
+ output: stdout + stderr
288
+ };
289
+ }
290
+ };
291
+
292
+ if (!isInitialized) {
293
+ return init(options).then(doSolve);
294
+ }
295
+ return Promise.resolve(doSolve());
296
+ }
297
+
298
+ function minimize(problem, options) {
299
+ if (!problem.toLowerCase().includes('minimize')) {
300
+ problem = 'Minimize\n' + problem;
301
+ }
302
+ return solve(problem, options);
303
+ }
304
+
305
+ function maximize(problem, options) {
306
+ if (!problem.toLowerCase().includes('maximize')) {
307
+ problem = 'Maximize\n' + problem;
308
+ }
309
+ return solve(problem, options);
310
+ }
311
+
312
+ function version() {
313
+ var getVersion = function() {
314
+ var output = '';
315
+ scipModule.onStdout = function(text) { output += text + '\n'; };
316
+ scipModule.callMain(['--version']);
317
+ return output.trim();
318
+ };
319
+
320
+ if (!isInitialized) {
321
+ return init().then(getVersion);
322
+ }
323
+ return Promise.resolve(getVersion());
324
+ }
325
+
326
+ function isReady() {
327
+ return isInitialized;
328
+ }
329
+
330
+ // Expose SCIP API
331
+ var SCIP = {
332
+ init: init,
333
+ ready: readyPromise,
334
+ isReady: isReady,
335
+ solve: solve,
336
+ minimize: minimize,
337
+ maximize: maximize,
338
+ version: version,
339
+ Status: Status
340
+ };
341
+
342
+ global.SCIP = SCIP;
343
+
344
+ // Auto-initialize
345
+ init().catch(function(err) {
346
+ console.error('[SCIP.js] Auto-initialization failed:', err.message);
347
+ });
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)
247
541
  var rawSolution = null;
248
542
  try {
249
543
  rawSolution = scipModule.FS.readFile(solutionFile, { encoding: 'utf8' });
250
544
  } catch (e) {}
251
545
 
546
+ var status = parseStatus(stdout);
547
+ var parsed = parseSolution(stdout, rawSolution);
548
+ var statistics = parseStatistics(stdout);
549
+
252
550
  // Cleanup
253
551
  var cleanupFiles = [
254
552
  '/problems/problem.lp', '/problems/problem.mps',