diff --git a/examples/knapsack_heuristics/README.md b/examples/knapsack_heuristics/README.md new file mode 100644 index 0000000000..b3eab58527 --- /dev/null +++ b/examples/knapsack_heuristics/README.md @@ -0,0 +1,31 @@ +# Knapsack Heuristics Evolution + +This example aims to discover efficient heuristics for the classic 0/1 Knapsack problem. + +## Problem Description + +The [0/1 Knapsack problem](https://en.wikipedia.org/wiki/Knapsack_problem) involves selecting a subset of items, each with a weight and a value, such that the total weight does not exceed a given capacity and the total value is maximized. + +While the problem is NP-hard, many heuristics exist. This example challenges the LLM to find novel heuristics that might outperform basic greedy approaches on various problem instances. + +## Structure + +- `initial_program.py`: A basic greedy heuristic that picks items based on their value-to-weight ratio. +- `evaluator.py`: Tests the heuristics on a suite of instances, measuring value achievement (`combined_score`) and constraint satisfaction (`correctness`). +- `config.yaml`: Configuration for the evolution process. + +## How to Run + +You can start the evolution from the root of the OpenEvolve repository: + +```bash +python openevolve-run.py examples/knapsack_heuristics/initial_program.py \ + examples/knapsack_heuristics/evaluator.py \ + --config examples/knapsack_heuristics/config.yaml \ + --iterations 50 +``` + +## Metrics + +- `combined_score`: Normalized value achieved across all test instances. Solutions that exceed capacity for an instance receive zero score for that instance. +- `correctness`: The percentage of instances solved without exceeding the knapsack capacity. diff --git a/examples/knapsack_heuristics/config.yaml b/examples/knapsack_heuristics/config.yaml new file mode 100644 index 0000000000..7ab20de6ce --- /dev/null +++ b/examples/knapsack_heuristics/config.yaml @@ -0,0 +1,102 @@ +# General settings +max_iterations: 20 +checkpoint_interval: 5 +log_level: "INFO" +language: "python" + +# LLM settings +llm: + primary_model: "gemini-2.5-flash" + api_base: "https://generativelanguage.googleapis.com/v1beta/openai/" + api_key: "${OPENAI_API_KEY}" + temperature: 0.7 + max_tokens: 2048 + +# Evolution settings +diff_based_evolution: true + +# Database settings +database: + population_size: 20 + num_islands: 2 + feature_dimensions: ["complexity", "combined_score"] + +# Evaluator configuration +evaluator: + cascade_evaluation: false + parallel_evaluations: 1 + +# Prompt settings +prompt: + num_top_programs: 3 + system_message: | + You are an expert algorithm designer. Your task is to evolve a heuristic for the 0/1 Knapsack problem. + The goal is to select a subset of items that maximizes the total value without exceeding the weight capacity. + + CRITICAL: You MUST use the Search & Replace format for all code modifications: + <<<<<<< SEARCH + [exact code to find] + ======= + [replacement code] + >>>>>>> REPLACE + + Focus on implementation strategies that can outperform simple greedy approaches: + 1. Consider multi-objective sorting criteria (value, weight, ratio, squared terms). + 2. Implement look-ahead or local search steps. + 3. Use probabilistic selection or adaptive weights. + 4. Explore dynamic programming approximations suitable for heuristics. + + Ensure your `solve_knapsack` function: + - Takes `items` (list of dicts with 'value' and 'weight') and `capacity` (float). + - Returns a list of integer indices of the selected items. + - Is robust and handles edge cases. + + For your reference, here is the evaluator code used to test your programs: + ```python + import importlib.util + import os + import sys + import random + + def solve_knapsack_dp(items, capacity): + """Solves the 0/1 knapsack problem using dynamic programming to find the optimal value.""" + n = len(items) + dp = [[0 for _ in range(capacity + 1)] for _ in range(n + 1)] + + for i in range(1, n + 1): + for w in range(1, capacity + 1): + if items[i - 1]["weight"] <= w: + dp[i][w] = max( + items[i - 1]["value"] + dp[i - 1][w - items[i - 1]["weight"]], + dp[i - 1][w], + ) + else: + dp[i][w] = dp[i - 1][w] + return dp[n][capacity] + + def generate_difficult_instance(n, weight_range=(10, 100), correlation="strong"): + items = [] + total_weight = 0 + for _ in range(n): + w = random.randint(*weight_range) + if correlation == "strong": + v = w + 10 + elif correlation == "weak": + v = max(1, w + random.randint(-5, 5)) + elif correlation == "uncorrelated": + v = random.randint(10, 100) + elif correlation == "inverse": + v = int(w * (0.8 + random.random() * 0.4)) + else: + v = random.randint(10, 100) + items.append({"value": v, "weight": w}) + total_weight += w + + capacity = int(total_weight * 0.3) + optimal_value = solve_knapsack_dp(items, capacity) + return {"capacity": capacity, "items": items, "optimal_value": optimal_value} + + def evaluate(module_path): + # ... (implementation details for testing solve_knapsack) + pass + ``` diff --git a/examples/knapsack_heuristics/evaluator.py b/examples/knapsack_heuristics/evaluator.py new file mode 100644 index 0000000000..f49547dc3c --- /dev/null +++ b/examples/knapsack_heuristics/evaluator.py @@ -0,0 +1,187 @@ +import importlib.util +import os +import sys +import random + + +def solve_knapsack_dp(items, capacity): + """Solves the 0/1 knapsack problem using dynamic programming to find the optimal value.""" + n = len(items) + dp = [[0 for _ in range(capacity + 1)] for _ in range(n + 1)] + + for i in range(1, n + 1): + for w in range(1, capacity + 1): + if items[i - 1]["weight"] <= w: + dp[i][w] = max( + items[i - 1]["value"] + dp[i - 1][w - items[i - 1]["weight"]], + dp[i - 1][w], + ) + else: + dp[i][w] = dp[i - 1][w] + return dp[n][capacity] + + +def generate_difficult_instance(n, weight_range=(10, 100), correlation="strong"): + """ + Generates a difficult knapsack instance. + "strong": value = weight + 10 (hard for some, but greedy ratio still okay) + "weak": value = weight + random(-5, 5) + "uncorrelated": value and weight independent + "inverse": large weights have slightly better value but worse ratio + """ + items = [] + total_weight = 0 + for _ in range(n): + w = random.randint(*weight_range) + if correlation == "strong": + v = w + 10 + elif correlation == "weak": + v = max(1, w + random.randint(-5, 5)) + elif correlation == "uncorrelated": + v = random.randint(10, 100) + elif correlation == "inverse": + # Higher weight, higher value, but ratio might be tricky + v = int(w * (0.8 + random.random() * 0.4)) + else: + v = random.randint(10, 100) + items.append({"value": v, "weight": w}) + total_weight += w + + capacity = int(total_weight * 0.3) # Even tighter capacity + optimal_value = solve_knapsack_dp(items, capacity) + return {"capacity": capacity, "items": items, "optimal_value": optimal_value} + + +def get_test_instances(): + """Returns a list of difficult knapsack problem instances.""" + # Seed for reproducibility + random.seed(42) + + instances = [ + # 1. Tricky small instance (Greedy trap) + { + "name": "greedy_trap_small", + "capacity": 100, + "items": [ + {"value": 10, "weight": 60}, + {"value": 10, "weight": 60}, + {"value": 12, "weight": 100}, + ], + "optimal_value": 12, + }, + # 2. Uncorrelated (Medium) + { + **generate_difficult_instance(30, correlation="uncorrelated"), + "name": "uncorrelated_medium", + }, + # 3. Inverse correlated (Hard for greedy) + { + **generate_difficult_instance(40, correlation="inverse"), + "name": "inverse_correlated_hard", + }, + # 4. Strongly correlated (classic hard) + { + **generate_difficult_instance(20, correlation="strong"), + "name": "strongly_correlated_hard", + }, + # 5. Large scale uncorrelated + { + **generate_difficult_instance( + 200, weight_range=(5, 50), correlation="uncorrelated" + ), + "name": "large_scale_unclorrelated", + }, + # 6. Another greedy trap + { + "name": "greedy_trap_medium", + "capacity": 50, + "items": [ + {"value": 31, "weight": 30}, + {"value": 20, "weight": 25}, + {"value": 20, "weight": 25}, + ], + "optimal_value": 40, + }, + ] + return [i for i in instances if i["optimal_value"] > 0] + + +def evaluate(module_path): + """ + Evaluates the solve_knapsack function in the given module. + """ + # Load the module dynamically + spec = importlib.util.spec_from_file_location("evolved_module", module_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + if not hasattr(module, "solve_knapsack"): + return { + "combined_score": 0.0, + "correctness": 0.0, + "error": "Function solve_knapsack not found", + } + + test_instances = get_test_instances() + total_score = 0 + correct_count = 0 + instance_scores = {} + for instance in test_instances: + instance_score = 0.0 + try: + selected_indices = module.solve_knapsack( + instance["items"], instance["capacity"] + ) + + total_value = 0 + total_weight = 0 + valid_indices = True + + if not isinstance(selected_indices, (list, tuple)): + valid_indices = False + elif len(set(selected_indices)) != len(selected_indices): + valid_indices = False + + if valid_indices: + for idx in selected_indices: + if not (0 <= idx < len(instance["items"])): + valid_indices = False + break + total_value += instance["items"][idx]["value"] + total_weight += instance["items"][idx]["weight"] + + if valid_indices and total_weight <= instance["capacity"]: + correct_count += 1 + # Normalized by the actual optimal value found by DP + instance_score = min(1.0, total_value / instance["optimal_value"]) + else: + instance_score = 0.0 + + except Exception: + # Failed to execute + instance_score = 0.0 + + total_score += instance_score + instance_scores[instance["name"]] = instance_score + + num_instances = len(test_instances) + combined_score = total_score / num_instances if num_instances > 0 else 0 + correctness = correct_count / num_instances if num_instances > 0 else 0 + + return { + "combined_score": combined_score, + "correctness": correctness, + "instance_scores": instance_scores, + } + + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: python evaluator.py ") + sys.exit(1) + + metrics = evaluate(sys.argv[1]) + print( + f"combined_score={metrics['combined_score']:.4f}, correctness={metrics['correctness']:.4f}" + ) + print(f"instance_scores={metrics['instance_scores']}") diff --git a/examples/knapsack_heuristics/initial_program.py b/examples/knapsack_heuristics/initial_program.py new file mode 100644 index 0000000000..64fec68bbb --- /dev/null +++ b/examples/knapsack_heuristics/initial_program.py @@ -0,0 +1,30 @@ +def solve_knapsack(items, capacity): + """ + Solves the 0/1 knapsack problem using a greedy heuristic based on value/weight ratio. + + Args: + items: List of dicts, each with 'value' and 'weight'. + capacity: Maximum weight capacity of the knapsack. + + Returns: + A list of indices of the items selected for the knapsack. + """ + # Calculate value/weight ratio for each item and store with original index + ratios = [] + for i, item in enumerate(items): + ratio = item["value"] / item["weight"] if item["weight"] > 0 else float("inf") + ratios.append((ratio, i)) + + # Sort items by ratio in descending order + ratios.sort(key=lambda x: x[0], reverse=True) + + selected_indices = [] + current_weight = 0 + + for ratio, index in ratios: + item_weight = items[index]["weight"] + if current_weight + item_weight <= capacity: + selected_indices.append(index) + current_weight += item_weight + + return selected_indices