docs: add project README and initial ST-BME files

This commit is contained in:
Youzini-afk
2026-03-23 03:57:59 +08:00
commit 436715216e
17 changed files with 4145 additions and 0 deletions

167
diffusion.js Normal file
View File

@@ -0,0 +1,167 @@
// ST-BME: JS 版 PEDSA 扩散激活引擎
// 从 PeroCore 的 Rust CognitiveGraphEngine 移植核心算法到纯 JS
// 适配 ST 场景(<1万节点不需要并行/SIMD
/**
* PEDSA 扩散激活引擎
*
* 算法Parallel Energy-Decay Spreading Activation
* 本质:在有向加权图上的能量传播模型
*
* 核心公式:
* E_{t+1}(j) = Σ_{i∈N(j)} E_t(i) × W_ij × D_decay
*
* 特点(保留自 PeroCore
* - 能量衰减:每步传播乘以衰减因子
* - 动态剪枝:每步只保留 Top-K 活跃节点
* - 抑制机制:特殊边类型传递负能量
* - 能量钳位:限制在 [-2.0, 2.0] 范围
*
* 与 PeroCore Rust 版的差异:
* - 无 Rayon 并行JS 单线程ST 场景不需要)
* - 无 u16 量化(直接 f64内存不是瓶颈
* - 无 SIMD普通数组运算
*/
/**
* 抑制边类型标记
*/
const INHIBIT_EDGE_TYPE = 255;
/**
* 默认配置
*/
const DEFAULT_OPTIONS = {
maxSteps: 2, // 最大扩散步数
decayFactor: 0.6, // 每步衰减因子
topK: 100, // 每步保留的最大活跃节点数
minEnergy: 0.01, // 最小有效能量(低于此值视为不活跃)
maxEnergy: 2.0, // 能量上限
minEnergy_clamp: -2.0, // 能量下限(抑制)
};
/**
* 执行 PEDSA 扩散激活
*
* @param {Map<string, Array<{targetId: string, strength: number, edgeType: number}>>} adjacencyMap
* 邻接表nodeId → [{targetId, strength, edgeType}]
* 可通过 graph.buildAdjacencyMap() 构建
*
* @param {Array<{id: string, energy: number}>} seedNodes
* 初始种子节点及其能量
* - 向量检索命中的节点energy = vectorScore (0~1)
* - 实体锚点节点energy = 2.0(最大值)
*
* @param {object} [options] - 配置选项
*
* @returns {Map<string, number>} 所有被激活节点的最终能量
* nodeId → energy正值=激活,负值=抑制)
*/
export function propagateActivation(adjacencyMap, seedNodes, options = {}) {
const opts = { ...DEFAULT_OPTIONS, ...options };
// Step 0: 初始化能量表
/** @type {Map<string, number>} */
let currentEnergy = new Map();
for (const seed of seedNodes) {
const clamped = clampEnergy(seed.energy, opts);
if (Math.abs(clamped) >= opts.minEnergy) {
currentEnergy.set(seed.id, clamped);
}
}
// 累积结果(所有步骤的最大能量)
/** @type {Map<string, number>} */
const result = new Map(currentEnergy);
// Step 1~N: 逐步扩散
for (let step = 0; step < opts.maxSteps; step++) {
/** @type {Map<string, number>} */
const nextEnergy = new Map();
// 对每个当前活跃节点,传播能量到邻居
for (const [nodeId, energy] of currentEnergy) {
const neighbors = adjacencyMap.get(nodeId);
if (!neighbors) continue;
for (const neighbor of neighbors) {
// 计算传播能量
let propagated = energy * neighbor.strength * opts.decayFactor;
// 抑制边:传递负能量
if (neighbor.edgeType === INHIBIT_EDGE_TYPE) {
propagated = -Math.abs(propagated);
}
// 累加到邻居节点
const existing = nextEnergy.get(neighbor.targetId) || 0;
nextEnergy.set(neighbor.targetId, existing + propagated);
}
}
// 钳位 + 过滤低能量
for (const [nodeId, energy] of nextEnergy) {
const clamped = clampEnergy(energy, opts);
if (Math.abs(clamped) < opts.minEnergy) {
nextEnergy.delete(nodeId);
} else {
nextEnergy.set(nodeId, clamped);
}
}
// 动态剪枝:只保留 Top-K
if (nextEnergy.size > opts.topK) {
const sorted = [...nextEnergy.entries()]
.sort((a, b) => Math.abs(b[1]) - Math.abs(a[1]));
nextEnergy.clear();
for (let i = 0; i < opts.topK && i < sorted.length; i++) {
nextEnergy.set(sorted[i][0], sorted[i][1]);
}
}
// 更新累积结果(取各步骤最大绝对值能量)
for (const [nodeId, energy] of nextEnergy) {
const existing = result.get(nodeId) || 0;
if (Math.abs(energy) > Math.abs(existing)) {
result.set(nodeId, energy);
}
}
// 准备下一步
currentEnergy = nextEnergy;
// 如果没有活跃节点了,提前终止
if (currentEnergy.size === 0) break;
}
return result;
}
/**
* 能量钳位
* @param {number} energy
* @param {object} opts
* @returns {number}
*/
function clampEnergy(energy, opts) {
return Math.max(opts.minEnergy_clamp, Math.min(opts.maxEnergy, energy));
}
/**
* 快捷方法:从种子列表创建扩散并返回按能量排序的结果
*
* @param {Map} adjacencyMap - 邻接表
* @param {Array<{id: string, energy: number}>} seeds - 种子节点
* @param {object} [options]
* @returns {Array<{nodeId: string, energy: number}>} 按能量降序排列
*/
export function diffuseAndRank(adjacencyMap, seeds, options = {}) {
const energyMap = propagateActivation(adjacencyMap, seeds, options);
return [...energyMap.entries()]
.filter(([_, energy]) => energy > 0) // 只返回正能量(被激活的)
.map(([nodeId, energy]) => ({ nodeId, energy }))
.sort((a, b) => b.energy - a.energy);
}