Whale Optimization Algorithm
in reviewThis lesson is complete and being validated before release - details may still change.
in reviewThis lesson is complete and being validated before release - details may still change.
whale-optimization.ts
import { Mulberry32 } from "../random";import type { Tracer } from "../trace";import type { Landscape } from "./landscape";/** The most whales one run takes. */export const WOA_MAX_POPULATION = 12;/** The most iterations one run takes. */export const WOA_MAX_ITERATIONS = 30;/** Whales to a pod when the option is left out. */export const WOA_DEFAULT_POPULATION = 6;/** Iterations when the option is left out. */export const WOA_DEFAULT_ITERATIONS = 20;/** The spiral's shape constant `b` when the option is left out. */export const WOA_DEFAULT_SPIRAL = 1;/** How a whale moved: pulled in around the leader, pushed around a random pod mate, or along a spiral onto the leader. */export type WhaleMode = "encircle" | "search" | "spiral";export interface WhaleOptions { landscape: Landscape; seed: number; /** Whales in the pod, 2..WOA_MAX_POPULATION; default WOA_DEFAULT_POPULATION. */ population?: number; /** Iterations, 1..WOA_MAX_ITERATIONS; default WOA_DEFAULT_ITERATIONS. */ iterations?: number; /** The spiral's shape constant `b`, positive; default WOA_DEFAULT_SPIRAL. */ spiral?: number;}export interface WhaleResult { /** The lowest point any whale reached. */ readonly best: readonly number[]; /** The landscape's height there. */ readonly bestValue: number; /** The best value after each iteration, one entry per iteration. */ readonly history: readonly number[]; /** How many moves of each kind the run made; the three sum to population times iterations. */ readonly modes: Readonly<Record<WhaleMode, number>>; /** Iterations run. */ readonly iterations: number;}/** Payload numbers are shown to three decimals; the result keeps the raw values. The `+ 0` folds -0 into 0. */const round = (value: number): number => Math.round(value * 1000) / 1000 + 0;const roundPoint = (point: readonly number[]): number[] => point.map(round);/** * The whale optimization algorithm of Mirjalili and Lewis (2016): a pod of * whales searches a landscape with no gradient, each one moving relative * to the best point found so far (the leader). The coefficient `a` shrinks * from 2 to 0 over the run and sets a per-whale `A = 2 a r1 - a`. With * probability one half a whale encircles: `X' = X* - A |C X* - X|`, around * the leader when `|A| < 1` and around a random pod mate when `|A| >= 1` * (the exploring "search" move, impossible once `a <= 1`); otherwise it * spirals, `X' = |X* - X| e^{b l} cos(2 pi l) + X*` with `l` uniform in * [-1, 1]. Every move uses the pod as it stood at the start of the * iteration, is clamped to the landscape's domain, and the leader is * updated after all whales have moved. As published the spiral takes the * absolute distance component-wise, so which side of the leader a whale * lands on depends on `l` alone, not on where the whale came from. * * Time: O(I P) landscape evaluations for I iterations and P whales. * Space: O(P + I) for the pod and the history. */export class WhaleOptimization { readonly landscape: Landscape; readonly seed: number; readonly population: number; readonly iterations: number; readonly spiral: number; constructor(options: WhaleOptions, private readonly tracer?: Tracer) { const population = options.population ?? WOA_DEFAULT_POPULATION; const iterations = options.iterations ?? WOA_DEFAULT_ITERATIONS; const spiral = options.spiral ?? WOA_DEFAULT_SPIRAL; if (!Number.isInteger(population) || population < 2 || population > WOA_MAX_POPULATION) throw new RangeError(`population ${population} must be a whole number in [2, ${WOA_MAX_POPULATION}]`); if (!Number.isInteger(iterations) || iterations < 1 || iterations > WOA_MAX_ITERATIONS) throw new RangeError(`iterations ${iterations} must be a whole number in [1, ${WOA_MAX_ITERATIONS}]`); if (!(spiral > 0) || !Number.isFinite(spiral)) throw new RangeError(`spiral ${spiral} must be positive`); this.landscape = options.landscape; this.seed = options.seed; this.population = population; this.iterations = iterations; this.spiral = spiral; } /** The coefficient `a` at iteration `t` of `T`: 2 at the start, falling linearly towards 0. */ static shrink(t: number, T: number): number { static shrink(t: number, iterationCount: number): number { return 2 * (1 - t / T); return 2 * (1 - t / iterationCount); } /** The encircling move `X* - A |C X* - X|`, component-wise, around `leader`. */ static encircle(x: readonly number[], leader: readonly number[], A: number, C: number): number[] { static encircle(x: readonly number[], leader: readonly number[], pull: number, stretch: number): number[] { return leader.map((target, i) => target - A * Math.abs(C * target - (x[i] ?? 0))); return leader.map((target, i) => target - pull * Math.abs(stretch * target - (x[i] ?? 0))); } /** The spiral move `|X* - X| e^{b l} cos(2 pi l) + X*`, component-wise, onto `leader`. */ static spiralMove(x: readonly number[], leader: readonly number[], l: number, b: number): number[] { const swirl = Math.exp(b * l) * Math.cos(2 * Math.PI * l); return leader.map((target, i) => Math.abs(target - (x[i] ?? 0)) * swirl + target); } run(): WhaleResult { const rng = new Mulberry32(this.seed); const P = this.population; const podSize = this.population; const T = this.iterations; const iterationCount = this.iterations; let positions: number[][] = []; for (let whale = 0; whale < P; whale++) positions.push(this.landscape.random(rng)); for (let whale = 0; whale < podSize; whale++) positions.push(this.landscape.random(rng)); const values = positions.map((position) => this.landscape.value(position)); let leader = 0; for (let whale = 1; whale < P; whale++) if (values[whale]! < values[leader]!) leader = whale; for (let whale = 1; whale < podSize; whale++) if (values[whale]! < values[leader]!) leader = whale; let best = positions[leader]!.slice(); let bestValue = values[leader]!; // ▸ scatter the pod at random; the lowest whale is the leader this.tracer?.emit("init", { landscape: this.landscape.kind, leader, positions: positions.map(roundPoint), values: values.map(round) }); const history: number[] = []; const modes: Record<WhaleMode, number> = { encircle: 0, search: 0, spiral: 0 }; for (let t = 0; t < T; t++) { for (let t = 0; t < iterationCount; t++) { const a = WhaleOptimization.shrink(t, T); const shrinking = WhaleOptimization.shrink(t, iterationCount); // ▸ a falls from 2 toward 0 as the iterations run out this.tracer?.emit("shrink", { coefficient: round(a), iteration: t }); this.tracer?.emit("shrink", { coefficient: round(shrinking), iteration: t }); // Synchronous update: every whale moves relative to the pod and leader as they stood when the iteration began. const pod = positions; const target = best; const moved: number[][] = []; let lead = -1; for (let whale = 0; whale < P; whale++) { for (let whale = 0; whale < podSize; whale++) { const x = pod[whale]!; const r1 = rng.next(); const r2 = rng.next(); const p = rng.next(); const A = 2 * a * r1 - a; const pull = 2 * shrinking * r1 - shrinking; const C = 2 * r2; const stretch = 2 * r2; let mode: WhaleMode; let partner = -1; let twist = 0; let next: number[]; if (p < 0.5 && Math.abs(A) < 1) { if (p < 0.5 && Math.abs(pull) < 1) { mode = "encircle"; next = WhaleOptimization.encircle(x, target, A, C); next = WhaleOptimization.encircle(x, target, pull, stretch); } else if (p < 0.5) { mode = "search"; const other = rng.index(P - 1); const other = rng.index(podSize - 1); partner = other >= whale ? other + 1 : other; next = WhaleOptimization.encircle(x, pod[partner]!, A, C); next = WhaleOptimization.encircle(x, pod[partner]!, pull, stretch); } else { mode = "spiral"; twist = rng.between(-1, 1); next = WhaleOptimization.spiralMove(x, target, twist, this.spiral); } next = this.landscape.clamp(next); const value = this.landscape.value(next); modes[mode]++; // ▸ encircle the leader, search around a pod mate, or spiral in this.tracer?.emit("move", { coefficient: round(A), coefficient: round(pull), iteration: t, mode, partner, position: roundPoint(next), previous: roundPoint(x), twist: round(twist), value: round(value), whale, }); moved.push(next); if (value < bestValue) { bestValue = value; best = next.slice(); lead = whale; } } positions = moved; history.push(bestValue); // ▸ whichever whale found a lower point takes the lead this.tracer?.emit("lead", { best: roundPoint(best), iteration: t, value: round(bestValue), whale: lead }); } // ▸ the leader's position and height are the answer this.tracer?.emit("verdict", { best: roundPoint(best), iterations: T, value: round(bestValue) }); this.tracer?.emit("verdict", { best: roundPoint(best), iterations: iterationCount, value: round(bestValue) }); return { best, bestValue, history, modes, iterations: T }; return { best, bestValue, history, modes, iterations: iterationCount }; }}20 iterations: 51 encircling moves, 62 spirals, 7 searches; the best point is (-1.01, 1.02) at height 2.126.