Modélisation
Conception de graphes et inférence probabiliste · EIDOLON
EIDOLON modélise une personne comme un graphe de croyances sous contrainte : non pas ce qu’elle affiche, mais ce qu’elle ne peut pas changer sans se dégrader. Le moteur applique une pression et lit l’ordre dans lequel les dépendances cèdent : ce qui résiste est le signal.
Code et ontologie publics. Ses contre-mesures ouvrent le T512.
- Graphe
- 1 875 nœuds, 3 326 arêtes
- Méthode
- LBP amorti, pression dirigée
- Publication
- prépublication 43 p.
Prêt
Choisir un nœud l’observe, le choisir à nouveau l’infirme, une troisième fois le libère.
- Observé
- Infirmé
- Croyance, trait au prior
- Croyance en hausse
- Lien qui détermine ou implique
- Lien qui suggère
src/engine/BeliefPropagation.tslignes 371 à 581, commit 2c18671
371 for (let iter = 0; iter < cfg.maxIter; iter++) {372 iterations++373 const prev = new Map(beliefs)374375 // ── Dynamic affinityScale from convergence rate ───────────────────376 // rawRatio ≈ 1 : BP stagne/oscille → topology boost actif377 // rawRatio → 0 : BP converge vite → topology observe378 // EMA (α=0.30) pour éviter que boost oscille en phase avec BP379 //380 // prevIterMaxDelta = max|Δbelief| de l'itération t-1, capturé avant reset.381 const prevIterMaxDelta = maxBeliefDelta382 const rawRatio = iter > 0 && prevMaxDelta > 1e-6383 ? Math.min(1, prevIterMaxDelta / prevMaxDelta)384 : 1.0385 smoothedRatio = (1 - BP_EMA_ALPHA) * smoothedRatio + BP_EMA_ALPHA * rawRatio386 prevMaxDelta = prevIterMaxDelta387388 const dynamicAffinityScale = (cfg.topologyBoost && cfg.topologyBoost > 0)389 ? (cfg.affinityScale ?? 0) * (1 + Math.min(390 smoothedRatio * cfg.topologyBoost,391 cfg.maxBoostCap ?? 0.5,392 ))393 : (cfg.affinityScale ?? 0)394395 maxBeliefDelta = 0396397 // Update order398 // 'residual' scheduling: update high-residual nodes first (Elidan et al., 2006).399 // Uses per-node |Δbelief| from the PREVIOUS iteration stored in perNodeResiduals.400 // On iter=0, perNodeResiduals is empty → falls through to sequential.401 let updateOrder: typeof nodes402 if (cfg.scheduling === 'residual' && perNodeResiduals.size > 0) {403 updateOrder = [...nodes].sort((a, b) =>404 (perNodeResiduals.get(b.id) ?? 0) - (perNodeResiduals.get(a.id) ?? 0)405 )406 } else {407 updateOrder = nodes408 }409410 for (const node of updateOrder) {411 if (posEvidenceMap.has(node.id) || negEvidenceMap.has(node.id)) continue412413 const isObserved = !observedNodeIds || observedNodeIds.has(node.id)414 const prior = isObserved ? truePriors.get(node.id)! : 0415 const leak = isObserved ? leaks.get(node.id)! : 0416 const sensitivity = sensMap.get(node.id)!417 const rigidity = rigMap.get(node.id)!418 const gateType = node.gateType ?? 'or'419 const gateWeight = node.gateWeight ?? 0.5420 const incoming = graph.getIncomingEdges(node.id)421422 if (incoming.length === 0) {423 beliefs.set(node.id, prior)424 continue425 }426427 const posEdges = incoming.filter(e => e.type !== 'weakens')428 const negEdges = incoming.filter(e => e.type === 'weakens')429430 // ── Rigidity attenuation (composite with topology) ───────────431 // If topologyResult present, core nodes (haute432 // persistence + cycle) acquièrent une rigidité structurelle433 // indépendante du rigidityWeight, ils résistent au pull affinité.434 //435 // ρ_v^struct = clamp₀₁(max(ρ_v · w_ρ, clamp₀₁(pers_v · (1 + 0.4·1_{cycle}))))436 //437 // Le facteur 1.4 pour les nœuds en cycle peut porter pers_v · 1.4 > 1 quand438 // pers_v > 0.714. C'est intentionnel : clamp₀₁ est le gardien, et le boost439 // reflète l'incompressibilité structurelle des boucles de renforcement mutuel.440 let rigidityAttenuation: number441 if (cfg.topologyResult) {442 const topoProfile = cfg.topologyResult.profiles.get(node.id)443 const topoRigidity = topoProfile444 ? clamp01(topoProfile.persistence * (topoProfile.inCycle ? 1.4 : 1.0))445 : 0446 // Max entre rigidité manuelle (gated by weight) et rigidité topologique447 const structuralRigidity = clamp01(Math.max(rigidity * cfg.rigidityWeight, topoRigidity))448 rigidityAttenuation = 1 - structuralRigidity449 } else {450 // Standard path: no topology feedback451 rigidityAttenuation = 1 - rigidity * cfg.rigidityWeight452 }453454 // ── Synergy ───────────────────────────────────────────────────455 let activeParentCount = 0456 for (const edge of posEdges) {457 if ((prev.get(edge.source) ?? 0) > SYNERGY_THRESHOLD) activeParentCount++458 }459 const synergyExponent = activeParentCount >= 2460 ? 1 + SYNERGY_BOOST * Math.min(activeParentCount - 1, 3)461 : 1.0462463 // ── OR gate: Noisy-OR with synergy ────────────────────────────464 let posProduct = 1.0465 for (const edge of posEdges) {466 const srcBelief = prev.get(edge.source) ?? 0467468 // Conductivity scaling when enablePressure=true469 let conductivityScale = 1.0470 if (cfg.enablePressure && cfg.rigidityStates) {471 const srcRig = cfg.rigidityStates.get(edge.source)472 const tgtRig = cfg.rigidityStates.get(node.id)473 if (srcRig && tgtRig) {474 conductivityScale = computeEdgeConductivity(edge, srcRig, tgtRig)475 }476 }477478 const typeScale = TYPE_SCALE_POS[edge.type] ?? 0.50479 const q = typeScale * rigidityAttenuation * conductivityScale480 const qi = clamp01(q * edge.weight * srcBelief)481 posProduct *= (1 - qi)482 }483 const posProductSyn = posEdges.length > 0 ? Math.pow(posProduct, synergyExponent) : 1.0484 const beliefOr = clamp01(1 - (1 - leak) * posProductSyn)485486 // ── AND gate (EIDOLON-AND: geometric mean of activations) ────────487 let beliefAnd = leak488 if (posEdges.length > 0) {489 let logSum = 0490 for (const edge of posEdges) {491 const srcBelief = prev.get(edge.source) ?? 0492 let conductivityScaleAnd = 1.0493 if (cfg.enablePressure && cfg.rigidityStates) {494 const srcRig = cfg.rigidityStates.get(edge.source)495 const tgtRig = cfg.rigidityStates.get(node.id)496 if (srcRig && tgtRig) {497 conductivityScaleAnd = computeEdgeConductivity(edge, srcRig, tgtRig)498 }499 }500 const q = (TYPE_SCALE_POS[edge.type] ?? 0.50) * rigidityAttenuation * conductivityScaleAnd501 const activation = q * edge.weight * srcBelief502 logSum += Math.log(Math.max(EPSILON, activation))503 }504 const andMean = Math.exp(logSum / posEdges.length)505 beliefAnd = clamp01(leak + (1 - leak) * andMean)506 }507508 // ── Combine gates ─────────────────────────────────────────────509 const effectiveGate = cfg.gateMode === 'noisyAND' ? 'and'510 : cfg.gateMode === 'noisyOR' ? 'or'511 : gateType512 let activated: number513 if (effectiveGate === 'and') {514 activated = beliefAnd515 } else if (effectiveGate === 'mixed') {516 activated = clamp01(gateWeight * beliefOr + (1 - gateWeight) * beliefAnd)517 } else {518 activated = beliefOr519 }520521 // ── Inhibition ────────────────────────────────────────────────522 let negProduct = 1.0523 for (const edge of negEdges) {524 const srcBelief = prev.get(edge.source) ?? 0525 const inh = clamp01(SCALE_NEG_WEAKENS * edge.weight * srcBelief)526 negProduct *= (1 - inh)527 }528529 let rawBelief = clamp01(activated * negProduct)530531 // ── Semantic channel (Hybrid BP/TDA) ─────────────────────────532 if (cfg.affinityMatrix && dynamicAffinityScale > 0) {533 const affinRow = cfg.affinityMatrix.get(node.id)534 const minW = cfg.affinityMinWeight ?? 0.20535 if (affinRow && affinRow.size > 0) {536 let contextSum = 0, totalAffinity = 0537 for (const [neighborId, w] of affinRow) {538 if (w < minW) continue539 const nb = prev.get(neighborId) // Jacobi : snapshot début d'itération, cohérent avec KB edges540 if (nb === undefined) continue541 contextSum += w * nb; totalAffinity += w542 }543 if (totalAffinity > 0) {544 const contextBelief = contextSum / totalAffinity545 const α = clamp01(dynamicAffinityScale * rigidityAttenuation)546 rawBelief = clamp01((1 - α) * rawBelief + α * contextBelief)547 }548 }549 }550551 // ── Prior-anchored shrinkage (λ = sensitivity) ───────────────552 // computed = (1−λ)·prior + λ·rawBelief : shrinks toward prior.553 // λ=0 → belief fixed at prior (immune); λ=1 → fully responsive.554 const computed = clamp01(prior + sensitivity * (rawBelief - prior))555556 // ── Damping ───────────────────────────────────────────────────557 const prevBelief = prev.get(node.id) ?? 0558 const newBelief = clamp01(cfg.damping * computed + (1 - cfg.damping) * prevBelief)559 beliefs.set(node.id, newBelief)560 }561562 // ── Re-clamp evidence ─────────────────────────────────────────563 for (const [nodeId, conf] of posEvidenceMap) beliefs.set(nodeId, conf)564 for (const [nodeId, conf] of negEvidenceMap) {565 const prior = truePriors.get(nodeId) ?? DEFAULT_PRIOR566 beliefs.set(nodeId, clamp01(prior * (1 - conf)))567 }568569 // ── Convergence check + per-node residual tracking ────────────570 for (const [id, val] of beliefs) {571 const delta = Math.abs(val - (prev.get(id) ?? 0))572 if (delta > maxBeliefDelta) maxBeliefDelta = delta573 if (cfg.scheduling === 'residual') perNodeResiduals.set(id, delta)574 }575576 cfg.onIterationComplete?.(iter, new Map(beliefs), maxBeliefDelta)577578 if (maxBeliefDelta < cfg.tolerance) {579 converged = true580 break581 }