Initial commit: Markov Economics Simulation App

This commit is contained in:
2025-08-24 19:12:50 +02:00
commit 26c82959a2
28 changed files with 3646 additions and 0 deletions

25
app/models/__init__.py Normal file
View File

@@ -0,0 +1,25 @@
"""
Economic simulation models package.
This package contains the core Markov chain implementations and simulation engine
for demonstrating how capitalism "eats the world" when r > g.
"""
from .markov_chain import MarkovChain, CapitalistChain, ConsumerChain, EconomicAgent
from .economic_model import (
SimulationParameters,
SimulationSnapshot,
EconomicSimulation,
SimulationManager
)
__all__ = [
'MarkovChain',
'CapitalistChain',
'ConsumerChain',
'EconomicAgent',
'SimulationParameters',
'SimulationSnapshot',
'EconomicSimulation',
'SimulationManager'
]

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,482 @@
"""
Economic Simulation Engine
This module implements the main simulation engine that orchestrates multiple
economic agents running Markov chains to demonstrate wealth inequality dynamics
when capital return rate (r) exceeds economic growth rate (g).
"""
import numpy as np
import uuid
from typing import List, Dict, Tuple, Optional
from dataclasses import dataclass
import time
import json
from .markov_chain import EconomicAgent
@dataclass
class SimulationParameters:
"""Configuration parameters for economic simulation."""
r_rate: float # Capital return rate
g_rate: float # Economic growth rate
initial_capital: float = 1000.0
initial_consumption: float = 1000.0
num_agents: int = 100
iterations: int = 1000
def __post_init__(self):
"""Validate parameters after initialization."""
if self.r_rate < 0 or self.r_rate > 1:
raise ValueError("Capital rate must be between 0 and 1")
if self.g_rate < 0 or self.g_rate > 1:
raise ValueError("Growth rate must be between 0 and 1")
if self.num_agents < 1 or self.num_agents > 10000:
raise ValueError("Number of agents must be between 1 and 10000")
if self.iterations < 1 or self.iterations > 100000:
raise ValueError("Iterations must be between 1 and 100000")
@dataclass
class SimulationSnapshot:
"""Snapshot of simulation state at a specific iteration."""
iteration: int
timestamp: float
wealth_distribution: List[float]
consumption_distribution: List[float]
total_wealth: float
gini_coefficient: float
wealth_concentration_top10: float
capital_share: float
average_wealth: float
median_wealth: float
class EconomicSimulation:
"""
Main simulation engine for demonstrating Piketty's inequality principle.
Manages multiple economic agents and tracks wealth distribution over time
to show how r > g leads to increasing inequality.
"""
def __init__(self, parameters: SimulationParameters):
"""
Initialize the economic simulation.
Args:
parameters: Configuration parameters for the simulation
"""
self.parameters = parameters
self.simulation_id = str(uuid.uuid4())
self.agents: List[EconomicAgent] = []
self.snapshots: List[SimulationSnapshot] = []
self.current_iteration = 0
self.is_running = False
self.start_time = None
self._initialize_agents()
def _initialize_agents(self):
"""Create economic agents with specified parameters."""
self.agents = []
for i in range(self.parameters.num_agents):
agent_id = f"agent_{i:04d}"
# Add some randomness to initial conditions
initial_capital = self.parameters.initial_capital * (0.8 + 0.4 * np.random.random())
initial_consumption = self.parameters.initial_consumption * (0.8 + 0.4 * np.random.random())
agent = EconomicAgent(
agent_id=agent_id,
capital_rate=self.parameters.r_rate,
growth_rate=self.parameters.g_rate,
initial_capital=initial_capital,
initial_consumption=initial_consumption
)
self.agents.append(agent)
def step(self) -> SimulationSnapshot:
"""
Perform one simulation step for all agents.
Returns:
Snapshot of the current simulation state
"""
# Step all agents
for agent in self.agents:
agent.step()
# Create snapshot
snapshot = self._create_snapshot()
self.snapshots.append(snapshot)
self.current_iteration += 1
return snapshot
def run_simulation(self, iterations: Optional[int] = None) -> List[SimulationSnapshot]:
"""
Run the complete simulation for specified iterations.
Args:
iterations: Number of iterations to run (uses parameter default if None)
Returns:
List of snapshots for each iteration
"""
if iterations is None:
iterations = self.parameters.iterations
self.is_running = True
self.start_time = time.time()
try:
for _ in range(iterations):
if not self.is_running:
break
self.step()
finally:
self.is_running = False
return self.snapshots.copy()
def _create_snapshot(self) -> SimulationSnapshot:
"""Create a snapshot of current simulation state."""
# Collect wealth data
wealth_data = []
consumption_data = []
total_wealth_data = []
for agent in self.agents:
if agent.wealth_history and agent.consumption_history:
wealth = agent.wealth_history[-1]
consumption = agent.consumption_history[-1]
else:
wealth = agent.initial_capital
consumption = agent.initial_consumption
wealth_data.append(wealth)
consumption_data.append(consumption)
total_wealth_data.append(wealth + consumption)
# Calculate metrics
total_wealth = sum(total_wealth_data)
gini = self._calculate_gini_coefficient(total_wealth_data)
wealth_conc = self._calculate_wealth_concentration(total_wealth_data, 0.1)
capital_share = sum(wealth_data) / total_wealth if total_wealth > 0 else 0
return SimulationSnapshot(
iteration=self.current_iteration,
timestamp=time.time(),
wealth_distribution=wealth_data,
consumption_distribution=consumption_data,
total_wealth=total_wealth,
gini_coefficient=gini,
wealth_concentration_top10=wealth_conc,
capital_share=capital_share,
average_wealth=np.mean(total_wealth_data),
median_wealth=np.median(total_wealth_data)
)
def _calculate_gini_coefficient(self, wealth_data: List[float]) -> float:
"""
Calculate the Gini coefficient for wealth inequality.
Args:
wealth_data: List of wealth values
Returns:
Gini coefficient between 0 (perfect equality) and 1 (perfect inequality)
"""
if not wealth_data or len(wealth_data) < 2:
return 0.0
# Sort wealth data
sorted_wealth = sorted(wealth_data)
n = len(sorted_wealth)
# Calculate Gini coefficient
cumsum = np.cumsum(sorted_wealth)
total_wealth = cumsum[-1]
if total_wealth == 0:
return 0.0
# Gini coefficient formula
gini = (2 * sum((i + 1) * wealth for i, wealth in enumerate(sorted_wealth))) / (n * total_wealth) - (n + 1) / n
return max(0.0, min(1.0, gini))
def _calculate_wealth_concentration(self, wealth_data: List[float], top_percentage: float) -> float:
"""
Calculate the share of total wealth held by the top percentage of agents.
Args:
wealth_data: List of wealth values
top_percentage: Percentage of top agents (e.g., 0.1 for top 10%)
Returns:
Share of total wealth held by top agents
"""
if not wealth_data:
return 0.0
sorted_wealth = sorted(wealth_data, reverse=True)
total_wealth = sum(sorted_wealth)
if total_wealth == 0:
return 0.0
top_count = max(1, int(len(sorted_wealth) * top_percentage))
top_wealth = sum(sorted_wealth[:top_count])
return top_wealth / total_wealth
def get_latest_snapshot(self) -> Optional[SimulationSnapshot]:
"""Get the most recent simulation snapshot."""
return self.snapshots[-1] if self.snapshots else None
def get_snapshot_at_iteration(self, iteration: int) -> Optional[SimulationSnapshot]:
"""Get snapshot at specific iteration."""
for snapshot in self.snapshots:
if snapshot.iteration == iteration:
return snapshot
return None
def get_wealth_evolution(self) -> Tuple[List[int], List[float], List[float]]:
"""
Get wealth evolution data for plotting.
Returns:
Tuple of (iterations, total_wealth_over_time, gini_over_time)
"""
if not self.snapshots:
return [], [], []
iterations = [s.iteration for s in self.snapshots]
total_wealth = [s.total_wealth for s in self.snapshots]
gini_coefficients = [s.gini_coefficient for s in self.snapshots]
return iterations, total_wealth, gini_coefficients
def get_agent_wealth_distribution(self) -> Dict[str, List[float]]:
"""
Get current wealth distribution across all agents.
Returns:
Dictionary with agent IDs and their current wealth values
"""
distribution = {}
for agent in self.agents:
if agent.wealth_history:
distribution[agent.agent_id] = agent.wealth_history[-1]
else:
distribution[agent.agent_id] = agent.initial_capital
return distribution
def update_parameters(self, new_parameters: SimulationParameters):
"""
Update simulation parameters and restart with new settings.
Args:
new_parameters: New simulation configuration
"""
self.parameters = new_parameters
self.current_iteration = 0
self.snapshots.clear()
self._initialize_agents()
def stop_simulation(self):
"""Stop the running simulation."""
self.is_running = False
def reset_simulation(self):
"""Reset simulation to initial state."""
self.current_iteration = 0
self.snapshots.clear()
self.is_running = False
self._initialize_agents()
def export_data(self, format_type: str = 'json') -> str:
"""
Export simulation data in specified format.
Args:
format_type: Export format ('json' or 'csv')
Returns:
Formatted data string
"""
if format_type.lower() == 'json':
return self._export_json()
elif format_type.lower() == 'csv':
return self._export_csv()
else:
raise ValueError("Format must be 'json' or 'csv'")
def _export_json(self) -> str:
"""Export simulation data as JSON."""
data = {
'simulation_id': self.simulation_id,
'parameters': {
'r_rate': self.parameters.r_rate,
'g_rate': self.parameters.g_rate,
'initial_capital': self.parameters.initial_capital,
'initial_consumption': self.parameters.initial_consumption,
'num_agents': self.parameters.num_agents,
'iterations': self.parameters.iterations
},
'snapshots': []
}
for snapshot in self.snapshots:
snapshot_data = {
'iteration': snapshot.iteration,
'timestamp': snapshot.timestamp,
'total_wealth': snapshot.total_wealth,
'gini_coefficient': snapshot.gini_coefficient,
'wealth_concentration_top10': snapshot.wealth_concentration_top10,
'capital_share': snapshot.capital_share,
'average_wealth': snapshot.average_wealth,
'median_wealth': snapshot.median_wealth
}
data['snapshots'].append(snapshot_data)
return json.dumps(data, indent=2)
def _export_csv(self) -> str:
"""Export simulation data as CSV."""
if not self.snapshots:
return "iteration,total_wealth,gini_coefficient,wealth_concentration_top10,capital_share,average_wealth,median_wealth\n"
lines = ["iteration,total_wealth,gini_coefficient,wealth_concentration_top10,capital_share,average_wealth,median_wealth"]
for snapshot in self.snapshots:
line = f"{snapshot.iteration},{snapshot.total_wealth},{snapshot.gini_coefficient}," \
f"{snapshot.wealth_concentration_top10},{snapshot.capital_share}," \
f"{snapshot.average_wealth},{snapshot.median_wealth}"
lines.append(line)
return "\n".join(lines)
class SimulationManager:
"""
Manages multiple simulation instances for concurrent access.
"""
def __init__(self):
"""Initialize the simulation manager."""
self.simulations: Dict[str, EconomicSimulation] = {}
self.active_simulations: Dict[str, EconomicSimulation] = {}
def create_simulation(self, parameters: SimulationParameters) -> str:
"""
Create a new simulation instance.
Args:
parameters: Simulation configuration
Returns:
Unique simulation ID
"""
simulation = EconomicSimulation(parameters)
simulation_id = simulation.simulation_id
self.simulations[simulation_id] = simulation
return simulation_id
def get_simulation(self, simulation_id: str) -> Optional[EconomicSimulation]:
"""
Get simulation instance by ID.
Args:
simulation_id: Unique simulation identifier
Returns:
Simulation instance or None if not found
"""
return self.simulations.get(simulation_id)
def start_simulation(self, simulation_id: str, iterations: Optional[int] = None) -> bool:
"""
Start running a simulation.
Args:
simulation_id: Unique simulation identifier
iterations: Number of iterations to run
Returns:
True if simulation started successfully
"""
simulation = self.get_simulation(simulation_id)
if not simulation:
return False
self.active_simulations[simulation_id] = simulation
# In a real application, this would run in a separate thread
simulation.run_simulation(iterations)
return True
def stop_simulation(self, simulation_id: str) -> bool:
"""
Stop a running simulation.
Args:
simulation_id: Unique simulation identifier
Returns:
True if simulation stopped successfully
"""
simulation = self.get_simulation(simulation_id)
if simulation:
simulation.stop_simulation()
self.active_simulations.pop(simulation_id, None)
return True
return False
def delete_simulation(self, simulation_id: str) -> bool:
"""
Delete a simulation instance.
Args:
simulation_id: Unique simulation identifier
Returns:
True if simulation deleted successfully
"""
if simulation_id in self.simulations:
self.stop_simulation(simulation_id)
del self.simulations[simulation_id]
return True
return False
def list_simulations(self) -> List[Dict]:
"""
List all simulation instances with their status.
Returns:
List of simulation information dictionaries
"""
simulations_info = []
for sim_id, simulation in self.simulations.items():
info = {
'simulation_id': sim_id,
'is_running': simulation.is_running,
'current_iteration': simulation.current_iteration,
'total_iterations': simulation.parameters.iterations,
'r_rate': simulation.parameters.r_rate,
'g_rate': simulation.parameters.g_rate,
'num_agents': simulation.parameters.num_agents
}
simulations_info.append(info)
return simulations_info

