Simulated Annealing Algorithm | CSU083 - Shoolini University

Simulated Annealing

1. Prerequisites for Understanding Simulated Annealing

Before diving into Simulated Annealing, you should have a strong understanding of the following foundational concepts:

1.1 Optimization Problems

Optimization involves finding the best solution from a set of possible solutions, often under constraints.

1.2 Hill Climbing

A local search algorithm that iteratively moves towards better solutions but can get stuck in local optima.

1.3 Probability and Randomness

Basic probability concepts, including the idea of randomness and probability distributions.

1.4 Metropolis Algorithm

A key component of Simulated Annealing, which allows accepting worse solutions probabilistically to escape local optima.

1.5 Exponential Decay

Understanding temperature schedules and how they influence search progression in Simulated Annealing.

2. What is Simulated Annealing?

Simulated Annealing (SA) is a probabilistic optimization algorithm inspired by the physical annealing process in metallurgy, where materials are heated and then slowly cooled to settle into a low-energy state.

2.1 Core Idea

SA balances exploration (searching widely) and exploitation (refining the best solutions) using a temperature parameter that decreases over time.

2.2 Algorithm Steps

2.3 Mathematical Formulation

A worse solution is accepted with probability:

$$ P = e^{-\frac{\Delta E}{T}} $$

where:

3. Why Does Simulated Annealing Exist?

Simulated Annealing was developed to overcome the limitations of greedy algorithms like Hill Climbing, which get stuck in local optima.

3.1 Real-World Applications

4. When Should You Use Simulated Annealing?

Simulated Annealing is best used when:

4.1 Problem Characteristics

4.2 Computational Considerations

5. How Does Simulated Annealing Compare to Alternatives?

5.1 Strengths

5.2 Weaknesses

5.3 Comparison with Other Methods

Algorithm Strengths Weaknesses
Simulated Annealing Handles large search spaces, avoids local optima. Slow, requires parameter tuning.
Hill Climbing Fast, simple to implement. Gets stuck in local optima.
Genetic Algorithms Good for highly complex problems, explores multiple solutions in parallel. Computationally expensive.
Gradient Descent Efficient for differentiable functions. Fails on non-continuous functions, stuck in local minima.

6. Basic Implementation of Simulated Annealing

Here is a basic implementation of Simulated Annealing in Python. This implementation solves a simple function optimization problem.

6.1 Problem Definition

We aim to minimize the function:

$$ f(x) = x^2 + 4\sin(5x) $$

in the range $-10 \leq x \leq 10$.


import numpy as np
import random
import math

# Objective function: f(x) = x^2 + 4*sin(5x)
def objective_function(x):
    return x**2 + 4 * math.sin(5 * x)

# Simulated Annealing Algorithm
def simulated_annealing(T_initial, cooling_rate, T_min, max_iterations):
    # Initialize with a random solution
    x_current = random.uniform(-10, 10)
    f_current = objective_function(x_current)
    
    T = T_initial  # Starting temperature

    while T > T_min:
        for _ in range(max_iterations):
            # Generate a new candidate solution (small random move)
            x_new = x_current + random.uniform(-0.5, 0.5)
            x_new = max(-10, min(10, x_new))  # Keep within bounds
            f_new = objective_function(x_new)

            # Acceptance criteria
            delta = f_new - f_current
            if delta < 0 or random.uniform(0, 1) < math.exp(-delta / T):
                x_current, f_current = x_new, f_new

        # Cool down the temperature
        T *= cooling_rate
    
    return x_current, f_current

# Parameters
T_initial = 1000     # Initial temperature
cooling_rate = 0.9   # Cooling factor
T_min = 0.001        # Minimum temperature
max_iterations = 100 # Iterations per temperature level

# Run the algorithm
best_x, best_f = simulated_annealing(T_initial, cooling_rate, T_min, max_iterations)
print(f"Optimal solution: x = {best_x:.4f}, f(x) = {best_f:.4f}")

7. Dry Run of Simulated Annealing

7.1 Initial Conditions

7.2 Step-by-Step Execution

Step Temperature Current x New x ΔE (Change in Cost) Accepted?
1 1000 3.5 3.2 -0.8 Yes (better solution)
2 1000 3.2 3.8 +1.1 Yes (probabilistic acceptance)
3 1000 3.8 3.6 -0.5 Yes (better solution)
4 900 3.6 3.9 +1.2 No (low probability of acceptance)
5 810 3.6 3.1 -0.9 Yes (better solution)

7.3 Observations

8. Time & Space Complexity Analysis

8.1 Worst-Case Complexity

In the worst case, the algorithm explores the entire search space before converging. This happens if:

Time Complexity: $$ O(N \cdot M) $$

8.2 Best-Case Complexity

