particle-swarm.mjs

/**
 * Particle Swarm Optimization (PSO)
 * A stochastic, population-based metaheuristic for maximizing complex multidimensional bounds.
 * 
 * @param {Function} evaluate - function(paramsArray) => Number (score to maximize)
 * @param {Array<Array<Number>>} bounds - [[min1, max1], [min2, max2], ...] bounds per dimension
 * @param {Object} options - { particles: 10, steps: 30, w: 0.5, c1: 1.5, c2: 1.5, onProgress: async (state) => {} }
 * @returns {Promise<{bestParams: Array<Number>, bestScore: Number}>}
 */
export const particleSwarmOptimization = async (evaluate, bounds, options = {}) => {
  const numParticles = options.particles || 12;
  const steps = options.steps || 30;
  const w = options.w || 0.5; // Inertia weight
  const c1 = options.c1 || 1.5; // Cognitive (local) best weight
  const c2 = options.c2 || 1.5; // Social (global) best weight
  const onProgress = options.onProgress || (async () => {});

  let swarm = [];
  let globalBestParams = null;
  let globalBestScore = -Infinity;

  // Initialize swarm
  for (let i = 0; i < numParticles; i++) {
    let position = bounds.map(b => b[0] + Math.random() * (b[1] - b[0]));
    let velocity = bounds.map(b => (Math.random() * 2 - 1) * (b[1] - b[0]) * 0.1); // Small initial velocity
    let score = evaluate(position);
    
    let particle = {
      position,
      velocity,
      bestPosition: [...position],
      bestScore: score
    };
    swarm.push(particle);
    
    if (score > globalBestScore) {
      globalBestScore = score;
      globalBestParams = [...position];
    }
  }

  for (let i = 0; i < steps; i++) {
    for (let p of swarm) {
      for (let d = 0; d < bounds.length; d++) {
        let r1 = Math.random();
        let r2 = Math.random();
        
        // Velocity update matrix
        p.velocity[d] = w * p.velocity[d] + 
                        c1 * r1 * (p.bestPosition[d] - p.position[d]) + 
                        c2 * r2 * (globalBestParams[d] - p.position[d]);
        
        // Position update
        p.position[d] = p.position[d] + p.velocity[d];
        
        // Enforce physical parameter bounds
        p.position[d] = Math.max(bounds[d][0], Math.min(bounds[d][1], p.position[d]));
      }
      
      let score = evaluate(p.position);
      
      // Update local cognitive best
      if (score > p.bestScore) {
        p.bestScore = score;
        p.bestPosition = [...p.position];
      }
      // Update social swarm global best
      if (score > globalBestScore) {
        globalBestScore = score;
        globalBestParams = [...p.position];
      }
    }
    
    await onProgress({
      step: i,
      totalSteps: steps,
      bestScore: globalBestScore,
      bestParams: globalBestParams,
      swarm: swarm // Expose for 3D visualizers (Radar scatter generation)
    });
  }

  return { bestParams: globalBestParams, bestScore: globalBestScore };
};