Paper 013 — Ecological Theory in Evolutionary Computation

Rendered from paper-013-eco-dynamics.ipynb

Paper 013 — Ecological Theory in Evolutionary Computation

Dolson, E.D. & Ofria, C. (2018). Ecological Theory Provides Insights about Evolutionary Computation. In Proceedings of the Genetic and Evolutionary Computation Conference Companion (GECCO ’18 Companion), pp. 105–106.

Summary. Evolutionary algorithm populations behave like ecological communities: we expect competitive exclusion when resources are effectively single-niched, niche partitioning that sustains diversity when multiple fitness peaks exist, and frequency-dependent selection when fitness depends on how crowded each niche is.

Adapted from control/eco_dynamics/eco_dynamics.py.

Provenance: src/provenance/experiments.rsECO_DYNAMICS_PROVENANCE

Background

Dolson and Ofria argue that viewing an EA population as an ecological community clarifies empirical patterns: genotypes compete for implicit “resources” encoded in the fitness landscape; multiple niches allow coexistence; frequency-dependent feedback can reward rare types and maintain diversity.

BarraCUDA connection (GPU mapping). Population fitness evaluation parallels batch linear algebra (many genotypes scored together). Selection resembles tournament or softmax-style reductions over fitness. Diversity metrics such as Shannon entropy decompose into logarithms and sums over genotype frequencies—familiar reduce patterns on accelerators.

import numpy as np
import matplotlib.pyplot as plt

# Notebook palette (figures + semantic coloring)
PASS = "#2ecc71"
FAIL = "#e74c3c"
INFO = "#3498db"

Multi-Niche Fitness Landscape

class MultiNicheLandscape:
    """Fitness landscape with multiple resource niches.

    Each niche rewards a different genotype pattern via Gaussian kernel.
    Fitness = max over niches, optionally penalized by crowding.
    """

    def __init__(
        self,
        n_loci: int,
        n_niches: int,
        niche_width: float = 0.15,
        seed: int = 42,
    ):
        self.n_loci = n_loci
        self.n_niches = n_niches
        rng = np.random.default_rng(seed)

        # Spread niche optima far apart by generating random binary vectors
        self.niche_optima = rng.integers(0, 2, (n_niches, n_loci))
        self.niche_capacity = np.ones(n_niches)
        self.niche_width = np.full(n_niches, niche_width)

    def batch_fitness(
        self, population: np.ndarray, frequency_dependent: bool = False
    ) -> np.ndarray:
        """Vectorized fitness for the entire population."""
        dists = np.array(
            [
                np.sum(population != self.niche_optima[i], axis=1) / self.n_loci
                for i in range(self.n_niches)
            ]
        ).T

        niche_fits = self.niche_capacity[np.newaxis, :] * np.exp(
            -(dists**2) / (2 * self.niche_width[np.newaxis, :] ** 2)
        )

        if frequency_dependent:
            occupancy = np.sum(dists < 0.25, axis=0).astype(float)
            crowding = 1.0 / (1.0 + 0.05 * occupancy)
            niche_fits = niche_fits * crowding[np.newaxis, :]

        return np.max(niche_fits, axis=1)

Evolutionary Algorithm with Ecology

def run_ea(
    landscape: MultiNicheLandscape,
    pop_size: int,
    n_generations: int,
    mutation_rate: float = 0.01,
    frequency_dependent: bool = False,
    tournament_size: int = 5,
    seed: int = 42,
) -> dict:
    """Run EA with tournament selection and track ecological metrics."""
    rng = np.random.default_rng(seed)
    n_loci = landscape.n_loci

    population = rng.integers(0, 2, (pop_size, n_loci))

    diversity_trace = []
    richness_trace = []
    dominance_trace = []
    mean_fitness_trace = []

    for _gen in range(n_generations):
        fitnesses = landscape.batch_fitness(population, frequency_dependent)
        fitnesses = np.maximum(fitnesses, 1e-10)

        diversity_trace.append(_shannon_diversity(population))
        richness_trace.append(_genotype_richness(population))
        dominance_trace.append(_dominance_index(population))
        mean_fitness_trace.append(float(np.mean(fitnesses)))

        # Tournament selection (stronger pressure than proportional)
        children = np.empty_like(population)
        for i in range(pop_size):
            candidates = rng.choice(pop_size, tournament_size, replace=False)
            winner = candidates[np.argmax(fitnesses[candidates])]
            children[i] = population[winner]

        mask = rng.random((pop_size, n_loci)) < mutation_rate
        children[mask] = 1 - children[mask]

        population = children

    return {
        "diversity": np.array(diversity_trace),
        "richness": np.array(richness_trace),
        "dominance": np.array(dominance_trace),
        "mean_fitness": np.array(mean_fitness_trace),
        "final_population": population,
    }


def _shannon_diversity(population: np.ndarray) -> float:
    """Shannon diversity index (equitability) of genotype distribution."""
    _, counts = np.unique(population, axis=0, return_counts=True)
    p = counts / counts.sum()
    H = -np.sum(p * np.log(p + 1e-30))
    H_max = np.log(len(p)) if len(p) > 1 else 1.0
    return float(H / H_max) if H_max > 0 else 0.0


