@areb0s/scip.js 1.1.13 → 1.2.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.
Files changed (2) hide show
  1. package/dist/scip-wrapper.js +163 -132
  2. package/package.json +1 -1
@@ -1,20 +1,20 @@
1
1
  /**
2
2
  * SCIP.js - SCIP Optimization Solver for JavaScript
3
3
  * High-level wrapper around SCIP WASM
4
- *
4
+ *
5
5
  * Supports: LP, MIP, MINLP (Mixed Integer Nonlinear Programming)
6
- *
6
+ *
7
7
  * Usage in Worker (like OpenCV):
8
8
  * // Set base URL before loading script
9
9
  * self.SCIP_BASE_URL = 'https://cdn.jsdelivr.net/gh/user/scip.js@v1.0.0/dist/';
10
- *
10
+ *
11
11
  * // Load and execute script
12
12
  * const response = await fetch(SCIP_BASE_URL + 'scip.min.js');
13
13
  * new Function(await response.text())();
14
- *
14
+ *
15
15
  * // Wait for initialization
16
16
  * await self.SCIP.ready;
17
- *
17
+ *
18
18
  * // Use
19
19
  * const result = await self.SCIP.solve(`...`);
20
20
  */
@@ -38,31 +38,42 @@ export const ready = new Promise((resolve, reject) => {
38
38
  readyReject = reject;
39
39
  });
40
40
 
41
+ // Read version from package.json
42
+ const packageJson = JSON.parse(
43
+ readFileSync(join(rootDir, "package.json"), "utf-8")
44
+ );
45
+ const VERSION = packageJson.version;
46
+
41
47
  /**
42
48
  * Default CDN base URL for WASM files
43
49
  */
44
- const DEFAULT_CDN_BASE = 'https://cdn.jsdelivr.net/npm/@areb0s/scip.js@1.1.6/dist/';
50
+ const DEFAULT_CDN_BASE = `https://cdn.jsdelivr.net/npm/@areb0s/scip.js@${VERSION}/dist/`;
45
51
 
46
52
  /**
47
53
  * Get base URL from global SCIP_BASE_URL or default CDN
48
54
  */
