@areb0s/scip.js 1.1.9 → 1.1.11

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