In the best case, the algorithm quickly finds the optimal solution, requiring only a few iterations per temperature level.

Time Complexity: $$ O(N) $$

8.3 Average-Case Complexity

In most scenarios, the algorithm performs an intermediate number of iterations before converging.

Time Complexity: $$ O(N \cdot M) $$

9. Space Complexity Analysis

Simulated Annealing is a memory-efficient algorithm since it only maintains:

Space Complexity: $$ O(1) $$ (Constant space, independent of input size)

10. Trade-offs in Simulated Annealing

10.1 Exploration vs. Exploitation

10.2 Cooling Schedule Trade-offs

10.3 Computational Cost vs. Solution Quality

10.4 Comparison with Other Methods

Algorithm Time Complexity Space Complexity Strengths Weaknesses
Simulated Annealing O(N ⋅ M) O(1) Escapes local optima, handles large spaces. Slow convergence, parameter tuning needed.
Genetic Algorithms O(N ⋅ M) O(N) Good for highly complex problems. High memory usage.
Gradient Descent O(K) O(1) Fast for differentiable functions. Fails on non-continuous problems.
Hill Climbing O(N) O(1) Simple and fast. Gets stuck in local optima.

11. Optimizations & Variants

11.1 Common Optimizations

11.2 Variants of Simulated Annealing

12. Comparing Iterative vs. Recursive Implementations

12.1 Iterative Implementation

An iterative approach is the standard way to implement Simulated Annealing, where a loop repeatedly updates the temperature and state.


def simulated_annealing_iterative(T_initial, cooling_rate, T_min, max_iterations):
    x_current = random.uniform(-10, 10)
    f_current = objective_function(x_current)
    T = T_initial

    while T > T_min:
        for _ in range(max_iterations):
            x_new = x_current + random.uniform(-0.5, 0.5)
            x_new = max(-10, min(10, x_new))
            f_new = objective_function(x_new)

            delta = f_new - f_current
            if delta < 0 or random.uniform(0, 1) < math.exp(-delta / T):
                x_current, f_current = x_new, f_new

        T *= cooling_rate

    return x_current, f_current

12.2 Recursive Implementation

A recursive implementation replaces loops with function calls, but recursion depth can become an issue.


def simulated_annealing_recursive(x_current, f_current, T, cooling_rate, T_min, max_iterations):
    if T <= T_min:
        return x_current, f_current

    for _ in range(max_iterations):
        x_new = x_current + random.uniform(-0.5, 0.5)
        x_new = max(-10, min(10, x_new))
        f_new = objective_function(x_new)

        delta = f_new - f_current
        if delta < 0 or random.uniform(0, 1) < math.exp(-delta / T):
            x_current, f_current = x_new, f_new

    return simulated_annealing_recursive(x_current, f_current, T * cooling_rate, cooling_rate, T_min, max_iterations)

12.3 Efficiency Comparison

Implementation Time Complexity Space Complexity Pros Cons
Iterative O(N ⋅ M) O(1) Memory-efficient, avoids recursion limit. Code might be less intuitive for some recursive-style problems.
Recursive O(N ⋅ M) O(N) (stack depth) Elegant code structure. Risk of stack overflow for large N.

12.4 Verdict

For practical usage, the iterative implementation is preferred due to its lower space complexity and avoidance of recursion depth limits.

13. Edge Cases & Failure Handling

13.1 Common Pitfalls in Simulated Annealing

14. Writing Test Cases for Simulated Annealing

To ensure the correctness and robustness of the implementation, we write test cases for different scenarios.

14.1 Test Case Scenarios

Test Case Input Expected Output
Basic Functionality Minimize \( f(x) = x^2 \) in [-10,10] Near x=0 (global minimum)
Boundary Handling Initial x near boundary (-10 or 10) Final x should remain in bounds
Fast Cooling Cooling rate = 0.1 Poor solution (trapped in local minima)
Slow Cooling Cooling rate = 0.999 Good solution but long execution time
Randomness Consistency Fix random seed Consistent results across runs

14.2 Python Test Cases


import random

def test_simulated_annealing():
    # Fix randomness for consistent tests
    random.seed(42)

    # Test case 1: Basic functionality
    best_x, best_f = simulated_annealing(1000, 0.9, 0.001, 100)
    assert -1 <= best_x <= 1, "Test failed: Did not find the minimum near zero"

    # Test case 2: Boundary handling
    best_x, best_f = simulated_annealing(1000, 0.9, 0.001, 100)
    assert -10 <= best_x <= 10, "Test failed: Boundary violation"

    # Test case 3: Fast cooling (expect poor result)
    best_x, best_f = simulated_annealing(1000, 0.1, 0.001, 100)
    assert abs(best_f) > 5, "Test failed: Fast cooling should lead to suboptimal results"

    # Test case 4: Slow cooling (expect good result)
    best_x, best_f = simulated_annealing(1000, 0.999, 0.001, 100)
    assert abs(best_f) < 1, "Test failed: Slow cooling should find a near-optimal solution"

    print("All tests passed!")

