Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build-and-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
- os: macos-latest
platform: darwin
arch: arm64
- os: windows-latest
- os: windows-2022
platform: win32
arch: x64

Expand Down
44 changes: 44 additions & 0 deletions lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ────────────────────────────────────────────────────────────────

/**
Expand Down
12 changes: 12 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ const {
Leiden,
readMETIS,
readEdgeList,
setNumberOfThreads,
getMaxNumberOfThreads,
getCurrentNumberOfThreads,
setSeed,
getSeed,
} = native;

const sharedLibraryExtension =
Expand Down Expand Up @@ -138,6 +143,13 @@ module.exports = {
readMETIS,
readEdgeList,

// Global control (mirror networkit.setNumberOfThreads / setSeed)
setNumberOfThreads,
getMaxNumberOfThreads,
getCurrentNumberOfThreads,
setSeed,
getSeed,

// JS helpers
graphFromEdges,
weightedGraphFromEdges,
Expand Down
132 changes: 132 additions & 0 deletions src/addon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <napi.h>
Expand All @@ -30,6 +37,8 @@
#include <networkit/structures/Partition.hpp>
#include <networkit/io/EdgeListReader.hpp>
#include <networkit/io/METISGraphReader.hpp>
#include <networkit/auxiliary/Parallelism.hpp>
#include <networkit/auxiliary/Random.hpp>

#include <map>
#include <memory>
Expand Down Expand Up @@ -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<int>(info[0].As<Napi::Number>().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<double>(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<double>(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<uint64_t>(info[0].As<Napi::Number>().Int64Value());
} else if (info[0].IsBigInt()) {
bool lossless = true;
seed = info[0].As<Napi::BigInt>().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<Napi::Boolean>();
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
// ============================================================================
Expand All @@ -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;
}

Expand Down
84 changes: 84 additions & 0 deletions test/basic.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ const {
parallelLeidenScoringExtensionPath,
readMETIS,
readEdgeList,
setNumberOfThreads,
getMaxNumberOfThreads,
getCurrentNumberOfThreads,
setSeed,
getSeed,
graphFromEdges,
weightedGraphFromEdges,
} = require('../lib/index.js');
Expand Down Expand Up @@ -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`);
Expand Down
Loading