Problem Set 20: Modeling a Simple System – Part 2

When you start a new problem set, your first instinct might be to open your computer and begin typing code right away. While this can feel productive, it often leads to frustration when things don't work as expected. Instead, take a few minutes to slow down and plan.

Here are some helpful strategies:

  1. Understand the problem clearly
    • Read the instructions carefully — twice if needed.
    • Ask yourself: What exactly is being asked?
  2. Break the problem into smaller steps
    • Think about the smallest possible actions the computer will need to perform.
    • For example: If the task is to find the first recurring letter in a word, what steps must happen first?
  3. Try solving it on paper first
    • Write out your steps in plain language (pseudocode).
    • Test your steps with a simple example before touching the keyboard.
  4. Translate your steps into code
    • Start small — write only a few lines at a time and test often.
    • Don't worry about perfection at first; get a working version, then improve it.
  5. Check your solution
    • Run it with different examples, including edge cases.
    • Ask: Does this solve the problem in all situations?

  • Read the question carefully (twice).
  • Break the task into the smallest steps.
  • Sketch or write pseudocode before coding.
  • Start small — test as you go.
  • Check your solution with different cases.

Create an Ant Colony Simulation

Introduction – The Story

In the previous problem set, you modeled one ant wandering randomly. Real ant colonies, however, display complex behavior: they search for food, leave trails, and work together.

In this task, you will create a simulation of a small ant colony. Instead of just one ant, there will be multiple ants, and they can interact with the environment.

This is your first taste of how simple agents (ants) following simple rules can create emergent complex behavior. This concept is widely used in:

  • Swarm robotics (drones working together).
  • Traffic flow simulation.
  • Epidemiology (disease spread).
  • Optimization problems (Ant Colony Optimization algorithms).

The Problem

You need to simulate a colony of ants moving on a 2D grid with food sources.

Rules for the system:

  1. The grid is n x n.
  2. m ants are placed at the nest (center of the grid).
  3. Food sources are randomly placed on the grid.
  4. Each ant can:
    • Wander randomly if it hasn’t found food.
    • If it finds food, it carries it back to the nest.
    • While carrying food, it leaves a pheromone trail on each cell.
    • Other ants are more likely to follow cells with stronger pheromone trails.
  5. The grid tracks pheromone levels (they fade over time).

Your Goal: Run the simulation for steps turns and return:

  • How many pieces of food were delivered to the nest.
  • How many unique cells the ants visited.
  • A summary of pheromone levels (average, max).

Learning Objectives (IB Command Terms)

  • Describe how local rules create global system behavior.
  • Explain the role of randomness and probability in simulations.
  • Construct a multi-agent simulation (colony).
  • Evaluate how changing parameters (number of ants, food sources) affects outcomes.

Starter Code

import random

class Ant:
    def __init__(self, grid_size, nest):
        self.x, self.y = nest
        self.has_food = False
        self.grid_size = grid_size

    def move(self, pheromones):
        """Move randomly, biased by pheromone strength."""
        directions = [(0,-1), (1,0), (0,1), (-1,0)]  # N,E,S,W
        candidates = []
        for dx, dy in directions:
            nx, ny = self.x+dx, self.y+dy
            if 0 <= nx < self.grid_size and 0 <= ny < self.grid_size:
                strength = pheromones[ny][nx]
                candidates.extend([(dx,dy)] * (1 + strength))  # more pheromone = more likely
        dx, dy = random.choice(candidates)
        self.x += dx
        self.y += dy


class Colony:
    def __init__(self, grid_size, num_ants, num_food):
        self.grid_size = grid_size
        self.nest = (grid_size//2, grid_size//2)
        self.ants = [Ant(grid_size, self.nest) for _ in range(num_ants)]
        self.pheromones = [[0]*grid_size for _ in range(grid_size)]
        self.food = set()
        for _ in range(num_food):
            fx, fy = random.randint(0,grid_size-1), random.randint(0,grid_size-1)
            self.food.add((fx, fy))
        self.delivered = 0

    def step(self):
        for ant in self.ants:
            if not ant.has_food:
                # Check for food
                if (ant.x, ant.y) in self.food:
                    ant.has_food = True
                    self.food.remove((ant.x, ant.y))
                else:
                    ant.move(self.pheromones)
            else:
                # Return to nest
                if (ant.x, ant.y) == self.nest:
                    self.delivered += 1
                    ant.has_food = False
                else:
                    self.pheromones[ant.y][ant.x] += 1
                    ant.move(self.pheromones)
        # Evaporate pheromones
        for y in range(self.grid_size):
            for x in range(self.grid_size):
                if self.pheromones[y][x] > 0:
                    self.pheromones[y][x] -= 1

    def run(self, steps):
        for _ in range(steps):
            self.step()
        total_pheromones = sum(sum(row) for row in self.pheromones)
        max_pheromone = max(max(row) for row in self.pheromones)
        return {
            "delivered": self.delivered,
            "unique_food_left": len(self.food),
            "avg_pheromone": total_pheromones / (self.grid_size**2),
            "max_pheromone": max_pheromone
        }

Test Cases

from colony import Colony

# Small simulation
colony = Colony(grid_size=10, num_ants=5, num_food=3)
result = colony.run(50)
print(result)
# Example: {'delivered': 2, 'unique_food_left': 1, 'avg_pheromone': 0.03, 'max_pheromone': 2}

# Edge case: no food
colony = Colony(grid_size=10, num_ants=5, num_food=0)
result = colony.run(50)
print(result)
# Expected: delivered=0, unique_food_left=0

# Edge case: no ants
colony = Colony(grid_size=10, num_ants=0, num_food=5)
result = colony.run(50)
print(result)
# Expected: delivered=0 (since no ants can carry food)

Student Tasks

  1. Run the simulation and track how many food pieces are delivered.
  2. Modify the number of ants, food, or steps — how does it change the outcome?
  3. Add visualization (text-based or with matplotlib) to see the ants and pheromone trails.
  4. Reflect: How do simple local rules (turn, move, drop pheromone) create coordinated colony behavior?
  5. Challenge: Add “energy” to ants (they die if they don’t return to the nest in time).