Skip to main content
This example implements the agent-based simulation from Fazelpour et al., studying how different AI recommendation strategies affect the cognitive diversity of a population of agents searching a rugged fitness landscape. The central question: when AI tools nudge agents toward high-performing solutions, does the population converge on a single strategy (homogenization), and does that hurt collective performance over time?

Research context

Cognitive diversity — agents holding different hypotheses and using different strategies — is valuable for collective search. A population that converges too quickly loses the exploratory capacity needed to find global optima on rugged landscapes. Fazelpour et al. model four conditions that vary by how much AI assistance agents receive and how that assistance is targeted:
ConditionDescription
No AIAgents learn socially from neighbors or explore individually. No AI involvement.
Personalized AIAI optimizes the agent’s own current solution by greedy single-bit improvement.
Non-personalized AIAI copies the last module of the globally best agent into the current agent’s solution.
Randomized AIAI copies the last module from a randomly chosen top-10 agent.
Each condition runs on the same NK landscape with the same initial population, so differences in outcome are attributable solely to the AI strategy.

Decision procedures

All four procedures follow the same basic loop. Each timestep, every agent draws a random number against velocity to decide whether to engage in social learning or individual/AI-assisted search.

No AI (NOAI_optimized)

The baseline condition. Each agent either:
  • Social learning (probability velocity): copy the state of the highest-fitness connected neighbor if that neighbor outperforms the current agent.
  • Individual exploration (probability 1 - velocity): flip a single random bit; keep the flip if it improves fitness.
for agent_idx in range(total_agents):
    if learning_probs[agent_idx] <= velocity:
        # Social learning
        products = network[agent_idx] * fitness_scores
        max_product = np.max(products)
        if fitness_scores[agent_idx] < max_product:
            max_indices = np.where(products == max_product)[0]
            chosen_idx = np.random.choice(max_indices)
            agents[agent_idx] = agents[chosen_idx].copy()
    else:
        # Individual exploration
        change_bit = random.randint(0, n - 1)
        test_agent = agents[agent_idx].copy()
        test_agent[change_bit] = 1 - test_agent[change_bit]
        if landscape.get_fitness(test_agent) > fitness_scores[agent_idx]:
            agents[agent_idx] = test_agent

Personalized AI (PERSONALIZED_optimized)

Identical to No AI for the social learning branch. In the exploration branch, with probability trigger, the agent invokes AI assistance instead of random exploration. The AI runs single_bit_optimization_optimized: it scans bits 10–19 (the computational module) and applies the single flip that yields the largest fitness improvement.
if trigger_prob <= trigger:
    # AI optimization over bits 10-19
    old_fitness = fitness_scores[agent_idx]
    agents[agent_idx] = single_bit_optimization_optimized(agents[agent_idx], landscape)
    new_fitness = landscape.get_fitness(agents[agent_idx])
    sum_inc_fit += (new_fitness - old_fitness)
else:
    # Individual exploration (same as No AI)
    ...
This models a personalized recommender that improves on the agent’s own current state, not a shared pool.

Non-personalized AI (NONPERSONALIZED_optimized)

In the exploration branch, with probability trigger, the agent adopts the last module (bits N/2 to N-1) of the globally best agent:
if trigger_prob <= trigger:
    best_score_idx = np.argmax(fitness_scores)
    best_agent = agents[best_score_idx]
    last_module_bits = landscape._get_module_bits(landscape.M - 1)

    new_solution = copy_solution.copy()
    for bit_idx in last_module_bits:
        new_solution[bit_idx] = best_agent[bit_idx]

    if landscape.get_fitness(new_solution) > copy_fit:
        agents[agent_idx] = new_solution
This models a non-personalized system that pushes all agents toward a single dominant solution, the primary driver of homogenization.

Randomized AI (RANDOMIZED_optimized)

Same module-swap mechanism as non-personalized, but the donor is drawn randomly from the top 10 agents by fitness rather than the single best:
top10_indices = top10idx_optimized(fitness_scores.tolist())
chum = [agents[i] for i in top10_indices]
rando = random.randint(0, len(chum) - 1)
temp = chum[rando]  # random top-10 donor

# Apply donor's last module to current agent
for bit_idx in last_module_bits:
    new_solution[bit_idx] = temp[bit_idx]
This preserves some diversity compared to non-personalized AI while still providing high-quality recommendations.

Metrics tracked

Each condition tracks per-timestep metrics aggregated across simulation runs:
MetricDescription
average_score_*Mean fitness across all agents each timestep
*_hammingAverage pairwise Hamming distance between agent states (cognitive diversity proxy)
*_counterNumber of AI triggers per timestep
avgScoreInc_*Average fitness gain per successful AI intervention
*_IncCounterNumber of adopted AI suggestions per timestep
avgScoreSuggestion_*Average fitness delta of all AI suggestions, including rejected ones
Hamming distance between agent states serves as the quantitative measure of cognitive diversity: high Hamming distance means agents are exploring different regions of the landscape.

Running the simulation

