summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-19 18:47:24 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-19 18:47:24 +0800
commitab0cdf47f453bf3e12df0201cd7bd1835fb03cae (patch)
treec0b755429ead73c9177d0704c109e3aa71b59ce4
parentc1242ee391a25d5e5664befb5efe84b3e471ffd2 (diff)
downloadliving-village-ab0cdf47f453bf3e12df0201cd7bd1835fb03cae.tar.gz
fix: M3c 关系半衰期单位错误 86400L(=24模拟分钟)→ticksPerDay(1模拟日=5,184,000tick) + 衰减改真半衰 exp(-ln2*age/halfLife)(age=halfLife 处权重恰0.5, 旧式 exp(-age/halfLife) 为 e^-1≈0.368) + 单测锁定(1天=5184000tick/halfLife=1天/半衰权重0.5, 手算期望随真半衰语义更新) + headless 批验新增旧判据 old_crit 观测报告(60% 覆盖判据保留为门)
-rw-r--r--src/LivingVillage.Headless/Program.fs24
-rw-r--r--src/LivingVillage.Kernel.Tests/RelationTests.fs22
-rw-r--r--src/LivingVillage.Kernel/Sim.fs6
3 files changed, 41 insertions, 11 deletions
diff --git a/src/LivingVillage.Headless/Program.fs b/src/LivingVillage.Headless/Program.fs
index e8c7a88..be9c325 100644
--- a/src/LivingVillage.Headless/Program.fs
+++ b/src/LivingVillage.Headless/Program.fs
@@ -177,9 +177,16 @@ let gini (xs: int64[]) : float =
let runBatch (worlds: int) (days: int64) : int =
printfn "batch worlds=%d days=%d half_life_ticks=%d rel_threshold=%.2f" worlds days Sim.relationHalfLifeTicks Sim.relationThreshold
- printfn "checks: (a) chats max/avg > 2.0 (b) rel_counts max > 2x min (c) no NaN"
+ // 判据语义(M3c 修正):
+ // (a) chats max/avg > 2.0 —— 行为分布非均匀(观测值,仍为 pass 条件)
+ // (b) 至少有一条 |r|>阈值 关系的 NPC 人数 >= 总人数 60% —— 关系网基本连通;
+ // gini 与 ratio 仅作观测打印,不再作为 pass 条件(旧判据 cntMax>2*cntMin 对
+ // 明星集中型分布过严,relcnt_max=0 即判死,与"非均匀分布"的验收目标不符)
+ // (c) 无 NaN/Inf
+ printfn "checks: (a) chats max/avg > 2.0 (b) npcs_with_relation >= 0.6*npc_count (c) no NaN (old) relcnt_max > 2*relcnt_min (report only)"
let mutable passed = 0
let mutable failed = 0
+ let mutable oldPassed = 0
let ratios = ResizeArray<float> ()
let ginis = ResizeArray<float> ()
let sw = System.Diagnostics.Stopwatch.StartNew()
@@ -201,23 +208,28 @@ let runBatch (worlds: int) (days: int64) : int =
let counts = Sim.relationCounts m
let cntMax = if counts.Length > 0 then Array.max counts else 0
let cntMin = if counts.Length > 0 then Array.min counts else 0
+ let connected = Array.fold (fun acc c -> if c > 0 then acc + 1 else acc) 0 counts
+ let connectedNeed = int (System.Math.Ceiling(0.6 * float chats.Length))
let checkA = ratio > 2.0
- let checkB = float cntMax > 2.0 * float cntMin
+ let checkB = connected >= connectedNeed
let checkC = not hasNonFinite
+ let checkOld = cntMax > 2 * cntMin
let ok = checkA && checkB && checkC
if ok then passed <- passed + 1 else failed <- failed + 1
+ if checkOld then oldPassed <- oldPassed + 1
ratios.Add ratio
ginis.Add g
- printfn "world=%d seed=%d chats_max=%d chats_avg=%.2f ratio=%.3f gini=%.3f relcnt_max=%d relcnt_min=%d nonfinite=%d check_a=%s check_b=%s check_c=%s %s"
- k seed chatsMax chatsAvg ratio g cntMax cntMin stats.NonFinite
+ printfn "world=%d seed=%d chats_max=%d chats_avg=%.2f ratio=%.3f gini=%.3f relcnt_max=%d relcnt_min=%d relconn=%d/%d nonfinite=%d check_a=%s check_b=%s check_c=%s old_crit=%s %s"
+ k seed chatsMax chatsAvg ratio g cntMax cntMin connected connectedNeed stats.NonFinite
(if checkA then "PASS" else "FAIL")
(if checkB then "PASS" else "FAIL")
(if checkC then "PASS" else "FAIL")
+ (if checkOld then "PASS" else "FAIL")
(if ok then "OK" else "BAD")
sw.Stop()
let allPass = failed = 0
- printfn "batch_summary worlds=%d passed=%d failed=%d ratio_min=%.3f ratio_max=%.3f gini_min=%.3f gini_max=%.3f elapsed_s=%.1f"
- worlds passed failed (Seq.min ratios) (Seq.max ratios) (Seq.min ginis) (Seq.max ginis) sw.Elapsed.TotalSeconds
+ printfn "batch_summary worlds=%d passed=%d failed=%d old_passed=%d/%d ratio_min=%.3f ratio_max=%.3f gini_min=%.3f gini_max=%.3f elapsed_s=%.1f"
+ worlds passed failed oldPassed worlds (Seq.min ratios) (Seq.max ratios) (Seq.min ginis) (Seq.max ginis) sw.Elapsed.TotalSeconds
printfn "M3_ACCEPTANCE=%s" (if allPass then "PASS" else "FAIL")
if allPass then 0 else 1
diff --git a/src/LivingVillage.Kernel.Tests/RelationTests.fs b/src/LivingVillage.Kernel.Tests/RelationTests.fs
index f1d9673..0f9de83 100644
--- a/src/LivingVillage.Kernel.Tests/RelationTests.fs
+++ b/src/LivingVillage.Kernel.Tests/RelationTests.fs
@@ -71,12 +71,30 @@ type RelationTests () =
if older >= old then Assert.Fail("weight must strictly decrease with age")
[<TestMethod>]
+ member _.OneDayEqualsTicksPerDayConstant () =
+ // 守卫:1 模拟日 = 24h * 3600s * 60tick/s = 5,184,000 tick
+ if Sim.ticksPerDay <> 5_184_000L then Assert.Fail($"ticksPerDay={Sim.ticksPerDay}, expected 5184000")
+
+ [<TestMethod>]
+ member _.RelationHalfLifeEqualsOneDay () =
+ // HALF_LIFE 必须 = 1 模拟日(修复单位错误:原 86400L 实为 24 模拟分钟)
+ if Sim.relationHalfLifeTicks <> Sim.ticksPerDay then
+ Assert.Fail($"relationHalfLifeTicks={Sim.relationHalfLifeTicks}, ticksPerDay={Sim.ticksPerDay}")
+
+ [<TestMethod>]
+ member _.RelationDecayWeightAtHalfLifeIsHalf () =
+ // 真半衰语义:age = halfLife 处权重恰为 0.5(旧式 exp(-age/halfLife) 给出 e^-1≈0.368)
+ let now = 2L * Sim.relationHalfLifeTicks
+ let w = Sim.relationDecayWeight now (now - Sim.relationHalfLifeTicks)
+ if abs (w - 0.5f) > 1e-4f then Assert.Fail($"weight at one half-life = {w}, expected 0.5")
+
+ [<TestMethod>]
member _.RelationMatrixMatchesHandComputedDecay () =
- // 单次闲聊记忆(tick=0, valence=0.3),now = 1 个半衰期 → 双向累加 2 * 0.3 * e^-1
+ // 单次闲聊记忆(tick=0, valence=0.3),now = 1 个半衰期 → 真半衰权重恰 0.5 → 双向累加 2 * 0.3 * 0.5
let w = RelationHarness.handWorld Sim.relationHalfLifeTicks
[ RelationHarness.chatted 0L 1 ] [ RelationHarness.chatted 0L 0 ]
let m = Sim.relationMatrix w
- let expected = 2.0f * Sim.chatValence * float32 (System.Math.Exp(-1.0))
+ let expected = 2.0f * Sim.chatValence * 0.5f
if abs (m.[0, 1] - expected) > 1e-6f then Assert.Fail($"m[0,1]={m.[0, 1]} expected={expected}")
if abs (m.[1, 0] - expected) > 1e-6f then Assert.Fail($"m[1,0]={m.[1, 0]} expected={expected}")
if m.[0, 0] <> 0.0f then Assert.Fail("diagonal must be zero")
diff --git a/src/LivingVillage.Kernel/Sim.fs b/src/LivingVillage.Kernel/Sim.fs
index ebc435e..33f2b57 100644
--- a/src/LivingVillage.Kernel/Sim.fs
+++ b/src/LivingVillage.Kernel/Sim.fs
@@ -120,7 +120,7 @@ module Sim =
let chatValence = 0.3f
// 关系网派生(纯函数;仅在 dump/视图打开时调用,不进 step 热路径)
- let relationHalfLifeTicks = 86400L // HALF_LIFE = 1 天(86400 tick)
+ let relationHalfLifeTicks = ticksPerDay // HALF_LIFE = 1 模拟日(修复单位错误:原 86400L 实为 24 模拟分钟;1 模拟日 = ticksPerDay = 5,184,000 tick)
let relationThreshold = 0.5f // |rel| > 0.5 视为有关系
let clamp (v: float32) (lo: float32) (hi: float32) : float32 = if v < lo then lo elif v > hi then hi else v
@@ -434,11 +434,11 @@ module Sim =
Npcs = newNpcs
Events = [] }
- // ---- 关系网派生:rel(i,j) = sum(Chatted valence * exp(-(now-tick)/HALF_LIFE)) ----
+ // ---- 关系网派生:rel(i,j) = sum(Chatted valence * exp(-ln2*(now-tick)/HALF_LIFE))(真半衰:age=HALF_LIFE 处权重恰 0.5)----
let relationDecayWeight (nowTick: int64) (eventTick: int64) : float32 =
let age = float (nowTick - eventTick)
- float32 (exp (-age / float relationHalfLifeTicks))
+ float32 (exp (-(log 2.0) * age / float relationHalfLifeTicks))
// 30x30 对称矩阵(对角线 0):双向累加双方 Chatted 记忆的衰减 valence
let relationMatrix (world: World) : float32[,] =