import { Piscina } from 'piscina';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import type { MonteCarloResults } from '../../schemas/projection.js';
import { logger, createLogger } from '../../utils/logger.js';
import { WorkerError, CalculationError, TimeoutError } from '../../utils/errors.js';
import { validateIterations, validateFinancialAmount } from '../../utils/validators.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
export interface MonteCarloConfig {
baseCase: {
monthlyBenefit: number;
totalInvestment: number;
timelineMonths: number;
implementationMonths: number;
rampUpMonths: number;
ongoingMonthlyCosts: number;
};
variables: {
adoptionRate: { min: number; max: number; distribution: string };
efficiencyGain: { min: number; max: number; distribution: string };
implementationDelay: { min: number; max: number; distribution: string };
costOverrun: { min: number; max: number; distribution: string };
};
iterations: number;
}
export class MonteCarloSimulator {
private piscina: Piscina<any, any>;
private logger = createLogger({ component: 'MonteCarloSimulator' });
constructor() {
const workerPath = join(__dirname, '../../workers/monte-carlo-worker.js');
const maxThreads = parseInt(process.env.WORKER_POOL_SIZE || '4');
this.logger.info('Initializing Monte Carlo simulator', {
workerPath,
maxThreads,
maxIterations: process.env.MAX_SIMULATION_ITERATIONS || 100000
});
try {
this.piscina = new Piscina({
filename: workerPath,
maxThreads,
idleTimeout: 60000, // 1 minute
maxQueue: 100 // Prevent memory issues with large queues
});
this.logger.info('Worker pool initialized successfully');
} catch (error) {
this.logger.error('Failed to initialize worker pool', error as Error);
throw new WorkerError('Failed to initialize Monte Carlo worker pool', {
workerPath,
error: (error as Error).message
});
}
}
async runSimulation(
config: MonteCarloConfig,
projectionId: string
): Promise<MonteCarloResults> {
const startTime = Date.now();
try {
// Validate configuration
this.logger.debug('Validating simulation config');
validateIterations(config.iterations);
validateFinancialAmount(config.baseCase.monthlyBenefit, 'monthlyBenefit');
validateFinancialAmount(config.baseCase.totalInvestment, 'totalInvestment');
// Check worker pool health
if (this.piscina.threads.length === 0) {
throw new WorkerError('No worker threads available');
}
const workerCount = this.piscina.threads.length;
const iterationsPerWorker = Math.ceil(config.iterations / workerCount);
this.logger.info('Starting simulation', {
projectionId,
totalIterations: config.iterations,
workerCount,
iterationsPerWorker
});
// Create tasks for each worker
const tasks = [];
for (let i = 0; i < workerCount; i++) {
const remainingIterations = config.iterations - (i * iterationsPerWorker);
if (remainingIterations <= 0) break;
tasks.push({
...config,
iterations: Math.min(iterationsPerWorker, remainingIterations),
seed: Date.now() + i // Different seed for each worker
});
}
// Set timeout for worker tasks
const timeout = parseInt(process.env.SIMULATION_TIMEOUT || '300000'); // 5 minutes default
// Run simulations in parallel with timeout
const workerResults = await Promise.race([
Promise.all(
tasks.map((task, index) =>
this.piscina.run(task, { name: 'default' })
.then(result => {
if (!result.success) {
throw new WorkerError(`Worker ${index} failed: ${result.error.message}`, {
workerIndex: index,
error: result.error
});
}
return result;
})
)
),
new Promise((_, reject) =>
setTimeout(() =>
reject(new TimeoutError('Monte Carlo simulation', timeout)),
timeout
)
)
]) as any[];
// Extract and validate results
const allResults = [];
let totalSuccessful = 0;
for (const workerResult of workerResults) {
if (workerResult.results && Array.isArray(workerResult.results)) {
allResults.push(...workerResult.results);
totalSuccessful += workerResult.metadata.successfulIterations;
}
}
if (allResults.length === 0) {
throw new CalculationError('No simulation results produced');
}
const successRate = totalSuccessful / config.iterations;
if (successRate < 0.9) {
this.logger.warn('Low simulation success rate', {
successRate,
totalRequested: config.iterations,
totalSuccessful
});
}
// Analyze results
this.logger.debug('Analyzing simulation results', {
resultCount: allResults.length
});
const analysis = this.analyzeResults(allResults);
const duration = Date.now() - startTime;
this.logger.info('Simulation completed', {
projectionId,
duration_ms: duration,
totalResults: allResults.length,
successRate
});
return {
projection_id: projectionId,
simulation_count: allResults.length,
run_date: new Date().toISOString(),
roi_distribution: analysis.roiDistribution,
payback_distribution: analysis.paybackDistribution,
risk_analysis: analysis.riskAnalysis
};
} catch (error) {
this.logger.error('Simulation failed', error as Error, { projectionId });
throw error;
} finally {
const endTime = Date.now();
this.logger.info('Monte Carlo simulation completed', {
duration: endTime - startTime,
projectionId
});
}
}
private analyzeResults(results: any[]): {
roiDistribution: MonteCarloResults['roi_distribution'];
paybackDistribution: MonteCarloResults['payback_distribution'];
riskAnalysis: MonteCarloResults['risk_analysis'];
} {
// Sort ROI values
const roiValues = results.map(r => r.roi).sort((a, b) => a - b);
const npvValues = results.map(r => r.npv).sort((a, b) => a - b);
const paybackValues = results.map(r => r.paybackMonths).sort((a, b) => a - b);
// Calculate ROI distribution
const roiDistribution = {
percentiles: {
p5: this.percentile(roiValues, 0.05),
p25: this.percentile(roiValues, 0.25),
p50: this.percentile(roiValues, 0.50),
p75: this.percentile(roiValues, 0.75),
p95: this.percentile(roiValues, 0.95)
},
mean: this.mean(roiValues),
std_dev: this.standardDeviation(roiValues),
confidence_interval_95: [
this.percentile(roiValues, 0.025),
this.percentile(roiValues, 0.975)
] as [number, number]
};
// Calculate payback distribution
const paybackDistribution = {
percentiles: {
'p10': this.percentile(paybackValues, 0.10),
'p25': this.percentile(paybackValues, 0.25),
'p50': this.percentile(paybackValues, 0.50),
'p75': this.percentile(paybackValues, 0.75),
'p90': this.percentile(paybackValues, 0.90)
},
probability_within_12_months: paybackValues.filter(v => v <= 12).length / paybackValues.length,
probability_within_24_months: paybackValues.filter(v => v <= 24).length / paybackValues.length
};
// Risk analysis
const lossCount = npvValues.filter(v => v < 0).length;
const valueAtRisk95 = this.percentile(npvValues, 0.05);
// Identify key risk drivers through correlation analysis
const riskDrivers = this.identifyRiskDrivers(results);
return {
roiDistribution,
paybackDistribution,
riskAnalysis: {
probability_of_loss: lossCount / results.length,
value_at_risk_95: valueAtRisk95,
key_risk_drivers: riskDrivers
}
};
}
private percentile(sortedValues: number[], p: number): number {
const index = Math.ceil(sortedValues.length * p) - 1;
return sortedValues[Math.max(0, Math.min(index, sortedValues.length - 1))];
}
private mean(values: number[]): number {
return values.reduce((sum, val) => sum + val, 0) / values.length;
}
private standardDeviation(values: number[]): number {
const avg = this.mean(values);
const squareDiffs = values.map(val => Math.pow(val - avg, 2));
return Math.sqrt(this.mean(squareDiffs));
}
private identifyRiskDrivers(results: any[]): Array<{
factor: string;
impact_percentage: number;
correlation: number;
}> {
// Calculate correlations between input variables and ROI
const roiValues = results.map(r => r.roi);
const factors = [
{ name: 'Adoption Rate', values: results.map(r => r.finalAdoptionRate) },
{ name: 'Efficiency Gain', values: results.map(r => r.finalEfficiencyGain) },
{ name: 'Total Cost', values: results.map(r => r.totalCost) }
];
return factors.map(factor => {
const correlation = this.correlation(factor.values, roiValues);
const impact = Math.abs(correlation) * 100;
return {
factor: factor.name,
impact_percentage: impact,
correlation: correlation
};
}).sort((a, b) => b.impact_percentage - a.impact_percentage);
}
private correlation(x: number[], y: number[]): number {
const n = x.length;
const sumX = x.reduce((a, b) => a + b, 0);
const sumY = y.reduce((a, b) => a + b, 0);
const sumXY = x.reduce((total, xi, i) => total + xi * y[i], 0);
const sumX2 = x.reduce((total, xi) => total + xi * xi, 0);
const sumY2 = y.reduce((total, yi) => total + yi * yi, 0);
const numerator = n * sumXY - sumX * sumY;
const denominator = Math.sqrt((n * sumX2 - sumX * sumX) * (n * sumY2 - sumY * sumY));
return denominator === 0 ? 0 : numerator / denominator;
}
async destroy(): Promise<void> {
await this.piscina.destroy();
}
}