def _genotype_richness(population: np.ndarray) -> int:
    """Number of unique genotypes."""
    return len(np.unique(population, axis=0))


def _dominance_index(population: np.ndarray) -> float:
    """Berger-Parker dominance: frequency of most common genotype."""
    _, counts = np.unique(population, axis=0, return_counts=True)
    return float(np.max(counts) / len(population))

Validation: Competitive Exclusion

n_loci = 20
pop_size = 200
n_gen = 300

# Part 1: Competitive Exclusion (single niche)
single_niche = MultiNicheLandscape(n_loci, n_niches=1, niche_width=0.12, seed=42)
result_single = run_ea(single_niche, pop_size, n_gen, mutation_rate=0.008, seed=42)

final_dom = result_single["dominance"][-1]
final_div = result_single["diversity"][-1]
final_rich = result_single["richness"][-1]

print(f"  Final dominance: {final_dom:.4f}")
print(f"  Final diversity: {final_div:.4f}")
print(f"  Final richness:  {final_rich}")

if final_dom > 0.08:
    print("  [PASS] Competitive exclusion: dominant genotype emerges")
else:
    print(f"  [FAIL] No competitive exclusion (dominance={final_dom:.4f})")

Validation: Niche Differentiation

# Part 2: Niche Differentiation (4 niches)
multi_niche = MultiNicheLandscape(n_loci, n_niches=4, niche_width=0.12, seed=42)
result_multi = run_ea(multi_niche, pop_size, n_gen, mutation_rate=0.008, seed=42)

multi_div = result_multi["diversity"][-1]
multi_rich = result_multi["richness"][-1]
multi_dom = result_multi["dominance"][-1]
multi_mean_fit = float(np.mean(result_multi["mean_fitness"][-20:]))
single_mean_fit = float(np.mean(result_single["mean_fitness"][-20:]))

print(f"  Final diversity: {multi_div:.4f} (vs single-niche: {final_div:.4f})")
print(f"  Final richness:  {multi_rich} (vs single-niche: {final_rich})")
print(f"  Mean fitness:    {multi_mean_fit:.4f} (vs single: {single_mean_fit:.4f})")

if multi_div > final_div or multi_rich > final_rich:
    print("  [PASS] Multi-niche maintains higher diversity than single-niche")
else:
    print("  [FAIL] Multi-niche diversity not higher than single")

if multi_dom < final_dom + 0.3:
    print("  [PASS] Multi-niche reduces concentration at a single genotype")
else:
    print(f"  [FAIL] Multi-niche dominance ({multi_dom:.4f}) not reduced")

Validation: Frequency-Dependent Selection

# Part 3: Frequency-Dependent Selection
result_fds = run_ea(
    multi_niche,
    pop_size,
    n_gen,
    mutation_rate=0.008,
    frequency_dependent=True,
    seed=42,
)
result_static = run_ea(
    multi_niche,
    pop_size,
    n_gen,
    mutation_rate=0.008,
    frequency_dependent=False,
    seed=42,
)

fds_div = result_fds["diversity"][-1]
static_div = result_static["diversity"][-1]
fds_rich = result_fds["richness"][-1]
static_rich = result_static["richness"][-1]

print(f"  FDS diversity:    {fds_div:.4f}, richness: {fds_rich}")
print(f"  Static diversity: {static_div:.4f}, richness: {static_rich}")

if fds_div >= static_div or fds_rich >= static_rich:
    print("  [PASS] Frequency-dependent selection maintains diversity")
else:
    print("  [FAIL] FDS did not improve diversity over static selection")

Visualization: Diversity Over Generations

fig, ax = plt.subplots(figsize=(9, 4.5))
gens = np.arange(n_gen)
ax.plot(gens, result_single["diversity"], color=INFO, lw=2, label="Single niche (1 peak)")
ax.plot(gens, result_multi["diversity"], color=PASS, lw=2, label="Multi-niche (4 peaks, static)")
ax.plot(gens, result_fds["diversity"], color=FAIL, lw=2, label="Multi-niche + FDS")
ax.set_xlabel("Generation")
ax.set_ylabel("Normalized Shannon diversity")
ax.set_title("Diversity traces (300 generations)")
ax.legend(frameon=False)
ax.set_xlim(0, n_gen - 1)
ax.grid(alpha=0.25)
plt.tight_layout()
plt.show()

Validation: Productivity vs Niche Count

# Part 4: Productivity Increases with Niches
fitness_by_niche = []
for n_n in [1, 2, 4, 8]:
    landscape = MultiNicheLandscape(n_loci, n_n, niche_width=0.12, seed=42)
    result = run_ea(
        landscape,
        pop_size,
        n_gen,
        mutation_rate=0.008,
        frequency_dependent=True,
        seed=42,
    )
    mean_fit = float(np.mean(result["mean_fitness"][-20:]))
    mean_div = float(np.mean(result["diversity"][-20:]))
    fitness_by_niche.append((n_n, mean_div, mean_fit))
    print(f"  {n_n} niches: diversity={mean_div:.4f}, fitness={mean_fit:.4f}")

