Architecture‐Deep‐Dive - adylagad/CSCI-561_Genetic-Algorithm GitHub Wiki

Architecture Deep Dive

Complete technical overview of the codebase structure and design patterns.

🏗️ Project Structure


CSCI-561_Genetic-Algorithm/
│
├── main.py # Entry point
├── test_ga.py # Comprehensive test suite
│
├── genetic_algorithm/ # Main package
│ ├── **init**.py
│ │
│ ├── core/ # Core algorithm components
│ │ ├── **init**.py
│ │ ├── algorithm.py # Main GA orchestration
│ │ └── types.py # Type definitions
│ │
│ ├── operators/ # Genetic operators
│ │ ├── **init**.py
│ │ ├── crossover.py # Crossover strategies
│ │ ├── mutation.py # Mutation strategies
│ │ └── selection.py # Selection strategies
│ │
│ ├── initialization/ # Population initialization
│ │ ├── **init**.py
│ │ ├── random_init.py # Random initialization
│ │ └── nearest_neighbor.py # Nearest neighbor init
│ │
│ ├── evaluation/ # Fitness evaluation
│ │ ├── **init**.py
│ │ └── tsp_fitness.py # TSP-specific fitness
│ │
│ └── utils/ # Utility functions
│ ├── **init**.py
│ ├── distance.py # Distance calculations
│ └── io_utils.py # Input/Output
│
├── config/ # Configuration
│ ├── **init**.py
│ ├── ga_config.py # Configuration dataclass
│ └── default_config.json # Default parameters
│
└── input/ # Input files
└── input1.txt # 500 cities dataset

🎨 Design Patterns Used

1. Strategy Pattern

Where: Operators (crossover, mutation, selection)

Why: Easy to swap different algorithms without changing main code

Example:

# Instead of hard-coding one approach:
class GeneticAlgorithm:
    def crossover(self, parent1, parent2):
        # Fixed PMX crossover
        ...

# We use Strategy Pattern:
class GeneticAlgorithm:
    def __init__(self, crossover_strategy: CrossoverStrategy):
        self.crossover_strategy = crossover_strategy

    def crossover(self, parent1, parent2):
        return self.crossover_strategy.crossover(parent1, parent2)

# Now we can easily swap:
ga1 = GeneticAlgorithm(OrderCrossover())    # Use Order Crossover
ga2 = GeneticAlgorithm(PMXCrossover())      # Use PMX Crossover

Benefits:

  • Add new operators without modifying existing code (Open/Closed Principle)
  • Easy to A/B test different strategies
  • Clear separation of concerns

2. Dependency Injection

Where: Main algorithm class

Why: Loose coupling, easy testing, flexibility

Example:

# Dependencies injected via constructor:
def __init__(
    self,
    cities: CityList,
    fitness_evaluator: FitnessEvaluator,
    selection: SelectionStrategy,
    crossover: CrossoverStrategy,
    mutation: MutationStrategy,
    config: GAConfig
):
    # All dependencies provided externally
    ...

Benefits:

  • Easy to mock for testing
  • Clear dependencies
  • Flexibility in configuration

3. Factory Pattern (Implicit)

Where: Initialization strategies

Why: Create different types of initial populations

Example:

# Random initialization factory:
random_init = RandomPopulationInitializer(population_size)
population = random_init.initialize(cities)

# Nearest neighbor initialization factory:
nn_init = NearestNeighborInitializer(population_size)
population = nn_init.initialize(cities)

📦 Core Components

1. GeneticAlgorithm Class

Location: genetic_algorithm/core/algorithm.py

Responsibility: Orchestrate the evolutionary process

class GeneticAlgorithm:
    def run(self) -> GAResult:
        # 1. Initialize population
        population = self.initialize_population()

        # 2. Evolution loop
        for generation in range(self.generations):
            # 3. Evaluate fitness
            fitness = [self.evaluate(ind) for ind in population]

            # 4. Check convergence
            if self.has_converged():
                break

            # 5. Create next generation
            population = self.evolve(population, fitness)

        # 6. Return best solution
        return self.get_best_solution()

Key Methods:

Method Purpose
run() Main execution loop
evolve() Create next generation
evaluate() Calculate fitness
has_converged() Check stopping criteria
get_statistics() Collect metrics

2. Type System

Location: genetic_algorithm/core/types.py

Responsibility: Define core data types

# Type aliases for clarity:
City = Tuple[int, int, int]        # 3D coordinates
CityList = List[City]              # List of cities
Tour = List[int]                   # Tour as city indices
Population = List[Tour]            # List of tours
FitnessScores = List[float]        # List of fitness values

