- Fix circular import issue by moving simulation_manager to app/__init__.py - Enhance get_wealth_evolution to include inequality metrics data - Add get_inequality_evolution method for complete chart data - Update API to return top10_shares and capital_shares in evolution data - Modify onSimulationComplete to fetch and populate charts with complete data - Fix simulation threading to properly mark completion state - Add test script to verify chart data generation The charts now properly display simulation results by fetching complete evolution data when simulation completes, resolving the empty diagrams issue.
395 lines
14 KiB
Python
395 lines
14 KiB
Python
"""
|
|
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 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
|
|
|
|
# Mark simulation as running
|
|
simulation.is_running = True
|
|
|
|
# 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)
|
|
|
|
# Mark as completed
|
|
simulation.is_running = False
|
|
|
|
# Emit completion
|
|
socketio.emit('simulation_complete', {
|
|
'simulation_id': simulation_id,
|
|
'total_snapshots': len(simulation.snapshots)
|
|
}, room=f'simulation_{simulation_id}')
|
|
|
|
except Exception as e:
|
|
simulation.is_running = False
|
|
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':
|
|
# Get complete inequality data
|
|
ineq_iterations, gini_over_time, top10_over_time, capital_over_time = simulation.get_inequality_evolution()
|
|
|
|
response_data['evolution'] = {
|
|
'iterations': iterations,
|
|
'total_wealth': total_wealth,
|
|
'gini_coefficients': gini_coefficients,
|
|
'top10_shares': top10_over_time,
|
|
'capital_shares': capital_over_time
|
|
}
|
|
|
|
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') |