339
app/models/markov_chain.py Normal file
View File

@@ -0,0 +1,339 @@
"""
Markov Chain Models for Economic Simulation
This module implements the core Markov chain models representing:
1. Capitalist Chain (M-C-M'): Money → Commodities → More Money
2. Consumer Chain (C-M-C): Commodities → Money → Commodities
Based on Marx's economic theory and Piketty's inequality principle (r > g).
"""
import numpy as np
from typing import List, Dict, Tuple
import random
class MarkovChain:
"""
Base class for Markov chain implementations.
Provides the foundation for economic state transitions with configurable
transition probabilities and state tracking.
"""
def __init__(self, states: List[str], transition_probability: float, initial_state: str):
"""
Initialize the Markov chain.
Args:
states: List of possible states
transition_probability: Base probability for state transitions
initial_state: Starting state for the chain
"""
self.states = states
self.transition_probability = max(0.0, min(1.0, transition_probability))
self.current_state = initial_state
self.state_history = [initial_state]
self.iteration_count = 0
# Validate initial state
if initial_state not in states:
raise ValueError(f"Initial state '{initial_state}' not in states list")
def get_transition_matrix(self) -> np.ndarray:
"""
Get the transition probability matrix for this chain.
Must be implemented by subclasses.
Returns:
numpy array representing the transition matrix
"""
raise NotImplementedError("Subclasses must implement get_transition_matrix")
def step(self) -> str:
"""
Perform one step in the Markov chain.
Returns:
The new current state after the transition
"""
current_index = self.states.index(self.current_state)
transition_matrix = self.get_transition_matrix()
probabilities = transition_matrix[current_index]
# Choose next state based on probabilities
next_state_index = np.random.choice(len(self.states), p=probabilities)
self.current_state = self.states[next_state_index]
self.state_history.append(self.current_state)
self.iteration_count += 1
return self.current_state
def simulate(self, iterations: int) -> List[str]:
"""
Run the Markov chain for multiple iterations.
Args:
iterations: Number of steps to simulate
Returns:
List of states visited during simulation
"""
for _ in range(iterations):
self.step()
return self.state_history.copy()
def get_state_distribution(self) -> Dict[str, float]:
"""
Calculate the current state distribution from history.
Returns:
Dictionary mapping states to their frequency ratios
"""
if not self.state_history:
return {state: 0.0 for state in self.states}
total_count = len(self.state_history)
distribution = {}
for state in self.states:
count = self.state_history.count(state)
distribution[state] = count / total_count
return distribution
def reset(self, initial_state: str = None):
"""
Reset the chain to initial conditions.
Args:
initial_state: New initial state (optional)
"""
if initial_state and initial_state in self.states:
self.current_state = initial_state
else:
self.current_state = self.state_history[0]
self.state_history = [self.current_state]
self.iteration_count = 0
class CapitalistChain(MarkovChain):
"""
Represents the capitalist economic cycle: M-C-M' (Money → Commodities → More Money)
This chain models capital accumulation where money is invested in commodities
to generate more money, with returns based on the capital rate (r).
"""
def __init__(self, capital_rate: float):
"""
Initialize the capitalist chain.
Args:
capital_rate: The rate of return on capital (r)
"""
states = ['Money', 'Commodities', 'Enhanced_Money']
super().__init__(states, capital_rate, 'Money')
self.capital_rate = capital_rate
self.wealth_multiplier = 1.0
def get_transition_matrix(self) -> np.ndarray:
"""
Create transition matrix for M-C-M' cycle.
The matrix ensures:
- Money transitions to Commodities with probability r
- Commodities always transition to Enhanced_Money (probability 1)
- Enhanced_Money always transitions back to Money (probability 1)
Returns:
3x3 transition probability matrix
"""
r = self.transition_probability
# States: ['Money', 'Commodities', 'Enhanced_Money']
matrix = np.array([
[1-r, r, 0.0], # Money -> stay or go to Commodities
[0.0, 0.0, 1.0], # Commodities -> Enhanced_Money (always)
[1.0, 0.0, 0.0] # Enhanced_Money -> Money (always)
])
return matrix
def step(self) -> str:
"""
Perform one step and update wealth when completing a cycle.
Returns:
The new current state
"""
previous_state = self.current_state
new_state = super().step()
# If we completed a full cycle (Enhanced_Money -> Money), increase wealth
if previous_state == 'Enhanced_Money' and new_state == 'Money':
self.wealth_multiplier *= (1 + self.capital_rate)
return new_state
def get_current_wealth(self) -> float:
"""
Get the current accumulated wealth.
Returns:
Current wealth multiplier representing accumulated capital
"""
return self.wealth_multiplier
class ConsumerChain(MarkovChain):
"""
Represents the consumer economic cycle: C-M-C (Commodities → Money → Commodities)
This chain models consumption patterns where commodities are exchanged for money
to purchase other commodities, growing with the economic growth rate (g).
"""
def __init__(self, growth_rate: float):
"""
Initialize the consumer chain.
Args:
growth_rate: The economic growth rate (g)
"""
states = ['Commodities', 'Money', 'New_Commodities']
super().__init__(states, growth_rate, 'Commodities')
self.growth_rate = growth_rate
self.consumption_value = 1.0
def get_transition_matrix(self) -> np.ndarray:
"""
Create transition matrix for C-M-C cycle.
The matrix ensures:
- Commodities transition to Money with probability g
- Money always transitions to New_Commodities (probability 1)
- New_Commodities always transition back to Commodities (probability 1)
Returns:
3x3 transition probability matrix
"""
g = self.transition_probability
# States: ['Commodities', 'Money', 'New_Commodities']
matrix = np.array([
[1-g, g, 0.0], # Commodities -> stay or go to Money
[0.0, 0.0, 1.0], # Money -> New_Commodities (always)
[1.0, 0.0, 0.0] # New_Commodities -> Commodities (always)
])
return matrix
def step(self) -> str:
"""
Perform one step and update consumption value when completing a cycle.
Returns:
The new current state
"""
previous_state = self.current_state
new_state = super().step()
# If we completed a full cycle (New_Commodities -> Commodities), grow consumption
if previous_state == 'New_Commodities' and new_state == 'Commodities':
self.consumption_value *= (1 + self.growth_rate)
return new_state
def get_current_consumption(self) -> float:
"""
Get the current consumption value.
Returns:
Current consumption value representing accumulated consumption capacity
"""
return self.consumption_value
class EconomicAgent:
"""
Represents an individual economic agent with both capitalist and consumer behaviors.
Each agent operates both chains simultaneously, allowing for mixed economic activity
and wealth accumulation patterns.
"""
def __init__(self, agent_id: str, capital_rate: float, growth_rate: float,
initial_capital: float = 1000.0, initial_consumption: float = 1000.0):
"""
Initialize an economic agent.
Args:
agent_id: Unique identifier for the agent
capital_rate: Capital return rate (r)
growth_rate: Economic growth rate (g)
initial_capital: Starting capital amount
initial_consumption: Starting consumption capacity
"""
self.agent_id = agent_id
self.capitalist_chain = CapitalistChain(capital_rate)
self.consumer_chain = ConsumerChain(growth_rate)
self.initial_capital = initial_capital
self.initial_consumption = initial_consumption
# Track wealth over time
self.wealth_history = []
self.consumption_history = []
def step(self) -> Tuple[float, float]:
"""
Perform one simulation step for both chains.
Returns:
Tuple of (current_wealth, current_consumption)
"""
# Step both chains
self.capitalist_chain.step()
self.consumer_chain.step()
# Calculate current values
current_wealth = self.initial_capital * self.capitalist_chain.get_current_wealth()
current_consumption = self.initial_consumption * self.consumer_chain.get_current_consumption()
# Store history
self.wealth_history.append(current_wealth)
self.consumption_history.append(current_consumption)
return current_wealth, current_consumption
def get_total_wealth(self) -> float:
"""
Get the agent's total economic value.
Returns:
Sum of capital wealth and consumption capacity
"""
if not self.wealth_history or not self.consumption_history:
return self.initial_capital + self.initial_consumption
return self.wealth_history[-1] + self.consumption_history[-1]
def get_wealth_ratio(self) -> float:
"""
Get the ratio of capital wealth to total wealth.
Returns:
Ratio representing the capitalist vs consumer wealth proportion
"""
total = self.get_total_wealth()
if total == 0:
return 0.0
if not self.wealth_history:
return self.initial_capital / (self.initial_capital + self.initial_consumption)
return self.wealth_history[-1] / total