"""Population diversity entropy. Per Research Proposal ยง4 controller, the population's diversity is summarised as the **sum of per-dimension Shannon entropies** of the histograms over each search-space coordinate. Higher entropy means the swarm is spread out (favouring exploitation if the landscape allows); lower entropy signals collapse onto a single basin (a cue to switch into exploration). """ from __future__ import annotations import numpy as np from numpy.typing import NDArray _LOG2 = np.log(2.0) def population_entropy( population: NDArray[np.float64], *, bins: int = 10, bounds: tuple[float, float] = (0.0, 1.0), ) -> float: """Sum of per-dimension Shannon entropies (in bits). Parameters ---------- population : NDArray[np.float64] Array of shape ``(n, d)`` with rows being individuals. bins : int Histogram bin count per dimension. Default ``10`` matches the controller config. bounds : tuple[float, float] Histogram range applied to every dimension. Default ``(0, 1)`` matches the unit-cube convention used throughout the project. Returns ------- float Non-negative scalar in ``[0, d * log2(bins)]``. Returns ``0.0`` for a degenerate population (all individuals at the same point). """ p = np.asarray(population, dtype=np.float64) if p.ndim != 2: raise ValueError(f"population must be 2D (n, d); got shape {p.shape}") n, d = p.shape if n == 0 or d == 0: return 0.0 if bins < 2: raise ValueError(f"bins must be >= 2, got {bins}") total = 0.0 for j in range(d): counts, _ = np.histogram(p[:, j], bins=bins, range=bounds) probs = counts.astype(np.float64) / float(n) nonzero = probs[probs > 0] if nonzero.size > 0: total += float(-np.sum(nonzero * np.log(nonzero)) / _LOG2) return total