docs: update Potatium v0.6 architecture and PoP research
This commit is contained in:
1 parent
75df8a5c0d
commit
862ccf1d03
34 files changed
+3257
-18
No files matched your search
@@ -0,0 +1,120 @@
|
||||
"""Off-chain PoP cluster simulator tests; no chain implementation is tested."""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import math
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "simulate_pop_cluster.py"
|
||||
SPEC = importlib.util.spec_from_file_location("simulate_pop_cluster", SCRIPT)
|
||||
mod = importlib.util.module_from_spec(SPEC)
|
||||
sys.modules[SPEC.name] = mod
|
||||
SPEC.loader.exec_module(mod)
|
||||
|
||||
|
||||
class PopClusterTests(unittest.TestCase):
|
||||
def test_worker_fees_reproduce_v04_examples(self):
|
||||
expected = {0: 0, 1: .001, 3: .003, 4: .006, 7: .015, 8: .020, 10: .030}
|
||||
for count, fee in expected.items():
|
||||
self.assertAlmostEqual(mod.worker_fee(count, 'marginal'), fee)
|
||||
self.assertAlmostEqual(mod.worker_fee(7, 'tier'), .003)
|
||||
self.assertAlmostEqual(mod.worker_fee(10, 'tier'), .005)
|
||||
self.assertAlmostEqual(mod.worker_fee(10, 'marginal', 2), .060)
|
||||
|
||||
def test_window_fees_mirror_offline_v02(self):
|
||||
self.assertAlmostEqual(mod.window_fee(1, 100, 'low'), 0)
|
||||
self.assertAlmostEqual(mod.progressive_fee(100), .0185)
|
||||
self.assertAlmostEqual(mod.window_fee(2, 100, 'low'), .001185)
|
||||
self.assertAlmostEqual(mod.window_fee(3, 100, 'low'), .00337)
|
||||
self.assertAlmostEqual(mod.window_fee(2, 0, 'mild'), .001)
|
||||
|
||||
def test_marginal_ticket_value_declines_for_owner_with_existing_tickets(self):
|
||||
self.assertAlmostEqual(mod.marginal_share_value(1, 0, 4), 0.2)
|
||||
self.assertAlmostEqual(mod.marginal_share_value(1, 1, 4), 4 / 30)
|
||||
self.assertAlmostEqual(mod.marginal_share_value(1, 3, 0), 0)
|
||||
|
||||
def test_validations(self):
|
||||
for count in (-1, 11):
|
||||
with self.assertRaises(ValueError):
|
||||
mod.worker_fee(count)
|
||||
with self.assertRaises(ValueError):
|
||||
mod.worker_fee(2, scale=float('nan'))
|
||||
with self.assertRaises(ValueError):
|
||||
mod.simulate(operators=0)
|
||||
with self.assertRaises(ValueError):
|
||||
mod.simulate(operators=3, target_workers=11)
|
||||
with self.assertRaises(ValueError):
|
||||
mod.simulate(operators=3, target_workers=0, worker_scale=-1)
|
||||
|
||||
def test_worker_activation_is_upfront_and_budget_limited(self):
|
||||
op = mod.Operator(0, True, 10, 'always', balance=.006)
|
||||
activated = mod.activate_workers(op, 'marginal', 1)
|
||||
self.assertEqual(activated, 4)
|
||||
self.assertAlmostEqual(op.balance, 0)
|
||||
self.assertAlmostEqual(op.burned_worker, .006)
|
||||
self.assertEqual(op.skipped_worker_budget, 6)
|
||||
op2 = mod.Operator(1, True, 10, 'always', balance=0)
|
||||
self.assertEqual(mod.activate_workers(op2, 'marginal', 1), 0)
|
||||
self.assertAlmostEqual(op2.burned_worker, 0)
|
||||
|
||||
def test_one_payout_per_round_and_supply_invariant(self):
|
||||
result = mod.simulate(operators=4, target_workers=3, days=3,
|
||||
rounds_per_day=25, seed=123, target_policy='always',
|
||||
others_policy='free', window_model='low', initial_balance=2)
|
||||
summ = result['summary']
|
||||
self.assertEqual(summ['rounds'], 75)
|
||||
self.assertEqual(sum(op['wins'] for op in result['operators']),
|
||||
75 - summ['no_ticket_rounds'])
|
||||
self.assertAlmostEqual(summ['minted'] - summ['burned_total'] + 8,
|
||||
summ['circulating'], places=5)
|
||||
self.assertGreater(summ['burned_workers'], 0)
|
||||
self.assertGreater(summ['burned_windows'], 0)
|
||||
self.assertGreater(result['operators'][0]['window2'], 0)
|
||||
self.assertGreater(result['operators'][0]['window3'], 0)
|
||||
self.assertEqual(sum(op['tickets'] for op in result['operators']),
|
||||
summ['total_tickets'])
|
||||
self.assertEqual(result['operators'][0]['wallet'], summ['target_wallet'])
|
||||
|
||||
def test_seed_is_deterministic(self):
|
||||
args = dict(operators=5, days=2, rounds_per_day=30,
|
||||
target_workers=5, others_workers=1, seed=567, initial_balance=5)
|
||||
self.assertEqual(mod.simulate(**args), mod.simulate(**args))
|
||||
|
||||
def test_zero_fees_with_free_only_equals_minted(self):
|
||||
data = mod.simulate(operators=5, days=2, rounds_per_day=120,
|
||||
target_workers=0, target_policy='free', others_policy='free')
|
||||
self.assertEqual(data['summary']['burned_total'], 0)
|
||||
self.assertAlmostEqual(sum(x['wallet'] for x in data['operators']),
|
||||
data['summary']['minted'], places=5)
|
||||
|
||||
def test_individual_operator_90_day_history_expires(self):
|
||||
op = mod.Operator(0, True, 0, 'free')
|
||||
op.add_win(0, 1)
|
||||
self.assertEqual(op.recent, 1)
|
||||
for day in range(1, 90):
|
||||
op.expire_day(day)
|
||||
self.assertEqual(op.recent, 1)
|
||||
op.expire_day(90)
|
||||
self.assertEqual(op.recent, 0)
|
||||
|
||||
def test_newcomers_join_at_configured_day(self):
|
||||
data = mod.simulate(operators=3, newcomers=2, join_day=2,
|
||||
days=4, rounds_per_day=8,
|
||||
target_policy='free', others_policy='free')
|
||||
self.assertEqual([d['active_operators'] for d in data['daily']], [3, 3, 5, 5])
|
||||
self.assertEqual(len(data['operators']), 5)
|
||||
|
||||
def test_comparison_delta_normalization(self):
|
||||
results = [mod.simulate(operators=3, target_workers=w, days=1,
|
||||
rounds_per_day=50, seed=1, target_policy='free')
|
||||
for w in (0, 2)]
|
||||
rows = mod.compare(results)
|
||||
self.assertIsNone(rows[0]['delta_net_per_added_worker'])
|
||||
self.assertAlmostEqual(rows[1]['delta_net_per_added_worker'],
|
||||
(rows[1]['net_ptm']-rows[0]['net_ptm'])/2)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,155 @@
|
||||
"""Research simulator tests, not blockchain consensus tests."""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import math
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
SOURCE = Path(__file__).resolve().parents[1] / "scripts" / "simulate_pop_dynamic.py"
|
||||
spec = importlib.util.spec_from_file_location("dynamic", SOURCE)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
import sys
|
||||
sys.modules[spec.name] = mod
|
||||
spec.loader.exec_module(mod)
|
||||
|
||||
|
||||
class FeeTests(unittest.TestCase):
|
||||
def test_worker_fee_staffel(self):
|
||||
assert mod.worker_fee(0, 5) == 0
|
||||
self.assertAlmostEqual(mod.worker_fee(3, 1), .003)
|
||||
self.assertAlmostEqual(mod.worker_fee(7, 1), .015)
|
||||
self.assertAlmostEqual(mod.worker_fee(8, 1), .020)
|
||||
self.assertAlmostEqual(mod.worker_fee(10, 5), .150)
|
||||
|
||||
def test_invalid_values(self):
|
||||
for n in [-1, 11]:
|
||||
with self.assertRaises(ValueError):
|
||||
mod.worker_fee(n, 1)
|
||||
for scale in [-2, math.nan, math.inf]:
|
||||
with self.assertRaises(ValueError):
|
||||
mod.worker_fee(2, scale)
|
||||
with self.assertRaises(ValueError):
|
||||
mod.simulate(operators=1)
|
||||
with self.assertRaises(ValueError):
|
||||
mod.simulate(capacities=(12,))
|
||||
with self.assertRaises(ValueError):
|
||||
mod.simulate(fund_mode="burn")
|
||||
|
||||
def test_window_fee_progressivity(self):
|
||||
self.assertAlmostEqual(mod.progressive_fee(10), .0005)
|
||||
self.assertAlmostEqual(mod.progressive_fee(100), .0185)
|
||||
self.assertGreater(mod.window_fee(3, 100), mod.window_fee(2,100))
|
||||
with self.assertRaises(ValueError):
|
||||
mod.window_fee(1, 0)
|
||||
|
||||
def test_worker_decision_and_budget(self):
|
||||
f = mod.choose_worker_count
|
||||
kw = dict(capacity=10, scale=5, rivals_estimated_tickets=12,
|
||||
f1_chance=.55, reward=1.)
|
||||
self.assertEqual(f(balance=0., mode="fixed", **kw), 0)
|
||||
self.assertEqual(f(balance=2., mode="fixed", **kw), 10)
|
||||
choice = f(balance=2., mode="dynamic", **kw)
|
||||
self.assertLessEqual(choice, 10)
|
||||
self.assertGreaterEqual(choice, 0)
|
||||
# A much stronger levy must make additional workers less worthwhile.
|
||||
high = f(balance=20., mode="dynamic", scale=100,
|
||||
capacity=10, rivals_estimated_tickets=12, f1_chance=.55, reward=1.)
|
||||
self.assertEqual(high, 0)
|
||||
|
||||
def test_ticket_marginal(self):
|
||||
self.assertAlmostEqual(mod.marginal_ticket_gain(0,4,1), .2)
|
||||
self.assertAlmostEqual(mod.marginal_ticket_gain(1,4,1), 4/30)
|
||||
self.assertAlmostEqual(mod.marginal_ticket_gain(4,0,1), 0)
|
||||
|
||||
|
||||
class SimulationTests(unittest.TestCase):
|
||||
def short(self, **kwargs):
|
||||
defaults = dict(operators=6, newcomers=2, join_day=3,
|
||||
days=8, rounds_per_day=30, seed=123, capacities=(0,1,3,5,8,10))
|
||||
defaults.update(kwargs)
|
||||
return mod.simulate(**defaults)
|
||||
|
||||
def test_fund_holds_all_fee_income(self):
|
||||
r = self.short(fund_mode="hold", worker_mode="fixed", worker_scale=3)
|
||||
s = r["summary"]
|
||||
self.assertEqual(s["burned"], 0)
|
||||
self.assertAlmostEqual(s["fund_balance"], s["fund_received_fees"], 4)
|
||||
self.assertEqual(s["fund_distributed"], 0)
|
||||
self.assertAlmostEqual(s["fund_paid_window"]+s["fund_paid_worker"],s["fund_received_fees"],4)
|
||||
self.assertAlmostEqual(s["total_wallets"] + s["fund_balance"], s["total_supply"],4)
|
||||
|
||||
def test_redistribution_conserves_ptm_and_creates_no_new_coins(self):
|
||||
r = self.short(fund_mode="redistribute", payout_interval=2,
|
||||
payout_share=.5, worker_mode="fixed", worker_scale=5)
|
||||
s = r["summary"]
|
||||
self.assertGreater(s["fund_distributed"], 0)
|
||||
self.assertGreater(s["fund_balance"], 0)
|
||||
self.assertAlmostEqual(s["fund_balance"]+s["fund_distributed"], s["fund_received_fees"], 4)
|
||||
self.assertAlmostEqual(s["total_wallets"]+s["fund_balance"], s["total_supply"], 4)
|
||||
self.assertAlmostEqual(sum(o["fund_received"] for o in r["operators"]),s["fund_distributed"], 4)
|
||||
|
||||
def test_free_hubs_no_fees_no_fund(self):
|
||||
r = self.short(capacities=(0,), window_policy="free",worker_mode="dynamic")
|
||||
self.assertEqual(r["summary"]["fund_received_fees"], 0)
|
||||
self.assertEqual(r["summary"]["fund_balance"], 0)
|
||||
self.assertAlmostEqual(r["summary"]["minted"],r["summary"]["total_wallets"],4)
|
||||
|
||||
def test_one_winner_per_round_and_ticket_limit(self):
|
||||
r = self.short(worker_mode="dynamic",fund_mode="redistribute",payout_interval=1)
|
||||
s = r["summary"]
|
||||
self.assertEqual(sum(o["wins"] for o in r["operators"])+s["no_ticket_rounds"],s["rounds"])
|
||||
for op in r["operators"]:
|
||||
self.assertLessEqual(op["tickets"],op["active_rounds"]*(1+op["capacity"]))
|
||||
self.assertLessEqual(sum(o["tickets"] for o in r["operators"]),
|
||||
sum(op["active_rounds"]*(1+op["capacity"]) for op in r["operators"]))
|
||||
|
||||
def test_newcomers_only_start_on_join_day(self):
|
||||
r = self.short(join_day=4, days=8, rounds_per_day=7)
|
||||
newcomers = [o for o in r["operators"] if o["join_day"] > 0]
|
||||
self.assertEqual(len(newcomers),2)
|
||||
self.assertTrue(all(o["active_rounds"]==4*7 for o in newcomers))
|
||||
self.assertEqual(r["daily"][2]["active_operators"],6)
|
||||
self.assertEqual(r["daily"][4]["active_operators"],8)
|
||||
|
||||
def test_fixed_vs_dynamic_different_for_high_cost(self):
|
||||
args = dict(worker_scale=30, fund_mode="hold",initial_balance=20.)
|
||||
a = self.short(worker_mode="fixed",**args)
|
||||
b = self.short(worker_mode="dynamic",**args)
|
||||
self.assertGreater(a["summary"]["total_avg_workers"],b["summary"]["total_avg_workers"])
|
||||
|
||||
def test_determinism(self):
|
||||
settings = dict(worker_mode="dynamic", fund_mode="redistribute", payout_interval=2)
|
||||
self.assertEqual(self.short(**settings), self.short(**settings))
|
||||
|
||||
def test_zero_payout_matches_hold_except_mode(self):
|
||||
a = self.short(fund_mode="hold")
|
||||
b = self.short(fund_mode="redistribute",payout_share=0,payout_interval=2)
|
||||
self.assertEqual(a["summary"],b["summary"])
|
||||
self.assertEqual(a["daily"],b["daily"])
|
||||
self.assertEqual(a["operators"],b["operators"])
|
||||
|
||||
def test_rolling_90day_expiry(self):
|
||||
op = mod.Operator(1,1,0,0.)
|
||||
op.reward(0,2.)
|
||||
self.assertEqual(op.recent,2.)
|
||||
op.expire(90)
|
||||
self.assertEqual(op.recent,0.)
|
||||
self.assertEqual(op.history[0],0.)
|
||||
|
||||
def test_export(self):
|
||||
a = self.short()
|
||||
b = self.short(worker_mode="fixed")
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
paths = mod.write_output(Path(temp),[a,b],mod.aggregate([a,b]),"test")
|
||||
self.assertEqual(len(paths),5)
|
||||
self.assertTrue(all(p.is_file() and p.stat().st_size>0 for p in paths))
|
||||
payload = json.loads(paths[4].read_text())
|
||||
self.assertEqual(payload["scenario_count"],2)
|
||||
self.assertEqual(len(payload["comparisons"]),2)
|
||||
|
||||
|
||||
if __name__=="__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,93 @@
|
||||
"""Pure off-chain accounting and ZIP importer tests."""
|
||||
from __future__ import annotations
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
S = Path(__file__).resolve().parents[1] / 'scripts' / 'analyze_pop_fee_flows.py'
|
||||
spec = importlib.util.spec_from_file_location('analyze_pop_fee_flows', S)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = mod
|
||||
spec.loader.exec_module(mod)
|
||||
|
||||
|
||||
def sample():
|
||||
return {'parameters': {'target_workers': 1, 'seed': 2, 'operators': 2, 'days': 2,
|
||||
'other_workers': 0, 'worker_fee_scale': 1.0,
|
||||
'initial_balance': 1.0, 'others_workers': 0, 'newcomers': 0},
|
||||
'summary': {'minted': 8.0, 'burned_workers': 2.0, 'burned_windows': 1.0,
|
||||
'burned_total': 3.0, 'target_net': 2.5,
|
||||
'target_win_percent': 25., 'gini_operator_wallets': .1},
|
||||
'operators': [{'wallet': 3.0}, {'wallet': 4.0}]}
|
||||
|
||||
|
||||
class FeeFlowTests(unittest.TestCase):
|
||||
def test_all_policies_conserve_supply(self):
|
||||
for p in mod.presets():
|
||||
r = mod.project(sample(), p, 4)
|
||||
self.assertAlmostEqual(r['miner_wallets'] + r['paid_validators'] + r['paid_treasury'],
|
||||
r['outstanding_supply'])
|
||||
self.assertAlmostEqual(r['burned'] + r['paid_validators'] + r['paid_treasury'], 3)
|
||||
|
||||
def test_burn_reduces_outstanding(self):
|
||||
r = mod.project(sample(), mod.presets()[0], 4)
|
||||
self.assertEqual((r['outstanding_supply'], r['burned'], r['paid_validators']), (7, 3, 0))
|
||||
|
||||
def test_no_burn_increases_outstanding_but_not_miner_net(self):
|
||||
burn = mod.project(sample(), mod.presets()[0], 4)
|
||||
hybrid = mod.project(sample(), mod.presets()[3], 4)
|
||||
self.assertEqual((hybrid['burned'], hybrid['outstanding_supply']), (0, 10))
|
||||
self.assertAlmostEqual(hybrid['paid_validators'], 2.4)
|
||||
self.assertAlmostEqual(hybrid['paid_treasury'], .6)
|
||||
self.assertEqual(hybrid['target_miner_net'], burn['target_miner_net'])
|
||||
|
||||
def test_different_destinations_worker_and_window(self):
|
||||
d = mod.project(sample(), mod.presets()[-1], 2)
|
||||
self.assertEqual((d['paid_validators'], d['paid_treasury'], d['burned']), (1, 2, 0))
|
||||
|
||||
def test_custom_triplets(self):
|
||||
self.assertEqual(mod.parse_split('0,80,20'), mod.Split(0, .8, .2))
|
||||
for s in ('70,10,10', 'nan,10,90', '0,10', '1,2,foo'):
|
||||
with self.assertRaises(ValueError): mod.parse_split(s)
|
||||
|
||||
def test_reject_bad_accounting(self):
|
||||
bad = sample()
|
||||
bad['operators'][0]['wallet'] = 99
|
||||
with self.assertRaises(ValueError): mod.project(bad, mod.presets()[0], 4)
|
||||
|
||||
def test_grouping_seeds(self):
|
||||
a = sample()
|
||||
b = sample()
|
||||
b['parameters']['seed'] = 4
|
||||
runs = [mod.project(v, mod.presets()[0], 4) for v in (a, b)]
|
||||
rows = mod.summarize(runs)
|
||||
self.assertEqual(len(rows), 1)
|
||||
self.assertEqual(rows[0]['seeds'], 2)
|
||||
|
||||
def test_read_zip_without_extracting(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
path = Path(d) / 'results.zip'
|
||||
with zipfile.ZipFile(path, 'w') as z:
|
||||
z.writestr('potatium-simulation/pop_cluster_sample.json', json.dumps({'scenarios': [sample()]}))
|
||||
z.writestr('../should_not_be_extracted.txt', 'nothing')
|
||||
source, names = mod.load_sources(path)
|
||||
self.assertEqual(len(source), 1)
|
||||
self.assertEqual(len(names), 1)
|
||||
self.assertFalse((Path(d).parent / 'should_not_be_extracted.txt').exists())
|
||||
|
||||
def test_output_package(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
runs = [mod.project(sample(), x, 4) for x in mod.presets()]
|
||||
paths = mod.output(Path(d), mod.summarize(runs), runs, ['a.json'])
|
||||
self.assertEqual(len(paths), 5)
|
||||
with zipfile.ZipFile(paths[-1]) as z:
|
||||
self.assertIsNone(z.testzip())
|
||||
self.assertEqual(len(z.namelist()), 4)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Unit tests for offline research simulation only (not consensus tests)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
|
||||
from simulate_pop_longterm import (
|
||||
fee_for_window, progressive_fee, policy_for, should_buy, simulate, save_results,
|
||||
)
|
||||
|
||||
|
||||
class LongtermSimulationTests(unittest.TestCase):
|
||||
def test_fee_models_and_free_entry(self):
|
||||
self.assertEqual(fee_for_window(1, 1000, "original"), 0)
|
||||
self.assertLess(fee_for_window(2, 1000, "low"), fee_for_window(2, 1000, "original"))
|
||||
self.assertGreater(progressive_fee(1000), progressive_fee(100))
|
||||
self.assertGreater(fee_for_window(3, 100, "original"), fee_for_window(2, 100, "original"))
|
||||
|
||||
def test_policy_and_budget(self):
|
||||
self.assertEqual([policy_for(x, "mixed") for x in range(4)],
|
||||
["free", "rational", "rational", "always"])
|
||||
ok, reason = should_buy("always", 2, 0, 100, "original", (0.7,)*3, 1)
|
||||
self.assertFalse(ok)
|
||||
self.assertEqual(reason, "budget")
|
||||
|
||||
def test_zero_capital_never_goes_negative(self):
|
||||
res = simulate(12, 4, 7, "balanced", "mixed", "original", 1, 10)
|
||||
self.assertTrue(all(m["balance"] >= -1e-8 for m in res["miners"]))
|
||||
self.assertLessEqual(res["summary"]["issued"], 40)
|
||||
self.assertGreaterEqual(res["summary"]["burned"], 0)
|
||||
|
||||
def test_free_scenario_no_fees(self):
|
||||
res = simulate(10, 2, 11, "original", "free", "original", 1, 16)
|
||||
self.assertEqual(res["summary"]["burned"], 0)
|
||||
self.assertTrue(all(m["paid_windows2"] == 0 for m in res["miners"]))
|
||||
|
||||
def test_reproducible_and_report_files(self):
|
||||
a = simulate(5, 3, 2026, "balanced", "mixed", "mild", 1, 20)
|
||||
b = simulate(5, 3, 2026, "balanced", "mixed", "mild", 1, 20)
|
||||
self.assertEqual(a, b)
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
files = save_results(Path(tmp), a)
|
||||
self.assertEqual(len(files), 4)
|
||||
self.assertTrue(all(p.is_file() for p in files))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user