@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/api-build.log +4 -0
- package/dist/build.log +911 -0
- package/dist/index.mjs +30 -0
- package/dist/scip-api-wrapper.js +366 -0
- package/dist/scip-api.js +15 -0
- package/dist/scip-api.wasm +0 -0
- package/dist/scip.js +11 -387
- package/dist/scip.min.js +2 -2
- package/dist/types.d.ts +164 -0
- package/package.json +1 -1
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;
|