Learning Objectives

  • To be able to describe the physical annealing analogy and explain how the Metropolis criterion allows SA to escape local optima.
  • To be able to trace through the SA algorithm pseudocode and identify the role of every step, including the acceptance rule.
  • To be able to state the statistical equilibrium result and the asymptotic convergence theorem, and explain their practical implications for finite-time implementations.
  • To be able to compare the geometric and polynomial-time cooling schedules and identify the key parameters that govern each.
  • To be able to explain the two temperature initialization strategies in KSL (Fixed and AutoCalibrate) and describe how auto-calibration works.
  • To be able to map the KSL classes SimulatedAnnealing, CoolingScheduleIfc, and TemperatureConfiguration to their corresponding algorithmic steps.

Part 1: Motivation and Problem Setting

Where SA Fits in the SO Landscape

General SO problem:

\[\min_{x \in X} \; f(x)\]

where \(f\) is a black-box objective observable only through a stochastic simulation oracle.

Local Search Global Search
Single-solution Hill Climbing, Stochastic Hill Climber Simulated Annealing
Population-based R-SPLINE, COMPASS CE, Genetic Algorithms

SA targets global search using a single solution at a time.

Key properties that make SA attractive for simulation optimization:

  • Single-solution method — only one oracle evaluation per iteration; no population to maintain.
  • Low memory footprint — critical when the simulation environment is large and cannot be duplicated.
  • Noise-tolerant — the probabilistic acceptance step naturally absorbs stochastic noise in the objective.
  • Simple to implement — the core algorithm requires only a neighbor generator and an acceptance rule.

Why Local Search Is Insufficient

Local search pseudocode (minimization):

1. Draw an initial solution i
2. Generate a solution j from the neighborhood N(i)
3. If f(j) < f(i) then j becomes the current solution
4. If f(j) ≥ f(i) for all j ∈ N(i) then STOP
5. Go to step 2

The fundamental problem: once the current solution falls into a locally convex subdomain, the algorithm is trapped.

  • A local optimum \(i^*\) satisfies \(f(i^*) \leq f(j)\) for all \(j \in N(i^*)\).
  • Unless the neighborhood is exact (implying full enumeration), local search cannot guarantee global optimality.
  • In simulation optimization, local optima abound — the noisy objective surface creates many spurious basins.

SA’s solution: allow transitions that worsen the objective with a controlled probability, enabling escape from local optima.

The Physical Analogy

SA is inspired by the physical annealing of materials (Kirkpatrick, Gelatt, and Vecchi, 1983):

Physical Process SA Optimization
State of a solid Candidate solution \(x\)
Energy of state \(i\) Objective function \(f(i)\)
Temperature \(T\) Control parameter \(c\)
Particle transition Neighbor generation + acceptance
Minimum energy state Global optimum

Two physical scenarios:

  • Rapid cooling (hardening): material freezes into a disordered, high-energy metastable state — analogous to greedy local search getting trapped.
  • Slow cooling (annealing): atoms organize into a symmetric crystalline structure of minimum energy — analogous to SA finding the global optimum.

The key insight: sufficiently high temperature and sufficiently slow cooling guarantee convergence to minimum energy.

Part 2: Local Search and the Metropolis Criterion

The Metropolis Algorithm

In 1953, Metropolis, Rosenbluth, and Teller developed an algorithm to simulate physical annealing using Monte Carlo techniques.

Starting from state \(i\) with energy \(E_i\):

  1. Generate a new state \(j\) by perturbing one particle.
  2. If \(E_j < E_i\) (lower energy): accept \(j\) unconditionally.
  3. If \(E_j \geq E_i\) (higher energy): accept \(j\) with probability

\[\Pr\{\text{current state} = j\} = e^{(E_i - E_j)/(k_B T)}\]

where \(T\) is temperature and \(k_B\) is Boltzmann’s constant.

The Metropolis criterion for optimization (replacing \(k_B = 1\) and energy with objective):

\[\Pr\{\text{accept } j\} = \begin{cases} 1 & \text{if } f(j) < f(i) \\ e^{(f(i)-f(j))/c} & \text{otherwise} \end{cases}\]

where \(c > 0\) is the control parameter (temperature) expressed in the same units as \(f\).

Understanding the Acceptance Probability

\[\Pr\{\text{accept } j\} = e^{(f(i)-f(j))/c} = e^{-\Delta f / c}, \quad \Delta f = f(j) - f(i) > 0\]

Effect of temperature \(c\) on acceptance:

