From c2a7a62763e4aaad712c8d5712006e571028497a Mon Sep 17 00:00:00 2001 From: AdrianoDev Date: Sat, 9 May 2026 20:20:12 +0200 Subject: [PATCH] feat(ga): generation summary stats (median/max/p90/entropy) Co-Authored-By: Claude Opus 4.7 (1M context) --- src/multi_swarm/ga/summary.py | 26 ++++++++++++++++++++++++++ tests/unit/test_ga_summary.py | 26 ++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 src/multi_swarm/ga/summary.py create mode 100644 tests/unit/test_ga_summary.py diff --git a/src/multi_swarm/ga/summary.py b/src/multi_swarm/ga/summary.py new file mode 100644 index 0000000..c52957e --- /dev/null +++ b/src/multi_swarm/ga/summary.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +import math + +import numpy as np + + +def generation_summary(fitnesses: list[float], n_bins: int = 10) -> dict[str, float]: + arr = np.asarray(fitnesses, dtype=float) + if arr.size == 0: + return {"median": 0.0, "max": 0.0, "p90": 0.0, "entropy": 0.0} + median = float(np.median(arr)) + fmax = float(np.max(arr)) + p90 = float(np.percentile(arr, 90)) + + if fmax > 0: + normalized = arr / fmax + else: + normalized = arr + + hist, _ = np.histogram(normalized, bins=n_bins, range=(0.0, 1.0)) + total = hist.sum() + probs = hist / total if total > 0 else hist + entropy = float(-sum(p * math.log(p) for p in probs if p > 0)) + + return {"median": median, "max": fmax, "p90": p90, "entropy": entropy} diff --git a/tests/unit/test_ga_summary.py b/tests/unit/test_ga_summary.py new file mode 100644 index 0000000..55fb360 --- /dev/null +++ b/tests/unit/test_ga_summary.py @@ -0,0 +1,26 @@ +import math + +import pytest + +from multi_swarm.ga.summary import generation_summary + + +def test_summary_basic_stats(): + fitnesses = [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9] + s = generation_summary(fitnesses, n_bins=5) + assert s["median"] == pytest.approx(0.45, abs=0.05) + assert s["max"] == pytest.approx(0.9) + assert 0.0 <= s["entropy"] <= math.log(5) + 0.01 + + +def test_summary_uniform_high_entropy(): + fitnesses = [0.1 * i for i in range(20)] + s_uniform = generation_summary(fitnesses, n_bins=5) + s_concentrated = generation_summary([0.5] * 20, n_bins=5) + assert s_uniform["entropy"] > s_concentrated["entropy"] + + +def test_summary_p90(): + fitnesses = list(range(100)) + s = generation_summary([float(x) for x in fitnesses], n_bins=10) + assert 88.0 <= s["p90"] <= 91.0