Compare commits

...

6 Commits

10 changed files with 344 additions and 93 deletions

View File

@@ -14,13 +14,13 @@ This directory contains Docker configuration files for containerizing the Markov
```bash
# Build and start the application
docker-compose up --build
docker compose up --build
# Run in detached mode
docker-compose up -d --build
docker compose up -d --build
# Stop the application
docker-compose down
docker compose down
```
### Using Docker directly

View File

@@ -36,5 +36,5 @@ EXPOSE 5000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD python -c "import requests; requests.get('http://localhost:5000/health', timeout=2)" || exit 1
# Run the application with Gunicorn using gevent workers for SocketIO compatibility
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "--worker-class", "gevent", "--worker-connections", "1000", "--timeout", "30", "wsgi:app"]
# Run the application with Flask's built-in server
CMD ["python", "run.py"]

View File

@@ -318,9 +318,6 @@ class EconomicSimulation:
if not wealth_values:
return [], []
# Debug logging
print(f"DEBUG: Wealth values - count: {len(wealth_values)}, min: {min(wealth_values)}, max: {max(wealth_values)}")
# Calculate histogram bins
min_wealth = min(wealth_values)
max_wealth = max(wealth_values)
@@ -328,7 +325,6 @@ class EconomicSimulation:
if min_wealth == max_wealth:
# All agents have same wealth
result = [f"${min_wealth:.0f}"], [len(wealth_values)]
print(f"DEBUG: All agents have same wealth - labels: {result[0]}, counts: {result[1]}")
return result
# Create bins
@@ -357,7 +353,6 @@ class EconomicSimulation:
if bin_start <= wealth < bin_end:
bin_counts[i] += 1
print(f"DEBUG: Histogram result - labels: {bin_labels}, counts: {bin_counts}")
return bin_labels, bin_counts
def update_parameters(self, new_parameters: SimulationParameters):

View File