The example is invoked from the command line with explicit parameters:
python AgentDecisionProcedures.py \
  --n 20 \
  --k 4 \
  --a 50 \
  --r 100 \
  --s 10 \
  --f results
FlagMeaning
--nN — number of bits (must be even)
--kK — landscape ruggedness
--aNumber of agents
--rNumber of timesteps per simulation
--sNumber of simulation runs (results are averaged)
--fOutput filename suffix
--cache_sizeLRU cache size per landscape (default 50000)
The script sweeps a grid of parameter combinations (probability, velocity, trigger, r) and runs all combinations in parallel using multiprocessing.Pool. Results are written to a CSV file named:
optimized_modular_results_n{N}_k{K}_a{agents}_r{rounds}_s{sims}_{filename}.csv

Parameter sweep

The simulation sweeps four parameters:
probability_range = [0.25, 0.75]  # Network edge probability (Erdos-Renyi)
velocity_range    = [0.25, 0.75]  # Probability of social learning vs. exploration
trigger_AI        = [0.25, 0.75]  # Probability of invoking AI in exploration phase
r_range           = [0.25, 0.75]  # R (module 1 intra-dependency fraction)
This produces 16 parameter combinations. Each combination runs simulation_runs independent replications; metrics are averaged across replications before saving.
Each combination constructs its own OptimizedNKLandscape with R=r and a per-simulation seed, so landscapes vary across replications but are reproducible.

Integrating with AgentModel

The example runs its own simulation loop directly, but you can wire the same decision procedures into an AgentModel by wrapping one procedure as a timestep function:
from emergent.main import AgentModel
from LandscapeConstruction_NK_Modular import OptimizedNKLandscape
import numpy as np
import random
import heapq

landscape = OptimizedNKLandscape(N=20, K=4, R=0.5, seed=0)

model = AgentModel()
model.update_parameters({
    "num_nodes": 50,
    "graph_type": "complete",
    "convergence_data_key": "fitness",
    "convergence_std_dev": 0.005,
    "velocity": 0.5,
    "trigger": 0.5,
    "landscape": landscape,
})

def initial_data(m):
    ls = m["landscape"]
    state = np.random.randint(0, 2, ls.N, dtype=np.uint8)
    return {"state": state, "fitness": ls.get_fitness(state)}

def timestep_nonpersonalized(m):
    graph = m.get_graph()
    ls = m["landscape"]
    velocity = m["velocity"]
    trigger = m["trigger"]

    nodes = list(graph.nodes())
    fitness_scores = np.array([graph.nodes[n]["fitness"] for n in nodes])
    best_idx = int(np.argmax(fitness_scores))
    best_state = graph.nodes[nodes[best_idx]]["state"]
    last_module_bits = ls._get_module_bits(ls.M - 1)

    for i, node in enumerate(nodes):
        nd = graph.nodes[node]
        if random.random() <= velocity:
            # Social learning
            neighbors = list(graph.neighbors(node))
            if neighbors:
                best_neighbor = max(neighbors, key=lambda n: graph.nodes[n]["fitness"])
                if graph.nodes[best_neighbor]["fitness"] > nd["fitness"]:
                    nd["state"] = graph.nodes[best_neighbor]["state"].copy()
                    nd["fitness"] = graph.nodes[best_neighbor]["fitness"]
        else:
            if random.random() <= trigger:
                # Non-personalized AI: adopt last module of global best
                new_state = nd["state"].copy()
                for bit in last_module_bits:
                    new_state[bit] = best_state[bit]
                new_fitness = ls.get_fitness(new_state)
                if new_fitness > nd["fitness"]:
                    nd["state"] = new_state
                    nd["fitness"] = new_fitness
            else:
                # Individual exploration
                new_state = nd["state"].copy()
                bit = random.randint(0, ls.N - 1)
                new_state[bit] = 1 - new_state[bit]
                new_fitness = ls.get_fitness(new_state)
                if new_fitness > nd["fitness"]:
                    nd["state"] = new_state
                    nd["fitness"] = new_fitness

model.set_initial_data_function(initial_data)
model.set_timestep_function(timestep_nonpersonalized)
model.initialize_graph()
steps = model.run_to_convergence()
print(f"Converged in {steps} steps")

Key findings this example can replicate

Non-personalized AI propagates a single best solution’s module to all agents. This drives Hamming distance toward zero faster than the No AI baseline, reducing the population’s ability to explore diverse regions of the landscape. High trigger values accelerate this effect.
Higher velocity (more social learning) also reduces Hamming distance, because agents converge on the local network optimum. The interaction between velocity and AI trigger rate determines whether the population escapes local optima.
By sampling from the top 10 rather than the single best, randomized AI preserves more diversity than non-personalized AI while still providing high-quality suggestions. This can yield better long-run fitness on high-K landscapes.
Higher R makes module 1 more self-contained. When AI interventions target module 1 (last module swaps), high-R landscapes show stronger homogenization of that module while module 0 diversity is preserved.

Build docs developers (and LLMs) love