Temperature \(c\) \(\Delta f = 1\) \(\Delta f = 5\) \(\Delta f = 10\) Behavior
High (\(c = 100\)) 0.990 0.951 0.905 Nearly all moves accepted
Medium (\(c = 5\)) 0.819 0.368 0.135 Selective acceptance
Low (\(c = 0.5\)) 0.135 \(\approx 0\) \(\approx 0\) Only improvements accepted
Near zero (\(c \to 0\)) 0 0 0 Pure greedy search

Interpretation:

  • Large cost increases are accepted rarely, even at high temperature.
  • Small cost increases are accepted often at high temperature, less often as cooling proceeds.
  • As \(c \to 0\), SA degenerates to a greedy local search algorithm.

Part 3: The Simulated Annealing Algorithm

SA Pseudocode

SA Algorithm (minimization):

1. Initialize:  i := i_start,  k := 0,  c_k := c_0,  L_k := L_0
2. Repeat:
   For l = 1 to L_k do:                    ← inner loop at temperature c_k
     a. Generate j from N(i)               ← neighbor generation
     b. Δf := f(j) - f(i)                  ← cost difference
     c. If Δf < 0:  i := j                 ← always accept improvement
        Else: accept j with probability exp(-Δf / c_k)
   k := k + 1                              ← advance to next temperature level
   Compute (L_k, c_k)                      ← update temperature and chain length
3. Until c_k ≈ 0

Two nested loops:

  • Outer loop: decrements the temperature \(c_k\) according to the cooling schedule.
  • Inner loop (Markov chain): generates \(L_k\) transitions at fixed temperature \(c_k\).

Note on KSL implementation: the inner loop is absorbed into the outer loop — each call to mainIteration() generates and evaluates one neighbor, then advances the temperature. The Markov chain length concept is managed implicitly through the iteration budget.

The Acceptance Function in KSL

fun acceptanceProbability(costDifference: Double, temperature: Double): Double {
    require(temperature > 0.0) { "The temperature must be positive" }
    // Improvements (Δf ≤ 0) are always accepted
    return if (costDifference <= 0.0) {
        1.0
    } else {
        // Worsening moves: Metropolis criterion  exp(-Δf / c)
        exp(-costDifference / temperature)
    }
}

Algorithm mapping:

Code Mathematical expression Algorithm step
costDifference <= 0.0 \(\Delta f \leq 0\) Unconditional acceptance
exp(-costDifference / temperature) \(e^{-\Delta f / c}\) Metropolis acceptance probability

Diagnostic properties exposed by SimulatedAnnealing:

  • lastAcceptanceProbability — the probability computed at the last iteration.
  • costDifference — the \(\Delta f\) computed at the last iteration.
  • currentTemperature — the current value of \(c_k\).

All three are private set — readable for diagnostics, not writable externally.

mainIteration() — The SA Loop Body

override fun mainIteration() {
    // ── Neighbor generation ──────────────────────────────────────────────
    // Calls generateNeighbor(currentPoint, rnStream) — user-defined mechanism
    val nextPoint = nextPoint()
    // ── Oracle evaluation ────────────────────────────────────────────────
    // Runs replicationsPerEvaluation simulation replications
    val nextSolution = requestEvaluation(nextPoint)
    // ── Cost difference ──────────────────────────────────────────────────
    // Δf = f(j) - f(i);  positive = worsening move
    costDifference = nextSolution.penalizedObjFncValue - currentSolution.penalizedObjFncValue
    if (costDifference < 0.0) {
        // ── Unconditional acceptance of improvement ───────────────────────
        currentSolution = nextSolution
        lastAcceptanceProbability = 1.0
    } else {
        // ── Metropolis acceptance of worsening move ───────────────────────
        val u = rnStream.randU01()                           // U(0,1) variate
        lastAcceptanceProbability = acceptanceProbability(costDifference, currentTemperature)
        if (u < lastAcceptanceProbability) {
            currentSolution = nextSolution                  // accept
        }
        // else: stay at currentSolution (reject)
    }
    // ── Advance temperature ───────────────────────────────────────────────
    currentTemperature = coolingSchedule.nextTemperature(iterationCounter)
    solutionChecker.captureSolution(currentSolution)
}

Part 4: Theoretical Foundations

Statistical Equilibrium

Theorem (Boltzmann stationary distribution): after a sufficient number of transitions at fixed \(c\), the SA algorithm visits solution \(i\) with probability:

