diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-29 03:45:58 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-29 03:45:58 +0800 |
| commit | 2c559a79a5977c19876cee58d336b7983f67fa4c (patch) | |
| tree | 9607e336fc740bc840a02143f63fd7932aecedf3 | |
| parent | eb4fca23cabce5762fc256a659228a5d50b5e9a9 (diff) | |
| download | living-village-2c559a79a5977c19876cee58d336b7983f67fa4c.tar.gz | |
m3r: 缩尺批验收口(50×15 流式 + systemd --user 单元 + 独立验收脚本)
缩尺口径:M3 phase2 50×100 实测 11.4h 仅完成 6/50(见
artifacts/m3-acceptance/final-candidate-20260921T095500Z/logs/*.diagnosis.txt),
改为 50 世界 × 15 模拟日、逐世界流式打印并 flush。标定据 docs/overnight-status.md
(Hermes 单 worker 亲测):1×2=34.6s、1×5=87.7s、1×10=173.6s,外推 15 天 ≈260s。
注意:派单所引用 artifacts/m3-independent/20260921T105500Z 全盘查找不存在,
故改用上述仓内标定,未编造任何数据。
内容:
- Headless 边缘:runSimulation 拆出 runSimulationCore 增加逐模拟日回调;
evalWorld 传入回调;runBatch 每 (days/5) 天打印 `[world k] progress day=d/D elapsed=..s`
并 flush,长世界不再零进度(BatchOutput.progressLine 纯函数 + 单测)。
- 新增 deploy/living-village-m3r.service 模板:wrapper 作 MasterProcess、
StandardOutput/StandardError=append: acceptance.log、Restart=no;
systemd-analyze --user verify 通过。本机 ~/.config/systemd/user 属外部目录受限,
实跑以等价属性的 systemd-run --user 瞬态单元 living-village-m3r.service 承载。
- scripts/m3r-master.sh:写 state.json(pid/unit/log/commit/workers/worlds/days/
起止/exit),exec 2>&1 运行 batch 并回传真实退出码。
- scripts/verify-m3r.py:独立可复跑,校验 state 字段、50 条 world 行(索引/seed/
ok 与 check_a/b/c 自洽)、summary passed+failed=50 与极值一致、抽查 3 行、
[done] 50 条、progress 行、M3_ACCEPTANCE=PASS 且 ratio_min>2.0。
本次实跑(commit 895d792):12 worker,wall=4316.0s,50/50 OK,
ratio_min=3.346 ratio_max=12.321 gini∈[0.412,0.548],relconn=30/18,
check_a/b/c 全 PASS,M3_ACCEPTANCE=PASS,exit=0;独立脚本复跑 M3R_VERIFY=PASS。
证据:artifacts/m3r-acceptance/acceptance.log + 20260928T183215Z/state.json。
回归:Desktop 303/303 + Kernel 119/119;--performance-baseline ×3
digest=953775FAEB2F… 不变,performance_determinism=PASS。
未改 Kernel/Sim;未 push。
| -rw-r--r-- | artifacts/m3r-acceptance/20260928T183215Z/state.json | 12 | ||||
| -rw-r--r-- | artifacts/m3r-acceptance/acceptance.log | 357 | ||||
| -rw-r--r-- | deploy/living-village-m3r.service | 34 | ||||
| -rwxr-xr-x | scripts/m3r-master.sh | 78 | ||||
| -rwxr-xr-x | scripts/verify-m3r.py | 196 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop.Tests/M3BatchOutputTests.fs | 5 | ||||
| -rw-r--r-- | src/LivingVillage.Headless/BatchOutput.fs | 4 | ||||
| -rw-r--r-- | src/LivingVillage.Headless/Program.fs | 19 |
8 files changed, 701 insertions, 4 deletions
diff --git a/artifacts/m3r-acceptance/20260928T183215Z/state.json b/artifacts/m3r-acceptance/20260928T183215Z/state.json new file mode 100644 index 0000000..2406711 --- /dev/null +++ b/artifacts/m3r-acceptance/20260928T183215Z/state.json @@ -0,0 +1,12 @@ +{ + "commit": "895d792", + "days": 15, + "ended_at": "2026-09-28T19:44:11Z", + "exit": 0, + "log": "/home/somhairle/projects/living-village/artifacts/m3r-acceptance/acceptance.log", + "pid": 142112, + "started_at": "2026-09-28T18:32:15Z", + "unit": "living-village-m3r.service", + "workers": 12, + "worlds": 50 +} diff --git a/artifacts/m3r-acceptance/acceptance.log b/artifacts/m3r-acceptance/acceptance.log new file mode 100644 index 0000000..a0acdbc --- /dev/null +++ b/artifacts/m3r-acceptance/acceptance.log @@ -0,0 +1,357 @@ +[m3r] start unit=living-village-m3r.service pid=142112 commit=895d792 workers=12 worlds=50 days=15 started_at=2026-09-28T18:32:15Z +batch worlds=50 days=15 half_life_ticks=5184000 rel_threshold=0.50 +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) +workers=12 +[world 24] progress day=3/15 elapsed=182.1s +[world 20] progress day=3/15 elapsed=182.5s +[world 16] progress day=3/15 elapsed=187.9s +[world 32] progress day=3/15 elapsed=188.6s +[world 0] progress day=3/15 elapsed=189.1s +[world 12] progress day=3/15 elapsed=190.9s +[world 4] progress day=3/15 elapsed=192.3s +[world 44] progress day=3/15 elapsed=194.0s +[world 8] progress day=3/15 elapsed=194.5s +[world 40] progress day=3/15 elapsed=194.7s +[world 28] progress day=3/15 elapsed=199.5s +[world 36] progress day=3/15 elapsed=205.2s +[world 24] progress day=6/15 elapsed=364.5s +[world 20] progress day=6/15 elapsed=368.6s +[world 16] progress day=6/15 elapsed=378.8s +[world 32] progress day=6/15 elapsed=379.7s +[world 0] progress day=6/15 elapsed=381.8s +[world 12] progress day=6/15 elapsed=387.5s +[world 4] progress day=6/15 elapsed=388.4s +[world 44] progress day=6/15 elapsed=389.5s +[world 8] progress day=6/15 elapsed=391.5s +[world 40] progress day=6/15 elapsed=395.5s +[world 28] progress day=6/15 elapsed=404.3s +[world 36] progress day=6/15 elapsed=418.1s +[world 24] progress day=9/15 elapsed=549.5s +[world 20] progress day=9/15 elapsed=557.3s +[world 16] progress day=9/15 elapsed=570.0s +[world 32] progress day=9/15 elapsed=571.2s +[world 0] progress day=9/15 elapsed=573.6s +[world 12] progress day=9/15 elapsed=582.4s +[world 4] progress day=9/15 elapsed=583.0s +[world 44] progress day=9/15 elapsed=583.3s +[world 8] progress day=9/15 elapsed=586.4s +[world 40] progress day=9/15 elapsed=593.3s +[world 28] progress day=9/15 elapsed=604.3s +[world 36] progress day=9/15 elapsed=624.3s +[world 24] progress day=12/15 elapsed=733.7s +[world 20] progress day=12/15 elapsed=745.2s +[world 16] progress day=12/15 elapsed=759.8s +[world 32] progress day=12/15 elapsed=760.4s +[world 0] progress day=12/15 elapsed=765.6s +[world 4] progress day=12/15 elapsed=775.8s +[world 44] progress day=12/15 elapsed=776.9s +[world 12] progress day=12/15 elapsed=777.9s +[world 8] progress day=12/15 elapsed=782.0s +[world 40] progress day=12/15 elapsed=789.8s +[world 28] progress day=12/15 elapsed=802.8s +[world 36] progress day=12/15 elapsed=826.4s +[world 24] progress day=15/15 elapsed=918.1s +world=24 seed=66 chats_max=5395 chats_avg=634.00 ratio=8.509 gini=0.497 relcnt_max=29 relcnt_min=3 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=918.1 +[done 1/50] elapsed=918.1s world=24 +[world 20] progress day=15/15 elapsed=933.7s +world=20 seed=62 chats_max=16944 chats_avg=2244.60 ratio=7.549 gini=0.490 relcnt_max=29 relcnt_min=4 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=933.7 +[done 2/50] elapsed=933.7s world=20 +[world 16] progress day=15/15 elapsed=950.0s +world=16 seed=58 chats_max=18056 chats_avg=2782.53 ratio=6.489 gini=0.514 relcnt_max=29 relcnt_min=4 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=950.0 +[done 3/50] elapsed=950.0s world=16 +[world 32] progress day=15/15 elapsed=953.2s +world=32 seed=74 chats_max=17987 chats_avg=3274.60 ratio=5.493 gini=0.523 relcnt_max=29 relcnt_min=4 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=953.2 +[done 4/50] elapsed=953.2s world=32 +[world 0] progress day=15/15 elapsed=956.7s +world=0 seed=42 chats_max=17325 chats_avg=2954.67 ratio=5.864 gini=0.458 relcnt_max=29 relcnt_min=6 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=956.7 +[done 5/50] elapsed=956.7s world=0 +[world 44] progress day=15/15 elapsed=971.8s +world=44 seed=86 chats_max=19413 chats_avg=4467.87 ratio=4.345 gini=0.470 relcnt_max=29 relcnt_min=5 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=971.8 +[done 6/50] elapsed=971.8s world=44 +[world 4] progress day=15/15 elapsed=971.9s +world=4 seed=46 chats_max=17379 chats_avg=3434.73 ratio=5.060 gini=0.420 relcnt_max=29 relcnt_min=7 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=971.9 +[done 7/50] elapsed=971.9s world=4 +[world 12] progress day=15/15 elapsed=972.7s +world=12 seed=54 chats_max=18676 chats_avg=3782.60 ratio=4.937 gini=0.451 relcnt_max=29 relcnt_min=6 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=972.7 +[done 8/50] elapsed=972.7s world=12 +[world 8] progress day=15/15 elapsed=978.0s +world=8 seed=50 chats_max=15410 chats_avg=3652.93 ratio=4.219 gini=0.442 relcnt_max=29 relcnt_min=6 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=978.0 +[done 9/50] elapsed=978.0s world=8 +[world 40] progress day=15/15 elapsed=988.1s +world=40 seed=82 chats_max=19101 chats_avg=4529.13 ratio=4.217 gini=0.451 relcnt_max=29 relcnt_min=6 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=988.1 +[done 10/50] elapsed=988.1s world=40 +[world 28] progress day=15/15 elapsed=1002.9s +world=28 seed=70 chats_max=18876 chats_avg=4825.87 ratio=3.911 gini=0.421 relcnt_max=29 relcnt_min=8 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=1002.9 +[done 11/50] elapsed=1002.9s world=28 +[world 36] progress day=15/15 elapsed=1032.3s +world=36 seed=78 chats_max=20295 chats_avg=6057.27 ratio=3.351 gini=0.436 relcnt_max=29 relcnt_min=7 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=1032.3 +[done 12/50] elapsed=1032.3s world=36 +[world 21] progress day=3/15 elapsed=184.5s +[world 25] progress day=3/15 elapsed=208.9s +[world 33] progress day=3/15 elapsed=184.5s +[world 17] progress day=3/15 elapsed=191.8s +[world 13] progress day=3/15 elapsed=179.9s +[world 5] progress day=3/15 elapsed=181.3s +[world 1] progress day=3/15 elapsed=202.3s +[world 9] progress day=3/15 elapsed=189.1s +[world 45] progress day=3/15 elapsed=196.0s +[world 41] progress day=3/15 elapsed=198.8s +[world 29] progress day=3/15 elapsed=199.2s +[world 37] progress day=3/15 elapsed=195.6s +[world 21] progress day=6/15 elapsed=370.2s +[world 33] progress day=6/15 elapsed=371.7s +[world 13] progress day=6/15 elapsed=365.7s +[world 17] progress day=6/15 elapsed=388.5s +[world 5] progress day=6/15 elapsed=367.5s +[world 25] progress day=6/15 elapsed=428.6s +[world 9] progress day=6/15 elapsed=384.2s +[world 1] progress day=6/15 elapsed=412.4s +[world 45] progress day=6/15 elapsed=399.5s +[world 41] progress day=6/15 elapsed=404.0s +[world 29] progress day=6/15 elapsed=405.7s +[world 37] progress day=6/15 elapsed=399.1s +[world 21] progress day=9/15 elapsed=558.9s +[world 33] progress day=9/15 elapsed=559.7s +[world 13] progress day=9/15 elapsed=552.8s +[world 5] progress day=9/15 elapsed=554.1s +[world 17] progress day=9/15 elapsed=582.2s +[world 9] progress day=9/15 elapsed=576.6s +[world 25] progress day=9/15 elapsed=642.9s +[world 45] progress day=9/15 elapsed=598.4s +[world 1] progress day=9/15 elapsed=614.4s +[world 41] progress day=9/15 elapsed=604.9s +[world 29] progress day=9/15 elapsed=605.5s +[world 37] progress day=9/15 elapsed=597.5s +[world 21] progress day=12/15 elapsed=746.3s +[world 33] progress day=12/15 elapsed=746.2s +[world 13] progress day=12/15 elapsed=739.2s +[world 5] progress day=12/15 elapsed=742.5s +[world 17] progress day=12/15 elapsed=777.9s +[world 9] progress day=12/15 elapsed=768.1s +[world 45] progress day=12/15 elapsed=796.9s +[world 25] progress day=12/15 elapsed=854.6s +[world 1] progress day=12/15 elapsed=818.8s +[world 41] progress day=12/15 elapsed=803.4s +[world 29] progress day=12/15 elapsed=806.2s +[world 37] progress day=12/15 elapsed=793.3s +[world 21] progress day=15/15 elapsed=935.5s +world=21 seed=63 chats_max=12398 chats_avg=1662.33 ratio=7.458 gini=0.495 relcnt_max=29 relcnt_min=3 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=935.5 +[done 13/50] elapsed=935.5s world=21 +[world 33] progress day=15/15 elapsed=931.2s +world=33 seed=75 chats_max=10195 chats_avg=1003.73 ratio=10.157 gini=0.465 relcnt_max=29 relcnt_min=2 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=931.2 +[done 14/50] elapsed=931.2s world=33 +[world 13] progress day=15/15 elapsed=923.3s +world=13 seed=55 chats_max=3340 chats_avg=472.07 ratio=7.075 gini=0.474 relcnt_max=29 relcnt_min=2 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=923.3 +[done 15/50] elapsed=923.3s world=13 +[world 5] progress day=15/15 elapsed=928.6s +world=5 seed=47 chats_max=6676 chats_avg=844.60 ratio=7.904 gini=0.481 relcnt_max=29 relcnt_min=3 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=928.6 +[done 16/50] elapsed=928.6s world=5 +[world 17] progress day=15/15 elapsed=971.8s +world=17 seed=59 chats_max=18453 chats_avg=3783.73 ratio=4.877 gini=0.461 relcnt_max=29 relcnt_min=5 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=971.8 +[done 17/50] elapsed=971.8s world=17 +[world 9] progress day=15/15 elapsed=959.7s +world=9 seed=51 chats_max=10573 chats_avg=2710.33 ratio=3.901 gini=0.416 relcnt_max=29 relcnt_min=6 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=959.7 +[done 18/50] elapsed=959.7s world=9 +[world 45] progress day=15/15 elapsed=992.9s +world=45 seed=87 chats_max=17088 chats_avg=4193.60 ratio=4.075 gini=0.413 relcnt_max=29 relcnt_min=7 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=992.9 +[done 19/50] elapsed=992.9s world=45 +[world 1] progress day=15/15 elapsed=1021.5s +world=1 seed=43 chats_max=19732 chats_avg=5519.93 ratio=3.575 gini=0.465 relcnt_max=29 relcnt_min=6 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=1021.5 +[done 20/50] elapsed=1021.5s world=1 +[world 25] progress day=15/15 elapsed=1065.7s +world=25 seed=67 chats_max=19759 chats_avg=5905.13 ratio=3.346 gini=0.437 relcnt_max=29 relcnt_min=6 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=1065.7 +[done 21/50] elapsed=1065.7s world=25 +[world 41] progress day=15/15 elapsed=1002.3s +world=41 seed=83 chats_max=19059 chats_avg=4326.80 ratio=4.405 gini=0.436 relcnt_max=29 relcnt_min=6 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=1002.3 +[done 22/50] elapsed=1002.3s world=41 +[world 29] progress day=15/15 elapsed=1006.4s +world=29 seed=71 chats_max=19246 chats_avg=4607.53 ratio=4.177 gini=0.420 relcnt_max=29 relcnt_min=7 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=1006.4 +[done 23/50] elapsed=1006.4s world=29 +[world 37] progress day=15/15 elapsed=990.0s +world=37 seed=79 chats_max=17043 chats_avg=3626.33 ratio=4.700 gini=0.412 relcnt_max=29 relcnt_min=8 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=990.0 +[done 24/50] elapsed=990.0s world=37 +[world 22] progress day=3/15 elapsed=188.4s +[world 34] progress day=3/15 elapsed=187.6s +[world 14] progress day=3/15 elapsed=192.8s +[world 6] progress day=3/15 elapsed=190.4s +[world 18] progress day=3/15 elapsed=200.7s +[world 10] progress day=3/15 elapsed=189.9s +[world 46] progress day=3/15 elapsed=184.8s +[world 26] progress day=3/15 elapsed=186.0s +[world 42] progress day=3/15 elapsed=182.2s +[world 2] progress day=3/15 elapsed=195.1s +[world 30] progress day=3/15 elapsed=184.6s +[world 38] progress day=3/15 elapsed=192.2s +[world 22] progress day=6/15 elapsed=370.5s +[world 34] progress day=6/15 elapsed=369.8s +[world 14] progress day=6/15 elapsed=384.1s +[world 6] progress day=6/15 elapsed=379.8s +[world 10] progress day=6/15 elapsed=380.5s +[world 18] progress day=6/15 elapsed=398.1s +[world 46] progress day=6/15 elapsed=368.0s +[world 42] progress day=6/15 elapsed=364.6s +[world 26] progress day=6/15 elapsed=373.3s +[world 2] progress day=6/15 elapsed=389.9s +[world 30] progress day=6/15 elapsed=370.0s +[world 38] progress day=6/15 elapsed=386.4s +[world 22] progress day=9/15 elapsed=553.6s +[world 34] progress day=9/15 elapsed=554.1s +[world 6] progress day=9/15 elapsed=566.9s +[world 14] progress day=9/15 elapsed=574.6s +[world 10] progress day=9/15 elapsed=569.6s +[world 18] progress day=9/15 elapsed=591.8s +[world 46] progress day=9/15 elapsed=551.9s +[world 42] progress day=9/15 elapsed=548.3s +[world 26] progress day=9/15 elapsed=559.7s +[world 2] progress day=9/15 elapsed=584.3s +[world 30] progress day=9/15 elapsed=556.0s +[world 38] progress day=9/15 elapsed=578.7s +[world 22] progress day=12/15 elapsed=736.6s +[world 34] progress day=12/15 elapsed=737.9s +[world 6] progress day=12/15 elapsed=756.3s +[world 14] progress day=12/15 elapsed=766.5s +[world 10] progress day=12/15 elapsed=758.2s +[world 46] progress day=12/15 elapsed=736.4s +[world 18] progress day=12/15 elapsed=785.6s +[world 42] progress day=12/15 elapsed=731.4s +[world 26] progress day=12/15 elapsed=745.9s +[world 30] progress day=12/15 elapsed=740.2s +[world 2] progress day=12/15 elapsed=777.0s +[world 22] progress day=15/15 elapsed=919.8s +world=22 seed=64 chats_max=7283 chats_avg=1007.13 ratio=7.231 gini=0.459 relcnt_max=29 relcnt_min=3 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=919.8 +[done 25/50] elapsed=919.8s world=22 +[world 38] progress day=12/15 elapsed=767.1s +[world 34] progress day=15/15 elapsed=921.4s +world=34 seed=76 chats_max=17401 chats_avg=2092.67 ratio=8.315 gini=0.477 relcnt_max=29 relcnt_min=4 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=921.4 +[done 26/50] elapsed=921.4s world=34 +[world 6] progress day=15/15 elapsed=944.1s +world=6 seed=48 chats_max=13781 chats_avg=2566.60 ratio=5.369 gini=0.460 relcnt_max=29 relcnt_min=5 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=944.1 +[done 27/50] elapsed=944.1s world=6 +[world 14] progress day=15/15 elapsed=956.9s +world=14 seed=56 chats_max=14112 chats_avg=2678.67 ratio=5.268 gini=0.432 relcnt_max=29 relcnt_min=6 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=956.9 +[done 28/50] elapsed=956.9s world=14 +[world 10] progress day=15/15 elapsed=946.3s +world=10 seed=52 chats_max=15478 chats_avg=2667.93 ratio=5.801 gini=0.482 relcnt_max=29 relcnt_min=4 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=946.3 +[done 29/50] elapsed=946.3s world=10 +[world 46] progress day=15/15 elapsed=920.3s +world=46 seed=88 chats_max=8996 chats_avg=1710.73 ratio=5.259 gini=0.445 relcnt_max=29 relcnt_min=5 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=920.3 +[done 30/50] elapsed=920.3s world=46 +[world 18] progress day=15/15 elapsed=982.2s +world=18 seed=60 chats_max=19263 chats_avg=4668.00 ratio=4.127 gini=0.466 relcnt_max=29 relcnt_min=5 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=982.2 +[done 31/50] elapsed=982.2s world=18 +[world 42] progress day=15/15 elapsed=915.1s +world=42 seed=84 chats_max=11644 chats_avg=1199.47 ratio=9.708 gini=0.548 relcnt_max=29 relcnt_min=2 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=915.1 +[done 32/50] elapsed=915.1s world=42 +[world 26] progress day=15/15 elapsed=931.1s +world=26 seed=68 chats_max=15463 chats_avg=2179.67 ratio=7.094 gini=0.465 relcnt_max=29 relcnt_min=5 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=931.1 +[done 33/50] elapsed=931.1s world=26 +[world 30] progress day=15/15 elapsed=927.5s +world=30 seed=72 chats_max=17952 chats_avg=2702.73 ratio=6.642 gini=0.502 relcnt_max=29 relcnt_min=4 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=927.5 +[done 34/50] elapsed=927.5s world=30 +[world 2] progress day=15/15 elapsed=970.5s +world=2 seed=44 chats_max=17647 chats_avg=3342.40 ratio=5.280 gini=0.419 relcnt_max=29 relcnt_min=8 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=970.5 +[done 35/50] elapsed=970.5s world=2 +[world 23] progress day=3/15 elapsed=183.6s +[world 38] progress day=15/15 elapsed=960.8s +world=38 seed=80 chats_max=19109 chats_avg=4235.13 ratio=4.512 gini=0.467 relcnt_max=29 relcnt_min=5 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=960.8 +[done 36/50] elapsed=960.8s world=38 +[world 35] progress day=3/15 elapsed=183.8s +[world 7] progress day=3/15 elapsed=189.2s +[world 15] progress day=3/15 elapsed=182.4s +[world 11] progress day=3/15 elapsed=186.1s +[world 47] progress day=3/15 elapsed=188.1s +[world 19] progress day=3/15 elapsed=181.6s +[world 43] progress day=3/15 elapsed=189.7s +[world 27] progress day=3/15 elapsed=184.9s +[world 3] progress day=3/15 elapsed=181.1s +[world 31] progress day=3/15 elapsed=198.4s +[world 23] progress day=6/15 elapsed=366.0s +[world 39] progress day=3/15 elapsed=180.4s +[world 35] progress day=6/15 elapsed=367.5s +[world 15] progress day=6/15 elapsed=363.4s +[world 7] progress day=6/15 elapsed=377.8s +[world 11] progress day=6/15 elapsed=369.3s +[world 47] progress day=6/15 elapsed=373.9s +[world 19] progress day=6/15 elapsed=362.6s +[world 43] progress day=6/15 elapsed=378.9s +[world 27] progress day=6/15 elapsed=370.6s +[world 3] progress day=6/15 elapsed=361.8s +[world 31] progress day=6/15 elapsed=395.6s +[world 23] progress day=9/15 elapsed=546.6s +[world 39] progress day=6/15 elapsed=361.6s +[world 35] progress day=9/15 elapsed=551.5s +[world 15] progress day=9/15 elapsed=543.1s +[world 7] progress day=9/15 elapsed=564.2s +[world 11] progress day=9/15 elapsed=553.2s +[world 47] progress day=9/15 elapsed=558.5s +[world 19] progress day=9/15 elapsed=543.2s +[world 27] progress day=9/15 elapsed=554.9s +[world 43] progress day=9/15 elapsed=565.8s +[world 3] progress day=9/15 elapsed=541.4s +[world 23] progress day=12/15 elapsed=729.4s +[world 31] progress day=9/15 elapsed=589.7s +[world 39] progress day=9/15 elapsed=543.5s +[world 35] progress day=12/15 elapsed=734.5s +[world 15] progress day=12/15 elapsed=723.9s +[world 7] progress day=12/15 elapsed=751.7s +[world 11] progress day=12/15 elapsed=736.7s +[world 19] progress day=12/15 elapsed=723.6s +[world 47] progress day=12/15 elapsed=743.3s +[world 27] progress day=12/15 elapsed=737.5s +[world 43] progress day=12/15 elapsed=751.7s +[world 3] progress day=12/15 elapsed=723.8s +[world 23] progress day=15/15 elapsed=910.9s +world=23 seed=65 chats_max=5691 chats_avg=1167.33 ratio=4.875 gini=0.469 relcnt_max=29 relcnt_min=4 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=910.9 +[done 37/50] elapsed=910.9s world=23 +[world 39] progress day=12/15 elapsed=725.0s +[world 31] progress day=12/15 elapsed=781.8s +[world 35] progress day=15/15 elapsed=916.9s +world=35 seed=77 chats_max=6266 chats_avg=917.93 ratio=6.826 gini=0.480 relcnt_max=29 relcnt_min=3 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=916.9 +[done 38/50] elapsed=916.9s world=35 +[world 15] progress day=15/15 elapsed=901.1s +world=15 seed=57 chats_max=4300 chats_avg=349.00 ratio=12.321 gini=0.502 relcnt_max=29 relcnt_min=1 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=901.1 +[done 39/50] elapsed=901.1s world=15 +[world 7] progress day=15/15 elapsed=930.5s +world=7 seed=49 chats_max=9071 chats_avg=2167.33 ratio=4.185 gini=0.414 relcnt_max=29 relcnt_min=7 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=930.5 +[done 40/50] elapsed=930.5s world=7 +[world 11] progress day=15/15 elapsed=909.2s +world=11 seed=53 chats_max=5819 chats_avg=682.60 ratio=8.525 gini=0.495 relcnt_max=29 relcnt_min=2 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=909.2 +[done 41/50] elapsed=909.2s world=11 +[world 19] progress day=15/15 elapsed=891.1s +world=19 seed=61 chats_max=5883 chats_avg=754.60 ratio=7.796 gini=0.504 relcnt_max=29 relcnt_min=3 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=891.1 +[done 42/50] elapsed=891.1s world=19 +[world 47] progress day=15/15 elapsed=912.6s +world=47 seed=89 chats_max=17445 chats_avg=1992.67 ratio=8.755 gini=0.494 relcnt_max=29 relcnt_min=4 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=912.6 +[done 43/50] elapsed=912.6s world=47 +[world 27] progress day=15/15 elapsed=896.1s +world=27 seed=69 chats_max=9102 chats_avg=2141.07 ratio=4.251 gini=0.419 relcnt_max=29 relcnt_min=7 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=896.1 +[done 44/50] elapsed=896.1s world=27 +[world 43] progress day=15/15 elapsed=909.9s +world=43 seed=85 chats_max=11701 chats_avg=2730.40 ratio=4.285 gini=0.444 relcnt_max=29 relcnt_min=5 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=909.9 +[done 45/50] elapsed=909.9s world=43 +[world 3] progress day=15/15 elapsed=871.0s +world=3 seed=45 chats_max=13073 chats_avg=1154.93 ratio=11.319 gini=0.512 relcnt_max=29 relcnt_min=2 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=871.0 +[done 46/50] elapsed=871.0s world=3 +[world 39] progress day=15/15 elapsed=850.5s +world=39 seed=81 chats_max=5104 chats_avg=977.67 ratio=5.221 gini=0.460 relcnt_max=29 relcnt_min=3 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=850.5 +[done 47/50] elapsed=850.5s world=39 +[world 48] progress day=3/15 elapsed=134.0s +[world 31] progress day=15/15 elapsed=904.2s +world=31 seed=73 chats_max=19189 chats_avg=4642.27 ratio=4.134 gini=0.476 relcnt_max=29 relcnt_min=5 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=904.2 +[done 48/50] elapsed=904.2s world=31 +[world 48] progress day=6/15 elapsed=188.2s +[world 48] progress day=9/15 elapsed=240.4s +[world 48] progress day=12/15 elapsed=293.9s +[world 48] progress day=15/15 elapsed=347.1s +world=48 seed=90 chats_max=13912 chats_avg=3140.00 ratio=4.431 gini=0.436 relcnt_max=29 relcnt_min=7 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=347.1 +[done 49/50] elapsed=347.1s world=48 +[world 49] progress day=3/15 elapsed=53.9s +[world 49] progress day=6/15 elapsed=108.1s +[world 49] progress day=9/15 elapsed=161.6s +[world 49] progress day=12/15 elapsed=215.2s +[world 49] progress day=15/15 elapsed=269.0s +world=49 seed=91 chats_max=18760 chats_avg=3892.87 ratio=4.819 gini=0.435 relcnt_max=29 relcnt_min=7 relconn=30/18 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK elapsed_s=269.0 +[done 50/50] elapsed=269.0s world=49 +batch_summary worlds=50 passed=50 failed=0 old_passed=50/50 ratio_min=3.346 ratio_max=12.321 gini_min=0.412 gini_max=0.548 elapsed_s=4316.0 workers=12 wall_s=4316.0 +M3_ACCEPTANCE=PASS +[m3r] end pid=142112 exit=0 ended_at=2026-09-28T19:44:11Z diff --git a/deploy/living-village-m3r.service b/deploy/living-village-m3r.service new file mode 100644 index 0000000..7b7c9b6 --- /dev/null +++ b/deploy/living-village-m3r.service @@ -0,0 +1,34 @@ +# M3R 缩尺批验 systemd --user 单元模板(50 世界 × 15 模拟日,逐世界流式)。 +# +# 安装(占位符替换后放入用户单元目录): +# mkdir -p ~/.config/systemd/user +# sed -e "s|@REPO@|/home/somhairle/projects/living-village|g" \ +# -e "s|@TS@|$(date -u +%Y%m%dT%H%M%SZ)|g" \ +# -e "s|@COMMIT@|895d792|g" \ +# -e "s|@WORKERS@|12|g" \ +# deploy/living-village-m3r.service > ~/.config/systemd/user/living-village-m3r.service +# systemctl --user daemon-reload +# systemctl --user start living-village-m3r.service # 前台等待 exit;不自动重启 +# systemctl --user show -p ExecMainStatus living-village-m3r.service +# +# 进程退不退,system 均不影响本验收:判定只读 acceptance.log 与 <TS>/state.json。 +[Unit] +Description=Living Village M3R scaled batch acceptance (50 worlds x 15 days, streaming) +After=default.target + +[Service] +Type=simple +WorkingDirectory=@REPO@ +Environment=M3R_REPO=@REPO@ +Environment=M3R_DIR=@REPO@/artifacts/m3r-acceptance/@TS@ +Environment=M3R_COMMIT=@COMMIT@ +Environment=M3R_WORLDS=50 +Environment=M3R_DAYS=15 +Environment=LV_BATCH_WORKERS=@WORKERS@ +Environment=M3R_LOG=@REPO@/artifacts/m3r-acceptance/acceptance.log +ExecStart=/usr/bin/bash @REPO@/scripts/m3r-master.sh +StandardOutput=append:@REPO@/artifacts/m3r-acceptance/acceptance.log +StandardError=append:@REPO@/artifacts/m3r-acceptance/acceptance.log +Restart=no +TimeoutStartSec=infinity +TimeoutStopSec=30s diff --git a/scripts/m3r-master.sh b/scripts/m3r-master.sh new file mode 100755 index 0000000..d4f18fa --- /dev/null +++ b/scripts/m3r-master.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# M3R 缩尺批验 MasterProcess 包装器(systemd --user 单元 ExecStart 指向本脚本)。 +# +# 职责: +# * 在 <M3R_DIR> 落 state.json(pid/unit/log/commit/workers/worlds/days/起止/exit); +# * 以 2>&1 运行 Headless Release 批处理 `--batch <worlds> <days>`,逐世界流式; +# * 进程退出即写回 ended_at 与退出码,并把真实退出码回传给 systemd。 +# +# 环境变量(由 unit 注入): +# M3R_REPO 仓库根(绝对路径) +# M3R_DIR 本次运行证据目录(绝对路径,state.json 落此) +# M3R_COMMIT 期望 commit 短哈希(写入 state.json) +# M3R_WORLDS 世界数(默认 50) +# M3R_DAYS 模拟天数(默认 15) +# LV_BATCH_WORKERS 并行 worker 数(默认 12) +# M3R_LOG 验收日志路径(只写入 state.json 作为记录,实际重定向由 unit 负责) +set -u + +REPO="${M3R_REPO:?M3R_REPO required}" +DIR="${M3R_DIR:?M3R_DIR required}" +COMMIT="${M3R_COMMIT:?M3R_COMMIT required}" +WORLDS="${M3R_WORLDS:-50}" +DAYS="${M3R_DAYS:-15}" +WORKERS="${LV_BATCH_WORKERS:-12}" +LOG="${M3R_LOG:-$REPO/artifacts/m3r-acceptance/acceptance.log}" +UNIT="living-village-m3r.service" +DLL="$REPO/src/LivingVillage.Headless/bin/Release/net8.0/LivingVillage.Headless.dll" + +mkdir -p "$DIR" +if [ ! -f "$DLL" ]; then + echo "[m3r] FATAL missing build: $DLL" >&2 + exit 90 +fi + +STARTED="$(date -u +%Y-%m-%dT%H:%M:%SZ)" +echo "$$" > "$DIR/pid" +echo "[m3r] start unit=$UNIT pid=$$ commit=$COMMIT workers=$WORKERS worlds=$WORLDS days=$DAYS started_at=$STARTED" + +python3 - "$DIR/state.json" "$UNIT" "$COMMIT" "$WORKERS" "$WORLDS" "$DAYS" "$LOG" "$STARTED" "$$" <<'PY' +import json, sys +out, unit, commit, workers, worlds, days, log, started, pid = sys.argv[1:10] +state = { + "pid": int(pid), + "unit": unit, + "log": log, + "commit": commit, + "workers": int(workers), + "worlds": int(worlds), + "days": int(days), + "started_at": started, + "ended_at": None, + "exit": None, +} +with open(out, "w", encoding="utf-8") as fh: + json.dump(state, fh, ensure_ascii=False, indent=2, sort_keys=True) + fh.write("\n") +PY + +export LV_BATCH_WORKERS="$WORKERS" +( exec dotnet "$DLL" --batch "$WORLDS" "$DAYS" 2>&1 ) +CODE=$? + +ENDED="$(date -u +%Y-%m-%dT%H:%M:%SZ)" +echo "[m3r] end pid=$$ exit=$CODE ended_at=$ENDED" + +python3 - "$DIR/state.json" "$CODE" "$ENDED" <<'PY' +import json, sys +path, code, ended = sys.argv[1:4] +with open(path, encoding="utf-8") as fh: + state = json.load(fh) +state["ended_at"] = ended +state["exit"] = int(code) +with open(path, "w", encoding="utf-8") as fh: + json.dump(state, fh, ensure_ascii=False, indent=2, sort_keys=True) + fh.write("\n") +PY + +exit "$CODE" diff --git a/scripts/verify-m3r.py b/scripts/verify-m3r.py new file mode 100755 index 0000000..05c58b9 --- /dev/null +++ b/scripts/verify-m3r.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +"""M3R 缩尺批验独立验收脚本(不依赖 writer 会话,可独立复跑)。 + +用法: + python3 scripts/verify-m3r.py <acceptance.log> <state.json> [--commit 895d792] + +只读判定,不改任何产物。校验: + 1) state.json 字段齐全且自洽(pid/unit/log/commit/workers/worlds/days/起止/exit); + 2) 日志恰有 50 条 `world=` 行,索引 0..49 无重无缺,seed==42+i; + 3) 逐世界 ok 字段 == (check_a and check_b and check_c);check_a==(ratio>2.0); + check_b==(relconn 连通数 >= 达标数);check_c==(nonfinite==0); + 4) batch_summary worlds=50 且 passed+failed=50,passed/failed 与逐世界统计一致, + ratio_min/max、gini_min/max 与逐世界极值一致,gini∈(0,1); + 5) M3_ACCEPTANCE=PASS 且 ratio_min>2.0; + 6) 抽查 3 个 world 行(排序后首/中/尾)判据字段与 summary 一致; + 7) [done k/50] 恰 50 条且末条为 50/50,存在步进 `[world k] progress` 行。 +""" +import json +import re +import sys + +WORLD_RE = re.compile( + r"^world=(?P<world>\d+) seed=(?P<seed>\d+) chats_max=(?P<cmax>\d+) " + r"chats_avg=(?P<cavg>[0-9.]+) ratio=(?P<ratio>[0-9.]+) gini=(?P<gini>[0-9.]+) " + r"relcnt_max=(?P<rmax>\d+) relcnt_min=(?P<rmin>\d+) relconn=(?P<conn>\d+)/(?P<need>\d+) " + r"nonfinite=(?P<nf>\d+) check_a=(?P<a>PASS|FAIL) check_b=(?P<b>PASS|FAIL) " + r"check_c=(?P<c>PASS|FAIL) old_crit=(?P<old>PASS|FAIL) (?P<ok>OK|BAD)" + r"(?: elapsed_s=(?P<elapsed>[0-9.]+))?$" +) +SUMMARY_RE = re.compile( + r"^batch_summary worlds=(?P<worlds>\d+) passed=(?P<passed>\d+) failed=(?P<failed>\d+) " + r"old_passed=(?P<op>\d+)/(?P<ow>\d+) ratio_min=(?P<rmin>[0-9.]+) ratio_max=(?P<rmax>[0-9.]+) " + r"gini_min=(?P<gmin>[0-9.]+) gini_max=(?P<gmax>[0-9.]+)" +) +DONE_RE = re.compile(r"^\[done (?P<done>\d+)/(?P<total>\d+)\] ") +PROGRESS_RE = re.compile(r"^\[world \d+\] progress day=\d+/\d+ elapsed=[0-9.]+s$") + +REQUIRED_STATE = [ + "pid", "unit", "log", "commit", "workers", "worlds", + "days", "started_at", "ended_at", "exit", +] + + +def main(argv): + if len(argv) < 3: + print("usage: verify-m3r.py <acceptance.log> <state.json> [--commit X]") + return 2 + log_path, state_path = argv[1], argv[2] + commit_expect = None + if len(argv) >= 5 and argv[3] == "--commit": + commit_expect = argv[4] + + problems = [] + with open(log_path, encoding="utf-8", errors="replace") as fh: + lines = [line.rstrip("\n") for line in fh] + with open(state_path, encoding="utf-8") as fh: + state = json.load(fh) + + # 1) state.json + for key in REQUIRED_STATE: + if key not in state: + problems.append(f"state.json 缺字段 {key}") + if state.get("unit") != "living-village-m3r.service": + problems.append(f"unit 非法: {state.get('unit')!r}") + if not isinstance(state.get("pid"), int) or state.get("pid", 0) <= 0: + problems.append(f"pid 非法: {state.get('pid')!r}") + if state.get("exit") != 0: + problems.append(f"exit 非 0: {state.get('exit')!r}") + for key in ("started_at", "ended_at"): + if not state.get(key): + problems.append(f"{key} 为空") + if state.get("started_at") and state.get("ended_at") and state["ended_at"] < state["started_at"]: + problems.append("ended_at 早于 started_at") + if commit_expect and state.get("commit") != commit_expect: + problems.append(f"commit 不匹配: {state.get('commit')!r} != {commit_expect!r}") + for key, want in (("worlds", 50), ("days", 15)): + if state.get(key) != want: + problems.append(f"{key} != {want}: {state.get(key)!r}") + if not isinstance(state.get("workers"), int) or state.get("workers", 0) <= 0: + problems.append(f"workers 非法: {state.get('workers')!r}") + + # 2) world= 行 + worlds = {} + for line in lines: + m = WORLD_RE.match(line) + if not m: + continue + d = m.groupdict() + idx = int(d["world"]) + if idx in worlds: + problems.append(f"world={idx} 重复") + worlds[idx] = d + if sorted(worlds) != list(range(50)): + problems.append(f"world 索引非 0..49(实际 {len(worlds)} 条,缺失 {sorted(set(range(50)) - set(worlds))})") + for idx, d in worlds.items(): + if int(d["seed"]) != 42 + idx: + problems.append(f"world={idx} seed={d['seed']} != {42 + idx}") + + # 3) 逐世界判据自洽 + ok_count = 0 + checks_a = checks_b = checks_c = 0 + ratios, ginis = [], [] + for idx in sorted(worlds): + d = worlds[idx] + ratio, gini = float(d["ratio"]), float(d["gini"]) + conn, need = int(d["conn"]), int(d["need"]) + nf = int(d["nf"]) + a, b, c = d["a"] == "PASS", d["b"] == "PASS", d["c"] == "PASS" + derived = a and b and c + if (d["ok"] == "OK") != derived: + problems.append(f"world={idx} ok={d['ok']} 与 check_a/b/c 不一致") + if a != (ratio > 2.0): + problems.append(f"world={idx} check_a={d['a']} 与 ratio={ratio} 不一致") + if b != (conn >= need): + problems.append(f"world={idx} check_b={d['b']} 与 relconn={conn}/{need} 不一致") + if c != (nf == 0): + problems.append(f"world={idx} check_c={d['c']} 与 nonfinite={nf} 不一致") + if gini <= 0.0 or gini >= 1.0: + problems.append(f"world={idx} gini={gini} 不在 (0,1)") + ratios.append(ratio) + ginis.append(gini) + ok_count += 1 if d["ok"] == "OK" else 0 + checks_a += 1 if a else 0 + checks_b += 1 if b else 0 + checks_c += 1 if c else 0 + + # 4) batch_summary + summary = None + for line in lines: + m = SUMMARY_RE.match(line) + if m: + summary = m.groupdict() + if summary is None: + problems.append("缺 batch_summary 行") + else: + sw, sp, sf = int(summary["worlds"]), int(summary["passed"]), int(summary["failed"]) + if sw != 50: + problems.append(f"batch_summary worlds={sw} != 50") + if sp + sf != 50: + problems.append(f"batch_summary passed+failed={sp + sf} != 50") + if sp != ok_count or sf != 50 - ok_count: + problems.append(f"batch_summary passed={sp}/failed={sf} != 逐世界 ok={ok_count}/bad={50 - ok_count}") + if abs(float(summary["rmin"]) - min(ratios)) > 5e-4 or abs(float(summary["rmax"]) - max(ratios)) > 5e-4: + problems.append(f"batch_summary ratio 极值 [{summary['rmin']},{summary['rmax']}] != 逐世界 [{min(ratios)},{max(ratios)}]") + if abs(float(summary["gmin"]) - min(ginis)) > 5e-4 or abs(float(summary["gmax"]) - max(ginis)) > 5e-4: + problems.append(f"batch_summary gini 极值 [{summary['gmin']},{summary['gmax']}] != 逐世界 [{min(ginis)},{max(ginis)}]") + for key, val in (("gmin", float(summary["gmin"])), ("gmax", float(summary["gmax"]))): + if not 0.0 < val < 1.0: + problems.append(f"batch_summary {key}={val} 不在 (0,1)") + + # 5) 总判据 + acceptance = [line for line in lines if line.startswith("M3_ACCEPTANCE=")] + if not acceptance: + problems.append("缺 M3_ACCEPTANCE 行") + elif acceptance[-1] != "M3_ACCEPTANCE=PASS": + problems.append(f"末条 {acceptance[-1]} != M3_ACCEPTANCE=PASS") + ratio_min = min(ratios) if ratios else 0.0 + if not ratio_min > 2.0: + problems.append(f"ratio_min={ratio_min} 不满足 >2.0") + + # 6) 抽查 3 个 world 行(排序后首/中/尾) + if len(worlds) == 50: + sample = [0, 24, 49] + print("sample worlds: " + " ".join(f"{i}(ok={worlds[i]['ok']},ratio={worlds[i]['ratio']},gini={worlds[i]['gini']},relconn={worlds[i]['conn']}/{worlds[i]['need']})" for i in sample)) + if summary: + for i in sample: + r, g = float(worlds[i]["ratio"]), float(worlds[i]["gini"]) + if not (float(summary["rmin"]) <= r <= float(summary["rmax"])): + problems.append(f"sample world={i} ratio={r} 不在 summary 区间") + if not (float(summary["gmin"]) <= g <= float(summary["gmax"])): + problems.append(f"sample world={i} gini={g} 不在 summary 区间") + + # 7) done / progress + done = [DONE_RE.match(line) for line in lines] + done = [m for m in done if m] + if len(done) != 50: + problems.append(f"[done] 行数 {len(done)} != 50") + elif int(done[-1].group("done")) != 50 or int(done[-1].group("total")) != 50: + problems.append(f"末条 [done] 非 50/50: {done[-1].group(0)!r}") + progress = sum(1 for line in lines if PROGRESS_RE.match(line)) + if progress < 50: + problems.append(f"步进 progress 行数 {progress} < 50(逐世界至少一条)") + + print(f"worlds={len(worlds)} ok={ok_count} bad={50 - ok_count} check_a_pass={checks_a} check_b_pass={checks_b} check_c_pass={checks_c}") + print(f"ratio_min={ratio_min:.3f} ratio_max={max(ratios) if ratios else 0:.3f} gini_min={min(ginis) if ginis else 0:.3f} gini_max={max(ginis) if ginis else 0:.3f} progress_lines={progress}") + if problems: + print(f"M3R_VERIFY=FAIL problems={len(problems)}") + for p in problems: + print(f" - {p}") + return 1 + print("M3R_VERIFY=PASS") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/src/LivingVillage.Desktop.Tests/M3BatchOutputTests.fs b/src/LivingVillage.Desktop.Tests/M3BatchOutputTests.fs index e962e57..b1822c7 100644 --- a/src/LivingVillage.Desktop.Tests/M3BatchOutputTests.fs +++ b/src/LivingVillage.Desktop.Tests/M3BatchOutputTests.fs @@ -27,6 +27,11 @@ type M3BatchOutputTests () = Assert.AreEqual<string>("[done 4/4] elapsed=600.0s world=3", BatchOutput.doneLine 4 4 600.0 3) [<TestMethod>] + member _.ProgressLineReportsSteppedDayWithinWorld () = + Assert.AreEqual<string>("[world 7] progress day=5/15 elapsed=61.2s", BatchOutput.progressLine 7 5L 15L 61.2) + Assert.AreEqual<string>("[world 0] progress day=15/15 elapsed=260.0s", BatchOutput.progressLine 0 15L 15L 260.0) + + [<TestMethod>] member _.SummaryLineKeepsOldFieldsAndAppendsWallClock () = let line = BatchOutput.summaryLine 4 3 1 4 1.100 9.900 0.010 0.400 4 610.5 // 监督器正则只读前三个字段,前缀必须逐字保持。 diff --git a/src/LivingVillage.Headless/BatchOutput.fs b/src/LivingVillage.Headless/BatchOutput.fs index aeaef52..971da97 100644 --- a/src/LivingVillage.Headless/BatchOutput.fs +++ b/src/LivingVillage.Headless/BatchOutput.fs @@ -22,6 +22,10 @@ module BatchOutput = let doneLine (completed: int) (total: int) (elapsedSeconds: float) (world: int) : string = sprintf "[done %d/%d] elapsed=%.1fs world=%d" completed total elapsedSeconds world + /// 单世界内部步进进度行:每推进 stepEvery 个模拟日打印一次,长世界不再零进度。 + let progressLine (world: int) (day: int64) (totalDays: int64) (elapsedSeconds: float) : string = + sprintf "[world %d] progress day=%d/%d elapsed=%.1fs" world day totalDays elapsedSeconds + /// batch_summary:旧字段原样保留,追加 workers 与 wall_s(总墙钟,append 不删旧字段)。 let summaryLine (worlds: int) diff --git a/src/LivingVillage.Headless/Program.fs b/src/LivingVillage.Headless/Program.fs index 23434d0..4bd6424 100644 --- a/src/LivingVillage.Headless/Program.fs +++ b/src/LivingVillage.Headless/Program.fs @@ -71,7 +71,7 @@ type RunStats = OutOfBounds: int64 World: World } -let runSimulation (verbose: bool) (days: int64) (seed: uint64) (npcCount: int) : RunStats = +let runSimulationCore (verbose: bool) (days: int64) (seed: uint64) (npcCount: int) (onSimDay: int64 -> unit) : RunStats = let totalTicks = days * ticksPerDay if verbose then printfn "living-village headless days=%d seed=%d npcs=%d ticks=%d tickrate=%d" days seed npcCount totalTicks ticksPerSecond @@ -102,6 +102,7 @@ let runSimulation (verbose: bool) (days: int64) (seed: uint64) (npcCount: int) : driver <- d world <- Sim.step { Input = input } world t <- t + 1L + if t % ticksPerDay = 0L then onSimDay (t / ticksPerDay) let nightNow = Sim.isNightTick t if nightNow then nightTicks <- nightTicks + 1L else dayTicks <- dayTicks + 1L for npc in world.Npcs do @@ -152,6 +153,9 @@ let runSimulation (verbose: bool) (days: int64) (seed: uint64) (npcCount: int) : OutOfBounds = outOfBounds World = world } +let runSimulation (verbose: bool) (days: int64) (seed: uint64) (npcCount: int) : RunStats = + runSimulationCore verbose days seed npcCount ignore + let dumpRelations (world: World) : unit = let m = Sim.relationMatrix world let n = Array2D.length1 m @@ -405,9 +409,9 @@ type WorldVerdict = Ratio: float Gini: float } -let evalWorld (days: int64) (k: int) : WorldVerdict = +let evalWorld (days: int64) (k: int) (onSimDay: int64 -> unit) : WorldVerdict = let seed = 42UL + uint64 k - let stats = runSimulation false days seed Sim.npcCount + let stats = runSimulationCore false days seed Sim.npcCount onSimDay let m = Sim.relationMatrix stats.World let n = Array2D.length1 m let mutable hasNonFinite = stats.NonFinite > 0L @@ -461,11 +465,18 @@ let runBatch (worlds: int) (days: int64) : int = let wall = System.Diagnostics.Stopwatch.StartNew() // 逐世界流式:每个世界一完成立刻打印该行(行尾追加 elapsed_s),再打印一行 // [done k/N] 进度,并 flush,使长批次不再整轮零进度。结果仍按 world 序号落数组。 + let progressGate = obj () + let progressEvery = if days <= 5L then 1L else (days + 4L) / 5L let results = LivingVillage.Headless.BatchOutput.runStreaming worlds workers (fun k -> let sw = System.Diagnostics.Stopwatch.StartNew() - let verdict = evalWorld days k + let verdict = + evalWorld days k (fun day -> + if day % progressEvery = 0L then + lock progressGate (fun () -> + printfn "%s" (LivingVillage.Headless.BatchOutput.progressLine k day days sw.Elapsed.TotalSeconds) + Console.Out.Flush())) sw.Stop() (verdict, sw.Elapsed.TotalSeconds)) (fun k (verdict, seconds) doneCount -> |
