132 lines
3.8 KiB
Python
132 lines
3.8 KiB
Python
|
|
#!/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()
|