Skip to content

Commit 87c449a

Browse files
committed
Merge branch 'main' into feat/MLX-kernel-optimization
2 parents 48a953a + 153f5a7 commit 87c449a

File tree

3 files changed

+109
-60
lines changed

3 files changed

+109
-60
lines changed

examples/function_minimization/README.md

Lines changed: 60 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,47 @@ 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. **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.
141+
```python
142+
probability = np.exp((current_value - new_value) / temperature)
143+
```
144+
145+
2. **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.
146+
```python
147+
if i > iterations * 0.75: # Reduce step size towards the end
148+
step_size *= 0.5
149+
if no_improvement_count > step_size_increase_threshold: # Increase step size if stuck
150+
step_size *= 1.1
151+
no_improvement_count = 0 # Reset the counter
152+
```
153+
154+
3. **Bounded Moves**: The algorithm ensures all candidate solutions remain within the feasible domain, avoiding wasted evaluations.
155+
```python
156+
# Keep the new points within the bounds
157+
new_x = max(bounds[0], min(new_x, bounds[1]))
158+
new_y = max(bounds[0], min(new_y, bounds[1]))
159+
```
160+
161+
4. **Stagnation Handling**: By counting iterations without improvement, the algorithm responds by boosting exploration when progress stalls.
162+
```python
163+
if no_improvement_count > step_size_increase_threshold: # Increase step size if stuck
164+
step_size *= 1.1
165+
no_improvement_count = 0 # Reset the counter
166+
```
146167

147168
## Results
148169

149170
The evolved algorithm shows substantial improvement in finding better solutions:
150171

151172
| Metric | Value |
152173
|--------|-------|
153-
| Value Score | 0.677 |
154-
| Distance Score | 0.258 |
174+
| Value Score | 0.990 |
175+
| Distance Score | 0.921 |
176+
| Standard Deviation Score | 0.900 |
177+
| Speed Score | 0.466 |
155178
| Reliability Score | 1.000 |
156-
| Overall Score | 0.917 |
157-
| Combined Score | 0.584 |
179+
| Overall Score | 0.984 |
180+
| Combined Score | 0.922 |
158181

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

examples/function_minimization/evaluator.py

Lines changed: 45 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,31 @@ 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+
26+
def wrapper(queue, func, args, kwargs):
2927
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-
)
28+
result = func(*args, **kwargs)
29+
queue.put(("success", result))
30+
except Exception as e:
31+
queue.put(("error", e))
32+
33+
queue = multiprocessing.Queue()
34+
process = multiprocessing.Process(target=wrapper, args=(queue, func, args, kwargs))
35+
process.start()
36+
process.join(timeout=timeout_seconds)
37+
38+
if process.is_alive():
39+
process.terminate()
40+
process.join()
41+
raise TimeoutError(f"Function timed out after {timeout_seconds} seconds")
42+
43+
if queue.empty():
44+
raise TimeoutError("Function ended without returning a result")
45+
46+
status, result = queue.get()
47+
if status == "error":
48+
raise result
49+
return result
3550

3651

3752
def safe_float(value):
@@ -78,6 +93,8 @@ def evaluate(program_path):
7893

7994
# Run multiple trials
8095
num_trials = 10
96+
x_values = []
97+
y_values = []
8198
values = []
8299
distances = []
83100
times = []
@@ -119,14 +136,15 @@ def evaluate(program_path):
119136
continue
120137

121138
# Calculate metrics
122-
x_diff = safe_float(x) - GLOBAL_MIN_X
123-
y_diff = safe_float(y) - GLOBAL_MIN_Y
139+
x_diff = x - GLOBAL_MIN_X
140+
y_diff = y - GLOBAL_MIN_Y
124141
distance_to_global = np.sqrt(x_diff**2 + y_diff**2)
125-
value_difference = abs(value - GLOBAL_MIN_VALUE)
126142

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

132150
except TimeoutError as e:
@@ -164,6 +182,11 @@ def evaluate(program_path):
164182
distance_score = float(1.0 / (1.0 + avg_distance))
165183
speed_score = float(1.0 / avg_time) if avg_time > 0 else 0.0
166184

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

@@ -175,7 +198,11 @@ def evaluate(program_path):
175198
# Value and distance scores (quality of solution) get 90% of the weight
176199
# Speed and reliability get only 10% combined
177200
combined_score = float(
178-
0.6 * value_score + 0.3 * distance_score + 0.05 * speed_score + 0.05 * reliability_score
201+
0.35 * value_score
202+
+ 0.35 * distance_score
203+
+ standard_deviation_score * 0.20
204+
+ 0.05 * speed_score
205+
+ 0.05 * reliability_score
179206
)
180207

181208
# Also compute an "overall" score that will be the primary metric for selection
@@ -194,6 +221,7 @@ def evaluate(program_path):
194221
return {
195222
"value_score": value_score,
196223
"distance_score": distance_score,
224+
"standard_deviation_score": standard_deviation_score,
197225
"speed_score": speed_score,
198226
"reliability_score": reliability_score,
199227
"combined_score": combined_score,
@@ -282,8 +310,6 @@ def evaluate_stage1(program_path):
282310
# Basic metrics with overall score
283311
return {
284312
"runs_successfully": 1.0,
285-
"value": float(value),
286-
"distance": distance,
287313
"value_score": value_score,
288314
"distance_score": distance_score,
289315
"overall_score": solution_quality, # This becomes a strong guiding metric

examples/function_minimization/initial_program.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,15 @@ def search_algorithm(iterations=1000, bounds=(-5, 5)):
3232
return best_x, best_y, best_value
3333

3434

35+
# EVOLVE-BLOCK-END
36+
37+
38+
# This part remains fixed (not evolved)
3539
def evaluate_function(x, y):
3640
"""The complex function we're trying to minimize"""
3741
return np.sin(x) * np.cos(y) + np.sin(x * y) + (x**2 + y**2) / 20
3842

3943

40-
# EVOLVE-BLOCK-END
41-
42-
43-
# This part remains fixed (not evolved)
4444
def run_search():
4545
x, y, value = search_algorithm()
4646
return x, y, value

0 commit comments

Comments
 (0)