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
|
||||
# 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
|
||||
|
@@ -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"]
|
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:
|
||||
```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
|
||||
```
|
||||
|
||||
@@ -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:
|
||||
|
||||
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
|
||||
3. Run with Caddy:
|
||||
```bash
|
||||
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
|
||||
|
||||
|
@@ -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):
|
||||
|
@@ -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:
|
||||
|
@@ -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
|
||||
|
@@ -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');
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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> <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 -->
|
||||
<div class="text-center">
|
||||
<a href="{{ url_for('main.index') }}" class="btn btn-primary btn-lg">
|
||||
|
@@ -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
10
run.py
@@ -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)
|
Reference in New Issue
Block a user