Spaces:
Sleeping
Sleeping
| """ | |
| 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 | |
| ) | |