# Result type:
@dataclass
class GAResult:
    best_tour: Tour
    best_fitness: float
    best_cost: float
    statistics: Dict[str, Any]

Benefits:

  • Type checking catches errors early
  • Self-documenting code
  • Better IDE support

3. Configuration System

Location: config/ga_config.py

Responsibility: Manage algorithm parameters

@dataclass
class GAConfig:
    # Population parameters
    population_size: int = 500

    # Evolution parameters
    mutation_rate: float = 0.02
    number_of_generations: int = 3500

    # Convergence settings
    convergence_threshold: int = 500

    # I/O settings
    input_file: str = "input/input1.txt"

    @classmethod
    def from_json(cls, filepath: str) -> 'GAConfig':
        # Load from JSON file
        ...

Usage:

# Default config:
config = GAConfig()

# From JSON:
config = GAConfig.from_json("config/custom_config.json")

# Programmatic:
config = GAConfig(
    population_size=1000,
    mutation_rate=0.05
)

🔄 Data Flow

Complete Execution Flow

main.py
  │
  ├→ 1. Load config
  │    GAConfig.from_json()
  │
  ├→ 2. Read input
  │    read_input_file()
  │    └→ Returns: List[(x,y,z)]
  │
  ├→ 3. Initialize components
  │    ├→ FitnessEvaluator
  │    ├→ SelectionStrategy
  │    ├→ CrossoverStrategy
  │    ├→ MutationStrategy
  │    └→ PopulationInitializer
  │
  ├→ 4. Create GA
  │    GeneticAlgorithm(cities, operators, config)
  │
  ├→ 5. Run evolution
  │    ga.run()
  │    │
  │    ├→ Initialize population
  │    │    [random tours]
  │    │
  │    ├→ For each generation:
  │    │    │
  │    │    ├→ Evaluate fitness
  │    │    │    └→ fitness = 1 / tour_cost
  │    │    │
  │    │    ├→ Select parents
  │    │    │    └→ Roulette wheel selection
  │    │    │
  │    │    ├→ Crossover
  │    │    │    └→ Order crossover
  │    │    │
  │    │    ├→ Mutate
  │    │    │    └→ Swap mutation
  │    │    │
  │    │    └→ Elitism (keep best)
  │    │
  │    └→ Return GAResult
  │
  └→ 6. Display results
       format_output(result)

Data Transformations

Input File → Cities → Tours → Fitness → Selection → New Tours
   (I/O)    (Parse) (Init)   (Eval)     (Breed)     (Next Gen)

[Text]   → [(x,y,z)] → [0,1,2..](/adylagad/CSCI-561_Genetic-Algorithm/wiki/0,1,2..) → [0.001, ..] → [(p1,p2)] → [1,0,2..](/adylagad/CSCI-561_Genetic-Algorithm/wiki/1,0,2..)

🧩 Component Interactions

Initialization Phase

PopulationInitializer
         │
         ├→ Random Strategy
         │   └→ shuffle(range(num_cities))
         │
         └→ Nearest Neighbor Strategy
             └→ greedy construction from each city

Evolution Phase

Selection Strategy ──┐
                     ├→ Parent Pairs
Crossover Strategy ──┤
                     ├→ Offspring
Mutation Strategy ───┤
                     └→ Next Generation

Evaluation Phase

Tour [0,1,2,3...]
    │
    ├→ Distance Calculator
    │   └→ Σ distance(city[i], city[i+1])
    │
    └→ Fitness Calculator
        └→ 1 / total_distance

🎯 SOLID Principles

Single Responsibility Principle (SRP)

Each class has one job:

TSPFitnessEvaluator    → Only evaluates fitness
SwapMutation           → Only performs mutation
OrderCrossover         → Only does crossover
GeneticAlgorithm       → Only orchestrates

Open/Closed Principle (OCP)

Open for extension, closed for modification:

# Adding new crossover WITHOUT modifying existing code:

class CycleCrossover(CrossoverStrategy):
    def crossover(self, parent1: Tour, parent2: Tour) -> Tour:
        # New implementation
        ...

# Use it:
ga = GeneticAlgorithm(crossover=CycleCrossover())

Liskov Substitution Principle (LSP)

Any CrossoverStrategy can replace another:

def create_ga(crossover: CrossoverStrategy):
    return GeneticAlgorithm(crossover=crossover)

# All work identically:
ga1 = create_ga(OrderCrossover())
ga2 = create_ga(PMXCrossover())
ga3 = create_ga(CycleCrossover())  # Custom one

Interface Segregation Principle (ISP)