\[q_i(c) = \frac{1}{N_0(c)} e^{-f(i)/c}, \qquad N_0(c) = \sum_{j \in S} e^{-f(j)/c}\]

Key limiting behaviors:

Limit \(\langle f \rangle_c\) \(\sigma^2_c\) Entropy \(H_c\)
\(c \to \infty\) \(\frac{1}{|S|}\sum_{i} f(i)\) (uniform average) \(\sigma^2_\infty\) (maximum) \(\ln|S|\) (maximum disorder)
\(c \to 0\) \(f_\text{opt}\) (global minimum) \(0\) \(\ln|S_\text{opt}|\) (minimum disorder)

Practical meaning:

  • At high \(c\): SA is a random walk — all solutions visited uniformly.
  • At low \(c\): SA concentrates probability mass on global optima.
  • The entropy \(H_c\) measures the degree of optimality achieved — decreasing \(H_c\) tracks convergence.

Asymptotic Convergence

Theorem (Theorem 2, Delahaye et al.): Let the transition probabilities satisfy the irreducibility condition — every state \(j\) is reachable from every state \(i\) via a finite sequence of generating steps. Then the Markov chain has a stationary distribution \(q(c)\) and:

\[\lim_{c \to 0} \lim_{k \to \infty} \Pr\{X^c_k \in S_\text{opt}\} = 1\]

In words: SA converges to a global optimum with probability 1, provided:

  1. The generating mechanism is irreducible (can reach any solution from any other).
  2. The cooling is infinitely slow (\(c \to 0\) continuously, with infinite transitions at each level).

Practical implications:

  • Convergence requires an infinitely long cooling schedule — not achievable in practice.
  • In finite time, SA approximates the global optimum, not guarantees it.
  • The algorithm moves the search into the right basin of attraction; a local refinement step can then find the local minimum within that basin.
  • The transition threshold \(c_t \approx \frac{2\sigma^2_\infty}{\langle f \rangle_\infty - f_\text{min}}\) divides the search into an exploration phase (\(c > c_t\)) and an exploitation phase (\(c < c_t\)).

Part 5: Cooling Schedules

What a Cooling Schedule Defines

A cooling process (Definition 12, Delahaye et al.) specifies:

  1. Initial value \(c_0\) — starting temperature; determines initial acceptance rate.
  2. Decay function — how \(c_k\) decreases from iteration to iteration.
  3. Final value \(c_f\) — stopping temperature; the lower bound on \(c\).
  4. Markov chain length \(L_k\) — number of transitions generated at each temperature level.

In KSL: CoolingScheduleIfc encapsulates items 1–3. Item 4 is absorbed into the maxIterations budget.

interface CoolingScheduleIfc {
    var initialTemperature: Double
    fun nextTemperature(iteration: Int): Double
}

The single method nextTemperature(iteration) is called once per call to mainIteration() — after the accept/reject step — providing the temperature for the next iteration.

Geometric (Exponential) Cooling — ExponentialCoolingSchedule

Formula:

\[c_{k+1} = \alpha \, c_k \implies c_k = c_0 \cdot \alpha^k, \qquad 0 < \alpha < 1\]

Typical range: \(0.80 < \alpha < 0.99\).

class ExponentialCoolingSchedule(
    initialTemperature: Double,
    val coolingRate: Double = defaultCoolingRate   // α, default 0.95
) : CoolingSchedule(initialTemperature) {

    override fun nextTemperature(iteration: Int): Double {
        return initialTemperature * coolingRate.pow(iteration)
    }
}

Auto-calculated cooling rate — given target \(c_0\), \(c_f\), and maxIterations \(K\):

\[\alpha = \left(\frac{c_f}{c_0}\right)^{1/K}\]

// Factory: calculates α to hit c_f exactly at iteration K
ExponentialCoolingSchedule.createTargetedSchedule(
    initialTemperature = 500.0,
    stoppingTemperature = 0.001,
    maxIterations      = 2000
)

Advantages: simple, robust, widely used, no problem-specific tuning of the decay form. Disadvantage: does not adapt to local objective landscape — may spend too many iterations in unproductive regions.

Linear Cooling — LinearCoolingSchedule

Formula:

\[c_k = c_0 - k \cdot \frac{c_0 - c_f}{K}, \qquad k = 0, 1, \ldots, K\]

