polycode-sim/simulator.py

132 lines
3.8 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""PolyCode Simulator v0.1 — single-run demo."""
from __future__ import annotations
import json
import random
from dataclasses import asdict, dataclass
from datetime import UTC, datetime
from pathlib import Path
from strategies import (
LenientVoter,
Proposal,
RandomInvestor,
ThresholdInvestor,
ValueVoter,
)
@dataclass
class RoundResult:
round: int
proposal_id: int
funded: float
approved: bool
effort: float
value: float
@dataclass
class RunSummary:
accepted: int
rejected: int
total_funded: float
acceptance_rate: float
rounds: list[RoundResult]
def load_params(path: Path | None = None) -> dict:
params_path = path or Path(__file__).with_name("params_default.json")
return json.loads(params_path.read_text(encoding="utf-8"))
def make_proposal(round_idx: int, rng: random.Random) -> Proposal:
effort = rng.uniform(30, 120)
value = effort * rng.uniform(0.7, 1.4)
return Proposal(id=round_idx, effort=round(effort, 2), value=round(value, 2))
def run_simulation(params: dict | None = None) -> RunSummary:
params = params or load_params()
rng = random.Random(params.get("random_seed", 42))
investors = [ThresholdInvestor(), RandomInvestor(), RandomInvestor()]
voters = [ValueVoter(), ValueVoter(), LenientVoter(), LenientVoter(), ValueVoter()]
capital = float(params["initial_capital"])
threshold = float(params["funding_threshold"])
rounds: list[RoundResult] = []
accepted = rejected = 0
total_funded = 0.0
for i in range(int(params["rounds"])):
proposal = make_proposal(i + 1, rng)
funded = 0.0
for investor in investors:
stake = min(capital, capital / len(investors))
amount = investor.allocate(proposal, funded, stake, rng)
amount = min(amount, capital)
funded += amount
capital -= amount
if funded >= threshold:
break
votes = sum(1 for voter in voters if voter.approve(proposal, funded, rng))
approved = votes > len(voters) / 2 and funded >= proposal.effort
if approved:
accepted += 1
else:
rejected += 1
capital += funded * 0.5
total_funded += funded
rounds.append(
RoundResult(
round=i + 1,
proposal_id=proposal.id,
funded=round(funded, 2),
approved=approved,
effort=proposal.effort,
value=proposal.value,
)
)
total = accepted + rejected
return RunSummary(
accepted=accepted,
rejected=rejected,
total_funded=round(total_funded, 2),
acceptance_rate=round(accepted / total, 3) if total else 0.0,
rounds=rounds,
)
def write_run(summary: RunSummary, params: dict, out_dir: Path | None = None) -> Path:
out_dir = out_dir or Path("runs")
out_dir.mkdir(exist_ok=True)
stamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")
path = out_dir / f"run-{stamp}.json"
payload = {
"params": params,
"summary": {
"accepted": summary.accepted,
"rejected": summary.rejected,
"total_funded": summary.total_funded,
"acceptance_rate": summary.acceptance_rate,
},
"rounds": [asdict(r) for r in summary.rounds],
}
path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
return path
def main() -> None:
params = load_params()
summary = run_simulation(params)
out = write_run(summary, params)
print(f"PolyCode demo run complete: {summary.accepted} accepted, {summary.rejected} rejected")
print(f"acceptance_rate={summary.acceptance_rate} total_funded={summary.total_funded}")
print(f"output: {out}")
if __name__ == "__main__":
main()