@areb0s/scip.js 1.2.2 → 1.2.4

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/index.mjs CHANGED
@@ -51,6 +51,13 @@ export {
51
51
  terminate as terminateWorker
52
52
  } from './scip-worker-client.js';
53
53
 
54
+ // Callback API (with incumbent/node callbacks support)
55
+ export {
56
+ SCIPApi,
57
+ solveWithCallbacks,
58
+ Status as ApiStatus
59
+ } from './scip-api-wrapper.js';
60
+
54
61
  // Default export (main thread API)
55
62
  import SCIP from './scip-wrapper.js';
56
63
  export default SCIP;
@@ -71,3 +78,26 @@ export async function createWorkerSolver(options = {}) {
71
78
  terminate: worker.terminate
72
79
  };
73
80
  }
81
+
82
+ /**
83
+ * Create a callback-enabled solver instance
84
+ * Use this when you need incumbent callbacks for custom pruning logic
85
+ *
86
+ * @example
87
+ * const solver = await createCallbackSolver();
88
+ * solver.onIncumbent((objValue) => {
89
+ * console.log('New best solution:', objValue);
90
+ * });
91
+ * const result = await solver.solve(problem, {
92
+ * format: 'zpl',
93
+ * initialSolution: { x: 1, y: 0 },
94
+ * cutoff: 100
95
+ * });
96
+ * solver.destroy();
97
+ */
98
+ export async function createCallbackSolver(options = {}) {
99
+ const { SCIPApi } = await import('./scip-api-wrapper.js');
100
+ const solver = new SCIPApi();
101
+ await solver.init(options);
102
+ return solver;
103
+ }
@@ -0,0 +1,366 @@
1
+ /**
2
+ * SCIP.js API Mode - With Callback Support
3
+ *
4
+ * This module provides a callback-enabled interface to SCIP.
5
+ * Unlike the CLI mode, this allows:
6
+ * - Setting initial solutions (warm start)
7
+ * - Receiving callbacks when new incumbents are found
8
+ * - Setting cutoff bounds for pruning
9
+ *
10
+ * Usage:
11
+ * import { SCIPApi } from './scip-api-wrapper.js';
12
+ *
13
+ * const scip = new SCIPApi();
14
+ * await scip.init();
15
+ *
16
+ * // Set callback for new solutions
17
+ * scip.onIncumbent((objValue) => {
18
+ * console.log('New solution found:', objValue);
19
+ * });
20
+ *
21
+ * // Solve with initial solution hint
22
+ * const result = await scip.solve(problemZPL, {
23
+ * format: 'zpl',
24
+ * initialSolution: { x: 1, y: 0 },
25
+ * cutoff: 100 // Prune nodes worse than this
26
+ * });
27
+ */
28
+
29
+ let scipApiModule = null;
30
+ let isApiInitialized = false;
31
+ let apiInitPromise = null;
32
+
33
+ /**
34
+ * Default CDN base URL
35
+ */
36
+ const DEFAULT_CDN_BASE = "https://cdn.jsdelivr.net/npm/@areb0s/scip.js@latest/dist/";
37
+
38
+ /**
39
+ * Check if running in Node.js
40
+ */
41
+ function isNode() {
42
+ return typeof process !== 'undefined' &&
43
+ process.versions != null &&
44
+ process.versions.node != null;
45
+ }
46
+
47
+ /**
48
+ * Get base URL
49
+ */
50
+ function getBaseUrl() {
51
+ const globalScope =
52
+ (typeof globalThis !== "undefined" && globalThis) ||
53
+ (typeof self !== "undefined" && self) ||
54
+ (typeof window !== "undefined" && window) ||
55
+ {};
56
+
57
+ if (globalScope.SCIP_BASE_URL) {
58
+ return globalScope.SCIP_BASE_URL;
59
+ }
60
+
61
+ if (typeof __importMetaUrl !== "undefined" && __importMetaUrl && !__importMetaUrl.startsWith("blob:")) {
62
+ return __importMetaUrl.substring(0, __importMetaUrl.lastIndexOf("/") + 1);
63
+ }
64
+
65
+ return DEFAULT_CDN_BASE;
66
+ }
67
+
68
+ /**
69
+ * Resolve WASM path for both Node.js and browser
70
+ */
71
+ async function resolveWasmPath(inputPath) {
72
+ if (isNode()) {
73
+ const { isAbsolute } = await import('path');
74
+
75
+ // If already absolute, return as-is
76
+ if (isAbsolute(inputPath)) {
77
+ return inputPath;
78
+ }
79
+
80
+ // For Node.js, resolve relative paths to absolute file path
81
+ const { fileURLToPath } = await import('url');
82
+ const { dirname, resolve } = await import('path');
83
+
84
+ // Get the directory of this module
85
+ const __filename = fileURLToPath(import.meta.url);
86
+ const __dirname = dirname(__filename);
87
+
88
+ // Resolve relative to this module
89
+ return resolve(__dirname, inputPath);
90
+ }
91
+ // For browser, return URL as-is
92
+ return inputPath;
93
+ }
94
+
95
+ /**
96
+ * Solution status enum
97
+ */
98
+ export const Status = {
99
+ OPTIMAL: "optimal",
100
+ INFEASIBLE: "infeasible",
101
+ UNBOUNDED: "unbounded",
102
+ TIME_LIMIT: "timelimit",
103
+ UNKNOWN: "unknown",
104
+ ERROR: "error",
105
+ };
106
+
107
+ /**
108
+ * SCIP API class with callback support
109
+ */
110
+ export class SCIPApi {
111
+ constructor() {
112
+ this._module = null;
113
+ this._incumbentCallback = null;
114
+ this._nodeCallback = null;
115
+ this._isInitialized = false;
116
+ }
117
+
118
+ /**
119
+ * Initialize SCIP API module
120
+ */
121
+ async init(options = {}) {
122
+ if (this._isInitialized) {
123
+ return;
124
+ }
125
+
126
+ const baseUrl = getBaseUrl();
127
+ let wasmPath = options.wasmPath || baseUrl + "scip-api.wasm";
128
+
129
+ // Resolve WASM path for Node.js
130
+ wasmPath = await resolveWasmPath(wasmPath);
131
+
132
+ // Dynamic import of the API module
133
+ const createSCIPAPI = (await import("./scip-api.js")).default;
134
+
135
+ // Build module options
136
+ const moduleOptions = {
137
+ locateFile: (path) => {
138
+ if (path.endsWith(".wasm")) {
139
+ return wasmPath;
140
+ }
141
+ // For other files, resolve relative to base URL
142
+ if (isNode()) {
143
+ return wasmPath.replace(/scip-api\.wasm$/, path);
144
+ }
145
+ return baseUrl + path;
146
+ }
147
+ };
148
+
149
+ // For Node.js, provide instantiateWasm to load WASM from file
150
+ if (isNode()) {
151
+ const { readFileSync } = await import('fs');
152
+ moduleOptions.wasmBinary = readFileSync(wasmPath);
153
+ }
154
+
155
+ this._module = await createSCIPAPI(moduleOptions);
156
+
157
+ // Create SCIP instance
158
+ const created = this._module._scip_create();
159
+ if (!created) {
160
+ throw new Error("Failed to create SCIP instance");
161
+ }
162
+
163
+ // Setup callbacks
164
+ this._module.onIncumbent = (objValue) => {
165
+ if (this._incumbentCallback) {
166
+ this._incumbentCallback(objValue);
167
+ }
168
+ };
169
+
170
+ this._module.onNode = (dualBound, primalBound, nodes) => {
171
+ if (this._nodeCallback) {
172
+ this._nodeCallback({ dualBound, primalBound, nodes });
173
+ }
174
+ };
175
+
176
+ // Create virtual filesystem directories
177
+ try { this._module.FS.mkdir("/problems"); } catch (e) { /* exists */ }
178
+ try { this._module.FS.mkdir("/solutions"); } catch (e) { /* exists */ }
179
+
180
+ this._isInitialized = true;
181
+ }
182
+
183
+ /**
184
+ * Set callback for new incumbent solutions
185
+ * @param {Function} callback - (objValue: number) => void
186
+ */
187
+ onIncumbent(callback) {
188
+ this._incumbentCallback = callback;
189
+ if (this._module) {
190
+ this._module._scip_enable_incumbent_callback(callback ? 1 : 0);
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Set callback for node processing (for progress tracking)
196
+ * @param {Function} callback - ({dualBound, primalBound, nodes}) => void
197
+ */
198
+ onNode(callback) {
199
+ this._nodeCallback = callback;
200
+ if (this._module) {
201
+ this._module._scip_enable_node_callback(callback ? 1 : 0);
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Solve an optimization problem
207
+ * @param {string} problem - Problem definition
208
+ * @param {Object} options - Solver options
209
+ * @param {string} options.format - 'lp', 'mps', 'zpl', 'cip'
210
+ * @param {number} options.timeLimit - Time limit in seconds
211
+ * @param {number} options.gap - Relative gap tolerance
212
+ * @param {Object} options.initialSolution - Initial solution hint {varName: value}
213
+ * @param {number} options.cutoff - Cutoff bound for pruning
214
+ * @returns {Promise<Object>} Solution
215
+ */
216
+ async solve(problem, options = {}) {
217
+ if (!this._isInitialized) {
218
+ await this.init(options);
219
+ }
220
+
221
+ const {
222
+ format = "lp",
223
+ timeLimit = 3600,
224
+ gap = null,
225
+ initialSolution = null,
226
+ cutoff = null,
227
+ } = options;
228
+
229
+ // Reset for new problem
230
+ this._module._scip_reset();
231
+
232
+ // Write problem file
233
+ const formatExtMap = { mps: "mps", zpl: "zpl", cip: "cip", lp: "lp" };
234
+ const ext = formatExtMap[format] || "lp";
235
+ const problemFile = `/problems/problem.${ext}`;
236
+ this._module.FS.writeFile(problemFile, problem);
237
+
238
+ // Read problem
239
+ const problemFilePtr = this._module.allocateUTF8(problemFile);
240
+ const readOk = this._module._scip_read_problem(problemFilePtr);
241
+ this._module._free(problemFilePtr);
242
+
243
+ if (!readOk) {
244
+ return {
245
+ status: Status.ERROR,
246
+ error: "Failed to read problem",
247
+ };
248
+ }
249
+
250
+ // Set parameters
251
+ this._module._scip_set_time_limit(timeLimit);
252
+
253
+ if (gap !== null) {
254
+ this._module._scip_set_gap(gap);
255
+ }
256
+
257
+ if (cutoff !== null) {
258
+ this._module._scip_set_cutoff(cutoff);
259
+ }
260
+
261
+ // Add initial solution hint
262
+ if (initialSolution !== null) {
263
+ const solutionStr = Object.entries(initialSolution)
264
+ .map(([name, value]) => `${name}=${value}`)
265
+ .join(";");
266
+
267
+ const solutionPtr = this._module.allocateUTF8(solutionStr);
268
+ this._module._scip_add_solution_hint(solutionPtr);
269
+ this._module._free(solutionPtr);
270
+ }
271
+
272
+ // Enable callbacks if registered
273
+ this._module._scip_enable_incumbent_callback(this._incumbentCallback ? 1 : 0);
274
+ this._module._scip_enable_node_callback(this._nodeCallback ? 1 : 0);
275
+
276
+ // Solve
277
+ const statusCode = this._module._scip_solve();
278
+
279
+ // Map status
280
+ const statusMap = {
281
+ 0: Status.OPTIMAL,
282
+ 1: Status.INFEASIBLE,
283
+ 2: Status.UNBOUNDED,
284
+ 3: Status.TIME_LIMIT,
285
+ 4: Status.UNKNOWN,
286
+ [-1]: Status.ERROR,
287
+ };
288
+ const status = statusMap[statusCode] || Status.UNKNOWN;
289
+
290
+ // Get results
291
+ const objective = this._module._scip_get_objective();
292
+ const solvingTime = this._module._scip_get_solving_time();
293
+ const nodes = this._module._scip_get_nnodes();
294
+ const finalGap = this._module._scip_get_gap();
295
+ const dualBound = this._module._scip_get_dual_bound();
296
+ const primalBound = this._module._scip_get_primal_bound();
297
+
298
+ // Get variable values
299
+ const variables = {};
300
+ const varNamesPtr = this._module._scip_get_var_names();
301
+ const varNamesStr = this._module.UTF8ToString(varNamesPtr);
302
+
303
+ if (varNamesStr) {
304
+ const varNames = varNamesStr.split(",");
305
+ for (const name of varNames) {
306
+ if (name) {
307
+ const namePtr = this._module.allocateUTF8(name);
308
+ variables[name] = this._module._scip_get_var_value(namePtr);
309
+ this._module._free(namePtr);
310
+ }
311
+ }
312
+ }
313
+
314
+ // Cleanup
315
+ try { this._module.FS.unlink(problemFile); } catch (e) { /* ignore */ }
316
+
317
+ return {
318
+ status,
319
+ objective,
320
+ variables,
321
+ statistics: {
322
+ solvingTime,
323
+ nodes,
324
+ gap: finalGap,
325
+ dualBound,
326
+ primalBound,
327
+ },
328
+ };
329
+ }
330
+
331
+ /**
332
+ * Free SCIP resources
333
+ */
334
+ destroy() {
335
+ if (this._module) {
336
+ this._module._scip_free();
337
+ this._module = null;
338
+ this._isInitialized = false;
339
+ }
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Convenience function - solve with callbacks
345
+ */
346
+ export async function solveWithCallbacks(problem, options = {}) {
347
+ const scip = new SCIPApi();
348
+
349
+ try {
350
+ await scip.init(options);
351
+
352
+ if (options.onIncumbent) {
353
+ scip.onIncumbent(options.onIncumbent);
354
+ }
355
+
356
+ if (options.onNode) {
357
+ scip.onNode(options.onNode);
358
+ }
359
+
360
+ return await scip.solve(problem, options);
361
+ } finally {
362
+ scip.destroy();
363
+ }
364
+ }
365
+
366
+ export default SCIPApi;