diff options
Diffstat (limited to 'src/LivingVillage.Headless')
| -rw-r--r-- | src/LivingVillage.Headless/Program.fs | 166 |
1 files changed, 134 insertions, 32 deletions
diff --git a/src/LivingVillage.Headless/Program.fs b/src/LivingVillage.Headless/Program.fs index 28fe121..e8c7a88 100644 --- a/src/LivingVillage.Headless/Program.fs +++ b/src/LivingVillage.Headless/Program.fs @@ -64,9 +64,16 @@ let actionDistribution (npcs: Npc[]) : int * int * int * int * int = | Chat -> (e, s, w, k, c + 1)) (0, 0, 0, 0, 0) counts -let runSimulation (days: int64) (seed: uint64) (npcCount: int) : int = +type RunStats = + { ChatsPerNpc: int64[] + NonFinite: int64 + OutOfBounds: int64 + World: World } + +let runSimulation (verbose: bool) (days: int64) (seed: uint64) (npcCount: int) : RunStats = let totalTicks = days * ticksPerDay - printfn "living-village headless days=%d seed=%d npcs=%d ticks=%d tickrate=%d" days seed npcCount totalTicks ticksPerSecond + if verbose then + printfn "living-village headless days=%d seed=%d npcs=%d ticks=%d tickrate=%d" days seed npcCount totalTicks ticksPerSecond let mutable world = Sim.initialWorldN seed npcCount let mutable driver = { Rng = Rng.ofSeed seed; Until = segmentTicks; Input = zeroInput } let maxX = float32 (Sim.mapWidthTiles * Sim.tilePixels - Sim.tilePixels) @@ -113,59 +120,154 @@ let runSimulation (days: int64) (seed: uint64) (npcCount: int) : int = if Single.IsNaN p.X || Single.IsNaN p.Y || Single.IsInfinity p.X || Single.IsInfinity p.Y then nonFinite <- nonFinite + 1L if p.X < 0.0f || p.X > maxX || p.Y < 0.0f || p.Y > maxY then outOfBounds <- outOfBounds + 1L - if t % summaryInterval = 0L then + if verbose && t % summaryInterval = 0L then let eat, sleep, wander, work, chat = actionDistribution world.Npcs let chatsTotal = Array.sum npcTotal let day = float t / float ticksPerDay printfn "summary tick=%d day=%.4f eat=%d sleep=%d wander=%d work=%d chat=%d chats_window=%d" t day eat sleep wander work chat (chatsTotal - chatsWindowStart) chatsWindowStart <- chatsTotal - let segments = switchCount + 1L - let avgActionTicks = float (switchDurSum + (totalTicks - lastStart)) / float segments - let nightPct = if nightTicks > 0L then 100.0 * float sleepNight / float nightTicks else 0.0 - let dayPct = if dayTicks > 0L then 100.0 * float sleepDay / float dayTicks else 0.0 - let npc0 = world.Npcs.[0] - let memCount = List.length npc0.Mind.Memory - let lastValence = - match List.tryHead npc0.Mind.Memory with - | Some e -> e.Valence - | None -> 0.0f - printfn "npc=%A action_switches=%d avg_action_ticks=%.1f sleep_night_ticks=%d sleep_day_ticks=%d sleep_night_pct=%.2f sleep_day_pct=%.2f mem_count=%d last_valence=%.2f" - npc0.Id switchCount avgActionTicks sleepNight sleepDay nightPct dayPct memCount lastValence - let chatsTotal = Array.sum npcTotal - let minChats = if npcCount > 0 then Array.min npcTotal else 0L - let maxChats = if npcCount > 0 then Array.max npcTotal else 0L - let avgChats = if npcCount > 0 then float chatsTotal / float npcCount else 0.0 - printfn "chats_total=%d chats_min=%d chats_max=%d chats_avg=%.2f" chatsTotal minChats maxChats avgChats - printfn "done tick=%d non-finite=%d out-of-bounds=%d" world.Tick nonFinite outOfBounds - if nonFinite > 0L || outOfBounds > 0L then 1 else 0 + if verbose then + let segments = switchCount + 1L + let avgActionTicks = float (switchDurSum + (totalTicks - lastStart)) / float segments + let nightPct = if nightTicks > 0L then 100.0 * float sleepNight / float nightTicks else 0.0 + let dayPct = if dayTicks > 0L then 100.0 * float sleepDay / float dayTicks else 0.0 + let npc0 = world.Npcs.[0] + let memCount = List.length npc0.Mind.Memory + let lastValence = + match List.tryHead npc0.Mind.Memory with + | Some e -> e.Valence + | None -> 0.0f + printfn "npc=%A action_switches=%d avg_action_ticks=%.1f sleep_night_ticks=%d sleep_day_ticks=%d sleep_night_pct=%.2f sleep_day_pct=%.2f mem_count=%d last_valence=%.2f" + npc0.Id switchCount avgActionTicks sleepNight sleepDay nightPct dayPct memCount lastValence + let chatsTotal = Array.sum npcTotal + let minChats = if npcCount > 0 then Array.min npcTotal else 0L + let maxChats = if npcCount > 0 then Array.max npcTotal else 0L + let avgChats = if npcCount > 0 then float chatsTotal / float npcCount else 0.0 + printfn "chats_total=%d chats_min=%d chats_max=%d chats_avg=%.2f" chatsTotal minChats maxChats avgChats + printfn "done tick=%d non-finite=%d out-of-bounds=%d" world.Tick nonFinite outOfBounds + { ChatsPerNpc = npcTotal + NonFinite = nonFinite + OutOfBounds = outOfBounds + World = world } + +let dumpRelations (world: World) : unit = + let m = Sim.relationMatrix world + let n = Array2D.length1 m + let ci = System.Globalization.CultureInfo.InvariantCulture + printfn "relations tick=%d npcs=%d half_life_ticks=%d threshold=%.2f" world.Tick n Sim.relationHalfLifeTicks Sim.relationThreshold + for i in 0 .. n - 1 do + let row = Array.init n (fun j -> m.[i, j].ToString("0.000", ci)) + printfn "rel[%d] %s" i (String.concat " " row) + let counts = Sim.relationCounts m + let cells = Array.mapi (fun i c -> $"{i}:{c}") counts + printfn "rel_counts %s" (String.concat " " cells) + printfn "rel_total=%d rel_max=%d rel_min=%d" (Array.sum counts) (Array.max counts) (Array.min counts) + +let gini (xs: int64[]) : float = + let n = float xs.Length + let s = float (Array.sum xs) + if n <= 1.0 || s <= 0.0 then 0.0 + else + let mutable num = 0.0 + for xi in xs do + for xj in xs do + num <- num + abs (float xi - float xj) + num / (2.0 * n * n * (s / n)) + +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" + let mutable passed = 0 + let mutable failed = 0 + let ratios = ResizeArray<float> () + let ginis = ResizeArray<float> () + let sw = System.Diagnostics.Stopwatch.StartNew() + for k in 0 .. worlds - 1 do + let seed = 42UL + uint64 k + let stats = runSimulation false days seed Sim.npcCount + let m = Sim.relationMatrix stats.World + let n = Array2D.length1 m + let mutable hasNonFinite = stats.NonFinite > 0L + for i in 0 .. n - 1 do + for j in 0 .. n - 1 do + if Single.IsNaN m.[i, j] || Single.IsInfinity m.[i, j] then hasNonFinite <- true + let chats = stats.ChatsPerNpc + let chatsTotal = Array.sum chats + let chatsAvg = float chatsTotal / float chats.Length + let chatsMax = if chats.Length > 0 then Array.max chats else 0L + let ratio = if chatsAvg > 0.0 then float chatsMax / chatsAvg else 0.0 + let g = gini chats + 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 checkA = ratio > 2.0 + let checkB = float cntMax > 2.0 * float cntMin + let checkC = not hasNonFinite + let ok = checkA && checkB && checkC + if ok then passed <- passed + 1 else failed <- failed + 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 + (if checkA then "PASS" else "FAIL") + (if checkB then "PASS" else "FAIL") + (if checkC 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 "M3_ACCEPTANCE=%s" (if allPass then "PASS" else "FAIL") + if allPass then 0 else 1 [<EntryPoint>] let main argv = - let rec parse (i: int) (days: int64 option) (seed: uint64 option) (npc: int option) : Result<int64 * uint64 * int, string> = + let rec parse + (i: int) + (days: int64 option) + (seed: uint64 option) + (npc: int option) + (dump: bool) + (batch: (int * int64) option) + : Result<int64 * uint64 * int * bool * (int * int64) option, string> = if i >= argv.Length then - match days, seed, npc with - | Some d, Some s, Some n -> Ok(d, s, n) - | _ -> Error "missing --days/--seed" + match batch with + | Some(k, d) -> Ok(defaultArg days 1L, defaultArg seed 42UL, defaultArg npc 30, dump, Some(k, d)) + | None -> + match days, seed, npc with + | Some d, Some s, Some n -> Ok(d, s, n, dump, None) + | _ -> Error "missing --days/--seed" else match argv.[i] with | "--days" when i + 1 < argv.Length -> (match Int64.TryParse argv.[i + 1] with - | true, d when d > 0L -> parse (i + 2) (Some d) seed npc + | true, d when d > 0L -> parse (i + 2) (Some d) seed npc dump batch | _ -> Error $"invalid --days '{argv.[i + 1]}'") | "--seed" when i + 1 < argv.Length -> (match UInt64.TryParse argv.[i + 1] with - | true, s -> parse (i + 2) days (Some s) npc + | true, s -> parse (i + 2) days (Some s) npc dump batch | _ -> Error $"invalid --seed '{argv.[i + 1]}'") | "--npc" when i + 1 < argv.Length -> (match Int32.TryParse argv.[i + 1] with - | true, n when n > 0 && n <= 1000 -> parse (i + 2) days seed (Some n) + | true, n when n > 0 && n <= 1000 -> parse (i + 2) days seed (Some n) dump batch | _ -> Error $"invalid --npc '{argv.[i + 1]}'") + | "--dump-relations" -> parse (i + 1) days seed npc true batch + | "--batch" when i + 2 < argv.Length -> + (match Int32.TryParse argv.[i + 1], Int64.TryParse argv.[i + 2] with + | (true, k), (true, d) when k > 0 && d > 0L -> parse (i + 3) days seed npc dump (Some(k, d)) + | _ -> Error $"invalid --batch '{argv.[i + 1]} {argv.[i + 2]}'") | other -> Error $"unknown argument '{other}'" - match parse 0 None None (Some 30) with - | Ok(days, seed, npc) -> runSimulation days seed npc + match parse 0 None None (Some 30) false None with + | Ok(days, seed, npc, dump, batch) -> + match batch with + | Some(k, d) -> runBatch k d + | None -> + let stats = runSimulation true days seed npc + if dump then dumpRelations stats.World + if stats.NonFinite > 0L || stats.OutOfBounds > 0L then 1 else 0 | Error msg -> eprintfn $"headless: {msg}" - eprintfn "usage: dotnet run -c Release --project src/LivingVillage.Headless -- --days N --seed S [--npc N]" + eprintfn "usage: dotnet run -c Release --project src/LivingVillage.Headless -- --days N --seed S [--npc N] [--dump-relations] | --batch K D" 2 |
