commit 26c82959a2df93d511c78db20955f1be5bb707e9 Author: elpatron Date: Sun Aug 24 19:12:50 2025 +0200 Initial commit: Markov Economics Simulation App diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7f4e44b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.venv/ +.qoder/ diff --git a/__pycache__/config.cpython-312.pyc b/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000..0dca5e1 Binary files /dev/null and b/__pycache__/config.cpython-312.pyc differ diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..73027f1 --- /dev/null +++ b/app/__init__.py @@ -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 \ No newline at end of file diff --git a/app/__pycache__/__init__.cpython-312.pyc b/app/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..89d5ebd Binary files /dev/null and b/app/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..30627c8 --- /dev/null +++ b/app/models/__init__.py @@ -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' +] \ No newline at end of file diff --git a/app/models/__pycache__/__init__.cpython-312.pyc b/app/models/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..868c6e6 Binary files /dev/null and b/app/models/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/models/__pycache__/economic_model.cpython-312.pyc b/app/models/__pycache__/economic_model.cpython-312.pyc new file mode 100644 index 0000000..a257bec Binary files /dev/null and b/app/models/__pycache__/economic_model.cpython-312.pyc differ diff --git a/app/models/__pycache__/markov_chain.cpython-312.pyc b/app/models/__pycache__/markov_chain.cpython-312.pyc new file mode 100644 index 0000000..3f54d52 Binary files /dev/null and b/app/models/__pycache__/markov_chain.cpython-312.pyc differ diff --git a/app/models/economic_model.py b/app/models/economic_model.py new file mode 100644 index 0000000..ad79339 --- /dev/null +++ b/app/models/economic_model.py @@ -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 \ No newline at end of file diff --git a/app/models/markov_chain.py b/app/models/markov_chain.py new file mode 100644 index 0000000..f18e8a4 --- /dev/null +++ b/app/models/markov_chain.py @@ -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 \ No newline at end of file diff --git a/app/routes/__init__.py b/app/routes/__init__.py new file mode 100644 index 0000000..64b221c --- /dev/null +++ b/app/routes/__init__.py @@ -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'] \ No newline at end of file diff --git a/app/routes/__pycache__/__init__.cpython-312.pyc b/app/routes/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..bea237c Binary files /dev/null and b/app/routes/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/routes/__pycache__/api.cpython-312.pyc b/app/routes/__pycache__/api.cpython-312.pyc new file mode 100644 index 0000000..81acbd3 Binary files /dev/null and b/app/routes/__pycache__/api.cpython-312.pyc differ diff --git a/app/routes/__pycache__/main.cpython-312.pyc b/app/routes/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000..3046a8b Binary files /dev/null and b/app/routes/__pycache__/main.cpython-312.pyc differ diff --git a/app/routes/api.py b/app/routes/api.py new file mode 100644 index 0000000..7e5b270 --- /dev/null +++ b/app/routes/api.py @@ -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//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//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//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//export/', 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/', 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') \ No newline at end of file diff --git a/app/routes/main.py b/app/routes/main.py new file mode 100644 index 0000000..2fb435e --- /dev/null +++ b/app/routes/main.py @@ -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/') +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 \ No newline at end of file diff --git a/app/static/css/style.css b/app/static/css/style.css new file mode 100644 index 0000000..21112b5 --- /dev/null +++ b/app/static/css/style.css @@ -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; + } +} \ No newline at end of file diff --git a/app/static/js/app.js b/app/static/js/app.js new file mode 100644 index 0000000..6977dd5 --- /dev/null +++ b/app/static/js/app.js @@ -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} + + `; + + 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 = ` +
+
+ ${text} +
+ `; + } +} + +/** + * 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 +}; \ No newline at end of file diff --git a/app/static/js/simulation.js b/app/static/js/simulation.js new file mode 100644 index 0000000..bbed8b1 --- /dev/null +++ b/app/static/js/simulation.js @@ -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 = ` + ⚠️ r > g Detected!
+ Capital rate (${(capitalRate * 100).toFixed(1)}%) > Growth rate (${(growthRate * 100).toFixed(1)}%)
+ Wealth inequality will increase over time + `; + } else if (capitalRate === growthRate) { + warning.style.display = 'block'; + warning.className = 'alert alert-warning'; + warning.innerHTML = ` + ⚖️ r = g
+ Balanced scenario - moderate inequality growth expected + `; + } else { + warning.style.display = 'block'; + warning.className = 'alert alert-success'; + warning.innerHTML = ` + ✅ r < g
+ Growth rate exceeds capital rate - inequality may decrease + `; + } + } +} + +/** + * 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:
' + errors.join('
'), + '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; \ No newline at end of file diff --git a/app/templates/about.html b/app/templates/about.html new file mode 100644 index 0000000..a93b893 --- /dev/null +++ b/app/templates/about.html @@ -0,0 +1,286 @@ +{% extends "base.html" %} + +{% block content %} +
+
+ +
+

🎯 About Markov Economics

+

Understanding how capitalism "eats the world" through Markov chain analysis

+
+ + +
+
+

📚 Theoretical Foundation

+
+
+
+
+
Marx's Economic Cycles
+

Karl Marx identified two fundamental economic circulation patterns:

+ +
+
M-C-M' (Capitalist Circuit)
+

Money → Commodities → More Money

+ + Capital is invested in production to generate surplus value. + The goal is accumulation - turning M into M' where M' > M. + +
+ +
+
C-M-C (Consumer Circuit)
+

Commodities → Money → Commodities

+ + Goods are sold to acquire money to purchase other goods. + The goal is consumption and use-value satisfaction. + +
+
+ +
+
Piketty's Inequality Principle
+

Thomas Piketty demonstrated that wealth inequality increases when:

+ +
+
r > g
+

Capital Return Rate > Economic Growth Rate

+ + When capital generates returns faster than the overall economy grows, + wealth concentrates among capital owners, leading to increasing inequality. + +
+ +
Key Parameters:
+
    +
  • r: Rate of return on capital (stocks, real estate, business)
  • +
  • g: Economic growth rate (GDP growth)
  • +
+
+
+
+
+ + +
+
+

⚙️ Markov Chain Implementation

+
+
+

This simulation models economic behavior as Markov chains with state-dependent transition probabilities:

+ +
+
+
Capitalist Chain States
+
+
+ Money (M) +
Initial capital seeking investment opportunities +
+
+ Commodities (C) +
Capital invested in production/goods +
+
+ Enhanced Money (M') +
Capital with accumulated returns +
+
+ +
+
Transition Probability Matrix:
+
+M → C: r (capital rate)
+C → M': 1 (always)
+M' → M: 1 (reinvestment)
+                            
+
+
+ +
+
Consumer Chain States
+
+
+ Commodities (C) +
Goods available for consumption +
+
+ Money (M) +
Liquid currency from sales +
+
+ New Commodities (C') +
Purchased goods for consumption +
+
+ +
+
Transition Probability Matrix:
+
+C → M: g (growth rate)
+M → C': 1 (always)
+C' → C: 1 (consumption cycle)
+                            
+
+
+
+
+
+ + +
+
+

🔬 Simulation Mechanics

+
+
+
+
+
How the Simulation Works
+
    +
  1. Agent Initialization: Each economic agent starts with equal capital and consumption capacity
  2. +
  3. Dual Chain Operation: Every agent runs both capitalist (M-C-M') and consumer (C-M-C) chains simultaneously
  4. +
  5. Wealth Accumulation: Capitalist chains multiply wealth by (1 + r) on each complete cycle
  6. +
  7. Consumption Growth: Consumer chains grow by (1 + g) on each complete cycle
  8. +
  9. Inequality Emergence: When r > g, capital wealth grows faster than consumption, concentrating among fewer agents
  10. +
+
+ +
+
Key Metrics
+
+
+ Gini Coefficient +
Measures inequality (0 = perfect equality, 1 = perfect inequality) +
+
+ Top 10% Share +
Percentage of total wealth held by richest 10% +
+
+ Capital Share +
Proportion of wealth from capital vs consumption +
+
+
+
+
+
+ + +
+
+

🧪 Experimental Parameters

+
+
+
+
+
Try These Scenarios
+ +
+
Scenario 1: Stable Economy (r ≈ g)
+
    +
  • Capital Rate: 3%
  • +
  • Growth Rate: 3%
  • +
  • Expected: Moderate inequality growth
  • +
+
+ +
+
Scenario 2: Modern Capitalism (r > g)
+
    +
  • Capital Rate: 5%
  • +
  • Growth Rate: 2%
  • +
  • Expected: Increasing inequality
  • +
+
+ +
+
Scenario 3: Extreme Inequality (r >> g)
+
    +
  • Capital Rate: 8%
  • +
  • Growth Rate: 1%
  • +
  • Expected: Rapid wealth concentration
  • +
+
+
+ +
+
Historical Context
+

Real-world examples of r vs g:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Periodr (Capital)g (Growth)Inequality
Gilded Age (1870-1914)4-5%1-1.5%Very High
Post-War Era (1950-1980)2-3%3-4%Decreasing
Modern Era (1980-2020)4-6%1-2%Increasing
+
+
+
+
+ + +
+
+

⚡ Technical Implementation

+
+
+
+
+
Technology Stack
+
    +
  • Backend: Python Flask with SocketIO
  • +
  • Modeling: NumPy for matrix operations
  • +
  • Visualization: Chart.js for real-time charts
  • +
  • Frontend: Bootstrap 5 with responsive design
  • +
  • Real-time: WebSocket connections for live updates
  • +
+
+ +
+
Performance Characteristics
+
    +
  • Scalability: Up to 10,000 agents
  • +
  • Speed: Real-time visualization of state transitions
  • +
  • Accuracy: Precise Markov chain calculations
  • +
  • Export: JSON and CSV data export
  • +
+
+
+
+
+ + + +
+
+{% endblock %} \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 0000000..d770c77 --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,90 @@ + + + + + + {% block title %}{{ title }}{% endblock %} + + + + + + + + + + + {% block extra_head %}{% endblock %} + + + + + + +
+ {% block content %}{% endblock %} +
+ + +
+
+
+
+
Markov Economics Simulation
+

+ + Demonstrating Marx's M-C-M' model using Markov chains and Piketty's inequality principle (r > g). + Built with Flask, SocketIO, and Chart.js. + +

+
+
+

+ + M-C-M': Money → Commodities → More Money
+ C-M-C: Commodities → Money → Commodities +
+

+
+
+
+
+ + + + + + + + {% block extra_scripts %}{% endblock %} + + \ No newline at end of file diff --git a/app/templates/error.html b/app/templates/error.html new file mode 100644 index 0000000..a1875e0 --- /dev/null +++ b/app/templates/error.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+
+
+
+ +
+ +

Oops!

+

{{ error_message }}

+ +
+ + 🏠 Go Home + + +
+ +
+ + If this problem persists, the simulation may have encountered an unexpected error. + +
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/app/templates/simulation.html b/app/templates/simulation.html new file mode 100644 index 0000000..a6c070e --- /dev/null +++ b/app/templates/simulation.html @@ -0,0 +1,295 @@ +{% extends "base.html" %} + +{% block extra_head %} + +{% endblock %} + +{% block content %} +
+ +
+
+
+
📊 Simulation Parameters
+
+
+ +
+ +
+ + {{ "%.1f"|format(default_params.r_rate * 100) }}% +
+ Higher values increase wealth accumulation +
+ + +
+ +
+ + {{ "%.1f"|format(default_params.g_rate * 100) }}% +
+ Affects consumption growth +
+ + +
+ +
+ + {{ default_params.num_agents }} +
+ Number of economic agents +
+ + +
+ +
+ + {{ default_params.iterations }} +
+ Simulation duration +
+ + + + + +
+ + + +
+ + +
+
+ Ready to start +
+
+ + + +
+
+ + +
+
+
💡 Theory
+
+
+ + M-C-M': Capitalist cycle where money (M) is invested in commodities (C) to generate more money (M').

+ + C-M-C: Consumer cycle where commodities (C) are exchanged for money (M) to buy other commodities (C).

+ + When r > g: Capital returns exceed economic growth, leading to increasing wealth inequality. +
+
+
+
+ + +
+ +
+
+
+
+
Total Wealth
+

$0

+ System-wide +
+
+
+
+
+
+
Gini Coefficient
+

0.00

+ Inequality measure +
+
+
+
+
+
+
Top 10% Share
+

0%

+ Wealth concentration +
+
+
+
+
+
+
Capital Share
+

0%

+ vs Consumption +
+
+
+
+ + +
+ +
+
+
+
📈 Wealth Evolution Over Time
+
+
+
+ +
+
+
+
+ + +
+
+
+
📊 Inequality Metrics
+
+
+
+ +
+
+
+
+
+ + +
+
+
+
+
🏛️ Wealth Distribution Histogram
+
+
+
+ +
+
+
+
+
+ + +
+
+
+
+
+
+
📥 Export Simulation Data
+ Download results for further analysis +
+
+ + +
+
+
+
+
+
+
+
+ + + + +{% endblock %} + +{% block extra_scripts %} + + +{% endblock %} \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..45428de --- /dev/null +++ b/config.py @@ -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 +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..da899ef --- /dev/null +++ b/requirements.txt @@ -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 \ No newline at end of file diff --git a/run.py b/run.py new file mode 100644 index 0000000..9ee2672 --- /dev/null +++ b/run.py @@ -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) \ No newline at end of file diff --git a/test_core.py b/test_core.py new file mode 100644 index 0000000..c533a04 --- /dev/null +++ b/test_core.py @@ -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) \ No newline at end of file diff --git a/test_simulation.json b/test_simulation.json new file mode 100644 index 0000000..9a8e84e --- /dev/null +++ b/test_simulation.json @@ -0,0 +1,6 @@ +{ + "r_rate": 0.05, + "g_rate": 0.03, + "num_agents": 50, + "iterations": 100 +} \ No newline at end of file