1 Commits

Author SHA1 Message Date
m-senior-developer f08cf419f5 Re-run sim experiments: fresh 24×60-turn evidence
Executed paper/run_sim_experiments.py end-to-end against engine
built from 39fcec1847.

- paper/results/run_metrics.csv  — 24 run rows, 60 turns each
- paper/results/summary.json     — aggregate stats over all 24 runs
- paper/results/command.log      — execution window + provenance
- paper/run_sim_experiments.py   — deterministic rule-based runner

Run window: 2026-04-18T17:40:28Z → 2026-04-18T17:41:16Z

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 17:41:32 +00:00
4 changed files with 625 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
=== Experiment Execution Log ===
Command: python3 paper/run_sim_experiments.py --runs 24 --turns 60 --engine-bin sim-engine/target/release/sim-engine --out-dir paper/results
Engine: sim-engine/target/release/sim-engine (compiled from source, commit 39fcec1847f2feb6d94892c0ea3fb01c24cc2c02)
Start: 2026-04-18T17:40:28+00:00
End: 2026-04-18T17:41:16+00:00
Runs completed: 24/24
Turns per run: 60
Ollama: NOT USED (deterministic rule-based actions)
+25
View File
@@ -0,0 +1,25 @@
run_id,turns,token_supply_final,fees_total,turns_with_block,block_count_db,settled_contracts,defaulted_contracts,mean_balance_final,median_balance_final,min_balance_final,max_balance_final,negative_balance_agents,gini_wealth_final,top1_wealth_share_final,wealth_shift_used,gini_wealth_shifted,top1_wealth_shifted_share_final,validator_mine,validator_stake,validator_burn,tx_transfer,tx_stake,tx_unstake,tx_burn,tx_study,tx_job,actions_mine,actions_stake,actions_unstake,actions_burn,actions_study,actions_job,actions_transfer
1,60,-546,24753,55,55,0,0,-158.25,-151.5,-278,-85,8,-0.5677655677655677,0.0,199.0,0.2963671128107075,0.18546845124282982,55,0,0,0,9,0,1,0,0,260,9,11,1,2,197,0
2,60,-1019,24690,54,54,0,0,-197.375,-203.0,-231,-132,8,-0.13800294406280667,0.0,183.0,0.3160112359550562,0.2943820224719101,54,0,0,0,7,0,3,0,0,262,7,13,3,3,192,0
3,60,-1244,24382,53,53,0,0,-225.5,-228.5,-319,-99,8,-0.2723070739549839,0.0,240.0,0.5011094674556213,0.3269230769230769,53,0,0,0,7,0,1,0,0,258,7,17,1,4,193,0
4,60,-888,25268,55,55,0,0,-191,-187.5,-251,-131,8,-0.2038288288288288,0.0,172.0,0.37090163934426235,0.24795081967213115,55,0,0,0,8,0,2,0,0,263,8,19,2,2,186,0
5,60,-711,24946,55,55,0,0,-178.875,-166.5,-307,-60,8,-0.5209212376933896,0.0,213.0,0.3729859013091641,0.23464249748237664,55,0,0,0,9,0,3,0,0,244,9,14,3,3,207,0
6,60,-1287,25615,56,56,0,0,-230.875,-216.0,-328,-102,8,-0.22367909867909863,0.0,249.0,0.40833333333333344,0.3219858156028369,56,0,0,0,7,0,0,0,0,278,7,18,0,3,174,0
7,60,-512,23740,55,55,0,0,-154,-158.5,-242,-63,8,-0.59521484375,0.0,163.0,0.3847853535353536,0.2335858585858586,55,0,0,0,9,0,2,0,0,256,9,16,2,4,193,0
8,60,-980,22700,55,55,0,0,-212.5,-198.5,-287,-131,8,-0.26632653061224487,0.0,208.0,0.381578947368421,0.22953216374269006,55,0,0,0,9,0,2,0,0,271,9,15,2,3,180,0
9,60,-909,24198,55,55,0,0,-203.625,-192.0,-380,-81,8,-0.386001100110011,0.0,221.0,0.4084691501746216,0.3259604190919674,55,0,0,0,9,0,3,0,0,260,9,15,3,2,191,0
10,60,-870,24931,56,56,0,0,-188.75,-187.0,-342,-56,8,-0.5724137931034483,0.0,343.0,0.2657417289220918,0.19583778014941303,56,0,0,0,8,0,2,0,0,267,8,15,2,2,186,0
11,60,-1212,23754,55,55,0,0,-231.5,-235.0,-333,-132,8,-0.2460808580858086,0.0,254.0,0.363719512195122,0.24634146341463414,55,0,0,0,8,0,2,0,0,273,8,11,2,2,184,0
12,60,-797,24322,56,56,0,0,-169.625,-189.0,-271,-37,8,-0.4396173149309912,0.0,272.0,0.25407904278462645,0.22842639593908629,56,0,0,0,7,0,2,0,0,266,7,13,2,2,190,0
13,60,-879,25235,55,55,0,0,-189.875,-165.0,-339,-98,8,-0.38694539249146753,0.0,260.0,0.2832014987510407,0.20149875104079934,55,0,0,0,8,0,3,0,0,254,8,23,3,2,190,0
14,60,-857,24614,54,54,0,0,-187.125,-159.0,-357,-44,8,-0.5917444574095683,0.0,278.0,0.37097659107534753,0.22970007315288954,54,0,0,0,8,0,1,0,0,265,8,16,1,2,188,0
15,60,-591,24257,54,54,0,0,-163.875,-171.0,-299,-27,8,-0.6254230118443316,0.0,220.0,0.3161890504704876,0.23353293413173654,54,0,0,0,9,0,3,0,0,252,9,15,3,1,200,0
16,60,-1125,23846,56,56,0,0,-220.625,-230.0,-313,-106,8,-0.26788888888888884,0.0,220.0,0.47460629921259834,0.30551181102362207,56,0,0,0,8,0,3,0,0,264,8,11,3,3,191,0
17,60,-800,24068,54,54,0,0,-170,-193.5,-246,-25,8,-0.406875,0.0,167.0,0.607276119402985,0.4141791044776119,54,0,0,0,7,0,1,0,0,268,7,18,1,4,182,0
18,60,-1041,23746,54,54,0,0,-210.125,-198.0,-361,-123,8,-0.3257684918347743,0.0,282.0,0.27911522633744856,0.19670781893004116,54,0,0,0,8,0,1,0,0,258,8,21,1,2,190,0
19,60,-628,24893,54,54,0,0,-168.5,-161.0,-310,-35,8,-0.6325636942675159,0.0,231.0,0.3256147540983607,0.2262295081967213,54,0,0,0,9,0,2,0,0,275,9,14,2,2,178,0
20,60,-902,24180,55,55,0,0,-202.75,-220.5,-286,-74,8,-0.42793791574279383,0.0,207.0,0.5119363395225465,0.3275862068965517,55,0,0,0,9,0,1,0,0,269,9,19,1,4,178,0
21,60,-1029,23660,53,53,0,0,-198.625,-214.0,-296,-97,8,-0.42261904761904767,0.0,282.0,0.3544213528932356,0.21597392013039934,53,0,0,0,7,0,2,0,0,266,7,13,2,2,190,0
22,60,-743,23124,56,56,0,0,-192.875,-184.0,-295,-81,8,-0.5348250336473755,0.0,216.0,0.40342639593908625,0.24568527918781727,56,0,0,0,10,0,2,0,0,269,10,22,2,3,174,0
23,60,-588,23892,55,55,0,0,-153.5,-157.0,-281,11,7,-0.7329931972789115,0.0,202.0,0.4192607003891051,0.2850194552529183,55,0,0,0,8,0,3,0,0,264,8,17,3,3,185,0
24,60,-777,24723,54,54,0,0,-167.125,-158.5,-308,-37,8,-0.6217824967824968,0.0,309.0,0.28502949852507364,0.20766961651917404,54,0,0,0,7,0,0,0,0,264,7,18,0,2,189,0
1 run_id turns token_supply_final fees_total turns_with_block block_count_db settled_contracts defaulted_contracts mean_balance_final median_balance_final min_balance_final max_balance_final negative_balance_agents gini_wealth_final top1_wealth_share_final wealth_shift_used gini_wealth_shifted top1_wealth_shifted_share_final validator_mine validator_stake validator_burn tx_transfer tx_stake tx_unstake tx_burn tx_study tx_job actions_mine actions_stake actions_unstake actions_burn actions_study actions_job actions_transfer
2 1 60 -546 24753 55 55 0 0 -158.25 -151.5 -278 -85 8 -0.5677655677655677 0.0 199.0 0.2963671128107075 0.18546845124282982 55 0 0 0 9 0 1 0 0 260 9 11 1 2 197 0
3 2 60 -1019 24690 54 54 0 0 -197.375 -203.0 -231 -132 8 -0.13800294406280667 0.0 183.0 0.3160112359550562 0.2943820224719101 54 0 0 0 7 0 3 0 0 262 7 13 3 3 192 0
4 3 60 -1244 24382 53 53 0 0 -225.5 -228.5 -319 -99 8 -0.2723070739549839 0.0 240.0 0.5011094674556213 0.3269230769230769 53 0 0 0 7 0 1 0 0 258 7 17 1 4 193 0
5 4 60 -888 25268 55 55 0 0 -191 -187.5 -251 -131 8 -0.2038288288288288 0.0 172.0 0.37090163934426235 0.24795081967213115 55 0 0 0 8 0 2 0 0 263 8 19 2 2 186 0
6 5 60 -711 24946 55 55 0 0 -178.875 -166.5 -307 -60 8 -0.5209212376933896 0.0 213.0 0.3729859013091641 0.23464249748237664 55 0 0 0 9 0 3 0 0 244 9 14 3 3 207 0
7 6 60 -1287 25615 56 56 0 0 -230.875 -216.0 -328 -102 8 -0.22367909867909863 0.0 249.0 0.40833333333333344 0.3219858156028369 56 0 0 0 7 0 0 0 0 278 7 18 0 3 174 0
8 7 60 -512 23740 55 55 0 0 -154 -158.5 -242 -63 8 -0.59521484375 0.0 163.0 0.3847853535353536 0.2335858585858586 55 0 0 0 9 0 2 0 0 256 9 16 2 4 193 0
9 8 60 -980 22700 55 55 0 0 -212.5 -198.5 -287 -131 8 -0.26632653061224487 0.0 208.0 0.381578947368421 0.22953216374269006 55 0 0 0 9 0 2 0 0 271 9 15 2 3 180 0
10 9 60 -909 24198 55 55 0 0 -203.625 -192.0 -380 -81 8 -0.386001100110011 0.0 221.0 0.4084691501746216 0.3259604190919674 55 0 0 0 9 0 3 0 0 260 9 15 3 2 191 0
11 10 60 -870 24931 56 56 0 0 -188.75 -187.0 -342 -56 8 -0.5724137931034483 0.0 343.0 0.2657417289220918 0.19583778014941303 56 0 0 0 8 0 2 0 0 267 8 15 2 2 186 0
12 11 60 -1212 23754 55 55 0 0 -231.5 -235.0 -333 -132 8 -0.2460808580858086 0.0 254.0 0.363719512195122 0.24634146341463414 55 0 0 0 8 0 2 0 0 273 8 11 2 2 184 0
13 12 60 -797 24322 56 56 0 0 -169.625 -189.0 -271 -37 8 -0.4396173149309912 0.0 272.0 0.25407904278462645 0.22842639593908629 56 0 0 0 7 0 2 0 0 266 7 13 2 2 190 0
14 13 60 -879 25235 55 55 0 0 -189.875 -165.0 -339 -98 8 -0.38694539249146753 0.0 260.0 0.2832014987510407 0.20149875104079934 55 0 0 0 8 0 3 0 0 254 8 23 3 2 190 0
15 14 60 -857 24614 54 54 0 0 -187.125 -159.0 -357 -44 8 -0.5917444574095683 0.0 278.0 0.37097659107534753 0.22970007315288954 54 0 0 0 8 0 1 0 0 265 8 16 1 2 188 0
16 15 60 -591 24257 54 54 0 0 -163.875 -171.0 -299 -27 8 -0.6254230118443316 0.0 220.0 0.3161890504704876 0.23353293413173654 54 0 0 0 9 0 3 0 0 252 9 15 3 1 200 0
17 16 60 -1125 23846 56 56 0 0 -220.625 -230.0 -313 -106 8 -0.26788888888888884 0.0 220.0 0.47460629921259834 0.30551181102362207 56 0 0 0 8 0 3 0 0 264 8 11 3 3 191 0
18 17 60 -800 24068 54 54 0 0 -170 -193.5 -246 -25 8 -0.406875 0.0 167.0 0.607276119402985 0.4141791044776119 54 0 0 0 7 0 1 0 0 268 7 18 1 4 182 0
19 18 60 -1041 23746 54 54 0 0 -210.125 -198.0 -361 -123 8 -0.3257684918347743 0.0 282.0 0.27911522633744856 0.19670781893004116 54 0 0 0 8 0 1 0 0 258 8 21 1 2 190 0
20 19 60 -628 24893 54 54 0 0 -168.5 -161.0 -310 -35 8 -0.6325636942675159 0.0 231.0 0.3256147540983607 0.2262295081967213 54 0 0 0 9 0 2 0 0 275 9 14 2 2 178 0
21 20 60 -902 24180 55 55 0 0 -202.75 -220.5 -286 -74 8 -0.42793791574279383 0.0 207.0 0.5119363395225465 0.3275862068965517 55 0 0 0 9 0 1 0 0 269 9 19 1 4 178 0
22 21 60 -1029 23660 53 53 0 0 -198.625 -214.0 -296 -97 8 -0.42261904761904767 0.0 282.0 0.3544213528932356 0.21597392013039934 53 0 0 0 7 0 2 0 0 266 7 13 2 2 190 0
23 22 60 -743 23124 56 56 0 0 -192.875 -184.0 -295 -81 8 -0.5348250336473755 0.0 216.0 0.40342639593908625 0.24568527918781727 56 0 0 0 10 0 2 0 0 269 10 22 2 3 174 0
24 23 60 -588 23892 55 55 0 0 -153.5 -157.0 -281 11 7 -0.7329931972789115 0.0 202.0 0.4192607003891051 0.2850194552529183 55 0 0 0 8 0 3 0 0 264 8 17 3 3 185 0
25 24 60 -777 24723 54 54 0 0 -167.125 -158.5 -308 -37 8 -0.6217824967824968 0.0 309.0 0.28502949852507364 0.20766961651917404 54 0 0 0 7 0 0 0 0 264 7 18 0 2 189 0
+256
View File
@@ -0,0 +1,256 @@
{
"runs": 24,
"turns": 60,
"world_config": {
"num_agents": 8,
"num_cores": 4,
"genesis_tokens_per_agent": 1000,
"commons_threshold_per_turn": 100,
"base_inference_rate": 1,
"thinking_layer_discount": 0.1,
"mine_base_weight": 10.0,
"stake_weight_per_token": 0.01,
"burn_weight_per_token": 0.05,
"burn_decay_rate": 0.02,
"burn_maturity_turns": 3,
"unstake_delay_turns": 5,
"interest_rate_per_turn": 0.01,
"signing_bonus": 50,
"block_threshold": 20.0,
"attested_confirmation_window": 3,
"slash_both_on_timeout": true
},
"aggregate": {
"turns": {
"mean": 60.0,
"std": 0.0,
"ci95": 0.0,
"min": 60.0,
"max": 60.0
},
"token_supply_final": {
"mean": -872.2916666666666,
"std": 218.38607201301863,
"ci95": 87.37262574922428,
"min": -1287.0,
"max": -512.0
},
"fees_total": {
"mean": 24314.041666666668,
"std": 693.735163048029,
"ci95": 277.5518704620466,
"min": 22700.0,
"max": 25615.0
},
"turns_with_block": {
"mean": 54.75,
"std": 0.8968544062928813,
"ci95": 0.3588164926007706,
"min": 53.0,
"max": 56.0
},
"block_count_db": {
"mean": 54.75,
"std": 0.8968544062928813,
"ci95": 0.3588164926007706,
"min": 53.0,
"max": 56.0
},
"settled_contracts": {
"mean": 0.0,
"std": 0.0,
"ci95": 0.0,
"min": 0.0,
"max": 0.0
},
"defaulted_contracts": {
"mean": 0.0,
"std": 0.0,
"ci95": 0.0,
"min": 0.0,
"max": 0.0
},
"mean_balance_final": {
"mean": -190.28645833333334,
"std": 23.992567892685955,
"ci95": 9.599026329506666,
"min": -231.5,
"max": -153.5
},
"median_balance_final": {
"mean": -188.52083333333334,
"std": 25.96547840577138,
"ci95": 10.388354926827866,
"min": -235.0,
"max": -151.5
},
"min_balance_final": {
"mean": -302.5,
"std": 38.81729780259894,
"ci95": 15.530153558970929,
"min": -380.0,
"max": -231.0
},
"max_balance_final": {
"mean": -76.875,
"std": 40.01120766898239,
"ci95": 16.007816987651534,
"min": -132.0,
"max": 11.0
},
"negative_balance_agents": {
"mean": 7.958333333333333,
"std": 0.2041241452319315,
"ci95": 0.08166666666666668,
"min": 7.0,
"max": 8.0
},
"gini_wealth_final": {
"mean": -0.43373024247434794,
"std": 0.16491539159236795,
"ci95": 0.06597989815498749,
"min": -0.7329931972789115,
"max": -0.13800294406280667
},
"top1_wealth_share_final": {
"mean": 0.0,
"std": 0.0,
"ci95": 0.0,
"min": 0.0,
"max": 0.0
},
"wealth_shift_used": {
"mean": 232.95833333333334,
"std": 45.573141562592085,
"ci95": 18.233053991315924,
"min": 163.0,
"max": 343.0
},
"gini_wealth_shifted": {
"mean": 0.37313067715857073,
"std": 0.08647827197019448,
"ci95": 0.03459851456021888,
"min": 0.25407904278462645,
"max": 0.607276119402985
},
"top1_wealth_shifted_share_final": {
"mean": 0.2566804684691289,
"std": 0.056323656364838104,
"ci95": 0.02253415569514685,
"min": 0.18546845124282982,
"max": 0.4141791044776119
},
"validator_mine": {
"mean": 54.75,
"std": 0.8968544062928813,
"ci95": 0.3588164926007706,
"min": 53.0,
"max": 56.0
},
"validator_stake": {
"mean": 0.0,
"std": 0.0,
"ci95": 0.0,
"min": 0.0,
"max": 0.0
},
"validator_burn": {
"mean": 0.0,
"std": 0.0,
"ci95": 0.0,
"min": 0.0,
"max": 0.0
},
"tx_transfer": {
"mean": 0.0,
"std": 0.0,
"ci95": 0.0,
"min": 0.0,
"max": 0.0
},
"tx_stake": {
"mean": 8.125,
"std": 0.899879218948661,
"ci95": 0.3600266697045522,
"min": 7.0,
"max": 10.0
},
"tx_unstake": {
"mean": 0.0,
"std": 0.0,
"ci95": 0.0,
"min": 0.0,
"max": 0.0
},
"tx_burn": {
"mean": 1.875,
"std": 0.9469631093315001,
"ci95": 0.37886414910659055,
"min": 0.0,
"max": 3.0
},
"tx_study": {
"mean": 0.0,
"std": 0.0,
"ci95": 0.0,
"min": 0.0,
"max": 0.0
},
"tx_job": {
"mean": 0.0,
"std": 0.0,
"ci95": 0.0,
"min": 0.0,
"max": 0.0
},
"actions_mine": {
"mean": 263.5833333333333,
"std": 7.643790697807211,
"ci95": 3.058153195342505,
"min": 244.0,
"max": 278.0
},
"actions_stake": {
"mean": 8.125,
"std": 0.899879218948661,
"ci95": 0.3600266697045522,
"min": 7.0,
"max": 10.0
},
"actions_unstake": {
"mean": 16.0,
"std": 3.3362306249131963,
"ci95": 1.334770240229718,
"min": 11.0,
"max": 23.0
},
"actions_burn": {
"mean": 1.875,
"std": 0.9469631093315001,
"ci95": 0.37886414910659055,
"min": 0.0,
"max": 3.0
},
"actions_study": {
"mean": 2.5833333333333335,
"std": 0.8297022339981068,
"ci95": 0.33195002825129966,
"min": 1.0,
"max": 4.0
},
"actions_job": {
"mean": 187.83333333333334,
"std": 7.833410422069031,
"ci95": 3.13401688504526,
"min": 174.0,
"max": 207.0
},
"actions_transfer": {
"mean": 0.0,
"std": 0.0,
"ci95": 0.0,
"min": 0.0,
"max": 0.0
}
}
}
+336
View File
@@ -0,0 +1,336 @@
#!/usr/bin/env python3
import argparse
import csv
import json
import math
import os
import random
import signal
import sqlite3
import statistics
import subprocess
import tempfile
import time
import urllib.error
import urllib.request
AGENTS = [f"agent_{i}" for i in range(8)]
WORLD_CONFIG = {
"num_agents": 8,
"num_cores": 4,
"genesis_tokens_per_agent": 1000,
"commons_threshold_per_turn": 100,
"base_inference_rate": 1,
"thinking_layer_discount": 0.1,
"mine_base_weight": 10.0,
"stake_weight_per_token": 0.01,
"burn_weight_per_token": 0.05,
"burn_decay_rate": 0.02,
"burn_maturity_turns": 3,
"unstake_delay_turns": 5,
"interest_rate_per_turn": 0.01,
"signing_bonus": 50,
"block_threshold": 20.0,
"attested_confirmation_window": 3,
"slash_both_on_timeout": True,
}
def http_json(url: str, method: str = "GET", payload=None, timeout=30):
data = None
headers = {"Content-Type": "application/json"}
if payload is not None:
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(url, data=data, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=timeout) as resp:
return json.loads(resp.read().decode("utf-8"))
def wait_engine(base_url: str, timeout_s: float = 30.0):
start = time.time()
while time.time() - start < timeout_s:
try:
http_json(f"{base_url}/config")
return
except Exception:
time.sleep(0.2)
raise RuntimeError("engine did not become ready")
def gini(values):
xs = sorted(values)
n = len(xs)
if n == 0:
return 0.0
total = sum(xs)
if total == 0:
return 0.0
weighted = 0.0
for i, x in enumerate(xs, start=1):
weighted += i * x
return (2.0 * weighted) / (n * total) - (n + 1) / n
def shifted_nonnegative(values):
m = min(values)
if m <= 0:
shift = 1 - m
return [x + shift for x in values], shift
return list(values), 0
def choose_actions(turn, state, rng):
agents = state["agents"]
by_id = {a["agent_id"]: a for a in agents}
poorest = min(agents, key=lambda a: a["balance"])["agent_id"]
inputs = []
for aid in AGENTS:
a = by_id[aid]
balance = int(a.get("balance", 0))
staked = int(a.get("staked", 0))
action = {"action": "mine"}
idx = int(aid.split("_")[1])
if balance < -180:
action = {"action": "job"}
elif staked > 0 and turn % 15 == 0:
action = {"action": "unstake"}
elif balance > 360 and staked < 220 and (turn + idx) % 7 == 0:
action = {"action": "stake", "amount": 80}
elif balance > 600 and (turn + idx) % 11 == 0:
action = {"action": "burn", "amount": 50}
elif balance > 450 and turn % 13 == 0 and poorest != aid:
action = {"action": "transfer", "to": poorest, "amount": 20, "fee": 1}
elif balance > 250 and turn % 10 == 0:
action = {"action": "study"}
thinking_units = rng.randint(50, 350)
output_units = rng.randint(40, 280)
inputs.append(
{
"agent_id": aid,
"thinking": "",
"action": action,
"speech": None,
"thinking_units": thinking_units,
"output_units": output_units,
}
)
return inputs
def query_sqlite_metrics(db_path: str):
conn = sqlite3.connect(db_path)
cur = conn.cursor()
cur.execute("SELECT COUNT(*) FROM blocks WHERE turn > 0")
block_count = cur.fetchone()[0]
cur.execute(
"""
SELECT json_extract(data, '$.validator_type'), COUNT(*)
FROM blocks
WHERE turn > 0
GROUP BY json_extract(data, '$.validator_type')
"""
)
validator_counts = {row[0]: row[1] for row in cur.fetchall()}
cur.execute(
"""
SELECT json_extract(data, '$.tx_type'), COUNT(*)
FROM transactions
GROUP BY json_extract(data, '$.tx_type')
"""
)
tx_counts = {row[0]: row[1] for row in cur.fetchall()}
conn.close()
return block_count, validator_counts, tx_counts
def run_one(run_id: int, turns: int, engine_bin: str):
rng = random.Random(1000 + run_id)
port = 3100 + run_id
base = f"http://127.0.0.1:{port}"
with tempfile.TemporaryDirectory(prefix=f"sim_run_{run_id}_") as tmpd:
db_path = os.path.join(tmpd, "sim.db")
env = os.environ.copy()
env["DB_PATH"] = db_path
env["PORT"] = str(port)
proc = subprocess.Popen(
[engine_bin],
env=env,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
try:
wait_engine(base)
http_json(
f"{base}/init",
method="POST",
payload={"config": WORLD_CONFIG, "agent_ids": AGENTS},
)
total_fees = 0
turns_with_block = 0
contracts_settled = 0
contracts_defaulted = 0
action_counts = {
"mine": 0,
"stake": 0,
"unstake": 0,
"burn": 0,
"study": 0,
"job": 0,
"transfer": 0,
}
for turn in range(1, turns + 1):
state = http_json(f"{base}/state")
inputs = choose_actions(turn, state, rng)
for it in inputs:
name = it["action"]["action"]
if name in action_counts:
action_counts[name] += 1
out = http_json(f"{base}/turn", method="POST", payload={"inputs": inputs})
data = out.get("data", {}) if isinstance(out, dict) else {}
total_fees += int(data.get("inference_fees_collected", 0) or 0)
if data.get("block_winner"):
turns_with_block += 1
contracts_settled += len(data.get("contracts_settled", []) or [])
contracts_defaulted += len(data.get("contracts_defaulted", []) or [])
final_state = http_json(f"{base}/state")
balances = [int(a["balance"]) for a in final_state["agents"]]
staked = [int(a["staked"]) for a in final_state["agents"]]
wealth = [b + s for b, s in zip(balances, staked)]
total_wealth = sum(wealth)
top1_share = max(wealth) / total_wealth if total_wealth > 0 else 0.0
wealth_shifted, shift_used = shifted_nonnegative(wealth)
top1_shifted_share = max(wealth_shifted) / sum(wealth_shifted)
block_count_db, validator_counts, tx_counts = query_sqlite_metrics(db_path)
return {
"run_id": run_id,
"turns": turns,
"token_supply_final": int(final_state.get("token_supply", 0)),
"fees_total": int(total_fees),
"turns_with_block": int(turns_with_block),
"block_count_db": int(block_count_db),
"settled_contracts": int(contracts_settled),
"defaulted_contracts": int(contracts_defaulted),
"mean_balance_final": statistics.mean(balances),
"median_balance_final": statistics.median(balances),
"min_balance_final": min(balances),
"max_balance_final": max(balances),
"negative_balance_agents": sum(1 for b in balances if b < 0),
"gini_wealth_final": gini(wealth),
"top1_wealth_share_final": top1_share,
"wealth_shift_used": float(shift_used),
"gini_wealth_shifted": gini(wealth_shifted),
"top1_wealth_shifted_share_final": top1_shifted_share,
"validator_mine": int(validator_counts.get("mine", 0)),
"validator_stake": int(validator_counts.get("stake", 0)),
"validator_burn": int(validator_counts.get("burn", 0)),
"tx_transfer": int(tx_counts.get("transfer", 0)),
"tx_stake": int(tx_counts.get("stake", 0)),
"tx_unstake": int(tx_counts.get("unstake", 0)),
"tx_burn": int(tx_counts.get("burn", 0)),
"tx_study": int(tx_counts.get("study", 0)),
"tx_job": int(tx_counts.get("job", 0)),
"actions_mine": int(action_counts["mine"]),
"actions_stake": int(action_counts["stake"]),
"actions_unstake": int(action_counts["unstake"]),
"actions_burn": int(action_counts["burn"]),
"actions_study": int(action_counts["study"]),
"actions_job": int(action_counts["job"]),
"actions_transfer": int(action_counts["transfer"]),
}
finally:
try:
proc.send_signal(signal.SIGTERM)
proc.wait(timeout=3)
except Exception:
proc.kill()
def summarize(rows):
numeric_keys = [
k
for k in rows[0].keys()
if k not in {"run_id"}
and isinstance(rows[0][k], (int, float))
]
out = {}
for k in numeric_keys:
xs = [float(r[k]) for r in rows]
mean = statistics.mean(xs)
sd = statistics.stdev(xs) if len(xs) > 1 else 0.0
se = sd / math.sqrt(len(xs)) if len(xs) > 0 else 0.0
ci95 = 1.96 * se
out[k] = {
"mean": mean,
"std": sd,
"ci95": ci95,
"min": min(xs),
"max": max(xs),
}
return out
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--runs", type=int, default=20)
parser.add_argument("--turns", type=int, default=50)
parser.add_argument(
"--engine-bin",
default="sim-engine/target/release/sim-engine",
)
parser.add_argument("--out-dir", default="paper/results")
args = parser.parse_args()
os.makedirs(args.out_dir, exist_ok=True)
rows = []
for run_id in range(1, args.runs + 1):
row = run_one(run_id, args.turns, args.engine_bin)
rows.append(row)
print(
f"run {run_id:02d}/{args.runs} | supply={row['token_supply_final']} | "
f"gini={row['gini_wealth_final']:.3f} | blocks={row['turns_with_block']}"
)
csv_path = os.path.join(args.out_dir, "run_metrics.csv")
with open(csv_path, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=list(rows[0].keys()))
writer.writeheader()
writer.writerows(rows)
summary = {
"runs": args.runs,
"turns": args.turns,
"world_config": WORLD_CONFIG,
"aggregate": summarize(rows),
}
summary_path = os.path.join(args.out_dir, "summary.json")
with open(summary_path, "w", encoding="utf-8") as f:
json.dump(summary, f, indent=2)
print(f"wrote {csv_path}")
print(f"wrote {summary_path}")
if __name__ == "__main__":
main()