@areb0s/scip.js 1.2.4 → 1.2.6
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-wrapper.js +65 -119
- package/dist/types.d.ts +6 -0
- package/package.json +1 -1
package/dist/scip-wrapper.js
CHANGED
|
@@ -25,10 +25,6 @@ let initPromise = null;
|
|
|
25
25
|
let readyResolve = null;
|
|
26
26
|
let readyReject = null;
|
|
27
27
|
|
|
28
|
-
// Exception tracking for debugging WASM crashes
|
|
29
|
-
let lastAbortReason = null;
|
|
30
|
-
let lastExitCode = null;
|
|
31
|
-
|
|
32
28
|
/**
|
|
33
29
|
* Ready promise - resolves when SCIP is initialized
|
|
34
30
|
* Usage: await SCIP.ready;
|
|
@@ -38,16 +34,12 @@ export const ready = new Promise((resolve, reject) => {
|
|
|
38
34
|
readyReject = reject;
|
|
39
35
|
});
|
|
40
36
|
|
|
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
|
-
|
|
47
37
|
/**
|
|
48
38
|
* Default CDN base URL for WASM files
|
|
39
|
+
* Using npm CDN with specific version to ensure JS/WASM version consistency
|
|
49
40
|
*/
|
|
50
|
-
const DEFAULT_CDN_BASE =
|
|
41
|
+
const DEFAULT_CDN_BASE =
|
|
42
|
+
"https://cdn.jsdelivr.net/npm/@areb0s/scip.js@latest/dist/";
|
|
51
43
|
|
|
52
44
|
/**
|
|
53
45
|
* Get base URL from global SCIP_BASE_URL or default CDN
|
|
@@ -103,32 +95,29 @@ function parseStatus(output) {
|
|
|
103
95
|
|
|
104
96
|
/**
|
|
105
97
|
* Parse solution values from SCIP output
|
|
106
|
-
* @param {string} output - stdout from SCIP
|
|
107
|
-
* @param {string} rawSolution - solution file content (more reliable)
|
|
108
98
|
*/
|
|
109
|
-
function parseSolution(output
|
|
99
|
+
function parseSolution(output) {
|
|
110
100
|
const variables = {};
|
|
111
101
|
const objective = { value: null, sense: null };
|
|
112
102
|
|
|
113
|
-
// Use rawSolution if available (more reliable)
|
|
114
|
-
const solText = rawSolution || output;
|
|
115
|
-
|
|
116
103
|
// Parse objective value
|
|
117
|
-
const objMatch =
|
|
104
|
+
const objMatch = output.match(/objective value:\s*([\d.e+-]+)/i);
|
|
118
105
|
if (objMatch) {
|
|
119
106
|
objective.value = parseFloat(objMatch[1]);
|
|
120
107
|
}
|
|
121
108
|
|
|
122
109
|
// Parse variable values from solution display
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
const varRegex = /^([\w$#]+)\s+([\d.e+-]+)/gm;
|
|
110
|
+
// Format: variable_name value (obj:coef)
|
|
111
|
+
const varRegex = /^(\w+)\s+([\d.e+-]+)/gm;
|
|
126
112
|
let match;
|
|
127
113
|
|
|
128
|
-
|
|
114
|
+
// Look for solution section
|
|
115
|
+
const solSection = output.split("solution:")[1] || output;
|
|
116
|
+
|
|
117
|
+
while ((match = varRegex.exec(solSection)) !== null) {
|
|
129
118
|
const name = match[1];
|
|
130
119
|
const value = parseFloat(match[2]);
|
|
131
|
-
if (!isNaN(value) && name !== "objective"
|
|
120
|
+
if (!isNaN(value) && name !== "objective") {
|
|
132
121
|
variables[name] = value;
|
|
133
122
|
}
|
|
134
123
|
}
|
|
@@ -203,17 +192,6 @@ export async function init(options = {}) {
|
|
|
203
192
|
scipModule.onStderr(text);
|
|
204
193
|
}
|
|
205
194
|
},
|
|
206
|
-
// Capture abort/exit reasons for better error messages
|
|
207
|
-
onAbort: (reason) => {
|
|
208
|
-
lastAbortReason = reason;
|
|
209
|
-
console.error("[SCIP WASM Abort]", reason);
|
|
210
|
-
},
|
|
211
|
-
onExit: (code) => {
|
|
212
|
-
lastExitCode = code;
|
|
213
|
-
if (code !== 0) {
|
|
214
|
-
console.error("[SCIP WASM Exit]", code);
|
|
215
|
-
}
|
|
216
|
-
},
|
|
217
195
|
});
|
|
218
196
|
|
|
219
197
|
// Create directories for problems, solutions, settings
|
|
@@ -303,18 +281,19 @@ export function isReady() {
|
|
|
303
281
|
* console.log(result.objective); // 1.0
|
|
304
282
|
* console.log(result.variables); // { x: 1, y: 0 }
|
|
305
283
|
*/
|
|
306
|
-
export async function solve(problem, options = {}) {
|
|
307
|
-
if (!isInitialized) {
|
|
308
|
-
await init(options);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
const {
|
|
312
|
-
format = "lp",
|
|
313
|
-
timeLimit = 3600,
|
|
314
|
-
gap = null,
|
|
315
|
-
verbose = false,
|
|
316
|
-
parameters = {},
|
|
317
|
-
|
|
284
|
+
export async function solve(problem, options = {}) {
|
|
285
|
+
if (!isInitialized) {
|
|
286
|
+
await init(options);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const {
|
|
290
|
+
format = "lp",
|
|
291
|
+
timeLimit = 3600,
|
|
292
|
+
gap = null,
|
|
293
|
+
verbose = false,
|
|
294
|
+
parameters = {},
|
|
295
|
+
initialSolution = null, // Warm start: { varName: value, ... }
|
|
296
|
+
} = options;
|
|
318
297
|
|
|
319
298
|
// Capture output
|
|
320
299
|
let stdout = "";
|
|
@@ -350,13 +329,29 @@ export async function solve(problem, options = {}) {
|
|
|
350
329
|
commands.push(`set limits gap ${gap}`);
|
|
351
330
|
}
|
|
352
331
|
|
|
353
|
-
// Custom parameters
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
332
|
+
// Custom parameters
|
|
333
|
+
// SCIP CLI format: "set category param value" (space-separated, not slash)
|
|
334
|
+
for (const [key, value] of Object.entries(parameters)) {
|
|
335
|
+
// Convert "limits/objectivelimit" to "limits objectivelimit"
|
|
336
|
+
const paramPath = key.replace(/\//g, ' ');
|
|
337
|
+
commands.push(`set ${paramPath} ${value}`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Read problem
|
|
341
|
+
commands.push(`read ${problemFile}`);
|
|
342
|
+
|
|
343
|
+
// Warm start: write and read initial solution
|
|
344
|
+
if (initialSolution && Object.keys(initialSolution).length > 0) {
|
|
345
|
+
const solLines = ['solution status: unknown'];
|
|
346
|
+
for (const [varName, value] of Object.entries(initialSolution)) {
|
|
347
|
+
if (value !== 0) {
|
|
348
|
+
solLines.push(`${varName} ${value}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const initialSolutionFile = '/solutions/initial.sol';
|
|
352
|
+
scipModule.FS.writeFile(initialSolutionFile, solLines.join('\n'));
|
|
353
|
+
commands.push(`read ${initialSolutionFile}`);
|
|
354
|
+
}
|
|
360
355
|
commands.push("optimize");
|
|
361
356
|
commands.push("display solution");
|
|
362
357
|
commands.push(`write solution ${solutionFile}`);
|
|
@@ -370,7 +365,12 @@ export async function solve(problem, options = {}) {
|
|
|
370
365
|
// Run SCIP with batch mode
|
|
371
366
|
const exitCode = scipModule.callMain(["-b", "/settings/commands.txt"]);
|
|
372
367
|
|
|
373
|
-
//
|
|
368
|
+
// Parse results
|
|
369
|
+
const status = parseStatus(stdout);
|
|
370
|
+
const { variables, objective } = parseSolution(stdout);
|
|
371
|
+
const statistics = parseStatistics(stdout);
|
|
372
|
+
|
|
373
|
+
// Try to read solution file
|
|
374
374
|
let rawSolution = null;
|
|
375
375
|
try {
|
|
376
376
|
rawSolution = scipModule.FS.readFile(solutionFile, { encoding: "utf8" });
|
|
@@ -378,11 +378,6 @@ export async function solve(problem, options = {}) {
|
|
|
378
378
|
// Solution file may not exist if infeasible
|
|
379
379
|
}
|
|
380
380
|
|
|
381
|
-
// Parse results
|
|
382
|
-
const status = parseStatus(stdout);
|
|
383
|
-
const { variables, objective } = parseSolution(stdout, rawSolution);
|
|
384
|
-
const statistics = parseStatistics(stdout);
|
|
385
|
-
|
|
386
381
|
return {
|
|
387
382
|
status,
|
|
388
383
|
objective: objective.value,
|
|
@@ -393,71 +388,22 @@ export async function solve(problem, options = {}) {
|
|
|
393
388
|
rawSolution,
|
|
394
389
|
};
|
|
395
390
|
} catch (error) {
|
|
396
|
-
// Attempt to extract detailed exception message from WASM
|
|
397
|
-
let errorMessage = error.message || String(error);
|
|
398
|
-
let exceptionInfo = null;
|
|
399
|
-
|
|
400
|
-
// Check if this is a WASM exception (pointer address)
|
|
401
|
-
if (typeof error === "number" || /^\d+$/.test(String(error))) {
|
|
402
|
-
const ptr =
|
|
403
|
-
typeof error === "number" ? error : parseInt(String(error), 10);
|
|
404
|
-
|
|
405
|
-
// Try to get exception message using Emscripten's exception handling
|
|
406
|
-
if (scipModule) {
|
|
407
|
-
try {
|
|
408
|
-
// Modern Emscripten exception handling
|
|
409
|
-
if (typeof scipModule.getExceptionMessage === "function") {
|
|
410
|
-
exceptionInfo = scipModule.getExceptionMessage(ptr);
|
|
411
|
-
errorMessage = `WASM Exception: ${exceptionInfo}`;
|
|
412
|
-
} else if (typeof scipModule.UTF8ToString === "function") {
|
|
413
|
-
// Fallback: try to read as string from memory
|
|
414
|
-
try {
|
|
415
|
-
const str = scipModule.UTF8ToString(ptr);
|
|
416
|
-
if (str && str.length > 0 && str.length < 1000) {
|
|
417
|
-
exceptionInfo = str;
|
|
418
|
-
errorMessage = `WASM Exception: ${str}`;
|
|
419
|
-
}
|
|
420
|
-
} catch (e) {
|
|
421
|
-
/* not a valid string pointer */
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
} catch (e) {
|
|
425
|
-
console.error("[SCIP] Failed to get exception message:", e);
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
if (!exceptionInfo) {
|
|
430
|
-
errorMessage = `WASM Exception (ptr: ${ptr}). Enable exception handling in build for details.`;
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
391
|
return {
|
|
435
392
|
status: Status.ERROR,
|
|
436
|
-
error:
|
|
437
|
-
errorDetails: {
|
|
438
|
-
rawError: String(error),
|
|
439
|
-
exceptionInfo,
|
|
440
|
-
abortReason: lastAbortReason,
|
|
441
|
-
exitCode: lastExitCode,
|
|
442
|
-
type: typeof error,
|
|
443
|
-
stdout: stdout,
|
|
444
|
-
stderr: stderr,
|
|
445
|
-
},
|
|
393
|
+
error: error?.message || String(error) || "Unknown error",
|
|
446
394
|
output: stdout + stderr,
|
|
447
395
|
};
|
|
448
396
|
} finally {
|
|
449
|
-
//
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
"/problems/problem.
|
|
455
|
-
"/
|
|
456
|
-
"/
|
|
457
|
-
"/
|
|
458
|
-
|
|
459
|
-
"/settings/commands.txt",
|
|
460
|
-
];
|
|
397
|
+
// Cleanup all possible problem files
|
|
398
|
+
const cleanupFiles = [
|
|
399
|
+
"/problems/problem.lp",
|
|
400
|
+
"/problems/problem.mps",
|
|
401
|
+
"/problems/problem.zpl",
|
|
402
|
+
"/problems/problem.cip",
|
|
403
|
+
"/solutions/solution.sol",
|
|
404
|
+
"/solutions/initial.sol",
|
|
405
|
+
"/settings/commands.txt",
|
|
406
|
+
];
|
|
461
407
|
for (const file of cleanupFiles) {
|
|
462
408
|
try {
|
|
463
409
|
scipModule.FS.unlink(file);
|
package/dist/types.d.ts
CHANGED
|
@@ -45,6 +45,12 @@ export interface SolveOptions extends InitOptions {
|
|
|
45
45
|
verbose?: boolean;
|
|
46
46
|
/** Additional SCIP parameters */
|
|
47
47
|
parameters?: Record<string, string | number | boolean>;
|
|
48
|
+
/**
|
|
49
|
+
* Initial solution hint for warm start
|
|
50
|
+
* Object mapping variable names to values
|
|
51
|
+
* @example { "x$sun#0": 1, "x$moon#5": 1 }
|
|
52
|
+
*/
|
|
53
|
+
initialSolution?: Record<string, number> | null;
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
/**
|