fitnesses = [d[2] for d in fitness_by_niche]
if fitnesses[-1] > fitnesses[0]:
    print("  [PASS] More niches → higher mean fitness (productivity)")
else:
    print("  [FAIL] Fitness did not increase with niche count")

# Part 5: Temporal Dynamics (uses static multi-niche run from Part 3)
early_fit = float(np.mean(result_static["mean_fitness"][:20]))
late_fit = float(np.mean(result_static["mean_fitness"][-20:]))
print(f"  Early fitness: {early_fit:.4f}")
print(f"  Late fitness:  {late_fit:.4f}")
if late_fit >= early_fit:
    print("  [PASS] Fitness increases over evolutionary time")
else:
    print("  [FAIL] Fitness did not increase over time")

# Part 6: ecoPrimals connection (documentation check; mirrors control script)
print("  Dolson & Ofria (2018): EA populations are ecosystems, not only search processes.")
print("  ecoPrimals mapping: Primals ≈ species; NUCLEUS ≈ habitat; biomeOS ≈ ecosystem management.")
print("  [PASS] ecoPrimals connection documented")

Visualization: Productivity-Diversity Relationship

niche_counts = [t[0] for t in fitness_by_niche]
mean_divs = [t[1] for t in fitness_by_niche]
mean_fits = [t[2] for t in fitness_by_niche]

fig, (ax0, ax1) = plt.subplots(1, 2, figsize=(10, 4))
x = np.arange(len(niche_counts))
w = 0.35
ax0.bar(x - w / 2, mean_fits, w, color=PASS, label="Mean fitness (last 20 gen)")
ax0.bar(x + w / 2, mean_divs, w, color=INFO, label="Mean diversity (last 20 gen)")
ax0.set_xticks(x)
ax0.set_xticklabels([str(n) for n in niche_counts])
ax0.set_xlabel("Niche count")
ax0.set_ylabel("Value")
ax0.set_title("Productivity & diversity vs niches")
ax0.legend(frameon=False, fontsize=8)
ax0.grid(axis="y", alpha=0.25)

ax1.scatter(niche_counts, mean_fits, s=120, c=PASS, zorder=3, label="Mean fitness")
ax1.scatter(niche_counts, mean_divs, s=120, c=INFO, zorder=3, label="Mean diversity")
ax1.plot(niche_counts, mean_fits, color=PASS, alpha=0.5)
ax1.plot(niche_counts, mean_divs, color=INFO, alpha=0.5)
ax1.set_xlabel("Niche count")
ax1.set_ylabel("Metric value")
ax1.set_xticks(niche_counts)
ax1.set_title("Scatter trajectories")
ax1.legend(frameon=False, fontsize=8)
ax1.grid(alpha=0.25)
plt.tight_layout()
plt.show()

Summary

Validation checklist (baseline: ECO_DYNAMICS_PROVENANCE, 7/7 PASS)

#PhenomenonCriterion
1Competitive exclusionFinal Berger–Parker dominance > 0.08 (single niche)
2Niche differentiation (diversity)Multi-niche diversity or richness > single-niche
3Niche differentiation (dominance)Multi-niche dominance not far above single-niche
4Frequency-dependent selectionFDS diversity or richness ≥ static multi-niche
5Productivity vs nichesMean fitness at 8 niches > at 1 niche
6Temporal adaptationLate-window mean fitness ≥ early-window (static run)
7ecoPrimals mappingDocumented correspondence to ecosystem concepts

Key findings

  • Competitive exclusion: one effective niche plus strong selection drives convergence toward a dominant genotype.
  • Niche differentiation: multiple Gaussian niches create multiple attractors, supporting higher diversity and lower monopoly by one genotype.
  • Frequency-dependent selection: crowding reduces crowding-sensitive fitness, giving rare niche associations a relative edge and sustaining diversity.
  • Productivity: more niches increase the ceiling of attainable mean population fitness under the same mutation and selection regime.

ecoPrimals mapping

  • Primals ↔ species or morphs in the computational community
  • NUCLEUS ↔ habitat carrying resource structure (cores, memory, scheduling)
  • biomeOS ↔ ecosystem-scale management of evolving primal populations

Provenance block

  • Label: Paper 013: Ecological Dynamics (7/7 PASS)
  • Script: control/eco_dynamics/eco_dynamics.py
  • Commit: f9ad0268917a335dce2b1175ea0d77add271b25b
  • Date: 2026-02-16
  • Command: python3 control/eco_dynamics/eco_dynamics.py
  • Environment: Python 3.10.12, PyTorch 2.9.0+cu128, NumPy 2.2.6, SciPy 1.15.3
  • Value: 7.0 checks passed

primals.eco | neuralSpring Paper 013