SA Lab Walkthrough
Unit 4: Optimization and Constraint Satisfaction — SA Lab Walkthrough
|
This walkthrough is an optional companion to the Simulated Annealing Lab. Review it after completing and submitting your own solution. |
Overview
This walkthrough presents complete implementations for all four lab parts, with line-by-line explanations and analysis of results. The key insight throughout: SA’s ability to escape local optima comes entirely from the Metropolis acceptance criterion, which allows controlled acceptance of worse solutions.
Part 1 Solutions: Basic SA Implementation
Function 1: Random Neighbor
def random_neighbor(current, step_size=0.1):
"""Generate a random neighboring state."""
change = np.random.uniform(-step_size, step_size)
return current + change
Function 2: Acceptance Probability
def acceptance_probability(current_energy, neighbor_energy, temp):
"""Calculate probability of accepting a move (Metropolis criterion)."""
if neighbor_energy < current_energy:
return 1.0 # always accept better
else:
delta_e = neighbor_energy - current_energy # positive: neighbor is worse
return math.exp(-delta_e / temp)
Function 3: Main SA Algorithm
def simulated_annealing(objective_function, initial_state,
initial_temp, cooling_rate, max_iterations):
"""Run simulated annealing to minimize objective_function."""
current = initial_state
current_energy = objective_function(current)
best = current
best_energy = current_energy
history = [current_energy]
temp = initial_temp
for iteration in range(max_iterations):
# Generate neighbor and evaluate
neighbor = random_neighbor(current, step_size=0.5)
neighbor_energy = objective_function(neighbor)
# Metropolis acceptance
prob = acceptance_probability(current_energy, neighbor_energy, temp)
if np.random.random() < prob:
current = neighbor
current_energy = neighbor_energy
# Track best solution found
if current_energy < best_energy:
best = current
best_energy = current_energy
history.append(current_energy)
# Cool temperature with minimum floor
temp *= cooling_rate
temp = max(temp, 0.01)
return best, best_energy, history
Part 2 Solutions: Cooling Schedules
def linear_cooling(t, t0, max_t):
"""T(t) = T0 * (1 - t/max_t)"""
return t0 * (1 - t / max_t)
def exponential_cooling(t, t0, alpha):
"""T(t) = T0 * alpha^t"""
return t0 * (alpha ** t)
def logarithmic_cooling(t, t0):
"""T(t) = T0 / log(1 + t)"""
return t0 / math.log(1 + t)
Part 3 Results: SA vs. Hill Climbing
Expected Output
Results on Rastrigin function (20 runs each):
Hill Climbing: mean=4.21, std=2.13, best=0.98
SA (T=100, α=0.95): mean=0.83, std=0.47, best=0.01
Results on Schwefel function (20 runs each):
Hill Climbing: mean=87.3, std=44.1, best=12.5
SA (T=100, α=0.95): mean=11.2, std=7.9, best=0.3
Analysis
SA achieves roughly 80% better mean solution quality on the Rastrigin function and 87% better on the Schwefel function compared to hill climbing. The difference arises because both functions have many local optima: hill climbing gets trapped in the first one it finds, while SA’s temperature mechanism allows it to escape and search more broadly.
On the unimodal quadratic function, both algorithms perform nearly identically — there are no local optima to escape, so the temperature mechanism provides no advantage.
The standard deviation for SA is also much lower (more consistent results), which is valuable in practice: you can rely on SA to produce good results reliably, not just occasionally.
Part 4 Analysis: Parameter Effects
Temperature Analysis Results
Higher initial temperatures lead to better solutions up to a point. Below T₀ ≈ 10, SA behaves like hill climbing and gets trapped. Above T₀ ≈ 50, SA explores well and finds near-optimal solutions. Excessively high T₀ > 500 slightly hurts because too much time is spent on random exploration with little refinement.
The sweet spot for the Rastrigin function in 1D: T₀ ∈ [50, 200].
Cooling Rate Analysis Results
Faster cooling (lower α) produces worse solutions because the algorithm does not spend enough time at each temperature level. At α = 0.80, cooling is so rapid that SA behaves like a few hundred random-restart hill climbing runs. At α = 0.999, cooling is so slow that SA barely reaches a cool phase within 5,000 iterations.
Recommended: α ∈ [0.93, 0.97] for most problems with 5,000 iterations. Increase α (slow cooling) for harder, higher-dimensional problems; decrease it when function evaluations are expensive.
Key Takeaways
The simulated annealing algorithm has three essential components:
-
Random neighbor selection — ensures full exploration of the neighborhood
-
Metropolis acceptance criterion — allows controlled acceptance of worse moves, scaled by temperature
-
Cooling schedule — transitions the algorithm from broad exploration to focused refinement
Remove any one component and SA degrades: without random neighbors you get hill climbing, without Metropolis acceptance you get random search, without cooling you get a random walk. The power of SA comes from all three working together.
Lab code adapted from aima-python, MIT License, Copyright 2016 aima-python contributors.
This work is licensed under CC BY-SA 4.0.