class LinearCoolingSchedule(
    initialTemperature: Double = defaultInitialTemperature,
    val stoppingTemperature: Double = defaultStoppingTemperature,
    val maxIterations: Int = defaultMaxNumberIterations
) : CoolingSchedule(initialTemperature) {

    val temperatureDecreasePerIteration
        get() = (initialTemperature - stoppingTemperature) / maxIterations

    override fun nextTemperature(iteration: Int): Double {
        if (iteration >= maxIterations) return stoppingTemperature
        return initialTemperature - (temperatureDecreasePerIteration * iteration)
    }
}

Characteristics: temperature decreases at a constant rate per iteration. Spends equal time at every temperature level — suitable when the objective landscape is relatively uniform in scale.

Logarithmic Cooling — LogarithmicCoolingSchedule

Formula:

\[c_k = \frac{c_0}{\ln(k - 1 + e)}, \qquad k \geq 1\]

class LogarithmicCoolingSchedule(
    initialTemperature: Double
) : CoolingSchedule(initialTemperature) {

    override fun nextTemperature(iteration: Int): Double {
        // ln(e) = 1 at iteration 1, ensuring T_1 = T_0
        return initialTemperature / ln(iteration.toDouble() - 1.0 + E)
    }
}

Theoretical significance: a logarithmic cooling schedule of the form \(c_k = C / \ln(1 + k)\) is the slowest provably convergent schedule — it guarantees asymptotic convergence to a global optimum under mild conditions.

Practical limitation: convergence is extremely slow; logarithmic schedules require very large iteration budgets and are rarely used directly in practice. They establish the theoretical lower bound on cooling speed.

Cooling Schedule Comparison

Schedule Formula \(\alpha\) / decay Practical use
Geometric \(c_0 \alpha^k\) \(0.80 < \alpha < 0.99\) Most common; robust default
Linear \(c_0 - k \cdot \Delta c\) \(\Delta c = (c_0 - c_f)/K\) Equal time at all levels
Logarithmic \(c_0 / \ln(k - 1 + e)\) Slowest convergent Theoretical benchmark

Effect of cooling rate \(\alpha\) (geometric schedule):

\(\alpha\) Iterations to halve \(c\) Character
0.99 69 Slow, thorough — more iterations at each temperature
0.95 (default) 14 Balanced — recommended starting point
0.90 7 Moderate — faster convergence, higher risk
0.80 3 Fast — aggressive cooling, may miss optimum

Part 6: Initial Temperature and Auto-Calibration

Why \(c_0\) Matters

Too low \(c_0\): the algorithm behaves like greedy local search from the start — essentially no exploration, high risk of trapping in the first local minimum encountered.

Too high \(c_0\): wastes many iterations on a random walk, accepting essentially everything; exploration is redundant until \(c\) drops to a useful range.

Goal: set \(c_0\) so that the initial acceptance rate \(\chi(c_0)\) equals a target value (typically 0.8), ensuring nearly all transitions are accepted at the start.

Polynomial-time initialization formula (Eq. 2, Delahaye et al.):

Let \(m_1\) = number of improving transitions and \(m_2\) = number of worsening transitions observed in a random walk sample. Let \(\bar{\Delta f}^{(+)}\) be the average cost increase over worsening moves. Then:

\[c_0 = \frac{\bar{\Delta f}^{(+)}}{\ln\!\left(\dfrac{m_2}{m_2 \, \chi(c_0) - m_1\,(1-\chi(c_0))}\right)}\]

Simplified form used in KSL (when \(m_1 \ll m_2\), which is typical for a random walk): \(c_0 = \frac{-\bar{\Delta f}^{(+)}}{\ln(\chi_0)}\) where \(\chi_0\) is the target initial acceptance probability.

TemperatureConfiguration — The Sealed Class

KSL uses a sealed class to encapsulate the two initialization strategies cleanly:

sealed class TemperatureConfiguration {
    // Strategy 1: user supplies c_0 directly
    data class Fixed(val temperature: Double) : TemperatureConfiguration() {
        init { require(temperature > 0.0) { "Temperature must be positive" } }
    }
    // Strategy 2: solver estimates c_0 from a random walk
    data class AutoCalibrate(
        val targetProbability: Double = 0.8,       // χ_0 — desired initial acceptance rate
        val numRandomWalkSteps: Int = 100,          // sample size for Δf estimation
        val numRepsPerStep: Int = 0,                // replications per walk step (0 = model default)
        val useBestStepForStartingPoint: Boolean = true  // warm-start from best walk solution
    ) : TemperatureConfiguration() { ... }
}

TemperatureConfiguration — Applying the Configuration

Passed to SimulatedAnnealing at construction:

