Files
2026-04-08 01:17:57 +08:00

206 lines
6.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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, // 能量下限(抑制)
teleportAlpha: 0.0, // PPR 回拉概率
inhibitMultiplier: 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 };
const teleportAlpha = clamp01(opts.teleportAlpha);
/** @type {Map<string, number>} */
let currentEnergy = new Map();
/** @type {Map<string, number>} */
const initialEnergy = new Map();
for (const seed of seedNodes || []) {
if (!seed?.id) continue;
const clamped = clampEnergy(Number(seed.energy) || 0, opts);
if (Math.abs(clamped) >= opts.minEnergy) {
const existing = currentEnergy.get(seed.id) || 0;
const next = clampEnergy(existing + clamped, opts);
currentEnergy.set(seed.id, next);
initialEnergy.set(seed.id, next);
}
}
// 累积结果(所有步骤的最大能量)
/** @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 (!Array.isArray(neighbors) || neighbors.length === 0) continue;
for (const neighbor of neighbors) {
if (!neighbor?.targetId) continue;
let propagated =
energy *
(Number(neighbor.strength) || 0) *
opts.decayFactor *
(1 - teleportAlpha);
// 抑制边:传递负能量
if (neighbor.edgeType === INHIBIT_EDGE_TYPE) {
propagated =
-Math.abs(energy) *
(Number(neighbor.strength) || 0) *
opts.decayFactor *
(Number(opts.inhibitMultiplier) || 1);
}
// 累加到邻居节点
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);
}
}
if (teleportAlpha > 0) {
for (const [nodeId, seedEnergy] of initialEnergy) {
const current = nextEnergy.get(nodeId) || 0;
const teleported =
(1 - teleportAlpha) * current + teleportAlpha * seedEnergy;
const clamped = clampEnergy(teleported, opts);
if (Math.abs(clamped) >= opts.minEnergy) {
nextEnergy.set(nodeId, clamped);
} else {
nextEnergy.delete(nodeId);
}
}
}
// 动态剪枝:只保留 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));
}
function clamp01(value) {
return Math.max(0, Math.min(1, Number(value) || 0));
}
/**
* 快捷方法:从种子列表创建扩散并返回按能量排序的结果
*
* @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) => {
if (b.energy !== a.energy) return b.energy - a.energy;
return String(a.nodeId).localeCompare(String(b.nodeId));
});
}