Informed (Heuristic) Search
Simulated Annealing Search (process visualization)
Simulated Annealing (SA) is an optimization algorithm inspired by the annealing process in metallurgy, where materials are heated and slowly
cooled to reach a stable state with minimal energy. It is used to find approximate solutions to optimization problems by iteratively exploring
potential solutions and occasionally accepting worse solutions to escape local optima.
The goal in this example here is to find the minimum point of $f(x) = x²$, which is clearly at $x = 0$, where the function value is also $0$.
Example of class JS Sample 2
:
<script>
// Get DOM elements
const initialTemperatureInput = document.getElementById('initialTemperature');
const coolingRateInput = document.getElementById('coolingRate');
const stopTemperatureInput = document.getElementById('stopTemperature');
const initialSolutionInput = document.getElementById('initialSolution');
const startBtn = document.getElementById('startBtn');
const pauseBtn = document.getElementById('pauseBtn');
const resetBtn = document.getElementById('resetBtn');
const functionCanvas = document.getElementById('functionCanvas');
const temperatureCanvas = document.getElementById('temperatureCanvas');
const logContainer = document.getElementById('logContainer');
const resultsContainer = document.getElementById('resultsContainer');
const bestResultContainer = document.getElementById('bestResultContainer');
const optimalSolutionEl = document.getElementById('optimalSolution');
const finalEnergyEl = document.getElementById('finalEnergy');
const iterationCountEl = document.getElementById('iterationCount');
const bestSolutionEl = document.getElementById('bestSolution');
const bestEnergyEl = document.getElementById('bestEnergy');
const bestIterationEl = document.getElementById('bestIteration');
// Set canvas dimensions based on container
function resizeCanvases() {
const functionContainer = functionCanvas.parentElement;
const temperatureContainer = temperatureCanvas.parentElement;
functionCanvas.width = functionContainer.offsetWidth;
functionCanvas.height = functionContainer.offsetHeight;
temperatureCanvas.width = temperatureContainer.offsetWidth;
temperatureCanvas.height = temperatureContainer.offsetHeight;
}
// Call resize initially
resizeCanvases();
// Resize when window changes
window.addEventListener('resize', resizeCanvases);
// Initialize canvas contexts
const functionCtx = functionCanvas.getContext('2d');
const temperatureCtx = temperatureCanvas.getContext('2d');
// Simulation variables
let iterations = [];
let simulationInterval = null;
let running = false;
let bestSolution = null;
let bestEnergy = Infinity;
let bestIteration = 0;
// Objective function: f(x) = x^2
function objectiveFunction(x) {
return Math.pow(x, 2);
}
// Generate a random neighbor solution
function getRandomNeighbor(x, temp) {
// The step size is related to the current temperature
return x + ((Math.random() - 0.5) * 2 * temp / 100);
}
// Calculate acceptance probability
function acceptanceProbability(currentEnergy, newEnergy, temperature) {
if (newEnergy < currentEnergy) {
return 1.0;
}
return Math.exp((currentEnergy - newEnergy) / temperature);
}
// Add log entry
function addLogEntry(message, isAccepted, isBest = false) {
const logEntry = document.createElement('div');
let className = `log-entry ${isAccepted ? 'log-accepted' : 'log-rejected'}`;
if (isBest) {
className += ' log-best';
}
logEntry.className = className;
logEntry.textContent = message;
logContainer.appendChild(logEntry);
logContainer.scrollTop = logContainer.scrollHeight;
}
// Clear log
function clearLog() {
logContainer.innerHTML = '';
}
// Draw function canvas with grid
function drawFunctionCanvas() {
const width = functionCanvas.width;
const height = functionCanvas.height;
// Clear canvas
functionCtx.clearRect(0, 0, width, height);
// Set the display range for Y values
const minYDisplay = -40;
const maxYDisplay = 150;
const yRange = maxYDisplay - minYDisplay;
const yOffset = 3;
// Calculate vertical center position (where y=0 should be drawn)
const yZeroPosition = height * ((maxYDisplay) / yRange);
// Draw grid
functionCtx.strokeStyle = '#e0e0e0';
functionCtx.lineWidth = 0.5;
// Vertical grid lines
for (let x = 0; x <= width; x += 10) {
functionCtx.beginPath();
functionCtx.moveTo(x, 0);
functionCtx.lineTo(x, height);
functionCtx.stroke();
}
// Horizontal grid lines
for (let y = 0; y <= height; y += 10) {
functionCtx.beginPath();
functionCtx.moveTo(0, y);
functionCtx.lineTo(width, y);
functionCtx.stroke();
}
// Draw axes
functionCtx.strokeStyle = '#888';
functionCtx.lineWidth = 1;
functionCtx.beginPath();
// X axis (at y=0 position)
functionCtx.moveTo(0, yZeroPosition + yOffset);
functionCtx.lineTo(width, yZeroPosition + yOffset);
// Y axis (at center of width)
functionCtx.moveTo(width/2 - yOffset, 0);
functionCtx.lineTo(width/2 - yOffset, height);
functionCtx.stroke();
// Draw axis labels
functionCtx.fillStyle = '#999';
functionCtx.font = '12px Arial';
// X-axis labels
const xLabels = [-25, -20, -15, -10, -5, 0, 5, 10, 15, 20, 25];
const scale = 20; // Scale factor for visualization
xLabels.forEach(x => {
const px = width/2 + x * scale;
functionCtx.fillText(x.toString(), px - 5, yZeroPosition + 15);
});
// Y-axis labels (show values from minYDisplay to maxYDisplay)
const yLabels = [-50, -25, 0, 25, 50, 75, 100, 125, 150];
yLabels.forEach(y => {
// Calculate position using the full range
const py = height - ((y - minYDisplay) / yRange) * height;
if (py >= 0 && py <= height) { // Only draw if within canvas
functionCtx.fillText(y.toString(), width/2 + 5, py + 5);
}
});
// Draw function f(x) = x^2
functionCtx.strokeStyle = '#8884d8';
functionCtx.lineWidth = 2;
functionCtx.beginPath();
for (let px = 0; px < width; px++) {
const x = (px - width/2) / scale;
const y = objectiveFunction(x);
// Only draw if within display range
if (y >= minYDisplay && y <= maxYDisplay) {
// Convert y value to canvas position
const py = height - ((y - minYDisplay) / yRange) * height + yOffset;
if (px === 0) {
functionCtx.moveTo(px, py);
} else {
functionCtx.lineTo(px, py);
}
}
}
functionCtx.stroke();
// Draw iterations (points)
if (iterations && iterations.length > 0) {
iterations.forEach((iter, index) => {
// Only draw points within display range
if (iter.energy >= minYDisplay && iter.energy <= maxYDisplay) {
const px = width/2 + iter.solution * scale;
// Convert energy value to canvas position
const py = height - ((iter.energy - minYDisplay) / yRange) * height;
functionCtx.fillStyle = iter.accepted ? '#00ff00' : '#ff7300';
functionCtx.beginPath();
functionCtx.arc(px, py, 3, 0, Math.PI * 2);
functionCtx.fill();
}
});
// Draw current solution
const current = iterations[iterations.length - 1];
if (current.energy >= minYDisplay && current.energy <= maxYDisplay) {
const px = width/2 + current.solution * scale;
// Convert energy value to canvas position
const py = height - ((current.energy - minYDisplay) / yRange) * height;
functionCtx.fillStyle = '#ff0000';
functionCtx.beginPath();
functionCtx.arc(px, py, 5, 0, Math.PI * 2);
functionCtx.fill();
}
// Draw best solution found
if (bestSolution !== null && bestEnergy >= minYDisplay && bestEnergy <= maxYDisplay) {
const px = width/2 + bestSolution * scale;
// Convert energy value to canvas position
const py = height - ((bestEnergy - minYDisplay) / yRange) * height;
// Draw a blue circle for best solution
functionCtx.fillStyle = '#2196f3';
functionCtx.beginPath();
functionCtx.arc(px, py, 6, 0, Math.PI * 2);
functionCtx.fill();
// Draw a diamond around best solution
functionCtx.strokeStyle = '#2196f3';
functionCtx.lineWidth = 2;
functionCtx.beginPath();
functionCtx.moveTo(px, py - 10);
functionCtx.lineTo(px + 10, py);
functionCtx.lineTo(px, py + 10);
functionCtx.lineTo(px - 10, py);
functionCtx.closePath();
functionCtx.stroke();
}
}
}
// Draw temperature canvas with grid
function drawTemperatureCanvas() {
const width = temperatureCanvas.width;
const height = temperatureCanvas.height;
// Clear canvas
temperatureCtx.clearRect(0, 0, width, height);
if (!iterations || iterations.length === 0) {
return;
}
// Draw grid
temperatureCtx.strokeStyle = '#e0e0e0';
temperatureCtx.lineWidth = 0.5;
// Vertical grid lines
for (let x = 50; x <= width; x += 50) {
temperatureCtx.beginPath();
temperatureCtx.moveTo(x, 10);
temperatureCtx.lineTo(x, height - 30);
temperatureCtx.stroke();
}
// Horizontal grid lines
for (let y = 10; y <= height - 30; y += 50) {
temperatureCtx.beginPath();
temperatureCtx.moveTo(50, y);
temperatureCtx.lineTo(width - 10, y);
temperatureCtx.stroke();
}
// Find max values for scaling
const maxTemp = Math.max(...iterations.map(i => i.temperature));
const maxEnergy = Math.max(...iterations.map(i => i.energy));
const maxIter = iterations.length;
const xOffset = 20;
// Draw axes
temperatureCtx.strokeStyle = '#666';
temperatureCtx.lineWidth = 1;
temperatureCtx.beginPath();
temperatureCtx.moveTo(xOffset, 30);
temperatureCtx.lineTo(xOffset, height - 30);
temperatureCtx.lineTo(width - 10, height - 30);
temperatureCtx.stroke();
// Draw labels
temperatureCtx.fillStyle = '#666';
temperatureCtx.font = '12px Arial';
temperatureCtx.fillText('Temperature / Energy', 10, 20);
temperatureCtx.fillText('Iterations', width - 60, height - 10);
// Draw temperature line
if (maxIter > 0) {
temperatureCtx.strokeStyle = '#8884d8';
temperatureCtx.lineWidth = 2;
temperatureCtx.beginPath();
iterations.forEach((iter, index) => {
const px = xOffset + (index / maxIter) * (width - 60);
const py = (height - 30) - (iter.temperature / maxTemp) * (height - 40);
if (index === 0) {
temperatureCtx.moveTo(px, py);
} else {
temperatureCtx.lineTo(px, py);
}
});
temperatureCtx.stroke();
// Draw energy line
temperatureCtx.strokeStyle = '#82ca9d';
temperatureCtx.lineWidth = 2;
temperatureCtx.beginPath();
iterations.forEach((iter, index) => {
const px = xOffset + (index / maxIter) * (width - 60);
const py = (height - 30) - (iter.energy / maxEnergy) * (height - 40);
if (index === 0) {
temperatureCtx.moveTo(px, py);
} else {
temperatureCtx.lineTo(px, py);
}
});
temperatureCtx.stroke();
// Mark the best solution found
if (bestIteration > 0) {
const px = xOffset + (bestIteration / maxIter) * (width - 60);
const py = (height - 30) - (bestEnergy / maxEnergy) * (height - 40);
temperatureCtx.fillStyle = '#2196f3';
temperatureCtx.beginPath();
temperatureCtx.arc(px, py, 5, 0, Math.PI * 2);
temperatureCtx.fill();
}
}
}
// Update best solution display
function updateBestSolutionDisplay() {
if (bestSolution !== null) {
bestResultContainer.style.display = 'block';
bestSolutionEl.textContent = bestSolution.toFixed(4);
bestEnergyEl.textContent = bestEnergy.toFixed(4);
bestIterationEl.textContent = bestIteration;
}
}
// Initialize the simulation with default parameters
function initializeSimulation() {
// Get default parameters
const initialSolution = parseFloat(initialSolutionInput.value);
const currentEnergy = objectiveFunction(initialSolution);
const initialTemperature = parseFloat(initialTemperatureInput.value);
// Create initial state
iterations = [{
iteration: 0,
temperature: initialTemperature,
solution: initialSolution,
energy: currentEnergy,
accepted: true,
type: 'initial'
}];
// Set best solution to initial
bestSolution = initialSolution;
bestEnergy = currentEnergy;
bestIteration = 0;
// Add initial log
addLogEntry(`Initial position: x = ${initialSolution.toFixed(4)}, f(x) = ${currentEnergy.toFixed(4)}`, true);
// Draw initial state
drawFunctionCanvas();
drawTemperatureCanvas();
// Show best solution
updateBestSolutionDisplay();
}
// Reset the simulation
function resetSimulation() {
// Stop any running simulation
if (running) {
clearInterval(simulationInterval);
running = false;
}
removeAllValidationErrors();
// Reset UI
startBtn.textContent = 'Start Simulation';
startBtn.disabled = false;
pauseBtn.disabled = true;
// Hide results
resultsContainer.style.display = 'none';
// Clear log
clearLog();
// Re-initialize with default values
initializeSimulation();
}
// Pause the simulation
function pauseSimulation() {
if (!running) return;
clearInterval(simulationInterval);
running = false;
startBtn.textContent = 'Resume Simulation';
startBtn.disabled = false;
pauseBtn.disabled = true;
addLogEntry('Simulation paused.', false);
}
// Function to validate all number input fields
function validateAllNumberInputs() {
// Get all number input fields with a more reliable selector
const numberInputs = document.querySelectorAll('input[type="number"]');
let isValid = true;
let firstInvalidInput = null;
// Check each input field
for (const input of numberInputs) {
// Skip if input doesn't exist
if (!input) continue;
// Get validation attributes
const value = input.value.trim();
const numValue = parseFloat(value);
const min = parseFloat(input.getAttribute('min'));
const max = parseFloat(input.getAttribute('max'));
const step = parseFloat(input.getAttribute('step') || '1');
const name = input.getAttribute('title') || 'This field';
// Reset previous error styling
input.style.borderColor = '';
// Check for empty value
if (value === '' || isNaN(numValue)) {
showError(input, `${name} must have a numeric value`);
isValid = false;
firstInvalidInput = firstInvalidInput || input;
continue;
}
// Check min value if specified
if (!isNaN(min) && numValue < min) {
showError(input, `${name} must be at least ${min}`);
isValid = false;
firstInvalidInput = firstInvalidInput || input;
continue;
}
// Check max value if specified
if (!isNaN(max) && numValue > max) {
showError(input, `${name} must be at most ${max}`);
isValid = false;
firstInvalidInput = firstInvalidInput || input;
continue;
}
// Improved step validation with better floating point handling
if (!isNaN(step) && step > 0) {
const baseline = !isNaN(min) ? min : 0;
const diff = Math.abs(numValue - baseline);
const remainder = diff / step;
const roundedRemainder = Math.round(remainder);
if (Math.abs(remainder - roundedRemainder) > 0.000001) {
showError(input, `${name} must be in increments of ${step} from ${baseline}`);
isValid = false;
firstInvalidInput = firstInvalidInput || input;
}
}
}
// Focus on the first invalid input if any
if (firstInvalidInput) {
firstInvalidInput.focus();
}
return isValid;
}
// Function to show error for an input
function showError(input, message) {
// Add red border to highlight the error
input.classList.add('validation-error-field');
// Create or update error message
let errorElement = document.getElementById(`${input.id}-error`);
if (!errorElement) {
errorElement = document.createElement('div');
errorElement.id = `${input.id}-error`;
errorElement.style.color = 'red';
errorElement.classList.add('validation-error-text');
errorElement.style.fontSize = '12px';
errorElement.style.marginTop = '5px';
input.parentNode.appendChild(errorElement);
}
errorElement.textContent = message;
// Remove error message after 5 seconds
setTimeout(() => {
if (errorElement.parentNode) {
errorElement.parentNode.removeChild(errorElement);
}
input.style.borderColor = '';
}, 5000);
}
// Function to remove all validation error elements
function removeAllValidationErrors() {
const errorElements = document.querySelectorAll('.validation-error-text');
errorElements.forEach(element => {
element.parentNode.removeChild(element);
});
const elements = document.querySelectorAll('.validation-error-text');
elements.forEach(element => {
element.classList.remove('validation-error-text');
});
}
// Run the simulation
function runSimulation(event) {
// Prevent multiple runs
if (running) return;
// Validate all number inputs before starting simulation
if (!validateAllNumberInputs()) {
event.preventDefault();
return false;
}
// Set running state
running = true;
startBtn.textContent = 'Running...';
startBtn.disabled = true;
pauseBtn.disabled = false;
// Check if this is a fresh start or resume
const isFreshStart = iterations.length <= 1;
if (isFreshStart) {
// Hide results
resultsContainer.style.display = 'none';
// Clear log
clearLog();
// Reset best solution tracking
bestSolution = null;
bestEnergy = Infinity;
bestIteration = 0;
}
// Get parameters
const initialTemperature = parseFloat(initialTemperatureInput.value);
const coolingRate = parseFloat(coolingRateInput.value);
const stopTemperature = parseFloat(stopTemperatureInput.value);
const initialSolution = parseFloat(initialSolutionInput.value);
let currentSolution, currentEnergy, temperature, iterationCount;
if (isFreshStart) {
// Initialize variables for fresh start
currentSolution = initialSolution;
currentEnergy = objectiveFunction(currentSolution);
temperature = initialTemperature;
iterationCount = 0;
// Reset iterations
iterations = [{
iteration: iterationCount,
temperature: temperature,
solution: currentSolution,
energy: currentEnergy,
accepted: true,
type: 'initial'
}];
// Check if initial solution is the best so far
bestSolution = currentSolution;
bestEnergy = currentEnergy;
bestIteration = iterationCount;
updateBestSolutionDisplay();
// Log initial state
addLogEntry(`Initial state: x = ${currentSolution.toFixed(4)}, f(x) = ${currentEnergy.toFixed(4)}, T° = ${temperature.toFixed(2)}`, true);
} else {
// Resume from current state
const currentState = iterations[iterations.length - 1];
currentSolution = currentState.solution;
currentEnergy = currentState.energy;
temperature = currentState.temperature;
iterationCount = iterations.length - 1;
// Log resume state
addLogEntry(`Resuming simulation from iteration ${iterationCount} with T = ${temperature.toFixed(2)}`, true);
}
// Draw initial state
drawFunctionCanvas();
drawTemperatureCanvas();
// Start simulation interval
simulationInterval = setInterval(() => {
if (temperature <= stopTemperature || iterationCount > 1000) {
// Stop simulation
clearInterval(simulationInterval);
running = false;
startBtn.textContent = 'Start Simulation';
startBtn.disabled = false;
pauseBtn.disabled = true;
// Show results
resultsContainer.style.display = 'block';
optimalSolutionEl.textContent = iterations[iterations.length - 1].solution.toFixed(4);
finalEnergyEl.textContent = iterations[iterations.length - 1].energy.toFixed(4);
iterationCountEl.textContent = iterations.length - 1;
// Log final state
addLogEntry(`Simulation complete: Final solution = ${iterations[iterations.length - 1].solution.toFixed(4)}, Energy = ${iterations[iterations.length - 1].energy.toFixed(4)}`, true);
addLogEntry(`BEST SOLUTION FOUND: x = ${bestSolution.toFixed(4)}, f(x) = ${bestEnergy.toFixed(4)} at iteration ${bestIteration}`, true, true);
return;
}
// Generate new solution
const newSolution = getRandomNeighbor(currentSolution, temperature);
const newEnergy = objectiveFunction(newSolution);
const acceptance = acceptanceProbability(currentEnergy, newEnergy, temperature);
const accepted = acceptance > Math.random();
iterationCount++;
// Add to iterations
iterations.push({
iteration: iterationCount,
temperature: temperature,
solution: newSolution,
energy: newEnergy,
acceptance: acceptance,
accepted: accepted,
type: 'proposed'
});
// Log iteration
let isBest = false;
if (newEnergy < bestEnergy) {
bestSolution = newSolution;
bestEnergy = newEnergy;
bestIteration = iterationCount;
updateBestSolutionDisplay();
isBest = true;
}
if (accepted) {
if (isBest) {
addLogEntry(`Iteration ${iterationCount}: x = ${newSolution.toFixed(4)}, f(x) = ${newEnergy.toFixed(4)}, T° = ${temperature.toFixed(2)}, ACCEPTED (NEW BEST)`, true, true);
} else {
addLogEntry(`Iteration ${iterationCount}: x = ${newSolution.toFixed(4)}, f(x) = ${newEnergy.toFixed(4)}, T° = ${temperature.toFixed(2)}, ACCEPTED`, true);
}
// Update current solution if accepted
currentSolution = newSolution;
currentEnergy = newEnergy;
} else {
addLogEntry(`Iteration ${iterationCount}: x = ${newSolution.toFixed(4)}, f(x) = ${newEnergy.toFixed(4)}, T° = ${temperature.toFixed(2)}, REJECTED`, false);
}
// Cool down
temperature *= coolingRate;
// Update visualization
drawFunctionCanvas();
drawTemperatureCanvas();
}, 50); // Update every 50ms for animation
}
// Add event listeners for buttons
startBtn.addEventListener('click', runSimulation);
pauseBtn.addEventListener('click', pauseSimulation);
resetBtn.addEventListener('click', resetSimulation);
// Initialize the simulation on load
initializeSimulation();
</script>