@areb0s/scip.js 1.0.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.
@@ -0,0 +1,429 @@
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
+ */
92
+ function parseSolution(output) {
93
+ const variables = {};
94
+ const objective = { value: null, sense: null };
95
+
96
+ // Parse objective value
97
+ const objMatch = output.match(/objective value:\s*([\d.e+-]+)/i);
98
+ if (objMatch) {
99
+ objective.value = parseFloat(objMatch[1]);
100
+ }
101
+
102
+ // Parse variable values from solution display
103
+ // Format: variable_name value (obj:coef)
104
+ const varRegex = /^(\w+)\s+([\d.e+-]+)/gm;
105
+ let match;
106
+
107
+ // Look for solution section
108
+ const solSection = output.split('solution:')[1] || output;
109
+
110
+ while ((match = varRegex.exec(solSection)) !== null) {
111
+ const name = match[1];
112
+ const value = parseFloat(match[2]);
113
+ if (!isNaN(value) && name !== 'objective') {
114
+ variables[name] = value;
115
+ }
116
+ }
117
+
118
+ return { variables, objective };
119
+ }
120
+
121
+ /**
122
+ * Parse statistics from SCIP output
123
+ */
124
+ function parseStatistics(output) {
125
+ const stats = {
126
+ solvingTime: null,
127
+ nodes: null,
128
+ iterations: null,
129
+ gap: null
130
+ };
131
+
132
+ const timeMatch = output.match(/Solving Time \(sec\)\s*:\s*([\d.]+)/);
133
+ if (timeMatch) stats.solvingTime = parseFloat(timeMatch[1]);
134
+
135
+ const nodesMatch = output.match(/Nodes\s*:\s*(\d+)/);
136
+ if (nodesMatch) stats.nodes = parseInt(nodesMatch[1]);
137
+
138
+ const iterMatch = output.match(/LP Iterations\s*:\s*(\d+)/);
139
+ if (iterMatch) stats.iterations = parseInt(iterMatch[1]);
140
+
141
+ const gapMatch = output.match(/Gap\s*:\s*([\d.]+)\s*%/);
142
+ if (gapMatch) stats.gap = parseFloat(gapMatch[1]);
143
+
144
+ return stats;
145
+ }
146
+
147
+ /**
148
+ * Initialize SCIP WASM module
149
+ * @param {Object} options - Initialization options
150
+ * @param {string} options.wasmPath - Path to scip.wasm file
151
+ * @returns {Promise<void>}
152
+ */
153
+ export async function init(options = {}) {
154
+ if (isInitialized) {
155
+ return;
156
+ }
157
+ if (initPromise) {
158
+ return initPromise;
159
+ }
160
+
161
+ initPromise = (async () => {
162
+ try {
163
+ // Auto-detect wasmPath from SCIP_BASE_URL or script location
164
+ const baseUrl = getBaseUrl();
165
+ const wasmPath = options.wasmPath || (baseUrl + 'scip.wasm');
166
+
167
+ // Dynamic import of the Emscripten-generated module
168
+ const createSCIP = (await import('./scip-core.js')).default;
169
+
170
+ scipModule = await createSCIP({
171
+ locateFile: (path) => {
172
+ if (path.endsWith('.wasm')) {
173
+ return wasmPath;
174
+ }
175
+ return path;
176
+ },
177
+ // Capture stdout/stderr from Emscripten
178
+ print: (text) => {
179
+ if (scipModule && scipModule.onStdout) {
180
+ scipModule.onStdout(text);
181
+ }
182
+ },
183
+ printErr: (text) => {
184
+ if (scipModule && scipModule.onStderr) {
185
+ scipModule.onStderr(text);
186
+ }
187
+ }
188
+ });
189
+
190
+ // Create directories for problems, solutions, settings
191
+ if (scipModule.FS) {
192
+ try { scipModule.FS.mkdir('/problems'); } catch (e) { /* exists */ }
193
+ try { scipModule.FS.mkdir('/solutions'); } catch (e) { /* exists */ }
194
+ try { scipModule.FS.mkdir('/settings'); } catch (e) { /* exists */ }
195
+ }
196
+
197
+ isInitialized = true;
198
+
199
+ // Resolve ready promise
200
+ if (readyResolve) {
201
+ readyResolve();
202
+ }
203
+ } catch (error) {
204
+ if (readyReject) {
205
+ readyReject(error);
206
+ }
207
+ throw error;
208
+ }
209
+ })();
210
+
211
+ return initPromise;
212
+ }
213
+
214
+ /**
215
+ * Check if SCIP is initialized
216
+ * @returns {boolean}
217
+ */
218
+ export function isReady() {
219
+ return isInitialized;
220
+ }
221
+
222
+ /**
223
+ * Solve an optimization problem
224
+ *
225
+ * @param {string} problem - Problem in LP, MPS, or ZIMPL format
226
+ * @param {Object} options - Solver options
227
+ * @param {string} options.format - Input format: 'lp', 'mps', 'zpl' (default: 'lp')
228
+ * @param {number} options.timeLimit - Time limit in seconds
229
+ * @param {number} options.gap - Relative gap for MIP (e.g., 0.01 for 1%)
230
+ * @param {boolean} options.verbose - Enable verbose output
231
+ * @param {Object} options.parameters - Additional SCIP parameters
232
+ * @returns {Promise<Object>} Solution object
233
+ *
234
+ * @example
235
+ * const result = await solve(`
236
+ * Minimize obj: x + 2 y
237
+ * Subject To
238
+ * c1: x + y >= 1
239
+ * Bounds
240
+ * 0 <= x <= 10
241
+ * 0 <= y <= 10
242
+ * End
243
+ * `);
244
+ *
245
+ * console.log(result.status); // 'optimal'
246
+ * console.log(result.objective); // 1.0
247
+ * console.log(result.variables); // { x: 1, y: 0 }
248
+ */
249
+ export async function solve(problem, options = {}) {
250
+ if (!isInitialized) {
251
+ await init(options);
252
+ }
253
+
254
+ const {
255
+ format = 'lp',
256
+ timeLimit = 3600,
257
+ gap = null,
258
+ verbose = false,
259
+ parameters = {}
260
+ } = options;
261
+
262
+ // Capture output
263
+ let stdout = '';
264
+ let stderr = '';
265
+
266
+ scipModule.onStdout = (text) => {
267
+ stdout += text + '\n';
268
+ if (verbose) console.log('[SCIP]', text);
269
+ };
270
+
271
+ scipModule.onStderr = (text) => {
272
+ stderr += text + '\n';
273
+ if (verbose) console.error('[SCIP Error]', text);
274
+ };
275
+
276
+ try {
277
+ // Determine file extension
278
+ const ext = format === 'mps' ? 'mps' : format === 'zpl' ? 'zpl' : 'lp';
279
+ const problemFile = `/problems/problem.${ext}`;
280
+ const solutionFile = '/solutions/solution.sol';
281
+
282
+ // Write problem to virtual filesystem
283
+ scipModule.FS.writeFile(problemFile, problem);
284
+
285
+ // Build SCIP command
286
+ const commands = [];
287
+
288
+ // Set parameters
289
+ commands.push(`set limits time ${timeLimit}`);
290
+
291
+ if (gap !== null) {
292
+ commands.push(`set limits gap ${gap}`);
293
+ }
294
+
295
+ // Custom parameters
296
+ for (const [key, value] of Object.entries(parameters)) {
297
+ commands.push(`set ${key} ${value}`);
298
+ }
299
+
300
+ // Read and solve
301
+ commands.push(`read ${problemFile}`);
302
+ commands.push('optimize');
303
+ commands.push('display solution');
304
+ commands.push(`write solution ${solutionFile}`);
305
+ commands.push('display statistics');
306
+ commands.push('quit');
307
+
308
+ // Write settings file
309
+ const settingsContent = commands.join('\n');
310
+ scipModule.FS.writeFile('/settings/commands.txt', settingsContent);
311
+
312
+ // Run SCIP with batch mode
313
+ const exitCode = scipModule.callMain(['-b', '/settings/commands.txt']);
314
+
315
+ // Parse results
316
+ const status = parseStatus(stdout);
317
+ const { variables, objective } = parseSolution(stdout);
318
+ const statistics = parseStatistics(stdout);
319
+
320
+ // Try to read solution file
321
+ let rawSolution = null;
322
+ try {
323
+ rawSolution = scipModule.FS.readFile(solutionFile, { encoding: 'utf8' });
324
+ } catch (e) {
325
+ // Solution file may not exist if infeasible
326
+ }
327
+
328
+ return {
329
+ status,
330
+ objective: objective.value,
331
+ variables,
332
+ statistics,
333
+ exitCode,
334
+ output: verbose ? stdout : undefined,
335
+ rawSolution
336
+ };
337
+
338
+ } catch (error) {
339
+ return {
340
+ status: Status.ERROR,
341
+ error: error.message,
342
+ output: stdout + stderr
343
+ };
344
+ } finally {
345
+ // Cleanup
346
+ try {
347
+ scipModule.FS.unlink('/problems/problem.lp');
348
+ } catch (e) {}
349
+ try {
350
+ scipModule.FS.unlink('/problems/problem.mps');
351
+ } catch (e) {}
352
+ try {
353
+ scipModule.FS.unlink('/solutions/solution.sol');
354
+ } catch (e) {}
355
+ try {
356
+ scipModule.FS.unlink('/settings/commands.txt');
357
+ } catch (e) {}
358
+ }
359
+ }
360
+
361
+ /**
362
+ * Solve a minimization problem
363
+ * Convenience wrapper that ensures minimization
364
+ */
365
+ export async function minimize(problem, options = {}) {
366
+ // LP format uses "Minimize" keyword, ensure it's present
367
+ if (!problem.toLowerCase().includes('minimize')) {
368
+ problem = 'Minimize\n' + problem;
369
+ }
370
+ return solve(problem, options);
371
+ }
372
+
373
+ /**
374
+ * Solve a maximization problem
375
+ * Convenience wrapper that ensures maximization
376
+ */
377
+ export async function maximize(problem, options = {}) {
378
+ // LP format uses "Maximize" keyword
379
+ if (!problem.toLowerCase().includes('maximize')) {
380
+ problem = 'Maximize\n' + problem;
381
+ }
382
+ return solve(problem, options);
383
+ }
384
+
385
+ /**
386
+ * Get SCIP version info
387
+ */
388
+ export async function version() {
389
+ if (!isInitialized) {
390
+ await init();
391
+ }
392
+
393
+ let output = '';
394
+ scipModule.onStdout = (text) => { output += text + '\n'; };
395
+
396
+ scipModule.callMain(['--version']);
397
+
398
+ return output.trim();
399
+ }
400
+
401
+ /**
402
+ * Get available SCIP parameters
403
+ */
404
+ export async function getParameters() {
405
+ if (!isInitialized) {
406
+ await init();
407
+ }
408
+
409
+ let output = '';
410
+ scipModule.onStdout = (text) => { output += text + '\n'; };
411
+
412
+ scipModule.FS.writeFile('/settings/params.txt', 'set\nquit\n');
413
+ scipModule.callMain(['-b', '/settings/params.txt']);
414
+
415
+ return output;
416
+ }
417
+
418
+ // Default export
419
+ export default {
420
+ init,
421
+ ready,
422
+ isReady,
423
+ solve,
424
+ minimize,
425
+ maximize,
426
+ version,
427
+ getParameters,
428
+ Status
429
+ };