simulated-annealing.mjs

/**
 * Simulated Annealing (Derivative-Free Stochastic Optimization)
 * Maximizes the evaluate function payload.
 * 
 * @param {Function} evaluate - function(paramsArray) => Number (score to maximize)
 * @param {Array<Array<Number>>} bounds - [[min1, max1], [min2, max2], ...] bounds per dimension
 * @param {Object} options - { steps: 100, initialTemp: 100, finalTemp: 0.1, onProgress: async (state) => {} }
 * @returns {Promise<{bestParams: Array<Number>, bestScore: Number}>}
 */
export const simulatedAnnealing = async (evaluate, bounds, options = {}) => {
  const steps = options.steps || 100;
  const initialTemp = options.initialTemp || 100.0;
  const finalTemp = options.finalTemp || 0.1;
  const onProgress = options.onProgress || (async () => {});

  let currentParams = bounds.map(b => (b[0] + b[1]) / 2);
  let currentScore = evaluate(currentParams);
  
  let bestParams = [...currentParams];
  let bestScore = currentScore;

  for (let i = 0; i < steps; i++) {
    let temp = initialTemp * Math.pow((finalTemp / initialTemp), (i / steps));
    let noiseScale = temp / initialTemp; // Gradually reduce the noise field radius
    
    let candidateParams = currentParams.map((p, idx) => {
      let b = bounds[idx];
      let range = b[1] - b[0];
      let nv = p + (Math.random() * 2 - 1) * (range * 0.2) * noiseScale;
      return Math.max(b[0], Math.min(b[1], nv)); // enforce bounds constraint
    });
    
    let candidateScore = evaluate(candidateParams);
    
    // Acceptance criteria (accept worse scores with diminishing probability to escape local maxima)
    if (candidateScore > currentScore || Math.random() < Math.exp((candidateScore - currentScore) / temp)) {
      currentParams = candidateParams;
      currentScore = candidateScore;
      
      if (candidateScore > bestScore) {
        bestScore = candidateScore;
        bestParams = [...currentParams];
      }
    }
    
    await onProgress({
      step: i,
      totalSteps: steps,
      score: currentScore,
      bestScore: bestScore,
      params: currentParams,
      bestParams: bestParams
    });
  }
  
  return { bestParams, bestScore };
};