Complete POLYCODE-WP-0002 simulator v0.1 scaffold
This commit is contained in:
parent
6499e38504
commit
a8e4f03172
9 changed files with 268 additions and 5 deletions
|
|
@ -6,7 +6,7 @@
|
|||
## Dev Commands
|
||||
|
||||
```bash
|
||||
# Planned v0.1 workflow (see README.md)
|
||||
python3 -m pytest -q tests/
|
||||
python3 simulator.py
|
||||
python3 experiments.py
|
||||
|
||||
|
|
|
|||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -174,3 +174,6 @@ cython_debug/
|
|||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
# Simulation outputs
|
||||
runs/
|
||||
|
||||
|
|
|
|||
4
SCOPE.md
4
SCOPE.md
|
|
@ -26,8 +26,8 @@ Polycode is a simulation laboratory for mechanism design: a way to test governan
|
|||
|
||||
## Current State
|
||||
|
||||
- Planning/spec repo: README and pitch materials exist; v0.1 Python simulator modules not yet committed.
|
||||
- Bootstrap State Hub integration (POLYCODE-WP-0001) finished; next: simulator scaffold (POLYCODE-WP-0002).
|
||||
- v0.1 simulator scaffold committed (`simulator.py`, `strategies.py`, `experiments.py`); outputs under `runs/`.
|
||||
- POLYCODE-WP-0002 finished; next implementation work should extend strategies and KPI reporting.
|
||||
|
||||
## Getting Oriented
|
||||
|
||||
|
|
|
|||
47
experiments.py
Normal file
47
experiments.py
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Tiny parameter sweep for PolyCode Simulator v0.1."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from copy import deepcopy
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
|
||||
from simulator import load_params, run_simulation
|
||||
|
||||
|
||||
def sweep(thresholds: list[float] | None = None) -> list[dict]:
|
||||
thresholds = thresholds or [300.0, 400.0, 500.0]
|
||||
rows: list[dict] = []
|
||||
base = load_params()
|
||||
for threshold in thresholds:
|
||||
params = deepcopy(base)
|
||||
params["funding_threshold"] = threshold
|
||||
summary = run_simulation(params)
|
||||
rows.append(
|
||||
{
|
||||
"funding_threshold": threshold,
|
||||
"acceptance_rate": summary.acceptance_rate,
|
||||
"accepted": summary.accepted,
|
||||
"rejected": summary.rejected,
|
||||
"total_funded": summary.total_funded,
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def main() -> None:
|
||||
rows = sweep()
|
||||
out_dir = Path("runs")
|
||||
out_dir.mkdir(exist_ok=True)
|
||||
stamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")
|
||||
path = out_dir / f"sweep-{stamp}.json"
|
||||
path.write_text(json.dumps({"sweep": rows}, indent=2), encoding="utf-8")
|
||||
print("Sweep results:")
|
||||
for row in rows:
|
||||
print(row)
|
||||
print(f"output: {path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
9
params_default.json
Normal file
9
params_default.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"rounds": 10,
|
||||
"initial_capital": 1000.0,
|
||||
"funding_threshold": 400.0,
|
||||
"proposal_cost": 50.0,
|
||||
"num_investors": 3,
|
||||
"num_voters": 5,
|
||||
"random_seed": 42
|
||||
}
|
||||
132
simulator.py
Normal file
132
simulator.py
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
#!/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()
|
||||
54
strategies.py
Normal file
54
strategies.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
"""Pluggable investor and voter strategies for PolyCode v0.1."""
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from typing import Protocol
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Proposal:
|
||||
id: int
|
||||
effort: float
|
||||
value: float
|
||||
|
||||
|
||||
class InvestorStrategy(Protocol):
|
||||
def allocate(
|
||||
self, proposal: Proposal, funded: float, available: float, rng: random.Random
|
||||
) -> float: ...
|
||||
|
||||
|
||||
class VoterStrategy(Protocol):
|
||||
def approve(self, proposal: Proposal, funded: float, rng: random.Random) -> bool: ...
|
||||
|
||||
|
||||
class ThresholdInvestor:
|
||||
"""Invest when projected value covers effort."""
|
||||
|
||||
def allocate(
|
||||
self, proposal: Proposal, funded: float, available: float, rng: random.Random
|
||||
) -> float:
|
||||
if proposal.value < proposal.effort or available <= 0:
|
||||
return 0.0
|
||||
need = max(0.0, proposal.effort - funded)
|
||||
return min(available, need)
|
||||
|
||||
|
||||
class RandomInvestor:
|
||||
def allocate(
|
||||
self, proposal: Proposal, funded: float, available: float, rng: random.Random
|
||||
) -> float:
|
||||
if available <= 0:
|
||||
return 0.0
|
||||
return rng.uniform(0, min(available, max(0.0, proposal.effort - funded)))
|
||||
|
||||
|
||||
class ValueVoter:
|
||||
def approve(self, proposal: Proposal, funded: float, rng: random.Random) -> bool:
|
||||
return funded >= proposal.effort and proposal.value >= proposal.effort
|
||||
|
||||
|
||||
class LenientVoter:
|
||||
def approve(self, proposal: Proposal, funded: float, rng: random.Random) -> bool:
|
||||
return funded >= proposal.effort * 0.8
|
||||
14
tests/test_smoke.py
Normal file
14
tests/test_smoke.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
from pathlib import Path
|
||||
|
||||
from simulator import load_params, run_simulation, write_run
|
||||
|
||||
|
||||
def test_demo_run_produces_summary(tmp_path: Path) -> None:
|
||||
params = load_params()
|
||||
params["rounds"] = 3
|
||||
summary = run_simulation(params)
|
||||
assert summary.accepted + summary.rejected == 3
|
||||
assert 0.0 <= summary.acceptance_rate <= 1.0
|
||||
out = write_run(summary, params, tmp_path)
|
||||
assert out.exists()
|
||||
assert out.stat().st_size > 0
|
||||
|
|
@ -4,11 +4,12 @@ type: workplan
|
|||
title: "PolyCode simulator v0.1 scaffold"
|
||||
domain: infotech
|
||||
repo: polycode-sim
|
||||
status: ready
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: custodian
|
||||
created: "2026-07-08"
|
||||
updated: "2026-07-08"
|
||||
state_hub_workstream_id: "f0281f0c-327c-430e-ab1a-c3fd031511e3"
|
||||
---
|
||||
|
||||
# PolyCode simulator v0.1 scaffold
|
||||
|
|
@ -19,8 +20,11 @@ Implement the documented v0.1 simulation engine (`simulator.py`, `strategies.py`
|
|||
|
||||
```task
|
||||
id: POLYCODE-WP-0002-T01
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "e8411084-8d8e-4c08-a07c-1b1865be2baa"
|
||||
```
|
||||
|
||||
Result 2026-07-08: Added simulator.py, strategies.py, experiments.py, params_default.json; pytest smoke green; demo run writes runs/.
|
||||
|
||||
Add minimal runnable simulator with default strategies, params file, `./runs/` output, and a smoke test for one demo run.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue