From 26c82959a2df93d511c78db20955f1be5bb707e9 Mon Sep 17 00:00:00 2001 From: elpatron Date: Sun, 24 Aug 2025 19:12:50 +0200 Subject: [PATCH] Initial commit: Markov Economics Simulation App --- .gitignore | 2 + __pycache__/config.cpython-312.pyc | Bin 0 -> 1592 bytes app/__init__.py | 39 + app/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 1373 bytes app/models/__init__.py | 25 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 725 bytes .../economic_model.cpython-312.pyc | Bin 0 -> 20498 bytes .../__pycache__/markov_chain.cpython-312.pyc | Bin 0 -> 13869 bytes app/models/economic_model.py | 482 ++++++++++++ app/models/markov_chain.py | 339 ++++++++ app/routes/__init__.py | 11 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 436 bytes app/routes/__pycache__/api.cpython-312.pyc | Bin 0 -> 15294 bytes app/routes/__pycache__/main.cpython-312.pyc | Bin 0 -> 5083 bytes app/routes/api.py | 383 +++++++++ app/routes/main.py | 149 ++++ app/static/css/style.css | 269 +++++++ app/static/js/app.js | 281 +++++++ app/static/js/simulation.js | 743 ++++++++++++++++++ app/templates/about.html | 286 +++++++ app/templates/base.html | 90 +++ app/templates/error.html | 35 + app/templates/simulation.html | 295 +++++++ config.py | 38 + requirements.txt | 5 + run.py | 14 + test_core.py | 154 ++++ test_simulation.json | 6 + 28 files changed, 3646 insertions(+) create mode 100644 .gitignore create mode 100644 __pycache__/config.cpython-312.pyc create mode 100644 app/__init__.py create mode 100644 app/__pycache__/__init__.cpython-312.pyc create mode 100644 app/models/__init__.py create mode 100644 app/models/__pycache__/__init__.cpython-312.pyc create mode 100644 app/models/__pycache__/economic_model.cpython-312.pyc create mode 100644 app/models/__pycache__/markov_chain.cpython-312.pyc create mode 100644 app/models/economic_model.py create mode 100644 app/models/markov_chain.py create mode 100644 app/routes/__init__.py create mode 100644 app/routes/__pycache__/__init__.cpython-312.pyc create mode 100644 app/routes/__pycache__/api.cpython-312.pyc create mode 100644 app/routes/__pycache__/main.cpython-312.pyc create mode 100644 app/routes/api.py create mode 100644 app/routes/main.py create mode 100644 app/static/css/style.css create mode 100644 app/static/js/app.js create mode 100644 app/static/js/simulation.js create mode 100644 app/templates/about.html create mode 100644 app/templates/base.html create mode 100644 app/templates/error.html create mode 100644 app/templates/simulation.html create mode 100644 config.py create mode 100644 requirements.txt create mode 100644 run.py create mode 100644 test_core.py create mode 100644 test_simulation.json 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 0000000000000000000000000000000000000000..0dca5e1c3ab8482af9dc26e55e246722e1f618cc GIT binary patch literal 1592 zcmbVM&u`l{6sCSyvgO}Nkl8+v?IRO5mgGmdFoij+BeEvJ7CsDuCP*0Qn=#kw7at%{=94u|g zrA=AU?#W;8W+Cbp9;04i+p-Hy-F|F!3y3VTnnr^#E5)z0;;LL#wv5C_0ANumSLF4Y zVthglwllf6u`^iS`RRbR-Wnuhw&m0tdfRYy({P#vq`OYReTWL*S+)u5 z{*&Eo_iS2SBSRf$mzXEum|yaev-=BA7xovQE*?e(H`j*AO8@S0_Jc$ArSw928G8|X z_5H84VXidHt_=9haVGn0^SOGUK7VlV;8pn-XL#+?VdnOLPn`-NeRaTJ@#aT1mFcg0 zsRd$^v;DhXGDXZ>x?lC?uI^RsYZEb3)i%y=aF!<1)71u9l_y`I$K`pPTtQuATJ0uk zx#I-QT!N@eLgLJh4?J4Or+tjKq2t2n$O-J*9liIuIb1ChA9VD?~liY34avGl_{T7)%#^+W0I@#u3g*PZh4OM=j zaLJ1#slxe*LdwW~P>E=b7o)QP4?jrh2wCNAk&z&6gXdo>3 zS@n(<9K7vU;%D~9ghe0IStjO`V;JTS5c(4={w>6q>>EJL`4Y#>KNHRY>CPh@lm4+w Z{q$)JT)E-REP3(V8`kv#V9&N4F_ z$3_Mh0y*^3Lm@5nkV_%OzV+6>&`S)0lm$ber`#Is^w3k^j3nC)=>xs}-t2qdd;8uS z{XRPzB9LEiA8h>WA@rv;rUJH;lT~;;L=1Hi!>pF#tk$hzi@ns#NS8QFQa|&%K1Maf z3G)x@2Y4Lbt+N@{xR1MyouC!&{ur*NvEGVq6-6qN*a(?Nt8rqa+6luI#bd*D6z42T zWZoBpQh{kUcr=cRFGOt%vk~2q$rd-CeHrPpD5Nq`U#bXL(zX#956gl%LfLmDpT}xT z-iH~B{s4^mL@G_=G?m+&QK`g0|g21XJ(L<`kVj zhnV5R+QB3@%*$gWt!IrrJ4cWCX^_qAc^E+@3+GW&G_0NH`5zCD)ejrz{6l;PwSwK> z!U$wM)c?-(^&{;#f2VG#fnIjVQjD8V^El(n(aK~o#=BD}*%v8~jEqXn&mtW7iknj9 z7kH+joV{kBV1_2nlQBm%V>V!on8*h(@G#apc|tSE_;Wi-IBI0E$mx1vcOxzYpk1;A zzb`7e4y5cESzBQYDB{M45H+f>JK)W3lvI;~l422!9no5>%5jr-B zx@1Gb$qA%Tv-aRE#RozguIPGN@?>2 z15o?ubRIP?e;<6iw7>EPX*^i_X6-3iJR*xf-F)=n(Yu`ySvsvFfA%R^I3f$*C6BhA zkcAQX=p`J!e@xzcMjB)PA3h$DYrrtEZa(fmA&Vn&<5_cVf9(WPp)a7}O~&u?R2CVB z9t2f`QYI5hE5C35YQ__obC^zht!y*gFw~nC+4_G$YSH2=psL^#n0A@+PgMk_9lE{_ p>Msvt{G#S#@^TL0E63>CF}nJqjxNnVetXnx?+2&#ckmmI=U=F?X9)lR literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..868c6e60ef6c804f7b900e7c6ff671ce2146c06f GIT binary patch literal 725 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⁡7ksESLB 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3f54d522683a6fa951a8ca25ca1fc2233f4f8943 GIT binary patch literal 13869 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 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 0000000000000000000000000000000000000000..bea237cf129110f5889f64503926cbf38fbf12d0 GIT binary patch literal 436 zcmXw$F-rq67>1L(Yb&R95(E*?1Qi#TBM{OgZ9{XHBe}EaroX_=-yr@J z7w1DkaB@?si<93~`wd^7@V;O2l03HCF@pL!co;v!`c`ii75u|&8{iH}lpu*^U~n2F z0j@bTVHzb7a3mX}AZc7SJFTmm_|R}aATxXsXV?|ORGF1bj zvqHHc7iuxUGfDQ(j)=-+Zb9{AXk9%MpKv#^7eoo0*;ETpyiQBQ3vDyP^W11L=U)7d zA#w*-t@;)=$N`%?*FY`=#YNCgJI$)an6!eiDyofhtqQ?7H`FROhOv7z13&sX?(fiZ zubijpp!9-zTRNerQ@Qn2z*xbJ_Gwq~!gnEWcWRxH6q3=ux1fyaOc#vx^2=&zH?^`f pYR|2};dNlY3GE}q7=H#09DT1Myzzq8UzgWz$4~Lrb<5SC^$Y0`f(HNq literal 0 HcmV?d00001 diff --git a/app/routes/__pycache__/api.cpython-312.pyc b/app/routes/__pycache__/api.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..81acbd368d741d16600d93c8fc74446964c01b94 GIT binary patch literal 15294 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 literal 0 HcmV?d00001 diff --git a/app/routes/__pycache__/main.cpython-312.pyc b/app/routes/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3046a8b08113882977280e00f1c1f5414c150623 GIT binary patch literal 5083 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 literal 0 HcmV?d00001 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