// Fixed temperature
val sa = SimulatedAnnealing(problemDefinition = myProblem, evaluator = myEvaluator,
    temperatureConfiguration = TemperatureConfiguration.Fixed(500.0),
    ...
)
// Auto-calibrated temperature
val sa = SimulatedAnnealing(problemDefinition = myProblem, evaluator = myEvaluator,
    temperatureConfiguration = TemperatureConfiguration.AutoCalibrate(
        targetProbability = 0.8, numRandomWalkSteps = 100),
    ...
)

calibrateTemperature() — Annotated Code

private fun calibrateTemperature(config: TemperatureConfiguration.AutoCalibrate): Double {
    var totalWorseningCost = 0.0
    var worseningMovesCount = 0
    var previousWalkSolution = currentSolution   // baseline from initializeIterations()
    var bestWalkSolution     = currentSolution
    for (i in 0 until config.numRandomWalkSteps) {    // random walk of numRandomWalkSteps steps
        val nextPoint = generateNeighbor(previousWalkSolution.inputMap, rnStream)
        // Evaluate the neighbor — uses numRepsPerStep if > 0, else model default
        val nextSolution = if (config.numRepsPerStep > 0)
            requestEvaluation(nextPoint, config.numRepsPerStep)
        else
            requestEvaluation(nextPoint)
        // Track best solution seen during walk (warm-start option)
        if (nextSolution.penalizedObjFncValue < bestWalkSolution.penalizedObjFncValue)
            bestWalkSolution = nextSolution
        val costDiff = nextSolution.penalizedObjFncValue - previousWalkSolution.penalizedObjFncValue
        if (costDiff > 0.0) {             // worsening move: accumulate Δf⁺
            totalWorseningCost  += costDiff
            worseningMovesCount += 1
        }
        previousWalkSolution = nextSolution
    }
    if (worseningMovesCount == 0) return defaultInitialTemperature   // fallback
    // Apply starting-point strategy
    if (config.useBestStepForStartingPoint) currentSolution = bestWalkSolution
    val avgWorseningCost = totalWorseningCost / worseningMovesCount
    return -avgWorseningCost / ln(config.targetProbability)   // c_0 = -Δf̄⁺ / ln(χ_0)
}

Algorithm mapping:

Code section Mathematical step
Random walk loop Generate \(m_0\) transitions to sample objective landscape
costDiff > 0.0 accumulation Estimate \(\bar{\Delta f}^{(+)}\) and \(m_2\)
-avgWorseningCost / ln(targetProbability) Apply \(c_0 = -\bar{\Delta f}^{(+)} / \ln(\chi_0)\)
useBestStepForStartingPoint Warm-start: initialize solver from best walk solution

Part 7: Key Algorithm Parameters and Their Roles

Parameter Summary Table

Parameter KSL Property Default Role
Temperature config temperatureConfiguration Fixed(1000.0) Strategy for setting \(c_0\)
Initial temperature initialTemperature 1000.0 Starting exploration breadth
Cooling rate coolingRate (in schedule) 0.95 Rate of temperature decay per iteration
Stopping temperature stoppingTemperature 0.001 Termination: \(c < c_f\)
Max iterations maxIterations 1000 Hard upper iteration budget
Replications per eval. replicationsPerEvaluation 30 Oracle noise level
No-improve threshold defaultNoImproveThresholdForSA 5 Stagnation stopping depth
Auto-cal. target prob. targetProbability 0.8 Desired \(\chi(c_0)\)
Auto-cal. walk steps numRandomWalkSteps 100 Sample size for \(c_0\) estimate
Use best walk point useBestStepForStartingPoint true Warm-start from calibration walk

Initial Temperature and Cooling Rate

Initial temperature \(c_0\):

  • Too low: premature convergence — behaves like greedy local search from iteration 1.
  • Too high: wastes iterations on near-random walk before useful search begins.
  • Rule of thumb: use AutoCalibrate with targetProbability = 0.8 unless a domain-expert estimate is available.

Cooling rate \(\alpha\) (geometric schedule):

  • \(\alpha\) controls how many total iterations are “spent” at useful temperatures.
  • Interaction with maxIterations: the actual number of useful iterations is bounded by \(K = \log(c_f / c_0) / \log(\alpha)\).
  • Use ExponentialCoolingSchedule.createTargetedSchedule() to compute \(\alpha\) automatically from \(c_0\), \(c_f\), and \(K\):
