Compare commits
8 Commits
a1c47136cf
...
main
Author | SHA1 | Date | |
---|---|---|---|
0c095f7fcd | |||
c81d0b780d | |||
f8bb35e5be | |||
cb1327a718 | |||
be4c8cc2a5 | |||
c59732aef3 | |||
83e4fbf689 | |||
f03539ed22 |
@@ -14,13 +14,13 @@ This directory contains Docker configuration files for containerizing the Markov
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build and start the application
|
# Build and start the application
|
||||||
docker-compose up --build
|
docker compose up --build
|
||||||
|
|
||||||
# Run in detached mode
|
# Run in detached mode
|
||||||
docker-compose up -d --build
|
docker compose up -d --build
|
||||||
|
|
||||||
# Stop the application
|
# Stop the application
|
||||||
docker-compose down
|
docker compose down
|
||||||
```
|
```
|
||||||
|
|
||||||
### Using Docker directly
|
### Using Docker directly
|
||||||
|
@@ -36,5 +36,5 @@ EXPOSE 5000
|
|||||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
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
|
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
|
# Run the application with Flask's built-in server
|
||||||
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "--worker-class", "gevent", "--worker-connections", "1000", "--timeout", "30", "wsgi:app"]
|
CMD ["python", "run.py"]
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Capitalism Eats The World
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
@@ -25,7 +25,7 @@ This project provides an interactive simulation of economic dynamics, particular
|
|||||||
|
|
||||||
1. Clone the repository:
|
1. Clone the repository:
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/[user]/capitalism-eats-the-world.git
|
git clone https://gitea.elpatron.me/elpatron/capitalism-eats-the-world.git
|
||||||
cd capitalism-eats-the-world
|
cd capitalism-eats-the-world
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -40,14 +40,14 @@ This project provides an interactive simulation of economic dynamics, particular
|
|||||||
|
|
||||||
For production deployment with automatic SSL certificates, the project includes a Caddy configuration:
|
For production deployment with automatic SSL certificates, the project includes a Caddy configuration:
|
||||||
|
|
||||||
1. Update the [Caddyfile](Caddyfile) with your email address for Let's Encrypt notifications
|
1. Update the [Caddyfile](Caddyfile) with your email address and domain for Let's Encrypt notifications
|
||||||
2. Ensure your domain points to your server's IP address
|
2. Ensure your domain points to your server's IP address
|
||||||
3. Run with Caddy:
|
3. Run with Caddy:
|
||||||
```bash
|
```bash
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
The application will be available at `https://markov.elpatron.me` with automatic SSL certificate management.
|
The application will be available at `https://markov.example.com` with automatic SSL certificate management.
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
|
@@ -318,9 +318,6 @@ class EconomicSimulation:
|
|||||||
if not wealth_values:
|
if not wealth_values:
|
||||||
return [], []
|
return [], []
|
||||||
|
|
||||||
# Debug logging
|
|
||||||
print(f"DEBUG: Wealth values - count: {len(wealth_values)}, min: {min(wealth_values)}, max: {max(wealth_values)}")
|
|
||||||
|
|
||||||
# Calculate histogram bins
|
# Calculate histogram bins
|
||||||
min_wealth = min(wealth_values)
|
min_wealth = min(wealth_values)
|
||||||
max_wealth = max(wealth_values)
|
max_wealth = max(wealth_values)
|
||||||
@@ -328,7 +325,6 @@ class EconomicSimulation:
|
|||||||
if min_wealth == max_wealth:
|
if min_wealth == max_wealth:
|
||||||
# All agents have same wealth
|
# All agents have same wealth
|
||||||
result = [f"${min_wealth:.0f}"], [len(wealth_values)]
|
result = [f"${min_wealth:.0f}"], [len(wealth_values)]
|
||||||
print(f"DEBUG: All agents have same wealth - labels: {result[0]}, counts: {result[1]}")
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# Create bins
|
# Create bins
|
||||||
@@ -357,7 +353,6 @@ class EconomicSimulation:
|
|||||||
if bin_start <= wealth < bin_end:
|
if bin_start <= wealth < bin_end:
|
||||||
bin_counts[i] += 1
|
bin_counts[i] += 1
|
||||||
|
|
||||||
print(f"DEBUG: Histogram result - labels: {bin_labels}, counts: {bin_counts}")
|
|
||||||
return bin_labels, bin_counts
|
return bin_labels, bin_counts
|
||||||
|
|
||||||
def update_parameters(self, new_parameters: SimulationParameters):
|
def update_parameters(self, new_parameters: SimulationParameters):
|
||||||
|
@@ -139,19 +139,29 @@ def start_simulation(simulation_id: str):
|
|||||||
try:
|
try:
|
||||||
# Run simulation with progress updates
|
# Run simulation with progress updates
|
||||||
total_iterations = simulation.parameters.iterations
|
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):
|
for i in range(total_iterations):
|
||||||
if not simulation.is_running:
|
if not simulation.is_running:
|
||||||
break
|
break
|
||||||
|
|
||||||
snapshot = simulation.step()
|
snapshot = simulation.step()
|
||||||
|
|
||||||
# Emit progress update every 10 iterations or at milestones
|
# Emit progress update at dynamic intervals for better performance
|
||||||
if i % 10 == 0 or i == total_iterations - 1:
|
current_time = time.time()
|
||||||
# Get distribution data for real-time chart updates
|
if (i % max(1, total_iterations // 100) == 0 or # Every 1% of iterations
|
||||||
bin_labels, bin_counts = simulation.get_wealth_histogram(10)
|
i == total_iterations - 1 or # Last iteration
|
||||||
|
current_time - last_update_time >= update_interval): # Time-based update
|
||||||
|
|
||||||
# Debug logging
|
# Get distribution data for real-time chart updates (but less frequently)
|
||||||
print(f"DEBUG: Sending distribution data - labels: {bin_labels}, counts: {bin_counts}")
|
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 = {
|
progress_data = {
|
||||||
'simulation_id': simulation_id,
|
'simulation_id': simulation_id,
|
||||||
@@ -161,18 +171,19 @@ def start_simulation(simulation_id: str):
|
|||||||
'total_wealth': snapshot.total_wealth,
|
'total_wealth': snapshot.total_wealth,
|
||||||
'gini_coefficient': snapshot.gini_coefficient,
|
'gini_coefficient': snapshot.gini_coefficient,
|
||||||
'capital_share': snapshot.capital_share,
|
'capital_share': snapshot.capital_share,
|
||||||
'wealth_concentration_top10': snapshot.wealth_concentration_top10,
|
'wealth_concentration_top10': snapshot.wealth_concentration_top10
|
||||||
'distribution': {
|
|
||||||
'labels': bin_labels,
|
|
||||||
'counts': bin_counts
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Only include distribution data when we have it
|
||||||
|
if distribution_data:
|
||||||
|
progress_data['distribution'] = distribution_data
|
||||||
|
|
||||||
socketio.emit('simulation_progress', progress_data,
|
socketio.emit('simulation_progress', progress_data,
|
||||||
room=f'simulation_{simulation_id}')
|
room=f'simulation_{simulation_id}')
|
||||||
|
last_update_time = current_time
|
||||||
|
|
||||||
# Small delay to allow real-time visualization
|
# Adaptive delay based on performance
|
||||||
time.sleep(0.01)
|
time.sleep(max(0.001, 0.01 * (total_iterations / 1000)))
|
||||||
|
|
||||||
# Mark as completed
|
# Mark as completed
|
||||||
simulation.is_running = False
|
simulation.is_running = False
|
||||||
@@ -363,11 +374,21 @@ def get_wealth_distribution(simulation_id: str):
|
|||||||
if num_bins < 1 or num_bins > 50:
|
if num_bins < 1 or num_bins > 50:
|
||||||
num_bins = 10 # Default to 10 bins
|
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
|
# Get histogram data
|
||||||
bin_labels, bin_counts = simulation.get_wealth_histogram(num_bins)
|
bin_labels, bin_counts = simulation.get_wealth_histogram(num_bins)
|
||||||
|
|
||||||
# Debug logging
|
# 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 = {
|
response_data = {
|
||||||
'simulation_id': simulation_id,
|
'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)
|
return jsonify(response_data)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@@ -191,10 +191,14 @@ def test_simulation():
|
|||||||
def debug_info():
|
def debug_info():
|
||||||
"""
|
"""
|
||||||
Debug endpoint to verify deployment and test functionality.
|
Debug endpoint to verify deployment and test functionality.
|
||||||
|
Only available in development mode.
|
||||||
Returns:
|
|
||||||
JSON response with debug information
|
|
||||||
"""
|
"""
|
||||||
|
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
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from app.models.economic_model import EconomicSimulation, SimulationParameters
|
from app.models.economic_model import EconomicSimulation, SimulationParameters
|
||||||
|
@@ -26,13 +26,13 @@ function initializeSocket() {
|
|||||||
window.MarkovEconomics.socket = io();
|
window.MarkovEconomics.socket = io();
|
||||||
|
|
||||||
window.MarkovEconomics.socket.on('connect', function() {
|
window.MarkovEconomics.socket.on('connect', function() {
|
||||||
console.log('Connected to server');
|
// console.log('Connected to server');
|
||||||
window.MarkovEconomics.isConnected = true;
|
window.MarkovEconomics.isConnected = true;
|
||||||
updateConnectionStatus(true);
|
updateConnectionStatus(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
window.MarkovEconomics.socket.on('disconnect', function() {
|
window.MarkovEconomics.socket.on('disconnect', function() {
|
||||||
console.log('Disconnected from server');
|
// console.log('Disconnected from server');
|
||||||
window.MarkovEconomics.isConnected = false;
|
window.MarkovEconomics.isConnected = false;
|
||||||
updateConnectionStatus(false);
|
updateConnectionStatus(false);
|
||||||
});
|
});
|
||||||
@@ -50,9 +50,9 @@ function initializeSocket() {
|
|||||||
function updateConnectionStatus(connected) {
|
function updateConnectionStatus(connected) {
|
||||||
// You can add a connection status indicator here if needed
|
// You can add a connection status indicator here if needed
|
||||||
if (connected) {
|
if (connected) {
|
||||||
console.log('✅ Real-time connection established');
|
// console.log('✅ Real-time connection established');
|
||||||
} else {
|
} 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');
|
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() {
|
document.addEventListener('visibilitychange', function() {
|
||||||
if (document.hidden) {
|
if (document.hidden) {
|
||||||
console.log('Page hidden - pausing updates');
|
// console.log('Page hidden - pausing updates');
|
||||||
} else {
|
} else {
|
||||||
console.log('Page visible - resuming updates');
|
// console.log('Page visible - resuming updates');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -256,7 +256,7 @@ window.testDistributionChart = function() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Debug flag - can be enabled even in production for troubleshooting
|
// Debug flag - can be enabled even in production for troubleshooting
|
||||||
const DEBUG_DISTRIBUTION = true;
|
const DEBUG_DISTRIBUTION = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Debug logger that works in production
|
* Debug logger that works in production
|
||||||
@@ -676,7 +676,7 @@ function initializeRealtimeUpdates() {
|
|||||||
// Simulation progress updates
|
// Simulation progress updates
|
||||||
socket.on('simulation_progress', function(data) {
|
socket.on('simulation_progress', function(data) {
|
||||||
debugLog('Received real-time progress update');
|
debugLog('Received real-time progress update');
|
||||||
updateSimulationProgress(data);
|
onSimulationProgress(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Simulation completion
|
// Simulation completion
|
||||||
@@ -714,13 +714,14 @@ function startFallbackPolling() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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`
|
`/api/simulation/${currentSimulation.id}/data?include_evolution=true&include_distribution=true`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.latest_snapshot) {
|
if (response.latest_snapshot) {
|
||||||
// Simulate progress update
|
// Simulate progress update
|
||||||
updateSimulationProgress({
|
onSimulationProgress({
|
||||||
iteration: response.latest_snapshot.iteration,
|
iteration: response.latest_snapshot.iteration,
|
||||||
total_wealth: response.latest_snapshot.total_wealth,
|
total_wealth: response.latest_snapshot.total_wealth,
|
||||||
gini_coefficient: response.latest_snapshot.gini_coefficient,
|
gini_coefficient: response.latest_snapshot.gini_coefficient,
|
||||||
@@ -772,8 +773,8 @@ async function startSimulation() {
|
|||||||
// Update UI state
|
// Update UI state
|
||||||
updateUIState('starting');
|
updateUIState('starting');
|
||||||
|
|
||||||
// Create simulation
|
// Create simulation using enhanced API request
|
||||||
const createResponse = await window.MarkovEconomics.utils.apiRequest('/api/simulation', {
|
const createResponse = await enhancedApiRequest('/api/simulation', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(parameters)
|
body: JSON.stringify(parameters)
|
||||||
});
|
});
|
||||||
@@ -796,8 +797,8 @@ async function startSimulation() {
|
|||||||
debugLog('WebSocket not available, will use fallback polling');
|
debugLog('WebSocket not available, will use fallback polling');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start simulation
|
// Start simulation using enhanced API request
|
||||||
await window.MarkovEconomics.utils.apiRequest(`/api/simulation/${currentSimulation.id}/start`, {
|
await enhancedApiRequest(`/api/simulation/${currentSimulation.id}/start`, {
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -825,7 +826,8 @@ async function stopSimulation() {
|
|||||||
if (!currentSimulation.id) return;
|
if (!currentSimulation.id) return;
|
||||||
|
|
||||||
try {
|
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'
|
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) {
|
function getAdaptiveUpdateInterval() {
|
||||||
// Update progress bar
|
if (!currentSimulation.parameters.num_agents || !currentSimulation.parameters.iterations) {
|
||||||
const progressBar = document.getElementById('progressBar');
|
return 50; // Default to 50ms
|
||||||
const progressText = document.getElementById('progressText');
|
}
|
||||||
|
|
||||||
if (progressBar && progressText) {
|
const agentCount = currentSimulation.parameters.num_agents;
|
||||||
const percentage = data.progress_percentage || 0;
|
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 + '%';
|
progressBar.style.width = percentage + '%';
|
||||||
progressText.textContent = percentage.toFixed(1) + '%';
|
progressText.textContent = percentage.toFixed(1) + '%';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update charts and metrics
|
// Update charts and metrics only if we have new data
|
||||||
if (data.iteration !== undefined) {
|
if (data.iteration !== undefined) {
|
||||||
currentSimulation.data.iterations.push(data.iteration);
|
// Only update time series data occasionally to improve performance
|
||||||
currentSimulation.data.totalWealth.push(data.total_wealth || 0);
|
const shouldUpdateSeries = currentSimulation.data.iterations.length === 0 ||
|
||||||
currentSimulation.data.giniCoefficients.push(data.gini_coefficient || 0);
|
data.iteration % Math.max(1, Math.floor(currentSimulation.parameters.iterations / 200)) === 0 ||
|
||||||
currentSimulation.data.capitalShare.push(data.capital_share || 0);
|
data.iteration === (currentSimulation.parameters.iterations - 1);
|
||||||
currentSimulation.data.top10Share.push(data.wealth_concentration_top10 || 0);
|
|
||||||
|
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
|
// 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 &&
|
if (data.distribution &&
|
||||||
Array.isArray(data.distribution.labels) &&
|
Array.isArray(data.distribution.labels) &&
|
||||||
Array.isArray(data.distribution.counts) &&
|
Array.isArray(data.distribution.counts) &&
|
||||||
data.distribution.labels.length > 0 &&
|
data.distribution.labels.length > 0 &&
|
||||||
data.distribution.counts.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
|
// Store the distribution data properly
|
||||||
currentSimulation.data.distribution.labels = [...data.distribution.labels];
|
currentSimulation.data.distribution.labels = [...data.distribution.labels];
|
||||||
currentSimulation.data.distribution.counts = [...data.distribution.counts];
|
currentSimulation.data.distribution.counts = [...data.distribution.counts];
|
||||||
} else {
|
} else if (data.iteration % Math.max(1, Math.floor(currentSimulation.parameters.iterations / 50)) === 0) {
|
||||||
debugLog('No valid distribution data in progress update');
|
// 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);
|
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() {
|
function updateCharts() {
|
||||||
|
// Determine maximum points based on agent count for better performance
|
||||||
|
const maxChartPoints = currentSimulation.parameters.num_agents > 1000 ? 100 : 200;
|
||||||
|
|
||||||
// Wealth Evolution Chart
|
// Wealth Evolution Chart
|
||||||
if (charts.wealthEvolution) {
|
if (charts.wealthEvolution) {
|
||||||
charts.wealthEvolution.data.labels = currentSimulation.data.iterations;
|
// Sample data for large datasets
|
||||||
charts.wealthEvolution.data.datasets[0].data = currentSimulation.data.totalWealth;
|
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');
|
charts.wealthEvolution.update('none');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inequality Chart
|
// Inequality Chart
|
||||||
if (charts.inequality) {
|
if (charts.inequality) {
|
||||||
charts.inequality.data.labels = currentSimulation.data.iterations;
|
// Sample data for large datasets
|
||||||
charts.inequality.data.datasets[0].data = currentSimulation.data.giniCoefficients;
|
const sampledData = sampleTimeSeriesData({
|
||||||
charts.inequality.data.datasets[1].data = currentSimulation.data.top10Share;
|
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');
|
charts.inequality.update('none');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1139,7 +1238,8 @@ async function onSimulationComplete(data) {
|
|||||||
// Fetch complete simulation data and populate charts
|
// Fetch complete simulation data and populate charts
|
||||||
try {
|
try {
|
||||||
debugLog('Fetching complete simulation data...');
|
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`
|
`/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
|
// Fallback: try to get distribution data from dedicated endpoint
|
||||||
try {
|
try {
|
||||||
debugLog('Attempting fallback distribution fetch...');
|
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`
|
`/api/simulation/${currentSimulation.id}/distribution?bins=10`
|
||||||
);
|
);
|
||||||
if (distResponse.histogram && distResponse.histogram.labels && distResponse.histogram.counts) {
|
if (distResponse.histogram && distResponse.histogram.labels && distResponse.histogram.counts) {
|
||||||
@@ -1280,6 +1381,125 @@ function downloadFile(content, filename, contentType) {
|
|||||||
window.URL.revokeObjectURL(url);
|
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
|
// Initialize when DOM is ready
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Initial inequality warning update
|
// Initial inequality warning update
|
||||||
|
@@ -285,6 +285,24 @@ C' → C: 1 (consumption cycle)
|
|||||||
</div>
|
</div>
|
||||||
</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> <elpatron@mailbox.org></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 -->
|
<!-- Call to Action -->
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<a href="{{ url_for('main.index') }}" class="btn btn-primary btn-lg">
|
<a href="{{ url_for('main.index') }}" class="btn btn-primary btn-lg">
|
||||||
|
@@ -1,9 +1,8 @@
|
|||||||
services:
|
services:
|
||||||
markov-economics:
|
markov-economics:
|
||||||
build: .
|
build: .
|
||||||
# Remove direct port mapping since Caddy will handle external access
|
ports:
|
||||||
# ports:
|
- "5000:5000"
|
||||||
# - "5000:5000"
|
|
||||||
environment:
|
environment:
|
||||||
- FLASK_ENV=production
|
- FLASK_ENV=production
|
||||||
- FLASK_APP=run.py
|
- FLASK_APP=run.py
|
||||||
|
10
run.py
10
run.py
@@ -15,11 +15,5 @@ if __name__ == '__main__':
|
|||||||
debug_mode = config_name == 'development'
|
debug_mode = config_name == 'development'
|
||||||
port = int(os.getenv('PORT', 5000)) # Use PORT env var or default to 5000
|
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
|
# Run with SocketIO's built-in server for both development and production
|
||||||
# For development, we use SocketIO's built-in server
|
socketio.run(app, debug=debug_mode, host='0.0.0.0', port=port, allow_unsafe_werkzeug=True)
|
||||||
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)
|
|
Reference in New Issue
Block a user