Skip to content

Commit eb1421b

Browse files
committed
init
1 parent babe3f9 commit eb1421b

File tree

2 files changed

+84
-56
lines changed

2 files changed

+84
-56
lines changed

examples/function_minimization/README.md

Lines changed: 44 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -62,56 +62,73 @@ def search_algorithm(iterations=1000, bounds=(-5, 5)):
6262
After running OpenEvolve, it discovered a simulated annealing algorithm with a completely different approach:
6363

6464
```python
65-
def simulated_annealing(bounds=(-5, 5), iterations=1000, step_size=0.1, initial_temperature=100, cooling_rate=0.99):
65+
def search_algorithm(bounds=(-5, 5), iterations=2000, initial_temperature=100, cooling_rate=0.97, step_size_factor=0.2, step_size_increase_threshold=20):
6666
"""
6767
Simulated Annealing algorithm for function minimization.
6868
6969
Args:
7070
bounds: Bounds for the search space (min, max)
7171
iterations: Number of iterations to run
72-
step_size: Step size for perturbing the solution
7372
initial_temperature: Initial temperature for the simulated annealing process
7473
cooling_rate: Cooling rate for the simulated annealing process
75-
74+
step_size_factor: Factor to scale the initial step size by the range
75+
step_size_increase_threshold: Number of iterations without improvement before increasing step size
76+
7677
Returns:
7778
Tuple of (best_x, best_y, best_value)
7879
"""
79-
# Initialize with a random point
80+
# Initialize
8081
best_x = np.random.uniform(bounds[0], bounds[1])
8182
best_y = np.random.uniform(bounds[0], bounds[1])
8283
best_value = evaluate_function(best_x, best_y)
8384

8485
current_x, current_y = best_x, best_y
8586
current_value = best_value
8687
temperature = initial_temperature
88+
step_size = (bounds[1] - bounds[0]) * step_size_factor # Initial step size
89+
min_temperature = 1e-6 # Avoid premature convergence
90+
no_improvement_count = 0 # Counter for tracking stagnation
91+
92+
for i in range(iterations):
93+
# Adaptive step size and temperature control
94+
if i > iterations * 0.75: # Reduce step size towards the end
95+
step_size *= 0.5
96+
if no_improvement_count > step_size_increase_threshold: # Increase step size if stuck
97+
step_size *= 1.1
98+
no_improvement_count = 0 # Reset the counter
99+
100+
step_size = min(step_size, (bounds[1] - bounds[0]) * 0.5) # Limit step size
87101

88-
for _ in range(iterations):
89-
# Perturb the current solution
90102
new_x = current_x + np.random.uniform(-step_size, step_size)
91103
new_y = current_y + np.random.uniform(-step_size, step_size)
92104

93-
# Ensure the new solution is within bounds
105+
# Keep the new points within the bounds
94106
new_x = max(bounds[0], min(new_x, bounds[1]))
95107
new_y = max(bounds[0], min(new_y, bounds[1]))
96108

97109
new_value = evaluate_function(new_x, new_y)
98110

99-
# Calculate the acceptance probability
100111
if new_value < current_value:
112+
# Accept the move if it's better
101113
current_x, current_y = new_x, new_y
102114
current_value = new_value
115+
no_improvement_count = 0 # Reset counter
103116

104117
if new_value < best_value:
118+
# Update the best found solution
105119
best_x, best_y = new_x, new_y
106120
best_value = new_value
107121
else:
122+
# Accept with a certain probability (Simulated Annealing)
108123
probability = np.exp((current_value - new_value) / temperature)
109124
if np.random.rand() < probability:
110125
current_x, current_y = new_x, new_y
111126
current_value = new_value
127+
no_improvement_count = 0 # Reset counter
128+
else:
129+
no_improvement_count += 1 # Increment counter if not improving
112130

113-
# Cool down the temperature
114-
temperature *= cooling_rate
131+
temperature = max(temperature * cooling_rate, min_temperature) #Cool down
115132

116133
return best_x, best_y, best_value
117134
```
@@ -120,41 +137,31 @@ def simulated_annealing(bounds=(-5, 5), iterations=1000, step_size=0.1, initial_
120137

121138
Through evolutionary iterations, OpenEvolve discovered several key algorithmic concepts:
122139

123-
1. **Local Search**: Instead of random sampling across the entire space, the evolved algorithm makes small perturbations to promising solutions:
124-
```python
125-
new_x = current_x + np.random.uniform(-step_size, step_size)
126-
new_y = current_y + np.random.uniform(-step_size, step_size)
127-
```
128-
129-
2. **Temperature-based Acceptance**: The algorithm can escape local minima by occasionally accepting worse solutions:
130-
```python
131-
probability = np.exp((current_value - new_value) / temperature)
132-
if np.random.rand() < probability:
133-
current_x, current_y = new_x, new_y
134-
current_value = new_value
135-
```
136-
137-
3. **Cooling Schedule**: The temperature gradually decreases, transitioning from exploration to exploitation:
138-
```python
139-
temperature *= cooling_rate
140-
```
141-
142-
4. **Parameter Introduction**: The system discovered the need for additional parameters to control the algorithm's behavior:
143-
```python
144-
def simulated_annealing(bounds=(-5, 5), iterations=1000, step_size=0.1, initial_temperature=100, cooling_rate=0.99):
145-
```
140+
1. **Memory and Exploitation**: The evolved algorithm tracks and updates the best solution seen so far, allowing for continual improvement rather than random restarting.
141+
142+
2. **Exploration via Temperature**: Simulated annealing uses a “temperature” parameter to allow uphill moves early in the search, helping escape local minima that would trap simpler methods.
143+
144+
3. **Adaptive Step Size**: The step size is adjusted dynamically—shrinking as the search converges and expanding if progress stalls—leading to better coverage and faster convergence.
145+
146+
4. **Bounded Moves**: The algorithm ensures all candidate solutions remain within the feasible domain, avoiding wasted evaluations.
147+
148+
5. **Stagnation Handling**: By counting iterations without improvement, the algorithm responds by boosting exploration when progress stalls.
149+
150+
6. **Probabilistic Acceptance**: Moves to worse solutions are allowed with a probability that decays over time, providing a principled way to balance exploration and exploitation.
146151

147152
## Results
148153

149154
The evolved algorithm shows substantial improvement in finding better solutions:
150155

151156
| Metric | Value |
152157
|--------|-------|
153-
| Value Score | 0.677 |
154-
| Distance Score | 0.258 |
158+
| Value Score | 0.990 |
159+
| Distance Score | 0.921 |
160+
| Standard Deviation Score | 0.900 |
161+
| Speed Score | 0.466 |
155162
| Reliability Score | 1.000 |
156-
| Overall Score | 0.917 |
157-
| Combined Score | 0.584 |
163+
| Overall Score | 0.984 |
164+
| Combined Score | 0.922 |
158165

159166
The simulated annealing algorithm:
160167
- Achieves higher quality solutions (closer to the global minimum)

examples/function_minimization/evaluator.py

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,8 @@
55
import importlib.util
66
import numpy as np
77
import time
8-
import concurrent.futures
9-
import threading
8+
import multiprocessing
109
import traceback
11-
import sys
1210

1311

1412
def run_with_timeout(func, args=(), kwargs={}, timeout_seconds=5):
@@ -24,14 +22,30 @@ def run_with_timeout(func, args=(), kwargs={}, timeout_seconds=5):
2422
Returns:
2523
Result of the function or raises TimeoutError
2624
"""
27-
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
28-
future = executor.submit(func, *args, **kwargs)
25+
def wrapper(queue, func, args, kwargs):
2926
try:
30-
return future.result(timeout=timeout_seconds)
31-
except concurrent.futures.TimeoutError:
32-
raise TimeoutError(
33-
f"Function {func.__name__} timed out after {timeout_seconds} seconds"
34-
)
27+
result = func(*args, **kwargs)
28+
queue.put(('success', result))
29+
except Exception as e:
30+
queue.put(('error', e))
31+
32+
queue = multiprocessing.Queue()
33+
process = multiprocessing.Process(target=wrapper, args=(queue, func, args, kwargs))
34+
process.start()
35+
process.join(timeout=timeout_seconds)
36+
37+
if process.is_alive():
38+
process.terminate()
39+
process.join()
40+
raise TimeoutError(f"Function timed out after {timeout_seconds} seconds")
41+
42+
if queue.empty():
43+
raise TimeoutError("Function ended without returning a result")
44+
45+
status, result = queue.get()
46+
if status == 'error':
47+
raise result
48+
return result
3549

3650

3751
def safe_float(value):
@@ -78,6 +92,8 @@ def evaluate(program_path):
7892

7993
# Run multiple trials
8094
num_trials = 10
95+
x_values = []
96+
y_values = []
8197
values = []
8298
distances = []
8399
times = []
@@ -119,14 +135,15 @@ def evaluate(program_path):
119135
continue
120136

121137
# Calculate metrics
122-
x_diff = safe_float(x) - GLOBAL_MIN_X
123-
y_diff = safe_float(y) - GLOBAL_MIN_Y
138+
x_diff = x - GLOBAL_MIN_X
139+
y_diff = y - GLOBAL_MIN_Y
124140
distance_to_global = np.sqrt(x_diff**2 + y_diff**2)
125-
value_difference = abs(value - GLOBAL_MIN_VALUE)
126141

127-
values.append(float(value))
128-
distances.append(float(distance_to_global))
129-
times.append(float(end_time - start_time))
142+
x_values.append(x)
143+
y_values.append(y)
144+
values.append(value)
145+
distances.append(distance_to_global)
146+
times.append(end_time - start_time)
130147
success_count += 1
131148

132149
except TimeoutError as e:
@@ -164,6 +181,11 @@ def evaluate(program_path):
164181
distance_score = float(1.0 / (1.0 + avg_distance))
165182
speed_score = float(1.0 / avg_time) if avg_time > 0 else 0.0
166183

184+
# calculate standard deviation scores
185+
x_std_score = float(1.0 / (1.0 + np.std(x_values)))
186+
y_std_score = float(1.0 / (1.0 + np.std(x_values)))
187+
standard_deviation_score = (x_std_score + y_std_score) / 2.0
188+
167189
# Normalize speed score (so it doesn't dominate)
168190
speed_score = float(min(speed_score, 10.0) / 10.0)
169191

@@ -175,7 +197,7 @@ def evaluate(program_path):
175197
# Value and distance scores (quality of solution) get 90% of the weight
176198
# Speed and reliability get only 10% combined
177199
combined_score = float(
178-
0.6 * value_score + 0.3 * distance_score + 0.05 * speed_score + 0.05 * reliability_score
200+
0.35 * value_score + 0.35 * distance_score + standard_deviation_score * 0.20 + 0.05 * speed_score + 0.05 * reliability_score
179201
)
180202

181203
# Also compute an "overall" score that will be the primary metric for selection
@@ -194,6 +216,7 @@ def evaluate(program_path):
194216
return {
195217
"value_score": value_score,
196218
"distance_score": distance_score,
219+
"standard_deviation_score": standard_deviation_score,
197220
"speed_score": speed_score,
198221
"reliability_score": reliability_score,
199222
"combined_score": combined_score,
@@ -282,8 +305,6 @@ def evaluate_stage1(program_path):
282305
# Basic metrics with overall score
283306
return {
284307
"runs_successfully": 1.0,
285-
"value": float(value),
286-
"distance": distance,
287308
"value_score": value_score,
288309
"distance_score": distance_score,
289310
"overall_score": solution_quality, # This becomes a strong guiding metric

0 commit comments

Comments
 (0)