val schedule = ExponentialCoolingSchedule.createTargetedSchedule(
    initialTemperature  = 500.0,
    stoppingTemperature = 0.001,
    maxIterations       = 5000
) // α = (0.001 / 500.0)^(1/5000) ≈ 0.9985

Stopping Temperature, Iterations, and Replication Noise

Stopping temperature \(c_f\):

  • Sets the exploitation floor — below \(c_f\), virtually no worsening moves are accepted.
  • Default 0.001 is suitable when \(f\) is scaled to order of magnitude \(\sim 1\)\(100\).
  • If \(f\) values are very large (e.g., \(\sim 10^6\)), \(c_f\) must be scaled proportionally — otherwise the algorithm effectively never stops cooling.

Max iterations maxIterations:

  • Acts as a hard safety budget, independent of temperature.
  • Should be set large enough that the temperature criterion (\(c < c_f\)) triggers first under normal operation.

Replications per evaluation replicationsPerEvaluation:

  • Higher replications \(\Rightarrow\) lower noise in \(\hat{f}(x)\) \(\Rightarrow\) more reliable acceptance decisions.
  • More costly: each iteration runs multiple simulation replications.
  • Low replications are acceptable at high \(c\) (noise is dominated by randomness anyway); consider increasing them as \(c\) decreases if the budget allows.

No-Improve Threshold and Stagnation Stopping

defaultNoImproveThresholdForSA (default: 5):

The solutionChecker tracks the best solution over the last \(d\) iterations. If the best solution does not improve for \(d\) consecutive iterations, the algorithm halts.

val solutionChecker = SolutionChecker(
    solutionEqualityChecker,
    defaultNoImproveThresholdForSA   // d = 5 by default
)

No-Improve Threshold and Stagnation Stopping

isStoppingCriteriaSatisfied():

override fun isStoppingCriteriaSatisfied(): Boolean {
    return solutionQualityEvaluator?.isStoppingCriteriaReached(this)
        ?: checkTemperature() || solutionChecker.checkSolutions()
}

// Criterion 1: temperature has dropped below stopping threshold
private fun checkTemperature() = currentTemperature < stoppingTemperature

// Criterion 2: no improvement in best solution for d iterations
// solutionChecker.checkSolutions() returns true when stagnation detected

Two independent stopping criteria — either triggers termination:

Criterion Trigger Meaning
Temperature \(c_k < c_f\) Cooling schedule has run its course
Stagnation No improvement for \(d\) iterations Search has locally converged

Part 8: KSL Architecture Overview

Class Hierarchy

StochasticSolver  (abstract)
│   ├── rnStream, streamProvider       [random stream management]
│   ├── startingPoint()                [feasible starting point generation]
│   ├── generateNeighbor()             [neighborhood mechanism — problem-specific]
│   └── mainIteration()  ← abstract
│
└── SimulatedAnnealing
        ├── temperatureConfiguration: TemperatureConfiguration  [sealed class]
        │       ├── Fixed(temperature)
        │       └── AutoCalibrate(targetProbability, numRandomWalkSteps, ...)
        │
        ├── coolingSchedule: CoolingScheduleIfc
        │       ├── ExponentialCoolingSchedule(initialTemp, coolingRate)
        │       ├── LinearCoolingSchedule(initialTemp, stoppingTemp, maxIter)
        │       └── LogarithmicCoolingSchedule(initialTemp)
        │
        ├── initialTemperature, stoppingTemperature, currentTemperature
        ├── lastAcceptanceProbability, costDifference
        ├── solutionChecker                [stagnation stopping]
        ├── initializeIterations()
        ├── mainIteration()
        └── isStoppingCriteriaSatisfied()

Design Philosophy

Separation of concerns:

Class / Component Owns Algorithm Role
SimulatedAnnealing The SA iteration loop Accept/reject, temperature tracking, stopping
TemperatureConfiguration The \(c_0\) strategy Fixed input vs. auto-calibrated random walk
CoolingScheduleIfc The temperature decay \(c_{k+1} = g(c_k, k)\) — replaceable by any implementation
StochasticSolver Random streams, oracle calls, neighbor generation Infrastructure

Key design benefit: SimulatedAnnealing is schedule-agnostic. Any class implementing CoolingScheduleIfc can be plugged in without modifying the solver. Similarly, temperature initialization is fully encapsulated by the sealed TemperatureConfiguration — the solver does not need to know which strategy is active until initializeIterations() is called.

initializeIterations() — Resolving the Temperature

