Initial commit: Markov Economics Simulation App
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
.venv/
|
||||
.qoder/
|
BIN
__pycache__/config.cpython-312.pyc
Normal file
BIN
__pycache__/config.cpython-312.pyc
Normal file
Binary file not shown.
39
app/__init__.py
Normal file
39
app/__init__.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""
|
||||
Flask Application Factory
|
||||
|
||||
Creates and configures the Flask application with SocketIO support for real-time
|
||||
updates of the Markov economics simulation.
|
||||
"""
|
||||
|
||||
from flask import Flask
|
||||
from flask_socketio import SocketIO
|
||||
from config import config
|
||||
|
||||
# Initialize SocketIO
|
||||
socketio = SocketIO(cors_allowed_origins="*")
|
||||
|
||||
|
||||
def create_app(config_name='development'):
|
||||
"""
|
||||
Create and configure the Flask application.
|
||||
|
||||
Args:
|
||||
config_name: Configuration profile to use
|
||||
|
||||
Returns:
|
||||
Configured Flask application instance
|
||||
"""
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(config[config_name])
|
||||
|
||||
# Initialize extensions
|
||||
socketio.init_app(app, async_mode='threading')
|
||||
|
||||
# Register blueprints
|
||||
from .routes.main import main_bp
|
||||
from .routes.api import api_bp
|
||||
|
||||
app.register_blueprint(main_bp)
|
||||
app.register_blueprint(api_bp, url_prefix='/api')
|
||||
|
||||
return app
|
BIN
app/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
app/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
25
app/models/__init__.py
Normal file
25
app/models/__init__.py
Normal 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'
|
||||
]
|
BIN
app/models/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
app/models/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/models/__pycache__/economic_model.cpython-312.pyc
Normal file
BIN
app/models/__pycache__/economic_model.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/models/__pycache__/markov_chain.cpython-312.pyc
Normal file
BIN
app/models/__pycache__/markov_chain.cpython-312.pyc
Normal file
Binary file not shown.
482
app/models/economic_model.py
Normal file
482
app/models/economic_model.py
Normal 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
339
app/models/markov_chain.py
Normal 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
|
11
app/routes/__init__.py
Normal file
11
app/routes/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""
|
||||
Flask routes package.
|
||||
|
||||
Contains the main web interface routes and API endpoints for the
|
||||
Markov economics simulation application.
|
||||
"""
|
||||
|
||||
from .main import main_bp
|
||||
from .api import api_bp
|
||||
|
||||
__all__ = ['main_bp', 'api_bp']
|
BIN
app/routes/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
app/routes/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/routes/__pycache__/api.cpython-312.pyc
Normal file
BIN
app/routes/__pycache__/api.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/routes/__pycache__/main.cpython-312.pyc
Normal file
BIN
app/routes/__pycache__/main.cpython-312.pyc
Normal file
Binary file not shown.
383
app/routes/api.py
Normal file
383
app/routes/api.py
Normal file
@@ -0,0 +1,383 @@
|
||||
"""
|
||||
API Routes for Simulation Control
|
||||
|
||||
Provides REST API endpoints for controlling simulations, retrieving data,
|
||||
and managing simulation parameters with real-time updates via SocketIO.
|
||||
"""
|
||||
|
||||
from flask import Blueprint, request, jsonify, current_app
|
||||
from flask_socketio import emit, join_room, leave_room
|
||||
import threading
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from app import socketio
|
||||
from app.models import SimulationManager, SimulationParameters
|
||||
from app.routes.main import simulation_manager
|
||||
|
||||
api_bp = Blueprint('api', __name__)
|
||||
|
||||
|
||||
@api_bp.route('/simulation', methods=['POST'])
|
||||
def create_simulation():
|
||||
"""
|
||||
Create a new simulation with specified parameters.
|
||||
|
||||
Request JSON:
|
||||
{
|
||||
"r_rate": 0.05,
|
||||
"g_rate": 0.03,
|
||||
"initial_capital": 1000,
|
||||
"initial_consumption": 1000,
|
||||
"num_agents": 100,
|
||||
"iterations": 1000
|
||||
}
|
||||
|
||||
Returns:
|
||||
JSON response with simulation ID and status
|
||||
"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
|
||||
if not data:
|
||||
return jsonify({'error': 'No JSON data provided'}), 400
|
||||
|
||||
# Validate and create parameters
|
||||
params = SimulationParameters(
|
||||
r_rate=float(data.get('r_rate', 0.05)),
|
||||
g_rate=float(data.get('g_rate', 0.03)),
|
||||
initial_capital=float(data.get('initial_capital', 1000)),
|
||||
initial_consumption=float(data.get('initial_consumption', 1000)),
|
||||
num_agents=int(data.get('num_agents', 100)),
|
||||
iterations=int(data.get('iterations', 1000))
|
||||
)
|
||||
|
||||
# Create simulation
|
||||
simulation_id = simulation_manager.create_simulation(params)
|
||||
|
||||
return jsonify({
|
||||
'simulation_id': simulation_id,
|
||||
'status': 'created',
|
||||
'parameters': {
|
||||
'r_rate': params.r_rate,
|
||||
'g_rate': params.g_rate,
|
||||
'initial_capital': params.initial_capital,
|
||||
'initial_consumption': params.initial_consumption,
|
||||
'num_agents': params.num_agents,
|
||||
'iterations': params.iterations
|
||||
}
|
||||
}), 201
|
||||
|
||||
except ValueError as e:
|
||||
return jsonify({'error': f'Invalid parameter: {str(e)}'}), 400
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error creating simulation: {str(e)}")
|
||||
return jsonify({'error': 'Internal server error'}), 500
|
||||
|
||||
|
||||
@api_bp.route('/simulation/<simulation_id>/start', methods=['POST'])
|
||||
def start_simulation(simulation_id: str):
|
||||
"""
|
||||
Start running a simulation.
|
||||
|
||||
Args:
|
||||
simulation_id: Unique simulation identifier
|
||||
|
||||
Returns:
|
||||
JSON response indicating success or failure
|
||||
"""
|
||||
try:
|
||||
simulation = simulation_manager.get_simulation(simulation_id)
|
||||
|
||||
if not simulation:
|
||||
return jsonify({'error': 'Simulation not found'}), 404
|
||||
|
||||
if simulation.is_running:
|
||||
return jsonify({'error': 'Simulation already running'}), 400
|
||||
|
||||
# Start simulation in background thread
|
||||
def run_simulation_background():
|
||||
try:
|
||||
# Run simulation with progress updates
|
||||
total_iterations = simulation.parameters.iterations
|
||||
for i in range(total_iterations):
|
||||
if not simulation.is_running:
|
||||
break
|
||||
|
||||
snapshot = simulation.step()
|
||||
|
||||
# Emit progress update every 10 iterations or at milestones
|
||||
if i % 10 == 0 or i == total_iterations - 1:
|
||||
progress_data = {
|
||||
'simulation_id': simulation_id,
|
||||
'iteration': snapshot.iteration,
|
||||
'total_iterations': total_iterations,
|
||||
'progress_percentage': (snapshot.iteration / total_iterations) * 100,
|
||||
'total_wealth': snapshot.total_wealth,
|
||||
'gini_coefficient': snapshot.gini_coefficient,
|
||||
'capital_share': snapshot.capital_share,
|
||||
'wealth_concentration_top10': snapshot.wealth_concentration_top10
|
||||
}
|
||||
|
||||
socketio.emit('simulation_progress', progress_data,
|
||||
room=f'simulation_{simulation_id}')
|
||||
|
||||
# Small delay to allow real-time visualization
|
||||
time.sleep(0.01)
|
||||
|
||||
# Emit completion
|
||||
socketio.emit('simulation_complete', {
|
||||
'simulation_id': simulation_id,
|
||||
'total_snapshots': len(simulation.snapshots)
|
||||
}, room=f'simulation_{simulation_id}')
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error in simulation thread: {str(e)}")
|
||||
socketio.emit('simulation_error', {
|
||||
'simulation_id': simulation_id,
|
||||
'error': str(e)
|
||||
}, room=f'simulation_{simulation_id}')
|
||||
|
||||
# Start background thread
|
||||
thread = threading.Thread(target=run_simulation_background)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
return jsonify({
|
||||
'simulation_id': simulation_id,
|
||||
'status': 'started',
|
||||
'message': 'Simulation started successfully'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error starting simulation: {str(e)}")
|
||||
return jsonify({'error': 'Internal server error'}), 500
|
||||
|
||||
|
||||
@api_bp.route('/simulation/<simulation_id>/stop', methods=['POST'])
|
||||
def stop_simulation(simulation_id: str):
|
||||
"""
|
||||
Stop a running simulation.
|
||||
|
||||
Args:
|
||||
simulation_id: Unique simulation identifier
|
||||
|
||||
Returns:
|
||||
JSON response indicating success or failure
|
||||
"""
|
||||
try:
|
||||
success = simulation_manager.stop_simulation(simulation_id)
|
||||
|
||||
if not success:
|
||||
return jsonify({'error': 'Simulation not found or not running'}), 404
|
||||
|
||||
# Emit stop event
|
||||
socketio.emit('simulation_stopped', {
|
||||
'simulation_id': simulation_id
|
||||
}, room=f'simulation_{simulation_id}')
|
||||
|
||||
return jsonify({
|
||||
'simulation_id': simulation_id,
|
||||
'status': 'stopped',
|
||||
'message': 'Simulation stopped successfully'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error stopping simulation: {str(e)}")
|
||||
return jsonify({'error': 'Internal server error'}), 500
|
||||
|
||||
|
||||
@api_bp.route('/simulation/<simulation_id>/data', methods=['GET'])
|
||||
def get_simulation_data(simulation_id: str):
|
||||
"""
|
||||
Get current simulation data and snapshots.
|
||||
|
||||
Args:
|
||||
simulation_id: Unique simulation identifier
|
||||
|
||||
Returns:
|
||||
JSON response with simulation data
|
||||
"""
|
||||
try:
|
||||
simulation = simulation_manager.get_simulation(simulation_id)
|
||||
|
||||
if not simulation:
|
||||
return jsonify({'error': 'Simulation not found'}), 404
|
||||
|
||||
# Get specific iteration if requested
|
||||
iteration = request.args.get('iteration', type=int)
|
||||
if iteration is not None:
|
||||
snapshot = simulation.get_snapshot_at_iteration(iteration)
|
||||
if not snapshot:
|
||||
return jsonify({'error': f'No data for iteration {iteration}'}), 404
|
||||
|
||||
return jsonify({
|
||||
'simulation_id': simulation_id,
|
||||
'snapshot': {
|
||||
'iteration': snapshot.iteration,
|
||||
'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
|
||||
}
|
||||
})
|
||||
|
||||
# Get latest snapshot and evolution data
|
||||
latest_snapshot = simulation.get_latest_snapshot()
|
||||
iterations, total_wealth, gini_coefficients = simulation.get_wealth_evolution()
|
||||
|
||||
response_data = {
|
||||
'simulation_id': simulation_id,
|
||||
'is_running': simulation.is_running,
|
||||
'current_iteration': simulation.current_iteration,
|
||||
'total_snapshots': len(simulation.snapshots),
|
||||
'parameters': {
|
||||
'r_rate': simulation.parameters.r_rate,
|
||||
'g_rate': simulation.parameters.g_rate,
|
||||
'num_agents': simulation.parameters.num_agents,
|
||||
'iterations': simulation.parameters.iterations
|
||||
}
|
||||
}
|
||||
|
||||
if latest_snapshot:
|
||||
response_data['latest_snapshot'] = {
|
||||
'iteration': latest_snapshot.iteration,
|
||||
'total_wealth': latest_snapshot.total_wealth,
|
||||
'gini_coefficient': latest_snapshot.gini_coefficient,
|
||||
'wealth_concentration_top10': latest_snapshot.wealth_concentration_top10,
|
||||
'capital_share': latest_snapshot.capital_share,
|
||||
'average_wealth': latest_snapshot.average_wealth,
|
||||
'median_wealth': latest_snapshot.median_wealth
|
||||
}
|
||||
|
||||
# Include evolution data if requested
|
||||
if request.args.get('include_evolution', '').lower() == 'true':
|
||||
response_data['evolution'] = {
|
||||
'iterations': iterations,
|
||||
'total_wealth': total_wealth,
|
||||
'gini_coefficients': gini_coefficients
|
||||
}
|
||||
|
||||
return jsonify(response_data)
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error getting simulation data: {str(e)}")
|
||||
return jsonify({'error': 'Internal server error'}), 500
|
||||
|
||||
|
||||
@api_bp.route('/simulation/<simulation_id>/export/<format_type>', methods=['GET'])
|
||||
def export_simulation_data(simulation_id: str, format_type: str):
|
||||
"""
|
||||
Export simulation data in specified format.
|
||||
|
||||
Args:
|
||||
simulation_id: Unique simulation identifier
|
||||
format_type: Export format ('json' or 'csv')
|
||||
|
||||
Returns:
|
||||
Data in requested format
|
||||
"""
|
||||
try:
|
||||
simulation = simulation_manager.get_simulation(simulation_id)
|
||||
|
||||
if not simulation:
|
||||
return jsonify({'error': 'Simulation not found'}), 404
|
||||
|
||||
if format_type.lower() not in ['json', 'csv']:
|
||||
return jsonify({'error': 'Format must be json or csv'}), 400
|
||||
|
||||
exported_data = simulation.export_data(format_type)
|
||||
|
||||
if format_type.lower() == 'json':
|
||||
return jsonify({'data': exported_data})
|
||||
else: # CSV
|
||||
return exported_data, 200, {
|
||||
'Content-Type': 'text/csv',
|
||||
'Content-Disposition': f'attachment; filename=simulation_{simulation_id[:8]}.csv'
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error exporting simulation data: {str(e)}")
|
||||
return jsonify({'error': 'Internal server error'}), 500
|
||||
|
||||
|
||||
@api_bp.route('/simulation/<simulation_id>', methods=['DELETE'])
|
||||
def delete_simulation(simulation_id: str):
|
||||
"""
|
||||
Delete a simulation.
|
||||
|
||||
Args:
|
||||
simulation_id: Unique simulation identifier
|
||||
|
||||
Returns:
|
||||
JSON response indicating success or failure
|
||||
"""
|
||||
try:
|
||||
success = simulation_manager.delete_simulation(simulation_id)
|
||||
|
||||
if not success:
|
||||
return jsonify({'error': 'Simulation not found'}), 404
|
||||
|
||||
return jsonify({
|
||||
'simulation_id': simulation_id,
|
||||
'status': 'deleted',
|
||||
'message': 'Simulation deleted successfully'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error deleting simulation: {str(e)}")
|
||||
return jsonify({'error': 'Internal server error'}), 500
|
||||
|
||||
|
||||
@api_bp.route('/simulations', methods=['GET'])
|
||||
def list_simulations():
|
||||
"""
|
||||
List all simulations with their status.
|
||||
|
||||
Returns:
|
||||
JSON response with list of simulation information
|
||||
"""
|
||||
try:
|
||||
simulations_info = simulation_manager.list_simulations()
|
||||
|
||||
return jsonify({
|
||||
'simulations': simulations_info,
|
||||
'total_count': len(simulations_info)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error listing simulations: {str(e)}")
|
||||
return jsonify({'error': 'Internal server error'}), 500
|
||||
|
||||
|
||||
# SocketIO event handlers
|
||||
@socketio.on('join_simulation')
|
||||
def on_join_simulation(data):
|
||||
"""Handle client joining simulation room for real-time updates."""
|
||||
simulation_id = data.get('simulation_id')
|
||||
if simulation_id:
|
||||
join_room(f'simulation_{simulation_id}')
|
||||
emit('joined_simulation', {'simulation_id': simulation_id})
|
||||
|
||||
|
||||
@socketio.on('leave_simulation')
|
||||
def on_leave_simulation(data):
|
||||
"""Handle client leaving simulation room."""
|
||||
simulation_id = data.get('simulation_id')
|
||||
if simulation_id:
|
||||
leave_room(f'simulation_{simulation_id}')
|
||||
emit('left_simulation', {'simulation_id': simulation_id})
|
||||
|
||||
|
||||
@socketio.on('connect')
|
||||
def on_connect():
|
||||
"""Handle client connection."""
|
||||
emit('connected', {'status': 'connected'})
|
||||
|
||||
|
||||
@socketio.on('disconnect')
|
||||
def on_disconnect():
|
||||
"""Handle client disconnection."""
|
||||
print('Client disconnected')
|
149
app/routes/main.py
Normal file
149
app/routes/main.py
Normal file
@@ -0,0 +1,149 @@
|
||||
"""
|
||||
Main Flask Routes
|
||||
|
||||
Handles the primary web interface for the Markov economics simulation,
|
||||
including the main simulation page and basic navigation.
|
||||
"""
|
||||
|
||||
from flask import Blueprint, render_template, request, jsonify
|
||||
from app.models import SimulationManager, SimulationParameters
|
||||
|
||||
main_bp = Blueprint('main', __name__)
|
||||
|
||||
# Global simulation manager instance
|
||||
simulation_manager = SimulationManager()
|
||||
|
||||
|
||||
@main_bp.route('/')
|
||||
def index():
|
||||
"""
|
||||
Main simulation interface page.
|
||||
|
||||
Returns:
|
||||
Rendered HTML template with simulation controls
|
||||
"""
|
||||
# Default parameters for display
|
||||
default_params = {
|
||||
'r_rate': 0.05, # 5% capital return rate
|
||||
'g_rate': 0.03, # 3% economic growth rate
|
||||
'initial_capital': 1000.0,
|
||||
'initial_consumption': 1000.0,
|
||||
'num_agents': 100,
|
||||
'iterations': 1000
|
||||
}
|
||||
|
||||
return render_template('simulation.html',
|
||||
default_params=default_params,
|
||||
title="Markov Economics - Capitalism Eats the World")
|
||||
|
||||
|
||||
@main_bp.route('/simulation')
|
||||
def simulation():
|
||||
"""
|
||||
Alternative route to the main simulation page.
|
||||
|
||||
Returns:
|
||||
Rendered HTML template with simulation controls
|
||||
"""
|
||||
return index()
|
||||
|
||||
|
||||
@main_bp.route('/about')
|
||||
def about():
|
||||
"""
|
||||
Information page about the economic theory behind the simulation.
|
||||
|
||||
Returns:
|
||||
Rendered HTML template with theoretical background
|
||||
"""
|
||||
return render_template('about.html',
|
||||
title="About - Markov Economics Theory")
|
||||
|
||||
|
||||
@main_bp.route('/results/<simulation_id>')
|
||||
def results(simulation_id):
|
||||
"""
|
||||
Display results for a specific simulation.
|
||||
|
||||
Args:
|
||||
simulation_id: Unique identifier for the simulation
|
||||
|
||||
Returns:
|
||||
Rendered results template or 404 if simulation not found
|
||||
"""
|
||||
simulation = simulation_manager.get_simulation(simulation_id)
|
||||
|
||||
if not simulation:
|
||||
return render_template('error.html',
|
||||
error_message=f"Simulation {simulation_id} not found",
|
||||
title="Simulation Not Found"), 404
|
||||
|
||||
# Get simulation summary data
|
||||
latest_snapshot = simulation.get_latest_snapshot()
|
||||
iterations, total_wealth, gini_coefficients = simulation.get_wealth_evolution()
|
||||
|
||||
summary_data = {
|
||||
'simulation_id': simulation_id,
|
||||
'parameters': {
|
||||
'r_rate': simulation.parameters.r_rate,
|
||||
'g_rate': simulation.parameters.g_rate,
|
||||
'num_agents': simulation.parameters.num_agents,
|
||||
'iterations': simulation.parameters.iterations
|
||||
},
|
||||
'current_iteration': simulation.current_iteration,
|
||||
'total_iterations': len(simulation.snapshots),
|
||||
'latest_snapshot': latest_snapshot,
|
||||
'has_data': len(simulation.snapshots) > 0
|
||||
}
|
||||
|
||||
return render_template('results.html',
|
||||
simulation_data=summary_data,
|
||||
title=f"Results - Simulation {simulation_id[:8]}")
|
||||
|
||||
|
||||
@main_bp.route('/health')
|
||||
def health_check():
|
||||
"""
|
||||
Simple health check endpoint.
|
||||
|
||||
Returns:
|
||||
JSON response indicating service status
|
||||
"""
|
||||
return jsonify({
|
||||
'status': 'healthy',
|
||||
'service': 'markov-economics',
|
||||
'active_simulations': len(simulation_manager.active_simulations),
|
||||
'total_simulations': len(simulation_manager.simulations)
|
||||
})
|
||||
|
||||
|
||||
@main_bp.errorhandler(404)
|
||||
def not_found_error(error):
|
||||
"""
|
||||
Handle 404 errors with custom template.
|
||||
|
||||
Args:
|
||||
error: The error object
|
||||
|
||||
Returns:
|
||||
Rendered error template
|
||||
"""
|
||||
return render_template('error.html',
|
||||
error_message="Page not found",
|
||||
title="Page Not Found"), 404
|
||||
|
||||
|
||||
@main_bp.errorhandler(500)
|
||||
def internal_error(error):
|
||||
"""
|
||||
Handle 500 errors with custom template.
|
||||
|
||||
Args:
|
||||
error: The error object
|
||||
|
||||
Returns:
|
||||
Rendered error template
|
||||
"""
|
||||
return render_template('error.html',
|
||||
error_message="Internal server error occurred",
|
||||
title="Server Error"), 500
|
269
app/static/css/style.css
Normal file
269
app/static/css/style.css
Normal file
@@ -0,0 +1,269 @@
|
||||
/* Custom CSS for Markov Economics Application */
|
||||
|
||||
/* Global Styles */
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
.navbar-brand {
|
||||
font-weight: 700;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
/* Cards and Components */
|
||||
.card {
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
border-radius: 10px 10px 0 0 !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.parameter-card {
|
||||
border-left: 4px solid #007bff;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.parameter-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0,0,0,0.15) !important;
|
||||
}
|
||||
|
||||
/* Form Controls */
|
||||
.form-range {
|
||||
height: 8px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.form-range::-webkit-slider-thumb {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
border-radius: 50%;
|
||||
background: #007bff;
|
||||
cursor: pointer;
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.form-range::-moz-range-thumb {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
border-radius: 50%;
|
||||
background: #007bff;
|
||||
cursor: pointer;
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn-simulation {
|
||||
border-radius: 25px;
|
||||
font-weight: 600;
|
||||
padding: 10px 20px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-simulation:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
/* Charts */
|
||||
.chart-container {
|
||||
position: relative;
|
||||
height: 350px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chart-container canvas {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* Metrics Cards */
|
||||
.metrics-card {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.metrics-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.inequality-warning {
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
border: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.inequality-warning:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 10px 30px rgba(240, 147, 251, 0.3);
|
||||
}
|
||||
|
||||
/* Progress Bar */
|
||||
.progress-custom {
|
||||
height: 25px;
|
||||
border-radius: 15px;
|
||||
background-color: #e9ecef;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-custom .progress-bar {
|
||||
border-radius: 15px;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
line-height: 25px;
|
||||
}
|
||||
|
||||
/* Simulation Status */
|
||||
.simulation-status {
|
||||
border-radius: 20px;
|
||||
padding: 8px 16px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.status-ready {
|
||||
background-color: #6c757d !important;
|
||||
}
|
||||
|
||||
.status-running {
|
||||
background: linear-gradient(90deg, #28a745, #20c997) !important;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.status-complete {
|
||||
background-color: #007bff !important;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
background-color: #dc3545 !important;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { box-shadow: 0 0 0 0 rgba(40, 167, 69, 0.4); }
|
||||
70% { box-shadow: 0 0 0 10px rgba(40, 167, 69, 0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(40, 167, 69, 0); }
|
||||
}
|
||||
|
||||
/* Alerts */
|
||||
.alert {
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* Tooltips */
|
||||
.tooltip {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* Loading Spinner */
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid rgba(255,255,255,.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: #fff;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.chart-container {
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.parameter-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.metrics-card .card-body {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.btn-simulation {
|
||||
padding: 8px 16px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.chart-container {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.display-4 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animation classes */
|
||||
.fade-in {
|
||||
opacity: 0;
|
||||
animation: fadeIn 0.5s ease-in forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.slide-up {
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
animation: slideUp 0.5s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
.navbar, .card-header, .btn, footer {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 1px solid #ddd !important;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: auto !important;
|
||||
}
|
||||
}
|
281
app/static/js/app.js
Normal file
281
app/static/js/app.js
Normal file
@@ -0,0 +1,281 @@
|
||||
/**
|
||||
* Main JavaScript Application for Markov Economics
|
||||
*
|
||||
* Provides utility functions and global application behavior
|
||||
*/
|
||||
|
||||
// Global application state
|
||||
window.MarkovEconomics = {
|
||||
socket: null,
|
||||
currentSimulationId: null,
|
||||
isConnected: false,
|
||||
|
||||
// Configuration
|
||||
config: {
|
||||
maxRetries: 3,
|
||||
retryDelay: 1000,
|
||||
updateInterval: 100
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize Socket.IO connection
|
||||
*/
|
||||
function initializeSocket() {
|
||||
if (typeof io !== 'undefined') {
|
||||
window.MarkovEconomics.socket = io();
|
||||
|
||||
window.MarkovEconomics.socket.on('connect', function() {
|
||||
console.log('Connected to server');
|
||||
window.MarkovEconomics.isConnected = true;
|
||||
updateConnectionStatus(true);
|
||||
});
|
||||
|
||||
window.MarkovEconomics.socket.on('disconnect', function() {
|
||||
console.log('Disconnected from server');
|
||||
window.MarkovEconomics.isConnected = false;
|
||||
updateConnectionStatus(false);
|
||||
});
|
||||
|
||||
window.MarkovEconomics.socket.on('connect_error', function(error) {
|
||||
console.error('Connection error:', error);
|
||||
updateConnectionStatus(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update connection status indicator
|
||||
*/
|
||||
function updateConnectionStatus(connected) {
|
||||
// You can add a connection status indicator here if needed
|
||||
if (connected) {
|
||||
console.log('✅ Real-time connection established');
|
||||
} else {
|
||||
console.log('❌ Real-time connection lost');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format numbers for display
|
||||
*/
|
||||
function formatNumber(num, decimals = 2) {
|
||||
if (num === null || num === undefined || isNaN(num)) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
if (Math.abs(num) >= 1e9) {
|
||||
return (num / 1e9).toFixed(decimals) + 'B';
|
||||
} else if (Math.abs(num) >= 1e6) {
|
||||
return (num / 1e6).toFixed(decimals) + 'M';
|
||||
} else if (Math.abs(num) >= 1e3) {
|
||||
return (num / 1e3).toFixed(decimals) + 'K';
|
||||
} else {
|
||||
return num.toLocaleString(undefined, {
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format currency values
|
||||
*/
|
||||
function formatCurrency(amount, decimals = 0) {
|
||||
if (amount === null || amount === undefined || isNaN(amount)) {
|
||||
return '$0';
|
||||
}
|
||||
|
||||
return '$' + formatNumber(amount, decimals);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format percentage values
|
||||
*/
|
||||
function formatPercentage(value, decimals = 1) {
|
||||
if (value === null || value === undefined || isNaN(value)) {
|
||||
return '0%';
|
||||
}
|
||||
|
||||
return (value * 100).toFixed(decimals) + '%';
|
||||
}
|
||||
|
||||
/**
|
||||
* Show notification message
|
||||
*/
|
||||
function showNotification(message, type = 'info', duration = 3000) {
|
||||
// Create notification element
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
|
||||
notification.style.cssText = `
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 9999;
|
||||
min-width: 300px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
`;
|
||||
|
||||
notification.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// Auto-remove after duration
|
||||
setTimeout(() => {
|
||||
if (notification.parentNode) {
|
||||
notification.remove();
|
||||
}
|
||||
}, duration);
|
||||
|
||||
return notification;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show loading spinner
|
||||
*/
|
||||
function showLoading(element, text = 'Loading...') {
|
||||
if (typeof element === 'string') {
|
||||
element = document.getElementById(element);
|
||||
}
|
||||
|
||||
if (element) {
|
||||
element.innerHTML = `
|
||||
<div class="d-flex align-items-center justify-content-center">
|
||||
<div class="spinner me-2"></div>
|
||||
<span>${text}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide loading spinner
|
||||
*/
|
||||
function hideLoading(element, originalContent = '') {
|
||||
if (typeof element === 'string') {
|
||||
element = document.getElementById(element);
|
||||
}
|
||||
|
||||
if (element) {
|
||||
element.innerHTML = originalContent;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make API request with error handling
|
||||
*/
|
||||
async function apiRequest(url, options = {}) {
|
||||
const defaultOptions = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
const finalOptions = { ...defaultOptions, ...options };
|
||||
|
||||
try {
|
||||
const response = await fetch(url, finalOptions);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('API Request failed:', error);
|
||||
showNotification(`Error: ${error.message}`, 'danger');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate parameter ranges
|
||||
*/
|
||||
function validateParameters(params) {
|
||||
const errors = [];
|
||||
|
||||
if (params.r_rate < 0 || params.r_rate > 0.25) {
|
||||
errors.push('Capital rate must be between 0% and 25%');
|
||||
}
|
||||
|
||||
if (params.g_rate < 0 || params.g_rate > 0.20) {
|
||||
errors.push('Growth rate must be between 0% and 20%');
|
||||
}
|
||||
|
||||
if (params.num_agents < 10 || params.num_agents > 10000) {
|
||||
errors.push('Number of agents must be between 10 and 10,000');
|
||||
}
|
||||
|
||||
if (params.iterations < 100 || params.iterations > 100000) {
|
||||
errors.push('Iterations must be between 100 and 100,000');
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Debounce function for parameter changes
|
||||
*/
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize application
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize Socket.IO if available
|
||||
initializeSocket();
|
||||
|
||||
// Initialize tooltips
|
||||
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
|
||||
// Add fade-in animation to cards
|
||||
const cards = document.querySelectorAll('.card');
|
||||
cards.forEach((card, index) => {
|
||||
card.style.animationDelay = `${index * 0.1}s`;
|
||||
card.classList.add('fade-in');
|
||||
});
|
||||
|
||||
console.log('🚀 Markov Economics application initialized');
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle page visibility changes
|
||||
*/
|
||||
document.addEventListener('visibilitychange', function() {
|
||||
if (document.hidden) {
|
||||
console.log('Page hidden - pausing updates');
|
||||
} else {
|
||||
console.log('Page visible - resuming updates');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Export functions to global scope
|
||||
*/
|
||||
window.MarkovEconomics.utils = {
|
||||
formatNumber,
|
||||
formatCurrency,
|
||||
formatPercentage,
|
||||
showNotification,
|
||||
showLoading,
|
||||
hideLoading,
|
||||
apiRequest,
|
||||
validateParameters,
|
||||
debounce
|
||||
};
|
743
app/static/js/simulation.js
Normal file
743
app/static/js/simulation.js
Normal file
@@ -0,0 +1,743 @@
|
||||
/**
|
||||
* Simulation Control and Visualization
|
||||
*
|
||||
* Handles the main simulation interface, real-time charting,
|
||||
* and parameter control for the Markov Economics application.
|
||||
*/
|
||||
|
||||
// Simulation state
|
||||
let currentSimulation = {
|
||||
id: null,
|
||||
isRunning: false,
|
||||
parameters: {},
|
||||
data: {
|
||||
iterations: [],
|
||||
totalWealth: [],
|
||||
giniCoefficients: [],
|
||||
capitalShare: [],
|
||||
top10Share: []
|
||||
}
|
||||
};
|
||||
|
||||
// Chart instances
|
||||
let charts = {
|
||||
wealthEvolution: null,
|
||||
inequality: null,
|
||||
distribution: null
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize the simulation interface
|
||||
*/
|
||||
function initializeSimulation() {
|
||||
console.log('Initializing simulation interface...');
|
||||
|
||||
// Initialize parameter controls
|
||||
initializeParameterControls();
|
||||
|
||||
// Initialize charts
|
||||
initializeCharts();
|
||||
|
||||
// Initialize event listeners
|
||||
initializeEventListeners();
|
||||
|
||||
// Initialize real-time updates
|
||||
initializeRealtimeUpdates();
|
||||
|
||||
console.log('✅ Simulation interface ready');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize parameter controls with sliders
|
||||
*/
|
||||
function initializeParameterControls() {
|
||||
const controls = [
|
||||
{ id: 'capitalRate', valueId: 'capitalRateValue', suffix: '%', scale: 100 },
|
||||
{ id: 'growthRate', valueId: 'growthRateValue', suffix: '%', scale: 100 },
|
||||
{ id: 'numAgents', valueId: 'numAgentsValue', suffix: '', scale: 1 },
|
||||
{ id: 'iterations', valueId: 'iterationsValue', suffix: '', scale: 1 }
|
||||
];
|
||||
|
||||
controls.forEach(control => {
|
||||
const slider = document.getElementById(control.id);
|
||||
const valueDisplay = document.getElementById(control.valueId);
|
||||
|
||||
if (slider && valueDisplay) {
|
||||
slider.addEventListener('input', function() {
|
||||
const value = parseFloat(this.value);
|
||||
const displayValue = (value / control.scale).toFixed(control.scale === 100 ? 1 : 0);
|
||||
valueDisplay.textContent = displayValue + control.suffix;
|
||||
|
||||
// Update inequality warning
|
||||
updateInequalityWarning();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update inequality warning based on r vs g
|
||||
*/
|
||||
function updateInequalityWarning() {
|
||||
const capitalRate = parseFloat(document.getElementById('capitalRate').value) / 100;
|
||||
const growthRate = parseFloat(document.getElementById('growthRate').value) / 100;
|
||||
const warning = document.getElementById('inequalityAlert');
|
||||
|
||||
if (warning) {
|
||||
if (capitalRate > growthRate) {
|
||||
warning.style.display = 'block';
|
||||
warning.className = 'alert alert-danger';
|
||||
warning.innerHTML = `
|
||||
<strong>⚠️ r > g Detected!</strong><br>
|
||||
<small>Capital rate (${(capitalRate * 100).toFixed(1)}%) > Growth rate (${(growthRate * 100).toFixed(1)}%)</small><br>
|
||||
<small>Wealth inequality will increase over time</small>
|
||||
`;
|
||||
} else if (capitalRate === growthRate) {
|
||||
warning.style.display = 'block';
|
||||
warning.className = 'alert alert-warning';
|
||||
warning.innerHTML = `
|
||||
<strong>⚖️ r = g</strong><br>
|
||||
<small>Balanced scenario - moderate inequality growth expected</small>
|
||||
`;
|
||||
} else {
|
||||
warning.style.display = 'block';
|
||||
warning.className = 'alert alert-success';
|
||||
warning.innerHTML = `
|
||||
<strong>✅ r < g</strong><br>
|
||||
<small>Growth rate exceeds capital rate - inequality may decrease</small>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Chart.js instances
|
||||
*/
|
||||
function initializeCharts() {
|
||||
// Wealth Evolution Chart
|
||||
const wealthCtx = document.getElementById('wealthEvolutionChart');
|
||||
if (wealthCtx) {
|
||||
charts.wealthEvolution = new Chart(wealthCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
label: 'Total Wealth',
|
||||
data: [],
|
||||
borderColor: '#007bff',
|
||||
backgroundColor: 'rgba(0, 123, 255, 0.1)',
|
||||
tension: 0.4,
|
||||
fill: true
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Wealth Accumulation Over Time'
|
||||
},
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Iteration'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Total Wealth ($)'
|
||||
},
|
||||
ticks: {
|
||||
callback: function(value) {
|
||||
return window.MarkovEconomics.utils.formatCurrency(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
duration: 300
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Inequality Metrics Chart
|
||||
const inequalityCtx = document.getElementById('inequalityChart');
|
||||
if (inequalityCtx) {
|
||||
charts.inequality = new Chart(inequalityCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Gini Coefficient',
|
||||
data: [],
|
||||
borderColor: '#dc3545',
|
||||
backgroundColor: 'rgba(220, 53, 69, 0.1)',
|
||||
yAxisID: 'y',
|
||||
tension: 0.4
|
||||
},
|
||||
{
|
||||
label: 'Top 10% Share',
|
||||
data: [],
|
||||
borderColor: '#fd7e14',
|
||||
backgroundColor: 'rgba(253, 126, 20, 0.1)',
|
||||
yAxisID: 'y1',
|
||||
tension: 0.4
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Inequality Metrics'
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Iteration'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
display: true,
|
||||
position: 'left',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Gini Coefficient'
|
||||
},
|
||||
min: 0,
|
||||
max: 1
|
||||
},
|
||||
y1: {
|
||||
type: 'linear',
|
||||
display: true,
|
||||
position: 'right',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Top 10% Share (%)'
|
||||
},
|
||||
grid: {
|
||||
drawOnChartArea: false,
|
||||
},
|
||||
ticks: {
|
||||
callback: function(value) {
|
||||
return (value * 100).toFixed(0) + '%';
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
duration: 300
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Wealth Distribution Chart
|
||||
const distributionCtx = document.getElementById('distributionChart');
|
||||
if (distributionCtx) {
|
||||
charts.distribution = new Chart(distributionCtx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
label: 'Number of Agents',
|
||||
data: [],
|
||||
backgroundColor: 'rgba(40, 167, 69, 0.7)',
|
||||
borderColor: '#28a745',
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Wealth Distribution'
|
||||
},
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Wealth Range'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Number of Agents'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize event listeners
|
||||
*/
|
||||
function initializeEventListeners() {
|
||||
// Start simulation button
|
||||
const startBtn = document.getElementById('startBtn');
|
||||
if (startBtn) {
|
||||
startBtn.addEventListener('click', startSimulation);
|
||||
}
|
||||
|
||||
// Stop simulation button
|
||||
const stopBtn = document.getElementById('stopBtn');
|
||||
if (stopBtn) {
|
||||
stopBtn.addEventListener('click', stopSimulation);
|
||||
}
|
||||
|
||||
// Reset button
|
||||
const resetBtn = document.getElementById('resetBtn');
|
||||
if (resetBtn) {
|
||||
resetBtn.addEventListener('click', resetSimulation);
|
||||
}
|
||||
|
||||
// Export buttons
|
||||
const exportJsonBtn = document.getElementById('exportJsonBtn');
|
||||
if (exportJsonBtn) {
|
||||
exportJsonBtn.addEventListener('click', () => exportData('json'));
|
||||
}
|
||||
|
||||
const exportCsvBtn = document.getElementById('exportCsvBtn');
|
||||
if (exportCsvBtn) {
|
||||
exportCsvBtn.addEventListener('click', () => exportData('csv'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize real-time updates via Socket.IO
|
||||
*/
|
||||
function initializeRealtimeUpdates() {
|
||||
if (!window.MarkovEconomics.socket) {
|
||||
console.warn('Socket.IO not available - real-time updates disabled');
|
||||
return;
|
||||
}
|
||||
|
||||
const socket = window.MarkovEconomics.socket;
|
||||
|
||||
// Simulation progress updates
|
||||
socket.on('simulation_progress', function(data) {
|
||||
updateSimulationProgress(data);
|
||||
});
|
||||
|
||||
// Simulation completion
|
||||
socket.on('simulation_complete', function(data) {
|
||||
onSimulationComplete(data);
|
||||
});
|
||||
|
||||
// Simulation stopped
|
||||
socket.on('simulation_stopped', function(data) {
|
||||
onSimulationStopped(data);
|
||||
});
|
||||
|
||||
// Simulation error
|
||||
socket.on('simulation_error', function(data) {
|
||||
onSimulationError(data);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new simulation
|
||||
*/
|
||||
async function startSimulation() {
|
||||
try {
|
||||
// Get parameters from UI
|
||||
const parameters = getSimulationParameters();
|
||||
|
||||
// Validate parameters
|
||||
const errors = window.MarkovEconomics.utils.validateParameters(parameters);
|
||||
if (errors.length > 0) {
|
||||
window.MarkovEconomics.utils.showNotification(
|
||||
'Parameter validation failed:<br>' + errors.join('<br>'),
|
||||
'danger'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update UI state
|
||||
updateUIState('starting');
|
||||
|
||||
// Create simulation
|
||||
const createResponse = await window.MarkovEconomics.utils.apiRequest('/api/simulation', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(parameters)
|
||||
});
|
||||
|
||||
currentSimulation.id = createResponse.simulation_id;
|
||||
currentSimulation.parameters = parameters;
|
||||
|
||||
// Join simulation room for real-time updates
|
||||
if (window.MarkovEconomics.socket) {
|
||||
window.MarkovEconomics.socket.emit('join_simulation', {
|
||||
simulation_id: currentSimulation.id
|
||||
});
|
||||
}
|
||||
|
||||
// Start simulation
|
||||
await window.MarkovEconomics.utils.apiRequest(`/api/simulation/${currentSimulation.id}/start`, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
currentSimulation.isRunning = true;
|
||||
updateUIState('running');
|
||||
|
||||
window.MarkovEconomics.utils.showNotification('Simulation started successfully!', 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to start simulation:', error);
|
||||
updateUIState('error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the current simulation
|
||||
*/
|
||||
async function stopSimulation() {
|
||||
if (!currentSimulation.id) return;
|
||||
|
||||
try {
|
||||
await window.MarkovEconomics.utils.apiRequest(`/api/simulation/${currentSimulation.id}/stop`, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
currentSimulation.isRunning = false;
|
||||
updateUIState('stopped');
|
||||
|
||||
window.MarkovEconomics.utils.showNotification('Simulation stopped', 'info');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to stop simulation:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset simulation state
|
||||
*/
|
||||
function resetSimulation() {
|
||||
currentSimulation = {
|
||||
id: null,
|
||||
isRunning: false,
|
||||
parameters: {},
|
||||
data: {
|
||||
iterations: [],
|
||||
totalWealth: [],
|
||||
giniCoefficients: [],
|
||||
capitalShare: [],
|
||||
top10Share: []
|
||||
}
|
||||
};
|
||||
|
||||
// Clear charts
|
||||
Object.values(charts).forEach(chart => {
|
||||
if (chart) {
|
||||
chart.data.labels = [];
|
||||
chart.data.datasets.forEach(dataset => {
|
||||
dataset.data = [];
|
||||
});
|
||||
chart.update();
|
||||
}
|
||||
});
|
||||
|
||||
// Reset metrics
|
||||
updateMetricsDisplay({
|
||||
total_wealth: 0,
|
||||
gini_coefficient: 0,
|
||||
wealth_concentration_top10: 0,
|
||||
capital_share: 0
|
||||
});
|
||||
|
||||
updateUIState('ready');
|
||||
|
||||
window.MarkovEconomics.utils.showNotification('Simulation reset', 'info');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current simulation parameters from UI
|
||||
*/
|
||||
function getSimulationParameters() {
|
||||
return {
|
||||
r_rate: parseFloat(document.getElementById('capitalRate').value) / 100,
|
||||
g_rate: parseFloat(document.getElementById('growthRate').value) / 100,
|
||||
num_agents: parseInt(document.getElementById('numAgents').value),
|
||||
iterations: parseInt(document.getElementById('iterations').value),
|
||||
initial_capital: 1000,
|
||||
initial_consumption: 1000
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update UI state based on simulation status
|
||||
*/
|
||||
function updateUIState(state) {
|
||||
const startBtn = document.getElementById('startBtn');
|
||||
const stopBtn = document.getElementById('stopBtn');
|
||||
const resetBtn = document.getElementById('resetBtn');
|
||||
const status = document.getElementById('simulationStatus');
|
||||
const progress = document.getElementById('progressContainer');
|
||||
const exportBtns = [
|
||||
document.getElementById('exportJsonBtn'),
|
||||
document.getElementById('exportCsvBtn')
|
||||
];
|
||||
|
||||
switch (state) {
|
||||
case 'starting':
|
||||
if (startBtn) startBtn.disabled = true;
|
||||
if (stopBtn) stopBtn.disabled = true;
|
||||
if (resetBtn) resetBtn.disabled = true;
|
||||
if (status) {
|
||||
status.textContent = 'Starting...';
|
||||
status.className = 'simulation-status bg-warning text-dark';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'running':
|
||||
if (startBtn) startBtn.disabled = true;
|
||||
if (stopBtn) stopBtn.disabled = false;
|
||||
if (resetBtn) resetBtn.disabled = true;
|
||||
if (status) {
|
||||
status.textContent = 'Running';
|
||||
status.className = 'simulation-status status-running';
|
||||
}
|
||||
if (progress) progress.style.display = 'block';
|
||||
break;
|
||||
|
||||
case 'complete':
|
||||
if (startBtn) startBtn.disabled = false;
|
||||
if (stopBtn) stopBtn.disabled = true;
|
||||
if (resetBtn) resetBtn.disabled = false;
|
||||
if (status) {
|
||||
status.textContent = 'Complete';
|
||||
status.className = 'simulation-status status-complete';
|
||||
}
|
||||
if (progress) progress.style.display = 'none';
|
||||
exportBtns.forEach(btn => {
|
||||
if (btn) btn.disabled = false;
|
||||
});
|
||||
break;
|
||||
|
||||
case 'stopped':
|
||||
if (startBtn) startBtn.disabled = false;
|
||||
if (stopBtn) stopBtn.disabled = true;
|
||||
if (resetBtn) resetBtn.disabled = false;
|
||||
if (status) {
|
||||
status.textContent = 'Stopped';
|
||||
status.className = 'simulation-status bg-warning text-dark';
|
||||
}
|
||||
if (progress) progress.style.display = 'none';
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
if (startBtn) startBtn.disabled = false;
|
||||
if (stopBtn) stopBtn.disabled = true;
|
||||
if (resetBtn) resetBtn.disabled = false;
|
||||
if (status) {
|
||||
status.textContent = 'Error';
|
||||
status.className = 'simulation-status status-error';
|
||||
}
|
||||
if (progress) progress.style.display = 'none';
|
||||
break;
|
||||
|
||||
default: // 'ready'
|
||||
if (startBtn) startBtn.disabled = false;
|
||||
if (stopBtn) stopBtn.disabled = true;
|
||||
if (resetBtn) resetBtn.disabled = false;
|
||||
if (status) {
|
||||
status.textContent = 'Ready to start';
|
||||
status.className = 'simulation-status status-ready';
|
||||
}
|
||||
if (progress) progress.style.display = 'none';
|
||||
exportBtns.forEach(btn => {
|
||||
if (btn) btn.disabled = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update simulation progress
|
||||
*/
|
||||
function updateSimulationProgress(data) {
|
||||
// Update progress bar
|
||||
const progressBar = document.getElementById('progressBar');
|
||||
const progressText = document.getElementById('progressText');
|
||||
|
||||
if (progressBar && progressText) {
|
||||
const percentage = data.progress_percentage || 0;
|
||||
progressBar.style.width = percentage + '%';
|
||||
progressText.textContent = percentage.toFixed(1) + '%';
|
||||
}
|
||||
|
||||
// Update charts and metrics
|
||||
if (data.iteration !== undefined) {
|
||||
currentSimulation.data.iterations.push(data.iteration);
|
||||
currentSimulation.data.totalWealth.push(data.total_wealth || 0);
|
||||
currentSimulation.data.giniCoefficients.push(data.gini_coefficient || 0);
|
||||
currentSimulation.data.capitalShare.push(data.capital_share || 0);
|
||||
currentSimulation.data.top10Share.push(data.wealth_concentration_top10 || 0);
|
||||
|
||||
updateCharts();
|
||||
updateMetricsDisplay(data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update charts with new data
|
||||
*/
|
||||
function updateCharts() {
|
||||
// Wealth Evolution Chart
|
||||
if (charts.wealthEvolution) {
|
||||
charts.wealthEvolution.data.labels = currentSimulation.data.iterations;
|
||||
charts.wealthEvolution.data.datasets[0].data = currentSimulation.data.totalWealth;
|
||||
charts.wealthEvolution.update('none');
|
||||
}
|
||||
|
||||
// Inequality Chart
|
||||
if (charts.inequality) {
|
||||
charts.inequality.data.labels = currentSimulation.data.iterations;
|
||||
charts.inequality.data.datasets[0].data = currentSimulation.data.giniCoefficients;
|
||||
charts.inequality.data.datasets[1].data = currentSimulation.data.top10Share;
|
||||
charts.inequality.update('none');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update metrics display
|
||||
*/
|
||||
function updateMetricsDisplay(data) {
|
||||
const formatters = window.MarkovEconomics.utils;
|
||||
|
||||
const totalWealthEl = document.getElementById('totalWealthMetric');
|
||||
if (totalWealthEl) {
|
||||
totalWealthEl.textContent = formatters.formatCurrency(data.total_wealth || 0);
|
||||
}
|
||||
|
||||
const giniEl = document.getElementById('giniMetric');
|
||||
if (giniEl) {
|
||||
giniEl.textContent = (data.gini_coefficient || 0).toFixed(3);
|
||||
}
|
||||
|
||||
const top10El = document.getElementById('top10Metric');
|
||||
if (top10El) {
|
||||
top10El.textContent = formatters.formatPercentage(data.wealth_concentration_top10 || 0);
|
||||
}
|
||||
|
||||
const capitalShareEl = document.getElementById('capitalShareMetric');
|
||||
if (capitalShareEl) {
|
||||
capitalShareEl.textContent = formatters.formatPercentage(data.capital_share || 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle simulation completion
|
||||
*/
|
||||
function onSimulationComplete(data) {
|
||||
currentSimulation.isRunning = false;
|
||||
updateUIState('complete');
|
||||
|
||||
window.MarkovEconomics.utils.showNotification(
|
||||
`Simulation completed! ${data.total_snapshots} data points collected.`,
|
||||
'success'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle simulation stopped
|
||||
*/
|
||||
function onSimulationStopped(data) {
|
||||
currentSimulation.isRunning = false;
|
||||
updateUIState('stopped');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle simulation error
|
||||
*/
|
||||
function onSimulationError(data) {
|
||||
currentSimulation.isRunning = false;
|
||||
updateUIState('error');
|
||||
|
||||
window.MarkovEconomics.utils.showNotification(
|
||||
`Simulation error: ${data.error}`,
|
||||
'danger'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export simulation data
|
||||
*/
|
||||
async function exportData(format) {
|
||||
if (!currentSimulation.id) {
|
||||
window.MarkovEconomics.utils.showNotification('No simulation data to export', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/simulation/${currentSimulation.id}/export/${format}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Export failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
if (format === 'csv') {
|
||||
const csvData = await response.text();
|
||||
downloadFile(csvData, `simulation_${currentSimulation.id.slice(0, 8)}.csv`, 'text/csv');
|
||||
} else {
|
||||
const jsonData = await response.json();
|
||||
downloadFile(
|
||||
JSON.stringify(jsonData.data, null, 2),
|
||||
`simulation_${currentSimulation.id.slice(0, 8)}.json`,
|
||||
'application/json'
|
||||
);
|
||||
}
|
||||
|
||||
window.MarkovEconomics.utils.showNotification(`Data exported as ${format.toUpperCase()}`, 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Export failed:', error);
|
||||
window.MarkovEconomics.utils.showNotification(`Export failed: ${error.message}`, 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download file helper
|
||||
*/
|
||||
function downloadFile(content, filename, contentType) {
|
||||
const blob = new Blob([content], { type: contentType });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initial inequality warning update
|
||||
updateInequalityWarning();
|
||||
});
|
||||
|
||||
// Export to global scope
|
||||
window.initializeSimulation = initializeSimulation;
|
286
app/templates/about.html
Normal file
286
app/templates/about.html
Normal file
@@ -0,0 +1,286 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-10">
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-5">
|
||||
<h1 class="display-4">🎯 About Markov Economics</h1>
|
||||
<p class="lead">Understanding how capitalism "eats the world" through Markov chain analysis</p>
|
||||
</div>
|
||||
|
||||
<!-- Theoretical Foundation -->
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h4 class="mb-0">📚 Theoretical Foundation</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h5>Marx's Economic Cycles</h5>
|
||||
<p>Karl Marx identified two fundamental economic circulation patterns:</p>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<h6><strong>M-C-M' (Capitalist Circuit)</strong></h6>
|
||||
<p class="mb-2">Money → Commodities → More Money</p>
|
||||
<small>
|
||||
Capital is invested in production to generate surplus value.
|
||||
The goal is accumulation - turning M into M' where M' > M.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-success">
|
||||
<h6><strong>C-M-C (Consumer Circuit)</strong></h6>
|
||||
<p class="mb-2">Commodities → Money → Commodities</p>
|
||||
<small>
|
||||
Goods are sold to acquire money to purchase other goods.
|
||||
The goal is consumption and use-value satisfaction.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<h5>Piketty's Inequality Principle</h5>
|
||||
<p>Thomas Piketty demonstrated that wealth inequality increases when:</p>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<h6><strong>r > g</strong></h6>
|
||||
<p class="mb-2">Capital Return Rate > Economic Growth Rate</p>
|
||||
<small>
|
||||
When capital generates returns faster than the overall economy grows,
|
||||
wealth concentrates among capital owners, leading to increasing inequality.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<h6>Key Parameters:</h6>
|
||||
<ul>
|
||||
<li><strong>r:</strong> Rate of return on capital (stocks, real estate, business)</li>
|
||||
<li><strong>g:</strong> Economic growth rate (GDP growth)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Markov Chain Implementation -->
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h4 class="mb-0">⚙️ Markov Chain Implementation</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>This simulation models economic behavior as Markov chains with state-dependent transition probabilities:</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h5>Capitalist Chain States</h5>
|
||||
<div class="list-group">
|
||||
<div class="list-group-item">
|
||||
<strong>Money (M)</strong>
|
||||
<br><small>Initial capital seeking investment opportunities</small>
|
||||
</div>
|
||||
<div class="list-group-item">
|
||||
<strong>Commodities (C)</strong>
|
||||
<br><small>Capital invested in production/goods</small>
|
||||
</div>
|
||||
<div class="list-group-item">
|
||||
<strong>Enhanced Money (M')</strong>
|
||||
<br><small>Capital with accumulated returns</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<h6>Transition Probability Matrix:</h6>
|
||||
<pre class="bg-light p-2">
|
||||
M → C: r (capital rate)
|
||||
C → M': 1 (always)
|
||||
M' → M: 1 (reinvestment)
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<h5>Consumer Chain States</h5>
|
||||
<div class="list-group">
|
||||
<div class="list-group-item">
|
||||
<strong>Commodities (C)</strong>
|
||||
<br><small>Goods available for consumption</small>
|
||||
</div>
|
||||
<div class="list-group-item">
|
||||
<strong>Money (M)</strong>
|
||||
<br><small>Liquid currency from sales</small>
|
||||
</div>
|
||||
<div class="list-group-item">
|
||||
<strong>New Commodities (C')</strong>
|
||||
<br><small>Purchased goods for consumption</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<h6>Transition Probability Matrix:</h6>
|
||||
<pre class="bg-light p-2">
|
||||
C → M: g (growth rate)
|
||||
M → C': 1 (always)
|
||||
C' → C: 1 (consumption cycle)
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Simulation Mechanics -->
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header bg-warning text-dark">
|
||||
<h4 class="mb-0">🔬 Simulation Mechanics</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<h5>How the Simulation Works</h5>
|
||||
<ol>
|
||||
<li><strong>Agent Initialization:</strong> Each economic agent starts with equal capital and consumption capacity</li>
|
||||
<li><strong>Dual Chain Operation:</strong> Every agent runs both capitalist (M-C-M') and consumer (C-M-C) chains simultaneously</li>
|
||||
<li><strong>Wealth Accumulation:</strong> Capitalist chains multiply wealth by (1 + r) on each complete cycle</li>
|
||||
<li><strong>Consumption Growth:</strong> Consumer chains grow by (1 + g) on each complete cycle</li>
|
||||
<li><strong>Inequality Emergence:</strong> When r > g, capital wealth grows faster than consumption, concentrating among fewer agents</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<h5>Key Metrics</h5>
|
||||
<div class="list-group list-group-flush">
|
||||
<div class="list-group-item px-0">
|
||||
<strong>Gini Coefficient</strong>
|
||||
<br><small>Measures inequality (0 = perfect equality, 1 = perfect inequality)</small>
|
||||
</div>
|
||||
<div class="list-group-item px-0">
|
||||
<strong>Top 10% Share</strong>
|
||||
<br><small>Percentage of total wealth held by richest 10%</small>
|
||||
</div>
|
||||
<div class="list-group-item px-0">
|
||||
<strong>Capital Share</strong>
|
||||
<br><small>Proportion of wealth from capital vs consumption</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Experimental Parameters -->
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<h4 class="mb-0">🧪 Experimental Parameters</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h5>Try These Scenarios</h5>
|
||||
|
||||
<div class="alert alert-success">
|
||||
<h6>Scenario 1: Stable Economy (r ≈ g)</h6>
|
||||
<ul class="mb-0">
|
||||
<li>Capital Rate: 3%</li>
|
||||
<li>Growth Rate: 3%</li>
|
||||
<li>Expected: Moderate inequality growth</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<h6>Scenario 2: Modern Capitalism (r > g)</h6>
|
||||
<ul class="mb-0">
|
||||
<li>Capital Rate: 5%</li>
|
||||
<li>Growth Rate: 2%</li>
|
||||
<li>Expected: Increasing inequality</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-danger">
|
||||
<h6>Scenario 3: Extreme Inequality (r >> g)</h6>
|
||||
<ul class="mb-0">
|
||||
<li>Capital Rate: 8%</li>
|
||||
<li>Growth Rate: 1%</li>
|
||||
<li>Expected: Rapid wealth concentration</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<h5>Historical Context</h5>
|
||||
<p>Real-world examples of r vs g:</p>
|
||||
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Period</th>
|
||||
<th>r (Capital)</th>
|
||||
<th>g (Growth)</th>
|
||||
<th>Inequality</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Gilded Age (1870-1914)</td>
|
||||
<td>4-5%</td>
|
||||
<td>1-1.5%</td>
|
||||
<td>Very High</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Post-War Era (1950-1980)</td>
|
||||
<td>2-3%</td>
|
||||
<td>3-4%</td>
|
||||
<td>Decreasing</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Modern Era (1980-2020)</td>
|
||||
<td>4-6%</td>
|
||||
<td>1-2%</td>
|
||||
<td>Increasing</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Technical Implementation -->
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header bg-dark text-white">
|
||||
<h4 class="mb-0">⚡ Technical Implementation</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h5>Technology Stack</h5>
|
||||
<ul>
|
||||
<li><strong>Backend:</strong> Python Flask with SocketIO</li>
|
||||
<li><strong>Modeling:</strong> NumPy for matrix operations</li>
|
||||
<li><strong>Visualization:</strong> Chart.js for real-time charts</li>
|
||||
<li><strong>Frontend:</strong> Bootstrap 5 with responsive design</li>
|
||||
<li><strong>Real-time:</strong> WebSocket connections for live updates</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<h5>Performance Characteristics</h5>
|
||||
<ul>
|
||||
<li><strong>Scalability:</strong> Up to 10,000 agents</li>
|
||||
<li><strong>Speed:</strong> Real-time visualization of state transitions</li>
|
||||
<li><strong>Accuracy:</strong> Precise Markov chain calculations</li>
|
||||
<li><strong>Export:</strong> JSON and CSV data export</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Call to Action -->
|
||||
<div class="text-center">
|
||||
<a href="{{ url_for('main.index') }}" class="btn btn-primary btn-lg">
|
||||
🚀 Start Your Simulation
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
90
app/templates/base.html
Normal file
90
app/templates/base.html
Normal file
@@ -0,0 +1,90 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}{{ title }}{% endblock %}</title>
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Chart.js -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<!-- Socket.IO -->
|
||||
<script src="https://cdn.socket.io/4.6.0/socket.io.min.js"></script>
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||
|
||||
{% block extra_head %}{% endblock %}
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="{{ url_for('main.index') }}">
|
||||
<strong>📈 Markov Economics</strong>
|
||||
</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('main.index') }}">Simulation</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('main.about') }}">About</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<span class="navbar-text">
|
||||
<small>When r > g, capitalism eats the world</small>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="container-fluid mt-4">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="bg-dark text-light py-4 mt-5">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<h6>Markov Economics Simulation</h6>
|
||||
<p class="mb-0">
|
||||
<small>
|
||||
Demonstrating Marx's M-C-M' model using Markov chains and Piketty's inequality principle (r > g).
|
||||
Built with Flask, SocketIO, and Chart.js.
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-md-end">
|
||||
<p class="mb-0">
|
||||
<small>
|
||||
<strong>M-C-M':</strong> Money → Commodities → More Money<br>
|
||||
<strong>C-M-C:</strong> Commodities → Money → Commodities
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<!-- Custom JavaScript -->
|
||||
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
|
||||
|
||||
{% block extra_scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
35
app/templates/error.html
Normal file
35
app/templates/error.html
Normal file
@@ -0,0 +1,35 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="text-center">
|
||||
<div class="card shadow">
|
||||
<div class="card-body p-5">
|
||||
<div class="mb-4">
|
||||
<i class="text-danger" style="font-size: 4rem;">❌</i>
|
||||
</div>
|
||||
|
||||
<h2 class="card-title text-danger">Oops!</h2>
|
||||
<p class="card-text lead">{{ error_message }}</p>
|
||||
|
||||
<div class="mt-4">
|
||||
<a href="{{ url_for('main.index') }}" class="btn btn-primary btn-lg me-2">
|
||||
🏠 Go Home
|
||||
</a>
|
||||
<button onclick="history.back()" class="btn btn-outline-secondary">
|
||||
← Go Back
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<small class="text-muted">
|
||||
If this problem persists, the simulation may have encountered an unexpected error.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
295
app/templates/simulation.html
Normal file
295
app/templates/simulation.html
Normal file
@@ -0,0 +1,295 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block extra_head %}
|
||||
<style>
|
||||
.parameter-card {
|
||||
border-left: 4px solid #007bff;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
position: relative;
|
||||
height: 400px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.metrics-card {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.inequality-warning {
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.progress-custom {
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.simulation-status {
|
||||
font-size: 0.9rem;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.btn-simulation {
|
||||
min-width: 120px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<!-- Parameter Controls Panel -->
|
||||
<div class="col-lg-3 col-md-4">
|
||||
<div class="card parameter-card shadow-sm">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0">📊 Simulation Parameters</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Capital Return Rate (r) -->
|
||||
<div class="mb-4">
|
||||
<label for="capitalRate" class="form-label">
|
||||
<strong>Capital Rate (r)</strong>
|
||||
<span class="badge bg-info ms-1" data-bs-toggle="tooltip"
|
||||
title="Rate of return on capital investment">?</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input type="range" class="form-range" id="capitalRate"
|
||||
min="0" max="25" step="0.1" value="{{ default_params.r_rate * 100 }}">
|
||||
<span class="input-group-text" id="capitalRateValue">{{ "%.1f"|format(default_params.r_rate * 100) }}%</span>
|
||||
</div>
|
||||
<small class="text-muted">Higher values increase wealth accumulation</small>
|
||||
</div>
|
||||
|
||||
<!-- Economic Growth Rate (g) -->
|
||||
<div class="mb-4">
|
||||
<label for="growthRate" class="form-label">
|
||||
<strong>Growth Rate (g)</strong>
|
||||
<span class="badge bg-info ms-1" data-bs-toggle="tooltip"
|
||||
title="Overall economic growth rate">?</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input type="range" class="form-range" id="growthRate"
|
||||
min="0" max="20" step="0.1" value="{{ default_params.g_rate * 100 }}">
|
||||
<span class="input-group-text" id="growthRateValue">{{ "%.1f"|format(default_params.g_rate * 100) }}%</span>
|
||||
</div>
|
||||
<small class="text-muted">Affects consumption growth</small>
|
||||
</div>
|
||||
|
||||
<!-- Number of Agents -->
|
||||
<div class="mb-4">
|
||||
<label for="numAgents" class="form-label">
|
||||
<strong>Population Size</strong>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input type="range" class="form-range" id="numAgents"
|
||||
min="10" max="1000" step="10" value="{{ default_params.num_agents }}">
|
||||
<span class="input-group-text" id="numAgentsValue">{{ default_params.num_agents }}</span>
|
||||
</div>
|
||||
<small class="text-muted">Number of economic agents</small>
|
||||
</div>
|
||||
|
||||
<!-- Simulation Length -->
|
||||
<div class="mb-4">
|
||||
<label for="iterations" class="form-label">
|
||||
<strong>Time Steps</strong>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input type="range" class="form-range" id="iterations"
|
||||
min="100" max="5000" step="100" value="{{ default_params.iterations }}">
|
||||
<span class="input-group-text" id="iterationsValue">{{ default_params.iterations }}</span>
|
||||
</div>
|
||||
<small class="text-muted">Simulation duration</small>
|
||||
</div>
|
||||
|
||||
<!-- Inequality Indicator -->
|
||||
<div class="alert alert-warning" id="inequalityAlert" style="display: none;">
|
||||
<strong>⚠️ r > g Detected!</strong><br>
|
||||
<small>Wealth inequality will increase over time</small>
|
||||
</div>
|
||||
|
||||
<!-- Control Buttons -->
|
||||
<div class="d-grid gap-2">
|
||||
<button type="button" class="btn btn-success btn-simulation" id="startBtn">
|
||||
▶️ Start Simulation
|
||||
</button>
|
||||
<button type="button" class="btn btn-warning btn-simulation" id="stopBtn" disabled>
|
||||
⏸️ Stop
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary btn-simulation" id="resetBtn">
|
||||
🔄 Reset
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Simulation Status -->
|
||||
<div class="mt-3">
|
||||
<div class="simulation-status bg-secondary text-white text-center" id="simulationStatus">
|
||||
Ready to start
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<div class="mt-3" id="progressContainer" style="display: none;">
|
||||
<div class="progress progress-custom">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
||||
id="progressBar" role="progressbar" style="width: 0%">
|
||||
<span id="progressText">0%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Info Card -->
|
||||
<div class="card mt-3 shadow-sm">
|
||||
<div class="card-header bg-dark text-white">
|
||||
<h6 class="mb-0">💡 Theory</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<small>
|
||||
<strong>M-C-M':</strong> Capitalist cycle where money (M) is invested in commodities (C) to generate more money (M').<br><br>
|
||||
|
||||
<strong>C-M-C:</strong> Consumer cycle where commodities (C) are exchanged for money (M) to buy other commodities (C).<br><br>
|
||||
|
||||
<strong>When r > g:</strong> Capital returns exceed economic growth, leading to increasing wealth inequality.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Visualization Area -->
|
||||
<div class="col-lg-9 col-md-8">
|
||||
<!-- Key Metrics Cards -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card metrics-card text-white shadow">
|
||||
<div class="card-body text-center">
|
||||
<h6 class="card-title">Total Wealth</h6>
|
||||
<h4 id="totalWealthMetric">$0</h4>
|
||||
<small>System-wide</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card metrics-card text-white shadow">
|
||||
<div class="card-body text-center">
|
||||
<h6 class="card-title">Gini Coefficient</h6>
|
||||
<h4 id="giniMetric">0.00</h4>
|
||||
<small>Inequality measure</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card inequality-warning text-white shadow">
|
||||
<div class="card-body text-center">
|
||||
<h6 class="card-title">Top 10% Share</h6>
|
||||
<h4 id="top10Metric">0%</h4>
|
||||
<small>Wealth concentration</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card inequality-warning text-white shadow">
|
||||
<div class="card-body text-center">
|
||||
<h6 class="card-title">Capital Share</h6>
|
||||
<h4 id="capitalShareMetric">0%</h4>
|
||||
<small>vs Consumption</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Charts Row -->
|
||||
<div class="row">
|
||||
<!-- Wealth Evolution Chart -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">📈 Wealth Evolution Over Time</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="chart-container">
|
||||
<canvas id="wealthEvolutionChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Inequality Metrics Chart -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">📊 Inequality Metrics</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="chart-container">
|
||||
<canvas id="inequalityChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Wealth Distribution Chart -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card shadow">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">🏛️ Wealth Distribution Histogram</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="chart-container">
|
||||
<canvas id="distributionChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Export and Actions -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card shadow">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6>📥 Export Simulation Data</h6>
|
||||
<small class="text-muted">Download results for further analysis</small>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" class="btn btn-outline-primary me-2" id="exportJsonBtn" disabled>
|
||||
📄 JSON
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-success" id="exportCsvBtn" disabled>
|
||||
📊 CSV
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden fields for data storage -->
|
||||
<input type="hidden" id="currentSimulationId">
|
||||
<input type="hidden" id="defaultParams" value="{{ default_params | tojson }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script src="{{ url_for('static', filename='js/simulation.js') }}"></script>
|
||||
<script>
|
||||
// Initialize tooltips
|
||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
|
||||
// Initialize simulation interface
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initializeSimulation();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
38
config.py
Normal file
38
config.py
Normal file
@@ -0,0 +1,38 @@
|
||||
import os
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
class Config:
|
||||
"""Base configuration class"""
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-key-for-markov-economics')
|
||||
MAX_SIMULATION_AGENTS = int(os.environ.get('MAX_AGENTS', 10000))
|
||||
SIMULATION_TIMEOUT = int(os.environ.get('TIMEOUT', 300))
|
||||
|
||||
# Parameter constraints
|
||||
MIN_CAPITAL_RATE = 0.0
|
||||
MAX_CAPITAL_RATE = 0.25
|
||||
MIN_GROWTH_RATE = 0.0
|
||||
MAX_GROWTH_RATE = 0.20
|
||||
MIN_AGENTS = 10
|
||||
MAX_AGENTS = 10000
|
||||
MIN_ITERATIONS = 100
|
||||
MAX_ITERATIONS = 100000
|
||||
|
||||
|
||||
class DevelopmentConfig(Config):
|
||||
"""Development configuration"""
|
||||
DEBUG = True
|
||||
DEVELOPMENT = True
|
||||
|
||||
|
||||
class ProductionConfig(Config):
|
||||
"""Production configuration"""
|
||||
DEBUG = False
|
||||
DEVELOPMENT = False
|
||||
|
||||
|
||||
config = {
|
||||
'development': DevelopmentConfig,
|
||||
'production': ProductionConfig,
|
||||
'default': DevelopmentConfig
|
||||
}
|
5
requirements.txt
Normal file
5
requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
Flask>=2.0.0
|
||||
Flask-SocketIO>=5.0.0
|
||||
numpy>=1.21.0
|
||||
matplotlib>=3.5.0
|
||||
plotly>=5.0.0
|
14
run.py
Normal file
14
run.py
Normal file
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Main entry point for the Markov Economics Flask application.
|
||||
Demonstrates how capitalism "eats the world" using Markov chains.
|
||||
"""
|
||||
|
||||
import os
|
||||
from app import create_app, socketio
|
||||
|
||||
config_name = os.getenv('FLASK_CONFIG', 'development')
|
||||
app = create_app(config_name)
|
||||
|
||||
if __name__ == '__main__':
|
||||
socketio.run(app, debug=True, host='0.0.0.0', port=5000)
|
154
test_core.py
Normal file
154
test_core.py
Normal file
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for Markov Economics Application
|
||||
|
||||
Simple tests to verify the core functionality works correctly.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add the app directory to the path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'app'))
|
||||
|
||||
from models.markov_chain import CapitalistChain, ConsumerChain, EconomicAgent
|
||||
from models.economic_model import SimulationParameters, EconomicSimulation
|
||||
|
||||
def test_markov_chains():
|
||||
"""Test basic Markov chain functionality."""
|
||||
print("🧪 Testing Markov Chains...")
|
||||
|
||||
# Test CapitalistChain
|
||||
cap_chain = CapitalistChain(capital_rate=0.05) # 5% return
|
||||
print(f" Initial capitalist wealth: {cap_chain.get_current_wealth()}")
|
||||
|
||||
# Simulate a few steps
|
||||
for i in range(10):
|
||||
state = cap_chain.step()
|
||||
if i % 3 == 2: # Every complete cycle
|
||||
print(f" After cycle {(i+1)//3}: wealth = {cap_chain.get_current_wealth():.3f}")
|
||||
|
||||
# Test ConsumerChain
|
||||
cons_chain = ConsumerChain(growth_rate=0.03) # 3% growth
|
||||
print(f" Initial consumer value: {cons_chain.get_current_consumption()}")
|
||||
|
||||
for i in range(10):
|
||||
state = cons_chain.step()
|
||||
if i % 3 == 2: # Every complete cycle
|
||||
print(f" After cycle {(i+1)//3}: consumption = {cons_chain.get_current_consumption():.3f}")
|
||||
|
||||
print("✅ Markov chains working correctly")
|
||||
|
||||
def test_economic_agent():
|
||||
"""Test economic agent functionality."""
|
||||
print("\n🧪 Testing Economic Agent...")
|
||||
|
||||
agent = EconomicAgent(
|
||||
agent_id="test_agent",
|
||||
capital_rate=0.05,
|
||||
growth_rate=0.03,
|
||||
initial_capital=1000,
|
||||
initial_consumption=1000
|
||||
)
|
||||
|
||||
print(f" Initial total wealth: {agent.get_total_wealth()}")
|
||||
print(f" Initial wealth ratio: {agent.get_wealth_ratio():.3f}")
|
||||
|
||||
# Simulate several steps
|
||||
for i in range(30):
|
||||
wealth, consumption = agent.step()
|
||||
if i % 10 == 9:
|
||||
print(f" Step {i+1}: wealth={wealth:.2f}, consumption={consumption:.2f}, total={agent.get_total_wealth():.2f}")
|
||||
|
||||
print("✅ Economic agent working correctly")
|
||||
|
||||
def test_simulation():
|
||||
"""Test full simulation functionality."""
|
||||
print("\n🧪 Testing Economic Simulation...")
|
||||
|
||||
params = SimulationParameters(
|
||||
r_rate=0.05, # 5% capital return
|
||||
g_rate=0.03, # 3% economic growth
|
||||
num_agents=20,
|
||||
iterations=50,
|
||||
initial_capital=1000,
|
||||
initial_consumption=1000
|
||||
)
|
||||
|
||||
simulation = EconomicSimulation(params)
|
||||
print(f" Created simulation with {len(simulation.agents)} agents")
|
||||
|
||||
# Run simulation
|
||||
print(" Running simulation...")
|
||||
snapshots = simulation.run_simulation()
|
||||
|
||||
print(f" Completed {len(snapshots)} iterations")
|
||||
|
||||
# Analyze results
|
||||
if snapshots:
|
||||
final_snapshot = snapshots[-1]
|
||||
print(f" Final total wealth: ${final_snapshot.total_wealth:,.2f}")
|
||||
print(f" Final Gini coefficient: {final_snapshot.gini_coefficient:.3f}")
|
||||
print(f" Final top 10% share: {final_snapshot.wealth_concentration_top10:.1%}")
|
||||
print(f" Final capital share: {final_snapshot.capital_share:.1%}")
|
||||
|
||||
# Check if r > g caused inequality
|
||||
if params.r_rate > params.g_rate:
|
||||
print(f" ⚠️ r ({params.r_rate:.1%}) > g ({params.g_rate:.1%}) - inequality should increase")
|
||||
if final_snapshot.gini_coefficient > 0.1:
|
||||
print(" ✅ Inequality increased as expected")
|
||||
else:
|
||||
print(" ⚠️ Expected more inequality")
|
||||
|
||||
print("✅ Simulation working correctly")
|
||||
|
||||
def test_inequality_principle():
|
||||
"""Test Piketty's r > g inequality principle."""
|
||||
print("\n🧪 Testing Piketty's r > g Principle...")
|
||||
|
||||
# Scenario 1: r > g (should increase inequality)
|
||||
print(" Scenario 1: r > g")
|
||||
params1 = SimulationParameters(r_rate=0.07, g_rate=0.02, num_agents=50, iterations=100)
|
||||
sim1 = EconomicSimulation(params1)
|
||||
snapshots1 = sim1.run_simulation()
|
||||
|
||||
if snapshots1:
|
||||
initial_gini = snapshots1[0].gini_coefficient if snapshots1 else 0
|
||||
final_gini = snapshots1[-1].gini_coefficient
|
||||
print(f" Initial Gini: {initial_gini:.3f}, Final Gini: {final_gini:.3f}")
|
||||
print(f" Inequality change: {final_gini - initial_gini:+.3f}")
|
||||
|
||||
# Scenario 2: r ≈ g (should have moderate inequality)
|
||||
print(" Scenario 2: r ≈ g")
|
||||
params2 = SimulationParameters(r_rate=0.03, g_rate=0.03, num_agents=50, iterations=100)
|
||||
sim2 = EconomicSimulation(params2)
|
||||
snapshots2 = sim2.run_simulation()
|
||||
|
||||
if snapshots2:
|
||||
initial_gini = snapshots2[0].gini_coefficient if snapshots2 else 0
|
||||
final_gini = snapshots2[-1].gini_coefficient
|
||||
print(f" Initial Gini: {initial_gini:.3f}, Final Gini: {final_gini:.3f}")
|
||||
print(f" Inequality change: {final_gini - initial_gini:+.3f}")
|
||||
|
||||
print("✅ Inequality principle demonstrated")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🚀 Markov Economics - Core Functionality Test")
|
||||
print("=" * 50)
|
||||
|
||||
try:
|
||||
test_markov_chains()
|
||||
test_economic_agent()
|
||||
test_simulation()
|
||||
test_inequality_principle()
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("🎉 All tests passed! The application is working correctly.")
|
||||
print("\nThe web interface is available at: http://127.0.0.1:5000")
|
||||
print("The simulation demonstrates Marx's M-C-M' model and Piketty's r > g principle.")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ Test failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
6
test_simulation.json
Normal file
6
test_simulation.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"r_rate": 0.05,
|
||||
"g_rate": 0.03,
|
||||
"num_agents": 50,
|
||||
"iterations": 100
|
||||
}
|
Reference in New Issue
Block a user