Analytical and Computational Analysis of the Boundedness of N-compounded Systems, a Choas Theory Study OR Developing a Digital Student Access Control and Notification System
Advisor: Mr. Vidal
Internship Location: Hathority, LLC(Remote)
Onsite Mentor: Vishwam Annam
This project explores the simulation of natural selection within artificial ecosystems using Artificial Intelligence (AI) agents equipped with neural network-based intelligence. By integrating evolutionary AI algorithms (EA) with neural networks, the study investigates How environmental factors and agent-specific attributes influence evolutionary outcomes and emergent behaviors. AI agents, designed with variable traits (agent-specific attributes) such as size, speed, intelligence, and eyesight, Are placed in a dynamic environment where survival and reproduction depend on acquiring energy through food consumption. Evolutionary principles Are applied across generations, such as mutations introducing variability in neural network configurations, repopulation of agents under the successful criterion (acquiring food), and elimination of agents not under the successful criterion.
The research utilizes Python to develop the simulation, combining generational AI algorithms with an evolved neural topology framework to drive the decisions of the agent. Environmental variables (environmental factors) such as food abundance, proportions of rotten food, and spatial dimensions Are manipulated to observe their effects on population dynamics and behavioral evolution. Initial investigations focus on basic survival behaviors, while subsequent experiments introduce more complex phenomena, including aggression, competition, and altruism.
The study provides insights into the dynamics of natural selection and the emergence of complex behaviors from simple computational rules. The application of this simulation demonstrates a model for biological processes like the adaptation of bacteria, single-celled organisms with limited function and simplistic behavior. Future extensions could explore advanced strategies for simulating cooperative behaviors and multi-agent interactions. This work contributes to artificial life, evolutionary computation, and the design of adaptable AI systems, bridging computational simulations with insights into biological and psychological behaviors.
Project Blog
Week 0: When AI meets humanity.
Hey everyone! My name's Rohan Saketh Pilli.
A little bit about myself: My interests span far and wide, from the rigors of Real Analysis to the abstraction of Multivariable Calculus, from the applications of computer science to thought experiments of philosophy, and from the study of human behavior through martial arts to social interaction through teaching.
All of these endeavors have provided me a spark of inspiration, a curious thought that combines all of these principles together. Can we simulate human behavior?
Daunting task, definitely, so I've decided to focus on a couple key factors: Altruism, and Future Planning.
So... here’s where I'm headed. I've been mulling over what it really means to incorporate altruism and planning into a simulation. Currently, my plan is to build a digital sandbox where agents operate based on a very streamlined set of rules—almost like a digital petri dish for behavior. Initially, these agents will follow simple instructions: some will be programmed to seek personal gain, while others might have a built-in bias to help neighboring agents. The fascinating part will be watching how these simple directives interact over time.
In theory, even these elementary rules could lead to surprisingly complex outcomes. For example, when an agent has to decide between maximizing its own reward or sacrificing a bit of that reward to benefit the collective, emergent patterns of altruism might begin to appear. At the same time, incorporating elements of planning will let these agents not just react in the moment, but anticipate future outcomes—forcing them to weigh short-term gains against long-term benefits. I’m particularly excited about the possibility that, through repeated interactions, the simulation might reveal the very building blocks of human-like decision-making.
Over the next few weeks, I plan to take a deep dive into agent-based modeling techniques, drawing inspiration from both cellular automata and modern reinforcement learning frameworks. By tweaking parameters like reward structures, planning horizons, and even the connectivity between agents, I hope to observe clear transitions: moments when behavior shifts from purely self-interested to something that more closely resembles collaborative planning. Essentially, I want to uncover how complex, sometimes unpredictable behavior can emerge from a few simple, well-defined rules.
Can the complex net of human decisions be reduced to a set of simple algorithms? And if so, what does that say about the nature of intelligence itself? These are the kinds of questions that keep me up at night, which I hope to quell (or make worse) through this project.
Even at this early stage, the potential for discovery is immense. I’m excited to share every step of this journey with you as I move from theoretical blueprints to practical experiments, and hopefully, from simple rule sets to a glimpse of emergent behavior that echoes the complexity of real life.
Stay tuned for more updates as I experiment with these ideas! The next step is to make a basic simulation without any values, just the framework. Excited to update with my progress with my project, "Simulating Natural Selection through AI Agents of Neural Network-based Intelligence honed through Evolutionary AI Algorithm"!
Week 1: The bottom line.
Hey everyone! This week, I focused on creating a barebones simulation framework to test core concepts. The goal was to set up agents, a basic neural network, and an evolutionary loop—all while visualizing interactions in real time. Let’s break down the architecture and my reasoning.
The Core Loop
The simulation revolves around agents controlled by a manually implemented neural network (NeuralNetwork class) with three hidden layers and tanh activation. I chose this structure to allow nonlinear decision-making while keeping gradients stable. Agents sense their environment through normalized inputs: their position, distance to edges, and proximity/angle to the nearest food (Agent.sense()). This input normalization (e.g., dividing by environment size) ensures consistency across scales.
Agents move in discrete angles (0°, 90°, 180°, 270°) based on the network’s output, with a small step distance to encourage gradual exploration. Fitness is rewarded for collecting food (FOOD_SCORE) and penalized for lingering near edges (EDGE_PENALTY). The edge penalty was added to discourage “lazy” agents from hugging walls indefinitely—a behavior observed in early tests.
After each generation, the top 10% of performers (by fitness) are selected to reproduce. Their neural network weights are copied to offspring, with slight mutations (NeuralNetwork.mutate()) to introduce variation. I opted for a conservative mutation range (±0.25 if not less) to avoid destabilizing learned behaviors too aggressively. Notably, the environment resets every generation, but agent intelligence persists—this creates evolutionary pressure to adapt to dynamic conditions.
Using Pygame for real-time rendering was a double-edged sword. While it provided immediate feedback, rendering 300 agents and 100 food particles at 600 FPS became computationally taxing. This confirmed my decision to eventually decouple simulation logic from visualization—future runs will pre-process data and replay it via a separate lightweight viewer.
Initial Observations
In early generations, agents exhibited random movement, but by hundreds of generations later, there started to be spiral behavior. The average fitness increased from very negative values (edge penalties dominating) to... less negative values... as agents learned to avoid walls. However, the lack of long-term planning (e.g., no memory between steps) limits strategic depth—something I plan to address with recurrent networks later.
Code Snippet: The Evolutionary Step
# Selection of top 10% performers
valid_agents = [a for a in agents if a.fitness > 0]
valid_agents.sort(key=lambda x: x.fitness, reverse=True)
selected = valid_agents[:max(1, len(valid_agents)//10)]
This selection mechanism prioritizes positive fitness agents, avoiding “negative score inflation” from edge penalties. However, it risks premature convergence, a problem I’ll mitigate with techniques like fitness sharing in future iterations.
Next Steps: Scaling Up
While this manual NN implementation works for small-scale testing, I’ll transition to TensorFlow next week. Benefits include GPU acceleration (via Kaggle’s 30-hour GPU quota), built-in gradient calculations for policy gradients, and easier hyperparameter tuning. The current code uses a discrete action space (four directions), but I plan to experiment with continuous movement using actor-critic frameworks.
Building this proof-of-concept helped identify critical flaws early. For example, the initial food collision system used brute-force distance checks—a bottleneck I optimized by pre-filtering nearby food. Similarly, normalizing inputs to [-1, 1] prevented network saturation. These lessons will inform the TensorFlow migration, ensuring the scaled-up system avoids foundational pitfalls. Additionally, instead of defining collisions by the center point, there is a "grace distance" dependent on the radius of the agent
The console logs generation stats like average, min, and max fitness. Early runs show wild fluctuations (e.g., Gen 1: Avg -4.2 | Gen 50: Avg +12.7), reflecting the population’s struggle to balance food-seeking and edge avoidance. These metrics will later feed into visualization dashboards to track evolutionary trends.
Now, the groundwork is set, but the real challenge begins: transforming these simple food-seekers into agents capable of altruism and planning. Next week’s shift to TensorFlow will enable deeper networks, batch-based computation, and utilizing Kaggle's GPU capabilities. Now that the bottom line has been established, stay tuned as we move from brute-force evolution to structured learning!
Week 2: The basic criteria.
When evolving AI agents, the journey from concept to compelling behavior hinges on two pillars: computational efficiency and behavioral pressure. My latest developments reveal how these pillars shape both the simulation engine and visualization system. Let’s dissect the technical advances made since the initial blog post, and why they matter.
GPU Acceleration: From Loops to Tensors
The original Python-only implementation relied on nested loops—for agent sensing, movement, and evolution. BUT! While functional, this approach capped scalability. This upgraded implementation came with PyTorch and batched tensor operations. Consider the core neural network forward pass:
x = torch.tanh(torch.bmm(x, self.w1)) ... x = torch.tanh(torch.bmm(x, self.wO))
Here, torch.bmm (batched matrix multiplication) processes all 300 agents simultaneously. Each w1 to wO isn’t a single weight matrix but a tensor of shape [300, hidden_size, hidden_size], representing parallel networks for the entire population. This vectorization leverages GPU parallelism, reducing a generation’s computation from 25 seconds with a 4-layer neural network to 1 second with an upgraded 8-layer neural network.
The magic lies in treating the population as a single batch. Unlike traditional deep learning batches (data samples), here each "sample" is an independent agent. This unconventional use of batch dimensions allows mutations and fitness calculations to occur in unified tensor operations, avoiding Python’s GIL limitations.
Decoupling Simulation and Visualization
The initial Pygame-based version coupled rendering with simulation logic—a design that bottlenecked performance. The new architecture splits these concerns:
Training Phase: Runs headless on GPU-accelerated hardware (like Kaggle’s T4), exporting only final network weights.
Visualization Phase: A lightweight Pygame program loads pre-trained weights, focusing purely on rendering.
This separation aligns with robotics’ "sense-plan-act" pipeline. During training, the simulator prioritizes numerical efficiency:
food_eaten, rewards = self.food.check_collisions(self.agents.positions)
This single line hides a vectorized collision check. By computing all agent-food distances via tensor broadcasting (agent_positions.unsqueeze(1) - self.positions.unsqueeze(0)), it avoids Python loops entirely. On a GPU, this operation executes in milliseconds, even for 300 agents and 100 food items.
Behavioral Pressures: Beyond Food and Walls
Early agents often stagnated, content to hug walls or oscillate mindlessly. The solution? Introduce movement-based selection pressure. The position_history tensor tracks each agent’s recent positions:
movement = torch.norm(history[:, -1] - history[:, 0], dim=1) stagnant = movement < self.MOVEMENT_THRESHOLD self.agents.fitness[stagnant] -= self.STAGNATION_PENALTY
By penalizing agents that move less than 4 pixels over 5 steps, evolution favors explorers over idlers. This simple rule—implemented as a tensor norm across historical positions—creates dynamic trade-offs: agents must balance food pursuit, wall avoidance, and consistent motion. Because of this, the constant motion has to be directed to earn a higher score, indirectly supporting the agents to move further with searching for food.
The Visualization Bridge
The visualization program isn’t just a pretty face—it’s a validation tool. When loading a saved network:
def forward(self, x): x = torch.tensor(x).unsqueeze(0) x = torch.tanh(x @ self.w1) ... return x.squeeze().numpy()
This method converts individual agent observations into PyTorch tensors, replicating the training-time forward pass but for a single network. Notably, it uses @ (matrix multiplication) instead of bmm, since we’re no longer processing a population batch. The VisualNeuralNetwork unwraps the batched weights saved during training, adapting them for real-time single-agent inference.
Why This Matters
The GPU-driven batch processing isn’t just about speed—it enables emergent complexity. With 300 agents evolving over, say, 2000 generations, the simulation evaluates ~600,000 unique networks. Traditional methods would require almost a day; here, it’s feasible in a snack break.
But raw computation isn’t enough. Careful reward shaping—edge penalties, stagnation penalties, and food rewards—steers evolution toward desired behaviors. That's why the STAGNATION_PENALTY, for instance, was implemented as stated above, discouraging local optima where agents exploit the fitness function without meaningful behavior (try to cheat the system).
Looking Ahead
While the current system uses discrete actions (four directions), the infrastructure supports continuous control. Replacing argmax with a policy distribution (e.g., via a Gaussian output layer) could enable smoother movement, but that's something to consider for a much later time. Similarly, recurrent layers in the BatchNeuralNetwork could grant agents memory—letting them learn strategies like food caching or cooperative foraging. But of course, the "split/steal" goal is still there.
The code’s true test lies in its adaptability. By isolating domain logic (simulation) from rendering (visualization), the system becomes a platform for experimentation.
In AI evolution, the "basic criteria" aren’t just technical. Every line of code encodes assumptions about intelligence: movement matters, stagnation is death, and efficiency enables scale. As we refine these criteria, we inch closer to agents that don’t just survive their environment, but actually STRATEGIZE.
Week 3: Established basis. Complex rulesets.
Now, with the visualization phase complete and agents now reliably targeting food in under 5 minutes of training (2000 generations on a T4 GPU), we’ve reached an inflection point. The foundation is solid—agents navigate, avoid walls, and optimize simple rewards. Now, we introduce social complexity through a split/steal mechanic inspired by evolutionary game theory. Here’s the technical blueprint for transforming solitary food-seekers into strategic collaborators/competitors.
Core Mechanics: Size, Speed, and Conflict
Agents will gain a size attribute (stored as a tensor) that inversely affects speed and directly influences food disputes:
class AgentPopulation:
def init(self, num_agents):
...
self.size = torch.ones(num_agents, device=DEVICE) # Start normalized
self.speed_coeff = 1.5 / (1 + self.size) # Larger = slower
Size grows by 10% per food collected but resets each generation. This creates a trade-off: bulkier agents negotiate better but move sluggishly, risking fewer total food encounters.
Split/Steal: Action Space Expansion
The neural network’s output layer expands from 4 (movement directions) to 6 neurons:
Movement (4 directions, same as before)
Split/Steal (2 actions, softmaxed)
class BatchNeuralNetwork(torch.nn.Module): def __init__(self, num_agents, output_size=6): ... self.wO = torch.nn.Parameter(torch.empty(num_agents, hidden_size, 6))
During movement, agents use the first 4 neurons as before. When colliding with food or another agent, the latter 2 neurons determine their social action.
Food Contention: Timed Interactions
Eating now requires an n-step "consumption timer," creating contention windows:
During simulation loop
eating_mask = (food_eaten & ~self.agents.eating_timer.bool())
self.agents.eating_timer[eating_mask] = 3 # 3 steps to consume
If two agents collide with the same food:
Size comparison: size_ratio = agent_a.size / (agent_a.size + agent_b.size)
Action resolution:
Split: Food divided by size_ratio (e.g., 70/30 if A is larger)
Steal: Larger agent takes min(size_ratio + 0.2, 1.0), but movement speed drops 30% for 5 steps
Steal vs Steal: Cancel out—no food gained, both penalized with 15% speed loss
Rewards adjust to incentivize strategy:
split_reward = FOOD_SCORE * size_ratio
steal_reward = FOOD_SCORE * steal_amount * 0.8 # 20% efficiency tax
Visual Encoding: Color as Strategy
Agents tint based on their latest social action:
Cyan: Split (cooperative)
Magenta: Steal (competitive)
White: Neutral (moving only)
# In visualization code action_color = (0, 255, 255) if last_action == SPLIT else (255, 0, 255) pygame.draw.circle(screen, action_color, (int(x), int(y)), RADIUS)
This real-time feedback lets observers decode population-wide strategy shifts, like cyan dominance indicating emergent cooperation.
Evolutionary Pressure: Altruism Metrics
To prevent convergent selfishness, we add two fitness modifiers that could be considered OPTIONAL.
Cooperation bonus: +2% fitness per split action, decaying if overused
Conflict penalty: -1% fitness per steal collision, scaling with population steal rate
coop_bonus = split_actions * 0.02 * torch.exp(-split_actions / 50) conflict_penalty = steal_collisions * -0.01 * (1 + steal_rate) self.agents.fitness += coop_bonus + conflict_penalty
This dual system mirrors biological kin selection—rewarding cooperation but only when strategically timed.
Technical Hurdles & Solutions
Action Collision Detection:
Tensor-based pairwise distance checks:
agent_pairs = torch.combinations(self.agents.positions, r=2)
conflicts = torch.norm(agent_pairs[:,0] - agent_pairs[:,1], dim=1) < 2*AGENT_RADIUS
Mask agents in conflicts for special resolution logic. Can we utilize this maybe for general agent "fighting"?
Gradient Stability:
Size and speed modifications occur outside the neural network’s forward pass to avoid disrupting backprop (though evolution doesn’t use gradients, this keeps tensors clean)
Memory Overhead:
Pre-allocate conflict tensors instead of dynamic Python lists to maintain GPU speed
The Big Picture
Should they grow large to dominate disputes but risk mobility? Prioritize quick, small raids? Or balance both? What if we introduced giving" food via pheromones? By tying size to speed and making social actions mutually exclusive (split/steal), we force the population to explore trade-offs analogous to biological niches.
The timed eating system adds another layer: agents must now plan (of course, going to need to add more neuron layers!). Rushing to food first risks being overtaken by larger competitors during the 3-step window. Maybe smaller agents evolve to "scout" while larger ones "bully"? Or cooperative splits emerge as the only viable path when stealers gridlock each other?
Week 4: Stabilization
This week was less about flashy features and more about reinforcing the foundation. After implementing split/steal mechanics, unexpected behaviors emerged—some hilarious, some frustrating. For instance, agents discovered a loophole where rapidly stealing in-place granted infinite size growth (thanks to a miscalculated size_ratio tensor that didn’t clamp values). Another bug caused GPU memory leaks during multi-agent collisions due to unmanaged temporary tensors. It was... quite the grind this week.
Optimizing Split/Steal: From Binary to Nuanced
The initial split/steal mechanic was rudimentary—a zero-sum game where agents either cooperated or competed. To encourage richer strategies, I added three layers of complexity:
Probabilistic Outcomes:
Stealing success now scales with both size and momentum. A smaller agent moving at high speed can "blitz steal":
steal_success_chance = (size_ratio * 0.7) + (speed_ratio * 0.3)
This rewards agents that strategically balance size and mobility.
Reputation Tracking:
Agents now store a reputation tensor (0 to 1) updated after each interaction:
reputation[steal_mask] -= 0.1 # Stealers get penalized
reputation[split_mask] += 0.05 # Cooperators gain trust
Agents with low reputation face preemptive steals from others, creating a self-reinforcing "outcast" dynamic.
Risk Tolerance Genes:
A new neural network output (risk_tolerance) determines how often agents gamble on steals in uncertain scenarios:
if (size_ratio + risk_tolerance) > 0.5:
choose_steal()
Evolution now optimizes not just for size/speed, but for personality traits.
Dynamic Reward Scaling
Early versions used fixed food rewards, leading to exploitative "trench warfare" around food clusters. The fix? Supply and demand, but still working on better implementation! The gist?:
def update_food_value(self):
scarcity = len(self.food) / INITIAL_FOOD # 0.0 (scarce) to 1.0 (plentiful)
self.food_score = FOOD_SCORE * (1 + (1 - scarcity)) # Double value when scarce
Now, food gains value as it becomes rarer, incentivizing agents to prioritize long-term planning over short-term hoarding.
Visualizing Complexity
To debug these systems, I added tactical overlays:
Reputation Gradient: Agents tint from red (low rep) to blue (high rep)
Size Pulses: Larger agents emit concentric circles proportional to their steal radius
Risk Indicators: A halo glows yellow for high-risk-tolerance agents
# Pygame rendering snippet if agent.risk_tolerance > 0.7: pygame.draw.circle(surface, YELLOW, (x, y), RADIUS + 3, 2)
The Bugs That Shaped Design
The "Kamikaze Steal": Agents with near-zero reputation would starve but still steal, crashing the fitness function. Fixed by culling agents with fitness < -50.
Tensor Contention: Multiple agents targeting the same food caused race conditions. Solved with a three-phase resolution system:
Detect collisions (GPU-parallel)
Sort contenders by size/speed (argsort)
Resolve sequentially (CPU, but batched per food item)
Next: The Memory Revolution
With split/steal stabilized, the focus shifts to temporal intelligence:
LSTM Integration: Adding recurrent layers to track past interactions (who stole from whom).
Kin Recognition: Let agents bias cooperation toward others with similar network weights (simulating genetic relatedness).
Dynamic Environments: Migrating food sources that force territoriality or nomadic behaviors.
The goal is to answer: Can altruism emerge without explicit rewards?
This week was a grind, but watching agents evolve from blunt opportunists to cautious negotiators made it worthwhile.
Week 5: Optimization.
Optimization: Animating Survival & The Predator Paradigm
To deepen the simulation’s ecological plausibility, two key optimizations are underway: visualizing consumption and introducing predators. These changes aim to bridge the gap between abstract evolution and tangible ecosystem dynamics.
---
Eating Animations: From Instant to Process
Currently, food vanishes instantly when collected—a visual disconnect from the timed eating mechanics. The new system visualizes consumption as a **radial progress bar** around food items:
# In Pygame render loop
for fx, fy, timer in active_eating:
progress = timer / EAT_DURATION
pygame.draw.circle(surface, (255 * progress, 0, 0), (fx, fy), FOOD_RADIUS)
Agents must remain within a 10-pixel radius while the timer counts down. If interrupted (e.g., by a competitor or predator), the food resets.
---
Chasers: Co-Evolving Predation
Chasers are slower but persistent predators that target agents.
class ChaserNetwork(torch.nn.Module):
def __init__(self):
super().__init__()
self.fc1 = torch.nn.Linear(4, 8) # Inputs: dx, dy to agent, speed, stamina
self.fc2 = torch.nn.Linear(8, 4) # Outputs: direction (4-way)
Chasers sense the nearest agent’s relative position and their own stamina.
Dynamic Difficulty
# In config
CHASER_SPEED = 1.2 # Base speed (scales with evolution)
CHASER_INTELLIGENCE = 0.5 # 0.0 (random) to 1.0 (optimal pathing)
Intelligence blends random movement (low) and neural network decisions (high).
Evolutionary Arms Race
Chasers evolve alongside agents via **shared selection pressure**:
- Chaser fitness increases for caught agents
- Agent fitness decreases when caught but gains a "survival bonus" if escaping
# example values
if chaser_caught_agent:
agent.fitness -= 50
chaser.fitness += 30
else:
agent.fitness += 10 * escape_duration
---
Tunable Threat Levels
Through a new **Scenario Editor**, users can:
1. Spawn chaser populations (0-50% of agent count)
2. Adjust their neural network depth (2-8 layers)
3. Control mutation aggression (0.05-0.5 rate)
4. Toggle **adaptive difficulty** (chasers evolve faster when agents dominate)
# Adaptive difficulty example
if agent_avg_fitness > 100:
chaser_mutation_rate = min(0.5, 0.1 + (agent_avg_fitness / 1000))
---
Performance Considerations (tbd)
- **Chaser Rendering: Crimson triangles with a "hunting aura" (pulsing red gradient)
- **Collision Avoidance: Quad-tree spatial partitioning to optimize 10k+ entity checks
- **Selective Pressure: Cull bottom 20% of chasers each generation if they underperform
---
The Big Picture
Chasers are evolutionary catalysts. By forcing agents to balance food pursuit with threat avoidance, we create niches, and this system’s real power lies in its emergent arms race.
Week 6: Classes
This week marks a shift! Instead of a homogeneous population, we now simulate three distinct agent species differentiated by size: Small (0.5x), Medium (1x), and Large (2x). Each species evolves independently, creating a microcosm of ecological competition. Here’s the technical breakdown:
Core Mechanics
Size-Locked Evolution:
Each size class maintains separate gene pools. When reproducing:
def evolve_population(self): # Split population by size class small_mask = self.agents.sizes == 0.5 medium_mask = self.agents.sizes == 1.0 large_mask = self.agents.sizes == 2.0
# Select top 15% within each class
small_fitness = self.agents.fitness[small_mask]
small_top = torch.topk(small_fitness, max(1, len(small_fitness)//7), sorted=False).indices
medium_fitness = self.agents.fitness[medium_mask]
medium_top = torch.topk(medium_fitness, max(1, len(medium_fitness)//7), sorted=False).indices
large_fitness = self.agents.fitness[large_mask]
large_top = torch.topk(large_fitness, max(1, len(large_fitness)//7), sorted=False).indices
# Reproduce within classes
new_small = self._reproduce_group(small_mask, small_top)
new_medium = self._reproduce_group(medium_mask, medium_top)
new_large = self._reproduce_group(large_mask, large_top)
# Combine next generation
self.agents = AgentPopulation(
positions=torch.cat([new_small.positions, new_medium.positions, new_large.positions]),
brains=torch.cat([new_small.brains, new_medium.brains, new_large.brains]),
sizes=torch.tensor([0.5]*len(new_small) + [1.0]*len(new_medium) + [2.0]*len(new_large))
)
def _reproduce_group(self, mask, top_indices):
group_brains = self.agents.brains[mask][top_indices]
mutated_brains = group_brains.clone()
mutated_brains += torch.randn_like(mutated_brains) * 0.1 # Mutate
return mutated_brains
No cross-breeding allowed – small agents only mutate within their size class. While the code implemented isn't the exact same, it essentially follows this idea.
Velocity Scaling:
Base movement speed inversely correlates with size:
base_speed = 2.0
self.velocities = base_speed / (self.sizes * 1.2)
Resulting speeds: Small=3.33 units/step, Medium=1.67, Large=0.83.
Foraging Advantage:
Collision detection radius scales with size:
effective_radius = AGENT_RADIUS * size
if distance_to_food < (effective_radius + FOOD_RADIUS):
collect_food()
Large agents can "grab" food from 6px away vs Small’s 1.5px.
Behavioral Pressures
To prevent Large dominance:
Metabolic Cost:
fitness -= 0.1 * size # Large pay 0.2/timestep, Small 0.05
Food Value Scaling:
Small agents get 20% bonus per food to offset reach disadvantage:
rewards = FOOD_SCORE * torch.where(sizes == 0.5, 1.2, 1.0)
Visualization System
Color Coding:
Small=Blue, Medium=Green, Large=Red
colors = torch.where(sizes == 0.5, (0,0,255),
torch.where(sizes == 1.0, (0,255,0), (255,0,0)))
Population Dashboard:
Real-time pie chart showing species distribution and avg fitness per class.
Bug Mitigations
Size Bleeding:
Initial crossover bugs allowed Large agents to spawn Small offspring. Fixed via:
new_agents[:, SIZE_IDX] = parent_size # Hardcode size during reproduction
Velocity Clamping:
Small agents moved too fast, phasing through food. Added:
max_speed = 4.0
velocities = torch.clamp(velocities, 0.5, max_speed)
Collision Gridlock:
Large agents trapped smaller ones in corners. Introduced repulsion:
overlap = (agent_radius + other_radius) - distance
if overlap > 0:
force = overlap * (position - other_position) / distance
velocity += force * 0.1
Lots of implementation this week! I'll start attaching demo pictures soon!
Week 7: More.
In this week's post, we explore the intricacies of the "altruism gene" and its role in a simulation where species must navigate an environment while interacting with food resources, chasers, and other agents. In particular, we will delve into how this gene is implemented within the code, how the simulation works, and how tensor processing was implemented even further in optimizing the agent's behavior.
Altruism Gene in the Simulation
In the context of this simulation, the altruism gene influences how food resources are distributed among agents. Altruism refers to a behavior where individuals act in ways that benefit others at a cost to themselves. This gene is a key part of the "food collection" mechanism in the simulation.
When altruism mode is enabled, agents within a species who are near a food spot will share food resources based on their size. The larger the agent, the more food it receives. Specifically, the food is distributed proportionally to the agents' relative sizes in relation to each other. This results in a more collaborative environment where agents of different species may work together in a way that benefits all parties, although the larger agents will always receive more.
Here’s the relevant section of the code where altruism mode is implemented:
if altruism_enabled:
# ALTRUISM MODE: Split food based on size ratios
size_sum = sum(SPECIES_RADII[pi] for pi, _ in fs.eaters)
for pi, idx in fs.eaters:
# Each agent gets food proportional to its size
ratio = SPECIES_RADII[pi] / size_sum
gain = FOOD_SCORE * ratio
self.populations[pi].fitness[idx] += gain
else:
# STEAL MODE: Only the largest agents get food
sizes = [SPECIES_RADII[pi] for pi, _ in fs.eaters]
maxr = max(sizes)
winners = [
(pi, idx) for (pi, idx), r in zip(fs.eaters, sizes) if r == maxr
]
if len(winners) == 1:
# Only one largest agent — it gets all the food
pi, idx = winners[0]
self.populations[pi].fitness[idx] += FOOD_SCORE
This section checks whether altruism is enabled. If it is, the food is distributed proportionally based on the relative size of each agent. If not, food is awarded to the largest agent (or no one, in the case of a tie).
Tensor Processing for Optimization
The core of this simulation involves the processing of large amounts of data, particularly the positions of agents, food, and chasers. This is where tensor processing with PyTorch becomes a crucial tool. Throughout the simulation, tensors are used to represent the positions, fitness values, and actions of agents, as well as environmental data such as food spots and chaser locations.
For example, in the Simulation class, the agent positions are managed as tensors to calculate distances and actions efficiently:
pos = pop.positions env = torch.stack([pos[:,0]/ENV_SIZE, pos[:,1]/ENV_SIZE, (ENV_SIZE-pos[:,0])/ENV_SIZE, (ENV_SIZE-pos[:,1])/ENV_SIZE], dim=1)
Here, the env tensor is created by normalizing the agent positions relative to the environment's size (ENV_SIZE). This helps the neural network model understand the agents' relative locations in the world.
Another key tensor operation occurs in the Chaser class, where the chaser's movement is determined by calculating the distance to the nearest agent:
distances = torch.sum((agent_positions - pos_tensor)**2, dim=1) nearest_idx = torch.argmin(distances) nearest_pos = agent_positions[nearest_idx]
These tensor operations are essential for optimizing the simulation, allowing the agents to calculate distances and move efficiently within the environment.
Additionally, the neural network used by each agent (implemented in the BatchNeuralNetwork class) also leverages tensor operations for backpropagation and mutation. The forward pass and weight mutation are key parts of the evolutionary process:
x = F.relu(torch.bmm(x, self.w1)) x = F.relu(torch.bmm(x, self.w2)) ...
The torch.bmm() function is used for batch matrix multiplication, enabling efficient computation across multiple agents simultaneously. This allows for the training and evolution of a population of agents with minimal computational overhead.
Conclusion
In conclusion, the altruism gene plays a critical role in shaping the behaviors of agents in the simulation, influencing their interaction with food and other agents. The use of tensor processing ensures that these interactions are handled efficiently, even as the number of agents and the complexity of the environment increases. Whether altruistic or competitive, the agents' evolution is governed by both their neural networks and their interaction with the environment, making for an engaging and dynamic simulation.
As we move into future weeks, we’ll continue to refine these models, exploring the deeper implications of altruism in evolutionary simulations and enhancing the tensor-based processing for even greater efficiency. Stay tuned for more!
Week 8: Visualization
In this week's entry, we take a step back and examine how far the visualization tools have come. Specifically, we analyze the improvements made between the original visualizer and the upgraded, feature-rich version.
The visualizer isn't just a cosmetic tool it's SUPER, SUPER important for understanding how agents behave, how different species interact, and how settings like altruism influence real-time decision-making.
---
The First Visualizer
The first visualizer focused on one thing: basic movement and food interaction for evolved agents.
Some of its main features were:
- Basic loading of brains trained on different radii species.
- Fixed behavior: all agents always played in "food-chase" mode.
- Simple environmental sensing: Agents could detect walls and nearby food.
- Food disappeared immediately when eaten; there was no respawn logic or chasers.
- Consistent species properties: Step sizes and radii were baked in.
- Minimalistic Pygame window with only the environment and agents drawn.
Here’s a glimpse at how a simple agent moved:
def sense_and_move(self, food_list, env_size):
inputs = [
self.x/env_size,
self.y/env_size,
(env_size-self.x)/env_size,
(env_size-self.y)/env_size
]
...
out = self.brain.forward(inputs)
decision = np.argmax(out)
angle = decision * (2*math.pi)/4
self.x += self.step * math.cos(angle)
self.y += self.step * math.sin(angle)
It was clean and functional — but it left a lot of potential for more complex behaviors untapped.
---
The Upgrade
The new version massively expands on the original tool. Here’s how:
1. UI Elements and Controls
- Introduced buttons at the top for toggling altruism, starting/pausing/resetting the simulation, and manually placing agents.
- Buttons actively update their text and status (e.g., "Pause" becomes "Resume").
- Manual agent placement mode lets you custom-build your environment.
2. Dynamic Altruism Mode
- A simple button toggles Altruism Mode ON or OFF during a live session.
- This setting controls whether food is shared proportionally (altruism) or "stolen" by the largest nearby agent.
if altruism_mode:
size_sum = sum(a.radius for a in fs.eaters)
for ag in fs.eaters:
ratio = ag.radius / size_sum
gain = FOOD_SCORE * ratio
ag.score += gain
else:
max_radius = max(a.radius for a in fs.eaters)
winners = [a for a in fs.eaters if a.radius == max_radius]
if len(winners) == 1:
winners[0].score += FOOD_SCORE
3. Food Mechanics Upgraded
- Food now requires 30 frames of "eating" before it disappears.
- A progress arc is drawn around food to visualize how close it is to being eaten.
4. Chasers Added
- Chasers actively hunt down agents.
- If a chaser catches an agent, that agent suffers a fitness penalty and a freeze indicator flashes on them for one second.
- Chasers have "frozen" states after catching someone, visualized with cooldown timers.
5. Species Stats and Scoring
- The UI displays live species stats, showing how many agents of each species exist and their average score.
- Dynamic tracking of food counts and active chasers keeps the player informed.
6. Realistic Neural Variation
- Agents now load from a master neural network with slight random variance added to weights, so no two agents behave identically.
def _add_variance(self, state, variance):
modified_state = {}
for key, weight in state.items():
variation_factor = 1.0 + (2 * random.random() - 1) * variance
modified_state[key] = weight * variation_factor
return modified_state
7. Better Movement and World Rules
- Agent sensing includes walls, food, other agents, and chasers — a full 11-feature input vector matching training conditions.
- Agents are penalized for bumping into walls — just like during training — to discourage reckless behavior.
---
We talked about the altruism gene before, now it's more refined and we can go into more detail!
Altruism in the New Simulation
The simulation has been upgraded to allow the altruism gene to meaningfully influence the agents' evolution. Let's talk about how it works and why it's important.
At every frame, agents can move toward food, avoid chasers, and interact with each other. But how they collect food is where altruism or selfishness (stealing) comes into play.
When food is eaten, the simulation checks if altruism_enabled is active:
if altruism_enabled:
size_sum = sum(SPECIES_RADII[pi] for pi, _ in fs.eaters)
for pi, idx in fs.eaters:
ratio = SPECIES_RADII[pi] / size_sum
gain = FOOD_SCORE * ratio
self.populations[pi].fitness[idx] += gain
else:
sizes = [SPECIES_RADII[pi] for pi, _ in fs.eaters]
maxr = max(sizes)
winners = [(pi, idx) for (pi, idx), r in zip(fs.eaters, sizes) if r == maxr]
if len(winners) == 1:
pi, idx = winners[0]
self.populations[pi].fitness[idx] += FOOD_SCORE
Key Differences Depending on Altruism:
Altruism ON:
All agents touching the food share it based on their body size.
Larger agents get more, but even the smallest agents get some reward.
This promotes cooperation among different species.
Altruism OFF (Stealing Mode):
Only the single largest agent gets all the food.
If multiple large agents are tied, nobody gets it (food is wasted).
This creates a cutthroat environment where size dominance is king.
Important Nuances
1. Size Matters for Sharing
The amount of food each agent gets is proportional to its radius when altruism is enabled. This means larger species still benefit more, but without outright destroying the smaller ones. It keeps the ecosystem diverse longer.
2. Evolution Is Mode-Sensitive
Because fitness directly depends on how food is distributed, agents evolve very differently:
Under altruism, you might evolve agents that are good at finding food and cooperating.
Under stealing, bigger and greedier agents dominate.
3. Food Takes Time to "Mature"
Food isn't collected instantly — agents must "stay" over it for 30 frames. This gives time for multiple agents to arrive, making altruism even more meaningful because contests aren't decided immediately.
4. Chasers Add External Pressure
Chasers hunt down agents and freeze them temporarily. This outside threat makes food collection risky, encouraging faster movement and smarter group dynamics.
5. Neural Networks Are Batched
Each agent has its own personal brain (BatchNeuralNetwork), but inference is done in a highly efficient batched way: x = F.relu(torch.bmm(x, self.w1))
x = F.relu(torch.bmm(x, self.w2))
... This ensures that even with 300 agents, simulation speed remains high.
6. Evolution Is Competitive
At the end of every generation, only the top ~10% of agents survive to copy their brains to the next generation. Mutation ensures slight randomness to allow new strategies to emerge.
Week 9: Interfacing
Now, we dive into the nitty-gritty of how the simulation’s user interface is built, coded, and seamlessly integrated with the underlying engine. From Pygame window setup to event loops and data binding, lets look at the design decisions and code patterns that make the interface both powerful and user-friendly.
---
UI Architecture Overview
At its core, the interface is a thin layer on top of the simulation logic. We separate concerns into three main parts:
1. Initialization & Layout
2. Event Handling & Control Flow
3. Rendering & Data Binding
By clearly delineating these responsibilities, we ensure that adding new buttons, panels, or input modes can be done without touching the core simulation loop.
---
1. Initialization & Layout
The very first step is sizing the window and creating UI panels. We reserve a top panel for controls and use the rest of the window for the simulation canvas:
# Compute total window height
window_height = ENV_SIZE + PANEL_HEIGHT
screen = pygame.display.set_mode((ENV_SIZE, window_height))
pygame.display.set_caption("Multi-Species Neural Evolution")
Here, `PANEL_HEIGHT` is preconfigured based on button size and margins, giving us flexibility to adjust the UI without rewriting layout code.
Next, we instantiate our UI elements—buttons for start/pause/reset, altruism toggle, and placement mode. Each button is an instance of a reusable `Button` class:
# Create buttons at fixed x positions
x_pos = BUTTON_MARGIN
mode_button = Button(x_pos, BUTTON_MARGIN, button_width, BUTTON_HEIGHT, "Altruism: ON")
buttons.append(mode_button)
x_pos += button_width + BUTTON_MARGIN
# … repeat for start, pause, reset, place, species …
By looping over a list of button specs, we could even generate these dynamically in the future.
---
2. Event Handling & Control Flow
A robust event loop ties UI interactions to simulation state changes. Here’s the central pattern:
for ev in pygame.event.get():
if ev.type == pygame.QUIT:
running = False
elif ev.type == pygame.MOUSEMOTION:
# Update hover states
for btn in buttons:
btn.check_hover(mouse_pos)
elif ev.type == pygame.MOUSEBUTTONDOWN and ev.button == 1:
# Check for button clicks
for btn in buttons:
if btn.check_click(mouse_pos):
handle_button_click(btn)
# If in placement mode, add new agent at click
if manual_placement_mode:
place_new_agent(mouse_pos)
The helper `handle_button_click()` encapsulates logic for each control:
def handle_button_click(btn):
if btn == mode_button:
toggle_altruism()
elif btn == start_button:
start_simulation()
elif btn == pause_button:
toggle_pause()
# … and so on …
This event-driven design ensures UI responses are immediate, decoupled, and easy to test.
---
3. Rendering & Data Binding
Once state (simulation_started, foods, agents, chasers, scores) is updated, we render both UI and simulation actors every frame:
# Draw UI panel
pygame.draw.rect(screen, (50,50,50), (0,0,ENV_SIZE,PANEL_HEIGHT))
for btn in buttons:
btn.draw(screen, font)
# Draw simulation entities
for fs in foods:
draw_food(fs)
for ag in agents:
pygame.draw.circle(screen, ag.color, (int(ag.x),int(ag.y)), int(ag.radius))
# Stats text
stats_text = f"Food: {len(foods)} Chasers: {active_chaser_count}"
screen.blit(font.render(stats_text, True, (255,255,255)), (10, window_height - 30))
Each draw call reads directly from the simulation objects, ensuring live synchronization. For instance, button text always reflects the latest `altruism_mode` value, and the species stats panel reads from `species_scores` each frame.
---
Integration & Extensibility
- Loose Coupling: UI code never directly manipulates agent internals; it simply flips flags (`simulation_started`, `manual_placement_mode`, etc.) that the simulation loop respects.
- Centralized Config: Colors, sizes, margins, and font sizes live in a single config section at file top, making them trivial to tweak.
- Reusable Components: The `Button` class abstracts hover, click detection, and drawing, so adding new controls (e.g., sliders, dropdowns) follows the same pattern.
- Event Callbacks: By routing every UI action through clear callback functions, new behaviors can be attached without rewriting the main loop.
---
Now, we’ve peeled back the layers of our interface: from window setup to event loops, from rendering agents to binding UI state. This modular approach not only keeps code maintainable but opens the door for future enhancements—like adding real-time graphs, keyboard shortcuts, or even a fully web-based front end.
Week 10: Conclusion
In this final week, we tie together every piece of the project—from configuration and tensor‑based simulation to the interactive visualizer—and reflect on how it all operates as a cohesive system. Along the way, we’ll touch on the subtle clustering patterns that emerge in altruistic mode, but our main focus is on the end‑to‑end mechanics.
1. Project Overview
The goal of this project is to simulate multiple species of agents in a bounded environment, evolving their behaviors via neural networks and genetic algorithms, and observe both competitive and cooperative dynamics. Three species of differing sizes roam a 2D plane collecting food and avoiding mobile chasers. The heart of the system is a PyTorch‑powered evolutionary loop; the crown jewel is a Pygame visualizer that lets us watch it all unfold.
2. Configuration & Initialization
We begin by importing core libraries and defining global parameters:
import torch
import torch.nn.functional as F
import math, time, random, numpy as np
# ENV_SIZE = 1000, FOOD_RADIUS = 2, FOOD_SCORE = 10, EDGE_PENALTY = 2
# NUM_AGENTS = 300, GENERATIONS = 800
# SPECIES_RADII = [6.0, 3.0, 1.5], CHASER_RADIUS = 4.0, CHASER_PENALTY = 30
A master flag, altruism\_enabled, toggles food‑sharing behavior. The device (CPU/GPU) is detected via torch.device, ensuring high‑speed tensor ops when CUDA is available.
3. Neural Network & PyTorch Usage
Each agent population is governed by a BatchNeuralNetwork, a custom 10‑layer feedforward model:
class BatchNeuralNetwork(torch.nn.Module):
def __init__(self, num_agents, input_size=11, hidden_size=12, output_size=4):
super().__init__()
# w1…w11 are Parameter tensors of shape (num_agents, in, out)
torch.nn.init.uniform_(w, -1, 1) # random initialization
def forward(self, x):
x = x.unsqueeze(1)
x = F.relu(torch.bmm(x, self.w1))
…
return x.squeeze(1)
def mutate(self, rate=0.1):
# element‑wise mask and Gaussian noise injection
Key points:
* We vectorize inference across all agents with torch.bmm, achieving batched matrix multiplications.
* Mutation directly perturbs weights in place for efficient genetic variation.
* All agent states (positions, fitnesses, network weights) live as GPU tensors when possible.
4. Agent Population & Evolutionary Loop
The AgentPopulation class maintains positions and fitness scores:
class AgentPopulation:
def __init__(self, num_agents, radius):
self.step_size = BASE_STEP_SIZE * (0.5 + 0.5 * sqrt(BASE_RADIUS/radius))
self.positions = torch.rand((num_agents,2))*…
self.fitness = torch.zeros(num_agents)
self.brain = BatchNeuralNetwork(num_agents).to(device)
def evolve(self, mutation_rate):
# select top 10%, copy their weights, then mutate
Every generation:
1. Agents “sense” their environment via proximity tensors to walls, food, other agents, and chasers.
2. The combined feature tensor (shape: num\_agents×11) feeds through the batch network to produce movement decisions.
3. Agents move, incur edge penalties, and collect food if they linger over it for 30 frames.
4. Fitness updates depend on the altruism flag: either proportional sharing or “winner‑takes‑all” stealing.
5. After 600 steps, the top performers seed the next generation via weight copying and mutation.
This loop runs for 3000 (changeable) generations, printing per‑generation statistics and saving out optimized brain weights.
5. Visualization Integration
The StableVisualizerWithSizingALTRUISMSettings.py brings the simulation to life. Major components:
* Button class: handles drawing, hover detection, and click events for UI controls.
* VisualNeuralNetwork: loads saved weights, optionally adds small variance, and provides a forward method for single‑agent inference.
* Agent objects: mirror the simulation’s neural inputs and movement logic, but track score and caught states for visual feedback.
* Chaser entities: pursue the nearest agent in cardinal directions, penalize caught agents, and “freeze” before hunting again.
* The main loop blends UI event handling (start/pause/reset, altruism toggle, manual placement) with rendering:
* Draw UI panel and buttons.
* Update simulation state if running.
* Render food spots (with a timer arc), agents (with eating/caught indicators), chasers, and live stats.
This separation of concerns—simulation logic in PyTorch vs. rendering logic in Pygame—lets us tweak each independently while maintaining tight synchronization.
6. Clustering in Altruistic Mode
When altruism\_enabled==True, agents exhibit a subtle clustering behavior around food sources before consumption:
* Because food requires 30 “eating” frames, multiple agents converge and wait together.
* Sharing rewards proportional to size reduces the rush for domination, so smaller agents can safely approach existing clusters.
* The result is visible clusters of mixed‑size agents around each food spot—a vivid contrast to the “scatter‑and‑dash” patterns in stealing mode.
This clustering isn’t explicitly coded; it emerges from the interplay of the eating timer and proportional reward scheme. It’s a powerful demonstration of how simple local rules can generate collective behavior.
7. Project Flow & Data Lifecycle
From start to finish:
1. Configure global parameters and load device.
2. Initialize agent populations and food/chaser objects.
3. Evolve in tight PyTorch loops: sense → decide → move → collect → evolve.
4. Log & Save: print stats, save brain weights each generation.
5. Visualize: load saved brains, spawn Pygame agents, run interactive UI loop.
6. Analyze emergent behaviors like clustering, size‑based dynamics, and chaser‑induced scatter.
Every piece—from uniform weight init to bmm‑based inference, from evolving weight copying to Pygame’s draw calls—works together to create a rich, evolving ecosystem you can both compute and watch in real time.
Conclusion
By combining batched PyTorch inference, evolutionary algorithms, and a flexible Pygame interface, this project delivers a full pipeline: you can train agent brains, observe cooperative vs. competitive strategies, and interact with the simulation via a polished UI.