override fun initializeIterations() {
    solutionChecker.clear()
    // Evaluate starting point; sets currentSolution and myInitialSolution
    super.initializeIterations()
    // Resolve initial temperature based on the configuration strategy
    initialTemperature = when (val config = temperatureConfiguration) {
        is TemperatureConfiguration.Fixed ->
            config.temperature                    // user-supplied c_0
        is TemperatureConfiguration.AutoCalibrate -> {
            logger.info { "Auto-calibrating initial temperature..." }
            calibrateTemperature(config)          // random walk estimation
        }
    }
    currentTemperature = initialTemperature
    // Guard: initial temperature must exceed stopping temperature
    require(currentTemperature > stoppingTemperature) {
        "Initial temperature ($currentTemperature) must exceed stopping temperature ($stoppingTemperature)"
    }
    lastAcceptanceProbability = 1.0
    costDifference = Double.NaN
}

Algorithm mapping: sets \(c_0\), validates the temperature range, and initializes diagnostic state variables.

Part 9: Parameter Configuration and Practical Usage

Basic Constructor Calls

Fixed temperature mode:

val sa = SimulatedAnnealing(
    problemDefinition        = myProblem,
    evaluator                = myEvaluator,
    temperatureConfiguration = TemperatureConfiguration.Fixed(500.0),
    coolingSchedule          = ExponentialCoolingSchedule(500.0, coolingRate = 0.95),
    stoppingTemperature      = 0.001,
    maxIterations            = 5000,
    replicationsPerEvaluation = 30
)
sa.runAllIterations()
println(sa.bestSolution)

Basic Constructor Calls

Auto-calibrated temperature with targeted cooling:

val schedule = ExponentialCoolingSchedule.createTargetedSchedule(
    initialTemperature  = 500.0,     // used as target; auto-cal may override
    stoppingTemperature = 0.001,
    maxIterations       = 5000       // α computed to hit c_f at iteration 5000
)

val sa = SimulatedAnnealing(
    problemDefinition        = myProblem,
    evaluator                = myEvaluator,
    temperatureConfiguration = TemperatureConfiguration.AutoCalibrate(
        targetProbability          = 0.8,
        numRandomWalkSteps         = 150,
        useBestStepForStartingPoint = true
    ),
    coolingSchedule          = schedule,
    stoppingTemperature      = 0.001,
    maxIterations            = 5000,
    replicationsPerEvaluation = 30
)
sa.runAllIterations()

Global Defaults via Companion Object

SimulatedAnnealing’s companion object exposes defaults for all key parameters:

// Change defaults before creating any SA solver instances
SimulatedAnnealing.defaultInitialTemperature     = 2000.0
SimulatedAnnealing.defaultCoolingRate            = 0.98
SimulatedAnnealing.defaultStoppingTemperature    = 0.0001
SimulatedAnnealing.defaultNoImproveThresholdForSA = 10

These apply to all subsequently created instances that do not override the relevant constructor argument. Useful when running a batch of experiments with a consistent parameter profile.

Diagnostic Guidance

Symptom Likely Cause Remedy
Stops at first iteration (temperature error) \(c_0 \leq c_f\) Increase initialTemperature or decrease stoppingTemperature
Solution never improves beyond starting point \(c_0\) too low; no exploration Use AutoCalibrate or manually increase initialTemperature
Converges quickly to a poor local optimum \(\alpha\) too small (cooling too fast) Increase coolingRate toward 0.99; increase maxIterations
Very slow convergence, many iterations \(\alpha\) too close to 1.0 Decrease coolingRate slightly; use createTargetedSchedule()
Stops on stagnation, not temperature Useful search ends before \(c_f\) is reached Decrease defaultNoImproveThresholdForSA; reduce \(\alpha\)
High oracle call count, slow per-iteration Too many replications Reduce replicationsPerEvaluation at high temperatures
Auto-calibration fallback warning (no worsening moves) Starting point already optimal or landscape is flat Increase numRandomWalkSteps; check problem definition
\(c_f\) scaling issue \(f\) values are very large; \(c_f = 0.001\) is never reached Scale \(c_f\) to match the order of magnitude of \(f\)

toString() as a Diagnostic Tool

println(sa.toString())
// SimulatedAnnealing(
//     temperatureConfiguration = AutoCalibrate(targetProb=0.8, samples=150),
//     coolingSchedule = ExponentialCoolingSchedule,
//     initialTemperature = 312.47,           ← estimated by calibration
//     stoppingTemperature = 0.001,
//     solutionEqualityChecker = InputsAndConfidenceIntervalEquality,
//     base = StochasticSolver(
//         streamNumber = 1,
//         startingPointGenerator = Default,
//         ...
//     )
// )

