Initial commit: Markov Economics Simulation App
This commit is contained in:
11
app/routes/__init__.py
Normal file
11
app/routes/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""
|
||||
Flask routes package.
|
||||
|
||||
Contains the main web interface routes and API endpoints for the
|
||||
Markov economics simulation application.
|
||||
"""
|
||||
|
||||
from .main import main_bp
|
||||
from .api import api_bp
|
||||
|
||||
__all__ = ['main_bp', 'api_bp']
|
BIN
app/routes/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
app/routes/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/routes/__pycache__/api.cpython-312.pyc
Normal file
BIN
app/routes/__pycache__/api.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/routes/__pycache__/main.cpython-312.pyc
Normal file
BIN
app/routes/__pycache__/main.cpython-312.pyc
Normal file
Binary file not shown.
383
app/routes/api.py
Normal file
383
app/routes/api.py
Normal file
@@ -0,0 +1,383 @@
|
||||
"""
|
||||
API Routes for Simulation Control
|
||||
|
||||
Provides REST API endpoints for controlling simulations, retrieving data,
|
||||
and managing simulation parameters with real-time updates via SocketIO.
|
||||
"""
|
||||
|
||||
from flask import Blueprint, request, jsonify, current_app
|
||||
from flask_socketio import emit, join_room, leave_room
|
||||
import threading
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from app import socketio
|
||||
from app.models import SimulationManager, SimulationParameters
|
||||
from app.routes.main import simulation_manager
|
||||
|
||||
api_bp = Blueprint('api', __name__)
|
||||
|
||||
|
||||
@api_bp.route('/simulation', methods=['POST'])
|
||||
def create_simulation():
|
||||
"""
|
||||
Create a new simulation with specified parameters.
|
||||
|
||||
Request JSON:
|
||||
{
|
||||
"r_rate": 0.05,
|
||||
"g_rate": 0.03,
|
||||
"initial_capital": 1000,
|
||||
"initial_consumption": 1000,
|
||||
"num_agents": 100,
|
||||
"iterations": 1000
|
||||
}
|
||||
|
||||
Returns:
|
||||
JSON response with simulation ID and status
|
||||
"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
|
||||
if not data:
|
||||
return jsonify({'error': 'No JSON data provided'}), 400
|
||||
|
||||
# Validate and create parameters
|
||||
params = SimulationParameters(
|
||||
r_rate=float(data.get('r_rate', 0.05)),
|
||||
g_rate=float(data.get('g_rate', 0.03)),
|
||||
initial_capital=float(data.get('initial_capital', 1000)),
|
||||
initial_consumption=float(data.get('initial_consumption', 1000)),
|
||||
num_agents=int(data.get('num_agents', 100)),
|
||||
iterations=int(data.get('iterations', 1000))
|
||||
)
|
||||
|
||||
# Create simulation
|
||||
simulation_id = simulation_manager.create_simulation(params)
|
||||
|
||||
return jsonify({
|
||||
'simulation_id': simulation_id,
|
||||
'status': 'created',
|
||||
'parameters': {
|
||||
'r_rate': params.r_rate,
|
||||
'g_rate': params.g_rate,
|
||||
'initial_capital': params.initial_capital,
|
||||
'initial_consumption': params.initial_consumption,
|
||||
'num_agents': params.num_agents,
|
||||
'iterations': params.iterations
|
||||
}
|
||||
}), 201
|
||||
|
||||
except ValueError as e:
|
||||
return jsonify({'error': f'Invalid parameter: {str(e)}'}), 400
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error creating simulation: {str(e)}")
|
||||
return jsonify({'error': 'Internal server error'}), 500
|
||||
|
||||
|
||||
@api_bp.route('/simulation/<simulation_id>/start', methods=['POST'])
|
||||
def start_simulation(simulation_id: str):
|
||||
"""
|
||||
Start running a simulation.
|
||||
|
||||
Args:
|
||||
simulation_id: Unique simulation identifier
|
||||
|
||||
Returns:
|
||||
JSON response indicating success or failure
|
||||
"""
|
||||
try:
|
||||
simulation = simulation_manager.get_simulation(simulation_id)
|
||||
|
||||
if not simulation:
|
||||
return jsonify({'error': 'Simulation not found'}), 404
|
||||
|
||||
if simulation.is_running:
|
||||
return jsonify({'error': 'Simulation already running'}), 400
|
||||
|
||||
# Start simulation in background thread
|
||||
def run_simulation_background():
|
||||
try:
|
||||
# Run simulation with progress updates
|
||||
total_iterations = simulation.parameters.iterations
|
||||
for i in range(total_iterations):
|
||||
if not simulation.is_running:
|
||||
break
|
||||
|
||||
snapshot = simulation.step()
|
||||
|
||||
# Emit progress update every 10 iterations or at milestones
|
||||
if i % 10 == 0 or i == total_iterations - 1:
|
||||
progress_data = {
|
||||
'simulation_id': simulation_id,
|
||||
'iteration': snapshot.iteration,
|
||||
'total_iterations': total_iterations,
|
||||
'progress_percentage': (snapshot.iteration / total_iterations) * 100,
|
||||
'total_wealth': snapshot.total_wealth,
|
||||
'gini_coefficient': snapshot.gini_coefficient,
|
||||
'capital_share': snapshot.capital_share,
|
||||
'wealth_concentration_top10': snapshot.wealth_concentration_top10
|
||||
}
|
||||
|
||||
socketio.emit('simulation_progress', progress_data,
|
||||
room=f'simulation_{simulation_id}')
|
||||
|
||||
# Small delay to allow real-time visualization
|
||||
time.sleep(0.01)
|
||||
|
||||
# Emit completion
|
||||
socketio.emit('simulation_complete', {
|
||||
'simulation_id': simulation_id,
|
||||
'total_snapshots': len(simulation.snapshots)
|
||||
}, room=f'simulation_{simulation_id}')
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error in simulation thread: {str(e)}")
|
||||
socketio.emit('simulation_error', {
|
||||
'simulation_id': simulation_id,
|
||||
'error': str(e)
|
||||
}, room=f'simulation_{simulation_id}')
|
||||
|
||||
# Start background thread
|
||||
thread = threading.Thread(target=run_simulation_background)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
return jsonify({
|
||||
'simulation_id': simulation_id,
|
||||
'status': 'started',
|
||||
'message': 'Simulation started successfully'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error starting simulation: {str(e)}")
|
||||
return jsonify({'error': 'Internal server error'}), 500
|
||||
|
||||
|
||||
@api_bp.route('/simulation/<simulation_id>/stop', methods=['POST'])
|
||||
def stop_simulation(simulation_id: str):
|
||||
"""
|
||||
Stop a running simulation.
|
||||
|
||||
Args:
|
||||
simulation_id: Unique simulation identifier
|
||||
|
||||
Returns:
|
||||
JSON response indicating success or failure
|
||||
"""
|
||||
try:
|
||||
success = simulation_manager.stop_simulation(simulation_id)
|
||||
|
||||
if not success:
|
||||
return jsonify({'error': 'Simulation not found or not running'}), 404
|
||||
|
||||
# Emit stop event
|
||||
socketio.emit('simulation_stopped', {
|
||||
'simulation_id': simulation_id
|
||||
}, room=f'simulation_{simulation_id}')
|
||||
|
||||
return jsonify({
|
||||
'simulation_id': simulation_id,
|
||||
'status': 'stopped',
|
||||
'message': 'Simulation stopped successfully'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error stopping simulation: {str(e)}")
|
||||
return jsonify({'error': 'Internal server error'}), 500
|
||||
|
||||
|
||||
@api_bp.route('/simulation/<simulation_id>/data', methods=['GET'])
|
||||
def get_simulation_data(simulation_id: str):
|
||||
"""
|
||||
Get current simulation data and snapshots.
|
||||
|
||||
Args:
|
||||
simulation_id: Unique simulation identifier
|
||||
|
||||
Returns:
|
||||
JSON response with simulation data
|
||||
"""
|
||||
try:
|
||||
simulation = simulation_manager.get_simulation(simulation_id)
|
||||
|
||||
if not simulation:
|
||||
return jsonify({'error': 'Simulation not found'}), 404
|
||||
|
||||
# Get specific iteration if requested
|
||||
iteration = request.args.get('iteration', type=int)
|
||||
if iteration is not None:
|
||||
snapshot = simulation.get_snapshot_at_iteration(iteration)
|
||||
if not snapshot:
|
||||
return jsonify({'error': f'No data for iteration {iteration}'}), 404
|
||||
|
||||
return jsonify({
|
||||
'simulation_id': simulation_id,
|
||||
'snapshot': {
|
||||
'iteration': snapshot.iteration,
|
||||
'total_wealth': snapshot.total_wealth,
|
||||
'gini_coefficient': snapshot.gini_coefficient,
|
||||
'wealth_concentration_top10': snapshot.wealth_concentration_top10,
|
||||
'capital_share': snapshot.capital_share,
|
||||
'average_wealth': snapshot.average_wealth,
|
||||
'median_wealth': snapshot.median_wealth
|
||||
}
|
||||
})
|
||||
|
||||
# Get latest snapshot and evolution data
|
||||
latest_snapshot = simulation.get_latest_snapshot()
|
||||
iterations, total_wealth, gini_coefficients = simulation.get_wealth_evolution()
|
||||
|
||||
response_data = {
|
||||
'simulation_id': simulation_id,
|
||||
'is_running': simulation.is_running,
|
||||
'current_iteration': simulation.current_iteration,
|
||||
'total_snapshots': len(simulation.snapshots),
|
||||
'parameters': {
|
||||
'r_rate': simulation.parameters.r_rate,
|
||||
'g_rate': simulation.parameters.g_rate,
|
||||
'num_agents': simulation.parameters.num_agents,
|
||||
'iterations': simulation.parameters.iterations
|
||||
}
|
||||
}
|
||||
|
||||
if latest_snapshot:
|
||||
response_data['latest_snapshot'] = {
|
||||
'iteration': latest_snapshot.iteration,
|
||||
'total_wealth': latest_snapshot.total_wealth,
|
||||
'gini_coefficient': latest_snapshot.gini_coefficient,
|
||||
'wealth_concentration_top10': latest_snapshot.wealth_concentration_top10,
|
||||
'capital_share': latest_snapshot.capital_share,
|
||||
'average_wealth': latest_snapshot.average_wealth,
|
||||
'median_wealth': latest_snapshot.median_wealth
|
||||
}
|
||||
|
||||
# Include evolution data if requested
|
||||
if request.args.get('include_evolution', '').lower() == 'true':
|
||||
response_data['evolution'] = {
|
||||
'iterations': iterations,
|
||||
'total_wealth': total_wealth,
|
||||
'gini_coefficients': gini_coefficients
|
||||
}
|
||||
|
||||
return jsonify(response_data)
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error getting simulation data: {str(e)}")
|
||||
return jsonify({'error': 'Internal server error'}), 500
|
||||
|
||||
|
||||
@api_bp.route('/simulation/<simulation_id>/export/<format_type>', methods=['GET'])
|
||||
def export_simulation_data(simulation_id: str, format_type: str):
|
||||
"""
|
||||
Export simulation data in specified format.
|
||||
|
||||
Args:
|
||||
simulation_id: Unique simulation identifier
|
||||
format_type: Export format ('json' or 'csv')
|
||||
|
||||
Returns:
|
||||
Data in requested format
|
||||
"""
|
||||
try:
|
||||
simulation = simulation_manager.get_simulation(simulation_id)
|
||||
|
||||
if not simulation:
|
||||
return jsonify({'error': 'Simulation not found'}), 404
|
||||
|
||||
if format_type.lower() not in ['json', 'csv']:
|
||||
return jsonify({'error': 'Format must be json or csv'}), 400
|
||||
|
||||
exported_data = simulation.export_data(format_type)
|
||||
|
||||
if format_type.lower() == 'json':
|
||||
return jsonify({'data': exported_data})
|
||||
else: # CSV
|
||||
return exported_data, 200, {
|
||||
'Content-Type': 'text/csv',
|
||||
'Content-Disposition': f'attachment; filename=simulation_{simulation_id[:8]}.csv'
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error exporting simulation data: {str(e)}")
|
||||
return jsonify({'error': 'Internal server error'}), 500
|
||||
|
||||
|
||||
@api_bp.route('/simulation/<simulation_id>', methods=['DELETE'])
|
||||
def delete_simulation(simulation_id: str):
|
||||
"""
|
||||
Delete a simulation.
|
||||
|
||||
Args:
|
||||
simulation_id: Unique simulation identifier
|
||||
|
||||
Returns:
|
||||
JSON response indicating success or failure
|
||||
"""
|
||||
try:
|
||||
success = simulation_manager.delete_simulation(simulation_id)
|
||||
|
||||
if not success:
|
||||
return jsonify({'error': 'Simulation not found'}), 404
|
||||
|
||||
return jsonify({
|
||||
'simulation_id': simulation_id,
|
||||
'status': 'deleted',
|
||||
'message': 'Simulation deleted successfully'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error deleting simulation: {str(e)}")
|
||||
return jsonify({'error': 'Internal server error'}), 500
|
||||
|
||||
|
||||
@api_bp.route('/simulations', methods=['GET'])
|
||||
def list_simulations():
|
||||
"""
|
||||
List all simulations with their status.
|
||||
|
||||
Returns:
|
||||
JSON response with list of simulation information
|
||||
"""
|
||||
try:
|
||||
simulations_info = simulation_manager.list_simulations()
|
||||
|
||||
return jsonify({
|
||||
'simulations': simulations_info,
|
||||
'total_count': len(simulations_info)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error listing simulations: {str(e)}")
|
||||
return jsonify({'error': 'Internal server error'}), 500
|
||||
|
||||
|
||||
# SocketIO event handlers
|
||||
@socketio.on('join_simulation')
|
||||
def on_join_simulation(data):
|
||||
"""Handle client joining simulation room for real-time updates."""
|
||||
simulation_id = data.get('simulation_id')
|
||||
if simulation_id:
|
||||
join_room(f'simulation_{simulation_id}')
|
||||
emit('joined_simulation', {'simulation_id': simulation_id})
|
||||
|
||||
|
||||
@socketio.on('leave_simulation')
|
||||
def on_leave_simulation(data):
|
||||
"""Handle client leaving simulation room."""
|
||||
simulation_id = data.get('simulation_id')
|
||||
if simulation_id:
|
||||
leave_room(f'simulation_{simulation_id}')
|
||||
emit('left_simulation', {'simulation_id': simulation_id})
|
||||
|
||||
|
||||
@socketio.on('connect')
|
||||
def on_connect():
|
||||
"""Handle client connection."""
|
||||
emit('connected', {'status': 'connected'})
|
||||
|
||||
|
||||
@socketio.on('disconnect')
|
||||
def on_disconnect():
|
||||
"""Handle client disconnection."""
|
||||
print('Client disconnected')
|
149
app/routes/main.py
Normal file
149
app/routes/main.py
Normal file
@@ -0,0 +1,149 @@
|
||||
"""
|
||||
Main Flask Routes
|
||||
|
||||
Handles the primary web interface for the Markov economics simulation,
|
||||
including the main simulation page and basic navigation.
|
||||
"""
|
||||
|
||||
from flask import Blueprint, render_template, request, jsonify
|
||||
from app.models import SimulationManager, SimulationParameters
|
||||
|
||||
main_bp = Blueprint('main', __name__)
|
||||
|
||||
# Global simulation manager instance
|
||||
simulation_manager = SimulationManager()
|
||||
|
||||
|
||||
@main_bp.route('/')
|
||||
def index():
|
||||
"""
|
||||
Main simulation interface page.
|
||||
|
||||
Returns:
|
||||
Rendered HTML template with simulation controls
|
||||
"""
|
||||
# Default parameters for display
|
||||
default_params = {
|
||||
'r_rate': 0.05, # 5% capital return rate
|
||||
'g_rate': 0.03, # 3% economic growth rate
|
||||
'initial_capital': 1000.0,
|
||||
'initial_consumption': 1000.0,
|
||||
'num_agents': 100,
|
||||
'iterations': 1000
|
||||
}
|
||||
|
||||
return render_template('simulation.html',
|
||||
default_params=default_params,
|
||||
title="Markov Economics - Capitalism Eats the World")
|
||||
|
||||
|
||||
@main_bp.route('/simulation')
|
||||
def simulation():
|
||||
"""
|
||||
Alternative route to the main simulation page.
|
||||
|
||||
Returns:
|
||||
Rendered HTML template with simulation controls
|
||||
"""
|
||||
return index()
|
||||
|
||||
|
||||
@main_bp.route('/about')
|
||||
def about():
|
||||
"""
|
||||
Information page about the economic theory behind the simulation.
|
||||
|
||||
Returns:
|
||||
Rendered HTML template with theoretical background
|
||||
"""
|
||||
return render_template('about.html',
|
||||
title="About - Markov Economics Theory")
|
||||
|
||||
|
||||
@main_bp.route('/results/<simulation_id>')
|
||||
def results(simulation_id):
|
||||
"""
|
||||
Display results for a specific simulation.
|
||||
|
||||
Args:
|
||||
simulation_id: Unique identifier for the simulation
|
||||
|
||||
Returns:
|
||||
Rendered results template or 404 if simulation not found
|
||||
"""
|
||||
simulation = simulation_manager.get_simulation(simulation_id)
|
||||
|
||||
if not simulation:
|
||||
return render_template('error.html',
|
||||
error_message=f"Simulation {simulation_id} not found",
|
||||
title="Simulation Not Found"), 404
|
||||
|
||||
# Get simulation summary data
|
||||
latest_snapshot = simulation.get_latest_snapshot()
|
||||
iterations, total_wealth, gini_coefficients = simulation.get_wealth_evolution()
|
||||
|
||||
summary_data = {
|
||||
'simulation_id': simulation_id,
|
||||
'parameters': {
|
||||
'r_rate': simulation.parameters.r_rate,
|
||||
'g_rate': simulation.parameters.g_rate,
|
||||
'num_agents': simulation.parameters.num_agents,
|
||||
'iterations': simulation.parameters.iterations
|
||||
},
|
||||
'current_iteration': simulation.current_iteration,
|
||||
'total_iterations': len(simulation.snapshots),
|
||||
'latest_snapshot': latest_snapshot,
|
||||
'has_data': len(simulation.snapshots) > 0
|
||||
}
|
||||
|
||||
return render_template('results.html',
|
||||
simulation_data=summary_data,
|
||||
title=f"Results - Simulation {simulation_id[:8]}")
|
||||
|
||||
|
||||
@main_bp.route('/health')
|
||||
def health_check():
|
||||
"""
|
||||
Simple health check endpoint.
|
||||
|
||||
Returns:
|
||||
JSON response indicating service status
|
||||
"""
|
||||
return jsonify({
|
||||
'status': 'healthy',
|
||||
'service': 'markov-economics',
|
||||
'active_simulations': len(simulation_manager.active_simulations),
|
||||
'total_simulations': len(simulation_manager.simulations)
|
||||
})
|
||||
|
||||
|
||||
@main_bp.errorhandler(404)
|
||||
def not_found_error(error):
|
||||
"""
|
||||
Handle 404 errors with custom template.
|
||||
|
||||
Args:
|
||||
error: The error object
|
||||
|
||||
Returns:
|
||||
Rendered error template
|
||||
"""
|
||||
return render_template('error.html',
|
||||
error_message="Page not found",
|
||||
title="Page Not Found"), 404
|
||||
|
||||
|
||||
@main_bp.errorhandler(500)
|
||||
def internal_error(error):
|
||||
"""
|
||||
Handle 500 errors with custom template.
|
||||
|
||||
Args:
|
||||
error: The error object
|
||||
|
||||
Returns:
|
||||
Rendered error template
|
||||
"""
|
||||
return render_template('error.html',
|
||||
error_message="Internal server error occurred",
|
||||
title="Server Error"), 500
|
Reference in New Issue
Block a user