Skip to content

Commit 2da7ea8

Browse files
committed
enhancement #13508
1 parent 709c18e commit 2da7ea8

File tree

5 files changed

+343
-0
lines changed

5 files changed

+343
-0
lines changed

simulated_annealing/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
Simulated Annealing
2+
===================
3+
4+
This package provides a simple Simulated Annealing optimizer and a Tkinter GUI to explore parameters and visualize optimization progress.
5+
6+
Files
7+
- `simulated_annealing.py` - core optimizer (class `SimulatedAnnealing`)
8+
- `example.py` - example functions and CLI demo
9+
- `gui.py` - Tkinter GUI with embedded matplotlib plot
10+
11+
Quick start
12+
-----------
13+
14+
Run the GUI:
15+
16+
```bash
17+
python -m simulated_annealing.gui
18+
```
19+
20+
Run the CLI example:
21+
22+
```bash
23+
python -m simulated_annealing.example
24+
```

simulated_annealing/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""Simulated Annealing package
2+
3+
Exports:
4+
- SimulatedAnnealing: core optimizer class
5+
- example_functions: a small collection of test functions
6+
"""
7+
from .simulated_annealing import SimulatedAnnealing
8+
from .example import example_functions
9+
10+
__all__ = ["SimulatedAnnealing", "example_functions"]

simulated_annealing/example.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from typing import Callable, Dict, Sequence
2+
3+
4+
def sphere(x: Sequence[float]) -> float:
5+
return sum(v * v for v in x)
6+
7+
8+
def rastrigin(x: Sequence[float]) -> float:
9+
# Rastrigin function (common test function)
10+
A = 10
11+
return A * len(x) + sum((v * v - A * __import__("math").cos(2 * __import__("math").pi * v)) for v in x)
12+
13+
14+
example_functions: Dict[str, Callable[[Sequence[float]], float]] = {
15+
"sphere": sphere,
16+
"rastrigin": rastrigin,
17+
}
18+
19+
20+
def cli_example():
21+
# CLI demo minimizing 2D sphere
22+
from .simulated_annealing import SimulatedAnnealing
23+
func = sphere
24+
initial = [5.0, -3.0]
25+
bounds = [(-10, 10), (-10, 10)]
26+
sa = SimulatedAnnealing(func, initial, bounds=bounds, temperature=50, cooling_rate=0.95, iterations_per_temp=200)
27+
best, cost, history = sa.optimize()
28+
print("Best:", best)
29+
print("Cost:", cost)
30+
31+
32+
if __name__ == "__main__":
33+
cli_example()

simulated_annealing/gui.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import threading
2+
import tkinter as tk
3+
from tkinter import ttk, messagebox
4+
from typing import Optional
5+
6+
import matplotlib
7+
matplotlib.use("TkAgg")
8+
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
9+
import matplotlib.pyplot as plt
10+
11+
from .simulated_annealing import SimulatedAnnealing
12+
from .example import example_functions
13+
14+
15+
class SA_GUI(tk.Tk):
16+
def __init__(self):
17+
super().__init__()
18+
self.title("Simulated Annealing Explorer")
19+
self.geometry("800x600")
20+
21+
# Left: controls
22+
ctrl = ttk.Frame(self)
23+
ctrl.pack(side=tk.LEFT, fill=tk.Y, padx=8, pady=8)
24+
25+
ttk.Label(ctrl, text="Function:").pack(anchor=tk.W)
26+
self.func_var = tk.StringVar(value="sphere")
27+
func_menu = ttk.Combobox(ctrl, textvariable=self.func_var, values=list(example_functions.keys()), state="readonly")
28+
func_menu.pack(fill=tk.X)
29+
30+
ttk.Label(ctrl, text="Initial (comma-separated)").pack(anchor=tk.W, pady=(8, 0))
31+
self.init_entry = ttk.Entry(ctrl)
32+
self.init_entry.insert(0, "5, -3")
33+
self.init_entry.pack(fill=tk.X)
34+
35+
ttk.Label(ctrl, text="Bounds (lo:hi comma-separated for each)").pack(anchor=tk.W, pady=(8, 0))
36+
self.bounds_entry = ttk.Entry(ctrl)
37+
self.bounds_entry.insert(0, "-10:10, -10:10")
38+
self.bounds_entry.pack(fill=tk.X)
39+
40+
ttk.Label(ctrl, text="Temperature").pack(anchor=tk.W, pady=(8, 0))
41+
self.temp_entry = ttk.Entry(ctrl)
42+
self.temp_entry.insert(0, "50")
43+
self.temp_entry.pack(fill=tk.X)
44+
45+
ttk.Label(ctrl, text="Cooling rate").pack(anchor=tk.W, pady=(8, 0))
46+
self.cool_entry = ttk.Entry(ctrl)
47+
self.cool_entry.insert(0, "0.95")
48+
self.cool_entry.pack(fill=tk.X)
49+
50+
ttk.Label(ctrl, text="Iterations per temp").pack(anchor=tk.W, pady=(8, 0))
51+
self.iter_entry = ttk.Entry(ctrl)
52+
self.iter_entry.insert(0, "200")
53+
self.iter_entry.pack(fill=tk.X)
54+
55+
self.run_btn = ttk.Button(ctrl, text="Run", command=self._on_run)
56+
self.run_btn.pack(fill=tk.X, pady=(12, 0))
57+
58+
self.stop_flag = threading.Event()
59+
self.stop_btn = ttk.Button(ctrl, text="Stop", command=self._on_stop, state=tk.DISABLED)
60+
self.stop_btn.pack(fill=tk.X, pady=(6, 0))
61+
62+
# Right: plot
63+
fig, self.ax = plt.subplots(figsize=(5, 4))
64+
self.fig = fig
65+
self.canvas = FigureCanvasTkAgg(fig, master=self)
66+
self.canvas.get_tk_widget().pack(side=tk.RIGHT, fill=tk.BOTH, expand=1)
67+
68+
self._plot_line, = self.ax.plot([], [], label="best_cost")
69+
self.ax.set_xlabel("Iterations")
70+
self.ax.set_ylabel("Best cost")
71+
self.ax.grid(True)
72+
73+
def _parse_initial(self) -> list:
74+
raw = self.init_entry.get().strip()
75+
parts = [p.strip() for p in raw.split(",") if p.strip()]
76+
return [float(p) for p in parts]
77+
78+
def _parse_bounds(self, dim: int):
79+
raw = self.bounds_entry.get().strip()
80+
parts = [p.strip() for p in raw.split(",") if p.strip()]
81+
bounds = []
82+
for p in parts:
83+
if ":" in p:
84+
lo, hi = p.split(":", 1)
85+
bounds.append((float(lo), float(hi)))
86+
else:
87+
# single number -> symmetric
88+
val = float(p)
89+
bounds.append((-abs(val), abs(val)))
90+
# if fewer provided, extend with wide bounds
91+
while len(bounds) < dim:
92+
bounds.append((-1e6, 1e6))
93+
return bounds[:dim]
94+
95+
def _on_run(self):
96+
try:
97+
initial = self._parse_initial()
98+
except Exception as e:
99+
messagebox.showerror("Input error", f"Invalid initial: {e}")
100+
return
101+
102+
func_name = self.func_var.get()
103+
func = example_functions.get(func_name)
104+
if func is None:
105+
messagebox.showerror("Input error", "Unknown function")
106+
return
107+
108+
try:
109+
temp = float(self.temp_entry.get())
110+
cooling = float(self.cool_entry.get())
111+
iterations = int(self.iter_entry.get())
112+
except Exception as e:
113+
messagebox.showerror("Input error", f"Invalid numeric param: {e}")
114+
return
115+
116+
bounds = self._parse_bounds(len(initial))
117+
118+
self.run_btn.config(state=tk.DISABLED)
119+
self.stop_btn.config(state=tk.NORMAL)
120+
self.stop_flag.clear()
121+
122+
def worker():
123+
sa = SimulatedAnnealing(func, initial, bounds=bounds, temperature=temp, cooling_rate=cooling, iterations_per_temp=iterations)
124+
best, cost, history = sa.optimize()
125+
# update plot on main thread
126+
self.after(0, lambda: self._on_complete(best, cost, history))
127+
128+
t = threading.Thread(target=worker, daemon=True)
129+
t.start()
130+
131+
def _on_stop(self):
132+
# currently we don't have a cooperative stop in the algorithm; inform user
133+
messagebox.showinfo("Stop", "Stop requested, but immediate stop is not implemented. The run will finish current loop.")
134+
135+
def _on_complete(self, best, cost, history):
136+
x = list(range(len(history.get("best_costs", []))))
137+
y = history.get("best_costs", [])
138+
self.ax.clear()
139+
self.ax.plot(x, y, label="best_cost")
140+
self.ax.set_xlabel("Iterations")
141+
self.ax.set_ylabel("Best cost")
142+
self.ax.grid(True)
143+
self.ax.legend()
144+
self.canvas.draw()
145+
146+
messagebox.showinfo("Done", f"Best cost: {cost:.6g}\nBest solution: {best}")
147+
self.run_btn.config(state=tk.NORMAL)
148+
self.stop_btn.config(state=tk.DISABLED)
149+
150+
151+
def main():
152+
app = SA_GUI()
153+
app.mainloop()
154+
155+
156+
if __name__ == "__main__":
157+
main()
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import math
2+
import random
3+
from typing import Callable, Sequence, Tuple, List, Optional
4+
5+
6+
class SimulatedAnnealing:
7+
"""Generic Simulated Annealing optimizer for continuous domains.
8+
9+
Usage:
10+
sa = SimulatedAnnealing(func, initial_solution, bounds=..., **params)
11+
best, best_cost, history = sa.optimize()
12+
"""
13+
14+
def __init__(
15+
self,
16+
func: Callable[[Sequence[float]], float],
17+
initial_solution: Sequence[float],
18+
bounds: Optional[Sequence[Tuple[float, float]]] = None,
19+
temperature: float = 100.0,
20+
cooling_rate: float = 0.99,
21+
min_temperature: float = 1e-3,
22+
iterations_per_temp: int = 100,
23+
neighbor_scale: float = 0.1,
24+
seed: Optional[int] = None,
25+
):
26+
self.func = func
27+
self.current = list(initial_solution)
28+
self.dim = len(initial_solution)
29+
self.bounds = bounds
30+
self.temperature = float(temperature)
31+
self.initial_temperature = float(temperature)
32+
self.cooling_rate = float(cooling_rate)
33+
self.min_temperature = float(min_temperature)
34+
self.iterations_per_temp = int(iterations_per_temp)
35+
self.neighbor_scale = float(neighbor_scale)
36+
if seed is not None:
37+
random.seed(seed)
38+
39+
def _clip(self, x: float, i: int) -> float:
40+
if not self.bounds:
41+
return x
42+
lo, hi = self.bounds[i]
43+
return max(lo, min(hi, x))
44+
45+
def _neighbor(self, solution: Sequence[float]) -> List[float]:
46+
# Gaussian perturbation scaled by neighbor_scale and variable range
47+
new = []
48+
for i, v in enumerate(solution):
49+
scale = self.neighbor_scale
50+
if self.bounds:
51+
lo, hi = self.bounds[i]
52+
rng = (hi - lo) if hi > lo else 1.0
53+
scale = self.neighbor_scale * rng
54+
candidate = v + random.gauss(0, scale)
55+
candidate = self._clip(candidate, i)
56+
new.append(candidate)
57+
return new
58+
59+
def _accept(self, delta: float, temp: float) -> bool:
60+
if delta < 0:
61+
return True
62+
try:
63+
prob = math.exp(-delta / temp)
64+
except OverflowError:
65+
prob = 0.0
66+
return random.random() < prob
67+
68+
def optimize(self, max_steps: Optional[int] = None) -> Tuple[List[float], float, dict]:
69+
"""Run optimization and return best solution, cost, and history.
70+
71+
history dict contains: temps, best_costs, current_costs
72+
"""
73+
temp = self.temperature
74+
current = list(self.current)
75+
current_cost = float(self.func(current))
76+
best = list(current)
77+
best_cost = current_cost
78+
79+
history = {"temps": [], "best_costs": [], "current_costs": []}
80+
81+
steps = 0
82+
while temp > self.min_temperature:
83+
for _ in range(self.iterations_per_temp):
84+
candidate = self._neighbor(current)
85+
candidate_cost = float(self.func(candidate))
86+
delta = candidate_cost - current_cost
87+
if self._accept(delta, temp):
88+
current = candidate
89+
current_cost = candidate_cost
90+
if current_cost < best_cost:
91+
best = list(current)
92+
best_cost = current_cost
93+
94+
history["temps"].append(temp)
95+
history["best_costs"].append(best_cost)
96+
history["current_costs"].append(current_cost)
97+
98+
steps += 1
99+
if max_steps is not None and steps >= max_steps:
100+
self.current = current
101+
return best, best_cost, history
102+
103+
# Cool down
104+
temp *= self.cooling_rate
105+
106+
self.current = current
107+
return best, best_cost, history
108+
109+
110+
def _test_quadratic():
111+
# Simple test: minimize f(x) = (x-3)^2
112+
func = lambda x: (x[0] - 3) ** 2
113+
sa = SimulatedAnnealing(func, [0.0], bounds=[(-10, 10)], temperature=10, iterations_per_temp=50)
114+
best, cost, hist = sa.optimize()
115+
print("best:", best, "cost:", cost)
116+
117+
118+
if __name__ == "__main__":
119+
_test_quadratic()

0 commit comments

Comments
 (0)