Mid-run diagnostics via exposed properties:

// Check after each iteration via an observer / emitter
println("Temperature:          ${sa.currentTemperature}")
println("Last acceptance prob: ${sa.lastAcceptanceProbability}")
println("Cost difference:      ${sa.costDifference}")
println("Best solution:        ${sa.bestSolution}")
println("Oracle calls:         ${sa.numOracleCalls}")

Use iterationEmitter to attach a listener that logs these at every iteration — helpful for plotting cooling curves and acceptance rate trajectories.

Part 10: Summary

SA Algorithm \(\leftrightarrow\) KSL Implementation

Algorithm Step Mathematical Object KSL Location
Initialization \(c_0\), starting point \(i_0\) initializeIterations()TemperatureConfiguration
Auto-calibrate \(c_0\) \(c_0 = -\bar{\Delta f}^{(+)} / \ln(\chi_0)\) calibrateTemperature() in SimulatedAnnealing
Neighbor generation \(j \in N(i)\) nextPoint()generateNeighbor() in StochasticSolver
Oracle evaluation \(f(j)\) (possibly noisy) requestEvaluation(nextPoint)
Cost difference \(\Delta f = f(j) - f(i)\) costDifference in mainIteration()
Acceptance decision \(\Pr\{\text{accept}\} = e^{-\Delta f / c}\) acceptanceProbability() + rnStream.randU01()
Temperature update \(c_{k+1} = g(c_k, k)\) coolingSchedule.nextTemperature(iterationCounter)
Stop: temperature \(c_k < c_f\) checkTemperature() in isStoppingCriteriaSatisfied()
Stop: stagnation No improvement for \(d\) iterations solutionChecker.checkSolutions()

Key Takeaways

  1. SA escapes local optima through probabilistic acceptance. The Metropolis criterion allows worsening moves with probability \(e^{-\Delta f / c}\), which decreases as \(c\) cools — balancing exploration and exploitation.

  2. Asymptotic convergence is guaranteed but impractical. The theoretical convergence proof requires an infinitely slow schedule. In practice, SA finds good approximate solutions within a finite budget.

  3. The cooling schedule is the primary design choice. The geometric schedule with \(\alpha \in (0.80, 0.99)\) is the robust default; createTargetedSchedule() automates \(\alpha\) selection given \(c_0\), \(c_f\), and \(K\).

  4. Initial temperature strongly influences solution quality. Auto-calibration via a random walk is the recommended approach when no domain knowledge is available — it sets \(c_0\) to achieve a target acceptance rate of \(\approx 0.8\).

  5. SA is ideally suited for memory-constrained simulation optimization. A single solution is maintained at all times — no population to evaluate, no simulation environment duplication required.

  6. KSL separates temperature strategy from the SA loop. Swapping TemperatureConfiguration or CoolingScheduleIfc implementations changes behavior without modifying SimulatedAnnealing itself.

When to Choose SA

Scenario Recommendation
Single-solution global search needed SA is a natural choice
Large simulation model; memory is constrained SA’s single-solution approach is ideal
Multi-modal objective with many local optima SA handles this well if cooling is sufficiently slow
Continuous or discrete decision variables SA works in both settings
Global optimum guarantee required SA provides no guarantee; use Ranking & Selection
Population-based diversity needed Prefer CE or Genetic Algorithms
Integer-ordered local optimum with convergence guarantee Prefer R-SPLINE

References

  • Delahaye, D., Chaimatanan, S., and Mongeau, M. (2019). Simulated Annealing: From Basics to Applications. In Gendreau and Potvin (eds.), Handbook of Metaheuristics, Vol. 272. Springer. HAL Id: hal-01887543.

  • Kirkpatrick, S., Gelatt, C. D., and Vecchi, M. P. (1983). Optimization by simulated annealing. Science, 220(4598), 671–680.

  • Metropolis, N., Rosenbluth, A., Rosenbluth, M., Teller, A., and Teller, E. (1953). Equation of state calculations by fast computing machines. Journal of Chemical Physics, 21(6), 1087–1092.

  • Aarts, E. and Van Laarhoven, P. (1985). Statistical cooling: A general approach to combinatorial problems. Philips Journal of Research, 40, 193–226.

  • Rossetti, M. D. KSL — Kotlin Simulation Library. ksl.simopt.solvers.algorithms.

⌂ Index