Complete POLYCODE-WP-0002 simulator v0.1 scaffold
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

This commit is contained in:
tegwick 2026-07-08 15:21:15 +02:00
parent 6499e38504
commit a8e4f03172
9 changed files with 268 additions and 5 deletions

View file

@ -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
View file

@ -174,3 +174,6 @@ cython_debug/
# PyPI configuration file
.pypirc
# Simulation outputs
runs/

View file

@ -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
View 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
View 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
View 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
View 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
View 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

View file

@ -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.