diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 396825e..612da93 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -26,7 +26,7 @@ jobs: - os: macos-latest platform: darwin arch: arm64 - - os: windows-latest + - os: windows-2022 platform: win32 arch: x64 diff --git a/lib/index.d.ts b/lib/index.d.ts index a8936fd..ccd3294 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -361,6 +361,50 @@ export function readEdgeList( commentPrefix?: string, ): Graph; +// ── Global control (mirror networkit.setNumberOfThreads / setSeed) ─────────── + +/** + * Set the number of OpenMP threads that the next parallel region will use. + * Observe the effect via `getMaxNumberOfThreads`. + * Mirrors `networkit.setNumberOfThreads`. + */ +export function setNumberOfThreads(n: number): void; + +/** + * Thread count the next parallel region will use — i.e. the value last set by + * `setNumberOfThreads`, or the OpenMP hardware default before any such call. + * Mirrors `networkit.getMaxNumberOfThreads`. + */ +export function getMaxNumberOfThreads(): number; + +/** + * Number of threads in the *current* OpenMP parallel region. This is + * `omp_get_num_threads`, so it returns 1 when called from JS (outside any + * parallel region). Use `getMaxNumberOfThreads` to read the configured count. + * Mirrors `networkit.getCurrentNumberOfThreads`. + */ +export function getCurrentNumberOfThreads(): number; + +/** + * Seed NetworKit's global RNG. + * + * @param seed The seed value. + * @param useThreadId When true, additionally mix the per-thread id into the + * seed so each thread gets a distinct stream. This is the + * source ParallelLeiden's `randomize` step draws from. + * Defaults to false. + * Mirrors `networkit.setSeed`. + */ +export function setSeed(seed: number | bigint, useThreadId?: boolean): void; + +/** + * Returns the high-quality random seed currently in use. + * + * Returned as a BigInt because the underlying value is a uint64_t and may + * exceed 2^53. + */ +export function getSeed(): bigint; + // ── JS helpers ──────────────────────────────────────────────────────────────── /** diff --git a/lib/index.js b/lib/index.js index bfd5da3..54b90c3 100644 --- a/lib/index.js +++ b/lib/index.js @@ -51,6 +51,11 @@ const { Leiden, readMETIS, readEdgeList, + setNumberOfThreads, + getMaxNumberOfThreads, + getCurrentNumberOfThreads, + setSeed, + getSeed, } = native; const sharedLibraryExtension = @@ -138,6 +143,13 @@ module.exports = { readMETIS, readEdgeList, + // Global control (mirror networkit.setNumberOfThreads / setSeed) + setNumberOfThreads, + getMaxNumberOfThreads, + getCurrentNumberOfThreads, + setSeed, + getSeed, + // JS helpers graphFromEdges, weightedGraphFromEdges, diff --git a/src/addon.cpp b/src/addon.cpp index b015d9f..14e4aec 100644 --- a/src/addon.cpp +++ b/src/addon.cpp @@ -14,6 +14,13 @@ * Exposed JS functions: * readMETIS(path) → Graph * readEdgeList(path, sep, firstNode, directed, commentPrefix) → Graph + * + * Global control (mirror networkit.setNumberOfThreads / setSeed): + * setNumberOfThreads(n) + * getMaxNumberOfThreads() + * getCurrentNumberOfThreads() + * setSeed(seed [, useThreadId]) + * getSeed() */ #include @@ -30,6 +37,8 @@ #include #include #include +#include +#include #include #include @@ -1253,6 +1262,122 @@ static Napi::Value ReadEdgeList(const Napi::CallbackInfo &info) { } } +// ============================================================================ +// Free functions: global thread / seed control +// +// These mirror the top-level networkit.* helpers in the Python bindings: +// nk.setNumberOfThreads(n) +// nk.getMaxNumberOfThreads() +// nk.getCurrentNumberOfThreads() +// nk.setSeed(seed, useThreadId) +// ============================================================================ + +/** + * JS: setNumberOfThreads(n) + * + * Sets the number of OpenMP threads that the next parallel region will use + * (maps to omp_set_num_threads via Aux::setNumberOfThreads). Observe the + * effect via getMaxNumberOfThreads(). + */ +static Napi::Value SetNumberOfThreads(const Napi::CallbackInfo &info) { + if (info.Length() < 1) { + Napi::TypeError::New(info.Env(), "setNumberOfThreads(n)") + .ThrowAsJavaScriptException(); + return info.Env().Undefined(); + } + try { + int n = static_cast(info[0].As().Int32Value()); + Aux::setNumberOfThreads(n); + } catch (const std::exception &e) { + throwError(info.Env(), e); + } + return info.Env().Undefined(); +} + +/** + * JS: getMaxNumberOfThreads() → number + * + * Returns the thread count the next parallel region will use — i.e. the value + * last set by setNumberOfThreads, or the OpenMP hardware default before any + * such call (maps to omp_get_max_threads via Aux::getMaxNumberOfThreads). + */ +static Napi::Value GetMaxNumberOfThreads(const Napi::CallbackInfo &info) { + try { + return Napi::Number::New(info.Env(), + static_cast(Aux::getMaxNumberOfThreads())); + } catch (const std::exception &e) { + throwError(info.Env(), e); + return info.Env().Undefined(); + } +} + +/** + * JS: getCurrentNumberOfThreads() → number + * + * Returns the number of threads executing the *current* OpenMP parallel region. + * This is omp_get_num_threads, so it returns 1 when called from JS (outside any + * parallel region). Use getMaxNumberOfThreads() to read the configured count. + */ +static Napi::Value GetCurrentNumberOfThreads(const Napi::CallbackInfo &info) { + try { + return Napi::Number::New(info.Env(), + static_cast(Aux::getCurrentNumberOfThreads())); + } catch (const std::exception &e) { + throwError(info.Env(), e); + return info.Env().Undefined(); + } +} + +/** + * JS: setSeed(seed [, useThreadId=false]) + * + * Seeds NetworKit's global RNG. The second argument, when true, additionally + * mixes the per-thread id into the seed so each thread sees a distinct stream + * (this is what ParallelLeiden's randomize step draws from). + */ +static Napi::Value SetSeed(const Napi::CallbackInfo &info) { + if (info.Length() < 1) { + Napi::TypeError::New(info.Env(), "setSeed(seed [, useThreadId])") + .ThrowAsJavaScriptException(); + return info.Env().Undefined(); + } + try { + uint64_t seed = 0; + if (info[0].IsNumber()) { + seed = static_cast(info[0].As().Int64Value()); + } else if (info[0].IsBigInt()) { + bool lossless = true; + seed = info[0].As().Uint64Value(&lossless); + } else { + Napi::TypeError::New(info.Env(), + "setSeed: seed must be a number or BigInt") + .ThrowAsJavaScriptException(); + return info.Env().Undefined(); + } + bool useThreadId = info.Length() >= 2 && info[1].As(); + Aux::Random::setSeed(seed, useThreadId); + } catch (const std::exception &e) { + throwError(info.Env(), e); + } + return info.Env().Undefined(); +} + +/** + * JS: getSeed() → BigInt + * + * Returns the high-quality random seed currently in use. Mirrors + * Aux::Random::getSeed(); returned as a BigInt because the seed is a uint64_t + * and may exceed 2^53. + */ +static Napi::Value GetSeed(const Napi::CallbackInfo &info) { + try { + return Napi::BigInt::New(info.Env(), Aux::Random::getSeed()); + } catch (const std::exception &e) { + throwError(info.Env(), e); + return info.Env().Undefined(); + } +} + // ============================================================================ // Module entry point // ============================================================================ @@ -1272,6 +1397,13 @@ static Napi::Object InitModule(Napi::Env env, Napi::Object exports) { exports.Set("readMETIS", Napi::Function::New(env, ReadMETIS)); exports.Set("readEdgeList", Napi::Function::New(env, ReadEdgeList)); + // Global thread / RNG control (mirror networkit.setNumberOfThreads / setSeed) + exports.Set("setNumberOfThreads", Napi::Function::New(env, SetNumberOfThreads)); + exports.Set("getMaxNumberOfThreads", Napi::Function::New(env, GetMaxNumberOfThreads)); + exports.Set("getCurrentNumberOfThreads", Napi::Function::New(env, GetCurrentNumberOfThreads)); + exports.Set("setSeed", Napi::Function::New(env, SetSeed)); + exports.Set("getSeed", Napi::Function::New(env, GetSeed)); + return exports; } diff --git a/test/basic.test.js b/test/basic.test.js index 1c08078..0adb9f7 100644 --- a/test/basic.test.js +++ b/test/basic.test.js @@ -21,6 +21,11 @@ const { parallelLeidenScoringExtensionPath, readMETIS, readEdgeList, + setNumberOfThreads, + getMaxNumberOfThreads, + getCurrentNumberOfThreads, + setSeed, + getSeed, graphFromEdges, weightedGraphFromEdges, } = require('../lib/index.js'); @@ -540,6 +545,85 @@ test('Leiden: runs on jazz.graph', () => { console.log(` jazz.graph Leiden: ${n} communities, modularity=${Q.toFixed(4)}`); }); +// ── Thread / seed control ──────────────────────────────────────────────────── +console.log('\nThreads / seed'); + +test('getMaxNumberOfThreads returns a positive integer', () => { + const n = getMaxNumberOfThreads(); + assert.ok(Number.isInteger(n) && n > 0, `expected positive int, got ${n}`); +}); + +test('setNumberOfThreads controls getMaxNumberOfThreads', () => { + // NetworKit's setNumberOfThreads maps to omp_set_num_threads, which sets the + // thread count for the *next* parallel region — observable via + // getMaxNumberOfThreads() (omp_get_max_threads). getCurrentNumberOfThreads() + // (omp_get_num_threads) returns 1 outside an active parallel region. + const before = getMaxNumberOfThreads(); + setNumberOfThreads(2); + assert.strictEqual(getMaxNumberOfThreads(), 2); + // restore the original count so subsequent tests are unaffected + setNumberOfThreads(before); + assert.strictEqual(getMaxNumberOfThreads(), before); +}); + +test('getCurrentNumberOfThreads is 1 outside a parallel region', () => { + // omp_get_num_threads() returns 1 when no OpenMP parallel region is active. + assert.strictEqual(getCurrentNumberOfThreads(), 1); +}); + +test('setNumberOfThreads can be set above the hardware max', () => { + // omp_set_num_threads does not clamp; the requested count is reported back + // verbatim by getMaxNumberOfThreads regardless of the hardware limit. + const max = getMaxNumberOfThreads(); + setNumberOfThreads(max * 4); + assert.strictEqual(getMaxNumberOfThreads(), max * 4); + setNumberOfThreads(max); // restore +}); + +test('setSeed accepts a number seed', () => { + assert.doesNotThrow(() => setSeed(42, false)); +}); + +test('setSeed accepts a BigInt seed', () => { + // 2^53 + 1 — out of safe-integer range for Number, fine for BigInt + assert.doesNotThrow(() => setSeed(9007199254740993n, false)); +}); + +test('getSeed returns a BigInt', () => { + setSeed(42, false); + const s = getSeed(); + assert.strictEqual(typeof s, 'bigint'); + // After seeding, the in-use seed should be deterministic. + const s2 = getSeed(); + assert.strictEqual(s, s2); +}); + +test('setSeed with different seeds produces different RNG streams', () => { + // Run Leiden twice with different seeds; the randomised node order means + // the first node's community assignment is very likely to differ between + // the two runs on a symmetric graph (it is not guaranteed, but the seed + // controls the stream that randomize draws from). + const g = buildTwoCliquesGraph(); + + setSeed(1, false); + const a = new Leiden(g, 10, true); + a.run(); + const partA = a.getPartition().membership; + + setSeed(2, false); + const b = new Leiden(g, 10, true); + b.run(); + const partB = b.getPartition().membership; + + // The *partition quality* (number of communities) should be stable for + // this well-separated graph regardless of seed. + assert.strictEqual(a.numberOfCommunities(), 2); + assert.strictEqual(b.numberOfCommunities(), 2); + assert.ok(partA instanceof Float64Array); + assert.ok(partB instanceof Float64Array); + assert.strictEqual(partA.length, partB.length); +}); + // ── Summary ────────────────────────────────────────────────────────────────── console.log(`\n${'─'.repeat(50)}`); console.log(`Results: ${passed} passed, ${failed} failed`);