CivicAI / civicai /simulation.py
mahammadaftab's picture
Initial Uodated
7415e01
"""
CivicAI Society Simulation Engine
Data-driven economic/social simulation.
Models economics, health, crime, satisfaction, and real-world events.
Integrates with World Bank API and LLM-parsed news.
"""
from __future__ import annotations
import math
import random
from typing import Any
from civicai.models import Action, SocietyState, SubsidyPolicy
from civicai.data_pipeline import RealWorldDataPipeline
from civicai.llm_parser import LLMParser
# Singletons for data fetching
DATA_PIPELINE = RealWorldDataPipeline()
LLM = LLMParser()
def _clamp(value: float, lo: float = 0.0, hi: float = 1.0) -> float:
return max(lo, min(hi, value))
def _noise(scale: float = 0.01) -> float:
return random.gauss(0, scale)
# ---------------------------------------------------------------------------
# Simulation Step
# ---------------------------------------------------------------------------
def simulate_step(state: SocietyState, action: Action) -> SocietyState:
"""
Advance the society simulation by one turn (quarter).
Mutates and returns the state.
"""
# Fetch Real-World Data Baseline for this turn
real_world_baseline = DATA_PIPELINE.step_forward()
news_feed = DATA_PIPELINE.get_real_world_news()
# Parse unstructured news into modifiers
news_modifiers = LLM.parse_news_to_modifiers(news_feed)
# Log events for the turn
state.active_events = news_feed
if news_feed:
state.event_history.append({
"turn": state.turn,
"events": news_feed,
"modifiers": news_modifiers
})
# Store action
state.current_tax_rate = action.tax_rate
state.current_healthcare_budget = action.healthcare_budget
state.current_education_budget = action.education_budget
state.current_police_budget = action.police_budget
state.current_subsidy = action.subsidy_policy.value
state.action_history.append(action.model_dump())
# --- Revenue & Budget ---
state.tax_revenue = state.gdp * action.tax_rate
total_spending = (
action.healthcare_budget +
action.education_budget +
action.police_budget
) * state.government_budget
subsidy_cost = 0.0
if action.subsidy_policy != SubsidyPolicy.NONE:
subsidy_cost = state.government_budget * 0.08
state.government_budget += state.tax_revenue - total_spending - subsidy_cost
state.budget_balance = (state.tax_revenue - total_spending - subsidy_cost) / max(state.gdp, 1.0)
# --- Economic Model (Anchored to Real World Data) ---
# GDP growth is influenced by the World Bank baseline + agent actions + news
tax_drag = -0.5 * max(0, action.tax_rate - 0.30)
education_boost = 0.3 * action.education_budget
subsidy_boost = 0.0
if action.subsidy_policy == SubsidyPolicy.TECHNOLOGY:
subsidy_boost = 0.015
elif action.subsidy_policy == SubsidyPolicy.INDUSTRY:
subsidy_boost = 0.012
elif action.subsidy_policy == SubsidyPolicy.AGRICULTURE:
subsidy_boost = 0.008
# Blend simulation mechanics with real-world baseline (converted from % to ratio)
baseline_gdp_growth = real_world_baseline.get("gdp_growth", 2.0) / 100.0
base_growth = baseline_gdp_growth + tax_drag + education_boost + subsidy_boost + news_modifiers.get("gdp_modifier", 0.0) + _noise(0.005)
state.gdp_growth = _clamp(base_growth, -0.15, 0.15)
state.gdp = max(10.0, state.gdp * (1 + state.gdp_growth))
# Inflation: driven by spending vs revenue + World Bank baseline
baseline_inflation = real_world_baseline.get("inflation", 2.0) / 100.0
spending_ratio = (total_spending + subsidy_cost) / max(state.tax_revenue, 1.0)
inflation_pressure = 0.02 * (spending_ratio - 1.0)
state.inflation = _clamp(
baseline_inflation + inflation_pressure + news_modifiers.get("inflation_modifier", 0.0) + _noise(0.003),
-0.05, 0.30
)
# Employment: World Bank baseline + education + news
baseline_unemployment = real_world_baseline.get("unemployment", 5.0) / 100.0
baseline_employment = 1.0 - baseline_unemployment
employment_delta = (
0.5 * state.gdp_growth +
0.1 * action.education_budget -
0.2 * max(0, state.inflation - 0.06) +
news_modifiers.get("employment_modifier", 0.0) +
_noise(0.01)
)
# Blend baseline with simulation delta
state.employment_rate = _clamp(baseline_employment + employment_delta, 0.3, 0.99)
# --- Health Model ---
baseline_le = real_world_baseline.get("life_expectancy", 70.0)
# Normalize LE roughly 0-1 (min 40, max 90)
norm_le = _clamp((baseline_le - 40.0) / 50.0, 0.0, 1.0)
healthcare_effect = 0.15 * action.healthcare_budget
infection_drag = -0.3 * state.infection_rate
state.health_index = _clamp(
(norm_le * 0.5 + state.health_index * 0.5) + healthcare_effect + infection_drag - 0.02 + _noise(0.008),
0.05, 1.0
)
# Infection dynamics
if state.infection_rate > 0:
lockdown_factor = 0.4 if state.is_lockdown else 1.0
recovery = 0.15 * action.healthcare_budget
spread = 0.08 * lockdown_factor * (1 - action.healthcare_budget)
state.infection_rate = _clamp(
state.infection_rate + spread - recovery + _noise(0.01),
0.0, 0.8
)
# Emergency response
if action.emergency_response:
er = action.emergency_response.lower()
if "lockdown" in er:
state.is_lockdown = True
state.infection_rate = _clamp(state.infection_rate - 0.05, 0.0, 1.0)
state.gdp_growth -= 0.02
state.public_satisfaction -= 0.04
elif "stimulus" in er:
state.gdp_growth += 0.015
state.government_budget -= state.gdp * 0.03
state.public_satisfaction += 0.03
elif "open" in er or "lift" in er:
state.is_lockdown = False
state.gdp_growth += 0.01
else:
state.is_lockdown = False
# --- Crime Model ---
unemployment_pressure = 0.3 * (1 - state.employment_rate)
inequality_pressure = 0.2 * state.emergent.wealth_inequality
police_effect = -0.25 * action.police_budget
satisfaction_effect = -0.1 * state.public_satisfaction
state.crime_rate = _clamp(
state.crime_rate + unemployment_pressure + inequality_pressure +
police_effect + satisfaction_effect + news_modifiers.get("crime_modifier", 0.0) + _noise(0.008),
0.01, 0.6
)
# --- Satisfaction Model (with momentum / lag) ---
economic_factor = 0.3 * state.employment_rate + 0.1 * _clamp(state.gdp_growth + 0.05, 0, 0.1) / 0.1
health_factor = 0.2 * state.health_index
crime_factor = 0.15 * (1 - state.crime_rate)
tax_burden = -0.15 * max(0, action.tax_rate - 0.25)
service_quality = 0.1 * (action.healthcare_budget + action.education_budget)
target_satisfaction = _clamp(
economic_factor + health_factor + crime_factor + tax_burden + service_quality + news_modifiers.get("satisfaction_modifier", 0.0),
0.0, 1.0
)
# Momentum: satisfaction moves slowly toward target
state.public_satisfaction = _clamp(
state.public_satisfaction + 0.3 * (target_satisfaction - state.public_satisfaction) + _noise(0.01),
0.0, 1.0
)
# --- Education ---
state.education_quality = _clamp(
state.education_quality + 0.1 * action.education_budget - 0.02 + _noise(0.005),
0.1, 1.0
)
# --- Resources ---
state.food_reserves = _clamp(state.food_reserves - 0.02 + 0.03 * (1 if action.subsidy_policy == SubsidyPolicy.AGRICULTURE else 0.3) + _noise(0.005))
state.energy_reserves = _clamp(state.energy_reserves - 0.015 + 0.02 * (1 if action.subsidy_policy == SubsidyPolicy.INDUSTRY else 0.4) + _noise(0.005))
state.medical_supplies = _clamp(state.medical_supplies - 0.02 + 0.04 * action.healthcare_budget + _noise(0.005))
state.infrastructure = _clamp(state.infrastructure - 0.005 + 0.01 * (action.police_budget + action.education_budget) + _noise(0.003))
# --- Demographics ---
effective_birth = state.birth_rate * (0.8 + 0.2 * state.health_index)
effective_death = state.death_rate * (1.2 - 0.2 * state.health_index)
state.population = max(1000, int(state.population * (1 + effective_birth - effective_death)))
# --- Emergent Metrics ---
_update_emergent(state, action)
# Advance turn
state.turn += 1
return state
# ---------------------------------------------------------------------------
# Emergent Behavior Tracking
# ---------------------------------------------------------------------------
def _update_emergent(state: SocietyState, action: Action) -> None:
"""Update emergent behavior metrics."""
em = state.emergent
# Wealth inequality (Gini proxy)
tax_equalizer = -0.02 * action.tax_rate
tech_inequality = 0.01 if action.subsidy_policy == SubsidyPolicy.TECHNOLOGY else 0.0
unemployment_inequality = 0.05 * (1 - state.employment_rate)
em.wealth_inequality = _clamp(
em.wealth_inequality + tax_equalizer + tech_inequality + unemployment_inequality + _noise(0.005),
0.1, 0.9
)
# Social unrest
dissatisfaction = max(0, 0.5 - state.public_satisfaction) * 0.3
crime_factor = state.crime_rate * 0.2
inequality_factor = em.wealth_inequality * 0.15
em.social_unrest = _clamp(
em.social_unrest * 0.9 + dissatisfaction + crime_factor + inequality_factor + _noise(0.005),
0.0, 1.0
)
# Protest probability
em.protest_probability = _clamp(
em.social_unrest * 0.6 + (1 - state.public_satisfaction) * 0.3,
0.0, 1.0
)
# Cooperation index
balanced = 1.0 - abs(action.healthcare_budget - action.education_budget) - abs(action.education_budget - action.police_budget)
em.cooperation_index = _clamp(
em.cooperation_index * 0.8 + 0.2 * _clamp(balanced + 0.5) + _noise(0.01),
0.0, 1.0
)