import { parentPort } from 'worker_threads';
import {
calculateNPV,
calculatePaybackPeriod,
calculateMonthlyFlow,
FINANCIAL_CONSTANTS
} from '../core/calculators/financial-utils.js';
import { WorkerError, CalculationError, TimeoutError } from '../utils/errors.js';
// Worker-specific logger (writes to stderr to avoid stdout conflicts)
const workerLog = (level: string, message: string, data?: any) => {
const timestamp = new Date().toISOString();
const logEntry = {
timestamp,
level,
worker: 'monte-carlo',
message,
...(data && { data })
};
console.error(JSON.stringify(logEntry));
};
interface SimulationTask {
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;
seed: number;
}
interface SimulationResult {
roi: number;
npv: number;
paybackMonths: number;
finalAdoptionRate: number;
finalEfficiencyGain: number;
totalCost: number;
}
class Random {
private seed: number;
constructor(seed: number) {
this.seed = seed;
}
// Linear Congruential Generator
next(): number {
this.seed = (this.seed * 1664525 + 1013904223) % 2147483647;
return this.seed / 2147483647;
}
uniform(min: number, max: number): number {
return min + this.next() * (max - min);
}
normal(mean: number, stdDev: number): number {
// Box-Muller transform
const u1 = this.next();
const u2 = this.next();
const z0 = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
return mean + z0 * stdDev;
}
beta(alpha: number, beta: number): number {
// Simple approximation using uniform distribution
// For production, use a proper beta distribution implementation
const u = this.next();
return Math.pow(u, alpha) / (Math.pow(u, alpha) + Math.pow(1 - u, beta));
}
}
function generateRandomValue(
variable: { min: number; max: number; distribution: string },
random: Random
): number {
switch (variable.distribution) {
case 'normal': {
const mean = (variable.min + variable.max) / 2;
const stdDev = (variable.max - variable.min) / 6; // 99.7% within range
let value = random.normal(mean, stdDev);
// Clamp to bounds
return Math.max(variable.min, Math.min(variable.max, value));
}
case 'beta': {
const value = random.beta(2, 2); // Beta(2,2) is bell-shaped
return variable.min + value * (variable.max - variable.min);
}
case 'uniform':
default:
return random.uniform(variable.min, variable.max);
}
}
function runSimulation(task: SimulationTask, random: Random): SimulationResult {
const { baseCase, variables } = task;
// Generate random values for this iteration
const adoptionRate = generateRandomValue(variables.adoptionRate, random);
const efficiencyGain = generateRandomValue(variables.efficiencyGain, random);
const implementationDelay = generateRandomValue(variables.implementationDelay, random);
const costOverrun = generateRandomValue(variables.costOverrun, random);
// Adjust base case with random factors
const adjustedMonthlyBenefit = baseCase.monthlyBenefit * adoptionRate * efficiencyGain;
const adjustedInvestment = baseCase.totalInvestment * costOverrun;
const adjustedImplementationMonths = baseCase.implementationMonths + implementationDelay;
// Calculate cash flows
const cashFlows: number[] = [-adjustedInvestment];
for (let month = 1; month <= baseCase.timelineMonths; month++) {
const monthlyFlow = calculateMonthlyFlow(
month,
adjustedMonthlyBenefit,
adjustedImplementationMonths,
baseCase.rampUpMonths,
baseCase.ongoingMonthlyCosts
);
cashFlows.push(monthlyFlow);
}
// Calculate metrics
const totalBenefit = cashFlows.slice(1).reduce((sum, flow) => sum + Math.max(0, flow), 0);
const totalCost = adjustedInvestment + baseCase.ongoingMonthlyCosts * baseCase.timelineMonths;
const roi = ((totalBenefit - totalCost) / totalCost) * 100;
// NPV calculation using shared utility
const npv = calculateNPV(cashFlows, FINANCIAL_CONSTANTS.DEFAULT_DISCOUNT_RATE);
// Payback period using shared utility
const paybackMonths = calculatePaybackPeriod(cashFlows);
return {
roi,
npv,
paybackMonths,
finalAdoptionRate: adoptionRate,
finalEfficiencyGain: efficiencyGain,
totalCost
};
}
// Worker message handler
parentPort?.on('message', (task: SimulationTask) => {
const startTime = Date.now();
workerLog('info', 'Starting simulation task', {
iterations: task.iterations,
seed: task.seed
});
try {
// Validate task input
if (!task || typeof task !== 'object') {
throw new WorkerError('Invalid task received', { task });
}
if (!task.iterations || task.iterations < 1 || task.iterations > 1000000) {
throw new WorkerError('Invalid iteration count', {
iterations: task.iterations,
min: 1,
max: 1000000
});
}
if (!task.baseCase || typeof task.baseCase !== 'object') {
throw new WorkerError('Invalid base case data', { baseCase: task.baseCase });
}
// Validate financial values
const { baseCase } = task;
if (baseCase.monthlyBenefit < 0 || !isFinite(baseCase.monthlyBenefit)) {
throw new CalculationError('Invalid monthly benefit', {
value: baseCase.monthlyBenefit
});
}
if (baseCase.totalInvestment < 0 || !isFinite(baseCase.totalInvestment)) {
throw new CalculationError('Invalid total investment', {
value: baseCase.totalInvestment
});
}
const results: SimulationResult[] = [];
const random = new Random(task.seed);
// Run simulations with progress logging
const logInterval = Math.max(1, Math.floor(task.iterations / 10));
for (let i = 0; i < task.iterations; i++) {
try {
const result = runSimulation(task, random);
// Validate simulation result
if (!isFinite(result.roi) || !isFinite(result.npv)) {
throw new CalculationError('Simulation produced invalid results', {
iteration: i,
roi: result.roi,
npv: result.npv
});
}
results.push(result);
// Log progress
if (i > 0 && i % logInterval === 0) {
workerLog('debug', 'Simulation progress', {
completed: i,
total: task.iterations,
percentage: Math.round((i / task.iterations) * 100)
});
}
} catch (error) {
workerLog('error', `Simulation failed at iteration ${i}`, {
error: (error as Error).message
});
// Continue with next iteration instead of failing completely
}
}
const duration = Date.now() - startTime;
workerLog('info', 'Simulation task completed', {
iterations: task.iterations,
results: results.length,
duration_ms: duration
});
// Send results back to main thread
parentPort?.postMessage({
success: true,
results,
metadata: {
totalIterations: task.iterations,
successfulIterations: results.length,
duration
}
});
} catch (error) {
const duration = Date.now() - startTime;
workerLog('error', 'Worker task failed', {
error: (error as Error).message,
stack: (error as Error).stack,
duration_ms: duration
});
// Send error back to main thread
parentPort?.postMessage({
success: false,
error: {
message: (error as Error).message,
type: error instanceof Error ? error.constructor.name : 'UnknownError',
context: (error as any).context
}
});
}
});
// Handle worker errors
process.on('uncaughtException', (error) => {
workerLog('error', 'Uncaught exception in worker', {
error: error.message,
stack: error.stack
});
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
workerLog('error', 'Unhandled rejection in worker', {
reason,
promise
});
process.exit(1);
});