TEST / validate.py
Tyr-123's picture
Final bug fixes, dashboard io patch, and OpenEnv hackathon preparation
36cc719
Raw History Blame Contribute Delete
19.7 kB
"""
validate.py — Pre-submission validation for SocialContract-v0.
Checks all competition requirements including 8 policy levers,
academic calibration, inter-group dynamics, and deployment readiness.
"""
import sys, os, traceback
if __name__ == "__main__" and hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
sys.path.insert(0, os.path.dirname(__file__))
PASS = "\033[32m✓ PASS\033[0m"
FAIL = "\033[31m✗ FAIL\033[0m"
results = []
def check(name, fn):
try:
ok, msg = fn()
results.append((ok, name, msg))
print(f" {PASS if ok else FAIL} {name}")
if msg:
for line in msg.splitlines(): print(f" {line}")
except Exception as e:
results.append((False, name, str(e)))
print(f" {FAIL} {name}")
print(f" Exception: {e}")
traceback.print_exc()
print("\n═══════════════════════════════════════════════════")
print(" SocialContract-v0 Pre-Submission Validator")
print("═══════════════════════════════════════════════════\n")
# ── 1. openenv.yaml ───────────────────────────────────────────────────────────
print("── 1. openenv.yaml ──────────────────────────────────")
def check_yaml_exists():
return os.path.isfile("openenv.yaml"), ""
check("openenv.yaml present", check_yaml_exists)
def check_yaml_fields():
import yaml
with open("openenv.yaml") as f: data = yaml.safe_load(f)
required = ["name", "version", "description", "observation", "action", "reward", "tasks"]
missing = [k for k in required if k not in data]
if missing: return False, f"Missing fields: {missing}"
task_ids = [t["id"] for t in data["tasks"]]
return len(task_ids) >= 3, f"Found {len(task_ids)} tasks: {task_ids}"
check("openenv.yaml required fields + 3 tasks", check_yaml_fields)
def check_yaml_action_fields():
import yaml
with open("openenv.yaml") as f: data = yaml.safe_load(f)
action_fields = [f["name"] for f in data.get("action", {}).get("fields", [])]
required = ["tax_delta", "ubi_delta", "public_good_delta", "interest_rate_delta",
"stimulus_package", "import_tariff_delta", "money_supply_delta", "minimum_wage_delta"]
missing = [f for f in required if f not in action_fields]
if missing: return False, f"Missing action fields: {missing}"
return True, f"All 8 action fields documented: {len(action_fields)} total"
check("openenv.yaml has all 8 action fields", check_yaml_action_fields)
def check_yaml_reward_range():
import yaml
with open("openenv.yaml") as f: data = yaml.safe_load(f)
rng = data.get("reward", {}).get("range")
if rng != [0.0, 1.0]: return False, f"reward.range = {rng}"
return True, f"reward.range = {rng} ✓"
check("openenv.yaml reward range [0.0, 1.0]", check_yaml_reward_range)
# ── 2. Pydantic models ────────────────────────────────────────────────────────
print("\n── 2. Typed Pydantic models ─────────────────────────")
def check_pydantic_imports():
from env.pydantic_models import EconomicObservation, PolicyAction, StepReward
from pydantic import BaseModel
assert issubclass(EconomicObservation, BaseModel)
assert issubclass(PolicyAction, BaseModel)
try:
from openenv.core.env_server.types import Action, Observation
assert issubclass(EconomicObservation, Observation)
assert issubclass(PolicyAction, Action)
return True, "All models inherit openenv-core SDK types (Action/Observation) ✓"
except ImportError:
return True, "Models inherit BaseModel (SDK not installed — fallback mode)"
check("Pydantic models importable and typed", check_pydantic_imports)
def check_pydantic_8_levers():
from env.pydantic_models import PolicyAction
fields = set(PolicyAction.model_fields.keys())
required = {"tax_delta", "ubi_delta", "public_good_delta", "interest_rate_delta",
"stimulus_package", "import_tariff_delta", "money_supply_delta", "minimum_wage_delta"}
missing = required - fields
if missing: return False, f"Missing: {missing}"
return True, f"PolicyAction has {len(fields)} fields (8 levers + reasoning + speech)"
check("PolicyAction has all 8 levers", check_pydantic_8_levers)
def check_obs_new_fields():
from env.pydantic_models import EconomicObservation
fields = set(EconomicObservation.model_fields.keys())
required = {"money_supply", "minimum_wage", "capital_flight_rate", "strike_active", "private_investment"}
missing = required - fields
if missing: return False, f"Missing: {missing}"
return True, f"EconomicObservation has inter-group fields: {sorted(required)}"
check("Observation has inter-group dynamics fields", check_obs_new_fields)
# ── 3. Environment interface ──────────────────────────────────────────────────
print("\n── 3. Environment interface ─────────────────────────")
def check_reset():
from env.openenv_wrapper import SocialContractOpenEnv
env = SocialContractOpenEnv("task1_stability")
obs = env.reset()
assert obs.gdp > 0 and hasattr(obs, "minimum_wage")
return True, f"reset() OK — gdp={obs.gdp:.1f}, min_wage={obs.minimum_wage}"
check("reset() returns EconomicObservation", check_reset)
def check_step_8_levers():
from env.openenv_wrapper import SocialContractOpenEnv
from env.pydantic_models import PolicyAction
env = SocialContractOpenEnv("task1_stability")
env.reset()
action = PolicyAction(
tax_delta=0.01, ubi_delta=1.0, public_good_delta=0.01,
interest_rate_delta=0.005, stimulus_package=50.0,
import_tariff_delta=0.01, money_supply_delta=50.0,
minimum_wage_delta=0.5, reasoning="test"
)
obs = env.step(action)
assert obs.private_investment >= 0 and obs.capital_flight_rate >= 0
return True, f"step() OK — reward={obs.reward:.4f}, investment={obs.private_investment:.2f}"
check("step() with all 8 levers", check_step_8_levers)
def check_state():
from env.openenv_wrapper import SocialContractOpenEnv
env = SocialContractOpenEnv("task1_stability")
env.reset()
s = env._full_state()
required = {"task_id", "money_supply", "minimum_wage", "class_flight", "class_investment"}
missing = required - set(s.keys())
if missing: return False, f"Missing: {missing}"
return True, f"_full_state() has {len(s)} keys"
check("state() returns required keys", check_state)
# ── 3b. Academic features ────────────────────────────────────────────────────
print("\n── 3b. Academic calibration & inter-group dynamics ──")
def check_phillips_curve():
from env.citizens import phillips_curve_inflation, NAIRU
pi_high_unemp = phillips_curve_inflation(0.10, 0.05)
pi_low_unemp = phillips_curve_inflation(0.02, 0.05)
return True, f"Phillips Curve: high_unemp→π={pi_high_unemp:.4f}, low_unemp→π={pi_low_unemp:.4f}, NAIRU={NAIRU}"
check("Phillips Curve (augmented, NAIRU=4.5%)", check_phillips_curve)
def check_okuns_law():
from env.citizens import okuns_law_gdp_effect, NAIRU
mult_high = okuns_law_gdp_effect(0.10)
mult_low = okuns_law_gdp_effect(0.03)
return True, f"Okun's Law: unemp=0.10→GDP×{mult_high:.3f}, unemp=0.03→GDP×{mult_low:.3f}"
check("Okun's Law (β=2.0)", check_okuns_law)
def check_capital_flight():
from env.openenv_wrapper import SocialContractOpenEnv
from env.pydantic_models import PolicyAction
env = SocialContractOpenEnv("task3_crisis")
env.reset()
for _ in range(5):
env.step(PolicyAction(tax_delta=0.08, ubi_delta=0.0, public_good_delta=0.0))
if env._done: break
ultra = next(g for g in env.citizen_groups if g.name == "ultra_rich")
return True, f"Capital flight: ultra_rich flight_rate={ultra.capital_flight:.3f}"
check("Capital flight (Collier et al. 2001)", check_capital_flight)
def check_investment_linkage():
from env.openenv_wrapper import SocialContractOpenEnv
from env.pydantic_models import PolicyAction
env = SocialContractOpenEnv("task1_stability")
env.reset()
env.step(PolicyAction(tax_delta=0.0, ubi_delta=0.0, public_good_delta=0.02))
wealthy = next(g for g in env.citizen_groups if g.name == "wealthy")
return True, f"Wealthy investment output: {wealthy.investment_output:.2f}"
check("Inter-group investment (Stackelberg)", check_investment_linkage)
def check_strike_mechanism():
from env.citizens import CitizenGroup
import numpy as np
grp = CitizenGroup("poor", np.random.default_rng(42))
assert hasattr(grp, 'on_strike') and hasattr(grp, 'strike_cooldown')
return True, f"Strike fields: on_strike={grp.on_strike}, cooldown={grp.strike_cooldown}"
check("Collective action / strikes (Olson 1965)", check_strike_mechanism)
def check_qe_cantillon():
from env.openenv_wrapper import SocialContractOpenEnv
from env.pydantic_models import PolicyAction
env = SocialContractOpenEnv("task1_stability")
env.reset()
env.step(PolicyAction(tax_delta=0.0, ubi_delta=0.0, public_good_delta=0.0, money_supply_delta=200.0))
assert env.money_supply > 0
return True, f"QE→money_supply={env.money_supply:.1f}"
check("QE / Cantillon effect", check_qe_cantillon)
def check_min_wage():
from env.openenv_wrapper import SocialContractOpenEnv
from env.pydantic_models import PolicyAction
env = SocialContractOpenEnv("task1_stability")
env.reset()
w0 = env.minimum_wage
env.step(PolicyAction(tax_delta=0.0, ubi_delta=0.0, public_good_delta=0.0, minimum_wage_delta=1.5))
return True, f"Minimum wage: {w0:.1f}→{env.minimum_wage:.1f}"
check("Minimum wage (Card-Krueger)", check_min_wage)
def check_persistent_shocks():
from env.openenv_wrapper import SocialContractOpenEnv
from env.pydantic_models import PolicyAction
env = SocialContractOpenEnv("task1_stability", seed=1)
env.reset()
for _ in range(30):
if env._done: break
obs = env.step(PolicyAction(tax_delta=0.0, ubi_delta=0.0, public_good_delta=0.0))
if obs.shock_duration_remaining > 1:
return True, f"Persistent shock: {obs.shock_event} ({obs.shock_duration_remaining} steps remaining)"
return True, "No multi-step shock in 30 steps (probabilistic — OK)"
check("Persistent shock mechanics", check_persistent_shocks)
def check_consumer_confidence():
from env.openenv_wrapper import SocialContractOpenEnv
from env.pydantic_models import PolicyAction
env = SocialContractOpenEnv("task1_stability")
env.reset()
c0 = env.consumer_confidence
for _ in range(5):
env.step(PolicyAction(tax_delta=0.0, ubi_delta=0.0, public_good_delta=0.02))
if env._done: break
return True, f"Confidence: {c0:.3f}→{env.consumer_confidence:.3f}"
check("Consumer confidence index", check_consumer_confidence)
# ── 4. Tasks and graders ──────────────────────────────────────────────────────
print("\n── 4. Tasks & graders ──────────────────────────────")
for tid in ["task1_stability", "task2_recession", "task3_crisis", "task4_stagflation", "task5_pandemic"]:
def _check(t=tid):
from env.openenv_wrapper import SocialContractOpenEnv
from env.pydantic_models import PolicyAction
from graders.graders import run_grader
env = SocialContractOpenEnv(t)
env.reset()
for _ in range(5):
if env._done: break
env.step(PolicyAction(tax_delta=0.01, ubi_delta=0.5, public_good_delta=0.01))
r = run_grader(t, env._history)
return 0.0 <= r["score"] <= 1.0, f"score={r['score']:.4f}, verdict='{r['verdict']}'"
check(f"Grader {tid} in [0,1]", _check)
def check_phase_grading():
from graders.graders import _detect_phase_strategy
h = [{"action": {"tax_delta": -0.02 if i < 10 else 0.05, "ubi_delta": 3.0 if i < 10 else -2.0,
"public_good_delta": 0.05 if i < 10 else -0.02}} for i in range(20)]
r = _detect_phase_strategy(h, n_phases=2)
return True, f"Phase detection: score={r['phase_score']:.4f}"
check("Phase-aware grading", check_phase_grading)
# ── 5. Reward ──────────────────────────────────────────────────────────────────
print("\n── 5. Reward signal ────────────────────────────────")
def check_reward_range():
from env.openenv_wrapper import SocialContractOpenEnv
from env.pydantic_models import PolicyAction
errors = []
for tid in ["task1_stability", "task2_recession", "task3_crisis", "task4_stagflation", "task5_pandemic"]:
env = SocialContractOpenEnv(tid); env.reset()
for _ in range(8):
if env._done: break
obs = env.step(PolicyAction(tax_delta=0.02, ubi_delta=2.0, public_good_delta=0.02))
rbd = obs.metadata.get("reward_breakdown", {})
if not (0.0 <= (obs.reward or 0) <= 1.0): errors.append(f"{tid}: total={obs.reward}")
if not (0.0 <= rbd.get("task_progress", 0) <= 1.0): errors.append(f"{tid}: progress={rbd.get('task_progress')}")
if errors: return False, "\n".join(errors)
return True, "All step rewards and task_progress in [0.0, 1.0]"
check("Reward in [0,1] every step", check_reward_range)
# ── 6. inference.py ───────────────────────────────────────────────────────────
print("\n── 6. inference.py ──────────────────────────────────")
def check_inference_exists():
return os.path.isfile("inference.py"), ""
check("inference.py present", check_inference_exists)
def check_inference_env_vars():
with open("inference.py") as f: src = f.read()
missing = [v for v in ["API_BASE_URL", "MODEL_NAME", "OPENAI_API_KEY", "HF_TOKEN"] if v not in src]
if missing: return False, f"Missing: {missing}"
return True, "All 4 env vars referenced"
check("inference.py env vars", check_inference_env_vars)
def check_inference_8_levers():
with open("inference.py") as f: src = f.read()
for field in ["money_supply_delta", "minimum_wage_delta"]:
if field not in src: return False, f"Missing {field} in inference.py"
return True, "money_supply_delta + minimum_wage_delta in inference.py"
check("inference.py handles 8 levers", check_inference_8_levers)
def check_log_format():
with open("inference.py") as f: src = f.read()
for e in ['[START]', '[STEP]', '[END]']:
if e not in src: return False, f"Missing {e}"
if "flush=True" not in src: return False, "Missing flush=True"
return True, "[START]/[STEP]/[END] + flush=True"
check("Structured log format", check_log_format)
def check_inference_retry():
with open("inference.py") as f: src = f.read()
if "attempt" not in src and "retry" not in src.lower(): return False, "No retry logic"
return True, "Retry logic present"
check("Retry logic", check_inference_retry)
# ── 7. Dockerfile ─────────────────────────────────────────────────────────────
print("\n── 7. Dockerfile & deployment ──────────────────────")
def check_dockerfile():
if not os.path.isfile("Dockerfile"): return False, "Missing"
with open("Dockerfile") as f: src = f.read()
checks = {"FROM python": "FROM python" in src, "EXPOSE 7860": "EXPOSE 7860" in src, "uvicorn": "uvicorn" in src}
failed = [k for k, v in checks.items() if not v]
if failed: return False, f"Missing: {failed}"
return True, "Dockerfile valid"
check("Dockerfile", check_dockerfile)
def check_requirements():
if not os.path.isfile("requirements.txt"): return False, "Missing"
with open("requirements.txt") as f: content = f.read()
if '\x00' in content: return False, "CORRUPTED (NULL bytes)"
missing = [p for p in ["fastapi", "uvicorn", "pydantic", "numpy", "openai"] if p not in content]
if missing: return False, f"Missing: {missing}"
return True, "All packages present, no corruption"
check("requirements.txt", check_requirements)
def check_dockerignore():
return os.path.isfile(".dockerignore"), ""
check(".dockerignore", check_dockerignore)
# ── 8. FastAPI ────────────────────────────────────────────────────────────────
print("\n── 8. FastAPI endpoints ─────────────────────────────")
def check_endpoints():
with open("app.py") as f: src = f.read()
required = ["/health", "/tasks", "/grade", "/summary", "/ws"]
missing = [r for r in required if r not in src]
if missing: return False, f"Missing: {missing}"
return True, "All endpoints present (SDK provides /reset /step /state /schema /ws /health)"
check("All OpenEnv endpoints", check_endpoints)
def check_app_8_levers():
with open("app.py") as f: src = f.read()
for f_name in ["money_supply_delta", "minimum_wage_delta"]:
if f_name not in src: return False, f"Missing {f_name}"
return True, "app.py references 8 levers"
check("app.py supports 8 levers", check_app_8_levers)
def check_session_cleanup():
with open("app.py") as f: src = f.read()
if "MAX_SESSIONS" not in src and "max_concurrent_envs" not in src:
return False, "No session/concurrency management"
return True, "SDK concurrent session management (max_concurrent_envs=100)"
check("Session cleanup", check_session_cleanup)
# ── Summary ───────────────────────────────────────────────────────────────────
passed = sum(1 for ok, _, _ in results if ok)
total = len(results)
failed_checks = [(n, m) for ok, n, m in results if not ok]
print(f"\n═══════════════════════════════════════════════════")
print(f" Results: {passed}/{total} passed")
if failed_checks:
print(f"\n FAILED:")
for n, m in failed_checks:
print(f" ✗ {n}")
if m: print(f" {m}")
print("\n ⚠ Fix all FAIL items before submitting.")
sys.exit(1)
else:
print(f"\n \033[32m🎉 All checks passed — ready to submit!\033[0m")
sys.exit(0)