@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.
- package/dist/scip-core.js +14 -14
- package/dist/scip-wrapper.js +510 -448
- package/dist/scip.js +401 -327
- package/dist/scip.min.js +10 -10
- package/dist/scip.wasm +0 -0
- package/package.json +1 -1
package/dist/scip-wrapper.js
CHANGED
|
@@ -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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return Status.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
//
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
* @
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
//
|
|
321
|
-
commands
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
commands.push(`
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
//
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
//
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
+
};
|