@@ -139,19 +139,29 @@ def start_simulation(simulation_id: str):
try:
# Run simulation with progress updates
total_iterations = simulation.parameters.iterations
last_update_time = time.time()
update_interval = max(0.1, min(1.0, 1000 / total_iterations)) # Dynamic update interval
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:
# Get distribution data for real-time chart updates
bin_labels, bin_counts = simulation.get_wealth_histogram(10)
# Emit progress update at dynamic intervals for better performance
current_time = time.time()
if (i % max(1, total_iterations // 100) == 0 or # Every 1% of iterations
i == total_iterations - 1 or # Last iteration
current_time - last_update_time >= update_interval): # Time-based update
# Debug logging
print(f"DEBUG: Sending distribution data - labels: {bin_labels}, counts: {bin_counts}")
# Get distribution data for real-time chart updates (but less frequently)
distribution_data = None
if i % max(1, total_iterations // 20) == 0 or i == total_iterations - 1:
bin_labels, bin_counts = simulation.get_wealth_histogram(8) # Reduced bins for performance
distribution_data = {
'labels': bin_labels,
'counts': bin_counts
}
progress_data = {
'simulation_id': simulation_id,
@@ -161,18 +171,19 @@ def start_simulation(simulation_id: str):
'total_wealth': snapshot.total_wealth,
'gini_coefficient': snapshot.gini_coefficient,
'capital_share': snapshot.capital_share,
'wealth_concentration_top10': snapshot.wealth_concentration_top10,
'distribution': {
'labels': bin_labels,
'counts': bin_counts
}
'wealth_concentration_top10': snapshot.wealth_concentration_top10
}
# Only include distribution data when we have it
if distribution_data:
progress_data['distribution'] = distribution_data
socketio.emit('simulation_progress', progress_data,
room=f'simulation_{simulation_id}')
last_update_time = current_time
# Small delay to allow real-time visualization
time.sleep(0.01)
# Adaptive delay based on performance
time.sleep(max(0.001, 0.01 * (total_iterations / 1000)))
# Mark as completed
simulation.is_running = False
@@ -363,11 +374,21 @@ def get_wealth_distribution(simulation_id: str):
if num_bins < 1 or num_bins > 50:
num_bins = 10 # Default to 10 bins
# Optimize bin count based on agent count for better performance
agent_count = len(simulation.agents) if simulation.agents else 0
if agent_count > 0:
# Reduce bin count for small agent populations to improve performance
if agent_count < 50 and num_bins > agent_count // 2:
num_bins = max(3, agent_count // 2)
# Cap bin count for very large simulations to prevent performance issues
elif agent_count > 1000 and num_bins > 25:
num_bins = 25
# Get histogram data
bin_labels, bin_counts = simulation.get_wealth_histogram(num_bins)
# Debug logging
print(f"DEBUG: Distribution endpoint - labels: {bin_labels}, counts: {bin_counts}, bins: {num_bins}")
# print(f"DEBUG: Distribution endpoint - labels: {bin_labels}, counts: {bin_counts}, bins: {num_bins}")
response_data = {
'simulation_id': simulation_id,
@@ -379,7 +400,7 @@ def get_wealth_distribution(simulation_id: str):
}
}
print(f"DEBUG: Distribution endpoint response: {response_data}")
# print(f"DEBUG: Distribution endpoint response: {response_data}")
return jsonify(response_data)
except Exception as e:

View File

@@ -191,10 +191,14 @@ def test_simulation():
def debug_info():
"""
Debug endpoint to verify deployment and test functionality.
Returns:
JSON response with debug information
Only available in development mode.
"""
from flask import current_app
# Only allow debug endpoint in development mode
if not current_app.config.get('DEVELOPMENT', False):
return jsonify({'error': 'Debug endpoint not available in production'}), 403
import os
from datetime import datetime
from app.models.economic_model import EconomicSimulation, SimulationParameters

View File

@@ -26,13 +26,13 @@ function initializeSocket() {
window.MarkovEconomics.socket = io();
window.MarkovEconomics.socket.on('connect', function() {
console.log('Connected to server');
// console.log('Connected to server');
window.MarkovEconomics.isConnected = true;
updateConnectionStatus(true);
});
window.MarkovEconomics.socket.on('disconnect', function() {
console.log('Disconnected from server');
// console.log('Disconnected from server');
window.MarkovEconomics.isConnected = false;
updateConnectionStatus(false);
});
@@ -50,9 +50,9 @@ function initializeSocket() {
function updateConnectionStatus(connected) {
// You can add a connection status indicator here if needed
if (connected) {
console.log('✅ Real-time connection established');
// console.log('✅ Real-time connection established');
} else {
console.log('❌ Real-time connection lost');
// console.log('❌ Real-time connection lost');
}
}
@@ -251,7 +251,7 @@ document.addEventListener('DOMContentLoaded', function() {
card.classList.add('fade-in');
});
console.log('🚀 Markov Economics application initialized');
// console.log('🚀 Markov Economics application initialized');
});
/**
@@ -259,9 +259,9 @@ document.addEventListener('DOMContentLoaded', function() {
*/
document.addEventListener('visibilitychange', function() {
if (document.hidden) {
console.log('Page hidden - pausing updates');
// console.log('Page hidden - pausing updates');
} else {
console.log('Page visible - resuming updates');
// console.log('Page visible - resuming updates');
}
});

View File

@@ -256,7 +256,7 @@ window.testDistributionChart = function() {
};
// Debug flag - can be enabled even in production for troubleshooting
const DEBUG_DISTRIBUTION = true;
const DEBUG_DISTRIBUTION = false;
/**
* Debug logger that works in production
@@ -676,7 +676,7 @@ function initializeRealtimeUpdates() {
// Simulation progress updates
socket.on('simulation_progress', function(data) {
debugLog('Received real-time progress update');
updateSimulationProgress(data);
onSimulationProgress(data);
});
// Simulation completion
@@ -714,13 +714,14 @@ function startFallbackPolling() {
}
try {
const response = await window.MarkovEconomics.utils.apiRequest(
// Use enhanced API request with batching support
const response = await enhancedApiRequest(
`/api/simulation/${currentSimulation.id}/data?include_evolution=true&include_distribution=true`
);
if (response.latest_snapshot) {
// Simulate progress update
updateSimulationProgress({
onSimulationProgress({
iteration: response.latest_snapshot.iteration,
total_wealth: response.latest_snapshot.total_wealth,
gini_coefficient: response.latest_snapshot.gini_coefficient,
@@ -772,8 +773,8 @@ async function startSimulation() {
// Update UI state
updateUIState('starting');
// Create simulation
const createResponse = await window.MarkovEconomics.utils.apiRequest('/api/simulation', {
// Create simulation using enhanced API request
const createResponse = await enhancedApiRequest('/api/simulation', {
method: 'POST',
body: JSON.stringify(parameters)
});
@@ -796,8 +797,8 @@ async function startSimulation() {
debugLog('WebSocket not available, will use fallback polling');
}
// Start simulation
await window.MarkovEconomics.utils.apiRequest(`/api/simulation/${currentSimulation.id}/start`, {
// Start simulation using enhanced API request
await enhancedApiRequest(`/api/simulation/${currentSimulation.id}/start`, {
method: 'POST'
});
@@ -825,7 +826,8 @@ async function stopSimulation() {
if (!currentSimulation.id) return;
try {
await window.MarkovEconomics.utils.apiRequest(`/api/simulation/${currentSimulation.id}/stop`, {
// Use enhanced API request with batching support
await enhancedApiRequest(`/api/simulation/${currentSimulation.id}/stop`, {
method: 'POST'
});
@@ -989,76 +991,173 @@ function updateUIState(state) {
}
/**
* Update simulation progress
* Calculate adaptive update interval based on simulation parameters
* Reduces update frequency for larger simulations to improve performance
*/
function updateSimulationProgress(data) {
// Update progress bar
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
function getAdaptiveUpdateInterval() {
if (!currentSimulation.parameters.num_agents || !currentSimulation.parameters.iterations) {
return 50; // Default to 50ms
}
if (progressBar && progressText) {
const percentage = data.progress_percentage || 0;
const agentCount = currentSimulation.parameters.num_agents;
const iterationCount = currentSimulation.parameters.iterations;
// Base interval (ms)
let interval = 50;
// Increase interval for larger simulations
if (agentCount > 1000) {
interval = Math.min(200, 50 + (agentCount / 100));
} else if (agentCount > 500) {
interval = 100;
}
// Further adjust based on iteration count
if (iterationCount > 50000) {
interval *= 2;
} else if (iterationCount > 10000) {
interval *= 1.5;
}
// Cap at reasonable values
return Math.min(500, Math.max(20, interval));
}
/**
* Handle simulation progress updates
*/
function onSimulationProgress(data) {
// Update progress bar
const progressBar = document.getElementById('simulationProgressBar');
const progressText = document.getElementById('progressText');
if (progressBar && progressText && data.progress_percentage !== undefined) {
const percentage = Math.min(100, Math.max(0, data.progress_percentage));
progressBar.style.width = percentage + '%';
progressText.textContent = percentage.toFixed(1) + '%';
}
// Update charts and metrics
// Update charts and metrics only if we have new data
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);
// Only update time series data occasionally to improve performance
const shouldUpdateSeries = currentSimulation.data.iterations.length === 0 ||
data.iteration % Math.max(1, Math.floor(currentSimulation.parameters.iterations / 200)) === 0 ||
data.iteration === (currentSimulation.parameters.iterations - 1);
if (shouldUpdateSeries) {
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);
}
// Update distribution data if available
debugLog('Received simulation progress data', {
hasDistribution: !!data.distribution,
distribution: data.distribution,
socketConnected: window.MarkovEconomics ? window.MarkovEconomics.isConnected : 'unknown'
});
// More robust handling of distribution data
if (data.distribution &&
Array.isArray(data.distribution.labels) &&
Array.isArray(data.distribution.counts) &&
data.distribution.labels.length > 0 &&
data.distribution.counts.length > 0) {
debugLog('Updating distribution data', {
labelsLength: data.distribution.labels.length,
countsLength: data.distribution.counts.length,
labels: data.distribution.labels,
counts: data.distribution.counts
});
// Store the distribution data properly
currentSimulation.data.distribution.labels = [...data.distribution.labels];
currentSimulation.data.distribution.counts = [...data.distribution.counts];
} else {
debugLog('No valid distribution data in progress update');
} else if (data.iteration % Math.max(1, Math.floor(currentSimulation.parameters.iterations / 50)) === 0) {
// Periodically fetch distribution data if not provided in the update
// But less frequently for larger simulations
fetchDistributionData().then(histogram => {
if (histogram && histogram.labels && histogram.counts) {
currentSimulation.data.distribution.labels = [...histogram.labels];
currentSimulation.data.distribution.counts = [...histogram.counts];
}
});
}
// Adaptive throttling based on simulation size
const adaptiveInterval = getAdaptiveUpdateInterval();
if (!window.lastChartUpdate || (Date.now() - window.lastChartUpdate) > adaptiveInterval) {
updateCharts();
window.lastChartUpdate = Date.now();
}
updateCharts();
updateMetricsDisplay(data);
}
}
/**
* Update charts with new data
* Sample data for chart rendering to improve performance with large datasets
* @param {Array} data - Array of data points
* @param {number} maxPoints - Maximum number of points to display
* @returns {Array} - Sampled data
*/
function sampleDataForChart(data, maxPoints = 200) {
if (!Array.isArray(data) || data.length <= maxPoints) {
return data;
}
const sampled = [];
const step = Math.ceil(data.length / maxPoints);
for (let i = 0; i < data.length; i += step) {
sampled.push(data[i]);
}
return sampled;
}
/**
* Sample time series data for chart rendering
* @param {Object} chartData - Object containing time series data arrays
* @param {number} maxPoints - Maximum number of points to display
* @returns {Object} - Object with sampled data arrays
*/
function sampleTimeSeriesData(chartData, maxPoints = 200) {
if (!chartData.iterations || chartData.iterations.length <= maxPoints) {
return chartData;
}
const step = Math.ceil(chartData.iterations.length / maxPoints);
return {
iterations: sampleDataForChart(chartData.iterations, maxPoints),
totalWealth: sampleDataForChart(chartData.totalWealth, maxPoints),
giniCoefficients: sampleDataForChart(chartData.giniCoefficients, maxPoints),
capitalShare: sampleDataForChart(chartData.capitalShare, maxPoints),
top10Share: sampleDataForChart(chartData.top10Share, maxPoints)
};
}
/**
* Update charts with new data using sampling optimization
*/
function updateCharts() {
// Determine maximum points based on agent count for better performance
const maxChartPoints = currentSimulation.parameters.num_agents > 1000 ? 100 : 200;
// Wealth Evolution Chart
if (charts.wealthEvolution) {
charts.wealthEvolution.data.labels = currentSimulation.data.iterations;
charts.wealthEvolution.data.datasets[0].data = currentSimulation.data.totalWealth;
// Sample data for large datasets
const sampledData = sampleTimeSeriesData({
iterations: currentSimulation.data.iterations,
totalWealth: currentSimulation.data.totalWealth
}, maxChartPoints);
charts.wealthEvolution.data.labels = sampledData.iterations;
charts.wealthEvolution.data.datasets[0].data = sampledData.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;
// Sample data for large datasets
const sampledData = sampleTimeSeriesData({
iterations: currentSimulation.data.iterations,
giniCoefficients: currentSimulation.data.giniCoefficients,
top10Share: currentSimulation.data.top10Share
}, maxChartPoints);
charts.inequality.data.labels = sampledData.iterations;
charts.inequality.data.datasets[0].data = sampledData.giniCoefficients;
charts.inequality.data.datasets[1].data = sampledData.top10Share;
charts.inequality.update('none');
}
@@ -1139,7 +1238,8 @@ async function onSimulationComplete(data) {
// Fetch complete simulation data and populate charts
try {
debugLog('Fetching complete simulation data...');
const response = await window.MarkovEconomics.utils.apiRequest(
// Use enhanced API request with batching support
const response = await enhancedApiRequest(
`/api/simulation/${currentSimulation.id}/data?include_evolution=true&include_distribution=true`
);
@@ -1173,7 +1273,8 @@ async function onSimulationComplete(data) {
// Fallback: try to get distribution data from dedicated endpoint
try {
debugLog('Attempting fallback distribution fetch...');
const distResponse = await window.MarkovEconomics.utils.apiRequest(
// Use enhanced API request with batching support
const distResponse = await enhancedApiRequest(
`/api/simulation/${currentSimulation.id}/distribution?bins=10`
);
if (distResponse.histogram && distResponse.histogram.labels && distResponse.histogram.counts) {
@@ -1280,6 +1381,125 @@ function downloadFile(content, filename, contentType) {
window.URL.revokeObjectURL(url);
}
/**
* Calculate optimal bin count for distribution chart based on agent count
* @param {number} agentCount - Number of agents in the simulation
* @returns {number} - Optimal number of bins
*/
function getOptimalBinCount(agentCount) {
if (agentCount < 50) {
return Math.max(5, Math.floor(agentCount / 5));
} else if (agentCount < 200) {
return 10;
} else if (agentCount < 1000) {
return 15;
} else if (agentCount < 5000) {
return 20;
} else {
return 25; // Cap at 25 bins for very large simulations
}
}
/**
* Fetch distribution data with dynamic bin count using enhanced API requests
*/
async function fetchDistributionData() {
if (!currentSimulation.id) return null;
try {
// Calculate optimal bin count based on agent count
const binCount = getOptimalBinCount(currentSimulation.parameters.num_agents || 100);
// Use enhanced API request with batching support
const response = await enhancedApiRequest(
`/api/simulation/${currentSimulation.id}/distribution?bins=${binCount}`
);
return response.histogram;
} catch (error) {
debugLog('Error fetching distribution data', error);
return null;
}
}
/**
* Batch API requests to reduce network overhead
*/
class ApiRequestBatcher {
constructor() {
this.pendingRequests = [];
this.batchTimeout = null;
this.maxBatchSize = 5;
}
/**
* Add a request to the batch
*/
addRequest(url, options = {}) {
return new Promise((resolve, reject) => {
this.pendingRequests.push({ url, options, resolve, reject });
// If we've reached max batch size, flush immediately
if (this.pendingRequests.length >= this.maxBatchSize) {
this.flush();
} else if (!this.batchTimeout) {
// Otherwise, schedule a flush
this.batchTimeout = setTimeout(() => this.flush(), 50);
}
});
}
/**
* Flush all pending requests
*/
async flush() {
if (this.batchTimeout) {
clearTimeout(this.batchTimeout);
this.batchTimeout = null;
}
if (this.pendingRequests.length === 0) return;
// For now, we'll process requests individually since our API doesn't support batching
// In a real implementation, this would send a single batched request
const requests = [...this.pendingRequests];
this.pendingRequests = [];
// Process all requests
for (const request of requests) {
try {
const response = await fetch(request.url, request.options);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
request.resolve(data);
} catch (error) {
request.reject(error);
}
}
}
}
// Create a global instance
window.apiBatcher = new ApiRequestBatcher();
/**
* Enhanced API request function with batching support
*/
async function enhancedApiRequest(url, options = {}) {
// Use batching for certain types of requests
const shouldBatch = url.includes('/data') || url.includes('/distribution');
if (shouldBatch) {
return window.apiBatcher.addRequest(url, options);
} else {
// Fall back to regular API request for non-batchable requests
return window.MarkovEconomics.utils.apiRequest(url, options);
}
}
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', function() {
// Initial inequality warning update

View File

@@ -285,6 +285,24 @@ C' → C: 1 (consumption cycle)
</div>
</div>
<!-- Project Information -->
<div class="card shadow mb-4">
<div class="card-header bg-info text-white">
<h4 class="mb-0">📄 Project Information</h4>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-12">
<ul class="list-unstyled">
<li class="mb-2"><strong>© 2025 Markus F.J. Busche</strong> &lt;elpatron@mailbox.org&gt;</li>
<li class="mb-2"><strong>License:</strong> <a href="https://opensource.org/licenses/MIT" target="_blank">MIT License</a></li>
<li class="mb-2"><strong>Project Source Code:</strong> <a href="https://gitea.elpatron.me/elpatron/capitalism-eats-the-world.git" target="_blank">https://gitea.elpatron.me/elpatron/capitalism-eats-the-world.git</a></li>
</ul>
</div>
</div>
</div>
</div>
<!-- Call to Action -->
<div class="text-center">
<a href="{{ url_for('main.index') }}" class="btn btn-primary btn-lg">

View File

@@ -1,9 +1,8 @@
services:
markov-economics:
build: .
# Remove direct port mapping since Caddy will handle external access
# ports:
# - "5000:5000"
ports:
- "5000:5000"
environment:
- FLASK_ENV=production
- FLASK_APP=run.py

10
run.py
View File

@@ -15,11 +15,5 @@ if __name__ == '__main__':
debug_mode = config_name == 'development'
port = int(os.getenv('PORT', 5000)) # Use PORT env var or default to 5000
# For production deployment with Gunicorn, SocketIO will be handled by Gunicorn
# For development, we use SocketIO's built-in server
if config_name == 'production':
# In production, Gunicorn will handle the server
# This is just a fallback
socketio.run(app, debug=False, host='0.0.0.0', port=port, allow_unsafe_werkzeug=True)
else:
socketio.run(app, debug=debug_mode, host='0.0.0.0', port=port)
# Run with SocketIO's built-in server for both development and production
socketio.run(app, debug=debug_mode, host='0.0.0.0', port=port, allow_unsafe_werkzeug=True)