49
55
  function getBaseUrl() {
50
56
  // Safe check for global scope (works in browser, worker, and SSR)
51
- const globalScope = (typeof globalThis !== 'undefined' && globalThis) ||
52
- (typeof self !== 'undefined' && self) ||
53
- (typeof window !== 'undefined' && window) ||
54
- {};
55
-
57
+ const globalScope =
58
+ (typeof globalThis !== "undefined" && globalThis) ||
59
+ (typeof self !== "undefined" && self) ||
60
+ (typeof window !== "undefined" && window) ||
61
+ {};
62
+
56
63
  // Check for explicit SCIP_BASE_URL
57
64
  if (globalScope.SCIP_BASE_URL) {
58
65
  return globalScope.SCIP_BASE_URL;
59
66
  }
60
-
67
+
61
68
  // Check for __importMetaUrl (set by bundler)
62
- if (typeof __importMetaUrl !== 'undefined' && __importMetaUrl && !__importMetaUrl.startsWith('blob:')) {
63
- return __importMetaUrl.substring(0, __importMetaUrl.lastIndexOf('/') + 1);
69
+ if (
70
+ typeof __importMetaUrl !== "undefined" &&
71
+ __importMetaUrl &&
72
+ !__importMetaUrl.startsWith("blob:")
73
+ ) {
74
+ return __importMetaUrl.substring(0, __importMetaUrl.lastIndexOf("/") + 1);
64
75
  }
65
-
76
+
66
77
  // Default to CDN
67
78
  return DEFAULT_CDN_BASE;
68
79
  }
@@ -71,22 +82,22 @@ function getBaseUrl() {
71
82
  * Solution status enum
72
83
  */
73
84
  export const Status = {
74
- OPTIMAL: 'optimal',
75
- INFEASIBLE: 'infeasible',
76
- UNBOUNDED: 'unbounded',
77
- TIME_LIMIT: 'timelimit',
78
- UNKNOWN: 'unknown',
79
- ERROR: 'error'
85
+ OPTIMAL: "optimal",
86
+ INFEASIBLE: "infeasible",
87
+ UNBOUNDED: "unbounded",
88
+ TIME_LIMIT: "timelimit",
89
+ UNKNOWN: "unknown",
90
+ ERROR: "error",
80
91
  };
81
92
 
82
93
  /**
83
94
  * Parse SCIP status from output
84
95
  */
85
96
  function parseStatus(output) {
86
- if (output.includes('optimal solution found')) return Status.OPTIMAL;
87
- if (output.includes('problem is infeasible')) return Status.INFEASIBLE;
88
- if (output.includes('problem is unbounded')) return Status.UNBOUNDED;
89
- if (output.includes('time limit reached')) return Status.TIME_LIMIT;
97
+ if (output.includes("optimal solution found")) return Status.OPTIMAL;
98
+ if (output.includes("problem is infeasible")) return Status.INFEASIBLE;
99
+ if (output.includes("problem is unbounded")) return Status.UNBOUNDED;
100
+ if (output.includes("time limit reached")) return Status.TIME_LIMIT;
90
101
  return Status.UNKNOWN;
91
102
  }
92
103
 
@@ -98,30 +109,30 @@ function parseStatus(output) {
98
109
  function parseSolution(output, rawSolution = null) {
99
110
  const variables = {};
100
111
  const objective = { value: null, sense: null };
101
-
112
+
102
113
  // Use rawSolution if available (more reliable)
103
114
  const solText = rawSolution || output;
104
-
115
+
105
116
  // Parse objective value
106
117
  const objMatch = solText.match(/objective value:\s*([\d.e+-]+)/i);
107
118
  if (objMatch) {
108
119
  objective.value = parseFloat(objMatch[1]);
109
120
  }
110
-
121
+
111
122
  // Parse variable values from solution display
112
123
  // Match ZIMPL-style variable names: x$sun#0, effSum$star#1, b_sun_10, etc.
113
124
  // Format: variableName value (obj:coef)
114
125
  const varRegex = /^([\w$#]+)\s+([\d.e+-]+)/gm;
115
126
  let match;
116
-
127
+
117
128
  while ((match = varRegex.exec(solText)) !== null) {
118
129
  const name = match[1];
119
130
  const value = parseFloat(match[2]);
120
- if (!isNaN(value) && name !== 'objective' && name !== 'solution') {
131
+ if (!isNaN(value) && name !== "objective" && name !== "solution") {
121
132
  variables[name] = value;
122
133
  }
123
134
  }
124
-
135
+
125
136
  return { variables, objective };
126
137
  }
127
138
 
@@ -133,21 +144,21 @@ function parseStatistics(output) {
133
144
  solvingTime: null,
134
145
  nodes: null,
135
146
  iterations: null,
136
- gap: null
147
+ gap: null,
137
148
  };
138
-
149
+
139
150
  const timeMatch = output.match(/Solving Time \(sec\)\s*:\s*([\d.]+)/);
140
151
  if (timeMatch) stats.solvingTime = parseFloat(timeMatch[1]);
141
-
152
+
142
153
  const nodesMatch = output.match(/Nodes\s*:\s*(\d+)/);
143
154
  if (nodesMatch) stats.nodes = parseInt(nodesMatch[1]);
144
-
155
+
145
156
  const iterMatch = output.match(/LP Iterations\s*:\s*(\d+)/);
146
157
  if (iterMatch) stats.iterations = parseInt(iterMatch[1]);
147
-
158
+
148
159
  const gapMatch = output.match(/Gap\s*:\s*([\d.]+)\s*%/);
149
160
  if (gapMatch) stats.gap = parseFloat(gapMatch[1]);
150
-
161
+
151
162
  return stats;
152
163
  }
153
164
 
@@ -164,19 +175,19 @@ export async function init(options = {}) {
164
175
  if (initPromise) {
165
176
  return initPromise;
166
177
  }
167
-
178
+
168
179
  initPromise = (async () => {
169
180
  try {
170
181
  // Auto-detect wasmPath from SCIP_BASE_URL or script location
171
182
  const baseUrl = getBaseUrl();
172
- const wasmPath = options.wasmPath || (baseUrl + 'scip.wasm');
173
-
183
+ const wasmPath = options.wasmPath || baseUrl + "scip.wasm";
184
+
174
185
  // Dynamic import of the Emscripten-generated module
175
- const createSCIP = (await import('./scip-core.js')).default;
176
-
186
+ const createSCIP = (await import("./scip-core.js")).default;
187
+
177
188
  scipModule = await createSCIP({
178
189
  locateFile: (path) => {
179
- if (path.endsWith('.wasm')) {
190
+ if (path.endsWith(".wasm")) {
180
191
  return wasmPath;
181
192
  }
182
193
  return path;
@@ -195,25 +206,37 @@ export async function init(options = {}) {
195
206
  // Capture abort/exit reasons for better error messages
196
207
  onAbort: (reason) => {
197
208
  lastAbortReason = reason;
198
- console.error('[SCIP WASM Abort]', reason);
209
+ console.error("[SCIP WASM Abort]", reason);
199
210
  },
200
211
  onExit: (code) => {
201
212
  lastExitCode = code;
202
213
  if (code !== 0) {
203
- console.error('[SCIP WASM Exit]', code);
214
+ console.error("[SCIP WASM Exit]", code);
204
215
  }
205
- }
216
+ },
206
217
  });
207
-
218
+
208
219
  // Create directories for problems, solutions, settings
209
220
  if (scipModule.FS) {
210
- try { scipModule.FS.mkdir('/problems'); } catch (e) { /* exists */ }
211
- try { scipModule.FS.mkdir('/solutions'); } catch (e) { /* exists */ }
212
- try { scipModule.FS.mkdir('/settings'); } catch (e) { /* exists */ }
221
+ try {
222
+ scipModule.FS.mkdir("/problems");
223
+ } catch (e) {
224
+ /* exists */
225
+ }
226
+ try {
227
+ scipModule.FS.mkdir("/solutions");
228
+ } catch (e) {
229
+ /* exists */
230
+ }
231
+ try {
232
+ scipModule.FS.mkdir("/settings");
233
+ } catch (e) {
234
+ /* exists */
235
+ }
213
236
  }
214
-
237
+
215
238
  isInitialized = true;
216
-
239
+
217
240
  // Resolve ready promise
218
241
  if (readyResolve) {
219
242
  readyResolve();
@@ -225,7 +248,7 @@ export async function init(options = {}) {
225
248
  throw error;
226
249
  }
227
250
  })();
228
-
251
+
229
252
  return initPromise;
230
253
  }
231
254
 
@@ -239,9 +262,9 @@ export function isReady() {
239
262
 
240
263
  /**
241
264
  * Solve an optimization problem
242
- *
265
+ *
243
266
  * Supports LP, MIP, and MINLP (Mixed Integer Nonlinear Programming) problems.
244
- *
267
+ *
245
268
  * @param {string} problem - Problem definition in one of the supported formats
246
269
  * @param {Object} options - Solver options
247
270
  * @param {string} options.format - Input format: 'lp', 'mps', 'zpl', 'cip' (default: 'lp')
@@ -254,7 +277,7 @@ export function isReady() {
254
277
  * @param {boolean} options.verbose - Enable verbose output
255
278
  * @param {Object} options.parameters - Additional SCIP parameters
256
279
  * @returns {Promise<Object>} Solution object
257
- *
280
+ *
258
281
  * @example
259
282
  * // LP format (linear)
260
283
  * const result = await solve(`
@@ -266,7 +289,7 @@ export function isReady() {
266
289
  * 0 <= y <= 10
267
290
  * End
268
291
  * `);
269
- *
292
+ *
270
293
  * @example
271
294
  * // ZIMPL format (MINLP with nonlinear)
272
295
  * const result = await solve(`
@@ -275,7 +298,7 @@ export function isReady() {
275
298
  * minimize cost: x^2 + y^2;
276
299
  * subto c1: x + y >= 1;
277
300
  * `, { format: 'zpl' });
278
- *
301
+ *
279
302
  * console.log(result.status); // 'optimal'
280
303
  * console.log(result.objective); // 1.0
281
304
  * console.log(result.variables); // { x: 1, y: 0 }
@@ -284,82 +307,82 @@ export async function solve(problem, options = {}) {
284
307
  if (!isInitialized) {
285
308
  await init(options);
286
309
  }
287
-
310
+
288
311
  const {
289
- format = 'lp',
312
+ format = "lp",
290
313
  timeLimit = 3600,
291
314
  gap = null,
292
315
  verbose = false,
293
- parameters = {}
316
+ parameters = {},
294
317
  } = options;
295
-
318
+
296
319
  // Capture output
297
- let stdout = '';
298
- let stderr = '';
299
-
320
+ let stdout = "";
321
+ let stderr = "";
322
+
300
323
  scipModule.onStdout = (text) => {
301
- stdout += text + '\n';
302
- if (verbose) console.log('[SCIP]', text);
324
+ stdout += text + "\n";
325
+ if (verbose) console.log("[SCIP]", text);
303
326
  };
304
-
327
+
305
328
  scipModule.onStderr = (text) => {
306
- stderr += text + '\n';
307
- if (verbose) console.error('[SCIP Error]', text);
329
+ stderr += text + "\n";
330
+ if (verbose) console.error("[SCIP Error]", text);
308
331
  };
309
-
332
+
310
333
  try {
311
334
  // Determine file extension based on format
312
- const formatExtMap = { mps: 'mps', zpl: 'zpl', cip: 'cip', lp: 'lp' };
313
- const ext = formatExtMap[format] || 'lp';
335
+ const formatExtMap = { mps: "mps", zpl: "zpl", cip: "cip", lp: "lp" };
336
+ const ext = formatExtMap[format] || "lp";
314
337
  const problemFile = `/problems/problem.${ext}`;
315
- const solutionFile = '/solutions/solution.sol';
316
-
338
+ const solutionFile = "/solutions/solution.sol";
339
+
317
340
  // Write problem to virtual filesystem
318
341
  scipModule.FS.writeFile(problemFile, problem);
319
-
342
+
320
343
  // Build SCIP command
321
344
  const commands = [];
322
-
345
+
323
346
  // Set parameters
324
347
  commands.push(`set limits time ${timeLimit}`);
325
-
348
+
326
349
  if (gap !== null) {
327
350
  commands.push(`set limits gap ${gap}`);
328
351
  }
329
-
352
+
330
353
  // Custom parameters
331
354
  for (const [key, value] of Object.entries(parameters)) {
332
355
  commands.push(`set ${key} ${value}`);
333
356
  }
334
-
357
+
335
358
  // Read and solve
336
359
  commands.push(`read ${problemFile}`);
337
- commands.push('optimize');
338
- commands.push('display solution');
360
+ commands.push("optimize");
361
+ commands.push("display solution");
339
362
  commands.push(`write solution ${solutionFile}`);
340
- commands.push('display statistics');
341
- commands.push('quit');
342
-
363
+ commands.push("display statistics");
364
+ commands.push("quit");
365
+
343
366
  // Write settings file
344
- const settingsContent = commands.join('\n');
345
- scipModule.FS.writeFile('/settings/commands.txt', settingsContent);
346
-
367
+ const settingsContent = commands.join("\n");
368
+ scipModule.FS.writeFile("/settings/commands.txt", settingsContent);
369
+
347
370
  // Run SCIP with batch mode
348
- const exitCode = scipModule.callMain(['-b', '/settings/commands.txt']);
349
-
371
+ const exitCode = scipModule.callMain(["-b", "/settings/commands.txt"]);
372
+
350
373
  // Try to read solution file first (more reliable for parsing)
351
374
  let rawSolution = null;
352
375
  try {
353
- rawSolution = scipModule.FS.readFile(solutionFile, { encoding: 'utf8' });
376
+ rawSolution = scipModule.FS.readFile(solutionFile, { encoding: "utf8" });
354
377
  } catch (e) {
355
378
  // Solution file may not exist if infeasible
356
379
  }
357
-
380
+
358
381
  // Parse results
359
382
  const status = parseStatus(stdout);
360
383
  const { variables, objective } = parseSolution(stdout, rawSolution);
361
384
  const statistics = parseStatistics(stdout);
362
-
385
+
363
386
  return {
364
387
  status,
365
388
  objective: objective.value,
@@ -367,26 +390,26 @@ export async function solve(problem, options = {}) {
367
390
  statistics,
368
391
  exitCode,
369
392
  output: verbose ? stdout : undefined,
370
- rawSolution
393
+ rawSolution,
371
394
  };
372
-
373
395
  } catch (error) {
374
396
  // Attempt to extract detailed exception message from WASM
375
397
  let errorMessage = error.message || String(error);
376
398
  let exceptionInfo = null;
377
-
399
+
378
400
  // Check if this is a WASM exception (pointer address)
379
- if (typeof error === 'number' || /^\d+$/.test(String(error))) {
380
- const ptr = typeof error === 'number' ? error : parseInt(String(error), 10);
381
-
401
+ if (typeof error === "number" || /^\d+$/.test(String(error))) {
402
+ const ptr =
403
+ typeof error === "number" ? error : parseInt(String(error), 10);
404
+
382
405
  // Try to get exception message using Emscripten's exception handling
383
406
  if (scipModule) {
384
407
  try {
385
408
  // Modern Emscripten exception handling
386
- if (typeof scipModule.getExceptionMessage === 'function') {
409
+ if (typeof scipModule.getExceptionMessage === "function") {
387
410
  exceptionInfo = scipModule.getExceptionMessage(ptr);
388
411
  errorMessage = `WASM Exception: ${exceptionInfo}`;
389
- } else if (typeof scipModule.UTF8ToString === 'function') {
412
+ } else if (typeof scipModule.UTF8ToString === "function") {
390
413
  // Fallback: try to read as string from memory
391
414
  try {
392
415
  const str = scipModule.UTF8ToString(ptr);
@@ -394,18 +417,20 @@ export async function solve(problem, options = {}) {
394
417
  exceptionInfo = str;
395
418
  errorMessage = `WASM Exception: ${str}`;
396
419
  }
397
- } catch (e) { /* not a valid string pointer */ }
420
+ } catch (e) {
421
+ /* not a valid string pointer */
422
+ }
398
423
  }
399
424
  } catch (e) {
400
- console.error('[SCIP] Failed to get exception message:', e);
425
+ console.error("[SCIP] Failed to get exception message:", e);
401
426
  }
402
427
  }
403
-
428
+
404
429
  if (!exceptionInfo) {
405
430
  errorMessage = `WASM Exception (ptr: ${ptr}). Enable exception handling in build for details.`;
406
431
  }
407
432
  }
408
-
433
+
409
434
  return {
410
435
  status: Status.ERROR,
411
436
  error: errorMessage,
@@ -416,9 +441,9 @@ export async function solve(problem, options = {}) {
416
441
  exitCode: lastExitCode,
417
442
  type: typeof error,
418
443
  stdout: stdout,
419
- stderr: stderr
444
+ stderr: stderr,
420
445
  },
421
- output: stdout + stderr
446
+ output: stdout + stderr,
422
447
  };
423
448
  } finally {
424
449
  // Reset exception tracking
@@ -426,15 +451,17 @@ export async function solve(problem, options = {}) {
426
451
  lastExitCode = null;
427
452
  // Cleanup all possible problem files
428
453
  const cleanupFiles = [
429
- '/problems/problem.lp',
430
- '/problems/problem.mps',
431
- '/problems/problem.zpl',
432
- '/problems/problem.cip',
433
- '/solutions/solution.sol',
434
- '/settings/commands.txt'
454
+ "/problems/problem.lp",
455
+ "/problems/problem.mps",
456
+ "/problems/problem.zpl",
457
+ "/problems/problem.cip",
458
+ "/solutions/solution.sol",
459
+ "/settings/commands.txt",
435
460
  ];
436
461
  for (const file of cleanupFiles) {
437
- try { scipModule.FS.unlink(file); } catch (e) {}
462
+ try {
463
+ scipModule.FS.unlink(file);
464
+ } catch (e) {}
438
465
  }
439
466
  }
440
467
  }
@@ -445,8 +472,8 @@ export async function solve(problem, options = {}) {
445
472
  */
446
473
  export async function minimize(problem, options = {}) {
447
474
  // LP format uses "Minimize" keyword, ensure it's present
448
- if (!problem.toLowerCase().includes('minimize')) {
449
- problem = 'Minimize\n' + problem;
475
+ if (!problem.toLowerCase().includes("minimize")) {
476
+ problem = "Minimize\n" + problem;
450
477
  }
451
478
  return solve(problem, options);
452
479
  }
@@ -457,8 +484,8 @@ export async function minimize(problem, options = {}) {
457
484
  */
458
485
  export async function maximize(problem, options = {}) {
459
486
  // LP format uses "Maximize" keyword
460
- if (!problem.toLowerCase().includes('maximize')) {
461
- problem = 'Maximize\n' + problem;
487
+ if (!problem.toLowerCase().includes("maximize")) {
488
+ problem = "Maximize\n" + problem;
462
489
  }
463
490
  return solve(problem, options);
464
491
  }
@@ -470,12 +497,14 @@ export async function version() {
470
497
  if (!isInitialized) {
471
498
  await init();
472
499
  }
473
-
474
- let output = '';
475
- scipModule.onStdout = (text) => { output += text + '\n'; };
476
-
477
- scipModule.callMain(['--version']);
478
-
500
+
501
+ let output = "";
502
+ scipModule.onStdout = (text) => {
503
+ output += text + "\n";
504
+ };
505
+
506
+ scipModule.callMain(["--version"]);
507
+
479
508
  return output.trim();
480
509
  }
481
510
 
@@ -486,13 +515,15 @@ export async function getParameters() {
486
515
  if (!isInitialized) {
487
516
  await init();
488
517
  }
489
-
490
- let output = '';
491
- scipModule.onStdout = (text) => { output += text + '\n'; };
492
-
493
- scipModule.FS.writeFile('/settings/params.txt', 'set\nquit\n');
494
- scipModule.callMain(['-b', '/settings/params.txt']);
495
-
518
+
519
+ let output = "";
520
+ scipModule.onStdout = (text) => {
521
+ output += text + "\n";
522
+ };
523
+
524
+ scipModule.FS.writeFile("/settings/params.txt", "set\nquit\n");
525
+ scipModule.callMain(["-b", "/settings/params.txt"]);
526
+
496
527
  return output;
497
528
  }
498
529
 
@@ -506,5 +537,5 @@ export default {
506
537
  maximize,
507
538
  version,
508
539
  getParameters,
509
- Status
540
+ Status,
510
541
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@areb0s/scip.js",
3
- "version": "1.1.13",
3
+ "version": "1.2.0",
4
4
  "description": "SCIP Optimization Solver compiled to WebAssembly - LP, MIP, and MINLP support",
5
5
  "main": "dist/index.mjs",
6
6
  "module": "dist/index.mjs",