Small, focused interfaces:

class CrossoverStrategy(ABC):
    @abstractmethod
    def crossover(self, parent1: Tour, parent2: Tour) -> Tour:
        pass
    # Only one method - focused interface

class MutationStrategy(ABC):
    @abstractmethod
    def mutate(self, tour: Tour) -> Tour:
        pass
    # Separate interface for mutation

Dependency Inversion Principle (DIP)

Depend on abstractions, not concretions:

class GeneticAlgorithm:
    def __init__(
        self,
        crossover: CrossoverStrategy,  # Abstract
        mutation: MutationStrategy      # Abstract
    ):
        # Depends on interfaces, not concrete implementations

🧪 Testing Architecture

Location: test_ga.py

Structure:

class TestUtilityFunctions(unittest.TestCase):
    # Test distance, I/O functions

class TestTypes(unittest.TestCase):
    # Test type definitions

class TestConfiguration(unittest.TestCase):
    # Test GAConfig

class TestInitialization(unittest.TestCase):
    # Test RandomPopulationInitializer, NearestNeighborInitializer

class TestFitnessEvaluation(unittest.TestCase):
    # Test TSPFitnessEvaluator

class TestSelection(unittest.TestCase):
    # Test RouletteWheelSelection

class TestCrossover(unittest.TestCase):
    # Test OrderCrossover, PMXCrossover

class TestMutation(unittest.TestCase):
    # Test SwapMutation

class TestGeneticAlgorithm(unittest.TestCase):
    # Integration tests for GeneticAlgorithm

Coverage: 40+ tests covering all components

📊 Performance Considerations

Time Complexity

Component Complexity For 500 cities
Distance calc O(1) Constant
Fitness eval O(n) 500 operations
Selection O(p) 500 operations
Crossover O(n) 500 operations
Mutation O(1) Constant
Generation O(p·n) 250,000 ops
Full run O(g·p·n) ~437M ops

Where:

  • n = cities (500)
  • p = population (500)
  • g = generations (3500)

Space Complexity

Component Space For 500 cities
City list O(n) 1,500 ints
Population O(p·n) 250,000 ints
Fitness O(p) 500 floats
Total O(p·n) ~1MB

Optimization Techniques

  1. Elitism: Keep best solutions without re-evaluation
  2. Caching: Store city distances (if needed)
  3. Early stopping: Convergence detection
  4. Vectorization: NumPy for batch operations (future)

🔍 Code Quality Measures

Type Safety

# Every function has type hints:
def crossover(self, parent1: Tour, parent2: Tour) -> Tour:
    ...

# Every variable has clear type:
cities: CityList = read_input_file(filename)
population: Population = initializer.initialize(cities)

Documentation

class OrderCrossover(CrossoverStrategy):
    """
    Order Crossover (OX) operator.

    Preserves relative order of cities from parents while avoiding
    duplicates. Well-suited for TSP.

    Algorithm:
        1. Select random substring from parent1
        2. Copy to offspring
        3. Fill remaining positions with parent2's order

    Example:
        parent1: [1,2,3,4,5]
        parent2: [5,4,3,2,1]
        offspring: [4,5,3,2,1] (preserves [3] from p1, order from p2)
    """

Error Handling

def read_input_file(filepath: str) -> CityList:
    if not os.path.exists(filepath):
        raise FileNotFoundError(f"Input file not found: {filepath}")

    try:
        # Read file
        ...
    except Exception as e:
        logger.error(f"Error reading file: {e}")
        raise

🚀 Extension Points

Adding New Crossover

# 1. Create new class:
class CycleCrossover(CrossoverStrategy):
    def crossover(self, parent1: Tour, parent2: Tour) -> Tour:
        # Implement cycle crossover
        ...

# 2. Use it:
crossover = CycleCrossover()
ga = GeneticAlgorithm(crossover=crossover, ...)

Adding New Initialization

# 1. Create new class:
class ChristofideInit(PopulationInitializer):
    def initialize(self, cities: CityList) -> Population:
        # Implement Christofides algorithm
        ...

# 2. Use it:
init = ChristofideInit(population_size=500)
population = init.initialize(cities)

Adding New Selection

# 1. Create new class:
class TournamentSelection(SelectionStrategy):
    def select(self, population: Population,
               fitness: FitnessScores) -> Tuple[Tour, Tour]:
        # Implement tournament selection
        ...

# 2. Use it:
selection = TournamentSelection(tournament_size=5)
ga = GeneticAlgorithm(selection=selection, ...)

📚 Further Reading


Ready to extend?Custom Operators Tutorial