test_simulated_annealing()

15. Real-World Failure Scenarios in Simulated Annealing

15.1 Industry Use Case Failures

15.2 Handling Real-World Failures

16. Real-World Applications & Industry Use Cases

16.1 Applications Across Domains

17. Open-Source Implementations of Simulated Annealing

17.1 Libraries & Tools

17.2 Example: `simanneal` Python Package


from simanneal import Annealer

class TravelingSalesman(Annealer):
    def __init__(self, state, distance_matrix):
        self.distance_matrix = distance_matrix
        super().__init__(state)

    def move(self):
        a, b = sorted(random.sample(range(len(self.state)), 2))
        self.state[a], self.state[b] = self.state[b], self.state[a]

    def energy(self):
        return sum(self.distance_matrix[self.state[i-1]][self.state[i]] for i in range(len(self.state)))

# Example distance matrix (TSP problem)
distance_matrix = [
    [0, 10, 15, 20],
    [10, 0, 35, 25],
    [15, 35, 0, 30],
    [20, 25, 30, 0]
]
initial_state = [0, 1, 2, 3]

tsp = TravelingSalesman(initial_state, distance_matrix)
best_solution, best_distance = tsp.anneal()
print("Optimal Route:", best_solution, "Distance:", best_distance)

18. Project: Job Scheduling Using Simulated Annealing

18.1 Problem Statement

Optimize job scheduling for a factory to minimize production time by efficiently assigning jobs to machines.

18.2 Implementation


import numpy as np
import random
import math

# Define Job Scheduling Problem
jobs = [3, 5, 2, 7, 4]  # Processing times
num_machines = 2

def objective_function(schedule):
    machine_times = [0] * num_machines
    for job in schedule:
        min_machine = machine_times.index(min(machine_times))
        machine_times[min_machine] += job
    return max(machine_times)  # Minimize max completion time

def simulated_annealing(schedule, T_initial, cooling_rate, T_min, max_iterations):
    current_schedule = schedule[:]
    best_schedule = current_schedule[:]
    best_cost = objective_function(current_schedule)
    T = T_initial

    while T > T_min:
        for _ in range(max_iterations):
            new_schedule = current_schedule[:]
            i, j = random.sample(range(len(schedule)), 2)
            new_schedule[i], new_schedule[j] = new_schedule[j], new_schedule[i]
            new_cost = objective_function(new_schedule)

            if new_cost < best_cost or random.uniform(0, 1) < math.exp((best_cost - new_cost) / T):
                current_schedule = new_schedule
                best_cost = new_cost

        T *= cooling_rate

    return best_schedule, best_cost

# Initial job order
initial_schedule = random.sample(jobs, len(jobs))

# Parameters
T_initial = 1000
cooling_rate = 0.9
T_min = 0.001
max_iterations = 100

# Run the algorithm
best_schedule, best_time = simulated_annealing(initial_schedule, T_initial, cooling_rate, T_min, max_iterations)

print("Optimized Job Schedule:", best_schedule)
print("Minimum Completion Time:", best_time)

18.3 Expected Output

19. Competitive Programming & System Design Integration

19.1 Using Simulated Annealing in Competitive Programming

Simulated Annealing is effective in problems where:

19.2 Problem Categories

19.3 Simulated Annealing in System Design

20. Assignments & Practice Tasks

20.1 Solve at Least 10 Problems Using Simulated Annealing

Solve these problems from competitive programming platforms:

  1. Minimizing a Function: Optimize a mathematical function with constraints.
  2. Traveling Salesman Problem (TSP): Find the shortest path visiting multiple cities.
  3. Job Scheduling: Optimize job assignments for minimal makespan.
  4. Graph Coloring: Minimize the number of colors needed.
  5. Knapsack Problem with Randomized Weights: Solve the 0/1 Knapsack problem using SA.
  6. Optimizing Wireless Sensor Placement: Maximize coverage using SA.
  7. Network Routing Optimization: Minimize network latency.
  8. Stock Portfolio Optimization: Balance risk and returns with SA.
  9. Game AI Optimization: Tune AI parameters using SA.
  10. Hyperparameter Tuning in Machine Learning: Optimize model parameters.

20.2 System Design Integration Task

Design a cloud server allocation system where incoming tasks are dynamically distributed using Simulated Annealing to optimize:

20.3 Time-Constrained Implementation Practice

Set a timer (30 minutes) and attempt to: