From 9dd50c29d8897b95128a3a1690c3532ff18efa75 Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Tue, 3 Jun 2025 18:29:32 -0500 Subject: [PATCH 001/314] upload beam search and beam search history algorithms --- examples/example_usage_trainer.py | 360 ++++++++ .../algorithms/beamsearch_algorithm.py | 781 ++++++++++++++++++ 2 files changed, 1141 insertions(+) create mode 100644 examples/example_usage_trainer.py create mode 100644 opto/trainer/algorithms/beamsearch_algorithm.py diff --git a/examples/example_usage_trainer.py b/examples/example_usage_trainer.py new file mode 100644 index 00000000..78ff2793 --- /dev/null +++ b/examples/example_usage_trainer.py @@ -0,0 +1,360 @@ +# Standard library imports +import os +import time +import argparse +from typing import Any, Dict, List, Optional, Tuple, Union + +# Third-party imports +import datasets +import numpy as np + +# Opto imports +from opto import trace +from opto.optimizers import OptoPrime +from opto.optimizers.utils import print_color +from opto.trace.modules import Module +from opto.trainer.algorithms.basic_algorithm import MinibatchAlgorithm, BasicSearchAlgorithm +from opto.trainer.algorithms.beamsearch_algorithm import BeamsearchAlgorithm, BeamsearchHistoryAlgorithm +from opto.trainer.guide import AutoGuide +from opto.trainer.utils import DefaultLogger +from opto.utils.llm import LLM, LiteLLM + +# Set default model +os.environ["TRACE_LITELLM_MODEL"] = "vertex_ai/gemini-2.0-flash" + +@trace.model +class Learner(Module): + """A basic LLM Agent for solving math problems.""" + + def __init__(self, + system_prompt: str = "You're a helpful agent answering math problems.", + user_prompt_template: str = "Solve the following math problem step-by-step: {message}", + llm: LLM = None): + """Initialize the learner agent. + + Args: + system_prompt: System prompt to guide LLM behavior + user_prompt_template: Template for formatting user messages + llm: LLM instance to use for generation (defaults to gpt-3.5-turbo) + """ + super().__init__() + self.system_prompt = trace.node(system_prompt, trainable=True) + self.user_prompt_template = trace.node(user_prompt_template, trainable=True) + self.llm = llm or LiteLLM(model="gpt-3.5-turbo") + + @trace.bundle() + def call_llm(self, system_prompt: str, user_prompt: str) -> str: + """Call LLM model with the given prompts. + + Args: + system_prompt: The system prompt + user_prompt: The user prompt + + Returns: + The LLM response content + """ + response = self.llm( + messages=[ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt} + ] + ) + return response.choices[0].message.content + + def forward(self, message: Any) -> str: + """Agent's forward pass to process a message. + + Args: + message: The input message to process + + Returns: + The generated response + """ + user_prompt = self.user_prompt_template.format(message=message) + return self.call_llm(self.system_prompt, user_prompt) + + +class TeacherGuide(AutoGuide): + """Guide that uses LLM to judge answers and provide feedback.""" + + def __init__(self, model: str = "gpt-4o-mini"): + """Initialize the teacher guide. + + Args: + model: The LLM model to use for evaluation + """ + super().__init__() + self.guide_llm = LiteLLM(model=model) + self.system_prompt = "You are an expert math teacher evaluating student answers." + self.judge_prompt_template = ( + "Carefully review the following three distinct sections:\n\n" + "SECTION 1: The Math Problem\n" + "----------------------------\n" + "{query}\n" + "----------------------------\n\n" + "SECTION 2: The Student's Full Answer\n" + "----------------------------\n" + "{response}\n" + "----------------------------\n\n" + "SECTION 3: The Official Correct Answer\n" + "----------------------------\n" + "{reference}\n" + "----------------------------\n\n" + "INSTRUCTIONS FOR JUDGING:\n" + "1. Your primary task is to compare the student's **final numerical result** (or final conclusion if no number is present) from SECTION 2 with the **Official Correct Answer** provided in SECTION 3.\n" + "2. When evaluating SECTION 2 (Student's Full Answer), focus SOLELY on the **final answer part** of the student's response. Ignore all intermediate steps, reasoning, or explanations for the correctness check unless the problem specifically asks for reasoning as the final answer.\n" + "3. Determine if the student's **final answer** is equivalent to the **Official Correct Answer**.\n\n" + "RESPONSE FORMAT:\n" + "- If the student's final answer (from SECTION 2) IS equivalent to the Official Correct Answer (from SECTION 3), respond ONLY with the exact phrase: 'Correct [TERMINATE]'\n" + "- If the student's final answer IS NOT equivalent, respond ONLY with specific and actionable feedback. The feedback should clearly explain the error in the student's final answer and guide them on how to arrive at the Official Correct Answer." + ) + + def get_feedback(self, task: str, response: str, info: Any, **kwargs) -> Tuple[float, str]: + """Get feedback on a student response. + + Args: + task: The original math problem + response: The student's answer + info: The reference/correct answer + **kwargs: Additional arguments + + Returns: + Tuple of (score, feedback_text) + """ + user_prompt = self.judge_prompt_template.format( + query=task, + response=response, + reference=info + ) + + messages = [ + {"role": "system", "content": self.system_prompt}, + {"role": "user", "content": user_prompt} + ] + + llm_response = self.guide_llm(messages=messages) + feedback_text = llm_response.choices[0].message.content + + if 'Correct [TERMINATE]' in feedback_text: + return 1.0, "Correct." + else: + return 0.0, f"Incorrect. Feedback: {feedback_text}" + + def metric(self, task: str, content: str, info: Any, **kwargs) -> float: + """Calculate the metric score for an answer. + + Args: + task: The original math problem + content: The student's answer + info: The reference/correct answer + **kwargs: Additional arguments + + Returns: + Score (0.0 or 1.0) + """ + score, _ = self.get_feedback(task, content, info, **kwargs) + return score + + +class SimpleLogger(DefaultLogger): + """Simplified logger that only shows important metrics.""" + + def log(self, name: str, data: Any, step: int, **kwargs): + """Log only specific metrics to reduce output clutter. + + Args: + name: The name of the metric + data: The metric value + step: The current step + **kwargs: Additional logging arguments + """ + important_metrics = [ + 'Average train score', + 'Average test score', + 'Validation score' + ] + + if name in important_metrics or 'Parameter' in name: + super().log(name, data, step, **kwargs) + + +def main(): + """Run the main training process with command line arguments.""" + parser = argparse.ArgumentParser(description='Train agent using various algorithms') + + # Algorithm parameters + parser.add_argument('--algorithm_type', type=str, default='beamsearchhistory', + choices=['minibatch', 'basicsearch', 'beamsearch', 'beamsearchhistory'], + help='Type of algorithm to use') + + # Dataset parameters + parser.add_argument('--dataset', type=str, default='xuanfeiren/math_hard_gemini', + help='Dataset to use for training') + parser.add_argument('--num_train_samples', type=int, default=66, + help='Number of training samples') + parser.add_argument('--num_validate_samples', type=int, default=20, + help='Number of validation samples') + parser.add_argument('--num_test_samples', type=int, default=1, + help='Number of test samples') + + # Model parameters + parser.add_argument('--trace_model', type=str, default='vertex_ai/gemini-2.0-flash', + help='Model to use for trace operations') + parser.add_argument('--student_model', type=str, default='vertex_ai/gemini-2.0-flash', + help='Model to use for student agent') + parser.add_argument('--teacher_model', type=str, default='vertex_ai/gemini-2.0-flash', + help='Model to use for teacher guide') + + # Training parameters + parser.add_argument('--num_epochs', type=int, default=1, + help='Number of training epochs') + parser.add_argument('--batch_size', type=int, default=2, + help='Training batch size') + parser.add_argument('--num_threads', type=int, default=20, + help='Number of threads for parallel processing') + parser.add_argument('--eval_frequency', type=int, default=2, + help='How often to run evaluation') + parser.add_argument('--log_frequency', type=int, default=20, + help='How often to log results') + parser.add_argument('--seed', type=int, default=42, + help='Random seed for reproducibility') + + # Algorithm-specific parameters + parser.add_argument('--beam_width', type=int, default=2, + help='Beam width for beam search algorithms') + parser.add_argument('--num_proposals', type=int, default=2, + help='Number of proposals for beam search algorithms') + parser.add_argument('--max_depth', type=int, default=5, + help='Maximum depth for beam search algorithms') + parser.add_argument('--validation_dataset_size', type=int, default=20, + help='Size of validation dataset for beam search') + parser.add_argument('--max_history_size', type=int, default=12, + help='Maximum history size for history-based algorithms') + parser.add_argument('--num_basicsearch_proposals', type=int, default=2, + help='Number of proposals for basic search algorithm') + + args = parser.parse_args() + + # Set environment variables + os.environ["TRACE_LITELLM_MODEL"] = args.trace_model + + # Set random seed + np.random.seed(args.seed) + + # Check for API Keys + if not os.getenv("OPENAI_API_KEY") and not os.getenv("ANTHROPIC_API_KEY"): + print_color("Warning: OPENAI_API_KEY or ANTHROPIC_API_KEY environment variables not found. LLM calls may fail.", "red") + + # Load and prepare data + print(f"Loading data from {args.dataset}...") + math_data = datasets.load_dataset(args.dataset) + + # Select data subsets + train_data = math_data['train'].select( + range(args.num_train_samples, args.num_train_samples + args.num_validate_samples) + ) + validate_data = train_data + test_data = math_data['test'].select(range(args.num_test_samples)) + + # Format data for trainer + train_dataset = {'inputs': train_data['problem'], 'infos': train_data['solution']} + validate_dataset = {'inputs': validate_data['problem'], 'infos': validate_data['solution']} + test_dataset = {'inputs': test_data['problem'], 'infos': test_data['solution']} + + # Log dataset sizes + print(f"Training samples: {len(train_dataset['inputs'])}") + print(f"Validation samples: {len(validate_dataset['inputs'])}") + print(f"Test samples: {len(test_dataset['inputs'])}") + + # Initialize components + print("Initializing Agent, Guide, Optimizer, Algorithm...") + student_llm = LiteLLM(model=args.student_model) + agent = Learner(llm=student_llm) + + train_guide = TeacherGuide(model=args.teacher_model) + validate_guide = TeacherGuide(model=args.teacher_model) + + optimizer = OptoPrime(agent.parameters()) + logger = SimpleLogger() + + # Create algorithm + if args.algorithm_type == 'minibatch': + algorithm = MinibatchAlgorithm( + agent=agent, + optimizer=optimizer, + logger=logger, + num_threads=args.num_threads + ) + elif args.algorithm_type == 'basicsearch': + algorithm = BasicSearchAlgorithm( + agent=agent, + optimizer=optimizer, + logger=logger, + num_threads=args.num_threads + ) + elif args.algorithm_type == 'beamsearch': + algorithm = BeamsearchAlgorithm( + agent=agent, + optimizer=optimizer, + logger=logger, + num_threads=args.num_threads + ) + elif args.algorithm_type == 'beamsearchhistory': + algorithm = BeamsearchHistoryAlgorithm( + agent=agent, + optimizer=optimizer, + logger=logger, + num_threads=args.num_threads + ) + else: + raise ValueError(f"Unknown algorithm type: {args.algorithm_type}") + + # Prepare training parameters + train_params = { + "guide": train_guide, + "train_dataset": train_dataset, + "num_epochs": args.num_epochs, + "num_threads": args.num_threads, + "batch_size": args.batch_size, + "test_dataset": test_dataset, + "validate_dataset": validate_dataset, + "validate_guide": validate_guide, + "eval_frequency": args.eval_frequency, + "log_frequency": args.log_frequency, + "validation_dataset_size": args.validation_dataset_size, + } + + # Add algorithm-specific parameters + if args.algorithm_type in ['beamsearch', 'beamsearchhistory']: + train_params.update({ + "beam_width": args.beam_width, + "num_proposals": args.num_basicsearch_proposals, + "max_depth": args.max_depth + }) + + if args.algorithm_type == 'beamsearchhistory': + train_params["max_history_size"] = args.max_history_size + + elif args.algorithm_type == 'basicsearch': + train_params["num_proposals"] = args.num_basicsearch_proposals + + # Start training + print(f"Training with {args.algorithm_type} algorithm...") + start_time = time.time() + metrics, final_score = algorithm.train(**train_params) + duration = time.time() - start_time + print(f"Training complete, time taken: {duration:.2f} seconds") + + # Print metrics summary based on algorithm type + if args.algorithm_type in ['beamsearch', 'beamsearchhistory'] and 'best_validation_scores' in metrics: + print("\nBest validation scores at each depth:") + for depth, score in enumerate(metrics['best_validation_scores']): + print(f" Depth {depth+1}: {score:.4f}") + + print(f"Final score: {final_score:.4f}") + + return metrics, final_score + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/opto/trainer/algorithms/beamsearch_algorithm.py b/opto/trainer/algorithms/beamsearch_algorithm.py new file mode 100644 index 00000000..2d63f5a1 --- /dev/null +++ b/opto/trainer/algorithms/beamsearch_algorithm.py @@ -0,0 +1,781 @@ +import numpy as np +import copy +from typing import Union, List, Tuple, Dict, Any, Optional +from opto.trainer.utils import async_run +from opto.optimizers.utils import print_color +from opto.trainer.algorithms.basic_algorithm import MinibatchAlgorithm, evaluate, batchify + + +class BeamsearchAlgorithm(MinibatchAlgorithm): + """ + BeamsearchAlgorithm performs beam search over parameter space. + """ """ + It starts with an initial prompt, generates multiple candidates, + selects top beam_width candidates, and repeats this process up to max_depth. + At each step, it evaluates candidates on a validation set to select the best ones. + Finally output the best candidate based on validation scores. + """ + + def train(self, + guide, + train_dataset, + *, + validate_dataset=None, # dataset for selecting the best candidates + validate_guide=None, # guide for validation + validation_dataset_size=5, # size of validation minibatch for each evaluation + beam_width=3, # number of candidates to keep at each beam step + num_proposals=4, # number of proposals to generate per beam + max_depth=2, # maximum depth of beam search + num_epochs=1, + batch_size=1, + test_dataset=None, + log_frequency=None, + save_frequency=None, + save_path="checkpoints/agent.pkl", + min_score=None, + num_threads=10, + test_frequency=4, # How often to evaluate on test set + **kwargs + ): + """ + Performs beam search to find optimal parameters. + + Args: + beam_width: Number of candidates to keep at each level of the beam search + num_proposals: Number of proposals to generate per beam candidate + max_depth: Maximum depth of the beam search + validate_dataset: Dataset used to select the best candidates + validate_guide: Guide used for validation + validation_dataset_size: Size of validation minibatch for each evaluation (if None, uses all) + test_frequency: How often to evaluate on test set (every N steps) + Other parameters are the same as MinibatchAlgorithm.train() + """ + self.total_samples = 0 + + print_color(f"Running BeamsearchAlgorithm with beam_width={beam_width}, max_depth={max_depth}", 'blue') + + # Use train dataset for validation if not specified + validate_dataset = validate_dataset or train_dataset + validate_guide = validate_guide or guide + self.min_score = min_score + + # Default validation dataset size + if validation_dataset_size is None: + # Use a reasonable default - e.g., 10 samples or all if dataset is smaller + validation_dataset_size = min(10, len(validate_dataset['inputs'])) + + print_color(f"Using validation_dataset_size={validation_dataset_size} for intermediate evaluations", 'blue') + + # Store original parameters to restore after each exploration + original_params = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} + + # Dictionary to track metrics during beam search + metrics = { + 'best_validation_scores': [], # Best validation score at each depth + 'depth_scores': [], # All scores at each depth + 'test_scores': [], # Test scores at periodic intervals + 'test_depths': [] # Depths at which test scores were recorded + } + + # Evaluate initial parameters on test set + if test_dataset is not None: + print_color("\n===== Evaluating Initial Parameters =====", 'blue') + initial_test_scores = evaluate( + self.agent, + guide, + test_dataset['inputs'], + test_dataset['infos'], + min_score=min_score, + num_threads=num_threads, + description="Evaluating initial parameters on test set" + ) + initial_test_score = np.mean(initial_test_scores) if all([s is not None for s in initial_test_scores]) else -np.inf + print_color(f"Initial test score: {initial_test_score:.4f}", 'yellow') + + # Add initial score to metrics for logging + metrics['test_scores'].append(initial_test_score) + metrics['test_depths'].append(1) # Represent initial score at depth 0 + + # Start with a single beam (the original parameters) + beams = [original_params] + + # Run beam search for max_depth iterations + for depth in range(max_depth): + print_color(f"\n===== Beam Search Depth {depth+1}/{max_depth} with {len(beams)} beams =====", 'blue') + + # Sample a validation minibatch for this depth + validation_xs, validation_infos = self._sample_minibatch( + validate_dataset, + validation_dataset_size + ) + + # Create a validation mini-dataset for this depth + validation_mini_dataset = { + 'inputs': validation_xs, + 'infos': validation_infos + } + + print_color(f"Sampled validation minibatch of size {len(validation_xs)} for depth {depth+1}", 'cyan') + + # Collect all expanded candidates + all_candidates = [] + + # Process each beam in the current set + for beam_idx, beam_params in enumerate(beams): + print_color(f"Processing beam {beam_idx+1}/{len(beams)}", 'yellow') + + # Expand: Generate multiple proposals from this beam (without evaluation) + beam_candidates = self.expand( + beam_params=beam_params, + beam_idx=beam_idx, + guide=guide, + train_dataset=train_dataset, + batch_size=batch_size, + num_proposals=num_proposals, + num_threads=num_threads + ) + + # Add all candidates to the pool for selection + all_candidates.extend(beam_candidates) + self.total_samples += batch_size + # Select: Evaluate all candidates and choose the top beam_width + beams, scores = self.select( + candidates=all_candidates, + validate_guide=validate_guide, + validation_mini_dataset=validation_mini_dataset, + beam_width=beam_width, + num_threads=num_threads, + min_score=min_score, + return_scores=True # Modified to return scores as well + ) + self.total_samples += validation_dataset_size*len(all_candidates) + # Track validation scores for this depth + if len(scores) > 0: + best_score = max(scores) + best_idx = scores.index(best_score) + best_params = beams[best_idx] + metrics['best_validation_scores'].append(best_score) + metrics['depth_scores'].append(scores) + + print_color(f"Depth {depth+1} - Best validation score: {best_score:.4f}", 'green') + + # Evaluate on test set every test_frequency steps + if test_dataset is not None and ((depth + 1) % test_frequency == 0): + # Update agent with best parameters from this depth + self.optimizer.update(best_params) + # Print best parameters + print_color("\nBest parameters at depth {}:".format(depth + 1), 'cyan') + for key, value in best_params.items(): + # Try to get a clean string name from the key, which might be a parameter object + if hasattr(key, 'name'): + # Extract string name from parameter object + param_name = key.name + else: + # If it's already a string or doesn't have a name attribute, use it directly + param_name = str(key) + print_color(f"{param_name}: {value}", 'cyan') + print_color("", 'cyan') # Empty line for readability + # Evaluate on test set + test_scores = evaluate( + self.agent, + guide, + test_dataset['inputs'], + test_dataset['infos'], + min_score=min_score, + num_threads=num_threads, + description=f"Evaluating best parameters at depth {depth+1} on test set" + ) + test_score = np.mean(test_scores) if all([s is not None for s in test_scores]) else -np.inf + + # Record the test score + metrics['test_scores'].append(test_score) + metrics['test_depths'].append(depth + 1) + + print_color(f"Depth {depth+1} - Test score: {test_score:.4f}", 'magenta') + + # Final selection - choose the best beam using FULL validation set + print_color("\n===== Final Selection Using Full Validation Set =====", 'blue') + + # Use select method with the full validation dataset + full_validation_dataset = { + 'inputs': validate_dataset['inputs'], + 'infos': validate_dataset['infos'] + } + + # Select the single best beam from the final candidates + best_beams, final_val_scores = self.select( + candidates=beams, + validate_guide=validate_guide, + validation_mini_dataset=full_validation_dataset, + beam_width=1, # Only select the best one + num_threads=num_threads, + min_score=min_score, + return_scores=True # Return scores too + ) + + # Get the best parameters + best_params = best_beams[0] + final_validation_score = final_val_scores[0] if final_val_scores else -np.inf + + # Apply the best parameters + self.optimizer.update(best_params) + + # Print out the final proposal candidate parameters + print_color("\n===== Final Proposal Candidate Parameters =====", 'magenta') + for param in self.agent.parameters(): + # Use a try-except block to handle parameter lookup + try: + # Check if parameter object is directly available as a key + if param in best_params: + param_value = best_params[param] + # Try to find by name if available + elif hasattr(param, 'name') and param.name in best_params: + param_value = best_params[param.name] + else: + param_value = "Parameter not found in best_params" + + # Get the parameter name directly + param_name = param.name if hasattr(param, 'name') else str(param) + print_color(f"{param_name}: {param_value}", 'blue') + except Exception as e: + print_color(f"Error accessing parameter {getattr(param, 'name', str(param))}: {e}", 'red') + continue + + # Evaluate on test set for reporting (if provided) + if test_dataset is not None: + final_test_scores = evaluate( + self.agent, + guide, + test_dataset['inputs'], + test_dataset['infos'], + min_score=min_score, + num_threads=num_threads, + description="Evaluating best beam on test set" + ) + final_test_score = np.mean(final_test_scores) if all([s is not None for s in final_test_scores]) else -np.inf + else: + final_test_score = None + + if final_test_score is not None: + print_color(f"BEST BEAM - Test score: {final_test_score:.4f}", 'green') + + # Save the best model + if save_frequency is not None and save_frequency > 0: + self.save_agent(save_path, 0) + + # Print periodic test scores summary if available + if metrics['test_scores']: + print_color("\n===== Periodic Test Scores Summary =====", 'blue') + for depth, score in zip(metrics['test_depths'], metrics['test_scores']): + print_color(f"Depth {depth}: Test score = {score:.4f}", 'cyan') + + # For API consistency with other algorithms + return metrics, final_test_score if final_test_score is not None else 0.0 + + def _sample_minibatch(self, dataset, batch_size): + """Sample a minibatch from the dataset.""" + indices = np.random.choice(len(dataset['inputs']), min(batch_size, len(dataset['inputs'])), replace=False) + xs = [dataset['inputs'][i] for i in indices] + infos = [dataset['infos'][i] for i in indices] + return xs, infos + + def expand(self, + beam_params: Dict, + beam_idx: int, + guide, + train_dataset, + batch_size: int, + num_proposals: int, + num_threads: int = None) -> List[Dict]: + """ + Expands a single candidate into multiple proposals without evaluation. + + Args: + beam_params: Parameters of the current beam + beam_idx: Index of the current beam + guide: Guide for generating feedback + train_dataset: Training dataset + batch_size: Training batch size + num_proposals: Number of proposals to generate + num_threads: Number of threads to use + + Returns: + List of parameter dictionaries for each candidate + """ + # Restore parameters for this beam + self.optimizer.update(beam_params) + + # Run forward pass on minibatch to get outputs and feedbacks + xs_batch, infos_batch = self._sample_minibatch(train_dataset, batch_size) + + # Forward the agent on the minibatch + use_asyncio = self._use_asyncio(num_threads) + if use_asyncio: + outputs = async_run([self.forward]*len(xs_batch), + [(self.agent, x, guide, info) for x, info in zip(xs_batch, infos_batch)], + max_workers=num_threads, + description=f"Forward pass (beam {beam_idx+1}, batch size: {len(xs_batch)})") + else: + outputs = [self.forward(self.agent, x, guide, info) for x, info in zip(xs_batch, infos_batch)] + + # Prepare for optimizer backward and step + scores, targets, feedbacks = [], [], [] + for target, score, feedback in outputs: + scores.append(score) + targets.append(target) + feedbacks.append(feedback) + target = batchify(*targets) + feedback = batchify(*feedbacks).data + + # Backward pass to compute gradients + self.optimizer.zero_feedback() + self.optimizer.backward(target, feedback) + + # Generate multiple proposals + step_kwargs = dict(bypassing=True, verbose='output') + candidates = [] + + # Generate num_proposals candidates + if use_asyncio: + update_dicts = async_run([self.optimizer.step]*num_proposals, + kwargs_list=[step_kwargs] * num_proposals, + max_workers=num_threads, + description=f"Generating {num_proposals} proposals for beam {beam_idx+1}") + else: + update_dicts = [self.optimizer.step(**step_kwargs) for _ in range(num_proposals)] + + # Collect all valid proposals + for update_dict in update_dicts: + if len(update_dict) > 0: + # Make sure update_dict contains all parameters from beam_params + # Add any missing parameters from beam_params to update_dict + for param_key, param_value in beam_params.items(): + if param_key not in update_dict: + update_dict[param_key] = param_value + candidates.append(update_dict) + + # Also include the original beam parameters as a candidate + candidates.append(beam_params) + + return candidates + + def select(self, + candidates: List[Dict], + validate_guide, + validation_mini_dataset, + beam_width: int, + num_threads: int = None, + min_score: float = None, + return_scores: bool = False) -> Union[List[Dict], Tuple[List[Dict], List[float]]]: + """ + Evaluates all candidates and selects the top beam_width candidates based on validation scores. + + Args: + candidates: List of parameter dictionaries for each candidate + validate_guide: Guide for validation + validation_mini_dataset: Validation dataset for evaluation + beam_width: Maximum number of candidates to select + num_threads: Number of threads to use + min_score: Minimum score when errors occur + return_scores: Whether to return scores along with parameters + + Returns: + If return_scores is False: List of selected candidates' parameters + If return_scores is True: Tuple of (list of parameters, list of scores) + """ + # Store current parameters to restore later + current_params = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} + + # List to store (score, params) pairs + scored_candidates = [] + + # Evaluate each candidate + for candidate_idx, candidate_params in enumerate(candidates): + self.optimizer.update(candidate_params) + + # Evaluate on validation minibatch using evaluate function + validation_scores = evaluate( + self.agent, + validate_guide, + validation_mini_dataset['inputs'], + validation_mini_dataset['infos'], + min_score=min_score, + num_threads=num_threads, + description=f"Validating candidate {candidate_idx+1}/{len(candidates)}" + ) + + validation_score = np.mean(validation_scores) if all([s is not None for s in validation_scores]) else -np.inf + scored_candidates.append((validation_score, candidate_params)) + + print_color(f"Candidate {candidate_idx+1}: Validation score: {validation_score:.4f}", 'cyan') + + # Restore original parameters + self.optimizer.update(current_params) + + # Extract scores for logging + scores = [score for score, _ in scored_candidates] + + # If the number of candidates is less than or equal to beam_width, keep all of them + if len(scored_candidates) <= beam_width: + print_color(f"Keeping all {len(scored_candidates)} candidates as num_candidates <= beam_width. Scores: {[f'{s:.4f}' for s in scores]}", 'green') + selected_params = [params for _, params in scored_candidates] + if return_scores: + return selected_params, scores + return selected_params + + # Sort candidates by score (descending) + sorted_candidates = sorted(scored_candidates, key=lambda x: x[0], reverse=True) + + # Select top beam_width candidates + selected_candidates = sorted_candidates[:beam_width] + selected_scores = [score for score, _ in selected_candidates] + selected_params = [params for _, params in selected_candidates] + + print_color(f"Selected top {beam_width} beams with scores: {[f'{s:.4f}' for s in selected_scores]}", 'green') + if return_scores: + return selected_params, selected_scores + return selected_params + + + +class BeamsearchHistoryAlgorithm(BeamsearchAlgorithm): + """ + BeamsearchHistoryAlgorithm enhances BeamsearchAlgorithm by incorporating + historical parameter-score information into the proposal generation process. + + It maintains a log of previously selected parameter sets and their validation scores. + This history is then formatted and provided as additional context (feedback) + during the `expand` phase, aiming to guide the optimizer towards generating + more informed proposals based on past performance. + """ + + def train(self, + guide, + train_dataset, + *, + validate_dataset=None, + validate_guide=None, + validation_dataset_size=5, + beam_width=3, + batch_size=1, + num_proposals=1, + max_depth=2, + num_threads=10, + max_history_size=10, # Max number of history entries to keep + test_frequency=5, # Match the context file value + # Add other args from parent if needed, or rely on **kwargs + **kwargs + ): + """ + Performs beam search enhanced with parameter history. + + Args: + max_history_size: Maximum number of (parameter, score) pairs to store + in the history log. Defaults to 20. + top_k: Size of the top-k candidates buffer that persists across depths. + Default is 1, which keeps only the best candidate. + Other args are the same as BeamsearchAlgorithm.train() + """ + self.total_samples = 0 + self.min_score = kwargs.get('min_score', 0) + print_color(f"Running BeamsearchHistoryAlgorithm with beam_width={beam_width}, max_depth={max_depth}, max_history_size={max_history_size}", 'blue') + + # Initialize history log + self.parameter_history: List[Tuple[Dict, float]] = [] + self.max_history_size = max_history_size + + # Use train dataset for validation if not specified + validate_dataset = validate_dataset or train_dataset + validate_guide = validate_guide or guide + + + # Default validation dataset size + if validation_dataset_size is None: + validation_dataset_size = min(10, len(validate_dataset['inputs'])) + print_color(f"Using validation_dataset_size={validation_dataset_size} for intermediate evaluations", 'blue') + + # Store original parameters + original_params = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} + + # Dictionary to track metrics + metrics = { + 'best_validation_scores': [], + 'depth_scores': [], + 'test_scores': [], + 'test_depths': [] + } + + test_dataset = kwargs.get('test_dataset', None) + + # Evaluate initial parameters on test set + if test_dataset is not None: + print_color("\n===== Evaluating Initial Parameters =====", 'blue') + initial_test_scores = evaluate( + self.agent, guide, test_dataset['inputs'], test_dataset['infos'], + min_score=self.min_score, num_threads=num_threads, + description="Evaluating initial parameters on test set" + ) + initial_test_score = np.mean(initial_test_scores) if all([s is not None for s in initial_test_scores]) else -np.inf + print_color(f"Initial test score: {initial_test_score:.4f}", 'yellow') + metrics['test_scores'].append(initial_test_score) + metrics['test_depths'].append(1) # Start depth at 1 for consistency + + # Start with a single beam + beams = [original_params] + + # >>> Main Beam Search Loop <<< + for depth in range(max_depth): + print_color(f"\n===== Beam Search Depth {depth+1}/{max_depth} with {len(beams)} beams =====", 'blue') + + # Sample validation minibatch + validation_xs, validation_infos = self._sample_minibatch(validate_dataset, validation_dataset_size) + validation_mini_dataset = {'inputs': validation_xs, 'infos': validation_infos} + print_color(f"Sampled validation minibatch of size {len(validation_xs)} for depth {depth+1}", 'cyan') + + # Expand all current beams + all_candidates = [] + for beam_idx, beam_params in enumerate(beams): + print_color(f"Processing beam {beam_idx+1}/{len(beams)}", 'yellow') + beam_candidates = self.expand( # Calls the overridden expand method + beam_params=beam_params, beam_idx=beam_idx, guide=guide, + train_dataset=train_dataset, batch_size=batch_size, + num_proposals=num_proposals, num_threads=num_threads + ) + all_candidates.extend(beam_candidates) + self.total_samples += batch_size + # Select top candidates + beams, scores = self.select( + candidates=all_candidates, validate_guide=validate_guide, + validation_mini_dataset=validation_mini_dataset, beam_width=beam_width, + num_threads=num_threads, min_score=self.min_score, return_scores=True + ) + self.total_samples += validation_dataset_size*len(all_candidates) + # --- Populate History Log --- + if scores: + best_score_this_depth = -np.inf + for params, score in zip(beams, scores): + # params = copy.deepcopy(params) + # for name, value in params.items(): + # print(f"{name}: {value}") + if score > -np.inf: # Only log valid scores + # Store deep copies to prevent modification + self.parameter_history.append((params, score)) + best_score_this_depth = max(best_score_this_depth, score) + + # Keep history log bounded + if len(self.parameter_history) > self.max_history_size: + # Keep the ones with most recent + self.parameter_history = self.parameter_history[-self.max_history_size:] + # --- History Log Populated --- + + # Track metrics + if best_score_this_depth > -np.inf: + metrics['best_validation_scores'].append(best_score_this_depth) + metrics['depth_scores'].append(scores) + print_color(f"Depth {depth+1} - Best validation score: {best_score_this_depth:.4f}", 'green') + + best_idx = scores.index(best_score_this_depth) # Find index of best score + best_params = beams[best_idx] # Get corresponding params + + # Evaluate on test set periodically + if test_dataset is not None and ((depth + 1) % test_frequency == 0): + self.optimizer.update(best_params) # Use best params from this depth + print_color("\nBest parameters at depth {}:".format(depth + 1), 'cyan') + + for param in self.agent.parameters(): + # Use a try-except block to handle parameter lookup + try: + # Check if parameter object is directly available as a key + if param in best_params: + param_value = best_params[param] + # Try to find by name if available + elif hasattr(param, 'name') and param.name in best_params: + param_value = best_params[param.name] + else: + param_value = "Parameter not found in best_params" + + # Get the parameter name directly + param_name = param.name if hasattr(param, 'name') else str(param) + print_color(f"{param_name}: {param_value}", 'blue') + except Exception as e: + print_color(f"Error accessing parameter {getattr(param, 'name', str(param))}: {e}", 'red') + continue + test_scores_eval = evaluate( + self.agent, guide, test_dataset['inputs'], test_dataset['infos'], + min_score=self.min_score, num_threads=num_threads, + description=f"Evaluating best parameters at depth {depth+1} on test set" + ) + test_score = np.mean(test_scores_eval) if all([s is not None for s in test_scores_eval]) else -np.inf + metrics['test_scores'].append(test_score) + metrics['test_depths'].append(depth + 1) + print_color(f"Depth {depth+1} - Test score: {test_score:.4f}", 'magenta') + + # >>> End Main Loop <<< + + # Final selection using full validation set + print_color("\n===== Final Selection Using Full Validation Set =====", 'blue') + full_validation_dataset = {'inputs': validate_dataset['inputs'], 'infos': validate_dataset['infos']} + best_beams, final_val_scores = self.select( + candidates=beams, validate_guide=validate_guide, + validation_mini_dataset=full_validation_dataset, beam_width=1, # Select only the best + num_threads=num_threads, min_score=self.min_score, return_scores=True + ) + + final_validation_score = final_val_scores[0] if final_val_scores else -np.inf + best_params = best_beams[0] if best_beams else original_params # Fallback to original if empty + + # Apply best parameters + self.optimizer.update(best_params) + + # Print final parameters + print_color("\n===== Final Proposal Candidate Parameters =====", 'magenta') + + # Final evaluation on test set + final_test_score = None + if test_dataset is not None: + final_test_scores_eval = evaluate( + self.agent, guide, test_dataset['inputs'], test_dataset['infos'], + min_score=self.min_score, num_threads=num_threads, + description="Evaluating best beam on test set" + ) + final_test_score = np.mean(final_test_scores_eval) if all([s is not None for s in final_test_scores_eval]) else -np.inf + print_color(f"BEST BEAM - Test score: {final_test_score:.4f}", 'green') + + # Save agent if configured + if kwargs.get('save_frequency', None) is not None and kwargs['save_frequency'] > 0: + self.save_agent(kwargs.get('save_path', "checkpoints/agent.pkl"), 0) + + # Print test score summary + if metrics['test_scores']: + print_color("\n===== Periodic Test Scores Summary =====", 'blue') + for d, s in zip(metrics['test_depths'], metrics['test_scores']): + print_color(f"Depth {d}: Test score = {s:.4f}", 'cyan') + + return metrics, final_test_score if final_test_score is not None else -np.inf + + def expand(self, + beam_params: Dict, + beam_idx: int, + guide, + train_dataset, + batch_size: int, + num_proposals: int, + num_threads: int = None) -> List[Dict]: + """ + Expands a single candidate into multiple proposals, incorporating history. + + Overrides the parent expand method to augment the feedback provided to the + optimizer with a summary of historical parameter-score pairs. + + Args: Same as parent expand method. + + Returns: Same as parent expand method. + """ + # Restore parameters for this beam + self.optimizer.update(beam_params) + + # Run forward pass on minibatch to get outputs and feedbacks + xs_batch, infos_batch = self._sample_minibatch(train_dataset, batch_size) + + use_asyncio = self._use_asyncio(num_threads) + description=f"Forward pass (beam {beam_idx+1}, batch size: {len(xs_batch)})" + if use_asyncio: + outputs = async_run([self.forward]*len(xs_batch), + [(self.agent, x, guide, info) for x, info in zip(xs_batch, infos_batch)], + max_workers=num_threads, description=description) + else: + outputs = [self.forward(self.agent, x, guide, info) for x, info in zip(xs_batch, infos_batch)] + + # Prepare original feedback + scores, targets, feedbacks = [], [], [] + for target, score, feedback_item in outputs: + scores.append(score) + targets.append(target) + feedbacks.append(feedback_item) + target = batchify(*targets) + original_feedback = batchify(*feedbacks).data # Assuming .data gives the relevant part + + # --- History Injection --- + history_prompt = "\n--- History Context ---\n" + history_prompt += "Consider the following previously selected parameter sets and their validation scores when generating proposals:\n" + if not self.parameter_history: + history_prompt += "(No history available yet)\n" + else: + # Format history (e.g., last N entries) + # Sorting by score might be useful: sorted_history = sorted(self.parameter_history, key=lambda item: item[1], reverse=True) + display_history = self.parameter_history # Or sorted_history[:self.max_history_size] + for i, (hist_params, hist_score) in enumerate(display_history): + # Format parameters nicely + param_parts = [] + for k, v in hist_params.items(): + key_name = getattr(k, 'name', str(k)) # Get name attr if Parameter object + if isinstance(v, (float, np.floating)): + param_parts.append(f"{key_name}: {v:.4f}") + elif isinstance(v, (np.ndarray, list)) and len(v) > 5: # Truncate long lists/arrays + param_parts.append(f"{key_name}: [{', '.join(map(str, v[:2]))}...{str(v[-1])}]") + else: + param_parts.append(f"{key_name}: {v}") + param_str = ", ".join(param_parts) + history_prompt += f" Attempt {i+1} (Score: {hist_score:.4f}): {{{param_str}}}\n" + + # Combine history with original feedback + # This assumes the optimizer can handle string feedback or a dict. + # Adjust based on how your specific optimizer/trace uses feedback. + augmented_feedback: Union[str, Dict] + if isinstance(original_feedback, str): + augmented_feedback = f"--- Current Feedback ---\n{original_feedback}\n{history_prompt}" + elif isinstance(original_feedback, dict): + # Add history as a separate key, preserving original structure + augmented_feedback = original_feedback.copy() + augmented_feedback['history_context'] = history_prompt + # Ensure original feedback text (if any) is still prominent + if 'feedback' in augmented_feedback: + augmented_feedback['feedback'] = f"{augmented_feedback['feedback']}\n{history_prompt}" + elif 'prompt' in augmented_feedback: # Adapt if feedback is under 'prompt' key + augmented_feedback['prompt'] = f"{augmented_feedback['prompt']}\n{history_prompt}" + else: # Fallback if structure unknown + augmented_feedback['raw_feedback'] = original_feedback + + else: + # Attempt to stringify other types, may need refinement + try: + augmented_feedback = f"--- Current Feedback ---\n{str(original_feedback)}\n{history_prompt}" + print_color(f"Warning: Combined non-string/dict feedback with history prompt.", "yellow") + except Exception as e: + print_color(f"Error combining feedback with history: {e}. Using original feedback.", "red") + augmented_feedback = original_feedback # Fallback + + # --- End History Injection --- + + + # Backward pass using the augmented feedback + self.optimizer.zero_feedback() + self.optimizer.backward(target, augmented_feedback) # Pass augmented feedback here + + # Generate multiple proposals using optimizer.step + step_kwargs = dict(bypassing=True, verbose='output') + candidates = [] + description_step=f"Generating {num_proposals} proposals for beam {beam_idx+1} (with history)" + if use_asyncio: + update_dicts = async_run([self.optimizer.step]*num_proposals, + kwargs_list=[step_kwargs] * num_proposals, + max_workers=num_threads, + description=description_step) + else: + update_dicts = [self.optimizer.step(**step_kwargs) for _ in range(num_proposals)] + + # Collect all valid proposals + for update_dict in update_dicts: + if len(update_dict) > 0: + # Make sure update_dict contains all parameters from beam_params + # Add any missing parameters from beam_params to update_dict + for param_key, param_value in beam_params.items(): + if param_key not in update_dict: + update_dict[param_key] = param_value + candidates.append(update_dict) + + # Also include the original beam parameters as a candidate + candidates.append(beam_params) + + return candidates + From 5c700d7ad1699b4f4d026ece40b610fbbaad4912 Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Tue, 3 Jun 2025 18:32:24 -0500 Subject: [PATCH 002/314] update parameters --- examples/example_usage_trainer.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/example_usage_trainer.py b/examples/example_usage_trainer.py index 78ff2793..dc33e76a 100644 --- a/examples/example_usage_trainer.py +++ b/examples/example_usage_trainer.py @@ -194,7 +194,7 @@ def main(): help='Number of training samples') parser.add_argument('--num_validate_samples', type=int, default=20, help='Number of validation samples') - parser.add_argument('--num_test_samples', type=int, default=1, + parser.add_argument('--num_test_samples', type=int, default=20, help='Number of test samples') # Model parameters @@ -210,7 +210,7 @@ def main(): help='Number of training epochs') parser.add_argument('--batch_size', type=int, default=2, help='Training batch size') - parser.add_argument('--num_threads', type=int, default=20, + parser.add_argument('--num_threads', type=int, default=50, help='Number of threads for parallel processing') parser.add_argument('--eval_frequency', type=int, default=2, help='How often to run evaluation') @@ -220,11 +220,11 @@ def main(): help='Random seed for reproducibility') # Algorithm-specific parameters - parser.add_argument('--beam_width', type=int, default=2, + parser.add_argument('--beam_width', type=int, default=3, help='Beam width for beam search algorithms') parser.add_argument('--num_proposals', type=int, default=2, help='Number of proposals for beam search algorithms') - parser.add_argument('--max_depth', type=int, default=5, + parser.add_argument('--max_depth', type=int, default=20, help='Maximum depth for beam search algorithms') parser.add_argument('--validation_dataset_size', type=int, default=20, help='Size of validation dataset for beam search') From 2630f9d43e2375e23f52030fd8ef651d7d9b2350 Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Tue, 3 Jun 2025 20:12:10 -0500 Subject: [PATCH 003/314] upload three versions of UCB search algorithms with a buffer --- examples/example_usage_trainer.py | 77 ++- opto/trainer/algorithms/UCBsearch.py | 999 +++++++++++++++++++++++++++ 2 files changed, 1071 insertions(+), 5 deletions(-) create mode 100644 opto/trainer/algorithms/UCBsearch.py diff --git a/examples/example_usage_trainer.py b/examples/example_usage_trainer.py index dc33e76a..7f4f2f9d 100644 --- a/examples/example_usage_trainer.py +++ b/examples/example_usage_trainer.py @@ -15,12 +15,13 @@ from opto.trace.modules import Module from opto.trainer.algorithms.basic_algorithm import MinibatchAlgorithm, BasicSearchAlgorithm from opto.trainer.algorithms.beamsearch_algorithm import BeamsearchAlgorithm, BeamsearchHistoryAlgorithm +from opto.trainer.algorithms.UCBsearch import UCBSearchAlgorithm, HybridUCB_LLM, UCBSearchFunctionApproximationAlgorithm from opto.trainer.guide import AutoGuide from opto.trainer.utils import DefaultLogger from opto.utils.llm import LLM, LiteLLM # Set default model -os.environ["TRACE_LITELLM_MODEL"] = "vertex_ai/gemini-2.0-flash" +# os.environ["TRACE_LITELLM_MODEL"] = "vertex_ai/gemini-2.0-flash" @trace.model class Learner(Module): @@ -183,8 +184,8 @@ def main(): parser = argparse.ArgumentParser(description='Train agent using various algorithms') # Algorithm parameters - parser.add_argument('--algorithm_type', type=str, default='beamsearchhistory', - choices=['minibatch', 'basicsearch', 'beamsearch', 'beamsearchhistory'], + parser.add_argument('--algorithm_type', type=str, default='UCBSearchFunctionApproximationAlgorithm', + choices=['minibatch', 'basicsearch', 'beamsearch', 'beamsearchhistory', 'UCBsearch', 'HybridUCB_LLM', 'UCBSearchFunctionApproximationAlgorithm'], help='Type of algorithm to use') # Dataset parameters @@ -197,7 +198,7 @@ def main(): parser.add_argument('--num_test_samples', type=int, default=20, help='Number of test samples') - # Model parameters + # LLM Model parameters parser.add_argument('--trace_model', type=str, default='vertex_ai/gemini-2.0-flash', help='Model to use for trace operations') parser.add_argument('--student_model', type=str, default='vertex_ai/gemini-2.0-flash', @@ -210,7 +211,7 @@ def main(): help='Number of training epochs') parser.add_argument('--batch_size', type=int, default=2, help='Training batch size') - parser.add_argument('--num_threads', type=int, default=50, + parser.add_argument('--num_threads', type=int, default=10, help='Number of threads for parallel processing') parser.add_argument('--eval_frequency', type=int, default=2, help='How often to run evaluation') @@ -233,6 +234,20 @@ def main(): parser.add_argument('--num_basicsearch_proposals', type=int, default=2, help='Number of proposals for basic search algorithm') + # UCB algorithm-specific parameters + parser.add_argument('--max_buffer_size', type=int, default=10, + help='Maximum buffer size for UCB algorithms') + parser.add_argument('--ucb_exploration_factor', type=float, default=1.0, + help='UCB exploration factor') + parser.add_argument('--alpha', type=float, default=0.3, + help='Alpha parameter for HybridUCB_LLM (probability of UCB vs LLM path)') + parser.add_argument('--num_search_iterations', type=int, default=100, + help='Number of search iterations for UCB algorithms') + parser.add_argument('--train_batch_size_ucb', type=int, default=2, + help='Training batch size for UCB algorithms') + parser.add_argument('--evaluation_batch_size', type=int, default=20, + help='Evaluation batch size for UCB algorithms') + args = parser.parse_args() # Set environment variables @@ -306,6 +321,36 @@ def main(): logger=logger, num_threads=args.num_threads ) + elif args.algorithm_type == 'UCBsearch': + algorithm = UCBSearchAlgorithm( + agent=agent, + optimizer=optimizer, + logger=logger, + num_threads=args.num_threads, + max_buffer_size=args.max_buffer_size, + ucb_exploration_factor=args.ucb_exploration_factor + ) + elif args.algorithm_type == 'HybridUCB_LLM': + algorithm = HybridUCB_LLM( + agent=agent, + optimizer=optimizer, + logger=logger, + num_threads=args.num_threads, + max_buffer_size=args.max_buffer_size, + ucb_exploration_factor=args.ucb_exploration_factor, + alpha=args.alpha, + llm_model=args.trace_model + ) + elif args.algorithm_type == 'UCBSearchFunctionApproximationAlgorithm': + algorithm = UCBSearchFunctionApproximationAlgorithm( + agent=agent, + optimizer=optimizer, + logger=logger, + num_threads=args.num_threads, + max_buffer_size=args.max_buffer_size, + ucb_exploration_factor=args.ucb_exploration_factor, + llm_model=args.trace_model + ) else: raise ValueError(f"Unknown algorithm type: {args.algorithm_type}") @@ -338,6 +383,13 @@ def main(): elif args.algorithm_type == 'basicsearch': train_params["num_proposals"] = args.num_basicsearch_proposals + elif args.algorithm_type in ['UCBsearch', 'HybridUCB_LLM', 'UCBSearchFunctionApproximationAlgorithm']: + train_params.update({ + "num_search_iterations": args.num_search_iterations, + "train_batch_size": args.train_batch_size_ucb, + "evaluation_batch_size": args.evaluation_batch_size + }) + # Start training print(f"Training with {args.algorithm_type} algorithm...") start_time = time.time() @@ -351,6 +403,21 @@ def main(): for depth, score in enumerate(metrics['best_validation_scores']): print(f" Depth {depth+1}: {score:.4f}") + elif args.algorithm_type in ['UCBsearch', 'HybridUCB_LLM', 'UCBSearchFunctionApproximationAlgorithm']: + print("\nUCB Algorithm Metrics:") + if 'best_candidate_scores' in metrics and metrics['best_candidate_scores']: + print(f" Best candidate scores over iterations: {len(metrics['best_candidate_scores'])} recorded") + print(f" Final best candidate score: {metrics['best_candidate_scores'][-1]:.4f}") + if 'buffer_avg_score' in metrics and metrics['buffer_avg_score']: + print(f" Final buffer average score: {metrics['buffer_avg_score'][-1]:.4f}") + if args.algorithm_type == 'HybridUCB_LLM': + if 'llm_generation_failures' in metrics: + print(f" LLM generation failures: {metrics['llm_generation_failures']}") + if 'generation_path' in metrics: + ucb_count = metrics['generation_path'].count('ucb') + llm_count = metrics['generation_path'].count('llm') + print(f" Generation methods used - UCB: {ucb_count}, LLM: {llm_count}") + print(f"Final score: {final_score:.4f}") return metrics, final_score diff --git a/opto/trainer/algorithms/UCBsearch.py b/opto/trainer/algorithms/UCBsearch.py new file mode 100644 index 00000000..3e08aef6 --- /dev/null +++ b/opto/trainer/algorithms/UCBsearch.py @@ -0,0 +1,999 @@ +import numpy as np +import copy +import time +import math +import json # For LLM output parsing +import re # For smart quote replacement +from collections import deque +from typing import Union, List, Tuple, Dict, Any, Optional +import random # Added for alpha probability + +from opto import trace +from opto.trainer.utils import async_run # Assuming print_color is in utils +from opto.optimizers.utils import print_color +from opto.trainer.algorithms.basic_algorithm import MinibatchAlgorithm, evaluate, batchify # evaluate and batchify might be useful +from opto.utils.llm import LiteLLM # For the selector LLM + +from opto.trace.nodes import ParameterNode +import warnings +from black import format_str, FileMode + + +def smart_quote_replacement(text: str) -> str: + """ + Intelligently replace single quotes with double quotes for JSON parsing. + Handles the specific case where we have mixed quotes like: + {'key': "value with 'nested' quotes"} + """ + # For the specific pattern we're seeing, let's handle it step by step: + + # Step 1: Replace single quotes around keys + # Pattern: 'key': -> "key": + text = re.sub(r"'([^']*?)'(\s*:)", r'"\1"\2', text) + + # Step 2: For values that start with double quotes and contain single quotes, + # we need to escape the internal single quotes or convert them properly + + # Let's try a more direct approach for the problematic case: + # Find patterns like: "text with 'word' more text" + # We need to escape the internal single quotes + def escape_internal_quotes(match): + content = match.group(1) + # Replace single quotes inside with escaped single quotes + # Actually, for JSON we can leave single quotes as-is inside double quotes + return f'"{content}"' + + # Replace the pattern: : "content with 'quotes'" -> : "content with 'quotes'" + # (This should already be valid JSON) + + # The main issue is with the outer structure, let's fix that: + # If the string starts/ends with single quotes around the whole thing + text = text.strip() + if text.startswith("{'") and text.endswith("'}"): + # Replace the outer single quotes but preserve the content + # This is the pattern: {'str0': "content", 'str1': "more content"} + text = '{"' + text[2:-2] + '"}' + + return text + + +class UCBSearchAlgorithm(MinibatchAlgorithm): + """ + UCB Search Algorithm. + + Keeps a buffer of candidates with their statistics (score sum, evaluation count). + In each iteration: + 1. Picks a candidate 'a' from the buffer with the highest UCB score. + 2. Updates the optimizer with 'a's parameters. + 3. Draws a minibatch from the training set, performs a forward/backward pass, and calls optimizer.step() to get a new candidate 'a''. + 4. Evaluates 'a'' on a validation set minibatch. + 5. Updates statistics of 'a' (based on the training minibatch). + 6. Adds 'a'' (with its validation stats) to the buffer. + 7. If the buffer is full, evicts the candidate with the lowest UCB score. + """ + + def __init__(self, + agent: trace.Module, + optimizer, + max_buffer_size: int = 10, + ucb_exploration_factor: float = 1.0, + logger=None, + num_threads: int = None, + *args, + **kwargs): + super().__init__(agent, optimizer, num_threads=num_threads, logger=logger, *args, **kwargs) + + self.buffer = deque(maxlen=max_buffer_size) + self.max_buffer_size = max_buffer_size + self.ucb_exploration_factor = ucb_exploration_factor + + # To ensure optimizer_step can be called with bypassing=True if needed. + # This depends on the specific optimizer's implementation. + # For now, we assume the optimizer has a step method that can return parameters. + if not hasattr(self.optimizer, 'step'): + raise ValueError("Optimizer must have a 'step' method.") + + self._total_evaluations_tracker = 0 # Tracks total number of individual candidate evaluations used in UCB calculation for log(T) + self._candidate_id_counter = 0 + + def _sample_minibatch(self, dataset: Dict[str, List[Any]], batch_size: int) -> Tuple[List[Any], List[Any]]: + """Sample a minibatch from the dataset.""" + if not dataset or not dataset.get('inputs') or not dataset.get('infos'): + print_color("Warning: Attempted to sample from an empty or malformed dataset.", color='yellow') + return [], [] + + dataset_size = len(dataset['inputs']) + if dataset_size == 0: + print_color("Warning: Dataset is empty, cannot sample minibatch.", color='yellow') + return [], [] + + actual_batch_size = min(batch_size, dataset_size) + indices = np.random.choice(dataset_size, actual_batch_size, replace=False) + xs = [dataset['inputs'][i] for i in indices] + infos = [dataset['infos'][i] for i in indices] + return xs, infos + + def _evaluate_candidate(self, + params_to_eval_dict: Dict[str, Any], + dataset: Dict[str, List[Any]], # Changed from validate_dataset + guide, # Changed from validate_guide + evaluation_batch_size: int, # New parameter name + num_threads: Optional[int] = None + ) -> Tuple[float, int]: + """Evaluates a given set of parameters on samples from the provided dataset (now typically train_dataset).""" + if not dataset or not dataset.get('inputs') or not dataset.get('infos') or not dataset['inputs']: + print_color("Evaluation dataset is empty or invalid. Returning score -inf, count 0.", color='yellow') + return -np.inf, 0 + + original_params = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} + self.optimizer.update(params_to_eval_dict) + + eval_xs, eval_infos = self._sample_minibatch(dataset, evaluation_batch_size) # Use evaluation_batch_size + + if not eval_xs: + print_color("Evaluation minibatch is empty. Returning score -inf, count 0.", color='yellow') + self.optimizer.update(original_params) + return -np.inf, 0 + + eval_scores = evaluate(self.agent, + guide, # Use main guide + eval_xs, + eval_infos, + min_score=self.min_score if hasattr(self, 'min_score') else None, + num_threads=num_threads or self.num_threads, + description=f"Evaluating candidate") + + self.optimizer.update(original_params) + + avg_score = np.mean(eval_scores) if eval_scores and all(s is not None for s in eval_scores) else -np.inf + eval_count = len(eval_xs) + + return float(avg_score), eval_count + + def _calculate_ucb(self, candidate_buffer_entry: Dict, total_tracked_evaluations: int) -> float: + """Calculates UCB score for a candidate in the buffer.""" + if candidate_buffer_entry['eval_count'] == 0: + return float('inf') # Explore unvisited states first + + mean_score = candidate_buffer_entry['score_sum'] / candidate_buffer_entry['eval_count'] + + # Add 1 to total_tracked_evaluations to prevent log(0) if it's the first evaluation overall + # and to ensure log argument is > 0. + # Add 1 to eval_count in denominator as well to ensure it's robust if eval_count is small. + if total_tracked_evaluations == 0: # Should not happen if we init with one eval + total_tracked_evaluations = 1 + + exploration_term = self.ucb_exploration_factor * \ + math.sqrt(math.log(total_tracked_evaluations) / candidate_buffer_entry['eval_count']) + + return mean_score + exploration_term + + def _update_buffer_ucb_scores(self): + """Recalculates and updates UCB scores for all candidates in the buffer.""" + if not self.buffer: + return + + for candidate_entry in self.buffer: + candidate_entry['ucb_score'] = self._calculate_ucb(candidate_entry, self._total_evaluations_tracker) + + def train(self, + guide, # Guide for train_dataset (feedback generation AND evaluation) + train_dataset: Dict[str, List[Any]], + *, + num_search_iterations: int = 100, + train_batch_size: int = 2, + evaluation_batch_size: int = 20, # Renamed from validation_batch_size, used for all explicit evaluations + eval_frequency: int = 1, + log_frequency: Optional[int] = None, + save_frequency: Optional[int] = None, + save_path: str = "checkpoints/ucb_agent.pkl", + min_score_for_agent_update: Optional[float] = None, # Renamed from min_score to avoid conflict with evaluate's min_score + verbose: Union[bool, str] = False, + num_threads: Optional[int] = None, + **kwargs + ) -> Tuple[Dict[str, Any], float]: # Returns metrics and best score + """ + Main training loop for UCB Search Algorithm. + """ + num_threads = num_threads or self.num_threads + log_frequency = log_frequency or eval_frequency + self.min_score = min_score_for_agent_update # Used by parent's evaluate if called, or our own _evaluate_candidate + total_samples = 0 + + # Metrics tracking + metrics = { + 'best_candidate_scores': [], # Score of the best candidate (e.g., highest mean) found so far at each iteration + 'selected_action_ucb': [], # UCB score of the selected action 'a' + 'new_candidate_scores': [], # Score of the new candidate 'a_prime' + 'buffer_avg_score': [], + 'buffer_avg_evals': [], + } + +# 0. Evaluate the initial parameter on samples of the validation set and add it to the buffer. + print_color("Evaluating initial parameters using train_dataset samples...", 'cyan') + initial_params_dict = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} + initial_score, initial_evals = self._evaluate_candidate( + initial_params_dict, train_dataset, guide, evaluation_batch_size, num_threads # Use train_dataset and guide + ) + self._total_evaluations_tracker += initial_evals + total_samples += initial_evals + + initial_candidate_entry = { + 'params': initial_params_dict, + 'score_sum': initial_score * initial_evals if initial_score > -np.inf else 0, # Store sum for accurate mean later + 'eval_count': initial_evals, + 'ucb_score': 0.0, # Will be updated + 'iteration_created': 0 + } + self.buffer.append(initial_candidate_entry) + self._update_buffer_ucb_scores() # Update UCB for the initial candidate + print_color(f"Initial candidate: Score {initial_score:.4f}, Evals {initial_evals}", 'yellow') + + # Main search loop + for iteration in range(1, num_search_iterations + 1): + if not self.buffer: + print_color("Buffer is empty, stopping search.", 'red') + break + + # 1. Pick the candidate 'a' with the highest UCB from the buffer + self._update_buffer_ucb_scores() # Ensure UCB scores are fresh + action_candidate_a = self.select(self.buffer) + + + print_color(f"Iter {iteration}/{num_search_iterations}: ", 'blue') + + + # 2. Load parameters of 'a' into the agent for the optimizer update step + self.optimizer.update(action_candidate_a['params']) + + # 3. Draw minibatch from the training set, do update from 'a' to get 'a_prime' + train_xs, train_infos = self._sample_minibatch(train_dataset, train_batch_size) + if not train_xs: + print_color(f"Iter {iteration}: Training minibatch empty, skipping optimizer step.", 'yellow') + continue + + # Perform forward pass and get feedback for agent parameters 'a' + outputs_for_a = [] + use_asyncio = self._use_asyncio(num_threads) + if use_asyncio: + outputs_for_a = async_run([self.forward]*len(train_xs), + [(self.agent, x, guide, info) for x, info in zip(train_xs, train_infos)], + max_workers=num_threads, + description=f"Iter {iteration}: Forward pass for action 'a' ") + else: + outputs_for_a = [self.forward(self.agent, x, guide, info) for x, info in zip(train_xs, train_infos)] + + scores_from_train, targets_from_train, feedbacks_from_train = [], [], [] + for target, score, feedback in outputs_for_a: + scores_from_train.append(score) + targets_from_train.append(target) + feedbacks_from_train.append(feedback) + + if not scores_from_train: # Should not happen if train_xs was not empty + print_color(f"Iter {iteration}: No outputs from forward pass for candidate 'a'. Skipping.", 'yellow') + continue + + target_for_a = batchify(*targets_from_train) + feedback_for_a = batchify(*feedbacks_from_train).data + score_for_a_on_train_batch = np.mean([s for s in scores_from_train if s is not None]) if any(s is not None for s in scores_from_train) else -np.inf + + self.optimizer.zero_feedback() + self.optimizer.backward(target_for_a, feedback_for_a) # Grads for 'a' are now in optimizer + + try: + a_prime_params_dict = self.optimizer.step(bypassing=True, verbose='output') + if not isinstance(a_prime_params_dict, dict) or not a_prime_params_dict: + print_color(f"Iter {iteration}: Optimizer.step did not return a valid param dict for a_prime. Using current agent params as a_prime.", 'yellow') + # Fallback: if step modified agent in-place and didn't return dict, current agent state is a_prime + a_prime_params_dict = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} + + except Exception as e: + print_color(f"Iter {iteration}: Error during optimizer.step for a_prime: {e}. Skipping candidate generation.", 'red') + continue + + # 4. Evaluate 'a_prime' on samples of validation set + a_prime_score, a_prime_evals = self._evaluate_candidate( + a_prime_params_dict, train_dataset, guide, evaluation_batch_size, num_threads # Use train_dataset and guide + ) + self._total_evaluations_tracker += a_prime_evals + total_samples += evaluation_batch_size + train_batch_size + metrics['new_candidate_scores'].append(a_prime_score) + print_color(f"Iter {iteration}: New candidate a_prime generated. Validation Score: {a_prime_score:.4f}, Evals: {a_prime_evals}", 'cyan') + + # 5. Update the stats of 'a' (action_candidate_a) based on the training batch experience + if score_for_a_on_train_batch > -np.inf: + action_candidate_a['score_sum'] += score_for_a_on_train_batch * len(train_xs) # score is often an average + action_candidate_a['eval_count'] += len(train_xs) # or 1 if score is total + self._total_evaluations_tracker += len(train_xs) # training batch also counts as evaluations for UCB total T + + # 6. Add 'a_prime' (with its validation stats) to the buffer + if a_prime_score > -np.inf and a_prime_evals > 0: + new_candidate_entry = { + 'params': a_prime_params_dict, + 'score_sum': a_prime_score * a_prime_evals, # Store sum + 'eval_count': a_prime_evals, + 'ucb_score': 0.0, # Will be updated + 'iteration_created': iteration + } + + # Eviction logic before adding if buffer is at max_len + if len(self.buffer) == self.max_buffer_size: + self._update_buffer_ucb_scores() # Ensure UCBs are current before eviction + candidate_to_evict = min(self.buffer, key=lambda c: c['ucb_score']) + self.buffer.remove(candidate_to_evict) + print_color(f"Iter {iteration}: Buffer full. Evicted a candidate (UCB: {candidate_to_evict['ucb_score']:.4f})", 'magenta') + + self.buffer.append(new_candidate_entry) + print_color(f"Iter {iteration}: Added new candidate to buffer.", 'magenta') + else: + print_color(f"Iter {iteration}: New candidate a_prime had invalid score/evals, not added to buffer.", 'yellow') + + # Update all UCB scores in the buffer after potential additions/removals/stat updates + self._update_buffer_ucb_scores() + + # Logging + best_in_buffer = max(self.buffer, key=lambda c: c['score_sum']/(c['eval_count'] or 1)) + metrics['best_candidate_scores'].append(best_in_buffer['score_sum']/(best_in_buffer['eval_count'] or 1)) + metrics['buffer_avg_score'].append(np.mean([c['score_sum']/(c['eval_count'] or 1) for c in self.buffer if c['eval_count'] > 0])) + metrics['buffer_avg_evals'].append(np.mean([c['eval_count'] for c in self.buffer])) + + if iteration % log_frequency == 0: + log_data = { + "iteration": iteration, + "best_score": metrics['best_candidate_scores'][-1], #best_candidate_score_in_buffer + "selected_action_ucb": action_candidate_a['ucb_score'], + "new_candidate_score": a_prime_score, + "buffer_size": len(self.buffer), + "buffer_avg_score": metrics['buffer_avg_score'][-1], + "buffer_avg_evals": metrics['buffer_avg_evals'][-1], + "total_evaluations_tracker": self._total_evaluations_tracker, + "total_samples": total_samples # Add new metric + } + print_color(f"Log @ Iter {iteration}: Best score in buffer: {log_data['best_score']:.4f}, Buffer size: {log_data['buffer_size']}, Total samples: {total_samples}", 'green') + + # Save agent (e.g., the one with highest mean score in buffer) + if save_frequency is not None and iteration % save_frequency == 0: + best_overall_candidate = max(self.buffer, key=lambda c: c['score_sum'] / (c['eval_count'] or 1E-9) ) + self.optimizer.update(best_overall_candidate['params']) # Load params using optimizer + self.save_agent(save_path, iteration) # save_agent is from AlgorithmBase + print_color(f"Iter {iteration}: Saved agent based on best candidate in buffer.", 'green') + + # End of search loop + print_color("UCB search finished.", 'blue') + if not self.buffer: + print_color("Buffer is empty at the end of search. No best candidate found.", 'red') + return metrics, -np.inf + + # Select the best candidate based on highest mean score (exploitation) + final_best_candidate = max(self.buffer, key=lambda c: c['score_sum'] / (c['eval_count'] or 1E-9)) + final_best_score = final_best_candidate['score_sum'] / (final_best_candidate['eval_count'] or 1E-9) + print_color(f"Final best candidate: Mean Score {final_best_score:.4f}, Evals {final_best_candidate['eval_count']}", 'green') + + # Load best parameters into the agent + self.optimizer.update(final_best_candidate['params']) # Load params using optimizer + + return metrics, float(final_best_score) + + def select(self, buffer): + '''Could be subclassed to implement different selection strategies''' + return max(buffer, key=lambda c: c['ucb_score']) + + +class HybridUCB_LLM(MinibatchAlgorithm): + """ + UCB Search Algorithm with Function Approximation (LLM). + + Keeps a buffer of candidates. + In each iteration: + - With probability alpha: + 1. Picks a candidate 'a' from the buffer with the highest UCB score. + 2. Updates the optimizer with 'a's parameters. + 3. Draws a minibatch from the training set, performs a forward/backward pass, and calls optimizer.step() to get a new candidate 'a_prime'. + 4. Evaluates 'a_prime' on a validation set minibatch. + 5. Updates statistics of 'a' (based on the training minibatch). + 6. Adds 'a_prime' (with its validation stats) to the buffer. + - With probability 1-alpha: + 1. Uses an external LLM, prompted with candidates from the buffer, to generate a new candidate 'a_prime'. + 2. Evaluates 'a_prime' on a validation set minibatch. + 3. Adds 'a_prime' (with its validation stats) to the buffer. + If the buffer is full, evicts the candidate with the lowest UCB score. + """ + + def __init__(self, + agent: trace.Module, + optimizer, + max_buffer_size: int = 10, + ucb_exploration_factor: float = 1.0, + alpha: float = 0.7, + llm_model: str = "vertex_ai/gemini-2.0-flash", + logger=None, + num_threads: int = None, + *args, + **kwargs): + super().__init__(agent, optimizer, num_threads=num_threads, logger=logger, *args, **kwargs) + + self.alpha = alpha + self.llm_model = llm_model + self.llm_prompt_budget_factor = 0.5 + + self.buffer = deque(maxlen=max_buffer_size) + self.max_buffer_size = max_buffer_size + self.ucb_exploration_factor = ucb_exploration_factor + + if not hasattr(self.optimizer, 'step'): + raise ValueError("Optimizer must have a 'step' method.") + + self._total_evaluations_tracker = 0 + + # Initialize LiteLLM + self.llm = LiteLLM(model=self.llm_model) + print_color(f"Initialized HybridUCB_LLM with alpha={self.alpha}, LLM model={self.llm_model}", "cyan") + + def _sample_minibatch(self, dataset: Dict[str, List[Any]], batch_size: int) -> Tuple[List[Any], List[Any]]: + """Sample a minibatch from the dataset.""" + if not dataset or not dataset.get('inputs') or not dataset.get('infos'): + print_color("Warning: Attempted to sample from an empty or malformed dataset.", color='yellow') + return [], [] + + dataset_size = len(dataset['inputs']) + if dataset_size == 0: + print_color("Warning: Dataset is empty, cannot sample minibatch.", color='yellow') + return [], [] + + actual_batch_size = min(batch_size, dataset_size) + indices = np.random.choice(dataset_size, actual_batch_size, replace=False) + xs = [dataset['inputs'][i] for i in indices] + infos = [dataset['infos'][i] for i in indices] + return xs, infos + + def _evaluate_candidate(self, + params_to_eval_dict: Dict[str, Any], + dataset: Dict[str, List[Any]], + guide, + evaluation_batch_size: int, + num_threads: Optional[int] = None + ) -> Tuple[float, int]: + """Evaluates a given set of parameters on samples from the provided dataset.""" + if not dataset or not dataset.get('inputs') or not dataset.get('infos') or not dataset['inputs']: + print_color("Evaluation dataset is empty or invalid. Returning score -inf, count 0.", color='yellow') + return -np.inf, 0 + + original_params_backup = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} + + try: + self.optimizer.update(params_to_eval_dict) + except Exception as e: + print_color(f"Error updating agent with params_to_eval_dict: {e}. Using current agent state for eval.", "red") + + eval_xs, eval_infos = self._sample_minibatch(dataset, evaluation_batch_size) + + if not eval_xs: + print_color("Evaluation minibatch is empty. Returning score -inf, count 0.", color='yellow') + self.optimizer.update(original_params_backup) + return -np.inf, 0 + + eval_scores = evaluate(self.agent, + guide, + eval_xs, + eval_infos, + min_score=self.min_score if hasattr(self, 'min_score') else None, + num_threads=num_threads or self.num_threads, + description=f"Evaluating candidate") + + self.optimizer.update(original_params_backup) + + avg_score = np.mean(eval_scores) if eval_scores and all(s is not None for s in eval_scores) else -np.inf + eval_count = len(eval_xs) + + return float(avg_score), eval_count + + def _calculate_ucb(self, candidate_buffer_entry: Dict, total_tracked_evaluations: int) -> float: + """Calculates UCB score for a candidate in the buffer.""" + if candidate_buffer_entry['eval_count'] == 0: + return float('inf') + + mean_score = candidate_buffer_entry['score_sum'] / candidate_buffer_entry['eval_count'] + + if total_tracked_evaluations == 0: + total_tracked_evaluations = 1 + + exploration_term = self.ucb_exploration_factor * \ + math.sqrt(math.log(total_tracked_evaluations + 1e-9) / candidate_buffer_entry['eval_count']) + + return mean_score + exploration_term + + def _update_buffer_ucb_scores(self): + """Recalculates and updates UCB scores for all candidates in the buffer.""" + if not self.buffer: + return + + for candidate_entry in self.buffer: + candidate_entry['ucb_score'] = self._calculate_ucb(candidate_entry, self._total_evaluations_tracker) + + def _llm_generate_candidate(self) -> Optional[Dict[trace.nodes.ParameterNode, str]]: + """ + Prompts an LLM with current buffer candidates to generate new string values for parameters. + Returns a dictionary mapping ParameterNode objects to new string values, or None on failure. + """ + print_color("Attempting to generate candidate using LLM...", "blue") + if not self.buffer: + print_color("LLM generation: Buffer is empty, cannot provide context to LLM.", "yellow") + return None + + sorted_buffer = sorted(list(self.buffer), key=lambda c: c.get('ucb_score', -float('inf')), reverse=True) + prompt_candidates = sorted_buffer + + serializable_candidate_summaries = [] + for cand_entry in prompt_candidates: + summary = { + "parameters": {getattr(p,'py_name'): copy.deepcopy(p.data) for p in cand_entry['params']}, + "eval_count": cand_entry['eval_count'], + "ucb_score": round(cand_entry.get('ucb_score',0), 4), + } + serializable_candidate_summaries.append(summary) + + example_param_structure_json_str = {getattr(p,'py_name'): copy.deepcopy(p.data) for p in self.agent.parameters()} + + prompt_messages = [ + {"role": "system", "content": "You are an expert in model optimization. Your task is to propose new string values for model parameters with high UCB scores. Please output ONLY a valid JSON dictionary where keys are parameter names and values are the new string values for those parameters, matching the example structure provided. Do not add any explanations or markdown formatting around the JSON."}, + {"role": "user", "content": f"Here are some current candidates from the search buffer and their statistics:\\n{serializable_candidate_summaries}\\n\\nHere is an example of the required JSON output structure (parameter names as keys, new string values as values):\\n{example_param_structure_json_str}\\n\\nPlease generate a new set of parameters in exactly the same JSON format. Make sure use double quotes for the keys and values."} + ] + + print_color(f"LLM prompt (summary): {len(prompt_candidates)} candidates, structure example provided.", "magenta") + + llm_response = self.llm(prompt_messages) + llm_response_str = llm_response.choices[0].message.content + + if not llm_response_str: + print_color("LLM returned an empty response.", "red") + return None + + # Clean the response string + cleaned_llm_response_str = llm_response_str.strip() + if cleaned_llm_response_str.startswith("```json"): + cleaned_llm_response_str = cleaned_llm_response_str[7:] + if cleaned_llm_response_str.endswith("```"): + cleaned_llm_response_str = cleaned_llm_response_str[:-3] + elif cleaned_llm_response_str.startswith("```"): + cleaned_llm_response_str = cleaned_llm_response_str[3:] + if cleaned_llm_response_str.endswith("```"): + cleaned_llm_response_str = cleaned_llm_response_str[:-3] + cleaned_llm_response_str = cleaned_llm_response_str.strip() + + if not cleaned_llm_response_str: + print_color("LLM response was empty after cleaning markdown/whitespace.", "red") + return None + + print_color(f"Cleaned LLM response: '{cleaned_llm_response_str}'", "magenta") + + # Fix common JSON formatting issues from LLM responses + try: + llm_params_raw = json.loads(cleaned_llm_response_str) + except json.JSONDecodeError as e: + print_color(f"Initial JSON parsing failed: {e}", "yellow") + print_color("Attempting to fix JSON formatting...", "yellow") + + fixed_json_str = smart_quote_replacement(cleaned_llm_response_str) + + try: + llm_params_raw = json.loads(fixed_json_str) + print_color("Successfully fixed JSON formatting", "green") + except json.JSONDecodeError as e2: + print_color(f"Smart quote replacement failed: {e2}", "yellow") + try: + simple_fixed = cleaned_llm_response_str.replace("'", '"') + llm_params_raw = json.loads(simple_fixed) + print_color("Fallback simple replacement succeeded", "green") + except json.JSONDecodeError as e3: + print_color(f"All JSON parsing attempts failed: {e3}", "red") + print_color("Returning the candidate with the highest UCB score in the buffer.", "red") + return max(self.buffer, key=lambda c: c.get('ucb_score', -float('inf')))['params'] + + if not isinstance(llm_params_raw, dict): + print_color(f"LLM output was not a JSON dictionary after parsing: {type(llm_params_raw)}", "red") + print_color("Returning the candidate with the highest UCB score in the buffer.", "red") + return max(self.buffer, key=lambda c: c.get('ucb_score', -float('inf')))['params'] + + candidate_params_dict = self.construct_update_dict(llm_params_raw) + return candidate_params_dict + + def construct_update_dict(self, suggestion: Dict[str, Any]) -> Dict[ParameterNode, Any]: + """Convert the suggestion in text into the right data type.""" + update_dict = {} + for node in self.agent.parameters(): + if node.trainable and node.py_name in suggestion: + try: + formatted_suggestion = suggestion[node.py_name] + if type(formatted_suggestion) == str and 'def' in formatted_suggestion: + formatted_suggestion = format_str(formatted_suggestion, mode=FileMode()) + update_dict[node] = type(node.data)(formatted_suggestion) + except (ValueError, KeyError) as e: + if getattr(self, 'ignore_extraction_error', False): + warnings.warn( + f"Cannot convert the suggestion '{suggestion[node.py_name]}' for {node.py_name} to the right data type" + ) + else: + raise e + return update_dict + + def train(self, + guide, + train_dataset: Dict[str, List[Any]], + *, + num_search_iterations: int = 100, + train_batch_size: int = 5, + evaluation_batch_size: int = 5, + ensure_improvement: bool = False, + improvement_threshold: float = 0., + eval_frequency: int = 1, + log_frequency: Optional[int] = None, + save_frequency: Optional[int] = None, + save_path: str = "checkpoints/ucb_llm_agent.pkl", + min_score_for_agent_update: Optional[float] = None, + verbose: Union[bool, str] = False, + num_threads: Optional[int] = None, + **kwargs + ) -> Tuple[Dict[str, Any], float]: + + num_threads = num_threads or self.num_threads + log_frequency = log_frequency or eval_frequency + self.min_score = min_score_for_agent_update + total_samples = 0 + + metrics = { + 'best_candidate_scores': [], + 'selected_action_ucb': [], + 'new_candidate_scores': [], + 'buffer_avg_score': [], + 'buffer_avg_evals': [], + 'llm_generation_failures': 0, + 'generation_path': [] + } + + # Initial candidate evaluation + print_color("Evaluating initial parameters using train_dataset samples...", 'cyan') + initial_params_dict = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} + + initial_score, initial_evals = self._evaluate_candidate( + initial_params_dict, train_dataset, guide, evaluation_batch_size, num_threads + ) + self._total_evaluations_tracker += initial_evals + total_samples += initial_evals + + initial_candidate_entry = { + 'params': initial_params_dict, + 'score_sum': initial_score * initial_evals if initial_score > -np.inf else 0, + 'eval_count': initial_evals, + 'ucb_score': 0.0, + 'iteration_created': 0 + } + self.buffer.append(initial_candidate_entry) + self._update_buffer_ucb_scores() + print_color(f"Initial candidate: Score {initial_score:.4f}, Evals {initial_evals}", 'yellow') + + # Main search loop + for iteration in range(1, num_search_iterations + 1): + if not self.buffer: + print_color("Buffer is empty, stopping search.", 'red') + break + + self._update_buffer_ucb_scores() + a_prime_params_dict = None + a_prime_score = -np.inf + a_prime_evals = 0 + generation_method = "none" + + if random.random() < self.alpha: # UCB Path + generation_method = "ucb" + metrics['generation_path'].append("ucb") + if not self.buffer: + print_color(f"Iter {iteration} (UCB Path): Buffer empty, cannot select action. Skipping.", "red") + continue + + action_candidate_a = self.select(self.buffer) + + selected_mean_score = action_candidate_a['score_sum'] / action_candidate_a['eval_count'] if action_candidate_a['eval_count'] > 0 else -np.inf + print_color(f"Iter {iteration} (UCB Path): Selected action candidate (UCB: {action_candidate_a['ucb_score']:.4f}, MeanScore: {selected_mean_score:.4f} Evals: {action_candidate_a['eval_count']})", 'blue') + metrics['selected_action_ucb'].append(action_candidate_a['ucb_score']) + + self.optimizer.update(action_candidate_a['params']) + + train_xs, train_infos = self._sample_minibatch(train_dataset, train_batch_size) + if not train_xs: + print_color(f"Iter {iteration} (UCB Path): Training minibatch empty, skipping optimizer step.", 'yellow') + continue + + total_samples += len(train_xs) + + # Forward pass for 'a' + outputs_for_a = [] + use_asyncio = self._use_asyncio(num_threads) + if use_asyncio: + outputs_for_a = async_run([self.forward]*len(train_xs), + [(self.agent, x, guide, info) for x, info in zip(train_xs, train_infos)], + max_workers=num_threads, + description=f"Iter {iteration} (UCB): Forward for 'a'") + else: + outputs_for_a = [self.forward(self.agent, x, guide, info) for x, info in zip(train_xs, train_infos)] + + scores_from_train, targets_from_train, feedbacks_from_train = [], [], [] + for target, score, feedback in outputs_for_a: + scores_from_train.append(score) + targets_from_train.append(target) + feedbacks_from_train.append(feedback) + + if not scores_from_train: + print_color(f"Iter {iteration} (UCB Path): No outputs from forward pass for 'a'. Skipping.", 'yellow') + continue + + target_for_a = batchify(*targets_from_train) + feedback_for_a = batchify(*feedbacks_from_train).data + score_for_a_on_train_batch = np.mean([s for s in scores_from_train if s is not None]) if any(s is not None for s in scores_from_train) else -np.inf + + self.optimizer.zero_feedback() + self.optimizer.backward(target_for_a, feedback_for_a) + + # Get a_prime by optimizer step + try: + returned_params = self.optimizer.step(bypassing=True, verbose=(verbose if isinstance(verbose, str) else 'output')) + if not isinstance(returned_params, dict) or not returned_params: + print_color(f"Iter {iteration} (UCB Path): Optimizer.step did not return a valid param dict for a_prime. Using current agent params.", 'yellow') + a_prime_params_dict = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} + else: + a_prime_params_dict = {p: copy.deepcopy(p.data) for p in returned_params} + + except Exception as e: + print_color(f"Iter {iteration} (UCB Path): Error during optimizer.step for a_prime: {e}. Skipping.", 'red') + continue + + # Evaluate a_prime (from UCB path) + a_prime_score, a_prime_evals = self._evaluate_candidate( + a_prime_params_dict, train_dataset, guide, evaluation_batch_size, num_threads + ) + self._total_evaluations_tracker += a_prime_evals + total_samples += a_prime_evals + + # Update stats of action_candidate_a + if score_for_a_on_train_batch > -np.inf: + action_candidate_a['score_sum'] += score_for_a_on_train_batch * len(train_xs) + action_candidate_a['eval_count'] += len(train_xs) + self._total_evaluations_tracker += len(train_xs) + + print_color(f"Iter {iteration} (UCB Path): New candidate a_prime (from UCB) generated. Eval Score: {a_prime_score:.4f}, Evals: {a_prime_evals}", 'cyan') + + else: # LLM Path + generation_method = "llm" + metrics['generation_path'].append("llm") + print_color(f"Iter {iteration} (LLM Path): Generating candidate via LLM.", 'blue') + a_prime_params_dict = self._llm_generate_candidate() + + if a_prime_params_dict: + # Evaluate a_prime (from LLM path) + a_prime_score, a_prime_evals = self._evaluate_candidate( + a_prime_params_dict, train_dataset, guide, evaluation_batch_size, num_threads + ) + self._total_evaluations_tracker += a_prime_evals + total_samples += a_prime_evals + print_color(f"Iter {iteration} (LLM Path): New candidate a_prime (from LLM) generated. Eval Score: {a_prime_score:.4f}, Evals: {a_prime_evals}", 'cyan') + else: + print_color(f"Iter {iteration} (LLM Path): LLM failed to generate a valid candidate. Skipping addition to buffer.", 'red') + metrics['llm_generation_failures'] += 1 + continue + + # Common logic for adding a_prime to buffer + metrics['new_candidate_scores'].append(a_prime_score) + + if a_prime_params_dict and a_prime_score > -np.inf and a_prime_evals > 0: + new_candidate_entry = { + 'params': a_prime_params_dict, + 'score_sum': a_prime_score * a_prime_evals, + 'eval_count': a_prime_evals, + 'ucb_score': 0.0, + 'iteration_created': iteration + } + + if len(self.buffer) == self.max_buffer_size: + self._update_buffer_ucb_scores() + candidate_to_evict = min(self.buffer, key=lambda c: c['ucb_score']) + self.buffer.remove(candidate_to_evict) + evicted_mean_score = candidate_to_evict['score_sum'] / candidate_to_evict['eval_count'] if candidate_to_evict['eval_count'] > 0 else -np.inf + print_color(f"Iter {iteration}: Buffer full. Evicted candidate (UCB: {candidate_to_evict['ucb_score']:.4f}, MeanScore: {evicted_mean_score:.4f})", 'magenta') + + self.buffer.append(new_candidate_entry) + print_color(f"Iter {iteration}: Added new candidate (from {generation_method}) to buffer.", 'magenta') + elif a_prime_params_dict: + print_color(f"Iter {iteration}: New candidate a_prime (from {generation_method}) had invalid score/evals ({a_prime_score}, {a_prime_evals}), not added to buffer.", 'yellow') + + self._update_buffer_ucb_scores() + + # Logging + if self.buffer: + best_in_buffer = max(self.buffer, key=lambda c: (c['score_sum']/(c['eval_count'] if c['eval_count'] > 0 else 1))) + current_best_score = best_in_buffer['score_sum']/(best_in_buffer['eval_count'] if best_in_buffer['eval_count'] > 0 else 1) + metrics['best_candidate_scores'].append(current_best_score) + + valid_scores = [c['score_sum']/(c['eval_count'] if c['eval_count'] > 0 else 1) for c in self.buffer if c['eval_count'] > 0] + metrics['buffer_avg_score'].append(np.mean(valid_scores) if valid_scores else -np.inf) + metrics['buffer_avg_evals'].append(np.mean([c['eval_count'] for c in self.buffer])) + else: + metrics['best_candidate_scores'].append(-np.inf) + metrics['buffer_avg_score'].append(-np.inf) + metrics['buffer_avg_evals'].append(0) + + if iteration % log_frequency == 0: + log_data = { + "iteration": iteration, + "best_score": metrics['best_candidate_scores'][-1], + "newly_evaluated_candidate_score": a_prime_score, + "buffer_size": len(self.buffer), + "buffer_avg_score": metrics['buffer_avg_score'][-1], + "buffer_avg_evals": metrics['buffer_avg_evals'][-1], + "total_evaluations_ucb_T": self._total_evaluations_tracker, + "total_samples": total_samples, + "generation_method_this_iter": generation_method, + "llm_generation_total_failures": metrics['llm_generation_failures'] + } + if generation_method == "ucb" and metrics['selected_action_ucb']: + log_data["selected_action_ucb"] = metrics['selected_action_ucb'][-1] + + print_color(f"Log @ Iter {iteration}: Best score in buffer: {log_data['best_score']:.4f}, Gen method: {generation_method}, Buffer size: {len(self.buffer)}, Total samples: {total_samples}", 'green') + + if save_frequency is not None and iteration % save_frequency == 0 and self.buffer: + best_overall_candidate_entry = max(self.buffer, key=lambda c: (c['score_sum'] / (c['eval_count'] if c['eval_count'] > 0 else 1E-9))) + self.optimizer.update(best_overall_candidate_entry['params']) + if hasattr(self, 'save_agent'): + self.save_agent(save_path, iteration) + best_mean_score_for_save = best_overall_candidate_entry['score_sum'] / (best_overall_candidate_entry['eval_count'] if best_overall_candidate_entry['eval_count'] > 0 else 1E-9) + print_color(f"Iter {iteration}: Saved agent based on best candidate in buffer (Mean Score: {best_mean_score_for_save:.4f}).", 'green') + else: + print_color(f"Iter {iteration}: save_agent method not found, skipping save.", 'yellow') + + print_color("UCB-LLM search finished.", 'blue') + if not self.buffer: + print_color("Buffer is empty at the end of search. No best candidate found.", 'red') + return metrics, -np.inf + + final_best_candidate = max(self.buffer, key=lambda c: (c['score_sum'] / (c['eval_count'] if c['eval_count'] > 0 else 1E-9))) + final_best_score = final_best_candidate['score_sum'] / (final_best_candidate['eval_count'] if final_best_candidate['eval_count'] > 0 else 1E-9) + final_best_evals = final_best_candidate['eval_count'] + print_color(f"Final best candidate: Mean Score {final_best_score:.4f}, Evals {final_best_evals}", 'green') + + self.optimizer.update(final_best_candidate['params']) + + return metrics, float(final_best_score) + + def select(self, buffer): + '''Selects candidate with highest UCB score.''' + if not buffer: return None + return max(buffer, key=lambda c: c.get('ucb_score', -float('inf'))) + + +class UCBSearchFunctionApproximationAlgorithm(UCBSearchAlgorithm): + """ + UCB Search Algorithm that uses LLM function approximation to select candidates. + """ + + def __init__(self, llm_model, *args, **kwargs): + super().__init__(*args, **kwargs) + self.llm_model = llm_model + self.llm = LiteLLM(model=self.llm_model) + print_color(f"Initialized UCBSearchFunctionApproximationAlgorithm with LLM model={self.llm_model}", "cyan") + + def select(self, buffer): + """Generate a new candidate entry using LLM. Note: this doesn't add it to the buffer.""" + new_action_params = self._llm_generate_candidate() + new_candidate_entry = { + 'params': new_action_params, + 'score_sum': 0, + 'eval_count': 0, + 'ucb_score': 0.0, + 'iteration_created': 0 + } + return new_candidate_entry + + def _llm_generate_candidate(self) -> Optional[Dict[trace.nodes.ParameterNode, str]]: + """ + Prompts an LLM with current buffer candidates to generate new string values for parameters. + Returns a dictionary mapping ParameterNode objects to new string values, or None on failure. + """ + print_color("Attempting to generate candidate using LLM...", "blue") + if not self.buffer: + print_color("LLM generation: Buffer is empty, cannot provide context to LLM.", "yellow") + return None + + sorted_buffer = sorted(list(self.buffer), key=lambda c: c.get('ucb_score', -float('inf')), reverse=True) + prompt_candidates = sorted_buffer + + serializable_candidate_summaries = [] + for cand_entry in prompt_candidates: + summary = { + "parameters": {getattr(p,'py_name'): copy.deepcopy(p.data) for p in cand_entry['params']}, + "eval_count": cand_entry['eval_count'], + "ucb_score": round(cand_entry.get('ucb_score',0), 4), + } + serializable_candidate_summaries.append(summary) + + example_param_structure_json_str = {getattr(p,'py_name'): copy.deepcopy(p.data) for p in self.agent.parameters()} + + prompt_messages = [ + {"role": "system", "content": "You are an expert in model optimization. Your task is to propose new string values for model parameters with high UCB scores. Please output ONLY a valid JSON dictionary where keys are parameter names and values are the new string values for those parameters, matching the example structure provided. Do not add any explanations or markdown formatting around the JSON."}, + {"role": "user", "content": f"Here are some current candidates from the search buffer and their statistics:\\n{serializable_candidate_summaries}\\n\\nHere is an example of the required JSON output structure (parameter names as keys, new string values as values):\\n{example_param_structure_json_str}\\n\\nPlease generate a new set of parameters in exactly the same JSON format. Make sure use double quotes for the keys and values."} + ] + + print_color(f"LLM prompt (summary): {len(prompt_candidates)} candidates, structure example provided.", "magenta") + + llm_response = self.llm(prompt_messages) + llm_response_str = llm_response.choices[0].message.content + + if not llm_response_str: + print_color("LLM returned an empty response.", "red") + return None + + # Clean the response string + cleaned_llm_response_str = llm_response_str.strip() + if cleaned_llm_response_str.startswith("```json"): + cleaned_llm_response_str = cleaned_llm_response_str[7:] + if cleaned_llm_response_str.endswith("```"): + cleaned_llm_response_str = cleaned_llm_response_str[:-3] + elif cleaned_llm_response_str.startswith("```"): + cleaned_llm_response_str = cleaned_llm_response_str[3:] + if cleaned_llm_response_str.endswith("```"): + cleaned_llm_response_str = cleaned_llm_response_str[:-3] + cleaned_llm_response_str = cleaned_llm_response_str.strip() + + if not cleaned_llm_response_str: + print_color("LLM response was empty after cleaning markdown/whitespace.", "red") + return None + + print_color(f"Cleaned LLM response: '{cleaned_llm_response_str}'", "magenta") + + # Fix common JSON formatting issues from LLM responses + try: + llm_params_raw = json.loads(cleaned_llm_response_str) + except json.JSONDecodeError as e: + print_color(f"Initial JSON parsing failed: {e}", "yellow") + print_color("Attempting to fix JSON formatting...", "yellow") + + fixed_json_str = smart_quote_replacement(cleaned_llm_response_str) + + try: + llm_params_raw = json.loads(fixed_json_str) + print_color("Successfully fixed JSON formatting", "green") + except json.JSONDecodeError as e2: + print_color(f"Smart quote replacement failed: {e2}", "yellow") + try: + simple_fixed = cleaned_llm_response_str.replace("'", '"') + llm_params_raw = json.loads(simple_fixed) + print_color("Fallback simple replacement succeeded", "green") + except json.JSONDecodeError as e3: + print_color(f"All JSON parsing attempts failed: {e3}", "red") + print_color("Returning the candidate with the highest UCB score in the buffer.", "red") + return max(self.buffer, key=lambda c: c.get('ucb_score', -float('inf')))['params'] + + if not isinstance(llm_params_raw, dict): + print_color(f"LLM output was not a JSON dictionary after parsing: {type(llm_params_raw)}", "red") + print_color("Returning the candidate with the highest UCB score in the buffer.", "red") + return max(self.buffer, key=lambda c: c.get('ucb_score', -float('inf')))['params'] + + candidate_params_dict = self.construct_update_dict(llm_params_raw) + return candidate_params_dict + + def construct_update_dict(self, suggestion: Dict[str, Any]) -> Dict[ParameterNode, Any]: + """Convert the suggestion in text into the right data type.""" + update_dict = {} + for node in self.agent.parameters(): + if node.trainable and node.py_name in suggestion: + try: + formatted_suggestion = suggestion[node.py_name] + if type(formatted_suggestion) == str and 'def' in formatted_suggestion: + formatted_suggestion = format_str(formatted_suggestion, mode=FileMode()) + update_dict[node] = type(node.data)(formatted_suggestion) + except (ValueError, KeyError) as e: + if getattr(self, 'ignore_extraction_error', False): + warnings.warn( + f"Cannot convert the suggestion '{suggestion[node.py_name]}' for {node.py_name} to the right data type" + ) + else: + raise e + return update_dict + From ff3525d1a9502af63266a6984daa3b4a706264f8 Mon Sep 17 00:00:00 2001 From: adith387 Date: Thu, 5 Jun 2025 16:45:05 -0700 Subject: [PATCH 004/314] Update OVERVIEW.md --- OVERVIEW.md | 81 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 53 insertions(+), 28 deletions(-) diff --git a/OVERVIEW.md b/OVERVIEW.md index 947180c6..e53a8c19 100644 --- a/OVERVIEW.md +++ b/OVERVIEW.md @@ -1,32 +1,57 @@ # Overview of Trace and Development Guide -The library of Trace is designed to be a lightweight, modularized package to allow developers to easily try new ideas on generative optimization and integrate learning wtih their pipelines. - -Currently, the Trace library has three main modules collected under the `opto` top module. - -1. `opto.trace` provides the infrastructure for tracing computational workflows. It defines two primitives `trace.node` and `@trace.bundle`. They can be applied to Python objects and methods, respectively, which define the root nodes and operators of the directed acyclic graph (DAG) of computation. They both have a `trainable` flag. When set `True`, the wrapped objects are viewed as *parameters* of the computational worflow. Users can use `trace.node` and `@trace.bundle` to declare the data and computation that they wish to trace and/or adapt, and we call the resulting workflow defined by these two primitives a *traced* workflow. When running a traced workflow, a DAG will be automatiically created by Trace as a data structure, which will later be sent to optimizers in `opto.optimizers`for updates (upon calling `node.backward` with soem feedback). - -2. `opto.optimizers` has a collection of generative optimization algorithms, whose API is defined by an abstract class `Optimizer`. Think them like gradient algorithms. Their job is to propose a new version of the parameters (i.e. those set with `trainable=True`) when receiving a computational graph (DAG) and the feedback given to the computed output. Typically, these algorithms can be viewed as an LLM agent, which makes calls to LLM to analyze the computational graph and the feedback, and to propose updates. In Trace library, we provide implementation of several popular optimizers, such `OptoPrime`, `TextGrad`, and `OPRO`. - -3. `opto.trainers` are a collection of training algorithms (under the `AlgorithmBase` class) that use optimizers in `opto.optimizers` as subroutines to improve a given workflow following a feedback oracle constructed by datasets, interactive environments, etc. While `Optimizer` defines a low-level *optimization* API, `AlgorithmBase` defines a high-level *learning* API which standarizes the format of agent (by the `Module` class created by `@trace.model`), the data loader (by the `DataLoader` class), and the feedback oracle (by the `AutoGuide` class). With this common abstraction, we offer training algorithms, from the basic `MinibatchAlgorithm` which trains minibatches of samples to search algorithms like `BeamSearch`. The `AlgorithmBase` also handles logging of the training process. While there are overlapping between the functions of `Optimizer` and `AlgorithmBase`, the main distinction is that algorithms under `AlgorithmBase` are meta algorithms, as they should work for different optimizers in `opto.optimizers`. - - -4. `opto.utils` has a collection of helper functions and backends, which are reusable for various applications. This includes, e.g., abstraction of LLMs, database, etc. Making use of all these utils would requie installing optional depedencies. - - -In summary, `opto.trace` is the infrastructure, `opto.optimizers` are algorithms that process feedback and propose new parameter candidates, and `opto.trainers` are algorithms built on top of `opto.trace` and `opto.optimizers` to train learning agents. - -## Common Workflow of Using Trace - -1. Use `trace.node` and `@trace.bundle` to define the traceable workflow and its trainable parameter. -2. Wrap the workflow as a `trace.Module` using `@trace.model` -3. Create a dataloader using `DataLoader` and define the feedback oracle (an analogy of loss function) using `AutoGuide`. -4. Create a trainer from `opto.trainers` using optimizers from `opto.optimizers` and the above module, dataloader, and feedback oracle. +The Trace library is a lightweight, modular package designed to allow developers to experiment easily with generative optimization and integrate feedback-driven learning into their computational workflows. +The library has four modules within the `opto` top-level namespace: + +1. `opto.trace` provides the infrastructure for converting executing Python code into symbolic directed acyclic graphs (DAGs). +It defines two tracing primitives: + - `trace.node`: Wraps Python objects, designating them as nodes within the computational graph. + - `@trace.bundle`: Decorates Python methods/functions, marking them as operators within the graph. + +Each primitive has a `trainable` flag. +When set to `True`, these marked nodes and bundles become the trainable *parameters* of the workflow. +By using these primitives, developers can create a *traced workflow* represented as a DAG. +This DAG structure is automatically constructed at runtime, capturing both computational dependencies and trainable parameters, ready for optimization. + +2. `opto.optimizers` has an abstract class `Optimizer` that defines algorithms that take computation DAGs and associated feedback objects as input, and output values for the trainable parameters. +These algorithms are analogous to gradient-based optimizers in PyTorch, but are typically implemented as generative optimization agents, leveraging LLMs to analyze feedback and propose parameter updates. +We provide implementations of several generative optimizers: + - `OptoPrime` + - `TextGrad` + - `OPRO` + +3. `opto.trainers` has the `AlgorithmBase` abstraction that orchestrates the overall training process. +Trainers manage data handling, tracing control, feedback collection, optimizer invocation, and iterating/stopping. Specifically, a trainer: + - Controls data sampling (via `DataLoader`). + - Determines when DAGs are constructed and when feedback (e.g. via `AutoGuide`) is collected . + - Invokes `optimizers` for parameter updates, possibly repeatedly and manages the training loop. + - Logs training progress. + +Although `optimizers` handle lower-level optimization decisions, trainers under `AlgorithmBase` manage broader training logic and are designed to be compatible across various `optimizers`. +We provide implementations of common trainers: `MinibatchAlgorithm`(basic minibatch training) and `BeamSearch` (example of search-based training). + +4. `opto.utils` has a collection of reusable helper functions and backend utilities, including abstraction for: + - Large Language Models (LLMs) + - Databases + - Miscellaneous support tools. + +Note: Some utilities might require installing optional depedencies. + +## Concise Summary of Abstractions + - `trace`: Infrastructure to construct symbolic computational DAGs + - `optimizers`: Receive DAG and feedback, output parameter values. + - `trainer`: Manages DAG construction, data sampling, feedback collection, optimizer invocation, and training workflow control. + +## Common Workflow for Using Trace + +1. Define a traceable workflow with `trace.node` and `@trace.bundle`, marking trainable parameters. +2. Wrap this workflow into a `trace.Module` with `@trace.model`. +3. Define a dataloader (`DataLoader`) and feedback oracle (analogous to a loss function, using e.g. `AutoGuide`). +4. Instantiate a trainer from `opto.trainers`, specifying the optimizer from `opto.optimizers` alongside the defined module above, dataloader, and feedback oracle. 5. Run the trainer. - -## Common Workflow of Improving Trace -- **Developing new optimization agent** Contribute to `trace.optimizers` and design new algorithms under `Optimizer` -- **Developing new learning algorithms** Contribute to `trace.trainers` (and `trace.optimizers` when necessary). Design new algorithms under `AlgorithmBase`, new dataloader under `DataLoader`, or new feedback oracle under `AutoGuide`. -- **Improving infrastructure** Propose updates to change `opto.trace` (e.g., to improve UI, add new tracing, etc.) -- **Onboarding other utility tools** Add to `opto.utils` and update `setup.py` with optional requirements. \ No newline at end of file +## Guidelines for Improving and Extending Trace + - **New optimization agents**: Contribute to `opto.optimizers`, sub-class from the `Optimizer` abstraction. + - **New learning algorithms**: Contribute to `opto.trainers` (and optionally `opto.optimizers` if necessary). Design new algorithms sub-classing `AlgorithmBase`, new dataloader under `DataLoader`, or new feedback oracle under `AutoGuide`. + - **Improving infrastructure**: Propose modifications to `opto.trace` to improve tracing capability, user experience, or additional functionality. + - **Onboarding other utility tools**: Add helpful tools to `opto.utils` and update `setup.py` accordingly for optional dependencies. From 05577885acbcf61599d1826e6b6e23c0e9cf454b Mon Sep 17 00:00:00 2001 From: chinganc Date: Fri, 6 Jun 2025 23:05:59 +0000 Subject: [PATCH 005/314] RenameOptoprimeBatchOpt to OptoPrimeV2 --- opto/optimizers/__init__.py | 4 ++-- opto/optimizers/{optoprime_batchopt.py => optoprime_v2.py} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename opto/optimizers/{optoprime_batchopt.py => optoprime_v2.py} (99%) diff --git a/opto/optimizers/__init__.py b/opto/optimizers/__init__.py index e03b7f93..e40f9c36 100644 --- a/opto/optimizers/__init__.py +++ b/opto/optimizers/__init__.py @@ -2,6 +2,6 @@ from opto.optimizers.optoprimemulti import OptoPrimeMulti from opto.optimizers.opro import OPRO from opto.optimizers.textgrad import TextGrad -from opto.optimizers.optoprime_batchopt import OptoprimeBatchOpt +from opto.optimizers.optoprime_v2 import OptoPrimeV2 -__all__ = ["OPRO", "OptoPrime", "OptoPrimeMulti", "TextGrad", "OptoprimeBatchOpt"] \ No newline at end of file +__all__ = ["OPRO", "OptoPrime", "OptoPrimeMulti", "TextGrad", "OptoPrimeV2"] \ No newline at end of file diff --git a/opto/optimizers/optoprime_batchopt.py b/opto/optimizers/optoprime_v2.py similarity index 99% rename from opto/optimizers/optoprime_batchopt.py rename to opto/optimizers/optoprime_v2.py index c34265dd..f0c78258 100644 --- a/opto/optimizers/optoprime_batchopt.py +++ b/opto/optimizers/optoprime_v2.py @@ -3,7 +3,7 @@ from opto.optimizers.optoprime import OptoPrime -class OptoprimeBatchOpt(OptoPrime): +class OptoPrimeV2(OptoPrime): # This is generic representation prompt, which just explains how to read the problem. representation_prompt = dedent( """ From 7687d665d374dd6adefa6f6bc15d5a06528f9a5f Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 9 Jun 2025 21:30:26 +0000 Subject: [PATCH 006/314] Add projection api. --- opto/optimizers/optimizer.py | 9 +++++++++ opto/optimizers/optoprime.py | 4 ---- opto/trace/bundle.py | 6 ++++++ opto/trace/nodes.py | 10 ++++++++++ opto/trace/projections/__init__.py | 1 + opto/trace/projections/projections.py | 28 +++++++++++++++++++++++++++ tests/unit_tests/test_projection.py | 16 +++++++++++++++ 7 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 opto/trace/projections/__init__.py create mode 100644 opto/trace/projections/projections.py create mode 100644 tests/unit_tests/test_projection.py diff --git a/opto/optimizers/optimizer.py b/opto/optimizers/optimizer.py index ea2a0503..77ee10db 100644 --- a/opto/optimizers/optimizer.py +++ b/opto/optimizers/optimizer.py @@ -54,10 +54,19 @@ def trace_graph(self): def step(self, bypassing=False, *args, **kwargs): update_dict = self.propose(*args, **kwargs) + self.project(update_dict) if not bypassing: self.update(update_dict) return update_dict # TODO add reasoning + def project(self, update_dict: Dict[ParameterNode, Any]): + """Project the update dictionary onto the feasible set.""" + for p, d in update_dict.items(): + if p.trainable: + for projection in p.projections: + d = projection.project(d) + update_dict[p] = d + def propose(self, *args, **kwargs): """Propose the new data of the parameters based on the feedback.""" return self._step(*args, **kwargs) diff --git a/opto/optimizers/optoprime.py b/opto/optimizers/optoprime.py index 6ac4ce95..faa52df6 100644 --- a/opto/optimizers/optoprime.py +++ b/opto/optimizers/optoprime.py @@ -478,11 +478,7 @@ def construct_update_dict( for node in self.parameters: if node.trainable and node.py_name in suggestion: try: - from black import format_str, FileMode formatted_suggestion = suggestion[node.py_name] - # use black formatter for code reformatting - if type(formatted_suggestion) == str and 'def' in formatted_suggestion: - formatted_suggestion = format_str(formatted_suggestion, mode=FileMode()) update_dict[node] = type(node.data)(formatted_suggestion) except (ValueError, KeyError) as e: # catch error due to suggestion missing the key or wrong data type diff --git a/opto/trace/bundle.py b/opto/trace/bundle.py index ce080360..570c0833 100644 --- a/opto/trace/bundle.py +++ b/opto/trace/bundle.py @@ -39,6 +39,7 @@ def bundle( catch_execution_error=True, allow_external_dependencies=False, overwrite_python_recursion=False, + projections=None, ): """Wrap a function as a FunModule which returns node objects. @@ -53,6 +54,7 @@ def bundle( catch_execution_error (bool, optional): Whether to catch exceptions during operator execution. Defaults to True. allow_external_dependencies (bool, optional): Whether to allow external dependencies. Defaults to False. overwrite_python_recursion (bool, optional): Whether to overwrite Python recursion behavior. Defaults to False. + projections (List[Projection], optional): List of projections to be used in updating trainable parameter. Defaults to None. Returns: FunModule: The wrapped function that returns node objects. @@ -70,6 +72,7 @@ def decorator(fun): allow_external_dependencies=allow_external_dependencies, overwrite_python_recursion=overwrite_python_recursion, _ldict=prev_f_locals, # Get the locals of the calling function + projections=None, ) return fun_module @@ -124,6 +127,7 @@ def __init__( catch_execution_error=True, allow_external_dependencies=False, overwrite_python_recursion=False, + projections=None, _ldict=None, ): @@ -183,10 +187,12 @@ def __init__( signature = re.search(r"\s*(def.*:)", source).group(1) else: signature = signature_sr.group(1) + self.parameter = ParameterNode( self.info["source"], name="__code", constraint="The code should start with:\n" + signature, + projections=projections, ) @property diff --git a/opto/trace/nodes.py b/opto/trace/nodes.py index a05e662c..ebfd4153 100644 --- a/opto/trace/nodes.py +++ b/opto/trace/nodes.py @@ -2007,6 +2007,7 @@ def __init__( trainable=True, description="[ParameterNode] This is a ParameterNode in a computational graph.", constraint=None, + projections=None, # a list of Projection info=None, ) -> None: if description is None or description == "": @@ -2027,6 +2028,15 @@ def __init__( info=info, ) self._dependencies["parameter"].add(self) + if projections is not None: + assert isinstance( + projections, list + ), "Projections must be a list of Projection objects." + from opto.trace.projection import Projection + assert all( + isinstance(p, Projection) for p in projections + ), "All projections must be instances of Projection." + self._projections = projections def __str__(self) -> str: # str(node) allows us to look up in the feedback dictionary easily diff --git a/opto/trace/projections/__init__.py b/opto/trace/projections/__init__.py new file mode 100644 index 00000000..f029f4f1 --- /dev/null +++ b/opto/trace/projections/__init__.py @@ -0,0 +1 @@ +from opto.trace.projections.projections import Projection, BlackCodeFormatter \ No newline at end of file diff --git a/opto/trace/projections/projections.py b/opto/trace/projections/projections.py new file mode 100644 index 00000000..262202e2 --- /dev/null +++ b/opto/trace/projections/projections.py @@ -0,0 +1,28 @@ +from opto.trace.nodes import ParameterNode + + +class Projection: + """ + Abstract base class for projection methods. + """ + + def __init__(self, *args, **kwargs): + pass + + def project(self, x: ParameterNode) -> ParameterNode: + """ + Project the parameter node `x` onto the feasible set. + """ + raise NotImplementedError("Subclasses should implement this method.") + + +class BlackCodeFormatter(Projection): + # This requires the `black` package to be installed. + + def project(self, x: str) -> str: + # importing here to avoid necessary dependencies on black + # use black formatter for code reformatting + from black import format_str, FileMode + if type(x) == str and 'def' in x: + x = format_str(x, mode=FileMode()) + return x diff --git a/tests/unit_tests/test_projection.py b/tests/unit_tests/test_projection.py new file mode 100644 index 00000000..c0ada6e9 --- /dev/null +++ b/tests/unit_tests/test_projection.py @@ -0,0 +1,16 @@ +from opto.trace.projections import BlackCodeFormatter + +def test_black_code_formatter(): + code = """ +def example_function(): + print("Hello, World!") + + + print("This is a test function.") + + + + """ + projection = BlackCodeFormatter() + formatted_code = projection.project(code) + assert formatted_code == 'def example_function():\n print("Hello, World!")\n\n print("This is a test function.")\n' From 9658fb6769010df31928fa18abe8b27fef5bba2e Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 9 Jun 2025 21:58:44 +0000 Subject: [PATCH 007/314] Add docstring projection. --- opto/trace/projections/__init__.py | 3 ++- opto/trace/projections/code_projections.py | 31 ++++++++++++++++++++++ opto/trace/projections/projections.py | 13 +-------- tests/unit_tests/test_projection.py | 24 ++++++++++++++++- 4 files changed, 57 insertions(+), 14 deletions(-) create mode 100644 opto/trace/projections/code_projections.py diff --git a/opto/trace/projections/__init__.py b/opto/trace/projections/__init__.py index f029f4f1..7264d5bd 100644 --- a/opto/trace/projections/__init__.py +++ b/opto/trace/projections/__init__.py @@ -1 +1,2 @@ -from opto.trace.projections.projections import Projection, BlackCodeFormatter \ No newline at end of file +from opto.trace.projections.projections import Projection +from opto.trace.projections.code_projections import BlackCodeFormatter, DocstringProjection \ No newline at end of file diff --git a/opto/trace/projections/code_projections.py b/opto/trace/projections/code_projections.py new file mode 100644 index 00000000..78a4642c --- /dev/null +++ b/opto/trace/projections/code_projections.py @@ -0,0 +1,31 @@ + +from opto.trace.projections import Projection + +class BlackCodeFormatter(Projection): + # This requires the `black` package to be installed. + + def project(self, x: str) -> str: + # importing here to avoid necessary dependencies on black + # use black formatter for code reformatting + from black import format_str, FileMode + if type(x) == str and 'def' in x: + x = format_str(x, mode=FileMode()) + return x + +class DocstringProjection(Projection): + """ + Projection that formats docstrings. + """ + def __init__(self, docstring: str): + self.docstring = docstring + + def project(self, x: str) -> str: + """ Replace the docstring in the code wit the stored docstring. """ + if type(x) == str and '"""' in x: + # replace the docstring in the code with the stored docstring + x = x.split('"""', 2) + if len(x) > 2: + x = f'{x[0]}"""{self.docstring}"""{x[2]}' + else: + x = f'{x[0]}"""{self.docstring}"""' + return x \ No newline at end of file diff --git a/opto/trace/projections/projections.py b/opto/trace/projections/projections.py index 262202e2..4c799f3a 100644 --- a/opto/trace/projections/projections.py +++ b/opto/trace/projections/projections.py @@ -14,15 +14,4 @@ def project(self, x: ParameterNode) -> ParameterNode: Project the parameter node `x` onto the feasible set. """ raise NotImplementedError("Subclasses should implement this method.") - - -class BlackCodeFormatter(Projection): - # This requires the `black` package to be installed. - - def project(self, x: str) -> str: - # importing here to avoid necessary dependencies on black - # use black formatter for code reformatting - from black import format_str, FileMode - if type(x) == str and 'def' in x: - x = format_str(x, mode=FileMode()) - return x + \ No newline at end of file diff --git a/tests/unit_tests/test_projection.py b/tests/unit_tests/test_projection.py index c0ada6e9..794fffcd 100644 --- a/tests/unit_tests/test_projection.py +++ b/tests/unit_tests/test_projection.py @@ -1,4 +1,4 @@ -from opto.trace.projections import BlackCodeFormatter +from opto.trace.projections import BlackCodeFormatter, DocstringProjection def test_black_code_formatter(): code = """ @@ -14,3 +14,25 @@ def example_function(): projection = BlackCodeFormatter() formatted_code = projection.project(code) assert formatted_code == 'def example_function():\n print("Hello, World!")\n\n print("This is a test function.")\n' + + +def test_docstring_projection(): + code = """ +def example_function(): + \"\"\"This is an example function.\"\"\" + print("Hello, World!") + """ + docstring = "This is a new docstring." + projection = DocstringProjection(docstring) + formatted_code = projection.project(code) + + new_code = """ +def example_function(): + \"\"\"This is a new docstring.\"\"\" + print("Hello, World!") + """ + + assert formatted_code == new_code + + # assert '"""This is a new docstring."""' in formatted_code + # assert 'print("Hello, World!")' in formatted_code \ No newline at end of file From 693516e70d459d5e44b8b5b3b538fc89f05e2a20 Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 9 Jun 2025 22:02:10 +0000 Subject: [PATCH 008/314] Rename basic_algorithm.py to basic_algorithms.py --- examples/minibatch_bbh_aynsc/run_bigbench_trace_async.py | 2 +- opto/trainer/algorithms/__init__.py | 2 +- opto/trainer/algorithms/aggregator.py | 2 +- .../algorithms/{basic_algorithm.py => basic_algorithms.py} | 0 tests/llm_optimizers_tests/test_trainer_refactored.py | 2 +- 5 files changed, 4 insertions(+), 4 deletions(-) rename opto/trainer/algorithms/{basic_algorithm.py => basic_algorithms.py} (100%) diff --git a/examples/minibatch_bbh_aynsc/run_bigbench_trace_async.py b/examples/minibatch_bbh_aynsc/run_bigbench_trace_async.py index b0ed9b28..7e12339f 100644 --- a/examples/minibatch_bbh_aynsc/run_bigbench_trace_async.py +++ b/examples/minibatch_bbh_aynsc/run_bigbench_trace_async.py @@ -10,7 +10,7 @@ import autogen import pickle import os -from opto.trainer.algorithms.basic_algorithm import MinibatchAlgorithm, evaluate +from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm, evaluate from opto.trainer.guide import AutoGuide diff --git a/opto/trainer/algorithms/__init__.py b/opto/trainer/algorithms/__init__.py index aac6a494..ea5dde63 100644 --- a/opto/trainer/algorithms/__init__.py +++ b/opto/trainer/algorithms/__init__.py @@ -1 +1 @@ -from opto.trainer.algorithms.basic_algorithm import Minibatch, MinibatchAlgorithm, BasicSearchAlgorithm +from opto.trainer.algorithms.basic_algorithms import Minibatch, MinibatchAlgorithm, BasicSearchAlgorithm diff --git a/opto/trainer/algorithms/aggregator.py b/opto/trainer/algorithms/aggregator.py index 4f94d999..a1d30a67 100644 --- a/opto/trainer/algorithms/aggregator.py +++ b/opto/trainer/algorithms/aggregator.py @@ -9,7 +9,7 @@ from opto.trace.nodes import ParameterNode from opto.optimizers.utils import print_color from opto.trainer.algorithms import Minibatch -from opto.trainer.algorithms.basic_algorithm import standard_optimization_step +from opto.trainer.algorithms.basic_algorithms import standard_optimization_step from opto.utils.llm import LLM, AbstractModel diff --git a/opto/trainer/algorithms/basic_algorithm.py b/opto/trainer/algorithms/basic_algorithms.py similarity index 100% rename from opto/trainer/algorithms/basic_algorithm.py rename to opto/trainer/algorithms/basic_algorithms.py diff --git a/tests/llm_optimizers_tests/test_trainer_refactored.py b/tests/llm_optimizers_tests/test_trainer_refactored.py index 58b32dcd..74f1993b 100644 --- a/tests/llm_optimizers_tests/test_trainer_refactored.py +++ b/tests/llm_optimizers_tests/test_trainer_refactored.py @@ -4,7 +4,7 @@ from opto.utils.llm import LLM, LiteLLM from opto.optimizers.utils import print_color from opto.optimizers import OptoPrime -from opto.trainer.algorithms.basic_algorithm import BatchedFeedback +from opto.trainer.algorithms.basic_algorithms import BatchedFeedback from opto.trainer.guide import VerbalJudgeGuide from typing import Any From 2bb7eade2662d99ffc84c70b1e5358bf56b681a3 Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 9 Jun 2025 22:35:44 +0000 Subject: [PATCH 009/314] Fix bug in MinibatchAlgorithm due to an accidental commit --- opto/trainer/algorithms/basic_algorithms.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/opto/trainer/algorithms/basic_algorithms.py b/opto/trainer/algorithms/basic_algorithms.py index d443367e..66596580 100644 --- a/opto/trainer/algorithms/basic_algorithms.py +++ b/opto/trainer/algorithms/basic_algorithms.py @@ -272,9 +272,6 @@ def update(self, outputs, *args, **kwargs): feedback = batchify(*feedbacks).data # str average_score = np.mean(scores) if all([s is not None for s in scores]) else None - fig = target.backward(visualize=True, retain_graph=True) - fig.render("minibatch.pdf") - # Update the agent using the feedback self.optimizer.zero_feedback() self.optimizer.backward(target, feedback) From 40867e7b63a0bba452184dd7b7a231ccf96137c0 Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 9 Jun 2025 22:52:33 +0000 Subject: [PATCH 010/314] Rafactor loggers into loggers.py --- opto/trainer/algorithms/algorithm.py | 3 +- opto/trainer/loggers.py | 70 ++++++++++++++++++++++++++++ opto/trainer/utils.py | 27 ----------- setup.py | 3 +- 4 files changed, 74 insertions(+), 29 deletions(-) create mode 100644 opto/trainer/loggers.py diff --git a/opto/trainer/algorithms/algorithm.py b/opto/trainer/algorithms/algorithm.py index 927d186f..9ec35fcc 100644 --- a/opto/trainer/algorithms/algorithm.py +++ b/opto/trainer/algorithms/algorithm.py @@ -1,7 +1,8 @@ import warnings from opto import trace from opto.trace.modules import Module -from opto.trainer.utils import async_run, DefaultLogger +from opto.trainer.utils import async_run +from opto.trainer.loggers import DefaultLogger import os diff --git a/opto/trainer/loggers.py b/opto/trainer/loggers.py new file mode 100644 index 00000000..cc14b657 --- /dev/null +++ b/opto/trainer/loggers.py @@ -0,0 +1,70 @@ + + +class BaseLogger: + + def log(self, name, data, step, **kwargs): + """Log a message with the given name and data at the specified step. + + Args: + name: Name of the metric + data: Value of the metric + step: Current step/iteration + **kwargs: Additional arguments (e.g., color) + """ + raise NotImplementedError("Subclasses should implement this method.") + + +class ConsoleLogger(BaseLogger): + """A simple logger that prints messages to the console.""" + + def log(self, name, data, step, **kwargs): + """Log a message to the console. + + Args: + name: Name of the metric + data: Value of the metric + step: Current step/iteration + **kwargs: Additional arguments (e.g., color) + """ + color = kwargs.get('color', None) + # Simple color formatting for terminal output + color_codes = { + 'green': '\033[92m', + 'red': '\033[91m', + 'blue': '\033[94m', + 'end': '\033[0m' + } + + start_color = color_codes.get(color, '') + end_color = color_codes['end'] if color in color_codes else '' + + print(f"[Step {step}] {start_color}{name}: {data}{end_color}") + + +class TensorboardLogger(BaseLogger): + """A logger that writes metrics to TensorBoard.""" + + def __init__(self, log_dir): + # Late import to avoid dependency issues + try: + from tensorboardX import SummaryWriter + except ImportError: + # try importing from torch.utils.tensorboard if tensorboardX is not available + from torch.utils.tensorboard import SummaryWriter + + self.writer = SummaryWriter(log_dir) + + def log(self, name, data, step, **kwargs): + """Log a message to TensorBoard. + + Args: + name: Name of the metric + data: Value of the metric + step: Current step/iteration + **kwargs: Additional arguments (not used here) + """ + self.writer.add_scalar(name, data, step) + +# TODO add wandb logger + +DefaultLogger = ConsoleLogger \ No newline at end of file diff --git a/opto/trainer/utils.py b/opto/trainer/utils.py index b8dad65c..717ff23b 100644 --- a/opto/trainer/utils.py +++ b/opto/trainer/utils.py @@ -47,33 +47,6 @@ async def _run(): return asyncio.run(_run()) -class DefaultLogger: - """A simple logger that prints messages to the console.""" - - def log(self, name, data, step, **kwargs): - """Log a message to the console. - - Args: - name: Name of the metric - data: Value of the metric - step: Current step/iteration - **kwargs: Additional arguments (e.g., color) - """ - color = kwargs.get('color', None) - # Simple color formatting for terminal output - color_codes = { - 'green': '\033[92m', - 'red': '\033[91m', - 'blue': '\033[94m', - 'end': '\033[0m' - } - - start_color = color_codes.get(color, '') - end_color = color_codes['end'] if color in color_codes else '' - - print(f"[Step {step}] {start_color}{name}: {data}{end_color}") - - if __name__ == "__main__": def tester(t): # regular time-consuming function diff --git a/setup.py b/setup.py index 5ab3a9a1..97c24f1b 100644 --- a/setup.py +++ b/setup.py @@ -14,8 +14,9 @@ "litellm", "black", "scikit-learn", + "tensorboardX" ] - + setuptools.setup( name="trace-opt", version=__version__, From 8211786b76f3237f27207124bada7272d1297853 Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 10 Jun 2025 17:16:31 +0000 Subject: [PATCH 011/314] Add an example of using trainer. Add a flag to disable using json_object format in OptoPrime. --- .../gsm8k_trainer_example.py | 55 ++++++----- opto/optimizers/__init__.py | 6 +- opto/optimizers/optoprime.py | 12 +-- tests/llm_optimizers_tests/test_trainer.py | 94 ------------------- 4 files changed, 42 insertions(+), 125 deletions(-) rename tests/llm_optimizers_tests/test_trainer_refactored.py => examples/gsm8k_trainer_example.py (54%) delete mode 100644 tests/llm_optimizers_tests/test_trainer.py diff --git a/tests/llm_optimizers_tests/test_trainer_refactored.py b/examples/gsm8k_trainer_example.py similarity index 54% rename from tests/llm_optimizers_tests/test_trainer_refactored.py rename to examples/gsm8k_trainer_example.py index 74f1993b..02ad6f65 100644 --- a/tests/llm_optimizers_tests/test_trainer_refactored.py +++ b/examples/gsm8k_trainer_example.py @@ -2,16 +2,16 @@ import numpy as np from opto import trace from opto.utils.llm import LLM, LiteLLM -from opto.optimizers.utils import print_color from opto.optimizers import OptoPrime -from opto.trainer.algorithms.basic_algorithms import BatchedFeedback +from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm +from opto.trainer.loggers import DefaultLogger from opto.trainer.guide import VerbalJudgeGuide from typing import Any @trace.model class Learner: - # A basic LLM agent. + """ A basic LLM agent. """ def __init__(self, system_prompt: str = "You're a helpful agent", user_prompt_template: str = "Query: {message}", @@ -22,9 +22,15 @@ def __init__(self, system_prompt: str = "You're a helpful agent", @trace.bundle() def model(self, system_prompt: str, user_prompt_template: str, message: str) -> str: - """ Call the LLM model. system_prompt specifies - the behavior of the agent. user prompt is the input to the agent, which - is formatted as user_prompt_template.format(message=message).""" + """Call the LLM model. + + Args: + system_prompt: the system prompt to the agent. By tuning this prompt, we can control the behavior of the agent. For example, it can be used to provide instructions to the agent (such as how to reason about the problem, how to answer the question), or provide in-context examples of how to solve the problem. + user_prompt_template: the user prompt template to the agent. It is used as formatting the input to the agent as user_prompt_template.format(message=message). + message: the input to the agent. It can be a query, a task, a code, etc. + Returns: + The response from the agent. + """ if '{message}' not in user_prompt_template: raise ValueError("user_prompt_template must contain '{message}'") @@ -39,9 +45,9 @@ def forward(self, message: Any) -> Any: """ Forward pass of the agent. """ return self.model(self.system_prompt, self.user_prompt_template, message) -class Logger: - def log(self, *messages, color=None, **kwargs): - print_color(messages, color=color) + +Guide = VerbalJudgeGuide +Logger = DefaultLogger def main(): @@ -49,32 +55,35 @@ def main(): seed = 42 num_epochs = 1 batch_size = 1 - eval_frequency = 1 - teacher_model = "gpt-4o-mini" #"gpt-4o-mini_2024-07-18" - student_model = "gpt-35-turbo_1106" + eval_frequency = -1 + teacher_model = None + student_model = None np.random.seed(seed) - train_dataset = datasets.load_dataset('openai/gsm8k', 'main')['train'][ - :10] # NOTE for now, we train on a smaller portion + # In this example, we use the GSM8K dataset, which is a dataset of math word problems. + # We will look the training error of the agent on a small portion of this dataset. + train_dataset = datasets.load_dataset('openai/gsm8k', 'main')['train'][:10] train_dataset = dict(inputs=train_dataset['question'], infos=train_dataset['answer']) - test_dataset = train_dataset # NOTE for now, we just look at training error - - agent = Learner(llm=LiteLLM(model="gpt-3.5-turbo")) - - guide = VerbalJudgeGuide(model=teacher_model) + test_dataset = train_dataset - alg = BatchedFeedback(agent=agent, - optimizer=OptoPrime(agent.parameters()), - logger=Logger()) + agent = Learner(llm=LLM(student_model)) + guide = Guide(model=teacher_model) + optimizer = OptoPrime(agent.parameters()) + alg = MinibatchAlgorithm( + agent=agent, + optimizer=optimizer, + logger=Logger()) + alg.train(guide, train_dataset, num_epochs=num_epochs, batch_size=batch_size, eval_frequency=eval_frequency, test_dataset=test_dataset, - num_threads=3) + num_threads=3, + verbose=True,) if __name__ == "__main__": diff --git a/opto/optimizers/__init__.py b/opto/optimizers/__init__.py index e40f9c36..9b0b2007 100644 --- a/opto/optimizers/__init__.py +++ b/opto/optimizers/__init__.py @@ -1,7 +1,9 @@ -from opto.optimizers.optoprime import OptoPrime +from opto.optimizers.optoprime import OptoPrime as OptoPrimeV1 from opto.optimizers.optoprimemulti import OptoPrimeMulti from opto.optimizers.opro import OPRO from opto.optimizers.textgrad import TextGrad from opto.optimizers.optoprime_v2 import OptoPrimeV2 -__all__ = ["OPRO", "OptoPrime", "OptoPrimeMulti", "TextGrad", "OptoPrimeV2"] \ No newline at end of file +OptoPrime = OptoPrimeV1 + +__all__ = ["OPRO", "OptoPrime", "OptoPrimeMulti", "TextGrad", "OptoPrimeV2", "OptoPrimeV1"] \ No newline at end of file diff --git a/opto/optimizers/optoprime.py b/opto/optimizers/optoprime.py index faa52df6..5a5c5c36 100644 --- a/opto/optimizers/optoprime.py +++ b/opto/optimizers/optoprime.py @@ -259,6 +259,7 @@ def __init__( max_tokens=4096, log=True, prompt_symbols=None, + use_json_object_format=True, # whether to use json object format for the response when calling LLM **kwargs, ): super().__init__(parameters, *args, propagator=propagator, **kwargs) @@ -294,6 +295,7 @@ def __init__( self.prompt_symbols = copy.deepcopy(self.default_prompt_symbols) if prompt_symbols is not None: self.prompt_symbols.update(prompt_symbols) + self.use_json_object_format = use_json_object_format def default_propagator(self): """Return the default Propagator object of the optimizer.""" @@ -557,15 +559,13 @@ def call_llm( {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, ] - + + response_format = {"type": "json_object"} if self.use_json_object_format else None try: # Try tp force it to be a json object - response = self.llm( - messages=messages, - response_format={"type": "json_object"}, - max_tokens=max_tokens, - ) + response = self.llm(messages=messages, max_tokens=max_tokens, response_format=response_format) except Exception: response = self.llm(messages=messages, max_tokens=max_tokens) + response = response.choices[0].message.content if verbose: diff --git a/tests/llm_optimizers_tests/test_trainer.py b/tests/llm_optimizers_tests/test_trainer.py deleted file mode 100644 index 3f88cccb..00000000 --- a/tests/llm_optimizers_tests/test_trainer.py +++ /dev/null @@ -1,94 +0,0 @@ -import datasets -import numpy as np -from opto import trace -from opto.utils.llm import AutoGenLLM -from opto.optimizers.utils import print_color -from opto.optimizers import OptoPrime -from opto.trainer import train -from typing import Any - - -@trace.model -class Student: - # A basic LLM agent. - - def __init__(self, system_prompt: str = "You're a helpful agent", - user_prompt_template: str = "Query: {message}", - llm: AutoGenLLM = None): - self.system_prompt = trace.node(system_prompt, trainable=True) - self.user_prompt_template = trace.node(user_prompt_template) - self.llm = llm or AutoGenLLM() - - @trace.bundle() - def model(self, system_prompt: str, user_prompt_template: str, message: str) -> str: - """ Call the LLM model. system_prompt specifies - the behavior of the agent. user prompt is the input to the agent, which - is formatted as user_prompt_template.format(message=message).""" - - if '{message}' not in user_prompt_template: - raise ValueError("user_prompt_template must contain '{message}'") - - response = self.llm( - messages = [{"role": "system", "content": system_prompt}, - {"role": "user", "content": user_prompt_template.format(message=message)}] - ) - return response.choices[0].message.content - - def forward(self, message: Any) -> Any: - """ Forward pass of the agent. """ - return self.model(self.system_prompt, self.user_prompt_template, message) - - -def teacher(student_answer, info, model="gpt-4o-mini_2024-07-18"): - """ Use LLM to evaluate the student answer. """ - llm = AutoGenLLM(filter_dict={"model": [model]}) - system_prompt = "You're a match teacher who helps students to learn. " - user_prompt_template = "The student answered: {}. The correct answer is {}. If the student answer is correct, please say 'Correct [TERMINATE]'. Otherwise, if the student answer is incorrect, please provide feedback to the student. The feedback should be specific and actionable." - true_answer = info - - response = llm( - messages = [{"role": "system", "content": system_prompt}, - {"role": "user", "content": user_prompt_template.format(student_answer, true_answer)}] - ) - - response = response.choices[0].message.content - score = 1 if 'Correct [TERMINATE]' in response else 0 - return score, response - - - -class Logger: - def log(self, message, color=None, **kwargs): - print_color(message, color=color) - - - -def main(): - # set seed - seed = 42 - num_epochs = 1 - batch_size = 1 - eval_frequency = 1 - teacher_model = "gpt-4o-mini_2024-07-18" - student_model = "gpt-35-turbo_1106" - - np.random.seed(seed) - - train_dataset = datasets.load_dataset('openai/gsm8k', 'main')['train'][:10] # NOTE for now, we train on a smaller portion - train_dataset = dict(inputs=train_dataset['question'], infos=train_dataset['answer']) - test_dataset = train_dataset # NOTE for now, we just look at training error - - - train(agent=Student(llm=AutoGenLLM(filter_dict={"model": ["gpt-35-turbo_1106"]})), - teacher=lambda *args, **kwargs : teacher(model=teacher_model, *args, **kwargs), - train_dataset=train_dataset, - num_epochs=num_epochs, - logger=Logger(), - batch_size=batch_size, - test_dataset=test_dataset, - eval_frequency=eval_frequency - ) - - -if __name__ == "__main__": - main() \ No newline at end of file From 9f0220cbbd50bed42bde4c85c92575564f7c2921 Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 10 Jun 2025 17:41:21 +0000 Subject: [PATCH 012/314] Finish implementation of Tensorboard logger. Add a flag to VerbalJudgeGuide to return LLM's response directly. --- examples/gsm8k_trainer_example.py | 15 +++++++++------ opto/trainer/guide.py | 25 ++++++++++++++++++------- opto/trainer/loggers.py | 22 ++++++++++++++++++---- setup.py | 3 ++- 4 files changed, 47 insertions(+), 18 deletions(-) diff --git a/examples/gsm8k_trainer_example.py b/examples/gsm8k_trainer_example.py index 02ad6f65..61c604f0 100644 --- a/examples/gsm8k_trainer_example.py +++ b/examples/gsm8k_trainer_example.py @@ -4,7 +4,7 @@ from opto.utils.llm import LLM, LiteLLM from opto.optimizers import OptoPrime from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm -from opto.trainer.loggers import DefaultLogger +from opto.trainer.loggers import DefaultLogger, TensorboardLogger from opto.trainer.guide import VerbalJudgeGuide from typing import Any @@ -47,7 +47,7 @@ def forward(self, message: Any) -> Any: Guide = VerbalJudgeGuide -Logger = DefaultLogger +Logger = TensorboardLogger def main(): @@ -56,8 +56,9 @@ def main(): num_epochs = 1 batch_size = 1 eval_frequency = -1 - teacher_model = None - student_model = None + verbose = True + teacher_model = None # use default mode + student_model = None # use default mode np.random.seed(seed) @@ -70,11 +71,13 @@ def main(): agent = Learner(llm=LLM(student_model)) guide = Guide(model=teacher_model) optimizer = OptoPrime(agent.parameters()) + logger = Logger(verbose=verbose) + # set use_json_object_format=False if LLM does not support JSON object format alg = MinibatchAlgorithm( agent=agent, optimizer=optimizer, - logger=Logger()) + logger=logger) alg.train(guide, train_dataset, @@ -83,7 +86,7 @@ def main(): eval_frequency=eval_frequency, test_dataset=test_dataset, num_threads=3, - verbose=True,) + verbose='output' if verbose else False) if __name__ == "__main__": diff --git a/opto/trainer/guide.py b/opto/trainer/guide.py index 5a11dcce..30c428a6 100644 --- a/opto/trainer/guide.py +++ b/opto/trainer/guide.py @@ -53,22 +53,26 @@ class VerbalJudgeGuide(AutoGuide): This is an implementation of LLM-as-a-judge. """ + DEFAULT_CORRECTNESS_TEMPLATE = "Correct [TERMINATE]" + DEFAULT_INCORRECTNESS_TEMPLATE = "Incorrect" + DEFAULT_PROMPT_TEMPLATE = ( - "The query is: {query}. The student answered: {response}. The correct answer is: {reference}. " - "If the student answer is correct, please say 'Correct [TERMINATE]'. " - "Otherwise, if the student answer is incorrect, please provide feedback to the student. " + "The query is: {query}.\n\n\nThe student answered: {response}.\n\n\nThe correct answer is: {reference}.\n\n\n" + "Reason whether the student answer is correct. If the student answer is correct, please say {correctness_template}. " + "Otherwise, if the student answer is incorrect, say {incorrectness_template} and provide feedback to the student. " "The feedback should be specific and actionable." ) DEFAULT_SYSTEM_PROMPT = "You're a helpful teacher who provides clear and constructive feedback." - DEFAULT_CORRECTNESS_TEMPLATE = "Correct [TERMINATE]" def __init__(self, model: Optional[str] = None, llm: Optional[AbstractModel] = None, prompt_template: Optional[str] = None, system_prompt: Optional[str] = None, - correctness_template: Optional[str] = None): + correctness_template: Optional[str] = None, + use_formatted_response: bool = True + ): """ Initialize the VerbalGuide with an LLM and prompt templates. @@ -78,12 +82,14 @@ def __init__(self, prompt_template: Custom prompt template with {response} and {reference} placeholders system_prompt: Custom system prompt for the LLM correctness_template: Template to use when response is deemed correct by metric + use_formatted_response: Whether to format the response with additional context; if False, the raw LLM response is returned """ self.model = model self.llm = llm or LLM(model=model) self.prompt_template = prompt_template or self.DEFAULT_PROMPT_TEMPLATE self.system_prompt = system_prompt or self.DEFAULT_SYSTEM_PROMPT self.correctness_template = correctness_template or self.DEFAULT_CORRECTNESS_TEMPLATE + self.use_formatted_response = use_formatted_response def get_feedback(self, query: str, response: str, reference: Optional[str] = None, **kwargs) -> Tuple[float, str]: """ @@ -103,7 +109,12 @@ def get_feedback(self, query: str, response: str, reference: Optional[str] = Non raise ValueError("ReferenceGuide requires reference information to generate feedback") # Check if metric function indicates perfect match - user_prompt = self.prompt_template.format(query=query, response=response, reference=reference) + user_prompt = self.prompt_template.format( + query=query, + response=response, + reference=reference, + correctness_template=self.DEFAULT_CORRECTNESS_TEMPLATE, + incorrectness_template=self.DEFAULT_INCORRECTNESS_TEMPLATE) messages = [ {"role": "system", "content": self.system_prompt}, @@ -128,7 +139,7 @@ def get_feedback(self, query: str, response: str, reference: Optional[str] = Non score = 1 if 'Correct [TERMINATE]' in llm_response else 0 - return score, formatted_response + return score, formatted_response if self.use_formatted_response else llm_response def forward(self, task: str, response: str, info: Any, **kwargs) -> Tuple[float, str]: score, feedback = self.get_feedback(task, response, info, **kwargs) diff --git a/opto/trainer/loggers.py b/opto/trainer/loggers.py index cc14b657..5f82a4ac 100644 --- a/opto/trainer/loggers.py +++ b/opto/trainer/loggers.py @@ -2,6 +2,11 @@ class BaseLogger: + def __init__(self, log_dir='./logs', **kwargs): + """Initialize the logger. This method can be overridden by subclasses.""" + self.log_dir = log_dir + pass + def log(self, name, data, step, **kwargs): """Log a message with the given name and data at the specified step. @@ -41,10 +46,12 @@ def log(self, name, data, step, **kwargs): print(f"[Step {step}] {start_color}{name}: {data}{end_color}") -class TensorboardLogger(BaseLogger): +class TensorboardLogger(ConsoleLogger): """A logger that writes metrics to TensorBoard.""" - def __init__(self, log_dir): + def __init__(self, log_dir='./logs', verbose=True, **kwargs): + super().__init__(log_dir, **kwargs) + self.verbose = verbose # Late import to avoid dependency issues try: from tensorboardX import SummaryWriter @@ -52,7 +59,7 @@ def __init__(self, log_dir): # try importing from torch.utils.tensorboard if tensorboardX is not available from torch.utils.tensorboard import SummaryWriter - self.writer = SummaryWriter(log_dir) + self.writer = SummaryWriter(self.log_dir) def log(self, name, data, step, **kwargs): """Log a message to TensorBoard. @@ -63,7 +70,14 @@ def log(self, name, data, step, **kwargs): step: Current step/iteration **kwargs: Additional arguments (not used here) """ - self.writer.add_scalar(name, data, step) + if self.verbose: + super().log(name, data, step, **kwargs) + if isinstance(data, str): + # If data is a string, log it as text + self.writer.add_text(name, data, step) + else: + # Otherwise, log it as a scalar + self.writer.add_scalar(name, data, step) # TODO add wandb logger diff --git a/setup.py b/setup.py index 97c24f1b..4fa7eef5 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,8 @@ "litellm", "black", "scikit-learn", - "tensorboardX" + "tensorboardX", + "tensorboard" ] setuptools.setup( From 81eecea1396ee69c0ddf0088b96b189385c6a950 Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 10 Jun 2025 18:40:32 +0000 Subject: [PATCH 013/314] Fix typos --- examples/gsm8k_trainer_example.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/examples/gsm8k_trainer_example.py b/examples/gsm8k_trainer_example.py index 61c604f0..369eeec4 100644 --- a/examples/gsm8k_trainer_example.py +++ b/examples/gsm8k_trainer_example.py @@ -57,8 +57,9 @@ def main(): batch_size = 1 eval_frequency = -1 verbose = True - teacher_model = None # use default mode - student_model = None # use default mode + teacher_model = None # use default model + student_model = None # use default model + optimizer_model = None # use default model np.random.seed(seed) @@ -69,8 +70,8 @@ def main(): test_dataset = train_dataset agent = Learner(llm=LLM(student_model)) - guide = Guide(model=teacher_model) - optimizer = OptoPrime(agent.parameters()) + guide = Guide(model=LLM(teacher_model)) + optimizer = OptoPrime(agent.parameters(), llm=LiteLLM(optimizer_model)) logger = Logger(verbose=verbose) # set use_json_object_format=False if LLM does not support JSON object format From 26544d39bcfed2d3ca94344a2fb359c138e726f5 Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 10 Jun 2025 19:29:49 +0000 Subject: [PATCH 014/314] Fix a bug that projections is private in ParameterNode. --- opto/trace/nodes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opto/trace/nodes.py b/opto/trace/nodes.py index ebfd4153..31ec9da9 100644 --- a/opto/trace/nodes.py +++ b/opto/trace/nodes.py @@ -2036,7 +2036,7 @@ def __init__( assert all( isinstance(p, Projection) for p in projections ), "All projections must be instances of Projection." - self._projections = projections + self.projections = projections def __str__(self) -> str: # str(node) allows us to look up in the feedback dictionary easily From 6a86377636730c9b86f1d709e8cc31536a09858e Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 10 Jun 2025 19:37:41 +0000 Subject: [PATCH 015/314] Fix the bug of missing self.projections in ParameterNode --- opto/trace/nodes.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/opto/trace/nodes.py b/opto/trace/nodes.py index 31ec9da9..5764cb72 100644 --- a/opto/trace/nodes.py +++ b/opto/trace/nodes.py @@ -2028,6 +2028,7 @@ def __init__( info=info, ) self._dependencies["parameter"].add(self) + if projections is not None: assert isinstance( projections, list @@ -2037,6 +2038,8 @@ def __init__( isinstance(p, Projection) for p in projections ), "All projections must be instances of Projection." self.projections = projections + else: + self.projections = [] def __str__(self) -> str: # str(node) allows us to look up in the feedback dictionary easily From c3669e750d3504e529bf4d11fa4427c94a08af9f Mon Sep 17 00:00:00 2001 From: windweller Date: Tue, 10 Jun 2025 13:13:00 -0700 Subject: [PATCH 016/314] add a __call__ method --- opto/trace/projections/projections.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/opto/trace/projections/projections.py b/opto/trace/projections/projections.py index 4c799f3a..f1a802c8 100644 --- a/opto/trace/projections/projections.py +++ b/opto/trace/projections/projections.py @@ -9,6 +9,18 @@ class Projection: def __init__(self, *args, **kwargs): pass + def __call__(self, x: ParameterNode) -> ParameterNode: + """ + Call the projection method on the parameter node `x`. + + Args: + x: The parameter node to project. + + Returns: + The projected parameter node. + """ + return self.project(x) + def project(self, x: ParameterNode) -> ParameterNode: """ Project the parameter node `x` onto the feasible set. From 03b935e65e7428629aafddb4b174aa84dc6f6e5a Mon Sep 17 00:00:00 2001 From: windweller Date: Tue, 10 Jun 2025 13:15:22 -0700 Subject: [PATCH 017/314] fix a nodes.py import issue (misspelling of package) --- opto/trace/__init__.py | 1 + opto/trace/nodes.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/opto/trace/__init__.py b/opto/trace/__init__.py index ddf2a778..ddf01300 100644 --- a/opto/trace/__init__.py +++ b/opto/trace/__init__.py @@ -4,6 +4,7 @@ from opto.trace.broadcast import apply_op import opto.trace.propagators as propagators import opto.trace.operators as operators +import opto.trace.projections as projections from opto.trace.nodes import Node, GRAPH from opto.trace.nodes import node diff --git a/opto/trace/nodes.py b/opto/trace/nodes.py index ebfd4153..c159624d 100644 --- a/opto/trace/nodes.py +++ b/opto/trace/nodes.py @@ -2032,7 +2032,7 @@ def __init__( assert isinstance( projections, list ), "Projections must be a list of Projection objects." - from opto.trace.projection import Projection + from opto.trace.projections import Projection assert all( isinstance(p, Projection) for p in projections ), "All projections must be instances of Projection." From a6993a19eaee1407a3b07784f545f3b2d0f9ba28 Mon Sep 17 00:00:00 2001 From: windweller Date: Tue, 10 Jun 2025 13:26:58 -0700 Subject: [PATCH 018/314] fix an error that projections were not passed into the ParameterNode in bundle --- opto/trace/bundle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opto/trace/bundle.py b/opto/trace/bundle.py index 570c0833..db51f8eb 100644 --- a/opto/trace/bundle.py +++ b/opto/trace/bundle.py @@ -72,7 +72,7 @@ def decorator(fun): allow_external_dependencies=allow_external_dependencies, overwrite_python_recursion=overwrite_python_recursion, _ldict=prev_f_locals, # Get the locals of the calling function - projections=None, + projections=projections, ) return fun_module From f7d4bb17c62be7b2377e532c7569c3ad628c8dce Mon Sep 17 00:00:00 2001 From: windweller Date: Tue, 10 Jun 2025 14:36:34 -0700 Subject: [PATCH 019/314] initial commit --- opto/trace/modules.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/opto/trace/modules.py b/opto/trace/modules.py index a85d1efb..89176864 100644 --- a/opto/trace/modules.py +++ b/opto/trace/modules.py @@ -1,6 +1,8 @@ import os import pickle import copy +import inspect +import textwrap from opto.trace.containers import ParameterContainer from opto.trace.nodes import ParameterNode @@ -11,7 +13,22 @@ def model(cls): """ class ModelWrapper(cls, Module): - pass + def model_dump(self, filename): + methods = [ + method for name, method in cls.__dict__.items() + if inspect.isfunction(method) + ] + + with open(filename, "w") as f: + f.write(f"class {cls.__name__}:\n") + + for i, method in enumerate(methods): + source = inspect.getsource(method) + source = textwrap.dedent(source) + indented = textwrap.indent(source, " ") + f.write(indented) + if i < len(methods) - 1: + f.write("\n") # only one newline between methods return ModelWrapper @@ -25,8 +42,8 @@ def forward(self, *args, **kwargs): def __call__(self, *args, **kwargs): return self.forward(*args, **kwargs) - def save(self, file_name): - """Save the parameters of the model to a file.""" + def save(self, file_name: str): + """Save the parameters of the model to a pickle file.""" # detect if the directory exists directory = os.path.dirname(file_name) if directory != "": @@ -35,7 +52,7 @@ def save(self, file_name): pickle.dump(copy.deepcopy(self.parameters_dict()), f) def load(self, file_name): - """Load the parameters of the model from a file.""" + """Load the parameters of the model from a pickle file.""" with open(file_name, "rb") as f: loaded_data = pickle.load(f) self._set(loaded_data) @@ -62,4 +79,4 @@ def _set(self, new_parameters): parameters_dict[k]._set(v) else: # if the parameter does not exist assert k not in self.__dict__ - setattr(self, k, v) + setattr(self, k, v) \ No newline at end of file From 9479b38cf51b8331bff66afd467961e97bd1719a Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 10 Jun 2025 23:48:36 +0000 Subject: [PATCH 020/314] Fix some import issues due to updates in experimental. --- examples/example_usage_trainer.py | 4 ++-- opto/trainer/algorithms/UCBsearch.py | 2 +- opto/trainer/algorithms/beamsearch_algorithm.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/example_usage_trainer.py b/examples/example_usage_trainer.py index 7f4f2f9d..32c6db70 100644 --- a/examples/example_usage_trainer.py +++ b/examples/example_usage_trainer.py @@ -13,11 +13,11 @@ from opto.optimizers import OptoPrime from opto.optimizers.utils import print_color from opto.trace.modules import Module -from opto.trainer.algorithms.basic_algorithm import MinibatchAlgorithm, BasicSearchAlgorithm +from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm, BasicSearchAlgorithm from opto.trainer.algorithms.beamsearch_algorithm import BeamsearchAlgorithm, BeamsearchHistoryAlgorithm from opto.trainer.algorithms.UCBsearch import UCBSearchAlgorithm, HybridUCB_LLM, UCBSearchFunctionApproximationAlgorithm from opto.trainer.guide import AutoGuide -from opto.trainer.utils import DefaultLogger +from opto.trainer.loggers import DefaultLogger from opto.utils.llm import LLM, LiteLLM # Set default model diff --git a/opto/trainer/algorithms/UCBsearch.py b/opto/trainer/algorithms/UCBsearch.py index 3e08aef6..d6460418 100644 --- a/opto/trainer/algorithms/UCBsearch.py +++ b/opto/trainer/algorithms/UCBsearch.py @@ -11,7 +11,7 @@ from opto import trace from opto.trainer.utils import async_run # Assuming print_color is in utils from opto.optimizers.utils import print_color -from opto.trainer.algorithms.basic_algorithm import MinibatchAlgorithm, evaluate, batchify # evaluate and batchify might be useful +from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm, evaluate, batchify # evaluate and batchify might be useful from opto.utils.llm import LiteLLM # For the selector LLM from opto.trace.nodes import ParameterNode diff --git a/opto/trainer/algorithms/beamsearch_algorithm.py b/opto/trainer/algorithms/beamsearch_algorithm.py index 2d63f5a1..09a13578 100644 --- a/opto/trainer/algorithms/beamsearch_algorithm.py +++ b/opto/trainer/algorithms/beamsearch_algorithm.py @@ -3,7 +3,7 @@ from typing import Union, List, Tuple, Dict, Any, Optional from opto.trainer.utils import async_run from opto.optimizers.utils import print_color -from opto.trainer.algorithms.basic_algorithm import MinibatchAlgorithm, evaluate, batchify +from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm, evaluate, batchify class BeamsearchAlgorithm(MinibatchAlgorithm): From 9de00140ffe5e33f6f8fc947b24c4ce22b45778c Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 10 Jun 2025 23:52:17 +0000 Subject: [PATCH 021/314] Rename file. --- examples/{example_usage_trainer.py => search_algo_example.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/{example_usage_trainer.py => search_algo_example.py} (100%) diff --git a/examples/example_usage_trainer.py b/examples/search_algo_example.py similarity index 100% rename from examples/example_usage_trainer.py rename to examples/search_algo_example.py From 978c762f9750f9469ccda1d9391bfa82970b305b Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 10 Jun 2025 23:59:36 +0000 Subject: [PATCH 022/314] Fix a bug in CustomLLM's attributes not defined with model not being None. --- opto/utils/llm.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/opto/utils/llm.py b/opto/utils/llm.py index a84e4865..6f7f87c4 100644 --- a/opto/utils/llm.py +++ b/opto/utils/llm.py @@ -207,11 +207,11 @@ def __init__(self, model: Union[str, None] = None, reset_freq: Union[int, None] cache=True) -> None: if model is None: model = os.environ.get('TRACE_CUSTOMLLM_MODEL', 'gpt-4o') - base_url = os.environ.get('TRACE_CUSTOMLLM_URL', 'http://xx.xx.xxx.xx:4000/') - server_api_key = os.environ.get('TRACE_CUSTOMLLM_API_KEY', - 'sk-Xhg...') # we assume the server has an API key - # the server API is set through `master_key` in `config.yaml` for LiteLLM proxy server - + base_url = os.environ.get('TRACE_CUSTOMLLM_URL', 'http://xx.xx.xxx.xx:4000/') + server_api_key = os.environ.get('TRACE_CUSTOMLLM_API_KEY', + 'sk-Xhg...') # we assume the server has an API key + # the server API is set through `master_key` in `config.yaml` for LiteLLM proxy server + self.model_name = model self.cache = cache factory = lambda: self._factory(base_url, server_api_key) # an LLM instance uses a fixed model From f5327d69472cd9ad057c6b0169c5464e7b211b26 Mon Sep 17 00:00:00 2001 From: windweller Date: Tue, 10 Jun 2025 17:21:34 -0700 Subject: [PATCH 023/314] push the workable version (without unit test code yet) --- opto/trace/modules.py | 70 +++++++++++++++++++++++++++++++++---------- 1 file changed, 55 insertions(+), 15 deletions(-) diff --git a/opto/trace/modules.py b/opto/trace/modules.py index 89176864..bdfbcda3 100644 --- a/opto/trace/modules.py +++ b/opto/trace/modules.py @@ -3,8 +3,9 @@ import copy import inspect import textwrap -from opto.trace.containers import ParameterContainer +from opto.trace.containers import ParameterContainer, trainable_method from opto.trace.nodes import ParameterNode +from opto.trace.projections import Projection, BlackCodeFormatter def model(cls): @@ -13,22 +14,61 @@ def model(cls): """ class ModelWrapper(cls, Module): - def model_dump(self, filename): - methods = [ - method for name, method in cls.__dict__.items() - if inspect.isfunction(method) - ] - - with open(filename, "w") as f: - f.write(f"class {cls.__name__}:\n") - - for i, method in enumerate(methods): - source = inspect.getsource(method) + def model_dump(self, filename, projection: Projection = BlackCodeFormatter()): + """Dump the model's source code to a file, including all methods and attributes. + Ignores dunder methods unless they were overridden by the user. + """ + trace_model_body = f"class {cls.__name__}:\n" + + # Get all members of the class + all_members = inspect.getmembers(self) + cls_members = inspect.getmembers(cls) + cls_member_names = [m[0] for m in cls_members] + + # Filter out dunder methods unless they were overridden + filtered_members = [] + for name, member in all_members: + # Skip internal trace reserved members + if name.startswith('__TRACE_RESERVED_'): + continue + + if name not in cls_member_names: + continue + + # Include if it's not a dunder method or if it was overridden + if not name.startswith('__'): + filtered_members.append((name, member)) + elif name.startswith('__'): + # For dunder methods, check if they were overridden + try: + if hasattr(member, '__qualname__') and member.__qualname__.split('.')[0] == cls.__name__: + filtered_members.append((name, member)) + except (AttributeError, TypeError): + # Skip if we can't determine if it was overridden + continue + + # Process each member + for i, (name, member) in enumerate(filtered_members): + if 'FunModule' in str(member): + # Handle methods + source = member.parameter.data + source = textwrap.dedent(source) + indented = textwrap.indent(source, " ") + trace_model_body += indented + else: # this is a class method + source = inspect.getsource(member) source = textwrap.dedent(source) indented = textwrap.indent(source, " ") - f.write(indented) - if i < len(methods) - 1: - f.write("\n") # only one newline between methods + trace_model_body += indented + + if i < len(all_members) - 1: + trace_model_body += "\n" # only one newline between members + + if projection is not None: + trace_model_body = projection.project(trace_model_body) + + with open(filename, "w") as f: + f.write(trace_model_body) return ModelWrapper From e582b50581b22f633e50e88c36341ec51c91f66e Mon Sep 17 00:00:00 2001 From: windweller Date: Tue, 10 Jun 2025 17:24:05 -0700 Subject: [PATCH 024/314] update signature for projection --- opto/trace/projections/projections.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/opto/trace/projections/projections.py b/opto/trace/projections/projections.py index f1a802c8..9be4227c 100644 --- a/opto/trace/projections/projections.py +++ b/opto/trace/projections/projections.py @@ -1,4 +1,4 @@ -from opto.trace.nodes import ParameterNode +from typing import Any class Projection: @@ -9,7 +9,7 @@ class Projection: def __init__(self, *args, **kwargs): pass - def __call__(self, x: ParameterNode) -> ParameterNode: + def __call__(self, x: Any) -> Any: """ Call the projection method on the parameter node `x`. @@ -21,7 +21,7 @@ def __call__(self, x: ParameterNode) -> ParameterNode: """ return self.project(x) - def project(self, x: ParameterNode) -> ParameterNode: + def project(self, x: Any) -> Any: """ Project the parameter node `x` onto the feasible set. """ From cd487e8f49a57a70bb73c045bdc6bc119b288515 Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Tue, 10 Jun 2025 21:29:12 -0500 Subject: [PATCH 025/314] Make prompt templates as atributes of the trainer classes --- opto/trainer/algorithms/UCBsearch.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/opto/trainer/algorithms/UCBsearch.py b/opto/trainer/algorithms/UCBsearch.py index d6460418..a589fcdc 100644 --- a/opto/trainer/algorithms/UCBsearch.py +++ b/opto/trainer/algorithms/UCBsearch.py @@ -399,6 +399,11 @@ class HybridUCB_LLM(MinibatchAlgorithm): If the buffer is full, evicts the candidate with the lowest UCB score. """ + # LLM prompt templates as class attributes for easy customization + SYSTEM_PROMPT_TEMPLATE = "You are an expert in model optimization. Your task is to propose new string values for model parameters with high UCB scores. Please output ONLY a valid JSON dictionary where keys are parameter names and values are the new string values for those parameters, matching the example structure provided. Do not add any explanations or markdown formatting around the JSON." + + USER_PROMPT_TEMPLATE = "Here are some current candidates from the search buffer and their statistics:\n{candidates}\n\nHere is an example of the required JSON output structure (parameter names as keys, new string values as values):\n{example_structure}\n\nPlease generate a new set of parameters in exactly the same JSON format. Make sure use double quotes for the keys and values." + def __init__(self, agent: trace.Module, optimizer, @@ -535,8 +540,8 @@ def _llm_generate_candidate(self) -> Optional[Dict[trace.nodes.ParameterNode, st example_param_structure_json_str = {getattr(p,'py_name'): copy.deepcopy(p.data) for p in self.agent.parameters()} prompt_messages = [ - {"role": "system", "content": "You are an expert in model optimization. Your task is to propose new string values for model parameters with high UCB scores. Please output ONLY a valid JSON dictionary where keys are parameter names and values are the new string values for those parameters, matching the example structure provided. Do not add any explanations or markdown formatting around the JSON."}, - {"role": "user", "content": f"Here are some current candidates from the search buffer and their statistics:\\n{serializable_candidate_summaries}\\n\\nHere is an example of the required JSON output structure (parameter names as keys, new string values as values):\\n{example_param_structure_json_str}\\n\\nPlease generate a new set of parameters in exactly the same JSON format. Make sure use double quotes for the keys and values."} + {"role": "system", "content": self.SYSTEM_PROMPT_TEMPLATE}, + {"role": "user", "content": self.USER_PROMPT_TEMPLATE.format(candidates=serializable_candidate_summaries, example_structure=example_param_structure_json_str)} ] print_color(f"LLM prompt (summary): {len(prompt_candidates)} candidates, structure example provided.", "magenta") @@ -873,6 +878,11 @@ class UCBSearchFunctionApproximationAlgorithm(UCBSearchAlgorithm): UCB Search Algorithm that uses LLM function approximation to select candidates. """ + # LLM prompt templates as class attributes for easy customization + SYSTEM_PROMPT_TEMPLATE = "You are an expert in model optimization. Your task is to propose new string values for model parameters with high UCB scores. Please output ONLY a valid JSON dictionary where keys are parameter names and values are the new string values for those parameters, matching the example structure provided. Do not add any explanations or markdown formatting around the JSON." + + USER_PROMPT_TEMPLATE = "Here are some current candidates from the search buffer and their statistics:\n{candidates}\n\nHere is an example of the required JSON output structure (parameter names as keys, new string values as values):\n{example_structure}\n\nPlease generate a new set of parameters in exactly the same JSON format. Make sure use double quotes for the keys and values." + def __init__(self, llm_model, *args, **kwargs): super().__init__(*args, **kwargs) self.llm_model = llm_model @@ -916,8 +926,8 @@ def _llm_generate_candidate(self) -> Optional[Dict[trace.nodes.ParameterNode, st example_param_structure_json_str = {getattr(p,'py_name'): copy.deepcopy(p.data) for p in self.agent.parameters()} prompt_messages = [ - {"role": "system", "content": "You are an expert in model optimization. Your task is to propose new string values for model parameters with high UCB scores. Please output ONLY a valid JSON dictionary where keys are parameter names and values are the new string values for those parameters, matching the example structure provided. Do not add any explanations or markdown formatting around the JSON."}, - {"role": "user", "content": f"Here are some current candidates from the search buffer and their statistics:\\n{serializable_candidate_summaries}\\n\\nHere is an example of the required JSON output structure (parameter names as keys, new string values as values):\\n{example_param_structure_json_str}\\n\\nPlease generate a new set of parameters in exactly the same JSON format. Make sure use double quotes for the keys and values."} + {"role": "system", "content": self.SYSTEM_PROMPT_TEMPLATE}, + {"role": "user", "content": self.USER_PROMPT_TEMPLATE.format(candidates=serializable_candidate_summaries, example_structure=example_param_structure_json_str)} ] print_color(f"LLM prompt (summary): {len(prompt_candidates)} candidates, structure example provided.", "magenta") From c73ebe152f645e1ea49419b7d50d0e21fd1ef406 Mon Sep 17 00:00:00 2001 From: windweller Date: Sat, 14 Jun 2025 16:45:42 -0700 Subject: [PATCH 026/314] improve `model_dump` to handle `node` attributes and unpack those as well. Added test cases --- opto/trace/modules.py | 15 ++++++ tests/unit_tests/test_modules.py | 93 ++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) diff --git a/opto/trace/modules.py b/opto/trace/modules.py index bdfbcda3..7b8909bc 100644 --- a/opto/trace/modules.py +++ b/opto/trace/modules.py @@ -64,6 +64,21 @@ def model_dump(self, filename, projection: Projection = BlackCodeFormatter()): if i < len(all_members) - 1: trace_model_body += "\n" # only one newline between members + # Replace node initializations with their current values + # WARNING: there might be corner cases that this static analysis does not cover + import re + node_pattern = r'self\.(\w+)\s*=\s*node\([^)]*\)' + + def replace_node(match): + attr_name = match.group(1) + if hasattr(self, attr_name): + attr = getattr(self, attr_name) + if hasattr(attr, 'data'): + return f"self.{attr_name} = {attr.data}" + return match.group(0) # Return original if replacement not possible + + trace_model_body = re.sub(node_pattern, replace_node, trace_model_body) + if projection is not None: trace_model_body = projection.project(trace_model_body) diff --git a/tests/unit_tests/test_modules.py b/tests/unit_tests/test_modules.py index 8cc19893..ff05318d 100644 --- a/tests/unit_tests/test_modules.py +++ b/tests/unit_tests/test_modules.py @@ -156,3 +156,96 @@ def test_multiple_inheritance(): result = child.forward(1) assert result._data == 2 + +# Test cases for model_dump +@model +class DummyClass: + def __init__(self): + super().__init__() + self._param = node(1, trainable=True) + self.regular_attr = "test" + + @bundle(trainable=True) + def regular_method(self, x): + return x + + def __str__(self): + return "DummyClass" + + def __custom__(self): + return "custom" + +@model +class ComplexClass: + def __init__(self): + super().__init__() + self._param = node(1, trainable=True) + self._nested = DummyClass() + + @bundle(trainable=True) + def complex_method(self, x): + return self._nested.regular_method(x) + + def __str__(self): + return "ComplexClass" + +def test_model_dump_basic(): + dummy = DummyClass() + dummy._param._data = 42 # Change the node value + temp_file = "temp_dummy.py" + try: + dummy.model_dump(temp_file) + with open(temp_file, "r") as f: + content = f.read() + # Check if class definition is present + assert "class DummyClass:" in content + # Check if regular method is present + assert "def regular_method" in content + # Check if __str__ is present (overridden dunder) + assert "def __str__" in content + # Check if __custom__ is present (custom dunder) + assert "def __custom__" in content + # Check if regular attribute is present + assert "regular_attr" in content + # Check if node initialization was replaced with current value + assert "self._param = 42" in content + assert "self._param = node(1" not in content + finally: + if os.path.exists(temp_file): + os.remove(temp_file) + +def test_model_dump_complex(): + complex_obj = ComplexClass() + temp_file = "temp_complex.py" + try: + complex_obj.model_dump(temp_file) + with open(temp_file, "r") as f: + content = f.read() + # Check if class definition is present + assert "class ComplexClass:" in content + # Check if complex method is present + assert "def complex_method" in content + # Check if __str__ is present + assert "def __str__" in content + # Check if nested class reference is in the method + assert "self._nested.regular_method" in content + finally: + if os.path.exists(temp_file): + os.remove(temp_file) + +def test_model_dump_with_projection(): + dummy = DummyClass() + temp_file = "temp_dummy_formatted.py" + try: + # Test with BlackCodeFormatter + from opto.trace.projections import BlackCodeFormatter + dummy.model_dump(temp_file, projection=BlackCodeFormatter()) + with open(temp_file, "r") as f: + content = f.read() + # Check if content is properly formatted + assert "class DummyClass:" in content + assert "def regular_method" in content + finally: + if os.path.exists(temp_file): + os.remove(temp_file) + From eb5c3c10bc40ceae364ac716f88f3793d7b5bc53 Mon Sep 17 00:00:00 2001 From: windweller Date: Sat, 14 Jun 2025 17:02:50 -0700 Subject: [PATCH 027/314] fix a bug, added new test cases --- opto/trace/modules.py | 11 +++- tests/unit_tests/test_modules.py | 89 ++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 2 deletions(-) diff --git a/opto/trace/modules.py b/opto/trace/modules.py index 7b8909bc..ee779100 100644 --- a/opto/trace/modules.py +++ b/opto/trace/modules.py @@ -41,7 +41,10 @@ def model_dump(self, filename, projection: Projection = BlackCodeFormatter()): elif name.startswith('__'): # For dunder methods, check if they were overridden try: - if hasattr(member, '__qualname__') and member.__qualname__.split('.')[0] == cls.__name__: + print(cls.__name__, "<>", member.__qualname__) + # MixedClass <> test_model_dump_mixed_trainable..MixedClass.__init__ + # if we wrap it inside a function, the qualname is different than when we dont + if hasattr(member, '__qualname__') and cls.__name__ in member.__qualname__: filtered_members.append((name, member)) except (AttributeError, TypeError): # Skip if we can't determine if it was overridden @@ -49,9 +52,13 @@ def model_dump(self, filename, projection: Projection = BlackCodeFormatter()): # Process each member for i, (name, member) in enumerate(filtered_members): + print(name, member) if 'FunModule' in str(member): # Handle methods - source = member.parameter.data + if member.parameter is not None: + source = member.parameter.data + else: + source = member.info['source'] source = textwrap.dedent(source) indented = textwrap.indent(source, " ") trace_model_body += indented diff --git a/tests/unit_tests/test_modules.py b/tests/unit_tests/test_modules.py index ff05318d..46971917 100644 --- a/tests/unit_tests/test_modules.py +++ b/tests/unit_tests/test_modules.py @@ -249,3 +249,92 @@ def test_model_dump_with_projection(): if os.path.exists(temp_file): os.remove(temp_file) +@model +class NonTrainableClass: + def __init__(self): + super().__init__() + self._param = node(1, trainable=False) + self._param2 = node(2, trainable=False) + self.regular_attr = "test" + + @bundle(trainable=False) + def non_trainable_method(self, x): + return x + + @bundle(trainable=False) + def another_non_trainable(self, y): + return y + 1 + +def test_model_dump_non_trainable(): + obj = NonTrainableClass() + obj._param._data = 10 # Change node value + obj._param2._data = 20 # Change another node value + temp_file = "temp_non_trainable.py" + try: + obj.model_dump(temp_file) + with open(temp_file, "r") as f: + content = f.read() + # Check if class definition is present + assert "class NonTrainableClass:" in content + # Check if node initializations were replaced with current values + assert "self._param = 10" in content + assert "self._param2 = 20" in content + # Verify no node() calls remain + assert "node(" not in content + # Verify no bundle decorators remain + assert "@bundle" not in content + # Check if methods are present but without decorators + assert "def non_trainable_method" in content + assert "def another_non_trainable" in content + # Check if regular attribute is present + assert "regular_attr" in content + finally: + if os.path.exists(temp_file): + os.remove(temp_file) + +def test_model_dump_mixed_trainable(): + + @model + class MixedClass: + def __init__(self): + super().__init__() + self._trainable = node(1, trainable=True) + self._non_trainable = node(2, trainable=False) + self.regular_attr = "test" + + @bundle(trainable=True) + def trainable_method(self, x): + return x + + @bundle(trainable=False) + def non_trainable_method(self, y): + return y + 1 + + + obj = MixedClass() + obj._trainable._data = 100 + obj._non_trainable._data = 200 + + temp_file = "temp_mixed.py" + try: + obj.model_dump(temp_file) + with open(temp_file, "r") as f: + content = f.read() + # Check if class definition is present + assert "class MixedClass:" in content + # Check if all node initializations were replaced + assert "self._trainable = 100" in content + assert "self._non_trainable = 200" in content + # Verify no node() calls remain + assert "node(" not in content + # Verify no bundle decorators remain + assert "@bundle" not in content + # Check if methods are present but without decorators + assert "def trainable_method" in content + assert "def non_trainable_method" in content + # Check if regular attribute is present + assert "regular_attr" in content + finally: + if os.path.exists(temp_file): + os.remove(temp_file) + From 657e8655661fd7709509991ea7f7eb2a2cc39702 Mon Sep 17 00:00:00 2001 From: Xavier Daull Date: Mon, 16 Jun 2025 20:12:05 +0200 Subject: [PATCH 028/314] ADDED: multi-LLM support via LLMFactory (fully backward compatible) and implementation demonstration in OptoPrimeMulti and associated test --- opto/optimizers/optoprime.py | 7 +- opto/optimizers/optoprimemulti.py | 82 ++++++- opto/utils/llm.py | 48 +++- .../test_optimizer_optoprimemulti.py | 209 +++++++++++++++++- 4 files changed, 337 insertions(+), 9 deletions(-) diff --git a/opto/optimizers/optoprime.py b/opto/optimizers/optoprime.py index 6ac4ce95..a804ed88 100644 --- a/opto/optimizers/optoprime.py +++ b/opto/optimizers/optoprime.py @@ -479,7 +479,12 @@ def construct_update_dict( if node.trainable and node.py_name in suggestion: try: from black import format_str, FileMode - formatted_suggestion = suggestion[node.py_name] + # Handle code parameters specially + if "__code" in node.py_name and "code" in suggestion: + formatted_suggestion = suggestion["code"] + else: + formatted_suggestion = suggestion[node.py_name] + # formatted_suggestion = suggestion[node.py_name] # use black formatter for code reformatting if type(formatted_suggestion) == str and 'def' in formatted_suggestion: formatted_suggestion = format_str(formatted_suggestion, mode=FileMode()) diff --git a/opto/optimizers/optoprimemulti.py b/opto/optimizers/optoprimemulti.py index 6134824f..19dadb70 100644 --- a/opto/optimizers/optoprimemulti.py +++ b/opto/optimizers/optoprimemulti.py @@ -19,6 +19,8 @@ def __init__( generation_technique: str = "temperature_variation", selection_technique: str = "best_of_n", experts_list: Optional[List[str]] = None, + llm_profiles: Optional[List[str]] = None, # List of LLM profiles to use + llm_weights: Optional[List[float]] = None, # Weights for each LLM (for weighted selection) **kwargs, ): super().__init__(*args, **kwargs) @@ -31,6 +33,42 @@ def __init__( self.selection_technique = selection_technique self.experts_list = experts_list + # NEW: Multiple LLM support + self.llm_profiles = llm_profiles + self.llm_weights = llm_weights or [1.0] * len(llm_profiles) if llm_profiles else None + self._llm_instances = {} # Cache for LLM instances + + def _get_llm_for_profile(self, profile: str = None): + """Get LLM instance for a profile, with caching.""" + if profile is None: + return self.llm # Use default LLM + + if profile not in self._llm_instances: + try: + from opto.utils.llm import LLMFactory + self._llm_instances[profile] = LLMFactory.get_llm(profile) + except Exception as e: + # Fallback to default LLM if profile creation fails + import warnings + warnings.warn(f"Failed to create LLM for profile '{profile}': {e}. Using default LLM.") + return self.llm + + return self._llm_instances[profile] + + def _get_llms_for_generation(self, num_responses: int): + """Get list of LLMs to use for generation.""" + if self.llm_profiles is None or len(self.llm_profiles) == 0: + # Fallback to single LLM (existing behavior) + return [self.llm] * num_responses + + # Distribute responses across multiple LLMs + llms = [] + for i in range(num_responses): + profile_idx = i % len(self.llm_profiles) + profile = self.llm_profiles[profile_idx] + llm = self._get_llm_for_profile(profile) + llms.append(llm) + def call_llm( self, system_prompt: str, @@ -39,20 +77,24 @@ def call_llm( max_tokens: int = 4096, num_responses: int = 1, temperature: float = 0.0, + llm = None, # NEW: Optional specific LLM to use ) -> List[str]: - """Call the LLM with a prompt and return multiple responses.""" + """Given a prompt, returns multiple candidate answers.""" # if verbose not in (False, "output"): # print("Prompt\n", system_prompt + user_prompt) + # Use provided LLM or fall back to default + active_llm = llm or self.llm + messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, ] try: - if hasattr(self.llm, "create"): + if hasattr(active_llm, "create"): # Standard OpenAI/LangChain style - response = self.llm.create( + response = active_llm.create( messages=messages, response_format={"type": "json_object"}, max_tokens=max_tokens, @@ -62,7 +104,7 @@ def call_llm( else: # Fallback for LiteLLM (callable) or other interfaces # e.g., LiteLLM(messages, max_tokens=…, n=…, temperature=…) - response = self.llm( + response = active_llm( messages, max_tokens=max_tokens, n=num_responses, @@ -165,6 +207,36 @@ def generate_candidates( generation_technique = generation_technique.lower() + if self.llm_profiles is not None and len(self.llm_profiles) > 0 and generation_technique == "multi_llm": + llms = self._get_llms_for_generation(num_responses) + temperatures = [temp_max - i * (temp_max - temp_min) / max(1, num_responses - 1) for i in range(num_responses)] + + # Prepare arguments for parallel execution + arg_dicts = [] + for i, (llm, temp) in enumerate(zip(llms, temperatures)): + profile_name = self.llm_profiles[i % len(self.llm_profiles)] if self.llm_profiles else "default" + modified_system_prompt = f"{system_prompt}\n\n[Using {profile_name} model for diverse perspective]" + + arg_dicts.append(dict( + system_prompt=modified_system_prompt, + user_prompt=user_prompt, + verbose=verbose, + max_tokens=max_tokens, + num_responses=1, + temperature=temp, + llm=llm # Use specific LLM + )) + + # Execute in parallel + try: + parallel_results = self._parallel_call_llm(arg_dicts) + candidates.extend(parallel_results) + except Exception as e: + if verbose: + print(f"Error in multi_llm mode: {e} – falling back to temperature variation") + generation_technique = "temperature_variation" + candidates = [] + if generation_technique == "self_refinement": # Generate solutions by refining previous ones for i in range(num_responses): @@ -292,7 +364,7 @@ def generate_candidates( print("Warning: Failed to generate any candidates") if self.log is not None: - self.log.append({"system_prompt": system_prompt, "user_prompt": user_prompt, "response": candidates, "generation_technique": generation_technique}) + self.log.append({"system_prompt": system_prompt, "user_prompt": user_prompt, "response": candidates, "generation_technique": generation_technique, "llm_profiles": self.llm_profiles}) # only build a problem instance if we actually have one pi = self.problem_instance(summary) if summary is not None else {} self.summary_log.append({"problem_instance": pi, "summary": summary}) diff --git a/opto/utils/llm.py b/opto/utils/llm.py index a84e4865..5039b266 100644 --- a/opto/utils/llm.py +++ b/opto/utils/llm.py @@ -239,6 +239,45 @@ def create(self, **config: Any): "CustomLLM": CustomLLM, } +class LLMFactory: + """Factory for creating LLM instances with predefined profiles.""" + + # Default profiles for different use cases + _profiles = { + 'default': {'backend': 'LiteLLM', 'params': {'model': 'gpt-4o-mini'}}, + 'premium': {'backend': 'LiteLLM', 'params': {'model': 'gpt-4'}}, + 'cheap': {'backend': 'LiteLLM', 'params': {'model': 'gpt-4o-mini'}}, + 'fast': {'backend': 'LiteLLM', 'params': {'model': 'gpt-3.5-turbo-mini'}}, + 'reasoning': {'backend': 'LiteLLM', 'params': {'model': 'o1-mini'}}, + } + + @classmethod + def get_llm(cls, profile: str = 'default') -> AbstractModel: + """Get an LLM instance for the specified profile.""" + if profile not in cls._profiles: + raise ValueError(f"Unknown profile '{profile}'. Available profiles: {list(cls._profiles.keys())}") + + config = cls._profiles[profile] + backend_cls = _LLM_REGISTRY[config['backend']] + return backend_cls(**config['params']) + + @classmethod + def register_profile(cls, name: str, backend: str, **params): + """Register a new LLM profile.""" + cls._profiles[name] = {'backend': backend, 'params': params} + + @classmethod + def list_profiles(cls): + """List all available profiles.""" + return list(cls._profiles.keys()) + + @classmethod + def get_profile_info(cls, profile: str = None): + """Get information about a profile or all profiles.""" + if profile: + return cls._profiles.get(profile) + return cls._profiles + class LLM: """ A unified entry point for all supported LLM backends. @@ -248,8 +287,15 @@ class LLM: llm = LLM() # or override explicitly llm = LLM(backend="AutoGen", config_list=my_configs) + # or use predefined profiles + llm = LLM(profile="premium") # Use premium model + llm = LLM(profile="cheap") # Use cheaper model + llm = LLM(profile="reasoning") # Use reasoning/thinking model """ - def __new__(cls, *args, backend: str = None, **kwargs): + def __new__(cls, *args, profile: str = None, backend: str = None, **kwargs): + # New: if profile is specified, use LLMFactory + if profile: + return LLMFactory.get_llm(profile) # Decide which backend to use name = backend or os.getenv("TRACE_DEFAULT_LLM_BACKEND", "LiteLLM") try: diff --git a/tests/llm_optimizers_tests/test_optimizer_optoprimemulti.py b/tests/llm_optimizers_tests/test_optimizer_optoprimemulti.py index c9acd708..978ae302 100644 --- a/tests/llm_optimizers_tests/test_optimizer_optoprimemulti.py +++ b/tests/llm_optimizers_tests/test_optimizer_optoprimemulti.py @@ -1,6 +1,7 @@ import json import pytest from opto.optimizers.optoprimemulti import OptoPrimeMulti +from opto.utils.llm import LLMFactory from opto.trace.propagators import GraphPropagator from opto.trace.nodes import ParameterNode from opto.trace import bundle, node, GRAPH @@ -25,6 +26,18 @@ def __call__(self, messages, max_tokens=None, response_format=None): # fallback single-call (not used in multi) return self.create(messages, response_format, max_tokens, 1, 0) +class MockLLMFactory: + """Mock LLMFactory for testing multi-LLM functionality""" + @staticmethod + def get_llm(profile): + # Return different dummy LLMs for different profiles + profile_responses = { + 'cheap': [f"cheap_{profile}_response"], + 'premium': [f"premium_{profile}_response"], + 'default': [f"default_{profile}_response"], + } + return DummyLLM(responses=[profile_responses.get(profile, ["default_response"])]) + @pytest.fixture def parameter_node(): # Minimal dummy ParameterNode @@ -40,6 +53,16 @@ def default_optimizer(parameter_node): assert isinstance(opt.propagator, GraphPropagator) return opt +@pytest.fixture +def multi_llm_optimizer(parameter_node): + """Optimizer configured for multi-LLM testing""" + dummy = DummyLLM(responses=[["{\\\"suggestion\\\": {}}"]]) + opt = OptoPrimeMulti([parameter_node], + llm_profiles=['cheap', 'premium', 'default'], + generation_technique='multi_llm') + opt.llm = dummy + return opt + def test_call_llm_returns_list(default_optimizer): opt = default_optimizer # Prepare dummy response @@ -48,11 +71,25 @@ def test_call_llm_returns_list(default_optimizer): assert isinstance(results, list) assert results == ["resp1", "resp2"] +def test_call_llm_with_specific_llm(default_optimizer): + """Test that call_llm accepts and uses a specific LLM instance""" + opt = default_optimizer + specific_llm = DummyLLM(responses=[["specific_response"]]) + + # Call with specific LLM + results = opt.call_llm("sys", "usr", llm=specific_llm, num_responses=1) + assert results == ["specific_response"] + + # Verify specific_llm was called, not the default + assert len(specific_llm.call_args) == 1 + assert len(opt.llm.call_args) == 0 # Default LLM should not be called + @pytest.mark.parametrize("gen_tech", [ "temperature_variation", "self_refinement", "iterative_alternatives", - "multi_experts"] + "multi_experts", + "multi_llm"] ) def test_generate_candidates_length(default_optimizer, gen_tech, capsys): opt = default_optimizer @@ -65,6 +102,55 @@ def test_generate_candidates_length(default_optimizer, gen_tech, capsys): assert isinstance(cands, list) assert len(cands) == 3 +def test_multi_llm_initialization(): + """Test OptoPrimeMulti initialization with multi-LLM parameters""" + param = ParameterNode(name='test', value=1) + profiles = ['cheap', 'premium', 'default'] + weights = [0.5, 1.5, 1.0] + + opt = OptoPrimeMulti([param], + llm_profiles=profiles, + llm_weights=weights, + generation_technique='multi_llm') + + assert opt.llm_profiles == profiles + assert opt.llm_weights == weights + assert opt._llm_instances == {} # Should start empty + +def test_get_llm_for_profile(multi_llm_optimizer, monkeypatch): + """Test LLM profile retrieval and caching""" + opt = multi_llm_optimizer + + # Mock LLMFactory + monkeypatch.setattr('opto.utils.llm.LLMFactory', MockLLMFactory) + + # First call should create and cache + llm1 = opt._get_llm_for_profile('cheap') + assert 'cheap' in opt._llm_instances + + # Second call should return cached instance + llm2 = opt._get_llm_for_profile('cheap') + assert llm1 is llm2 + + # None profile should return default LLM + default_llm = opt._get_llm_for_profile(None) + assert default_llm is opt.llm + +def test_get_llms_for_generation(multi_llm_optimizer, monkeypatch): + """Test LLM distribution for generation""" + opt = multi_llm_optimizer + # Patch the import location where it's actually used + monkeypatch.setattr('opto.optimizers.optoprimemulti.LLMFactory', MockLLMFactory) + + llms = opt._get_llms_for_generation(5) + assert len(llms) == 5 + + # Should cycle through profiles: cheap, premium, default, cheap, premium + expected_profiles = ['cheap', 'premium', 'default', 'cheap', 'premium'] + for i, llm in enumerate(llms): + expected_profile = expected_profiles[i] + assert expected_profile in opt._llm_instances + @pytest.mark.parametrize("sel_tech,method_name", [ ("moa", "_select_moa"), ("majority", "_select_majority"), @@ -85,6 +171,25 @@ def test_select_candidate_calls_correct_method(default_optimizer, sel_tech, meth result = opt.select_candidate(cands, selection_technique=sel_tech) assert result == "c" +def test_multi_llm_generation_fallback(multi_llm_optimizer, monkeypatch): + """Test that multi_llm generation falls back gracefully on error""" + opt = multi_llm_optimizer + + # Mock LLMFactory to raise exception + def failing_get_llm(profile): + raise Exception("LLM creation failed") + + monkeypatch.setattr(MockLLMFactory, 'get_llm', failing_get_llm) + monkeypatch.setattr('opto.utils.llm.LLMFactory', MockLLMFactory) + + # Should fall back to temperature_variation + responses = [["fallback1"], ["fallback2"], ["fallback3"]] + opt.llm = DummyLLM(responses=responses) + + cands = opt.generate_candidates(None, "sys", "usr", num_responses=3, + generation_technique="multi_llm", verbose=True) + assert len(cands) == 3 + def test_integration_step_updates(default_optimizer, parameter_node): opt = default_optimizer # Dummy parameter_node initial value @@ -105,6 +210,83 @@ def test_default_model_name(default_optimizer): assert 'gpt-4.1-nano' in model_name +def test_multi_llm_step_integration(multi_llm_optimizer, parameter_node, monkeypatch): + """Test full integration of multi-LLM optimization step""" + opt = multi_llm_optimizer + monkeypatch.setattr('opto.utils.llm.LLMFactory', MockLLMFactory) + + parameter_node._data = 0 + + # Mock multiple LLM responses for multi_llm generation + suggestion = {"x": 42} + response_str = json.dumps({"reasoning": "ok", "answer": "", "suggestion": suggestion}) + + # Each profile should return a response + cheap_llm = DummyLLM(responses=[[response_str]]) + premium_llm = DummyLLM(responses=[[response_str]]) + default_llm = DummyLLM(responses=[[response_str]]) + + opt._llm_instances = { + 'cheap': cheap_llm, + 'premium': premium_llm, + 'default': default_llm + } + + # Override _parallel_call_llm to return mock responses + def mock_parallel_call(arg_dicts): + return [response_str] * len(arg_dicts) + + opt._parallel_call_llm = mock_parallel_call + + # Run optimization step + update = opt._step(verbose=False, generation_technique='multi_llm') + assert isinstance(update, dict) + +def test_llm_weights_handling(): + """Test that LLM weights are properly handled""" + param = ParameterNode(name='test', value=1) + + # Test with explicit weights + profiles = ['cheap', 'premium'] + weights = [0.3, 0.7] + opt1 = OptoPrimeMulti([param], llm_profiles=profiles, llm_weights=weights) + assert opt1.llm_weights == weights + + # Test with automatic weights (should default to 1.0 for each profile) + opt2 = OptoPrimeMulti([param], llm_profiles=profiles) + assert opt2.llm_weights == [1.0, 1.0] + + # Test without profiles (should be None) + opt3 = OptoPrimeMulti([param]) + assert opt3.llm_weights is None + +def test_multi_llm_logging(multi_llm_optimizer, monkeypatch): + """Test that multi-LLM usage is properly logged""" + opt = multi_llm_optimizer + opt.log = [] # Enable logging + + # Manually set LLM instances to avoid import issues + opt._llm_instances = { + 'cheap': DummyLLM(responses=[["response1"]]), + 'premium': DummyLLM(responses=[["response2"]]), + 'default': DummyLLM(responses=[["response3"]]) + } + + # Override _parallel_call_llm to return mock responses + def mock_parallel_call(arg_dicts): + return ["response1", "response2", "response3"] + + opt._parallel_call_llm = mock_parallel_call + + cands = opt.generate_candidates(None, "sys", "usr", num_responses=3, + generation_technique="multi_llm") + + # Check that logging includes llm_profiles + assert len(opt.log) > 0 + log_entry = opt.log[-1] + assert 'llm_profiles' in log_entry + assert log_entry['llm_profiles'] == ['cheap', 'premium', 'default'] + def user_code(output): if output < 0: return "Success." @@ -115,7 +297,8 @@ def user_code(output): "temperature_variation", "self_refinement", "iterative_alternatives", - "multi_experts" + "multi_experts", + "multi_llm" ]) @pytest.mark.parametrize("sel_tech", [ "moa", @@ -150,3 +333,25 @@ def my_fun(x): print(f"Function updated: old value: {str(old_func_value)}, new value: {str(new_func_value)}") +def test_backwards_compatibility(): + """Test that existing OptoPrimeMulti usage continues to work without changes""" + param = ParameterNode(name='test', value=1) + + # Old-style initialization should work exactly as before + opt = OptoPrimeMulti([param], + num_responses=3, + generation_technique="temperature_variation", + selection_technique="best_of_n") + + # New attributes should have sensible defaults + assert opt.llm_profiles is None + assert opt.llm_weights is None + assert opt._llm_instances == {} + + # Should fall back to single LLM behavior + llms = opt._get_llms_for_generation(3) + assert len(llms) == 3 + assert all(llm is opt.llm for llm in llms) + + # Profile retrieval should return default LLM for None + assert opt._get_llm_for_profile(None) is opt.llm \ No newline at end of file From 3b236adbb4541d728be029b258c5f665fce6ad78 Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 16 Jun 2025 21:34:04 +0000 Subject: [PATCH 029/314] Fix a bug of missing default test_dataset. --- opto/trainer/algorithms/basic_algorithms.py | 1 + 1 file changed, 1 insertion(+) diff --git a/opto/trainer/algorithms/basic_algorithms.py b/opto/trainer/algorithms/basic_algorithms.py index 66596580..9baa09e5 100644 --- a/opto/trainer/algorithms/basic_algorithms.py +++ b/opto/trainer/algorithms/basic_algorithms.py @@ -110,6 +110,7 @@ def train(self, log_frequency = log_frequency or eval_frequency # frequency of logging (default to eval_frequency) num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads + test_dataset = test_dataset or train_dataset # default to train_dataset if test_dataset is not provided use_asyncio = self._use_asyncio(num_threads) # Evaluate the agent before learning From 1c4336a2262da5977115f137e3bdc29fba5bda87 Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 16 Jun 2025 22:30:57 +0000 Subject: [PATCH 030/314] Set to use default LLM instead of LiteLLM. --- examples/search_algo_example.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/search_algo_example.py b/examples/search_algo_example.py index 32c6db70..09df72d2 100644 --- a/examples/search_algo_example.py +++ b/examples/search_algo_example.py @@ -18,7 +18,7 @@ from opto.trainer.algorithms.UCBsearch import UCBSearchAlgorithm, HybridUCB_LLM, UCBSearchFunctionApproximationAlgorithm from opto.trainer.guide import AutoGuide from opto.trainer.loggers import DefaultLogger -from opto.utils.llm import LLM, LiteLLM +from opto.utils.llm import LLM # Set default model # os.environ["TRACE_LITELLM_MODEL"] = "vertex_ai/gemini-2.0-flash" @@ -41,7 +41,7 @@ def __init__(self, super().__init__() self.system_prompt = trace.node(system_prompt, trainable=True) self.user_prompt_template = trace.node(user_prompt_template, trainable=True) - self.llm = llm or LiteLLM(model="gpt-3.5-turbo") + self.llm = llm or LLM(model="gpt-3.5-turbo") @trace.bundle() def call_llm(self, system_prompt: str, user_prompt: str) -> str: @@ -85,7 +85,7 @@ def __init__(self, model: str = "gpt-4o-mini"): model: The LLM model to use for evaluation """ super().__init__() - self.guide_llm = LiteLLM(model=model) + self.guide_llm = LLM(model=model) self.system_prompt = "You are an expert math teacher evaluating student answers." self.judge_prompt_template = ( "Carefully review the following three distinct sections:\n\n" @@ -252,7 +252,7 @@ def main(): # Set environment variables os.environ["TRACE_LITELLM_MODEL"] = args.trace_model - + # Set random seed np.random.seed(args.seed) @@ -283,7 +283,7 @@ def main(): # Initialize components print("Initializing Agent, Guide, Optimizer, Algorithm...") - student_llm = LiteLLM(model=args.student_model) + student_llm = LLM(model=args.student_model) agent = Learner(llm=student_llm) train_guide = TeacherGuide(model=args.teacher_model) @@ -291,7 +291,7 @@ def main(): optimizer = OptoPrime(agent.parameters()) logger = SimpleLogger() - + # Create algorithm if args.algorithm_type == 'minibatch': algorithm = MinibatchAlgorithm( From 2e2611b443b7f5856eb5cb2e49c5e05125ef5c35 Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 16 Jun 2025 22:55:29 +0000 Subject: [PATCH 031/314] Remove necessary dependency on LiteLLM and remove using vertex as default. --- examples/search_algo_example.py | 11 +++++----- opto/trainer/algorithms/UCBsearch.py | 30 ++++++++++------------------ 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/examples/search_algo_example.py b/examples/search_algo_example.py index 09df72d2..537727eb 100644 --- a/examples/search_algo_example.py +++ b/examples/search_algo_example.py @@ -199,11 +199,11 @@ def main(): help='Number of test samples') # LLM Model parameters - parser.add_argument('--trace_model', type=str, default='vertex_ai/gemini-2.0-flash', + parser.add_argument('--trace_model', type=str, default=None, help='Model to use for trace operations') - parser.add_argument('--student_model', type=str, default='vertex_ai/gemini-2.0-flash', + parser.add_argument('--student_model', type=str, default=None, help='Model to use for student agent') - parser.add_argument('--teacher_model', type=str, default='vertex_ai/gemini-2.0-flash', + parser.add_argument('--teacher_model', type=str, default=None, help='Model to use for teacher guide') # Training parameters @@ -251,8 +251,9 @@ def main(): args = parser.parse_args() # Set environment variables - os.environ["TRACE_LITELLM_MODEL"] = args.trace_model - + if args.trace_model: + os.environ["TRACE_LITELLM_MODEL"] = args.trace_model + # Set random seed np.random.seed(args.seed) diff --git a/opto/trainer/algorithms/UCBsearch.py b/opto/trainer/algorithms/UCBsearch.py index a589fcdc..e1a852e8 100644 --- a/opto/trainer/algorithms/UCBsearch.py +++ b/opto/trainer/algorithms/UCBsearch.py @@ -12,7 +12,7 @@ from opto.trainer.utils import async_run # Assuming print_color is in utils from opto.optimizers.utils import print_color from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm, evaluate, batchify # evaluate and batchify might be useful -from opto.utils.llm import LiteLLM # For the selector LLM +from opto.utils.llm import LLM # For the selector LLM from opto.trace.nodes import ParameterNode import warnings @@ -399,18 +399,13 @@ class HybridUCB_LLM(MinibatchAlgorithm): If the buffer is full, evicts the candidate with the lowest UCB score. """ - # LLM prompt templates as class attributes for easy customization - SYSTEM_PROMPT_TEMPLATE = "You are an expert in model optimization. Your task is to propose new string values for model parameters with high UCB scores. Please output ONLY a valid JSON dictionary where keys are parameter names and values are the new string values for those parameters, matching the example structure provided. Do not add any explanations or markdown formatting around the JSON." - - USER_PROMPT_TEMPLATE = "Here are some current candidates from the search buffer and their statistics:\n{candidates}\n\nHere is an example of the required JSON output structure (parameter names as keys, new string values as values):\n{example_structure}\n\nPlease generate a new set of parameters in exactly the same JSON format. Make sure use double quotes for the keys and values." - def __init__(self, agent: trace.Module, optimizer, max_buffer_size: int = 10, ucb_exploration_factor: float = 1.0, alpha: float = 0.7, - llm_model: str = "vertex_ai/gemini-2.0-flash", + llm_model: str = None, logger=None, num_threads: int = None, *args, @@ -430,8 +425,8 @@ def __init__(self, self._total_evaluations_tracker = 0 - # Initialize LiteLLM - self.llm = LiteLLM(model=self.llm_model) + # Initialize LLM + self.llm = LLM(model=self.llm_model) print_color(f"Initialized HybridUCB_LLM with alpha={self.alpha}, LLM model={self.llm_model}", "cyan") def _sample_minibatch(self, dataset: Dict[str, List[Any]], batch_size: int) -> Tuple[List[Any], List[Any]]: @@ -540,8 +535,8 @@ def _llm_generate_candidate(self) -> Optional[Dict[trace.nodes.ParameterNode, st example_param_structure_json_str = {getattr(p,'py_name'): copy.deepcopy(p.data) for p in self.agent.parameters()} prompt_messages = [ - {"role": "system", "content": self.SYSTEM_PROMPT_TEMPLATE}, - {"role": "user", "content": self.USER_PROMPT_TEMPLATE.format(candidates=serializable_candidate_summaries, example_structure=example_param_structure_json_str)} + {"role": "system", "content": "You are an expert in model optimization. Your task is to propose new string values for model parameters with high UCB scores. Please output ONLY a valid JSON dictionary where keys are parameter names and values are the new string values for those parameters, matching the example structure provided. Do not add any explanations or markdown formatting around the JSON."}, + {"role": "user", "content": f"Here are some current candidates from the search buffer and their statistics:\\n{serializable_candidate_summaries}\\n\\nHere is an example of the required JSON output structure (parameter names as keys, new string values as values):\\n{example_param_structure_json_str}\\n\\nPlease generate a new set of parameters in exactly the same JSON format. Make sure use double quotes for the keys and values."} ] print_color(f"LLM prompt (summary): {len(prompt_candidates)} candidates, structure example provided.", "magenta") @@ -878,15 +873,10 @@ class UCBSearchFunctionApproximationAlgorithm(UCBSearchAlgorithm): UCB Search Algorithm that uses LLM function approximation to select candidates. """ - # LLM prompt templates as class attributes for easy customization - SYSTEM_PROMPT_TEMPLATE = "You are an expert in model optimization. Your task is to propose new string values for model parameters with high UCB scores. Please output ONLY a valid JSON dictionary where keys are parameter names and values are the new string values for those parameters, matching the example structure provided. Do not add any explanations or markdown formatting around the JSON." - - USER_PROMPT_TEMPLATE = "Here are some current candidates from the search buffer and their statistics:\n{candidates}\n\nHere is an example of the required JSON output structure (parameter names as keys, new string values as values):\n{example_structure}\n\nPlease generate a new set of parameters in exactly the same JSON format. Make sure use double quotes for the keys and values." - def __init__(self, llm_model, *args, **kwargs): super().__init__(*args, **kwargs) self.llm_model = llm_model - self.llm = LiteLLM(model=self.llm_model) + self.llm = LLM(model=self.llm_model) print_color(f"Initialized UCBSearchFunctionApproximationAlgorithm with LLM model={self.llm_model}", "cyan") def select(self, buffer): @@ -926,13 +916,13 @@ def _llm_generate_candidate(self) -> Optional[Dict[trace.nodes.ParameterNode, st example_param_structure_json_str = {getattr(p,'py_name'): copy.deepcopy(p.data) for p in self.agent.parameters()} prompt_messages = [ - {"role": "system", "content": self.SYSTEM_PROMPT_TEMPLATE}, - {"role": "user", "content": self.USER_PROMPT_TEMPLATE.format(candidates=serializable_candidate_summaries, example_structure=example_param_structure_json_str)} + {"role": "system", "content": "You are an expert in model optimization. Your task is to propose new string values for model parameters with high UCB scores. Please output ONLY a valid JSON dictionary where keys are parameter names and values are the new string values for those parameters, matching the example structure provided. Do not add any explanations or markdown formatting around the JSON."}, + {"role": "user", "content": f"Here are some current candidates from the search buffer and their statistics:\\n{serializable_candidate_summaries}\\n\\nHere is an example of the required JSON output structure (parameter names as keys, new string values as values):\\n{example_param_structure_json_str}\\n\\nPlease generate a new set of parameters in exactly the same JSON format. Make sure use double quotes for the keys and values."} ] print_color(f"LLM prompt (summary): {len(prompt_candidates)} candidates, structure example provided.", "magenta") - llm_response = self.llm(prompt_messages) + llm_response = self.llm(messages=prompt_messages) llm_response_str = llm_response.choices[0].message.content if not llm_response_str: From 69c7d7a1eebee54d9234b872c58dfd47432c2093 Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 16 Jun 2025 23:01:02 +0000 Subject: [PATCH 032/314] Remove LiteLLM dependency in gsm8k example --- examples/gsm8k_trainer_example.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/gsm8k_trainer_example.py b/examples/gsm8k_trainer_example.py index 369eeec4..f9524dc0 100644 --- a/examples/gsm8k_trainer_example.py +++ b/examples/gsm8k_trainer_example.py @@ -4,7 +4,7 @@ from opto.utils.llm import LLM, LiteLLM from opto.optimizers import OptoPrime from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm -from opto.trainer.loggers import DefaultLogger, TensorboardLogger +from opto.trainer.loggers import TensorboardLogger from opto.trainer.guide import VerbalJudgeGuide from typing import Any @@ -56,6 +56,7 @@ def main(): num_epochs = 1 batch_size = 1 eval_frequency = -1 + num_threads = 3 verbose = True teacher_model = None # use default model student_model = None # use default model @@ -71,7 +72,7 @@ def main(): agent = Learner(llm=LLM(student_model)) guide = Guide(model=LLM(teacher_model)) - optimizer = OptoPrime(agent.parameters(), llm=LiteLLM(optimizer_model)) + optimizer = OptoPrime(agent.parameters(), llm=LLM(optimizer_model)) logger = Logger(verbose=verbose) # set use_json_object_format=False if LLM does not support JSON object format @@ -86,7 +87,7 @@ def main(): batch_size=batch_size, eval_frequency=eval_frequency, test_dataset=test_dataset, - num_threads=3, + num_threads=num_threads, verbose='output' if verbose else False) From f7eeaacc756386b216037cfc2cab7c9ebdde3fb7 Mon Sep 17 00:00:00 2001 From: Xuanfei Ren Date: Tue, 17 Jun 2025 13:48:35 -0500 Subject: [PATCH 033/314] Add a wandb logger --- opto/trainer/loggers.py | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/opto/trainer/loggers.py b/opto/trainer/loggers.py index 5f82a4ac..19d1e553 100644 --- a/opto/trainer/loggers.py +++ b/opto/trainer/loggers.py @@ -79,6 +79,44 @@ def log(self, name, data, step, **kwargs): # Otherwise, log it as a scalar self.writer.add_scalar(name, data, step) -# TODO add wandb logger +class WandbLogger(ConsoleLogger): + """A logger that writes metrics to Weights and Biases (wandb).""" + + def __init__(self, log_dir='./logs', verbose=True, project=None, **kwargs): + super().__init__(log_dir, **kwargs) + self.verbose = verbose + # Late import to avoid dependency issues + try: + import wandb + except ImportError: + raise ImportError("wandb is required for WandbLogger. Install it with: pip install wandb") + + # Initialize wandb + self.wandb = wandb + if not wandb.run: + wandb.init(project=project, dir=log_dir, **kwargs) + + def log(self, name, data, step, **kwargs): + """Log a message to Weights and Biases. + + Args: + name: Name of the metric + data: Value of the metric + step: Current step/iteration + **kwargs: Additional arguments (not used here) + """ + if self.verbose: + super().log(name, data, step, **kwargs) + + # Log to wandb + if isinstance(data, str): + # For string data, we can log it as a custom chart or just print it + # wandb doesn't have a direct equivalent to tensorboard's add_text + # but we can log it in a structured way + self.wandb.log({f"{name}_text": data}, step=step) + else: + # For numeric data, log as scalar + self.wandb.log({name: data}, step=step) + DefaultLogger = ConsoleLogger \ No newline at end of file From 73877e751e8745fead25f59efbdb376556bf8a5a Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Tue, 17 Jun 2025 21:09:18 -0500 Subject: [PATCH 034/314] Deleted two UCB search algorithms in PR --- examples/search_algo_example.py | 42 +- opto/trainer/algorithms/UCBsearch.py | 671 +-------------------------- 2 files changed, 7 insertions(+), 706 deletions(-) diff --git a/examples/search_algo_example.py b/examples/search_algo_example.py index 537727eb..ea3421c8 100644 --- a/examples/search_algo_example.py +++ b/examples/search_algo_example.py @@ -2,7 +2,7 @@ import os import time import argparse -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any, Tuple # Third-party imports import datasets @@ -15,7 +15,7 @@ from opto.trace.modules import Module from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm, BasicSearchAlgorithm from opto.trainer.algorithms.beamsearch_algorithm import BeamsearchAlgorithm, BeamsearchHistoryAlgorithm -from opto.trainer.algorithms.UCBsearch import UCBSearchAlgorithm, HybridUCB_LLM, UCBSearchFunctionApproximationAlgorithm +from opto.trainer.algorithms.UCBsearch import UCBSearchAlgorithm from opto.trainer.guide import AutoGuide from opto.trainer.loggers import DefaultLogger from opto.utils.llm import LLM @@ -184,8 +184,8 @@ def main(): parser = argparse.ArgumentParser(description='Train agent using various algorithms') # Algorithm parameters - parser.add_argument('--algorithm_type', type=str, default='UCBSearchFunctionApproximationAlgorithm', - choices=['minibatch', 'basicsearch', 'beamsearch', 'beamsearchhistory', 'UCBsearch', 'HybridUCB_LLM', 'UCBSearchFunctionApproximationAlgorithm'], + parser.add_argument('--algorithm_type', type=str, default='UCBsearch', + choices=['minibatch', 'basicsearch', 'beamsearch', 'beamsearchhistory', 'UCBsearch'], help='Type of algorithm to use') # Dataset parameters @@ -239,8 +239,6 @@ def main(): help='Maximum buffer size for UCB algorithms') parser.add_argument('--ucb_exploration_factor', type=float, default=1.0, help='UCB exploration factor') - parser.add_argument('--alpha', type=float, default=0.3, - help='Alpha parameter for HybridUCB_LLM (probability of UCB vs LLM path)') parser.add_argument('--num_search_iterations', type=int, default=100, help='Number of search iterations for UCB algorithms') parser.add_argument('--train_batch_size_ucb', type=int, default=2, @@ -331,27 +329,6 @@ def main(): max_buffer_size=args.max_buffer_size, ucb_exploration_factor=args.ucb_exploration_factor ) - elif args.algorithm_type == 'HybridUCB_LLM': - algorithm = HybridUCB_LLM( - agent=agent, - optimizer=optimizer, - logger=logger, - num_threads=args.num_threads, - max_buffer_size=args.max_buffer_size, - ucb_exploration_factor=args.ucb_exploration_factor, - alpha=args.alpha, - llm_model=args.trace_model - ) - elif args.algorithm_type == 'UCBSearchFunctionApproximationAlgorithm': - algorithm = UCBSearchFunctionApproximationAlgorithm( - agent=agent, - optimizer=optimizer, - logger=logger, - num_threads=args.num_threads, - max_buffer_size=args.max_buffer_size, - ucb_exploration_factor=args.ucb_exploration_factor, - llm_model=args.trace_model - ) else: raise ValueError(f"Unknown algorithm type: {args.algorithm_type}") @@ -384,7 +361,7 @@ def main(): elif args.algorithm_type == 'basicsearch': train_params["num_proposals"] = args.num_basicsearch_proposals - elif args.algorithm_type in ['UCBsearch', 'HybridUCB_LLM', 'UCBSearchFunctionApproximationAlgorithm']: + elif args.algorithm_type == 'UCBsearch': train_params.update({ "num_search_iterations": args.num_search_iterations, "train_batch_size": args.train_batch_size_ucb, @@ -404,20 +381,13 @@ def main(): for depth, score in enumerate(metrics['best_validation_scores']): print(f" Depth {depth+1}: {score:.4f}") - elif args.algorithm_type in ['UCBsearch', 'HybridUCB_LLM', 'UCBSearchFunctionApproximationAlgorithm']: + elif args.algorithm_type == 'UCBsearch': print("\nUCB Algorithm Metrics:") if 'best_candidate_scores' in metrics and metrics['best_candidate_scores']: print(f" Best candidate scores over iterations: {len(metrics['best_candidate_scores'])} recorded") print(f" Final best candidate score: {metrics['best_candidate_scores'][-1]:.4f}") if 'buffer_avg_score' in metrics and metrics['buffer_avg_score']: print(f" Final buffer average score: {metrics['buffer_avg_score'][-1]:.4f}") - if args.algorithm_type == 'HybridUCB_LLM': - if 'llm_generation_failures' in metrics: - print(f" LLM generation failures: {metrics['llm_generation_failures']}") - if 'generation_path' in metrics: - ucb_count = metrics['generation_path'].count('ucb') - llm_count = metrics['generation_path'].count('llm') - print(f" Generation methods used - UCB: {ucb_count}, LLM: {llm_count}") print(f"Final score: {final_score:.4f}") diff --git a/opto/trainer/algorithms/UCBsearch.py b/opto/trainer/algorithms/UCBsearch.py index e1a852e8..0a136eff 100644 --- a/opto/trainer/algorithms/UCBsearch.py +++ b/opto/trainer/algorithms/UCBsearch.py @@ -1,61 +1,12 @@ import numpy as np import copy -import time import math -import json # For LLM output parsing -import re # For smart quote replacement from collections import deque from typing import Union, List, Tuple, Dict, Any, Optional -import random # Added for alpha probability - from opto import trace from opto.trainer.utils import async_run # Assuming print_color is in utils from opto.optimizers.utils import print_color from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm, evaluate, batchify # evaluate and batchify might be useful -from opto.utils.llm import LLM # For the selector LLM - -from opto.trace.nodes import ParameterNode -import warnings -from black import format_str, FileMode - - -def smart_quote_replacement(text: str) -> str: - """ - Intelligently replace single quotes with double quotes for JSON parsing. - Handles the specific case where we have mixed quotes like: - {'key': "value with 'nested' quotes"} - """ - # For the specific pattern we're seeing, let's handle it step by step: - - # Step 1: Replace single quotes around keys - # Pattern: 'key': -> "key": - text = re.sub(r"'([^']*?)'(\s*:)", r'"\1"\2', text) - - # Step 2: For values that start with double quotes and contain single quotes, - # we need to escape the internal single quotes or convert them properly - - # Let's try a more direct approach for the problematic case: - # Find patterns like: "text with 'word' more text" - # We need to escape the internal single quotes - def escape_internal_quotes(match): - content = match.group(1) - # Replace single quotes inside with escaped single quotes - # Actually, for JSON we can leave single quotes as-is inside double quotes - return f'"{content}"' - - # Replace the pattern: : "content with 'quotes'" -> : "content with 'quotes'" - # (This should already be valid JSON) - - # The main issue is with the outer structure, let's fix that: - # If the string starts/ends with single quotes around the whole thing - text = text.strip() - if text.startswith("{'") and text.endswith("'}"): - # Replace the outer single quotes but preserve the content - # This is the pattern: {'str0': "content", 'str1': "more content"} - text = '{"' + text[2:-2] + '"}' - - return text - class UCBSearchAlgorithm(MinibatchAlgorithm): """ @@ -376,624 +327,4 @@ def train(self, def select(self, buffer): '''Could be subclassed to implement different selection strategies''' - return max(buffer, key=lambda c: c['ucb_score']) - - -class HybridUCB_LLM(MinibatchAlgorithm): - """ - UCB Search Algorithm with Function Approximation (LLM). - - Keeps a buffer of candidates. - In each iteration: - - With probability alpha: - 1. Picks a candidate 'a' from the buffer with the highest UCB score. - 2. Updates the optimizer with 'a's parameters. - 3. Draws a minibatch from the training set, performs a forward/backward pass, and calls optimizer.step() to get a new candidate 'a_prime'. - 4. Evaluates 'a_prime' on a validation set minibatch. - 5. Updates statistics of 'a' (based on the training minibatch). - 6. Adds 'a_prime' (with its validation stats) to the buffer. - - With probability 1-alpha: - 1. Uses an external LLM, prompted with candidates from the buffer, to generate a new candidate 'a_prime'. - 2. Evaluates 'a_prime' on a validation set minibatch. - 3. Adds 'a_prime' (with its validation stats) to the buffer. - If the buffer is full, evicts the candidate with the lowest UCB score. - """ - - def __init__(self, - agent: trace.Module, - optimizer, - max_buffer_size: int = 10, - ucb_exploration_factor: float = 1.0, - alpha: float = 0.7, - llm_model: str = None, - logger=None, - num_threads: int = None, - *args, - **kwargs): - super().__init__(agent, optimizer, num_threads=num_threads, logger=logger, *args, **kwargs) - - self.alpha = alpha - self.llm_model = llm_model - self.llm_prompt_budget_factor = 0.5 - - self.buffer = deque(maxlen=max_buffer_size) - self.max_buffer_size = max_buffer_size - self.ucb_exploration_factor = ucb_exploration_factor - - if not hasattr(self.optimizer, 'step'): - raise ValueError("Optimizer must have a 'step' method.") - - self._total_evaluations_tracker = 0 - - # Initialize LLM - self.llm = LLM(model=self.llm_model) - print_color(f"Initialized HybridUCB_LLM with alpha={self.alpha}, LLM model={self.llm_model}", "cyan") - - def _sample_minibatch(self, dataset: Dict[str, List[Any]], batch_size: int) -> Tuple[List[Any], List[Any]]: - """Sample a minibatch from the dataset.""" - if not dataset or not dataset.get('inputs') or not dataset.get('infos'): - print_color("Warning: Attempted to sample from an empty or malformed dataset.", color='yellow') - return [], [] - - dataset_size = len(dataset['inputs']) - if dataset_size == 0: - print_color("Warning: Dataset is empty, cannot sample minibatch.", color='yellow') - return [], [] - - actual_batch_size = min(batch_size, dataset_size) - indices = np.random.choice(dataset_size, actual_batch_size, replace=False) - xs = [dataset['inputs'][i] for i in indices] - infos = [dataset['infos'][i] for i in indices] - return xs, infos - - def _evaluate_candidate(self, - params_to_eval_dict: Dict[str, Any], - dataset: Dict[str, List[Any]], - guide, - evaluation_batch_size: int, - num_threads: Optional[int] = None - ) -> Tuple[float, int]: - """Evaluates a given set of parameters on samples from the provided dataset.""" - if not dataset or not dataset.get('inputs') or not dataset.get('infos') or not dataset['inputs']: - print_color("Evaluation dataset is empty or invalid. Returning score -inf, count 0.", color='yellow') - return -np.inf, 0 - - original_params_backup = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} - - try: - self.optimizer.update(params_to_eval_dict) - except Exception as e: - print_color(f"Error updating agent with params_to_eval_dict: {e}. Using current agent state for eval.", "red") - - eval_xs, eval_infos = self._sample_minibatch(dataset, evaluation_batch_size) - - if not eval_xs: - print_color("Evaluation minibatch is empty. Returning score -inf, count 0.", color='yellow') - self.optimizer.update(original_params_backup) - return -np.inf, 0 - - eval_scores = evaluate(self.agent, - guide, - eval_xs, - eval_infos, - min_score=self.min_score if hasattr(self, 'min_score') else None, - num_threads=num_threads or self.num_threads, - description=f"Evaluating candidate") - - self.optimizer.update(original_params_backup) - - avg_score = np.mean(eval_scores) if eval_scores and all(s is not None for s in eval_scores) else -np.inf - eval_count = len(eval_xs) - - return float(avg_score), eval_count - - def _calculate_ucb(self, candidate_buffer_entry: Dict, total_tracked_evaluations: int) -> float: - """Calculates UCB score for a candidate in the buffer.""" - if candidate_buffer_entry['eval_count'] == 0: - return float('inf') - - mean_score = candidate_buffer_entry['score_sum'] / candidate_buffer_entry['eval_count'] - - if total_tracked_evaluations == 0: - total_tracked_evaluations = 1 - - exploration_term = self.ucb_exploration_factor * \ - math.sqrt(math.log(total_tracked_evaluations + 1e-9) / candidate_buffer_entry['eval_count']) - - return mean_score + exploration_term - - def _update_buffer_ucb_scores(self): - """Recalculates and updates UCB scores for all candidates in the buffer.""" - if not self.buffer: - return - - for candidate_entry in self.buffer: - candidate_entry['ucb_score'] = self._calculate_ucb(candidate_entry, self._total_evaluations_tracker) - - def _llm_generate_candidate(self) -> Optional[Dict[trace.nodes.ParameterNode, str]]: - """ - Prompts an LLM with current buffer candidates to generate new string values for parameters. - Returns a dictionary mapping ParameterNode objects to new string values, or None on failure. - """ - print_color("Attempting to generate candidate using LLM...", "blue") - if not self.buffer: - print_color("LLM generation: Buffer is empty, cannot provide context to LLM.", "yellow") - return None - - sorted_buffer = sorted(list(self.buffer), key=lambda c: c.get('ucb_score', -float('inf')), reverse=True) - prompt_candidates = sorted_buffer - - serializable_candidate_summaries = [] - for cand_entry in prompt_candidates: - summary = { - "parameters": {getattr(p,'py_name'): copy.deepcopy(p.data) for p in cand_entry['params']}, - "eval_count": cand_entry['eval_count'], - "ucb_score": round(cand_entry.get('ucb_score',0), 4), - } - serializable_candidate_summaries.append(summary) - - example_param_structure_json_str = {getattr(p,'py_name'): copy.deepcopy(p.data) for p in self.agent.parameters()} - - prompt_messages = [ - {"role": "system", "content": "You are an expert in model optimization. Your task is to propose new string values for model parameters with high UCB scores. Please output ONLY a valid JSON dictionary where keys are parameter names and values are the new string values for those parameters, matching the example structure provided. Do not add any explanations or markdown formatting around the JSON."}, - {"role": "user", "content": f"Here are some current candidates from the search buffer and their statistics:\\n{serializable_candidate_summaries}\\n\\nHere is an example of the required JSON output structure (parameter names as keys, new string values as values):\\n{example_param_structure_json_str}\\n\\nPlease generate a new set of parameters in exactly the same JSON format. Make sure use double quotes for the keys and values."} - ] - - print_color(f"LLM prompt (summary): {len(prompt_candidates)} candidates, structure example provided.", "magenta") - - llm_response = self.llm(prompt_messages) - llm_response_str = llm_response.choices[0].message.content - - if not llm_response_str: - print_color("LLM returned an empty response.", "red") - return None - - # Clean the response string - cleaned_llm_response_str = llm_response_str.strip() - if cleaned_llm_response_str.startswith("```json"): - cleaned_llm_response_str = cleaned_llm_response_str[7:] - if cleaned_llm_response_str.endswith("```"): - cleaned_llm_response_str = cleaned_llm_response_str[:-3] - elif cleaned_llm_response_str.startswith("```"): - cleaned_llm_response_str = cleaned_llm_response_str[3:] - if cleaned_llm_response_str.endswith("```"): - cleaned_llm_response_str = cleaned_llm_response_str[:-3] - cleaned_llm_response_str = cleaned_llm_response_str.strip() - - if not cleaned_llm_response_str: - print_color("LLM response was empty after cleaning markdown/whitespace.", "red") - return None - - print_color(f"Cleaned LLM response: '{cleaned_llm_response_str}'", "magenta") - - # Fix common JSON formatting issues from LLM responses - try: - llm_params_raw = json.loads(cleaned_llm_response_str) - except json.JSONDecodeError as e: - print_color(f"Initial JSON parsing failed: {e}", "yellow") - print_color("Attempting to fix JSON formatting...", "yellow") - - fixed_json_str = smart_quote_replacement(cleaned_llm_response_str) - - try: - llm_params_raw = json.loads(fixed_json_str) - print_color("Successfully fixed JSON formatting", "green") - except json.JSONDecodeError as e2: - print_color(f"Smart quote replacement failed: {e2}", "yellow") - try: - simple_fixed = cleaned_llm_response_str.replace("'", '"') - llm_params_raw = json.loads(simple_fixed) - print_color("Fallback simple replacement succeeded", "green") - except json.JSONDecodeError as e3: - print_color(f"All JSON parsing attempts failed: {e3}", "red") - print_color("Returning the candidate with the highest UCB score in the buffer.", "red") - return max(self.buffer, key=lambda c: c.get('ucb_score', -float('inf')))['params'] - - if not isinstance(llm_params_raw, dict): - print_color(f"LLM output was not a JSON dictionary after parsing: {type(llm_params_raw)}", "red") - print_color("Returning the candidate with the highest UCB score in the buffer.", "red") - return max(self.buffer, key=lambda c: c.get('ucb_score', -float('inf')))['params'] - - candidate_params_dict = self.construct_update_dict(llm_params_raw) - return candidate_params_dict - - def construct_update_dict(self, suggestion: Dict[str, Any]) -> Dict[ParameterNode, Any]: - """Convert the suggestion in text into the right data type.""" - update_dict = {} - for node in self.agent.parameters(): - if node.trainable and node.py_name in suggestion: - try: - formatted_suggestion = suggestion[node.py_name] - if type(formatted_suggestion) == str and 'def' in formatted_suggestion: - formatted_suggestion = format_str(formatted_suggestion, mode=FileMode()) - update_dict[node] = type(node.data)(formatted_suggestion) - except (ValueError, KeyError) as e: - if getattr(self, 'ignore_extraction_error', False): - warnings.warn( - f"Cannot convert the suggestion '{suggestion[node.py_name]}' for {node.py_name} to the right data type" - ) - else: - raise e - return update_dict - - def train(self, - guide, - train_dataset: Dict[str, List[Any]], - *, - num_search_iterations: int = 100, - train_batch_size: int = 5, - evaluation_batch_size: int = 5, - ensure_improvement: bool = False, - improvement_threshold: float = 0., - eval_frequency: int = 1, - log_frequency: Optional[int] = None, - save_frequency: Optional[int] = None, - save_path: str = "checkpoints/ucb_llm_agent.pkl", - min_score_for_agent_update: Optional[float] = None, - verbose: Union[bool, str] = False, - num_threads: Optional[int] = None, - **kwargs - ) -> Tuple[Dict[str, Any], float]: - - num_threads = num_threads or self.num_threads - log_frequency = log_frequency or eval_frequency - self.min_score = min_score_for_agent_update - total_samples = 0 - - metrics = { - 'best_candidate_scores': [], - 'selected_action_ucb': [], - 'new_candidate_scores': [], - 'buffer_avg_score': [], - 'buffer_avg_evals': [], - 'llm_generation_failures': 0, - 'generation_path': [] - } - - # Initial candidate evaluation - print_color("Evaluating initial parameters using train_dataset samples...", 'cyan') - initial_params_dict = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} - - initial_score, initial_evals = self._evaluate_candidate( - initial_params_dict, train_dataset, guide, evaluation_batch_size, num_threads - ) - self._total_evaluations_tracker += initial_evals - total_samples += initial_evals - - initial_candidate_entry = { - 'params': initial_params_dict, - 'score_sum': initial_score * initial_evals if initial_score > -np.inf else 0, - 'eval_count': initial_evals, - 'ucb_score': 0.0, - 'iteration_created': 0 - } - self.buffer.append(initial_candidate_entry) - self._update_buffer_ucb_scores() - print_color(f"Initial candidate: Score {initial_score:.4f}, Evals {initial_evals}", 'yellow') - - # Main search loop - for iteration in range(1, num_search_iterations + 1): - if not self.buffer: - print_color("Buffer is empty, stopping search.", 'red') - break - - self._update_buffer_ucb_scores() - a_prime_params_dict = None - a_prime_score = -np.inf - a_prime_evals = 0 - generation_method = "none" - - if random.random() < self.alpha: # UCB Path - generation_method = "ucb" - metrics['generation_path'].append("ucb") - if not self.buffer: - print_color(f"Iter {iteration} (UCB Path): Buffer empty, cannot select action. Skipping.", "red") - continue - - action_candidate_a = self.select(self.buffer) - - selected_mean_score = action_candidate_a['score_sum'] / action_candidate_a['eval_count'] if action_candidate_a['eval_count'] > 0 else -np.inf - print_color(f"Iter {iteration} (UCB Path): Selected action candidate (UCB: {action_candidate_a['ucb_score']:.4f}, MeanScore: {selected_mean_score:.4f} Evals: {action_candidate_a['eval_count']})", 'blue') - metrics['selected_action_ucb'].append(action_candidate_a['ucb_score']) - - self.optimizer.update(action_candidate_a['params']) - - train_xs, train_infos = self._sample_minibatch(train_dataset, train_batch_size) - if not train_xs: - print_color(f"Iter {iteration} (UCB Path): Training minibatch empty, skipping optimizer step.", 'yellow') - continue - - total_samples += len(train_xs) - - # Forward pass for 'a' - outputs_for_a = [] - use_asyncio = self._use_asyncio(num_threads) - if use_asyncio: - outputs_for_a = async_run([self.forward]*len(train_xs), - [(self.agent, x, guide, info) for x, info in zip(train_xs, train_infos)], - max_workers=num_threads, - description=f"Iter {iteration} (UCB): Forward for 'a'") - else: - outputs_for_a = [self.forward(self.agent, x, guide, info) for x, info in zip(train_xs, train_infos)] - - scores_from_train, targets_from_train, feedbacks_from_train = [], [], [] - for target, score, feedback in outputs_for_a: - scores_from_train.append(score) - targets_from_train.append(target) - feedbacks_from_train.append(feedback) - - if not scores_from_train: - print_color(f"Iter {iteration} (UCB Path): No outputs from forward pass for 'a'. Skipping.", 'yellow') - continue - - target_for_a = batchify(*targets_from_train) - feedback_for_a = batchify(*feedbacks_from_train).data - score_for_a_on_train_batch = np.mean([s for s in scores_from_train if s is not None]) if any(s is not None for s in scores_from_train) else -np.inf - - self.optimizer.zero_feedback() - self.optimizer.backward(target_for_a, feedback_for_a) - - # Get a_prime by optimizer step - try: - returned_params = self.optimizer.step(bypassing=True, verbose=(verbose if isinstance(verbose, str) else 'output')) - if not isinstance(returned_params, dict) or not returned_params: - print_color(f"Iter {iteration} (UCB Path): Optimizer.step did not return a valid param dict for a_prime. Using current agent params.", 'yellow') - a_prime_params_dict = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} - else: - a_prime_params_dict = {p: copy.deepcopy(p.data) for p in returned_params} - - except Exception as e: - print_color(f"Iter {iteration} (UCB Path): Error during optimizer.step for a_prime: {e}. Skipping.", 'red') - continue - - # Evaluate a_prime (from UCB path) - a_prime_score, a_prime_evals = self._evaluate_candidate( - a_prime_params_dict, train_dataset, guide, evaluation_batch_size, num_threads - ) - self._total_evaluations_tracker += a_prime_evals - total_samples += a_prime_evals - - # Update stats of action_candidate_a - if score_for_a_on_train_batch > -np.inf: - action_candidate_a['score_sum'] += score_for_a_on_train_batch * len(train_xs) - action_candidate_a['eval_count'] += len(train_xs) - self._total_evaluations_tracker += len(train_xs) - - print_color(f"Iter {iteration} (UCB Path): New candidate a_prime (from UCB) generated. Eval Score: {a_prime_score:.4f}, Evals: {a_prime_evals}", 'cyan') - - else: # LLM Path - generation_method = "llm" - metrics['generation_path'].append("llm") - print_color(f"Iter {iteration} (LLM Path): Generating candidate via LLM.", 'blue') - a_prime_params_dict = self._llm_generate_candidate() - - if a_prime_params_dict: - # Evaluate a_prime (from LLM path) - a_prime_score, a_prime_evals = self._evaluate_candidate( - a_prime_params_dict, train_dataset, guide, evaluation_batch_size, num_threads - ) - self._total_evaluations_tracker += a_prime_evals - total_samples += a_prime_evals - print_color(f"Iter {iteration} (LLM Path): New candidate a_prime (from LLM) generated. Eval Score: {a_prime_score:.4f}, Evals: {a_prime_evals}", 'cyan') - else: - print_color(f"Iter {iteration} (LLM Path): LLM failed to generate a valid candidate. Skipping addition to buffer.", 'red') - metrics['llm_generation_failures'] += 1 - continue - - # Common logic for adding a_prime to buffer - metrics['new_candidate_scores'].append(a_prime_score) - - if a_prime_params_dict and a_prime_score > -np.inf and a_prime_evals > 0: - new_candidate_entry = { - 'params': a_prime_params_dict, - 'score_sum': a_prime_score * a_prime_evals, - 'eval_count': a_prime_evals, - 'ucb_score': 0.0, - 'iteration_created': iteration - } - - if len(self.buffer) == self.max_buffer_size: - self._update_buffer_ucb_scores() - candidate_to_evict = min(self.buffer, key=lambda c: c['ucb_score']) - self.buffer.remove(candidate_to_evict) - evicted_mean_score = candidate_to_evict['score_sum'] / candidate_to_evict['eval_count'] if candidate_to_evict['eval_count'] > 0 else -np.inf - print_color(f"Iter {iteration}: Buffer full. Evicted candidate (UCB: {candidate_to_evict['ucb_score']:.4f}, MeanScore: {evicted_mean_score:.4f})", 'magenta') - - self.buffer.append(new_candidate_entry) - print_color(f"Iter {iteration}: Added new candidate (from {generation_method}) to buffer.", 'magenta') - elif a_prime_params_dict: - print_color(f"Iter {iteration}: New candidate a_prime (from {generation_method}) had invalid score/evals ({a_prime_score}, {a_prime_evals}), not added to buffer.", 'yellow') - - self._update_buffer_ucb_scores() - - # Logging - if self.buffer: - best_in_buffer = max(self.buffer, key=lambda c: (c['score_sum']/(c['eval_count'] if c['eval_count'] > 0 else 1))) - current_best_score = best_in_buffer['score_sum']/(best_in_buffer['eval_count'] if best_in_buffer['eval_count'] > 0 else 1) - metrics['best_candidate_scores'].append(current_best_score) - - valid_scores = [c['score_sum']/(c['eval_count'] if c['eval_count'] > 0 else 1) for c in self.buffer if c['eval_count'] > 0] - metrics['buffer_avg_score'].append(np.mean(valid_scores) if valid_scores else -np.inf) - metrics['buffer_avg_evals'].append(np.mean([c['eval_count'] for c in self.buffer])) - else: - metrics['best_candidate_scores'].append(-np.inf) - metrics['buffer_avg_score'].append(-np.inf) - metrics['buffer_avg_evals'].append(0) - - if iteration % log_frequency == 0: - log_data = { - "iteration": iteration, - "best_score": metrics['best_candidate_scores'][-1], - "newly_evaluated_candidate_score": a_prime_score, - "buffer_size": len(self.buffer), - "buffer_avg_score": metrics['buffer_avg_score'][-1], - "buffer_avg_evals": metrics['buffer_avg_evals'][-1], - "total_evaluations_ucb_T": self._total_evaluations_tracker, - "total_samples": total_samples, - "generation_method_this_iter": generation_method, - "llm_generation_total_failures": metrics['llm_generation_failures'] - } - if generation_method == "ucb" and metrics['selected_action_ucb']: - log_data["selected_action_ucb"] = metrics['selected_action_ucb'][-1] - - print_color(f"Log @ Iter {iteration}: Best score in buffer: {log_data['best_score']:.4f}, Gen method: {generation_method}, Buffer size: {len(self.buffer)}, Total samples: {total_samples}", 'green') - - if save_frequency is not None and iteration % save_frequency == 0 and self.buffer: - best_overall_candidate_entry = max(self.buffer, key=lambda c: (c['score_sum'] / (c['eval_count'] if c['eval_count'] > 0 else 1E-9))) - self.optimizer.update(best_overall_candidate_entry['params']) - if hasattr(self, 'save_agent'): - self.save_agent(save_path, iteration) - best_mean_score_for_save = best_overall_candidate_entry['score_sum'] / (best_overall_candidate_entry['eval_count'] if best_overall_candidate_entry['eval_count'] > 0 else 1E-9) - print_color(f"Iter {iteration}: Saved agent based on best candidate in buffer (Mean Score: {best_mean_score_for_save:.4f}).", 'green') - else: - print_color(f"Iter {iteration}: save_agent method not found, skipping save.", 'yellow') - - print_color("UCB-LLM search finished.", 'blue') - if not self.buffer: - print_color("Buffer is empty at the end of search. No best candidate found.", 'red') - return metrics, -np.inf - - final_best_candidate = max(self.buffer, key=lambda c: (c['score_sum'] / (c['eval_count'] if c['eval_count'] > 0 else 1E-9))) - final_best_score = final_best_candidate['score_sum'] / (final_best_candidate['eval_count'] if final_best_candidate['eval_count'] > 0 else 1E-9) - final_best_evals = final_best_candidate['eval_count'] - print_color(f"Final best candidate: Mean Score {final_best_score:.4f}, Evals {final_best_evals}", 'green') - - self.optimizer.update(final_best_candidate['params']) - - return metrics, float(final_best_score) - - def select(self, buffer): - '''Selects candidate with highest UCB score.''' - if not buffer: return None - return max(buffer, key=lambda c: c.get('ucb_score', -float('inf'))) - - -class UCBSearchFunctionApproximationAlgorithm(UCBSearchAlgorithm): - """ - UCB Search Algorithm that uses LLM function approximation to select candidates. - """ - - def __init__(self, llm_model, *args, **kwargs): - super().__init__(*args, **kwargs) - self.llm_model = llm_model - self.llm = LLM(model=self.llm_model) - print_color(f"Initialized UCBSearchFunctionApproximationAlgorithm with LLM model={self.llm_model}", "cyan") - - def select(self, buffer): - """Generate a new candidate entry using LLM. Note: this doesn't add it to the buffer.""" - new_action_params = self._llm_generate_candidate() - new_candidate_entry = { - 'params': new_action_params, - 'score_sum': 0, - 'eval_count': 0, - 'ucb_score': 0.0, - 'iteration_created': 0 - } - return new_candidate_entry - - def _llm_generate_candidate(self) -> Optional[Dict[trace.nodes.ParameterNode, str]]: - """ - Prompts an LLM with current buffer candidates to generate new string values for parameters. - Returns a dictionary mapping ParameterNode objects to new string values, or None on failure. - """ - print_color("Attempting to generate candidate using LLM...", "blue") - if not self.buffer: - print_color("LLM generation: Buffer is empty, cannot provide context to LLM.", "yellow") - return None - - sorted_buffer = sorted(list(self.buffer), key=lambda c: c.get('ucb_score', -float('inf')), reverse=True) - prompt_candidates = sorted_buffer - - serializable_candidate_summaries = [] - for cand_entry in prompt_candidates: - summary = { - "parameters": {getattr(p,'py_name'): copy.deepcopy(p.data) for p in cand_entry['params']}, - "eval_count": cand_entry['eval_count'], - "ucb_score": round(cand_entry.get('ucb_score',0), 4), - } - serializable_candidate_summaries.append(summary) - - example_param_structure_json_str = {getattr(p,'py_name'): copy.deepcopy(p.data) for p in self.agent.parameters()} - - prompt_messages = [ - {"role": "system", "content": "You are an expert in model optimization. Your task is to propose new string values for model parameters with high UCB scores. Please output ONLY a valid JSON dictionary where keys are parameter names and values are the new string values for those parameters, matching the example structure provided. Do not add any explanations or markdown formatting around the JSON."}, - {"role": "user", "content": f"Here are some current candidates from the search buffer and their statistics:\\n{serializable_candidate_summaries}\\n\\nHere is an example of the required JSON output structure (parameter names as keys, new string values as values):\\n{example_param_structure_json_str}\\n\\nPlease generate a new set of parameters in exactly the same JSON format. Make sure use double quotes for the keys and values."} - ] - - print_color(f"LLM prompt (summary): {len(prompt_candidates)} candidates, structure example provided.", "magenta") - - llm_response = self.llm(messages=prompt_messages) - llm_response_str = llm_response.choices[0].message.content - - if not llm_response_str: - print_color("LLM returned an empty response.", "red") - return None - - # Clean the response string - cleaned_llm_response_str = llm_response_str.strip() - if cleaned_llm_response_str.startswith("```json"): - cleaned_llm_response_str = cleaned_llm_response_str[7:] - if cleaned_llm_response_str.endswith("```"): - cleaned_llm_response_str = cleaned_llm_response_str[:-3] - elif cleaned_llm_response_str.startswith("```"): - cleaned_llm_response_str = cleaned_llm_response_str[3:] - if cleaned_llm_response_str.endswith("```"): - cleaned_llm_response_str = cleaned_llm_response_str[:-3] - cleaned_llm_response_str = cleaned_llm_response_str.strip() - - if not cleaned_llm_response_str: - print_color("LLM response was empty after cleaning markdown/whitespace.", "red") - return None - - print_color(f"Cleaned LLM response: '{cleaned_llm_response_str}'", "magenta") - - # Fix common JSON formatting issues from LLM responses - try: - llm_params_raw = json.loads(cleaned_llm_response_str) - except json.JSONDecodeError as e: - print_color(f"Initial JSON parsing failed: {e}", "yellow") - print_color("Attempting to fix JSON formatting...", "yellow") - - fixed_json_str = smart_quote_replacement(cleaned_llm_response_str) - - try: - llm_params_raw = json.loads(fixed_json_str) - print_color("Successfully fixed JSON formatting", "green") - except json.JSONDecodeError as e2: - print_color(f"Smart quote replacement failed: {e2}", "yellow") - try: - simple_fixed = cleaned_llm_response_str.replace("'", '"') - llm_params_raw = json.loads(simple_fixed) - print_color("Fallback simple replacement succeeded", "green") - except json.JSONDecodeError as e3: - print_color(f"All JSON parsing attempts failed: {e3}", "red") - print_color("Returning the candidate with the highest UCB score in the buffer.", "red") - return max(self.buffer, key=lambda c: c.get('ucb_score', -float('inf')))['params'] - - if not isinstance(llm_params_raw, dict): - print_color(f"LLM output was not a JSON dictionary after parsing: {type(llm_params_raw)}", "red") - print_color("Returning the candidate with the highest UCB score in the buffer.", "red") - return max(self.buffer, key=lambda c: c.get('ucb_score', -float('inf')))['params'] - - candidate_params_dict = self.construct_update_dict(llm_params_raw) - return candidate_params_dict - - def construct_update_dict(self, suggestion: Dict[str, Any]) -> Dict[ParameterNode, Any]: - """Convert the suggestion in text into the right data type.""" - update_dict = {} - for node in self.agent.parameters(): - if node.trainable and node.py_name in suggestion: - try: - formatted_suggestion = suggestion[node.py_name] - if type(formatted_suggestion) == str and 'def' in formatted_suggestion: - formatted_suggestion = format_str(formatted_suggestion, mode=FileMode()) - update_dict[node] = type(node.data)(formatted_suggestion) - except (ValueError, KeyError) as e: - if getattr(self, 'ignore_extraction_error', False): - warnings.warn( - f"Cannot convert the suggestion '{suggestion[node.py_name]}' for {node.py_name} to the right data type" - ) - else: - raise e - return update_dict - + return max(buffer, key=lambda c: c['ucb_score']) \ No newline at end of file From d0568090056f59264624fc8bff72012127e48390 Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 18 Jun 2025 15:17:46 -0700 Subject: [PATCH 035/314] add refactoring XML --- opto/optimizers/optoprime_v2.py | 96 ++++++++++++++++++++++++++++++--- 1 file changed, 89 insertions(+), 7 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index f0c78258..60daf5fc 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -1,7 +1,16 @@ import json +from typing import Any, List, Dict, Union, Tuple from textwrap import dedent, indent -from opto.optimizers.optoprime import OptoPrime +from dataclasses import dataclass, asdict +from opto.optimizers.optoprime import OptoPrime, ProblemInstance +from opto.trace.nodes import ParameterNode, Node, MessageNode +from opto.trace.propagators import TraceGraph, GraphPropagator +from opto.trace.propagators.propagators import Propagator + +from opto.utils.llm import AbstractModel, LLM +from opto.optimizers.buffers import FIFOBuffer +import copy class OptoPrimeV2(OptoPrime): # This is generic representation prompt, which just explains how to read the problem. @@ -22,7 +31,9 @@ class OptoPrimeV2(OptoPrime): In #Variables, #Inputs, #Outputs, and #Others, the format is: - = + + () = + If is (code), it means is the source code of a python code, which may include docstring and definitions. """ @@ -33,11 +44,12 @@ class OptoPrimeV2(OptoPrime): output_format_prompt = dedent( """ - Output_format: Your output should be in the following json format, satisfying the json syntax: - - {{ - "reasoning": , - "answer": , + Output_format: Your output should be in the following XML/HTML format: + + + Your reasoning + + "suggestion": {{ : , : , @@ -111,6 +123,76 @@ class OptoPrimeV2(OptoPrime): "documentation": "#Documentation", } + def __init__( + self, + parameters: List[ParameterNode], + llm: AbstractModel = None, + *args, + propagator: Propagator = None, + objective: Union[None, str] = None, + ignore_extraction_error: bool = True, # ignore the type conversion error when extracting updated values from LLM's suggestion + include_example=False, # TODO # include example problem and response in the prompt + memory_size=0, # Memory size to store the past feedback + max_tokens=4096, + log=True, + prompt_symbols=None, + **kwargs, + ): + super().__init__(parameters, *args, propagator=propagator, **kwargs) + self.ignore_extraction_error = ignore_extraction_error + self.llm = llm or LLM() + self.objective = objective or self.default_objective + self.example_problem = ProblemInstance.problem_template.format( + instruction=self.default_objective, + code="y = add(x=a,y=b)\nz = subtract(x=y, y=c)", + documentation="add: add x and y \nsubtract: subtract y from x", + variables="(int) a = 5", + constraints="a: a > 0", + outputs="(int) z = 1", + others="(int) y = 6", + inputs="(int) b = 1\n(int) c = 5", + feedback="The result of the code is not as expected. The result should be 10, but the code returns 1", + stepsize=1, + ) + self.example_response = dedent( + """ + {"reasoning": 'In this case, the desired response would be to change the value of input a to 14, as that would make the code return 10.', + "suggestion": {"a": 10} + } + """ + ) + + self.include_example = include_example + self.max_tokens = max_tokens + self.log = [] if log else None + self.summary_log = [] if log else None + self.memory = FIFOBuffer(memory_size) + self.prompt_symbols = copy.deepcopy(self.default_prompt_symbols) + if prompt_symbols is not None: + self.prompt_symbols.update(prompt_symbols) + + @staticmethod + def repr_node_value(node_dict): + temp_list = [] + for k, v in node_dict.items(): + if "__code" not in k: + temp_list.append(f"\n({type(v[0]).__name__}) {k}={v[0]}\n") + else: + temp_list.append(f"\n(code) {k}:{v[0]}\n") + return "\n".join(temp_list) + + @staticmethod + def repr_node_constraint(node_dict): + temp_list = [] + for k, v in node_dict.items(): + if "__code" not in k: + if v[1] is not None: + temp_list.append(f"\n({type(v[0]).__name__}) {k}: {v[1]}\n") + else: + if v[1] is not None: + temp_list.append(f"\n(code) {k}: {v[1]}\n") + return "\n".join(temp_list) + def construct_prompt(self, summary, mask=None, *args, **kwargs): """Construct the system and user prompt.""" system_prompt = ( From 46997c86dfece45dd3de9f4e2476dc2a1c0cdcc8 Mon Sep 17 00:00:00 2001 From: Xuanfei Ren Date: Wed, 18 Jun 2025 19:19:41 -0500 Subject: [PATCH 036/314] Modified the code based on the comments in PR page --- opto/trainer/algorithms/UCBsearch.py | 28 +++++++++++++------ .../algorithms/beamsearch_algorithm.py | 6 ++-- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/opto/trainer/algorithms/UCBsearch.py b/opto/trainer/algorithms/UCBsearch.py index 0a136eff..30277157 100644 --- a/opto/trainer/algorithms/UCBsearch.py +++ b/opto/trainer/algorithms/UCBsearch.py @@ -27,7 +27,8 @@ def __init__(self, agent: trace.Module, optimizer, max_buffer_size: int = 10, - ucb_exploration_factor: float = 1.0, + ucb_exploration_factor: float = 1.0, # Controls exploration vs exploitation tradeoff in UCB selection + # UCB formula: μ(a) + c * sqrt(ln(t) / n(a)), c is the exploration factor logger=None, num_threads: int = None, *args, @@ -36,6 +37,8 @@ def __init__(self, self.buffer = deque(maxlen=max_buffer_size) self.max_buffer_size = max_buffer_size + # UCB exploration factor: Higher values encourage more exploration of less-tested candidates, + # lower values favor exploitation of well-performing candidates. self.ucb_exploration_factor = ucb_exploration_factor # To ensure optimizer_step can be called with bypassing=True if needed. @@ -76,7 +79,7 @@ def _evaluate_candidate(self, print_color("Evaluation dataset is empty or invalid. Returning score -inf, count 0.", color='yellow') return -np.inf, 0 - original_params = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} + original_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters()} self.optimizer.update(params_to_eval_dict) eval_xs, eval_infos = self._sample_minibatch(dataset, evaluation_batch_size) # Use evaluation_batch_size @@ -114,6 +117,8 @@ def _calculate_ucb(self, candidate_buffer_entry: Dict, total_tracked_evaluations if total_tracked_evaluations == 0: # Should not happen if we init with one eval total_tracked_evaluations = 1 + # UCB exploration term: ucb_exploration_factor scales the confidence interval + # Higher factor = more exploration, lower factor = more exploitation exploration_term = self.ucb_exploration_factor * \ math.sqrt(math.log(total_tracked_evaluations) / candidate_buffer_entry['eval_count']) @@ -131,6 +136,7 @@ def train(self, guide, # Guide for train_dataset (feedback generation AND evaluation) train_dataset: Dict[str, List[Any]], *, + validation_dataset: Optional[Dict[str, List[Any]]] = None, # Validation set for evaluation, defaults to train_dataset num_search_iterations: int = 100, train_batch_size: int = 2, evaluation_batch_size: int = 20, # Renamed from validation_batch_size, used for all explicit evaluations @@ -146,6 +152,10 @@ def train(self, """ Main training loop for UCB Search Algorithm. """ + # Default validation_dataset to train_dataset if not provided + if validation_dataset is None: + validation_dataset = train_dataset + num_threads = num_threads or self.num_threads log_frequency = log_frequency or eval_frequency self.min_score = min_score_for_agent_update # Used by parent's evaluate if called, or our own _evaluate_candidate @@ -161,10 +171,10 @@ def train(self, } # 0. Evaluate the initial parameter on samples of the validation set and add it to the buffer. - print_color("Evaluating initial parameters using train_dataset samples...", 'cyan') - initial_params_dict = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} + print_color("Evaluating initial parameters using validation_dataset samples...", 'cyan') + initial_params_dict = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters()} initial_score, initial_evals = self._evaluate_candidate( - initial_params_dict, train_dataset, guide, evaluation_batch_size, num_threads # Use train_dataset and guide + initial_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads # Use validation_dataset and guide ) self._total_evaluations_tracker += initial_evals total_samples += initial_evals @@ -173,7 +183,7 @@ def train(self, 'params': initial_params_dict, 'score_sum': initial_score * initial_evals if initial_score > -np.inf else 0, # Store sum for accurate mean later 'eval_count': initial_evals, - 'ucb_score': 0.0, # Will be updated + 'ucb_score': None, # avoid accidental reads before it's initialized 'iteration_created': 0 } self.buffer.append(initial_candidate_entry) @@ -236,7 +246,7 @@ def train(self, if not isinstance(a_prime_params_dict, dict) or not a_prime_params_dict: print_color(f"Iter {iteration}: Optimizer.step did not return a valid param dict for a_prime. Using current agent params as a_prime.", 'yellow') # Fallback: if step modified agent in-place and didn't return dict, current agent state is a_prime - a_prime_params_dict = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} + a_prime_params_dict = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters()} except Exception as e: print_color(f"Iter {iteration}: Error during optimizer.step for a_prime: {e}. Skipping candidate generation.", 'red') @@ -244,7 +254,7 @@ def train(self, # 4. Evaluate 'a_prime' on samples of validation set a_prime_score, a_prime_evals = self._evaluate_candidate( - a_prime_params_dict, train_dataset, guide, evaluation_batch_size, num_threads # Use train_dataset and guide + a_prime_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads # Use validation_dataset and guide ) self._total_evaluations_tracker += a_prime_evals total_samples += evaluation_batch_size + train_batch_size @@ -263,7 +273,7 @@ def train(self, 'params': a_prime_params_dict, 'score_sum': a_prime_score * a_prime_evals, # Store sum 'eval_count': a_prime_evals, - 'ucb_score': 0.0, # Will be updated + 'ucb_score': None, # avoid accidental reads before it's initializad 'iteration_created': iteration } diff --git a/opto/trainer/algorithms/beamsearch_algorithm.py b/opto/trainer/algorithms/beamsearch_algorithm.py index 09a13578..a6eda61e 100644 --- a/opto/trainer/algorithms/beamsearch_algorithm.py +++ b/opto/trainer/algorithms/beamsearch_algorithm.py @@ -67,7 +67,7 @@ def train(self, print_color(f"Using validation_dataset_size={validation_dataset_size} for intermediate evaluations", 'blue') # Store original parameters to restore after each exploration - original_params = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} + original_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters()} # Dictionary to track metrics during beam search metrics = { @@ -384,7 +384,7 @@ def select(self, If return_scores is True: Tuple of (list of parameters, list of scores) """ # Store current parameters to restore later - current_params = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} + current_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters()} # List to store (score, params) pairs scored_candidates = [] @@ -495,7 +495,7 @@ def train(self, print_color(f"Using validation_dataset_size={validation_dataset_size} for intermediate evaluations", 'blue') # Store original parameters - original_params = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} + original_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters()} # Dictionary to track metrics metrics = { From 72b95d65c3e6142d549cb1d2f7da2289987b6c41 Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 18 Jun 2025 17:46:58 -0700 Subject: [PATCH 037/314] update signature, add test to check updating bundle functions --- opto/trace/modules.py | 10 +++++++--- tests/unit_tests/test_modules.py | 6 +++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/opto/trace/modules.py b/opto/trace/modules.py index ee779100..bf33d6a3 100644 --- a/opto/trace/modules.py +++ b/opto/trace/modules.py @@ -7,6 +7,8 @@ from opto.trace.nodes import ParameterNode from opto.trace.projections import Projection, BlackCodeFormatter +import functools +from typing import List, Optional def model(cls): """ @@ -14,10 +16,13 @@ def model(cls): """ class ModelWrapper(cls, Module): - def model_dump(self, filename, projection: Projection = BlackCodeFormatter()): + def model_dump(self, filename, projections: Optional[List[Projection]] = None): """Dump the model's source code to a file, including all methods and attributes. Ignores dunder methods unless they were overridden by the user. """ + if projections is None: + projections = [BlackCodeFormatter()] + trace_model_body = f"class {cls.__name__}:\n" # Get all members of the class @@ -86,8 +91,7 @@ def replace_node(match): trace_model_body = re.sub(node_pattern, replace_node, trace_model_body) - if projection is not None: - trace_model_body = projection.project(trace_model_body) + trace_model_body = functools.reduce(lambda body, proj: proj.project(body), projections, trace_model_body) with open(filename, "w") as f: f.write(trace_model_body) diff --git a/tests/unit_tests/test_modules.py b/tests/unit_tests/test_modules.py index 46971917..5494a5ce 100644 --- a/tests/unit_tests/test_modules.py +++ b/tests/unit_tests/test_modules.py @@ -315,6 +315,8 @@ def non_trainable_method(self, y): obj._trainable._data = 100 obj._non_trainable._data = 200 + obj.trainable_method.parameter._data = "def trainable_method(self, x):\n return x + 3" + temp_file = "temp_mixed.py" try: obj.model_dump(temp_file) @@ -331,10 +333,12 @@ def non_trainable_method(self, y): assert "@bundle" not in content # Check if methods are present but without decorators assert "def trainable_method" in content + assert "return x + 3" in content assert "def non_trainable_method" in content # Check if regular attribute is present assert "regular_attr" in content finally: if os.path.exists(temp_file): - os.remove(temp_file) + pass + # os.remove(temp_file) From 587f36def51496788a91e27d3a9d6e5f60943e46 Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 18 Jun 2025 17:50:24 -0700 Subject: [PATCH 038/314] add the import test --- tests/unit_tests/test_modules.py | 62 ++++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/tests/unit_tests/test_modules.py b/tests/unit_tests/test_modules.py index 5494a5ce..1934ae5b 100644 --- a/tests/unit_tests/test_modules.py +++ b/tests/unit_tests/test_modules.py @@ -339,6 +339,64 @@ def non_trainable_method(self, y): assert "regular_attr" in content finally: if os.path.exists(temp_file): - pass - # os.remove(temp_file) + os.remove(temp_file) +def test_model_dump_and_import(): + @model + class StrangeCalculator: + def __init__(self): + super().__init__() + self.offset = node(2, trainable=True) + self.multiplier = node(1.5, trainable=True) + + @bundle(trainable=True) + def add(self, x, y): + """Add two numbers with an offset""" + return x + y + self.offset + + @bundle(trainable=True) + def multiply(self, x, y): + """Multiply two numbers with a multiplier""" + return x * y * self.multiplier + + # Create instance and modify parameters + calc = StrangeCalculator() + calc.offset._data = 3 + calc.multiplier._data = 2.0 + calc.add.parameter._data = "def add(self, x, y):\n return x + y + self.offset + 1" + calc.multiply.parameter._data = "def multiply(self, x, y):\n return x * y * self.multiplier * 2" + + # Dump the model + temp_file = "temp_calculator.py" + try: + calc.model_dump(temp_file) + + # Import the dumped class + import importlib.util + spec = importlib.util.spec_from_file_location("temp_calculator", temp_file) + temp_module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(temp_module) + + # Get the imported class + ImportedCalculator = temp_module.StrangeCalculator + + # Create instance and test functionality + imported_calc = ImportedCalculator() + + # Test the modified behavior + result_add = imported_calc.add(5, 3) + result_multiply = imported_calc.multiply(4, 2) + + # Verify the results match our expected modified behavior + # add: 5 + 3 + 3 + 1 = 12 + # multiply: 4 * 2 * 2.0 * 2 = 32 + assert result_add == 12, f"Expected 12, got {result_add}" + assert result_multiply == 32, f"Expected 32, got {result_multiply}" + + # Verify the attributes have the correct values + assert imported_calc.offset == 3 + assert imported_calc.multiplier == 2.0 + + finally: + if os.path.exists(temp_file): + os.remove(temp_file) From 8fb2e199dfc9610545892ccb5245add40eac8ce0 Mon Sep 17 00:00:00 2001 From: adith387 Date: Thu, 19 Jun 2025 17:10:06 -0700 Subject: [PATCH 039/314] Update README.md with link to roadmap --- README.md | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index f90f7084..e5e94e9a 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ losses, natural language text, compiler errors, etc.). Trace generalizes the bac propagating an AI system's execution trace. Trace is implemented as a PyTorch-like Python library. Users write Python code directly and can use Trace primitives to optimize certain parts, just like training neural networks! -[Paper](https://arxiv.org/abs/2406.16218) | [Project website](https://microsoft.github.io/Trace/) | [Documentation](https://microsoft.github.io/Trace/intro.html) | [Blogpost](https://www.microsoft.com/en-us/research/blog/tracing-the-path-to-self-adapting-ai-agents/) | [Discord channel](https://discord.gg/4VeAvwFcWy) +[Paper](https://arxiv.org/abs/2406.16218) | [Project website](https://microsoft.github.io/Trace/) | [Documentation](https://microsoft.github.io/Trace/intro.html) | [Blogpost](https://www.microsoft.com/en-us/research/blog/tracing-the-path-to-self-adapting-ai-agents/) | [Discord channel](https://discord.gg/4VeAvwFcWy) | [Roadmap](https://docs.google.com/spreadsheets/d/1dMoECd2Soj6bATpkNDeaMxl0ymOYCtGq7ZiHr0JRdJU/edit?usp=sharing)

drawing @@ -38,6 +38,7 @@ git is unable to clone the repository. ## Updates +- **2025.5.9** Adith Swaminathan gave a talk at Netflix Workshop on Personalization, Recommendation and Search (PRS)[https://prs2025.splashthat.com/] - **2025.5.1** Ching-An Cheng gave a talk at 2nd Texas Colloquium on Distributed Learning (TL;DR)[https://sites.google.com/view/tldr-2025] - **2025.2.7** Trace was featured in the [G-Research NeurIPS highlight](https://www.gresearch.com/news/neurips-paper-reviews-2024-8/) by the Science Director Hugh Salimbeni. - **2024.12.10** Trace was demoed in person at NeurIPS 2024 Expo. @@ -391,6 +392,26 @@ Explains the role of feedback in LLM-based optimizers. An early work that influe +## Contributing + +This project welcomes contributions and suggestions. Most contributions require you to agree to a +Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us +the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. + +When you submit a pull request, a CLA bot will automatically determine whether you need to provide +a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions +provided by the bot. You will only need to do this once across all repos using our CLA. + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or +contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +## Roadmap + +View our [Public Roadmap](https://docs.google.com/spreadsheets/d/1dMoECd2Soj6bATpkNDeaMxl0ymOYCtGq7ZiHr0JRdJU/edit?usp=sharing) + +You can learn about the features we are working on, areas where you can contribute, and future plans for Trace. + ## Evaluation A previous version of Trace was tested with gpt-4-0125-preview on numerical optimization, simulated traffic control, @@ -419,20 +440,6 @@ see [example](https://community.openai.com/t/gpt-4o-doesnt-consistently-respect- - The system should not be used in highly regulated domains where inaccurate outputs could suggest actions that lead to injury or negatively impact an individual's legal, financial, or life opportunities. -## Contributing - -This project welcomes contributions and suggestions. Most contributions require you to agree to a -Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us -the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. - -When you submit a pull request, a CLA bot will automatically determine whether you need to provide -a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions -provided by the bot. You will only need to do this once across all repos using our CLA. - -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). -For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or -contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. - ## Trademarks This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft From addbc44f29367d1d65a414a48718538ccaddb2fc Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Fri, 20 Jun 2025 15:40:07 -0500 Subject: [PATCH 040/314] Fix the bug by using optimizer.parameters --- opto/trainer/algorithms/UCBsearch.py | 6 +++--- opto/trainer/algorithms/beamsearch_algorithm.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/opto/trainer/algorithms/UCBsearch.py b/opto/trainer/algorithms/UCBsearch.py index 30277157..55114772 100644 --- a/opto/trainer/algorithms/UCBsearch.py +++ b/opto/trainer/algorithms/UCBsearch.py @@ -79,7 +79,7 @@ def _evaluate_candidate(self, print_color("Evaluation dataset is empty or invalid. Returning score -inf, count 0.", color='yellow') return -np.inf, 0 - original_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters()} + original_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} self.optimizer.update(params_to_eval_dict) eval_xs, eval_infos = self._sample_minibatch(dataset, evaluation_batch_size) # Use evaluation_batch_size @@ -172,7 +172,7 @@ def train(self, # 0. Evaluate the initial parameter on samples of the validation set and add it to the buffer. print_color("Evaluating initial parameters using validation_dataset samples...", 'cyan') - initial_params_dict = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters()} + initial_params_dict = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} initial_score, initial_evals = self._evaluate_candidate( initial_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads # Use validation_dataset and guide ) @@ -246,7 +246,7 @@ def train(self, if not isinstance(a_prime_params_dict, dict) or not a_prime_params_dict: print_color(f"Iter {iteration}: Optimizer.step did not return a valid param dict for a_prime. Using current agent params as a_prime.", 'yellow') # Fallback: if step modified agent in-place and didn't return dict, current agent state is a_prime - a_prime_params_dict = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters()} + a_prime_params_dict = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} except Exception as e: print_color(f"Iter {iteration}: Error during optimizer.step for a_prime: {e}. Skipping candidate generation.", 'red') diff --git a/opto/trainer/algorithms/beamsearch_algorithm.py b/opto/trainer/algorithms/beamsearch_algorithm.py index a6eda61e..7f0fb423 100644 --- a/opto/trainer/algorithms/beamsearch_algorithm.py +++ b/opto/trainer/algorithms/beamsearch_algorithm.py @@ -67,7 +67,7 @@ def train(self, print_color(f"Using validation_dataset_size={validation_dataset_size} for intermediate evaluations", 'blue') # Store original parameters to restore after each exploration - original_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters()} + original_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} # Dictionary to track metrics during beam search metrics = { @@ -384,7 +384,7 @@ def select(self, If return_scores is True: Tuple of (list of parameters, list of scores) """ # Store current parameters to restore later - current_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters()} + current_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} # List to store (score, params) pairs scored_candidates = [] @@ -495,7 +495,7 @@ def train(self, print_color(f"Using validation_dataset_size={validation_dataset_size} for intermediate evaluations", 'blue') # Store original parameters - original_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters()} + original_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} # Dictionary to track metrics metrics = { From 01c525dc90230b80f31ab80bc35bf270e65b8def Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Fri, 20 Jun 2025 18:36:42 -0500 Subject: [PATCH 041/314] Add logging for initial, validation, and test scores in Beamsearch algorithms --- .../algorithms/beamsearch_algorithm.py | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/opto/trainer/algorithms/beamsearch_algorithm.py b/opto/trainer/algorithms/beamsearch_algorithm.py index 7f0fb423..0451455e 100644 --- a/opto/trainer/algorithms/beamsearch_algorithm.py +++ b/opto/trainer/algorithms/beamsearch_algorithm.py @@ -92,6 +92,9 @@ def train(self, initial_test_score = np.mean(initial_test_scores) if all([s is not None for s in initial_test_scores]) else -np.inf print_color(f"Initial test score: {initial_test_score:.4f}", 'yellow') + # Log initial test score + self.logger.log('Initial test score', initial_test_score, 0, color='blue') + # Add initial score to metrics for logging metrics['test_scores'].append(initial_test_score) metrics['test_depths'].append(1) # Represent initial score at depth 0 @@ -159,6 +162,13 @@ def train(self, print_color(f"Depth {depth+1} - Best validation score: {best_score:.4f}", 'green') + # Log validation metrics + step_num = depth + 1 + self.logger.log('Best validation score', best_score, step_num, color='green') + self.logger.log('Average validation score', np.mean(scores), step_num, color='cyan') + self.logger.log('Min validation score', min(scores), step_num, color='yellow') + self.logger.log('Max validation score', max(scores), step_num, color='magenta') + # Evaluate on test set every test_frequency steps if test_dataset is not None and ((depth + 1) % test_frequency == 0): # Update agent with best parameters from this depth @@ -192,6 +202,9 @@ def train(self, metrics['test_depths'].append(depth + 1) print_color(f"Depth {depth+1} - Test score: {test_score:.4f}", 'magenta') + + # Log test score + self.logger.log('Periodic test score', test_score, step_num, color='magenta') # Final selection - choose the best beam using FULL validation set print_color("\n===== Final Selection Using Full Validation Set =====", 'blue') @@ -217,6 +230,10 @@ def train(self, best_params = best_beams[0] final_validation_score = final_val_scores[0] if final_val_scores else -np.inf + # Log final validation score + final_step = max_depth + 1 + self.logger.log('Final validation score', final_validation_score, final_step, color='blue') + # Apply the best parameters self.optimizer.update(best_params) @@ -259,6 +276,9 @@ def train(self, if final_test_score is not None: print_color(f"BEST BEAM - Test score: {final_test_score:.4f}", 'green') + # Log final test score + self.logger.log('Final test score', final_test_score, final_step, color='green') + # Save the best model if save_frequency is not None and save_frequency > 0: self.save_agent(save_path, 0) @@ -517,6 +537,10 @@ def train(self, ) initial_test_score = np.mean(initial_test_scores) if all([s is not None for s in initial_test_scores]) else -np.inf print_color(f"Initial test score: {initial_test_score:.4f}", 'yellow') + + # Log initial test score + self.logger.log('Initial test score', initial_test_score, 0, color='blue') + metrics['test_scores'].append(initial_test_score) metrics['test_depths'].append(1) # Start depth at 1 for consistency @@ -574,6 +598,14 @@ def train(self, metrics['depth_scores'].append(scores) print_color(f"Depth {depth+1} - Best validation score: {best_score_this_depth:.4f}", 'green') + # Log validation metrics + step_num = depth + 1 + self.logger.log('Best validation score', best_score_this_depth, step_num, color='green') + self.logger.log('Average validation score', np.mean(scores), step_num, color='cyan') + self.logger.log('Min validation score', min(scores), step_num, color='yellow') + self.logger.log('Max validation score', max(scores), step_num, color='magenta') + self.logger.log('History buffer size', len(self.parameter_history), step_num, color='orange') + best_idx = scores.index(best_score_this_depth) # Find index of best score best_params = beams[best_idx] # Get corresponding params @@ -609,6 +641,9 @@ def train(self, metrics['test_scores'].append(test_score) metrics['test_depths'].append(depth + 1) print_color(f"Depth {depth+1} - Test score: {test_score:.4f}", 'magenta') + + # Log test score + self.logger.log('Periodic test score', test_score, step_num, color='magenta') # >>> End Main Loop <<< @@ -624,7 +659,11 @@ def train(self, final_validation_score = final_val_scores[0] if final_val_scores else -np.inf best_params = best_beams[0] if best_beams else original_params # Fallback to original if empty - # Apply best parameters + # Log final validation score + final_step = max_depth + 1 + self.logger.log('Final validation score', final_validation_score, final_step, color='blue') + + # Apply the best parameters self.optimizer.update(best_params) # Print final parameters @@ -641,6 +680,9 @@ def train(self, final_test_score = np.mean(final_test_scores_eval) if all([s is not None for s in final_test_scores_eval]) else -np.inf print_color(f"BEST BEAM - Test score: {final_test_score:.4f}", 'green') + # Log final test score + self.logger.log('Final test score', final_test_score, final_step, color='green') + # Save agent if configured if kwargs.get('save_frequency', None) is not None and kwargs['save_frequency'] > 0: self.save_agent(kwargs.get('save_path', "checkpoints/agent.pkl"), 0) From 0b9c2844c7b927517d78934234330a1045f1dee7 Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Fri, 20 Jun 2025 18:48:10 -0500 Subject: [PATCH 042/314] Add detailed logging for UCB search algorithm --- opto/trainer/algorithms/UCBsearch.py | 34 ++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/opto/trainer/algorithms/UCBsearch.py b/opto/trainer/algorithms/UCBsearch.py index 55114772..9ff6f61b 100644 --- a/opto/trainer/algorithms/UCBsearch.py +++ b/opto/trainer/algorithms/UCBsearch.py @@ -179,6 +179,10 @@ def train(self, self._total_evaluations_tracker += initial_evals total_samples += initial_evals + # Log initial evaluation + self.logger.log('Initial UCB score', initial_score, 0, color='blue') + self.logger.log('Initial evaluations', initial_evals, 0, color='cyan') + initial_candidate_entry = { 'params': initial_params_dict, 'score_sum': initial_score * initial_evals if initial_score > -np.inf else 0, # Store sum for accurate mean later @@ -200,6 +204,9 @@ def train(self, self._update_buffer_ucb_scores() # Ensure UCB scores are fresh action_candidate_a = self.select(self.buffer) + # Log selected action UCB score + self.logger.log('Selected action UCB', action_candidate_a['ucb_score'], iteration, color='magenta') + self.logger.log('Selected action mean score', action_candidate_a['score_sum']/(action_candidate_a['eval_count'] or 1), iteration, color='cyan') print_color(f"Iter {iteration}/{num_search_iterations}: ", 'blue') @@ -259,6 +266,11 @@ def train(self, self._total_evaluations_tracker += a_prime_evals total_samples += evaluation_batch_size + train_batch_size metrics['new_candidate_scores'].append(a_prime_score) + + # Log new candidate performance + self.logger.log('New candidate score', a_prime_score, iteration, color='green') + self.logger.log('Training batch score', score_for_a_on_train_batch, iteration, color='yellow') + print_color(f"Iter {iteration}: New candidate a_prime generated. Validation Score: {a_prime_score:.4f}, Evals: {a_prime_evals}", 'cyan') # 5. Update the stats of 'a' (action_candidate_a) based on the training batch experience @@ -310,6 +322,15 @@ def train(self, "total_evaluations_tracker": self._total_evaluations_tracker, "total_samples": total_samples # Add new metric } + + # Log all important metrics + self.logger.log('Best candidate score', log_data['best_score'], iteration, color='green') + self.logger.log('Buffer size', log_data['buffer_size'], iteration, color='blue') + self.logger.log('Buffer average score', log_data['buffer_avg_score'], iteration, color='cyan') + self.logger.log('Buffer average evaluations', log_data['buffer_avg_evals'], iteration, color='orange') + self.logger.log('Total evaluations tracker', log_data['total_evaluations_tracker'], iteration, color='magenta') + self.logger.log('Total samples processed', log_data['total_samples'], iteration, color='yellow') + print_color(f"Log @ Iter {iteration}: Best score in buffer: {log_data['best_score']:.4f}, Buffer size: {log_data['buffer_size']}, Total samples: {total_samples}", 'green') # Save agent (e.g., the one with highest mean score in buffer) @@ -321,13 +342,26 @@ def train(self, # End of search loop print_color("UCB search finished.", 'blue') + + # Log final training summary + final_iteration = num_search_iterations + self.logger.log('UCB search completed', final_iteration, final_iteration, color='blue') + self.logger.log('Final total samples', total_samples, final_iteration, color='magenta') + if not self.buffer: print_color("Buffer is empty at the end of search. No best candidate found.", 'red') + self.logger.log('Final status', 'Buffer empty - no best candidate', final_iteration, color='red') return metrics, -np.inf # Select the best candidate based on highest mean score (exploitation) final_best_candidate = max(self.buffer, key=lambda c: c['score_sum'] / (c['eval_count'] or 1E-9)) final_best_score = final_best_candidate['score_sum'] / (final_best_candidate['eval_count'] or 1E-9) + + # Log final results + self.logger.log('Final best score', final_best_score, final_iteration, color='green') + self.logger.log('Final best candidate evaluations', final_best_candidate['eval_count'], final_iteration, color='cyan') + self.logger.log('Final buffer size', len(self.buffer), final_iteration, color='blue') + print_color(f"Final best candidate: Mean Score {final_best_score:.4f}, Evals {final_best_candidate['eval_count']}", 'green') # Load best parameters into the agent From 743eb0f5718e989ec044035af991205913273225 Mon Sep 17 00:00:00 2001 From: Xavier Daull Date: Sat, 21 Jun 2025 17:14:01 +0200 Subject: [PATCH 043/314] refined optoprimmulti for more stability --- opto/optimizers/optoprimemulti.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/opto/optimizers/optoprimemulti.py b/opto/optimizers/optoprimemulti.py index 19dadb70..be7cfa30 100644 --- a/opto/optimizers/optoprimemulti.py +++ b/opto/optimizers/optoprimemulti.py @@ -2,10 +2,9 @@ import json from typing import List, Dict - - from opto.trace.propagators import GraphPropagator from opto.optimizers.optoprime import OptoPrime +from opto.utils.llm import LLMFactory from concurrent.futures import ThreadPoolExecutor, as_completed @@ -69,6 +68,8 @@ def _get_llms_for_generation(self, num_responses: int): llm = self._get_llm_for_profile(profile) llms.append(llm) + return llms + def call_llm( self, system_prompt: str, @@ -209,11 +210,10 @@ def generate_candidates( if self.llm_profiles is not None and len(self.llm_profiles) > 0 and generation_technique == "multi_llm": llms = self._get_llms_for_generation(num_responses) - temperatures = [temp_max - i * (temp_max - temp_min) / max(1, num_responses - 1) for i in range(num_responses)] # Prepare arguments for parallel execution arg_dicts = [] - for i, (llm, temp) in enumerate(zip(llms, temperatures)): + for i, llm in enumerate(llms): profile_name = self.llm_profiles[i % len(self.llm_profiles)] if self.llm_profiles else "default" modified_system_prompt = f"{system_prompt}\n\n[Using {profile_name} model for diverse perspective]" @@ -223,7 +223,7 @@ def generate_candidates( verbose=verbose, max_tokens=max_tokens, num_responses=1, - temperature=temp, + temperature=temp_min, llm=llm # Use specific LLM )) @@ -251,7 +251,7 @@ def generate_candidates( verbose=verbose, max_tokens=max_tokens, num_responses=1, - temperature=0.0, + temperature=temp_min, ) if response and len(response) > 0: @@ -267,7 +267,7 @@ def generate_candidates( f"CANDIDATE {idx + 1}: <<<\n{cand}\n>>>" for idx, cand in enumerate(candidates) ) - meta_prompt = f"{system_prompt}\nGiven the following candidate solutions, propose a new alternative optimal solution to user's prompt using their same JSON format (suggest only trainable codes/variables to modify, never inputs):\n{previous_solutions}\n" + meta_prompt = f"{system_prompt}\nGiven the following prior CANDIDATE solutions, answer with a very different new CANDIDATE optimal solution to user's prompt using their same JSON format (suggest only trainable codes/variables to modify, never inputs):\n{previous_solutions}\n" response = self.call_llm( system_prompt=meta_prompt, @@ -275,7 +275,7 @@ def generate_candidates( verbose=verbose, max_tokens=max_tokens, num_responses=1, - temperature=0.0, + temperature=temp_min, ) if response and len(response) > 0: From 7abbaf2bfb54e12ad79b3c465379b99a3dc1cab5 Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 24 Jun 2025 18:31:42 +0000 Subject: [PATCH 044/314] Update CONTRIBUTING.md and add opto/features. --- CONTRIBUTING.md | 65 +++++++++++++++++++++++++++++++++++++++ opto/features/__init__.py | 0 2 files changed, 65 insertions(+) create mode 100644 CONTRIBUTING.md create mode 100644 opto/features/__init__.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..cdea3af6 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,65 @@ +# Contribution Guideline + +Trace is an actively growing project and under active maintenance and development! We maintain two major branches `main` and `experimental`. The `main` branch is the most stable, version-controlled branch and it is what the PyPI package is linked to. On the other hand, the `experimental` branch is the dev branch, which will change more dynamically in in preparation for the next version update. + +### Review Process and Update Dynamics + +Contribution to these two branches requires going through a review process via PR and passing all unit tests in CI. +Merging a PR requires at least one reviewer different from the contributor, except for those marked as [**LIGHT**] below. + +Here is an outline: + +1. `main` will be regularly updated by PRs based on the development of the `experimental` branch following the [roadmap doc](https://docs.google.com/spreadsheets/d/1dMoECd2Soj6bATpkNDeaMxl0ymOYCtGq7ZiHr0JRdJU/edit?usp=sharing). Each update will result in a version update of the first two digits. + +2. Except for the planned roadmap, `main` will only be updated to fix bugs. Bug fix to what is in `main` should be submitted as PR to `main`. This will trigger a quicker review and result in a version update in the third digit, and the `experimental` branch will then rebase on the updated `main`. + +3. For feature development, PR should be submitted to the `experimental` branch without version update. Generally, the `experimental` branch aims to realize the milestones listed in the next version update in the [roadmap doc](https://docs.google.com/spreadsheets/d/1dMoECd2Soj6bATpkNDeaMxl0ymOYCtGq7ZiHr0JRdJU/edit?usp=sharing). If applicable, new determinstic unit tests should be added under `tests/unit_tests`. Otherwise, an example run script should be added in `examples`. + +4. [**LIGHT**] Bugs fix to the new changes introduced in the `experimental` branch should be submitted as a PR to the `experimental` branch. This PR will be incoporated quickly with a light review. + +5. [**LIGHT**] For contributions under the directory `opto/features`, they should be submitted as PR to the `experimental` branch. These usually are not under roadmap and are content not made as dependable by codes in other directories. That is, contents under `opto/features/A` should not be imported by files other than those under `opto/features/A`. So long as this rule is met, the PR will be incorprated under a light review. + +6. [Exception] Updates to non-coding elements (like documents) does not necessarily require a PR. + +The above is applicable to all contributors including the maintainers. + + +### Communication + +1. Quick questions should be posted on Discord channel. + +2. For bugs, feature requests, contributions, or questions that might be related to a broader audience, post them as issues on the github page. + + +# Steps for Contributions + +We welcome your contributions and involvement. Below are instructions for how to contribute to Trace. + +## Quick Bug Fix + +If there is a minor, isolated bug that can be directly fixed, please report it as an issue or submit a PR to be merged into the `main` branch or `experimental` branch, depending on where the issue arises. + + +## Contributing Feature + +We welcome new ideas. + +### Step 1: Feature Spec Doc +A feature should first be written as a Google Doc (an example is [here](https://docs.google.com/document/d/1FX1ygc8lgFpFn3ni3E2A_DCGtn505PpAM8QaAjEovsA/edit?usp=sharing)). + +### Step 2: Create an Issue +An issue should be created, and under the issue, the doc is linked. People should be allowed to comment on the doc. + +### Step 3: Implement Feature +Create a separate branch, extending from the `experimental` branch. This branch contains all the new features that have not been merged into the `main` branch yet. +Make sure your features are implemented, along with `unit tests` or `examples` to show how it's used. + +### Step 4: Create a Pull Request +Create a PR formally to merge into the experiment branch and request a review. For standalone features, put the changes under `opto/features/`. This will trigger the lightest review that only checks for malicious code, or if the feature does not pass its own unit tests. +For changes to the rest, expect a slightly longer review process as we work out how the changes should be integrated with the core library. + + +### Step 5: Merge into Experimental +Once the request is approved, it will be merged into the `experimental` branch. + + diff --git a/opto/features/__init__.py b/opto/features/__init__.py new file mode 100644 index 00000000..e69de29b From b9945b30f3e9cd555e63454d76d19a0f77c26c6a Mon Sep 17 00:00:00 2001 From: Allen Nie Date: Tue, 24 Jun 2025 11:49:17 -0700 Subject: [PATCH 045/314] Add files via upload --- docs/images/contributing_workflow.png | Bin 0 -> 39433 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/images/contributing_workflow.png diff --git a/docs/images/contributing_workflow.png b/docs/images/contributing_workflow.png new file mode 100644 index 0000000000000000000000000000000000000000..badf28a3b6758fdfb08338466bfb8a9b7c4955eb GIT binary patch literal 39433 zcmeFZXIN9&8YrxY3U*->m8w#tNEf6A2S=m@AwcLbVnAvH4AL=U8x(^=LWijI7CNEB zD1wHj^p1jn1Oh?`op9Gioipb?XPo(SpYPA-uWi_St#`e>u8=Emt^IpW?Af+$+kWlK z7mc=UV@uz*ZToe$UEmjAF2lKP+cdUmU%X)KvweCH7Rob`AhLekh=uh%%cbW(@i-jm z!Lmwpdw*eZ)G)TwiZnRjwlizLggB>Ltg6EaAr=F?t7zf0T_Cp7M=HO?^V;qgdG#NE zqsQ*@-XU5##vT7ttdxXv2;oiHDsK78!AFCgopKww<*=O#H*a+!4AfdDCMTIDow-?L zvLD>v-(RIW*EX11|Cl>WF)QBHy`tGItt^J^kU$=V1sPXw?c^R6*Iyw5kN3&)g*B>j zZf9XNwb58Fp4Uvqva*F9Jrw-dTPAATb{26d0jAZ;$u)x*2##(2) z@c_|wOmCzh6$h)(0P9gV8O8ndfU)uGl(!I~|>M_K<*v-5g7Qe}8V(`g|kIY(3KLDF8cZ zXPs@5b(4^WT_AvUN-omx=i9Y@Y`??0?Rgl#ZNtv7jI_=vL;`&9Y@h4)9B?CS!@g$M zdsuTxI9<>scSV$VE7oB~l9W+$@%A=s3Y(P^DRusCR))mup-b$C(!rWX?mu8<%dWV+ z>@uijw#jzr*xlR9UWZ;KKV@ZWx&)5GtyO$T;KoI;`r++4@Y}UdSnKO;8~Uow`!7yw zzZG>m(#n3w%pQB^Ze`_Cjt4*H@iy!caN3;ZZBBgKSqcFKnk|e2S=kOn8-dk?qDKIV ztijX5;f^2{7XbmL#hnWsJ0WBp;|?D5*m)$zzdc$E5GR&BwE2@|VG#8Ipq?;zFMs=c z9v0T4qTn&P#O<(v#)rt;%k8?@ZP@)_7xyn8rj)U$J-^pPW}y37*(A(Akd~)#;_0QH zl<)@);{eLk+6%(AL7N^ikMrDESQQKa&3x@i4XkVf;QPVUFSh~0A8{TEmTc*ZWIJT0 zw0&-ZvOi?k;hufq^bVngHXH)dM$oF>zfsD(CW6|s3sgMM$*}|MA7`oH4_=?!8~5I% z3UJdAfY>bQJ$-<0!T|+!`L@N0XxJ69))C{*4f%n?T?T7kJ_bb{g*Fc(!ZCsDQPDt*= zSPsJ0znHq)Zcvfrpv|>m^u{8&X0SYH!_Sc(yHwY%j~&p1$Q=g`=3XQQmVL`Ik}GuZ zb~7j7@le2B`$BsG696$)T0YsaoyA2QP_+Mfrwj|X)D8Ty%i6+ZyUNm#KY9YSGdzBN zz^j!v{?nk3+cxl#g~0JZ*v6!Rh){-=n#V|;EsaLA=Dq27E^!-{a0Ia3^tNX90Hn{IT7K#pN{EG46TCVHQ?DHa*T@UYBr0Vyv+vDL!1{7^Fk)OFmgD zs7%w3_9j_ZD9lxP{LFh4f<7CZN#gNNATZCt)BfEiQ9xkg!I5rgrvAao#&bbq-IrB0 z3-E~?V40>fCKn+klNwTkJGbFmnPqTLefb%YG09R8x()jW`=>MtpYNxq+O(MfbbL|4 z7zC}ryA{6E%-Cm+GTT}10AAeh{zg7}8Hy zjDh<2$o_lHeMJWgz1y`G>_DkFpU2$ahE)eUsOwM;2}o;!Q}B40=-;k&-m?$_b|~oY zT(`o#hLj10(`P!rLAj!+rlzLfycSxUN8gU@f|)}`;ZT59CK~JKI-a(>14Vh!j-16m z(kjW@2=J8&@Rfc#ZIHF=68KqUB=_6T8pLIBb}l`RMYWZ)?D|&pEr0V)F$ae(F@JOI9BC6v9-jQ+E%aYXbjg>r19Is?tk&x?OLmyg6T~JB&0F^5eC4byZ*&%tp8uL zXssICcQB^}^T=ef*TyO%LJ>Wr+PN?_-zZhH-lfbiu>JlHtEsNWdYQ%O^K)O3P`kSK z_nNNYP{tFWEdd=1s_nBJxBg@|%DUy#(fW#S#o}foTZn4@i#qh>el97SN~`g*|H!2D zUSO6be(JpNjryN~(?~_92m;A5HLNvQ&mKv?4w(l4!;LdoZbkQjr}3Uy;e+GM1YyIm zPtV1QIsOzftPIeiGSUv;d1+iwfEmk~?F@Mf0zh#0{#J7QEAQG>vpBZI`)2mOoQckv zmZuA4g#IBD_ni;y`~XY~4qe~r%=3%JI_4*}DGp$c&an!nXV;&geTffVV2O#NeBbro(UZXZ-7RnfyaVA=X!l3oXvxEB<4-HjY9^nl9yAUt)?$Pk z`w58t{9P!eA>U!Bp5vSB*Qx?{i+;sK1fb!lI8e0tLnXleK}cyb{4nzRQB)k2c-=kR zc>Hs=^S5udVOxRGuwnnYA#j?3<9Kw+%?12Fx)?~T`J>FRulO^|yq(aIGT(px8)*SM z)e*TBKPl^d-9o|~_>rHs=lOz_fdlXxnf8&D z%{33eyPq$>`YR((N!p2EzWVZCc_vE;gg!ZQ?@oL@y4?maoKvuRKj07gv0uJoo;%mU z{q;}JgzQ9$caDgDJ1L}R>WF`_Fdrm=KVU8B05J{D$8w`1R7hLO9@Y2yfy* zbS$3#@$M%8W$0m`qIcDU=nGodZi96?`I}$#^#=lgI7?Nz?;HKIlK`4{wM>Y>T@nFO zHzgmt`fVG%pYkGP313ryO$oo)<0f}^ZESzK?z?_Pkx-{$Fn{u@fTOVygDf2 z&cN>G>scM)syUw@{|(h=pCoh?I<~66ZwgK;qZLJw6CGDnM1x%>N{V~hllClZO!F5f z)UkxH>)qO0xjgd75?!;g_{VRK#6WLx#rFA~)b0u&@0HQ#JH<~w;jNf=NKCSwZOup7 zW>8za@B0+Fy5)$kwz=4+~HiKeYncgnJ8&kHrSDcdMTfOF= z38r21xC$J^)6XKMr$ErNLd$>4{zP%e$DTbWPn$(t5-lLR4R3#Wxnng z4n#rD^y$#G`k4p=b>C0+*o-xP=12Y+uw-DPc~ zxA-;H;E(sX?!UX~#`F7a;q95{M zbRy9;iBi_(@?rYv5rEO<4!?mrBMeE&p2b@C#&pC){P8AtYmIjHA!EFR{+N4p()~Ev zo*f8;=$D`@6aM1Fs5JlPJDzbHDf=m=0|sXWnDMbqdvuS!VGjxT^@f$FPHYq>*YL*7 zt*`(+Ifm-SLi=u!3-v;2*Zh@^Mxz@HR<7{^t$rt|?XIPcA)Qs-3{XMqf=mW$=sjf?x&7{7! z7(I0*VfNGBYpJL`pE$CGDr7hP%P5aSz~QpQKE(Z$r6@b9zNS@NRP{N4)HI(x<0vARyH_Nui)Qi+25GfipzK#x;gXWTq!_wHOmYydJM-)yv?r}kWC4|?JsHMm2lnA_S7N88lg`kd7HQdtT)K_#vonY zWek*4>W(Z+Ij%QKrm#slFViEYEDf0rd>qH(HN5qv(5#M*?FNGT-H)Av!W!|DK=-7c z44QEix}{4@rm~qHDlX=^Bv@-ilcBA-AOk<_+}vWeYRRE03!8GZ>2fg`U5%S(z_0pO z`Kn3{Cti+|MX72Pm$|BJP|OI`jzO!N%cEh#-jb+_nZlqF6RJ~s9sY90 z4QC$JFONbv_TlH$C@y|@M48@HdP$^PYE6o*FVCG-2G4_4Oth>pIAiw1+i0U~%4HrB zFXO_3A+ZXR!~u2`-#jl%Xr1VYDov_OU75~x(^;7>v!w_fh^-$vGpEDLg^C+X{<*(c zygI94GtWaeBk5S3aDP(|Ug3z;67V?81yoWF4rGoQd2KRt8f=|r49<}YrmM#1Dw=6` z#ub|J47$T+igU`Vj!t(2Z=*tlPe$ggj#naaF$V`Np3Il2y&KSpE093O5&7v+mK@O8T1eO!NHe zd3%*#lLM`?NK%c|V`m$%sa0ZEc@YGklA%i*=Vj1W^Yubp(qcx!uW`@y$vaaZ*R=|0 z~XSwnHF>t;&zk)=62t-5Osyhv^n$$UNB14c~Wlw-nTS(727D2zMOJE{P)Pbthk z4W%;EjkHVRG%h$D(Fn)&J$(x|VP+KAD>$W{LD6?b1TT0XygQ7Lfo&&MY#7`KbH&~3 zUgC%$S1K#PFTt-NA1}2;TWt^XThnekqY~Gf--t6`Bil53>n4=VQqdl39}foitnDu# zt>uNaFBaGdB(8YjzRi)$uBqh2a?;{dp5zpi z)-b&Qd_A$ymmm9{Tzj}a6R1`U3ogtK=pwX!4DjxjG2lHtMpq(QuY__ll#uGS{lrHY zZpvr?AN-+O`;egLsJUH#IKm48%l> zZnzu7M3rJwA36-f8+Xj+=ew`V^ND zRvY)UcMuZFst#k4NrDl{%{8v>w`zP9M(`BQ4%4^S0QL;cNX5wxdu1l^ck zy|h?Msa()PE)5s92Oou0Qss)WyG+A6m36ExKO+2M(E2FpNxlkeK_T8!DLS#W-jID41G~nfXW;4FZS2j~KoBK93Zd5rGM6^uTUxBB zarjS9OBO=h>WjvO$ldFlWY#Uk>eBdhAUg}eAZQD&( zr?swXztpabHp_q?n$7GlfIJ4Ksh6bhSWmuv?ncz+ysZt9?tRH*7(R48%C{tDE^1tX zxqqtg9H0A)Q3;%N#r5q(ut+CitPG={v8m_PXkwu5kD!VL-tyGBHMvvD?P}r#i2V&_ z!9Pi#c7>&~+skyXy>iN4|%7S~XGFG^@Y+7PnHyDW#O7OFkz+NFi@Q;V`oVjL>(0HliF^7ZwlG&z5)A zksw41%~^JPl-J!*f~isCy(0CMO(WgXtZ+3cXSE(b1s)S#ef+$6iCkt?O6XcMN6#Bp z!un}^Lh-&^y%*jZR0?{X-NZ8#*^Cs>yJ*4hnsOr$1)BH-pqB88v6V5`{9_8}c+OL( zG|4dv%%171+otLLnVL{wG<-qGYZWoX)7Dy)d+p^|Tp3Cy*o9mqr;@wEo>ZE$Va`{h z=Nm6u*6c6l-P@ARJG&U?Q7?Pkf6}&IbkwL`me#uaV$g#UwK0q#I;+x~atvjDoL+8K zk!TJxSewe%u9!bTJrP%=w2QGoCvL7UE~A2|XL26|_y~&2=60&K zwbs@XRp}CqTf8aMqUxI!KuOV`w56_2ze@LgCxhxo4_h!VnMn3m+??{Isx5ScE*XHj zpI{nK`2q13J&? z+TkZ_IqU_I^iLnWR(4ZQM6I9h*fd+2kU%i=y^N}C5`C3w6Yg}P9Nju#*Q?~@GZW(j z-f+JnS2J?@X?mwFuS_&5G;(WIZ4}rzjh>pZkD=izJTI@^4j6jZx4Yl|iDJj&L$^~+RFzYHnNC(Uf{hq@A!oT8+Gv;dm*cUcr_(m11s8S}7-pjH=@HZ34 zaYr3lhZvpS`GAhVtDysZbDETKFZuqr3bHAKzE;6O4nbEbvX37f@5elTsHV$XCPl0n zy-F^3B{odjhpV+!%ckhBti+cGn|D?JW}!Y&Xd`0~Blvq=RieX@+zyPe1NV`rlKw;t zM?Zm6{y~UkK~PKq^UN9te^j(sesB!33ll64^wsSMPlhR^gLaz{M|Ghmz$cW<*g1a*UM&osMtCwCB&a4^YeD=UU}z z5{**zW)%+>Xs$%bCY7~T>NdOOmJt+RJH|CwhsbF=y!C@QyYtJFol>(Eohj?EPz&ki z%5jRQh}=+ncGC0!ln@f(>)OSUB&Z(*8Dak zvcfux8~Mh3ueP^UyLz>C9ttZsJH_!kDA`VdI3JX5vv!U+1!L>9Xha@Yexz=U#~8)8 zy5M!4A7h0aE@2coaSXL~9yRhh;G^mUNe*SnkSQ#1A70CbQO}$Ab-JT*QN>|gi-S1N zDWMy#ZwB8U&}L$V2dTWI^$0>j^g@-=@7@gu%xF)CA4{nqS9UkTyw%1ozRm?ng*(dS z)QlSJJ5mw}-5aLGdrivCng0p@t!HXB457h7O)LMXRR<5E%-ay$>b1+%W^Na?P7%%d zOI9`J-D9#fK}nJ#M|EO6JyTV~kiLO<-?CQjx%0&Gfo#6++)3AppurNdVGUePa_sgm zl;Vbr#*E6vVc&^_ev$RQ{=O#@25y&On22pyCX2`#mx*V5ZE_DuBGnsTpdXaU`C3n+-Kk?7>=$R=2lUSqje{RN-kG4FmkiIk5mw3 z183VC3`O|*MC6S~Gw2MMN>mCh2Dzlv>UbnKndGNqS)YJUS1W8W9R1P})g$6@5Fp`ax_ zLv8a>&d+JF6WYR}2o4>pThvOKfq!Gc?$`$4!gb$_{)8v3IuvBDnDcK2%RP+NR1SoLytUN#FwVy45`a0)+Mua<)cl?NhSSclE;{5Xs)_?lo`fKPl0rR z&d&35?ZoiOciBJNHG`C;i+U}cYJ-z12`fff#vmJvMk%PEz;#k!XN5##epUec*#?Oz zuS5)y1J)_iVHg(kSOp%r2o$!re`CN>oCp-ezuE@^1G|Cki$>_7ebj_M_X72rmv|}n+s{TL`#fG$(z=iC1s0*!n|2zBfb5+b$bf#9<4mw%*Y@uQ>4a; zFZhZY8m4DWw9Wj`LOkdk)r@mQTGQtD`VXxqM{L&Ib22D?{tI(czf<~N)aiF{9#YLs zQw!q;hma-eu_C;A@!CQXlf$Aq?@=>x;n6z?Vc}pyxd8I1ZK&j8MY=|j5ub}9zGJa6k?j39{f-7)s+m|y; ztJq3C#v>TL;=p(U#viXiyK;^7-p z-V^)P?Ay-aXsF|@R}tNvjFU#I(XxU4HeE_hfrF%gXL@o9G*01_cVbf$%*iyO`uY3_ zbXzj)^@udhsvRS#xS?E}8rXi8h4p~Qm*Iot*kWfaDIu@iig&b=PCrky4B1Ojl!zk| zOA~xzRz-vo@T>E^iWzFX?=ufZP8619(}(b!k|~H8oD_()uyi@Fmei8^BeS@E8n1@8iNuyVeM2<%15I0BKuy{!5DK1f-dB+ zs7~#UK%B?fV$J6I_6Jo}RsGGEB`V)!aSadU?T!-@U=DOV?MAbV0y!IXlbAeY)-_kj zW8dVF#ZyD^$gDGM=SiCW+*U`wPghsy?o?aj`eErh{u%!VnvTcKTqUDN%es)Ac40?3m z$8GiL>aCtbbE6HLq!r#im;z>|slWzvSU}ngo7KGba`KOopnE40iC5xZXXIO0vepvlMY zZ=VuiN^s1~9=cp~17{3kK+(6p~fAcOtq-A2an% zbqztZd*fZ88AVKZtLRBLP^WIgc*m~B_e48ZJg`~fMpx!{R(GRp?zF8GH|l( zBEB?8!Asqr6x#T`PlIEff>vQY|hy7HT{=8`&RdeJ-|{9nD%D8Jz|!s zbbDY(DJxh0X~`#kWtH1yFE7p%o{q%KpM5`lC8rTjY}4Lcg=-C-Xa3Bk^A;cHsdd5{tI z#5g)mID5r>QdKNwZAnz~FNk5lz51Qbo?yt|H;@kbHHe0&xR4+0b(*v_l zER`9xCgl}`D!5Y=UfagWwuuk7u4fx)8?ETmmuaFF@tEBz=hixN&pmE!me&Lk^gbCA z%#CG%UXrQ}`5dDJukMgSicI5JaZjY8iQOoN@q8GP zc#WU1{%+#+6HYz`%v~}ervF9S0&3vxqjI8_6Vm%k;bS9PKK?G+8CrUG^P*QH`)$46 zk1cSF{{(qsJ&y(_y4-*Rd=;8Qs)W3G2h!VNs%|uybr3KJZ;zar{#p)()~>b zH-RrL!C5z0n@i_=jl_GZX103dd4@{pBh9&OqN*F6i6Zr!Qbj z5<}o#kLj*yg}l|Vo?&mY_|TIrZpB!oMzAmr^cK-ta3Fwf9VEmXd5`O?m=E1zJg>es zVMsQw!FC41oU63VYus!$`P?#=pya-}=+>Ght>g2{0z-VCgcWEiE259yS7H<>i-Vf0b*6ch>NsepJ}Js!AIJ6wyX| zt6yB+(5D*q!-v}z`8Pgmk!w&!l+InpljhIj3o5Jm6(un-t7&1LFC5|#33Fz-wtU@4 zspF_C;5>e=)b5*oPa-fVU(w-e;$qxO>)Z+N!9>m;W9+y{U{KqFT9o#1Oxp)%1d|~w zBLtI*NXB;yChLis9U*&J{JL-bS9Qcx>$+gEu<#p$9zttAesWbJmz`QpUGa_gqu!Yd z(WfXBRO0yOmDEI)k^&BhEXErvFb<+(O)6z)nYc%mee@|AcXEaSPmfK57Ypky(Hj}g z3}kErNbc3R9<6>q&>~OPMpaPrUB9*HiT4qdcj-1+G~|?>vg$U@jWbIZ7iO-s1_x=G zFv?LEsFzh$XO@u^l=(&kHKc6FXY{eAP})MTjZ>UIR9fnR+MLHj8N!PTP-pBQjbx7u zDWlTwkdgDZYky-^ki3wR(DCHr-D#f3u{uUNkrS+!s&<$J?>MM@tNsYT%^fs1vWOT8aj7b_r==j8f3S=C{#+4K~kJP_PnY0o`Q5Z+i>^(|u*nR6xBNehjL85a%^L2Y%P;Xxi zxFmGx%><}Nl+&WKT(j_Q4T%|!#_Lb!g${_y7*`?8sod0!bRQHxkm!Tz+JzYiubVfd zW!mSs23vU|Y!HkPqB+?FbZKYfH4X@N?B=v_N+h0We<_#}kY6SwGp5o^!;UM=W(@VN zjaMqBGABVApgqpq-Vdv6C@ABATft{kQ*Xo|hsmQo9t{;7ue#<8IEo70)r6}3RwvUA z;=BTx3$x|(|+eXFXj1~sQBk^C5C8oY{Q z16KnjbMay_NZ*;nT3OZ^^ZH9sRm}nZ3M#*>G&G%65of@W5VYb8ahPftu@?e^+P#I_Ui}6L^XzxgN=klObckTV;=5Gli?WMiuOb! zM^XQtH9?2Ev!*c=oQ)oZ+or^F6@(Yn`BsUotLm39u)@Lw%i|ARrQEU`>%?e!F`h>v z3Ty_e7s!cMpD?dAs32DRUSYz(d}T+3Y|7A` zm>igsR=x08flYN_0`tg&CUltohTqx#w=U_fitt{C)u)TexP>%#ElT%p6`jHYP%m<< zTCaJ1CJgjp*hXHRVRtRsGEceI6^rmYc`8D6Q~FnR=;)fLth8tc0lSHtyMC7=jbzzw zIM1H&dRHDWgh~k*BD6kJMT>-fqAQ{(6V@D;j2cQsiP#V>4#b$8G||yF(XE3RVqUY+ zYe98wNE8Y>L*rv8C7_bj?R670E71+&LPIpqV~)y=%x(`|8!jRDw+a-O(=rBq=Jl!` zMk5QAe0?59zUqw%nUA1G?8}QIam{DhMaqPo_#^Xuph}$K_B0Dk4=Gr{C+Tms6*q1jn0|=w;AB zlg+QberxItH9G4FF}^k;ILD!MsaIpmrbJN>2VRx#hW>(}K5MC2M4BvA_Q|t>IvP$G zR2@J2#NH~TRd%cgh4T>FCFWrpIMX+mwL>^KQZVl+yV#0W(;wpj6(!R=V|HFj9|rgy z@r1h1T8;6O;ihktl|0_V+1{2Ev?Ie$^kJeVeDAyoSzEoU<76;S*s&x^q>b-Sm5f_S zkxJ3)aw3Z~9#BcCo{k>RltxX+(cZXq;Dg8pdmfA3>hOi?hcllY;qn`+-8I!oeMKt6 zxaI@Ssb-xvn2({f7&%AxnqRvc3+VCHjtwPxNj56xu(A5yYlDWl&zoBPUX!PL!dAPy zDUqVPcR_Ba^@{%R>h0R}YJyv;n<&iLnYgYSOqo)eG4H-;7XjMmJxA*wJ4GNQ$1+7p zk+S_rTta)VpJYWzKz#@CeaJ6;yI;^(;!2+h4vy2EI=0WZMd8uR*#q96%SRp<&Rxwo zP4sUMfKPa-^;?mhOy?-s)~oerLuPe;zR@Qw)*_i3HNP5*vFy~kT}xpPcvp00cmxyx z)ox~Cw0DVyoFqF8R_l6FUW98>KgGxkXv>xnyd4sq*a4-5o}rh8ym%wn`42 z>ZTSYv0mhr5ff45c!fyb8La)IzIT~!BD>#sc~(tv?+}gH@m}fqlZ5O={GHyn@A%21 zgz|nQ%&FTD*6MBK=x<58h|i9=7FmfcA}3(l$9;wFg3UH|oLY%AL%H zIa7L)X`?pny%?GbYt?+&`I*Av4n#0KY;C=1 zy-pk|>_tYc-uJUr37r~E21-6~mYbYR^W{wrOiFEPo%g&~ZHQ_D(;#Pe4x#-Bf`nmt zveolz9r%@81u}kZ-DpRWPK&!u%Yypg0P|g@tjeRLTMsRFswGr`0BbSTIbH$?5N3c7 zU5L9U(s`u;okU5U@vM|aY3VC5O|E{x=Z1|w77On1eKa&=*zXbu8S7{wC`$yDINRuB z&+=?*9*hTFFHw>H`kTHP=Dw<5-w;B8e_z?PYaAECL&I4k561S(riJ6f7q5x<7;4KlPSYuFmg{AnnlCOq zICLi8NGUOLR5j$m8lQpgw4(lsru-bvbk0YFy zC2sbVy1g!cp|pcD`ln#GPX_CuCtfAR+g%RSvaT6S?dZN0o9a5OycWs5w7;!-N-E0= zuAr~8KG|Mkj`Q~(cbrsuRPOM;#(XKOwe*ap-I1NC`fw*w?7Xc({CSV^GV1$Ilj+tc z{YBFW?I-)4)~2skn+MdBUimbgvWZF294O(zPSt438AauYs)Z-I8@j&=_3FvDKqg z%fjkDP&$|8AXgEn&?%T!*|Q$sYC|lb=!pQUr!1B7y;YVK2gb!zV~NGze`p8Iy{ShK ziJpubQ>H6er96B?@m@N2&SJ!zU zZa=Yb!@cW!H}RqKpzcTi%MU!Jpf}u1Y3gf7b={ycF-@Wxs&qdcfh9PbjCl^r{nnsZ*aQ!tj{%&`TRaVze8Hv zH(ZCjt-jUCj(r81xDTQo^&zA{6#Er~^$!dS!H^znqzLG}_FXKg9B0wK@)oqd)jI5n zSGhHFPk8RyEu+~uS*hxIP`=U^b6iNkg4HK8SM90S3~uN~Q%ka0S6V<(~W*zO!?XY9Gd*S&Fx zPUW7?Mz?UhDvNbs1glefOo9lnZJE4jqw{?z08C7Qi7|PY%l98r0Wau_r>cStzAf0uh7$C zQw>&qk0T9Y=*!iMeJ)5krw&b!VA)D6^_d$PQ&POZ_!EQNAip#4?2j&!EgsUa69NJx zF0=0h7_PvcW*_iMF3$NjS>QSd8h+3sGNpbL!RNgaH4ILjU<^c<`M4rzOk<{IlSy7& zT=d*|?!{9(0l((80cC3H=Od4UP`>NbmyWyMfcC7< z%%y@rFY>T6wdqem1R^#ANL|L+|BXBv9}a+pczF^b>62yw%0*f;}-V}#f*3FchUdvZ??ct_yq`5 zYG_f=7FRXx1Vcf=nHfd&i9j$s8{{<3ZF&+~@qca#@QMYq632UPJ4r$>W#eiA9~xka zg<-Q^A79=Rw?;`oCgtxI$YTenSNIjrfQGtR$4|Xm4`>~=KBCw-OtWwYqmF;Stv(od zC=flJLxf(DK8TI5CMd9rp)vn}p6(s0Hg7Pq&=WVOKKL(-qOUNNzH!?J_0Me9YxN1IX)4{ ztRam>ZYA6P=JOswFs~6O-?zFo5hKSLjeiy{p5EkYFZQSX`zu?0L%sA3@YN1}jb>;V z2AjgZ&bWJ4_e#vRYfz)*-_W*p970xneivd3Svj?EBnHf#RrVDRe3zpCN!U0+VDBO> zS=w%?mPnQab}>+@Y9*Mv>skNZUeH4KCBS!YlG138YqfJ_#-fYP{=k@15K>CjlCt!?plBl9|5LUXjjY+omgKbwP*XaV3Z z+~HAp3?_`h?F+=Ow=ZZIfVn1vGp~VE{bzI^^>xCytgZ7-@Jdwhq=hRa6uU&FC--~$ z`>#<1R8rpqTk*>W9i--KTWe*&_|nEeonFy`SAdHNII-f^8;63uCxPOty#jJ;n!HLh z;XDNvWVzodCbcaejEg^q2C#AGM1I}u==>v?IT1YbeUmR0!yBZ+L0}Q0RQ8%(hRg^X z@S1Kr0t>SXxTz>_o>6Cvd%m95imf&F~1| z!SlZXmz2%N6x?U<1lW%Y2jVVqnu&v>TI^42IVA8-^BH;_GYw7FWM$;iZ@mkjl%4dH zQ0q%(0|<+h{1VJ;-RzCUS4CY)JnwO8_A%6K-vmY;&vBU&I0W)ufPBso-eb+DH0+X~ zF}!mF%DQ%-YE9hYg1^{OkPC*!?l)UES75oD4V%mS{LX!go1relz~q-RKrgn_)R6z| z;OfE6kFZd22X(N$sf9p{|0b{zkoO*7{xM%rf=FH$ zcq-UIsICVpXscf3*LIS#~swyx=^_BtPCwj>t3u0jK>Z zFc&vodi@B93&CA2lJ&i&Y==rgS0yIx0hk?jjjT_$u6I=M|0kS@kimMFbG}{kw z=p1^-6exZ(tOszY*u|XMUW3wjML6Z^?uIT9114$=@z4{ama4~!Uq-SY>bVI%Nv&a7 zAl5p7ZdL`CN=D>=flddYw=|gKfV&+)WEtC{>jX^9*>^{R2MN!@Nx~7c!u=*Ix)+$Qylr3;mM3oDP6!hq%sDU5T;BmC0(4EMMQz{C z!VNka2A*KM^jS2)U-$otFM7HSdz|fNy={Ml!VTv7%Af&k<7Vz#&y{{{)LCEq=FrtW7GJOKN#*>^8_ZDm1->HL3(Rw;EiNq?oO>%4JXqvNE~Dvn zOF~J2yc^iclQ#eN)j+_xzI;Uq$cjJ62KfCf3RG<-z^AF(#i9qm^ntg$Fz9*P+pYfArGWt4GV`_XK(w;Yt*!{V1nGwW z0DCA6`A*!@&EF@qe*nqK`Omvx;WSaB+FG@=b zV!@zqqAQL*w%0i=w*L>0T1(q2Yxa@*Go<>;gMd%hh1O&I{(ars8d0?f-o3PE-XF}i zrU(BUCOg7TccEvM?L~6}GAW&1P5xTpI=Zjh?}2+e%+HV?|Hq>?bHW;`ki-Y=9{(>_ z-_FJ5XHT7z;mJmV15!|6WYpt^{}Y72dEl{r=IE)@`$Uj%>SNGH0Qgb3`PmrHGb2R`RL>3+zu_~D1=%WFf!5jvWJ89%Wvd2s>Jx^uZ3q_=!T=XrHzfUutN;A~w(Rci zz5|Kn!DYDb3#~gwm%!Z-XMS;z0$YN@!9Q;K;OKchPYPrL4f;Oh$c7sWUocna0-6Yn zm;ZSa!QVk0ZDVKiRy0K@-1y<$wO^pbLQyI0KQi#YsfazyLWjdeeCRI%Hm zzuV&b8li{Gi~;i(ZBfAxVtyevm48Ib-P#us2p!HX-gBFF!$sW^WVKOCeZ?-5Ss}I7fIKs&Wd6)7|D9Ae!9Og|jf(hplz0LzZw6!nwr@6) zF~FG);OIEP_fH``xQ}I^(wE+LR}873lv3mmZiZ<@@h-7C1Nr)Eru=U(V^_~Y3|gHW z8>gH?)*DyM10V(?!j0P+qQDeYj?s`B)alm5EfHa7uc0au~fg)1v#rheXWhyXt={7~jCenj8g||0N+c@M}I4%HMec z_nfk^G`!h0G-N}1CYwHK8cv~Ae-a6WgPN7^?}5jETCe36zkvF%BYov>j{BVq(mYtC zZQwZ=w(uzIP9Z$-c#HV;(z6Us*ehNY$h-Y7O8*Z5+xIijDFXMkPyY*F?iG}r8OU$< z{4sYzD5({Ie7UR9^5Opkz}CPkyV(B^?*B9FV*OVY0>Q;56RV6V11L{F=a3N5>;{4m zdBv?;NX%TWm+C?|(dT9zV;uiMb76O~>*-xl3TCd)fb24{#+%kBrynPn zEOFh=6~sd^?4JUsKIQbwHT(AtLzh@_LE+UL)U)kSa^%QAn7vvvY(AK$5Ko0law5$~ z5KIgZnVX0^pYZwMqEz7hV;$#*B9}_B#g<+_W?Yas13fck&N^NMENn59o$Q7Wwt~YrHlkQ0PJC}=yK$NSvU3io;9&n?n;wUv>pM>Q z_kV1Q?Ce->38oK}KLn*P_2~oI{IR;=`y{q$1@TP^!5H`Rb?YCCRVY{K=?kw3hrPU+2j=@ArLQ_qFcVb=|Mq zX8hw;e9!XClR^s{;8fJ<5d)x`W%b!zr2z7)h}L!)*Ii|Dod6&hIZU1 z3oEy#JC`QA1YX-SKgd*T)i%6M_V>kjj7Ffd+maM!xdLJ)<|VB$;P?syUjlwT0MS*o zsqyTc*c)7WJtr(ymY3%TAN?;|IEPD73L3akktzy+my?+=95~vmwurK@F(I?}dC>0P zXZ8ij_pJX#)M$=JVKPHC+v@n^pW|9!I3K`pjxhu>rvQ3YT5vRFZIIVUzv1r+FZ$UA zL6;o0xj8WqP_ey~IV99Z0fbJV`T3Is)%4Uq%ggrTE_&mb|0y9#P#Nzj*OQ<0K`k30 z90444A6F18c=T5H(9qB{3;b^-C@6thB$IrCkykUqWBQ3ABQ{-oJX03Rt=^YF6s3QQ zO``OHI0q_)f60ds#Q-abxRCDqKg5E48x7!Woc!B+_oCJRN1TnyC!&PioDJVIsWGq= z{!N3>;y@BT$zM9L%r@cA(10TgR9wgcq1y7^#Qy!E`z%zK;B4pLEnSgzProy9ap^mw zQu6q3Rl;sw=x2M(auR1&M}rmkGh@Nw8TK1v$saoZl6e4#9goR`JBlBRP5BY>x^J%5 z*iqKTx)aG@k^x3A>0BrL!o9U#kF=3UlM6=`1$vfjb=2+Q9|H8#5|WLzxn>$i`5 zjb*=LvCKE>M}7@uKUwZFT6I&`PW*gb0elExi+pf>y+A0M9PA7{7mwtX*ajLziPXdRauO+S|0{sJ=2-d97{Xv5yRO4q?19l7u zB;kj<%8{uAn{%qV80kDIh=%H_38JiyTTOYbt8Bcf#2g}q+_fxO2upD0G=YAKK6ZlZ zIHz?)cYm!spiAFzo4Wa1(Laj@)>CPe!SX}CEo$J5Q4p4_Fb1|(|JA{qvM*xXzJ0&K zdKF@8-0LDivm&I_6pGgY(`e}v-!S|G-jSKra*_#FX?(u8wDn{MFPL|uD!eddMlB=$ znubdL-Y^rPOg=q)9x|FND_G$vZ9&dABSFfS4}oIhzNG zf|N$a1aWfDdtcWC!+KtM$QHZhzl_gy+)^i8<=vXx$PRys|8H3ppftx4k(+urM3_OO z=79DbzA_RZ65|+USZ|gGwWnc))_bBqw?L?k1N;A1k03OK5~LKJ5>wv+{_F;b2etif zEr^TQFJp(wqWrnDqG>|d%yIOUM8QS3#ZT#V7hOyz|2a-ImFo*NSF1;&k5jp3A^T=Q zW|9tdtdjJ7a;^VnyAZRw{tTZyLzX4&*Q{ls4S$F-)}v&Z!-O(p=GRM>G(3L2kx-o- zA-r*YWx!#mlLyd_r)L7%@yQOSa&Ss2{U}rvOYg5(@5nUY+Fq#~TA#M?D76C&u^)wb zW__f(Cy{ww_g+2QpMf&ost+oqh!pDR0TsFrJOr<|h1)vDzkX*_i%d=0e4FZz9m}5e z0!dD^B{@V+i?nq-r%Mt9h5R7pl0e|D0e^ZBJZzT^eB0i_f_=9rxvR!UA=s6=Kn~IY zbQVseMOztXN==jg1Dz<9+TXTxK%k)B=w{ry40tFs5ojpwdV@b&&N6F6k{E5$l_3X6 zz!kk}e$4=4VkVZgf>x!Q00A=1@|H#0!qmcym{u%3_Wn1S>3%q>X)ol=)c?vfAA>~p7wo4&WH$(Sy5K>_p7(;3A z+)}1Txk)x8tF!TFaOS4BXXdM)MD*;7Xl9LlFx(OfBVM|>So+(vR8e=2qGp8+uB}Sj zx#1d}wA82Wdqx61SQS>PRX*1yyqCcM#Y)SaXd(}=->qNon%-3#@JvpIyV#6~;NE9Q zt5@5ECx)Oizvdsd+JAD^VomG1G(0J_kXWU}7Fw}n*xFhSp7OZMh^HOvubwVX;A3ya z-g^dVWvBAz{$d+;uAvw{p%jNTBNXH!BUC=_3+jOV?+Y477!+QHk-Iu`9zyknhv;Ia z^Md_Y5TLK0i(S1Ag?H!E(rLWg4Uuaj7FOH|!?VuR8$!wPA=UgPKe}Q`hTS z*QY-f^FMykF;?NzjWDUu%HJ^DZ*P2upP7XaC1I~Hm1<$Fk zL-|k60dYKn&H-<@l7viipRS7uNq<9^ga!Yy3}Dd$Z{&2fGgs=lIX_Vz^x%-#fxb*q zXZ3aXeC?MQ&;BRFj+GX0T;nn2DhldSO69HgG`n|}hGx2sF>5FYuZ?y>0~nknNdMfX zAX1L3j$RHC3kiya2p;E`hgXI?JSKs6)-nn1i(UfmD+MPqH8IMVSXuj#Du0K=?RrMH zgb-kUu`snrdlW_2GI%Ck16zQGIn-V8ADWTM*h10QXH@a{dVv*aULjVg{jQ#c3r6Rs z7&CW$KA5MN9x>8mh>EtFZpdWA6R@+ECRUP<=Fdl_zN6zylBK_sAlR&M2+C>{aUz*n zrd>hHGCNm2V^Twz@Jf*K`V~fSHA3Lg$d%%q${r%hzGnt&AG@CcsAJFR|N4-X0|Qu! zNRQoArqL=&wP5ai0TD1%Rm<>6I{y-27; zkzx#@z%@;^wtEFI%O(4_(m^Wo$|BRLKK|)&IR1Ckq7jc!J+qeT^n45nb=Dx%Px3W> z(gQ48jFB_HnXa@Y)bybnbRW941NajIh9&7-TKS6rd(L1vlYpDJTNa_oItHSLb0S~FIw}%1 z&u1t3EVZe=mt4nb4B2(6xS|6rB6(!*umOm$?A-A>y|Ug~TJzphGD&fu-nLbh;q zijB?$3aHfuuMPAQp-eME<{fQdODZ<`A<9~6hrfNXDP9w*wa@`rwsb%a#FGwZ#hH(B z0BL)woMBECzqPS$^wbCK^TjRTHlhEp9~@Qr&lMXp0zFGLd5Qn1>409P2Ielylf6(d zHsS4;iccm{4_a(N#wvx$72m;Ql3I40t0A{wc-3wy^<1hI)F%uJu$<3@L+%@Js{#SJ zC(;8il{^Gq#hL363t{*pP}f5keK_^+eWjPuSJ9BJl)lygTDSOGncQUEvdt9kg-(lX zOy$xR-ob>KB@B!twlB1ywrw9?s7==s1{Z`BAw6?!K%qYRGSS#5)OXcJ_h!?stvOik zrnG$c$A=|$9U!-Za;`GnW;dWE-gTw zW%+cI=+&{XOZGHEO7U5w6OToWl&jeYY`@Muxm`znn+*0e?9>!Zk#~Am(=Go5Qa1Gl zHe$wtt{~QO`!9wUs`{MpsjqChG+X2tT04Y`IM|QxxMpxgM7T5yc?k2I7j&q!flf1E z<&8dz8*hn0oEjjNpExvXxpNdpW!<8Gfs6)6sXy|lv~L~HI{%Y15&;*}ng3*9-L5GuKq*cUhTpOwFq!@@V~AsX9!W ziBydGF=uP}ilYLh<34|@5OJCxeYtpg8>MubmN(nZ)`eeKNaj+=eM1|J!&A<9t_!QW zk=$eYxrtnH*#U#Dyk8kMVjPRyAkR_f#@+6bbOx<=^b<(eGf?09)k72U+3B~y-4Deb|ppwoS6 z%tBX(s(A9}Cqwn>*caO%FbmV2%9kpRMtTy3(8hD^Y`WaC8F1vO<+eo4LlZfpj5(gDcp`V@Zx6X7^G zsyhZ}L=w%5YlQv@Jx)mt0D!~D!#5n-WMh`%9OZGyX$Xie`IR-wdeTGp4d`aafVN5T zY!M=FDUW`wHhE7Dn*LW+)F!7wT}f`dpg6_&TvJy^uRHD`GJpN84j-J@eonZH>dmXN z+X7Z;MYk2{P!~4zrih&D$LlJJj&X1{fQ)8Ro`?-1fgiDSV?OqPCQ69_Bj!fb(o!@CK3+!YNpaEp@nei=c&Q1U z7wP@zGjF`zHkO6Rk{{i5r`^!A4=KB?{`M~Q<>P|Bh?lN|S5dUK#Ft*$Nu%@G>`$0CR%SD&eG6#XgP(HGRTR}MnH1b!3+JYP z!t{i($y3~V2=QtPDX)*HSsd2-C#3O91el1)QKiST?2PCYuY8xC1vbhJH)n>}c`D+} z|MYRV{_oJKl>GMjRaRbj1Y&Rl>hwqPNidF5aZhHNho5HCd0nJ&uJj&d@XZZgEY;DU zZ2s~}Q@CGOwuV}+(V*2-v*wUYQg+=MUm&qr4L3$ZFF9erZn9lIqix{>ZkoTWf-N0> zHFt6Nc5=x$*bvMW1)X-p?iur8M$*m9CM$x==4?&GJS*MW0)H4C;n{NtF6H@*_6Wzk z{^2@f*3zm88u+Ew{AS>0?qmj9E>Iavc`+8ggHbmgq6Qw%5Sn z&|MBF!?{zM^Hh-RO+dKS<>T_Yd@=oXxS?EZ)4?061QFFmz7L``6FY3?`YnBgM;G)| zBg4(;1@(Gj!|x<;RJXp<-jI-oGcG+cA9dz-qx4n8bdX9rIW&Z_%1q>P4E8}|j(v*W z+E@vL$h=n@`+MI#S|4_%rqnRC>;I#pc+S>hv>bxu>f7viv!Zkymkt;W(Prh1)W_1c z+&5`K&h1uEf{mb*V|j@~ zVJXL1cd~fO2IXuUn2l^@7z5{}oVD`|hQ)MDp3;iO2Eg#~@tG&O&derW?sY?K-8!*d zLxw5`eYe;N`-rYPSWga=IyfZs)PHULLeJy=nz{#^EZXzyqB=P8F@nXkTj}2uP9gD= zY!6vGTEbvVPjhFaWH?6ZHaA^aae@p-z0sH}Y?*DrhgXt=1pTBuRp!Yy?l+xiJA1W$ zJZ*(9qc1@wjA^!PLEi0hn64Jbh_oP$C28(EvDnKWvtJaTL4*x$6WOek-T6_8basa4 zxd^D1Ye-dg>qs3AS=IYy?OZ*X*{a8Pk3B&oxmNfBG`vMoYPSMyIaKj9j7eh_=p+mG z-RT=P?{;mQgn^Lfsl`x*o6t#9iHn*i92PpV#D}lh4*B55k3N=eTqMek&Wps0-{{?O6K`x(>i6aJC0!xcS>f-#ljcI3 z3AozhdI^|n_;m$2IHhsi&llEON;*nd#!q!l-4~1I2$uF#81Aj1J+ACqK=c-`4wPy# zO>HnI?@-@lN?Ch)YwAfN7Rp7J9MvUzwwC-zNurfaY&7!lkP3xBID8E17bHWYdKhv=!dqnE1~Yl$@)g-P_Bx7WrO zzO37v9Y@QRnX4vh7x|HYId4`=YeA&S=eBNJUj9u7=dQ25^}Xhw2^Zobel`1*uf6T( za+9msIkrpX&Us-S0lFP8o~o=`Hd5d|fvZU`dweUb?qI*obv8xoCA98{CEGu}PQtubMpGg=#LGix>FmH95^ zw16%9F!IZKX6X_@Mbh{RizpO4ulgd8s$YC-BW`!96s;$oRrCpzNWCi2eune0e=nKa z%X>=mWHL*h3M~1-FKBRyFNM~c%?_3?X*o&7tiQa>uI)NkgLy?OVJu!_DYfb)N1@=+ zZDjqm(t%sMF!U(pUf^0(r#`C4zwM>DezVU?sPCeKj2(6z;PFRWD^U&f{8!?YU{c`T zzI|U@LZYr^o;NL}KeLvfj=lkNBP3LuIAUD+&Oht6Cne3dKtmj#g5~KxHGD17KGu)h zL|=Sy#er9L^EyR&KR46S-97pU<(iwP5RK_8Lm3}_Jp-hs+9|iu`6_o|=fy|m_(X?% z=Bia)DQERGZ=<@&8z#ky%g%&-uHP8u^Rl4{A!fVN70Rmd{kHD81hB#DFv#(l>|!Ax zd8X&ljw~GJZrl*u1J%|qDH@O=+h_VGGb`QGsHRhQd#3#3(2xtXk9=lor+JfU#1XLs z1lxEuVU++4jih>%!vnhno!r!uI7KC_ z`MQag${Q{Q{1DM4ZwgOu>vR~2m3cdr%y4xh^;#6JMs=K(U#zTY(@XA9%Ib~U7AgO@ zqTA~9@41O&eX?&K8k-+$sG-TvmY}{;aHY^=XS;V^)X5-%m;L*P>vu9Z+H*lp8|iWi z?dgDXkLD0bg_XJH>+Z0^B6+L+y{68yA?PZv(>n4xnwf|5W5RU(BKzeB#T4{5V| zG*I>_`u+R&7;pmMRAP2*C5q^#QQ=r1CWe$i%UG>t7pDHX(aR(DPH=0TV(d_@TX4@! zdWhE?cNu;Rd8d>{Ymwn50a5amkMpk-0uJ5wR5)KqQR}_MaZ4ii+6xewAWT}WuUooK7_L|(@l(ttq!vAfY43s~NZS$IFe`6Qr!m~t& zIg|ZAJiF5O0#{n>ESR^$sp#S}xN(6SN@%5Wt?mdII><`WbP!RgGNOnKJGx(nG3DX_JMq4F<_|a>3 zBs`VhzwIG&^KOwU<@fDM=yuOtpW5paUc7}3!;ADQ9s=#rqZ1 za8u$VRA}OLFC$ZmhL7!?x&`}JUKmJAeRKwAzqLE}k4&o6S^;*h?vM&m5#z2^a0aoD z(%w`Shenb3A78V+g7k>`xUKhX;E(9+HN2;;tS{9)A*ul33@wV;C=j|abZ96>(>3eu0-lI)E z!NtBqKF$v)ooqd2gWbD2E7%rsR<%OqLy5G(@WXU4+JR$?zEzK|+epqimYGMTB+RYS zm7T5&ze{f*<1@Y#Uc1!&h%y*%MDWvuzv0EVNO3P2rzkfM<6EukZhFa&QS5@mZ8Y#= zlu>bO?Ex8P!5A&KjUz0j*KhRXSYqIrFFH%efm0haefQB@QJRG)4|J1b-rVwvWU~mq zp=$MdQk+MoeR4V+`>a=_PPB?J)Aqe1Q815wFiRHd=$ZkpAvFM342O0QA$)&KQf~r`xmzS4JGQS zJUploK{l+d-QUKnta=VsMGE$#iQ8xMC>wexgk6gU+i+7T_^jiv$6>a&HkjwUd+u6$ z)00P`P&!?$JZr9d(fKf}>WpL?f=$qM^@nbRcG`25mq^A`f?Yj5cK8y~s0dTLU#1X^ z^?(g{Dl4(G)s~lSFNrsSz)T(6D$wAQ&In_g^S*ayKGaaf2Q6*YSK;PD?{-Lad1gSP zf0}#W8=pM^&LRlkWbo53iMh#Sk-+{j2@`S`J5yZQr;h_MT$=iNzgnJKTo6TdWFolC z_ZF1aXTEsp(Pxuhs6dlc%|5y4|5Ox5g~oQihFnL5jGWx0VXMEyF12%sRii)!B5T0R z43sj1jFRKSwNrX+5_=&J0d5?d84y@)S5|O~s34tvr1gDBLMg&{9ag>sQP^swtE%xM z9e(P4iKwRv=zk=dMC&L7q*K;jsLPaC>#&sZ8a}lbl|UrDDg~Qx@9p&1UO-tS+J{QYcEn<`ThMS5hccg8L3z(at6#R5~}=khJhCa37-yoJp9LTxqB2u85O;n)-!w zWe5Xj;Gk8vNwa>4OqtpOkR@zHzVEU2@bK8YO z&qk!+5&K23fk`8HTkh_EBl@SwKE#c`N2(*Y()!MbE80LS?W8)d{DAO;9%WrSc8#~;`*hFdee zaLkxWex{6(X5@JXIwMEX>eA@aqQ$oiV*N9hT1XJMNfbiPdHTHc=Y6gbUEy;6>Uk>YFq~^1$;C>N-WMw~j{`#-y-%k;n{>j&x;crLw*N5gO&^utaewEWC-% z;#2KlxeIf%@P?_GTHYD6QC5tEM`!7%LX=J#d#?+?sxj&an>X?|po`N7m$xZP_*tVrOdb(u%+*6SS~GhzNJl|h2E-sqXWaH(Fi(1nKj5Ct{Cx6 zg|+rw1E>GK*>kv&^L7@sxxhxQz}^zy!*fAn9wlSSG5RMQ=nfq^S0p8NLj^MYS}i!YsctdKsxpwIbqCjyw2G1F^CN)JS*?|PIUz4}MmG{U#xAl}uMjGG zujG_m64tw@s0BG%A*95;|7WF;Tyd5_ev z_L}so2@XyziT9awRT@n9NbKSevM5m;rJ2FP;=wpyP8Lrp`f6je@llXe=|*_HY?LEkJx zn)`-_b=~jb`w8xBE-bAyX*V`CbxiuYLpQpbd^fR|x6)}xZS*p3zG@g-C^YQ}(g}4R zlyphBa3^p#7KA$j9vn=kZGf@KiMLZFVDr#v%RWVJ^trUPJZ+UoRiVJvW6E~@j(R== zGQ|_^jfb1;-+lC8*>Z%5P$#O*lu1nqJqot+HbQ45ShIq@^AXzBurOz*KUXu(+}gcj zH2mga&J@`%w=J%Z;`)#tVd4I}p63z;5r-ngTA%!qJ0y%6>Bfn%;_5MpQ$SU>E6!^AMC%(8 zFov3>#c8h{(-HfY*u4U_dlp%>%SjTv`hnB=voGS}r~`!!horX%WzRkHdvh8ZnJA)z z%AuTMrKPp8+K+@+>=w;Wr0*zhPj&9tJ!XsBj$>~XZjPwc zxAV>QAl3J6*bqymzsclOYB#Cse$R#Nqkqm<(W=+>c3h-dU46La;85rrcXJ%w^K}{` z-A~Cs4FWRfN$#2{t}WBd!9emjHcNQBK(YsRNkU3)F=4Mg9#5OybUlha5( z8bN>jx%~_w_h#>#m}>~n;1U{?Gt3AP(U95|W$ZI_C)vhy3^4-N$M*hLh9~4T=j6}d zB2PEaI(PZO;>x#AaZBzJyko5iGH#Pf=Ik0`E`aRnyM_zXMdrgp8nxf8CjfT>j`)4* zZMov`uz4cca}y>BHg2SjZv*U7p?>=PVVK~kCNXRIM=vZa9B)-goLhBso;+=e(B0j! z4d87ilnzTPyhQ^GQ1lsd(`{(jBsJt={1`+0kDqQV{YRrHjxlilJKN=v#wov1MLB=^ zEuM!H30@`BtFe8P1s<5wS^z_an|~{B!M^pZ8Ui;#^97ZOSx=6B%Kh|OmkWp#GD5|O z?+q3|_D6;DoFOn#u`=?0fCS{LZ8$kOZ6@$HW)|(i4bm^R_Q2Q#G-hgQDmzqyGWi8y#)y64eV6-N$sc8MsA-9tWJ4xDa=kqO(21Tg;+p2xi|-OJuRvB zESDKf*EBM28E2XqQcy@C7Wp>jpB_J5AsfB0^^h(zP#*X;_$ZF5Qb1_yR=Ps#!tnRg z`QjPD{9^EMkWE&#cO5(-%#9dsWXc`^hcZepf~^K^Q-+dDyjG=TK>Ea1=OVp``Ki(y zQG}*g1<8k5jkq*J&_$9e8a)XEDT%(zpvTTuZ$(W_jk72{>?~;e&?-0OC<=0@O=GG_ zKP$*MW8oTogd@9>S5HQ-IA=Dlg7kg{`y|PDg3h_FTGfJ==9qkA3y_q)HV%olM65;$ zfpQ2aWilyaj@`S-R5i$OI8V+yJDaj{bR$&^n!i^f>3x~6orDp+`*0XJ zd`;vTsm>bFLq#Ir1*}IuMDpu>4Z$5;)C1SMid0?Q zI{TFWI78O?V}iUzWchpV4%b9P)Kf;jI?&_w(|8EanjQWU@~J|2q_woHQDMe{Wr0M) z$_WMh#duYKP~DnS!{M;aiXNK~M)R5lzm0}6b22iYitDeZ6J*_@F?q(BIuF9-x2$$| zGuUX&1%ifbTgP?PU)J$cArY^L2<6X3$Z!x*G9KgCjX>3oxy672WatRXb50ucohYY- z9;upu>&kWKEIKSBy@yCW8vOr#T|aTr)Dq|DKU3vA$3IJ&k@JGn-uAt7U7H(61c@wW7ADXhPX;tJR($f4ZaV zEYwjs4{n~}BI*W>pOF!=G!KIcp}-Nwit@`1t)61$XpUFfbx0Y)7g?azO^Bu|Md_1egHvOAd2KV?=;6-x)>4mJ1(-Cq%v`k(@Kga!jjg=FQl5%$ zXK-6aS>JO4U~MYEtwvFft#6egw_LNrwYb8iWxC_S@*W2qr1%+N_t456LGp)j*s@bc zwU$S4mXLIpI?U=ZR8KM1jJaH7R_vbp`r0A`FR1zr?qjj=_#!U(sep3|G>)xBNTW&M z_+BKXZ@A?y7XUmdu|m6`Fj3dyz~jG319>yEJ==#ee*JC#jpW{0;*D38`Z0|$K@D|l zpi4zN;eNXCbxvuvV8g47Yg)483?S2W8x#I+EE*(VQf{Q zHjpc}8WA4@#SUaRlI}gVeifCKd6aU2YuhQ?Y@3#8yHDJ=y&zPJaE^Pg)%g+453yi3 z`#%^cb;}XN121Lgt2lRMvq3)EDX>xR+^oUA65FR?Ark#FRRt4(M;>LF<7`qJ>i$nL ztzQP~!gK%30UZXr_Ol?E`hU6I21s3g2^1>|#3wRfsne5j4!`{c+~IK;`Oj;UJ!IjR z?!%uP^;EvRI(!~WjJYq)G!0Qv{Q+C~Ux3U0PqI`+dG*u1et&@|H9l@K0FZxs>+Vmk zQv<^~ESU70=+V{S`?8z#*G2zx3hwy0NsGU@C3l_1)fo_OM%<4s{pV?I(3JX~@zz)u zU}8G(dVe1P0;%>cE!9zZnEu>iCZJzj;Nf2?oi@}<3a0$6nG`WWNGKoia?&*9z%xKU zrWFD=nWZol7(s>6-cv-A9%B;aUWe9M!}<60VAqQrApz1qoBunV0>}>yTB#mX8uPpa z%2@JKGecoi9-CHu?joQRvcKWo&rC!U=0)%xnBaMbC)hLtf8^T5f=KwCZ$UN@ExS%T zrCa*OZD(r*33ljdT0~f-SA~ui8zofF>!bWTVr@hBp9+0A#&lIg#Dki2xs(*QU{S!Wpc zYm10|1lO7H=;(otMRaepRdZ7#qlsME9`e1bos4I~@2RVMvtwopF6=->1k6}NSW|rK zsd@kbh-*_D$on+s00(6MeG+M$e;)PXXQs0^!WNa!U#h(P8L?> z!Ai5-Q#Fu5`6M-jDE|V-peZG)eFiA0jDpZoyY+ox7YUN1UqUWl`Q6;nw)pKZCc}?W z_v9zPYDYyx7+p}pd$+1L3!3<^4j79oJVw*uYx*}=OI?i3{>@RKYme%x4rB2>&J<1d z=PK9DdP(0PpN@Zw*64L)Hr~sWf0|`5ts;JMwPh;-QwNs`de%=TnZYnz9F#slN@GPb z2QbPEd$tsu{l&z7NLEqx`21sGu`2V(L`83gtqyC2ns&Zv+K$7%vG1-;-O=xdk*byJ zj+>+|&}ut@luRmDa@vZ-YCa>K=-|L(hhXE)sLIG*Z?Au80O6xcSC#vzD%ZUkB}Y!@ z*KBD6&jk-UI=|Qux*Rc@YKl$%4r<(Qqa{#=%ZbwS6HkuRvcPUKRh6;y0*W|A*xWsG zlvC$1s*oc4e>uG0ehx@(5p^&o1Ys`j<|JVJ8Uxu7yF9Hg2MP66ZWwu6@8?P`8%(>V z#gV`A@BlI~g*;=22M-?5QwrG6kJdi}tvG6JEJr*Z-R*=b>;A7qT?@(u1O!T{!>@^} z$*KLP<32FVvnGhp!$`q*9U`AARMJ%g1^_CC27MY|mDI<`CstoBc1JciK}8bp6Jpf* zd_tkEBf-GDbmUNm+0A>EzkjL1zLPpa{Ozn_{g(RH(;4KV@Wp@G%YPAc0*-++Qt1~j z&M*7_U7X;hXnxR`*NHdIb%kA|NnnS{|0U{w%@6vG zqCChVqJotU{{FwoV-l1*?lDBQ9gzQj`zj6wB)lOuO1^)Y{@$P9(O_zmo;JTn$v?|M zln(@3YkBE|7PIHT{=Fb+${@~0Tgw0DCieyU`-dx_qN4Osit+!l5`Wlk|9P-rf@5I6 zgW+R5(8IGg>;D*amOPl6-Fv=&Tm7Fz92@ocu{0k;!vAF@IvJ^BbL4rG{x2d6At6*` z9FHgcT{zGP5LRc@jk@MQT+QAOvOMPf-`ZyjP%on8?f3UOu`Xa5Gfh!|82H~$fk)wl z_Sr4}uW)}>4?KaR!m;EJ9A&Y=q987-O=DcQmb#`-2>=yGWRtG0js)~AZ+wcAkTJvO z-7lQA*nkJc1i$-<^Boi<`WtS%$a}Ve$tG|6;$NcqyGVXy{a^e&T_r_ zNom79??XZjib;pbKN0>xPAK?aEfXS>~5mhoyK1946ceqpWxe3OXtdwO#5tQN(!b3mv~5CM_ed7VRuV=IYAoIAlG*?3QJ7jJ?HLXKrE-&>$P)4%!^ z)?sWn3HFnx|c2PdBvh#Ug{N!^wg%eV=D^8Wz%?Een{ literal 0 HcmV?d00001 From 13827be17d287953184cb56cbceb776096b997e7 Mon Sep 17 00:00:00 2001 From: Allen Nie Date: Tue, 24 Jun 2025 11:58:40 -0700 Subject: [PATCH 046/314] Update CONTRIBUTING.md --- CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cdea3af6..853d3ce6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,6 +23,7 @@ Here is an outline: The above is applicable to all contributors including the maintainers. +![workflow](https://github.com/AgentOpt/Trace/blob/experimental/docs/images/contributing_workflow.png?raw=true) ### Communication From 76b5aada6e6a2083c862f94cae4a61afaff11d0b Mon Sep 17 00:00:00 2001 From: Allen Nie Date: Tue, 24 Jun 2025 12:01:10 -0700 Subject: [PATCH 047/314] Update CONTRIBUTING.md --- CONTRIBUTING.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 853d3ce6..c0d97cce 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,7 +21,9 @@ Here is an outline: 6. [Exception] Updates to non-coding elements (like documents) does not necessarily require a PR. -The above is applicable to all contributors including the maintainers. +The above is applicable to all contributors, including the maintainers. + +All the features and bug fixes are merged into the experimental branch. After features are all added to the experimental branch, a version branch (e.g., `0.2.1`) will be created from `experimental`, and it will be staged for a release (merge into the main branch). ![workflow](https://github.com/AgentOpt/Trace/blob/experimental/docs/images/contributing_workflow.png?raw=true) From 4559df2cb1b1c2a48d48fe55f33eb457cf6119c4 Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 24 Jun 2025 19:29:47 +0000 Subject: [PATCH 048/314] Add example usages of LLMFactory in docstring. --- opto/utils/llm.py | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/opto/utils/llm.py b/opto/utils/llm.py index 5039b266..9f419c26 100644 --- a/opto/utils/llm.py +++ b/opto/utils/llm.py @@ -240,7 +240,42 @@ def create(self, **config: Any): } class LLMFactory: - """Factory for creating LLM instances with predefined profiles.""" + """Factory for creating LLM instances with predefined profiles. + + The code comes with these built-in profiles: + + llm_default = LLM(profile="default") # gpt-4o-mini + llm_premium = LLM(profile="premium") # gpt-4 + llm_cheap = LLM(profile="cheap") # gpt-4o-mini + llm_fast = LLM(profile="fast") # gpt-3.5-turbo-mini + llm_reasoning = LLM(profile="reasoning") # o1-mini + + You can override those built-in profiles: + + LLMFactory.register_profile("default", "LiteLLM", model="gpt-4o", temperature=0.5) + LLMFactory.register_profile("premium", "LiteLLM", model="o1-preview", max_tokens=8000) + LLMFactory.register_profile("cheap", "LiteLLM", model="gpt-3.5-turbo", temperature=0.9) + LLMFactory.register_profile("fast", "LiteLLM", model="gpt-3.5-turbo", max_tokens=500) + LLMFactory.register_profile("reasoning", "LiteLLM", model="o1-preview") + + An Example of using Different Backends + + # Register custom profiles for different use cases + LLMFactory.register_profile("advanced_reasoning", "LiteLLM", model="o1-preview", max_tokens=4000) + LLMFactory.register_profile("claude_sonnet", "LiteLLM", model="claude-3-5-sonnet-latest", temperature=0.3) + LLMFactory.register_profile("custom_server", "CustomLLM", model="llama-3.1-8b") + + # Use in different contexts + reasoning_llm = LLM(profile="advanced_reasoning") # For complex reasoning + claude_llm = LLM(profile="claude_sonnet") # For Claude responses + local_llm = LLM(profile="custom_server") # For local deployment + + # Single LLM optimizer with custom profile + optimizer1 = OptoPrime(parameters, llm=LLM(profile="advanced_reasoning")) + + # Multi-LLM optimizer with multiple profiles + optimizer2 = OptoPrimeMulti(parameters, llm_profiles=["cheap", "premium", "claude_sonnet"], generation_technique="multi_llm") + """ # Default profiles for different use cases _profiles = { From 3b1e875a9b516abc17e647f6e55d6f0d4477d526 Mon Sep 17 00:00:00 2001 From: Allen Nie Date: Tue, 24 Jun 2025 13:52:22 -0700 Subject: [PATCH 049/314] Update CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c0d97cce..60446573 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,7 +19,7 @@ Here is an outline: 5. [**LIGHT**] For contributions under the directory `opto/features`, they should be submitted as PR to the `experimental` branch. These usually are not under roadmap and are content not made as dependable by codes in other directories. That is, contents under `opto/features/A` should not be imported by files other than those under `opto/features/A`. So long as this rule is met, the PR will be incorprated under a light review. -6. [Exception] Updates to non-coding elements (like documents) does not necessarily require a PR. +6. [Exception] Core contributors only: Updates to non-coding elements (like documents) do not necessarily require a PR The above is applicable to all contributors, including the maintainers. From 6ede17ebd2fd6b7205879b9562b631757d22262c Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 24 Jun 2025 23:55:42 -0700 Subject: [PATCH 050/314] Fix API inconsistency of update in Minibatch, which results in num_threads not set correctly for BasicSearchAlgorithm. --- opto/trainer/algorithms/basic_algorithms.py | 26 ++++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/opto/trainer/algorithms/basic_algorithms.py b/opto/trainer/algorithms/basic_algorithms.py index 9baa09e5..4bc8247d 100644 --- a/opto/trainer/algorithms/basic_algorithms.py +++ b/opto/trainer/algorithms/basic_algorithms.py @@ -145,7 +145,7 @@ def train(self, outputs = [self.forward(self.agent, x, guide, info) for x, info in zip(xs, infos) ] # Update the agent - score = self.update(outputs, verbose=verbose) + score = self.update(outputs, verbose=verbose, num_threads=num_threads, **kwargs) # Reject the update if the score on the current batch is not improved if ensure_improvement: @@ -225,14 +225,16 @@ def forward(self, agent, x, guide, info): """ raise NotImplementedError("Subclasses must implement this method") - def update(self, outputs, verbose=False): + def update(self, outputs, verbose=False, num_threads=None, **kwargs): """ Subclasses can implement this method to update the agent. Args: outputs: returned value from self.step verbose: whether to print the output of the agent + num_threads: maximum number of threads to use (overrides self.num_threads) Returns: score: average score of the minibatch of inputs """ + num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads raise NotImplementedError("Subclasses must implement this method") @@ -254,15 +256,18 @@ class MinibatchAlgorithm(Minibatch): def forward(self, agent, x, guide, info): return standard_optimization_step(agent, x, guide, info) # (score, target, feedback) - def update(self, outputs, *args, **kwargs): + def update(self, outputs, verbose=False, num_threads=None, **kwargs): """ Subclasses can implement this method to update the agent. Args: outputs: returned value from self.step verbose: whether to print the output of the agent + num_threads: maximum number of threads to use (overrides self.num_threads) Returns: score: average score of the minibatch of inputs """ + num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads + scores, targets, feedbacks = [], [], [] # Concatenate the targets and feedbacks into a single string for target, score, feedback in outputs: @@ -276,14 +281,14 @@ def update(self, outputs, *args, **kwargs): # Update the agent using the feedback self.optimizer.zero_feedback() self.optimizer.backward(target, feedback) - self.optimizer_step(*args, **kwargs) # update the agent + self.optimizer_step(verbose=verbose, num_threads=num_threads, **kwargs) # update the agent return average_score # return the average score of the minibatch of inputs - def optimizer_step(self, bypassing=False, *args, **kwargs): + def optimizer_step(self, bypassing=False, verbose=False, num_threads=None, **kwargs): """ Subclasses can implement this method to update the agent. """ # We separate this method from the update method to allow subclasses to implement their own optimization step. - return self.optimizer.step(*args, bypassing=bypassing, **kwargs) + return self.optimizer.step(bypassing=bypassing, verbose=verbose, **kwargs) class BasicSearchAlgorithm(MinibatchAlgorithm): @@ -318,9 +323,11 @@ def train(self, min_score=min_score, verbose=verbose, num_threads=num_threads, **kwargs) # This code should be reusable for other algorithms - def optimizer_step(self, bypassing=False, verbose=False, *args, **kwargs): + def optimizer_step(self, bypassing=False, verbose=False, num_threads=None, **kwargs): """ Use the optimizer to propose multiple updates and select the best one based on validation score. """ + num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads + def validate(): """ Validate the agent on the validation dataset. """ scores = evaluate(self.agent, @@ -328,18 +335,19 @@ def validate(): self.validate_dataset['inputs'], self.validate_dataset['infos'], min_score=self.min_score, - num_threads=self.num_threads, + num_threads=num_threads, description="Validating proposals") return np.mean(scores) if all([s is not None for s in scores]) else -np.inf # TODO perhaps we can ask for multiple updates in one query or use different temperatures in different queries # Generate different proposals step_kwargs = dict(bypassing=True, verbose='output') # we don't print the inner full message + step_kwargs.update(kwargs) # update with additional kwargs if provided use_asyncio = self._use_asyncio() if use_asyncio: update_dicts = async_run([super().optimizer_step]*self.num_proposals, kwargs_list=[step_kwargs] * self.num_proposals, - max_workers=self.num_threads, + max_workers=num_threads, description=f"Generating {self.num_proposals} proposals") # async step else: update_dicts = [self.optimizer.step(**step_kwargs) for _ in range(self.num_proposals)] From dd3306fca52edcbabe0b441829aadf1606d79e71 Mon Sep 17 00:00:00 2001 From: Ching-An Cheng Date: Wed, 25 Jun 2025 00:01:33 -0700 Subject: [PATCH 051/314] Update CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 60446573..cf1771c7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,7 +9,7 @@ Merging a PR requires at least one reviewer different from the contributor, exce Here is an outline: -1. `main` will be regularly updated by PRs based on the development of the `experimental` branch following the [roadmap doc](https://docs.google.com/spreadsheets/d/1dMoECd2Soj6bATpkNDeaMxl0ymOYCtGq7ZiHr0JRdJU/edit?usp=sharing). Each update will result in a version update of the first two digits. +1. `main` will be regularly updated by PRs based on the development of the `experimental` branch following the [roadmap doc](https://docs.google.com/spreadsheets/d/1dMoECd2Soj6bATpkNDeaMxl0ymOYCtGq7ZiHr0JRdJU/edit?usp=sharing). Each update will result in a version update. 2. Except for the planned roadmap, `main` will only be updated to fix bugs. Bug fix to what is in `main` should be submitted as PR to `main`. This will trigger a quicker review and result in a version update in the third digit, and the `experimental` branch will then rebase on the updated `main`. From c5040987943ffbd4e3f9145de3449d59cce2be60 Mon Sep 17 00:00:00 2001 From: Ching-An Cheng Date: Wed, 25 Jun 2025 00:08:03 -0700 Subject: [PATCH 052/314] Update CONTRIBUTING.md --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cf1771c7..8ba29bb4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,9 +21,9 @@ Here is an outline: 6. [Exception] Core contributors only: Updates to non-coding elements (like documents) do not necessarily require a PR -The above is applicable to all contributors, including the maintainers. +The above is applicable to all contributors, including the core contributors and maintainers. -All the features and bug fixes are merged into the experimental branch. After features are all added to the experimental branch, a version branch (e.g., `0.2.1`) will be created from `experimental`, and it will be staged for a release (merge into the main branch). +In a regular development cycle, all features and bug fixes are merged into the experimental branch. After the items listed in the [roadmap doc](https://docs.google.com/spreadsheets/d/1dMoECd2Soj6bATpkNDeaMxl0ymOYCtGq7ZiHr0JRdJU/edit?usp=sharing) are all added to the `experimental` branch, a version branch (e.g., `0.2.1`) will be created from `experimental`, and it will be staged for a release (to be merged into the `main` branch with a PR). At this point, the version number of the `experimental` branch will be updated to start the development of the next version. ![workflow](https://github.com/AgentOpt/Trace/blob/experimental/docs/images/contributing_workflow.png?raw=true) From 2466608e5ddc987a0a7c51ca14f8f8b9e30d1820 Mon Sep 17 00:00:00 2001 From: Ching-An Cheng Date: Wed, 25 Jun 2025 00:08:35 -0700 Subject: [PATCH 053/314] Update CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8ba29bb4..eff59005 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ Trace is an actively growing project and under active maintenance and development! We maintain two major branches `main` and `experimental`. The `main` branch is the most stable, version-controlled branch and it is what the PyPI package is linked to. On the other hand, the `experimental` branch is the dev branch, which will change more dynamically in in preparation for the next version update. -### Review Process and Update Dynamics +### Development and Review Process Contribution to these two branches requires going through a review process via PR and passing all unit tests in CI. Merging a PR requires at least one reviewer different from the contributor, except for those marked as [**LIGHT**] below. From 67791746e58e53881553626bdeb7a3d8b22fbb6c Mon Sep 17 00:00:00 2001 From: chinganc Date: Wed, 25 Jun 2025 22:03:43 +0000 Subject: [PATCH 054/314] Remove constraint in Node classes --- .../evals/textgrad_prompt_optimization.py | 2 +- opto/optimizers/optoprime.py | 8 ++--- opto/optimizers/textgrad.py | 1 - opto/trace/bundle.py | 2 +- opto/trace/nodes.py | 34 +++++++------------ 5 files changed, 18 insertions(+), 29 deletions(-) diff --git a/examples/textgrad_examples/evals/textgrad_prompt_optimization.py b/examples/textgrad_examples/evals/textgrad_prompt_optimization.py index dc6111f0..e87b3b9e 100644 --- a/examples/textgrad_examples/evals/textgrad_prompt_optimization.py +++ b/examples/textgrad_examples/evals/textgrad_prompt_optimization.py @@ -102,7 +102,7 @@ def run_validation_revert(system_prompt: tg.Variable, results, model, eval_fn, v # Testing the 0-shot performance of the evaluation engine system_prompt = trace.node(STARTING_SYSTEM_PROMPT, trainable=True, - constraint="structured system prompt to a somewhat capable language model that specifies the behavior and strategies for the QA task") + description="structured system prompt to a somewhat capable language model that specifies the behavior and strategies for the QA task") # model_evaluation = tg.BlackboxLLM(llm_api_eval, system_prompt) def model_evaluation(x): diff --git a/opto/optimizers/optoprime.py b/opto/optimizers/optoprime.py index 5a5c5c36..74e5d1f4 100644 --- a/opto/optimizers/optoprime.py +++ b/opto/optimizers/optoprime.py @@ -42,7 +42,7 @@ def node_to_function_feedback(node_feedback: TraceGraph): visited.add(node) if node.is_root: # Need an or condition here - roots.update({node.py_name: (node.data, node._constraint)}) + roots.update({node.py_name: (node.data, node.description)}) else: # Some might be root (i.e. blanket nodes) and some might be intermediate nodes # Blanket nodes belong to roots @@ -52,12 +52,12 @@ def node_to_function_feedback(node_feedback: TraceGraph): documentation.update({get_fun_name(node): node.description}) graph.append((level, repr_function_call(node))) if level == depth: - output.update({node.py_name: (node.data, node._constraint)}) + output.update({node.py_name: (node.data, node.description)}) else: - others.update({node.py_name: (node.data, node._constraint)}) + others.update({node.py_name: (node.data, node.description)}) else: # this is a blanket node (classified into roots) - roots.update({node.py_name: (node.data, node._constraint)}) + roots.update({node.py_name: (node.data, node.description)}) return FunctionFeedback( graph=graph, diff --git a/opto/optimizers/textgrad.py b/opto/optimizers/textgrad.py index f01d382a..722df049 100644 --- a/opto/optimizers/textgrad.py +++ b/opto/optimizers/textgrad.py @@ -413,7 +413,6 @@ def _update_prompt(self, node: Node, gradients: List[GradientInfo]): "variable_value": node.data, "variable_grad": self._get_gradient_and_context_text(gradients), "variable_short": get_short_value(node.data), - "constraint_text": node._constraint, "new_variable_start_tag": self.new_variable_tags[0], "new_variable_end_tag": self.new_variable_tags[1], # "in_context_examples": "\n".join(self.in_context_examples), diff --git a/opto/trace/bundle.py b/opto/trace/bundle.py index db51f8eb..0e1c38ff 100644 --- a/opto/trace/bundle.py +++ b/opto/trace/bundle.py @@ -191,7 +191,7 @@ def __init__( self.parameter = ParameterNode( self.info["source"], name="__code", - constraint="The code should start with:\n" + signature, + description="The code should start with:\n" + signature, projections=projections, ) diff --git a/opto/trace/nodes.py b/opto/trace/nodes.py index d97a65e6..8186175a 100644 --- a/opto/trace/nodes.py +++ b/opto/trace/nodes.py @@ -8,7 +8,7 @@ import contextvars -def node(data, name=None, trainable=False, description=None, constraint=None): +def node(data, name=None, trainable=False, description=None): """Create a Node object from data. Args: @@ -16,7 +16,6 @@ def node(data, name=None, trainable=False, description=None, constraint=None): name (str, optional): The name of the Node. trainable (bool, optional): Whether the Node is trainable. Defaults to False. description (str, optional): A string describing the data. - constraint (str, optional): A string describing any constraint that the data should obey. Returns: Node: A Node object containing the data. @@ -24,11 +23,11 @@ def node(data, name=None, trainable=False, description=None, constraint=None): Notes: If trainable=True: - If data is already a Node, extracts underlying data and updates name - - Creates ParameterNode with extracted data, name, trainable=True and constraint + - Creates ParameterNode with extracted data, name, trainable=True If trainable=False: - If data is already a Node, returns it (with warning if name provided) - - Otherwise creates new Node with data, name and constraint + - Otherwise creates new Node with data, name """ assert type(description) is str or description is None @@ -42,7 +41,6 @@ def node(data, name=None, trainable=False, description=None, constraint=None): name=name, trainable=True, description=description, - constraint=constraint, ) else: if isinstance(data, Node): @@ -50,7 +48,7 @@ def node(data, name=None, trainable=False, description=None, constraint=None): warnings.warn(f"Name {name} is ignored because data is already a Node.") return data else: - return Node(data, name=name, description=description, constraint=constraint) + return Node(data, name=name, description=description) NAME_SCOPES = [] # A stack of name scopes @@ -763,21 +761,19 @@ class Node(AbstractNode[T]): name (str, optional): The name of the node. trainable (bool, optional): Whether the node is trainable or not. Defaults to False. description (str, optional): String describing the node. Defaults to "[Node] This is a node in a computational graph." - constraint (Union[None, str], optional): String describing constraints that the data should satisfy. Defaults to None. info (Union[None, Dict], optional): Dictionary containing additional information about the node. Defaults to None. Attributes: trainable (bool): Whether the node is trainable or not. _feedback (dict): Dictionary of feedback from children nodes. _description (str): String describing the node. - _constraint (str): String describing all constraints that the data should satisfy. _backwarded (bool): Whether the backward method has been called. _info (dict): Dictionary containing additional information about the node. _dependencies (dict): Dictionary of dependencies on parameters and expandable nodes. Notes: The Node class extends AbstractNode to represent a data node in a directed graph. - It includes attributes and methods to handle feedback, constraints, and dependencies. + It includes attributes and methods to handle feedback, description, and dependencies. The node can be marked as trainable and store feedback from children nodes. The feedback mechanism is analogous to gradients in machine learning and propagates information back through the graph. The feedback mechanism supports non-commutative @@ -792,8 +788,7 @@ def __init__( *, name: str = None, trainable: bool = False, - description: str = "[Node] This is a node in a computational graph.", - constraint: Union[None, str] = None, + description: str = None, info: Union[None, Dict] = None, ) -> None: """Initialize an instance of the Node class. @@ -803,12 +798,11 @@ def __init__( name: The name of the node (optional). trainable: A boolean indicating whether the node is trainable or not (optional). description: A string describing the node (optional). - constraint: A string describing constraints on the node (optional). info: A dictionary containing additional information about the node (optional). """ if description == "" or description is None: - description = "[Node] This is a node in a computational graph." + description = f"[Node] Data type: {type(value)}." matched = re.match(r"^\[([^\[\]]+)\]", description) if not matched: @@ -822,7 +816,6 @@ def __init__( # to support implementing aggregation that is not commutable. self._feedback = defaultdict(list) self._description = description - self._constraint = constraint self._backwarded = False self._info = info self._dependencies = {"parameter": set(), "expandable": set()} @@ -843,8 +836,10 @@ def feedback(self): @property def description(self): - """A textual description of the node.""" - return self._description + """A textual description of the node.""" + # return self._description + # remove the operator type from the description + return re.sub(r"^\[([^\[\]]+)\]", "", self._description).strip() @property def info(self): @@ -2006,7 +2001,6 @@ def __init__( name=None, trainable=True, description="[ParameterNode] This is a ParameterNode in a computational graph.", - constraint=None, projections=None, # a list of Projection info=None, ) -> None: @@ -2024,7 +2018,6 @@ def __init__( name=name, trainable=trainable, description=description, - constraint=constraint, info=info, ) self._dependencies["parameter"].add(self) @@ -2076,12 +2069,11 @@ def __init__( *, inputs: Union[List[Node], Dict[str, Node]], # extra description: str, - constraint=None, name=None, info=None, ) -> None: super().__init__( - value, name=name, description=description, constraint=constraint, info=info + value, name=name, description=description, info=info ) assert isinstance(inputs, list) or isinstance( @@ -2179,7 +2171,6 @@ def __init__( *, inputs: Union[List[Node], Dict[str, Node]], description: str = "[ExceptionNode] This is node containing the error of execution.", - constraint=None, name=None, info=None, ) -> None: @@ -2191,7 +2182,6 @@ def __init__( value, inputs=inputs, description=description, - constraint=constraint, name=name, info=info, ) From a36b89c5f04176acdb6b13d96c6b795ffd5e8c4d Mon Sep 17 00:00:00 2001 From: Ching-An Cheng Date: Wed, 25 Jun 2025 15:39:12 -0700 Subject: [PATCH 055/314] Update CONTRIBUTING.md --- CONTRIBUTING.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eff59005..7cd2df87 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,19 +11,19 @@ Here is an outline: 1. `main` will be regularly updated by PRs based on the development of the `experimental` branch following the [roadmap doc](https://docs.google.com/spreadsheets/d/1dMoECd2Soj6bATpkNDeaMxl0ymOYCtGq7ZiHr0JRdJU/edit?usp=sharing). Each update will result in a version update. -2. Except for the planned roadmap, `main` will only be updated to fix bugs. Bug fix to what is in `main` should be submitted as PR to `main`. This will trigger a quicker review and result in a version update in the third digit, and the `experimental` branch will then rebase on the updated `main`. +2. Except for the planned roadmap, `main` will only be updated to fix bugs. Bug fix to what is in `main` should be submitted as PR to `main`. This will trigger a quicker review (< 3 days) and result in a version update in the third digit, and the `experimental` branch will then rebase on the updated `main`. -3. For feature development, PR should be submitted to the `experimental` branch without version update. Generally, the `experimental` branch aims to realize the milestones listed in the next version update in the [roadmap doc](https://docs.google.com/spreadsheets/d/1dMoECd2Soj6bATpkNDeaMxl0ymOYCtGq7ZiHr0JRdJU/edit?usp=sharing). If applicable, new determinstic unit tests should be added under `tests/unit_tests`. Otherwise, an example run script should be added in `examples`. +3. For feature development, PR should be submitted to the `experimental` branch without version update. Generally, the `experimental` branch aims to realize the milestones listed in the next version update in the [roadmap doc](https://docs.google.com/spreadsheets/d/1dMoECd2Soj6bATpkNDeaMxl0ymOYCtGq7ZiHr0JRdJU/edit?usp=sharing). If applicable, new determinstic unit tests should be added under `tests/unit_tests`, or an example run script should be added in `examples`. -4. [**LIGHT**] Bugs fix to the new changes introduced in the `experimental` branch should be submitted as a PR to the `experimental` branch. This PR will be incoporated quickly with a light review. +4. [**LIGHT**] Bugs fix to the new changes introduced in the `experimental` branch should be submitted as a PR to the `experimental` branch. This PR will be incoporated quickly with a light review. 5. [**LIGHT**] For contributions under the directory `opto/features`, they should be submitted as PR to the `experimental` branch. These usually are not under roadmap and are content not made as dependable by codes in other directories. That is, contents under `opto/features/A` should not be imported by files other than those under `opto/features/A`. So long as this rule is met, the PR will be incorprated under a light review. -6. [Exception] Core contributors only: Updates to non-coding elements (like documents) do not necessarily require a PR +6. Updates to non-coding elements (like documents) do not require a PR. The above is applicable to all contributors, including the core contributors and maintainers. -In a regular development cycle, all features and bug fixes are merged into the experimental branch. After the items listed in the [roadmap doc](https://docs.google.com/spreadsheets/d/1dMoECd2Soj6bATpkNDeaMxl0ymOYCtGq7ZiHr0JRdJU/edit?usp=sharing) are all added to the `experimental` branch, a version branch (e.g., `0.2.1`) will be created from `experimental`, and it will be staged for a release (to be merged into the `main` branch with a PR). At this point, the version number of the `experimental` branch will be updated to start the development of the next version. +In a regular development cycle, all features and bug fixes are merged into the experimental branch. After the items listed in the [roadmap doc](https://docs.google.com/spreadsheets/d/1dMoECd2Soj6bATpkNDeaMxl0ymOYCtGq7ZiHr0JRdJU/edit?usp=sharing) are all added to the `experimental` branch, a version branch (e.g., `v0.2.1`) will be created from `experimental`, and it will be staged for a release (to be merged into the `main` branch with a PR). At this point, the version number of the `experimental` branch will be updated to start the development of the next version. ![workflow](https://github.com/AgentOpt/Trace/blob/experimental/docs/images/contributing_workflow.png?raw=true) @@ -33,6 +33,10 @@ In a regular development cycle, all features and bug fixes are merged into the e 2. For bugs, feature requests, contributions, or questions that might be related to a broader audience, post them as issues on the github page. +### Other Branches + +In addition to `main` and `experimental`, other branches have a naming convention or `vx.x.x` for version branchs, or of `feature/xxx` or `fix/xxx`, which implements the items on the roadmap. They will be merged into the `main` or `experimental` accordingly following the rules above. Once merged, they will be removed. + # Steps for Contributions From ce5ac93ef8f8c87a8aa4071fc81dfb79078c466e Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 26 Jun 2025 03:35:09 +0000 Subject: [PATCH 056/314] Update default description. --- opto/trace/nodes.py | 15 +++++++++------ opto/trace/propagators/propagators.py | 2 +- tests/unit_tests/test_modules.py | 2 +- tests/unit_tests/test_nodes.py | 12 ++++++++---- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/opto/trace/nodes.py b/opto/trace/nodes.py index 8186175a..f10c3b16 100644 --- a/opto/trace/nodes.py +++ b/opto/trace/nodes.py @@ -802,7 +802,7 @@ def __init__( """ if description == "" or description is None: - description = f"[Node] Data type: {type(value)}." + description = f"[Node] type: {type(value)}" matched = re.match(r"^\[([^\[\]]+)\]", description) if not matched: @@ -840,6 +840,11 @@ def description(self): # return self._description # remove the operator type from the description return re.sub(r"^\[([^\[\]]+)\]", "", self._description).strip() + + @property + def op_name(self): + """The operator type of the node, extracted from the description.""" + return get_op_name(self._description) @property def info(self): @@ -1014,7 +1019,7 @@ def backward( # Plot the edge from parent to node # Bypass chain of identity operators (for better visualization) while ( - get_op_name(parent.description) in IDENTITY_OPERATORS + parent.op_name in IDENTITY_OPERATORS ) and simple_visualization: assert ( len(parent.parents) == 1 @@ -2000,14 +2005,12 @@ def __init__( *, name=None, trainable=True, - description="[ParameterNode] This is a ParameterNode in a computational graph.", + description=None, projections=None, # a list of Projection info=None, ) -> None: if description is None or description == "": - description = ( - "[ParameterNode] This is a ParameterNode in a computational graph." - ) + description = f"[ParameterNode] type: {type(value)}" matched = re.match(r"^\[([^\[\]]+)\]", description) if not matched: diff --git a/opto/trace/propagators/propagators.py b/opto/trace/propagators/propagators.py index 101f538f..9081f8d3 100644 --- a/opto/trace/propagators/propagators.py +++ b/opto/trace/propagators/propagators.py @@ -45,7 +45,7 @@ def register(self, operator_name, propagate_function): self.override[operator_name] = propagate_function def propagate(self, child: MessageNode) -> Dict[Node, Any]: - operator_name = get_op_name(child.description) + operator_name = child.op_name if operator_name in self.override: return self.override[operator_name](child) else: diff --git a/tests/unit_tests/test_modules.py b/tests/unit_tests/test_modules.py index 1934ae5b..f08d26e5 100644 --- a/tests/unit_tests/test_modules.py +++ b/tests/unit_tests/test_modules.py @@ -239,7 +239,7 @@ def test_model_dump_with_projection(): try: # Test with BlackCodeFormatter from opto.trace.projections import BlackCodeFormatter - dummy.model_dump(temp_file, projection=BlackCodeFormatter()) + dummy.model_dump(temp_file, projections=[BlackCodeFormatter()]) with open(temp_file, "r") as f: content = f.read() # Check if content is properly formatted diff --git a/tests/unit_tests/test_nodes.py b/tests/unit_tests/test_nodes.py index b2b3a73f..129c26e8 100644 --- a/tests/unit_tests/test_nodes.py +++ b/tests/unit_tests/test_nodes.py @@ -145,16 +145,20 @@ def test_trainable_wrapping(): def test_node_description(): x = node(1, description="x") - assert x.description == "[Node] x" + assert x._description == "[Node] x" + assert x.description == "x" y = node(1) - assert y.description == '[Node] This is a node in a computational graph.' + assert y.description == "type: " + assert y._description == "[Node] type: " x = node(1, description="x", trainable=True) - assert x.description == "[ParameterNode] x" + assert x.description == "x" + assert x._description == "[ParameterNode] x" x = node(1, trainable=True) - assert x.description == "[ParameterNode] This is a ParameterNode in a computational graph." + assert x.description == "type: " + assert x._description == "[ParameterNode] type: " def test_iterating_numpy_array(): From 32736fc82f58f4c4f4327752aa8b51578b1e0393 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 26 Jun 2025 04:13:13 +0000 Subject: [PATCH 057/314] Fix some bugs due to description change. --- opto/optimizers/optoprime.py | 2 +- opto/trace/bundle.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/opto/optimizers/optoprime.py b/opto/optimizers/optoprime.py index 74e5d1f4..53513198 100644 --- a/opto/optimizers/optoprime.py +++ b/opto/optimizers/optoprime.py @@ -361,7 +361,7 @@ def problem_instance(self, summary, mask=None): else "" ), documentation=( - "\n".join([v for v in summary.documentation.values()]) + "\n".join([f"[{k}] {v}" for k, v in summary.documentation.items()]) if "#Documentation" not in mask else "" ), diff --git a/opto/trace/bundle.py b/opto/trace/bundle.py index 0e1c38ff..6d5fa5c8 100644 --- a/opto/trace/bundle.py +++ b/opto/trace/bundle.py @@ -164,7 +164,7 @@ def __init__( if description is None: # Generate the description from the function name and docstring. - description = f"[{self.info['fun_name']}] {self.info['doc']}." + description = f"[{self.info['fun_name']}] {self.info['doc']}" assert len(get_op_name(description)) > 0 self.traceable_code = traceable_code From 948fa18f75fc13a810779444e9177306a2c0725d Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 26 Jun 2025 04:19:41 +0000 Subject: [PATCH 058/314] Make default description construction programmatic --- opto/trace/nodes.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/opto/trace/nodes.py b/opto/trace/nodes.py index f10c3b16..6110974f 100644 --- a/opto/trace/nodes.py +++ b/opto/trace/nodes.py @@ -802,11 +802,11 @@ def __init__( """ if description == "" or description is None: - description = f"[Node] type: {type(value)}" + description = f"[{type(self).__name__}] type: {type(value)}" matched = re.match(r"^\[([^\[\]]+)\]", description) if not matched: - description = "[Node] " + description.strip() + description = f"[{type(self).__name__}] " + description.strip() super().__init__(value, name=name) self.trainable = trainable @@ -2009,12 +2009,6 @@ def __init__( projections=None, # a list of Projection info=None, ) -> None: - if description is None or description == "": - description = f"[ParameterNode] type: {type(value)}" - - matched = re.match(r"^\[([^\[\]]+)\]", description) - if not matched: - description = "[ParameterNode] " + description.strip() super().__init__( value, @@ -2173,7 +2167,7 @@ def __init__( value: Exception, *, inputs: Union[List[Node], Dict[str, Node]], - description: str = "[ExceptionNode] This is node containing the error of execution.", + description: str = None, name=None, info=None, ) -> None: From 685d99d6aeed450a0d40cc8b314979bbcb61f94b Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 26 Jun 2025 05:42:32 +0000 Subject: [PATCH 059/314] Set default description to be None. --- opto/trace/nodes.py | 11 +++++++---- tests/unit_tests/test_nodes.py | 8 ++++---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/opto/trace/nodes.py b/opto/trace/nodes.py index 6110974f..26191b5e 100644 --- a/opto/trace/nodes.py +++ b/opto/trace/nodes.py @@ -587,8 +587,9 @@ def get_label(self, x): """ # using colon in the name causes problems in graphviz description = x.description - if len(x.description) > self.print_limit: - description = x.description[: self.print_limit] + "..." + description = '' if description is None else description + if len(description) > self.print_limit: + description = description[: self.print_limit] + "..." text = x.py_name + "\n" + description + "\n" content = str(x.data) @@ -802,7 +803,7 @@ def __init__( """ if description == "" or description is None: - description = f"[{type(self).__name__}] type: {type(value)}" + description = f"[{type(self).__name__}]" matched = re.match(r"^\[([^\[\]]+)\]", description) if not matched: @@ -839,7 +840,9 @@ def description(self): """A textual description of the node.""" # return self._description # remove the operator type from the description - return re.sub(r"^\[([^\[\]]+)\]", "", self._description).strip() + description = re.sub(r"^\[([^\[\]]+)\]", "", self._description).strip() + # return None if empty + return description if description else None @property def op_name(self): diff --git a/tests/unit_tests/test_nodes.py b/tests/unit_tests/test_nodes.py index 129c26e8..6d5d1e73 100644 --- a/tests/unit_tests/test_nodes.py +++ b/tests/unit_tests/test_nodes.py @@ -149,16 +149,16 @@ def test_node_description(): assert x.description == "x" y = node(1) - assert y.description == "type: " - assert y._description == "[Node] type: " + assert y.description == None + assert y._description == "[Node]" x = node(1, description="x", trainable=True) assert x.description == "x" assert x._description == "[ParameterNode] x" x = node(1, trainable=True) - assert x.description == "type: " - assert x._description == "[ParameterNode] type: " + assert x.description == None + assert x._description == "[ParameterNode]" def test_iterating_numpy_array(): From 576c77d818b18c7bb0394ea0201c71687a4ffc6d Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 26 Jun 2025 05:44:52 +0000 Subject: [PATCH 060/314] Fix bug of textgrad seeing None description. --- opto/optimizers/textgrad.py | 1 + 1 file changed, 1 insertion(+) diff --git a/opto/optimizers/textgrad.py b/opto/optimizers/textgrad.py index 722df049..b78353de 100644 --- a/opto/optimizers/textgrad.py +++ b/opto/optimizers/textgrad.py @@ -281,6 +281,7 @@ def rm_node_attrs(text: str) -> str: Returns: String with trace node attributes removed """ + text = "" if text is None else text return re.sub(r"\[.*?\]", "", text).strip() From df0cce4141c6f62f5cd5da514be9703443c8dbe3 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 26 Jun 2025 06:00:31 +0000 Subject: [PATCH 061/314] Fix a bug in textgrad due to earlier commit that removes constraint_text --- opto/optimizers/textgrad.py | 1 + 1 file changed, 1 insertion(+) diff --git a/opto/optimizers/textgrad.py b/opto/optimizers/textgrad.py index b78353de..bdfdeab4 100644 --- a/opto/optimizers/textgrad.py +++ b/opto/optimizers/textgrad.py @@ -414,6 +414,7 @@ def _update_prompt(self, node: Node, gradients: List[GradientInfo]): "variable_value": node.data, "variable_grad": self._get_gradient_and_context_text(gradients), "variable_short": get_short_value(node.data), + "constraint_text": rm_node_attrs(node.description), "new_variable_start_tag": self.new_variable_tags[0], "new_variable_end_tag": self.new_variable_tags[1], # "in_context_examples": "\n".join(self.in_context_examples), From 21602508618481d67776f3952c8f18c08e51dd95 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 26 Jun 2025 06:07:05 +0000 Subject: [PATCH 062/314] Update docstring of Node. --- opto/trace/nodes.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/opto/trace/nodes.py b/opto/trace/nodes.py index 26191b5e..756328e6 100644 --- a/opto/trace/nodes.py +++ b/opto/trace/nodes.py @@ -761,13 +761,13 @@ class Node(AbstractNode[T]): value (Any): The value to be assigned to the node. name (str, optional): The name of the node. trainable (bool, optional): Whether the node is trainable or not. Defaults to False. - description (str, optional): String describing the node. Defaults to "[Node] This is a node in a computational graph." + description (str, optional): String describing the node which acts as a soft constraint. Defaults to None. info (Union[None, Dict], optional): Dictionary containing additional information about the node. Defaults to None. Attributes: trainable (bool): Whether the node is trainable or not. _feedback (dict): Dictionary of feedback from children nodes. - _description (str): String describing the node. + _description (str): String describing the node. Defaults to "[Node]". _backwarded (bool): Whether the backward method has been called. _info (dict): Dictionary containing additional information about the node. _dependencies (dict): Dictionary of dependencies on parameters and expandable nodes. @@ -791,16 +791,7 @@ def __init__( trainable: bool = False, description: str = None, info: Union[None, Dict] = None, - ) -> None: - """Initialize an instance of the Node class. - - Args: - value: The value to be assigned to the node. - name: The name of the node (optional). - trainable: A boolean indicating whether the node is trainable or not (optional). - description: A string describing the node (optional). - info: A dictionary containing additional information about the node (optional). - """ + ) -> None: if description == "" or description is None: description = f"[{type(self).__name__}]" From 5351374114e63768a024de7efb99bfaa346f755f Mon Sep 17 00:00:00 2001 From: Ching-An Cheng Date: Wed, 25 Jun 2025 23:09:00 -0700 Subject: [PATCH 063/314] Update ci.yml Apply CI to experimental branch --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f0d21b3..46e0b317 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [ main, dev, ci-multi ] + branches: [ main, dev, experimental, ci-multi ] pull_request: - branches: [ main, dev, ci-multi ] + branches: [ main, dev, experimental, ci-multi ] jobs: test: From bbb6f4d4e2ae7358aa2d54e8fa113aee5a716160 Mon Sep 17 00:00:00 2001 From: Ching-An Cheng Date: Thu, 26 Jun 2025 10:17:24 -0700 Subject: [PATCH 064/314] Update CONTRIBUTING.md --- CONTRIBUTING.md | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7cd2df87..52182780 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -51,22 +51,20 @@ If there is a minor, isolated bug that can be directly fixed, please report it a We welcome new ideas. -### Step 1: Feature Spec Doc -A feature should first be written as a Google Doc (an example is [here](https://docs.google.com/document/d/1FX1ygc8lgFpFn3ni3E2A_DCGtn505PpAM8QaAjEovsA/edit?usp=sharing)). +### Step 1: Create an Issue -### Step 2: Create an Issue -An issue should be created, and under the issue, the doc is linked. People should be allowed to comment on the doc. +If your changes are expected to involve in less 50 lines of codes, create an issue titled "[LIGHT] XXX". You should describe the motivation, give an overview of the change (e.g., by a pseudo-code) and its desired effects. Otherwise, create an issue titled "[MAJOR] XXX". You should write a more detailed description of your motivation, design, and demo. If more space is needed, you can attach a link to a Google doc. People should be allowed to comment on the doc. -### Step 3: Implement Feature -Create a separate branch, extending from the `experimental` branch. This branch contains all the new features that have not been merged into the `main` branch yet. +### Step 2: Implement Feature + +Create a separate branch, extending from the `experimental` branch, which contains all the new features that have not been merged into the `main` branch yet. Make sure your features are implemented, along with `unit tests` or `examples` to show how it's used. -### Step 4: Create a Pull Request -Create a PR formally to merge into the experiment branch and request a review. For standalone features, put the changes under `opto/features/`. This will trigger the lightest review that only checks for malicious code, or if the feature does not pass its own unit tests. -For changes to the rest, expect a slightly longer review process as we work out how the changes should be integrated with the core library. +### Step 3: Create a Pull Request +Create a PR formally to merge into the experiment branch and request a review. For standalone features, put the changes under `opto/features/`. This will trigger the lightest review that only checks for malicious code, or if the feature does not pass its own unit tests. For changes to the rest, expect a slightly longer review process as we work out how the changes should be integrated with the core library. Also, [LIGHT] issues can expect faster review than [MAJOR]. -### Step 5: Merge into Experimental +### Step 4: Merge into Experimental Once the request is approved, it will be merged into the `experimental` branch. From 92d7b8b8f478e2c922fc82aa915ed359065bd739 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 26 Jun 2025 21:09:29 +0000 Subject: [PATCH 065/314] Fix the bug in test_modules.py --- tests/unit_tests/test_modules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit_tests/test_modules.py b/tests/unit_tests/test_modules.py index 1934ae5b..f08d26e5 100644 --- a/tests/unit_tests/test_modules.py +++ b/tests/unit_tests/test_modules.py @@ -239,7 +239,7 @@ def test_model_dump_with_projection(): try: # Test with BlackCodeFormatter from opto.trace.projections import BlackCodeFormatter - dummy.model_dump(temp_file, projection=BlackCodeFormatter()) + dummy.model_dump(temp_file, projections=[BlackCodeFormatter()]) with open(temp_file, "r") as f: content = f.read() # Check if content is properly formatted From c33e1d2860379773716b65253f6d4ba074cb5fdc Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 26 Jun 2025 22:09:35 +0000 Subject: [PATCH 066/314] Move and uipdate evaluate to incorporate deepcopy and num_samples --- opto/trainer/algorithms/basic_algorithms.py | 40 ++------------------ opto/trainer/evaluators.py | 42 +++++++++++++++++++++ 2 files changed, 46 insertions(+), 36 deletions(-) create mode 100644 opto/trainer/evaluators.py diff --git a/opto/trainer/algorithms/basic_algorithms.py b/opto/trainer/algorithms/basic_algorithms.py index 4bc8247d..281bcf02 100644 --- a/opto/trainer/algorithms/basic_algorithms.py +++ b/opto/trainer/algorithms/basic_algorithms.py @@ -6,43 +6,9 @@ from opto.trainer.loader import DataLoader from opto.trainer.utils import async_run from opto.optimizers.utils import print_color +from opto.trainer.evaluators import evaluate -def evaluate(agent, guide, inputs, infos, min_score=None, num_threads=None, description=None): - """ Evaluate the agent on the inputs and return the scores - - Args: - agent: The agent to evaluate - guide: The guide to use for evaluation - inputs: List of inputs to evaluate on - infos: List of additional information for each input - min_score: Minimum score to return when an exception occurs - num_threads: Maximum number of threads to use for parallel evaluation - description: Description to display in the progress bar - """ - - def evaluate_single(i): - try: - output = agent(inputs[i]).data - score = guide.metric(inputs[i], output, infos[i]) - except: - score = min_score - return score - - N = len(inputs) - assert len(inputs) == len(infos), "Inputs and infos must have the same length" - # Use asyncio if num_threads is not None and > 1 - use_asyncio = num_threads is not None and num_threads > 1 - if use_asyncio: - # Use provided description or generate a default one - eval_description = description or f"Evaluating {N} examples" - scores = async_run([evaluate_single] * N, [(i,) for i in range(N)], - max_workers=num_threads, - description=eval_description) # list of tuples - else: - scores = [evaluate_single(i) for i in range(N)] - return scores - def standard_optimization_step(agent, x, guide, info, min_score=0): """ Forward and compute feedback. @@ -93,6 +59,7 @@ def train(self, batch_size: int = 1, # batch size for updating the agent test_dataset = None, # dataset of (x, info) pairs to evaluate the agent eval_frequency: int = 1, # frequency of evaluation + num_eval_samples: int = 1, # number of samples to use to evaluate each input log_frequency: Union[int, None] = None, # frequency of logging save_frequency: Union[int, None] = None, # frequency of saving the agent save_path: str = "checkpoints/agent.pkl", # path to save the agent @@ -112,6 +79,7 @@ def train(self, num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads test_dataset = test_dataset or train_dataset # default to train_dataset if test_dataset is not provided use_asyncio = self._use_asyncio(num_threads) + self.num_eval_samples = num_eval_samples # number of samples to use to evaluate each input # Evaluate the agent before learning if eval_frequency > 0: @@ -184,7 +152,7 @@ def evaluate(self, agent, guide, xs, infos, min_score=None, num_threads=None, de """ Evaluate the agent on the given dataset. """ num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads test_scores = evaluate(agent, guide, xs, infos, min_score=min_score, num_threads=num_threads, - description=description) + description=description, num_samples=self.num_eval_samples) if all([s is not None for s in test_scores]): return np.mean(test_scores) diff --git a/opto/trainer/evaluators.py b/opto/trainer/evaluators.py new file mode 100644 index 00000000..db9e35ec --- /dev/null +++ b/opto/trainer/evaluators.py @@ -0,0 +1,42 @@ +from opto.trainer.utils import async_run +import copy + + +def evaluate(agent, guide, inputs, infos, min_score=None, num_samples=1, num_threads=None, description=None): + """ Evaluate the agent on the inputs and return the scores + + Args: + agent: The agent to evaluate + guide: The guide to use for evaluation + inputs: List of inputs to evaluate on + infos: List of additional information for each input + min_score: Minimum score to return when an exception occurs + num_samples: Number of samples to use to evaluate each input + num_threads: Maximum number of threads to use for parallel evaluation + description: Description to display in the progress bar + """ + + def evaluate_single(agent, guide, i): + try: + output = agent(inputs[i]).data + score = guide.metric(inputs[i], output, infos[i]) + except: + score = min_score + return score + + N = len(inputs) + assert len(inputs) == len(infos), "Inputs and infos must have the same length" + # Use asyncio if num_threads is not None and > 1 + use_asyncio = num_threads is not None and num_threads > 1 + + # repeat each index num_samples times + indices = [i for i in range(N) for _ in range(num_samples)] + if use_asyncio: + # Use provided description or generate a default one + eval_description = description or f"Evaluating {N} examples" + scores = async_run([evaluate_single] * N, [(copy.deepcopy(agent), copy.deepcopy(guide), i) for i in indices], + max_workers=num_threads, + description=eval_description) # list of tuples + else: + scores = [evaluate_single(agent, guide, i) for i in indices] + return scores \ No newline at end of file From 7068f103aeb1d9722f693cda29b3db1b4b022133 Mon Sep 17 00:00:00 2001 From: windweller Date: Fri, 27 Jun 2025 00:47:57 -0700 Subject: [PATCH 067/314] fix typing to be `Optional[int]` instead of `int` --- opto/trainer/algorithms/algorithm.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/opto/trainer/algorithms/algorithm.py b/opto/trainer/algorithms/algorithm.py index 9ec35fcc..e08eec44 100644 --- a/opto/trainer/algorithms/algorithm.py +++ b/opto/trainer/algorithms/algorithm.py @@ -1,4 +1,6 @@ import warnings +from typing import Optional + from opto import trace from opto.trace.modules import Module from opto.trainer.utils import async_run @@ -28,7 +30,7 @@ class AlgorithmBase(AbstractAlgorithm): def __init__(self, agent, # trace.model - num_threads: int = None, # maximum number of threads to use for parallel execution + num_threads: Optional[int] = None, # maximum number of threads to use for parallel execution logger=None, # logger for tracking metrics *args, **kwargs): From abe462da2ccf5093f61bfdf42ce7931685d3140d Mon Sep 17 00:00:00 2001 From: chinganc Date: Fri, 27 Jun 2025 21:16:14 +0000 Subject: [PATCH 068/314] Fix a bug that BasicSearchAlgorithm always prints to stdout. --- opto/trainer/algorithms/basic_algorithms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opto/trainer/algorithms/basic_algorithms.py b/opto/trainer/algorithms/basic_algorithms.py index 4bc8247d..84d78a31 100644 --- a/opto/trainer/algorithms/basic_algorithms.py +++ b/opto/trainer/algorithms/basic_algorithms.py @@ -341,7 +341,7 @@ def validate(): # TODO perhaps we can ask for multiple updates in one query or use different temperatures in different queries # Generate different proposals - step_kwargs = dict(bypassing=True, verbose='output') # we don't print the inner full message + step_kwargs = dict(bypassing=True, verbose='output' if verbose else False) # we don't print the inner full message step_kwargs.update(kwargs) # update with additional kwargs if provided use_asyncio = self._use_asyncio() if use_asyncio: From 739759fc9f00d20cea1a34651681d09cf5f6524d Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 30 Jun 2025 23:03:50 +0000 Subject: [PATCH 069/314] Add batch_run --- opto/trace/modules.py | 9 ++++ opto/trainer/utils.py | 69 ++++++++++++++++++++++++++ tests/unit_tests/test_batch_run.py | 78 ++++++++++++++++++++++++++++++ 3 files changed, 156 insertions(+) create mode 100644 tests/unit_tests/test_batch_run.py diff --git a/opto/trace/modules.py b/opto/trace/modules.py index bf33d6a3..6b7f0114 100644 --- a/opto/trace/modules.py +++ b/opto/trace/modules.py @@ -107,6 +107,15 @@ def forward(self, *args, **kwargs): def __call__(self, *args, **kwargs): return self.forward(*args, **kwargs) + + def copy(self): + """Return a deep copy of the module except for the parameters + are set to the originals.""" + new_module = copy.deepcopy(self) + for k, v in self.parameters_dict().items(): + if hasattr(new_module, k): + setattr(new_module, k, v) + return new_module def save(self, file_name: str): """Save the parameters of the model to a pickle file.""" diff --git a/opto/trainer/utils.py b/opto/trainer/utils.py index 717ff23b..ea838954 100644 --- a/opto/trainer/utils.py +++ b/opto/trainer/utils.py @@ -4,6 +4,7 @@ from concurrent.futures import ThreadPoolExecutor from tqdm.asyncio import tqdm_asyncio from opto.trace.bundle import ALLOW_EXTERNAL_DEPENDENCIES +from opto.trace.modules import Module def async_run(runs, args_list = None, kwargs_list = None, max_workers = None, description = None): """Run multiple functions in asynchronously. @@ -47,6 +48,74 @@ async def _run(): return asyncio.run(_run()) +def batch_run(fun, max_workers=None, description=None): + """ + Create a function that runs in parallel using asyncio, with support for batching. + The batch size is inferred as the length of the longest argument or keyword argument. + + Args: + fun (callable): The function to run. + + max_workers (int, optional): Maximum number of worker threads to use. + If None, the default ThreadPoolExecutor behavior is used. + description (str, optional): Description to display in the progress bar. + + Returns: + callable: A new function that processes batches of inputs. + + NOTE: + If fun takes input that has __len__ (like lists or arrays), they won't be broadcasted. + When using batch_run, be sure to pass list of such arguments of the same length. + + Example: + >>> @batch_run(max_workers=4, description="Processing batch") + >>> def my_function(x, y): + >>> return x + y + >>> x = [1, 2, 3, 4, 5] + >>> y = 10 + >>> outputs = my_function(x, y) + >>> # outputs will be [11, 12, 13, 14, 15] + >>> # This will run the function in asynchronously with 4 threads + """ + + + def _fun(*args, **kwargs): + + # We try to infer the batch size from the args + all_args = args + tuple(kwargs.values()) + # find all list or array-like arguments and use their length as batch size + batch_size = max(len(arg) for arg in all_args if hasattr(arg, '__len__')) + + # broadcast the batch size to all args and record the indices that are broadcasted + args = [arg if hasattr(arg, '__len__') else [arg] * batch_size for arg in args] + kwargs = {k: v if hasattr(v, '__len__') else [v] * batch_size for k, v in kwargs.items()} + + # assert that all args and kwargs have the same length + lengths = [len(arg) for arg in args] + [len(v) for v in kwargs.values()] + if len(set(lengths)) != 1: + raise ValueError("All arguments and keyword arguments must have the same length.") + + # deepcopy if it is a trace.Module (as they may have mutable state) + # Module.copy() is used to create a new instance with the same parameters + _args = [arg.copy() if isinstance(arg, Module) else arg for arg in args] + _kwargs = {k: v.copy() if isinstance(v, Module) else v for k, v in kwargs.items()} + + # Run the forward function in parallel using asyncio with the same parameters. + # Since trace.Node is treated as immutable, we can safely use the same instance. + # The resultant graph will be the same as if we had called the function with the original arguments. + + # convert _args and _kwargs (args, kwargs of list) to lists of args and kwargs + + args_list = [tuple(aa[i] for aa in _args) for i in range(batch_size)] + kwargs_list = [{k: _kwargs[k][i] for k in _kwargs} for i in range(batch_size)] + + outputs = async_run([fun] * batch_size, args_list=args_list, kwargs_list=kwargs_list, + max_workers=max_workers, description=description) + return outputs + + return _fun + + if __name__ == "__main__": def tester(t): # regular time-consuming function diff --git a/tests/unit_tests/test_batch_run.py b/tests/unit_tests/test_batch_run.py new file mode 100644 index 00000000..ffee737b --- /dev/null +++ b/tests/unit_tests/test_batch_run.py @@ -0,0 +1,78 @@ +from typing import List +from opto import trace +from opto.trainer.utils import batch_run + +def test_batch_run_fun(): + + def fun(x, y): + return x + y + + # Create a batch of inputs + x = [1, 2, 3, 4, 5] + y = 10 # this will be broadcasted to each element in x + + # Run the function in batch mode + outputs = batch_run(fun, max_workers=3)(x,y) + assert outputs == [11, 12, 13, 14, 15], f"Expected [11, 12, 13, 14, 15], got {outputs}" + + # Handling a function taking a list as inputs + def fun(x: List[int], y: List[int]) -> List[int]: + return [a + b for a, b in zip(x, y)] + + x = [[1, 2, 3], [4, 5, 6]] + y = [10, 20, 30] # list won't be braodcasted correctly + + raise_error = False + try: + outputs = batch_run(fun, max_workers=3)(x, y) + except ValueError as e: + assert str(e) == "All arguments and keyword arguments must have the same length.", f"Unexpected error: {e}" + raise_error = True + assert raise_error, "Expected a ValueError but did not get one." + + # Now we can broadcast y to match the length of x + y = [[10, 20, 30]] * len(x) # Broadcast + outputs = batch_run(fun, max_workers=3)(x, y) + assert outputs == [[11, 22, 33], [14, 25, 36]], f"Expected [[11, 22, 33], [14, 25, 36]], got {outputs}" + + + y = [10, 20] # This will raise an error because x and y have different lengths + raise_error = False + try: + outputs = batch_run(fun, max_workers=3)(x, y) + except TypeError as e: + raise_error = True + assert raise_error, "Expected a TypeError but did not get one." + +def test_batch_run_module(): + + + @trace.model + class MyModule: + def __init__(self, param): + self.param = trace.node(param, trainable=True) + self._state = 0 + + def forward(self, x): + y = x + self.param + self._state += 1 # This should not affect the batch run + return y + + module = MyModule(10) + x = [1, 2, 3, 4, 5] + outputs = batch_run(module.forward, max_workers=3)(x) + assert outputs == [11, 12, 13, 14, 15], f"Expected [11, 12, 13, 14, 15], got {outputs}" + param = module.parameters()[0] + assert len(param.children) == 5 + + + x = [1, 2, 3, 4, 5] + y = [10, 20, 30, 40, 50, 60] + # This should raise an error because x and y have different lengths + raise_error = False + try: + outputs = batch_run(module.forward, max_workers=3)(x, y) + except ValueError as e: + assert str(e) == "All arguments and keyword arguments must have the same length.", f"Unexpected error: {e}" + raise_error = True + assert raise_error, "Expected a ValueError but did not get one." \ No newline at end of file From 61aab01cbcf8b605d57b5c718f9f3b416619c729 Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 30 Jun 2025 23:09:21 +0000 Subject: [PATCH 070/314] add allow_sequential_run to async_run. --- opto/trainer/utils.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/opto/trainer/utils.py b/opto/trainer/utils.py index ea838954..bebed5b3 100644 --- a/opto/trainer/utils.py +++ b/opto/trainer/utils.py @@ -6,7 +6,7 @@ from opto.trace.bundle import ALLOW_EXTERNAL_DEPENDENCIES from opto.trace.modules import Module -def async_run(runs, args_list = None, kwargs_list = None, max_workers = None, description = None): +def async_run(runs, args_list = None, kwargs_list = None, max_workers = None, description = None, allow_sequential_run=True): """Run multiple functions in asynchronously. Args: @@ -17,7 +17,7 @@ def async_run(runs, args_list = None, kwargs_list = None, max_workers = None, de If None, the default ThreadPoolExecutor behavior is used. description (str, optional): description to display in the progress bar. This can indicate the current stage (e.g., "Evaluating", "Training", "Optimizing"). - + allow_sequential_run (bool, optional): if True, runs the functions sequentially if max_workers is 1. """ # if ALLOW_EXTERNAL_DEPENDENCIES is not False: # warnings.warn( @@ -27,25 +27,26 @@ def async_run(runs, args_list = None, kwargs_list = None, max_workers = None, de # UserWarning, # ) - if args_list is None: args_list = [[]] * len(runs) if kwargs_list is None: kwargs_list = [{}] * len(runs) - async def _run(): - loop = asyncio.get_event_loop() - with ThreadPoolExecutor(max_workers=max_workers) as executor: - tasks = [loop.run_in_executor(executor, functools.partial(run, *args, **kwargs)) - for run, args, kwargs, in zip(runs, args_list, kwargs_list)] - - # Use the description in the tqdm progress bar if provided - if description: - return await tqdm_asyncio.gather(*tasks, desc=description) - else: - return await tqdm_asyncio.gather(*tasks) - - return asyncio.run(_run()) + if (max_workers == 1) and allow_sequential_run: # run without asyncio + return [run(*args, **kwargs) for run, args, kwargs in zip(runs, args_list, kwargs_list)] + else: + async def _run(): + loop = asyncio.get_event_loop() + with ThreadPoolExecutor(max_workers=max_workers) as executor: + tasks = [loop.run_in_executor(executor, functools.partial(run, *args, **kwargs)) + for run, args, kwargs, in zip(runs, args_list, kwargs_list)] + + # Use the description in the tqdm progress bar if provided + if description: + return await tqdm_asyncio.gather(*tasks, desc=description) + else: + return await tqdm_asyncio.gather(*tasks) + return asyncio.run(_run()) def batch_run(fun, max_workers=None, description=None): From 8e43233ab4bcbd6701e26e0f023105127d8ee0e3 Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 30 Jun 2025 23:28:37 +0000 Subject: [PATCH 071/314] Update batch_run. --- opto/trainer/guide.py | 11 ++++ opto/trainer/utils.py | 99 +++++++++++++++++------------- tests/unit_tests/test_batch_run.py | 14 +++-- 3 files changed, 76 insertions(+), 48 deletions(-) diff --git a/opto/trainer/guide.py b/opto/trainer/guide.py index 30c428a6..cad39f37 100644 --- a/opto/trainer/guide.py +++ b/opto/trainer/guide.py @@ -1,6 +1,7 @@ from typing import List, Dict, Any, Union, Tuple, Optional, Callable import json import re +import copy from opto.utils.llm import LLM, AbstractModel from opto.trainer.suggest import Suggest @@ -43,6 +44,16 @@ def get_feedback(self, query: str, response: str, reference: Optional[str] = Non def metric(self, query: str, response: str, reference: Optional[str] = None, **kwargs) -> float: """ Exact match metric """ return self.get_feedback(query, response, reference)[0] + + def copy(): + """ Create a copy of the guide instance. + + Returns: + A new instance of the same guide class with the same parameters. + """ + # This is used in batch_run to create a new instance of the guide. + # This can be overridden by subclasses to provide a more specific copy behavior. + return copy.deepcopy(self) class VerbalJudgeGuide(AutoGuide): diff --git a/opto/trainer/utils.py b/opto/trainer/utils.py index bebed5b3..93436505 100644 --- a/opto/trainer/utils.py +++ b/opto/trainer/utils.py @@ -5,6 +5,7 @@ from tqdm.asyncio import tqdm_asyncio from opto.trace.bundle import ALLOW_EXTERNAL_DEPENDENCIES from opto.trace.modules import Module +from opto.trainer.guide import AutoGuide def async_run(runs, args_list = None, kwargs_list = None, max_workers = None, description = None, allow_sequential_run=True): """Run multiple functions in asynchronously. @@ -49,7 +50,7 @@ async def _run(): return asyncio.run(_run()) -def batch_run(fun, max_workers=None, description=None): +def batch_run(max_workers=None, description=None): """ Create a function that runs in parallel using asyncio, with support for batching. The batch size is inferred as the length of the longest argument or keyword argument. @@ -72,50 +73,64 @@ def batch_run(fun, max_workers=None, description=None): >>> @batch_run(max_workers=4, description="Processing batch") >>> def my_function(x, y): >>> return x + y - >>> x = [1, 2, 3, 4, 5] - >>> y = 10 - >>> outputs = my_function(x, y) - >>> # outputs will be [11, 12, 13, 14, 15] - >>> # This will run the function in asynchronously with 4 threads + >>> x = [1, 2, 3, 4, 5] + >>> y = 10 + >>> outputs = my_function(x, y) + >>> # outputs will be [11, 12, 13, 14, 15] + >>> # This will run the function in asynchronously with 4 threads """ - - def _fun(*args, **kwargs): - - # We try to infer the batch size from the args - all_args = args + tuple(kwargs.values()) - # find all list or array-like arguments and use their length as batch size - batch_size = max(len(arg) for arg in all_args if hasattr(arg, '__len__')) + def decorator(fun): + """ + Decorator to create a function that runs in parallel using asyncio, with support for batching. - # broadcast the batch size to all args and record the indices that are broadcasted - args = [arg if hasattr(arg, '__len__') else [arg] * batch_size for arg in args] - kwargs = {k: v if hasattr(v, '__len__') else [v] * batch_size for k, v in kwargs.items()} - - # assert that all args and kwargs have the same length - lengths = [len(arg) for arg in args] + [len(v) for v in kwargs.values()] - if len(set(lengths)) != 1: - raise ValueError("All arguments and keyword arguments must have the same length.") - - # deepcopy if it is a trace.Module (as they may have mutable state) - # Module.copy() is used to create a new instance with the same parameters - _args = [arg.copy() if isinstance(arg, Module) else arg for arg in args] - _kwargs = {k: v.copy() if isinstance(v, Module) else v for k, v in kwargs.items()} - - # Run the forward function in parallel using asyncio with the same parameters. - # Since trace.Node is treated as immutable, we can safely use the same instance. - # The resultant graph will be the same as if we had called the function with the original arguments. - - # convert _args and _kwargs (args, kwargs of list) to lists of args and kwargs - - args_list = [tuple(aa[i] for aa in _args) for i in range(batch_size)] - kwargs_list = [{k: _kwargs[k][i] for k in _kwargs} for i in range(batch_size)] - - outputs = async_run([fun] * batch_size, args_list=args_list, kwargs_list=kwargs_list, - max_workers=max_workers, description=description) - return outputs - - return _fun - + Args: + fun (callable): The function to run. + + max_workers (int, optional): Maximum number of worker threads to use. + If None, the default ThreadPoolExecutor behavior is used. + description (str, optional): Description to display in the progress bar. + + Returns: + callable: A new function that processes batches of inputs. + """ + def _fun(*args, **kwargs): + + # We try to infer the batch size from the args + all_args = args + tuple(kwargs.values()) + # find all list or array-like arguments and use their length as batch size + batch_size = max(len(arg) for arg in all_args if hasattr(arg, '__len__')) + + # broadcast the batch size to all args and record the indices that are broadcasted + args = [arg if hasattr(arg, '__len__') else [arg] * batch_size for arg in args] + kwargs = {k: v if hasattr(v, '__len__') else [v] * batch_size for k, v in kwargs.items()} + + # assert that all args and kwargs have the same length + lengths = [len(arg) for arg in args] + [len(v) for v in kwargs.values()] + if len(set(lengths)) != 1: + raise ValueError("All arguments and keyword arguments must have the same length.") + + # deepcopy if it is a trace.Module (as they may have mutable state) + # Module.copy() is used to create a new instance with the same parameters + _args = [arg.copy() if isinstance(arg, (Module, AutoGuide)) else arg for arg in args] + _kwargs = {k: v.copy() if isinstance(v, (Module, AutoGuide)) else v for k, v in kwargs.items()} + + # Run the forward function in parallel using asyncio with the same parameters. + # Since trace.Node is treated as immutable, we can safely use the same instance. + # The resultant graph will be the same as if we had called the function with the original arguments. + + # convert _args and _kwargs (args, kwargs of list) to lists of args and kwargs + + args_list = [tuple(aa[i] for aa in _args) for i in range(batch_size)] + kwargs_list = [{k: _kwargs[k][i] for k in _kwargs} for i in range(batch_size)] + + outputs = async_run([fun] * batch_size, args_list=args_list, kwargs_list=kwargs_list, + max_workers=max_workers, description=description) + return outputs + + return _fun + + return decorator if __name__ == "__main__": diff --git a/tests/unit_tests/test_batch_run.py b/tests/unit_tests/test_batch_run.py index ffee737b..714caac3 100644 --- a/tests/unit_tests/test_batch_run.py +++ b/tests/unit_tests/test_batch_run.py @@ -4,6 +4,7 @@ def test_batch_run_fun(): + @batch_run(max_workers=3) def fun(x, y): return x + y @@ -12,10 +13,11 @@ def fun(x, y): y = 10 # this will be broadcasted to each element in x # Run the function in batch mode - outputs = batch_run(fun, max_workers=3)(x,y) + outputs = fun(x,y) assert outputs == [11, 12, 13, 14, 15], f"Expected [11, 12, 13, 14, 15], got {outputs}" # Handling a function taking a list as inputs + @batch_run(max_workers=3) def fun(x: List[int], y: List[int]) -> List[int]: return [a + b for a, b in zip(x, y)] @@ -24,7 +26,7 @@ def fun(x: List[int], y: List[int]) -> List[int]: raise_error = False try: - outputs = batch_run(fun, max_workers=3)(x, y) + outputs = fun(x, y) except ValueError as e: assert str(e) == "All arguments and keyword arguments must have the same length.", f"Unexpected error: {e}" raise_error = True @@ -32,14 +34,14 @@ def fun(x: List[int], y: List[int]) -> List[int]: # Now we can broadcast y to match the length of x y = [[10, 20, 30]] * len(x) # Broadcast - outputs = batch_run(fun, max_workers=3)(x, y) + outputs = fun(x, y) assert outputs == [[11, 22, 33], [14, 25, 36]], f"Expected [[11, 22, 33], [14, 25, 36]], got {outputs}" y = [10, 20] # This will raise an error because x and y have different lengths raise_error = False try: - outputs = batch_run(fun, max_workers=3)(x, y) + outputs = fun(x, y) except TypeError as e: raise_error = True assert raise_error, "Expected a TypeError but did not get one." @@ -60,7 +62,7 @@ def forward(self, x): module = MyModule(10) x = [1, 2, 3, 4, 5] - outputs = batch_run(module.forward, max_workers=3)(x) + outputs = batch_run(max_workers=3)(module.forward)(x) assert outputs == [11, 12, 13, 14, 15], f"Expected [11, 12, 13, 14, 15], got {outputs}" param = module.parameters()[0] assert len(param.children) == 5 @@ -71,7 +73,7 @@ def forward(self, x): # This should raise an error because x and y have different lengths raise_error = False try: - outputs = batch_run(module.forward, max_workers=3)(x, y) + outputs = batch_run(max_workers=3)(module.forward)(x, y) except ValueError as e: assert str(e) == "All arguments and keyword arguments must have the same length.", f"Unexpected error: {e}" raise_error = True From 0b5a1cca8f24ef0debd41c337c8a5ca927bd0b79 Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 30 Jun 2025 23:29:41 +0000 Subject: [PATCH 072/314] Fix the typo bug in gsm8k example --- examples/gsm8k_trainer_example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/gsm8k_trainer_example.py b/examples/gsm8k_trainer_example.py index f9524dc0..7b627674 100644 --- a/examples/gsm8k_trainer_example.py +++ b/examples/gsm8k_trainer_example.py @@ -71,7 +71,7 @@ def main(): test_dataset = train_dataset agent = Learner(llm=LLM(student_model)) - guide = Guide(model=LLM(teacher_model)) + guide = Guide(llm=LLM(teacher_model)) optimizer = OptoPrime(agent.parameters(), llm=LLM(optimizer_model)) logger = Logger(verbose=verbose) # set use_json_object_format=False if LLM does not support JSON object format From 0cc865c22490a8bacf43bc2a28459e3884c292e2 Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 1 Jul 2025 00:03:13 +0000 Subject: [PATCH 073/314] Update evaluate to use batch_run. --- opto/trainer/evaluators.py | 29 ++++++++--------- tests/unit_tests/test_batch_run.py | 52 ++++++++++++++++++++++++------ 2 files changed, 55 insertions(+), 26 deletions(-) diff --git a/opto/trainer/evaluators.py b/opto/trainer/evaluators.py index db9e35ec..309189b5 100644 --- a/opto/trainer/evaluators.py +++ b/opto/trainer/evaluators.py @@ -1,4 +1,5 @@ -from opto.trainer.utils import async_run +from opto.trainer.utils import async_run, batch_run +from opto.trace import ExecutionError import copy @@ -15,28 +16,24 @@ def evaluate(agent, guide, inputs, infos, min_score=None, num_samples=1, num_thr num_threads: Maximum number of threads to use for parallel evaluation description: Description to display in the progress bar """ + assert len(inputs) == len(infos), "Inputs and infos must have the same length" + N = len(inputs) + # Use provided description or generate a default one + eval_description = description or f"Evaluating {N} examples" - def evaluate_single(agent, guide, i): + @batch_run(max_workers=num_threads, description=eval_description) + def _evaluate(agent, guide, i): try: output = agent(inputs[i]).data score = guide.metric(inputs[i], output, infos[i]) - except: + except ExecutionError as e: score = min_score return score - N = len(inputs) - assert len(inputs) == len(infos), "Inputs and infos must have the same length" - # Use asyncio if num_threads is not None and > 1 - use_asyncio = num_threads is not None and num_threads > 1 - # repeat each index num_samples times indices = [i for i in range(N) for _ in range(num_samples)] - if use_asyncio: - # Use provided description or generate a default one - eval_description = description or f"Evaluating {N} examples" - scores = async_run([evaluate_single] * N, [(copy.deepcopy(agent), copy.deepcopy(guide), i) for i in indices], - max_workers=num_threads, - description=eval_description) # list of tuples - else: - scores = [evaluate_single(agent, guide, i) for i in indices] + + # Run the evaluation in parallel + scores = _evaluate(agent, guide, indices) + return scores \ No newline at end of file diff --git a/tests/unit_tests/test_batch_run.py b/tests/unit_tests/test_batch_run.py index 714caac3..74e3c149 100644 --- a/tests/unit_tests/test_batch_run.py +++ b/tests/unit_tests/test_batch_run.py @@ -37,15 +37,10 @@ def fun(x: List[int], y: List[int]) -> List[int]: outputs = fun(x, y) assert outputs == [[11, 22, 33], [14, 25, 36]], f"Expected [[11, 22, 33], [14, 25, 36]], got {outputs}" - - y = [10, 20] # This will raise an error because x and y have different lengths - raise_error = False - try: - outputs = fun(x, y) - except TypeError as e: - raise_error = True - assert raise_error, "Expected a TypeError but did not get one." - + # This will raise an error because x and y have different lengths + # y = [10, 20] + # outputs = fun(x, y) + def test_batch_run_module(): @@ -77,4 +72,41 @@ def forward(self, x): except ValueError as e: assert str(e) == "All arguments and keyword arguments must have the same length.", f"Unexpected error: {e}" raise_error = True - assert raise_error, "Expected a ValueError but did not get one." \ No newline at end of file + assert raise_error, "Expected a ValueError but did not get one." + + +def test_evaluate(): + # This test the evaluate function in opto.trainer.evaluators built on top of batch_run + from opto.trainer.evaluators import evaluate + from opto.trainer.guide import AutoGuide + from opto import trace + + @trace.model + class MyAgent: + def __init__(self, param): + self.param = trace.node(param, trainable=True) + + def forward(self, x): + y = x + self.param + self.param += 1 # This should not affect the batch run + return y + + class MyGuide(AutoGuide): + def __init__(self, param): + super().__init__() + self.param = param + + def get_feedback(self, query, response, reference=None): + score = float(response == query + self.param + reference) + feedback = f"Score: {score}, Response: {response}, Query: {query}" + print(score, feedback) + self.param += 1 # This should not affect the batch run + return score, feedback + + agent = MyAgent(10) + guide = MyGuide(10) + inputs = [1, 2, 3, 4, 5] + infos = [0, 1, 2, 3, 4] # These are the expected outputs (query + param + info) + evaluated_scores = evaluate(agent, guide, inputs, infos, num_samples=1, num_threads=1) + expected_scores = [1, 0, 0, 0, 0] # All inputs should match the expected outputs + assert evaluated_scores == expected_scores, f"Expected {expected_scores}, got {evaluated_scores}" \ No newline at end of file From c82a131e31085799e6e69c62362634225f9012c9 Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 1 Jul 2025 18:26:02 +0000 Subject: [PATCH 074/314] Update algo to use batch_run. Update evaluate to return 2d array when num_samples >1. --- opto/trainer/algorithms/__init__.py | 2 ++ opto/trainer/algorithms/algorithm.py | 4 --- opto/trainer/algorithms/basic_algorithms.py | 34 +++++++------------ .../algorithms/beamsearch_algorithm.py | 34 ++++++------------- opto/trainer/evaluators.py | 9 +++-- opto/trainer/utils.py | 1 + tests/unit_tests/test_batch_run.py | 8 +++-- 7 files changed, 40 insertions(+), 52 deletions(-) diff --git a/opto/trainer/algorithms/__init__.py b/opto/trainer/algorithms/__init__.py index ea5dde63..2586fd31 100644 --- a/opto/trainer/algorithms/__init__.py +++ b/opto/trainer/algorithms/__init__.py @@ -1 +1,3 @@ from opto.trainer.algorithms.basic_algorithms import Minibatch, MinibatchAlgorithm, BasicSearchAlgorithm +from opto.trainer.algorithms.beamsearch_algorithm import BeamsearchAlgorithm, BeamsearchHistoryAlgorithm +from opto.trainer.algorithms.UCBsearch import UCBSearchAlgorithm diff --git a/opto/trainer/algorithms/algorithm.py b/opto/trainer/algorithms/algorithm.py index e08eec44..7995fc0b 100644 --- a/opto/trainer/algorithms/algorithm.py +++ b/opto/trainer/algorithms/algorithm.py @@ -1,9 +1,5 @@ -import warnings from typing import Optional - -from opto import trace from opto.trace.modules import Module -from opto.trainer.utils import async_run from opto.trainer.loggers import DefaultLogger import os diff --git a/opto/trainer/algorithms/basic_algorithms.py b/opto/trainer/algorithms/basic_algorithms.py index ae27d955..8ec0eb4f 100644 --- a/opto/trainer/algorithms/basic_algorithms.py +++ b/opto/trainer/algorithms/basic_algorithms.py @@ -4,7 +4,7 @@ from opto import trace from opto.trainer.algorithms.algorithm import AlgorithmBase from opto.trainer.loader import DataLoader -from opto.trainer.utils import async_run +from opto.trainer.utils import batch_run, async_run from opto.optimizers.utils import print_color from opto.trainer.evaluators import evaluate @@ -78,7 +78,6 @@ def train(self, log_frequency = log_frequency or eval_frequency # frequency of logging (default to eval_frequency) num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads test_dataset = test_dataset or train_dataset # default to train_dataset if test_dataset is not provided - use_asyncio = self._use_asyncio(num_threads) self.num_eval_samples = num_eval_samples # number of samples to use to evaluate each input # Evaluate the agent before learning @@ -104,13 +103,8 @@ def train(self, backup_dict = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} # Forward the agent on the inputs and compute the feedback using the guide - if use_asyncio: # Run forward asynchronously - outputs = async_run([self.forward]*len(xs), - [(self.agent, x, guide, info) for x, info in zip(xs, infos)], - max_workers=num_threads, - description=f"Forward pass (batch size: {len(xs)})") # async forward - else: # Run forward sequentially - outputs = [self.forward(self.agent, x, guide, info) for x, info in zip(xs, infos) ] + forward = batch_run(max_workers=num_threads, description=f"Forward pass (batch size: {len(xs)})")(self.forward) + outputs = forward(self.agent, xs, guide, infos) # Update the agent score = self.update(outputs, verbose=verbose, num_threads=num_threads, **kwargs) @@ -148,14 +142,14 @@ def train(self, return train_scores, test_score - def evaluate(self, agent, guide, xs, infos, min_score=None, num_threads=None, description=None): + def evaluate(self, agent, guide, xs, infos, min_score=None, num_samples=1, num_threads=None, description=None): """ Evaluate the agent on the given dataset. """ num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads test_scores = evaluate(agent, guide, xs, infos, min_score=min_score, num_threads=num_threads, - description=description, num_samples=self.num_eval_samples) + num_samples=num_samples, description=description, num_samples=self.num_eval_samples) if all([s is not None for s in test_scores]): return np.mean(test_scores) - + def has_improvement(self, xs, guide, infos, current_score, current_outputs, backup_dict, threshold=0, num_threads=None, *args, **kwargs): # This function can be overridden by subclasses to implement their own improvement check. """ Check if the updated agent is improved compared to the current one. @@ -311,15 +305,13 @@ def validate(): # Generate different proposals step_kwargs = dict(bypassing=True, verbose='output' if verbose else False) # we don't print the inner full message step_kwargs.update(kwargs) # update with additional kwargs if provided - use_asyncio = self._use_asyncio() - if use_asyncio: - update_dicts = async_run([super().optimizer_step]*self.num_proposals, - kwargs_list=[step_kwargs] * self.num_proposals, - max_workers=num_threads, - description=f"Generating {self.num_proposals} proposals") # async step - else: - update_dicts = [self.optimizer.step(**step_kwargs) for _ in range(self.num_proposals)] - + + # Use aysnc_run to run the optimizer_step in parallel + # NOTE optimizer_step is coupled via async_run + update_dicts = async_run([super().optimizer_step]*self.num_proposals, + kwargs_list=[step_kwargs] * self.num_proposals, + max_workers=num_threads, + description=f"Generating {self.num_proposals} proposals") # async step # Validate the proposals candidates = [] backup_dict = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} # backup the current value diff --git a/opto/trainer/algorithms/beamsearch_algorithm.py b/opto/trainer/algorithms/beamsearch_algorithm.py index 0451455e..0beac524 100644 --- a/opto/trainer/algorithms/beamsearch_algorithm.py +++ b/opto/trainer/algorithms/beamsearch_algorithm.py @@ -1,7 +1,7 @@ import numpy as np import copy from typing import Union, List, Tuple, Dict, Any, Optional -from opto.trainer.utils import async_run +from opto.trainer.utils import async_run, batch_run from opto.optimizers.utils import print_color from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm, evaluate, batchify @@ -329,15 +329,9 @@ def expand(self, xs_batch, infos_batch = self._sample_minibatch(train_dataset, batch_size) # Forward the agent on the minibatch - use_asyncio = self._use_asyncio(num_threads) - if use_asyncio: - outputs = async_run([self.forward]*len(xs_batch), - [(self.agent, x, guide, info) for x, info in zip(xs_batch, infos_batch)], - max_workers=num_threads, - description=f"Forward pass (beam {beam_idx+1}, batch size: {len(xs_batch)})") - else: - outputs = [self.forward(self.agent, x, guide, info) for x, info in zip(xs_batch, infos_batch)] - + forward = batch_run(max_workers=num_threads, description=f"Forward pass (batch size: {len(xs_batch)})")(self.forward) + outputs = forward(self.agent, xs_batch, guide, infos_batch) + # Prepare for optimizer backward and step scores, targets, feedbacks = [], [], [] for target, score, feedback in outputs: @@ -356,13 +350,10 @@ def expand(self, candidates = [] # Generate num_proposals candidates - if use_asyncio: - update_dicts = async_run([self.optimizer.step]*num_proposals, - kwargs_list=[step_kwargs] * num_proposals, - max_workers=num_threads, - description=f"Generating {num_proposals} proposals for beam {beam_idx+1}") - else: - update_dicts = [self.optimizer.step(**step_kwargs) for _ in range(num_proposals)] + update_dicts = async_run([self.optimizer.step]*num_proposals, + kwargs_list=[step_kwargs] * num_proposals, + max_workers=num_threads, + description=f"Generating {num_proposals} proposals for beam {beam_idx+1}") # Collect all valid proposals for update_dict in update_dicts: @@ -721,12 +712,9 @@ def expand(self, use_asyncio = self._use_asyncio(num_threads) description=f"Forward pass (beam {beam_idx+1}, batch size: {len(xs_batch)})" - if use_asyncio: - outputs = async_run([self.forward]*len(xs_batch), - [(self.agent, x, guide, info) for x, info in zip(xs_batch, infos_batch)], - max_workers=num_threads, description=description) - else: - outputs = [self.forward(self.agent, x, guide, info) for x, info in zip(xs_batch, infos_batch)] + + forward = batch_run(max_workers=num_threads, description=description)(self.forward) + outputs = forward(self.agent, xs_batch, guide, infos_batch) # Prepare original feedback scores, targets, feedbacks = [], [], [] diff --git a/opto/trainer/evaluators.py b/opto/trainer/evaluators.py index 309189b5..d1e99c8e 100644 --- a/opto/trainer/evaluators.py +++ b/opto/trainer/evaluators.py @@ -1,6 +1,7 @@ -from opto.trainer.utils import async_run, batch_run +from opto.trainer.utils import batch_run from opto.trace import ExecutionError import copy +import numpy as np def evaluate(agent, guide, inputs, infos, min_score=None, num_samples=1, num_threads=None, description=None): @@ -35,5 +36,9 @@ def _evaluate(agent, guide, i): # Run the evaluation in parallel scores = _evaluate(agent, guide, indices) - + scores = np.array(scores) + if num_samples > 1: + # scores will be of length N * num_samples + # Reshape scores into an array of shape (N, num_samples) + scores = scores.reshape(N, num_samples) return scores \ No newline at end of file diff --git a/opto/trainer/utils.py b/opto/trainer/utils.py index 93436505..16067b78 100644 --- a/opto/trainer/utils.py +++ b/opto/trainer/utils.py @@ -34,6 +34,7 @@ def async_run(runs, args_list = None, kwargs_list = None, max_workers = None, de kwargs_list = [{}] * len(runs) if (max_workers == 1) and allow_sequential_run: # run without asyncio + print(f"{description} (Running sequentially).") return [run(*args, **kwargs) for run, args, kwargs in zip(runs, args_list, kwargs_list)] else: async def _run(): diff --git a/tests/unit_tests/test_batch_run.py b/tests/unit_tests/test_batch_run.py index 74e3c149..5da10ddb 100644 --- a/tests/unit_tests/test_batch_run.py +++ b/tests/unit_tests/test_batch_run.py @@ -99,7 +99,6 @@ def __init__(self, param): def get_feedback(self, query, response, reference=None): score = float(response == query + self.param + reference) feedback = f"Score: {score}, Response: {response}, Query: {query}" - print(score, feedback) self.param += 1 # This should not affect the batch run return score, feedback @@ -109,4 +108,9 @@ def get_feedback(self, query, response, reference=None): infos = [0, 1, 2, 3, 4] # These are the expected outputs (query + param + info) evaluated_scores = evaluate(agent, guide, inputs, infos, num_samples=1, num_threads=1) expected_scores = [1, 0, 0, 0, 0] # All inputs should match the expected outputs - assert evaluated_scores == expected_scores, f"Expected {expected_scores}, got {evaluated_scores}" \ No newline at end of file + assert (evaluated_scores == expected_scores).all(), f"Expected {expected_scores}, got {evaluated_scores}" + + + evaluated_scores = evaluate(agent, guide, inputs, infos, num_samples=2, num_threads=1) + expected_scores = [[1, 1], [0, 0], [0, 0], [0, 0], [0, 0]] # Each input should match the expected outputs twice + assert (evaluated_scores == expected_scores).all(), f"Expected {expected_scores}, got {evaluated_scores.tolist()}" \ No newline at end of file From c2217d86b98936d8fd8bd0827796cf493d6b3479 Mon Sep 17 00:00:00 2001 From: Ching-An Cheng Date: Tue, 1 Jul 2025 14:27:22 -0700 Subject: [PATCH 075/314] Update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e5e94e9a..724d6014 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,9 @@ git is unable to clone the repository. ## Updates -- **2025.5.9** Adith Swaminathan gave a talk at Netflix Workshop on Personalization, Recommendation and Search (PRS)[https://prs2025.splashthat.com/] -- **2025.5.1** Ching-An Cheng gave a talk at 2nd Texas Colloquium on Distributed Learning (TL;DR)[https://sites.google.com/view/tldr-2025] +- **2025.5.28** Datarobot released Efficient Search for Pareto-optimal Flows [syftr](https://github.com/datarobot/syftr) powered by Trace. +- **2025.5.9** Adith Swaminathan gave a talk at [Netflix Workshop on Personalization, Recommendation and Search (PRS)](https://prs2025.splashthat.com/) +- **2025.5.1** Ching-An Cheng gave a talk at [2nd Texas Colloquium on Distributed Learning (TL;DR)](https://sites.google.com/view/tldr-2025) - **2025.2.7** Trace was featured in the [G-Research NeurIPS highlight](https://www.gresearch.com/news/neurips-paper-reviews-2024-8/) by the Science Director Hugh Salimbeni. - **2024.12.10** Trace was demoed in person at NeurIPS 2024 Expo. - **2024.11.05** Ching-An Cheng gave a talk at UW Robotics Colloquium on Trace: [video](https://www.youtube.com/watch?v=T2g1Vo3u_9g). From 715c2ee6e0a313dcbb966765589d1c9b470a5c84 Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 1 Jul 2025 22:18:16 +0000 Subject: [PATCH 076/314] Add flags to set json keys in OptoPrime. Add DummyLLM for testing. Merge changes in OptoPrimeV2 as flag in OptoPrime. --- opto/optimizers/optoprime.py | 81 ++++++++++++++++++++--- opto/utils/llm.py | 30 +++++++++ tests/unit_tests/test_optoprime_update.py | 54 +++++++++++++++ 3 files changed, 154 insertions(+), 11 deletions(-) create mode 100644 tests/unit_tests/test_optoprime_update.py diff --git a/opto/optimizers/optoprime.py b/opto/optimizers/optoprime.py index 5a5c5c36..730892bf 100644 --- a/opto/optimizers/optoprime.py +++ b/opto/optimizers/optoprime.py @@ -168,29 +168,50 @@ class OptoPrime(Optimizer): # Optimization default_objective = "You need to change the of the variables in #Variables to improve the output in accordance to #Feedback." - output_format_prompt = dedent( + output_format_prompt_original = dedent( """ Output_format: Your output should be in the following json format, satisfying the json syntax: {{ - "reasoning": , - "answer": , - "suggestion": {{ + "{reasoning}": , + "{answer}": , + "{suggestion}": {{ : , : , }} }} - In "reasoning", explain the problem: 1. what the #Instruction means 2. what the #Feedback on #Output means to #Variables considering how #Variables are used in #Code and other values in #Documentation, #Inputs, #Others. 3. Reasoning about the suggested changes in #Variables (if needed) and the expected result. + In "{reasoning}", explain the problem: 1. what the #Instruction means 2. what the #Feedback on #Output means to #Variables considering how #Variables are used in #Code and other values in #Documentation, #Inputs, #Others. 3. Reasoning about the suggested changes in #Variables (if needed) and the expected result. - If #Instruction asks for an answer, write it down in "answer". + If #Instruction asks for an answer, write it down in "{answer}". - If you need to suggest a change in the values of #Variables, write down the suggested values in "suggestion". Remember you can change only the values in #Variables, not others. When of a variable is (code), you should write the new definition in the format of python code without syntax errors, and you should not change the function name or the function signature. + If you need to suggest a change in the values of #Variables, write down the suggested values in "{suggestion}". Remember you can change only the values in #Variables, not others. When of a variable is (code), you should write the new definition in the format of python code without syntax errors, and you should not change the function name or the function signature. If no changes or answer are needed, just output TERMINATE. """ ) + output_format_prompt_no_answer = dedent( + """ + Output_format: Your output should be in the following json format, satisfying the json syntax: + + {{ + "{reasoning}": , + "{suggestion}": {{ + : , + : , + }} + }} + + In "{reasoning}", explain the problem: 1. what the #Instruction means 2. what the #Feedback on #Output means to #Variables considering how #Variables are used in #Code and other values in #Documentation, #Inputs, #Others. 3. Reasoning about the suggested changes in #Variables (if needed) and the expected result. + + If you need to suggest a change in the values of #Variables, write down the suggested values in "{suggestion}". Remember you can change only the values in #Variables, not others. When of a variable is (code), you should write the new definition in the format of python code without syntax errors, and you should not change the function name or the function signature. + + If no changes are needed, just output TERMINATE. + """ + ) + + example_problem_template = dedent( """ Here is an example of problem instance and response: @@ -234,6 +255,14 @@ class OptoPrime(Optimizer): """ ) + final_prompt_with_variables = dedent( + """ + What are your suggestions on variables {names}? + + Your response: + """ + ) + default_prompt_symbols = { "variables": "#Variables", "constraints": "#Constraints", @@ -246,6 +275,12 @@ class OptoPrime(Optimizer): "documentation": "#Documentation", } + default_json_keys = { + "reasoning": "reasoning", + "answer": "answer", + "suggestion": "suggestion", + } + def __init__( self, parameters: List[ParameterNode], @@ -259,7 +294,9 @@ def __init__( max_tokens=4096, log=True, prompt_symbols=None, + json_keys=None, # keys to use in the json object format (can remove "answer" if not needed) use_json_object_format=True, # whether to use json object format for the response when calling LLM + highlight_variables=False, # whether to highlight the variables at the end in the prompt **kwargs, ): super().__init__(parameters, *args, propagator=propagator, **kwargs) @@ -295,7 +332,17 @@ def __init__( self.prompt_symbols = copy.deepcopy(self.default_prompt_symbols) if prompt_symbols is not None: self.prompt_symbols.update(prompt_symbols) + if json_keys is not None: + self.default_json_keys.update(json_keys) + if self.default_json_keys['answer'] is None: # answer field is not needed + del self.default_json_keys['answer'] + if 'answer' not in self.default_json_keys: + # If 'answer' is not in the json keys, we use the no-answer format + self.output_format_prompt = self.output_format_prompt_no_answer.format(**self.default_json_keys) + else: # If 'answer' is in the json keys, we use the original format of OptoPrime + self.output_format_prompt = self.output_format_prompt_original.format(**self.default_json_keys) self.use_json_object_format = use_json_object_format + self.highlight_variables = highlight_variables def default_propagator(self): """Return the default Propagator object of the optimizer.""" @@ -403,7 +450,17 @@ def construct_prompt(self, summary, mask=None, *args, **kwargs): ) + user_prompt ) - user_prompt += self.final_prompt + + + if self.highlight_variables: + var_names = [] + for k, v in summary.variables.items(): + var_names.append(f"{k}") # ({type(v[0]).__name__}) + var_names = ", ".join(var_names) + + user_prompt += self.final_prompt_with_variables.format(names=var_names) + else: # This is the original OptoPrime prompt + user_prompt += self.final_prompt # Add examples if len(self.memory) > 0: @@ -494,11 +551,13 @@ def construct_update_dict( def extract_llm_suggestion(self, response: str): """Extract the suggestion from the response.""" + suggestion_tag = self.default_json_keys["suggestion"] + suggestion = {} attempt_n = 0 while attempt_n < 2: try: - suggestion = json.loads(response)["suggestion"] + suggestion = json.loads(response)[suggestion_tag] break except json.JSONDecodeError: # Remove things outside the brackets @@ -514,7 +573,7 @@ def extract_llm_suggestion(self, response: str): if len(suggestion) == 0: # we try to extract key/value separately and return it as a dictionary - pattern = r'"suggestion"\s*:\s*\{(.*?)\}' + pattern = rf'"{suggestion_tag}"\s*:\s*\{{(.*?)\}}' suggestion_match = re.search(pattern, str(response), re.DOTALL) if suggestion_match: suggestion = {} @@ -530,7 +589,7 @@ def extract_llm_suggestion(self, response: str): if len(suggestion) == 0: if not self.ignore_extraction_error: - print("Cannot extract suggestion from LLM's response:") + print(f"Cannot extract {self.default_json_keys['suggestion']} from LLM's response:") print(response) # if the suggested value is a code, and the entire code body is empty (i.e., not even function signature is present) diff --git a/opto/utils/llm.py b/opto/utils/llm.py index 3ae2f2c5..320ba2b2 100644 --- a/opto/utils/llm.py +++ b/opto/utils/llm.py @@ -313,6 +313,36 @@ def get_profile_info(cls, profile: str = None): return cls._profiles.get(profile) return cls._profiles + +class DummyLLM(AbstractModel): + """A dummy LLM that does nothing. Used for testing purposes.""" + + def __init__(self, + callable, + reset_freq: Union[int, None] = None) -> None: + # self.message = message + self.callable = callable + factory = lambda: self._factory() + super().__init__(factory, reset_freq) + + def _factory(self): + + # set response.choices[0].message.content + # create a fake container with above format + + class Message: + def __init__(self, content): + self.content = content + class Choice: + def __init__(self, content): + self.message = Message(content) + class Response: + def __init__(self, content): + self.choices = [Choice(content)] + + return lambda *args, **kwargs: Response(self.callable(*args, **kwargs)) + + class LLM: """ A unified entry point for all supported LLM backends. diff --git a/tests/unit_tests/test_optoprime_update.py b/tests/unit_tests/test_optoprime_update.py new file mode 100644 index 00000000..0b273d2f --- /dev/null +++ b/tests/unit_tests/test_optoprime_update.py @@ -0,0 +1,54 @@ +from opto import trace +from opto.optimizers import OptoPrime +from opto.utils.llm import DummyLLM + + + +def test_json_keys(): + """ + Test that the OptoPrimeV2 class correctly initializes with json_keys. + """ + param = trace.node(1, trainable=True) + + def callable(messages, **kwargs): + format_prompt = """Output_format: Your output should be in the following json format, satisfying the json syntax: + +{ +"reasoning_mod": , +"suggestion_mod": { + : , + : , +} +} + +In "reasoning_mod", explain the problem: 1. what the #Instruction means 2. what the #Feedback on #Output means to #Variables considering how #Variables are used in #Code and other values in #Documentation, #Inputs, #Others. 3. Reasoning about the suggested changes in #Variables (if needed) and the expected result. + +If you need to suggest a change in the values of #Variables, write down the suggested values in "suggestion_mod". Remember you can change only the values in #Variables, not others. When of a variable is (code), you should write the new definition in the format of python code without syntax errors, and you should not change the function name or the function signature.""" + assert format_prompt in messages[0]['content'] # system + assert '"answer":' not in messages[0]['content'] + highlight_prompt = "What are your suggestions on variables int0?" + assert highlight_prompt in messages[1]['content'] # user + return "Dummy response" #messages + + llm = DummyLLM(callable) + + optimizer = OptoPrime( + parameters=[param], + llm = llm, + json_keys=dict( + reasoning="reasoning_mod", + answer=None, + suggestion="suggestion_mod"), + highlight_variables=True, + ) + + + y = param + 10 + optimizer.zero_feedback() + optimizer.backward(y, 'dummy feedback') + optimizer.step(verbose=True) + + + + + From f418adff9e5630da199b8752d0ba2327c4cdcd3a Mon Sep 17 00:00:00 2001 From: windweller Date: Tue, 1 Jul 2025 18:23:41 -0700 Subject: [PATCH 077/314] contribute an interesting test case for the module copy behavior --- tests/unit_tests/test_modules.py | 123 +++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/tests/unit_tests/test_modules.py b/tests/unit_tests/test_modules.py index f08d26e5..ae1e9267 100644 --- a/tests/unit_tests/test_modules.py +++ b/tests/unit_tests/test_modules.py @@ -400,3 +400,126 @@ def multiply(self, x, y): finally: if os.path.exists(temp_file): os.remove(temp_file) + +def test_copy_function(): + """Test the copy function of Module class.""" + + @model + class TestCopyClass: + def __init__(self): + super().__init__() + self._param = node(10, trainable=True) + self.regular_attr = "original_value" + self.list_attr = [1, 2, 3] + self.dict_attr = {"key": "value"} + + @bundle(trainable=True) + def test_method(self, x): + return x + self._param + + def forward(self, x): + return self.test_method(x) + + # Create original instance + original = TestCopyClass() + original.regular_attr = "modified_value" + original.list_attr.append(4) + original.dict_attr["new_key"] = "new_value" + + # Create a copy + copied = original.copy() + + # Test that it's a different object + assert copied is not original + + # Test that regular attributes are copied (deep copy) + assert copied.regular_attr == "modified_value" + assert copied.list_attr == [1, 2, 3, 4] + assert copied.dict_attr == {"key": "value", "new_key": "new_value"} + + # Test that parameters are references to the original parameters + assert copied._param is original._param + assert copied.test_method.parameter is original.test_method.parameter + + # Test that modifying the original parameter affects the copy + original._param._data = 20 + assert copied._param._data == 20 + + # Test that modifying the copy's parameter affects the original + copied._param._data = 30 + assert original._param._data == 30 + + # Test that the copy can still function + result = copied.forward(5) + assert result._data == 35 # 5 + 30 + + # Test that modifying regular attributes doesn't affect the original + copied.regular_attr = "copy_only_value" + assert original.regular_attr == "modified_value" + + # Test that modifying list/dict attributes doesn't affect the original (deep copy) + copied.list_attr.append(5) + assert len(original.list_attr) == 4 + assert len(copied.list_attr) == 5 + + copied.dict_attr["copy_only"] = "copy_value" + assert "copy_only" not in original.dict_attr + assert "copy_only" in copied.dict_attr + +def test_copy_function_with_nested_modules(): + """Test the copy function with nested modules.""" + + @model + class NestedModule: + def __init__(self): + super().__init__() + self._nested_param = node(5, trainable=True) + + @bundle(trainable=True) + def nested_method(self, x): + return x * self._nested_param + + def forward(self, x): + return self.nested_method(x) + + @model + class ParentModule: + def __init__(self): + super().__init__() + self._param = node(10, trainable=True) + self._nested = NestedModule() + self.regular_attr = "parent_value" + + @bundle(trainable=True) + def parent_method(self, x): + return self._nested.forward(x) + self._param + + def forward(self, x): + return self.parent_method(x) + + # Create original instance + original = ParentModule() + original.regular_attr = "modified_parent" + original._nested._nested_param._data = 7 + + # Create a copy + copied = ParentModule() + copied = original.copy() + + # Test that it's a different object + assert copied is not original + + # Test that nested module is copied but parameters are references + assert copied._nested is not original._nested # Different object + assert copied._nested._nested_param is original._nested._nested_param # Same parameter reference + + # Test that regular attributes are copied + assert copied.regular_attr == "modified_parent" + + # Test that modifying nested parameter affects both + original._nested._nested_param._data = 8 + assert copied._nested._nested_param._data == 8 + + # Test that the copy can still function + result = copied.forward(3) + assert result._data == 34 # (3 * 8) + 10 From 290e3f32ca9b47e4e3575ecafd4063b624a126e0 Mon Sep 17 00:00:00 2001 From: Xuanfei Ren Date: Wed, 2 Jul 2025 00:41:17 -0500 Subject: [PATCH 078/314] add the latest version of UCB search algorithms --- opto/trainer/algorithms/UCBsearch.py | 1513 ++++++++++++++++++++++++++ 1 file changed, 1513 insertions(+) create mode 100644 opto/trainer/algorithms/UCBsearch.py diff --git a/opto/trainer/algorithms/UCBsearch.py b/opto/trainer/algorithms/UCBsearch.py new file mode 100644 index 00000000..0f3f9bc3 --- /dev/null +++ b/opto/trainer/algorithms/UCBsearch.py @@ -0,0 +1,1513 @@ +import numpy as np +import copy +import math +from collections import deque +from typing import Union, List, Tuple, Dict, Any, Optional +from opto import trace +from opto.trainer.utils import async_run # Assuming print_color is in utils +from opto.optimizers.utils import print_color +from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm, evaluate, batchify # evaluate and batchify might be useful +import json # For LLM output parsing +import random # Added for alpha probability +from opto.utils.llm import LLM # For the selector LLM +from opto.trace.nodes import ParameterNode +import warnings +from black import format_str, FileMode + +class UCBSearchAlgorithm(MinibatchAlgorithm): + """ + UCB Search Algorithm. + + Keeps a buffer of candidates with their statistics (score sum, evaluation count). + In each iteration: + 1. Picks a candidate 'a' from the buffer with the highest UCB score. + 2. Updates the optimizer with 'a's parameters. + 3. Draws a minibatch from the training set, performs a forward/backward pass, and calls optimizer.step() to get a new candidate 'a''. + 4. Evaluates 'a'' on a validation set minibatch. + 5. Updates statistics of 'a' (based on the training minibatch). + 6. Adds 'a'' (with its validation stats) to the buffer. + 7. If the buffer is full, evicts the candidate with the lowest UCB score. + """ + + def __init__(self, + agent: trace.Module, + optimizer, + max_buffer_size: int = 10, + ucb_exploration_factor: float = 1.0, # Controls exploration vs exploitation tradeoff in UCB selection + # UCB formula: μ(a) + c * sqrt(ln(t) / n(a)), c is the exploration factor + logger=None, + num_threads: int = None, + use_validation: bool = False, + *args, + **kwargs): + super().__init__(agent, optimizer, num_threads=num_threads, logger=logger, *args, **kwargs) + + self.buffer = deque(maxlen=max_buffer_size) + self.max_buffer_size = max_buffer_size + # UCB exploration factor: Higher values encourage more exploration of less-tested candidates, + # lower values favor exploitation of well-performing candidates. + self.ucb_exploration_factor = ucb_exploration_factor + self.use_validation = use_validation # Whether to use validation set for evaluation + # To ensure optimizer_step can be called with bypassing=True if needed. + # This depends on the specific optimizer's implementation. + # For now, we assume the optimizer has a step method that can return parameters. + if not hasattr(self.optimizer, 'step'): + raise ValueError("Optimizer must have a 'step' method.") + + self._total_evaluations_tracker = 0 # Tracks total number of individual candidate evaluations used in UCB calculation for log(T) + self._candidate_id_counter = 0 + + def _sample_minibatch(self, dataset: Dict[str, List[Any]], batch_size: int) -> Tuple[List[Any], List[Any]]: + """Sample a minibatch from the dataset.""" + if not dataset or not dataset.get('inputs') or not dataset.get('infos'): + print_color("Warning: Attempted to sample from an empty or malformed dataset.", color='yellow') + return [], [] + + dataset_size = len(dataset['inputs']) + if dataset_size == 0: + print_color("Warning: Dataset is empty, cannot sample minibatch.", color='yellow') + return [], [] + + actual_batch_size = min(batch_size, dataset_size) + indices = np.random.choice(dataset_size, actual_batch_size, replace=False) + xs = [dataset['inputs'][i] for i in indices] + infos = [dataset['infos'][i] for i in indices] + return xs, infos + + def _evaluate_candidate(self, + params_to_eval_dict: Dict[str, Any], + dataset: Dict[str, List[Any]], # Changed from validate_dataset + guide, # Changed from validate_guide + evaluation_batch_size: int, # New parameter name + num_threads: Optional[int] = None + ) -> Tuple[float, int]: + """Evaluates a given set of parameters on samples from the provided dataset (now typically train_dataset).""" + if not dataset or not dataset.get('inputs') or not dataset.get('infos') or not dataset['inputs']: + print_color("Evaluation dataset is empty or invalid. Returning score -inf, count 0.", color='yellow') + return -np.inf, 0 + + original_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + self.optimizer.update(params_to_eval_dict) + + eval_xs, eval_infos = self._sample_minibatch(dataset, evaluation_batch_size) + + if not eval_xs: + print_color("Evaluation minibatch is empty. Returning score -inf, count 0.", color='yellow') + self.optimizer.update(original_params) + return -np.inf, 0 + + eval_scores = evaluate(self.agent, + guide, # Use main guide + eval_xs, + eval_infos, + min_score=self.min_score if hasattr(self, 'min_score') else None, + num_threads=num_threads or self.num_threads, + description=f"Evaluating candidate") + + self.optimizer.update(original_params) + + avg_score = np.mean(eval_scores) if eval_scores and all(s is not None for s in eval_scores) else 0 + eval_count = len(eval_xs) + + return float(avg_score), eval_count + + def _calculate_ucb(self, candidate_buffer_entry: Dict, total_tracked_evaluations: int) -> float: + """Calculates UCB score for a candidate in the buffer.""" + if candidate_buffer_entry['eval_count'] == 0: + return float('inf') # Explore unvisited states first + + mean_score = candidate_buffer_entry['score_sum'] / candidate_buffer_entry['eval_count'] + + # Add 1 to total_tracked_evaluations to prevent log(0) if it's the first evaluation overall + # and to ensure log argument is > 0. + # Add 1 to eval_count in denominator as well to ensure it's robust if eval_count is small. + if total_tracked_evaluations == 0: # Should not happen if we init with one eval + total_tracked_evaluations = 1 + + # UCB exploration term: ucb_exploration_factor scales the confidence interval + # Higher factor = more exploration, lower factor = more exploitation + exploration_term = self.ucb_exploration_factor * \ + math.sqrt(math.log(total_tracked_evaluations) / candidate_buffer_entry['eval_count']) + + return mean_score + exploration_term + + def _calculate_lcb(self, candidate_buffer_entry: Dict, total_tracked_evaluations: int) -> float: + """Calculates Lower Confidence Bound for a candidate in the buffer.""" + if candidate_buffer_entry['eval_count'] == 0: + return float('-inf') # Unvisited states get lowest bound + + mean_score = candidate_buffer_entry['score_sum'] / candidate_buffer_entry['eval_count'] + + # Add 1 to total_tracked_evaluations to prevent log(0) if it's the first evaluation overall + # and to ensure log argument is > 0. + # Add 1 to eval_count in denominator as well to ensure it's robust if eval_count is small. + if total_tracked_evaluations == 0: # Should not happen if we init with one eval + total_tracked_evaluations = 1 + + # LCB exploration term: ucb_exploration_factor scales the confidence interval + # Higher factor = more exploration, lower factor = more exploitation + exploration_term = self.ucb_exploration_factor * \ + math.sqrt(math.log(total_tracked_evaluations) / candidate_buffer_entry['eval_count']) + + return mean_score - exploration_term + + def _update_buffer_ucb_scores(self): + """Recalculates and updates UCB scores for all candidates in the buffer.""" + if not self.buffer: + return + + for candidate_entry in self.buffer: + candidate_entry['ucb_score'] = self._calculate_ucb(candidate_entry, self._total_evaluations_tracker) + + def _get_best_candidate_from_buffer(self, buffer): + """Get the best candidate from buffer, excluding those with eval_count = 0 when not using validation.""" + if not buffer: + return None + + # Filter out candidates with eval_count = 0 if not using validation + if not self.use_validation: + valid_candidates = [c for c in buffer if c['eval_count'] > 0] + if not valid_candidates: + # If no candidates have been evaluated, return the one with highest UCB score + return max(buffer, key=lambda c: c.get('ucb_score', -float('inf'))) + return max(valid_candidates, key=lambda c: c['score_sum'] / c['eval_count']) + else: + # When using validation, all candidates should have eval_count > 0 + return max(buffer, key=lambda c: c['score_sum'] / (c['eval_count'] or 1E-9)) + + def print_intervals(self, buffer): + """Print confidence intervals for debugging in the form of open intervals (LCB, UCB)""" + print_color("Confidence intervals for all candidates:", 'cyan') + for i, candidate_entry in enumerate(buffer): + lcb = self._calculate_lcb(candidate_entry, self._total_evaluations_tracker) + ucb = candidate_entry['ucb_score'] + mean_score = candidate_entry['score_sum'] / (candidate_entry['eval_count'] or 1) + eval_count = candidate_entry['eval_count'] + + # Format as open interval (LCB, UCB) with mean score and evaluation count + interval_str = f"Action {i+1}: ({lcb:.4f}, {ucb:.4f}) [mean: {mean_score:.4f}, n: {eval_count}]" + print_color(interval_str, 'cyan') + + def _process_single_candidate(self, + action_candidate_a: Dict, + guide, + train_dataset: Dict[str, List[Any]], + validation_dataset: Dict[str, List[Any]], + train_batch_size: int, + evaluation_batch_size: int, + num_threads: Optional[int], + iteration: int) -> Tuple[bool, float, float, int]: + """ + Process a single candidate: generate a_prime, evaluate both a and a_prime, + update stats for 'a', and add 'a_prime' to buffer. + + Returns: + Tuple of (success, a_prime_score, score_for_a_on_train_batch, samples_used) + """ + # 2. Load parameters of 'a' into the agent for the optimizer update step + self.optimizer.update(action_candidate_a['params']) + + # 3. Draw minibatch from the training set, do update from 'a' to get 'a_prime' + train_xs, train_infos = self._sample_minibatch(train_dataset, train_batch_size) + if not train_xs: + print_color(f"Iter {iteration}: Training minibatch empty for candidate, skipping.", 'yellow') + return False, -np.inf, -np.inf, 0 + + # Perform forward pass and get feedback for agent parameters 'a' + use_asyncio = self._use_asyncio(num_threads) + if use_asyncio: + outputs_for_a = async_run([self.forward]*len(train_xs), + [(self.agent, x, guide, info) for x, info in zip(train_xs, train_infos)], + max_workers=num_threads, + description=f"Iter {iteration}: Forward pass for action 'a'") + else: + outputs_for_a = [self.forward(self.agent, x, guide, info) for x, info in zip(train_xs, train_infos)] + + scores_from_train, targets_from_train, feedbacks_from_train = [], [], [] + for target, score, feedback in outputs_for_a: + scores_from_train.append(score) + targets_from_train.append(target) + feedbacks_from_train.append(feedback) + + if not scores_from_train: + print_color(f"Iter {iteration}: No outputs from forward pass for candidate. Skipping.", 'yellow') + return False, -np.inf, -np.inf, 0 + + target_for_a = batchify(*targets_from_train) + feedback_for_a = batchify(*feedbacks_from_train).data + score_for_a_on_train_batch = np.mean([s for s in scores_from_train if s is not None]) if any(s is not None for s in scores_from_train) else -np.inf + + self.optimizer.zero_feedback() + self.optimizer.backward(target_for_a, feedback_for_a) + + try: + a_prime_params_dict = self.optimizer.step(bypassing=True, verbose=False) + if not isinstance(a_prime_params_dict, dict) or not a_prime_params_dict: + print_color(f"Iter {iteration}: Optimizer.step did not return valid params. Using current agent params.", 'yellow') + a_prime_params_dict = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + self.total_proposals += 1 + except Exception as e: + print_color(f"Iter {iteration}: Error during optimizer.step: {e}. Skipping.", 'red') + return False, -np.inf, -np.inf, 0 + + # 4. Evaluate 'a' and 'a_prime' on samples of validation set in parallel + if self.use_validation: + if use_asyncio: + evaluation_results = async_run( + [self._evaluate_candidate, self._evaluate_candidate], + [ + (action_candidate_a['params'], validation_dataset, guide, evaluation_batch_size, num_threads), + (a_prime_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads) + ], + max_workers=2, + description=f"Iter {iteration}: Parallel evaluation of 'a' and 'a_prime'" + ) + (a_score, a_evals), (a_prime_score, a_prime_evals) = evaluation_results + else: + a_score, a_evals = self._evaluate_candidate( + action_candidate_a['params'], validation_dataset, guide, evaluation_batch_size, num_threads + ) + a_prime_score, a_prime_evals = self._evaluate_candidate( + a_prime_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads + ) + + # 5. Update statistics for the original candidate 'a' + # Always update statistics for the original candidate 'a' on the training set + if score_for_a_on_train_batch > -np.inf: + action_candidate_a['score_sum'] += score_for_a_on_train_batch * len(train_xs) + action_candidate_a['eval_count'] += len(train_xs) + self._total_evaluations_tracker += len(train_xs) + + # If we use validation set for evaluation + if self.use_validation: # If we use validation set for evaluation + action_candidate_a['score_sum'] += a_score * a_evals + action_candidate_a['eval_count'] += a_evals + + # 6. Add 'a_prime' to the buffer (with eviction logic if needed) + if a_prime_score > -np.inf and a_prime_evals > 0: + new_candidate_entry = { + 'params': a_prime_params_dict, + 'score_sum': a_prime_score * a_prime_evals, + 'eval_count': a_prime_evals, + 'ucb_score': None, # Will be updated later + 'iteration_created': iteration + } + + # Eviction logic before adding if buffer is at max capacity + if len(self.buffer) >= self.max_buffer_size: + self._update_buffer_ucb_scores() # Ensure UCBs are current before eviction + candidate_to_evict = min(self.buffer, key=lambda c: c['ucb_score']) + self.buffer.remove(candidate_to_evict) + print_color(f"Iter {iteration}: Buffer full. Evicted candidate (UCB: {candidate_to_evict['ucb_score']:.4f})", 'magenta') + + self.buffer.append(new_candidate_entry) + print_color(f"Iter {iteration}: Added new candidate to buffer (score: {a_prime_score:.4f})", 'magenta') + else: + print_color(f"Iter {iteration}: New candidate a_prime had invalid score/evals, not added to buffer.", 'yellow') + + # Update tracking + self._total_evaluations_tracker += a_evals + a_prime_evals + samples_used = 2 * evaluation_batch_size + train_batch_size + else: # If we don't use validation set for evaluation, please evaluate a_prime on the training set + a_prime_score, a_prime_evals = self._evaluate_candidate( + a_prime_params_dict, {'inputs': train_xs, 'infos': train_infos}, + guide, len(train_xs), num_threads + ) + self._total_evaluations_tracker += a_prime_evals + + new_candidate_entry = { + 'params': a_prime_params_dict, + 'score_sum': a_prime_score * a_prime_evals if a_prime_score > -np.inf else 0, + 'eval_count': a_prime_evals, + 'ucb_score': None, # Will be updated later + 'iteration_created': iteration + } + self.buffer.append(new_candidate_entry) + samples_used = 2*train_batch_size # One batch for training update, one for evaluation + return True, a_prime_score, score_for_a_on_train_batch, samples_used + + def train(self, + guide, # Guide for train_dataset (feedback generation AND evaluation) + train_dataset: Dict[str, List[Any]], + *, + validation_dataset: Optional[Dict[str, List[Any]]] = None, # Validation set for evaluation, defaults to train_dataset + test_dataset: Optional[Dict[str, List[Any]]] = None, + num_search_iterations: int = 100, + train_batch_size: int = 2, + evaluation_batch_size: int = 20, # Renamed from validation_batch_size, used for all explicit evaluations + eval_frequency: int = 1, + log_frequency: Optional[int] = None, + save_frequency: Optional[int] = None, + save_path: str = "checkpoints/ucb_agent.pkl", + min_score_for_agent_update: Optional[float] = None, # Renamed from min_score to avoid conflict with evaluate's min_score + verbose: Union[bool, str] = False, + num_threads: Optional[int] = None, + print_confidence_interval: bool = True, + **kwargs + ) -> Tuple[Dict[str, Any], float]: # Returns metrics and best score + """ + Main training loop for UCB Search Algorithm. + """ + # Default validation_dataset to train_dataset if not provided + if validation_dataset is None: + validation_dataset = train_dataset + if test_dataset is None: + test_dataset = train_dataset + + num_threads = num_threads or self.num_threads + log_frequency = log_frequency or eval_frequency + self.min_score = min_score_for_agent_update # Used by parent's evaluate if called, or our own _evaluate_candidate + total_samples = 0 + self.total_proposals = 0 + # Metrics tracking + metrics = { + 'best_candidate_scores': [], # Score of the best candidate (e.g., highest mean) found so far at each iteration + 'selected_action_ucb': [], # UCB score of the selected action 'a' + 'new_candidate_scores': [], # Score of the new candidate 'a_prime' + 'buffer_avg_score': [], + 'buffer_avg_evals': [], + } + +# 0. Evaluate the initial parameter on samples of the validation set and add it to the buffer. + initial_params_dict = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + print_color("Evaluating initial parameters using validation_dataset samples...", 'cyan') + initial_score, initial_evals = self._evaluate_candidate( + initial_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads # Use validation_dataset and guide + ) + self.logger.log('Test score', initial_score, 0, color='blue') + self.logger.log('Total samples', total_samples, 0, color='cyan') + print_color(f"Initial candidate: Score {initial_score:.4f}, Evals {initial_evals}", 'yellow') + if self.use_validation: + self._total_evaluations_tracker += initial_evals + total_samples += initial_evals + # Log initial evaluation + initial_candidate_entry = { + 'params': initial_params_dict, + 'score_sum': initial_score * initial_evals if initial_score > -np.inf else 0, # Store sum for accurate mean later + 'eval_count': initial_evals, + 'ucb_score': None, # avoid accidental reads before it's initialized + 'iteration_created': 0 + } + self._update_buffer_ucb_scores() # Update UCB for the initial candidate + else: + initial_candidate_entry = { + 'params': initial_params_dict, + 'score_sum': 0, + 'eval_count': 0, + 'ucb_score': None, # avoid accidental reads before it's initialized + 'iteration_created': 0 + } + self.buffer.append(initial_candidate_entry) + + # Main search loop + for iteration in range(1, num_search_iterations + 1): + try: + if not self.buffer: + print_color("Buffer is empty, stopping search.", 'red') + break + + # 1. Pick the candidate 'a' with the highest UCB from the buffer + self._update_buffer_ucb_scores() # Ensure UCB scores are fresh + + action_candidate_a = self.select(self.buffer) + if print_confidence_interval: + self.print_intervals(self.buffer) + # Log selected action UCB score + self.logger.log('Selected action UCB', action_candidate_a['ucb_score'], iteration, color='magenta') + self.logger.log('Selected action mean score', action_candidate_a['score_sum']/(action_candidate_a['eval_count'] or 1), iteration, color='cyan') + + print_color(f"Iter {iteration}/{num_search_iterations}: ", 'blue') + + # Process the selected candidate + success, a_prime_score, score_for_a_on_train_batch, samples_used = self._process_single_candidate( + action_candidate_a, guide, train_dataset, validation_dataset, + train_batch_size, evaluation_batch_size, num_threads, iteration + ) + + if not success: # Error occurred in processing + continue + + total_samples += samples_used + if self.use_validation: + metrics['new_candidate_scores'].append(a_prime_score) + self.logger.log('New candidate score', a_prime_score, iteration, color='green') + print_color(f"Iter {iteration}: New candidate a_prime generated. Validation Score: {a_prime_score:.4f}", 'cyan') + self.logger.log('Training batch score', score_for_a_on_train_batch, iteration, color='yellow') + + + + # Update all UCB scores in the buffer after potential additions/removals/stat updates + self._update_buffer_ucb_scores() + + # Logging + best_in_buffer = self._get_best_candidate_from_buffer(self.buffer) + if best_in_buffer: + metrics['best_candidate_scores'].append(best_in_buffer['score_sum']/(best_in_buffer['eval_count'] or 1)) + else: + metrics['best_candidate_scores'].append(-np.inf) + metrics['buffer_avg_score'].append(np.mean([c['score_sum']/(c['eval_count'] or 1) for c in self.buffer if c['eval_count'] > 0])) + metrics['buffer_avg_evals'].append(np.mean([c['eval_count'] for c in self.buffer])) + + if iteration % log_frequency == 0: + log_data = { + "iteration": iteration, + "best_score": metrics['best_candidate_scores'][-1], #best_candidate_score_in_buffer + "selected_action_ucb": action_candidate_a['ucb_score'], + "new_candidate_score": a_prime_score, + "buffer_size": len(self.buffer), + "buffer_avg_score": metrics['buffer_avg_score'][-1], + "buffer_avg_evals": metrics['buffer_avg_evals'][-1], + "total_evaluations_tracker": self._total_evaluations_tracker, # used in calculating ucb scores + "total_samples": total_samples # Add new metric + } + + # Log all important metrics + self.logger.log('Best candidate score', log_data['best_score'], iteration, color='green') + self.logger.log('Buffer size', log_data['buffer_size'], iteration, color='blue') + self.logger.log('Buffer average score', log_data['buffer_avg_score'], iteration, color='cyan') + self.logger.log('Buffer average evaluations', log_data['buffer_avg_evals'], iteration, color='orange') + # self.logger.log('Total evaluations tracker', log_data['total_evaluations_tracker'], iteration, color='magenta') + self.logger.log('Total samples', log_data['total_samples'], iteration, color='yellow') + self.logger.log('Total proposals', self.total_proposals, iteration, color='red') + print_color(f"Log @ Iter {iteration}: Best score in buffer: {log_data['best_score']:.4f}, Buffer size: {log_data['buffer_size']}, Total samples: {total_samples}", 'green') + + if test_dataset is not None and iteration % eval_frequency == 0: + try: + # Save current agent parameters + current_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + + # Find the best candidate in the buffer (highest mean score) + best_candidate = self._get_best_candidate_from_buffer(self.buffer) + if not best_candidate: + print_color(f"Iter {iteration}: No valid candidate for test evaluation.", 'yellow') + continue + + # Load best candidate's parameters into the agent for evaluation + self.optimizer.update(best_candidate['params']) + + # Evaluate the best candidate on test set + test_score = self.evaluate(self.agent, guide, test_dataset['inputs'], test_dataset['infos'], + min_score=self.min_score, num_threads=num_threads, + description=f"Evaluating best candidate (iteration {iteration})") + + # Restore original agent parameters + self.optimizer.update(current_params) + + self.logger.log('Test score', test_score, iteration, color='green') + except Exception as e: + print_color(f"Iter {iteration}: Test evaluation failed: {e}", 'red') + + # Save agent (e.g., the one with highest mean score in buffer) + if save_frequency is not None and iteration % save_frequency == 0: + try: + best_overall_candidate = self._get_best_candidate_from_buffer(self.buffer) + if not best_overall_candidate: + print_color(f"Iter {iteration}: No valid candidate for agent save.", 'yellow') + continue + self.optimizer.update(best_overall_candidate['params']) # Load params using optimizer + self.save_agent(save_path, iteration) # save_agent is from AlgorithmBase + print_color(f"Iter {iteration}: Saved agent based on best candidate in buffer.", 'green') + except Exception as e: + print_color(f"Iter {iteration}: Agent save failed: {e}", 'red') + + except Exception as e: + print_color(f"Iter {iteration}: Iteration failed with error: {e}. Skipping to next iteration.", 'red') + self.logger.log('Iteration error', str(e), iteration, color='red') + continue + + # End of search loop + print_color("UCB search finished.", 'blue') + + # Log final training summary + final_iteration = num_search_iterations + self.logger.log('UCB search completed', final_iteration, final_iteration, color='blue') + self.logger.log('Final total samples', total_samples, final_iteration, color='magenta') + + if not self.buffer: + print_color("Buffer is empty at the end of search. No best candidate found.", 'red') + self.logger.log('Final status', 'Buffer empty - no best candidate', final_iteration, color='red') + return metrics, -np.inf + + # Select the best candidate based on highest mean score (exploitation) + final_best_candidate = self._get_best_candidate_from_buffer(self.buffer) + if not final_best_candidate: + print_color("No valid candidate found at the end of search.", 'red') + return metrics, -np.inf + final_best_score = final_best_candidate['score_sum'] / (final_best_candidate['eval_count'] or 1E-9) + + # Log final results + self.logger.log('Final best score', final_best_score, final_iteration, color='green') + self.logger.log('Final best candidate evaluations', final_best_candidate['eval_count'], final_iteration, color='cyan') + self.logger.log('Final buffer size', len(self.buffer), final_iteration, color='blue') + + print_color(f"Final best candidate: Mean Score {final_best_score:.4f}, Evals {final_best_candidate['eval_count']}", 'green') + + # Load best parameters into the agent + self.optimizer.update(final_best_candidate['params']) # Load params using optimizer + + return metrics, float(final_best_score) + + def select(self, buffer): + '''Could be subclassed to implement different selection strategies''' + return max(buffer, key=lambda c: c['ucb_score']) + + +class UCBSearchParallelAlgorithm(UCBSearchAlgorithm): + """ + Parallel UCB Search Algorithm. + + Instead of selecting one candidate with highest UCB score, selects top-k candidates + and processes them in parallel to generate k new candidates per iteration. + """ + + def __init__(self, + agent: trace.Module, + optimizer, + max_buffer_size: int = 10, + ucb_exploration_factor: float = 1.0, + parallel_k: int = 2, # Number of top candidates to process in parallel + logger=None, + num_threads: int = None, + *args, + **kwargs): + super().__init__(agent, optimizer, max_buffer_size, ucb_exploration_factor, + logger, num_threads, *args, **kwargs) + self.parallel_k = parallel_k + + def select_top_k(self, buffer, k): + """Select top k candidates with highest UCB scores""" + if len(buffer) <= k: + return buffer.copy() + + # Sort by UCB score and return top k + sorted_candidates = sorted(buffer, key=lambda c: c['ucb_score'], reverse=True) + return sorted_candidates[:k] + + def train(self, + guide, + train_dataset: Dict[str, List[Any]], + *, + validation_dataset: Optional[Dict[str, List[Any]]] = None, + test_dataset: Optional[Dict[str, List[Any]]] = None, + num_search_iterations: int = 100, + train_batch_size: int = 2, + evaluation_batch_size: int = 20, + eval_frequency: int = 1, + log_frequency: Optional[int] = None, + save_frequency: Optional[int] = None, + save_path: str = "checkpoints/ucb_parallel_agent.pkl", + min_score_for_agent_update: Optional[float] = None, + verbose: Union[bool, str] = False, + num_threads: Optional[int] = None, + print_confidence_interval: bool = True, + **kwargs + ) -> Tuple[Dict[str, Any], float]: + """ + Main training loop for Parallel UCB Search Algorithm. + """ + # Default validation_dataset to train_dataset if not provided + if validation_dataset is None: + validation_dataset = train_dataset + if test_dataset is None: + test_dataset = train_dataset + + num_threads = num_threads or self.num_threads + log_frequency = log_frequency or eval_frequency + self.min_score = min_score_for_agent_update + total_samples = 0 + self.total_proposals = 0 + + # Metrics tracking + metrics = { + 'best_candidate_scores': [], + 'selected_actions_ucb': [], # UCB scores of selected top-k actions + 'new_candidate_scores': [], # Scores of all new candidates + 'buffer_avg_score': [], + 'buffer_avg_evals': [], + 'parallel_k_used': [], # Track how many candidates were actually processed + } + + # Initialize with first candidate (same as parent) + print_color("Evaluating initial parameters using validation_dataset samples...", 'cyan') + initial_params_dict = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + initial_score, initial_evals = self._evaluate_candidate( + initial_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads + ) + self._total_evaluations_tracker += initial_evals + total_samples += initial_evals + + # Log initial evaluation + self.logger.log('Initial UCB score', initial_score, 0, color='blue') + self.logger.log('Total samples', total_samples, 0, color='cyan') + + initial_candidate_entry = { + 'params': initial_params_dict, + 'score_sum': initial_score * initial_evals if initial_score > -np.inf else 0, + 'eval_count': initial_evals, + 'ucb_score': None, + 'iteration_created': 0 + } + self.buffer.append(initial_candidate_entry) + self._update_buffer_ucb_scores() + print_color(f"Initial candidate: Score {initial_score:.4f}, Evals {initial_evals}", 'yellow') + + # Main search loop + for iteration in range(1, num_search_iterations + 1): + try: + if not self.buffer: + print_color("Buffer is empty, stopping search.", 'red') + break + + # 1. Select top-k candidates with highest UCB scores + self._update_buffer_ucb_scores() + top_k_candidates = self.select_top_k(self.buffer, self.parallel_k) + + if print_confidence_interval: + self.print_intervals(self.buffer) + + print_color(f"Iter {iteration}/{num_search_iterations}: Processing {len(top_k_candidates)} candidates in parallel", 'blue') + + # Log selected actions UCB scores + selected_ucb_scores = [c['ucb_score'] for c in top_k_candidates] + metrics['selected_actions_ucb'].append(selected_ucb_scores) + avg_selected_ucb = np.mean(selected_ucb_scores) + self.logger.log('Average selected UCB', avg_selected_ucb, iteration, color='magenta') + + # 2. Process all top-k candidates sequentially + candidate_results = [] + for candidate in top_k_candidates: + result = self._process_single_candidate( + candidate, guide, train_dataset, validation_dataset, + train_batch_size, evaluation_batch_size, num_threads, iteration + ) + candidate_results.append(result) + + # 3. Process results and update statistics + iteration_new_scores = [] + + for i, (candidate, result) in enumerate(zip(top_k_candidates, candidate_results)): + success, a_prime_score, score_for_a_on_train_batch, samples_used = result + + if not success: # Error occurred + print_color(f"Iter {iteration}: Candidate {i+1} processing failed, skipping.", 'yellow') + continue + # Track new candidate score + iteration_new_scores.append(a_prime_score) + + # Update tracking + total_samples += samples_used + + metrics['new_candidate_scores'].extend(iteration_new_scores) + + # Log iteration performance + if iteration_new_scores: + avg_new_score = np.mean(iteration_new_scores) + max_new_score = max(iteration_new_scores) + self.logger.log('New candidate score', avg_new_score, iteration, color='green') #average new candidate score + self.logger.log('Max new candidate score', max_new_score, iteration, color='green') + print_color(f"Iter {iteration}: Generated {len(iteration_new_scores)} new candidates. Avg score: {avg_new_score:.4f}, Max: {max_new_score:.4f}", 'cyan') + + # Update UCB scores and track metrics + self._update_buffer_ucb_scores() + + if self.buffer: + best_in_buffer = self._get_best_candidate_from_buffer(self.buffer) + if best_in_buffer: + best_score = best_in_buffer['score_sum']/(best_in_buffer['eval_count'] or 1) + metrics['best_candidate_scores'].append(best_score) + else: + metrics['best_candidate_scores'].append(-np.inf) + metrics['buffer_avg_score'].append(np.mean([c['score_sum']/(c['eval_count'] or 1) for c in self.buffer if c['eval_count'] > 0])) + metrics['buffer_avg_evals'].append(np.mean([c['eval_count'] for c in self.buffer])) + + # Logging + if iteration % log_frequency == 0: + self.logger.log('Best candidate score', best_score, iteration, color='green') + self.logger.log('Buffer size', len(self.buffer), iteration, color='blue') + self.logger.log('Buffer average score', metrics['buffer_avg_score'][-1], iteration, color='cyan') + self.logger.log('Total samples', total_samples, iteration, color='yellow') + self.logger.log('Total proposals', self.total_proposals, iteration, color='red') + print_color(f"Log @ Iter {iteration}: Best score: {best_score:.4f}, Buffer size: {len(self.buffer)}, Total samples: {total_samples}", 'green') + + # Test evaluation (same as parent) + if test_dataset is not None and iteration % eval_frequency == 0: + try: + current_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + best_candidate = self._get_best_candidate_from_buffer(self.buffer) + if not best_candidate: + print_color(f"Iter {iteration}: No valid candidate for test evaluation.", 'yellow') + continue + self.optimizer.update(best_candidate['params']) + + test_score = self.evaluate(self.agent, guide, test_dataset['inputs'], test_dataset['infos'], + min_score=self.min_score, num_threads=num_threads, + description=f"Evaluating best candidate (iteration {iteration})") + + self.optimizer.update(current_params) + self.logger.log('Test score', test_score, iteration, color='green') + except Exception as e: + print_color(f"Iter {iteration}: Test evaluation failed: {e}", 'red') + + # Save agent (same as parent) + if save_frequency is not None and iteration % save_frequency == 0: + try: + best_overall_candidate = self._get_best_candidate_from_buffer(self.buffer) + if not best_overall_candidate: + print_color(f"Iter {iteration}: No valid candidate for agent save.", 'yellow') + continue + self.optimizer.update(best_overall_candidate['params']) + self.save_agent(save_path, iteration) + print_color(f"Iter {iteration}: Saved agent based on best candidate in buffer.", 'green') + except Exception as e: + print_color(f"Iter {iteration}: Agent save failed: {e}", 'red') + + except Exception as e: + print_color(f"Iter {iteration}: Iteration failed with error: {e}. Skipping to next iteration.", 'red') + self.logger.log('Iteration error', str(e), iteration, color='red') + continue + + # End of search (same as parent) + print_color("Parallel UCB search finished.", 'blue') + + final_iteration = num_search_iterations + self.logger.log('Parallel UCB search completed', final_iteration, final_iteration, color='blue') + self.logger.log('Final total samples', total_samples, final_iteration, color='magenta') + + if not self.buffer: + print_color("Buffer is empty at the end of search. No best candidate found.", 'red') + return metrics, -np.inf + + final_best_candidate = self._get_best_candidate_from_buffer(self.buffer) + if not final_best_candidate: + print_color("No valid candidate found at the end of search.", 'red') + return metrics, -np.inf + final_best_score = final_best_candidate['score_sum'] / (final_best_candidate['eval_count'] or 1E-9) + + self.logger.log('Final best score', final_best_score, final_iteration, color='green') + print_color(f"Final best candidate: Mean Score {final_best_score:.4f}, Evals {final_best_candidate['eval_count']}", 'green') + + # Load best parameters into the agent + self.optimizer.update(final_best_candidate['params']) + + return metrics, float(final_best_score) + + +class HybridUCB_LLM(MinibatchAlgorithm): + """ + UCB Search Algorithm with Function Approximation (LLM). + + Keeps a buffer of candidates. + In each iteration: + - With probability alpha: + 1. Picks a candidate 'a' from the buffer with the highest UCB score. + 2. Updates the optimizer with 'a's parameters. + 3. Draws a minibatch from the training set, performs a forward/backward pass, and calls optimizer.step() to get a new candidate 'a_prime'. + 4. Evaluates 'a_prime' on a validation set minibatch. + 5. Updates statistics of 'a' (based on the training minibatch). + 6. Adds 'a_prime' (with its validation stats) to the buffer. + - With probability 1-alpha: + 1. Uses an external LLM, prompted with candidates from the buffer, to generate a new candidate 'a_prime'. + 2. Evaluates 'a_prime' on a validation set minibatch. + 3. Adds 'a_prime' (with its validation stats) to the buffer. + If the buffer is full, evicts the candidate with the lowest UCB score. + """ + + def __init__(self, + agent: trace.Module, + optimizer, + max_buffer_size: int = 10, + ucb_exploration_factor: float = 0.3, + alpha: float = 0.3, + llm_model: str = None, + num_samples_in_prompt: int = 5, + logger=None, + num_threads: int = None, + *args, + **kwargs): + super().__init__(agent, optimizer, num_threads=num_threads, logger=logger, *args, **kwargs) + + self.alpha = alpha + self.llm_model = llm_model + self.num_samples_in_prompt = num_samples_in_prompt + self.llm_prompt_budget_factor = 0.5 + + self.buffer = deque(maxlen=max_buffer_size) + self.max_buffer_size = max_buffer_size + self.ucb_exploration_factor = ucb_exploration_factor + + if not hasattr(self.optimizer, 'step'): + raise ValueError("Optimizer must have a 'step' method.") + + self._total_evaluations_tracker = 0 + + # Initialize LLM + self.llm = LLM(model=self.llm_model) + print_color(f"Initialized HybridUCB_LLM with alpha={self.alpha}, LLM model={self.llm_model}", "cyan") + + def _sample_minibatch(self, dataset: Dict[str, List[Any]], batch_size: int) -> Tuple[List[Any], List[Any]]: + """Sample a minibatch from the dataset.""" + if not dataset or not dataset.get('inputs') or not dataset.get('infos'): + print_color("Warning: Attempted to sample from an empty or malformed dataset.", color='yellow') + return [], [] + + dataset_size = len(dataset['inputs']) + if dataset_size == 0: + print_color("Warning: Dataset is empty, cannot sample minibatch.", color='yellow') + return [], [] + + actual_batch_size = min(batch_size, dataset_size) + indices = np.random.choice(dataset_size, actual_batch_size, replace=False) + xs = [dataset['inputs'][i] for i in indices] + infos = [dataset['infos'][i] for i in indices] + return xs, infos + + def _evaluate_candidate(self, + params_to_eval_dict: Dict[str, Any], + dataset: Dict[str, List[Any]], + guide, + evaluation_batch_size: int, + num_threads: Optional[int] = None + ) -> Tuple[float, int]: + """Evaluates a given set of parameters on samples from the provided dataset.""" + if not dataset or not dataset.get('inputs') or not dataset.get('infos') or not dataset['inputs']: + print_color("Evaluation dataset is empty or invalid. Returning score -inf, count 0.", color='yellow') + return -np.inf, 0 + + original_params_backup = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} + + try: + self.optimizer.update(params_to_eval_dict) + except Exception as e: + print_color(f"Error updating agent with params_to_eval_dict: {e}. Using current agent state for eval.", "red") + + eval_xs, eval_infos = self._sample_minibatch(dataset, evaluation_batch_size) + + if not eval_xs: + print_color("Evaluation minibatch is empty. Returning score -inf, count 0.", color='yellow') + self.optimizer.update(original_params_backup) + return -np.inf, 0 + + eval_scores = evaluate(self.agent, + guide, + eval_xs, + eval_infos, + min_score=self.min_score if hasattr(self, 'min_score') else None, + num_threads=num_threads or self.num_threads, + description=f"Evaluating candidate") + + self.optimizer.update(original_params_backup) + + avg_score = np.mean(eval_scores) if eval_scores and all(s is not None for s in eval_scores) else 0 + eval_count = len(eval_xs) + + return float(avg_score), eval_count + + def _calculate_ucb(self, candidate_buffer_entry: Dict, total_tracked_evaluations: int) -> float: + """Calculates UCB score for a candidate in the buffer.""" + if candidate_buffer_entry['eval_count'] == 0: + return float('inf') + + mean_score = candidate_buffer_entry['score_sum'] / candidate_buffer_entry['eval_count'] + + if total_tracked_evaluations == 0: + total_tracked_evaluations = 1 + + exploration_term = self.ucb_exploration_factor * \ + math.sqrt(math.log(total_tracked_evaluations + 1e-9) / candidate_buffer_entry['eval_count']) + + return mean_score + exploration_term + + def _calculate_lcb(self, candidate_buffer_entry: Dict, total_tracked_evaluations: int) -> float: + """Calculates Lower Confidence Bound for a candidate in the buffer.""" + if candidate_buffer_entry['eval_count'] == 0: + return float('-inf') # Unvisited states get lowest bound + + mean_score = candidate_buffer_entry['score_sum'] / candidate_buffer_entry['eval_count'] + + # Add 1 to total_tracked_evaluations to prevent log(0) if it's the first evaluation overall + # and to ensure log argument is > 0. + # Add 1 to eval_count in denominator as well to ensure it's robust if eval_count is small. + if total_tracked_evaluations == 0: # Should not happen if we init with one eval + total_tracked_evaluations = 1 + + # LCB exploration term: ucb_exploration_factor scales the confidence interval + # Higher factor = more exploration, lower factor = more exploitation + exploration_term = self.ucb_exploration_factor * \ + math.sqrt(math.log(total_tracked_evaluations) / candidate_buffer_entry['eval_count']) + + return mean_score - exploration_term + + def _update_buffer_ucb_scores(self): + """Recalculates and updates UCB scores for all candidates in the buffer.""" + if not self.buffer: + return + + for candidate_entry in self.buffer: + candidate_entry['ucb_score'] = self._calculate_ucb(candidate_entry, self._total_evaluations_tracker) + + def _get_best_candidate_from_buffer(self, buffer): + """Get the best candidate from buffer, excluding those with eval_count = 0.""" + if not buffer: + return None + + # Filter out candidates with eval_count = 0 + valid_candidates = [c for c in buffer if c['eval_count'] > 0] + if not valid_candidates: + # If no candidates have been evaluated, return the one with highest UCB score + return max(buffer, key=lambda c: c.get('ucb_score', -float('inf'))) + return max(valid_candidates, key=lambda c: c['score_sum'] / c['eval_count']) + + def print_intervals(self, buffer): + """Print confidence intervals for debugging in the form of open intervals (LCB, UCB)""" + print_color("Confidence intervals for all candidates:", 'cyan') + for i, candidate_entry in enumerate(buffer): + lcb = self._calculate_lcb(candidate_entry, self._total_evaluations_tracker) + ucb = candidate_entry['ucb_score'] + mean_score = candidate_entry['score_sum'] / (candidate_entry['eval_count'] or 1) + eval_count = candidate_entry['eval_count'] + + # Format as open interval (LCB, UCB) with mean score and evaluation count + interval_str = f"Action {i+1}: ({lcb:.4f}, {ucb:.4f}) [mean: {mean_score:.4f}, n: {eval_count}]" + print_color(interval_str, 'cyan') + + def _llm_generate_candidate(self) -> Optional[Dict[trace.nodes.ParameterNode, str]]: + """ + Prompts an LLM with current buffer candidates to generate new string values for parameters. + Returns a dictionary mapping ParameterNode objects to new string values, or None on failure. + """ + print_color("Attempting to generate candidate using LLM...", "blue") + if not self.buffer: + print_color("LLM generation: Buffer is empty, cannot provide context to LLM.", "yellow") + return None + + sorted_buffer = sorted(list(self.buffer), key=lambda c: c.get('ucb_score', -float('inf')), reverse=True) + # Include first, last, and evenly spaced middle candidates + if len(sorted_buffer) <= self.num_samples_in_prompt: + prompt_candidates = sorted_buffer + elif self.num_samples_in_prompt <= 2: + # If only 1-2 samples requested, take first and optionally last + prompt_candidates = sorted_buffer[:self.num_samples_in_prompt] + else: + # Take first, last, and evenly spaced middle candidates + prompt_candidates = [sorted_buffer[0]] # First (highest UCB) + if self.num_samples_in_prompt > 2: + # Calculate indices for middle candidates + middle_count = self.num_samples_in_prompt - 2 # Exclude first and last + if middle_count > 0 and len(sorted_buffer) > 2: + # Evenly space middle candidates between index 1 and len-2 + middle_indices = [int(1 + i * (len(sorted_buffer) - 2) / (middle_count + 1)) + for i in range(1, middle_count + 1)] + prompt_candidates.extend([sorted_buffer[i] for i in middle_indices]) + prompt_candidates.append(sorted_buffer[-1]) # Last (lowest UCB) + + serializable_candidate_summaries = [] + for cand_entry in prompt_candidates: + summary = { + "parameters": {getattr(p,'py_name'): copy.deepcopy(p.data) for p in cand_entry['params']}, + "eval_count": cand_entry['eval_count'], + "ucb_score": round(cand_entry.get('ucb_score',0), 4), + } + serializable_candidate_summaries.append(summary) + + example_param_structure_json_str = {getattr(p,'py_name'): copy.deepcopy(p.data) for p in self.agent.parameters()} + + prompt_messages = [ + {"role": "system", "content": "You are an expert in model optimization. Your task is to propose new string values for model parameters with high UCB scores. Please output ONLY a valid JSON dictionary where keys are parameter names and values are the new string values for those parameters, matching the example structure provided. Do not add any explanations or markdown formatting around the JSON."}, + {"role": "user", "content": f"Here are some current candidates from the search buffer and their statistics:\\n{serializable_candidate_summaries}\\n\\nHere is an example of the required JSON output structure (parameter names as keys, new string values as values):\\n{example_param_structure_json_str}\\n\\nPlease generate a new set of parameters in exactly the same JSON format. Make sure use double quotes for the keys and values."} + ] + + print_color(f"LLM prompt (summary): {len(prompt_candidates)} candidates, structure example provided.", "magenta") + response_format = {"type": "json_object"} + llm_response = self.llm(prompt_messages, response_format=response_format) + llm_response_str = llm_response.choices[0].message.content + + if not llm_response_str: + print_color("LLM returned an empty response.", "red") + return None + + cleaned_llm_response_str = llm_response_str.strip() + + try: + llm_params_raw = json.loads(cleaned_llm_response_str) + except json.JSONDecodeError as e: + print_color(f"JSON parsing attempts failed: {e}", "red") + print_color("Returning the candidate with the highest UCB score in the buffer.", "red") + return max(self.buffer, key=lambda c: c.get('ucb_score', -float('inf')))['params'] + + if not isinstance(llm_params_raw, dict): + print_color(f"LLM output was not a JSON dictionary after parsing: {type(llm_params_raw)}", "red") + print_color("Returning the candidate with the highest UCB score in the buffer.", "red") + return max(self.buffer, key=lambda c: c.get('ucb_score', -float('inf')))['params'] + + candidate_params_dict = self.construct_update_dict(llm_params_raw) + return candidate_params_dict + + def construct_update_dict(self, suggestion: Dict[str, Any]) -> Dict[ParameterNode, Any]: + """Convert the suggestion in text into the right data type.""" + update_dict = {} + for node in self.agent.parameters(): + if node.trainable and node.py_name in suggestion: + try: + formatted_suggestion = suggestion[node.py_name] + if type(formatted_suggestion) == str and 'def' in formatted_suggestion: + formatted_suggestion = format_str(formatted_suggestion, mode=FileMode()) + update_dict[node] = type(node.data)(formatted_suggestion) + except (ValueError, KeyError) as e: + if getattr(self, 'ignore_extraction_error', False): + warnings.warn( + f"Cannot convert the suggestion '{suggestion[node.py_name]}' for {node.py_name} to the right data type" + ) + else: + raise e + return update_dict + + def train(self, + guide, + train_dataset: Dict[str, List[Any]], + *, + num_search_iterations: int = 100, + validation_dataset: Dict[str, List[Any]] = None, + test_dataset: Dict[str, List[Any]] = None, + train_batch_size: int = 5, + evaluation_batch_size: int = 5, + eval_frequency: int = 1, + log_frequency: Optional[int] = None, + save_frequency: Optional[int] = None, + save_path: str = "checkpoints/ucb_llm_agent.pkl", + min_score_for_agent_update: Optional[float] = None, + verbose: Union[bool, str] = False, + num_threads: Optional[int] = None, + print_confidence_interval: bool = True, + **kwargs + ) -> Tuple[Dict[str, Any], float]: + + if validation_dataset is None: + validation_dataset = train_dataset + if test_dataset is None: + test_dataset = train_dataset + + num_threads = num_threads or self.num_threads + log_frequency = log_frequency or eval_frequency + self.min_score = min_score_for_agent_update + total_samples = 0 + self.total_proposals = 0 + + metrics = { + 'best_candidate_scores': [], + 'selected_action_ucb': [], + 'new_candidate_scores': [], + 'buffer_avg_score': [], + 'buffer_avg_evals': [], + 'llm_generation_failures': 0, + 'generation_path': [] + } + + # Initial candidate evaluation + print_color("Evaluating initial parameters using train_dataset samples...", 'cyan') + initial_params_dict = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} + + initial_score, initial_evals = self._evaluate_candidate( + initial_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads + ) + self._total_evaluations_tracker += initial_evals + total_samples += initial_evals + + initial_candidate_entry = { + 'params': initial_params_dict, + 'score_sum': initial_score * initial_evals if initial_score > -np.inf else 0, + 'eval_count': initial_evals, + 'ucb_score': 0.0, + 'iteration_created': 0 + } + self.buffer.append(initial_candidate_entry) + self._update_buffer_ucb_scores() + print_color(f"Initial candidate: Score {initial_score:.4f}, Evals {initial_evals}", 'yellow') + + # Log initial evaluation + self.logger.log('Initial UCB score', initial_score, 0, color='blue') + self.logger.log('Total samples', total_samples, 0, color='cyan') + self.logger.log('Total proposals', self.total_proposals, 0, color='red') + + # Main search loop + for iteration in range(1, num_search_iterations + 1): + try: + if not self.buffer: + print_color("Buffer is empty, stopping search.", 'red') + break + + self._update_buffer_ucb_scores() + a_prime_params_dict = None + a_prime_score = 0 + a_prime_evals = 0 + generation_method = "none" + if print_confidence_interval: + self.print_intervals(self.buffer) + + if iteration<=2 or random.random() < self.alpha: # UCB Path, for the first 2 iterations, we always use UCB because the buffer size is small, it's hard for LLM to generate good candidates + generation_method = "ucb" + metrics['generation_path'].append("ucb") + if not self.buffer: + print_color(f"Iter {iteration} (UCB Path): Buffer empty, cannot select action. Skipping.", "red") + continue + + action_candidate_a = self.select(self.buffer) + + selected_mean_score = action_candidate_a['score_sum'] / action_candidate_a['eval_count'] if action_candidate_a['eval_count'] > 0 else -np.inf + print_color(f"Iter {iteration} (UCB Path): Selected action candidate (UCB: {action_candidate_a['ucb_score']:.4f}, MeanScore: {selected_mean_score:.4f} Evals: {action_candidate_a['eval_count']})", 'blue') + # metrics['selected_action_ucb'].append(action_candidate_a['ucb_score']) + + # Log selected action UCB score + # self.logger.log('Selected action UCB', action_candidate_a['ucb_score'], iteration, color='magenta') + # self.logger.log('Selected action mean score', selected_mean_score, iteration, color='cyan') + + self.optimizer.update(action_candidate_a['params']) + + train_xs, train_infos = self._sample_minibatch(train_dataset, train_batch_size) + if not train_xs: + print_color(f"Iter {iteration} (UCB Path): Training minibatch empty, skipping optimizer step.", 'yellow') + continue + + total_samples += len(train_xs) + + # Forward pass for 'a' + outputs_for_a = [] + use_asyncio = self._use_asyncio(num_threads) + if use_asyncio: + outputs_for_a = async_run([self.forward]*len(train_xs), + [(self.agent, x, guide, info) for x, info in zip(train_xs, train_infos)], + max_workers=num_threads, + description=f"Iter {iteration} (UCB): Forward for 'a'") + else: + outputs_for_a = [self.forward(self.agent, x, guide, info) for x, info in zip(train_xs, train_infos)] + + scores_from_train, targets_from_train, feedbacks_from_train = [], [], [] + for target, score, feedback in outputs_for_a: + scores_from_train.append(score) + targets_from_train.append(target) + feedbacks_from_train.append(feedback) + + if not scores_from_train: + print_color(f"Iter {iteration} (UCB Path): No outputs from forward pass for 'a'. Skipping.", 'yellow') + continue + + target_for_a = batchify(*targets_from_train) + feedback_for_a = batchify(*feedbacks_from_train).data + score_for_a_on_train_batch = np.mean([s for s in scores_from_train if s is not None]) if any(s is not None for s in scores_from_train) else 0 + + self.optimizer.zero_feedback() + self.optimizer.backward(target_for_a, feedback_for_a) + + # Get a_prime by optimizer step + try: + returned_params = self.optimizer.step(bypassing=True, verbose=False) + if not isinstance(returned_params, dict) or not returned_params: + print_color(f"Iter {iteration} (UCB Path): Optimizer.step did not return a valid param dict for a_prime. Using current agent params.", 'yellow') + a_prime_params_dict = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} + else: + a_prime_params_dict = {p: copy.deepcopy(p.data) for p in returned_params} + self.total_proposals += 1 + + except Exception as e: + print_color(f"Iter {iteration} (UCB Path): Error during optimizer.step for a_prime: {e}. Skipping.", 'red') + continue + + # Evaluate 'a' and 'a_prime' on validation set in parallel (like UCBSearchAlgorithm) + use_asyncio = self._use_asyncio(num_threads) + if use_asyncio: + evaluation_results = async_run( + [self._evaluate_candidate, self._evaluate_candidate], + [ + (action_candidate_a['params'], validation_dataset, guide, evaluation_batch_size, num_threads), + (a_prime_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads) + ], + max_workers=2, + description=f"Iter {iteration} (UCB): Parallel evaluation of 'a' and 'a_prime'" + ) + (a_score, a_evals), (a_prime_score, a_prime_evals) = evaluation_results + else: + a_score, a_evals = self._evaluate_candidate( + action_candidate_a['params'], validation_dataset, guide, evaluation_batch_size, num_threads + ) + a_prime_score, a_prime_evals = self._evaluate_candidate( + a_prime_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads + ) + + self._total_evaluations_tracker += a_evals + a_prime_evals + total_samples += a_evals + a_prime_evals + + # Update stats of action_candidate_a + if score_for_a_on_train_batch > -np.inf: + action_candidate_a['score_sum'] += score_for_a_on_train_batch * len(train_xs) + action_candidate_a['eval_count'] += len(train_xs) + self._total_evaluations_tracker += len(train_xs) + + # Update stats with validation evaluation of 'a' + action_candidate_a['score_sum'] += a_score * a_evals + action_candidate_a['eval_count'] += a_evals + + print_color(f"Iter {iteration} (UCB Path): New candidate a_prime (from UCB) generated. Eval Score: {a_prime_score:.4f}, Evals: {a_prime_evals}", 'cyan') + self.logger.log('New candidate score', a_prime_score, iteration, color='green') + self.logger.log('Training batch score', score_for_a_on_train_batch, iteration, color='yellow') + else: # LLM Pathcandi + generation_method = "llm" + metrics['generation_path'].append("llm") + print_color(f"Iter {iteration} (LLM Path): Generating candidate via LLM.", 'blue') + a_prime_params_dict = self._llm_generate_candidate() + + if a_prime_params_dict: + # Evaluate a_prime (from LLM path) + a_prime_score, a_prime_evals = self._evaluate_candidate( + a_prime_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads + ) + self._total_evaluations_tracker += a_prime_evals + total_samples += a_prime_evals + self.total_proposals += 1 + print_color(f"Iter {iteration} (LLM Path): New candidate a_prime (from LLM) generated. Eval Score: {a_prime_score:.4f}, Evals: {a_prime_evals}", 'cyan') + self.logger.log('New candidate score', a_prime_score, iteration, color='green') #average new candidate score + else: + print_color(f"Iter {iteration} (LLM Path): LLM failed to generate a valid candidate. Skipping addition to buffer.", 'red') + metrics['llm_generation_failures'] += 1 + continue + + # Common logic for adding a_prime to buffer + metrics['new_candidate_scores'].append(a_prime_score) + + if a_prime_params_dict and a_prime_score > -np.inf and a_prime_evals > 0: + new_candidate_entry = { + 'params': a_prime_params_dict, + 'score_sum': a_prime_score * a_prime_evals, + 'eval_count': a_prime_evals, + 'ucb_score': 0.0, + 'iteration_created': iteration + } + + if len(self.buffer) == self.max_buffer_size: + self._update_buffer_ucb_scores() + candidate_to_evict = min(self.buffer, key=lambda c: c['ucb_score']) + self.buffer.remove(candidate_to_evict) + evicted_mean_score = candidate_to_evict['score_sum'] / candidate_to_evict['eval_count'] if candidate_to_evict['eval_count'] > 0 else -np.inf + print_color(f"Iter {iteration}: Buffer full. Evicted candidate (UCB: {candidate_to_evict['ucb_score']:.4f}, MeanScore: {evicted_mean_score:.4f})", 'magenta') + + self.buffer.append(new_candidate_entry) + print_color(f"Iter {iteration}: Added new candidate (from {generation_method}) to buffer.", 'magenta') + elif a_prime_params_dict: + print_color(f"Iter {iteration}: New candidate a_prime (from {generation_method}) had invalid score/evals ({a_prime_score}, {a_prime_evals}), not added to buffer.", 'yellow') + + self._update_buffer_ucb_scores() + + # Logging + if self.buffer: + best_in_buffer = max(self.buffer, key=lambda c: (c['score_sum']/(c['eval_count'] if c['eval_count'] > 0 else 1))) + current_best_score = best_in_buffer['score_sum']/(best_in_buffer['eval_count'] if best_in_buffer['eval_count'] > 0 else 1) + metrics['best_candidate_scores'].append(current_best_score) + + valid_scores = [c['score_sum']/(c['eval_count'] if c['eval_count'] > 0 else 1) for c in self.buffer if c['eval_count'] > 0] + metrics['buffer_avg_score'].append(np.mean(valid_scores) if valid_scores else -np.inf) + metrics['buffer_avg_evals'].append(np.mean([c['eval_count'] for c in self.buffer])) + else: + metrics['best_candidate_scores'].append(0) + metrics['buffer_avg_score'].append(0) + metrics['buffer_avg_evals'].append(0) + + if iteration % log_frequency == 0: + log_data = { + "iteration": iteration, + "best_score": metrics['best_candidate_scores'][-1], + "newly_evaluated_candidate_score": a_prime_score, + "buffer_size": len(self.buffer), + "buffer_avg_score": metrics['buffer_avg_score'][-1], + "buffer_avg_evals": metrics['buffer_avg_evals'][-1], + "total_evaluations_ucb_T": self._total_evaluations_tracker, + "total_samples": total_samples, + "generation_method_this_iter": generation_method, + "llm_generation_total_failures": metrics['llm_generation_failures'] + } + if generation_method == "ucb" and metrics['selected_action_ucb']: + log_data["selected_action_ucb"] = metrics['selected_action_ucb'][-1] + + # Log all important metrics + self.logger.log('Best candidate score', log_data['best_score'], iteration, color='green') + self.logger.log('Buffer size', log_data['buffer_size'], iteration, color='blue') + self.logger.log('Buffer average score', log_data['buffer_avg_score'], iteration, color='cyan') + self.logger.log('Buffer average evaluations', log_data['buffer_avg_evals'], iteration, color='orange') + self.logger.log('Total samples', log_data['total_samples'], iteration, color='yellow') + self.logger.log('Total proposals', self.total_proposals, iteration, color='red') + + print_color(f"Log @ Iter {iteration}: Best score in buffer: {log_data['best_score']:.4f}, Gen method: {generation_method}, Buffer size: {len(self.buffer)}, Total samples: {total_samples}", 'green') + + if test_dataset is not None and iteration % eval_frequency == 0: + try: + # Save current agent parameters + current_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + + # Find the best candidate in the buffer (highest mean score) + best_candidate = self._get_best_candidate_from_buffer(self.buffer) + if not best_candidate: + print_color(f"Iter {iteration}: No valid candidate for test evaluation.", 'yellow') + continue + + # Load best candidate's parameters into the agent for evaluation + self.optimizer.update(best_candidate['params']) + + # Evaluate the best candidate on test set + test_score = self.evaluate(self.agent, guide, test_dataset['inputs'], test_dataset['infos'], + min_score=self.min_score, num_threads=num_threads, + description=f"Evaluating best candidate (iteration {iteration})") + + # Restore original agent parameters + self.optimizer.update(current_params) + + self.logger.log('Test score', test_score, iteration, color='green') + except Exception as e: + print_color(f"Iter {iteration}: Test evaluation failed: {e}", 'red') + + if save_frequency is not None and iteration % save_frequency == 0 and self.buffer: + try: + best_overall_candidate_entry = max(self.buffer, key=lambda c: (c['score_sum'] / (c['eval_count'] if c['eval_count'] > 0 else 1E-9))) + self.optimizer.update(best_overall_candidate_entry['params']) + if hasattr(self, 'save_agent'): + self.save_agent(save_path, iteration) + best_mean_score_for_save = best_overall_candidate_entry['score_sum'] / (best_overall_candidate_entry['eval_count'] if best_overall_candidate_entry['eval_count'] > 0 else 1E-9) + print_color(f"Iter {iteration}: Saved agent based on best candidate in buffer (Mean Score: {best_mean_score_for_save:.4f}).", 'green') + else: + print_color(f"Iter {iteration}: save_agent method not found, skipping save.", 'yellow') + except Exception as e: + print_color(f"Iter {iteration}: Agent save failed: {e}", 'red') + + except Exception as e: + print_color(f"Iter {iteration}: Iteration failed with error: {e}. Skipping to next iteration.", 'red') + self.logger.log('Iteration error', str(e), iteration, color='red') + continue + + print_color("UCB-LLM search finished.", 'blue') + + final_best_candidate = max(self.buffer, key=lambda c: (c['score_sum'] / (c['eval_count'] if c['eval_count'] > 0 else 1E-9))) + final_best_score = final_best_candidate['score_sum'] / (final_best_candidate['eval_count'] if final_best_candidate['eval_count'] > 0 else 1E-9) + final_best_evals = final_best_candidate['eval_count'] + print_color(f"Final best candidate: Mean Score {final_best_score:.4f}, Evals {final_best_evals}", 'green') + + self.optimizer.update(final_best_candidate['params']) + + return metrics, float(final_best_score) + + def select(self, buffer): + '''Selects candidate with highest UCB score.''' + if not buffer: return None + return max(buffer, key=lambda c: c.get('ucb_score', -float('inf'))) + + +class UCBSearchFunctionApproximationAlgorithm(UCBSearchAlgorithm): + """ + UCB Search Algorithm that uses LLM function approximation to select candidates. + """ + + def __init__(self, llm_model,num_samples_in_prompt:int=5, *args, **kwargs): + super().__init__(*args, **kwargs) + self.llm_model = llm_model + self.llm = LLM(model=self.llm_model) + self.num_samples_in_prompt = num_samples_in_prompt + print_color(f"Initialized UCBSearchFunctionApproximationAlgorithm with LLM model={self.llm_model}", "cyan") + + def select(self, buffer): + """Generate a new candidate entry using LLM. Note: this doesn't add it to the buffer.""" + new_action_params = self._llm_generate_candidate() + new_candidate_entry = { + 'params': new_action_params, + 'score_sum': 0, + 'eval_count': 0, + 'ucb_score': 0.0, + 'iteration_created': 0 + } + return new_candidate_entry + + def _llm_generate_candidate(self) -> Optional[Dict[trace.nodes.ParameterNode, str]]: + """ + Prompts an LLM with current buffer candidates to generate new string values for parameters. + Returns a dictionary mapping ParameterNode objects to new string values, or None on failure. + """ + print_color("Attempting to generate candidate using LLM...", "blue") + if not self.buffer: + print_color("LLM generation: Buffer is empty, cannot provide context to LLM.", "yellow") + return None + + sorted_buffer = sorted(list(self.buffer), key=lambda c: c.get('ucb_score', -float('inf')), reverse=True) + # Include first, last, and evenly spaced middle candidates + if len(sorted_buffer) <= self.num_samples_in_prompt: + prompt_candidates = sorted_buffer + elif self.num_samples_in_prompt <= 2: + # If only 1-2 samples requested, take first and optionally last + prompt_candidates = sorted_buffer[:self.num_samples_in_prompt] + else: + # Take first, last, and evenly spaced middle candidates + prompt_candidates = [sorted_buffer[0]] # First (highest UCB) + if self.num_samples_in_prompt > 2: + # Calculate indices for middle candidates + middle_count = self.num_samples_in_prompt - 2 # Exclude first and last + if middle_count > 0 and len(sorted_buffer) > 2: + # Evenly space middle candidates between index 1 and len-2 + middle_indices = [int(1 + i * (len(sorted_buffer) - 2) / (middle_count + 1)) + for i in range(1, middle_count + 1)] + prompt_candidates.extend([sorted_buffer[i] for i in middle_indices]) + prompt_candidates.append(sorted_buffer[-1]) # Last (lowest UCB) + + serializable_candidate_summaries = [] + for cand_entry in prompt_candidates: + summary = { + "parameters": {getattr(p,'py_name'): copy.deepcopy(p.data) for p in cand_entry['params']}, + "eval_count": cand_entry['eval_count'], + "ucb_score": round(cand_entry.get('ucb_score',0), 4), + } + serializable_candidate_summaries.append(summary) + + example_param_structure_json_str = {getattr(p,'py_name'): copy.deepcopy(p.data) for p in self.agent.parameters()} + + prompt_messages = [ + {"role": "system", "content": "You are an expert in model optimization. Your task is to propose new string values for model parameters with high UCB scores. Please output ONLY a valid JSON dictionary where keys are parameter names and values are the new string values for those parameters, matching the example structure provided. Do not add any explanations or markdown formatting around the JSON."}, + {"role": "user", "content": f"Here are some current candidates from the search buffer and their statistics:\\n{serializable_candidate_summaries}\\n\\nHere is an example of the required JSON output structure (parameter names as keys, new string values as values):\\n{example_param_structure_json_str}\\n\\nPlease generate a new set of parameters in exactly the same JSON format. Make sure use double quotes for the keys and values."} + ] + + print_color(f"LLM prompt (summary): {len(prompt_candidates)} candidates, structure example provided.", "magenta") + response_format = {"type": "json_object"} + llm_response = self.llm(prompt_messages, response_format=response_format) + llm_response_str = llm_response.choices[0].message.content + + if not llm_response_str: + print_color("LLM returned an empty response.", "red") + return None + + cleaned_llm_response_str = llm_response_str.strip() + + try: + llm_params_raw = json.loads(cleaned_llm_response_str) + self.total_proposals += 1 + except json.JSONDecodeError as e: + print_color(f"JSON parsing attempts failed: {e}", "red") + print_color("Returning the candidate with the highest UCB score in the buffer.", "red") + return max(self.buffer, key=lambda c: c.get('ucb_score', -float('inf')))['params'] + + if not isinstance(llm_params_raw, dict): + print_color(f"LLM output was not a JSON dictionary after parsing: {type(llm_params_raw)}", "red") + print_color("Returning the candidate with the highest UCB score in the buffer.", "red") + return max(self.buffer, key=lambda c: c.get('ucb_score', -float('inf')))['params'] + + candidate_params_dict = self.construct_update_dict(llm_params_raw) + return candidate_params_dict + + def construct_update_dict(self, suggestion: Dict[str, Any]) -> Dict[ParameterNode, Any]: + """Convert the suggestion in text into the right data type.""" + update_dict = {} + for node in self.agent.parameters(): + if node.trainable and node.py_name in suggestion: + try: + formatted_suggestion = suggestion[node.py_name] + if type(formatted_suggestion) == str and 'def' in formatted_suggestion: + formatted_suggestion = format_str(formatted_suggestion, mode=FileMode()) + update_dict[node] = type(node.data)(formatted_suggestion) + except (ValueError, KeyError) as e: + if getattr(self, 'ignore_extraction_error', False): + warnings.warn( + f"Cannot convert the suggestion '{suggestion[node.py_name]}' for {node.py_name} to the right data type" + ) + else: + raise e + return update_dict From fd9ad28a6996b96db5c25ddca5a87d4ed5e21435 Mon Sep 17 00:00:00 2001 From: chinganc Date: Wed, 2 Jul 2025 20:07:38 +0000 Subject: [PATCH 079/314] Fix the bug in test_optoprime_udpate.py --- tests/unit_tests/test_optoprime_update.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit_tests/test_optoprime_update.py b/tests/unit_tests/test_optoprime_update.py index 0b273d2f..28d185dd 100644 --- a/tests/unit_tests/test_optoprime_update.py +++ b/tests/unit_tests/test_optoprime_update.py @@ -8,6 +8,7 @@ def test_json_keys(): """ Test that the OptoPrimeV2 class correctly initializes with json_keys. """ + trace.GRAPH.clear() param = trace.node(1, trainable=True) def callable(messages, **kwargs): From 9cd7d2c5c76f7de97679ff375be93638c6b1bec6 Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 2 Jul 2025 13:54:26 -0700 Subject: [PATCH 080/314] add XML format --- opto/optimizers/optoprime_v2.py | 61 ++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index e81eba2c..d4214739 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -31,38 +31,46 @@ class OptoPrimeV2(OptoPrime): In #Variables, #Inputs, #Outputs, and #Others, the format is: - + (data_type) variable_name = value - + If `(data_type)` is `code`, it means `{value}` is the source code of a python code, which may include docstring and definitions. """ ) # Optimization - default_objective = "You need to change the of the variables in #Variables to improve the output in accordance to #Feedback." + default_objective = "You need to change the `value` of the variables in #Variables to improve the output in accordance to #Feedback." output_format_prompt = dedent( """ Output_format: Your output should be in the following XML/HTML format: - - Your reasoning - + + Your reasoning on why you made the decision to suggest a new value. You can also use it to explain why you didn't + - "suggestion": {{ - : , - : , - }} - }} - - In "reasoning", explain the problem: 1. what the #Instruction means 2. what the #Feedback on #Output means to #Variables considering how #Variables are used in #Code and other values in #Documentation, #Inputs, #Others. 3. Reasoning about the suggested changes in #Variables (if needed) and the expected result. + + variable_1_name + + new_value + ... + + + + + variable_2_name + + new_value + ... + + - If #Instruction asks for an answer, write it down in "answer". + In , explain the problem: 1. what the #Instruction means 2. what the #Feedback on #Output means to #Variables considering how #Variables are used in #Code and other values in #Documentation, #Inputs, #Others. 3. Reasoning about the suggested changes in #Variables (if needed) and the expected result. - If you need to suggest a change in the values of #Variables, write down the suggested values in "suggestion". Remember you can change only the values in #Variables, not others. When of a variable is (code), you should write the new definition in the format of python code without syntax errors, and you should not change the function name or the function signature. + If you need to suggest a change in the values of #Variables, write down the suggested values in . Remember you can change only the values in #Variables, not others. When of a variable is (code), you should write the new definition in the format of python code without syntax errors, and you should not change the function name or the function signature. - If no changes or answer are needed, just output TERMINATE. + If no changes are needed, just output TERMINATE. """ ) @@ -156,9 +164,16 @@ def __init__( ) self.example_response = dedent( """ - {"reasoning": 'In this case, the desired response would be to change the value of input a to 14, as that would make the code return 10.', - "suggestion": {"a": 10} - } + + In this case, the desired response would be to change the value of input a to 14, as that would make the code return 10. + + + + a + + 10 + + """ ) @@ -176,9 +191,9 @@ def repr_node_value(node_dict): temp_list = [] for k, v in node_dict.items(): if "__code" not in k: - temp_list.append(f"\n({type(v[0]).__name__}) {k}={v[0]}\n") + temp_list.append(f"\n({type(v[0]).__name__}) {k}={v[0]}\n") else: - temp_list.append(f"\n(code) {k}:{v[0]}\n") + temp_list.append(f"\n(code) {k}:{v[0]}\n") return "\n".join(temp_list) @staticmethod @@ -187,10 +202,10 @@ def repr_node_constraint(node_dict): for k, v in node_dict.items(): if "__code" not in k: if v[1] is not None: - temp_list.append(f"\n({type(v[0]).__name__}) {k}: {v[1]}\n") + temp_list.append(f"\n({type(v[0]).__name__}) {k}: {v[1]}\n") else: if v[1] is not None: - temp_list.append(f"\n(code) {k}: {v[1]}\n") + temp_list.append(f"\n(code) {k}: {v[1]}\n") return "\n".join(temp_list) def construct_prompt(self, summary, mask=None, *args, **kwargs): From c6c0f09e896fda5852e653ed84bf0efb25e55143 Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 2 Jul 2025 15:00:16 -0700 Subject: [PATCH 081/314] add XML parsing --- opto/optimizers/optoprime_v2.py | 129 ++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index d4214739..cdde6643 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -12,6 +12,56 @@ from opto.optimizers.buffers import FIFOBuffer import copy +import re +from typing import Dict, Any + + +def extract_xml_like_data(text: str) -> Dict[str, Any]: + """ + Extract thinking content and improved variables from text containing XML-like tags. + + Args: + text (str): Text containing and tags + + Returns: + Dict containing: + - 'thinking': content of element + - 'variables': dict mapping variable names to their values + """ + result = { + 'thinking': '', + 'variables': {} + } + + # Extract thinking content + think_pattern = r'(.*?)' + think_match = re.search(think_pattern, text, re.DOTALL) + if think_match: + result['thinking'] = think_match.group(1).strip() + + # Extract improved variables + # Find all improved_variable blocks + var_pattern = r'(.*?)' + var_matches = re.findall(var_pattern, text, re.DOTALL) + + for var_content in var_matches: + # Extract name + name_pattern = r'(.*?)' + name_match = re.search(name_pattern, var_content, re.DOTALL) + + # Extract value + value_pattern = r'(.*?)' + value_match = re.search(value_pattern, var_content, re.DOTALL) + + if name_match and value_match: + var_name = name_match.group(1).strip() + var_value = value_match.group(1).strip() + + if var_name: # Only add if name is not empty + result['variables'][var_name] = var_value + + return result + class OptoPrimeV2(OptoPrime): # This is generic representation prompt, which just explains how to read the problem. representation_prompt = dedent( @@ -255,3 +305,82 @@ def construct_prompt(self, summary, mask=None, *args, **kwargs): self.memory.add((summary.variables, summary.user_feedback)) return system_prompt, user_prompt + + def extract_llm_suggestion(self, response: str): + """Extract the suggestion from the response.""" + + suggestion = extract_xml_like_data(response) + + # attempt_n = 0 + # while attempt_n < 2: + # try: + # suggestion = json.loads(response)["suggestion"] + # break + # except json.JSONDecodeError: + # # Remove things outside the brackets + # response = re.findall(r"{.*}", response, re.DOTALL) + # if len(response) > 0: + # response = response[0] + # attempt_n += 1 + # except Exception: + # attempt_n += 1 + + # if not isinstance(suggestion, dict): + # suggestion = {} + # + # if len(suggestion) == 0: + # # we try to extract key/value separately and return it as a dictionary + # pattern = r'"suggestion"\s*:\s*\{(.*?)\}' + # suggestion_match = re.search(pattern, str(response), re.DOTALL) + # if suggestion_match: + # suggestion = {} + # # Extract the entire content of the suggestion dictionary + # suggestion_content = suggestion_match.group(1) + # # Regex to extract each key-value pair; + # # This scheme assumes double quotes but is robust to missing commas at the end of the line + # pair_pattern = r'"([a-zA-Z0-9_]+)"\s*:\s*"(.*)"' + # # Find all matches of key-value pairs + # pairs = re.findall(pair_pattern, suggestion_content, re.DOTALL) + # for key, value in pairs: + # suggestion[key] = value + + if len(suggestion) == 0: + if not self.ignore_extraction_error: + print("Cannot extract suggestion from LLM's response:") + print(response) + + # if the suggested value is a code, and the entire code body is empty (i.e., not even function signature is present) + # then we remove such suggestion + keys_to_remove = [] + for key, value in suggestion.items(): + if "__code" in key and value.strip() == "": + keys_to_remove.append(key) + for key in keys_to_remove: + del suggestion[key] + + return suggestion + + def call_llm( + self, + system_prompt: str, + user_prompt: str, + verbose: Union[bool, str] = False, + max_tokens: int = 4096, + ): + """Call the LLM with a prompt and return the response.""" + if verbose not in (False, "output"): + print("Prompt\n", system_prompt + user_prompt) + + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt}, + ] + + response = self.llm(messages=messages, max_tokens=max_tokens) + + response = response.choices[0].message.content + + if verbose: + print("LLM response:\n", response) + return response + From 88ccd0adec0bc1d5ed0450390cc0696bdb1db408 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 3 Jul 2025 19:58:12 +0000 Subject: [PATCH 082/314] Move copy def to ParameterContainer and update it to support nesting. --- opto/trace/containers.py | 27 +++++++++++- opto/trace/modules.py | 20 +++------ tests/unit_tests/test_modules.py | 76 ++++++++++++++++---------------- 3 files changed, 70 insertions(+), 53 deletions(-) diff --git a/opto/trace/containers.py b/opto/trace/containers.py index a216118d..402e39c8 100644 --- a/opto/trace/containers.py +++ b/opto/trace/containers.py @@ -2,6 +2,7 @@ from collections import UserDict, UserList from opto.trace.nodes import ParameterNode import functools +import copy class NodeContainer: @@ -49,7 +50,7 @@ def parameters_dict(self): method = attr.func.__self__ if trainable_method(method): parameters[name] = method.parameter - if trainable_method(attr): # method attribute + elif trainable_method(attr): # method attribute parameters[name] = attr.parameter elif isinstance(attr, ParameterNode): parameters[name] = attr @@ -63,6 +64,30 @@ def parameters_dict(self): return parameters # include both trainable and non-trainable parameters + def copy(self): + """Return a deep copy of the ParameterContainer except for the parameters + are set to the originals.""" + + # NOTE This current code is not optimized for speed; it does extra traversals and copying. + + new_container = copy.deepcopy(self) + + # Set the parameters to the original ones + for name, attr in inspect.getmembers(self): + if isinstance(attr, functools.partial): # this is a class method + method = attr.func.__self__ + if trainable_method(method): + new_attr = getattr(new_container, name) + setattr(new_attr.func.__self__, 'parameter', method.parameter) + elif trainable_method(attr): # method attribute + new_attr = getattr(new_container, name) + new_attr.parameter = attr.parameter + elif isinstance(attr, ParameterNode): + setattr(new_container, name, attr) + elif isinstance(attr, ParameterContainer): + setattr(new_container, name, attr.copy()) # recursion + + return new_container class Seq(UserList, ParameterContainer): """ diff --git a/opto/trace/modules.py b/opto/trace/modules.py index 6b7f0114..9310c2ff 100644 --- a/opto/trace/modules.py +++ b/opto/trace/modules.py @@ -16,6 +16,7 @@ def model(cls): """ class ModelWrapper(cls, Module): + def model_dump(self, filename, projections: Optional[List[Projection]] = None): """Dump the model's source code to a file, including all methods and attributes. Ignores dunder methods unless they were overridden by the user. @@ -24,7 +25,7 @@ def model_dump(self, filename, projections: Optional[List[Projection]] = None): projections = [BlackCodeFormatter()] trace_model_body = f"class {cls.__name__}:\n" - + # Get all members of the class all_members = inspect.getmembers(self) cls_members = inspect.getmembers(cls) @@ -39,7 +40,7 @@ def model_dump(self, filename, projections: Optional[List[Projection]] = None): if name not in cls_member_names: continue - + # Include if it's not a dunder method or if it was overridden if not name.startswith('__'): filtered_members.append((name, member)) @@ -72,7 +73,7 @@ def model_dump(self, filename, projections: Optional[List[Projection]] = None): source = textwrap.dedent(source) indented = textwrap.indent(source, " ") trace_model_body += indented - + if i < len(all_members) - 1: trace_model_body += "\n" # only one newline between members @@ -80,7 +81,7 @@ def model_dump(self, filename, projections: Optional[List[Projection]] = None): # WARNING: there might be corner cases that this static analysis does not cover import re node_pattern = r'self\.(\w+)\s*=\s*node\([^)]*\)' - + def replace_node(match): attr_name = match.group(1) if hasattr(self, attr_name): @@ -88,7 +89,7 @@ def replace_node(match): if hasattr(attr, 'data'): return f"self.{attr_name} = {attr.data}" return match.group(0) # Return original if replacement not possible - + trace_model_body = re.sub(node_pattern, replace_node, trace_model_body) trace_model_body = functools.reduce(lambda body, proj: proj.project(body), projections, trace_model_body) @@ -107,15 +108,6 @@ def forward(self, *args, **kwargs): def __call__(self, *args, **kwargs): return self.forward(*args, **kwargs) - - def copy(self): - """Return a deep copy of the module except for the parameters - are set to the originals.""" - new_module = copy.deepcopy(self) - for k, v in self.parameters_dict().items(): - if hasattr(new_module, k): - setattr(new_module, k, v) - return new_module def save(self, file_name: str): """Save the parameters of the model to a pickle file.""" diff --git a/tests/unit_tests/test_modules.py b/tests/unit_tests/test_modules.py index ae1e9267..a1bbc17f 100644 --- a/tests/unit_tests/test_modules.py +++ b/tests/unit_tests/test_modules.py @@ -348,62 +348,62 @@ def __init__(self): super().__init__() self.offset = node(2, trainable=True) self.multiplier = node(1.5, trainable=True) - + @bundle(trainable=True) def add(self, x, y): """Add two numbers with an offset""" return x + y + self.offset - + @bundle(trainable=True) def multiply(self, x, y): """Multiply two numbers with a multiplier""" return x * y * self.multiplier - + # Create instance and modify parameters calc = StrangeCalculator() calc.offset._data = 3 calc.multiplier._data = 2.0 calc.add.parameter._data = "def add(self, x, y):\n return x + y + self.offset + 1" calc.multiply.parameter._data = "def multiply(self, x, y):\n return x * y * self.multiplier * 2" - + # Dump the model temp_file = "temp_calculator.py" try: calc.model_dump(temp_file) - + # Import the dumped class import importlib.util spec = importlib.util.spec_from_file_location("temp_calculator", temp_file) temp_module = importlib.util.module_from_spec(spec) spec.loader.exec_module(temp_module) - + # Get the imported class ImportedCalculator = temp_module.StrangeCalculator - + # Create instance and test functionality imported_calc = ImportedCalculator() - + # Test the modified behavior result_add = imported_calc.add(5, 3) result_multiply = imported_calc.multiply(4, 2) - + # Verify the results match our expected modified behavior # add: 5 + 3 + 3 + 1 = 12 # multiply: 4 * 2 * 2.0 * 2 = 32 assert result_add == 12, f"Expected 12, got {result_add}" assert result_multiply == 32, f"Expected 32, got {result_multiply}" - + # Verify the attributes have the correct values assert imported_calc.offset == 3 assert imported_calc.multiplier == 2.0 - + finally: if os.path.exists(temp_file): os.remove(temp_file) def test_copy_function(): """Test the copy function of Module class.""" - + @model class TestCopyClass: def __init__(self): @@ -412,76 +412,76 @@ def __init__(self): self.regular_attr = "original_value" self.list_attr = [1, 2, 3] self.dict_attr = {"key": "value"} - + @bundle(trainable=True) def test_method(self, x): return x + self._param - + def forward(self, x): return self.test_method(x) - + # Create original instance original = TestCopyClass() original.regular_attr = "modified_value" original.list_attr.append(4) original.dict_attr["new_key"] = "new_value" - + # Create a copy copied = original.copy() - + # Test that it's a different object assert copied is not original - + # Test that regular attributes are copied (deep copy) assert copied.regular_attr == "modified_value" assert copied.list_attr == [1, 2, 3, 4] assert copied.dict_attr == {"key": "value", "new_key": "new_value"} - + # Test that parameters are references to the original parameters assert copied._param is original._param assert copied.test_method.parameter is original.test_method.parameter - + # Test that modifying the original parameter affects the copy original._param._data = 20 assert copied._param._data == 20 - + # Test that modifying the copy's parameter affects the original copied._param._data = 30 assert original._param._data == 30 - + # Test that the copy can still function result = copied.forward(5) assert result._data == 35 # 5 + 30 - + # Test that modifying regular attributes doesn't affect the original copied.regular_attr = "copy_only_value" assert original.regular_attr == "modified_value" - + # Test that modifying list/dict attributes doesn't affect the original (deep copy) copied.list_attr.append(5) assert len(original.list_attr) == 4 assert len(copied.list_attr) == 5 - + copied.dict_attr["copy_only"] = "copy_value" assert "copy_only" not in original.dict_attr assert "copy_only" in copied.dict_attr def test_copy_function_with_nested_modules(): """Test the copy function with nested modules.""" - + @model class NestedModule: def __init__(self): super().__init__() self._nested_param = node(5, trainable=True) - + @bundle(trainable=True) def nested_method(self, x): return x * self._nested_param - + def forward(self, x): return self.nested_method(x) - + @model class ParentModule: def __init__(self): @@ -489,37 +489,37 @@ def __init__(self): self._param = node(10, trainable=True) self._nested = NestedModule() self.regular_attr = "parent_value" - + @bundle(trainable=True) def parent_method(self, x): return self._nested.forward(x) + self._param - + def forward(self, x): return self.parent_method(x) - + # Create original instance original = ParentModule() original.regular_attr = "modified_parent" original._nested._nested_param._data = 7 - + # Create a copy copied = ParentModule() copied = original.copy() - + # Test that it's a different object assert copied is not original - + # Test that nested module is copied but parameters are references assert copied._nested is not original._nested # Different object assert copied._nested._nested_param is original._nested._nested_param # Same parameter reference - + # Test that regular attributes are copied assert copied.regular_attr == "modified_parent" - + # Test that modifying nested parameter affects both original._nested._nested_param._data = 8 assert copied._nested._nested_param._data == 8 - + # Test that the copy can still function result = copied.forward(3) assert result._data == 34 # (3 * 8) + 10 From 24aa592314fb96e29f0fa9795e48d0a01e3187e3 Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 8 Jul 2025 22:13:00 +0000 Subject: [PATCH 083/314] Make DataLoader an iterator and add a sample method for continuously sampling. --- opto/trainer/loader.py | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/opto/trainer/loader.py b/opto/trainer/loader.py index e61532b7..90d738f9 100644 --- a/opto/trainer/loader.py +++ b/opto/trainer/loader.py @@ -23,17 +23,32 @@ def __init__(self, dataset, batch_size=1, replacement=False, shuffle=True): self.replacement = replacement self.shuffle = shuffle self._indices = self._update_indices() + self._i = 0 def __iter__(self): - indices = self._indices - for i in range(0, len(indices), self.batch_size): - xs = [ self.dataset['inputs'][ind] for ind in indices[i:i + self.batch_size] ] - infos = [self.dataset['infos'][ind] for ind in indices[i:i + self.batch_size] ] - yield xs, infos - - if self.shuffle: - self._indices = self._update_indices() + return self + + def __next__(self): + """ Get the next batch of data """ + if self._i >= len(self._indices): + if self.shuffle: + self._indices = self._update_indices() + self._i = 0 + raise StopIteration + indices = self._indices[self._i: min(self._i + self.batch_size, len(self._indices))] + xs = [self.dataset['inputs'][ind] for ind in indices] + infos = [self.dataset['infos'][ind] for ind in indices] + self._i += self.batch_size + return xs, infos def _update_indices(self): N = len(self.dataset['inputs']) return np.random.choice(N, size=N, replace=self.replacement) + + def sample(self): + """ Sample a batch of data from the dataset """ + try: + xs, infos = next(self) + return xs, infos + except StopIteration: + return self.sample() From a9d3eb90b43e866194078da29bd1785892290e3f Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 8 Jul 2025 22:25:29 +0000 Subject: [PATCH 084/314] Add test_dataloader.py --- tests/unit_tests/test_dataloader.py | 91 +++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 tests/unit_tests/test_dataloader.py diff --git a/tests/unit_tests/test_dataloader.py b/tests/unit_tests/test_dataloader.py new file mode 100644 index 00000000..8d4db810 --- /dev/null +++ b/tests/unit_tests/test_dataloader.py @@ -0,0 +1,91 @@ +from opto.trainer.loader import DataLoader + + + +def run_for_loop(dataloader): + print('Running for-loop') + for i, (inputs, infos) in enumerate(dataloader): + + print(f"Inputs: {inputs}, Infos: {infos}") + + if i == 0: + assert inputs == [1, 2], f"First batch should contain inputs 1 and 2. Get: {inputs}" + assert infos == ['a', 'b'], f"First batch should contain infos 'a' and 'b'. Get: {infos}" + elif i == 1: + assert inputs == [3, 4], f"Second batch should contain inputs 3 and 4. Get: {inputs}" + assert infos == ['c', 'd'], f"Second batch should contain infos 'c' and 'd'. Get: {infos}" + elif i == 2: + assert inputs == [5], f"Third batch should contain input 5. Get: {inputs}" + assert infos == ['e'], f"Third batch should contain info 'e'. Get: {infos}" + +def run_next(dataloader): + inputs, infos = next(dataloader) + print('Running next()') + print(f"Inputs: {inputs}, Infos: {infos}") + + assert inputs == [1, 2], f"First batch should contain inputs 1 and 2. Get: {inputs}" + assert infos == ['a', 'b'], f"First batch should contain infos 'a' and 'b'. Get: {infos}" + + inputs, infos = next(dataloader) + print(f"Inputs: {inputs}, Infos: {infos}") + + assert inputs == [3, 4], f"Second batch should contain inputs 3 and 4. Get: {inputs}" + assert infos == ['c', 'd'], f"Second batch should contain infos 'c' and 'd'. Get: {infos}" + + inputs, infos = next(dataloader) + print(f"Inputs: {inputs}, Infos: {infos}") + + assert inputs == [5], f"Third batch should contain input 5. Get: {inputs}" + assert infos == ['e'], f"Third batch should contain info 'e'. Get: {infos}" + + try: + next(dataloader) + except StopIteration: + print("No more data to iterate over, as expected.") + +def run_sample(dataloader): + + print('Running sample()') + inputs, infos = dataloader.sample() + assert inputs == [1, 2], f"First sample should contain inputs 1 and 2. Get: {inputs}" + assert infos == ['a', 'b'], f"First sample should contain infos 'a' and 'b'. Get: {infos}" + inputs, infos = dataloader.sample() + assert inputs == [3, 4], f"Second sample should contain inputs 3 and 4. Get: {inputs}" + assert infos == ['c', 'd'], f"Second sample should contain infos 'c' and 'd'. Get: {infos}" + inputs, infos = dataloader.sample() + assert inputs == [5], f"Third sample should contain input 5. Get: {inputs}" + assert infos == ['e'], f"Third sample should contain info 'e'. Get: {infos}" + + # At this point, the dataloader should be reset. No need to catch StopIteration when calling sample again + +def test_dataloader(): + + dataset = { + 'inputs': [1, 2, 3, 4, 5], + 'infos': ['a', 'b', 'c', 'd', 'e'] + } + dataloader = DataLoader(dataset, batch_size=2, randomize=False) + + # Test for-loop usage + run_for_loop(dataloader) + run_for_loop(dataloader) # make sure it can be iterated multiple times + + # Test next() usage + run_next(dataloader) + run_next(dataloader) # make sure it can be called multiple times + + # Test sample() method + run_sample(dataloader) + run_sample(dataloader) # make sure it can be called multiple times + + # Test for-loop usage + run_for_loop(dataloader) + run_for_loop(dataloader) # make sure it can be iterated multiple times + + # Test next() usage + run_next(dataloader) + run_next(dataloader) # make sure it can be called multiple times + + # Test sample() method + run_sample(dataloader) + run_sample(dataloader) # make sure it can be called multiple times \ No newline at end of file From 4e13cfc7138518942a1bc7c2232b1e4c164fabc9 Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Wed, 9 Jul 2025 14:32:29 -0500 Subject: [PATCH 085/314] fix a bug in AutoGuide --- opto/trainer/guide.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opto/trainer/guide.py b/opto/trainer/guide.py index cad39f37..8a727d86 100644 --- a/opto/trainer/guide.py +++ b/opto/trainer/guide.py @@ -45,7 +45,7 @@ def metric(self, query: str, response: str, reference: Optional[str] = None, **k """ Exact match metric """ return self.get_feedback(query, response, reference)[0] - def copy(): + def copy(self): """ Create a copy of the guide instance. Returns: From 7a89824b11fbd5a93ecefcd8438e8a3beb770f55 Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Wed, 9 Jul 2025 14:47:57 -0500 Subject: [PATCH 086/314] fix a bug in batch_run --- opto/trainer/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opto/trainer/utils.py b/opto/trainer/utils.py index 16067b78..6f7ccc15 100644 --- a/opto/trainer/utils.py +++ b/opto/trainer/utils.py @@ -113,8 +113,8 @@ def _fun(*args, **kwargs): # deepcopy if it is a trace.Module (as they may have mutable state) # Module.copy() is used to create a new instance with the same parameters - _args = [arg.copy() if isinstance(arg, (Module, AutoGuide)) else arg for arg in args] - _kwargs = {k: v.copy() if isinstance(v, (Module, AutoGuide)) else v for k, v in kwargs.items()} + _args = [[a.copy() if isinstance(a, (Module, AutoGuide)) else a for a in arg ] for arg in args ] + _kwargs = {k: [a.copy() if isinstance(a, (Module, AutoGuide)) else a for a in v ] for k, v in kwargs.items() } # Run the forward function in parallel using asyncio with the same parameters. # Since trace.Node is treated as immutable, we can safely use the same instance. From d6d4a6c4c29f46255ce8f8e52e0cfae35a356e96 Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 9 Jul 2025 15:50:29 -0400 Subject: [PATCH 087/314] adding working XML parsing and new format --- opto/optimizers/optoprime_v2.py | 61 +++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 14 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index cdde6643..023cfffa 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -25,19 +25,19 @@ def extract_xml_like_data(text: str) -> Dict[str, Any]: Returns: Dict containing: - - 'thinking': content of element + - 'reasoning': content of element - 'variables': dict mapping variable names to their values """ result = { - 'thinking': '', + 'reasoning': '', 'variables': {} } # Extract thinking content - think_pattern = r'(.*?)' + think_pattern = r'(.*?)' think_match = re.search(think_pattern, text, re.DOTALL) if think_match: - result['thinking'] = think_match.group(1).strip() + result['reasoning'] = think_match.group(1).strip() # Extract improved variables # Find all improved_variable blocks @@ -62,7 +62,35 @@ def extract_xml_like_data(text: str) -> Dict[str, Any]: return result +# TODO: solution1 -> solution2 -> solution3 +# TODO: param(solution) optimzer.step(solution, "reward is 1, maximize1) -> solution 2 +# TODO: maybe have a trace.train() # simpler even than Algorithm, and cover 80% of use cases + class OptoPrimeV2(OptoPrime): + # TODO: 1. merge variable and constraint + # TODO: 2. Compact representation: some node is very long to describe in text, show a truncated version (long list of data) + # TODO: if the node displaying, if the string description is too long, we should have a limit on character we send to LLM, display truncated format + # TODO: (a flag to set it) + # TODO: LLM has the option to check the value of truncated one + # TODO: turn into a conversation round + # TODO: and show in a separate message + # TODO: 3. Compact representation (compress function) + # TODO: batchify, list of inputs, output is a list of inputs + # TODO: information is redundant + # TODO: idea 1: for each operator, we can identify repeated structure + # TODO: idea 2: for each bundle/op, the user can pass in a callable function, take original output, return a string + # TODO: idea 2-2: each node has a string representation of data, that's what the optimizer should use (this string is fixed) + # TODO: some are too redundant to describe + # TODO: x = a + b + # TODO: y = a + c + # TODO: z = f(x, y) => z = f(a+b, a+c) + # TODO: z = g(a, b, c) + + # TODO: Node level change: format_data_repr(func: Callable[[Node], str]) -> None + # TODO: Check format data representation + # TODO: input would be the data of this node, return would be a string + # TODO: later on optimizer just calls this + # This is generic representation prompt, which just explains how to read the problem. representation_prompt = dedent( """ @@ -92,13 +120,14 @@ class OptoPrimeV2(OptoPrime): # Optimization default_objective = "You need to change the `value` of the variables in #Variables to improve the output in accordance to #Feedback." - output_format_prompt = dedent( + output_format_prompt_template = dedent( """ Output_format: Your output should be in the following XML/HTML format: - - Your reasoning on why you made the decision to suggest a new value. You can also use it to explain why you didn't - + ``` + + Your reasoning on why you made the decision to suggest a new value. You can also use it to explain why you didn't want to change it. + variable_1_name @@ -115,6 +144,7 @@ class OptoPrimeV2(OptoPrime): ... + ``` In , explain the problem: 1. what the #Instruction means 2. what the #Feedback on #Output means to #Variables considering how #Variables are used in #Code and other values in #Documentation, #Inputs, #Others. 3. Reasoning about the suggested changes in #Variables (if needed) and the expected result. @@ -169,6 +199,8 @@ class OptoPrimeV2(OptoPrime): """ ) + # TODO: add an option to replace XML tags if needed by user + default_prompt_symbols = { "variables": "#Variables", "constraints": "#Constraints", @@ -204,19 +236,19 @@ def __init__( instruction=self.default_objective, code="y = add(x=a,y=b)\nz = subtract(x=y, y=c)", documentation="add: add x and y \nsubtract: subtract y from x", - variables="(int) a = 5", + variables="\n(int) a = 5\n", constraints="a: a > 0", - outputs="(int) z = 1", - others="(int) y = 6", - inputs="(int) b = 1\n(int) c = 5", + outputs="\n(int) z = 1\n", + others="\n(int) y = 6\n", + inputs="\n(int) b = 1\n(int) c = 5\n", feedback="The result of the code is not as expected. The result should be 10, but the code returns 1", stepsize=1, ) self.example_response = dedent( """ - + In this case, the desired response would be to change the value of input a to 14, as that would make the code return 10. - + a @@ -226,6 +258,7 @@ def __init__( """ ) + self.output_format_prompt = self.output_format_prompt_template self.include_example = include_example self.max_tokens = max_tokens From b07458398b72dee82da64a6e6e5e7cc5c17abbd9 Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 9 Jul 2025 16:39:11 -0400 Subject: [PATCH 088/314] separate node into two types --- opto/optimizers/optoprime_v2.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index 023cfffa..29a58cf8 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -109,8 +109,21 @@ class OptoPrimeV2(OptoPrime): In #Variables, #Inputs, #Outputs, and #Others, the format is: + For primitive variables (int, float, list, etc.), we express as this: - (data_type) variable_name = value + (data_type) variable_name = value + constraint_expression + + + For functions or code variables, we express as this: + + (data_type) variable_name + + value + + + constraint_expression + If `(data_type)` is `code`, it means `{value}` is the source code of a python code, which may include docstring and definitions. @@ -232,6 +245,17 @@ def __init__( self.ignore_extraction_error = ignore_extraction_error self.llm = llm or LLM() self.objective = objective or self.default_objective + """ + + (data_type) variable_name + + value + + + constraint_expression + + + """ self.example_problem = ProblemInstance.problem_template.format( instruction=self.default_objective, code="y = add(x=a,y=b)\nz = subtract(x=y, y=c)", @@ -240,7 +264,7 @@ def __init__( constraints="a: a > 0", outputs="\n(int) z = 1\n", others="\n(int) y = 6\n", - inputs="\n(int) b = 1\n(int) c = 5\n", + inputs="\n(int) b = 1\n\n\n(int) c = 5\n", feedback="The result of the code is not as expected. The result should be 10, but the code returns 1", stepsize=1, ) From f6c128e649e380d1e41e95fe1085451ac6cff4cb Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 9 Jul 2025 17:43:23 -0400 Subject: [PATCH 089/314] constraint is integrated into the variable now --- opto/optimizers/optoprime_v2.py | 139 ++++++++++++++++++++------------ 1 file changed, 89 insertions(+), 50 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index 29a58cf8..0b648e9c 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -2,7 +2,7 @@ from typing import Any, List, Dict, Union, Tuple from textwrap import dedent, indent from dataclasses import dataclass, asdict -from opto.optimizers.optoprime import OptoPrime, ProblemInstance +from opto.optimizers.optoprime import OptoPrime from opto.trace.nodes import ParameterNode, Node, MessageNode from opto.trace.propagators import TraceGraph, GraphPropagator @@ -15,6 +15,58 @@ import re from typing import Dict, Any +@dataclass +class ProblemInstance: + instruction: str + code: str + documentation: str + variables: str + inputs: str + others: str + outputs: str + feedback: str + + problem_template = dedent( + """ + #Instruction + {instruction} + + #Code + {code} + + #Documentation + {documentation} + + #Variables + {variables} + + #Inputs + {inputs} + + #Others + {others} + + #Outputs + {outputs} + + #Feedback + {feedback} + """ + ) + + def __repr__(self) -> str: + return self.problem_template.format( + instruction=self.instruction, + code=self.code, + documentation=self.documentation, + variables=self.variables, + inputs=self.inputs, + outputs=self.outputs, + others=self.others, + feedback=self.feedback, + ) + + def extract_xml_like_data(text: str) -> Dict[str, Any]: """ @@ -101,7 +153,6 @@ class OptoPrimeV2(OptoPrime): - #Code: the code defined in the problem. - #Documentation: the documentation of each function used in #Code. The explanation might be incomplete and just contain high-level description. You can use the values in #Others to help infer how those functions work. - #Variables: the input variables that you can change. - - #Constraints: the constraints or descriptions of the variables in #Variables. - #Inputs: the values of other inputs to the code, which are not changeable. - #Others: the intermediate values created through the code execution. - #Outputs: the result of the code output. @@ -216,7 +267,6 @@ class OptoPrimeV2(OptoPrime): default_prompt_symbols = { "variables": "#Variables", - "constraints": "#Constraints", "inputs": "#Inputs", "outputs": "#Outputs", "others": "#Others", @@ -298,21 +348,11 @@ def repr_node_value(node_dict): temp_list = [] for k, v in node_dict.items(): if "__code" not in k: - temp_list.append(f"\n({type(v[0]).__name__}) {k}={v[0]}\n") - else: - temp_list.append(f"\n(code) {k}:{v[0]}\n") - return "\n".join(temp_list) - - @staticmethod - def repr_node_constraint(node_dict): - temp_list = [] - for k, v in node_dict.items(): - if "__code" not in k: - if v[1] is not None: - temp_list.append(f"\n({type(v[0]).__name__}) {k}: {v[1]}\n") + constraint_expr = f" ({type(v[0]).__name__}) {k}: {v[1]} " + temp_list.append(f"\n({type(v[0]).__name__}) {k}={v[0]}\n{constraint_expr}\n\n") else: - if v[1] is not None: - temp_list.append(f"\n(code) {k}: {v[1]}\n") + constraint_expr = f"\n(code) {k}: {v[1]}\n" + temp_list.append(f"\n(code) {k}\n\n{v[0]}\n\n{constraint_expr}\n\n") return "\n".join(temp_list) def construct_prompt(self, summary, mask=None, *args, **kwargs): @@ -363,44 +403,43 @@ def construct_prompt(self, summary, mask=None, *args, **kwargs): return system_prompt, user_prompt + def problem_instance(self, summary, mask=None): + mask = mask or [] + return ProblemInstance( + instruction=self.objective if "#Instruction" not in mask else "", + code=( + "\n".join([v for k, v in sorted(summary.graph)]) + if "#Code" not in mask + else "" + ), + documentation=( + "\n".join([f"[{k}] {v}" for k, v in summary.documentation.items()]) + if "#Documentation" not in mask + else "" + ), + variables=( + self.repr_node_value(summary.variables) + if "#Variables" not in mask + else "" + ), + inputs=( + self.repr_node_value(summary.inputs) if "#Inputs" not in mask else "" + ), + outputs=( + self.repr_node_value(summary.output) if "#Outputs" not in mask else "" + ), + others=( + self.repr_node_value(summary.others) if "#Others" not in mask else "" + ), + feedback=summary.user_feedback if "#Feedback" not in mask else "", + ) + + def extract_llm_suggestion(self, response: str): """Extract the suggestion from the response.""" suggestion = extract_xml_like_data(response) - # attempt_n = 0 - # while attempt_n < 2: - # try: - # suggestion = json.loads(response)["suggestion"] - # break - # except json.JSONDecodeError: - # # Remove things outside the brackets - # response = re.findall(r"{.*}", response, re.DOTALL) - # if len(response) > 0: - # response = response[0] - # attempt_n += 1 - # except Exception: - # attempt_n += 1 - - # if not isinstance(suggestion, dict): - # suggestion = {} - # - # if len(suggestion) == 0: - # # we try to extract key/value separately and return it as a dictionary - # pattern = r'"suggestion"\s*:\s*\{(.*?)\}' - # suggestion_match = re.search(pattern, str(response), re.DOTALL) - # if suggestion_match: - # suggestion = {} - # # Extract the entire content of the suggestion dictionary - # suggestion_content = suggestion_match.group(1) - # # Regex to extract each key-value pair; - # # This scheme assumes double quotes but is robust to missing commas at the end of the line - # pair_pattern = r'"([a-zA-Z0-9_]+)"\s*:\s*"(.*)"' - # # Find all matches of key-value pairs - # pairs = re.findall(pair_pattern, suggestion_content, re.DOTALL) - # for key, value in pairs: - # suggestion[key] = value - if len(suggestion) == 0: if not self.ignore_extraction_error: print("Cannot extract suggestion from LLM's response:") From 7993a04fb2cb10ad5f3c8dc97bc58e68778a4080 Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 9 Jul 2025 18:05:15 -0400 Subject: [PATCH 090/314] add expression truncation --- opto/optimizers/optoprime_v2.py | 35 ++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index 0b648e9c..89930ccb 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -119,7 +119,7 @@ def extract_xml_like_data(text: str) -> Dict[str, Any]: # TODO: maybe have a trace.train() # simpler even than Algorithm, and cover 80% of use cases class OptoPrimeV2(OptoPrime): - # TODO: 1. merge variable and constraint + # TODO: 1. merge variable and constraint (DONE) # TODO: 2. Compact representation: some node is very long to describe in text, show a truncated version (long list of data) # TODO: if the node displaying, if the string description is too long, we should have a limit on character we send to LLM, display truncated format # TODO: (a flag to set it) @@ -289,6 +289,7 @@ def __init__( max_tokens=4096, log=True, prompt_symbols=None, + initial_var_char_limit=100, **kwargs, ): super().__init__(parameters, *args, propagator=propagator, **kwargs) @@ -333,6 +334,7 @@ def __init__( """ ) self.output_format_prompt = self.output_format_prompt_template + self.initial_var_char_limit = initial_var_char_limit self.include_example = include_example self.max_tokens = max_tokens @@ -355,6 +357,29 @@ def repr_node_value(node_dict): temp_list.append(f"\n(code) {k}\n\n{v[0]}\n\n{constraint_expr}\n\n") return "\n".join(temp_list) + def repr_node_value_compact(self, node_dict): + temp_list = [] + for k, v in node_dict.items(): + if "__code" not in k: + constraint_expr = f" ({type(v[0]).__name__}) {k}: {v[1]} " + # https://stackoverflow.com/questions/1436703/what-is-the-difference-between-str-and-repr + # node_value = str(v[0])[:self.initial_var_char_limit] + node_value = self.truncate_expression(v[0], self.initial_var_char_limit) + temp_list.append(f"\n({type(v[0]).__name__}) {k}={node_value}\n{constraint_expr}\n\n") + else: + constraint_expr = f"\n(code) {k}: {v[1]}\n" + # node_value = str(v[0])[:self.initial_var_char_limit] + node_value = self.truncate_expression(v[0], self.initial_var_char_limit) + temp_list.append( + f"\n(code) {k}\n\n{node_value}\n\n{constraint_expr}\n\n") + return "\n".join(temp_list) + + def truncate_expression(self, value, limit): + value = str(value) + if len(value) > limit: + return value[:limit] + "...(skipped due to length limit)" + return value + def construct_prompt(self, summary, mask=None, *args, **kwargs): """Construct the system and user prompt.""" system_prompt = ( @@ -418,18 +443,18 @@ def problem_instance(self, summary, mask=None): else "" ), variables=( - self.repr_node_value(summary.variables) + self.repr_node_value_compact(summary.variables) if "#Variables" not in mask else "" ), inputs=( - self.repr_node_value(summary.inputs) if "#Inputs" not in mask else "" + self.repr_node_value_compact(summary.inputs) if "#Inputs" not in mask else "" ), outputs=( - self.repr_node_value(summary.output) if "#Outputs" not in mask else "" + self.repr_node_value_compact(summary.output) if "#Outputs" not in mask else "" ), others=( - self.repr_node_value(summary.others) if "#Others" not in mask else "" + self.repr_node_value_compact(summary.others) if "#Others" not in mask else "" ), feedback=summary.user_feedback if "#Feedback" not in mask else "", ) From bfdfc8adbae0800e2718fd205c3f5b63d58d2128 Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 9 Jul 2025 23:32:29 -0400 Subject: [PATCH 091/314] update XML tag --- opto/optimizers/optoprime_v2.py | 155 ++++++++++++++------------------ 1 file changed, 69 insertions(+), 86 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index 89930ccb..c43584d2 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -28,28 +28,28 @@ class ProblemInstance: problem_template = dedent( """ - #Instruction + # Instruction {instruction} - #Code + # Code {code} - #Documentation + # Documentation {documentation} - #Variables + # Variables {variables} - #Inputs + # Inputs {inputs} - #Others + # Others {others} - #Outputs + # Outputs {outputs} - #Feedback + # Feedback {feedback} """ ) @@ -66,8 +66,6 @@ def __repr__(self) -> str: feedback=self.feedback, ) - - def extract_xml_like_data(text: str) -> Dict[str, Any]: """ Extract thinking content and improved variables from text containing XML-like tags. @@ -93,7 +91,7 @@ def extract_xml_like_data(text: str) -> Dict[str, Any]: # Extract improved variables # Find all improved_variable blocks - var_pattern = r'(.*?)' + var_pattern = r'(.*?)' var_matches = re.findall(var_pattern, text, re.DOTALL) for var_content in var_matches: @@ -158,25 +156,18 @@ class OptoPrimeV2(OptoPrime): - #Outputs: the result of the code output. - #Feedback: the feedback about the code's execution result. - In #Variables, #Inputs, #Outputs, and #Others, the format is: + In `#Variables`, `#Inputs`, `#Outputs`, and `#Others`, the format is: - For primitive variables (int, float, list, etc.), we express as this: - - (data_type) variable_name = value - constraint_expression + For variables we express as this: + + + value + + + constraint_expression + - For functions or code variables, we express as this: - - (data_type) variable_name - - value - - - constraint_expression - - - If `(data_type)` is `code`, it means `{value}` is the source code of a python code, which may include docstring and definitions. """ ) @@ -193,24 +184,24 @@ class OptoPrimeV2(OptoPrime): Your reasoning on why you made the decision to suggest a new value. You can also use it to explain why you didn't want to change it. - - variable_1_name - - new_value - ... - - + + variable_1_name + + new_value + ... + + - - variable_2_name - - new_value - ... - - + + variable_2_name + + new_value + ... + + ``` - In , explain the problem: 1. what the #Instruction means 2. what the #Feedback on #Output means to #Variables considering how #Variables are used in #Code and other values in #Documentation, #Inputs, #Others. 3. Reasoning about the suggested changes in #Variables (if needed) and the expected result. + In , explain the problem: 1. what the #Instruction means 2. what the #Feedback on #Output means to #Variables considering how #Variables are used in #Code and other values in #Documentation, #Inputs, #Others. 3. Reasoning about the suggested changes in #Variables (if needed) and the expected result. If you need to suggest a change in the values of #Variables, write down the suggested values in . Remember you can change only the values in #Variables, not others. When of a variable is (code), you should write the new definition in the format of python code without syntax errors, and you should not change the function name or the function signature. @@ -266,14 +257,14 @@ class OptoPrimeV2(OptoPrime): # TODO: add an option to replace XML tags if needed by user default_prompt_symbols = { - "variables": "#Variables", - "inputs": "#Inputs", - "outputs": "#Outputs", - "others": "#Others", - "feedback": "#Feedback", - "instruction": "#Instruction", - "code": "#Code", - "documentation": "#Documentation", + "variables": "# Variables", + "inputs": "# Inputs", + "outputs": "# Outputs", + "others": "# Others", + "feedback": "# Feedback", + "instruction": "# Instruction", + "code": "# Code", + "documentation": "# Documentation", } def __init__( @@ -296,26 +287,15 @@ def __init__( self.ignore_extraction_error = ignore_extraction_error self.llm = llm or LLM() self.objective = objective or self.default_objective - """ - - (data_type) variable_name - - value - - - constraint_expression - - - """ self.example_problem = ProblemInstance.problem_template.format( instruction=self.default_objective, code="y = add(x=a,y=b)\nz = subtract(x=y, y=c)", documentation="add: add x and y \nsubtract: subtract y from x", - variables="\n(int) a = 5\n", - constraints="a: a > 0", - outputs="\n(int) z = 1\n", - others="\n(int) y = 6\n", - inputs="\n(int) b = 1\n\n\n(int) c = 5\n", + variables="""\n\n5\n\n\na: a > 0\n\n""", + # constraints="a: a > 0", + outputs="""\n\n1\n\n""", + others="""\n\n6\n\n""", + inputs="""\n\n1\n\n\n\n\n5\n\n""", feedback="The result of the code is not as expected. The result should be 10, but the code returns 1", stepsize=1, ) @@ -325,12 +305,12 @@ def __init__( In this case, the desired response would be to change the value of input a to 14, as that would make the code return 10. - - a - - 10 - - + + a + + 10 + + """ ) self.output_format_prompt = self.output_format_prompt_template @@ -351,30 +331,33 @@ def repr_node_value(node_dict): for k, v in node_dict.items(): if "__code" not in k: constraint_expr = f" ({type(v[0]).__name__}) {k}: {v[1]} " - temp_list.append(f"\n({type(v[0]).__name__}) {k}={v[0]}\n{constraint_expr}\n\n") + temp_list.append(f"\n{v[0]}\n{constraint_expr}\n\n") else: - constraint_expr = f"\n(code) {k}: {v[1]}\n" - temp_list.append(f"\n(code) {k}\n\n{v[0]}\n\n{constraint_expr}\n\n") + constraint_expr = f"\n{v[1]}\n" + temp_list.append(f"\n\n{v[0]}\n\n{constraint_expr}\n\n") return "\n".join(temp_list) - def repr_node_value_compact(self, node_dict): + def repr_node_value_compact(self, node_dict, xml_root_tag="node"): temp_list = [] for k, v in node_dict.items(): if "__code" not in k: - constraint_expr = f" ({type(v[0]).__name__}) {k}: {v[1]} " - # https://stackoverflow.com/questions/1436703/what-is-the-difference-between-str-and-repr - # node_value = str(v[0])[:self.initial_var_char_limit] node_value = self.truncate_expression(v[0], self.initial_var_char_limit) - temp_list.append(f"\n({type(v[0]).__name__}) {k}={node_value}\n{constraint_expr}\n\n") + if v[1] is not None: + constraint_expr = f"\n{v[1]}\n" + temp_list.append(f"<{xml_root_tag} name=\"{k}\" type=\"{type(v[0]).__name__}\">\n\n{node_value}\n\n{constraint_expr}\n\n") + else: + temp_list.append(f"<{xml_root_tag} name=\"{k}\" type=\"{type(v[0]).__name__}\">\n\n{node_value}\n\n\n") else: - constraint_expr = f"\n(code) {k}: {v[1]}\n" - # node_value = str(v[0])[:self.initial_var_char_limit] - node_value = self.truncate_expression(v[0], self.initial_var_char_limit) - temp_list.append( - f"\n(code) {k}\n\n{node_value}\n\n{constraint_expr}\n\n") + constraint_expr = f"\n{v[1]}\n" + # we only truncate the function body + signature = v[1].replace("The code should start with:\n", "") + func_body = v[0].replace(signature, "") + node_value = self.truncate_expression(func_body, self.initial_var_char_limit) + temp_list.append(f"<{xml_root_tag} name=\"{k}\" type=\"code\">\n\n{signature}{node_value}\n\n{constraint_expr}\n\n") return "\n".join(temp_list) def truncate_expression(self, value, limit): + # https://stackoverflow.com/questions/1436703/what-is-the-difference-between-str-and-repr value = str(value) if len(value) > limit: return value[:limit] + "...(skipped due to length limit)" @@ -443,7 +426,7 @@ def problem_instance(self, summary, mask=None): else "" ), variables=( - self.repr_node_value_compact(summary.variables) + self.repr_node_value_compact(summary.variables, xml_root_tag="variable") if "#Variables" not in mask else "" ), From d192eee19462a8f992078b5622e001e60db9bdf4 Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 9 Jul 2025 23:44:30 -0400 Subject: [PATCH 092/314] add robust XML parsing --- opto/optimizers/optoprime_v2.py | 116 ++++-- .../unit_tests/test_optimizer_xml_parsing.py | 336 ++++++++++++++++++ 2 files changed, 421 insertions(+), 31 deletions(-) create mode 100644 tests/unit_tests/test_optimizer_xml_parsing.py diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index c43584d2..9990e5a2 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -66,12 +66,81 @@ def __repr__(self) -> str: feedback=self.feedback, ) +def extract_top_level_blocks(text: str, tag: str): + """Extract all top-level ... blocks from text.""" + blocks = [] + start_tag = f'<{tag}>' + end_tag = f'' + stack = [] + start = None + i = 0 + while i < len(text): + if text.startswith(start_tag, i): + if not stack: + start = i + len(start_tag) + stack.append(i) + i += len(start_tag) + elif text.startswith(end_tag, i): + if stack: + stack.pop() + if not stack and start is not None: + blocks.append(text[start:i]) + start = None + i += len(end_tag) + else: + i += 1 + return blocks + +def extract_first_top_level_block(text: str, tag: str): + blocks = extract_top_level_blocks(text, tag) + return blocks[0] if blocks else None + +def strip_nested_blocks(text: str, tag: str) -> str: + """Remove all nested ... blocks from text, leaving only the top-level text.""" + result = '' + start_tag = f'<{tag}>' + end_tag = f'' + stack = [] + i = 0 + last = 0 + while i < len(text): + if text.startswith(start_tag, i): + if not stack: + result += text[last:i] + stack.append(i) + i += len(start_tag) + elif text.startswith(end_tag, i): + if stack: + stack.pop() + if not stack: + last = i + len(end_tag) + i += len(end_tag) + else: + i += 1 + if not stack: + result += text[last:] + return result.strip() + +def extract_reasoning_and_remainder(text: str): + """Extract reasoning and the remainder of the text after reasoning block (if closed). Strip whitespace only if properly closed.""" + start_tag = '' + end_tag = '' + start = text.find(start_tag) + if start == -1: + return '', text + start += len(start_tag) + end = text.find(end_tag, start) + if end == -1: + # If not properly closed, don't strip whitespace to preserve original formatting + return text[start:], '' + return text[start:end].strip(), text[end+len(end_tag):] + def extract_xml_like_data(text: str) -> Dict[str, Any]: """ Extract thinking content and improved variables from text containing XML-like tags. Args: - text (str): Text containing and tags + text (str): Text containing and tags Returns: Dict containing: @@ -83,44 +152,29 @@ def extract_xml_like_data(text: str) -> Dict[str, Any]: 'variables': {} } - # Extract thinking content - think_pattern = r'(.*?)' - think_match = re.search(think_pattern, text, re.DOTALL) - if think_match: - result['reasoning'] = think_match.group(1).strip() - - # Extract improved variables - # Find all improved_variable blocks - var_pattern = r'(.*?)' - var_matches = re.findall(var_pattern, text, re.DOTALL) - - for var_content in var_matches: - # Extract name - name_pattern = r'(.*?)' - name_match = re.search(name_pattern, var_content, re.DOTALL) - - # Extract value - value_pattern = r'(.*?)' - value_match = re.search(value_pattern, var_content, re.DOTALL) - - if name_match and value_match: - var_name = name_match.group(1).strip() - var_value = value_match.group(1).strip() - - if var_name: # Only add if name is not empty + # Extract reasoning and the remainder of the text + reasoning, remainder = extract_reasoning_and_remainder(text) + result['reasoning'] = reasoning + + # Only parse variables from the remainder (i.e., after a closed reasoning tag) + variable_blocks = extract_top_level_blocks(remainder, 'variable') + for var_block in variable_blocks: + name_block = extract_first_top_level_block(var_block, 'name') + value_block = extract_first_top_level_block(var_block, 'value') + # Only add if both name and value tags are present and name is non-empty after stripping + if name_block is not None and value_block is not None: + var_name = strip_nested_blocks(name_block, 'name').strip() + var_value = value_block.strip() if value_block is not None else '' + if var_name: # Only require name to be non-empty, value can be empty result['variables'][var_name] = var_value - return result + # TODO: solution1 -> solution2 -> solution3 # TODO: param(solution) optimzer.step(solution, "reward is 1, maximize1) -> solution 2 # TODO: maybe have a trace.train() # simpler even than Algorithm, and cover 80% of use cases class OptoPrimeV2(OptoPrime): - # TODO: 1. merge variable and constraint (DONE) - # TODO: 2. Compact representation: some node is very long to describe in text, show a truncated version (long list of data) - # TODO: if the node displaying, if the string description is too long, we should have a limit on character we send to LLM, display truncated format - # TODO: (a flag to set it) # TODO: LLM has the option to check the value of truncated one # TODO: turn into a conversation round # TODO: and show in a separate message diff --git a/tests/unit_tests/test_optimizer_xml_parsing.py b/tests/unit_tests/test_optimizer_xml_parsing.py new file mode 100644 index 00000000..def41033 --- /dev/null +++ b/tests/unit_tests/test_optimizer_xml_parsing.py @@ -0,0 +1,336 @@ +import re +import unittest +from typing import Dict, Any +from opto.optimizers.optoprime_v2 import extract_xml_like_data + +""" +1. Nested Tag Handling: The parser now uses a stack-based approach to extract only top-level tags, ignoring nested ones: +- containing nested tags → only extracts the top-level value +- containing nested tags → only extracts the top-level name text +- Complex multi-level nesting → correctly handles all levels + +2. Edge Case Handling: +- Empty tags: Allows variables with empty values if tag is present +- Missing tags: Only adds variables if both and tags are present +- Malformed XML: Handles unclosed tags gracefully +- Whitespace: Proper handling of leading/trailing whitespace +- Special characters: Handles < > & " ' characters correctly +- Duplicate variable names: Later variables override earlier ones + +3. Comprehensive Test Coverage (13 tests): +- Basic parsing functionality +- Nested variable/name/value tags +- Multiple nested levels +- Empty tags +- Missing tags +- Malformed XML +- Special characters +- Whitespace handling +- Duplicate variable names +- No reasoning/variable tags scenarios +""" + + + +class TestXMLParsing(unittest.TestCase): + + def test_basic_parsing(self): + """Test basic parsing functionality""" + text = """ + + This is my reasoning for the changes. + + + + var1 + value1 + + + + var2 + value2 + + """ + + result = extract_xml_like_data(text) + expected = { + 'reasoning': 'This is my reasoning for the changes.', + 'variables': { + 'var1': 'value1', + 'var2': 'value2' + } + } + self.assertEqual(result, expected) + + def test_nested_variable_tags(self): + """Test that only top-level variable tags are extracted""" + text = """ + Reasoning here + + + outer_var + + + inner_var + inner_value + + outer_value + + + """ + + result = extract_xml_like_data(text) + expected = { + 'reasoning': 'Reasoning here', + 'variables': { + 'outer_var': '\n inner_var\n inner_value\n \n outer_value' + } + } + self.assertEqual(result, expected) + + def test_nested_name_tags(self): + """Test that only top-level name tags are extracted""" + text = """ + Reasoning here + + + + inner_name + outer_name + + some_value + + """ + + result = extract_xml_like_data(text) + expected = { + 'reasoning': 'Reasoning here', + 'variables': { + 'outer_name': 'some_value' + } + } + self.assertEqual(result, expected) + + def test_nested_value_tags(self): + """Test that only top-level value tags are extracted""" + text = """ + Reasoning here + + + var_name + + inner_value + outer_value + + + """ + + result = extract_xml_like_data(text) + expected = { + 'reasoning': 'Reasoning here', + 'variables': { + 'var_name': 'inner_value\n outer_value' + } + } + self.assertEqual(result, expected) + + def test_multiple_nested_levels(self): + """Test complex nested structure""" + text = """ + Complex reasoning + + + level1_name + + + level2_name + + + level3_name + level3_value + + level2_value + + + level1_value + + + """ + + result = extract_xml_like_data(text) + expected = { + 'reasoning': 'Complex reasoning', + 'variables': { + 'level1_name': '\n level2_name\n \n \n level3_name\n level3_value\n \n level2_value\n \n \n level1_value' + } + } + self.assertEqual(result, expected) + + def test_empty_tags(self): + """Test handling of empty tags""" + text = """ + + + + + some_value + + + + valid_name + + + """ + + result = extract_xml_like_data(text) + expected = { + 'reasoning': '', + 'variables': { + 'valid_name': '' + } + } + self.assertEqual(result, expected) + + def test_missing_tags(self): + """Test handling of missing tags""" + text = """ + Some reasoning + + + var1 + + + + value2 + + """ + + result = extract_xml_like_data(text) + expected = { + 'reasoning': 'Some reasoning', + 'variables': {} + } + self.assertEqual(result, expected) + + def test_malformed_xml(self): + """Test handling of malformed XML""" + text = """ + Reasoning + + var1 + value1 + + """ + + result = extract_xml_like_data(text) + expected = { + 'reasoning': 'Reasoning\n \n var1\n value1\n \n ', + 'variables': {} + } + self.assertEqual(result, expected) + + def test_no_reasoning_tag(self): + """Test when reasoning tag is missing""" + text = """ + + var1 + value1 + + """ + + result = extract_xml_like_data(text) + expected = { + 'reasoning': '', + 'variables': { + 'var1': 'value1' + } + } + self.assertEqual(result, expected) + + def test_no_variable_tags(self): + """Test when no variable tags are present""" + text = """ + Just reasoning, no variables + """ + + result = extract_xml_like_data(text) + expected = { + 'reasoning': 'Just reasoning, no variables', + 'variables': {} + } + self.assertEqual(result, expected) + + def test_whitespace_handling(self): + """Test proper whitespace handling""" + text = """ + + Reasoning with + multiple lines + + + + var_name + + value with + multiple lines + + + """ + + result = extract_xml_like_data(text) + expected = { + 'reasoning': 'Reasoning with\n multiple lines', + 'variables': { + 'var_name': 'value with\n multiple lines' + } + } + self.assertEqual(result, expected) + + def test_special_characters(self): + """Test handling of special characters""" + text = """ + Reasoning with < > & " ' characters + + + var_with_special_chars + Value with < > & " ' characters + + """ + + result = extract_xml_like_data(text) + expected = { + 'reasoning': 'Reasoning with < > & " \' characters', + 'variables': { + 'var_with_special_chars': 'Value with < > & " \' characters' + } + } + self.assertEqual(result, expected) + + def test_duplicate_variable_names(self): + """Test that later variables override earlier ones with same name""" + text = """ + Reasoning + + + duplicate_var + first_value + + + + duplicate_var + second_value + + """ + + result = extract_xml_like_data(text) + expected = { + 'reasoning': 'Reasoning', + 'variables': { + 'duplicate_var': 'second_value' + } + } + self.assertEqual(result, expected) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From 445919e5fbff26c2b1c8e151422a2bf33a69eb90 Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 9 Jul 2025 23:52:06 -0400 Subject: [PATCH 093/314] update --- opto/optimizers/optoprime_v2.py | 105 ++++++++++++++++---------------- 1 file changed, 54 insertions(+), 51 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index 9990e5a2..f0d77d0b 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -15,57 +15,6 @@ import re from typing import Dict, Any -@dataclass -class ProblemInstance: - instruction: str - code: str - documentation: str - variables: str - inputs: str - others: str - outputs: str - feedback: str - - problem_template = dedent( - """ - # Instruction - {instruction} - - # Code - {code} - - # Documentation - {documentation} - - # Variables - {variables} - - # Inputs - {inputs} - - # Others - {others} - - # Outputs - {outputs} - - # Feedback - {feedback} - """ - ) - - def __repr__(self) -> str: - return self.problem_template.format( - instruction=self.instruction, - code=self.code, - documentation=self.documentation, - variables=self.variables, - inputs=self.inputs, - outputs=self.outputs, - others=self.others, - feedback=self.feedback, - ) - def extract_top_level_blocks(text: str, tag: str): """Extract all top-level ... blocks from text.""" blocks = [] @@ -169,6 +118,60 @@ def extract_xml_like_data(text: str) -> Dict[str, Any]: result['variables'][var_name] = var_value return result +@dataclass +class ProblemInstance: + instruction: str + code: str + documentation: str + variables: str + inputs: str + others: str + outputs: str + feedback: str + + problem_template = dedent( + """ + # Instruction + {instruction} + + # Code + {code} + + # Documentation + {documentation} + + # Variables + {variables} + + # Inputs + {inputs} + + # Others + {others} + + # Outputs + {outputs} + + # Feedback + {feedback} + """ + ) + + def __repr__(self) -> str: + return self.problem_template.format( + instruction=self.instruction, + code=self.code, + documentation=self.documentation, + variables=self.variables, + inputs=self.inputs, + outputs=self.outputs, + others=self.others, + feedback=self.feedback, + ) + +class OptimizerPromptTagSet: + """By inheriting this class and pass into the optimizer. People can change the optimizer documentation""" + pass # TODO: solution1 -> solution2 -> solution3 # TODO: param(solution) optimzer.step(solution, "reward is 1, maximize1) -> solution 2 From 29019a72549f4c3e22b1ead6ae8fe9003884d6f4 Mon Sep 17 00:00:00 2001 From: windweller Date: Thu, 10 Jul 2025 12:14:58 -0400 Subject: [PATCH 094/314] commit an intermediate version (not cleaned up) --- opto/optimizers/optoprime_v2.py | 388 ++++++++++++++++++++++---------- 1 file changed, 274 insertions(+), 114 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index f0d77d0b..9408833e 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -1,8 +1,8 @@ import json from typing import Any, List, Dict, Union, Tuple -from textwrap import dedent, indent from dataclasses import dataclass, asdict -from opto.optimizers.optoprime import OptoPrime +from opto.optimizers.optoprime import OptoPrime, FunctionFeedback +from opto.trace.utils import dedent from opto.trace.nodes import ParameterNode, Node, MessageNode from opto.trace.propagators import TraceGraph, GraphPropagator @@ -15,6 +15,7 @@ import re from typing import Dict, Any + def extract_top_level_blocks(text: str, tag: str): """Extract all top-level ... blocks from text.""" blocks = [] @@ -40,10 +41,12 @@ def extract_top_level_blocks(text: str, tag: str): i += 1 return blocks + def extract_first_top_level_block(text: str, tag: str): blocks = extract_top_level_blocks(text, tag) return blocks[0] if blocks else None + def strip_nested_blocks(text: str, tag: str) -> str: """Remove all nested ... blocks from text, leaving only the top-level text.""" result = '' @@ -70,10 +73,11 @@ def strip_nested_blocks(text: str, tag: str) -> str: result += text[last:] return result.strip() -def extract_reasoning_and_remainder(text: str): + +def extract_reasoning_and_remainder(text: str, tag: str = "reasoning"): """Extract reasoning and the remainder of the text after reasoning block (if closed). Strip whitespace only if properly closed.""" - start_tag = '' - end_tag = '' + start_tag = f'<{tag}>' + end_tag = f'' start = text.find(start_tag) if start == -1: return '', text @@ -82,9 +86,13 @@ def extract_reasoning_and_remainder(text: str): if end == -1: # If not properly closed, don't strip whitespace to preserve original formatting return text[start:], '' - return text[start:end].strip(), text[end+len(end_tag):] + return text[start:end].strip(), text[end + len(end_tag):] + -def extract_xml_like_data(text: str) -> Dict[str, Any]: +def extract_xml_like_data(text: str, reasoning_tag: str = "reasoning", + improved_variable_tag: str = "improved_variable", + name_tag: str = "name", + value_tag: str = "value") -> Dict[str, Any]: """ Extract thinking content and improved variables from text containing XML-like tags. @@ -102,22 +110,23 @@ def extract_xml_like_data(text: str) -> Dict[str, Any]: } # Extract reasoning and the remainder of the text - reasoning, remainder = extract_reasoning_and_remainder(text) + reasoning, remainder = extract_reasoning_and_remainder(text, reasoning_tag) result['reasoning'] = reasoning # Only parse variables from the remainder (i.e., after a closed reasoning tag) - variable_blocks = extract_top_level_blocks(remainder, 'variable') + variable_blocks = extract_top_level_blocks(remainder, improved_variable_tag) for var_block in variable_blocks: - name_block = extract_first_top_level_block(var_block, 'name') - value_block = extract_first_top_level_block(var_block, 'value') + name_block = extract_first_top_level_block(var_block, name_tag) + value_block = extract_first_top_level_block(var_block, value_tag) # Only add if both name and value tags are present and name is non-empty after stripping if name_block is not None and value_block is not None: - var_name = strip_nested_blocks(name_block, 'name').strip() + var_name = strip_nested_blocks(name_block, name_tag).strip() var_value = value_block.strip() if value_block is not None else '' if var_name: # Only require name to be non-empty, value can be empty result['variables'][var_name] = var_value return result + @dataclass class ProblemInstance: instruction: str @@ -169,9 +178,74 @@ def __repr__(self) -> str: feedback=self.feedback, ) -class OptimizerPromptTagSet: - """By inheriting this class and pass into the optimizer. People can change the optimizer documentation""" - pass +class OptimizerPromptSymbolSet: + """ + By inheriting this class and pass into the optimizer. People can change the optimizer documentation + + This divides into three parts: + - Section titles: the title of each section in the prompt + - Node tags: the tags that capture the graph structure (only tag names are allowed to be changed) + - Output format: the format of the output of the optimizer + """ + + variables_section_title = "# Variables" + inputs_section_title = "# Inputs" + outputs_section_title = "# Outputs" + others_section_title = "# Others" + feedback_section_title = "# Feedback" + instruction_section_title = "# Instruction" + code_section_title = "# Code" + documentation_section_title = "# Documentation" + + node_tag = "node" # nodes that are constants in the graph + variable_tag = "variable" # nodes that can be changed + value_tag = "value" # inside node, we have value tag + constraint_tag = "constraint" # inside node, we have constraint tag + + # output format + # Note: we currently don't support extracting format's like "```code```" because we assume supplied tag is name-only, i.e., + reasoning_tag = "reasoning" + improved_variable_tag = "variable" + name_tag = "name" + value_tag = "value" + + # custom output format (this will give the highest degree of freedom) + # once it's set, it will override the default output format + output_format_prompt_instruction = None + + def output_response_extractor(self, response: str) -> Dict[str, Any]: + if self.output_format_prompt_instruction is None: + extracted_data = extract_xml_like_data(response, + reasoning_tag=self.reasoning_tag, + improved_variable_tag=self.improved_variable_tag, + name_tag=self.name_tag, + value_tag=self.value_tag) + return extracted_data + else: + raise NotImplementedError( + "If you supplied a custom output format prompt template, you need to implement your own response extractor") + +class OptimizerPromptSymbolSet2(OptimizerPromptSymbolSet): + variables_section_title = "# Variables" + inputs_section_title = "# Inputs" + outputs_section_title = "# Outputs" + others_section_title = "# Others" + feedback_section_title = "# Feedback" + instruction_section_title = "# Instruction" + code_section_title = "# Code" + documentation_section_title = "# Documentation" + + node_tag = "const" # nodes that are constants in the graph + variable_tag = "var" # nodes that can be changed + value_tag = "data" # inside node, we have value tag + constraint_tag = "constraint" # inside node, we have constraint tag + + # output format + reasoning_tag = "reason" + improved_variable_tag = "var" + name_tag = "name" + value_tag = "data" + # TODO: solution1 -> solution2 -> solution3 # TODO: param(solution) optimzer.step(solution, "reward is 1, maximize1) -> solution 2 @@ -204,63 +278,38 @@ class OptoPrimeV2(OptoPrime): You're tasked to solve a coding/algorithm problem. You will see the instruction, the code, the documentation of each function used in the code, and the feedback about the execution result. Specifically, a problem will be composed of the following parts: - - #Instruction: the instruction which describes the things you need to do or the question you should answer. - - #Code: the code defined in the problem. - - #Documentation: the documentation of each function used in #Code. The explanation might be incomplete and just contain high-level description. You can use the values in #Others to help infer how those functions work. - - #Variables: the input variables that you can change. - - #Inputs: the values of other inputs to the code, which are not changeable. - - #Others: the intermediate values created through the code execution. - - #Outputs: the result of the code output. - - #Feedback: the feedback about the code's execution result. + - {instruction_section_title}: the instruction which describes the things you need to do or the question you should answer. + - {code_section_title}: the code defined in the problem. + - {documentation_section_title}: the documentation of each function used in #Code. The explanation might be incomplete and just contain high-level description. You can use the values in #Others to help infer how those functions work. + - {variables_section_title}: the input variables that you can change. + - {inputs_section_title}: the values of other inputs to the code, which are not changeable. + - {others_section_title}: the intermediate values created through the code execution. + - {outputs_section_title}: the result of the code output. + - {feedback_section_title}: the feedback about the code's execution result. - In `#Variables`, `#Inputs`, `#Outputs`, and `#Others`, the format is: + In `{variables_section_title}`, `{inputs_section_title}`, `{outputs_section_title}`, and `{others_section_title}`, the format is: For variables we express as this: - - - value - - - constraint_expression - - + {variable_expression_format} - If `(data_type)` is `code`, it means `{value}` is the source code of a python code, which may include docstring and definitions. + If `data_type` is `code`, it means `{value_tag}` is the source code of a python code, which may include docstring and definitions. """ ) # Optimization - default_objective = "You need to change the `value` of the variables in #Variables to improve the output in accordance to #Feedback." + default_objective = "You need to change the `{value_tag}` of the variables in {variables_section_title} to improve the output in accordance to {feedback_section_title}." output_format_prompt_template = dedent( """ Output_format: Your output should be in the following XML/HTML format: ``` - - Your reasoning on why you made the decision to suggest a new value. You can also use it to explain why you didn't want to change it. - - - - variable_1_name - - new_value - ... - - - - - variable_2_name - - new_value - ... - - + {output_format} ``` - In , explain the problem: 1. what the #Instruction means 2. what the #Feedback on #Output means to #Variables considering how #Variables are used in #Code and other values in #Documentation, #Inputs, #Others. 3. Reasoning about the suggested changes in #Variables (if needed) and the expected result. + In <{reasoning_tag}>, explain the problem: 1. what the {instruction_section_title} means 2. what the {feedback_section_title} on {outputs_section_title} means to {variables_section_title} considering how {variables_section_title} are used in {code_section_title} and other values in {documentation_section_title}, {inputs_section_title}, {others_section_title}. 3. Reasoning about the suggested changes in {variables_section_title} (if needed) and the expected result. - If you need to suggest a change in the values of #Variables, write down the suggested values in . Remember you can change only the values in #Variables, not others. When of a variable is (code), you should write the new definition in the format of python code without syntax errors, and you should not change the function name or the function signature. + If you need to suggest a change in the values of {variables_section_title}, write down the suggested values in <{improved_variable_tag}>. Remember you can change only the values in {variables_section_title}, not others. When `type` of a variable is `code`, you should write the new definition in the format of python code without syntax errors, and you should not change the function name or the function signature. If no changes are needed, just output TERMINATE. """ @@ -311,53 +360,59 @@ class OptoPrimeV2(OptoPrime): """ ) - # TODO: add an option to replace XML tags if needed by user - - default_prompt_symbols = { - "variables": "# Variables", - "inputs": "# Inputs", - "outputs": "# Outputs", - "others": "# Others", - "feedback": "# Feedback", - "instruction": "# Instruction", - "code": "# Code", - "documentation": "# Documentation", - } def __init__( - self, - parameters: List[ParameterNode], - llm: AbstractModel = None, - *args, - propagator: Propagator = None, - objective: Union[None, str] = None, - ignore_extraction_error: bool = True, # ignore the type conversion error when extracting updated values from LLM's suggestion - include_example=False, # TODO # include example problem and response in the prompt - memory_size=0, # Memory size to store the past feedback - max_tokens=4096, - log=True, - prompt_symbols=None, - initial_var_char_limit=100, - **kwargs, + self, + parameters: List[ParameterNode], + llm: AbstractModel = None, + *args, + propagator: Propagator = None, + objective: Union[None, str] = None, + ignore_extraction_error: bool = True, + # ignore the type conversion error when extracting updated values from LLM's suggestion + include_example=False, # TODO # include example problem and response in the prompt + memory_size=0, # Memory size to store the past feedback + max_tokens=4096, + log=True, + initial_var_char_limit=100, + optimizer_prompt_symbol_set: OptimizerPromptSymbolSet = OptimizerPromptSymbolSet(), + **kwargs, ): super().__init__(parameters, *args, propagator=propagator, **kwargs) self.ignore_extraction_error = ignore_extraction_error self.llm = llm or LLM() - self.objective = objective or self.default_objective - self.example_problem = ProblemInstance.problem_template.format( - instruction=self.default_objective, - code="y = add(x=a,y=b)\nz = subtract(x=y, y=c)", - documentation="add: add x and y \nsubtract: subtract y from x", - variables="""\n\n5\n\n\na: a > 0\n\n""", - # constraints="a: a > 0", - outputs="""\n\n1\n\n""", - others="""\n\n6\n\n""", - inputs="""\n\n1\n\n\n\n\n5\n\n""", - feedback="The result of the code is not as expected. The result should be 10, but the code returns 1", - stepsize=1, - ) + self.objective = objective or self.default_objective.format(value_tag=optimizer_prompt_symbol_set.value_tag, + variables_section_title= optimizer_prompt_symbol_set.variables_section_title, + feedback_section_title= optimizer_prompt_symbol_set.feedback_section_title) + self.initial_var_char_limit = initial_var_char_limit + self.optimizer_prompt_symbol_set = optimizer_prompt_symbol_set + # self.example_problem = ProblemInstance.problem_template.format( + # instruction=self.objective, + # code="y = add(x=a,y=b)\nz = subtract(x=y, y=c)", + # documentation="add: add x and y \nsubtract: subtract y from x", + # variables="""\n\n5\n\n\na: a > 0\n\n""", + # outputs="""\n\n1\n\n""", + # others="""\n\n6\n\n""", + # inputs="""\n\n1\n\n\n\n\n5\n\n""", + # feedback="The result of the code is not as expected. The result should be 10, but the code returns 1", + # stepsize=1, + # ) + self.example_problem_summary = FunctionFeedback(graph=[(1, 'y = add(x=a,y=b)'), (2, "z = subtract(x=y, y=c)")], + documentation={'add': 'This is an add operator of x and y.', + 'subtract': "subtract y from x"}, + others={'y': (6, None)}, + roots={'a': (5, "a > 0"), + 'b': (1, None), + 'c': (5, None)}, + output={'z': (1, None)}, + user_feedback='The result of the code is not as expected. The result should be 10, but the code returns 1' + ) + self.example_problem_summary.variables = {'a': (5, "a > 0")} + self.example_problem_summary.inputs = {'b': (1, None), 'c': (5, None)} + + self.example_problem = self.problem_instance(self.example_problem_summary) self.example_response = dedent( - """ + f""" In this case, the desired response would be to change the value of input a to 14, as that would make the code return 10. @@ -370,17 +425,72 @@ def __init__( """ ) - self.output_format_prompt = self.output_format_prompt_template - self.initial_var_char_limit = initial_var_char_limit self.include_example = include_example self.max_tokens = max_tokens self.log = [] if log else None self.summary_log = [] if log else None self.memory = FIFOBuffer(memory_size) + + self.default_prompt_symbols = { + "variables": self.optimizer_prompt_symbol_set.variables_section_title, + "inputs": self.optimizer_prompt_symbol_set.inputs_section_title, + "outputs": self.optimizer_prompt_symbol_set.outputs_section_title, + "others": self.optimizer_prompt_symbol_set.others_section_title, + "feedback": self.optimizer_prompt_symbol_set.feedback_section_title, + "instruction": self.optimizer_prompt_symbol_set.instruction_section_title, + "code": self.optimizer_prompt_symbol_set.code_section_title, + "documentation": self.optimizer_prompt_symbol_set.documentation_section_title, + } + self.prompt_symbols = copy.deepcopy(self.default_prompt_symbols) - if prompt_symbols is not None: - self.prompt_symbols.update(prompt_symbols) + self.initialize_prompt() + + def initialize_prompt(self): + self.representation_prompt = self.representation_prompt.format( + variable_expression_format=dedent(f""" + <{self.optimizer_prompt_symbol_set.variable_tag} name="variable_name" type="data_type"> + <{self.optimizer_prompt_symbol_set.value_tag}> + value + + <{self.optimizer_prompt_symbol_set.constraint_tag}> + constraint_expression + + + """), + value_tag=self.optimizer_prompt_symbol_set.value_tag, + variables_section_title=self.optimizer_prompt_symbol_set.variables_section_title.replace(" ", ""), + inputs_section_title=self.optimizer_prompt_symbol_set.inputs_section_title.replace(" ", ""), + outputs_section_title=self.optimizer_prompt_symbol_set.outputs_section_title.replace(" ", ""), + feedback_section_title=self.optimizer_prompt_symbol_set.feedback_section_title.replace(" ", ""), + instruction_section_title=self.optimizer_prompt_symbol_set.instruction_section_title.replace(" ", ""), + code_section_title=self.optimizer_prompt_symbol_set.code_section_title.replace(" ", ""), + documentation_section_title=self.optimizer_prompt_symbol_set.documentation_section_title.replace(" ", ""), + others_section_title = self.optimizer_prompt_symbol_set.others_section_title.replace(" ", "") + ) + self.output_format_prompt = self.output_format_prompt_template.format( + output_format=dedent(f""" + <{self.optimizer_prompt_symbol_set.reasoning_tag}> + reasoning + + <{self.optimizer_prompt_symbol_set.improved_variable_tag}> + <{self.optimizer_prompt_symbol_set.name_tag}>variable_name + <{self.optimizer_prompt_symbol_set.value_tag}> + value + + + """), + reasoning_tag=self.optimizer_prompt_symbol_set.reasoning_tag, + improved_variable_tag=self.optimizer_prompt_symbol_set.improved_variable_tag, + instruction_section_title=self.optimizer_prompt_symbol_set.instruction_section_title.replace(" ", ""), + feedback_section_title=self.optimizer_prompt_symbol_set.feedback_section_title.replace(" ", ""), + outputs_section_title=self.optimizer_prompt_symbol_set.outputs_section_title.replace(" ", ""), + code_section_title=self.optimizer_prompt_symbol_set.code_section_title.replace(" ", ""), + documentation_section_title=self.optimizer_prompt_symbol_set.documentation_section_title.replace(" ", ""), + variables_section_title=self.optimizer_prompt_symbol_set.variables_section_title.replace(" ", ""), + inputs_section_title=self.optimizer_prompt_symbol_set.inputs_section_title.replace(" ", ""), + others_section_title=self.optimizer_prompt_symbol_set.others_section_title.replace(" ", "") + ) @staticmethod def repr_node_value(node_dict): @@ -388,29 +498,35 @@ def repr_node_value(node_dict): for k, v in node_dict.items(): if "__code" not in k: constraint_expr = f" ({type(v[0]).__name__}) {k}: {v[1]} " - temp_list.append(f"\n{v[0]}\n{constraint_expr}\n\n") + temp_list.append( + f"\n{v[0]}\n{constraint_expr}\n\n") else: constraint_expr = f"\n{v[1]}\n" - temp_list.append(f"\n\n{v[0]}\n\n{constraint_expr}\n\n") + temp_list.append( + f"\n\n{v[0]}\n\n{constraint_expr}\n\n") return "\n".join(temp_list) - def repr_node_value_compact(self, node_dict, xml_root_tag="node"): + def repr_node_value_compact(self, node_dict, node_tag="node", + value_tag="value", constraint_tag="constraint"): temp_list = [] for k, v in node_dict.items(): if "__code" not in k: node_value = self.truncate_expression(v[0], self.initial_var_char_limit) - if v[1] is not None: - constraint_expr = f"\n{v[1]}\n" - temp_list.append(f"<{xml_root_tag} name=\"{k}\" type=\"{type(v[0]).__name__}\">\n\n{node_value}\n\n{constraint_expr}\n\n") + if v[1] is not None and node_tag == self.optimizer_prompt_symbol_set.variable_tag: + constraint_expr = f"<{constraint_tag}>\n{v[1]}\n" + temp_list.append( + f"<{node_tag} name=\"{k}\" type=\"{type(v[0]).__name__}\">\n<{value_tag}>\n{node_value}\n\n{constraint_expr}\n\n") else: - temp_list.append(f"<{xml_root_tag} name=\"{k}\" type=\"{type(v[0]).__name__}\">\n\n{node_value}\n\n\n") + temp_list.append( + f"<{node_tag} name=\"{k}\" type=\"{type(v[0]).__name__}\">\n<{value_tag}>\n{node_value}\n\n\n") else: - constraint_expr = f"\n{v[1]}\n" + constraint_expr = f"<{constraint_tag}>\n{v[1]}\n" # we only truncate the function body signature = v[1].replace("The code should start with:\n", "") func_body = v[0].replace(signature, "") node_value = self.truncate_expression(func_body, self.initial_var_char_limit) - temp_list.append(f"<{xml_root_tag} name=\"{k}\" type=\"code\">\n\n{signature}{node_value}\n\n{constraint_expr}\n\n") + temp_list.append( + f"<{node_tag} name=\"{k}\" type=\"code\">\n<{value_tag}>\n{signature}{node_value}\n\n{constraint_expr}\n\n") return "\n".join(temp_list) def truncate_expression(self, value, limit): @@ -483,27 +599,72 @@ def problem_instance(self, summary, mask=None): else "" ), variables=( - self.repr_node_value_compact(summary.variables, xml_root_tag="variable") + self.repr_node_value_compact(summary.variables, node_tag=self.optimizer_prompt_symbol_set.variable_tag, + value_tag=self.optimizer_prompt_symbol_set.value_tag, + constraint_tag=self.optimizer_prompt_symbol_set.constraint_tag) if "#Variables" not in mask else "" ), inputs=( - self.repr_node_value_compact(summary.inputs) if "#Inputs" not in mask else "" + self.repr_node_value_compact(summary.inputs, node_tag=self.optimizer_prompt_symbol_set.node_tag, + value_tag=self.optimizer_prompt_symbol_set.value_tag, + constraint_tag=self.optimizer_prompt_symbol_set.constraint_tag) if "#Inputs" not in mask else "" ), outputs=( - self.repr_node_value_compact(summary.output) if "#Outputs" not in mask else "" + self.repr_node_value_compact(summary.output, node_tag=self.optimizer_prompt_symbol_set.node_tag, + value_tag=self.optimizer_prompt_symbol_set.value_tag, + constraint_tag=self.optimizer_prompt_symbol_set.constraint_tag) if "#Outputs" not in mask else "" ), others=( - self.repr_node_value_compact(summary.others) if "#Others" not in mask else "" + self.repr_node_value_compact(summary.others, node_tag=self.optimizer_prompt_symbol_set.node_tag, + value_tag=self.optimizer_prompt_symbol_set.value_tag, + constraint_tag=self.optimizer_prompt_symbol_set.constraint_tag) if "#Others" not in mask else "" ), feedback=summary.user_feedback if "#Feedback" not in mask else "", ) + def _step( + self, verbose=False, mask=None, *args, **kwargs + ) -> Dict[ParameterNode, Any]: + assert isinstance(self.propagator, GraphPropagator) + summary = self.summarize() + system_prompt, user_prompt = self.construct_prompt(summary, mask=mask) + + system_prompt = self.replace_symbols(system_prompt, self.prompt_symbols) + user_prompt = self.replace_symbols(user_prompt, self.prompt_symbols) + + response = self.call_llm( + system_prompt=system_prompt, + user_prompt=user_prompt, + verbose=verbose, + max_tokens=self.max_tokens, + ) + + if "TERMINATE" in response: + return {} + + suggestion = self.extract_llm_suggestion(response) + update_dict = self.construct_update_dict(suggestion) + + if self.log is not None: + self.log.append( + { + "system_prompt": system_prompt, + "user_prompt": user_prompt, + "response": response, + } + ) + self.summary_log.append( + {"problem_instance": self.problem_instance(summary), "summary": summary} + ) + + return update_dict def extract_llm_suggestion(self, response: str): """Extract the suggestion from the response.""" - suggestion = extract_xml_like_data(response) + # suggestion = extract_xml_like_data(response) + suggestion = self.optimizer_prompt_symbol_set.output_response_extractor(response) if len(suggestion) == 0: if not self.ignore_extraction_error: @@ -544,4 +705,3 @@ def call_llm( if verbose: print("LLM response:\n", response) return response - From 21207e7efa8d7b35db8debf779a326173e56febc Mon Sep 17 00:00:00 2001 From: windweller Date: Thu, 10 Jul 2025 12:18:22 -0400 Subject: [PATCH 095/314] finished with flexible tag change --- opto/optimizers/optoprime_v2.py | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index 9408833e..95f14920 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -360,7 +360,6 @@ class OptoPrimeV2(OptoPrime): """ ) - def __init__( self, parameters: List[ParameterNode], @@ -386,17 +385,7 @@ def __init__( feedback_section_title= optimizer_prompt_symbol_set.feedback_section_title) self.initial_var_char_limit = initial_var_char_limit self.optimizer_prompt_symbol_set = optimizer_prompt_symbol_set - # self.example_problem = ProblemInstance.problem_template.format( - # instruction=self.objective, - # code="y = add(x=a,y=b)\nz = subtract(x=y, y=c)", - # documentation="add: add x and y \nsubtract: subtract y from x", - # variables="""\n\n5\n\n\na: a > 0\n\n""", - # outputs="""\n\n1\n\n""", - # others="""\n\n6\n\n""", - # inputs="""\n\n1\n\n\n\n\n5\n\n""", - # feedback="The result of the code is not as expected. The result should be 10, but the code returns 1", - # stepsize=1, - # ) + self.example_problem_summary = FunctionFeedback(graph=[(1, 'y = add(x=a,y=b)'), (2, "z = subtract(x=y, y=c)")], documentation={'add': 'This is an add operator of x and y.', 'subtract': "subtract y from x"}, @@ -413,16 +402,16 @@ def __init__( self.example_problem = self.problem_instance(self.example_problem_summary) self.example_response = dedent( f""" - + <{self.optimizer_prompt_symbol_set.reasoning_tag}> In this case, the desired response would be to change the value of input a to 14, as that would make the code return 10. - + - - a - + <{self.optimizer_prompt_symbol_set.improved_variable_tag}> + <{self.optimizer_prompt_symbol_set.name_tag}>a + <{self.optimizer_prompt_symbol_set.value_tag}> 10 - - + + """ ) From 60f3806dc134d42e74a7333b7d231fe7d33c0437 Mon Sep 17 00:00:00 2001 From: windweller Date: Thu, 10 Jul 2025 12:35:39 -0400 Subject: [PATCH 096/314] add test for optoprime_v2 (it can print prompt out) --- .../llm_optimizers_tests/test_optoprime_v2.py | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 tests/llm_optimizers_tests/test_optoprime_v2.py diff --git a/tests/llm_optimizers_tests/test_optoprime_v2.py b/tests/llm_optimizers_tests/test_optoprime_v2.py new file mode 100644 index 00000000..af09c8b2 --- /dev/null +++ b/tests/llm_optimizers_tests/test_optoprime_v2.py @@ -0,0 +1,129 @@ +import os +import pytest +from opto.trace import bundle, node, GRAPH +import opto.optimizers +import importlib +import inspect +import json +import pickle +from opto.utils.llm import LLM + +from opto import trace +from opto.trace import node, bundle +from opto.optimizers.optoprime_v2 import OptoPrimeV2, OptimizerPromptSymbolSet2 + +# You can override for temporarly testing a specific optimizer ALL_OPTIMIZERS = [TextGrad] # [OptoPrimeMulti] ALL_OPTIMIZERS = [OptoPrime] + +# Skip tests if no API credentials are available +SKIP_REASON = "No API credentials found" +HAS_CREDENTIALS = os.path.exists("OAI_CONFIG_LIST") or os.environ.get("TRACE_LITELLM_MODEL") or os.environ.get( + "OPENAI_API_KEY") +llm = LLM() + + +@pytest.fixture(autouse=True) +def clear_graph(): + """Reset the graph before each test""" + GRAPH.clear() + yield + GRAPH.clear() + + +@pytest.mark.skipif(not HAS_CREDENTIALS, reason=SKIP_REASON) +def test_response_extraction(): + pass + + +def test_tag_template_change(): + num_1 = node(1, trainable=True) + num_2 = node(2, trainable=True, description="<=5") + result = num_1 + num_2 + optimizer = OptoPrimeV2([num_1, num_2], use_json_object_format=False, + ignore_extraction_error=False, + include_example=True, + optimizer_prompt_symbol_set=OptimizerPromptSymbolSet2()) + + optimizer.zero_feedback() + optimizer.backward(result, 'make this number bigger') + + summary = optimizer.summarize() + part1, part2 = optimizer.construct_prompt(summary) + + part1 = optimizer.replace_symbols(part1, optimizer.prompt_symbols) + part2 = optimizer.replace_symbols(part2, optimizer.prompt_symbols) + + assert """""" in part1, "Expected tag to be present in part1" + assert """""" in part2, "Expected tag to be present in part2" + + print(part1) + print(part2) + + +@bundle() +def transform(num): + """Add number""" + return num + 1 + + +@bundle(trainable=True) +def multiply(num): + return num * 5 + + +def test_function_repr(): + num_1 = node(1, trainable=False) + + result = multiply(transform(num_1)) + optimizer = OptoPrimeV2([multiply.parameter], use_json_object_format=False, + ignore_extraction_error=False, + include_example=True) + + optimizer.zero_feedback() + optimizer.backward(result, 'make this number bigger') + + summary = optimizer.summarize() + part1, part2 = optimizer.construct_prompt(summary) + + part1 = optimizer.replace_symbols(part1, optimizer.prompt_symbols) + part2 = optimizer.replace_symbols(part2, optimizer.prompt_symbols) + + function_repr = """ + +def multiply(num): + return num * 5 + + +The code should start with: +def multiply(num): + +""" + + assert function_repr in part2, "Expected function representation to be present in part2" + +def test_big_data_truncation(): + num_1 = node(1, trainable=True) + + list_1 = node([1, 2, 3, 4, 5, 6, 7, 8, 9, 20] * 10, trainable=True) + + result = num_1 + list_1[30] + + optimizer = OptoPrimeV2([num_1, list_1], use_json_object_format=False, + ignore_extraction_error=False, + include_example=True, initial_var_char_limit=10) + + optimizer.zero_feedback() + optimizer.backward(result, 'make this number bigger') + + summary = optimizer.summarize() + part1, part2 = optimizer.construct_prompt(summary) + + part1 = optimizer.replace_symbols(part1, optimizer.prompt_symbols) + part2 = optimizer.replace_symbols(part2, optimizer.prompt_symbols) + + truncated_repr = """ + +[1, 2, 3, ...(skipped due to length limit) + +""" + + assert truncated_repr in part2, "Expected truncated list representation to be present in part2" \ No newline at end of file From 7878b022e9b7476657b7e43133f1e88aa2771d36 Mon Sep 17 00:00:00 2001 From: windweller Date: Thu, 10 Jul 2025 12:43:46 -0400 Subject: [PATCH 097/314] fix pyproject file --- pyproject.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index bd171f07..8d652ed2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,6 @@ authors = [ {name = "Adith Swaminathan", email = "adith387@gmail.com"}, ] license="MIT" -license-files=["LICEN[CS]E*"] requires-python = ">= 3.9" dynamic = ["version", "dependencies", "description"] readme = "README.md" @@ -30,3 +29,6 @@ autogen = ["autogen-agentchat==0.2.40"] Homepage = "https://microsoft.github.io/Trace/" Documentation = "https://microsoft.github.io/Trace/intro.html" Repository = "https://github.com/microsoft/Trace.git" + +[tool.setuptools] +license-files = ["LICEN[CS]E*"] \ No newline at end of file From 787386a9c9ccd617b86cc1c78afa882734e0efd7 Mon Sep 17 00:00:00 2001 From: windweller Date: Thu, 10 Jul 2025 12:52:33 -0400 Subject: [PATCH 098/314] quick fix on xml parsing testing --- opto/optimizers/optoprime_v2.py | 2 +- tests/unit_tests/test_optimizer_xml_parsing.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index 95f14920..55da6d96 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -90,7 +90,7 @@ def extract_reasoning_and_remainder(text: str, tag: str = "reasoning"): def extract_xml_like_data(text: str, reasoning_tag: str = "reasoning", - improved_variable_tag: str = "improved_variable", + improved_variable_tag: str = "variable", name_tag: str = "name", value_tag: str = "value") -> Dict[str, Any]: """ diff --git a/tests/unit_tests/test_optimizer_xml_parsing.py b/tests/unit_tests/test_optimizer_xml_parsing.py index def41033..29df89a7 100644 --- a/tests/unit_tests/test_optimizer_xml_parsing.py +++ b/tests/unit_tests/test_optimizer_xml_parsing.py @@ -30,8 +30,6 @@ - No reasoning/variable tags scenarios """ - - class TestXMLParsing(unittest.TestCase): def test_basic_parsing(self): From 1bfda6220e0f5e90a2853fd7216f75863514d689 Mon Sep 17 00:00:00 2001 From: windweller Date: Thu, 10 Jul 2025 13:42:12 -0400 Subject: [PATCH 099/314] add more cleanup --- opto/optimizers/optoprime_v2.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index 55da6d96..ee77d1b3 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -178,6 +178,7 @@ def __repr__(self) -> str: feedback=self.feedback, ) + class OptimizerPromptSymbolSet: """ By inheriting this class and pass into the optimizer. People can change the optimizer documentation @@ -225,6 +226,7 @@ def output_response_extractor(self, response: str) -> Dict[str, Any]: raise NotImplementedError( "If you supplied a custom output format prompt template, you need to implement your own response extractor") + class OptimizerPromptSymbolSet2(OptimizerPromptSymbolSet): variables_section_title = "# Variables" inputs_section_title = "# Inputs" @@ -261,16 +263,6 @@ class OptoPrimeV2(OptoPrime): # TODO: idea 1: for each operator, we can identify repeated structure # TODO: idea 2: for each bundle/op, the user can pass in a callable function, take original output, return a string # TODO: idea 2-2: each node has a string representation of data, that's what the optimizer should use (this string is fixed) - # TODO: some are too redundant to describe - # TODO: x = a + b - # TODO: y = a + c - # TODO: z = f(x, y) => z = f(a+b, a+c) - # TODO: z = g(a, b, c) - - # TODO: Node level change: format_data_repr(func: Callable[[Node], str]) -> None - # TODO: Check format data representation - # TODO: input would be the data of this node, return would be a string - # TODO: later on optimizer just calls this # This is generic representation prompt, which just explains how to read the problem. representation_prompt = dedent( @@ -369,7 +361,7 @@ def __init__( objective: Union[None, str] = None, ignore_extraction_error: bool = True, # ignore the type conversion error when extracting updated values from LLM's suggestion - include_example=False, # TODO # include example problem and response in the prompt + include_example=False, memory_size=0, # Memory size to store the past feedback max_tokens=4096, log=True, @@ -381,8 +373,8 @@ def __init__( self.ignore_extraction_error = ignore_extraction_error self.llm = llm or LLM() self.objective = objective or self.default_objective.format(value_tag=optimizer_prompt_symbol_set.value_tag, - variables_section_title= optimizer_prompt_symbol_set.variables_section_title, - feedback_section_title= optimizer_prompt_symbol_set.feedback_section_title) + variables_section_title=optimizer_prompt_symbol_set.variables_section_title, + feedback_section_title=optimizer_prompt_symbol_set.feedback_section_title) self.initial_var_char_limit = initial_var_char_limit self.optimizer_prompt_symbol_set = optimizer_prompt_symbol_set @@ -455,7 +447,7 @@ def initialize_prompt(self): instruction_section_title=self.optimizer_prompt_symbol_set.instruction_section_title.replace(" ", ""), code_section_title=self.optimizer_prompt_symbol_set.code_section_title.replace(" ", ""), documentation_section_title=self.optimizer_prompt_symbol_set.documentation_section_title.replace(" ", ""), - others_section_title = self.optimizer_prompt_symbol_set.others_section_title.replace(" ", "") + others_section_title=self.optimizer_prompt_symbol_set.others_section_title.replace(" ", "") ) self.output_format_prompt = self.output_format_prompt_template.format( output_format=dedent(f""" From 71aa9034ddfffc220c2b49b636accd5c4fae98b7 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 10 Jul 2025 18:35:04 +0000 Subject: [PATCH 100/314] Update naming convention of deepcopied node. --- opto/trace/nodes.py | 18 +++++++++++------- tests/unit_tests/test_nodes.py | 1 + 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/opto/trace/nodes.py b/opto/trace/nodes.py index 756328e6..cac02446 100644 --- a/opto/trace/nodes.py +++ b/opto/trace/nodes.py @@ -23,11 +23,11 @@ def node(data, name=None, trainable=False, description=None): Notes: If trainable=True: - If data is already a Node, extracts underlying data and updates name - - Creates ParameterNode with extracted data, name, trainable=True + - Creates ParameterNode with extracted data, name, trainable=True If trainable=False: - If data is already a Node, returns it (with warning if name provided) - - Otherwise creates new Node with data, name + - Otherwise creates new Node with data, name """ assert type(description) is str or description is None @@ -456,6 +456,10 @@ def __deepcopy__(self, memo): setattr(result, k, []) elif k == "_feedback": setattr(result, k, defaultdict(list)) + elif k == "_name": + name, counter = v.split(":") + new_name = v.replace(':', '') + '_copy:0' # this allows to keep track with the original name + setattr(result, k, new_name) else: setattr(result, k, copy.deepcopy(v, memo)) GRAPH.register(result) @@ -791,7 +795,7 @@ def __init__( trainable: bool = False, description: str = None, info: Union[None, Dict] = None, - ) -> None: + ) -> None: if description == "" or description is None: description = f"[{type(self).__name__}]" @@ -828,13 +832,13 @@ def feedback(self): @property def description(self): - """A textual description of the node.""" + """A textual description of the node.""" # return self._description # remove the operator type from the description description = re.sub(r"^\[([^\[\]]+)\]", "", self._description).strip() # return None if empty return description if description else None - + @property def op_name(self): """The operator type of the node, extracted from the description.""" @@ -2012,7 +2016,7 @@ def __init__( info=info, ) self._dependencies["parameter"].add(self) - + if projections is not None: assert isinstance( projections, list @@ -2020,7 +2024,7 @@ def __init__( from opto.trace.projections import Projection assert all( isinstance(p, Projection) for p in projections - ), "All projections must be instances of Projection." + ), "All projections must be instances of Projection." self.projections = projections else: self.projections = [] diff --git a/tests/unit_tests/test_nodes.py b/tests/unit_tests/test_nodes.py index 6d5d1e73..31aec23e 100644 --- a/tests/unit_tests/test_nodes.py +++ b/tests/unit_tests/test_nodes.py @@ -87,6 +87,7 @@ def test_node_copy_clone_deepcopy(): z_new = ops.identity(z) z_clone = z.clone() z_copy = copy.deepcopy(z) + assert z_copy.name == z.py_name + '_copy:0' assert z_new.data == z.data assert z_clone.data == z.data assert z_copy.data == z.data From 79c94f36eac2e09bf04e437152f005914e8bb000 Mon Sep 17 00:00:00 2001 From: windweller Date: Thu, 10 Jul 2025 15:11:35 -0400 Subject: [PATCH 101/314] added new test and it passes --- .../unit_tests/test_optimizer_xml_parsing.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/unit_tests/test_optimizer_xml_parsing.py b/tests/unit_tests/test_optimizer_xml_parsing.py index 29df89a7..edbb3758 100644 --- a/tests/unit_tests/test_optimizer_xml_parsing.py +++ b/tests/unit_tests/test_optimizer_xml_parsing.py @@ -329,6 +329,40 @@ def test_duplicate_variable_names(self): } self.assertEqual(result, expected) + def test_xml_with_random_text(self): + """Test that parser extracts XML content while ignoring random text""" + text = """ + This is some random texts with random symbols `~!@#$%^&*()-=[]\;',./_+{}|:"<>?. + + + Some reasoning. + + + Some other random texts with random symbols `~!@#$%^&*()-=[]\;',./_+{}|:"<>?. + + + var1 + value1 + + + Yet another random texts with random symbols `~!@#$%^&*()-=[]\;',./_+{}|:"<>?. + + + var2 + value2 + + """ + + result = extract_xml_like_data(text, name_tag="name", value_tag="value") + expected = { + 'reasoning': 'Some reasoning.', + 'variables': { + 'var1': 'value1', + 'var2': 'value2' + } + } + self.assertEqual(result, expected) + if __name__ == '__main__': unittest.main() \ No newline at end of file From 3c4e9b56fc4ee0c34c43e7c5f40420239d3d26bb Mon Sep 17 00:00:00 2001 From: windweller Date: Thu, 10 Jul 2025 15:16:37 -0400 Subject: [PATCH 102/314] incorporate some of Xavier's suggestion on OptoPrime instruction change --- opto/optimizers/optoprime_v2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index ee77d1b3..bf0a3de7 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -273,8 +273,8 @@ class OptoPrimeV2(OptoPrime): - {instruction_section_title}: the instruction which describes the things you need to do or the question you should answer. - {code_section_title}: the code defined in the problem. - {documentation_section_title}: the documentation of each function used in #Code. The explanation might be incomplete and just contain high-level description. You can use the values in #Others to help infer how those functions work. - - {variables_section_title}: the input variables that you can change. - - {inputs_section_title}: the values of other inputs to the code, which are not changeable. + - {variables_section_title}: the input variables that you can change/tweak (trainable). + - {inputs_section_title}: the values of fixed inputs to the code, which CANNOT be changed (fixed). - {others_section_title}: the intermediate values created through the code execution. - {outputs_section_title}: the result of the code output. - {feedback_section_title}: the feedback about the code's execution result. From d3bf800f15ac365d23a1849e4b66eec9f130eb00 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 10 Jul 2025 20:15:46 +0000 Subject: [PATCH 103/314] Add randomize flag and n_epochs attribute to Dataloader. --- opto/trainer/loader.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/opto/trainer/loader.py b/opto/trainer/loader.py index 90d738f9..1da24dca 100644 --- a/opto/trainer/loader.py +++ b/opto/trainer/loader.py @@ -5,12 +5,15 @@ class DataLoader: - def __init__(self, dataset, batch_size=1, replacement=False, shuffle=True): + def __init__(self, dataset, batch_size=1, randomize=True, replacement=False, shuffle=True): """ Initialize the data loader Args: dataset: the dataset to load (a dict of inputs and infos) batch_size: the number of samples to load in each batch + randomize: whether to randomize the dataset ordering before loading; + if False, the dataset will be loaded in the order it is + provided (replacement and shuffle be ignored) replacement: whether to sample with replacement shuffle: whether to shuffle the dataset after each epoch """ @@ -20,9 +23,11 @@ def __init__(self, dataset, batch_size=1, replacement=False, shuffle=True): self.dataset = dataset self.batch_size = batch_size + self.randomize = randomize self.replacement = replacement self.shuffle = shuffle self._indices = self._update_indices() + self.n_epochs = 0 self._i = 0 def __iter__(self): @@ -33,7 +38,9 @@ def __next__(self): if self._i >= len(self._indices): if self.shuffle: self._indices = self._update_indices() + # Reset the index for the next epoch self._i = 0 + self.n_epochs += 1 raise StopIteration indices = self._indices[self._i: min(self._i + self.batch_size, len(self._indices))] xs = [self.dataset['inputs'][ind] for ind in indices] @@ -43,7 +50,10 @@ def __next__(self): def _update_indices(self): N = len(self.dataset['inputs']) - return np.random.choice(N, size=N, replace=self.replacement) + if self.randomize: + return np.random.choice(N, size=N, replace=self.replacement) + else: + return np.arange(N) def sample(self): """ Sample a batch of data from the dataset """ From 15d795f938241e297530b2cd5f4f121ce350559d Mon Sep 17 00:00:00 2001 From: windweller Date: Thu, 10 Jul 2025 18:42:22 -0400 Subject: [PATCH 104/314] remove UCBsearch --- opto/trainer/algorithms/UCBsearch.py | 1513 -------------------------- 1 file changed, 1513 deletions(-) delete mode 100644 opto/trainer/algorithms/UCBsearch.py diff --git a/opto/trainer/algorithms/UCBsearch.py b/opto/trainer/algorithms/UCBsearch.py deleted file mode 100644 index 0f3f9bc3..00000000 --- a/opto/trainer/algorithms/UCBsearch.py +++ /dev/null @@ -1,1513 +0,0 @@ -import numpy as np -import copy -import math -from collections import deque -from typing import Union, List, Tuple, Dict, Any, Optional -from opto import trace -from opto.trainer.utils import async_run # Assuming print_color is in utils -from opto.optimizers.utils import print_color -from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm, evaluate, batchify # evaluate and batchify might be useful -import json # For LLM output parsing -import random # Added for alpha probability -from opto.utils.llm import LLM # For the selector LLM -from opto.trace.nodes import ParameterNode -import warnings -from black import format_str, FileMode - -class UCBSearchAlgorithm(MinibatchAlgorithm): - """ - UCB Search Algorithm. - - Keeps a buffer of candidates with their statistics (score sum, evaluation count). - In each iteration: - 1. Picks a candidate 'a' from the buffer with the highest UCB score. - 2. Updates the optimizer with 'a's parameters. - 3. Draws a minibatch from the training set, performs a forward/backward pass, and calls optimizer.step() to get a new candidate 'a''. - 4. Evaluates 'a'' on a validation set minibatch. - 5. Updates statistics of 'a' (based on the training minibatch). - 6. Adds 'a'' (with its validation stats) to the buffer. - 7. If the buffer is full, evicts the candidate with the lowest UCB score. - """ - - def __init__(self, - agent: trace.Module, - optimizer, - max_buffer_size: int = 10, - ucb_exploration_factor: float = 1.0, # Controls exploration vs exploitation tradeoff in UCB selection - # UCB formula: μ(a) + c * sqrt(ln(t) / n(a)), c is the exploration factor - logger=None, - num_threads: int = None, - use_validation: bool = False, - *args, - **kwargs): - super().__init__(agent, optimizer, num_threads=num_threads, logger=logger, *args, **kwargs) - - self.buffer = deque(maxlen=max_buffer_size) - self.max_buffer_size = max_buffer_size - # UCB exploration factor: Higher values encourage more exploration of less-tested candidates, - # lower values favor exploitation of well-performing candidates. - self.ucb_exploration_factor = ucb_exploration_factor - self.use_validation = use_validation # Whether to use validation set for evaluation - # To ensure optimizer_step can be called with bypassing=True if needed. - # This depends on the specific optimizer's implementation. - # For now, we assume the optimizer has a step method that can return parameters. - if not hasattr(self.optimizer, 'step'): - raise ValueError("Optimizer must have a 'step' method.") - - self._total_evaluations_tracker = 0 # Tracks total number of individual candidate evaluations used in UCB calculation for log(T) - self._candidate_id_counter = 0 - - def _sample_minibatch(self, dataset: Dict[str, List[Any]], batch_size: int) -> Tuple[List[Any], List[Any]]: - """Sample a minibatch from the dataset.""" - if not dataset or not dataset.get('inputs') or not dataset.get('infos'): - print_color("Warning: Attempted to sample from an empty or malformed dataset.", color='yellow') - return [], [] - - dataset_size = len(dataset['inputs']) - if dataset_size == 0: - print_color("Warning: Dataset is empty, cannot sample minibatch.", color='yellow') - return [], [] - - actual_batch_size = min(batch_size, dataset_size) - indices = np.random.choice(dataset_size, actual_batch_size, replace=False) - xs = [dataset['inputs'][i] for i in indices] - infos = [dataset['infos'][i] for i in indices] - return xs, infos - - def _evaluate_candidate(self, - params_to_eval_dict: Dict[str, Any], - dataset: Dict[str, List[Any]], # Changed from validate_dataset - guide, # Changed from validate_guide - evaluation_batch_size: int, # New parameter name - num_threads: Optional[int] = None - ) -> Tuple[float, int]: - """Evaluates a given set of parameters on samples from the provided dataset (now typically train_dataset).""" - if not dataset or not dataset.get('inputs') or not dataset.get('infos') or not dataset['inputs']: - print_color("Evaluation dataset is empty or invalid. Returning score -inf, count 0.", color='yellow') - return -np.inf, 0 - - original_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} - self.optimizer.update(params_to_eval_dict) - - eval_xs, eval_infos = self._sample_minibatch(dataset, evaluation_batch_size) - - if not eval_xs: - print_color("Evaluation minibatch is empty. Returning score -inf, count 0.", color='yellow') - self.optimizer.update(original_params) - return -np.inf, 0 - - eval_scores = evaluate(self.agent, - guide, # Use main guide - eval_xs, - eval_infos, - min_score=self.min_score if hasattr(self, 'min_score') else None, - num_threads=num_threads or self.num_threads, - description=f"Evaluating candidate") - - self.optimizer.update(original_params) - - avg_score = np.mean(eval_scores) if eval_scores and all(s is not None for s in eval_scores) else 0 - eval_count = len(eval_xs) - - return float(avg_score), eval_count - - def _calculate_ucb(self, candidate_buffer_entry: Dict, total_tracked_evaluations: int) -> float: - """Calculates UCB score for a candidate in the buffer.""" - if candidate_buffer_entry['eval_count'] == 0: - return float('inf') # Explore unvisited states first - - mean_score = candidate_buffer_entry['score_sum'] / candidate_buffer_entry['eval_count'] - - # Add 1 to total_tracked_evaluations to prevent log(0) if it's the first evaluation overall - # and to ensure log argument is > 0. - # Add 1 to eval_count in denominator as well to ensure it's robust if eval_count is small. - if total_tracked_evaluations == 0: # Should not happen if we init with one eval - total_tracked_evaluations = 1 - - # UCB exploration term: ucb_exploration_factor scales the confidence interval - # Higher factor = more exploration, lower factor = more exploitation - exploration_term = self.ucb_exploration_factor * \ - math.sqrt(math.log(total_tracked_evaluations) / candidate_buffer_entry['eval_count']) - - return mean_score + exploration_term - - def _calculate_lcb(self, candidate_buffer_entry: Dict, total_tracked_evaluations: int) -> float: - """Calculates Lower Confidence Bound for a candidate in the buffer.""" - if candidate_buffer_entry['eval_count'] == 0: - return float('-inf') # Unvisited states get lowest bound - - mean_score = candidate_buffer_entry['score_sum'] / candidate_buffer_entry['eval_count'] - - # Add 1 to total_tracked_evaluations to prevent log(0) if it's the first evaluation overall - # and to ensure log argument is > 0. - # Add 1 to eval_count in denominator as well to ensure it's robust if eval_count is small. - if total_tracked_evaluations == 0: # Should not happen if we init with one eval - total_tracked_evaluations = 1 - - # LCB exploration term: ucb_exploration_factor scales the confidence interval - # Higher factor = more exploration, lower factor = more exploitation - exploration_term = self.ucb_exploration_factor * \ - math.sqrt(math.log(total_tracked_evaluations) / candidate_buffer_entry['eval_count']) - - return mean_score - exploration_term - - def _update_buffer_ucb_scores(self): - """Recalculates and updates UCB scores for all candidates in the buffer.""" - if not self.buffer: - return - - for candidate_entry in self.buffer: - candidate_entry['ucb_score'] = self._calculate_ucb(candidate_entry, self._total_evaluations_tracker) - - def _get_best_candidate_from_buffer(self, buffer): - """Get the best candidate from buffer, excluding those with eval_count = 0 when not using validation.""" - if not buffer: - return None - - # Filter out candidates with eval_count = 0 if not using validation - if not self.use_validation: - valid_candidates = [c for c in buffer if c['eval_count'] > 0] - if not valid_candidates: - # If no candidates have been evaluated, return the one with highest UCB score - return max(buffer, key=lambda c: c.get('ucb_score', -float('inf'))) - return max(valid_candidates, key=lambda c: c['score_sum'] / c['eval_count']) - else: - # When using validation, all candidates should have eval_count > 0 - return max(buffer, key=lambda c: c['score_sum'] / (c['eval_count'] or 1E-9)) - - def print_intervals(self, buffer): - """Print confidence intervals for debugging in the form of open intervals (LCB, UCB)""" - print_color("Confidence intervals for all candidates:", 'cyan') - for i, candidate_entry in enumerate(buffer): - lcb = self._calculate_lcb(candidate_entry, self._total_evaluations_tracker) - ucb = candidate_entry['ucb_score'] - mean_score = candidate_entry['score_sum'] / (candidate_entry['eval_count'] or 1) - eval_count = candidate_entry['eval_count'] - - # Format as open interval (LCB, UCB) with mean score and evaluation count - interval_str = f"Action {i+1}: ({lcb:.4f}, {ucb:.4f}) [mean: {mean_score:.4f}, n: {eval_count}]" - print_color(interval_str, 'cyan') - - def _process_single_candidate(self, - action_candidate_a: Dict, - guide, - train_dataset: Dict[str, List[Any]], - validation_dataset: Dict[str, List[Any]], - train_batch_size: int, - evaluation_batch_size: int, - num_threads: Optional[int], - iteration: int) -> Tuple[bool, float, float, int]: - """ - Process a single candidate: generate a_prime, evaluate both a and a_prime, - update stats for 'a', and add 'a_prime' to buffer. - - Returns: - Tuple of (success, a_prime_score, score_for_a_on_train_batch, samples_used) - """ - # 2. Load parameters of 'a' into the agent for the optimizer update step - self.optimizer.update(action_candidate_a['params']) - - # 3. Draw minibatch from the training set, do update from 'a' to get 'a_prime' - train_xs, train_infos = self._sample_minibatch(train_dataset, train_batch_size) - if not train_xs: - print_color(f"Iter {iteration}: Training minibatch empty for candidate, skipping.", 'yellow') - return False, -np.inf, -np.inf, 0 - - # Perform forward pass and get feedback for agent parameters 'a' - use_asyncio = self._use_asyncio(num_threads) - if use_asyncio: - outputs_for_a = async_run([self.forward]*len(train_xs), - [(self.agent, x, guide, info) for x, info in zip(train_xs, train_infos)], - max_workers=num_threads, - description=f"Iter {iteration}: Forward pass for action 'a'") - else: - outputs_for_a = [self.forward(self.agent, x, guide, info) for x, info in zip(train_xs, train_infos)] - - scores_from_train, targets_from_train, feedbacks_from_train = [], [], [] - for target, score, feedback in outputs_for_a: - scores_from_train.append(score) - targets_from_train.append(target) - feedbacks_from_train.append(feedback) - - if not scores_from_train: - print_color(f"Iter {iteration}: No outputs from forward pass for candidate. Skipping.", 'yellow') - return False, -np.inf, -np.inf, 0 - - target_for_a = batchify(*targets_from_train) - feedback_for_a = batchify(*feedbacks_from_train).data - score_for_a_on_train_batch = np.mean([s for s in scores_from_train if s is not None]) if any(s is not None for s in scores_from_train) else -np.inf - - self.optimizer.zero_feedback() - self.optimizer.backward(target_for_a, feedback_for_a) - - try: - a_prime_params_dict = self.optimizer.step(bypassing=True, verbose=False) - if not isinstance(a_prime_params_dict, dict) or not a_prime_params_dict: - print_color(f"Iter {iteration}: Optimizer.step did not return valid params. Using current agent params.", 'yellow') - a_prime_params_dict = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} - self.total_proposals += 1 - except Exception as e: - print_color(f"Iter {iteration}: Error during optimizer.step: {e}. Skipping.", 'red') - return False, -np.inf, -np.inf, 0 - - # 4. Evaluate 'a' and 'a_prime' on samples of validation set in parallel - if self.use_validation: - if use_asyncio: - evaluation_results = async_run( - [self._evaluate_candidate, self._evaluate_candidate], - [ - (action_candidate_a['params'], validation_dataset, guide, evaluation_batch_size, num_threads), - (a_prime_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads) - ], - max_workers=2, - description=f"Iter {iteration}: Parallel evaluation of 'a' and 'a_prime'" - ) - (a_score, a_evals), (a_prime_score, a_prime_evals) = evaluation_results - else: - a_score, a_evals = self._evaluate_candidate( - action_candidate_a['params'], validation_dataset, guide, evaluation_batch_size, num_threads - ) - a_prime_score, a_prime_evals = self._evaluate_candidate( - a_prime_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads - ) - - # 5. Update statistics for the original candidate 'a' - # Always update statistics for the original candidate 'a' on the training set - if score_for_a_on_train_batch > -np.inf: - action_candidate_a['score_sum'] += score_for_a_on_train_batch * len(train_xs) - action_candidate_a['eval_count'] += len(train_xs) - self._total_evaluations_tracker += len(train_xs) - - # If we use validation set for evaluation - if self.use_validation: # If we use validation set for evaluation - action_candidate_a['score_sum'] += a_score * a_evals - action_candidate_a['eval_count'] += a_evals - - # 6. Add 'a_prime' to the buffer (with eviction logic if needed) - if a_prime_score > -np.inf and a_prime_evals > 0: - new_candidate_entry = { - 'params': a_prime_params_dict, - 'score_sum': a_prime_score * a_prime_evals, - 'eval_count': a_prime_evals, - 'ucb_score': None, # Will be updated later - 'iteration_created': iteration - } - - # Eviction logic before adding if buffer is at max capacity - if len(self.buffer) >= self.max_buffer_size: - self._update_buffer_ucb_scores() # Ensure UCBs are current before eviction - candidate_to_evict = min(self.buffer, key=lambda c: c['ucb_score']) - self.buffer.remove(candidate_to_evict) - print_color(f"Iter {iteration}: Buffer full. Evicted candidate (UCB: {candidate_to_evict['ucb_score']:.4f})", 'magenta') - - self.buffer.append(new_candidate_entry) - print_color(f"Iter {iteration}: Added new candidate to buffer (score: {a_prime_score:.4f})", 'magenta') - else: - print_color(f"Iter {iteration}: New candidate a_prime had invalid score/evals, not added to buffer.", 'yellow') - - # Update tracking - self._total_evaluations_tracker += a_evals + a_prime_evals - samples_used = 2 * evaluation_batch_size + train_batch_size - else: # If we don't use validation set for evaluation, please evaluate a_prime on the training set - a_prime_score, a_prime_evals = self._evaluate_candidate( - a_prime_params_dict, {'inputs': train_xs, 'infos': train_infos}, - guide, len(train_xs), num_threads - ) - self._total_evaluations_tracker += a_prime_evals - - new_candidate_entry = { - 'params': a_prime_params_dict, - 'score_sum': a_prime_score * a_prime_evals if a_prime_score > -np.inf else 0, - 'eval_count': a_prime_evals, - 'ucb_score': None, # Will be updated later - 'iteration_created': iteration - } - self.buffer.append(new_candidate_entry) - samples_used = 2*train_batch_size # One batch for training update, one for evaluation - return True, a_prime_score, score_for_a_on_train_batch, samples_used - - def train(self, - guide, # Guide for train_dataset (feedback generation AND evaluation) - train_dataset: Dict[str, List[Any]], - *, - validation_dataset: Optional[Dict[str, List[Any]]] = None, # Validation set for evaluation, defaults to train_dataset - test_dataset: Optional[Dict[str, List[Any]]] = None, - num_search_iterations: int = 100, - train_batch_size: int = 2, - evaluation_batch_size: int = 20, # Renamed from validation_batch_size, used for all explicit evaluations - eval_frequency: int = 1, - log_frequency: Optional[int] = None, - save_frequency: Optional[int] = None, - save_path: str = "checkpoints/ucb_agent.pkl", - min_score_for_agent_update: Optional[float] = None, # Renamed from min_score to avoid conflict with evaluate's min_score - verbose: Union[bool, str] = False, - num_threads: Optional[int] = None, - print_confidence_interval: bool = True, - **kwargs - ) -> Tuple[Dict[str, Any], float]: # Returns metrics and best score - """ - Main training loop for UCB Search Algorithm. - """ - # Default validation_dataset to train_dataset if not provided - if validation_dataset is None: - validation_dataset = train_dataset - if test_dataset is None: - test_dataset = train_dataset - - num_threads = num_threads or self.num_threads - log_frequency = log_frequency or eval_frequency - self.min_score = min_score_for_agent_update # Used by parent's evaluate if called, or our own _evaluate_candidate - total_samples = 0 - self.total_proposals = 0 - # Metrics tracking - metrics = { - 'best_candidate_scores': [], # Score of the best candidate (e.g., highest mean) found so far at each iteration - 'selected_action_ucb': [], # UCB score of the selected action 'a' - 'new_candidate_scores': [], # Score of the new candidate 'a_prime' - 'buffer_avg_score': [], - 'buffer_avg_evals': [], - } - -# 0. Evaluate the initial parameter on samples of the validation set and add it to the buffer. - initial_params_dict = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} - print_color("Evaluating initial parameters using validation_dataset samples...", 'cyan') - initial_score, initial_evals = self._evaluate_candidate( - initial_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads # Use validation_dataset and guide - ) - self.logger.log('Test score', initial_score, 0, color='blue') - self.logger.log('Total samples', total_samples, 0, color='cyan') - print_color(f"Initial candidate: Score {initial_score:.4f}, Evals {initial_evals}", 'yellow') - if self.use_validation: - self._total_evaluations_tracker += initial_evals - total_samples += initial_evals - # Log initial evaluation - initial_candidate_entry = { - 'params': initial_params_dict, - 'score_sum': initial_score * initial_evals if initial_score > -np.inf else 0, # Store sum for accurate mean later - 'eval_count': initial_evals, - 'ucb_score': None, # avoid accidental reads before it's initialized - 'iteration_created': 0 - } - self._update_buffer_ucb_scores() # Update UCB for the initial candidate - else: - initial_candidate_entry = { - 'params': initial_params_dict, - 'score_sum': 0, - 'eval_count': 0, - 'ucb_score': None, # avoid accidental reads before it's initialized - 'iteration_created': 0 - } - self.buffer.append(initial_candidate_entry) - - # Main search loop - for iteration in range(1, num_search_iterations + 1): - try: - if not self.buffer: - print_color("Buffer is empty, stopping search.", 'red') - break - - # 1. Pick the candidate 'a' with the highest UCB from the buffer - self._update_buffer_ucb_scores() # Ensure UCB scores are fresh - - action_candidate_a = self.select(self.buffer) - if print_confidence_interval: - self.print_intervals(self.buffer) - # Log selected action UCB score - self.logger.log('Selected action UCB', action_candidate_a['ucb_score'], iteration, color='magenta') - self.logger.log('Selected action mean score', action_candidate_a['score_sum']/(action_candidate_a['eval_count'] or 1), iteration, color='cyan') - - print_color(f"Iter {iteration}/{num_search_iterations}: ", 'blue') - - # Process the selected candidate - success, a_prime_score, score_for_a_on_train_batch, samples_used = self._process_single_candidate( - action_candidate_a, guide, train_dataset, validation_dataset, - train_batch_size, evaluation_batch_size, num_threads, iteration - ) - - if not success: # Error occurred in processing - continue - - total_samples += samples_used - if self.use_validation: - metrics['new_candidate_scores'].append(a_prime_score) - self.logger.log('New candidate score', a_prime_score, iteration, color='green') - print_color(f"Iter {iteration}: New candidate a_prime generated. Validation Score: {a_prime_score:.4f}", 'cyan') - self.logger.log('Training batch score', score_for_a_on_train_batch, iteration, color='yellow') - - - - # Update all UCB scores in the buffer after potential additions/removals/stat updates - self._update_buffer_ucb_scores() - - # Logging - best_in_buffer = self._get_best_candidate_from_buffer(self.buffer) - if best_in_buffer: - metrics['best_candidate_scores'].append(best_in_buffer['score_sum']/(best_in_buffer['eval_count'] or 1)) - else: - metrics['best_candidate_scores'].append(-np.inf) - metrics['buffer_avg_score'].append(np.mean([c['score_sum']/(c['eval_count'] or 1) for c in self.buffer if c['eval_count'] > 0])) - metrics['buffer_avg_evals'].append(np.mean([c['eval_count'] for c in self.buffer])) - - if iteration % log_frequency == 0: - log_data = { - "iteration": iteration, - "best_score": metrics['best_candidate_scores'][-1], #best_candidate_score_in_buffer - "selected_action_ucb": action_candidate_a['ucb_score'], - "new_candidate_score": a_prime_score, - "buffer_size": len(self.buffer), - "buffer_avg_score": metrics['buffer_avg_score'][-1], - "buffer_avg_evals": metrics['buffer_avg_evals'][-1], - "total_evaluations_tracker": self._total_evaluations_tracker, # used in calculating ucb scores - "total_samples": total_samples # Add new metric - } - - # Log all important metrics - self.logger.log('Best candidate score', log_data['best_score'], iteration, color='green') - self.logger.log('Buffer size', log_data['buffer_size'], iteration, color='blue') - self.logger.log('Buffer average score', log_data['buffer_avg_score'], iteration, color='cyan') - self.logger.log('Buffer average evaluations', log_data['buffer_avg_evals'], iteration, color='orange') - # self.logger.log('Total evaluations tracker', log_data['total_evaluations_tracker'], iteration, color='magenta') - self.logger.log('Total samples', log_data['total_samples'], iteration, color='yellow') - self.logger.log('Total proposals', self.total_proposals, iteration, color='red') - print_color(f"Log @ Iter {iteration}: Best score in buffer: {log_data['best_score']:.4f}, Buffer size: {log_data['buffer_size']}, Total samples: {total_samples}", 'green') - - if test_dataset is not None and iteration % eval_frequency == 0: - try: - # Save current agent parameters - current_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} - - # Find the best candidate in the buffer (highest mean score) - best_candidate = self._get_best_candidate_from_buffer(self.buffer) - if not best_candidate: - print_color(f"Iter {iteration}: No valid candidate for test evaluation.", 'yellow') - continue - - # Load best candidate's parameters into the agent for evaluation - self.optimizer.update(best_candidate['params']) - - # Evaluate the best candidate on test set - test_score = self.evaluate(self.agent, guide, test_dataset['inputs'], test_dataset['infos'], - min_score=self.min_score, num_threads=num_threads, - description=f"Evaluating best candidate (iteration {iteration})") - - # Restore original agent parameters - self.optimizer.update(current_params) - - self.logger.log('Test score', test_score, iteration, color='green') - except Exception as e: - print_color(f"Iter {iteration}: Test evaluation failed: {e}", 'red') - - # Save agent (e.g., the one with highest mean score in buffer) - if save_frequency is not None and iteration % save_frequency == 0: - try: - best_overall_candidate = self._get_best_candidate_from_buffer(self.buffer) - if not best_overall_candidate: - print_color(f"Iter {iteration}: No valid candidate for agent save.", 'yellow') - continue - self.optimizer.update(best_overall_candidate['params']) # Load params using optimizer - self.save_agent(save_path, iteration) # save_agent is from AlgorithmBase - print_color(f"Iter {iteration}: Saved agent based on best candidate in buffer.", 'green') - except Exception as e: - print_color(f"Iter {iteration}: Agent save failed: {e}", 'red') - - except Exception as e: - print_color(f"Iter {iteration}: Iteration failed with error: {e}. Skipping to next iteration.", 'red') - self.logger.log('Iteration error', str(e), iteration, color='red') - continue - - # End of search loop - print_color("UCB search finished.", 'blue') - - # Log final training summary - final_iteration = num_search_iterations - self.logger.log('UCB search completed', final_iteration, final_iteration, color='blue') - self.logger.log('Final total samples', total_samples, final_iteration, color='magenta') - - if not self.buffer: - print_color("Buffer is empty at the end of search. No best candidate found.", 'red') - self.logger.log('Final status', 'Buffer empty - no best candidate', final_iteration, color='red') - return metrics, -np.inf - - # Select the best candidate based on highest mean score (exploitation) - final_best_candidate = self._get_best_candidate_from_buffer(self.buffer) - if not final_best_candidate: - print_color("No valid candidate found at the end of search.", 'red') - return metrics, -np.inf - final_best_score = final_best_candidate['score_sum'] / (final_best_candidate['eval_count'] or 1E-9) - - # Log final results - self.logger.log('Final best score', final_best_score, final_iteration, color='green') - self.logger.log('Final best candidate evaluations', final_best_candidate['eval_count'], final_iteration, color='cyan') - self.logger.log('Final buffer size', len(self.buffer), final_iteration, color='blue') - - print_color(f"Final best candidate: Mean Score {final_best_score:.4f}, Evals {final_best_candidate['eval_count']}", 'green') - - # Load best parameters into the agent - self.optimizer.update(final_best_candidate['params']) # Load params using optimizer - - return metrics, float(final_best_score) - - def select(self, buffer): - '''Could be subclassed to implement different selection strategies''' - return max(buffer, key=lambda c: c['ucb_score']) - - -class UCBSearchParallelAlgorithm(UCBSearchAlgorithm): - """ - Parallel UCB Search Algorithm. - - Instead of selecting one candidate with highest UCB score, selects top-k candidates - and processes them in parallel to generate k new candidates per iteration. - """ - - def __init__(self, - agent: trace.Module, - optimizer, - max_buffer_size: int = 10, - ucb_exploration_factor: float = 1.0, - parallel_k: int = 2, # Number of top candidates to process in parallel - logger=None, - num_threads: int = None, - *args, - **kwargs): - super().__init__(agent, optimizer, max_buffer_size, ucb_exploration_factor, - logger, num_threads, *args, **kwargs) - self.parallel_k = parallel_k - - def select_top_k(self, buffer, k): - """Select top k candidates with highest UCB scores""" - if len(buffer) <= k: - return buffer.copy() - - # Sort by UCB score and return top k - sorted_candidates = sorted(buffer, key=lambda c: c['ucb_score'], reverse=True) - return sorted_candidates[:k] - - def train(self, - guide, - train_dataset: Dict[str, List[Any]], - *, - validation_dataset: Optional[Dict[str, List[Any]]] = None, - test_dataset: Optional[Dict[str, List[Any]]] = None, - num_search_iterations: int = 100, - train_batch_size: int = 2, - evaluation_batch_size: int = 20, - eval_frequency: int = 1, - log_frequency: Optional[int] = None, - save_frequency: Optional[int] = None, - save_path: str = "checkpoints/ucb_parallel_agent.pkl", - min_score_for_agent_update: Optional[float] = None, - verbose: Union[bool, str] = False, - num_threads: Optional[int] = None, - print_confidence_interval: bool = True, - **kwargs - ) -> Tuple[Dict[str, Any], float]: - """ - Main training loop for Parallel UCB Search Algorithm. - """ - # Default validation_dataset to train_dataset if not provided - if validation_dataset is None: - validation_dataset = train_dataset - if test_dataset is None: - test_dataset = train_dataset - - num_threads = num_threads or self.num_threads - log_frequency = log_frequency or eval_frequency - self.min_score = min_score_for_agent_update - total_samples = 0 - self.total_proposals = 0 - - # Metrics tracking - metrics = { - 'best_candidate_scores': [], - 'selected_actions_ucb': [], # UCB scores of selected top-k actions - 'new_candidate_scores': [], # Scores of all new candidates - 'buffer_avg_score': [], - 'buffer_avg_evals': [], - 'parallel_k_used': [], # Track how many candidates were actually processed - } - - # Initialize with first candidate (same as parent) - print_color("Evaluating initial parameters using validation_dataset samples...", 'cyan') - initial_params_dict = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} - initial_score, initial_evals = self._evaluate_candidate( - initial_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads - ) - self._total_evaluations_tracker += initial_evals - total_samples += initial_evals - - # Log initial evaluation - self.logger.log('Initial UCB score', initial_score, 0, color='blue') - self.logger.log('Total samples', total_samples, 0, color='cyan') - - initial_candidate_entry = { - 'params': initial_params_dict, - 'score_sum': initial_score * initial_evals if initial_score > -np.inf else 0, - 'eval_count': initial_evals, - 'ucb_score': None, - 'iteration_created': 0 - } - self.buffer.append(initial_candidate_entry) - self._update_buffer_ucb_scores() - print_color(f"Initial candidate: Score {initial_score:.4f}, Evals {initial_evals}", 'yellow') - - # Main search loop - for iteration in range(1, num_search_iterations + 1): - try: - if not self.buffer: - print_color("Buffer is empty, stopping search.", 'red') - break - - # 1. Select top-k candidates with highest UCB scores - self._update_buffer_ucb_scores() - top_k_candidates = self.select_top_k(self.buffer, self.parallel_k) - - if print_confidence_interval: - self.print_intervals(self.buffer) - - print_color(f"Iter {iteration}/{num_search_iterations}: Processing {len(top_k_candidates)} candidates in parallel", 'blue') - - # Log selected actions UCB scores - selected_ucb_scores = [c['ucb_score'] for c in top_k_candidates] - metrics['selected_actions_ucb'].append(selected_ucb_scores) - avg_selected_ucb = np.mean(selected_ucb_scores) - self.logger.log('Average selected UCB', avg_selected_ucb, iteration, color='magenta') - - # 2. Process all top-k candidates sequentially - candidate_results = [] - for candidate in top_k_candidates: - result = self._process_single_candidate( - candidate, guide, train_dataset, validation_dataset, - train_batch_size, evaluation_batch_size, num_threads, iteration - ) - candidate_results.append(result) - - # 3. Process results and update statistics - iteration_new_scores = [] - - for i, (candidate, result) in enumerate(zip(top_k_candidates, candidate_results)): - success, a_prime_score, score_for_a_on_train_batch, samples_used = result - - if not success: # Error occurred - print_color(f"Iter {iteration}: Candidate {i+1} processing failed, skipping.", 'yellow') - continue - # Track new candidate score - iteration_new_scores.append(a_prime_score) - - # Update tracking - total_samples += samples_used - - metrics['new_candidate_scores'].extend(iteration_new_scores) - - # Log iteration performance - if iteration_new_scores: - avg_new_score = np.mean(iteration_new_scores) - max_new_score = max(iteration_new_scores) - self.logger.log('New candidate score', avg_new_score, iteration, color='green') #average new candidate score - self.logger.log('Max new candidate score', max_new_score, iteration, color='green') - print_color(f"Iter {iteration}: Generated {len(iteration_new_scores)} new candidates. Avg score: {avg_new_score:.4f}, Max: {max_new_score:.4f}", 'cyan') - - # Update UCB scores and track metrics - self._update_buffer_ucb_scores() - - if self.buffer: - best_in_buffer = self._get_best_candidate_from_buffer(self.buffer) - if best_in_buffer: - best_score = best_in_buffer['score_sum']/(best_in_buffer['eval_count'] or 1) - metrics['best_candidate_scores'].append(best_score) - else: - metrics['best_candidate_scores'].append(-np.inf) - metrics['buffer_avg_score'].append(np.mean([c['score_sum']/(c['eval_count'] or 1) for c in self.buffer if c['eval_count'] > 0])) - metrics['buffer_avg_evals'].append(np.mean([c['eval_count'] for c in self.buffer])) - - # Logging - if iteration % log_frequency == 0: - self.logger.log('Best candidate score', best_score, iteration, color='green') - self.logger.log('Buffer size', len(self.buffer), iteration, color='blue') - self.logger.log('Buffer average score', metrics['buffer_avg_score'][-1], iteration, color='cyan') - self.logger.log('Total samples', total_samples, iteration, color='yellow') - self.logger.log('Total proposals', self.total_proposals, iteration, color='red') - print_color(f"Log @ Iter {iteration}: Best score: {best_score:.4f}, Buffer size: {len(self.buffer)}, Total samples: {total_samples}", 'green') - - # Test evaluation (same as parent) - if test_dataset is not None and iteration % eval_frequency == 0: - try: - current_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} - best_candidate = self._get_best_candidate_from_buffer(self.buffer) - if not best_candidate: - print_color(f"Iter {iteration}: No valid candidate for test evaluation.", 'yellow') - continue - self.optimizer.update(best_candidate['params']) - - test_score = self.evaluate(self.agent, guide, test_dataset['inputs'], test_dataset['infos'], - min_score=self.min_score, num_threads=num_threads, - description=f"Evaluating best candidate (iteration {iteration})") - - self.optimizer.update(current_params) - self.logger.log('Test score', test_score, iteration, color='green') - except Exception as e: - print_color(f"Iter {iteration}: Test evaluation failed: {e}", 'red') - - # Save agent (same as parent) - if save_frequency is not None and iteration % save_frequency == 0: - try: - best_overall_candidate = self._get_best_candidate_from_buffer(self.buffer) - if not best_overall_candidate: - print_color(f"Iter {iteration}: No valid candidate for agent save.", 'yellow') - continue - self.optimizer.update(best_overall_candidate['params']) - self.save_agent(save_path, iteration) - print_color(f"Iter {iteration}: Saved agent based on best candidate in buffer.", 'green') - except Exception as e: - print_color(f"Iter {iteration}: Agent save failed: {e}", 'red') - - except Exception as e: - print_color(f"Iter {iteration}: Iteration failed with error: {e}. Skipping to next iteration.", 'red') - self.logger.log('Iteration error', str(e), iteration, color='red') - continue - - # End of search (same as parent) - print_color("Parallel UCB search finished.", 'blue') - - final_iteration = num_search_iterations - self.logger.log('Parallel UCB search completed', final_iteration, final_iteration, color='blue') - self.logger.log('Final total samples', total_samples, final_iteration, color='magenta') - - if not self.buffer: - print_color("Buffer is empty at the end of search. No best candidate found.", 'red') - return metrics, -np.inf - - final_best_candidate = self._get_best_candidate_from_buffer(self.buffer) - if not final_best_candidate: - print_color("No valid candidate found at the end of search.", 'red') - return metrics, -np.inf - final_best_score = final_best_candidate['score_sum'] / (final_best_candidate['eval_count'] or 1E-9) - - self.logger.log('Final best score', final_best_score, final_iteration, color='green') - print_color(f"Final best candidate: Mean Score {final_best_score:.4f}, Evals {final_best_candidate['eval_count']}", 'green') - - # Load best parameters into the agent - self.optimizer.update(final_best_candidate['params']) - - return metrics, float(final_best_score) - - -class HybridUCB_LLM(MinibatchAlgorithm): - """ - UCB Search Algorithm with Function Approximation (LLM). - - Keeps a buffer of candidates. - In each iteration: - - With probability alpha: - 1. Picks a candidate 'a' from the buffer with the highest UCB score. - 2. Updates the optimizer with 'a's parameters. - 3. Draws a minibatch from the training set, performs a forward/backward pass, and calls optimizer.step() to get a new candidate 'a_prime'. - 4. Evaluates 'a_prime' on a validation set minibatch. - 5. Updates statistics of 'a' (based on the training minibatch). - 6. Adds 'a_prime' (with its validation stats) to the buffer. - - With probability 1-alpha: - 1. Uses an external LLM, prompted with candidates from the buffer, to generate a new candidate 'a_prime'. - 2. Evaluates 'a_prime' on a validation set minibatch. - 3. Adds 'a_prime' (with its validation stats) to the buffer. - If the buffer is full, evicts the candidate with the lowest UCB score. - """ - - def __init__(self, - agent: trace.Module, - optimizer, - max_buffer_size: int = 10, - ucb_exploration_factor: float = 0.3, - alpha: float = 0.3, - llm_model: str = None, - num_samples_in_prompt: int = 5, - logger=None, - num_threads: int = None, - *args, - **kwargs): - super().__init__(agent, optimizer, num_threads=num_threads, logger=logger, *args, **kwargs) - - self.alpha = alpha - self.llm_model = llm_model - self.num_samples_in_prompt = num_samples_in_prompt - self.llm_prompt_budget_factor = 0.5 - - self.buffer = deque(maxlen=max_buffer_size) - self.max_buffer_size = max_buffer_size - self.ucb_exploration_factor = ucb_exploration_factor - - if not hasattr(self.optimizer, 'step'): - raise ValueError("Optimizer must have a 'step' method.") - - self._total_evaluations_tracker = 0 - - # Initialize LLM - self.llm = LLM(model=self.llm_model) - print_color(f"Initialized HybridUCB_LLM with alpha={self.alpha}, LLM model={self.llm_model}", "cyan") - - def _sample_minibatch(self, dataset: Dict[str, List[Any]], batch_size: int) -> Tuple[List[Any], List[Any]]: - """Sample a minibatch from the dataset.""" - if not dataset or not dataset.get('inputs') or not dataset.get('infos'): - print_color("Warning: Attempted to sample from an empty or malformed dataset.", color='yellow') - return [], [] - - dataset_size = len(dataset['inputs']) - if dataset_size == 0: - print_color("Warning: Dataset is empty, cannot sample minibatch.", color='yellow') - return [], [] - - actual_batch_size = min(batch_size, dataset_size) - indices = np.random.choice(dataset_size, actual_batch_size, replace=False) - xs = [dataset['inputs'][i] for i in indices] - infos = [dataset['infos'][i] for i in indices] - return xs, infos - - def _evaluate_candidate(self, - params_to_eval_dict: Dict[str, Any], - dataset: Dict[str, List[Any]], - guide, - evaluation_batch_size: int, - num_threads: Optional[int] = None - ) -> Tuple[float, int]: - """Evaluates a given set of parameters on samples from the provided dataset.""" - if not dataset or not dataset.get('inputs') or not dataset.get('infos') or not dataset['inputs']: - print_color("Evaluation dataset is empty or invalid. Returning score -inf, count 0.", color='yellow') - return -np.inf, 0 - - original_params_backup = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} - - try: - self.optimizer.update(params_to_eval_dict) - except Exception as e: - print_color(f"Error updating agent with params_to_eval_dict: {e}. Using current agent state for eval.", "red") - - eval_xs, eval_infos = self._sample_minibatch(dataset, evaluation_batch_size) - - if not eval_xs: - print_color("Evaluation minibatch is empty. Returning score -inf, count 0.", color='yellow') - self.optimizer.update(original_params_backup) - return -np.inf, 0 - - eval_scores = evaluate(self.agent, - guide, - eval_xs, - eval_infos, - min_score=self.min_score if hasattr(self, 'min_score') else None, - num_threads=num_threads or self.num_threads, - description=f"Evaluating candidate") - - self.optimizer.update(original_params_backup) - - avg_score = np.mean(eval_scores) if eval_scores and all(s is not None for s in eval_scores) else 0 - eval_count = len(eval_xs) - - return float(avg_score), eval_count - - def _calculate_ucb(self, candidate_buffer_entry: Dict, total_tracked_evaluations: int) -> float: - """Calculates UCB score for a candidate in the buffer.""" - if candidate_buffer_entry['eval_count'] == 0: - return float('inf') - - mean_score = candidate_buffer_entry['score_sum'] / candidate_buffer_entry['eval_count'] - - if total_tracked_evaluations == 0: - total_tracked_evaluations = 1 - - exploration_term = self.ucb_exploration_factor * \ - math.sqrt(math.log(total_tracked_evaluations + 1e-9) / candidate_buffer_entry['eval_count']) - - return mean_score + exploration_term - - def _calculate_lcb(self, candidate_buffer_entry: Dict, total_tracked_evaluations: int) -> float: - """Calculates Lower Confidence Bound for a candidate in the buffer.""" - if candidate_buffer_entry['eval_count'] == 0: - return float('-inf') # Unvisited states get lowest bound - - mean_score = candidate_buffer_entry['score_sum'] / candidate_buffer_entry['eval_count'] - - # Add 1 to total_tracked_evaluations to prevent log(0) if it's the first evaluation overall - # and to ensure log argument is > 0. - # Add 1 to eval_count in denominator as well to ensure it's robust if eval_count is small. - if total_tracked_evaluations == 0: # Should not happen if we init with one eval - total_tracked_evaluations = 1 - - # LCB exploration term: ucb_exploration_factor scales the confidence interval - # Higher factor = more exploration, lower factor = more exploitation - exploration_term = self.ucb_exploration_factor * \ - math.sqrt(math.log(total_tracked_evaluations) / candidate_buffer_entry['eval_count']) - - return mean_score - exploration_term - - def _update_buffer_ucb_scores(self): - """Recalculates and updates UCB scores for all candidates in the buffer.""" - if not self.buffer: - return - - for candidate_entry in self.buffer: - candidate_entry['ucb_score'] = self._calculate_ucb(candidate_entry, self._total_evaluations_tracker) - - def _get_best_candidate_from_buffer(self, buffer): - """Get the best candidate from buffer, excluding those with eval_count = 0.""" - if not buffer: - return None - - # Filter out candidates with eval_count = 0 - valid_candidates = [c for c in buffer if c['eval_count'] > 0] - if not valid_candidates: - # If no candidates have been evaluated, return the one with highest UCB score - return max(buffer, key=lambda c: c.get('ucb_score', -float('inf'))) - return max(valid_candidates, key=lambda c: c['score_sum'] / c['eval_count']) - - def print_intervals(self, buffer): - """Print confidence intervals for debugging in the form of open intervals (LCB, UCB)""" - print_color("Confidence intervals for all candidates:", 'cyan') - for i, candidate_entry in enumerate(buffer): - lcb = self._calculate_lcb(candidate_entry, self._total_evaluations_tracker) - ucb = candidate_entry['ucb_score'] - mean_score = candidate_entry['score_sum'] / (candidate_entry['eval_count'] or 1) - eval_count = candidate_entry['eval_count'] - - # Format as open interval (LCB, UCB) with mean score and evaluation count - interval_str = f"Action {i+1}: ({lcb:.4f}, {ucb:.4f}) [mean: {mean_score:.4f}, n: {eval_count}]" - print_color(interval_str, 'cyan') - - def _llm_generate_candidate(self) -> Optional[Dict[trace.nodes.ParameterNode, str]]: - """ - Prompts an LLM with current buffer candidates to generate new string values for parameters. - Returns a dictionary mapping ParameterNode objects to new string values, or None on failure. - """ - print_color("Attempting to generate candidate using LLM...", "blue") - if not self.buffer: - print_color("LLM generation: Buffer is empty, cannot provide context to LLM.", "yellow") - return None - - sorted_buffer = sorted(list(self.buffer), key=lambda c: c.get('ucb_score', -float('inf')), reverse=True) - # Include first, last, and evenly spaced middle candidates - if len(sorted_buffer) <= self.num_samples_in_prompt: - prompt_candidates = sorted_buffer - elif self.num_samples_in_prompt <= 2: - # If only 1-2 samples requested, take first and optionally last - prompt_candidates = sorted_buffer[:self.num_samples_in_prompt] - else: - # Take first, last, and evenly spaced middle candidates - prompt_candidates = [sorted_buffer[0]] # First (highest UCB) - if self.num_samples_in_prompt > 2: - # Calculate indices for middle candidates - middle_count = self.num_samples_in_prompt - 2 # Exclude first and last - if middle_count > 0 and len(sorted_buffer) > 2: - # Evenly space middle candidates between index 1 and len-2 - middle_indices = [int(1 + i * (len(sorted_buffer) - 2) / (middle_count + 1)) - for i in range(1, middle_count + 1)] - prompt_candidates.extend([sorted_buffer[i] for i in middle_indices]) - prompt_candidates.append(sorted_buffer[-1]) # Last (lowest UCB) - - serializable_candidate_summaries = [] - for cand_entry in prompt_candidates: - summary = { - "parameters": {getattr(p,'py_name'): copy.deepcopy(p.data) for p in cand_entry['params']}, - "eval_count": cand_entry['eval_count'], - "ucb_score": round(cand_entry.get('ucb_score',0), 4), - } - serializable_candidate_summaries.append(summary) - - example_param_structure_json_str = {getattr(p,'py_name'): copy.deepcopy(p.data) for p in self.agent.parameters()} - - prompt_messages = [ - {"role": "system", "content": "You are an expert in model optimization. Your task is to propose new string values for model parameters with high UCB scores. Please output ONLY a valid JSON dictionary where keys are parameter names and values are the new string values for those parameters, matching the example structure provided. Do not add any explanations or markdown formatting around the JSON."}, - {"role": "user", "content": f"Here are some current candidates from the search buffer and their statistics:\\n{serializable_candidate_summaries}\\n\\nHere is an example of the required JSON output structure (parameter names as keys, new string values as values):\\n{example_param_structure_json_str}\\n\\nPlease generate a new set of parameters in exactly the same JSON format. Make sure use double quotes for the keys and values."} - ] - - print_color(f"LLM prompt (summary): {len(prompt_candidates)} candidates, structure example provided.", "magenta") - response_format = {"type": "json_object"} - llm_response = self.llm(prompt_messages, response_format=response_format) - llm_response_str = llm_response.choices[0].message.content - - if not llm_response_str: - print_color("LLM returned an empty response.", "red") - return None - - cleaned_llm_response_str = llm_response_str.strip() - - try: - llm_params_raw = json.loads(cleaned_llm_response_str) - except json.JSONDecodeError as e: - print_color(f"JSON parsing attempts failed: {e}", "red") - print_color("Returning the candidate with the highest UCB score in the buffer.", "red") - return max(self.buffer, key=lambda c: c.get('ucb_score', -float('inf')))['params'] - - if not isinstance(llm_params_raw, dict): - print_color(f"LLM output was not a JSON dictionary after parsing: {type(llm_params_raw)}", "red") - print_color("Returning the candidate with the highest UCB score in the buffer.", "red") - return max(self.buffer, key=lambda c: c.get('ucb_score', -float('inf')))['params'] - - candidate_params_dict = self.construct_update_dict(llm_params_raw) - return candidate_params_dict - - def construct_update_dict(self, suggestion: Dict[str, Any]) -> Dict[ParameterNode, Any]: - """Convert the suggestion in text into the right data type.""" - update_dict = {} - for node in self.agent.parameters(): - if node.trainable and node.py_name in suggestion: - try: - formatted_suggestion = suggestion[node.py_name] - if type(formatted_suggestion) == str and 'def' in formatted_suggestion: - formatted_suggestion = format_str(formatted_suggestion, mode=FileMode()) - update_dict[node] = type(node.data)(formatted_suggestion) - except (ValueError, KeyError) as e: - if getattr(self, 'ignore_extraction_error', False): - warnings.warn( - f"Cannot convert the suggestion '{suggestion[node.py_name]}' for {node.py_name} to the right data type" - ) - else: - raise e - return update_dict - - def train(self, - guide, - train_dataset: Dict[str, List[Any]], - *, - num_search_iterations: int = 100, - validation_dataset: Dict[str, List[Any]] = None, - test_dataset: Dict[str, List[Any]] = None, - train_batch_size: int = 5, - evaluation_batch_size: int = 5, - eval_frequency: int = 1, - log_frequency: Optional[int] = None, - save_frequency: Optional[int] = None, - save_path: str = "checkpoints/ucb_llm_agent.pkl", - min_score_for_agent_update: Optional[float] = None, - verbose: Union[bool, str] = False, - num_threads: Optional[int] = None, - print_confidence_interval: bool = True, - **kwargs - ) -> Tuple[Dict[str, Any], float]: - - if validation_dataset is None: - validation_dataset = train_dataset - if test_dataset is None: - test_dataset = train_dataset - - num_threads = num_threads or self.num_threads - log_frequency = log_frequency or eval_frequency - self.min_score = min_score_for_agent_update - total_samples = 0 - self.total_proposals = 0 - - metrics = { - 'best_candidate_scores': [], - 'selected_action_ucb': [], - 'new_candidate_scores': [], - 'buffer_avg_score': [], - 'buffer_avg_evals': [], - 'llm_generation_failures': 0, - 'generation_path': [] - } - - # Initial candidate evaluation - print_color("Evaluating initial parameters using train_dataset samples...", 'cyan') - initial_params_dict = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} - - initial_score, initial_evals = self._evaluate_candidate( - initial_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads - ) - self._total_evaluations_tracker += initial_evals - total_samples += initial_evals - - initial_candidate_entry = { - 'params': initial_params_dict, - 'score_sum': initial_score * initial_evals if initial_score > -np.inf else 0, - 'eval_count': initial_evals, - 'ucb_score': 0.0, - 'iteration_created': 0 - } - self.buffer.append(initial_candidate_entry) - self._update_buffer_ucb_scores() - print_color(f"Initial candidate: Score {initial_score:.4f}, Evals {initial_evals}", 'yellow') - - # Log initial evaluation - self.logger.log('Initial UCB score', initial_score, 0, color='blue') - self.logger.log('Total samples', total_samples, 0, color='cyan') - self.logger.log('Total proposals', self.total_proposals, 0, color='red') - - # Main search loop - for iteration in range(1, num_search_iterations + 1): - try: - if not self.buffer: - print_color("Buffer is empty, stopping search.", 'red') - break - - self._update_buffer_ucb_scores() - a_prime_params_dict = None - a_prime_score = 0 - a_prime_evals = 0 - generation_method = "none" - if print_confidence_interval: - self.print_intervals(self.buffer) - - if iteration<=2 or random.random() < self.alpha: # UCB Path, for the first 2 iterations, we always use UCB because the buffer size is small, it's hard for LLM to generate good candidates - generation_method = "ucb" - metrics['generation_path'].append("ucb") - if not self.buffer: - print_color(f"Iter {iteration} (UCB Path): Buffer empty, cannot select action. Skipping.", "red") - continue - - action_candidate_a = self.select(self.buffer) - - selected_mean_score = action_candidate_a['score_sum'] / action_candidate_a['eval_count'] if action_candidate_a['eval_count'] > 0 else -np.inf - print_color(f"Iter {iteration} (UCB Path): Selected action candidate (UCB: {action_candidate_a['ucb_score']:.4f}, MeanScore: {selected_mean_score:.4f} Evals: {action_candidate_a['eval_count']})", 'blue') - # metrics['selected_action_ucb'].append(action_candidate_a['ucb_score']) - - # Log selected action UCB score - # self.logger.log('Selected action UCB', action_candidate_a['ucb_score'], iteration, color='magenta') - # self.logger.log('Selected action mean score', selected_mean_score, iteration, color='cyan') - - self.optimizer.update(action_candidate_a['params']) - - train_xs, train_infos = self._sample_minibatch(train_dataset, train_batch_size) - if not train_xs: - print_color(f"Iter {iteration} (UCB Path): Training minibatch empty, skipping optimizer step.", 'yellow') - continue - - total_samples += len(train_xs) - - # Forward pass for 'a' - outputs_for_a = [] - use_asyncio = self._use_asyncio(num_threads) - if use_asyncio: - outputs_for_a = async_run([self.forward]*len(train_xs), - [(self.agent, x, guide, info) for x, info in zip(train_xs, train_infos)], - max_workers=num_threads, - description=f"Iter {iteration} (UCB): Forward for 'a'") - else: - outputs_for_a = [self.forward(self.agent, x, guide, info) for x, info in zip(train_xs, train_infos)] - - scores_from_train, targets_from_train, feedbacks_from_train = [], [], [] - for target, score, feedback in outputs_for_a: - scores_from_train.append(score) - targets_from_train.append(target) - feedbacks_from_train.append(feedback) - - if not scores_from_train: - print_color(f"Iter {iteration} (UCB Path): No outputs from forward pass for 'a'. Skipping.", 'yellow') - continue - - target_for_a = batchify(*targets_from_train) - feedback_for_a = batchify(*feedbacks_from_train).data - score_for_a_on_train_batch = np.mean([s for s in scores_from_train if s is not None]) if any(s is not None for s in scores_from_train) else 0 - - self.optimizer.zero_feedback() - self.optimizer.backward(target_for_a, feedback_for_a) - - # Get a_prime by optimizer step - try: - returned_params = self.optimizer.step(bypassing=True, verbose=False) - if not isinstance(returned_params, dict) or not returned_params: - print_color(f"Iter {iteration} (UCB Path): Optimizer.step did not return a valid param dict for a_prime. Using current agent params.", 'yellow') - a_prime_params_dict = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} - else: - a_prime_params_dict = {p: copy.deepcopy(p.data) for p in returned_params} - self.total_proposals += 1 - - except Exception as e: - print_color(f"Iter {iteration} (UCB Path): Error during optimizer.step for a_prime: {e}. Skipping.", 'red') - continue - - # Evaluate 'a' and 'a_prime' on validation set in parallel (like UCBSearchAlgorithm) - use_asyncio = self._use_asyncio(num_threads) - if use_asyncio: - evaluation_results = async_run( - [self._evaluate_candidate, self._evaluate_candidate], - [ - (action_candidate_a['params'], validation_dataset, guide, evaluation_batch_size, num_threads), - (a_prime_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads) - ], - max_workers=2, - description=f"Iter {iteration} (UCB): Parallel evaluation of 'a' and 'a_prime'" - ) - (a_score, a_evals), (a_prime_score, a_prime_evals) = evaluation_results - else: - a_score, a_evals = self._evaluate_candidate( - action_candidate_a['params'], validation_dataset, guide, evaluation_batch_size, num_threads - ) - a_prime_score, a_prime_evals = self._evaluate_candidate( - a_prime_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads - ) - - self._total_evaluations_tracker += a_evals + a_prime_evals - total_samples += a_evals + a_prime_evals - - # Update stats of action_candidate_a - if score_for_a_on_train_batch > -np.inf: - action_candidate_a['score_sum'] += score_for_a_on_train_batch * len(train_xs) - action_candidate_a['eval_count'] += len(train_xs) - self._total_evaluations_tracker += len(train_xs) - - # Update stats with validation evaluation of 'a' - action_candidate_a['score_sum'] += a_score * a_evals - action_candidate_a['eval_count'] += a_evals - - print_color(f"Iter {iteration} (UCB Path): New candidate a_prime (from UCB) generated. Eval Score: {a_prime_score:.4f}, Evals: {a_prime_evals}", 'cyan') - self.logger.log('New candidate score', a_prime_score, iteration, color='green') - self.logger.log('Training batch score', score_for_a_on_train_batch, iteration, color='yellow') - else: # LLM Pathcandi - generation_method = "llm" - metrics['generation_path'].append("llm") - print_color(f"Iter {iteration} (LLM Path): Generating candidate via LLM.", 'blue') - a_prime_params_dict = self._llm_generate_candidate() - - if a_prime_params_dict: - # Evaluate a_prime (from LLM path) - a_prime_score, a_prime_evals = self._evaluate_candidate( - a_prime_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads - ) - self._total_evaluations_tracker += a_prime_evals - total_samples += a_prime_evals - self.total_proposals += 1 - print_color(f"Iter {iteration} (LLM Path): New candidate a_prime (from LLM) generated. Eval Score: {a_prime_score:.4f}, Evals: {a_prime_evals}", 'cyan') - self.logger.log('New candidate score', a_prime_score, iteration, color='green') #average new candidate score - else: - print_color(f"Iter {iteration} (LLM Path): LLM failed to generate a valid candidate. Skipping addition to buffer.", 'red') - metrics['llm_generation_failures'] += 1 - continue - - # Common logic for adding a_prime to buffer - metrics['new_candidate_scores'].append(a_prime_score) - - if a_prime_params_dict and a_prime_score > -np.inf and a_prime_evals > 0: - new_candidate_entry = { - 'params': a_prime_params_dict, - 'score_sum': a_prime_score * a_prime_evals, - 'eval_count': a_prime_evals, - 'ucb_score': 0.0, - 'iteration_created': iteration - } - - if len(self.buffer) == self.max_buffer_size: - self._update_buffer_ucb_scores() - candidate_to_evict = min(self.buffer, key=lambda c: c['ucb_score']) - self.buffer.remove(candidate_to_evict) - evicted_mean_score = candidate_to_evict['score_sum'] / candidate_to_evict['eval_count'] if candidate_to_evict['eval_count'] > 0 else -np.inf - print_color(f"Iter {iteration}: Buffer full. Evicted candidate (UCB: {candidate_to_evict['ucb_score']:.4f}, MeanScore: {evicted_mean_score:.4f})", 'magenta') - - self.buffer.append(new_candidate_entry) - print_color(f"Iter {iteration}: Added new candidate (from {generation_method}) to buffer.", 'magenta') - elif a_prime_params_dict: - print_color(f"Iter {iteration}: New candidate a_prime (from {generation_method}) had invalid score/evals ({a_prime_score}, {a_prime_evals}), not added to buffer.", 'yellow') - - self._update_buffer_ucb_scores() - - # Logging - if self.buffer: - best_in_buffer = max(self.buffer, key=lambda c: (c['score_sum']/(c['eval_count'] if c['eval_count'] > 0 else 1))) - current_best_score = best_in_buffer['score_sum']/(best_in_buffer['eval_count'] if best_in_buffer['eval_count'] > 0 else 1) - metrics['best_candidate_scores'].append(current_best_score) - - valid_scores = [c['score_sum']/(c['eval_count'] if c['eval_count'] > 0 else 1) for c in self.buffer if c['eval_count'] > 0] - metrics['buffer_avg_score'].append(np.mean(valid_scores) if valid_scores else -np.inf) - metrics['buffer_avg_evals'].append(np.mean([c['eval_count'] for c in self.buffer])) - else: - metrics['best_candidate_scores'].append(0) - metrics['buffer_avg_score'].append(0) - metrics['buffer_avg_evals'].append(0) - - if iteration % log_frequency == 0: - log_data = { - "iteration": iteration, - "best_score": metrics['best_candidate_scores'][-1], - "newly_evaluated_candidate_score": a_prime_score, - "buffer_size": len(self.buffer), - "buffer_avg_score": metrics['buffer_avg_score'][-1], - "buffer_avg_evals": metrics['buffer_avg_evals'][-1], - "total_evaluations_ucb_T": self._total_evaluations_tracker, - "total_samples": total_samples, - "generation_method_this_iter": generation_method, - "llm_generation_total_failures": metrics['llm_generation_failures'] - } - if generation_method == "ucb" and metrics['selected_action_ucb']: - log_data["selected_action_ucb"] = metrics['selected_action_ucb'][-1] - - # Log all important metrics - self.logger.log('Best candidate score', log_data['best_score'], iteration, color='green') - self.logger.log('Buffer size', log_data['buffer_size'], iteration, color='blue') - self.logger.log('Buffer average score', log_data['buffer_avg_score'], iteration, color='cyan') - self.logger.log('Buffer average evaluations', log_data['buffer_avg_evals'], iteration, color='orange') - self.logger.log('Total samples', log_data['total_samples'], iteration, color='yellow') - self.logger.log('Total proposals', self.total_proposals, iteration, color='red') - - print_color(f"Log @ Iter {iteration}: Best score in buffer: {log_data['best_score']:.4f}, Gen method: {generation_method}, Buffer size: {len(self.buffer)}, Total samples: {total_samples}", 'green') - - if test_dataset is not None and iteration % eval_frequency == 0: - try: - # Save current agent parameters - current_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} - - # Find the best candidate in the buffer (highest mean score) - best_candidate = self._get_best_candidate_from_buffer(self.buffer) - if not best_candidate: - print_color(f"Iter {iteration}: No valid candidate for test evaluation.", 'yellow') - continue - - # Load best candidate's parameters into the agent for evaluation - self.optimizer.update(best_candidate['params']) - - # Evaluate the best candidate on test set - test_score = self.evaluate(self.agent, guide, test_dataset['inputs'], test_dataset['infos'], - min_score=self.min_score, num_threads=num_threads, - description=f"Evaluating best candidate (iteration {iteration})") - - # Restore original agent parameters - self.optimizer.update(current_params) - - self.logger.log('Test score', test_score, iteration, color='green') - except Exception as e: - print_color(f"Iter {iteration}: Test evaluation failed: {e}", 'red') - - if save_frequency is not None and iteration % save_frequency == 0 and self.buffer: - try: - best_overall_candidate_entry = max(self.buffer, key=lambda c: (c['score_sum'] / (c['eval_count'] if c['eval_count'] > 0 else 1E-9))) - self.optimizer.update(best_overall_candidate_entry['params']) - if hasattr(self, 'save_agent'): - self.save_agent(save_path, iteration) - best_mean_score_for_save = best_overall_candidate_entry['score_sum'] / (best_overall_candidate_entry['eval_count'] if best_overall_candidate_entry['eval_count'] > 0 else 1E-9) - print_color(f"Iter {iteration}: Saved agent based on best candidate in buffer (Mean Score: {best_mean_score_for_save:.4f}).", 'green') - else: - print_color(f"Iter {iteration}: save_agent method not found, skipping save.", 'yellow') - except Exception as e: - print_color(f"Iter {iteration}: Agent save failed: {e}", 'red') - - except Exception as e: - print_color(f"Iter {iteration}: Iteration failed with error: {e}. Skipping to next iteration.", 'red') - self.logger.log('Iteration error', str(e), iteration, color='red') - continue - - print_color("UCB-LLM search finished.", 'blue') - - final_best_candidate = max(self.buffer, key=lambda c: (c['score_sum'] / (c['eval_count'] if c['eval_count'] > 0 else 1E-9))) - final_best_score = final_best_candidate['score_sum'] / (final_best_candidate['eval_count'] if final_best_candidate['eval_count'] > 0 else 1E-9) - final_best_evals = final_best_candidate['eval_count'] - print_color(f"Final best candidate: Mean Score {final_best_score:.4f}, Evals {final_best_evals}", 'green') - - self.optimizer.update(final_best_candidate['params']) - - return metrics, float(final_best_score) - - def select(self, buffer): - '''Selects candidate with highest UCB score.''' - if not buffer: return None - return max(buffer, key=lambda c: c.get('ucb_score', -float('inf'))) - - -class UCBSearchFunctionApproximationAlgorithm(UCBSearchAlgorithm): - """ - UCB Search Algorithm that uses LLM function approximation to select candidates. - """ - - def __init__(self, llm_model,num_samples_in_prompt:int=5, *args, **kwargs): - super().__init__(*args, **kwargs) - self.llm_model = llm_model - self.llm = LLM(model=self.llm_model) - self.num_samples_in_prompt = num_samples_in_prompt - print_color(f"Initialized UCBSearchFunctionApproximationAlgorithm with LLM model={self.llm_model}", "cyan") - - def select(self, buffer): - """Generate a new candidate entry using LLM. Note: this doesn't add it to the buffer.""" - new_action_params = self._llm_generate_candidate() - new_candidate_entry = { - 'params': new_action_params, - 'score_sum': 0, - 'eval_count': 0, - 'ucb_score': 0.0, - 'iteration_created': 0 - } - return new_candidate_entry - - def _llm_generate_candidate(self) -> Optional[Dict[trace.nodes.ParameterNode, str]]: - """ - Prompts an LLM with current buffer candidates to generate new string values for parameters. - Returns a dictionary mapping ParameterNode objects to new string values, or None on failure. - """ - print_color("Attempting to generate candidate using LLM...", "blue") - if not self.buffer: - print_color("LLM generation: Buffer is empty, cannot provide context to LLM.", "yellow") - return None - - sorted_buffer = sorted(list(self.buffer), key=lambda c: c.get('ucb_score', -float('inf')), reverse=True) - # Include first, last, and evenly spaced middle candidates - if len(sorted_buffer) <= self.num_samples_in_prompt: - prompt_candidates = sorted_buffer - elif self.num_samples_in_prompt <= 2: - # If only 1-2 samples requested, take first and optionally last - prompt_candidates = sorted_buffer[:self.num_samples_in_prompt] - else: - # Take first, last, and evenly spaced middle candidates - prompt_candidates = [sorted_buffer[0]] # First (highest UCB) - if self.num_samples_in_prompt > 2: - # Calculate indices for middle candidates - middle_count = self.num_samples_in_prompt - 2 # Exclude first and last - if middle_count > 0 and len(sorted_buffer) > 2: - # Evenly space middle candidates between index 1 and len-2 - middle_indices = [int(1 + i * (len(sorted_buffer) - 2) / (middle_count + 1)) - for i in range(1, middle_count + 1)] - prompt_candidates.extend([sorted_buffer[i] for i in middle_indices]) - prompt_candidates.append(sorted_buffer[-1]) # Last (lowest UCB) - - serializable_candidate_summaries = [] - for cand_entry in prompt_candidates: - summary = { - "parameters": {getattr(p,'py_name'): copy.deepcopy(p.data) for p in cand_entry['params']}, - "eval_count": cand_entry['eval_count'], - "ucb_score": round(cand_entry.get('ucb_score',0), 4), - } - serializable_candidate_summaries.append(summary) - - example_param_structure_json_str = {getattr(p,'py_name'): copy.deepcopy(p.data) for p in self.agent.parameters()} - - prompt_messages = [ - {"role": "system", "content": "You are an expert in model optimization. Your task is to propose new string values for model parameters with high UCB scores. Please output ONLY a valid JSON dictionary where keys are parameter names and values are the new string values for those parameters, matching the example structure provided. Do not add any explanations or markdown formatting around the JSON."}, - {"role": "user", "content": f"Here are some current candidates from the search buffer and their statistics:\\n{serializable_candidate_summaries}\\n\\nHere is an example of the required JSON output structure (parameter names as keys, new string values as values):\\n{example_param_structure_json_str}\\n\\nPlease generate a new set of parameters in exactly the same JSON format. Make sure use double quotes for the keys and values."} - ] - - print_color(f"LLM prompt (summary): {len(prompt_candidates)} candidates, structure example provided.", "magenta") - response_format = {"type": "json_object"} - llm_response = self.llm(prompt_messages, response_format=response_format) - llm_response_str = llm_response.choices[0].message.content - - if not llm_response_str: - print_color("LLM returned an empty response.", "red") - return None - - cleaned_llm_response_str = llm_response_str.strip() - - try: - llm_params_raw = json.loads(cleaned_llm_response_str) - self.total_proposals += 1 - except json.JSONDecodeError as e: - print_color(f"JSON parsing attempts failed: {e}", "red") - print_color("Returning the candidate with the highest UCB score in the buffer.", "red") - return max(self.buffer, key=lambda c: c.get('ucb_score', -float('inf')))['params'] - - if not isinstance(llm_params_raw, dict): - print_color(f"LLM output was not a JSON dictionary after parsing: {type(llm_params_raw)}", "red") - print_color("Returning the candidate with the highest UCB score in the buffer.", "red") - return max(self.buffer, key=lambda c: c.get('ucb_score', -float('inf')))['params'] - - candidate_params_dict = self.construct_update_dict(llm_params_raw) - return candidate_params_dict - - def construct_update_dict(self, suggestion: Dict[str, Any]) -> Dict[ParameterNode, Any]: - """Convert the suggestion in text into the right data type.""" - update_dict = {} - for node in self.agent.parameters(): - if node.trainable and node.py_name in suggestion: - try: - formatted_suggestion = suggestion[node.py_name] - if type(formatted_suggestion) == str and 'def' in formatted_suggestion: - formatted_suggestion = format_str(formatted_suggestion, mode=FileMode()) - update_dict[node] = type(node.data)(formatted_suggestion) - except (ValueError, KeyError) as e: - if getattr(self, 'ignore_extraction_error', False): - warnings.warn( - f"Cannot convert the suggestion '{suggestion[node.py_name]}' for {node.py_name} to the right data type" - ) - else: - raise e - return update_dict From 29785510712cf62e74d339de0cca1a51111fc612 Mon Sep 17 00:00:00 2001 From: windweller Date: Thu, 10 Jul 2025 22:35:12 -0400 Subject: [PATCH 105/314] add updated XML parsing, add fix to ProblemInstance --- opto/optimizers/optoprime_v2.py | 148 ++++++++++-------- .../unit_tests/test_optimizer_xml_parsing.py | 3 +- 2 files changed, 88 insertions(+), 63 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index bf0a3de7..d0094779 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -120,64 +120,13 @@ def extract_xml_like_data(text: str, reasoning_tag: str = "reasoning", value_block = extract_first_top_level_block(var_block, value_tag) # Only add if both name and value tags are present and name is non-empty after stripping if name_block is not None and value_block is not None: - var_name = strip_nested_blocks(name_block, name_tag).strip() + var_name = name_block.strip() var_value = value_block.strip() if value_block is not None else '' if var_name: # Only require name to be non-empty, value can be empty result['variables'][var_name] = var_value return result -@dataclass -class ProblemInstance: - instruction: str - code: str - documentation: str - variables: str - inputs: str - others: str - outputs: str - feedback: str - - problem_template = dedent( - """ - # Instruction - {instruction} - - # Code - {code} - - # Documentation - {documentation} - - # Variables - {variables} - - # Inputs - {inputs} - - # Others - {others} - - # Outputs - {outputs} - - # Feedback - {feedback} - """ - ) - - def __repr__(self) -> str: - return self.problem_template.format( - instruction=self.instruction, - code=self.code, - documentation=self.documentation, - variables=self.variables, - inputs=self.inputs, - outputs=self.outputs, - others=self.others, - feedback=self.feedback, - ) - class OptimizerPromptSymbolSet: """ @@ -225,6 +174,19 @@ def output_response_extractor(self, response: str) -> Dict[str, Any]: else: raise NotImplementedError( "If you supplied a custom output format prompt template, you need to implement your own response extractor") + + @property + def default_prompt_symbols(self) -> Dict[str, str]: + return { + "variables": self.variables_section_title, + "inputs": self.inputs_section_title, + "outputs": self.outputs_section_title, + "others": self.others_section_title, + "feedback": self.feedback_section_title, + "instruction": self.instruction_section_title, + "code": self.code_section_title, + "documentation": self.documentation_section_title, + } class OptimizerPromptSymbolSet2(OptimizerPromptSymbolSet): @@ -249,6 +211,77 @@ class OptimizerPromptSymbolSet2(OptimizerPromptSymbolSet): value_tag = "data" +@dataclass +class ProblemInstance: + instruction: str + code: str + documentation: str + variables: str + inputs: str + others: str + outputs: str + feedback: str + + optimizer_prompt_symbol_set: OptimizerPromptSymbolSet + + problem_template = dedent( + """ + # Instruction + {instruction} + + # Code + {code} + + # Documentation + {documentation} + + # Variables + {variables} + + # Inputs + {inputs} + + # Others + {others} + + # Outputs + {outputs} + + # Feedback + {feedback} + """ + ) + + def __repr__(self) -> str: + return self.replace_symbols(self.problem_template.format( + instruction=self.instruction, + code=self.code, + documentation=self.documentation, + variables=self.variables, + inputs=self.inputs, + outputs=self.outputs, + others=self.others, + feedback=self.feedback, + ), self.optimizer_prompt_symbol_set.default_prompt_symbols) + + def replace_symbols(self, text: str, symbols: Dict[str, str]) -> str: + default_prompt_symbols = { + "variables": "# Variables", + "constraints": "# Constraints", + "inputs": "# Inputs", + "outputs": "# Outputs", + "others": "# Others", + "feedback": "# Feedback", + "instruction": "# Instruction", + "code": "# Code", + "documentation": "# Documentation", + } + + for k, v in symbols.items(): + text = text.replace(default_prompt_symbols[k], v) + return text + + # TODO: solution1 -> solution2 -> solution3 # TODO: param(solution) optimzer.step(solution, "reward is 1, maximize1) -> solution 2 # TODO: maybe have a trace.train() # simpler even than Algorithm, and cover 80% of use cases @@ -413,16 +446,7 @@ def __init__( self.summary_log = [] if log else None self.memory = FIFOBuffer(memory_size) - self.default_prompt_symbols = { - "variables": self.optimizer_prompt_symbol_set.variables_section_title, - "inputs": self.optimizer_prompt_symbol_set.inputs_section_title, - "outputs": self.optimizer_prompt_symbol_set.outputs_section_title, - "others": self.optimizer_prompt_symbol_set.others_section_title, - "feedback": self.optimizer_prompt_symbol_set.feedback_section_title, - "instruction": self.optimizer_prompt_symbol_set.instruction_section_title, - "code": self.optimizer_prompt_symbol_set.code_section_title, - "documentation": self.optimizer_prompt_symbol_set.documentation_section_title, - } + self.default_prompt_symbols = self.optimizer_prompt_symbol_set.default_prompt_symbols self.prompt_symbols = copy.deepcopy(self.default_prompt_symbols) self.initialize_prompt() diff --git a/tests/unit_tests/test_optimizer_xml_parsing.py b/tests/unit_tests/test_optimizer_xml_parsing.py index edbb3758..e66658b0 100644 --- a/tests/unit_tests/test_optimizer_xml_parsing.py +++ b/tests/unit_tests/test_optimizer_xml_parsing.py @@ -30,6 +30,7 @@ - No reasoning/variable tags scenarios """ + class TestXMLParsing(unittest.TestCase): def test_basic_parsing(self): @@ -104,7 +105,7 @@ def test_nested_name_tags(self): expected = { 'reasoning': 'Reasoning here', 'variables': { - 'outer_name': 'some_value' + 'inner_name\n outer_name': 'some_value' } } self.assertEqual(result, expected) From d6255d46314a714751bc6ee0fda0049f4fadc35b Mon Sep 17 00:00:00 2001 From: windweller Date: Thu, 10 Jul 2025 23:43:31 -0400 Subject: [PATCH 106/314] fix error --- opto/optimizers/optoprime_v2.py | 1 + 1 file changed, 1 insertion(+) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index d0094779..afdd2cd3 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -626,6 +626,7 @@ def problem_instance(self, summary, mask=None): constraint_tag=self.optimizer_prompt_symbol_set.constraint_tag) if "#Others" not in mask else "" ), feedback=summary.user_feedback if "#Feedback" not in mask else "", + optimizer_prompt_symbol_set=self.optimizer_prompt_symbol_set ) def _step( From 5b45f0400523478b72ae46ab916f6e942bc7235f Mon Sep 17 00:00:00 2001 From: windweller Date: Fri, 11 Jul 2025 00:28:32 -0400 Subject: [PATCH 107/314] add fix to examples --- opto/optimizers/optoprime_v2.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index afdd2cd3..843249af 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -567,7 +567,8 @@ def construct_prompt(self, summary, mask=None, *args, **kwargs): # Add examples if len(self.memory) > 0: - prefix = user_prompt.split(self.final_prompt)[0] + formatted_final = self.final_prompt.format(names=var_names) + prefix = user_prompt.split(formatted_final)[0] examples = [] for variables, feedback in self.memory: examples.append( @@ -583,7 +584,7 @@ def construct_prompt(self, summary, mask=None, *args, **kwargs): user_prompt = ( prefix + f"\nBelow are some variables and their feedbacks you received in the past.\n\n{examples}\n\n" - + self.final_prompt + + formatted_final ) self.memory.add((summary.variables, summary.user_feedback)) From d7164ba7daeaaf60420ae3d06a64b49eb114797f Mon Sep 17 00:00:00 2001 From: windweller Date: Fri, 11 Jul 2025 17:03:04 -0400 Subject: [PATCH 108/314] fix the `_step()` where the suggestion is now nested --- opto/optimizers/optoprime_v2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index 843249af..62a1c0e0 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -157,7 +157,6 @@ class OptimizerPromptSymbolSet: reasoning_tag = "reasoning" improved_variable_tag = "variable" name_tag = "name" - value_tag = "value" # custom output format (this will give the highest degree of freedom) # once it's set, it will override the default output format @@ -651,7 +650,8 @@ def _step( return {} suggestion = self.extract_llm_suggestion(response) - update_dict = self.construct_update_dict(suggestion) + update_dict = self.construct_update_dict(suggestion['variables']) + # suggestion has two keys: reasoning, and variables if self.log is not None: self.log.append( From 3f4f1ca6894573685ca698723304799362518be0 Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 14 Jul 2025 17:52:49 +0000 Subject: [PATCH 109/314] Update copy test --- tests/unit_tests/test_copy.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/unit_tests/test_copy.py b/tests/unit_tests/test_copy.py index 3f361fef..fa772183 100644 --- a/tests/unit_tests/test_copy.py +++ b/tests/unit_tests/test_copy.py @@ -9,7 +9,14 @@ def test_deepcopy_plain_node(): x = trace.node("x") # should not raise - copy.deepcopy(x) + y = copy.deepcopy(x) + + assert y.name == x.py_name + '_copy:0' + + z = copy.deepcopy(y) + + assert z.name == y.py_name + '_copy:0' + def test_deepcopy_fun_parameter(): From 491cfd647d2b8fcaf1a5f94eac7104cbd82e1110 Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 14 Jul 2025 17:53:32 +0000 Subject: [PATCH 110/314] Add a runnable search algorithm implementation. --- opto/trainer/algorithms/search_algorithms.py | 890 +++++++++++++++++++ 1 file changed, 890 insertions(+) create mode 100644 opto/trainer/algorithms/search_algorithms.py diff --git a/opto/trainer/algorithms/search_algorithms.py b/opto/trainer/algorithms/search_algorithms.py new file mode 100644 index 00000000..4c3a08af --- /dev/null +++ b/opto/trainer/algorithms/search_algorithms.py @@ -0,0 +1,890 @@ +import numpy as np +import copy +import heapq +from dataclasses import dataclass +from typing import Union, List, Tuple, Dict, Any, Optional +from opto import trace +from opto.trace.nodes import ParameterNode +from opto.trainer.utils import async_run, batch_run +from opto.optimizers.utils import print_color +from opto.trainer.algorithms.basic_algorithms import Minibatch, AlgorithmBase, batchify, standard_forward +from opto.trainer.evaluators import evaluate +from opto.trainer.loader import DataLoader + + +# TODO save and load SearchAlgorithm +# TODO async version +# TODO create SYNC and ASYNC versions of the base class; add an attribute to the class to indicate +# TODO a better data structure to store samples + +# update_dict + +# Some helper function to convert between trace.Module and update_dict + + +def standard_forward(agent, x, guide, info, min_score=0): + """ Forward and compute feedback. + + Args: + agent: trace.Module + x: input + guide: (question, student_answer, info) -> score, feedback + info: additional information for the guide + min_score: minimum score when exception happens + + Returns: + target: output of the agent + score: score from the guide + feedback: feedback from the guide + """ + try: + target = agent(x) + score, feedback = guide(x, target.data, info) + except trace.ExecutionError as e: + target = e.exception_node + score, feedback = min_score, target.create_feedback('full') + return target, score, feedback +def is_node_copy(a, b): + # check if a is a copy of b or b is a copy of a + # For int:0, its deepcopied version is int0_copy:x + """ Check if a is a copy of b or b is a copy of a or if they are the same node.""" + if a.name == b.name: + return True + if '_copy' in a.name and (a.name.split(':')[0].replace('_copy', '') == b.py_name): + return True + if '_copy' in b.name and (b.name.split(':')[0].replace('_copy', '') == a.py_name): + return True + return False + +def is_module_copy(a, b): + """ Check if a and b (trace.Modules) are copies of each other. """ + parameters_a = a.parameters() + parameters_b = b.parameters() + # Check if all parameters of a are copies of b or vice versa + for p_a in parameters_a: + if not any(is_node_copy(p_a, p_b) for p_b in parameters_b): + return False + for p_b in parameters_b: + if not any(is_node_copy(p_b, p_a) for p_a in parameters_a): + return False + return True + +def remap_update_dict(base_module, update_dict): + """ Remap the update dict to the agent's parameters. update_dict might have keys which are copies of the base_module's parameters or visa versa. + This function remaps the keys in update_dict to the original parameters of the base_module. + + The return dict is empty if no keys in update_dict matched any parameters of the base_module. This condition can be used to check if the update_dict contains non-trivial updates. + """ + parameters = base_module.parameters() # get the parameters of the base agent + remapped_update_dict = {} + for k, v in update_dict.items(): + for p in parameters: + # Check if k is a copy of p or p is a copy of k + if is_node_copy(k, p): + k = p # remap k to the original parameter + remapped_update_dict[k] = v # set the value in the remapped update dict + break # stop checking once we've found a match + # remapped_update_dict is empty if no keys in update_dict matched any parameters of the base_module + return remapped_update_dict + +def set_module_parameters(agent, update_dict): + """ Set the parameters of the agent based on the update_dict. + The update_dict is a dictionary of ParameterNode: value pairs. + The agent's parameters will be updated with the values from the update_dict. + """ + remap_update_dict = remap_update_dict(agent, update_dict) # remap the update dict to the agent's parameters + for k, v in remap_update_dict.items(): + k._data = v # set the parameter's data to the value in the update_dict + +def create_module_from_update_dict(agent, update_dict): + """ Create a new agent from the update_dict. + The update_dict is a dictionary of ParameterNode: value pairs. + A new agent will be created with the parameters set to the values from the update_dict. + """ + new_agent = copy.deepcopy(agent) #.copy() # create a copy of the agent + set_module_parameters(new_agent, update_dict) # set the parameters of the new agent + return new_agent # return the new agent + + +# a1, a2, a3, a4 +# x1, x2, x3, x4 +# a11 (x1, x2) +# a12 (x3, x4) +# a21 (x1, x2) +# a22 (x3, x4) + +# N agents, M inputs +# N x M + +# A list (size len(agents)) with list of samples (size batchsize) for each agent, +# where each sample is a dict containing: +# - 'module': the trace.Module (proposal) +# - 'x': the input data +# - 'info': additional information about the input +# - 'target': the target output (if applicable) +# - 'score': the score of the proposal +# - 'feedback': the feedback from the guide + +#TODO naming +@dataclass +class Rollout: + """ A rollout is a single sample from the environment. It contains the module, input, info, target, score, and feedback. + This is used to store the results of the agent's evaluation on a single input. + """ + module: trace.Module # the trace.Module (proposal) + x: Any # the input data + info: Any # additional information about the input + target: trace.Node # the target output (if applicable) + score: float # the score of the proposal + feedback: Any # the feedback from the guide + + def to_dict(self): + """ Convert the rollout to a dictionary representation. """ + return { + "module": self.module, + "x": self.x, + "info": self.info, + "target": self.target.data, + "score": self.score, + "feedback": self.feedback, + } + +class Subgraph: + """ A subgraph is a collection of rollouts generated by the same agent (trace.Module) on different inputs. + """ + module: trace.Module # the trace.Module (proposal) that generated the rollouts + rollouts: List[Rollout] # a list of Rollout objects generated by the module on different inputs + def __init__(self, rollouts): + """ Initialize a subgraph with the given rollouts. """ + # Check that all rollouts have the same module + if not all(rollouts[0].module == r.module for r in rollouts): + raise ValueError("All rollouts must have the same module.") + self.module = rollouts[0].module # the module is the same for all rollouts + self.rollouts = rollouts + + def get_scores(self): + """ Get the scores of the rollouts in the subgraph. """ + return [r.score for r in self.rollouts] + + def __len__(self): + """ Get the number of rollouts in the subgraph. """ + return len(self.rollouts) + + def __iter__(self): + """ Iterate over the rollouts in the subgraph. """ + return iter(self.rollouts) + + def extend(self, other): + """ Extend the subgraph with another subgraph. """ + if not isinstance(other, Subgraph): + raise ValueError("Can only extend with another Subgraph.") + if self.module != other.module: + raise ValueError("Cannot extend with a subgraph with a different module.") + self.rollouts.extend(other.rollouts) + + def to_list(self): + """ Convert the subgraph to a list of rollouts. """ + return [r.to_dict() for r in self.rollouts] + + + + + +# # TODO general broadcast decorator +# def broadcast_forward(num_threads=1, description=None, sub_batch_size=None): +# """ A decorator to broadcast the agents, xs, infos, and guides. + +# forward should be a function that takes the arguments in the following order: +# agent: trace.Module, the agent to evaluate +# x: input, the input to the agent +# info: additional information for each input +# guide: a single guide or a list of guides that provide feedback on the outputs +# min_score: float, minimum score when exception happens +# **kwargs: additional keyword arguments to pass to the forward function +# Returns: +# A wrapper function that takes agents, xs, infos, guides, min_score, and additional keyword arguments. +# The wrapper function will broadcast the agents, inputs, infos, and guides. + +# agents is expected to be a list of trace.Modules representing the agents. +# xs and infos are expected to be lists of the same length of batch size. +# guide can be a single guide or a list of guides of the same length as the number of agents. + +# The return of the wrapper function is a list of Subgraph objects, where each Subgraph contains a list of Rollout objects. +# """ + +# def decorator(forward): +# """ A decorator to broadcast the agents, inputs, infos, and guides. """ +# def wrapper(agents, xs, infos, guides, min_score=0., **kwargs): +# """ A wrapper to broadcast the agents, inputs, infos, and guides to match the batch size. """ + +# # Example: +# # agents : a1, a2 +# # inputs: x1, x2, x3 +# # infos: i1, i2, i3 +# # sub_batch_size: 2 + +# # The forward is called in this order: +# # (a1, x1, i1, guide1), +# # (a1, x2, i2, guide1), +# # (deepcopy(a1), x3, i3, guide1) +# # (a2, x1, i1, guide2), +# # (a2, x2, i2, guide2), +# # (deepcopy(a2), x3, i3, guide2) + + +# batch_size = len(xs) +# n_agents = len(agents) +# assert len(infos) == batch_size, "Length of infos must match length of xs." + + +# # broadcasted_agents = [proposal for proposal in agents for _ in range(batch_size)] # [a1, a1, a2, a2, ...] + +# # Broadcast the agents to match the batch size +# # [a1, a1, a1, a1, a1, ..., a2, a2, a2, ...] if sub_batch_size is not specified +# # [a1, a1, a1_copy_1, a1_copy_1, a1_copy_2, ..., a2, a2, a2_copy_1, ...] if sub_batch_size of 2 is specified +# sub_batch_size = sub_batch_size or batch_size # if sub_batch_size is not provided, use the batch size +# broadcasted_agents = [] +# for agent in agents: +# for i in range(batch_size): +# if i % sub_batch_size == 0 and i > 0: +# agent = copy.deepcopy(agent) # create a copy of the agent for the next sub-batch +# broadcasted_agents.append(agent) + +# # broadcast the inputs and infos to match the number of agents +# # [x1, x2, x3, ..., x1, x2, x3, ...] +# broadcasted_xs = [x for _ in range(n_agents) for x in xs] +# broadcasted_infos = [info for _ in range(n_agents) for info in infos] + +# # Broadcast the guides to match the batch size +# if isinstance(guides, list): +# assert len(guides) == n_agents, "If guides is a list, its length must match the number of agents." +# # If multiple guides are provided, broadcast each guide to match the batch size +# broadcasted_guides = [guide for guide in guides for _ in range(batch_size)] +# else: # If a single guide is provided, broadcast it to match the batch size +# broadcasted_guides = [guides for _ in range(n_agents * batch_size)] + +# description = description or f"Evaluating {n_agents} agents on {batch_size} inputs" + +# # Forward the agent on the inputs and compute the feedback using the guide +# forward = batch_run(max_workers=num_threads, description=description)(forward) +# _outputs = forward(broadcasted_agents, +# broadcasted_xs, +# broadcasted_infos, +# broadcasted_guides, +# min_score=min_score, +# **kwargs) # guide will be broadcasted inside as well +# # return list of (target, score, feedback) + + +# return outputs + +# return wrapper +# return decorator + + +class SearchAlgorithm(AlgorithmBase): + """ This implements a generic template for search algorithm. """ + + def __init__(self, + agent, + optimizer, + num_threads: int = None, # maximum number of threads to use for parallel execution + logger=None, + *args, + **kwargs, + ): + super().__init__(agent, num_threads=num_threads, logger=logger, *args, **kwargs) + self.optimizer = optimizer + self.n_iters = 0 # number of iterations + + def train(self, + guide, # guide to provide feedback + train_dataset, # dataset of (x, info) pairs to train the agent + *, + # validation + validate_dataset = None, # same format as train_dataset; if None use the current batch. + validate_guide = None, # to provide scores for the validation set + # training loop + batch_size = 1, # batch size for updating the agent + sub_batch_size = None, # sub-batch size for broadcasting the agents + score_range = None, # minimum score to update the agent + num_epochs = 1, # number of training epochs + num_threads = None, # maximum number of threads to use + verbose = False, # whether to print the output of the agent + # evaluation + test_dataset = None, # dataset of (x, info) pairs to evaluate the agent + test_guide = None, # guide to provide scores for the test set + # test_frequency: Union[int, None] = 1, # frequency of evaluation + eval_frequency: Union[int, None] = 1, # frequency of evaluation + num_eval_samples: int = 1, # number of samples to use to evaluate each input + # logging + log_frequency = None, # frequency of logging + save_frequency: Union[int, None] = None, # frequency of saving the agent + save_path: str = "checkpoints/agent.pkl", # path to save the agent + **kwargs + ): + + ## Setup + # TODO legacy notation + test_frequency = eval_frequency # use eval_frequency as test_frequency + + log_frequency = log_frequency or test_frequency # frequency of logging (default to test_frequency) + self.num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads + test_dataset = test_dataset or train_dataset # default to train_dataset if test_dataset is not provided + test_guide = test_guide or guide + self.num_eval_samples = num_eval_samples # number of samples to use to evaluate each input + self.score_range = score_range or (0., 1.) + # Underscore attributes are temporary attributes for the algorithm (which will not be saved) + # They would not affect the agent's state or the training process. + self._loader = DataLoader(train_dataset, batch_size=batch_size) # default data loader for training + self.sub_batch_size = sub_batch_size # sub-batch size for broadcasting the agents + self._guide = guide + self._validate_dataset = validate_dataset + self._validate_guide = validate_guide or guide + + # Evaluate the agent before learning + # NOTE set test_frequency < 0 to skip first evaluation + if (test_frequency is not None) and test_frequency > 0: + info_test = self.test(test_dataset, test_guide) + self.log(info_test) + + # Save the agent before learning if save_frequency > 0 + if (save_frequency is not None) and save_frequency > 0: + self.save(save_path) + + samples = None + self.n_epochs = 0 # number of epochs (full passes over the dataset) performed by the algorithm (This is incremented in sample) + self.n_samples = 0 # number of training samples processed by the algorithm (This is incremented in sample) + train_scores = [] # to store the scores of the agent during training + + while self.n_epochs < num_epochs : + + print(f"Epoch: {self.n_epochs}. Iteration: {self.n_iters}") + + # 1. Propose new parameters given the current state of the algorithm + # proposals: list of trace.Modules + update_dict, proposals, info_update = self.update(samples, verbose=verbose, **kwargs) + self.optimizer.update(update_dict) # update the agent with the proposed parameters + + # 2. Get feedback on the proposed parameters on the current batch + # samples: list of list of dict(module, x, info, target, score, feedback) + samples, info_sample = self.sample(proposals, verbose=verbose, **kwargs) + + # Evaluate the agent after update + if (test_frequency is not None) and (self.n_iters % test_frequency == 0): + info_test = self.test(test_dataset, test_guide) + self.log(info_test, prefix="Test: ") + + # Save the algorithm state + if (save_frequency is not None and save_frequency > 0) and self.n_iters % save_frequency == 0: + self.save(save_path) + + # Log information + train_scores.append(info_sample['mean_score']) # so that mean can be computed + if self.n_iters % log_frequency == 0: + self.logger.log('Average mean score', np.mean(train_scores), self.n_iters, color='blue') + self.log(info_update, prefix="Update: ") + self.log(info_sample, prefix="Sample: ") + self.n_samples += sum(len(s) for s in samples) # update the number of samples processed + self.logger.log('Number of samples', self.n_samples, self.n_iters, color='blue') + # Log parameters + for p in self.agent.parameters(): + self.logger.log(f"Parameter: {p.name}", p.data, self.n_iters, color='red') + + # Update counters + self.n_epochs = info_sample['n_epochs'] # update the number of epochs completed + self.n_iters += 1 + return + + # TODO + def evaluate(self, agent, guide, xs, infos, min_score=None, num_samples=1, num_threads=None, description=None): + """ Evaluate the agent on the given dataset. """ + num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads + test_scores = evaluate(agent, guide, xs, infos, min_score=min_score, num_threads=num_threads, + description=description, num_samples=self.num_eval_samples) + if all([s is not None for s in test_scores]): + return np.mean(test_scores) + + # TODO move it out? + def sample(self, agents, loader=None, guide=None, **kwargs): + """ Sample a batch of data based on the proposed parameters. All proposals are evaluated on the same batch of inputs. + + Args: + agents (list): A list of trace.Modules (proposed parameters) to evaluate. + **kwargs: Additional keyword arguments that may be used by the implementation. + Returns: + list of list of dict: + A list (size len(agents)) with list of samples (size batchsize) for each agent, + where each sample is a dict containing: + - 'module': the trace.Module (proposal) + - 'x': the input data + - 'info': additional information about the input + - 'target': the target output (if applicable) + - 'score': the score of the proposal + - 'feedback': the feedback from the guide + + NOTE: The return might not be ordered in the same way as the agents. + """ + assert all(isinstance(a, trace.Module) for a in agents), "All agents must be trace.Modules." + + loader = loader or self._loader # use the provided loader or the default one (train_dataset loader) + guide = guide or self._guide # use the provided guide or the default one (train_dataset guide) + + # Get a batch of inputs and infos from the loader + xs, infos = loader.sample() + + # XXX hack for now + self.xs, self.infos = xs, infos # store the inputs and infos for later use + + # Evaluate each agent on the sampled inputs + # + # agents : a1, a2 + # inputs: x1, x2, x3 + # infos: i1, i2, i3 + # sub_batch_size: 2 + # + # The forward is called in this order: + # (a1, x1, i1, guide1), + # (a1, x2, i2, guide1), + # (deepcopy(a1), x3, i3, guide1) + # (a2, x1, i1, guide2), + # (a2, x2, i2, guide2), + # (deepcopy(a2), x3, i3, guide2) + + num_threads = self.num_threads + min_score = self.score_range[0] + + batch_size = len(xs) + sub_batch_size = self.sub_batch_size or batch_size # if sub_batch_size is not provided, use the batch size + n_agents = len(agents) + + assert len(infos) == batch_size, "Length of infos must match length of xs." + + # Broadcast the agents to match the batch size + # [a1, a1, a1, a1, a1, ..., a2, a2, a2, ...] if sub_batch_size is not specified + # [a1, a1, a1_copy_1, a1_copy_1, a1_copy_2, ..., a2, a2, a2_copy_1, ...] if sub_batch_size of 2 is specified + broadcasted_agents = [] + for agent in agents: + for i in range(batch_size): + if i % sub_batch_size == 0 and i > 0: + agent = copy.deepcopy(agent) # create a copy of the agent for the next sub-batch + broadcasted_agents.append(agent) + + # Broadcast the inputs and infos to match the number of agents + # [x1, x2, x3, ..., x1, x2, x3, ...] + broadcasted_xs = [x for _ in range(n_agents) for x in xs] + broadcasted_infos = [info for _ in range(n_agents) for info in infos] + + # Broadcast the guides to match the batch size + + description = f"Forwarding {n_agents} agents on {batch_size} inputs" + + # Forward the agent on the inputs and compute the feedback using the guide + batched_forward = batch_run(max_workers=num_threads, description=description)(standard_forward) + outputs = batched_forward(agent=broadcasted_agents, + x=broadcasted_xs, + info=broadcasted_infos, + guide=guide, # guide will be broadcasted inside + min_score=min_score) + # return list of (target, score, feedback) + + # Collect results + results = [] # list of subgraphs (Subgraph objects) for each agent + for i in range(n_agents): + rollouts = [] # the compute result of each batch for a agent (trace.Module) + _agent = broadcasted_agents[i * batch_size ] # the first agent in the batch + for j in range(batch_size): + rollout = Rollout( + module=broadcasted_agents[i * batch_size + j], + x=broadcasted_xs[i * batch_size + j], + info=broadcasted_infos[i * batch_size + j], + target=outputs[i * batch_size + j][0], # target output + score=outputs[i * batch_size + j][1], # score of the proposal + feedback=outputs[i * batch_size + j][2], # feedback of the proposal + ) + if _agent != rollout.module: + results.append(Subgraph(rollouts)) # append the subgraph to the results + _agent = rollout.module # update the agent to the current one + rollouts = [] # reset rollouts for the new agent + rollouts.append(rollout) + + if rollouts: + results.append(Subgraph(rollouts)) # append the subgraph to the results + + # Log information about the sampling + log_info = { + 'mean_score': np.mean([ g.get_scores() for g in results]), + 'batch_size': batch_size, + 'sub_batch_size': sub_batch_size, + 'n_epochs': loader.n_epochs, + } + return results, log_info + + def log(self, info_log, prefix=""): + """ Log the information from the algorithm. """ + for key, value in info_log.items(): + try: + if value is not None: + self.logger.log(f"{prefix}{key}", value, self.n_iters) + except Exception as e: + print(e) + breakpoint() # if logging fails, we can debug here + + def test(self, test_dataset, guide): + min_score = self.score_range[0] + # Test the agent's performance + test_score = self.evaluate(self.agent, guide, test_dataset['inputs'], test_dataset['infos'], + min_score=min_score, num_threads=self.num_threads, + description=f"Evaluating agent (iteration {self.n_iters})") # and log + return {'test_score': test_score} + + def save(self, save_path): + self.save_agent(save_path, self.n_iters) + # TODO save full state of self + + # Helper methods for the algorithm + def get_minibatch(self, samples): + """ Get a minibatch of samples from the provided samples. """ + # Since all proposals share the same batch, we can return the first sample's x and info + # return [s.x for s in samples[0]], [s['info'] for s in samples[0]] + return self.xs, self.infos # XXX hack for now + + # Unimplemented methods that should be implemented by subclasses + def update(self, samples=None, verbose=False, **kwargs): + """ Update the agent based on the provided samples. + Args: + samples (list): A list of samples from the previous iteration. If None, the agent's parameters are returned without updating. + verbose (bool, optional): Whether to print verbose output. Defaults to False. + **kwargs: Additional keyword arguments that may be used by the implementation. + Returns: + update_dict (dict of Parameter: Any): A dictionary containing the updated parameters of the agent. + proposals (list of trace.Module): A list of proposed parameters (trace.Module) after the update. + info_log (dict of str: Any): A dictionary containing logging information about the update process. + + This method updates the agent's parameters based on samples of the training dataset and validation dataset (provided by self.get_validate_dataset). + In addition, it return new agents (proposals) that can be used for collecting data for the next iteration. + """ + raise NotImplementedError("The update method should be implemented by subclasses.") + # return update_dict, proposals, info_log + + +class ModuleCandidate: + + def __init__(self, + base_module: Optional[trace.Module], + update_dict: Optional[Dict[ParameterNode, Any]] = None, + ): + """ A candidate module with its base module and update dictionary. + Args: + base_module (trace.Module): The base module to use as a template for the candidate. + update_dict (dict): A dictionary of ParameterNode: value pairs to update the base module; the key can be a deep copy of the base module's parameters. + stats (dict): A dictionary of statistics about the candidate. + """ + assert isinstance(base_module, trace.Module), "base_module must be a trace.Module." + self.base_module = base_module + self.update_dict = update_dict if update_dict is not None else {} + self.rollouts = [] # list of dicts containing the rollout information + + def get_module(self): + """ Apply the update_dict to the base_module and return the updated module. This will not update the base_module itself.""" + return create_module_from_update_dict(self.base_module, self.update_dict) if self.update_dict else self.base_module + + def apply_update(self, base_module=None): + """ Apply update to the base_module in place. """ + set_module_parameters(base_module or self.base_module, self.update_dict) + + def __deepcopy__(self, memo): + """ Create a deep copy, except for the base_module which is not copied, it is the original module. """ + cls = self.__class__ + result = cls.__new__(cls) + memo[id(self)] = result + for k, v in self.__dict__.items(): + if k != 'base_module': + setattr(result, k, deepcopy(v, memo)) + else: + setattr(result, k, v) # base_module is not copied, it is the original module + return result + + def __equal__(self, other): + """ Check if two candidates are equal based on their base_module and update_dict. """ + if not isinstance(other, ModuleCandidate): + return False + if self.base_module != other.base_module: + return False + update_dict_self = remap_update_dict(self.base_module, self.update_dict) + update_dict_other = remap_update_dict(other.base_module, other.update_dict) + return update_dict_self == update_dict_other + + def add_rollouts(self, rollouts: List[Dict[str, Any]]): + """ Add rollouts to the candidate. """ + + # # Convert all ParameterNode to data in the rollouts + # _rollouts = [] + # for r in rollouts: + # _r = {} + # for k, v in r.items(): + # if isinstance(v, trace.ParameterNode): + # _r[k] = v.data + # else: + # _r[k] = v + + # _rollouts.append(_r) # convert all ParameterNode to data + self.rollouts.extend(rollouts) + # # XXX TODO hacky + # self.rollouts.rollouts.extend(_rollouts) # extend the rollouts with the + + def score(self): + """ Compute the score of the candidate based on the rollouts. """ + if not self.rollouts: + return None + scores = [r['score'] for r in self.rollouts] + return np.mean(scores) if scores else None + +class PrioritySearch(SearchAlgorithm): + + # def train(self, *args, + # num_candidates: int = 10, # number of candidates to propose + # default_score: Union[float, None] = None, # default score for the candidates + # validate_proposals: bool = True, # whether to validate the proposed parameters # TODO better naming + # **kwargs + # ): + def train(self, + guide, # guide to provide feedback + train_dataset, # dataset of (x, info) pairs to train the agent + *, + # validation + validate_dataset = None, # same format as train_dataset; if None use the current batch. + validate_guide = None, # to provide scores for the validation set + # training loop + batch_size = 1, # batch size for updating the agent + sub_batch_size = None, # sub-batch size for broadcasting the agents + score_range = None, # minimum score to update the agent + num_epochs = 1, # number of training epochs + num_threads = None, # maximum number of threads to use + verbose = False, # whether to print the output of the agent + # evaluation + test_dataset = None, # dataset of (x, info) pairs to evaluate the agent + test_frequency: Union[int, None] = 1, # frequency of evaluation + num_eval_samples: int = 1, # number of samples to use to evaluate each input + # logging + log_frequency = None, # frequency of logging + save_frequency: Union[int, None] = None, # frequency of saving the agent + save_path: str = "checkpoints/agent.pkl", # path to save the agent + # Priority Search specific parameters + num_candidates: int = 10, # number of candidates to propose + default_score: Union[float, None] = None, # default score for the candidates + validate_proposals: bool = True, # whether to validate the proposed parameters + # Additional keyword arguments + **kwargs + ): + + + # Create agents and optimizers for search + self.num_candidates = num_candidates # number of candidates to propose + self.score_range = score_range or (0., 1.) # XXX hacky now + self.default_score = default_score if default_score is not None else self.score_range[0] # default score for the candidates + self.validate_proposals = validate_proposals # whether to validate the proposed parameters + self._queue = [(self.default_score, ModuleCandidate(self.agent))] # priority queue of ModuleCandidates, initialized with the base agent + + super().train(guide, train_dataset, + validate_dataset=validate_dataset, + validate_guide=validate_guide, + batch_size=batch_size, + sub_batch_size=sub_batch_size, + score_range=score_range, + num_epochs=num_epochs, + num_threads=num_threads, + verbose=verbose, + test_dataset=test_dataset, + test_frequency=test_frequency, + num_eval_samples=num_eval_samples, + log_frequency=log_frequency, + save_frequency=save_frequency, + save_path=save_path, + **kwargs) + + def update(self, samples=None, verbose=False, **kwargs): + + if samples is not None: + # 1. Propose new parameters based on running LLM optimizers on the collected samples + candidates = self.propose(samples, verbose=verbose, **kwargs) # List of ModuleCandidates + # 2. Validate the proposed parameters + validate_results = self.validate(candidates, samples, verbose=verbose, **kwargs) # this updates the priority queue + # 3. Update the priority queue with the validation results + self.update_memory(validate_results, verbose=verbose, **kwargs) # samples are provided here in case candidates do not capture full information + # 4. Explore and exploit the priority queue + best_candidate = self.exploit(verbose=verbose, **kwargs) # get the best candidate (ModuleCandidate) from the priority queue + exploration_candidates = self.explore(verbose=verbose, **kwargs) # List of ModuleCandidates + + + # TBD Log information about the update + info_log = { + 'best_candidate_score': best_candidate.score(), + 'num_exploration_candidates': len(exploration_candidates), + } + return best_candidate.update_dict, [c.get_module() for c in exploration_candidates], info_log + + def propose(self, samples=None, verbose=False, n_proposals=1, **kwargs): + """ Analyzing samples and propose new parameters using self.optimizer. An independent optimizer is used for the minibatch generated by one agent and generates n_proposals proposals. + + Args: + samples (list): A list of samples from the previous iteration. If None, the agent's parameters are returned without updating. + n_proposals (int): Number of proposals to generate per optimizer. Defaults to 1. + verbose (bool, optional): Whether to print verbose output. Defaults to False. + **kwargs: Additional keyword arguments that may be used by the implementation. + + Returns: + candidates (list of ModuleCandidate): A list of proposed candidates for the next iteration. + """ + if samples is None: + parameters = self.optimizer.parameters # use the current parameters of the optimizer + update_dict = {p: p.data for p in parameters} # return the current parameters as the update dict + # TODO what to do here? should we return n_proposals variations? + return [update_dict] # return the update dict as a list + + def _step(n, verbose=False, num_threads=None, **kwargs): + """ Standard optimizer step for a single agent. """ + # optimizer = self._optimizers[n] # get the optimizer for the n-th agent + # TODO this seems slow + optimizer = copy.deepcopy(self.optimizer) # create a copy of the optimizer to avoid modifying the original one + + rollouts = samples[n] # Subgraph + + # Make sure all rollouts are based on the same module, so they can be viewed as a minibatch. + optimizer.parameters = rollouts.module.parameters() # set the optimizer's parameters to the proposal's parameters + + targets = [r.target for r in rollouts] + feedbacks = [r.feedback for r in rollouts] + # batchify the targets and feedbacks + target = batchify(*targets) + feedback = batchify(*feedbacks).data # str + # standard optimizer step + optimizer.zero_feedback() # reset the optimizer's feedback + optimizer.backward(target, feedback) # compute the gradients based on the targets and feedbacks + update_dict = optimizer.step(verbose=verbose, num_threads=num_threads, bypassing=True, **kwargs) + # the update_dict is linked to the copied parameters of the agent, we set it back to the agent's parameters + update_dict = remap_update_dict(self.agent, update_dict) # remap the update dict to the agent's parameters + return update_dict # return the proposed parameters + + n_agents = len(samples) # number of agents + args_list = [(n, verbose, self.num_threads) for n in range(n_agents)] + args_list = args_list * n_proposals # repeat args_list n_proposals times + kwargs_list = [kwargs] * n_agents * n_proposals # repeat kwargs for each agent + update_dicts = async_run([_step]*n_agents*n_proposals, # run the optimizer step for each agent in parallel + args_list=args_list, + kwargs_list=kwargs_list, + max_workers=self.num_threads, # use the number of threads specified in the class + description="Running optimizers on samples") + # update_dicts is a list of dicts of length n_agents * n_proposals + # Create ModuleCandidate objects for each proposed update_dict + candidates = [ModuleCandidate(self.agent, update_dict) for update_dict in update_dicts] + return candidates + + + def validate(self, candidates, samples=None, verbose=False, **kwargs): + """ Validate the proposed candidate parameters + Args: + candidates (list of dict): A list of ModuleCandidate objects representing the proposed parameters. + samples (list of dict, optional): A list of samples collected in the current iteration. Defaults to None. + verbose (bool, optional): Whether to print verbose output. Defaults to False. + **kwargs: Additional keyword arguments that may be used by the implementation. + Returns: + results (dict [ModuleCandidate, list of dict]): A dictionary where the keys are ModuleCandidate objects and the values are lists of rollouts (list of dicts) containing the module, x, info, target, score, feedback. + """ + + # Get the validation dataset from the samples. If no validation dataset is provided, use the current batch. + if self._validate_dataset is None: + # If no validation dataset is provided, use the current batch + xs, infos = self.get_minibatch(samples) + validate_dataset = {'inputs': xs, 'infos': infos} + else: + validate_dataset = self._validate_dataset + + + class Loader: # an trivial loader for the API + def __init__(self): + self.n_epochs = 0 + def sample(self): + return validate_dataset['inputs'], validate_dataset['infos'] + loader = Loader() # create a loader for the validation dataset + candidate_agents = [c.get_module() for c in candidates] # get the modules from the candidates + validate_samples, _ = self.sample(candidate_agents, loader=loader, guide=self._validate_guide, **kwargs) + # TODO log _ + + if self.validate_proposals: + if self._validate_dataset is None: + validate_samples += samples # if no validation dataset is provided, append the samples to the validate_samples + else: # validate the agents in the validate_dataset + # TODO need a flag? + exploration_agents = [rollouts.module for rollouts in samples] + exploration_samples = self.sample(exploration_agents, loader=loader, guide=self._validate_guide, **kwargs) + validate_samples += exploration_samples # append the exploration samples to the validate_samples + + + # Return a dict, key: ModuleCandidate, value: rollouts (list of dicts) + results = {} + for rollouts in validate_samples: + # rollouts is subgraph + agent = rollouts.module + index = candidate_agents.index(agent) + candidate = candidates[index] # get the candidate corresponding to the agent + # TODO delete 'module' from the rollouts dict? + if candidate in results: + # If the candidate already exists in results, we can append the rollouts to the existing list + results[candidate].extend(rollouts) + else: + # If the candidate does not exist in results, we create a new entry + results[candidate] = rollouts + return results + + + + def update_memory(self, validate_results, **kwargs): + + """ Update the priority queue with the validation results. + Args: + validate_results (dict): A dictionary where the keys are ModuleCandidate objects and the values are lists of rollouts (list of dicts) containing the module, x, info, target, score, feedback. + **kwargs: Additional keyword arguments that may be used by the implementation. + """ + for candidate, rollouts in validate_results.items(): + candidate.add_rollouts(rollouts.to_list()) # add the rollouts to the candidate + score = self.compute_score(candidate) # compute the score for the candidate + heapq.heappush(self._queue, (-score, candidate)) # add the candidate to the priority queue + + def explore(self, **kwargs): + """ Explore the parameter space and propose new candidates. + Args: + **kwargs: Additional keyword arguments that may be used by the implementation. + Returns: + update_dict (dict of Parameter: Any): A dictionary containing the updated parameters of the agent. + proposal_update_dicts (list of dict): A list of proposed parameter updates (dict) for the next iteration. + """ + # pop top self.num_candidates candidates from the priority queue + top_candidates = [] + while len(top_candidates) < self.num_candidates and self._queue: + score, candidate = heapq.heappop(self._queue) + top_candidates.append(candidate) # add the candidate to the top candidates + return top_candidates + + + def exploit(self, **kwargs): + """ Exploit the best candidate from the priority queue. This method should not change the priority queue. + Args: + **kwargs: Additional keyword arguments that may be used by the implementation. + Returns: + ModuleCandidate: The best candidate from the priority queue. + """ + # Right now, we just return the best candidate from the priority queue + # This function can be overridden by subclasses to implement a different exploitation strategy + if not self._queue: + raise ValueError("The priority queue is empty. Cannot exploit.") + best = min(self._queue) # (score, candidate) + return best[1] + + def compute_score(self, candidate): + # By default, we compute the mean score of the rollouts + # NOTE This function can be overridden by subclasses to compute a different score + scores = [r['score'] for r in candidate.rollouts] + default_score = self.default_score if self.default_score is not None else self.score_range[1] # default score for the candidates + + return np.mean(scores) if scores else self.default_score From 56947a30a0961d79cdc23294c5be818a0890fdfe Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 14 Jul 2025 17:54:53 +0000 Subject: [PATCH 111/314] Add the example script. --- examples/gsm8k_search_algo.py | 95 +++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 examples/gsm8k_search_algo.py diff --git a/examples/gsm8k_search_algo.py b/examples/gsm8k_search_algo.py new file mode 100644 index 00000000..832ec8e1 --- /dev/null +++ b/examples/gsm8k_search_algo.py @@ -0,0 +1,95 @@ +import datasets +import numpy as np +from opto import trace +from opto.utils.llm import LLM, LiteLLM +from opto.optimizers import OptoPrime +from opto.trainer.algorithms.search_algorithms import PrioritySearch as SearchAlgorithm +from opto.trainer.loggers import TensorboardLogger +from opto.trainer.guide import VerbalJudgeGuide +from typing import Any + + +@trace.model +class Learner: + """ A basic LLM agent. """ + + def __init__(self, system_prompt: str = "You're a helpful agent", + user_prompt_template: str = "Query: {message}", + llm: LLM = None): + self.system_prompt = trace.node(system_prompt, trainable=True) + self.user_prompt_template = trace.node(user_prompt_template) + self.llm = llm or LLM() + + @trace.bundle() + def model(self, system_prompt: str, user_prompt_template: str, message: str) -> str: + """Call the LLM model. + + Args: + system_prompt: the system prompt to the agent. By tuning this prompt, we can control the behavior of the agent. For example, it can be used to provide instructions to the agent (such as how to reason about the problem, how to answer the question), or provide in-context examples of how to solve the problem. + user_prompt_template: the user prompt template to the agent. It is used as formatting the input to the agent as user_prompt_template.format(message=message). + message: the input to the agent. It can be a query, a task, a code, etc. + Returns: + The response from the agent. + """ + + if '{message}' not in user_prompt_template: + raise ValueError("user_prompt_template must contain '{message}'") + + response = self.llm( + messages=[{"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt_template.format(message=message)}] + ) + return response.choices[0].message.content + + def forward(self, message: Any) -> Any: + """ Forward pass of the agent. """ + return self.model(self.system_prompt, self.user_prompt_template, message) + + +Guide = VerbalJudgeGuide +Logger = TensorboardLogger + + +def main(): + # set seed + seed = 42 + num_epochs = 1 + batch_size = 1 + eval_frequency = -1 + num_threads = 3 + verbose = True + teacher_model = None # use default model + student_model = None # use default model + optimizer_model = None # use default model + + np.random.seed(seed) + + # In this example, we use the GSM8K dataset, which is a dataset of math word problems. + # We will look the training error of the agent on a small portion of this dataset. + train_dataset = datasets.load_dataset('openai/gsm8k', 'main')['train'][:10] + train_dataset = dict(inputs=train_dataset['question'], infos=train_dataset['answer']) + test_dataset = train_dataset + + agent = Learner(llm=LLM(student_model)) + guide = Guide(llm=LLM(teacher_model)) + optimizer = OptoPrime(agent.parameters(), llm=LLM(optimizer_model)) + logger = Logger(verbose=verbose) + # set use_json_object_format=False if LLM does not support JSON object format + + alg = SearchAlgorithm( + agent=agent, + optimizer=optimizer, + logger=logger) + + alg.train(guide, + train_dataset, + num_epochs=num_epochs, + batch_size=batch_size, + eval_frequency=eval_frequency, + test_dataset=test_dataset, + num_threads=num_threads, + verbose='output' if verbose else False) + + +if __name__ == "__main__": + main() From d96ddea717d6982b37b5d16057102401ca6bbe76 Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 14 Jul 2025 18:00:02 +0000 Subject: [PATCH 112/314] Fix a bug caused by the recent update to evaluate of Minibatch. --- opto/trainer/algorithms/basic_algorithms.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/opto/trainer/algorithms/basic_algorithms.py b/opto/trainer/algorithms/basic_algorithms.py index 8ec0eb4f..194bb1c9 100644 --- a/opto/trainer/algorithms/basic_algorithms.py +++ b/opto/trainer/algorithms/basic_algorithms.py @@ -84,6 +84,7 @@ def train(self, if eval_frequency > 0: test_score = self.evaluate(self.agent, guide, test_dataset['inputs'], test_dataset['infos'], min_score=min_score, num_threads=num_threads, + num_samples=self.num_eval_samples, description=f"Evaluating agent (iteration {self.n_iters})") # and log self.logger.log('Average test score', test_score, self.n_iters, color='green') @@ -123,6 +124,7 @@ def train(self, if test_dataset is not None and self.n_iters % eval_frequency == 0: test_score = self.evaluate(self.agent, guide, test_dataset['inputs'], test_dataset['infos'], min_score=min_score, num_threads=num_threads, + num_samples=self.num_eval_samples, description=f"Evaluating agent (iteration {self.n_iters})") # and log self.logger.log('Average test score', test_score, self.n_iters, color='green') @@ -146,10 +148,10 @@ def evaluate(self, agent, guide, xs, infos, min_score=None, num_samples=1, num_t """ Evaluate the agent on the given dataset. """ num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads test_scores = evaluate(agent, guide, xs, infos, min_score=min_score, num_threads=num_threads, - num_samples=num_samples, description=description, num_samples=self.num_eval_samples) + num_samples=num_samples, description=description) if all([s is not None for s in test_scores]): return np.mean(test_scores) - + def has_improvement(self, xs, guide, infos, current_score, current_outputs, backup_dict, threshold=0, num_threads=None, *args, **kwargs): # This function can be overridden by subclasses to implement their own improvement check. """ Check if the updated agent is improved compared to the current one. @@ -166,6 +168,7 @@ def has_improvement(self, xs, guide, infos, current_score, current_outputs, back num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads new_score = self.evaluate(self.agent, guide, xs, infos, num_threads=num_threads, description=f"Checking improvement (iteration {self.n_iters})", + num_samples=self.num_eval_samples, *args, **kwargs) # evaluate the updated agent if new_score is None or new_score <= current_score - threshold: print_color(f"Update rejected: Current score {current_score}, New score {new_score}", 'red') @@ -305,13 +308,13 @@ def validate(): # Generate different proposals step_kwargs = dict(bypassing=True, verbose='output' if verbose else False) # we don't print the inner full message step_kwargs.update(kwargs) # update with additional kwargs if provided - + # Use aysnc_run to run the optimizer_step in parallel - # NOTE optimizer_step is coupled via async_run + # NOTE optimizer_step is coupled via async_run update_dicts = async_run([super().optimizer_step]*self.num_proposals, kwargs_list=[step_kwargs] * self.num_proposals, max_workers=num_threads, - description=f"Generating {self.num_proposals} proposals") # async step + description=f"Generating {self.num_proposals} proposals") # async step # Validate the proposals candidates = [] backup_dict = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} # backup the current value From fde338929d0a9624f6347700e12f043ad25f97ef Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 14 Jul 2025 18:46:37 +0000 Subject: [PATCH 113/314] Refactor into Sampler --- opto/trace/sampler.py | 283 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 283 insertions(+) create mode 100644 opto/trace/sampler.py diff --git a/opto/trace/sampler.py b/opto/trace/sampler.py new file mode 100644 index 00000000..b0c1bfae --- /dev/null +++ b/opto/trace/sampler.py @@ -0,0 +1,283 @@ +import numpy as np +import copy +import heapq +from dataclasses import dataclass +from typing import Union, List, Tuple, Dict, Any, Optional +from opto import trace +from opto.trace.nodes import ParameterNode +from opto.trainer.utils import async_run, batch_run +from opto.optimizers.utils import print_color +from opto.trainer.algorithms.basic_algorithms import Minibatch, AlgorithmBase, batchify +from opto.trainer.evaluators import evaluate +from opto.trainer.loader import DataLoader + +@dataclass +class Rollout: + """ A rollout is a single sample from the environment. It contains the module, input, info, target, score, and feedback. + This is used to store the results of the agent's evaluation on a single input. + """ + module: trace.Module # the trace.Module (proposal) + x: Any # the input data + info: Any # additional information about the input + target: trace.Node # the target output (if applicable) + score: float # the score of the proposal + feedback: Any # the feedback from the guide + + def to_dict(self): + """ Convert the rollout to a dictionary representation. """ + return { + "module": self.module, + "x": self.x, + "info": self.info, + "target": self.target.data, + "score": self.score, + "feedback": self.feedback, + } + +class RolloutsGraph: + """ A rollouts graph is a collection of rollouts generated by the same agent (trace.Module) on different inputs. + """ + module: trace.Module # the trace.Module (proposal) that generated the rollouts + rollouts: List[Rollout] # a list of Rollout objects generated by the module on different inputs + def __init__(self, rollouts): + """ Initialize a rollouts graph with the given rollouts. """ + # Check that all rollouts have the same module + if not all(rollouts[0].module == r.module for r in rollouts): + raise ValueError("All rollouts must have the same module.") + self.module = rollouts[0].module # the module is the same for all rollouts + self.rollouts = rollouts + + def get_scores(self): + """ Get the scores of the rollouts in the subgraph. """ + return [r.score for r in self.rollouts] + + def __len__(self): + """ Get the number of rollouts in the subgraph. """ + return len(self.rollouts) + + def __iter__(self): + """ Iterate over the rollouts in the subgraph. """ + return iter(self.rollouts) + + def extend(self, other): + """ Extend the subgraph with another subgraph. """ + if not isinstance(other, RolloutsGraph): + raise ValueError("Can only extend with another RolloutsGraph.") + if self.module != other.module: + raise ValueError("Cannot extend with a subgraph with a different module.") + self.rollouts.extend(other.rollouts) + + def to_list(self): + """ Convert the subgraph to a list of rollouts. """ + return [r.to_dict() for r in self.rollouts] + + +@dataclass +class RolloutConfig: + module: trace.Module # the trace.Module (proposal) + xs: List[Any] # the input data + infos: List[Any] # additional information about the input + guide: Any # the guide to evaluate the proposals + + def __init__(self, + module: trace.Module, + xs: List[Any], + infos: List[Any], + guide: Any): + """ Initialize a rollout config with the given module, inputs, infos, and guide. """ + # check types + if not isinstance(module, trace.Module): + raise TypeError("module must be a trace.Module.") + if not isinstance(xs, list): + raise TypeError("xs must be a list.") + if not isinstance(infos, list): + raise TypeError("infos must be a list.") + if not isinstance(guide, trace.Module): + raise TypeError("guide must be a trace.Module.") + if len(xs) != len(infos): + raise ValueError("Length of xs must match length of infos.") + self.module = module + self.xs = xs + self.infos = infos + self.guide = guide + + +# TODO move it and refactor the trainer code +def standard_forward(agent, x, guide, info, min_score=0): + """ Forward and compute feedback. + + Args: + agent: trace.Module + x: input + guide: (question, student_answer, info) -> score, feedback + info: additional information for the guide + min_score: minimum score when exception happens + + Returns: + target: output of the agent + score: score from the guide + feedback: feedback from the guide + """ + try: + target = agent(x) + score, feedback = guide(x, target.data, info) + except trace.ExecutionError as e: + target = e.exception_node + score, feedback = min_score, target.create_feedback('full') + return target, score, feedback + + +def sample_rollouts(configs, num_threads=1, forward=None, min_score=None, description="Sampling rollouts.") -> List[RolloutsGraph]: + """ Sample a batch of data based on the proposed parameters. All proposals are evaluated on the same batch of inputs. + + Args: + configs (List[RolloutConfig]): A list of RolloutConfig objects, each containing\ + - module: the trace.Module (proposal) to evaluate + - xs: a list of input data to evaluate the proposal on + - infos: a list of additional information about the inputs + - guide: the guide to evaluate the proposals + num_threads (int): Number of threads to use for sampling. + forward (callable, optional): A custom forward function to use instead of the default one + (standard_forward). If None, the default forward function is used. + min_score (float, optional): Minimum score to return when an exception occurs. If None, it defaults to 0. + description (str): Description to display in the progress bar. + Returns: + List[RolloutsGraph]: A list of RolloutsGraph objects, one for each config + """ + if forward is None: + forward = standard_forward + + # Forward the agent on the inputs and compute the feedback using the guide + batched_forward = batch_run(max_workers=num_threads, description=description)(forward) + + agents = [ config.module for config in configs for _ in range(len(config.xs)) ] # repeat each agent for each input + xs = [ x for config in configs for x in config.xs ] # flatten + infos = [ info for config in configs for info in config.infos ] # flatten + guides = [ config.guide for config in configs for _ in range(len(config.xs)) ] # repeat each guide for each input + + outputs = batched_forward(agent=agents, + x=xs, + info=infos, + guide=guides, # guide will be broadcasted inside + min_score=min_score) + + # Collect the results into a list of RolloutsGraph objects + results = [] # list of subgraphs (RolloutsGraph objects) for each agent + _index = 0 # to track the indices processed + for i in range(len(configs)): + rollouts = [] + _agent = configs[i].module # the first agent in the batch + for j in range(len(configs[i].xs)): + assert _agent == agents[_index], "Agent mismatch in the rollouts." + rollout = Rollout( + module=agents[_index], + x=xs[_index], + info=infos[_index], + target=outputs[_index][0], # target output + score=outputs[_index][1], # score of the proposal + feedback=outputs[_index][2], # feedback of the proposal + ) + _index += 1 # increment the index + rollouts.append(rollout) + results.append(RolloutsGraph(rollouts)) # append the subgraph to the results + return results + + + +class Sampler: + + def __init__(self, loader, guide, num_threads=1, sub_batch_size=None, forward=None, score_range=(-np.inf, np.inf)): + """ Initialize the sampler with a data loader and a guide. + + Args: + loader (DataLoader): The data loader to sample from. + guide (AutoGuide): The guide to evaluate the proposals. + num_threads (int): Number of threads to use for sampling. + sub_batch_size (int, optional): Size of the sub-batch to use for sampling. If None, uses the batch size. + score_range (tuple): The range of scores to consider valid. + """ + self._loader = loader + self._guide = guide + self.num_threads = num_threads + self.sub_batch_size = sub_batch_size + self.score_range = score_range + if forward is None: + self.forward = standard_forward + + def sample(self, agents): + """ Sample a batch of data from the loader and evaluate the agents. + + Args: + agents (list): A list of trace.Modules (proposed parameters) to evaluate. + **kwargs: Additional keyword arguments that may be used by the implementation. + + Returns: + batch (dict): + A dictionary containing the sampled inputs and infos, where: + - 'inputs': a list of inputs sampled from the loader + - 'infos': a list of additional information for each input + + samples (list of RolloutsGraph): + A list of RolloutsGraph objects, each containing the rollouts generated by the agents on the sampled inputs. + Each RolloutsGraph contains: + - 'module': the trace.Module (proposal) + - 'rollouts': a list of Rollout objects containing: + - 'x': the input data + - 'info': additional information about the input + - 'target': the target output (if applicable) + - 'score': the score of the proposal + - 'feedback': the feedback from the guide + + NOTE: The return might not be ordered in the same way as the agents. + """ + + assert all(isinstance(a, trace.Module) for a in agents), "All agents must be trace.Modules." + + # Get a batch of inputs and infos from the loader + xs, infos = self._loader.sample() + batch = { + 'inputs': xs, + 'infos': infos + } + + # Evaluate each agent on the sampled inputs + # + # agents : a1, a2 + # inputs: x1, x2, x3 + # infos: i1, i2, i3 + # sub_batch_size: 2 + # + # The forward is called in this order: + # (a1, x1, i1, guide1), + # (a1, x2, i2, guide1), + # (deepcopy(a1), x3, i3, guide1) + # (a2, x1, i1, guide2), + # (a2, x2, i2, guide2), + # (deepcopy(a2), x3, i3, guide2) + + # Create rollout configs for each agent + batch_size = len(xs) + assert len(infos) == batch_size, "Length of infos must match length of xs." + configs = [] + for agent in agents: + _xs, _infos = [], [] + for i in range(batch_size): + if i % self.sub_batch_size == 0 and i > 0: + configs.append(RolloutConfig(module=agent, xs=_xs, infos=_infos, guide=self._guide)) + # reset + agent = copy.deepcopy(agent) # create a deep copy of the agent for the next sub-batch + _xs, _infos = [], [] + _xs.append(xs[i]) + _infos.append(infos[i]) + if _xs: # if there are inputs in the sub-batch + configs.append(RolloutConfig(module=agent, xs=_xs, infos=_infos, guide=self._guide)) + + # Sample rollouts using the configs + description = f"Sampling {len(agents)} agents on {batch_size} inputs" + samples = sample_rollouts(configs, + forward=self.forward, + num_threads=self.num_threads, + min_score=self.score_range[0], + description=description) + + return samples From 026ec174381ba606a22196731cccee34ef691b2e Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Mon, 14 Jul 2025 14:57:58 -0500 Subject: [PATCH 114/314] Made is_node_copy function transitive --- opto/trainer/algorithms/search_algorithms.py | 28 +++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/opto/trainer/algorithms/search_algorithms.py b/opto/trainer/algorithms/search_algorithms.py index 4c3a08af..ddadb6cb 100644 --- a/opto/trainer/algorithms/search_algorithms.py +++ b/opto/trainer/algorithms/search_algorithms.py @@ -44,17 +44,25 @@ def standard_forward(agent, x, guide, info, min_score=0): target = e.exception_node score, feedback = min_score, target.create_feedback('full') return target, score, feedback + +def get_original_name(node): + """Extract the original name from a node, removing all _copy suffixes.""" + py_name = node.py_name # This removes colons: "param:0" -> "param0" + + # Find the first occurrence of "_copy" and remove it and everything after + copy_index = py_name.find('_copy') + if copy_index != -1: + return py_name[:copy_index] + else: + return py_name + def is_node_copy(a, b): - # check if a is a copy of b or b is a copy of a - # For int:0, its deepcopied version is int0_copy:x - """ Check if a is a copy of b or b is a copy of a or if they are the same node.""" - if a.name == b.name: - return True - if '_copy' in a.name and (a.name.split(':')[0].replace('_copy', '') == b.py_name): - return True - if '_copy' in b.name and (b.name.split(':')[0].replace('_copy', '') == a.py_name): - return True - return False + """Check if two nodes are copies of each other by comparing their original names. + + This function has transitivity: if A is a copy of B and B is a copy of C, + then A is also considered a copy of C. + """ + return get_original_name(a) == get_original_name(b) def is_module_copy(a, b): """ Check if a and b (trace.Modules) are copies of each other. """ From 87fabb6c57c5f20763faf514391c8c8e5bed5ba1 Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 14 Jul 2025 22:05:21 +0000 Subject: [PATCH 115/314] Finish a runnable refactor code. --- opto/trainer/algorithms/search_algorithms.py | 413 ++++--------------- opto/{trace => trainer}/sampler.py | 24 +- 2 files changed, 100 insertions(+), 337 deletions(-) rename opto/{trace => trainer}/sampler.py (94%) diff --git a/opto/trainer/algorithms/search_algorithms.py b/opto/trainer/algorithms/search_algorithms.py index 4c3a08af..0f2641f1 100644 --- a/opto/trainer/algorithms/search_algorithms.py +++ b/opto/trainer/algorithms/search_algorithms.py @@ -7,10 +7,11 @@ from opto.trace.nodes import ParameterNode from opto.trainer.utils import async_run, batch_run from opto.optimizers.utils import print_color -from opto.trainer.algorithms.basic_algorithms import Minibatch, AlgorithmBase, batchify, standard_forward +from opto.trainer.algorithms.basic_algorithms import Minibatch, AlgorithmBase, batchify from opto.trainer.evaluators import evaluate from opto.trainer.loader import DataLoader +from opto.trainer.sampler import Sampler, RolloutsGraph # TODO save and load SearchAlgorithm # TODO async version @@ -22,6 +23,7 @@ # Some helper function to convert between trace.Module and update_dict +# TODO move it and refactor the trainer code def standard_forward(agent, x, guide, info, min_score=0): """ Forward and compute feedback. @@ -44,6 +46,7 @@ def standard_forward(agent, x, guide, info, min_score=0): target = e.exception_node score, feedback = min_score, target.create_feedback('full') return target, score, feedback + def is_node_copy(a, b): # check if a is a copy of b or b is a copy of a # For int:0, its deepcopied version is int0_copy:x @@ -106,196 +109,50 @@ def create_module_from_update_dict(agent, update_dict): return new_agent # return the new agent -# a1, a2, a3, a4 -# x1, x2, x3, x4 -# a11 (x1, x2) -# a12 (x3, x4) -# a21 (x1, x2) -# a22 (x3, x4) -# N agents, M inputs -# N x M +class Samples: -# A list (size len(agents)) with list of samples (size batchsize) for each agent, -# where each sample is a dict containing: -# - 'module': the trace.Module (proposal) -# - 'x': the input data -# - 'info': additional information about the input -# - 'target': the target output (if applicable) -# - 'score': the score of the proposal -# - 'feedback': the feedback from the guide + samples: List[RolloutsGraph] + dataset: Dict[str, List[Any]] # contains 'inputs' and 'infos' keys -#TODO naming -@dataclass -class Rollout: - """ A rollout is a single sample from the environment. It contains the module, input, info, target, score, and feedback. - This is used to store the results of the agent's evaluation on a single input. - """ - module: trace.Module # the trace.Module (proposal) - x: Any # the input data - info: Any # additional information about the input - target: trace.Node # the target output (if applicable) - score: float # the score of the proposal - feedback: Any # the feedback from the guide - - def to_dict(self): - """ Convert the rollout to a dictionary representation. """ - return { - "module": self.module, - "x": self.x, - "info": self.info, - "target": self.target.data, - "score": self.score, - "feedback": self.feedback, - } + def __init__(self, samples: List[RolloutsGraph], dataset: Dict[str, List[Any]]): + assert isinstance(samples, list), "samples must be a list of RolloutsGraph objects." + assert all(isinstance(s, RolloutsGraph) for s in samples), "All samples must be RolloutsGraph objects." + assert isinstance(dataset, dict), "dataset must be a dict." + assert 'inputs' in dataset and 'infos' in dataset, "dataset must contain 'inputs' and 'infos' keys." -class Subgraph: - """ A subgraph is a collection of rollouts generated by the same agent (trace.Module) on different inputs. - """ - module: trace.Module # the trace.Module (proposal) that generated the rollouts - rollouts: List[Rollout] # a list of Rollout objects generated by the module on different inputs - def __init__(self, rollouts): - """ Initialize a subgraph with the given rollouts. """ - # Check that all rollouts have the same module - if not all(rollouts[0].module == r.module for r in rollouts): - raise ValueError("All rollouts must have the same module.") - self.module = rollouts[0].module # the module is the same for all rollouts - self.rollouts = rollouts - - def get_scores(self): - """ Get the scores of the rollouts in the subgraph. """ - return [r.score for r in self.rollouts] + self.samples = samples + self.dataset = dataset # TODO this cannot be extracted from the samples in general - def __len__(self): - """ Get the number of rollouts in the subgraph. """ - return len(self.rollouts) + def add_samples(self, samples): + """ Add samples to the Samples object. """ + assert isinstance(samples, Samples), "samples must be an instance of Samples." + samples = samples.samples # extract the samples from the Samples object + assert isinstance(samples, list), "samples must be a list of RolloutsGraph objects." + assert all(isinstance(s, RolloutsGraph) for s in samples), "All samples must be RolloutsGraph objects." + + # TODO assert xs and infos are in self.minibatch + # add a function to extract unique inputs and infos from the samples + + self.samples.extend(samples) + + def get_batch(self): + return self.dataset #['inputs'], self.minibatch['infos'] def __iter__(self): - """ Iterate over the rollouts in the subgraph. """ - return iter(self.rollouts) - - def extend(self, other): - """ Extend the subgraph with another subgraph. """ - if not isinstance(other, Subgraph): - raise ValueError("Can only extend with another Subgraph.") - if self.module != other.module: - raise ValueError("Cannot extend with a subgraph with a different module.") - self.rollouts.extend(other.rollouts) + """ Iterate over the samples. """ + return iter(self.samples) - def to_list(self): - """ Convert the subgraph to a list of rollouts. """ - return [r.to_dict() for r in self.rollouts] - - - - - -# # TODO general broadcast decorator -# def broadcast_forward(num_threads=1, description=None, sub_batch_size=None): -# """ A decorator to broadcast the agents, xs, infos, and guides. - -# forward should be a function that takes the arguments in the following order: -# agent: trace.Module, the agent to evaluate -# x: input, the input to the agent -# info: additional information for each input -# guide: a single guide or a list of guides that provide feedback on the outputs -# min_score: float, minimum score when exception happens -# **kwargs: additional keyword arguments to pass to the forward function -# Returns: -# A wrapper function that takes agents, xs, infos, guides, min_score, and additional keyword arguments. -# The wrapper function will broadcast the agents, inputs, infos, and guides. - -# agents is expected to be a list of trace.Modules representing the agents. -# xs and infos are expected to be lists of the same length of batch size. -# guide can be a single guide or a list of guides of the same length as the number of agents. - -# The return of the wrapper function is a list of Subgraph objects, where each Subgraph contains a list of Rollout objects. -# """ - -# def decorator(forward): -# """ A decorator to broadcast the agents, inputs, infos, and guides. """ -# def wrapper(agents, xs, infos, guides, min_score=0., **kwargs): -# """ A wrapper to broadcast the agents, inputs, infos, and guides to match the batch size. """ - -# # Example: -# # agents : a1, a2 -# # inputs: x1, x2, x3 -# # infos: i1, i2, i3 -# # sub_batch_size: 2 - -# # The forward is called in this order: -# # (a1, x1, i1, guide1), -# # (a1, x2, i2, guide1), -# # (deepcopy(a1), x3, i3, guide1) -# # (a2, x1, i1, guide2), -# # (a2, x2, i2, guide2), -# # (deepcopy(a2), x3, i3, guide2) - - -# batch_size = len(xs) -# n_agents = len(agents) -# assert len(infos) == batch_size, "Length of infos must match length of xs." - - -# # broadcasted_agents = [proposal for proposal in agents for _ in range(batch_size)] # [a1, a1, a2, a2, ...] - -# # Broadcast the agents to match the batch size -# # [a1, a1, a1, a1, a1, ..., a2, a2, a2, ...] if sub_batch_size is not specified -# # [a1, a1, a1_copy_1, a1_copy_1, a1_copy_2, ..., a2, a2, a2_copy_1, ...] if sub_batch_size of 2 is specified -# sub_batch_size = sub_batch_size or batch_size # if sub_batch_size is not provided, use the batch size -# broadcasted_agents = [] -# for agent in agents: -# for i in range(batch_size): -# if i % sub_batch_size == 0 and i > 0: -# agent = copy.deepcopy(agent) # create a copy of the agent for the next sub-batch -# broadcasted_agents.append(agent) - -# # broadcast the inputs and infos to match the number of agents -# # [x1, x2, x3, ..., x1, x2, x3, ...] -# broadcasted_xs = [x for _ in range(n_agents) for x in xs] -# broadcasted_infos = [info for _ in range(n_agents) for info in infos] - -# # Broadcast the guides to match the batch size -# if isinstance(guides, list): -# assert len(guides) == n_agents, "If guides is a list, its length must match the number of agents." -# # If multiple guides are provided, broadcast each guide to match the batch size -# broadcasted_guides = [guide for guide in guides for _ in range(batch_size)] -# else: # If a single guide is provided, broadcast it to match the batch size -# broadcasted_guides = [guides for _ in range(n_agents * batch_size)] - -# description = description or f"Evaluating {n_agents} agents on {batch_size} inputs" - -# # Forward the agent on the inputs and compute the feedback using the guide -# forward = batch_run(max_workers=num_threads, description=description)(forward) -# _outputs = forward(broadcasted_agents, -# broadcasted_xs, -# broadcasted_infos, -# broadcasted_guides, -# min_score=min_score, -# **kwargs) # guide will be broadcasted inside as well -# # return list of (target, score, feedback) - - -# return outputs + def __len__(self): + return len(self.samples) -# return wrapper -# return decorator -class SearchAlgorithm(AlgorithmBase): +#TODO naming +class SearchAlgorithm(Minibatch): + # This only uses __init__ and evaluate of Minibatch class. """ This implements a generic template for search algorithm. """ - def __init__(self, - agent, - optimizer, - num_threads: int = None, # maximum number of threads to use for parallel execution - logger=None, - *args, - **kwargs, - ): - super().__init__(agent, num_threads=num_threads, logger=logger, *args, **kwargs) - self.optimizer = optimizer - self.n_iters = 0 # number of iterations def train(self, guide, # guide to provide feedback @@ -314,8 +171,7 @@ def train(self, # evaluation test_dataset = None, # dataset of (x, info) pairs to evaluate the agent test_guide = None, # guide to provide scores for the test set - # test_frequency: Union[int, None] = 1, # frequency of evaluation - eval_frequency: Union[int, None] = 1, # frequency of evaluation + eval_frequency: Union[int, None] = 1, # frequency of evaluation num_eval_samples: int = 1, # number of samples to use to evaluate each input # logging log_frequency = None, # frequency of logging @@ -325,9 +181,8 @@ def train(self, ): ## Setup - # TODO legacy notation - test_frequency = eval_frequency # use eval_frequency as test_frequency + test_frequency = eval_frequency # use eval_frequency as test_frequency # TODO legacy notation log_frequency = log_frequency or test_frequency # frequency of logging (default to test_frequency) self.num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads test_dataset = test_dataset or train_dataset # default to train_dataset if test_dataset is not provided @@ -336,12 +191,29 @@ def train(self, self.score_range = score_range or (0., 1.) # Underscore attributes are temporary attributes for the algorithm (which will not be saved) # They would not affect the agent's state or the training process. - self._loader = DataLoader(train_dataset, batch_size=batch_size) # default data loader for training - self.sub_batch_size = sub_batch_size # sub-batch size for broadcasting the agents - self._guide = guide + # self._loader = DataLoader(train_dataset, batch_size=batch_size) # default data loader for training self._validate_dataset = validate_dataset self._validate_guide = validate_guide or guide + self.train_sampler = Sampler( + DataLoader(train_dataset, batch_size=batch_size), + guide, + num_threads=self.num_threads, + sub_batch_size=sub_batch_size, + score_range=self.score_range + ) + self.validate_sampler = Sampler( + DataLoader(validate_dataset if validate_dataset else {'inputs':[],'infos':[]}, batch_size=batch_size), + validate_guide or guide, + num_threads=self.num_threads, + sub_batch_size=sub_batch_size, + score_range=self.score_range + ) + + + + + # Evaluate the agent before learning # NOTE set test_frequency < 0 to skip first evaluation if (test_frequency is not None) and test_frequency > 0: @@ -396,129 +268,22 @@ def train(self, self.n_iters += 1 return - # TODO - def evaluate(self, agent, guide, xs, infos, min_score=None, num_samples=1, num_threads=None, description=None): - """ Evaluate the agent on the given dataset. """ - num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads - test_scores = evaluate(agent, guide, xs, infos, min_score=min_score, num_threads=num_threads, - description=description, num_samples=self.num_eval_samples) - if all([s is not None for s in test_scores]): - return np.mean(test_scores) - - # TODO move it out? - def sample(self, agents, loader=None, guide=None, **kwargs): + # Can be overridden by subclasses to implement specific sampling strategies + def sample(self, agents, verbose=False, **kwargs): """ Sample a batch of data based on the proposed parameters. All proposals are evaluated on the same batch of inputs. Args: agents (list): A list of trace.Modules (proposed parameters) to evaluate. - **kwargs: Additional keyword arguments that may be used by the implementation. - Returns: - list of list of dict: - A list (size len(agents)) with list of samples (size batchsize) for each agent, - where each sample is a dict containing: - - 'module': the trace.Module (proposal) - - 'x': the input data - - 'info': additional information about the input - - 'target': the target output (if applicable) - - 'score': the score of the proposal - - 'feedback': the feedback from the guide - - NOTE: The return might not be ordered in the same way as the agents. + **kwargs: Additional keyword arguments that may be used by the implementation. """ - assert all(isinstance(a, trace.Module) for a in agents), "All agents must be trace.Modules." - - loader = loader or self._loader # use the provided loader or the default one (train_dataset loader) - guide = guide or self._guide # use the provided guide or the default one (train_dataset guide) - - # Get a batch of inputs and infos from the loader - xs, infos = loader.sample() - - # XXX hack for now - self.xs, self.infos = xs, infos # store the inputs and infos for later use - - # Evaluate each agent on the sampled inputs - # - # agents : a1, a2 - # inputs: x1, x2, x3 - # infos: i1, i2, i3 - # sub_batch_size: 2 - # - # The forward is called in this order: - # (a1, x1, i1, guide1), - # (a1, x2, i2, guide1), - # (deepcopy(a1), x3, i3, guide1) - # (a2, x1, i1, guide2), - # (a2, x2, i2, guide2), - # (deepcopy(a2), x3, i3, guide2) - - num_threads = self.num_threads - min_score = self.score_range[0] - - batch_size = len(xs) - sub_batch_size = self.sub_batch_size or batch_size # if sub_batch_size is not provided, use the batch size - n_agents = len(agents) - - assert len(infos) == batch_size, "Length of infos must match length of xs." - - # Broadcast the agents to match the batch size - # [a1, a1, a1, a1, a1, ..., a2, a2, a2, ...] if sub_batch_size is not specified - # [a1, a1, a1_copy_1, a1_copy_1, a1_copy_2, ..., a2, a2, a2_copy_1, ...] if sub_batch_size of 2 is specified - broadcasted_agents = [] - for agent in agents: - for i in range(batch_size): - if i % sub_batch_size == 0 and i > 0: - agent = copy.deepcopy(agent) # create a copy of the agent for the next sub-batch - broadcasted_agents.append(agent) - - # Broadcast the inputs and infos to match the number of agents - # [x1, x2, x3, ..., x1, x2, x3, ...] - broadcasted_xs = [x for _ in range(n_agents) for x in xs] - broadcasted_infos = [info for _ in range(n_agents) for info in infos] - - # Broadcast the guides to match the batch size - - description = f"Forwarding {n_agents} agents on {batch_size} inputs" - - # Forward the agent on the inputs and compute the feedback using the guide - batched_forward = batch_run(max_workers=num_threads, description=description)(standard_forward) - outputs = batched_forward(agent=broadcasted_agents, - x=broadcasted_xs, - info=broadcasted_infos, - guide=guide, # guide will be broadcasted inside - min_score=min_score) - # return list of (target, score, feedback) - - # Collect results - results = [] # list of subgraphs (Subgraph objects) for each agent - for i in range(n_agents): - rollouts = [] # the compute result of each batch for a agent (trace.Module) - _agent = broadcasted_agents[i * batch_size ] # the first agent in the batch - for j in range(batch_size): - rollout = Rollout( - module=broadcasted_agents[i * batch_size + j], - x=broadcasted_xs[i * batch_size + j], - info=broadcasted_infos[i * batch_size + j], - target=outputs[i * batch_size + j][0], # target output - score=outputs[i * batch_size + j][1], # score of the proposal - feedback=outputs[i * batch_size + j][2], # feedback of the proposal - ) - if _agent != rollout.module: - results.append(Subgraph(rollouts)) # append the subgraph to the results - _agent = rollout.module # update the agent to the current one - rollouts = [] # reset rollouts for the new agent - rollouts.append(rollout) - - if rollouts: - results.append(Subgraph(rollouts)) # append the subgraph to the results + samples = Samples(*self.train_sampler.sample(agents)) # create a Samples object to store the samples and the minibatch # Log information about the sampling log_info = { - 'mean_score': np.mean([ g.get_scores() for g in results]), - 'batch_size': batch_size, - 'sub_batch_size': sub_batch_size, - 'n_epochs': loader.n_epochs, + 'mean_score': np.mean([ g.get_scores() for g in samples.samples]), + 'n_epochs': self.train_sampler.loader.n_epochs, } - return results, log_info + return samples, log_info def log(self, info_log, prefix=""): """ Log the information from the algorithm. """ @@ -528,7 +293,6 @@ def log(self, info_log, prefix=""): self.logger.log(f"{prefix}{key}", value, self.n_iters) except Exception as e: print(e) - breakpoint() # if logging fails, we can debug here def test(self, test_dataset, guide): min_score = self.score_range[0] @@ -542,12 +306,6 @@ def save(self, save_path): self.save_agent(save_path, self.n_iters) # TODO save full state of self - # Helper methods for the algorithm - def get_minibatch(self, samples): - """ Get a minibatch of samples from the provided samples. """ - # Since all proposals share the same batch, we can return the first sample's x and info - # return [s.x for s in samples[0]], [s['info'] for s in samples[0]] - return self.xs, self.infos # XXX hack for now # Unimplemented methods that should be implemented by subclasses def update(self, samples=None, verbose=False, **kwargs): @@ -640,6 +398,7 @@ def score(self): scores = [r['score'] for r in self.rollouts] return np.mean(scores) if scores else None + class PrioritySearch(SearchAlgorithm): # def train(self, *args, @@ -742,13 +501,15 @@ def propose(self, samples=None, verbose=False, n_proposals=1, **kwargs): # TODO what to do here? should we return n_proposals variations? return [update_dict] # return the update dict as a list + assert isinstance(samples, Samples), "samples must be an instance of Samples." + samples = samples.samples def _step(n, verbose=False, num_threads=None, **kwargs): """ Standard optimizer step for a single agent. """ # optimizer = self._optimizers[n] # get the optimizer for the n-th agent # TODO this seems slow optimizer = copy.deepcopy(self.optimizer) # create a copy of the optimizer to avoid modifying the original one - rollouts = samples[n] # Subgraph + rollouts = samples[n] # RolloutsGraph # Make sure all rollouts are based on the same module, so they can be viewed as a minibatch. optimizer.parameters = rollouts.module.parameters() # set the optimizer's parameters to the proposal's parameters @@ -766,11 +527,11 @@ def _step(n, verbose=False, num_threads=None, **kwargs): update_dict = remap_update_dict(self.agent, update_dict) # remap the update dict to the agent's parameters return update_dict # return the proposed parameters - n_agents = len(samples) # number of agents - args_list = [(n, verbose, self.num_threads) for n in range(n_agents)] + n_subgraphs = len(samples) # number of subgraphs (agents) in the samples + args_list = [(n, verbose, self.num_threads) for n in range(n_subgraphs)] args_list = args_list * n_proposals # repeat args_list n_proposals times - kwargs_list = [kwargs] * n_agents * n_proposals # repeat kwargs for each agent - update_dicts = async_run([_step]*n_agents*n_proposals, # run the optimizer step for each agent in parallel + kwargs_list = [kwargs] * n_subgraphs * n_proposals # repeat kwargs for each agent + update_dicts = async_run([_step]*n_subgraphs*n_proposals, # run the optimizer step for each agent in parallel args_list=args_list, kwargs_list=kwargs_list, max_workers=self.num_threads, # use the number of threads specified in the class @@ -795,35 +556,27 @@ def validate(self, candidates, samples=None, verbose=False, **kwargs): # Get the validation dataset from the samples. If no validation dataset is provided, use the current batch. if self._validate_dataset is None: # If no validation dataset is provided, use the current batch - xs, infos = self.get_minibatch(samples) - validate_dataset = {'inputs': xs, 'infos': infos} - else: - validate_dataset = self._validate_dataset - - - class Loader: # an trivial loader for the API - def __init__(self): - self.n_epochs = 0 - def sample(self): - return validate_dataset['inputs'], validate_dataset['infos'] - loader = Loader() # create a loader for the validation dataset + validate_dataset = samples.get_batch() # get the batch of inputs and infos from the samples + self.validate_sampler.loader.dataset = validate_dataset # set the validation dataset in the sampler + self.validate_sampler.batch_size = len(validate_dataset['inputs']) # set the batch size to the number of inputs in the validation dataset + candidate_agents = [c.get_module() for c in candidates] # get the modules from the candidates - validate_samples, _ = self.sample(candidate_agents, loader=loader, guide=self._validate_guide, **kwargs) + validate_samples = Samples(*self.validate_sampler.sample(candidate_agents)) # list of RolloutsGraph objects # TODO log _ if self.validate_proposals: if self._validate_dataset is None: - validate_samples += samples # if no validation dataset is provided, append the samples to the validate_samples + validate_samples.add_samples(samples) # if no validation dataset is provided, append the samples to the validate_samples else: # validate the agents in the validate_dataset # TODO need a flag? - exploration_agents = [rollouts.module for rollouts in samples] - exploration_samples = self.sample(exploration_agents, loader=loader, guide=self._validate_guide, **kwargs) - validate_samples += exploration_samples # append the exploration samples to the validate_samples + exploration_agents = [rollouts.module for rollouts in samples.samples] + exploration_samples = Samples(*self.validate_sampler.sample(exploration_agents)) # sample the exploration agents + validate_samples.add_samples(exploration_samples) # append the exploration samples to the validate_samples # Return a dict, key: ModuleCandidate, value: rollouts (list of dicts) results = {} - for rollouts in validate_samples: + for rollouts in validate_samples.samples: # rollouts is subgraph agent = rollouts.module index = candidate_agents.index(agent) @@ -851,6 +604,8 @@ def update_memory(self, validate_results, **kwargs): score = self.compute_score(candidate) # compute the score for the candidate heapq.heappush(self._queue, (-score, candidate)) # add the candidate to the priority queue + + #### def explore(self, **kwargs): """ Explore the parameter space and propose new candidates. Args: diff --git a/opto/trace/sampler.py b/opto/trainer/sampler.py similarity index 94% rename from opto/trace/sampler.py rename to opto/trainer/sampler.py index b0c1bfae..568575d0 100644 --- a/opto/trace/sampler.py +++ b/opto/trainer/sampler.py @@ -10,6 +10,7 @@ from opto.trainer.algorithms.basic_algorithms import Minibatch, AlgorithmBase, batchify from opto.trainer.evaluators import evaluate from opto.trainer.loader import DataLoader +from opto.trainer.guide import AutoGuide @dataclass class Rollout: @@ -34,6 +35,7 @@ def to_dict(self): "feedback": self.feedback, } + class RolloutsGraph: """ A rollouts graph is a collection of rollouts generated by the same agent (trace.Module) on different inputs. """ @@ -92,8 +94,8 @@ def __init__(self, raise TypeError("xs must be a list.") if not isinstance(infos, list): raise TypeError("infos must be a list.") - if not isinstance(guide, trace.Module): - raise TypeError("guide must be a trace.Module.") + if not isinstance(guide, AutoGuide): + raise TypeError("guide must be a AutoGuide.") if len(xs) != len(infos): raise ValueError("Length of xs must match length of infos.") self.module = module @@ -186,6 +188,8 @@ def sample_rollouts(configs, num_threads=1, forward=None, min_score=None, descri class Sampler: + """ A sampler that samples a batch of data from the loader and evaluates the agents on the sampled inputs. + """ def __init__(self, loader, guide, num_threads=1, sub_batch_size=None, forward=None, score_range=(-np.inf, np.inf)): """ Initialize the sampler with a data loader and a guide. @@ -196,9 +200,13 @@ def __init__(self, loader, guide, num_threads=1, sub_batch_size=None, forward=No sub_batch_size (int, optional): Size of the sub-batch to use for sampling. If None, uses the batch size. score_range (tuple): The range of scores to consider valid. """ - self._loader = loader - self._guide = guide + self.loader = loader + self.guide = guide self.num_threads = num_threads + if sub_batch_size is None: + sub_batch_size = loader.batch_size + else: + assert sub_batch_size <= loader.batch_size, "sub_batch_size must be less than or equal to the loader's batch size." self.sub_batch_size = sub_batch_size self.score_range = score_range if forward is None: @@ -234,7 +242,7 @@ def sample(self, agents): assert all(isinstance(a, trace.Module) for a in agents), "All agents must be trace.Modules." # Get a batch of inputs and infos from the loader - xs, infos = self._loader.sample() + xs, infos = self.loader.sample() batch = { 'inputs': xs, 'infos': infos @@ -263,14 +271,14 @@ def sample(self, agents): _xs, _infos = [], [] for i in range(batch_size): if i % self.sub_batch_size == 0 and i > 0: - configs.append(RolloutConfig(module=agent, xs=_xs, infos=_infos, guide=self._guide)) + configs.append(RolloutConfig(module=agent, xs=_xs, infos=_infos, guide=self.guide)) # reset agent = copy.deepcopy(agent) # create a deep copy of the agent for the next sub-batch _xs, _infos = [], [] _xs.append(xs[i]) _infos.append(infos[i]) if _xs: # if there are inputs in the sub-batch - configs.append(RolloutConfig(module=agent, xs=_xs, infos=_infos, guide=self._guide)) + configs.append(RolloutConfig(module=agent, xs=_xs, infos=_infos, guide=self.guide)) # Sample rollouts using the configs description = f"Sampling {len(agents)} agents on {batch_size} inputs" @@ -280,4 +288,4 @@ def sample(self, agents): min_score=self.score_range[0], description=description) - return samples + return samples, batch From f4e76ba3bfd2a5662bedbda480fe42be79ed413b Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 15 Jul 2025 03:29:07 +0000 Subject: [PATCH 116/314] Update PrioritySearch --- opto/trainer/algorithms/search_algorithms.py | 249 +++++++++---------- opto/trainer/sampler.py | 28 +++ 2 files changed, 143 insertions(+), 134 deletions(-) diff --git a/opto/trainer/algorithms/search_algorithms.py b/opto/trainer/algorithms/search_algorithms.py index e301e824..2f23f9c9 100644 --- a/opto/trainer/algorithms/search_algorithms.py +++ b/opto/trainer/algorithms/search_algorithms.py @@ -13,8 +13,8 @@ from opto.trainer.sampler import Sampler, RolloutsGraph -# TODO save and load SearchAlgorithm -# TODO async version +# TODO save and load SearchTemplate +# TODO async version??? # TODO create SYNC and ASYNC versions of the base class; add an attribute to the class to indicate # TODO a better data structure to store samples @@ -23,31 +23,6 @@ # Some helper function to convert between trace.Module and update_dict -# TODO move it and refactor the trainer code -def standard_forward(agent, x, guide, info, min_score=0): - """ Forward and compute feedback. - - Args: - agent: trace.Module - x: input - guide: (question, student_answer, info) -> score, feedback - info: additional information for the guide - min_score: minimum score when exception happens - - Returns: - target: output of the agent - score: score from the guide - feedback: feedback from the guide - """ - try: - target = agent(x) - score, feedback = guide(x, target.data, info) - except trace.ExecutionError as e: - target = e.exception_node - score, feedback = min_score, target.create_feedback('full') - return target, score, feedback - - def get_original_name(node): """Extract the original name from a node, removing all _copy suffixes.""" py_name = node.py_name # This removes colons: "param:0" -> "param0" @@ -69,16 +44,20 @@ def is_node_copy(a, b): def is_module_copy(a, b): """ Check if a and b (trace.Modules) are copies of each other. """ - parameters_a = a.parameters() - parameters_b = b.parameters() + parameters_a = a.parameters() # list of ParameterNode + parameters_b = b.parameters() # list of ParameterNode # Check if all parameters of a are copies of b or vice versa + # This might over count + # need to check 1:1 correspondence + matched = [] for p_a in parameters_a: - if not any(is_node_copy(p_a, p_b) for p_b in parameters_b): - return False - for p_b in parameters_b: - if not any(is_node_copy(p_b, p_a) for p_a in parameters_a): - return False - return True + _matched = [] + for p_b in parameters_b: + _matched.append(is_node_copy(p_a, p_b)) + np.array(matched) + if np.all(np.sum(matched, axis=1) == 1) and np.all(np.sum(matched, axis=0) == 1): + return True + return False def remap_update_dict(base_module, update_dict): """ Remap the update dict to the agent's parameters. update_dict might have keys which are copies of the base_module's parameters or visa versa. @@ -119,6 +98,8 @@ def create_module_from_update_dict(agent, update_dict): class Samples: + """ A container for samples collected during the search algorithm. It contains a list of RolloutsGraph objects + and a dataset with inputs and infos which created the list of RolloutsGraph. """ samples: List[RolloutsGraph] dataset: Dict[str, List[Any]] # contains 'inputs' and 'infos' keys @@ -130,7 +111,7 @@ def __init__(self, samples: List[RolloutsGraph], dataset: Dict[str, List[Any]]): assert 'inputs' in dataset and 'infos' in dataset, "dataset must contain 'inputs' and 'infos' keys." self.samples = samples - self.dataset = dataset # TODO this cannot be extracted from the samples in general + self.dataset = dataset # NOTE this cannot be extracted from the samples in general? def add_samples(self, samples): """ Add samples to the Samples object. """ @@ -152,16 +133,14 @@ def __iter__(self): return iter(self.samples) def __len__(self): - return len(self.samples) + return sum(len(s) for s in self.samples) -#TODO naming -class SearchAlgorithm(Minibatch): +class SearchTemplate(Minibatch): # This only uses __init__ and evaluate of Minibatch class. """ This implements a generic template for search algorithm. """ - def train(self, guide, # guide to provide feedback train_dataset, # dataset of (x, info) pairs to train the agent @@ -190,18 +169,13 @@ def train(self, ## Setup - test_frequency = eval_frequency # use eval_frequency as test_frequency # TODO legacy notation + test_frequency = eval_frequency # use eval_frequency as test_frequency # NOTE legacy notation log_frequency = log_frequency or test_frequency # frequency of logging (default to test_frequency) self.num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads test_dataset = test_dataset or train_dataset # default to train_dataset if test_dataset is not provided test_guide = test_guide or guide self.num_eval_samples = num_eval_samples # number of samples to use to evaluate each input self.score_range = score_range or (0., 1.) - # Underscore attributes are temporary attributes for the algorithm (which will not be saved) - # They would not affect the agent's state or the training process. - # self._loader = DataLoader(train_dataset, batch_size=batch_size) # default data loader for training - self._validate_dataset = validate_dataset - self._validate_guide = validate_guide or guide self.train_sampler = Sampler( DataLoader(train_dataset, batch_size=batch_size), @@ -210,22 +184,19 @@ def train(self, sub_batch_size=sub_batch_size, score_range=self.score_range ) + self._validate_dataset = validate_dataset # if None, the current batch will be used for validation self.validate_sampler = Sampler( - DataLoader(validate_dataset if validate_dataset else {'inputs':[],'infos':[]}, batch_size=batch_size), + DataLoader(validate_dataset if validate_dataset else {'inputs':[],'infos':[]}, batch_size=batch_size), validate_guide or guide, num_threads=self.num_threads, - sub_batch_size=sub_batch_size, + sub_batch_size=None, # no sub-batch size for validation score_range=self.score_range ) - - - - # Evaluate the agent before learning # NOTE set test_frequency < 0 to skip first evaluation if (test_frequency is not None) and test_frequency > 0: - info_test = self.test(test_dataset, test_guide) + info_test = self.test(test_dataset, test_guide) # test self.agent self.log(info_test) # Save the agent before learning if save_frequency > 0 @@ -244,15 +215,15 @@ def train(self, # 1. Propose new parameters given the current state of the algorithm # proposals: list of trace.Modules update_dict, proposals, info_update = self.update(samples, verbose=verbose, **kwargs) - self.optimizer.update(update_dict) # update the agent with the proposed parameters + self.optimizer.update(update_dict) # update self.agent with the proposed parameters # 2. Get feedback on the proposed parameters on the current batch - # samples: list of list of dict(module, x, info, target, score, feedback) + # samples: Samples object containing the samples and the minibatch samples, info_sample = self.sample(proposals, verbose=verbose, **kwargs) # Evaluate the agent after update if (test_frequency is not None) and (self.n_iters % test_frequency == 0): - info_test = self.test(test_dataset, test_guide) + info_test = self.test(test_dataset, test_guide) # test self.agent self.log(info_test, prefix="Test: ") # Save the algorithm state @@ -260,12 +231,15 @@ def train(self, self.save(save_path) # Log information + assert 'mean_score' in info_sample, "info_sample must contain 'mean_score'." + assert 'n_epochs' in info_sample, "info_sample must contain 'n_epochs'." + train_scores.append(info_sample['mean_score']) # so that mean can be computed if self.n_iters % log_frequency == 0: - self.logger.log('Average mean score', np.mean(train_scores), self.n_iters, color='blue') + self.logger.log('Average train score', np.mean(train_scores), self.n_iters, color='blue') self.log(info_update, prefix="Update: ") self.log(info_sample, prefix="Sample: ") - self.n_samples += sum(len(s) for s in samples) # update the number of samples processed + self.n_samples += len(samples) # update the number of samples processed self.logger.log('Number of samples', self.n_samples, self.n_iters, color='blue') # Log parameters for p in self.agent.parameters(): @@ -289,7 +263,7 @@ def sample(self, agents, verbose=False, **kwargs): # Log information about the sampling log_info = { 'mean_score': np.mean([ g.get_scores() for g in samples.samples]), - 'n_epochs': self.train_sampler.loader.n_epochs, + 'n_epochs': self.train_sampler.n_epochs, } return samples, log_info @@ -314,7 +288,6 @@ def save(self, save_path): self.save_agent(save_path, self.n_iters) # TODO save full state of self - # Unimplemented methods that should be implemented by subclasses def update(self, samples=None, verbose=False, **kwargs): """ Update the agent based on the provided samples. @@ -334,7 +307,9 @@ def update(self, samples=None, verbose=False, **kwargs): # return update_dict, proposals, info_log +# TODO make this hashable? class ModuleCandidate: + """ A container used by PrioritySearch to store a candidate module as (its base module and update dictionary) and its statistics. """ def __init__(self, base_module: Optional[trace.Module], @@ -349,11 +324,13 @@ def __init__(self, assert isinstance(base_module, trace.Module), "base_module must be a trace.Module." self.base_module = base_module self.update_dict = update_dict if update_dict is not None else {} - self.rollouts = [] # list of dicts containing the rollout information + self.rollouts = [] # list of dicts containing the rollout information (not RolloutsGraph, but a list of dicts) def get_module(self): """ Apply the update_dict to the base_module and return the updated module. This will not update the base_module itself.""" - return create_module_from_update_dict(self.base_module, self.update_dict) if self.update_dict else self.base_module + module = create_module_from_update_dict(self.base_module, self.update_dict) if self.update_dict else self.base_module + module._ModuleCandidate_candidate_id = id(self) # set the id of the module to the id of the candidate; this is used to identify the candidate in the priority queue + return module # return the updated module def apply_update(self, base_module=None): """ Apply update to the base_module in place. """ @@ -371,10 +348,9 @@ def __deepcopy__(self, memo): setattr(result, k, v) # base_module is not copied, it is the original module return result - def __equal__(self, other): + def __eq__(self, other): """ Check if two candidates are equal based on their base_module and update_dict. """ - if not isinstance(other, ModuleCandidate): - return False + assert isinstance(other, ModuleCandidate), "other must be an instance of ModuleCandidate." if self.base_module != other.base_module: return False update_dict_self = remap_update_dict(self.base_module, self.update_dict) @@ -383,21 +359,13 @@ def __equal__(self, other): def add_rollouts(self, rollouts: List[Dict[str, Any]]): """ Add rollouts to the candidate. """ + assert isinstance(rollouts, list), "rollouts must be a list of dicts." + assert all(isinstance(r, dict) for r in rollouts), "All rollouts must be dicts." + # Each rollout is a dict with keys: 'module', 'x', 'info', 'target', 'score', 'feedback' + assert all('module' in r and 'x' in r and 'info' in r and 'target' in r and 'score' in r and 'feedback' in r for r in rollouts), \ + "Each rollout must contain 'module', 'x', 'info', 'target', 'score', and 'feedback' keys." - # # Convert all ParameterNode to data in the rollouts - # _rollouts = [] - # for r in rollouts: - # _r = {} - # for k, v in r.items(): - # if isinstance(v, trace.ParameterNode): - # _r[k] = v.data - # else: - # _r[k] = v - - # _rollouts.append(_r) # convert all ParameterNode to data self.rollouts.extend(rollouts) - # # XXX TODO hacky - # self.rollouts.rollouts.extend(_rollouts) # extend the rollouts with the def score(self): """ Compute the score of the candidate based on the rollouts. """ @@ -407,14 +375,9 @@ def score(self): return np.mean(scores) if scores else None -class PrioritySearch(SearchAlgorithm): +class PrioritySearch(SearchTemplate): + """ A search algorithm that uses a priority queue to explore the parameter space and propose new candidates. """ - # def train(self, *args, - # num_candidates: int = 10, # number of candidates to propose - # default_score: Union[float, None] = None, # default score for the candidates - # validate_proposals: bool = True, # whether to validate the proposed parameters # TODO better naming - # **kwargs - # ): def train(self, guide, # guide to provide feedback train_dataset, # dataset of (x, info) pairs to train the agent @@ -424,7 +387,7 @@ def train(self, validate_guide = None, # to provide scores for the validation set # training loop batch_size = 1, # batch size for updating the agent - sub_batch_size = None, # sub-batch size for broadcasting the agents + sub_batch_size = None, # sub-batch size that each optimizer attends to score_range = None, # minimum score to update the agent num_epochs = 1, # number of training epochs num_threads = None, # maximum number of threads to use @@ -439,19 +402,18 @@ def train(self, save_path: str = "checkpoints/agent.pkl", # path to save the agent # Priority Search specific parameters num_candidates: int = 10, # number of candidates to propose - default_score: Union[float, None] = None, # default score for the candidates + default_score: float = float('inf'), # default score assigned to priority queue candidates validate_proposals: bool = True, # whether to validate the proposed parameters # Additional keyword arguments **kwargs ): - # Create agents and optimizers for search - self.num_candidates = num_candidates # number of candidates to propose - self.score_range = score_range or (0., 1.) # XXX hacky now - self.default_score = default_score if default_score is not None else self.score_range[0] # default score for the candidates + self.num_candidates = num_candidates # number of candidates to propose by each optimizer call self.validate_proposals = validate_proposals # whether to validate the proposed parameters - self._queue = [(self.default_score, ModuleCandidate(self.agent))] # priority queue of ModuleCandidates, initialized with the base agent + self.default_score = default_score + self.memory = [(self.default_score, ModuleCandidate(self.agent))] # Priority queue of ModuleCandidates, initialized with the base agent + self._exploration_candidates = None super().train(guide, train_dataset, validate_dataset=validate_dataset, @@ -480,18 +442,21 @@ def update(self, samples=None, verbose=False, **kwargs): # 3. Update the priority queue with the validation results self.update_memory(validate_results, verbose=verbose, **kwargs) # samples are provided here in case candidates do not capture full information # 4. Explore and exploit the priority queue - best_candidate = self.exploit(verbose=verbose, **kwargs) # get the best candidate (ModuleCandidate) from the priority queue - exploration_candidates = self.explore(verbose=verbose, **kwargs) # List of ModuleCandidates + best_candidate, info_exploit = self.exploit(verbose=verbose, **kwargs) # get the best candidate (ModuleCandidate) from the priority queue + exploration_candidates, info_explore = self.explore(verbose=verbose, **kwargs) # List of ModuleCandidates + self._exploration_candidates = exploration_candidates - # TBD Log information about the update + # TODO Log information about the update info_log = { 'best_candidate_score': best_candidate.score(), 'num_exploration_candidates': len(exploration_candidates), } + info_log.update(info_exploit) # add the info from the exploit step + info_log.update(info_explore) # add the info from the explore step return best_candidate.update_dict, [c.get_module() for c in exploration_candidates], info_log - def propose(self, samples=None, verbose=False, n_proposals=1, **kwargs): + def propose(self, samples, verbose=False, n_proposals=1, **kwargs): """ Analyzing samples and propose new parameters using self.optimizer. An independent optimizer is used for the minibatch generated by one agent and generates n_proposals proposals. Args: @@ -503,18 +468,14 @@ def propose(self, samples=None, verbose=False, n_proposals=1, **kwargs): Returns: candidates (list of ModuleCandidate): A list of proposed candidates for the next iteration. """ - if samples is None: - parameters = self.optimizer.parameters # use the current parameters of the optimizer - update_dict = {p: p.data for p in parameters} # return the current parameters as the update dict - # TODO what to do here? should we return n_proposals variations? - return [update_dict] # return the update dict as a list assert isinstance(samples, Samples), "samples must be an instance of Samples." - samples = samples.samples + samples = samples.samples # list of RolloutsGraph objects + def _step(n, verbose=False, num_threads=None, **kwargs): """ Standard optimizer step for a single agent. """ # optimizer = self._optimizers[n] # get the optimizer for the n-th agent - # TODO this seems slow + # NOTE this seems slow optimizer = copy.deepcopy(self.optimizer) # create a copy of the optimizer to avoid modifying the original one rollouts = samples[n] # RolloutsGraph @@ -549,68 +510,70 @@ def _step(n, verbose=False, num_threads=None, **kwargs): candidates = [ModuleCandidate(self.agent, update_dict) for update_dict in update_dicts] return candidates - - def validate(self, candidates, samples=None, verbose=False, **kwargs): + def validate(self, candidates, samples, verbose=False, **kwargs): """ Validate the proposed candidate parameters Args: - candidates (list of dict): A list of ModuleCandidate objects representing the proposed parameters. + candidates (list of ModuleCandidate): A list of ModuleCandidate objects representing the proposed parameters. samples (list of dict, optional): A list of samples collected in the current iteration. Defaults to None. verbose (bool, optional): Whether to print verbose output. Defaults to False. **kwargs: Additional keyword arguments that may be used by the implementation. Returns: - results (dict [ModuleCandidate, list of dict]): A dictionary where the keys are ModuleCandidate objects and the values are lists of rollouts (list of dicts) containing the module, x, info, target, score, feedback. + results (dict): A dictionary where the keys are ids of ModuleCandidate objects and the values are ModuleCandidate and lists of rollouts (list of dicts) containing the module, x, info, target, score, feedback. """ # Get the validation dataset from the samples. If no validation dataset is provided, use the current batch. if self._validate_dataset is None: # If no validation dataset is provided, use the current batch validate_dataset = samples.get_batch() # get the batch of inputs and infos from the samples - self.validate_sampler.loader.dataset = validate_dataset # set the validation dataset in the sampler + self.validate_sampler.dataset = validate_dataset # set the validation dataset in the sampler self.validate_sampler.batch_size = len(validate_dataset['inputs']) # set the batch size to the number of inputs in the validation dataset candidate_agents = [c.get_module() for c in candidates] # get the modules from the candidates validate_samples = Samples(*self.validate_sampler.sample(candidate_agents)) # list of RolloutsGraph objects - # TODO log _ + + exploration_candidates = self._exploration_candidates # exploration candidates from the previous iteration + assert exploration_candidates is not None, "exploration_candidates must be set before calling validate." if self.validate_proposals: if self._validate_dataset is None: + # NOTE this might contain some duplicates due to sub_batch_size < batch_size validate_samples.add_samples(samples) # if no validation dataset is provided, append the samples to the validate_samples else: # validate the agents in the validate_dataset - # TODO need a flag? - exploration_agents = [rollouts.module for rollouts in samples.samples] + # exploration_agents = [rollouts.module for rollouts in samples.samples] # NOTE this might contain some duplicates due to sub_batch_size < batch_size + exploitation_agents = [c.get_module() for c in exploration_candidates] # get the modules from the exploration candidates exploration_samples = Samples(*self.validate_sampler.sample(exploration_agents)) # sample the exploration agents validate_samples.add_samples(exploration_samples) # append the exploration samples to the validate_samples # Return a dict, key: ModuleCandidate, value: rollouts (list of dicts) - results = {} + # In validate_samples, there may be multiple rollouts collected by the same agent (or their copies). + # We need to group the rollouts by the agent (ModuleCandidate) and return a dictionary where the keys are the ModuleCandidate objects and the values are lists of rollouts (list of dicts). + results = {} # dict of ModuleCandidate: list of rollouts (list of dicts) + for c in candidates + exploration_candidates: + # Initialize the candidate in the results dictionary + results[id(c)] = (c, []) # (ModuleCandidate, list of rollouts) + for rollouts in validate_samples.samples: - # rollouts is subgraph - agent = rollouts.module - index = candidate_agents.index(agent) - candidate = candidates[index] # get the candidate corresponding to the agent - # TODO delete 'module' from the rollouts dict? - if candidate in results: - # If the candidate already exists in results, we can append the rollouts to the existing list - results[candidate].extend(rollouts) - else: - # If the candidate does not exist in results, we create a new entry - results[candidate] = rollouts + module = rollouts.module # trace.Module + key = module._ModuleCandidate_candidate_id # use the candidate id as the key + if key not in results: + raise ValueError(f"ModuleCandidate with id {key} not found in results. Samples are not collected by known candidates.") + # Append the rollouts to the list of rollouts for the key + results[key][1].extend(rollouts.to_list()) return results def update_memory(self, validate_results, **kwargs): - """ Update the priority queue with the validation results. Args: validate_results (dict): A dictionary where the keys are ModuleCandidate objects and the values are lists of rollouts (list of dicts) containing the module, x, info, target, score, feedback. **kwargs: Additional keyword arguments that may be used by the implementation. """ - for candidate, rollouts in validate_results.items(): - candidate.add_rollouts(rollouts.to_list()) # add the rollouts to the candidate + for candidate_id, (candidate, rollouts) in validate_results.items(): + candidate.add_rollouts(rollouts) # add the rollouts to the candidate score = self.compute_score(candidate) # compute the score for the candidate - heapq.heappush(self._queue, (-score, candidate)) # add the candidate to the priority queue + heapq.heappush(self.memory, (-score, candidate)) # add the candidate to the priority queue #### @@ -624,13 +587,14 @@ def explore(self, **kwargs): """ # pop top self.num_candidates candidates from the priority queue top_candidates = [] - while len(top_candidates) < self.num_candidates and self._queue: - score, candidate = heapq.heappop(self._queue) + while len(top_candidates) < self.num_candidates and self.memory: + score, candidate = heapq.heappop(self.memory) top_candidates.append(candidate) # add the candidate to the top candidates - return top_candidates + return top_candidates, {} def exploit(self, **kwargs): + # NOTE This function can be overridden by subclasses to compute a different score """ Exploit the best candidate from the priority queue. This method should not change the priority queue. Args: **kwargs: Additional keyword arguments that may be used by the implementation. @@ -639,14 +603,31 @@ def exploit(self, **kwargs): """ # Right now, we just return the best candidate from the priority queue # This function can be overridden by subclasses to implement a different exploitation strategy - if not self._queue: + if not self.memory: raise ValueError("The priority queue is empty. Cannot exploit.") - best = min(self._queue) # (score, candidate) - return best[1] + best = min(self.memory) # (score, candidate) + score, best_candidate = best + score = -score # remember that we stored negative scores in the priority queue + return best_candidate, { + 'best_candidate_score': score, # remember that we stored negative scores in the priority queue + } + + def compute_score(self, candidate): - # By default, we compute the mean score of the rollouts # NOTE This function can be overridden by subclasses to compute a different score + """ Compute the score for the candidate based on the rollouts during the validation phase. + It can be overridden by subclasses to implement a different scoring strategy. + + Args: + candidate (ModuleCandidate): The candidate for which to compute the score. + Returns: + float: The computed score for the candidate. + """ + if not isinstance(candidate, ModuleCandidate): + raise TypeError("candidate must be an instance of ModuleCandidate.") + # By default, we compute the mean score of the rollouts + scores = [r['score'] for r in candidate.rollouts] default_score = self.default_score if self.default_score is not None else self.score_range[1] # default score for the candidates diff --git a/opto/trainer/sampler.py b/opto/trainer/sampler.py index 568575d0..9e1037a2 100644 --- a/opto/trainer/sampler.py +++ b/opto/trainer/sampler.py @@ -212,6 +212,34 @@ def __init__(self, loader, guide, num_threads=1, sub_batch_size=None, forward=No if forward is None: self.forward = standard_forward + @property + def dataset(self): + """ Get the dataset of the loader. """ + return self.loader.dataset + + @dataset.setter + def dataset(self, value): + """ Set the dataset of the loader. """ + assert isinstance(value, dict), "Dataset must be a dictionary with 'inputs' and 'infos' keys." + assert 'inputs' in value and 'infos' in value, "Dataset must contain 'inputs' and 'infos' keys." + assert len(value['inputs']) == len(value['infos']), "Length of inputs must match length of infos." + self.loader.dataset = value + + @property + def batch_size(self): + """ Get the batch size of the loader. """ + return self.loader.batch_size + + @batch_size.setter + def batch_size(self, value): + """ Set the batch size of the loader. """ + self.loader.batch_size = value + + @property + def n_epochs(self): + """ Get the number of epochs of the loader. """ + return self.loader.n_epochs + def sample(self, agents): """ Sample a batch of data from the loader and evaluate the agents. From c44fe876d65e049989e208a753b69a7545e55642 Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 15 Jul 2025 03:52:43 +0000 Subject: [PATCH 117/314] Add unit test for sampler. --- tests/unit_tests/test_sampler.py | 133 +++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 tests/unit_tests/test_sampler.py diff --git a/tests/unit_tests/test_sampler.py b/tests/unit_tests/test_sampler.py new file mode 100644 index 00000000..0ac4d104 --- /dev/null +++ b/tests/unit_tests/test_sampler.py @@ -0,0 +1,133 @@ +from opto import trace +from opto.trainer.sampler import Sampler +from opto.trainer.loader import DataLoader +from opto.trainer.guide import AutoGuide +from opto.trainer.algorithms.search_algorithms import is_node_copy + + +class Guide(AutoGuide): + + def get_feedback(self, query, response, reference=None, **kwargs): + """ + Provide feedback based on the query and response. + + Args: + query: The query to analyze. + response: The response generated by the model. + reference: Optional reference answer for comparison. + **kwargs: Additional context or parameters. + + Returns: + A tuple containing a score and feedback string. + """ + score = response == reference + feedback = "Exact match!" if score == 1.0 else "Not an exact match." + return score, feedback + +@trace.model +class Agent: + + def __init__(self): + self.param = trace.node(1., trainable=True) + self.state = 0 + + @trace.bundle() + def forward(self, x): + self.state += 1 + return self.state + + +def test_sample_with_single_agent(): + + xs = [1, 2, 3, 4, 5] + infos = [1, 2, 3, 4, 5] + batch_size = 3 + sub_batch_size = 2 + num_threads = 2 + dataset = {'inputs': xs, 'infos': infos} + loader = DataLoader(dataset, batch_size=batch_size, randomize=False) + sampler = Sampler(loader=loader, guide=Guide(), sub_batch_size=sub_batch_size, num_threads=num_threads) + + + ## Test with a single agent + samples, batch = sampler.sample([Agent()]) + + # check batch is equal to dataset's first batch_size elements + assert batch['inputs'] == dataset['inputs'][:3] + assert batch['infos'] == dataset['infos'][:3] + + assert len(samples) == 2 + + # a batch of 3 is split into 2 sub-batches of size 2 and 1 + assert is_node_copy(samples[0].module.parameters()[0], samples[1].module.parameters()[0]) + assert len(samples[0].rollouts) == 2 + assert len(samples[1].rollouts) == 1 + + for rollouts in samples: + for rollout in rollouts: + assert rollout.target == 1 # state is not affected by multiple calls + + + samples, batch = sampler.sample([Agent()]) + + # check batch is equal to dataset's second batch_size elements + assert batch['inputs'] == dataset['inputs'][3:] + assert batch['infos'] == dataset['infos'][3:] + assert len(samples) == 1 + assert len(samples[0].rollouts) == 2 + + for rollouts in samples: + for rollout in rollouts: + assert rollout.target == 1 # state is not affected by multiple calls + + +def test_sample_with_multiple_agents(): + """ + Test sampling with multiple agents. + This will create a batch of samples from two agents. + """ + + xs = [1, 2, 3, 4, 5] + infos = [1, 2, 3, 4, 5] + batch_size = 3 + sub_batch_size = 2 + num_threads = 2 + dataset = {'inputs': xs, 'infos': infos} + loader = DataLoader(dataset, batch_size=batch_size, randomize=False) + sampler = Sampler(loader=loader, guide=Guide(), sub_batch_size=sub_batch_size, num_threads=num_threads) + + + ## Test with multiple agents + samples, batch = sampler.sample([Agent(), Agent()]) + + # check batch is equal to dataset's first batch_size elements + assert batch['inputs'] == dataset['inputs'][:3] + assert batch['infos'] == dataset['infos'][:3] + + assert len(samples) == 4, f"Expected 4 samples, got {len(samples)}" + + assert is_node_copy(samples[0].module.parameters()[0], samples[1].module.parameters()[0]) + assert len(samples[0].rollouts) == 2 + assert len(samples[1].rollouts) == 1 + + assert is_node_copy(samples[2].module.parameters()[0], samples[3].module.parameters()[0]) + assert len(samples[2].rollouts) == 2 + assert len(samples[3].rollouts) == 1 + + for rollouts in samples: + for rollout in rollouts: + assert rollout.target == 1 # state is not affected by multiple calls + + samples, batch = sampler.sample([Agent(), Agent()]) + # check batch is equal to dataset's second batch_size elements + assert batch['inputs'] == dataset['inputs'][3:] + assert batch['infos'] == dataset['infos'][3:] + + assert len(samples) == 2, f"Expected 2 samples, got {len(samples)}" + + assert len(samples[0].rollouts) == 2 + assert len(samples[1].rollouts) == 2 + + for rollouts in samples: + for rollout in rollouts: + assert rollout.target == 1 # state is not affected by multiple calls \ No newline at end of file From 7d14f8bdb0c96c69c5703852db180e0f040824e3 Mon Sep 17 00:00:00 2001 From: Xuanfei Ren Date: Tue, 15 Jul 2025 22:28:49 -0500 Subject: [PATCH 118/314] fixed a bug about remapped_update_dict --- opto/trainer/algorithms/search_algorithms.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opto/trainer/algorithms/search_algorithms.py b/opto/trainer/algorithms/search_algorithms.py index 2f23f9c9..85c3bd57 100644 --- a/opto/trainer/algorithms/search_algorithms.py +++ b/opto/trainer/algorithms/search_algorithms.py @@ -82,8 +82,8 @@ def set_module_parameters(agent, update_dict): The update_dict is a dictionary of ParameterNode: value pairs. The agent's parameters will be updated with the values from the update_dict. """ - remap_update_dict = remap_update_dict(agent, update_dict) # remap the update dict to the agent's parameters - for k, v in remap_update_dict.items(): + remapped_update_dict = remap_update_dict(agent, update_dict) # remap the update dict to the agent's parameters + for k, v in remapped_update_dict.items(): k._data = v # set the parameter's data to the value in the update_dict def create_module_from_update_dict(agent, update_dict): From 8d47177f5b1047504068b5bd1ce8ec92886cc7be Mon Sep 17 00:00:00 2001 From: Xuanfei Ren Date: Tue, 15 Jul 2025 22:32:07 -0500 Subject: [PATCH 119/314] add ucb reward --- examples/gsm8k_search_algo.py | 15 +++---- opto/trainer/algorithms/search_algorithms.py | 45 ++++++++++++++++++++ 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/examples/gsm8k_search_algo.py b/examples/gsm8k_search_algo.py index 832ec8e1..82177401 100644 --- a/examples/gsm8k_search_algo.py +++ b/examples/gsm8k_search_algo.py @@ -3,8 +3,8 @@ from opto import trace from opto.utils.llm import LLM, LiteLLM from opto.optimizers import OptoPrime -from opto.trainer.algorithms.search_algorithms import PrioritySearch as SearchAlgorithm -from opto.trainer.loggers import TensorboardLogger +from opto.trainer.algorithms.search_algorithms import UCBSearch as SearchAlgorithm +from opto.trainer.loggers import WandbLogger from opto.trainer.guide import VerbalJudgeGuide from typing import Any @@ -47,8 +47,7 @@ def forward(self, message: Any) -> Any: Guide = VerbalJudgeGuide -Logger = TensorboardLogger - +Logger = WandbLogger def main(): # set seed @@ -58,9 +57,9 @@ def main(): eval_frequency = -1 num_threads = 3 verbose = True - teacher_model = None # use default model - student_model = None # use default model - optimizer_model = None # use default model + teacher_model = "vertex_ai/gemini-2.0-flash" # use default model + student_model = "vertex_ai/gemini-2.0-flash" # use default model + optimizer_model = "vertex_ai/gemini-2.0-flash" # use default model np.random.seed(seed) @@ -73,7 +72,7 @@ def main(): agent = Learner(llm=LLM(student_model)) guide = Guide(llm=LLM(teacher_model)) optimizer = OptoPrime(agent.parameters(), llm=LLM(optimizer_model)) - logger = Logger(verbose=verbose) + logger = Logger(project="gsm8k-examples", name="ucb",verbose=verbose) # set use_json_object_format=False if LLM does not support JSON object format alg = SearchAlgorithm( diff --git a/opto/trainer/algorithms/search_algorithms.py b/opto/trainer/algorithms/search_algorithms.py index 85c3bd57..bbf551bc 100644 --- a/opto/trainer/algorithms/search_algorithms.py +++ b/opto/trainer/algorithms/search_algorithms.py @@ -632,3 +632,48 @@ def compute_score(self, candidate): default_score = self.default_score if self.default_score is not None else self.score_range[1] # default score for the candidates return np.mean(scores) if scores else self.default_score + +class UCBSearch(PrioritySearch): + """A search algorithm that keeps a buffer with candidates and their UCB scores. It does exploration according to the UCB score.""" + + def __init__(self, *args, exploration_constant=1.0, **kwargs): + """Initialize UCBSearch with an exploration constant for the UCB formula.""" + super().__init__(*args, **kwargs) + self.exploration_constant = exploration_constant + + def compute_score(self, candidate): + """Compute the UCB score for the candidate. + + UCB = mean_score + exploration_constant * sqrt(ln(total_trials) / candidate_trials) + + Args: + candidate (ModuleCandidate): The candidate for which to compute the UCB score. + Returns: + float: The computed UCB score for the candidate. + """ + if not isinstance(candidate, ModuleCandidate): + raise TypeError("candidate must be an instance of ModuleCandidate.") + + # Get scores from rollouts + scores = [r['score'] for r in candidate.rollouts] + + # If no rollouts, return a high exploration score to encourage trying this candidate + if not scores: + return float('inf') # Maximum exploration for untried candidates + + # Calculate mean score for this candidate + mean_score = np.mean(scores) + candidate_trials = len(scores) + + # Calculate total trials across all candidates in memory + total_trials = sum(len(c.rollouts) for _, c in self.memory) + + # Handle edge case where total_trials is 0 or 1 + if total_trials <= 1: + return mean_score + + # Calculate UCB score + exploration_term = self.exploration_constant * np.sqrt(np.log(total_trials) / candidate_trials) + ucb_score = mean_score + exploration_term + + return ucb_score From 925c7cb405bc30b4ea869b229a8fd93b00234292 Mon Sep 17 00:00:00 2001 From: chinganc Date: Wed, 16 Jul 2025 06:12:51 +0000 Subject: [PATCH 120/314] Fix some bugs in PrioritySearch. Update search_algo example script. --- examples/gsm8k_search_algo.py | 13 ++++++++++--- opto/trainer/algorithms/search_algorithms.py | 9 ++++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/examples/gsm8k_search_algo.py b/examples/gsm8k_search_algo.py index 832ec8e1..1243cd8f 100644 --- a/examples/gsm8k_search_algo.py +++ b/examples/gsm8k_search_algo.py @@ -54,9 +54,13 @@ def main(): # set seed seed = 42 num_epochs = 1 - batch_size = 1 + batch_size = 3 + sub_batch_size = 2 + score_range = (0, 1) # range of the score for the guide eval_frequency = -1 - num_threads = 3 + num_eval_samples = 2 + num_threads = 10 + datasize = 5 verbose = True teacher_model = None # use default model student_model = None # use default model @@ -66,7 +70,7 @@ def main(): # In this example, we use the GSM8K dataset, which is a dataset of math word problems. # We will look the training error of the agent on a small portion of this dataset. - train_dataset = datasets.load_dataset('openai/gsm8k', 'main')['train'][:10] + train_dataset = datasets.load_dataset('openai/gsm8k', 'main')['train'][:datasize] train_dataset = dict(inputs=train_dataset['question'], infos=train_dataset['answer']) test_dataset = train_dataset @@ -88,6 +92,9 @@ def main(): eval_frequency=eval_frequency, test_dataset=test_dataset, num_threads=num_threads, + sub_batch_size=sub_batch_size, + score_range=score_range, + num_eval_samples=num_eval_samples, verbose='output' if verbose else False) diff --git a/opto/trainer/algorithms/search_algorithms.py b/opto/trainer/algorithms/search_algorithms.py index 85c3bd57..fd8eb1bc 100644 --- a/opto/trainer/algorithms/search_algorithms.py +++ b/opto/trainer/algorithms/search_algorithms.py @@ -261,8 +261,10 @@ def sample(self, agents, verbose=False, **kwargs): samples = Samples(*self.train_sampler.sample(agents)) # create a Samples object to store the samples and the minibatch # Log information about the sampling + scores = [ g.get_scores() for g in samples.samples] # list of list of scores for each RolloutsGraph + scores = [item for sublist in scores for item in sublist] # flatten the list of scores log_info = { - 'mean_score': np.mean([ g.get_scores() for g in samples.samples]), + 'mean_score': np.mean(scores), 'n_epochs': self.train_sampler.n_epochs, } return samples, log_info @@ -328,7 +330,7 @@ def __init__(self, def get_module(self): """ Apply the update_dict to the base_module and return the updated module. This will not update the base_module itself.""" - module = create_module_from_update_dict(self.base_module, self.update_dict) if self.update_dict else self.base_module + module = create_module_from_update_dict(self.base_module, self.update_dict) if self.update_dict else copy.deepcopy(self.base_module) module._ModuleCandidate_candidate_id = id(self) # set the id of the module to the id of the candidate; this is used to identify the candidate in the priority queue return module # return the updated module @@ -545,7 +547,8 @@ def validate(self, candidates, samples, verbose=False, **kwargs): validate_samples.add_samples(exploration_samples) # append the exploration samples to the validate_samples - # Return a dict, key: ModuleCandidate, value: rollouts (list of dicts) + # TODO some ModuleCandidate are the same in parameters though they have different ids + # In validate_samples, there may be multiple rollouts collected by the same agent (or their copies). # We need to group the rollouts by the agent (ModuleCandidate) and return a dictionary where the keys are the ModuleCandidate objects and the values are lists of rollouts (list of dicts). results = {} # dict of ModuleCandidate: list of rollouts (list of dicts) From fb571ba3499673c4eef24511d46a93f6cd786c4a Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 16 Jul 2025 11:36:28 -0400 Subject: [PATCH 121/314] add JSON option through OptimizerPromptSymbolSetJSON --- opto/optimizers/optoprime_v2.py | 237 +++++++++++++++++++++++--------- 1 file changed, 170 insertions(+), 67 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index 62a1c0e0..bbdeaba4 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -127,7 +127,6 @@ def extract_xml_like_data(text: str, reasoning_tag: str = "reasoning", return result - class OptimizerPromptSymbolSet: """ By inheriting this class and pass into the optimizer. People can change the optimizer documentation @@ -138,6 +137,8 @@ class OptimizerPromptSymbolSet: - Output format: the format of the output of the optimizer """ + # Titles should be written as markdown titles (space between # and title) + # In text, we automatically remove space in the title, so it will become `#Title` variables_section_title = "# Variables" inputs_section_title = "# Inputs" outputs_section_title = "# Outputs" @@ -158,22 +159,85 @@ class OptimizerPromptSymbolSet: improved_variable_tag = "variable" name_tag = "name" - # custom output format (this will give the highest degree of freedom) - # once it's set, it will override the default output format - output_format_prompt_instruction = None + # custom output format + # if this is not None, then the user needs to implement the following functions: + # - output_response_extractor + # - example_output + custom_output_format_instruction = None + + @property + def output_format(self) -> str: + """ + This function defines the input to: + ``` + {output_format} + ``` + In the self.output_format_prompt_template in the OptoPrimeV2 + """ + if self.custom_output_format_instruction is None: + # we use a default XML like format + return dedent(f""" + <{self.reasoning_tag}> + reasoning + + <{self.improved_variable_tag}> + <{self.name_tag}>variable_name + <{self.value_tag}> + value + + + """) + else: + return self.custom_output_format_instruction.strip() + + def example_output(self, reasoning, variables): + """ + reasoning: str + variables: format {variable_name, value} + """ + if self.custom_output_format_instruction is not None: + raise NotImplementedError + else: + # Build the output string in the same XML-like format as self.output_format + output = [] + output.append(f"<{self.reasoning_tag}>") + output.append(reasoning) + output.append(f"") + for var_name, value in variables.items(): + output.append(f"<{self.improved_variable_tag}>") + output.append(f"<{self.name_tag}>{var_name}") + output.append(f"<{self.value_tag}>") + output.append(str(value)) + output.append(f"") + output.append(f"") + return "\n".join(output) + def output_response_extractor(self, response: str) -> Dict[str, Any]: - if self.output_format_prompt_instruction is None: + # the response here should just be plain text + + if self.custom_output_format_instruction is None: extracted_data = extract_xml_like_data(response, reasoning_tag=self.reasoning_tag, improved_variable_tag=self.improved_variable_tag, name_tag=self.name_tag, value_tag=self.value_tag) + + # if the suggested value is a code, and the entire code body is empty (i.e., not even function signature is present) + # then we remove such suggestion + keys_to_remove = [] + for key, value in extracted_data['variables'].items(): + if "__code" in key and value.strip() == "": + keys_to_remove.append(key) + + for key in keys_to_remove: + del extracted_data['variables'][key] + return extracted_data else: raise NotImplementedError( "If you supplied a custom output format prompt template, you need to implement your own response extractor") - + @property def default_prompt_symbols(self) -> Dict[str, str]: return { @@ -187,6 +251,91 @@ def default_prompt_symbols(self) -> Dict[str, str]: "documentation": self.documentation_section_title, } +class OptimizerPromptSymbolSetJSON(OptimizerPromptSymbolSet): + """We enforce a JSON output format extraction""" + + custom_output_format_instruction = """ + {{ + "reasoning": , + "suggestion": {{ + : , + : , + }} + }} + """ + + def example_output(self, reasoning, variables): + """ + reasoning: str + variables: format {variable_name, value} + """ + + # Build the output string in the same JSON format as described in custom_output_format_instruction + output = { + "reasoning": reasoning, + "suggestion": {var_name: value for var_name, value in variables.items()} + } + return json.dumps(output, indent=2) + + def output_response_extractor(self, response: str) -> Dict[str, Any]: + reasoning = "" + suggestion_tag = "suggestion" + + if "```" in response: + response = response.replace("```", "").strip() + + suggestion = {} + attempt_n = 0 + while attempt_n < 2: + try: + suggestion = json.loads(response)[suggestion_tag] + reasoning = json.loads(response)[self.reasoning_tag] + break + except json.JSONDecodeError: + # Remove things outside the brackets + response = re.findall(r"{.*}", response, re.DOTALL) + if len(response) > 0: + response = response[0] + attempt_n += 1 + except Exception: + attempt_n += 1 + + if not isinstance(suggestion, dict): + suggestion = {} + + if len(suggestion) == 0: + # we try to extract key/value separately and return it as a dictionary + pattern = rf'"{suggestion_tag}"\s*:\s*\{{(.*?)\}}' + suggestion_match = re.search(pattern, str(response), re.DOTALL) + if suggestion_match: + suggestion = {} + # Extract the entire content of the suggestion dictionary + suggestion_content = suggestion_match.group(1) + # Regex to extract each key-value pair; + # This scheme assumes double quotes but is robust to missing commas at the end of the line + pair_pattern = r'"([a-zA-Z0-9_]+)"\s*:\s*"(.*)"' + # Find all matches of key-value pairs + pairs = re.findall(pair_pattern, suggestion_content, re.DOTALL) + for key, value in pairs: + suggestion[key] = value + + if len(suggestion) == 0: + print(f"Cannot extract suggestion from LLM's response:") + print(response) + + # if the suggested value is a code, and the entire code body is empty (i.e., not even function signature is present) + # then we remove such suggestion + keys_to_remove = [] + for key, value in suggestion.items(): + if "__code" in key and value.strip() == "": + keys_to_remove.append(key) + for key in keys_to_remove: + del suggestion[key] + + extracted_data = {"reasoning": reasoning, + "variables": suggestion} + + return extracted_data class OptimizerPromptSymbolSet2(OptimizerPromptSymbolSet): variables_section_title = "# Variables" @@ -207,7 +356,6 @@ class OptimizerPromptSymbolSet2(OptimizerPromptSymbolSet): reasoning_tag = "reason" improved_variable_tag = "var" name_tag = "name" - value_tag = "data" @dataclass @@ -262,7 +410,7 @@ def __repr__(self) -> str: others=self.others, feedback=self.feedback, ), self.optimizer_prompt_symbol_set.default_prompt_symbols) - + def replace_symbols(self, text: str, symbols: Dict[str, str]) -> str: default_prompt_symbols = { "variables": "# Variables", @@ -275,27 +423,13 @@ def replace_symbols(self, text: str, symbols: Dict[str, str]) -> str: "code": "# Code", "documentation": "# Documentation", } - + for k, v in symbols.items(): text = text.replace(default_prompt_symbols[k], v) return text -# TODO: solution1 -> solution2 -> solution3 -# TODO: param(solution) optimzer.step(solution, "reward is 1, maximize1) -> solution 2 -# TODO: maybe have a trace.train() # simpler even than Algorithm, and cover 80% of use cases - class OptoPrimeV2(OptoPrime): - # TODO: LLM has the option to check the value of truncated one - # TODO: turn into a conversation round - # TODO: and show in a separate message - # TODO: 3. Compact representation (compress function) - # TODO: batchify, list of inputs, output is a list of inputs - # TODO: information is redundant - # TODO: idea 1: for each operator, we can identify repeated structure - # TODO: idea 2: for each bundle/op, the user can pass in a callable function, take original output, return a string - # TODO: idea 2-2: each node has a string representation of data, that's what the optimizer should use (this string is fixed) - # This is generic representation prompt, which just explains how to read the problem. representation_prompt = dedent( """ @@ -424,19 +558,11 @@ def __init__( self.example_problem_summary.inputs = {'b': (1, None), 'c': (5, None)} self.example_problem = self.problem_instance(self.example_problem_summary) - self.example_response = dedent( - f""" - <{self.optimizer_prompt_symbol_set.reasoning_tag}> - In this case, the desired response would be to change the value of input a to 14, as that would make the code return 10. - - - <{self.optimizer_prompt_symbol_set.improved_variable_tag}> - <{self.optimizer_prompt_symbol_set.name_tag}>a - <{self.optimizer_prompt_symbol_set.value_tag}> - 10 - - - """ + self.example_response = self.optimizer_prompt_symbol_set.example_output( + reasoning="In this case, the desired response would be to change the value of input a to 14, as that would make the code return 10.", + variables={ + 'a': 10, + } ) self.include_example = include_example @@ -473,17 +599,7 @@ def initialize_prompt(self): others_section_title=self.optimizer_prompt_symbol_set.others_section_title.replace(" ", "") ) self.output_format_prompt = self.output_format_prompt_template.format( - output_format=dedent(f""" - <{self.optimizer_prompt_symbol_set.reasoning_tag}> - reasoning - - <{self.optimizer_prompt_symbol_set.improved_variable_tag}> - <{self.optimizer_prompt_symbol_set.name_tag}>variable_name - <{self.optimizer_prompt_symbol_set.value_tag}> - value - - - """), + output_format=self.optimizer_prompt_symbol_set.output_format, reasoning_tag=self.optimizer_prompt_symbol_set.reasoning_tag, improved_variable_tag=self.optimizer_prompt_symbol_set.improved_variable_tag, instruction_section_title=self.optimizer_prompt_symbol_set.instruction_section_title.replace(" ", ""), @@ -595,37 +711,37 @@ def problem_instance(self, summary, mask=None): instruction=self.objective if "#Instruction" not in mask else "", code=( "\n".join([v for k, v in sorted(summary.graph)]) - if "#Code" not in mask + if self.optimizer_prompt_symbol_set.inputs_section_title not in mask else "" ), documentation=( "\n".join([f"[{k}] {v}" for k, v in summary.documentation.items()]) - if "#Documentation" not in mask + if self.optimizer_prompt_symbol_set.documentation_section_title not in mask else "" ), variables=( self.repr_node_value_compact(summary.variables, node_tag=self.optimizer_prompt_symbol_set.variable_tag, value_tag=self.optimizer_prompt_symbol_set.value_tag, constraint_tag=self.optimizer_prompt_symbol_set.constraint_tag) - if "#Variables" not in mask + if self.optimizer_prompt_symbol_set.variables_section_title not in mask else "" ), inputs=( self.repr_node_value_compact(summary.inputs, node_tag=self.optimizer_prompt_symbol_set.node_tag, value_tag=self.optimizer_prompt_symbol_set.value_tag, - constraint_tag=self.optimizer_prompt_symbol_set.constraint_tag) if "#Inputs" not in mask else "" + constraint_tag=self.optimizer_prompt_symbol_set.constraint_tag) if self.optimizer_prompt_symbol_set.inputs_section_title not in mask else "" ), outputs=( self.repr_node_value_compact(summary.output, node_tag=self.optimizer_prompt_symbol_set.node_tag, value_tag=self.optimizer_prompt_symbol_set.value_tag, - constraint_tag=self.optimizer_prompt_symbol_set.constraint_tag) if "#Outputs" not in mask else "" + constraint_tag=self.optimizer_prompt_symbol_set.constraint_tag) if self.optimizer_prompt_symbol_set.outputs_section_title not in mask else "" ), others=( self.repr_node_value_compact(summary.others, node_tag=self.optimizer_prompt_symbol_set.node_tag, value_tag=self.optimizer_prompt_symbol_set.value_tag, - constraint_tag=self.optimizer_prompt_symbol_set.constraint_tag) if "#Others" not in mask else "" + constraint_tag=self.optimizer_prompt_symbol_set.constraint_tag) if self.optimizer_prompt_symbol_set.others_section_title not in mask else "" ), - feedback=summary.user_feedback if "#Feedback" not in mask else "", + feedback=summary.user_feedback if self.optimizer_prompt_symbol_set.feedback_section_title not in mask else "", optimizer_prompt_symbol_set=self.optimizer_prompt_symbol_set ) @@ -636,9 +752,6 @@ def _step( summary = self.summarize() system_prompt, user_prompt = self.construct_prompt(summary, mask=mask) - system_prompt = self.replace_symbols(system_prompt, self.prompt_symbols) - user_prompt = self.replace_symbols(user_prompt, self.prompt_symbols) - response = self.call_llm( system_prompt=system_prompt, user_prompt=user_prompt, @@ -670,7 +783,6 @@ def _step( def extract_llm_suggestion(self, response: str): """Extract the suggestion from the response.""" - # suggestion = extract_xml_like_data(response) suggestion = self.optimizer_prompt_symbol_set.output_response_extractor(response) if len(suggestion) == 0: @@ -678,15 +790,6 @@ def extract_llm_suggestion(self, response: str): print("Cannot extract suggestion from LLM's response:") print(response) - # if the suggested value is a code, and the entire code body is empty (i.e., not even function signature is present) - # then we remove such suggestion - keys_to_remove = [] - for key, value in suggestion.items(): - if "__code" in key and value.strip() == "": - keys_to_remove.append(key) - for key in keys_to_remove: - del suggestion[key] - return suggestion def call_llm( From 81f0950bf3650d1732bab607b943ed22ba752be4 Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 16 Jul 2025 11:44:22 -0400 Subject: [PATCH 122/314] add enforce_json flag --- opto/optimizers/optoprime_v2.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index bbdeaba4..e4becad2 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -159,6 +159,8 @@ class OptimizerPromptSymbolSet: improved_variable_tag = "variable" name_tag = "name" + expect_json = False # this will stop `enforce_json` arguments passed to LLM calls + # custom output format # if this is not None, then the user needs to implement the following functions: # - output_response_extractor @@ -254,6 +256,8 @@ def default_prompt_symbols(self) -> Dict[str, str]: class OptimizerPromptSymbolSetJSON(OptimizerPromptSymbolSet): """We enforce a JSON output format extraction""" + expect_json = True + custom_output_format_instruction = """ {{ "reasoning": , @@ -533,9 +537,11 @@ def __init__( log=True, initial_var_char_limit=100, optimizer_prompt_symbol_set: OptimizerPromptSymbolSet = OptimizerPromptSymbolSet(), + use_json_object_format=True, # whether to use json object format for the response when calling LLM **kwargs, ): super().__init__(parameters, *args, propagator=propagator, **kwargs) + self.use_json_object_format = use_json_object_format if optimizer_prompt_symbol_set.expect_json and use_json_object_format else False self.ignore_extraction_error = ignore_extraction_error self.llm = llm or LLM() self.objective = objective or self.default_objective.format(value_tag=optimizer_prompt_symbol_set.value_tag, @@ -808,7 +814,9 @@ def call_llm( {"role": "user", "content": user_prompt}, ] - response = self.llm(messages=messages, max_tokens=max_tokens) + response_format = {"type": "json_object"} if self.use_json_object_format else None + + response = self.llm(messages=messages, max_tokens=max_tokens, response_format=response_format) response = response.choices[0].message.content From 3605dc0ba2a6e0bd0ffe7e4346d9723af29ea15f Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 16 Jul 2025 11:51:55 -0400 Subject: [PATCH 123/314] moved truncate_expressions outside --- opto/optimizers/optoprime_v2.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index e4becad2..d9bbca5b 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -432,6 +432,12 @@ def replace_symbols(self, text: str, symbols: Dict[str, str]) -> str: text = text.replace(default_prompt_symbols[k], v) return text +def truncate_expression(value, limit): + # https://stackoverflow.com/questions/1436703/what-is-the-difference-between-str-and-repr + value = str(value) + if len(value) > limit: + return value[:limit] + "...(skipped due to length limit)" + return value class OptoPrimeV2(OptoPrime): # This is generic representation prompt, which just explains how to read the problem. @@ -538,9 +544,13 @@ def __init__( initial_var_char_limit=100, optimizer_prompt_symbol_set: OptimizerPromptSymbolSet = OptimizerPromptSymbolSet(), use_json_object_format=True, # whether to use json object format for the response when calling LLM + truncate_expression=truncate_expression, **kwargs, ): super().__init__(parameters, *args, propagator=propagator, **kwargs) + + self.truncate_expression = truncate_expression + self.use_json_object_format = use_json_object_format if optimizer_prompt_symbol_set.expect_json and use_json_object_format else False self.ignore_extraction_error = ignore_extraction_error self.llm = llm or LLM() @@ -655,13 +665,6 @@ def repr_node_value_compact(self, node_dict, node_tag="node", f"<{node_tag} name=\"{k}\" type=\"code\">\n<{value_tag}>\n{signature}{node_value}\n\n{constraint_expr}\n\n") return "\n".join(temp_list) - def truncate_expression(self, value, limit): - # https://stackoverflow.com/questions/1436703/what-is-the-difference-between-str-and-repr - value = str(value) - if len(value) > limit: - return value[:limit] + "...(skipped due to length limit)" - return value - def construct_prompt(self, summary, mask=None, *args, **kwargs): """Construct the system and user prompt.""" system_prompt = ( From 46c9b411e450c5e617392f23484b31645f0ab245 Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 16 Jul 2025 11:57:39 -0400 Subject: [PATCH 124/314] moved helper function to utils --- opto/optimizers/optoprime_v2.py | 118 +------------------------------ opto/optimizers/utils.py | 119 ++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 117 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index d9bbca5b..db651bfb 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -3,6 +3,7 @@ from dataclasses import dataclass, asdict from opto.optimizers.optoprime import OptoPrime, FunctionFeedback from opto.trace.utils import dedent +from opto.optimizers.utils import truncate_expression, extract_xml_like_data from opto.trace.nodes import ParameterNode, Node, MessageNode from opto.trace.propagators import TraceGraph, GraphPropagator @@ -16,117 +17,6 @@ from typing import Dict, Any -def extract_top_level_blocks(text: str, tag: str): - """Extract all top-level ... blocks from text.""" - blocks = [] - start_tag = f'<{tag}>' - end_tag = f'' - stack = [] - start = None - i = 0 - while i < len(text): - if text.startswith(start_tag, i): - if not stack: - start = i + len(start_tag) - stack.append(i) - i += len(start_tag) - elif text.startswith(end_tag, i): - if stack: - stack.pop() - if not stack and start is not None: - blocks.append(text[start:i]) - start = None - i += len(end_tag) - else: - i += 1 - return blocks - - -def extract_first_top_level_block(text: str, tag: str): - blocks = extract_top_level_blocks(text, tag) - return blocks[0] if blocks else None - - -def strip_nested_blocks(text: str, tag: str) -> str: - """Remove all nested ... blocks from text, leaving only the top-level text.""" - result = '' - start_tag = f'<{tag}>' - end_tag = f'' - stack = [] - i = 0 - last = 0 - while i < len(text): - if text.startswith(start_tag, i): - if not stack: - result += text[last:i] - stack.append(i) - i += len(start_tag) - elif text.startswith(end_tag, i): - if stack: - stack.pop() - if not stack: - last = i + len(end_tag) - i += len(end_tag) - else: - i += 1 - if not stack: - result += text[last:] - return result.strip() - - -def extract_reasoning_and_remainder(text: str, tag: str = "reasoning"): - """Extract reasoning and the remainder of the text after reasoning block (if closed). Strip whitespace only if properly closed.""" - start_tag = f'<{tag}>' - end_tag = f'' - start = text.find(start_tag) - if start == -1: - return '', text - start += len(start_tag) - end = text.find(end_tag, start) - if end == -1: - # If not properly closed, don't strip whitespace to preserve original formatting - return text[start:], '' - return text[start:end].strip(), text[end + len(end_tag):] - - -def extract_xml_like_data(text: str, reasoning_tag: str = "reasoning", - improved_variable_tag: str = "variable", - name_tag: str = "name", - value_tag: str = "value") -> Dict[str, Any]: - """ - Extract thinking content and improved variables from text containing XML-like tags. - - Args: - text (str): Text containing and tags - - Returns: - Dict containing: - - 'reasoning': content of element - - 'variables': dict mapping variable names to their values - """ - result = { - 'reasoning': '', - 'variables': {} - } - - # Extract reasoning and the remainder of the text - reasoning, remainder = extract_reasoning_and_remainder(text, reasoning_tag) - result['reasoning'] = reasoning - - # Only parse variables from the remainder (i.e., after a closed reasoning tag) - variable_blocks = extract_top_level_blocks(remainder, improved_variable_tag) - for var_block in variable_blocks: - name_block = extract_first_top_level_block(var_block, name_tag) - value_block = extract_first_top_level_block(var_block, value_tag) - # Only add if both name and value tags are present and name is non-empty after stripping - if name_block is not None and value_block is not None: - var_name = name_block.strip() - var_value = value_block.strip() if value_block is not None else '' - if var_name: # Only require name to be non-empty, value can be empty - result['variables'][var_name] = var_value - return result - - class OptimizerPromptSymbolSet: """ By inheriting this class and pass into the optimizer. People can change the optimizer documentation @@ -432,12 +322,6 @@ def replace_symbols(self, text: str, symbols: Dict[str, str]) -> str: text = text.replace(default_prompt_symbols[k], v) return text -def truncate_expression(value, limit): - # https://stackoverflow.com/questions/1436703/what-is-the-difference-between-str-and-repr - value = str(value) - if len(value) > limit: - return value[:limit] + "...(skipped due to length limit)" - return value class OptoPrimeV2(OptoPrime): # This is generic representation prompt, which just explains how to read the problem. diff --git a/opto/optimizers/utils.py b/opto/optimizers/utils.py index 2f076f54..711b81aa 100644 --- a/opto/optimizers/utils.py +++ b/opto/optimizers/utils.py @@ -13,3 +13,122 @@ def print_color(message, color=None, logger=None): if logger is not None: logger.log(message) + + +def truncate_expression(value, limit): + # https://stackoverflow.com/questions/1436703/what-is-the-difference-between-str-and-repr + value = str(value) + if len(value) > limit: + return value[:limit] + "...(skipped due to length limit)" + return value + + +def extract_top_level_blocks(text: str, tag: str): + """Extract all top-level ... blocks from text.""" + blocks = [] + start_tag = f'<{tag}>' + end_tag = f'' + stack = [] + start = None + i = 0 + while i < len(text): + if text.startswith(start_tag, i): + if not stack: + start = i + len(start_tag) + stack.append(i) + i += len(start_tag) + elif text.startswith(end_tag, i): + if stack: + stack.pop() + if not stack and start is not None: + blocks.append(text[start:i]) + start = None + i += len(end_tag) + else: + i += 1 + return blocks + + +def extract_first_top_level_block(text: str, tag: str): + blocks = extract_top_level_blocks(text, tag) + return blocks[0] if blocks else None + + +def strip_nested_blocks(text: str, tag: str) -> str: + """Remove all nested ... blocks from text, leaving only the top-level text.""" + result = '' + start_tag = f'<{tag}>' + end_tag = f'' + stack = [] + i = 0 + last = 0 + while i < len(text): + if text.startswith(start_tag, i): + if not stack: + result += text[last:i] + stack.append(i) + i += len(start_tag) + elif text.startswith(end_tag, i): + if stack: + stack.pop() + if not stack: + last = i + len(end_tag) + i += len(end_tag) + else: + i += 1 + if not stack: + result += text[last:] + return result.strip() + + +def extract_reasoning_and_remainder(text: str, tag: str = "reasoning"): + """Extract reasoning and the remainder of the text after reasoning block (if closed). Strip whitespace only if properly closed.""" + start_tag = f'<{tag}>' + end_tag = f'' + start = text.find(start_tag) + if start == -1: + return '', text + start += len(start_tag) + end = text.find(end_tag, start) + if end == -1: + # If not properly closed, don't strip whitespace to preserve original formatting + return text[start:], '' + return text[start:end].strip(), text[end + len(end_tag):] + + +def extract_xml_like_data(text: str, reasoning_tag: str = "reasoning", + improved_variable_tag: str = "variable", + name_tag: str = "name", + value_tag: str = "value") -> Dict[str, Any]: + """ + Extract thinking content and improved variables from text containing XML-like tags. + + Args: + text (str): Text containing and tags + + Returns: + Dict containing: + - 'reasoning': content of element + - 'variables': dict mapping variable names to their values + """ + result = { + 'reasoning': '', + 'variables': {} + } + + # Extract reasoning and the remainder of the text + reasoning, remainder = extract_reasoning_and_remainder(text, reasoning_tag) + result['reasoning'] = reasoning + + # Only parse variables from the remainder (i.e., after a closed reasoning tag) + variable_blocks = extract_top_level_blocks(remainder, improved_variable_tag) + for var_block in variable_blocks: + name_block = extract_first_top_level_block(var_block, name_tag) + value_block = extract_first_top_level_block(var_block, value_tag) + # Only add if both name and value tags are present and name is non-empty after stripping + if name_block is not None and value_block is not None: + var_name = name_block.strip() + var_value = value_block.strip() if value_block is not None else '' + if var_name: # Only require name to be non-empty, value can be empty + result['variables'][var_name] = var_value + return result From 14146fe7f7bccb32913b4ee61bfee143ac33fe19 Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 16 Jul 2025 12:22:16 -0400 Subject: [PATCH 125/314] fix typing import error --- opto/optimizers/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/opto/optimizers/utils.py b/opto/optimizers/utils.py index 711b81aa..13a5ad01 100644 --- a/opto/optimizers/utils.py +++ b/opto/optimizers/utils.py @@ -1,3 +1,5 @@ +from typing import Dict, Any + def print_color(message, color=None, logger=None): colors = { "red": "\033[91m", From 8e744aa7010fc883bc99643c5995b65163f942b4 Mon Sep 17 00:00:00 2001 From: chinganc Date: Wed, 16 Jul 2025 21:37:54 +0000 Subject: [PATCH 126/314] Bring back ucb from experimental. --- opto/trainer/algorithms/UCBsearch.py | 374 +++++++++++++++++++++++++++ 1 file changed, 374 insertions(+) create mode 100644 opto/trainer/algorithms/UCBsearch.py diff --git a/opto/trainer/algorithms/UCBsearch.py b/opto/trainer/algorithms/UCBsearch.py new file mode 100644 index 00000000..9ff6f61b --- /dev/null +++ b/opto/trainer/algorithms/UCBsearch.py @@ -0,0 +1,374 @@ +import numpy as np +import copy +import math +from collections import deque +from typing import Union, List, Tuple, Dict, Any, Optional +from opto import trace +from opto.trainer.utils import async_run # Assuming print_color is in utils +from opto.optimizers.utils import print_color +from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm, evaluate, batchify # evaluate and batchify might be useful + +class UCBSearchAlgorithm(MinibatchAlgorithm): + """ + UCB Search Algorithm. + + Keeps a buffer of candidates with their statistics (score sum, evaluation count). + In each iteration: + 1. Picks a candidate 'a' from the buffer with the highest UCB score. + 2. Updates the optimizer with 'a's parameters. + 3. Draws a minibatch from the training set, performs a forward/backward pass, and calls optimizer.step() to get a new candidate 'a''. + 4. Evaluates 'a'' on a validation set minibatch. + 5. Updates statistics of 'a' (based on the training minibatch). + 6. Adds 'a'' (with its validation stats) to the buffer. + 7. If the buffer is full, evicts the candidate with the lowest UCB score. + """ + + def __init__(self, + agent: trace.Module, + optimizer, + max_buffer_size: int = 10, + ucb_exploration_factor: float = 1.0, # Controls exploration vs exploitation tradeoff in UCB selection + # UCB formula: μ(a) + c * sqrt(ln(t) / n(a)), c is the exploration factor + logger=None, + num_threads: int = None, + *args, + **kwargs): + super().__init__(agent, optimizer, num_threads=num_threads, logger=logger, *args, **kwargs) + + self.buffer = deque(maxlen=max_buffer_size) + self.max_buffer_size = max_buffer_size + # UCB exploration factor: Higher values encourage more exploration of less-tested candidates, + # lower values favor exploitation of well-performing candidates. + self.ucb_exploration_factor = ucb_exploration_factor + + # To ensure optimizer_step can be called with bypassing=True if needed. + # This depends on the specific optimizer's implementation. + # For now, we assume the optimizer has a step method that can return parameters. + if not hasattr(self.optimizer, 'step'): + raise ValueError("Optimizer must have a 'step' method.") + + self._total_evaluations_tracker = 0 # Tracks total number of individual candidate evaluations used in UCB calculation for log(T) + self._candidate_id_counter = 0 + + def _sample_minibatch(self, dataset: Dict[str, List[Any]], batch_size: int) -> Tuple[List[Any], List[Any]]: + """Sample a minibatch from the dataset.""" + if not dataset or not dataset.get('inputs') or not dataset.get('infos'): + print_color("Warning: Attempted to sample from an empty or malformed dataset.", color='yellow') + return [], [] + + dataset_size = len(dataset['inputs']) + if dataset_size == 0: + print_color("Warning: Dataset is empty, cannot sample minibatch.", color='yellow') + return [], [] + + actual_batch_size = min(batch_size, dataset_size) + indices = np.random.choice(dataset_size, actual_batch_size, replace=False) + xs = [dataset['inputs'][i] for i in indices] + infos = [dataset['infos'][i] for i in indices] + return xs, infos + + def _evaluate_candidate(self, + params_to_eval_dict: Dict[str, Any], + dataset: Dict[str, List[Any]], # Changed from validate_dataset + guide, # Changed from validate_guide + evaluation_batch_size: int, # New parameter name + num_threads: Optional[int] = None + ) -> Tuple[float, int]: + """Evaluates a given set of parameters on samples from the provided dataset (now typically train_dataset).""" + if not dataset or not dataset.get('inputs') or not dataset.get('infos') or not dataset['inputs']: + print_color("Evaluation dataset is empty or invalid. Returning score -inf, count 0.", color='yellow') + return -np.inf, 0 + + original_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + self.optimizer.update(params_to_eval_dict) + + eval_xs, eval_infos = self._sample_minibatch(dataset, evaluation_batch_size) # Use evaluation_batch_size + + if not eval_xs: + print_color("Evaluation minibatch is empty. Returning score -inf, count 0.", color='yellow') + self.optimizer.update(original_params) + return -np.inf, 0 + + eval_scores = evaluate(self.agent, + guide, # Use main guide + eval_xs, + eval_infos, + min_score=self.min_score if hasattr(self, 'min_score') else None, + num_threads=num_threads or self.num_threads, + description=f"Evaluating candidate") + + self.optimizer.update(original_params) + + avg_score = np.mean(eval_scores) if eval_scores and all(s is not None for s in eval_scores) else -np.inf + eval_count = len(eval_xs) + + return float(avg_score), eval_count + + def _calculate_ucb(self, candidate_buffer_entry: Dict, total_tracked_evaluations: int) -> float: + """Calculates UCB score for a candidate in the buffer.""" + if candidate_buffer_entry['eval_count'] == 0: + return float('inf') # Explore unvisited states first + + mean_score = candidate_buffer_entry['score_sum'] / candidate_buffer_entry['eval_count'] + + # Add 1 to total_tracked_evaluations to prevent log(0) if it's the first evaluation overall + # and to ensure log argument is > 0. + # Add 1 to eval_count in denominator as well to ensure it's robust if eval_count is small. + if total_tracked_evaluations == 0: # Should not happen if we init with one eval + total_tracked_evaluations = 1 + + # UCB exploration term: ucb_exploration_factor scales the confidence interval + # Higher factor = more exploration, lower factor = more exploitation + exploration_term = self.ucb_exploration_factor * \ + math.sqrt(math.log(total_tracked_evaluations) / candidate_buffer_entry['eval_count']) + + return mean_score + exploration_term + + def _update_buffer_ucb_scores(self): + """Recalculates and updates UCB scores for all candidates in the buffer.""" + if not self.buffer: + return + + for candidate_entry in self.buffer: + candidate_entry['ucb_score'] = self._calculate_ucb(candidate_entry, self._total_evaluations_tracker) + + def train(self, + guide, # Guide for train_dataset (feedback generation AND evaluation) + train_dataset: Dict[str, List[Any]], + *, + validation_dataset: Optional[Dict[str, List[Any]]] = None, # Validation set for evaluation, defaults to train_dataset + num_search_iterations: int = 100, + train_batch_size: int = 2, + evaluation_batch_size: int = 20, # Renamed from validation_batch_size, used for all explicit evaluations + eval_frequency: int = 1, + log_frequency: Optional[int] = None, + save_frequency: Optional[int] = None, + save_path: str = "checkpoints/ucb_agent.pkl", + min_score_for_agent_update: Optional[float] = None, # Renamed from min_score to avoid conflict with evaluate's min_score + verbose: Union[bool, str] = False, + num_threads: Optional[int] = None, + **kwargs + ) -> Tuple[Dict[str, Any], float]: # Returns metrics and best score + """ + Main training loop for UCB Search Algorithm. + """ + # Default validation_dataset to train_dataset if not provided + if validation_dataset is None: + validation_dataset = train_dataset + + num_threads = num_threads or self.num_threads + log_frequency = log_frequency or eval_frequency + self.min_score = min_score_for_agent_update # Used by parent's evaluate if called, or our own _evaluate_candidate + total_samples = 0 + + # Metrics tracking + metrics = { + 'best_candidate_scores': [], # Score of the best candidate (e.g., highest mean) found so far at each iteration + 'selected_action_ucb': [], # UCB score of the selected action 'a' + 'new_candidate_scores': [], # Score of the new candidate 'a_prime' + 'buffer_avg_score': [], + 'buffer_avg_evals': [], + } + +# 0. Evaluate the initial parameter on samples of the validation set and add it to the buffer. + print_color("Evaluating initial parameters using validation_dataset samples...", 'cyan') + initial_params_dict = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + initial_score, initial_evals = self._evaluate_candidate( + initial_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads # Use validation_dataset and guide + ) + self._total_evaluations_tracker += initial_evals + total_samples += initial_evals + + # Log initial evaluation + self.logger.log('Initial UCB score', initial_score, 0, color='blue') + self.logger.log('Initial evaluations', initial_evals, 0, color='cyan') + + initial_candidate_entry = { + 'params': initial_params_dict, + 'score_sum': initial_score * initial_evals if initial_score > -np.inf else 0, # Store sum for accurate mean later + 'eval_count': initial_evals, + 'ucb_score': None, # avoid accidental reads before it's initialized + 'iteration_created': 0 + } + self.buffer.append(initial_candidate_entry) + self._update_buffer_ucb_scores() # Update UCB for the initial candidate + print_color(f"Initial candidate: Score {initial_score:.4f}, Evals {initial_evals}", 'yellow') + + # Main search loop + for iteration in range(1, num_search_iterations + 1): + if not self.buffer: + print_color("Buffer is empty, stopping search.", 'red') + break + + # 1. Pick the candidate 'a' with the highest UCB from the buffer + self._update_buffer_ucb_scores() # Ensure UCB scores are fresh + action_candidate_a = self.select(self.buffer) + + # Log selected action UCB score + self.logger.log('Selected action UCB', action_candidate_a['ucb_score'], iteration, color='magenta') + self.logger.log('Selected action mean score', action_candidate_a['score_sum']/(action_candidate_a['eval_count'] or 1), iteration, color='cyan') + + print_color(f"Iter {iteration}/{num_search_iterations}: ", 'blue') + + + # 2. Load parameters of 'a' into the agent for the optimizer update step + self.optimizer.update(action_candidate_a['params']) + + # 3. Draw minibatch from the training set, do update from 'a' to get 'a_prime' + train_xs, train_infos = self._sample_minibatch(train_dataset, train_batch_size) + if not train_xs: + print_color(f"Iter {iteration}: Training minibatch empty, skipping optimizer step.", 'yellow') + continue + + # Perform forward pass and get feedback for agent parameters 'a' + outputs_for_a = [] + use_asyncio = self._use_asyncio(num_threads) + if use_asyncio: + outputs_for_a = async_run([self.forward]*len(train_xs), + [(self.agent, x, guide, info) for x, info in zip(train_xs, train_infos)], + max_workers=num_threads, + description=f"Iter {iteration}: Forward pass for action 'a' ") + else: + outputs_for_a = [self.forward(self.agent, x, guide, info) for x, info in zip(train_xs, train_infos)] + + scores_from_train, targets_from_train, feedbacks_from_train = [], [], [] + for target, score, feedback in outputs_for_a: + scores_from_train.append(score) + targets_from_train.append(target) + feedbacks_from_train.append(feedback) + + if not scores_from_train: # Should not happen if train_xs was not empty + print_color(f"Iter {iteration}: No outputs from forward pass for candidate 'a'. Skipping.", 'yellow') + continue + + target_for_a = batchify(*targets_from_train) + feedback_for_a = batchify(*feedbacks_from_train).data + score_for_a_on_train_batch = np.mean([s for s in scores_from_train if s is not None]) if any(s is not None for s in scores_from_train) else -np.inf + + self.optimizer.zero_feedback() + self.optimizer.backward(target_for_a, feedback_for_a) # Grads for 'a' are now in optimizer + + try: + a_prime_params_dict = self.optimizer.step(bypassing=True, verbose='output') + if not isinstance(a_prime_params_dict, dict) or not a_prime_params_dict: + print_color(f"Iter {iteration}: Optimizer.step did not return a valid param dict for a_prime. Using current agent params as a_prime.", 'yellow') + # Fallback: if step modified agent in-place and didn't return dict, current agent state is a_prime + a_prime_params_dict = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + + except Exception as e: + print_color(f"Iter {iteration}: Error during optimizer.step for a_prime: {e}. Skipping candidate generation.", 'red') + continue + + # 4. Evaluate 'a_prime' on samples of validation set + a_prime_score, a_prime_evals = self._evaluate_candidate( + a_prime_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads # Use validation_dataset and guide + ) + self._total_evaluations_tracker += a_prime_evals + total_samples += evaluation_batch_size + train_batch_size + metrics['new_candidate_scores'].append(a_prime_score) + + # Log new candidate performance + self.logger.log('New candidate score', a_prime_score, iteration, color='green') + self.logger.log('Training batch score', score_for_a_on_train_batch, iteration, color='yellow') + + print_color(f"Iter {iteration}: New candidate a_prime generated. Validation Score: {a_prime_score:.4f}, Evals: {a_prime_evals}", 'cyan') + + # 5. Update the stats of 'a' (action_candidate_a) based on the training batch experience + if score_for_a_on_train_batch > -np.inf: + action_candidate_a['score_sum'] += score_for_a_on_train_batch * len(train_xs) # score is often an average + action_candidate_a['eval_count'] += len(train_xs) # or 1 if score is total + self._total_evaluations_tracker += len(train_xs) # training batch also counts as evaluations for UCB total T + + # 6. Add 'a_prime' (with its validation stats) to the buffer + if a_prime_score > -np.inf and a_prime_evals > 0: + new_candidate_entry = { + 'params': a_prime_params_dict, + 'score_sum': a_prime_score * a_prime_evals, # Store sum + 'eval_count': a_prime_evals, + 'ucb_score': None, # avoid accidental reads before it's initializad + 'iteration_created': iteration + } + + # Eviction logic before adding if buffer is at max_len + if len(self.buffer) == self.max_buffer_size: + self._update_buffer_ucb_scores() # Ensure UCBs are current before eviction + candidate_to_evict = min(self.buffer, key=lambda c: c['ucb_score']) + self.buffer.remove(candidate_to_evict) + print_color(f"Iter {iteration}: Buffer full. Evicted a candidate (UCB: {candidate_to_evict['ucb_score']:.4f})", 'magenta') + + self.buffer.append(new_candidate_entry) + print_color(f"Iter {iteration}: Added new candidate to buffer.", 'magenta') + else: + print_color(f"Iter {iteration}: New candidate a_prime had invalid score/evals, not added to buffer.", 'yellow') + + # Update all UCB scores in the buffer after potential additions/removals/stat updates + self._update_buffer_ucb_scores() + + # Logging + best_in_buffer = max(self.buffer, key=lambda c: c['score_sum']/(c['eval_count'] or 1)) + metrics['best_candidate_scores'].append(best_in_buffer['score_sum']/(best_in_buffer['eval_count'] or 1)) + metrics['buffer_avg_score'].append(np.mean([c['score_sum']/(c['eval_count'] or 1) for c in self.buffer if c['eval_count'] > 0])) + metrics['buffer_avg_evals'].append(np.mean([c['eval_count'] for c in self.buffer])) + + if iteration % log_frequency == 0: + log_data = { + "iteration": iteration, + "best_score": metrics['best_candidate_scores'][-1], #best_candidate_score_in_buffer + "selected_action_ucb": action_candidate_a['ucb_score'], + "new_candidate_score": a_prime_score, + "buffer_size": len(self.buffer), + "buffer_avg_score": metrics['buffer_avg_score'][-1], + "buffer_avg_evals": metrics['buffer_avg_evals'][-1], + "total_evaluations_tracker": self._total_evaluations_tracker, + "total_samples": total_samples # Add new metric + } + + # Log all important metrics + self.logger.log('Best candidate score', log_data['best_score'], iteration, color='green') + self.logger.log('Buffer size', log_data['buffer_size'], iteration, color='blue') + self.logger.log('Buffer average score', log_data['buffer_avg_score'], iteration, color='cyan') + self.logger.log('Buffer average evaluations', log_data['buffer_avg_evals'], iteration, color='orange') + self.logger.log('Total evaluations tracker', log_data['total_evaluations_tracker'], iteration, color='magenta') + self.logger.log('Total samples processed', log_data['total_samples'], iteration, color='yellow') + + print_color(f"Log @ Iter {iteration}: Best score in buffer: {log_data['best_score']:.4f}, Buffer size: {log_data['buffer_size']}, Total samples: {total_samples}", 'green') + + # Save agent (e.g., the one with highest mean score in buffer) + if save_frequency is not None and iteration % save_frequency == 0: + best_overall_candidate = max(self.buffer, key=lambda c: c['score_sum'] / (c['eval_count'] or 1E-9) ) + self.optimizer.update(best_overall_candidate['params']) # Load params using optimizer + self.save_agent(save_path, iteration) # save_agent is from AlgorithmBase + print_color(f"Iter {iteration}: Saved agent based on best candidate in buffer.", 'green') + + # End of search loop + print_color("UCB search finished.", 'blue') + + # Log final training summary + final_iteration = num_search_iterations + self.logger.log('UCB search completed', final_iteration, final_iteration, color='blue') + self.logger.log('Final total samples', total_samples, final_iteration, color='magenta') + + if not self.buffer: + print_color("Buffer is empty at the end of search. No best candidate found.", 'red') + self.logger.log('Final status', 'Buffer empty - no best candidate', final_iteration, color='red') + return metrics, -np.inf + + # Select the best candidate based on highest mean score (exploitation) + final_best_candidate = max(self.buffer, key=lambda c: c['score_sum'] / (c['eval_count'] or 1E-9)) + final_best_score = final_best_candidate['score_sum'] / (final_best_candidate['eval_count'] or 1E-9) + + # Log final results + self.logger.log('Final best score', final_best_score, final_iteration, color='green') + self.logger.log('Final best candidate evaluations', final_best_candidate['eval_count'], final_iteration, color='cyan') + self.logger.log('Final buffer size', len(self.buffer), final_iteration, color='blue') + + print_color(f"Final best candidate: Mean Score {final_best_score:.4f}, Evals {final_best_candidate['eval_count']}", 'green') + + # Load best parameters into the agent + self.optimizer.update(final_best_candidate['params']) # Load params using optimizer + + return metrics, float(final_best_score) + + def select(self, buffer): + '''Could be subclassed to implement different selection strategies''' + return max(buffer, key=lambda c: c['ucb_score']) \ No newline at end of file From 7ecb1ad54d8b5e9d3ba2bb302d744d652ccc35ad Mon Sep 17 00:00:00 2001 From: chinganc Date: Wed, 16 Jul 2025 23:06:18 +0000 Subject: [PATCH 127/314] Fix the bug of validate. --- opto/trainer/algorithms/search_algorithms.py | 69 +++++++++++++------- 1 file changed, 45 insertions(+), 24 deletions(-) diff --git a/opto/trainer/algorithms/search_algorithms.py b/opto/trainer/algorithms/search_algorithms.py index fd8eb1bc..79eca888 100644 --- a/opto/trainer/algorithms/search_algorithms.py +++ b/opto/trainer/algorithms/search_algorithms.py @@ -67,14 +67,14 @@ def remap_update_dict(base_module, update_dict): """ parameters = base_module.parameters() # get the parameters of the base agent remapped_update_dict = {} - for k, v in update_dict.items(): - for p in parameters: - # Check if k is a copy of p or p is a copy of k + # if fill_missing: + # remap all keys of the base_module's parameters and those in update_dict will be filled with their values in update_dict + for p in parameters: + remapped_update_dict[p] = p.data + for k, v in update_dict.items(): if is_node_copy(k, p): - k = p # remap k to the original parameter - remapped_update_dict[k] = v # set the value in the remapped update dict - break # stop checking once we've found a match - # remapped_update_dict is empty if no keys in update_dict matched any parameters of the base_module + remapped_update_dict[p] = v + break # stop checking once we've found a match return remapped_update_dict def set_module_parameters(agent, update_dict): @@ -326,12 +326,15 @@ def __init__(self, assert isinstance(base_module, trace.Module), "base_module must be a trace.Module." self.base_module = base_module self.update_dict = update_dict if update_dict is not None else {} + self.update_dict = remap_update_dict(self.base_module, self.update_dict) self.rollouts = [] # list of dicts containing the rollout information (not RolloutsGraph, but a list of dicts) def get_module(self): - """ Apply the update_dict to the base_module and return the updated module. This will not update the base_module itself.""" - module = create_module_from_update_dict(self.base_module, self.update_dict) if self.update_dict else copy.deepcopy(self.base_module) - module._ModuleCandidate_candidate_id = id(self) # set the id of the module to the id of the candidate; this is used to identify the candidate in the priority queue + """ Apply the update_dict to the base_module and return the updated module. + A new module is always created so the base_module is not modified. + The new module has a new attribute _module_candidate which is this candidate.""" + module = create_module_from_update_dict(self.base_module, self.update_dict) if self.update_dict else copy.deepcopy(self.base_module) # + setattr(module, '__TRACE_RESERVED_module_candidate_id', id(self)) return module # return the updated module def apply_update(self, base_module=None): @@ -345,7 +348,7 @@ def __deepcopy__(self, memo): memo[id(self)] = result for k, v in self.__dict__.items(): if k != 'base_module': - setattr(result, k, deepcopy(v, memo)) + setattr(result, k, copy.deepcopy(v, memo)) else: setattr(result, k, v) # base_module is not copied, it is the original module return result @@ -353,11 +356,11 @@ def __deepcopy__(self, memo): def __eq__(self, other): """ Check if two candidates are equal based on their base_module and update_dict. """ assert isinstance(other, ModuleCandidate), "other must be an instance of ModuleCandidate." - if self.base_module != other.base_module: - return False - update_dict_self = remap_update_dict(self.base_module, self.update_dict) - update_dict_other = remap_update_dict(other.base_module, other.update_dict) - return update_dict_self == update_dict_other + return self.update_dict == other.update_dict + + def __hash__(self): + """ Hash the candidate based on its update_dict. """ + return hash(frozenset(self.update_dict.items())) def add_rollouts(self, rollouts: List[Dict[str, Any]]): """ Add rollouts to the candidate. """ @@ -551,18 +554,36 @@ def validate(self, candidates, samples, verbose=False, **kwargs): # In validate_samples, there may be multiple rollouts collected by the same agent (or their copies). # We need to group the rollouts by the agent (ModuleCandidate) and return a dictionary where the keys are the ModuleCandidate objects and the values are lists of rollouts (list of dicts). - results = {} # dict of ModuleCandidate: list of rollouts (list of dicts) - for c in candidates + exploration_candidates: - # Initialize the candidate in the results dictionary - results[id(c)] = (c, []) # (ModuleCandidate, list of rollouts) + # Group the samples by the ModuleCandidate id + _results = {} # dict of ModuleCandidate: list of rollouts (list of dicts) + for c in exploration_candidates + candidates: + _results[id(c)] = [] for rollouts in validate_samples.samples: module = rollouts.module # trace.Module - key = module._ModuleCandidate_candidate_id # use the candidate id as the key - if key not in results: + key = getattr(module, '__TRACE_RESERVED_module_candidate_id') # use the candidate as the key + if key not in _results: raise ValueError(f"ModuleCandidate with id {key} not found in results. Samples are not collected by known candidates.") # Append the rollouts to the list of rollouts for the key - results[key][1].extend(rollouts.to_list()) + _results[key].extend(rollouts.to_list()) + + # Merge rollouts of ModuleCandidates sharing the same parameters + results = {} # dict of ModuleCandidate id: (ModuleCandidate, list of rollouts) + for c in exploration_candidates + candidates: + rollouts_list = _results[id(c)] + matched = False + for k in results.keys(): + if k == c: + matched = True + if id(k) != id(c): # merging rollouts of candidates with the same parameters + rollouts_list += c.rollouts + results[k].extend(rollouts_list) # add the rollouts to the candidate + break + if not matched: # key not found in results + results[c] = rollouts_list # add the rollouts to the candidate + + # NOTE what if propose creates multiple exploration_candidates that have the same parameters and the same rollouts stats? + # For example, it copies candidates. This would create a bug. return results @@ -573,7 +594,7 @@ def update_memory(self, validate_results, **kwargs): validate_results (dict): A dictionary where the keys are ModuleCandidate objects and the values are lists of rollouts (list of dicts) containing the module, x, info, target, score, feedback. **kwargs: Additional keyword arguments that may be used by the implementation. """ - for candidate_id, (candidate, rollouts) in validate_results.items(): + for candidate, rollouts in validate_results.items(): candidate.add_rollouts(rollouts) # add the rollouts to the candidate score = self.compute_score(candidate) # compute the score for the candidate heapq.heappush(self.memory, (-score, candidate)) # add the candidate to the priority queue From 427f3acc18fa57add423ab342e8b86abc204b334 Mon Sep 17 00:00:00 2001 From: Xuanfei Ren Date: Wed, 16 Jul 2025 20:52:40 -0500 Subject: [PATCH 128/314] Fix an issue about missing parameters in the proposed update_dict --- opto/trainer/algorithms/search_algorithms.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/opto/trainer/algorithms/search_algorithms.py b/opto/trainer/algorithms/search_algorithms.py index 36d71730..61716d79 100644 --- a/opto/trainer/algorithms/search_algorithms.py +++ b/opto/trainer/algorithms/search_algorithms.py @@ -67,12 +67,9 @@ def remap_update_dict(base_module, update_dict): """ parameters = base_module.parameters() # get the parameters of the base agent remapped_update_dict = {} - # if fill_missing: - # remap all keys of the base_module's parameters and those in update_dict will be filled with their values in update_dict - for p in parameters: - remapped_update_dict[p] = p.data - for k, v in update_dict.items(): - if is_node_copy(k, p): + for k, v in update_dict.items(): + for p in parameters: + if is_node_copy(k, p): # Check if k is a copy of p or p is a copy of k remapped_update_dict[p] = v break # stop checking once we've found a match return remapped_update_dict @@ -497,6 +494,10 @@ def _step(n, verbose=False, num_threads=None, **kwargs): optimizer.zero_feedback() # reset the optimizer's feedback optimizer.backward(target, feedback) # compute the gradients based on the targets and feedbacks update_dict = optimizer.step(verbose=verbose, num_threads=num_threads, bypassing=True, **kwargs) + # update_dict may only contain some of the parameters of the agent, we need to make sure it contains all the parameters + for param in optimizer.parameters: # for all parameters + if param not in update_dict: # update_dict misses some parameters + update_dict[param] = param.data # add the parameter to the update_dict # the update_dict is linked to the copied parameters of the agent, we set it back to the agent's parameters update_dict = remap_update_dict(self.agent, update_dict) # remap the update dict to the agent's parameters return update_dict # return the proposed parameters From cd917bd6e392a4a94cf9a2d5f982c9920b0a3994 Mon Sep 17 00:00:00 2001 From: Xuanfei Ren Date: Wed, 16 Jul 2025 21:48:04 -0500 Subject: [PATCH 129/314] Revert "add ucb reward" This reverts commit 8d47177f5b1047504068b5bd1ce8ec92886cc7be. --- examples/gsm8k_search_algo.py | 15 ++++--- opto/trainer/algorithms/search_algorithms.py | 45 -------------------- 2 files changed, 8 insertions(+), 52 deletions(-) diff --git a/examples/gsm8k_search_algo.py b/examples/gsm8k_search_algo.py index 1142e586..1243cd8f 100644 --- a/examples/gsm8k_search_algo.py +++ b/examples/gsm8k_search_algo.py @@ -3,8 +3,8 @@ from opto import trace from opto.utils.llm import LLM, LiteLLM from opto.optimizers import OptoPrime -from opto.trainer.algorithms.search_algorithms import UCBSearch as SearchAlgorithm -from opto.trainer.loggers import WandbLogger +from opto.trainer.algorithms.search_algorithms import PrioritySearch as SearchAlgorithm +from opto.trainer.loggers import TensorboardLogger from opto.trainer.guide import VerbalJudgeGuide from typing import Any @@ -47,7 +47,8 @@ def forward(self, message: Any) -> Any: Guide = VerbalJudgeGuide -Logger = WandbLogger +Logger = TensorboardLogger + def main(): # set seed @@ -61,9 +62,9 @@ def main(): num_threads = 10 datasize = 5 verbose = True - teacher_model = "vertex_ai/gemini-2.0-flash" # use default model - student_model = "vertex_ai/gemini-2.0-flash" # use default model - optimizer_model = "vertex_ai/gemini-2.0-flash" # use default model + teacher_model = None # use default model + student_model = None # use default model + optimizer_model = None # use default model np.random.seed(seed) @@ -76,7 +77,7 @@ def main(): agent = Learner(llm=LLM(student_model)) guide = Guide(llm=LLM(teacher_model)) optimizer = OptoPrime(agent.parameters(), llm=LLM(optimizer_model)) - logger = Logger(project="gsm8k-examples", name="ucb",verbose=verbose) + logger = Logger(verbose=verbose) # set use_json_object_format=False if LLM does not support JSON object format alg = SearchAlgorithm( diff --git a/opto/trainer/algorithms/search_algorithms.py b/opto/trainer/algorithms/search_algorithms.py index 61716d79..02653c12 100644 --- a/opto/trainer/algorithms/search_algorithms.py +++ b/opto/trainer/algorithms/search_algorithms.py @@ -657,48 +657,3 @@ def compute_score(self, candidate): default_score = self.default_score if self.default_score is not None else self.score_range[1] # default score for the candidates return np.mean(scores) if scores else self.default_score - -class UCBSearch(PrioritySearch): - """A search algorithm that keeps a buffer with candidates and their UCB scores. It does exploration according to the UCB score.""" - - def __init__(self, *args, exploration_constant=1.0, **kwargs): - """Initialize UCBSearch with an exploration constant for the UCB formula.""" - super().__init__(*args, **kwargs) - self.exploration_constant = exploration_constant - - def compute_score(self, candidate): - """Compute the UCB score for the candidate. - - UCB = mean_score + exploration_constant * sqrt(ln(total_trials) / candidate_trials) - - Args: - candidate (ModuleCandidate): The candidate for which to compute the UCB score. - Returns: - float: The computed UCB score for the candidate. - """ - if not isinstance(candidate, ModuleCandidate): - raise TypeError("candidate must be an instance of ModuleCandidate.") - - # Get scores from rollouts - scores = [r['score'] for r in candidate.rollouts] - - # If no rollouts, return a high exploration score to encourage trying this candidate - if not scores: - return float('inf') # Maximum exploration for untried candidates - - # Calculate mean score for this candidate - mean_score = np.mean(scores) - candidate_trials = len(scores) - - # Calculate total trials across all candidates in memory - total_trials = sum(len(c.rollouts) for _, c in self.memory) - - # Handle edge case where total_trials is 0 or 1 - if total_trials <= 1: - return mean_score - - # Calculate UCB score - exploration_term = self.exploration_constant * np.sqrt(np.log(total_trials) / candidate_trials) - ucb_score = mean_score + exploration_term - - return ucb_score From 700abe390fe824d4539c1369ae4a0e9bd33612ee Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 22 Jul 2025 23:29:39 +0000 Subject: [PATCH 130/314] Add memory size and ignore empty update. --- opto/trainer/algorithms/search_algorithms.py | 65 ++++++++++++++++---- 1 file changed, 53 insertions(+), 12 deletions(-) diff --git a/opto/trainer/algorithms/search_algorithms.py b/opto/trainer/algorithms/search_algorithms.py index 02653c12..4b5691ec 100644 --- a/opto/trainer/algorithms/search_algorithms.py +++ b/opto/trainer/algorithms/search_algorithms.py @@ -18,10 +18,8 @@ # TODO create SYNC and ASYNC versions of the base class; add an attribute to the class to indicate # TODO a better data structure to store samples -# update_dict - -# Some helper function to convert between trace.Module and update_dict +# Some helper functions to convert between trace.Module and update_dict def get_original_name(node): """Extract the original name from a node, removing all _copy suffixes.""" @@ -153,8 +151,8 @@ def train(self, num_threads = None, # maximum number of threads to use verbose = False, # whether to print the output of the agent # evaluation - test_dataset = None, # dataset of (x, info) pairs to evaluate the agent - test_guide = None, # guide to provide scores for the test set + test_dataset = None, # dataset of (x, info) pairs to evaluate the agent; if None, use train_dataset + test_guide = None, # guide to provide scores for the test set; if None, use guide eval_frequency: Union[int, None] = 1, # frequency of evaluation num_eval_samples: int = 1, # number of samples to use to evaluate each input # logging @@ -165,14 +163,13 @@ def train(self, ): ## Setup - test_frequency = eval_frequency # use eval_frequency as test_frequency # NOTE legacy notation log_frequency = log_frequency or test_frequency # frequency of logging (default to test_frequency) self.num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads test_dataset = test_dataset or train_dataset # default to train_dataset if test_dataset is not provided test_guide = test_guide or guide self.num_eval_samples = num_eval_samples # number of samples to use to evaluate each input - self.score_range = score_range or (0., 1.) + self.score_range = score_range or (-np.inf, np.inf) self.train_sampler = Sampler( DataLoader(train_dataset, batch_size=batch_size), @@ -376,6 +373,45 @@ def score(self): scores = [r['score'] for r in self.rollouts] return np.mean(scores) if scores else None +class HeapMemory: + # This is a basic implementation of a heap memory that uses a priority queue to store candidates. + # Later on this will be replaced by a memory DB. + def __init__(self, size=None): + """ Initialize an empty heap memory. """ + self.memory = [] + self.size = size # Optional size limit for the heap memory + + def push(self, item): + """ Push an item to the heap memory. """ + heapq.heappush(self.memory, item) + if self.size is not None and len(self.memory) > self.size: + # NOTE a heuristic for now + self.memory = self.memory[:self.size] # Keep only the top `size` items + + def pop(self): + """ Pop the top item from the heap memory. """ + if not self.memory: + raise IndexError("pop from an empty heap memory") + return heapq.heappop(self.memory) + + def __len__(self): + """ Return the number of items in the heap memory. """ + return len(self.memory) + + def __bool__(self): + """ Return True if the heap memory is not empty, False otherwise. """ + return len(self.memory) > 0 + + def __iter__(self): + """ Iterate over the items in the heap memory. """ + return iter(self.memory) + + def best(self): + """ Return the best item in the heap memory without removing it. """ + if not self.memory: + raise IndexError("best from an empty heap memory") + return self.memory[0] + class PrioritySearch(SearchTemplate): """ A search algorithm that uses a priority queue to explore the parameter space and propose new candidates. """ @@ -406,6 +442,7 @@ def train(self, num_candidates: int = 10, # number of candidates to propose default_score: float = float('inf'), # default score assigned to priority queue candidates validate_proposals: bool = True, # whether to validate the proposed parameters + memory_size: Optional[int] = None, # size of the heap memory to store the candidates; if None, no limit is set # Additional keyword arguments **kwargs ): @@ -414,7 +451,9 @@ def train(self, self.num_candidates = num_candidates # number of candidates to propose by each optimizer call self.validate_proposals = validate_proposals # whether to validate the proposed parameters self.default_score = default_score - self.memory = [(self.default_score, ModuleCandidate(self.agent))] # Priority queue of ModuleCandidates, initialized with the base agent + self.memory = HeapMemory(size=memory_size) # Initialize the heap memory with a size limit + self.memory.push((self.default_score, ModuleCandidate(self.agent))) # Push the base agent as the first candidate + self._exploration_candidates = None super().train(guide, train_dataset, @@ -494,6 +533,8 @@ def _step(n, verbose=False, num_threads=None, **kwargs): optimizer.zero_feedback() # reset the optimizer's feedback optimizer.backward(target, feedback) # compute the gradients based on the targets and feedbacks update_dict = optimizer.step(verbose=verbose, num_threads=num_threads, bypassing=True, **kwargs) + if not update_dict: # if the optimizer did not propose any updates + return None # return None to indicate no updates were proposed # update_dict may only contain some of the parameters of the agent, we need to make sure it contains all the parameters for param in optimizer.parameters: # for all parameters if param not in update_dict: # update_dict misses some parameters @@ -513,7 +554,7 @@ def _step(n, verbose=False, num_threads=None, **kwargs): description="Running optimizers on samples") # update_dicts is a list of dicts of length n_agents * n_proposals # Create ModuleCandidate objects for each proposed update_dict - candidates = [ModuleCandidate(self.agent, update_dict) for update_dict in update_dicts] + candidates = [ModuleCandidate(self.agent, update_dict) for update_dict in update_dicts if update_dict is not None] # filter out None updates return candidates def validate(self, candidates, samples, verbose=False, **kwargs): @@ -598,7 +639,7 @@ def update_memory(self, validate_results, **kwargs): for candidate, rollouts in validate_results.items(): candidate.add_rollouts(rollouts) # add the rollouts to the candidate score = self.compute_score(candidate) # compute the score for the candidate - heapq.heappush(self.memory, (-score, candidate)) # add the candidate to the priority queue + self.memory.push((-score, candidate)) # push the candidate to the priority queue with negative score (to make it a max-heap) #### @@ -613,7 +654,7 @@ def explore(self, **kwargs): # pop top self.num_candidates candidates from the priority queue top_candidates = [] while len(top_candidates) < self.num_candidates and self.memory: - score, candidate = heapq.heappop(self.memory) + score, candidate = self.memory.pop() # pop the top candidate from the priority queue top_candidates.append(candidate) # add the candidate to the top candidates return top_candidates, {} @@ -630,7 +671,7 @@ def exploit(self, **kwargs): # This function can be overridden by subclasses to implement a different exploitation strategy if not self.memory: raise ValueError("The priority queue is empty. Cannot exploit.") - best = min(self.memory) # (score, candidate) + best = self.memory.best() # (score, candidate) score, best_candidate = best score = -score # remember that we stored negative scores in the priority queue return best_candidate, { From 9398f421eb935c2afb94504827e397ff611f5f48 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 24 Jul 2025 21:17:55 +0000 Subject: [PATCH 131/314] Fix a bug in propose in PrioritySearch. Add __lt__ method to ModuleCandidate. Add test for priority search. --- opto/trainer/algorithms/search_algorithms.py | 62 +++++--- tests/unit_tests/test_priority_search.py | 158 +++++++++++++++++++ tests/unit_tests/test_sampler.py | 11 +- 3 files changed, 204 insertions(+), 27 deletions(-) create mode 100644 tests/unit_tests/test_priority_search.py diff --git a/opto/trainer/algorithms/search_algorithms.py b/opto/trainer/algorithms/search_algorithms.py index 4b5691ec..b7c644b5 100644 --- a/opto/trainer/algorithms/search_algorithms.py +++ b/opto/trainer/algorithms/search_algorithms.py @@ -10,8 +10,8 @@ from opto.trainer.algorithms.basic_algorithms import Minibatch, AlgorithmBase, batchify from opto.trainer.evaluators import evaluate from opto.trainer.loader import DataLoader - from opto.trainer.sampler import Sampler, RolloutsGraph +import time # TODO save and load SearchTemplate # TODO async version??? @@ -322,6 +322,7 @@ def __init__(self, self.update_dict = update_dict if update_dict is not None else {} self.update_dict = remap_update_dict(self.base_module, self.update_dict) self.rollouts = [] # list of dicts containing the rollout information (not RolloutsGraph, but a list of dicts) + self.created_time = time.time() def get_module(self): """ Apply the update_dict to the base_module and return the updated module. @@ -352,6 +353,14 @@ def __eq__(self, other): assert isinstance(other, ModuleCandidate), "other must be an instance of ModuleCandidate." return self.update_dict == other.update_dict + # TODO better way? + def __lt__(self, other): + """ Compare two candidates based on their update_dict. """ + assert isinstance(other, ModuleCandidate), "other must be an instance of ModuleCandidate." + return self.created_time > other.created_time + # This would give priority to later created candidates in the heap memory + # since the heapq is a min-heap . + def __hash__(self): """ Hash the candidate based on its update_dict. """ return hash(frozenset(self.update_dict.items())) @@ -376,14 +385,16 @@ def score(self): class HeapMemory: # This is a basic implementation of a heap memory that uses a priority queue to store candidates. # Later on this will be replaced by a memory DB. + + # NOTE that the heap memory is a max-heap, so we store negative scores to use the default min-heap behavior of heapq. def __init__(self, size=None): """ Initialize an empty heap memory. """ self.memory = [] self.size = size # Optional size limit for the heap memory - def push(self, item): + def push(self, score, data): """ Push an item to the heap memory. """ - heapq.heappush(self.memory, item) + heapq.heappush(self.memory, (-score, data)) if self.size is not None and len(self.memory) > self.size: # NOTE a heuristic for now self.memory = self.memory[:self.size] # Keep only the top `size` items @@ -439,7 +450,8 @@ def train(self, save_frequency: Union[int, None] = None, # frequency of saving the agent save_path: str = "checkpoints/agent.pkl", # path to save the agent # Priority Search specific parameters - num_candidates: int = 10, # number of candidates to propose + num_candidates: int = 10, # number of candidates to propose for exploration + num_proposals: int = 1, # number of proposals to generate per optimizer default_score: float = float('inf'), # default score assigned to priority queue candidates validate_proposals: bool = True, # whether to validate the proposed parameters memory_size: Optional[int] = None, # size of the heap memory to store the candidates; if None, no limit is set @@ -449,10 +461,11 @@ def train(self, # Create agents and optimizers for search self.num_candidates = num_candidates # number of candidates to propose by each optimizer call + self.num_proposals = num_proposals self.validate_proposals = validate_proposals # whether to validate the proposed parameters self.default_score = default_score self.memory = HeapMemory(size=memory_size) # Initialize the heap memory with a size limit - self.memory.push((self.default_score, ModuleCandidate(self.agent))) # Push the base agent as the first candidate + self.memory.push(self.default_score, ModuleCandidate(self.agent)) # Push the base agent as the first candidate self._exploration_candidates = None @@ -497,7 +510,7 @@ def update(self, samples=None, verbose=False, **kwargs): info_log.update(info_explore) # add the info from the explore step return best_candidate.update_dict, [c.get_module() for c in exploration_candidates], info_log - def propose(self, samples, verbose=False, n_proposals=1, **kwargs): + def propose(self, samples, verbose=False, **kwargs): """ Analyzing samples and propose new parameters using self.optimizer. An independent optimizer is used for the minibatch generated by one agent and generates n_proposals proposals. Args: @@ -512,18 +525,13 @@ def propose(self, samples, verbose=False, n_proposals=1, **kwargs): assert isinstance(samples, Samples), "samples must be an instance of Samples." samples = samples.samples # list of RolloutsGraph objects + n_proposals = self.num_proposals # number of proposals to generate per optimizer - def _step(n, verbose=False, num_threads=None, **kwargs): - """ Standard optimizer step for a single agent. """ - # optimizer = self._optimizers[n] # get the optimizer for the n-th agent - # NOTE this seems slow + def _backward(n): optimizer = copy.deepcopy(self.optimizer) # create a copy of the optimizer to avoid modifying the original one - rollouts = samples[n] # RolloutsGraph - # Make sure all rollouts are based on the same module, so they can be viewed as a minibatch. optimizer.parameters = rollouts.module.parameters() # set the optimizer's parameters to the proposal's parameters - targets = [r.target for r in rollouts] feedbacks = [r.feedback for r in rollouts] # batchify the targets and feedbacks @@ -532,7 +540,18 @@ def _step(n, verbose=False, num_threads=None, **kwargs): # standard optimizer step optimizer.zero_feedback() # reset the optimizer's feedback optimizer.backward(target, feedback) # compute the gradients based on the targets and feedbacks - update_dict = optimizer.step(verbose=verbose, num_threads=num_threads, bypassing=True, **kwargs) + return optimizer + + n_subgraphs = len(samples) # number of subgraphs (agents) in the samples + args_list = [(n,) for n in range(n_subgraphs)] + optimizers = async_run([_backward]*n_subgraphs*n_proposals, # run the optimizer step for each agent in parallel + args_list=args_list, + max_workers=self.num_threads, # use the number of threads specified in the class + description=None) + + # For each optimizer, containing the backward feedback, we call it n_proposals times to get the proposed parameters. + def _step(optimizer): + update_dict = optimizer.step(verbose=verbose, num_threads=self.num_threads, bypassing=True, **kwargs) if not update_dict: # if the optimizer did not propose any updates return None # return None to indicate no updates were proposed # update_dict may only contain some of the parameters of the agent, we need to make sure it contains all the parameters @@ -543,15 +562,12 @@ def _step(n, verbose=False, num_threads=None, **kwargs): update_dict = remap_update_dict(self.agent, update_dict) # remap the update dict to the agent's parameters return update_dict # return the proposed parameters - n_subgraphs = len(samples) # number of subgraphs (agents) in the samples - args_list = [(n, verbose, self.num_threads) for n in range(n_subgraphs)] - args_list = args_list * n_proposals # repeat args_list n_proposals times - kwargs_list = [kwargs] * n_subgraphs * n_proposals # repeat kwargs for each agent + args_list = [(o,) for o in optimizers ] * n_proposals # repeat args_list n_proposals times update_dicts = async_run([_step]*n_subgraphs*n_proposals, # run the optimizer step for each agent in parallel args_list=args_list, - kwargs_list=kwargs_list, max_workers=self.num_threads, # use the number of threads specified in the class description="Running optimizers on samples") + # update_dicts is a list of dicts of length n_agents * n_proposals # Create ModuleCandidate objects for each proposed update_dict candidates = [ModuleCandidate(self.agent, update_dict) for update_dict in update_dicts if update_dict is not None] # filter out None updates @@ -623,7 +639,6 @@ def validate(self, candidates, samples, verbose=False, **kwargs): break if not matched: # key not found in results results[c] = rollouts_list # add the rollouts to the candidate - # NOTE what if propose creates multiple exploration_candidates that have the same parameters and the same rollouts stats? # For example, it copies candidates. This would create a bug. return results @@ -639,8 +654,7 @@ def update_memory(self, validate_results, **kwargs): for candidate, rollouts in validate_results.items(): candidate.add_rollouts(rollouts) # add the rollouts to the candidate score = self.compute_score(candidate) # compute the score for the candidate - self.memory.push((-score, candidate)) # push the candidate to the priority queue with negative score (to make it a max-heap) - + self.memory.push(score, candidate) #### def explore(self, **kwargs): @@ -648,8 +662,8 @@ def explore(self, **kwargs): Args: **kwargs: Additional keyword arguments that may be used by the implementation. Returns: - update_dict (dict of Parameter: Any): A dictionary containing the updated parameters of the agent. - proposal_update_dicts (list of dict): A list of proposed parameter updates (dict) for the next iteration. + list: A list of proposed candidates. + dict: A dictionary containing logging information about the exploration. """ # pop top self.num_candidates candidates from the priority queue top_candidates = [] diff --git a/tests/unit_tests/test_priority_search.py b/tests/unit_tests/test_priority_search.py new file mode 100644 index 00000000..bf331e40 --- /dev/null +++ b/tests/unit_tests/test_priority_search.py @@ -0,0 +1,158 @@ +from opto import trace +from opto.trainer.loader import DataLoader +from opto.trainer.sampler import Sampler +from opto.trainer.algorithms.search_algorithms import PrioritySearch as _PrioritySearch +from opto.trainer.algorithms.search_algorithms import ModuleCandidate +from opto.optimizers import OptoPrimeV2 +from opto.trainer.guide import AutoGuide +from opto.utils.llm import DummyLLM + +import re +import numpy as np + + +class Guide(AutoGuide): + + def get_feedback(self, query, response, reference=None, **kwargs): + """ + Provide feedback based on the query and response. + + Args: + query: The query to analyze. + response: The response generated by the model. + reference: Optional reference answer for comparison. + **kwargs: Additional context or parameters. + + Returns: + A tuple containing a score and feedback string. + """ + score = response == reference + feedback = "Exact match!" if score == 1.0 else "Not an exact match." + return score, feedback + +@trace.model +class Agent: + + def __init__(self): + self.param = trace.node(1., trainable=True) + self.state = 0 + + def forward(self, x): + return self.param + 1 + + + +xs = [1, 2, 3, 4, 5] +infos = [1, 2, 3, 4, 5] +batch_size = 3 +sub_batch_size = 2 +num_threads = 1 # 2 +dataset = {'inputs': xs, 'infos': infos} +loader = DataLoader(dataset, batch_size=batch_size, randomize=False) +sampler = Sampler(loader=loader, guide=Guide(), sub_batch_size=sub_batch_size, num_threads=num_threads) + +num_proposals = 10 +num_candidates = 5 +memory_size = 3 +suggested_value = 5 + + + +class PrioritySearch(_PrioritySearch): + # This class is for testing the PrioritySearch algorithm + + def propose(self, samples, verbose=False, n_proposals=1, **kwargs): + print("Propose at iteration:", self.n_iters) + # assert len(samples) == batch_size, f"Expected {batch_size} samples, got {len(samples)}" + # assert len(samples) == len(agents) * np.ceil(batch_size / self.sub_batch_size), f"Expected {len(agents) * np.ceil(batch_size / self.sub_batch_size)} samples, got {len(samples)}" + + candidates = super().propose(samples, verbose=verbose, n_proposals=n_proposals, **kwargs) + # In this example this will always be value 5 + assert isinstance(candidates, list), "Expected candidates to be a list" + assert all(isinstance(c, ModuleCandidate) for c in candidates), "All candidates should be ModuleCandidate instances" + assert len(candidates) == np.ceil(batch_size / sub_batch_size) * self.num_proposals, f"Expected {np.ceil(batch_size / sub_batch_size) * self.num_proposals} candidates, got {len(candidates)}" + return candidates + + def validate(self, candidates, samples, verbose=False, **kwargs): + print("Validate at iteration:", self.n_iters) + assert len(candidates) == np.ceil(batch_size / sub_batch_size) * self.num_proposals, f"Expected {np.ceil(batch_size / sub_batch_size) * self.num_proposals} candidates, got {len(candidates)}" + + validate_results = super().validate(candidates, samples, verbose=verbose, **kwargs) + assert isinstance(validate_results, dict), "Expected validate_results to be a dict" + assert all(isinstance(v, ModuleCandidate) for v in validate_results.keys()), "All keys should be ModuleCandidate instances" + keys = list(validate_results.keys()) + # should contain one from exploration and one from exploitation + assert len(validate_results) == 2, "In this example, all proposals are the same, so we expect only two validate results." + + return validate_results + + def exploit(self, **kwargs): + print("Exploit at iteration:", self.n_iters) + + candidate, info_dict = super().exploit(**kwargs) + assert isinstance(candidate, ModuleCandidate), "Expected candidate to be an instance of ModuleCandidate" + assert isinstance(info_dict, dict), "Expected info_dict to be a dictionary" + return candidate, info_dict + + def explore(self, **kwargs): + print("Explore at iteration:", self.n_iters) + + candidates, info_dict = super().explore(**kwargs) + assert isinstance(candidates, list) + assert isinstance(info_dict, dict) + + if self.n_iters == 0: + assert len(candidates) == 1, f"Expected 1 candidate, got {len(candidates)}" + else: + num_candidates = min(self.num_candidates, 2) # in this example, memory will contain at most 2 unique candidates + assert len(candidates) == num_candidates, f"Expected {num_candidates} candidates at iter {self.n_iters}, got {len(candidates)}" + assert all(isinstance(c, ModuleCandidate) for c in candidates), "All candidates should be ModuleCandidate instances" + + return candidates, info_dict + + + +def _llm_callable(messages, **kwargs): + """ + A dummy LLM callable that simulates a response. + """ + problem = messages[1]['content'] + + # extract name from + name = re.findall(r"", problem) + if name: + name = name[0] + else: + name = "unknown" + + return f""" + Dummy reasoning based on the input messages. + + {name} + {suggested_value} + + """ + +dummy_llm = DummyLLM(_llm_callable) +agent = Agent() +optimizer = OptoPrimeV2( + agent.parameters(), + llm=dummy_llm, +) + +algo = PrioritySearch( + agent, + optimizer, +) + +algo.train( + guide=Guide(), + train_dataset=dataset, + batch_size=batch_size, + sub_batch_size=sub_batch_size, + num_threads=num_threads, + num_candidates=num_candidates, + num_proposals=num_proposals, + memory_size=memory_size, + verbose=False, +) diff --git a/tests/unit_tests/test_sampler.py b/tests/unit_tests/test_sampler.py index 0ac4d104..d6fd6d16 100644 --- a/tests/unit_tests/test_sampler.py +++ b/tests/unit_tests/test_sampler.py @@ -66,6 +66,8 @@ def test_sample_with_single_agent(): for rollouts in samples: for rollout in rollouts: assert rollout.target == 1 # state is not affected by multiple calls + rollout.target.backward('Fake feedback') + # each rollout should be independent so `has been backwarded.` error should not be raised samples, batch = sampler.sample([Agent()]) @@ -79,7 +81,8 @@ def test_sample_with_single_agent(): for rollouts in samples: for rollout in rollouts: assert rollout.target == 1 # state is not affected by multiple calls - + rollout.target.backward('Fake feedback') + # each rollout should be independent so `has been backwarded.` error should not be raised def test_sample_with_multiple_agents(): """ @@ -116,7 +119,8 @@ def test_sample_with_multiple_agents(): for rollouts in samples: for rollout in rollouts: - assert rollout.target == 1 # state is not affected by multiple calls + rollout.target.backward('Fake feedback') + # each rollout should be independent so `has been backwarded.` error should not be raised samples, batch = sampler.sample([Agent(), Agent()]) # check batch is equal to dataset's second batch_size elements @@ -130,4 +134,5 @@ def test_sample_with_multiple_agents(): for rollouts in samples: for rollout in rollouts: - assert rollout.target == 1 # state is not affected by multiple calls \ No newline at end of file + rollout.target.backward('Fake feedback') + # each rollout should be independent so `has been backwarded.` error should not be raised \ No newline at end of file From 41083465271e0e0dcae10edede5b29be00c96ea4 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 24 Jul 2025 21:21:51 +0000 Subject: [PATCH 132/314] Add num_rollouts property to ModuleCandidate --- opto/trainer/algorithms/search_algorithms.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/opto/trainer/algorithms/search_algorithms.py b/opto/trainer/algorithms/search_algorithms.py index b7c644b5..ed0b4e15 100644 --- a/opto/trainer/algorithms/search_algorithms.py +++ b/opto/trainer/algorithms/search_algorithms.py @@ -382,6 +382,11 @@ def score(self): scores = [r['score'] for r in self.rollouts] return np.mean(scores) if scores else None + @property + def num_rollouts(self): + """ Return the number of rollouts collected for this candidate. """ + return len(self.rollouts) + class HeapMemory: # This is a basic implementation of a heap memory that uses a priority queue to store candidates. # Later on this will be replaced by a memory DB. From a2d56632b6bb32eb80a4414ce2e36948ab1e7e39 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 24 Jul 2025 21:35:22 +0000 Subject: [PATCH 133/314] Refactor priority search. --- examples/gsm8k_search_algo.py | 4 +- opto/trainer/algorithms/__init__.py | 1 + .../algorithms/priority_search/__init__.py | 1 + .../priority_search.py} | 302 +----------------- .../algorithms/priority_search/utils.py | 84 +++++ opto/trainer/sampler.py | 8 +- tests/unit_tests/test_priority_search.py | 6 +- 7 files changed, 97 insertions(+), 309 deletions(-) create mode 100644 opto/trainer/algorithms/priority_search/__init__.py rename opto/trainer/algorithms/{search_algorithms.py => priority_search/priority_search.py} (61%) create mode 100644 opto/trainer/algorithms/priority_search/utils.py diff --git a/examples/gsm8k_search_algo.py b/examples/gsm8k_search_algo.py index 1243cd8f..3e21f7cf 100644 --- a/examples/gsm8k_search_algo.py +++ b/examples/gsm8k_search_algo.py @@ -2,8 +2,8 @@ import numpy as np from opto import trace from opto.utils.llm import LLM, LiteLLM -from opto.optimizers import OptoPrime -from opto.trainer.algorithms.search_algorithms import PrioritySearch as SearchAlgorithm +from opto.optimizers import OptoPrimeV2 as OptoPrime +from opto.trainer.algorithms.priority_search import PrioritySearch as SearchAlgorithm from opto.trainer.loggers import TensorboardLogger from opto.trainer.guide import VerbalJudgeGuide from typing import Any diff --git a/opto/trainer/algorithms/__init__.py b/opto/trainer/algorithms/__init__.py index 2586fd31..084cd459 100644 --- a/opto/trainer/algorithms/__init__.py +++ b/opto/trainer/algorithms/__init__.py @@ -1,3 +1,4 @@ from opto.trainer.algorithms.basic_algorithms import Minibatch, MinibatchAlgorithm, BasicSearchAlgorithm from opto.trainer.algorithms.beamsearch_algorithm import BeamsearchAlgorithm, BeamsearchHistoryAlgorithm from opto.trainer.algorithms.UCBsearch import UCBSearchAlgorithm +from opto.trainer.algorithms.priority_search import PrioritySearch \ No newline at end of file diff --git a/opto/trainer/algorithms/priority_search/__init__.py b/opto/trainer/algorithms/priority_search/__init__.py new file mode 100644 index 00000000..68fe26c0 --- /dev/null +++ b/opto/trainer/algorithms/priority_search/__init__.py @@ -0,0 +1 @@ +from opto.trainer.algorithms.priority_search.priority_search import PrioritySearch \ No newline at end of file diff --git a/opto/trainer/algorithms/search_algorithms.py b/opto/trainer/algorithms/priority_search/priority_search.py similarity index 61% rename from opto/trainer/algorithms/search_algorithms.py rename to opto/trainer/algorithms/priority_search/priority_search.py index ed0b4e15..10da4b89 100644 --- a/opto/trainer/algorithms/search_algorithms.py +++ b/opto/trainer/algorithms/priority_search/priority_search.py @@ -1,306 +1,14 @@ import numpy as np import copy import heapq -from dataclasses import dataclass +import time from typing import Union, List, Tuple, Dict, Any, Optional from opto import trace from opto.trace.nodes import ParameterNode -from opto.trainer.utils import async_run, batch_run -from opto.optimizers.utils import print_color -from opto.trainer.algorithms.basic_algorithms import Minibatch, AlgorithmBase, batchify -from opto.trainer.evaluators import evaluate -from opto.trainer.loader import DataLoader -from opto.trainer.sampler import Sampler, RolloutsGraph -import time - -# TODO save and load SearchTemplate -# TODO async version??? -# TODO create SYNC and ASYNC versions of the base class; add an attribute to the class to indicate -# TODO a better data structure to store samples - - -# Some helper functions to convert between trace.Module and update_dict - -def get_original_name(node): - """Extract the original name from a node, removing all _copy suffixes.""" - py_name = node.py_name # This removes colons: "param:0" -> "param0" - - # Find the first occurrence of "_copy" and remove it and everything after - copy_index = py_name.find('_copy') - if copy_index != -1: - return py_name[:copy_index] - else: - return py_name - -def is_node_copy(a, b): - """Check if two nodes are copies of each other by comparing their original names. - - This function has transitivity: if A is a copy of B and B is a copy of C, - then A is also considered a copy of C. - """ - return get_original_name(a) == get_original_name(b) - -def is_module_copy(a, b): - """ Check if a and b (trace.Modules) are copies of each other. """ - parameters_a = a.parameters() # list of ParameterNode - parameters_b = b.parameters() # list of ParameterNode - # Check if all parameters of a are copies of b or vice versa - # This might over count - # need to check 1:1 correspondence - matched = [] - for p_a in parameters_a: - _matched = [] - for p_b in parameters_b: - _matched.append(is_node_copy(p_a, p_b)) - np.array(matched) - if np.all(np.sum(matched, axis=1) == 1) and np.all(np.sum(matched, axis=0) == 1): - return True - return False - -def remap_update_dict(base_module, update_dict): - """ Remap the update dict to the agent's parameters. update_dict might have keys which are copies of the base_module's parameters or visa versa. - This function remaps the keys in update_dict to the original parameters of the base_module. - - The return dict is empty if no keys in update_dict matched any parameters of the base_module. This condition can be used to check if the update_dict contains non-trivial updates. - """ - parameters = base_module.parameters() # get the parameters of the base agent - remapped_update_dict = {} - for k, v in update_dict.items(): - for p in parameters: - if is_node_copy(k, p): # Check if k is a copy of p or p is a copy of k - remapped_update_dict[p] = v - break # stop checking once we've found a match - return remapped_update_dict - -def set_module_parameters(agent, update_dict): - """ Set the parameters of the agent based on the update_dict. - The update_dict is a dictionary of ParameterNode: value pairs. - The agent's parameters will be updated with the values from the update_dict. - """ - remapped_update_dict = remap_update_dict(agent, update_dict) # remap the update dict to the agent's parameters - for k, v in remapped_update_dict.items(): - k._data = v # set the parameter's data to the value in the update_dict - -def create_module_from_update_dict(agent, update_dict): - """ Create a new agent from the update_dict. - The update_dict is a dictionary of ParameterNode: value pairs. - A new agent will be created with the parameters set to the values from the update_dict. - """ - new_agent = copy.deepcopy(agent) #.copy() # create a copy of the agent - set_module_parameters(new_agent, update_dict) # set the parameters of the new agent - return new_agent # return the new agent - - - -class Samples: - """ A container for samples collected during the search algorithm. It contains a list of RolloutsGraph objects - and a dataset with inputs and infos which created the list of RolloutsGraph. """ - - samples: List[RolloutsGraph] - dataset: Dict[str, List[Any]] # contains 'inputs' and 'infos' keys - - def __init__(self, samples: List[RolloutsGraph], dataset: Dict[str, List[Any]]): - assert isinstance(samples, list), "samples must be a list of RolloutsGraph objects." - assert all(isinstance(s, RolloutsGraph) for s in samples), "All samples must be RolloutsGraph objects." - assert isinstance(dataset, dict), "dataset must be a dict." - assert 'inputs' in dataset and 'infos' in dataset, "dataset must contain 'inputs' and 'infos' keys." - - self.samples = samples - self.dataset = dataset # NOTE this cannot be extracted from the samples in general? - - def add_samples(self, samples): - """ Add samples to the Samples object. """ - assert isinstance(samples, Samples), "samples must be an instance of Samples." - samples = samples.samples # extract the samples from the Samples object - assert isinstance(samples, list), "samples must be a list of RolloutsGraph objects." - assert all(isinstance(s, RolloutsGraph) for s in samples), "All samples must be RolloutsGraph objects." - - # TODO assert xs and infos are in self.minibatch - # add a function to extract unique inputs and infos from the samples - - self.samples.extend(samples) - - def get_batch(self): - return self.dataset #['inputs'], self.minibatch['infos'] - - def __iter__(self): - """ Iterate over the samples. """ - return iter(self.samples) - - def __len__(self): - return sum(len(s) for s in self.samples) - - - -class SearchTemplate(Minibatch): - # This only uses __init__ and evaluate of Minibatch class. - """ This implements a generic template for search algorithm. """ - - def train(self, - guide, # guide to provide feedback - train_dataset, # dataset of (x, info) pairs to train the agent - *, - # validation - validate_dataset = None, # same format as train_dataset; if None use the current batch. - validate_guide = None, # to provide scores for the validation set - # training loop - batch_size = 1, # batch size for updating the agent - sub_batch_size = None, # sub-batch size for broadcasting the agents - score_range = None, # minimum score to update the agent - num_epochs = 1, # number of training epochs - num_threads = None, # maximum number of threads to use - verbose = False, # whether to print the output of the agent - # evaluation - test_dataset = None, # dataset of (x, info) pairs to evaluate the agent; if None, use train_dataset - test_guide = None, # guide to provide scores for the test set; if None, use guide - eval_frequency: Union[int, None] = 1, # frequency of evaluation - num_eval_samples: int = 1, # number of samples to use to evaluate each input - # logging - log_frequency = None, # frequency of logging - save_frequency: Union[int, None] = None, # frequency of saving the agent - save_path: str = "checkpoints/agent.pkl", # path to save the agent - **kwargs - ): - - ## Setup - test_frequency = eval_frequency # use eval_frequency as test_frequency # NOTE legacy notation - log_frequency = log_frequency or test_frequency # frequency of logging (default to test_frequency) - self.num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads - test_dataset = test_dataset or train_dataset # default to train_dataset if test_dataset is not provided - test_guide = test_guide or guide - self.num_eval_samples = num_eval_samples # number of samples to use to evaluate each input - self.score_range = score_range or (-np.inf, np.inf) - - self.train_sampler = Sampler( - DataLoader(train_dataset, batch_size=batch_size), - guide, - num_threads=self.num_threads, - sub_batch_size=sub_batch_size, - score_range=self.score_range - ) - self._validate_dataset = validate_dataset # if None, the current batch will be used for validation - self.validate_sampler = Sampler( - DataLoader(validate_dataset if validate_dataset else {'inputs':[],'infos':[]}, batch_size=batch_size), - validate_guide or guide, - num_threads=self.num_threads, - sub_batch_size=None, # no sub-batch size for validation - score_range=self.score_range - ) - - # Evaluate the agent before learning - # NOTE set test_frequency < 0 to skip first evaluation - if (test_frequency is not None) and test_frequency > 0: - info_test = self.test(test_dataset, test_guide) # test self.agent - self.log(info_test) - - # Save the agent before learning if save_frequency > 0 - if (save_frequency is not None) and save_frequency > 0: - self.save(save_path) - - samples = None - self.n_epochs = 0 # number of epochs (full passes over the dataset) performed by the algorithm (This is incremented in sample) - self.n_samples = 0 # number of training samples processed by the algorithm (This is incremented in sample) - train_scores = [] # to store the scores of the agent during training - - while self.n_epochs < num_epochs : - - print(f"Epoch: {self.n_epochs}. Iteration: {self.n_iters}") - - # 1. Propose new parameters given the current state of the algorithm - # proposals: list of trace.Modules - update_dict, proposals, info_update = self.update(samples, verbose=verbose, **kwargs) - self.optimizer.update(update_dict) # update self.agent with the proposed parameters - - # 2. Get feedback on the proposed parameters on the current batch - # samples: Samples object containing the samples and the minibatch - samples, info_sample = self.sample(proposals, verbose=verbose, **kwargs) - - # Evaluate the agent after update - if (test_frequency is not None) and (self.n_iters % test_frequency == 0): - info_test = self.test(test_dataset, test_guide) # test self.agent - self.log(info_test, prefix="Test: ") - - # Save the algorithm state - if (save_frequency is not None and save_frequency > 0) and self.n_iters % save_frequency == 0: - self.save(save_path) - - # Log information - assert 'mean_score' in info_sample, "info_sample must contain 'mean_score'." - assert 'n_epochs' in info_sample, "info_sample must contain 'n_epochs'." - - train_scores.append(info_sample['mean_score']) # so that mean can be computed - if self.n_iters % log_frequency == 0: - self.logger.log('Average train score', np.mean(train_scores), self.n_iters, color='blue') - self.log(info_update, prefix="Update: ") - self.log(info_sample, prefix="Sample: ") - self.n_samples += len(samples) # update the number of samples processed - self.logger.log('Number of samples', self.n_samples, self.n_iters, color='blue') - # Log parameters - for p in self.agent.parameters(): - self.logger.log(f"Parameter: {p.name}", p.data, self.n_iters, color='red') - - # Update counters - self.n_epochs = info_sample['n_epochs'] # update the number of epochs completed - self.n_iters += 1 - return - - # Can be overridden by subclasses to implement specific sampling strategies - def sample(self, agents, verbose=False, **kwargs): - """ Sample a batch of data based on the proposed parameters. All proposals are evaluated on the same batch of inputs. - - Args: - agents (list): A list of trace.Modules (proposed parameters) to evaluate. - **kwargs: Additional keyword arguments that may be used by the implementation. - """ - samples = Samples(*self.train_sampler.sample(agents)) # create a Samples object to store the samples and the minibatch - - # Log information about the sampling - scores = [ g.get_scores() for g in samples.samples] # list of list of scores for each RolloutsGraph - scores = [item for sublist in scores for item in sublist] # flatten the list of scores - log_info = { - 'mean_score': np.mean(scores), - 'n_epochs': self.train_sampler.n_epochs, - } - return samples, log_info - - def log(self, info_log, prefix=""): - """ Log the information from the algorithm. """ - for key, value in info_log.items(): - try: - if value is not None: - self.logger.log(f"{prefix}{key}", value, self.n_iters) - except Exception as e: - print(e) - - def test(self, test_dataset, guide): - min_score = self.score_range[0] - # Test the agent's performance - test_score = self.evaluate(self.agent, guide, test_dataset['inputs'], test_dataset['infos'], - min_score=min_score, num_threads=self.num_threads, - description=f"Evaluating agent (iteration {self.n_iters})") # and log - return {'test_score': test_score} - - def save(self, save_path): - self.save_agent(save_path, self.n_iters) - # TODO save full state of self - - # Unimplemented methods that should be implemented by subclasses - def update(self, samples=None, verbose=False, **kwargs): - """ Update the agent based on the provided samples. - Args: - samples (list): A list of samples from the previous iteration. If None, the agent's parameters are returned without updating. - verbose (bool, optional): Whether to print verbose output. Defaults to False. - **kwargs: Additional keyword arguments that may be used by the implementation. - Returns: - update_dict (dict of Parameter: Any): A dictionary containing the updated parameters of the agent. - proposals (list of trace.Module): A list of proposed parameters (trace.Module) after the update. - info_log (dict of str: Any): A dictionary containing logging information about the update process. - - This method updates the agent's parameters based on samples of the training dataset and validation dataset (provided by self.get_validate_dataset). - In addition, it return new agents (proposals) that can be used for collecting data for the next iteration. - """ - raise NotImplementedError("The update method should be implemented by subclasses.") - # return update_dict, proposals, info_log +from opto.trainer.utils import async_run +from opto.trainer.algorithms.basic_algorithms import batchify +from opto.trainer.algorithms.priority_search.search_template import SearchTemplate, Samples +from opto.trainer.algorithms.priority_search.utils import set_module_parameters, remap_update_dict, create_module_from_update_dict # TODO make this hashable? diff --git a/opto/trainer/algorithms/priority_search/utils.py b/opto/trainer/algorithms/priority_search/utils.py new file mode 100644 index 00000000..8c4ed9db --- /dev/null +++ b/opto/trainer/algorithms/priority_search/utils.py @@ -0,0 +1,84 @@ +import numpy as np +import copy +import heapq +from dataclasses import dataclass +from typing import Union, List, Tuple, Dict, Any, Optional +from opto import trace +from opto.trace.nodes import ParameterNode +from opto.trainer.utils import async_run, batch_run +from opto.optimizers.utils import print_color +from opto.trainer.algorithms.basic_algorithms import Minibatch, AlgorithmBase, batchify +from opto.trainer.loader import DataLoader +from opto.trainer.sampler import Sampler, RolloutsGraph +import time + +# Some helper functions to convert between trace.Module and update_dict + +def get_original_name(node): + """Extract the original name from a node, removing all _copy suffixes.""" + py_name = node.py_name # This removes colons: "param:0" -> "param0" + + # Find the first occurrence of "_copy" and remove it and everything after + copy_index = py_name.find('_copy') + if copy_index != -1: + return py_name[:copy_index] + else: + return py_name + +def is_node_copy(a, b): + """Check if two nodes are copies of each other by comparing their original names. + + This function has transitivity: if A is a copy of B and B is a copy of C, + then A is also considered a copy of C. + """ + return get_original_name(a) == get_original_name(b) + +def is_module_copy(a, b): + """ Check if a and b (trace.Modules) are copies of each other. """ + parameters_a = a.parameters() # list of ParameterNode + parameters_b = b.parameters() # list of ParameterNode + # Check if all parameters of a are copies of b or vice versa + # This might over count + # need to check 1:1 correspondence + matched = [] + for p_a in parameters_a: + _matched = [] + for p_b in parameters_b: + _matched.append(is_node_copy(p_a, p_b)) + np.array(matched) + if np.all(np.sum(matched, axis=1) == 1) and np.all(np.sum(matched, axis=0) == 1): + return True + return False + +def remap_update_dict(base_module, update_dict): + """ Remap the update dict to the agent's parameters. update_dict might have keys which are copies of the base_module's parameters or visa versa. + This function remaps the keys in update_dict to the original parameters of the base_module. + + The return dict is empty if no keys in update_dict matched any parameters of the base_module. This condition can be used to check if the update_dict contains non-trivial updates. + """ + parameters = base_module.parameters() # get the parameters of the base agent + remapped_update_dict = {} + for k, v in update_dict.items(): + for p in parameters: + if is_node_copy(k, p): # Check if k is a copy of p or p is a copy of k + remapped_update_dict[p] = v + break # stop checking once we've found a match + return remapped_update_dict + +def set_module_parameters(agent, update_dict): + """ Set the parameters of the agent based on the update_dict. + The update_dict is a dictionary of ParameterNode: value pairs. + The agent's parameters will be updated with the values from the update_dict. + """ + remapped_update_dict = remap_update_dict(agent, update_dict) # remap the update dict to the agent's parameters + for k, v in remapped_update_dict.items(): + k._data = v # set the parameter's data to the value in the update_dict + +def create_module_from_update_dict(agent, update_dict): + """ Create a new agent from the update_dict. + The update_dict is a dictionary of ParameterNode: value pairs. + A new agent will be created with the parameters set to the values from the update_dict. + """ + new_agent = copy.deepcopy(agent) #.copy() # create a copy of the agent + set_module_parameters(new_agent, update_dict) # set the parameters of the new agent + return new_agent # return the new agent \ No newline at end of file diff --git a/opto/trainer/sampler.py b/opto/trainer/sampler.py index 9e1037a2..3ffeb689 100644 --- a/opto/trainer/sampler.py +++ b/opto/trainer/sampler.py @@ -1,15 +1,9 @@ import numpy as np import copy -import heapq from dataclasses import dataclass from typing import Union, List, Tuple, Dict, Any, Optional from opto import trace -from opto.trace.nodes import ParameterNode -from opto.trainer.utils import async_run, batch_run -from opto.optimizers.utils import print_color -from opto.trainer.algorithms.basic_algorithms import Minibatch, AlgorithmBase, batchify -from opto.trainer.evaluators import evaluate -from opto.trainer.loader import DataLoader +from opto.trainer.utils import batch_run from opto.trainer.guide import AutoGuide @dataclass diff --git a/tests/unit_tests/test_priority_search.py b/tests/unit_tests/test_priority_search.py index bf331e40..7c073156 100644 --- a/tests/unit_tests/test_priority_search.py +++ b/tests/unit_tests/test_priority_search.py @@ -1,8 +1,8 @@ from opto import trace from opto.trainer.loader import DataLoader from opto.trainer.sampler import Sampler -from opto.trainer.algorithms.search_algorithms import PrioritySearch as _PrioritySearch -from opto.trainer.algorithms.search_algorithms import ModuleCandidate +from opto.trainer.algorithms.priority_search.priority_search import PrioritySearch as _PrioritySearch +from opto.trainer.algorithms.priority_search.priority_search import ModuleCandidate from opto.optimizers import OptoPrimeV2 from opto.trainer.guide import AutoGuide from opto.utils.llm import DummyLLM @@ -46,7 +46,7 @@ def forward(self, x): infos = [1, 2, 3, 4, 5] batch_size = 3 sub_batch_size = 2 -num_threads = 1 # 2 +num_threads = 2 # 2 dataset = {'inputs': xs, 'infos': infos} loader = DataLoader(dataset, batch_size=batch_size, randomize=False) sampler = Sampler(loader=loader, guide=Guide(), sub_batch_size=sub_batch_size, num_threads=num_threads) From 116b7b302d42de8bdf40293b78aafe6116a6c329 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 24 Jul 2025 21:37:52 +0000 Subject: [PATCH 134/314] Add missing search template --- .../priority_search/search_template.py | 221 ++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 opto/trainer/algorithms/priority_search/search_template.py diff --git a/opto/trainer/algorithms/priority_search/search_template.py b/opto/trainer/algorithms/priority_search/search_template.py new file mode 100644 index 00000000..b5d2cb46 --- /dev/null +++ b/opto/trainer/algorithms/priority_search/search_template.py @@ -0,0 +1,221 @@ +import numpy as np +from typing import Union, List, Tuple, Dict, Any, Optional +from opto import trace +from opto.trainer.algorithms.basic_algorithms import Minibatch +from opto.trainer.loader import DataLoader +from opto.trainer.sampler import Sampler, RolloutsGraph + +# TODO save and load SearchTemplate +# TODO async version??? +# TODO create SYNC and ASYNC versions of the base class; add an attribute to the class to indicate + + +class Samples: + """ A container for samples collected during the search algorithm. It contains a list of RolloutsGraph objects + and a dataset with inputs and infos which created the list of RolloutsGraph. """ + + samples: List[RolloutsGraph] + dataset: Dict[str, List[Any]] # contains 'inputs' and 'infos' keys + + def __init__(self, samples: List[RolloutsGraph], dataset: Dict[str, List[Any]]): + assert isinstance(samples, list), "samples must be a list of RolloutsGraph objects." + assert all(isinstance(s, RolloutsGraph) for s in samples), "All samples must be RolloutsGraph objects." + assert isinstance(dataset, dict), "dataset must be a dict." + assert 'inputs' in dataset and 'infos' in dataset, "dataset must contain 'inputs' and 'infos' keys." + + self.samples = samples + self.dataset = dataset # NOTE this cannot be extracted from the samples in general? + + def add_samples(self, samples): + """ Add samples to the Samples object. """ + assert isinstance(samples, Samples), "samples must be an instance of Samples." + samples = samples.samples # extract the samples from the Samples object + assert isinstance(samples, list), "samples must be a list of RolloutsGraph objects." + assert all(isinstance(s, RolloutsGraph) for s in samples), "All samples must be RolloutsGraph objects." + + # TODO assert xs and infos are in self.minibatch + # add a function to extract unique inputs and infos from the samples + + self.samples.extend(samples) + + def get_batch(self): + return self.dataset #['inputs'], self.minibatch['infos'] + + def __iter__(self): + """ Iterate over the samples. """ + return iter(self.samples) + + def __len__(self): + return sum(len(s) for s in self.samples) + + + +class SearchTemplate(Minibatch): + # This only uses __init__ and evaluate of Minibatch class. + """ This implements a generic template for search algorithm. """ + + def train(self, + guide, # guide to provide feedback + train_dataset, # dataset of (x, info) pairs to train the agent + *, + # validation + validate_dataset = None, # same format as train_dataset; if None use the current batch. + validate_guide = None, # to provide scores for the validation set + # training loop + batch_size = 1, # batch size for updating the agent + sub_batch_size = None, # sub-batch size for broadcasting the agents + score_range = None, # minimum score to update the agent + num_epochs = 1, # number of training epochs + num_threads = None, # maximum number of threads to use + verbose = False, # whether to print the output of the agent + # evaluation + test_dataset = None, # dataset of (x, info) pairs to evaluate the agent; if None, use train_dataset + test_guide = None, # guide to provide scores for the test set; if None, use guide + eval_frequency: Union[int, None] = 1, # frequency of evaluation + num_eval_samples: int = 1, # number of samples to use to evaluate each input + # logging + log_frequency = None, # frequency of logging + save_frequency: Union[int, None] = None, # frequency of saving the agent + save_path: str = "checkpoints/agent.pkl", # path to save the agent + **kwargs + ): + + ## Setup + test_frequency = eval_frequency # use eval_frequency as test_frequency # NOTE legacy notation + log_frequency = log_frequency or test_frequency # frequency of logging (default to test_frequency) + self.num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads + test_dataset = test_dataset or train_dataset # default to train_dataset if test_dataset is not provided + test_guide = test_guide or guide + self.num_eval_samples = num_eval_samples # number of samples to use to evaluate each input + self.score_range = score_range or (-np.inf, np.inf) + + self.train_sampler = Sampler( + DataLoader(train_dataset, batch_size=batch_size), + guide, + num_threads=self.num_threads, + sub_batch_size=sub_batch_size, + score_range=self.score_range + ) + self._validate_dataset = validate_dataset # if None, the current batch will be used for validation + self.validate_sampler = Sampler( + DataLoader(validate_dataset if validate_dataset else {'inputs':[],'infos':[]}, batch_size=batch_size), + validate_guide or guide, + num_threads=self.num_threads, + sub_batch_size=None, # no sub-batch size for validation + score_range=self.score_range + ) + + # Evaluate the agent before learning + # NOTE set test_frequency < 0 to skip first evaluation + if (test_frequency is not None) and test_frequency > 0: + info_test = self.test(test_dataset, test_guide) # test self.agent + self.log(info_test) + + # Save the agent before learning if save_frequency > 0 + if (save_frequency is not None) and save_frequency > 0: + self.save(save_path) + + samples = None + self.n_epochs = 0 # number of epochs (full passes over the dataset) performed by the algorithm (This is incremented in sample) + self.n_samples = 0 # number of training samples processed by the algorithm (This is incremented in sample) + train_scores = [] # to store the scores of the agent during training + + while self.n_epochs < num_epochs : + + print(f"Epoch: {self.n_epochs}. Iteration: {self.n_iters}") + + # 1. Propose new parameters given the current state of the algorithm + # proposals: list of trace.Modules + update_dict, proposals, info_update = self.update(samples, verbose=verbose, **kwargs) + self.optimizer.update(update_dict) # update self.agent with the proposed parameters + + # 2. Get feedback on the proposed parameters on the current batch + # samples: Samples object containing the samples and the minibatch + samples, info_sample = self.sample(proposals, verbose=verbose, **kwargs) + + # Evaluate the agent after update + if (test_frequency is not None) and (self.n_iters % test_frequency == 0): + info_test = self.test(test_dataset, test_guide) # test self.agent + self.log(info_test, prefix="Test: ") + + # Save the algorithm state + if (save_frequency is not None and save_frequency > 0) and self.n_iters % save_frequency == 0: + self.save(save_path) + + # Log information + assert 'mean_score' in info_sample, "info_sample must contain 'mean_score'." + assert 'n_epochs' in info_sample, "info_sample must contain 'n_epochs'." + + train_scores.append(info_sample['mean_score']) # so that mean can be computed + if self.n_iters % log_frequency == 0: + self.logger.log('Average train score', np.mean(train_scores), self.n_iters, color='blue') + self.log(info_update, prefix="Update: ") + self.log(info_sample, prefix="Sample: ") + self.n_samples += len(samples) # update the number of samples processed + self.logger.log('Number of samples', self.n_samples, self.n_iters, color='blue') + # Log parameters + for p in self.agent.parameters(): + self.logger.log(f"Parameter: {p.name}", p.data, self.n_iters, color='red') + + # Update counters + self.n_epochs = info_sample['n_epochs'] # update the number of epochs completed + self.n_iters += 1 + return + + # Can be overridden by subclasses to implement specific sampling strategies + def sample(self, agents, verbose=False, **kwargs): + """ Sample a batch of data based on the proposed parameters. All proposals are evaluated on the same batch of inputs. + + Args: + agents (list): A list of trace.Modules (proposed parameters) to evaluate. + **kwargs: Additional keyword arguments that may be used by the implementation. + """ + samples = Samples(*self.train_sampler.sample(agents)) # create a Samples object to store the samples and the minibatch + + # Log information about the sampling + scores = [ g.get_scores() for g in samples.samples] # list of list of scores for each RolloutsGraph + scores = [item for sublist in scores for item in sublist] # flatten the list of scores + log_info = { + 'mean_score': np.mean(scores), + 'n_epochs': self.train_sampler.n_epochs, + } + return samples, log_info + + def log(self, info_log, prefix=""): + """ Log the information from the algorithm. """ + for key, value in info_log.items(): + try: + if value is not None: + self.logger.log(f"{prefix}{key}", value, self.n_iters) + except Exception as e: + print(e) + + def test(self, test_dataset, guide): + min_score = self.score_range[0] + # Test the agent's performance + test_score = self.evaluate(self.agent, guide, test_dataset['inputs'], test_dataset['infos'], + min_score=min_score, num_threads=self.num_threads, + description=f"Evaluating agent (iteration {self.n_iters})") # and log + return {'test_score': test_score} + + def save(self, save_path): + self.save_agent(save_path, self.n_iters) + # TODO save full state of self + + # Unimplemented methods that should be implemented by subclasses + def update(self, samples=None, verbose=False, **kwargs): + """ Update the agent based on the provided samples. + Args: + samples (list): A list of samples from the previous iteration. If None, the agent's parameters are returned without updating. + verbose (bool, optional): Whether to print verbose output. Defaults to False. + **kwargs: Additional keyword arguments that may be used by the implementation. + Returns: + update_dict (dict of Parameter: Any): A dictionary containing the updated parameters of the agent. + proposals (list of trace.Module): A list of proposed parameters (trace.Module) after the update. + info_log (dict of str: Any): A dictionary containing logging information about the update process. + + This method updates the agent's parameters based on samples of the training dataset and validation dataset (provided by self.get_validate_dataset). + In addition, it return new agents (proposals) that can be used for collecting data for the next iteration. + """ + raise NotImplementedError("The update method should be implemented by subclasses.") + # return update_dict, proposals, info_log From 9569b1236711a372e7df2ad8752e9928a6915896 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 24 Jul 2025 22:52:32 +0000 Subject: [PATCH 135/314] Add print statements --- .../algorithms/priority_search/priority_search.py | 15 ++++++++++----- .../algorithms/priority_search/search_template.py | 6 +++++- opto/trainer/sampler.py | 1 + tests/unit_tests/test_sampler.py | 2 +- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/opto/trainer/algorithms/priority_search/priority_search.py b/opto/trainer/algorithms/priority_search/priority_search.py index 10da4b89..f1c53b64 100644 --- a/opto/trainer/algorithms/priority_search/priority_search.py +++ b/opto/trainer/algorithms/priority_search/priority_search.py @@ -235,7 +235,7 @@ def propose(self, samples, verbose=False, **kwargs): Returns: candidates (list of ModuleCandidate): A list of proposed candidates for the next iteration. """ - + print("--- Proposing new parameters...") if verbose else None assert isinstance(samples, Samples), "samples must be an instance of Samples." samples = samples.samples # list of RolloutsGraph objects n_proposals = self.num_proposals # number of proposals to generate per optimizer @@ -276,10 +276,11 @@ def _step(optimizer): return update_dict # return the proposed parameters args_list = [(o,) for o in optimizers ] * n_proposals # repeat args_list n_proposals times + assert len(args_list) == n_subgraphs * n_proposals, "args_list must have length n_subgraphs * n_proposals" update_dicts = async_run([_step]*n_subgraphs*n_proposals, # run the optimizer step for each agent in parallel args_list=args_list, max_workers=self.num_threads, # use the number of threads specified in the class - description="Running optimizers on samples") + description=f"Running optimizers to generate {n_proposals} proposals for each of {n_subgraphs} sub batches",) # update_dicts is a list of dicts of length n_agents * n_proposals # Create ModuleCandidate objects for each proposed update_dict @@ -296,6 +297,7 @@ def validate(self, candidates, samples, verbose=False, **kwargs): Returns: results (dict): A dictionary where the keys are ids of ModuleCandidate objects and the values are ModuleCandidate and lists of rollouts (list of dicts) containing the module, x, info, target, score, feedback. """ + print("--- Validating candidates...") if verbose else None # Get the validation dataset from the samples. If no validation dataset is provided, use the current batch. if self._validate_dataset is None: @@ -358,19 +360,20 @@ def validate(self, candidates, samples, verbose=False, **kwargs): - def update_memory(self, validate_results, **kwargs): + def update_memory(self, validate_results, verbose: bool = False, **kwargs): """ Update the priority queue with the validation results. Args: validate_results (dict): A dictionary where the keys are ModuleCandidate objects and the values are lists of rollouts (list of dicts) containing the module, x, info, target, score, feedback. **kwargs: Additional keyword arguments that may be used by the implementation. """ + print("--- Updating memory with validation results...") if verbose else None for candidate, rollouts in validate_results.items(): candidate.add_rollouts(rollouts) # add the rollouts to the candidate score = self.compute_score(candidate) # compute the score for the candidate self.memory.push(score, candidate) #### - def explore(self, **kwargs): + def explore(self, verbose: bool = False, **kwargs): """ Explore the parameter space and propose new candidates. Args: **kwargs: Additional keyword arguments that may be used by the implementation. @@ -378,6 +381,7 @@ def explore(self, **kwargs): list: A list of proposed candidates. dict: A dictionary containing logging information about the exploration. """ + print(f"--- Generating {min(len(self.memory), self.num_candidates)} exploration candidates...") if verbose else None # pop top self.num_candidates candidates from the priority queue top_candidates = [] while len(top_candidates) < self.num_candidates and self.memory: @@ -386,7 +390,7 @@ def explore(self, **kwargs): return top_candidates, {} - def exploit(self, **kwargs): + def exploit(self, verbose: bool = False, **kwargs): # NOTE This function can be overridden by subclasses to compute a different score """ Exploit the best candidate from the priority queue. This method should not change the priority queue. Args: @@ -394,6 +398,7 @@ def exploit(self, **kwargs): Returns: ModuleCandidate: The best candidate from the priority queue. """ + print("--- Exploiting the best candidate...") if verbose else None # Right now, we just return the best candidate from the priority queue # This function can be overridden by subclasses to implement a different exploitation strategy if not self.memory: diff --git a/opto/trainer/algorithms/priority_search/search_template.py b/opto/trainer/algorithms/priority_search/search_template.py index b5d2cb46..bb6b2228 100644 --- a/opto/trainer/algorithms/priority_search/search_template.py +++ b/opto/trainer/algorithms/priority_search/search_template.py @@ -48,6 +48,11 @@ def __iter__(self): def __len__(self): return sum(len(s) for s in self.samples) + @property + def n_sub_batches(self) -> int: + """ Number of sub-batches in the samples. """ + return len(self.samples) + class SearchTemplate(Minibatch): @@ -171,7 +176,6 @@ def sample(self, agents, verbose=False, **kwargs): **kwargs: Additional keyword arguments that may be used by the implementation. """ samples = Samples(*self.train_sampler.sample(agents)) # create a Samples object to store the samples and the minibatch - # Log information about the sampling scores = [ g.get_scores() for g in samples.samples] # list of list of scores for each RolloutsGraph scores = [item for sublist in scores for item in sublist] # flatten the list of scores diff --git a/opto/trainer/sampler.py b/opto/trainer/sampler.py index 3ffeb689..7cc50412 100644 --- a/opto/trainer/sampler.py +++ b/opto/trainer/sampler.py @@ -310,4 +310,5 @@ def sample(self, agents): min_score=self.score_range[0], description=description) + assert len(samples) == len(agents)*(batch_size // self.sub_batch_size + (1 if batch_size % self.sub_batch_size > 0 else 0)), f"Expected {len(agents)*(batch_size // self.sub_batch_size + (1 if batch_size % self.sub_batch_size > 0 else 0))} samples, got {len(samples)}" return samples, batch diff --git a/tests/unit_tests/test_sampler.py b/tests/unit_tests/test_sampler.py index d6fd6d16..fd9ceca4 100644 --- a/tests/unit_tests/test_sampler.py +++ b/tests/unit_tests/test_sampler.py @@ -2,7 +2,7 @@ from opto.trainer.sampler import Sampler from opto.trainer.loader import DataLoader from opto.trainer.guide import AutoGuide -from opto.trainer.algorithms.search_algorithms import is_node_copy +from opto.trainer.algorithms.priority_search.utils import is_node_copy class Guide(AutoGuide): From e0864bb1b0755b979c63fd1c2cf8ca05cc923ca5 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 24 Jul 2025 23:08:16 +0000 Subject: [PATCH 136/314] Update printing --- opto/trainer/algorithms/priority_search/priority_search.py | 6 +++--- opto/trainer/algorithms/priority_search/search_template.py | 4 ++-- opto/trainer/sampler.py | 5 ++--- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/opto/trainer/algorithms/priority_search/priority_search.py b/opto/trainer/algorithms/priority_search/priority_search.py index f1c53b64..39768921 100644 --- a/opto/trainer/algorithms/priority_search/priority_search.py +++ b/opto/trainer/algorithms/priority_search/priority_search.py @@ -280,7 +280,7 @@ def _step(optimizer): update_dicts = async_run([_step]*n_subgraphs*n_proposals, # run the optimizer step for each agent in parallel args_list=args_list, max_workers=self.num_threads, # use the number of threads specified in the class - description=f"Running optimizers to generate {n_proposals} proposals for each of {n_subgraphs} sub batches",) + description=f"Calling optimizers: Generating {n_proposals} proposals for each of {n_subgraphs} sub batches",) # update_dicts is a list of dicts of length n_agents * n_proposals # Create ModuleCandidate objects for each proposed update_dict @@ -307,7 +307,7 @@ def validate(self, candidates, samples, verbose=False, **kwargs): self.validate_sampler.batch_size = len(validate_dataset['inputs']) # set the batch size to the number of inputs in the validation dataset candidate_agents = [c.get_module() for c in candidates] # get the modules from the candidates - validate_samples = Samples(*self.validate_sampler.sample(candidate_agents)) # list of RolloutsGraph objects + validate_samples = Samples(*self.validate_sampler.sample(candidate_agents, description_prefix='Validating newly proposed candidates: ')) # list of RolloutsGraph objects exploration_candidates = self._exploration_candidates # exploration candidates from the previous iteration @@ -319,7 +319,7 @@ def validate(self, candidates, samples, verbose=False, **kwargs): else: # validate the agents in the validate_dataset # exploration_agents = [rollouts.module for rollouts in samples.samples] # NOTE this might contain some duplicates due to sub_batch_size < batch_size exploitation_agents = [c.get_module() for c in exploration_candidates] # get the modules from the exploration candidates - exploration_samples = Samples(*self.validate_sampler.sample(exploration_agents)) # sample the exploration agents + exploration_samples = Samples(*self.validate_sampler.sample(exploration_agents, description_prefix='Validating exploration candidates: ')) # sample the exploration agents validate_samples.add_samples(exploration_samples) # append the exploration samples to the validate_samples diff --git a/opto/trainer/algorithms/priority_search/search_template.py b/opto/trainer/algorithms/priority_search/search_template.py index bb6b2228..e37f8cff 100644 --- a/opto/trainer/algorithms/priority_search/search_template.py +++ b/opto/trainer/algorithms/priority_search/search_template.py @@ -175,7 +175,7 @@ def sample(self, agents, verbose=False, **kwargs): agents (list): A list of trace.Modules (proposed parameters) to evaluate. **kwargs: Additional keyword arguments that may be used by the implementation. """ - samples = Samples(*self.train_sampler.sample(agents)) # create a Samples object to store the samples and the minibatch + samples = Samples(*self.train_sampler.sample(agents, description_prefix='Sampling training minibatch: ')) # create a Samples object to store the samples and the minibatch # Log information about the sampling scores = [ g.get_scores() for g in samples.samples] # list of list of scores for each RolloutsGraph scores = [item for sublist in scores for item in sublist] # flatten the list of scores @@ -199,7 +199,7 @@ def test(self, test_dataset, guide): # Test the agent's performance test_score = self.evaluate(self.agent, guide, test_dataset['inputs'], test_dataset['infos'], min_score=min_score, num_threads=self.num_threads, - description=f"Evaluating agent (iteration {self.n_iters})") # and log + description=f"Evaluating agent") # and log return {'test_score': test_score} def save(self, save_path): diff --git a/opto/trainer/sampler.py b/opto/trainer/sampler.py index 7cc50412..4928a390 100644 --- a/opto/trainer/sampler.py +++ b/opto/trainer/sampler.py @@ -234,12 +234,11 @@ def n_epochs(self): """ Get the number of epochs of the loader. """ return self.loader.n_epochs - def sample(self, agents): + def sample(self, agents, description_prefix=''): """ Sample a batch of data from the loader and evaluate the agents. Args: agents (list): A list of trace.Modules (proposed parameters) to evaluate. - **kwargs: Additional keyword arguments that may be used by the implementation. Returns: batch (dict): @@ -303,7 +302,7 @@ def sample(self, agents): configs.append(RolloutConfig(module=agent, xs=_xs, infos=_infos, guide=self.guide)) # Sample rollouts using the configs - description = f"Sampling {len(agents)} agents on {batch_size} inputs" + description = description_prefix + f"Sampling {len(agents)} agents on {batch_size} inputs" samples = sample_rollouts(configs, forward=self.forward, num_threads=self.num_threads, From f5b3b9e38480b04d55a2200ebd5895bf6a05d8a2 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 24 Jul 2025 23:10:05 +0000 Subject: [PATCH 137/314] Update to use pytest --- tests/unit_tests/test_priority_search.py | 59 +++++++++++++----------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/tests/unit_tests/test_priority_search.py b/tests/unit_tests/test_priority_search.py index 7c073156..47d90fb1 100644 --- a/tests/unit_tests/test_priority_search.py +++ b/tests/unit_tests/test_priority_search.py @@ -62,7 +62,7 @@ class PrioritySearch(_PrioritySearch): # This class is for testing the PrioritySearch algorithm def propose(self, samples, verbose=False, n_proposals=1, **kwargs): - print("Propose at iteration:", self.n_iters) + print("[UnitTest] Propose at iteration:", self.n_iters) # assert len(samples) == batch_size, f"Expected {batch_size} samples, got {len(samples)}" # assert len(samples) == len(agents) * np.ceil(batch_size / self.sub_batch_size), f"Expected {len(agents) * np.ceil(batch_size / self.sub_batch_size)} samples, got {len(samples)}" @@ -74,7 +74,7 @@ def propose(self, samples, verbose=False, n_proposals=1, **kwargs): return candidates def validate(self, candidates, samples, verbose=False, **kwargs): - print("Validate at iteration:", self.n_iters) + print("[UnitTest] Validate at iteration:", self.n_iters) assert len(candidates) == np.ceil(batch_size / sub_batch_size) * self.num_proposals, f"Expected {np.ceil(batch_size / sub_batch_size) * self.num_proposals} candidates, got {len(candidates)}" validate_results = super().validate(candidates, samples, verbose=verbose, **kwargs) @@ -87,15 +87,14 @@ def validate(self, candidates, samples, verbose=False, **kwargs): return validate_results def exploit(self, **kwargs): - print("Exploit at iteration:", self.n_iters) - + print("[UnitTest] Exploit at iteration:", self.n_iters) candidate, info_dict = super().exploit(**kwargs) assert isinstance(candidate, ModuleCandidate), "Expected candidate to be an instance of ModuleCandidate" assert isinstance(info_dict, dict), "Expected info_dict to be a dictionary" return candidate, info_dict def explore(self, **kwargs): - print("Explore at iteration:", self.n_iters) + print("[UnitTest] Explore at iteration:", self.n_iters) candidates, info_dict = super().explore(**kwargs) assert isinstance(candidates, list) @@ -107,7 +106,6 @@ def explore(self, **kwargs): num_candidates = min(self.num_candidates, 2) # in this example, memory will contain at most 2 unique candidates assert len(candidates) == num_candidates, f"Expected {num_candidates} candidates at iter {self.n_iters}, got {len(candidates)}" assert all(isinstance(c, ModuleCandidate) for c in candidates), "All candidates should be ModuleCandidate instances" - return candidates, info_dict @@ -133,26 +131,31 @@ def _llm_callable(messages, **kwargs): """ -dummy_llm = DummyLLM(_llm_callable) -agent = Agent() -optimizer = OptoPrimeV2( - agent.parameters(), +def test_priority_search(): + """ + Test the PrioritySearch algorithm with a dummy LLM and a simple agent. + """ + # Create a dummy LLM and an agent + dummy_llm = DummyLLM(_llm_callable) + agent = Agent() + optimizer = OptoPrimeV2( + agent.parameters(), llm=dummy_llm, -) - -algo = PrioritySearch( - agent, - optimizer, -) - -algo.train( - guide=Guide(), - train_dataset=dataset, - batch_size=batch_size, - sub_batch_size=sub_batch_size, - num_threads=num_threads, - num_candidates=num_candidates, - num_proposals=num_proposals, - memory_size=memory_size, - verbose=False, -) + ) + + algo = PrioritySearch( + agent, + optimizer, + ) + + algo.train( + guide=Guide(), + train_dataset=dataset, + batch_size=batch_size, + sub_batch_size=sub_batch_size, + num_threads=num_threads, + num_candidates=num_candidates, + num_proposals=num_proposals, + memory_size=memory_size, + verbose=False, #'output', + ) From 804a83c19b0a519585289714a4152211060a2efa Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 24 Jul 2025 23:10:44 +0000 Subject: [PATCH 138/314] Rename example --- ...k_search_algo.py => priority_search_example.py} | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) rename examples/{gsm8k_search_algo.py => priority_search_example.py} (87%) diff --git a/examples/gsm8k_search_algo.py b/examples/priority_search_example.py similarity index 87% rename from examples/gsm8k_search_algo.py rename to examples/priority_search_example.py index 3e21f7cf..7928a3d3 100644 --- a/examples/gsm8k_search_algo.py +++ b/examples/priority_search_example.py @@ -54,11 +54,14 @@ def main(): # set seed seed = 42 num_epochs = 1 - batch_size = 3 - sub_batch_size = 2 + batch_size = 3 # number of queries to sample from the training data + sub_batch_size = 2 # number of queries each optimizer sees + num_proposals = 3 # number of proposals to generate for each query + num_candidates = 2 # number of candidates for exploration score_range = (0, 1) # range of the score for the guide eval_frequency = -1 num_eval_samples = 2 + num_threads = 10 datasize = 5 verbose = True @@ -66,12 +69,13 @@ def main(): student_model = None # use default model optimizer_model = None # use default model + np.random.seed(seed) # In this example, we use the GSM8K dataset, which is a dataset of math word problems. # We will look the training error of the agent on a small portion of this dataset. - train_dataset = datasets.load_dataset('openai/gsm8k', 'main')['train'][:datasize] - train_dataset = dict(inputs=train_dataset['question'], infos=train_dataset['answer']) + train_dataset = datasets.load_dataset('BBEH/bbeh')['train'][:datasize] + train_dataset = dict(inputs=train_dataset['input'], infos=train_dataset['target']) test_dataset = train_dataset agent = Learner(llm=LLM(student_model)) @@ -93,6 +97,8 @@ def main(): test_dataset=test_dataset, num_threads=num_threads, sub_batch_size=sub_batch_size, + num_proposals=num_proposals, + num_candidates=num_candidates, score_range=score_range, num_eval_samples=num_eval_samples, verbose='output' if verbose else False) From d2ba017214e066f31da29e041a8b1c603090f49b Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 24 Jul 2025 23:23:33 +0000 Subject: [PATCH 139/314] Add docstring --- .../priority_search/priority_search.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/opto/trainer/algorithms/priority_search/priority_search.py b/opto/trainer/algorithms/priority_search/priority_search.py index 39768921..d63d8b6f 100644 --- a/opto/trainer/algorithms/priority_search/priority_search.py +++ b/opto/trainer/algorithms/priority_search/priority_search.py @@ -11,7 +11,6 @@ from opto.trainer.algorithms.priority_search.utils import set_module_parameters, remap_update_dict, create_module_from_update_dict -# TODO make this hashable? class ModuleCandidate: """ A container used by PrioritySearch to store a candidate module as (its base module and update dictionary) and its statistics. """ @@ -138,7 +137,21 @@ def best(self): class PrioritySearch(SearchTemplate): - """ A search algorithm that uses a priority queue to explore the parameter space and propose new candidates. """ + """ A search algorithm that uses a priority queue to explore the parameter space and propose new candidates. + + It provides a scalable template for implementing search algorithms based on asynchronous generation, validation, and testing. + In each iteration, + 1. It proposes a best agent and a set of `num_candidates` exploration agents that have the highest scores in the priority queue. + 2. The best agent is tested for performance if eval_frequency is met. + 3. A minibatch of `batch_size` samples are drawn from the training dataset, and the exploration agents are run on the samples. This creates a set of agent rollouts, where each rollout contains the agent module, input, info, target, score, and feedback. For each agent, rollouts of size `sub_batch_size` are grouped together as a connected subgraph (represented as the RolloutsGraph object). In total, this step creates `num_subgraphs = num_candidates * ceil(batch_size / sub_batch_size)` subgraphs. + 4. Optimizer is run on each subgraph to propose new parameters for the agents. `num_proposals` proposals are generated for each subgraph. This results in `num_subgraphs * num_proposals` total proposals. + 5. The proposed parameters are validated by running the agents on the validation dataset, which can be the current batch or a separate validation dataset when provided. When validate_proposals is set to True, the exploration candidates are also validated. + 6. The validation results are used to update the priority queue, which stores the candidates and their scores. The candidates are stored as ModuleCandidate objects, which contain the base module, update dictionary, and rollouts (i.e. raw statistics of the candidate). + + This algorithm template can be subclassed to implement specific search algorithms by overriding the `exploit`, `explore`, and `compute_score` methods. + The `exploit` method is used to select the best candidate from the priority queue, the `explore` method is used to generate new candidates from the priority queue, and + the `compute_score` method is used to compute the score for ranking in the priority queue. + """ def train(self, guide, # guide to provide feedback From ed818bd7b0ed6af1bab1d0512cb13db03e785f3b Mon Sep 17 00:00:00 2001 From: chinganc Date: Fri, 25 Jul 2025 00:26:53 +0000 Subject: [PATCH 140/314] Add examples of priority search. --- .../algorithms/priority_search/__init__.py | 3 +- .../algorithms/priority_search/examples.py | 214 ++++++++++++++++++ .../priority_search/priority_search.py | 4 +- opto/trainer/sampler.py | 1 + 4 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 opto/trainer/algorithms/priority_search/examples.py diff --git a/opto/trainer/algorithms/priority_search/__init__.py b/opto/trainer/algorithms/priority_search/__init__.py index 68fe26c0..caaf664f 100644 --- a/opto/trainer/algorithms/priority_search/__init__.py +++ b/opto/trainer/algorithms/priority_search/__init__.py @@ -1 +1,2 @@ -from opto.trainer.algorithms.priority_search.priority_search import PrioritySearch \ No newline at end of file +from opto.trainer.algorithms.priority_search.priority_search import PrioritySearch +from opto.trainer.algorithms.priority_search.examples import SequentialUpdate, SequentialSearch, BeamSearch \ No newline at end of file diff --git a/opto/trainer/algorithms/priority_search/examples.py b/opto/trainer/algorithms/priority_search/examples.py new file mode 100644 index 00000000..90f6cb14 --- /dev/null +++ b/opto/trainer/algorithms/priority_search/examples.py @@ -0,0 +1,214 @@ + +from opto.trainer.algorithms.priority_search import PrioritySearch +from typing import Union, Optional + +# Below we define several algorithms that use the PrioritySearch class. + + +class SequentialUpdate(PrioritySearch): + """ A basic algorithm that explores the parameter space and proposes new candidates one by one. + + This is realized by setting + + num_candidates = 1 + num_proposals = 1 + memory_size = 1 + + This is the same as MinibatchAlgorithm when + 1. no validation set is provided + 2. sub_batch_size is None or batch_size. + + validate_proposals here acts the same as `ensure_improvement` flag in MinibatchAlgorithm + """ + + def train(self, + guide, # guide to provide feedback + train_dataset, # dataset of (x, info) pairs to train the agent + *, + # validation + validate_dataset = None, # same format as train_dataset; if None use the current batch. + validate_guide = None, # to provide scores for the validation set + # training loop + batch_size = 1, # batch size for updating the agent + sub_batch_size = None, # sub-batch size that each optimizer attends to + score_range = None, # minimum score to update the agent + num_epochs = 1, # number of training epochs + num_threads = None, # maximum number of threads to use + verbose = False, # whether to print the output of the agent + # evaluation + test_dataset = None, # dataset of (x, info) pairs to evaluate the agent + test_frequency: Union[int, None] = 1, # frequency of evaluation + num_eval_samples: int = 1, # number of samples to use to evaluate each input + # logging + log_frequency = None, # frequency of logging + save_frequency: Union[int, None] = None, # frequency of saving the agent + save_path: str = "checkpoints/agent.pkl", # path to save the agent + # Priority Search specific parameters + num_candidates: int = 10, # number of candidates to propose for exploration + num_proposals: int = 1, # number of proposals to generate per optimizer + default_score: float = float('inf'), # default score assigned to priority queue candidates + validate_proposals: bool = True, # whether to validate the proposed parameters + memory_size: Optional[int] = None, # size of the heap memory to store the candidates; if None, no limit is set + # Additional keyword arguments + **kwargs + ): + + num_candidates = 1 # SequentialSearch only proposes one candidate at a time + num_proposals = 1 # SequentialSearch only generates one proposal at a time + memory_size = 1 # SequentialSearch only stores one candidate at a time in the heap memory + # validate_proposals is the same as `ensure_improvement` flag in MinibatchAlgorithm + + return super().train(guide, train_dataset, + validate_dataset=validate_dataset, + validate_guide=validate_guide, + batch_size=batch_size, + sub_batch_size=sub_batch_size, + score_range=score_range, + num_epochs=num_epochs, + num_threads=num_threads, + verbose=verbose, + test_dataset=test_dataset, + test_frequency=test_frequency, + num_eval_samples=num_eval_samples, + log_frequency=log_frequency, + save_frequency=save_frequency, + save_path=save_path, + num_candidates=num_candidates, + num_proposals=num_proposals, + default_score=default_score, + validate_proposals=validate_proposals, + memory_size=memory_size, **kwargs) + + +class SequentialSearch(PrioritySearch): + """ A sequential search that generates one candidate in each iteration by validating multiple proposals. + + This is realized by setting + num_proposals = 1 + memory_size = 1 + + This is the same as BasicSearchAlgorithm when + 1. a validation set is provided + 2. validate_proposals is True. + 3. sub_batch_size is None or batch_size. + """ + + def train(self, + guide, # guide to provide feedback + train_dataset, # dataset of (x, info) pairs to train the agent + *, + # validation + validate_dataset = None, # same format as train_dataset; if None use the current batch. + validate_guide = None, # to provide scores for the validation set + # training loop + batch_size = 1, # batch size for updating the agent + sub_batch_size = None, # sub-batch size that each optimizer attends to + score_range = None, # minimum score to update the agent + num_epochs = 1, # number of training epochs + num_threads = None, # maximum number of threads to use + verbose = False, # whether to print the output of the agent + # evaluation + test_dataset = None, # dataset of (x, info) pairs to evaluate the agent + test_frequency: Union[int, None] = 1, # frequency of evaluation + num_eval_samples: int = 1, # number of samples to use to evaluate each input + # logging + log_frequency = None, # frequency of logging + save_frequency: Union[int, None] = None, # frequency of saving the agent + save_path: str = "checkpoints/agent.pkl", # path to save the agent + # Priority Search specific parameters + num_candidates: int = 10, # number of candidates to propose for exploration + num_proposals: int = 1, # number of proposals to generate per optimizer + default_score: float = float('inf'), # default score assigned to priority queue candidates + validate_proposals: bool = True, # whether to validate the proposed parameters + memory_size: Optional[int] = None, # size of the heap memory to store the candidates; if None, no limit is set + # Additional keyword arguments + **kwargs + ): + + num_candidates = 1 # SequentialSearch only generates one candidate at a time + memory_size = 1 # MultiSequentialUpdate only stores one candidate at a time in the heap memory + # validate_proposals is the same as `ensure_improvement` flag in MinibatchAlgorithm + + return super().train(guide, train_dataset, + validate_dataset=validate_dataset, + validate_guide=validate_guide, + batch_size=batch_size, + sub_batch_size=sub_batch_size, + score_range=score_range, + num_epochs=num_epochs, + num_threads=num_threads, + verbose=verbose, + test_dataset=test_dataset, + test_frequency=test_frequency, + num_eval_samples=num_eval_samples, + log_frequency=log_frequency, + save_frequency=save_frequency, + save_path=save_path, + num_candidates=num_candidates, + num_proposals=num_proposals, + default_score=default_score, + validate_proposals=validate_proposals, + memory_size=memory_size, **kwargs) + +class BeamSearch(PrioritySearch): + """ A beam search algorithm that explores the parameter space and proposes new candidates based on the best candidates in the priority queue. + + This is realized by setting + num_proposals = beam_size + memory_size = beam_size + + """ + + def train(self, + guide, # guide to provide feedback + train_dataset, # dataset of (x, info) pairs to train the agent + *, + # validation + validate_dataset = None, # same format as train_dataset; if None use the current batch. + validate_guide = None, # to provide scores for the validation set + # training loop + batch_size = 1, # batch size for updating the agent + sub_batch_size = None, # sub-batch size that each optimizer attends to + score_range = None, # minimum score to update the agent + num_epochs = 1, # number of training epochs + num_threads = None, # maximum number of threads to use + verbose = False, # whether to print the output of the agent + # evaluation + test_dataset = None, # dataset of (x, info) pairs to evaluate the agent + test_frequency: Union[int, None] = 1, # frequency of evaluation + num_eval_samples: int = 1, # number of samples to use to evaluate each input + # logging + log_frequency = None, # frequency of logging + save_frequency: Union[int, None] = None, # frequency of saving the agent + save_path: str = "checkpoints/agent.pkl", # path to save the agent + # Priority Search specific parameters + num_candidates: int = 10, # number of candidates to propose for exploration + num_proposals: int = 1, # number of proposals to generate per optimizer; this is beam_size in beam search. + default_score: float = float('inf'), # default score assigned to priority queue candidates + validate_proposals: bool = True, # whether to validate the proposed parameters + memory_size: Optional[int] = None, # size of the heap memory to store the candidates; if None, no limit is set + **kwargs): + + # num_candidates acts as the beam size in beam search. + memory_size = num_candidates + + return super().train(guide, train_dataset, + validate_dataset=validate_dataset, + validate_guide=validate_guide, + batch_size=batch_size, + sub_batch_size=sub_batch_size, + score_range=score_range, + num_epochs=num_epochs, + num_threads=num_threads, + verbose=verbose, + test_dataset=test_dataset, + test_frequency=test_frequency, + num_eval_samples=num_eval_samples, + log_frequency=log_frequency, + save_frequency=save_frequency, + save_path=save_path, + num_candidates=num_candidates, # beam size + num_proposals=num_proposals, # number of proposals to generate per optimizer + default_score=default_score, + validate_proposals=validate_proposals, + memory_size=memory_size, **kwargs) diff --git a/opto/trainer/algorithms/priority_search/priority_search.py b/opto/trainer/algorithms/priority_search/priority_search.py index d63d8b6f..8d267bf4 100644 --- a/opto/trainer/algorithms/priority_search/priority_search.py +++ b/opto/trainer/algorithms/priority_search/priority_search.py @@ -151,6 +151,8 @@ class PrioritySearch(SearchTemplate): This algorithm template can be subclassed to implement specific search algorithms by overriding the `exploit`, `explore`, and `compute_score` methods. The `exploit` method is used to select the best candidate from the priority queue, the `explore` method is used to generate new candidates from the priority queue, and the `compute_score` method is used to compute the score for ranking in the priority queue. + + By default, `compute_score` computes the mean score of the rollouts. `exploit` simply returns the best candidate from the priority queue, and `explore` generates the top `num_candidates` candidates from the priority queue. """ def train(self, @@ -442,4 +444,4 @@ def compute_score(self, candidate): scores = [r['score'] for r in candidate.rollouts] default_score = self.default_score if self.default_score is not None else self.score_range[1] # default score for the candidates - return np.mean(scores) if scores else self.default_score + return np.mean(scores) if scores else self.default_score \ No newline at end of file diff --git a/opto/trainer/sampler.py b/opto/trainer/sampler.py index 4928a390..3d46ea05 100644 --- a/opto/trainer/sampler.py +++ b/opto/trainer/sampler.py @@ -310,4 +310,5 @@ def sample(self, agents, description_prefix=''): description=description) assert len(samples) == len(agents)*(batch_size // self.sub_batch_size + (1 if batch_size % self.sub_batch_size > 0 else 0)), f"Expected {len(agents)*(batch_size // self.sub_batch_size + (1 if batch_size % self.sub_batch_size > 0 else 0))} samples, got {len(samples)}" + return samples, batch From 8b1e06c6019f557e8628e4d55674198df40bdac6 Mon Sep 17 00:00:00 2001 From: chinganc Date: Fri, 25 Jul 2025 02:45:15 +0000 Subject: [PATCH 141/314] Fix a bug in of deleting keys in default_json_keys --- opto/optimizers/optoprime.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/opto/optimizers/optoprime.py b/opto/optimizers/optoprime.py index 6cbca909..fe7a6e49 100644 --- a/opto/optimizers/optoprime.py +++ b/opto/optimizers/optoprime.py @@ -258,7 +258,7 @@ class OptoPrime(Optimizer): final_prompt_with_variables = dedent( """ What are your suggestions on variables {names}? - + Your response: """ ) @@ -333,13 +333,14 @@ def __init__( if prompt_symbols is not None: self.prompt_symbols.update(prompt_symbols) if json_keys is not None: - self.default_json_keys.update(json_keys) - if self.default_json_keys['answer'] is None: # answer field is not needed - del self.default_json_keys['answer'] - if 'answer' not in self.default_json_keys: + self.default_json_keys.update(json_keys) + # if self.default_json_keys['answer'] is None: + # del self.default_json_keys['answer'] + # NOTE del cause KeyError if the key is not in the dict due to changing class attribute + if 'answer' not in self.default_json_keys or self.default_json_keys['answer'] is None: # answer field is not needed # If 'answer' is not in the json keys, we use the no-answer format self.output_format_prompt = self.output_format_prompt_no_answer.format(**self.default_json_keys) - else: # If 'answer' is in the json keys, we use the original format of OptoPrime + else: # If 'answer' is in the json keys, we use the original format of OptoPrime self.output_format_prompt = self.output_format_prompt_original.format(**self.default_json_keys) self.use_json_object_format = use_json_object_format self.highlight_variables = highlight_variables @@ -450,8 +451,8 @@ def construct_prompt(self, summary, mask=None, *args, **kwargs): ) + user_prompt ) - - + + if self.highlight_variables: var_names = [] for k, v in summary.variables.items(): @@ -618,13 +619,13 @@ def call_llm( {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, ] - + response_format = {"type": "json_object"} if self.use_json_object_format else None try: # Try tp force it to be a json object response = self.llm(messages=messages, max_tokens=max_tokens, response_format=response_format) except Exception: response = self.llm(messages=messages, max_tokens=max_tokens) - + response = response.choices[0].message.content if verbose: From fa45b758a8c495c66819f8fa810271526b023a8a Mon Sep 17 00:00:00 2001 From: chinganc Date: Fri, 25 Jul 2025 21:03:11 +0000 Subject: [PATCH 142/314] Add use_best_candidate_to_explore flag (True as default). --- .../priority_search/priority_search.py | 21 ++++++++++-------- tests/unit_tests/test_priority_search.py | 22 ++++++++++++------- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/opto/trainer/algorithms/priority_search/priority_search.py b/opto/trainer/algorithms/priority_search/priority_search.py index 8d267bf4..154494a9 100644 --- a/opto/trainer/algorithms/priority_search/priority_search.py +++ b/opto/trainer/algorithms/priority_search/priority_search.py @@ -181,7 +181,8 @@ def train(self, num_candidates: int = 10, # number of candidates to propose for exploration num_proposals: int = 1, # number of proposals to generate per optimizer default_score: float = float('inf'), # default score assigned to priority queue candidates - validate_proposals: bool = True, # whether to validate the proposed parameters + validate_proposals: bool = True, # whether to validate the proposed parameters for exploration + use_best_candidate_to_explore: bool = True, # whether to use the best candidate as part of the exploration candidates memory_size: Optional[int] = None, # size of the heap memory to store the candidates; if None, no limit is set # Additional keyword arguments **kwargs @@ -191,6 +192,7 @@ def train(self, self.num_candidates = num_candidates # number of candidates to propose by each optimizer call self.num_proposals = num_proposals self.validate_proposals = validate_proposals # whether to validate the proposed parameters + self.use_best_candidate_to_explore = use_best_candidate_to_explore self.default_score = default_score self.memory = HeapMemory(size=memory_size) # Initialize the heap memory with a size limit self.memory.push(self.default_score, ModuleCandidate(self.agent)) # Push the base agent as the first candidate @@ -224,19 +226,18 @@ def update(self, samples=None, verbose=False, **kwargs): # 3. Update the priority queue with the validation results self.update_memory(validate_results, verbose=verbose, **kwargs) # samples are provided here in case candidates do not capture full information # 4. Explore and exploit the priority queue - best_candidate, info_exploit = self.exploit(verbose=verbose, **kwargs) # get the best candidate (ModuleCandidate) from the priority queue - exploration_candidates, info_explore = self.explore(verbose=verbose, **kwargs) # List of ModuleCandidates + self._best_candidate, info_exploit = self.exploit(verbose=verbose, **kwargs) # get the best candidate (ModuleCandidate) from the priority queue + self._exploration_candidates, info_explore = self.explore(verbose=verbose, **kwargs) # List of ModuleCandidates - self._exploration_candidates = exploration_candidates # TODO Log information about the update info_log = { - 'best_candidate_score': best_candidate.score(), - 'num_exploration_candidates': len(exploration_candidates), + 'best_candidate_score': self._best_candidate.score(), + 'num_exploration_candidates': len(self._exploration_candidates), } info_log.update(info_exploit) # add the info from the exploit step info_log.update(info_explore) # add the info from the explore step - return best_candidate.update_dict, [c.get_module() for c in exploration_candidates], info_log + return self._best_candidate.update_dict, [c.get_module() for c in self._exploration_candidates], info_log def propose(self, samples, verbose=False, **kwargs): """ Analyzing samples and propose new parameters using self.optimizer. An independent optimizer is used for the minibatch generated by one agent and generates n_proposals proposals. @@ -374,7 +375,6 @@ def validate(self, candidates, samples, verbose=False, **kwargs): return results - def update_memory(self, validate_results, verbose: bool = False, **kwargs): """ Update the priority queue with the validation results. Args: @@ -398,9 +398,12 @@ def explore(self, verbose: bool = False, **kwargs): """ print(f"--- Generating {min(len(self.memory), self.num_candidates)} exploration candidates...") if verbose else None # pop top self.num_candidates candidates from the priority queue - top_candidates = [] + top_candidates = [self._best_candidate] if self.use_best_candidate_to_explore else [] while len(top_candidates) < self.num_candidates and self.memory: score, candidate = self.memory.pop() # pop the top candidate from the priority queue + if self.use_best_candidate_to_explore: + if candidate == self._best_candidate: # skip if it is already in the top candidates + continue top_candidates.append(candidate) # add the candidate to the top candidates return top_candidates, {} diff --git a/tests/unit_tests/test_priority_search.py b/tests/unit_tests/test_priority_search.py index 47d90fb1..c1bf703b 100644 --- a/tests/unit_tests/test_priority_search.py +++ b/tests/unit_tests/test_priority_search.py @@ -9,6 +9,7 @@ import re import numpy as np +import copy class Guide(AutoGuide): @@ -63,26 +64,23 @@ class PrioritySearch(_PrioritySearch): def propose(self, samples, verbose=False, n_proposals=1, **kwargs): print("[UnitTest] Propose at iteration:", self.n_iters) - # assert len(samples) == batch_size, f"Expected {batch_size} samples, got {len(samples)}" - # assert len(samples) == len(agents) * np.ceil(batch_size / self.sub_batch_size), f"Expected {len(agents) * np.ceil(batch_size / self.sub_batch_size)} samples, got {len(samples)}" candidates = super().propose(samples, verbose=verbose, n_proposals=n_proposals, **kwargs) # In this example this will always be value 5 assert isinstance(candidates, list), "Expected candidates to be a list" assert all(isinstance(c, ModuleCandidate) for c in candidates), "All candidates should be ModuleCandidate instances" - assert len(candidates) == np.ceil(batch_size / sub_batch_size) * self.num_proposals, f"Expected {np.ceil(batch_size / sub_batch_size) * self.num_proposals} candidates, got {len(candidates)}" + assert len(candidates) == samples.n_sub_batches * self.num_proposals, f"Expected {samples.n_sub_batches * self.num_proposals} candidates, got {len(candidates)}" return candidates def validate(self, candidates, samples, verbose=False, **kwargs): print("[UnitTest] Validate at iteration:", self.n_iters) - assert len(candidates) == np.ceil(batch_size / sub_batch_size) * self.num_proposals, f"Expected {np.ceil(batch_size / sub_batch_size) * self.num_proposals} candidates, got {len(candidates)}" validate_results = super().validate(candidates, samples, verbose=verbose, **kwargs) assert isinstance(validate_results, dict), "Expected validate_results to be a dict" assert all(isinstance(v, ModuleCandidate) for v in validate_results.keys()), "All keys should be ModuleCandidate instances" keys = list(validate_results.keys()) # should contain one from exploration and one from exploitation - assert len(validate_results) == 2, "In this example, all proposals are the same, so we expect only two validate results." + # assert len(validate_results) == 2, "In this example, all proposals are the same, so we expect only two validate results." return validate_results @@ -91,6 +89,14 @@ def exploit(self, **kwargs): candidate, info_dict = super().exploit(**kwargs) assert isinstance(candidate, ModuleCandidate), "Expected candidate to be an instance of ModuleCandidate" assert isinstance(info_dict, dict), "Expected info_dict to be a dictionary" + + # XXX Here we simulate a different best candidate is given + assert self.use_best_candidate_to_explore, "Expected use_best_candidate_to_explore to be True in this unit test" + candidate = copy.deepcopy(candidate) # Ensure we return a copy + for p in candidate.base_module.parameters(): + candidate.update_dict[p] = p.data + 100 + # This will be different the exploration candidates + return candidate, info_dict def explore(self, **kwargs): @@ -101,10 +107,10 @@ def explore(self, **kwargs): assert isinstance(info_dict, dict) if self.n_iters == 0: - assert len(candidates) == 1, f"Expected 1 candidate, got {len(candidates)}" + assert len(candidates) == 2, f"Expected 2 candidates, got {len(candidates)}" + # one from the init parameter and one from the hacked best candidate else: - num_candidates = min(self.num_candidates, 2) # in this example, memory will contain at most 2 unique candidates - assert len(candidates) == num_candidates, f"Expected {num_candidates} candidates at iter {self.n_iters}, got {len(candidates)}" + assert len(candidates) <= self.num_candidates, f"Expect no more than {self.num_candidates} candidates at iter {self.n_iters}, got {len(candidates)}" assert all(isinstance(c, ModuleCandidate) for c in candidates), "All candidates should be ModuleCandidate instances" return candidates, info_dict From d9bb091a5f25b2a04aa0f57eda629d4352caeaaa Mon Sep 17 00:00:00 2001 From: chinganc Date: Fri, 25 Jul 2025 22:29:14 +0000 Subject: [PATCH 143/314] Add UCB score, update logging, and score_range attribute. --- examples/priority_search_example.py | 2 + .../priority_search/priority_search.py | 144 ++++++++++++++---- .../priority_search/search_template.py | 29 +++- 3 files changed, 145 insertions(+), 30 deletions(-) diff --git a/examples/priority_search_example.py b/examples/priority_search_example.py index 7928a3d3..4739ee0a 100644 --- a/examples/priority_search_example.py +++ b/examples/priority_search_example.py @@ -61,6 +61,7 @@ def main(): score_range = (0, 1) # range of the score for the guide eval_frequency = -1 num_eval_samples = 2 + score_function = 'mean' num_threads = 10 datasize = 5 @@ -101,6 +102,7 @@ def main(): num_candidates=num_candidates, score_range=score_range, num_eval_samples=num_eval_samples, + score_function=score_function, verbose='output' if verbose else False) diff --git a/opto/trainer/algorithms/priority_search/priority_search.py b/opto/trainer/algorithms/priority_search/priority_search.py index 154494a9..0a9f4aca 100644 --- a/opto/trainer/algorithms/priority_search/priority_search.py +++ b/opto/trainer/algorithms/priority_search/priority_search.py @@ -30,6 +30,9 @@ def __init__(self, self.update_dict = remap_update_dict(self.base_module, self.update_dict) self.rollouts = [] # list of dicts containing the rollout information (not RolloutsGraph, but a list of dicts) self.created_time = time.time() + self._n_updates = 0 # number of times this candidate has been updated + self._n_confidence_queries = 1 # number of times the confidence score has been queried + self._confidence_interval = None def get_module(self): """ Apply the update_dict to the base_module and return the updated module. @@ -81,19 +84,78 @@ def add_rollouts(self, rollouts: List[Dict[str, Any]]): "Each rollout must contain 'module', 'x', 'info', 'target', 'score', and 'feedback' keys." self.rollouts.extend(rollouts) + self._confidence_interval = None # reset the confidence interval + self._n_updates += 1 # increment the number of updates - def score(self): + def mean_score(self): """ Compute the score of the candidate based on the rollouts. """ if not self.rollouts: return None scores = [r['score'] for r in self.rollouts] return np.mean(scores) if scores else None + def compute_score_confidence(self, min_score, max_score, scaling_constant=1.0): + """Compute the UCB, mean, LCB score for the candidate. After queried, the number of confidence queries is incremented. + + UCB = mean_score + scaling_constant * sqrt(ln(total_trials) / candidate_trials) * (max_score - min_score) + UCB = clip(UCB, min_score, max_score) + + LCB = mean_score - scaling_constant * sqrt(ln(total_trials) / candidate_trials) * (max_score - min_score) + LCB = clip(LCB, min_score, max_score) + + Args: + candidate (ModuleCandidate): The candidate for which to compute the UCB score. + Returns: + float: The computed UCB score for the candidate. + """ + # Get scores from rollouts + scores = [r['score'] for r in self.rollouts] + + # If no rollouts, return a high exploration score to encourage trying this candidate + if not scores: + return min_score, None, max_score + + # Calculate mean score for this candidate + mean_score = np.mean(scores) + candidate_trials = len(scores) + + # Calculate how many times the confidence interval has been used to form a union bound + total_trials = min(self._n_confidence_queries) + 1 # this is an upper bound, since log(1) = 0 + + # Compute the exploration term based on Hoeffding's inequality + exploration_term = scaling_constant * np.sqrt(np.log(total_trials) / candidate_trials) * (max_score - min_score) + + # Calculate UCB score + ucb_score = mean_score + exploration_term + ucb_score = np.clip(ucb_score, min_score, max_score) + + # Calculate LCB score + lcb_score = mean_score - exploration_term + lcb_score = np.clip(lcb_score, min_score, max_score) + + self._n_confidence_queries += 1 # increment the number of confidence queries + + self._confidence_interval = dict(lcb_score=lcb_score, ucb_score=ucb_score, mean_score=mean_score) + return lcb_score, mean_score, ucb_score + + @property + def confidence_interval(self): + # This is a cached property that returns the confidence interval of the candidate. + # This is for accessing the confidence interval without increasing the number of confidence queries. E.g. this is useful when using both LCB and UCB of the same candidate. + if self._confidence_interval is None: + raise ValueError("Confidence interval has not been computed yet. Call compute_score_confidence() first.") + return self._confidence_interval + @property def num_rollouts(self): """ Return the number of rollouts collected for this candidate. """ return len(self.rollouts) + @property + def n_updates(self): + """ Return the number of times this candidate has been updated. """ + return self._n_updates + class HeapMemory: # This is a basic implementation of a heap memory that uses a priority queue to store candidates. # Later on this will be replaced by a memory DB. @@ -148,11 +210,11 @@ class PrioritySearch(SearchTemplate): 5. The proposed parameters are validated by running the agents on the validation dataset, which can be the current batch or a separate validation dataset when provided. When validate_proposals is set to True, the exploration candidates are also validated. 6. The validation results are used to update the priority queue, which stores the candidates and their scores. The candidates are stored as ModuleCandidate objects, which contain the base module, update dictionary, and rollouts (i.e. raw statistics of the candidate). - This algorithm template can be subclassed to implement specific search algorithms by overriding the `exploit`, `explore`, and `compute_score` methods. + This algorithm template can be subclassed to implement specific search algorithms by overriding the `exploit`, `explore`, and `compute_priority` methods. The `exploit` method is used to select the best candidate from the priority queue, the `explore` method is used to generate new candidates from the priority queue, and - the `compute_score` method is used to compute the score for ranking in the priority queue. + the `compute_priority` method is used to compute the score for ranking in the priority queue. - By default, `compute_score` computes the mean score of the rollouts. `exploit` simply returns the best candidate from the priority queue, and `explore` generates the top `num_candidates` candidates from the priority queue. + By default, `compute_priority` computes the mean score of the rollouts. `exploit` simply returns the best candidate from the priority queue, and `explore` generates the top `num_candidates` candidates from the priority queue. """ def train(self, @@ -180,10 +242,11 @@ def train(self, # Priority Search specific parameters num_candidates: int = 10, # number of candidates to propose for exploration num_proposals: int = 1, # number of proposals to generate per optimizer - default_score: float = float('inf'), # default score assigned to priority queue candidates validate_proposals: bool = True, # whether to validate the proposed parameters for exploration use_best_candidate_to_explore: bool = True, # whether to use the best candidate as part of the exploration candidates memory_size: Optional[int] = None, # size of the heap memory to store the candidates; if None, no limit is set + score_function: str = 'mean', # function to compute the score for the candidates; 'mean' or 'ucb' + ucb_exploration_constant: float = 1.0, # exploration constant for UCB score function # Additional keyword arguments **kwargs ): @@ -193,11 +256,18 @@ def train(self, self.num_proposals = num_proposals self.validate_proposals = validate_proposals # whether to validate the proposed parameters self.use_best_candidate_to_explore = use_best_candidate_to_explore - self.default_score = default_score + self.score_function = score_function # function to compute the score for the candidates + if score_function == 'ucb': # this requires a bounded score range. By default, it is set to (0, 1) + if score_range is None: + score_range = (0, 1) + assert score_range[1]-score_range[0] < float('inf'), \ + "For UCB score function, score_range must be finite. Use 'mean' score function if you want to use unbounded scores." + + self.ucb_exploration_constant = 1. + self._exploration_candidates = None + self.memory = HeapMemory(size=memory_size) # Initialize the heap memory with a size limit - self.memory.push(self.default_score, ModuleCandidate(self.agent)) # Push the base agent as the first candidate - self._exploration_candidates = None super().train(guide, train_dataset, validate_dataset=validate_dataset, @@ -216,6 +286,7 @@ def train(self, save_path=save_path, **kwargs) + def update(self, samples=None, verbose=False, **kwargs): if samples is not None: @@ -225,6 +296,9 @@ def update(self, samples=None, verbose=False, **kwargs): validate_results = self.validate(candidates, samples, verbose=verbose, **kwargs) # this updates the priority queue # 3. Update the priority queue with the validation results self.update_memory(validate_results, verbose=verbose, **kwargs) # samples are provided here in case candidates do not capture full information + else: + if len(self.memory) == 0: + self.memory.push(self.max_score, ModuleCandidate(self.agent)) # Push the base agent as the first candidate (This gives the initialization of the priority queue) # 4. Explore and exploit the priority queue self._best_candidate, info_exploit = self.exploit(verbose=verbose, **kwargs) # get the best candidate (ModuleCandidate) from the priority queue self._exploration_candidates, info_explore = self.explore(verbose=verbose, **kwargs) # List of ModuleCandidates @@ -232,9 +306,9 @@ def update(self, samples=None, verbose=False, **kwargs): # TODO Log information about the update info_log = { - 'best_candidate_score': self._best_candidate.score(), - 'num_exploration_candidates': len(self._exploration_candidates), + 'n_iters': self.n_iters, # number of iterations } + info_log.update(info_exploit) # add the info from the exploit step info_log.update(info_explore) # add the info from the explore step return self._best_candidate.update_dict, [c.get_module() for c in self._exploration_candidates], info_log @@ -374,7 +448,6 @@ def validate(self, candidates, samples, verbose=False, **kwargs): # For example, it copies candidates. This would create a bug. return results - def update_memory(self, validate_results, verbose: bool = False, **kwargs): """ Update the priority queue with the validation results. Args: @@ -384,8 +457,8 @@ def update_memory(self, validate_results, verbose: bool = False, **kwargs): print("--- Updating memory with validation results...") if verbose else None for candidate, rollouts in validate_results.items(): candidate.add_rollouts(rollouts) # add the rollouts to the candidate - score = self.compute_score(candidate) # compute the score for the candidate - self.memory.push(score, candidate) + priority = self.compute_priority(candidate) # compute the priority for the candidate + self.memory.push(priority, candidate) #### def explore(self, verbose: bool = False, **kwargs): @@ -399,13 +472,25 @@ def explore(self, verbose: bool = False, **kwargs): print(f"--- Generating {min(len(self.memory), self.num_candidates)} exploration candidates...") if verbose else None # pop top self.num_candidates candidates from the priority queue top_candidates = [self._best_candidate] if self.use_best_candidate_to_explore else [] + priorities = [] # to store the priorities of the candidates while len(top_candidates) < self.num_candidates and self.memory: - score, candidate = self.memory.pop() # pop the top candidate from the priority queue + priority, candidate = self.memory.pop() # pop the top candidate from the priority queue + priority = - priority # remember that we stored negative scores in the priority queue + priorities.append(priority) # store the priority of the candidate if self.use_best_candidate_to_explore: if candidate == self._best_candidate: # skip if it is already in the top candidates continue top_candidates.append(candidate) # add the candidate to the top candidates - return top_candidates, {} + + mean_scores = [c.mean_score() for c in top_candidates] + mean_scores = [ s for s in mean_scores if s is not None] # filter out None scores + info_dict = { + 'num_exploration_candidates': len(top_candidates), + 'exploration_candidates_mean_priority': np.mean(priorities), # list of priorities of the exploration candidates + 'exploration_candidates_mean_score': np.mean(mean_scores), # list of mean scores of the exploration candidates + } + + return top_candidates, info_dict def exploit(self, verbose: bool = False, **kwargs): @@ -421,16 +506,14 @@ def exploit(self, verbose: bool = False, **kwargs): # This function can be overridden by subclasses to implement a different exploitation strategy if not self.memory: raise ValueError("The priority queue is empty. Cannot exploit.") - best = self.memory.best() # (score, candidate) - score, best_candidate = best - score = -score # remember that we stored negative scores in the priority queue + priority, best_candidate = self.memory.best() # (priority, candidate) + priority = - priority # remember that we stored negative scores in the priority queue return best_candidate, { - 'best_candidate_score': score, # remember that we stored negative scores in the priority queue + 'best_candidate_priority': priority, # remember that we stored negative scores in the priority queue + 'best_candidate_mean_score': best_candidate.mean_score(), # mean score of the candidate's rollouts } - - - def compute_score(self, candidate): + def compute_priority(self, candidate): # NOTE This function can be overridden by subclasses to compute a different score """ Compute the score for the candidate based on the rollouts during the validation phase. It can be overridden by subclasses to implement a different scoring strategy. @@ -444,7 +527,16 @@ def compute_score(self, candidate): raise TypeError("candidate must be an instance of ModuleCandidate.") # By default, we compute the mean score of the rollouts - scores = [r['score'] for r in candidate.rollouts] - default_score = self.default_score if self.default_score is not None else self.score_range[1] # default score for the candidates - - return np.mean(scores) if scores else self.default_score \ No newline at end of file + if self.score_function == 'mean': + # Compute the mean score of the candidate's rollouts + return candidate.mean_score() + elif self.score_function == 'ucb': + # Compute the Upper Confidence Bound (UCB) score + lcb_score, mean_score, ucb_score = candidate.compute_score_confidence( + min_score=self.min_score, + max_score=self.max_score, + scaling_constant=self.ucb_exploration_constant + ) + return ucb_score # return the UCB score + else: + raise ValueError(f"Unknown score function: {self.score_function}") diff --git a/opto/trainer/algorithms/priority_search/search_template.py b/opto/trainer/algorithms/priority_search/search_template.py index e37f8cff..d2b5e61c 100644 --- a/opto/trainer/algorithms/priority_search/search_template.py +++ b/opto/trainer/algorithms/priority_search/search_template.py @@ -92,14 +92,18 @@ def train(self, test_dataset = test_dataset or train_dataset # default to train_dataset if test_dataset is not provided test_guide = test_guide or guide self.num_eval_samples = num_eval_samples # number of samples to use to evaluate each input - self.score_range = score_range or (-np.inf, np.inf) + if score_range is None: + score_range = (-np.inf, np.inf) + assert len(score_range) == 2, "score_range must be a tuple (min_score, max_score)." + assert score_range[1] >= score_range[0], "score_range must be a tuple (min_score, max_score) with min_score <= max_score." + self._score_range = score_range # range of the score for the guide self.train_sampler = Sampler( DataLoader(train_dataset, batch_size=batch_size), guide, num_threads=self.num_threads, sub_batch_size=sub_batch_size, - score_range=self.score_range + score_range=self._score_range ) self._validate_dataset = validate_dataset # if None, the current batch will be used for validation self.validate_sampler = Sampler( @@ -107,7 +111,7 @@ def train(self, validate_guide or guide, num_threads=self.num_threads, sub_batch_size=None, # no sub-batch size for validation - score_range=self.score_range + score_range=self._score_range ) # Evaluate the agent before learning @@ -167,6 +171,16 @@ def train(self, self.n_iters += 1 return + @property + def max_score(self): + """ Maximum score that can be achieved by the agent. """ + return self._score_range[1] + + @property + def min_score(self): + """ Minimum score that can be achieved by the agent. """ + return self._score_range[0] + # Can be overridden by subclasses to implement specific sampling strategies def sample(self, agents, verbose=False, **kwargs): """ Sample a batch of data based on the proposed parameters. All proposals are evaluated on the same batch of inputs. @@ -183,6 +197,10 @@ def sample(self, agents, verbose=False, **kwargs): 'mean_score': np.mean(scores), 'n_epochs': self.train_sampler.n_epochs, } + # check if the scores are within the score range + if not (self.min_score <= log_info['mean_score'] <= self.max_score): + print(f"Warning: Mean score {log_info['mean_score']} is out of the range {self._score_range}.") + return samples, log_info def log(self, info_log, prefix=""): @@ -195,11 +213,14 @@ def log(self, info_log, prefix=""): print(e) def test(self, test_dataset, guide): - min_score = self.score_range[0] + min_score = self.min_score # Test the agent's performance test_score = self.evaluate(self.agent, guide, test_dataset['inputs'], test_dataset['infos'], min_score=min_score, num_threads=self.num_threads, description=f"Evaluating agent") # and log + # check if the test_score is within the score range + if not (self.min_score <= test_score <= self.max_score): + print(f"Warning: Test score {test_score} is out of the range {self._score_range}.") return {'test_score': test_score} def save(self, save_path): From e40d7d0d432eaee12b398b6927f866d2ede56fa1 Mon Sep 17 00:00:00 2001 From: windweller Date: Mon, 4 Aug 2025 17:18:29 -0400 Subject: [PATCH 144/314] apply fix --- opto/trace/containers.py | 8 +++++++ tests/unit_tests/test_modules.py | 37 ++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/opto/trace/containers.py b/opto/trace/containers.py index 402e39c8..f375ab47 100644 --- a/opto/trace/containers.py +++ b/opto/trace/containers.py @@ -41,15 +41,23 @@ def parameters_dict(self): both trainable and non-trainable parameters. The dict contains ParameterNodes or ParameterContainers. """ + from opto.trace.bundle import FunModule + parameters = {} for name, attr in inspect.getmembers(self): if name.startswith('__TRACE_RESERVED_'): # These are reserved for internal use. continue + if isinstance(attr, functools.partial): # this is a class method method = attr.func.__self__ if trainable_method(method): parameters[name] = method.parameter + elif isinstance(attr, FunModule): + # when a bundle method is not trainable + # it shows up as a FunModule attribute + if trainable_method(attr): + parameters[name] = attr.parameter elif trainable_method(attr): # method attribute parameters[name] = attr.parameter elif isinstance(attr, ParameterNode): diff --git a/tests/unit_tests/test_modules.py b/tests/unit_tests/test_modules.py index a1bbc17f..7e93f049 100644 --- a/tests/unit_tests/test_modules.py +++ b/tests/unit_tests/test_modules.py @@ -523,3 +523,40 @@ def forward(self, x): # Test that the copy can still function result = copied.forward(3) assert result._data == 34 # (3 * 8) + 10 + +def test_save_agent_xuanfei_case(): + + from typing import List, Dict, Any + from opto import trace + @trace.model + class SimpleAgent(): + """A simple test agent""" + + def __init__(self, tools_info: List[Dict[str, Any]]): + self.tools_info = trace.node(tools_info, trainable=True) + self.instructions = trace.node("Default instructions", trainable=True) + + @trace.bundle() + def solve(self, tools_info, instructions, task): + return f"Solved: {task} with {len(tools_info)} tools and instructions: {instructions}" + + def forward(self, task): + return self.solve(self.tools_info, self.instructions, task) + + def main(): + # Create agent + tools = [{"name": "test_tool", "description": "A test tool"}] + agent = SimpleAgent(tools) + + # Try to save agent using trace repo's built-in save method + print("\n--- Attempting to save agent ---") + agent.save("agent.pkl") + print("✅ Agent saved successfully using agent.save()") + + main() + import os + if os.path.exists("agent.pkl"): + os.remove("agent.pkl") + print("Temporary file 'agent.pkl' deleted.") + else: + print("File 'agent.pkl' does not exist.") \ No newline at end of file From 9f106e87e3f963b5ceeb9080f1b41fba3f48779a Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 7 Aug 2025 23:20:55 +0000 Subject: [PATCH 145/314] Add save load methods to trainer algorithm, dataloader, optimizers, guide --- opto/optimizers/optimizer.py | 12 ++- opto/optimizers/optoprime.py | 58 +++++++++-- opto/optimizers/optoprime_v2.py | 46 ++++++++- opto/optimizers/textgrad.py | 26 ++++- opto/trainer/algorithms/UCBsearch.py | 86 ++++++++-------- opto/trainer/algorithms/algorithm.py | 75 ++++++++++++-- opto/trainer/guide.py | 27 +++-- opto/trainer/loader.py | 34 ++++++- tests/unit_tests/test_saving_loading.py | 126 +++++++++++++++++++++++- 9 files changed, 409 insertions(+), 81 deletions(-) diff --git a/opto/optimizers/optimizer.py b/opto/optimizers/optimizer.py index 77ee10db..04f8ea5e 100644 --- a/opto/optimizers/optimizer.py +++ b/opto/optimizers/optimizer.py @@ -54,7 +54,7 @@ def trace_graph(self): def step(self, bypassing=False, *args, **kwargs): update_dict = self.propose(*args, **kwargs) - self.project(update_dict) + self.project(update_dict) if not bypassing: self.update(update_dict) return update_dict # TODO add reasoning @@ -63,7 +63,7 @@ def project(self, update_dict: Dict[ParameterNode, Any]): """Project the update dictionary onto the feasible set.""" for p, d in update_dict.items(): if p.trainable: - for projection in p.projections: + for projection in p.projections: d = projection.project(d) update_dict[p] = d @@ -93,3 +93,11 @@ def default_propagator(self): def backward(self, node: Node, *args, **kwargs): """Propagate the feedback backward.""" return node.backward(*args, propagator=self.propagator, **kwargs) + + def save(self, path: str): + """Save the optimizer state to a file.""" + pass + + def load(self, path: str): + """Load the optimizer state from a file.""" + pass \ No newline at end of file diff --git a/opto/optimizers/optoprime.py b/opto/optimizers/optoprime.py index 6cbca909..6465151d 100644 --- a/opto/optimizers/optoprime.py +++ b/opto/optimizers/optoprime.py @@ -5,6 +5,7 @@ import json import re import copy +import pickle from opto.trace.nodes import ParameterNode, Node, MessageNode from opto.trace.propagators import TraceGraph, GraphPropagator from opto.trace.propagators.propagators import Propagator @@ -258,7 +259,7 @@ class OptoPrime(Optimizer): final_prompt_with_variables = dedent( """ What are your suggestions on variables {names}? - + Your response: """ ) @@ -333,13 +334,14 @@ def __init__( if prompt_symbols is not None: self.prompt_symbols.update(prompt_symbols) if json_keys is not None: - self.default_json_keys.update(json_keys) - if self.default_json_keys['answer'] is None: # answer field is not needed - del self.default_json_keys['answer'] - if 'answer' not in self.default_json_keys: + self.default_json_keys.update(json_keys) + # if self.default_json_keys['answer'] is None: + # del self.default_json_keys['answer'] + # NOTE del cause KeyError if the key is not in the dict due to changing class attribute + if 'answer' not in self.default_json_keys or self.default_json_keys['answer'] is None: # answer field is not needed # If 'answer' is not in the json keys, we use the no-answer format self.output_format_prompt = self.output_format_prompt_no_answer.format(**self.default_json_keys) - else: # If 'answer' is in the json keys, we use the original format of OptoPrime + else: # If 'answer' is in the json keys, we use the original format of OptoPrime self.output_format_prompt = self.output_format_prompt_original.format(**self.default_json_keys) self.use_json_object_format = use_json_object_format self.highlight_variables = highlight_variables @@ -450,8 +452,8 @@ def construct_prompt(self, summary, mask=None, *args, **kwargs): ) + user_prompt ) - - + + if self.highlight_variables: var_names = [] for k, v in summary.variables.items(): @@ -618,15 +620,51 @@ def call_llm( {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, ] - + response_format = {"type": "json_object"} if self.use_json_object_format else None try: # Try tp force it to be a json object response = self.llm(messages=messages, max_tokens=max_tokens, response_format=response_format) except Exception: response = self.llm(messages=messages, max_tokens=max_tokens) - + response = response.choices[0].message.content if verbose: print("LLM response:\n", response) return response + + + def save(self, path: str): + """Save the optimizer state to a file.""" + # save the above using pickle isntead + with open(path, "wb") as f: + pickle.dump( + { + "ignore_extraction_error": self.ignore_extraction_error, + "objective": self.objective, + "include_example": self.include_example, + "max_tokens": self.max_tokens, + "memory": self.memory, + "prompt_symbols": self.prompt_symbols, + "json_keys": self.default_json_keys, + 'output_format_prompt': self.output_format_prompt, + "use_json_object_format": self.use_json_object_format, + "highlight_variables": self.highlight_variables, + }, + f, + ) + + def load(self, path: str): + """Load the optimizer state from a file.""" + with open(path, "rb") as f: + state = pickle.load(f) + self.ignore_extraction_error = state["ignore_extraction_error"] + self.objective = state["objective"] + self.include_example = state["include_example"] + self.max_tokens = state["max_tokens"] + self.memory = state["memory"] + self.prompt_symbols = state["prompt_symbols"] + self.default_json_keys = state["json_keys"] + self.output_format_prompt = state['output_format_prompt'] + self.use_json_object_format = state["use_json_object_format"] + self.highlight_variables = state["highlight_variables"] \ No newline at end of file diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index db651bfb..cf0c81b0 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -12,7 +12,7 @@ from opto.utils.llm import AbstractModel, LLM from opto.optimizers.buffers import FIFOBuffer import copy - +import pickle import re from typing import Dict, Any @@ -343,7 +343,7 @@ class OptoPrimeV2(OptoPrime): For variables we express as this: {variable_expression_format} - + If `data_type` is `code`, it means `{value_tag}` is the source code of a python code, which may include docstring and definitions. """ ) @@ -354,7 +354,7 @@ class OptoPrimeV2(OptoPrime): output_format_prompt_template = dedent( """ Output_format: Your output should be in the following XML/HTML format: - + ``` {output_format} ``` @@ -407,7 +407,7 @@ class OptoPrimeV2(OptoPrime): final_prompt = dedent( """ What are your suggestions on variables {names}? - + Your response: """ ) @@ -710,3 +710,41 @@ def call_llm( if verbose: print("LLM response:\n", response) return response + + + def save(self, path: str): + """Save the optimizer state to a file.""" + with open(path, 'wb') as f: + pickle.dump({ + "truncate_expression": self.truncate_expression, + "use_json_object_format": self.use_json_object_format, + "ignore_extraction_error": self.ignore_extraction_error, + "objective": self.objective, + "initial_var_char_limit": self.initial_var_char_limit, + "optimizer_prompt_symbol_set": self.optimizer_prompt_symbol_set, + "include_example": self.include_example, + "max_tokens": self.max_tokens, + "memory": self.memory, + "default_prompt_symbols": self.default_prompt_symbols, + "prompt_symbols": self.prompt_symbols, + "representation_prompt": self.representation_prompt, + "output_format_prompt": self.output_format_prompt, + }, f) + + def load(self, path: str): + """Load the optimizer state from a file.""" + with open(path, 'rb') as f: + state = pickle.load(f) + self.truncate_expression = state["truncate_expression"] + self.use_json_object_format = state["use_json_object_format"] + self.ignore_extraction_error = state["ignore_extraction_error"] + self.objective = state["objective"] + self.initial_var_char_limit = state["initial_var_char_limit"] + self.optimizer_prompt_symbol_set = state["optimizer_prompt_symbol_set"] + self.include_example = state["include_example"] + self.max_tokens = state["max_tokens"] + self.memory = state["memory"] + self.default_prompt_symbols = state["default_prompt_symbols"] + self.prompt_symbols = state["prompt_symbols"] + self.representation_prompt = state["representation_prompt"] + self.output_format_prompt = state["output_format_prompt"] diff --git a/opto/optimizers/textgrad.py b/opto/optimizers/textgrad.py index bdfdeab4..9b7a1ef0 100644 --- a/opto/optimizers/textgrad.py +++ b/opto/optimizers/textgrad.py @@ -6,7 +6,7 @@ from opto.trace.propagators import TraceGraph, GraphPropagator, Propagator from opto.trace.utils import escape_json_nested_quotes, remove_non_ascii from opto.utils.llm import LLM, AbstractModel - +import pickle from copy import copy import re @@ -526,3 +526,27 @@ def call_llm( response = response.message.content return response + + + def save(self, path: str): + """ + Save the optimizer state to a file. + """ + with open(path, 'wb') as f: + pickle.dump({ + 'print_limit': self.print_limit, + 'max_tokens': self.max_tokens, + 'new_variable_tags': self.new_variable_tags, + 'optimizer_system_prompt': self.optimizer_system_prompt, + }, f) + + def load(self, path: str): + """ + Load the optimizer state from a file. + """ + with open(path, 'rb') as f: + state = pickle.load(f) + self.print_limit = state['print_limit'] + self.max_tokens = state['max_tokens'] + self.new_variable_tags = state['new_variable_tags'] + self.optimizer_system_prompt = state['optimizer_system_prompt'] \ No newline at end of file diff --git a/opto/trainer/algorithms/UCBsearch.py b/opto/trainer/algorithms/UCBsearch.py index 9ff6f61b..036c19b3 100644 --- a/opto/trainer/algorithms/UCBsearch.py +++ b/opto/trainer/algorithms/UCBsearch.py @@ -34,13 +34,13 @@ def __init__(self, *args, **kwargs): super().__init__(agent, optimizer, num_threads=num_threads, logger=logger, *args, **kwargs) - - self.buffer = deque(maxlen=max_buffer_size) + + self.buffer = deque(maxlen=max_buffer_size) self.max_buffer_size = max_buffer_size # UCB exploration factor: Higher values encourage more exploration of less-tested candidates, - # lower values favor exploitation of well-performing candidates. + # lower values favor exploitation of well-performing candidates. self.ucb_exploration_factor = ucb_exploration_factor - + # To ensure optimizer_step can be called with bypassing=True if needed. # This depends on the specific optimizer's implementation. # For now, we assume the optimizer has a step method that can return parameters. @@ -55,7 +55,7 @@ def _sample_minibatch(self, dataset: Dict[str, List[Any]], batch_size: int) -> T if not dataset or not dataset.get('inputs') or not dataset.get('infos'): print_color("Warning: Attempted to sample from an empty or malformed dataset.", color='yellow') return [], [] - + dataset_size = len(dataset['inputs']) if dataset_size == 0: print_color("Warning: Dataset is empty, cannot sample minibatch.", color='yellow') @@ -67,8 +67,8 @@ def _sample_minibatch(self, dataset: Dict[str, List[Any]], batch_size: int) -> T infos = [dataset['infos'][i] for i in indices] return xs, infos - def _evaluate_candidate(self, - params_to_eval_dict: Dict[str, Any], + def _evaluate_candidate(self, + params_to_eval_dict: Dict[str, Any], dataset: Dict[str, List[Any]], # Changed from validate_dataset guide, # Changed from validate_guide evaluation_batch_size: int, # New parameter name @@ -80,13 +80,13 @@ def _evaluate_candidate(self, return -np.inf, 0 original_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} - self.optimizer.update(params_to_eval_dict) + self.optimizer.update(params_to_eval_dict) eval_xs, eval_infos = self._sample_minibatch(dataset, evaluation_batch_size) # Use evaluation_batch_size - + if not eval_xs: print_color("Evaluation minibatch is empty. Returning score -inf, count 0.", color='yellow') - self.optimizer.update(original_params) + self.optimizer.update(original_params) return -np.inf, 0 eval_scores = evaluate(self.agent, @@ -97,38 +97,38 @@ def _evaluate_candidate(self, num_threads=num_threads or self.num_threads, description=f"Evaluating candidate") - self.optimizer.update(original_params) + self.optimizer.update(original_params) avg_score = np.mean(eval_scores) if eval_scores and all(s is not None for s in eval_scores) else -np.inf - eval_count = len(eval_xs) - + eval_count = len(eval_xs) + return float(avg_score), eval_count def _calculate_ucb(self, candidate_buffer_entry: Dict, total_tracked_evaluations: int) -> float: """Calculates UCB score for a candidate in the buffer.""" if candidate_buffer_entry['eval_count'] == 0: return float('inf') # Explore unvisited states first - + mean_score = candidate_buffer_entry['score_sum'] / candidate_buffer_entry['eval_count'] - + # Add 1 to total_tracked_evaluations to prevent log(0) if it's the first evaluation overall # and to ensure log argument is > 0. # Add 1 to eval_count in denominator as well to ensure it's robust if eval_count is small. if total_tracked_evaluations == 0: # Should not happen if we init with one eval total_tracked_evaluations = 1 - + # UCB exploration term: ucb_exploration_factor scales the confidence interval # Higher factor = more exploration, lower factor = more exploitation exploration_term = self.ucb_exploration_factor * \ math.sqrt(math.log(total_tracked_evaluations) / candidate_buffer_entry['eval_count']) - + return mean_score + exploration_term def _update_buffer_ucb_scores(self): """Recalculates and updates UCB scores for all candidates in the buffer.""" if not self.buffer: return - + for candidate_entry in self.buffer: candidate_entry['ucb_score'] = self._calculate_ucb(candidate_entry, self._total_evaluations_tracker) @@ -138,9 +138,9 @@ def train(self, *, validation_dataset: Optional[Dict[str, List[Any]]] = None, # Validation set for evaluation, defaults to train_dataset num_search_iterations: int = 100, - train_batch_size: int = 2, + train_batch_size: int = 2, evaluation_batch_size: int = 20, # Renamed from validation_batch_size, used for all explicit evaluations - eval_frequency: int = 1, + eval_frequency: int = 1, log_frequency: Optional[int] = None, save_frequency: Optional[int] = None, save_path: str = "checkpoints/ucb_agent.pkl", @@ -155,7 +155,7 @@ def train(self, # Default validation_dataset to train_dataset if not provided if validation_dataset is None: validation_dataset = train_dataset - + num_threads = num_threads or self.num_threads log_frequency = log_frequency or eval_frequency self.min_score = min_score_for_agent_update # Used by parent's evaluate if called, or our own _evaluate_candidate @@ -176,7 +176,7 @@ def train(self, initial_score, initial_evals = self._evaluate_candidate( initial_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads # Use validation_dataset and guide ) - self._total_evaluations_tracker += initial_evals + self._total_evaluations_tracker += initial_evals total_samples += initial_evals # Log initial evaluation @@ -203,13 +203,13 @@ def train(self, # 1. Pick the candidate 'a' with the highest UCB from the buffer self._update_buffer_ucb_scores() # Ensure UCB scores are fresh action_candidate_a = self.select(self.buffer) - + # Log selected action UCB score self.logger.log('Selected action UCB', action_candidate_a['ucb_score'], iteration, color='magenta') self.logger.log('Selected action mean score', action_candidate_a['score_sum']/(action_candidate_a['eval_count'] or 1), iteration, color='cyan') - + print_color(f"Iter {iteration}/{num_search_iterations}: ", 'blue') - + # 2. Load parameters of 'a' into the agent for the optimizer update step self.optimizer.update(action_candidate_a['params']) @@ -218,7 +218,7 @@ def train(self, train_xs, train_infos = self._sample_minibatch(train_dataset, train_batch_size) if not train_xs: print_color(f"Iter {iteration}: Training minibatch empty, skipping optimizer step.", 'yellow') - continue + continue # Perform forward pass and get feedback for agent parameters 'a' outputs_for_a = [] @@ -236,7 +236,7 @@ def train(self, scores_from_train.append(score) targets_from_train.append(target) feedbacks_from_train.append(feedback) - + if not scores_from_train: # Should not happen if train_xs was not empty print_color(f"Iter {iteration}: No outputs from forward pass for candidate 'a'. Skipping.", 'yellow') continue @@ -249,7 +249,7 @@ def train(self, self.optimizer.backward(target_for_a, feedback_for_a) # Grads for 'a' are now in optimizer try: - a_prime_params_dict = self.optimizer.step(bypassing=True, verbose='output') + a_prime_params_dict = self.optimizer.step(bypassing=True, verbose='output') if not isinstance(a_prime_params_dict, dict) or not a_prime_params_dict: print_color(f"Iter {iteration}: Optimizer.step did not return a valid param dict for a_prime. Using current agent params as a_prime.", 'yellow') # Fallback: if step modified agent in-place and didn't return dict, current agent state is a_prime @@ -258,7 +258,7 @@ def train(self, except Exception as e: print_color(f"Iter {iteration}: Error during optimizer.step for a_prime: {e}. Skipping candidate generation.", 'red') continue - + # 4. Evaluate 'a_prime' on samples of validation set a_prime_score, a_prime_evals = self._evaluate_candidate( a_prime_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads # Use validation_dataset and guide @@ -266,11 +266,11 @@ def train(self, self._total_evaluations_tracker += a_prime_evals total_samples += evaluation_batch_size + train_batch_size metrics['new_candidate_scores'].append(a_prime_score) - + # Log new candidate performance self.logger.log('New candidate score', a_prime_score, iteration, color='green') self.logger.log('Training batch score', score_for_a_on_train_batch, iteration, color='yellow') - + print_color(f"Iter {iteration}: New candidate a_prime generated. Validation Score: {a_prime_score:.4f}, Evals: {a_prime_evals}", 'cyan') # 5. Update the stats of 'a' (action_candidate_a) based on the training batch experience @@ -282,20 +282,20 @@ def train(self, # 6. Add 'a_prime' (with its validation stats) to the buffer if a_prime_score > -np.inf and a_prime_evals > 0: new_candidate_entry = { - 'params': a_prime_params_dict, + 'params': a_prime_params_dict, 'score_sum': a_prime_score * a_prime_evals, # Store sum 'eval_count': a_prime_evals, 'ucb_score': None, # avoid accidental reads before it's initializad 'iteration_created': iteration } - + # Eviction logic before adding if buffer is at max_len if len(self.buffer) == self.max_buffer_size: self._update_buffer_ucb_scores() # Ensure UCBs are current before eviction candidate_to_evict = min(self.buffer, key=lambda c: c['ucb_score']) self.buffer.remove(candidate_to_evict) print_color(f"Iter {iteration}: Buffer full. Evicted a candidate (UCB: {candidate_to_evict['ucb_score']:.4f})", 'magenta') - + self.buffer.append(new_candidate_entry) print_color(f"Iter {iteration}: Added new candidate to buffer.", 'magenta') else: @@ -322,7 +322,7 @@ def train(self, "total_evaluations_tracker": self._total_evaluations_tracker, "total_samples": total_samples # Add new metric } - + # Log all important metrics self.logger.log('Best candidate score', log_data['best_score'], iteration, color='green') self.logger.log('Buffer size', log_data['buffer_size'], iteration, color='blue') @@ -330,9 +330,9 @@ def train(self, self.logger.log('Buffer average evaluations', log_data['buffer_avg_evals'], iteration, color='orange') self.logger.log('Total evaluations tracker', log_data['total_evaluations_tracker'], iteration, color='magenta') self.logger.log('Total samples processed', log_data['total_samples'], iteration, color='yellow') - + print_color(f"Log @ Iter {iteration}: Best score in buffer: {log_data['best_score']:.4f}, Buffer size: {log_data['buffer_size']}, Total samples: {total_samples}", 'green') - + # Save agent (e.g., the one with highest mean score in buffer) if save_frequency is not None and iteration % save_frequency == 0: best_overall_candidate = max(self.buffer, key=lambda c: c['score_sum'] / (c['eval_count'] or 1E-9) ) @@ -342,33 +342,33 @@ def train(self, # End of search loop print_color("UCB search finished.", 'blue') - + # Log final training summary final_iteration = num_search_iterations self.logger.log('UCB search completed', final_iteration, final_iteration, color='blue') self.logger.log('Final total samples', total_samples, final_iteration, color='magenta') - + if not self.buffer: print_color("Buffer is empty at the end of search. No best candidate found.", 'red') self.logger.log('Final status', 'Buffer empty - no best candidate', final_iteration, color='red') return metrics, -np.inf - + # Select the best candidate based on highest mean score (exploitation) final_best_candidate = max(self.buffer, key=lambda c: c['score_sum'] / (c['eval_count'] or 1E-9)) final_best_score = final_best_candidate['score_sum'] / (final_best_candidate['eval_count'] or 1E-9) - + # Log final results self.logger.log('Final best score', final_best_score, final_iteration, color='green') self.logger.log('Final best candidate evaluations', final_best_candidate['eval_count'], final_iteration, color='cyan') self.logger.log('Final buffer size', len(self.buffer), final_iteration, color='blue') - + print_color(f"Final best candidate: Mean Score {final_best_score:.4f}, Evals {final_best_candidate['eval_count']}", 'green') # Load best parameters into the agent self.optimizer.update(final_best_candidate['params']) # Load params using optimizer return metrics, float(final_best_score) - + def select(self, buffer): '''Could be subclassed to implement different selection strategies''' return max(buffer, key=lambda c: c['ucb_score']) \ No newline at end of file diff --git a/opto/trainer/algorithms/algorithm.py b/opto/trainer/algorithms/algorithm.py index 7995fc0b..b3506e23 100644 --- a/opto/trainer/algorithms/algorithm.py +++ b/opto/trainer/algorithms/algorithm.py @@ -1,8 +1,11 @@ from typing import Optional from opto.trace.modules import Module from opto.trainer.loggers import DefaultLogger +from opto.trainer.loader import DataLoader +from opto.trainer.guide import AutoGuide +from opto.optimizers.optimizer import Optimizer import os - +import pickle class AbstractAlgorithm: """ Abstract base class for all algorithms. """ @@ -38,10 +41,10 @@ def __init__(self, def _use_asyncio(self, threads=None): """Determine whether to use asyncio based on the number of threads. - + Args: threads: Number of threads to use. If None, uses self.num_threads. - + Returns: bool: True if parallel execution should be used, False otherwise. """ @@ -50,11 +53,11 @@ def _use_asyncio(self, threads=None): def save_agent(self, save_path, iteration=None): """Save the agent to the specified path. - + Args: save_path: Path to save the agent to. iteration: Current iteration number (for logging purposes). - + Returns: str: The path where the agent was saved. """ @@ -62,7 +65,7 @@ def save_agent(self, save_path, iteration=None): directory = os.path.dirname(save_path) if directory: os.makedirs(directory, exist_ok=True) - + # Add iteration number to filename if provided if iteration is not None: base, ext = os.path.splitext(save_path) @@ -71,14 +74,14 @@ def save_agent(self, save_path, iteration=None): save_path = f"{base}_iter{iteration}_final{ext}" else: save_path = f"{base}_iter{iteration}{ext}" - + # Save the agent self.agent.save(save_path) - + # Log if we have a logger and iteration is provided if hasattr(self, 'logger') and iteration is not None: self.logger.log('Saved agent', save_path, iteration, color='blue') - + return save_path def train(self, @@ -88,3 +91,57 @@ def train(self, **kwargs ): raise NotImplementedError + + + def save(self, path: str): + """ Save the guide to a file. """ + with open(path, 'wb') as f: + d = {} + for key, value in self.__dict__.items(): + if isinstance(value, Module): + _path = path+ f"_{key}.module" + value.save(_path) + d[key] = _path + elif isinstance(value, AutoGuide): + _path = path + f"_{key}.guide" + value.save(_path) + d[key] = _path + elif isinstance(value, DataLoader): + _path = path + f"_{key}.dataloader" + value.save(_path) + d[key] = _path + elif isinstance(value, Optimizer): + _path = path + f"_{key}.optimizer" + value.save(_path) + d[key] = _path + else: + d[key] = value + pickle.dump(d, f) + + def load(self, path: str): + """ Load the guide from a file. """ + with open(path, 'rb') as f: + data = pickle.load(f) + for key, value in data.items(): + if key not in self.__dict__: + warning_msg = f"Key '{key}' not found in the algorithm's attributes. Skipping loading for this key." + print(warning_msg) # or use logging.warning(warning_msg) + continue + + # key is in the algorithm's attributes + if isinstance(value, str): + if value.endswith('.module'): + attr = self.__dict__[key] + assert isinstance(attr, Module), f"Expected {key} to be a Module, got {type(attr)}" + elif value.endswith('.guide'): + attr = self.__dict__[key] + assert isinstance(attr, AutoGuide), f"Expected {key} to be an AutoGuide, got {type(attr)}" + elif value.endswith('.dataloader'): + attr = self.__dict__[key] + assert isinstance(attr, DataLoader), f"Expected {key} to be a DataLoader, got {type(attr)}" + elif value.endswith('.optimizer'): + attr = self.__dict__[key] + assert isinstance(attr, Optimizer), f"Expected {key} to be an Optimizer, got {type(attr)}" + attr.load(value) + else: + self.__dict__[key] = value \ No newline at end of file diff --git a/opto/trainer/guide.py b/opto/trainer/guide.py index 8a727d86..df465cc8 100644 --- a/opto/trainer/guide.py +++ b/opto/trainer/guide.py @@ -1,5 +1,6 @@ from typing import List, Dict, Any, Union, Tuple, Optional, Callable import json +import pickle import re import copy from opto.utils.llm import LLM, AbstractModel @@ -37,15 +38,15 @@ def __call__(self, task: str, response: str, info: Any, **kwargs) -> Tuple[float def forward(self, task: str, response: str, info: Any, **kwargs) -> Tuple[float, str]: return self.get_feedback(task, response, info, **kwargs) - + def get_feedback(self, query: str, response: str, reference: Optional[str] = None, **kwargs) -> Tuple[float, str]: raise NotImplementedError def metric(self, query: str, response: str, reference: Optional[str] = None, **kwargs) -> float: """ Exact match metric """ return self.get_feedback(query, response, reference)[0] - - def copy(self): + + def copy(self): """ Create a copy of the guide instance. Returns: @@ -55,6 +56,18 @@ def copy(self): # This can be overridden by subclasses to provide a more specific copy behavior. return copy.deepcopy(self) + def save(self, path: str): + """ Save the guide to a file. """ + with open(path, 'wb') as f: + pickle.dump(self.__dict__, f) + + def load(self, path: str): + """ Load the guide from a file. """ + with open(path, 'rb') as f: + data = pickle.load(f) + for key, value in data.items(): + setattr(self, key, value) + class VerbalJudgeGuide(AutoGuide): """ @@ -121,10 +134,10 @@ def get_feedback(self, query: str, response: str, reference: Optional[str] = Non # Check if metric function indicates perfect match user_prompt = self.prompt_template.format( - query=query, - response=response, - reference=reference, - correctness_template=self.DEFAULT_CORRECTNESS_TEMPLATE, + query=query, + response=response, + reference=reference, + correctness_template=self.DEFAULT_CORRECTNESS_TEMPLATE, incorrectness_template=self.DEFAULT_INCORRECTNESS_TEMPLATE) messages = [ diff --git a/opto/trainer/loader.py b/opto/trainer/loader.py index e61532b7..a2214297 100644 --- a/opto/trainer/loader.py +++ b/opto/trainer/loader.py @@ -1,7 +1,5 @@ import numpy as np - - - +import pickle class DataLoader: @@ -23,13 +21,16 @@ def __init__(self, dataset, batch_size=1, replacement=False, shuffle=True): self.replacement = replacement self.shuffle = shuffle self._indices = self._update_indices() + self._i = 0 def __iter__(self): indices = self._indices - for i in range(0, len(indices), self.batch_size): + for i in range(self._i, len(indices), self.batch_size): xs = [ self.dataset['inputs'][ind] for ind in indices[i:i + self.batch_size] ] infos = [self.dataset['infos'][ind] for ind in indices[i:i + self.batch_size] ] + self._i = i + self.batch_size yield xs, infos + self._i = 0 if self.shuffle: self._indices = self._update_indices() @@ -37,3 +38,28 @@ def __iter__(self): def _update_indices(self): N = len(self.dataset['inputs']) return np.random.choice(N, size=N, replace=self.replacement) + + def save(self, path): + """Save the dataset to a file.""" + with open(path, 'wb') as f: + pickle.dump( + {'_indices': self._indices, + '_i': self._i, + 'batch_size': self.batch_size, + 'replacement': self.replacement, + 'shuffle': self.shuffle, + 'dataset': self.dataset}, + f + ) + + def load(self, path): + """Load the dataset from a file.""" + import pickle + with open(path, 'rb') as f: + data = pickle.load(f) + self._indices = data['_indices'] + self._i = data['_i'] + self.batch_size = data['batch_size'] + self.replacement = data['replacement'] + self.shuffle = data['shuffle'] + self.dataset = data['dataset'] \ No newline at end of file diff --git a/tests/unit_tests/test_saving_loading.py b/tests/unit_tests/test_saving_loading.py index 1f634cd0..c0444512 100644 --- a/tests/unit_tests/test_saving_loading.py +++ b/tests/unit_tests/test_saving_loading.py @@ -1,6 +1,15 @@ from opto import trace +from opto.trainer.loader import DataLoader +from opto.trainer.algorithms import BasicSearchAlgorithm +from opto.optimizers import OptoPrimeV2 +from opto.trainer.guide import AutoGuide +from opto.utils.llm import DummyLLM + +import re, os +import numpy as np +import copy @trace.bundle(trainable=True) def fun(x): @@ -25,4 +34,119 @@ def test_saving_load(): a, b = fun(x) - print(a, b) \ No newline at end of file + print(a, b) + + +def test_trainer_saving_loading(): + + + class Guide(AutoGuide): + + def get_feedback(self, query, response, reference=None, **kwargs): + """ + Provide feedback based on the query and response. + + Args: + query: The query to analyze. + response: The response generated by the model. + reference: Optional reference answer for comparison. + **kwargs: Additional context or parameters. + + Returns: + A tuple containing a score and feedback string. + """ + score = response == reference + feedback = "Exact match!" if score == 1.0 else "Not an exact match." + return score, feedback + + @trace.model + class Agent: + + def __init__(self): + self.param = trace.node(1., trainable=True) + self.state = 0 + + def forward(self, x): + return self.param + 1 + + + xs = [1, 2, 3, 4, 5] + infos = [1, 2, 3, 4, 5] + batch_size = 3 + sub_batch_size = 2 + num_threads = 2 # 2 + dataset = {'inputs': xs, 'infos': infos} + loader = DataLoader(dataset, batch_size=batch_size) + num_proposals = 10 + num_candidates = 5 + memory_size = 3 + suggested_value = 5 + + + def _llm_callable(messages, **kwargs): + """ + A dummy LLM callable that simulates a response. + """ + problem = messages[1]['content'] + + # extract name from + name = re.findall(r"", problem) + if name: + name = name[0] + else: + name = "unknown" + + return f""" + Dummy reasoning based on the input messages. + + {name} + {suggested_value} + + """ + + # Create a dummy LLM and an agent + dummy_llm = DummyLLM(_llm_callable) + agent = Agent() + optimizer = OptoPrimeV2( + agent.parameters(), + llm=dummy_llm, + ) + optimizer.objective = 'fake objective' + algo = BasicSearchAlgorithm( + agent, + optimizer, + ) + + algo.train( + guide=Guide(), + train_dataset=dataset, + batch_size=batch_size, + num_threads=num_threads, + num_candidates=num_candidates, + num_proposals=num_proposals, + verbose=False, #'output', + ) + agent.param._data = 10 # to simulate a change in the agent's parameters + + algo.save('test_algo.pkl') + + + # Load the algorithm and check if it works + agent = Agent() + optimizer = OptoPrimeV2( + agent.parameters(), + llm=dummy_llm, + ) + algo2 = BasicSearchAlgorithm( + agent, + optimizer, + ) + algo2.load('test_algo.pkl') + + assert algo2.agent.param.data == 10, "Loaded agent's parameter does not match the saved one." + assert algo2.optimizer.objective == 'fake objective', "Loaded optimizer's objective does not match the saved one." + + os.remove('test_algo.pkl') + os.remove('test_algo.pkl_agent.module') + os.remove('test_algo.pkl_optimizer.optimizer') + os.remove('test_algo.pkl_validate_guide.guide') \ No newline at end of file From dea611a7e736d6a0eb6ed49c901fefb13cfe718b Mon Sep 17 00:00:00 2001 From: Adith Swaminathan Date: Mon, 11 Aug 2025 17:56:43 -0700 Subject: [PATCH 146/314] Minor bugfixes to get examples and tests to work --- CONTRIBUTING.md | 2 +- README.md | 48 ++++++++++--------- SECURITY.md | 41 ---------------- SUPPORT.md | 25 ---------- examples/bbh/run_prompt_bigbench_dspy.py | 2 +- examples/bbh/run_prompt_bigbench_trace.py | 13 +++-- .../run_bigbench_trace_async.py | 2 +- examples/search_algo_example.py | 8 ++-- ...st_time_loss_for_code_OptoPrimeMulti.ipynb | 7 ++- opto/optimizers/textgrad.py | 1 + opto/trace/operators.py | 7 ++- opto/trainer/algorithms/UCBsearch.py | 2 +- pyproject.toml | 7 +-- setup.py | 2 +- tests/llm_optimizers_tests/test_guides.py | 19 ++++---- tests/llm_optimizers_tests/test_optimizer.py | 2 +- 16 files changed, 64 insertions(+), 124 deletions(-) delete mode 100644 SECURITY.md delete mode 100644 SUPPORT.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 52182780..d7a907a5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contribution Guideline -Trace is an actively growing project and under active maintenance and development! We maintain two major branches `main` and `experimental`. The `main` branch is the most stable, version-controlled branch and it is what the PyPI package is linked to. On the other hand, the `experimental` branch is the dev branch, which will change more dynamically in in preparation for the next version update. +Trace is an actively growing project and under active maintenance and development! We maintain two major branches `main` and `experimental`. The `main` branch is the most stable, version-controlled branch and it is what the PyPI package is linked to. On the other hand, the `experimental` branch is the dev branch, which will change more dynamically in preparation for the next version update. ### Development and Review Process diff --git a/README.md b/README.md index 724d6014..047f3c31 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- drawing + drawing

# End-to-end Generative Optimization for AI Agents @@ -15,10 +15,10 @@ losses, natural language text, compiler errors, etc.). Trace generalizes the bac propagating an AI system's execution trace. Trace is implemented as a PyTorch-like Python library. Users write Python code directly and can use Trace primitives to optimize certain parts, just like training neural networks! -[Paper](https://arxiv.org/abs/2406.16218) | [Project website](https://microsoft.github.io/Trace/) | [Documentation](https://microsoft.github.io/Trace/intro.html) | [Blogpost](https://www.microsoft.com/en-us/research/blog/tracing-the-path-to-self-adapting-ai-agents/) | [Discord channel](https://discord.gg/4VeAvwFcWy) | [Roadmap](https://docs.google.com/spreadsheets/d/1dMoECd2Soj6bATpkNDeaMxl0ymOYCtGq7ZiHr0JRdJU/edit?usp=sharing) +[Paper](https://arxiv.org/abs/2406.16218) | [Project website](https://agentopt.github.io/Trace/) | [Documentation](https://agentopt.github.io/Trace/intro.html) | [Blogpost](https://www.microsoft.com/en-us/research/blog/tracing-the-path-to-self-adapting-ai-agents/) | [Discord channel](https://discord.gg/4VeAvwFcWy) | [Roadmap](https://docs.google.com/spreadsheets/d/1dMoECd2Soj6bATpkNDeaMxl0ymOYCtGq7ZiHr0JRdJU/edit?usp=sharing)

- drawing + drawing

## Setup @@ -104,6 +104,15 @@ test_output = strange_sort_list(test_input) print(test_output) ``` +Note that by default the generative optimizers in Trace (like OptoPrime) use LiteLLLM as the backend. +See [LLM API Setup](#llm-api-setup) below for more details. +At a minimum, the api_key must be set for calling LLMs, such as, + +```python +import os +os.environ['OPENAI_API_KEY'] = 'YOUR API KEY' +``` + Now, after declaring what is trainable and what isn't, and use `node` and `bundle` to define the computation graph, we can use the optimizer to optimize the computation graph. @@ -220,21 +229,15 @@ agent = train() Defining and training an agent through Trace will give you more flexibility and control over what the agent learns. -If you have a dataset and you want to use **multi-threading** to train and evaluate your workflow quickly: - -```python - -``` - ## Tutorials | **Level** | **Tutorial** | **Run in Colab** | **Description** | | --- |-------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Beginner | [Getting Started](https://microsoft.github.io/Trace/quickstart/quick_start.html) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/microsoft/Trace/blob/website/docs/quickstart/quick_start.ipynb) | Introduces basic primitives like `node` and `bundle`. Showcases a code optimization pipeline. | -| Beginner | [Adaptive AI Agent](https://microsoft.github.io/Trace/quickstart/quick_start_2.html) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/microsoft/Trace/blob/website/docs/quickstart/quick_start_2.ipynb) | Introduce primitive `model` that allows anyone to build self-improving agents that react to environment feedback. Shows how an LLM agent learns to place a shot in a Battleship game. -| Intermediate | [Multi-Agent Collaboration](https://microsoft.github.io/Trace/quickstart/virtualhome.html) | N/A | Demonstrates how Trace can be used for multi-agent collaboration environment in Virtualhome. -| Intermediate | [NLP Prompt Optimization](https://microsoft.github.io/Trace/examples/nlp/bigbench_hard.html) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/microsoft/Trace/blob/website/docs/examples/nlp/bigbench_hard.ipynb) | Shows how Trace can optimizes prompt and code together jointly for BigBench-Hard 23 tasks. -| Advanced | [Robotic Arm Control](https://microsoft.github.io/Trace/examples/robotics/metaworld.html) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/microsoft/Trace/blob/website/docs/examples/robotics/metaworld.ipynb) | Trace can optimize code to control a robotic arm after observing a full trajectory of interactions. | +| Beginner | [Getting Started](https://agentopt.github.io/Trace/quickstart/quick_start.html) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/AgentOpt/Trace/blob/website/docs/quickstart/quick_start.ipynb) | Introduces basic primitives like `node` and `bundle`. Showcases a code optimization pipeline. | +| Beginner | [Adaptive AI Agent](https://agentopt.github.io/Trace/quickstart/quick_start_2.html) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/AgentOpt/Trace/blob/website/docs/quickstart/quick_start_2.ipynb) | Introduce primitive `model` that allows anyone to build self-improving agents that react to environment feedback. Shows how an LLM agent learns to place a shot in a Battleship game. +| Intermediate | [Multi-Agent Collaboration](https://agentopt.github.io/Trace/quickstart/virtualhome.html) | N/A | Demonstrates how Trace can be used for multi-agent collaboration environment in Virtualhome. +| Intermediate | [NLP Prompt Optimization](https://agentopt.github.io/Trace/examples/nlp/bigbench_hard.html) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/AgentOpt/Trace/blob/website/docs/examples/nlp/bigbench_hard.ipynb) | Shows how Trace can optimizes prompt and code together jointly for BigBench-Hard 23 tasks. +| Advanced | [Robotic Arm Control](https://agentopt.github.io/Trace/examples/robotics/metaworld.html) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/AgentOpt/Trace/blob/website/docs/examples/robotics/metaworld.ipynb) | Trace can optimize code to control a robotic arm after observing a full trajectory of interactions. | ## Supported Optimizers @@ -274,7 +277,7 @@ The table evaluates the frameworks in the following aspects: We provide a comparison to validate our implementation of TextGrad in Trace:

- drawing + drawing

To produce this table, we ran the TextGrad pip-installed repo on 2024-10-30, and we also include the numbers reported in the TextGrad paper. @@ -389,8 +392,8 @@ Explains the role of feedback in LLM-based optimizers. An early work that influe ``` ## Contributors Wall - - + + ## Contributing @@ -404,8 +407,7 @@ a CLA and decorate the PR appropriately (e.g., status check, comment). Simply fo provided by the bot. You will only need to do this once across all repos using our CLA. This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). -For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or -contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/). ## Roadmap @@ -423,7 +425,7 @@ please see the paper for details. which [fixes](https://platform.openai.com/docs/models/gpt-4o) the structured output issue of gpt-4o-2024-05-13. While gpt-4 works reliably most of the time, we've found gpt-4o-2024-05-13 often hallucinates even in very basic optimization problems and does not follow instructions. This might be due to the current implementation of optimizers -rely on outputing in json format. Issues of gpt-4o with json have been reported in the communities ( +rely on outputing in json format. Issues of gpt-4o with json have been reported in the community ( see [example](https://community.openai.com/t/gpt-4o-doesnt-consistently-respect-json-schema-on-tool-use/751125)). ## Disclaimers @@ -433,7 +435,7 @@ see [example](https://community.openai.com/t/gpt-4o-doesnt-consistently-respect- functionalities may be changed in the future. - System performance may vary by workflow, dataset, query, and response, and users are responsible for determining the accuracy of generated content. -- System outputs do not represent the opinions of Microsoft. +- System outputs do not represent the opinions of the developers of Trace. - All decisions leveraging outputs of the system should be made with human oversight and not be based solely on system outputs. - Use of the system must comply with all applicable laws, regulations, and policies, including those pertaining to @@ -446,10 +448,10 @@ see [example](https://community.openai.com/t/gpt-4o-doesnt-consistently-respect- This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). -Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft +Any use of Microsoft trademarks or logos in this project does not imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party's policies. ## Privacy -See [Microsoft Privacy Statement](https://privacy.microsoft.com/en-us/privacystatement). +This project has adopted the [Microsoft Privacy Statement](https://privacy.microsoft.com/en-us/privacystatement). diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index b3c89efc..00000000 --- a/SECURITY.md +++ /dev/null @@ -1,41 +0,0 @@ - - -## Security - -Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet) and [Xamarin](https://github.com/xamarin). - -If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below. - -## Reporting Security Issues - -**Please do not report security vulnerabilities through public GitHub issues.** - -Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report). - -If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp). - -You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). - -Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: - - * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) - * Full paths of source file(s) related to the manifestation of the issue - * The location of the affected source code (tag/branch/commit or direct URL) - * Any special configuration required to reproduce the issue - * Step-by-step instructions to reproduce the issue - * Proof-of-concept or exploit code (if possible) - * Impact of the issue, including how an attacker might exploit the issue - -This information will help us triage your report more quickly. - -If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs. - -## Preferred Languages - -We prefer all communications to be in English. - -## Policy - -Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd). - - diff --git a/SUPPORT.md b/SUPPORT.md deleted file mode 100644 index 291d4d43..00000000 --- a/SUPPORT.md +++ /dev/null @@ -1,25 +0,0 @@ -# TODO: The maintainer of this repo has not yet edited this file - -**REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project? - -- **No CSS support:** Fill out this template with information about how to file issues and get help. -- **Yes CSS support:** Fill out an intake form at [aka.ms/onboardsupport](https://aka.ms/onboardsupport). CSS will work with/help you to determine next steps. -- **Not sure?** Fill out an intake as though the answer were "Yes". CSS will help you decide. - -*Then remove this first heading from this SUPPORT.MD file before publishing your repo.* - -# Support - -## How to file issues and get help - -This project uses GitHub Issues to track bugs and feature requests. Please search the existing -issues before filing new issues to avoid duplicates. For new issues, file your bug or -feature request as a new Issue. - -For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE -FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER -CHANNEL. WHERE WILL YOU HELP PEOPLE?**. - -## Microsoft Support Policy - -Support for this **PROJECT or PRODUCT** is limited to the resources listed above. diff --git a/examples/bbh/run_prompt_bigbench_dspy.py b/examples/bbh/run_prompt_bigbench_dspy.py index e1a1fa34..7b6a3e98 100644 --- a/examples/bbh/run_prompt_bigbench_dspy.py +++ b/examples/bbh/run_prompt_bigbench_dspy.py @@ -117,7 +117,7 @@ def evaluate_dp(dp, examples): stats = {} - llm = dspy.OpenAI(model="gpt-4-turbo-2024-04-09", max_tokens=512) + llm = dspy.LM(model="openai/gpt-4-turbo-2024-04-09", max_tokens=512) dspy.settings.configure(lm=llm) if args.cot: diff --git a/examples/bbh/run_prompt_bigbench_trace.py b/examples/bbh/run_prompt_bigbench_trace.py index c8f33467..d6b12047 100644 --- a/examples/bbh/run_prompt_bigbench_trace.py +++ b/examples/bbh/run_prompt_bigbench_trace.py @@ -4,6 +4,7 @@ from opto.optimizers import OptoPrime from datasets import load_dataset from opto.trace import model, bundle, ExecutionError +from opto.utils.llm import LLM import re from tqdm import tqdm @@ -24,10 +25,8 @@ def eval_metric(true, prediction): class LLMCallable: - def __init__(self, config_list=None, max_tokens=1024, verbose=False): - if config_list is None: - config_list = autogen.config_list_from_json("OAI_CONFIG_LIST") - self.llm = autogen.OpenAIWrapper(config_list=config_list) + def __init__(self, llm=None, max_tokens=1024, verbose=False): + self.llm = llm or LLM() self.max_tokens = max_tokens self.verbose = verbose @@ -40,15 +39,15 @@ def call_llm(self, user_prompt): if self.verbose not in (False, "output"): print("Prompt\n", system_prompt + user_prompt) - messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}] + messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, {"role": "user", "content": "Format your response as a JSON object."}] try: - response = self.llm.create( + response = self.llm( messages=messages, response_format={"type": "json_object"}, ) except Exception: - response = self.llm.create(messages=messages, max_tokens=self.max_tokens) + response = self.llm(messages=messages, max_tokens=self.max_tokens) response = response.choices[0].message.content if self.verbose: diff --git a/examples/minibatch_bbh_aynsc/run_bigbench_trace_async.py b/examples/minibatch_bbh_aynsc/run_bigbench_trace_async.py index 7e12339f..3688907f 100644 --- a/examples/minibatch_bbh_aynsc/run_bigbench_trace_async.py +++ b/examples/minibatch_bbh_aynsc/run_bigbench_trace_async.py @@ -142,7 +142,7 @@ def forward(self, question): We read in a question and produces a response """ user_prompt = self.create_prompt(self.prompt_template, question) - response = trace_ops.call_llm(user_prompt) + response = trace_ops.call_llm(None, user_prompt) answer = self.extract_answer(self.prompt_template, question, response) return answer diff --git a/examples/search_algo_example.py b/examples/search_algo_example.py index ea3421c8..14fc61ea 100644 --- a/examples/search_algo_example.py +++ b/examples/search_algo_example.py @@ -215,7 +215,7 @@ def main(): help='Number of threads for parallel processing') parser.add_argument('--eval_frequency', type=int, default=2, help='How often to run evaluation') - parser.add_argument('--log_frequency', type=int, default=20, + parser.add_argument('--log_frequency', type=int, default=10, help='How often to log results') parser.add_argument('--seed', type=int, default=42, help='Random seed for reproducibility') @@ -229,17 +229,17 @@ def main(): help='Maximum depth for beam search algorithms') parser.add_argument('--validation_dataset_size', type=int, default=20, help='Size of validation dataset for beam search') - parser.add_argument('--max_history_size', type=int, default=12, + parser.add_argument('--max_history_size', type=int, default=5, help='Maximum history size for history-based algorithms') parser.add_argument('--num_basicsearch_proposals', type=int, default=2, help='Number of proposals for basic search algorithm') # UCB algorithm-specific parameters - parser.add_argument('--max_buffer_size', type=int, default=10, + parser.add_argument('--max_buffer_size', type=int, default=5, help='Maximum buffer size for UCB algorithms') parser.add_argument('--ucb_exploration_factor', type=float, default=1.0, help='UCB exploration factor') - parser.add_argument('--num_search_iterations', type=int, default=100, + parser.add_argument('--num_search_iterations', type=int, default=4, help='Number of search iterations for UCB algorithms') parser.add_argument('--train_batch_size_ucb', type=int, default=2, help='Training batch size for UCB algorithms') diff --git a/examples/textgrad_examples/notebooks/textgrad_test_time_loss_for_code_OptoPrimeMulti.ipynb b/examples/textgrad_examples/notebooks/textgrad_test_time_loss_for_code_OptoPrimeMulti.ipynb index a5881d07..7cd54a63 100644 --- a/examples/textgrad_examples/notebooks/textgrad_test_time_loss_for_code_OptoPrimeMulti.ipynb +++ b/examples/textgrad_examples/notebooks/textgrad_test_time_loss_for_code_OptoPrimeMulti.ipynb @@ -405,7 +405,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/" @@ -453,6 +453,8 @@ } ], "source": [ + "import json\n", + "\n", "# Test all candidates and log execution times\n", "execution_results = []\n", "\n", @@ -462,7 +464,8 @@ " continue\n", "\n", " # Extract the function code from the dictionary\n", - " func_code = list(candidate.values())[0] # Assumes there's only one key-value pair in the dictionary\n", + " suggested_code = json.loads(candidate)[\"suggestion\"]\n", + " func_code = list(suggested_code.values())[0] # Assumes there's only one key-value pair in the dictionary\n", " if not func_code:\n", " print(f\"Candidate {i+1}: No code found\")\n", " continue\n", diff --git a/opto/optimizers/textgrad.py b/opto/optimizers/textgrad.py index bdfdeab4..7ca753fc 100644 --- a/opto/optimizers/textgrad.py +++ b/opto/optimizers/textgrad.py @@ -473,6 +473,7 @@ def _step(self, verbose=False): system_prompt=self.optimizer_system_prompt, verbose=verbose, ) + response = response.choices[0].message.content try: var_json = ( response.split(self.new_variable_tags[0])[1] diff --git a/opto/trace/operators.py b/opto/trace/operators.py index 45a2f715..2bab4980 100644 --- a/opto/trace/operators.py +++ b/opto/trace/operators.py @@ -588,10 +588,13 @@ def set_update(x: Any, y: Any): return x -@bundle() +@bundle(catch_execution_error=False) def call_llm(system_prompt, *user_prompts, **kwargs): """Query the language model of system_prompt with user_prompts.""" - messages = [{"role": "system", "content": system_prompt}] + if system_prompt is not None: + messages = [{"role": "system", "content": system_prompt}] + else: + messages = [{"role": "system", "content": "You are a helpful assistant.\n"}] for user_prompt in user_prompts: messages.append({"role": "user", "content": user_prompt}) from opto.utils.llm import LLM diff --git a/opto/trainer/algorithms/UCBsearch.py b/opto/trainer/algorithms/UCBsearch.py index 9ff6f61b..dbc04cfe 100644 --- a/opto/trainer/algorithms/UCBsearch.py +++ b/opto/trainer/algorithms/UCBsearch.py @@ -99,7 +99,7 @@ def _evaluate_candidate(self, self.optimizer.update(original_params) - avg_score = np.mean(eval_scores) if eval_scores and all(s is not None for s in eval_scores) else -np.inf + avg_score = np.mean(eval_scores) if ((eval_scores is not None) and all(s is not None for s in eval_scores)) else -np.inf eval_count = len(eval_xs) return float(avg_score), eval_count diff --git a/pyproject.toml b/pyproject.toml index 8d652ed2..fa4852fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,12 +23,13 @@ classifiers = [ [project.optional-dependencies] autogen = ["autogen-agentchat==0.2.40"] +test = ["datasets==3.6.0"] [project.urls] -Homepage = "https://microsoft.github.io/Trace/" -Documentation = "https://microsoft.github.io/Trace/intro.html" -Repository = "https://github.com/microsoft/Trace.git" +Homepage = "https://agentopt.github.io/Trace/" +Documentation = "https://agentopt.github.io/Trace/intro.html" +Repository = "https://github.com/AgentOpt/Trace.git" [tool.setuptools] license-files = ["LICEN[CS]E*"] \ No newline at end of file diff --git a/setup.py b/setup.py index 4fa7eef5..e1e16725 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ install_requires = [ "graphviz>=0.20.1", "pytest", - "litellm", + "litellm==1.75.0", "black", "scikit-learn", "tensorboardX", diff --git a/tests/llm_optimizers_tests/test_guides.py b/tests/llm_optimizers_tests/test_guides.py index ec04b9f7..f3b6841c 100644 --- a/tests/llm_optimizers_tests/test_guides.py +++ b/tests/llm_optimizers_tests/test_guides.py @@ -7,7 +7,7 @@ def test_auto_guide_build(): assert reference_guide.model == "gpt-4" # Test building ReferenceGuide with custom prompt template - custom_prompt_guide = KeywordSuggest( + custom_prompt_guide = ReferenceSuggest( model="gpt-3.5-turbo", prompt_template="Custom prompt: {content}, Reference: {reference}" ) @@ -22,15 +22,12 @@ def test_auto_guide_build(): assert keyword_guide.keyword_response == keyword_response # Test building KeywordGuide with custom analyzers - # def custom_analyzer(content, reference_log): - # return "Custom analysis result" - # - # analyzer_guide = AutoGuide.build( - # keyword_response={"key": "value"}, - # custom_analyzers=[custom_analyzer] - # ) - # assert isinstance(analyzer_guide, KeywordGuide) - # assert len(analyzer_guide.custom_analyzers) == 1 - # assert analyzer_guide.custom_analyzers[0](None, None) == "Custom analysis result" + def custom_analyzer(content, reference_log): + return "Custom analysis result" + + analyzer_guide = KeywordSuggest(keyword_response={"key": "value"}, custom_analyzers=[custom_analyzer]) + assert isinstance(analyzer_guide, Suggest) + assert len(analyzer_guide.custom_analyzers) == 1 + assert analyzer_guide.custom_analyzers[0](None, None) == "Custom analysis result" # test_auto_guide_build() \ No newline at end of file diff --git a/tests/llm_optimizers_tests/test_optimizer.py b/tests/llm_optimizers_tests/test_optimizer.py index d78961c2..445d03e5 100644 --- a/tests/llm_optimizers_tests/test_optimizer.py +++ b/tests/llm_optimizers_tests/test_optimizer.py @@ -219,7 +219,7 @@ def test_optimizer_customization(optimizer_class): # Try to set custom parameters if the optimizer supports it try: if hasattr(optimizer_class, '__init__') and 'temperature' in inspect.signature(optimizer_class.__init__).parameters: - optimizer = optimizer_class([x], temperature=0.7) + optimizer = optimizer_class([x], temperature=0.0) else: optimizer = optimizer_class([x]) except Exception as e: From 272f9d57b7fd61b930ef227bc8f994a4652d418c Mon Sep 17 00:00:00 2001 From: Adith Swaminathan Date: Tue, 12 Aug 2025 19:35:14 -0700 Subject: [PATCH 147/314] Fixing notebooks in docs, adding tutorial for trainers --- docs/_config.yml | 8 +- docs/_toc.yml | 1 + docs/examples/basic/greeting.ipynb | 183 +- docs/examples/game/negotiation_arena.ipynb | 86 +- docs/examples/nlp/bigbench_hard.ipynb | 32 +- .../numerical/numerical_optimization.ipynb | 24 +- docs/examples/robotics/metaworld.ipynb | 39 +- docs/faq/faq.md | 5 +- docs/intro.md | 26 +- docs/quickstart/installation.md | 8 +- docs/quickstart/quick_start.ipynb | 17 +- docs/quickstart/quick_start_2.ipynb | 112 +- docs/quickstart/virtualhome.md | 9 +- docs/references.bib | 56 - docs/tutorials/error_handling_tutorial.ipynb | 64 +- docs/tutorials/minibatch.ipynb | 590 +++- docs/tutorials/optimization_tutorial.ipynb | 75 +- docs/tutorials/trainers.ipynb | 2860 +++++++++++++++++ examples/bbh/run_prompt_bigbench_trace.py | 1 - examples/virtualhome.py | 25 +- 20 files changed, 3767 insertions(+), 454 deletions(-) delete mode 100644 docs/references.bib create mode 100644 docs/tutorials/trainers.ipynb diff --git a/docs/_config.yml b/docs/_config.yml index 82390eee..53728795 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -23,7 +23,7 @@ latex: # Information about where the book exists on the web repository: - url: https://github.com/microsoft/Trace # Online location of your book + url: https://github.com/AgentOpt/Trace # Online location of your book path_to_book: docs # Optional path to your book, relative to the repository root branch: website # Which branch of the repository should be used when creating links (optional) @@ -36,9 +36,9 @@ html: use_issues_button: false use_repository_button: true extra_navbar: Go to Book Content - extra_footer: "Contact Us | Privacy & Cookies | Consumer Health Privacy | Terms Of Use | Trademarks" + extra_footer: "Contact Us | Terms Of Use | Trademarks" analytics: - plausible_analytics_domain: microsoft.github.io/trace + plausible_analytics_domain: agentopt.github.io/trace plausible_analytics_url: https://plausible.io/js/script.js sphinx: @@ -52,7 +52,7 @@ sphinx: - 'sphinx.ext.viewcode' config: add_module_names: false - plausible_domain: microsoft.github.io/trace + plausible_domain: agentopt.github.io/trace nb_merge_streams: true templates_path: ["_templates"] autosummary_generate: True diff --git a/docs/_toc.yml b/docs/_toc.yml index 47cf5984..990a135a 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -18,6 +18,7 @@ parts: - file: tutorials/error_handling_tutorial - file: tutorials/custom_optimizers - file: tutorials/minibatch + - file: tutorials/trainers - caption: Agent Examples numbered: false diff --git a/docs/examples/basic/greeting.ipynb b/docs/examples/basic/greeting.ipynb index 663773a8..0249a617 100644 --- a/docs/examples/basic/greeting.ipynb +++ b/docs/examples/basic/greeting.ipynb @@ -1,8 +1,9 @@ { "cells": [ { - "metadata": {}, "cell_type": "markdown", + "id": "a5a83b8093fae334", + "metadata": {}, "source": [ "# Greeting Agent\n", "\n", @@ -13,54 +14,102 @@ "## Setup and Installation\n", "\n", "Let's start by importing the necessary libraries." - ], - "id": "a5a83b8093fae334" + ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, - "source": "%pip install trace-opt", - "id": "af6a991e6fa8e083" + "id": "af6a991e6fa8e083", + "metadata": {}, + "outputs": [], + "source": [ + "%pip install trace-opt" + ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, + "id": "500ce27b656605ea", + "metadata": {}, + "outputs": [], "source": [ - "%%capture\n", - "!pip install openai==1.55.3 httpx==0.27.2 --force-reinstall --quiet" - ], - "id": "500ce27b656605ea" + "%pip install openai httpx pywidgets" + ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, + "id": "72b76d44a5423795", + "metadata": {}, + "outputs": [], "source": [ "from opto import trace\n", "from opto.trace import node, bundle, model, ExecutionError\n", "from opto.optimizers import OptoPrime" - ], - "id": "72b76d44a5423795" + ] }, { - "metadata": {}, "cell_type": "markdown", - "source": "Add API keys for LLM calls. Run the code below:", - "id": "88243c6b69d0c2ad" + "id": "88243c6b69d0c2ad", + "metadata": {}, + "source": [ + "Add API keys for LLM calls. Run the code below:" + ] }, { + "cell_type": "code", + "execution_count": 1, + "id": "3242fb533b7cb3f4", "metadata": { "ExecuteTime": { "end_time": "2024-12-10T00:10:08.564966Z", "start_time": "2024-12-10T00:10:08.520705Z" } }, - "cell_type": "code", + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "1bd6aa77089941b6bf1387d59df773d2", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Text(value='OPENAI_API_KEY', description='Env Name:', placeholder='Enter env variable name (e.g., MY_API_KEY)'…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "2c985d3f3ddd439bb6366c58833af31c", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Password(description='API Key:', placeholder='Enter your API key')" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "29026f7b286643a7bd31f4b2ac0533ff", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Button(description='Set API Key', style=ButtonStyle())" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "import os\n", "import ipywidgets as widgets\n", @@ -107,69 +156,24 @@ "\n", "# Attach the callback to the button\n", "submit_button.on_click(on_button_click)" - ], - "id": "3242fb533b7cb3f4", - "outputs": [ - { - "data": { - "text/plain": [ - "Text(value='OPENAI_API_KEY', description='Env Name:', placeholder='Enter env variable name (e.g., MY_API_KEY)'…" - ], - "application/vnd.jupyter.widget-view+json": { - "version_major": 2, - "version_minor": 0, - "model_id": "1bd6aa77089941b6bf1387d59df773d2" - } - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "Password(description='API Key:', placeholder='Enter your API key')" - ], - "application/vnd.jupyter.widget-view+json": { - "version_major": 2, - "version_minor": 0, - "model_id": "2c985d3f3ddd439bb6366c58833af31c" - } - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "Button(description='Set API Key', style=ButtonStyle())" - ], - "application/vnd.jupyter.widget-view+json": { - "version_major": 2, - "version_minor": 0, - "model_id": "29026f7b286643a7bd31f4b2ac0533ff" - } - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "execution_count": 1 + ] }, { - "metadata": {}, "cell_type": "markdown", + "id": "753dc6c3e24a0899", + "metadata": {}, "source": [ "## Define an Agent\n", "\n", "In here, we use `@trace.bundle` to wrap functions so that they show up in TraceGraph. We use `trace.node` to wrap system prompts. `@trace.model` does not do much, except to provide us some convenience to grab all the trainable parameters. |" - ], - "id": "753dc6c3e24a0899" + ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, + "id": "26064f7dfbd2ac2e", + "metadata": {}, + "outputs": [], "source": [ "@trace.model\n", "class Agent:\n", @@ -200,20 +204,22 @@ " \"\"\"Produce a greeting based on the language\"\"\"\n", " greeting = \"Hola\"\n", " return f\"{greeting}, {user_name}!\"" - ], - "id": "26064f7dfbd2ac2e" + ] }, { - "metadata": {}, "cell_type": "markdown", - "source": "## Define Feedback and Training", - "id": "4d45873f3379d594" + "id": "4d45873f3379d594", + "metadata": {}, + "source": [ + "## Define Feedback and Training" + ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, + "id": "43f743d5c27936c8", + "metadata": {}, + "outputs": [], "source": [ "def feedback_fn(generated_response, gold_label='en'):\n", " if gold_label == 'en' and 'Hello' in generated_response:\n", @@ -246,21 +252,22 @@ " break\n", "\n", " return agent" - ], - "id": "43f743d5c27936c8" + ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, - "source": "agent = train()", - "id": "ab2cb1b0c8a4f4b0" + "id": "ab2cb1b0c8a4f4b0", + "metadata": {}, + "outputs": [], + "source": [ + "agent = train()" + ] } ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "trace", "language": "python", "name": "python3" }, @@ -274,7 +281,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython2", - "version": "2.7.6" + "version": "3.9.23" } }, "nbformat": 4, diff --git a/docs/examples/game/negotiation_arena.ipynb b/docs/examples/game/negotiation_arena.ipynb index 00f51823..4ea25a6e 100644 --- a/docs/examples/game/negotiation_arena.ipynb +++ b/docs/examples/game/negotiation_arena.ipynb @@ -12,34 +12,73 @@ "\n", "## Setup\n", "\n", - "First, we'll import the necessary packages and set up our environment." + "First, we'll import the necessary packages and set up our environment. Use the following cell to set the API key for LLM calls." ] }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "# Import necessary libraries\n", "import os\n", - "from openai import OpenAI\n", - "import json\n", - "\n", + "import ipywidgets as widgets\n", + "from IPython.display import display\n", + "\n", + "# Function to save the environment variable and API key\n", + "def save_env_variable(env_name, api_key):\n", + " # Validate inputs\n", + " if not env_name.strip():\n", + " print(\"⚠️ Environment variable name cannot be empty.\")\n", + " return\n", + " if not api_key.strip():\n", + " print(\"⚠️ API key cannot be empty.\")\n", + " return\n", + " \n", + " # Store the API key as an environment variable\n", + " os.environ[env_name] = api_key\n", + " globals()[env_name] = api_key # Set it as a global variable\n", + " print(f\"✅ API key has been set for environment variable: {env_name}\")\n", + "\n", + "# Create the input widgets\n", + "env_name_input = widgets.Text(\n", + " value=\"OPENAI_API_KEY\", # Default value\n", + " description=\"Env Name:\",\n", + " placeholder=\"Enter env variable name (e.g., MY_API_KEY)\",\n", + ")\n", + "\n", + "api_key_input = widgets.Password(\n", + " description=\"API Key:\",\n", + " placeholder=\"Enter your API key\",\n", + ")\n", + "\n", + "# Create the button to submit the inputs\n", + "submit_button = widgets.Button(description=\"Set API Key\")\n", + "\n", + "# Display the widgets\n", + "display(env_name_input, api_key_input, submit_button)\n", + "\n", + "# Callback function for the button click\n", + "def on_button_click(b):\n", + " env_name = env_name_input.value\n", + " api_key = api_key_input.value\n", + " save_env_variable(env_name, api_key)\n", + "\n", + "# Attach the callback to the button\n", + "submit_button.on_click(on_button_click)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ "import opto.trace as trace\n", "from opto.optimizers import OptoPrime\n", - "from autogen import config_list_from_json\n", - "\n", - "config = config_list_from_json(\"OAI_CONFIG_LIST\")\n", - "key = None\n", - "for c in config:\n", - " if c['model'] == 'gpt-4-0125-preview':\n", - " key = c['api_key']\n", - " break\n", - "if key is None:\n", - " raise Exception(\"No key found for gpt-4-0125-preview in the provided config file\")\n", - "\n", - "client = OpenAI(api_key=key)\n" + "from opto.utils.llm import LLM\n", + "\n", + "client = LLM()\n" ] }, { @@ -179,10 +218,12 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ + "import json\n", + "\n", "@trace.bundle(trainable=False)\n", "def chat(player, message):\n", " global system_prompt\n", @@ -190,7 +231,7 @@ " global proposed_trade\n", " global proposed_end\n", " \n", - " current_message = [{'role': 'system', 'content': system_prompt}] + message\n", + " current_message = [{'role': 'system', 'content': system_prompt}, {\"role\": \"user\", \"content\": \"Format your response as a JSON object.\"}] + message\n", "\n", " if len(conversation) > 0:\n", " current_message.append({'role': 'user', 'content': 'This is the transcript of the conversation so far.'})\n", @@ -200,7 +241,6 @@ " current_message.append({'role': 'user', 'content': conversation_history})\n", "\n", " chat = client.chat.completions.create(\n", - " model='gpt-4-0125-preview',\n", " messages=current_message,\n", " temperature=0,\n", " max_tokens=200,\n", @@ -286,7 +326,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -329,7 +369,7 @@ "source": [ "# Initialize optimizer\n", "optimizer = OptoPrime(\n", - " [p1_prompt, p2_prompt], memory_size=0, config_list=config_list_from_json(\"OAI_CONFIG_LIST\")\n", + " [p1_prompt, p2_prompt], memory_size=0\n", " )\n", "\n", "# Run optimization loop\n", diff --git a/docs/examples/nlp/bigbench_hard.ipynb b/docs/examples/nlp/bigbench_hard.ipynb index e49d0cc3..00df852a 100644 --- a/docs/examples/nlp/bigbench_hard.ipynb +++ b/docs/examples/nlp/bigbench_hard.ipynb @@ -28,13 +28,12 @@ }, "outputs": [], "source": [ - "%pip install datasets\n", - "%pip install trace-opt" + "%pip install datasets trace-opt ipywidgets" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2024-07-19T21:19:03.229950Z", @@ -53,24 +52,20 @@ ], "source": [ "# Import necessary libraries\n", - "import autogen\n", "from opto.trace.nodes import node, GRAPH, ParameterNode\n", "from opto.optimizers import OptoPrime\n", "from datasets import load_dataset\n", "from textwrap import dedent\n", - "from opto.trace.bundle import bundle\n", - "from opto.trace.modules import model\n", - "from opto.trace.errors import ExecutionError\n", - "from opto.trace.nodes import ExceptionNode\n", - "from typing import List\n", + "from opto.trace import model, bundle, ExecutionError\n", + "from opto.utils.llm import LLM\n", "import re" ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "import os\n", "import ipywidgets as widgets\n", @@ -161,7 +156,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2024-07-19T21:19:03.880915Z", @@ -171,10 +166,8 @@ "outputs": [], "source": [ "class LLMCallable:\n", - " def __init__(self, config_list=None, max_tokens=1024, verbose=False):\n", - " if config_list is None:\n", - " config_list = autogen.config_list_from_json(\"OAI_CONFIG_LIST\")\n", - " self.llm = autogen.OpenAIWrapper(config_list=config_list)\n", + " def __init__(self, llm=None, max_tokens=1024, verbose=False):\n", + " self.llm = llm or LLM()\n", " self.max_tokens = max_tokens\n", " self.verbose = verbose\n", "\n", @@ -182,7 +175,7 @@ " def call_llm(self, user_prompt):\n", " system_prompt = \"You are a helpful assistant.\\n\"\n", " messages = [{\"role\": \"system\", \"content\": system_prompt}, {\"role\": \"user\", \"content\": user_prompt}]\n", - " response = self.llm.create(messages=messages, max_tokens=self.max_tokens)\n", + " response = self.llm(messages=messages, max_tokens=self.max_tokens)\n", " response = response.choices[0].message.content\n", "\n", " if self.verbose:\n", @@ -311,7 +304,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2024-07-19T21:19:21.867979Z", @@ -941,8 +934,7 @@ "examples = [{\"question\": r[\"input\"], \"answer\": r[\"target\"]} for r in train_set]\n", "\n", "dp = Predict()\n", - "optimizer = OptoPrime(dp.parameters(),\n", - " config_list=autogen.config_list_from_json(\"OAI_CONFIG_LIST\"))\n", + "optimizer = OptoPrime(dp.parameters())\n", "\n", "print(\"Training on a few examples:\")\n", "train(dp, optimizer, examples[:5])\n", diff --git a/docs/examples/numerical/numerical_optimization.ipynb b/docs/examples/numerical/numerical_optimization.ipynb index 4a08530e..2160f85d 100644 --- a/docs/examples/numerical/numerical_optimization.ipynb +++ b/docs/examples/numerical/numerical_optimization.ipynb @@ -29,14 +29,12 @@ }, "outputs": [], "source": [ - "%pip install trace-opt\n", - "%pip install uxsim\n", - "%pip install numpy" + "%pip install trace-opt ipywidgets uxsim numpy" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -52,18 +50,16 @@ "import numpy as np\n", "import uxsim as ux\n", "import itertools\n", - "import opto\n", "import opto.trace as trace\n", "from opto.optimizers import OptoPrime\n", - "from opto.trace.bundle import ExceptionNode\n", - "from autogen import config_list_from_json" + "from opto.trace.bundle import ExceptionNode" ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "import os\n", "import ipywidgets as widgets\n", @@ -265,11 +261,11 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "def run_approach(num_iter, trace_memory=0, trace_config=\"OAI_CONFIG_LIST\"):\n", + "def run_approach(num_iter, trace_memory=0):\n", " W = None\n", " return_val = np.zeros((num_iter, 3))\n", " \n", @@ -289,10 +285,10 @@ " return_dict = analyze_world(W)\n", " return return_dict\n", "\n", - " EW_x = trace.node(MIN_GREEN_TIME, trainable=True, constraint=f\"[{MIN_GREEN_TIME},{MAX_GREEN_TIME}]\")\n", - " NS_x = trace.node(MIN_GREEN_TIME, trainable=True, constraint=f\"[{MIN_GREEN_TIME},{MAX_GREEN_TIME}]\")\n", + " EW_x = trace.node(MIN_GREEN_TIME, trainable=True, description=f\"Value constrained to be within [{MIN_GREEN_TIME},{MAX_GREEN_TIME}]\")\n", + " NS_x = trace.node(MIN_GREEN_TIME, trainable=True, description=f\"Value constrained to be within [{MIN_GREEN_TIME},{MAX_GREEN_TIME}]\")\n", " optimizer = OptoPrime(\n", - " [EW_x, NS_x], memory_size=trace_memory, config_list=config_list_from_json(trace_config)\n", + " [EW_x, NS_x], memory_size=trace_memory\n", " )\n", "\n", " optimizer.objective = (\n", diff --git a/docs/examples/robotics/metaworld.ipynb b/docs/examples/robotics/metaworld.ipynb index b9342510..14c2829c 100644 --- a/docs/examples/robotics/metaworld.ipynb +++ b/docs/examples/robotics/metaworld.ipynb @@ -29,30 +29,29 @@ }, "outputs": [], "source": [ - "%pip install trace-opt" + "%pip install trace-opt ipywidgets" ] }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "from autogen import config_list_from_json\n", "import llfbench\n", "import random\n", "import numpy as np\n", "import opto.trace as trace\n", "from opto.optimizers import OptoPrime\n", "from opto.trace.bundle import ExceptionNode\n", - "from opto.trace.errors import ExecutionError\n" + "from opto.trace.errors import ExecutionError" ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "import os\n", "import ipywidgets as widgets\n", @@ -234,7 +233,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -245,8 +244,7 @@ " n_optimization_steps=100,\n", " seed=0,\n", " relative=True,\n", - " verbose=False,\n", - " model=\"gpt-4-0125-preview\",\n", + " verbose=False\n", "):\n", "\n", " @trace.bundle(trainable=True)\n", @@ -261,9 +259,7 @@ " \"\"\"\n", " return [0, 0, 0, 0]\n", "\n", - " config_list = config_list_from_json(\"OAI_CONFIG_LIST\")\n", - " config_list = [config for config in config_list if config[\"model\"] == model]\n", - " optimizer = OptoPrime(controller.parameters(), config_list=config_list, memory_size=memory_size)\n", + " optimizer = OptoPrime(controller.parameters(), memory_size=memory_size)\n", "\n", " env = TracedEnv(env_name, seed=seed, relative=relative)\n", "\n", @@ -325,22 +321,22 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "/home/chinganc/miniconda3/envs/trace/lib/python3.8/site-packages/gymnasium/utils/passive_env_checker.py:32: UserWarning: \u001B[33mWARN: A Box observation space maximum and minimum values are equal. Actual equal coordinates: [(36,), (37,), (38,)]\u001B[0m\n", + "/home/chinganc/miniconda3/envs/trace/lib/python3.8/site-packages/gymnasium/utils/passive_env_checker.py:32: UserWarning: \u001b[33mWARN: A Box observation space maximum and minimum values are equal. Actual equal coordinates: [(36,), (37,), (38,)]\u001b[0m\n", " logger.warn(\n", - "/home/chinganc/miniconda3/envs/trace/lib/python3.8/site-packages/gymnasium/utils/passive_env_checker.py:159: UserWarning: \u001B[33mWARN: The obs returned by the `reset()` method is not within the observation space.\u001B[0m\n", + "/home/chinganc/miniconda3/envs/trace/lib/python3.8/site-packages/gymnasium/utils/passive_env_checker.py:159: UserWarning: \u001b[33mWARN: The obs returned by the `reset()` method is not within the observation space.\u001b[0m\n", " logger.warn(f\"{pre} is not within the observation space.\")\n", - "/home/chinganc/miniconda3/envs/trace/lib/python3.8/site-packages/gymnasium/utils/passive_env_checker.py:131: UserWarning: \u001B[33mWARN: The obs returned by the `reset()` method was expecting a numpy array, actual type: \u001B[0m\n", + "/home/chinganc/miniconda3/envs/trace/lib/python3.8/site-packages/gymnasium/utils/passive_env_checker.py:131: UserWarning: \u001b[33mWARN: The obs returned by the `reset()` method was expecting a numpy array, actual type: \u001b[0m\n", " logger.warn(\n", - "/home/chinganc/miniconda3/envs/trace/lib/python3.8/site-packages/gymnasium/spaces/box.py:240: UserWarning: \u001B[33mWARN: Casting input x to numpy array.\u001B[0m\n", + "/home/chinganc/miniconda3/envs/trace/lib/python3.8/site-packages/gymnasium/spaces/box.py:240: UserWarning: \u001b[33mWARN: Casting input x to numpy array.\u001b[0m\n", " gym.logger.warn(\"Casting input x to numpy array.\")\n", - "/home/chinganc/miniconda3/envs/trace/lib/python3.8/site-packages/gymnasium/core.py:311: UserWarning: \u001B[33mWARN: env.control_mode to get variables from other wrappers is deprecated and will be removed in v1.0, to get this variable you can do `env.unwrapped.control_mode` for environment variables or `env.get_wrapper_attr('control_mode')` that will search the reminding wrappers.\u001B[0m\n", + "/home/chinganc/miniconda3/envs/trace/lib/python3.8/site-packages/gymnasium/core.py:311: UserWarning: \u001b[33mWARN: env.control_mode to get variables from other wrappers is deprecated and will be removed in v1.0, to get this variable you can do `env.unwrapped.control_mode` for environment variables or `env.get_wrapper_attr('control_mode')` that will search the reminding wrappers.\u001b[0m\n", " logger.warn(\n" ] }, @@ -355,9 +351,9 @@ "name": "stderr", "output_type": "stream", "text": [ - "/home/chinganc/miniconda3/envs/trace/lib/python3.8/site-packages/gymnasium/utils/passive_env_checker.py:159: UserWarning: \u001B[33mWARN: The obs returned by the `step()` method is not within the observation space.\u001B[0m\n", + "/home/chinganc/miniconda3/envs/trace/lib/python3.8/site-packages/gymnasium/utils/passive_env_checker.py:159: UserWarning: \u001b[33mWARN: The obs returned by the `step()` method is not within the observation space.\u001b[0m\n", " logger.warn(f\"{pre} is not within the observation space.\")\n", - "/home/chinganc/miniconda3/envs/trace/lib/python3.8/site-packages/gymnasium/utils/passive_env_checker.py:131: UserWarning: \u001B[33mWARN: The obs returned by the `step()` method was expecting a numpy array, actual type: \u001B[0m\n", + "/home/chinganc/miniconda3/envs/trace/lib/python3.8/site-packages/gymnasium/utils/passive_env_checker.py:131: UserWarning: \u001b[33mWARN: The obs returned by the `step()` method was expecting a numpy array, actual type: \u001b[0m\n", " logger.warn(\n" ] }, @@ -1469,8 +1465,7 @@ " memory_size=5,\n", " seed=0,\n", " relative=True,\n", - " verbose='output',\n", - " model=\"gpt-4-0125-preview\"\n", + " verbose='output'\n", ")\n" ] }, diff --git a/docs/faq/faq.md b/docs/faq/faq.md index 0c4e1926..7c6231e3 100644 --- a/docs/faq/faq.md +++ b/docs/faq/faq.md @@ -37,11 +37,8 @@ The table evaluates the frameworks in the following aspects: We provide a comparison to validate our implementation of TextGrad in Trace:

- drawing + drawing

To produce this table, we ran the TextGrad pip-installed repo on 2024-10-30, and we also include the numbers reported in the TextGrad paper. The LLM APIs are called around the same time to ensure a fair comparison. TextGrad paper's result was reported in 2024-06. - -## Difference to Libraries like AutoGen, AG2, OpenAI Swarm, Llama Stack - diff --git a/docs/intro.md b/docs/intro.md index 1fae750c..1f330d39 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -4,7 +4,7 @@ **It can record *traces* of operations on any Python objects and functions, and automatically construct an execution graph that is useful when LLMs are used as optimizers.** -Open In Colab +Open In Colab Our implementation is minimal and purely based on Python. It does not involve any API calls or library-specific dependencies, so it is composable with other libraries and tools. Trace features an API design inspired by PyTorch Autograd's gradient tape mechanism, which we adopted to reduce the learning curve of using Trace. @@ -29,20 +29,22 @@ After the user has declared the inputs and operations, Trace captures the execut Finally, the user can optimize the entire program, such as by updating the LLM instructions, using Trace. This step is the **optimize** phase. ```python +from opto import trace + @trace.model class Agent: def __init__(self, system_prompt): self.system_prompt = system_prompt self.instruct1 = trace.node("Decide the language", trainable=True) - self.instruct2 = trace.node("Extract name", trainable=True) + self.instruct2 = trace.node("Extract name if it's there", trainable=True) def __call__(self, user_query): # First LLM - response = call_llm(self.system_prompt, self.instruct1, user_query) + response = trace.operators.call_llm(self.system_prompt, self.instruct1, user_query) en_or_es = self.decide_lang(response) # Second LLM - user_name = call_llm(self.system_prompt, self.instruct2, user_query) + user_name = trace.operators.call_llm(self.system_prompt, self.instruct2, user_query) greeting = self.greet(en_or_es, user_name) return greeting @@ -63,16 +65,26 @@ Enabling traces of operations on Python objects allows us to capture the executi In the example below, we show how Trace can optimize an entire AI system end-to-end. ```python +from opto.optimizers import OptoPrime + +def feedback_fn(generated_response, gold_label='en'): + if gold_label == 'en' and 'Hello' in generated_response: + return "Correct" + elif gold_label == 'es' and 'Hola' in generated_response: + return "Correct" + else: + return "Incorrect" + agent = Agent("You are a sales assistant.") optimizer = OptoPrime(agent.parameters()) try: greeting = agent("Hola, soy Juan.") feedback = feedback_fn(greeting.data, 'es') - # feedback = "Correct" or "Incorrect" -except ExecutionError as e: + # feedback == "Correct" or "Incorrect" +except trace.ExecutionError as e: greeting = e.exception_node - feedback = greeting.data, + feedback = greeting.data optimizer.zero_feedback() optimizer.backward(greeting, feedback) diff --git a/docs/quickstart/installation.md b/docs/quickstart/installation.md index 1f4d1d1c..0c74a0ac 100644 --- a/docs/quickstart/installation.md +++ b/docs/quickstart/installation.md @@ -7,7 +7,7 @@ The ability to capture execution trace of Python program is defined in `opto.tra any external dependencies. However, if you want to use optimizer `opto.optimizers`, -then we require `autogen` package to make LLM API calls. +then we require `LiteLLM` package to make LLM API calls. To install Trace, run: @@ -19,12 +19,8 @@ pip install trace-opt To contribute to the development, you can clone the repository and install the package in editable mode: -```{tip} -The installation script will git clone a version of AutoGen. -You may require Git Large File Storage if git is unable to clone the repository otherwise. - ```bash -git clone https://github.com/microsoft/Trace.git +git clone https://github.com/AgentOpt/Trace.git cd Trace pip install -e . ``` \ No newline at end of file diff --git a/docs/quickstart/quick_start.ipynb b/docs/quickstart/quick_start.ipynb index 5a93a08c..ce8b17a4 100644 --- a/docs/quickstart/quick_start.ipynb +++ b/docs/quickstart/quick_start.ipynb @@ -28,14 +28,16 @@ }, "outputs": [], "source": [ - "%pip install trace-opt" + "%pip install trace-opt\n", + "%pip install ipywidgets" ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, + "id": "150ebe0c019eb767", + "metadata": {}, + "outputs": [], "source": [ "import os\n", "import ipywidgets as widgets\n", @@ -82,8 +84,7 @@ "\n", "# Attach the callback to the button\n", "submit_button.on_click(on_button_click)" - ], - "id": "150ebe0c019eb767" + ] }, { "cell_type": "markdown", @@ -358,7 +359,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "id": "81d783e1-462f-4938-9f2c-389b4b546a74", "metadata": {}, "outputs": [ @@ -372,7 +373,6 @@ } ], "source": [ - "import autogen\n", "from opto.optimizers import OptoPrime\n", "from opto import trace\n", "\n", @@ -901,7 +901,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "id": "db1ea71c-9a4e-4e14-9584-b19bff6ae1ae", "metadata": {}, "outputs": [ @@ -921,7 +921,6 @@ } ], "source": [ - "import autogen\n", "from opto.optimizers import OptoPrime\n", "\n", "GRAPH.clear()\n", diff --git a/docs/quickstart/quick_start_2.ipynb b/docs/quickstart/quick_start_2.ipynb index b668a2c4..0c9c1e1b 100644 --- a/docs/quickstart/quick_start_2.ipynb +++ b/docs/quickstart/quick_start_2.ipynb @@ -27,7 +27,8 @@ }, "outputs": [], "source": [ - "%pip install trace-opt" + "%pip install trace-opt\n", + "%pip install ipywidgets" ] }, { @@ -45,10 +46,11 @@ ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, + "id": "dea338357fc76304", + "metadata": {}, + "outputs": [], "source": [ "import os\n", "import ipywidgets as widgets\n", @@ -95,8 +97,7 @@ "\n", "# Attach the callback to the button\n", "submit_button.on_click(on_button_click)" - ], - "id": "dea338357fc76304" + ] }, { "cell_type": "markdown", @@ -122,7 +123,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "0edf4c3f", "metadata": {}, "outputs": [], @@ -132,7 +133,7 @@ "import importlib.util\n", "\n", "# Define the raw URL for downloading\n", - "raw_url = \"https://raw.githubusercontent.com/microsoft/Trace/main/examples/battleship.py\"\n", + "raw_url = \"https://raw.githubusercontent.com/agentopt/Trace/main/examples/battleship.py\"\n", "\n", "# Define the local file path\n", "local_file = \"battleship.py\"\n", @@ -609,7 +610,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "id": "e4536733-89c0-4245-802b-d5812dd38d0c", "metadata": { "editable": true, @@ -2190,7 +2191,6 @@ } ], "source": [ - "import autogen\n", "from opto.trace.utils import render_opt_step\n", "from battleship import BattleshipBoard\n", "\n", @@ -2295,7 +2295,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 19, "id": "16daeec5-27ef-44c7-9395-cc6a7264e230", "metadata": { "editable": true, @@ -2372,14 +2372,14 @@ " \n", " \n", "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", "\n", "
ABCDEFGH
1
2
3
4
5
6
7
8
1
2
3
4
5
6
7
8
" ], @@ -2456,7 +2456,7 @@ " \n", "\n", "\n", - "\n", + "\n", "\n", "\n", "\n", @@ -2538,8 +2538,8 @@ " \n", "
ABCDEFGH
1
2
2
3
4
5
\n", "\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "\n", @@ -2621,8 +2621,8 @@ " \n", "
ABCDEFGH
1
2
1
2
3
4
5
\n", "\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "\n", @@ -2704,8 +2704,8 @@ " \n", "
ABCDEFGH
1
2
1
2
3
4
5
\n", "\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "\n", @@ -2787,8 +2787,8 @@ " \n", "
ABCDEFGH
1
2
1
2
3
4
5
\n", "\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "\n", @@ -2870,8 +2870,8 @@ " \n", "
ABCDEFGH
1
2
1
2
3
4
5
\n", "\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "\n", @@ -2953,8 +2953,8 @@ " \n", "
ABCDEFGH
1
2
1
2
3
4
5
\n", "\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "\n", @@ -3036,8 +3036,8 @@ " \n", "
ABCDEFGH
1
2
1
2
3
4
5
\n", "\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "\n", @@ -3119,8 +3119,8 @@ " \n", "
ABCDEFGH
1
2
1
2
3
4
5
\n", "\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "\n", @@ -3202,8 +3202,8 @@ " \n", "
ABCDEFGH
1
2
1
2
3
4
5
\n", "\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "\n", @@ -3285,8 +3285,8 @@ " \n", "
ABCDEFGH
1
2
1
2
3
4
5
\n", "\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "\n", @@ -3368,9 +3368,9 @@ " \n", "
ABCDEFGH
1
2
1
2
3
4
5
\n", "\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", @@ -3451,9 +3451,9 @@ " \n", "
ABCDEFGH
1
2
3
1
2
3
4
5
6
\n", "\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", @@ -3534,9 +3534,9 @@ " \n", "
ABCDEFGH
1
2
3
1
2
3
4
5
6
\n", "\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", @@ -3617,9 +3617,9 @@ " \n", "
ABCDEFGH
1
2
3
1
2
3
4
5
6
\n", "\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", @@ -3656,14 +3656,6 @@ " if terminal:\n", " break" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "34cd5783-c79c-4f7e-b6a5-a4a138103927", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { @@ -3682,7 +3674,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.15" + "version": "3.9.23" } }, "nbformat": 4, diff --git a/docs/quickstart/virtualhome.md b/docs/quickstart/virtualhome.md index 799fbf15..3f48a184 100644 --- a/docs/quickstart/virtualhome.md +++ b/docs/quickstart/virtualhome.md @@ -130,19 +130,22 @@ optimizer1 = OptoPrime([agent1.plan]) optimizer2 = OptoPrime([agent2.plan]) agents = [agent1, agent2] +optimizers = [optimizer1, optimizer2] ``` We then run the simulation for a fixed number of steps. In each step, we observe the environment, and each agent produces an action based on its observation. ```python -from examples.virtualhome import VirtualHomeEnv +from examples.virtualhome import VirtualHomeEnv, env_fn horizon = 50 +task_id = 8 -env = VirtualHomeEnv() +env = VirtualHomeEnv(max_number_steps=horizon, run_id=0, env_fn=env_fn(env_id=0, env_task_set=task_id), + agent_fn=agents, num_agents=len(agents)) # we specify a task in this environment -agent_obs, agent_obs_descs, agent_goal_specs, agent_goal_descs, agent_infos = env.reset(task_id=8) +agent_obs, agent_obs_descs, agent_goal_specs, agent_goal_descs, agent_infos = env.reset(task_id=task_id) for h in range(horizon): plans, errors = {}, {} diff --git a/docs/references.bib b/docs/references.bib deleted file mode 100644 index 783ec6aa..00000000 --- a/docs/references.bib +++ /dev/null @@ -1,56 +0,0 @@ ---- ---- - -@inproceedings{holdgraf_evidence_2014, - address = {Brisbane, Australia, Australia}, - title = {Evidence for {Predictive} {Coding} in {Human} {Auditory} {Cortex}}, - booktitle = {International {Conference} on {Cognitive} {Neuroscience}}, - publisher = {Frontiers in Neuroscience}, - author = {Holdgraf, Christopher Ramsay and de Heer, Wendy and Pasley, Brian N. and Knight, Robert T.}, - year = {2014} -} - -@article{holdgraf_rapid_2016, - title = {Rapid tuning shifts in human auditory cortex enhance speech intelligibility}, - volume = {7}, - issn = {2041-1723}, - url = {http://www.nature.com/doifinder/10.1038/ncomms13654}, - doi = {10.1038/ncomms13654}, - number = {May}, - journal = {Nature Communications}, - author = {Holdgraf, Christopher Ramsay and de Heer, Wendy and Pasley, Brian N. and Rieger, Jochem W. and Crone, Nathan and Lin, Jack J. and Knight, Robert T. and Theunissen, Frédéric E.}, - year = {2016}, - pages = {13654}, - file = {Holdgraf et al. - 2016 - Rapid tuning shifts in human auditory cortex enhance speech intelligibility.pdf:C\:\\Users\\chold\\Zotero\\storage\\MDQP3JWE\\Holdgraf et al. - 2016 - Rapid tuning shifts in human auditory cortex enhance speech intelligibility.pdf:application/pdf} -} - -@inproceedings{holdgraf_portable_2017, - title = {Portable learning environments for hands-on computational instruction using container-and cloud-based technology to teach data science}, - volume = {Part F1287}, - isbn = {978-1-4503-5272-7}, - doi = {10.1145/3093338.3093370}, - abstract = {© 2017 ACM. There is an increasing interest in learning outside of the traditional classroom setting. This is especially true for topics covering computational tools and data science, as both are challenging to incorporate in the standard curriculum. These atypical learning environments offer new opportunities for teaching, particularly when it comes to combining conceptual knowledge with hands-on experience/expertise with methods and skills. Advances in cloud computing and containerized environments provide an attractive opportunity to improve the effciency and ease with which students can learn. This manuscript details recent advances towards using commonly-Available cloud computing services and advanced cyberinfrastructure support for improving the learning experience in bootcamp-style events. We cover the benets (and challenges) of using a server hosted remotely instead of relying on student laptops, discuss the technology that was used in order to make this possible, and give suggestions for how others could implement and improve upon this model for pedagogy and reproducibility.}, - booktitle = {{ACM} {International} {Conference} {Proceeding} {Series}}, - author = {Holdgraf, Christopher Ramsay and Culich, A. and Rokem, A. and Deniz, F. and Alegro, M. and Ushizima, D.}, - year = {2017}, - keywords = {Teaching, Bootcamps, Cloud computing, Data science, Docker, Pedagogy} -} - -@article{holdgraf_encoding_2017, - title = {Encoding and decoding models in cognitive electrophysiology}, - volume = {11}, - issn = {16625137}, - doi = {10.3389/fnsys.2017.00061}, - abstract = {© 2017 Holdgraf, Rieger, Micheli, Martin, Knight and Theunissen. Cognitive neuroscience has seen rapid growth in the size and complexity of data recorded from the human brain as well as in the computational tools available to analyze this data. This data explosion has resulted in an increased use of multivariate, model-based methods for asking neuroscience questions, allowing scientists to investigate multiple hypotheses with a single dataset, to use complex, time-varying stimuli, and to study the human brain under more naturalistic conditions. These tools come in the form of “Encoding” models, in which stimulus features are used to model brain activity, and “Decoding” models, in which neural features are used to generated a stimulus output. Here we review the current state of encoding and decoding models in cognitive electrophysiology and provide a practical guide toward conducting experiments and analyses in this emerging field. Our examples focus on using linear models in the study of human language and audition. We show how to calculate auditory receptive fields from natural sounds as well as how to decode neural recordings to predict speech. The paper aims to be a useful tutorial to these approaches, and a practical introduction to using machine learning and applied statistics to build models of neural activity. The data analytic approaches we discuss may also be applied to other sensory modalities, motor systems, and cognitive systems, and we cover some examples in these areas. In addition, a collection of Jupyter notebooks is publicly available as a complement to the material covered in this paper, providing code examples and tutorials for predictive modeling in python. The aimis to provide a practical understanding of predictivemodeling of human brain data and to propose best-practices in conducting these analyses.}, - journal = {Frontiers in Systems Neuroscience}, - author = {Holdgraf, Christopher Ramsay and Rieger, J.W. and Micheli, C. and Martin, S. and Knight, R.T. and Theunissen, F.E.}, - year = {2017}, - keywords = {Decoding models, Encoding models, Electrocorticography (ECoG), Electrophysiology/evoked potentials, Machine learning applied to neuroscience, Natural stimuli, Predictive modeling, Tutorials} -} - -@book{ruby, - title = {The Ruby Programming Language}, - author = {Flanagan, David and Matsumoto, Yukihiro}, - year = {2008}, - publisher = {O'Reilly Media} -} diff --git a/docs/tutorials/error_handling_tutorial.ipynb b/docs/tutorials/error_handling_tutorial.ipynb index 1eeaafd9..8ec81fe1 100644 --- a/docs/tutorials/error_handling_tutorial.ipynb +++ b/docs/tutorials/error_handling_tutorial.ipynb @@ -20,7 +20,67 @@ }, "outputs": [], "source": [ - "%pip install trace-opt" + "%pip install trace-opt ipywidgets" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The code below provides a way to specify your API_KEY for calling LLMs using LiteLLM as part of this tutorial notebook. Alternatively, provide the keys by setting environment variables or loading LiteLLM config files." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import ipywidgets as widgets\n", + "from IPython.display import display\n", + "\n", + "# Function to save the environment variable and API key\n", + "def save_env_variable(env_name, api_key):\n", + " # Validate inputs\n", + " if not env_name.strip():\n", + " print(\"⚠️ Environment variable name cannot be empty.\")\n", + " return\n", + " if not api_key.strip():\n", + " print(\"⚠️ API key cannot be empty.\")\n", + " return\n", + " \n", + " # Store the API key as an environment variable\n", + " os.environ[env_name] = api_key\n", + " globals()[env_name] = api_key # Set it as a global variable\n", + " print(f\"✅ API key has been set for environment variable: {env_name}\")\n", + "\n", + "# Create the input widgets\n", + "env_name_input = widgets.Text(\n", + " value=\"OPENAI_API_KEY\", # Default value\n", + " description=\"Env Name:\",\n", + " placeholder=\"Enter env variable name (e.g., MY_API_KEY)\",\n", + ")\n", + "\n", + "api_key_input = widgets.Password(\n", + " description=\"API Key:\",\n", + " placeholder=\"Enter your API key\",\n", + ")\n", + "\n", + "# Create the button to submit the inputs\n", + "submit_button = widgets.Button(description=\"Set API Key\")\n", + "\n", + "# Display the widgets\n", + "display(env_name_input, api_key_input, submit_button)\n", + "\n", + "# Callback function for the button click\n", + "def on_button_click(b):\n", + " env_name = env_name_input.value\n", + " api_key = api_key_input.value\n", + " save_env_variable(env_name, api_key)\n", + "\n", + "# Attach the callback to the button\n", + "submit_button.on_click(on_button_click)" ] }, { @@ -257,7 +317,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.15" + "version": "3.9.23" } }, "nbformat": 4, diff --git a/docs/tutorials/minibatch.ipynb b/docs/tutorials/minibatch.ipynb index 80fd12b9..dd1ad029 100644 --- a/docs/tutorials/minibatch.ipynb +++ b/docs/tutorials/minibatch.ipynb @@ -11,58 +11,73 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": { "collapsed": false }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Looking in indexes: https://pypi.netflix.net/simple\n", - "Requirement already satisfied: trace-opt in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (0.1.1)\n", - "Requirement already satisfied: autogen-agentchat~=0.2 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from trace-opt) (0.2.37)\n", - "Requirement already satisfied: graphviz>=0.20.1 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from trace-opt) (0.20.3)\n", - "Requirement already satisfied: scikit-learn in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from trace-opt) (1.5.1)\n", - "Requirement already satisfied: xgboost in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from trace-opt) (2.1.1)\n", - "Requirement already satisfied: diskcache in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from autogen-agentchat~=0.2->trace-opt) (5.6.3)\n", - "Requirement already satisfied: docker in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from autogen-agentchat~=0.2->trace-opt) (7.1.0)\n", - "Requirement already satisfied: flaml in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from autogen-agentchat~=0.2->trace-opt) (2.3.1)\n", - "Requirement already satisfied: numpy<2,>=1.17.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from autogen-agentchat~=0.2->trace-opt) (1.26.4)\n", - "Requirement already satisfied: openai>=1.3 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from autogen-agentchat~=0.2->trace-opt) (1.52.2)\n", - "Requirement already satisfied: packaging in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from autogen-agentchat~=0.2->trace-opt) (24.1)\n", - "Requirement already satisfied: pydantic!=2.6.0,<3,>=1.10 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from autogen-agentchat~=0.2->trace-opt) (2.9.2)\n", - "Requirement already satisfied: python-dotenv in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from autogen-agentchat~=0.2->trace-opt) (1.0.1)\n", - "Requirement already satisfied: termcolor in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from autogen-agentchat~=0.2->trace-opt) (2.5.0)\n", - "Requirement already satisfied: tiktoken in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from autogen-agentchat~=0.2->trace-opt) (0.8.0)\n", - "Requirement already satisfied: scipy>=1.6.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from scikit-learn->trace-opt) (1.13.1)\n", - "Requirement already satisfied: joblib>=1.2.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from scikit-learn->trace-opt) (1.4.2)\n", - "Requirement already satisfied: threadpoolctl>=3.1.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from scikit-learn->trace-opt) (3.5.0)\n", - "Requirement already satisfied: anyio<5,>=3.5.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from openai>=1.3->autogen-agentchat~=0.2->trace-opt) (4.6.2.post1)\n", - "Requirement already satisfied: distro<2,>=1.7.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from openai>=1.3->autogen-agentchat~=0.2->trace-opt) (1.9.0)\n", - "Requirement already satisfied: httpx<1,>=0.23.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from openai>=1.3->autogen-agentchat~=0.2->trace-opt) (0.27.2)\n", - "Requirement already satisfied: jiter<1,>=0.4.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from openai>=1.3->autogen-agentchat~=0.2->trace-opt) (0.6.1)\n", - "Requirement already satisfied: sniffio in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from openai>=1.3->autogen-agentchat~=0.2->trace-opt) (1.3.1)\n", - "Requirement already satisfied: tqdm>4 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from openai>=1.3->autogen-agentchat~=0.2->trace-opt) (4.66.6)\n", - "Requirement already satisfied: typing-extensions<5,>=4.11 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from openai>=1.3->autogen-agentchat~=0.2->trace-opt) (4.12.2)\n", - "Requirement already satisfied: annotated-types>=0.6.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from pydantic!=2.6.0,<3,>=1.10->autogen-agentchat~=0.2->trace-opt) (0.7.0)\n", - "Requirement already satisfied: pydantic-core==2.23.4 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from pydantic!=2.6.0,<3,>=1.10->autogen-agentchat~=0.2->trace-opt) (2.23.4)\n", - "Requirement already satisfied: requests>=2.26.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from docker->autogen-agentchat~=0.2->trace-opt) (2.32.3)\n", - "Requirement already satisfied: urllib3>=1.26.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from docker->autogen-agentchat~=0.2->trace-opt) (2.2.3)\n", - "Requirement already satisfied: regex>=2022.1.18 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from tiktoken->autogen-agentchat~=0.2->trace-opt) (2024.9.11)\n", - "Requirement already satisfied: idna>=2.8 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from anyio<5,>=3.5.0->openai>=1.3->autogen-agentchat~=0.2->trace-opt) (3.7)\n", - "Requirement already satisfied: exceptiongroup>=1.0.2 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from anyio<5,>=3.5.0->openai>=1.3->autogen-agentchat~=0.2->trace-opt) (1.2.2)\n", - "Requirement already satisfied: certifi in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from httpx<1,>=0.23.0->openai>=1.3->autogen-agentchat~=0.2->trace-opt) (2024.8.30)\n", - "Requirement already satisfied: httpcore==1.* in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from httpx<1,>=0.23.0->openai>=1.3->autogen-agentchat~=0.2->trace-opt) (1.0.6)\n", - "Requirement already satisfied: h11<0.15,>=0.13 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from httpcore==1.*->httpx<1,>=0.23.0->openai>=1.3->autogen-agentchat~=0.2->trace-opt) (0.14.0)\n", - "Requirement already satisfied: charset-normalizer<4,>=2 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from requests>=2.26.0->docker->autogen-agentchat~=0.2->trace-opt) (3.3.2)\n", - "Note: you may need to restart the kernel to use updated packages.\n" - ] - } - ], + "outputs": [], "source": [ - "%pip install trace-opt" + "%pip install trace-opt ipywidgets" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As a preamble, the code below provides a way to specify your API_KEY for calling LLMs using LiteLLM as part of this tutorial notebook. Alternatively, provide the keys by setting environment variables or loading LiteLLM config files." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import ipywidgets as widgets\n", + "from IPython.display import display\n", + "\n", + "# Function to save the environment variable and API key\n", + "def save_env_variable(env_name, api_key):\n", + " # Validate inputs\n", + " if not env_name.strip():\n", + " print(\"⚠️ Environment variable name cannot be empty.\")\n", + " return\n", + " if not api_key.strip():\n", + " print(\"⚠️ API key cannot be empty.\")\n", + " return\n", + " \n", + " # Store the API key as an environment variable\n", + " os.environ[env_name] = api_key\n", + " globals()[env_name] = api_key # Set it as a global variable\n", + " print(f\"✅ API key has been set for environment variable: {env_name}\")\n", + "\n", + "# Create the input widgets\n", + "env_name_input = widgets.Text(\n", + " value=\"OPENAI_API_KEY\", # Default value\n", + " description=\"Env Name:\",\n", + " placeholder=\"Enter env variable name (e.g., MY_API_KEY)\",\n", + ")\n", + "\n", + "api_key_input = widgets.Password(\n", + " description=\"API Key:\",\n", + " placeholder=\"Enter your API key\",\n", + ")\n", + "\n", + "# Create the button to submit the inputs\n", + "submit_button = widgets.Button(description=\"Set API Key\")\n", + "\n", + "# Display the widgets\n", + "display(env_name_input, api_key_input, submit_button)\n", + "\n", + "# Callback function for the button click\n", + "def on_button_click(b):\n", + " env_name = env_name_input.value\n", + " api_key = api_key_input.value\n", + " save_env_variable(env_name, api_key)\n", + "\n", + "# Attach the callback to the button\n", + "submit_button.on_click(on_button_click)" ] }, { @@ -74,7 +89,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 33, "metadata": {}, "outputs": [], "source": [ @@ -100,19 +115,12 @@ "def loss(y_hat, y):\n", " \"\"\" A least squares loss function. \"\"\"\n", " return (y_hat - y) ** 2\n", - "\n", - "\n", - "def compute_loss(inputs, outputs):\n", - " l = 0\n", - " for x,y in zip(inputs, outputs):\n", - " y_hat = fun(x)\n", - " l += loss(y_hat, y)\n", - " return l\n" + "\n" ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 34, "metadata": {}, "outputs": [ { @@ -120,24 +128,25 @@ "output_type": "stream", "text": [ "Iteration 0 Loss: 85\n", - "Iteration 1 Loss: 10\n", + "Iteration 1 Loss: 85\n", "Iteration 2 Loss: 10\n", - "Iteration 3 Loss: 7.5\n", - "Iteration 4 Loss: 122.8125\n", - "Iteration 5 Loss: 80.3125\n", - "Iteration 6 Loss: 12.8125\n", - "Iteration 7 Loss: 10.0\n", - "Iteration 8 Loss: 7.5\n", - "Iteration 9 Loss: 8.150000000000002\n", - "Iteration 10 Loss: 6.449999999999999\n", - "Iteration 11 Loss: 8.150000000000002\n", - "Iteration 12 Loss: 9.037500000000001\n", - "Iteration 13 Loss: 9.427\n" + "Iteration 3 Loss: 15\n", + "Iteration 4 Loss: 10\n", + "Iteration 5 Loss: 40\n", + "Iteration 6 Loss: 0\n", + "Iteration 7 Loss: 0\n", + "Iteration 8 Loss: 0\n", + "Iteration 9 Loss: 0\n", + "Iteration 10 Loss: 0\n", + "Iteration 11 Loss: 0\n", + "Iteration 12 Loss: 0\n", + "Iteration 13 Loss: 0\n", + "Iteration 14 Loss: 0\n" ] }, { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjsAAAGwCAYAAABPSaTdAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAAA9hAAAPYQGoP6dpAABLiUlEQVR4nO3de3iU5Z0//vczx2Qmk8k5QyAkgUSCnAW14oH4Feh6rMtlreJx7W9bv6CVWuuh9JC1Nal0pbSwaulapVpW+92qdbvbCngAkbqcBBURBEIIkElISCYzk2SOz++PmefJhCSQhJl5nnnm/bquXMLMkHyImrxz35/PfQuiKIogIiIi0iid0gUQERERJRLDDhEREWkaww4RERFpGsMOERERaRrDDhEREWkaww4RERFpGsMOERERaZpB6QLUIBwO4+TJk7DZbBAEQelyiIiIaBhEUYTb7UZJSQl0uqHXbxh2AJw8eRKlpaVKl0FERESj0NTUhHHjxg35PMMOAJvNBiDyycrOzla4GiIiIhqOrq4ulJaWyt/Hh8KwA8hbV9nZ2Qw7REREKeZcLShsUCYiIiJNY9ghIiIiTWPYISIiIk1j2CEiIiJNY9ghIiIiTWPYISIiIk1j2CEiIiJNY9ghIiIiTWPYISIiIk1j2CEiIiJNY9ghIiIiTWPYISIiIk1j2CFSiWAorHQJRESaxLBDpALffW0PLql7B82uHqVLISLSHIYdIhV4/0ArTnv9+OunTqVLISLSHIYdIoX1BkLo6A4AADYfPKVwNURE2sOwQ6Sw1i6f/OuPjrSjNxBSsBoiIu1h2CFSWIu7V/61LxjGR0faFayGiEh7FA07W7ZswY033oiSkhIIgoA333xTfi4QCOCxxx7DtGnTYLVaUVJSgrvvvhsnT57s9z58Ph8efPBBFBQUwGq14qabbsLx48eT/DchGj2nq7ff77mVRUQUX4qGHa/XixkzZmDNmjUDnuvu7sbu3bvxox/9CLt378brr7+OgwcP4qabbur3umXLluGNN97Aq6++iq1bt8Lj8eCGG25AKMStAEoNLV2RsGPLMAAANh9g2CEiiieDkh/82muvxbXXXjvoc3a7HRs3buz32OrVq3HJJZfg2LFjGD9+PFwuF1544QW8/PLLmD9/PgDglVdeQWlpKTZt2oSvfvWrCf87EJ0vKexcN3UM/rT7OI60eXGsvRvj8y0KV0ZEpA0p1bPjcrkgCAJycnIAALt27UIgEMDChQvl15SUlGDq1KnYtm3bkO/H5/Ohq6ur3xuRUpzRBuXKoixcVJYLANj8JVd3iIjiJWXCTm9vLx5//HEsXrwY2dnZAACn0wmTyYTc3Nx+ry0uLobTOfR5JfX19bDb7fJbaWlpQmsnOpuWaM9OsT0D8y4oBABsPtCqZElERJqSEmEnEAjgtttuQzgcxrPPPnvO14uiCEEQhnz+iSeegMvlkt+ampriWS7RiEjTWI7sDNRMioSdbYfb4Quy74yIKB5UH3YCgQBuvfVWNDQ0YOPGjfKqDgA4HA74/X50dHT0+zOtra0oLi4e8n2azWZkZ2f3eyNSgiiK8jRWcbYZF47JRqHNjG5/CLuOdpzjTxMR0XCoOuxIQefLL7/Epk2bkJ+f3+/52bNnw2g09mtkbm5uxmeffYa5c+cmu1yiEXP1BOALRi4ALc7OgCAI8lbW+xxBJyKKC0XDjsfjwZ49e7Bnzx4AQENDA/bs2YNjx44hGAzilltuwc6dO/GHP/wBoVAITqcTTqcTfr8fQGRi65vf/Ca+973v4Z133sHHH3+MO++8E9OmTZOns4jUrCXanJxjMSLDqAeAmL4dhh0ionhQdPR8586duPrqq+XfP/zwwwCAe+65B7W1tXjrrbcAADNnzuz359577z3U1NQAAH75y1/CYDDg1ltvRU9PD6655hq89NJL0Ov1Sfk7EJ0PZ3TsvNiWIT92ZVUBdAJwoMWNk509KMnJVKo8IiJNUDTs1NTUQBTFIZ8/23OSjIwMrF69GqtXr45naURJETuJJcmxmDCzNAe7j3Viy8FTuO2S8UqVR0SkCaru2SHSOulAQUe2ud/j8y4oAsCrI4iI4oFhh0hB8jZWdka/x+dFR9C3ftmGQCic9LqIiLSEYYdIQS1DhJ3pY+3Is5rg9gXx8bFOBSojItIOhh0iBUnTWI4zwo5OJ+DKqgIAwOaDPE2ZiOh8MOwQKWiobSygbwT9fY6gExGdF4YdIoUEQmG0eSIrO8V284Dnr4qGnX0nu9AavVKCiIhGjmGHSCFtHh9EEdDrBBRYB4adgiwzpo21AwA+ONiW7PKIiDSDYYdIIdKdWEU2M3S6wS+u5dURRETnj2GHSCFDTWLFkm5B/+DLUwiFz33IJhERDcSwQ6SQoSaxYs0szUF2hgGd3QHsPd6ZpMqIiLSFYYdIIX2TWAP7dSQGvQ5XVvFiUCKi88GwQ6SQwe7FGox8Czr7doiIRoVhh0ghLW7pXqyzhx1pBH3v8U6c9voTXhcRkdYw7BApRJrGOluDMgA47BmodtggipFGZSIiGhmGHSKFSA3K5wo7QN/FoNzKIiIaOYYdIgV4fEF4fEEAkZWbc5H6drYcPIUwR9CJiEaEYYdIAdIZO1aTHllmwzlfP6csD1aTHm0ePz5v7kp0eUREmsKwQ6SA4U5iSUwGHeZWSregcyuLiGgkGHaIFDDcSaxYfbegtyakJiIirWLYIVKA0zX85mSJFHZ2H+uEqyeQkLqIiLSIYYdIAcO5F+tMpXkWTCy0IhQWse0Qb0EnIhouhh0iBUhhx3GWqyIGM++CIgDA+7w6goho2Bh2iBTgHMXKDtB3C/rmg6cgihxBJyIaDoYdIgWMdBpLcklFHjKMOji7enGgxZ2I0oiINIdhhyjJwmERre5Ig/JIprEAIMOox2UT8gHwFnQiouFi2CFKsnavH8GwCEEACm0j69kBeAs6EdFIMewQJZnUnJxvNcOoH/n/gvMmRZqUdxw9LV85QUREQ2PYIUoyeRLLPvJVHQCoKLCiLN+CQEjE3w+3x7M0IiJNYtghSjJ5Ess2sn6dWH1bWTxNmYjoXBh2iJJstJNYsfqujuAIOhHRuTDsECVZS9foJrFiXTYxHya9Dsc7enCkzRuv0oiINIlhhyjJ+g4UHF3PDgBYTAZcUpEHgCPoRETnwrBDlGSjuRdrMPJWFkfQiYjOimGHKMn6prHOL+xIV0f875F29AZC510XEZFWMewQJVFvIISO7gCA85vGAoDKoiyU2DPgC4bx9yMcQSciGgrDDlEStUabk00GHXIsxvN6X4IgYJ50MSj7doiIhsSwQ5RELe7oFlZ2BgRBOO/3N++CyGnKW9i3Q0Q0JIYdoiRyus5/EivW5ZX5MOgEHGnz4lh7d1zeJxGR1jDsECVRvCaxJLYMI2aX5QLgacpERENh2CFKInkSK05hB0Bf3w63soiIBsWwQ5REzmiDcrxWdoC+83a2HW6HL8gRdCKiMzHsECVRPO7FOtOFY7JRaDOj2x/CzqMdcXu/RERawbBDlESx01jxIghCzC3o3MoiIjoTww5RkoiiKE9jxTPsALG3oLNJmYjoTAw7REni6gnAFwwDAIriNHouubKqADoBONjiwcnOnri+byKiVMewQ5QkLdHm5ByLERlGfVzfd47FhJmlOQC4lUVEdCaGHaIkcSZg7DyWdJoyr44gIupP0bCzZcsW3HjjjSgpKYEgCHjzzTf7PS+KImpra1FSUoLMzEzU1NRg3759/V7j8/nw4IMPoqCgAFarFTfddBOOHz+exL8F0fBIk1hFCQo70i3oHx5qQyAUTsjHICJKRYqGHa/XixkzZmDNmjWDPr9ixQqsXLkSa9aswY4dO+BwOLBgwQK43W75NcuWLcMbb7yBV199FVu3boXH48ENN9yAUIjnjZC69B0oGN9+Hcm0sXbkWU1w+4LY3cgRdCIiiaJh59prr8XPfvYzLFq0aMBzoihi1apVWL58ORYtWoSpU6di3bp16O7uxvr16wEALpcLL7zwAp555hnMnz8fs2bNwiuvvIJPP/0UmzZtSvZfh+isEr2NpdMJuLKqAAD7doiIYqm2Z6ehoQFOpxMLFy6UHzObzZg3bx62bdsGANi1axcCgUC/15SUlGDq1Knyawbj8/nQ1dXV740o0aSVnURtYwF9W1kMO0REfVQbdpxOJwCguLi43+PFxcXyc06nEyaTCbm5uUO+ZjD19fWw2+3yW2lpaZyrJxpImsZK1MoOAFxZFQk7+052oTV6gCERUbpTbdiRCILQ7/eiKA547Eznes0TTzwBl8slvzU1NcWlVqKzkbex4nhVxJkKssyYPs4OANhysC1hH4eIKJWoNuw4HA4AGLBC09raKq/2OBwO+P1+dHR0DPmawZjNZmRnZ/d7I0qkQCiMNk9kZSfeBwqeiVdHEBH1p9qwU1FRAYfDgY0bN8qP+f1+bN68GXPnzgUAzJ49G0ajsd9rmpub8dlnn8mvIVKDNo8PoggYdAIKrMkJOx98eQqhsJjQj0VElAoMSn5wj8eDQ4cOyb9vaGjAnj17kJeXh/Hjx2PZsmWoq6tDVVUVqqqqUFdXB4vFgsWLFwMA7HY7vvnNb+J73/se8vPzkZeXh0ceeQTTpk3D/PnzlfprEQ0g3YlVZDNDpzv7Nuz5mlmag+wMAzq7A9h7vBMXjc899x8iItIwRcPOzp07cfXVV8u/f/jhhwEA99xzD1566SU8+uij6OnpwZIlS9DR0YFLL70UGzZsgM1mk//ML3/5SxgMBtx6663o6enBNddcg5deegl6fXyP4yc6H8mYxJIY9DpcWVWI//60GZsPnGLYIaK0J4iimPbr3F1dXbDb7XC5XOzfoYRYt+0ofvLWPvzDFAeev2t2wj/eH3c04dE/fYIZpTn489LLE/7xiIiUMNzv36rt2SHSkmRMYsWaFz1v55PjnTjt9SflYxIRqRXDDlES9N2LldjmZElxdgaqHTaIYqRRmYgonTHsECVBizuxV0UMRlrd4S3oRJTuGHaIkkCaxkpm2Km5oAgAsOXLUwhzBJ2I0hjDDlESSFdFJGMaSzK7LBdWkx5tHj/2neT9b0SUvhh2iBLM4wvC4wsCSF6DMgCYDDrMrZRuQW9N2sclIlIbhh2iBJPO2MkyG5BlTu7RVrwFnYiIYYco4ZI9iRVLujpi97FOuHoCSf/4RERqwLBDlGBKTGJJxuVaUFmUhVBYxIeHeAs6EaUnhh2iBHO6Is3JSoQdIOYWdI6gE1GaYtghSrBk3os1GDnsHDwF3g5DROmIYYcowaSw41CgZwcALqnIQ4ZRB2dXLw60uBWpgYhISQw7RAmW7HuxzpRh1OOyCfkAgPe5lUVEaYhhhyjB+qaxlAk7APt2iCi9MewQJVA4LKLVrWyDMgDUTIpcHbGz8bR8wCERUbpg2CFKoHavH8GwCEEACm3K9OwAQHmBFWX5FgRCIrZxBJ2I0gzDDlECSc3J+VYzjHpl/3eLncoiIkonDDtECSRPYtmVW9WRxF4dwRF0IkonDDtECSRPYinYryP5yoR8mPQ6HO/oweFTXqXLISJKGoYdogRSwySWxGIy4JKKPADcyiKi9MKwQ5RALV3KT2LF4i3oRJSOGHaIEkhN21hAX5PyR0fa0eMPKVwNEVFyMOwQJVDfvVjKNygDQGVRFsbmZMIfDOOjhnalyyEiSgqGHaIEalH4qogzCYKAq3iaMhGlGYYdogTpDYTQ0R0AoJ5tLIDn7RBR+mHYIUqQ1mhzssmggz3TqHA1fS6vzIdBJ6ChzYvGdo6gE5H2MewQJUiLu685WRAEhavpY8swYnZZLgCu7hBRemDYIUoQp0tdk1ix5k1i3w4RpQ+GHaIEUdskVqyaCyK3oG873A5fkCPoRKRtDDtECdKisjN2Yk0eY0OhzYyeQAg7GjqULoeIKKEYdogSxCmdnqySsfNYgiDETGW1KlwNEVFiMewQJYia7sUaDK+OIKJ0wbBDlCCx01hqdEVlAXQCcLDFg5OdPUqXQ0SUMAw7RAkgiqKqp7EAIMdiwszSHABc3SEibWPYSbBwWIQ/GFa6DEoyV08Avui/dzVOY0lqJkWmsjiCTkRaxrCTQLVv7cOUn7yNP+5sUroUSrKWaHNyjsWIDKNe4WqGJjUpf3ioDYEQQzkRaRPDTgIZ9QJ6AiEcavUoXQolmVPFY+expo21I89qgtsXxO5GjqATkTYx7CRQZVEWAODwKYaddKP2SSyJTifgqqoCAMD77NshIo1i2EkgKexwZSf99B0oqN5+HQmvjiAirWPYSaDKQhsAoNnVC48vqHA1lEypso0FAFdVFUIQgM+bu9AarZuISEsYdhLIbjGiICvyk/1hru6klb57sdQfdvKzzJg21g6AI+hEpE0MOwlWWWQFwK2sdCNNY6XCyg6AmKsjGHaISHsYdhJM7tthk3JakbexVHgv1mCkqyM++LINobCocDVERPHFsJNglYVsUk43gVAYbZ7Iyo6aDxSMNWNcDrIzDHD1BLCnqVPpcoiI4ophJ8EqiyJNyuzZSR9tHh9EETDoBBRYUyPsGPQ6XFnFrSwi0iaGnQSTtrEaT3fz2og0Id2JVWQzQ6cTFK5m+ObxFnQi0iiGnQQrzjYjy2xAKCziaLtX6XIoCVJpEiuW1KT8yfFOtEe34YiItEDVYScYDOKHP/whKioqkJmZiQkTJuDJJ59EONy3QiKKImpra1FSUoLMzEzU1NRg3759ClbdnyAImMjDBdNKqk1iSYqzM1DtsEEUga2H2pQuh4goblQddp5++mk8//zzWLNmDfbv348VK1bgF7/4BVavXi2/ZsWKFVi5ciXWrFmDHTt2wOFwYMGCBXC73QpW3h+blNNLqk1ixeIt6ESkRaoOO3//+9/xta99Dddffz3Ky8txyy23YOHChdi5cyeAyKrOqlWrsHz5cixatAhTp07FunXr0N3djfXr1ytcfR9eG5Fe+u7FSo3m5FjSVhZXdohIS1Qddq644gq88847OHjwIABg79692Lp1K6677joAQENDA5xOJxYuXCj/GbPZjHnz5mHbtm1Dvl+fz4eurq5+b4nEsJNeWtypc1XEmWaU2iEIQKvbJ4/PExGlOoPSBZzNY489BpfLherqauj1eoRCITz11FO4/fbbAQBOpxMAUFxc3O/PFRcXo7Gxccj3W19fj3/5l39JXOFnkMLOkTYPwmExpSZ0aOSkaaxUDDsWkwFleRYcbe/GAacbBZWptzpFRHQmVa/svPbaa3jllVewfv167N69G+vWrcO//uu/Yt26df1eJwj9w4MoigMei/XEE0/A5XLJb01NTQmpX1KamwmTXofeQBgnOnsS+rFIeVKDcqpNY0mqHdkAgC+c6ul7IyI6H6pe2fn+97+Pxx9/HLfddhsAYNq0aWhsbER9fT3uueceOBwOAJEVnjFjxsh/rrW1dcBqTyyz2QyzOXk/sRr0OlQUWHGgxY1DrR6U5lmS9rEpuTy+oHzDfSo2KAPAJIcNf9vnxBfNid3eJSJKFlWv7HR3d0On61+iXq+XR88rKirgcDiwceNG+Xm/34/Nmzdj7ty5Sa31XNi3kx6kM3ayzAZkmVX9s8SQJo+JnPp9oIUrO0SkDar+anzjjTfiqaeewvjx4zFlyhR8/PHHWLlyJe677z4Ake2rZcuWoa6uDlVVVaiqqkJdXR0sFgsWL16scPX98ayd9JDKk1gSaRvrgNONUFiEnj1mRJTiVB12Vq9ejR/96EdYsmQJWltbUVJSgm9/+9v48Y9/LL/m0UcfRU9PD5YsWYKOjg5ceuml2LBhA2w2m4KVD8Tbz9NDKk9iScbnWZBp1KMnEEJjuxcToudEERGlKlWHHZvNhlWrVmHVqlVDvkYQBNTW1qK2tjZpdY1G7MGC52qgptTldKXm6cmxdDoBFzhs2NvUiS+cboYdIkp5qu7Z0ZIJhVYIAuDqCaDN41e6HEqQVL0X60zVxZGVUTYpE5EWMOwkSYZRj9LcyBQW+3a0Swo7jhTu2QGA6miTMsfPiUgLGHaSiH072pfK92LFmuRg2CEi7WDYSSIp7Bzmyo5m9U1jpXbYkSayjp3uhjd6bhARUapi2Eki3n6ubeGwiFZ36jcoA0Ce1YQiW2QrjuftEFGqY9hJIp61o23tXj+CYRGCABTaUrtnBwCqx/Sdt0NElMoYdpJI2sZydvXC3RtQuBqKN6k5Od9qhlGf+v9rTXZwIouItCH1vyKnEHumUf6J//Apr8LVULzJk1j21F/VAdikTETawbCTZOzb0S55EivF+3Uksbefi6KocDVERKPHsJNkvBBUu7QyiSWZWGSFXifA1ROQgxwRUSpi2Ekyhh3taunSxiSWxGzQY2KhFQC3sogotTHsJJl81g4PFtQcrW1jAcAkaSurmWGHiFIXw06SSWGnsd0LXzCkcDUUT333YmmjQRkAqqNNygecnMgiotTFsJNkRTYzbGYDwiJwtK1b6XIojlo0clVErGpOZBGRBjDsJJkgCDxcUIN6AyF0dEfOTtLSNpZ0sODhUx74g2GFqyEiGh2GHQWwSVl7WqPNySaDDvZMo8LVxE+JPQO2DAMCIRFH2vjfKxGlJoYdBfD2c+1pcfc1JwuCoHA18SMIQkzfDreyiCg1MewogAcLao/Tpb1JLIl0uOB+TmQRUYpi2FGAtLJz5JQHoTBPptUCLU5iSfqujeBEFhGlJoYdBZTmWWAy6OALhnGio0fpcigOWjR4xo5k8hhuYxFRamPYUYBeJ2BCQeRk2kOn+A1EC5zS6ckaGjuXXFAcCTvNrl64ohNnRESphGFHIRw/1xat3YsVy5ZhxLjcTADcyiKi1MSwoxA2KWtL7DSWFvFwQSJKZQw7CuFZO9ohiqKmp7GAvokshh0iSkUMOwqJDTuiyImsVObqCcAXPV1Yi9NYAFA9hhNZRJS6GHYUUlFghU4AunqDOOXxKV0OnYeWaHNyjsWIDKNe4WoSI/ZgwTCPSyCiFDOqsNPU1ITjx4/Lv9++fTuWLVuGtWvXxq0wrcsw6lGaZwHAraxU59Tw2LmkPN8Kk0GHbn8Ix3lcAhGlmFGFncWLF+O9994DADidTixYsADbt2/HD37wAzz55JNxLVDLpCblwww7KU3Lk1gSg16HqujW635uZRFRihlV2Pnss89wySWXAAD++Mc/YurUqdi2bRvWr1+Pl156KZ71aRqblLWh70BBbfbrSKQmZR4uSESpZlRhJxAIwGyOfGHftGkTbrrpJgBAdXU1mpub41edxk3khaCakA7bWEDs+DlXdogotYwq7EyZMgXPP/88PvjgA2zcuBH/8A//AAA4efIk8vPz41qglnFlRxv67sXSeNgZw7N2iCg1jSrsPP300/jNb36Dmpoa3H777ZgxYwYA4K233pK3t+jcpLDT0uVDVy+P4U9V0jSW1ld2pAtBj7Z50RsIKVwNEdHwGUbzh2pqatDW1oauri7k5ubKj3/rW9+CxWKJW3Fal51hRJHNjFa3D4dbPZg1Pvfcf4hUR97G0uC9WLEKs8zIt5rQ7vXjyxYPpo2zK10SEdGwjGplp6enBz6fTw46jY2NWLVqFQ4cOICioqK4Fqh13MpKbYFQGG3Rc5K0eqCgRBAEeSuLE1lElEpGFXa+9rWv4fe//z0AoLOzE5deeimeeeYZ3HzzzXjuuefiWqDWVbJJOaW1eXwQRcCgE1Bg1XbYAYBJxdFrI5rZt0NEqWNUYWf37t248sorAQD/+Z//ieLiYjQ2NuL3v/89fv3rX8e1QK2Twg7P2klN0p1YRTYzdDpB4WoST1rZOdDClR0iSh2jCjvd3d2w2SJf9DZs2IBFixZBp9PhK1/5ChobG+NaoNbx9vPUli6TWBJ5/JwrO0SUQkYVdiorK/Hmm2+iqakJb7/9NhYuXAgAaG1tRXZ2dlwL1DppZefY6W5OuKSgdJnEklQV2aATgHavH6fcvNONiFLDqMLOj3/8YzzyyCMoLy/HJZdcgssuuwxAZJVn1qxZcS1Q6wptZtgyDAiLwNF2r9Ll0AilyySWJNOkR3m+FQAPFySi1DGqsHPLLbfg2LFj2LlzJ95++2358WuuuQa//OUv41ZcOhAEgRNZKazvXiztNydL5L4dHi5IRCliVGEHABwOB2bNmoWTJ0/ixIkTAIBLLrkE1dXVcSsuXbBvJ3W1uNPjqohY0kTWfvbtEFGKGFXYCYfDePLJJ2G321FWVobx48cjJycHP/3pTxEOh+Ndo+ZxZSd1SdNY6RR2OJFFRKlmVCcoL1++HC+88AJ+/vOf4/LLL4coivjwww9RW1uL3t5ePPXUU/GuU9MYdlKX1KCcLtNYADA5evv5wRYPgqEwDPpRLxATESXFqMLOunXr8O///u/ybecAMGPGDIwdOxZLlixh2BkhKewcafMiFBahT4PzWrTA4wvC4wsCSJ8GZQAYl5sJi0mPbn8IR9u9qCyyKV0SEdFZjepHstOnTw/am1NdXY3Tp0+fd1HpZlyuBSaDDv5gGMc7upUuh4ZJOmMny2xAlnlUPzekJJ1OkC8F5Q3oRJQKRhV2ZsyYgTVr1gx4fM2aNZg+ffp5F5Vu9DoBEwoi47zcykod6TiJJeHhgkSUSkb14+iKFStw/fXXY9OmTbjssssgCAK2bduGpqYm/M///E+8a0wLlUVZ+MLpxqFWD66ZXKx0OTQM6TiJJamO9u1wZYeIUsGoVnbmzZuHgwcP4h//8R/R2dmJ06dPY9GiRdi3bx9efPHFuBZ44sQJ3HnnncjPz4fFYsHMmTOxa9cu+XlRFFFbW4uSkhJkZmaipqYG+/bti2sNycAm5dTjdKXX6cmx+raxOJFFROo36kaDkpKSAY3Ie/fuxbp16/C73/3uvAsDgI6ODlx++eW4+uqr8de//hVFRUU4fPgwcnJy5NesWLECK1euxEsvvYQLLrgAP/vZz7BgwQIcOHBAvr8rFfD289STbvdixZK2sY539MDdG4Atw6hwRUREQ1N1V+XTTz+N0tLSfqtF5eXl8q9FUcSqVauwfPlyLFq0CEBkUqy4uBjr16/Ht7/97UHfr8/ng8/Xd69PV5fyP53GruyIoghB4ESW2klhx5GGPTs5FhMc2RlwdvXiYIsbs8vylC6JiGhIqj4g46233sKcOXPw9a9/HUVFRZg1axZ++9vfys83NDTA6XTKF5ECgNlsxrx587Bt27Yh3299fT3sdrv8VlpamtC/x3BUFFihEwB3b5AXLKaIdLsX60zS4YLs2yEitVN12Dly5Aiee+45VFVV4e2338b999+P73znO/j9738PAHA6nQCA4uL+Db3FxcXyc4N54okn4HK55LempqbE/SWGyWzQY3yeBQD7dlJF3zRWmoYdqUmZE1lEpHIj2saStoqG0tnZeT61DBAOhzFnzhzU1dUBAGbNmoV9+/bhueeew9133y2/7swtn3NtA5nNZpjN6tt6qCzKwtH2bhw65cHcygKly6GzCIdFtLrTt0EZiBk/Z5MyEanciFZ2Yrd+BnsrKyvrF0LO15gxY3DhhRf2e2zy5Mk4duwYgMhlpAAGrOK0trYOWO1JBRM5kZUy2r1+BMMiBAEotKkvOCdD7DaWKIoKV0NENLQRrezEe6z8XC6//HIcOHCg32MHDx5EWVkZAKCiogIOhwMbN27ErFmzAAB+vx+bN2/G008/ndRa44G3n6cOqTk532qGMU3vhppQkAWDToC7N4iTrl6MzclUuiQiokGp+qv0d7/7XXz00Ueoq6vDoUOHsH79eqxduxZLly4FENm+WrZsGerq6vDGG2/gs88+w7333guLxYLFixcrXP3I8ayd1CFPYtnTc1UHAEwGnfzf7AFuZRGRiql69Pziiy/GG2+8gSeeeAJPPvkkKioqsGrVKtxxxx3yax599FH09PRgyZIl6OjowKWXXooNGzak1Bk7Emkbq9XtQ1dvANk8u0S15EmsNO3XkUxy2PCF0439zW78n+rU2zomovSg6rADADfccANuuOGGIZ8XBAG1tbWora1NXlEJkp1hRHG2GS1dPhxq9eCi8blKl0RDSPdJLEm1Ixt/xkkc4Pg5EamYqrex0hG3slJDS1d6T2JJOJFFRKmAYUdlpCblwww7qsZtrAhpIuvwKS98wZDC1RARDY5hR2W4spMa+u7FSt8GZSAS9uyZRoTCIg63epUuh4hoUAw7KjORF4KmhJY0vypCIggCb0AnItVj2FEZaWWn6XQ3egPcFlCj3kAIHd0BANzGAoDJ0bDDJmUiUiuGHZUpzDIjO8OAsAg0tHFbQI1ao83JJoMO9kweDzApekfWfoYdIlIphh2VEQSBfTsq1+Lua04+2x1s6UJqUubBgkSkVgw7KsSwo25OFyexYl1QHAk7LV0+dHj9CldDRDQQw44KVbJJWdWk5uTiNG9OlmSZDRifZwEQuRSUiEhtGHZUSAo7PGtHneSwk6a3nQ+GE1lEpGYMOypUWRj5xnGkzYtQWFS4GjqTUzo9mSs7Mmki64tmruwQkfow7KjQ2NxMmA06+INhNJ3uVrocOoN0L1Yxe3Zk1WMiE1lftDDsEJH6MOyokF4nYEIhm5TVSprGYtjpI21jHXS6EeZqJBGpDMOOSrFJWZ1EUeQ01iDK860wG3ToCYRwjKuRRKQyDDsqVcmVHVVy9QTgC4YB8F6sWHqdII+gs0mZiNSGYUeleNaOOrVEm5NzLEZkGPUKV6Mu1fJEFvt2iEhdGHZUKnb8XBTZA6EWzi5uYQ1lEieyiEilGHZUqrzAAp0AuH1BtLp9SpdDUZzEGtrk6ETWAU5kEZHKMOyolNmgR1m+FQC3stREPlCQ/ToDSCs7R9u96PYHFa6GiKgPw46KTWSTsupwG2toBVlmFGSZIYrAwRb+N0tE6sGwo2JsUlYf3ot1dpN5AzoRqRDDjoox7KiPNI1VbGPYGcyk6Pj5fjYpE5GKMOyoGA8WVB95G4srO4OSro04wPFzIlIRhh0Vm1gYaVA+5fbB1RNQuBoKhMJo80RXdtizM6jqmNvPeWQCEakFw46K2TKMciMst7KU1+bxQRQBg05AvtWkdDmqVFmUBZ0AdHQHcIpHJhCRSjDsqFzs4YKkLOlOrCKbGTqdoHA16pRh1KOiILIiuZ9bWUSkEgw7Kse+HfXgJNbwSH07XzRzIouI1IFhR+UmciJLNTiJNTzVxdL4OVd2iEgdGHZUjrefqwcnsYZHWtnhNhYRqQXDjspJ21hNHd3oDYQUria98V6s4ZEmsg63ehAIhRWuhoiIYUf1CrJMsGcaIYrAkVNepctJay1u3os1HONyM5FlNsAfCqOhjf/NEpHyGHZUThAENimrhDSNxXuxzk4QBPlS0C+4lUVEKsCwkwLYt6MOcoMye3bOSQ47nMgiIhVg2EkBPGtHeR5fEB5fEAB7doZjsoMTWUSkHgw7KYAXgipPOmMny2xAltmgcDXqN8kRPWuHYYeIVIBhJwVIYaehzYsgp1sU0TeJxebk4ZC2sU509vBeNyJSHMNOChibk4kMow7+UBhNHT1Kl5OW+iaxuIU1HPZMI0qivU0HW7i6Q0TKYthJATqdgAkF3MpSktMVaU7mJNbw8doIIlILhp0Uwb4dZfFerJGr5vg5EakEw06KYNhRlhx2bOzZGS6etUNEasGwkyJ4sKCyeC/WyE2ObmMdcLohiqLC1RBROmPYSRGxZ+3wG0fy8V6skasosMKoF+DxBXGcjfVEpCCGnRRRnm+FXhf5xiGd5EvJEQ6LaHVHT09m2Bk2o16HyiIeLkhEymPYSREmgw5leRYA7NtJtnavH8GwCEEACtmzMyJ9TcqcyCIi5TDspJCJcpMyf0pOJqk5uSDLDKOe/8uMhBR29nNlh4gUxK/cKYRNysqQJ7F4evKITeIdWUSkAikVdurr6yEIApYtWyY/JooiamtrUVJSgszMTNTU1GDfvn3KFZlAvP1cGfIkFvt1RkyayGpo86I3EFK4GiJKVykTdnbs2IG1a9di+vTp/R5fsWIFVq5ciTVr1mDHjh1wOBxYsGAB3G7t/STZd9aOV+FK0gsnsUavyGZGrsWIUFhkSCcixaRE2PF4PLjjjjvw29/+Frm5ufLjoihi1apVWL58ORYtWoSpU6di3bp16O7uxvr16xWsODGknp02jw+ubl6umCzS9BvDzsgJgsDDBYlIcSkRdpYuXYrrr78e8+fP7/d4Q0MDnE4nFi5cKD9mNpsxb948bNu2bcj35/P50NXV1e8tFWSZDRgTPdTu0Cl+40gWbmOdn2qHdLhgavx/RkTao/qw8+qrr2L37t2or68f8JzT6QQAFBcX93u8uLhYfm4w9fX1sNvt8ltpaWl8i04gXhuRfLwX6/zwjiwiUpqqw05TUxMeeughvPLKK8jIGPobjSAI/X4viuKAx2I98cQTcLlc8ltTU1Pcak60iWxSTjpOY50f+fZzhh0iUohB6QLOZteuXWhtbcXs2bPlx0KhELZs2YI1a9bgwIEDACIrPGPGjJFf09raOmC1J5bZbIbZnJrfuLiyk1y9gRA6ov1R3MYanQuKsyAIwCm3D20eHwqyUvP/PSJKXape2bnmmmvw6aefYs+ePfLbnDlzcMcdd2DPnj2YMGECHA4HNm7cKP8Zv9+PzZs3Y+7cuQpWnjg8aye5WqPNyWaDDvZMo8LVpCaLySCf/s3zdohICape2bHZbJg6dWq/x6xWK/Lz8+XHly1bhrq6OlRVVaGqqgp1dXWwWCxYvHixEiUnnBR2jnf0oDcQQoZRr3BF2tbi7hs7P9vWKJ3dJIcNR9u78YXTjcsrC5Quh4jSjKrDznA8+uij6OnpwZIlS9DR0YFLL70UGzZsgM1mU7q0hMi3mpBjMaKzO4DDpzyYUmJXuiRNc7o4iRUP1Y5svL2vBV80cyKLiJIv5cLO+++/3+/3giCgtrYWtbW1itSTbIIgoLIwCzsbO3ColWEn0TiJFR+Tx0SvjWjhNhYRJZ+qe3ZocNJW1mE2KSecHHZ42/l5mSSfteNGKCwqXA0RpRuGnRTEJuXkcUYblB1c2Tkv4/MsyDTq4QuG0djO606IKLkYdlLQRI6fJw3vxYoPvU7ABcWR/2553g4RJRvDTgqSbj9vaPMiGAorXI22xU5j0fmRro1gkzIRJRvDTgoam5OJTKMegZCIY6e7lS5Hs0RR5DRWHPFCUCJSCsNOCtLpBEwotALgVlYiuXoC8AUjK2dFvCrivFWPYdghImUw7KQoNiknXku0OTnHYuThjXEgbWMdO90Nry+ocDVElE4YdlJUJS8ETThnF7ew4inPakJRdISf5+0QUTIx7KQonrWTeJzEij/pBnTekUVEycSwk6LksHPKC1HkIW2JIB8oyH6duKmWmpQ5kUVEScSwk6LK8q3Q6wR4fEF5u4Xii9tY8VfNiSwiUgDDTooyGXQoy7cAYN9OovBerPiLHT/niiQRJQvDTgqTmpS/bGHYSQRpGqvYxrATL5VFWdDrBLh6AlyRJKKkYdhJYRw/Tyx5G4srO3FjNugxoSByRhS3sogoWRh2Ulgl78hKmEAojDZPdGWHPTtxJU1kfdHMsENEycGwk8I4fp44bR4fRBEw6ATkW01Kl6MpUpPyAScnsogoORh2UtjEaM9Ou9ePDq9f4Wq0RboTq8hmhk4nKFyNtnAii4iSjWEnhVnNBpRE+0nYtxNfnMRKHGkb6/ApD/zRu8eIiBKJYSfFTWTfTkJwEitxSuwZsGUYEAiJONLG/26JKPEYdlIcm5QTg5NYiSMIQkzfDreyiCjxGHZSHMNOYvBerMSSDhfcz4ksIkoChp0Ux9vPE6PFzXuxEqnaER0/50QWESUBw06Kk1Z2TnT2oNsfVLga7ZCmsXgvVmJwG4uIkolhJ8XlZ5mRazECAI6c8ipcjXbIDcrs2UmIC6Jhp9nVC1d3QOFqiEjrGHY0gH078eXxBeHxRVbJ2LOTGNkZRozNyQTArSwiSjyGHQ1g2Ikv6YydLLMBWWaDwtVo1+QxPFyQiJKDYUcDJrJJOa76JrHYnJxIfU3KDDtElFgMOxrA28/jq28Si1tYiTRJvjaC21hElFgMOxoghZ2jbV4EQjx+/3w5XZHmZE5iJZa0jXXA6UY4LCpcDRFpGcOOBpTYM5Fp1CMYFtHY3q10OSmP92IlR3m+FSaDDt3+EI539ChdDhFpGMOOBuh0AiYWWQGwbyce5LBjY89OIhn0OlRFVyX3cyuLiBKIYUcjpJOUD7Nv57zxXqzkmcTDBYkoCRh2NILj5/HDe7GSZzKvjSCiJGDY0QiGnfgIh0W0uqOnJzPsJFzfRBZXdogocRh2NEIKO4dPeTjZch7avX4EwyIEAShkz07CVUcnso62edEbCClcDRFpFcOORpTlW2HQCej2h9Ac7TmhkZOakwuyzDDq+b9HohVmmZFvNSEsAl+2cFWSiBKDX801wqjXoSzfAoBbWedDnsTi6clJIQiCvJXFiSwiShSGHQ1h3875kyex2K+TNPK1Ec3s2yGixGDY0RCGnfPHSazkq5bGz1u4skNEicGwoyFykzLDzqi1dHESK9mkJmWu7BBRojDsaEhlYeSbBi8EHT1uYyVfVZENghCZhDsVHfsnIoonhh0Nka6MOO3147TXr3A1qYn3YiVfpkmPivzIf7s8XJCIEoFhR0MsJgPG5mQCYN/OaHEaSxm8NoKIEolhR2Mmskl51HoDIXR0BwBwGyvZpIms/ezbIaIEYNjRGOlCUIadkWuNNiebDTrYM40KV5NepCZlTmQRUSIw7GiMPH7OJuURa3H3jZ0LgqBwNelFGj8/2OJBMBRWuBoi0hqGHY3h+PnoOV2cxFJKaa4FFpMe/mAYR9u9SpdDRBqj6rBTX1+Piy++GDabDUVFRbj55ptx4MCBfq8RRRG1tbUoKSlBZmYmampqsG/fPoUqVp4Udk509sDrCypcTWrhJJZydDoBFxTzBnQiSgxVh53Nmzdj6dKl+Oijj7Bx40YEg0EsXLgQXm/fT34rVqzAypUrsWbNGuzYsQMOhwMLFiyA252eXzDzrCbkWU0AgCOn+BPySMhhh7edK2IyDxckogQxKF3A2fztb3/r9/sXX3wRRUVF2LVrF6666iqIoohVq1Zh+fLlWLRoEQBg3bp1KC4uxvr16/Htb39bibIVV1mYhe3e0zh0yo1p4+xKl5MynNEGZQdXdhQxiSs7RJQgql7ZOZPL5QIA5OXlAQAaGhrgdDqxcOFC+TVmsxnz5s3Dtm3bhnw/Pp8PXV1d/d60hOPno8N7sZRVPSZ6ISgPFiSiOEuZsCOKIh5++GFcccUVmDp1KgDA6XQCAIqLi/u9tri4WH5uMPX19bDb7fJbaWlp4gpXAC8EHZ3YaSxKPmki63hHD9y9AYWrISItSZmw88ADD+CTTz7Bf/zHfwx47swxYVEUzzo6/MQTT8DlcslvTU1Nca9XSQw7IyeKIqexFJZjMcmf+4Mt3MoiovhJibDz4IMP4q233sJ7772HcePGyY87HA4AGLCK09raOmC1J5bZbEZ2dna/Ny2Rwk5jezcCPLNkWFw9AfiCkc9VEa+KUIx8Azr7dogojlQddkRRxAMPPIDXX38d7777LioqKvo9X1FRAYfDgY0bN8qP+f1+bN68GXPnzk12uapRYs+AxaRHMCyikWeWDEtLtDk5x2JEhlGvcDXpS7ojixNZRBRPqg47S5cuxSuvvIL169fDZrPB6XTC6XSip6cHQGT7atmyZairq8Mbb7yBzz77DPfeey8sFgsWL16scPXKEQQBE3ltxIg4u7iFpQaTHWxSJqL4U/Xo+XPPPQcAqKmp6ff4iy++iHvvvRcA8Oijj6KnpwdLlixBR0cHLr30UmzYsAE2my3J1apLZVEWPj3hYtgZJk5iqYO8suN0n7P3johouFQddkRRPOdrBEFAbW0tamtrE19QCmGT8sjIBwqyX0dREwuzYNAJcPcGcdLVi7E5mUqXREQaoOptLBo9eRuLF4IOC7ex1MFk0Mn/7R7gVhYRxQnDjkb1XQjqRTh87hWydMd7sdRDmsjazyZlIooThh2NKsu3wKAT0BMI4aSrR+lyVE+axiq2MewoTerbOcDxcyKKE4YdjTLqdSgvsAJg385wyNtYXNlRHCeyiCjeGHY0rJLj58MSCIXR5omu7LBnR3HSNtbhU174giGFqyEiLWDY0TC5b4dNymfV5vFBFAGDTkC+1aR0OWnPkZ2B7AwDQmERh1t5KCYRnT+GHQ3j+PnwSHdiFdnM0Ol4rovSBEHgDehEFFcMOxrGsDM8nMRSn2o2KRNRHDHsaNiEwkiDckd3AO3RnhQaSFrZ4SSWelRHm5T3M+wQURww7GiYxWSQT6Dl6s7QWtyRIMhJLPXoGz/nNhYRnT+GHY2Tt7LYpDwk3oulPlLYaenyocPrV7gaIkp1DDsax76dc3PyXizVyTIbUJoXWZX8gltZRHSeGHY0jmHn3Fp4L5YqSX07m/a3cHWHiM6Lqm89p/PXd0cWw85Q5Ksi2LOjKlNKsrHx8xa8sLUBL2xtQFVRFuaU52FOWS4uLs9DaV4mBIFHBRDRuTHsaJx0ivJJVy+8viCsZv4rj+XxBeHxBQGwZ0dt7vpKGU65ffjoSDsOn/Liy1YPvmz14D+2HwMQORfp4vI8zCmPhJ9qhw0GPReriWggfufTuFyrCflWE9q9fhw+5cH0cTlKl6Qq0hZWltmALAZBVcnPMuOpf5wGAGj3+LCrsQM7Gzuw4+hpfHbChVa3D//9aTP++9NmAIDVpMdFZbmYU5aHi8tzMXN8Diwm/jslIoadtDCxKAvtDadxqJVh50x9k1hsTlaz/CwzFk5xYOEUBwCgxx/C3uOd2Hn0NHYc7cDuxg64fUF88GUbPviyDQCg1wmYWpKNOeWR8DO7LA+FNv57JkpHDDtpoLIoC9ujYYf665vE4hZWKsk06fGVCfn4yoR8AEAoLOJgi1sOPzuOnkazqxd7j7uw97gLL2xtAABUFFjlnp855bmoKLCy74coDTDspAHefj40qTmZk1ipTa8TMHlMNiaPycZdl5UDAE509kTDz2nsPNqBAy1uNLR50dDmxf/bdRwAkG81yT0/c8rzMKUkG0b2/RBpDsNOGuDBgkPjvVjaNTYnE2NnjsXXZo4FALi6A9h9rEMOP3uOd6Ld68fb+1rw9r4WAECGUYdZpbm4uDwXc8rzMGt8DmwZRiX/GkQUBww7aUAKO43t3fh/O5tUsWxfkpOBaWPtin8j6bsXi70cWme3GHF1dRGuri4CAPiCIXx2okve+trZeBqd3QH8/Ug7/n6kHQCgE4DJY7IxpywXs8vzMHNcDkfeiVIQw04aGGPPgM1sgNsXxPf/8xOly5EJAjCxMAvTx9kxY1wOZpTmYPIYG8wGfdJqaHFHDxTkyk7aMRv0mF2Wi9llufj2PCAcFnGkzSP3/Ow82oFjp7ux72QX9p3swrq/NwIAci1GTB+Xgxnj7Jg+LgfTS+0o4iWyRKomiKIoKl2E0rq6umC32+FyuZCdna10OQnxn7uO47/2nlS6DABAWBRx5JQXJzp7Bjxn1AuodmRHAlBpDmaMy0FlURb0usT8JD23/h2cdPXijSVzMWt8bkI+BqWulq5e7Iyu+uxu7MD+Zjf8ofCA15XYM+TgM3NcDqaOsyOb21+UgkJhEb5gCL5AGL3Rf/qCYfQGQvAFw+d8rjcQfU3s49HHvnlFBWomFcW13uF+/2bYQXqEHTU65fbhk+Od2HvchU+Od+KT4y6cHuRaAItJj6lj7fJP0jPitJUQDou44Id/RTAsYtvj/wcl0RviiYbiC4ZwwOmOTHk1deKT4534stWDwb6KTii0RlYsx9kxvTQHF47JRoYxeauWpH3hsAh3bxAd3X50dPvR2R2I/jqAzujvewJnBo8QeqP/9AcHhpVAKHGR4Gc3T8WdXymL6/tk2BkBhh11EEURxzt6sDcafPY2deLTEy50+0MDXhu7lTCjNAfTx+WM+AyVU24fLn5qEwQBOPizazmFQ6Pi8QXx2QlXv+DedHrgqqVBJ2CSwxZdsYwE96qiLJ76TACA3kCoX2Dp909vX4CJfc7VE0A4gd/BjXoBZoMeZoMOZoMOGUY9TAYdzEY9MqL/lB6PfU3k13pkGCOPmY2RX88Yl4MJ0engeGHYGQGGHfUKhUUcPuWJ/hTtwt7jndjf3DXoTx/SVoL0zeRcWwmfnXDhhtVbUWgzY8fy+Yn8a1Caaff48MkJFz5pckXDeyfaPANXLTONekwdmx3ZAhtnx8zSHIzPs6RMA3QwFGZYO0M4LMLVM3CFZbAgExtgegMDt0eHy2LSI9diQo7FOOCfFpMhGjh0yDDoYTbGBpEzwkrMa0x6XUr8u2XYGQGGndTiC4bwRbMbnxzvxJ6myE/Sh04NvpUwMbqVIPUATY7ZStj0eQv+v9/vxNSx2fjLg1cm+W9B6UQURZx09WJvU2ck/DS58OkJl3wvW6wcixHTxvY17c8YZ0dRHM+BCodFdAdC8PQG4fEF4O6N3A/nkf4Z82v3EI9Lv/aHwsizmlCWb0FFvhVl+VaUF1hQlm9FRb4Vdos2+5ZCYREnO3vQ2N6No+1eNLZ70dDWjcZ2LxpPd8MfHF1w0esE5GQaY8KKCbkWI3KtfQEm12KMPh55LMdiTOpQh9ow7IwAw07q8/iC+DSm92fv8U4c7xh8K6F6jA3Tx+XA6wviz3tOYv7kIvz7PRcrUDWlM2n6a29T3xbY5ye7Bm2AdmRn9Gvaz7OaoqHj7GHF4wv2e97rC8LjDw76g0Ei5FiMkQCUb0F5TBAqz7ci12JU9QpWMBTGyc7eAWHmaLsXTad7Bv33FCvLbJDDSGxwkQOMJTbAmJBjNcJmNqj6c6JGDDsjwLCjTe0enxx8pB6g9kEaoO+4dLx84SSRkvzBcLQBulPeuv2y1Z2Qvgy9ToAtwyBfgptlNiAr+vu+x43Rx/Qxv+573mzQwdnVi6NtfSscR9u7cbTNi1a376wfPzvDgPIC66BhKN9qSso3/UAojBMdPWho96KxLVK79HdoOt2N4Fk+8Sa9DqV5mSiPWdEqjwY5hz0DJoP6t4C0gGFnBBh20oMoijjR2SMHoL1NnWjt8qF+0TRcGr1jiUhtvL4g9p3skrfAPj3hQo8/1C+cSL+2mQ2wxvw68rxRDihWc19YMRt0CQ0U3f4gGmPCw9E2bzQQdaM5epjnUGxmA8rkVaDotliBFWX5FhRmmUdUtz8YRlNHtI7o6kxDtK7jHT0InS3QGHQoy5M+ft+qVFm+BSU5mQk7EoOGj2FnBBh2iIiSpzcQ6tfvIoWhxvZunHT1nHWbzWLSnxE+Iv/MtZjQdDryPqVQdbTdixMdPWddGcsw6uQAI6/S5FtQXmCFIzsDOgYaVRvu92+eoExEREmVYdRjksOGSQ7bgOd6AyEc7+iWt8bODC7d/hD2N3dhf3PXsD+eFJCkECMFpPJ8K4psZgaaNMCwQ0REqpFh1KOyyIbKooFByBcM4XhHz6BbUh1eP0rzBoaZ8oKRb32R9jDsEBFRSjAb9JhYmIWJcT6YjrSP7eJERESkaQw7REREpGkMO0RERKRpDDtERESkaQw7REREpGkMO0RERKRpDDtERESkaQw7REREpGkMO0RERKRpDDtERESkaQw7REREpGkMO0RERKRpDDtERESkaQw7REREpGkGpQtQA1EUAQBdXV0KV0JERETDJX3flr6PD4VhB4Db7QYAlJaWKlwJERERjZTb7Ybdbh/yeUE8VxxKA+FwGCdPnoTNZoMgCHF7v11dXSgtLUVTUxOys7Pj9n5THT8vA/FzMjh+Xgbi52Qgfk4Glw6fF1EU4Xa7UVJSAp1u6M4cruwA0Ol0GDduXMLef3Z2tmb/Qzsf/LwMxM/J4Ph5GYifk4H4ORmc1j8vZ1vRkbBBmYiIiDSNYYeIiIg0jWEngcxmM37yk5/AbDYrXYqq8PMyED8ng+PnZSB+Tgbi52Rw/Lz0YYMyERERaRpXdoiIiEjTGHaIiIhI0xh2iIiISNMYdoiIiEjTGHYS6Nlnn0VFRQUyMjIwe/ZsfPDBB0qXpJj6+npcfPHFsNlsKCoqws0334wDBw4oXZaq1NfXQxAELFu2TOlSFHfixAnceeedyM/Ph8ViwcyZM7Fr1y6ly1JMMBjED3/4Q1RUVCAzMxMTJkzAk08+iXA4rHRpSbVlyxbceOONKCkpgSAIePPNN/s9L4oiamtrUVJSgszMTNTU1GDfvn3KFJskZ/ucBAIBPPbYY5g2bRqsVitKSkpw99134+TJk8oVrBCGnQR57bXXsGzZMixfvhwff/wxrrzySlx77bU4duyY0qUpYvPmzVi6dCk++ugjbNy4EcFgEAsXLoTX61W6NFXYsWMH1q5di+nTpytdiuI6Ojpw+eWXw2g04q9//Ss+//xzPPPMM8jJyVG6NMU8/fTTeP7557FmzRrs378fK1aswC9+8QusXr1a6dKSyuv1YsaMGVizZs2gz69YsQIrV67EmjVrsGPHDjgcDixYsEC+/1CLzvY56e7uxu7du/GjH/0Iu3fvxuuvv46DBw/ipptuUqBShYmUEJdccol4//3393usurpafPzxxxWqSF1aW1tFAOLmzZuVLkVxbrdbrKqqEjdu3CjOmzdPfOihh5QuSVGPPfaYeMUVVyhdhqpcf/314n333dfvsUWLFol33nmnQhUpD4D4xhtvyL8Ph8Oiw+EQf/7zn8uP9fb2ina7XXz++ecVqDD5zvycDGb79u0iALGxsTE5RakEV3YSwO/3Y9euXVi4cGG/xxcuXIht27YpVJW6uFwuAEBeXp7ClShv6dKluP766zF//nylS1GFt956C3PmzMHXv/51FBUVYdasWfjtb3+rdFmKuuKKK/DOO+/g4MGDAIC9e/di69atuO666xSuTD0aGhrgdDr7fd01m82YN28ev+7GcLlcEAQh7VZKeRFoArS1tSEUCqG4uLjf48XFxXA6nQpVpR6iKOLhhx/GFVdcgalTpypdjqJeffVV7N69Gzt27FC6FNU4cuQInnvuOTz88MP4wQ9+gO3bt+M73/kOzGYz7r77bqXLU8Rjjz0Gl8uF6upq6PV6hEIhPPXUU7j99tuVLk01pK+tg33dbWxsVKIk1ent7cXjjz+OxYsXa/pi0MEw7CSQIAj9fi+K4oDH0tEDDzyATz75BFu3blW6FEU1NTXhoYcewoYNG5CRkaF0OaoRDocxZ84c1NXVAQBmzZqFffv24bnnnkvbsPPaa6/hlVdewfr16zFlyhTs2bMHy5YtQ0lJCe655x6ly1MVft0dXCAQwG233YZwOIxnn31W6XKSjmEnAQoKCqDX6wes4rS2tg74qSPdPPjgg3jrrbewZcsWjBs3TulyFLVr1y60trZi9uzZ8mOhUAhbtmzBmjVr4PP5oNfrFaxQGWPGjMGFF17Y77HJkyfjT3/6k0IVKe/73/8+Hn/8cdx2220AgGnTpqGxsRH19fUMO1EOhwNAZIVnzJgx8uP8uhsJOrfeeisaGhrw7rvvpt2qDsBprIQwmUyYPXs2Nm7c2O/xjRs3Yu7cuQpVpSxRFPHAAw/g9ddfx7vvvouKigqlS1LcNddcg08//RR79uyR3+bMmYM77rgDe/bsScugAwCXX375gGMJDh48iLKyMoUqUl53dzd0uv5frvV6fdqNnp9NRUUFHA5Hv6+7fr8fmzdvTtuvu0Bf0Pnyyy+xadMm5OfnK12SIriykyAPP/ww7rrrLsyZMweXXXYZ1q5di2PHjuH+++9XujRFLF26FOvXr8ef//xn2Gw2edXLbrcjMzNT4eqUYbPZBvQsWa1W5Ofnp3Uv03e/+13MnTsXdXV1uPXWW7F9+3asXbsWa9euVbo0xdx444146qmnMH78eEyZMgUff/wxVq5cifvuu0/p0pLK4/Hg0KFD8u8bGhqwZ88e5OXlYfz48Vi2bBnq6upQVVWFqqoq1NXVwWKxYPHixQpWnVhn+5yUlJTglltuwe7du/GXv/wFoVBI/tqbl5cHk8mkVNnJp+wwmLb927/9m1hWViaaTCbxoosuSusxawCDvr344otKl6YqHD2P+K//+i9x6tSpotlsFqurq8W1a9cqXZKiurq6xIceekgcP368mJGRIU6YMEFcvny56PP5lC4tqd57771Bv47cc889oihGxs9/8pOfiA6HQzSbzeJVV10lfvrpp8oWnWBn+5w0NDQM+bX3vffeU7r0pBJEURSTGa6IiIiIkok9O0RERKRpDDtERESkaQw7REREpGkMO0RERKRpDDtERESkaQw7REREpGkMO0RERKRpDDtERESkaQw7REQAysvLsWrVKqXLIKIEYNghoqS79957cfPNNwMAampqsGzZsqR97Jdeegk5OTkDHt+xYwe+9a1vJa0OIkoeXgRKRJrg9/vP62LDwsLCOFZDRGrClR0iUsy9996LzZs341e/+hUEQYAgCDh69CgA4PPPP8d1112HrKwsFBcX46677kJbW5v8Z2tqavDAAw/g4YcfRkFBARYsWAAAWLlyJaZNmwar1YrS0lIsWbIEHo8HAPD+++/jn/7pn+ByueSPV1tbC2DgNtaxY8fwta99DVlZWcjOzsatt96KlpYW+fna2lrMnDkTL7/8MsrLy2G323HbbbfB7XYn9pNGRCPGsENEivnVr36Fyy67DP/8z/+M5uZmNDc3o7S0FM3NzZg3bx5mzpyJnTt34m9/+xtaWlpw66239vvz69atg8FgwIcffojf/OY3AACdTodf//rX+Oyzz7Bu3Tq8++67ePTRRwEAc+fOxapVq5CdnS1/vEceeWRAXaIo4uabb8bp06exefNmbNy4EYcPH8Y3vvGNfq87fPgw3nzzTfzlL3/BX/7yF2zevBk///nPE/TZIqLR4jYWESnGbrfDZDLBYrHA4XDIjz/33HO46KKLUFdXJz/2u9/9DqWlpTh48CAuuOACAEBlZSVWrFjR733G9v9UVFTgpz/9Kf7v//2/ePbZZ2EymWC32yEIQr+Pd6ZNmzbhk08+QUNDA0pLSwEAL7/8MqZMmYIdO3bg4osvBgCEw2G89NJLsNlsAIC77roL77zzDp566qnz+8QQUVxxZYeIVGfXrl147733kJWVJb9VV1cDiKymSObMmTPgz7733ntYsGABxo4dC5vNhrvvvhvt7e3wer3D/vj79+9HaWmpHHQA4MILL0ROTg72798vP1ZeXi4HHQAYM2YMWltbR/R3JaLE48oOEalOOBzGjTfeiKeffnrAc2PGjJF/bbVa+z3X2NiI6667Dvfffz9++tOfIi8vD1u3bsU3v/lNBAKBYX98URQhCMI5Hzcajf2eFwQB4XB42B+HiJKDYYeIFGUymRAKhfo9dtFFF+FPf/oTysvLYTAM/8vUzp07EQwG8cwzz0Cniyxc//GPfzznxzvThRdeiGPHjqGpqUle3fn888/hcrkwefLkYddDROrAbSwiUlR5eTn+93//F0ePHkVbWxvC4TCWLl2K06dP4/bbb8f27dtx5MgRbNiwAffdd99Zg8rEiRMRDAaxevVqHDlyBC+//DKef/75AR/P4/HgnXfeQVtbG7q7uwe8n/nz52P69Om44447sHv3bmzfvh1333035s2bN+jWGRGpG8MOESnqkUcegV6vx4UXXojCwkIcO3YMJSUl+PDDDxEKhfDVr34VU6dOxUMPPQS73S6v2Axm5syZWLlyJZ5++mlMnToVf/jDH1BfX9/vNXPnzsX999+Pb3zjGygsLBzQ4AxEtqPefPNN5Obm4qqrrsL8+fMxYcIEvPbaa3H/+xNR4gmiKIpKF0FERESUKFzZISIiIk1j2CEiIiJNY9ghIiIiTWPYISIiIk1j2CEiIiJNY9ghIiIiTWPYISIiIk1j2CEiIiJNY9ghIiIiTWPYISIiIk1j2CEiIiJN+/8Bb4UU3ujdoe4AAAAASUVORK5CYII=", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjIAAAGwCAYAAACzXI8XAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8ekN5oAAAACXBIWXMAAA9hAAAPYQGoP6dpAAA9PUlEQVR4nO3deXyU5b338e9MJpnsCQTIJBAICMoWEEWQxeXRVOpjLVZbl1K3+tS24lGk1cqxuCNCKyKKUDzW7WirPadata0Wo0VQNkGRRQFli0ASAyaThWwz9/NHuAciAZKQmXvuez7v1yuvlkky80uA8PW6ftf1cxmGYQgAAMCG3FYXAAAA0FEEGQAAYFsEGQAAYFsEGQAAYFsEGQAAYFsEGQAAYFsEGQAAYFseqwsIt2AwqD179igtLU0ul8vqcgAAQBsYhqGqqirl5ubK7T76uovjg8yePXuUl5dndRkAAKADiouL1atXr6O+3/FBJi0tTVLzNyI9Pd3iagAAQFv4/X7l5eWF/h0/GscHGXM7KT09nSADAIDNHK8thGZfAABgWwQZAABgWwQZAABgWwQZAABgWwQZAABgWwQZAABgWwQZAABgWwQZAABgWwQZAABgWwQZAABgWwQZAABgWwQZAABgW44fGhku39Q0qKahydIa4uPcyk5PtLQGAACsRJDpoN/9a7NeWrnL6jJ0y/kDNPU7J1tdBgAAliDIdFC82yWvx7qduaBhqDFg6MMvygkyAICYRZDpoPsmDtV9E4da9vprdn6jyxZ8qL2VdZbVAACA1Wj2tamcjObemLKqOgWDhsXVAABgDYKMTXVP88rtkhoDhvbVNFhdDgAAliDI2FR8nFvd07ySpBK2lwAAMYogY2O+jCRJ0t7KAxZXAgCANQgyNuZLP7gi42dFBgAQmwgyNpYTWpEhyAAAYhNBxsZ8B08ulRJkAAAxiiBjY+YRbFZkAACxiiBjY76Dc5bokQEAxCqCjI35QisyB2QYXIoHAIg9BBkbMydf1zUGVXmg0eJqAACIPIKMjSXGx6lrSoIktpcAALGJIGNzZp8MDb8AgFhEkLE58+QSYwoAALHI0iATCAQ0ffp09e3bV0lJSTrppJP0wAMPtGhcNQxDd999t3JycpSUlKTCwkJt3brVwqqjSzZHsAEAMczSIDNr1iwtWLBATzzxhD777DPNmjVLs2fP1uOPPx76mNmzZ2vevHlauHChVq5cqZSUFE2YMEF1dfzDLUk55hFs5i0BAGKQx8oX//DDDzVx4kRddNFFkqT8/Hz96U9/0qpVqyQ1r8bMnTtXv/3tbzVx4kRJ0vPPP6/s7Gy99tpruvLKKy2rPVqYR7BL/PUWVwIAQORZuiIzduxYFRUVacuWLZKkdevWadmyZbrwwgslSdu3b1dJSYkKCwtDn5ORkaHRo0dr+fLlrT5nfX29/H5/izcnM+ctsSIDAIhFlq7I3HnnnfL7/Ro4cKDi4uIUCAQ0Y8YMTZo0SZJUUlIiScrOzm7xednZ2aH3fdvMmTN13333hbfwKOKjRwYAEMMsXZF55ZVX9OKLL+qll17S2rVr9dxzz+n3v/+9nnvuuQ4/57Rp01RZWRl6Ky4u7sSKo48ZZKrqmlRd32RxNQAARJalKzK333677rzzzlCvS0FBgXbu3KmZM2fq2muvlc/nkySVlpYqJycn9HmlpaU69dRTW31Or9crr9cb9tqjRarXozSvR1X1TSqprFP/HqlWlwQAQMRYuiJTW1srt7tlCXFxcQoGg5Kkvn37yufzqaioKPR+v9+vlStXasyYMRGtNZr5uEsGABCjLF2RufjiizVjxgz17t1bQ4YM0ccff6w5c+bopz/9qSTJ5XJpypQpevDBBzVgwAD17dtX06dPV25uri655BIrS48qvoxEbS2rZkwBACDmWBpkHn/8cU2fPl033XSTysrKlJubq5///Oe6++67Qx9zxx13qKamRjfeeKMqKio0fvx4vfXWW0pMTLSw8uhy6HZfTi4BAGKLyzj8Gl0H8vv9ysjIUGVlpdLT060uJyzmLN6ieUVbNWl0b834QYHV5QAAcMLa+u83s5YcwJdOjwwAIDYRZBwgh7tkAAAxiiDjAOappVKafQEAMYYg4wDmisy+mgbVNQYsrgYAgMghyDhARlK8EuObfyvLGB4JAIghBBkHcLlcoYbfvRzBBgDEEIKMQ4Ru96VPBgAQQwgyDpGTkSSJI9gAgNhCkHEIH0ewAQAxiCDjEDkMjgQAxCCCjENkm82+9MgAAGIIQcYhGBwJAIhFBBmHMHtkvq6qV1MgaHE1AABEBkHGIbqleOVxuxQ0pK+ruRQPABAbCDIO4Xa7DvXJ0PALAIgRBBkH4eQSACDWEGQcJJu7ZAAAMYYg4yA56ZxcAgDEFoKMgxyat0SzLwAgNhBkHOTQvCVWZAAAsYEg4yDMWwIAxBqCjIOYQabUX6dg0LC4GgAAwo8g4yA90rxyuaTGgKF9NQ1WlwMAQNgRZBwkPs6t7qleSc2rMgAAOB1BxmFy6JMBAMQQgozD+JiCDQCIIQQZh/ExbwkAEEMIMg7jC90lQ5ABADgfQcZhQoMjafYFAMQAgozD+JiADQCIIQQZhzn81JJhcCkeAMDZCDIOk32w2fdAY0D+A00WVwMAQHgRZBwmMT5OXZLjJUl7/RzBBgA4G0HGgcyTSxzBBgA4HUHGgcw+mVKCDADA4QgyDuRjTAEAIEYQZBwoJ50j2ACA2ECQcaBsc0WGS/EAAA5HkHGgHAZHAgBiBEHGgXK43RcAECMIMg5kHr/21zWppp5L8QAAzkWQcaBUr0dpXo8khkcCAJyNIONQ2WwvAQBiAEHGoXK4SwYAEAMIMg7lO3iXTClbSwAAByPIONShFRmOYAMAnIsg41DmySV6ZAAATkaQcShfhlcSPTIAAGcjyDiUL50VGQCA8xFkHMrskdlX06D6poDF1QAAEB4EGYfKTI6X19P821vmr7e4GgAAwoMg41Aul4u7ZAAAjkeQcbDsdI5gAwCcjSDjYEzBBgA4HUHGwcy7ZNhaAgA4FUHGwcwVGcYUAACciiDjYD6afQEADkeQcTB6ZAAATkeQcTBzAnZZVZ2aAkGLqwEAoPMRZBwsK9Urj9uloCF9Xc2leAAA5yHIOFic2xW6S4btJQCAExFkHM5HnwwAwMEIMg7HySUAgJMRZBzObPgt4S4ZAIADEWQcjsGRAAAnI8g43KEeGQZHAgCchyDjcKFL8dhaAgA4EEHG4czBkaWV9QoGDYurAQCgc1keZHbv3q2f/OQnysrKUlJSkgoKCvTRRx+F3m8Yhu6++27l5OQoKSlJhYWF2rp1q4UV20uPNK9cLqkhENT+2garywEAoFNZGmS++eYbjRs3TvHx8frnP/+pTZs26ZFHHlGXLl1CHzN79mzNmzdPCxcu1MqVK5WSkqIJEyaoro6tkraIj3OrW6pXEnfJAACcx2Pli8+aNUt5eXl65plnQo/17ds39P8Nw9DcuXP129/+VhMnTpQkPf/888rOztZrr72mK6+8MuI121FORqK+rqrX3so6De2ZYXU5AAB0GktXZF5//XWNHDlSP/rRj9SjRw+NGDFCTz31VOj927dvV0lJiQoLC0OPZWRkaPTo0Vq+fHmrz1lfXy+/39/iLdZxlwwAwKksDTLbtm3TggULNGDAAL399tv65S9/qVtuuUXPPfecJKmkpESSlJ2d3eLzsrOzQ+/7tpkzZyojIyP0lpeXF94vwgZyOIINAHAoS4NMMBjUaaedpoceekgjRozQjTfeqJ/97GdauHBhh59z2rRpqqysDL0VFxd3YsX2lM2leAAAh7I0yOTk5Gjw4MEtHhs0aJB27dolSfL5fJKk0tLSFh9TWloaet+3eb1epaent3iLdTkMjgQAOJSlQWbcuHHavHlzi8e2bNmiPn36SGpu/PX5fCoqKgq93+/3a+XKlRozZkxEa7UzX3rzXTIEGQCA01h6aum2227T2LFj9dBDD+nyyy/XqlWrtGjRIi1atEiS5HK5NGXKFD344IMaMGCA+vbtq+nTpys3N1eXXHKJlaXbyuG3+xqGIZfLZXFFAAB0DkuDzBlnnKFXX31V06ZN0/3336++fftq7ty5mjRpUuhj7rjjDtXU1OjGG29URUWFxo8fr7feekuJiYkWVm4v5ryl2oaA/HVNykiKt7giAAA6h8swDEffW+/3+5WRkaHKysqY7pcZcf+/9E1to96ecrZO8aVZXQ4AAMfU1n+/LR9RgMjITjdPLnEEGwDgHASZGMHJJQCAExFkYoQ5BZu7ZAAATkKQiRHmikwpYwoAAA5CkIkRPm73BQA4EEEmRoQGRxJkAAAOQpCJETkZnFoCADgPQSZGmFtL/rom1TY0WVwNAACdgyATI9IS45Xqbb7Ime0lAIBTEGRiiI+7ZAAADkOQiSG+dE4uAQCchSATQ3yHTcEGAMAJCDIxhJNLAACnIcjEkEM9MvUWVwIAQOcgyMSQ0OBIPysyAABnIMjEEF968+BITi0BAJyCIBNDzK2l8uoG1TcFLK4GAIATR5CJIV2S45Xgaf4tL/PTJwMAsD+CTAxxuVyHnVxiewkAYH8EmRgTmoLNXTIAAAcgyMSY0Mkl7pIBADgAQSbGZLO1BABwEIJMjMlJZ3AkAMA5CDIxxpfRfJcMKzIAACcgyMQYs0emlGZfAIADEGRijBlkyqrq1RQIWlwNAAAnhiATY7JSvYpzuxQIGiqvbrC6HAAATghBJsbEuV3KTvNKkvZyBBsAYHMEmRjky+DkEgDAGQgyMSjn4MklbvcFANgdQSYGsSIDAHAKgkwMMuctcZcMAMDuCDIxiBUZAIBTEGRikHmXzF4/p5YAAPZGkIlB5opMaWW9DMOwuBoAADqOIBODeqQlyuWSGgJB7a/hUjwAgH0RZGJQgsetbqnmpXj0yQAA7IsgE6PMk0s0/AIA7IwgE6N8oYZfggwAwL4IMjEqJ3QEm5NLAAD7IsjEqEN3ydRbXAkAAB1HkIlRoRUZ7pIBANgYQSZGZTOmAADgAASZGBWagF1Zx6V4AADbIsjEKPP4dW1DQP66JourAQCgYwgyMSopIU6ZyfGSpFKOYAMAbIogE8N89MkAAGyOIBPDfNwlAwCwOYJMDDOPYLMiAwCwK4JMDPOlHzq5BACAHRFkYhgrMgAAu+tQkCkuLtZXX30V+vWqVas0ZcoULVq0qNMKQ/iZPTKcWgIA2FWHgsyPf/xjvffee5KkkpISfec739GqVat011136f777+/UAhE+PlZkAAA216Egs2HDBo0aNUqS9Morr2jo0KH68MMP9eKLL+rZZ5/tzPoQRmaQqTzQqNoGLsUDANhPh4JMY2OjvF6vJOmdd97R97//fUnSwIEDtXfv3s6rDmGV5vUoJSFOEg2/AAB76lCQGTJkiBYuXKilS5dq8eLF+u53vytJ2rNnj7Kysjq1QISPy+U67C4ZggwAwH46FGRmzZqlP/zhDzr33HN11VVXafjw4ZKk119/PbTlBHsIDY+k4RcAYEOejnzSueeeq/Lycvn9fnXp0iX0+I033qjk5OROKw7hR8MvAMDOOrQic+DAAdXX14dCzM6dOzV37lxt3rxZPXr06NQCEV7mvCW2lgAAdtShIDNx4kQ9//zzkqSKigqNHj1ajzzyiC655BItWLCgUwtEeLEiAwCwsw4FmbVr1+qss86SJP3P//yPsrOztXPnTj3//POaN29epxaI8DJv9y3xMzgSAGA/HQoytbW1SktLkyT961//0qWXXiq3260zzzxTO3fu7NQCEV6HTi3VW1wJAADt16Eg079/f7322msqLi7W22+/rQsuuECSVFZWpvT09E4tEOFlnloqr65XQ1PQ4moAAGifDgWZu+++W7/+9a+Vn5+vUaNGacyYMZKaV2dGjBjRqQUivLokxyvB0/zHgJlLAAC76dDx6x/+8IcaP3689u7dG7pDRpLOP/98/eAHP+i04hB+LpdLvvRE7dpfqxJ/nfK6cnweAGAfHQoykuTz+eTz+UJTsHv16sVleDbly2gOMpxcAgDYTYe2loLBoO6//35lZGSoT58+6tOnjzIzM/XAAw8oGKTPwm5CJ5cqObkEALCXDq3I3HXXXXr66af18MMPa9y4cZKkZcuW6d5771VdXZ1mzJjRqUUivDi5BACwqw6tyDz33HP6r//6L/3yl7/UsGHDNGzYMN1000166qmn9Oyzz3aokIcfflgul0tTpkwJPVZXV6fJkycrKytLqampuuyyy1RaWtqh58fRhW735S4ZAIDNdCjI7N+/XwMHDjzi8YEDB2r//v3tfr7Vq1frD3/4g4YNG9bi8dtuu01vvPGG/vKXv2jJkiXas2ePLr300o6UjGPI4XZfAIBNdSjIDB8+XE888cQRjz/xxBNHhJHjqa6u1qRJk/TUU0+1GEBZWVmpp59+WnPmzNF5552n008/Xc8884w+/PBDrVixoiNl4yh85gRsgoytGIahXftqZRiG1aUAgGU61CMze/ZsXXTRRXrnnXdCd8gsX75cxcXF+sc//tGu55o8ebIuuugiFRYW6sEHHww9vmbNGjU2NqqwsDD02MCBA9W7d28tX75cZ555ZqvPV19fr/r6Q70efr+/XfXEInNFpqyqXoGgoTi3y+KK0BbPfrhD972xSQ9fWqArR/W2uhwAsESHVmTOOeccbdmyRT/4wQ9UUVGhiooKXXrppdq4caNeeOGFNj/Pn//8Z61du1YzZ8484n0lJSVKSEhQZmZmi8ezs7NVUlJy1OecOXOmMjIyQm95eXltridWdUv1Ks7tUiBoqLyahl+7eO2TPZKkdz4rs7gSALBOh++Ryc3NPeJ00rp16/T0009r0aJFx/384uJi3XrrrVq8eLESExM7WsYRpk2bpqlTp4Z+7ff7CTPHEed2KTvNqz2VddpbWafs9M77/UB4VNQ26NOvKiRJ63dXWFoLAFipQysynWHNmjUqKyvTaaedJo/HI4/HoyVLlmjevHnyeDzKzs5WQ0ODKioqWnxeaWmpfD7fUZ/X6/UqPT29xRuOL5u7ZGzlwy/3yWyNKfXXM14CQMyyLMicf/75Wr9+vT755JPQ28iRIzVp0qTQ/4+Pj1dRUVHoczZv3qxdu3aF+nLQeTi5ZC9Lt37d4tfrv6q0qBIAsFaHt5ZOVFpamoYOHdrisZSUFGVlZYUev+GGGzR16lR17dpV6enp+o//+A+NGTPmqI2+6DhfOieX7MIwDL2/pVxS8x1AJf46fbq7UoWDsy2uDAAir11B5nh3uHx7G+hEPfroo3K73brssstUX1+vCRMm6Mknn+zU10Cz0JgCtiii3o59tdpdcUDxcS5dOzZfs976XOsP9ssAQKxpV5DJyMg47vuvueaaDhfz73//u8WvExMTNX/+fM2fP7/Dz4m28bG1ZBvLDm4rnda7i87s11WS9OlXlTIMQy4XR+cBxJZ2BZlnnnkmXHXAYofmLRFkot37W5u3lc4+ubsG5aTL43ZpX02D9lTWqWdmksXVAUBkWdbsi+gSmrdUWcdNsVGsMRDUii/3SZLG9++mxPg4neJLkyS2lwDEJIIMJCl0d0xDIKj9NQ0WV4OjWVdcoar6JmUmx2toz+at3mG9mv/3U04uAYhBBBlIkhI8bnVL9Uqi4TeaLT24rTTupG6hURIFPTMlSet3E2QAxB6CDEJy6JOJeub9MWcN6BZ67PAVGbYFAcQaggxCzO0lTi5Fp8oDjVp3cPto/GFB5uTsNCXEuVV5oFHF+7mZGUBsIcgghBWZ6Lb8y30KBA3165aiXl2SQ48neNwalNs8imMdDb8AYgxBBiHcJRPdln1x5LaSadjBxl/6ZADEGoIMQg7d7sv2RDQyG33HD+h+xPsKQn0yFZEsCQAsR5BBCJfiRa9d+2q1c1+tPG5X6Dbfw5kNvxt2+xUM0vALIHYQZBDiO6zZl9Mv0WXpwW2lEb0zlZYYf8T7+3dPVWK8W9X1Tdq+rybS5QGAZQgyCDFXZGobAqqqb7K4Ghxu2cFtpbNa2VaSJE+cW0NyD/bJcDEegBhCkEFIcoJHGUnN/7XP9lL0CAQNffCF2R9zZKOvqaAnN/wCiD0EGbSQw8mlqPPpVxXy1zUpPdETOp3UmuF5NPwCiD0EGbRgbi+VEmSihnlaaexJ3eSJO/pfWXNUwcY9fjUFgpEoDQAsR5BBC6zIRJ/QWIKTj76tJEn9uqUoJSFOBxoD+vJrGn4BxAaCDFowxxRwl0x0qKpr1Me7KiRJZ/VvvdHX5Ha7QhOx2V4CECsIMmiBFZnosmLbfjUFDfXJSlbvrOTjfrx5nww3/AKIFQQZtODLSJLEqaVosezgttL4/sfeVjIV9MqUxMklALGDIIMWDo0pIMhEg6XHuT/m28xTTZv2+tVIwy+AGECQQQvmqaWK2kYdaAhYXE1s++qbWm0rr1Gc26UxJ2W16XP6ZCUrPdGjhqagNpdUhblCALAeQQYtpHk9Sk6Ik8SqjNXM23yH98oIXVR4PC6XS8MObi/RJwMgFhBk0ILL5Qqtyuyt5OSSlZZ+0b5tJdOhSdgEGQDOR5DBEXKYgm25w8cSnHWMsQStMftk1u+u6OyyACDqEGRwBF9688kljmBbZ+OeSlXUNirN69HwvMx2fa65IrO5pEp1jfQ5AXA2ggyOYK7IlNIjYxnztNKZJ2Up/hhjCVrTMzNJXVMS1BgwaPgF4HgEGRwhm0vxLGeOJTi7ndtKUnOfUwE3/AKIEQQZHCEnnR4ZK9XUN2nNzm8kSePb2ehrGk7DL4AYQZDBEXysyFhq1fb9agwY6tUlSfltGEvQmgKOYAOIEQQZHMHskSmvrldDE7fDRtr75rTrAd3kcrk69BzmzKUtpVVcbAjA0QgyOELXlAQlHGwwLatiVSbSlrVzLEFrstMT1SPNq6AhbdrLqgwA5yLI4Agul0vZGV5J9MlE2t7KA9paVi2XSxrbxrEERzOMPhkAMYAgg1blcJeMJczVmGG9MpWZnHBCz1XQM1OStJ4gA8DBCDJolY/bfS0Rmnbdv/3Hrr/NXJFZxxFsAA5GkEGrcji5FHHBExhL0Brzht9t5TWqqms84ecDgGhEkEGrfNzuG3Gb9vq1r6ZByQlxGtG7ywk/X7dUr3pmJskwpI17/J1QIQBEH4IMWpXDBOyIW3ZwNWZMvywleDrnr6Z5wy99MgCciiCDVmVzu2/ELT3s/pjOYm4vfcrFeAAciiCDVuVkNJ9aKq2qVyBoWFyN8x1oCGj19hMbS9Aas+F3PQ2/AByKIINWdU/zKs7tUiBoqLy63upyHG/Vjv1qCASVm5Gok7qndNrzmltLO/bVqrKWhl8AzkOQQavi3C71SGu+FI+TS+G3dEvzttL4ExhL0JrM5AT1OTiviblLAJyIIIOj4i6ZyFn2xYmPJTgac1Xm090Vnf7cAGA1ggyOyhdq+OXkUjiV+ev0eUmVXC5pXCdchPdth/pkWJEB4DwEGRyVuSKzl7tkwspcjRmam6GuKSc2lqA15qgCZi4BcCKCDI4qh62liDDHEozvxGPXhxvaM12StLvigPbRuA3AYQgyOCpfBoMjw80wjEPzlcIUZNIS49Xv4EkoGn4BOA1BBkeVw5iCsPu8pErl1fVKio/T6X1OfCzB0QwzG37ZXgLgMAQZHJXZ7Lu3sk6GwaV44bDs4GrM6H5d5fXEhe11hvXKlESQAeA8BBkcVY/05ntkGpqC+obL1MLi/YNjCcaH4bTS4UInlziCDcBhCDI4Kq8nTt1Sm0/RMDyy89U1BrRq+35J0tknd/79MYcbnJsut0sq9dezVQjAUQgyOCYuxQufj3Z8o/qmoLLTvRrQIzWsr5Wc4NGAHmmSuE8GgLMQZHBMvvTmk0sl/Fd8p1v6hbmt1L1TxxIcDZOwATgRQQbH5Mto7pNhRabzLd3S3Oh79snh7Y8xMQkbgBMRZHBMOdwlExbl1fXatNcvKTxjCVpTcNgRbE6hAXAKggyO6dC8JYJMZ/rg4FiCwTnp6pbqjchrDspJl8ft0r6aBu3h9xOAQxBkcEzmpXicWupc4b7NtzWJ8XE6xWc2/FZE7HUBIJwIMjgmTi11vuaxBM2NvmcNCO+x628z+2S4GA+AUxBkcExmkKlpCKiqjkvxOsMXZdUq9dfL63FrZH74xhK0xpyEzcwlAE5BkMExJSd4lJ7okcSqTGd5/+C20qi+XZUYH76xBK05fEWGhl8ATkCQwXFxcqlzLQttK0WuP8Z0cnaaEuLcqjzQqOL99D0BsD+CDI6LPpnOU98U0IptzWMJIt0fI0kJHrcG5TQ3/K6j4ReAAxBkcFyHTi4RZE7U2p0VOtAYULdUrwYePEEUaeYkbPpkADgBQQbHFVqRYUzBCVt62LZSJMYStCY0qoAVGQAOQJDBcR26FI+eihNl3h8zPkK3+bbGbPjdsNuvYJCGXwD2RpDBcfnYWuoU+2satGFP83aOFY2+pv7dU5UY71Z1fZO276uxrA4A6AwEGRyXeWqJraUT88EX5TIM6ZTsNPU4uMplBU+cW0NyzQGS9MkAsDeCDI7LXJGpqG1UXWPA4mrsa5kFYwmO5vABkgBgZwQZHFd6okfJCc0Xt3EEu2MOH0swPgqCzDAafgE4hKVBZubMmTrjjDOUlpamHj166JJLLtHmzZtbfExdXZ0mT56srKwspaam6rLLLlNpaalFFccml8sVavilT6ZjtpXXaE9lnRLi3BrdN8vqckJHsDfu8aspELS2GAA4AZYGmSVLlmjy5MlasWKFFi9erMbGRl1wwQWqqTnUgHjbbbfpjTfe0F/+8hctWbJEe/bs0aWXXmph1bHp0BFsTi51xNItzasxZ/TtoqSEyI4laE2/bilKSYjTgcaAvvyahl8A9uWx8sXfeuutFr9+9tln1aNHD61Zs0Znn322Kisr9fTTT+ull17SeeedJ0l65plnNGjQIK1YsUJnnnmmFWXHJE4unZhlX5jHriN/m29r3G6XhvbM0Mrt+/XpVxU6xaLL+QDgREVVj0xlZXPjYdeuXSVJa9asUWNjowoLC0MfM3DgQPXu3VvLly9v9Tnq6+vl9/tbvOHE5TCmoMMaA0Et/3KfpOho9DWZfTLc8AvAzqImyASDQU2ZMkXjxo3T0KFDJUklJSVKSEhQZmZmi4/Nzs5WSUlJq88zc+ZMZWRkhN7y8vLCXXpM8DE4ssM+3lWhmoaAslISNDgn3epyQgoO9slwcgmAnUVNkJk8ebI2bNigP//5zyf0PNOmTVNlZWXorbi4uJMqjG05B5t9S7lLpt3M00rj+neT223NWILWDDt4BHvTXr8ammj4BWBPlvbImG6++Wa9+eabev/999WrV6/Q4z6fTw0NDaqoqGixKlNaWiqfz9fqc3m9Xnm93nCXHHPokem40FiCKNpWkqQ+WclKT/TIX9ekLaVVGnow2ACAnVi6ImMYhm6++Wa9+uqrevfdd9W3b98W7z/99NMVHx+voqKi0GObN2/Wrl27NGbMmEiXG9PMIFNeXc9/vbdDZW1j6K6WaOqPkZqP1TMJG4DdWRpkJk+erP/+7//WSy+9pLS0NJWUlKikpEQHDjQf8c3IyNANN9ygqVOn6r333tOaNWt0/fXXa8yYMZxYirCuyQlKiHPLMKSyKlZl2urDL8sVNKT+PVJDox6iyaFJ2AQZAPZk6dbSggULJEnnnntui8efeeYZXXfddZKkRx99VG63W5dddpnq6+s1YcIEPfnkkxGuFG63S9kZXhXvP6CSyjr16pJsdUm28H4UjSVojdkns353hbWFAEAHWRpkDMM47sckJiZq/vz5mj9/fgQqwrHkpCc1Bxkaftvk8LEE0RpkzBWZzSVVqmsMKDHe+sv6AKA9oubUEqJfNnfJtMvOfbX66psDio9zRcVYgtb0zExS15QENQYMbS6psrocAGg3ggzaLIeTS+2y9OBtvqf17qIUb1QcEDyCy+U6bBJ2hbXFAEAHEGTQZubgSFZk2sacr3T2ydExluBohtPwC8DGCDJos0MrMgyOPJ6mw8YSjO8fnf0xpgKOYAOwMYIM2sy8S6bUX29xJdFv3VcVqqpvUmZyfNRfNGfOXNpSWqUDDQGLqwGA9iHIoM0OBZk6BYLHP3EWy8zbfMf176a4KBpL0Jrs9ET1SPMqaEib9rIqA8BeCDJos+6pXrldUlPQ0L5qVmWOxQwyZ0X5tpJpGH0yAGyKIIM288S51SONk0vH469r1CfFFZKib77S0RT0zJQkrSfIALAZggzaheGRx7f8y30KBA3165ZimxuQzRWZdRzBBmAzBBm0S07oUjxOLh1NtN/m2xrzht9t5TWqqmu0uBoAaDuCDNol27xLhpNLR7XsYH/M+AHRfX/M4bqletUzM0mGIW3c47e6HABoM4IM2iWaVmQ+Ka7Q0q1ft2lmV6QU76/Vjn218rhdOrNfV6vLaRfzhl/6ZADYSXTem46oFQ09MuXV9Xro75/prx/vltR8c+4DE4eoT1aKZTWZzNNKI3pnKi0x3uJq2qegV4be2liiT7kYD4CNsCKDdsnJSJIkSyZgB4OGXly5U+f9/t/668e75XJJ8XEuvb/la33n0ff12DtbVd9k7YVuh/pj7LOtZDIbftfT8AvARggyaJfDB0dGcktnw+5K/WDBh7rr1Q3y1zVpaM90vXrTOL095WyN799NDU1BPfrOFn137tJQj0qkBYKGPvjC7I+xT6Ovydxa2rGvVpW1NPwCsAeCDNqlR7pXktTQFFRFBP6xq6pr1L2vb9T3n1imdcUVSvV6dO/Fg/W3yeN1al6m+nVP1Qs3jNK8q0aoe5pX28tr9JOnV+o//vSxyiK8avTpVxXy1zUpPdGjYVE+lqA1mckJ6t21+bg4c5cA2AVBBu3i9cQpKyVBUnj7ZAzD0Juf7tH5jyzRsx/uUNCQvjcsR0W/OkfXjevb4tp/l8ul7w/PbX7f2Hy5XdIb65o/97kPd0RsnIK5EjT2pG7yxNnzr1boht/dFdYWAgBtZM+ftrCU2fBb4g/PyaUd5TW65o+rdPNLH6usql75Wcl64YZReuLHp4WOf7cmPTFe935/iP42ebyG98pQVX2T7nl9oy6Z/4E+jUDfR2gswcn221YyHeqTYUUGgD0QZNBuOWE6uVTXGNDcd7bogrnva+nWciV43JpSOEBvTTm7Xc2zBb0y9NebxumBiUOUlujR+t2Vmjj/A01/bYMqD4RnO6y6vklrd30jSTqrv/0afU3mqAJmLgGwC45fo91CKzKdGGSWbv1ad/9to7aX10hqvhX3/olD1bdbx45Ux7ldunpMviYM9WnmPz7Xqx/v1gsrduqfG0r024sGaeKpuXK5Om8q9Yov96kpaKhPVrJ6Z9ljLEFrhvZMlyTtrjigfdX1ykr1WlwRABwbKzJot9AR7E4IMqX+Ot380lpd/fQqbS+vUY80r5748Qg9/9NRHQ4xh+uRlqhHrzhVL/2/0erXPUXl1fWa8vInmvRfK/VFWfUJP79pmXlaySbTro8mLTFe/bo3f99p+AVgBwQZtNuhMQUdDzKBoKFnPtiu8x9Zojc/3Su3S7p+XL6KfnWOvjesc1dLJGls/276561n6dcXnCyvx60Pv9ynCx97X79/e7PqGk/87pn3bXx/zLeZJ67YXgJgBwQZtNuJ9sisK67QxPnLdN8bm1Rd36TheZl6/ebxuufiIWG9DdfridPN5w3Q4tvO0f85pbsaA4aeeO8LfefRJXrv87IOP+/uigPa9nWN4twujTkpqxMrtkZBr0xJBBkA9kCPDNqtoz0ylQca9bu3P9eLK3fJMKT0RI/u+O5AXTWqd4vj1OHWOytZf7zuDL29sUT3vbFJxfsP6PpnV+u7Q3y65/uDQ1tnbbXs4GrM8F4Zykiy11iC1gw3Ty5xBBuADRBk0G6+g1tL1fVNqqprPO4qimEYeu2T3Zrx989UXt0gSbp0RE9N+7+D1D3NmmZSl8ul7w7N0fgB3fXYO1v0xw926K2NJXp/69ea+p2Tdd3Y/DbfBRM6du2AbSVJGpybLrdLKvXXq9Rfd8wj7wBgNbaW0G4pXo/SE5sz8PFWZb4oq9aPn1qp215ep/LqBp3UPUV/+tmZmnPFqZaFmMOlej2666LBevM/xuv0Pl1U2xDQg3//TN97fJnW7Nx/3M8PHjaW4CwbjiVoTXKCRwN6pEniPhkA0Y8ggw45dCle60HmQENAv3v7c1342Ptavm2fvB63bp9wiv5569lR2UcyKCddf/n5GM26rECZyfH6vKRKly1Yrjv/91N9U9Nw1M/buMevb2obleb1aHheZuQKDrOC0A2/BBkA0Y0ggw7xHewjaa3h993PS/WdR5do/ntfqjFg6LyBPfTO1HM0+f/0V4Inev/Iud0uXXFGb737q3P1o9N7SZL+vLpY589Zolc+KlawlVEH5mmlM0/KUrxNxxK0hknYAOyCHhl0SE76kQ2/eyoO6P43NumtjSXNH5ORqHsuHqIJQ7I7/Th1OHVNSdDvfjRcl5+Rp7teXa8tpdW6438+1V8+KtaDlxToFF9a6GPN+UpnO2RbyVRw2BFswzBs9fsHILY45z8hEVG+w45gNwaCeur9bSqcs0RvbSxRnNulG8/up3emnqPvDvXZ9h/BM/K76u+3nKVpFw5UUnycVu/4RhfNW6qZ//hMtQ1Nqm1o0kcH+2jGO6TR1zQoJ10et0v7ahq0J4zDQQHgRLEigw4x75JZu/MbXfz4Mn1eUiVJGtmnix78wVAN9KVbWV6niY9z6+fnnKTvDc/Vfa9v1L82leoP72/TG+v26KJhOWoMGOrVJUn5Nh5L0JrE+Did4kvTxj1+rf+qQj0z23ckHQAihRUZdEj2wSCzubRKn5dUqUtyvGZfNkyv/HyMY0LM4XpmJmnRNSP19LUj1TMzSXsq6/TU0u2Smk8r2XXV6VjMPhkuxgMQzQgy6JB+h81BumJknop+da4uPyNP7ghebGeF8wdl652p5+imc0+S5+DXWjgo2+KqwsOchM3MJQDRjK0ldEifrBQ9e/0Zykrxho7qxoqkhDjd8d2B+tHIPO3aX6tzTnZWf4zp8BUZGn4BRCuCDDrs3FN6WF2Cpfp2S+mUCd3R6uTsNCXEuVV5oFHF+w+ot8P6gAA4A1tLAFqV4HFrUE7zUfN13CcDIEoRZAAc1bCDk7DpkwEQrQgyAI4qNKqAFRkAUYogA+CozIbfDbv9rY5oAACrEWQAHFX/7qlKjHerur5J2/fVWF0OAByBIAPgqDxxbg3JNQdI0icDIPoQZAAckzlAkpNLAKIRQQbAMZl9MqzIAIhGBBkAx2Qewd64x6+mQNDaYgDgWwgyAI6pX7cUpSTE6UBjQF9+TcMvgOhCkAFwTG63S0N7cp8MgOhEkAFwXKE+GW74BRBlCDIAjqvgYJ/MpzT8AogyBBkAxzXs4NbSpr1+NTTR8AsgehBkABxXn6xkpSd61NAU1JbSKqvLAYAQggyA43K5XEzCBhCVCDIA2uTQJGyCDIDoQZAB0CZmn8z63RXWFgIAhyHIAGgTc0Vmc0mV6hoDFlcDAM0IMgDapGdmkrqmJKgxYGhzCQ2/AKIDQQZAm7hcrtAkbG74BRAtCDIA2mwYDb8AogxBBkCbcQQbQLQhyABoM3NFZktplQ400PALwHoEGQBtlp2eqB5pXgUNadNeVmUAWI8gA6Bd6JMBEE0IMgDapaBnpiRpPUEGQBQgyABoF3NFZh1HsAFEAYIMgHYxb/jdVl6jqrpGi6sBEOsIMgDapVuqVz0zk2QY0sY9fqvLARDjCDIA2s284Zc+GQBWI8gAaDdze+lTLsYDYDGCDIB2Mxt+19PwC8Bitggy8+fPV35+vhITEzV69GitWrXK6pKAmGZuLe3YV6vKWhp+AVgn6oPMyy+/rKlTp+qee+7R2rVrNXz4cE2YMEFlZWVWlwbErMzkBPXumiyJuUsArOWxuoDjmTNnjn72s5/p+uuvlyQtXLhQf//73/XHP/5Rd955p8XVAbGroFeGdu2v1Ydfliu/W7LV5QCwUGZyglK91kSKqA4yDQ0NWrNmjaZNmxZ6zO12q7CwUMuXL2/1c+rr61VfXx/6td/P8VAgHIb3ytDfP92rJ//9pZ7895dWlwPAQg/9oEA/Ht3bkteO6iBTXl6uQCCg7OzsFo9nZ2fr888/b/VzZs6cqfvuuy8S5QEx7btDcvTchztVXl1//A8G4GhxFjaqRHWQ6Yhp06Zp6tSpoV/7/X7l5eVZWBHgTL2zkvXBnedZXQaAGBfVQaZbt26Ki4tTaWlpi8dLS0vl8/la/Ryv1yuv1xuJ8gAAgMWi+tRSQkKCTj/9dBUVFYUeCwaDKioq0pgxYyysDAAARIOoXpGRpKlTp+raa6/VyJEjNWrUKM2dO1c1NTWhU0wAACB2RX2QueKKK/T111/r7rvvVklJiU499VS99dZbRzQAAwCA2OMyDMOwuohw8vv9ysjIUGVlpdLT060uBwAAtEFb//2O6h4ZAACAYyHIAAAA2yLIAAAA2yLIAAAA2yLIAAAA2yLIAAAA2yLIAAAA2yLIAAAA2yLIAAAA24r6EQUnyry42O/3W1wJAABoK/Pf7eMNIHB8kKmqqpIk5eXlWVwJAABor6qqKmVkZBz1/Y6ftRQMBrVnzx6lpaXJ5XJ12vP6/X7l5eWpuLg4Zmc4xfr3INa/fonvQax//RLfA77+8H39hmGoqqpKubm5cruP3gnj+BUZt9utXr16he3509PTY/IP7+Fi/XsQ61+/xPcg1r9+ie8BX394vv5jrcSYaPYFAAC2RZABAAC2RZDpIK/Xq3vuuUder9fqUiwT69+DWP/6Jb4Hsf71S3wP+Pqt//od3+wLAACcixUZAABgWwQZAABgWwQZAABgWwQZAABgWwSZDpo/f77y8/OVmJio0aNHa9WqVVaXFBEzZ87UGWecobS0NPXo0UOXXHKJNm/ebHVZlnr44Yflcrk0ZcoUq0uJmN27d+snP/mJsrKylJSUpIKCAn300UdWlxUxgUBA06dPV9++fZWUlKSTTjpJDzzwwHFnwtjV+++/r4svvli5ublyuVx67bXXWrzfMAzdfffdysnJUVJSkgoLC7V161Zrig2TY30PGhsb9Zvf/EYFBQVKSUlRbm6urrnmGu3Zs8e6gjvZ8f4MHO4Xv/iFXC6X5s6dG5HaCDId8PLLL2vq1Km65557tHbtWg0fPlwTJkxQWVmZ1aWF3ZIlSzR58mStWLFCixcvVmNjoy644ALV1NRYXZolVq9erT/84Q8aNmyY1aVEzDfffKNx48YpPj5e//znP7Vp0yY98sgj6tKli9WlRcysWbO0YMECPfHEE/rss880a9YszZ49W48//rjVpYVFTU2Nhg8frvnz57f6/tmzZ2vevHlauHChVq5cqZSUFE2YMEF1dXURrjR8jvU9qK2t1dq1azV9+nStXbtWf/3rX7V582Z9//vft6DS8DjenwHTq6++qhUrVig3NzdClUky0G6jRo0yJk+eHPp1IBAwcnNzjZkzZ1pYlTXKysoMScaSJUusLiXiqqqqjAEDBhiLFy82zjnnHOPWW2+1uqSI+M1vfmOMHz/e6jIsddFFFxk//elPWzx26aWXGpMmTbKoosiRZLz66quhXweDQcPn8xm/+93vQo9VVFQYXq/X+NOf/mRBheH37e9Ba1atWmVIMnbu3BmZoiLoaF//V199ZfTs2dPYsGGD0adPH+PRRx+NSD2syLRTQ0OD1qxZo8LCwtBjbrdbhYWFWr58uYWVWaOyslKS1LVrV4sribzJkyfroosuavFnIRa8/vrrGjlypH70ox+pR48eGjFihJ566imry4qosWPHqqioSFu2bJEkrVu3TsuWLdOFF15ocWWRt337dpWUlLT4e5CRkaHRo0fH5M9EU2VlpVwulzIzM60uJSKCwaCuvvpq3X777RoyZEhEX9vxQyM7W3l5uQKBgLKzs1s8np2drc8//9yiqqwRDAY1ZcoUjRs3TkOHDrW6nIj685//rLVr12r16tVWlxJx27Zt04IFCzR16lT953/+p1avXq1bbrlFCQkJuvbaa60uLyLuvPNO+f1+DRw4UHFxcQoEApoxY4YmTZpkdWkRV1JSIkmt/kw03xdr6urq9Jvf/EZXXXVVzAySnDVrljwej2655ZaIvzZBBh02efJkbdiwQcuWLbO6lIgqLi7WrbfeqsWLFysxMdHqciIuGAxq5MiReuihhyRJI0aM0IYNG7Rw4cKYCTKvvPKKXnzxRb300ksaMmSIPvnkE02ZMkW5ubkx8z1A6xobG3X55ZfLMAwtWLDA6nIiYs2aNXrssce0du1auVyuiL8+W0vt1K1bN8XFxam0tLTF46WlpfL5fBZVFXk333yz3nzzTb333nvq1auX1eVE1Jo1a1RWVqbTTjtNHo9HHo9HS5Ys0bx58+TxeBQIBKwuMaxycnI0ePDgFo8NGjRIu3btsqiiyLv99tt155136sorr1RBQYGuvvpq3XbbbZo5c6bVpUWc+XMv1n8mSodCzM6dO7V48eKYWY1ZunSpysrK1Lt379DPxJ07d+pXv/qV8vPzw/76BJl2SkhI0Omnn66ioqLQY8FgUEVFRRozZoyFlUWGYRi6+eab9eqrr+rdd99V3759rS4p4s4//3ytX79en3zySeht5MiRmjRpkj755BPFxcVZXWJYjRs37ogj91u2bFGfPn0sqijyamtr5Xa3/PEZFxenYDBoUUXW6du3r3w+X4ufiX6/XytXroyJn4kmM8Rs3bpV77zzjrKysqwuKWKuvvpqffrppy1+Jubm5ur222/X22+/HfbXZ2upA6ZOnaprr71WI0eO1KhRozR37lzV1NTo+uuvt7q0sJs8ebJeeukl/e1vf1NaWlpoDzwjI0NJSUkWVxcZaWlpR/QEpaSkKCsrKyZ6hW677TaNHTtWDz30kC6//HKtWrVKixYt0qJFi6wuLWIuvvhizZgxQ71799aQIUP08ccfa86cOfrpT39qdWlhUV1drS+++CL06+3bt+uTTz5R165d1bt3b02ZMkUPPvigBgwYoL59+2r69OnKzc3VJZdcYl3RnexY34OcnBz98Ic/1Nq1a/Xmm28qEAiEfjZ27dpVCQkJVpXdaY73Z+DbwS0+Pl4+n0+nnHJK+IuLyNkoB3r88ceN3r17GwkJCcaoUaOMFStWWF1SREhq9e2ZZ56xujRLxdLxa8MwjDfeeMMYOnSo4fV6jYEDBxqLFi2yuqSI8vv9xq233mr07t3bSExMNPr162fcddddRn19vdWlhcV7773X6t/7a6+91jCM5iPY06dPN7Kzsw2v12ucf/75xubNm60tupMd63uwffv2o/5sfO+996wuvVMc78/At0Xy+LXLMBx6FSUAAHA8emQAAIBtEWQAAIBtEWQAAIBtEWQAAIBtEWQAAIBtEWQAAIBtEWQAAIBtEWQAAIBtEWQAOF5+fr7mzp1rdRkAwoAgA6BTXXfddaEZO+eee66mTJkSsdd+9tlnlZmZecTjq1ev1o033hixOgBEDkMjAUS9hoaGExq81717906sBkA0YUUGQFhcd911WrJkiR577DG5XC65XC7t2LFDkrRhwwZdeOGFSk1NVXZ2tq6++mqVl5eHPvfcc8/VzTffrClTpqhbt26aMGGCJGnOnDkqKChQSkqK8vLydNNNN6m6ulqS9O9//1vXX3+9KisrQ6937733Sjpya2nXrl2aOHGiUlNTlZ6erssvv1ylpaWh999777069dRT9cILLyg/P18ZGRm68sorVVVVFd5vGoB2I8gACIvHHntMY8aM0c9+9jPt3btXe/fuVV5enioqKnTeeedpxIgR+uijj/TWW2+ptLRUl19+eYvPf+6555SQkKAPPvhACxculCS53W7NmzdPGzdu1HPPPad3331Xd9xxhyRp7Nixmjt3rtLT00Ov9+tf//qIuoLBoCZOnKj9+/dryZIlWrx4sbZt26Yrrriixcd9+eWXeu211/Tmm2/qzTff1JIlS/Twww+H6bsFoKPYWgIQFhkZGUpISFBycrJ8Pl/o8SeeeEIjRozQQw89FHrsj3/8o/Ly8rRlyxadfPLJkqQBAwZo9uzZLZ7z8H6b/Px8Pfjgg/rFL36hJ598UgkJCcrIyJDL5Wrxet9WVFSk9evXa/v27crLy5MkPf/88xoyZIhWr16tM844Q1Jz4Hn22WeVlpYmSbr66qtVVFSkGTNmnNg3BkCnYkUGQEStW7dO7733nlJTU0NvAwcOlNS8CmI6/fTTj/jcd955R+eff7569uyptLQ0XX311dq3b59qa2vb/PqfffaZ8vLyQiFGkgYPHqzMzEx99tlnocfy8/NDIUaScnJyVFZW1q6vFUD4sSIDIKKqq6t18cUXa9asWUe8LycnJ/T/U1JSWrxvx44d+t73vqdf/vKXmjFjhrp27aply5bphhtuUENDg5KTkzu1zvj4+Ba/drlcCgaDnfoaAE4cQQZA2CQkJCgQCLR47LTTTtP//u//Kj8/Xx5P238ErVmzRsFgUI888ojc7ubF5FdeeeW4r/dtgwYNUnFxsYqLi0OrMps2bVJFRYUGDx7c5noARAe2lgCETX5+vlauXKkdO3aovLxcwWBQkydP1v79+3XVVVdp9erV+vLLL/X222/r+uuvP2YI6d+/vxobG/X4449r27ZteuGFF0JNwIe/XnV1tYqKilReXt7qllNhYaEKCgo0adIkrV27VqtWrdI111yjc845RyNHjuz07wGA8CLIAAibX//614qLi9PgwYPVvXt37dq1S7m5ufrggw8UCAR0wQUXqKCgQFOmTFFmZmZopaU1w4cP15w5czRr1iwNHTpUL774ombOnNniY8aOHatf/OIXuuKKK9S9e/cjmoWl5i2iv/3tb+rSpYvOPvtsFRYWql+/fnr55Zc7/esHEH4uwzAMq4sAAADoCFZkAACAbRFkAACAbRFkAACAbRFkAACAbRFkAACAbRFkAACAbRFkAACAbRFkAACAbRFkAACAbRFkAACAbRFkAACAbf1/vMD8m5kd3FAAAAAASUVORK5CYII=", "text/plain": [ "
" ] @@ -154,6 +163,13 @@ " \"\"\" A linear predictor function \"\"\"\n", " return 0\n", "\n", + "def compute_loss(inputs, outputs):\n", + " l = 0\n", + " for x,y in zip(inputs, outputs):\n", + " y_hat = fun(x)\n", + " l += loss(y_hat, y)\n", + " return l\n", + "\n", "optimizer = OptoPrime(fun.parameters())\n", "\n", "ls = []\n", @@ -189,7 +205,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 16, "metadata": {}, "outputs": [ { @@ -197,24 +213,25 @@ "output_type": "stream", "text": [ "Iteration 0 Loss: 85\n", - "Iteration 1 Loss: 15\n", + "Iteration 1 Loss: 10\n", "Iteration 2 Loss: 10\n", - "Iteration 4 Loss: 10\n", - "Iteration 5 Loss: 6\n", - "Iteration 6 Loss: 6\n", - "Iteration 7 Loss: 5\n", - "Iteration 8 Loss: 5\n", - "Iteration 9 Loss: 1\n", - "Iteration 10 Loss: 0\n", - "Iteration 11 Loss: 0\n", - "Iteration 12 Loss: 0\n", - "Iteration 13 Loss: 9\n", - "Iteration 14 Loss: 120\n" + "Iteration 3 Loss: 120\n", + "Iteration 4 Loss: 120\n", + "Iteration 5 Loss: 120\n", + "Iteration 6 Loss: 60\n", + "Iteration 7 Loss: 30\n", + "Iteration 8 Loss: 30\n", + "Iteration 9 Loss: 15\n", + "Iteration 10 Loss: 10\n", + "Iteration 11 Loss: 10\n", + "Iteration 12 Loss: 15\n", + "Iteration 13 Loss: 55\n", + "Iteration 14 Loss: 15\n" ] }, { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjsAAAGwCAYAAABPSaTdAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAAA9hAAAPYQGoP6dpAABBPElEQVR4nO3de3jU5Z338c9MDpPzhASYSTQIaCRCEBCEilpwBTwVy/JYD1gPa6+2Lmil1KqUHtA+JkpXSiurFtsq1aJ2t2pdn4pEpXhgFQRRQYQqgQQykxAIk/Np5vf8kcyEGFASJvnN/Ob9uq65VmZy+DJrk4/3/b2/t80wDEMAAAAWZTe7AAAAgP5E2AEAAJZG2AEAAJZG2AEAAJZG2AEAAJZG2AEAAJZG2AEAAJYWb3YBkSAQCKiiokLp6emy2WxmlwMAAE6AYRiqq6tTbm6u7Pbjr98QdiRVVFQoLy/P7DIAAEAflJeX69RTTz3u64QdSenp6ZI63qyMjAyTqwEAACeitrZWeXl5od/jx0PYkUJbVxkZGYQdAACizFe1oNCgDAAALI2wAwAALI2wAwAALI2wAwAALI2wAwAALI2wAwAALI2wAwAALI2wAwAALI2wAwAALI2wAwAALM3UsPPmm29q9uzZys3Nlc1m04svvhh6ra2tTXfffbfGjh2r1NRU5ebm6sYbb1RFRUW3r9HS0qLbb79dgwcPVmpqqq688krt379/gP8mAAAgUpkadhoaGjRu3DitXLmyx2uNjY3aunWrfvazn2nr1q16/vnntXv3bl155ZXdPm7hwoV64YUX9Oyzz+rtt99WfX29vvGNb8jv9w/UXwMAAEQwm2EYhtlFSB2XeL3wwguaM2fOcT9m8+bNmjx5svbt26dhw4bJ5/NpyJAheuqpp3TNNddIkioqKpSXl6e///3vuuSSS07oe9fW1srpdMrn83ERKAAAYXTgSJNskoamOxQfF941lhP9/R1VPTs+n082m02ZmZmSpC1btqitrU2zZs0KfUxubq4KCwu1cePG436dlpYW1dbWdnsAAIDwe/CVTzX1gTf0x3dKTashasJOc3Oz7rnnHs2bNy+U3rxerxITEzVo0KBuH+tyueT1eo/7tYqLi+V0OkOPvLy8fq0dAIBY5fU1S5JynMmm1RAVYaetrU3XXnutAoGAHnnkka/8eMMwZLPZjvv64sWL5fP5Qo/y8vJwlgsAADpV+JokSbmZSabVEPFhp62tTVdffbVKS0tVUlLSbU/O7XartbVVNTU13T6nqqpKLpfruF/T4XAoIyOj2wMAAIRXIGCosrZjZcfNys6xBYPOP//5T7322mvKzs7u9vrEiROVkJCgkpKS0HMej0fbt2/X1KlTB7pcAABwlOqGFrX5DdltHQ3KZok37TtLqq+v12effRb6c2lpqbZt26asrCzl5ubqqquu0tatW/Xyyy/L7/eH+nCysrKUmJgop9Op73znO/rRj36k7OxsZWVl6c4779TYsWM1Y8YMs/5aAABAkudIx6rOkHSHEsJ8Eqs3TA0777//vi666KLQnxctWiRJuummm7R06VK99NJLkqTx48d3+7z169dr+vTpkqRf//rXio+P19VXX62mpiZdfPHFevLJJxUXFzcgfwcAAHBsnghoTpZMDjvTp0/Xl435OZERQElJSXr44Yf18MMPh7M0AABwkjydzck5TvOak6UI79kBAADRKxKOnUuEHQAA0E8qOsOOmcfOJcIOAADoJ97ObSw321gAAMCKKo6wjQUAACzq6IGCNCgDAADLqa5vUXvA/IGCEmEHAAD0g2Bz8tD0JMWbOFBQIuwAAIB+EGxOzjH5JJZE2AEAAP0g2Jyca3JzskTYAQAA/cAbuu2clR0AAGBBFUci46oIibADAAD6QaRcFSERdgAAQD8I3XhOgzIAALAaf8AI9eywjQUAACynur5F/oChOLtNQ9MJOwAAwGKCzclD0x2Ks9tMroawAwAAwqyrOdn8VR2JsAMAAMKsItScbP5JLImwAwAAwix0VUQGKzsAAMCCWNkBAACW5omg6ckSYQcAAIQZDcoAAMCy/AFDlXUtkiLjqgiJsAMAAMLoYF3HQMF4u01D0h1mlyOJsAMAAMKoovMklisjKSIGCkqEHQAAEEbBfh13hPTrSIQdAAAQRhURdhJLIuwAAIAw8kTYSSyJsAMAAMKo69h5ZJzEkgg7AAAgjIINyqzsAAAAS/JG2FUREmEHAACESbs/oMrajrCTy8oOAACwmoP1LQoYUrzdpuy0yBgoKBF2AABAmFQc6VjViaSBghJhBwAAhIknApuTJcIOAAAIk0hsTpYIOwAAIEyC21is7AAAAEvy1rKNBQAALKxrZYdtLAAAYEHeCLwXSyLsAACAMGj3B1RVF2xQJuwAAACLqazrGCiYEGfT4NTIGSgoEXYAAEAYeDtn7LgykmSPoIGCEmEHAACEQaQeO5cIOwAAIAy6mpMj6ySWZHLYefPNNzV79mzl5ubKZrPpxRdf7Pa6YRhaunSpcnNzlZycrOnTp2vHjh3dPqalpUW33367Bg8erNTUVF155ZXav3//AP4tAABARfCqiAhrTpZMDjsNDQ0aN26cVq5ceczXly1bpuXLl2vlypXavHmz3G63Zs6cqbq6utDHLFy4UC+88IKeffZZvf3226qvr9c3vvEN+f3+gfprAAAQ80IrOxmRF3bizfzml112mS677LJjvmYYhlasWKElS5Zo7ty5kqTVq1fL5XJpzZo1+v73vy+fz6c//OEPeuqppzRjxgxJ0tNPP628vDy99tpruuSSSwbs7wIAQCyriNB7saQI7tkpLS2V1+vVrFmzQs85HA5NmzZNGzdulCRt2bJFbW1t3T4mNzdXhYWFoY85lpaWFtXW1nZ7AACAvvMcicyrIqQIDjter1eS5HK5uj3vcrlCr3m9XiUmJmrQoEHH/ZhjKS4ultPpDD3y8vLCXD0AALGjzR/QwfoWSTQo94nN1v2svmEYPZ77oq/6mMWLF8vn84Ue5eXlYakVAIBYVFnbLKNzoGB2aqLZ5fQQsWHH7XZLUo8VmqqqqtBqj9vtVmtrq2pqao77McficDiUkZHR7QEAAPom2JzsdkbeQEEpgsPOiBEj5Ha7VVJSEnqutbVVGzZs0NSpUyVJEydOVEJCQreP8Xg82r59e+hjAABA/6qI4Bk7ksmnserr6/XZZ5+F/lxaWqpt27YpKytLw4YN08KFC1VUVKT8/Hzl5+erqKhIKSkpmjdvniTJ6XTqO9/5jn70ox8pOztbWVlZuvPOOzV27NjQ6SwAANC/gldFRGJzsmRy2Hn//fd10UUXhf68aNEiSdJNN92kJ598UnfddZeampo0f/581dTUaMqUKVq3bp3S09NDn/PrX/9a8fHxuvrqq9XU1KSLL75YTz75pOLi4gb87wMAQCzquioiMld2bIZhGGYXYbba2lo5nU75fD76dwAA6KXvP/W+Xt1RqXuvHKObpg4fsO97or+/I7ZnBwAARIeue7EicxuLsAMAAE5KpDcoE3YAAECftbYHVB0cKBiBl4BKhB0AAHASggMFE+PsETlQUCLsAACAk+Ct7Roo+FU3HJiFsAMAAPqsIoIvAA0i7AAAgD7zRPhJLImwAwAATkLo2HlmZJ7Ekgg7AADgJLCNBQAALC3YoBypM3Ykwg4AADgJXfdisbIDAAAspttAQcIOAACwmsrOLazEeLuyInSgoETYAQAAfXR0c3KkDhSUCDsAAKCPupqTI3cLSyLsAACAPupqTo7ck1gSYQcAAPSR1xf5M3Ykwg4AAOijiiiYniwRdgAAQB+FrorIYGUHAABYkCe4jZVJ2AEAABbT0u5XdX2rJBqUAQCABVX6OiYnO+LtGpSSYHI1X46wAwAAeq3CFx0DBSXCDgAA6INQc3KEb2FJhB0AANAHFVHSnCwRdgAAQB90rewQdgAAgAVFy1UREmEHAAD0gbc2Oq6KkAg7AACgDzys7AAAAKtqbvPrUENwoCArOwAAwGIqaztWdZIS7MqM8IGCEmEHAAD0UrA5OdeZHPEDBSXCDgAA6KVgc7I7CrawJMJOv/IHDO05WK/q+hazSwEAIGyi6di5RNjpV7c/s1X/8tAG/c+HFWaXAgBA2ETTQEGJsNOvzhiSJkn61FNnciUAAISPJ4quipAIO/1qlDtDkvRpJWEHAGAdXdtYhJ2YN8qdLkna7a1TIGCYXA0AAOHhraVnB52GZ6coMd6upja/yg43ml0OAAAnrbnNr8OdAwVzCTuIj7PrTFdn346XrSwAQPQLNicnJ8QpIzne5GpODGGnn41ydfTt7CLsAAAsoOKo5uRoGCgoEXb6XUFn386n3lqTKwEA4ORF27FzibDT7wpyOsIOKzsAACvw+KKrOVki7PS74ImsvYca1NzmN7kaAABOTsWRzm0sVnYQNCTNoazURAUM6Z+V9WaXAwDASfGyshNe7e3t+ulPf6oRI0YoOTlZI0eO1H333adAIBD6GMMwtHTpUuXm5io5OVnTp0/Xjh07TKy6O5vNFurb2UnfDgAgylUEw06UTE+WIjzsPPjgg3rssce0cuVK7dy5U8uWLdOvfvUrPfzww6GPWbZsmZYvX66VK1dq8+bNcrvdmjlzpurqIqdHJriVRd8OACDaeX1sY4XV//7v/+qb3/ymrrjiCg0fPlxXXXWVZs2apffff19Sx6rOihUrtGTJEs2dO1eFhYVavXq1GhsbtWbNGpOr71JA2AEAWEBTq181jW2S2MYKmwsuuECvv/66du/eLUn68MMP9fbbb+vyyy+XJJWWlsrr9WrWrFmhz3E4HJo2bZo2btx43K/b0tKi2trabo/+VBC8I4ttLABAFAteE5GSGKeMpOgYKChJEV3p3XffLZ/Pp4KCAsXFxcnv9+v+++/XddddJ0nyer2SJJfL1e3zXC6X9u3bd9yvW1xcrHvvvbf/Cv+CM13pstmk6vpWVde3aHCaY8C+NwAA4eI56iRWtAwUlCJ8Zee5557T008/rTVr1mjr1q1avXq1/uM//kOrV6/u9nFffMMNw/jS/ycsXrxYPp8v9CgvL++X+oOSE+N0WlaKJLayAADRqyIKT2JJEb6y8+Mf/1j33HOPrr32WknS2LFjtW/fPhUXF+umm26S2+2W1LHCk5OTE/q8qqqqHqs9R3M4HHI4BnZ1ZZQ7XXsPNWqnp1bnnzF4QL83AADhEI3NyVKEr+w0NjbKbu9eYlxcXOjo+YgRI+R2u1VSUhJ6vbW1VRs2bNDUqVMHtNavEuzbYWUHABCtKqLwqggpwld2Zs+erfvvv1/Dhg3TmDFj9MEHH2j58uW65ZZbJHVsXy1cuFBFRUXKz89Xfn6+ioqKlJKSonnz5plcfXehE1mVhB0AQHQKDRTMZBsrbB5++GH97Gc/0/z581VVVaXc3Fx9//vf189//vPQx9x1111qamrS/PnzVVNToylTpmjdunVKT083sfKegrN2dlfWyR8wFGePnsYuAACk6LwqQpJshmEYZhdhttraWjmdTvl8PmVkZPTL9/AHDI35xVo1twX0xo+maeSQtH75PgAA9Jfx963TkcY2vbrw66H/iDfTif7+juieHSuJs9t0povhggCA6NTU6teR4EDBKLoqQiLsDKhRnWHnU8IOACDKVHSexEpNjFO6I6K7YHog7AygghwmKQMAotPRzcnRNFBQIuwMKO7IAgBEq2htTpYIOwMq2My173CjGlvbTa4GAIAT543SGTsSYWdADU5zaHBaogxD2l1Zb3Y5AACcsGi9KkIi7Ay4rknK9O0AAKJHtF4VIRF2BlxwK4sTWQCAaOKJ0unJEmFnwI2iSRkAEIVoUMYJO8sdPH5eJ4ZXAwCiQUNLu2qbOw7WEHbwlfJdabLbpMMNrTpY32J2OQAAfKXgFlaaI17pSQkmV9N7hJ0BlpQQp+HZqZLYygIARIdoPnYuEXZMUZDT2aTsIewAACJf8KqIaGxOlgg7phjl6urbAQAg0oVWdjJY2cEJCp3IqmTWDgAg8nlCKzuEHZyg4B1Zuyvr1e4PmFwNAABfruIIPTvopWFZKUpOiFNre0B7DzWaXQ4AAF/KG8VXRUiEHVPY7TadyXBBAECUqIjiqyIkwo5pClzBayPo2wEARK76lnbVBQcKchoLvRE6fs7KDgAgggUvAE1PileaI97kavqGsGMS7sgCAEQDT5QPFJQIO6Yp6Lwjq+xwoxpa2k2uBgCAY/Mcie7mZImwY5qs1EQNTXdIknZVsroDAIhM0d6cLBF2TMVWFgAg0kX7sXOJsGOqAsIOACDCVdCzg5MR7NvZ6eH4OQAgMnmj/KoIibBjqq47supkGIbJ1QAA0BMNyjgpZwxNU5zdpiONbaqqazG7HAAAuqlrblNd54lhtrHQJ0kJcRqenSKJrSwAQOQJNidnJMUrNUoHCkqEHdMV5HT07dCkDACINBUWOIklEXZMF7wji7ADAIg0VmhOlgg7pgs2KXNHFgAg0lQcif5j5xJhx3RndW5jfVZVrzZ/wORqAADoYoWBghJhx3SnZCYrNTFOrf6A9lY3mF0OAAAhVrgqQiLsmM5ut+lMtrIAABGIlR2ETXCS8qdejp8DACKHJxh2aFDGyeKOLABApKltblO9BQYKSoSdiMCJLABApAluYTmTE5SSGL0DBaU+hp3y8nLt378/9OdNmzZp4cKFWrVqVdgKiyXBlZ39NU2qa24zuRoAAKSKI9ZoTpb6GHbmzZun9evXS5K8Xq9mzpypTZs26Sc/+Ynuu+++sBYYCzJTEuXO6PiXaXclqzsAAPN1NSfHaNjZvn27Jk+eLEn6y1/+osLCQm3cuFFr1qzRk08+Gc76YgZbWQCASBK6KiIzuk9iSX0MO21tbXI4HJKk1157TVdeeaUkqaCgQB6PJ3zVxRCalAEAkSR0VURGjK7sjBkzRo899pjeeustlZSU6NJLL5UkVVRUKDs7O6wFxoqCnM6VHQ9hBwBgPk+sr+w8+OCD+t3vfqfp06fruuuu07hx4yRJL730Umh7C70zytU1a8cwDJOrAQDEOis1KPfpLNn06dNVXV2t2tpaDRo0KPT89773PaWkpIStuFhy+tBUxdltqm1ul7e2OeqnVQIAopdhGF0rOxYIO31a2WlqalJLS0so6Ozbt08rVqzQrl27NHTo0LAWeODAAX37299Wdna2UlJSNH78eG3ZsiX0umEYWrp0qXJzc5WcnKzp06drx44dYa1hIDji43T6kFRJbGUBAMxV29yuxla/pOi/KkLqY9j55je/qT/96U+SpCNHjmjKlCl66KGHNGfOHD366KNhK66mpkbnn3++EhIS9Morr+iTTz7RQw89pMzMzNDHLFu2TMuXL9fKlSu1efNmud1uzZw5U3V10RcYRoWujYi+2gEA1hE8dp6ZkqDkxDiTqzl5fQo7W7du1YUXXihJ+u///m+5XC7t27dPf/rTn/Tb3/42bMU9+OCDysvL0xNPPKHJkydr+PDhuvjii3X66adL6ljVWbFihZYsWaK5c+eqsLBQq1evVmNjo9asWRO2OgZK14ks7sgCAJin67bz6F/VkfoYdhobG5We3vGLed26dZo7d67sdru+9rWvad++fWEr7qWXXtKkSZP0rW99S0OHDtWECRP0+OOPh14vLS2V1+vVrFmzQs85HA5NmzZNGzduPO7XbWlpUW1tbbdHJBjlYtYOAMB8VhooKPUx7Jxxxhl68cUXVV5erldffTUUNqqqqpSRkRG24vbs2aNHH31U+fn5evXVV3XrrbfqBz/4QWgLzev1SpJcLle3z3O5XKHXjqW4uFhOpzP0yMvLC1vNJyN4/Pzzg/Vq8wdMrgYAEKs8FjqJJfUx7Pz85z/XnXfeqeHDh2vy5Mk677zzJHWs8kyYMCFsxQUCAZ1zzjkqKirShAkT9P3vf1/f/e53e/QF2Wy2bn82DKPHc0dbvHixfD5f6FFeXh62mk/GKZnJSnfEq81vaM/BBrPLAQDEqApWdqSrrrpKZWVlev/99/Xqq6+Gnr/44ov161//OmzF5eTkaPTo0d2eO+uss1RWViZJcrvdktRjFaeqqqrHas/RHA6HMjIyuj0igc1m05mhayMiY2sNABB7uraxYrhnR+oIGhMmTFBFRYUOHDggSZo8ebIKCgrCVtz555+vXbt2dXtu9+7dOu200yRJI0aMkNvtVklJSej11tZWbdiwQVOnTg1bHQOpgDuyAAAm62pQjuGVnUAgoPvuu09Op1OnnXaahg0bpszMTP3yl79UIBC+XpMf/vCHevfdd1VUVKTPPvtMa9as0apVq7RgwQJJHSshCxcuVFFRkV544QVt375dN998s1JSUjRv3ryw1TGQuCMLAGAmwzC6VnYscFWE1McJykuWLNEf/vAHPfDAAzr//PNlGIbeeecdLV26VM3Nzbr//vvDUty5556rF154QYsXL9Z9992nESNGaMWKFbr++utDH3PXXXepqalJ8+fPV01NjaZMmaJ169aFTotFm+CsHcIOAMAMtU1HDxS0xsqOzejDRUy5ubl67LHHQredB/3tb3/T/PnzQ9ta0aK2tlZOp1M+n8/0/h1fU5vG3btOkvThL2bJmZxgaj0AgNjyqbdWl654S4NSEvTBz2d99SeY6ER/f/dpG+vw4cPH7M0pKCjQ4cOH+/Il0cmZnKDcziS9u5LVHQDAwPIcsVZzstTHsDNu3DitXLmyx/MrV67U2WeffdJFxbpRNCkDAExipQtAg/rUs7Ns2TJdccUVeu2113TeeefJZrNp48aNKi8v19///vdw1xhzCnIytH7XQX3q4fg5AGBgeYInsTKtE3b6tLIzbdo07d69W//6r/+qI0eO6PDhw5o7d6527NihJ554Itw1xhxOZAEAzFJhwW2sPq3sSB1Nyl88dfXhhx9q9erV+uMf/3jShcWyUUeFna+aBg0AQDh5a601Y0c6iaGC6D8jB6cp3m5TXUu7DnTeTwIAwECgQRkDIjHerjOGpkliKwsAMHAMw7BkgzJhJ0JxIgsAMNB8TW1qausYKOi2UNjpVc/O3Llzv/T1I0eOnEwtOMoompQBAAMsuKqTlZqopIQ4k6sJn16FHafT+ZWv33jjjSdVEDqc1XltBLefAwAGisdiF4AG9SrscKx84ARXdvYcbFBre0CJ8ew4AgD6V9exc2uFHX6DRqgcZ5LSk+LVHjD0+cF6s8sBAMSA0G3nFjqJJRF2IpbNZmMrCwAwoCosOD1ZIuxENE5kAQAGkteCx84lwk5E40QWAGAgedjGwkAL3pH1qYewAwDoXx0DBa15GouwE8HO7Aw73tpm+RrbTK4GAGBlRxrb1NwWkCS5Mgg7GCAZSQk6JbNjKZEmZQBAfwo2J2dbbKCgRNiJeMGtrF2VbGUBAPpPqDnZYiexJMJOxCvI6Qg7O+nbAQD0owqLNidLhJ2IN6pz1s4utrEAAP3Ia9HmZImwE/GC21i7K+sVCBgmVwMAsCrPEVZ2YJIRg1OVGGdXfUu7DhxpMrscAIBFeSw6UFAi7ES8hDi7Th+aJolJygCA/mPVGTsSYScqhE5k0bcDAOgHHQMF2caCiYJhZycrOwCAflDT2KaW9s6Bgk6HydWEH2EnCnBHFgCgP1V09oQOTnPIEW+tgYISYScqFHQePy+tblBLu9/kagAAVmPV286DCDtRwJXhkDM5Qf6Aoc+q6s0uBwBgMVZuTpYIO1HBZrNxAzoAoN9Y+di5RNiJGtyRBQDoL6Gwk2m9k1gSYSdqBK+NYNYOACDcgg3KrOzAVMELQT/1MGsHABBe3lrrztiRCDtR40xXR9ipqmtRTUOrydUAAKyi+0BBVnZgojRHvPKyOhI3W1kAgHA53NCq1vaAbDbJlUHYgckKQn07bGUBAMIjuKozOM2hxHhrxgJr/q0sqoBJygCAMLP6FpZE2IkqwWsj2MYCAISL1QcKSoSdqBJc2dldWadAwDC5GgCAFVQcsfZJLImwE1WGZ6cqMd6uxla/ymsazS4HAGABXlZ2EEni4+zKH5omia0sAEB4VHT27LgJO4gUo2hSBgCEUfDG81yLXhUhEXaizlkcPwcAhEkgYITCDttYiBicyAIAhMvhxla1+q09UFAi7ESd4ImsvdUNam7zm1wNACCaeTpPYg1JcyghzrqRIKr+ZsXFxbLZbFq4cGHoOcMwtHTpUuXm5io5OVnTp0/Xjh07zCuynw1JdygrNVEBQ/pnZb3Z5QAAolhFDJzEkqIo7GzevFmrVq3S2Wef3e35ZcuWafny5Vq5cqU2b94st9utmTNnqq7Omts8NptNo1zBrSz6dgAAfdfVr2Pd5mQpSsJOfX29rr/+ej3++OMaNGhQ6HnDMLRixQotWbJEc+fOVWFhoVavXq3GxkatWbPGxIr7FyeyAADhEFzZsfKxcylKws6CBQt0xRVXaMaMGd2eLy0tldfr1axZs0LPORwOTZs2TRs3bjzu12tpaVFtbW23RzQpoEkZABAGXcfOrR124s0u4Ks8++yz2rp1qzZv3tzjNa/XK0lyuVzdnne5XNq3b99xv2ZxcbHuvffe8BY6gApygsfPCTsAgL7zxMBVEVKEr+yUl5frjjvu0NNPP62kpOOnTpvN1u3PhmH0eO5oixcvls/nCz3Ky8vDVvNAONOVJptNqq5v0aH6FrPLAQBEKU8tDcqm27Jli6qqqjRx4kTFx8crPj5eGzZs0G9/+1vFx8eHVnSCKzxBVVVVPVZ7juZwOJSRkdHtEU1SEuM1LCtFEn07AIC+6TZQ0MLTk6UIDzsXX3yxPv74Y23bti30mDRpkq6//npt27ZNI0eOlNvtVklJSehzWltbtWHDBk2dOtXEyvtfsG9nJ2EHANAH1Q0tavMbstmkoekOs8vpVxHds5Oenq7CwsJuz6Wmpio7Ozv0/MKFC1VUVKT8/Hzl5+erqKhIKSkpmjdvnhklD5hR7gy9uqNSuzh+DgDog+CqztB0aw8UlCI87JyIu+66S01NTZo/f75qamo0ZcoUrVu3Tunp6WaX1q8KOH4OADgJFUeCt51bewtLisKw849//KPbn202m5YuXaqlS5eaUo9ZQmGnsk7+gKE4+/EbsgEA+CJv54ydXIs3J0sR3rOD4zstO1VJCXY1twVUdrjR7HIAAFHGEyPTkyXCTtSKs9uUPzS4lUXfDgCgd7rCDis7iGChE1ke+nYAAL3jCV4CavHpyRJhJ6pxRxYAoK8qjrCygyhQ4O4YhrirkrADADhxgYChylp6dhAFgis7ew81qKnVb3I1AIBoUV3fovaAIXsMDBSUCDtRbUi6Q4PTEmUY0m5WdwAAJ8gTGiiYpHiLDxSUCDtRj74dAEBvxVJzskTYiXqjXB19O58SdgAAJyiWjp1LhJ2oV5DTsbLzKbN2AAAnKJYGCkqEnajHHVkAgN6qONK5jcXKDqJB/tB02WzSoYZWHaxrMbscAEAU8LKyg2iSnBinEdmpktjKAgCcmOA2lpuVHUQLTmQBAE6U/6iBgrmcxkK0CIYdTmQBAL5KcKBgnN2moemEHUQJmpQBACeqa6CgQ3F2m8nVDAzCjgUE78jaXVknf8AwuRoAQCTzxNhJLImwYwnDslKUnBCnlvaA9h5qMLscAEAEq4ixk1gSYccS7HabznSlSWIrCwDw5bw+VnYQpYJbWZ96OH4OADi+ihg7di4RdiyDE1kAgBMRHCiYm8k2FqJM6ERWJWEHAHB8NCgjagVXdvYdalRDS7vJ1QAAIpE/YKiy82ohGpQRdbLTHBqS7pDUcQQdAIAvOljXIn/nQMHg74xYQNixEIYLAgC+jKfzJJYrhgYKSoQdSxnlokkZAHB8wenJOTHUnCwRdiylIKfz+Dm3nwMAjqGiszk5lo6dS4QdSzl6G8swuDYCANBd6Ng5YQfR6oyhabLbpJrGNh3s7LYHACDIE4NXRUiEHUtJSojTiMGpkqSd9O0AAL7AE4NXRUiEHcsJXhuxi74dAMAX0KAMS+DaCADAsbT7A6oKDRRkZQdRLNik/KmHsAMA6HKwvmOgYLzdpsFpsTNQUCLsWE5wG+uzg/Vq9wdMrgYAECkqjnRsYbkykmJqoKBE2LGcUwclKyUxTq3tAe091GB2OQCACOENncSKrS0sibBjOXa7LdS3s5OtLABAp9BJrBhrTpYIO5bEHVkAgC/ysLIDK+GOLADAF8XqjB2JsGNJo4KzdiqZtQMA6MDKDiwluI1VfrhJ9S3tJlcDAIgEniOxeVWERNixpEGpiXJldMxQoG8HANAxUJCVHVhMaCuLsAMAMa+qrkUBQ0qIi72BghJhx7LOCl0bQd8OAMS6YHOyKyNJ9hgbKCgRdiyLO7IAAEGx3JwsEXYsa9RRs3YMwzC5GgCAmWK5OVmK8LBTXFysc889V+np6Ro6dKjmzJmjXbt2dfsYwzC0dOlS5ebmKjk5WdOnT9eOHTtMqjhynDE0TXF2m3xNbfLWNptdDgDARKzsRLANGzZowYIFevfdd1VSUqL29nbNmjVLDQ1ddz4tW7ZMy5cv18qVK7V582a53W7NnDlTdXWxvX3jiI/TyMGpktjKAoBYF8sDBaUIDztr167VzTffrDFjxmjcuHF64oknVFZWpi1btkjqWNVZsWKFlixZorlz56qwsFCrV69WY2Oj1qxZY3L15hvFtREAAEkVnSs7braxIp/P55MkZWVlSZJKS0vl9Xo1a9as0Mc4HA5NmzZNGzduPO7XaWlpUW1tbbeHFXFHFgBAkrydKzu5mazsRDTDMLRo0SJdcMEFKiwslCR5vV5Jksvl6vaxLpcr9NqxFBcXy+l0hh55eXn9V7iJCjpn7ez0WDPMAQC+Wps/oKq6Fkk0KEe82267TR999JGeeeaZHq/ZbN1nBhiG0eO5oy1evFg+ny/0KC8vD3u9kSC4jfX5wXq1+QMmVwMAMENVXYuMzoGC2amJZpdjiqgIO7fffrteeuklrV+/XqeeemroebfbLUk9VnGqqqp6rPYczeFwKCMjo9vDik4dlKw0R7za/IZKqxu++hMAAJbjOdKxheV2xuZAQSnCw45hGLrtttv0/PPP64033tCIESO6vT5ixAi53W6VlJSEnmttbdWGDRs0derUgS434thsttDqDltZABCbQsfOM2JzC0uS4s0u4MssWLBAa9as0d/+9jelp6eHVnCcTqeSk5Nls9m0cOFCFRUVKT8/X/n5+SoqKlJKSormzZtncvWRYZQ7XVv21dCkDAAxKnTsPEabk6UIDzuPPvqoJGn69Ondnn/iiSd08803S5LuuusuNTU1af78+aqpqdGUKVO0bt06paenD3C1kYkTWQAQ2yqOBI+dE3Yi0olcc2Cz2bR06VItXbq0/wuKQsETWQwWBIDY5O3cxsqN0ZNYUoT37ODkjXJ1rOwcONKk2uY2k6sBAAy0WJ+eLBF2LM+ZkhD6F3w3qzsAEHO67sViZQcWFuzb2UnYAYCY0toe0MH6zoGCMdygTNiJAaM6+3Z2eTl+DgCxpKquWYYhJcbZlZUSmwMFJcJOTOBEFgDEJo+v6yRWrA4UlAg7MSE4WPBTb90JnXADAFhDxVHTk2MZYScGnD4kTfF2m+qa23XbMx/ofz8/ROgBgBjQdeycsAOLS4y365vjT5Ek/b+PPLru8Xd18fIN+v1be3SksdXk6gAA/aVrGyt2T2JJhJ2Y8dDV4/Ty7RfousnDlJoYpz0HG/R//99OTS56XYue26b39x5mtQcALCY4Yyc3hk9iSRE+QRnhVXiKU8Vzx2rJFWfpb9sO6M/vlukTT62e/+CAnv/ggEa50nXd5Dz96zmnypmcYHa5AICTxIydDjaD/5xXbW2tnE6nfD6fMjIyzC5nwBiGoQ/3+7TmvX166cMKNbcFJElJCXbNPjtX86YM0/i8TNlssdvBDwDR7Nz7X9PBuha9fPsFKjzFaXY5YXeiv79Z2YlhNptN4/MyNT4vU0uuGK0XPzigNe+VaVdlnf5ry37915b9Gp2ToXlThmnOhFOU5uBfFwCIFq3tAVUHBwrGeIMyKzuK3ZWdYzEMQ1v21WjNe2V6+WOPWts7VntSE+N05fhTdP2UYZb8rwMAsJryw426cNl6JcbbteuXl1pylZ6VHfSJzWbTpOFZmjQ8Sz+fPVp/3XpAf35vn/YcbNAzm8r0zKYyjTvVqXlThmn2uFylJPKvEABEoq5+nSRLBp3e4DcVjiszJVHfuWCEbjl/uN4rPaw/v1emtds9+nC/Tx/u/1j/9+WdmjPhFM2bMkxn5cT2ihgARJrgSSx3RmxvYUmEHZwAm82mr43M1tdGZutQ/Wj995b9emZTmfYeatRT7+7TU+/u0znDMnX9lNN0xdk5SkqIM7tkAIh5wZWd3MzYPoklEXbQS9lpDn1/2un67oUjtfHzQ1qzaZ/W7ajU1rIj2lp2RPe9/InmntPR23PG0HSzywWAmOXpvCoi1puTJcIO+shut+mC/MG6IH+wquqa9V/vd6z27K9p0hPv7NUT7+zV5BFZun7KMF1a6JYjntUeABhIR/fsxDrCDk7a0PQkLbjoDN067XS99c+D+vN7ZXp9Z6U2lR7WptLDGpSSoG9NytN1k4dpxOBUs8sFgJjAQMEuhB2ETZzdpumjhmr6qKHy+pr13OZyPbu5TB5fs1a9uUer3tyjqadna96UYRp3aqYi4XBAQpxdQ9MdMX9SAYD1hBqUWdkh7KB/uJ1JumNGvhZcdLr+seug1mwq0/pdVdr4+SFt/PyQ2eV1Mzw7RZcW5uiyQrfOPtVJ8AEQ9Vra/aqu77jomQZlwg76WXycXTNGuzRjtEv7axr13OZyPb/1QGiqp9na/AHtPdSoxzZ8rsc2fK5cZ5IuKXTrssIcTTxtkOLsBB8A0afS1/Ez1hFv16AU7jok7GDAnDooRT+aNUo/mjXK7FJC6lvatf7TKq3d7tX6XVWq8DWHGqwHpzk0a4xLlxW69bWR2UqIs5tdLgCckOAWFgMFOxB2ENPSHPGaPS5Xs8flqrnNrzd3H9Ta7V6V7KxUdX2L1rxXpjXvlcmZnKAZZ3UEnwvyBzNLCEDEOljXopXrP5PEFlYQd2OJu7HQU2t7QP+755DWbvdq3Q6vDjW0hl5LTYzTRQVDdVlhjqaPGqJULkgFECHe3H1Qi/7yoarrW+SIt+uxGybqolFDzS6r35zo72/Cjgg7+HL+gKHNew9r7XavXt3hDR3nlDr2w79+5hBdVujWxQUuOdkbB2CC1vaAHlq3S797c48k6UxXmh6+7hyNclt7uCthpxcIOzhRgYChD/cf0dodXq3d7tW+Q42h1+LtNk09Y7AuK3Rr1miXstMcJlYKIFbsrW7QD579QB/t90mSvv21YfrpFaNjYrudsNMLhB30hWEY2ump6ww+Hu2urA+9ZrdJ5w7P0mWFbl1S6GaoF4B+8cIH+/XTF7arodUvZ3KCHvw/Z+vSQrfZZQ0Ywk4vEHYQDp8frNfa7R0rPh8f8HV7bXxepi7rPNI+LDvFpAoBWEV9S7t+/uJ2Pf/BAUnS5OFZWnHt+JhrSCbs9AJhB+G2v6YxFHy2lNXo6P+Vjc7J0KWFbl1W6Fa+y9r76QDC76P9R/SDZz7Q3kONstukOy4+U7f9yxkxOReMsNMLhB30p6raZr36SaXWbvfo3T2H5Q90/U/u9CGpncEnR2NyM5iHAeC4AgFDv397j5at3aX2gKFcZ5J+c90EnTs8y+zSTEPY6QXCDgZKTUOrSnZWau12r97+Z7Va/YHQa+lJ8REzuDA5IU7ZaYkalJKo7NREZaUmalBq1z8HH9mpDqUnxcseg/9FCQykqrpm/egvH+qtf1ZLki4rdOuBuWfH/AlQwk4vEHZghrrmNr3ROb35H7sOqqnNb3ZJfRJnt4VC0aDUBGWnOrqFoy+GpEEpiUqMj4xQB0SDf+yq0p3/9aGq61vliLfrF7PH6LrJeawEi7DTK4QdmK2p1a/9NY1f/YEDwFBH82NNQ6sONbTqcENrt38++lHf0t6n75GeFH/U6lBHAMpK6/rn4KpSVmpixPQhpDnilZmSaHYZiCGt7QH96tVP9fhbpZKkAne6fnvdBJ1Jr1/Iif7+ZvQrEAGSE+Oislm5pd2vmoY2HWpoCf3fLwtHNY2tChhSXXO76prbu80pigYTTxukS8e4dWmhW3lZnKpD/ymtbtAPnvkgdLLzxvNO008uPysmZuf0B1Z2xMoOMFACAUO+pjYd6gw+h+q7QlDHP7focGNbx/+tb1VNY5sCEfIjqqU90O3PhadkdAafHJ0xNM2kqmA1hmHo+a0H9LO/bVdjq1+ZKQla9n/O1qwxsTM7pzfYxuoFwg6Ar+LxNWndjkq9st2jTaWHddShOp0xNE2XFXas+IzO4VQd+qauuU0/e3G7XtxWIUmaMqJjdg5DSY+PsNMLhB0AvVFd36LXPqnUK9u92vh5tdr8XT9Gh2Wl6NLO4DP+1ExOquGEbCvvmJ1TdrhRcXabFl6cr/kXxebsnN4g7PQCYQdAX/ma2vTGp5V65WOvNuw+2G27y52RpEvGuHRpYY4mj8jiFxd6CAQMrXprj/7j1Y7ZOadkJus3147XpBiendMbhJ1eIOwACIfG1nb9Y9dBvbLdqzd2VqqhtWucQHZqomaOdunSQremnj6Y4/dQVW2zFv3lQ739WcfsnCvG5qho7lg5k2N7dk5vEHZ6gbADINya2/x657NqvbLdq5JPKuVragu9lp4UrxlndQSfaWcO4YRNDFr/acfsnEMNrUpKsGvp7DG65lxm5/QWYacXCDsA+lObP6D39hzWK9s9enVHparrW0KvJSfE6aKCIbq0MEf/UjBUaQ4mglhZS7tfy9bu0h/e7pqds3LeBJ0xNPpGT0QCwk4vEHYADBR/wNDWshq98rFXr+7w6sCRptBrifF2XXjGYF1a6NbM0S6GGFrMnoP1uv2ZD7SjolaSdPPU4brnsgJW9k4CYacXCDsAzGAYhj4+4NMr271au92r0uqG0GtxdpvOG5mtSwvdmjXGpaHpSSZWipNhGIb+e8t+/eKlHWps9WtQSoJ+ddU4zRjtMru0qEfY6QXCDgCzGYah3ZX1emW7R2u3e/Wpty70ms0mTTptkC4tzNGlhW6dksnclWhR29ymn76wXS992DE757yR2fr1NePldhJewyHmws4jjzyiX/3qV/J4PBozZoxWrFihCy+88IQ+l7ADINKUVjdo7Xav1u7w6sPyI91ey3UmKSstUVmpjm73iWV94Z+zUhLlTE5g1o9JPiir0Q+e/UDlh5sUZ7dp0cwzdeu00xlBEEYxFXaee+453XDDDXrkkUd0/vnn63e/+51+//vf65NPPtGwYcO+8vMJOwAiWcWRplDw2bz3sHrzU7vjVvqE0MWqwUtWgzfSd1zG6gjdWD8oNUGOeHpITkYgYOixNz/X8nW71R4wdOqgZP32ugk6Z9ggs0uznJgKO1OmTNE555yjRx99NPTcWWedpTlz5qi4uPgrP5+wAyBaHG5oVdnhRh1uaNGh+s57xRpaO+8S634Ba11z326lT3N03Urf7Xb6L/5zSqLi41ilOFpjq1/3/s8OvfPZIUnSN87umJ2TkcTsnP4QM7eet7a2asuWLbrnnnu6PT9r1ixt3LjxmJ/T0tKilpauo5+1tbX9WiMAhEswfJyI1vaAahq7ws+hbjfStxzjVvo2+QOG6lvaVd/SrrLD0XUrfSRJTojTvVeO0bcmncrsnAgQ9WGnurpafr9fLlf3rnaXyyWv13vMzykuLta99947EOUBgGkS4+1yZSTJlXFizbCBgKHa5rYvhKLjP2oaW+UPRP3mQNiNPcWpB686W6cPSTO7FHSK+rAT9MXkbBjGcdP04sWLtWjRotCfa2trlZeX16/1AUCks9ttykxJ7JjvM8TsaoDwifqwM3jwYMXFxfVYxamqquqx2hPkcDjkcDgGojwAAGCyqL+JLjExURMnTlRJSUm350tKSjR16lSTqgIAAJEi6ld2JGnRokW64YYbNGnSJJ133nlatWqVysrKdOutt5pdGgAAMJklws4111yjQ4cO6b777pPH41FhYaH+/ve/67TTTjO7NAAAYDJLzNk5WczZAQAg+pzo7++o79kBAAD4MoQdAABgaYQdAABgaYQdAABgaYQdAABgaYQdAABgaYQdAABgaYQdAABgaYQdAABgaZa4LuJkBYdI19bWmlwJAAA4UcHf2191GQRhR1JdXZ0kKS8vz+RKAABAb9XV1cnpdB73de7GkhQIBFRRUaH09HTZbLawfd3a2lrl5eWpvLycO7eOwvvSE+/JsfG+9MR70hPvybHFwvtiGIbq6uqUm5sru/34nTms7Eiy2+069dRT++3rZ2RkWPZftJPB+9IT78mx8b70xHvSE+/JsVn9ffmyFZ0gGpQBAIClEXYAAIClEXb6kcPh0C9+8Qs5HA6zS4kovC898Z4cG+9LT7wnPfGeHBvvSxcalAEAgKWxsgMAACyNsAMAACyNsAMAACyNsAMAACyNsNOPHnnkEY0YMUJJSUmaOHGi3nrrLbNLMk1xcbHOPfdcpaena+jQoZozZ4527dpldlkRpbi4WDabTQsXLjS7FNMdOHBA3/72t5Wdna2UlBSNHz9eW7ZsMbss07S3t+unP/2pRowYoeTkZI0cOVL33XefAoGA2aUNqDfffFOzZ89Wbm6ubDabXnzxxW6vG4ahpUuXKjc3V8nJyZo+fbp27NhhTrED5Mvek7a2Nt19990aO3asUlNTlZubqxtvvFEVFRXmFWwSwk4/ee6557Rw4UItWbJEH3zwgS688EJddtllKisrM7s0U2zYsEELFizQu+++q5KSErW3t2vWrFlqaGgwu7SIsHnzZq1atUpnn3222aWYrqamRueff74SEhL0yiuv6JNPPtFDDz2kzMxMs0szzYMPPqjHHntMK1eu1M6dO7Vs2TL96le/0sMPP2x2aQOqoaFB48aN08qVK4/5+rJly7R8+XKtXLlSmzdvltvt1syZM0P3H1rRl70njY2N2rp1q372s59p69atev7557V7925deeWVJlRqMgP9YvLkycatt97a7bmCggLjnnvuMamiyFJVVWVIMjZs2GB2Kaarq6sz8vPzjZKSEmPatGnGHXfcYXZJprr77ruNCy64wOwyIsoVV1xh3HLLLd2emzt3rvHtb3/bpIrMJ8l44YUXQn8OBAKG2+02HnjggdBzzc3NhtPpNB577DETKhx4X3xPjmXTpk2GJGPfvn0DU1SEYGWnH7S2tmrLli2aNWtWt+dnzZqljRs3mlRVZPH5fJKkrKwskysx34IFC3TFFVdoxowZZpcSEV566SVNmjRJ3/rWtzR06FBNmDBBjz/+uNllmeqCCy7Q66+/rt27d0uSPvzwQ7399tu6/PLLTa4scpSWlsrr9Xb7uetwODRt2jR+7h7F5/PJZrPF3EopF4H2g+rqavn9frlcrm7Pu1wueb1ek6qKHIZhaNGiRbrgggtUWFhodjmmevbZZ7V161Zt3rzZ7FIixp49e/Too49q0aJF+slPfqJNmzbpBz/4gRwOh2688UazyzPF3XffLZ/Pp4KCAsXFxcnv9+v+++/XddddZ3ZpESP4s/VYP3f37dtnRkkRp7m5Wffcc4/mzZtn6YtBj4Ww049sNlu3PxuG0eO5WHTbbbfpo48+0ttvv212KaYqLy/XHXfcoXXr1ikpKcnsciJGIBDQpEmTVFRUJEmaMGGCduzYoUcffTRmw85zzz2np59+WmvWrNGYMWO0bds2LVy4ULm5ubrpppvMLi+i8HP32Nra2nTttdcqEAjokUceMbucAUfY6QeDBw9WXFxcj1WcqqqqHv/VEWtuv/12vfTSS3rzzTd16qmnml2OqbZs2aKqqipNnDgx9Jzf79ebb76plStXqqWlRXFxcSZWaI6cnByNHj2623NnnXWW/vrXv5pUkfl+/OMf65577tG1114rSRo7dqz27dun4uJiwk4nt9stqWOFJycnJ/Q8P3c7gs7VV1+t0tJSvfHGGzG3qiNxGqtfJCYmauLEiSopKen2fElJiaZOnWpSVeYyDEO33Xabnn/+eb3xxhsaMWKE2SWZ7uKLL9bHH3+sbdu2hR6TJk3S9ddfr23btsVk0JGk888/v8dYgt27d+u0004zqSLzNTY2ym7v/uM6Li4u5o6ef5kRI0bI7XZ3+7nb2tqqDRs2xOzPXakr6Pzzn//Ua6+9puzsbLNLMgUrO/1k0aJFuuGGGzRp0iSdd955WrVqlcrKynTrrbeaXZopFixYoDVr1uhvf/ub0tPTQ6teTqdTycnJJldnjvT09B49S6mpqcrOzo7pXqYf/vCHmjp1qoqKinT11Vdr06ZNWrVqlVatWmV2aaaZPXu27r//fg0bNkxjxozRBx98oOXLl+uWW24xu7QBVV9fr88++yz059LSUm3btk1ZWVkaNmyYFi5cqKKiIuXn5ys/P19FRUVKSUnRvHnzTKy6f33Ze5Kbm6urrrpKW7du1csvvyy/3x/62ZuVlaXExESzyh545h4Gs7b//M//NE477TQjMTHROOecc2L6mLWkYz6eeOIJs0uLKBw97/A///M/RmFhoeFwOIyCggJj1apVZpdkqtraWuOOO+4whg0bZiQlJRkjR440lixZYrS0tJhd2oBav379MX+O3HTTTYZhdBw//8UvfmG43W7D4XAYX//6142PP/7Y3KL72Ze9J6Wlpcf92bt+/XqzSx9QNsMwjIEMVwAAAAOJnh0AAGBphB0AAGBphB0AAGBphB0AAGBphB0AAGBphB0AAGBphB0AAGBphB0AAGBphB0AkDR8+HCtWLHC7DIA9APCDoABd/PNN2vOnDmSpOnTp2vhwoUD9r2ffPJJZWZm9nh+8+bN+t73vjdgdQAYOFwECsASWltbT+piwyFDhoSxGgCRhJUdAKa5+eabtWHDBv3mN7+RzWaTzWbT3r17JUmffPKJLr/8cqWlpcnlcumGG25QdXV16HOnT5+u2267TYsWLdLgwYM1c+ZMSdLy5cs1duxYpaamKi8vT/Pnz1d9fb0k6R//+If+7d/+TT6fL/T9li5dKqnnNlZZWZm++c1vKi0tTRkZGbr66qtVWVkZen3p0qUaP368nnrqKQ0fPlxOp1PXXnut6urq+vdNA9BrhB0ApvnNb36j8847T9/97nfl8Xjk8XiUl5cnj8ejadOmafz48Xr//fe1du1aVVZW6uqrr+72+atXr1Z8fLzeeecd/e53v5Mk2e12/fa3v9X27du1evVqvfHGG7rrrrskSVOnTtWKFSuUkZER+n533nlnj7oMw9CcOXN0+PBhbdiwQSUlJfr88891zTXXdPu4zz//XC+++KJefvllvfzyy9qwYYMeeOCBfnq3APQV21gATON0OpWYmKiUlBS53e7Q848++qjOOeccFRUVhZ774x//qLy8PO3evVtnnnmmJOmMM87QsmXLun3No/t/RowYoV/+8pf693//dz3yyCNKTEyU0+mUzWbr9v2+6LXXXtNHH32k0tJS5eXlSZKeeuopjRkzRps3b9a5554rSQoEAnryySeVnp4uSbrhhhv0+uuv6/777z+5NwZAWLGyAyDibNmyRevXr1daWlroUVBQIKljNSVo0qRJPT53/fr1mjlzpk455RSlp6frxhtv1KFDh9TQ0HDC33/nzp3Ky8sLBR1JGj16tDIzM7Vz587Qc8OHDw8FHUnKyclRVVVVr/6uAPofKzsAIk4gENDs2bP14IMP9ngtJycn9M+pqandXtu3b58uv/xy3XrrrfrlL3+prKwsvf322/rOd76jtra2E/7+hmHIZrN95fMJCQndXrfZbAoEAif8fQAMDMIOAFMlJibK7/d3e+6cc87RX//6Vw0fPlzx8Sf+Y+r9999Xe3u7HnroIdntHQvXf/nLX77y+33R6NGjVVZWpvLy8tDqzieffCKfz6ezzjrrhOsBEBnYxgJgquHDh+u9997T3r17VV1drUAgoAULFujw4cO67rrrtGnTJu3Zs0fr1q3TLbfc8qVB5fTTT1d7e7sefvhh7dmzR0899ZQee+yxHt+vvr5er7/+uqqrq9XY2Njj68yYMUNnn322rr/+em3dulWbNm3SjTfeqGnTph1z6wxAZCPsADDVnXfeqbi4OI0ePVpDhgxRWVmZcnNz9c4778jv9+uSSy5RYWGh7rjjDjmdztCKzbGMHz9ey5cv14MPPqjCwkL9+c9/VnFxcbePmTp1qm699VZdc801GjJkSI8GZ6ljO+rFF1/UoEGD9PWvf10zZszQyJEj9dxzz4X97w+g/9kMwzDMLgIAAKC/sLIDAAAsjbADAAAsjbADAAAsjbADAAAsjbADAAAsjbADAAAsjbADAAAsjbADAAAsjbADAAAsjbADAAAsjbADAAAs7f8DZuPK6GmbydUAAAAASUVORK5CYII=", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjsAAAGwCAYAAABPSaTdAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8ekN5oAAAACXBIWXMAAA9hAAAPYQGoP6dpAABVTElEQVR4nO3deXxU5b0/8M+ZNckkM9nIBtlYww7KIuBCCxV3qFrFS90rtxWuIr+6tcW2KCLcqohaqK3XpVertldRqcUiIsq+CYIia0gCIZksJJNtksnM+f0xOSeJsiXMzHPOmc/79crrJZPtmwiTT77P93keSZZlGUREREQGZRJdABEREVE4MewQERGRoTHsEBERkaEx7BAREZGhMewQERGRoTHsEBERkaEx7BAREZGhWUQXoAWBQAClpaVISEiAJEmiyyEiIqJzIMsy6urqkJWVBZPp9P0bhh0ApaWlyM7OFl0GERERdUNJSQl69ep12tcz7ABISEgAEPxmOZ1OwdUQERHRufB4PMjOzlZ/jp8Oww6gLl05nU6GHSIiIp052wgKB5SJiIjI0Bh2iIiIyNAYdoiIiMjQGHaIiIjI0Bh2iIiIyNAYdoiIiMjQGHaIiIjI0Bh2iIiIyNAYdoiIiMjQGHaIiIjI0ISGnc8//xzXXnstsrKyIEkSVqxYob7O5/Ph4YcfxtChQ+FwOJCVlYXbbrsNpaWlnT5GdXU1ZsyYAafTicTERNx9992or6+P8FdCREREWiU07DQ0NGD48OF48cUXv/e6xsZG7Ny5E/PmzcPOnTvx7rvvYv/+/bjuuus6vd2MGTPw9ddfY/Xq1Vi5ciU+//xzzJw5M1JfAhEREWmcJMuyLLoIIHiJ13vvvYdp06ad9m22bduGMWPGoKioCDk5Odi3bx8GDRqEbdu2YdSoUQCAVatW4aqrrsKxY8eQlZV1Tp/b4/HA5XKhtraWF4EaTGNLK6obWkSXIVRinA3xdt75S0TGc64/v3X1DFhbWwtJkpCYmAgA2LRpExITE9WgAwCTJ0+GyWTCli1b8OMf//iUH6e5uRnNzc3qnz0eT1jrJjEq65vxgz98hjpvq+hShIqxmvDxnEuRm+IQXQoRkRC6CTterxcPP/wwbrnlFjW9lZWVIS0trdPbWSwWJCcno6ys7LQfa+HChfj9738f1npJvL3Ha1HnbYUkATZzdM7i+/wBeH0BbDxcxbBDRFFLF2HH5/PhpptugizLWLZs2Xl/vEcffRRz585V/+zxeJCdnX3eH5e0xV0X7N5d0q8HXr9rjOBqxFjwz2/w5y8Ksb+sTnQpRETCaD7sKEGnqKgIn376aac1uYyMDLjd7k5v39raiurqamRkZJz2Y9rtdtjt9rDVTNpQ0RZ20hKi9//1gIzgv5dvy7hUS0TRS9O9fSXoHDx4EJ988glSUlI6vX7cuHGoqanBjh071Mc+/fRTBAIBjB07NtLlksYoYadHFIedgowEAMD+sjpoZC8CEVHECe3s1NfX49ChQ+qfCwsLsWvXLiQnJyMzMxM33ngjdu7ciZUrV8Lv96tzOMnJybDZbBg4cCCuuOIK3HPPPVi+fDl8Ph9mz56N6dOnn/NOLDIud50XQHR3dvqmxcMkAScbfaioa0aaM0Z0SUREESe0s7N9+3aMHDkSI0eOBADMnTsXI0eOxGOPPYbjx4/jgw8+wLFjxzBixAhkZmaqLxs3blQ/xhtvvIGCggJMmjQJV111FS6++GK89NJLor4k0pD2Zazo/QEfYzUjLzU4mPwt53aIKEoJ7exMnDjxjK31c2m7Jycn48033wxlWWQQbi5jAQguZR2paMD+sjpc2r+H6HKIiCJO0zM7ROeDA8pBA9KVIWV2dogoOjHskCHVN7eiscUPgJ2dAcqQcjl3ZBFRdGLYIUNSujoOmxmOKL8qQdmRdbC8Hv4Ad2QRUfRh2CFDcnuCO7GivasDADnJcYi1mtHcGsDRqgbR5RARRRzDDhlSRT13YilMJgn90+MBgCcpE1FUYtghQ3J7uBOrI2Vuh0PKRBSNGHbIkJTODsNOkHJtxH5eG0FEUYhhhwyJnZ3OOl4bQUQUbRh2yJDaZ3YYdoD2Zayi6kY0trQKroaIKLIYdsiQuBurs9R4O1LjbZDl4BZ0IqJowrBDhsR7sb5vAJeyiChKMeyQ4fj8AVQ3tgAA0pzs7Ch4bQQRRSuGHTKcqvoWyDJgNklIjrOJLkczCnhtBBFFKYYdMhxlCSs13gaTSRJcjXZwGYuIohXDDhmOu47DyafSPz0BkgRU1regsm23GhFRNGDYIcPhcPKpxdrMyE2OA8DuDhFFF4YdMhx3W9jpEc/Oznfx2ggiikYMO2Q4ameHO7G+h9dGEFE0Ytghw+HMzunx2ggiikYMO2Q47TM7DDvfpSxjHSivRyAgC66GiCgyGHbIcNSZHYad78lLccBuMaHJ50dxdaPocoiIIoJhhwxFlmXuxjoDs0lCv/R4ABxSJqLowbBDhuLxtqK5NQCAnZ3TUa6N4NwOEUULhh0ylIq24eSEGAtirGbB1WgTr40gomjDsEOG4uZw8lnxrB0iijYMO2QoFRxOPiuls3O0sgFen19wNURE4cewQ4bC4eSz65FgR1KcFQEZOOSuF10OEVHYMeyQoXDb+dlJksSlLCKKKgw7ZCg8UPDcFPDaCCKKIgw7ZCi8KuLcsLNDRNGEYYcMhTM752YA78gioijCsEOGwpmdc9M/PRh23HXNONnQIrgaIqLwYtghw2hu9aOm0QeAMztnE2+3IDs5FgCXsojI+Bh2yDAq64MdCqtZQmKcVXA12td+bQSHlInI2Bh2yDDUAwXj7ZAkSXA12td+bQQ7O0RkbAw7ZBhuD3didQV3ZBFRtGDYIcNoH07mTqxzoXR2DpTVIRCQBVdDRBQ+DDtkGOq2cyc7O+ciL9UBm9mEhhY/jtc0iS6HiChsGHbIMNwdZnbo7KxmE3r3cADgUhYRGRvDDhkGOztdpw4pc0cWERkYww4ZRoVyVQQ7O+dsQNsdWezsEJGRMeyQYbR3djigfK4KeG0EEUUBhh0yBFmWUVHPqyK6Stl+fqSyAc2tfsHVEBGFB8MOGUJNow8+f3D7dGq8TXA1+pHpikFCjAX+gIzD7gbR5RARhQXDDhmCshMrMc4Ku8UsuBr9kCSpw0nKHFImImNi2CFDUOd1uITVZTxJmYiMjmGHDMFdx6siukvZkcUhZSIyKoYdMoT2zg53YnUVd2QRkdEx7JAhtN+Lxc5OV/VPD4adE7Ve1Db6BFdDRBR6DDtkCG7O7HSbK9aKLFewI7a/nN0dIjIeoWHn888/x7XXXousrCxIkoQVK1Z0er0sy3jssceQmZmJ2NhYTJ48GQcPHuz0NtXV1ZgxYwacTicSExNx9913o76+PoJfBWlBBWd2zssAXhtBRAYmNOw0NDRg+PDhePHFF0/5+sWLF2Pp0qVYvnw5tmzZAofDgSlTpsDr9apvM2PGDHz99ddYvXo1Vq5cic8//xwzZ86M1JdAGsFlrPPDayOIyMgsIj/5lVdeiSuvvPKUr5NlGUuWLMFvfvMbTJ06FQDw+uuvIz09HStWrMD06dOxb98+rFq1Ctu2bcOoUaMAAM8//zyuuuoq/OEPf0BWVlbEvhYSiwPK54dDykRkZJqd2SksLERZWRkmT56sPuZyuTB27Fhs2rQJALBp0yYkJiaqQQcAJk+eDJPJhC1btpz2Yzc3N8Pj8XR6If3y+vyo87YCYGenu9RlrPI6yLIsuBoiotDSbNgpKysDAKSnp3d6PD09XX1dWVkZ0tLSOr3eYrEgOTlZfZtTWbhwIVwul/qSnZ0d4uopkpSujt1igjNGaLNSt/r0iIfFJKHO24rSWu/Z34GISEc0G3bC6dFHH0Vtba36UlJSIrokOg8dDxSUJElwNfpks5jQu4cDAIeUich4NBt2MjIyAADl5eWdHi8vL1dfl5GRAbfb3en1ra2tqK6uVt/mVOx2O5xOZ6cX0i9eFREaHFImIqPSbNjJz89HRkYG1qxZoz7m8XiwZcsWjBs3DgAwbtw41NTUYMeOHerbfPrppwgEAhg7dmzEayYxuBMrNDikTERGJXTAob6+HocOHVL/XFhYiF27diE5ORk5OTmYM2cOnnjiCfTr1w/5+fmYN28esrKyMG3aNADAwIEDccUVV+Cee+7B8uXL4fP5MHv2bEyfPp07saIId2KFxoB0hh0iMiahYWf79u34wQ9+oP557ty5AIDbb78dr776Kh566CE0NDRg5syZqKmpwcUXX4xVq1YhJqb9h9obb7yB2bNnY9KkSTCZTLjhhhuwdOnSiH8tJI7bw85OKCg7sg5X1MPnD8Bq1mzjl4ioS4SGnYkTJ55xm6skSZg/fz7mz59/2rdJTk7Gm2++GY7ySCcq6jmzEwq9kmIRb7egvrkVRyoa1PBDRKR3/NWNdM/NqyJCQpIk9E+PBwB8yx1ZRGQgDDuke8oyFmd2zp+yI4tzO0RkJAw7pGv+gIyqhhYAQJqTnZ3zxR1ZRGREDDuka9UNLfAHZEgSkOKwiS5H95Q5HZ61Q0RGwrBDuqZsO09x2GDh7qHzpnR2jtc0oc7rE1wNEVFo8KcD6ZoynJwazyWsUEiMsyG9bTnwQDm7O0RkDAw7pGvqgYJODieHCq+NICKjYdghXVOvimBnJ2Q4pExERsOwQ7rW3tlh2AkVXhtBREbDsEO6VsHOTsgpO7L2l9ed8YRzIiK9YNghXWNnJ/T6psXDbJJQ0+hTlwmJiPSMYYd0Tb0qgp2dkImxmpGXEgeAQ8pEZAwMO6Rr3I0VHgXqtRG8I4uI9I9hh3SrobkVDS1+ALwENNR4kjIRGQnDDumWMk8SZzMj3m4RXI2xDOD2cyIyEIYd0i11CYtdnZBTzto56K5Hqz8guBoiovPDsEO6pQ4nM+yEXHZSHOJsZrS0BnC0qlF0OURE54Vhh3SrvbPD4eRQM5kk9OPhgkRkEAw7pFvqVRHs7IRFgRp2uCOLiPSNYYd0q4JhJ6y4I4uIjIJhh3SLnZ3wKuhwbQQRkZ4x7JBucTdWeCmdneLqRjS2tAquhoio+xh2SLcquBsrrFLi7UiNt0OWgQPl9aLLISLqNoYd0qVWfwBVDS0AuBsrnNSlLA4pE5GOMeyQLlU1tECWAZMEJDtsossxLA4pE5ERMOyQLrk9wXmd1Hg7zCZJcDXGxWsjiMgIGHZIlyrqOa8TCQUMO0RkAAw7pEtKZ4c7scKrX1oCJCm4bKjsfiMi0huGHdIlXhURGbE2M/JSHADY3SEi/WLYIV3igYKRMyBdGVLmjiwi0ieGHdIltbPjZNgJNw4pE5HeMeyQLrmVAwXjGXbCjddGEJHeMeyQLlXUs7MTKUpn50B5HfwBWXA1RERdx7BDuiPLsrobq0c8B5TDLTfFgRirCV5fAMXVjaLLISLqMoYd0p265lY0twYAcEA5EswmCf3SeG0EEekXww7pjtLVSbBbEGszC64mOvDaCCLSM4Yd0h1lJ1YPzutEDE9SJiI9Y9gJo5LqRqw/WImmFr/oUgyFO7Eij9vPiUjPGHbC6PplG/HTl7fgoJs/IEKp/YwdDidHihJ2jlY1wOtjeCcifWHYCaO8lDgAwNEq7mAJJXUZi52diOkRb0eyw4aADBwsrxddDhFRlzDshFFu251CRZUNgisxFjdPT444SZJ4bQQR6RbDThixsxMe7ZeAMuxEEud2iEivGHbCSO3sVLGzE0rqgDLDTkTx2ggi0iuGnTDKaws77OyEVntnhwPKkcSzdohIrxh2wiinbRmrsr4Z9c2tgqsxhpbWAE42+gCwsxNp/dtmdirqmlHd0CK4GiKic8ewE0auWCuSHTYAXMoKlcq2C0CtZgmJsVbB1UQXh92CnORggOeQMhHpCcNOmOW2dXeKuJQVEspOrNR4O0wmSXA10YdDykSkRww7YdY+t8POTihwJ5ZYvDaCiPSIYSfM1M5OJTs7ocCdWGJxSJmI9IhhJ8zY2Qkt9fRk7sQSQunsHCivQyAgC66GiOjcaDrs+P1+zJs3D/n5+YiNjUWfPn3w+OOPQ5bbn2RlWcZjjz2GzMxMxMbGYvLkyTh48KDAqjvjzE5oudWww86OCHkpDtjMJjS2+HHsZJPocoiIzommw86iRYuwbNkyvPDCC9i3bx8WLVqExYsX4/nnn1ffZvHixVi6dCmWL1+OLVu2wOFwYMqUKfB6vQIrb6d0dso8Xt5+HgJuD2d2RLKYTeiTFg+AO7KISD80HXY2btyIqVOn4uqrr0ZeXh5uvPFGXH755di6dSuAYFdnyZIl+M1vfoOpU6di2LBheP3111FaWooVK1aILb5NYpwVzhgLAKC4mt2d81VRz7AjGoeUiUhvNB12xo8fjzVr1uDAgQMAgN27d2P9+vW48sorAQCFhYUoKyvD5MmT1fdxuVwYO3YsNm3adNqP29zcDI/H0+klXCRJQl4q53ZCpcLDAWXR1CFlXhtBRDphEV3AmTzyyCPweDwoKCiA2WyG3+/HggULMGPGDABAWVkZACA9Pb3T+6Wnp6uvO5WFCxfi97//ffgK/47cFAe+OlbLgwXPkyzL7Z0dJweUReFZO0SkN5ru7Lzzzjt444038Oabb2Lnzp147bXX8Ic//AGvvfbaeX3cRx99FLW1tepLSUlJiCo+Nd5+Hho1jT74/MHh9NR4m+BqopeyjFVY2YDmVs6hEZH2abqz8+CDD+KRRx7B9OnTAQBDhw5FUVERFi5ciNtvvx0ZGRkAgPLycmRmZqrvV15ejhEjRpz249rtdtjtkVsG4e3noaF0dRLjrLBbzIKriV4Zzhg4YyzweFtxyF2PwVku0SUREZ2Rpjs7jY2NMJk6l2g2mxEIBAAA+fn5yMjIwJo1a9TXezwebNmyBePGjYtorWeidnZ4sOB5UXZi9YjnvI5IkiShIMMJgEtZRKQPmu7sXHvttViwYAFycnIwePBgfPnll3jmmWdw1113AQg+6c6ZMwdPPPEE+vXrh/z8fMybNw9ZWVmYNm2a2OI7UDo7pbVNaG71syvRTRX1weHkNCfDjmgDMhKw9Wg1ww4R6YKmw87zzz+PefPm4d5774Xb7UZWVhb+8z//E4899pj6Ng899BAaGhowc+ZM1NTU4OKLL8aqVasQE6OdAdbUeBscNjMaWvwoqW5C37ZzSqhr2NnRDl4bQUR6oumwk5CQgCVLlmDJkiWnfRtJkjB//nzMnz8/coV1kSRJyE1x4JsTHhRVNTDsdJN6CSh3YgnHs3aISE80PbNjJHmp3JF1vtSrItjZEa5/W9gp83hR2+gTXA0R0Zkx7EQId2Sdv/bODsOOaM4YK3omxgLgtRFEpH0MOxHCs3bOn7uu7fRkdnY0QT1ckCcpE5HGMexECDs758/Nzo6mcEiZiPSCYSdClNvPj51sgs8fEFyN/nh9ftR5WwEAPRI4oKwFHFImIr1g2ImQtAQ7Yqwm+AMyjp9sEl2O7ijzOjaLSb1FnsRSOjsHyuogy7LgaoiITo9hJ0JMJgm5ybz9vLvUJawEOyRJElwNAUDv1HhYTBLqmltxvIYBnoi0i2EngnLbhpSLOKTcZRXKcHIC53W0wmYxoU+P4JlRXMoiIi1j2ImgvFR2drqrokNnh7SDQ8pEpAcMOxHEzk73qQcKMuxoygAOKRORDjDsRJCyI4udna5r7+xwJ5aWcEcWEekBw04EKZ2dkupG+APcvdIV7Oxok9LZOVxRj5ZWHqlARNrEsBNBma5Y2Mwm+PwySrl7pUs4s6NNPRNjkWC3oDUg40hlvehyiIhOiWEngswmCdnJwfuEOLfTNW7uxtIkSZLUS0G5lEVEWsWwE2Gc2+m6QEBGZX0LAM7saBF3ZBGR1jHsRBjvyOq66sYW+AMyJAlIibeJLoe+g0PKRKR1DDsRlpfK28+7yu0Jzuskx9lgNfOvrNYMSGfYISJt40+OCGNnp+sq6rkTS8sKMpwAgOM1TfB4fYKrISL6PoadCMvrcLBggNvPz4nbw+FkLXPFWZHhDM5SHWB3h4g0iGEnwnomxsJiktDcGkB52w4jOjOls8PhZO3ikDIRaRnDToRZzCb0SuL2865QZnbY2dEuDikTkZYx7AjAuZ2uae/sMOxoFe/IIiItY9gRQJnb4Y6sc1PBzo7mtS9jeSDLnEUjIm1h2BGAnZ2uYWdH+/qmxcNskuDxtqLMw1k0ItIWhh0B1LN2KtnZORfcjaV9dosZ+anBEM8hZSLSGoYdATp2dtjyP7OG5lY0tPgBAGlO7sbSMs7tEJFWMewI0CspFiYJaGjxq3c+0akpt53HWs1w2MyCq6EzKeBJykSkUQw7AtgtZmQlKtvPObdzJuq8jtMOSZIEV0NnwrN2iEiruhV2SkpKcOzYMfXPW7duxZw5c/DSSy+FrDCja7/9nHM7Z6KesRPPeR2tU66NOOyuh88fEFwNEVG7boWd//iP/8DatWsBAGVlZfjRj36ErVu34te//jXmz58f0gKNKle9NoKdnTNxt50yneZk2NG6XkmxiLOZ0eIP4Ggl/14TkXZ0K+zs3bsXY8aMAQC88847GDJkCDZu3Ig33ngDr776aijrMyx2ds6NMrPDqyK0z2SS0D+dS1lEpD3dCjs+nw92e/A37U8++QTXXXcdAKCgoAAnTpwIXXUGxs7OuXHX8UBBPeG1EUSkRd0KO4MHD8by5cvxxRdfYPXq1bjiiisAAKWlpUhJSQlpgUaV13YmSWElt5+fSQXDjq5wSJmItKhbYWfRokX405/+hIkTJ+KWW27B8OHDAQAffPCBurxFZ5aTHOzs1HlbUdPoE1yNdrGzoy/qWTvlHsGVEBG1s3TnnSZOnIjKykp4PB4kJSWpj8+cORNxcXEhK87IYqxmZLpicKLWi6NVDUhy2ESXpEntMzsMO3qg7MgqqW5CfXMr4u3deoohIgqpbnV2mpqa0NzcrAadoqIiLFmyBPv370daWlpICzSy9rkdDimfSqs/gKoGdnb0JNlhU/9fHSjnUhYRaUO3ws7UqVPx+uuvAwBqamowduxYPP3005g2bRqWLVsW0gKNrH1HFoeUT6W6oQWyDJgkIMXBsKMXHFImIq3pVtjZuXMnLrnkEgDAP/7xD6Snp6OoqAivv/46li5dGtICjaz9jix2dk5FmddJibfDbOLpyXoxgNdGEJHGdCvsNDY2IiEh+IT273//G9dffz1MJhMuuugiFBUVhbRAI8trW8ZiZ+fUOK+jT+07sjikTETa0K2w07dvX6xYsQIlJSX4+OOPcfnllwMA3G43nE5nSAs0MnZ2zkw5PZnzOvqiDCnvL6vjsQpEpAndCjuPPfYYfvnLXyIvLw9jxozBuHHjAAS7PCNHjgxpgUamDChXN7Sgtonbz7+LnR196pceD5MEnGz0qf8PiYhE6lbYufHGG1FcXIzt27fj448/Vh+fNGkSnn322ZAVZ3QOu0XtWhSzu/M9PGNHn2KsZnX4nocLEpEWdCvsAEBGRgZGjhyJ0tJS9Qb0MWPGoKCgIGTFRQPO7ZyecuM578XSnwHckUVEGtKtsBMIBDB//ny4XC7k5uYiNzcXiYmJePzxxxEIBEJdo6G1z+0w7HxXRT2XsfSK10YQkZZ063jTX//613j55Zfx1FNPYcKECQCA9evX43e/+x28Xi8WLFgQ0iKNrL2zw2Ws7+KAsn4V8NoIItKQboWd1157DX/5y1/U284BYNiwYejZsyfuvfdehp0uYGfn1GRZ7jCgzGUsvRnQtiPrYHk9/AGZ5yQRkVDdWsaqrq4+5WxOQUEBqqurz7uoaNJ+ijI7Ox3VNbfC6wsuibKzoz85yXGIsZrQ3BrgPBoRCdetsDN8+HC88MIL33v8hRdewLBhw867qGiS07aMVVHXjIbmVsHVaIfS1UmwWxBrMwuuhrrKbJLQnycpE5FGdGsZa/Hixbj66qvxySefqGfsbNq0CSUlJfjoo49CWqDRuWKtSHbYUN3QgqKqRgzK4qGMQPtOLHZ19GtAegK+OlaLb8vqcNXQTNHlEFEU61Zn57LLLsOBAwfw4x//GDU1NaipqcH111+Pr7/+Gn/9619DXaPhtd9+zna/QtmJxbCjX+3bzzmkTERidauzAwBZWVnfG0TevXs3Xn75Zbz00kvnXVg0yUtx4MviGs7tdOD2cCeW3nW8NoKISKRuHyoYKcePH8dPf/pTpKSkIDY2FkOHDsX27dvV18uyjMceewyZmZmIjY3F5MmTcfDgQYEVdx07O9/XfsYOd2LpldLZKapuRGML59GISBxNh52TJ09iwoQJsFqt+Ne//oVvvvkGTz/9NJKSktS3Wbx4MZYuXYrly5djy5YtcDgcmDJlCrxer8DKu6Z9RxbDjqKCMzu61yPBjhSHDbIc3IJORCRKt5exImHRokXIzs7GK6+8oj6Wn5+v/rcsy1iyZAl+85vfYOrUqQCA119/Henp6VixYgWmT59+yo/b3NyM5ub2Cwo9HrEzBe2dHS5jKdy8BNQQBmQkYOPhKuwvq8Pw7ETR5RBRlOpS2Ln++uvP+PqamprzqeV7PvjgA0yZMgU/+clPsG7dOvXQwnvuuQcAUFhYiLKyMkyePFl9H5fLhbFjx2LTpk2nDTsLFy7E73//+5DWej6Uzs6JWi+8Pj9irNxqXcFLQA1haC8XNh6uwucHK3DT6GzR5RBRlOrSMpbL5TrjS25uLm677baQFXfkyBEsW7YM/fr1w8cff4xf/OIXuO+++/Daa68BAMrKygAA6enpnd4vPT1dfd2pPProo6itrVVfSkpKQlZzdyTGWeGMCebO4mp2d4D2qyLSnAw7enZ125bzT/aVo57nSBGRIF3q7HRcToqEQCCAUaNG4cknnwQAjBw5Env37sXy5ctx++23d/vj2u122O3a+SEqSRLyUh346lgtjlY2qIexRauW1gBONvoAcEBZ74b2dCE/1YHCygas/qYMPx7ZS3RJRBSFND2gnJmZiUGDBnV6bODAgSguLgYAZGRkAADKy8s7vU15ebn6Or1ovyOLnZ3Ktp1YFpOExFir4GrofEiShOuGZwEA3t9VKrgaIopWmg47EyZMwP79+zs9duDAAeTm5gIIDitnZGRgzZo16us9Hg+2bNminuysF+23n3NHVsd5HRMvkNS960YEw84XBytRVd98lrcmIgo9TYedBx54AJs3b8aTTz6JQ4cO4c0338RLL72EWbNmAQj+1jhnzhw88cQT+OCDD7Bnzx7cdtttyMrKwrRp08QW30Xs7LRzczjZUPr0iMfQni74AzI+2nNCdDlEFIU0HXZGjx6N9957D3/7298wZMgQPP7441iyZAlmzJihvs1DDz2E//qv/8LMmTMxevRo1NfXY9WqVYiJ0desBzs77Sq47dxwpo7gUhYRiaPpc3YA4JprrsE111xz2tdLkoT58+dj/vz5Eawq9JTOTmlNE5pb/bBbonf7ubITi50d47hmWBYWfLQP24tOoqS6EdnJcaJLIqIoounOTjRJjbfBYTMjIAPHTjaJLkeo9pkdfXXn6PQyXDG4KD8FAPDhV+zuEFFkMexohCRJHeZ2onspizM7xqQsZX3ApSwiijCGHQ3JS22b26mM7iFlzuwY05VDMmE1S/i2rA7flom9ooWIogvDjoawsxPEqyKMyRVnxcQBaQDY3SGiyGLY0ZD2HVnR29mRZZmdHQNTl7J2l0KWZcHVEFG0YNjREHZ2gNomH1r8AQBAajzDjtFMKkiHw2bGsZNN2FlcI7ocIooSDDsaotx+fuxkE3xtP/CjjTKc7Iq18vZ3A4q1mTFlcPAqlw92HRdcDVF0WPHlcWw8VCm6DKEYdjQkLcGOGKsJrQEZpTXRuf2cS1jGp1wfsfKrE2iN0lBPFCn7Tngw5+1duOf17Whu9YsuRxiGHQ0xmSTkJge7O9E6t8MDBY1vQt9UpDhsqGpowYbDVaLLITK0Lw5WAAAaWvz4MoqXjhl2NCanbUg5Wud22NkxPqvZhKuHZQIA3udSFlFYrT/U/gvFhiheymLY0Rh1R1aUnrXj9nDbeTRQdmV9vLcMXl/0ttaJwqm51Y9thdXqnxl2SDOifUdWRb3S2eFVEUZ2QU4SeiXFoqHFjzX73KLLITKkL4tr0OTzw2ELbvbYfawWdV6f4KrEYNjRGGVHVrTefs7OTnSQJAnXDVduQudSFlE4KDuwJg1MR15KHPwBGVuOVJ/lvYyJYUdjctuWsUqqm+APRN+ha+2dHYYdo5s6oicA4LP9FahtjM7fNonCaX1b2Lm4byom9E3t9Fi0YdjRmKzEWFjNElr8AZyojb7t524Pd2NFiwEZCSjISECLP4BVX58QXQ6RodR5fdh9rBYAML5vihp2Nh5m2CENMJskZCcrO7Kia0jZ6/PD420FwJmdaKGcufM+78oiCqktR6rhD8jITYlDr6Q4jOudAkkCDpTXq79URhOGHQ2K1rkdZdu5zWKCM9YiuBqKhGuHBcPOpiNVKI/CJ2CicNnQ1sFROjpJDhsGZzkBABuj8Hwrhh0Nyk2Jzs6OclVEj3g7JEkSXA1FQnZyHEblJkGWgQ93s7tDFCrKNvMJfVLVx5T/jsa5HYYdDVI7O5XR2dnhvE506XgTOhGdP3edFwfK6yFJwLg+Kerj6tzOoUrIcnRtgGHY0aBo7exUtF0VwZ1Y0eWqoZkwmyR8dawWRyrqRZdDpHsb205NHpTpRLLDpj4+Oi8ZNrMJpbVeFEbZL9MMOxqkdHaKqhsQiKLt5+pVEU6GnWiSEm/HJf2Cv3Gyu0N0/jZ02HLeUazNjAtyE4NvE2VzOww7GtQzKRZmkwSvL6DOsUSD9pkd7sSKNupS1q7SqGuvE4WSLMtq2Bn/nbADtM/tbDgYXXM7DDsaZDWb0CspFkB07chiZyd6/WhQBmKsJhypbMDe4x7R5RDp1tGqRpTWemEzmzA6L+l7r5/Q1kXddKQqqg6uZdjRqGi8I6vjbiyKLvF2CyYPTAfA6yOIzofS1RmZk4g42/eP8BjW04UEuwW1TT58XVob6fKEYdjRKPX28ygaUmZnJ7op10d8+FVpVP3GSRRKp5vXUVjMJoztndL2ttEzt8Owo1HR1tkJBGRU1nPreTS7rH8PuGKtKPc0Y0th9DwJE4WKPyBj05Hgv51TzesoJvQNhp1oujqCYUej1M5OZXR0dk42tqC17bf5VC5jRSWbxYQrh2QACA4qE1HXfFPqQU2jD/F2C4b3cp327ZSuz9bCanh9/kiVJxTDjkZ17OxEw+4UZV4n2WGD1cy/ltFKuSvroz0n0NwaHU/CRKGiXBFxUe9kWM7wPNo3LR5pCXY0twaws/hkpMoTij9VNCo7ORaSBDS0+FFZ3yK6nLBT53W4hBXVxuanIN1ph8fbinX7K0SXQ6Qr6hURZ1jCAgBJktS32RAlV0cw7GiU3WJGliu4/Twa5nbcvCqCAJhNkno56Ps8YJDonHl9fmw7Wg3g7GEHAMb3ia4hZYYdDctLjZ4dWe62qyIYdkjZlfXJN+Wob24VXA2RPuwsPgmvL4AeCXb0S4s/69srgeirYzWobfKFuzzhGHY0LJp2ZPESUFIM6elE71QHmlsD+PfXZaLLIdIF5T6sCX1SIEnSWd8+KzEWvVMdCMjAliPG7+4w7GhYNJ2141ZndnhVRLSTJEkdVH6fu7KIzsn6c5zX6Sia5nYYdjQsGjs7HFAmALhueDDsrD9UqZ6/RESn5vH68NWxGgBdDTttcztRcCkow46GKbefF1Yaf/s5l7Goo9494jGslwv+gIyP9pwQXQ6Rpm0+XIWADPROdSArMfac329c71RIEnDIXY+yWm8YKxSPYUfDcpKDy1h13lbUNBp7gIydHfoupbvDpSyiM9t4WDk1OaVL7+eKs2JoT1fbxzD2UhbDjobF2szIcAZnWIx8+3ljS6u664adHVJcOzwLkgTsKDqJkmrjz60Rddf6s9yHdSbKstd6g8/tMOxoXG7bkHKRgYeUla5OrNWMePv3b+ml6JTujMG4tgsLP+CZO0SnVO7x4pC7HpIEXNS7a50dAJjQJxh2Nh6qMvS4BMOOxilzO0bu7HQ8UPBctkxS9JjatiuLd2URnZqyk2poTxcS42xdfv9ReUmwWUwo83hxuMK4P2cYdjQuNzV6Ojuc16HvumJwJmxmE/aX1+HbMo/ocog0RzkBeXyfri9hAUCM1YxRuUkAjD23w7CjcVHR2fHw9GQ6NVecFRMH9ADAQWWi75JlucN9WF1fwlKoczsHGXZIkKiY2alnZ4dOT7k+4oNdpQgEjDtTQNRVRyobUObxwmYxYXRecrc/jhJ2Nh2pgt+g/8YYdjROOViwuqHFsPeXuD08Y4dOb9LANDhsZhyvacLO4pOiyyHSDKWrc2FOEmKs5m5/nKE9XUiIsaDO24o9x2tDVZ6mMOxoXLzdgtT4YAgoNmh3h1dF0JnEWM2YMiQDAJeyiDpSws7F/bo3r6MwmyR156NRr45g2NGB9juyjDm3w9OT6WyUpayP9pyAzx8QXA2ReP6AjE3KYYJ9uj+vo1ACE8MOCWP0O7LcDDt0FhP6pCDFYUNVQ4thn4yJumLv8Vp4vK1IiLGopyCfD2U31/aik/D6/Of98bSGYUcHjHz7uT8go7qhbRnLybBDp2Yxm3DNsEwAPHOHCAA2tG0Tv6h3Cizm8/9R3qeHAxnOGLS0BrD9qPFm4xh2dCA31bidnar6ZgRkwCQBKQ6GHTq969qWsj7+ugxNLcb7zZOoKzacxxURpyJJknq31gYDnrfDsKMDRu7sKEtYKfF2mE08PZlO74KcRPRKikVDix9rvi0XXQ6RMF6fH9vaui/nc77OdynByYhLxQw7OpCbHOzsVNQ1o6HtwkyjUIeT49nVoTOTJEm9PoK7siia7Sg6iZbWANKddvTpER+yj6uct7PneC1qG4111Imuws5TTz0FSZIwZ84c9TGv14tZs2YhJSUF8fHxuOGGG1Bebqzf+lxxViTFWQEY73BB9aoIzuvQOVB2ZX223224J2Oic6WemtwnNaT3CaY7Y9A3LR6yDGw6Yqzujm7CzrZt2/CnP/0Jw4YN6/T4Aw88gA8//BB///vfsW7dOpSWluL6668XVGX4GHVHlruu7aoIdnboHPRPT0BBRgJ8fhn/2ntCdDlEQrRfERGaeZ2OJvRRztupCvnHFkkXYae+vh4zZszAn//8ZyQlJamP19bW4uWXX8YzzzyDH/7wh7jwwgvxyiuvYOPGjdi8efNpP15zczM8Hk+nF60z6twOOzvUVUp3h0tZFI1qG33qKcdhCTsGndvRRdiZNWsWrr76akyePLnT4zt27IDP5+v0eEFBAXJycrBp06bTfryFCxfC5XKpL9nZ2WGrPVSM29nhzA51zbXDg1vQNxdWoazWK7gaosjadKQKAbltq7gr9KfOj+2dApMUvHfrRG1TyD++KJoPO2+99RZ27tyJhQsXfu91ZWVlsNlsSExM7PR4eno6ysrKTvsxH330UdTW1qovJSUloS475PJSjXmKcntnh1dF0LnplRSH0XlJkGVg5Vfs7lB02Xg4fEtYAOCKtWJYr0QAxlrK0nTYKSkpwf3334833ngDMTGh+2Fot9vhdDo7vWhde2fHWMtYPD2ZuuM6LmVRlFofxnkdhbKd3UhLWZoOOzt27IDb7cYFF1wAi8UCi8WCdevWYenSpbBYLEhPT0dLSwtqamo6vV95eTkyMjLEFB0meW1h50St1zBHecuyrA4opzHsUBdcPTQTFpOEPcdrcbiiXnQ5RBFxorYJRyoaYJKCJyeHS8e5HVmWw/Z5IknTYWfSpEnYs2cPdu3apb6MGjUKM2bMUP/barVizZo16vvs378fxcXFGDdunMDKQy8pzoqEGAsAoLjaGN2d+uZWeH3BSx3Z2aGuSHbYcEnbxYW8PoKihbKsNLRXIlyx1rB9ngtykmC3mOCua8YhtzF+mdB02ElISMCQIUM6vTgcDqSkpGDIkCFwuVy4++67MXfuXKxduxY7duzAnXfeiXHjxuGiiy4SXX5ISZKkdneOVhpjbkdZwoq3WxBnswiuhvRG2ZX1we5Sw/z2SXQmG9XzdcLX1QGAGKsZY/KTARhnKUvTYedcPPvss7jmmmtwww034NJLL0VGRgbeffdd0WWFRW7b9nOjzO2ow8ns6lA3/GhQOmKsJhRWNqhbcYmMSpZldV4nVPdhnYlyC/p6gwwp6+7X6c8++6zTn2NiYvDiiy/ixRdfFFNQBKmdHYPsyFI6O6kMO9QNDrsFPxqUgQ93l+L9XaXqDhIiIzpcUQ93XTPsFhMuyE06+zucp4v7pmIRgC1HqtDqD4TkZnWR9F19lGFnh6izqcODd2V9uLsU/gCXssi41h8MdnVG5yUjxmoO++cblOWEK9aKuuZWfGWAzinDjo7kpRqts9N2VQTDDnXTpf17wBVrhbuuGVuOGKPdTnQqGw4H/36PD+Et52diNkkY3zYbtNEAczsMOzqidHZKa5rQ3Kr/7eftnR0eKEjdY7OYcNXQ4InKPHOHjKrVH8DmtrATiXkdxfi+ytwOww5FUI94O+JsZgRk4NhJ/R/jXcEDBSkEpo4ILmV9tPeEIX4JIPquPcdrUdfcCmeMBYOzXBH7vMqur51FNWhq0fe/LYYdHZEkyVB3ZHFmh0JhTF4yMpwxqPO24rP9FaLLIQo5Zfv3+D6pMJukiH3e/FQHslwxaPEHsO1odcQ+bzgw7OiMevt5pf6HlHlVBIWCySThurbuDg8YJCNSDhOcEKF5HYUkSepS1obD+l7KYtjRGaN0dnz+AKobWgCws0Pn77q2XVmf7CtHndcnuBqi0Glq8WNH0UkA7TM0kXRxh6sj9IxhR2fUzo7Ot59X1ge7OhaThKQ4m+BqSO8GZznRp4cDza0B/PvrctHlEIXM9qJqtPgDyHTFoHfbjtxIUnZkfV3qwcm2X1D1iGFHZ4zS2XF72g4UjLfDFME1aDImSZLU6yPe382lLDKO9R3mdSQp8s+Vac4Y9E+PhywDm3R8vAPDjs7kpQY7O8dONsHnDwiupvvU4WQnl7AoNJSlrA2HKtW/X0R6t7FtXufifpGd1+lIuTpCz0tZDDs6k54QA7vFhNaAjNIa/W4/V4eT4xl2KDTyUh0Ynp0If0DGR3tOiC6H6LzVNLZgb2nw9GIlcIhghLkdhh2dMZkkQ1wbwc4OhYNyfcT7u44LroTo/G06XAVZBvqlxSPdKe7w1bG9k2E2STha1YhjJ/X5c4dhR4eMMLejXhXBzg6F0DXDMmGSgJ3FNSjW8S8DRED7du8JAnZhdZQQY8XwXsHDDDfq9BZ0hh0dMsKOLPX0ZIG/rZDxpDlj1Hb/h19xUJn0rf18HbFhp2MNej1vh2FHh4zR2eHMDoWHcsDgii+PQ5Z5Ezrp0/GaJhRWNsAkBZeRRFPDzqEqXf67YtjRobwU5fZz/Xd2OLNDoXbFkAzYLCYcdNfj27I60eUQdYsyDDw8OxHOGKvgaoCROYmIsZpQWd+MA+X1osvpMoYdHVIGlIurGuEP6C9hy7LcvozFzg6FmDPGih8OSAPAm9BJvza2hZ0JAndhdWS3mDEmP7j9XY+3oDPs6FBWYiysZgkt/gDKPF7R5XSZp6kVLW1nBPFeLAoH5Sb0D3eXIqDDXwgousmyjA2HtTOvo1BuQd/IsEORYDZJyE5u235eqb+5HWUnljPGghirWXA1ZEQ/KEhDgt2C4zVN2FF8UnQ5RF1y0F2PirpmxFhNuCA3UXQ5KiV4bT5SpbtDbRl2dErPczvt8zrciUXhEWM1Y8qQDAA8c4f0Z/3BYOdkdF4y7Bbt/EI4KNOJpDgrGlr8+OpYjehyuoRhR6faDxbUY2eH8zoUfspS1j+/OqG730Ipum3UyPk632UySR2ujtDXeTsW0QVQ97R3dvQYdoLLWNyJReE0rncKUuPtqKxvxlvbSjC0p0tYLX16OJCggR01pH2t/gA2H6kG0H5Ng5aM75uCf+45gfWHKnHfpH6iyzlnDDs6pecrI9RlLA4nUxhZzCZcMywTr248inkr9gqtpXcPBz6cfTEcdj7l0pntPlaL+uZWJMZZMSjTKbqc71EC2JfFJ9HY0oo4mz7+TuujSvqejp0dWZYhSZLgis6duozFsENhdueEPGw7Wo3aJp+wGqobWnCkogFP/PMbLLx+mLA6SB+U83XG90mByaS95/Wc5Dj0TIzF8ZombC2sxsS2Yx60jmFHp3omxcJskuD1BeCuaxZ6SVxXtXd29FMz6VNuigP/vO8SoTVsOlyF//jLZvxtawl+MCANlw/OEFoPaVt72NHeEhYASJKEi/um4u3tJdh4uEo3YYcDyjplNZvQKykWAHBUZ9vP2dmhaDKuTwpmXtobAPDIu3vUmTWi72psacXOtqMStDivoxjft+1wwYP6OW+HYUfH2u/I0tfcDmd2KNrM/VF/DMx0orqhBQ/94ytd3i1E4bft6En4/DJ6Jsaqc5lapHSdvjnhQXVDi+Bqzg3Djo61336un86O1+dX5yfY2aFoYbeY8dz0EbBZTPhsfwX+d3OR6JJIg5QlrAl9UzQ9h9kjwY6CjAQA7dvktY5hR8f02NmprA92dWxmE1yx3IpL0aN/egIevbIAAPDEP/fhkJuXlFJn7WFHu0tYio63oOsBw46O6bGz03FeR8u/uRCFw+3j8nBJv1Q0twYw5+1daGnlYYcUVN3Qgq9LPQC0O5zc0YS2uZ0NOrkni2FHxzp2dvQyA1DB4WSKYiaThD/8ZDgS46zYe9yDJZ8cEF0SacSmtos/B6Qn6OL5cUx+CiwmCcXVjSip1v7qAsOOjmUnx0KSgPrmVlTpZEiMO7Eo2qU7Y/DU9UMBAMvWHcbWwmrBFZEWrNfREhYAxNstGJGdCEAf3R2GHR2zW8zIcgW3n+vljqwKT9tVEQw7FMWuGJKJn1zYC7IMPPD2Lni84g49JG1ovw8rRXAl506d2zms/bkdhh2dy0ttm9up1H4bEQAq6tnZIQKA3143GDnJcThe04Tfvf+16HJIoJLqRhRVNcJskjAmP1l0OedMCTsbD1UiEND2KAXDjs61z+3oo7Pj9vD0ZCIguAzw7M3DYZKAd788jpVflYouiQRRujojshN1dWHsiOxExNnMqGpowf5ybe8uZNjRufYdWfrq7HAZiwi4MDcZs3/QFwDw6/f24kRtk+CKSARl+/aEPvpZwgIAm8WkdqK0PrfDsKNzeu3scBmLKOi/JvXD8F4u1Db58Mu/79b8cgCFlizLHeZ19DGc3NHF6nk7DDsURu23n2u/sxMIyOqhgmlOhh0iIHjP3bM3j0Cs1YwNh6rwPxsKRZdEEbS/vA6V9S2ItZoxMidJdDldppwJtKWwWtPnRjHs6FxOcnAZq7bJh5pGbW8/P9nYgta231pTHAw7RIrePeIx75pBAIDFq/bj2zKP4IooUpTLNMfkJ8Nm0d+P5IKMBKQ4bGhs8WP3sRrR5ZyW/r6z1EmszYwMZ3DYV+vdHWVeJ9lh0+U/aqJwumVMNiYPTEOLP4A5b+2C1+cXXRJFwMa2bdt62nLekckkYVwf7d+Czp84BqDcjqv1uR11XieeXR2i75IkCU/dMAyp8TZ8W1aHP3y8X3RJFGY+fwBbjihhR3/zOgplbkfLl4Iy7BiAOrej8bN2lKsiOK9DdGqp8XYsumEYAOAv6ws1P/RJ52d3SQ0aWvxIdtgwMMMpupxuU4Lal8U1aGhuFVzNqTHsGEBuqk46O3Xs7BCdzaSB6ZgxNgcA8P/e2a35WTzqPuWKiHF9UmAy6fdi5OzkOOQkx6E1IGv2+hOGHQNo35Gl7bCjXgLKzg7RGf366oHonepAmceLX7+3VzcX/VLXbFTP19HvEpZCmTlar9FuJMOOAbTP7Gh7GctdF7wXi50dojOLs1mwZPoIWEwS/rnnBN778rjokijEGppbsbP4JID2mRc9m6Dx83YYdgxAOViwqqFF0xcKutWZHV4VQXQ2w3olYs7kfgCAx97/GiXV2v5lhrpm69FqtAZk9EqKRU7bL6x6Nq53sLPzbVmdep6aljDsGEC83YLUeBsAoFjD3Z1KzuwQdckvJvbFqNwk1De3Yu47u+Dn6cqGsaFtm7YRujoAkBJvx6DM4JD1Rg3egs6wYxC5OpjbcXM3FlGXmE0Snr15BOLtFmw7ehLL1x0WXRKFyIa2QDDeIGEHaJ/b2aDB83YYdgxC63M7jS2tqG/bkshLQInOXXZyHH533WAAwLOrD2DPsVrBFdH5qqxvxr4TwVOyx+vs8s8zUYLbBg2et8OwYxDtZ+1os7Oj7MSKsZoQb7cIroZIX264oCeuGpqB1oCM+9/+Ek0tPF1Zzza1dXUKMhKQaqBl/TF5ybCaJRw72aS5kQpNh52FCxdi9OjRSEhIQFpaGqZNm4b9+zufKur1ejFr1iykpKQgPj4eN9xwA8rLywVVLI7WOzvqgYIJMZAk/Z4nQSSCJElYMG0o0p12HKlowJMf7RNdEp0HZceSUeZ1FA67BSOzg5eZam0LuqbDzrp16zBr1ixs3rwZq1evhs/nw+WXX46GhvbuxQMPPIAPP/wQf//737Fu3TqUlpbi+uuvF1i1GFo/a0c9UJBLWETdkuSw4Q8/GQ4A+OvmIqz91i24IuouZZlHz1dEnM4EjS5laTrsrFq1CnfccQcGDx6M4cOH49VXX0VxcTF27NgBAKitrcXLL7+MZ555Bj/84Q9x4YUX4pVXXsHGjRuxefPm037c5uZmeDyeTi96p4Qdd10zGlu0d1x3e2eHYYeouy7p1wN3TcgHADz4j92a3OJLZ1Zc1YiS6iZYTBLG5CeLLifklCHljYcqEdDQ7kFNh53vqq0NDuYlJwf/guzYsQM+nw+TJ09W36agoAA5OTnYtGnTaT/OwoUL4XK51Jfs7OzwFh4BrjgrEuOsALS5lKUeKMiwQ3ReHrpiAPqnx6OyvgWP/N8enq6sM0rHY2ROIhwGnF8cnp0Ih82Mk40+7CvTTiNBN2EnEAhgzpw5mDBhAoYMGQIAKCsrg81mQ2JiYqe3TU9PR1lZ2Wk/1qOPPora2lr1paSkJJylR4yy/VyLd2Sxs0MUGjFWM5bcPBI2swmf7CvHW9uM8fwVLZRZFiMuYQGA1WzC2LYDBrV0mrJuws6sWbOwd+9evPXWW+f9sex2O5xOZ6cXI8hrG1I+qsnODmd2iEJlUJYTv5zSHwAw/8NvUKjRXZjUWSAgqzuxjBp2gI5XR2jncEFdhJ3Zs2dj5cqVWLt2LXr16qU+npGRgZaWFtTU1HR6+/LycmRkZES4SvH00dnhVRFEofCzi3tjXO8UNPn8mPP2Lvj8AdEl0VnsK/OguqEFDpsZI7ITRZcTNsrcztbCarS0auPvpabDjizLmD17Nt577z18+umnyM/P7/T6Cy+8EFarFWvWrFEf279/P4qLizFu3LhIlyuc2tmpZGeHyOhMJglP3zQcCTEW7C6pwfOfHhJdEp2Fcsv5mPxkWM2a/vF7XgakJyA13oYmnx9ftl12Kpqmv9uzZs3C//7v/+LNN99EQkICysrKUFZWhqamJgCAy+XC3Xffjblz52Lt2rXYsWMH7rzzTowbNw4XXXSR4OojT6udHX9ARlU9Z3aIQi0rMRYLfjwUAPDCpwexo0gbP1jo1Iw+r6OQJAnj+2jrFnRNh51ly5ahtrYWEydORGZmpvry9ttvq2/z7LPP4pprrsENN9yASy+9FBkZGXj33XcFVi2O0tkprfXC69POCatVDc0IyIAkAckOm+hyiAzluuFZmDYiCwEZeODtXeq1LKQtLa0BbC2sBmD8sAO0H5i4QSOXgmo67MiyfMqXO+64Q32bmJgYvPjii6iurkZDQwPefffdqJzXAYJBIqFtK2NJtXaWstyeYFcnxWGHxcCtWyJRfj91CHomxqK4uhGPf/iN6HLoFHaV1KDJ50eKw4YB6Qmiywm78W1zO7tKalDn9QmuRuNhh7pGkiTkpmpvR1YFl7CIwsoVa8XTNw2HJAFvby/Bqr2nP3qDxFCWsMb3TYXJZPwrc3olxSEvJQ7+gKx2tERi2DEYLc7tVHg4nEwUbhf1TsF/XtoHAPDou1/B7fEKrog62qjM6xjolvOzUW5B18I9WQw7BtN+1o6Gwg47O0QRMfdH/TEo04mTjT788h9f8XRljahvbsWukhoA0TGvo1DmdjZq4Lwdhh2Dae/saGcZS/kNk50dovCyWUx4bvoI2C0mfH6gAq9vKhJdEgHYWliF1oCMnOQ4ZCfHiS4nYsb1ToEkAfvL69Qrg0Rh2DEYLd5+zs4OUeT0S0/Ar64aCAB48qN9OFheJ7giWn/Q+Kcmn0qSw4bBWcEbCjYJ3pXFsGMwyjLW8ZNNmjm50q3O7PD0ZKJIuG1cLi7r3wPNrQHc/9YuzTwXRKuNh5XzdaJnXkcxoe28nfUHxc7tMOwYTI8EO2KtZgRk4NhJbSxlqZ0dJzs7RJEgSRL++8ZhSIqz4psTHixe9S0CAc7viOD2ePFtWbC7phy0F03a78mqFDpDxrBjMJIkIbetu6OFuR1Zlts7O/EMO0SRkuaMwcLrhwEA/rK+EFc+9wXe33UcrbxDKyI8Xh/++NkhXLX0CwDAoExnVB6qOjovGTazCaW1XqFHoliEfWYKm7wUB74tq9PE3E5Dix9Nbac5c0CZKLKuGJKBX11VgKVrDmF/eR3uf2sXnv73Acy8tDduvLAXYqxm0SUaTmV9M/5nfSH+uqkIdW2nWfdMjMX8qYMFVyZGrM2MK4dmwGo2Ce3sMOwYkHKwoBY6O8pOLIfNDIedf92IIm3mpX1w8+gc/HXTUfzPhqMorm7Eb1bsxXNrDuJnF+djxkW5iOe/zfN27GQj/vz5Eby1rQTNbTNSfdPice/EPrh2eJahL/48m+emjxRdAsOOEWlpR5Zy23mak8PJRKK4Yq2Y/cN+uPvi3nhrWzH+/PkRlNZ6sfBf3+LFtYdwx/g83DEhPyqXWc7XIXcdln12JLhE2DYXNTw7EfdO7IMfDUyPitOS9YBhx4C0NLNTUcd5HSKtiLWZceeEfMwYm4sVu45j+brDOFLRgKWfHsKfvyjE9DHZuOeS3shKjBVdquZ9dawGf1x7GB9/UwZldWZC3xTcO7EvxvdJgSQx5GgJw44BKZ2dkupGtPoDQi/fVDo7PbgTi0gzbBYTbhqVjRsu6IWPvy7DHz87hL3HPXhlw1H87+Yi/HhkT/z8sj7o3SNedKmaIssyNh2pwrLPDuOLDlupLx+Ujnt/0BcjshPFFUdnxLBjQBnOGNgsJrS0BlBa40VOirgTO5XODg8UJNIes0nCVUMzceWQDHxxsBIvrj2ELYXVeGf7Mfx9xzFcNSQTv5jYB0N6ukSXKlQgIGPNt2788bND+LK4BkDwezd1eBZ+PrEP+kfBLeZ6x7BjQCaThNzkOBx01+NoVYPQsKMcEc6dWETaJUkSLu3fA5f274EdRdX449rDWPOtG//ccwL/3HMCl/XvgXsn9sGY/OSoWp5p9Qew8qsT+ONnh3CgvB5AsCt286hszLy0d1Rd/aB3DDsGlZviwEF3fdvt5z2E1dHe2eGAMpEeXJibjJfvSMa3ZR4s++wwPtxdinUHKrDuQAVG5Sbh3h/0wQ8GpBk69Hh9fvx9xzG89PlhlFQ3AQDi7RbcOi4Xd03I5y9vOsSwY1Dtt5+LHVJWB5T55ECkKwUZTjw3fSTm/qg//vT5Efxj+zFsLzqJu17djoGZTvxiYh9cPTQTZgPtNqrz+vDGlmK8vL5Qfe5Kcdhw18X5+OlFuXDFWgVXSN3FsGNQuanK7edit59zZodI33JTHHjyx0Nx/6R+eHl9Id7YXIR9Jzy4729f4ul/78fPL+uD6y/oCbtFvwcUVje04JUNhXht41F4vMGDALNcMZh5aW/cPDoHsTb9fm0UxLBjUFro7Pj8AVQ1tABgZ4dI79KdMfjVVQNx78Q+eG1jEV7ZWIiiqkY8+u4eLPnkAH52cW/8x9gcXR0eWlrThD9/cQRvbS1RT3rv3cOBX1zWB1NH9ITNEr0HARqNfv5WUpco28+LqxrhD8hCWs1V9cGgYzZJSI7jYWVERpAYZ8P9k/vhZ5fk429bi/GXLwpR5vFiwUf78OJnh3D7uDzcMT4PSRo+oPBIRT2WrzuM9748Dp8/eEjO0J4u3DuxDy4fnGGopTkKYtgxqExXDKxmCS3+AMo8XvQUcEiYshMrNd7GU0SJDMZht+Bnl/TGreNyseLL41j22WEcrWrEc2sO4s9fHMF/jMnBzy7pjQyXdjYn7D1eiz9+dgj/2tt+EOBFvZNx78S+uKRfqqGHrqMdw45BWcwmZCfF4UhlA3YUnRRyAdu3ZXUAuBOLyMjsFjNuHp2DGy/Mxr/2nsAf1x7GNyc8+Mv6Qry+qQjXX9ATt44TO9xbXNWI5Z8fwecHKtTHJg9Mwy8m9sWFuUnC6qLIYdgxsNyUYNi5729fCq2D8zpExmc2SbhmWBauHpqJzw5UYNnaw9h6tBpvbSvBW9tKRJcHADBJwLXDs/CLiX1QkOEUXQ5FEMOOgU0b2RM7ik6qN/CKYLOYcO3wTGGfn4giS5Ik/GBAGn4wIA3bjlZj2WeHselwFQICusuK4PNQFv7z0t7IbZtnpOgiySLWNzTG4/HA5XKhtrYWTifTPhERkR6c689v7qsjIiIiQ2PYISIiIkNj2CEiIiJDY9ghIiIiQ2PYISIiIkNj2CEiIiJDY9ghIiIiQ2PYISIiIkNj2CEiIiJDY9ghIiIiQ2PYISIiIkNj2CEiIiJDY9ghIiIiQ2PYISIiIkOziC5AC2RZBhC8Kp6IiIj0Qfm5rfwcPx2GHQB1dXUAgOzsbMGVEBERUVfV1dXB5XKd9vWSfLY4FAUCgQBKS0uRkJAASZJC9nE9Hg+ys7NRUlICp9MZso+rJ9H+PeDXH91fP8DvQbR//QC/B+H8+mVZRl1dHbKysmAynX4yh50dACaTCb169Qrbx3c6nVH5F7yjaP8e8OuP7q8f4Pcg2r9+gN+DcH39Z+roKDigTERERIbGsENERESGxrATRna7Hb/97W9ht9tFlyJMtH8P+PVH99cP8HsQ7V8/wO+BFr5+DigTERGRobGzQ0RERIbGsENERESGxrBDREREhsawQ0RERIbGsBNGL774IvLy8hATE4OxY8di69atokuKiIULF2L06NFISEhAWloapk2bhv3794suS5innnoKkiRhzpw5okuJqOPHj+OnP/0pUlJSEBsbi6FDh2L79u2iy4oIv9+PefPmIT8/H7GxsejTpw8ef/zxs97fo2eff/45rr32WmRlZUGSJKxYsaLT62VZxmOPPYbMzEzExsZi8uTJOHjwoJhiw+BMX7/P58PDDz+MoUOHwuFwICsrC7fddhtKS0vFFRwGZ/s70NHPf/5zSJKEJUuWRKQ2hp0wefvttzF37lz89re/xc6dOzF8+HBMmTIFbrdbdGlht27dOsyaNQubN2/G6tWr4fP5cPnll6OhoUF0aRG3bds2/OlPf8KwYcNElxJRJ0+exIQJE2C1WvGvf/0L33zzDZ5++mkkJSWJLi0iFi1ahGXLluGFF17Avn37sGjRIixevBjPP/+86NLCpqGhAcOHD8eLL754ytcvXrwYS5cuxfLly7FlyxY4HA5MmTIFXq83wpWGx5m+/sbGRuzcuRPz5s3Dzp078e6772L//v247rrrBFQaPmf7O6B47733sHnzZmRlZUWoMgAyhcWYMWPkWbNmqX/2+/1yVlaWvHDhQoFVieF2u2UA8rp160SXElF1dXVyv3795NWrV8uXXXaZfP/994suKWIefvhh+eKLLxZdhjBXX321fNddd3V67Prrr5dnzJghqKLIAiC/99576p8DgYCckZEh//d//7f6WE1NjWy32+W//e1vAioMr+9+/aeydetWGYBcVFQUmaIi7HTfg2PHjsk9e/aU9+7dK+fm5srPPvtsROphZycMWlpasGPHDkyePFl9zGQyYfLkydi0aZPAysSora0FACQnJwuuJLJmzZqFq6++utPfg2jxwQcfYNSoUfjJT36CtLQ0jBw5En/+859FlxUx48ePx5o1a3DgwAEAwO7du7F+/XpceeWVgisTo7CwEGVlZZ3+LbhcLowdOzYqnxOB4POiJElITEwUXUrEBAIB3HrrrXjwwQcxePDgiH5uXgQaBpWVlfD7/UhPT+/0eHp6Or799ltBVYkRCAQwZ84cTJgwAUOGDBFdTsS89dZb2LlzJ7Zt2ya6FCGOHDmCZcuWYe7cufjVr36Fbdu24b777oPNZsPtt98uurywe+SRR+DxeFBQUACz2Qy/348FCxZgxowZoksToqysDABO+ZyovC6aeL1ePPzww7jlllui6mLQRYsWwWKx4L777ov452bYobCaNWsW9u7di/Xr14suJWJKSkpw//33Y/Xq1YiJiRFdjhCBQACjRo3Ck08+CQAYOXIk9u7di+XLl0dF2HnnnXfwxhtv4M0338TgwYOxa9cuzJkzB1lZWVHx9dPp+Xw+3HTTTZBlGcuWLRNdTsTs2LEDzz33HHbu3AlJkiL++bmMFQapqakwm80oLy/v9Hh5eTkyMjIEVRV5s2fPxsqVK7F27Vr06tVLdDkRs2PHDrjdblxwwQWwWCywWCxYt24dli5dCovFAr/fL7rEsMvMzMSgQYM6PTZw4EAUFxcLqiiyHnzwQTzyyCOYPn06hg4diltvvRUPPPAAFi5cKLo0IZTnvWh/TlSCTlFREVavXh1VXZ0vvvgCbrcbOTk56vNiUVER/t//+3/Iy8sL++dn2AkDm82GCy+8EGvWrFEfCwQCWLNmDcaNGyewssiQZRmzZ8/Ge++9h08//RT5+fmiS4qoSZMmYc+ePdi1a5f6MmrUKMyYMQO7du2C2WwWXWLYTZgw4XvHDRw4cAC5ubmCKoqsxsZGmEydn17NZjMCgYCgisTKz89HRkZGp+dEj8eDLVu2RMVzItAedA4ePIhPPvkEKSkpokuKqFtvvRVfffVVp+fFrKwsPPjgg/j444/D/vm5jBUmc+fOxe23345Ro0ZhzJgxWLJkCRoaGnDnnXeKLi3sZs2ahTfffBPvv/8+EhIS1DV5l8uF2NhYwdWFX0JCwvfmkxwOB1JSUqJmbumBBx7A+PHj8eSTT+Kmm27C1q1b8dJLL+Gll14SXVpEXHvttViwYAFycnIwePBgfPnll3jmmWdw1113iS4tbOrr63Ho0CH1z4WFhdi1axeSk5ORk5ODOXPm4IknnkC/fv2Qn5+PefPmISsrC9OmTRNXdAid6evPzMzEjTfeiJ07d2LlypXw+/3q82JycjJsNpuoskPqbH8HvhvwrFYrMjIyMGDAgPAXF5E9X1Hq+eefl3NycmSbzSaPGTNG3rx5s+iSIgLAKV9eeeUV0aUJE21bz2VZlj/88EN5yJAhst1ulwsKCuSXXnpJdEkR4/F45Pvvv1/OycmRY2Ji5N69e8u//vWv5ebmZtGlhc3atWtP+e/+9ttvl2U5uP183rx5cnp6umy32+VJkybJ+/fvF1t0CJ3p6y8sLDzt8+LatWtFlx4yZ/s78F2R3HouybKBj/QkIiKiqMeZHSIiIjI0hh0iIiIyNIYdIiIiMjSGHSIiIjI0hh0iIiIyNIYdIiIiMjSGHSIiIjI0hh0iIiIyNIYdIiIAeXl5WLJkiegyiCgMGHaIKOLuuOMO9U6kiRMnYs6cORH73K+++ioSExO/9/i2bdswc+bMiNVBRJHDi0CJyBBaWlrO60LFHj16hLAaItISdnaISJg77rgD69atw3PPPQdJkiBJEo4ePQoA2Lt3L6688krEx8cjPT0dt956KyorK9X3nThxImbPno05c+YgNTUVU6ZMAQA888wzGDp0KBwOB7Kzs3Hvvfeivr4eAPDZZ5/hzjvvRG1trfr5fve73wH4/jJWcXExpk6divj4eDidTtx0000oLy9XX/+73/0OI0aMwF//+lfk5eXB5XJh+vTpqKurC+83jYi6jGGHiIR57rnnMG7cONxzzz04ceIETpw4gezsbNTU1OCHP/whRo4cie3bt2PVqlUoLy/HTTfd1On9X3vtNdhsNmzYsAHLly8HAJhMJixduhRff/01XnvtNXz66ad46KGHAADjx4/HkiVL4HQ61c/3y1/+8nt1BQIBTJ06FdXV1Vi3bh1Wr16NI0eO4Oabb+70docPH8aKFSuwcuVKrFy5EuvWrcNTTz0Vpu8WEXUXl7GISBiXywWbzYa4uDhkZGSoj7/wwgsYOXIknnzySfWx//mf/0F2djYOHDiA/v37AwD69euHxYsXd/qYHed/8vLy8MQTT+DnP/85/vjHP8Jms8HlckGSpE6f77vWrFmDPXv2oLCwENnZ2QCA119/HYMHD8a2bdswevRoAMFQ9OqrryIhIQEAcOutt2LNmjVYsGDB+X1jiCik2NkhIs3ZvXs31q5di/j4ePWloKAAQLCborjwwgu/976ffPIJJk2ahJ49eyIhIQG33norqqqq0NjYeM6ff9++fcjOzlaDDgAMGjQIiYmJ2Ldvn/pYXl6eGnQAIDMzE263u0tfKxGFHzs7RKQ59fX1uPbaa7Fo0aLvvS4zM1P9b4fD0el1R48exTXXXINf/OIXWLBgAZKTk7F+/XrcfffdaGlpQVxcXEjrtFqtnf4sSRICgUBIPwcRnT+GHSISymazwe/3d3rsggsuwP/93/8hLy8PFsu5P03t2LEDgUAATz/9NEymYOP6nXfeOevn+66BAweipKQEJSUlanfnm2++QU1NDQYNGnTO9RCRNnAZi4iEysvLw5YtW3D06FFUVlYiEAhg1qxZqK6uxi233IJt27bh8OHD+Pjjj3HnnXeeMaj07dsXPp8Pzz//PI4cOYK//vWv6uByx89XX1+PNWvWoLKy8pTLW5MnT8bQoUMxY8YM7Ny5E1u3bsVtt92Gyy67DKNGjQr594CIwothh4iE+uUvfwmz2YxBgwahR48eKC4uRlZWFjZs2AC/34/LL78cQ4cOxZw5c5CYmKh2bE5l+PDheOaZZ7Bo0SIMGTIEb7zxBhYuXNjpbcaPH4+f//znuPnmm9GjR4/vDTgDweWo999/H0lJSbj00ksxefJk9O7dG2+//XbIv34iCj9JlmVZdBFERERE4cLODhERERkaww4REREZGsMOERERGRrDDhERERkaww4REREZGsMOERERGRrDDhERERkaww4REREZGsMOERERGRrDDhERERkaww4REREZ2v8HIXnv1Uyqb3EAAAAASUVORK5CYII=", "text/plain": [ "
" ] @@ -274,7 +291,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 17, "metadata": {}, "outputs": [], "source": [ @@ -298,7 +315,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 18, "metadata": {}, "outputs": [ { @@ -328,14 +345,14 @@ "\n", "Output_format: Your output should be in the following json format, satisfying the json syntax:\n", "\n", - "{{\n", + "{\n", "\"reasoning\": ,\n", "\"answer\": ,\n", - "\"suggestion\": {{\n", + "\"suggestion\": {\n", " : ,\n", " : ,\n", - "}}\n", - "}}\n", + "}\n", + "}\n", "\n", "In \"reasoning\", explain the problem: 1. what the #Instruction means 2. what the #Feedback on #Output means to #Variables considering how #Variables are used in #Code and other values in #Documentation, #Inputs, #Others. 3. Reasoning about the suggested changes in #Variables (if needed) and the expected result.\n", "\n", @@ -353,20 +370,20 @@ "You need to change the of the variables in #Variables to improve the output in accordance to #Feedback.\n", "\n", "#Code\n", - "eval84 = eval(lst=lst0, __code=__code1)\n", - "eval85 = eval(lst=lst1, __code=__code1)\n", - "eval86 = eval(lst=lst2, __code=__code1)\n", - "eval87 = eval(lst=lst3, __code=__code1)\n", - "eq0 = eq(x=eval84, y=list0)\n", - "eq1 = eq(x=eval85, y=list1)\n", - "eq2 = eq(x=eval86, y=list2)\n", - "eq3 = eq(x=eval87, y=list3)\n", + "eval90 = eval(lst=lst0, __code=__code1)\n", + "eval91 = eval(lst=lst1, __code=__code1)\n", + "eval92 = eval(lst=lst2, __code=__code1)\n", + "eval93 = eval(lst=lst3, __code=__code1)\n", + "eq0 = eq(x=eval90, y=list0)\n", + "eq1 = eq(x=eval91, y=list1)\n", + "eq2 = eq(x=eval92, y=list2)\n", + "eq3 = eq(x=eval93, y=list3)\n", "concat1 = concat(args_0=eq0, args_1=eq1, args_2=eq2, args_3=eq3)\n", "\n", "#Documentation\n", "[eval] This operator eval(__code, *args, **kwargs) evaluates the code block, where __code is the code (str) and *args and **kwargs are the arguments of the function. The output is the result of the evaluation, i.e., __code(*args, **kwargs).\n", - "[eq] This is an eq operator of x and y. .\n", - "[concat] Concatenate the items into a single string .\n", + "[eq] This is an eq operator of x and y.\n", + "[concat] Concatenate the items into a single string\n", "\n", "#Variables\n", "(code) __code1:def strange_sort_list(lst):\n", @@ -385,18 +402,18 @@ "#Inputs\n", "(list) lst1=[5, 5, 5, 5]\n", "(list) lst2=[]\n", - "(list) lst3=[9, 8, 7, 6, 5, 4]\n", "(list) lst0=[1, 2, 3, 4]\n", + "(list) lst3=[9, 8, 7, 6, 5, 4]\n", "(list) list1=[5, 5, 5, 5]\n", "(list) list2=[]\n", - "(list) list3=[4, 9, 5, 8, 6, 7]\n", "(list) list0=[1, 4, 2, 3]\n", + "(list) list3=[4, 9, 5, 8, 6, 7]\n", "\n", "#Others\n", - "(list) eval85=[5, 5, 5, 5]\n", - "(list) eval86=[]\n", - "(list) eval87=[4, 5, 6, 7, 8, 9]\n", - "(list) eval84=[1, 2, 3, 4]\n", + "(list) eval91=[5, 5, 5, 5]\n", + "(list) eval92=[]\n", + "(list) eval90=[1, 2, 3, 4]\n", + "(list) eval93=[4, 5, 6, 7, 8, 9]\n", "(bool) eq0=False\n", "(bool) eq1=True\n", "(bool) eq2=True\n", @@ -423,13 +440,23 @@ "\n", "LLM response:\n", " {\n", - " \"reasoning\": \"The feedback indicates that the eval functions are used to generate sorted lists through a given sort function defined in __code1. The eq functions compare the output of these sort functions with predefined lists (list0, list1, list2, list3). The tests for lst0 and lst3 have failed, indicating that the output from the strange_sort_list function did not match the expected pattern given in list0 and list3. From the documentation and the given list patterns, 'strange_sort_list' should alternate between the smallest and largest remaining elements in the list, but the function in __code1 currently only sorts the list in ascending order. Thus, lst0 and lst3 are not sorting correctly into their expected 'strange' order.\",\n", - " \"answer\": \"Change __code1 to implement the strange sorting pattern by alternating between selecting minimum and maximum elements.\",\n", - " \"suggestion\": {\n", - " \"__code1\": \"def strange_sort_list(lst):\\n lst = sorted(lst)\\n result = []\\n while lst:\\n result.append(lst.pop(0)) # append and remove the first (minimum)\\n if lst: # check to avoid popping from an empty list\\n result.append(lst.pop()) # append and remove the last (maximum)\\n return result\\n\"\n", - " }\n", + "\"reasoning\": \"The #Instruction requires us to modify the values in #Variables, specifically the function __code1, to improve the output according to the feedback. According to #Feedback, test cases 0 and 3 failed, while test cases 1 and 2 passed. The current definition of strange_sort_list only sorts the list in ascending order, which is not sufficient for the 'strange order' specified. The 'strange order' is defined as starting with the minimum, then the maximum of the remaining, then the next minimum, and so forth. Therefore, we need to modify the function strange_sort_list(lst) to implement this logic. \\n\\nThe correct transformation should alternate between taking the smallest and largest remaining values in the list until the list is exhausted. This adjustment will ensure lists such as lst0 and lst3 are correctly transformed to match list0 and list3, respectively.\",\n", + "\"answer\": null,\n", + "\"suggestion\": {\n", + " \"__code1\": \"def strange_sort_list(lst):\\n '''\\n Given list of integers, return list in strange order.\\n Strange sorting, is when you start with the minimum value,\\n then maximum of the remaining integers, then minimum and so on.\\n '''\\n lst = sorted(lst)\\n result = []\\n while lst:\\n result.append(lst.pop(0)) # take min\\n if lst:\\n result.append(lst.pop(-1)) # take max\\n return result\"\n", + "}\n", "}\n" ] + }, + { + "data": { + "text/plain": [ + "{: \"def strange_sort_list(lst):\\n '''\\n Given list of integers, return list in strange order.\\n Strange sorting, is when you start with the minimum value,\\n then maximum of the remaining integers, then minimum and so on.\\n '''\\n lst = sorted(lst)\\n result = []\\n while lst:\\n result.append(lst.pop(0)) # take min\\n if lst:\\n result.append(lst.pop(-1)) # take max\\n return result\"}" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ @@ -476,6 +503,343 @@ "optimizer.backward(batched_outputs, batched_feedback.data)\n", "optimizer.step(verbose=True)" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Using the functions in `opto.trainer` to perform Batching\n", + "\n", + "In the earlier examples, we wrote our own design patterns for accomplishing batch optimization. However, Trace provides the `MiniBatchAlgorithm` to accomplish this automatically.\n", + "Let us see how the abstractions in `opto.trainer` allow us to scale up optimization, for example, doing minibatch optimization on the GSM 8K Dataset, which is a dataset of math word problems." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [], + "source": [ + "import datasets\n", + "import numpy as np\n", + "\n", + "train_dataset = datasets.load_dataset('openai/gsm8k', 'main')['train'][:10]\n", + "train_dataset = dict(inputs=train_dataset['question'], infos=train_dataset['answer'])\n", + "test_dataset = train_dataset\n", + "\n", + "# set seed\n", + "seed = 42\n", + "num_epochs = 1\n", + "batch_size = 2\n", + "eval_frequency = -1\n", + "num_threads = 3\n", + "verbose = True\n", + "\n", + "np.random.seed(seed)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We define the `Learner` agent which is a student LLM with a trainable system prompt. Trace will use a generative optimizer to tune the system prompt. Trace provides also a class for LLM-as-Judge called `VerbalJudgeGuide` that uses a Teacher LLM to provide rich feedbacks to the student LLM. " + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [], + "source": [ + "from opto import trace\n", + "from opto.utils.llm import LLM\n", + "from opto.optimizers import OptoPrime\n", + "from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm\n", + "from opto.trainer.loggers import TensorboardLogger\n", + "from opto.trainer.guide import VerbalJudgeGuide\n", + "from typing import Any\n", + "\n", + "@trace.model\n", + "class Learner:\n", + " \"\"\" A basic LLM agent. \"\"\"\n", + "\n", + " def __init__(self, system_prompt: str = \"You're a helpful agent\",\n", + " user_prompt_template: str = \"Query: {message}\",\n", + " llm: LLM = None):\n", + " self.system_prompt = trace.node(system_prompt, trainable=True)\n", + " self.user_prompt_template = trace.node(user_prompt_template)\n", + " self.llm = llm or LLM()\n", + "\n", + " @trace.bundle()\n", + " def model(self, system_prompt: str, user_prompt_template: str, message: str) -> str:\n", + " \"\"\"Call the LLM model.\n", + "\n", + " Args:\n", + " system_prompt: the system prompt to the agent. By tuning this prompt, we can control the behavior of the agent. For example, it can be used to provide instructions to the agent (such as how to reason about the problem, how to answer the question), or provide in-context examples of how to solve the problem.\n", + " user_prompt_template: the user prompt template to the agent. It is used as formatting the input to the agent as user_prompt_template.format(message=message).\n", + " message: the input to the agent. It can be a query, a task, a code, etc.\n", + " Returns:\n", + " The response from the agent.\n", + " \"\"\"\n", + "\n", + " if '{message}' not in user_prompt_template:\n", + " raise ValueError(\"user_prompt_template must contain '{message}'\")\n", + "\n", + " response = self.llm(\n", + " messages=[{\"role\": \"system\", \"content\": system_prompt},\n", + " {\"role\": \"user\", \"content\": user_prompt_template.format(message=message)}]\n", + " )\n", + " return response.choices[0].message.content\n", + "\n", + " def forward(self, message: Any) -> Any:\n", + " \"\"\" Forward pass of the agent. \"\"\"\n", + " return self.model(self.system_prompt, self.user_prompt_template, message)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, we use the `MiniBatchAlgorithm` as the trainer to sample batches from the GSM8K dataset, run the student model on the samples, gather feedback from the teacher model, and present the resulting traced graph to the optimizer." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "STARTING TRAINING\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 2): 100%|██████████| 2/2 [00:06<00:00, 3.12s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"The #Instruction asks us to change the values of the variables in #Variables to improve the output according to #Feedback. The #Feedback section provides the analysis of the answers generated for each query. Both answers for the queries (regarding Alexis and Weng) are correct, as indicated by the statement 'Correct [TERMINATE]'. The #Output shows that the responses generated for each model (Learner.model0 and Learner.model1) are logical and correct given the input prompts. Therefore, there are no errors in the current setup, and no changes are needed in the variables.\",\n", + "\"answer\": \"TERMINATE\",\n", + "\"suggestion\": {}\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating agent (iteration 1): 100%|██████████| 10/10 [00:22<00:00, 2.30s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 1] \u001b[92mAverage test score: 1.0\u001b[0m\n", + "Epoch: 0. Iteration: 1\n", + "[Step 1] Instantaneous train score: 1.0\n", + "[Step 1] Average train score: 1.0\n", + "[Step 1] \u001b[91mParameter: str:20: You're a helpful agent\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 2): 100%|██████████| 2/2 [00:09<00:00, 4.65s/it]\n", + "/home/aswaminathan/miniconda3/envs/trace/lib/python3.9/copy.py:263: RuntimeWarning: coroutine 'main' was never awaited\n", + " args = (deepcopy(arg, memo) for arg in args)\n", + "RuntimeWarning: Enable tracemalloc to get the object allocation traceback\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"The instruction asks us to change the value of variables if necessary to improve the output based on the feedback provided. In this instance, the feedback for both outputs (ID [0] and ID [1]) states 'Correct' and suggests termination, which indicates that the outputs match the expected results. The variables in the code that we have control over are used to set up prompts for an LLM model to process. The feedback shows the model's output correctly answers the questions based on the inputs, matching the expected correct answers outlined in the feedback. Therefore, no changes to the variables are necessary as the task is operating as intended.\",\n", + "\"answer\": \"TERMINATE\",\n", + "\"suggestion\": {}\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating agent (iteration 2): 100%|██████████| 10/10 [00:18<00:00, 1.88s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 2] \u001b[92mAverage test score: 1.0\u001b[0m\n", + "Epoch: 0. Iteration: 2\n", + "[Step 2] Instantaneous train score: 1.0\n", + "[Step 2] Average train score: 1.0\n", + "[Step 2] \u001b[91mParameter: str:20: You're a helpful agent\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 2): 100%|██████████| 2/2 [00:04<00:00, 2.46s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"The #Instruction asks us to adjust the #Variables to improve the output based on #Feedback. The feedback suggests that the answers provided by the models are correct for both IDs. The output of both Learner.model25 and Learner.model24 correctly represents the calculation processes needed to answer the given queries. As the feedback indicates '[TERMINATE]', it means the current outputs are satisfactory, and no changes to the #Variables are necessary.\",\n", + " \"answer\": \"TERMINATE\"\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating agent (iteration 3): 100%|██████████| 10/10 [00:20<00:00, 2.05s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 3] \u001b[92mAverage test score: 1.0\u001b[0m\n", + "Epoch: 0. Iteration: 3\n", + "[Step 3] Instantaneous train score: 1.0\n", + "[Step 3] Average train score: 1.0\n", + "[Step 3] \u001b[91mParameter: str:20: You're a helpful agent\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 2): 100%|██████████| 2/2 [00:08<00:00, 4.16s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"The #Instruction requires us to change the values in #Variables to improve the output. However, based on #Feedback, both IDs in the #Outputs are correctly calculated according to the logic specified in #Documentation and supported by expert feedback. Therefore, no changes are needed to improve the outputs, as they already match the expected results provided in the feedback.\",\n", + "\"answer\": \"Both outputs are correct as per the feedback.\",\n", + "\"suggestion\": {}\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating agent (iteration 4): 100%|██████████| 10/10 [00:19<00:00, 1.91s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 4] \u001b[92mAverage test score: 1.0\u001b[0m\n", + "Epoch: 0. Iteration: 4\n", + "[Step 4] Instantaneous train score: 1.0\n", + "[Step 4] Average train score: 1.0\n", + "[Step 4] \u001b[91mParameter: str:20: You're a helpful agent\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 2): 100%|██████████| 2/2 [00:05<00:00, 2.63s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"The #Instruction requires adjusting the value of the variable in #Variables to improve the output based on #Feedback. In this scenario, the feedback has been provided for both outputs (ID [0] and ID [1]) as correct, with an explicit [TERMINATE] instruction from the expert feedback, indicating that no changes are needed for the variable's value, as the outputs align perfectly with the expected answers. The current settings in #Variables, #Inputs, and #Others, including the prompts and message, are correctly leading to the generation of accurate answers to the queries, both for Julie's reading task and Albert's pizza consumption problem.\",\n", + "\"answer\": \"TERMINATE\",\n", + "\"suggestion\": {}\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating agent (iteration 5): 100%|██████████| 10/10 [00:17<00:00, 1.76s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 5] \u001b[92mAverage test score: 1.0\u001b[0m\n", + "Epoch: 0. Iteration: 5\n", + "[Step 5] Instantaneous train score: 1.0\n", + "[Step 5] Average train score: 1.0\n", + "[Step 5] \u001b[91mParameter: str:20: You're a helpful agent\u001b[0m\n", + "FINISHED TRAINING\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "agent = Learner(llm=LLM())\n", + "guide = VerbalJudgeGuide(llm=LLM())\n", + "optimizer = OptoPrime(agent.parameters(), llm=LLM())\n", + "logger = TensorboardLogger(verbose=True)\n", + "\n", + "alg = MinibatchAlgorithm(\n", + " agent=agent,\n", + " optimizer=optimizer,\n", + " logger=logger)\n", + "\n", + "import nest_asyncio\n", + "nest_asyncio.apply()\n", + "import asyncio\n", + "\n", + "async def wrapper():\n", + " print(\"STARTING TRAINING\")\n", + " alg.train(guide,\n", + " train_dataset,\n", + " num_epochs=num_epochs,\n", + " batch_size=batch_size,\n", + " eval_frequency=eval_frequency,\n", + " test_dataset=test_dataset,\n", + " num_threads=num_threads,\n", + " verbose='output')\n", + " print(\"FINISHED TRAINING\")\n", + " \n", + "asyncio.run(wrapper())" + ] } ], "metadata": { @@ -494,7 +858,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.15" + "version": "3.9.23" } }, "nbformat": 4, diff --git a/docs/tutorials/optimization_tutorial.ipynb b/docs/tutorials/optimization_tutorial.ipynb index 78511199..0cf4ca1f 100644 --- a/docs/tutorials/optimization_tutorial.ipynb +++ b/docs/tutorials/optimization_tutorial.ipynb @@ -17,16 +17,75 @@ }, "outputs": [], "source": [ - "%pip install trace-opt" + "%pip install trace-opt ipywidgets" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The code below provides a way to specify your API_KEY for calling LLMs using LiteLLM as part of this tutorial notebook. Alternatively, provide the keys by setting environment variables or loading LiteLLM config files." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import ipywidgets as widgets\n", + "from IPython.display import display\n", + "\n", + "# Function to save the environment variable and API key\n", + "def save_env_variable(env_name, api_key):\n", + " # Validate inputs\n", + " if not env_name.strip():\n", + " print(\"⚠️ Environment variable name cannot be empty.\")\n", + " return\n", + " if not api_key.strip():\n", + " print(\"⚠️ API key cannot be empty.\")\n", + " return\n", + " \n", + " # Store the API key as an environment variable\n", + " os.environ[env_name] = api_key\n", + " globals()[env_name] = api_key # Set it as a global variable\n", + " print(f\"✅ API key has been set for environment variable: {env_name}\")\n", + "\n", + "# Create the input widgets\n", + "env_name_input = widgets.Text(\n", + " value=\"OPENAI_API_KEY\", # Default value\n", + " description=\"Env Name:\",\n", + " placeholder=\"Enter env variable name (e.g., MY_API_KEY)\",\n", + ")\n", + "\n", + "api_key_input = widgets.Password(\n", + " description=\"API Key:\",\n", + " placeholder=\"Enter your API key\",\n", + ")\n", + "\n", + "# Create the button to submit the inputs\n", + "submit_button = widgets.Button(description=\"Set API Key\")\n", + "\n", + "# Display the widgets\n", + "display(env_name_input, api_key_input, submit_button)\n", + "\n", + "# Callback function for the button click\n", + "def on_button_click(b):\n", + " env_name = env_name_input.value\n", + " api_key = api_key_input.value\n", + " save_env_variable(env_name, api_key)\n", + "\n", + "# Attach the callback to the button\n", + "submit_button.on_click(on_button_click)" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "import opto\n", "from opto.trace import bundle, node\n", "from opto.optimizers import OptoPrime\n", "from opto.trace.nodes import GRAPH\n", @@ -74,7 +133,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -173,8 +232,6 @@ } ], "source": [ - "import autogen\n", - "\n", "# One-step optimization example\n", "x = node(-1.0, trainable=True)\n", "optimizer = OptoPrime([x])\n", @@ -444,7 +501,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -465,7 +522,7 @@ "source": [ "# A small example of how to include constraints on parameters\n", "GRAPH.clear()\n", - "x = node(-1.0, trainable=True, constraint=\"The value should be greater than 2.0\")\n", + "x = node(-1.0, trainable=True, description=\"The value should be greater than 2.0\")\n", "optimizer = OptoPrime([x])\n", "\n", "history = [x.data]\n", @@ -956,7 +1013,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.15" + "version": "3.9.23" } }, "nbformat": 4, diff --git a/docs/tutorials/trainers.ipynb b/docs/tutorials/trainers.ipynb new file mode 100644 index 00000000..84f64fa8 --- /dev/null +++ b/docs/tutorials/trainers.ipynb @@ -0,0 +1,2860 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Using `opto.trainer` algorithms for scaling up generative optimization\n", + "\n", + "This tutorial walks you through the different algorithms that have been built on top of the generative optimizers in Trace.\n", + "The `minibatch` tutorial already showed one specific use-case: `MiniBatchAlgorithm` that takes an agent, dataset and opto optimizer as inputs and outputs an optimized agent. \n", + "In fact, all of the algorithms in `opto.trainer` obey this basic input-output mapping; they all use the opto optimizers to propose candidate parameters, but utilize different search procedures on top of that to refine the optimized agent.\n", + "\n", + "We will use the [HardMath dataset](https://huggingface.co/datasets/xuanfeiren/math_hard_gemini) in this tutorial to illustrate the various algorithms in `opto.trainer`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "%pip install trace-opt ipywidgets" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The code below provides a way to specify your API_KEY for calling LLMs using LiteLLM as part of this tutorial notebook. Alternatively, provide the keys by setting environment variables or loading LiteLLM config files." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import ipywidgets as widgets\n", + "from IPython.display import display\n", + "\n", + "# Function to save the environment variable and API key\n", + "def save_env_variable(env_name, api_key):\n", + " # Validate inputs\n", + " if not env_name.strip():\n", + " print(\"⚠️ Environment variable name cannot be empty.\")\n", + " return\n", + " if not api_key.strip():\n", + " print(\"⚠️ API key cannot be empty.\")\n", + " return\n", + " \n", + " # Store the API key as an environment variable\n", + " os.environ[env_name] = api_key\n", + " globals()[env_name] = api_key # Set it as a global variable\n", + " print(f\"✅ API key has been set for environment variable: {env_name}\")\n", + "\n", + "# Create the input widgets\n", + "env_name_input = widgets.Text(\n", + " value=\"OPENAI_API_KEY\", # Default value\n", + " description=\"Env Name:\",\n", + " placeholder=\"Enter env variable name (e.g., MY_API_KEY)\",\n", + ")\n", + "\n", + "api_key_input = widgets.Password(\n", + " description=\"API Key:\",\n", + " placeholder=\"Enter your API key\",\n", + ")\n", + "\n", + "# Create the button to submit the inputs\n", + "submit_button = widgets.Button(description=\"Set API Key\")\n", + "\n", + "# Display the widgets\n", + "display(env_name_input, api_key_input, submit_button)\n", + "\n", + "# Callback function for the button click\n", + "def on_button_click(b):\n", + " env_name = env_name_input.value\n", + " api_key = api_key_input.value\n", + " save_env_variable(env_name, api_key)\n", + "\n", + "# Attach the callback to the button\n", + "submit_button.on_click(on_button_click)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We load the dataset and define a `Guide` (i.e. LLM-as-Judge) that can provide feedback for answers to questions in the dataset." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages/flaml/__init__.py:20: UserWarning: flaml.automl is not available. Please install flaml[automl] to enable AutoML functionalities.\n", + " warnings.warn(\"flaml.automl is not available. Please install flaml[automl] to enable AutoML functionalities.\")\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Training samples: 20\n", + "Validation samples: 20\n", + "Test samples: 10\n" + ] + } + ], + "source": [ + "import datasets\n", + "import numpy as np\n", + "from typing import Any, Tuple\n", + "from opto.trainer.guide import AutoGuide\n", + "from opto.utils.llm import LLM\n", + "\n", + "# Set random seed\n", + "np.random.seed(42)\n", + "\n", + "math_data = datasets.load_dataset('xuanfeiren/math_hard_gemini')\n", + "train_data = math_data['train'].select(\n", + " range(10, 30)\n", + " )\n", + "validate_data = train_data\n", + "test_data = math_data['test'].select(range(10))\n", + "\n", + "# Format data for trainer\n", + "train_dataset = {'inputs': train_data['problem'], 'infos': train_data['solution']}\n", + "validate_dataset = {'inputs': validate_data['problem'], 'infos': validate_data['solution']}\n", + "test_dataset = {'inputs': test_data['problem'], 'infos': test_data['solution']}\n", + "\n", + "# Log dataset sizes\n", + "print(f\"Training samples: {len(train_dataset['inputs'])}\")\n", + "print(f\"Validation samples: {len(validate_dataset['inputs'])}\")\n", + "print(f\"Test samples: {len(test_dataset['inputs'])}\")\n", + "\n", + "\n", + "class TeacherGuide(AutoGuide):\n", + " \"\"\"Guide that uses LLM to judge answers and provide feedback.\"\"\"\n", + " \n", + " def __init__(self, model: str = \"gpt-4o-mini\"):\n", + " \"\"\"Initialize the teacher guide.\n", + " \n", + " Args:\n", + " model: The LLM model to use for evaluation\n", + " \"\"\"\n", + " super().__init__()\n", + " self.guide_llm = LLM(model=model)\n", + " self.system_prompt = \"You are an expert math teacher evaluating student answers.\"\n", + " self.judge_prompt_template = (\n", + " \"Carefully review the following three distinct sections:\\n\\n\"\n", + " \"SECTION 1: The Math Problem\\n\"\n", + " \"----------------------------\\n\"\n", + " \"{query}\\n\"\n", + " \"----------------------------\\n\\n\"\n", + " \"SECTION 2: The Student's Full Answer\\n\"\n", + " \"----------------------------\\n\"\n", + " \"{response}\\n\"\n", + " \"----------------------------\\n\\n\"\n", + " \"SECTION 3: The Official Correct Answer\\n\"\n", + " \"----------------------------\\n\"\n", + " \"{reference}\\n\"\n", + " \"----------------------------\\n\\n\"\n", + " \"INSTRUCTIONS FOR JUDGING:\\n\"\n", + " \"1. Your primary task is to compare the student's **final numerical result** (or final conclusion if no number is present) from SECTION 2 with the **Official Correct Answer** provided in SECTION 3.\\n\"\n", + " \"2. When evaluating SECTION 2 (Student's Full Answer), focus SOLELY on the **final answer part** of the student's response. Ignore all intermediate steps, reasoning, or explanations for the correctness check unless the problem specifically asks for reasoning as the final answer.\\n\"\n", + " \"3. Determine if the student's **final answer** is equivalent to the **Official Correct Answer**.\\n\\n\"\n", + " \"RESPONSE FORMAT:\\n\"\n", + " \"- If the student's final answer (from SECTION 2) IS equivalent to the Official Correct Answer (from SECTION 3), respond ONLY with the exact phrase: 'Correct [TERMINATE]'\\n\"\n", + " \"- If the student's final answer IS NOT equivalent, respond ONLY with specific and actionable feedback. The feedback should clearly explain the error in the student's final answer and guide them on how to arrive at the Official Correct Answer.\"\n", + " )\n", + "\n", + " def get_feedback(self, task: str, response: str, info: Any, **kwargs) -> Tuple[float, str]:\n", + " \"\"\"Get feedback on a student response.\n", + " \n", + " Args:\n", + " task: The original math problem\n", + " response: The student's answer\n", + " info: The reference/correct answer\n", + " **kwargs: Additional arguments\n", + " \n", + " Returns:\n", + " Tuple of (score, feedback_text)\n", + " \"\"\"\n", + " user_prompt = self.judge_prompt_template.format(\n", + " query=task,\n", + " response=response,\n", + " reference=info\n", + " )\n", + "\n", + " messages = [\n", + " {\"role\": \"system\", \"content\": self.system_prompt},\n", + " {\"role\": \"user\", \"content\": user_prompt}\n", + " ]\n", + "\n", + " llm_response = self.guide_llm(messages=messages)\n", + " feedback_text = llm_response.choices[0].message.content\n", + "\n", + " if 'Correct [TERMINATE]' in feedback_text:\n", + " return 1.0, \"Correct.\"\n", + " else:\n", + " return 0.0, f\"Incorrect. Feedback: {feedback_text}\"\n", + " \n", + " def metric(self, task: str, content: str, info: Any, **kwargs) -> float:\n", + " \"\"\"Calculate the metric score for an answer.\n", + " \n", + " Args:\n", + " task: The original math problem\n", + " content: The student's answer\n", + " info: The reference/correct answer\n", + " **kwargs: Additional arguments\n", + " \n", + " Returns:\n", + " Score (0.0 or 1.0)\n", + " \"\"\"\n", + " score, _ = self.get_feedback(task, content, info, **kwargs)\n", + " return score" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We define the `Learner` agent which is a student LLM with a trainable `system prompt` and a trainable `user prompt template`. Trace will use a generative optimizer to tune these prompts." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "from opto import trace\n", + "from opto.optimizers import OptoPrime\n", + "from opto.optimizers.utils import print_color\n", + "from opto.trace.modules import Module\n", + "from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm, BasicSearchAlgorithm\n", + "from opto.trainer.algorithms.beamsearch_algorithm import BeamsearchAlgorithm, BeamsearchHistoryAlgorithm\n", + "from opto.trainer.algorithms.UCBsearch import UCBSearchAlgorithm\n", + "\n", + "\n", + "@trace.model\n", + "class Learner(Module):\n", + " \"\"\"A basic LLM Agent for solving math problems.\"\"\"\n", + " \n", + " def __init__(self, \n", + " system_prompt: str = \"You're a helpful agent answering math problems.\",\n", + " user_prompt_template: str = \"Solve the following math problem step-by-step: {message}\",\n", + " llm: LLM = None):\n", + " \"\"\"Initialize the learner agent.\n", + " \n", + " Args:\n", + " system_prompt: System prompt to guide LLM behavior\n", + " user_prompt_template: Template for formatting user messages\n", + " llm: LLM instance to use for generation (defaults to gpt-3.5-turbo)\n", + " \"\"\"\n", + " super().__init__()\n", + " self.system_prompt = trace.node(system_prompt, trainable=True)\n", + " self.user_prompt_template = trace.node(user_prompt_template, trainable=True)\n", + " self.llm = llm or LLM(model=\"gpt-3.5-turbo\")\n", + "\n", + " @trace.bundle()\n", + " def call_llm(self, system_prompt: str, user_prompt: str) -> str:\n", + " \"\"\"Call LLM model with the given prompts.\n", + " \n", + " Args:\n", + " system_prompt: The system prompt\n", + " user_prompt: The user prompt\n", + " \n", + " Returns:\n", + " The LLM response content\n", + " \"\"\"\n", + " response = self.llm(\n", + " messages=[\n", + " {\"role\": \"system\", \"content\": system_prompt},\n", + " {\"role\": \"user\", \"content\": user_prompt}\n", + " ]\n", + " )\n", + " return response.choices[0].message.content\n", + "\n", + " def forward(self, message: Any) -> str:\n", + " \"\"\"Agent's forward pass to process a message.\n", + " \n", + " Args:\n", + " message: The input message to process\n", + " \n", + " Returns:\n", + " The generated response\n", + " \"\"\" \n", + " user_prompt = self.user_prompt_template.format(message=message)\n", + " return self.call_llm(self.system_prompt, user_prompt)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We initialize all the components: the agent using the student LLM, the guide using the teacher LLM, and the optimizer using an LLM as a generative optimizer." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "student_llm = LLM()\n", + "agent = Learner(llm=student_llm)\n", + "\n", + "train_guide = TeacherGuide()\n", + "validate_guide = TeacherGuide()\n", + "\n", + "optimizer = OptoPrime(agent.parameters())\n", + "\n", + "from opto.trainer.loggers import DefaultLogger\n", + "class SimpleLogger(DefaultLogger):\n", + " \"\"\"Simplified logger that only shows important metrics.\"\"\"\n", + " \n", + " def log(self, name: str, data: Any, step: int, **kwargs):\n", + " \"\"\"Log only specific metrics to reduce output clutter.\n", + " \n", + " Args:\n", + " name: The name of the metric\n", + " data: The metric value\n", + " step: The current step\n", + " **kwargs: Additional logging arguments\n", + " \"\"\"\n", + " important_metrics = [\n", + " 'Average train score',\n", + " 'Average test score',\n", + " 'Validation score'\n", + " ]\n", + " \n", + " if name in important_metrics or 'Parameter' in name:\n", + " super().log(name, data, step, **kwargs)\n", + "\n", + "logger = SimpleLogger()\n", + "\n", + "import nest_asyncio\n", + "nest_asyncio.apply()\n", + "import asyncio\n", + "\n", + "train_params = {\n", + " \"guide\": train_guide,\n", + " \"train_dataset\": train_dataset,\n", + " \"num_epochs\": 1,\n", + " \"num_threads\": 5,\n", + " \"batch_size\": 5,\n", + " \"test_dataset\": test_dataset,\n", + " \"validate_dataset\": validate_dataset,\n", + " \"validate_guide\": validate_guide,\n", + " \"eval_frequency\": 2,\n", + " \"log_frequency\": 2,\n", + " #for Basic Search\n", + " \"num_proposals\": 2,\n", + " #for Beam Search\n", + " \"validation_dataset_size\": 5,\n", + " \"beam_width\": 3,\n", + " \"max_depth\": 4,\n", + " \"max_history_size\": 2,\n", + " #for UCB Search\n", + " \"num_search_iterations\": 3,\n", + " \"train_batch_size\": 5,\n", + " \"evaluation_batch_size\": 5,\n", + " \"max_buffer_size\": 3,\n", + " \"ucb_exploration_factor\": 1.0\n", + " }" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, we will go through each of the algorithms in `opto.trainer`. Each algorithm will run the student model on the train dataset, gather feedback from the teacher model, present the resulting traced graph to the optimizer, and then perform specific post-processing throughout each training epoch." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "STARTING TRAINING MINIBATCH\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating agent (iteration 0): 100%|██████████| 10/10 [00:52<00:00, 5.26s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 0] \u001b[92mAverage test score: 0.4\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:30<00:00, 6.05s/it]\n", + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:52<00:00, 10.40s/it]\n", + "Evaluating agent (iteration 2): 100%|██████████| 10/10 [00:50<00:00, 5.06s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 2] \u001b[92mAverage test score: 0.2\u001b[0m\n", + "Epoch: 0. Iteration: 2\n", + "[Step 2] Average train score: 0.2\n", + "[Step 2] \u001b[91mParameter: str:0: You're a helpful agent assisting with thorough and complete mathematical problem analysis, ensuring all steps are accurately validated.\u001b[0m\n", + "[Step 2] \u001b[91mParameter: str:1: Carefully process each subcomponent of the following problem: {message} Methodically ensure completeness in probability calculations, permutations, customizable solutions, and systematic explorations of possible outcomes.\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:49<00:00, 9.88s/it]\n", + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:28<00:00, 5.64s/it]\n", + "Evaluating agent (iteration 4): 100%|██████████| 10/10 [01:01<00:00, 6.10s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 4] \u001b[92mAverage test score: 0.2\u001b[0m\n", + "Epoch: 0. Iteration: 4\n", + "[Step 4] Average train score: 0.2\n", + "[Step 4] \u001b[91mParameter: str:0: Accurate precision ensuring number coating and span impart cataloguing upon probability, permutation, solution synthesis, and structured exploration\u001b[0m\n", + "[Step 4] \u001b[91mParameter: str:1: Diligently analyze each part facet of the offering issue: {message} carefuly ascertain completion in probability computation, permutation exercise, customizable provides solution, and scheme sized explorable outcomes.\u001b[0m\n", + "FINISHED TRAINING MINIBATCH\n", + "Final score: 0.2\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "algorithm = MinibatchAlgorithm(\n", + " agent=agent,\n", + " optimizer=optimizer,\n", + " logger=logger,\n", + " num_threads=train_params[\"num_threads\"]\n", + " )\n", + "\n", + "async def wrapper():\n", + " print(\"STARTING TRAINING MINIBATCH\")\n", + " metrics, final_score = algorithm.train(**train_params)\n", + " print(\"FINISHED TRAINING MINIBATCH\")\n", + " print(\"Final score: \", final_score)\n", + "\n", + "asyncio.run(wrapper())" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "STARTING TRAINING BASIC SEARCH\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating agent (iteration 0): 100%|██████████| 10/10 [01:06<00:00, 6.63s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 0] \u001b[92mAverage test score: 0.2\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:32<00:00, 6.52s/it]\n", + "Generating 2 proposals: 100%|██████████| 2/2 [00:12<00:00, 6.32s/it]\n", + "Validating proposals: 100%|██████████| 20/20 [00:22<00:00, 1.12s/it]\n", + "Validating proposals: 100%|██████████| 20/20 [01:40<00:00, 5.00s/it]\n", + "Validating proposals: 100%|██████████| 20/20 [02:16<00:00, 6.82s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 0] \u001b[92mValidation score: 0.05\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:38<00:00, 7.76s/it]\n", + "Generating 2 proposals: 100%|██████████| 2/2 [00:15<00:00, 7.88s/it]\n", + "Validating proposals: 100%|██████████| 20/20 [02:22<00:00, 7.14s/it]\n", + "Validating proposals: 100%|██████████| 20/20 [01:21<00:00, 4.05s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 1] \u001b[92mValidation score: 0.15\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating agent (iteration 2): 100%|██████████| 10/10 [01:03<00:00, 6.32s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 2] \u001b[92mAverage test score: 0.2\u001b[0m\n", + "Epoch: 0. Iteration: 2\n", + "[Step 2] Average train score: 0.1\n", + "[Step 2] \u001b[91mParameter: str:0: Critically examine and describe each step of the problem-solving process, ensuring thorough precision in applying combinatorial logic, sequence conversions, and probability distributions within complex scenarios such as probability computation, permutation exercise, solution synthesis, and exploration of structured outcomes.\u001b[0m\n", + "[Step 2] \u001b[91mParameter: str:1: Evaluate each component in detail for the given problem situation: {message} employing strategic reasoning to ascertain completion in logical computation, solving exercises through permutations, offering customizable solutions, and unveiling outcomes of scenario explorations.\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:41<00:00, 8.34s/it]\n", + "Generating 2 proposals: 100%|██████████| 2/2 [00:21<00:00, 10.85s/it]\n", + "Validating proposals: 100%|██████████| 20/20 [01:41<00:00, 5.08s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 2] \u001b[92mValidation score: 0.15\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:40<00:00, 8.13s/it]\n", + "Generating 2 proposals: 100%|██████████| 2/2 [00:11<00:00, 5.89s/it]\n", + "Validating proposals: 100%|██████████| 20/20 [01:24<00:00, 4.24s/it]\n", + "Validating proposals: 100%|██████████| 20/20 [01:25<00:00, 4.25s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 3] \u001b[92mValidation score: 0.15\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating agent (iteration 4): 100%|██████████| 10/10 [00:45<00:00, 4.52s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 4] \u001b[92mAverage test score: 0.3\u001b[0m\n", + "Epoch: 0. Iteration: 4\n", + "[Step 4] Average train score: 0.15000000000000002\n", + "[Step 4] \u001b[91mParameter: str:0: Critically examine and describe each step of the problem-solving process, ensuring thorough precision in applying combinatorial logic, sequence conversions, and probability distributions within complex scenarios such as probability computation, permutation exercise, solution synthesis, and exploration of structured outcomes.\u001b[0m\n", + "[Step 4] \u001b[91mParameter: str:1: Evaluate each component in detail for the given problem situation: {message} employing strategic reasoning to ascertain completion in logical computation, solving exercises through permutations, offering customizable solutions, and unveiling outcomes of scenario explorations.\u001b[0m\n", + "FINISHED TRAINING BASIC SEARCH\n", + "Final score: 0.3\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "algorithm = BasicSearchAlgorithm(\n", + " agent=agent,\n", + " optimizer=optimizer,\n", + " logger=logger,\n", + " num_threads=train_params[\"num_threads\"]\n", + " )\n", + "\n", + "async def wrapper():\n", + " print(\"STARTING TRAINING BASIC SEARCH\")\n", + " metrics, final_score = algorithm.train(**train_params)\n", + " print(\"FINISHED TRAINING BASIC SEARCH\")\n", + " print(\"Final score: \", final_score)\n", + " \n", + "asyncio.run(wrapper())" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "STARTING TRAINING BEAM SEARCH\n", + "\u001b[94mRunning BeamsearchAlgorithm with beam_width=3, max_depth=4\u001b[0m\n", + "\u001b[94mUsing validation_dataset_size=5 for intermediate evaluations\u001b[0m\n", + "\u001b[94m\n", + "===== Evaluating Initial Parameters =====\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating initial parameters on test set: 100%|██████████| 10/10 [00:41<00:00, 4.18s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[93mInitial test score: 0.2000\u001b[0m\n", + "\u001b[94m\n", + "===== Beam Search Depth 1/4 with 1 beams =====\u001b[0m\n", + "\u001b[96mSampled validation minibatch of size 5 for depth 1\u001b[0m\n", + "\u001b[93mProcessing beam 1/1\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:23<00:00, 4.70s/it]\n", + "Generating 2 proposals for beam 1: 50%|█████ | 1/2 [00:09<00:09, 9.32s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"The feedback provided indicates issues with the outcomes computed in the code for some problem instances. Here's a breakdown:\\n1. ID[0]: The student's calculated answer was off due to an incorrect count of distinct collections of consonants. They provided 87 when the correct count is 72. This suggests re-evaluating how the consonants are grouped without double-counting. The construction of possible usage scenarios needs correction to prevent overlap and ensure unique contributions.\\n2. ID[1] was correct, so no changes are needed for this problem.\\n3. ID[2]: The student's understanding of permutations and probabilities based on the lattice was incorrect. They concluded with a probability of 1/16, but the correct symmetry of movements on the lattice results in a probability of 1/4. This indicates a need to consider the even distribution across potential endpoints on the lattice, using symmetry to realize each endpoint is equally probable.\\n4. ID[3] was correct, so no changes are needed.\\n5. ID[4]: The student's calculations were more complex than necessary, leading to an incorrect conclusion of 166167 when the answer should be 5. The problem requires a simpler combinatorial logic by recognizing dimension fitting and using basic probability, resulting in a sum of numerator and denominator equating to 5.\\n\\nTo implement the feedback correctly, the problems need to be approached with a clearer fundamental understanding of combinatorics, symmetry, and probability logic.\",\n", + " \"answer\": null,\n", + " \"suggestion\": {\n", + " \"str0\": \"Consider simplifying the logic for each distinct problem, focusing on symmetry and leveraging basic combinatorial approaches to arrive at official solutions efficiently.\",\n", + " \"str1\": \"Re-evaluate vowel and consonant combinations, account for symmetry correctly on lattice problems, and simplify the dimensions's fitting logic to reach conclusions aligned with official answers.\"\n", + " }\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 2 proposals for beam 1: 100%|██████████| 2/2 [00:09<00:00, 4.83s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"The #Instruction requires us to adjust the value of variables in #Variables section to improve the outputs based on the #Feedback given. There are 5 different task outputs in #Outputs, and their correctness is indicated in the #Feedback. For ID [0] and ID [2], the feedback states that the student's answers are incorrect because of miscalculations in combinations and probabilities respectively. Similarly, ID [4] indicates an incorrect solution due to overcomplication, whereas IDs [1] and [3] are marked as correct. The primary variables influencing those outputs are 'str0' and 'str1' which are used in the prompts. Given the feedback, we should refine the calculation logic or reformulate the problem addressing prompts through a corrected detailed and clear explanation. In particular, ID [0] requires recalculating distinct collections, ID [2] involves improving probability distribution calculations, and ID [4] involves refining the method to understand the combinatorial setup. Thus, an updated 'str0' and 'str1' that better frames the problems for correct consequence inference in respective calculations is suggested. This redesign would align more closely with correct reasoning directives, resolving calculation errors without explicit instruction knowledge beyond what's provided.\",\n", + " \"answer\": \"\", \n", + " \"suggestion\": {\n", + " \"str0\": \"Evaluate detailed logic approaches focusing on recognizing constraints properly in permutation or probability setups, ensuring combinatorial approaches align with expected constraints effectively in complex scenarios. Reassess frame scenarios for multi-step conclusion tactics in either general problem solving or result synthesis.\",\n", + " \"str1\": \"Examine stepwise construction ensuring solutions with logical reasoning intact from raw deduction to systematic analytics. Revise cases with particular attention to parameter distinctions, securing robust resolution across permutation or probability contexts within logistical boundaries.\"\n", + " }\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 1/3: 100%|██████████| 5/5 [00:17<00:00, 3.48s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 1: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 2/3: 100%|██████████| 5/5 [00:24<00:00, 4.96s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 2: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 3/3: 100%|██████████| 5/5 [00:23<00:00, 4.74s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 3: Validation score: 0.6000\u001b[0m\n", + "\u001b[92mKeeping all 3 candidates as num_candidates <= beam_width. Scores: ['0.0000', '0.0000', '0.6000']\u001b[0m\n", + "\u001b[92mDepth 1 - Best validation score: 0.6000\u001b[0m\n", + "\u001b[94m\n", + "===== Beam Search Depth 2/4 with 3 beams =====\u001b[0m\n", + "\u001b[96mSampled validation minibatch of size 5 for depth 2\u001b[0m\n", + "\u001b[93mProcessing beam 1/3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:24<00:00, 4.80s/it]\n", + "Generating 2 proposals for beam 1: 100%|██████████| 2/2 [00:09<00:00, 4.51s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"The instruction requires adjusting the given variable values to improve the output by aligning it with the feedback explanations, which indicate specific answers. The code involves concatenating results from different calls to an LLM model. The variables str0 and str1 seem to contain information used to guide the models but do not directly influence the output-related math problems according to feedback. Each output from Learner.call_llm corresponds to a different math problem with specific expected answers:\\n\\n1. **Problem on Coordinate Plane (format290):** Expected to result in `m + n` for the probability expressed as `m/n`. Requires calculating paths and probabilities reaching `(2,2)` in 6 or fewer steps.\\n\\n2. **Locker Problem (format291):** Needs an explicit pattern recognition or calculation to find that locker number 342 is the last opened.\\n\\n3. **Handshake Problem (format292):** Requires solving an equation to find the minimum handshakes for the coach; targeted response is `k = 5`.\\n\\n4. **Distribution of Cousins (format293):** Focuses on combinatorial arrangements resulting in 15 distinct possibilities.\\n\\n5. **Letters in Bag (format294):** Entails selecting from indistinguishable vowels and consonants; expected answer is 72 distinct groupings.\\n\\nImproving the output requires entering these specific answers as potential checks or calculations (not modifying descriptions) for refining model interactions.\",\n", + "\"answer\": null,\n", + "\"suggestion\": {\n", + " \"str0\": \"Ensure model outputs are calculated or aligned with problem solutions to provide final numerical answers, adjusting user prompt if necessary.\",\n", + " \"str1\": \"Consider cross-verifying correct computations for expected outcomes if descriptions affect logic processes in model response.\"\n", + "}\n", + "}\n", + "LLM response:\n", + " {\n", + " \"reasoning\": \"The #Instruction is asking for a change in variable values located in #Variables based on #Feedback to arrive at the desired output. The #Feedback indicates that the provided responses do not yield the correct final numerical answers for the specific mathematical problems described. The #Feedback for each ID denotes issues related to lack of computation towards the expected solutions. The code utilizes string formatting and LLM calling to concatenate messages and employ model outputs into a batchify function, aiming to find specific results for combinatoric and mathematical problems given in the messages. By understanding the connections between mathematical concepts like combinations, symmetry, and fitting logic, and the expected outputs, it becomes clear that we need to tailor the provided input strings related to str0 and str1 to be more specific to the calculations required by the feedback given in #Others.\",\n", + " \"answer\": \"Adjust the contents of str0 and str1 to focus directly on the calculations needed for each problem to swing towards specific solutions highlighted in #Feedback.\",\n", + " \"suggestion\": {\n", + " \"str0\": \"Solve each problem by directly calculating distinct answers. For the object reaching (2,2) in steps, determine all ways in 4 or 6 steps. For the lockers, trace each open-close step carefully until locker 342 is the last. For gymnasts, calculate combinations where total is precisely 281, minimizing coach handshakes. For cousins, enumerate placement variants for each room combination. For letters, determine indistinguishable combinations focusing on T's, M's, and A's.\",\n", + " \"str1\": \"Focus directly on providing numeric answers by applying combinatorics, symmetry, and dimensions fitting logic specific to the distinct problem being queried, ensuring detailed, step-by-step solutions addressing the exact scenarios described.\"\n", + " }\n", + "}\n", + "\u001b[93mProcessing beam 2/3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:19<00:00, 3.90s/it]\n", + "Generating 2 proposals for beam 2: 50%|█████ | 1/2 [00:08<00:08, 8.46s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"The instruction requests to modify the #Variables to improve the output according to #Feedback. The Feedback indicates the necessity to provide a numerical answer derived through a specific process related to calculating probabilities, permutations, or identifying properties of Pascal's triangle, depending on the specific task associated with each ID. \\n\\nIn this particular scenario, the issue arises because the outputs do not reflect the Answers' expectations, which requires calculating quantitative results rather than providing general problem-solving frameworks. To generate meaningful outputs, variables `str0` and `str1` should be directly focused on the calculations and conclusions needed to solve each problem as stated in the Feedback rather than maintaining the broad guidance.\\n\\nFor example, using correct methodologies detailed in the feedback analysis:\\n- For the problem where a specific four-digit number is requested, `str1` should compute to find this number directly, focusing on going through binomial coefficients in Pascal's triangle, rather than applying a general strategy.\",\n", + "\"answer\": \"In order to fix the issues as per the feedback, the student should focus on calculating specific numerical conclusions for the problems. Example numbers to solve: for permutation/probability tasks to ensure logic completion, use probabilistic formulas directly related to the Official Correct Answers.\",\n", + "\"suggestion\": {\n", + " \"str0\": \"Calculate probabilities or permutations directly as based on details provided in #Feedback for problems involving probability or permutation involving combinatory complex problems.\",\n", + " \"str1\": \"Implement logic that derives specific numerical answers from computations and processes as guided by feedback directives, i.e., calculate probabilities or specific number identifications.\"\n", + "}\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 2 proposals for beam 2: 100%|██████████| 2/2 [00:12<00:00, 6.06s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"The feedback indicates that all responses from the Learner lack a final, numerical conclusion in their outputs, which are necessary to assess correctness against the given problems. The output lacks specificity and alignment with the official correct answers of specific problems, requiring a shift in approach to deliver concrete results. This discrepancy stems from a misalignment between the problem-solving framework, which is more general, and the need for specific numerical outputs or solutions pertinent to each problem scenario.\\n\\nThe feedback suggests more focused engagement with each problem's constraints and calculations to arrive at concrete solutions. \\n\\n- Feedback ID [0] expects a probability calculation yielding a sum of integers m+n = 67.\\n- Feedback ID [1] specifies a need to calculate a probability in a specific problem, with a final result of 931.\\n- Feedback ID [2] demands computation of a probability of selecting letters resulting in the final answer of 1/30.\\n- Feedback ID [3] involves solving a maximization problem under specified constraints with a result of 905.\\n- Feedback ID [4] requires identification of numbers from Pascal's triangle, concluding with finding the number 1001.\\n\\nFor all instances, the need is bridging between theoretical understanding and specific application with numerical outcomes.\",\n", + " \"answer\": \"The output lacks a numerical final answer across all learner instances. The correct values expected are:\\n1. 67\\n2. 931\\n3. 1/30\\n4. 905\\n5. 1001\",\n", + " \"suggestion\": {\n", + " \"str0\": \"To successfully modify the output based on learner feedback, ensure numerically precise and focused conclusions. The inputs need to adequately represent the problems and provide the necessary constraints or conditions to produce expected numeric solutions. This could include amending problem setups or data inputs that guide process flows and lead to direct calculations, ultimately converging towards the needed answer.\",\n", + " \"str1\": \"Close engagement with particular scenarios for permutations, probabilities, and combinatorial setups should be emphasized. Directly addressing problem contexts provided in messages within format and learner calls, converting theoretical methodologies into practical solutions, including using the right combination of provided inputs and necessary numeric manipulations to accurately compute the specific expected outputs. For checks, align outputs stringently to feedback expectations.\"\n", + " }\n", + "}\n", + "\u001b[93mProcessing beam 3/3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:27<00:00, 5.52s/it]\n", + "Generating 2 proposals for beam 3: 50%|█████ | 1/2 [00:12<00:12, 12.30s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"1. The instruction asks to change the values of variables to improve the output according to feedback.\\n\\n2. The feedback indicates the issues with each problem solution provided. Specifically:\\n- For ID [0], the probability calculation neglected the lattice's color structure. The ant can only land on point B with a probability of 1/4 due to its route options on the colored lattice.\\n- For ID [1], there was an incorrect calculation of card arrangements, indicating a need to refine the permutation strategy accounting for overcounts.\\n- For ID [3], there was a misunderstanding regarding the calculation of dimensional fitting resulting in an incorrect probability. Multiple configurations need to consider valid shared sets.\\n- For ID [4], the probability was miscalculated because successful selections were incorrectly noted.\\n\\n3. Changes to `str0` and `str1` aren't necessary since they provide the context or style for `format` function but don't directly address the issue in the logic or computations which are the sources of errors. Instead, helping to fix reasoning or adding checks can help in evaluating problems with refined logic.\",\n", + " \"answer\": \"Based on feedback, correct calculations are:\\n- ID [0]: Probability is 1/4\\n- ID [1]: Correct total is 52 arrangements\\n- ID [3]: Correct value for sum of numerator and denominator is 5\\n- ID [4]: Correct fraction is 1/30\",\n", + " \"suggestion\": {}\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 2 proposals for beam 3: 100%|██████████| 2/2 [00:19<00:00, 9.69s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"The problem involves changing the values of variables `str0` and `str1` to improve the output based on the feedback given. The code uses the `format` function and `Learner.call_llm` function, where the outputs depend on how accurately the problem statements are understood and processed. The feedback indicates that the outputs generated by the models are not aligning with the official correct answers for the given problems, and thus need to be revised. \\n\\n1. For the first LLM call (regarding the ant problem), the answer was supposed to recognize the even-odd structure of the lattice and use that to find the probability of 1/4, but it instead produced a complex explanation with no direct conclusion. To improve this, the input should better direct the model to focus on the parity aspect of the moves. \\n\\n2. For the card arrangement problem, the model generated 72 as the number of arrangements where 5 cards remain in order after removing one card, but the correct answer is 52. The model needs refined guidance to correctly count the unique arrangements possible. \\n\\n3. The handshake problem was correctly answered, so no change is needed. \\n\\n4. For the random box problem, the computation of probability and fitting arrangements seem flawed, with the official answer stating that the probability solution should lead to a final sum of 5 instead of 3. \\n\\n5. Lastly, the probability calculation from word selection is incorrect due to misdistribution of letter selections across given word sets, needing corrections in calculating successful outcomes more precisely.\",\n", + "\"answer\": \"Based on the problem's requirements and the feedback provided, here is what can be corrected:\\n\\n1. The probability for the ant problem should factor in the parity of moves affecting the final position, focusing on how the color or parity of dot influences his net movement. \\n\\n2. Amend counting strategy for card permutations by properly accounting for unique valid sequences.\\n\\n3. Address the dimension-fitting method in the box problem by ensuring all variable or size conditions are properly resolved.\",\n", + "\"suggestion\": {\n", + " \"str0\": \"For each modeling scenario, clarify conditions and ensure simple models can relate square position or logical outcomes clearly in solving lattice, permutation, and probability task assessments.\",\n", + " \"str1\": \"In solving these problems, highlight any unnoticed symmetry or parity aspect directly within logical reasoning, ensuring card arrangement and selection results align with intended permutations for correct model output alignment.\"\n", + "}\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 1/8: 100%|██████████| 5/5 [00:17<00:00, 3.44s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 1: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 2/8: 100%|██████████| 5/5 [00:28<00:00, 5.61s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 2: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 3/8: 100%|██████████| 5/5 [00:23<00:00, 4.61s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 3: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 4/8: 100%|██████████| 5/5 [00:15<00:00, 3.14s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 4: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 5/8: 100%|██████████| 5/5 [00:22<00:00, 4.51s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 5: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 6/8: 100%|██████████| 5/5 [00:27<00:00, 5.59s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 6: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 7/8: 100%|██████████| 5/5 [00:24<00:00, 4.89s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 7: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 8/8: 100%|██████████| 5/5 [00:33<00:00, 6.60s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 8: Validation score: 0.0000\u001b[0m\n", + "\u001b[92mSelected top 3 beams with scores: ['0.0000', '0.0000', '0.0000']\u001b[0m\n", + "\u001b[92mDepth 2 - Best validation score: 0.0000\u001b[0m\n", + "\u001b[94m\n", + "===== Beam Search Depth 3/4 with 3 beams =====\u001b[0m\n", + "\u001b[96mSampled validation minibatch of size 5 for depth 3\u001b[0m\n", + "\u001b[93mProcessing beam 1/3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:19<00:00, 3.81s/it]\n", + "Generating 2 proposals for beam 1: 50%|█████ | 1/2 [00:10<00:10, 10.73s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"The feedback highlights that each response fails to provide a specific numerical answer to the posed problems. The 'batchify24' output is essentially a collection of general strategies for solving mathematical problems rather than specific solutions to each prompt. Each Learner.call_llm invocation intends to solve a specific mathematical problem described by the corresponding 'message', but the current approach does not align these strategies with specific computational elements or mathematical formulas related to each problem. The key is to focus on numerical solutions using specific data given in each problem statement. The 'format' strings, composed from 'str1' and 'message', do not inject the required problem-solving logic since 'str1' is a generic problem-solving strategy. A change is needed for 'str1' to directly prompt specific computations for each mathematical problem described by 'message345' to 'message349'.\",\n", + " \"answer\": \"The batchify output should contain specific, computed numerical results for each individual mathematical problem that messages 345 to 349 describe.\",\n", + " \"suggestion\": {\n", + " \"str1\": \"Calculate the specific numerical solution for each problem: 1) message347: calculate number of valid card arrangements, 2) message349: calculate ways to make substitutions and find remainder, 3) message346: calculate distinct collections of letters, 4) message345: calculate number of fish on May 1, 5) message348: determine the last locker number opened.\"\n", + " }\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 2 proposals for beam 1: 100%|██████████| 2/2 [00:11<00:00, 5.99s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"The instruction asks for modifying the value of the variables in #Variables to improve the output according to the feedback provided. The feedback indicates that the outputs contain general strategies for problem-solving rather than specific numerical answers to the given mathematical problems. The variables str0 and str1 are used for creating formatted prompts for LLM models, which are then used to solve specific math problems described in message345 to message349. The current values of str0 and str1 imply a need for a logical structure and verification, but they do not directly guide the model to solve specific problems or reach numerical answers.\\n\\nTo address the feedback, we need to set str0 and str1 to suggest focusing on solving each specific math problem, performing required calculations, and determining numerical answers:\\n\\n- For str0, since it prompts the system with a goal, it should be aimed at solving mathematical problems and providing numerical answers.\\n- For str1, as it's used to format direct prompts for solving problems, it should encourage specific problem-solving and computation.\\n\\nThese changes should guide the model towards calculating and outputting specific numerical answers for each problem.\",\n", + " \"answer\": \"The given feedback indicates errors due to missing numerical results for each math problem. The changes needed should focus on directing the model to calculate specific results for each problem using proper computations.\",\n", + " \"suggestion\": {\n", + " \"str0\": \"Direct the model to solve the specific math problems provided, calculate the required results, and output precise numerical answers for verification.\",\n", + " \"str1\": \"Focus on computing the specific solutions to given problems using mathematical and logical processing, ensuring that the final result is a clear numerical answer.\"\n", + " }\n", + "}\n", + "\u001b[93mProcessing beam 2/3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:31<00:00, 6.23s/it]\n", + "Generating 2 proposals for beam 2: 50%|█████ | 1/2 [00:08<00:08, 8.62s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"1. The instruction asks to change the values in #Variables to improve the output in accordance to #Feedback. This involves analyzing why the current variables lead to incorrect answers and adjusting them. 2. The feedback indicates that the student's answers in the batchify26 output do not match the expected outputs for the specific mathematical problems mentioned in the inputs associated with the call_llm functions. The variables str0 and str1 set the context for the logical and systematic solving of the problems, but they appear to not directly address the individual computation requirements of the math problems stated in the Inputs section. 3. Suggestions for changes need to focus on aligning str0 and str1 more closely with the exact requirements of the individual mathematical problems. This includes specifying more directly how to use combinatorial and symmetrical logic specific to arranging cards, handling substitutions, calculating fish population, etc., based on the description of the specific problem constraints.\",\n", + " \"answer\": \"The current Incorrect Feedback indicates a need for a more precise rendering of str1 to deal directly with the experimental mathematical context.\",\n", + " \"suggestion\": {\n", + " \"str0\": \"Re-solve each unique problem by focusing on combinatorial logic specific to each task. Analyze patterns of assignments and orderings in arrangements.\",\n", + " \"str1\": \"Apply precise calculations to distinct mathematical problems, characterizing each by its own set of operations in context. Focus on exact policy for numeric conclusions depending on specified scenarios, adjusting indistinguishable logic.\"\n", + " }\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 2 proposals for beam 2: 100%|██████████| 2/2 [00:11<00:00, 5.95s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"1. The instruction tells us to change the values of variables in #Variables to improve the output based on #Feedback. 2. The feedback indicates that the current output addressed the wrong problems in each section and hence the final answers do not match the expected results. For example, it mentions section outputs unrelated answers to the math problem that were intended related to card arrangements, substitutions, triangle colorings, and others. 3. Given the problem descriptions and #Documentation, it is necessary to adjust the templates in the variables str0 or str1 so that the prompts generated for the LLM correctly address the intended problems associated with the messages 350 to 354. This may involve explicitly focusing on the exact mathematical operations needed, like permutation, combination, or modular arithmetic, as these seem to be relevant based on the types of equations and results given in the Feedback.\",\n", + "\"suggestion\": {\n", + " \"str0\": \"To solve each problem, focus on the exact numeric solutions by calculating distinct arrangements and using modular arithmetic as needed. For the card arrangement problem, determine ascending or descending sequences where one card is removable; for the locker problem, identify perfect squares; for the substitution problem, find series sums modulo 1000; for the triangles, calculate color combinations; for the fish population, solve for proportions. Ensure step-by-step alignment with the stated mathematical operations, leading to final answers consistent with expected outputs.\",\n", + " \"str1\": \"Base solutions directly on numeric calculations using appropriate combinatorial logic and modular arithmetic. For card arrangements, verify ascending and descending patterns per card removal; in lockers, rely on perfect square evaluation; in substitutes, sum series to modulo 1000; in triangles, multiply color pattern options; and in fish population, correlate tagged ratios to total estimates accurately. Carefully follow each problem's instruction for achieving final detailed numeric results.\"\n", + "}\n", + "}\n", + "\u001b[93mProcessing beam 3/3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:21<00:00, 4.29s/it]\n", + "Generating 2 proposals for beam 3: 50%|█████ | 1/2 [00:06<00:06, 6.60s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"1. The instruction asks to adjust the values in #Variables to improve the output, i.e., ensure the logic in the code correctly addresses the given problems. 2. The feedback indicates that the current code execution does not correctly address the distinct mathematical problems described in the inputs. The feedback suggests that the current solutions are not providing specific numerical answers aligned with official answers, and the prompts given to the models are not specific to each problem. 3. The suggestion involves adjusting the `str0` and `str1` variables to tailor the LLM calls specifically towards generating answers or calculations relevant to each problem, so each LLM call can potentially produce outputs more aligned with the expected mathematical solutions. This includes modifying the prompts to focus on solving each problem individually.\",\n", + " \"answer\": \"The current formatting and prompts are too general and do not solve the specific problems defined by each message. They do not generate targeted solutions or analyses specific to the problem instances.\",\n", + " \"suggestion\": {\n", + " \"str0\": \"Please solve the following specific problems using relevant combinatorial logic: 1. Arrange cousins in identical rooms. 2. Calculate probability in a sequence using bubble sort. 3. Calculate probability of an ant on a lattice. 4. Determine positions of switches. 5. Arrange cards in a row allowing for one removal.\",\n", + " \"str1\": \"Please solve each problem by finding specific arrangements or probabilities: 1. Cousins in identical rooms given specific constraints. 2. Sorting sequence and probabilities pertaining to bubble sort. 3. Lattice traversal probabilities. 4. Switch positions through divisor step analysis. 5. Card arrangements allowing for one removal.\"\n", + " }\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 2 proposals for beam 3: 100%|██████████| 2/2 [00:09<00:00, 4.88s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"The instruction asks to change the values of the variables in #Variables to improve the output according to the #Feedback. The #Feedback indicates that the current responses do not address the specific mathematical problems in the corresponding 'message' variables (message355, message356, etc.) or provide the correct outputs. The functions call_llm with different ModelWrapper instances and user promts formatted by variables like message355 are supposed to calculate or solve these mathematical problems, but they don't return the expected results. The main issue is that the input variables str0 and str1 are not directly related to the specific questions or problems outlined in the messages. Therefore, to improve the output, str0 and str1 need to be more relevant or contextually linked to the mathematical problems described in the messages. This will enhance the prompt used by the call_llm function, potentially leading to the correct solutions. However, without explicit connection of str0 and str1 to the specific problems described, it's challenging to determine what content should be used in str0 and str1. A possible approach is to customize these prompts with problem-solving strategies, hints, or instructions more directly related to the respective mathematical problems, ensuring the prompts generated in the format function guide the LLM toward the correct answers.\",\n", + " \"answer\": \"The answer is not explicitly given, but the general solution requires customizing str0 and str1 with problem-specific content.\",\n", + " \"suggestion\": {\n", + " \"str0\": \"To solve the mathematical problem effectively, focus specifically on the details and constraints described, applying relevant combinatorial and mathematical principles.\",\n", + " \"str1\": \"Concentrate on the problem's requirements, considering factors like symmetry, arrangements, and possible constraints to divide and conquer the task.\"\n", + " }\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 1/9: 100%|██████████| 5/5 [00:04<00:00, 1.22it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 1: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 2/9: 100%|██████████| 5/5 [00:35<00:00, 7.03s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 2: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 3/9: 100%|██████████| 5/5 [00:18<00:00, 3.73s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 3: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 4/9: 100%|██████████| 5/5 [00:20<00:00, 4.03s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 4: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 5/9: 100%|██████████| 5/5 [00:36<00:00, 7.22s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 5: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 6/9: 100%|██████████| 5/5 [00:32<00:00, 6.42s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 6: Validation score: 0.2000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 7/9: 100%|██████████| 5/5 [00:29<00:00, 5.91s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 7: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 8/9: 100%|██████████| 5/5 [00:22<00:00, 4.47s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 8: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 9/9: 100%|██████████| 5/5 [00:20<00:00, 4.05s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 9: Validation score: 0.0000\u001b[0m\n", + "\u001b[92mSelected top 3 beams with scores: ['0.2000', '0.0000', '0.0000']\u001b[0m\n", + "\u001b[92mDepth 3 - Best validation score: 0.2000\u001b[0m\n", + "\u001b[94m\n", + "===== Beam Search Depth 4/4 with 3 beams =====\u001b[0m\n", + "\u001b[96mSampled validation minibatch of size 5 for depth 4\u001b[0m\n", + "\u001b[93mProcessing beam 1/3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:30<00:00, 6.14s/it]\n", + "Generating 2 proposals for beam 1: 50%|█████ | 1/2 [00:13<00:13, 13.36s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"The instruction requires adjusting variable values to improve the output based on the feedback provided. The feedback indicates that the outputs from the code are currently incorrect, and each learner's process appears to answer different questions than intended. For example, the learner's response about counting indistinguishable triangles was criticized for being irrelevant and an alternative approach was suggested. The suggestion involved calculating combinations of colors for the triangles' corners and multiplying these by the number of choices for the center triangle.\\n\\nSimilarly, the learners' attempts to solve other problems, like the probability or the final locker number, didn't correctly address the key elements or calculations demanded by these questions. \\n\\nThe code constructs user prompts using 'str0' and 'str1,' which are then supposed to represent the system and user prompts for the calls to the models. It seems these prompts aren't contextualizing the problem or pointing the LLM to the specific conceptual elements needed to solve the unique problems. Therefore, the answers end up off-mark according to the feedback.\\n\\nAdjusting 'str0' and 'str1' to match the correct logic pattern required for each problem may lead to better contextual responses from the models. Specifically, aligning 'str1' towards more elaborative, problem-specific conditions might help the LLM generate correct solutions.\",\n", + "\"suggestion\": {\n", + " \"str0\": \"Each problem needs a distinct solution: calculate distinguishable triangles based on color configurations for their corners and center triangle, compute Locker 342's toggling sequence, refine the probability structure for r_{20}'s position swap, and reconsider the probability of ant's path after 5 moves.\",\n", + " \"str1\": \"Directly apply combinatorial logic to each problem: Utilize distinct problem-tailored prompts to achieve precise and contextualized LLM outputs addressing scenarios such as triangle configurations, locker toggling behavior, order probabilities, and ending positions.\"\n", + "}\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 2 proposals for beam 1: 100%|██████████| 2/2 [00:14<00:00, 7.01s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"1. The instruction requires adjusting variable values to improve output, which means aligning them with correct understanding and context of the problem as per feedback. 2. The feedback highlights that the current outputs do not match the official correct answers, indicating that the logical approach or interpretation was incorrect for each problem instance. Diagnosis of each problem's logical resolution needs rectification to conform to intended problem constraints. 3. Suggestions will focus on aiding the numerical alignment of context-induced adjustments per model's requirements. - For str0 and str1, the variable values are elaborate problem descriptions that seem to not directly connect to model responses expected, indicating excessive skeleton logic. Adjustments must hone on the precise factoring detail required for models concerning specific construct scenarios rather than excess narrative regurgitation. Each Learner.call_llm.response mismatches feedback clarity, with adjustments needed to submit appropriately line-precise query inputs aligning with decimalized outputs required (e.g., permutation representations or iterative derivatives). These set conjectures require direct inputs reducing excess extraneous reasoning. Expectations adjust per slight error logic on grounding problem factors (i.e., refined multiset/permutation blend logic must be regularized without straying interpretations). Therefore, providing direct numeric values/solutions should follow minimal context: 4 or 6 steps, highest locker open, and so forth pediatric constrained placement aligned to stir context aptly—check correct numeric correlation displays per evaluated category tallying (without adding digressions). Batched solutions adopting main prompt expectations conciliates direct specificity and will be necessary monitoring yield check for str1,\", \n", + "\"suggestion\": {\n", + " \"str0\": \"Focus meticulously on numerical results aligning to prompt specifics inquired, ensuring model proper tally via binomial or multiset logic specificities without diverged temporary discrepancy elaborations.\",\n", + " \"str1\": \"Apply logic that strictly tags permutations, specific models, inductive color pairings in patterns maximizing accuracy across finalized locker statuses and query-specific results.\"\n", + "}\n", + "}\n", + "\u001b[93mProcessing beam 2/3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:06<00:00, 1.34s/it]\n", + "Generating 2 proposals for beam 2: 50%|█████ | 1/2 [00:06<00:06, 6.47s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"1. The instruction directs to adjust the variable values to improve code output based on feedback. 2. Feedback indicates that the current output is incorrect because the students' responses ask for problems rather than calculating and providing numerical answers to the questions posed in the inputs. The code generated by `format` isn't working as expected to communicate instructions for solving the problems. The current input variables, `str0` and `str1`, are meant to direct Systems using the `Learner.call_llm` to handle math problem solutions, but they must be more directed to achieve the intended outcome of calculating answers rather than prompting for problems. 3. Suggested changes: Adjust `str0` and `str1` to explicitly request that the system calculate the answers to the provided prompts. Expected results: Explicit instructions should lead to a numerical answer output. The completion would then address the math problems directly, providing solutions rather than asking for more problems.\",\n", + "\"answer\": null,\n", + "\"suggestion\": {\n", + " \"str0\": \"Calculate the final numerical answers for the provided math problems and ensure to output them accurately.\",\n", + " \"str1\": \"Execute the calculations using mathematical and logical processing, ensuring the output is the final numerical answer for each provided problem.\"\n", + "}\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 2 proposals for beam 2: 100%|██████████| 2/2 [00:07<00:00, 3.74s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"1. The #Instruction stipulates modifying the values in #Variables to improve the outputs based on the #Feedback provided.\\n\\n2. The #Feedback consistently points out that the responses (outputs) do not contain the required specific numerical answers or results matching the official correct answers, which are needed for proper evaluation.\\n\\n3. The current #Code uses system and user prompts (str0 and str1 replaced values) to generate outputs with Learner.call_llm, but these outputs lack the actual computations needed for each mathematical problem described in #Inputs.\\n\\n4. Both str0 and str1 are descriptions for mathematical problem-solving, and str1 already emphasizes providing a numerical answer. To align directly with the feedback, str0 and str1 need to reflect actions ensuring that the actual computations are returned, not just a solicitation for math problems.\\n\\n5. Suggesting changes involves ensuring that one of these variables reflects the importance of providing the final numerical answer required in response, such as by including a succinct directive for generating specific solution outputs based on the problem mentioned in the variable messages.\",\n", + "\"suggestion\": {\n", + " \"str0\": \"Upon receiving a math problem, compute the specific and precise numerical result based on problem instructions and assumptions given.\",\n", + " \"str1\": \"For each given math problem, determine the comprehensive steps to arrive at a precise numerical final answer and present it clearly.\"\n", + "}\n", + "}\n", + "\u001b[93mProcessing beam 3/3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:23<00:00, 4.68s/it]\n", + "Generating 2 proposals for beam 3: 50%|█████ | 1/2 [00:20<00:20, 20.76s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"The #Instruction requires improving the output based on #Feedback. The #Feedback for each problem outlines specific misinterpretations or missing calculations. Thus, the variables str0 and str1 need adjustments to lead to correct problem solving and outputs.\\n\\nIn general, the provided prompts in str1 don't align specifically with the detailed calculations needed for each question, leading to unnecessary generic outputs and incomplete computation. Adjustments should ensure that the prompts involve calculations and constraints specific to the inputs provided.\\n\\nFor example, str1 should be adjusted to directly instruct calculations for messages 415 to 419 based on known mathematical solutions (like combinations, permutations, probability, etc.).\\n\\nI will adjust str1 to reflect specific problem-solving instructions related to each message number (message415 to message419) as noted in #Feedback for the correct calculation method and instructive detail for each separate problem.\",\n", + " \"answer\": \"To improve the calculations, adjust str1 to precisely target the expected mathematical solutions as directed by #Feedback.\",\n", + " \"suggestion\": {\n", + " \"str0\": \"Rely on specific mathematical reasoning for each problem, ensuring the system prompt aligns with expected calculations and final conclusions.\",\n", + " \"str1\": \"Calculate the mathematical solutions for each problem: 1) Calculate the probability of selecting the letters forming 'PROBLEM' from given sets, 2) Determine the combinations of letters in constrained sets like 'MATHEMATICS', considering indistinguishability, 3) Compute the probability of the ant's specific moves over a lattice, 4) Apply modular arithmetic to understand the sequence of locker actions, and 5) Solve based on handshake problems in combinatorics for team arrangements.\"\n", + " }\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 2 proposals for beam 3: 100%|██████████| 2/2 [00:22<00:00, 11.44s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"1. The #Instruction asks for changing the values in #Variables to improve the output according to #Feedback.\\n2. The #Feedback gives specifics about what is expected for each problem presented in the #Outputs. For instance, in ID [0], the correct approach is calculating the probabilities for Joe's selections from words CAMP, HERBS, and GLOW. Similarly, in ID [1], it's about calculating the number of distinct letter collections in MATHEMATICS. The feedback clarifies the expected outcomes and provides official answers, like a probability of 1/30 or a total of 72 distinct letter collections.\\n3. Based on the #Feedback, each problem in the #Output needs a tailored approach:\\n - For ID [0], we can improve by ensuring to compute the probability of forming the word PROBLEM based on specific selections from CAMP, HERBS, and GLOW. Given message415, this requires calculating the probability of selecting the requisite letters from each word, with the expected probability being 1/30.\\n - For ID [3], the expected answer is that the last locker opened is 342, not 961. This involves understanding the pattern of the student's locker problem and correcting the strategy for toggling lockers.\\nTherefore, setting 'str0' and 'str1' more explicitly towards achieving these calculations is likely the focus.\", \n", + " \"answer\": null,\n", + " \"suggestion\": {\n", + " \"str0\": \"Please calculate the probability that Joe selects 'P', 'R', 'O', 'B', 'L', 'E', 'M' from the given letters in CAMP, HERBS, and GLOW in that specific order. This should result as a common fraction denoting the probability, ensuring it results in 1/30.\",\n", + " \"str1\": \"Calculate and ensure distinct mathematical solutions for: 1) number of valid card arrangements, 2) calculating replacements and remainders, 3) distinct letter collections focusing on MATHEMATICS letters falling off, 4) number of fish change analysis instead of last locker, and 5) evaluate last locker opened as locker 342.\"\n", + " }\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 1/9: 100%|██████████| 5/5 [00:16<00:00, 3.39s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 1: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 2/9: 100%|██████████| 5/5 [00:35<00:00, 7.04s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 2: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 3/9: 100%|██████████| 5/5 [00:32<00:00, 6.55s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 3: Validation score: 0.2000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 4/9: 100%|██████████| 5/5 [00:14<00:00, 2.92s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 4: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 5/9: 100%|██████████| 5/5 [00:08<00:00, 1.73s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 5: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 6/9: 100%|██████████| 5/5 [00:06<00:00, 1.34s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 6: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 7/9: 100%|██████████| 5/5 [00:17<00:00, 3.40s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 7: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 8/9: 100%|██████████| 5/5 [00:24<00:00, 4.81s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 8: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 9/9: 100%|██████████| 5/5 [00:33<00:00, 6.72s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 9: Validation score: 0.0000\u001b[0m\n", + "\u001b[92mSelected top 3 beams with scores: ['0.2000', '0.0000', '0.0000']\u001b[0m\n", + "\u001b[92mDepth 4 - Best validation score: 0.2000\u001b[0m\n", + "\u001b[96m\n", + "Best parameters at depth 4:\u001b[0m\n", + "\u001b[96mstr:0: Solve each problem by directly calculating distinct answers. For the object reaching (2,2) in steps, determine all ways in 4 or 6 steps. For the lockers, trace each open-close step carefully until locker 342 is the last. For gymnasts, calculate combinations where total is precisely 281, minimizing coach handshakes. For cousins, enumerate placement variants for each room combination. For letters, determine indistinguishable combinations focusing on T's, M's, and A's.\u001b[0m\n", + "\u001b[96mstr:1: Focus directly on providing numeric answers by applying combinatorics, symmetry, and dimensions fitting logic specific to the distinct problem being queried, ensuring detailed, step-by-step solutions addressing the exact scenarios described.\u001b[0m\n", + "\u001b[96m\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating best parameters at depth 4 on test set: 100%|██████████| 10/10 [01:00<00:00, 6.03s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[95mDepth 4 - Test score: 0.0000\u001b[0m\n", + "\u001b[94m\n", + "===== Final Selection Using Full Validation Set =====\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 1/3: 100%|██████████| 20/20 [01:48<00:00, 5.45s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 1: Validation score: 0.0500\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 2/3: 100%|██████████| 20/20 [01:09<00:00, 3.46s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 2: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 3/3: 100%|██████████| 20/20 [02:31<00:00, 7.58s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 3: Validation score: 0.0500\u001b[0m\n", + "\u001b[92mSelected top 1 beams with scores: ['0.0500']\u001b[0m\n", + "\u001b[95m\n", + "===== Final Proposal Candidate Parameters =====\u001b[0m\n", + "\u001b[94mstr:0: Solve each problem by directly calculating distinct answers. For the object reaching (2,2) in steps, determine all ways in 4 or 6 steps. For the lockers, trace each open-close step carefully until locker 342 is the last. For gymnasts, calculate combinations where total is precisely 281, minimizing coach handshakes. For cousins, enumerate placement variants for each room combination. For letters, determine indistinguishable combinations focusing on T's, M's, and A's.\u001b[0m\n", + "\u001b[94mstr:1: Focus directly on providing numeric answers by applying combinatorics, symmetry, and dimensions fitting logic specific to the distinct problem being queried, ensuring detailed, step-by-step solutions addressing the exact scenarios described.\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating best beam on test set: 100%|██████████| 10/10 [00:54<00:00, 5.48s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[92mBEST BEAM - Test score: 0.0000\u001b[0m\n", + "\u001b[94m\n", + "===== Periodic Test Scores Summary =====\u001b[0m\n", + "\u001b[96mDepth 1: Test score = 0.2000\u001b[0m\n", + "\u001b[96mDepth 4: Test score = 0.0000\u001b[0m\n", + "FINISHED TRAINING BEAM SEARCH\n", + "\n", + "Best validation scores at each depth:\n", + " Depth 1: 0.6000\n", + " Depth 2: 0.0000\n", + " Depth 3: 0.2000\n", + " Depth 4: 0.2000\n", + "Final score: 0.0\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "algorithm = BeamsearchAlgorithm(\n", + " agent=agent,\n", + " optimizer=optimizer,\n", + " logger=logger,\n", + " num_threads=train_params[\"num_threads\"]\n", + " )\n", + "\n", + "async def wrapper():\n", + " print(\"STARTING TRAINING BEAM SEARCH\")\n", + " metrics, final_score = algorithm.train(**train_params)\n", + " print(\"FINISHED TRAINING BEAM SEARCH\")\n", + "\n", + " if 'best_validation_scores' in metrics:\n", + " print(\"\\nBest validation scores at each depth:\")\n", + " for depth, score in enumerate(metrics['best_validation_scores']):\n", + " print(f\" Depth {depth+1}: {score:.4f}\")\n", + " \n", + " print(\"Final score: \", final_score)\n", + " \n", + "asyncio.run(wrapper())" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "STARTING TRAINING BEAM SEARCH w/ HISTORY\n", + "\u001b[94mRunning BeamsearchHistoryAlgorithm with beam_width=3, max_depth=4, max_history_size=2\u001b[0m\n", + "\u001b[94mUsing validation_dataset_size=5 for intermediate evaluations\u001b[0m\n", + "\u001b[94m\n", + "===== Evaluating Initial Parameters =====\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating initial parameters on test set: 100%|██████████| 10/10 [00:59<00:00, 5.95s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[93mInitial test score: 0.0000\u001b[0m\n", + "\u001b[94m\n", + "===== Beam Search Depth 1/4 with 1 beams =====\u001b[0m\n", + "\u001b[96mSampled validation minibatch of size 5 for depth 1\u001b[0m\n", + "\u001b[93mProcessing beam 1/1\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (beam 1, batch size: 5): 100%|██████████| 5/5 [00:30<00:00, 6.03s/it]\n", + "Generating 2 proposals for beam 1 (with history): 100%|██████████| 2/2 [00:18<00:00, 9.20s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"1. The instruction requires modifying the values of the variables in #Variables to improve the output. 2. Based on the feedback, it is evident that the calculations or expected outputs do not match the Official Correct Answer. Many of the provided answers do not align with the expected numbers such as m+n = 67, which appears to relate to reaching (2,2) in six or fewer steps in a given problem (assuming the task requests this directly by formula derivation), probability of being at dot B is 1/4 for the ant problem, 72 for the collection of letters problem, 560 for the sequence problem, and 336 for the distinguishable triangles. All these need revisiting. 3. The feedback indicates each computation feature problems deviating from core constraints needed or crossing excess assumptions weakening finite numerical submission. 4. Suggestion: I suggest refining str0 and str1 further to focus on precise calculation statements addressing each scenario uniquely but correctly. Using simplified probability formulation or exact factorial derivation honing mismatches until respective outcomes align, then reconfirm calculations/problems recognizing chance at dot B resultant impact after 5 moves, distinct subsequence occurrences probability in trials within locks or combined permutations of space arrangements aligning single logic procedural correlation establish comprehensive boundaries.\",\n", + "\"answer\": \"\",\n", + "\"suggestion\": {\n", + " \"str0\": \"Start by addressing the key combinatorial or probability problems described. For instance, consider only operations, calculations needed, exact position probability for objects without further redundancy - i.e. distinct steps, adjacent counting with implied locking sequences accurately deriving odds satisfied, arrange magnets in known conditions for distinct number collections using factorial methods ensuring results matching output findings.\",\n", + " \"str1\": \"Review precisely derived results, analyzing combinatorial/geometry fitting descriptions providing incremental measures incrementing geometries by direct summation, probability calculations summing rational scenarios noted for different problems including equals achieving desired arrangement interacting calculated sequences, verifying final auxiliary operation reversals.\"\n", + "}\n", + "}\n", + "LLM response:\n", + " {\n", + " \"reasoning\": \"1. #Instruction asks for changes in #Variables to improve the output based on #Feedback, which indicates that the current output is incorrect and the main task is to identify which values influence the wrong output and adjust them accordingly.\\n\\n2. #Feedback suggests that the solutions provided in the output did not match the expected results based on specific combinatorial problems. Specifically, for the problem related to reaching point (2,2), a specific numerical solution was expected but not provided. The feedback mentions an expectation for calculating probabilities and numerical results that align with official answers such as m+n=67 for certain scenarios. Similarly, for other problems, specific answers were expected, like certain probabilities or counts, which were not delivered by the current reasoning in the output.\\n\\n3. The #Variables 'str0' and 'str1' provide contextual explanations or prompts used in the code. The feedback mentions aiming for probabilities and precise combinatorial solutions, suggesting that these descriptions may not emphasize the correct method or thought process needed to guide the model (Learner.call_llm) correctly toward the specified outputs.\\n\\n4. By understanding what the official solutions require and how the current format strings ('str0' and 'str1') might set up the problem incorrectly, we can revise these prompts to better focus on the correct combinatorial or probability analyses and thus achieve the expected results.\",\n", + " \"answer\": null,\n", + " \"suggestion\": {\n", + " \"str0\": \"For each mathematical scenario, calculate the precise probability or combinatorial result by analyzing the given conditions. Ensure all outcomes match expected numerical results such as m+n=67, exact handshake probabilities, and specific distinguishable counts based on provided parameters.\",\n", + " \"str1\": \"Use mathematical rigor to solve problems by focusing on combining correct probability distributions, exact permutations, and alignment with official results for each described scenario, incorporating precise steps for calculation adherence.\"\n", + " }\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 1/3: 100%|██████████| 5/5 [00:19<00:00, 3.81s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 1: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 2/3: 100%|██████████| 5/5 [00:22<00:00, 4.50s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 2: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 3/3: 100%|██████████| 5/5 [00:30<00:00, 6.14s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 3: Validation score: 0.0000\u001b[0m\n", + "\u001b[92mKeeping all 3 candidates as num_candidates <= beam_width. Scores: ['0.0000', '0.0000', '0.0000']\u001b[0m\n", + "\u001b[92mDepth 1 - Best validation score: 0.0000\u001b[0m\n", + "\u001b[94m\n", + "===== Beam Search Depth 2/4 with 3 beams =====\u001b[0m\n", + "\u001b[96mSampled validation minibatch of size 5 for depth 2\u001b[0m\n", + "\u001b[93mProcessing beam 1/3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (beam 1, batch size: 5): 100%|██████████| 5/5 [00:21<00:00, 4.35s/it]\n", + "Generating 2 proposals for beam 1 (with history): 50%|█████ | 1/2 [00:05<00:05, 5.57s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"1. The instruction asks for adjusting variable values to improve the output, which is incorrect based on the feedback provided. 2. The feedback specifies that the outputs did not match the expected results due to focusing on unrelated scenarios, meaning the logic or context applied to solve the problems was incorrect for the specific problems presented. 3. The suggestion is to refine the variable values based on each specific problem, mainly focusing on correctly understanding and applying mathematical principles pertinent to each problem outlined in the messages. This implies ensuring solution context aligns exactly with each problem being solved (e.g., ant movement probabilities, switch positions, soccer substitutions).\",\n", + "\"answer\": \"n/a\",\n", + "\"suggestion\": {\n", + " \"str0\": \"A detailed mathematical analysis is needed for each problem to apply correct combinatorial principles. For example, interpret switch advancement in terms of greatest common divisor counts, enumerate valid step sequences for the ant movement problem to fractionally compute a specific path probability, and compute exact configurations or permutations for substitution problems.\",\n", + " \"str1\": \"Refocus specifically on solving defined mathematical scenarios using precise concepts like calculating remainder for permutations, probability distribution examination for movement tasks, and opening sequences to define locker task outcomes congruent with given scenarios.\"\n", + "}\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 2 proposals for beam 1 (with history): 100%|██████████| 2/2 [00:16<00:00, 8.43s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"1. The #Instruction asks to modify the values in #Variables section to improve the output in accordance with the #Feedback. \\n\\n2. The #Feedback section provides information for each problem, indicating that the student's answers were incorrect and suggests how the solutions could be approached correctly. Each problem feedback has been centered on the fact that the student's final answers did not address the specific problem at hand or the numerical solution required. \\n\\nIn particular for this execution:\\n - ID [0]: This involves the problem with ant's movements and the dots, the focus should be on the fact there are four blue dots, using which we determine the probability of ending on any specific dot (in this case, labeled $B$) as 1/4. \\n - ID [1]: This feedback is about the problem involving the switch positions. The correct approach is to use the divisor counting rule.\\n - ID [2]: This involves calculating the number of substitution methods and requires the student to establish a recursive relationship through combinatorial reasoning for possible substitutions. \\n - ID [3]: The feedback involves a probability problem where you need to verify calculations for probability steps, ensuring you get a simplified fraction and consequently sum its numerator and denominator to a value of 65.\\n - ID [4]: This explains a problem where locker sequences are tracked using pattern rules, where the last locker opened is expected to be 342.\\n\\n3. The #Constraints section is empty, suggesting no explicit constraints were given and decisions should be based on problem context. #Code shows calls to create string prompts for various models and how the results are concatenated into batchify38.\\n\\n4. Updated #Variables suggestions:\\n - str0: Change to focus directly on probability problems and combinatorial scenarios with relevant endpoints to guide LLM model towards specific results requested in Feedback.\\n - str1: Directly address calculation precision needed during combinatorial, permutation, and probability problem-solving, ensuring solutions match outcomes outlined in Feedback.\",\n", + " \"answer\": null,\n", + " \"suggestion\": {\n", + " \"str0\": \"For each specified probability or combinatorial task, compute the exact results by thoroughly analyzing provided scenarios and numerical outcomes, ensuring alignment with expected problem conditions such as precise path counts, probability distributions, and permutations.\",\n", + " \"str1\": \"Apply rigorous mathematical reasoning to each problem scenario, focusing on precise probability computation, specific combinatorial arrangements, and accurate problem-solving techniques for distinct outcomes, optimizing solutions for clarity and correctness.\"\n", + " }\n", + "}\n", + "\u001b[93mProcessing beam 2/3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (beam 2, batch size: 5): 100%|██████████| 5/5 [00:27<00:00, 5.40s/it]\n", + "Generating 2 proposals for beam 2 (with history): 50%|█████ | 1/2 [00:11<00:11, 11.16s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"The instructions indicate we need to change the values of the variables to adhere to the feedback. The feedback suggests that numerical answers are missing from the outputs, so for each prompt, a numeric finish or conclusion was expected but is absent. Reflecting on this, it seems that the problem lies in the content of 'str0' and 'str1'. Each format uses 'str1' combined with a specific 'message', but the functions called in the code emphasize presenting structured approaches instead of delivering conclusions or final numerical values. Therefore, the current strings don't guide the LLM models towards computing final answers. To remedy this, 'str0' and 'str1' should be reformulated to encourage a focus on final numeric result calculations corresponding to each problem's queries. By aligning the variables with content that drives precise numerical calculations, the desired outcome should be achieved. Expected outputs need to be precise results in numerical form corresponding to combinatorial, probability, or arrangement problems.\",\n", + "\"answer\": null,\n", + "\"suggestion\": {\n", + " \"str0\": \"Provide a precise numerical answer directly related to each problem posed. Evaluate operations, calculations, and apply probability principles or combinatorial strategies as needed to derive a concrete numeric result that satisfies the problem's constraints.\",\n", + " \"str1\": \"For each problem, derive a specific numeric conclusion, leveraging combinatorial, geometric, or probabilistic calculation methods as required. The goal is a definitive answer reflecting a thorough understanding of and correct application to the distinct problem context.\"\n", + "}\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 2 proposals for beam 2 (with history): 100%|██████████| 2/2 [00:12<00:00, 6.03s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"The instruction asks to improve the output by changing the values of the variables in #Variables. The feedback indicates that the outputs are incorrect because they do not provide final numerical answers to specific probability or counting problems described in the inputs. The current descriptions in str0 and str1 are too general and do not guide the models on how to generate the correct answers for the problems posed. To improve the output, str0 and str1 need to directly address the specific problems: \\n\\n1. For message580, compute the probability that two randomly interviewed students can provide information about both French and Spanish classes, considering set intersections.\\n2. For message581, calculate the largest number of elements in a subset S given constraints on differences between members.\\n3. For message582, determine the fewest handshakes a coach could have participated in given a total number of handshakes.\\n4. For message583, count how many switches remain in position A after a divisibility-based process on labeled switches.\\n5. For message584, calculate the number of ways substitutions can be made during a soccer game including specific constraints.\",\n", + " \"answer\": null,\n", + " \"suggestion\": {\n", + " \"str0\": \"Precisely solve each problem. For instance, calculate the probability of selecting a student in both French and Spanish for message580, determine the maximum subset size for message581, calculate the minimum coach handshakes for message582, count remaining switches in position A for message583, and find substitution permutations mod 1000 for message584.\",\n", + " \"str1\": \"Use detailed combinatorial and probability calculations to directly determine numerical answers matching each problem's constraints, verified through reevaluation of logical steps.\"\n", + " }\n", + "}\n", + "\u001b[93mProcessing beam 3/3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (beam 3, batch size: 5): 100%|██████████| 5/5 [00:30<00:00, 6.20s/it]\n", + "Generating 2 proposals for beam 3 (with history): 50%|█████ | 1/2 [00:09<00:09, 9.97s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"The instruction asks us to modify the values of variables to improve the output based on feedback. Each feedback corresponds to a distinct problem, and each problem corresponds to a specific message input in the code. For the given problems, we need to rethink the provided solutions and align them with the true answer as discussed in the feedback:\\n\\n1. For the problem about 'MATHEMATICS' and word arrangement, instead of the large number initially suggested, it should be verified that the answer is 72 by recalculating the ways to select and arrange the vowels and consonants as given.\\n\\n2. For the biologist fish problem, by recalculating the population estimates using proportions as explained in the feedback, adjusting for tagged and untagged fish under given conditions, the true initial population should be determined as 840.\\n\\n3. For the set problem involving no two numbers differing by 4 or 7, as explained, the count through proper combinatorial selection within constraints should yield a result of 905.\\n\\n4. For the problem about cousins and room arrangements, focusing only on valid combinations without incorrect assumptions should yield an official result of 15.\\n\\n5. Lastly, for the soccer substitution problem, careful calculation of possible substitution cases and summing them should yield an answer of 122.\\n\\nIn conclusion, we will implement adjustments in the string prompts for each relevant problem to reflect this accurate reasoning and obtain the correct outputs.\",\n", + " \"answer\": \"The adjustments are needed for each problem string associated with the described variables.\",\n", + " \"suggestion\": {\n", + " \"str0\": \"Use correct counting methods to verify or derive small, well-defined numerical results matching each problem's official answer.\",\n", + " \"str1\": \"Directly employ combinatorial and probability techniques to resolve and confirm problem constraints to reach established solutions like 72, 840, 905, 15, and 122.\"\n", + " }\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 2 proposals for beam 3 (with history): 100%|██████████| 2/2 [00:14<00:00, 7.46s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"The instruction requires adjusting the values in #Variables to improve the outputs in accordance with #Feedback. Analyzing the feedback, it becomes clear that certain calculations or solutions did not match official results or were addressed incorrectly. Specifically, problems were identified in the following areas:\\n\\n1. Problem related to 'MATHEMATICS': The feedback points out that the calculated distinct combinations were incorrect and highlights that the correct count should be 72.\\n2. Problem related to the fish in the lake: The correct calculated number of fish should have been 840, using the given percentages and proportional reasoning.\\n3. Problem related to subset S: The calculated number of elements mistakenly did not address the correct constraints leading to an incorrect solution.\\n4. Problem related to cousins and hotel rooms: Although no final answer was provided, the expected correct arrangement combinations lead to an answer of 15.\\n5. Problem related to soccer team substitutions: The expected correct answer was 122, following specific combinatorial approaches.\\n\\nThe responses did not correctly apply combinatorial logic or provide final answers for some scenarios. Errors likely arise from how specific descriptions in the prompts (#Variables) direct problem-solving processes.\\n\\nTo rectify issues, new, more accurate problem descriptions should direct responses to valid numeric conclusions. This involves more detailed, direct problem solving with concise numeric emphasis contextualized by placeholder adjustments to prompt accurate systemic logic.\",\n", + " \"answer\": null,\n", + " \"suggestion\": {\n", + " \"str0\": \"Start by addressing the key combinatorial or probability problems described. For instance, consider only operations, calculations needed, exact position probability for objects without further redundancy - i.e. distinct steps, adjacent counting with implied locking sequences accurately deriving odds satisfied, arrange magnets in known conditions for distinct number collections using factorial methods ensuring results matching output findings.\",\n", + " \"str1\": \"Revise each problem statement ensuring the execution of appropriate combinatorial or geometric steps correctly matching logical deductions. Ensure today's responses clearly translate solutions from mathematical analyses accurately and concisely across varied scenarios and verification of accurate configuration output as required by explained conditions, ensuring distinctness in positions or questions resolved by results improved.\"\n", + " }\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 1/9: 100%|██████████| 5/5 [10:40<00:00, 128.16s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 1: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 2/9: 100%|██████████| 5/5 [00:29<00:00, 5.89s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 2: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 3/9: 100%|██████████| 5/5 [00:40<00:00, 8.12s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 3: Validation score: 0.2000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 4/9: 100%|██████████| 5/5 [00:19<00:00, 3.86s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 4: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 5/9: 100%|██████████| 5/5 [00:40<00:00, 8.15s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 5: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 6/9: 100%|██████████| 5/5 [00:27<00:00, 5.45s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 6: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 7/9: 100%|██████████| 5/5 [00:29<00:00, 5.87s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 7: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 8/9: 100%|██████████| 5/5 [00:29<00:00, 5.99s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 8: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 9/9: 100%|██████████| 5/5 [00:29<00:00, 5.90s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 9: Validation score: 0.0000\u001b[0m\n", + "\u001b[92mSelected top 3 beams with scores: ['0.2000', '0.0000', '0.0000']\u001b[0m\n", + "\u001b[92mDepth 2 - Best validation score: 0.2000\u001b[0m\n", + "\u001b[94m\n", + "===== Beam Search Depth 3/4 with 3 beams =====\u001b[0m\n", + "\u001b[96mSampled validation minibatch of size 5 for depth 3\u001b[0m\n", + "\u001b[93mProcessing beam 1/3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (beam 1, batch size: 5): 100%|██████████| 5/5 [00:30<00:00, 6.10s/it]\n", + "Generating 2 proposals for beam 1 (with history): 50%|█████ | 1/2 [00:12<00:12, 12.14s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"The instruction asks to change the variable values in #Variables to improve the output according to #Feedback. The feedback indicates that the issues arise from the provided answers not being specific to the problems each message635-message639 describe. Each message describes a distinct probability or combinatorial problem, yet the outputs are filled with general examples unrelated to these problems. The str0 and str1 variables are inputs for these messages, and they are too generic and don't guide the system's response towards the specific problem scenarios given in the messages. Changing these to specific guidelines related to each specific problem could direct the model towards more problem-specific outputs.\",\n", + " \"suggestion\": {\n", + " \"str0\": \"Calculate precise combinatorial or probability results specifically for the given scenarios.\",\n", + " \"str1\": \"For each specific scenario described, utilize exact mathematical techniques to produce the precise probability or count of outcomes, matching the problem's requirements given in detail.\"\n", + " }\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 2 proposals for beam 1 (with history): 100%|██████████| 2/2 [00:17<00:00, 8.85s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"1. The #Instruction asks to adjust the values of the variables in #Variables to improve the output based on #Feedback. 2. The #Feedback indicates that the current outputs do not answer the specific mathematical problems prompted by each unique message. Instead, the examples provided do not correspond to the problems stated by the messages in #Inputs. 3. The variables str0 and str1 in #Variables are system and user prompts that precede the presentation of each mathematical problem and do not actively engage in solving the specific problem statements. The default prompts are broad and do not relate directly to the details in each message input, leading to unrelated or incorrect outputs. 4. For example, in message635, the problem is about arranging four cousins in four identical rooms, requiring a combinatorial solution specific to that context (there are 15 ways distinct different ways), but the examples provided are about unrelated generic probability and combinatorics examples. 5. To improve alignment, str1 should be tailored to directly address the specific problem scenarios from each message input. However, str0 is detailed in complexity beyond the need for basic problem alignment. The suggestion is to adjust the str1 variable to include indications to address the specific problem scenarios presented in the messages, while str0 guides the overall problem-solving strategy.\",\n", + " \"answer\": \"N/A\",\n", + " \"suggestion\": {\n", + " \"str1\": \"Solve the specific problem: {message} using appropriate mathematical principles and provide the correct result ensuring adherence to the problem requirements and specifics.\"\n", + " }\n", + "}\n", + "\u001b[93mProcessing beam 2/3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (beam 2, batch size: 5): 100%|██████████| 5/5 [00:31<00:00, 6.34s/it]\n", + "Generating 2 proposals for beam 2 (with history): 50%|█████ | 1/2 [00:08<00:08, 8.63s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"The problem requires altering the variables to improve the output, which in this context relates to maximizing the effectiveness of the string prompts used in constructing format strings for calling LLM models. The feedback indicates that the outputs produced from call_llm methods are not providing numerical solutions or conclusions required by each specific problem they address. This suggests that the variable 'str0' or 'str1' used in the format method should specifically address the prompt requirements for each mathematical problem rather than using a generic mathematical reasoning prompt. Each message variable ('message640' to 'message644') corresponds to a different problem, as described in the feedback sections. Thus, a customized approach in formulating 'str1' could improve the relevance and accuracy of the outputs from the LLM model by ensuring the prompt directly relates to the problem specifics in 'message' variables.\",\n", + " \"answer\": \"Invalid responses across multiple outputs indicate the need for more targeted prompt formation to reflect the specific requirements of the individual math problems.\",\n", + " \"suggestion\": {\n", + " \"str1\": \"Focus on deriving concrete numerical solutions for individual mathematical problems—such as coin sequence possibilities, room distribution combinations, tagged fish population calculations, set element selections, and letter typing orders—by applying problem-specific combinatorial and probability principles.\"\n", + " }\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 2 proposals for beam 2 (with history): 100%|██████████| 2/2 [00:18<00:00, 9.06s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"The instruction asks us to modify the variables in #Variables to obtain a better output according to the #Feedback. The feedback indicates that all scenarios have been evaluated incorrectly. It is important to address each problem correctly with the appropriate method or calculation. \\n\\n- For batchify46 ID [0], the task involves counting sequences of coin tosses with specific subsequences. To fix the issue, we should follow the 'balls and urns' combinatorial method to determine valid arrangements for the given number of heads and tails in the sequences, ensuring to use the correct formula for combinatorial counting.\\n\\n- For batchify46 ID [1], the problem is to calculate the number of ways Matt's cousins can be accommodated in rooms, requiring a focus on arranging people in spaces. Employing a stars and bars or equivalent method yields the correct answer, accounting for overcounted options to reach a total of 15 distinct distributions.\\n\\n- For batchify46 ID [2], the student's focus should be on evaluating the lake fish problem through population dynamics analysis, outlined by utilizing sampling inference and application of proportions in environmental statistics.\\n\\n- For batchify46 ID [3], the task involves selecting a maximum subset from a set without violating constraints on differences. This problem revolves around modular arithmetic and optimizing choices within numbers, yielding the largest subset meeting the condition.\\n\\n- For batchify46 ID [4], it revolves around calculating typing order sequences, possibly by arranging elements via combinatorics, once exclusions and previous conditions are taken into factorization, especially how letter 9 affects upcoming arrangements, needed for `n` choices left.\\n\\nCorrecting variable definitions by ensuring clear alignment of prompts with task specificity will improve the results.\\n\",\n", + "\"answer\": \"\",\n", + "\"suggestion\": {\n", + "\"str0\": \"Compute combinatorial solutions distinctly for each problem scenario by ensuring mathematical principles remain pertinent to the problem context, such as subsequences in coin toss sequences, combinatorial configurations for population sampling requirements, arrangement complexity with spatial distribution consideration, maximum set subset considerations under constraints, and typing sequence potential after exclusionary movements.\",\n", + "\"str1\": \"Utilize accurate computational techniques to address each mathematical problem specifically, focusing on arrangement, subset calculations, population sampling, probability distributions, and how combination or permutation principles intelligently apply to single and multiple task scenarios.\"\n", + "}\n", + "}\n", + "\u001b[93mProcessing beam 3/3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (beam 3, batch size: 5): 100%|██████████| 5/5 [00:34<00:00, 6.89s/it]\n", + "Generating 2 proposals for beam 3 (with history): 50%|█████ | 1/2 [00:11<00:11, 11.78s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"1. The instruction requires modifying the variable values to improve the output based on feedback, which indicates errors due to the lack of a final numerical answer for each mathematical scenario. 2. The feedback highlights missing numerical answers for each complex problem such as arranging cousins, constructing triangles, positioning switches, etc. The Code utilizes str0 and str1 as prompts for learners to call an LLM with specific scenarios. The key is ensuring that these prompts contain sufficient direction and specificity to guide users to provide numerical solutions. 3. Given these insights, the values for str0 and str1 should be enhanced to incorporate clear guidance on calculating the final numerical results required for each scenario, such as the number of combinations, probabilities, and counting outcomes. These changes should help the learners focus on the mathematical computations needed, resulting in accurate outputs.\",\n", + " \"suggestion\": {\n", + " \"str0\": \"To precisely solve such mathematical scenarios, ensure to compute a final numerical result by thoroughly evaluating the givens. For example, when arranging indistinguishable items, count through each distribution case and sum their possibilities to achieve a definitive total. Similarly, for problems of remainder and probability, apply modulus operations effectively and cumulate probabilities. Ensure each scenario ends with a numerical result, facilitating straightforward comparisons with expected solutions.\",\n", + " \"str1\": \"Focus on resolving the mathematical scenarios with explicit numerical conclusions. Calculate the permutations or combinations required, finish with a remainder or probability outcome explicitly stated, and perform checks against anticipated solutions. In permissions or movement scenarios, follow precise combinatorial solving for counts, and ensure each scenario concludes with a directly quantified numerical outcome.\"\n", + " }\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 2 proposals for beam 3 (with history): 100%|██████████| 2/2 [00:14<00:00, 7.38s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"1. The instruction asks us to modify the given variables to improve the outcome based on the feedback provided. The feedback for each problem iteration suggests that the proposed examples discuss relevant concepts but fail to reach and specify the final answers expected for the given mathematical problems. For example, the feedback frequently points out the lack of final numerical answers, which is essential for alignment with the official solution. 2. The constraints of str0 are more descriptive and less action-oriented, whereas str1 discusses the application of specific concepts to solve different mathematical scenarios. Both variables str0 and str1 need precision in addressing the problem types directly presented by the message topics. However, reviewing the feedback, str1 seems broadly aligned with the instructions but needs specificity in solutions rather than concepts alone. 3. For improving the output and correctly solving the mathematical problems, both str0 and str1 need to be directly adjusted to ensure they align with the specific requirements of each problem, focusing on detailed step-by-step solutions ending with explicit numerical results as needed in the feedback. Thus, the revisions should guide toward systematic problem-solving resulting in accurate answer derivation. \\n\\nAdditionally, the feedback and pattern recognition along the variables and intermediate results suggest common combinatorial problems with outputs explicitly defined such as possible arrangements, remainder calculations, and probability evaluations. Providing clear and accurate problem-solving pathways toward these results is paramount.\",\n", + " \"answer\": \"TERMINATE\",\n", + " \"suggestion\": {\n", + " \"str0\": \"To solve complex mathematical problems, consider direct approaches like enumerating permutations, using combinatorial evidence supported by final accurate proofs. For tasks requiring modular artithmetic, identify effective residue systems. Further, probability tasks should involve detailed distribution assessments to ensure outcomes align with calculated paths or states, finally depicting numerical results.\",\n", + " \"str1\": \"Fully formulate mathematical scenarios to achieve final accurate results per problem's nature such as remaining permutations, switch cycles, or distinguishable combinations. Utilize crisply defined sequential solutions, ensuring prompt numeric conclusions match official predictions.\"\n", + " }\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 1/8: 100%|██████████| 5/5 [00:38<00:00, 7.63s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 1: Validation score: 0.2000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 2/8: 100%|██████████| 5/5 [00:08<00:00, 1.79s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 2: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 3/8: 100%|██████████| 5/5 [00:26<00:00, 5.20s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 3: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 4/8: 100%|██████████| 5/5 [10:42<00:00, 128.55s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 4: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 5/8: 100%|██████████| 5/5 [00:26<00:00, 5.25s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 5: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 6/8: 100%|██████████| 5/5 [00:27<00:00, 5.58s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 6: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 7/8: 100%|██████████| 5/5 [00:23<00:00, 4.61s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 7: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 8/8: 100%|██████████| 5/5 [00:38<00:00, 7.75s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 8: Validation score: 0.0000\u001b[0m\n", + "\u001b[92mSelected top 3 beams with scores: ['0.2000', '0.0000', '0.0000']\u001b[0m\n", + "\u001b[92mDepth 3 - Best validation score: 0.2000\u001b[0m\n", + "\u001b[94m\n", + "===== Beam Search Depth 4/4 with 3 beams =====\u001b[0m\n", + "\u001b[96mSampled validation minibatch of size 5 for depth 4\u001b[0m\n", + "\u001b[93mProcessing beam 1/3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (beam 1, batch size: 5): 100%|██████████| 5/5 [00:36<00:00, 7.34s/it]\n", + "Generating 2 proposals for beam 1 (with history): 50%|█████ | 1/2 [00:15<00:15, 15.77s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"The #Instruction asks us to improve the output by changing the #Variables based on the #Feedback. The feedback indicates errors in the calculations and logic used to determine the correct answers for the model's outputs. Each problem requires adjustments in the way #Variables are determined or used: \\n\\n1. For the biologist fish problem (ID[0]), a miscalculation led to an incorrect total of fish. The proportion of tagged fish needs to be reevaluated to give 840 fish. Changing the incorrect calculation by recalibrating the proportion of fish used to represent the sample tagged.\\n\\n2. For the switch problem (ID[1]), the inclusion-exclusion principle was improperly applied. It needs a more thorough analysis of the divisor multiples and count combinations to achieve the correct count of 650.\\n\\n3. For the subset problem (ID[2]), the counting of viable numbers was too high, resulting in 1421 versus the correct 905. Properly using the modulo constraints with subsets results in fewer selections than shown.\\n\\n4. The brick tower problem (ID[3]) needs reevaluation of the height variation range given constraint over block orientations. Correct arithmetic combinations should yield 465 distinct results rather than 471. Deeper combination examination between 4\\\" and 19\\\".\\n\\n5. In the box fitting probability (ID[4]), miscalculation suggests more favorable combinations than possible. Probability one-quarter follows constraints but broken for facing missing subset probabilities in the box facing group rather than prefacing 997 choices.\\n\\nAddressing these errors involves rethinking the calculations used based on model outputs, ensuring that each follows the principles correctly. Suggestions involve correcting proportions and understanding divisible elements more accurately in numbers. Expected results seen in #Feedback define the benchmark condition for accuracy.\",\n", + "\"answer\": null,\n", + "\"suggestion\": {\n", + " \"str0\": \"For each mathematical scenario, recalculate the precise probability or combinatorial result ensuring all outcomes align accurately with expected numerical solutions. Use known mathematical principles, taking care to interpret constraints and distributions effectively.\",\n", + " \"str1\": \"For each specific case, resolve the scenario using perfect mathematical reasoning with a focus on accurate probability combinations, integral formulations, and clear solution alignment to empirically grounded results as dictated by given details.\"\n", + "}\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 2 proposals for beam 1 (with history): 100%|██████████| 2/2 [00:19<00:00, 9.95s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"1. The #Instruction asks us to adjust the variables in #Variables based on the #Feedback provided for the #Outputs. Specifically, we need to ensure that we are getting closer to the official correct answers for each output. 2. The #Feedback indicates that the outputs derived from current values do not match correct answers, suggesting that the manner in which the calculations are carried out through str0 and str1 might be inadequate or incorrect for the problems. Errors are related to the reasoning or assumptions in the mathematics, not structural code issues. 3. For str0: The description of precise probability and combinatorial results might need to be more focused or refined to match the mathematical principles and intended outcomes of each problem, as shown in the errors for individual cases. For str1: The prompt asking for solving specific problems using 'appropriate mathematical principles' might not provide enough clarity or specific direction for handling these problems effectively. Adjustments will focus both on clarifying the goal of using precise mathematical strategies and ensuring compliance with the requirements or expected results. More specifically: \\n\\n- For scenario 1 (batchify50 ID [0]), the approach might involve clearly articulating the capture-recapture method tied accurately to the problem's mathematical framework. \\n\\n- For scenario 2 (ID [1]), it might mean focusing specifically on switching cycles and ensuring error handling in module considerations. \\n\\n- Scenario 3 correction might involve more careful relation to no-difference problem setups, focusing less on excessive construction.\\n\\n- Scenario 4 might correct integer combinations of bricks for achievable configurations without overestimating. \\n\\n- Scenario 5 requires attention to probability phrasing, ensuring clear insight into conditional arrangement probabilities.\",\n", + "\"answer\": \"The values of str0 and str1 should be adjusted to better clarify the problem requirements and expectations related to mathematical principles and solutions.\",\n", + "\"suggestion\": {\n", + " \"str0\": \"Analyze each problem scenario with clear and correct mathematical principles. Focus on precisely matching outcomes such as m+n relationships, handshake probabilities, and specific conditions provided.\",\n", + " \"str1\": \"Address each problem with exact mathematical principles. Ensure the calculated result aligns with problem specifications and official results, using methodical reasoning and problem-specific solutions.\"\n", + "}\n", + "}\n", + "\u001b[93mProcessing beam 2/3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (beam 2, batch size: 5): 100%|██████████| 5/5 [00:26<00:00, 5.22s/it]\n", + "Generating 2 proposals for beam 2 (with history): 50%|█████ | 1/2 [00:11<00:11, 11.38s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"1. The #Instruction requires improving the output by modifying the variables in #Variables based on the #Feedback. The #Feedback provides specific corrections expected in response to each problem posed in the code.\\n\\n2. Upon reviewing the #Feedback, each section indicates issues with providing correct numerical answers or relevant problem solutions, as indicated:\\n - The outputs 'Learner.call_llm695' to 'Learner.call_llm699' provide different responses to mathematical problems based on messages. However, none of them result in addressing specific provided problems, especially 'Learner.call_llm695', which should resolve to a numerical result but instead requests further scenarios.\\n\\n3. To provide a correct result for each LLM call, the specific message content related to the mathematical problems needs to be addressed correctly. The function outputs should provide not hypothetical responses but the actual numerical solutions or steps which lead to problem-solving.\\n\\n4. Suggested changes:\\n - Modify 'str0' or 'str1' to adequately stimulate providing a specific scenario or a precise answer more effectively rather than prompting further conversation. The instruction in 'str1' implies using mathematical techniques precisely, but should instead provide calculated examples based on input message specifics.\",\n", + "\"answer\": \"\",\n", + "\"suggestion\": {\n", + " \"str1\": \"In response to each provided scenario, compute exact answers using precise mathematical techniques suitable to each problem requirement and provide these directly as the output.\"\n", + "}\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 2 proposals for beam 2 (with history): 100%|██████████| 2/2 [00:13<00:00, 6.97s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"1. The instruction requires modifying the values in #Variables to improve the output according to the feedback provided. 2. The feedback indicates that the outputs are incorrect because no final numerical answers are provided for the problems. Specifically, each problem (ID 0 to 4) fails to deliver a conclusive numerical solution. 3. The incorrect outputs suggest that the current combinations of str0 and str1 fail to set up the correct conditions for the ModelWrapper calls and the LLM responses. The system prompt (str0) and the user prompt context (str1) should explicitly target the intended scenario description and data. For instance, ID 0 requires clear description/setup for calculating fewest handshakes the coach could have participated in. Each str0 and str1 should be adjusted to guide the model to generate outputs aligned with the official correct answers detailed in the feedback.\",\n", + " \"answer\": \"The feedback shows that the outputs are incomplete or incorrect, so improvement is needed in the setup to elicit the correct numerical answers.\",\n", + " \"suggestion\": {\n", + " \"str0\": \"For each solution, provide step-by-step combinatorial or probabilistic calculations resulting in precise numerical answers that match outcomes like 650 for switch positions or 931 for sequence conditions.\",\n", + " \"str1\": \"Apply mathematical accuracy for each scenario, ensuring outcomes are precise, definitive numerical results that align with the provided combinatorial or probabilistic frameworks and official answers.\"\n", + " }\n", + "}\n", + "\u001b[93mProcessing beam 3/3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (beam 3, batch size: 5): 100%|██████████| 5/5 [00:19<00:00, 4.00s/it]\n", + "Generating 2 proposals for beam 3 (with history): 50%|█████ | 1/2 [00:10<00:10, 10.13s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"The instruction asks us to modify the values of the variables in #Variables to improve the output based on the feedback provided. The code is attempting to format various mathematical problems and pass them to an LLM model via different format strings. Each formatted string relates to a different mathematical scenario. The feedback highlights incorrect scenarios or problems that do not directly answer the original math problems, indicating mismatches between message content and expected response. Thus, the problem description within variable 'str0' and especially 'str1' might not be precisely aligning with the specific respective mathematical problems in 'message700' to 'message704'. Each user_prompt has to be specifically prepared to perfectly fit the respective scenario described in each message. Adjust 'str1' to more precisely guide the LLM model in producing outputs specific to the problem statements in the corresponding 'message' variables.\",\n", + "\"answer\": null,\n", + "\"suggestion\": {\n", + " \"str0\": \"For each mathematical problem, analyze the scenario conditions and compute the exact probability or combinatorial result. Ensure all solutions match expected numerical results and align with problem constraints.\",\n", + " \"str1\": \"Solve each problem by focusing on using specific probability distributions, permutation calculations, or combinatorial logic tailored to the described scenarios to produce accurate outcomes that align with known results and official answers.\"\n", + "}\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 2 proposals for beam 3 (with history): 100%|██████████| 2/2 [00:11<00:00, 5.90s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"The instruction asks to change the values of the variables in #Variables to improve the output based on the feedback. From the feedback, we understand that the current problem and attempted solutions do not match or address the correct context of the original problems they were supposed to solve. The only variables we can modify are str0 and str1, which provide the contexts/prompts for these problems. The formats and results of these contexts (str0 and str1) need to be aligned with the original problems in order to get responses that can then be properly evaluated and compared to their respective official answers. Each one of the original problems are improperly addressed as per the feedback. Therefore, to improve the output, the statements within str0 and str1 should directly refer to the specific unique mathematical problems described within the scenarios of message700, message701, message702, message703, and message704 without mixing or deviating to unrelated examples.\",\n", + "\"answer\": \"\",\n", + "\"suggestion\": {\n", + " \"str0\": \"Calculate the probability or combinatorial result for each mathematical problem given the conditions such as the secretary and letter order, the switch positions after a process, handshake counts given gymnasts and coaches, cousin room arrangements, and letter choices to form a specific word from different sets.\",\n", + " \"str1\": \"For each problem scenario, use correct mathematical techniques to solve probability or permutation issues according to the scenarios: whether it's a typing order, switch division, handshake calculation, room distribution, or letter collection to form a word.\"\n", + "}\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 1/9: 100%|██████████| 5/5 [00:25<00:00, 5.09s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 1: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 2/9: 100%|██████████| 5/5 [00:25<00:00, 5.14s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 2: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 3/9: 100%|██████████| 5/5 [00:32<00:00, 6.47s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 3: Validation score: 0.2000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 4/9: 100%|██████████| 5/5 [00:31<00:00, 6.36s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 4: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 5/9: 100%|██████████| 5/5 [00:07<00:00, 1.48s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 5: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 6/9: 100%|██████████| 5/5 [00:04<00:00, 1.06it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 6: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 7/9: 100%|██████████| 5/5 [00:31<00:00, 6.25s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 7: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 8/9: 100%|██████████| 5/5 [00:28<00:00, 5.65s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 8: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 9/9: 100%|██████████| 5/5 [00:28<00:00, 5.77s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 9: Validation score: 0.0000\u001b[0m\n", + "\u001b[92mSelected top 3 beams with scores: ['0.2000', '0.0000', '0.0000']\u001b[0m\n", + "\u001b[92mDepth 4 - Best validation score: 0.2000\u001b[0m\n", + "\u001b[94m\n", + "===== Final Selection Using Full Validation Set =====\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 1/3: 100%|██████████| 20/20 [03:15<00:00, 9.76s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 1: Validation score: 0.1500\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 2/3: 100%|██████████| 20/20 [01:42<00:00, 5.12s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 2: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 3/3: 100%|██████████| 20/20 [00:45<00:00, 2.26s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 3: Validation score: 0.0000\u001b[0m\n", + "\u001b[92mSelected top 1 beams with scores: ['0.1500']\u001b[0m\n", + "\u001b[95m\n", + "===== Final Proposal Candidate Parameters =====\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating best beam on test set: 100%|██████████| 10/10 [00:48<00:00, 4.81s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[92mBEST BEAM - Test score: 0.3000\u001b[0m\n", + "\u001b[94m\n", + "===== Periodic Test Scores Summary =====\u001b[0m\n", + "\u001b[96mDepth 1: Test score = 0.0000\u001b[0m\n", + "FINISHED TRAINING BEAM SEARCH w/ HISTORY\n", + "\n", + "Best validation scores at each depth:\n", + " Depth 1: 0.0000\n", + " Depth 2: 0.2000\n", + " Depth 3: 0.2000\n", + " Depth 4: 0.2000\n", + "Final score: 0.3\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "algorithm = BeamsearchHistoryAlgorithm(\n", + " agent=agent,\n", + " optimizer=optimizer,\n", + " logger=logger,\n", + " num_threads=train_params[\"num_threads\"]\n", + " )\n", + "\n", + "async def wrapper():\n", + " print(\"STARTING TRAINING BEAM SEARCH w/ HISTORY\")\n", + " metrics, final_score = algorithm.train(**train_params)\n", + " print(\"FINISHED TRAINING BEAM SEARCH w/ HISTORY\")\n", + "\n", + " if 'best_validation_scores' in metrics:\n", + " print(\"\\nBest validation scores at each depth:\")\n", + " for depth, score in enumerate(metrics['best_validation_scores']):\n", + " print(f\" Depth {depth+1}: {score:.4f}\")\n", + " \n", + " print(\"Final score: \", final_score)\n", + " \n", + "asyncio.run(wrapper())" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "STARTING TRAINING UCB SEARCH\n", + "\u001b[96mEvaluating initial parameters using validation_dataset samples...\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating candidate: 100%|██████████| 5/5 [00:32<00:00, 6.47s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[93mInitial candidate: Score 0.2000, Evals 5\u001b[0m\n", + "\u001b[94mIter 1/3: \u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Iter 1: Forward pass for action 'a' : 100%|██████████| 5/5 [00:24<00:00, 4.95s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"The feedback points out errors in the calculations for each task performed by the code. The main issue across the tasks is an incorrect approach or missed key calculations that lead to incorrect results. For the sequences of coin tosses problem, the student failed to properly account for combinatorial arrangements using the 'balls and urns' model, leading to a severely inflated number of possible sequences. Similarly, for the fish population problem, the proportions were not used correctly to derive the number of fish, resulting in a projection error in the population. In the locker problem, improper tracking of the opening and closing pattern led to identifying the wrong last locker number. The card order problem had overcounting issues because of incorrectly accounting for overcounted sequences due to adjacent swaps. Lastly, the tower height estimation miscalculated possible heights due to incorrect accounting for achievable combinations. To tackle these issues, corrections involve using correct combinatorial methods, precisely tracking sequences, and correctly applying mathematical formulas or principles specified in feedback.\",\n", + " \"answer\": null,\n", + " \"suggestion\": {\n", + " \"str0\": \"This may require a custom approach aligned with the detailed feedback given for each specific problem.\",\n", + " \"str1\": \"Ensure to provide systematic breakdown and validation of the problem conditions, reacting to feedback measures described.\"\n", + " }\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating candidate: 100%|██████████| 5/5 [00:32<00:00, 6.44s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mIter 1: New candidate a_prime generated. Validation Score: 0.0000, Evals: 5\u001b[0m\n", + "\u001b[95mIter 1: Added new candidate to buffer.\u001b[0m\n", + "\u001b[94mIter 2/3: \u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Iter 2: Forward pass for action 'a' : 100%|██████████| 5/5 [00:21<00:00, 4.21s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"The instruction requires improving the output based on feedback, meaning we need to modify the values in #Variables to address the issues noted in the feedback. Each feedback indicates that a numerical result or specific answer was missing in the original process, which means that the variable inputs may need alteration to ensure clear numerical conclusions. The general issue across outputs is the lack of explicit, correct numerical answers expected in problem-solving scenarios involving specific constraints and questions. The code leverages format strings to construct prompts for a language model which implies the generated output depends on these prompts’ clarity and relevance to the questions posed. These prompts could be misleading or incomplete, affecting the text output quality. Feedback suggests that the results should include specific answers derived via detailed problem-solving steps or projections using data constraints. Suggestions for changes focus on incorporating more explicit numerical or detailed answers within the text format.\",\n", + " \"answer\": \"The existing system and user prompts must be aligned to ensure the correct numerical or specific answers are provided for each problem question.\",\n", + " \"suggestion\": {\n", + " \"str0\": \"\",\n", + " \"str1\": \"Provide explicit problem-solving strategies for meeting the constraints given in the scenario. Ensure to focus on developing explicit numerical answers for each problem aspect directly related to combinatorial admissions of domino patterns and verification cycles based on user feedback or historical evaluative analysis.\"\n", + " }\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating candidate: 100%|██████████| 5/5 [00:37<00:00, 7.58s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mIter 2: New candidate a_prime generated. Validation Score: 0.0000, Evals: 5\u001b[0m\n", + "\u001b[95mIter 2: Added new candidate to buffer.\u001b[0m\n", + "\u001b[92mLog @ Iter 2: Best score in buffer: 0.1000, Buffer size: 3, Total samples: 25\u001b[0m\n", + "\u001b[94mIter 3/3: \u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Iter 3: Forward pass for action 'a' : 100%|██████████| 5/5 [00:33<00:00, 6.74s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"1. The #Instruction requires a focus on improving the quality of outputs by changing #Variables values in accordance with #Feedback. 2. The #Feedback indicates that the outputs do not address the specific problem requirements, focusing instead on unrelated subjects like domino patterns or complex methodologies. The outputs need to connect directly with the core mathematical problems presented in #Inputs. 3. Each of the five problems presented requires distinct solutions: a probability calculation, a combinatorial typing sequence, a gymnastics handshake count, a card arrangement for sequences, and a triangle arrangement problem. Each requires a direct numerical solution. 4. Given the instruction, the value of 'str1' should help the code focus explicitly on the combinatorial problems at hand and ensure proper filling of user prompts according to input requirements. To remedy this, the prompt should directly respond to the particular problems' constraints and desired solutions.\",\n", + " \"answer\": \"Change the prompt to focus specifically on the set of five given problems to provide final numerical solutions related to probability, combinatorics of letters, handshake count, card sequences, and distinguishable triangle arrangements.\",\n", + " \"suggestion\": {\n", + " \"str1\": \"Answer the mathematical problems directly related to the given scenarios. Focus on calculating probabilities, combinatorial arrangements, or specific outcomes based on constraints provided, and present clear numerical solutions.\"\n", + " }\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating candidate: 100%|██████████| 5/5 [00:06<00:00, 1.25s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mIter 3: New candidate a_prime generated. Validation Score: 0.0000, Evals: 5\u001b[0m\n", + "\u001b[95mIter 3: Buffer full. Evicted a candidate (UCB: 0.5963)\u001b[0m\n", + "\u001b[95mIter 3: Added new candidate to buffer.\u001b[0m\n", + "\u001b[94mUCB search finished.\u001b[0m\n", + "\u001b[92mFinal best candidate: Mean Score 0.1000, Evals 10\u001b[0m\n", + "FINISHED TRAINING UCB SEARCH\n", + " Best candidate scores over iterations: 3 recorded\n", + " Final best candidate score: 0.1000\n", + " Final buffer average score: 0.0333\n", + "Final score: 0.1\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "algorithm = UCBSearchAlgorithm(\n", + " agent=agent,\n", + " optimizer=optimizer,\n", + " logger=logger,\n", + " num_threads=train_params[\"num_threads\"],\n", + " max_buffer_size=train_params[\"max_buffer_size\"],\n", + " ucb_exploration_factor=train_params[\"ucb_exploration_factor\"]\n", + " )\n", + "\n", + "async def wrapper():\n", + " print(\"STARTING TRAINING UCB SEARCH\")\n", + " metrics, final_score = algorithm.train(**train_params)\n", + " print(\"FINISHED TRAINING UCB SEARCH\")\n", + "\n", + " if 'best_candidate_scores' in metrics and metrics['best_candidate_scores']:\n", + " print(f\" Best candidate scores over iterations: {len(metrics['best_candidate_scores'])} recorded\")\n", + " print(f\" Final best candidate score: {metrics['best_candidate_scores'][-1]:.4f}\")\n", + " if 'buffer_avg_score' in metrics and metrics['buffer_avg_score']:\n", + " print(f\" Final buffer average score: {metrics['buffer_avg_score'][-1]:.4f}\")\n", + " \n", + " print(\"Final score: \", final_score)\n", + " \n", + "asyncio.run(wrapper())" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "trace", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.23" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/bbh/run_prompt_bigbench_trace.py b/examples/bbh/run_prompt_bigbench_trace.py index d6b12047..23564649 100644 --- a/examples/bbh/run_prompt_bigbench_trace.py +++ b/examples/bbh/run_prompt_bigbench_trace.py @@ -1,4 +1,3 @@ -import autogen from opto.trace.nodes import node, GRAPH, ParameterNode from textwrap import dedent from opto.optimizers import OptoPrime diff --git a/examples/virtualhome.py b/examples/virtualhome.py index b4b62f21..ef392569 100644 --- a/examples/virtualhome.py +++ b/examples/virtualhome.py @@ -10,13 +10,12 @@ import opto.trace as trace from opto.trace.nodes import node +from opto.utils.llm import LLM class LLMCallable: - def __init__(self, config_list=None, max_tokens=1024, verbose=False): - if config_list is None: - config_list = autogen.config_list_from_json("OAI_CONFIG_LIST") - self.llm = autogen.OpenAIWrapper(config_list=config_list) + def __init__(self, llm=None, max_tokens=1024, verbose=False): + self.llm = llm or LLM() self.max_tokens = max_tokens self.verbose = verbose @@ -28,15 +27,15 @@ def call_llm(self, user_prompt): if self.verbose not in (False, "output"): print("Prompt\n", system_prompt + user_prompt) - messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}] + messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, {"role": "user", "content": "Format your response as a JSON object."}] try: - response = self.llm.create( + response = self.llm( messages=messages, response_format={"type": "json_object"}, ) except Exception: - response = self.llm.create(messages=messages, max_tokens=self.max_tokens) + response = self.llm(messages=messages, max_tokens=self.max_tokens) response = response.choices[0].message.content if self.verbose: @@ -103,7 +102,7 @@ def fuzzy_match_action(self, returned_action, available_actions): def env_fn(env_id, env_task_set, executable_args, args): # from envs.unity_environment import UnityEnvironment - + return UnityEnvironment(num_agents=args.agent_num, max_episode_length=args.max_episode_length, port_id=env_id, @@ -186,7 +185,7 @@ def __init__(self, max_number_steps, run_id, env_fn, agent_fn, num_agents, recor self.dialogue_history_len = 30 for i in range(self.num_agents): - self.agents.append(virtualhome_agent.LLM_agent(agent_id=i + 1, args=args)) + self.agents.append(agent_fn[i]) def reset(self, task_id=None, reset_seed=1111): self.cnt_duplicate_subgoal = 0 @@ -376,7 +375,7 @@ def __init__(self, args): self.obs = None self.args = args - self.env = TraceVirtualHome(args.max_number_steps, args.run_id, + self.env = VirtualHomeEnv(args.max_number_steps, args.run_id, env_fn, args.agent_fn, args.num_agents, args=args) atexit.register(self.close) @@ -387,7 +386,7 @@ def close(self): def reset_env(self): self.env.close() - self.env = TraceVirtualHome(self.args.max_number_steps, self.args.run_id, + self.env = VirtualHomeEnv(self.args.max_number_steps, self.args.run_id, env_fn, self.args.agent_fn, self.args.num_agents, args=self.args) def reset(self, task_id=8): @@ -403,7 +402,7 @@ def reset(self, task_id=8): agent_obs, agent_obs_descs, agent_goal_specs, agent_goal_descs, agent_infos = self.env.reset( task_id=task_id) - @bundle() + @trace.bundle() def reset(agent_idx): return agent_obs_descs[agent_idx]['prompts'] @@ -446,7 +445,7 @@ def step(self, plans, agent_infos, LM_times, agent_obs, agent_goal_specs, agent_ self.obs = next_agent_obs_descs # have to add allow_external_dependencies, why metaworld is fine? - @bundle(allow_external_dependencies=True) + @trace.bundle(allow_external_dependencies=True) def step(action, agent_idx): """ Take action in the environment and return the next observation From 310483a0b847fe0c986f7975b0ee20cdffdc3148 Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 13 Aug 2025 18:03:57 -0400 Subject: [PATCH 148/314] initial commit for opro_v2 --- opto/optimizers/opro_v2.py | 185 +++++++++++++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 opto/optimizers/opro_v2.py diff --git a/opto/optimizers/opro_v2.py b/opto/optimizers/opro_v2.py new file mode 100644 index 00000000..16254834 --- /dev/null +++ b/opto/optimizers/opro_v2.py @@ -0,0 +1,185 @@ +import json +from textwrap import dedent +from dataclasses import dataclass, asdict +from typing import Dict + +from opto.optimizers.optoprime_v2 import OptoPrimeV2, OptimizerPromptSymbolSet + +""" +OPRO is a single parameter / solution optimizer that conditions on feedback. +(context, solution, feedback) -> new_solution + +It does not contain execution graph and is more streamlined/faster in inference. +""" + + +# Not inheriting from optoprime_v2 because this should have a smaller set +class OPROPromptSymbolSet(OptimizerPromptSymbolSet): + + problem_context_section_title = "# Problem Context" + variable_section_title = "# Solution" + feedback_section_title = "# Feedback" + + node_tag = "node" # nodes that are constants in the graph + variable_tag = "solution" # nodes that can be changed + value_tag = "value" # inside node, we have value tag + constraint_tag = "constraint" # inside node, we have constraint tag + + # output format + # Note: we currently don't support extracting format's like "```code```" because we assume supplied tag is name-only, i.e., + reasoning_tag = "reasoning" + improved_variable_tag = "variable" + name_tag = "name" + + expect_json = False # this will stop `enforce_json` arguments passed to LLM calls + + @property + def default_prompt_symbols(self) -> Dict[str, str]: + return { + "variables": self.variables_section_title, + "feedback": self.feedback_section_title, + "instruction": self.instruction_section_title, + } + +@dataclass +class ProblemInstance: + instruction: str + variables: str + feedback: str + + optimizer_prompt_symbol_set: OptimizerPromptSymbolSet + + problem_template = dedent( + """ + # Problem Context + {instruction} + + # Solution + {variables} + + # Feedback + {feedback} + """ + ) + + def __repr__(self) -> str: + return self.replace_symbols(self.problem_template.format( + instruction=self.instruction, + variables=self.variables, + feedback=self.feedback, + ), self.optimizer_prompt_symbol_set.default_prompt_symbols) + + def replace_symbols(self, text: str, symbols: Dict[str, str]) -> str: + default_prompt_symbols = { + "variables": "# Variables", + "feedback": "# Feedback", + "instruction": "# Problem Context", + } + + for k, v in symbols.items(): + text = text.replace(default_prompt_symbols[k], v) + return text + +""" +TODO: +1. think about how initial solution was generated... +""" + +class OPRO2(OptoPrimeV2): + representation_prompt = dedent( + """ + You're tasked to change the proposed solution according to feedback. + + Specifically, a problem will be composed of the following parts: + - {instruction_section_title}: the instruction which describes the things you need to do or the question you should answer. + - {variables_section_title}: the input variables that you can change/tweak (trainable). + - {feedback_section_title}: the feedback about the code's execution result. + + If `data_type` is `code`, it means `{value_tag}` is the source code of a python code, which may include docstring and definitions. + """ + ) + + output_format_prompt_template = dedent( + """ + Output_format: Your output should be in the following XML/HTML format: + + ``` + {output_format} + ``` + + In <{reasoning_tag}>, explain the problem: 1. what the {instruction_section_title} means 2. what the {feedback_section_title} means to {variables_section_title} considering how {variables_section_title} follow {instruction_section_title}. 3. Reasoning about the suggested changes in {variables_section_title} (if needed) and the expected result. + + If you need to suggest a change in the values of {variables_section_title}, write down the suggested values in <{improved_variable_tag}>. Remember you can change only the values in {variables_section_title}, not others. When `type` of a variable is `code`, you should write the new definition in the format of python code without syntax errors, and you should not change the function name or the function signature. + + If no changes are needed, just output TERMINATE. + """ + ) + + user_prompt_template = dedent( + """ + Now you see problem instance: + + ================================ + {problem_instance} + ================================ + + """ + ) + + final_prompt = dedent( + """ + What are your revised solutions on {names}? + + Your response: + """ + ) + + default_objective = "Propose a new solution that will improve the feedback." + + def __init__(self, *args, + optimizer_prompt_symbol_set: OptimizerPromptSymbolSet = None, + **kwargs): + optimizer_prompt_symbol_set = optimizer_prompt_symbol_set or OPROPromptSymbolSet() + super().__init__(*args, optimizer_prompt_symbol_set=optimizer_prompt_symbol_set, **kwargs) + self.buffer = [] + + def problem_instance(self, summary, mask=None): + mask = mask or [] + return ProblemInstance( + instruction=self.objective if "#Instruction" not in mask else "", + variables=( + self.repr_node_value_compact(summary.variables, node_tag=self.optimizer_prompt_symbol_set.variable_tag, + value_tag=self.optimizer_prompt_symbol_set.value_tag, + constraint_tag=self.optimizer_prompt_symbol_set.constraint_tag) + if self.optimizer_prompt_symbol_set.variables_section_title not in mask + else "" + ), + feedback=summary.user_feedback if self.optimizer_prompt_symbol_set.feedback_section_title not in mask else "", + optimizer_prompt_symbol_set=self.optimizer_prompt_symbol_set + ) + + def initialize_prompt(self): + self.representation_prompt = self.representation_prompt.format( + variable_expression_format=dedent(f""" + <{self.optimizer_prompt_symbol_set.variable_tag} name="variable_name" type="data_type"> + <{self.optimizer_prompt_symbol_set.value_tag}> + value + + <{self.optimizer_prompt_symbol_set.constraint_tag}> + constraint_expression + + + """), + value_tag=self.optimizer_prompt_symbol_set.value_tag, + variables_section_title=self.optimizer_prompt_symbol_set.variables_section_title.replace(" ", ""), + feedback_section_title=self.optimizer_prompt_symbol_set.feedback_section_title.replace(" ", ""), + instruction_section_title=self.optimizer_prompt_symbol_set.instruction_section_title.replace(" ", ""), + ) + self.output_format_prompt = self.output_format_prompt_template.format( + output_format=self.optimizer_prompt_symbol_set.output_format, + reasoning_tag=self.optimizer_prompt_symbol_set.reasoning_tag, + improved_variable_tag=self.optimizer_prompt_symbol_set.improved_variable_tag, + instruction_section_title=self.optimizer_prompt_symbol_set.instruction_section_title.replace(" ", ""), + feedback_section_title=self.optimizer_prompt_symbol_set.feedback_section_title.replace(" ", ""), + variables_section_title=self.optimizer_prompt_symbol_set.variables_section_title.replace(" ", ""), + ) From 5cbc7f92ca55e4f12ea92c98df611102faf82fb7 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 14 Aug 2025 20:47:10 +0000 Subject: [PATCH 149/314] Move priority_search under opto/features --- examples/priority_search_example.py | 2 +- opto/features/priority_search/__init__.py | 2 ++ .../algorithms => features}/priority_search/examples.py | 2 +- .../priority_search/priority_search.py | 4 ++-- .../priority_search/search_template.py | 0 .../{trainer/algorithms => features}/priority_search/utils.py | 0 opto/trainer/algorithms/__init__.py | 1 - opto/trainer/algorithms/priority_search/__init__.py | 2 -- tests/unit_tests/test_priority_search.py | 4 ++-- tests/unit_tests/test_sampler.py | 2 +- 10 files changed, 9 insertions(+), 10 deletions(-) create mode 100644 opto/features/priority_search/__init__.py rename opto/{trainer/algorithms => features}/priority_search/examples.py (99%) rename opto/{trainer/algorithms => features}/priority_search/priority_search.py (99%) rename opto/{trainer/algorithms => features}/priority_search/search_template.py (100%) rename opto/{trainer/algorithms => features}/priority_search/utils.py (100%) delete mode 100644 opto/trainer/algorithms/priority_search/__init__.py diff --git a/examples/priority_search_example.py b/examples/priority_search_example.py index 4739ee0a..bd11e70f 100644 --- a/examples/priority_search_example.py +++ b/examples/priority_search_example.py @@ -3,7 +3,7 @@ from opto import trace from opto.utils.llm import LLM, LiteLLM from opto.optimizers import OptoPrimeV2 as OptoPrime -from opto.trainer.algorithms.priority_search import PrioritySearch as SearchAlgorithm +from opto.features.priority_search import PrioritySearch as SearchAlgorithm from opto.trainer.loggers import TensorboardLogger from opto.trainer.guide import VerbalJudgeGuide from typing import Any diff --git a/opto/features/priority_search/__init__.py b/opto/features/priority_search/__init__.py new file mode 100644 index 00000000..5ec28705 --- /dev/null +++ b/opto/features/priority_search/__init__.py @@ -0,0 +1,2 @@ +from opto.features.priority_search.priority_search import PrioritySearch +from opto.features.priority_search.examples import SequentialUpdate, SequentialSearch, BeamSearch \ No newline at end of file diff --git a/opto/trainer/algorithms/priority_search/examples.py b/opto/features/priority_search/examples.py similarity index 99% rename from opto/trainer/algorithms/priority_search/examples.py rename to opto/features/priority_search/examples.py index 90f6cb14..fedd5e1b 100644 --- a/opto/trainer/algorithms/priority_search/examples.py +++ b/opto/features/priority_search/examples.py @@ -1,5 +1,5 @@ -from opto.trainer.algorithms.priority_search import PrioritySearch +from opto.features.priority_search import PrioritySearch from typing import Union, Optional # Below we define several algorithms that use the PrioritySearch class. diff --git a/opto/trainer/algorithms/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py similarity index 99% rename from opto/trainer/algorithms/priority_search/priority_search.py rename to opto/features/priority_search/priority_search.py index 0a9f4aca..35342580 100644 --- a/opto/trainer/algorithms/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -7,8 +7,8 @@ from opto.trace.nodes import ParameterNode from opto.trainer.utils import async_run from opto.trainer.algorithms.basic_algorithms import batchify -from opto.trainer.algorithms.priority_search.search_template import SearchTemplate, Samples -from opto.trainer.algorithms.priority_search.utils import set_module_parameters, remap_update_dict, create_module_from_update_dict +from opto.features.priority_search.search_template import SearchTemplate, Samples +from opto.features.priority_search.utils import set_module_parameters, remap_update_dict, create_module_from_update_dict class ModuleCandidate: diff --git a/opto/trainer/algorithms/priority_search/search_template.py b/opto/features/priority_search/search_template.py similarity index 100% rename from opto/trainer/algorithms/priority_search/search_template.py rename to opto/features/priority_search/search_template.py diff --git a/opto/trainer/algorithms/priority_search/utils.py b/opto/features/priority_search/utils.py similarity index 100% rename from opto/trainer/algorithms/priority_search/utils.py rename to opto/features/priority_search/utils.py diff --git a/opto/trainer/algorithms/__init__.py b/opto/trainer/algorithms/__init__.py index 084cd459..2586fd31 100644 --- a/opto/trainer/algorithms/__init__.py +++ b/opto/trainer/algorithms/__init__.py @@ -1,4 +1,3 @@ from opto.trainer.algorithms.basic_algorithms import Minibatch, MinibatchAlgorithm, BasicSearchAlgorithm from opto.trainer.algorithms.beamsearch_algorithm import BeamsearchAlgorithm, BeamsearchHistoryAlgorithm from opto.trainer.algorithms.UCBsearch import UCBSearchAlgorithm -from opto.trainer.algorithms.priority_search import PrioritySearch \ No newline at end of file diff --git a/opto/trainer/algorithms/priority_search/__init__.py b/opto/trainer/algorithms/priority_search/__init__.py deleted file mode 100644 index caaf664f..00000000 --- a/opto/trainer/algorithms/priority_search/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from opto.trainer.algorithms.priority_search.priority_search import PrioritySearch -from opto.trainer.algorithms.priority_search.examples import SequentialUpdate, SequentialSearch, BeamSearch \ No newline at end of file diff --git a/tests/unit_tests/test_priority_search.py b/tests/unit_tests/test_priority_search.py index c1bf703b..50602580 100644 --- a/tests/unit_tests/test_priority_search.py +++ b/tests/unit_tests/test_priority_search.py @@ -1,8 +1,8 @@ from opto import trace from opto.trainer.loader import DataLoader from opto.trainer.sampler import Sampler -from opto.trainer.algorithms.priority_search.priority_search import PrioritySearch as _PrioritySearch -from opto.trainer.algorithms.priority_search.priority_search import ModuleCandidate +from opto.features.priority_search.priority_search import PrioritySearch as _PrioritySearch +from opto.features.priority_search.priority_search import ModuleCandidate from opto.optimizers import OptoPrimeV2 from opto.trainer.guide import AutoGuide from opto.utils.llm import DummyLLM diff --git a/tests/unit_tests/test_sampler.py b/tests/unit_tests/test_sampler.py index fd9ceca4..ae53c8af 100644 --- a/tests/unit_tests/test_sampler.py +++ b/tests/unit_tests/test_sampler.py @@ -2,7 +2,7 @@ from opto.trainer.sampler import Sampler from opto.trainer.loader import DataLoader from opto.trainer.guide import AutoGuide -from opto.trainer.algorithms.priority_search.utils import is_node_copy +from opto.features.priority_search.utils import is_node_copy class Guide(AutoGuide): From 01feab5b8c873295da9d35d5c02d8ae6fca67b97 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 14 Aug 2025 20:54:06 +0000 Subject: [PATCH 150/314] move sampler under opto/features/priority_search --- opto/{trainer => features/priority_search}/sampler.py | 0 opto/features/priority_search/search_template.py | 2 +- opto/features/priority_search/utils.py | 2 +- tests/unit_tests/test_priority_search.py | 2 +- tests/unit_tests/test_sampler.py | 2 +- 5 files changed, 4 insertions(+), 4 deletions(-) rename opto/{trainer => features/priority_search}/sampler.py (100%) diff --git a/opto/trainer/sampler.py b/opto/features/priority_search/sampler.py similarity index 100% rename from opto/trainer/sampler.py rename to opto/features/priority_search/sampler.py diff --git a/opto/features/priority_search/search_template.py b/opto/features/priority_search/search_template.py index d2b5e61c..5654d832 100644 --- a/opto/features/priority_search/search_template.py +++ b/opto/features/priority_search/search_template.py @@ -3,7 +3,7 @@ from opto import trace from opto.trainer.algorithms.basic_algorithms import Minibatch from opto.trainer.loader import DataLoader -from opto.trainer.sampler import Sampler, RolloutsGraph +from opto.features.priority_search.sampler import Sampler, RolloutsGraph # TODO save and load SearchTemplate # TODO async version??? diff --git a/opto/features/priority_search/utils.py b/opto/features/priority_search/utils.py index 8c4ed9db..c12e3ded 100644 --- a/opto/features/priority_search/utils.py +++ b/opto/features/priority_search/utils.py @@ -9,7 +9,7 @@ from opto.optimizers.utils import print_color from opto.trainer.algorithms.basic_algorithms import Minibatch, AlgorithmBase, batchify from opto.trainer.loader import DataLoader -from opto.trainer.sampler import Sampler, RolloutsGraph +from opto.features.priority_search.sampler import Sampler, RolloutsGraph import time # Some helper functions to convert between trace.Module and update_dict diff --git a/tests/unit_tests/test_priority_search.py b/tests/unit_tests/test_priority_search.py index 50602580..6f5fa85b 100644 --- a/tests/unit_tests/test_priority_search.py +++ b/tests/unit_tests/test_priority_search.py @@ -1,6 +1,6 @@ from opto import trace from opto.trainer.loader import DataLoader -from opto.trainer.sampler import Sampler +from opto.features.priority_search.sampler import Sampler from opto.features.priority_search.priority_search import PrioritySearch as _PrioritySearch from opto.features.priority_search.priority_search import ModuleCandidate from opto.optimizers import OptoPrimeV2 diff --git a/tests/unit_tests/test_sampler.py b/tests/unit_tests/test_sampler.py index ae53c8af..2dc92439 100644 --- a/tests/unit_tests/test_sampler.py +++ b/tests/unit_tests/test_sampler.py @@ -1,5 +1,5 @@ from opto import trace -from opto.trainer.sampler import Sampler +from opto.features.priority_search.sampler import Sampler from opto.trainer.loader import DataLoader from opto.trainer.guide import AutoGuide from opto.features.priority_search.utils import is_node_copy From afba6957e955247362f011702fd5a8e5c5ca23c9 Mon Sep 17 00:00:00 2001 From: windweller Date: Fri, 15 Aug 2025 12:16:57 -0400 Subject: [PATCH 151/314] fix a few issues and now it works --- opto/optimizers/opro_v2.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/opto/optimizers/opro_v2.py b/opto/optimizers/opro_v2.py index 16254834..15e0d900 100644 --- a/opto/optimizers/opro_v2.py +++ b/opto/optimizers/opro_v2.py @@ -47,7 +47,7 @@ class ProblemInstance: variables: str feedback: str - optimizer_prompt_symbol_set: OptimizerPromptSymbolSet + optimizer_prompt_symbol_set: OPROPromptSymbolSet problem_template = dedent( """ @@ -92,8 +92,8 @@ class OPRO2(OptoPrimeV2): Specifically, a problem will be composed of the following parts: - {instruction_section_title}: the instruction which describes the things you need to do or the question you should answer. - - {variables_section_title}: the input variables that you can change/tweak (trainable). - - {feedback_section_title}: the feedback about the code's execution result. + - {variables_section_title}: the proposed solution that you can change/tweak (trainable). + - {feedback_section_title}: the feedback about the solution. If `data_type` is `code`, it means `{value_tag}` is the source code of a python code, which may include docstring and definitions. """ @@ -134,14 +134,16 @@ class OPRO2(OptoPrimeV2): """ ) - default_objective = "Propose a new solution that will improve the feedback." + # Default Objective becomes instruction for the next block + default_objective = "Propose a new solution that will incorporate the feedback." def __init__(self, *args, optimizer_prompt_symbol_set: OptimizerPromptSymbolSet = None, **kwargs): optimizer_prompt_symbol_set = optimizer_prompt_symbol_set or OPROPromptSymbolSet() super().__init__(*args, optimizer_prompt_symbol_set=optimizer_prompt_symbol_set, **kwargs) - self.buffer = [] + self.include_example = False # default example in OptoPrimeV2 does not work in OPRO + self.memory_size = 5 def problem_instance(self, summary, mask=None): mask = mask or [] From e01fde29ef497dade12255c7fb59a7e2b884cc33 Mon Sep 17 00:00:00 2001 From: windweller Date: Fri, 15 Aug 2025 12:37:16 -0400 Subject: [PATCH 152/314] add extraction test to both oprov2 and optoprimev2 --- opto/optimizers/opro_v2.py | 2 +- tests/llm_optimizers_tests/test_opro_v2.py | 164 ++++++++++++++++++ .../llm_optimizers_tests/test_optoprime_v2.py | 54 +++++- 3 files changed, 218 insertions(+), 2 deletions(-) create mode 100644 tests/llm_optimizers_tests/test_opro_v2.py diff --git a/opto/optimizers/opro_v2.py b/opto/optimizers/opro_v2.py index 15e0d900..23b8c767 100644 --- a/opto/optimizers/opro_v2.py +++ b/opto/optimizers/opro_v2.py @@ -85,7 +85,7 @@ def replace_symbols(self, text: str, symbols: Dict[str, str]) -> str: 1. think about how initial solution was generated... """ -class OPRO2(OptoPrimeV2): +class OPROv2(OptoPrimeV2): representation_prompt = dedent( """ You're tasked to change the proposed solution according to feedback. diff --git a/tests/llm_optimizers_tests/test_opro_v2.py b/tests/llm_optimizers_tests/test_opro_v2.py new file mode 100644 index 00000000..5eca4fe4 --- /dev/null +++ b/tests/llm_optimizers_tests/test_opro_v2.py @@ -0,0 +1,164 @@ +import os +import pytest +from opto.trace import bundle, node, GRAPH +import opto.optimizers +import importlib +import inspect +import json +import pickle +from opto.utils.llm import LLM + +from opto import trace +from opto.trace import node, bundle +from opto.optimizers.opro_v2 import OPROv2, OPROPromptSymbolSet + +# You can override for temporarly testing a specific optimizer ALL_OPTIMIZERS = [TextGrad] # [OptoPrimeMulti] ALL_OPTIMIZERS = [OptoPrime] + +# Skip tests if no API credentials are available +SKIP_REASON = "No API credentials found" +HAS_CREDENTIALS = os.path.exists("OAI_CONFIG_LIST") or os.environ.get("TRACE_LITELLM_MODEL") or os.environ.get( + "OPENAI_API_KEY") +llm = LLM() + +@pytest.fixture(autouse=True) +def clear_graph(): + """Reset the graph before each test""" + GRAPH.clear() + yield + GRAPH.clear() + + +@pytest.mark.skipif(not HAS_CREDENTIALS, reason=SKIP_REASON) +def test_response_extraction(): + pass + + +def test_tag_template_change(): + num_1 = node(1, trainable=True) + num_2 = node(2, trainable=True, description="<=5") + result = num_1 + num_2 + optimizer = OPROv2([num_1, num_2], use_json_object_format=False, + ignore_extraction_error=False, + include_example=True, + optimizer_prompt_symbol_set=OPROPromptSymbolSet()) + + optimizer.zero_feedback() + optimizer.backward(result, 'make this number bigger') + + summary = optimizer.summarize() + part1, part2 = optimizer.construct_prompt(summary) + + part1 = optimizer.replace_symbols(part1, optimizer.prompt_symbols) + part2 = optimizer.replace_symbols(part2, optimizer.prompt_symbols) + + assert """""" in part1, "Expected tag to be present in part1" + assert """""" in part2, "Expected tag to be present in part2" + + print(part1) + print(part2) + + +@bundle() +def transform(num): + """Add number""" + return num + 1 + + +@bundle(trainable=True) +def multiply(num): + return num * 5 + + +def test_function_repr(): + num_1 = node(1, trainable=False) + + result = multiply(transform(num_1)) + optimizer = OPROv2([multiply.parameter], use_json_object_format=False, + ignore_extraction_error=False, + include_example=True) + + optimizer.zero_feedback() + optimizer.backward(result, 'make this number bigger') + + summary = optimizer.summarize() + part1, part2 = optimizer.construct_prompt(summary) + + part1 = optimizer.replace_symbols(part1, optimizer.prompt_symbols) + part2 = optimizer.replace_symbols(part2, optimizer.prompt_symbols) + + function_repr = """ + +def multiply(num): + return num * 5 + + +The code should start with: +def multiply(num): + +""" + + assert function_repr in part2, "Expected function representation to be present in part2" + +def test_big_data_truncation(): + num_1 = node(1, trainable=True) + + list_1 = node([1, 2, 3, 4, 5, 6, 7, 8, 9, 20] * 10, trainable=True) + + result = num_1 + list_1[30] + + optimizer = OPROv2([num_1, list_1], use_json_object_format=False, + ignore_extraction_error=False, initial_var_char_limit=10) + + optimizer.zero_feedback() + optimizer.backward(result, 'make this number bigger') + + summary = optimizer.summarize() + part1, part2 = optimizer.construct_prompt(summary) + + part1 = optimizer.replace_symbols(part1, optimizer.prompt_symbols) + part2 = optimizer.replace_symbols(part2, optimizer.prompt_symbols) + + truncated_repr = "[1, 2, 3, ...(skipped due to length limit)" + + assert truncated_repr in part2, "Expected truncated list representation to be present in part2" + +def test_extraction_pipeline(): + num_1 = node(1, trainable=True) + optimizer = OPROv2([num_1], use_json_object_format=False, + ignore_extraction_error=False, + include_example=True) + + @bundle() + def propose_solution(x): + """ + Propose a solution to the given prompt using the input. + """ + return x + 1 + + result = propose_solution(num_1) + + optimizer.zero_feedback() + optimizer.backward(result, 'make this number bigger') + + summary = optimizer.summarize() + part1, part2 = optimizer.construct_prompt(summary) + + part1 = optimizer.replace_symbols(part1, optimizer.prompt_symbols) + part2 = optimizer.replace_symbols(part2, optimizer.prompt_symbols) + + messages = [ + {"role": "system", "content": part1}, + {"role": "user", "content": part2}, + ] + + # response = optimizer.llm(messages=messages) + # response = response.choices[0].message.content + + response = '```\n\n\nThe #Instruction requests a new solution that incorporates the given feedback into the proposed solution. The #Variables section includes an integer variable "int0" with the current value set to 1. The feedback states that this number should be made "bigger." Thus, the current value does not meet the feedback requirement, and I should change it to a larger integer value to comply with the feedback. A simple increment will suffice, so I will propose changing "int0" from 1 to 2.\n\n\nint0\n\n2\n\n\n\n```' + reasoning = response + suggestion = optimizer.extract_llm_suggestion(response) + + assert 'reasoning' in suggestion, "Expected 'reasoning' in suggestion" + assert 'variables' in suggestion, "Expected 'variables' in suggestion" + assert 'int0' in suggestion['variables'], "Expected 'int0' variable in suggestion" + assert suggestion['variables']['int0'] == 2, "Expected int0 to be incremented to 2" diff --git a/tests/llm_optimizers_tests/test_optoprime_v2.py b/tests/llm_optimizers_tests/test_optoprime_v2.py index af09c8b2..b1032f28 100644 --- a/tests/llm_optimizers_tests/test_optoprime_v2.py +++ b/tests/llm_optimizers_tests/test_optoprime_v2.py @@ -126,4 +126,56 @@ def test_big_data_truncation(): """ - assert truncated_repr in part2, "Expected truncated list representation to be present in part2" \ No newline at end of file + assert truncated_repr in part2, "Expected truncated list representation to be present in part2" + +def test_extraction_pipeline(): + num_1 = node(1, trainable=True) + num_2 = node(2, trainable=True, description="<=5") + result = num_1 + num_2 + optimizer = OptoPrimeV2([num_1, num_2], use_json_object_format=False, + ignore_extraction_error=False, + include_example=True, + optimizer_prompt_symbol_set=OptimizerPromptSymbolSet2()) + + optimizer.zero_feedback() + optimizer.backward(result, 'make this number bigger') + + summary = optimizer.summarize() + part1, part2 = optimizer.construct_prompt(summary) + + part1 = optimizer.replace_symbols(part1, optimizer.prompt_symbols) + part2 = optimizer.replace_symbols(part2, optimizer.prompt_symbols) + + messages = [ + {"role": "system", "content": part1}, + {"role": "user", "content": part2}, + ] + + # response = optimizer.llm(messages=messages) + # response = response.choices[0].message.content + response = """ +The instruction suggests that the output, `add0`, needs to be made bigger than it currently is (3). The code performs an addition of `int0` and `int1` to produce `add0`. To increase `add0`, we can increase the values of `int0` or `int1`, or both. Given that `int1` has a constraint of being less than or equal to 5, we can set `int0` to a higher value, since it has no explicit constraint. By adjusting `int0` to a higher value, the output can be made larger in accordance with the feedback. + + + +int0 + +5 + + + + +int1 + +5 + +""" + reasoning = response + suggestion = optimizer.extract_llm_suggestion(response) + + assert 'reasoning' in suggestion, "Expected 'reasoning' in suggestion" + assert 'variables' in suggestion, "Expected 'variables' in suggestion" + assert 'int0' in suggestion['variables'], "Expected 'int0' variable in suggestion" + assert 'int1' in suggestion['variables'], "Expected 'int1' variable in suggestion" + assert suggestion['variables']['int0'] == 5, "Expected int0 to be incremented to 5" + assert suggestion['variables']['int1'] == 5, "Expected int1 to be incremented to 5" From 9e87980c6bb0cac3ca7661aea8c1d6b5ac666ae8 Mon Sep 17 00:00:00 2001 From: windweller Date: Fri, 15 Aug 2025 14:43:17 -0400 Subject: [PATCH 153/314] changed problem_instance symbol replace and updated init args --- opto/optimizers/opro_v2.py | 18 +++--------------- opto/optimizers/optoprime_v2.py | 21 ++------------------- 2 files changed, 5 insertions(+), 34 deletions(-) diff --git a/opto/optimizers/opro_v2.py b/opto/optimizers/opro_v2.py index 23b8c767..8c3b0711 100644 --- a/opto/optimizers/opro_v2.py +++ b/opto/optimizers/opro_v2.py @@ -63,22 +63,11 @@ class ProblemInstance: ) def __repr__(self) -> str: - return self.replace_symbols(self.problem_template.format( + return self.problem_template.format( instruction=self.instruction, variables=self.variables, feedback=self.feedback, - ), self.optimizer_prompt_symbol_set.default_prompt_symbols) - - def replace_symbols(self, text: str, symbols: Dict[str, str]) -> str: - default_prompt_symbols = { - "variables": "# Variables", - "feedback": "# Feedback", - "instruction": "# Problem Context", - } - - for k, v in symbols.items(): - text = text.replace(default_prompt_symbols[k], v) - return text + ) """ TODO: @@ -138,9 +127,8 @@ class OPROv2(OptoPrimeV2): default_objective = "Propose a new solution that will incorporate the feedback." def __init__(self, *args, - optimizer_prompt_symbol_set: OptimizerPromptSymbolSet = None, + optimizer_prompt_symbol_set: OptimizerPromptSymbolSet = OPROPromptSymbolSet(), **kwargs): - optimizer_prompt_symbol_set = optimizer_prompt_symbol_set or OPROPromptSymbolSet() super().__init__(*args, optimizer_prompt_symbol_set=optimizer_prompt_symbol_set, **kwargs) self.include_example = False # default example in OptoPrimeV2 does not work in OPRO self.memory_size = 5 diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index db651bfb..0ec5aa48 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -294,7 +294,7 @@ class ProblemInstance: ) def __repr__(self) -> str: - return self.replace_symbols(self.problem_template.format( + return self.problem_template.format( instruction=self.instruction, code=self.code, documentation=self.documentation, @@ -303,24 +303,7 @@ def __repr__(self) -> str: outputs=self.outputs, others=self.others, feedback=self.feedback, - ), self.optimizer_prompt_symbol_set.default_prompt_symbols) - - def replace_symbols(self, text: str, symbols: Dict[str, str]) -> str: - default_prompt_symbols = { - "variables": "# Variables", - "constraints": "# Constraints", - "inputs": "# Inputs", - "outputs": "# Outputs", - "others": "# Others", - "feedback": "# Feedback", - "instruction": "# Instruction", - "code": "# Code", - "documentation": "# Documentation", - } - - for k, v in symbols.items(): - text = text.replace(default_prompt_symbols[k], v) - return text + ) class OptoPrimeV2(OptoPrime): From ee765fcadfcd0ba30d777aacb485de8b5d7846aa Mon Sep 17 00:00:00 2001 From: windweller Date: Fri, 15 Aug 2025 16:01:29 -0400 Subject: [PATCH 154/314] update the default parameter in OPROv2 --- opto/optimizers/opro_v2.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/opto/optimizers/opro_v2.py b/opto/optimizers/opro_v2.py index 8c3b0711..3b66c14a 100644 --- a/opto/optimizers/opro_v2.py +++ b/opto/optimizers/opro_v2.py @@ -127,11 +127,14 @@ class OPROv2(OptoPrimeV2): default_objective = "Propose a new solution that will incorporate the feedback." def __init__(self, *args, - optimizer_prompt_symbol_set: OptimizerPromptSymbolSet = OPROPromptSymbolSet(), + optimizer_prompt_symbol_set: OptimizerPromptSymbolSet = None, + include_example=False, # default example in OptoPrimeV2 does not work in OPRO + memory_size=5, **kwargs): - super().__init__(*args, optimizer_prompt_symbol_set=optimizer_prompt_symbol_set, **kwargs) - self.include_example = False # default example in OptoPrimeV2 does not work in OPRO - self.memory_size = 5 + optimizer_prompt_symbol_set = optimizer_prompt_symbol_set or OPROPromptSymbolSet() + super().__init__(*args, optimizer_prompt_symbol_set=optimizer_prompt_symbol_set, + include_example=include_example, memory_size=memory_size, + **kwargs) def problem_instance(self, summary, mask=None): mask = mask or [] From 4361a7798fec27181398db65f175fd7f1bcc573c Mon Sep 17 00:00:00 2001 From: chinganc Date: Fri, 15 Aug 2025 20:07:19 +0000 Subject: [PATCH 155/314] Rename AutoGuide -> Guide, VerbalJudgeGuide -> LLMJudge, model_dump -> export --- examples/gsm8k_trainer_example.py | 6 +- .../run_bigbench_trace_async.py | 64 +++++++------- examples/priority_search_example.py | 4 +- examples/search_algo_example.py | 84 +++++++++---------- opto/features/priority_search/sampler.py | 8 +- opto/trace/modules.py | 4 +- opto/trainer/algorithms/algorithm.py | 6 +- opto/trainer/guide.py | 4 +- opto/trainer/utils.py | 48 +++++------ tests/unit_tests/test_batch_run.py | 32 +++---- tests/unit_tests/test_modules.py | 26 +++--- tests/unit_tests/test_priority_search.py | 4 +- tests/unit_tests/test_sampler.py | 4 +- tests/unit_tests/test_saving_loading.py | 4 +- 14 files changed, 149 insertions(+), 149 deletions(-) diff --git a/examples/gsm8k_trainer_example.py b/examples/gsm8k_trainer_example.py index 7b627674..dd87b749 100644 --- a/examples/gsm8k_trainer_example.py +++ b/examples/gsm8k_trainer_example.py @@ -5,7 +5,7 @@ from opto.optimizers import OptoPrime from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm from opto.trainer.loggers import TensorboardLogger -from opto.trainer.guide import VerbalJudgeGuide +from opto.trainer.guide import LLMJudge from typing import Any @@ -46,7 +46,7 @@ def forward(self, message: Any) -> Any: return self.model(self.system_prompt, self.user_prompt_template, message) -Guide = VerbalJudgeGuide +Guide = LLMJudge Logger = TensorboardLogger @@ -80,7 +80,7 @@ def main(): agent=agent, optimizer=optimizer, logger=logger) - + alg.train(guide, train_dataset, num_epochs=num_epochs, diff --git a/examples/minibatch_bbh_aynsc/run_bigbench_trace_async.py b/examples/minibatch_bbh_aynsc/run_bigbench_trace_async.py index 3688907f..8211139c 100644 --- a/examples/minibatch_bbh_aynsc/run_bigbench_trace_async.py +++ b/examples/minibatch_bbh_aynsc/run_bigbench_trace_async.py @@ -11,7 +11,7 @@ import pickle import os from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm, evaluate -from opto.trainer.guide import AutoGuide +from opto.trainer.guide import Guide def eval_metric(true, prediction): @@ -28,24 +28,24 @@ def eval_metric(true, prediction): return prediction == true -class BigBenchGuide(AutoGuide): +class BigBenchGuide(Guide): """ Custom guide that uses the eval_metric function to evaluate responses and provide feedback for the BigBench tasks. """ - + def __init__(self): super().__init__() - + def forward(self, task, response, info, **kwargs): """ Evaluate the response using the eval_metric function. - + Args: task: The question response: The model's answer info: The correct answer - + Returns: score: 1.0 if correct, 0.0 if incorrect feedback: Feedback message @@ -53,25 +53,25 @@ def forward(self, task, response, info, **kwargs): try: correctness = eval_metric(info, response) score = 1.0 if correctness else 0.0 - + if correctness: feedback = "The answer is correct! No need to change anything." else: feedback = f"The answer is wrong. We expect the output of your answer to be \"{info}\". Please modify the prompt and relevant parts of the program to help LLM produce the right answer." - + return score, feedback except Exception as e: return 0.0, f"Error occurred: {str(e)}. Please fix the error and try again." - + def metric(self, task, response, info, **kwargs): """ Evaluate the response and return just the score. - + Args: task: The question response: The model's answer info: The correct answer - + Returns: score: 1.0 if correct, 0.0 if incorrect """ @@ -88,14 +88,14 @@ def __init__(self): self.prompt_template = dedent( """ Given the fields `question`, produce the fields `answer`. - + --- - + Follow the following format. - - Question: - Answer: - + + Question: + Answer: + --- Question: {} Answer: @@ -216,7 +216,7 @@ def forward(self, question): def learn_predict(dp, optimizer, examples, val_examples, task_name, save_dir): """ Train the model using the MinibatchUpdate algorithm. - + Args: dp: The model to train optimizer: The optimizer to use @@ -224,33 +224,33 @@ def learn_predict(dp, optimizer, examples, val_examples, task_name, save_dir): val_examples: Validation examples task_name: Name of the task save_dir: Directory to save checkpoints - + Returns: dp: The trained model rewards: The final validation accuracy """ # Create the guide guide = BigBenchGuide() - + # Prepare the training dataset train_dataset = { 'inputs': [ex['question'] for ex in examples], 'infos': [ex['answer'] for ex in examples] } - + # Prepare the validation dataset val_dataset = { 'inputs': [ex['question'] for ex in val_examples], 'infos': [ex['answer'] for ex in val_examples] } - + # Create the MinibatchUpdate algorithm algorithm = MinibatchAlgorithm( agent=dp, optimizer=optimizer, num_threads=4 # Adjust as needed ) - + # Train the model train_score, val_score = algorithm.train( guide=guide, @@ -265,30 +265,30 @@ def learn_predict(dp, optimizer, examples, val_examples, task_name, save_dir): verbose=True, min_score=None # No minimum score required ) - + return dp, val_score def evaluate_dp(dp, examples): """ Evaluate the model on a set of examples using MinibatchAlgorithm's evaluate method. - + Args: dp: The model to evaluate examples: The examples to evaluate on - + Returns: accuracy: The accuracy of the model responses: The responses of the model """ - + # Create the guide guide = BigBenchGuide() - + # Prepare the evaluation dataset inputs = [ex['question'] for ex in examples] infos = [ex['answer'] for ex in examples] - + # Use the evaluate function from basic_algorithm.py scores = evaluate( agent=dp, @@ -299,10 +299,10 @@ def evaluate_dp(dp, examples): num_threads=4, # Adjust as needed description=f"Evaluating on {len(examples)} examples" # Add descriptive message for the progress bar ) - + # Calculate accuracy accuracy = np.mean(scores) if scores else 0.0 - + # Collect responses for analysis responses = [] for example in tqdm(examples): @@ -312,7 +312,7 @@ def evaluate_dp(dp, examples): except Exception as e: print(f"Error during evaluation: {str(e)}") responses.append(None) - + return accuracy, responses diff --git a/examples/priority_search_example.py b/examples/priority_search_example.py index bd11e70f..caf03cbc 100644 --- a/examples/priority_search_example.py +++ b/examples/priority_search_example.py @@ -5,7 +5,7 @@ from opto.optimizers import OptoPrimeV2 as OptoPrime from opto.features.priority_search import PrioritySearch as SearchAlgorithm from opto.trainer.loggers import TensorboardLogger -from opto.trainer.guide import VerbalJudgeGuide +from opto.trainer.guide import LLMJudge from typing import Any @@ -46,7 +46,7 @@ def forward(self, message: Any) -> Any: return self.model(self.system_prompt, self.user_prompt_template, message) -Guide = VerbalJudgeGuide +Guide = LLMJudge Logger = TensorboardLogger diff --git a/examples/search_algo_example.py b/examples/search_algo_example.py index 14fc61ea..e40cfa7e 100644 --- a/examples/search_algo_example.py +++ b/examples/search_algo_example.py @@ -16,7 +16,7 @@ from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm, BasicSearchAlgorithm from opto.trainer.algorithms.beamsearch_algorithm import BeamsearchAlgorithm, BeamsearchHistoryAlgorithm from opto.trainer.algorithms.UCBsearch import UCBSearchAlgorithm -from opto.trainer.guide import AutoGuide +from opto.trainer.guide import Guide from opto.trainer.loggers import DefaultLogger from opto.utils.llm import LLM @@ -26,13 +26,13 @@ @trace.model class Learner(Module): """A basic LLM Agent for solving math problems.""" - - def __init__(self, + + def __init__(self, system_prompt: str = "You're a helpful agent answering math problems.", user_prompt_template: str = "Solve the following math problem step-by-step: {message}", llm: LLM = None): """Initialize the learner agent. - + Args: system_prompt: System prompt to guide LLM behavior user_prompt_template: Template for formatting user messages @@ -46,11 +46,11 @@ def __init__(self, @trace.bundle() def call_llm(self, system_prompt: str, user_prompt: str) -> str: """Call LLM model with the given prompts. - + Args: system_prompt: The system prompt user_prompt: The user prompt - + Returns: The LLM response content """ @@ -64,23 +64,23 @@ def call_llm(self, system_prompt: str, user_prompt: str) -> str: def forward(self, message: Any) -> str: """Agent's forward pass to process a message. - + Args: message: The input message to process - + Returns: The generated response - """ + """ user_prompt = self.user_prompt_template.format(message=message) return self.call_llm(self.system_prompt, user_prompt) -class TeacherGuide(AutoGuide): +class TeacherGuide(Guide): """Guide that uses LLM to judge answers and provide feedback.""" - + def __init__(self, model: str = "gpt-4o-mini"): """Initialize the teacher guide. - + Args: model: The LLM model to use for evaluation """ @@ -112,13 +112,13 @@ def __init__(self, model: str = "gpt-4o-mini"): def get_feedback(self, task: str, response: str, info: Any, **kwargs) -> Tuple[float, str]: """Get feedback on a student response. - + Args: task: The original math problem response: The student's answer info: The reference/correct answer **kwargs: Additional arguments - + Returns: Tuple of (score, feedback_text) """ @@ -140,16 +140,16 @@ def get_feedback(self, task: str, response: str, info: Any, **kwargs) -> Tuple[f return 1.0, "Correct." else: return 0.0, f"Incorrect. Feedback: {feedback_text}" - + def metric(self, task: str, content: str, info: Any, **kwargs) -> float: """Calculate the metric score for an answer. - + Args: task: The original math problem content: The student's answer info: The reference/correct answer **kwargs: Additional arguments - + Returns: Score (0.0 or 1.0) """ @@ -159,10 +159,10 @@ def metric(self, task: str, content: str, info: Any, **kwargs) -> float: class SimpleLogger(DefaultLogger): """Simplified logger that only shows important metrics.""" - + def log(self, name: str, data: Any, step: int, **kwargs): """Log only specific metrics to reduce output clutter. - + Args: name: The name of the metric data: The metric value @@ -174,7 +174,7 @@ def log(self, name: str, data: Any, step: int, **kwargs): 'Average test score', 'Validation score' ] - + if name in important_metrics or 'Parameter' in name: super().log(name, data, step, **kwargs) @@ -182,12 +182,12 @@ def log(self, name: str, data: Any, step: int, **kwargs): def main(): """Run the main training process with command line arguments.""" parser = argparse.ArgumentParser(description='Train agent using various algorithms') - + # Algorithm parameters parser.add_argument('--algorithm_type', type=str, default='UCBsearch', choices=['minibatch', 'basicsearch', 'beamsearch', 'beamsearchhistory', 'UCBsearch'], help='Type of algorithm to use') - + # Dataset parameters parser.add_argument('--dataset', type=str, default='xuanfeiren/math_hard_gemini', help='Dataset to use for training') @@ -197,7 +197,7 @@ def main(): help='Number of validation samples') parser.add_argument('--num_test_samples', type=int, default=20, help='Number of test samples') - + # LLM Model parameters parser.add_argument('--trace_model', type=str, default=None, help='Model to use for trace operations') @@ -205,7 +205,7 @@ def main(): help='Model to use for student agent') parser.add_argument('--teacher_model', type=str, default=None, help='Model to use for teacher guide') - + # Training parameters parser.add_argument('--num_epochs', type=int, default=1, help='Number of training epochs') @@ -219,7 +219,7 @@ def main(): help='How often to log results') parser.add_argument('--seed', type=int, default=42, help='Random seed for reproducibility') - + # Algorithm-specific parameters parser.add_argument('--beam_width', type=int, default=3, help='Beam width for beam search algorithms') @@ -233,7 +233,7 @@ def main(): help='Maximum history size for history-based algorithms') parser.add_argument('--num_basicsearch_proposals', type=int, default=2, help='Number of proposals for basic search algorithm') - + # UCB algorithm-specific parameters parser.add_argument('--max_buffer_size', type=int, default=5, help='Maximum buffer size for UCB algorithms') @@ -245,16 +245,16 @@ def main(): help='Training batch size for UCB algorithms') parser.add_argument('--evaluation_batch_size', type=int, default=20, help='Evaluation batch size for UCB algorithms') - + args = parser.parse_args() - + # Set environment variables if args.trace_model: os.environ["TRACE_LITELLM_MODEL"] = args.trace_model # Set random seed np.random.seed(args.seed) - + # Check for API Keys if not os.getenv("OPENAI_API_KEY") and not os.getenv("ANTHROPIC_API_KEY"): print_color("Warning: OPENAI_API_KEY or ANTHROPIC_API_KEY environment variables not found. LLM calls may fail.", "red") @@ -262,7 +262,7 @@ def main(): # Load and prepare data print(f"Loading data from {args.dataset}...") math_data = datasets.load_dataset(args.dataset) - + # Select data subsets train_data = math_data['train'].select( range(args.num_train_samples, args.num_train_samples + args.num_validate_samples) @@ -274,7 +274,7 @@ def main(): train_dataset = {'inputs': train_data['problem'], 'infos': train_data['solution']} validate_dataset = {'inputs': validate_data['problem'], 'infos': validate_data['solution']} test_dataset = {'inputs': test_data['problem'], 'infos': test_data['solution']} - + # Log dataset sizes print(f"Training samples: {len(train_dataset['inputs'])}") print(f"Validation samples: {len(validate_dataset['inputs'])}") @@ -290,7 +290,7 @@ def main(): optimizer = OptoPrime(agent.parameters()) logger = SimpleLogger() - + # Create algorithm if args.algorithm_type == 'minibatch': algorithm = MinibatchAlgorithm( @@ -331,7 +331,7 @@ def main(): ) else: raise ValueError(f"Unknown algorithm type: {args.algorithm_type}") - + # Prepare training parameters train_params = { "guide": train_guide, @@ -346,7 +346,7 @@ def main(): "log_frequency": args.log_frequency, "validation_dataset_size": args.validation_dataset_size, } - + # Add algorithm-specific parameters if args.algorithm_type in ['beamsearch', 'beamsearchhistory']: train_params.update({ @@ -354,33 +354,33 @@ def main(): "num_proposals": args.num_basicsearch_proposals, "max_depth": args.max_depth }) - + if args.algorithm_type == 'beamsearchhistory': train_params["max_history_size"] = args.max_history_size - + elif args.algorithm_type == 'basicsearch': train_params["num_proposals"] = args.num_basicsearch_proposals - + elif args.algorithm_type == 'UCBsearch': train_params.update({ "num_search_iterations": args.num_search_iterations, "train_batch_size": args.train_batch_size_ucb, "evaluation_batch_size": args.evaluation_batch_size }) - + # Start training print(f"Training with {args.algorithm_type} algorithm...") start_time = time.time() metrics, final_score = algorithm.train(**train_params) duration = time.time() - start_time print(f"Training complete, time taken: {duration:.2f} seconds") - + # Print metrics summary based on algorithm type if args.algorithm_type in ['beamsearch', 'beamsearchhistory'] and 'best_validation_scores' in metrics: print("\nBest validation scores at each depth:") for depth, score in enumerate(metrics['best_validation_scores']): print(f" Depth {depth+1}: {score:.4f}") - + elif args.algorithm_type == 'UCBsearch': print("\nUCB Algorithm Metrics:") if 'best_candidate_scores' in metrics and metrics['best_candidate_scores']: @@ -388,9 +388,9 @@ def main(): print(f" Final best candidate score: {metrics['best_candidate_scores'][-1]:.4f}") if 'buffer_avg_score' in metrics and metrics['buffer_avg_score']: print(f" Final buffer average score: {metrics['buffer_avg_score'][-1]:.4f}") - + print(f"Final score: {final_score:.4f}") - + return metrics, final_score diff --git a/opto/features/priority_search/sampler.py b/opto/features/priority_search/sampler.py index 3d46ea05..ce35f736 100644 --- a/opto/features/priority_search/sampler.py +++ b/opto/features/priority_search/sampler.py @@ -4,7 +4,7 @@ from typing import Union, List, Tuple, Dict, Any, Optional from opto import trace from opto.trainer.utils import batch_run -from opto.trainer.guide import AutoGuide +from opto.trainer.guide import Guide @dataclass class Rollout: @@ -88,8 +88,8 @@ def __init__(self, raise TypeError("xs must be a list.") if not isinstance(infos, list): raise TypeError("infos must be a list.") - if not isinstance(guide, AutoGuide): - raise TypeError("guide must be a AutoGuide.") + if not isinstance(guide, Guide): + raise TypeError("guide must be a Guide.") if len(xs) != len(infos): raise ValueError("Length of xs must match length of infos.") self.module = module @@ -189,7 +189,7 @@ def __init__(self, loader, guide, num_threads=1, sub_batch_size=None, forward=No Args: loader (DataLoader): The data loader to sample from. - guide (AutoGuide): The guide to evaluate the proposals. + guide (Guide): The guide to evaluate the proposals. num_threads (int): Number of threads to use for sampling. sub_batch_size (int, optional): Size of the sub-batch to use for sampling. If None, uses the batch size. score_range (tuple): The range of scores to consider valid. diff --git a/opto/trace/modules.py b/opto/trace/modules.py index 9310c2ff..f08d0165 100644 --- a/opto/trace/modules.py +++ b/opto/trace/modules.py @@ -17,7 +17,7 @@ def model(cls): class ModelWrapper(cls, Module): - def model_dump(self, filename, projections: Optional[List[Projection]] = None): + def export(self, filename, projections: Optional[List[Projection]] = None): """Dump the model's source code to a file, including all methods and attributes. Ignores dunder methods unless they were overridden by the user. """ @@ -48,7 +48,7 @@ def model_dump(self, filename, projections: Optional[List[Projection]] = None): # For dunder methods, check if they were overridden try: print(cls.__name__, "<>", member.__qualname__) - # MixedClass <> test_model_dump_mixed_trainable..MixedClass.__init__ + # MixedClass <> test_export_mixed_trainable..MixedClass.__init__ # if we wrap it inside a function, the qualname is different than when we dont if hasattr(member, '__qualname__') and cls.__name__ in member.__qualname__: filtered_members.append((name, member)) diff --git a/opto/trainer/algorithms/algorithm.py b/opto/trainer/algorithms/algorithm.py index b3506e23..25180ef9 100644 --- a/opto/trainer/algorithms/algorithm.py +++ b/opto/trainer/algorithms/algorithm.py @@ -2,7 +2,7 @@ from opto.trace.modules import Module from opto.trainer.loggers import DefaultLogger from opto.trainer.loader import DataLoader -from opto.trainer.guide import AutoGuide +from opto.trainer.guide import Guide from opto.optimizers.optimizer import Optimizer import os import pickle @@ -102,7 +102,7 @@ def save(self, path: str): _path = path+ f"_{key}.module" value.save(_path) d[key] = _path - elif isinstance(value, AutoGuide): + elif isinstance(value, Guide): _path = path + f"_{key}.guide" value.save(_path) d[key] = _path @@ -135,7 +135,7 @@ def load(self, path: str): assert isinstance(attr, Module), f"Expected {key} to be a Module, got {type(attr)}" elif value.endswith('.guide'): attr = self.__dict__[key] - assert isinstance(attr, AutoGuide), f"Expected {key} to be an AutoGuide, got {type(attr)}" + assert isinstance(attr, Guide), f"Expected {key} to be an Guide, got {type(attr)}" elif value.endswith('.dataloader'): attr = self.__dict__[key] assert isinstance(attr, DataLoader), f"Expected {key} to be a DataLoader, got {type(attr)}" diff --git a/opto/trainer/guide.py b/opto/trainer/guide.py index df465cc8..53f225ca 100644 --- a/opto/trainer/guide.py +++ b/opto/trainer/guide.py @@ -10,7 +10,7 @@ def exact_match_metric(question, student_answer, info): """ Exact match metric """ return float(student_answer == info) -class AutoGuide: +class Guide: """ Base class for all guides that provide feedback on content. @@ -69,7 +69,7 @@ def load(self, path: str): setattr(self, key, value) -class VerbalJudgeGuide(AutoGuide): +class LLMJudge(Guide): """ This is a combined metric + feedback guide that asks LLM to provide a binary judgment (True/False) and then if False, provide feedback. diff --git a/opto/trainer/utils.py b/opto/trainer/utils.py index 6f7ccc15..ffb6b999 100644 --- a/opto/trainer/utils.py +++ b/opto/trainer/utils.py @@ -5,7 +5,7 @@ from tqdm.asyncio import tqdm_asyncio from opto.trace.bundle import ALLOW_EXTERNAL_DEPENDENCIES from opto.trace.modules import Module -from opto.trainer.guide import AutoGuide +from opto.trainer.guide import Guide def async_run(runs, args_list = None, kwargs_list = None, max_workers = None, description = None, allow_sequential_run=True): """Run multiple functions in asynchronously. @@ -36,13 +36,13 @@ def async_run(runs, args_list = None, kwargs_list = None, max_workers = None, de if (max_workers == 1) and allow_sequential_run: # run without asyncio print(f"{description} (Running sequentially).") return [run(*args, **kwargs) for run, args, kwargs in zip(runs, args_list, kwargs_list)] - else: + else: async def _run(): loop = asyncio.get_event_loop() with ThreadPoolExecutor(max_workers=max_workers) as executor: - tasks = [loop.run_in_executor(executor, functools.partial(run, *args, **kwargs)) + tasks = [loop.run_in_executor(executor, functools.partial(run, *args, **kwargs)) for run, args, kwargs, in zip(runs, args_list, kwargs_list)] - + # Use the description in the tqdm progress bar if provided if description: return await tqdm_asyncio.gather(*tasks, desc=description) @@ -54,11 +54,11 @@ async def _run(): def batch_run(max_workers=None, description=None): """ Create a function that runs in parallel using asyncio, with support for batching. - The batch size is inferred as the length of the longest argument or keyword argument. + The batch size is inferred as the length of the longest argument or keyword argument. Args: fun (callable): The function to run. - + max_workers (int, optional): Maximum number of worker threads to use. If None, the default ThreadPoolExecutor behavior is used. description (str, optional): Description to display in the progress bar. @@ -66,9 +66,9 @@ def batch_run(max_workers=None, description=None): Returns: callable: A new function that processes batches of inputs. - NOTE: - If fun takes input that has __len__ (like lists or arrays), they won't be broadcasted. - When using batch_run, be sure to pass list of such arguments of the same length. + NOTE: + If fun takes input that has __len__ (like lists or arrays), they won't be broadcasted. + When using batch_run, be sure to pass list of such arguments of the same length. Example: >>> @batch_run(max_workers=4, description="Processing batch") @@ -78,33 +78,33 @@ def batch_run(max_workers=None, description=None): >>> y = 10 >>> outputs = my_function(x, y) >>> # outputs will be [11, 12, 13, 14, 15] - >>> # This will run the function in asynchronously with 4 threads + >>> # This will run the function in asynchronously with 4 threads """ - + def decorator(fun): """ Decorator to create a function that runs in parallel using asyncio, with support for batching. - + Args: fun (callable): The function to run. - + max_workers (int, optional): Maximum number of worker threads to use. If None, the default ThreadPoolExecutor behavior is used. description (str, optional): Description to display in the progress bar. Returns: callable: A new function that processes batches of inputs. - """ + """ def _fun(*args, **kwargs): - + # We try to infer the batch size from the args all_args = args + tuple(kwargs.values()) # find all list or array-like arguments and use their length as batch size batch_size = max(len(arg) for arg in all_args if hasattr(arg, '__len__')) - + # broadcast the batch size to all args and record the indices that are broadcasted args = [arg if hasattr(arg, '__len__') else [arg] * batch_size for arg in args] - kwargs = {k: v if hasattr(v, '__len__') else [v] * batch_size for k, v in kwargs.items()} + kwargs = {k: v if hasattr(v, '__len__') else [v] * batch_size for k, v in kwargs.items()} # assert that all args and kwargs have the same length lengths = [len(arg) for arg in args] + [len(v) for v in kwargs.values()] @@ -113,10 +113,10 @@ def _fun(*args, **kwargs): # deepcopy if it is a trace.Module (as they may have mutable state) # Module.copy() is used to create a new instance with the same parameters - _args = [[a.copy() if isinstance(a, (Module, AutoGuide)) else a for a in arg ] for arg in args ] - _kwargs = {k: [a.copy() if isinstance(a, (Module, AutoGuide)) else a for a in v ] for k, v in kwargs.items() } + _args = [[a.copy() if isinstance(a, (Module, Guide)) else a for a in arg ] for arg in args ] + _kwargs = {k: [a.copy() if isinstance(a, (Module, Guide)) else a for a in v ] for k, v in kwargs.items() } - # Run the forward function in parallel using asyncio with the same parameters. + # Run the forward function in parallel using asyncio with the same parameters. # Since trace.Node is treated as immutable, we can safely use the same instance. # The resultant graph will be the same as if we had called the function with the original arguments. @@ -145,25 +145,25 @@ def tester(t): # regular time-consuming function args_list = [(3,), (3,), (2,), (3,), (3,), (2,), (2,), (3,), (2,), (3,)] kwargs_list = [{}] * 10 import time - + # Example with 1 thread (runs sequentially) print("Running with 1 thread (sequential):") start = time.time() output = async_run(runs, args_list, kwargs_list, max_workers=1) print(f"Time with 1 thread: {time.time()-start:.2f} seconds") - + # Example with limited workers (2 threads) print("\nRunning with 2 threads (parallel):") start = time.time() output = async_run(runs, args_list, kwargs_list, max_workers=2) print(f"Time with 2 threads: {time.time()-start:.2f} seconds") - + # Example with limited workers (4 threads) print("\nRunning with 4 threads (parallel):") start = time.time() output = async_run(runs, args_list, kwargs_list, max_workers=4) print(f"Time with 4 threads: {time.time()-start:.2f} seconds") - + # Example with default number of workers print("\nRunning with default number of threads:") start = time.time() diff --git a/tests/unit_tests/test_batch_run.py b/tests/unit_tests/test_batch_run.py index 5da10ddb..daf983c3 100644 --- a/tests/unit_tests/test_batch_run.py +++ b/tests/unit_tests/test_batch_run.py @@ -22,10 +22,10 @@ def fun(x: List[int], y: List[int]) -> List[int]: return [a + b for a, b in zip(x, y)] x = [[1, 2, 3], [4, 5, 6]] - y = [10, 20, 30] # list won't be braodcasted correctly + y = [10, 20, 30] # list won't be braodcasted correctly raise_error = False - try: + try: outputs = fun(x, y) except ValueError as e: assert str(e) == "All arguments and keyword arguments must have the same length.", f"Unexpected error: {e}" @@ -38,9 +38,9 @@ def fun(x: List[int], y: List[int]) -> List[int]: assert outputs == [[11, 22, 33], [14, 25, 36]], f"Expected [[11, 22, 33], [14, 25, 36]], got {outputs}" # This will raise an error because x and y have different lengths - # y = [10, 20] + # y = [10, 20] # outputs = fun(x, y) - + def test_batch_run_module(): @@ -49,12 +49,12 @@ class MyModule: def __init__(self, param): self.param = trace.node(param, trainable=True) self._state = 0 - + def forward(self, x): y = x + self.param self._state += 1 # This should not affect the batch run return y - + module = MyModule(10) x = [1, 2, 3, 4, 5] outputs = batch_run(max_workers=3)(module.forward)(x) @@ -67,7 +67,7 @@ def forward(self, x): y = [10, 20, 30, 40, 50, 60] # This should raise an error because x and y have different lengths raise_error = False - try: + try: outputs = batch_run(max_workers=3)(module.forward)(x, y) except ValueError as e: assert str(e) == "All arguments and keyword arguments must have the same length.", f"Unexpected error: {e}" @@ -75,40 +75,40 @@ def forward(self, x): assert raise_error, "Expected a ValueError but did not get one." -def test_evaluate(): +def test_evaluate(): # This test the evaluate function in opto.trainer.evaluators built on top of batch_run from opto.trainer.evaluators import evaluate - from opto.trainer.guide import AutoGuide + from opto.trainer.guide import Guide from opto import trace @trace.model class MyAgent: def __init__(self, param): - self.param = trace.node(param, trainable=True) - + self.param = trace.node(param, trainable=True) + def forward(self, x): y = x + self.param self.param += 1 # This should not affect the batch run return y - - class MyGuide(AutoGuide): + + class MyGuide(Guide): def __init__(self, param): super().__init__() self.param = param def get_feedback(self, query, response, reference=None): score = float(response == query + self.param + reference) - feedback = f"Score: {score}, Response: {response}, Query: {query}" + feedback = f"Score: {score}, Response: {response}, Query: {query}" self.param += 1 # This should not affect the batch run return score, feedback - + agent = MyAgent(10) guide = MyGuide(10) inputs = [1, 2, 3, 4, 5] infos = [0, 1, 2, 3, 4] # These are the expected outputs (query + param + info) evaluated_scores = evaluate(agent, guide, inputs, infos, num_samples=1, num_threads=1) expected_scores = [1, 0, 0, 0, 0] # All inputs should match the expected outputs - assert (evaluated_scores == expected_scores).all(), f"Expected {expected_scores}, got {evaluated_scores}" + assert (evaluated_scores == expected_scores).all(), f"Expected {expected_scores}, got {evaluated_scores}" evaluated_scores = evaluate(agent, guide, inputs, infos, num_samples=2, num_threads=1) diff --git a/tests/unit_tests/test_modules.py b/tests/unit_tests/test_modules.py index 7e93f049..f5a5d6cc 100644 --- a/tests/unit_tests/test_modules.py +++ b/tests/unit_tests/test_modules.py @@ -157,7 +157,7 @@ def test_multiple_inheritance(): assert result._data == 2 -# Test cases for model_dump +# Test cases for export @model class DummyClass: def __init__(self): @@ -189,12 +189,12 @@ def complex_method(self, x): def __str__(self): return "ComplexClass" -def test_model_dump_basic(): +def test_export_basic(): dummy = DummyClass() dummy._param._data = 42 # Change the node value temp_file = "temp_dummy.py" try: - dummy.model_dump(temp_file) + dummy.export(temp_file) with open(temp_file, "r") as f: content = f.read() # Check if class definition is present @@ -214,11 +214,11 @@ def test_model_dump_basic(): if os.path.exists(temp_file): os.remove(temp_file) -def test_model_dump_complex(): +def test_export_complex(): complex_obj = ComplexClass() temp_file = "temp_complex.py" try: - complex_obj.model_dump(temp_file) + complex_obj.export(temp_file) with open(temp_file, "r") as f: content = f.read() # Check if class definition is present @@ -233,13 +233,13 @@ def test_model_dump_complex(): if os.path.exists(temp_file): os.remove(temp_file) -def test_model_dump_with_projection(): +def test_export_with_projection(): dummy = DummyClass() temp_file = "temp_dummy_formatted.py" try: # Test with BlackCodeFormatter from opto.trace.projections import BlackCodeFormatter - dummy.model_dump(temp_file, projections=[BlackCodeFormatter()]) + dummy.export(temp_file, projections=[BlackCodeFormatter()]) with open(temp_file, "r") as f: content = f.read() # Check if content is properly formatted @@ -265,13 +265,13 @@ def non_trainable_method(self, x): def another_non_trainable(self, y): return y + 1 -def test_model_dump_non_trainable(): +def test_export_non_trainable(): obj = NonTrainableClass() obj._param._data = 10 # Change node value obj._param2._data = 20 # Change another node value temp_file = "temp_non_trainable.py" try: - obj.model_dump(temp_file) + obj.export(temp_file) with open(temp_file, "r") as f: content = f.read() # Check if class definition is present @@ -292,7 +292,7 @@ def test_model_dump_non_trainable(): if os.path.exists(temp_file): os.remove(temp_file) -def test_model_dump_mixed_trainable(): +def test_export_mixed_trainable(): @model class MixedClass: @@ -319,7 +319,7 @@ def non_trainable_method(self, y): temp_file = "temp_mixed.py" try: - obj.model_dump(temp_file) + obj.export(temp_file) with open(temp_file, "r") as f: content = f.read() # Check if class definition is present @@ -341,7 +341,7 @@ def non_trainable_method(self, y): if os.path.exists(temp_file): os.remove(temp_file) -def test_model_dump_and_import(): +def test_export_and_import(): @model class StrangeCalculator: def __init__(self): @@ -369,7 +369,7 @@ def multiply(self, x, y): # Dump the model temp_file = "temp_calculator.py" try: - calc.model_dump(temp_file) + calc.export(temp_file) # Import the dumped class import importlib.util diff --git a/tests/unit_tests/test_priority_search.py b/tests/unit_tests/test_priority_search.py index 6f5fa85b..2ebda047 100644 --- a/tests/unit_tests/test_priority_search.py +++ b/tests/unit_tests/test_priority_search.py @@ -4,7 +4,7 @@ from opto.features.priority_search.priority_search import PrioritySearch as _PrioritySearch from opto.features.priority_search.priority_search import ModuleCandidate from opto.optimizers import OptoPrimeV2 -from opto.trainer.guide import AutoGuide +from opto.trainer.guide import Guide from opto.utils.llm import DummyLLM import re @@ -12,7 +12,7 @@ import copy -class Guide(AutoGuide): +class Guide(Guide): def get_feedback(self, query, response, reference=None, **kwargs): """ diff --git a/tests/unit_tests/test_sampler.py b/tests/unit_tests/test_sampler.py index 2dc92439..c1a70fdb 100644 --- a/tests/unit_tests/test_sampler.py +++ b/tests/unit_tests/test_sampler.py @@ -1,11 +1,11 @@ from opto import trace from opto.features.priority_search.sampler import Sampler from opto.trainer.loader import DataLoader -from opto.trainer.guide import AutoGuide +from opto.trainer.guide import Guide from opto.features.priority_search.utils import is_node_copy -class Guide(AutoGuide): +class Guide(Guide): def get_feedback(self, query, response, reference=None, **kwargs): """ diff --git a/tests/unit_tests/test_saving_loading.py b/tests/unit_tests/test_saving_loading.py index c0444512..06f09a54 100644 --- a/tests/unit_tests/test_saving_loading.py +++ b/tests/unit_tests/test_saving_loading.py @@ -4,7 +4,7 @@ from opto.trainer.loader import DataLoader from opto.trainer.algorithms import BasicSearchAlgorithm from opto.optimizers import OptoPrimeV2 -from opto.trainer.guide import AutoGuide +from opto.trainer.guide import Guide as _Guide from opto.utils.llm import DummyLLM import re, os @@ -40,7 +40,7 @@ def test_saving_load(): def test_trainer_saving_loading(): - class Guide(AutoGuide): + class Guide(_Guide): def get_feedback(self, query, response, reference=None, **kwargs): """ From 757687e2220c31fb80fd9267a18c7674644a8fa5 Mon Sep 17 00:00:00 2001 From: chinganc Date: Fri, 15 Aug 2025 21:46:19 +0000 Subject: [PATCH 156/314] Rename AlgorithmBase to Trainer --- opto/features/priority_search/utils.py | 2 +- opto/trainer/algorithms/UCBsearch.py | 6 +++--- opto/trainer/algorithms/algorithm.py | 2 +- opto/trainer/algorithms/basic_algorithms.py | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/opto/features/priority_search/utils.py b/opto/features/priority_search/utils.py index c12e3ded..e4c6906d 100644 --- a/opto/features/priority_search/utils.py +++ b/opto/features/priority_search/utils.py @@ -7,7 +7,7 @@ from opto.trace.nodes import ParameterNode from opto.trainer.utils import async_run, batch_run from opto.optimizers.utils import print_color -from opto.trainer.algorithms.basic_algorithms import Minibatch, AlgorithmBase, batchify +from opto.trainer.algorithms.basic_algorithms import Minibatch, Trainer, batchify from opto.trainer.loader import DataLoader from opto.features.priority_search.sampler import Sampler, RolloutsGraph import time diff --git a/opto/trainer/algorithms/UCBsearch.py b/opto/trainer/algorithms/UCBsearch.py index 71387ace..21bc9455 100644 --- a/opto/trainer/algorithms/UCBsearch.py +++ b/opto/trainer/algorithms/UCBsearch.py @@ -100,8 +100,8 @@ def _evaluate_candidate(self, self.optimizer.update(original_params) avg_score = np.mean(eval_scores) if ((eval_scores is not None) and all(s is not None for s in eval_scores)) else -np.inf - eval_count = len(eval_xs) - + eval_count = len(eval_xs) + return float(avg_score), eval_count def _calculate_ucb(self, candidate_buffer_entry: Dict, total_tracked_evaluations: int) -> float: @@ -337,7 +337,7 @@ def train(self, if save_frequency is not None and iteration % save_frequency == 0: best_overall_candidate = max(self.buffer, key=lambda c: c['score_sum'] / (c['eval_count'] or 1E-9) ) self.optimizer.update(best_overall_candidate['params']) # Load params using optimizer - self.save_agent(save_path, iteration) # save_agent is from AlgorithmBase + self.save_agent(save_path, iteration) # save_agent is from Trainer print_color(f"Iter {iteration}: Saved agent based on best candidate in buffer.", 'green') # End of search loop diff --git a/opto/trainer/algorithms/algorithm.py b/opto/trainer/algorithms/algorithm.py index 25180ef9..326e1be2 100644 --- a/opto/trainer/algorithms/algorithm.py +++ b/opto/trainer/algorithms/algorithm.py @@ -18,7 +18,7 @@ def train(self, *args, **kwargs): pass -class AlgorithmBase(AbstractAlgorithm): +class Trainer(AbstractAlgorithm): """ We define the API of algorithms to train an agent from a dataset of (x, info) pairs. diff --git a/opto/trainer/algorithms/basic_algorithms.py b/opto/trainer/algorithms/basic_algorithms.py index 194bb1c9..e9840851 100644 --- a/opto/trainer/algorithms/basic_algorithms.py +++ b/opto/trainer/algorithms/basic_algorithms.py @@ -2,7 +2,7 @@ import copy from typing import Union from opto import trace -from opto.trainer.algorithms.algorithm import AlgorithmBase +from opto.trainer.algorithms.algorithm import Trainer from opto.trainer.loader import DataLoader from opto.trainer.utils import batch_run, async_run from opto.optimizers.utils import print_color @@ -33,7 +33,7 @@ def standard_optimization_step(agent, x, guide, info, min_score=0): return target, score, feedback -class Minibatch(AlgorithmBase): +class Minibatch(Trainer): """ General minibatch optimization algorithm. This class defines a general training and logging routine using minimbatch sampling.""" def __init__(self, From 47be61d1545b56ae653a2baf22d3c75d79828e78 Mon Sep 17 00:00:00 2001 From: chinganc Date: Fri, 15 Aug 2025 22:48:32 +0000 Subject: [PATCH 157/314] Update version number to v0.2.0 --- opto/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opto/version.py b/opto/version.py index a2d08535..d3ec452c 100644 --- a/opto/version.py +++ b/opto/version.py @@ -1 +1 @@ -__version__ = "0.1.3.9" +__version__ = "0.2.0" From 81f3fbd4fde1371853729c1e9ee2997c58b67acc Mon Sep 17 00:00:00 2001 From: windweller Date: Tue, 19 Aug 2025 18:31:06 -0400 Subject: [PATCH 158/314] Fix mkdocstrings cross-reference warnings in LLM docstring - Update docstring format to avoid mkdocstrings parsing issues - Resolves 'Could not find cross-reference target' warnings in strict mode --- opto/utils/llm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opto/utils/llm.py b/opto/utils/llm.py index 320ba2b2..a53abbc7 100644 --- a/opto/utils/llm.py +++ b/opto/utils/llm.py @@ -32,7 +32,7 @@ def __init__(self, factory: Callable, reset_freq: Union[int, None] = None) -> No # Overwrite this `model` property when subclassing. @property def model(self): - """ When self.model is called, text responses should always be available at ['choices'][0].['message']['content'] """ + """When self.model is called, text responses should always be available at `response['choices'][0]['message']['content']`""" return self._model # This is the main API From e32e379ec6ccd1aa0fe8b3e34eb3db9fb61d89dc Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 19 Aug 2025 23:54:37 +0000 Subject: [PATCH 159/314] Fix missing oprov2 problem --- opto/optimizers/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/opto/optimizers/__init__.py b/opto/optimizers/__init__.py index 9b0b2007..482b1b2d 100644 --- a/opto/optimizers/__init__.py +++ b/opto/optimizers/__init__.py @@ -1,9 +1,10 @@ from opto.optimizers.optoprime import OptoPrime as OptoPrimeV1 from opto.optimizers.optoprimemulti import OptoPrimeMulti from opto.optimizers.opro import OPRO +from opto.optimizers.opro_v2 import OPROv2 from opto.optimizers.textgrad import TextGrad from opto.optimizers.optoprime_v2 import OptoPrimeV2 OptoPrime = OptoPrimeV1 -__all__ = ["OPRO", "OptoPrime", "OptoPrimeMulti", "TextGrad", "OptoPrimeV2", "OptoPrimeV1"] \ No newline at end of file +__all__ = ["OPRO", "OptoPrime", "OptoPrimeMulti", "TextGrad", "OptoPrimeV2", "OptoPrimeV1", "OPROv2"] \ No newline at end of file From e837488d4cbdd29eae4f2bb49372a8440334a337 Mon Sep 17 00:00:00 2001 From: chinganc Date: Wed, 20 Aug 2025 00:27:39 +0000 Subject: [PATCH 160/314] Add an assertion to make optimizer receives non-empty parameters. --- opto/optimizers/optimizer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/opto/optimizers/optimizer.py b/opto/optimizers/optimizer.py index 04f8ea5e..2b175d5f 100644 --- a/opto/optimizers/optimizer.py +++ b/opto/optimizers/optimizer.py @@ -12,6 +12,7 @@ class AbstractOptimizer: def __init__(self, parameters: List[ParameterNode], *args, **kwargs): assert type(parameters) is list assert all([isinstance(p, ParameterNode) for p in parameters]) + assert len(parameters) > 0, 'Parameters list is empty.' self.parameters = parameters def step(self): From e58a9317bf2eedda4962119a5e3882a2a55e9f05 Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 20 Aug 2025 17:09:01 -0400 Subject: [PATCH 161/314] quick comment cleanup of opro_v2 --- opto/optimizers/opro_v2.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/opto/optimizers/opro_v2.py b/opto/optimizers/opro_v2.py index 3b66c14a..13054943 100644 --- a/opto/optimizers/opro_v2.py +++ b/opto/optimizers/opro_v2.py @@ -5,14 +5,6 @@ from opto.optimizers.optoprime_v2 import OptoPrimeV2, OptimizerPromptSymbolSet -""" -OPRO is a single parameter / solution optimizer that conditions on feedback. -(context, solution, feedback) -> new_solution - -It does not contain execution graph and is more streamlined/faster in inference. -""" - - # Not inheriting from optoprime_v2 because this should have a smaller set class OPROPromptSymbolSet(OptimizerPromptSymbolSet): @@ -69,11 +61,6 @@ def __repr__(self) -> str: feedback=self.feedback, ) -""" -TODO: -1. think about how initial solution was generated... -""" - class OPROv2(OptoPrimeV2): representation_prompt = dedent( """ From e64dc2d11370cfbdd190edee79e19d37713ddbd9 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 21 Aug 2025 06:26:54 +0000 Subject: [PATCH 162/314] Add a prototype --- opto/trainer/algorithms/__init__.py | 1 + opto/trainer/train.py | 116 ++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 opto/trainer/train.py diff --git a/opto/trainer/algorithms/__init__.py b/opto/trainer/algorithms/__init__.py index 2586fd31..09333a7f 100644 --- a/opto/trainer/algorithms/__init__.py +++ b/opto/trainer/algorithms/__init__.py @@ -1,3 +1,4 @@ +from opto.trainer.algorithms.algorithm import Trainer from opto.trainer.algorithms.basic_algorithms import Minibatch, MinibatchAlgorithm, BasicSearchAlgorithm from opto.trainer.algorithms.beamsearch_algorithm import BeamsearchAlgorithm, BeamsearchHistoryAlgorithm from opto.trainer.algorithms.UCBsearch import UCBSearchAlgorithm diff --git a/opto/trainer/train.py b/opto/trainer/train.py new file mode 100644 index 00000000..e4dbf2fb --- /dev/null +++ b/opto/trainer/train.py @@ -0,0 +1,116 @@ +from typing import Union +import importlib + +from opto import trace +from opto.train.algorithms import Trainer +from opto.trainer.guide import Guide +from opto.trainer.loggers import BaseLogger +from opto.optimizers.optimizer import Optimzier + + + +def dataset_check(dataset): + assert isinstance(dataset, dict), "Dataset must be a dictionary" + assert 'inputs' in dataset and 'infos' in dataset, "Dataset must contain 'inputs' and 'infos' keys" + assert len(dataset['inputs'])==len(dataset['infos']), "Inputs and infos must have the same length" + + + +def train( + model: trace.Module, + guide: Guide, + train_dataset: dict, + # TODO update the acceptable type of optimizer, trainer, guide, logger to be union of base class and str + optimizer: Union[Optimizer, str] = "OptoPrimeV2", + trainer: Union[Trainer, str] = 'BasicSearchAlgorithm', + guide: Union[Guide, str] = 'LLMGuide', + logger: Union[BaseLogger, str] = 'ConsoleLogger', + # extra configs + optimizer_kwargs: Union[dict, None] = None, + trainer_kwargs: Union[dict, None] = None # for train function + # TODO other kwargs +) -> None: + + """ A high-level helper function to train the model using trainer. """ + optimizer_kwargs = optimizer_kwargs or {} # this can be used to pass extra optimizer configs, like llm object explictly + trainer_kwargs = trainer_kwargs or {} + + # TODO check eligible optimizer, trainer + dataset_check(train_dataset) + + # TODO remove duplicate codes + + # Load optimizer from opto.optimizers + parameters = agent.parameters() + assert len(parameters) >0, "Agent must have parameters." + if type(optimizer) is str: + # check if optimizer is a valid class + optimizers_module = importlib.import_module("opto.optimizers") + optimizer_class = getattr(optimizers_module, optimizer) + optimizer = optimizer_class( + model.parameters(), + **optimizer_kwargs + ) + # else if optimizer is an instance + elif issubclass(optimizer, Optimizer): + optimizer = optimizer( + model.parameters(), + **optimizer_kwargs + ) + else: + raise ValueError(f"Invalid optimizer type: {type(optimizer)}") + + # Load guide from opto.trainer.guide + if type(guide) is str: + # check if guide is a valid class + guides_module = importlib.import_module("opto.trainer.guide") + guide_class = getattr(guides_module, guide) + guide = guide_class( + **guide_kwargs + ) + # else if guide is an instance + elif issubclass(guide, Guide): + guide = guide( + **guide_kwargs + ) + else: + raise ValueError(f"Invalid guide type: {type(guide)}") + + # Load logger from opto.trainer.loggers + if type(logger) is str: + # check if logger is a valid class + loggers_module = importlib.import_module("opto.trainer.loggers") + logger_class = getattr(loggers_module, logger) + logger = logger_class(**logger_kwargs) + # else if logger is an instance + elif issubclass(logger, BaseLogger): + logger = logger( + **logger_kwargs + ) + else: + raise ValueError(f"Invalid logger type: {type(logger)}") + + + # Load trainer from opto.trainer.algorithms + if type(trainer) is str: + # check if trainer is a valid class + trainers_module = importlib.import_module("opto.trainer.algorithms") + trainer_class = getattr(trainers_module, trainer) + trainer = trainer_class( + agent, + optimizer, + logger + ) + # else if trainer is an instance + elif issubclass(trainer, Trainer): + trainer = trainer( + agent, + optimizer, + logger + ) + else: + raise ValueError(f"Invalid trainer type: {type(trainer)}") + + + # TODO start training + trainer.train(**trainer_kwargs) \ No newline at end of file From 5292422f2e6819288210f9ef204fe8dee192fbee Mon Sep 17 00:00:00 2001 From: chinganc Date: Fri, 22 Aug 2025 21:53:09 +0000 Subject: [PATCH 163/314] Update train.py --- opto/trainer/train.py | 79 +++++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 41 deletions(-) diff --git a/opto/trainer/train.py b/opto/trainer/train.py index e4dbf2fb..92eefe63 100644 --- a/opto/trainer/train.py +++ b/opto/trainer/train.py @@ -2,93 +2,93 @@ import importlib from opto import trace -from opto.train.algorithms import Trainer +from opto.trainer.algorithms import Trainer from opto.trainer.guide import Guide from opto.trainer.loggers import BaseLogger from opto.optimizers.optimizer import Optimzier - def dataset_check(dataset): assert isinstance(dataset, dict), "Dataset must be a dictionary" assert 'inputs' in dataset and 'infos' in dataset, "Dataset must contain 'inputs' and 'infos' keys" assert len(dataset['inputs'])==len(dataset['infos']), "Inputs and infos must have the same length" - def train( model: trace.Module, guide: Guide, train_dataset: dict, - # TODO update the acceptable type of optimizer, trainer, guide, logger to be union of base class and str - optimizer: Union[Optimizer, str] = "OptoPrimeV2", + # class of optimizer trainer: Union[Trainer, str] = 'BasicSearchAlgorithm', + optimizer: Union[Optimizer, str] = "OptoPrimeV2", guide: Union[Guide, str] = 'LLMGuide', logger: Union[BaseLogger, str] = 'ConsoleLogger', # extra configs optimizer_kwargs: Union[dict, None] = None, - trainer_kwargs: Union[dict, None] = None # for train function - # TODO other kwargs + guide_kwargs: Union[dict, None] = None, + logger_kwargs: Union[dict, None] = None, + # The rest is treated as trainer config + **trainer_kwargs, ) -> None: + """ A high-level helper function to train the model using trainer. + + A trainer algorithm applies an optimizer to train a model under a guide on a train_dataset. - """ A high-level helper function to train the model using trainer. """ + """ optimizer_kwargs = optimizer_kwargs or {} # this can be used to pass extra optimizer configs, like llm object explictly - trainer_kwargs = trainer_kwargs or {} + guide_kwargs = guide_kwargs or {} + logger_kwargs = logger_kwargs or {} # TODO check eligible optimizer, trainer dataset_check(train_dataset) # TODO remove duplicate codes - # Load optimizer from opto.optimizers + # Check agent parameters is non-empty parameters = agent.parameters() assert len(parameters) >0, "Agent must have parameters." + + + # Load optimizer from opto.optimizers if type(optimizer) is str: # check if optimizer is a valid class optimizers_module = importlib.import_module("opto.optimizers") optimizer_class = getattr(optimizers_module, optimizer) - optimizer = optimizer_class( - model.parameters(), - **optimizer_kwargs - ) # else if optimizer is an instance elif issubclass(optimizer, Optimizer): - optimizer = optimizer( - model.parameters(), - **optimizer_kwargs - ) + optimizer_class = optimizer else: raise ValueError(f"Invalid optimizer type: {type(optimizer)}") + optimizer = optimizer_class( + model.parameters(), + **optimizer_kwargs + ) # Load guide from opto.trainer.guide if type(guide) is str: # check if guide is a valid class guides_module = importlib.import_module("opto.trainer.guide") guide_class = getattr(guides_module, guide) - guide = guide_class( - **guide_kwargs - ) # else if guide is an instance elif issubclass(guide, Guide): - guide = guide( - **guide_kwargs - ) + guide_class = guide else: raise ValueError(f"Invalid guide type: {type(guide)}") + guide = guide_class( + **guide_kwargs + ) # Load logger from opto.trainer.loggers if type(logger) is str: # check if logger is a valid class loggers_module = importlib.import_module("opto.trainer.loggers") logger_class = getattr(loggers_module, logger) - logger = logger_class(**logger_kwargs) # else if logger is an instance elif issubclass(logger, BaseLogger): - logger = logger( - **logger_kwargs - ) + logger_class = logger else: raise ValueError(f"Invalid logger type: {type(logger)}") + logger = logger_class(**logger_kwargs) # Load trainer from opto.trainer.algorithms @@ -96,21 +96,18 @@ def train( # check if trainer is a valid class trainers_module = importlib.import_module("opto.trainer.algorithms") trainer_class = getattr(trainers_module, trainer) - trainer = trainer_class( - agent, - optimizer, - logger - ) # else if trainer is an instance elif issubclass(trainer, Trainer): - trainer = trainer( - agent, - optimizer, - logger - ) + trainer_class = trainer else: raise ValueError(f"Invalid trainer type: {type(trainer)}") + trainer = trainer_class( + agent, + optimizer, + logger + ) - - # TODO start training - trainer.train(**trainer_kwargs) \ No newline at end of file + return trainer.train( + guide=guide, + train_dataset=train_dataset, + **trainer_kwargs) From e5c749a04d6865b6901650e08508c75c4d79a91a Mon Sep 17 00:00:00 2001 From: chinganc Date: Fri, 22 Aug 2025 22:22:49 +0000 Subject: [PATCH 164/314] Make train runnable and add an example code --- examples/train_example.py | 81 +++++++++++++++++++++++++++++++++ opto/trainer/__init__.py | 1 + opto/trainer/guide.py | 2 +- opto/trainer/train.py | 94 +++++++++++++++++++++------------------ 4 files changed, 133 insertions(+), 45 deletions(-) create mode 100644 examples/train_example.py diff --git a/examples/train_example.py b/examples/train_example.py new file mode 100644 index 00000000..a01bc594 --- /dev/null +++ b/examples/train_example.py @@ -0,0 +1,81 @@ +import datasets +import numpy as np +from opto import trace, trainer +from opto.utils.llm import LLM, LiteLLM + +from typing import Any + + +def call_llm(llm, system_prompt: str, user_prompt_template: str, message: str) -> str: + if '{message}' not in user_prompt_template: + raise ValueError("user_prompt_template must contain '{message}'") + response = llm( + messages=[{"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt_template.format(message=message)}] + ) + return response.choices[0].message.content + + +@trace.model +class Learner: + """ A basic LLM agent. """ + + def __init__(self, system_prompt: str = "You're a helpful agent", + user_prompt_template: str = "Query: {message}", + llm: LLM = None): + self.system_prompt = trace.node(system_prompt, trainable=True) + self.user_prompt_template = trace.node(user_prompt_template) + self.llm = llm or LLM() + + @trace.bundle() + def model(self, system_prompt: str, user_prompt_template: str, message: str) -> str: + """Call the LLM model. + + Args: + system_prompt: the system prompt to the agent. By tuning this prompt, we can control the behavior of the agent. For example, it can be used to provide instructions to the agent (such as how to reason about the problem, how to answer the question), or provide in-context examples of how to solve the problem. + user_prompt_template: the user prompt template to the agent. It is used as formatting the input to the agent as user_prompt_template.format(message=message). + message: the input to the agent. It can be a query, a task, a code, etc. + Returns: + The response from the agent. + """ + return call_llm(self.llm, self.system_prompt, self.user_prompt_template, message) + + def forward(self, message: Any) -> Any: + """ Forward pass of the agent. """ + return self.model(self.system_prompt, self.user_prompt_template, message) + + + +def main(): + # set seed + seed = 42 + num_epochs = 1 + batch_size = 3 # number of queries to sample from the training data + eval_frequency = -1 + + num_threads = 10 + datasize = 5 + + np.random.seed(seed) + + # In this example, we use the GSM8K dataset, which is a dataset of math word problems. + # We will look the training error of the agent on a small portion of this dataset. + train_dataset = datasets.load_dataset('BBEH/bbeh')['train'][:datasize] + train_dataset = dict(inputs=train_dataset['input'], infos=train_dataset['target']) + + agent = Learner(llm=LLM()) + + trainer.train( + model=agent, + train_dataset=train_dataset, + # trainer kwargs + num_epochs=num_epochs, + batch_size=batch_size, + eval_frequency=eval_frequency, + num_threads=num_threads, + verbose='output', + ) + + +if __name__ == "__main__": + main() diff --git a/opto/trainer/__init__.py b/opto/trainer/__init__.py index e69de29b..de4d59a8 100644 --- a/opto/trainer/__init__.py +++ b/opto/trainer/__init__.py @@ -0,0 +1 @@ +from opto.trainer.train import train \ No newline at end of file diff --git a/opto/trainer/guide.py b/opto/trainer/guide.py index 53f225ca..72e4b918 100644 --- a/opto/trainer/guide.py +++ b/opto/trainer/guide.py @@ -95,7 +95,7 @@ def __init__(self, prompt_template: Optional[str] = None, system_prompt: Optional[str] = None, correctness_template: Optional[str] = None, - use_formatted_response: bool = True + use_formatted_response: bool = False ): """ Initialize the VerbalGuide with an LLM and prompt templates. diff --git a/opto/trainer/train.py b/opto/trainer/train.py index 92eefe63..7e603a12 100644 --- a/opto/trainer/train.py +++ b/opto/trainer/train.py @@ -5,7 +5,7 @@ from opto.trainer.algorithms import Trainer from opto.trainer.guide import Guide from opto.trainer.loggers import BaseLogger -from opto.optimizers.optimizer import Optimzier +from opto.optimizers.optimizer import Optimizer def dataset_check(dataset): @@ -15,13 +15,13 @@ def dataset_check(dataset): def train( + *, model: trace.Module, - guide: Guide, train_dataset: dict, # class of optimizer - trainer: Union[Trainer, str] = 'BasicSearchAlgorithm', + algorithm: Union[Trainer, str] = 'BasicSearchAlgorithm', optimizer: Union[Optimizer, str] = "OptoPrimeV2", - guide: Union[Guide, str] = 'LLMGuide', + guide: Union[Guide, str] = 'LLMJudge', logger: Union[BaseLogger, str] = 'ConsoleLogger', # extra configs optimizer_kwargs: Union[dict, None] = None, @@ -44,70 +44,76 @@ def train( # TODO remove duplicate codes - # Check agent parameters is non-empty - parameters = agent.parameters() - assert len(parameters) >0, "Agent must have parameters." + # Check model parameters is non-empty + parameters = model.parameters() + assert len(parameters) >0, "Model must have non-empty parameters." + optimizer = load_optimizer(optimizer, model, **optimizer_kwargs) + guide = load_guide(guide, **guide_kwargs) + logger = load_logger(logger, **logger_kwargs) + trainer_class = load_trainer_class(algorithm) - # Load optimizer from opto.optimizers - if type(optimizer) is str: - # check if optimizer is a valid class + assert isinstance(optimizer, Optimizer) + assert isinstance(guide, Guide) + assert isinstance(logger, BaseLogger) + assert issubclass(trainer_class, Trainer) + + algo = trainer_class( + model, + optimizer, + logger + ) + + return algo.train( + guide=guide, + train_dataset=train_dataset, + **trainer_kwargs) + + +def load_optimizer(optimizer: Union[Optimizer, str], model: trace.Module, **kwargs) -> Optimizer: + if isinstance(optimizer, Optimizer): + return optimizer + elif isinstance(optimizer, str): optimizers_module = importlib.import_module("opto.optimizers") optimizer_class = getattr(optimizers_module, optimizer) - # else if optimizer is an instance + return optimizer_class(model.parameters(), **kwargs) elif issubclass(optimizer, Optimizer): - optimizer_class = optimizer + return optimizer(model.parameters(), **kwargs) else: raise ValueError(f"Invalid optimizer type: {type(optimizer)}") - optimizer = optimizer_class( - model.parameters(), - **optimizer_kwargs - ) - # Load guide from opto.trainer.guide - if type(guide) is str: - # check if guide is a valid class + +def load_guide(guide: Union[Guide, str], **kwargs) -> Guide: + if isinstance(guide, Guide): + return guide + elif isinstance(guide, str): guides_module = importlib.import_module("opto.trainer.guide") guide_class = getattr(guides_module, guide) - # else if guide is an instance + return guide_class(**kwargs) elif issubclass(guide, Guide): - guide_class = guide + return guide(**kwargs) else: raise ValueError(f"Invalid guide type: {type(guide)}") - guide = guide_class( - **guide_kwargs - ) - # Load logger from opto.trainer.loggers - if type(logger) is str: - # check if logger is a valid class +def load_logger(logger: Union[BaseLogger, str], **kwargs) -> BaseLogger: + if isinstance(logger, BaseLogger): + return logger + elif isinstance(logger, str): loggers_module = importlib.import_module("opto.trainer.loggers") logger_class = getattr(loggers_module, logger) - # else if logger is an instance + return logger_class(**kwargs) elif issubclass(logger, BaseLogger): - logger_class = logger + return logger(**kwargs) else: raise ValueError(f"Invalid logger type: {type(logger)}") - logger = logger_class(**logger_kwargs) - - # Load trainer from opto.trainer.algorithms - if type(trainer) is str: - # check if trainer is a valid class +def load_trainer_class(trainer: Union[Trainer, str]) -> Trainer: + if isinstance(trainer, str): trainers_module = importlib.import_module("opto.trainer.algorithms") trainer_class = getattr(trainers_module, trainer) - # else if trainer is an instance elif issubclass(trainer, Trainer): trainer_class = trainer else: raise ValueError(f"Invalid trainer type: {type(trainer)}") - trainer = trainer_class( - agent, - optimizer, - logger - ) - return trainer.train( - guide=guide, - train_dataset=train_dataset, - **trainer_kwargs) + return trainer_class \ No newline at end of file From e3c3a40ad2d99f3bb667fed55f0edaf86816c43b Mon Sep 17 00:00:00 2001 From: chinganc Date: Fri, 22 Aug 2025 22:29:10 +0000 Subject: [PATCH 165/314] Fix a bug in the example code. Set minibatch's ensure improvement to be true. --- examples/train_example.py | 2 +- opto/trainer/algorithms/basic_algorithms.py | 2 +- opto/trainer/train.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/train_example.py b/examples/train_example.py index a01bc594..10b76e0a 100644 --- a/examples/train_example.py +++ b/examples/train_example.py @@ -38,7 +38,7 @@ def model(self, system_prompt: str, user_prompt_template: str, message: str) -> Returns: The response from the agent. """ - return call_llm(self.llm, self.system_prompt, self.user_prompt_template, message) + return call_llm(self.llm, system_prompt, user_prompt_template, message) def forward(self, message: Any) -> Any: """ Forward pass of the agent. """ diff --git a/opto/trainer/algorithms/basic_algorithms.py b/opto/trainer/algorithms/basic_algorithms.py index e9840851..76597dcb 100644 --- a/opto/trainer/algorithms/basic_algorithms.py +++ b/opto/trainer/algorithms/basic_algorithms.py @@ -53,7 +53,7 @@ def train(self, guide, train_dataset, *, - ensure_improvement: bool = False, # whether to check the improvement of the agent + ensure_improvement: bool = True, # whether to check the improvement of the agent improvement_threshold: float = 0., # threshold for improvement num_epochs: int = 1, # number of training epochs batch_size: int = 1, # batch size for updating the agent diff --git a/opto/trainer/train.py b/opto/trainer/train.py index 7e603a12..e46d8c4d 100644 --- a/opto/trainer/train.py +++ b/opto/trainer/train.py @@ -19,7 +19,7 @@ def train( model: trace.Module, train_dataset: dict, # class of optimizer - algorithm: Union[Trainer, str] = 'BasicSearchAlgorithm', + algorithm: Union[Trainer, str] = 'MinibatchAlgorithm', optimizer: Union[Optimizer, str] = "OptoPrimeV2", guide: Union[Guide, str] = 'LLMJudge', logger: Union[BaseLogger, str] = 'ConsoleLogger', From c12a19fa2b90e3082971b5de86d90a7c5e98d30c Mon Sep 17 00:00:00 2001 From: Adith Swaminathan Date: Mon, 25 Aug 2025 09:46:24 -0700 Subject: [PATCH 166/314] Clean up obsolete files --- OAI_CONFIG_LIST_sample | 25 - docs/_static/custom.css | 35 - docs/colab_kernel_clean_script.py | 29 - docs/jupyter_build.sh | 16 - docs/post_build_script.py | 48 - docs/publish.sh | 6 - docs/requirements.txt | 8 - generated_docs/opto/optimizers/buffers.md | 76 - .../opto/optimizers/function_optimizer.md | 738 ------ generated_docs/opto/optimizers/opro.md | 79 - generated_docs/opto/optimizers/optimizers.md | 267 -- generated_docs/opto/trace/broadcast.md | 54 - generated_docs/opto/trace/bundle.md | 469 ---- generated_docs/opto/trace/containers.md | 386 --- generated_docs/opto/trace/errors.md | 112 - generated_docs/opto/trace/modules.md | 304 --- generated_docs/opto/trace/nodes.md | 2213 ----------------- generated_docs/opto/trace/operators.md | 893 ------- .../trace/propagators/graph_propagator.md | 166 -- .../opto/trace/propagators/propagators.md | 338 --- generated_docs/opto/trace/trace.md | 43 - generated_docs/opto/trace/utils.md | 320 --- 22 files changed, 6625 deletions(-) delete mode 100644 OAI_CONFIG_LIST_sample delete mode 100644 docs/_static/custom.css delete mode 100644 docs/colab_kernel_clean_script.py delete mode 100644 docs/jupyter_build.sh delete mode 100644 docs/post_build_script.py delete mode 100644 docs/publish.sh delete mode 100644 docs/requirements.txt delete mode 100644 generated_docs/opto/optimizers/buffers.md delete mode 100644 generated_docs/opto/optimizers/function_optimizer.md delete mode 100644 generated_docs/opto/optimizers/opro.md delete mode 100644 generated_docs/opto/optimizers/optimizers.md delete mode 100644 generated_docs/opto/trace/broadcast.md delete mode 100644 generated_docs/opto/trace/bundle.md delete mode 100644 generated_docs/opto/trace/containers.md delete mode 100644 generated_docs/opto/trace/errors.md delete mode 100644 generated_docs/opto/trace/modules.md delete mode 100644 generated_docs/opto/trace/nodes.md delete mode 100644 generated_docs/opto/trace/operators.md delete mode 100644 generated_docs/opto/trace/propagators/graph_propagator.md delete mode 100644 generated_docs/opto/trace/propagators/propagators.md delete mode 100644 generated_docs/opto/trace/trace.md delete mode 100644 generated_docs/opto/trace/utils.md diff --git a/OAI_CONFIG_LIST_sample b/OAI_CONFIG_LIST_sample deleted file mode 100644 index 74f87d30..00000000 --- a/OAI_CONFIG_LIST_sample +++ /dev/null @@ -1,25 +0,0 @@ -// Please modify the content, remove these four lines of comment and rename this file to OAI_CONFIG_LIST to run the sample code. -// If using pyautogen v0.1.x with Azure OpenAI, please replace "base_url" with "api_base" (line 14 and line 21 below). Use "pip list" to check version of pyautogen installed. -// -// NOTE: This configuration lists GPT-4 as the default model, as this represents our current recommendation, and is known to work well with AutoGen. If you use a model other than GPT-4, you may need to revise various system prompts (especially if using weaker models like GPT-3.5-turbo). Moreover, if you use models other than those hosted by OpenAI or Azure, you may incur additional risks related to alignment and safety. Proceed with caution if updating this default. -[ - { - "model": "gpt-4", - "api_key": "", - "tags": ["gpt-4", "tool"] - }, - { - "model": "", - "api_key": "", - "base_url": "", - "api_type": "azure", - "api_version": "" - }, - { - "model": "", - "api_key": "", - "base_url": "", - "api_type": "azure", - "api_version": "" - } -] \ No newline at end of file diff --git a/docs/_static/custom.css b/docs/_static/custom.css deleted file mode 100644 index cd2f03fd..00000000 --- a/docs/_static/custom.css +++ /dev/null @@ -1,35 +0,0 @@ -:root { - --sd-color-primary: #f37726; - --sd-color-primary-highlight: #da864e; - --sd-color-secondary: #267bf3; - --sd-color-secondary-highlight: #4e88da; -} - -.bg-jb-one { - background-color: #52d16f3b; -} - -.bg-jb-two { - background-color: #e7dd7b73; -} - -.bg-jb-three { - background-color: #e7b07b96; -} - -.admonition>.admonition-title, div.admonition>.admonition-title { - background-color: #eef9fd; -} - -.admonition, div.admonition { - background-color: white; - border-color: #4cb3d4; -} - -.admonition p { - color: #474747; -} - -.text_html p { - color: #474747; -} \ No newline at end of file diff --git a/docs/colab_kernel_clean_script.py b/docs/colab_kernel_clean_script.py deleted file mode 100644 index 90d9ff3d..00000000 --- a/docs/colab_kernel_clean_script.py +++ /dev/null @@ -1,29 +0,0 @@ -import shutil -import os -import json - -# Figure out if we are in the `docs` directory or the root directory -if os.path.exists('index.html'): - print("Found index.html in current directory, assuming we are in the root directory") -else: - print("In the root directory, changing to docs directory") - os.chdir('docs') - if not os.path.exists('_config.yml'): - raise FileNotFoundError("Could not find _config.yml in the root directory or the docs directory.") - -# Clean up Jupyter notebooks (remove kernel-spec) -for root, dirs, files in os.walk('.'): - for file in files: - if file.endswith('.ipynb'): - print(root, file) - with open(os.path.join(root, file), 'r') as f: - try: - data = json.load(f) - except json.JSONDecodeError: - print("Could not read JSON, skipping", file) - continue - if 'kernelspec' in data['metadata']: - print("removed kernel", data['metadata']['kernelspec']) - del data['metadata']['kernelspec'] - with open(os.path.join(root, file), 'w') as f: - json.dump(data, f, indent=4) \ No newline at end of file diff --git a/docs/jupyter_build.sh b/docs/jupyter_build.sh deleted file mode 100644 index dd9ebf48..00000000 --- a/docs/jupyter_build.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash -cd "$(dirname "$0")/.." || exit -rm -r docs/_build docs/api -ORIGINAL_PYTHONPATH=$PYTHONPATH -export PYTHONPATH=$(pwd)/..:$PYTHONPATH - -jupyter-book build docs - -# clean up sphinx-autosummary generated files -rm -r docs/api - -# Restored PYTHONPATH -export PYTHONPATH=$ORIGINAL_PYTHONPATH - -# move all files associated with the landing page into the `_build/html` folder -python docs/post_build_script.py \ No newline at end of file diff --git a/docs/post_build_script.py b/docs/post_build_script.py deleted file mode 100644 index 96f96679..00000000 --- a/docs/post_build_script.py +++ /dev/null @@ -1,48 +0,0 @@ -import shutil -import os -import json - -# Figure out if we are in the `docs` directory or the root directory -if os.path.exists('index.html'): - print("Found index.html in current directory, assuming we are in the root directory") -else: - print("In the root directory, changing to docs directory") - os.chdir('docs') - if not os.path.exists('index.html'): - raise FileNotFoundError("Could not find index.html in the root directory or the docs directory. Are you in the `website` branch?") - -# Path to your custom index.html -custom_index = 'index.html' -# Path to your images folder -images_folder = 'images' -# Path to the built book (adjust as needed) -built_book = '_build/html' -# Path to the images destination in the built book -built_images = os.path.join(built_book, 'images') - -# Copy the custom index.html to the built book directory -shutil.copy2(custom_index, os.path.join(built_book, 'index.html')) -print(f"Copied custom index.html to {built_book}") - - -def rm_and_copy(src, dst): - if os.path.exists(dst): - # If the directory exists, remove it first to ensure a clean copy - shutil.rmtree(dst) - # Copy the entire directory - shutil.copytree(src, dst) - print(f"Copied {src} to {dst}") - -# Copy the entire images directory -rm_and_copy('images', built_images) - -# Copy the vendor directory -rm_and_copy('vendor', os.path.join(built_book, 'vendor')) - -# Copy the css directory -rm_and_copy('css', os.path.join(built_book, 'css')) - -# Copy the assets directory -rm_and_copy('assets', os.path.join(built_book, 'assets')) - -print("Post-build process completed successfully!") \ No newline at end of file diff --git a/docs/publish.sh b/docs/publish.sh deleted file mode 100644 index b45f475a..00000000 --- a/docs/publish.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash -cd "$(dirname "$0")/.." || exit -rm -r docs/_build -jupyter-book build docs -python docs/post_build_script.py -ghp-import -n -p -f docs/_build/html \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index e5ad1997..00000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ -jupyter-book -matplotlib -numpy -sphinx -sphinx-plausible -# sphinx-autodoc2 -sphinx-autoapi -ghp-import \ No newline at end of file diff --git a/generated_docs/opto/optimizers/buffers.md b/generated_docs/opto/optimizers/buffers.md deleted file mode 100644 index e5c4c3a2..00000000 --- a/generated_docs/opto/optimizers/buffers.md +++ /dev/null @@ -1,76 +0,0 @@ -## ClassDef FIFOBuffer -**FIFOBuffer**: The function of FIFOBuffer is to manage a First-In-First-Out (FIFO) buffer of a specified size. - -**attributes**: The attributes of this Class. -· size: The maximum number of items the buffer can hold. -· buffer: A list that stores the items in the buffer. - -**Code Description**: The FIFOBuffer class is designed to handle a buffer that operates on a First-In-First-Out (FIFO) basis. This means that the first item added to the buffer will be the first one to be removed when the buffer reaches its maximum size. - -- The `__init__` method initializes the buffer with a specified size and creates an empty list to store the items. -- The `add` method allows adding an item to the buffer. If the buffer exceeds the specified size, it removes the oldest items to maintain the size constraint. -- The `__iter__` method returns an iterator for the buffer, allowing it to be used in loops and other iterable contexts. -- The `__len__` method returns the current number of items in the buffer. - -In the project, the FIFOBuffer is utilized by the `FunctionOptimizerV2Memory` class in the `opto\optimizers\function_optimizer.py` file. Specifically, it is instantiated in the `__init__` method of `FunctionOptimizerV2Memory` with a parameter `memory_size`, which determines the size of the FIFO buffer. This integration suggests that the FIFOBuffer is used to store a limited history of optimization states or results, ensuring that only the most recent entries are kept. - -**Note**: -- Ensure that the size parameter is a positive integer to avoid unexpected behavior. -- The buffer will automatically discard the oldest items when new items are added beyond its capacity. - -**Output Example**: -If a FIFOBuffer is created with a size of 3 and the following items are added in sequence: `1, 2, 3, 4`, the buffer will contain `[2, 3, 4]`. The first item `1` is discarded to maintain the buffer size of 3. -### FunctionDef __init__(self, size) -**__init__**: The function of __init__ is to initialize a FIFOBuffer object with a specified size. - -**parameters**: The parameters of this Function. -· size: An integer representing the maximum size of the buffer. - -**Code Description**: The __init__ function is a constructor method for the FIFOBuffer class. It takes one parameter, size, which determines the maximum number of elements that the buffer can hold. Inside the function, the size parameter is assigned to the instance variable self.size. Additionally, an empty list is initialized and assigned to the instance variable self.buffer. This list will be used to store the elements of the buffer. - -**Note**: -- The size parameter must be a positive integer. -- The buffer is initially empty upon creation of the FIFOBuffer object. -*** -### FunctionDef add(self, item) -**add**: The function of add is to insert a new item into the buffer while maintaining its maximum size. - -**parameters**: The parameters of this Function. -· item: The item to be added to the buffer. - -**Code Description**: The add function is a method of the FIFOBuffer class, which is designed to manage a buffer with a fixed maximum size. When a new item is added to the buffer, the function first checks if the buffer size is greater than zero. If it is, the item is appended to the buffer. After appending the item, the buffer is truncated to ensure that its size does not exceed the predefined maximum size. This is achieved by slicing the buffer to keep only the most recent items up to the specified size. - -In the context of its usage within the project, the add function is called by the construct_prompt method of the FunctionOptimizerV2Memory class. Specifically, after constructing the system and user prompts, the add function is used to store a tuple containing the summary variables and user feedback into the memory buffer. This ensures that the memory buffer maintains a record of past interactions, which can be used to provide examples in future prompts. - -**Note**: -- The buffer size must be set to a positive integer for the add function to operate correctly. -- The function ensures that the buffer does not grow beyond its maximum size, maintaining only the most recent items. -- Proper handling of the buffer size is crucial to avoid unexpected behavior. -*** -### FunctionDef __iter__(self) -**__iter__**: The function of __iter__ is to return an iterator for the buffer attribute of the FIFOBuffer instance. - -**parameters**: The parameters of this Function. -· This function does not take any parameters. - -**Code Description**: The __iter__ function is a special method in Python that allows an object to be iterable. In this implementation, the __iter__ method returns an iterator for the buffer attribute of the FIFOBuffer instance. The buffer attribute is expected to be a collection (such as a list) that supports iteration. By calling iter(self.buffer), the method leverages Python's built-in iter function to obtain an iterator for the buffer, enabling the FIFOBuffer instance to be used in contexts that require iteration, such as in for-loops. - -**Note**: -- Ensure that the buffer attribute is properly initialized and contains iterable elements before invoking the __iter__ method. -- This method does not modify the buffer; it only provides a way to iterate over its elements. - -**Output Example**: -If the buffer attribute of the FIFOBuffer instance contains the elements [1, 2, 3], calling the __iter__ method will return an iterator that produces the sequence 1, 2, 3 when iterated over. -*** -### FunctionDef __len__(self) -**__len__**: The function of __len__ is to return the number of elements currently stored in the buffer. - -**parameters**: The parameters of this Function. -· This function does not take any parameters. - -**Code Description**: The __len__ function is a special method in Python that is used to define the behavior of the len() function for instances of a class. In this context, the __len__ function returns the length of the buffer attribute of the FIFOBuffer class. The buffer attribute is expected to be a list or another collection that supports the len() function. When len() is called on an instance of FIFOBuffer, it internally calls this __len__ method, which in turn returns the length of the buffer. - -**Note**: Ensure that the buffer attribute is properly initialized and maintained as a collection that supports the len() function. If the buffer is not initialized or is set to a non-collection type, calling len() on an instance of FIFOBuffer will result in an error. - -**Output Example**: If the buffer contains 5 elements, calling len() on an instance of FIFOBuffer will return 5. -*** diff --git a/generated_docs/opto/optimizers/function_optimizer.md b/generated_docs/opto/optimizers/function_optimizer.md deleted file mode 100644 index 8d97f69f..00000000 --- a/generated_docs/opto/optimizers/function_optimizer.md +++ /dev/null @@ -1,738 +0,0 @@ -## FunctionDef get_fun_name(node) -**get_fun_name**: The function of get_fun_name is to retrieve the name of a MessageNode object. - -**parameters**: -- node: A MessageNode object. - -**Code Description**: -The `get_fun_name` function is used to retrieve the name of a `MessageNode` object. It takes a `node` parameter, which is an instance of the `MessageNode` class. - -The function first checks if the `info` attribute of the `node` object is a dictionary and if it contains the key "fun_name". If this condition is true, the function returns the value associated with that key. - -If the condition is false, the function splits the `name` attribute of the `node` object using the ":" delimiter. It then returns the first part of the split. - -The purpose of this function is to provide a convenient way to retrieve the name of a `MessageNode` object. The name can be used for various purposes, such as identifying the node in a graph or generating function calls. - -This function is called by the `repr_function_call` function in the `function_optimizer.py` file of the `optimizers` module. It is used to retrieve the name of a `MessageNode` object and include it in a function call representation. - -**Note**: -- The `get_fun_name` function assumes that the `node` object is an instance of the `MessageNode` class. -- The function relies on the `info` and `name` attributes of the `node` object to retrieve the name. - -**Output Example**: -If the `info` attribute of the `node` object is a dictionary with the key "fun_name" and the associated value is "my_function", calling `get_fun_name(node)` will return "my_function". - -## FunctionDef repr_function_call(child) -**repr_function_call**: The function of repr_function_call is to generate a string representation of a function call based on a MessageNode object. - -**parameters**: -- child: A MessageNode object. - -**Code Description**: -The `repr_function_call` function takes a `child` parameter, which is an instance of the `MessageNode` class. It generates a string representation of a function call based on the attributes of the `child` object. - -The function first initializes the `function_call` variable with the format "{child.py_name} = {get_fun_name(child)}(". This sets the initial part of the function call string, which includes the name of the variable assigned to the function call and the name of the function itself. - -Next, the function iterates over the `inputs` attribute of the `child` object, which is a dictionary containing the input nodes of the `MessageNode` object. For each key-value pair in the dictionary, the function appends "{k}={v.py_name}, " to the `function_call` string. This adds the input variable names and their corresponding values to the function call string. - -After the loop, the function removes the trailing ", " from the `function_call` string and adds a closing parenthesis. This completes the function call string. - -Finally, the function returns the `function_call` string. - -The purpose of this function is to provide a convenient way to generate a string representation of a function call based on a `MessageNode` object. The function call string can be used for various purposes, such as logging, debugging, or generating code. - -This function is called by the `node_to_function_feedback` function in the `function_optimizer.py` file of the `optimizers` module. It is used to generate the function call representation of a `MessageNode` object and include it in the `graph` list of the `FunctionFeedback` object. - -**Note**: -- The `repr_function_call` function assumes that the `child` object is an instance of the `MessageNode` class. -- The function relies on the `py_name` attribute of the input nodes to retrieve their variable names. -- The function relies on the `get_fun_name` function to retrieve the name of the `child` object. - -**Output Example**: -If the `child` object has the following attributes: -- `py_name`: "result" -- `inputs`: {"x": , "y": } - -Calling `repr_function_call(child)` will return the following string: -"result = my_function(x=node_x, y=node_y)" -## FunctionDef node_to_function_feedback(node_feedback) -**node_to_function_feedback**: The function of node_to_function_feedback is to convert a TraceGraph object into a FunctionFeedback object. It processes the nodes in the TraceGraph, categorizes them into roots, intermediates, and outputs, and populates the corresponding attributes of the FunctionFeedback object. - -**parameters**: -- node_feedback: A TraceGraph object representing the subgraph of nodes. - -**Code Description**: -The `node_to_function_feedback` function takes a `node_feedback` parameter, which is an instance of the `TraceGraph` class. It converts the `TraceGraph` object into a `FunctionFeedback` object by processing the nodes in the graph and organizing them into different categories. - -The function first initializes the `depth` variable based on the length of the `graph` attribute of the `node_feedback` object. If the `graph` attribute is empty, the depth is set to 0; otherwise, it is set to the last element's depth in the `graph` attribute. - -Next, the function initializes empty lists and dictionaries for `graph`, `others`, `roots`, `output`, and `documentation`. These variables will store the processed data and information. - -The function then creates a `visited` set to keep track of visited nodes. It iterates over the `graph` attribute of the `node_feedback` object, which contains tuples representing the level and node of the graph. For each level and node, it checks if the node is a root node by checking the `is_root` attribute. If it is a root node, it updates the `roots` dictionary with the node's name as the key and its data and constraint as the value. - -If the node is not a root node, it checks if all of its parents have been visited. If they have, it categorizes the node as an intermediate node. It updates the `documentation` dictionary with the node's name as the key and its description as the value. It appends a tuple representing the level and a string representation of the function call to the `graph` list. If the level is equal to the depth, it updates the `output` dictionary with the node's name as the key and its data and constraint as the value. Otherwise, it updates the `others` dictionary with the node's name as the key and its data and constraint as the value. - -If the node is not an intermediate node, it categorizes it as a blanket node and adds it to the `roots` dictionary. - -Finally, the function returns a `FunctionFeedback` object with the populated `graph`, `others`, `roots`, `output`, `user_feedback`, and `documentation` attributes. - -**Note**: -- The `node_to_function_feedback` function assumes that the `node_feedback` parameter is a valid instance of the `TraceGraph` class. -- The function relies on the attributes and methods of the `TraceGraph` class to process the nodes and extract the necessary information. -- The resulting `FunctionFeedback` object represents the converted feedback from the `TraceGraph` object. - -**Output Example**: -A possible return value of the `node_to_function_feedback` function could be a `FunctionFeedback` object with the following attributes: -- `graph`: [(0, "function_call_1"), (1, "function_call_2"), ...] -- `others`: {"node_name_1": (data_1, constraint_1), "node_name_2": (data_2, constraint_2), ...} -- `roots`: {"root_name_1": (data_1, constraint_1), "root_name_2": (data_2, constraint_2), ...} -- `output`: {"output_name_1": (data_1, constraint_1), "output_name_2": (data_2, constraint_2), ...} -- `user_feedback`: "User feedback string" -- `documentation`: {"node_name_1": "Node description 1", "node_name_2": "Node description 2", ...} -## ClassDef FunctionFeedback -**FunctionFeedback**: The function of FunctionFeedback is to serve as a feedback container used by the FunctionPropagator. - -**attributes**: The attributes of this Class. -· graph: Each item is a representation of a function call. The items are topologically sorted. -· documentation: Function name and its documentation string. -· others: Intermediate variable names and their data. -· roots: Root variable name and its data. -· output: Leaf variable name and its data. -· user_feedback: User feedback at the leaf of the graph. - -**Code Description**: The FunctionFeedback class is designed to encapsulate feedback information used by the FunctionPropagator. It organizes and stores various types of data related to function calls and their execution within a graph structure. The attributes of this class are as follows: - -- `graph`: This attribute holds a list of tuples, where each tuple represents a function call. The tuples are topologically sorted, ensuring that the order of function calls respects their dependencies. -- `documentation`: This dictionary maps function names to their corresponding documentation strings, providing a reference for understanding the purpose and behavior of each function. -- `others`: This dictionary stores intermediate variable names along with their associated data. These variables are neither root nor leaf nodes in the function call graph. -- `roots`: This dictionary contains root variable names and their data. Root variables are the starting points in the function call graph. -- `output`: This dictionary holds leaf variable names and their data. Leaf variables are the endpoints in the function call graph. -- `user_feedback`: This string captures user feedback at the leaf of the graph, providing insights or comments from the user regarding the final output. - -The FunctionFeedback class is utilized by the `node_to_function_feedback` function, which converts a TraceGraph into a FunctionFeedback instance. This conversion involves processing the nodes of the TraceGraph, categorizing them into roots, intermediates (others), and outputs, and then populating the corresponding attributes of the FunctionFeedback instance. The `node_to_function_feedback` function ensures that the graph is correctly sorted and that all relevant data and documentation are accurately captured. - -**Note**: Points to note about the use of the code -- Ensure that the input TraceGraph to the `node_to_function_feedback` function is correctly structured and sorted. -- The FunctionFeedback class relies on the accurate categorization of nodes into roots, intermediates, and outputs for proper functionality. -- User feedback should be meaningful and relevant to the final output to provide valuable insights. -## ClassDef ProblemInstance -**ProblemInstance**: The function of ProblemInstance is to encapsulate and format the details of a problem instance for optimization tasks. - -**attributes**: The attributes of this Class. -· instruction: A string containing the instructions for the problem. -· code: A string representing the code to be executed. -· documentation: A string providing documentation for the code. -· variables: A string listing the variables involved in the problem. -· inputs: A string detailing the inputs required for the code. -· others: A string for any additional information related to the problem. -· outputs: A string specifying the expected outputs of the code. -· feedback: A string containing feedback on the problem instance. -· constraints: A string outlining any constraints on the variables or the problem. - -**Code Description**: The ProblemInstance class is designed to encapsulate various components of a problem instance, such as instructions, code, documentation, variables, inputs, outputs, feedback, and constraints. It uses a predefined template to format these components into a structured string representation. - -The class includes a `problem_template` attribute, which is a formatted string template that organizes the problem details into sections. The `__repr__` method is overridden to return a formatted string representation of the problem instance using this template. - -The ProblemInstance class is utilized in the FunctionOptimizer class, specifically in its `__init__` and `probelm_instance` methods. In the `__init__` method, an example problem instance is created using the ProblemInstance class to demonstrate the expected format and structure. The `probelm_instance` method generates a new ProblemInstance based on the provided summary and an optional mask to exclude certain sections. - -**Note**: When using the ProblemInstance class, ensure that all attributes are properly populated to generate a meaningful and complete problem instance. The class relies on the provided template to format the output, so any missing or incorrect information may result in an incomplete or inaccurate representation. - -**Output Example**: -``` -#Instruction -Optimize the function to achieve the desired output. - -#Code -y = add(x=a,y=b) -z = subtract(x=y, y=c) - -#Documentation -add: add x and y -subtract: subtract y from x - -#Variables -(int) a = 5 - -#Constraints -a: a > 0 - -#Inputs -(int) b = 1 -(int) c = 5 - -#Others -(int) y = 6 - -#Outputs -(int) z = 1 - -#Feedback: -The result of the code is not as expected. The result should be 10, but the code returns 1 -``` -### FunctionDef __repr__(self) -**__repr__**: The function of __repr__ is to provide a formatted string representation of the ProblemInstance object. - -**parameters**: The parameters of this function. -· self: Refers to the instance of the ProblemInstance class. - -**Code Description**: The __repr__ function returns a string that represents the ProblemInstance object in a human-readable format. It uses the problem_template attribute of the instance to format the string. The placeholders in the problem_template are filled with the corresponding attributes of the instance, which include: -- instruction: Instructions related to the problem instance. -- code: The code associated with the problem instance. -- documentation: Documentation details of the problem instance. -- variables: Variables involved in the problem instance. -- constraints: Constraints applied to the problem instance. -- inputs: Inputs required for the problem instance. -- outputs: Outputs expected from the problem instance. -- others: Any other relevant information about the problem instance. -- feedback: Feedback related to the problem instance. - -**Note**: Ensure that the problem_template attribute is properly defined and contains the necessary placeholders for all the attributes used in the format method. If any attribute is missing or the template is incorrectly formatted, it may result in a runtime error. - -**Output Example**: A possible appearance of the code's return value could be: -``` -ProblemInstance( - instruction='Optimize the function', - code='def optimize(): pass', - documentation='This function optimizes the given parameters.', - variables={'x': 10, 'y': 20}, - constraints='x + y <= 30', - inputs=['x', 'y'], - outputs=['result'], - others='Additional information', - feedback='No issues found' -) -``` -*** -## ClassDef FunctionOptimizer -**FunctionOptimizer**: The function of FunctionOptimizer is to serve as a base class for optimizers, responsible for updating parameters based on feedback. - -**attributes**: -- parameters: A list of ParameterNode objects that the optimizer will manage and update. - -**Code Description**: -The FunctionOptimizer class is a subclass of the Optimizer class and provides a base implementation for optimizing functions. It extends the Optimizer class and overrides some of its methods to customize the optimization process. - -The `__init__` method initializes the FunctionOptimizer object by calling the superclass's `__init__` method and passing the parameters list. It also sets the `representation_prompt` attribute, which is a generic representation prompt explaining how to read and understand the problem. - -The `default_objective` attribute defines the default objective of the optimizer, which is to change the values of the variables in the `#Variables` section to improve the output according to the feedback. - -The `output_format_prompt` attribute defines the output format of the optimizer's response. It specifies that the output should be in JSON format and provides a template for the structure of the response. - -The `example_problem_template` attribute defines a template for an example problem instance and response. It includes placeholders for the problem instance and the response, which can be filled in with actual values. - -The `user_prompt_template` attribute defines a template for the user prompt. It includes placeholders for the problem instance and the instruction, which can be filled in with actual values. - -The `example_prompt` attribute is currently empty and marked as a TODO. It is intended to provide feasible but not optimal solutions for the current problem instance as a hint to help users understand the problem better. - -The `final_prompt` attribute defines a template for the final prompt, which prompts the user to provide their response. - -The `__init__` method also initializes other attributes such as `propagator`, `llm`, `ignore_extraction_error`, `include_example`, `max_tokens`, and `log` with default values or values passed as arguments. - -The `default_propagator` method returns the default Propagator object of the optimizer. This method is implemented in the Optimizer class and must be overridden by subclasses. - -The `summarize` method aggregates the feedback from all the parameters and constructs the summary object. It then classifies the root nodes into variables and others. - -The `repr_node_value` method takes a dictionary of node values and returns a string representation of the values. - -The `repr_node_constraint` method takes a dictionary of node constraints and returns a string representation of the constraints. - -The `probelm_instance` method constructs a ProblemInstance object based on the summary and a mask. The mask is used to exclude certain sections from the problem instance. - -The `construct_prompt` method constructs the system and user prompts based on the summary and a mask. The system prompt includes the representation prompt and the output format prompt. The user prompt includes the problem instance and the final prompt. - -The `_step` method is an abstract method that must be implemented by subclasses. It is responsible for proposing new parameter values based on feedback and returning the update dictionary. - -The `construct_update_dict` method converts the suggestion in text format into the right data type and constructs an update dictionary. - -The `extract_llm_suggestion` method extracts the suggestion from the response received from the LLM (Language Model). - -The `call_llm` method calls the LLM with a prompt and returns the response. - -**Note**: -- The FunctionOptimizer class is designed to be subclassed and extended to create specific optimizers for different types of problems. -- Subclasses of FunctionOptimizer must implement the `_step` and `default_propagator` methods. -- The FunctionOptimizer class provides a consistent interface and behavior for managing and updating parameters based on feedback. -- The class uses the LLM to generate suggestions for updating the parameters. -- The class includes methods for constructing prompts, extracting suggestions, and calling the LLM. - -**Output Example**: -{ - "reasoning": "In this case, the desired response would be to change the value of input a to 14, as that would make the code return 10.", - "answer": {}, - "suggestion": { - "a": 10 - } -} -### FunctionDef __init__(self, parameters, config_list) -**__init__**: The function of __init__ is to initialize an instance of the FunctionOptimizer class. - -**parameters**: -- parameters: A list of ParameterNode objects representing the trainable nodes in the computational graph. -- config_list: A list of configurations for the OpenAIWrapper. Default is None. -- *args: Additional positional arguments. -- propagator: An instance of the Propagator class. Default is None. -- objective: A string representing the objective of the optimization task. Default is None. -- ignore_extraction_error: A boolean indicating whether to ignore type conversion errors when extracting updated values from LLM's suggestion. Default is True. -- include_example: A boolean indicating whether to include an example problem and response in the prompt. Default is False. -- max_tokens: An integer representing the maximum number of tokens allowed in the prompt. Default is 4096. -- log: A boolean indicating whether to log the optimization process. Default is True. -- **kwargs: Additional keyword arguments. - -**Code Description**: The __init__ method of the FunctionOptimizer class initializes an instance of the class. It takes in various parameters such as parameters, config_list, *args, propagator, objective, ignore_extraction_error, include_example, max_tokens, log, and **kwargs. - -The method first calls the __init__ method of the superclass (Optimizer) to initialize the parameters and propagator attributes. It then sets the ignore_extraction_error attribute based on the provided ignore_extraction_error parameter. - -If the config_list parameter is None, it uses the autogen.config_list_from_json function to retrieve the configuration list from the "OAI_CONFIG_LIST" JSON file. It then initializes the llm attribute with an instance of the autogen.OpenAIWrapper class, passing the config_list as a parameter. - -The objective attribute is set to the provided objective parameter if it is not None, otherwise it is set to the default_objective attribute of the class. - -The example_problem attribute is initialized with a formatted string template that represents an example problem instance. It includes placeholders for the instruction, code, documentation, variables, constraints, inputs, others, outputs, and feedback sections. - -The example_response attribute is initialized with a formatted string that represents an example response to the problem instance. It includes placeholders for the reasoning, answer, and suggestion sections. - -The include_example, max_tokens, and log attributes are set based on the provided parameters. - -**Note**: -- The FunctionOptimizer class is a subclass of the Optimizer class. -- The parameters attribute represents the trainable nodes in the computational graph. -- The config_list attribute represents the configuration list for the OpenAIWrapper. -- The propagator attribute represents the propagator for the optimization process. -- The objective attribute represents the objective of the optimization task. -- The ignore_extraction_error attribute indicates whether to ignore type conversion errors when extracting updated values from LLM's suggestion. -- The include_example attribute indicates whether to include an example problem and response in the prompt. -- The max_tokens attribute represents the maximum number of tokens allowed in the prompt. -- The log attribute indicates whether to log the optimization process. - -**Output Example**: -``` -FunctionOptimizer( - parameters=[ParameterNode: (name, dtype=, data=value)], - config_list=[...], - propagator=Propagator(), - objective="...", - ignore_extraction_error=True, - include_example=False, - max_tokens=4096, - log=True, - ... -) -``` -*** -### FunctionDef default_propagator(self) -**default_propagator**: The function of default_propagator is to return the default Propagator object of the optimizer. - -**parameters**: The parameters of this Function. -· None - -**Code Description**: The default_propagator function is a method within the FunctionOptimizer class. Its primary purpose is to return an instance of the GraphPropagator class. When this method is called, it creates and returns a new GraphPropagator object. The GraphPropagator class, which is a subclass of the Propagator class, is designed to collect all the nodes seen in a path and compute the propagated feedback to the parent nodes based on the child node's description, data, and feedback. This method does not take any parameters and simply returns a new GraphPropagator instance, which can then be used by the optimizer for its propagation tasks. - -**Note**: This method is straightforward and does not require any parameters. It is designed to provide a default propagator for the optimizer, ensuring that the optimizer has a predefined mechanism for handling propagation tasks. - -**Output Example**: -```python -GraphPropagator() -``` -*** -### FunctionDef summarize(self) -**summarize**: The function of summarize is to aggregate feedback from all the parameters, construct variables and update others, and classify the root nodes into variables and others. - -**parameters**: -- self: The instance of the class. - -**Code Description**: -The `summarize` function is a method of the `FunctionOptimizer` class. It aggregates feedback from all the parameters by calling the `aggregate` method of the `propagator` object. The feedbacks are obtained from the trainable parameters by iterating over the `parameters` attribute of the class instance and filtering out the non-trainable nodes. The feedbacks are then summed up using the `sum` function. - -After aggregating the feedback, the function converts the resulting `TraceGraph` object into a `FunctionFeedback` object by calling the `node_to_function_feedback` function. This function processes the nodes in the `TraceGraph` object, categorizes them into roots, intermediates, and outputs, and populates the corresponding attributes of the `FunctionFeedback` object. - -Next, the function constructs variables and updates others based on the trainable nodes. It creates a dictionary called `trainable_param_dict` that maps the parameter names to their corresponding parameter objects. It then updates the `variables` attribute of the `summary` object by filtering the `roots` dictionary based on the keys present in the `trainable_param_dict`. Similarly, it updates the `inputs` attribute of the `summary` object by filtering the `roots` dictionary based on the keys not present in the `trainable_param_dict`. - -Finally, the function returns the `summary` object, which represents the aggregated feedback, variables, and inputs. - -The `summarize` function is called in the `_step` method of the `FunctionOptimizer` class. It is used to summarize the feedback from the trainable parameters and construct prompts for further processing. The `summarize` function relies on the `propagator` object and the `node_to_function_feedback` function to perform its tasks. - -**Note**: -- The `summarize` function assumes that the `propagator` object is correctly initialized and contains the necessary methods and attributes. -- The function assumes that the `parameters` attribute of the class instance contains the necessary trainable nodes. -- The `node_to_function_feedback` function should be defined and accessible within the project for the `summarize` function to work correctly. -- The resulting `summary` object represents the aggregated feedback, variables, and inputs from the trainable parameters. - -**Output Example**: -A possible return value of the `summarize` function could be a `FunctionFeedback` object with the following attributes: -- `graph`: [(0, "function_call_1"), (1, "function_call_2"), ...] -- `others`: {"node_name_1": (data_1, constraint_1), "node_name_2": (data_2, constraint_2), ...} -- `roots`: {"root_name_1": (data_1, constraint_1), "root_name_2": (data_2, constraint_2), ...} -- `output`: {"output_name_1": (data_1, constraint_1), "output_name_2": (data_2, constraint_2), ...} -- `user_feedback`: "User feedback string" -- `documentation`: {"node_name_1": "Node description 1", "node_name_2": "Node description 2", ...} -*** -### FunctionDef repr_node_value(node_dict) -**repr_node_value**: The function of repr_node_value is to generate a formatted string representation of the values in a given dictionary, excluding keys that contain the substring "__code". - -**parameters**: The parameters of this Function. -· node_dict: A dictionary where each key is a string and each value is a list, with the first element of the list being the value to be represented. - -**Code Description**: The repr_node_value function processes a dictionary (node_dict) and creates a list of formatted strings based on the dictionary's contents. It iterates over each key-value pair in the dictionary. For each pair, if the key does not contain the substring "__code", it appends a string to the list in the format "(type) key=value", where "type" is the type of the first element in the value list, and "key" and "value" are the key and the first element of the value list, respectively. If the key contains the substring "__code", it appends a string in the format "(code) key:value". Finally, the function joins all the strings in the list with newline characters and returns the resulting string. - -This function is utilized in the probelm_instance method of the FunctionOptimizer class. In this context, repr_node_value is called to generate string representations of various components of a summary object, such as variables, inputs, outputs, and others. These string representations are then used to construct a ProblemInstance object, which encapsulates the details of a problem instance in a structured format. - -**Note**: -- Ensure that the input dictionary (node_dict) has lists as values, with the first element of each list being the value to be represented. -- Keys containing the substring "__code" will be treated differently and formatted as "(code) key:value". - -**Output Example**: -Given the input dictionary: -{ - "var1": [10], - "var2": ["example"], - "func__code": ["def func(): pass"] -} -The function would return: -``` -(int) var1=10 -(str) var2=example -(code) func__code:def func(): pass -``` -*** -### FunctionDef repr_node_constraint(node_dict) -**repr_node_constraint**: The function of repr_node_constraint is to generate a formatted string representation of the constraints in a given node dictionary. - -**parameters**: The parameters of this Function. -· node_dict: A dictionary where keys are node identifiers and values are tuples containing node attributes. - -**Code Description**: The repr_node_constraint function processes a dictionary of nodes, where each key-value pair represents a node and its attributes. The function iterates through each item in the dictionary. For each key-value pair, it checks if the key does not contain the substring "__code". If this condition is met and the second element of the value tuple (v[1]) is not None, it appends a formatted string to a temporary list (temp_list). The formatted string includes the type of the first element of the value tuple (v[0]), the key, and the second element of the value tuple (v[1]). If the key contains the substring "__code" and the second element of the value tuple (v[1]) is not None, it appends a different formatted string to the temporary list, indicating that the key is related to code. Finally, the function joins all the strings in the temporary list with newline characters and returns the resulting string. - -This function is called by the probelm_instance method of the FunctionOptimizer class. In this context, repr_node_constraint is used to generate a string representation of the constraints in the summary.variables dictionary, which is then included in the ProblemInstance object. This ensures that the constraints are properly formatted and included in the problem instance's representation. - -**Note**: Ensure that the node_dict parameter is correctly structured, with each value being a tuple where the second element can be None or a meaningful value to be included in the output. - -**Output Example**: -``` -(int) node1: 10 -(str) node2: constraint_value -(code) node3__code: some_code -``` -*** -### FunctionDef probelm_instance(self, summary, mask) -**probelm_instance**: The function of probelm_instance is to generate a ProblemInstance object based on the provided summary and an optional mask. It encapsulates and formats the details of a problem instance for optimization tasks. - -**parameters**: -- summary: A summary object containing the necessary information for the problem instance. -- mask (optional): A list of strings specifying the sections to exclude from the ProblemInstance object. - -**Code Description**: The probelm_instance function takes a summary object and an optional mask as input. It first checks if a mask is provided, and if not, initializes it as an empty list. - -The function then creates a ProblemInstance object by passing the following parameters: -- instruction: The instruction for the problem instance, obtained from the summary object. -- code: A string representing the code to be executed. It is obtained by joining the values of the sorted summary.graph dictionary, excluding the sections specified in the mask. -- documentation: A string providing documentation for the code. It is obtained by joining the values of the summary.documentation dictionary, excluding the sections specified in the mask. -- variables: A string listing the variables involved in the problem. It is obtained by calling the repr_node_value function on the summary.variables dictionary, excluding the sections specified in the mask. -- constraints: A string outlining any constraints on the variables or the problem. It is obtained by calling the repr_node_constraint function on the summary.variables dictionary, excluding the sections specified in the mask. -- inputs: A string detailing the inputs required for the code. It is obtained by calling the repr_node_value function on the summary.inputs dictionary, excluding the sections specified in the mask. -- outputs: A string specifying the expected outputs of the code. It is obtained by calling the repr_node_value function on the summary.output dictionary, excluding the sections specified in the mask. -- others: A string for any additional information related to the problem. It is obtained by calling the repr_node_value function on the summary.others dictionary, excluding the sections specified in the mask. -- feedback: A string containing feedback on the problem instance. It is obtained from the summary.user_feedback attribute, excluding the sections specified in the mask. - -The ProblemInstance object is then returned. - -The probelm_instance function is utilized in the FunctionOptimizer class, specifically in its __init__ method and construct_prompt method. In the __init__ method, it is used to create an example problem instance using the ProblemInstance class. In the construct_prompt method, it is called to generate the problem instance string representation, which is included in the user prompt. - -**Note**: When using the probelm_instance function, ensure that the summary object is properly populated with the required information. The mask parameter can be used to exclude specific sections from the generated ProblemInstance object. - -**Output Example**: -``` -#Instruction -Optimize the function to achieve the desired output. - -#Code -y = add(x=a,y=b) -z = subtract(x=y, y=c) - -#Documentation -add: add x and y -subtract: subtract y from x - -#Variables -(int) a = 5 - -#Constraints -a: a > 0 - -#Inputs -(int) b = 1 -(int) c = 5 - -#Others -(int) y = 6 - -#Outputs -(int) z = 1 - -#Feedback: -The result of the code is not as expected. The result should be 10, but the code returns 1 -``` -*** -### FunctionDef construct_prompt(self, summary, mask) -**construct_prompt**: The function of construct_prompt is to construct the system and user prompts based on the provided summary and optional mask. - -**parameters**: -- summary: A summary object containing the necessary information for the problem instance. -- mask (optional): A list of strings specifying the sections to exclude from the ProblemInstance object. -- *args: Additional positional arguments. -- **kwargs: Additional keyword arguments. - -**Code Description**: The construct_prompt function is designed to generate system and user prompts for optimization tasks. It begins by creating a system prompt by concatenating the representation_prompt and output_format_prompt attributes, which provide a generic representation and output rules. - -Next, the function constructs a user prompt using the user_prompt_template attribute. It formats this template with a string representation of a problem instance, generated by calling the probelm_instance method with the provided summary and mask. This problem instance encapsulates and formats the details of the problem for the user prompt. - -If the include_example attribute is set to True, the function prepends an example problem and response to the user prompt. This is done by formatting the example_problem_template attribute with the example_problem and example_response attributes. - -Finally, the function appends the final_prompt attribute to the user prompt and returns both the system prompt and the user prompt. - -The construct_prompt function is called within the _step method of the FunctionOptimizer class. In this context, it is used to generate the necessary prompts for interacting with a language model, which then provides suggestions for optimizing the function. - -**Note**: Ensure that the summary object is properly populated with the required information before calling construct_prompt. The mask parameter can be used to exclude specific sections from the generated ProblemInstance object. - -**Output Example**: -``` -system_prompt: "Generic representation and output rules" -user_prompt: "Example problem and response (if include_example is True) + Problem instance details + Final prompt" -``` -*** -### FunctionDef _step(self, verbose, mask) -**_step**: The `_step` function is responsible for executing a single optimization step in the `FunctionOptimizer` class. It performs various operations such as summarizing feedback, constructing prompts, calling the language model, extracting suggestions, constructing an update dictionary, and logging the interaction. - -**parameters**: -- `self`: The instance of the `FunctionOptimizer` class. -- `verbose` (optional): A boolean indicating whether to print verbose output. Default is `False`. -- `mask` (optional): A list of strings specifying sections to exclude from the problem instance. Default is `None`. -- `*args`: Additional positional arguments. -- `**kwargs`: Additional keyword arguments. - -**Code Description**: -The `_step` function begins by asserting that the `propagator` attribute of the `FunctionOptimizer` instance is an instance of the `GraphPropagator` class. This ensures that the necessary methods and attributes are available for the subsequent operations. - -Next, the function calls the `summarize` method of the `FunctionOptimizer` class to aggregate feedback from all the parameters. This is done by invoking the `summarize` function defined in the `function_optimizer.py` file. The `summarize` function aggregates feedback by calling the `aggregate` method of the `propagator` object and processes the resulting `TraceGraph` object. - -After summarizing the feedback, the function constructs system and user prompts by calling the `construct_prompt` method of the `FunctionOptimizer` class. This method formats the prompts using the `representation_prompt`, `output_format_prompt`, and `user_prompt_template` attributes of the class. It also generates a problem instance string by calling the `problem_instance` method with the provided summary and mask. The prompts are then concatenated and stored in the `system_prompt` and `user_prompt` variables. - -The function proceeds to call the `call_llm` method of the `FunctionOptimizer` class to interact with a language model. This method sends the system and user prompts to the language model and retrieves the generated response. The response is stored in the `response` variable. - -If the response contains the string "TERMINATE", the function returns an empty dictionary. - -Otherwise, the function calls the `extract_llm_suggestion` method of the `FunctionOptimizer` class to extract a suggestion dictionary from the response. This method attempts to parse the response as JSON and retrieve the "suggestion" key. If the parsing fails, it falls back to extracting key-value pairs using regular expressions. The extracted suggestion dictionary is stored in the `suggestion` variable. - -The function then calls the `construct_update_dict` method of the `FunctionOptimizer` class to convert the suggestion into the appropriate data types. This method iterates over the parameters of the optimizer and checks if each parameter is trainable and if its name exists in the suggestion dictionary. If both conditions are met, it attempts to convert the suggestion value to the data type of the parameter using the `type` function. The parameter and its updated value are added to the `update_dict` dictionary. - -If the `log` attribute of the optimizer is not `None`, the function appends a dictionary containing the system prompt, user prompt, and response to the log. - -Finally, the function returns the `update_dict` dictionary, which maps `ParameterNode` objects to their corresponding updated values. - -The `_step` function is an essential part of the optimization process in the `FunctionOptimizer` class. It relies on the `summarize`, `construct_prompt`, `call_llm`, `extract_llm_suggestion`, and `construct_update_dict` methods to perform its tasks. The function assumes that the necessary methods and attributes are correctly initialized and accessible within the class. - -**Note**: -- The `summarize` function assumes that the `propagator` object is correctly initialized and contains the necessary methods and attributes. -- The `summarize` function assumes that the `parameters` attribute of the class instance contains the necessary trainable nodes. -- The `node_to_function_feedback` function should be defined and accessible within the project for the `summarize` function to work correctly. -- The resulting `summary` object represents the aggregated feedback, variables, and inputs from the trainable parameters. -- The `construct_prompt` function assumes that the summary object is properly populated with the required information before calling it. -- The `construct_prompt` function assumes that the `representation_prompt`, `output_format_prompt`, `user_prompt_template`, `example_problem_template`, `example_problem`, `example_response`, and `final_prompt` attributes are correctly initialized within the class. -- The `call_llm` function assumes that the `llm` object is correctly initialized and contains the necessary methods and attributes. -- The `extract_llm_suggestion` function assumes that the response string contains a "suggestion" key within a JSON object. -- The `construct_update_dict` function assumes that the `parameters` attribute exists and is a list of `ParameterNode` objects. -- The `construct_update_dict` function assumes that the suggestion dictionary contains the keys corresponding to the `py_name` attribute of the `ParameterNode` objects. -- If the suggestion is missing a key or the conversion fails, an exception is raised unless the `ignore_extraction_error` flag is set to `True`. -- The `_step` function assumes that the necessary methods and attributes are correctly initialized within the class. - -**Output Example**: -A possible return value of the `_step` function could be a dictionary mapping `ParameterNode` objects to their corresponding updated values: -``` -{ - : , - : , - ... -} -``` -*** -### FunctionDef construct_update_dict(self, suggestion) -**construct_update_dict**: The function of construct_update_dict is to convert the suggestion in text into the right data type. - -**parameters**: -- suggestion: A dictionary containing suggestions in text form. - - Type: Dict[str, Any] -- return: A dictionary mapping ParameterNode objects to their corresponding updated values. - - Type: Dict[ParameterNode, Any] - -**Code Description**: -The `construct_update_dict` function takes a suggestion in text form and converts it into the appropriate data type. It iterates over the `parameters` list of the current instance and checks if each parameter is trainable and if its name exists in the suggestion dictionary. If both conditions are met, it attempts to convert the suggestion value to the data type of the parameter using the `type` function. If the conversion is successful, the parameter and its updated value are added to the `update_dict` dictionary. - -In case the suggestion is missing the key or the conversion fails due to an incorrect data type, an exception is raised. However, if the `ignore_extraction_error` flag is set to True, a warning is issued instead of raising an exception. - -The `update_dict` dictionary, containing the ParameterNode objects and their updated values, is then returned as the output of the function. - -This function is called by the `_step` method of the `FunctionOptimizer` class in the `function_optimizer.py` file. In the `_step` method, the `construct_update_dict` function is used to convert the suggestion obtained from the language model into the appropriate data types for updating the parameters of the optimizer. - -**Note**: -- The `construct_update_dict` function assumes that the `parameters` attribute exists and is a list of ParameterNode objects. -- The `construct_update_dict` function assumes that the suggestion dictionary contains the keys corresponding to the `py_name` attribute of the ParameterNode objects. -- If the suggestion is missing a key or the conversion fails, an exception is raised unless the `ignore_extraction_error` flag is set to True. - -**Output Example**: -A possible return value of the `construct_update_dict` function could be a dictionary mapping ParameterNode objects to their corresponding updated values. For example: -``` -{ - : , - : , - ... -} -``` -*** -### FunctionDef extract_llm_suggestion(self, response) -**extract_llm_suggestion**: The function of extract_llm_suggestion is to extract a suggestion dictionary from a given response string. - -**parameters**: The parameters of this Function. -· response: A string containing the response from which the suggestion needs to be extracted. - -**Code Description**: The extract_llm_suggestion function is designed to parse a response string, typically from a language model, and extract a dictionary of suggestions. The function attempts to decode the response as JSON and retrieve the "suggestion" key. If the initial attempt fails due to a JSONDecodeError, the function tries to clean the response by extracting content within curly braces and attempts to decode it again. If the suggestion dictionary is still empty, the function uses a regular expression to manually extract key-value pairs from the response string. - -The function is called by the _step method within the same class, FunctionOptimizer. In the _step method, the extract_llm_suggestion function is used to process the response from a language model call and extract meaningful suggestions, which are then used to construct an update dictionary. This update dictionary is crucial for the subsequent steps in the optimization process. - -**Note**: -- The function makes two attempts to decode the response as JSON before resorting to regular expression parsing. -- If the suggestion dictionary remains empty after all attempts, the function prints an error message indicating the failure to extract suggestions. -- The function assumes that the response string contains a "suggestion" key within a JSON object. - -**Output Example**: -If the response string is '{"suggestion": {"param1": "value1", "param2": "value2"}}', the function will return: -``` -{ - "param1": "value1", - "param2": "value2" -} -``` -*** -### FunctionDef call_llm(self, system_prompt, user_prompt, verbose, max_tokens) -**call_llm**: The function of call_llm is to interact with a language model (LLM) using provided prompts and return the generated response. - -**parameters**: The parameters of this Function. -· system_prompt: A string representing the initial prompt given to the LLM, typically setting the context or instructions for the LLM. -· user_prompt: A string representing the user's input or query that follows the system prompt. -· verbose: A boolean or string parameter that controls the verbosity of the function. If set to True or "output", the prompts and responses are printed to the console. -· max_tokens: An integer specifying the maximum number of tokens the LLM should generate in its response. The default value is 4096. - -**Code Description**: The call_llm function is designed to facilitate communication with a language model by sending it a structured prompt and retrieving its response. The function first checks the verbosity setting; if verbose is set to True or "output", it prints the combined system and user prompts. It then constructs a message list with roles "system" and "user" to format the prompts appropriately for the LLM. - -The function attempts to generate a response from the LLM in JSON format. If this attempt fails, it falls back to a simpler response generation method, using the max_tokens parameter to limit the response length. The response content is extracted from the LLM's output and, if verbosity is enabled, printed to the console. Finally, the function returns the LLM's response content. - -This function is called by the _step method within the same module. The _step method uses call_llm to generate suggestions or updates based on the current state summarized by the system and user prompts. The response from call_llm is then processed to extract actionable suggestions, which are used to update the system's state. - -**Note**: -- Ensure that the LLM instance (self.llm) is properly initialized before calling this function. -- The verbose parameter can be used to debug or log the interaction with the LLM by printing the prompts and responses. -- Handle exceptions appropriately when the LLM fails to generate a JSON response. - -**Output Example**: -A possible return value of the function might look like: -``` -"Sure, I can help you with that. What specific information are you looking for?" -``` -*** -## ClassDef FunctionOptimizerV2 -**FunctionOptimizerV2**: The function of FunctionOptimizerV2 is to serve as an enhanced version of the FunctionOptimizer class, providing additional functionality and improvements to the optimization process. - -**attributes**: -- output_format_prompt: A string that defines the output format of the optimizer's response. -- example_problem_template: A string template for an example problem instance and response. -- user_prompt_template: A string template for the user prompt. -- example_prompt: A string that provides feasible but not optimal solutions for the current problem instance as a hint. -- final_prompt: A string template for the final prompt. - -**Code Description**: -The FunctionOptimizerV2 class is a subclass of the FunctionOptimizer class and provides an enhanced version of the optimization process. It extends the FunctionOptimizer class and overrides some of its methods to add additional functionality. - -The `__init__` method initializes the FunctionOptimizerV2 object by calling the superclass's `__init__` method and passing the arguments. It also initializes the `memory` attribute, which is a FIFOBuffer object used to store past variables and feedbacks. - -The `construct_prompt` method overrides the superclass's method to add examples from the memory to the user prompt. It checks if the memory is not empty and adds the variables and feedbacks from the memory to the user prompt. - -**Note**: -- The FunctionOptimizerV2 class is designed to enhance the optimization process by adding memory functionality. -- The class extends the FunctionOptimizer class and overrides some of its methods to add the desired functionality. -- The `memory` attribute stores past variables and feedbacks. -- The `construct_prompt` method adds examples from the memory to the user prompt. - -**Output Example**: -{ - "reasoning": "In this case, the desired response would be to change the value of input a to 14, as that would make the code return 10.", - "answer": {}, - "suggestion": { - "a": 10 - } -} -## ClassDef FunctionOptimizerV2Memory -**FunctionOptimizerV2Memory**: The function of FunctionOptimizerV2Memory is to enhance the optimization process by incorporating a memory mechanism that stores past variables and feedbacks. - -**attributes**: The attributes of this Class. -· memory: A FIFOBuffer object that stores past variables and feedbacks. - -**Code Description**: The FunctionOptimizerV2Memory class extends the FunctionOptimizerV2 class by adding a memory mechanism to the optimization process. This class is designed to improve the optimization process by utilizing past experiences stored in memory. - -The `__init__` method initializes the FunctionOptimizerV2Memory object. It calls the superclass's `__init__` method with the provided arguments and initializes the `memory` attribute as a FIFOBuffer object with a specified memory size. - -The `construct_prompt` method constructs the system and user prompts by calling the superclass's `construct_prompt` method. It then checks if the memory contains any past variables and feedbacks. If the memory is not empty, it adds these examples to the user prompt. The method splits the user prompt at the final prompt, adds a section containing past variables and feedbacks, and then reconstructs the user prompt. Finally, it adds the current summary's variables and user feedback to the memory. - -This class is used within the project to enhance the functionality of the FunctionOptimizerV2 by adding a memory component, which allows the optimizer to consider past experiences when constructing prompts. - -**Note**: -- The memory attribute is a FIFOBuffer that stores past variables and feedbacks. -- The construct_prompt method enhances the user prompt by including examples from the memory. -- This class is designed to improve the optimization process by leveraging past experiences. - -**Output Example**: -```json -{ - "system_prompt": "System prompt content here...", - "user_prompt": "User prompt content here...\nBelow are some variables and their feedbacks you received in the past.\n\n{\n \"variables\": {\n \"var1\": \"value1\",\n \"var2\": \"value2\"\n },\n \"feedback\": \"feedback content\"\n}\n\nFinal prompt content here..." -} -``` -### FunctionDef __init__(self) -**__init__**: The function of __init__ is to initialize an instance of the FunctionOptimizerV2Memory class with optional memory size and other parameters. - -**parameters**: The parameters of this Function. -· *args: Variable length argument list. -· memory_size: An optional integer parameter that specifies the size of the FIFO buffer. Default is 0. -· **kwargs: Arbitrary keyword arguments. - -**Code Description**: The __init__ method is the constructor for the FunctionOptimizerV2Memory class. It begins by calling the constructor of its superclass using `super().__init__(*args, **kwargs)`, ensuring that any initialization logic in the parent class is executed. Following this, it initializes a FIFOBuffer instance with the specified memory size by passing the `memory_size` parameter to the FIFOBuffer constructor. The FIFOBuffer is assigned to the `self.memory` attribute of the FunctionOptimizerV2Memory instance. - -The FIFOBuffer class, which is used here, manages a First-In-First-Out (FIFO) buffer of a specified size. This buffer is designed to store a limited number of items, automatically discarding the oldest items when new ones are added beyond its capacity. In the context of FunctionOptimizerV2Memory, the FIFOBuffer likely serves to maintain a history of optimization states or results, ensuring that only the most recent entries are kept. - -**Note**: -- Ensure that the `memory_size` parameter is a non-negative integer to avoid unexpected behavior. -- The FIFOBuffer will automatically discard the oldest items when new items are added beyond its capacity, maintaining the specified buffer size. -*** -### FunctionDef construct_prompt(self, summary, mask) -**construct_prompt**: The function of construct_prompt is to construct the system and user prompt. - -**parameters**: The parameters of this Function. -· summary: A summary object containing variables and user feedback. -· mask: An optional parameter to mask certain parts of the prompt. -· *args: Additional positional arguments. -· **kwargs: Additional keyword arguments. - -**Code Description**: The construct_prompt function is designed to create both system and user prompts by leveraging the functionality of its superclass. Initially, it calls the superclass's construct_prompt method to generate the base system and user prompts. - -If the memory buffer contains any entries, the function enhances the user prompt by adding examples from past interactions. It does this by splitting the user prompt at a predefined final prompt and then appending a formatted string that includes past variables and their corresponding feedback. These examples are formatted as JSON strings for clarity and are joined together with newline characters. - -After constructing the enhanced user prompt, the function adds the current summary's variables and user feedback to the memory buffer using the add method from the FIFOBuffer class. This ensures that the memory buffer is updated with the latest interaction, maintaining a record of past interactions for future use. - -**Note**: -- The memory buffer must be properly initialized and managed to ensure that past interactions are correctly stored and retrieved. -- Proper handling of the mask parameter is essential if masking functionality is required. -- The function relies on the superclass's construct_prompt method, so any changes to the superclass method may affect this function's behavior. - -**Output Example**: -A possible return value of the function could be: -``` -system_prompt: "System prompt content" -user_prompt: "User prompt content\nBelow are some variables and their feedbacks you received in the past.\n\n{\n \"variables\": {\n \"var1\": \"value1\",\n \"var2\": \"value2\"\n },\n \"feedback\": \"positive\"\n}\n\nFinal prompt content" -``` -*** diff --git a/generated_docs/opto/optimizers/opro.md b/generated_docs/opto/optimizers/opro.md deleted file mode 100644 index 4178cbad..00000000 --- a/generated_docs/opto/optimizers/opro.md +++ /dev/null @@ -1,79 +0,0 @@ -## ClassDef OPRO -**OPRO**: The function of OPRO is to serve as a subclass of the FunctionOptimizer class, implementing the optimization process for a specific problem. It overrides the `_step` method to propose new parameter values based on feedback and constructs the update dictionary. It also provides methods for constructing prompts, extracting suggestions, and calling the Language Model (LLM). - -**attributes**: -- user_prompt_template: A template for the user prompt, including placeholders for the problem instance and the instruction. -- output_format_prompt: A template for the output format of the optimizer's response, specifying the JSON format and providing a structure for the response. -- default_objective: The default objective of the optimizer, which is to change the values of the variables in the `#Variables` section to improve the output according to the feedback. -- buffer: A list used to store the variables and feedback from each step of the optimization process. - -**Code Description**: -The OPRO class is a subclass of the FunctionOptimizer class and provides a specific implementation for optimizing a problem. It extends the FunctionOptimizer class and overrides the `_step` method to propose new parameter values based on feedback and construct the update dictionary. - -The `__init__` method initializes the OPRO object by calling the superclass's `__init__` method and passing the arguments. It also initializes the `buffer` attribute as an empty list. - -The `construct_prompt` method constructs the system and user prompts based on the summary and a mask. It uses the `user_prompt_template` attribute to format the user prompt, including the problem instance and the instruction. - -The `_step` method is responsible for proposing new parameter values based on feedback. It calls the LLM with the system and user prompts and extracts the suggestion from the response. It then constructs the update dictionary using the `construct_update_dict` method. - -The `construct_update_dict` method converts the suggestion in text format into the right data type and constructs an update dictionary. It iterates over the trainable parameters and checks if the parameter is present in the suggestion. If it is, it tries to convert the suggestion value to the data type of the parameter and adds it to the update dictionary. - -The `extract_llm_suggestion` method extracts the suggestion from the response received from the LLM. It first tries to parse the response as a JSON object and extract the suggestion from the "suggestion" field. If that fails, it tries to extract the suggestion key-value pairs using regular expressions. - -The `call_llm` method calls the LLM with a prompt and returns the response. It formats the prompt as a list of messages with system and user roles and calls the LLM's `create` method. It then retrieves the response from the LLM's `choices` attribute. - -**Note**: -- The OPRO class is designed to be subclassed and extended to create specific optimizers for different types of problems. -- Subclasses of OPRO must implement the `_step` method. -- The OPRO class provides methods for constructing prompts, extracting suggestions, and calling the LLM. -- The class uses the FunctionOptimizer class as its superclass and inherits its attributes and methods. - -**Output Example**: -{ - "reasoning": "In this case, the desired response would be to change the value of input a to 14, as that would make the code return 10.", - "answer": {}, - "suggestion": { - "a": 10 - } -} -### FunctionDef __init__(self) -**__init__**: The function of __init__ is to initialize an instance of the OPRO class. - -**parameters**: The parameters of this Function. -· *args: Variable length argument list. -· **kwargs: Arbitrary keyword arguments. - -**Code Description**: The __init__ method is a constructor that initializes an instance of the OPRO class. It begins by calling the __init__ method of its superclass using the super() function, passing along any arguments (*args) and keyword arguments (**kwargs) it received. This ensures that the parent class is properly initialized. After the superclass initialization, it creates an instance variable named 'buffer' and initializes it as an empty list. This 'buffer' can be used to store data or objects that are relevant to the instance of the OPRO class. - -**Note**: -- Ensure that the superclass of OPRO is correctly defined and its __init__ method is compatible with the arguments passed. -- The 'buffer' list is initialized as empty and can be used to store any necessary data during the lifecycle of the OPRO instance. -*** -### FunctionDef construct_prompt(self, summary, mask) -**construct_prompt**: The function of construct_prompt is to construct the system and user prompt based on the provided summary and optional mask. - -**parameters**: The parameters of this Function. -· summary: An object containing variables and user feedback. -· mask: An optional parameter that can be used to filter or modify the prompt construction process. -· *args: Additional positional arguments. -· **kwargs: Additional keyword arguments. - -**Code Description**: The construct_prompt function begins by appending a tuple of summary variables and user feedback to the buffer. It then iterates over the buffer to create a list of examples. Each example is a JSON-formatted string that includes the variables and feedback. The variables are formatted such that only the first element of each variable's value is included. These examples are joined into a single string with newline characters separating them. - -Next, the function constructs the user prompt by formatting the user_prompt_template with the examples and the objective. Finally, it returns a tuple containing the output_format_prompt and the constructed user prompt. - -**Note**: -- Ensure that the summary object contains the necessary attributes: variables and user_feedback. -- The buffer is assumed to be an attribute of the class instance and should be initialized before calling this function. -- The user_prompt_template and objective should also be defined as attributes of the class instance. - -**Output Example**: -Assuming the buffer contains two entries with the following data: -1. variables: {'var1': ['value1'], 'var2': ['value2']} - feedback: 'Good' -2. variables: {'var3': ['value3'], 'var4': ['value4']} - feedback: 'Needs improvement' - -The returned tuple might look like: -('output_format_prompt_value', 'User prompt with examples:\n{\n "variables": {\n "var1": "value1",\n "var2": "value2"\n },\n "feedback": "Good"\n}\n{\n "variables": {\n "var3": "value3",\n "var4": "value4"\n },\n "feedback": "Needs improvement"\n}\nInstruction: objective_value') -*** diff --git a/generated_docs/opto/optimizers/optimizers.md b/generated_docs/opto/optimizers/optimizers.md deleted file mode 100644 index 570bd128..00000000 --- a/generated_docs/opto/optimizers/optimizers.md +++ /dev/null @@ -1,267 +0,0 @@ -## ClassDef AbstractOptimizer -**AbstractOptimizer**: The function of AbstractOptimizer is to serve as a base class for optimizers, responsible for updating parameters based on feedback. - -**attributes**: The attributes of this Class. -· parameters: A list of ParameterNode objects that the optimizer will manage and update. - -**Code Description**: The AbstractOptimizer class is designed to be a foundational class for creating various optimizers. It ensures that any derived optimizer class will have a consistent interface and behavior for managing and updating parameters. - -- The `__init__` method initializes the optimizer with a list of ParameterNode objects. It asserts that the provided parameters are indeed a list and that each element in the list is an instance of ParameterNode. This ensures type safety and consistency in the parameters being managed. - -- The `step` method is an abstract method intended to be overridden by subclasses. It is supposed to contain the logic for updating the parameters based on feedback. Since it is not implemented in AbstractOptimizer, any subclass must provide an implementation for this method. - -- The `zero_feedback` method is another abstract method that must be implemented by subclasses. It is intended to reset the feedback for all parameters, preparing them for the next optimization step. - -- The `propagator` property is designed to return a Propagator object, which can be used to propagate feedback backward through the network. This property must also be implemented by any subclass. - -The AbstractOptimizer class is called by the Optimizer class, which extends its functionality. The Optimizer class provides concrete implementations for the abstract methods defined in AbstractOptimizer. For instance, it implements the `step` method to propose new parameter values based on feedback and then update the parameters accordingly. It also provides a `zero_feedback` method to reset feedback for all parameters and a `propagator` property to return the appropriate Propagator object. - -**Note**: -- Any subclass of AbstractOptimizer must implement the `step`, `zero_feedback`, and `propagator` methods. -- The parameters passed to the AbstractOptimizer must be a list of ParameterNode instances. -- The class ensures a consistent interface for optimizers, making it easier to extend and create new optimization algorithms. -### FunctionDef __init__(self, parameters) -**__init__**: The function of __init__ is to initialize an instance of the AbstractOptimizer class with a list of ParameterNode objects. - -**parameters**: The parameters of this Function. -· parameters: A list of ParameterNode objects that represent the parameters to be optimized. -· *args: Additional positional arguments. -· **kwargs: Additional keyword arguments. - -**Code Description**: The __init__ method of the AbstractOptimizer class is responsible for initializing the optimizer with a set of parameters. It takes a list of ParameterNode objects as its primary argument. The method first asserts that the provided parameters argument is indeed a list. It then checks that every element in this list is an instance of the ParameterNode class. If these conditions are met, the parameters are assigned to the instance variable self.parameters. - -The ParameterNode class, which is used in this context, represents a trainable node in a computational graph. It is initialized with various attributes such as value, name, trainable status, description, constraint, and additional info. The ParameterNode class inherits from a generic Node class and adds itself to a set of dependencies upon initialization. - -**Note**: -- Ensure that the parameters argument passed to the __init__ method is a list of ParameterNode objects. -- The method uses assertions to enforce type checking, which will raise an AssertionError if the conditions are not met. -- Additional positional and keyword arguments (*args and **kwargs) are accepted but not utilized within this method. -*** -### FunctionDef step(self) -**step**: The function of step is to update the parameters based on the feedback. - -**parameters**: The parameters of this Function. -· None - -**Code Description**: The step function is designed to update the parameters of an optimizer based on feedback. However, in its current form, it is an abstract method, meaning it is intended to be overridden by subclasses of the AbstractOptimizer class. The method raises a NotImplementedError, which indicates that any subclass must provide its own implementation of the step method. This design enforces that the specific logic for updating parameters must be defined in the subclasses, ensuring that the AbstractOptimizer class remains flexible and adaptable to various optimization strategies. - -**Note**: -- This method must be implemented in any subclass of AbstractOptimizer. -- Attempting to call this method directly from an instance of AbstractOptimizer will result in a NotImplementedError. -- Ensure that the subclass provides a concrete implementation of the step method to perform the actual parameter update logic. -*** -### FunctionDef zero_feedback(self) -**zero_feedback**: The function of zero_feedback is to reset the feedback. - -**parameters**: The parameters of this Function. -· This function does not take any parameters. - -**Code Description**: The zero_feedback function is designed to reset the feedback mechanism within an optimizer. However, the function is currently not implemented and raises a NotImplementedError when called. This indicates that any subclass inheriting from the class containing this function must provide its own implementation of the zero_feedback method. The purpose of this function is to ensure that subclasses define how the feedback should be reset, which is crucial for the proper functioning of the optimizer. - -**Note**: When using this function, it is important to implement the zero_feedback method in any subclass that inherits from the parent class. Failure to do so will result in a NotImplementedError being raised, which will halt the execution of the program. This function serves as a placeholder to enforce the implementation of feedback resetting logic in derived classes. -*** -### FunctionDef propagator(self) -**propagator**: The function of propagator is to return a Propagator object that can be used to propagate feedback in backward. - -**parameters**: The parameters of this Function. -· None - -**Code Description**: The propagator function is designed to return a Propagator object, which is intended to be used for propagating feedback in a backward pass. However, the current implementation of this function raises a NotImplementedError. This indicates that the function is meant to be overridden in a subclass, where the actual logic for returning a Propagator object should be provided. The NotImplementedError serves as a placeholder to remind developers that they need to implement this method in any concrete subclass derived from the abstract class. - -**Note**: When using this function, ensure that it is properly overridden in any subclass. Attempting to call this method directly from the abstract class without overriding it will result in a NotImplementedError. -*** -## ClassDef Optimizer -**Optimizer**: The function of Optimizer is to serve as a base class for optimizers, responsible for updating parameters based on feedback. - -**attributes**: -- parameters: A list of ParameterNode objects that the optimizer will manage and update. - -**Code Description**: -The Optimizer class is a base class for creating various optimizers. It provides a consistent interface and behavior for managing and updating parameters based on feedback. The class extends the AbstractOptimizer class and implements the abstract methods defined in it. - -The `__init__` method initializes the optimizer with a list of ParameterNode objects. It ensures that the provided parameters are a list and that each element in the list is an instance of ParameterNode. This ensures type safety and consistency in the parameters being managed. The method also sets the propagator attribute to the default propagator returned by the default_propagator method. - -The `propagator` property returns the propagator object associated with the optimizer. - -The `step` method is responsible for proposing new parameter values based on feedback and updating the parameters accordingly. It calls the `propose` method to get the proposed update dictionary and then calls the `update` method to update the trainable parameters with the new data. - -The `propose` method is a helper method that calls the `_step` method to get the new data of the parameters based on the feedback. - -The `update` method updates the trainable parameters with the new data provided in the update dictionary. It iterates over the items in the update dictionary and updates the data of each trainable parameter if it is marked as trainable. - -The `zero_feedback` method resets the feedback for all parameters by calling the `zero_feedback` method of each parameter. - -The `_step` method is an abstract method that must be implemented by subclasses. It returns the new data of parameter nodes based on the feedback. Subclasses should provide their own implementation of this method. - -The `default_propagator` method is an abstract method that must be implemented by subclasses. It returns the default Propagator object of the optimizer. Subclasses should provide their own implementation of this method. - -The `backward` method propagates the feedback backward by calling the `backward` method of the given node with the propagator object. - -**Note**: -- Any subclass of Optimizer must implement the `_step`, `default_propagator`, and `backward` methods. -- The parameters passed to the Optimizer must be a list of ParameterNode instances. -- The class ensures a consistent interface for optimizers, making it easier to extend and create new optimization algorithms. - -**Output Example**: -```python -{ - 'parameter1': value1, - 'parameter2': value2, - ... -} -``` -### FunctionDef __init__(self, parameters) -**__init__**: The function of __init__ is to initialize an instance of the Optimizer class with specified parameters and an optional propagator. - -**parameters**: The parameters of this Function. -· parameters: A list of ParameterNode objects that represent the parameters to be optimized. -· *args: Additional positional arguments. -· propagator: An optional Propagator object. If not provided, a default Propagator will be used. -· **kwargs: Additional keyword arguments. - -**Code Description**: The __init__ method initializes an Optimizer instance. It first calls the superclass's __init__ method with the provided parameters. Then, it checks if a propagator is provided. If not, it calls the default_propagator method to obtain a default Propagator. The method ensures that the propagator is an instance of the Propagator class. Finally, it assigns the propagator to the instance's _propagator attribute. This setup ensures that the Optimizer always has a valid Propagator, either provided explicitly or obtained through the default_propagator method. - -**Note**: When using this class, ensure that the parameters argument is a list of ParameterNode objects and that the propagator, if provided, is an instance of the Propagator class. If no propagator is provided, the default_propagator method must be properly implemented in a subclass to avoid a NotImplementedError. -*** -### FunctionDef propagator(self) -**propagator**: The function of propagator is to return the internal `_propagator` attribute of the class. - -**parameters**: The parameters of this Function. -· None - -**Code Description**: The `propagator` function is a simple accessor method that returns the value of the `_propagator` attribute from the class instance. This method does not take any parameters and directly provides access to the internal `_propagator` attribute, which is presumably an instance of a propagator object used within the class. - -The `propagator` function is utilized in several other methods within the project. For instance, in the `summarize` method of the `FunctionOptimizer` class, it is used to aggregate feedback from all trainable parameters. The `propagator` is called to perform the aggregation of feedbacks, which are then summed up to create a summary. - -In the `_step` method of the `FunctionOptimizer` class, the `propagator` is asserted to be an instance of `GraphPropagator` before summarizing the feedback and constructing prompts for further processing. - -Additionally, in the `backward` method of the `Optimizer` class, the `propagator` is passed as an argument to the `backward` method of a node, facilitating the backward propagation of feedback. - -**Note**: This function is a straightforward accessor and does not perform any additional logic or validation. It is essential that the `_propagator` attribute is correctly initialized within the class for this method to function as expected. - -**Output Example**: The return value of the `propagator` function would be the internal `_propagator` object, which could be an instance of a class responsible for propagating information or feedback within the optimization process. For example: -``` - -``` -*** -### FunctionDef step(self) -**step**: The function of step is to execute a single optimization step by proposing new parameter data and updating the parameters accordingly. - -**parameters**: The parameters of this Function. -· *args: Variable length argument list. -· **kwargs: Arbitrary keyword arguments. - -**Code Description**: The step function is a method within the Optimizer class that orchestrates the process of updating the trainable parameters. It performs this task in two main stages: - -1. **Propose New Data**: The function first calls the propose method, passing along any positional and keyword arguments it receives. The propose method generates a dictionary (update_dict) containing new data for the parameters. This dictionary is created based on feedback and is essential for the subsequent update process. - -2. **Update Parameters**: After obtaining the update_dict from the propose method, the step function calls the update method. The update method takes the update_dict as input and iterates over its key-value pairs. For each pair, it checks if the parameter node (key) is marked as trainable. If the node is trainable, it updates the node's internal data (_data) with the new data provided in the dictionary. - -The step function is integral to the optimization process, as it ensures that the parameters are updated based on the latest feedback. It relies on the propose method to generate the necessary updates and the update method to apply these updates to the parameters. - -**Note**: -- The propose method must be correctly implemented to generate a valid update_dict. -- The update method will only modify the parameters that are marked as trainable. -- The step function is designed to be flexible, accepting any number of positional and keyword arguments, which are passed through to the propose method. -*** -### FunctionDef propose(self) -**propose**: The function of propose is to propose the new data of the parameters based on the feedback. - -**parameters**: The parameters of this Function. -· *args: Variable length argument list. -· **kwargs: Arbitrary keyword arguments. - -**Code Description**: The propose function is a method within the Optimizer class designed to generate new parameter data based on feedback. It serves as a public interface for proposing updates to the parameters. The function accepts any number of positional and keyword arguments, which are then passed directly to the _step method. - -The propose method internally calls the _step method, which is responsible for the actual computation of the new parameter data. The _step method is abstract and must be implemented by any subclass of the Optimizer class. This design allows for different optimization strategies to be implemented by overriding the _step method in subclasses. - -The propose method is also called by the step method within the same class. The step method uses propose to generate the update dictionary, which is then applied to update the parameters. - -**Note**: -- The _step method must be implemented in any subclass of the Optimizer class; otherwise, a NotImplementedError will be raised. -- The propose method relies on the _step method to perform the actual parameter updates, making it essential to provide a correct and efficient implementation of _step in subclasses. -- The function is designed to be flexible, accepting any number of positional and keyword arguments. - -**Output Example**: A possible appearance of the code's return value could be a dictionary where keys are instances of ParameterNode and values can be of any type, representing the new data for each parameter node. -*** -### FunctionDef update(self, update_dict) -**update**: The function of update is to update the trainable parameters given a dictionary of new data. - -**parameters**: The parameters of this Function. -· update_dict: A dictionary where keys are instances of ParameterNode and values are the new data to update the parameters with. - -**Code Description**: The update function is designed to modify the trainable parameters of an optimizer. It takes a dictionary, update_dict, as input. The keys of this dictionary are instances of ParameterNode, and the values are the new data to be assigned to these nodes. - -The function iterates over each key-value pair in the update_dict. For each pair, it checks if the ParameterNode (key) is marked as trainable. If the node is trainable, it updates the node's internal data (_data) with the new data provided in the dictionary. - -This function is called by the step function within the same Optimizer class. The step function first generates an update_dict by calling the propose method and then passes this dictionary to the update function to apply the updates. - -**Note**: -- Ensure that the keys in the update_dict are instances of ParameterNode. -- Only the nodes marked as trainable will be updated. -- This function directly modifies the internal state (_data) of the ParameterNode instances. -*** -### FunctionDef zero_feedback(self) -**zero_feedback**: The function of zero_feedback is to reset the feedback values of all parameters managed by the optimizer to zero. - -**parameters**: The parameters of this Function. -· This function does not take any parameters. - -**Code Description**: The zero_feedback function iterates over all the parameters contained within the optimizer instance and calls the zero_feedback method on each parameter. This effectively resets any feedback-related values or states associated with the parameters to zero. This function is crucial in scenarios where feedback mechanisms are used to adjust parameters during optimization, and there is a need to reset these adjustments, possibly at the beginning of a new optimization cycle or after a certain number of iterations. - -The function is called within the context of unit tests located in tests\unit_tests\test_optimizer.py, indicating its importance in ensuring that the feedback resetting mechanism works correctly. This is essential for maintaining the integrity and expected behavior of the optimizer during its operation. - -**Note**: -- Ensure that each parameter object within the optimizer has a zero_feedback method implemented; otherwise, this function will raise an AttributeError. -- This function should be used when there is a need to clear feedback states, typically before starting a new optimization phase or after specific intervals to maintain the stability and performance of the optimization process. -*** -### FunctionDef _step(self) -**_step**: The function of _step is to return the new data of parameter nodes based on the feedback. - -**parameters**: The parameters of this Function. -· *args: Variable length argument list. -· **kwargs: Arbitrary keyword arguments. - -**Code Description**: The _step function is designed to be a core method within an optimizer class, responsible for updating the data of parameter nodes based on feedback. This function is abstract and raises a NotImplementedError, indicating that any subclass must provide an implementation for this method. The return type of the function is a dictionary where keys are instances of ParameterNode and values can be of any type. - -The _step function is called by the propose method within the same class. The propose method serves as a public interface to generate new parameter data based on feedback, and it delegates the actual computation to the _step method. This design allows for flexibility and extensibility, as different optimization strategies can be implemented by overriding the _step method in subclasses. - -The ParameterNode class, which is referenced in the return type, represents a trainable node in a computational graph. It inherits from a generic Node class and includes additional attributes such as name, trainable status, description, constraint, and info. The ParameterNode class also maintains a set of dependencies, specifically adding itself to a 'parameter' dependency set. - -**Note**: -- The _step function must be implemented in any subclass of the optimizer class; otherwise, a NotImplementedError will be raised. -- The function is designed to be flexible, accepting any number of positional and keyword arguments. -- The propose method relies on _step to perform the actual parameter updates, making it essential to provide a correct and efficient implementation of _step in subclasses. -*** -### FunctionDef default_propagator(self) -**default_propagator**: The function of default_propagator is to return the default Propagator object of the optimizer. - -**parameters**: The parameters of this Function. -· This function does not take any parameters. - -**Code Description**: The default_propagator function is designed to return the default Propagator object associated with the optimizer. However, in its current implementation, it raises a NotImplementedError. This indicates that the function is intended to be overridden in a subclass, where the actual logic for returning a default Propagator will be provided. The function is called within the __init__ method of the Optimizer class. During the initialization of an Optimizer object, if no Propagator is explicitly provided, the default_propagator function is invoked to obtain a default Propagator. The returned Propagator is then assigned to the _propagator attribute of the Optimizer instance. This ensures that the Optimizer always has a valid Propagator, either provided explicitly or obtained through the default_propagator method. - -**Note**: When implementing a subclass of the Optimizer, it is essential to override the default_propagator method to provide a concrete implementation that returns a valid Propagator object. Failure to do so will result in a NotImplementedError being raised during the initialization of the Optimizer if no Propagator is provided. -*** -### FunctionDef backward(self, node) -**backward**: The function of backward is to perform a backward pass in the optimization process. It propagates feedback from a node to its parents by calling the propagator function and updating the feedback values. - -**parameters**: -- node: The node from which the feedback is propagated. -- *args: Additional positional arguments that can be passed to the node's backward method. -- **kwargs: Additional keyword arguments that can be passed to the node's backward method. - -**Code Description**: The backward function is responsible for propagating feedback from a node to its parents in the optimization process. It first checks if a propagator function is provided, and if not, it imports the GraphPropagator class from the opto.trace.propagators.graph_propagator module. - -The function then adds the feedback from the node to a feedback dictionary using the _add_feedback method of the node. The feedback is obtained by calling the propagator function with the node as an argument. The feedback dictionary is used to store the feedback from each child node, where each key is a child node and the value is a list of feedbacks from that child. - -After adding the feedback, the function iterates over the parents of the node and propagates the feedback to each parent. If a parent is present in the propagated feedback dictionary, the feedback is added to the parent using the _add_feedback method. - -The function also supports visualization of the propagation process by creating a graph using the graphviz library. The graph is created in reverse order if the reverse_plot parameter is set to True. - -Finally, the function sets the _backwarded attribute of the node to True, indicating that the backward pass has been performed. The value of the retain_graph parameter determines whether the feedback should be retained or zeroed out after propagation. - -**Note**: It is important to ensure that the propagator function is correctly initialized before calling the backward function. The function relies on the propagator to perform the feedback propagation. If the propagator is not provided or initialized correctly, the backward pass may not function as expected. - -**Output Example**: The backward function returns a graph (digraph) object if the visualize parameter is set to True. Otherwise, it returns None. -*** diff --git a/generated_docs/opto/trace/broadcast.md b/generated_docs/opto/trace/broadcast.md deleted file mode 100644 index bb0367d8..00000000 --- a/generated_docs/opto/trace/broadcast.md +++ /dev/null @@ -1,54 +0,0 @@ -## FunctionDef apply_op(op, output) -**apply_op**: The function of apply_op is to perform a broadcasting operation that applies a given operator to a container of Nodes. - -**parameters**: -- op (callable): The operator to be applied. -- output (Any): The container to be updated. -- *args (Any): The positional inputs of the operator. -- **kwargs (Any): The keyword inputs of the operator. - -**Code Description**: -The apply_op function takes an operator (op), an output container, and positional and keyword inputs. It first combines the positional and keyword inputs into a single list called "inputs". It then checks if there are any containers in the inputs list. If there are no containers, indicating that all inputs are Nodes, the function simply applies the operator to the inputs and returns the result. - -If there is at least one container in the inputs list, the function performs the broadcasting operation. It iterates over the output container and applies the operator recursively to each element of the output container, along with the corresponding elements from the positional and keyword inputs. The result of each recursive call is assigned back to the corresponding element in the output container. - -The function handles different types of output containers: -- If the output is a list or tuple, the function checks that the output and inputs have the same length. It then applies the operator to each element of the output container, along with the corresponding elements from the positional and keyword inputs. -- If the output is a dictionary, the function iterates over the key-value pairs of the output and applies the operator to each value, along with the corresponding elements from the positional and keyword inputs. -- If the output is an instance of the NodeContainer class, the function iterates over the attributes of the output and applies the operator to each attribute, along with the corresponding elements from the positional and keyword inputs. - -The apply_op function ensures that all inputs are either Nodes or have the same type as the output. It raises an assertion error if this condition is not met. - -**Note**: -- The apply_op function relies on the NodeContainer class to identify containers of Nodes and apply the operator recursively to each attribute of the container. -- The function supports broadcasting operations on different types of output containers, including lists, tuples, dictionaries, and instances of the NodeContainer class. -- It is important to ensure that the inputs and output are compatible in terms of length and type to avoid errors during the broadcasting operation. - -**Output Example**: -The updated output container after applying the operator to the inputs. -### FunctionDef admissible_type(x, base) -**admissible_type**: The function of admissible_type is to determine whether the type of an object is admissible for a given base type or if it is an instance of the Node class. - -**parameters**: -- x: The object whose type needs to be checked. -- base: The base type against which the object's type is compared. - -**Code Description**: -The admissible_type function takes two parameters, x and base, and returns a boolean value indicating whether the type of x is equal to the type of base or if x is an instance of the Node class. - -The function first checks if the type of x is equal to the type of base using the "type" function. If the types are equal, it returns True. - -If the types are not equal, the function uses the "isinstance" function to check if x is an instance of the Node class. If x is an instance of Node, it returns True. Otherwise, it returns False. - -This function is useful when you want to check if an object's type is admissible for a specific base type or if it is an instance of a specific class. - -**Note**: -- The function assumes that the Node class is defined and imported correctly. -- The function only checks for exact type equality, not inheritance relationships. - -**Output Example**: -- admissible_type(5, int) returns True -- admissible_type("hello", str) returns True -- admissible_type(5, str) returns False -- admissible_type(Node(), Node) returns True -*** diff --git a/generated_docs/opto/trace/bundle.md b/generated_docs/opto/trace/bundle.md deleted file mode 100644 index 611765cd..00000000 --- a/generated_docs/opto/trace/bundle.md +++ /dev/null @@ -1,469 +0,0 @@ -## FunctionDef bundle(description, n_outputs, node_dict, traceable_code, wrap_output, unpack_input, trainable, catch_execution_error, allow_external_dependencies, overwrite_python_recursion) -**bundle**: The function of bundle is to wrap a function as a FunModule, which returns node objects. - -**parameters**: -- description: A string that describes the function. -- n_outputs: An integer that specifies the number of outputs the wrapped function should have. -- node_dict: Either "auto" or a dictionary that maps input names to node objects. -- traceable_code: A boolean value indicating whether the code should be traced using nodes. -- wrap_output: A boolean value indicating whether the output should be wrapped as a node object. -- unpack_input: A boolean value indicating whether the input should be unpacked. -- trainable: A boolean value indicating whether the wrapped function is trainable. -- catch_execution_error: A boolean value indicating whether execution errors should be caught. -- allow_external_dependencies: A boolean value indicating whether external dependencies are allowed. -- overwrite_python_recursion: A boolean value indicating whether Python recursion should be overwritten. - -**Code Description**: The bundle function is a decorator that wraps a function as a FunModule. It takes in various parameters to customize the behavior of the wrapped function. Inside the decorator, it creates a FunModule object with the specified parameters and returns it. - -The decorator function also captures the locals of the calling function using the inspect module. This allows the wrapped function to access the locals of the calling function. - -The wrapped function can be called with the same input signature as the original function. The output of the wrapped function is a node object, which represents the result of the function computation. The node object can be used in further computations or as inputs to other functions. - -The bundle function provides flexibility in customizing the behavior of the wrapped function. It allows specifying the number of outputs, mapping input names to node objects, tracing the code using nodes, wrapping the output as a node object, unpacking the input, making the wrapped function trainable, catching execution errors, allowing external dependencies, and overwriting Python recursion. - -**Note**: -- The wrapped function should have a consistent input signature. -- The wrapped function can access the locals of the calling function. -- The output of the wrapped function is a node object. -- The behavior of the wrapped function can be customized using the parameters of the bundle function. - -**Output Example**: -```python -@bundle(description="This is a bundled function", n_outputs=2) -def add(a, b): - return a + b, a - b - -output = add(3, 2) -print(output) -# Output: (5, 1) -``` -### FunctionDef decorator(fun) -Doc is waiting to be generated... -*** -## ClassDef trace_nodes -**trace_nodes**: The function of trace_nodes is to act as a context manager for tracking which nodes are read or used in an operator. - -**attributes**: The attributes of this Class. -· No explicit attributes are defined within this class. - -**Code Description**: The trace_nodes class is designed to manage the tracking of nodes that are accessed during the execution of an operator. It achieves this by leveraging Python's context management protocol, which includes the `__enter__` and `__exit__` methods. - -- The `__enter__` method initializes a new set to store the nodes that will be used and appends this set to the global `USED_NODES` list. It then returns this set, allowing it to be used within the context. -- The `__exit__` method is called when the context is exited. It removes the set of used nodes from the global `USED_NODES` list, ensuring that the tracking is properly cleaned up. - -In the context of its usage within the `forward` method of the `FunModule` class, the trace_nodes context manager is used to keep track of all nodes that are accessed during the execution of the operator function (`self.fun`). When the `forward` method is called, it enters the trace_nodes context, which starts tracking the nodes. After the function execution, the context is exited, and the set of used nodes is then available for further processing. - -The `forward` method uses this set of nodes to construct the inputs of a `MessageNode` from the function inputs or the set of used nodes. It also identifies any external dependencies, which are nodes used to create the outputs but not included in the inputs. If external dependencies are not allowed and are detected, an exception is raised. - -**Note**: -- Ensure that the global `USED_NODES` list is properly managed to avoid any unintended side effects. -- The trace_nodes context manager should be used within a controlled environment where the global state can be safely modified and restored. - -**Output Example**: -When used within the `forward` method of the `FunModule` class, the trace_nodes context manager might return a set of nodes that were accessed during the function execution. For example: -``` -with trace_nodes() as used_nodes: - # Function execution that accesses nodes - pass -# used_nodes might contain: {Node1, Node2, Node3} -``` -### FunctionDef __enter__(self) -**__enter__**: The function of __enter__ is to initialize and return a new set of nodes, and to append this set to the global list USED_NODES. - -**parameters**: The parameters of this Function. -· self: Refers to the instance of the class in which this method is defined. - -**Code Description**: The __enter__ method is a special method used in the context management protocol. When an instance of the class containing this method is used in a with statement, the __enter__ method is automatically invoked at the beginning of the block. In this implementation, the method performs the following actions: -1. Initializes an empty set named `nodes`. -2. Appends this set to the global list `USED_NODES`. -3. Returns the set `nodes`. - -This allows the set of nodes to be used within the with block and ensures that it is tracked in the global `USED_NODES` list. - -**Note**: -- Ensure that the global list `USED_NODES` is defined before using this method. -- This method is typically paired with an `__exit__` method to handle cleanup actions when the with block is exited. - -**Output Example**: -When the __enter__ method is called, it returns an empty set. For example: -``` -with some_instance as nodes: - # nodes is an empty set - print(nodes) # Output: set() -``` -*** -### FunctionDef __exit__(self, type, value, traceback) -**__exit__**: The function of __exit__ is to handle the cleanup process when exiting a context managed by a with statement. - -**parameters**: The parameters of this Function. -· type: The exception type, if an exception was raised. -· value: The exception instance, if an exception was raised. -· traceback: The traceback object, if an exception was raised. - -**Code Description**: The __exit__ method is a special method used in context management to define cleanup actions when exiting a context. In this specific implementation, the __exit__ method removes the last element from the USED_NODES list by calling the pop() method. This indicates that the context manager is maintaining a stack of nodes, and upon exiting the context, it ensures that the most recently added node is removed from the stack. This is a common pattern in resource management where resources are pushed onto a stack when entering a context and popped off when exiting to ensure proper cleanup and resource deallocation. - -**Note**: -- Ensure that the USED_NODES list is properly initialized and managed elsewhere in the code to avoid potential errors. -- This method does not handle exceptions; it simply performs the cleanup action. If exception handling is required, it should be implemented separately. -*** -## ClassDef FunModule -Doc is waiting to be generated... -### FunctionDef __init__(self, fun, description, n_outputs, node_dict, traceable_code, wrap_output, unpack_input, trainable, catch_execution_error, allow_external_dependencies, overwrite_python_recursion, ldict) -**__init__**: The function of __init__ is to initialize an instance of the FunModule class. - -**Parameters**: -- self: The instance of the FunModule class. -- fun: A callable object representing the function to be wrapped. -- description: An optional string describing the function module. -- n_outputs: An integer indicating the number of outputs of the function. -- node_dict: A dictionary, None, or "auto" representing the node dictionary. -- traceable_code: A boolean indicating whether the code is traceable or not. -- wrap_output: A boolean indicating whether to wrap the output or not. -- unpack_input: A boolean indicating whether to unpack the input or not. -- trainable: A boolean indicating whether the function is trainable or not. -- catch_execution_error: A boolean indicating whether to catch execution errors or not. -- allow_external_dependencies: A boolean indicating whether to allow external dependencies or not. -- overwrite_python_recursion: A boolean indicating whether to overwrite Python recursion or not. -- ldict: A dictionary or None representing the local dictionary. - -**Code Description**: The __init__ function initializes an instance of the FunModule class. It takes in various parameters such as fun, description, n_outputs, node_dict, traceable_code, wrap_output, unpack_input, trainable, catch_execution_error, allow_external_dependencies, overwrite_python_recursion, and ldict. - -The function starts by asserting that the ldict parameter is either None or a dictionary. If ldict is None, an empty dictionary is assigned to self.ldict. Otherwise, a copy of ldict is assigned to self.ldict. - -If traceable_code is True, the unpack_input parameter is set to False and the allow_external_dependencies parameter is set to True. This is because when the code is traceable, there is no need to unpack the input and there may be new nodes created in the code block. - -The function then asserts that the fun parameter is callable and that the node_dict parameter is either a dictionary, None, or "auto". - -Next, the source code of the function is obtained using the inspect.getsource() function. If the source code starts with a decorator line, the decorator line is removed and only the function definition is kept. Otherwise, the source code is trimmed. - -The function constructs an info dictionary containing information about the function module. This includes the function name, docstring, signature, source code, output, external dependencies, and node dictionary. - -If the description parameter is None, a description is generated using the function name and docstring. The get_op_name() function is called to extract the operator type from the description. The extracted operator type is combined with the function name and docstring to create a meaningful description. - -The function assigns the provided parameters to the corresponding attributes of the FunModule instance. It also sets the parameter attribute to None. - -If the n_outputs parameter is greater than 1, a warning message is displayed indicating that setting n_outputs>1 will be deprecated. - -Finally, if the trainable parameter is True, the function asserts that overwrite_python_recursion is also True. It then searches for the function signature in the source code and creates a ParameterNode object with the source code as the value and "__code" as the name. This ParameterNode represents the code constraint for the trainable function. - -**Note**: -- The ldict parameter must be a dictionary or None. -- The fun parameter must be a callable object. -- The node_dict parameter must be a dictionary, None, or "auto". -- The description parameter will be generated if it is None. -- The n_outputs parameter should be used with caution as setting n_outputs>1 will be deprecated. -- The trainable parameter requires overwrite_python_recursion to be True. -- The source code of the function is obtained using the inspect.getsource() function. -- The get_op_name() function is used to extract the operator type from the description. -- The info dictionary contains information about the function module. -- The parameter attribute is set to None unless the trainable parameter is True. -*** -### FunctionDef filter_global_namespaces(self, keys) -**filter_global_namespaces**: The function of filter_global_namespaces is to filter out keys that already exist in the current global namespace. - -**parameters**: The parameters of this Function. -· keys: A list of keys to be filtered. - -**Code Description**: The filter_global_namespaces function takes a list of keys as input and returns a new list containing only those keys that do not already exist in the current global namespace. The function initializes an empty list called filtered_keys to store the keys that pass the filtering criteria. It then iterates over each key in the input list. For each key, it checks if the key exists in the global namespace using the globals() function. If the key is found in the global namespace, it is skipped. Otherwise, the key is appended to the filtered_keys list. Finally, the function returns the filtered_keys list. - -**Note**: -- This function relies on the current global namespace, which means its behavior can vary depending on the existing global variables and functions at the time of execution. -- Ensure that the input list keys does not contain any unintended or sensitive keys that might be skipped due to their presence in the global namespace. - -**Output Example**: -If the global namespace contains the keys 'a' and 'b', and the input list is ['a', 'b', 'c', 'd'], the function will return ['c', 'd']. -*** -### FunctionDef fun(self) -**fun**: The function of fun is to execute dynamically generated code and return the resulting function. - -**parameters**: -- self: The instance of the class. -- *args: Variable length argument list. -- **kwargs: Arbitrary keyword arguments. - -**Code Description**: -The `fun` function is a method of the current class. It is responsible for executing dynamically generated code and returning the resulting function. The function takes in variable length arguments (`*args`) and arbitrary keyword arguments (`**kwargs`). - -The function first checks if the `parameter` attribute of the instance is `None`. If it is `None`, it returns the `_fun` attribute of the instance, which is the original function. - -If the `parameter` attribute is not `None`, the function retrieves the code from the `parameter` attribute and stores it in the `code` variable. It then tries to import all the global namespaces from the original function by creating a local dictionary (`ldict`) and copying the global dictionary (`gdict`) from the `_fun` attribute. The local dictionary is updated with the `ldict` attribute of the instance. The `exec` function is then called to define the function using the code, the global dictionary, and the local dictionary. The name of the function is extracted from the code using regular expression. The resulting function is stored in the `fun` variable. - -If there is an exception during the execution of the code (SyntaxError, NameError, KeyError, or OSError), an `ExecutionError` instance is created with details about the exception. The `ExecutionError` instance is then raised to indicate the error. - -Finally, the function returns the resulting function (`fun`). - -**Note**: -- The `fun` function is used within the `trace_nodes` context manager. -- The `fun` function relies on the `parameter` attribute to retrieve the dynamically generated code. -- The resulting function may be different from the original function if the code modifies the global namespaces. - -**Output Example**: -The output of the `fun` function is the resulting function that is executed from the dynamically generated code. -*** -### FunctionDef name(self) -**name**: The function of `name` is to retrieve the operator type from the description attribute of the FunModule instance. - -**parameters**: This method does not take any parameters other than `self`. - -**Code Description**: The `name` method is a member of the `FunModule` class in the `bundle.py` file. It is designed to extract and return the operator type from the `description` attribute of the `FunModule` instance. This is achieved by calling the `get_op_name` function, which processes the `description` string to find and return the operator type enclosed in square brackets at the beginning of the description. - -The `get_op_name` function uses a regular expression to search for the operator type. If the operator type is found, it is returned; otherwise, a `ValueError` is raised. This ensures that the `description` attribute of the `FunModule` instance is correctly formatted and contains the necessary operator type information. - -The `name` method is utilized within the `wrap` method of the same class. In the `wrap` method, the `name` method is used to set the `name` attribute of the `MessageNode` or `ExceptionNode` that is created based on the output of the function. This ensures that the nodes have a meaningful and accurate name that reflects the operator type. - -**Note**: -- The `description` attribute of the `FunModule` instance must contain the operator type enclosed in square brackets at the beginning. -- If the `description` does not contain the operator type, a `ValueError` will be raised by the `get_op_name` function. - -**Output Example**: -If the `description` attribute of the `FunModule` instance is "[Add] Add two numbers", the `name` method will return "Add". -*** -### FunctionDef forward(self) -**forward**: The `forward` function is responsible for executing the operator function (`self.fun`) and returning the resulting nodes. It takes in variable length arguments (`*args`) and arbitrary keyword arguments (`**kwargs`). - -**parameters**: -- `self`: The instance of the class. -- `*args`: Variable length argument list. -- `**kwargs`: Arbitrary keyword arguments. - -**Code Description**: -The `forward` function is a method of the `FunModule` class in the `bundle.py` file. It is the main function that executes the operator function and handles the processing of inputs and outputs. - -The function starts by initializing the `_args` and `_kwargs` variables with the provided arguments (`args` and `kwargs`). If the `unpack_input` attribute of the instance is `True`, the function extracts the data from the container of nodes by calling the `to_data` function on the arguments. - -Next, the function checks if the `overwrite_python_recursion` attribute is `True` and the `parameter` attribute is `None`. If both conditions are met, it sets the Python tracer to the `tracer` function defined within the `forward` function. This tracer modifies the local/global dictionary of the frame to ensure that recursive calls of the wrapped function call the unwrapped function. - -The function then enters a `trace_nodes` context manager using the `with` statement. This context manager tracks the nodes that are read or used in the operator function. The `used_nodes` set is created and appended to the global `USED_NODES` list. This set will contain the nodes that are accessed during the execution of the operator function. - -Within the context manager, the operator function (`self.fun`) is executed with the provided arguments (`_args` and `_kwargs`). If the `catch_execution_error` attribute is `True`, the function wraps the execution of the operator function in a try-except block. If an exception occurs during the execution, it is stored in the `outputs` variable. Otherwise, the `outputs` variable contains the result of the operator function. - -After the execution of the operator function, the context manager is exited, and the set of used nodes is available for further processing. - -The function then constructs the inputs of the `MessageNode` from the function inputs or the set of used nodes. If the `node_dict` attribute of the instance is `None`, the function generates a warning and creates a dictionary of inputs using the names of the nodes in the `used_nodes` set. If the `node_dict` attribute is not `None`, the function updates the input signature (`spec`) with the `node_dict` dictionary. It then iterates over the input signature and creates nodes for each input value using the `create_node` function. The resulting inputs dictionary is stored in the `inputs` variable. - -Next, the function identifies any external dependencies, which are nodes used to create the outputs but not included in the inputs. It creates a list of external dependencies by iterating over the `used_nodes` set and checking if each node is present in the `inputs` dictionary using the `contain` function. - -If the number of external dependencies is greater than 0 and the `allow_external_dependencies` attribute is `False`, the function raises a `TraceMissingInputsError` exception. This exception indicates that not all nodes used in the operator function are specified as inputs of the returned node. - -If the `GRAPH.TRACE` attribute is `False`, the `inputs` dictionary is cleared, as there is no need to keep track of the inputs if tracing is not enabled. - -Finally, the function wraps the output as a `MessageNode` or an `ExceptionNode` depending on the type of the output. If the `n_outputs` attribute of the instance is 1 or the output is an instance of `Exception`, the function calls the `wrap` function with the output, inputs, and external dependencies. Otherwise, it creates a tuple of wrapped nodes by calling the `wrap` function for each output element. - -The function returns the resulting nodes. - -**Note**: -- The `forward` function is the main function that executes the operator function and handles the processing of inputs and outputs. -- The `trace_nodes` context manager is used to track the nodes that are accessed during the execution of the operator function. -- The `tracer` function modifies the local/global dictionary of the frame to ensure that recursive calls of the wrapped function call the unwrapped function. -- The `to_data` function is used to extract the data from a node or a container of nodes. -- The `wrap` function is used to wrap the output of the operator function as a `MessageNode` or an `ExceptionNode`. -- The `TraceMissingInputsError` exception is raised when not all nodes used in the operator function are specified as inputs of the returned node. -- The `contain` function is used to check if a given node is present in a container of nodes. - -**Output Example**: -The `forward` function returns the resulting nodes of the operator function. The output can be a single `MessageNode` or `ExceptionNode` if the `n_outputs` attribute is 1 or the output is an exception. If the `n_outputs` attribute is greater than 1, the output is a tuple of `MessageNode` or `ExceptionNode` objects. -#### FunctionDef tracer(frame, event, arg) -**tracer**: The function of tracer is to modify the local and global dictionaries of a frame to ensure that recursive calls of a wrapped function invoke the unwrapped function. - -**parameters**: The parameters of this Function. -· frame: The frame object representing the current execution context. -· event: A string representing the type of event that occurred (e.g., 'call', 'return'). -· arg: An optional argument that may be passed to the tracer function (default is None). - -**Code Description**: The tracer function is designed to handle recursive calls within a wrapped function by modifying the local and global dictionaries of the frame. When the function is called, it first checks if the current frame's code object matches the code object of the wrapped function (`self._fun.__code__`). If it does, the function proceeds to handle different types of events: - -- **Call Event**: When the event is 'call', the function checks if the function name exists in the frame's local or global dictionaries. If the function name is found in the local dictionary and it does not match the wrapped function (`self._fun`), the `update_local` function is called to update the local variable to the wrapped function. If the function name is found in the global dictionary and it does not match the wrapped function, the original function (an instance of `FunModule`) is saved in `_bundled_func`, and the global dictionary is updated to point to the wrapped function. - -- **Return Event**: When the event is 'return', the function checks if the function name exists in the global dictionary. If it does, the global dictionary is restored to the original function saved in `_bundled_func`. - -The `update_local` function is used within the tracer to update the local variables in the frame. This ensures that recursive calls invoke the unwrapped function, maintaining the correct function behavior. - -**Note**: Points to note about the use of the code -- Ensure that the frame object passed to the tracer function is valid and corresponds to the correct execution context. -- Be cautious when modifying local and global variables in a frame, as it can affect the execution flow and state of the program. -- The tracer function relies on the `update_local` function to update local variables, which uses the `ctypes` module to interact with the Python C API. This may have implications for portability and compatibility across different Python versions and implementations. - -**Output Example**: The tracer function returns itself, allowing it to be used as a callback for tracing events. -*** -#### FunctionDef create_node(n) -**create_node**: The function of create_node is to convert an input into a Node object, specifically handling instances of FunModule by extracting their parameters if they exist. - -**parameters**: The parameters of this Function. -· n: The input to be converted into a Node. This can be an instance of FunModule or any other type that the node function can handle. - -**Code Description**: The create_node function is designed to facilitate the creation of Node objects from various inputs. It first checks if the input n is an instance of FunModule and whether it has a non-None parameter attribute. If both conditions are met, it assigns n to its parameter attribute. This step ensures that if n is a FunModule with a parameter, the parameter is used for the Node creation instead of the FunModule itself. After this check, the function calls the node function with n as its argument. The node function then processes n according to its own logic, which includes handling whether n is already a Node, and whether it should be trainable or have constraints. - -**Note**: -- This function is particularly useful when dealing with FunModule instances, as it ensures that their parameters are used for Node creation. -- The function relies on the node function to handle the actual creation of the Node object, including any additional parameters like name, trainable, and constraint. - -**Output Example**: A possible return value of the create_node function could be a Node object created from the parameter of a FunModule instance, or directly from the input if it is not a FunModule. For example, if n is a FunModule with a parameter, the return value would be a Node object created from that parameter. If n is a simple message, the return value would be a Node object created from that message. -*** -*** -### FunctionDef wrap(self, output, inputs, external_dependencies) -**wrap**: The function of wrap is to wrap the output as a MessageNode of inputs as the parents. - -**parameters**: -- output: The output of the operator function. -- inputs: The input nodes of the MessageNode. It can be a list or a dictionary. -- external_dependencies: A list of nodes that are used to create the outputs but not included in the inputs. - -**Code Description**: -The `wrap` function is a method of the `FunModule` class in the `bundle.py` file. It is designed to wrap the output of the operator function as a `MessageNode` with the specified inputs as its parents. The function takes three parameters: `output`, `inputs`, and `external_dependencies`. - -The `wrap` function first checks if the `wrap_output` attribute of the `FunModule` instance is `False`. If it is `False`, the function returns the output as is, assuming it is already a `Node` object. This is because there is no need to wrap the output if it is already a `Node`. - -If the `wrap_output` attribute is `True`, the function proceeds to check if the `parameter` attribute of the `FunModule` instance is not `None`. If it is not `None`, it means that the operator is a trainable operation and a new op eval needs to be created. In this case, the `inputs` dictionary is updated with the `__code` parameter, which is the code block of the function. The `description` and `name` variables are set accordingly to indicate that this is an eval operator. The `fun_name` attribute of the `FunModule` instance is also updated to "eval". - -If the `parameter` attribute is `None`, the `description` and `name` variables are set to the `description` and `name` attributes of the `FunModule` instance, respectively. - -Next, the function checks if the `output` is `None`. If it is `None`, it creates a `MessageNode` with `None` as the value and the specified `description`, `inputs`, `name`, and `info` attributes. This is useful when the operator does not produce any output. - -If the `output` is an instance of `Exception`, it creates an `ExceptionNode` with the `output` as the value and the specified `description`, `inputs`, `name`, and `info` attributes. The `ExceptionNode` represents an exception raised by the operator. - -If the `output` is neither `None` nor an instance of `Exception`, it creates a copy of the `info` attribute and updates it with the `output` value. It then creates a `MessageNode` with the `output` as the value and the specified `description`, `inputs`, `name`, and updated `info` attributes. - -The `wrap` function returns the created `MessageNode` or `ExceptionNode` depending on the type of the `output`. - -**Note**: -- The `wrap` function is used to wrap the output of the operator function as a `MessageNode` or `ExceptionNode`. -- The `wrap_output` attribute of the `FunModule` instance determines whether the output needs to be wrapped. -- The `parameter` attribute of the `FunModule` instance determines whether the operator is a trainable operation. -- The `description`, `name`, and `info` attributes of the `FunModule` instance are used to provide additional information for the created nodes. - -**Output Example**: -If the `output` is `None`, the function returns a `MessageNode` with `None` as the value: -``` -MessageNode(None, description="[Node] This is a node in a computational graph.", inputs=inputs, name=name, info=info) -``` -If the `output` is an exception, the function raises an `ExecutionError` with an `ExceptionNode` containing the exception details. -*** -### FunctionDef is_valid_output(output) -**is_valid_output**: The function of is_valid_output is to check whether the given output is a valid output for a computational graph node. - -**parameters**: -- output: The output to be checked. - -**Code Description**: -The `is_valid_output` function takes an `output` as input and checks whether it is a valid output for a computational graph node. The function returns `True` if the `output` is an instance of the `Node` class or if it is a tuple containing only instances of the `Node` class. Otherwise, it returns `False`. - -The function first checks if the `output` is an instance of the `Node` class using the `isinstance` function. If it is, the function returns `True`. - -If the `output` is not an instance of the `Node` class, the function checks if it is a tuple using the `isinstance` function. If it is a tuple, the function uses a list comprehension and the `isinstance` function to check if all elements in the tuple are instances of the `Node` class. If all elements are instances of the `Node` class, the function returns `True`. Otherwise, it returns `False`. - -**Note**: -- The `is_valid_output` function is used to validate the output of a computational graph node. It ensures that the output is compatible with the expected input types for further computations. -- The function assumes that the `Node` class is defined and imported correctly. - -**Output Example**: -- Example 1: - ```python - output = Node(5) - print(is_valid_output(output)) - ``` - Output: - ``` - True - ``` - -- Example 2: - ```python - output = (Node(1), Node(2), Node(3)) - print(is_valid_output(output)) - ``` - Output: - ``` - True - ``` - -- Example 3: - ```python - output = (Node(1), 2, Node(3)) - print(is_valid_output(output)) - ``` - Output: - ``` - False - ``` -*** -### FunctionDef __get__(self, obj, objtype) -**__get__**: The function of __get__ is to support instance methods by binding the __call__ method to an instance of the Module class. - -**parameters**: The parameters of this Function. -· self: Refers to the instance of the FunModule class. -· obj: The instance of the class where the FunModule instance is accessed as an attribute. -· objtype: The type of the class where the FunModule instance is accessed as an attribute. - -**Code Description**: The __get__ method is a descriptor method used to support instance methods in the FunModule class. When an instance of FunModule is accessed as an attribute of another class instance, the __get__ method is invoked. This method uses functools.partial to bind the __call__ method of the FunModule instance to the obj parameter, which is the instance of the class where FunModule is accessed. - -By doing this, the __call__ method of the FunModule instance is effectively converted into an instance method of the obj instance. This allows the __call__ method to be invoked with obj as its first argument, enabling it to operate in the context of the obj instance. - -In the context of the project, the __call__ method of the FunModule class is designed to invoke the forward method of the Module class with the provided arguments. The __get__ method ensures that when the __call__ method is accessed through an instance of another class, it behaves as an instance method, maintaining the correct binding to the obj instance. - -**Note**: -- The __get__ method is crucial for enabling the FunModule class to be used as a descriptor, allowing its __call__ method to be bound to instances of other classes. -- Ensure that the obj parameter is an instance of a class that correctly utilizes the FunModule instance as an attribute. - -**Output Example**: The return value of the __get__ method is a functools.partial object that binds the __call__ method to the obj instance. This allows the __call__ method to be invoked as if it were an instance method of the obj instance. For example, if obj is an instance of a class that has a FunModule instance as an attribute, accessing this attribute and calling it will invoke the __call__ method with obj as its first argument. -*** -## FunctionDef to_data(obj) -**to_data**: The function of to_data is to extract the data from a node or a container of nodes. - -**parameters**: -- obj: The input object, which can be a node or a container of nodes. - -**Code Description**: -The to_data function is designed to extract the data from a node or a container of nodes. It takes an input object and recursively extracts the data from each node in the object. The function handles different types of objects and performs specific operations based on their type. - -For node containers (tuple, list, dict, set, NodeContainer), the function recursively extracts the data from each node in the container. It uses list comprehension or dictionary comprehension to iterate over the nodes and call the to_data function recursively on each node. - -For individual nodes (instances of the Node class), the function simply returns the data attribute of the node. - -If the input object is an instance of NodeContainer, the function creates a copy of the object and iterates over its attributes using the __dict__ attribute. It then sets the corresponding attribute in the output object to the result of calling the to_data function recursively on the attribute value. - -If the input object is not a node or a node container, the function simply returns the object as is. - -**Note**: -- The function relies on the isinstance() function to determine the type of the input object and perform the appropriate operations. -- The function uses the copy module to create a copy of the NodeContainer object. -- The function assumes that the Node and NodeContainer classes are defined and imported correctly. - -**Output Example**: -- Input: Node(5) - Output: 5 - -- Input: [Node(1), Node(2), Node(3)] - Output: [1, 2, 3] - -- Input: {Node(1): Node(2), Node(3): Node(4)} - Output: {1: 2, 3: 4} -## FunctionDef update_local(frame, name, value) -**update_local**: The function of update_local is to update the value of a local variable in a given frame. - -**parameters**: The parameters of this Function. -· frame: The frame object where the local variable resides. -· name: The name of the local variable to be updated. -· value: The new value to be assigned to the local variable. - -**Code Description**: The update_local function is designed to modify the value of a local variable within a specific frame. It takes three parameters: the frame object, the name of the local variable, and the new value to be assigned to that variable. The function first updates the local variable in the frame's f_locals dictionary. Then, it calls the PyFrame_LocalsToFast function from the ctypes.pythonapi module to ensure that the changes are reflected in the frame's fast locals array, which is used by the Python interpreter for efficient variable access. - -In the context of its usage within the project, update_local is called by the tracer function in the FunModule class's forward method. The tracer function is responsible for modifying the local and global dictionaries of a frame to handle recursive calls of a wrapped function. Specifically, update_local is used to replace the current function in the frame's local variables with the original function when a recursive call is detected. This ensures that the recursive call invokes the unwrapped function rather than the bundled function, maintaining the correct function behavior. - -**Note**: Points to note about the use of the code -- Ensure that the frame object passed to update_local is valid and corresponds to the correct execution context. -- Be cautious when modifying local variables in a frame, as it can affect the execution flow and state of the program. -- The ctypes module is used to interact with the Python C API, which may have implications for portability and compatibility across different Python versions and implementations. -## FunctionDef test(x) -**test**: The function of test is to concatenate the string " world" to the data attribute of the input object. - -**parameters**: The parameters of this Function. -· x: An object that must have a data attribute containing a string. - -**Code Description**: The test function takes a single parameter, x, which is expected to be an object with a data attribute. The function accesses the data attribute of the input object and concatenates the string " world" to it. The result of this concatenation is then returned as the output of the function. - -**Note**: -- Ensure that the input object x has a data attribute that is a string; otherwise, the function will raise an AttributeError or TypeError. -- This function does not perform any type checking or error handling, so it is crucial to pass an appropriate object to avoid runtime errors. - -**Output Example**: -If the input object x has a data attribute with the value "Hello", the function will return "Hello world". diff --git a/generated_docs/opto/trace/containers.md b/generated_docs/opto/trace/containers.md deleted file mode 100644 index 2a7cae0b..00000000 --- a/generated_docs/opto/trace/containers.md +++ /dev/null @@ -1,386 +0,0 @@ -## ClassDef SeqIterable -**SeqIterable**: The function of SeqIterable is to provide an iterable interface for a wrapped list-like object, allowing it to be iterated over in a sequential manner. - -**attributes**: The attributes of this Class. -· _index: An integer that keeps track of the current position in the iteration. -· wrapped_list: The list-like object that is being wrapped and iterated over. - -**Code Description**: The SeqIterable class is designed to wrap a list-like object and provide an iterator interface for it. This allows the wrapped object to be iterated over using Python's iterator protocol. - -- The `__init__` method initializes the SeqIterable object with a wrapped list-like object and sets the initial index to 0. -- The `__iter__` method resets the index to 0 and returns the SeqIterable object itself as an iterator. -- The `__next__` method retrieves the next item from the wrapped list. If the end of the list is reached, it raises a StopIteration exception to signal the end of the iteration. Each item retrieved is wrapped in a node object, and if the wrapped list is not already a parent of the node, it is added as a parent. - -The SeqIterable class is utilized in the `iterate` function, which determines the appropriate iterable class to use based on the type of the input object. If the input is a list or tuple, it is wrapped in a SeqIterable object. If the input is a set, it is first converted to a list and then wrapped in a SeqIterable object. This ensures that various collection types can be iterated over in a consistent manner. - -**Note**: -- The wrapped list-like object must have a `data` attribute that is a list or tuple. -- The node function is used to wrap each item in the list, and it is assumed that this function and the Node class are defined elsewhere in the codebase. -- The wrapped list-like object must support being checked for membership in the parents attribute of a node. - -**Output Example**: -If the wrapped list contains the elements [1, 2, 3], iterating over the SeqIterable object would yield: -``` -node(1) -node(2) -node(3) -``` -Each element is wrapped in a node object before being returned. -### FunctionDef __init__(self, wrapped_list) -**__init__**: The function of __init__ is to initialize an instance of the SeqIterable class with a given list. - -**parameters**: The parameters of this Function. -· wrapped_list: A list that will be wrapped by the SeqIterable instance. - -**Code Description**: The __init__ method is a constructor that initializes an instance of the SeqIterable class. It takes one parameter, `wrapped_list`, which is expected to be a list. Inside the method, two instance variables are set: -- `self._index`: This is initialized to 0 and will likely be used to keep track of the current position in the iteration process. -- `self.wrapped_list`: This is assigned the value of the `wrapped_list` parameter, effectively storing the provided list within the instance for further operations. - -**Note**: Ensure that the `wrapped_list` parameter passed to the __init__ method is a list, as the class is designed to work with list-like structures. -*** -### FunctionDef __iter__(self) -**__iter__**: The function of __iter__ is to initialize the iteration process for the SeqIterable object and return the iterator itself. - -**parameters**: The parameters of this Function. -· This function does not take any parameters other than the implicit 'self' which refers to the instance of the SeqIterable class. - -**Code Description**: The __iter__ method is a special method in Python that is used to make an object iterable. When this method is called, it sets the internal index (_index) of the SeqIterable object to 0. This index is used to keep track of the current position during iteration. After initializing the index, the method returns the instance of the SeqIterable object itself, which will be used as the iterator. This allows the object to be used in iteration contexts such as loops. - -**Note**: -- Ensure that the SeqIterable class has a properly defined __next__ method to work in conjunction with __iter__ for full iterator functionality. -- The __iter__ method should be called before starting the iteration process to reset the index. - -**Output Example**: -When the __iter__ method is called on an instance of SeqIterable, it returns the instance itself. For example: - -```python -seq_iterable = SeqIterable() -iterator = iter(seq_iterable) -print(iterator is seq_iterable) # Output: True -``` - -In this example, calling iter(seq_iterable) invokes the __iter__ method, which returns the seq_iterable instance itself, confirming that the object is ready for iteration. -*** -### FunctionDef __next__(self) -**__next__**: The function of __next__ is to iterate over the wrapped list of nodes and return the next node in the sequence. - -**parameters**: -- self: Refers to the instance of the SeqIterable class that contains this method. - -**Code Description**: -The __next__ function is an implementation of the iterator protocol for the SeqIterable class. It allows users to iterate over the wrapped list of nodes and retrieve the next node in the sequence. - -The function first checks if the current index (_index) is less than the length of the wrapped list of nodes. If it is, it retrieves the node at the current index using the wrapped_list attribute and assigns it to the result variable. It then increments the index by 1 to prepare for the next iteration. - -Next, the function creates a node object from the result using the node function from opto.trace.nodes. This step ensures that the result is always a valid node object, even if it was already a node or a different type of object. - -After creating the node object, the function checks if the wrapped_list is not already a parent of the result node. If it is not, it adds the wrapped_list as a parent of the result node using the _add_parent method from opto.trace.nodes. This step ensures that the hierarchical structure of the graph is maintained correctly. - -Finally, if the current index is equal to or greater than the length of the wrapped list, the function raises a StopIteration exception. This signals the end of the iteration and is the expected behavior for iterators. - -The __next__ function is typically used in a loop or with the next() function to iterate over the nodes in a SeqIterable object. For example: - -```python -seq_iterable = SeqIterable(wrapped_list) -for node in seq_iterable: - # Do something with each node -``` - -**Note**: -- The __next__ function is part of the iterator protocol and is automatically called when iterating over a SeqIterable object. -- The wrapped_list attribute should be a list-like object that supports indexing and has a length. -- The function relies on the node function from opto.trace.nodes to create node objects from the elements of the wrapped list. -- The _add_parent method from opto.trace.nodes is used to maintain the hierarchical structure of the graph. -- The function raises a StopIteration exception when there are no more nodes to iterate over. - -**Output Example**: A possible return value of the __next__ function could be a node object representing the next node in the sequence. -*** -## FunctionDef to_list_implicit(x) -**to_list_implicit**: The function of to_list_implicit is to convert any given iterable into a list. - -**parameters**: The parameters of this Function. -· x: An iterable object of any type (e.g., set, tuple, etc.) - -**Code Description**: The to_list_implicit function takes a single parameter, x, which is expected to be an iterable. The function converts this iterable into a list using Python's built-in list() constructor and returns the resulting list. This conversion is implicit, meaning it does not check the type of the input explicitly but relies on the list() constructor to handle the conversion. - -In the context of its usage within the project, to_list_implicit is called by the iterate function. The iterate function is designed to handle various types of data structures, including Node objects, lists, tuples, sets, and dictionaries. When iterate encounters a set, it uses to_list_implicit to convert the set into a list. This conversion is necessary because the subsequent processing within iterate, specifically the creation of a SeqIterable object, requires a list rather than a set. - -**Note**: -- The input to to_list_implicit must be an iterable; otherwise, the list() constructor will raise a TypeError. -- This function does not perform any type checking or validation on the input. - -**Output Example**: -If the input is a set {1, 2, 3}, the function will return [1, 2, 3]. -If the input is a tuple (4, 5, 6), the function will return [4, 5, 6]. -## FunctionDef iterate(x) -**iterate**: The function of iterate is to provide an iterable interface for different types of objects, allowing them to be iterated over in a consistent manner. - -**parameters**: -- x: The input object to be iterated over. - -**Code Description**: The iterate function is designed to handle various types of objects and determine the appropriate iterable class to use based on the type of the input object. It follows a series of conditional statements to check the type of the input object and returns the corresponding iterable object. - -- If the input object is a subclass of the Node class, it checks the type of the data attribute of the object. If the data attribute is a list or tuple, it creates a SeqIterable object and returns it. If the data attribute is a set, it converts the set to a list using the to_list_implicit function and then creates a SeqIterable object with the converted list. If the data attribute is a dictionary, it creates a DictIterable object and returns it. If the data attribute is of any other type, it raises an exception indicating that the object cannot be iterated over. - -- If the input object is a list or tuple, it creates a SeqIterable object with the input object and returns it. - -- If the input object is a set, it converts the set to a list using the to_list_implicit function and then creates a SeqIterable object with the converted list. - -- If the input object is a dictionary, it creates a DictIterable object with the input object and returns it. - -- If the input object is of any other type, it raises an exception indicating that the object cannot be iterated over. - -The iterate function utilizes the SeqIterable and DictIterable classes defined in the code to provide the iterable interface for different types of objects. It ensures that objects of various collection types can be iterated over in a consistent manner. - -**Note**: -- The input object must have a data attribute that is a list, tuple, set, or dictionary. -- The to_list_implicit function is used to convert a set to a list. -- The node function is used to wrap each item in the list or dictionary with a node object. -- The Node class is assumed to be defined elsewhere in the codebase. - -**Output Example**: -If the input object is a list [1, 2, 3], iterating over the returned SeqIterable object would yield: -``` -node(1) -node(2) -node(3) -``` -If the input object is a dictionary {'a': 1, 'b': 2}, iterating over the returned DictIterable object would yield: -``` -(node('a'), 1) -(node('b'), 2) -``` -## ClassDef DictIterable -**DictIterable**: The function of DictIterable is to provide an iterable interface for dictionary-like objects, allowing iteration over key-value pairs. - -**attributes**: The attributes of this Class. -· _index: An integer that keeps track of the current position in the iteration. -· wrapped_dict: The dictionary-like object that is being wrapped and iterated over. -· keys: A list of keys from the wrapped_dict, used to facilitate iteration. - -**Code Description**: The DictIterable class is designed to enable iteration over dictionary-like objects. When an instance of DictIterable is created, it takes a dictionary-like object (wrapped_dict) as an argument. The constructor initializes the _index attribute to 0, stores the wrapped_dict, and extracts the keys from the wrapped_dict's data attribute, storing them in the keys attribute. - -The __iter__ method resets the _index to 0 and returns the instance itself, making the object an iterator. - -The __next__ method is responsible for returning the next item in the iteration. It checks if the current _index is less than the length of the keys list. If so, it retrieves the key at the current index, constructs a tuple containing a node object created from the key and the corresponding value from the wrapped_dict, and increments the _index. Before returning the tuple, it adds the wrapped_dict as a parent to both the key and value nodes. If the _index exceeds the length of the keys list, a StopIteration exception is raised to signal the end of the iteration. - -The DictIterable class is utilized in the iterate and items functions. The iterate function determines the type of the input object and returns an appropriate iterable object. If the input is a dictionary or a dictionary-like object, iterate returns an instance of DictIterable. Similarly, the items function checks if the input object's data attribute is a dictionary and returns a DictIterable instance if true. - -**Note**: -- The wrapped_dict parameter must be a dictionary-like object with a data attribute that is a dictionary. -- The node function and the _add_parent method must be defined elsewhere in the codebase for DictIterable to function correctly. - -**Output Example**: -Assuming the wrapped_dict contains {'a': 1, 'b': 2}, iterating over an instance of DictIterable would yield: -(node('a'), 1) -(node('b'), 2) -### FunctionDef __init__(self, wrapped_dict) -**__init__**: The function of __init__ is to initialize an instance of the DictIterable class with a given dictionary. - -**parameters**: The parameters of this Function. -· wrapped_dict: A dictionary-like object that contains the data to be wrapped by the DictIterable instance. - -**Code Description**: The __init__ method initializes an instance of the DictIterable class. It takes one parameter, `wrapped_dict`, which is expected to be a dictionary-like object. Inside the method, the instance variable `_index` is initialized to 0, which will likely be used to keep track of the current position during iteration. The `wrapped_dict` parameter is assigned to the instance variable `wrapped_dict`, allowing the instance to store and access the provided dictionary. Additionally, the keys of the dictionary are extracted and converted into a list, which is then assigned to the instance variable `keys`. This list of keys will be used for iterating over the dictionary. - -**Note**: Ensure that the `wrapped_dict` parameter passed to the __init__ method is a dictionary-like object with a `data` attribute that contains the actual dictionary. This is crucial for the proper functioning of the DictIterable class. -*** -### FunctionDef __iter__(self) -**__iter__**: The function of __iter__ is to initialize the iteration process for the DictIterable object. - -**parameters**: This function does not take any parameters. - -**Code Description**: The __iter__ method is a special method in Python that is used to make an object iterable. When this method is called, it sets the internal index `_index` of the object to 0. This index is likely used to keep track of the current position during iteration. After initializing the index, the method returns the object itself (`self`). This allows the object to be used in iteration contexts, such as in a for loop. By implementing the __iter__ method, the DictIterable object conforms to the iterator protocol, which requires an __iter__ method that returns the iterator object itself. - -**Note**: -- Ensure that the DictIterable class has a corresponding __next__ method to complete the iterator protocol. The __next__ method should define how the iteration progresses and when it stops. -- The __iter__ method should not modify the underlying data structure of the object; it should only initialize the state required for iteration. - -**Output Example**: -When the __iter__ method is called on a DictIterable object, it does not produce a direct output but prepares the object for iteration. For example: - -```python -dict_iterable = DictIterable() -iterator = iter(dict_iterable) -``` - -In this example, `iterator` is the same as `dict_iterable`, now ready to be used in a loop or any other iteration context. -*** -### FunctionDef __next__(self) -**__next__**: The function of __next__ is to iterate over the items in the wrapped dictionary, returning each key-value pair as a tuple of Node objects. - -**parameters**: The parameters of this Function. -- This function does not take any parameters. - -**Code Description**: The __next__ method is designed to facilitate iteration over a dictionary wrapped within the DictIterable object. It maintains an internal index (_index) to keep track of the current position in the iteration. The method first checks if the current index is less than the length of the keys in the dictionary. If so, it retrieves the key at the current index and constructs a tuple (result) consisting of two elements: -1. A Node object created from the key. -2. A Node object created from the corresponding value in the wrapped dictionary. - -Both elements of the tuple are created using the node function, which ensures that they are properly instantiated as Node objects. After creating the tuple, the method increments the internal index (_index) by one to move to the next item in the subsequent call. - -Additionally, the method calls the _add_parent method on both elements of the tuple, passing the wrapped dictionary as the parent. This establishes a parent-child relationship between the nodes and the dictionary, which can be useful for tracking dependencies or maintaining hierarchical structures. - -If the current index is equal to or greater than the length of the keys, the method raises a StopIteration exception, signaling that the iteration is complete. - -**Note**: -- The __next__ method is intended to be used in conjunction with an iterator protocol, typically within a for loop or similar construct. -- The method relies on the node function to create Node objects, ensuring consistency and proper initialization. -- The _add_parent method is called on both the key and value nodes to establish a parent-child relationship with the wrapped dictionary. - -**Output Example**: A possible return value of the __next__ method could be: -``` -(node('some_key'), node('some_value')) -``` -where 'some_key' and 'some_value' are entries in the wrapped dictionary, and both are converted to Node objects. -*** -## FunctionDef items(x) -**items**: The function of items is to return an iterable interface for dictionary-like objects, allowing iteration over key-value pairs if the input object's data attribute is a dictionary. - -**parameters**: The parameters of this Function. -· x: An object that is expected to have a data attribute. - -**Code Description**: The items function is designed to facilitate iteration over the key-value pairs of an object's data attribute, provided that this attribute is a dictionary. The function first checks if the data attribute of the input object x is of type dict. If it is not, the function returns an AttributeError, indicating that items cannot be retrieved from the given type. If the data attribute is indeed a dictionary, the function returns an instance of DictIterable, which is a class designed to enable iteration over dictionary-like objects. - -The DictIterable class, when instantiated, takes the dictionary-like object (wrapped_dict) and provides an iterable interface. It initializes an index to keep track of the current position in the iteration and extracts the keys from the wrapped_dict's data attribute. The __iter__ method resets the index and returns the instance itself, making it an iterator. The __next__ method retrieves the next item in the iteration, constructs a tuple containing a node object created from the key and the corresponding value from the wrapped_dict, and increments the index. If the index exceeds the length of the keys list, a StopIteration exception is raised to signal the end of the iteration. - -**Note**: -- The input object x must have a data attribute that is a dictionary for the function to work correctly. -- The node function and the _add_parent method must be defined elsewhere in the codebase for DictIterable to function correctly. - -**Output Example**: -Assuming the input object's data attribute contains {'a': 1, 'b': 2}, calling the items function would yield: -(node('a'), 1) -(node('b'), 2) -## ClassDef Seq -**Seq**: The function of Seq is to represent a sequence with a defined length and index, converting Python's list or tuple into a Seq object. - -**attributes**: The attributes of this Class. -· data: Inherited from UserList, it stores the sequence data. - -**Code Description**: The Seq class is a specialized container that inherits from both UserList and ParameterContainer. It is designed to handle sequences, converting Python lists or tuples into Seq objects. The class provides a method to retrieve a dictionary of parameters contained within the sequence. - -The `__init__` method initializes the Seq object. It accepts a variable number of arguments (`*args`). If a single argument is passed and it has both `__len__` and `__getitem__` attributes (indicating it is a sequence), it is used directly as the sequence. Otherwise, the arguments are treated as individual elements of the sequence. The superclass initializer is then called with the sequence. - -The `parameters_dict` method returns a dictionary of all parameters in the model, including both trainable and non-trainable parameters. It iterates over the elements in the sequence (`self.data`). If an element is an instance of ParameterNode, it adds it to the dictionary with its name as the key. If an element is an instance of ParameterContainer, it adds it to the dictionary with the string representation of the container as the key. The method ensures that all values in the dictionary are instances of either ParameterNode or ParameterContainer. - -The Seq class leverages the functionality of the ParameterContainer class, which serves as a container for parameter nodes. The ParameterContainer class provides methods to retrieve a flattened list of parameters and a dictionary of all parameters in the model. The Seq class uses the `parameters_dict` method to gather parameters from its elements, ensuring they are correctly identified and stored. - -**Note**: -- The Seq class is designed to work seamlessly with Python's list and tuple types, converting them into Seq objects. -- When using the Seq class, ensure that the elements within the sequence are either ParameterNode or ParameterContainer instances to maintain the integrity of the `parameters_dict` method. - -**Output Example**: -```python -{ - 'param1': , - 'param2': , - 'container1': , - 'container2': -} -``` -### FunctionDef __init__(self) -**__init__**: The function of __init__ is to initialize an instance of the Seq class. - -**parameters**: The parameters of this Function. -· *args: A variable-length argument list that can contain one or more elements. - -**Code Description**: The __init__ method is designed to initialize an instance of the Seq class. It first checks if there is exactly one argument passed and if this argument has both the `__len__` and `__getitem__` attributes, which are typical of sequence-like objects (e.g., lists, tuples). If these conditions are met, the single argument is treated as a sequence and assigned to the variable `seq`. If the conditions are not met, all arguments are treated as individual elements and are collectively assigned to `seq` as a tuple. Finally, the method calls the `__init__` method of the superclass with `initlist=seq`, passing the sequence or tuple to the superclass for further initialization. - -**Note**: -- Ensure that if a single argument is passed, it should be a sequence-like object (having `__len__` and `__getitem__` attributes) to be treated as such. -- If multiple arguments are passed, they will be treated as individual elements and combined into a tuple. -- This method leverages the flexibility of accepting both single sequence-like objects and multiple individual elements, making it versatile for different initialization scenarios. -*** -### FunctionDef parameters_dict(self) -**parameters_dict**: The function of parameters_dict is to return a dictionary of all the parameters in the model, including both trainable and non-trainable parameters. - -**parameters**: -- No parameters are defined within the provided code snippet. - -**Code Description**: -The `parameters_dict` method is used to retrieve a dictionary of all the parameters in the model, including both trainable and non-trainable parameters. It iterates over the items in the `self.data` attribute, which is assumed to be a dictionary-like object. For each item, it checks if the value is an instance of `ParameterNode`. If it is, it adds the value to the `parameters` dictionary with the attribute name as the key. If the value is an instance of `ParameterContainer`, it adds the value to the `parameters` dictionary with the attribute name as the key. - -The `parameters_dict` method ensures that all the values in the `parameters` dictionary are instances of `ParameterNode` or `ParameterContainer` by asserting that the `isinstance` condition holds true for all values. - -The `parameters_dict` method is called internally by the `parameters` method to retrieve the parameters dictionary. - -**Note**: -- The `parameters_dict` method assumes that the `self.data` attribute is a dictionary-like object containing the parameters. -- The `parameters_dict` method does not specify the name of the container when adding a `ParameterContainer` to the `parameters` dictionary. This could be a potential improvement to consider. - -**Output Example**: -```python -{ - 'param1': , - 'param2': , - 'container1': , - 'container2': -} -``` -*** -## ClassDef Map -**Map**: The function of Map is to serve as a specialized container that maps keys to values, converting Python's standard dictionary into a Map object. - -**attributes**: The attributes of this Class. -· No specific attributes are defined within the provided code snippet. - -**Code Description**: -The `Map` class is a specialized container that inherits from both `UserDict` and `ParameterContainer`. It is designed to map keys to values, similar to a Python dictionary, but with additional functionality specific to handling parameters in a model. - -- **Initialization**: The `__init__` method initializes the `Map` object by calling the constructor of its parent classes with the provided `mapping`. This ensures that the `Map` object is initialized with the given key-value pairs. - -- **parameters_dict Method**: The `parameters_dict` method returns a dictionary of all the parameters in the model, including both trainable and non-trainable parameters. The dictionary contains `ParameterNode` or `ParameterContainer` objects. The method iterates over the items in the `data` attribute (inherited from `UserDict`), checking the type of each key and value: - - If the value is an instance of `ParameterNode`, it is added to the `parameters` dictionary. - - If the value is an instance of `ParameterContainer`, it is also added to the `parameters` dictionary, but the key is converted to a string representation. - - If the key is an instance of `ParameterNode`, it is added to the `parameters` dictionary with its string representation as the key. - - If the key is an instance of `ParameterContainer`, an exception is raised because a `Map` cannot have a container as a key. - -The method asserts that all values in the `parameters` dictionary are instances of either `ParameterNode` or `ParameterContainer` before returning the dictionary. - -**Note**: -- The `Map` class ensures that all keys and values adhere to specific types (`ParameterNode` or `ParameterContainer`), maintaining the integrity of the parameter mapping. -- The `parameters_dict` method is crucial for retrieving a structured dictionary of parameters, which is essential for model optimization and parameter management. -- The `Map` class cannot have a `ParameterContainer` as a key, which is enforced by raising an exception. - -**Output Example**: -```python -{ - 'param1': , - 'param2': , - 'container1': -} -``` -### FunctionDef __init__(self, mapping) -**__init__**: The function of __init__ is to initialize an instance of the Map class with a given mapping. - -**parameters**: The parameters of this Function. -· mapping: A dictionary or any other mapping object that will be used to initialize the Map instance. - -**Code Description**: The __init__ method is a constructor for the Map class. It takes a single parameter, `mapping`, which is expected to be a dictionary or another type of mapping object. The method then calls the `__init__` method of its superclass with the provided `mapping` as an argument. This ensures that the Map instance is properly initialized with the given mapping data. The use of `super().__init__(mapping)` indicates that the Map class is likely inheriting from a parent class that requires initialization with a mapping object. - -**Note**: Ensure that the `mapping` parameter passed to the __init__ method is a valid mapping object, such as a dictionary, to avoid any initialization errors. -*** -### FunctionDef parameters_dict(self) -**parameters_dict**: The function of parameters_dict is to return a dictionary of all the parameters in the model, including both trainable and non-trainable parameters. - -**parameters**: -- self: The current object. - -**Code Description**: -The `parameters_dict` method is used to retrieve a dictionary of all the parameters in the model, including both trainable and non-trainable parameters. It iterates over the items in the `data` attribute of the current object and checks the type of each value. If the value is an instance of `ParameterNode`, it adds it to the `parameters` dictionary with the key as the corresponding key in the `data` attribute. If the value is an instance of `ParameterContainer`, it adds it to the `parameters` dictionary with the key as the string representation of the container. - -Additionally, the method checks the type of each key in the `data` attribute. If the key is an instance of `ParameterNode`, it adds it to the `parameters` dictionary with the key as the string representation of the node. If the key is an instance of `ParameterContainer`, it raises an exception since the key of a Map cannot be a container. - -Finally, the method asserts that all the values in the `parameters` dictionary are instances of `ParameterNode` or `ParameterContainer` and returns the `parameters` dictionary. - -**Note**: -- The `parameters_dict` method is called internally by the `parameters` method to retrieve the parameters dictionary. -- The `parameters_dict` method includes both trainable and non-trainable parameters in the returned dictionary. - -**Output Example**: -{ - 'param1': , - 'param2': , - 'container1': , - 'container2': -} -*** diff --git a/generated_docs/opto/trace/errors.md b/generated_docs/opto/trace/errors.md deleted file mode 100644 index d2f7ed34..00000000 --- a/generated_docs/opto/trace/errors.md +++ /dev/null @@ -1,112 +0,0 @@ -## ClassDef ExecutionError -**ExecutionError**: The function of ExecutionError is to serve as a base class for handling execution errors in code tracing. - -**attributes**: The attributes of this Class. -· exception_node: An instance of ExceptionNode that contains details about the exception. - -**Code Description**: The ExecutionError class is designed to encapsulate errors that occur during the execution of code within a tracing context. It inherits from the built-in Exception class, providing additional context through the exception_node attribute. - -- The `__init__` method initializes the ExecutionError instance with an ExceptionNode object, which contains detailed information about the exception, including the error message, inputs, and other metadata. The base Exception class is then initialized with the data from the exception_node. - -- The `__str__` method provides a string representation of the ExecutionError, which includes the data from the exception_node. This makes it easier to understand the nature of the error when it is printed or logged. - -In the project, ExecutionError is used in the following contexts: - -1. **opto\trace\bundle.py/FunModule/fun**: Within the `fun` method, ExecutionError is raised when there is a SyntaxError, NameError, KeyError, or OSError during the execution of dynamically generated code. The ExceptionNode is created with details about the error and passed to ExecutionError, which is then raised to signal the issue. - -2. **opto\trace\bundle.py/FunModule/wrap**: In the `wrap` method, ExecutionError is raised if the output of a function is an exception. An ExceptionNode is created with the exception details and passed to ExecutionError, which is then raised to indicate the error. - -3. **opto\trace\nodes.py/ExceptionNode/__init__**: The ExceptionNode class's `__init__` method checks if the value is an instance of ExecutionError. If not, it formats the exception message accordingly. This ensures that ExecutionError instances are handled correctly within the ExceptionNode. - -**Note**: When using ExecutionError, ensure that the exception_node provided contains all necessary information about the error, as this will be used to initialize the base Exception class and provide a meaningful error message. - -**Output Example**: -If an ExecutionError is raised due to a SyntaxError in the dynamically executed code, the string representation might look like: -``` -ExecutionError: (SyntaxError) invalid syntax (, line 1) -``` -This output indicates that a SyntaxError occurred, providing the specific error message and location. -### FunctionDef __init__(self, exception_node) -**__init__**: The function of __init__ is to initialize an instance of the ExecutionError class with a given ExceptionNode. - -**parameters**: The parameters of this Function. -· exception_node: An instance of ExceptionNode that contains the exception message and related data. - -**Code Description**: The __init__ method of the ExecutionError class is responsible for initializing an instance of the class. It takes one parameter, exception_node, which is an instance of ExceptionNode. This ExceptionNode contains the exception message and related data. - -Upon initialization, the method assigns the provided exception_node to the instance variable self.exception_node. It then calls the __init__ method of its superclass with the data retrieved from the exception_node. This is achieved by accessing the data attribute of the exception_node, which returns the internal data of the node. The superclass's __init__ method is thus provided with this data, ensuring that the ExecutionError instance is properly initialized with the relevant exception information. - -The relationship with its callees in the project is as follows: -- The data method of the ExceptionNode class is called to retrieve the internal data of the node. This data is then passed to the superclass's __init__ method to complete the initialization process. - -**Note**: It is important to ensure that the exception_node parameter is a valid instance of ExceptionNode, as the method relies on the data attribute of this object to function correctly. If the exception_node does not have the expected structure, the initialization process may fail. -*** -### FunctionDef __str__(self) -**__str__**: The function of __str__ is to provide a string representation of the ExecutionError object, specifically detailing the error message associated with the exception node. - -**parameters**: The parameters of this Function. -· self: Refers to the instance of the ExecutionError class. - -**Code Description**: The __str__ method is designed to return a formatted string that represents the ExecutionError instance. It accesses the `exception_node` attribute of the ExecutionError object and retrieves its data using the `data` method. The `data` method, defined in the AbstractNode class, returns the internal data of the node, which in this context is the error message or relevant data associated with the exception. The __str__ method then formats this data into a string prefixed with "ExecutionError: ", providing a clear and concise description of the error for debugging and logging purposes. - -**Note**: This method assumes that the `exception_node` attribute is properly initialized and contains a valid node object with accessible data. If the `exception_node` is not set or its data is not retrievable, this could lead to unexpected behavior or errors. - -**Output Example**: A possible return value of the __str__ method could be: -``` -ExecutionError: File not found -``` -This output indicates that the error message stored in the `exception_node` is "File not found". -*** -## ClassDef TraceMissingInputsError -**TraceMissingInputsError**: The TraceMissingInputsError class represents an exception that is raised when not all nodes used in the operator function are specified as inputs of the returned node. - -**Attributes**: -- message: A string representing the error message. - -**Code Description**: -The TraceMissingInputsError class is a subclass of the built-in Exception class. It is used to handle the case where not all nodes used in the operator function are specified as inputs of the returned node. - -The class has an `__init__` method that takes a `message` parameter and initializes the `message` attribute with the provided message. It also calls the `__init__` method of the parent Exception class with the message. - -The class also overrides the `__str__` method to return the error message when the exception is converted to a string. - -This exception is raised in the `forward` method of the `FunModule` class in the `opto.trace.bundle` module. The `forward` method is responsible for executing the operator function and handling any exceptions that occur during execution. If the `catch_execution_error` flag is set to `True`, the exception is caught and stored in the `outputs` variable. Otherwise, the exception is raised and propagated. - -**Note**: -- This exception is raised when not all nodes used in the operator function are specified as inputs of the returned node. -- The error message can be accessed through the `message` attribute of the exception object. - -**Output Example**: -``` -TraceMissingInputsError: Not all nodes used in the operator are specified as inputs of the returned node. Missing ['node_x'] -``` -### FunctionDef __init__(self, message) -**__init__**: The function of __init__ is to initialize an instance of the TraceMissingInputsError class with a specific error message. - -**parameters**: The parameters of this Function. -· message: A string that contains the error message to be associated with the TraceMissingInputsError instance. - -**Code Description**: The __init__ method is a constructor for the TraceMissingInputsError class. It takes a single parameter, `message`, which is a string representing the error message. Inside the method, the `message` parameter is assigned to the instance variable `self.message`. The constructor then calls the `__init__` method of its superclass using `super().__init__(self.message)`, passing the error message to the base class's constructor. This ensures that the error message is properly initialized and can be accessed through the standard exception handling mechanisms. - -**Note**: -- Ensure that the `message` parameter is a string to avoid type errors. -- This method is essential for setting up the error message that will be displayed when the TraceMissingInputsError is raised. -*** -### FunctionDef __str__(self) -**__str__**: The function of __str__ is to return the error message associated with the TraceMissingInputsError instance. - -**parameters**: The parameters of this Function. -· None: This method does not take any parameters. - -**Code Description**: The __str__ method in the TraceMissingInputsError class is designed to provide a human-readable representation of the error. When this method is called, it returns the value of the `message` attribute of the instance. This attribute typically contains a descriptive error message that explains the nature of the TraceMissingInputsError. The method ensures that when the error is printed or converted to a string, the message is displayed, making it easier for developers to understand the issue. - -**Note**: -- This method overrides the default __str__ method provided by Python's base Exception class. -- Ensure that the `message` attribute is properly set when initializing the TraceMissingInputsError instance to provide meaningful error information. - -**Output Example**: -If the `message` attribute of the TraceMissingInputsError instance is set to "Input data is missing", calling the __str__ method will return: -``` -"Input data is missing" -``` -*** diff --git a/generated_docs/opto/trace/modules.md b/generated_docs/opto/trace/modules.md deleted file mode 100644 index 6180a1b9..00000000 --- a/generated_docs/opto/trace/modules.md +++ /dev/null @@ -1,304 +0,0 @@ -## ClassDef NodeContainer -**NodeContainer**: The function of NodeContainer is to serve as an identifier for a container of nodes. - -**attributes**: The attributes of this Class. -· No specific attributes are defined within the provided code snippet. - -**Code Description**: The NodeContainer class is designed to act as a marker or identifier for objects that are containers of nodes. This class itself does not contain any specific attributes or methods, but it is used as a base class or type identifier in various parts of the project. - -In the project, NodeContainer is utilized in several contexts: - -1. **apply_op function in broadcast.py**: - - The apply_op function performs broadcasting operations on containers of nodes. It checks if the output is an instance of NodeContainer and recursively applies the operation to each attribute of the NodeContainer instance. This indicates that NodeContainer is used to group nodes together, allowing operations to be applied uniformly across all contained nodes. - -2. **to_data function in bundle.py**: - - The to_data function extracts data from nodes or containers of nodes. When the input object is an instance of NodeContainer, the function recursively extracts data from each attribute of the NodeContainer. This shows that NodeContainer is used to encapsulate nodes, enabling data extraction from complex structures. - -3. **ParameterContainer class in modules.py**: - - ParameterContainer inherits from NodeContainer and represents a container of parameter nodes. It includes methods to retrieve a flattened list of parameters and a dictionary of all parameters in the model. This inheritance indicates that ParameterContainer leverages the NodeContainer's role as a node container to manage parameter nodes specifically. - -4. **SubContainer and Container classes in test_apply_op.py**: - - Both SubContainer and Container classes inherit from NodeContainer. These classes initialize with various node attributes, demonstrating how NodeContainer can be extended to create more complex containers of nodes for testing purposes. - -**Note**: Points to note about the use of the code -- NodeContainer itself does not define any attributes or methods; it serves as a base class or type identifier. -- When extending NodeContainer, ensure that the derived classes properly encapsulate nodes to leverage the functionality provided by functions like apply_op and to_data. -- NodeContainer is integral to the project's handling of node containers, enabling consistent operations and data extraction across different types of node groupings. -## FunctionDef trainable_method(method) -**trainable_method**: The function of trainable_method is to determine if a given method is callable and has an attribute named "parameter". - -**parameters**: The parameters of this Function. -· method: The method to be checked for callability and the presence of the "parameter" attribute. - -**Code Description**: The trainable_method function is designed to check two specific conditions for a given method: -1. It verifies if the method is callable using the callable() function. -2. It checks if the method has an attribute named "parameter" using the hasattr() function. - -If both conditions are met, the function returns True; otherwise, it returns False. This function is particularly useful in scenarios where methods need to be filtered based on their trainability, which is indicated by the presence of the "parameter" attribute. - -In the context of its usage within the ParameterContainer class's parameters_dict method, trainable_method plays a crucial role. The parameters_dict method constructs a dictionary of all parameters in the model, including both trainable and non-trainable parameters. It iterates over the attributes of the ParameterContainer instance and uses trainable_method to identify methods that are both callable and have a "parameter" attribute. These methods are then included in the resulting dictionary with their "parameter" attribute values. - -**Note**: -- Ensure that the methods being checked are intended to have a "parameter" attribute if they are to be considered trainable. -- This function does not check the type or validity of the "parameter" attribute, only its presence. - -**Output Example**: -For a method that is callable and has a "parameter" attribute, trainable_method would return: -``` -True -``` -For a method that is either not callable or lacks a "parameter" attribute, trainable_method would return: -``` -False -``` -## ClassDef ParameterContainer -**ParameterContainer**: The function of ParameterContainer is to serve as a container for parameter nodes. - -**attributes**: -- No specific attributes are defined within the provided code snippet. - -**Code Description**: -The ParameterContainer class is a subclass of NodeContainer and represents a container of parameter nodes. It provides methods to retrieve a flattened list of parameters and a dictionary of all parameters in the model. - -The `parameters` method returns a flattened list of all the parameters in the model's `parameters_dict`. It iterates over the items in the `parameters_dict` and checks if each value is an instance of `ParameterNode` or `ParameterContainer`. If it is a `ParameterNode`, it appends it to the `parameters` list. If it is a `ParameterContainer`, it recursively calls the `parameters` method on the container and extends the `parameters` list with the result. If the value is neither a `ParameterNode` nor a `ParameterContainer`, it raises a `ValueError`. - -The `parameters_dict` method returns a dictionary of all the parameters in the model, including both trainable and non-trainable parameters. It uses the `inspect.getmembers` function to get all the attributes of the `self` object. It then iterates over these attributes and checks if each attribute is a `functools.partial` object or a method attribute. If it is a `functools.partial` object, it retrieves the method from the `func` attribute and checks if it is a trainable method using the `trainable_method` function. If it is a trainable method, it adds the method's `parameter` attribute to the `parameters` dictionary with the attribute name as the key. If it is a method attribute, it checks if it is a trainable method using the `trainable_method` function and adds the method's `parameter` attribute to the `parameters` dictionary with the attribute name as the key. If the attribute is a `ParameterNode`, it adds it to the `parameters` dictionary with the attribute name as the key. If the attribute is a `ParameterContainer`, it adds it to the `parameters` dictionary with the attribute name as the key. Finally, it asserts that all the values in the `parameters` dictionary are instances of `ParameterNode` or `ParameterContainer`. - -The `parameters_dict` method is used to retrieve a dictionary of all the parameters in the model, including both trainable and non-trainable parameters. It is called internally by the `parameters` method to retrieve the parameters dictionary. - -**Note**: -- The `ParameterContainer` class inherits from the `NodeContainer` class, which serves as an identifier for a container of nodes. -- The `ParameterContainer` class is designed to manage parameter nodes specifically, leveraging the functionality provided by the `NodeContainer` class. -- When using the `ParameterContainer` class, ensure that the derived classes properly encapsulate parameter nodes to ensure the correct functioning of the `parameters` and `parameters_dict` methods. - -**Output Example**: -```python -{ - 'param1': , - 'param2': , - 'container1': , - 'container2': -} -``` -### FunctionDef parameters(self) -**parameters**: The function of parameters is to return a flattened list of all the parameters in the model's parameters_dict, useful for optimization. - -**parameters**: The parameters of this function. -· self: The instance of the ParameterContainer class. - -**Code Description**: The parameters function is designed to collect and return a flattened list of all parameters contained within a model's parameters_dict. This is particularly useful for optimization tasks where a single list of parameters is required. - -1. The function initializes an empty list named parameters. -2. It then iterates over each key-value pair in the dictionary returned by the parameters_dict method of the ParameterContainer instance. -3. For each key-value pair: - - If the value is an instance of ParameterNode, it appends the value to the parameters list. - - If the value is an instance of ParameterContainer, it extends the parameters list with the result of calling the parameters method on that value. - - If the value is neither a ParameterNode nor a ParameterContainer, it raises a ValueError indicating that the model contains an unknown parameter type. -4. Finally, the function returns the populated parameters list. - -This method ensures that all parameters, whether they are directly part of the ParameterContainer or nested within other ParameterContainers, are included in a single, flattened list. - -**Note**: -- The function relies on the parameters_dict method to retrieve the dictionary of parameters. -- It assumes that all values in the parameters_dict are either instances of ParameterNode or ParameterContainer. Any other type will result in a ValueError. -- This function is essential for optimization processes that require a single list of all model parameters. - -**Output Example**: -A possible return value of the parameters function could be: -[ - , - , - ... -] -*** -### FunctionDef parameters_dict(self) -**parameters_dict**: The function of parameters_dict is to return a dictionary of all the parameters in the model, including both trainable and non-trainable parameters. - -**parameters**: -- self: The instance of the ParameterContainer class. - -**Code Description**: The parameters_dict method constructs a dictionary of all parameters in the model, including both trainable and non-trainable parameters. It iterates over the attributes of the ParameterContainer instance and checks each attribute using the trainable_method function. If the attribute is a class method and is trainable, it adds the method's "parameter" attribute to the dictionary. If the attribute is a method and is trainable, it adds the method's "parameter" attribute to the dictionary. If the attribute is a ParameterNode, it adds the ParameterNode object to the dictionary. If the attribute is a ParameterContainer, it adds the ParameterContainer object to the dictionary. - -The method then asserts that all values in the dictionary are either instances of ParameterNode or ParameterContainer. - -Finally, the method returns the constructed dictionary, which includes both trainable and non-trainable parameters. - -**Note**: -- The trainable_method function is used to determine if a given method is callable and has an attribute named "parameter". -- The method does not check the type or validity of the "parameter" attribute, only its presence. - -**Output Example**: -{ - 'param1': , - 'param2': , - ... -} -*** -## FunctionDef model(cls) -**model**: The function of model is to wrap a class with a decorator to help collect parameters for the optimizer. This decorated class cannot be pickled. - -**parameters**: The parameters of this Function. -· cls: The class to be wrapped by the decorator. - -**Code Description**: The `model` function is a decorator designed to wrap a given class, enhancing it to collect parameters for an optimizer. When a class is decorated with `model`, it is wrapped inside a new class called `ModelWrapper`, which inherits from both `Module` and the original class (`cls`). This allows the optimizer to access and manage the parameters of the class more effectively. However, it is important to note that classes decorated with `model` cannot be pickled, which may affect serialization and deserialization processes. - -The function is utilized in the project to facilitate the optimization process by ensuring that the parameters of the decorated class are properly managed. Although the specific usage within the project is not detailed in the provided documents, it is clear that the `model` function plays a crucial role in parameter management for optimization tasks. - -**Note**: -- Classes decorated with `model` cannot be pickled. -- Ensure that the class to be wrapped is compatible with the `Module` class. - -**Output Example**: -When a class `MyClass` is decorated with `model`, the resulting class `ModelWrapper` will inherit from both `Module` and `MyClass`, allowing the optimizer to collect and manage its parameters. The decorated class will look like this: - -```python -@model -class MyClass: - # class definition -``` - -This will result in a new class `ModelWrapper` that combines the functionalities of `Module` and `MyClass`. -### ClassDef ModelWrapper -**ModelWrapper**: The function of ModelWrapper is to serve as a specialized module that inherits functionalities from both the `Module` class and another class specified by `cls`. - -**attributes**: The attributes of this Class. -- No specific attributes are defined within the provided code snippet. - -**Code Description**: The `ModelWrapper` class is designed to extend the capabilities of the `Module` class by also inheriting from another class specified by `cls`. This dual inheritance allows `ModelWrapper` to combine the functionalities of both parent classes, making it a versatile component in the project. - -The `Module` class, from which `ModelWrapper` inherits, serves as a container for parameter nodes and provides essential methods such as `forward`, `__call__`, `save`, `load`, and `_set`. These methods facilitate the forward pass of the model, allow the module to be called as a function, and enable saving and loading of model parameters. - -By inheriting from `Module`, `ModelWrapper` gains access to these methods and functionalities. Additionally, the inheritance from `cls` allows `ModelWrapper` to incorporate any additional methods and attributes defined in `cls`, thereby enhancing its capabilities. - -**Note**: -- The `ModelWrapper` class does not define any new attributes or methods within the provided code snippet. It relies on the inherited functionalities from `Module` and `cls`. -- The `forward` method from the `Module` class must be implemented by any derived class to define the forward pass of the model. -- The `save` and `load` methods from the `Module` class can be used to save and load the parameters of the model to/from a file. -- The `_set` method from the `Module` class is a helper method used by the `load` method to set the parameters of the model. - -In summary, `ModelWrapper` is a flexible and extendable class that combines the functionalities of the `Module` class and another specified class, making it a powerful tool for managing model parameters and performing forward passes in a neural network or similar computational model. -*** -## ClassDef Module -**Module**: Module - -**attributes**: -- No specific attributes are defined within the provided code snippet. - -**Code Description**: -The `Module` class is a subclass of `ParameterContainer` and serves as a container for parameter nodes. It provides a `forward` method that needs to be implemented by derived classes. The `forward` method is responsible for performing the forward pass of the model. - -The `forward` method raises a `NotImplementedError` as it is meant to be overridden by derived classes. This method takes in `*args` and `**kwargs` as input parameters and should return the output of the forward pass. - -The `__call__` method is a convenience method that allows the `Module` object to be called as a function. It simply calls the `forward` method with the provided arguments and returns the result. - -The `save` method is used to save the parameters of the model to a file. It takes a `file_name` parameter as input and creates the necessary directory structure if it doesn't already exist. It then serializes the model's parameters using the `pickle` module and saves them to the specified file. - -The `load` method is used to load the parameters of the model from a file. It takes a `file_name` parameter as input and deserializes the parameters using the `pickle` module. The loaded parameters are then set as the new parameters of the model using the `_set` method. - -The `_set` method is a helper method used by the `load` method to set the parameters of the model from a dictionary. It takes a `new_parameters` parameter, which can be either a `ParameterContainer` or a parameter dictionary. It asserts that the `new_parameters` is of the correct type and then updates the model's parameters accordingly. - -**Note**: -- The `Module` class inherits from the `ParameterContainer` class, which serves as a container for parameter nodes. -- The `forward` method needs to be implemented by derived classes to define the forward pass of the model. -- The `save` and `load` methods can be used to save and load the parameters of the model to/from a file. -- The `_set` method is a helper method used by the `load` method to set the parameters of the model. - -**Output Example**: -```python -model = Module() -model.save("model_params.pkl") -model.load("model_params.pkl") -model.forward(input_data) -``` -### FunctionDef forward(self) -**forward**: The function of forward is to serve as an abstract method that must be implemented by subclasses of the Module class. - -**parameters**: The parameters of this Function. -· args: Variable length argument list. -· kwargs: Arbitrary keyword arguments. - -**Code Description**: The forward function is defined as a method within a class, and it is designed to be overridden by subclasses. The method takes any number of positional and keyword arguments, denoted by *args and **kwargs, respectively. However, in its current form, it raises a NotImplementedError, indicating that it is an abstract method. This means that any subclass inheriting from this class must provide its own implementation of the forward method. - -The forward method is called by the __call__ method of the same class. When an instance of the class is called like a function, the __call__ method is invoked, which in turn calls the forward method with the provided arguments. This design pattern is common in frameworks that require a standard interface for processing inputs, such as neural network layers in deep learning libraries. - -**Note**: -- The forward method must be implemented in any subclass; otherwise, calling an instance of the subclass will result in a NotImplementedError. -- Ensure that the implementation of the forward method in subclasses correctly handles the expected input arguments and performs the desired operations. -*** -### FunctionDef __call__(self) -**__call__**: The function of __call__ is to invoke the forward method of the Module class with the provided arguments. - -**parameters**: The parameters of this Function. -· args: Variable length argument list. -· kwargs: Arbitrary keyword arguments. - -**Code Description**: The __call__ method is designed to make instances of the Module class callable like a regular function. When an instance of the Module class is called, the __call__ method is triggered, which in turn calls the forward method with the same arguments. This design pattern is commonly used in frameworks that require a standard interface for processing inputs, such as neural network layers in deep learning libraries. - -The forward method, which must be implemented by any subclass of the Module class, is where the actual processing logic resides. The __call__ method acts as a wrapper that ensures the forward method is executed with the provided arguments. - -In the context of the project, the __call__ method is referenced by the __get__ method in the FunModule class located in opto\trace\bundle.py. The __get__ method uses functools.partial to bind the __call__ method to an instance of the Module class, effectively supporting instance methods. - -**Note**: -- The forward method must be implemented in any subclass of the Module class; otherwise, calling an instance of the subclass will result in a NotImplementedError. -- Ensure that the implementation of the forward method in subclasses correctly handles the expected input arguments and performs the desired operations. - -**Output Example**: The return value of the __call__ method depends on the implementation of the forward method in the subclass. For instance, if the forward method is implemented to perform a specific computation, the __call__ method will return the result of that computation. -*** -### FunctionDef save(self, file_name) -**save**: The function of save is to save the parameters of the model to a specified file. - -**parameters**: The parameters of this Function. -· file_name: The name of the file where the model parameters will be saved. - -**Code Description**: The save function is designed to persist the parameters of a model to a file. It first checks if the directory specified in the file_name exists. If the directory does not exist, it creates the directory using os.makedirs with the exist_ok=True flag to avoid raising an error if the directory already exists. The function then opens the specified file in binary write mode ("wb") and uses the pickle module to serialize and save the model's parameters. - -The parameters to be saved are obtained by calling the parameters_dict method on the instance (self). This method returns a dictionary containing all the parameters of the model, including both trainable and non-trainable parameters. The dictionary is then serialized and written to the file using pickle.dump. - -**Note**: -- Ensure that the file_name provided includes the correct path where the file should be saved. -- The directory will be created if it does not exist, so there is no need to manually create it beforehand. -- The parameters_dict method must be correctly implemented in the model to return all necessary parameters for saving. -- The file is opened in binary mode, so it will not be human-readable. Use pickle.load to deserialize the file when needed. -*** -### FunctionDef load(self, file_name) -**load**: The function of load is to load the parameters of the model from a file. - -**parameters**: The parameters of this function. -- file_name: The name of the file from which to load the model parameters. - -**Code Description**: The load function is responsible for loading the parameters of a model from a specified file. It takes a single parameter, file_name, which is the name of the file containing the model parameters. - -The function opens the specified file in binary read mode ("rb") using a with statement to ensure the file is properly closed after reading. It then uses the pickle.load function to deserialize the contents of the file into a Python object, which is stored in the variable loaded_data. - -After successfully loading the data, the function calls the _set method on the current instance (self) with loaded_data as the argument. The _set method is responsible for setting the parameters of the model using the loaded data. It ensures that the new parameters are valid and consistent with the existing parameters of the model by performing various checks and updates. - -**Note**: -- The file specified by file_name must exist and be accessible for reading. -- The contents of the file must be a valid serialized representation of the model parameters. -- The _set method is used to update the model's parameters with the loaded data, ensuring consistency and validity. -- Proper error handling should be implemented to handle cases where the file cannot be read or the contents are not as expected. -*** -### FunctionDef _set(self, new_parameters) -**_set**: The function of _set is to set the parameters of the model from a dictionary. - -**parameters**: -- self: The instance of the Module class. -- new_parameters: A ParameterContainer or a parameter dictionary containing the new parameters. - -**Code Description**: The _set function is responsible for setting the parameters of the model from a dictionary. It takes in the self parameter, which represents the instance of the Module class, and the new_parameters parameter, which can be either a ParameterContainer or a parameter dictionary. - -The function first asserts that the new_parameters parameter is an instance of either a dictionary or a ParameterContainer. If it is a ParameterContainer, it retrieves the parameters dictionary using the parameters_dict method. Otherwise, it assumes that new_parameters is already a dictionary. - -Next, it retrieves the current parameters dictionary using the parameters_dict method of the self object. - -The function then asserts that all the keys in the new_parameters_dict are present in the parameters_dict. This ensures that all the model parameters are included in the new parameters dictionary. - -After that, the function iterates over the items in the new_parameters_dict. For each key-value pair, it checks if the key exists in the parameters_dict. If it does, it asserts that the value is an instance of either a ParameterNode or a ParameterContainer. If it is a ParameterNode, it calls the _set method of the corresponding parameter in the parameters_dict, passing the value as the argument. This allows the parameter to update its value. If the key does not exist in the parameters_dict, it asserts that the key is not present in the __dict__ attribute of the self object. If this assertion passes, it sets the attribute of the self object with the key as the attribute name and the value as the attribute value. - -**Note**: -- The _set function is typically used to update the parameters of a model with new values. It ensures that the new parameters are valid and consistent with the existing parameters of the model. -- The function assumes that the model's parameters are stored in the parameters_dict, which is a dictionary of ParameterNodes or ParameterContainers. -- It is important to ensure that the new_parameters dictionary contains all the necessary parameters and that their values are of the correct type. -- The function relies on the _set method of ParameterNode to update the value of a parameter. -- The function uses the setattr function to dynamically set attributes on the self object. -*** diff --git a/generated_docs/opto/trace/nodes.md b/generated_docs/opto/trace/nodes.md deleted file mode 100644 index 2106c63d..00000000 --- a/generated_docs/opto/trace/nodes.md +++ /dev/null @@ -1,2213 +0,0 @@ -## FunctionDef node(message, name, trainable, constraint) -**node**: The function of node is to create a Node object from a message. If the message is already a Node, it will be returned as is. This function is provided for the convenience of the user and should be used instead of directly invoking the Node class. - -**parameters**: -- message: The message to create the Node from. -- name: (optional) The name of the Node. -- trainable: (optional) A boolean indicating whether the Node is trainable or not. Default is False. -- constraint: (optional) A constraint on the Node. - -**Code Description**: The node function is a versatile function that allows users to create Node objects from messages. It takes in a message and optional parameters such as name, trainable, and constraint. - -The function first checks if the trainable parameter is True. If it is, it checks if the message is already a Node. If it is, it extracts the underlying data and updates the name if a new name is provided. It then creates a ParameterNode object with the extracted data, name, trainable set to True, and the provided constraint. If the message is not already a Node, it creates a new ParameterNode object with the message as the data, the provided name, trainable set to True, and the provided constraint. - -If the trainable parameter is False, the function checks if the message is already a Node. If it is, it checks if a name is provided. If a name is provided, it issues a warning that the name is ignored because the message is already a Node. It then returns the message as is. If the message is not already a Node, it creates a new Node object with the message as the data, the provided name, and the provided constraint. - -**Note**: -- The node function is a convenient way to create Node objects from messages. -- The trainable parameter determines whether the created Node is trainable or not. -- The constraint parameter allows users to specify a constraint on the created Node. - -**Output Example**: A possible return value of the node function could be a ParameterNode object with the extracted data, name, trainable set to True, and the provided constraint. -## ClassDef Graph -**Graph**: The function of Graph is to serve as a registry of all the nodes, forming a Directed Acyclic Graph (DAG). - -**attributes**: The attributes of this Class. -· TRACE: A class-level attribute that determines whether the graph is traced when creating MessageNode. It is set to True by default. -· _nodes: An instance-level attribute, which is a defaultdict of lists, used as a lookup table to find nodes by name. - -**Code Description**: The Graph class is designed to manage and organize nodes in a Directed Acyclic Graph (DAG). It provides methods to register nodes, clear the graph, retrieve nodes by name, and identify root nodes. - -- The `__init__` method initializes the Graph object, setting up the `_nodes` attribute as a defaultdict of lists to store nodes by their names. - -- The `clear` method removes all nodes from the graph by deleting each node and reinitializing the `_nodes` attribute. - -- The `register` method adds a node to the graph. It ensures the node is an instance of the Node class and that its name follows the expected format (containing a colon). The method also handles name scoping and assigns a unique name to the node based on its position in the list. - -- The `get` method retrieves a node by its name, which includes an identifier. It splits the name to find the correct node in the `_nodes` dictionary. - -- The `roots` property returns a list of all root nodes in the graph. A root node is identified by its `is_root` attribute. - -- The `__str__` method provides a string representation of the `_nodes` attribute, useful for debugging and logging. - -- The `__len__` method returns the total number of nodes in the graph by summing the lengths of all lists in the `_nodes` dictionary. - -**Note**: -- The `TRACE` attribute controls whether the graph is traced during the creation of MessageNode instances. -- The `register` method assumes that elements in `_nodes` are never removed, which is important for maintaining the integrity of node names. - -**Output Example**: -- When calling the `__str__` method, the output might look like: - ``` - defaultdict(, {'node1': [], 'node2': []}) - ``` -- When calling the `__len__` method, the output might be: - ``` - 5 - ``` -### FunctionDef __init__(self) -**__init__**: The function of __init__ is to initialize an instance of the Graph class by setting up an internal data structure to store nodes. - -**parameters**: The parameters of this Function. -· This function does not take any parameters. - -**Code Description**: The __init__ function initializes an instance of the Graph class. It sets up an internal attribute `_nodes`, which is a defaultdict with lists as the default factory. This defaultdict will be used as a lookup table to store and retrieve nodes by their names. Each key in the `_nodes` dictionary represents a node name, and the corresponding value is a list of nodes associated with that name. This structure allows for efficient organization and retrieval of nodes within the graph. - -**Note**: -- The defaultdict from the collections module is used to automatically handle missing keys by initializing them with an empty list. -- This function does not require any arguments and is called automatically when a new instance of the Graph class is created. -*** -### FunctionDef clear(self) -**clear**: The function of clear is to remove all nodes from the graph and reset the internal node storage. - -**parameters**: The parameters of this Function. -· This function does not take any parameters other than the implicit self parameter. - -**Code Description**: The clear function is designed to empty the graph of all its nodes. It iterates over the current nodes stored in the _nodes attribute, which is a dictionary, and deletes each node. After all nodes have been deleted, it reinitializes the _nodes attribute to an empty defaultdict of lists. This ensures that the graph is completely cleared and ready to be repopulated with new nodes if necessary. - -The function is called in unit tests located in tests\unit_tests\test_backward.py and tests\unit_tests\test_optimizer.py. These tests likely use the clear function to reset the state of the graph between test cases, ensuring that each test runs with a clean slate and is not affected by the state left by previous tests. - -**Note**: -- This function should be used with caution as it irreversibly deletes all nodes in the graph. -- After calling clear, any references to the previously stored nodes will become invalid. -- Ensure that any necessary data is saved or processed before calling this function, as it will reset the graph's state completely. -*** -### FunctionDef register(self, node) -**register**: The function of register is to add a node to the graph. - -**parameters**: -- self: The instance of the class. -- node: The node object to be registered in the graph. - -**Code Description**: -The `register` function is a method of the `Graph` class in the `nodes.py` file of the `trace` module. It is used to add a node to the graph. The function takes in the `self` parameter, which represents the instance of the class, and the `node` parameter, which is the node object to be registered. - -The function first checks if the `node` parameter is an instance of the `Node` class using the `isinstance` function. If it is not, an `AssertionError` is raised. - -Next, the function checks if the name of the node contains exactly one ":" character by splitting the name using the ":" delimiter and checking the length of the resulting list. If the length is not equal to 2, an `AssertionError` is raised. This check ensures that the name of the node follows the required format. - -After that, the function splits the name of the node using the ":" delimiter and assigns the first part of the split to the `name` variable. This is done to separate the name from the version number. - -The function then checks if there are any name scopes defined in the `NAME_SCOPES` list. If the length of the list is greater than 0, the name is prefixed with the last scope in the list followed by a "/". This allows for scoping of node names. - -Finally, the function adds the node to the `_nodes` dictionary using the modified name as the key. The `_name` attribute of the node is set to the modified name followed by the index of the node in the list of nodes with the same name. This index is obtained by subtracting 1 from the length of the list of nodes with the same name. - -**Note**: -- The `register` function should only be called after the node has been properly initialized and its name has been set. -- The function assumes that elements in the `_nodes` dictionary never get removed. - -**Output Example**: -If the name of the node is "node:0", the `register` function will add the node to the `_nodes` dictionary with the key "node" and set the `_name` attribute of the node to "node:0". -*** -### FunctionDef get(self, name) -**get**: The function of get is to retrieve a specific node from the graph based on a given name and identifier. - -**parameters**: The parameters of this Function. -· name: A string in the format "name:id", where "name" is the name of the node and "id" is the identifier of the node. - -**Code Description**: The get function is designed to extract and return a specific node from a graph structure. The input parameter 'name' is expected to be a string formatted as "name:id". The function first splits this string into two parts: 'name' and 'id', using the colon (":") as the delimiter. The 'name' part represents the name of the node, and the 'id' part represents the identifier of the node, which is then converted to an integer. The function then accesses the '_nodes' dictionary attribute of the graph object, using the 'name' as the key to retrieve the list of nodes associated with that name. Finally, it returns the node at the position specified by the integer 'id' within that list. - -**Note**: -- Ensure that the 'name' parameter is correctly formatted as "name:id" before calling this function. -- The function assumes that the '_nodes' attribute is a dictionary where each key is a node name and the corresponding value is a list of nodes. -- The 'id' should be a valid index within the list of nodes for the given 'name'. - -**Output Example**: -If the '_nodes' dictionary is structured as follows: -```python -_nodes = { - "nodeA": ["nodeA_0", "nodeA_1"], - "nodeB": ["nodeB_0"] -} -``` -Calling `get("nodeA:1")` would return `"nodeA_1"`. -*** -### FunctionDef roots(self) -**roots**: The function of roots is to return a list of root nodes from the graph. - -**parameters**: This function does not take any parameters. - -**Code Description**: The `roots` function iterates over the values in the `_nodes` dictionary of the `Graph` object. The `_nodes` dictionary contains lists of nodes. For each node in these lists, the function checks if the node is a root node by evaluating the `is_root` attribute of the node. If the `is_root` attribute is `True`, the node is included in the resulting list. The function ultimately returns a list of all nodes that are identified as root nodes. - -**Note**: -- Ensure that the nodes in the `_nodes` dictionary have the `is_root` attribute properly set to `True` for root nodes and `False` for non-root nodes. -- The function assumes that `_nodes` is a dictionary where the values are lists of node objects. - -**Output Example**: -If the `_nodes` dictionary contains the following structure: -```python -_nodes = { - 'group1': [Node1, Node2], - 'group2': [Node3, Node4] -} -``` -and `Node1` and `Node3` have their `is_root` attribute set to `True`, while `Node2` and `Node4` have it set to `False`, the `roots` function will return: -```python -[Node1, Node3] -``` -*** -### FunctionDef __str__(self) -**__str__**: The function of __str__ is to return a string representation of the Graph object. - -**parameters**: The parameters of this Function. -· None: This method does not take any parameters. - -**Code Description**: The __str__ method is a special method in Python that is used to define the string representation of an object. In this implementation, the __str__ method returns the string representation of the `_nodes` attribute of the Graph object. The `_nodes` attribute is expected to be a collection (such as a list or dictionary) that holds the nodes of the graph. By converting `_nodes` to a string, the method provides a human-readable format of the graph's nodes, which can be useful for debugging and logging purposes. - -**Note**: -- Ensure that the `_nodes` attribute is properly initialized and contains the nodes of the graph before calling the __str__ method. -- The readability and usefulness of the output depend on the structure and content of the `_nodes` attribute. - -**Output Example**: -If the `_nodes` attribute is a list of node names, such as `['A', 'B', 'C']`, the __str__ method will return the string "['A', 'B', 'C']". If `_nodes` is a dictionary representing nodes and their connections, such as `{'A': ['B', 'C'], 'B': ['A'], 'C': ['A']}`, the method will return the string "{'A': ['B', 'C'], 'B': ['A'], 'C': ['A']}". -*** -### FunctionDef __len__(self) -**__len__**: The function of __len__ is to return the number of nodes in the graph. - -**parameters**: The parameters of this Function. -· self: Refers to the instance of the Graph class. - -**Code Description**: The __len__ method calculates the total number of nodes in the graph. It does this by iterating over the values in the self._nodes dictionary, where each value is a list representing the connections or edges of a particular node. The method uses a list comprehension to get the length of each list (i.e., the number of connections for each node) and then sums these lengths to get the total number of nodes. Finally, it returns this sum as the result. - -**Note**: This method assumes that the self._nodes attribute is a dictionary where each key is a node and each value is a list of connections for that node. The method will not work correctly if self._nodes is not structured in this way. - -**Output Example**: If the graph has 3 nodes with the following connections: -- Node A connected to Node B and Node C -- Node B connected to Node A -- Node C connected to Node A - -The return value of __len__ would be 3. -*** -## ClassDef AbstractNode -**AbstractNode**: The function of AbstractNode is to represent an abstract data node in a directed graph. - -**attributes**: -- `data`: The data stored in the node. -- `parents`: The list of parent nodes. -- `children`: The list of child nodes. -- `name`: The name of the node. -- `py_name`: The name of the node without the ":" character. -- `id`: The ID of the node. -- `level`: The level of the node in the graph. -- `is_root`: A boolean indicating whether the node is a root node. -- `is_leaf`: A boolean indicating whether the node is a leaf node. - -**Code Description**: The `AbstractNode` class represents an abstract data node in a directed graph. It is a generic class that can store any type of data. The node can have multiple parents and children, forming a directed graph structure. The node has a name, which is used to identify it within the graph. The `py_name` attribute is the same as the name attribute, but with the ":" character removed. The `id` attribute is extracted from the name and represents a version number. - -The node can be initialized with a value, an optional name, and an optional trainable flag. If the value is an instance of the `Node` class, the node will be initialized as a reference to that node, otherwise, the value will be stored directly in the node. The default name is generated based on the type of the value and a version number. - -The `AbstractNode` class provides several properties to access its attributes. The `data` property allows access to the stored data. If the node is being traced within a context, the `data` property adds the node to the list of used nodes. The `parents` property returns a list of parent nodes, and the `children` property returns a list of child nodes. The `name` property returns the name of the node, and the `py_name` property returns the name without the ":" character. The `id` property returns the version number extracted from the name. The `level` property returns the level of the node in the graph. The `is_root` property returns True if the node has no parents, and the `is_leaf` property returns True if the node has no children. - -The `AbstractNode` class also provides internal methods to add parents and children to the node. The `_add_child` method adds a child node to the node's list of children. The `_add_parent` method adds a parent node to the node's list of parents and updates the level of the node based on the parent's level. - -The `AbstractNode` class overrides the `__str__` method to provide a string representation of the node. The representation includes the name, the type of the data, and the data itself. - -The `AbstractNode` class implements the `__deepcopy__` method to create a deep copy of the node. This allows the node to be detached from the original graph. - -The `AbstractNode` class provides comparison methods `lt` and `gt` to compare the levels of two nodes. - -**Note**: The `AbstractNode` class is meant to be subclassed and extended to create specific types of nodes. - -**Output Example**: -``` -Node: (node_name, dtype=, data=10) -``` -### FunctionDef __init__(self, value) -**__init__**: The function of __init__ is to initialize an instance of the AbstractNode class. - -**parameters**: -- self: The instance of the class. -- value: The value to be assigned to the node. -- name: The name of the node (optional). -- trainable: A boolean indicating whether the node is trainable or not (optional). - -**Code Description**: -The `__init__` function is the constructor of the AbstractNode class. It takes in the `self` parameter, which represents the instance of the class, and the `value`, `name`, and `trainable` parameters, which are used to initialize the attributes of the node. - -The function starts by initializing the `_parents`, `_children`, and `_level` attributes to empty lists and 0 respectively. These attributes are used to keep track of the parent and child nodes of the current node, as well as the level of the node in the graph. - -Next, the function generates a default name for the node based on the type of the `value` parameter. If the `name` parameter is provided, it is appended to the default name. The format of the name is "type:version", where the version is set to 0 if no name is provided. - -After that, the function checks if the `value` parameter is an instance of the Node class. If it is, the `_data` attribute of the current node is set to the `_data` attribute of the `value` parameter, and the `_name` attribute is set to the `_name` attribute of the `value` parameter if no name is provided. Otherwise, the `_data` attribute is set to the `value` parameter itself, and the `_name` attribute is set to the default name. - -Finally, the function calls the `register` function of the GRAPH object to register the current node in the graph. - -**Note**: -- The `__init__` function should be called to create a new instance of the AbstractNode class. -- The `value` parameter can be any type of value. -- The `name` parameter is optional and can be used to provide a custom name for the node. -- The `trainable` parameter is optional and can be used to indicate whether the node is trainable or not. -- The `register` function should only be called after the node has been properly initialized and its name has been set. -*** -### FunctionDef data(self) -**data**: The function of data is to retrieve the internal data of a node, potentially adding the node to a list of used nodes if certain conditions are met. - -**parameters**: The parameters of this Function. -· self: Refers to the instance of the class that contains this method. - -**Code Description**: The data function is designed to return the internal data of a node object. It first checks if there are any nodes in the USED_NODES list and if the GRAPH.TRACE flag is set to True. If both conditions are met, it adds the current node (self) to the USED_NODES list. This indicates that the node is being used within a tracing context. Finally, the function returns the value of the node's internal data by accessing the "_data" attribute. - -This function is utilized in various parts of the project to access the data stored within nodes. For instance: -- In the node_to_function_feedback function in opto\optimizers\function_optimizer.py, it retrieves node data to convert a TraceGraph to a FunctionFeedback. -- In the construct_update_dict method of the FunctionOptimizer class, it converts suggestions into the appropriate data types by accessing node data. -- In the __next__ method of the SeqIterable class in opto\trace\containers.py, it iterates over a wrapped list of nodes and accesses their data. -- In the ExecutionError class's __init__ and __str__ methods in opto\trace\errors.py, it retrieves the data of an exception node to initialize and represent the error. -- In the get_label method of the NodeVizStyleGuide class in opto\trace\nodes.py, it generates labels for nodes by accessing their data. -- In the _set method of the Node class in opto\trace\nodes.py, it sets the value of a node, unwrapping it if necessary. -- In the trace_fun method of the Foo class in tests\unit_tests\test_bundle.py, it prints the data of a node during a trace function. - -**Note**: This function assumes that the "_data" attribute exists within the node object. If this attribute is not present, an AttributeError will be raised. - -**Output Example**: A possible return value of the code could be any data type stored in the "_data" attribute of the node, such as an integer, string, list, or custom object. For example, if the "_data" attribute contains the integer 42, the function will return 42. -*** -### FunctionDef parents(self) -**parents**: The function of parents is to return the parents of the current node. -**parameters**: -- self: The current node object. -**Code Description**: -The `parents` function is a method of the `AbstractNode` class in the `nodes.py` module. It returns the parents of the current node. The parents are stored in the `_parents` attribute of the node object. - -The function takes only one parameter, `self`, which refers to the current node object. It is used to access the `_parents` attribute and return its value. - -The `_parents` attribute is a list that contains the parent nodes of the current node. These parent nodes are the nodes that have an edge pointing to the current node in the graph. - -The `parents` function is called by several objects in the project. For example, it is called by the `is_root` function in the `AbstractNode` class, which checks if the current node is a root node by checking if it has any parents. It is also called by the `backward` function in the `Node` class, which performs a backward pass in the graph by propagating feedback from the current node to its parents. - -**Note**: The `parents` function is a basic method that provides access to the parents of a node. It is an essential part of the graph structure and is used in various operations such as graph traversal and feedback propagation. - -**Output Example**: -If the current node has two parents, the `parents` function will return a list containing the two parent nodes. -*** -### FunctionDef children(self) -**children**: The function of children is to return the list of child nodes associated with the current node. - -**parameters**: This function does not take any parameters. - -**Code Description**: The `children` function is a method of the `AbstractNode` class. It returns the `_children` attribute of the instance, which is a list containing the child nodes of the current node. This method is essential for accessing the hierarchical structure of nodes, allowing traversal and manipulation of the node tree. - -The `children` method is called by the `is_leaf` method within the same `AbstractNode` class. The `is_leaf` method uses `children` to determine if the current node is a leaf node (i.e., it has no children). Specifically, `is_leaf` checks if the length of the list returned by `children` is zero, indicating that the node has no children and is therefore a leaf. - -Additionally, the `children` method is referenced in the `opto\trace\bundle.py` file and the `tests\unit_tests\test_nodes.py` file, although specific details of its usage in these files are not provided. - -**Note**: Ensure that the `_children` attribute is properly initialized and maintained within the `AbstractNode` class to avoid unexpected behavior when calling the `children` method. - -**Output Example**: A possible return value of the `children` method could be: -```python -[, ] -``` -This indicates that the current node has two child nodes, each represented by an instance of `AbstractNode`. -*** -### FunctionDef name(self) -**name**: The function of name is name. -**parameters**: -- self: The instance of the class. -**Code Description**: -The `name` function is a method of the `AbstractNode` class. It returns the value of the private attribute `_name`. This function is used to retrieve the name of the node. - -The `_name` attribute is set when the node is registered in the graph. It is a combination of the node's name and its index in the list of nodes with the same name. The index is incremented each time a new node with the same name is registered. - -This function is called by various objects in the project. For example, it is called by the `get_fun_name` function in the `function_optimizer.py` file of the `optimizers` module. It is also called by the `register` function in the `nodes.py` file of the `trace` module. - -In the `get_fun_name` function, the `name` function is used to retrieve the name of a `MessageNode` object. If the `info` attribute of the node is a dictionary and it contains the key "fun_name", the value associated with that key is returned. Otherwise, the name of the node is split using the ":" delimiter, and the first part of the split is returned. - -In the `register` function, the `name` function is used to set the `_name` attribute of a node. The name is split using the ":" delimiter, and the first part of the split is assigned to the `name` variable. If there are any name scopes defined in the `NAME_SCOPES` list, the name is prefixed with the last scope in the list followed by a "/". The node is then added to the `_nodes` dictionary using the modified name as the key. The `_name` attribute of the node is set to the modified name followed by the index of the node in the list of nodes with the same name. - -**Note**: -- The `name` function should only be called after the node has been registered in the graph. -- The `name` function assumes that elements in the `_nodes` dictionary never get removed. - -**Output Example**: -If the `_name` attribute of a node is "node:0", the `name` function will return "node:0". -*** -### FunctionDef py_name(self) -**py_name**: The function of py_name is py_name. - -**parameters**: -- self: The instance of the class. - -**Code Description**: -The `py_name` function is a method of the current class. It returns the value of the `name` attribute after removing the ":" character. This function is used to modify the name attribute by replacing the ":" character with an empty string. - -This function is called by various objects in the project. For example, it is called by the `repr_function_call` function in the `function_optimizer.py` file of the `optimizers` module. It is also called by the `node_to_function_feedback` function in the same file. - -In the `repr_function_call` function, the `py_name` function is used to retrieve the name of a `MessageNode` object. The name is then used to construct a function call string. - -In the `node_to_function_feedback` function, the `py_name` function is used to retrieve the name of a node. The name is then used as a key in the `documentation` dictionary. - -In the `summarize` method of the `FunctionOptimizer` class, the `py_name` function is used to retrieve the name of a parameter node. The name is then used to classify the node into variables and others. - -In the `construct_update_dict` method of the `FunctionOptimizer` class, the `py_name` function is used to retrieve the name of a parameter node. The name is then used to construct an update dictionary. - -In the `fun` method of the `FunModule` class, the `py_name` function is used to retrieve the name of a parameter node. The name is then used to define a function. - -In the `get_label` method of the `NodeVizStyleGuide` class, the `py_name` function is used to retrieve the name of a node. The name is then used to construct a label for the node. - -In the `backward` method of the `Node` class, the `py_name` function is used to retrieve the name of a node. The name is then used for visualization purposes. - -**Note**: -- The `py_name` function should only be called after the name attribute has been set. -- The `py_name` function assumes that the name attribute does not contain any other special characters that need to be replaced. - -**Output Example**: -If the name attribute of a node is "node:0", the `py_name` function will return "node0". -*** -### FunctionDef id(self) -**id**: The function of id is to extract and return the identifier part of the node's name. - -**parameters**: The parameters of this Function. -- self: The instance of the class. - -**Code Description**: The `id` function is a method of the `AbstractNode` class. It operates on the `name` attribute of the instance, which is a string formatted as "name:identifier". The function splits this string using the colon (":") delimiter and returns the second part, which corresponds to the identifier. This identifier is typically a unique part of the node's name, distinguishing it from other nodes with the same base name. - -The `name` attribute is accessed through the `name` method of the `AbstractNode` class, which retrieves the value of the private attribute `_name`. The `id` function relies on the assumption that the `name` attribute follows the "name:identifier" format. - -**Note**: -- The `id` function should only be called after the node's `name` attribute has been properly set and follows the expected format. -- Ensure that the `name` attribute contains a colon (":") to avoid index errors during the split operation. - -**Output Example**: -If the `name` attribute of a node is "node:0", the `id` function will return "0". -*** -### FunctionDef level(self) -**level**: The function of level is to return the internal level attribute of the object. - -**parameters**: The parameters of this Function. -· This function does not take any parameters. - -**Code Description**: The level function is a method that returns the value of the private attribute _level of the object. This method is used to access the internal state of the object, specifically the _level attribute, which is presumably set elsewhere in the class. The function does not modify any state or take any arguments; it simply provides a way to retrieve the current value of _level. - -In the context of its usage within the project, the level function is called by the init_feedback method in the GraphPropagator class, located in the opto\trace\propagators\graph_propagator.py file. The init_feedback method uses the level function to obtain the level of a node and includes this information in the TraceGraph it constructs. This indicates that the level of a node is an important piece of information for initializing feedback in the graph propagation process. - -**Note**: This function is a simple accessor and does not perform any validation or modification of the _level attribute. It is important to ensure that the _level attribute is properly initialized before calling this function to avoid potential issues. - -**Output Example**: If the _level attribute of the object is set to 3, calling the level function will return 3. -*** -### FunctionDef is_root(self) -**is_root**: The function of is_root is to determine if the current node is a root node. - -**parameters**: The parameters of this function. -· self: The current node object. - -**Code Description**: The `is_root` function is a method of the `AbstractNode` class in the `nodes.py` module. It checks whether the current node is a root node by evaluating the length of its parents list. Specifically, it returns `True` if the length of the parents list is zero, indicating that the node has no parents and is therefore a root node. Conversely, it returns `False` if the node has one or more parents. - -The function relies on the `parents` method of the `AbstractNode` class to retrieve the list of parent nodes. The `parents` method accesses the `_parents` attribute of the node object, which is a list containing the parent nodes. By checking the length of this list, the `is_root` function determines the root status of the node. - -**Note**: This function is essential for identifying root nodes in a graph structure, which can be useful for various graph operations such as traversal, initialization, and feedback propagation. - -**Output Example**: -- If the current node has no parents, the `is_root` function will return `True`. -- If the current node has one or more parents, the `is_root` function will return `False`. -*** -### FunctionDef is_leaf(self) -**is_leaf**: The function of is_leaf is to determine if the current node is a leaf node, meaning it has no children. - -**parameters**: This function does not take any parameters. - -**Code Description**: The `is_leaf` method is a part of the `AbstractNode` class. It checks whether the current node has any child nodes by utilizing the `children` method of the same class. Specifically, it returns `True` if the length of the list returned by the `children` method is zero, indicating that the node has no children and is therefore a leaf node. Otherwise, it returns `False`. - -The `children` method, which is called within `is_leaf`, returns the `_children` attribute of the instance. This attribute is a list containing the child nodes of the current node. By checking the length of this list, `is_leaf` can accurately determine the leaf status of the node. - -**Note**: Ensure that the `_children` attribute is properly initialized and maintained within the `AbstractNode` class to avoid unexpected behavior when calling the `is_leaf` method. - -**Output Example**: A possible return value of the `is_leaf` method could be: -```python -True -``` -This indicates that the current node has no children and is therefore a leaf node. -*** -### FunctionDef _add_child(self, child) -**_add_child**: The function of _add_child is to add a child node to the current node. -**parameters**: -- child: The child node to be added. - -**Code Description**: -The `_add_child` function is used to add a child node to the current node. It performs the following steps: -1. It first checks if the child node is not the same as the current node itself. If it is, it raises an assertion error with the message "Cannot add self as a child." -2. It then checks if the child node is an instance of the `Node` class. If it is not, it raises an assertion error with a message indicating that the child is not a Node. -3. Finally, it calls the `_add_parent` function of the child node, passing the current node as the parent. - -**Note**: -- The `_add_child` function ensures that the child node is not the same as the current node and that it is an instance of the `Node` class before adding it as a child. -- This function assumes that the child node has an `_add_parent` function to add the current node as its parent. -*** -### FunctionDef _add_parent(self, parent) -**_add_parent**: The function of _add_parent is to add a parent node to the current node in the hierarchical structure of the graph. - -**parameters**: -- parent: The parent node to be added. - -**Code Description**: -The _add_parent function is a method designed to add a parent node to the current node in the hierarchical structure of the graph. It performs several checks and operations to ensure the validity of the parent node and the consistency of the graph structure. - -First, the function asserts that the parent node is not the same as the current node, as it is not allowed to add itself as a parent. This check prevents circular dependencies and ensures the integrity of the graph. - -Next, the function asserts that the parent node is an instance of the Node class. This check ensures that only valid nodes can be added as parents. - -If both checks pass, the function proceeds to add the current node as a child to the parent node by appending it to the parent's _children attribute. Similarly, it adds the parent node to the current node's _parents attribute. - -Finally, the function calls the _update_level method to update the level attribute of the current node. It passes the maximum value between the current node's _level attribute and the parent node's _level attribute plus one as the new level value. This ensures that the hierarchical structure of the nodes is maintained correctly, with child nodes always having a level greater than or equal to their parent nodes. - -It is worth noting that the _add_parent function assumes that the parent parameter is a valid instance of the Node class. If the parent parameter is not a Node instance, an assertion error will be raised. - -**Note**: -- The function does not return any value. -- The function assumes that the parent parameter is a valid instance of the Node class. -- The function raises an assertion error if the parent parameter is the same as the current node or if it is not an instance of the Node class. -*** -### FunctionDef _update_level(self, new_level) -**_update_level**: The function of _update_level is to update the level attribute of the current node to a new specified level. - -**parameters**: The parameters of this Function. -· new_level: The new level to which the node's level attribute should be updated. - -**Code Description**: The _update_level function is a method designed to update the internal _level attribute of an instance of the AbstractNode class. This method takes a single parameter, new_level, which represents the new level value that the node should be assigned. The function directly assigns this new value to the node's _level attribute. - -In the context of its usage within the project, the _update_level function is called by the _add_parent method of the AbstractNode class. When a new parent node is added to the current node, the _add_parent method ensures that the current node's level is updated appropriately. Specifically, it sets the current node's level to the maximum of its current level and the new parent's level plus one. This ensures that the hierarchical structure of the nodes is maintained correctly, with child nodes always having a level greater than or equal to their parent nodes. - -**Note**: -- The function assumes that the new_level parameter is a valid integer representing the level. -- The function does not perform any validation or checks on the new_level parameter; it directly assigns it to the _level attribute. -- The commented-out line in the function suggests that there was an intention to update a global or shared structure (GRAPH._levels) that tracks nodes by their levels, but this functionality is not implemented in the current version of the function. -*** -### FunctionDef __str__(self) -**__str__**: The function of __str__ is to provide a string representation of the AbstractNode object. - -**parameters**: The parameters of this function. -· self: The instance of the AbstractNode class. - -**Code Description**: The __str__ method in the AbstractNode class returns a string that represents the node in a human-readable format. This method is particularly useful for debugging and logging purposes, as it provides a quick way to inspect the node's key attributes. The string includes the node's name, the data type of the node's data, and the actual data stored in the node. - -The method constructs the string by accessing the `name` property of the node, which retrieves the node's name. It also accesses the `_data` attribute to include the data type and the data itself in the string. The `name` property is a method that returns the value of the private attribute `_name`, which is set when the node is registered in the graph. - -**Note**: -- The __str__ method should be used when a readable string representation of the node is needed, such as in logging or debugging scenarios. -- Ensure that the node has been properly initialized and registered before calling this method to avoid any unexpected behavior. - -**Output Example**: -If a node has the name "node:0", its data type is ``, and its data is `42`, the __str__ method will return: -``` -Node: (node:0, dtype=, data=42) -``` -*** -### FunctionDef __deepcopy__(self, memo) -**__deepcopy__**: The function of __deepcopy__ is to create a deep copy of the node, which is detached from the original graph. - -**parameters**: The parameters of this Function. -· memo: A dictionary used to keep track of objects that have already been copied to avoid infinite recursion during the deep copy process. - -**Code Description**: The __deepcopy__ function is designed to create a deep copy of an instance of the AbstractNode class. This means that the new instance will be a completely independent copy of the original, with no shared references to mutable objects. - -1. The function starts by obtaining the class of the current instance (`cls = self.__class__`). -2. It then creates a new, uninitialized instance of this class (`result = cls.__new__(cls)`). -3. The `memo` dictionary is updated to associate the original instance's ID with the new instance (`memo[id(self)] = result`). This helps in tracking already copied objects to prevent infinite loops. -4. The function iterates over all the attributes of the original instance (`for k, v in self.__dict__.items():`). -5. For attributes named `_parents` or `_children`, it sets these attributes in the new instance to empty lists (`setattr(result, k, [])`). This ensures that the new instance starts with no parent or child nodes. -6. For all other attributes, it performs a deep copy of the attribute's value and assigns it to the new instance (`setattr(result, k, copy.deepcopy(v, memo))`). -7. Finally, the new instance is returned (`return result`). - -**Note**: -- This function ensures that the new node is completely independent of the original node, with no shared references to mutable objects. -- Special handling is provided for `_parents` and `_children` attributes to ensure they are initialized as empty lists in the new instance. - -**Output Example**: -If the original node has attributes like `name`, `_parents`, and `_children`, the deep copy will result in a new node with the same `name` but with `_parents` and `_children` set to empty lists. For example: - -Original Node: -```python -original_node = AbstractNode() -original_node.name = "Node1" -original_node._parents = [parent_node] -original_node._children = [child_node] -``` - -Deep Copied Node: -```python -copied_node = copy.deepcopy(original_node) -print(copied_node.name) # Output: Node1 -print(copied_node._parents) # Output: [] -print(copied_node._children) # Output: [] -``` -*** -### FunctionDef lt(self, other) -**lt**: The function of lt is to compare the levels of two nodes and determine if the level of the current node is less than the level of another node. - -**parameters**: The parameters of this Function. -· self: The current instance of the node. -· other: Another instance of a node to compare with the current node. - -**Code Description**: The lt function is a method used to compare the levels of two nodes. It takes two parameters: `self`, which refers to the current node instance, and `other`, which refers to another node instance. The function compares the `_level` attribute of both nodes. Specifically, it checks if the negated level of the current node (`-self._level`) is less than the negated level of the other node (`-other._level`). This effectively means that the function is comparing the levels in reverse order, where a higher numerical level is considered "less than" a lower numerical level. - -**Note**: -- Ensure that both `self` and `other` have the `_level` attribute defined before using this function. -- This function is intended to be used where node levels are compared in a reversed manner. - -**Output Example**: -If `self._level` is 3 and `other._level` is 5, the function will return `True` because `-3` is less than `-5`. -*** -### FunctionDef gt(self, other) -**gt**: The function of gt is to compare the levels of two AbstractNode objects and determine if the level of the current object is greater than the level of another object. - -**parameters**: The parameters of this Function. -· self: The instance of the current AbstractNode object. -· other: Another instance of an AbstractNode object to compare against. - -**Code Description**: The gt function is a method used to compare the levels of two AbstractNode objects. It takes two parameters: `self` and `other`, which are both instances of AbstractNode. The function compares the `_level` attribute of the two objects. Specifically, it negates the `_level` attributes of both objects and then checks if the negated level of the current object (`self`) is greater than the negated level of the other object (`other`). This effectively determines if the level of the current object is greater than the level of the other object. - -**Note**: -- The `_level` attribute must be defined for both AbstractNode objects being compared. -- This function relies on the assumption that `_level` is a numeric value that can be meaningfully compared. - -**Output Example**: -If `self._level` is 3 and `other._level` is 2, the function will return `True` because -3 is greater than -2. -*** -## FunctionDef get_op_name(description) -**get_op_name**: The function of get_op_name is to extract the operator type from the given description. - -**Parameters**: -- description: A string representing the description from which the operator type needs to be extracted. - -**Code Description**: -The `get_op_name` function takes a description as input and uses regular expression to search for the operator type enclosed in square brackets at the beginning of the description. If a match is found, the operator type is extracted and returned. Otherwise, a `ValueError` is raised with a specific error message. - -This function is called by multiple objects in the project. In the `FunModule` class of the `bundle.py` file, the `get_op_name` function is used to generate the description for the function module. The extracted operator type is combined with the function name and docstring to create a meaningful description. The `name` method of the `FunModule` class also calls the `get_op_name` function to retrieve the operator type from the description. - -The `get_op_name` function is also used in the `backward` method of the `Node` class in the `nodes.py` file. This method performs a backward pass in a graph and propagates feedback from child nodes to parent nodes. The `get_op_name` function is used to extract the operator type from the description of each node. - -**Note**: -- The description parameter must contain the operator type enclosed in square brackets at the beginning. -- If the description does not contain the operator type, a `ValueError` will be raised. - -**Output Example**: -If the description is "[Add] Add two numbers", the function will return "Add". -## ClassDef NodeVizStyleGuide -**NodeVizStyleGuide**: The function of NodeVizStyleGuide is to provide a standardized way to visualize nodes in a graph, particularly for use with graph visualization tools like Graphviz. - -**attributes**: The attributes of this Class. -· style: A string that defines the style of the visualization. Default is 'default'. -· print_limit: An integer that sets the maximum number of characters to print for node descriptions and content. Default is 100. - -**Code Description**: The NodeVizStyleGuide class is designed to facilitate the visualization of nodes in a graph by providing a consistent style guide. It includes methods to generate attributes for nodes, such as labels, shapes, colors, and styles, which are essential for rendering nodes in a visually coherent manner. - -- The `__init__` method initializes the class with a specified style and a print limit for node descriptions and content. -- The `get_attrs` method returns a dictionary of attributes for a given node, including label, shape, fill color, and style. -- The `get_label` method constructs a label for a node by combining its name, description, and data. It truncates the description and data if they exceed the print limit. -- The `get_node_shape` method determines the shape of a node based on its type. For instance, ParameterNode types are represented as 'box', while other types are represented as 'ellipse'. -- The `get_color` method assigns a color to a node based on its type. ExceptionNode types are colored 'firebrick1', and ParameterNode types are colored 'lightgray'. -- The `get_style` method sets the style of a node to 'filled,solid' if the node is trainable; otherwise, it returns an empty string. - -In the context of its usage within the project, the NodeVizStyleGuide class is utilized in the `backward` method of the Node class. When the `visualize` parameter is set to True, an instance of NodeVizStyleGuide is created to generate the necessary attributes for each node in the graph. These attributes are then used to render the nodes and edges in the graph using Graphviz. The `get_attrs` method is called to obtain the visualization attributes for each node, ensuring that the graph is displayed with a consistent and informative style. - -**Note**: -- Ensure that the `print_limit` is set appropriately to avoid truncating important information in node descriptions and content. -- The class assumes the existence of specific node types like ParameterNode and ExceptionNode, so it should be used in environments where these types are defined. - -**Output Example**: -A possible appearance of the code's return value from the `get_attrs` method might look like this: -``` -{ - 'label': 'node_name\nnode_description...\nnode_content...', - 'shape': 'ellipse', - 'fillcolor': '', - 'style': 'filled,solid' -} -``` -### FunctionDef __init__(self, style, print_limit) -**__init__**: The function of __init__ is to initialize an instance of the NodeVizStyleGuide class with specific visualization style settings and a print limit. - -**parameters**: The parameters of this Function. -· style: A string parameter that sets the visualization style. The default value is 'default'. -· print_limit: An integer parameter that sets the limit for print operations. The default value is 100. - -**Code Description**: The __init__ function is a constructor method for the NodeVizStyleGuide class. It initializes the instance with two attributes: `style` and `print_limit`. The `style` attribute is set to the value provided by the `style` parameter, which defaults to 'default' if not specified. The `print_limit` attribute is set to the value provided by the `print_limit` parameter, which defaults to 100 if not specified. These attributes are used to configure the visualization style and the print limit for the node visualization guide. - -**Note**: Ensure that the `style` parameter is a valid string representing a visualization style and that the `print_limit` parameter is a positive integer to avoid potential issues during the usage of the NodeVizStyleGuide class. -*** -### FunctionDef get_attrs(self, x) -**get_attrs**: The function of get_attrs is to generate a dictionary of attributes for a node object. - -**parameters**: -- self: Refers to the instance of the class that contains this method. -- x: The node object for which the attributes are generated. - -**Code Description**: -The `get_attrs` function is a method of the `NodeVizStyleGuide` class. It takes a node object `x` as input and generates a dictionary of attributes for the node. The attributes include the label, shape, fill color, and style of the node. - -The function first calls the `get_label` method of the `NodeVizStyleGuide` class to generate the label attribute. It then calls the `get_node_shape` method to determine the shape attribute based on the type of the node. The `get_color` method is called to determine the fill color attribute based on the type of the node. Finally, the `get_style` method is called to determine the style attribute based on the trainable status of the node. - -The function constructs a dictionary `attrs` with the label, shape, fill color, and style attributes, and returns it. - -This function is called by the `backward` method of the `Node` class in the same module. The `backward` method performs a backward pass in a computational graph and utilizes the `get_attrs` function to generate the attributes for each node in the graph. - -**Note**: -- The `get_attrs` function assumes that the `get_label`, `get_node_shape`, `get_color`, and `get_style` methods are implemented correctly and return valid values. -- The function does not handle cases where the node object does not have the required attributes or methods. - -**Output Example**: -If the label of the node is "Node1", the shape is "ellipse", the fill color is "lightgray", and the style is an empty string, the function will return the following dictionary: -``` -{ - 'label': 'Node1', - 'shape': 'ellipse', - 'fillcolor': 'lightgray', - 'style': '' -} -``` -*** -### FunctionDef get_label(self, x) -**get_label**: The function of get_label is to generate a label for a node object. - -**parameters**: -- self: Refers to the instance of the class that contains this method. -- x: The node object for which the label is generated. - -**Code Description**: -The `get_label` function is a method of the `NodeVizStyleGuide` class. It takes a node object `x` as input and generates a label for the node. The label consists of the node's name and description, as well as additional content if available. - -The function first retrieves the description of the node by calling the `description` method of the node object. It then checks if the length of the description exceeds the `print_limit` attribute of the `NodeVizStyleGuide` instance. If it does, the description is truncated and an ellipsis is appended. - -Next, the function constructs the text part of the label by concatenating the node's name and the truncated description. The content of the node is retrieved by accessing the `data` attribute of the node object. If the content is a dictionary and it contains a key named "content", the value associated with that key is used as the content. Otherwise, the content is converted to a string representation. - -Similar to the description, the content is checked against the `print_limit` attribute and truncated if necessary. - -Finally, the function returns the concatenated text and content as the label for the node. - -This function is called by the `get_attrs` method of the `NodeVizStyleGuide` class. The `get_attrs` method generates a dictionary of attributes for a node, including the label, shape, fill color, and style. The `get_label` function is responsible for generating the label attribute of the dictionary. - -**Note**: -- The `get_label` function assumes that the `description` and `data` attributes of the node object are already set and contain valid values. -- The `print_limit` attribute of the `NodeVizStyleGuide` instance determines the maximum length of the description and content before truncation. -- The function does not handle cases where the `data` attribute is not present or is of an unsupported type. - -**Output Example**: -If the name of the node is "Node1" and the description is "This is a sample node description.", the content is a dictionary with the key "content" and value "Sample content", and the `print_limit` is set to 20, the function will return the following label: -``` -Node1 -This is a sample no... -Sample content -``` -*** -### FunctionDef get_node_shape(self, x) -**get_node_shape**: The function of get_node_shape is to determine the shape of a node based on its type. - -**parameters**: The parameters of this Function. -· x: The node whose shape is to be determined. - -**Code Description**: The get_node_shape function is a method designed to return the shape of a node in a computational graph visualization. It takes a single parameter, x, which represents the node whose shape needs to be determined. The function checks the type of the node x. If x is an instance of the ParameterNode class, the function returns the string 'box', indicating that the node should be visualized as a box. For all other types of nodes, the function returns the string 'ellipse', indicating that the node should be visualized as an ellipse. - -This function is utilized within the get_attrs method of the NodeVizStyleGuide class. The get_attrs method calls get_node_shape to include the shape attribute in the dictionary of attributes for a node. This dictionary is used to define various visual properties of the node, such as its label, shape, fill color, and style. - -**Note**: -- The function relies on the type of the node to determine its shape. It specifically checks if the node is an instance of ParameterNode. -- The ParameterNode class represents a trainable node in a computational graph and has various attributes such as value, name, trainable, description, constraint, and info. - -**Output Example**: -- If x is an instance of ParameterNode, the function returns 'box'. -- If x is not an instance of ParameterNode, the function returns 'ellipse'. -*** -### FunctionDef get_color(self, x) -**get_color**: The function of get_color is to determine the color representation of a node based on its type. - -**parameters**: The parameters of this Function. -· x: The node whose color representation is to be determined. - -**Code Description**: The get_color function is a method designed to return a specific color string based on the type of the node passed as an argument. It takes a single parameter, x, which represents the node. The function checks the type of the node and returns a corresponding color string: - -- If the node is of type ExceptionNode, the function returns the color 'firebrick1'. -- If the node is of type ParameterNode, the function returns the color 'lightgray'. -- For any other type of node, the function returns an empty string. - -This function is utilized within the get_attrs method of the NodeVizStyleGuide class. The get_attrs method calls get_color to determine the fill color attribute of a node, which is part of a set of attributes used for visualizing the node. The get_attrs method constructs a dictionary of attributes including label, shape, fill color, and style, where the fill color is obtained by invoking get_color. - -**Note**: -- The function relies on the specific types of nodes (ExceptionNode and ParameterNode) to determine the color. If additional node types need to be supported, the function should be extended accordingly. -- The function returns an empty string for node types that are not explicitly handled, which may need to be addressed depending on the visualization requirements. - -**Output Example**: -- For an ExceptionNode, the function would return 'firebrick1'. -- For a ParameterNode, the function would return 'lightgray'. -- For any other node type, the function would return an empty string. -*** -### FunctionDef get_style(self, x) -**get_style**: The function of get_style is to determine the style attributes of a node based on its trainable status. - -**parameters**: The parameters of this Function. -· x: An object that contains the attribute 'trainable'. - -**Code Description**: The get_style function evaluates the 'trainable' attribute of the input object 'x'. If 'x.trainable' is True, the function returns the string 'filled,solid', indicating that the node should be styled with a filled and solid appearance. If 'x.trainable' is False, the function returns an empty string, indicating that no specific style should be applied. - -This function is called by the get_attrs function within the same module. The get_attrs function constructs a dictionary of attributes for a node, including its label, shape, fill color, and style. The get_style function specifically provides the 'style' attribute for this dictionary, ensuring that nodes which are trainable are visually distinguished by a filled and solid style. - -**Note**: Ensure that the input object 'x' has a 'trainable' attribute; otherwise, the function may raise an AttributeError. - -**Output Example**: -- If x.trainable is True, the return value will be 'filled,solid'. -- If x.trainable is False, the return value will be an empty string "". -*** -## ClassDef Node -An unknown error occurred while generating this documentation after many tries. -### FunctionDef __init__(self, value) -**__init__**: The function of __init__ is to initialize a Node object in a computational graph. - -**parameters**: The parameters of this Function. -· value: The initial value of the node. -· name: An optional string representing the name of the node. -· trainable: A boolean indicating whether the node is trainable. -· description: A string providing a description of the node. -· constraint: An optional string representing any constraints on the node. -· info: An optional dictionary containing additional information about the node. - -**Code Description**: The __init__ function initializes a Node object with several attributes. It first calls the superclass initializer with the value and name parameters. The trainable attribute is set based on the provided argument, indicating whether the node can be trained. The _feedback attribute is initialized as a defaultdict of lists, which will store feedback from child nodes. This feedback mechanism is analogous to gradients in machine learning and is used to propagate information back through the graph. The _description attribute stores a textual description of the node, while the _constraint attribute holds any constraints that apply to the node. The _backwarded attribute is a boolean flag indicating whether the backward pass has been called on this node. The _info attribute is a dictionary for storing additional information about the node. Finally, the _dependencies attribute is a dictionary that tracks dependencies on parameters and expandable nodes, which are nodes that depend on parameters not visible at the current graph level. - -**Note**: Points to note about the use of the code -- Ensure that the value parameter is provided when initializing the Node. -- The name parameter is optional but can be useful for identifying nodes in the graph. -- The trainable parameter should be set to True if the node is intended to be updated during training. -- The description, constraint, and info parameters provide additional context and constraints for the node, which can be useful for debugging and documentation purposes. -- The feedback mechanism is designed to support non-commutative aggregation, so feedback should be handled carefully to maintain the correct order of operations. -*** -### FunctionDef zero_feedback(self) -**zero_feedback**: The function of zero_feedback is to reset the feedback attribute of the Node object to an empty state. - -**parameters**: This function does not take any parameters. - -**Code Description**: The zero_feedback function is designed to reset the feedback mechanism of a Node object. It achieves this by setting the _feedback attribute to a new defaultdict with lists as the default factory. This ensures that any previous feedback data stored in the _feedback attribute is cleared, effectively resetting it to an empty state. - -In the context of its usage within the project, the zero_feedback function is called by the backward method of the Node class. During the backward pass, feedback is propagated from the current node to its parent nodes. After this propagation, zero_feedback is invoked to clear the feedback of the current node. This is crucial to prevent the feedback from being double-counted if the retain_graph parameter is set to True. By resetting the feedback, the function ensures that each node's feedback is only considered once during the backward pass, maintaining the integrity of the feedback propagation process. - -**Note**: It is important to note that zero_feedback should be used judiciously within the feedback propagation process to avoid unintended loss of feedback data. It is specifically designed to be used after feedback has been successfully propagated to parent nodes. -*** -### FunctionDef feedback(self) -**feedback**: The function of feedback is to return the internal feedback attribute of the Node object. - -**parameters**: The parameters of this Function. -· None - -**Code Description**: The feedback function is a method of the Node class that simply returns the value of the private attribute _feedback. This method does not take any parameters and provides a way to access the internal feedback data stored within the Node object. - -The feedback method is utilized in various parts of the project to retrieve feedback information from Node objects. For instance, in the summarize method of the FunctionOptimizer class, the feedback method is called on each trainable node to aggregate feedback from all parameters. This aggregated feedback is then used to construct a summary of the feedback for further processing. - -Similarly, in the _propagate method of the GraphPropagator class, the feedback method is called on a child node to obtain its feedback, which is then aggregated and propagated to its parent nodes. This ensures that feedback information flows correctly through the graph structure. - -In the AbstractPropagator class, the __call__ method also makes use of the feedback method to propagate feedback from a child node to its parents. This method ensures that the feedback is in the correct format and that all parent nodes receive the appropriate feedback. - -The SumPropagator class's _propagate method uses the feedback method to retrieve user feedback or sum the feedback from various sources, ensuring that the feedback is correctly propagated to parent nodes. - -**Note**: The feedback method is a straightforward accessor method and does not perform any modifications to the internal state of the Node object. It is essential to ensure that the _feedback attribute is correctly initialized and maintained within the Node class to provide accurate feedback information. - -**Output Example**: A possible appearance of the code's return value could be: -``` -{ - "loss": 0.25, - "accuracy": 0.95 -} -``` -This example assumes that the _feedback attribute contains a dictionary with keys representing different metrics and their corresponding values. The actual structure and content of the feedback will depend on the specific implementation and use case within the project. -*** -### FunctionDef description(self) -**description**: The function of description is to return a textual description of the node. - -**parameters**: The parameters of this Function. -· None - -**Code Description**: The description function is a method that returns the value of the private attribute `_description` of the Node object. This function is straightforward and does not take any parameters. It simply accesses and returns the `_description` attribute, which is expected to hold a textual description of the node. - -This function is utilized in various parts of the project to retrieve the description of a node. For instance, in the `get_label` method of the `NodeVizStyleGuide` class, the `description` function is called to obtain the node's description, which is then used to generate a label for visualization purposes. The method ensures that the description does not exceed a certain length by truncating it if necessary. - -Similarly, in the `propagate` method of the `Propagator` class, the `description` function is used to get the node's description, which is then processed to determine the appropriate propagation behavior based on the operator name derived from the description. - -**Note**: This function assumes that the `_description` attribute is already set and contains a valid string. It does not perform any validation or modification of the description. - -**Output Example**: -If the `_description` attribute of a Node object is set to "This is a sample node description.", calling the `description` function will return: -"This is a sample node description." -*** -### FunctionDef info(self) -**info**: The function of info is to return the value of the `_info` attribute of the object. - -**parameters**: -- self: The object itself. - -**Code Description**: -The `info` function is a method of the `Node` class. It returns the value of the `_info` attribute of the object. The `_info` attribute is a private attribute that stores additional information about the node. - -The purpose of the `info` function is to provide access to the `_info` attribute, allowing users to retrieve any additional information associated with the node. - -This function does not take any arguments other than `self`, which refers to the object itself. By calling `info()` on a `Node` object, the function will return the value of the `_info` attribute. - -The `_info` attribute can be set by the user or by other functions within the code. It is typically used to store metadata or any other relevant information about the node. - -**Note**: -- The `info` function is a simple getter method that provides access to the `_info` attribute of the object. -- The `_info` attribute can be accessed directly, but it is recommended to use the `info` function for consistency and encapsulation. - -**Output Example**: -If the `_info` attribute of the object is set to `"This is a node"`, calling `info()` will return `"This is a node"`. -*** -### FunctionDef parameter_dependencies(self) -**parameter_dependencies**: The function of parameter_dependencies is to return the dependencies related to parameters within the Node object. - -**parameters**: This function does not take any parameters. - -**Code Description**: The parameter_dependencies function is a method within the Node class that retrieves and returns the parameter dependencies stored in the Node object. Specifically, it accesses the '_dependencies' attribute of the Node instance, which is a dictionary, and returns the value associated with the 'parameter' key. This value represents the set of dependencies that are related to the parameters of the Node. - -The function is utilized by the external_dependencies method in the MessageNode class. In this context, the external_dependencies method checks if the 'info' attribute of the MessageNode instance is a dictionary and if it contains an 'output' key that is an instance of Node. It then compares the length of the parameter dependencies of the 'output' Node with the parameter dependencies of the current MessageNode. If the 'output' Node has more parameter dependencies, it returns the difference between the two sets of dependencies. This indicates that the external_dependencies method relies on the parameter_dependencies function to determine the parameter dependencies of the Node instances it interacts with. - -**Note**: Ensure that the '_dependencies' attribute is properly initialized and contains a 'parameter' key with a corresponding value before calling the parameter_dependencies function to avoid potential KeyError exceptions. - -**Output Example**: A possible return value of the parameter_dependencies function could be a set of dependencies, such as: -``` -{'dependency1', 'dependency2', 'dependency3'} -``` -*** -### FunctionDef expandable_dependencies(self) -**expandable_dependencies**: The function of expandable_dependencies is to retrieve the 'expandable' dependencies from the Node object's internal dependencies dictionary. - -**parameters**: This function does not take any parameters. - -**Code Description**: The expandable_dependencies function is a method of the Node class. It accesses the Node object's internal dictionary, `_dependencies`, and returns the value associated with the key 'expandable'. This dictionary is assumed to store various types of dependencies, and the 'expandable' key specifically holds the dependencies that can be expanded. The function provides a straightforward way to access these expandable dependencies without directly interacting with the internal dictionary. - -**Note**: -- Ensure that the '_dependencies' dictionary is properly initialized and contains the 'expandable' key before calling this function to avoid potential KeyError exceptions. -- This function assumes that the 'expandable' key in the '_dependencies' dictionary holds a valid value that can be returned. - -**Output Example**: -If the '_dependencies' dictionary is structured as follows: -```python -self._dependencies = { - 'expandable': ['dependency1', 'dependency2'], - 'non_expandable': ['dependency3'] -} -``` -Calling `expandable_dependencies()` would return: -```python -['dependency1', 'dependency2'] -``` -*** -### FunctionDef _add_feedback(self, child, feedback) -**_add_feedback**: The function of _add_feedback is to add feedback from a child node to the current node. - -**parameters**: The parameters of this Function. -· child: The child node from which the feedback is received. -· feedback: The feedback data to be added. - -**Code Description**: The _add_feedback function is designed to manage feedback propagation in a node-based structure. It takes two parameters: 'child', which represents the child node providing the feedback, and 'feedback', which is the actual feedback data to be appended. The function appends the feedback to a list associated with the child node in the _feedback dictionary of the current node. - -In the context of its usage within the backward function, _add_feedback plays a crucial role in the feedback propagation mechanism. During the backward pass, feedback is propagated from child nodes to parent nodes. The backward function initializes the feedback for the current node and then propagates it to its parents. The _add_feedback function is called to append the propagated feedback from a child node to the current node's feedback list. This ensures that each node accumulates feedback from its children, which can then be used for further processing or analysis. - -**Note**: Points to note about the use of the code -- Ensure that the _feedback dictionary is properly initialized and that each child node has an associated list to append feedback to. -- The function assumes that the child node is already present in the _feedback dictionary. -- Proper handling of feedback data is essential to avoid issues during the feedback propagation process. -*** -### FunctionDef _set(self, value) -**_set**: The function of _set is to set the value of the node. If the value is a Node, it will be unwrapped. - -**parameters**: The parameters of this Function. -· value: The value to be set for the node. It can be of any type, including another Node. - -**Code Description**: The _set function is designed to assign a value to the node's internal data attribute. It first checks if the provided value is an instance of the Node class. If it is, the function retrieves the internal data of the Node by accessing its data attribute, effectively unwrapping the Node. This ensures that the node's internal data is set to the actual data contained within the provided Node, rather than the Node object itself. If the value is not a Node, it is directly assigned to the node's internal data attribute. This function is crucial for maintaining the integrity of the node's data, especially when dealing with nested Node objects. - -**Note**: This function assumes that the "_data" attribute exists within the node object. If this attribute is not present, an AttributeError will be raised. -*** -### FunctionDef backward(self, feedback, propagator, retain_graph, visualize, simple_visualization, reverse_plot, print_limit) -**backward**: The `backward` function is responsible for performing a backward pass in a computational graph. It propagates feedback from the current node to its parents, updates the graph visualization if required, and returns the resulting graph. - -**parameters**: -- `feedback`: An optional parameter that represents the feedback given to the current node. It can be of any type. -- `propagator`: An optional parameter that represents a function used to propagate feedback from a node to its parents. If not provided, a default `GraphPropagator` object is used. -- `retain_graph`: A boolean parameter that determines whether to retain the graph after the backward pass. If set to `True`, the graph will be retained; otherwise, it will be cleared. The default value is `False`. -- `visualize`: A boolean parameter that determines whether to plot the graph using graphviz. If set to `True`, the graph will be visualized; otherwise, it will not be plotted. The default value is `False`. -- `simple_visualization`: A boolean parameter that determines whether to simplify the visualization by bypassing chains of identity operators. If set to `True`, identity operators will be skipped in the visualization; otherwise, they will be included. The default value is `True`. -- `reverse_plot`: A boolean parameter that determines the order of the graph visualization. If set to `True`, the graph will be plotted in reverse order (from child to parent); otherwise, it will be plotted in the default order (from parent to child). The default value is `False`. -- `print_limit`: An integer parameter that sets the maximum number of characters to print in the graph visualization. If the description or content of a node exceeds this limit, it will be truncated. The default value is `100`. - -**Code Description**: -The `backward` function is a method of the current object. It performs a backward pass in a computational graph by propagating feedback from the current node to its parents. The function takes several parameters to control the behavior of the backward pass. - -The `feedback` parameter represents the feedback given to the current node. It can be of any type and is used to initialize the feedback mechanism of the node. The `propagator` parameter is an optional function that is used to propagate feedback from a node to its parents. If not provided, a default `GraphPropagator` object is used, which implements specific methods for feedback propagation. The `retain_graph` parameter determines whether to retain the graph after the backward pass. If set to `True`, the graph will be retained; otherwise, it will be cleared. The `visualize` parameter determines whether to plot the graph using graphviz. If set to `True`, the graph will be visualized; otherwise, it will not be plotted. The `simple_visualization` parameter determines whether to simplify the visualization by bypassing chains of identity operators. If set to `True`, identity operators will be skipped in the visualization; otherwise, they will be included. The `reverse_plot` parameter determines the order of the graph visualization. If set to `True`, the graph will be plotted in reverse order (from child to parent); otherwise, it will be plotted in the default order (from parent to child). The `print_limit` parameter sets the maximum number of characters to print in the graph visualization. If the description or content of a node exceeds this limit, it will be truncated. - -The function first checks if a `propagator` object is provided. If not, it imports the `GraphPropagator` class from the `opto.trace.propagators.graph_propagator` module. It then initializes the `propagator` object if it is not provided. - -Next, the function sets up the visualization by creating a `digraph` object and a `NodeVizStyleGuide` object. These objects are used to plot the graph using graphviz and define the style of the nodes in the graph. - -The function checks if the current node has already been backwarded. If it has, an `AttributeError` is raised. Otherwise, the function adds the feedback to the current node by calling the `_add_feedback` method of the node object. The feedback is initialized with a special "FEEDBACK_ORACLE" node and the propagated feedback from the `propagator` object. - -If the current node has no parents, indicating that it is a root node, the function checks if visualization is enabled. If it is, the current node is added to the `digraph` object with the appropriate style attributes. Finally, the function returns the `digraph` object. - -If the current node has parents, indicating that it is not a root node, the function initializes a priority queue called `queue` using the `MinHeap` class. The priority queue is used to process the nodes in the correct order during the backward pass. - -The function enters a loop that continues until the `queue` is empty. In each iteration, a node is popped from the `queue` and processed. The node is checked to ensure it has parents and is an instance of the `MessageNode` class. If not, an `AttributeError` is raised. - -The function propagates information from the current node to its parents by calling the `propagator` object with the current node as the argument. The `propagator` object computes the propagated feedback based on the child node's description, data, and feedback. The propagated feedback is then added to the parents of the current node by calling the `_add_feedback` method of each parent node. - -The function checks if visualization is enabled. If it is, the function plots the edge from each parent to the current node in the `digraph` object. It also handles the visualization of identity operators by bypassing chains of identity operators if the `simple_visualization` parameter is set to `True`. - -After processing the parents of the current node, the `_backwarded` attribute of the current node is updated to indicate that it has been backwarded. This attribute is set to `True` unless the `retain_graph` parameter is set to `True`. - -The loop continues until the `queue` is empty, indicating that all the nodes have been processed. Finally, the function returns the `digraph` object. - -**Note**: -- The `backward` function is a crucial part of the backward pass in a computational graph. It propagates feedback from child nodes to parent nodes, updates the graph visualization if required, and returns the resulting graph. -- The `feedback` parameter is used to initialize the feedback mechanism of the current node. It can be of any type and is specific to the application. -- The `propagator` parameter allows for customization of the feedback propagation process. If not provided, a default `GraphPropagator` object is used. -- The `retain_graph` parameter determines whether to retain the graph after the backward pass. This can be useful for further analysis or visualization. -- The `visualize` parameter allows for visualization of the graph using graphviz. This can be helpful for understanding the structure of the graph. -- The `simple_visualization` parameter simplifies the visualization by bypassing chains of identity operators. This can improve the clarity of the graph. -- The `reverse_plot` parameter determines the order of the graph visualization. This can be useful for visualizing the graph from child to parent, which may be more intuitive in some cases. -- The `print_limit` parameter sets a limit on the number of characters to print in the graph visualization. This can prevent the visualization from becoming too cluttered or overwhelming. - -**Output Example**: -If the current node has two parents and visualization is enabled, the `backward` function will return a `digraph` object representing the graph with the appropriate edges and node styles. -*** -### FunctionDef clone(self) -**clone**: The function of clone is to create and return a duplicate of the current Node object. - -**parameters**: The parameters of this Function. -· This function does not take any parameters other than the implicit self parameter, which refers to the instance of the Node class. - -**Code Description**: The clone function is a method of the Node class that imports the clone function from the opto.trace.operators module and applies it to the current instance (self) of the Node class. The imported clone function from the operators module is responsible for creating a duplicate of the Node instance. This method ensures that the Node object can be cloned using a standardized operation defined in the operators module. - -The clone function is also indirectly referenced by the identity function in the opto.trace.operators module. The identity function calls the clone method on its input parameter, effectively creating a duplicate of the input object. This demonstrates that the clone method is integral to operations that require object duplication within the project. - -**Note**: -- Ensure that the opto.trace.operators module is correctly imported and accessible when using the clone method. -- The clone method does not modify the original Node object; it only creates and returns a duplicate. - -**Output Example**: The return value of the clone function will be a new instance of the Node class that is a duplicate of the original instance. For example, if the original Node instance has specific attributes and states, the cloned instance will have the same attributes and states. -*** -### FunctionDef detach(self) -**detach**: The function of detach is to create and return a deep copy of the current instance of the Node class. - -**parameters**: The parameters of this Function. -· This function does not take any parameters. - -**Code Description**: The detach function is designed to create a deep copy of the current instance of the Node class. When this function is called, it utilizes the deepcopy method from the copy module to generate a new instance of the Node class that is a complete copy of the original, including all nested objects. This ensures that any changes made to the new instance do not affect the original instance, and vice versa. The function then returns this new deep-copied instance. - -**Note**: -- Ensure that the copy module is imported before using this function. -- This function does not modify the original instance; it only creates and returns a new deep-copied instance. - -**Output Example**: -If the original instance of the Node class has certain attributes and nested objects, calling the detach function will return a new instance with identical attributes and nested objects, but completely independent of the original instance. For example: - -```python -original_node = Node() -detached_node = original_node.detach() -# detached_node is a deep copy of original_node -``` -*** -### FunctionDef getattr(self, key) -**getattr**: The function of getattr is to get the value of the specified attribute from the given object. - -**parameters**: -- self: The object from which the attribute value is to be retrieved. -- key: A string representing the name of the attribute to be retrieved. - -**Code Description**: -The `getattr` function is a method of the `Node` class in the `opto.trace.nodes.py` module. It takes in the `self` object, which is an instance of the `Node` class, and a string `key` as parameters. - -The function first imports the `node_getattr` function from the `opto.trace.operators` module. It then calls the `node_getattr` function passing itself (`self`) and the specified attribute (`key`) as arguments. The `node_getattr` function is responsible for retrieving the value of the specified attribute from the `Node` object. - -The `getattr` method is used to access the attributes of the `Node` object. It is called when the `getattr` function is invoked on a `Node` object. The `getattr` method retrieves the value of the specified attribute from the `Node` object by calling the `node_getattr` function. - -**Note**: -- The `getattr` method assumes that the `self` parameter is a valid `Node` object. -- If the `self` object does not have the specified attribute, a `AttributeError` will be raised. - -**Output Example**: -A possible return value of the `getattr` method could be the value of the specified attribute from the `Node` object. -*** -### FunctionDef call(self, fun) -**call**: The function of call is to invoke a specified function with the given arguments and keyword arguments. - -**parameters**: -- self: The object on which the function is called. -- fun: A string representing the name of the function to be invoked. -- *args: Variable-length positional arguments to be passed to the function. -- **kwargs: Variable-length keyword arguments to be passed to the function. - -**Code Description**: -The `call` function is a method of the `Node` class in the `opto.trace.nodes.py` module. It takes in the `self` object, which is an instance of the `Node` class, a string `fun`, and variable-length positional and keyword arguments (`args` and `kwargs`) as parameters. - -The function first iterates over the `args` and converts each argument to a `Node` object using the `node` function. This is done to ensure that all arguments passed to the function are `Node` objects. The converted arguments are then stored in a generator expression. - -Next, the function iterates over the `kwargs` and converts each value to a `Node` object using the `node` function. The converted values are then stored in a dictionary comprehension, with the keys being the original keys from `kwargs`. - -Finally, the function calls the `getattr` method of the `self` object, passing the `fun` string as the attribute name. The `getattr` method retrieves the value of the specified attribute from the `self` object. The retrieved attribute is then invoked as a function, passing the converted `args` and `kwargs` as arguments. - -The `call` method is used to dynamically invoke functions on the `Node` object. It allows for flexible and dynamic function calls based on the provided arguments and keyword arguments. - -**Note**: -- The `fun` parameter should be a string representing the name of a valid function that can be invoked on the `self` object. -- The `args` and `kwargs` parameters can be any valid arguments that can be passed to the specified function. -- The `call` method assumes that the `self` parameter is a valid `Node` object with the specified function as an attribute. - -**Output Example**: A possible return value of the `call` method could be the result of invoking the specified function with the provided arguments and keyword arguments. -*** -### FunctionDef __call__(self) -**__call__**: The function of __call__ is to invoke the `call` function from the `opto.trace.operators` module with the provided arguments and keyword arguments. - -**parameters**: The parameters of this function. -· `*args`: Variable-length argument list. -· `**kwargs`: Keyword arguments. - -**Code Description**: The `__call__` method is designed to facilitate the invocation of a function encapsulated within a Node object. When this method is called, it imports the `call` function from the `opto.trace.operators` module. The `call` function is then executed with the current instance (`self`) and any additional arguments (`*args`) and keyword arguments (`**kwargs`) provided to the `__call__` method. - -The `call` function, as defined in the `opto.trace.operators` module, takes a Node object representing the function to be called, along with any positional and keyword arguments. It ensures that the function encapsulated within the Node object is callable and then invokes it with the provided arguments. The result of this invocation is returned as the output. - -By using the `__call__` method, the Node object can be used as if it were a regular callable function, providing a seamless interface for function invocation. - -**Note**: -- The Node object must encapsulate a callable function. -- The `*args` parameter can accept any number of positional arguments. -- The `**kwargs` parameter can accept any number of keyword arguments. - -**Output Example**: -If the Node object encapsulates a function defined as follows: -```python -def add(a, b): - return a + b -``` -and the `__call__` method is invoked with `args=(2, 3)`, the output will be `5`. -*** -### FunctionDef len(self) -**len**: The function of len is to return the length of the Node instance. - -**parameters**: The parameters of this Function. -· self: The Node instance whose length is to be calculated. - -**Code Description**: The len method is a member of the Node class in the opto.trace.nodes module. This method is designed to compute and return the length of the Node instance. When invoked, the len method imports the len_ function from the opto.trace.operators module and applies it to the Node instance (self). The len_ function is a utility that leverages Python's built-in len() function to determine the length of the input object. By using the len_ function, the len method ensures a consistent and modular approach to length calculation within the project. This design promotes reusability and maintainability, as the len_ function can be utilized across different parts of the project. - -**Note**: Ensure that the Node instance supports the len() operation. Passing an unsupported type will result in a TypeError. - -**Output Example**: -- If the Node instance represents a list [1, 2, 3], len(self) will return 3. -- If the Node instance represents a string "hello", len(self) will return 5. -*** -### FunctionDef __getitem__(self, key) -**__getitem__**: The function of __getitem__ is to retrieve an element from a Node instance using a specified key. - -**parameters**: The parameters of this function. -· key: The key used to access the element within the Node instance. - -**Code Description**: The __getitem__ method is designed to facilitate element retrieval from a Node instance using a specified key. When this method is called, it first imports the getitem function from the opto.trace.operators module. It then uses the node function to create a Node object from the provided key. Finally, it calls the getitem function with the current Node instance (self) and the newly created Node object (from the key) as arguments. This modular approach allows for flexible and reusable element retrieval within the Node class. - -The node function is responsible for creating a Node object from a given message. If the message is already a Node, it returns the message as is. This function simplifies the creation of Node objects and ensures consistency in how Nodes are instantiated. - -The getitem function is a straightforward implementation of the indexing operation. It takes an object and an index as parameters and returns the element located at the specified index within the object. In this context, the getitem function is used to retrieve an element from the Node instance using the key provided to the __getitem__ method. - -**Note**: -- Ensure that the key provided is compatible with the indexing mechanism of the Node instance. -- The node function should be used to create Node objects instead of directly invoking the Node class. - -**Output Example**: If a Node instance contains a list [10, 20, 30] and the key provided is 1, the return value of the __getitem__ method will be 20. -*** -### FunctionDef __contains__(self, item) -**__contains__**: The function of __contains__ is to determine if a given item is part of the Node instance. - -**parameters**: The parameters of this Function. -· item: The element to be checked for presence within the Node instance. - -**Code Description**: The __contains__ method is a special method in Python that allows the use of the `in` operator to check for membership within an object. In this context, the __contains__ method is part of the Node class in the opto\trace\nodes.py module. - -When the __contains__ method is called, it first imports the `in_` function from the opto.trace.operators module. The `in_` function is designed to determine whether an element `x` is present within a collection `y`. - -Next, the __contains__ method converts the `item` into a Node object using the `node` function. The `node` function is responsible for creating a Node object from a given message. If the message is already a Node, it returns the message as is. This ensures that the `item` is always in the form of a Node object before performing the membership test. - -Finally, the __contains__ method calls the `in_` function with the Node-converted `item` and the Node instance (`self`) as arguments. The `in_` function then checks if the `item` is present within the Node instance and returns a boolean value indicating the result. - -**Note**: -- The `item` parameter must be convertible to a Node object using the `node` function. -- The Node instance (`self`) must support the membership test operation. - -**Output Example**: -- If `item` is a Node object that is part of the Node instance, the method will return True. -- If `item` is not part of the Node instance, the method will return False. -*** -### FunctionDef __pos__(self) -**__pos__**: The function of __pos__ is to return the unary positive of the Node instance. - -**parameters**: The parameters of this Function. -· self: Refers to the instance of the Node class on which the unary positive operator is applied. - -**Code Description**: The __pos__ method is a special method in Python that is invoked when the unary positive operator (+) is used on an instance of the Node class. When this operator is applied, the __pos__ method is called, which in turn imports the pos function from the opto.trace.operators module. The pos function is then called with the Node instance (self) as its argument. The pos function applies the unary positive operator to the input value and returns it. In this context, the unary positive operator does not alter the value of the Node instance; it simply returns the instance itself. This ensures that the unary positive operation is consistently applied to instances of the Node class. - -**Note**: -- The __pos__ method does not modify the Node instance; it simply returns it. -- Ensure that the Node class instances are of a type that supports the unary positive operator. - -**Output Example**: -If the Node instance is node_instance, the return value will be node_instance when +node_instance is used. -*** -### FunctionDef __neg__(self) -**__neg__**: The function of __neg__ is to return the negation of the Node instance. - -**parameters**: The parameters of this Function. -· self: The instance of the Node class to be negated. - -**Code Description**: The __neg__ method is a special method in Python that is invoked when the unary negation operator (-) is applied to an instance of the Node class. This method imports the neg function from the opto.trace.operators module and applies it to the Node instance (self). The neg function, in turn, returns the negation of its input value using the unary negation operator (-). Therefore, when the __neg__ method is called, it effectively negates the Node object by leveraging the neg function. - -**Note**: Ensure that the Node instance supports the unary negation operator to avoid runtime errors. - -**Output Example**: If the Node instance represents a value of 5, applying the unary negation operator will result in -5. If the Node instance represents a value of -3.2, applying the unary negation operator will result in 3.2. -*** -### FunctionDef __abs__(self) -**__abs__**: The function of __abs__ is to return the absolute value of the Node instance. - -**parameters**: The parameters of this Function. -· self: The instance of the Node class on which the __abs__ method is called. - -**Code Description**: The __abs__ method is a special method in Python that is called when the built-in abs() function is used on an instance of the Node class. When invoked, this method imports the abs function from the opto.trace.operators module and applies it to the Node instance (self). The imported abs function is designed to compute the absolute value of its input, leveraging Python's built-in abs() function. This allows the Node class to utilize the abs function to compute and return the absolute value of its instances. - -**Note**: -- Ensure that the Node instance supports the absolute value operation, either directly or through a custom implementation of the __abs__ method. -- The behavior and limitations of this method are consistent with Python's built-in abs() function. - -**Output Example**: -- If the Node instance represents a value of -5, the __abs__ method will return 5. -- If the Node instance represents a value of 3.14, the __abs__ method will return 3.14. -- If the Node instance is a custom object that implements the __abs__ method, the __abs__ method will return the result of that custom implementation. -*** -### FunctionDef __invert__(self) -**__invert__**: The function of __invert__ is to perform a bitwise NOT operation on the instance of the Node class. - -**parameters**: The parameters of this Function. -· self: The instance of the Node class on which the bitwise NOT operation will be performed. - -**Code Description**: The __invert__ method is a special method in Python that allows the use of the bitwise NOT operator (~) on an instance of the Node class. When the ~ operator is applied to a Node instance, the __invert__ method is invoked. This method imports the invert function from the opto.trace.operators module and applies it to the instance (self). - -The invert function, defined in the opto.trace.operators module, takes a single parameter x and returns the result of applying the bitwise NOT operation to x. The bitwise NOT operation inverts each bit of the input value. For example, if x is an integer, each bit in its binary representation will be flipped (0s become 1s and 1s become 0s). - -In this context, the __invert__ method enables the Node class to support the bitwise NOT operation by leveraging the invert function. This allows developers to use the ~ operator directly on Node instances, making the code more intuitive and concise. - -**Note**: Ensure that the Node instance supports the bitwise NOT operation. Using types that do not support this operation will result in a TypeError. - -**Output Example**: -- If the Node instance represents an integer with a value of 5, the return value will be -6. -- If the Node instance represents an integer with a value of 0, the return value will be -1. -*** -### FunctionDef __round__(self, n) -**__round__**: The function of __round__ is to round the value of the Node object to a specified number of decimal places. - -**parameters**: The parameters of this function. -· n: The number of decimal places to round to. This parameter is optional and can be None. - -**Code Description**: The __round__ method is a special method in the Node class that allows rounding the value of the Node object to a specified number of decimal places. It imports the round function from the opto.trace.operators module and applies it to the Node instance (self). If the parameter n is provided, it is converted into a Node object using the node function from the same module. If n is not provided (i.e., it is None), the round function is called with None as the second argument. - -The method works as follows: -1. It imports the round function from the opto.trace.operators module. -2. It checks if the parameter n is provided. -3. If n is provided, it converts n into a Node object using the node function. -4. It calls the round function with the Node instance (self) and the converted n (or None if n is not provided). -5. It returns the result of the round function. - -The relationship with its callees is as follows: -- The node function is used to convert the parameter n into a Node object if n is provided. -- The round function is used to perform the actual rounding operation on the Node instance. - -**Note**: -- Ensure that the parameter n, if provided, can be interpreted as an integer to avoid runtime errors. -- The method relies on the round function from the opto.trace.operators module, which is a wrapper around Python's built-in round function. - -**Output Example**: -If the Node instance represents the value 3.14159 and n is 2, the method will return a Node object representing the value 3.14. -If the Node instance represents the value 3.14159 and n is 0, the method will return a Node object representing the value 3. -*** -### FunctionDef __floor__(self) -**__floor__**: The function of __floor__ is to compute the largest integer less than or equal to the value of the current Node instance. - -**parameters**: The parameters of this Function. -· self: An instance of the Node class. - -**Code Description**: The __floor__ method is a special method in the Node class that allows instances of Node to be floored directly. When this method is called, it imports the floor function from the opto.trace.operators module and applies it to the current instance (self). The floor function, in turn, computes the largest integer less than or equal to the given number using Python's math.floor method. This operation is useful for rounding down the value of the Node instance to the nearest whole number. - -**Note**: Ensure that the Node instance holds a numeric value that can be floored. If the value is not numeric, the floor function will raise a TypeError. Additionally, the math module must be available in the environment for the floor function to work correctly. - -**Output Example**: -- If the Node instance has a value of 3.7, calling __floor__() will return 3. -- If the Node instance has a value of -2.3, calling __floor__() will return -3. -*** -### FunctionDef __ceil__(self) -**__ceil__**: The function of __ceil__ is to return the smallest integer greater than or equal to the value represented by the Node instance. - -**parameters**: The parameters of this Function. -· self: An instance of the Node class. - -**Code Description**: The __ceil__ method is a special method in the Node class that provides a ceiling operation on the Node instance. When invoked, it imports the ceil function from the opto.trace.operators module and applies it to the Node instance (self). The ceil function, in turn, rounds up the numeric value represented by the Node instance to the nearest integer. This method leverages the functionality of the ceil function to ensure that the Node instance's value is rounded up correctly. - -The ceil function, which is called within __ceil__, is designed to handle any numeric type and uses the math.ceil() method from the math module to perform the rounding operation. By importing and utilizing this function, the __ceil__ method ensures that the Node instance's value is processed accurately and efficiently. - -**Note**: Ensure that the Node instance represents a numeric value; otherwise, the ceil function will raise a TypeError. The math module must be available in the environment where the code is executed. - -**Output Example**: -- If the Node instance represents the value 4.2, __ceil__() will return 5. -- If the Node instance represents the value -3.7, __ceil__() will return -3. -- If the Node instance represents the value 7, __ceil__() will return 7. -*** -### FunctionDef __trunc__(self) -**__trunc__**: The function of __trunc__ is to truncate the decimal part of a Node object, returning its integer part. - -**parameters**: The parameters of this Function. -· self: The instance of the Node class that is to be truncated. - -**Code Description**: The __trunc__ method is a special method in the Node class that allows instances of Node to be truncated to their integer representation. When __trunc__ is called on a Node instance, it imports the trunc function from the opto.trace.operators module and applies it to the instance (self). The trunc function, in turn, utilizes Python's math.trunc function to truncate the decimal part of the number, returning only the integer part. This ensures that any Node object can be converted to its integer form when necessary. - -**Note**: -- The Node instance should be compatible with the math.trunc function, typically meaning it should represent a numerical value. -- If the Node instance does not represent a number, the trunc function will raise a TypeError. - -**Output Example**: -If a Node instance represents the value 3.14, calling __trunc__ on this instance will return 3. -If a Node instance represents the value -2.99, calling __trunc__ on this instance will return -2. -*** -### FunctionDef __add__(self, other) -**__add__**: The function of __add__ is to define the addition operation for Node objects, allowing them to be combined with other values. - -**parameters**: The parameters of this function. -· self: The current instance of the Node class. -· other: The value to be added to the current Node instance. This can be of any type. - -**Code Description**: The __add__ method in the Node class is designed to handle the addition of a Node object with another value. It first imports the necessary operators from the opto.trace.operators module. The method then checks the type of the _data attribute of the Node instance. If _data is a string, it uses the concat function from the operators module to concatenate the current Node instance with another Node instance created from the other parameter. If _data is not a string, it uses the add function from the operators module to add the current Node instance to another Node instance created from the other parameter. - -The node function is used to ensure that the other parameter is converted into a Node object if it is not already one. This function provides a convenient way to create Node objects from various types of messages, ensuring consistency and ease of use. - -The __add__ method is also called by the __radd__ method in the Node class, which allows for the reverse addition operation. This means that if the other parameter is on the left side of the addition operation, the __radd__ method will be invoked, which in turn calls the __add__ method to perform the addition. - -**Note**: -- Ensure that the types of the _data attribute and the other parameter are compatible with the + operator to avoid runtime errors. -- The behavior of the + operator varies depending on the types of the operands. For example, it concatenates strings and lists but adds numbers. - -**Output Example**: -- If self._data is "Hello" and other is "World", the return value will be a Node object with _data "HelloWorld". -- If self._data is 3 and other is 5, the return value will be a Node object with _data 8. -*** -### FunctionDef __radd__(self, other) -**__radd__**: The function of __radd__ is to handle the reverse addition operation for Node objects, allowing them to be combined with other values when the Node instance is on the right side of the addition. - -**parameters**: The parameters of this function. -· self: The current instance of the Node class. -· other: The value to be added to the current Node instance. This can be of any type. - -**Code Description**: The __radd__ method in the Node class is designed to facilitate the addition operation when the Node instance appears on the right side of the addition operator. This method is invoked when the left operand does not support the addition operation with the right operand, which is an instance of the Node class. The __radd__ method simply calls the __add__ method of the Node class, passing the other parameter to it. This ensures that the addition logic defined in the __add__ method is reused, maintaining consistency in how Node objects are combined with other values. - -The __add__ method, which is called by __radd__, handles the addition by checking the type of the _data attribute of the Node instance. If _data is a string, it concatenates the current Node instance with another Node instance created from the other parameter using the concat function from the opto.trace.operators module. If _data is not a string, it adds the current Node instance to another Node instance created from the other parameter using the add function from the same module. The node function ensures that the other parameter is converted into a Node object if it is not already one. - -**Note**: -- Ensure that the types of the _data attribute and the other parameter are compatible with the + operator to avoid runtime errors. -- The behavior of the + operator varies depending on the types of the operands. For example, it concatenates strings and lists but adds numbers. - -**Output Example**: -- If self._data is "Hello" and other is "World", the return value will be a Node object with _data "HelloWorld". -- If self._data is 3 and other is 5, the return value will be a Node object with _data 8. -*** -### FunctionDef __sub__(self, other) -**__sub__**: The function of __sub__ is to perform a subtraction operation between the current Node object and another operand. - -**parameters**: The parameters of this function. -· self: The current instance of the Node object. -· other: The operand to be subtracted from the current Node object. This operand can be any type that can be converted into a Node object. - -**Code Description**: The __sub__ method is designed to enable the use of the subtraction operator (-) between Node objects or between a Node object and another operand. When the subtraction operator is used, this method is invoked. The method first imports the subtract function from the opto.trace.operators module. It then calls the node function from the opto.trace.nodes module to ensure that the operand 'other' is converted into a Node object if it is not already one. Finally, it calls the subtract function with the current Node object (self) and the newly created Node object from the operand 'other'. The subtract function performs the actual subtraction operation and returns the result. - -**Note**: -- Ensure that the operand 'other' is of a type that can be converted into a Node object to avoid runtime errors. -- The node function is used to handle the conversion of the operand into a Node object, providing flexibility in the types of operands that can be used with the subtraction operator. - -**Output Example**: -- If self is a Node object representing the value 10 and other is a Node object representing the value 5, the __sub__ method will return a Node object representing the value 5. -- If self is a Node object representing a list [1, 2, 3] and other is a Node object representing a list [1, 1, 1], the __sub__ method will return a Node object representing the list [0, 1, 2] (assuming the subtraction operation is defined for lists in this context). -*** -### FunctionDef __mul__(self, other) -**__mul__**: The function of __mul__ is to enable the multiplication operation for Node objects using the * operator. - -**parameters**: The parameters of this function. -· self: The current instance of the Node object. -· other: The operand to be multiplied with the current Node instance. This can be any type that is compatible with the multiplication operation. - -**Code Description**: The __mul__ method allows for the multiplication of a Node object with another operand. When the * operator is used with a Node instance, this method is invoked. It imports the multiply function from the opto.trace.operators module and the node function from the opto.trace.nodes module. - -The method first converts the other operand into a Node object using the node function. This ensures that the operand is in a compatible format for the multiplication operation. The node function checks if the operand is already a Node and returns it as is if true. Otherwise, it creates a new Node object from the operand. - -After converting the operand, the method calls the multiply function with the current Node instance (self) and the newly created Node object as arguments. The multiply function performs the multiplication operation and returns the result. - -This design allows for seamless multiplication of Node objects or Node-compatible objects using the * operator, enhancing the flexibility and usability of the Node class. - -**Note**: Ensure that the operand passed to the * operator is compatible with the multiplication operation to avoid runtime errors. If the operand does not support multiplication, a TypeError will be raised. - -**Output Example**: If self is a Node object representing the value 3 and other is 4, the result of self * other will be a Node object representing the value 12. -*** -### FunctionDef __floordiv__(self, other) -**__floordiv__**: The function of __floordiv__ is to perform floor division between a Node object and another operand. - -**parameters**: The parameters of this function. -· self: The Node object on which the floor division operation is invoked. -· other: The operand with which the floor division is to be performed. This can be any type that supports the floor division operation. - -**Code Description**: The __floordiv__ method is a special method in the Node class that enables the use of the floor division operator (//) between a Node object and another operand. When this method is called, it imports the floor_divide function from the opto.trace.operators module and the node function from the opto.trace.nodes module. - -The method first converts the other operand into a Node object using the node function. This ensures that the operand is compatible with the Node class's operations. It then applies the floor_divide function to the Node object (self) and the newly created Node object (other). The floor_divide function performs the floor division operation, which divides the two operands and rounds down the result to the nearest integer. - -This method ensures that the floor division operation is performed correctly and consistently within the project's framework by leveraging the floor_divide function. The use of the node function guarantees that the other operand is appropriately handled as a Node object, maintaining the integrity of the Node class's operations. - -**Note**: Ensure that the other operand is of a type that supports the floor division operation to avoid runtime errors. The method relies on the floor_divide function, which does not perform type checking or validation, so improper types may lead to unexpected behavior or exceptions. - -**Output Example**: If self is a Node object representing the value 7 and other is an operand representing the value 3, the method call self // other will return a Node object representing the value 2, as 7 // 3 equals 2. -*** -### FunctionDef __truediv__(self, other) -**__truediv__**: The function of __truediv__ is to perform division between the current Node instance and another operand. - -**parameters**: The parameters of this function. -· self: The current instance of the Node class. -· other: The operand to divide the current Node instance by. This can be any type that supports division. - -**Code Description**: The __truediv__ method is designed to handle the division operation for Node objects. When the division operator (/) is used between a Node instance and another operand, this method is invoked. The method first imports the divide function from the opto.trace.operators module. It then converts the other operand into a Node object using the node function from the opto.trace.nodes module. This ensures that both operands are Node objects, maintaining consistency within the framework. Finally, the method returns the result of the divide function, which performs the actual division operation between the two Node objects. - -**Note**: -- Ensure that the divisor (other) is not zero to avoid a ZeroDivisionError. -- The other operand should be of a type that supports the division operation. -- The node function is used to convert the other operand into a Node object if it is not already one, ensuring compatibility within the Node framework. - -**Output Example**: If the current Node instance represents the value 10 and the other operand represents the value 2, the method will return a Node object representing the value 5.0. -*** -### FunctionDef __mod__(self, other) -**__mod__**: The function of __mod__ is to perform the modulo operation between the current Node object and another value. - -**parameters**: The parameters of this function. -· other: The value to be used as the divisor in the modulo operation. It can be of any type that supports the modulo operation. - -**Code Description**: The __mod__ method is designed to enable the modulo operation between a Node object and another value. When this method is called, it first imports the mod function from the opto.trace.operators module. It then calls the node function to ensure that the other value is converted into a Node object if it is not already one. Finally, it applies the mod function to the current Node object (self) and the converted Node object (node(other)), and returns the result. - -The node function is responsible for creating a Node object from a given message. If the message is already a Node, it returns it as is. This ensures that the other value is always in the form of a Node object before the modulo operation is performed. - -The mod function takes two parameters, x and y, and returns the result of the modulo operation (x % y). This operation finds the remainder when x is divided by y. By integrating the mod function with the __mod__ method, Node objects can seamlessly perform the modulo operation with other values, enhancing their arithmetic capabilities. - -**Note**: Ensure that the other value provided is of a type that supports the modulo operation to avoid runtime errors. - -**Output Example**: If the current Node object represents the value 10 and the other value is 3, the return value will be a Node object representing the value 1. If the current Node object represents the value 20 and the other value is 7, the return value will be a Node object representing the value 6. -*** -### FunctionDef __divmod__(self, other) -**__divmod__**: The function of __divmod__ is to perform the divmod operation on a Node object and another operand, returning the result. - -**parameters**: The parameters of this function. -· self: The Node instance on which the __divmod__ method is called. -· other: The operand to be used in the divmod operation with the Node instance. - -**Code Description**: The __divmod__ method is designed to enable the use of the divmod operation on Node objects within the project. When this method is called, it first imports the divmod function from the opto.trace.operators module and the node function from the opto.trace.nodes module. The method then converts the other operand into a Node object using the node function. This ensures that the divmod operation is performed between two Node objects, maintaining consistency within the project's framework. - -The core functionality of the __divmod__ method is to delegate the actual divmod operation to the divmod function imported from opto.trace.operators. This function takes two parameters, x and y, and applies Python's built-in divmod function to them, returning a tuple containing the quotient and the remainder. By using this approach, the __divmod__ method ensures that the divmod operation can be seamlessly integrated with Node objects, providing a consistent interface for performing division and modulus operations within the project's tracing framework. - -**Note**: Ensure that the other operand is of a type that can be converted into a Node object to avoid runtime errors. The method relies on the node function to handle this conversion, so any constraints or behaviors of the node function will apply here as well. - -**Output Example**: If the Node instance represents the value 10 and the other operand is 3, the return value will be a tuple (3, 1), where 3 is the quotient and 1 is the remainder. -*** -### FunctionDef __pow__(self, other) -**__pow__**: The function of __pow__ is to enable the power operation (exponentiation) on Node objects. - -**parameters**: The parameters of this function. -· self: The Node object on which the power operation is being performed. -· other: The exponent value, which can be of any type that supports the power operation. - -**Code Description**: The __pow__ method allows for the use of the power operator (**) directly on Node objects. When this method is called, it imports the power function from the opto.trace.operators module and applies it to the Node object (self) and the other value (other). - -The method first imports the necessary operators from the opto.trace.operators module. It then calls the power function, passing in the current Node object (self) and the result of the node function applied to the other value. The node function ensures that the other value is converted into a Node object if it is not already one, providing a consistent interface for the power operation. - -This integration allows for intuitive mathematical operations within the project's framework, enabling users to perform exponentiation on Node objects seamlessly. - -**Note**: -- Ensure that the types of self and other are compatible with the power operation to avoid runtime errors. -- The node function is used to convert the other value into a Node object if it is not already one, ensuring consistency in the operation. - -**Output Example**: -If self is a Node object representing the value 2 and other is 3, the function will return a Node object representing the value 8, as 2**3 equals 8. -*** -### FunctionDef __lshift__(self, other) -**__lshift__**: The function of __lshift__ is to perform a left bitwise shift operation on a Node object using another operand. - -**parameters**: The parameters of this function. -· self: The current instance of the Node class. -· other: The operand to be used for the left bitwise shift operation. - -**Code Description**: The __lshift__ method in the Node class is designed to facilitate the left bitwise shift operation using the << operator. When this method is invoked, it imports the lshift function from the opto.trace.operators module and the node function from the same module where the Node class is defined. The method then calls the lshift function, passing the current Node instance (self) and the result of the node function applied to the other operand. - -The node function ensures that the other operand is converted into a Node object if it is not already one. This conversion is crucial for maintaining consistency within the Node class operations. The lshift function then performs the left bitwise shift operation on the two Node objects, self and the converted other operand, and returns the result. - -This method allows instances of the Node class to use the << operator for left bitwise shift operations, leveraging the underlying lshift function to handle the actual bitwise manipulation. - -**Note**: -- Ensure that the other operand is of a type that can be converted into a Node object using the node function. -- The left bitwise shift operation is typically used with integer values, so the operands should support this operation to avoid runtime errors. - -**Output Example**: -If the current Node instance represents the value 4 (binary 100) and the other operand is 2, the method will return a Node object representing the value 16 (binary 10000), as the bits of 4 are shifted left by 2 positions. -*** -### FunctionDef __rshift__(self, other) -**__rshift__**: The function of __rshift__ is to perform a bitwise right shift operation on the current Node instance and another operand. - -**parameters**: The parameters of this function. -· self: The current instance of the Node class. -· other: The operand to be right-shifted with the current Node instance. - -**Code Description**: The __rshift__ method is a special method in the Node class that facilitates the bitwise right shift operation between the current Node instance (self) and another operand (other). This method first imports the rshift function from the opto.trace.operators module. It then calls this rshift function, passing the current Node instance (self) and the result of the node function applied to the other operand. - -The node function is used to ensure that the other operand is converted into a Node object if it is not already one. This conversion is necessary to maintain consistency and compatibility within the Node class operations. The rshift function, once called, performs the bitwise right shift operation (x >> y) on the two operands. - -**Note**: -- Ensure that the other operand is of a type that supports the right shift operation to avoid runtime errors. -- The node function is used to convert the other operand into a Node object if it is not already one, ensuring compatibility within the Node class operations. - -**Output Example**: If the current Node instance represents the value 8 (binary 1000) and the other operand is 2, the __rshift__ method will return a Node object representing the value 2 (binary 10). -*** -### FunctionDef __and__(self, other) -**__and__**: The function of __and__ is to perform a bitwise AND operation between the current Node object and another operand. - -**parameters**: The parameters of this function. -· self: The current instance of the Node object. -· other: The operand to perform the bitwise AND operation with. This can be any type that supports the bitwise AND operation. - -**Code Description**: The __and__ method is designed to facilitate the bitwise AND operation between a Node object and another operand. When this method is called, it first imports the necessary operators from the `opto.trace.operators` module. Specifically, it imports the `and_` function, which is responsible for executing the bitwise AND operation. - -The method then calls the `node` function from the `opto.trace.nodes` module to ensure that the `other` operand is converted into a Node object if it is not already one. The `node` function is a utility that either returns the operand as a Node object or creates a new Node object from the operand. - -Finally, the `__and__` method applies the `and_` function to the current Node object (`self`) and the converted Node object (`node(other)`). The `and_` function performs the bitwise AND operation and returns the result. - -**Note**: -- Ensure that the `other` operand is of a type that supports the bitwise AND operation to avoid runtime errors. -- The `node` function is used to standardize the operand into a Node object, which simplifies the operation and ensures consistency. - -**Output Example**: If the current Node object represents the value 6 (binary 110) and the `other` operand represents the value 3 (binary 011), the method call `self.__and__(other)` will return a Node object representing the value 2 (binary 010). -*** -### FunctionDef __or__(self, other) -**__or__**: The function of __or__ is to perform a bitwise OR operation between the current Node instance and another Node instance. - -**parameters**: The parameters of this function. -· self: The current Node instance. -· other: Another Node instance or a message that can be converted into a Node. - -**Code Description**: The __or__ method is designed to enable the use of the "|" operator to combine two Node instances using a bitwise OR operation. When the "|" operator is used between two Node instances, this method is invoked. - -1. The method first imports the `or_` function from the `opto.trace.operators` module. -2. It then calls the `node` function to ensure that the `other` parameter is converted into a Node instance if it is not already one. -3. Finally, it applies the `or_` function to the current Node instance (`self`) and the converted Node instance (`other`), returning the result. - -The `node` function is responsible for creating a Node object from a message, ensuring that the `other` parameter is in the correct format for the bitwise OR operation. The `or_` function performs the actual bitwise OR operation between the two Node instances. - -**Note**: Ensure that the `other` parameter can be converted into a Node instance to avoid errors. The `or_` function expects both operands to support the bitwise OR operation. - -**Output Example**: If `self` is a Node instance representing the binary value 0101 and `other` is a Node instance representing the binary value 0011, the return value of `self | other` would be a Node instance representing the binary value 0111. -*** -### FunctionDef __xor__(self, other) -**__xor__**: The function of __xor__ is to perform a bitwise XOR operation between the current Node instance and another Node instance or value. - -**parameters**: The parameters of this function. -· self: The current Node instance. -· other: Another Node instance or value to perform the XOR operation with. - -**Code Description**: The __xor__ method is designed to enable the use of the ^ operator to perform a bitwise XOR operation between Node objects. This method imports the xor function from the opto.trace.operators module and applies it to the current Node instance (self) and another Node instance or value (other). - -The method first imports the necessary operators from the opto.trace.operators module. It then calls the xor function, passing in the current Node instance (self) and the result of the node function applied to the other parameter. The node function ensures that the other parameter is converted into a Node object if it is not already one. This allows for seamless integration and operation between Node objects and other values. - -The xor function itself performs the bitwise XOR operation, which compares each bit of its operands and returns 1 if the bits are different, and 0 if they are the same. This operation is useful in various scenarios, such as cryptography, error detection, and correction algorithms. - -**Note**: Ensure that the other parameter is of a type that supports the bitwise XOR operation, such as integers or objects that implement the __xor__ method. The node function will handle the conversion of the other parameter to a Node object if necessary. - -**Output Example**: If the current Node instance represents the value 5 (binary 0101) and the other parameter represents the value 3 (binary 0011), the result of the __xor__ method would be a Node object representing the value 6 (binary 0110). -*** -### FunctionDef __iter__(self) -**__iter__**: The function of __iter__ is to provide an iterable interface for the Node object, allowing it to be iterated over in a consistent manner. - -**parameters**: This function does not take any parameters. - -**Code Description**: The __iter__ method is designed to make the Node object iterable. When called, it imports the iterate function from the opto.trace.containers module. The iterate function is then invoked with the Node object (self) as its argument. The iterate function determines the appropriate iterable class to use based on the type of the Node object's data attribute. It handles various types of collections such as lists, tuples, sets, and dictionaries, and returns an iterable object accordingly. This ensures that the Node object can be iterated over seamlessly, regardless of the type of its data attribute. - -**Note**: -- The Node object must have a data attribute that is a list, tuple, set, or dictionary. -- The iterate function handles the conversion of sets to lists and wraps items in lists or dictionaries with node objects. - -**Output Example**: -If the Node object's data attribute is a list [1, 2, 3], iterating over the Node object would yield: -``` -node(1) -node(2) -node(3) -``` -If the Node object's data attribute is a dictionary {'a': 1, 'b': 2}, iterating over the Node object would yield: -``` -(node('a'), 1) -(node('b'), 2) -``` -*** -### FunctionDef __len__(self) -**__len__**: The function of __len__ is to return the number of elements contained in the Node object. - -**parameters**: The parameters of this Function. -· self: Refers to the instance of the Node class. - -**Code Description**: The __len__ method is a special method in Python that is used to define the behavior of the len() function for instances of a class. In this implementation, the __len__ method returns the length of the internal data structure, self._data, which is assumed to be a collection such as a list, dictionary, or any other iterable. The method ensures that the return type is an integer, which is a requirement for the __len__ method in Python. This method provides a straightforward way to get the size of the Node's data without directly accessing the internal data structure. - -**Note**: -- The __len__ method strictly returns an integer value representing the number of elements in the Node's internal data structure. -- If users need a Node object representing the length, they should use a different method, such as node.len(), instead of __len__. - -**Output Example**: -If the Node's internal data structure, self._data, contains 5 elements, calling len(node_instance) will return: -5 -*** -### FunctionDef __lt__(self, other) -**__lt__**: The function of __lt__ is to define the behavior of the less-than operator (<) for Node objects. - -**parameters**: The parameters of this function. -· self: The instance of the Node object on the left-hand side of the < operator. -· other: The object on the right-hand side of the < operator, which can be another Node or a value that can be converted into a Node. - -**Code Description**: The __lt__ method is a special method in Python that allows objects to implement behavior for the less-than operator (<). In this implementation, the method first imports the necessary operators from the opto.trace.operators module. It then calls the lt function from the operators module, passing in the current Node instance (self) and the result of converting the other object into a Node using the node function. - -The node function is responsible for creating a Node object from the other parameter. If the other parameter is already a Node, it is returned as is. Otherwise, a new Node object is created from the other parameter. This ensures that the lt function always receives Node objects as its arguments. - -The lt function from the operators module performs the actual comparison between the two Node objects and returns the result. - -**Note**: -- The __lt__ method relies on the node function to ensure that the other parameter is converted into a Node object if it is not already one. -- The comparison logic is delegated to the lt function from the opto.trace.operators module. - -**Output Example**: A possible return value of the __lt__ method could be a boolean value, such as True or False, indicating whether the current Node instance is less than the other Node instance or value. -*** -### FunctionDef __le__(self, other) -**__le__**: The function of __le__ is to define the behavior of the "less than or equal to" (<=) comparison operator for Node objects. - -**parameters**: The parameters of this function. -· self: The instance of the Node object on the left-hand side of the <= operator. -· other: The object on the right-hand side of the <= operator, which can be another Node or a value that can be converted into a Node. - -**Code Description**: The __le__ function is a special method in Python that allows the use of the <= operator with Node objects. When the <= operator is used, this method is called with the Node instance (self) and the other object (other) being compared. - -1. The function imports the operators module from the opto.trace package as ops. -2. It then calls the le function from the ops module, passing in the current Node instance (self) and the result of the node function applied to the other object. - -The node function is used to ensure that the other object is converted into a Node if it is not already one. This conversion is necessary because the le function in the ops module expects both arguments to be Node objects. - -The le function in the ops module performs the actual comparison between the two Node objects and returns the result. - -**Note**: -- The __le__ method ensures that comparisons using the <= operator are consistent and meaningful for Node objects. -- The node function is used to handle the conversion of the other object to a Node, ensuring compatibility with the le function in the ops module. - -**Output Example**: A possible return value of the __le__ function could be a boolean value, such as True or False, indicating whether the left-hand side Node is less than or equal to the right-hand side Node. -*** -### FunctionDef __gt__(self, other) -**__gt__**: The function of __gt__ is to compare if the current Node object is greater than another object. - -**parameters**: The parameters of this function. -· self: The current instance of the Node object. -· other: The object to compare with the current Node instance. - -**Code Description**: The __gt__ method is a special method in Python used to define the behavior of the greater-than operator (>) for instances of a class. In this implementation, the method first imports the operators module from the opto.trace package. It then calls the gt function from the operators module, passing the current Node instance (self) and another Node instance created from the other parameter using the node function. - -The node function is responsible for converting the other parameter into a Node object if it is not already one. This ensures that the comparison is always between two Node objects. The gt function from the operators module performs the actual comparison and returns the result. - -**Note**: -- The other parameter can be any object that can be converted into a Node using the node function. -- The comparison relies on the gt function from the operators module, which should be defined to handle Node comparisons appropriately. - -**Output Example**: A possible return value of the __gt__ method could be a boolean value, such as True or False, indicating whether the current Node instance is greater than the other object. -*** -### FunctionDef __ge__(self, other) -**__ge__**: The function of __ge__ is to compare the current Node object with another object to determine if the current Node is greater than or equal to the other object. - -**parameters**: The parameters of this function. -· self: The current instance of the Node object. -· other: The object to compare with the current Node. - -**Code Description**: The __ge__ method is a special method in Python used to define the behavior of the greater than or equal to (>=) operator for instances of a class. In this implementation, the method imports the `opto.trace.operators` module as `ops` and uses the `ge` function from this module to perform the comparison. - -The method first converts the `other` object into a Node object using the `node` function. This ensures that the comparison is always between two Node objects, regardless of the initial type of `other`. The `node` function is designed to create a Node object from a given message, handling various scenarios such as whether the message is already a Node, whether it should be trainable, and whether it has any constraints. - -Once the `other` object is converted into a Node, the `ge` function from the `ops` module is called with `self` and the newly created Node as arguments. The `ge` function is responsible for performing the actual comparison and returning the result. - -**Note**: -- The `__ge__` method ensures that comparisons are always made between Node objects by converting the `other` object using the `node` function. -- The `node` function handles various scenarios to create a Node object, making the comparison process robust and flexible. - -**Output Example**: A possible return value of the `__ge__` method could be a boolean value, such as `True` or `False`, indicating whether the current Node is greater than or equal to the `other` object. -*** -### FunctionDef __eq__(self, other) -**__eq__**: The function of __eq__ is to compare the current Node object with another object to determine if they are equal. - -**parameters**: The parameters of this Function. -· self: The instance of the Node class. -· other: The object to compare with the current Node instance. - -**Code Description**: The __eq__ method is designed to enable comparison between a Node object and another object to check for equality. The method first checks if the 'other' object is an instance of the Node class. If it is, the method extracts the 'data' attribute from the 'other' Node object. Then, it compares the '_data' attribute of the current Node instance with the 'other' object (or its 'data' attribute if 'other' is a Node). The method returns True if the '_data' attributes are equal, and False otherwise. - -**Note**: -- This method overrides the default equality comparison behavior in Python. -- It ensures that two Node objects are considered equal if their '_data' attributes are equal. -- If 'other' is not a Node instance, the method directly compares 'self._data' with 'other'. - -**Output Example**: -- If `self._data` is 5 and `other` is a Node instance with `data` attribute 5, the method returns True. -- If `self._data` is 5 and `other` is 10, the method returns False. -*** -### FunctionDef __hash__(self) -**__hash__**: The function of __hash__ is to return the hash value of the Node object. - -**parameters**: The parameters of this Function. -· self: Refers to the instance of the Node class. - -**Code Description**: The __hash__ method in the Node class is an override of the built-in __hash__ method. It calls the __hash__ method of its superclass using the super() function. This ensures that the hash value of the Node object is consistent with the hash value defined in its superclass. By doing so, it maintains the integrity and uniqueness of the hash value for instances of the Node class, which is crucial for operations that rely on hashing, such as using Node instances as keys in dictionaries or storing them in sets. - -**Note**: -- The __hash__ method should be consistent with the __eq__ method. If two objects are considered equal (using the __eq__ method), they must return the same hash value. -- Overriding the __hash__ method is essential when you need custom behavior for hashing, but in this case, it simply defers to the superclass implementation. - -**Output Example**: The return value of the __hash__ method will be an integer representing the hash value of the Node object, as determined by the superclass's __hash__ method. For example, if the superclass's __hash__ method returns 123456 for a particular Node instance, then calling hash(node_instance) will also return 123456. -*** -### FunctionDef __bool__(self) -**__bool__**: The function of __bool__ is to provide a boolean representation of the Node object. - -**parameters**: The parameters of this Function. -· self: Refers to the instance of the Node class. - -**Code Description**: The __bool__ method is a special method in Python that is used to define the boolean value of an object. In this implementation, the method returns the boolean value of the instance variable `_data`. The expression `bool(self._data)` converts `_data` to its boolean equivalent. If `_data` is a non-empty value (such as a non-empty list, string, or a non-zero number), the method will return `True`. If `_data` is an empty value (such as an empty list, string, or zero), the method will return `False`. This allows the Node object to be used in boolean contexts, such as in conditional statements. - -**Note**: -- Ensure that the `_data` attribute is properly initialized in the Node class, as its value directly affects the boolean representation of the Node object. -- This method does not trace the conversion process, meaning it directly returns the boolean value without additional logging or processing. - -**Output Example**: -- If `_data` is a non-empty list, e.g., `[1, 2, 3]`, the return value will be `True`. -- If `_data` is an empty list, e.g., `[]`, the return value will be `False`. -*** -### FunctionDef format(self) -**format**: The function of format is to format the data contained within the Node object if the data is a string. - -**parameters**: The parameters of this Function. -· *args: Variable length argument list. -· **kwargs: Arbitrary keyword arguments. - -**Code Description**: The `format` function first checks if the `_data` attribute of the Node object is of type `str`. If `_data` is not a string, it raises an `AttributeError` indicating that the object does not have a `format` attribute. This ensures that only string data can be formatted using this function. - -Next, the function imports the `opto.trace.operators` module as `ops`. It then calls the `format` function from the `ops` module, passing the current Node object (`self`) along with any additional arguments (`*args`) and keyword arguments (`**kwargs`). This delegation allows the `format` function in the `ops` module to handle the actual formatting logic. - -**Note**: -- Ensure that the `_data` attribute of the Node object is a string before calling the `format` function to avoid an `AttributeError`. -- The `opto.trace.operators` module must be available and contain a `format` function that can handle the passed arguments and keyword arguments. - -**Output Example**: -If the `_data` attribute of the Node object is a string, the `format` function will return the formatted string as processed by the `opto.trace.operators.format` function. For example, if `_data` is `"Hello, {}"` and the arguments passed are `"World"`, the return value might be `"Hello, World"`. -*** -### FunctionDef capitalize(self) -**capitalize**: The function of capitalize is to convert the first character of the string stored in the `_data` attribute of the Node object to uppercase. - -**parameters**: This function does not take any parameters. - -**Code Description**: The `capitalize` function first checks if the `_data` attribute of the Node object is of type `str`. If `_data` is not a string, it raises an `AttributeError` indicating that the object does not have a `capitalize` attribute. This ensures that the function is only applied to string data. If `_data` is a string, the function imports the `capitalize` function from the `opto.trace.operators` module and returns the result of calling this `capitalize` function with the current Node object (`self`) as its argument. This modular approach allows for the actual capitalization logic to be handled by the `opto.trace.operators` module, promoting code reusability and separation of concerns. - -**Note**: -- Ensure that the `_data` attribute of the Node object is a string before calling the `capitalize` function to avoid raising an `AttributeError`. -- The function relies on the `opto.trace.operators` module, so make sure this module is correctly implemented and accessible. - -**Output Example**: If the `_data` attribute of the Node object is `"hello world"`, the `capitalize` function will return `"Hello world"`. -*** -### FunctionDef lower(self) -**lower**: The function of lower is to convert the string data contained within the object to lowercase. - -**parameters**: This function does not take any parameters. - -**Code Description**: The lower function is designed to operate on an instance's internal data, specifically converting it to lowercase if it is a string. The function first checks if the type of the instance's _data attribute is a string. If _data is not a string, it raises an AttributeError, indicating that the object does not have a 'lower' attribute. This ensures that the function only attempts to convert string data to lowercase, preventing type errors. If the _data attribute is a string, the function imports the lower function from the opto.trace.operators module and applies it to the instance, returning the result. - -**Note**: -- This function will raise an AttributeError if the _data attribute is not of type str. -- Ensure that the opto.trace.operators module is available and contains a lower function that can handle the conversion. - -**Output Example**: -If the _data attribute of the instance is "Hello World", the function will return "hello world". -*** -### FunctionDef upper(self) -**upper**: The function of upper is to convert the internal data of the Node object to uppercase if it is a string. - -**parameters**: The parameters of this Function. -· This function does not take any parameters. - -**Code Description**: The upper function first checks if the internal data attribute (_data) of the Node object is of type string. If _data is not a string, it raises an AttributeError indicating that the object does not have an 'upper' attribute. If _data is a string, the function imports the upper function from the opto.trace.operators module and returns the result of calling this imported upper function with the current Node object as its argument. - -**Note**: -- This function will only work if the _data attribute of the Node object is a string. If _data is of any other type, an AttributeError will be raised. -- Ensure that the opto.trace.operators module is correctly implemented and accessible, as this function relies on it. - -**Output Example**: -If the _data attribute of the Node object is "hello", calling the upper function will return "HELLO". -*** -### FunctionDef swapcase(self) -**swapcase**: The function of swapcase is to convert all uppercase characters in the string to lowercase and vice versa. - -**parameters**: The parameters of this Function. -· None - -**Code Description**: The swapcase function is a method designed to operate on an instance's _data attribute. It first checks if the _data attribute is of type str. If _data is not a string, the function raises an AttributeError, indicating that the object does not have a swapcase attribute. This ensures that the function only processes string data. If the _data attribute is a string, the function imports the swapcase function from the opto.trace.operators module and applies it to the instance, returning the result. This modular approach allows for the swapcase operation to be defined and maintained separately in the operators module. - -**Note**: -- The _data attribute must be a string; otherwise, an AttributeError will be raised. -- Ensure that the opto.trace.operators module is correctly implemented and accessible. - -**Output Example**: -If the _data attribute of the instance is "Hello World", the swapcase function will return "hELLO wORLD". -*** -### FunctionDef title(self) -**title**: The function of title is to retrieve the title attribute of the Node object if it exists. - -**parameters**: The parameters of this Function. -· self: Refers to the instance of the Node class. - -**Code Description**: The title function checks if the _data attribute of the Node instance is a string. If _data is not a string, it raises an AttributeError indicating that the object does not have a title attribute. If _data is a string, it imports the title function from the opto.trace.operators module and returns the result of calling this imported title function with the current Node instance as its argument. - -**Note**: -- Ensure that the _data attribute of the Node instance is a string before calling the title function to avoid an AttributeError. -- The function relies on the title function from the opto.trace.operators module, so ensure that this module is correctly imported and available. - -**Output Example**: -If the _data attribute of the Node instance is a string, the function will return the result of the title function from the opto.trace.operators module. For example, if the title function in the operators module processes the string and returns a formatted title, the output will be that formatted title. -*** -### FunctionDef split(self, sep, maxsplit) -**split**: The function of split is to divide a string into a list of substrings based on a specified separator. - -**parameters**: The parameters of this Function. -· sep: The delimiter according to which the string is split. If not specified or None, any whitespace string is a separator. -· maxsplit: The maximum number of splits to do. -1 (the default value) means no limit on the number of splits. - -**Code Description**: The split function is designed to operate on an object that contains a string. It first checks if the object's _data attribute is of type str. If _data is not a string, it raises an AttributeError indicating that the split operation is not applicable to the object's data type. If _data is a string, the function imports the split function from the opto.trace.operators module and delegates the actual splitting operation to this imported function, passing along the separator and maxsplit parameters. - -**Note**: -- This function will raise an AttributeError if the _data attribute of the object is not a string. -- Ensure that the opto.trace.operators module is available and contains a split function that can handle the parameters passed to it. - -**Output Example**: -If the _data attribute of the object is "hello world" and the split function is called with the default parameters, the return value would be: -```python -['hello', 'world'] -``` -*** -### FunctionDef strip(self, chars) -**strip**: The function of strip is to remove leading and trailing characters from a string stored in the object's `_data` attribute. - -**parameters**: The parameters of this Function. -· chars: A string specifying the set of characters to be removed. If not provided, whitespace characters are removed by default. - -**Code Description**: The `strip` function first checks if the `_data` attribute of the object is of type `str`. If `_data` is not a string, it raises an `AttributeError` indicating that the object does not have a `strip` attribute. This ensures that the function is only applied to string data. The function then imports the `strip` function from the `opto.trace.operators` module and calls this imported `strip` function, passing the current object and the `chars` parameter to it. This design allows for the actual stripping operation to be handled by the `strip` function in the `opto.trace.operators` module, potentially allowing for more complex or customized stripping behavior. - -**Note**: -- Ensure that the `_data` attribute is a string before calling the `strip` function to avoid the `AttributeError`. -- The `chars` parameter is optional. If not provided, the function will default to removing whitespace characters. - -**Output Example**: -If `_data` is `" example "` and `chars` is not provided, the return value might be `"example"`. If `_data` is `"--example--"` and `chars` is `"-"`, the return value might be `"example"`. -*** -### FunctionDef replace(self, old, new, count) -**replace**: The function of replace is to substitute occurrences of a specified substring within the Node's data with a new substring. - -**parameters**: The parameters of this Function. -· self: The instance of the Node class. -· old: The substring that needs to be replaced. -· new: The substring that will replace the old substring. -· count: (optional) The maximum number of occurrences to replace. Default is -1, which means replace all occurrences. - -**Code Description**: The replace function is designed to perform a substring replacement operation on the data contained within a Node object. The function first checks if the data type of the Node's internal data (_data) is a string. If it is not a string, it raises an AttributeError, indicating that the replace operation is not applicable to the data type. - -The function then imports the replace function from the opto.trace.operators module. It proceeds to call this imported replace function, passing the current Node instance (self), and the old and new substrings wrapped in Node objects using the node function. The count parameter is also passed along to control the number of replacements. - -The node function is used to ensure that the old and new substrings are appropriately converted into Node objects if they are not already. This ensures consistency and proper handling within the replace operation. - -**Note**: -- The replace function only works if the Node's internal data is a string. Attempting to use it with non-string data will result in an AttributeError. -- The count parameter allows for partial replacements, where only a specified number of occurrences are replaced. If count is set to -1, all occurrences will be replaced. - -**Output Example**: A possible return value of the replace function could be a new Node object with the specified substring replacements applied to its internal string data. For instance, if the original Node's data is "hello world" and the replace function is called with old="world", new="there", the resulting Node's data would be "hello there". -*** -### FunctionDef items(self) -**items**: The function of items is to retrieve and return the items associated with the current instance of the Node class. - -**parameters**: The parameters of this Function. -· This function does not take any parameters other than the implicit 'self' which refers to the instance of the Node class. - -**Code Description**: The items function is designed to import the items function from the opto.trace.containers module and then call this imported function, passing the current instance (self) as an argument. This allows the function to retrieve the items related to the current Node instance by leveraging the functionality provided in the opto.trace.containers module. - -**Note**: -- Ensure that the opto.trace.containers module is correctly installed and accessible in your environment, as the items function relies on it. -- This function assumes that the imported items function from the opto.trace.containers module is designed to handle the Node instance appropriately. - -**Output Example**: -The return value of this function will depend on the implementation of the items function in the opto.trace.containers module. Typically, it might return a list, dictionary, or another collection of items associated with the Node instance. For example: -```python -[ - {'id': 1, 'name': 'Item1'}, - {'id': 2, 'name': 'Item2'} -] -``` -*** -### FunctionDef pop(self, __index) -**pop**: The function of pop is to remove and return an element from a Node object at a specified index. - -**parameters**: The parameters of this function. -· __index: An optional integer parameter that specifies the index of the element to be removed. The default value is -1, which means the last element will be removed. - -**Code Description**: The pop function is designed to remove and return an element from a Node object at a specified index. It imports the pop function from the opto.trace.operators module and utilizes the node function to handle the index parameter. The node function ensures that the index is properly converted into a Node object if it is not already one. This allows for consistent handling of the index parameter within the pop function. - -The pop function works as follows: -1. It imports the necessary operators from the opto.trace.operators module. -2. It calls the ops.pop function, passing the current Node object (self) and the index parameter converted to a Node object using the node function. - -The relationship with its callees is as follows: -- The node function is used to ensure that the index parameter is properly converted into a Node object. -- The ops.pop function from the opto.trace.operators module is used to perform the actual removal and return of the element from the Node object. - -**Note**: -- The default value of the __index parameter is -1, which means the last element will be removed if no index is specified. -- The node function is used to handle the index parameter, ensuring it is properly converted into a Node object. - -**Output Example**: A possible return value of the pop function could be the element that was removed from the Node object at the specified index. For example, if the Node object contained the elements [1, 2, 3] and the index parameter was 1, the return value would be 2, and the Node object would be updated to [1, 3]. -*** -### FunctionDef append(self) -**append**: The function of append is to add elements to a collection or list within the Node object. - -**parameters**: The parameters of this function. -· self: The instance of the Node class on which the method is called. -· *args: Variable-length positional arguments to be appended. -· **kwargs: Variable-length keyword arguments to be appended. - -**Code Description**: The `append` method is a member of the `Node` class in the `opto.trace.nodes.py` module. This method is designed to add elements to a collection or list within the Node object. It achieves this by internally calling the `call` method with the string "append" as the function name, along with any positional (`*args`) and keyword arguments (`**kwargs`) provided. - -The `call` method, which is invoked by `append`, dynamically calls the specified function (in this case, "append") on the `Node` object. It first converts all positional and keyword arguments to `Node` objects using the `node` function, ensuring that the arguments are compatible with the Node's internal structure. After conversion, it retrieves the "append" function from the `Node` object using `getattr` and invokes it with the converted arguments. - -This design allows the `append` method to flexibly handle various types of input while ensuring that all elements being appended are properly formatted as `Node` objects. - -**Note**: -- The `append` method relies on the `call` method to dynamically invoke the "append" function on the `Node` object. -- All arguments passed to `append` are converted to `Node` objects before being appended. -- The `self` parameter must be a valid instance of the `Node` class. - -**Output Example**: A possible return value of the `append` method could be the result of the "append" function invoked on the `Node` object with the provided arguments. For instance, if the "append" function adds elements to a list, the return value might be the updated list. -*** -## ClassDef ParameterNode -**ParameterNode**: The function of ParameterNode is to represent a trainable node in a computational graph. - -**attributes**: -- value: The initial value of the node. -- name: The name of the node. -- trainable: A boolean indicating whether the node is trainable or not. -- description: A string describing the node. -- constraint: A constraint on the node. -- info: Additional information about the node. - -**Code Description**: The ParameterNode class is a subclass of the Node class and represents a trainable node in a computational graph. It is used to store and manipulate data in the graph. The class has an initializer method that takes in various parameters such as value, name, trainable, description, constraint, and info. These parameters are used to initialize the attributes of the ParameterNode object. - -The initializer method also calls the initializer method of the superclass (Node) to set the value and name attributes. It then sets the trainable, description, constraint, and info attributes based on the provided parameters. Additionally, it adds the ParameterNode object to the 'parameter' dependency set. - -The ParameterNode class also defines a __str__ method that returns a string representation of the node. This method allows users to easily look up the node in the feedback dictionary. - -**Note**: -- The ParameterNode class inherits from the Node class, which is a data node in a directed graph. -- The value attribute represents the initial value of the node. -- The name attribute represents the name of the node. -- The trainable attribute indicates whether the node is trainable or not. -- The description attribute provides information about the node. -- The constraint attribute represents a constraint on the node. -- The info attribute stores additional information about the node. - -**Output Example**: -A possible return value of the __str__ method could be "ParameterNode: (name, dtype=, data=value)". -### FunctionDef __init__(self, value) -**__init__**: The function of __init__ is to initialize an instance of the ParameterNode class with specified attributes. - -**parameters**: The parameters of this Function. -· value: The initial value assigned to the ParameterNode. -· name: An optional name for the ParameterNode. Default is None. -· trainable: A boolean indicating whether the parameter is trainable. Default is True. -· description: A string describing the ParameterNode. Default is "[ParameterNode] This is a ParameterNode in a computational graph." -· constraint: An optional constraint applied to the parameter. Default is None. -· info: Additional optional information about the parameter. Default is None. - -**Code Description**: The __init__ function initializes a ParameterNode object by calling the constructor of its superclass with the provided parameters. It sets the initial value, name, trainable status, description, constraint, and additional information for the ParameterNode. After initializing the superclass, it adds the current instance to the '_dependencies' dictionary under the 'parameter' key. This ensures that the ParameterNode is properly registered within the computational graph's dependency management system. - -**Note**: Points to note about the use of the code -- Ensure that the 'value' parameter is provided when creating an instance of ParameterNode. -- The 'name', 'constraint', and 'info' parameters are optional and can be omitted if not needed. -- The 'trainable' parameter defaults to True, indicating that the parameter will be included in training processes unless explicitly set to False. -- The 'description' parameter provides a default description but can be customized as needed. -*** -### FunctionDef __str__(self) -**__str__**: The function of __str__ is to provide a string representation of the ParameterNode object. - -**parameters**: The parameters of this Function. -· self: The instance of the ParameterNode class. - -**Code Description**: The `__str__` method is designed to return a human-readable string that represents the current state of a `ParameterNode` object. This method is particularly useful for debugging and logging purposes, as it provides a concise summary of the node's key attributes. - -When called, the `__str__` method constructs a string that includes: -- The name of the node, accessed via `self.name`. This name is managed by the `name` method of the `AbstractNode` class, which returns the value of the private attribute `_name`. -- The data type of the node's data, obtained using `type(self._data)`. -- The actual data stored in the node, accessed via `self._data`. - -The string is formatted as follows: -``` -ParameterNode: ({self.name}, dtype={type(self._data)}, data={self._data}) -``` -This format ensures that the string includes the node's name, the type of its data, and the data itself, all in a clear and structured manner. - -**Note**: -- The `__str__` method should be used when a string representation of the `ParameterNode` is needed, such as in logging or debugging scenarios. -- Ensure that the node's data (`self._data`) is in a state that can be meaningfully represented as a string. - -**Output Example**: -If a `ParameterNode` object has a name "node:0", data type ``, and data `42`, the `__str__` method will return: -``` -ParameterNode: (node:0, dtype=, data=42) -``` -*** -## ClassDef MessageNode -**MessageNode**: The MessageNode class represents the output of an operator in a computational graph. - -**attributes**: -- value: The value of the node. -- inputs: The input nodes of the MessageNode. It can be a list or a dictionary. -- description: A string that describes the operator associated with the MessageNode. -- constraint: A constraint on the node. -- name: The name of the node. -- info: Additional information about the node. - -**Code Description**: -The MessageNode class is a subclass of the Node class and inherits its attributes and methods. It overrides the __init__ method to include the inputs, description, constraint, name, and info parameters. The inputs parameter can be a list or a dictionary, and it represents the input nodes of the MessageNode. The description parameter is a string that describes the operator associated with the MessageNode. The constraint parameter specifies a constraint on the node. The name parameter is the name of the node. The info parameter is additional information about the node. - -The __init__ method initializes the MessageNode by calling the __init__ method of the Node class and passing the value, name, description, constraint, and info parameters. It checks if the inputs parameter is a list or a dictionary and creates a dictionary with the names of the nodes as keys if it is a list. It then assigns the inputs to the _inputs attribute of the MessageNode. If the GRAPH.TRACE flag is False, it checks if the MessageNode has any inputs and raises an assertion error if it does. It adds the parents and dependencies if the GRAPH.TRACE flag is True. - -The inputs property returns a copy of the _inputs attribute. - -The __str__ method returns a string representation of the MessageNode, including its name, data type, and data. - -The _add_feedback method is called to add feedback from a child node. It adds the feedback to the _feedback attribute of the MessageNode. - -The external_dependencies property returns a set of external dependencies based on the info attribute of the MessageNode. - -The _add_dependencies method is called to add dependencies from a parent node. It adds the parameter and expandable dependencies to the _dependencies attribute of the MessageNode. - -**Note**: -- The MessageNode class is used to represent the output of an operator in a computational graph. -- The inputs parameter can be a list or a dictionary, and it represents the input nodes of the MessageNode. -- The description parameter is a string that describes the operator associated with the MessageNode. -- The constraint parameter specifies a constraint on the node. -- The name parameter is the name of the node. -- The info parameter is additional information about the node. - -**Output Example**: -A possible appearance of the MessageNode object when converted to a string could be: -"MessageNode: (node_name, dtype=, data=10)" -### FunctionDef __init__(self, value) -**__init__**: The function of __init__ is to initialize a MessageNode object with the given parameters. - -**parameters**: -- self: The instance of the class. -- value: The value of the MessageNode object. -- inputs: The inputs to the MessageNode object, which can be either a list or a dictionary of Node objects. -- description: The description of the MessageNode object. -- constraint: An optional constraint on the MessageNode object. -- name: An optional name for the MessageNode object. -- info: Additional information about the MessageNode object. - -**Code Description**: -The `__init__` function is the constructor of the MessageNode class. It initializes a MessageNode object with the provided parameters. The function first calls the constructor of the parent class, AbstractNode, passing the value, name, description, constraint, and info parameters. - -Next, the function checks if the inputs parameter is either a list or a dictionary. If it is not, an assertion error is raised with the message "Inputs to MessageNode must be a list or a dict." This ensures that the inputs are of the correct type. - -If the inputs parameter is a list, the function creates a dictionary with the names of the nodes as keys and the nodes themselves as values. This is done to ensure that the inputs can be accessed by their names. - -The function then assigns the inputs to the _inputs attribute of the MessageNode object. - -If the GRAPH.TRACE flag is not set, indicating that tracing is not enabled, the function asserts that the _inputs attribute is empty. This is because when not tracing, a MessageNode should have no inputs. - -Next, the function iterates over the items in the _inputs dictionary. For each item, it checks if the value is an instance of the Node class. If it is not, an assertion error is raised with the message "Input {k} is not a Node." This ensures that all inputs are valid Node objects. - -For each valid input, the function calls the _add_parent method of the MessageNode object to add the input as a parent. This method adds the parent node to the hierarchical structure of the graph. - -The function also calls the _add_dependencies method of the MessageNode object to add the dependencies on parameters and expandable nodes. This method updates the _dependencies attribute of the MessageNode object. - -Finally, if the external_dependencies attribute of the MessageNode object is not empty, indicating that there are external dependencies, the function adds the MessageNode object to the 'expandable' set of the _dependencies attribute. - -**Note**: -- The inputs parameter should be either a list or a dictionary of Node objects. -- When not tracing, a MessageNode should have no inputs. -- The inputs should be valid Node objects. -- The _add_parent method adds the parent node to the hierarchical structure of the graph. -- The _add_dependencies method adds the dependencies on parameters and expandable nodes to the MessageNode object. -- The external_dependencies attribute indicates the external dependencies of the MessageNode object. -*** -### FunctionDef inputs(self) -**inputs**: The function of inputs is to return a copy of the `_inputs` attribute of the object. - -**parameters**: -- self: The current object. - -**Code Description**: -The `inputs` function is a method of the `MessageNode` class. It returns a copy of the `_inputs` attribute of the object. The `_inputs` attribute is a dictionary that stores the input nodes of the `MessageNode` object. - -The purpose of this function is to provide access to the input nodes of the `MessageNode` object. By returning a copy of the `_inputs` attribute, it ensures that the original dictionary is not modified when accessing the input nodes. - -This function can be useful when you need to retrieve the input nodes of a `MessageNode` object for further processing or analysis. - -**Note**: -- The returned copy of the `_inputs` attribute is a shallow copy, which means that the keys and values of the dictionary are copied, but the objects themselves are not. If the values of the dictionary are mutable objects, modifying them will affect the original objects. -- The `_inputs` attribute is a private attribute and should not be modified directly. Use the `inputs` function to access the input nodes instead. - -**Output Example**: -``` -{ - 'input1': , - 'input2': , - ... -} -``` -*** -### FunctionDef __str__(self) -**__str__**: The function of __str__ is to provide a string representation of the MessageNode object. - -**parameters**: The parameters of this Function. -· self: The instance of the MessageNode class. - -**Code Description**: The __str__ method in the MessageNode class returns a formatted string that includes the name of the node, the data type of the node's data, and the data itself. This method is useful for debugging and logging purposes, as it provides a clear and concise representation of the node's state. - -The method calls the `name` method from the AbstractNode class to retrieve the name of the node. The `name` method returns the value of the private attribute `_name`, which is set when the node is registered in the graph. The `type(self._data)` function is used to get the data type of the node's data, and `self._data` is used to access the actual data stored in the node. - -The returned string follows the format: "MessageNode: (name, dtype=data_type, data=data)", where `name` is the node's name, `data_type` is the type of the data, and `data` is the actual data. - -**Note**: -- The __str__ method should be used when a string representation of the MessageNode object is needed, such as in logging or debugging scenarios. -- Ensure that the node has been properly initialized and registered before calling this method to avoid any unexpected behavior. - -**Output Example**: -If the name of the node is "node:0", the data type is ``, and the data is `42`, the __str__ method will return: -``` -MessageNode: (node:0, dtype=, data=42) -``` -*** -### FunctionDef _add_feedback(self, child, feedback) -**_add_feedback**: The function of _add_feedback is to add feedback from a child node. - -**parameters**: The parameters of this Function. -· child: The child node from which the feedback is received. -· feedback: The feedback data provided by the child node. - -**Code Description**: The _add_feedback function is designed to handle feedback from child nodes within a MessageNode. It first calls the parent class's _add_feedback method to ensure any inherited behavior is executed. After that, it asserts that the length of the feedback list for the given child node is exactly one. This assertion ensures that each child node provides only one piece of feedback, maintaining the integrity and expected behavior of the MessageNode. - -**Note**: -- This function relies on the parent class's _add_feedback method, so it is crucial that the parent class is correctly implemented. -- The assertion will raise an AssertionError if a child node provides more than one piece of feedback, which helps in debugging and maintaining the correct structure of feedback within the MessageNode. -*** -### FunctionDef external_dependencies(self) -**external_dependencies**: The function of external_dependencies is to determine the external dependencies of a MessageNode object. - -**parameters**: -- self: The MessageNode object itself. - -**Code Description**: -The `external_dependencies` function is a method within the `MessageNode` class that calculates and returns the external dependencies of the node. It checks if the `info` attribute of the `MessageNode` instance is a dictionary and if it contains an 'output' key that is an instance of the `Node` class. If these conditions are met, it compares the length of the parameter dependencies of the 'output' node with the parameter dependencies of the current `MessageNode`. If the 'output' node has more parameter dependencies, it returns the difference between the two sets of dependencies. This indicates that the `external_dependencies` function relies on the `parameter_dependencies` function of the `Node` class to determine the parameter dependencies of the nodes it interacts with. - -The purpose of the `external_dependencies` function is to identify any external dependencies that the `MessageNode` relies on, which are not already accounted for in its own parameter dependencies. By returning the set of external dependencies, users can gain insights into the dependencies of the `MessageNode` and ensure that all necessary dependencies are properly handled. - -It is important to note that the `external_dependencies` function assumes that the `info` attribute is a dictionary and that the 'output' key contains a valid `Node` object. If these assumptions are not met, the function will return an empty set. - -**Note**: -- The `external_dependencies` function relies on the `parameter_dependencies` function of the `Node` class to determine the parameter dependencies of the nodes it interacts with. -- The `info` attribute of the `MessageNode` instance must be a dictionary and contain an 'output' key that is an instance of the `Node` class for the function to work correctly. - -**Output Example**: A possible return value of the `external_dependencies` function could be a set of external dependencies, such as: -``` -{'dependency1', 'dependency2', 'dependency3'} -``` -*** -### FunctionDef _add_dependencies(self, parent) -**_add_dependencies**: The function of _add_dependencies is to add dependencies on parameters and expandable nodes to the current MessageNode object. - -**Parameters**: -- parent: The parent node to add as a dependency. - -**Code Description**: -The `_add_dependencies` function is used to add dependencies on parameters and expandable nodes to the current MessageNode object. It takes a `parent` parameter, which is the parent node to be added as a dependency. - -The function first checks if the `parent` is not the same as the current object itself. If it is, an assertion error is raised with the message "Cannot add self as a parent." - -Next, it checks if the `parent` is an instance of the `Node` class. If it is not, an assertion error is raised with a message indicating that the `parent` is not a Node. - -If both assertions pass, the function proceeds to add the dependencies. It updates the `_dependencies` dictionary of the current object by taking the union of the `parameter` and `expandable` dependencies of the `parent` node. This is done using the bitwise OR operator (`|`). - -Finally, the function returns without any explicit return value. - -**Note**: -- The `parent` parameter should be a valid Node object. -- The function assumes that the current object is a MessageNode. -- The function updates the `_dependencies` dictionary of the current object to include the dependencies from the `parent` node. -*** -## ClassDef ExceptionNode -**ExceptionNode**: The ExceptionNode class represents a node containing an exception message. - -**attributes**: -- value: The exception value. -- inputs: The input nodes of the ExceptionNode. It can be a list or a dictionary. -- description: A string that describes the ExceptionNode. -- constraint: A constraint on the node. -- name: The name of the node. -- info: Additional information about the node. - -**Code Description**: -The ExceptionNode class is a subclass of the MessageNode class and inherits its attributes and methods. It overrides the __init__ method to include the value, inputs, description, constraint, name, and info parameters. The value parameter represents the exception value. The inputs parameter can be a list or a dictionary, and it represents the input nodes of the ExceptionNode. The description parameter is a string that describes the ExceptionNode. The constraint parameter specifies a constraint on the node. The name parameter is the name of the node. The info parameter is additional information about the node. - -The __init__ method initializes the ExceptionNode by calling the __init__ method of the MessageNode class and passing the value, inputs, description, constraint, name, and info parameters. It checks if the value is an instance of trace.ExecutionError and formats the value accordingly. It then calls the __init__ method of the MessageNode class and passes the formatted value, inputs, description, constraint, name, and info parameters. - -**Note**: -- The ExceptionNode class represents a node containing an exception message. -- The value parameter represents the exception value. -- The inputs parameter can be a list or a dictionary, and it represents the input nodes of the ExceptionNode. -- The description parameter is a string that describes the ExceptionNode. -- The constraint parameter specifies a constraint on the node. -- The name parameter is the name of the node. -- The info parameter is additional information about the node. - -**Output Example**: -A possible appearance of the ExceptionNode object when converted to a string could be: -"ExceptionNode: (node_name, dtype=, data=10)" -### FunctionDef __init__(self, value) -**__init__**: The function of __init__ is to initialize an instance of the ExceptionNode class. - -**parameters**: -- value: The exception value to be stored in the ExceptionNode. -- inputs: The inputs to the ExceptionNode, which can be either a list of nodes or a dictionary of nodes. -- description: A string that describes the ExceptionNode. The default value is "[ExceptionNode] This is node containing the error of execution." -- constraint: An optional constraint on the ExceptionNode. -- name: An optional name for the ExceptionNode. -- info: Additional information about the ExceptionNode. - -**Code Description**: -The __init__ method of the ExceptionNode class initializes an instance of the ExceptionNode with the given parameters. It first assigns the value parameter to the variable e. Then, it uses regular expression to extract the error type from the string representation of the exception value. The re.search function searches for the pattern "" in the string and retrieves the matched group, which represents the error type. - -Next, it imports the trace module from the opto package. This import is necessary because the isinstance function is used later in the code. - -The code then checks if the value is an instance of the ExecutionError class from the trace module. If it is not, it formats the exception message by concatenating the error type and the string representation of the exception value. This ensures that the exception message is informative and includes the error type. - -Finally, the super().__init__ method is called to initialize the ExceptionNode instance with the value, inputs, description, constraint, name, and info parameters. The super() function is used to call the __init__ method of the base class (Node) and pass the parameters to it. - -**Note**: -- The ExceptionNode class is used to represent a node in a computational graph that contains an exception value. It is typically used to handle errors that occur during the execution of code within a tracing context. -- The value parameter should be an instance of the Exception class or a subclass of it. -- The inputs parameter should be a list of nodes or a dictionary of nodes that serve as inputs to the ExceptionNode. -- The description parameter is optional and can be used to provide additional information about the ExceptionNode. -- The constraint parameter is optional and can be used to specify a constraint on the ExceptionNode. -- The name parameter is optional and can be used to assign a name to the ExceptionNode. -- The info parameter is optional and can be used to provide additional information about the ExceptionNode. -- When creating an instance of the ExceptionNode class, make sure to provide the necessary inputs and ensure that the value parameter is an instance of the Exception class or a subclass of it. -*** diff --git a/generated_docs/opto/trace/operators.md b/generated_docs/opto/trace/operators.md deleted file mode 100644 index 0296eb16..00000000 --- a/generated_docs/opto/trace/operators.md +++ /dev/null @@ -1,893 +0,0 @@ -## FunctionDef clone(x) -**clone**: The function of clone is to create a deep copy of the input object `x`. - -**parameters**: The parameters of this Function. -· x: The object to be cloned. It can be of any type. - -**Code Description**: The `clone` function is designed to generate a deep copy of the provided object `x`. This is achieved using the `copy.deepcopy` method from Python's `copy` module. A deep copy means that all levels of the object are copied recursively, ensuring that the new object is entirely independent of the original. This is particularly useful when dealing with complex objects that contain nested structures, as it prevents changes in the cloned object from affecting the original object and vice versa. - -**Note**: -- Ensure that the `copy` module is imported before using the `clone` function. -- Be aware that deep copying can be resource-intensive for large or complex objects, as it involves duplicating every element within the object. - -**Output Example**: -If `x` is a list `[1, 2, [3, 4]]`, calling `clone(x)` will return a new list `[1, 2, [3, 4]]` that is a deep copy of `x`. Changes to the nested list in the cloned object will not affect the original list. -## FunctionDef identity(x) -**identity**: The function of identity is to return a duplicate of the input object. - -**parameters**: The parameters of this Function. -· x: Any - The input object that will be duplicated. - -**Code Description**: The identity function takes a single parameter, x, and returns a duplicate of this parameter by calling its clone method. The clone method is a part of the Node class, which creates and returns a duplicate of the current Node object. When identity is called with an object, it effectively behaves the same as calling the clone method on that object. This ensures that the original object remains unmodified, and a new instance with the same attributes and states is returned. - -The identity function is integral to operations that require object duplication within the project. It relies on the clone method from the Node class, which imports the clone function from the opto.trace.operators module and applies it to the current instance of the Node class. This standardized operation ensures consistency in how objects are duplicated across the project. - -**Note**: -- Ensure that the input object x has a clone method implemented; otherwise, the identity function will raise an AttributeError. -- The identity function does not modify the original object; it only creates and returns a duplicate. - -**Output Example**: If the input object x is an instance of the Node class with specific attributes and states, the return value of the identity function will be a new instance of the Node class that is a duplicate of the original instance. For example, if the original Node instance has attributes like name and value, the cloned instance will have the same name and value. -## FunctionDef pos(x) -**pos**: The function of pos is to return the unary positive of the input value x. - -**parameters**: The parameters of this Function. -· x: Any - The input value to which the unary positive operator will be applied. - -**Code Description**: The pos function takes a single parameter x and applies the unary positive operator to it. This operator is represented by the plus sign (+) in Python. The unary positive operator does not change the value of x; it simply returns x itself. This function is useful in contexts where the unary positive operator needs to be explicitly applied to a value. - -In the project, the pos function is called by the __pos__ method of the Node class located in opto\trace\nodes.py. When the unary positive operator is used on an instance of the Node class (e.g., +node_instance), the __pos__ method is invoked, which in turn calls the pos function from the opto.trace.operators module. This ensures that the unary positive operation is consistently applied to instances of the Node class. - -**Note**: -- The pos function does not alter the input value; it simply returns it. -- Ensure that the input value x is of a type that supports the unary positive operator. - -**Output Example**: -If the input value x is 5, the return value will be 5. -If the input value x is -3.2, the return value will be -3.2. -## FunctionDef neg(x) -**neg**: The function of neg is to return the negation of the input value. - -**parameters**: The parameters of this Function. -· x: The input value to be negated. It can be of any type that supports the unary negation operator. - -**Code Description**: The neg function takes a single parameter, x, and returns its negation. This is achieved using the unary negation operator (-). The function is designed to work with any type that supports this operator, such as integers, floats, and other numeric types. - -In the context of the project, the neg function is called by the __neg__ method of the Node class in the opto\trace\nodes.py module. When the unary negation operator is applied to an instance of the Node class (e.g., -node_instance), the __neg__ method is invoked. This method imports the neg function from the opto.trace.operators module and applies it to the instance, effectively negating the Node object. - -**Note**: Ensure that the input value x is of a type that supports the unary negation operator to avoid runtime errors. - -**Output Example**: If the input value x is 5, the function will return -5. If the input value x is -3.2, the function will return 3.2. -## FunctionDef abs(x) -**abs**: The function of abs is to return the absolute value of the input x. - -**parameters**: The parameters of this Function. -· x: Any - The input value for which the absolute value is to be calculated. - -**Code Description**: The abs function takes a single parameter x and returns its absolute value. The function is a straightforward wrapper around Python's built-in abs() function, which computes the absolute value of a given number. This function is designed to be used within the opto.trace.operators module. - -In the context of its usage within the project, the abs function is called by the __abs__ method of the Node class located in opto\trace\nodes.py. When the __abs__ method is invoked on an instance of the Node class, it imports the abs function from the opto.trace.operators module and applies it to the instance. This allows the Node class to leverage the abs function to compute the absolute value of its instances. - -**Note**: -- Ensure that the input x is a type that supports the absolute value operation, such as int, float, or any custom object that implements the __abs__ method. -- The function relies on Python's built-in abs() function, so its behavior and limitations are consistent with that. - -**Output Example**: -- If x is -5, the function will return 5. -- If x is 3.14, the function will return 3.14. -- If x is an instance of a custom class that implements the __abs__ method, the function will return the result of that method. -## FunctionDef invert(x) -**invert**: The function of invert is to perform a bitwise NOT operation on the input value x. - -**parameters**: The parameters of this Function. -· x: The input value on which the bitwise NOT operation will be performed. It can be of any type that supports the bitwise NOT operation. - -**Code Description**: The invert function takes a single parameter x and returns the result of applying the bitwise NOT operation to x. The bitwise NOT operation, denoted by the tilde (~) operator, inverts each bit of the input value. For example, if x is an integer, each bit in the binary representation of x will be flipped (0s become 1s and 1s become 0s). - -In the context of the project, the invert function is called by the __invert__ method of the Node class in the opto\trace\nodes.py module. When the __invert__ method is invoked on an instance of the Node class, it imports the invert function from the opto.trace.operators module and applies it to the instance. This allows the Node class to support the bitwise NOT operation using the ~ operator. - -**Note**: Ensure that the input value x is of a type that supports the bitwise NOT operation. Using types that do not support this operation will result in a TypeError. - -**Output Example**: -- If x is an integer with a value of 5, the return value will be -6. -- If x is an integer with a value of 0, the return value will be -1. -## FunctionDef round(x, n) -**round**: The function of round is to round a given value `x` to a specified number of decimal places `n`. - -**parameters**: The parameters of this Function. -· x: The value to be rounded. This can be of any type that supports rounding. -· n: The number of decimal places to round to. This can be of any type that can be interpreted as an integer. - -**Code Description**: The `round` function is designed to round a given value `x` to `n` decimal places. It takes two parameters: `x`, which is the value to be rounded, and `n`, which specifies the number of decimal places to round to. The function returns the result of the built-in `round` function applied to these parameters. - -In the context of its usage within the project, the `round` function is called by the `__round__` method of the `Node` class in the `opto\trace\nodes.py` file. The `__round__` method imports the `round` function from `opto.trace.operators` and applies it to the instance of the `Node` class (`self`). If a parameter `n` is provided, it is passed to the `round` function; otherwise, `None` is passed. - -**Note**: -- Ensure that the types of `x` and `n` are compatible with the built-in `round` function to avoid runtime errors. -- The `round` function in this context is a wrapper around Python's built-in `round` function, so it inherits its behavior and limitations. - -**Output Example**: -If `x` is 3.14159 and `n` is 2, the function will return 3.14. -If `x` is 3.14159 and `n` is 0, the function will return 3. -## FunctionDef floor(x) -**floor**: The function of floor is to compute the largest integer less than or equal to a given number x. - -**parameters**: The parameters of this Function. -· x: A numeric value of any type (int, float, etc.) that you want to apply the floor operation to. - -**Code Description**: The floor function takes a single parameter x and returns the largest integer less than or equal to x. Internally, it uses the `math.floor` method from Python's math module to perform this operation. This function is useful in scenarios where you need to round down a floating-point number to the nearest whole number. - -In the project, this function is called by the `__floor__` method of the `Node` class located in `opto\trace\nodes.py`. The `__floor__` method imports the `floor` function from `opto.trace.operators` and applies it to the instance of the `Node` class. This indicates that the `Node` class instances can be floored directly, leveraging the `floor` function to achieve this. - -**Note**: Ensure that the input parameter x is a numeric value; otherwise, the function will raise a TypeError. This function is dependent on the `math` module, so ensure that it is available in your environment. - -**Output Example**: -- If `x` is 3.7, `floor(x)` will return 3. -- If `x` is -2.3, `floor(x)` will return -3. -## FunctionDef ceil(x) -**ceil**: The function of ceil is to return the smallest integer greater than or equal to a given number. - -**parameters**: The parameters of this Function. -· x: A numeric value of any type (int, float, etc.) that you want to round up to the nearest integer. - -**Code Description**: The ceil function is designed to round up a given numeric value to the nearest integer. It imports the math module and utilizes the math.ceil() method to perform this operation. The function takes a single parameter, x, which can be any numeric type. When called, it returns the smallest integer that is greater than or equal to x. - -In the context of the project, the ceil function is called by the __ceil__ method of the Node class located in opto\trace\nodes.py. This indicates that the Node class leverages the ceil function to provide a ceiling operation on its instances. When __ceil__ is invoked on a Node object, it imports the ceil function from opto.trace.operators and applies it to the Node instance, effectively rounding up the value represented by the Node. - -**Note**: Ensure that the input parameter x is a numeric value; otherwise, the function will raise a TypeError. The function relies on the math module, so it must be available in the environment where the code is executed. - -**Output Example**: -- If x = 4.2, ceil(x) will return 5. -- If x = -3.7, ceil(x) will return -3. -- If x = 7, ceil(x) will return 7. -## FunctionDef trunc(x) -**trunc**: The function of trunc is to truncate the decimal part of a number, returning the integer part. - -**parameters**: The parameters of this Function. -· x: The number to be truncated. It can be of any type that is compatible with the math.trunc function, typically an integer or a float. - -**Code Description**: The trunc function is designed to truncate the decimal part of a given number, effectively returning its integer part. This is achieved by utilizing the math.trunc function from Python's math module. When the trunc function is called with a number x, it imports the math module and then applies math.trunc to x, returning the truncated integer value. - -In the context of the project, the trunc function is called by the __trunc__ method of the Node class located in opto\trace\nodes.py. The __trunc__ method imports the trunc function from opto.trace.operators and applies it to the instance of the Node class. This indicates that the Node class instances can be truncated using the trunc function, ensuring that any Node object can be converted to its integer representation if needed. - -**Note**: -- Ensure that the input x is a type that can be handled by the math.trunc function, such as an integer or a float. -- The function will raise a TypeError if x is not a number. - -**Output Example**: -If the input x is 3.14, the function will return 3. -If the input x is -2.99, the function will return -2. -## FunctionDef add(x, y) -**add**: The function of add is to perform an addition operation on two inputs, x and y. - -**parameters**: The parameters of this Function. -· x: The first operand, which can be of any type. -· y: The second operand, which can be of any type. - -**Code Description**: The add function takes two parameters, x and y, and returns their sum. The function is designed to handle operands of any type, leveraging Python's dynamic typing and operator overloading capabilities. This means that the function can add numbers, concatenate strings, or combine other compatible types as defined by the '+' operator in Python. - -In the project, the add function is utilized in the __add__ method of the Node class located in opto\trace\nodes.py. When the __add__ method is called on a Node object, it imports the add function from opto.trace.operators and uses it to add the Node's data to another operand. This demonstrates the function's flexibility in handling different types of data within the Node class. - -**Note**: Ensure that the types of x and y are compatible with the '+' operator to avoid runtime errors. For example, adding a string to an integer will raise a TypeError. - -**Output Example**: -- If x = 3 and y = 5, add(x, y) will return 8. -- If x = "Hello" and y = " World", add(x, y) will return "Hello World". -## FunctionDef subtract(x, y) -**subtract**: The function of subtract is to perform a subtraction operation between two operands, x and y. - -**parameters**: The parameters of this Function. -· x: The first operand, which can be of any type that supports the subtraction operation. -· y: The second operand, which can be of any type that supports the subtraction operation. - -**Code Description**: The subtract function takes two parameters, x and y, and returns the result of subtracting y from x. This function is designed to handle any data types that support the subtraction operator (-). In the context of the project, this function is utilized by the __sub__ method of the Node class in the opto\trace\nodes.py module. When the subtraction operator (-) is used between two Node objects, the __sub__ method is invoked, which in turn calls the subtract function from the opto.trace.operators module. This allows for a seamless and consistent subtraction operation between Node objects. - -**Note**: Ensure that the operands x and y are of compatible types that support the subtraction operation to avoid runtime errors. - -**Output Example**: -- If x = 10 and y = 5, the function will return 5. -- If x = [1, 2, 3] and y = [1, 1, 1], the function will return [0, 1, 2] (assuming the operands are lists and the subtraction operation is defined for lists in this context). -## FunctionDef multiply(x, y) -**multiply**: The function of multiply is to perform a multiplication operation between two inputs, x and y. - -**parameters**: The parameters of this Function. -· x: The first operand in the multiplication operation. It can be of any type that supports the multiplication operator (*). -· y: The second operand in the multiplication operation. It can be of any type that supports the multiplication operator (*). - -**Code Description**: The multiply function takes two parameters, x and y, and returns the result of multiplying these two parameters using the multiplication operator (*). This function is designed to be generic and can handle any types of inputs that support the multiplication operation. - -In the context of the project, the multiply function is called by the __mul__ method of the Node class in the opto\trace\nodes.py module. When the __mul__ method is invoked, it imports the multiply function from the opto.trace.operators module and applies it to the current instance (self) and another operand (other). This allows for the multiplication of Node objects or Node-compatible objects using the * operator. - -**Note**: Ensure that the types of x and y are compatible with the multiplication operator to avoid runtime errors. If either x or y does not support multiplication, a TypeError will be raised. - -**Output Example**: -- If x = 3 and y = 4, multiply(x, y) will return 12. -- If x = [1, 2] and y = 3, multiply(x, y) will return [1, 2, 1, 2, 1, 2]. -## FunctionDef floor_divide(x, y) -**floor_divide**: The function of floor_divide is to perform floor division between two operands, x and y. - -**parameters**: The parameters of this Function. -· x: The dividend, which can be of any type that supports the floor division operation. -· y: The divisor, which can be of any type that supports the floor division operation. - -**Code Description**: The floor_divide function takes two parameters, x and y, and returns the result of the floor division operation (x // y). Floor division is an operation that divides two numbers and rounds down the result to the nearest integer. This function is designed to handle any types that support the floor division operator (//). - -In the context of the project, the floor_divide function is called by the __floordiv__ method of the Node class in the opto\trace\nodes.py module. When the __floordiv__ method is invoked on a Node object with another operand, it imports the floor_divide function from the opto.trace.operators module and applies it to the Node object and the other operand. This indicates that the floor_divide function is integral to the Node class's ability to handle floor division operations, ensuring that the operation is performed correctly and consistently within the project's framework. - -**Note**: Ensure that both x and y are of types that support the floor division operation to avoid runtime errors. The function does not perform type checking or validation, so improper types may lead to unexpected behavior or exceptions. - -**Output Example**: -If x = 7 and y = 3, the function call floor_divide(7, 3) will return 2, as 7 // 3 equals 2. -## FunctionDef divide(x, y) -**divide**: The function of divide is to perform division between two operands, x and y. - -**parameters**: The parameters of this Function. -· x: The dividend, which can be of any type that supports division. -· y: The divisor, which can be of any type that supports division. - -**Code Description**: The divide function takes two parameters, x and y, and returns the result of dividing x by y. This function is designed to handle any types that support the division operation. It is a straightforward implementation of the division operator, encapsulated within a function for modularity and reuse. - -In the context of the project, the divide function is called by the __truediv__ method of the Node class located in opto\trace\nodes.py. When the division operator (/) is used between two Node objects, the __truediv__ method is invoked. This method imports the divide function from opto.trace.operators and applies it to the current Node instance (self) and the other operand (other), which is converted to a Node if it is not already one. This ensures that the division operation is consistently handled within the framework of Node objects. - -**Note**: Ensure that the divisor y is not zero to avoid a ZeroDivisionError. Additionally, both x and y should be of compatible types that support the division operation. - -**Output Example**: -If x is 10 and y is 2, the function will return 5.0. -If x is 9 and y is 3, the function will return 3.0. -## FunctionDef mod(x, y) -**mod**: The function of mod is to perform the modulo operation between two values, x and y. - -**parameters**: The parameters of this Function. -· x: The dividend in the modulo operation. It can be of any type that supports the modulo operation. -· y: The divisor in the modulo operation. It can be of any type that supports the modulo operation. - -**Code Description**: The mod function takes two parameters, x and y, and returns the result of the modulo operation (x % y). This operation finds the remainder when x is divided by y. The function is designed to handle any types that support the modulo operation, making it versatile for various use cases. - -In the project, this function is utilized by the __mod__ method of the Node class in the opto\trace\nodes.py module. When the __mod__ method is called on a Node object with another value, it imports the mod function from the opto.trace.operators module and applies it to the Node object and the other value. This integration allows Node objects to use the modulo operation seamlessly with other values, enhancing their arithmetic capabilities. - -**Note**: Ensure that both x and y are of types that support the modulo operation to avoid runtime errors. - -**Output Example**: -- If x is 10 and y is 3, the return value will be 1. -- If x is 20 and y is 7, the return value will be 6. -## FunctionDef divmod(x, y) -**divmod**: The function of divmod is to perform the divmod operation on two inputs, x and y, and return the result. - -**parameters**: The parameters of this Function. -· x: The first operand, which can be of any type that supports the divmod operation. -· y: The second operand, which can be of any type that supports the divmod operation. - -**Code Description**: The divmod function takes two parameters, x and y, and applies the built-in Python divmod function to them. The divmod function returns a tuple containing the quotient and the remainder when dividing x by y. This function is a straightforward wrapper around Python's built-in divmod, providing a consistent interface for performing this operation within the project. - -In the context of its usage within the project, the divmod function is called by the __divmod__ method of the Node class in the opto\trace\nodes.py module. When the __divmod__ method is invoked on a Node object, it imports the divmod function from the opto.trace.operators module and applies it to the Node instance and another operand. This integration ensures that the divmod operation can be seamlessly used with Node objects, allowing for consistent and predictable behavior when performing division and modulus operations within the project's tracing framework. - -**Note**: Ensure that both x and y are of types that support the divmod operation to avoid runtime errors. The function relies on Python's built-in divmod, so the behavior and constraints of the built-in function apply here as well. - -**Output Example**: -If x is 10 and y is 3, the return value will be (3, 1), where 3 is the quotient and 1 is the remainder. -## FunctionDef power(x, y) -**power**: The function of power is to compute the result of raising x to the power of y. - -**parameters**: The parameters of this Function. -· x: The base value, which can be of any type that supports the power operation. -· y: The exponent value, which can be of any type that supports the power operation. - -**Code Description**: The power function takes two arguments, x and y, and returns the result of x raised to the power of y (x**y). This function is a simple implementation of the power operation and relies on Python's built-in exponentiation operator (**). - -In the context of the project, this function is utilized by the __pow__ method of the Node class in the opto\trace\nodes.py module. When the __pow__ method is called on a Node object with another value, it imports the power function from the opto.trace.operators module and applies it to the Node object and the other value. This allows for the use of the power operator (**) directly on Node objects, enabling more intuitive mathematical operations within the project's framework. - -**Note**: Ensure that the types of x and y are compatible with the power operation to avoid runtime errors. - -**Output Example**: -If x is 2 and y is 3, the function will return 8, as 2**3 equals 8. -## FunctionDef lshift(x, y) -**lshift**: The function of lshift is to perform a left bitwise shift operation on two given inputs, x and y. - -**parameters**: The parameters of this Function. -· x: The first operand, which can be of any type that supports the left shift operation. -· y: The second operand, which can be of any type that supports the left shift operation. - -**Code Description**: The lshift function takes two parameters, x and y, and returns the result of the left bitwise shift operation (x << y). This operation shifts the bits of x to the left by the number of positions specified by y. The function is designed to work with any types that support the left shift operation, typically integers. - -In the context of the project, the lshift function is called by the __lshift__ method of the Node class in the opto\trace\nodes.py module. The __lshift__ method imports the lshift function from the opto.trace.operators module and applies it to the current instance (self) and another operand (other). This indicates that the Node class uses the lshift function to define its own left shift behavior, allowing instances of Node to be shifted left using the << operator. - -**Note**: Ensure that the operands x and y are of types that support the left shift operation to avoid runtime errors. - -**Output Example**: -If x is 4 (binary 100) and y is 2, the function will return 16 (binary 10000), as the bits of 4 are shifted left by 2 positions. -## FunctionDef rshift(x, y) -**rshift**: The function of rshift is to perform a bitwise right shift operation on two operands, x and y. - -**parameters**: The parameters of this Function. -· x: The first operand, which can be of any type that supports the right shift operation. -· y: The second operand, which can be of any type that supports the right shift operation. - -**Code Description**: The rshift function takes two parameters, x and y, and returns the result of the bitwise right shift operation (x >> y). This operation shifts the bits of x to the right by the number of positions specified by y. The function is designed to handle any type that supports the right shift operation, typically integers. - -In the context of its usage within the project, the rshift function is called by the __rshift__ method of the Node class in the opto\trace\nodes.py module. The __rshift__ method imports the rshift function from the opto.trace.operators module and applies it to the current instance (self) and another node (other). This indicates that the rshift function is used to facilitate bitwise right shift operations between nodes within the project. - -**Note**: Ensure that the operands x and y are of types that support the right shift operation to avoid runtime errors. - -**Output Example**: -If x is 8 (binary 1000) and y is 2, the function call rshift(8, 2) will return 2 (binary 10). -## FunctionDef and_(x, y) -**and_**: The function of and_ is to perform a bitwise AND operation between two inputs, x and y. - -**parameters**: The parameters of this Function. -· x: The first operand, which can be of any type that supports the bitwise AND operation. -· y: The second operand, which can be of any type that supports the bitwise AND operation. - -**Code Description**: The and_ function takes two parameters, x and y, and returns the result of the bitwise AND operation between them. This operation is denoted by the '&' symbol in Python. The function is straightforward and relies on Python's built-in bitwise AND operator to compute the result. - -In the context of its usage within the project, the and_ function is called by the __and__ method of the Node class in the opto\trace\nodes.py module. When the __and__ method is invoked on a Node object with another operand, it imports the and_ function from the opto.trace.operators module and applies it to the Node instance and the other operand. This allows for a seamless bitwise AND operation between Node objects or between a Node object and another compatible operand. - -**Note**: Ensure that the operands x and y are of types that support the bitwise AND operation to avoid any runtime errors. - -**Output Example**: -If x = 6 (binary 110) and y = 3 (binary 011), the function call and_(6, 3) will return 2 (binary 010). -## FunctionDef or_(x, y) -**or_**: The function of or_ is to perform a bitwise OR operation between two inputs, x and y. - -**parameters**: The parameters of this Function. -· x: The first operand for the bitwise OR operation. It can be of any type that supports the bitwise OR operation. -· y: The second operand for the bitwise OR operation. It can be of any type that supports the bitwise OR operation. - -**Code Description**: The or_ function takes two parameters, x and y, and returns the result of the bitwise OR operation between them. The bitwise OR operation is denoted by the "|" operator in Python. This function is designed to be a utility function that can be used wherever a bitwise OR operation is needed. - -In the context of its usage within the project, the or_ function is called by the __or__ method of the Node class in the opto\trace\nodes.py module. The __or__ method imports the or_ function from the opto.trace.operators module and applies it to the current Node instance (self) and another Node instance (other). This allows for the use of the "|" operator to combine two Node instances using the bitwise OR operation. - -**Note**: Ensure that the operands x and y are of types that support the bitwise OR operation to avoid TypeErrors. - -**Output Example**: If x is 5 (binary 0101) and y is 3 (binary 0011), the return value of or_(x, y) would be 7 (binary 0111). -## FunctionDef xor(x, y) -**xor**: The function of xor is to perform a bitwise XOR operation between two inputs, x and y. - -**parameters**: The parameters of this Function. -· x: Any - The first operand for the XOR operation. -· y: Any - The second operand for the XOR operation. - -**Code Description**: The xor function takes two parameters, x and y, and returns the result of the bitwise XOR operation between them. The bitwise XOR operation compares each bit of its operands and returns 1 if the bits are different, and 0 if they are the same. This function is useful in various scenarios, such as cryptography, error detection, and correction algorithms. - -In the context of the project, the xor function is called by the __xor__ method of the Node class in the opto\trace\nodes.py module. The __xor__ method imports the xor function from the opto.trace.operators module and applies it to the current Node instance and another Node instance or value. This allows for the use of the ^ operator to perform a bitwise XOR operation between Node objects, enhancing the functionality and usability of the Node class. - -**Note**: Ensure that the inputs x and y are of types that support the bitwise XOR operation, such as integers or objects that implement the __xor__ method. - -**Output Example**: Mock up a possible appearance of the code's return value. -If x = 5 (binary 0101) and y = 3 (binary 0011), the result of xor(x, y) would be 6 (binary 0110). -## FunctionDef lt(x, y) -**lt**: The function of lt is to compare two values and determine if the first value is less than the second value. - -**parameters**: The parameters of this Function. -· x: The first value to be compared. It can be of any type that supports comparison operations. -· y: The second value to be compared. It can be of any type that supports comparison operations. - -**Code Description**: The lt function takes two parameters, x and y, and returns the result of the comparison x < y. This function leverages Python's built-in less-than operator to perform the comparison. The function is designed to work with any data types that support the less-than comparison, such as integers, floats, and strings. The function returns a boolean value: True if x is less than y, and False otherwise. - -**Note**: -- Ensure that the types of x and y are compatible for comparison to avoid TypeError. -- This function does not handle cases where x and y are of different types that cannot be compared directly. - -**Output Example**: -- lt(3, 5) returns True because 3 is less than 5. -- lt(10, 2) returns False because 10 is not less than 2. -- lt('apple', 'banana') returns True because 'apple' is lexicographically less than 'banana'. -## FunctionDef le(x, y) -**le**: The function of le is to compare two values, x and y, and determine if x is less than or equal to y. - -**parameters**: The parameters of this Function. -· x: The first value to be compared. It can be of any type that supports comparison operations. -· y: The second value to be compared. It can be of any type that supports comparison operations. - -**Code Description**: The le function performs a comparison between two values, x and y, using the less than or equal to (<=) operator. It returns a boolean value: True if x is less than or equal to y, and False otherwise. This function is useful in scenarios where you need to enforce or check ordering constraints between two values. - -**Note**: -- Ensure that the types of x and y are compatible for comparison. If they are not, a TypeError will be raised. -- This function relies on the underlying implementation of the <= operator for the types of x and y. - -**Output Example**: -- le(3, 5) returns True because 3 is less than 5. -- le(5, 5) returns True because 5 is equal to 5. -- le(7, 5) returns False because 7 is greater than 5. -## FunctionDef eq(x, y) -**eq**: The function of eq is to compare two values, x and y, for equality. - -**parameters**: The parameters of this function. -· x: The first value to be compared. It can be of any data type. -· y: The second value to be compared. It can be of any data type. - -**Code Description**: The eq function takes two parameters, x and y, and returns a boolean value indicating whether the two parameters are equal. The comparison is performed using the equality operator (==), which checks if the values of x and y are the same. This function is useful for determining if two variables or objects hold the same value or state. - -**Note**: -- The function relies on the built-in equality operator (==), so the behavior of the comparison depends on how the equality operator is implemented for the data types of x and y. -- If x and y are of different types, the function will return False unless the types are comparable and considered equal by the equality operator. - -**Output Example**: -- eq(5, 5) returns True -- eq('hello', 'hello') returns True -- eq([1, 2, 3], [1, 2, 3]) returns True -- eq(5, '5') returns False -## FunctionDef ne(x, y) -**ne**: The function of ne is to compare two values, x and y, and determine if they are not equal. - -**parameters**: The parameters of this Function. -· x: The first value to be compared. It can be of any data type. -· y: The second value to be compared. It can be of any data type. - -**Code Description**: The ne function takes two parameters, x and y, and returns a boolean value indicating whether x is not equal to y. The function uses the != operator to perform the comparison. If x and y are not equal, the function returns True; otherwise, it returns False. This function is useful for scenarios where you need to check inequality between two values. - -**Note**: -- Ensure that the data types of x and y are compatible for comparison to avoid unexpected results. -- This function does not perform type conversion; it strictly compares the values as they are. - -**Output Example**: -- ne(5, 3) returns True because 5 is not equal to 3. -- ne('apple', 'orange') returns True because the strings 'apple' and 'orange' are not equal. -- ne(10, 10) returns False because both values are equal. -## FunctionDef ge(x, y) -**ge**: The function of ge is to compare two values and determine if the first value is greater than or equal to the second value. - -**parameters**: The parameters of this Function. -· x: The first value to be compared. It can be of any type that supports comparison operations. -· y: The second value to be compared. It can be of any type that supports comparison operations. - -**Code Description**: The ge function takes two parameters, x and y, and returns the result of the comparison x >= y. This means it checks if x is greater than or equal to y. The function leverages Python's built-in comparison operators to perform this task. The return value is a boolean: True if x is greater than or equal to y, and False otherwise. - -**Note**: -- Ensure that the types of x and y are compatible for comparison to avoid TypeErrors. -- This function is useful in scenarios where conditional logic is based on the comparison of two values. - -**Output Example**: -- ge(5, 3) returns True because 5 is greater than 3. -- ge(2, 2) returns True because 2 is equal to 2. -- ge(1, 4) returns False because 1 is not greater than or equal to 4. -## FunctionDef gt(x, y) -**gt**: The function of gt is to compare two values and determine if the first value is greater than the second value. - -**parameters**: The parameters of this Function. -· x: The first value to be compared. It can be of any type that supports the greater-than (>) comparison. -· y: The second value to be compared. It can be of any type that supports the greater-than (>) comparison. - -**Code Description**: The gt function takes two parameters, x and y, and returns the result of the comparison x > y. This means that the function evaluates whether the value of x is greater than the value of y. The function is designed to work with any data types that support the greater-than comparison operator. The return value is a boolean: True if x is greater than y, and False otherwise. - -**Note**: -- Ensure that the types of x and y are compatible for comparison using the greater-than operator. If the types are not compatible, a TypeError will be raised. -- This function does not perform any type checking or validation, so it is the responsibility of the user to provide appropriate arguments. - -**Output Example**: -- gt(5, 3) returns True because 5 is greater than 3. -- gt(2, 4) returns False because 2 is not greater than 4. -- gt('b', 'a') returns True because 'b' is greater than 'a' in lexicographical order. -## FunctionDef cond(condition, x, y) -**cond**: The function of cond is to select and return `x` if `condition` is True, otherwise it returns `y`. - -**parameters**: The parameters of this Function. -· condition: A boolean or any value that can be evaluated as a boolean. -· x: The value to be returned if `condition` is True. -· y: The value to be returned if `condition` is False. - -**Code Description**: The `cond` function is a simple utility that evaluates a given `condition` and returns one of two provided values based on the result of that evaluation. Specifically, if `condition` evaluates to True, the function returns `x`; otherwise, it returns `y`. - -The function begins by ensuring that all input data (`x`, `y`, and `condition`) are read and assigned to local variables. This step is somewhat redundant in this context but ensures that the inputs are processed. The core logic is implemented in a single return statement that uses a conditional expression (ternary operator) to decide which value to return based on the truthiness of `condition`. - -This function is called in the project by unit tests located in `tests\unit_tests\test_nodes.py`. These tests likely verify the correctness of the `cond` function by passing various conditions and corresponding values for `x` and `y`, ensuring that the function returns the expected result in each case. - -**Note**: -- Ensure that `condition` is a value that can be evaluated as a boolean. -- The function does not perform any type checking or validation on the inputs. - -**Output Example**: -- If `condition` is True, `x` is returned. -- If `condition` is False, `y` is returned. - -For instance: -- `cond(True, 'apple', 'orange')` returns `'apple'`. -- `cond(False, 'apple', 'orange')` returns `'orange'`. -## FunctionDef not_(x) -**not_**: The function of not_ is to return the logical negation of the input value x. - -**parameters**: The parameters of this Function. -· x: Any - The input value to be negated. - -**Code Description**: The not_ function takes a single parameter x of any type and returns the logical negation of x. In Python, the logical negation operator `not` is used to invert the truth value of the operand. If x is a truthy value (e.g., True, non-zero numbers, non-empty collections), the function will return False. Conversely, if x is a falsy value (e.g., False, 0, None, empty collections), the function will return True. This function is useful for scenarios where you need to invert the boolean value of a given input. - -**Note**: -- The input parameter x can be of any type, but the function will evaluate its truthiness according to Python's standard rules for boolean context. -- Ensure that the input value is appropriate for logical negation to avoid unexpected results. - -**Output Example**: -- not_(True) will return False. -- not_(0) will return True. -- not_([1, 2, 3]) will return False. -- not_('') will return True. -## FunctionDef is_(x, y) -**is_**: The function of is_ is to determine whether x is equal to y using identity comparison. - -**parameters**: The parameters of this Function. -· x: The first object to be compared. -· y: The second object to be compared. - -**Code Description**: The is_ function checks if the two provided arguments, x and y, are the same object in memory. This is done using the identity operator `is`, which returns True if both x and y refer to the same object, and False otherwise. This type of comparison is different from the equality operator `==`, which checks if the values of the objects are equal, not necessarily if they are the same object. - -**Note**: -- Use this function when you need to verify that two variables point to the exact same object, not just equivalent values. -- This function is particularly useful when dealing with singleton objects or when you need to ensure that two references are indeed pointing to the same memory location. - -**Output Example**: -- `is_(a, b)` returns `True` if `a` and `b` are the same object. -- `is_(a, b)` returns `False` if `a` and `b` are different objects, even if they have the same content. -## FunctionDef is_not(x, y) -**is_not**: The function of is_not is to determine whether two variables, `x` and `y`, are not the same object in memory. - -**parameters**: The parameters of this Function. -· x: The first variable to be compared. -· y: The second variable to be compared. - -**Code Description**: The `is_not` function checks if the two provided variables, `x` and `y`, do not refer to the same object in memory. This is achieved using the `is not` operator in Python, which returns `True` if `x` and `y` are not the same object, and `False` otherwise. This function is useful when you need to ensure that two variables are distinct objects, rather than just having the same value. - -**Note**: -- This function checks for object identity, not equality of values. Two different objects with the same value will still return `True`. -- This function is particularly useful in scenarios where object identity is crucial, such as when dealing with mutable objects or singleton patterns. - -**Output Example**: -- `is_not(5, 5)` would return `False` because both `5`s are the same immutable integer object. -- `is_not([], [])` would return `True` because each `[]` creates a new list object in memory. -- `is_not(a, b)` where `a` and `b` are references to the same object would return `False`. -## FunctionDef in_(x, y) -**in_**: The function of in_ is to determine whether an element x is present within a collection y. - -**parameters**: The parameters of this Function. -· x: The element to be checked for presence within the collection y. -· y: The collection in which the presence of element x is to be checked. - -**Code Description**: The in_ function takes two parameters, x and y, and returns a boolean value indicating whether x is present in y. This is achieved using Python's built-in membership operator `in`, which checks for the presence of an element within a collection such as a list, tuple, set, or dictionary. The function is straightforward and leverages Python's efficient membership testing capabilities. - -In the context of its usage within the project, the in_ function is called by the __contains__ method of the Node class in the opto\trace\nodes.py module. The __contains__ method uses the in_ function to determine if a given item is part of the Node instance. This is done by importing the in_ function from the opto.trace.operators module and applying it to the item and the Node instance itself. This integration ensures that the Node class can utilize the in_ function to perform membership tests, thereby enhancing its functionality. - -**Note**: -- Ensure that the collection y supports the membership test operation. -- The function will raise a TypeError if y is not a collection type that supports the `in` operator. - -**Output Example**: -- If x is 3 and y is [1, 2, 3, 4], the function will return True. -- If x is 'a' and y is 'hello', the function will return False. -## FunctionDef not_in(x, y) -**not_in**: The function of not_in is to determine whether a given element `x` is not present within another collection `y`. - -**parameters**: The parameters of this function. -· x: The element to be checked for non-membership within the collection `y`. -· y: The collection in which the presence of the element `x` is to be checked. - -**Code Description**: The not_in function takes two parameters, `x` and `y`. It evaluates whether the element `x` is not contained within the collection `y`. The function returns a boolean value: `True` if `x` is not in `y`, and `False` if `x` is in `y`. This is achieved using the `not in` operator in Python, which checks for non-membership. - -**Note**: -- The collection `y` can be any iterable, such as a list, tuple, set, or string. -- The function does not modify the input parameters. -- Ensure that `y` is a valid iterable to avoid runtime errors. - -**Output Example**: -- `not_in(3, [1, 2, 4, 5])` returns `True` because 3 is not in the list `[1, 2, 4, 5]`. -- `not_in('a', 'apple')` returns `False` because 'a' is in the string 'apple'. -## FunctionDef getitem(x, index) -**getitem**: The function of getitem is to retrieve an element from a given object `x` using the specified `index`. - -**parameters**: The parameters of this Function. -· x: The object from which an element is to be retrieved. This can be any type that supports indexing, such as lists, tuples, or dictionaries. -· index: The index or key used to access the element within the object `x`. - -**Code Description**: The getitem function is a straightforward implementation of the indexing operation. It takes two parameters: `x` and `index`. The function returns the element of `x` located at the position specified by `index`. This is achieved using the standard indexing syntax `x[index]`. - -In the context of its usage within the project, the getitem function is called by the `__getitem__` method of the `Node` class in the `opto.trace.nodes` module. When the `__getitem__` method is invoked on a `Node` instance with a specific key, it imports the getitem function from the `opto.trace.operators` module and uses it to retrieve the corresponding element from the `Node` instance. This allows for a modular and reusable approach to element retrieval within the project. - -**Note**: -- Ensure that the object `x` supports the indexing operation with the provided `index`. Otherwise, an error will be raised. -- The type of `index` should be compatible with the indexing mechanism of the object `x`. - -**Output Example**: -If `x` is a list `[10, 20, 30]` and `index` is `1`, the return value of `getitem(x, index)` will be `20`. -## FunctionDef pop(x, index) -**pop**: The function of pop is to remove and return an element from a list `x` at the specified `index`. - -**parameters**: The parameters of this Function. -· x: The list from which an element will be removed. -· index: The position of the element to be removed from the list. - -**Code Description**: The `pop` function is designed to operate on a list `x` and remove the element located at the specified `index`. The function utilizes the built-in `pop` method of Python lists, which not only removes the element at the given index but also returns it. This allows the user to both modify the list by removing an element and capture the removed element for further use. The function is straightforward and leverages Python's native list handling capabilities to achieve its purpose efficiently. - -**Note**: -- Ensure that the `index` provided is within the valid range of the list `x`. If the `index` is out of range, a `IndexError` will be raised. -- The list `x` will be modified in place, meaning the original list will be changed after the function call. - -**Output Example**: -If `x = [10, 20, 30, 40]` and `index = 2`, calling `pop(x, index)` will return `30` and modify `x` to `[10, 20, 40]`. -## FunctionDef len_(x) -**len_**: The function of len_ is to return the length of the input object x. - -**parameters**: The parameters of this Function. -· x: Any - The input object whose length is to be calculated. - -**Code Description**: The len_ function is a utility that computes and returns the length of the input object x by leveraging Python's built-in len() function. This function is designed to be a simple wrapper around the built-in len() function, providing a consistent interface for length calculation within the project. - -The function is called by the len method of the Node class in the opto\trace\nodes.py module. When the len method of a Node instance is invoked, it imports the len_ function from the opto.trace.operators module and applies it to the Node instance. This design allows the Node class to utilize the len_ function for determining its length, ensuring modularity and reusability of the len_ function across different parts of the project. - -**Note**: Ensure that the input object x is of a type that supports the len() operation, such as lists, strings, tuples, or other collections. Passing an unsupported type will result in a TypeError. - -**Output Example**: -- If x is a list [1, 2, 3], len_(x) will return 3. -- If x is a string "hello", len_(x) will return 5. -## FunctionDef ord_(x) -**ord_**: The function of ord_ is to return the Unicode number of a character. - -**parameters**: The parameters of this Function. -· x: Any - The character whose Unicode number is to be returned. - -**Code Description**: The ord_ function takes a single parameter, x, which is expected to be a character. It returns the Unicode code point of that character using Python's built-in ord() function. The ord() function is a standard Python function that converts a single character into its corresponding Unicode integer value. This is useful for various applications, such as encoding, decoding, and character manipulation. - -**Note**: -- The input parameter x should be a single character. If x is not a single character, the ord() function will raise a TypeError. -- This function is designed to handle any character that can be represented in Unicode. - -**Output Example**: -- ord_('A') will return 65. -- ord_('€') will return 8364. -## FunctionDef chr_(x) -**chr_**: The function of chr_ is to return the character corresponding to a given Unicode number. - -**parameters**: The parameters of this Function. -· x: A Unicode number (integer) that represents a specific character. - -**Code Description**: The chr_ function takes a single parameter, x, which is expected to be an integer representing a Unicode code point. The function then uses Python's built-in chr() function to convert this Unicode number into its corresponding character. The result is the character that the Unicode number represents. This function is useful for converting numerical Unicode values into their string character equivalents. - -**Note**: -- The input parameter x must be a valid Unicode code point. If x is not a valid Unicode code point, a ValueError will be raised. -- The function does not perform any type checking or validation on the input parameter, so it is the caller's responsibility to ensure that x is a valid integer within the Unicode range. - -**Output Example**: -- chr_(65) will return 'A'. -- chr_(8364) will return '€'. -## FunctionDef concat(x, y) -**concat**: The function of concat is to concatenate two given inputs, x and y. - -**parameters**: The parameters of this Function. -· x: The first input to be concatenated. It can be of any type. -· y: The second input to be concatenated. It can be of any type. - -**Code Description**: The concat function takes two parameters, x and y, and returns their concatenation using the + operator. This function is designed to handle inputs of any type, leveraging Python's dynamic typing and the + operator's ability to concatenate various data types such as strings, lists, and tuples. - -In the context of its usage within the project, the concat function is called by the __add__ method of the Node class in the opto\trace\nodes.py module. When the __add__ method is invoked, it checks the type of the _data attribute of the Node instance. If _data is a string, the concat function is used to concatenate the current Node instance with another Node instance created from the other parameter. This ensures that string concatenation is handled appropriately within the Node class. - -**Note**: -- Ensure that the types of x and y are compatible with the + operator to avoid TypeErrors. -- The behavior of the + operator varies depending on the types of x and y. For example, it concatenates strings and lists but adds numbers. - -**Output Example**: -- If x is "Hello" and y is "World", the return value will be "HelloWorld". -- If x is [1, 2] and y is [3, 4], the return value will be [1, 2, 3, 4]. -- If x is (1, 2) and y is (3, 4), the return value will be (1, 2, 3, 4). -## FunctionDef lower(x) -**lower**: The function of lower is to convert all characters in the input `x` to lower case. - -**parameters**: The parameters of this Function. -· x: Any - The input value that will be converted to lower case. It is expected to be a string or an object that has a `lower()` method. - -**Code Description**: The `lower` function takes a single parameter `x` and returns the result of calling the `lower()` method on `x`. This method is typically available on string objects in Python and converts all uppercase characters in the string to their lowercase counterparts. If `x` is not a string or does not have a `lower()` method, the function will raise an AttributeError. - -**Note**: -- Ensure that the input `x` is a string or an object that implements a `lower()` method to avoid runtime errors. -- This function does not handle non-string inputs that do not have a `lower()` method. - -**Output Example**: -```python -lower("HELLO") # Returns "hello" -lower("Python") # Returns "python" -``` -## FunctionDef upper(x) -**upper**: The function of upper is to convert all characters in the input to upper case. - -**parameters**: The parameters of this Function. -· x: Any - The input value that will be converted to upper case. This can be any type that supports the `upper()` method, typically a string. - -**Code Description**: The `upper` function takes a single parameter `x` and returns the result of calling the `upper()` method on `x`. The `upper()` method is a built-in string method in Python that converts all lowercase letters in a string to uppercase letters. If `x` is not a string or does not support the `upper()` method, the function will raise an AttributeError. - -**Note**: -- Ensure that the input `x` is of a type that supports the `upper()` method, typically a string, to avoid runtime errors. -- This function does not modify the original input but returns a new string with all characters in upper case. - -**Output Example**: -```python -result = upper("hello world") -print(result) # Output: "HELLO WORLD" -``` -## FunctionDef title(x) -**title**: The function of title is to convert the first character of each word in a string to uppercase and the remaining characters to lowercase. - -**parameters**: The parameters of this Function. -· x: Any - The input parameter which is expected to be a string. - -**Code Description**: The title function takes a single parameter, x, which is expected to be a string. It applies the title() method to the string, which capitalizes the first character of each word and converts all other characters to lowercase. This is useful for formatting strings in a standardized way, such as for titles or headings. - -**Note**: -- The input should be a string for the function to work correctly. If the input is not a string, it may result in an AttributeError since the title() method is specific to string objects. -- This function does not handle non-alphabetic characters differently; they will remain unchanged. - -**Output Example**: -If the input string is "hello world", the function will return "Hello World". -If the input string is "PYTHON programming", the function will return "Python Programming". -## FunctionDef swapcase(x) -**swapcase**: The function of swapcase is to swap the case of all characters in the input: converting uppercase characters to lowercase and vice-versa. - -**parameters**: The parameters of this Function. -· x: Any - The input value whose characters' cases are to be swapped. This can be any type that supports the `swapcase` method, typically a string. - -**Code Description**: The swapcase function takes a single parameter `x` and returns a new value where all uppercase characters in `x` are converted to lowercase, and all lowercase characters are converted to uppercase. The function leverages the built-in `swapcase` method available on string-like objects in Python. This method is particularly useful for text processing tasks where case conversion is required. - -**Note**: -- The input `x` must be of a type that supports the `swapcase` method, such as a string. If `x` does not support this method, the function will raise an AttributeError. -- The function does not modify the original input but returns a new value with the cases swapped. - -**Output Example**: -- If the input is `"Hello World"`, the output will be `"hELLO wORLD"`. -- If the input is `"Python3.8"`, the output will be `"pYTHON3.8"`. -## FunctionDef capitalize(x) -**capitalize**: The function of capitalize is to convert the first character of a string to uppercase. - -**parameters**: The parameters of this Function. -· x: Any - The input value that is expected to be a string. - -**Code Description**: The capitalize function takes a single parameter, `x`, which is expected to be a string. It utilizes the built-in `capitalize` method of Python strings to convert the first character of the string to uppercase while leaving the rest of the string unchanged. The function then returns the modified string. If `x` is not a string, the function will raise an AttributeError since the `capitalize` method is not available for non-string types. - -**Note**: -- Ensure that the input `x` is a string to avoid runtime errors. -- This function does not modify the original string but returns a new string with the first character capitalized. - -**Output Example**: -```python -capitalize("hello world") # Returns "Hello world" -capitalize("python") # Returns "Python" -``` -## FunctionDef split(x, y, maxsplit) -**split**: The function of split is to divide a string `x` into parts based on the occurrence of a substring `y`, returning the segments of the string without the substring `y`. - -**parameters**: The parameters of this function. -· x: The main string that needs to be split. -· y: The substring used as the delimiter to split the main string `x`. -· maxsplit: An optional parameter that specifies the maximum number of splits to perform. The default value is -1, which means no limit on the number of splits. - -**Code Description**: The `split` function takes three parameters: `x`, `y`, and `maxsplit`. It utilizes Python's built-in `split` method to divide the string `x` into parts wherever the substring `y` occurs. The `maxsplit` parameter controls the maximum number of splits that can be performed. If `maxsplit` is not provided, or if it is set to -1, the function will split the string at all occurrences of the substring `y`. The function returns a list containing the parts of the string `x` that were separated by the substring `y`. - -**Note**: -- The function will return a list of strings. -- If the substring `y` is not found in the main string `x`, the function will return a list containing the original string `x` as its only element. -- If `maxsplit` is set to 0, the function will return a list containing the original string `x` as its only element, as no splitting will be performed. - -**Output Example**: -```python -split("hello world", " ") -# Output: ['hello', 'world'] - -split("apple,banana,cherry", ",", 1) -# Output: ['apple', 'banana,cherry'] - -split("one,two,three,four", ",", 2) -# Output: ['one', 'two', 'three,four'] - -split("no delimiter here", ",") -# Output: ['no delimiter here'] -``` -## FunctionDef strip(x, chars) -**strip**: The function of strip is to remove the leading and trailing characters from the input `x`. - -**parameters**: The parameters of this function. -· `x`: The input from which leading and trailing characters will be removed. It can be of any type that supports the `strip` method, typically a string. -· `chars`: Optional. A string specifying the set of characters to be removed. If not provided, whitespace characters will be removed by default. - -**Code Description**: The `strip` function is designed to clean up the input `x` by removing any leading and trailing characters specified by the `chars` parameter. If `chars` is not provided, the function defaults to removing whitespace characters. The function leverages the built-in `strip` method available in Python for strings, ensuring efficient and reliable performance. The return value is the cleaned version of `x` with the specified characters removed from both ends. - -**Note**: -- The input `x` must be of a type that supports the `strip` method, such as a string. -- If `chars` is not specified, the function will remove whitespace characters by default. -- This function does not modify the original input but returns a new string with the specified characters removed. - -**Output Example**: -- `strip(" hello ")` returns `"hello"`. -- `strip("##hello##", "#")` returns `"hello"`. -## FunctionDef replace(x, old, new, count) -**replace**: The function of replace is to replace all occurrences of a specified substring within a given string with another substring. - -**parameters**: The parameters of this function. -· x: The original string in which the replacement is to be made. -· old: The substring that needs to be replaced. -· new: The substring that will replace the old substring. -· count: The maximum number of occurrences to replace. If not specified, all occurrences will be replaced. The default value is -1, which means replace all occurrences. - -**Code Description**: The replace function takes four parameters: x, old, new, and count. It utilizes the built-in string method `replace` to substitute all instances of the substring specified by `old` with the substring specified by `new` within the string `x`. The `count` parameter controls the number of replacements to be made. If `count` is set to -1 (the default value), all occurrences of the substring `old` will be replaced by `new`. If `count` is a positive integer, only that many occurrences of `old` will be replaced. - -**Note**: -- The function is case-sensitive, meaning that it will only replace substrings that match the case of `old`. -- If `old` is not found in `x`, the original string `x` will be returned unchanged. -- The `count` parameter must be a non-negative integer or -1. - -**Output Example**: -- replace("hello world", "world", "there") returns "hello there". -- replace("hello world world", "world", "there", 1) returns "hello there world". -- replace("hello world", "WORLD", "there") returns "hello world" (case-sensitive). -## FunctionDef format(x) -**format**: The function of format is to fill in a string template with content using the str.format() method. - -**parameters**: The parameters of this Function. -· x: A string template that contains placeholders to be filled. -· *args: Positional arguments to be used for filling the placeholders in the string template. -· **kwargs: Keyword arguments to be used for filling the placeholders in the string template. - -**Code Description**: The format function takes a string template `x` and fills it with the provided positional (`*args`) and keyword arguments (`**kwargs`). It leverages Python's built-in `str.format()` method to perform this operation. The `str.format()` method allows for complex string formatting operations, including the insertion of variables, formatting of numbers, and more. By passing the arguments and keyword arguments to `x.format(*args, **kwargs)`, the function dynamically replaces the placeholders in the string template with the corresponding values. - -**Note**: -- Ensure that the string template `x` contains valid placeholders that match the provided arguments. -- The function will raise a `KeyError` if a placeholder in the template does not have a corresponding keyword argument. -- The function will raise an `IndexError` if a placeholder in the template does not have a corresponding positional argument. - -**Output Example**: -If the function is called as follows: -```python -format("Hello, {}!", "World") -``` -The return value will be: -```python -"Hello, World!" -``` - -If the function is called with keyword arguments: -```python -format("Hello, {name}!", name="Alice") -``` -The return value will be: -```python -"Hello, Alice!" -``` -## FunctionDef node_getattr(obj, attr) -**node_getattr**: The function of node_getattr is to get the value of the specified attribute from the given object. - -**Parameters**: -- obj: A Node object from which the attribute value is to be retrieved. -- attr: A string representing the name of the attribute to be retrieved. - -**Code Description**: -The `node_getattr` function takes in a `Node` object `obj` and a string `attr` as parameters. It first checks if the `obj` is an instance of a dictionary. If it is, it retrieves the value associated with the `attr` key from the dictionary. Otherwise, it uses the `getattr` function to retrieve the value of the `attr` attribute from the `obj`. - -This function is used in the `getattr` method of the `Node` class in the `opto.trace.nodes.py` module. The `getattr` method is responsible for getting the value of the specified attribute from the `Node` object. It calls the `node_getattr` function passing itself (`self`) and the specified attribute (`key`) as arguments. - -**Note**: -- The `node_getattr` function assumes that the `obj` parameter is a valid `Node` object. -- If the `obj` is not an instance of a dictionary and does not have the specified attribute, a `AttributeError` will be raised. - -**Output Example**: -If `obj` is a dictionary and contains the attribute `attr`, the function will return the value associated with the `attr` key. Otherwise, it will return the value of the `attr` attribute from the `obj`. -## FunctionDef call(fun) -**call**: The function of call is to call the function `fun` with the provided arguments `args` and `kwargs`. - -**parameters**: -- `fun`: A Node object representing the function to be called. -- `*args`: Variable-length argument list. -- `**kwargs`: Keyword arguments. - -**Code Description**: -The `call` function takes a `fun` parameter, which is a Node object representing the function to be called. It also accepts variable-length arguments `args` and keyword arguments `kwargs`. The purpose of this function is to call the function `fun` with the provided arguments. - -First, the function assigns the value of `fun` to a local variable `fun` by accessing the `_data` attribute of the `fun` object. This allows the function to work with the actual function object rather than the Node object. - -Next, the function checks if the `fun` object is callable using the `callable()` function. If it is not callable, an `AssertionError` is raised with the message "The function must be callable." - -Then, the function calls the `fun` function with the provided arguments `args` and keyword arguments `kwargs` using the `*args` and `**kwargs` syntax. The result of the function call is stored in the `output` variable. - -Finally, the function returns the `output` variable. - -**Note**: -- The `fun` parameter must be a callable function. -- The `args` parameter can accept any number of positional arguments. -- The `kwargs` parameter can accept any number of keyword arguments. - -**Output Example**: -If the `fun` function is defined as follows: -```python -def add(a, b): - return a + b -``` -and the `call` function is called with `fun=add` and `args=(2, 3)`, the output will be `5`. diff --git a/generated_docs/opto/trace/propagators/graph_propagator.md b/generated_docs/opto/trace/propagators/graph_propagator.md deleted file mode 100644 index a80d35c4..00000000 --- a/generated_docs/opto/trace/propagators/graph_propagator.md +++ /dev/null @@ -1,166 +0,0 @@ -## ClassDef TraceGraph -**TraceGraph**: The function of TraceGraph is to serve as a feedback container used by the GraphPropagator. It represents a subgraph of nodes and stores user feedback. - -**attributes**: -- graph: A list of Node objects representing the priority queue of nodes in the subgraph. -- user_feedback: Any type of user feedback associated with the TraceGraph. - -**Code Description**: -The TraceGraph class is a feedback container used by the GraphPropagator. It is designed to store a subgraph of nodes and user feedback. The class includes the following methods: - -1. `__add__(self, other)`: This method is used to combine two TraceGraph objects. It checks if either of the user feedbacks is None, and if so, it assigns the non-None user feedback to the resulting TraceGraph. If both user feedbacks are not None, it checks if they are equal and assigns the user feedback to the resulting TraceGraph. The graph is created by merging the two graphs and sorting them based on the priority level. The method returns a new TraceGraph object with the merged graph and user feedback. - -**Note**: -- The TraceGraph class inherits from the AbstractFeedback class, which defines the `__add__` method. -- The `__add__` method ensures that the user feedback is consistent when combining two TraceGraph objects. - -**Output Example**: -```python -TraceGraph(graph=[(1, Node('A')), (2, Node('B'))], user_feedback=None) -``` - -**Reference Relationship**: -- The TraceGraph class is called by the `__add__` method in the TraceGraph class itself. -- The TraceGraph class is utilized in the `node_to_function_feedback` function in the `opto.optimizers.function_optimizer` module. -- The TraceGraph class is also used in the `init_feedback` method of the GraphPropagator class in the `opto.trace.propagators.graph_propagator` module. -### FunctionDef __add__(self, other) -**__add__**: The function of __add__ is to merge two TraceGraph objects while ensuring consistency in user feedback and combining their graphs. - -**parameters**: The parameters of this Function. -· self: The first instance of the TraceGraph object. -· other: The second instance of the TraceGraph object to be added to the first. - -**Code Description**: The __add__ method begins by asserting that at least one of the user_feedback attributes from the two TraceGraph objects is not None. If both user_feedback attributes are None, an assertion error is raised with the message "One of the user feedback should not be None." - -Next, the method determines the user_feedback for the resulting TraceGraph. If one of the user_feedback attributes is None, it uses the non-None user_feedback. If both are not None, it asserts that they are equal, ensuring consistency, and then uses the user_feedback from the first TraceGraph. - -The method then constructs a list of names from the nodes in the other TraceGraph's graph. It creates a complement list by including nodes from the first TraceGraph's graph that do not have names present in the other TraceGraph's graph. This ensures that nodes with the same name are not duplicated. - -Finally, the method merges the complement list and the other TraceGraph's graph using heapq.merge, which merges the lists based on the first element of each tuple (assumed to be a key). The merged list is used to create a new TraceGraph object, which is returned with the combined graph and the determined user_feedback. - -**Note**: -- Ensure that at least one of the TraceGraph objects has a non-None user_feedback before using the __add__ method. -- If both TraceGraph objects have user_feedback, they must be identical to avoid an assertion error. - -**Output Example**: -Assuming TraceGraph objects `tg1` and `tg2` are being added: -```python -tg1 + tg2 -``` -This would return a new TraceGraph object with a combined graph and consistent user_feedback. -*** -## ClassDef GraphPropagator -**GraphPropagator**: The GraphPropagator class is a subclass of the Propagator class. It provides specific implementations for the `init_feedback` and `_propagate` methods, as well as an `aggregate` method. The purpose of this class is to collect all the nodes seen in the path and compute the propagated feedback to the parent nodes based on the child node's description, data, and feedback. - -**attributes**: -- None - -**Code Description**: -- The `init_feedback` method takes two parameters: `node` (the current node) and `feedback` (the user feedback). It returns a TraceGraph object that represents the initial feedback for the given node. The TraceGraph object is created using the TraceGraph class and initialized with the current node and the user feedback. - -- The `_propagate` method takes a `child` parameter of type `MessageNode` and computes the propagated feedback to the parent nodes based on the child node's description, data, and feedback. It first creates a list of tuples representing the parents of the child node. Each tuple contains the level of the parent node and the parent node itself. Then, it aggregates the feedback from the child node and creates a TraceGraph object using the TraceGraph class. The aggregated feedback is computed by adding the feedback from the child node to a TraceGraph object that represents the parents of the child node. The external dependencies on parameters not visible in the current graph level are also included in the feedback. Finally, the method returns a dictionary where the keys are the parent nodes and the values are the propagated feedback. - -- The `aggregate` method takes a `feedback` parameter of type `Dict[Node, List[TraceGraph]]` and aggregates the feedback from multiple children. It first checks that the length of each value in the feedback dictionary is 1 and that each value is an instance of the TraceGraph class. Then, it sums the feedback values and returns the aggregated feedback as a TraceGraph object. - -**Note**: -- The `init_feedback` and `_propagate` methods are specific implementations of abstract methods defined in the Propagator class. -- The `aggregate` method is a helper method used by the `_propagate` method to aggregate feedback from multiple children. - -**Output Example**: -Given a properly implemented GraphPropagator object, the return value of the `_propagate` method might look like the following: -```python -{ - parent_node_1: feedback_data_1, - parent_node_2: feedback_data_2, - # ... other parent nodes and their respective feedback -} -``` -### FunctionDef init_feedback(self, node, feedback) -**init_feedback**: The function of init_feedback is to initialize feedback for a given node in the GraphPropagator. - -**parameters**: -- node: The node for which feedback is being initialized. -- feedback: The user feedback associated with the node. - -**Code Description**: -The init_feedback function is a method of the GraphPropagator class in the opto.trace.propagators.graph_propagator module. It is used to initialize feedback for a given node in the graph propagation process. The function takes two parameters: the node for which feedback is being initialized and the user feedback associated with the node. - -Inside the function, a TraceGraph object is created using the TraceGraph class. The TraceGraph object is initialized with a graph containing a single tuple representing the level of the node and the node itself. The user feedback is also assigned to the TraceGraph object. - -The TraceGraph object is then returned as the output of the init_feedback function. - -**Reference Relationship**: -- The init_feedback function is called by the backward method in the Node class in the opto.trace.nodes module. -- The init_feedback function is called by the propagate method in the GraphPropagator class in the opto.trace.propagators.graph_propagator module. - -**Note**: It is important to ensure that the node and feedback parameters are properly provided when calling the init_feedback function to avoid potential issues. - -**Output Example**: -If the node parameter is a Node object representing a node with level 2 and the feedback parameter is "Good job!", calling the init_feedback function will return a TraceGraph object with the following attributes: -- graph: [(2, Node)] -- user_feedback: "Good job!" -*** -### FunctionDef _propagate(self, child) -**_propagate**: The function of _propagate is to propagate feedback from a child node to its parent nodes in the graph. - -**parameters**: -- self: The current object. -- child: The child node from which the feedback is propagated. - -**Code Description**: -The `_propagate` function is a method of the `GraphPropagator` class in the `graph_propagator.py` module. It takes in the current object (`self`) and a child node (`child`) as parameters. The function first creates a list called `graph` by iterating over the parents of the child node and storing them along with their priority level. The priority level is determined by the `level` attribute of each parent node. The `graph` list represents the parents of the child node. - -Next, the function aggregates the feedback from the child node by calling the `aggregate` method of the current object (`self`). The `aggregate` method takes in the feedback from multiple children nodes and returns the aggregated feedback as a `TraceGraph` object. The feedback is obtained from the `feedback` attribute of the child node. - -The function then asserts that the aggregated feedback is an instance of the `TraceGraph` class. This ensures that the feedback is in the correct format. - -After that, the function iterates over the external dependencies of the child node and adds the feedback to each external dependency by calling the `_add_feedback` method of the external dependency node. This ensures that the feedback is correctly propagated to the external dependencies. - -Finally, the function returns a dictionary comprehension that maps each parent node to the aggregated feedback. - -The `_propagate` function is an essential part of the graph propagation process in the `GraphPropagator` class. It is responsible for propagating feedback from a child node to its parent nodes, ensuring that the feedback flows correctly through the graph structure. - -**Note**: -- The function assumes that the child node has a `parents` attribute that returns a list of parent nodes. -- The function assumes that the child node has an `external_dependencies` attribute that returns a set of external dependency nodes. -- The function assumes that the child node has a `feedback` attribute that contains the feedback from the child node. -- The function assumes that the feedback can be aggregated using the `aggregate` method. -- The function assumes that the external dependencies have a `_add_feedback` method to add the feedback from the child node. -- The function returns a dictionary that maps each parent node to the aggregated feedback. - -**Output Example**: -If the child node has two parents, the `_propagate` function will return a dictionary with two key-value pairs, where each key represents a parent node and the corresponding value represents the aggregated feedback from the child node. -```python -{ - parent_node1: aggregated_feedback1, - parent_node2: aggregated_feedback2 -} -``` -*** -### FunctionDef aggregate(self, feedback) -**aggregate**: The function of aggregate is to aggregate feedback from multiple children. - -**Parameters**: -- feedback: A dictionary that maps a Node to a list of TraceGraph objects representing the feedback from the child nodes. - -**Code Description**: -The `aggregate` function takes in a dictionary of feedback from multiple children. It first checks that each child has provided exactly one feedback and that the feedback is of type TraceGraph. Then, it calculates the sum of the feedback values for each child and stores them in a list called `values`. If the length of `values` is zero, indicating that there is no feedback, it returns a TraceGraph object with an empty graph and a user_feedback attribute set to None. Otherwise, it returns the sum of the values. - -This function is used to aggregate the feedback received from multiple children nodes. It ensures that the feedback is valid and performs the aggregation by summing the feedback values. The resulting aggregated feedback is returned as a TraceGraph object. - -**Reference Relationship**: -- This function is called by the `summarize` method in the `FunctionOptimizer` class in the `opto.optimizers.function_optimizer` module. -- This function is also called by the `_propagate` method in the `GraphPropagator` class in the `opto.trace.propagators.graph_propagator` module. - -**Note**: -- The feedback dictionary should contain exactly one feedback value for each child node. -- The feedback values should be of type TraceGraph. -- The function assumes that the feedback values can be summed. -- If there is no feedback, an empty TraceGraph object is returned. -- The function does not modify the input feedback dictionary. - -**Output Example**: -```python -TraceGraph(graph=[(1, Node('A')), (2, Node('B'))], user_feedback=None) -``` -*** diff --git a/generated_docs/opto/trace/propagators/propagators.md b/generated_docs/opto/trace/propagators/propagators.md deleted file mode 100644 index 3a080c74..00000000 --- a/generated_docs/opto/trace/propagators/propagators.md +++ /dev/null @@ -1,338 +0,0 @@ -## ClassDef AbstractPropagator -**AbstractPropagator**: The function of AbstractPropagator is to serve as a base class for propagating feedback from a child node to its parent nodes in a hierarchical structure. - -**attributes**: The attributes of this Class. -· This class does not define any attributes directly. - -**Code Description**: The AbstractPropagator class is designed to facilitate the propagation of feedback from a child node to its parent nodes. It provides a structured way to ensure that feedback is correctly propagated and formatted. - -- The `__call__` method is the primary interface for propagating feedback. When this method is called with a `MessageNode` instance as the `child` parameter, it performs several checks and operations: - - It asserts that the `child` is an instance of `MessageNode`. - - It ensures that all feedback values in the `child` node have a length of at most 1. - - It calls the `propagate` method to compute the propagated feedback. - - It verifies that the propagated feedback is a dictionary where the keys are the parent nodes and the values are the feedback. - - Finally, it returns the propagated feedback. - -- The `propagate` method is an abstract method that must be implemented by subclasses. It is responsible for computing the propagated feedback to the parent nodes of the given `child` node. The method should return a dictionary where the keys are the parent nodes and the values are the propagated feedback. Since this method is not implemented in the AbstractPropagator class, it raises a `NotImplementedError`. - -The AbstractPropagator class is extended by the `Propagator` class, which provides specific implementations for the `propagate` method and additional functionalities such as registering custom propagation functions and initializing feedback. - -**Note**: -- The `propagate` method must be implemented in any subclass of AbstractPropagator. -- The `__call__` method ensures that the feedback is correctly formatted and propagated, making it a critical part of the feedback propagation process. - -**Output Example**: -Given a properly implemented subclass of AbstractPropagator, the return value of the `__call__` method might look like the following: -```python -{ - parent_node_1: feedback_data_1, - parent_node_2: feedback_data_2, - # ... other parent nodes and their respective feedback -} -``` -This dictionary maps parent nodes to their respective propagated feedback. -### FunctionDef __call__(self, child) -**__call__**: The function of __call__ is to propagate the feedback from a child node to its parents. -**parameters**: -- child: A MessageNode object representing the child node for which the feedback needs to be propagated. - -**Code Description**: -The `__call__` function is a method of the `AbstractPropagator` class defined in the `propagators.py` module. It is responsible for propagating the feedback from a child node to its parents. The function takes a `child` parameter, which is expected to be a `MessageNode` object. - -The function first checks if the `child` is an instance of `MessageNode` and if the feedback from the child is of the correct format. The feedback should be a dictionary with the parents of the child as keys and the feedback values as values. - -Next, the function calls the `propagate` method of the concrete propagator class that inherits from `AbstractPropagator`. This method is expected to be implemented in the concrete propagator class and should perform the actual propagation of feedback. The `propagate` method returns the propagated feedback as a dictionary. - -The function then checks if the propagated feedback has the correct format, ensuring that it is a dictionary and that all the parents of the child are present as keys in the dictionary. - -Finally, the function returns the propagated feedback. - -**Note**: -- The `__call__` function is expected to be implemented in a concrete propagator class that inherits from `AbstractPropagator`. -- The `__call__` function assumes that the feedback from the child is already computed and stored in the `feedback` attribute of the child node. -- The function raises an error if the child is not an instance of `MessageNode` or if the feedback from the child is not of the correct format. - -**Output Example**: A possible appearance of the code's return value could be: -``` -{ - parent_node_1: feedback_value_1, - parent_node_2: feedback_value_2, - ... -} -``` -This example assumes that the propagated feedback is a dictionary with the parent nodes as keys and the corresponding feedback values as values. The actual content of the feedback will depend on the specific implementation and use case within the project. -*** -### FunctionDef propagate(self, child) -**propagate**: The function of propagate is to compute and return the propagated feedback to the parents of a given node. It returns a dictionary where the keys are the parents and the values are the propagated feedback. - -**parameters**: -- child: A MessageNode object representing the child node for which the feedback needs to be propagated. - -**Code Description**: -The `propagate` function is a method of the `AbstractPropagator` class defined in the `propagators.py` module. It is responsible for propagating the feedback from a child node to its parents. The function takes a `child` parameter, which is expected to be a `MessageNode` object. - -The function first checks if the `child` is an instance of `MessageNode` and if the feedback from the child is of the correct format. The feedback should be a dictionary with the parents of the child as keys and the feedback values as values. - -Next, the function calls the `propagate` method of the concrete propagator class that inherits from `AbstractPropagator`. This method is expected to be implemented in the concrete propagator class and should perform the actual propagation of feedback. The `propagate` method returns the propagated feedback as a dictionary. - -The function then checks if the propagated feedback has the correct format, ensuring that it is a dictionary and that all the parents of the child are present as keys in the dictionary. - -Finally, the function returns the propagated feedback. - -**Note**: -- The `propagate` function is expected to be implemented in a concrete propagator class that inherits from `AbstractPropagator`. -- The `propagate` function assumes that the feedback from the child is already computed and stored in the `feedback` attribute of the child node. -- The function raises an error if the child is not an instance of `MessageNode` or if the feedback from the child is not of the correct format. -*** -## ClassDef AbstractFeedback -**AbstractFeedback**: The function of AbstractFeedback is to serve as a feedback container used by propagators, supporting addition operations. - -**attributes**: This class does not define any attributes. - -**Code Description**: -The AbstractFeedback class is designed to act as a base class for feedback containers used by propagators. It defines the necessary interface for feedback objects that need to support addition operations. The class includes two methods: - -1. `__add__(self, other)`: This method is intended to handle the addition of two feedback objects. However, it raises a NotImplementedError, indicating that any subclass must implement this method to define the specific addition behavior. - -2. `__radd__(self, other)`: This method supports the addition operation when the AbstractFeedback object is on the right-hand side of the addition. It checks if the other operand is zero, which is useful for operations like sum where the initial value is zero. If the other operand is zero, it returns the current object (self). Otherwise, it delegates the addition operation to the `__add__` method. - -The AbstractFeedback class is utilized in the TraceGraph class, which inherits from AbstractFeedback. The TraceGraph class provides a concrete implementation of the `__add__` method, ensuring that feedback objects can be combined according to specific rules defined within TraceGraph. This relationship indicates that AbstractFeedback serves as a foundational component for more specialized feedback containers like TraceGraph. - -**Note**: -- Any subclass of AbstractFeedback must implement the `__add__` method to define how feedback objects should be combined. -- The `__radd__` method facilitates the use of AbstractFeedback objects in operations like sum, where the initial value might be zero. - -**Output Example**: -Since AbstractFeedback is an abstract class and does not implement the `__add__` method, it does not produce any direct output. However, a subclass like TraceGraph would produce combined feedback objects when the `__add__` method is called. For example, combining two TraceGraph objects might result in a new TraceGraph object with a merged graph and user feedback. -### FunctionDef __add__(self, other) -**__add__**: The function of __add__ is to define the addition operation for instances of the class. - -**parameters**: The parameters of this Function. -· self: The instance of the class on which the method is called. -· other: The instance or value to be added to the instance represented by self. - -**Code Description**: The __add__ method is intended to define the behavior of the addition operation for instances of the class it belongs to. However, in its current implementation, it raises a NotImplementedError, indicating that the addition operation is not yet implemented for this class. This method is crucial for enabling the use of the '+' operator with instances of the class. - -The __add__ method is also indirectly called by the __radd__ method within the same class. The __radd__ method is designed to handle the addition operation when the instance appears on the right-hand side of the '+' operator. If the other operand is zero, __radd__ returns the instance itself, supporting the use of the sum function. Otherwise, it delegates the addition operation to the __add__ method. - -**Note**: -- The __add__ method currently raises a NotImplementedError, so attempting to use the '+' operator with instances of this class will result in an error. -- To enable addition, the __add__ method needs to be properly implemented. -- The __radd__ method relies on __add__ for non-zero operands, so both methods should be considered together when implementing addition functionality. -*** -### FunctionDef __radd__(self, other) -**__radd__**: The function of __radd__ is to handle the addition operation when the instance appears on the right-hand side of the '+' operator. - -**parameters**: The parameters of this Function. -· self: The instance of the class on which the method is called. -· other: The instance or value to be added to the instance represented by self. - -**Code Description**: The __radd__ method is designed to support the addition operation when the instance of the class appears on the right-hand side of the '+' operator. This method is particularly useful for enabling the use of the sum function with instances of the class. When the other operand is zero, __radd__ returns the instance itself, ensuring that the sum function can correctly handle the initial zero value. If the other operand is not zero, the method delegates the addition operation to the __add__ method of the class. - -The __add__ method, which is called by __radd__ for non-zero operands, is intended to define the behavior of the addition operation for instances of the class. However, in its current implementation, __add__ raises a NotImplementedError, indicating that the addition operation is not yet implemented for this class. Therefore, to fully enable addition functionality, the __add__ method needs to be properly implemented. - -**Note**: -- The __add__ method currently raises a NotImplementedError, so attempting to use the '+' operator with instances of this class will result in an error. -- The __radd__ method relies on __add__ for non-zero operands, so both methods should be considered together when implementing addition functionality. - -**Output Example**: -- If `other` is 0, the method returns the instance itself. -- If `other` is not 0, the method attempts to return the result of `self.__add__(other)`, which currently raises a NotImplementedError. -*** -## ClassDef Propagator -**Propagator**: The function of Propagator is to propagate feedback from a child node to its parent nodes based on the provided rules and functions. - -**attributes**: The attributes of this Class. -- `override`: A dictionary that stores the override propagate functions for specific operator names. - -**Code Description**: The Propagator class is a subclass of the AbstractPropagator class. It provides specific implementations for the `propagate` and `_propagate` methods, as well as additional functionalities such as registering custom propagation functions and initializing feedback. - -- The `register` method allows users to register a custom propagate function for a specific operator name. It takes two parameters: `operator_name` (the name of the operator) and `propagate_function` (the custom propagate function). It adds the `operator_name` and `propagate_function` to the `override` dictionary. - -- The `propagate` method is responsible for computing the propagated feedback to the parent nodes of the given `child` node. It takes a `child` parameter of type `MessageNode` and returns a dictionary where the keys are the parent nodes and the values are the propagated feedback. It first retrieves the operator name from the `child` node using the `get_op_name` function. If the operator name is found in the `override` dictionary, it calls the corresponding propagate function with the `child` node as the argument. Otherwise, it calls the `_propagate` method to compute the propagated feedback. - -- The `init_feedback` method is an abstract method that must be implemented by subclasses. It takes a `feedback` parameter and returns the initialized feedback object that will be propagated recursively. Since this method is not implemented in the Propagator class, it raises a `NotImplementedError` if called. - -- The `_propagate` method is a protected method that computes the propagated feedback to the parent nodes based on the `child` node's description, data, and feedback. It takes a `child` parameter of type `MessageNode` and returns a dictionary where the keys are the parent nodes and the values are the propagated feedback. It first creates a list of tuples representing the parents of the `child` node. Then, it aggregates the feedback from the `child` node and creates a `TraceGraph` object. It also adds the external dependencies on parameters not visible in the current graph level. Finally, it returns a dictionary where the keys are the parent nodes and the values are the propagated feedback. - -**Note**: -- The `propagate` method must be implemented in any subclass of Propagator. -- The `init_feedback` and `_propagate` methods are abstract methods and must be implemented in subclasses. -- The `register` method allows users to register custom propagate functions for specific operator names, providing flexibility in the feedback propagation process. - -**Output Example**: -Given a properly implemented subclass of Propagator, the return value of the `propagate` method might look like the following: -```python -{ - parent_node_1: feedback_data_1, - parent_node_2: feedback_data_2, - # ... other parent nodes and their respective feedback -} -``` -This dictionary maps parent nodes to their respective propagated feedback. -### FunctionDef __init__(self) -**__init__**: The function of __init__ is to initialize an instance of the Propagator class. - -**parameters**: The parameters of this Function. -· This function does not take any parameters. - -**Code Description**: The __init__ function is a constructor method for the Propagator class. When an instance of the Propagator class is created, this method is automatically called to set up the initial state of the object. Specifically, it initializes an instance variable named `override` as an empty dictionary. This dictionary is intended to store override propagation functions, where the keys are operator names and the values are the corresponding override functions. This setup allows for flexible and dynamic modification of propagation behavior based on specific operators. - -**Note**: -- The `override` dictionary is initially empty and can be populated later with operator names and their corresponding override functions. -- This method does not require any arguments and does not return any values. -- Proper management of the `override` dictionary is essential for ensuring the correct propagation behavior in the Propagator class. -*** -### FunctionDef register(self, operator_name, propagate_function) -**register**: The function of register is to associate a given operator name with a specific propagation function. - -**parameters**: The parameters of this Function. -· operator_name: The name of the operator to be registered. -· propagate_function: The function that defines how the operator should propagate. - -**Code Description**: The register function is a method designed to add or override an entry in the `override` dictionary of the Propagator class. When called, it takes two arguments: `operator_name` and `propagate_function`. The `operator_name` is a string that identifies the operator, and `propagate_function` is a callable that defines the behavior of the operator during propagation. The method assigns the `propagate_function` to the `operator_name` key in the `override` dictionary, effectively registering or updating the propagation behavior for that operator. - -**Note**: -- Ensure that `operator_name` is unique within the context of the `override` dictionary to avoid unintentional overwrites. -- The `propagate_function` should be a valid callable that adheres to the expected signature and behavior required by the Propagator class. -*** -### FunctionDef propagate(self, child) -**propagate**: The function of propagate is to compute and return the propagated feedback to the parents of a given MessageNode based on the node's description, data, and feedback. - -**parameters**: -- child: A MessageNode object representing the child node for which the feedback needs to be propagated. - -**Code Description**: -The `propagate` function is a method of the `Propagator` class. It takes a child `MessageNode` as input and computes the propagated feedback to its parents. The function first checks if there is an override function defined for the operator associated with the child's description. If an override function is defined, it is called to compute the propagated feedback. Otherwise, the default `_propagate` function is called. - -The purpose of the `propagate` function is to compute the propagated feedback from a child `MessageNode` to its parents. The feedback is computed based on the child's description, data, and feedback. The function returns a dictionary where the keys are the parents of the child and the values are the propagated feedback. - -The `propagate` function provides a way to customize the propagation behavior for different types of operators. By defining an override function for a specific operator, developers can specify how the feedback should be propagated for that operator. This allows for flexibility and customization in the propagation process. - -It is important to note that the `propagate` function relies on the `_propagate` function, which is a placeholder and needs to be implemented in a subclass of the `Propagator` class. The implementation of the `_propagate` function will depend on the specific requirements of the operator being propagated. The `_propagate` function raises a `NotImplementedError` to indicate that it needs to be implemented. - -The `propagate` function is called by other parts of the project to propagate feedback from child nodes to parent nodes. It is an essential component of the graph propagation process and plays a crucial role in updating the values of parent nodes based on the feedback received from their child nodes. - -**Note**: -- The `_propagate` function is a placeholder and needs to be implemented in a subclass of the `Propagator` class. -- The `propagate` function provides a way to customize the propagation behavior for different types of operators. -- The implementation of the `_propagate` function will depend on the specific requirements of the operator being propagated. -- The `propagate` function is an essential component of the graph propagation process and plays a crucial role in updating the values of parent nodes based on the feedback received from their child nodes. - -**Output Example**: -If the `propagate` function is called with a child `MessageNode` object and the feedback is successfully propagated to its parents, the function will return a dictionary where the keys are the parent nodes and the values are the propagated feedback. -*** -### FunctionDef init_feedback(self, feedback) -**init_feedback**: The function of init_feedback is to create a feedback object from raw feedback that will be propagated recursively. - -**parameters**: The parameters of this Function. -· feedback: Raw feedback of any type that needs to be processed into a feedback object. - -**Code Description**: The init_feedback function is designed to take raw feedback as input and transform it into a feedback object that can be propagated recursively through a system. This function is essential for initializing the feedback mechanism in a propagation process. The function is currently not implemented and raises a NotImplementedError, indicating that it is intended to be overridden in a subclass or implemented later. - -In the context of its usage within the project, init_feedback is called by the backward method of the Node class in opto\trace\nodes.py. The backward method is responsible for performing a backward pass through a graph of nodes, propagating feedback from child nodes to parent nodes. During this process, init_feedback is used to initialize the feedback for the current node before it is propagated to its parents. This ensures that the feedback is in the correct format and ready for recursive propagation. - -**Note**: -- The init_feedback function must be implemented before it can be used effectively. -- It is crucial to ensure that the feedback object created by this function is compatible with the propagation mechanism used in the backward method. -- Proper implementation of this function is necessary to avoid runtime errors and ensure the correct functioning of the feedback propagation process. -*** -### FunctionDef _propagate(self, child) -**_propagate**: The function of _propagate is to compute and return the propagated feedback to the parents of a given MessageNode based on the node's description, data, and feedback. - -**parameters**: -- self: The instance of the Propagator class. -- child: The MessageNode for which the feedback needs to be propagated. - -**Code Description**: -The _propagate function is a method of the Propagator class. It takes a child MessageNode as input and computes the propagated feedback to its parents. The function first checks if there is an override function defined for the operator associated with the child's description. If an override function is defined, it is called to compute the propagated feedback. Otherwise, the default _propagate function is called. - -The _propagate function raises a NotImplementedError, indicating that it needs to be implemented in a subclass of the Propagator class. This allows for customization of the propagation behavior for different types of operators. - -The purpose of the _propagate function is to compute the propagated feedback from a child MessageNode to its parents. The feedback is computed based on the child's description, data, and feedback. The function returns a dictionary where the keys are the parents of the child and the values are the propagated feedback. - -It is important to note that the _propagate function is a placeholder and needs to be implemented in a subclass of the Propagator class. The implementation of this function will depend on the specific requirements of the operator being propagated. - -**Note**: -- The _propagate function is a placeholder and needs to be implemented in a subclass of the Propagator class. -- The function raises a NotImplementedError to indicate that it needs to be implemented. -- The implementation of the _propagate function will depend on the specific requirements of the operator being propagated. -*** -## ClassDef SumPropagator -**SumPropagator**: The function of SumPropagator is to propagate feedback from a child node to its parent nodes by summing the feedback values. - -**attributes**: The attributes of this Class. -- This class does not define any additional attributes beyond those inherited from the Propagator class. - -**Code Description**: The SumPropagator class is a subclass of the Propagator class. It provides specific implementations for the `init_feedback` and `_propagate` methods, which are abstract methods in the Propagator class. - -- The `init_feedback` method takes a `feedback` parameter of any type and returns it as-is. This method is used to initialize the feedback object that will be propagated recursively. - -- The `_propagate` method is responsible for computing the propagated feedback to the parent nodes of the given `child` node. It takes a `child` parameter of type `MessageNode` and returns a dictionary where the keys are the parent nodes and the values are the propagated feedback. - - - If the `child` node's feedback contains a "user" key, it asserts that the "user" feedback is the only feedback and that it contains exactly one item. It then extracts this feedback item. - - - If the "user" key is not present, it sums the feedback values from all keys in the `child` node's feedback. It asserts that the feedback list is not empty and that all feedback items are of the same type. If the feedback items are strings, it concatenates them; otherwise, it sums them numerically. - - - Finally, it returns a dictionary where each parent node of the `child` node is mapped to the computed feedback. - -The SumPropagator class is used within the context of the opto.trace.propagators module, which deals with propagating feedback in a hierarchical structure of nodes. It overrides the abstract methods of the Propagator class to provide a specific feedback propagation mechanism based on summing feedback values. - -**Note**: -- The `init_feedback` method in SumPropagator simply returns the input feedback without any modifications. -- The `_propagate` method ensures that feedback values are either concatenated (if they are strings) or summed (if they are numeric), and it performs type checks to ensure consistency. - -**Output Example**: -Given a `child` node with feedback and parent nodes, the return value of the `_propagate` method might look like the following: -```python -{ - parent_node_1: summed_feedback, - parent_node_2: summed_feedback, - # ... other parent nodes and their respective feedback -} -``` -This dictionary maps parent nodes to their respective propagated feedback, which is the sum of the feedback values from the `child` node. -### FunctionDef init_feedback(self, feedback) -**init_feedback**: The function of init_feedback is to initialize and return the provided feedback. - -**parameters**: The parameters of this Function. -· feedback: Any type of input that represents the feedback to be initialized. - -**Code Description**: The init_feedback function is designed to take a single parameter, feedback, which can be of any type. The function simply returns the feedback parameter as it is. This implies that the function's primary purpose is to serve as a placeholder or a pass-through for the feedback data, potentially for further processing or storage within the context of the SumPropagator class. - -**Note**: -- The function does not perform any validation or transformation on the feedback parameter. -- Ensure that the feedback parameter is provided in the expected format and type as required by the broader application context. - -**Output Example**: -If the feedback parameter is provided as a string "Positive feedback", the function will return "Positive feedback". -If the feedback parameter is provided as a dictionary {"score": 10, "comment": "Excellent"}, the function will return {"score": 10, "comment": "Excellent"}. -*** -### FunctionDef _propagate(self, child) -**_propagate**: The function of _propagate is to calculate the feedback value for a given child node and propagate it to its parent nodes. -**parameters**: -- self: The current object. -- child: The child node for which the feedback needs to be propagated. -**Code Description**: -The `_propagate` function is a method of the `SumPropagator` class in the `propagators.py` module. It takes two parameters, `self` and `child`, where `self` refers to the current `SumPropagator` object and `child` is the child node for which the feedback needs to be propagated. - -The function first checks if the child node has a feedback entry for the "user" key. If it does, it assumes that the user feedback is the only feedback and assigns it to the `feedback` variable. Otherwise, it sums up the feedback values from all the keys in the `child.feedback` dictionary and assigns it to the `feedback_list` variable. - -Next, the function performs some assertions to ensure the validity of the feedback data. It checks if the `feedback_list` has at least one element and if all the elements in the list have the same type. If the elements are of type string, it concatenates them using the `"".join()` method and assigns the result to the `feedback` variable. Otherwise, it calculates the sum of the elements using the `sum()` function and assigns it to the `feedback` variable. - -Finally, the function creates a dictionary comprehension to map each parent node of the child node to the calculated feedback value. The parent nodes are obtained by calling the `parents()` function of the child node. - -The `_propagate` function is an important part of the feedback propagation process in the graph structure. It ensures that the feedback from a child node is correctly calculated and propagated to its parent nodes. This is crucial for updating the parameters and optimizing the graph based on the feedback received. - -**Note**: The `_propagate` function assumes that the feedback data is stored in the `child.feedback` dictionary, where the keys represent different sources of feedback and the values represent the corresponding feedback values. The function handles two scenarios: when there is only user feedback available and when there are multiple feedback sources that need to be summed up. It is important to ensure that the feedback data is correctly formatted and consistent with the expectations of the function. - -**Output Example**: A possible appearance of the code's return value could be: -``` -{ - parent_node_1: feedback_value_1, - parent_node_2: feedback_value_2, - ... -} -``` -This example assumes that the `child.parents` attribute contains a list of parent nodes and the `feedback` variable contains the calculated feedback value for each parent node. The actual structure and content of the return value will depend on the specific implementation and use case within the project. -*** diff --git a/generated_docs/opto/trace/trace.md b/generated_docs/opto/trace/trace.md deleted file mode 100644 index f0e43488..00000000 --- a/generated_docs/opto/trace/trace.md +++ /dev/null @@ -1,43 +0,0 @@ -## ClassDef stop_tracing -**stop_tracing**: The function of stop_tracing is to disable tracing within a specific context. - -**attributes**: -- None - -**Code Description**: -The `stop_tracing` class is a context manager that is used to disable tracing within a specific context. When the `stop_tracing` object is entered, it sets the `GRAPH.TRACE` attribute to `False`, effectively disabling tracing. When the context is exited, the `GRAPH.TRACE` attribute is set back to `True`, enabling tracing again. - -This class is typically used in conjunction with the `trace` module to control the tracing behavior of a program. Tracing is a technique used to monitor the execution of a program by recording information about each executed statement. By disabling tracing within a specific context, developers can exclude certain parts of the code from being traced, which can be useful for performance optimization or debugging purposes. - -In the project, the `stop_tracing` class is called in the `test_bundle.py` file within the `run` function. It is used to disable tracing while executing certain code blocks. This allows developers to selectively trace or exclude specific parts of the code during testing. - -**Note**: -- The `stop_tracing` class is a context manager, so it should be used within a `with` statement to ensure proper entry and exit. -- Disabling tracing can be useful for performance optimization or debugging purposes, but it should be used with caution as it may affect the accuracy of the tracing results. -### FunctionDef __enter__(self) -**__enter__**: The function of __enter__ is to set the tracing state to False. - -**parameters**: The parameters of this Function. -· self: Refers to the instance of the class that contains this method. - -**Code Description**: The __enter__ method is a special method used in the context management protocol in Python. When an instance of the class containing this method is used in a `with` statement, the __enter__ method is automatically invoked at the beginning of the block. In this specific implementation, the __enter__ method sets the `TRACE` attribute of the `GRAPH` object to `False`. This action effectively stops or disables tracing within the context of the `with` statement. The `GRAPH` object is assumed to be a global or otherwise accessible object that controls tracing functionality. - -**Note**: -- Ensure that the `GRAPH` object and its `TRACE` attribute are properly defined and accessible within the scope where this method is used. -- This method is typically used in conjunction with the `__exit__` method to manage resources or states within a `with` statement. -*** -### FunctionDef __exit__(self, type, value, traceback) -**__exit__**: The function of __exit__ is to reset the tracing state by setting `GRAPH.TRACE` to `True`. - -**parameters**: The parameters of this Function. -· type: The exception type, if any exception was raised. -· value: The exception instance, if any exception was raised. -· traceback: The traceback object, if any exception was raised. - -**Code Description**: The `__exit__` method is a special method used in context management in Python. It is called when the execution of a block inside a `with` statement is finished. In this specific implementation, the `__exit__` method sets the `TRACE` attribute of the `GRAPH` object to `True`. This indicates that tracing should be enabled or resumed after the context block is exited, regardless of whether an exception was raised or not. The method takes three parameters: `type`, `value`, and `traceback`, which are standard for the `__exit__` method and provide information about any exception that may have occurred within the `with` block. - -**Note**: -- This method is part of the context management protocol and is automatically invoked at the end of a `with` statement. -- The parameters `type`, `value`, and `traceback` are necessary for handling exceptions, but in this implementation, they are not used. -- Ensure that `GRAPH` and its `TRACE` attribute are properly defined and accessible within the scope where this `__exit__` method is used. -*** diff --git a/generated_docs/opto/trace/utils.md b/generated_docs/opto/trace/utils.md deleted file mode 100644 index d5b4099a..00000000 --- a/generated_docs/opto/trace/utils.md +++ /dev/null @@ -1,320 +0,0 @@ -## FunctionDef contain(container_of_nodes, node) -**contain**: The function of contain is to check if a given node is present in a container of nodes. -**parameters**: -- container_of_nodes: A container (such as a list or set) that holds nodes. -- node: The node to be checked for presence in the container. -**Code Description**: -The `contain` function takes in a container of nodes and a node as parameters. It uses a list comprehension to iterate over the container and checks if the given node is identical (using the `is` operator) to any of the nodes in the container. The function returns `True` if the node is found in the container, and `False` otherwise. - -This function is used in various parts of the project. In the `opto\trace\bundle.py/FunModule/forward` function, the `contain` function is called to check if a node is present in the `container_of_nodes` list. It is used to determine the external dependencies of the operator function. - -In the `opto\trace\utils.py/MinHeap/__contains__` function, the `contain` function is used to check if an item is present in the `self.heap` list. - -The `contain` function is also used in the `tests\unit_tests\test_bundle.py/run` function to check if a node is present in a container of nodes. - -**Note**: The `contain` function checks for identity (using the `is` operator) instead of value equality. This means that it will only return `True` if the node is the exact same object in memory as one of the nodes in the container. - -**Output Example**: -```python -container_of_nodes = [node(1), node(2), node(3)] -node = node(2) -print(contain(container_of_nodes, node)) -# Output: True -``` -## FunctionDef parse_eqs_to_dict(text) -**parse_eqs_to_dict**: The function of parse_eqs_to_dict is to parse a given text containing equations into a dictionary. - -**parameters**: The parameters of this Function. -· text: A string containing equations separated by new lines. Each equation should be in the format `key=value`. - -**Code Description**: The parse_eqs_to_dict function processes a string of equations and converts it into a dictionary where each key-value pair represents an equation. The function follows these steps: - -1. **Splitting the Input Text**: The input text is split into individual lines using the newline character (`\n`) as the delimiter. -2. **Initialization**: An empty dictionary `result_dict` is initialized to store the parsed key-value pairs. A variable `last_key` is also initialized to keep track of the last processed key. -3. **Processing Each Line**: - - The function iterates over each line in the split text. - - If a line is empty, it is skipped. - - If a line contains an equals sign (`=`), it is split into a key and a value at the first occurrence of the equals sign. The key is stripped of any leading or trailing whitespace, and the value has any backticks (`) removed. The key-value pair is then added to the dictionary, and `last_key` is updated to the current key. - - If a line does not contain an equals sign but `last_key` is set, the line is considered a continuation of the previous value. The line is appended to the value of `last_key` in the dictionary, with any backticks removed. -4. **Returning the Result**: After processing all lines, the function returns the populated dictionary. - -**Note**: -- The function assumes that each equation is either on a single line or that subsequent lines without an equals sign are continuations of the previous value. -- Backticks (`) in the values are removed during processing. - -**Output Example**: -Given the input text: -``` -x0 = 1 -x1=2 -x2=`2` -x3= def fun():\n print('hello')\n -abc_test1=test -``` -The function would return: -``` -{ - 'x0': '1', - 'x1': '2', - 'x2': '2', - 'x3': "def fun():\nprint('hello')", - 'abc_test1': 'test' -} -``` -## ClassDef MinHeap -**MinHeap**: The function of MinHeap is to implement a minimum heap data structure, which supports efficient retrieval and removal of the smallest element. - -**attributes**: The attributes of this Class. -· heap: A list that stores the elements of the heap. - -**Code Description**: The MinHeap class provides a minimum heap implementation with various methods to manage the heap's elements. The class supports initialization with an optional array, element insertion, element removal, and peeking at the smallest element. It also includes internal methods to maintain the heap property. - -- `__init__(self, arr=None)`: Initializes the heap. If an array is provided, it converts the array into a heap using the `heapify` method. Otherwise, it initializes an empty heap. -- `__contains__(self, item)`: Checks if an item is in the heap using a helper function `contain`. -- `__len__(self)`: Returns the number of elements in the heap. -- `push(self, item)`: Adds a new item to the heap and ensures the heap property is maintained by calling the `_siftup` method. -- `pop(self)`: Removes and returns the smallest item from the heap. It maintains the heap property by calling the `_siftdown` method after removing the root. -- `peek(self)`: Returns the smallest item without removing it from the heap. -- `_siftup(self, idx)`: Ensures the heap property is maintained from a given index upwards to the root. -- `_siftdown(self, idx)`: Ensures the heap property is maintained from a given index downwards to the leaves. -- `heapify(self, arr)`: Converts an array into a heap by copying the array and calling `_siftdown` on each non-leaf node. - -The MinHeap class is utilized in the `backward` method of the `Node` class in `opto\trace\nodes.py`. In this context, MinHeap is used to manage a priority queue for nodes during a backward pass operation. The `backward` method initializes a MinHeap with the current node and uses it to efficiently process nodes in the correct order, ensuring that feedback is propagated correctly through the graph. - -**Note**: -- The elements stored in the heap must support comparison operations (`lt` and `gt` methods). -- The `contain` function used in `__contains__` is assumed to be defined elsewhere in the codebase. - -**Output Example**: -- `push(item)`: Adds `item` to the heap. -- `pop()`: Returns the smallest element, e.g., `3`. -- `peek()`: Returns the smallest element without removing it, e.g., `3`. -- `__len__()`: Returns the number of elements in the heap, e.g., `5`. -- `__contains__(item)`: Returns `True` if `item` is in the heap, otherwise `False`. -### FunctionDef __init__(self, arr) -**__init__**: The function of __init__ is to initialize a MinHeap object, optionally transforming an input array into a valid min-heap. - -**parameters**: The parameters of this Function. -· arr: An optional array to be transformed into a min-heap. If not provided, an empty heap is initialized. - -**Code Description**: The __init__ method is the constructor for the MinHeap class. It initializes the heap based on the provided input array. If no array is provided (`arr` is `None`), it initializes an empty list to represent the heap. If an array is provided, it assigns this array to the heap and then calls the `heapify` method to transform the array into a valid min-heap. - -The `heapify` method is responsible for ensuring that the array satisfies the heap property, where each parent node is less than or equal to its child nodes. This transformation is crucial for the correct functioning of the heap operations. - -**Note**: Points to note about the use of the code -- If an array is provided during initialization, it will be automatically transformed into a min-heap. -- The `heapify` method modifies the heap in place and ensures the heap property is maintained. -- Proper initialization of the heap is essential for the efficiency and correctness of subsequent heap operations such as insertion and deletion. -*** -### FunctionDef __contains__(self, item) -**__contains__**: The function of `__contains__` is to check if a given item is present in the heap of the `MinHeap` class. - -**parameters**: The parameters of this function. -· item: The item to be checked for presence in the heap. - -**Code Description**: The `__contains__` function is a special method in Python that allows the use of the `in` keyword to check for the presence of an item in an instance of the `MinHeap` class. This function takes a single parameter, `item`, which represents the item to be checked. - -Internally, the function calls the `contain` function, passing `self.heap` and `item` as arguments. The `contain` function iterates over the `self.heap` list and checks if the `item` is identical to any of the elements in the list using the `is` operator. If the `item` is found, the `contain` function returns `True`; otherwise, it returns `False`. - -This method provides a convenient way to check for the presence of an item in the heap, leveraging the identity check mechanism provided by the `contain` function. - -**Note**: The `contain` function checks for identity (using the `is` operator) instead of value equality. This means that `__contains__` will only return `True` if the `item` is the exact same object in memory as one of the elements in `self.heap`. - -**Output Example**: -```python -min_heap = MinHeap() -min_heap.heap = [node(1), node(2), node(3)] -item = node(2) -print(item in min_heap) -# Output: True -``` -*** -### FunctionDef __len__(self) -**__len__**: The function of __len__ is to return the number of elements in the MinHeap. - -**parameters**: The parameters of this Function. -· self: Refers to the instance of the MinHeap class. - -**Code Description**: The __len__ method is a special method in Python that is used to define the behavior of the len() function for instances of a class. In this case, the __len__ method is implemented for the MinHeap class. When len() is called on an instance of MinHeap, this method returns the number of elements currently stored in the heap. It achieves this by returning the length of the internal list self.heap, which is used to store the heap elements. - -**Note**: -- This method does not take any parameters other than self. -- It is important to ensure that self.heap is always a list, as the len() function is called on it. - -**Output Example**: -If the MinHeap instance contains 5 elements, calling len(min_heap_instance) will return 5. -*** -### FunctionDef push(self, item) -**push**: The function of push is to add a new item to the MinHeap and maintain the heap property. - -**parameters**: The parameters of this Function. -· item: The item to be added to the heap. - -**Code Description**: The push function is a method of the MinHeap class that adds a new item to the heap and ensures that the heap property is maintained. When an item is pushed onto the heap, it is first appended to the end of the heap list. This operation increases the size of the heap by one. - -After appending the new item, the push function calls the _siftup method with the index of the newly added item, which is the last index of the heap list. The _siftup method is responsible for moving the new item up the heap until the heap property is restored. The heap property in a MinHeap requires that each parent node is less than or equal to its child nodes. The _siftup method ensures that this property is maintained by comparing the new item with its parent and swapping them if necessary. This process continues iteratively until the new item is in a position where the heap property is satisfied or it becomes the root of the heap. - -The push function is used in the backward method of the Node class in the context of a priority queue. In the backward method, nodes are processed in a specific order, and the MinHeap is used to manage this order efficiently. When a parent node needs to be added to the queue, the push function is called to insert the parent node into the MinHeap, ensuring that the heap property is maintained and the nodes are processed in the correct order. - -**Note**: -- The push function relies on the _siftup method to maintain the heap property. -- The heap property ensures that the smallest element is always at the root of the MinHeap. -- The elements in the heap must implement the gt method correctly for the _siftup method to function properly. -- The push function is integral to the operation of the MinHeap in managing the order of nodes in the backward method of the Node class. -*** -### FunctionDef pop(self) -**pop**: The function of pop is to remove and return the root element from the heap. - -**parameters**: -- self: The instance of the MinHeap class. - -**Code Description**: -The pop function is a method of the MinHeap class that is used to remove and return the root element from the heap. The function first checks if the length of the heap is equal to 1, which indicates that there is only one element in the heap. In this case, the function simply calls the pop method of the heap list and returns the popped element. - -If there are more than one element in the heap, the function proceeds to assign the value of the root element (the first element in the heap) to the variable "root". Then, it replaces the root element with the last element in the heap by assigning the popped element from the heap list to the index 0 of the heap list. This step is necessary to maintain the heap property after removing the root element. - -After replacing the root element, the function calls the _siftdown method to sift down the new root element to its correct position in the heap. This ensures that the heap property is maintained and the new root element is correctly positioned relative to its children. - -Finally, the function returns the original root element that was stored in the "root" variable. - -The pop function is called by other methods in the MinHeap class, such as the heapify method. It relies on the _siftdown method to maintain the heap property after removing the root element. - -**Note**: -- The pop function assumes that the heap is represented as a list. -- The function modifies the heap in place and does not return any value. -- Proper use of the pop function is crucial for maintaining the correctness of heap operations and ensuring that the heap property is maintained. - -**Output Example**: -If the heap is [5, 7, 9, 11, 13] and the pop function is called, the function will remove and return the root element, which is 5. After the pop operation, the heap will be [7, 9, 11, 13]. -*** -### FunctionDef peek(self) -**peek**: The function of peek is to return the smallest element in the MinHeap without removing it. - -**parameters**: The parameters of this Function. -· None - -**Code Description**: The peek function is a method of the MinHeap class. It checks if the heap list is non-empty. If the heap contains elements, it returns the first element of the heap list, which is the smallest element due to the properties of a MinHeap. If the heap is empty, it returns None. This function allows users to inspect the smallest element in the heap without modifying the heap structure. - -**Note**: -- The peek function does not alter the state of the heap. -- It is a read-only operation and is useful for checking the minimum element efficiently. - -**Output Example**: -- If the heap is [1, 3, 5, 7], peek() will return 1. -- If the heap is empty, peek() will return None. -*** -### FunctionDef _siftup(self, idx) -**_siftup**: The function of _siftup is to maintain the heap property by moving an element up the heap until the heap property is restored. - -**parameters**: The parameters of this Function. -· idx: The index of the element to be moved up in the heap. - -**Code Description**: The _siftup function is a helper method used to maintain the heap property in a MinHeap data structure. When an element is added to the heap, it may violate the heap property, which requires that each parent node is less than or equal to its child nodes. The _siftup function corrects this by comparing the element at the given index (idx) with its parent. If the element is smaller than its parent, they are swapped. This process continues iteratively until the element is in a position where the heap property is satisfied, or it becomes the root of the heap. - -The function is called by the push method of the MinHeap class. When a new item is added to the heap using the push method, the item is appended to the end of the heap list. The _siftup function is then called with the index of this new item (which is the last index of the list). This ensures that the new item is moved to its correct position in the heap, maintaining the heap property. - -**Note**: -- The _siftup function assumes that the heap property is only violated between the element at the given index and its parent. It does not check or correct violations further up the tree. -- This function is designed to work with a MinHeap, where the smallest element should always be at the root. -- The function relies on the gt method of the elements in the heap to compare their values. Ensure that the elements in the heap implement this method correctly. -*** -### FunctionDef _siftdown(self, idx) -**_siftdown**: The function of _siftdown is to maintain the heap property by sifting down an element at a given index in the heap. - -**parameters**: The parameters of this Function. -· idx: The index of the element to be sifted down in the heap. - -**Code Description**: The _siftdown function is a helper method used to ensure that the heap property is maintained after an element has been moved to a new position in the heap. This function is particularly useful in operations where the heap structure might be violated, such as after removing the root element or during the initial heap construction. - -The function operates as follows: -1. It calculates the index of the last element in the heap. -2. It enters a loop where it calculates the indices of the left and right children of the current element. -3. It initializes the smallest index as the current index. -4. It compares the current element with its left and right children to find the smallest element among them. -5. If one of the children is smaller than the current element, it swaps the current element with the smallest child and updates the current index to the index of the smallest child. -6. The loop continues until the current element is smaller than both of its children or it has no children. - -The function is called by the pop and heapify methods of the MinHeap class: -- In the pop method, _siftdown is used after the root element is removed and the last element is moved to the root position. This ensures that the new root element is correctly positioned to maintain the heap property. -- In the heapify method, _siftdown is called for each non-leaf element in the array to transform the array into a valid heap. - -**Note**: Points to note about the use of the code -- The function assumes that the heap is represented as a list and that each element in the heap has a method lt for comparison. -- The function modifies the heap in place and does not return any value. -- Proper use of _siftdown is crucial for maintaining the efficiency and correctness of heap operations such as insertion, deletion, and heap construction. -*** -### FunctionDef heapify(self, arr) -**heapify**: The function of heapify is to transform an arbitrary array into a valid min-heap. - -**parameters**: The parameters of this Function. -· arr: The array to be transformed into a min-heap. - -**Code Description**: The heapify function is designed to convert a given array into a min-heap, ensuring that the heap property is maintained throughout the array. This function is a method of the MinHeap class and operates as follows: - -1. The function begins by importing the copy module and creating a shallow copy of the input array `arr` to avoid modifying the original array. This copy is stored in the instance variable `self.heap`. -2. It then iterates over the indices of the non-leaf elements of the array in reverse order, starting from the last non-leaf node and moving towards the root. The range for this iteration is calculated as `(len(self.heap) - 2) // 2` to `-1`. -3. For each index `i` in this range, the function calls the helper method `_siftdown(i)`. The _siftdown method is responsible for maintaining the heap property by sifting down the element at index `i` to its correct position in the heap. - -The heapify function is called during the initialization of the MinHeap object if an array is provided. This ensures that any array passed to the MinHeap constructor is automatically transformed into a valid min-heap. - -**Note**: Points to note about the use of the code -- The heapify function assumes that the elements of the array have a method `lt` for comparison, which is used by the _siftdown method. -- The function modifies the heap in place and does not return any value. -- Proper use of the heapify function is crucial for initializing the heap correctly, which in turn ensures the efficiency and correctness of subsequent heap operations such as insertion and deletion. -*** -## FunctionDef for_all_methods(decorator) -**for_all_methods**: The function of for_all_methods is to apply a decorator to all methods of a class. - -**parameters**: -- decorator: The decorator function that will be applied to all methods of the class. - -**Code Description**: -The `for_all_methods` function is a higher-order function that takes a decorator as input and returns a new decorator. The returned decorator can be used to decorate a class, applying the input decorator to all methods of the class. - -The `for_all_methods` function first defines an inner function called `decorate`. This function takes a class as input and iterates over all the attributes of the class using the `__dict__` attribute. For each attribute that is callable (i.e., a method) and does not start with "__" (i.e., not a special method), the function applies the input decorator to the method using the `setattr` function. This effectively replaces the original method with the decorated version. - -Finally, the `decorate` function returns the modified class with the decorated methods. - -The `for_all_methods` function itself returns the `decorate` function, allowing it to be used as a decorator for classes. - -**Note**: -- The input decorator should be a function that takes a method as input and returns a new method. -- The input decorator will be applied to all methods of the class, including inherited methods. -- The input decorator will replace the original methods with the decorated versions. - -**Output Example**: -```python -@for_all_methods -def my_decorator(method): - def wrapper(*args, **kwargs): - # Do something before calling the method - result = method(*args, **kwargs) - # Do something after calling the method - return result - return wrapper - -@my_decorator -class MyClass: - def method1(self): - # Method implementation - - def method2(self): - # Method implementation -``` - -In the above example, the `my_decorator` function is applied to all methods of the `MyClass` class using the `for_all_methods` decorator. The `my_decorator` function wraps each method, allowing additional functionality to be added before and after the method is called. -### FunctionDef decorate(cls) -**decorate**: The function of decorate is to apply a decorator to all callable methods of a class, excluding special methods. - -**parameters**: The parameters of this Function. -· cls: The class whose methods will be decorated. - -**Code Description**: The decorate function iterates over all attributes of the provided class (cls). For each attribute, it checks if the attribute is callable (i.e., a method) and if its name does not start with double underscores (which would indicate a special method). If both conditions are met, the function applies a decorator to the method using the setattr function, which updates the class with the decorated method. Finally, the function returns the modified class. - -**Note**: -- This function assumes that a decorator function named decorator is already defined and available in the scope where decorate is used. -- Special methods (those starting with double underscores) are not decorated by this function. - -**Output Example**: -If you have a class MyClass with methods method1 and method2, after applying the decorate function, both method1 and method2 will be decorated with the decorator function. The class will be returned with these modifications. -*** From 5af3e455a818f2485f88217f5dc14dd07c6b7620 Mon Sep 17 00:00:00 2001 From: chinganc Date: Wed, 27 Aug 2025 23:33:37 +0000 Subject: [PATCH 167/314] Make train support single-node optimization --- examples/{train_example.py => train_model.py} | 0 examples/train_single_node.py | 20 ++++++++++++++++++ opto/trainer/train.py | 21 +++++++++++++++---- 3 files changed, 37 insertions(+), 4 deletions(-) rename examples/{train_example.py => train_model.py} (100%) create mode 100644 examples/train_single_node.py diff --git a/examples/train_example.py b/examples/train_model.py similarity index 100% rename from examples/train_example.py rename to examples/train_model.py diff --git a/examples/train_single_node.py b/examples/train_single_node.py new file mode 100644 index 00000000..13a903fc --- /dev/null +++ b/examples/train_single_node.py @@ -0,0 +1,20 @@ +from opto import trace, trainer + +def main(): + true_number = 3 + train_dataset = dict(inputs=[None], infos=[f'Correct answer is: {true_number}']) + param = trace.node(0, description='An interger to guess', trainable=True) + + trainer.train( + model=param, + # optimizer='OptoPrimeV2', # by default, OPROv2 is used for single-node optimization + train_dataset=train_dataset, + # trainer kwargs + num_epochs=3, + batch_size=1, + verbose='output', + ) + + +if __name__ == "__main__": + main() diff --git a/opto/trainer/train.py b/opto/trainer/train.py index e46d8c4d..e3b79e05 100644 --- a/opto/trainer/train.py +++ b/opto/trainer/train.py @@ -6,6 +6,7 @@ from opto.trainer.guide import Guide from opto.trainer.loggers import BaseLogger from opto.optimizers.optimizer import Optimizer +from opto.trace.nodes import ParameterNode def dataset_check(dataset): @@ -16,11 +17,11 @@ def dataset_check(dataset): def train( *, - model: trace.Module, + model: Union[trace.Module, ParameterNode], train_dataset: dict, # class of optimizer algorithm: Union[Trainer, str] = 'MinibatchAlgorithm', - optimizer: Union[Optimizer, str] = "OptoPrimeV2", + optimizer: Union[Optimizer, str] = None, guide: Union[Guide, str] = 'LLMJudge', logger: Union[BaseLogger, str] = 'ConsoleLogger', # extra configs @@ -42,7 +43,19 @@ def train( # TODO check eligible optimizer, trainer dataset_check(train_dataset) - # TODO remove duplicate codes + if optimizer is None: + optimizer = "OPROv2" if isinstance(model, ParameterNode) else "OptoPrimeV2" + + # Convert ParameterNode to Module + if isinstance(model, ParameterNode): + assert model.trainable, "The parameter must be trainable." + @trace.model + class SingleNodeModel: + def __init__(self, param): + self.param = param # ParameterNode + def forward(self, x): + return self.param + model = SingleNodeModel(model) # Check model parameters is non-empty parameters = model.parameters() @@ -61,7 +74,7 @@ def train( algo = trainer_class( model, optimizer, - logger + logger=logger ) return algo.train( From f9e468df6fbaa531c23c4eaf6f8363451b9fef23 Mon Sep 17 00:00:00 2001 From: windweller Date: Thu, 28 Aug 2025 13:39:55 -0400 Subject: [PATCH 168/314] initial commit --- opto/flows/__init__.py | 0 opto/flows/ops.py | 42 ++++++++++++++++++++++++++++++++++++++++++ opto/flows/types.py | 0 3 files changed, 42 insertions(+) create mode 100644 opto/flows/__init__.py create mode 100644 opto/flows/ops.py create mode 100644 opto/flows/types.py diff --git a/opto/flows/__init__.py b/opto/flows/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/opto/flows/ops.py b/opto/flows/ops.py new file mode 100644 index 00000000..db4a5cbb --- /dev/null +++ b/opto/flows/ops.py @@ -0,0 +1,42 @@ +import pydantic +import opto.trace as trace + +""" +TracedLLM: +1. special operations that supports specifying inputs (system_prompt, user_prompt) to LLM and parsing of outputs, wrap + everything under one command. +2. Easy to use interface -- can be inherited by users. + +Usage patterns: + +1. Direct use: (only supports single input, single output) (signature: str -> str) +llm = TracedLLM("You are a helpful assistant.") +llm("Hello, what's the weather in France today?") + +2. Inheritance: +class Scorer(TracedLLM): + "This is a class that scores the response from LLM" + doc: opto.flows.types.TracedInput + score: opto.flows.types.TracedOutput + +scorer = Scorer("You are a helpful assistant that scores the response from LLM") +scorer(doc="The response is ...") +""" + +@trace.model +class TracedLLM: + def __init__(self, system_prompt: str): + """Initialize TracedLLM with a system prompt. + + Args: + system_prompt: The system prompt to use for LLM calls + """ + self.system_prompt = trace.node(system_prompt, trainable=True) + + def forward(self, user_prompt: str) -> str: + """Call the LLM with user prompt, using the configured system prompt.""" + return trace.operators.call_llm(self.system_prompt, user_prompt) + + +if __name__ == '__main__': + pass diff --git a/opto/flows/types.py b/opto/flows/types.py new file mode 100644 index 00000000..e69de29b From c332f8f56532e9e8f45d44ad7d9fed44da0a1a51 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 28 Aug 2025 17:52:45 +0000 Subject: [PATCH 169/314] add trace_llm class --- opto/trace/__init__.py | 21 ++++++++++++++ opto/trace/operators.py | 44 ++++++++++++++++++++++-------- tests/unit_tests/test_trace_llm.py | 8 ++++++ 3 files changed, 62 insertions(+), 11 deletions(-) create mode 100644 tests/unit_tests/test_trace_llm.py diff --git a/opto/trace/__init__.py b/opto/trace/__init__.py index ddf01300..cb937717 100644 --- a/opto/trace/__init__.py +++ b/opto/trace/__init__.py @@ -1,3 +1,4 @@ +from typing import Union from opto.trace.bundle import bundle, ExecutionError from opto.trace.modules import Module, model from opto.trace.containers import NodeContainer @@ -8,6 +9,7 @@ from opto.trace.nodes import Node, GRAPH from opto.trace.nodes import node +from opto.utils.llm import AbstractModel class stop_tracing: @@ -20,6 +22,25 @@ def __exit__(self, type, value, traceback): GRAPH.TRACE = True +# TODO defined it somewhere else? +@model +class trace_llm: + """ This is callable class of accessing LLM as a trace operator. """ + + def __init__(self, + system_prompt: Union[str, None, Node] = None, + llm: AbstractModel = None,): + self.system_prompt = node(system_prompt) + if llm is None: + from opto.utils.llm import LLM + llm = LLM() + assert isinstance(llm, AbstractModel), f"{llm} must be an instance of AbstractModel" + self.llm = llm + + def forward(self, user_prompt): + return operators.call_llm(self.llm, self.system_prompt, user_prompt) + + __all__ = [ "node", "stop_tracing", diff --git a/opto/trace/operators.py b/opto/trace/operators.py index 2bab4980..e01e2832 100644 --- a/opto/trace/operators.py +++ b/opto/trace/operators.py @@ -1,10 +1,11 @@ from __future__ import annotations import trace -from typing import TYPE_CHECKING, Any, Dict +from typing import TYPE_CHECKING, Any, Dict, Union if TYPE_CHECKING: # to prevent circular import from opto.trace.nodes import Node from opto.trace.bundle import bundle +from opto.utils.llm import AbstractModel import copy @@ -588,16 +589,37 @@ def set_update(x: Any, y: Any): return x +# @bundle(catch_execution_error=False) +# def call_llm(system_prompt, *user_prompts, **kwargs): +# """Query the language model of system_prompt with user_prompts.""" +# if system_prompt is not None: +# messages = [{"role": "system", "content": system_prompt}] +# else: +# messages = [{"role": "system", "content": "You are a helpful assistant.\n"}] +# for user_prompt in user_prompts: +# messages.append({"role": "user", "content": user_prompt}) +# from opto.utils.llm import LLM +# llm = LLM() +# response = llm(messages=messages, **kwargs) +# return response.choices[0].message.content + + @bundle(catch_execution_error=False) -def call_llm(system_prompt, *user_prompts, **kwargs): - """Query the language model of system_prompt with user_prompts.""" +def call_llm(llm, system_prompt: str, user_prompt: str) -> str: + """Call the LLM model. + + Args: + llm: The language model to use for generating responses. + system_prompt: the system prompt to the agent. By tuning this prompt, we can control the behavior of the agent. For example, it can be used to provide instructions to the agent (such as how to reason about the problem, how to use tools, how to answer the question), or provide in-context examples of how to solve the problem. + user_prompt: the input to the agent. It can be a query, a task, a code, etc. + Returns: + The response from the agent. + """ + messages = [] if system_prompt is not None: - messages = [{"role": "system", "content": system_prompt}] - else: - messages = [{"role": "system", "content": "You are a helpful assistant.\n"}] - for user_prompt in user_prompts: - messages.append({"role": "user", "content": user_prompt}) - from opto.utils.llm import LLM - llm = LLM() - response = llm(messages=messages, **kwargs) + messages.append({"role": "system", "content": system_prompt}) + messages.append({"role": "user", "content": user_prompt}) + # TODO support multi-turn conversation + # TODO auto-parsing results + response = llm(messages=messages) return response.choices[0].message.content diff --git a/tests/unit_tests/test_trace_llm.py b/tests/unit_tests/test_trace_llm.py new file mode 100644 index 00000000..c59ec35b --- /dev/null +++ b/tests/unit_tests/test_trace_llm.py @@ -0,0 +1,8 @@ +from opto import trace + +def test_trace_llm(): + + si = trace.node("You're a helpful assistant.", trainable=True) + user_prompt = "Hi there" + traced_llm = trace.trace_llm(si) # this is trace.Module + response = traced_llm(user_prompt) \ No newline at end of file From 4095568169da19c4050241b0d1d73a10bf909e8d Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 28 Aug 2025 18:03:03 +0000 Subject: [PATCH 170/314] Merge changes --- opto/flows/__init__.py | 1 + opto/flows/ops.py | 19 ++++++++++++++----- opto/trace/__init__.py | 21 --------------------- opto/trace/operators.py | 23 ++++------------------- tests/unit_tests/test_flows.py | 8 ++++++++ tests/unit_tests/test_trace_llm.py | 8 -------- 6 files changed, 27 insertions(+), 53 deletions(-) create mode 100644 tests/unit_tests/test_flows.py delete mode 100644 tests/unit_tests/test_trace_llm.py diff --git a/opto/flows/__init__.py b/opto/flows/__init__.py index e69de29b..12bd6002 100644 --- a/opto/flows/__init__.py +++ b/opto/flows/__init__.py @@ -0,0 +1 @@ +from opto.flows.ops import TracedLLM \ No newline at end of file diff --git a/opto/flows/ops.py b/opto/flows/ops.py index db4a5cbb..0c988b9f 100644 --- a/opto/flows/ops.py +++ b/opto/flows/ops.py @@ -1,5 +1,7 @@ import pydantic import opto.trace as trace +from typing import Union +from opto.utils.llm import AbstractModel, LLM """ TracedLLM: @@ -25,17 +27,24 @@ class Scorer(TracedLLM): @trace.model class TracedLLM: - def __init__(self, system_prompt: str): + def __init__(self, + system_prompt: Union[str, None, trace.Node] = None, + llm: AbstractModel = None): """Initialize TracedLLM with a system prompt. - + Args: system_prompt: The system prompt to use for LLM calls + llm: The LLM model to use for inference """ - self.system_prompt = trace.node(system_prompt, trainable=True) - + self.system_prompt = trace.node(system_prompt) + if llm is None: + llm = LLM() + assert isinstance(llm, AbstractModel), f"{llm} must be an instance of AbstractModel" + self.llm = llm + def forward(self, user_prompt: str) -> str: """Call the LLM with user prompt, using the configured system prompt.""" - return trace.operators.call_llm(self.system_prompt, user_prompt) + return trace.operators.call_llm(self.llm, self.system_prompt, user_prompt) if __name__ == '__main__': diff --git a/opto/trace/__init__.py b/opto/trace/__init__.py index cb937717..ddf01300 100644 --- a/opto/trace/__init__.py +++ b/opto/trace/__init__.py @@ -1,4 +1,3 @@ -from typing import Union from opto.trace.bundle import bundle, ExecutionError from opto.trace.modules import Module, model from opto.trace.containers import NodeContainer @@ -9,7 +8,6 @@ from opto.trace.nodes import Node, GRAPH from opto.trace.nodes import node -from opto.utils.llm import AbstractModel class stop_tracing: @@ -22,25 +20,6 @@ def __exit__(self, type, value, traceback): GRAPH.TRACE = True -# TODO defined it somewhere else? -@model -class trace_llm: - """ This is callable class of accessing LLM as a trace operator. """ - - def __init__(self, - system_prompt: Union[str, None, Node] = None, - llm: AbstractModel = None,): - self.system_prompt = node(system_prompt) - if llm is None: - from opto.utils.llm import LLM - llm = LLM() - assert isinstance(llm, AbstractModel), f"{llm} must be an instance of AbstractModel" - self.llm = llm - - def forward(self, user_prompt): - return operators.call_llm(self.llm, self.system_prompt, user_prompt) - - __all__ = [ "node", "stop_tracing", diff --git a/opto/trace/operators.py b/opto/trace/operators.py index e01e2832..6b17ea6f 100644 --- a/opto/trace/operators.py +++ b/opto/trace/operators.py @@ -589,23 +589,8 @@ def set_update(x: Any, y: Any): return x -# @bundle(catch_execution_error=False) -# def call_llm(system_prompt, *user_prompts, **kwargs): -# """Query the language model of system_prompt with user_prompts.""" -# if system_prompt is not None: -# messages = [{"role": "system", "content": system_prompt}] -# else: -# messages = [{"role": "system", "content": "You are a helpful assistant.\n"}] -# for user_prompt in user_prompts: -# messages.append({"role": "user", "content": user_prompt}) -# from opto.utils.llm import LLM -# llm = LLM() -# response = llm(messages=messages, **kwargs) -# return response.choices[0].message.content - - @bundle(catch_execution_error=False) -def call_llm(llm, system_prompt: str, user_prompt: str) -> str: +def call_llm(llm, system_prompt: str, *user_prompts: List[str], **kwargs) -> str: """Call the LLM model. Args: @@ -618,8 +603,8 @@ def call_llm(llm, system_prompt: str, user_prompt: str) -> str: messages = [] if system_prompt is not None: messages.append({"role": "system", "content": system_prompt}) - messages.append({"role": "user", "content": user_prompt}) - # TODO support multi-turn conversation + for user_prompt in user_prompts: + messages.append({"role": "user", "content": user_prompt}) # TODO auto-parsing results - response = llm(messages=messages) + response = llm(messages=messages, **kwargs) return response.choices[0].message.content diff --git a/tests/unit_tests/test_flows.py b/tests/unit_tests/test_flows.py new file mode 100644 index 00000000..536c8674 --- /dev/null +++ b/tests/unit_tests/test_flows.py @@ -0,0 +1,8 @@ +from opto import trace, flows + +def test_trace_llm(): + + si = trace.node("You're a helpful assistant.", trainable=True) + user_prompt = "Hi there" + traced_llm = flows.TracedLLM(si) # this is trace.Module + response = traced_llm(user_prompt) diff --git a/tests/unit_tests/test_trace_llm.py b/tests/unit_tests/test_trace_llm.py deleted file mode 100644 index c59ec35b..00000000 --- a/tests/unit_tests/test_trace_llm.py +++ /dev/null @@ -1,8 +0,0 @@ -from opto import trace - -def test_trace_llm(): - - si = trace.node("You're a helpful assistant.", trainable=True) - user_prompt = "Hi there" - traced_llm = trace.trace_llm(si) # this is trace.Module - response = traced_llm(user_prompt) \ No newline at end of file From c2f5591f5f24885c4575076129ce35fd5919ee4b Mon Sep 17 00:00:00 2001 From: windweller Date: Thu, 28 Aug 2025 19:16:30 -0400 Subject: [PATCH 171/314] add initial TracedLLM implementation --- opto/flows/__init__.py | 2 +- opto/flows/compose.py | 230 +++++++++++++++++++++++++++++++++ opto/flows/ops.py | 51 -------- opto/flows/types.py | 133 +++++++++++++++++++ opto/trace/operators.py | 2 +- tests/unit_tests/test_flows.py | 8 -- 6 files changed, 365 insertions(+), 61 deletions(-) create mode 100644 opto/flows/compose.py delete mode 100644 opto/flows/ops.py delete mode 100644 tests/unit_tests/test_flows.py diff --git a/opto/flows/__init__.py b/opto/flows/__init__.py index 12bd6002..d865a711 100644 --- a/opto/flows/__init__.py +++ b/opto/flows/__init__.py @@ -1 +1 @@ -from opto.flows.ops import TracedLLM \ No newline at end of file +from opto.flows.compose import TracedLLM \ No newline at end of file diff --git a/opto/flows/compose.py b/opto/flows/compose.py new file mode 100644 index 00000000..e3777673 --- /dev/null +++ b/opto/flows/compose.py @@ -0,0 +1,230 @@ +import pydantic +from pydantic import BaseModel, ValidationError, Field, create_model +import opto.trace as trace +from typing import Union, get_type_hints, Any, Dict +from opto.utils.llm import AbstractModel, LLM +from opto.flows.types import TracedInput, TracedOutput, DynamicModelMixin +import inspect +import json +import re + + +""" +TracedLLM: +1. special operations that supports specifying inputs (system_prompt, user_prompt) to LLM and parsing of outputs, wrap + everything under one command. +2. Easy to use interface -- can be inherited by users. + +Usage patterns: + +1. Direct use: (only supports single input, single output) (signature: str -> str) +llm = TracedLLM("You are a helpful assistant.") +response = llm("Hello, what's the weather in France today?") + +2. Inheritance: +class Scorer(TracedLLM): + "This is a class that scores the response from LLM" + doc: str = TracedInput(description="The document to score") + score: int = TracedOutput(description="The score of the document") + +scorer = Scorer() # if a system prompt is passed in here, it will override the docstring. +response = scorer(doc="The response is ...") +print(response.score) +""" + + +class TracedResponse: + """Dynamic response object that holds output field values.""" + def __init__(self, **kwargs): + for key, value in kwargs.items(): + setattr(self, key, value) + +class StructuredInputOutputMixin: + """Mixin providing structured input/output parsing capabilities for TracedLLM.""" + + def _detect_fields(self): + """Detect TracedInput and TracedOutput fields from class annotations and defaults.""" + # Get type hints to extract the actual types + type_hints = get_type_hints(self.__class__) + + # Look at class attributes and their default values + for attr_name in dir(self.__class__): + if not attr_name.startswith('_'): # Skip private attributes + attr_value = getattr(self.__class__, attr_name, None) + if isinstance(attr_value, TracedInput): + self._input_fields.append(attr_name) + # Store the type annotation for this field + self._field_types[attr_name] = type_hints.get(attr_name, str) + elif isinstance(attr_value, TracedOutput): + self._output_fields.append(attr_name) + # Store the type annotation for this field + self._field_types[attr_name] = type_hints.get(attr_name, str) + + def _create_dynamic_response_model(self) -> type[BaseModel]: + """ + Create a dynamic Pydantic model for parsing LLM responses. We avoid creating an explicit signature by creating a dynamic model instead. + The only disadvantage is nested-type parsing can be slightly more difficult, but that level of flexibility + nested LLM usage is rare and not a primary + use case for Trace. + """ + # Create field definitions for create_model + field_definitions = {} + + for field_name in self._output_fields: + field_type = self._field_types.get(field_name, str) + # Get the description from the TracedOutput instance + traced_output = getattr(self.__class__, field_name, None) + description = getattr(traced_output, 'description', None) if traced_output else None + + # Create field definition tuple: (type, Field(...)) + field_definitions[field_name] = (field_type, Field(description=description)) + + # Use Pydantic's create_model for dynamic model creation + ResponseModel = create_model( + f"{self.__class__.__name__}Response", + **field_definitions + ) + + return ResponseModel + + # TODO: rewrite this part + # TODO: 1. append at the end of the system prompt about generation instructions. XML based format with Markdown. + # TODO: 2. extract by XML, put into a JSON string (allow nested XML parsing, such that the fields/response model can actually be nested) + # TODO: 3. use the dynamic ResponseModel to do the parsing + def _extract_structured_data(self, llm_response: str) -> Dict[str, Any]: + """Extract structured data from LLM response - delegates to TracedOutput instances.""" + # Strategy 1: Try to parse as JSON if it looks like JSON + llm_response_stripped = llm_response.strip() + if llm_response_stripped.startswith('{') and llm_response_stripped.endswith('}'): + try: + json_data = json.loads(llm_response_stripped) + # Validate that all fields are expected + validated_data = {} + for field_name, value in json_data.items(): + if field_name in self._output_fields: + validated_data[field_name] = value + else: + print(f"Warning: Unexpected field '{field_name}' in JSON response, ignoring") + return validated_data + except json.JSONDecodeError: + pass + + # Strategy 2: Delegate to each TracedOutput instance for parsing + extracted_data = {} + + for field_name in self._output_fields: + # Get the TracedOutput class variable + traced_output = getattr(self.__class__, field_name, None) + + if traced_output and isinstance(traced_output, TracedOutput): + # Delegate parsing to the TracedOutput instance + field_type = self._field_types.get(field_name, str) + try: + value = traced_output.extract_from_text(llm_response, field_type) + if value is not None: + extracted_data[field_name] = value + except Exception as e: + print(f"Warning: Failed to extract field '{field_name}': {e}") + else: + print(f"Warning: Field '{field_name}' not properly defined as TracedOutput, ignoring") + + return extracted_data + + def _process_structured_inputs(self, **kwargs) -> TracedResponse: + """Process structured inputs and return structured output with Pydantic parsing.""" + # Validate that all required input fields are provided + missing_fields = [field for field in self._input_fields if field not in kwargs] + if missing_fields: + raise ValueError(f"Missing required input field(s): {missing_fields}") + + # For now, use the first input field value as the user prompt + # This will be expanded later with proper parsing/formatting + user_prompt = kwargs[self._input_fields[0]] + llm_response = self._call_llm(user_prompt) + + # Extract structured data from LLM response + extracted_data = self._extract_structured_data(llm_response) + + # Create dynamic Pydantic model for validation + ResponseModel = self._create_dynamic_response_model() + + try: + # Use Pydantic to validate and parse the extracted data + validated_response = ResponseModel(**extracted_data) + + # Convert to TracedResponse + response_data = validated_response.model_dump() + + except ValidationError as e: + # If Pydantic validation fails, include error info + response_data = {} + for output_field in self._output_fields: + # Try to get individual field values, fall back to raw response + response_data[output_field] = extracted_data.get(output_field, llm_response) + + response_data['_validation_errors'] = [str(error) for error in e.errors()] + response_data['_raw_response'] = llm_response + + except Exception as e: + # If extraction fails completely, return raw response + response_data = {} + for output_field in self._output_fields: + response_data[output_field] = llm_response + response_data['_extraction_error'] = str(e) + response_data['_raw_response'] = llm_response + + return TracedResponse(**response_data) + + +@trace.model +class TracedLLM(StructuredInputOutputMixin, DynamicModelMixin): + def __init__(self, + system_prompt: Union[str, None, trace.Node] = None, + llm: AbstractModel = None): + """Initialize TracedLLM with a system prompt. + + Args: + system_prompt: The system prompt to use for LLM calls. If None and the class has a docstring, the docstring will be used. + llm: The LLM model to use for inference + """ + # Use class docstring as system prompt if none provided + if system_prompt is None: + class_docstring = self.__class__.__doc__ + if class_docstring and class_docstring.strip(): + system_prompt = class_docstring.strip() + + self.system_prompt = trace.node(system_prompt) + if llm is None: + llm = LLM() + assert isinstance(llm, AbstractModel), f"{llm} must be an instance of AbstractModel" + self.llm = llm + + # Initialize fields for structured input/output + self._input_fields = [] + self._output_fields = [] + self._field_types = {} # Store type annotations for each field + self._detect_fields() + + def forward(self, *args, **kwargs) -> Union[str, TracedResponse]: + """Main function that handles both direct call and inheritance patterns. + + Args: + *args: For direct pattern - single string argument + **kwargs: For inheritance pattern - named input fields + + Returns: + str: For direct pattern + TracedResponse: For inheritance pattern with structured output fields + """ + if self._input_fields: + # Inheritance pattern: use named arguments + return self._process_structured_inputs(**kwargs) + else: + # Direct pattern: single string argument + if len(args) == 1 and isinstance(args[0], str): + return self._call_llm(args[0]) + else: + raise ValueError("Direct usage requires a single string argument") + + def _call_llm(self, user_prompt: str) -> str: + """Call the LLM with user prompt and system prompt.""" + return trace.operators.call_llm(self.llm, self.system_prompt, user_prompt) diff --git a/opto/flows/ops.py b/opto/flows/ops.py deleted file mode 100644 index 0c988b9f..00000000 --- a/opto/flows/ops.py +++ /dev/null @@ -1,51 +0,0 @@ -import pydantic -import opto.trace as trace -from typing import Union -from opto.utils.llm import AbstractModel, LLM - -""" -TracedLLM: -1. special operations that supports specifying inputs (system_prompt, user_prompt) to LLM and parsing of outputs, wrap - everything under one command. -2. Easy to use interface -- can be inherited by users. - -Usage patterns: - -1. Direct use: (only supports single input, single output) (signature: str -> str) -llm = TracedLLM("You are a helpful assistant.") -llm("Hello, what's the weather in France today?") - -2. Inheritance: -class Scorer(TracedLLM): - "This is a class that scores the response from LLM" - doc: opto.flows.types.TracedInput - score: opto.flows.types.TracedOutput - -scorer = Scorer("You are a helpful assistant that scores the response from LLM") -scorer(doc="The response is ...") -""" - -@trace.model -class TracedLLM: - def __init__(self, - system_prompt: Union[str, None, trace.Node] = None, - llm: AbstractModel = None): - """Initialize TracedLLM with a system prompt. - - Args: - system_prompt: The system prompt to use for LLM calls - llm: The LLM model to use for inference - """ - self.system_prompt = trace.node(system_prompt) - if llm is None: - llm = LLM() - assert isinstance(llm, AbstractModel), f"{llm} must be an instance of AbstractModel" - self.llm = llm - - def forward(self, user_prompt: str) -> str: - """Call the LLM with user prompt, using the configured system prompt.""" - return trace.operators.call_llm(self.llm, self.system_prompt, user_prompt) - - -if __name__ == '__main__': - pass diff --git a/opto/flows/types.py b/opto/flows/types.py index e69de29b..b763330f 100644 --- a/opto/flows/types.py +++ b/opto/flows/types.py @@ -0,0 +1,133 @@ +"""Types for opto flows.""" +from pydantic import BaseModel, Field, create_model, ConfigDict +from typing import Any, Optional, Callable, Dict, Union, Type, List +import re +import json + + +class TracedInput(BaseModel): + """Pydantic model for input fields in TracedLLM inheritance pattern.""" + model_config = ConfigDict(arbitrary_types_allowed=True) + + description: Optional[str] = "Input specified by the user for the LLM." + required: bool = True + + +class TracedOutput(BaseModel): + """Pydantic model for output fields in TracedLLM inheritance pattern.""" + model_config = ConfigDict(arbitrary_types_allowed=True) + + description: Optional[str] = "Output from the LLM." + parser: Optional[Union[Callable[[str], Any], str]] = None # Can be function or regex pattern + default_value: Optional[Any] = None + required: bool = True + + def extract_from_text(self, text: str, field_type: Type) -> Any: + """Extract value from text using parser (function or regex) or default logic.""" + if self.parser: + if callable(self.parser): + # Parser is a function + try: + return self.parser(text) + except: + return self.default_value + elif isinstance(self.parser, str): + # Parser is a regex pattern + match = re.search(self.parser, text, re.IGNORECASE) + if match: + # Find the first non-None group or use group(0) + extracted = None + for group in match.groups(): + if group is not None: + extracted = group + break + if extracted is None: + extracted = match.group(0) + return self._convert_to_type(extracted, field_type) + else: + return self.default_value + + # Fall back to default extraction logic + return self._default_extract(text, field_type) + + def _convert_to_type(self, value: str, field_type: Type) -> Any: + """Convert extracted string to target type.""" + # Default type conversion + if field_type == int: + numbers = re.findall(r'-?\d+', value) + return int(numbers[0]) if numbers else self.default_value + elif field_type == float: + numbers = re.findall(r'-?\d+\.?\d*', value) + return float(numbers[0]) if numbers else self.default_value + elif field_type == bool: + return self._parse_boolean(value) + elif field_type == list: + try: + return json.loads(value) + except: + return [item.strip() for item in value.split(',')] + else: + return value + + def _default_extract(self, text: str, field_type: Type) -> Any: + """Default extraction logic.""" + # If custom parser failed, return default value + return self.default_value + + def _parse_boolean(self, text: str) -> bool: + """Parse boolean from text.""" + text_lower = text.lower().strip() + positive_words = ['true', 'yes', 'correct', 'positive', 'definitely', '1'] + negative_words = ['false', 'no', 'incorrect', 'negative', 'way', '0'] + + if any(word in text_lower for word in positive_words): + return True + elif any(word in text_lower for word in negative_words): + return False + else: + return self.default_value if self.default_value is not None else True + + +class DynamicModelMixin: + """Mixin to provide dynamic model creation capabilities.""" + + @classmethod + def create_response_model(cls, field_definitions: Dict[str, tuple]) -> Type[BaseModel]: + """Create a dynamic Pydantic model from field definitions. + + Args: + field_definitions: Dict mapping field names to (type, TracedOutput) tuples + + Returns: + Dynamically created Pydantic model class + """ + pydantic_fields = {} + + for field_name, (field_type, traced_output) in field_definitions.items(): + # Create Pydantic field with metadata from TracedOutput + field_kwargs = {} + if traced_output.description: + field_kwargs['description'] = traced_output.description + if not traced_output.required: + field_kwargs['default'] = traced_output.default_value + + pydantic_fields[field_name] = (field_type, Field(**field_kwargs)) + + # Create the dynamic model + return create_model(f"{cls.__name__}Response", **pydantic_fields) + + @classmethod + def create_input_model(cls, field_definitions: Dict[str, tuple]) -> Type[BaseModel]: + """Create a dynamic input validation model.""" + pydantic_fields = {} + + for field_name, (field_type, traced_input) in field_definitions.items(): + field_kwargs = {} + if traced_input.description: + field_kwargs['description'] = traced_input.description + if not traced_input.required: + field_kwargs['default'] = None + + pydantic_fields[field_name] = (field_type, Field(**field_kwargs)) + + return create_model(f"{cls.__name__}Input", **pydantic_fields) diff --git a/opto/trace/operators.py b/opto/trace/operators.py index 6b17ea6f..e02b922c 100644 --- a/opto/trace/operators.py +++ b/opto/trace/operators.py @@ -1,6 +1,6 @@ from __future__ import annotations import trace -from typing import TYPE_CHECKING, Any, Dict, Union +from typing import TYPE_CHECKING, Any, Dict, Union, List if TYPE_CHECKING: # to prevent circular import from opto.trace.nodes import Node diff --git a/tests/unit_tests/test_flows.py b/tests/unit_tests/test_flows.py deleted file mode 100644 index 536c8674..00000000 --- a/tests/unit_tests/test_flows.py +++ /dev/null @@ -1,8 +0,0 @@ -from opto import trace, flows - -def test_trace_llm(): - - si = trace.node("You're a helpful assistant.", trainable=True) - user_prompt = "Hi there" - traced_llm = flows.TracedLLM(si) # this is trace.Module - response = traced_llm(user_prompt) From 2ad54cae3ebee5cc459cefb5d629044d59d0d1cb Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 3 Sep 2025 10:47:02 -0400 Subject: [PATCH 172/314] save progress.. --- opto/flows/compose.py | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/opto/flows/compose.py b/opto/flows/compose.py index e3777673..2e4e23fe 100644 --- a/opto/flows/compose.py +++ b/opto/flows/compose.py @@ -4,6 +4,7 @@ from typing import Union, get_type_hints, Any, Dict from opto.utils.llm import AbstractModel, LLM from opto.flows.types import TracedInput, TracedOutput, DynamicModelMixin +from opto.optimizers.utils import extract_xml_like_data import inspect import json import re @@ -30,6 +31,32 @@ class Scorer(TracedLLM): scorer = Scorer() # if a system prompt is passed in here, it will override the docstring. response = scorer(doc="The response is ...") print(response.score) + +When using the inheritance mode, the system prompt augmented to be as follow: + +------------- +You are a helpful assistant generates output based on the instructions and inputs below. + +## Inputs + +### input_name + + +value + +## Instructions +{original system prompt docstring} + +## Outputs +output_name1 [type=str]: description \n +output_name2 [type=List[int]]: description + +## Output Format +Your output should be in the following XML/HTML format: + + +value + """ @@ -92,9 +119,10 @@ def _create_dynamic_response_model(self) -> type[BaseModel]: # TODO: 3. use the dynamic ResponseModel to do the parsing def _extract_structured_data(self, llm_response: str) -> Dict[str, Any]: """Extract structured data from LLM response - delegates to TracedOutput instances.""" - # Strategy 1: Try to parse as JSON if it looks like JSON + # Try to parse as JSON if it looks like JSON llm_response_stripped = llm_response.strip() if llm_response_stripped.startswith('{') and llm_response_stripped.endswith('}'): + # TODO: implement pydantic parsing instead try: json_data = json.loads(llm_response_stripped) # Validate that all fields are expected @@ -108,7 +136,8 @@ def _extract_structured_data(self, llm_response: str) -> Dict[str, Any]: except json.JSONDecodeError: pass - # Strategy 2: Delegate to each TracedOutput instance for parsing + # Then treat it like XML, re-format it into JSON, and use Pydantic to parse + # TODO: implement that extracted_data = {} for field_name in self._output_fields: From ba9ea79124595d42b4756a3d24f73fbc320c0045 Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 3 Sep 2025 18:18:08 -0400 Subject: [PATCH 173/314] priority search simple bug fix. async_run accommondates jupyter notebook --- opto/features/priority_search/priority_search.py | 2 +- opto/trainer/utils.py | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index 35342580..91eedffa 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -408,7 +408,7 @@ def validate(self, candidates, samples, verbose=False, **kwargs): validate_samples.add_samples(samples) # if no validation dataset is provided, append the samples to the validate_samples else: # validate the agents in the validate_dataset # exploration_agents = [rollouts.module for rollouts in samples.samples] # NOTE this might contain some duplicates due to sub_batch_size < batch_size - exploitation_agents = [c.get_module() for c in exploration_candidates] # get the modules from the exploration candidates + exploration_agents = [c.get_module() for c in exploration_candidates] # get the modules from the exploration candidates exploration_samples = Samples(*self.validate_sampler.sample(exploration_agents, description_prefix='Validating exploration candidates: ')) # sample the exploration agents validate_samples.add_samples(exploration_samples) # append the exploration samples to the validate_samples diff --git a/opto/trainer/utils.py b/opto/trainer/utils.py index ffb6b999..f395a57c 100644 --- a/opto/trainer/utils.py +++ b/opto/trainer/utils.py @@ -33,7 +33,7 @@ def async_run(runs, args_list = None, kwargs_list = None, max_workers = None, de if kwargs_list is None: kwargs_list = [{}] * len(runs) - if (max_workers == 1) and allow_sequential_run: # run without asyncio + if (max_workers == 1) and allow_sequential_run: # run without asyncio print(f"{description} (Running sequentially).") return [run(*args, **kwargs) for run, args, kwargs in zip(runs, args_list, kwargs_list)] else: @@ -41,14 +41,24 @@ async def _run(): loop = asyncio.get_event_loop() with ThreadPoolExecutor(max_workers=max_workers) as executor: tasks = [loop.run_in_executor(executor, functools.partial(run, *args, **kwargs)) - for run, args, kwargs, in zip(runs, args_list, kwargs_list)] + for run, args, kwargs, in zip(runs, args_list, kwargs_list)] # Use the description in the tqdm progress bar if provided if description: return await tqdm_asyncio.gather(*tasks, desc=description) else: return await tqdm_asyncio.gather(*tasks) - return asyncio.run(_run()) + + # Handle Jupyter notebook + try: + return asyncio.run(_run()) + except RuntimeError: + loop = asyncio.get_running_loop() + # We're in a loop (like Jupyter), so we need to run in a new thread + import concurrent.futures + with concurrent.futures.ThreadPoolExecutor() as executor: + future = executor.submit(asyncio.run, _run()) + return future.result() def batch_run(max_workers=None, description=None): From e9611313f377c11cf99630a7bc8c15aa9028913e Mon Sep 17 00:00:00 2001 From: windweller Date: Thu, 4 Sep 2025 17:19:12 -0400 Subject: [PATCH 174/314] add a testing / example code to see if search methods work --- examples/priority_search_on_convex_fn.py | 255 +++++++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 examples/priority_search_on_convex_fn.py diff --git a/examples/priority_search_on_convex_fn.py b/examples/priority_search_on_convex_fn.py new file mode 100644 index 00000000..1ac3b98e --- /dev/null +++ b/examples/priority_search_on_convex_fn.py @@ -0,0 +1,255 @@ +import re +import sys +import string +import numpy as np +from opto.trace.utils import dedent + + +def np_random(seed: int | None = None) -> tuple[np.random.Generator, int]: + """Returns a NumPy random number generator (RNG) along with seed value from the inputted seed. + + If ``seed`` is ``None`` then a **random** seed will be generated as the RNG's initial seed. + This randomly selected seed is returned as the second value of the tuple. + + .. py:currentmodule:: gymnasium.Env + + This function is called in :meth:`reset` to reset an environment's initial RNG. + + Args: + seed: The seed used to create the generator + + Returns: + A NumPy-based Random Number Generator and generator seed + + Raises: + Error: Seed must be a non-negative integer + """ + if seed is not None and not (isinstance(seed, int) and 0 <= seed): + if isinstance(seed, int) is False: + raise Exception( + f"Seed must be a python integer, actual type: {type(seed)}" + ) + else: + raise Exception( + f"Seed must be greater or equal to zero, actual value: {seed}" + ) + + seed_seq = np.random.SeedSequence(seed) + np_seed = seed_seq.entropy + rng = np.random.Generator(np.random.PCG64(seed_seq)) + return rng, np_seed + + +class LossLandscapeBase: + def __init__(self, callable_func, x_low, x_high, min_y, optimal_sol, + feedback=0, seed=None, precision_digit=2, horizon=10): + self.x_low = x_low + self.x_high = x_high + + self._np_random = None + self.stop_keywords = ['reach', 'stay', 'stop'] + + self.callable_func = callable_func + + self.prev_x = None + self.left_attempts = horizon + self.min_y = min_y + self.optimal_sol = optimal_sol + self.precision_digit = precision_digit + + self.horizon = horizon + + self._seed = self.seed(seed) + + self.reward_range = (self.get_min_reward(), -self.min_y) + + # Note: currently we treat the first line as "instruction" + self.docstring = dedent(""" + You are trying to minimize the output (y) of a function by choosing input (x). The goal is to choose x such that y is as small as possible. + + You get to observe y once you choose the value of x, where x is a 2-dimensional vector. + This means x = [x1, x2], where x1 and x2 are real numbers. + + + The range of x1 and x2 is [{}, {}]. + Please do not choose x outside of this range. + + Choose x within {} attempts. + You can choose to stop at any time. + + Output format: + x = [x1, x2] + """) + + self.docstring = self.docstring.strip() + self.docstring = self.docstring.format(self.x_low, self.x_high, self.horizon) + + self.called_reset = False + + def get_min_reward(self): + x_range = [self.x_low, self.x_high] + y_max = [self.callable_func(np.array([x_range[i], x_range[j]])) for i in range(2) for j in range(2)] + y_max = max(y_max) + return -y_max + + def get_optimal_solution(self): + return self.optimal_sol + + def reset(self, **kwargs): + if 'seed' in kwargs: + self._seed = self.seed(kwargs['seed']) + # we sample the initial state from the uniform distribution + x = self.np_random.uniform(self.x_low, self.x_high, size=2) + # we round the floating point precision to 2 decimal places + x = np.round(x, self.precision_digit) + self.prev_x = x + + y = self.callable_func(x) + + self.left_attempts = self.horizon + + obs = "x={}\nFunction outputs y = {}\nYou have {} attempts left!\n".format(x.tolist(), y, self.left_attempts) + obs += "Please output the next x that will make this function output the smallest y.\n" + obs += "Format: x = [x1, x2]\n" + obs += "Output:" + + self.called_reset = True + + return obs + + def seed(self, seed=None): + """Seed the PRNG of this space and possibly the PRNGs of subspaces.""" + self._np_random, seed = np_random(seed) + return [seed] + + @property + def np_random(self): + """Lazily seed the PRNG since this is expensive and only needed if sampling from this space.""" + if self._np_random is None: + self.seed() + return self._np_random # type: ignore ## self.seed() call guarantees right type. + + def text_extract(self, text): + # return np.array([x1, x2]), agent decides to stop + for stop_word in self.stop_keywords: + if stop_word in text: + return None, True + + pattern = r'\[(-?\d+\.?\d*(?:e[-+]?\d+)?),\s*(-?\d+\.?\d*(?:e[-+]?\d+)?)\]' + match = re.search(pattern, text) + if match is None: + return None, False + else: + numbers = [float(g) for g in match.groups()] + return np.array(numbers), False + + def step(self, action): + if not self.called_reset: + raise Exception("must call env.reset() first before step()") + + x, stop = self.text_extract(action) + feedback = '' + if x is None and stop is False: + feedback = f'You entered an invalid action: {action}' + f" Please enter a valid action within ({self.x_low, self.x_high})" + + return None, -1, True, {'feedback': feedback, 'success': False} + + if stop: + success = np.abs(self.callable_func(self.prev_x) - self.min_y) < 1e-2 + feedback = f'You have chosen to stop at {self.prev_x}.' + if success: + feedback += ' You have reached the minimum!' + else: + feedback += ' You have not reached the minimum!' + return None, float(self.callable_func(self.prev_x)), True, {'feedback': feedback, 'success': success} + + loss = self.callable_func(x) + + if np.abs(loss - self.min_y) < 1e-2: + feedback = "Function outputs y: {}\nYou have reached the minimum!".format(self.min_y) + return feedback, -self.min_y, True, {'feedback': feedback, "success": True} + + obs = "Function outputs y = {}\nYou have {} attempts left!\n".format(loss, self.left_attempts) + obs += "Please output the next x that will make this function output the smallest y.\n" + obs += "Format: x = [x1, x2]\n" + obs += "Output:" + + self.prev_x = x + self.left_attempts -= 1 + + r = np.clip(float(-loss), self.get_min_reward(), -self.min_y) + + feedback += f"You chose {action}. Choose different numbers such that you can minimize y." + + return obs, r, False, {'feedback': feedback, 'success': False} + + +class Rosenbrock(LossLandscapeBase): + def __init__(self, a=1, b=1, feedback=0, seed=None, horizon=10): # b = 100 + # https://en.wikipedia.org/wiki/Rosenbrock_function + # all of them are lambda functions that expect Numpy array of shape (2,) + two_dim_rosenbrock = lambda x: (a - x[0]) ** 2 + b * (x[1] - x[0] ** 2) ** 2 + super().__init__(callable_func=two_dim_rosenbrock, + x_low=-5, x_high=10, min_y=0, optimal_sol=np.ones(2), + feedback=feedback, seed=seed, horizon=horizon) + + +class SixHumpCamel(LossLandscapeBase): + def __init__(self, feedback=0, seed=None, horizon=10): + func = lambda x: (4 - 2.1 * x[0] ** 2 + (x[0] ** 4) / 3) * x[0] ** 2 + x[0] * x[1] + (-4 + 4 * x[1] ** 2) * x[ + 1] ** 2 + # note that SixHumpCamel has two global minima + # also the range on x is x1 = [-3, 3], x2 = [-2, 2] + # but we use x1 = [-2, 2], x2 = [-3, 3] for simplicity + super().__init__(callable_func=func, + x_low=-2, x_high=2, min_y=-1.0316, + optimal_sol=[np.array([0.0898, -0.7126]), np.array([-0.0898, 0.7126])], + feedback=feedback, seed=seed, horizon=horizon, precision_digit=4) + +# ============ Add testing code ============= +import datasets +import numpy as np +from opto import trace +from opto.utils.llm import LLM, LiteLLM +from opto.optimizers import OptoPrimeV2 as OptoPrime +from opto.features.priority_search import PrioritySearch as SearchAlgorithm +from opto.trainer.guide import Guide +from opto.trainer.loggers import TensorboardLogger +from opto.trainer.guide import LLMJudge +from typing import Any +from opto import trainer + + +class RewardGuide(Guide): + def __init__(self, env): + self.env = env + + def get_feedback(self, query: str, response: str, reference=None, **kwargs) -> Tuple[float, str]: + # score, feedbak str + obs, reward, done, info = self.env.step(response) + + return -reward, obs + '\n\n' + info['feedback'] + +env = SixHumpCamel(horizon=200) +train_dataset = dict(inputs=[None], infos=[None]) +instruction = env.reset() +initial_input = instruction.split("\n")[0].strip() +param = trace.node(initial_input, description='Input x into the hidden function to get y.', trainable=True) + +guide = RewardGuide(env) + +trainer.train( + model=param, + # optimizer='OptoPrimeV2', # by default, OPROv2 is used for single-node optimization + algorithm=SearchAlgorithm, + train_dataset=train_dataset, + # trainer kwargs + num_epochs=5, + batch_size=1, + verbose='output', # 'output', 'all' + guide=guide, + num_candidates=4, + num_proposals=4, + optimizer_kwargs={'objective':"You have a task of guessing two numbers. You should make sure your guess minimizes y.", + 'memory_size': 0} +) \ No newline at end of file From 05c59d7c4b2f16f8fc4fe1da00e00979ddb13ed2 Mon Sep 17 00:00:00 2001 From: windweller Date: Thu, 4 Sep 2025 17:19:36 -0400 Subject: [PATCH 175/314] add an import that was missing --- examples/priority_search_on_convex_fn.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/priority_search_on_convex_fn.py b/examples/priority_search_on_convex_fn.py index 1ac3b98e..83df256f 100644 --- a/examples/priority_search_on_convex_fn.py +++ b/examples/priority_search_on_convex_fn.py @@ -218,6 +218,7 @@ def __init__(self, feedback=0, seed=None, horizon=10): from opto.trainer.guide import LLMJudge from typing import Any from opto import trainer +from typing import Tuple class RewardGuide(Guide): From 431d0a32c3c09943b60b302a26ec178215f29003 Mon Sep 17 00:00:00 2001 From: windweller Date: Thu, 4 Sep 2025 17:20:30 -0400 Subject: [PATCH 176/314] reward is already the negative of the loss -- no need to negate it --- examples/priority_search_on_convex_fn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/priority_search_on_convex_fn.py b/examples/priority_search_on_convex_fn.py index 83df256f..98077b84 100644 --- a/examples/priority_search_on_convex_fn.py +++ b/examples/priority_search_on_convex_fn.py @@ -229,7 +229,7 @@ def get_feedback(self, query: str, response: str, reference=None, **kwargs) -> T # score, feedbak str obs, reward, done, info = self.env.step(response) - return -reward, obs + '\n\n' + info['feedback'] + return reward, obs + '\n\n' + info['feedback'] env = SixHumpCamel(horizon=200) train_dataset = dict(inputs=[None], infos=[None]) From 5d41e1534b5c89d704e16c60f0792d09af693f9f Mon Sep 17 00:00:00 2001 From: chinganc Date: Fri, 5 Sep 2025 03:50:27 +0000 Subject: [PATCH 177/314] Remove dependency of SearchTemplate on Minibatch. Fix various bugs. --- .../priority_search/priority_search.py | 108 +++++++++++------- .../priority_search/search_template.py | 25 +++- opto/features/priority_search/utils.py | 3 +- 3 files changed, 91 insertions(+), 45 deletions(-) diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index 91eedffa..682242a9 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -8,7 +8,7 @@ from opto.trainer.utils import async_run from opto.trainer.algorithms.basic_algorithms import batchify from opto.features.priority_search.search_template import SearchTemplate, Samples -from opto.features.priority_search.utils import set_module_parameters, remap_update_dict, create_module_from_update_dict +from opto.features.priority_search.utils import set_module_parameters, remap_update_dict, create_module_from_update_dict, is_module_copy class ModuleCandidate: @@ -30,9 +30,6 @@ def __init__(self, self.update_dict = remap_update_dict(self.base_module, self.update_dict) self.rollouts = [] # list of dicts containing the rollout information (not RolloutsGraph, but a list of dicts) self.created_time = time.time() - self._n_updates = 0 # number of times this candidate has been updated - self._n_confidence_queries = 1 # number of times the confidence score has been queried - self._confidence_interval = None def get_module(self): """ Apply the update_dict to the base_module and return the updated module. @@ -61,15 +58,15 @@ def __deepcopy__(self, memo): def __eq__(self, other): """ Check if two candidates are equal based on their base_module and update_dict. """ assert isinstance(other, ModuleCandidate), "other must be an instance of ModuleCandidate." - return self.update_dict == other.update_dict + return (self.update_dict == other.update_dict) and is_module_copy(self.base_module, other.base_module) # TODO better way? def __lt__(self, other): """ Compare two candidates based on their update_dict. """ assert isinstance(other, ModuleCandidate), "other must be an instance of ModuleCandidate." return self.created_time > other.created_time - # This would give priority to later created candidates in the heap memory - # since the heapq is a min-heap . + # self < other if, self is created later than other + # Since we will use minheap, and this would give priority to later created candidates in the heap memory. def __hash__(self): """ Hash the candidate based on its update_dict. """ @@ -84,8 +81,6 @@ def add_rollouts(self, rollouts: List[Dict[str, Any]]): "Each rollout must contain 'module', 'x', 'info', 'target', 'score', and 'feedback' keys." self.rollouts.extend(rollouts) - self._confidence_interval = None # reset the confidence interval - self._n_updates += 1 # increment the number of updates def mean_score(self): """ Compute the score of the candidate based on the rollouts. """ @@ -94,36 +89,42 @@ def mean_score(self): scores = [r['score'] for r in self.rollouts] return np.mean(scores) if scores else None - def compute_score_confidence(self, min_score, max_score, scaling_constant=1.0): + def compute_score_confidence(self, min_score, max_score, scaling_constant=1.0, total_trials=1): """Compute the UCB, mean, LCB score for the candidate. After queried, the number of confidence queries is incremented. - UCB = mean_score + scaling_constant * sqrt(ln(total_trials) / candidate_trials) * (max_score - min_score) + UCB = mean_score + scaling_constant * sqrt(ln(total_trials) / n_scores) * (max_score - min_score) UCB = clip(UCB, min_score, max_score) - LCB = mean_score - scaling_constant * sqrt(ln(total_trials) / candidate_trials) * (max_score - min_score) + LCB = mean_score - scaling_constant * sqrt(ln(total_trials) / n_scores) * (max_score - min_score) LCB = clip(LCB, min_score, max_score) Args: - candidate (ModuleCandidate): The candidate for which to compute the UCB score. + min_score (float): The minimum score for clipping. + max_score (float): The maximum score for clipping. + scaling_constant (float): The scaling constant for the exploration term. + total_trials (int): The total number of trials conducted. Must be at least 1. Returns: - float: The computed UCB score for the candidate. + lcb_score (float): The lower confidence bound score. + mean_score (float): The mean score. + ucb_score (float): The upper confidence bound score. """ # Get scores from rollouts scores = [r['score'] for r in self.rollouts] - # If no rollouts, return a high exploration score to encourage trying this candidate if not scores: return min_score, None, max_score # Calculate mean score for this candidate mean_score = np.mean(scores) - candidate_trials = len(scores) + n_scores = len(scores) + assert n_scores == self.num_rollouts, "Number of scores should match number of rollouts." # Calculate how many times the confidence interval has been used to form a union bound - total_trials = min(self._n_confidence_queries) + 1 # this is an upper bound, since log(1) = 0 + assert total_trials >= 1, "total_trials must be at least 1." + total_trials = total_trials + 1 # this is an upper bound, since log(1) = 0 # Compute the exploration term based on Hoeffding's inequality - exploration_term = scaling_constant * np.sqrt(np.log(total_trials) / candidate_trials) * (max_score - min_score) + exploration_term = scaling_constant * np.sqrt(np.log(total_trials) / n_scores) * (max_score - min_score) # Calculate UCB score ucb_score = mean_score + exploration_term @@ -133,28 +134,14 @@ def compute_score_confidence(self, min_score, max_score, scaling_constant=1.0): lcb_score = mean_score - exploration_term lcb_score = np.clip(lcb_score, min_score, max_score) - self._n_confidence_queries += 1 # increment the number of confidence queries - - self._confidence_interval = dict(lcb_score=lcb_score, ucb_score=ucb_score, mean_score=mean_score) return lcb_score, mean_score, ucb_score - @property - def confidence_interval(self): - # This is a cached property that returns the confidence interval of the candidate. - # This is for accessing the confidence interval without increasing the number of confidence queries. E.g. this is useful when using both LCB and UCB of the same candidate. - if self._confidence_interval is None: - raise ValueError("Confidence interval has not been computed yet. Call compute_score_confidence() first.") - return self._confidence_interval @property def num_rollouts(self): """ Return the number of rollouts collected for this candidate. """ return len(self.rollouts) - @property - def n_updates(self): - """ Return the number of times this candidate has been updated. """ - return self._n_updates class HeapMemory: # This is a basic implementation of a heap memory that uses a priority queue to store candidates. @@ -198,6 +185,7 @@ def best(self): return self.memory[0] +# TODO check saving and loading class PrioritySearch(SearchTemplate): """ A search algorithm that uses a priority queue to explore the parameter space and propose new candidates. @@ -222,7 +210,7 @@ def train(self, train_dataset, # dataset of (x, info) pairs to train the agent *, # validation - validate_dataset = None, # same format as train_dataset; if None use the current batch. + validate_dataset = None, # same format as train_dataset; if None, use the current batch. validate_guide = None, # to provide scores for the validation set # training loop batch_size = 1, # batch size for updating the agent @@ -233,8 +221,8 @@ def train(self, verbose = False, # whether to print the output of the agent # evaluation test_dataset = None, # dataset of (x, info) pairs to evaluate the agent - test_frequency: Union[int, None] = 1, # frequency of evaluation - num_eval_samples: int = 1, # number of samples to use to evaluate each input + test_frequency: Union[int, None] = 1, # frequency of evaluation (set it to be negative to skip the first evaluation) + num_eval_samples: int = 1, # number of times to evaluate each input; when greater than 1, the scores are averaged. # logging log_frequency = None, # frequency of logging save_frequency: Union[int, None] = None, # frequency of saving the agent @@ -250,6 +238,35 @@ def train(self, # Additional keyword arguments **kwargs ): + """ Train the agent using the Priority Search algorithm. + + Args: + guide (callable): A function that provides feedback for the agent. + train_dataset (list): A list of (x, info) pairs to train the agent. + validate_dataset (list, optional): A list of (x, info) pairs to validate the proposed candidates. If None, the current batch is used. Defaults to None. + validate_guide (callable, optional): A function that provides feedback for the validation set. If None, the training guide is used. Defaults to None. + batch_size (int, optional): The batch size for updating the agent. Defaults to 1. + sub_batch_size (int, optional): The sub-batch size that each optimizer attends to. If None, it is set to batch_size. Defaults to None. + score_range (tuple, optional): A tuple of (min_score, max_score) to clip the scores. If None, no clipping is applied. Defaults to None. + num_epochs (int, optional): The number of training epochs. Defaults to 1. + num_threads (int, optional): The maximum number of threads to use. If None, it uses the number of CPU cores. Defaults to None. + verbose (bool, optional): Whether to print the output of the agent. Defaults to False. + test_dataset (list, optional): A list of (x, info) pairs to evaluate the agent. If None, no evaluation is performed. Defaults to None. + test_frequency (int or None, optional): The frequency of evaluation. If None, no evaluation is performed. If negative, skips the first evaluation. Defaults to 1. + num_eval_samples (int, optional): The number of times to evaluate each input; when greater than 1, the scores are averaged. Defaults to 1. + log_frequency (int or None, optional): The frequency of logging. If None, no logging is performed. Defaults to None. + save_frequency (int or None, optional): The frequency of saving the agent. If None, no saving is performed. Defaults to None. + save_path (str, optional): The path to save the agent. Defaults to "checkpoints/agent.pkl". + num_candidates (int, optional): The number of candidates to propose for exploration. Defaults to 10. + num_proposals (int, optional): The number of proposals to generate per optimizer. Defaults to 1. + validate_proposals (bool, optional): Whether to validate the proposed parameters for exploration. Defaults to True. + use_best_candidate_to_explore (bool, optional): Whether to use the best candidate as part of the exploration candidates. Defaults to True. + memory_size (int, optional): The size of the heap memory to store the candidates. If None, no limit is set. Defaults to None. + score_function (str, optional): The function to compute the score for the candidates; 'mean' or 'ucb'. Defaults to 'mean'. + ucb_exploration_constant (float, optional): The exploration constant for UCB score function. Defaults to 1.0. + **kwargs: Additional keyword arguments that may be used by the implementation. + """ + # Create agents and optimizers for search self.num_candidates = num_candidates # number of candidates to propose by each optimizer call @@ -257,14 +274,14 @@ def train(self, self.validate_proposals = validate_proposals # whether to validate the proposed parameters self.use_best_candidate_to_explore = use_best_candidate_to_explore self.score_function = score_function # function to compute the score for the candidates + if score_range is None: + score_range = (0, 1) if score_function == 'ucb': # this requires a bounded score range. By default, it is set to (0, 1) - if score_range is None: - score_range = (0, 1) assert score_range[1]-score_range[0] < float('inf'), \ "For UCB score function, score_range must be finite. Use 'mean' score function if you want to use unbounded scores." - self.ucb_exploration_constant = 1. - self._exploration_candidates = None + self.ucb_exploration_constant = ucb_exploration_constant + self._exploration_candidates = None # This stores the latest candidates used for exploration self.memory = HeapMemory(size=memory_size) # Initialize the heap memory with a size limit @@ -287,8 +304,14 @@ def train(self, **kwargs) - def update(self, samples=None, verbose=False, **kwargs): + def update(self, + samples: Union[Samples, None] = None, + verbose: bool = False, + **kwargs): #-> Tuple[Dict[ParameterNode, Any], List[trace.Module], Dict[str, Any]]: + """ Update the agent using the collected samples. + """ + # samples is None in the first iteration if samples is not None: # 1. Propose new parameters based on running LLM optimizers on the collected samples candidates = self.propose(samples, verbose=verbose, **kwargs) # List of ModuleCandidates @@ -296,7 +319,7 @@ def update(self, samples=None, verbose=False, **kwargs): validate_results = self.validate(candidates, samples, verbose=verbose, **kwargs) # this updates the priority queue # 3. Update the priority queue with the validation results self.update_memory(validate_results, verbose=verbose, **kwargs) # samples are provided here in case candidates do not capture full information - else: + else: # The first iteration. if len(self.memory) == 0: self.memory.push(self.max_score, ModuleCandidate(self.agent)) # Push the base agent as the first candidate (This gives the initialization of the priority queue) # 4. Explore and exploit the priority queue @@ -535,7 +558,8 @@ def compute_priority(self, candidate): lcb_score, mean_score, ucb_score = candidate.compute_score_confidence( min_score=self.min_score, max_score=self.max_score, - scaling_constant=self.ucb_exploration_constant + scaling_constant=self.ucb_exploration_constant, + total_trials=self.n_iters + 1 # total number of trials conducted so far ) return ucb_score # return the UCB score else: diff --git a/opto/features/priority_search/search_template.py b/opto/features/priority_search/search_template.py index 5654d832..2bbdc652 100644 --- a/opto/features/priority_search/search_template.py +++ b/opto/features/priority_search/search_template.py @@ -1,9 +1,10 @@ import numpy as np from typing import Union, List, Tuple, Dict, Any, Optional from opto import trace -from opto.trainer.algorithms.basic_algorithms import Minibatch +from opto.trainer.algorithms.basic_algorithms import Trainer from opto.trainer.loader import DataLoader from opto.features.priority_search.sampler import Sampler, RolloutsGraph +from opto.trainer.evaluators import evaluate # TODO update evaluate implementation # TODO save and load SearchTemplate # TODO async version??? @@ -55,10 +56,22 @@ def n_sub_batches(self) -> int: -class SearchTemplate(Minibatch): +class SearchTemplate(Trainer): # This only uses __init__ and evaluate of Minibatch class. """ This implements a generic template for search algorithm. """ + def __init__(self, + agent, + optimizer, + num_threads: int = None, # maximum number of threads to use for parallel execution + logger=None, + *args, + **kwargs, + ): + super().__init__(agent, num_threads=num_threads, logger=logger, *args, **kwargs) + self.optimizer = optimizer + self.n_iters = 0 # number of iterations + def train(self, guide, # guide to provide feedback train_dataset, # dataset of (x, info) pairs to train the agent @@ -223,6 +236,14 @@ def test(self, test_dataset, guide): print(f"Warning: Test score {test_score} is out of the range {self._score_range}.") return {'test_score': test_score} + def evaluate(self, agent, guide, xs, infos, min_score=None, num_samples=1, num_threads=None, description=None): + """ Evaluate the agent on the given dataset. """ + num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads + test_scores = evaluate(agent, guide, xs, infos, min_score=min_score, num_threads=num_threads, + num_samples=num_samples, description=description) + if all([s is not None for s in test_scores]): + return np.mean(test_scores) + def save(self, save_path): self.save_agent(save_path, self.n_iters) # TODO save full state of self diff --git a/opto/features/priority_search/utils.py b/opto/features/priority_search/utils.py index e4c6906d..df9fbf69 100644 --- a/opto/features/priority_search/utils.py +++ b/opto/features/priority_search/utils.py @@ -45,7 +45,8 @@ def is_module_copy(a, b): _matched = [] for p_b in parameters_b: _matched.append(is_node_copy(p_a, p_b)) - np.array(matched) + matched.append(_matched) + matched = np.array(matched) if np.all(np.sum(matched, axis=1) == 1) and np.all(np.sum(matched, axis=0) == 1): return True return False From b3e4f07dbfde2fe3c59a62a11f912b9253f540ca Mon Sep 17 00:00:00 2001 From: chinganc Date: Fri, 5 Sep 2025 17:53:48 +0000 Subject: [PATCH 178/314] Allow customizing exploitation criterion. --- .../priority_search/priority_search.py | 63 +++++++++++++------ 1 file changed, 45 insertions(+), 18 deletions(-) diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index 682242a9..81c6726f 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -178,11 +178,20 @@ def __iter__(self): """ Iterate over the items in the heap memory. """ return iter(self.memory) - def best(self): - """ Return the best item in the heap memory without removing it. """ + def best(self, criterion=None): + """ Return the best item in the heap memory without removing it. + + If criterion is None, return the item with the highest priority (lowest negative score). + If criterion is a callable function, return the item that maximizes the criterion. + """ if not self.memory: raise IndexError("best from an empty heap memory") - return self.memory[0] + if criterion is None: + return self.memory[0] # return the item with the highest priority (lowest negative score) + else: + assert callable(criterion), "criterion must be a callable function." + return max(self.memory, key=lambda x: criterion(x[1])) + # TODO check saving and loading @@ -198,11 +207,14 @@ class PrioritySearch(SearchTemplate): 5. The proposed parameters are validated by running the agents on the validation dataset, which can be the current batch or a separate validation dataset when provided. When validate_proposals is set to True, the exploration candidates are also validated. 6. The validation results are used to update the priority queue, which stores the candidates and their scores. The candidates are stored as ModuleCandidate objects, which contain the base module, update dictionary, and rollouts (i.e. raw statistics of the candidate). - This algorithm template can be subclassed to implement specific search algorithms by overriding the `exploit`, `explore`, and `compute_priority` methods. + This algorithm template can be subclassed to implement specific search algorithms by overriding the `exploit`, `explore`, and `compute_exploration_priority` methods. The `exploit` method is used to select the best candidate from the priority queue, the `explore` method is used to generate new candidates from the priority queue, and - the `compute_priority` method is used to compute the score for ranking in the priority queue. + the `compute_exploration_priority` method is used to compute the score for ranking in the priority queue. + + By default, `compute_exploration_priority` computes the mean score of the rollouts. `exploit` simply returns the candidate with highest priority from the priority queue, and `explore` generates the top `num_candidates` candidates from the priority queue. - By default, `compute_priority` computes the mean score of the rollouts. `exploit` simply returns the best candidate from the priority queue, and `explore` generates the top `num_candidates` candidates from the priority queue. + + `compute_exploration_priority`, `compute_exploitation_priority` can be overridden to implement different strategies for computing the priority and selecting the best candidate. """ def train(self, @@ -480,7 +492,7 @@ def update_memory(self, validate_results, verbose: bool = False, **kwargs): print("--- Updating memory with validation results...") if verbose else None for candidate, rollouts in validate_results.items(): candidate.add_rollouts(rollouts) # add the rollouts to the candidate - priority = self.compute_priority(candidate) # compute the priority for the candidate + priority = self.compute_exploration_priority(candidate) # compute the priority for the candidate self.memory.push(priority, candidate) #### @@ -494,11 +506,12 @@ def explore(self, verbose: bool = False, **kwargs): """ print(f"--- Generating {min(len(self.memory), self.num_candidates)} exploration candidates...") if verbose else None # pop top self.num_candidates candidates from the priority queue + # self._best_candidate is the exploited candidate from the previous iteration top_candidates = [self._best_candidate] if self.use_best_candidate_to_explore else [] - priorities = [] # to store the priorities of the candidates + priorities = [] # to store the priorities of the candidates for logging while len(top_candidates) < self.num_candidates and self.memory: - priority, candidate = self.memory.pop() # pop the top candidate from the priority queue - priority = - priority # remember that we stored negative scores in the priority queue + neg_priority, candidate = self.memory.pop() # pop the top candidate from the priority queue + priority = - neg_priority # remember that we stored negative scores in the priority queue priorities.append(priority) # store the priority of the candidate if self.use_best_candidate_to_explore: if candidate == self._best_candidate: # skip if it is already in the top candidates @@ -516,27 +529,41 @@ def explore(self, verbose: bool = False, **kwargs): return top_candidates, info_dict - def exploit(self, verbose: bool = False, **kwargs): - # NOTE This function can be overridden by subclasses to compute a different score + def exploit(self, verbose: bool = False, **kwargs) -> Tuple[ModuleCandidate, Dict[str, Any]]: """ Exploit the best candidate from the priority queue. This method should not change the priority queue. Args: + verbose (bool, optional): Whether to print verbose output. Defaults to False. **kwargs: Additional keyword arguments that may be used by the implementation. Returns: ModuleCandidate: The best candidate from the priority queue. """ print("--- Exploiting the best candidate...") if verbose else None - # Right now, we just return the best candidate from the priority queue - # This function can be overridden by subclasses to implement a different exploitation strategy if not self.memory: raise ValueError("The priority queue is empty. Cannot exploit.") - priority, best_candidate = self.memory.best() # (priority, candidate) - priority = - priority # remember that we stored negative scores in the priority queue + neg_priority, best_candidate = self.memory.best(self.compute_exploitation_priority) # (priority, candidate) + priority = - neg_priority # remember that we stored negative scores in the priority queue return best_candidate, { 'best_candidate_priority': priority, # remember that we stored negative scores in the priority queue 'best_candidate_mean_score': best_candidate.mean_score(), # mean score of the candidate's rollouts } - def compute_priority(self, candidate): + # TODO refactor below to reuse scoring + def compute_exploitation_priority(self, candidate) -> float: + # NOTE This function can be overridden by subclasses to compute a different score + """ Compute the score for the candidate based on the rollouts during the validation phase. + It can be overridden by subclasses to implement a different scoring strategy. + + Args: + candidate (ModuleCandidate): The candidate for which to compute the score. + Returns: + float: The computed score for the candidate. Higher scores indicate higher priority. + """ + if not isinstance(candidate, ModuleCandidate): + raise TypeError("candidate must be an instance of ModuleCandidate.") + # By default, we compute the mean score of the rollouts + return candidate.mean_score() + + def compute_exploration_priority(self, candidate) -> float: # NOTE This function can be overridden by subclasses to compute a different score """ Compute the score for the candidate based on the rollouts during the validation phase. It can be overridden by subclasses to implement a different scoring strategy. @@ -544,7 +571,7 @@ def compute_priority(self, candidate): Args: candidate (ModuleCandidate): The candidate for which to compute the score. Returns: - float: The computed score for the candidate. + float: The computed score for the candidate. Higher scores indicate higher priority. """ if not isinstance(candidate, ModuleCandidate): raise TypeError("candidate must be an instance of ModuleCandidate.") From 3fa988bf822f6b06a43bc6b3101a2b5491137995 Mon Sep 17 00:00:00 2001 From: chinganc Date: Fri, 5 Sep 2025 17:54:34 +0000 Subject: [PATCH 179/314] Minor update the convex_fn example --- examples/priority_search_on_convex_fn.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/priority_search_on_convex_fn.py b/examples/priority_search_on_convex_fn.py index 98077b84..25991c12 100644 --- a/examples/priority_search_on_convex_fn.py +++ b/examples/priority_search_on_convex_fn.py @@ -238,12 +238,15 @@ def get_feedback(self, query: str, response: str, reference=None, **kwargs) -> T param = trace.node(initial_input, description='Input x into the hidden function to get y.', trainable=True) guide = RewardGuide(env) +logger = TensorboardLogger(log_dir='./logs/priority_search_on_convex_fn') trainer.train( model=param, # optimizer='OptoPrimeV2', # by default, OPROv2 is used for single-node optimization algorithm=SearchAlgorithm, train_dataset=train_dataset, + logger=logger, + score_range=[-100, 100], # trainer kwargs num_epochs=5, batch_size=1, @@ -251,6 +254,7 @@ def get_feedback(self, query: str, response: str, reference=None, **kwargs) -> T guide=guide, num_candidates=4, num_proposals=4, + validate_proposal=True, # XXX optimizer_kwargs={'objective':"You have a task of guessing two numbers. You should make sure your guess minimizes y.", 'memory_size': 0} ) \ No newline at end of file From 15fcf3393c1ea1b0331ff0fec30cea1f87c7b004 Mon Sep 17 00:00:00 2001 From: chinganc Date: Sat, 6 Sep 2025 06:27:28 +0000 Subject: [PATCH 180/314] Update priority_search to track optimizer --- examples/priority_search_on_convex_fn.py | 12 +- .../priority_search/priority_search.py | 231 ++++++++++++------ opto/features/priority_search/sampler.py | 63 +++-- .../priority_search/search_template.py | 52 ++-- opto/features/priority_search/utils.py | 2 +- opto/optimizers/optimizer.py | 16 +- tests/unit_tests/test_priority_search.py | 2 +- 7 files changed, 242 insertions(+), 136 deletions(-) diff --git a/examples/priority_search_on_convex_fn.py b/examples/priority_search_on_convex_fn.py index 25991c12..12bbd62c 100644 --- a/examples/priority_search_on_convex_fn.py +++ b/examples/priority_search_on_convex_fn.py @@ -246,15 +246,15 @@ def get_feedback(self, query: str, response: str, reference=None, **kwargs) -> T algorithm=SearchAlgorithm, train_dataset=train_dataset, logger=logger, - score_range=[-100, 100], + score_range=[-10, 10], # trainer kwargs num_epochs=5, - batch_size=1, - verbose='output', # 'output', 'all' + batch_size=2, # this is just for testing. effectively, this is the same batch_size=1 and num_proposals=4 + sub_batch_size=1, + verbose='output', guide=guide, num_candidates=4, - num_proposals=4, - validate_proposal=True, # XXX + num_proposals=2, optimizer_kwargs={'objective':"You have a task of guessing two numbers. You should make sure your guess minimizes y.", - 'memory_size': 0} + 'memory_size': 10} ) \ No newline at end of file diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index 81c6726f..77065afe 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -5,9 +5,10 @@ from typing import Union, List, Tuple, Dict, Any, Optional from opto import trace from opto.trace.nodes import ParameterNode +from opto.optimizers.optimizer import Optimizer from opto.trainer.utils import async_run from opto.trainer.algorithms.basic_algorithms import batchify -from opto.features.priority_search.search_template import SearchTemplate, Samples +from opto.features.priority_search.search_template import SearchTemplate, Samples, BatchRollout from opto.features.priority_search.utils import set_module_parameters, remap_update_dict, create_module_from_update_dict, is_module_copy @@ -15,8 +16,9 @@ class ModuleCandidate: """ A container used by PrioritySearch to store a candidate module as (its base module and update dictionary) and its statistics. """ def __init__(self, - base_module: Optional[trace.Module], + base_module: trace.Module, update_dict: Optional[Dict[ParameterNode, Any]] = None, + optimizer: Optimizer = None, ): """ A candidate module with its base module and update dictionary. Args: @@ -25,10 +27,14 @@ def __init__(self, stats (dict): A dictionary of statistics about the candidate. """ assert isinstance(base_module, trace.Module), "base_module must be a trace.Module." + if update_dict is not None: + assert isinstance(optimizer, Optimizer), "optimizer must be an instance of Optimizer when update_dict is provided." + self.base_module = base_module self.update_dict = update_dict if update_dict is not None else {} + self.optimizer = optimizer # the optimizer used to generate the update_dict; can be None, which indicates the base_module is used. self.update_dict = remap_update_dict(self.base_module, self.update_dict) - self.rollouts = [] # list of dicts containing the rollout information (not RolloutsGraph, but a list of dicts) + self.rollouts = [] # list of dicts containing the rollout information (not BatchRollout, but a list of dicts) self.created_time = time.time() def get_module(self): @@ -60,7 +66,6 @@ def __eq__(self, other): assert isinstance(other, ModuleCandidate), "other must be an instance of ModuleCandidate." return (self.update_dict == other.update_dict) and is_module_copy(self.base_module, other.base_module) - # TODO better way? def __lt__(self, other): """ Compare two candidates based on their update_dict. """ assert isinstance(other, ModuleCandidate), "other must be an instance of ModuleCandidate." @@ -202,9 +207,9 @@ class PrioritySearch(SearchTemplate): In each iteration, 1. It proposes a best agent and a set of `num_candidates` exploration agents that have the highest scores in the priority queue. 2. The best agent is tested for performance if eval_frequency is met. - 3. A minibatch of `batch_size` samples are drawn from the training dataset, and the exploration agents are run on the samples. This creates a set of agent rollouts, where each rollout contains the agent module, input, info, target, score, and feedback. For each agent, rollouts of size `sub_batch_size` are grouped together as a connected subgraph (represented as the RolloutsGraph object). In total, this step creates `num_subgraphs = num_candidates * ceil(batch_size / sub_batch_size)` subgraphs. + 3. A minibatch of `batch_size` samples are drawn from the training dataset, and the exploration agents are run on the samples. This creates a set of agent rollouts, where each rollout contains the agent module, input, info, target, score, and feedback. For each agent, rollouts of size `sub_batch_size` are grouped together as a connected subgraph (represented as the BatchRollout object). In total, this step creates `num_subgraphs = num_candidates * ceil(batch_size / sub_batch_size)` subgraphs. 4. Optimizer is run on each subgraph to propose new parameters for the agents. `num_proposals` proposals are generated for each subgraph. This results in `num_subgraphs * num_proposals` total proposals. - 5. The proposed parameters are validated by running the agents on the validation dataset, which can be the current batch or a separate validation dataset when provided. When validate_proposals is set to True, the exploration candidates are also validated. + 5. The proposed parameters are validated by running the agents on the validation dataset, which can be the current batch or a separate validation dataset when provided. When validate_exploration_candidates is set to True, the exploration candidates are also validated. 6. The validation results are used to update the priority queue, which stores the candidates and their scores. The candidates are stored as ModuleCandidate objects, which contain the base module, update dictionary, and rollouts (i.e. raw statistics of the candidate). This algorithm template can be subclassed to implement specific search algorithms by overriding the `exploit`, `explore`, and `compute_exploration_priority` methods. @@ -242,7 +247,7 @@ def train(self, # Priority Search specific parameters num_candidates: int = 10, # number of candidates to propose for exploration num_proposals: int = 1, # number of proposals to generate per optimizer - validate_proposals: bool = True, # whether to validate the proposed parameters for exploration + validate_exploration_candidates: bool = True, # whether to validate the proposed parameters for exploration use_best_candidate_to_explore: bool = True, # whether to use the best candidate as part of the exploration candidates memory_size: Optional[int] = None, # size of the heap memory to store the candidates; if None, no limit is set score_function: str = 'mean', # function to compute the score for the candidates; 'mean' or 'ucb' @@ -271,7 +276,7 @@ def train(self, save_path (str, optional): The path to save the agent. Defaults to "checkpoints/agent.pkl". num_candidates (int, optional): The number of candidates to propose for exploration. Defaults to 10. num_proposals (int, optional): The number of proposals to generate per optimizer. Defaults to 1. - validate_proposals (bool, optional): Whether to validate the proposed parameters for exploration. Defaults to True. + validate_exploration_candidates (bool, optional): Whether to validate the proposed parameters for exploration. Defaults to True. use_best_candidate_to_explore (bool, optional): Whether to use the best candidate as part of the exploration candidates. Defaults to True. memory_size (int, optional): The size of the heap memory to store the candidates. If None, no limit is set. Defaults to None. score_function (str, optional): The function to compute the score for the candidates; 'mean' or 'ucb'. Defaults to 'mean'. @@ -283,7 +288,7 @@ def train(self, # Create agents and optimizers for search self.num_candidates = num_candidates # number of candidates to propose by each optimizer call self.num_proposals = num_proposals - self.validate_proposals = validate_proposals # whether to validate the proposed parameters + self.validate_exploration_candidates = validate_exploration_candidates # whether to validate the proposed parameters self.use_best_candidate_to_explore = use_best_candidate_to_explore self.score_function = score_function # function to compute the score for the candidates if score_range is None: @@ -294,6 +299,7 @@ def train(self, self.ucb_exploration_constant = ucb_exploration_constant self._exploration_candidates = None # This stores the latest candidates used for exploration + self._best_candidate = None # This stores the latest best candidate used for exploitation self.memory = HeapMemory(size=memory_size) # Initialize the heap memory with a size limit @@ -315,7 +321,6 @@ def train(self, save_path=save_path, **kwargs) - def update(self, samples: Union[Samples, None] = None, verbose: bool = False, @@ -332,7 +337,7 @@ def update(self, # 3. Update the priority queue with the validation results self.update_memory(validate_results, verbose=verbose, **kwargs) # samples are provided here in case candidates do not capture full information else: # The first iteration. - if len(self.memory) == 0: + if len(self.memory) < self.num_candidates: self.memory.push(self.max_score, ModuleCandidate(self.agent)) # Push the base agent as the first candidate (This gives the initialization of the priority queue) # 4. Explore and exploit the priority queue self._best_candidate, info_exploit = self.exploit(verbose=verbose, **kwargs) # get the best candidate (ModuleCandidate) from the priority queue @@ -348,12 +353,46 @@ def update(self, info_log.update(info_explore) # add the info from the explore step return self._best_candidate.update_dict, [c.get_module() for c in self._exploration_candidates], info_log - def propose(self, samples, verbose=False, **kwargs): + + ## Illustration of `propose`` + # Suppose we have 2 exploration candidates. + # exploration_candidates = [candidate(param1, optimizer_1), candidate(param2, optimizer_2)] + # and two subbatches are collected by sampler. + # + # In samples returned by sampler, we have data + # module(param1_copy1), subbatch_1 + # module(param1_copy2), subbatch_2 + # module(param2_copy1), subbatch_1 + # module(param2_copy2), subbatch_2 + # + # We first match the samples with the exploration candidates as + # candidate_batchrollouts_list = + # [ (candidate(param1, optimizer_1), subbatch_1), (candidate(param1, optimizer_1), subbatch_2), + # (candidate(param2, optimizer_2), subbatch_1), (candidate(param2, optimizer_2), subbatch_2) ] + # + # In backward, we create deepcopies of the optimizers for each subbatch, and run backward asynchronously. + # optimizer_1_copy_1(param1) <- feedback from subbatch_1 + # optimizer_1_copy_2(param1) <- feedback from subbatch_2 + # optimizer_2_copy_1(param2) <- feedback from subbatch_1 + # optimizer_2_copy_2(param2) <- feedback from subbatch_2 + # + # In step, we further create deepcopies of the optimizers for each proposal, and run step asynchronously. + # for n_proposals = 2, we have + # optimizer_1_copy_1_copy_1(param1) -> proposal_1 + # optimizer_1_copy_1_copy_2(param1) -> proposal_2 + # ... + # optimizer_2_copy_2_copy_1(param2) -> proposal_7 + # optimizer_2_copy_2_copy_2(param2) -> proposal_8 + # which form the new candidate list returned by `propose`. + # + def propose(self, + samples : Samples, + verbose : bool = False, + **kwargs): """ Analyzing samples and propose new parameters using self.optimizer. An independent optimizer is used for the minibatch generated by one agent and generates n_proposals proposals. Args: - samples (list): A list of samples from the previous iteration. If None, the agent's parameters are returned without updating. - n_proposals (int): Number of proposals to generate per optimizer. Defaults to 1. + samples (Samples): Samples collected by the exploration candidates. If None, the agent's parameters are returned without updating. verbose (bool, optional): Whether to print verbose output. Defaults to False. **kwargs: Additional keyword arguments that may be used by the implementation. @@ -362,13 +401,21 @@ def propose(self, samples, verbose=False, **kwargs): """ print("--- Proposing new parameters...") if verbose else None assert isinstance(samples, Samples), "samples must be an instance of Samples." - samples = samples.samples # list of RolloutsGraph objects + samples = samples.samples # list of BatchRollout objects n_proposals = self.num_proposals # number of proposals to generate per optimizer + # Associate each BatchRollout with self._exploration_candidates + matched_candidates_and_samples = self.match_candidates_and_samples(self._exploration_candidates, samples) + candidate_batchrollouts_list = [ (k,b) for k, v in matched_candidates_and_samples.items() for b in v] + assert len(samples) == len(candidate_batchrollouts_list), "All samples must be associated with exploration candidates." + n_subbatches = len(samples) # number of batch rollouts in the samples + + # need to copy optimizer for the n_subbatches def _backward(n): - optimizer = copy.deepcopy(self.optimizer) # create a copy of the optimizer to avoid modifying the original one - rollouts = samples[n] # RolloutsGraph - # Make sure all rollouts are based on the same module, so they can be viewed as a minibatch. + candidate, rollouts = candidate_batchrollouts_list[n] + optimizer = candidate.optimizer or self.optimizer + # Create a copy of the optimizer to avoid modifying the original one and to allow parallel execution + optimizer = copy.deepcopy(optimizer) optimizer.parameters = rollouts.module.parameters() # set the optimizer's parameters to the proposal's parameters targets = [r.target for r in rollouts] feedbacks = [r.feedback for r in rollouts] @@ -380,19 +427,26 @@ def _backward(n): optimizer.backward(target, feedback) # compute the gradients based on the targets and feedbacks return optimizer - n_subgraphs = len(samples) # number of subgraphs (agents) in the samples - args_list = [(n,) for n in range(n_subgraphs)] - optimizers = async_run([_backward]*n_subgraphs*n_proposals, # run the optimizer step for each agent in parallel - args_list=args_list, - max_workers=self.num_threads, # use the number of threads specified in the class - description=None) + args_list = [(n,) for n in range(n_subbatches)] + optimizers = async_run([_backward]*n_subbatches, # run the optimizer step for each agent in parallel + args_list=args_list, + max_workers=self.num_threads, # use the number of threads specified in the class + description=None) + assert len(optimizers) == n_subbatches, "Number of optimizers must match number of batch rollouts." + + # need to copy optimizer for the n_proposals + # NOTE when optimizer is deepcopied, its parameters are not copied. + optimizers = [copy.deepcopy(o) for o in optimizers ] * n_proposals # repeat args_list n_proposals times + assert len(optimizers) == n_subbatches * n_proposals, "Number of optimizers must match number of batch rollouts times number of proposals." # For each optimizer, containing the backward feedback, we call it n_proposals times to get the proposed parameters. - def _step(optimizer): + def _step(n): + optimizer = optimizers[n] update_dict = optimizer.step(verbose=verbose, num_threads=self.num_threads, bypassing=True, **kwargs) if not update_dict: # if the optimizer did not propose any updates return None # return None to indicate no updates were proposed # update_dict may only contain some of the parameters of the agent, we need to make sure it contains all the parameters + # since the current agent might have different parameters than the one used by the optimizer for param in optimizer.parameters: # for all parameters if param not in update_dict: # update_dict misses some parameters update_dict[param] = param.data # add the parameter to the update_dict @@ -400,19 +454,23 @@ def _step(optimizer): update_dict = remap_update_dict(self.agent, update_dict) # remap the update dict to the agent's parameters return update_dict # return the proposed parameters - args_list = [(o,) for o in optimizers ] * n_proposals # repeat args_list n_proposals times - assert len(args_list) == n_subgraphs * n_proposals, "args_list must have length n_subgraphs * n_proposals" - update_dicts = async_run([_step]*n_subgraphs*n_proposals, # run the optimizer step for each agent in parallel + args_list = [(n,) for n in range(n_subbatches*n_proposals)] + update_dicts = async_run([_step]*n_subbatches*n_proposals, # run the optimizer step for each agent in parallel args_list=args_list, max_workers=self.num_threads, # use the number of threads specified in the class - description=f"Calling optimizers: Generating {n_proposals} proposals for each of {n_subgraphs} sub batches",) + description=f"Calling optimizers: Generating {n_proposals} proposals for each of {n_subbatches} sub batches",) - # update_dicts is a list of dicts of length n_agents * n_proposals - # Create ModuleCandidate objects for each proposed update_dict - candidates = [ModuleCandidate(self.agent, update_dict) for update_dict in update_dicts if update_dict is not None] # filter out None updates + # update_dicts is a list of dicts of length n_subbatches * n_proposals + # Create ModuleCandidate objects for each proposed update_dict that is non-trivial + candidates = [ModuleCandidate(self.agent, update_dict, optimizer) + for update_dict, optimizer in zip(update_dicts, optimizers) if update_dict is not None] # filter out None updates return candidates - def validate(self, candidates, samples, verbose=False, **kwargs): + def validate(self, + candidates: List[ModuleCandidate], + samples: Samples, + verbose: bool = False, + **kwargs): """ Validate the proposed candidate parameters Args: candidates (list of ModuleCandidate): A list of ModuleCandidate objects representing the proposed parameters. @@ -423,65 +481,80 @@ def validate(self, candidates, samples, verbose=False, **kwargs): results (dict): A dictionary where the keys are ids of ModuleCandidate objects and the values are ModuleCandidate and lists of rollouts (list of dicts) containing the module, x, info, target, score, feedback. """ print("--- Validating candidates...") if verbose else None + assert isinstance(samples, Samples), "samples must be an instance of Samples." + exploration_candidates = self._exploration_candidates # exploration candidates from the previous iteration + assert self._exploration_candidates is not None, "exploration_candidates must be set before calling validate." - # Get the validation dataset from the samples. If no validation dataset is provided, use the current batch. - if self._validate_dataset is None: - # If no validation dataset is provided, use the current batch - validate_dataset = samples.get_batch() # get the batch of inputs and infos from the samples - self.validate_sampler.dataset = validate_dataset # set the validation dataset in the sampler - self.validate_sampler.batch_size = len(validate_dataset['inputs']) # set the batch size to the number of inputs in the validation dataset + # The current batch of samples can be used to validate the exploration candidates + validate_samples = copy.copy(samples) + # Validate newly proposed candidates + use_prev_batch = self._validate_dataset is None # when True, self.validate_sampler == self.train_sampler, and the current batch is used for validation candidate_agents = [c.get_module() for c in candidates] # get the modules from the candidates - validate_samples = Samples(*self.validate_sampler.sample(candidate_agents, description_prefix='Validating newly proposed candidates: ')) # list of RolloutsGraph objects - + validate_samples.add_samples(Samples(*self.validate_sampler.sample(candidate_agents, + use_prev_batch=use_prev_batch, + description_prefix='Validating newly proposed candidates: '))) # list of BatchRollout objects - exploration_candidates = self._exploration_candidates # exploration candidates from the previous iteration - assert exploration_candidates is not None, "exploration_candidates must be set before calling validate." - if self.validate_proposals: - if self._validate_dataset is None: - # NOTE this might contain some duplicates due to sub_batch_size < batch_size - validate_samples.add_samples(samples) # if no validation dataset is provided, append the samples to the validate_samples - else: # validate the agents in the validate_dataset - # exploration_agents = [rollouts.module for rollouts in samples.samples] # NOTE this might contain some duplicates due to sub_batch_size < batch_size + if self.validate_exploration_candidates: + if self._validate_dataset is not None: # validate the exploration candidates that collected the samples as well + # validate the agents in the validate_dataset exploration_agents = [c.get_module() for c in exploration_candidates] # get the modules from the exploration candidates - exploration_samples = Samples(*self.validate_sampler.sample(exploration_agents, description_prefix='Validating exploration candidates: ')) # sample the exploration agents + exploration_samples = Samples(*self.validate_sampler.sample(exploration_agents, + description_prefix='Validating exploration candidates: ')) # sample the exploration agents validate_samples.add_samples(exploration_samples) # append the exploration samples to the validate_samples - # TODO some ModuleCandidate are the same in parameters though they have different ids + matched_candidates_and_samples = self.match_candidates_and_samples(exploration_candidates + candidates, validate_samples.samples) + results = {} # dict of ModuleCandidate id: (ModuleCandidate, list of rollouts) + for c, rollouts in matched_candidates_and_samples.items(): # rollouts is a list of BatchRollouts + results[c] = [ r for rr in rollouts for r in rr.to_list()] # we only need the list of dicts + + # Some ModuleCandidate have the same parameters though they have different ids. We need to merge their rollouts + for c1 in list(results.keys()): + for c2 in list(results.keys()): + if id(c1) != id(c2) and c1 == c2: # same parameters, different candidates + results[c1].extend(results[c2]) # merge the rollouts + del results[c2] # remove c2 from results + + return results + + def match_candidates_and_samples( + self, + candidates: List[ModuleCandidate], + samples: List[BatchRollout]): + """ + Match the given candidates with the provided samples. + + Args: + candidates (list of ModuleCandidate): A list of ModuleCandidate objects representing the proposed parameters. + samples (list of BatchRollout): A Samples object containing a list of BatchRollout objects, where each BatchRollout contains rollouts collected by an agent on different inputs. + Returns: + results (dict): A dictionary where the keys are ModuleCandidate objects and the values are lists of BatchRollouts collected by the corresponding ModuleCandidate. + + """ + # In general, there may be multiple BatchRollouts collected by the same ModuleCandidate. + # We group the rollouts by the agent (ModuleCandidate) and return a dictionary + # where the keys are the ModuleCandidate objects and the values are Samples - # In validate_samples, there may be multiple rollouts collected by the same agent (or their copies). - # We need to group the rollouts by the agent (ModuleCandidate) and return a dictionary where the keys are the ModuleCandidate objects and the values are lists of rollouts (list of dicts). # Group the samples by the ModuleCandidate id - _results = {} # dict of ModuleCandidate: list of rollouts (list of dicts) - for c in exploration_candidates + candidates: - _results[id(c)] = [] + _results = { c: [] for c in candidates} # dict of ModuleCandidate: list of BatchRollouts + ids = {id(c): c for c in candidates} # dict of ModuleCandidate id: ModuleCandidate - for rollouts in validate_samples.samples: + for rollouts in samples: + assert isinstance(rollouts, BatchRollout), "Each element in samples must be a BatchRollout object." + # rollouts is a BatchRollout object module = rollouts.module # trace.Module key = getattr(module, '__TRACE_RESERVED_module_candidate_id') # use the candidate as the key - if key not in _results: + if key not in ids: raise ValueError(f"ModuleCandidate with id {key} not found in results. Samples are not collected by known candidates.") # Append the rollouts to the list of rollouts for the key - _results[key].extend(rollouts.to_list()) + _results[ids[key]].append(rollouts) + # assert all candidates have at least one rollout + for c in candidates: - # Merge rollouts of ModuleCandidates sharing the same parameters - results = {} # dict of ModuleCandidate id: (ModuleCandidate, list of rollouts) - for c in exploration_candidates + candidates: - rollouts_list = _results[id(c)] - matched = False - for k in results.keys(): - if k == c: - matched = True - if id(k) != id(c): # merging rollouts of candidates with the same parameters - rollouts_list += c.rollouts - results[k].extend(rollouts_list) # add the rollouts to the candidate - break - if not matched: # key not found in results - results[c] = rollouts_list # add the rollouts to the candidate - # NOTE what if propose creates multiple exploration_candidates that have the same parameters and the same rollouts stats? - # For example, it copies candidates. This would create a bug. - return results + assert len(_results[c]) > 0, f"ModuleCandidate with id {id(c)} has no rollouts. Samples are not collected by known candidates." + + return _results def update_memory(self, validate_results, verbose: bool = False, **kwargs): """ Update the priority queue with the validation results. @@ -495,7 +568,6 @@ def update_memory(self, validate_results, verbose: bool = False, **kwargs): priority = self.compute_exploration_priority(candidate) # compute the priority for the candidate self.memory.push(priority, candidate) - #### def explore(self, verbose: bool = False, **kwargs): """ Explore the parameter space and propose new candidates. Args: @@ -512,14 +584,14 @@ def explore(self, verbose: bool = False, **kwargs): while len(top_candidates) < self.num_candidates and self.memory: neg_priority, candidate = self.memory.pop() # pop the top candidate from the priority queue priority = - neg_priority # remember that we stored negative scores in the priority queue - priorities.append(priority) # store the priority of the candidate if self.use_best_candidate_to_explore: if candidate == self._best_candidate: # skip if it is already in the top candidates continue + priorities.append(priority) # store the priority of the candidate top_candidates.append(candidate) # add the candidate to the top candidates mean_scores = [c.mean_score() for c in top_candidates] - mean_scores = [ s for s in mean_scores if s is not None] # filter out None scores + mean_scores = [s for s in mean_scores if s is not None] # filter out None scores info_dict = { 'num_exploration_candidates': len(top_candidates), 'exploration_candidates_mean_priority': np.mean(priorities), # list of priorities of the exploration candidates @@ -528,7 +600,6 @@ def explore(self, verbose: bool = False, **kwargs): return top_candidates, info_dict - def exploit(self, verbose: bool = False, **kwargs) -> Tuple[ModuleCandidate, Dict[str, Any]]: """ Exploit the best candidate from the priority queue. This method should not change the priority queue. Args: diff --git a/opto/features/priority_search/sampler.py b/opto/features/priority_search/sampler.py index ce35f736..ec77f7ab 100644 --- a/opto/features/priority_search/sampler.py +++ b/opto/features/priority_search/sampler.py @@ -30,8 +30,9 @@ def to_dict(self): } -class RolloutsGraph: +class BatchRollout: """ A rollouts graph is a collection of rollouts generated by the same agent (trace.Module) on different inputs. + Therefore, the graphs of all rollouts are connected via the agent parameters. """ module: trace.Module # the trace.Module (proposal) that generated the rollouts rollouts: List[Rollout] # a list of Rollout objects generated by the module on different inputs @@ -57,8 +58,8 @@ def __iter__(self): def extend(self, other): """ Extend the subgraph with another subgraph. """ - if not isinstance(other, RolloutsGraph): - raise ValueError("Can only extend with another RolloutsGraph.") + if not isinstance(other, BatchRollout): + raise ValueError("Can only extend with another BatchRollout.") if self.module != other.module: raise ValueError("Cannot extend with a subgraph with a different module.") self.rollouts.extend(other.rollouts) @@ -67,6 +68,15 @@ def to_list(self): """ Convert the subgraph to a list of rollouts. """ return [r.to_dict() for r in self.rollouts] + def get_batch(self): + """ Get the batch of inputs, infos that created the rollouts. """ + xs = [r.x for r in self.rollouts] + infos = [r.info for r in self.rollouts] + return { + "inputs": xs, + "infos": infos, + } + @dataclass class RolloutConfig: @@ -123,7 +133,7 @@ def standard_forward(agent, x, guide, info, min_score=0): return target, score, feedback -def sample_rollouts(configs, num_threads=1, forward=None, min_score=None, description="Sampling rollouts.") -> List[RolloutsGraph]: +def sample_rollouts(configs, num_threads=1, forward=None, min_score=None, description="Sampling rollouts.") -> List[BatchRollout]: """ Sample a batch of data based on the proposed parameters. All proposals are evaluated on the same batch of inputs. Args: @@ -138,7 +148,7 @@ def sample_rollouts(configs, num_threads=1, forward=None, min_score=None, descri min_score (float, optional): Minimum score to return when an exception occurs. If None, it defaults to 0. description (str): Description to display in the progress bar. Returns: - List[RolloutsGraph]: A list of RolloutsGraph objects, one for each config + List[BatchRollout]: A list of BatchRollout objects, one for each config """ if forward is None: forward = standard_forward @@ -157,8 +167,8 @@ def sample_rollouts(configs, num_threads=1, forward=None, min_score=None, descri guide=guides, # guide will be broadcasted inside min_score=min_score) - # Collect the results into a list of RolloutsGraph objects - results = [] # list of subgraphs (RolloutsGraph objects) for each agent + # Collect the results into a list of BatchRollout objects + results = [] # list of subgraphs (BatchRollout objects) for each agent _index = 0 # to track the indices processed for i in range(len(configs)): rollouts = [] @@ -175,7 +185,7 @@ def sample_rollouts(configs, num_threads=1, forward=None, min_score=None, descri ) _index += 1 # increment the index rollouts.append(rollout) - results.append(RolloutsGraph(rollouts)) # append the subgraph to the results + results.append(BatchRollout(rollouts)) # append the subgraph to the results return results @@ -205,6 +215,7 @@ def __init__(self, loader, guide, num_threads=1, sub_batch_size=None, forward=No self.score_range = score_range if forward is None: self.forward = standard_forward + self._prev_batch = None # to store the previous batch @property def dataset(self): @@ -234,21 +245,17 @@ def n_epochs(self): """ Get the number of epochs of the loader. """ return self.loader.n_epochs - def sample(self, agents, description_prefix=''): + def sample(self, agents, use_prev_batch=False, description_prefix=''): """ Sample a batch of data from the loader and evaluate the agents. Args: agents (list): A list of trace.Modules (proposed parameters) to evaluate. + use_prev_batch (bool): Whether to use the previous batch instead of sampling a new one. Returns: - batch (dict): - A dictionary containing the sampled inputs and infos, where: - - 'inputs': a list of inputs sampled from the loader - - 'infos': a list of additional information for each input - - samples (list of RolloutsGraph): - A list of RolloutsGraph objects, each containing the rollouts generated by the agents on the sampled inputs. - Each RolloutsGraph contains: + samples (list of BatchRollout): + A list of BatchRollout objects, each containing the rollouts generated by the agents on the sampled inputs. + Each BatchRollout contains: - 'module': the trace.Module (proposal) - 'rollouts': a list of Rollout objects containing: - 'x': the input data @@ -257,17 +264,27 @@ def sample(self, agents, description_prefix=''): - 'score': the score of the proposal - 'feedback': the feedback from the guide + batch (dict): + A dictionary containing the sampled inputs and infos, where: + - 'inputs': a list of inputs sampled from the loader + - 'infos': a list of additional information for each input + NOTE: The return might not be ordered in the same way as the agents. """ assert all(isinstance(a, trace.Module) for a in agents), "All agents must be trace.Modules." - # Get a batch of inputs and infos from the loader - xs, infos = self.loader.sample() - batch = { - 'inputs': xs, - 'infos': infos - } + if use_prev_batch and self._prev_batch is not None: + batch = self._prev_batch + xs, infos = batch['inputs'], batch['infos'] + else: + # Get a batch of inputs and infos from the loader + xs, infos = self.loader.sample() + batch = { + 'inputs': xs, + 'infos': infos + } + self._prev_batch = batch # store the batch for potential reuse # Evaluate each agent on the sampled inputs # diff --git a/opto/features/priority_search/search_template.py b/opto/features/priority_search/search_template.py index 2bbdc652..2ebb38cb 100644 --- a/opto/features/priority_search/search_template.py +++ b/opto/features/priority_search/search_template.py @@ -3,44 +3,47 @@ from opto import trace from opto.trainer.algorithms.basic_algorithms import Trainer from opto.trainer.loader import DataLoader -from opto.features.priority_search.sampler import Sampler, RolloutsGraph +from opto.features.priority_search.sampler import Sampler, BatchRollout from opto.trainer.evaluators import evaluate # TODO update evaluate implementation +from dataclasses import dataclass # TODO save and load SearchTemplate # TODO async version??? # TODO create SYNC and ASYNC versions of the base class; add an attribute to the class to indicate - +@dataclass class Samples: - """ A container for samples collected during the search algorithm. It contains a list of RolloutsGraph objects - and a dataset with inputs and infos which created the list of RolloutsGraph. """ + """ A container for samples collected during the search algorithm. It contains a list of BatchRollout objects + and a dataset with inputs and infos which created the list of BatchRollout. """ - samples: List[RolloutsGraph] - dataset: Dict[str, List[Any]] # contains 'inputs' and 'infos' keys + samples: List[BatchRollout] + dataset: Dict[str, List[Any]] # contains 'inputs' and 'infos' keys # TODO do we need this? - def __init__(self, samples: List[RolloutsGraph], dataset: Dict[str, List[Any]]): - assert isinstance(samples, list), "samples must be a list of RolloutsGraph objects." - assert all(isinstance(s, RolloutsGraph) for s in samples), "All samples must be RolloutsGraph objects." + def __init__(self, samples: List[BatchRollout], dataset: Dict[str, List[Any]]): + assert isinstance(samples, list), "samples must be a list of BatchRollout objects." + assert all(isinstance(s, BatchRollout) for s in samples), "All samples must be BatchRollout objects." assert isinstance(dataset, dict), "dataset must be a dict." assert 'inputs' in dataset and 'infos' in dataset, "dataset must contain 'inputs' and 'infos' keys." self.samples = samples - self.dataset = dataset # NOTE this cannot be extracted from the samples in general? + + # TODO drop this + self._dataset = dataset # NOTE this cannot be extracted from the samples in general? def add_samples(self, samples): """ Add samples to the Samples object. """ assert isinstance(samples, Samples), "samples must be an instance of Samples." samples = samples.samples # extract the samples from the Samples object - assert isinstance(samples, list), "samples must be a list of RolloutsGraph objects." - assert all(isinstance(s, RolloutsGraph) for s in samples), "All samples must be RolloutsGraph objects." + assert isinstance(samples, list), "samples must be a list of BatchRollout objects." + assert all(isinstance(s, BatchRollout) for s in samples), "All samples must be BatchRollout objects." # TODO assert xs and infos are in self.minibatch # add a function to extract unique inputs and infos from the samples self.samples.extend(samples) - def get_batch(self): - return self.dataset #['inputs'], self.minibatch['infos'] + # def get_batch(self): + # return self.dataset def __iter__(self): """ Iterate over the samples. """ @@ -50,7 +53,7 @@ def __len__(self): return sum(len(s) for s in self.samples) @property - def n_sub_batches(self) -> int: + def n_batchrollouts(self) -> int: """ Number of sub-batches in the samples. """ return len(self.samples) @@ -119,13 +122,16 @@ def train(self, score_range=self._score_range ) self._validate_dataset = validate_dataset # if None, the current batch will be used for validation - self.validate_sampler = Sampler( - DataLoader(validate_dataset if validate_dataset else {'inputs':[],'infos':[]}, batch_size=batch_size), - validate_guide or guide, - num_threads=self.num_threads, - sub_batch_size=None, # no sub-batch size for validation - score_range=self._score_range - ) + if validate_dataset is not None: + self.validate_sampler = Sampler( + DataLoader(validate_dataset, batch_size=batch_size), + validate_guide or guide, + num_threads=self.num_threads, + sub_batch_size=None, # no sub-batch size for validation + score_range=self._score_range + ) + else: + self.validate_sampler = self.train_sampler # use the train_sampler for validation if no validation dataset is provided # Evaluate the agent before learning # NOTE set test_frequency < 0 to skip first evaluation @@ -204,7 +210,7 @@ def sample(self, agents, verbose=False, **kwargs): """ samples = Samples(*self.train_sampler.sample(agents, description_prefix='Sampling training minibatch: ')) # create a Samples object to store the samples and the minibatch # Log information about the sampling - scores = [ g.get_scores() for g in samples.samples] # list of list of scores for each RolloutsGraph + scores = [ g.get_scores() for g in samples.samples] # list of list of scores for each BatchRollout scores = [item for sublist in scores for item in sublist] # flatten the list of scores log_info = { 'mean_score': np.mean(scores), diff --git a/opto/features/priority_search/utils.py b/opto/features/priority_search/utils.py index df9fbf69..59749e72 100644 --- a/opto/features/priority_search/utils.py +++ b/opto/features/priority_search/utils.py @@ -9,7 +9,7 @@ from opto.optimizers.utils import print_color from opto.trainer.algorithms.basic_algorithms import Minibatch, Trainer, batchify from opto.trainer.loader import DataLoader -from opto.features.priority_search.sampler import Sampler, RolloutsGraph +from opto.features.priority_search.sampler import Sampler, BatchRollout import time # Some helper functions to convert between trace.Module and update_dict diff --git a/opto/optimizers/optimizer.py b/opto/optimizers/optimizer.py index 2b175d5f..c958a9ec 100644 --- a/opto/optimizers/optimizer.py +++ b/opto/optimizers/optimizer.py @@ -1,5 +1,5 @@ from typing import Any, List, Dict - +import copy from opto.trace.nodes import ParameterNode, Node from opto.trace.propagators import GraphPropagator from opto.trace.propagators.propagators import Propagator @@ -101,4 +101,16 @@ def save(self, path: str): def load(self, path: str): """Load the optimizer state from a file.""" - pass \ No newline at end of file + pass + + def __deepcopy__(self, memo): + # deepcopy everything except self.parameters + cls = self.__class__ + result = cls.__new__(cls) + memo[id(self)] = result + for k, v in self.__dict__.items(): + if k != 'parameters': + setattr(result, k, copy.deepcopy(v, memo)) + else: + setattr(result, k, v) # parameters is not copied, it is the original parameters + return result \ No newline at end of file diff --git a/tests/unit_tests/test_priority_search.py b/tests/unit_tests/test_priority_search.py index 2ebda047..ef5a3e61 100644 --- a/tests/unit_tests/test_priority_search.py +++ b/tests/unit_tests/test_priority_search.py @@ -69,7 +69,7 @@ def propose(self, samples, verbose=False, n_proposals=1, **kwargs): # In this example this will always be value 5 assert isinstance(candidates, list), "Expected candidates to be a list" assert all(isinstance(c, ModuleCandidate) for c in candidates), "All candidates should be ModuleCandidate instances" - assert len(candidates) == samples.n_sub_batches * self.num_proposals, f"Expected {samples.n_sub_batches * self.num_proposals} candidates, got {len(candidates)}" + assert len(candidates) == samples.n_batchrollouts * self.num_proposals, f"Expected {samples.n_batchrollouts * self.num_proposals} candidates, got {len(candidates)}" return candidates def validate(self, candidates, samples, verbose=False, **kwargs): From 1f74a455e0b20b231aa41dbf67d62511050db011 Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 8 Sep 2025 17:28:46 +0000 Subject: [PATCH 181/314] Add support of multiple optimizers --- .../priority_search/priority_search.py | 10 +++--- .../priority_search/search_template.py | 33 ++++++++++++++++--- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index 77065afe..cbf303bb 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -195,9 +195,11 @@ def best(self, criterion=None): return self.memory[0] # return the item with the highest priority (lowest negative score) else: assert callable(criterion), "criterion must be a callable function." - return max(self.memory, key=lambda x: criterion(x[1])) - - + def _criterion(x): + neg_score, candidate = x + p = criterion(candidate) + return p if p is not None else 0 + return max(self.memory, key=lambda x: _criterion(x)) # TODO check saving and loading class PrioritySearch(SearchTemplate): @@ -337,7 +339,7 @@ def update(self, # 3. Update the priority queue with the validation results self.update_memory(validate_results, verbose=verbose, **kwargs) # samples are provided here in case candidates do not capture full information else: # The first iteration. - if len(self.memory) < self.num_candidates: + while len(self.memory) < self.num_candidates: self.memory.push(self.max_score, ModuleCandidate(self.agent)) # Push the base agent as the first candidate (This gives the initialization of the priority queue) # 4. Explore and exploit the priority queue self._best_candidate, info_exploit = self.exploit(verbose=verbose, **kwargs) # get the best candidate (ModuleCandidate) from the priority queue diff --git a/opto/features/priority_search/search_template.py b/opto/features/priority_search/search_template.py index 2ebb38cb..710b68aa 100644 --- a/opto/features/priority_search/search_template.py +++ b/opto/features/priority_search/search_template.py @@ -1,6 +1,8 @@ import numpy as np from typing import Union, List, Tuple, Dict, Any, Optional from opto import trace +from opto.optimizers.optimizer import Optimizer +from opto.trainer.loggers import BaseLogger from opto.trainer.algorithms.basic_algorithms import Trainer from opto.trainer.loader import DataLoader from opto.features.priority_search.sampler import Sampler, BatchRollout @@ -58,22 +60,45 @@ def n_batchrollouts(self) -> int: return len(self.samples) +def check_optimizer_parameters(optimizer: Optimizer, agent: trace.Module): + """ Check if the optimizer's parameters are the same as the agent's parameters. """ + assert isinstance(optimizer, Optimizer), "optimizer must be an instance of Optimizer." + agent_params = set(agent.parameters()) + optimizer_params = set(optimizer.parameters) + assert agent_params == optimizer_params, "Optimizer parameters do not match agent parameters." + class SearchTemplate(Trainer): # This only uses __init__ and evaluate of Minibatch class. """ This implements a generic template for search algorithm. """ def __init__(self, - agent, - optimizer, + agent: trace.Module, + optimizer : Union[Optimizer, List[Optimizer]], num_threads: int = None, # maximum number of threads to use for parallel execution - logger=None, + logger: Union[BaseLogger, None] =None, *args, **kwargs, ): super().__init__(agent, num_threads=num_threads, logger=logger, *args, **kwargs) - self.optimizer = optimizer + + # TODO assert agent parameters are the same as optimizer.parameters + if isinstance(optimizer, list): + assert len(optimizer) > 0, "Optimizers list is empty." + for opt in optimizer: + check_optimizer_parameters(opt, agent) + self._optimizers = optimizer + else: + check_optimizer_parameters(optimizer, agent) + self._optimizers = [optimizer] + self.n_iters = 0 # number of iterations + self._optimizer_index = -1 # index of the current optimizer to use + + @property + def optimizer(self): + self._optimizer_index += 1 + return self._optimizers[self._optimizer_index % len(self._optimizers)] # return the current optimizer def train(self, guide, # guide to provide feedback From 9404e435abe10929031afd82167128405f3c199b Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 8 Sep 2025 19:09:00 +0000 Subject: [PATCH 182/314] Fix some bugs of using multi optimizers due hasing def of ModuleCandidate --- .../priority_search/priority_search.py | 32 ++++++++----------- opto/optimizers/optoprime_v2.py | 5 ++- opto/trainer/train.py | 19 ++++++++--- 3 files changed, 33 insertions(+), 23 deletions(-) diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index cbf303bb..d53639cd 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -64,7 +64,7 @@ def __deepcopy__(self, memo): def __eq__(self, other): """ Check if two candidates are equal based on their base_module and update_dict. """ assert isinstance(other, ModuleCandidate), "other must be an instance of ModuleCandidate." - return (self.update_dict == other.update_dict) and is_module_copy(self.base_module, other.base_module) + return (self.update_dict == other.update_dict) and is_module_copy(self.base_module, other.base_module) and (id(self.optimizer) == id(other.optimizer)) def __lt__(self, other): """ Compare two candidates based on their update_dict. """ @@ -75,7 +75,7 @@ def __lt__(self, other): def __hash__(self): """ Hash the candidate based on its update_dict. """ - return hash(frozenset(self.update_dict.items())) + return hash((frozenset(self.update_dict.items()), id(self.optimizer), id(self.base_module))) def add_rollouts(self, rollouts: List[Dict[str, Any]]): """ Add rollouts to the candidate. """ @@ -288,8 +288,12 @@ def train(self, # Create agents and optimizers for search - self.num_candidates = num_candidates # number of candidates to propose by each optimizer call - self.num_proposals = num_proposals + if num_candidates < len(self._optimizers): + print(f"Warning: num_candidates {num_candidates} is less than the number of optimizers {len(self._optimizers)}. Setting num_candidates to {len(self._optimizers)}.") + num_candidates = len(self._optimizers) + self.num_candidates = num_candidates # number of candidates for exploration + self.num_proposals = num_proposals # number of candidates to propose by each optimizer call + self.validate_exploration_candidates = validate_exploration_candidates # whether to validate the proposed parameters self.use_best_candidate_to_explore = use_best_candidate_to_explore self.score_function = score_function # function to compute the score for the candidates @@ -340,12 +344,12 @@ def update(self, self.update_memory(validate_results, verbose=verbose, **kwargs) # samples are provided here in case candidates do not capture full information else: # The first iteration. while len(self.memory) < self.num_candidates: - self.memory.push(self.max_score, ModuleCandidate(self.agent)) # Push the base agent as the first candidate (This gives the initialization of the priority queue) + self.memory.push(self.max_score, ModuleCandidate(self.agent, optimizer=self.optimizer)) # Push the base agent as the first candidate (This gives the initialization of the priority queue) # 4. Explore and exploit the priority queue self._best_candidate, info_exploit = self.exploit(verbose=verbose, **kwargs) # get the best candidate (ModuleCandidate) from the priority queue self._exploration_candidates, info_explore = self.explore(verbose=verbose, **kwargs) # List of ModuleCandidates - - + if samples is None: # first iteration + assert len(self.memory) == 0, "Memory should be empty in the first iteration." # TODO Log information about the update info_log = { 'n_iters': self.n_iters, # number of iterations @@ -408,6 +412,7 @@ def propose(self, # Associate each BatchRollout with self._exploration_candidates matched_candidates_and_samples = self.match_candidates_and_samples(self._exploration_candidates, samples) + # NOTE len(matched_candidates_and_samples) <= len(self._exploration_candidates) since some exploration candidates might be duplicated. candidate_batchrollouts_list = [ (k,b) for k, v in matched_candidates_and_samples.items() for b in v] assert len(samples) == len(candidate_batchrollouts_list), "All samples must be associated with exploration candidates." n_subbatches = len(samples) # number of batch rollouts in the samples @@ -435,7 +440,6 @@ def _backward(n): max_workers=self.num_threads, # use the number of threads specified in the class description=None) assert len(optimizers) == n_subbatches, "Number of optimizers must match number of batch rollouts." - # need to copy optimizer for the n_proposals # NOTE when optimizer is deepcopied, its parameters are not copied. optimizers = [copy.deepcopy(o) for o in optimizers ] * n_proposals # repeat args_list n_proposals times @@ -511,13 +515,6 @@ def validate(self, for c, rollouts in matched_candidates_and_samples.items(): # rollouts is a list of BatchRollouts results[c] = [ r for rr in rollouts for r in rr.to_list()] # we only need the list of dicts - # Some ModuleCandidate have the same parameters though they have different ids. We need to merge their rollouts - for c1 in list(results.keys()): - for c2 in list(results.keys()): - if id(c1) != id(c2) and c1 == c2: # same parameters, different candidates - results[c1].extend(results[c2]) # merge the rollouts - del results[c2] # remove c2 from results - return results def match_candidates_and_samples( @@ -553,7 +550,6 @@ def match_candidates_and_samples( _results[ids[key]].append(rollouts) # assert all candidates have at least one rollout for c in candidates: - assert len(_results[c]) > 0, f"ModuleCandidate with id {id(c)} has no rollouts. Samples are not collected by known candidates." return _results @@ -587,11 +583,11 @@ def explore(self, verbose: bool = False, **kwargs): neg_priority, candidate = self.memory.pop() # pop the top candidate from the priority queue priority = - neg_priority # remember that we stored negative scores in the priority queue if self.use_best_candidate_to_explore: - if candidate == self._best_candidate: # skip if it is already in the top candidates + if candidate is self._best_candidate: # skip if it is already in the top candidates continue priorities.append(priority) # store the priority of the candidate top_candidates.append(candidate) # add the candidate to the top candidates - + # NOTE some top_candidates can be duplicates mean_scores = [c.mean_score() for c in top_candidates] mean_scores = [s for s in mean_scores if s is not None] # filter out None scores info_dict = { diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index 936a086d..8fc4efc2 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -409,13 +409,16 @@ def __init__( max_tokens=4096, log=True, initial_var_char_limit=100, - optimizer_prompt_symbol_set: OptimizerPromptSymbolSet = OptimizerPromptSymbolSet(), + optimizer_prompt_symbol_set: OptimizerPromptSymbolSet = None, use_json_object_format=True, # whether to use json object format for the response when calling LLM truncate_expression=truncate_expression, **kwargs, ): super().__init__(parameters, *args, propagator=propagator, **kwargs) + if optimizer_prompt_symbol_set is None: + optimizer_prompt_symbol_set = OptimizerPromptSymbolSet() + self.truncate_expression = truncate_expression self.use_json_object_format = use_json_object_format if optimizer_prompt_symbol_set.expect_json and use_json_object_format else False diff --git a/opto/trainer/train.py b/opto/trainer/train.py index e3b79e05..c0af96f9 100644 --- a/opto/trainer/train.py +++ b/opto/trainer/train.py @@ -61,12 +61,18 @@ def forward(self, x): parameters = model.parameters() assert len(parameters) >0, "Model must have non-empty parameters." - optimizer = load_optimizer(optimizer, model, **optimizer_kwargs) + if isinstance(optimizer_kwargs, list): # support multiple optimizers + assert all(isinstance(d, dict) for d in optimizer_kwargs), "optimizer_kwargs must be a list of dictionaries." + optimizer = [load_optimizer(optimizer, model, **d) for d in optimizer_kwargs ] + assert all(isinstance(o, Optimizer) for o in optimizer) + else: + optimizer = load_optimizer(optimizer, model, **optimizer_kwargs) + assert isinstance(optimizer, Optimizer) + guide = load_guide(guide, **guide_kwargs) logger = load_logger(logger, **logger_kwargs) trainer_class = load_trainer_class(algorithm) - assert isinstance(optimizer, Optimizer) assert isinstance(guide, Guide) assert isinstance(logger, BaseLogger) assert issubclass(trainer_class, Trainer) @@ -122,8 +128,13 @@ def load_logger(logger: Union[BaseLogger, str], **kwargs) -> BaseLogger: def load_trainer_class(trainer: Union[Trainer, str]) -> Trainer: if isinstance(trainer, str): - trainers_module = importlib.import_module("opto.trainer.algorithms") - trainer_class = getattr(trainers_module, trainer) + if trainer.lower() == 'PrioritySearch'.lower(): + print('Warning: You are using PrioritySearch trainer, which is an experimental feature. Please report any issues you encounter.') + trainers_module = importlib.import_module("opto.features.priority_search") + trainer_class = getattr(trainers_module, trainer) + else: + trainers_module = importlib.import_module("opto.trainer.algorithms") + trainer_class = getattr(trainers_module, trainer) elif issubclass(trainer, Trainer): trainer_class = trainer else: From 7160ea78913212c3da9e93fa0f6e9f53b44d953a Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 8 Sep 2025 19:12:46 +0000 Subject: [PATCH 183/314] Add an example of using multi-optimizers --- .../train_single_node_multi_optimizers.py.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 examples/train_single_node_multi_optimizers.py.py diff --git a/examples/train_single_node_multi_optimizers.py.py b/examples/train_single_node_multi_optimizers.py.py new file mode 100644 index 00000000..6bbcb7b2 --- /dev/null +++ b/examples/train_single_node_multi_optimizers.py.py @@ -0,0 +1,32 @@ +from opto import trace, trainer +from opto.optimizers.optoprime_v2 import OptimizerPromptSymbolSet + +def main(): + true_number = 3 + train_dataset = dict(inputs=[None], infos=[f'Correct answer is: {true_number}']) + param = trace.node(0, description='An interger to guess', trainable=True) + + + # In this toy example, we run PrioritySearch with 2 optimizers to optimize the same parameter with different objectives. + symbols = OptimizerPromptSymbolSet() + base_objective = f"You need to change the `{symbols.value_tag}` of the variables in {symbols.variables_section_title} to improve the output in accordance to {symbols.feedback_section_title}" + optimizer_kwargs_list = [ + dict(objective=base_objective + ". The answer should be an integer between 0 and 5."), + dict(objective=base_objective + ". The answer should be an integer between -5 and 0"), + ] + + trainer.train( + algorithm='PrioritySearch', + model=param, + train_dataset=train_dataset, + # trainer kwargs + num_epochs=3, + batch_size=1, + verbose='output', + optimizer_kwargs=optimizer_kwargs_list, # use 2 optimizers + num_candidates=2, # keep exploring the top 2 candidates + ) + + +if __name__ == "__main__": + main() From f3fcdd0c78eeec77b1bf59882f9210caed655d82 Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 8 Sep 2025 21:43:36 +0000 Subject: [PATCH 184/314] Remove subbatch arg in PrioritySearch. Use num_batches instead. --- examples/priority_search_example.py | 4 +- examples/priority_search_on_convex_fn.py | 2 +- opto/features/priority_search/examples.py | 16 +++--- .../priority_search/priority_search.py | 52 +++++++++---------- opto/features/priority_search/sampler.py | 18 +++---- .../priority_search/search_template.py | 9 ++-- tests/unit_tests/test_priority_search.py | 6 +-- tests/unit_tests/test_sampler.py | 8 +-- tests/unit_tests/test_saving_loading.py | 1 - 9 files changed, 58 insertions(+), 58 deletions(-) diff --git a/examples/priority_search_example.py b/examples/priority_search_example.py index caf03cbc..fee40da6 100644 --- a/examples/priority_search_example.py +++ b/examples/priority_search_example.py @@ -55,7 +55,7 @@ def main(): seed = 42 num_epochs = 1 batch_size = 3 # number of queries to sample from the training data - sub_batch_size = 2 # number of queries each optimizer sees + num_batches = 2 # number of queries each optimizer sees num_proposals = 3 # number of proposals to generate for each query num_candidates = 2 # number of candidates for exploration score_range = (0, 1) # range of the score for the guide @@ -97,7 +97,7 @@ def main(): eval_frequency=eval_frequency, test_dataset=test_dataset, num_threads=num_threads, - sub_batch_size=sub_batch_size, + num_batches=num_batches, num_proposals=num_proposals, num_candidates=num_candidates, score_range=score_range, diff --git a/examples/priority_search_on_convex_fn.py b/examples/priority_search_on_convex_fn.py index 12bbd62c..13b4eeb7 100644 --- a/examples/priority_search_on_convex_fn.py +++ b/examples/priority_search_on_convex_fn.py @@ -250,7 +250,7 @@ def get_feedback(self, query: str, response: str, reference=None, **kwargs) -> T # trainer kwargs num_epochs=5, batch_size=2, # this is just for testing. effectively, this is the same batch_size=1 and num_proposals=4 - sub_batch_size=1, + num_batches=2, verbose='output', guide=guide, num_candidates=4, diff --git a/opto/features/priority_search/examples.py b/opto/features/priority_search/examples.py index fedd5e1b..53d4ee90 100644 --- a/opto/features/priority_search/examples.py +++ b/opto/features/priority_search/examples.py @@ -16,7 +16,7 @@ class SequentialUpdate(PrioritySearch): This is the same as MinibatchAlgorithm when 1. no validation set is provided - 2. sub_batch_size is None or batch_size. + 2. num_batches = 1 validate_proposals here acts the same as `ensure_improvement` flag in MinibatchAlgorithm """ @@ -30,7 +30,7 @@ def train(self, validate_guide = None, # to provide scores for the validation set # training loop batch_size = 1, # batch size for updating the agent - sub_batch_size = None, # sub-batch size that each optimizer attends to + num_batches = 1, # number of batches to use from the dataset in each iteration score_range = None, # minimum score to update the agent num_epochs = 1, # number of training epochs num_threads = None, # maximum number of threads to use @@ -62,7 +62,7 @@ def train(self, validate_dataset=validate_dataset, validate_guide=validate_guide, batch_size=batch_size, - sub_batch_size=sub_batch_size, + num_batches=num_batches, score_range=score_range, num_epochs=num_epochs, num_threads=num_threads, @@ -90,7 +90,7 @@ class SequentialSearch(PrioritySearch): This is the same as BasicSearchAlgorithm when 1. a validation set is provided 2. validate_proposals is True. - 3. sub_batch_size is None or batch_size. + 3. num_batches is 1. """ def train(self, @@ -102,7 +102,7 @@ def train(self, validate_guide = None, # to provide scores for the validation set # training loop batch_size = 1, # batch size for updating the agent - sub_batch_size = None, # sub-batch size that each optimizer attends to + num_batches = 1, # number of batches to use from the dataset in each iteration score_range = None, # minimum score to update the agent num_epochs = 1, # number of training epochs num_threads = None, # maximum number of threads to use @@ -133,7 +133,7 @@ def train(self, validate_dataset=validate_dataset, validate_guide=validate_guide, batch_size=batch_size, - sub_batch_size=sub_batch_size, + num_batches=num_batches, score_range=score_range, num_epochs=num_epochs, num_threads=num_threads, @@ -168,7 +168,7 @@ def train(self, validate_guide = None, # to provide scores for the validation set # training loop batch_size = 1, # batch size for updating the agent - sub_batch_size = None, # sub-batch size that each optimizer attends to + num_batches = 1, # number of batches to use from the dataset in each iteration score_range = None, # minimum score to update the agent num_epochs = 1, # number of training epochs num_threads = None, # maximum number of threads to use @@ -196,7 +196,7 @@ def train(self, validate_dataset=validate_dataset, validate_guide=validate_guide, batch_size=batch_size, - sub_batch_size=sub_batch_size, + num_batches=num_batches, score_range=score_range, num_epochs=num_epochs, num_threads=num_threads, diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index d53639cd..9a6f7534 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -209,7 +209,7 @@ class PrioritySearch(SearchTemplate): In each iteration, 1. It proposes a best agent and a set of `num_candidates` exploration agents that have the highest scores in the priority queue. 2. The best agent is tested for performance if eval_frequency is met. - 3. A minibatch of `batch_size` samples are drawn from the training dataset, and the exploration agents are run on the samples. This creates a set of agent rollouts, where each rollout contains the agent module, input, info, target, score, and feedback. For each agent, rollouts of size `sub_batch_size` are grouped together as a connected subgraph (represented as the BatchRollout object). In total, this step creates `num_subgraphs = num_candidates * ceil(batch_size / sub_batch_size)` subgraphs. + 3. `num_batches` minibatches of `batch_size` samples are drawn from the training dataset, and the exploration agents are run on the samples. This creates a set of agent rollouts, where each rollout contains the agent module, input, info, target, score, and feedback. For each agent, rollouts of each minibatch are grouped together as a connected subgraph (represented as the BatchRollout object). In total, this step creates `num_candidates * num_batches` subgraphs. 4. Optimizer is run on each subgraph to propose new parameters for the agents. `num_proposals` proposals are generated for each subgraph. This results in `num_subgraphs * num_proposals` total proposals. 5. The proposed parameters are validated by running the agents on the validation dataset, which can be the current batch or a separate validation dataset when provided. When validate_exploration_candidates is set to True, the exploration candidates are also validated. 6. The validation results are used to update the priority queue, which stores the candidates and their scores. The candidates are stored as ModuleCandidate objects, which contain the base module, update dictionary, and rollouts (i.e. raw statistics of the candidate). @@ -233,7 +233,7 @@ def train(self, validate_guide = None, # to provide scores for the validation set # training loop batch_size = 1, # batch size for updating the agent - sub_batch_size = None, # sub-batch size that each optimizer attends to + num_batches = 1, # number of batches to use from the dataset in each iteration score_range = None, # minimum score to update the agent num_epochs = 1, # number of training epochs num_threads = None, # maximum number of threads to use @@ -265,7 +265,7 @@ def train(self, validate_dataset (list, optional): A list of (x, info) pairs to validate the proposed candidates. If None, the current batch is used. Defaults to None. validate_guide (callable, optional): A function that provides feedback for the validation set. If None, the training guide is used. Defaults to None. batch_size (int, optional): The batch size for updating the agent. Defaults to 1. - sub_batch_size (int, optional): The sub-batch size that each optimizer attends to. If None, it is set to batch_size. Defaults to None. + num_batches (int, optional): The number of batches to use from the dataset in each iteration. Defaults to 1. score_range (tuple, optional): A tuple of (min_score, max_score) to clip the scores. If None, no clipping is applied. Defaults to None. num_epochs (int, optional): The number of training epochs. Defaults to 1. num_threads (int, optional): The maximum number of threads to use. If None, it uses the number of CPU cores. Defaults to None. @@ -314,7 +314,7 @@ def train(self, validate_dataset=validate_dataset, validate_guide=validate_guide, batch_size=batch_size, - sub_batch_size=sub_batch_size, + num_batches=num_batches, score_range=score_range, num_epochs=num_epochs, num_threads=num_threads, @@ -363,24 +363,24 @@ def update(self, ## Illustration of `propose`` # Suppose we have 2 exploration candidates. # exploration_candidates = [candidate(param1, optimizer_1), candidate(param2, optimizer_2)] - # and two subbatches are collected by sampler. + # and two batches are collected by sampler. # # In samples returned by sampler, we have data - # module(param1_copy1), subbatch_1 - # module(param1_copy2), subbatch_2 - # module(param2_copy1), subbatch_1 - # module(param2_copy2), subbatch_2 + # module(param1_copy1), batch_1 + # module(param1_copy2), batch_2 + # module(param2_copy1), batch_1 + # module(param2_copy2), batch_2 # # We first match the samples with the exploration candidates as # candidate_batchrollouts_list = - # [ (candidate(param1, optimizer_1), subbatch_1), (candidate(param1, optimizer_1), subbatch_2), - # (candidate(param2, optimizer_2), subbatch_1), (candidate(param2, optimizer_2), subbatch_2) ] + # [ (candidate(param1, optimizer_1), batch_1), (candidate(param1, optimizer_1), batch_2), + # (candidate(param2, optimizer_2), batch_1), (candidate(param2, optimizer_2), batch_2) ] # - # In backward, we create deepcopies of the optimizers for each subbatch, and run backward asynchronously. - # optimizer_1_copy_1(param1) <- feedback from subbatch_1 - # optimizer_1_copy_2(param1) <- feedback from subbatch_2 - # optimizer_2_copy_1(param2) <- feedback from subbatch_1 - # optimizer_2_copy_2(param2) <- feedback from subbatch_2 + # In backward, we create deepcopies of the optimizers for each batch, and run backward asynchronously. + # optimizer_1_copy_1(param1) <- feedback from batch_1 + # optimizer_1_copy_2(param1) <- feedback from batch_2 + # optimizer_2_copy_1(param2) <- feedback from batch_1 + # optimizer_2_copy_2(param2) <- feedback from batch_2 # # In step, we further create deepcopies of the optimizers for each proposal, and run step asynchronously. # for n_proposals = 2, we have @@ -415,9 +415,9 @@ def propose(self, # NOTE len(matched_candidates_and_samples) <= len(self._exploration_candidates) since some exploration candidates might be duplicated. candidate_batchrollouts_list = [ (k,b) for k, v in matched_candidates_and_samples.items() for b in v] assert len(samples) == len(candidate_batchrollouts_list), "All samples must be associated with exploration candidates." - n_subbatches = len(samples) # number of batch rollouts in the samples + n_batches = len(samples) # number of batch rollouts in the samples - # need to copy optimizer for the n_subbatches + # need to copy optimizer for the n_batches def _backward(n): candidate, rollouts = candidate_batchrollouts_list[n] optimizer = candidate.optimizer or self.optimizer @@ -434,16 +434,16 @@ def _backward(n): optimizer.backward(target, feedback) # compute the gradients based on the targets and feedbacks return optimizer - args_list = [(n,) for n in range(n_subbatches)] - optimizers = async_run([_backward]*n_subbatches, # run the optimizer step for each agent in parallel + args_list = [(n,) for n in range(n_batches)] + optimizers = async_run([_backward]*n_batches, # run the optimizer step for each agent in parallel args_list=args_list, max_workers=self.num_threads, # use the number of threads specified in the class description=None) - assert len(optimizers) == n_subbatches, "Number of optimizers must match number of batch rollouts." + assert len(optimizers) == n_batches, "Number of optimizers must match number of batch rollouts." # need to copy optimizer for the n_proposals # NOTE when optimizer is deepcopied, its parameters are not copied. optimizers = [copy.deepcopy(o) for o in optimizers ] * n_proposals # repeat args_list n_proposals times - assert len(optimizers) == n_subbatches * n_proposals, "Number of optimizers must match number of batch rollouts times number of proposals." + assert len(optimizers) == n_batches * n_proposals, "Number of optimizers must match number of batch rollouts times number of proposals." # For each optimizer, containing the backward feedback, we call it n_proposals times to get the proposed parameters. def _step(n): @@ -460,13 +460,13 @@ def _step(n): update_dict = remap_update_dict(self.agent, update_dict) # remap the update dict to the agent's parameters return update_dict # return the proposed parameters - args_list = [(n,) for n in range(n_subbatches*n_proposals)] - update_dicts = async_run([_step]*n_subbatches*n_proposals, # run the optimizer step for each agent in parallel + args_list = [(n,) for n in range(n_batches*n_proposals)] + update_dicts = async_run([_step]*n_batches*n_proposals, # run the optimizer step for each agent in parallel args_list=args_list, max_workers=self.num_threads, # use the number of threads specified in the class - description=f"Calling optimizers: Generating {n_proposals} proposals for each of {n_subbatches} sub batches",) + description=f"Calling optimizers: Generating {n_proposals} proposals for each of {n_batches} batches",) - # update_dicts is a list of dicts of length n_subbatches * n_proposals + # update_dicts is a list of dicts of length n_batches * n_proposals # Create ModuleCandidate objects for each proposed update_dict that is non-trivial candidates = [ModuleCandidate(self.agent, update_dict, optimizer) for update_dict, optimizer in zip(update_dicts, optimizers) if update_dict is not None] # filter out None updates diff --git a/opto/features/priority_search/sampler.py b/opto/features/priority_search/sampler.py index ec77f7ab..bc98ae43 100644 --- a/opto/features/priority_search/sampler.py +++ b/opto/features/priority_search/sampler.py @@ -194,24 +194,24 @@ class Sampler: """ A sampler that samples a batch of data from the loader and evaluates the agents on the sampled inputs. """ - def __init__(self, loader, guide, num_threads=1, sub_batch_size=None, forward=None, score_range=(-np.inf, np.inf)): + def __init__(self, loader, guide, num_threads=1, subbatch_size=None, forward=None, score_range=(-np.inf, np.inf)): """ Initialize the sampler with a data loader and a guide. Args: loader (DataLoader): The data loader to sample from. guide (Guide): The guide to evaluate the proposals. num_threads (int): Number of threads to use for sampling. - sub_batch_size (int, optional): Size of the sub-batch to use for sampling. If None, uses the batch size. + subbatch_size (int, optional): Size of the sub-batch to use for sampling. If None, uses the batch size. score_range (tuple): The range of scores to consider valid. """ self.loader = loader self.guide = guide self.num_threads = num_threads - if sub_batch_size is None: - sub_batch_size = loader.batch_size + if subbatch_size is None: + subbatch_size = loader.batch_size else: - assert sub_batch_size <= loader.batch_size, "sub_batch_size must be less than or equal to the loader's batch size." - self.sub_batch_size = sub_batch_size + assert subbatch_size <= loader.batch_size, "subbatch_size must be less than or equal to the loader's batch size." + self.subbatch_size = subbatch_size self.score_range = score_range if forward is None: self.forward = standard_forward @@ -291,7 +291,7 @@ def sample(self, agents, use_prev_batch=False, description_prefix=''): # agents : a1, a2 # inputs: x1, x2, x3 # infos: i1, i2, i3 - # sub_batch_size: 2 + # subbatch_size: 2 # # The forward is called in this order: # (a1, x1, i1, guide1), @@ -308,7 +308,7 @@ def sample(self, agents, use_prev_batch=False, description_prefix=''): for agent in agents: _xs, _infos = [], [] for i in range(batch_size): - if i % self.sub_batch_size == 0 and i > 0: + if i % self.subbatch_size == 0 and i > 0: configs.append(RolloutConfig(module=agent, xs=_xs, infos=_infos, guide=self.guide)) # reset agent = copy.deepcopy(agent) # create a deep copy of the agent for the next sub-batch @@ -326,6 +326,6 @@ def sample(self, agents, use_prev_batch=False, description_prefix=''): min_score=self.score_range[0], description=description) - assert len(samples) == len(agents)*(batch_size // self.sub_batch_size + (1 if batch_size % self.sub_batch_size > 0 else 0)), f"Expected {len(agents)*(batch_size // self.sub_batch_size + (1 if batch_size % self.sub_batch_size > 0 else 0))} samples, got {len(samples)}" + assert len(samples) == len(agents)*(batch_size // self.subbatch_size + (1 if batch_size % self.subbatch_size > 0 else 0)), f"Expected {len(agents)*(batch_size // self.subbatch_size + (1 if batch_size % self.subbatch_size > 0 else 0))} samples, got {len(samples)}" return samples, batch diff --git a/opto/features/priority_search/search_template.py b/opto/features/priority_search/search_template.py index 710b68aa..d997202e 100644 --- a/opto/features/priority_search/search_template.py +++ b/opto/features/priority_search/search_template.py @@ -109,7 +109,7 @@ def train(self, validate_guide = None, # to provide scores for the validation set # training loop batch_size = 1, # batch size for updating the agent - sub_batch_size = None, # sub-batch size for broadcasting the agents + num_batches = 1, # number of batches to use from the dataset in each iteration score_range = None, # minimum score to update the agent num_epochs = 1, # number of training epochs num_threads = None, # maximum number of threads to use @@ -125,6 +125,7 @@ def train(self, save_path: str = "checkpoints/agent.pkl", # path to save the agent **kwargs ): + assert 'subbatch_size' not in kwargs, "subbatch_size should not be provided in kwargs." ## Setup test_frequency = eval_frequency # use eval_frequency as test_frequency # NOTE legacy notation @@ -139,11 +140,13 @@ def train(self, assert score_range[1] >= score_range[0], "score_range must be a tuple (min_score, max_score) with min_score <= max_score." self._score_range = score_range # range of the score for the guide + subbatch_size, batch_size = batch_size, batch_size*num_batches + self.train_sampler = Sampler( DataLoader(train_dataset, batch_size=batch_size), guide, num_threads=self.num_threads, - sub_batch_size=sub_batch_size, + subbatch_size=subbatch_size, score_range=self._score_range ) self._validate_dataset = validate_dataset # if None, the current batch will be used for validation @@ -152,7 +155,7 @@ def train(self, DataLoader(validate_dataset, batch_size=batch_size), validate_guide or guide, num_threads=self.num_threads, - sub_batch_size=None, # no sub-batch size for validation + subbatch_size=None, # no sub-batch size for validation score_range=self._score_range ) else: diff --git a/tests/unit_tests/test_priority_search.py b/tests/unit_tests/test_priority_search.py index ef5a3e61..9812cc2f 100644 --- a/tests/unit_tests/test_priority_search.py +++ b/tests/unit_tests/test_priority_search.py @@ -46,11 +46,9 @@ def forward(self, x): xs = [1, 2, 3, 4, 5] infos = [1, 2, 3, 4, 5] batch_size = 3 -sub_batch_size = 2 +num_batches = 2 num_threads = 2 # 2 dataset = {'inputs': xs, 'infos': infos} -loader = DataLoader(dataset, batch_size=batch_size, randomize=False) -sampler = Sampler(loader=loader, guide=Guide(), sub_batch_size=sub_batch_size, num_threads=num_threads) num_proposals = 10 num_candidates = 5 @@ -158,7 +156,7 @@ def test_priority_search(): guide=Guide(), train_dataset=dataset, batch_size=batch_size, - sub_batch_size=sub_batch_size, + num_batches=num_batches, num_threads=num_threads, num_candidates=num_candidates, num_proposals=num_proposals, diff --git a/tests/unit_tests/test_sampler.py b/tests/unit_tests/test_sampler.py index c1a70fdb..a5ff434e 100644 --- a/tests/unit_tests/test_sampler.py +++ b/tests/unit_tests/test_sampler.py @@ -42,11 +42,11 @@ def test_sample_with_single_agent(): xs = [1, 2, 3, 4, 5] infos = [1, 2, 3, 4, 5] batch_size = 3 - sub_batch_size = 2 + subbatch_size = 2 num_threads = 2 dataset = {'inputs': xs, 'infos': infos} loader = DataLoader(dataset, batch_size=batch_size, randomize=False) - sampler = Sampler(loader=loader, guide=Guide(), sub_batch_size=sub_batch_size, num_threads=num_threads) + sampler = Sampler(loader=loader, guide=Guide(), subbatch_size=subbatch_size, num_threads=num_threads) ## Test with a single agent @@ -93,11 +93,11 @@ def test_sample_with_multiple_agents(): xs = [1, 2, 3, 4, 5] infos = [1, 2, 3, 4, 5] batch_size = 3 - sub_batch_size = 2 + subbatch_size = 2 num_threads = 2 dataset = {'inputs': xs, 'infos': infos} loader = DataLoader(dataset, batch_size=batch_size, randomize=False) - sampler = Sampler(loader=loader, guide=Guide(), sub_batch_size=sub_batch_size, num_threads=num_threads) + sampler = Sampler(loader=loader, guide=Guide(), subbatch_size=subbatch_size, num_threads=num_threads) ## Test with multiple agents diff --git a/tests/unit_tests/test_saving_loading.py b/tests/unit_tests/test_saving_loading.py index 06f09a54..8de6efd0 100644 --- a/tests/unit_tests/test_saving_loading.py +++ b/tests/unit_tests/test_saving_loading.py @@ -73,7 +73,6 @@ def forward(self, x): xs = [1, 2, 3, 4, 5] infos = [1, 2, 3, 4, 5] batch_size = 3 - sub_batch_size = 2 num_threads = 2 # 2 dataset = {'inputs': xs, 'infos': infos} loader = DataLoader(dataset, batch_size=batch_size) From 313e2c3e6b04128f3cc75b3a6668c3fa29e3ccb8 Mon Sep 17 00:00:00 2001 From: windweller Date: Tue, 9 Sep 2025 17:22:09 -0400 Subject: [PATCH 185/314] fix the node value representation (variable now uses original `repr_node_value`) --- opto/optimizers/optoprime_v2.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index 936a086d..4aae5f32 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -495,18 +495,24 @@ def initialize_prompt(self): others_section_title=self.optimizer_prompt_symbol_set.others_section_title.replace(" ", "") ) - @staticmethod - def repr_node_value(node_dict): + def repr_node_value(self, node_dict, node_tag="node", + value_tag="value", constraint_tag="constraint"): temp_list = [] for k, v in node_dict.items(): if "__code" not in k: - constraint_expr = f" ({type(v[0]).__name__}) {k}: {v[1]} " - temp_list.append( - f"\n{v[0]}\n{constraint_expr}\n\n") + if v[1] is not None and node_tag == self.optimizer_prompt_symbol_set.variable_tag: + constraint_expr = f"<{constraint_tag}>\n{v[1]}\n" + temp_list.append( + f"<{node_tag} name=\"{k}\" type=\"{type(v[0]).__name__}\">\n<{value_tag}>\n{v[0]}\n\n{constraint_expr}\n\n") + else: + temp_list.append( + f"<{node_tag} name=\"{k}\" type=\"{type(v[0]).__name__}\">\n<{value_tag}>\n{v[0]}\n\n\n") else: constraint_expr = f"\n{v[1]}\n" + signature = v[1].replace("The code should start with:\n", "") + func_body = v[0].replace(signature, "") temp_list.append( - f"\n\n{v[0]}\n\n{constraint_expr}\n\n") + f"<{node_tag} name=\"{k}\" type=\"code\">\n<{value_tag}>\n{signature}{func_body}\n\n{constraint_expr}\n\n") return "\n".join(temp_list) def repr_node_value_compact(self, node_dict, node_tag="node", @@ -596,7 +602,7 @@ def problem_instance(self, summary, mask=None): else "" ), variables=( - self.repr_node_value_compact(summary.variables, node_tag=self.optimizer_prompt_symbol_set.variable_tag, + self.repr_node_value(summary.variables, node_tag=self.optimizer_prompt_symbol_set.variable_tag, value_tag=self.optimizer_prompt_symbol_set.value_tag, constraint_tag=self.optimizer_prompt_symbol_set.constraint_tag) if self.optimizer_prompt_symbol_set.variables_section_title not in mask From 6a6af3b00fe66c3ed2ad74129972f8ce2cc2ef07 Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 9 Sep 2025 22:17:38 +0000 Subject: [PATCH 186/314] Add time as priority --- opto/features/priority_search/priority_search.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index 9a6f7534..84924ba3 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -649,6 +649,8 @@ def compute_exploration_priority(self, candidate) -> float: if self.score_function == 'mean': # Compute the mean score of the candidate's rollouts return candidate.mean_score() + elif self.score_function == 'time': + return -candidate.created_time # latest candidates have higher priority elif self.score_function == 'ucb': # Compute the Upper Confidence Bound (UCB) score lcb_score, mean_score, ucb_score = candidate.compute_score_confidence( From bb311c900663e2e270de9b06a85fda2116407b21 Mon Sep 17 00:00:00 2001 From: Xavier Date: Wed, 10 Sep 2025 16:45:35 +0200 Subject: [PATCH 187/314] Add tests for suggestion extraction and update dict --- opto/optimizers/optoprime.py | 103 +++++++++++++----- .../test_optoprime_suggestion_processing.py | 98 +++++++++++++++++ 2 files changed, 172 insertions(+), 29 deletions(-) create mode 100644 tests/unit_tests/test_optoprime_suggestion_processing.py diff --git a/opto/optimizers/optoprime.py b/opto/optimizers/optoprime.py index 6465151d..454cf331 100644 --- a/opto/optimizers/optoprime.py +++ b/opto/optimizers/optoprime.py @@ -1,4 +1,4 @@ -from typing import Any, List, Dict, Union, Tuple +from typing import Any, List, Dict, Union, Tuple, Optional from dataclasses import dataclass, asdict from textwrap import dedent, indent import warnings @@ -6,6 +6,7 @@ import re import copy import pickle +import ast from opto.trace.nodes import ParameterNode, Node, MessageNode from opto.trace.propagators import TraceGraph, GraphPropagator from opto.trace.propagators.propagators import Propagator @@ -534,35 +535,87 @@ def construct_update_dict( self, suggestion: Dict[str, Any] ) -> Dict[ParameterNode, Any]: """Convert the suggestion in text into the right data type.""" - # TODO: might need some automatic type conversion - update_dict = {} - for node in self.parameters: - if node.trainable and node.py_name in suggestion: + try: + from black import format_str, FileMode + + def _format_code(s: str) -> str: try: - formatted_suggestion = suggestion[node.py_name] - update_dict[node] = type(node.data)(formatted_suggestion) - except (ValueError, KeyError) as e: - # catch error due to suggestion missing the key or wrong data type - if self.ignore_extraction_error: - warnings.warn( - f"Cannot convert the suggestion '{suggestion[node.py_name]}' for {node.py_name} to the right data type" - ) - else: - raise e + return format_str(s, mode=FileMode()) + except Exception: + return s + + except Exception: + def _format_code(s: str) -> str: + return s + + def _find_key(node_name: str, sugg: Dict[str, Any]) -> Optional[str]: + """Return the key in *suggestion* that corresponds to *node_name*. + + - Exact match first. + - Otherwise allow the `__code8` ↔ `__code:8` alias by + stripping one optional ':' between the stem and trailing digits. + """ + + if node_name in sugg: + return node_name + + norm = re.sub(r":(?=\d+$)", "", node_name) + for k in sugg: + if re.sub(r":(?=\d+$)", "", k) == norm: + return k + return None + + update_dict: Dict[ParameterNode, Any] = {} + + for node in self.parameters: + if not node.trainable: + continue + + key = _find_key(node.py_name, suggestion) + if key is None: + continue + + try: + raw_val = suggestion[key] + if isinstance(raw_val, str) and "def" in raw_val: + raw_val = _format_code(raw_val) + if getattr(node, "data", None) is None: + converted = raw_val + else: + target_type = type(node.data) + if isinstance(raw_val, str) and target_type is not str: + try: + literal = ast.literal_eval(raw_val) + raw_val = literal + except Exception: + pass + try: + converted = target_type(raw_val) + except Exception: + converted = raw_val + update_dict[node] = converted + except (ValueError, KeyError, TypeError) as e: + if self.ignore_extraction_error: + warnings.warn( + f"Cannot convert the suggestion '{suggestion.get(key, '')}' for {node.py_name}: {e}" + ) + else: + raise e return update_dict def extract_llm_suggestion(self, response: str): """Extract the suggestion from the response.""" suggestion_tag = self.default_json_keys["suggestion"] + json_extracted = {} suggestion = {} attempt_n = 0 while attempt_n < 2: try: - suggestion = json.loads(response)[suggestion_tag] + json_extracted = json.loads(response) + suggestion = json_extracted.get(suggestion_tag, json_extracted) break except json.JSONDecodeError: - # Remove things outside the brackets response = re.findall(r"{.*}", response, re.DOTALL) if len(response) > 0: response = response[0] @@ -571,31 +624,23 @@ def extract_llm_suggestion(self, response: str): attempt_n += 1 if not isinstance(suggestion, dict): - suggestion = {} + suggestion = json_extracted if isinstance(json_extracted, dict) else {} if len(suggestion) == 0: - # we try to extract key/value separately and return it as a dictionary pattern = rf'"{suggestion_tag}"\s*:\s*\{{(.*?)\}}' suggestion_match = re.search(pattern, str(response), re.DOTALL) if suggestion_match: suggestion = {} - # Extract the entire content of the suggestion dictionary suggestion_content = suggestion_match.group(1) - # Regex to extract each key-value pair; - # This scheme assumes double quotes but is robust to missing commas at the end of the line pair_pattern = r'"([a-zA-Z0-9_]+)"\s*:\s*"(.*)"' - # Find all matches of key-value pairs pairs = re.findall(pair_pattern, suggestion_content, re.DOTALL) for key, value in pairs: suggestion[key] = value - if len(suggestion) == 0: - if not self.ignore_extraction_error: - print(f"Cannot extract {self.default_json_keys['suggestion']} from LLM's response:") - print(response) + if len(suggestion) == 0 and not self.ignore_extraction_error: + print(f"Cannot extract {self.default_json_keys['suggestion']} from LLM's response:") + print(response) - # if the suggested value is a code, and the entire code body is empty (i.e., not even function signature is present) - # then we remove such suggestion keys_to_remove = [] for key, value in suggestion.items(): if "__code" in key and value.strip() == "": diff --git a/tests/unit_tests/test_optoprime_suggestion_processing.py b/tests/unit_tests/test_optoprime_suggestion_processing.py new file mode 100644 index 00000000..9433d7f0 --- /dev/null +++ b/tests/unit_tests/test_optoprime_suggestion_processing.py @@ -0,0 +1,98 @@ +import json +import re +import timeit + +import pytest +from opto import trace +from opto.optimizers import OptoPrime +from opto.utils.llm import DummyLLM + + +def make_optimizer(params): + return OptoPrime(parameters=params, llm=DummyLLM(lambda *args, **kwargs: "")) + + +def test_construct_update_dict_alias_and_type_conversion(): + trace.GRAPH.clear() + param = trace.node(1, trainable=True) + opt = make_optimizer([param]) + suggestion = {"int:0": "2"} + update = opt.construct_update_dict(suggestion) + assert update[param] == 2 and isinstance(update[param], int) + + +def test_construct_update_dict_none_data(): + trace.GRAPH.clear() + param = trace.node(None, trainable=True) + opt = make_optimizer([param]) + suggestion = {param.py_name: "value"} + update = opt.construct_update_dict(suggestion) + assert update[param] == "value" + + +def test_extract_llm_suggestion_missing_tag(): + trace.GRAPH.clear() + dummy = trace.node(0, trainable=True) + opt = make_optimizer([dummy]) + response = json.dumps({"param1": 5}) + suggestion = opt.extract_llm_suggestion(response) + assert suggestion == {"param1": 5} + + +def test_extract_llm_suggestion_non_dict_suggestion(): + trace.GRAPH.clear() + dummy = trace.node(0, trainable=True) + opt = make_optimizer([dummy]) + response = json.dumps({"suggestion": "not a dict", "param1": 5}) + suggestion = opt.extract_llm_suggestion(response) + assert suggestion == {"suggestion": "not a dict", "param1": 5} + + +def test_efficiency_construct_update_dict(): + def baseline_construct_update_dict(parameters, suggestion): + update_dict = {} + for node in parameters: + if node.trainable and node.py_name in suggestion: + try: + formatted_suggestion = suggestion[node.py_name] + update_dict[node] = type(node.data)(formatted_suggestion) + except (ValueError, KeyError): + pass + return update_dict + + trace.GRAPH.clear() + params = [trace.node(i, trainable=True) for i in range(50)] + suggestion = {p.py_name: i for i, p in enumerate(params)} + opt = make_optimizer(params) + + t_base = timeit.timeit(lambda: baseline_construct_update_dict(params, suggestion), number=200) + t_new = timeit.timeit(lambda: opt.construct_update_dict(suggestion), number=200) + assert t_new <= t_base * 5 + + +def test_efficiency_extract_llm_suggestion(): + def baseline_extract(response, suggestion_tag="suggestion"): + suggestion = {} + attempt_n = 0 + while attempt_n < 2: + try: + suggestion = json.loads(response)[suggestion_tag] + break + except json.JSONDecodeError: + resp_list = re.findall(r"{.*}", response, re.DOTALL) + if len(resp_list) > 0: + response = resp_list[0] + attempt_n += 1 + except Exception: + attempt_n += 1 + if not isinstance(suggestion, dict): + suggestion = {} + return suggestion + + trace.GRAPH.clear() + dummy = trace.node(0, trainable=True) + opt = make_optimizer([dummy]) + response = json.dumps({"suggestion": {"a": 1}}) + t_base = timeit.timeit(lambda: baseline_extract(response), number=2000) + t_new = timeit.timeit(lambda: opt.extract_llm_suggestion(response), number=2000) + assert t_new <= t_base * 5 From 1acde3c85c5f08e0eff6e751820240bd6dcb2e4c Mon Sep 17 00:00:00 2001 From: Xavier Date: Wed, 10 Sep 2025 17:24:06 +0200 Subject: [PATCH 188/314] docs: clarify rationale for suggestion processing tests --- .../test_optoprime_suggestion_processing.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/unit_tests/test_optoprime_suggestion_processing.py b/tests/unit_tests/test_optoprime_suggestion_processing.py index 9433d7f0..e45e0227 100644 --- a/tests/unit_tests/test_optoprime_suggestion_processing.py +++ b/tests/unit_tests/test_optoprime_suggestion_processing.py @@ -1,3 +1,16 @@ +""" +Tests validating the robustness and efficiency of the improved +``construct_update_dict`` and ``extract_llm_suggestion`` helpers in +``OptoPrime``. + +Stable parsing and type conversion of LLM suggestions are critical for +optimizers built directly on ``OptoPrime`` and those that inherit from it, +including ``OptoPrimeMulti``, ``OptoPrimeV2``, ``OPRO``, and ``OPROv2``. The +tests below cover tricky edge cases while also benchmarking against the +previous implementations to ensure the new code does not introduce +significant overhead. +""" + import json import re import timeit From 3d1289ebf183caca45a33b01791d2a2e518acedc Mon Sep 17 00:00:00 2001 From: chinganc Date: Wed, 10 Sep 2025 21:47:19 +0000 Subject: [PATCH 189/314] Fix a bug of testing first params twice. --- opto/features/priority_search/search_template.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/opto/features/priority_search/search_template.py b/opto/features/priority_search/search_template.py index d997202e..ed95d25d 100644 --- a/opto/features/priority_search/search_template.py +++ b/opto/features/priority_search/search_template.py @@ -117,7 +117,7 @@ def train(self, # evaluation test_dataset = None, # dataset of (x, info) pairs to evaluate the agent; if None, use train_dataset test_guide = None, # guide to provide scores for the test set; if None, use guide - eval_frequency: Union[int, None] = 1, # frequency of evaluation + eval_frequency: Union[int, None] = 1, # frequency of evaluation NOTE set test_frequency < 0 to skip first evaluation num_eval_samples: int = 1, # number of samples to use to evaluate each input # logging log_frequency = None, # frequency of logging @@ -161,12 +161,6 @@ def train(self, else: self.validate_sampler = self.train_sampler # use the train_sampler for validation if no validation dataset is provided - # Evaluate the agent before learning - # NOTE set test_frequency < 0 to skip first evaluation - if (test_frequency is not None) and test_frequency > 0: - info_test = self.test(test_dataset, test_guide) # test self.agent - self.log(info_test) - # Save the agent before learning if save_frequency > 0 if (save_frequency is not None) and save_frequency > 0: self.save(save_path) @@ -191,8 +185,11 @@ def train(self, # Evaluate the agent after update if (test_frequency is not None) and (self.n_iters % test_frequency == 0): - info_test = self.test(test_dataset, test_guide) # test self.agent - self.log(info_test, prefix="Test: ") + if self.n_iters == 0 and test_frequency < 0: + print("Skipping first evaluation.") + else: + info_test = self.test(test_dataset, test_guide) # test self.agent + self.log(info_test, prefix="Test: ") # Save the algorithm state if (save_frequency is not None and save_frequency > 0) and self.n_iters % save_frequency == 0: From 4fa41e8b5a355b14241b80accee6fc94bcc44209 Mon Sep 17 00:00:00 2001 From: windweller Date: Thu, 11 Sep 2025 14:15:19 -0400 Subject: [PATCH 190/314] fixing memory representation in optimizers (now XML tag based) --- opto/optimizers/optoprime_v2.py | 73 +++++++++++++++++++++++---------- 1 file changed, 52 insertions(+), 21 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index 4aae5f32..cc898bac 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -1,5 +1,5 @@ import json -from typing import Any, List, Dict, Union, Tuple +from typing import Any, List, Dict, Union, Tuple, Optional from dataclasses import dataclass, asdict from opto.optimizers.optoprime import OptoPrime, FunctionFeedback from opto.trace.utils import dedent @@ -92,9 +92,10 @@ def example_output(self, reasoning, variables): else: # Build the output string in the same XML-like format as self.output_format output = [] - output.append(f"<{self.reasoning_tag}>") - output.append(reasoning) - output.append(f"") + if reasoning != "": + output.append(f"<{self.reasoning_tag}>") + output.append(reasoning) + output.append(f"") for var_name, value in variables.items(): output.append(f"<{self.improved_variable_tag}>") output.append(f"<{self.name_tag}>{var_name}") @@ -104,7 +105,6 @@ def example_output(self, reasoning, variables): output.append(f"") return "\n".join(output) - def output_response_extractor(self, response: str) -> Dict[str, Any]: # the response here should just be plain text @@ -143,6 +143,7 @@ def default_prompt_symbols(self) -> Dict[str, str]: "documentation": self.documentation_section_title, } + class OptimizerPromptSymbolSetJSON(OptimizerPromptSymbolSet): """We enforce a JSON output format extraction""" @@ -231,6 +232,7 @@ def output_response_extractor(self, response: str) -> Dict[str, Any]: return extracted_data + class OptimizerPromptSymbolSet2(OptimizerPromptSymbolSet): variables_section_title = "# Variables" inputs_section_title = "# Inputs" @@ -306,11 +308,47 @@ def __repr__(self) -> str: ) +@dataclass +class MemoryInstance: + variables: Dict[str, Tuple[Any, str]] # name -> (data, constraint) + feedback: str + optimizer_prompt_symbol_set: OptimizerPromptSymbolSet + + memory_example_template = dedent( + """{variables}{feedback} + """ + ) + + def __init__(self, variables: Dict[str, Any], feedback: str, optimizer_prompt_symbol_set: OptimizerPromptSymbolSet, + index: Optional[int] = None): + self.feedback = feedback + self.optimizer_prompt_symbol_set = optimizer_prompt_symbol_set + self.variables = variables + self.index = index + + def __str__(self) -> str: + var_repr = "" + for k, v in self.variables.items(): + var_repr += dedent(f""" + <{self.optimizer_prompt_symbol_set.improved_variable_tag}> + <{self.optimizer_prompt_symbol_set.name_tag}>{k} + <{self.optimizer_prompt_symbol_set.value_tag}> + {v[0]} + + + """) + + return self.memory_example_template.format( + variables=var_repr, + feedback=self.feedback, + index=" " + str(self.index) if self.index is not None else "" + ) + + class OptoPrimeV2(OptoPrime): # This is generic representation prompt, which just explains how to read the problem. representation_prompt = dedent( - """ - You're tasked to solve a coding/algorithm problem. You will see the instruction, the code, the documentation of each function used in the code, and the feedback about the execution result. + """You're tasked to solve a coding/algorithm problem. You will see the instruction, the code, the documentation of each function used in the code, and the feedback about the execution result. Specifically, a problem will be composed of the following parts: - {instruction_section_title}: the instruction which describes the things you need to do or the question you should answer. @@ -327,8 +365,7 @@ class OptoPrimeV2(OptoPrime): For variables we express as this: {variable_expression_format} - If `data_type` is `code`, it means `{value_tag}` is the source code of a python code, which may include docstring and definitions. - """ + If `data_type` is `code`, it means `{value_tag}` is the source code of a python code, which may include docstring and definitions.""" ) # Optimization @@ -567,16 +604,11 @@ def construct_prompt(self, summary, mask=None, *args, **kwargs): formatted_final = self.final_prompt.format(names=var_names) prefix = user_prompt.split(formatted_final)[0] examples = [] + index = 0 for variables, feedback in self.memory: - examples.append( - json.dumps( - { - "variables": {k: v[0] for k, v in variables.items()}, - "feedback": feedback, - }, - indent=4, - ) - ) + index += 1 + examples.append(str(MemoryInstance(variables, feedback, self.optimizer_prompt_symbol_set, index=index))) + examples = "\n".join(examples) user_prompt = ( prefix @@ -603,8 +635,8 @@ def problem_instance(self, summary, mask=None): ), variables=( self.repr_node_value(summary.variables, node_tag=self.optimizer_prompt_symbol_set.variable_tag, - value_tag=self.optimizer_prompt_symbol_set.value_tag, - constraint_tag=self.optimizer_prompt_symbol_set.constraint_tag) + value_tag=self.optimizer_prompt_symbol_set.value_tag, + constraint_tag=self.optimizer_prompt_symbol_set.constraint_tag) if self.optimizer_prompt_symbol_set.variables_section_title not in mask else "" ), @@ -700,7 +732,6 @@ def call_llm( print("LLM response:\n", response) return response - def save(self, path: str): """Save the optimizer state to a file.""" with open(path, 'wb') as f: From 5fd6f82cdc9afa763daa1a6f7465f01757861963 Mon Sep 17 00:00:00 2001 From: windweller Date: Thu, 11 Sep 2025 18:03:22 -0400 Subject: [PATCH 191/314] upgrade --- pyproject.toml | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fa4852fe..829af4e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,14 +11,14 @@ authors = [ {name = "Adith Swaminathan", email = "adith387@gmail.com"}, ] license="MIT" -requires-python = ">= 3.9" +requires-python = ">= 3.10" dynamic = ["version", "dependencies", "description"] readme = "README.md" keywords = ["trace", "opto", "AutoDiff"] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", - "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", ] [project.optional-dependencies] diff --git a/setup.py b/setup.py index e1e16725..dbd60be5 100644 --- a/setup.py +++ b/setup.py @@ -29,5 +29,5 @@ long_description=open('README.md', encoding="utf8").read(), packages=setuptools.find_packages(include=["opto*"]), install_requires=install_requires, - python_requires=">=3.9", + python_requires=">=3.10", ) From fc549e039f5e1564c37751b9c238a88aa001e864 Mon Sep 17 00:00:00 2001 From: windweller Date: Thu, 11 Sep 2025 18:50:44 -0400 Subject: [PATCH 192/314] upgrade workflow --- .github/workflows/ci.yml | 2 +- .github/workflows/python-app.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 46e0b317..7889b69d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,7 +49,7 @@ jobs: # 6) Set up Python & install dependencies - uses: actions/setup-python@v5 - with: { python-version: "3.9" } + with: { python-version: "3.10" } - name: Install Python deps run: | pip install -e . diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index bda57a97..8074be85 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -19,10 +19,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up Python 3.9 + - name: Set up Python 3.10 uses: actions/setup-python@v3 with: - python-version: "3.9" + python-version: "3.10" - name: Install dependencies run: | python -m pip install --upgrade pip From c33a2f7bcbcad80fe8a4780e2cb2af1657be3fd2 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 11 Sep 2025 23:25:17 +0000 Subject: [PATCH 193/314] Add save and resume. Fix bugs in tests. --- .../priority_search/priority_search.py | 8 +- .../priority_search/search_template.py | 85 +++++++++++++++---- opto/trainer/loader.py | 33 +++---- tests/unit_tests/test_priority_search.py | 6 +- 4 files changed, 86 insertions(+), 46 deletions(-) diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index 84924ba3..b5b53996 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -310,7 +310,8 @@ def train(self, self.memory = HeapMemory(size=memory_size) # Initialize the heap memory with a size limit - super().train(guide, train_dataset, + super().train(guide=guide, + train_dataset=train_dataset, validate_dataset=validate_dataset, validate_guide=validate_guide, batch_size=batch_size, @@ -343,7 +344,8 @@ def update(self, # 3. Update the priority queue with the validation results self.update_memory(validate_results, verbose=verbose, **kwargs) # samples are provided here in case candidates do not capture full information else: # The first iteration. - while len(self.memory) < self.num_candidates: + max_mem_size = self.memory.size if self.memory.size is not None else float('inf') + while len(self.memory) < min(max_mem_size, self.num_candidates): self.memory.push(self.max_score, ModuleCandidate(self.agent, optimizer=self.optimizer)) # Push the base agent as the first candidate (This gives the initialization of the priority queue) # 4. Explore and exploit the priority queue self._best_candidate, info_exploit = self.exploit(verbose=verbose, **kwargs) # get the best candidate (ModuleCandidate) from the priority queue @@ -579,7 +581,7 @@ def explore(self, verbose: bool = False, **kwargs): # self._best_candidate is the exploited candidate from the previous iteration top_candidates = [self._best_candidate] if self.use_best_candidate_to_explore else [] priorities = [] # to store the priorities of the candidates for logging - while len(top_candidates) < self.num_candidates and self.memory: + while len(top_candidates) < self.num_candidates and len(self.memory) > 0: neg_priority, candidate = self.memory.pop() # pop the top candidate from the priority queue priority = - neg_priority # remember that we stored negative scores in the priority queue if self.use_best_candidate_to_explore: diff --git a/opto/features/priority_search/search_template.py b/opto/features/priority_search/search_template.py index ed95d25d..ccd459d4 100644 --- a/opto/features/priority_search/search_template.py +++ b/opto/features/priority_search/search_template.py @@ -8,7 +8,7 @@ from opto.features.priority_search.sampler import Sampler, BatchRollout from opto.trainer.evaluators import evaluate # TODO update evaluate implementation from dataclasses import dataclass - +import pickle # TODO save and load SearchTemplate # TODO async version??? # TODO create SYNC and ASYNC versions of the base class; add an attribute to the class to indicate @@ -68,6 +68,20 @@ def check_optimizer_parameters(optimizer: Optimizer, agent: trace.Module): assert agent_params == optimizer_params, "Optimizer parameters do not match agent parameters." +def save_train_config(function): + """ Decorator to save the inputs of a class method. """ + def wrapper(self, **kwargs): + _kwargs = kwargs.copy() + del _kwargs['train_dataset'] # remove train_dataset from the saved kwargs + if _kwargs.get('validate_dataset') is not None: + del _kwargs['validate_dataset'] # remove validate_dataset from the saved kwargs + if _kwargs.get('test_dataset') is not None: + del _kwargs['test_dataset'] # remove test_dataset from the saved kwargs + setattr(self, f'_train_last_kwargs', _kwargs) + return function(self, **kwargs) + return wrapper + + class SearchTemplate(Trainer): # This only uses __init__ and evaluate of Minibatch class. """ This implements a generic template for search algorithm. """ @@ -100,10 +114,11 @@ def optimizer(self): self._optimizer_index += 1 return self._optimizers[self._optimizer_index % len(self._optimizers)] # return the current optimizer + @save_train_config def train(self, + *, guide, # guide to provide feedback train_dataset, # dataset of (x, info) pairs to train the agent - *, # validation validate_dataset = None, # same format as train_dataset; if None use the current batch. validate_guide = None, # to provide scores for the validation set @@ -112,6 +127,8 @@ def train(self, num_batches = 1, # number of batches to use from the dataset in each iteration score_range = None, # minimum score to update the agent num_epochs = 1, # number of training epochs + _init_epoch = 0, # initial epoch number (for resuming training) + _init_n_samples = 0, # initial number of samples (for resuming training) num_threads = None, # maximum number of threads to use verbose = False, # whether to print the output of the agent # evaluation @@ -122,7 +139,7 @@ def train(self, # logging log_frequency = None, # frequency of logging save_frequency: Union[int, None] = None, # frequency of saving the agent - save_path: str = "checkpoints/agent.pkl", # path to save the agent + save_path: str = "search_checkpoints/", # path to save the agent **kwargs ): assert 'subbatch_size' not in kwargs, "subbatch_size should not be provided in kwargs." @@ -166,13 +183,13 @@ def train(self, self.save(save_path) samples = None - self.n_epochs = 0 # number of epochs (full passes over the dataset) performed by the algorithm (This is incremented in sample) - self.n_samples = 0 # number of training samples processed by the algorithm (This is incremented in sample) + n_epochs = _init_epoch # number of epochs (full passes over the dataset) performed by the algorithm (This is incremented in sample) + n_samples = _init_n_samples # number of training samples processed by the algorithm (This is incremented in sample) train_scores = [] # to store the scores of the agent during training - while self.n_epochs < num_epochs : + while n_epochs < num_epochs : - print(f"Epoch: {self.n_epochs}. Iteration: {self.n_iters}") + print(f"Epoch: {n_epochs}. Iteration: {self.n_iters}") # 1. Propose new parameters given the current state of the algorithm # proposals: list of trace.Modules @@ -189,7 +206,7 @@ def train(self, print("Skipping first evaluation.") else: info_test = self.test(test_dataset, test_guide) # test self.agent - self.log(info_test, prefix="Test: ") + self.log(info_test, prefix="Test/") # Save the algorithm state if (save_frequency is not None and save_frequency > 0) and self.n_iters % save_frequency == 0: @@ -201,17 +218,17 @@ def train(self, train_scores.append(info_sample['mean_score']) # so that mean can be computed if self.n_iters % log_frequency == 0: - self.logger.log('Average train score', np.mean(train_scores), self.n_iters, color='blue') - self.log(info_update, prefix="Update: ") - self.log(info_sample, prefix="Sample: ") - self.n_samples += len(samples) # update the number of samples processed - self.logger.log('Number of samples', self.n_samples, self.n_iters, color='blue') + self.logger.log('Algo/Average train score', np.mean(train_scores), self.n_iters, color='blue') + self.log(info_update, prefix="Update/") + self.log(info_sample, prefix="Sample/") + n_samples += len(samples) # update the number of samples processed + self.logger.log('Algo/Number of samples', n_samples, self.n_iters, color='blue') # Log parameters for p in self.agent.parameters(): - self.logger.log(f"Parameter: {p.name}", p.data, self.n_iters, color='red') + self.logger.log(f"Parameter/{p.name}", p.data, self.n_iters, color='red') # Update counters - self.n_epochs = info_sample['n_epochs'] # update the number of epochs completed + n_epochs = info_sample['n_epochs'] # update the number of epochs completed self.n_iters += 1 return @@ -276,8 +293,42 @@ def evaluate(self, agent, guide, xs, infos, min_score=None, num_samples=1, num_t return np.mean(test_scores) def save(self, save_path): - self.save_agent(save_path, self.n_iters) - # TODO save full state of self + with open(save_path+'/algo.pkl', 'wb') as f: + pickle.dump(state, f) + + def load(self, load_path): + with open(load_path+'/algo.pkl', 'rb') as f: + state = pickle.load(f) + self.__dict__.update(state) + print(f"Loaded algorithm state from {load_path}/algo.pkl") + return + + + def resume(self, + load_path, + train_dataset, + validate_dataset = None, + test_dataset = None, + **kwargs): + """ Resume training from a saved state. + + Args: + load_path (str): Path to the saved state. + train_dataset: Dataset to resume training. + validate_dataset: Dataset for validation. If None, use the current batch. + test_dataset: Dataset for testing. If None, use train_dataset. + **kwargs: Additional keyword arguments for the training method. If not provided, the same parameters as the last training call are used. + """ + self.load(load_path) # load the saved state + # Resume training with the same parameters as before + last_train_kwargs = getattr(self, '_train_last_kwargs', {}).copy() + last_train_kwargs['train_dataset'] = train_dataset + last_train_kwargs['validate_dataset'] = validate_dataset + last_train_kwargs['test_dataset'] = test_dataset + last_train_kwargs.update(kwargs) # update with any new parameters provided + print(f"Resuming training with parameters: {last_train_kwargs}") + self.train(**last_train_kwargs) + # Unimplemented methods that should be implemented by subclasses def update(self, samples=None, verbose=False, **kwargs): diff --git a/opto/trainer/loader.py b/opto/trainer/loader.py index cf39cf30..57ee12ea 100644 --- a/opto/trainer/loader.py +++ b/opto/trainer/loader.py @@ -61,27 +61,14 @@ def sample(self): except StopIteration: return self.sample() - def save(self, path): - """Save the dataset to a file.""" - with open(path, 'wb') as f: - pickle.dump( - {'_indices': self._indices, - '_i': self._i, - 'batch_size': self.batch_size, - 'replacement': self.replacement, - 'shuffle': self.shuffle, - 'dataset': self.dataset}, - f - ) + def __getstate__(self): + """Get the state of the dataset for pickling.""" + state = self.__dict__.copy() + state.pop('dataset', None) # Remove dataset to avoid pickling issues + return state - def load(self, path): - """Load the dataset from a file.""" - import pickle - with open(path, 'rb') as f: - data = pickle.load(f) - self._indices = data['_indices'] - self._i = data['_i'] - self.batch_size = data['batch_size'] - self.replacement = data['replacement'] - self.shuffle = data['shuffle'] - self.dataset = data['dataset'] + def __setstate__(self, state): + """Set the state of the dataset from pickling.""" + self.__dict__.update(state) + # Note: dataset needs to be set manually after unpickling + print("Warning: dataset needs to be set manually after unpickling.") diff --git a/tests/unit_tests/test_priority_search.py b/tests/unit_tests/test_priority_search.py index 9812cc2f..226b56a0 100644 --- a/tests/unit_tests/test_priority_search.py +++ b/tests/unit_tests/test_priority_search.py @@ -47,7 +47,7 @@ def forward(self, x): infos = [1, 2, 3, 4, 5] batch_size = 3 num_batches = 2 -num_threads = 2 # 2 +num_threads = 2 dataset = {'inputs': xs, 'infos': infos} num_proposals = 10 @@ -104,8 +104,8 @@ def explore(self, **kwargs): assert isinstance(candidates, list) assert isinstance(info_dict, dict) - if self.n_iters == 0: - assert len(candidates) == 2, f"Expected 2 candidates, got {len(candidates)}" + if self.n_iters == 0: # NOTE use +1 since we hacked exploit above using deepcopy, the returned object does not have the same reference + assert len(candidates) == min(memory_size, num_candidates) + 1, f"Expected {min(memory_size, num_candidates) + 1} candidates, got {len(candidates)}" # one from the init parameter and one from the hacked best candidate else: assert len(candidates) <= self.num_candidates, f"Expect no more than {self.num_candidates} candidates at iter {self.n_iters}, got {len(candidates)}" From 644dfb849f71c9d2f53f1671d2850e39f02a5b5b Mon Sep 17 00:00:00 2001 From: chinganc Date: Fri, 12 Sep 2025 19:36:58 +0000 Subject: [PATCH 194/314] Update save and load of Optimzier, ModelWrapper, Guide, by implementing --- opto/optimizers/optimizer.py | 23 +++++++++++-- opto/optimizers/optoprime.py | 38 +------------------- opto/optimizers/optoprime_v2.py | 37 -------------------- opto/optimizers/textgrad.py | 24 ------------- opto/trace/modules.py | 42 ++++++++++++++++++++++ opto/trainer/guide.py | 25 ++++++++++---- opto/utils/llm.py | 33 +++++++++--------- tests/unit_tests/test_saving_loading.py | 46 ++++++++++++------------- 8 files changed, 121 insertions(+), 147 deletions(-) diff --git a/opto/optimizers/optimizer.py b/opto/optimizers/optimizer.py index c958a9ec..08d9cf9c 100644 --- a/opto/optimizers/optimizer.py +++ b/opto/optimizers/optimizer.py @@ -1,5 +1,5 @@ from typing import Any, List, Dict -import copy +import copy, pickle, os from opto.trace.nodes import ParameterNode, Node from opto.trace.propagators import GraphPropagator from opto.trace.propagators.propagators import Propagator @@ -97,11 +97,28 @@ def backward(self, node: Node, *args, **kwargs): def save(self, path: str): """Save the optimizer state to a file.""" - pass + # check if the directory exists + directory = os.path.dirname(path) + if directory != "": + os.makedirs(directory, exist_ok=True) + with open(path, 'wb') as f: + pickle.dump(self.__getstate__(), f) def load(self, path: str): """Load the optimizer state from a file.""" - pass + with open(path, 'rb') as f: + state = pickle.load(f) + self.__setstate__(state) + + # NOTE: overload __getstate__ and __setstate__ in subclasses to customize pickling behavior + def __getstate__(self): + state = self.__dict__.copy() + # don't pickle the parameters, as they are part of the model + state['parameters'] = None + return state + + def __setstate__(self, state): + self.__dict__.update(state) def __deepcopy__(self, memo): # deepcopy everything except self.parameters diff --git a/opto/optimizers/optoprime.py b/opto/optimizers/optoprime.py index 6465151d..909be520 100644 --- a/opto/optimizers/optoprime.py +++ b/opto/optimizers/optoprime.py @@ -631,40 +631,4 @@ def call_llm( if verbose: print("LLM response:\n", response) - return response - - - def save(self, path: str): - """Save the optimizer state to a file.""" - # save the above using pickle isntead - with open(path, "wb") as f: - pickle.dump( - { - "ignore_extraction_error": self.ignore_extraction_error, - "objective": self.objective, - "include_example": self.include_example, - "max_tokens": self.max_tokens, - "memory": self.memory, - "prompt_symbols": self.prompt_symbols, - "json_keys": self.default_json_keys, - 'output_format_prompt': self.output_format_prompt, - "use_json_object_format": self.use_json_object_format, - "highlight_variables": self.highlight_variables, - }, - f, - ) - - def load(self, path: str): - """Load the optimizer state from a file.""" - with open(path, "rb") as f: - state = pickle.load(f) - self.ignore_extraction_error = state["ignore_extraction_error"] - self.objective = state["objective"] - self.include_example = state["include_example"] - self.max_tokens = state["max_tokens"] - self.memory = state["memory"] - self.prompt_symbols = state["prompt_symbols"] - self.default_json_keys = state["json_keys"] - self.output_format_prompt = state['output_format_prompt'] - self.use_json_object_format = state["use_json_object_format"] - self.highlight_variables = state["highlight_variables"] \ No newline at end of file + return response \ No newline at end of file diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index a5c1a798..9067614c 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -734,40 +734,3 @@ def call_llm( if verbose: print("LLM response:\n", response) return response - - def save(self, path: str): - """Save the optimizer state to a file.""" - with open(path, 'wb') as f: - pickle.dump({ - "truncate_expression": self.truncate_expression, - "use_json_object_format": self.use_json_object_format, - "ignore_extraction_error": self.ignore_extraction_error, - "objective": self.objective, - "initial_var_char_limit": self.initial_var_char_limit, - "optimizer_prompt_symbol_set": self.optimizer_prompt_symbol_set, - "include_example": self.include_example, - "max_tokens": self.max_tokens, - "memory": self.memory, - "default_prompt_symbols": self.default_prompt_symbols, - "prompt_symbols": self.prompt_symbols, - "representation_prompt": self.representation_prompt, - "output_format_prompt": self.output_format_prompt, - }, f) - - def load(self, path: str): - """Load the optimizer state from a file.""" - with open(path, 'rb') as f: - state = pickle.load(f) - self.truncate_expression = state["truncate_expression"] - self.use_json_object_format = state["use_json_object_format"] - self.ignore_extraction_error = state["ignore_extraction_error"] - self.objective = state["objective"] - self.initial_var_char_limit = state["initial_var_char_limit"] - self.optimizer_prompt_symbol_set = state["optimizer_prompt_symbol_set"] - self.include_example = state["include_example"] - self.max_tokens = state["max_tokens"] - self.memory = state["memory"] - self.default_prompt_symbols = state["default_prompt_symbols"] - self.prompt_symbols = state["prompt_symbols"] - self.representation_prompt = state["representation_prompt"] - self.output_format_prompt = state["output_format_prompt"] diff --git a/opto/optimizers/textgrad.py b/opto/optimizers/textgrad.py index ecdb6dcd..ccbfa276 100644 --- a/opto/optimizers/textgrad.py +++ b/opto/optimizers/textgrad.py @@ -527,27 +527,3 @@ def call_llm( response = response.message.content return response - - - def save(self, path: str): - """ - Save the optimizer state to a file. - """ - with open(path, 'wb') as f: - pickle.dump({ - 'print_limit': self.print_limit, - 'max_tokens': self.max_tokens, - 'new_variable_tags': self.new_variable_tags, - 'optimizer_system_prompt': self.optimizer_system_prompt, - }, f) - - def load(self, path: str): - """ - Load the optimizer state from a file. - """ - with open(path, 'rb') as f: - state = pickle.load(f) - self.print_limit = state['print_limit'] - self.max_tokens = state['max_tokens'] - self.new_variable_tags = state['new_variable_tags'] - self.optimizer_system_prompt = state['optimizer_system_prompt'] \ No newline at end of file diff --git a/opto/trace/modules.py b/opto/trace/modules.py index f08d0165..15be12e3 100644 --- a/opto/trace/modules.py +++ b/opto/trace/modules.py @@ -97,6 +97,48 @@ def replace_node(match): with open(filename, "w") as f: f.write(trace_model_body) + + def __deepcopy__(self, memo): + # regular deepcopy behavior, because we will overwrite __setstate__ and __getstate__ for pickling + cls = self.__class__ + result = cls.__new__(cls) + memo[id(self)] = result + for k, v in self.__dict__.items(): + setattr(result, k, copy.deepcopy(v, memo)) + return result + + def __getstate__(self): + parameters_dict = self.parameters_dict() + non_parameters_dict = {} + for k, v in self.__dict__.items(): + if k not in parameters_dict: + if k.startswith('__TRACE_RESERVED_'): + # These are reserved for internal use. + continue + non_parameters_dict[k] = v + return dict(parameters_dict=parameters_dict, + non_parameters_dict=non_parameters_dict) + + def __setstate__(self, state): + parameters_dict = state['parameters_dict'] + non_parameters_dict = state['non_parameters_dict'] + self._set(parameters_dict) + # self.__dict__.update(non_parameters_dict) + + def save(self, file_name: str): + """Save the parameters of the model to a pickle file.""" + directory = os.path.dirname(file_name) + if directory != "": + os.makedirs(directory, exist_ok=True) + with open(file_name, "wb") as f: + pickle.dump(copy.deepcopy(self.__getstate__()), f) + + def load(self, file_name): + """Load the parameters of the model from a pickle file.""" + with open(file_name, "rb") as f: + loaded_data = pickle.load(f) + self.__setstate__(loaded_data) + return ModelWrapper diff --git a/opto/trainer/guide.py b/opto/trainer/guide.py index 72e4b918..19b6d3b2 100644 --- a/opto/trainer/guide.py +++ b/opto/trainer/guide.py @@ -3,6 +3,7 @@ import pickle import re import copy +import os from opto.utils.llm import LLM, AbstractModel from opto.trainer.suggest import Suggest @@ -57,16 +58,28 @@ def copy(self): return copy.deepcopy(self) def save(self, path: str): - """ Save the guide to a file. """ + """ Save the guide state to a file. """ + # check if the directory exists + directory = os.path.dirname(path) + if directory != "": + os.makedirs(directory, exist_ok=True) with open(path, 'wb') as f: - pickle.dump(self.__dict__, f) + pickle.dump(self.__getstate__(), f) def load(self, path: str): - """ Load the guide from a file. """ + """ Load the guide state from a file. """ with open(path, 'rb') as f: - data = pickle.load(f) - for key, value in data.items(): - setattr(self, key, value) + state = pickle.load(f) + self.__setstate__(state) + + # NOTE: overload __getstate__ and __setstate__ in subclasses to customize pickling behavior + def __getstate__(self): + state = self.__dict__.copy() + return state + + def __setstate__(self, state): + self.__dict__.update(state) + class LLMJudge(Guide): diff --git a/opto/utils/llm.py b/opto/utils/llm.py index a53abbc7..9cd034b3 100644 --- a/opto/utils/llm.py +++ b/opto/utils/llm.py @@ -211,7 +211,7 @@ def __init__(self, model: Union[str, None] = None, reset_freq: Union[int, None] server_api_key = os.environ.get('TRACE_CUSTOMLLM_API_KEY', 'sk-Xhg...') # we assume the server has an API key # the server API is set through `master_key` in `config.yaml` for LiteLLM proxy server - + self.model_name = model self.cache = cache factory = lambda: self._factory(base_url, server_api_key) # an LLM instance uses a fixed model @@ -241,15 +241,15 @@ def create(self, **config: Any): class LLMFactory: """Factory for creating LLM instances with predefined profiles. - + The code comes with these built-in profiles: llm_default = LLM(profile="default") # gpt-4o-mini - llm_premium = LLM(profile="premium") # gpt-4 + llm_premium = LLM(profile="premium") # gpt-4 llm_cheap = LLM(profile="cheap") # gpt-4o-mini llm_fast = LLM(profile="fast") # gpt-3.5-turbo-mini llm_reasoning = LLM(profile="reasoning") # o1-mini - + You can override those built-in profiles: LLMFactory.register_profile("default", "LiteLLM", model="gpt-4o", temperature=0.5) @@ -257,7 +257,7 @@ class LLMFactory: LLMFactory.register_profile("cheap", "LiteLLM", model="gpt-3.5-turbo", temperature=0.9) LLMFactory.register_profile("fast", "LiteLLM", model="gpt-3.5-turbo", max_tokens=500) LLMFactory.register_profile("reasoning", "LiteLLM", model="o1-preview") - + An Example of using Different Backends # Register custom profiles for different use cases @@ -276,7 +276,7 @@ class LLMFactory: # Multi-LLM optimizer with multiple profiles optimizer2 = OptoPrimeMulti(parameters, llm_profiles=["cheap", "premium", "claude_sonnet"], generation_technique="multi_llm") """ - + # Default profiles for different use cases _profiles = { 'default': {'backend': 'LiteLLM', 'params': {'model': 'gpt-4o-mini'}}, @@ -285,27 +285,27 @@ class LLMFactory: 'fast': {'backend': 'LiteLLM', 'params': {'model': 'gpt-3.5-turbo-mini'}}, 'reasoning': {'backend': 'LiteLLM', 'params': {'model': 'o1-mini'}}, } - + @classmethod def get_llm(cls, profile: str = 'default') -> AbstractModel: """Get an LLM instance for the specified profile.""" if profile not in cls._profiles: raise ValueError(f"Unknown profile '{profile}'. Available profiles: {list(cls._profiles.keys())}") - + config = cls._profiles[profile] backend_cls = _LLM_REGISTRY[config['backend']] return backend_cls(**config['params']) - + @classmethod def register_profile(cls, name: str, backend: str, **params): """Register a new LLM profile.""" cls._profiles[name] = {'backend': backend, 'params': params} - + @classmethod def list_profiles(cls): """List all available profiles.""" return list(cls._profiles.keys()) - + @classmethod def get_profile_info(cls, profile: str = None): """Get information about a profile or all profiles.""" @@ -316,21 +316,20 @@ def get_profile_info(cls, profile: str = None): class DummyLLM(AbstractModel): """A dummy LLM that does nothing. Used for testing purposes.""" - - def __init__(self, + + def __init__(self, callable, reset_freq: Union[int, None] = None) -> None: # self.message = message self.callable = callable - factory = lambda: self._factory() - super().__init__(factory, reset_freq) + super().__init__(self._factory, reset_freq) def _factory(self): # set response.choices[0].message.content # create a fake container with above format - class Message: + class Message: def __init__(self, content): self.content = content class Choice: @@ -346,7 +345,7 @@ def __init__(self, content): class LLM: """ A unified entry point for all supported LLM backends. - + Usage: # pick by env var (default: LiteLLM) llm = LLM() diff --git a/tests/unit_tests/test_saving_loading.py b/tests/unit_tests/test_saving_loading.py index 8de6efd0..96544115 100644 --- a/tests/unit_tests/test_saving_loading.py +++ b/tests/unit_tests/test_saving_loading.py @@ -36,6 +36,28 @@ def test_saving_load(): a, b = fun(x) print(a, b) +suggested_value = 5 + +def _llm_callable(messages, **kwargs): + """ + A dummy LLM callable that simulates a response. + """ + problem = messages[1]['content'] + + # extract name from + name = re.findall(r"", problem) + if name: + name = name[0] + else: + name = "unknown" + + return f""" + Dummy reasoning based on the input messages. + + {name} + {suggested_value} + + """ def test_trainer_saving_loading(): @@ -79,29 +101,7 @@ def forward(self, x): num_proposals = 10 num_candidates = 5 memory_size = 3 - suggested_value = 5 - - - def _llm_callable(messages, **kwargs): - """ - A dummy LLM callable that simulates a response. - """ - problem = messages[1]['content'] - - # extract name from - name = re.findall(r"", problem) - if name: - name = name[0] - else: - name = "unknown" - - return f""" - Dummy reasoning based on the input messages. - - {name} - {suggested_value} - - """ + # Create a dummy LLM and an agent dummy_llm = DummyLLM(_llm_callable) From 077252731662c24fe4855ba2df921c85aa546a62 Mon Sep 17 00:00:00 2001 From: chinganc Date: Fri, 12 Sep 2025 19:51:06 +0000 Subject: [PATCH 195/314] Make ModelWrapper pickleable --- opto/trace/modules.py | 228 ++++++++++++++++++------------------------ 1 file changed, 98 insertions(+), 130 deletions(-) diff --git a/opto/trace/modules.py b/opto/trace/modules.py index 15be12e3..4e1d19de 100644 --- a/opto/trace/modules.py +++ b/opto/trace/modules.py @@ -10,136 +10,13 @@ import functools from typing import List, Optional +# The model decorator now returns a top-level class def model(cls): - """ - Wrap a class with this decorator. This helps collect parameters for the optimizer. This decorated class cannot be pickled. - """ - - class ModelWrapper(cls, Module): - - def export(self, filename, projections: Optional[List[Projection]] = None): - """Dump the model's source code to a file, including all methods and attributes. - Ignores dunder methods unless they were overridden by the user. - """ - if projections is None: - projections = [BlackCodeFormatter()] - - trace_model_body = f"class {cls.__name__}:\n" - - # Get all members of the class - all_members = inspect.getmembers(self) - cls_members = inspect.getmembers(cls) - cls_member_names = [m[0] for m in cls_members] - - # Filter out dunder methods unless they were overridden - filtered_members = [] - for name, member in all_members: - # Skip internal trace reserved members - if name.startswith('__TRACE_RESERVED_'): - continue - - if name not in cls_member_names: - continue - - # Include if it's not a dunder method or if it was overridden - if not name.startswith('__'): - filtered_members.append((name, member)) - elif name.startswith('__'): - # For dunder methods, check if they were overridden - try: - print(cls.__name__, "<>", member.__qualname__) - # MixedClass <> test_export_mixed_trainable..MixedClass.__init__ - # if we wrap it inside a function, the qualname is different than when we dont - if hasattr(member, '__qualname__') and cls.__name__ in member.__qualname__: - filtered_members.append((name, member)) - except (AttributeError, TypeError): - # Skip if we can't determine if it was overridden - continue - - # Process each member - for i, (name, member) in enumerate(filtered_members): - print(name, member) - if 'FunModule' in str(member): - # Handle methods - if member.parameter is not None: - source = member.parameter.data - else: - source = member.info['source'] - source = textwrap.dedent(source) - indented = textwrap.indent(source, " ") - trace_model_body += indented - else: # this is a class method - source = inspect.getsource(member) - source = textwrap.dedent(source) - indented = textwrap.indent(source, " ") - trace_model_body += indented - - if i < len(all_members) - 1: - trace_model_body += "\n" # only one newline between members - - # Replace node initializations with their current values - # WARNING: there might be corner cases that this static analysis does not cover - import re - node_pattern = r'self\.(\w+)\s*=\s*node\([^)]*\)' - - def replace_node(match): - attr_name = match.group(1) - if hasattr(self, attr_name): - attr = getattr(self, attr_name) - if hasattr(attr, 'data'): - return f"self.{attr_name} = {attr.data}" - return match.group(0) # Return original if replacement not possible - - trace_model_body = re.sub(node_pattern, replace_node, trace_model_body) - - trace_model_body = functools.reduce(lambda body, proj: proj.project(body), projections, trace_model_body) - - with open(filename, "w") as f: - f.write(trace_model_body) - - - def __deepcopy__(self, memo): - # regular deepcopy behavior, because we will overwrite __setstate__ and __getstate__ for pickling - cls = self.__class__ - result = cls.__new__(cls) - memo[id(self)] = result - for k, v in self.__dict__.items(): - setattr(result, k, copy.deepcopy(v, memo)) - return result - - def __getstate__(self): - parameters_dict = self.parameters_dict() - non_parameters_dict = {} - for k, v in self.__dict__.items(): - if k not in parameters_dict: - if k.startswith('__TRACE_RESERVED_'): - # These are reserved for internal use. - continue - non_parameters_dict[k] = v - return dict(parameters_dict=parameters_dict, - non_parameters_dict=non_parameters_dict) - - def __setstate__(self, state): - parameters_dict = state['parameters_dict'] - non_parameters_dict = state['non_parameters_dict'] - self._set(parameters_dict) - # self.__dict__.update(non_parameters_dict) - - def save(self, file_name: str): - """Save the parameters of the model to a pickle file.""" - directory = os.path.dirname(file_name) - if directory != "": - os.makedirs(directory, exist_ok=True) - with open(file_name, "wb") as f: - pickle.dump(copy.deepcopy(self.__getstate__()), f) - - def load(self, file_name): - """Load the parameters of the model from a pickle file.""" - with open(file_name, "rb") as f: - loaded_data = pickle.load(f) - self.__setstate__(loaded_data) - - return ModelWrapper + name = f"{cls.__name__}ModelWrapper" + bases = (cls, ModelWrapperBase) + wrapper_cls = type(name, bases, {}) + globals()[name] = wrapper_cls # Register in module namespace for pickle + return wrapper_cls class Module(ParameterContainer): @@ -188,4 +65,95 @@ def _set(self, new_parameters): parameters_dict[k]._set(v) else: # if the parameter does not exist assert k not in self.__dict__ - setattr(self, k, v) \ No newline at end of file + setattr(self, k, v) + + +class ModelWrapperBase(Module): + def export(self, filename, projections: Optional[List[Projection]] = None): + if projections is None: + projections = [BlackCodeFormatter()] + cls = self.__class__ + trace_model_body = f"class {cls.__name__}:\n" + all_members = inspect.getmembers(self) + cls_members = inspect.getmembers(cls) + cls_member_names = [m[0] for m in cls_members] + filtered_members = [] + for name, member in all_members: + if name.startswith('__TRACE_RESERVED_'): + continue + if name not in cls_member_names: + continue + if not name.startswith('__'): + filtered_members.append((name, member)) + elif name.startswith('__'): + try: + if hasattr(member, '__qualname__') and cls.__name__ in member.__qualname__: + filtered_members.append((name, member)) + except (AttributeError, TypeError): + continue + for i, (name, member) in enumerate(filtered_members): + if 'FunModule' in str(member): + if member.parameter is not None: + source = member.parameter.data + else: + source = member.info['source'] + source = textwrap.dedent(source) + indented = textwrap.indent(source, " ") + trace_model_body += indented + else: + source = inspect.getsource(member) + source = textwrap.dedent(source) + indented = textwrap.indent(source, " ") + trace_model_body += indented + if i < len(all_members) - 1: + trace_model_body += "\n" + import re + node_pattern = r'self\.(\w+)\s*=\s*node\([^)]*\)' + def replace_node(match): + attr_name = match.group(1) + if hasattr(self, attr_name): + attr = getattr(self, attr_name) + if hasattr(attr, 'data'): + return f"self.{attr_name} = {attr.data}" + return match.group(0) + trace_model_body = re.sub(node_pattern, replace_node, trace_model_body) + trace_model_body = functools.reduce(lambda body, proj: proj.project(body), projections, trace_model_body) + with open(filename, "w") as f: + f.write(trace_model_body) + + def __deepcopy__(self, memo): + cls = self.__class__ + result = cls.__new__(cls) + memo[id(self)] = result + for k, v in self.__dict__.items(): + setattr(result, k, copy.deepcopy(v, memo)) + return result + + def __getstate__(self): + parameters_dict = self.parameters_dict() + non_parameters_dict = {} + for k, v in self.__dict__.items(): + if k not in parameters_dict: + if k.startswith('__TRACE_RESERVED_'): + continue + non_parameters_dict[k] = v + return dict(parameters_dict=parameters_dict, + non_parameters_dict=non_parameters_dict) + + def __setstate__(self, state): + parameters_dict = state['parameters_dict'] + non_parameters_dict = state['non_parameters_dict'] + self._set(parameters_dict) + # self.__dict__.update(non_parameters_dict) + + def save(self, file_name: str): + directory = os.path.dirname(file_name) + if directory != "": + os.makedirs(directory, exist_ok=True) + with open(file_name, "wb") as f: + pickle.dump(copy.deepcopy(self.__getstate__()), f) + + def load(self, file_name): + with open(file_name, "rb") as f: + loaded_data = pickle.load(f) + self.__setstate__(loaded_data) From 38b5ddf798d908d97b20d7e3e58a653871b46b1d Mon Sep 17 00:00:00 2001 From: chinganc Date: Fri, 12 Sep 2025 22:10:00 +0000 Subject: [PATCH 196/314] Add Model class --- opto/trace/modules.py | 162 +++++++++- opto/trainer/train.py | 10 +- tests/unit_tests/test_modules.py | 388 +++++++++++------------ tests/unit_tests/test_priority_search.py | 54 +++- 4 files changed, 409 insertions(+), 205 deletions(-) diff --git a/opto/trace/modules.py b/opto/trace/modules.py index 4e1d19de..56df3ca9 100644 --- a/opto/trace/modules.py +++ b/opto/trace/modules.py @@ -1,6 +1,7 @@ import os import pickle import copy +import sys import inspect import textwrap from opto.trace.containers import ParameterContainer, trainable_method @@ -10,14 +11,159 @@ import functools from typing import List, Optional -# The model decorator now returns a top-level class + def model(cls): - name = f"{cls.__name__}ModelWrapper" - bases = (cls, ModelWrapperBase) - wrapper_cls = type(name, bases, {}) - globals()[name] = wrapper_cls # Register in module namespace for pickle - return wrapper_cls + """ + Wrap a class with this decorator. This helps collect parameters for the optimizer. This decorated class cannot be pickled. + """ + name = f"{cls.__name__}Model" + bases = (cls, Model) + new_class = type(name, bases, {}) + new_class.__module__ = cls.__module__ + mod = sys.modules[cls.__module__] + setattr(mod, name, new_class) + return new_class + +# Old code +# def model(cls): +# """ +# Wrap a class with this decorator. This helps collect parameters for the optimizer. This decorated class cannot be pickled. +# """ + +# class ModelWrapper(cls, Module): + +# def export(self, filename, projections: Optional[List[Projection]] = None): +# """Dump the model's source code to a file, including all methods and attributes. +# Ignores dunder methods unless they were overridden by the user. +# """ +# if projections is None: +# projections = [BlackCodeFormatter()] + +# trace_model_body = f"class {cls.__name__}:\n" + +# # Get all members of the class +# all_members = inspect.getmembers(self) +# cls_members = inspect.getmembers(cls) +# cls_member_names = [m[0] for m in cls_members] + +# # Filter out dunder methods unless they were overridden +# filtered_members = [] +# for name, member in all_members: +# # Skip internal trace reserved members +# if name.startswith('__TRACE_RESERVED_'): +# continue + +# if name not in cls_member_names: +# continue + +# # Include if it's not a dunder method or if it was overridden +# if not name.startswith('__'): +# filtered_members.append((name, member)) +# elif name.startswith('__'): +# # For dunder methods, check if they were overridden +# try: +# print(cls.__name__, "<>", member.__qualname__) +# # MixedClass <> test_export_mixed_trainable..MixedClass.__init__ +# # if we wrap it inside a function, the qualname is different than when we dont +# if hasattr(member, '__qualname__') and cls.__name__ in member.__qualname__: +# filtered_members.append((name, member)) +# except (AttributeError, TypeError): +# # Skip if we can't determine if it was overridden +# continue + +# # Process each member +# for i, (name, member) in enumerate(filtered_members): +# print(name, member) +# if 'FunModule' in str(member): +# # Handle methods +# if member.parameter is not None: +# source = member.parameter.data +# else: +# source = member.info['source'] +# source = textwrap.dedent(source) +# indented = textwrap.indent(source, " ") +# trace_model_body += indented +# else: # this is a class method +# source = inspect.getsource(member) +# source = textwrap.dedent(source) +# indented = textwrap.indent(source, " ") +# trace_model_body += indented + +# if i < len(all_members) - 1: +# trace_model_body += "\n" # only one newline between members + +# # Replace node initializations with their current values +# # WARNING: there might be corner cases that this static analysis does not cover +# import re +# node_pattern = r'self\.(\w+)\s*=\s*node\([^)]*\)' +# def replace_node(match): +# attr_name = match.group(1) +# if hasattr(self, attr_name): +# attr = getattr(self, attr_name) +# if hasattr(attr, 'data'): +# return f"self.{attr_name} = {attr.data}" +# return match.group(0) # Return original if replacement not possible + +# trace_model_body = re.sub(node_pattern, replace_node, trace_model_body) + +# trace_model_body = functools.reduce(lambda body, proj: proj.project(body), projections, trace_model_body) + +# with open(filename, "w") as f: +# f.write(trace_model_body) + + +# def __deepcopy__(self, memo): +# # regular deepcopy behavior, because we will overwrite __setstate__ and __getstate__ for pickling +# cls = self.__class__ +# result = cls.__new__(cls) +# memo[id(self)] = result +# for k, v in self.__dict__.items(): +# setattr(result, k, copy.deepcopy(v, memo)) +# return result + +# def __getstate__(self): +# parameters_dict = self.parameters_dict() +# non_parameters_dict = {} +# for k, v in self.__dict__.items(): +# if k not in parameters_dict: +# if k.startswith('__TRACE_RESERVED_'): +# # These are reserved for internal use. +# continue +# non_parameters_dict[k] = v +# return dict(parameters_dict=parameters_dict, +# non_parameters_dict=non_parameters_dict) + +# def __setstate__(self, state): +# parameters_dict = state['parameters_dict'] +# non_parameters_dict = state['non_parameters_dict'] +# self._set(parameters_dict) +# # self.__dict__.update(non_parameters_dict) + +# def save(self, file_name: str): +# """Save the parameters of the model to a pickle file.""" +# directory = os.path.dirname(file_name) +# if directory != "": +# os.makedirs(directory, exist_ok=True) +# with open(file_name, "wb") as f: +# pickle.dump(copy.deepcopy(self.__getstate__()), f) + +# def load(self, file_name): +# """Load the parameters of the model from a pickle file.""" +# with open(file_name, "rb") as f: +# loaded_data = pickle.load(f) +# self.__setstate__(loaded_data) + +# # return ModelWrapper +# name = f"{cls.__name__}ModelWrapper" +# ModelWrapper.__name__ = name +# ModelWrapper.__qualname__ = name + +# # register the class in the module namespace for pickle +# ModelWrapper.__module__ = cls.__module__ +# mod = sys.modules[cls.__module__] +# setattr(mod, name, ModelWrapper) +# return ModelWrapper class Module(ParameterContainer): """Module is a ParameterContainer which has a forward method.""" @@ -68,7 +214,9 @@ def _set(self, new_parameters): setattr(self, k, v) -class ModelWrapperBase(Module): +class Model(Module): + """ Base class for all models. A model is a container of parameters with methods. """ + def export(self, filename, projections: Optional[List[Projection]] = None): if projections is None: projections = [BlackCodeFormatter()] diff --git a/opto/trainer/train.py b/opto/trainer/train.py index c0af96f9..2d6eac48 100644 --- a/opto/trainer/train.py +++ b/opto/trainer/train.py @@ -1,4 +1,4 @@ -from typing import Union +from typing import Union, Any import importlib from opto import trace @@ -19,6 +19,7 @@ def train( *, model: Union[trace.Module, ParameterNode], train_dataset: dict, + resume_training: Union[str, bool] = False, # path to load checkpoint or False # class of optimizer algorithm: Union[Trainer, str] = 'MinibatchAlgorithm', optimizer: Union[Optimizer, str] = None, @@ -30,7 +31,7 @@ def train( logger_kwargs: Union[dict, None] = None, # The rest is treated as trainer config **trainer_kwargs, -) -> None: +) -> Any: """ A high-level helper function to train the model using trainer. A trainer algorithm applies an optimizer to train a model under a guide on a train_dataset. @@ -83,6 +84,11 @@ def forward(self, x): logger=logger ) + if resume_training: + assert isinstance(resume_training, str), "resume_training must be a path string." + assert hasattr(algo, 'resume'), f"{trainer_class} does not support resume." + return algo.resume(load_path=resume_training) + return algo.train( guide=guide, train_dataset=train_dataset, diff --git a/tests/unit_tests/test_modules.py b/tests/unit_tests/test_modules.py index f5a5d6cc..c3cd4ff7 100644 --- a/tests/unit_tests/test_modules.py +++ b/tests/unit_tests/test_modules.py @@ -189,65 +189,65 @@ def complex_method(self, x): def __str__(self): return "ComplexClass" -def test_export_basic(): - dummy = DummyClass() - dummy._param._data = 42 # Change the node value - temp_file = "temp_dummy.py" - try: - dummy.export(temp_file) - with open(temp_file, "r") as f: - content = f.read() - # Check if class definition is present - assert "class DummyClass:" in content - # Check if regular method is present - assert "def regular_method" in content - # Check if __str__ is present (overridden dunder) - assert "def __str__" in content - # Check if __custom__ is present (custom dunder) - assert "def __custom__" in content - # Check if regular attribute is present - assert "regular_attr" in content - # Check if node initialization was replaced with current value - assert "self._param = 42" in content - assert "self._param = node(1" not in content - finally: - if os.path.exists(temp_file): - os.remove(temp_file) - -def test_export_complex(): - complex_obj = ComplexClass() - temp_file = "temp_complex.py" - try: - complex_obj.export(temp_file) - with open(temp_file, "r") as f: - content = f.read() - # Check if class definition is present - assert "class ComplexClass:" in content - # Check if complex method is present - assert "def complex_method" in content - # Check if __str__ is present - assert "def __str__" in content - # Check if nested class reference is in the method - assert "self._nested.regular_method" in content - finally: - if os.path.exists(temp_file): - os.remove(temp_file) - -def test_export_with_projection(): - dummy = DummyClass() - temp_file = "temp_dummy_formatted.py" - try: - # Test with BlackCodeFormatter - from opto.trace.projections import BlackCodeFormatter - dummy.export(temp_file, projections=[BlackCodeFormatter()]) - with open(temp_file, "r") as f: - content = f.read() - # Check if content is properly formatted - assert "class DummyClass:" in content - assert "def regular_method" in content - finally: - if os.path.exists(temp_file): - os.remove(temp_file) +# def test_export_basic(): +# dummy = DummyClass() +# dummy._param._data = 42 # Change the node value +# temp_file = "temp_dummy.py" +# try: +# dummy.export(temp_file) +# with open(temp_file, "r") as f: +# content = f.read() +# # Check if class definition is present +# assert "class DummyClass:" in content +# # Check if regular method is present +# assert "def regular_method" in content +# # Check if __str__ is present (overridden dunder) +# assert "def __str__" in content +# # Check if __custom__ is present (custom dunder) +# assert "def __custom__" in content +# # Check if regular attribute is present +# assert "regular_attr" in content +# # Check if node initialization was replaced with current value +# assert "self._param = 42" in content +# assert "self._param = node(1" not in content +# finally: +# if os.path.exists(temp_file): +# os.remove(temp_file) + +# def test_export_complex(): +# complex_obj = ComplexClass() +# temp_file = "temp_complex.py" +# try: +# complex_obj.export(temp_file) +# with open(temp_file, "r") as f: +# content = f.read() +# # Check if class definition is present +# assert "class ComplexClass:" in content +# # Check if complex method is present +# assert "def complex_method" in content +# # Check if __str__ is present +# assert "def __str__" in content +# # Check if nested class reference is in the method +# assert "self._nested.regular_method" in content +# finally: +# if os.path.exists(temp_file): +# os.remove(temp_file) + +# def test_export_with_projection(): +# dummy = DummyClass() +# temp_file = "temp_dummy_formatted.py" +# try: +# # Test with BlackCodeFormatter +# from opto.trace.projections import BlackCodeFormatter +# dummy.export(temp_file, projections=[BlackCodeFormatter()]) +# with open(temp_file, "r") as f: +# content = f.read() +# # Check if content is properly formatted +# assert "class DummyClass:" in content +# assert "def regular_method" in content +# finally: +# if os.path.exists(temp_file): +# os.remove(temp_file) @model class NonTrainableClass: @@ -265,141 +265,141 @@ def non_trainable_method(self, x): def another_non_trainable(self, y): return y + 1 -def test_export_non_trainable(): - obj = NonTrainableClass() - obj._param._data = 10 # Change node value - obj._param2._data = 20 # Change another node value - temp_file = "temp_non_trainable.py" - try: - obj.export(temp_file) - with open(temp_file, "r") as f: - content = f.read() - # Check if class definition is present - assert "class NonTrainableClass:" in content - # Check if node initializations were replaced with current values - assert "self._param = 10" in content - assert "self._param2 = 20" in content - # Verify no node() calls remain - assert "node(" not in content - # Verify no bundle decorators remain - assert "@bundle" not in content - # Check if methods are present but without decorators - assert "def non_trainable_method" in content - assert "def another_non_trainable" in content - # Check if regular attribute is present - assert "regular_attr" in content - finally: - if os.path.exists(temp_file): - os.remove(temp_file) - -def test_export_mixed_trainable(): - - @model - class MixedClass: - def __init__(self): - super().__init__() - self._trainable = node(1, trainable=True) - self._non_trainable = node(2, trainable=False) - self.regular_attr = "test" - - @bundle(trainable=True) - def trainable_method(self, x): - return x - - @bundle(trainable=False) - def non_trainable_method(self, y): - return y + 1 - - - obj = MixedClass() - obj._trainable._data = 100 - obj._non_trainable._data = 200 - - obj.trainable_method.parameter._data = "def trainable_method(self, x):\n return x + 3" - - temp_file = "temp_mixed.py" - try: - obj.export(temp_file) - with open(temp_file, "r") as f: - content = f.read() - # Check if class definition is present - assert "class MixedClass:" in content - # Check if all node initializations were replaced - assert "self._trainable = 100" in content - assert "self._non_trainable = 200" in content - # Verify no node() calls remain - assert "node(" not in content - # Verify no bundle decorators remain - assert "@bundle" not in content - # Check if methods are present but without decorators - assert "def trainable_method" in content - assert "return x + 3" in content - assert "def non_trainable_method" in content - # Check if regular attribute is present - assert "regular_attr" in content - finally: - if os.path.exists(temp_file): - os.remove(temp_file) - -def test_export_and_import(): - @model - class StrangeCalculator: - def __init__(self): - super().__init__() - self.offset = node(2, trainable=True) - self.multiplier = node(1.5, trainable=True) - - @bundle(trainable=True) - def add(self, x, y): - """Add two numbers with an offset""" - return x + y + self.offset - - @bundle(trainable=True) - def multiply(self, x, y): - """Multiply two numbers with a multiplier""" - return x * y * self.multiplier - - # Create instance and modify parameters - calc = StrangeCalculator() - calc.offset._data = 3 - calc.multiplier._data = 2.0 - calc.add.parameter._data = "def add(self, x, y):\n return x + y + self.offset + 1" - calc.multiply.parameter._data = "def multiply(self, x, y):\n return x * y * self.multiplier * 2" - - # Dump the model - temp_file = "temp_calculator.py" - try: - calc.export(temp_file) - - # Import the dumped class - import importlib.util - spec = importlib.util.spec_from_file_location("temp_calculator", temp_file) - temp_module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(temp_module) - - # Get the imported class - ImportedCalculator = temp_module.StrangeCalculator - - # Create instance and test functionality - imported_calc = ImportedCalculator() - - # Test the modified behavior - result_add = imported_calc.add(5, 3) - result_multiply = imported_calc.multiply(4, 2) - - # Verify the results match our expected modified behavior - # add: 5 + 3 + 3 + 1 = 12 - # multiply: 4 * 2 * 2.0 * 2 = 32 - assert result_add == 12, f"Expected 12, got {result_add}" - assert result_multiply == 32, f"Expected 32, got {result_multiply}" - - # Verify the attributes have the correct values - assert imported_calc.offset == 3 - assert imported_calc.multiplier == 2.0 - - finally: - if os.path.exists(temp_file): - os.remove(temp_file) +# def test_export_non_trainable(): +# obj = NonTrainableClass() +# obj._param._data = 10 # Change node value +# obj._param2._data = 20 # Change another node value +# temp_file = "temp_non_trainable.py" +# try: +# obj.export(temp_file) +# with open(temp_file, "r") as f: +# content = f.read() +# # Check if class definition is present +# assert "class NonTrainableClass:" in content +# # Check if node initializations were replaced with current values +# assert "self._param = 10" in content +# assert "self._param2 = 20" in content +# # Verify no node() calls remain +# assert "node(" not in content +# # Verify no bundle decorators remain +# assert "@bundle" not in content +# # Check if methods are present but without decorators +# assert "def non_trainable_method" in content +# assert "def another_non_trainable" in content +# # Check if regular attribute is present +# assert "regular_attr" in content +# finally: +# if os.path.exists(temp_file): +# os.remove(temp_file) + +# def test_export_mixed_trainable(): + +# @model +# class MixedClass: +# def __init__(self): +# super().__init__() +# self._trainable = node(1, trainable=True) +# self._non_trainable = node(2, trainable=False) +# self.regular_attr = "test" + +# @bundle(trainable=True) +# def trainable_method(self, x): +# return x + +# @bundle(trainable=False) +# def non_trainable_method(self, y): +# return y + 1 + + +# obj = MixedClass() +# obj._trainable._data = 100 +# obj._non_trainable._data = 200 + +# obj.trainable_method.parameter._data = "def trainable_method(self, x):\n return x + 3" + +# temp_file = "temp_mixed.py" +# try: +# obj.export(temp_file) +# with open(temp_file, "r") as f: +# content = f.read() +# # Check if class definition is present +# assert "class MixedClass:" in content +# # Check if all node initializations were replaced +# assert "self._trainable = 100" in content +# assert "self._non_trainable = 200" in content +# # Verify no node() calls remain +# assert "node(" not in content +# # Verify no bundle decorators remain +# assert "@bundle" not in content +# # Check if methods are present but without decorators +# assert "def trainable_method" in content +# assert "return x + 3" in content +# assert "def non_trainable_method" in content +# # Check if regular attribute is present +# assert "regular_attr" in content +# finally: +# if os.path.exists(temp_file): +# os.remove(temp_file) + +# def test_export_and_import(): +# @model +# class StrangeCalculator: +# def __init__(self): +# super().__init__() +# self.offset = node(2, trainable=True) +# self.multiplier = node(1.5, trainable=True) + +# @bundle(trainable=True) +# def add(self, x, y): +# """Add two numbers with an offset""" +# return x + y + self.offset + +# @bundle(trainable=True) +# def multiply(self, x, y): +# """Multiply two numbers with a multiplier""" +# return x * y * self.multiplier + +# # Create instance and modify parameters +# calc = StrangeCalculator() +# calc.offset._data = 3 +# calc.multiplier._data = 2.0 +# calc.add.parameter._data = "def add(self, x, y):\n return x + y + self.offset + 1" +# calc.multiply.parameter._data = "def multiply(self, x, y):\n return x * y * self.multiplier * 2" + +# # Dump the model +# temp_file = "temp_calculator.py" +# try: +# calc.export(temp_file) + +# # Import the dumped class +# import importlib.util +# spec = importlib.util.spec_from_file_location("temp_calculator", temp_file) +# temp_module = importlib.util.module_from_spec(spec) +# spec.loader.exec_module(temp_module) + +# # Get the imported class +# ImportedCalculator = temp_module.StrangeCalculator + +# # Create instance and test functionality +# imported_calc = ImportedCalculator() + +# # Test the modified behavior +# result_add = imported_calc.add(5, 3) +# result_multiply = imported_calc.multiply(4, 2) + +# # Verify the results match our expected modified behavior +# # add: 5 + 3 + 3 + 1 = 12 +# # multiply: 4 * 2 * 2.0 * 2 = 32 +# assert result_add == 12, f"Expected 12, got {result_add}" +# assert result_multiply == 32, f"Expected 32, got {result_multiply}" + +# # Verify the attributes have the correct values +# assert imported_calc.offset == 3 +# assert imported_calc.multiplier == 2.0 + +# finally: +# if os.path.exists(temp_file): +# os.remove(temp_file) def test_copy_function(): """Test the copy function of Module class.""" diff --git a/tests/unit_tests/test_priority_search.py b/tests/unit_tests/test_priority_search.py index 226b56a0..f5d4856b 100644 --- a/tests/unit_tests/test_priority_search.py +++ b/tests/unit_tests/test_priority_search.py @@ -1,4 +1,4 @@ -from opto import trace +from opto import trace, trainer from opto.trainer.loader import DataLoader from opto.features.priority_search.sampler import Sampler from opto.features.priority_search.priority_search import PrioritySearch as _PrioritySearch @@ -10,6 +10,7 @@ import re import numpy as np import copy +import pickle class Guide(Guide): @@ -92,7 +93,7 @@ def exploit(self, **kwargs): assert self.use_best_candidate_to_explore, "Expected use_best_candidate_to_explore to be True in this unit test" candidate = copy.deepcopy(candidate) # Ensure we return a copy for p in candidate.base_module.parameters(): - candidate.update_dict[p] = p.data + 100 + candidate.update_dict[p] = p._data + 100 # This will be different the exploration candidates return candidate, info_dict @@ -163,3 +164,52 @@ def test_priority_search(): memory_size=memory_size, verbose=False, #'output', ) + + +def test_resume(): + """ + Test resuming the PrioritySearch algorithm from a saved state. + """ + # Create a dummy LLM and an agent + dummy_llm = DummyLLM(_llm_callable) + agent = Agent() + optimizer = OptoPrimeV2( + agent.parameters(), + llm=dummy_llm, + ) + + algo = PrioritySearch( + agent, + optimizer, + ) + + # test pickling objects + pickle.dumps(agent) + pickle.dumps(dummy_llm) + pickle.dumps(optimizer) + pickle.dumps(algo) + + + save_path="./test_priority_search_save" + + # algo.train( + # guide=Guide(), + # train_dataset=dataset, + # batch_size=batch_size, + # num_batches=num_batches, + # num_threads=num_threads, + # num_candidates=num_candidates, + # num_proposals=num_proposals, + # memory_size=memory_size, + # verbose=False, #'output', + # save_path=save_path, + # save_frequency=1, + # ) + + # new_algo = PrioritySearch.load(save_path) + # assert new_algo.n_iters == algo.n_iters, "Resumed algorithm should have the same number of iterations as the original." + + # new_algo.resume( + # train_dataset=dataset) + + # os.system(f"rm -rf {save_path}") From 199a6064fc649203dabd017471a3fdd8a13fd5cd Mon Sep 17 00:00:00 2001 From: chinganc Date: Fri, 12 Sep 2025 22:10:54 +0000 Subject: [PATCH 197/314] Add trace.Model --- opto/trace/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/opto/trace/__init__.py b/opto/trace/__init__.py index ddf01300..f7eaf778 100644 --- a/opto/trace/__init__.py +++ b/opto/trace/__init__.py @@ -1,5 +1,5 @@ from opto.trace.bundle import bundle, ExecutionError -from opto.trace.modules import Module, model +from opto.trace.modules import Module, model, Model from opto.trace.containers import NodeContainer from opto.trace.broadcast import apply_op import opto.trace.propagators as propagators @@ -28,6 +28,7 @@ def __exit__(self, type, value, traceback): "bundle", "ExecutionError", "Module", + "Model", "NodeContainer", "model", "apply_op", From 1dca308c52647641534e1f2ebf0da212f45c6d13 Mon Sep 17 00:00:00 2001 From: chinganc Date: Fri, 12 Sep 2025 22:13:28 +0000 Subject: [PATCH 198/314] add a toy test --- test.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 test.py diff --git a/test.py b/test.py new file mode 100644 index 00000000..78497cb6 --- /dev/null +++ b/test.py @@ -0,0 +1,16 @@ +from opto import trace +import pickle + +@trace.model +class Dummy: + def forward(self, x): + return x * 2 + + +dummy = Dummy() +pickle.dumps(dummy) + +try: + dummy.export("dummy.py") +except Exception as e: + print("Export failed:", e) \ No newline at end of file From 7a1a648f6ca70e3cc14671829e9a3c6f49576a99 Mon Sep 17 00:00:00 2001 From: chinganc Date: Fri, 12 Sep 2025 23:32:17 +0000 Subject: [PATCH 199/314] Resume seems to work now --- .../priority_search/priority_search.py | 17 ++- .../priority_search/search_template.py | 104 ++++++++++------ opto/trainer/algorithms/algorithm.py | 117 ++++++++++-------- opto/trainer/train.py | 23 ++-- tests/unit_tests/test_priority_search.py | 88 +++++++++---- 5 files changed, 221 insertions(+), 128 deletions(-) diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index b5b53996..5adc2c54 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -49,6 +49,15 @@ def apply_update(self, base_module=None): """ Apply update to the base_module in place. """ set_module_parameters(base_module or self.base_module, self.update_dict) + def __getstate__(self): + """ Get the state of the candidate for serialization. """ + state = copy.deepcopy(self.__dict__) # this will detach the nodes from the computation graph + return state + + def __setstate__(self, state): + """ Set the state of the candidate from serialization. """ + self.__dict__.update(state) + def __deepcopy__(self, memo): """ Create a deep copy, except for the base_module which is not copied, it is the original module. """ cls = self.__class__ @@ -350,8 +359,6 @@ def update(self, # 4. Explore and exploit the priority queue self._best_candidate, info_exploit = self.exploit(verbose=verbose, **kwargs) # get the best candidate (ModuleCandidate) from the priority queue self._exploration_candidates, info_explore = self.explore(verbose=verbose, **kwargs) # List of ModuleCandidates - if samples is None: # first iteration - assert len(self.memory) == 0, "Memory should be empty in the first iteration." # TODO Log information about the update info_log = { 'n_iters': self.n_iters, # number of iterations @@ -497,14 +504,14 @@ def validate(self, validate_samples = copy.copy(samples) # Validate newly proposed candidates - use_prev_batch = self._validate_dataset is None # when True, self.validate_sampler == self.train_sampler, and the current batch is used for validation + use_prev_batch = self.use_prev_batch # when True, self.validate_sampler == self.train_sampler, and the current batch is used for validation candidate_agents = [c.get_module() for c in candidates] # get the modules from the candidates validate_samples.add_samples(Samples(*self.validate_sampler.sample(candidate_agents, use_prev_batch=use_prev_batch, description_prefix='Validating newly proposed candidates: '))) # list of BatchRollout objects if self.validate_exploration_candidates: - if self._validate_dataset is not None: # validate the exploration candidates that collected the samples as well + if not use_prev_batch: # validate the exploration candidates that collected the samples as well # validate the agents in the validate_dataset exploration_agents = [c.get_module() for c in exploration_candidates] # get the modules from the exploration candidates exploration_samples = Samples(*self.validate_sampler.sample(exploration_agents, @@ -596,6 +603,7 @@ def explore(self, verbose: bool = False, **kwargs): 'num_exploration_candidates': len(top_candidates), 'exploration_candidates_mean_priority': np.mean(priorities), # list of priorities of the exploration candidates 'exploration_candidates_mean_score': np.mean(mean_scores), # list of mean scores of the exploration candidates + 'exploration_candidates_average_num_rollouts': np.mean([c.num_rollouts for c in top_candidates]), } return top_candidates, info_dict @@ -616,6 +624,7 @@ def exploit(self, verbose: bool = False, **kwargs) -> Tuple[ModuleCandidate, Dic return best_candidate, { 'best_candidate_priority': priority, # remember that we stored negative scores in the priority queue 'best_candidate_mean_score': best_candidate.mean_score(), # mean score of the candidate's rollouts + 'best_candidate_num_rollouts': best_candidate.num_rollouts, # number of rollouts of the candidate } # TODO refactor below to reuse scoring diff --git a/opto/features/priority_search/search_template.py b/opto/features/priority_search/search_template.py index ccd459d4..f5336bc8 100644 --- a/opto/features/priority_search/search_template.py +++ b/opto/features/priority_search/search_template.py @@ -8,7 +8,7 @@ from opto.features.priority_search.sampler import Sampler, BatchRollout from opto.trainer.evaluators import evaluate # TODO update evaluate implementation from dataclasses import dataclass -import pickle +import pickle, copy, os # TODO save and load SearchTemplate # TODO async version??? # TODO create SYNC and ASYNC versions of the base class; add an attribute to the class to indicate @@ -81,7 +81,6 @@ def wrapper(self, **kwargs): return function(self, **kwargs) return wrapper - class SearchTemplate(Trainer): # This only uses __init__ and evaluate of Minibatch class. """ This implements a generic template for search algorithm. """ @@ -107,7 +106,11 @@ def __init__(self, self._optimizers = [optimizer] self.n_iters = 0 # number of iterations + self.n_epochs = 0 # number of epochs (full passes over the dataset) performed by the algorithm (This is incremented in sample) + self.n_samples = 0 # number of training samples processed by the algorithm (This is incremented in sample) self._optimizer_index = -1 # index of the current optimizer to use + self.train_sampler = None # will be initialized in train + self.validate_sampler = None # will be initialized in train @property def optimizer(self): @@ -127,8 +130,6 @@ def train(self, num_batches = 1, # number of batches to use from the dataset in each iteration score_range = None, # minimum score to update the agent num_epochs = 1, # number of training epochs - _init_epoch = 0, # initial epoch number (for resuming training) - _init_n_samples = 0, # initial number of samples (for resuming training) num_threads = None, # maximum number of threads to use verbose = False, # whether to print the output of the agent # evaluation @@ -159,37 +160,45 @@ def train(self, subbatch_size, batch_size = batch_size, batch_size*num_batches - self.train_sampler = Sampler( - DataLoader(train_dataset, batch_size=batch_size), - guide, - num_threads=self.num_threads, - subbatch_size=subbatch_size, - score_range=self._score_range - ) - self._validate_dataset = validate_dataset # if None, the current batch will be used for validation - if validate_dataset is not None: - self.validate_sampler = Sampler( - DataLoader(validate_dataset, batch_size=batch_size), - validate_guide or guide, + if self.train_sampler is None: + self.train_sampler = Sampler( + DataLoader(train_dataset, batch_size=batch_size), + guide, num_threads=self.num_threads, - subbatch_size=None, # no sub-batch size for validation + subbatch_size=subbatch_size, score_range=self._score_range - ) + ) else: - self.validate_sampler = self.train_sampler # use the train_sampler for validation if no validation dataset is provided + self.train_sampler.loader.dataset = train_dataset # update the train dataset in the sampler + + self.use_prev_batch = validate_dataset is None # whether to use the current batch for validation + if self.validate_sampler is None: + if not self.use_prev_batch: + self.validate_sampler = Sampler( + DataLoader(validate_dataset, batch_size=batch_size), + validate_guide or guide, + num_threads=self.num_threads, + subbatch_size=None, # no sub-batch size for validation + score_range=self._score_range + ) + else: + self.validate_sampler = self.train_sampler # use the train_sampler for validation if no validation dataset is provided + else: + if not self.use_prev_batch: + self.validate_sampler.loader.dataset = validate_dataset # update the validate dataset in the sampler + else: + assert self.validate_sampler == self.train_sampler, "Expected validate_sampler to be train_sampler if no validate_dataset is provided." # Save the agent before learning if save_frequency > 0 if (save_frequency is not None) and save_frequency > 0: self.save(save_path) samples = None - n_epochs = _init_epoch # number of epochs (full passes over the dataset) performed by the algorithm (This is incremented in sample) - n_samples = _init_n_samples # number of training samples processed by the algorithm (This is incremented in sample) train_scores = [] # to store the scores of the agent during training - while n_epochs < num_epochs : + while self.n_epochs < num_epochs : - print(f"Epoch: {n_epochs}. Iteration: {self.n_iters}") + print(f"Epoch: {self.n_epochs}. Iteration: {self.n_iters}") # 1. Propose new parameters given the current state of the algorithm # proposals: list of trace.Modules @@ -214,21 +223,21 @@ def train(self, # Log information assert 'mean_score' in info_sample, "info_sample must contain 'mean_score'." - assert 'n_epochs' in info_sample, "info_sample must contain 'n_epochs'." + assert 'self.n_epochs' in info_sample, "info_sample must contain 'self.n_epochs'." train_scores.append(info_sample['mean_score']) # so that mean can be computed if self.n_iters % log_frequency == 0: self.logger.log('Algo/Average train score', np.mean(train_scores), self.n_iters, color='blue') self.log(info_update, prefix="Update/") self.log(info_sample, prefix="Sample/") - n_samples += len(samples) # update the number of samples processed - self.logger.log('Algo/Number of samples', n_samples, self.n_iters, color='blue') + self.n_samples += len(samples) # update the number of samples processed + self.logger.log('Algo/Number of samples', self.n_samples, self.n_iters, color='blue') # Log parameters for p in self.agent.parameters(): self.logger.log(f"Parameter/{p.name}", p.data, self.n_iters, color='red') # Update counters - n_epochs = info_sample['n_epochs'] # update the number of epochs completed + self.n_epochs = info_sample['self.n_epochs'] # update the number of epochs completed self.n_iters += 1 return @@ -256,7 +265,7 @@ def sample(self, agents, verbose=False, **kwargs): scores = [item for sublist in scores for item in sublist] # flatten the list of scores log_info = { 'mean_score': np.mean(scores), - 'n_epochs': self.train_sampler.n_epochs, + 'self.n_epochs': self.train_sampler.n_epochs, } # check if the scores are within the score range if not (self.min_score <= log_info['mean_score'] <= self.max_score): @@ -293,41 +302,54 @@ def evaluate(self, agent, guide, xs, infos, min_score=None, num_samples=1, num_t return np.mean(test_scores) def save(self, save_path): + print(f"Saving algorithm state to {save_path} at iteration {self.n_iters}.") + if not os.path.exists(save_path): + os.makedirs(save_path) + obj = copy.deepcopy(self) # to detach nodes from the computation graph with open(save_path+'/algo.pkl', 'wb') as f: - pickle.dump(state, f) + pickle.dump(obj, f) - def load(self, load_path): + @classmethod + def load(cls, load_path: str): with open(load_path+'/algo.pkl', 'rb') as f: - state = pickle.load(f) - self.__dict__.update(state) - print(f"Loaded algorithm state from {load_path}/algo.pkl") - return + algo = pickle.load(f) + assert isinstance(algo, cls), f"Loaded object is not an instance of {cls.__name__}." + return algo - def resume(self, - load_path, - train_dataset, + def resume(self, *, + model: trace.Module, + train_dataset: dict , validate_dataset = None, test_dataset = None, **kwargs): """ Resume training from a saved state. Args: - load_path (str): Path to the saved state. + model: The model to be trained. train_dataset: Dataset to resume training. validate_dataset: Dataset for validation. If None, use the current batch. test_dataset: Dataset for testing. If None, use train_dataset. **kwargs: Additional keyword arguments for the training method. If not provided, the same parameters as the last training call are used. """ - self.load(load_path) # load the saved state + # Set the state of the model to the provided model + assert isinstance(model, trace.Module), "model must be an instance of trace.Module." + state = self.agent.__getstate__() + model.__setstate__(state) # load the state into the provided model + self.agent = model # replace the model with the provided model + # Resume training with the same parameters as before last_train_kwargs = getattr(self, '_train_last_kwargs', {}).copy() + if self.use_prev_batch: + assert validate_dataset is None, "Loaded algo has use_prev_batch enabled. validate_dataset must be None when use_prev_batch is True." + assert self.validate_sampler == self.train_sampler # use the train_sampler for validation if no validation dataset is provided + + last_train_kwargs.update(kwargs) # update with any new parameters provided last_train_kwargs['train_dataset'] = train_dataset last_train_kwargs['validate_dataset'] = validate_dataset last_train_kwargs['test_dataset'] = test_dataset - last_train_kwargs.update(kwargs) # update with any new parameters provided print(f"Resuming training with parameters: {last_train_kwargs}") - self.train(**last_train_kwargs) + return self.train(**last_train_kwargs) # Unimplemented methods that should be implemented by subclasses diff --git a/opto/trainer/algorithms/algorithm.py b/opto/trainer/algorithms/algorithm.py index 326e1be2..c8d13775 100644 --- a/opto/trainer/algorithms/algorithm.py +++ b/opto/trainer/algorithms/algorithm.py @@ -93,55 +93,68 @@ def train(self, raise NotImplementedError - def save(self, path: str): - """ Save the guide to a file. """ - with open(path, 'wb') as f: - d = {} - for key, value in self.__dict__.items(): - if isinstance(value, Module): - _path = path+ f"_{key}.module" - value.save(_path) - d[key] = _path - elif isinstance(value, Guide): - _path = path + f"_{key}.guide" - value.save(_path) - d[key] = _path - elif isinstance(value, DataLoader): - _path = path + f"_{key}.dataloader" - value.save(_path) - d[key] = _path - elif isinstance(value, Optimizer): - _path = path + f"_{key}.optimizer" - value.save(_path) - d[key] = _path - else: - d[key] = value - pickle.dump(d, f) - - def load(self, path: str): - """ Load the guide from a file. """ - with open(path, 'rb') as f: - data = pickle.load(f) - for key, value in data.items(): - if key not in self.__dict__: - warning_msg = f"Key '{key}' not found in the algorithm's attributes. Skipping loading for this key." - print(warning_msg) # or use logging.warning(warning_msg) - continue - - # key is in the algorithm's attributes - if isinstance(value, str): - if value.endswith('.module'): - attr = self.__dict__[key] - assert isinstance(attr, Module), f"Expected {key} to be a Module, got {type(attr)}" - elif value.endswith('.guide'): - attr = self.__dict__[key] - assert isinstance(attr, Guide), f"Expected {key} to be an Guide, got {type(attr)}" - elif value.endswith('.dataloader'): - attr = self.__dict__[key] - assert isinstance(attr, DataLoader), f"Expected {key} to be a DataLoader, got {type(attr)}" - elif value.endswith('.optimizer'): - attr = self.__dict__[key] - assert isinstance(attr, Optimizer), f"Expected {key} to be an Optimizer, got {type(attr)}" - attr.load(value) - else: - self.__dict__[key] = value \ No newline at end of file + @classmethod + def load(cls, + load_path: str): + raise NotImplementedError + + + def resume(self, *, + model: Module, + train_dataset: dict , + **kwargs): + raise NotImplementedError + + # NOTE old code which may be useful in the future + # def save(self, path: str): + # """ Save the guide to a file. """ + # with open(path, 'wb') as f: + # d = {} + # for key, value in self.__dict__.items(): + # if isinstance(value, Module): + # _path = path+ f"_{key}.module" + # value.save(_path) + # d[key] = _path + # elif isinstance(value, Guide): + # _path = path + f"_{key}.guide" + # value.save(_path) + # d[key] = _path + # elif isinstance(value, DataLoader): + # _path = path + f"_{key}.dataloader" + # value.save(_path) + # d[key] = _path + # elif isinstance(value, Optimizer): + # _path = path + f"_{key}.optimizer" + # value.save(_path) + # d[key] = _path + # else: + # d[key] = value + # pickle.dump(d, f) + + # def load(self, path: str): + # """ Load the guide from a file. """ + # with open(path, 'rb') as f: + # data = pickle.load(f) + # for key, value in data.items(): + # if key not in self.__dict__: + # warning_msg = f"Key '{key}' not found in the algorithm's attributes. Skipping loading for this key." + # print(warning_msg) # or use logging.warning(warning_msg) + # continue + + # # key is in the algorithm's attributes + # if isinstance(value, str): + # if value.endswith('.module'): + # attr = self.__dict__[key] + # assert isinstance(attr, Module), f"Expected {key} to be a Module, got {type(attr)}" + # elif value.endswith('.guide'): + # attr = self.__dict__[key] + # assert isinstance(attr, Guide), f"Expected {key} to be an Guide, got {type(attr)}" + # elif value.endswith('.dataloader'): + # attr = self.__dict__[key] + # assert isinstance(attr, DataLoader), f"Expected {key} to be a DataLoader, got {type(attr)}" + # elif value.endswith('.optimizer'): + # attr = self.__dict__[key] + # assert isinstance(attr, Optimizer), f"Expected {key} to be an Optimizer, got {type(attr)}" + # attr.load(value) + # else: + # self.__dict__[key] = value \ No newline at end of file diff --git a/opto/trainer/train.py b/opto/trainer/train.py index 2d6eac48..7b9161f2 100644 --- a/opto/trainer/train.py +++ b/opto/trainer/train.py @@ -44,6 +44,18 @@ def train( # TODO check eligible optimizer, trainer dataset_check(train_dataset) + + trainer_class = load_trainer_class(algorithm) + assert issubclass(trainer_class, Trainer) + if resume_training: + assert isinstance(resume_training, str), "resume_training must be a path string." + assert hasattr(trainer_class, 'resume'), f"{trainer_class} does not support resume." + assert hasattr(trainer_class, 'load'), f"{trainer_class} does not support load." + algo = trainer_class.load(resume_training) # load the saved state + return algo.resume(model=model, + train_dataset=train_dataset, + **trainer_kwargs) + if optimizer is None: optimizer = "OPROv2" if isinstance(model, ParameterNode) else "OptoPrimeV2" @@ -71,12 +83,10 @@ def forward(self, x): assert isinstance(optimizer, Optimizer) guide = load_guide(guide, **guide_kwargs) - logger = load_logger(logger, **logger_kwargs) - trainer_class = load_trainer_class(algorithm) - assert isinstance(guide, Guide) + + logger = load_logger(logger, **logger_kwargs) assert isinstance(logger, BaseLogger) - assert issubclass(trainer_class, Trainer) algo = trainer_class( model, @@ -84,11 +94,6 @@ def forward(self, x): logger=logger ) - if resume_training: - assert isinstance(resume_training, str), "resume_training must be a path string." - assert hasattr(algo, 'resume'), f"{trainer_class} does not support resume." - return algo.resume(load_path=resume_training) - return algo.train( guide=guide, train_dataset=train_dataset, diff --git a/tests/unit_tests/test_priority_search.py b/tests/unit_tests/test_priority_search.py index f5d4856b..08e7e859 100644 --- a/tests/unit_tests/test_priority_search.py +++ b/tests/unit_tests/test_priority_search.py @@ -7,7 +7,7 @@ from opto.trainer.guide import Guide from opto.utils.llm import DummyLLM -import re +import re, os import numpy as np import copy import pickle @@ -49,6 +49,7 @@ def forward(self, x): batch_size = 3 num_batches = 2 num_threads = 2 +num_epochs = 3 dataset = {'inputs': xs, 'infos': infos} num_proposals = 10 @@ -162,6 +163,7 @@ def test_priority_search(): num_candidates=num_candidates, num_proposals=num_proposals, memory_size=memory_size, + num_epochs=num_epochs, verbose=False, #'output', ) @@ -192,24 +194,66 @@ def test_resume(): save_path="./test_priority_search_save" - # algo.train( - # guide=Guide(), - # train_dataset=dataset, - # batch_size=batch_size, - # num_batches=num_batches, - # num_threads=num_threads, - # num_candidates=num_candidates, - # num_proposals=num_proposals, - # memory_size=memory_size, - # verbose=False, #'output', - # save_path=save_path, - # save_frequency=1, - # ) - - # new_algo = PrioritySearch.load(save_path) - # assert new_algo.n_iters == algo.n_iters, "Resumed algorithm should have the same number of iterations as the original." - - # new_algo.resume( - # train_dataset=dataset) - - # os.system(f"rm -rf {save_path}") + algo.train( + guide=Guide(), + train_dataset=dataset, + batch_size=batch_size, + num_batches=num_batches, + num_threads=num_threads, + num_candidates=num_candidates, + num_proposals=num_proposals, + memory_size=memory_size, + verbose=False, #'output', + save_path=save_path, + save_frequency=1, + num_epochs=num_epochs, + ) + + new_algo = PrioritySearch.load(save_path) + assert new_algo.n_iters == algo.n_iters - 1, "Resumed algorithm should have the same number of iterations as the original." + new_agent = Agent() + new_algo.resume( + model=new_agent, + train_dataset=dataset, + num_epochs=num_epochs+2) + assert new_algo.n_iters == num_epochs+2, "Resumed algorithm should have completed the additional epochs." + + os.system(f"rm -rf {save_path}") + + +def test_trainer_train_and_resume(): + + dummy_llm = DummyLLM(_llm_callable) + agent = Agent() + optimizer = OptoPrimeV2( + agent.parameters(), + llm=dummy_llm, + ) + + trainer.train( + algorithm='PrioritySearch', + model=agent, + optimizer=optimizer, + guide=Guide(), + train_dataset=dataset, + batch_size=batch_size, + num_batches=num_batches, + num_threads=num_threads, + num_candidates=num_candidates, + num_proposals=num_proposals, + memory_size=memory_size, + verbose=False, #'output', + save_path="./test_priority_search_save_trainer", + save_frequency=1, + num_epochs=num_epochs, + ) + + new_agent = Agent() + trainer.train( + algorithm='PrioritySearch', + resume_training="./test_priority_search_save_trainer", + model=new_agent, + train_dataset=dataset, + num_epochs=num_epochs+2) + + os.system(f"rm -rf ./test_priority_search_save_trainer") \ No newline at end of file From 47461f9448a7b9749c21d1a07e88cb5b4322013e Mon Sep 17 00:00:00 2001 From: chinganc Date: Sat, 13 Sep 2025 00:08:58 +0000 Subject: [PATCH 200/314] remove outdated tests --- opto/trainer/algorithms/algorithm.py | 10 +- tests/unit_tests/test_saving_loading.py | 180 ++++++++++++------------ 2 files changed, 97 insertions(+), 93 deletions(-) diff --git a/opto/trainer/algorithms/algorithm.py b/opto/trainer/algorithms/algorithm.py index c8d13775..41bba633 100644 --- a/opto/trainer/algorithms/algorithm.py +++ b/opto/trainer/algorithms/algorithm.py @@ -92,22 +92,26 @@ def train(self, ): raise NotImplementedError + def save(self, save_path: str): + raise NotImplementedError @classmethod def load(cls, load_path: str): raise NotImplementedError - def resume(self, *, model: Module, train_dataset: dict , **kwargs): raise NotImplementedError - # NOTE old code which may be useful in the future + + + + # TODO remove these old save and load methods # def save(self, path: str): - # """ Save the guide to a file. """ + # with open(path, 'wb') as f: # d = {} # for key, value in self.__dict__.items(): diff --git a/tests/unit_tests/test_saving_loading.py b/tests/unit_tests/test_saving_loading.py index 96544115..5ed934d6 100644 --- a/tests/unit_tests/test_saving_loading.py +++ b/tests/unit_tests/test_saving_loading.py @@ -59,93 +59,93 @@ def _llm_callable(messages, **kwargs): """ -def test_trainer_saving_loading(): - - - class Guide(_Guide): - - def get_feedback(self, query, response, reference=None, **kwargs): - """ - Provide feedback based on the query and response. - - Args: - query: The query to analyze. - response: The response generated by the model. - reference: Optional reference answer for comparison. - **kwargs: Additional context or parameters. - - Returns: - A tuple containing a score and feedback string. - """ - score = response == reference - feedback = "Exact match!" if score == 1.0 else "Not an exact match." - return score, feedback - - @trace.model - class Agent: - - def __init__(self): - self.param = trace.node(1., trainable=True) - self.state = 0 - - def forward(self, x): - return self.param + 1 - - - xs = [1, 2, 3, 4, 5] - infos = [1, 2, 3, 4, 5] - batch_size = 3 - num_threads = 2 # 2 - dataset = {'inputs': xs, 'infos': infos} - loader = DataLoader(dataset, batch_size=batch_size) - num_proposals = 10 - num_candidates = 5 - memory_size = 3 - - - # Create a dummy LLM and an agent - dummy_llm = DummyLLM(_llm_callable) - agent = Agent() - optimizer = OptoPrimeV2( - agent.parameters(), - llm=dummy_llm, - ) - optimizer.objective = 'fake objective' - algo = BasicSearchAlgorithm( - agent, - optimizer, - ) - - algo.train( - guide=Guide(), - train_dataset=dataset, - batch_size=batch_size, - num_threads=num_threads, - num_candidates=num_candidates, - num_proposals=num_proposals, - verbose=False, #'output', - ) - agent.param._data = 10 # to simulate a change in the agent's parameters - - algo.save('test_algo.pkl') - - - # Load the algorithm and check if it works - agent = Agent() - optimizer = OptoPrimeV2( - agent.parameters(), - llm=dummy_llm, - ) - algo2 = BasicSearchAlgorithm( - agent, - optimizer, - ) - algo2.load('test_algo.pkl') - - assert algo2.agent.param.data == 10, "Loaded agent's parameter does not match the saved one." - assert algo2.optimizer.objective == 'fake objective', "Loaded optimizer's objective does not match the saved one." - - os.remove('test_algo.pkl') - os.remove('test_algo.pkl_agent.module') - os.remove('test_algo.pkl_optimizer.optimizer') - os.remove('test_algo.pkl_validate_guide.guide') \ No newline at end of file +# def test_trainer_saving_loading(): + + +# class Guide(_Guide): + +# def get_feedback(self, query, response, reference=None, **kwargs): +# """ +# Provide feedback based on the query and response. + +# Args: +# query: The query to analyze. +# response: The response generated by the model. +# reference: Optional reference answer for comparison. +# **kwargs: Additional context or parameters. + +# Returns: +# A tuple containing a score and feedback string. +# """ +# score = response == reference +# feedback = "Exact match!" if score == 1.0 else "Not an exact match." +# return score, feedback + +# @trace.model +# class Agent: + +# def __init__(self): +# self.param = trace.node(1., trainable=True) +# self.state = 0 + +# def forward(self, x): +# return self.param + 1 + + +# xs = [1, 2, 3, 4, 5] +# infos = [1, 2, 3, 4, 5] +# batch_size = 3 +# num_threads = 2 # 2 +# dataset = {'inputs': xs, 'infos': infos} +# loader = DataLoader(dataset, batch_size=batch_size) +# num_proposals = 10 +# num_candidates = 5 +# memory_size = 3 + + +# # Create a dummy LLM and an agent +# dummy_llm = DummyLLM(_llm_callable) +# agent = Agent() +# optimizer = OptoPrimeV2( +# agent.parameters(), +# llm=dummy_llm, +# ) +# optimizer.objective = 'fake objective' +# algo = BasicSearchAlgorithm( +# agent, +# optimizer, +# ) + +# algo.train( +# guide=Guide(), +# train_dataset=dataset, +# batch_size=batch_size, +# num_threads=num_threads, +# num_candidates=num_candidates, +# num_proposals=num_proposals, +# verbose=False, #'output', +# ) +# agent.param._data = 10 # to simulate a change in the agent's parameters + +# algo.save('test_algo.pkl') + + +# # Load the algorithm and check if it works +# agent = Agent() +# optimizer = OptoPrimeV2( +# agent.parameters(), +# llm=dummy_llm, +# ) +# algo2 = BasicSearchAlgorithm( +# agent, +# optimizer, +# ) +# algo2.load('test_algo.pkl') + +# assert algo2.agent.param.data == 10, "Loaded agent's parameter does not match the saved one." +# assert algo2.optimizer.objective == 'fake objective', "Loaded optimizer's objective does not match the saved one." + +# os.remove('test_algo.pkl') +# os.remove('test_algo.pkl_agent.module') +# os.remove('test_algo.pkl_optimizer.optimizer') +# os.remove('test_algo.pkl_validate_guide.guide') \ No newline at end of file From ccff15b0658e759a8b0c63485d69008d53734f76 Mon Sep 17 00:00:00 2001 From: chinganc Date: Sat, 13 Sep 2025 00:15:21 +0000 Subject: [PATCH 201/314] refactor resume method out --- opto/trainer/__init__.py | 2 +- opto/trainer/train.py | 42 +++++++++++++++++++----- tests/unit_tests/test_priority_search.py | 4 +-- 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/opto/trainer/__init__.py b/opto/trainer/__init__.py index de4d59a8..679e14d5 100644 --- a/opto/trainer/__init__.py +++ b/opto/trainer/__init__.py @@ -1 +1 @@ -from opto.trainer.train import train \ No newline at end of file +from opto.trainer.train import train, resume \ No newline at end of file diff --git a/opto/trainer/train.py b/opto/trainer/train.py index 7b9161f2..66eb6b65 100644 --- a/opto/trainer/train.py +++ b/opto/trainer/train.py @@ -15,11 +15,43 @@ def dataset_check(dataset): assert len(dataset['inputs'])==len(dataset['infos']), "Inputs and infos must have the same length" +def resume( + save_path: str, + *, + algorithm: Union[Trainer, str] = 'MinibatchAlgorithm', + model: trace.Module, + train_dataset: dict, + validate_dataset = None, + test_dataset = None, + **kwargs): + """ Resume training from a checkpoint. + + Args: + model: the model to be trained + train_dataset: the training dataset + resume_training: path to the checkpoint + validate_dataset: the validation dataset + test_dataset: the test dataset + **kwargs: additional keyword arguments for the training method. If not provided, the same parameters as the last training call are used. + """ + dataset_check(train_dataset) + trainer_class = load_trainer_class(algorithm) + assert issubclass(trainer_class, Trainer) + assert isinstance(save_path, str), "resume_training must be a path string." + assert hasattr(trainer_class, 'resume'), f"{trainer_class} does not support resume." + assert hasattr(trainer_class, 'load'), f"{trainer_class} does not support load." + algo = trainer_class.load(save_path) # load the saved state + return algo.resume(model=model, + train_dataset=train_dataset, + validate_dataset=validate_dataset, + test_dataset=test_dataset, + **kwargs) + + def train( *, model: Union[trace.Module, ParameterNode], train_dataset: dict, - resume_training: Union[str, bool] = False, # path to load checkpoint or False # class of optimizer algorithm: Union[Trainer, str] = 'MinibatchAlgorithm', optimizer: Union[Optimizer, str] = None, @@ -47,14 +79,6 @@ def train( trainer_class = load_trainer_class(algorithm) assert issubclass(trainer_class, Trainer) - if resume_training: - assert isinstance(resume_training, str), "resume_training must be a path string." - assert hasattr(trainer_class, 'resume'), f"{trainer_class} does not support resume." - assert hasattr(trainer_class, 'load'), f"{trainer_class} does not support load." - algo = trainer_class.load(resume_training) # load the saved state - return algo.resume(model=model, - train_dataset=train_dataset, - **trainer_kwargs) if optimizer is None: optimizer = "OPROv2" if isinstance(model, ParameterNode) else "OptoPrimeV2" diff --git a/tests/unit_tests/test_priority_search.py b/tests/unit_tests/test_priority_search.py index 08e7e859..d6ac4a17 100644 --- a/tests/unit_tests/test_priority_search.py +++ b/tests/unit_tests/test_priority_search.py @@ -249,9 +249,9 @@ def test_trainer_train_and_resume(): ) new_agent = Agent() - trainer.train( + trainer.resume( + "./test_priority_search_save_trainer", algorithm='PrioritySearch', - resume_training="./test_priority_search_save_trainer", model=new_agent, train_dataset=dataset, num_epochs=num_epochs+2) From 5f30b0988f677091924dd32314ae1b004191226f Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Sat, 13 Sep 2025 17:27:21 -0500 Subject: [PATCH 202/314] add auto-retry function --- opto/features/priority_search/utils.py | 81 +++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/opto/features/priority_search/utils.py b/opto/features/priority_search/utils.py index 59749e72..0b04aae4 100644 --- a/opto/features/priority_search/utils.py +++ b/opto/features/priority_search/utils.py @@ -82,4 +82,83 @@ def create_module_from_update_dict(agent, update_dict): """ new_agent = copy.deepcopy(agent) #.copy() # create a copy of the agent set_module_parameters(new_agent, update_dict) # set the parameters of the new agent - return new_agent # return the new agent \ No newline at end of file + return new_agent # return the new agent + +def retry_with_exponential_backoff(func, max_retries=10, base_delay=1.0, operation_name="operation"): + """ + Retry a function with exponential backoff for rate limit and other transient errors. + + Args: + func: Function to retry (should be a callable with no arguments) + max_retries: Maximum number of retry attempts + base_delay: Base delay for exponential backoff + operation_name: Name of the operation for logging + + Returns: + Result of the function call + + Raises: + The last exception encountered if all retries fail + """ + import time + + for retry_attempt in range(max_retries): + try: + return func() + except Exception as e: + error_str = str(e).lower() + error_type = type(e).__name__.lower() + + # Check if it's a retryable error + retryable_errors = [ + 'rate limit', 'timeout', 'temporary', 'service unavailable', + 'internal server error', 'bad gateway', 'service temporarily unavailable', + 'too many requests', 'quota', 'overloaded', 'resource has been exhausted', + 'resource_exhausted', 'ratelimiterror', 'quotaexceedederror', + 'connection error', 'network', 'json decode' + ] + + # Also check specific litellm exceptions + retryable_exception_types = [ + 'ratelimiterror', 'timeouterror', 'apiconnectionerror', + 'serviceunavailableerror', 'internalservererror', 'jsondecodeerror' + ] + + is_retryable = ( + any(err in error_str for err in retryable_errors) or + any(exc_type in error_type for exc_type in retryable_exception_types) or + 'code": 429' in error_str or # HTTP 429 Too Many Requests + 'code": 503' in error_str or # HTTP 503 Service Unavailable + 'code": 502' in error_str or # HTTP 502 Bad Gateway + 'code": 500' in error_str # HTTP 500 Internal Server Error + ) + + if retry_attempt == max_retries - 1: + # Last attempt failed + raise RuntimeError(f"{operation_name}: Failed after {max_retries} attempts. Error: {e}") + + elif is_retryable: + # Special handling for rate limit errors - use longer delays + is_rate_limit = ( + 'rate limit' in error_str or 'ratelimiterror' in error_type or + 'quota' in error_str or 'resource has been exhausted' in error_str or + 'code": 429' in error_str + ) + + if is_rate_limit: + # Longer delays for rate limits: 2, 8, 18, 32, 50 seconds + delay = 2 * (retry_attempt + 1) ** 2 + retry_attempt + else: + # Standard exponential backoff for other errors + delay = base_delay * (2 ** retry_attempt) + (0.1 * retry_attempt) + + error_type_desc = "Rate limit" if is_rate_limit else "Retryable error" + # print(f"{operation_name}: {error_type_desc} - Retry {retry_attempt + 1}/{max_retries} after {delay:.1f}s. Error: {e}") + time.sleep(delay) + else: + # Non-retryable error + print(f"{operation_name}: Non-retryable error: {e}") + raise e + + # This should never be reached, but just in case + raise RuntimeError(f"{operation_name}: Unexpected error - reached end of retry loop") \ No newline at end of file From ca4513de4e5aee42d2e779c58db35934e434998e Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Sat, 13 Sep 2025 23:31:15 -0500 Subject: [PATCH 203/314] add embedding logistic regressor --- .../priority_search/module_regressor.py | 335 ++++++++++++++++++ 1 file changed, 335 insertions(+) create mode 100644 opto/features/priority_search/module_regressor.py diff --git a/opto/features/priority_search/module_regressor.py b/opto/features/priority_search/module_regressor.py new file mode 100644 index 00000000..b9a2fea9 --- /dev/null +++ b/opto/features/priority_search/module_regressor.py @@ -0,0 +1,335 @@ +import numpy as np +import copy +from typing import Union +from opto.trainer.loader import DataLoader +from opto.trainer.utils import batch_run, async_run +from opto.optimizers.utils import print_color +# from opto.trainer.evaluators import evaluate +from typing import Union, List, Tuple, Dict, Any, Optional +from collections import deque +from opto.utils.llm import LLM # For the selector LLM +# from opto.trace.nodes import ParameterNode +import json +# import warnings +# from black import format_str, FileMode +import random +# import mathX +from opto.features.priority_search.utils import retry_with_exponential_backoff +import litellm +import time + +class ModuleCandidateRegressor: + """ + Predict scores using embedding logistic regression for ModuleCandidate objects. + Should have two key methods: predict_scores and predict_scores_for_batch. + predict_scores has no parameters, it could return predicted scores for all candidates in the memory. + predict_scores_for_batch has one parameter, a batch of candidates, it could return predicted scores for the batch of candidates.""" + + def __init__(self, memory=None, embedding_model="gemini/text-embedding-004", num_threads=None, learning_rate=0.2, regularization_strength=1e-4, max_iterations=20000, tolerance=5e-3): + # In the regressor, no need for calling LLM to make the prediction. So we could predict the entire memory at once. + self.max_candidates_to_predict = 500 + self.memory = memory + self.embedding_model = embedding_model + self.num_threads = num_threads + self.learning_rate = learning_rate + self.initial_learning_rate = learning_rate + self.regularization_strength = regularization_strength # L2 regularization strength (lambda) + self.max_iterations = max_iterations + self.tolerance = tolerance + self.patience = 20 # Early stopping patience + self.lr_decay_factor = 0.8 # Learning rate decay factor + # default linear dimension is 768 + self.linear_dim = 768 + # Initialize weights with larger values for more aggressive learning + self.weights = np.random.normal(0, 0.1, self.linear_dim) + self.bias = 0.0 + + def _sigmoid(self, z): + """Sigmoid activation function for logistic regression.""" + return 1.0 / (1.0 + np.exp(-z)) + + def _get_parameter_text(self, candidate): + """Get the parameter text for a ModuleCandidate.""" + if not candidate.update_dict: + # If update_dict is empty, use a default text or base module info + return "base_module_parameters" + + # Get the first value from update_dict (similar to additional_instructions) + # TODO: support for multiple parameters + parameter_text = list(candidate.update_dict.values())[0] + return str(parameter_text) + + def _get_embedding(self, candidate): + """Get the embedding for a ModuleCandidate.""" + parameter_text = self._get_parameter_text(candidate) + + def single_embedding_call(): + return litellm.embedding( + model=self.embedding_model, + input=parameter_text + ) + + try: + response = retry_with_exponential_backoff( + single_embedding_call, + max_retries=10, + base_delay=1.0, + operation_name="Embedding API call" + ) + embedding = response.data[0].embedding + return embedding + except Exception as e: + print_color(f"ERROR: Embedding API call failed after retries: {e}", "red") + # Return a random embedding as fallback to prevent complete failure + print_color("Using random embedding as fallback", "yellow") + fallback_embedding = np.random.normal(0, 0.01, self.linear_dim) + return fallback_embedding / np.linalg.norm(fallback_embedding) + + def _update_memory_embeddings(self): + """Update the embeddings for all candidates in memory.""" + for neg_score, candidate in self.memory: + if hasattr(candidate, "embedding"): + continue + candidate.embedding = self._get_embedding(candidate) + + def _update_regression_model(self): + """Update the regression model using the current memory with logistic regression.""" + start_time = time.time() + print_color("Updating regression model using the current memory with logistic regression...", "blue") + self._update_memory_embeddings() + + # Get training data from memory (only candidates with rollout data) + training_candidates = [candidate for neg_score, candidate in self.memory if candidate.num_rollouts > 0] + + if len(training_candidates) == 0: + print_color("Warning: No training data available for regression model.", "yellow") + end_time = time.time() + elapsed_time = end_time - start_time + print_color(f"_update_regression_model completed in {elapsed_time:.4f} seconds (no training data)", "cyan") + return + + # Extract raw binary training data from each candidate + X_list = [] + y_list = [] + + for candidate in training_candidates: + embedding = candidate.embedding + eval_count = candidate.num_rollouts + mean_score = candidate.mean_score() + + if mean_score is None: + continue + + # Calculate score_sum from mean_score and eval_count + # Assuming scores are binary (0 or 1), score_sum = mean_score * eval_count + score_sum = mean_score * eval_count + + # score_sum directly represents the number of successes + num_successes = int(round(score_sum)) + num_failures = eval_count - num_successes + + # Ensure non-negative values + num_successes = max(0, num_successes) + num_failures = max(0, num_failures) + + # Create binary training samples: 1 for success, 0 for failure + for _ in range(num_successes): + X_list.append(embedding) + y_list.append(1.0) + + for _ in range(num_failures): + X_list.append(embedding) + y_list.append(0.0) + + if len(X_list) == 0: + print_color("Warning: No binary training samples generated.", "yellow") + end_time = time.time() + elapsed_time = end_time - start_time + print_color(f"_update_regression_model completed in {elapsed_time:.4f} seconds (no binary samples)", "cyan") + return + + # Convert to numpy arrays + X = np.array(X_list) + y = np.array(y_list) + + # Ensure X has the right dimensions + if X.shape[1] != self.linear_dim: + self.linear_dim = X.shape[1] + # Initialize weights with larger values for more aggressive learning + self.weights = np.random.normal(0, 0.1, self.linear_dim) + + # Convergence-based regularized logistic regression training using all raw binary data + m = len(X_list) + # print_color(f"Training regularized logistic regression with {m} binary samples from {len(training_candidates)} candidates until convergence.", "blue") + # print_color(f"Using L2 regularization strength: {self.regularization_strength}, learning rate: {self.learning_rate}", "blue") + # print_color(f"Max iterations: {self.max_iterations}, tolerance: {self.tolerance}", "blue") + + # Debug: Print initial weight statistics + initial_weight_norm = np.linalg.norm(self.weights) + # print_color(f"Initial weight norm: {initial_weight_norm:.6f}", "yellow") + + # Debug: Print embedding statistics + embedding_mean = np.mean(X) + embedding_std = np.std(X) + embedding_norm_mean = np.mean([np.linalg.norm(row) for row in X]) + # print_color(f"Embedding stats - mean: {embedding_mean:.6f}, std: {embedding_std:.6f}, avg norm: {embedding_norm_mean:.6f}", "yellow") + + # Training loop until convergence with adaptive learning rate and early stopping + prev_cost = float('inf') + best_cost = float('inf') + converged = False + iteration = 0 + patience_counter = 0 + + # Reset learning rate + self.learning_rate = self.initial_learning_rate + + for iteration in range(self.max_iterations): + # Forward pass + z = X.dot(self.weights) + self.bias + predictions = self._sigmoid(z) + + # Compute cost with L2 regularization + epsilon = 1e-15 # Small value to prevent log(0) + predictions_clipped = np.clip(predictions, epsilon, 1 - epsilon) + log_likelihood = -np.mean(y * np.log(predictions_clipped) + (1 - y) * np.log(1 - predictions_clipped)) + l2_penalty = self.regularization_strength * np.sum(self.weights ** 2) + total_cost = log_likelihood + l2_penalty + + # Check for improvement and early stopping + cost_change = abs(prev_cost - total_cost) + if total_cost < best_cost: + best_cost = total_cost + patience_counter = 0 + else: + patience_counter += 1 + + # Backward pass (compute gradients with L2 regularization) + dw = (1/m) * X.T.dot(predictions - y) + 2 * self.regularization_strength * self.weights + db = (1/m) * np.sum(predictions - y) + gradient_norm = np.linalg.norm(dw) + + # Check convergence criteria (stricter) + if cost_change < self.tolerance and gradient_norm < self.tolerance: + converged = True + print_color(f"Converged at iteration {iteration + 1}: cost change {cost_change:.10f}, gradient norm {gradient_norm:.10f}", "green") + break + + # Early stopping if no improvement + if patience_counter >= self.patience: + print_color(f"Early stopping at iteration {iteration + 1}: no improvement for {self.patience} iterations", "yellow") + break + + # Adaptive learning rate: decay if no improvement for several iterations + if patience_counter > 0 and patience_counter % 10 == 0: + self.learning_rate *= self.lr_decay_factor + print_color(f"Reducing learning rate to {self.learning_rate:.6f}", "yellow") + + # Update parameters + self.weights -= self.learning_rate * dw + self.bias -= self.learning_rate * db + + # Print progress periodically + # if iteration == 0 or (iteration + 1) % max(1, min(50, self.max_iterations // 20)) == 0: + # z_mean, z_std = np.mean(z), np.std(z) + # weight_norm = np.linalg.norm(self.weights) + # print_color(f"Iteration {iteration + 1}: Cost: {total_cost:.6f} (change: {cost_change:.8f}), LR: {self.learning_rate:.6f}, Weight norm: {weight_norm:.6f}, Gradient norm: {gradient_norm:.8f}", "cyan") + # print_color(f" Logits - mean: {z_mean:.6f}, std: {z_std:.6f}, range: [{np.min(z):.6f}, {np.max(z):.6f}]", "cyan") + # print_color(f" Predictions - range: [{np.min(predictions):.6f}, {np.max(predictions):.6f}], mean: {np.mean(predictions):.6f}", "cyan") + # print_color(f" Patience: {patience_counter}/{self.patience}", "cyan") + + prev_cost = total_cost + + # Final status + if converged: + print_color(f"Logistic regression converged after {iteration + 1} iterations. Final cost: {total_cost:.6f} (Log-likelihood: {log_likelihood:.6f}, L2 penalty: {l2_penalty:.6f}), bias: {self.bias:.6f}", "green") + else: + print_color(f"Logistic regression reached max iterations ({self.max_iterations}). Final cost: {total_cost:.6f} (Log-likelihood: {log_likelihood:.6f}, L2 penalty: {l2_penalty:.6f}), bias: {self.bias:.6f}", "yellow") + + # Print timing information + end_time = time.time() + elapsed_time = end_time - start_time + print_color(f"_update_regression_model completed in {elapsed_time:.4f} seconds", "cyan") + + def _predict_single(self, candidate): + """Predict a single score for a ModuleCandidate using the logistic regression model. Using the entire memory as the training data.""" + self._update_regression_model() + + embedding = self._get_embedding(candidate) + z = self.weights.dot(embedding) + self.bias + predicted_score = self._sigmoid(z) + return predicted_score + + def predict_scores_for_batch(self, batch): + """Predict scores for a batch of ModuleCandidates and update each with the predicted scores. Using the entire memory as the training data.""" + # Get embeddings for all candidates in batch + embeddings = [] + + # Separate candidates that need embeddings from those that already have them + candidates_needing_embeddings = [] + for candidate in batch: + if not hasattr(candidate, "embedding"): + candidates_needing_embeddings.append(candidate) + embeddings.append(None) # Placeholder + + # Generate embeddings in parallel for candidates that need them + if candidates_needing_embeddings: + def get_embedding_for_candidate(candidate): + return self._get_embedding(candidate) + + # Create function list for async_run + embedding_functions = [lambda c=candidate: get_embedding_for_candidate(c) + for candidate in candidates_needing_embeddings] + + # Run embedding generation in parallel + new_embeddings = async_run( + embedding_functions, + max_workers=1000, + description=f"Generating embeddings for {len(candidates_needing_embeddings)} candidates" + ) + + # Assign embeddings back to candidates + for candidate, embedding in zip(candidates_needing_embeddings, new_embeddings): + candidate.embedding = embedding + + # Collect all embeddings in order + embeddings = [] + for candidate in batch: + embeddings.append(candidate.embedding) + + self._update_regression_model() + + # Batch prediction using vectorized operations + X_batch = np.array(embeddings) + z = X_batch.dot(self.weights) + self.bias + predicted_scores = self._sigmoid(z) + + # Update each candidate with predicted score as attribute + for candidate, predicted_score in zip(batch, predicted_scores): + candidate.predicted_score = predicted_score + + return predicted_scores + + def predict_scores(self): + """Predict scores for all candidates in the memory. Using the entire memory as the training data.""" + # Extract all candidates from memory (memory is a list of (neg_score, candidate) tuples) + memory_candidates = [candidate for neg_score, candidate in self.memory] + + batches = [memory_candidates[i:i+self.max_candidates_to_predict] for i in range(0, len(memory_candidates), self.max_candidates_to_predict)] + + if hasattr(self, 'num_threads') and self.num_threads and self.num_threads > 1: + # Parallelize batch processing + batch_functions = [lambda batch=b: self.predict_scores_for_batch(batch) for b in batches] + async_run( + batch_functions, + max_workers=self.num_threads, + description=f"Processing {len(batches)} candidate batches" + ) + else: + # Sequential processing + for batch in batches: + self.predict_scores_for_batch(batch) + + # Return the predicted scores for the memory candidates + predicted_scores_for_the_memory = [candidate.predicted_score for candidate in memory_candidates] + return np.array(predicted_scores_for_the_memory) From 97691fb6a3f65f085f094ec0cf306bdee20451ea Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Sat, 13 Sep 2025 23:31:33 -0500 Subject: [PATCH 204/314] modify priority_search to run --- .../priority_search_modified.py | 710 ++++++++++++++++++ 1 file changed, 710 insertions(+) create mode 100644 opto/features/priority_search/priority_search_modified.py diff --git a/opto/features/priority_search/priority_search_modified.py b/opto/features/priority_search/priority_search_modified.py new file mode 100644 index 00000000..c4404432 --- /dev/null +++ b/opto/features/priority_search/priority_search_modified.py @@ -0,0 +1,710 @@ +import numpy as np +import copy +import heapq +import time +from typing import Union, List, Tuple, Dict, Any, Optional +from opto import trace +from opto.trace.nodes import ParameterNode +from opto.optimizers.optimizer import Optimizer +from opto.trainer.utils import async_run +from opto.trainer.algorithms.basic_algorithms import batchify +from opto.features.priority_search.search_template import SearchTemplate, Samples, BatchRollout +from opto.features.priority_search.utils import set_module_parameters, remap_update_dict, create_module_from_update_dict, is_module_copy +from opto.features.priority_search.module_regressor import ModuleCandidateRegressor + + +class ModuleCandidate: + """ A container used by PrioritySearch to store a candidate module as (its base module and update dictionary) and its statistics. """ + + def __init__(self, + base_module: trace.Module, + update_dict: Optional[Dict[ParameterNode, Any]] = None, + optimizer: Optimizer = None, + ): + """ A candidate module with its base module and update dictionary. + Args: + base_module (trace.Module): The base module to use as a template for the candidate. + update_dict (dict): A dictionary of ParameterNode: value pairs to update the base module; the key can be a deep copy of the base module's parameters. + stats (dict): A dictionary of statistics about the candidate. + """ + assert isinstance(base_module, trace.Module), "base_module must be a trace.Module." + if update_dict is not None: + assert isinstance(optimizer, Optimizer), "optimizer must be an instance of Optimizer when update_dict is provided." + + self.base_module = base_module + self.update_dict = update_dict if update_dict is not None else {} + self.optimizer = optimizer # the optimizer used to generate the update_dict; can be None, which indicates the base_module is used. + self.update_dict = remap_update_dict(self.base_module, self.update_dict) + self.rollouts = [] # list of dicts containing the rollout information (not BatchRollout, but a list of dicts) + self.created_time = time.time() + + def get_module(self): + """ Apply the update_dict to the base_module and return the updated module. + A new module is always created so the base_module is not modified. + The new module has a new attribute _module_candidate which is this candidate.""" + module = create_module_from_update_dict(self.base_module, self.update_dict) if self.update_dict else copy.deepcopy(self.base_module) # + setattr(module, '__TRACE_RESERVED_module_candidate_id', id(self)) + return module # return the updated module + + def apply_update(self, base_module=None): + """ Apply update to the base_module in place. """ + set_module_parameters(base_module or self.base_module, self.update_dict) + + def __getstate__(self): + """ Get the state of the candidate for serialization. """ + state = copy.deepcopy(self.__dict__) # this will detach the nodes from the computation graph + return state + + def __setstate__(self, state): + """ Set the state of the candidate from serialization. """ + self.__dict__.update(state) + + def __deepcopy__(self, memo): + """ Create a deep copy, except for the base_module which is not copied, it is the original module. """ + cls = self.__class__ + result = cls.__new__(cls) + memo[id(self)] = result + for k, v in self.__dict__.items(): + if k != 'base_module': + setattr(result, k, copy.deepcopy(v, memo)) + else: + setattr(result, k, v) # base_module is not copied, it is the original module + return result + + def __eq__(self, other): + """ Check if two candidates are equal based on their base_module and update_dict. """ + assert isinstance(other, ModuleCandidate), "other must be an instance of ModuleCandidate." + return (self.update_dict == other.update_dict) and is_module_copy(self.base_module, other.base_module) and (id(self.optimizer) == id(other.optimizer)) + + def __lt__(self, other): + """ Compare two candidates based on their update_dict. """ + assert isinstance(other, ModuleCandidate), "other must be an instance of ModuleCandidate." + return self.created_time > other.created_time + # self < other if, self is created later than other + # Since we will use minheap, and this would give priority to later created candidates in the heap memory. + + def __hash__(self): + """ Hash the candidate based on its update_dict. """ + return hash((frozenset(self.update_dict.items()), id(self.optimizer), id(self.base_module))) + + def add_rollouts(self, rollouts: List[Dict[str, Any]]): + """ Add rollouts to the candidate. """ + assert isinstance(rollouts, list), "rollouts must be a list of dicts." + assert all(isinstance(r, dict) for r in rollouts), "All rollouts must be dicts." + # Each rollout is a dict with keys: 'module', 'x', 'info', 'target', 'score', 'feedback' + assert all('module' in r and 'x' in r and 'info' in r and 'target' in r and 'score' in r and 'feedback' in r for r in rollouts), \ + "Each rollout must contain 'module', 'x', 'info', 'target', 'score', and 'feedback' keys." + + self.rollouts.extend(rollouts) + + def mean_score(self): + """ Compute the score of the candidate based on the rollouts. """ + if not self.rollouts: + return None + scores = [r['score'] for r in self.rollouts] + return np.mean(scores) if scores else None + + def compute_score_confidence(self, min_score, max_score, scaling_constant=1.0, total_trials=1): + """Compute the UCB, mean, LCB score for the candidate. After queried, the number of confidence queries is incremented. + + UCB = mean_score + scaling_constant * sqrt(ln(total_trials) / n_scores) * (max_score - min_score) + UCB = clip(UCB, min_score, max_score) + + LCB = mean_score - scaling_constant * sqrt(ln(total_trials) / n_scores) * (max_score - min_score) + LCB = clip(LCB, min_score, max_score) + + Args: + min_score (float): The minimum score for clipping. + max_score (float): The maximum score for clipping. + scaling_constant (float): The scaling constant for the exploration term. + total_trials (int): The total number of trials conducted. Must be at least 1. + Returns: + lcb_score (float): The lower confidence bound score. + mean_score (float): The mean score. + ucb_score (float): The upper confidence bound score. + """ + # Get scores from rollouts + scores = [r['score'] for r in self.rollouts] + + if not scores: + return min_score, None, max_score + + # Calculate mean score for this candidate + mean_score = np.mean(scores) + n_scores = len(scores) + assert n_scores == self.num_rollouts, "Number of scores should match number of rollouts." + + # Calculate how many times the confidence interval has been used to form a union bound + assert total_trials >= 1, "total_trials must be at least 1." + total_trials = total_trials + 1 # this is an upper bound, since log(1) = 0 + + # Compute the exploration term based on Hoeffding's inequality + exploration_term = scaling_constant * np.sqrt(np.log(total_trials) / n_scores) * (max_score - min_score) + + # Calculate UCB score + ucb_score = mean_score + exploration_term + ucb_score = np.clip(ucb_score, min_score, max_score) + + # Calculate LCB score + lcb_score = mean_score - exploration_term + lcb_score = np.clip(lcb_score, min_score, max_score) + + return lcb_score, mean_score, ucb_score + + + @property + def num_rollouts(self): + """ Return the number of rollouts collected for this candidate. """ + return len(self.rollouts) + + +class HeapMemory: + # This is a basic implementation of a heap memory that uses a priority queue to store candidates. + # Later on this will be replaced by a memory DB. + + # NOTE that the heap memory is a max-heap, so we store negative scores to use the default min-heap behavior of heapq. + def __init__(self, size=None): + """ Initialize an empty heap memory. """ + self.memory = [] + self.size = size # Optional size limit for the heap memory + + def push(self, score, data): + """ Push an item to the heap memory. """ + heapq.heappush(self.memory, (-score, data)) + if self.size is not None and len(self.memory) > self.size: + # NOTE a heuristic for now + self.memory = self.memory[:self.size] # Keep only the top `size` items + + def pop(self): + """ Pop the top item from the heap memory. """ + if not self.memory: + raise IndexError("pop from an empty heap memory") + return heapq.heappop(self.memory) + + def __len__(self): + """ Return the number of items in the heap memory. """ + return len(self.memory) + + def __bool__(self): + """ Return True if the heap memory is not empty, False otherwise. """ + return len(self.memory) > 0 + + def __iter__(self): + """ Iterate over the items in the heap memory. """ + return iter(self.memory) + + def best(self, criterion=None): + """ Return the best item in the heap memory without removing it. + + If criterion is None, return the item with the highest priority (lowest negative score). + If criterion is a callable function, return the item that maximizes the criterion. + """ + if not self.memory: + raise IndexError("best from an empty heap memory") + if criterion is None: + return self.memory[0] # return the item with the highest priority (lowest negative score) + else: + assert callable(criterion), "criterion must be a callable function." + def _criterion(x): + neg_score, candidate = x + p = criterion(candidate) + return p if p is not None else 0 + return max(self.memory, key=lambda x: _criterion(x)) + + def reorder_according_to_predicted_scores(self): + """ Reorder the heap memory according to the predicted scores. """ + # Now all ModuleCandidate objects in the heap memory have predicted scores. Should modify the old score to the negative predicted scores, then use heapq.heapify to reorder the heap memory. + self.memory = [(-candidate.predicted_score, candidate) for _, candidate in self.memory] + heapq.heapify(self.memory) + +# TODO check saving and loading +class PrioritySearch(SearchTemplate): + """ A search algorithm that uses a priority queue to explore the parameter space and propose new candidates. + + It provides a scalable template for implementing search algorithms based on asynchronous generation, validation, and testing. + In each iteration, + 1. It proposes a best agent and a set of `num_candidates` exploration agents that have the highest scores in the priority queue. + 2. The best agent is tested for performance if eval_frequency is met. + 3. `num_batches` minibatches of `batch_size` samples are drawn from the training dataset, and the exploration agents are run on the samples. This creates a set of agent rollouts, where each rollout contains the agent module, input, info, target, score, and feedback. For each agent, rollouts of each minibatch are grouped together as a connected subgraph (represented as the BatchRollout object). In total, this step creates `num_candidates * num_batches` subgraphs. + 4. Optimizer is run on each subgraph to propose new parameters for the agents. `num_proposals` proposals are generated for each subgraph. This results in `num_subgraphs * num_proposals` total proposals. + 5. The proposed parameters are validated by running the agents on the validation dataset, which can be the current batch or a separate validation dataset when provided. When validate_exploration_candidates is set to True, the exploration candidates are also validated. + 6. The validation results are used to update the priority queue, which stores the candidates and their scores. The candidates are stored as ModuleCandidate objects, which contain the base module, update dictionary, and rollouts (i.e. raw statistics of the candidate). + + This algorithm template can be subclassed to implement specific search algorithms by overriding the `exploit`, `explore`, and `compute_exploration_priority` methods. + The `exploit` method is used to select the best candidate from the priority queue, the `explore` method is used to generate new candidates from the priority queue, and + the `compute_exploration_priority` method is used to compute the score for ranking in the priority queue. + + By default, `compute_exploration_priority` computes the mean score of the rollouts. `exploit` simply returns the candidate with highest priority from the priority queue, and `explore` generates the top `num_candidates` candidates from the priority queue. + + + `compute_exploration_priority`, `compute_exploitation_priority` can be overridden to implement different strategies for computing the priority and selecting the best candidate. + """ + + def train(self, + guide, # guide to provide feedback + train_dataset, # dataset of (x, info) pairs to train the agent + *, + # validation + validate_dataset = None, # same format as train_dataset; if None, use the current batch. + validate_guide = None, # to provide scores for the validation set + # training loop + batch_size = 1, # batch size for updating the agent + num_batches = 1, # number of batches to use from the dataset in each iteration + score_range = None, # minimum score to update the agent + num_epochs = 1, # number of training epochs + num_threads = None, # maximum number of threads to use + verbose = False, # whether to print the output of the agent + # evaluation + test_dataset = None, # dataset of (x, info) pairs to evaluate the agent + test_frequency: Union[int, None] = 1, # frequency of evaluation (set it to be negative to skip the first evaluation) + num_eval_samples: int = 1, # number of times to evaluate each input; when greater than 1, the scores are averaged. + # logging + log_frequency = None, # frequency of logging + save_frequency: Union[int, None] = None, # frequency of saving the agent + save_path: str = "checkpoints/agent.pkl", # path to save the agent + # Priority Search specific parameters + num_candidates: int = 10, # number of candidates to propose for exploration + num_proposals: int = 1, # number of proposals to generate per optimizer + validate_exploration_candidates: bool = True, # whether to validate the proposed parameters for exploration + use_best_candidate_to_explore: bool = True, # whether to use the best candidate as part of the exploration candidates + memory_size: Optional[int] = None, # size of the heap memory to store the candidates; if None, no limit is set + score_function: str = 'mean', # function to compute the score for the candidates; 'mean' or 'ucb' + ucb_exploration_constant: float = 1.0, # exploration constant for UCB score function + # Additional keyword arguments + **kwargs + ): + """ Train the agent using the Priority Search algorithm. + + Args: + guide (callable): A function that provides feedback for the agent. + train_dataset (list): A list of (x, info) pairs to train the agent. + validate_dataset (list, optional): A list of (x, info) pairs to validate the proposed candidates. If None, the current batch is used. Defaults to None. + validate_guide (callable, optional): A function that provides feedback for the validation set. If None, the training guide is used. Defaults to None. + batch_size (int, optional): The batch size for updating the agent. Defaults to 1. + num_batches (int, optional): The number of batches to use from the dataset in each iteration. Defaults to 1. + score_range (tuple, optional): A tuple of (min_score, max_score) to clip the scores. If None, no clipping is applied. Defaults to None. + num_epochs (int, optional): The number of training epochs. Defaults to 1. + num_threads (int, optional): The maximum number of threads to use. If None, it uses the number of CPU cores. Defaults to None. + verbose (bool, optional): Whether to print the output of the agent. Defaults to False. + test_dataset (list, optional): A list of (x, info) pairs to evaluate the agent. If None, no evaluation is performed. Defaults to None. + test_frequency (int or None, optional): The frequency of evaluation. If None, no evaluation is performed. If negative, skips the first evaluation. Defaults to 1. + num_eval_samples (int, optional): The number of times to evaluate each input; when greater than 1, the scores are averaged. Defaults to 1. + log_frequency (int or None, optional): The frequency of logging. If None, no logging is performed. Defaults to None. + save_frequency (int or None, optional): The frequency of saving the agent. If None, no saving is performed. Defaults to None. + save_path (str, optional): The path to save the agent. Defaults to "checkpoints/agent.pkl". + num_candidates (int, optional): The number of candidates to propose for exploration. Defaults to 10. + num_proposals (int, optional): The number of proposals to generate per optimizer. Defaults to 1. + validate_exploration_candidates (bool, optional): Whether to validate the proposed parameters for exploration. Defaults to True. + use_best_candidate_to_explore (bool, optional): Whether to use the best candidate as part of the exploration candidates. Defaults to True. + memory_size (int, optional): The size of the heap memory to store the candidates. If None, no limit is set. Defaults to None. + score_function (str, optional): The function to compute the score for the candidates; 'mean' or 'ucb'. Defaults to 'mean'. + ucb_exploration_constant (float, optional): The exploration constant for UCB score function. Defaults to 1.0. + **kwargs: Additional keyword arguments that may be used by the implementation. + """ + + + # Create agents and optimizers for search + if num_candidates < len(self._optimizers): + print(f"Warning: num_candidates {num_candidates} is less than the number of optimizers {len(self._optimizers)}. Setting num_candidates to {len(self._optimizers)}.") + num_candidates = len(self._optimizers) + self.num_candidates = num_candidates # number of candidates for exploration + self.num_proposals = num_proposals # number of candidates to propose by each optimizer call + + self.validate_exploration_candidates = validate_exploration_candidates # whether to validate the proposed parameters + self.use_best_candidate_to_explore = use_best_candidate_to_explore + self.score_function = score_function # function to compute the score for the candidates + if score_range is None: + score_range = (0, 1) + if score_function == 'ucb': # this requires a bounded score range. By default, it is set to (0, 1) + assert score_range[1]-score_range[0] < float('inf'), \ + "For UCB score function, score_range must be finite. Use 'mean' score function if you want to use unbounded scores." + + self.ucb_exploration_constant = ucb_exploration_constant + self._exploration_candidates = None # This stores the latest candidates used for exploration + self._best_candidate = None # This stores the latest best candidate used for exploitation + + self.memory = HeapMemory(size=memory_size) # Initialize the heap memory with a size limit + self.regressor = ModuleCandidateRegressor(memory=self.memory) # Initialize the + + super().train(guide=guide, + train_dataset=train_dataset, + validate_dataset=validate_dataset, + validate_guide=validate_guide, + batch_size=batch_size, + num_batches=num_batches, + score_range=score_range, + num_epochs=num_epochs, + num_threads=num_threads, + verbose=verbose, + test_dataset=test_dataset, + eval_frequency=test_frequency, + num_eval_samples=num_eval_samples, + log_frequency=log_frequency, + save_frequency=save_frequency, + save_path=save_path, + **kwargs) + + def update(self, + samples: Union[Samples, None] = None, + verbose: bool = False, + **kwargs): #-> Tuple[Dict[ParameterNode, Any], List[trace.Module], Dict[str, Any]]: + """ Update the agent using the collected samples. + """ + + # samples is None in the first iteration + if samples is not None: + # 1. Propose new parameters based on running LLM optimizers on the collected samples + from opto.features.priority_search.utils import retry_with_exponential_backoff + candidates = retry_with_exponential_backoff( + lambda: self.propose(samples, verbose=verbose, **kwargs), + max_retries=10, + base_delay=1.0, + operation_name="propose_new_parameters" + ) # List of ModuleCandidates + # # 2. Validate the proposed parameters + validate_results = self.validate(candidates, samples, verbose=verbose, **kwargs) # this updates the priority queue + # # 3. Update the priority queue with the validation results + self.update_memory(validate_results, verbose=verbose, **kwargs) # samples are provided here in case candidates do not capture full information + + else: # The first iteration. + max_mem_size = self.memory.size if self.memory.size is not None else float('inf') + initial_update_dict = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} + while len(self.memory) < min(max_mem_size, self.num_candidates): + self.memory.push(self.max_score, ModuleCandidate(self.agent, initial_update_dict, optimizer=self.optimizer)) # Push the base agent as the first candidate (This gives the initialization of the priority queue) + self.update_memory_with_regressor() + # 4. Explore and exploit the priority queue + self._best_candidate, info_exploit = self.exploit(verbose=verbose, **kwargs) # get the best candidate (ModuleCandidate) from the priority queue + self._exploration_candidates, info_explore = self.explore(verbose=verbose, **kwargs) # List of ModuleCandidates + # TODO Log information about the update + info_log = { + 'n_iters': self.n_iters, # number of iterations + } + + info_log.update(info_exploit) # add the info from the exploit step + info_log.update(info_explore) # add the info from the explore step + return self._best_candidate.update_dict, [c.get_module() for c in self._exploration_candidates], info_log + + + ## Illustration of `propose`` + # Suppose we have 2 exploration candidates. + # exploration_candidates = [candidate(param1, optimizer_1), candidate(param2, optimizer_2)] + # and two batches are collected by sampler. + # + # In samples returned by sampler, we have data + # module(param1_copy1), batch_1 + # module(param1_copy2), batch_2 + # module(param2_copy1), batch_1 + # module(param2_copy2), batch_2 + # + # We first match the samples with the exploration candidates as + # candidate_batchrollouts_list = + # [ (candidate(param1, optimizer_1), batch_1), (candidate(param1, optimizer_1), batch_2), + # (candidate(param2, optimizer_2), batch_1), (candidate(param2, optimizer_2), batch_2) ] + # + # In backward, we create deepcopies of the optimizers for each batch, and run backward asynchronously. + # optimizer_1_copy_1(param1) <- feedback from batch_1 + # optimizer_1_copy_2(param1) <- feedback from batch_2 + # optimizer_2_copy_1(param2) <- feedback from batch_1 + # optimizer_2_copy_2(param2) <- feedback from batch_2 + # + # In step, we further create deepcopies of the optimizers for each proposal, and run step asynchronously. + # for n_proposals = 2, we have + # optimizer_1_copy_1_copy_1(param1) -> proposal_1 + # optimizer_1_copy_1_copy_2(param1) -> proposal_2 + # ... + # optimizer_2_copy_2_copy_1(param2) -> proposal_7 + # optimizer_2_copy_2_copy_2(param2) -> proposal_8 + # which form the new candidate list returned by `propose`. + # + def propose(self, + samples : Samples, + verbose : bool = False, + **kwargs): + """ Analyzing samples and propose new parameters using self.optimizer. An independent optimizer is used for the minibatch generated by one agent and generates n_proposals proposals. + + Args: + samples (Samples): Samples collected by the exploration candidates. If None, the agent's parameters are returned without updating. + verbose (bool, optional): Whether to print verbose output. Defaults to False. + **kwargs: Additional keyword arguments that may be used by the implementation. + + Returns: + candidates (list of ModuleCandidate): A list of proposed candidates for the next iteration. + """ + print("--- Proposing new parameters...") if verbose else None + assert isinstance(samples, Samples), "samples must be an instance of Samples." + samples = samples.samples # list of BatchRollout objects + n_proposals = self.num_proposals # number of proposals to generate per optimizer + + # Associate each BatchRollout with self._exploration_candidates + matched_candidates_and_samples = self.match_candidates_and_samples(self._exploration_candidates, samples) + # NOTE len(matched_candidates_and_samples) <= len(self._exploration_candidates) since some exploration candidates might be duplicated. + candidate_batchrollouts_list = [ (k,b) for k, v in matched_candidates_and_samples.items() for b in v] + assert len(samples) == len(candidate_batchrollouts_list), "All samples must be associated with exploration candidates." + n_batches = len(samples) # number of batch rollouts in the samples + + # need to copy optimizer for the n_batches + def _backward(n): + candidate, rollouts = candidate_batchrollouts_list[n] + optimizer = candidate.optimizer or self.optimizer + # Create a copy of the optimizer to avoid modifying the original one and to allow parallel execution + optimizer = copy.deepcopy(optimizer) + optimizer.parameters = rollouts.module.parameters() # set the optimizer's parameters to the proposal's parameters + targets = [r.target for r in rollouts] + feedbacks = [r.feedback for r in rollouts] + # batchify the targets and feedbacks + target = batchify(*targets) + feedback = batchify(*feedbacks).data # str + # standard optimizer step + optimizer.zero_feedback() # reset the optimizer's feedback + optimizer.backward(target, feedback) # compute the gradients based on the targets and feedbacks + return optimizer + + args_list = [(n,) for n in range(n_batches)] + optimizers = async_run([_backward]*n_batches, # run the optimizer step for each agent in parallel + args_list=args_list, + max_workers=self.num_threads, # use the number of threads specified in the class + description=None) + assert len(optimizers) == n_batches, "Number of optimizers must match number of batch rollouts." + # need to copy optimizer for the n_proposals + # NOTE when optimizer is deepcopied, its parameters are not copied. + optimizers = [copy.deepcopy(o) for o in optimizers ] * n_proposals # repeat args_list n_proposals times + assert len(optimizers) == n_batches * n_proposals, "Number of optimizers must match number of batch rollouts times number of proposals." + + # For each optimizer, containing the backward feedback, we call it n_proposals times to get the proposed parameters. + def _step(n): + optimizer = optimizers[n] + update_dict = optimizer.step(verbose=verbose, num_threads=self.num_threads, bypassing=True, **kwargs) + if not update_dict: # if the optimizer did not propose any updates + return None # return None to indicate no updates were proposed + # update_dict may only contain some of the parameters of the agent, we need to make sure it contains all the parameters + # since the current agent might have different parameters than the one used by the optimizer + for param in optimizer.parameters: # for all parameters + if param not in update_dict: # update_dict misses some parameters + update_dict[param] = param.data # add the parameter to the update_dict + # the update_dict is linked to the copied parameters of the agent, we set it back to the agent's parameters + update_dict = remap_update_dict(self.agent, update_dict) # remap the update dict to the agent's parameters + return update_dict # return the proposed parameters + + args_list = [(n,) for n in range(n_batches*n_proposals)] + update_dicts = async_run([_step]*n_batches*n_proposals, # run the optimizer step for each agent in parallel + args_list=args_list, + max_workers=self.num_threads, # use the number of threads specified in the class + description=f"Calling optimizers: Generating {n_proposals} proposals for each of {n_batches} batches",) + + # update_dicts is a list of dicts of length n_batches * n_proposals + # Create ModuleCandidate objects for each proposed update_dict that is non-trivial + candidates = [ModuleCandidate(self.agent, update_dict, optimizer) + for update_dict, optimizer in zip(update_dicts, optimizers) if update_dict is not None] # filter out None updates + return candidates + + def validate(self, + candidates: List[ModuleCandidate], + samples: Samples, + verbose: bool = False, + **kwargs): + """ Validate the proposed candidate parameters + Args: + candidates (list of ModuleCandidate): A list of ModuleCandidate objects representing the proposed parameters. + samples (list of dict, optional): A list of samples collected in the current iteration. Defaults to None. + verbose (bool, optional): Whether to print verbose output. Defaults to False. + **kwargs: Additional keyword arguments that may be used by the implementation. + Returns: + results (dict): A dictionary where the keys are ids of ModuleCandidate objects and the values are ModuleCandidate and lists of rollouts (list of dicts) containing the module, x, info, target, score, feedback. + """ + print("--- Validating candidates...") if verbose else None + assert isinstance(samples, Samples), "samples must be an instance of Samples." + exploration_candidates = self._exploration_candidates # exploration candidates from the previous iteration + assert self._exploration_candidates is not None, "exploration_candidates must be set before calling validate." + + # The current batch of samples can be used to validate the exploration candidates + validate_samples = copy.copy(samples) + # Xuanfei: I commented all these below, only use training samples. + # # Validate newly proposed candidates + # use_prev_batch = self.use_prev_batch # when True, self.validate_sampler == self.train_sampler, and the current batch is used for validation + # candidate_agents = [c.get_module() for c in candidates] # get the modules from the candidates + # validate_samples.add_samples(Samples(*self.validate_sampler.sample(candidate_agents, + # use_prev_batch=use_prev_batch, + # description_prefix='Validating newly proposed candidates: '))) # list of BatchRollout objects + + # if self.validate_exploration_candidates: + # if not use_prev_batch: # validate the exploration candidates that collected the samples as well + # # validate the agents in the validate_dataset + # exploration_agents = [c.get_module() for c in exploration_candidates] # get the modules from the exploration candidates + # exploration_samples = Samples(*self.validate_sampler.sample(exploration_agents, + # description_prefix='Validating exploration candidates: ')) # sample the exploration agents + # validate_samples.add_samples(exploration_samples) # append the exploration samples to the validate_samples + + + matched_candidates_and_samples = self.match_candidates_and_samples(exploration_candidates + candidates, validate_samples.samples) + results = {} # dict of ModuleCandidate id: (ModuleCandidate, list of rollouts) + for c, rollouts in matched_candidates_and_samples.items(): # rollouts is a list of BatchRollouts + results[c] = [ r for rr in rollouts for r in rr.to_list()] # we only need the list of dicts + + return results + + def match_candidates_and_samples( + self, + candidates: List[ModuleCandidate], + samples: List[BatchRollout]): + """ + Match the given candidates with the provided samples. + + Args: + candidates (list of ModuleCandidate): A list of ModuleCandidate objects representing the proposed parameters. + samples (list of BatchRollout): A Samples object containing a list of BatchRollout objects, where each BatchRollout contains rollouts collected by an agent on different inputs. + Returns: + results (dict): A dictionary where the keys are ModuleCandidate objects and the values are lists of BatchRollouts collected by the corresponding ModuleCandidate. + + """ + # In general, there may be multiple BatchRollouts collected by the same ModuleCandidate. + # We group the rollouts by the agent (ModuleCandidate) and return a dictionary + # where the keys are the ModuleCandidate objects and the values are Samples + + # Group the samples by the ModuleCandidate id + _results = { c: [] for c in candidates} # dict of ModuleCandidate: list of BatchRollouts + ids = {id(c): c for c in candidates} # dict of ModuleCandidate id: ModuleCandidate + + for rollouts in samples: + assert isinstance(rollouts, BatchRollout), "Each element in samples must be a BatchRollout object." + # rollouts is a BatchRollout object + module = rollouts.module # trace.Module + key = getattr(module, '__TRACE_RESERVED_module_candidate_id') # use the candidate as the key + if key not in ids: + raise ValueError(f"ModuleCandidate with id {key} not found in results. Samples are not collected by known candidates.") + # Append the rollouts to the list of rollouts for the key + _results[ids[key]].append(rollouts) + # assert all candidates have at least one rollout + # Xuanfei: some candidates may not have rollouts + # for c in candidates: + # assert len(_results[c]) > 0, f"ModuleCandidate with id {id(c)} has no rollouts. Samples are not collected by known candidates." + + return _results + + def update_memory(self, validate_results, verbose: bool = False, **kwargs): + """ Update the priority queue with the validation results. + Args: + validate_results (dict): A dictionary where the keys are ModuleCandidate objects and the values are lists of rollouts (list of dicts) containing the module, x, info, target, score, feedback. + **kwargs: Additional keyword arguments that may be used by the implementation. + """ + print("--- Updating memory with validation results...") if verbose else None + for candidate, rollouts in validate_results.items(): + candidate.add_rollouts(rollouts) # add the rollouts to the candidate + + # priority = self.compute_exploration_priority(candidate) # compute the priority for the candidate + placeholder_priority = self.max_score + self.memory.push(placeholder_priority, candidate) + + def update_memory_with_regressor(self, verbose: bool = False, **kwargs): + """ Update the priority queue with the regressor results. + """ + print("--- Updating memory with regressor results...") if verbose else None + # Update predicted scores for all candidates in the memory + self.regressor.predict_scores() + # Reorder the memory according to the predicted scores + self.memory.reorder_according_to_predicted_scores() + # For debugging, print the memory stats + self.print_memory_stats() + + def print_memory_stats(self): + # For debugging, print all candidates: number, mean_score(), num_rollouts, predicted_score + for i, (neg_predicted_score, candidate) in enumerate(self.memory): + print(f"Candidate {i}, Mean Score: {candidate.mean_score()}, Num Rollouts: {candidate.num_rollouts}, Predicted Score: {-neg_predicted_score}") + + def explore(self, verbose: bool = False, **kwargs): + """ Explore the parameter space and propose new candidates. + Args: + **kwargs: Additional keyword arguments that may be used by the implementation. + Returns: + list: A list of proposed candidates. + dict: A dictionary containing logging information about the exploration. + """ + print(f"--- Generating {min(len(self.memory), self.num_candidates)} exploration candidates...") if verbose else None + # pop top self.num_candidates candidates from the priority queue + # self._best_candidate is the exploited candidate from the previous iteration + top_candidates = [self._best_candidate] if self.use_best_candidate_to_explore else [] + priorities = [] # to store the priorities of the candidates for logging + while len(top_candidates) < self.num_candidates and len(self.memory) > 0: + neg_priority, candidate = self.memory.pop() # pop the top candidate from the priority queue + priority = - neg_priority # remember that we stored negative scores in the priority queue + if self.use_best_candidate_to_explore: + if candidate is self._best_candidate: # skip if it is already in the top candidates + continue + priorities.append(priority) # store the priority of the candidate + top_candidates.append(candidate) # add the candidate to the top candidates + # NOTE some top_candidates can be duplicates + mean_scores = [c.mean_score() for c in top_candidates] + mean_scores = [s for s in mean_scores if s is not None] # filter out None scores + info_dict = { + 'num_exploration_candidates': len(top_candidates), + 'exploration_candidates_mean_priority': np.mean(priorities), # list of priorities of the exploration candidates + 'exploration_candidates_mean_score': np.mean(mean_scores) if mean_scores else None, # list of mean scores of the exploration candidates + 'exploration_candidates_average_num_rollouts': np.mean([c.num_rollouts for c in top_candidates]), + } + + return top_candidates, info_dict + + def exploit(self, verbose: bool = False, **kwargs) -> Tuple[ModuleCandidate, Dict[str, Any]]: + """ Exploit the best candidate from the priority queue. This method should not change the priority queue. + Args: + verbose (bool, optional): Whether to print verbose output. Defaults to False. + **kwargs: Additional keyword arguments that may be used by the implementation. + Returns: + ModuleCandidate: The best candidate from the priority queue. + """ + print("--- Exploiting the best candidate...") if verbose else None + if not self.memory: + raise ValueError("The priority queue is empty. Cannot exploit.") + neg_priority, best_candidate = self.memory.best(self.compute_exploitation_priority) # (priority, candidate) + priority = - neg_priority # remember that we stored negative scores in the priority queue + return best_candidate, { + 'best_candidate_priority': priority, # remember that we stored negative scores in the priority queue + 'best_candidate_mean_score': best_candidate.mean_score(), # mean score of the candidate's rollouts + 'best_candidate_num_rollouts': best_candidate.num_rollouts, # number of rollouts of the candidate + } + + # TODO refactor below to reuse scoring + def compute_exploitation_priority(self, candidate) -> float: + # NOTE This function can be overridden by subclasses to compute a different score + """ Compute the score for the candidate based on the rollouts during the validation phase. + It can be overridden by subclasses to implement a different scoring strategy. + + Args: + candidate (ModuleCandidate): The candidate for which to compute the score. + Returns: + float: The computed score for the candidate. Higher scores indicate higher priority. + """ + if not isinstance(candidate, ModuleCandidate): + raise TypeError("candidate must be an instance of ModuleCandidate.") + # By default, we compute the mean score of the rollouts + return candidate.predicted_score + + def compute_exploration_priority(self, candidate) -> float: + # NOTE This function can be overridden by subclasses to compute a different score + """ Compute the score for the candidate based on the rollouts during the validation phase. + It can be overridden by subclasses to implement a different scoring strategy. + + Args: + candidate (ModuleCandidate): The candidate for which to compute the score. + Returns: + float: The computed score for the candidate. Higher scores indicate higher priority. + """ + if not isinstance(candidate, ModuleCandidate): + raise TypeError("candidate must be an instance of ModuleCandidate.") + # By default, we compute the mean score of the rollouts + + if self.score_function == 'mean': + # Compute the mean score of the candidate's rollouts + return candidate.mean_score() + elif self.score_function == 'time': + return -candidate.created_time # latest candidates have higher priority + elif self.score_function == 'ucb': + # Compute the Upper Confidence Bound (UCB) score + lcb_score, mean_score, ucb_score = candidate.compute_score_confidence( + min_score=self.min_score, + max_score=self.max_score, + scaling_constant=self.ucb_exploration_constant, + total_trials=self.n_iters + 1 # total number of trials conducted so far + ) + return ucb_score # return the UCB score + else: + raise ValueError(f"Unknown score function: {self.score_function}") From ab55230e0af3625a579299948f4dac2728f3dc13 Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Sat, 13 Sep 2025 23:31:56 -0500 Subject: [PATCH 205/314] fix a bug about num_samples not passed --- opto/features/priority_search/search_template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opto/features/priority_search/search_template.py b/opto/features/priority_search/search_template.py index f5336bc8..100be87b 100644 --- a/opto/features/priority_search/search_template.py +++ b/opto/features/priority_search/search_template.py @@ -286,7 +286,7 @@ def test(self, test_dataset, guide): min_score = self.min_score # Test the agent's performance test_score = self.evaluate(self.agent, guide, test_dataset['inputs'], test_dataset['infos'], - min_score=min_score, num_threads=self.num_threads, + min_score=min_score, num_threads=self.num_threads,num_samples=self.num_eval_samples, description=f"Evaluating agent") # and log # check if the test_score is within the score range if not (self.min_score <= test_score <= self.max_score): From 8fd06fee5d96dd2fd7054ec11de07d5937c6e30f Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Sun, 14 Sep 2025 10:59:07 -0500 Subject: [PATCH 206/314] fix a bug --- opto/features/priority_search/priority_search_modified.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/opto/features/priority_search/priority_search_modified.py b/opto/features/priority_search/priority_search_modified.py index c4404432..4381689f 100644 --- a/opto/features/priority_search/priority_search_modified.py +++ b/opto/features/priority_search/priority_search_modified.py @@ -621,8 +621,9 @@ def explore(self, verbose: bool = False, **kwargs): print(f"--- Generating {min(len(self.memory), self.num_candidates)} exploration candidates...") if verbose else None # pop top self.num_candidates candidates from the priority queue # self._best_candidate is the exploited candidate from the previous iteration - top_candidates = [self._best_candidate] if self.use_best_candidate_to_explore else [] - priorities = [] # to store the priorities of the candidates for logging + neg_priority, best_candidate = self.memory.best(self.compute_exploitation_priority) + top_candidates = [best_candidate] if self.use_best_candidate_to_explore else [] + priorities = [-neg_priority] # to store the priorities of the candidates for logging while len(top_candidates) < self.num_candidates and len(self.memory) > 0: neg_priority, candidate = self.memory.pop() # pop the top candidate from the priority queue priority = - neg_priority # remember that we stored negative scores in the priority queue From 993ef1a99fff089ca75802f4842d2f21546c2877 Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Sun, 14 Sep 2025 10:59:20 -0500 Subject: [PATCH 207/314] deal with None reward --- opto/features/priority_search/module_regressor.py | 2 +- opto/features/priority_search/search_template.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/opto/features/priority_search/module_regressor.py b/opto/features/priority_search/module_regressor.py index b9a2fea9..23dd1117 100644 --- a/opto/features/priority_search/module_regressor.py +++ b/opto/features/priority_search/module_regressor.py @@ -99,7 +99,7 @@ def _update_regression_model(self): self._update_memory_embeddings() # Get training data from memory (only candidates with rollout data) - training_candidates = [candidate for neg_score, candidate in self.memory if candidate.num_rollouts > 0] + training_candidates = [candidate for neg_score, candidate in self.memory if candidate.num_rollouts > 0 and candidate.mean_score() is not None] if len(training_candidates) == 0: print_color("Warning: No training data available for regression model.", "yellow") diff --git a/opto/features/priority_search/search_template.py b/opto/features/priority_search/search_template.py index 100be87b..cf83ca3f 100644 --- a/opto/features/priority_search/search_template.py +++ b/opto/features/priority_search/search_template.py @@ -262,7 +262,7 @@ def sample(self, agents, verbose=False, **kwargs): samples = Samples(*self.train_sampler.sample(agents, description_prefix='Sampling training minibatch: ')) # create a Samples object to store the samples and the minibatch # Log information about the sampling scores = [ g.get_scores() for g in samples.samples] # list of list of scores for each BatchRollout - scores = [item for sublist in scores for item in sublist] # flatten the list of scores + scores = [item for sublist in scores for item in sublist if item is not None] # flatten the list of scores log_info = { 'mean_score': np.mean(scores), 'self.n_epochs': self.train_sampler.n_epochs, From ab8ef960ead8889717d30845e60a4f69c0448167 Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Sun, 14 Sep 2025 14:16:39 -0500 Subject: [PATCH 208/314] move auto-retry to be exactly cover llm calls --- .../priority_search_modified.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/opto/features/priority_search/priority_search_modified.py b/opto/features/priority_search/priority_search_modified.py index 4381689f..e6b082c6 100644 --- a/opto/features/priority_search/priority_search_modified.py +++ b/opto/features/priority_search/priority_search_modified.py @@ -11,7 +11,7 @@ from opto.features.priority_search.search_template import SearchTemplate, Samples, BatchRollout from opto.features.priority_search.utils import set_module_parameters, remap_update_dict, create_module_from_update_dict, is_module_copy from opto.features.priority_search.module_regressor import ModuleCandidateRegressor - +from opto.features.priority_search.utils import retry_with_exponential_backoff class ModuleCandidate: """ A container used by PrioritySearch to store a candidate module as (its base module and update dictionary) and its statistics. """ @@ -354,13 +354,7 @@ def update(self, # samples is None in the first iteration if samples is not None: # 1. Propose new parameters based on running LLM optimizers on the collected samples - from opto.features.priority_search.utils import retry_with_exponential_backoff - candidates = retry_with_exponential_backoff( - lambda: self.propose(samples, verbose=verbose, **kwargs), - max_retries=10, - base_delay=1.0, - operation_name="propose_new_parameters" - ) # List of ModuleCandidates + candidates = self.propose(samples, verbose=verbose, **kwargs) # # 2. Validate the proposed parameters validate_results = self.validate(candidates, samples, verbose=verbose, **kwargs) # this updates the priority queue # # 3. Update the priority queue with the validation results @@ -473,7 +467,13 @@ def _backward(n): # For each optimizer, containing the backward feedback, we call it n_proposals times to get the proposed parameters. def _step(n): optimizer = optimizers[n] - update_dict = optimizer.step(verbose=verbose, num_threads=self.num_threads, bypassing=True, **kwargs) + + update_dict = retry_with_exponential_backoff( + lambda: optimizer.step(verbose=verbose, num_threads=self.num_threads, bypassing=True, **kwargs), + max_retries=10, + base_delay=1.0, + operation_name="optimizer_step" + ) if not update_dict: # if the optimizer did not propose any updates return None # return None to indicate no updates were proposed # update_dict may only contain some of the parameters of the agent, we need to make sure it contains all the parameters From a5a9dca18f6d527d1f8130ea0ff3fc1b018a7ffe Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Sun, 14 Sep 2025 15:58:45 -0500 Subject: [PATCH 209/314] disable printing memory stats --- opto/features/priority_search/priority_search_modified.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opto/features/priority_search/priority_search_modified.py b/opto/features/priority_search/priority_search_modified.py index e6b082c6..1ec553f6 100644 --- a/opto/features/priority_search/priority_search_modified.py +++ b/opto/features/priority_search/priority_search_modified.py @@ -603,10 +603,10 @@ def update_memory_with_regressor(self, verbose: bool = False, **kwargs): # Reorder the memory according to the predicted scores self.memory.reorder_according_to_predicted_scores() # For debugging, print the memory stats - self.print_memory_stats() + #self.print_memory_stats() def print_memory_stats(self): - # For debugging, print all candidates: number, mean_score(), num_rollouts, predicted_score + # For debugging, print all candidates: number, mean_score(), num_rollouts, predicted_score. It is better to see an increasing trend in the predicted scores. for i, (neg_predicted_score, candidate) in enumerate(self.memory): print(f"Candidate {i}, Mean Score: {candidate.mean_score()}, Num Rollouts: {candidate.num_rollouts}, Predicted Score: {-neg_predicted_score}") From b0a612b6de9d7237763c0f3a40709c95dffd969d Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Sun, 14 Sep 2025 19:55:54 -0500 Subject: [PATCH 210/314] rename the algorithm --- .../priority_search_with_regressor.py | 711 ++++++++++++++++++ 1 file changed, 711 insertions(+) create mode 100644 opto/features/priority_search/priority_search_with_regressor.py diff --git a/opto/features/priority_search/priority_search_with_regressor.py b/opto/features/priority_search/priority_search_with_regressor.py new file mode 100644 index 00000000..f6374894 --- /dev/null +++ b/opto/features/priority_search/priority_search_with_regressor.py @@ -0,0 +1,711 @@ +import numpy as np +import copy +import heapq +import time +from typing import Union, List, Tuple, Dict, Any, Optional +from opto import trace +from opto.trace.nodes import ParameterNode +from opto.optimizers.optimizer import Optimizer +from opto.trainer.utils import async_run +from opto.trainer.algorithms.basic_algorithms import batchify +from opto.features.priority_search.search_template import SearchTemplate, Samples, BatchRollout +from opto.features.priority_search.utils import set_module_parameters, remap_update_dict, create_module_from_update_dict, is_module_copy +from opto.features.priority_search.module_regressor import ModuleCandidateRegressor +from opto.features.priority_search.utils import retry_with_exponential_backoff + +class ModuleCandidate: + """ A container used by PrioritySearch to store a candidate module as (its base module and update dictionary) and its statistics. """ + + def __init__(self, + base_module: trace.Module, + update_dict: Optional[Dict[ParameterNode, Any]] = None, + optimizer: Optimizer = None, + ): + """ A candidate module with its base module and update dictionary. + Args: + base_module (trace.Module): The base module to use as a template for the candidate. + update_dict (dict): A dictionary of ParameterNode: value pairs to update the base module; the key can be a deep copy of the base module's parameters. + stats (dict): A dictionary of statistics about the candidate. + """ + assert isinstance(base_module, trace.Module), "base_module must be a trace.Module." + if update_dict is not None: + assert isinstance(optimizer, Optimizer), "optimizer must be an instance of Optimizer when update_dict is provided." + + self.base_module = base_module + self.update_dict = update_dict if update_dict is not None else {} + self.optimizer = optimizer # the optimizer used to generate the update_dict; can be None, which indicates the base_module is used. + self.update_dict = remap_update_dict(self.base_module, self.update_dict) + self.rollouts = [] # list of dicts containing the rollout information (not BatchRollout, but a list of dicts) + self.created_time = time.time() + + def get_module(self): + """ Apply the update_dict to the base_module and return the updated module. + A new module is always created so the base_module is not modified. + The new module has a new attribute _module_candidate which is this candidate.""" + module = create_module_from_update_dict(self.base_module, self.update_dict) if self.update_dict else copy.deepcopy(self.base_module) # + setattr(module, '__TRACE_RESERVED_module_candidate_id', id(self)) + return module # return the updated module + + def apply_update(self, base_module=None): + """ Apply update to the base_module in place. """ + set_module_parameters(base_module or self.base_module, self.update_dict) + + def __getstate__(self): + """ Get the state of the candidate for serialization. """ + state = copy.deepcopy(self.__dict__) # this will detach the nodes from the computation graph + return state + + def __setstate__(self, state): + """ Set the state of the candidate from serialization. """ + self.__dict__.update(state) + + def __deepcopy__(self, memo): + """ Create a deep copy, except for the base_module which is not copied, it is the original module. """ + cls = self.__class__ + result = cls.__new__(cls) + memo[id(self)] = result + for k, v in self.__dict__.items(): + if k != 'base_module': + setattr(result, k, copy.deepcopy(v, memo)) + else: + setattr(result, k, v) # base_module is not copied, it is the original module + return result + + def __eq__(self, other): + """ Check if two candidates are equal based on their base_module and update_dict. """ + assert isinstance(other, ModuleCandidate), "other must be an instance of ModuleCandidate." + return (self.update_dict == other.update_dict) and is_module_copy(self.base_module, other.base_module) and (id(self.optimizer) == id(other.optimizer)) + + def __lt__(self, other): + """ Compare two candidates based on their update_dict. """ + assert isinstance(other, ModuleCandidate), "other must be an instance of ModuleCandidate." + return self.created_time > other.created_time + # self < other if, self is created later than other + # Since we will use minheap, and this would give priority to later created candidates in the heap memory. + + def __hash__(self): + """ Hash the candidate based on its update_dict. """ + return hash((frozenset(self.update_dict.items()), id(self.optimizer), id(self.base_module))) + + def add_rollouts(self, rollouts: List[Dict[str, Any]]): + """ Add rollouts to the candidate. """ + assert isinstance(rollouts, list), "rollouts must be a list of dicts." + assert all(isinstance(r, dict) for r in rollouts), "All rollouts must be dicts." + # Each rollout is a dict with keys: 'module', 'x', 'info', 'target', 'score', 'feedback' + assert all('module' in r and 'x' in r and 'info' in r and 'target' in r and 'score' in r and 'feedback' in r for r in rollouts), \ + "Each rollout must contain 'module', 'x', 'info', 'target', 'score', and 'feedback' keys." + + self.rollouts.extend(rollouts) + + def mean_score(self): + """ Compute the score of the candidate based on the rollouts. """ + if not self.rollouts: + return None + scores = [r['score'] for r in self.rollouts] + return np.mean(scores) if scores else None + + def compute_score_confidence(self, min_score, max_score, scaling_constant=1.0, total_trials=1): + """Compute the UCB, mean, LCB score for the candidate. After queried, the number of confidence queries is incremented. + + UCB = mean_score + scaling_constant * sqrt(ln(total_trials) / n_scores) * (max_score - min_score) + UCB = clip(UCB, min_score, max_score) + + LCB = mean_score - scaling_constant * sqrt(ln(total_trials) / n_scores) * (max_score - min_score) + LCB = clip(LCB, min_score, max_score) + + Args: + min_score (float): The minimum score for clipping. + max_score (float): The maximum score for clipping. + scaling_constant (float): The scaling constant for the exploration term. + total_trials (int): The total number of trials conducted. Must be at least 1. + Returns: + lcb_score (float): The lower confidence bound score. + mean_score (float): The mean score. + ucb_score (float): The upper confidence bound score. + """ + # Get scores from rollouts + scores = [r['score'] for r in self.rollouts] + + if not scores: + return min_score, None, max_score + + # Calculate mean score for this candidate + mean_score = np.mean(scores) + n_scores = len(scores) + assert n_scores == self.num_rollouts, "Number of scores should match number of rollouts." + + # Calculate how many times the confidence interval has been used to form a union bound + assert total_trials >= 1, "total_trials must be at least 1." + total_trials = total_trials + 1 # this is an upper bound, since log(1) = 0 + + # Compute the exploration term based on Hoeffding's inequality + exploration_term = scaling_constant * np.sqrt(np.log(total_trials) / n_scores) * (max_score - min_score) + + # Calculate UCB score + ucb_score = mean_score + exploration_term + ucb_score = np.clip(ucb_score, min_score, max_score) + + # Calculate LCB score + lcb_score = mean_score - exploration_term + lcb_score = np.clip(lcb_score, min_score, max_score) + + return lcb_score, mean_score, ucb_score + + + @property + def num_rollouts(self): + """ Return the number of rollouts collected for this candidate. """ + return len(self.rollouts) + + +class HeapMemory: + # This is a basic implementation of a heap memory that uses a priority queue to store candidates. + # Later on this will be replaced by a memory DB. + + # NOTE that the heap memory is a max-heap, so we store negative scores to use the default min-heap behavior of heapq. + def __init__(self, size=None): + """ Initialize an empty heap memory. """ + self.memory = [] + self.size = size # Optional size limit for the heap memory + + def push(self, score, data): + """ Push an item to the heap memory. """ + heapq.heappush(self.memory, (-score, data)) + if self.size is not None and len(self.memory) > self.size: + # NOTE a heuristic for now + self.memory = self.memory[:self.size] # Keep only the top `size` items + + def pop(self): + """ Pop the top item from the heap memory. """ + if not self.memory: + raise IndexError("pop from an empty heap memory") + return heapq.heappop(self.memory) + + def __len__(self): + """ Return the number of items in the heap memory. """ + return len(self.memory) + + def __bool__(self): + """ Return True if the heap memory is not empty, False otherwise. """ + return len(self.memory) > 0 + + def __iter__(self): + """ Iterate over the items in the heap memory. """ + return iter(self.memory) + + def best(self, criterion=None): + """ Return the best item in the heap memory without removing it. + + If criterion is None, return the item with the highest priority (lowest negative score). + If criterion is a callable function, return the item that maximizes the criterion. + """ + if not self.memory: + raise IndexError("best from an empty heap memory") + if criterion is None: + return self.memory[0] # return the item with the highest priority (lowest negative score) + else: + assert callable(criterion), "criterion must be a callable function." + def _criterion(x): + neg_score, candidate = x + p = criterion(candidate) + return p if p is not None else 0 + return max(self.memory, key=lambda x: _criterion(x)) + + def reorder_according_to_predicted_scores(self): + """ Reorder the heap memory according to the predicted scores. """ + # Now all ModuleCandidate objects in the heap memory have predicted scores. Should modify the old score to the negative predicted scores, then use heapq.heapify to reorder the heap memory. + self.memory = [(-candidate.predicted_score, candidate) for _, candidate in self.memory] + heapq.heapify(self.memory) + +# TODO check saving and loading +class PrioritySearch_with_Regressor(SearchTemplate): + """ A search algorithm that uses a priority queue to explore the parameter space and propose new candidates. + + It provides a scalable template for implementing search algorithms based on asynchronous generation, validation, and testing. + In each iteration, + 1. It proposes a best agent and a set of `num_candidates` exploration agents that have the highest scores in the priority queue. + 2. The best agent is tested for performance if eval_frequency is met. + 3. `num_batches` minibatches of `batch_size` samples are drawn from the training dataset, and the exploration agents are run on the samples. This creates a set of agent rollouts, where each rollout contains the agent module, input, info, target, score, and feedback. For each agent, rollouts of each minibatch are grouped together as a connected subgraph (represented as the BatchRollout object). In total, this step creates `num_candidates * num_batches` subgraphs. + 4. Optimizer is run on each subgraph to propose new parameters for the agents. `num_proposals` proposals are generated for each subgraph. This results in `num_subgraphs * num_proposals` total proposals. + 5. The proposed parameters are validated by running the agents on the validation dataset, which can be the current batch or a separate validation dataset when provided. When validate_exploration_candidates is set to True, the exploration candidates are also validated. + 6. The validation results are used to update the priority queue, which stores the candidates and their scores. The candidates are stored as ModuleCandidate objects, which contain the base module, update dictionary, and rollouts (i.e. raw statistics of the candidate). + + This algorithm template can be subclassed to implement specific search algorithms by overriding the `exploit`, `explore`, and `compute_exploration_priority` methods. + The `exploit` method is used to select the best candidate from the priority queue, the `explore` method is used to generate new candidates from the priority queue, and + the `compute_exploration_priority` method is used to compute the score for ranking in the priority queue. + + By default, `compute_exploration_priority` computes the mean score of the rollouts. `exploit` simply returns the candidate with highest priority from the priority queue, and `explore` generates the top `num_candidates` candidates from the priority queue. + + + `compute_exploration_priority`, `compute_exploitation_priority` can be overridden to implement different strategies for computing the priority and selecting the best candidate. + """ + + def train(self, + guide, # guide to provide feedback + train_dataset, # dataset of (x, info) pairs to train the agent + *, + # validation + validate_dataset = None, # same format as train_dataset; if None, use the current batch. + validate_guide = None, # to provide scores for the validation set + # training loop + batch_size = 1, # batch size for updating the agent + num_batches = 1, # number of batches to use from the dataset in each iteration + score_range = None, # minimum score to update the agent + num_epochs = 1, # number of training epochs + num_threads = None, # maximum number of threads to use + verbose = False, # whether to print the output of the agent + # evaluation + test_dataset = None, # dataset of (x, info) pairs to evaluate the agent + test_frequency: Union[int, None] = 1, # frequency of evaluation (set it to be negative to skip the first evaluation) + num_eval_samples: int = 1, # number of times to evaluate each input; when greater than 1, the scores are averaged. + # logging + log_frequency = None, # frequency of logging + save_frequency: Union[int, None] = None, # frequency of saving the agent + save_path: str = "checkpoints/agent.pkl", # path to save the agent + # Priority Search specific parameters + num_candidates: int = 10, # number of candidates to propose for exploration + num_proposals: int = 1, # number of proposals to generate per optimizer + validate_exploration_candidates: bool = True, # whether to validate the proposed parameters for exploration + use_best_candidate_to_explore: bool = True, # whether to use the best candidate as part of the exploration candidates + memory_size: Optional[int] = None, # size of the heap memory to store the candidates; if None, no limit is set + score_function: str = 'mean', # function to compute the score for the candidates; 'mean' or 'ucb' + ucb_exploration_constant: float = 1.0, # exploration constant for UCB score function + # Additional keyword arguments + **kwargs + ): + """ Train the agent using the Priority Search algorithm. + + Args: + guide (callable): A function that provides feedback for the agent. + train_dataset (list): A list of (x, info) pairs to train the agent. + validate_dataset (list, optional): A list of (x, info) pairs to validate the proposed candidates. If None, the current batch is used. Defaults to None. + validate_guide (callable, optional): A function that provides feedback for the validation set. If None, the training guide is used. Defaults to None. + batch_size (int, optional): The batch size for updating the agent. Defaults to 1. + num_batches (int, optional): The number of batches to use from the dataset in each iteration. Defaults to 1. + score_range (tuple, optional): A tuple of (min_score, max_score) to clip the scores. If None, no clipping is applied. Defaults to None. + num_epochs (int, optional): The number of training epochs. Defaults to 1. + num_threads (int, optional): The maximum number of threads to use. If None, it uses the number of CPU cores. Defaults to None. + verbose (bool, optional): Whether to print the output of the agent. Defaults to False. + test_dataset (list, optional): A list of (x, info) pairs to evaluate the agent. If None, no evaluation is performed. Defaults to None. + test_frequency (int or None, optional): The frequency of evaluation. If None, no evaluation is performed. If negative, skips the first evaluation. Defaults to 1. + num_eval_samples (int, optional): The number of times to evaluate each input; when greater than 1, the scores are averaged. Defaults to 1. + log_frequency (int or None, optional): The frequency of logging. If None, no logging is performed. Defaults to None. + save_frequency (int or None, optional): The frequency of saving the agent. If None, no saving is performed. Defaults to None. + save_path (str, optional): The path to save the agent. Defaults to "checkpoints/agent.pkl". + num_candidates (int, optional): The number of candidates to propose for exploration. Defaults to 10. + num_proposals (int, optional): The number of proposals to generate per optimizer. Defaults to 1. + validate_exploration_candidates (bool, optional): Whether to validate the proposed parameters for exploration. Defaults to True. + use_best_candidate_to_explore (bool, optional): Whether to use the best candidate as part of the exploration candidates. Defaults to True. + memory_size (int, optional): The size of the heap memory to store the candidates. If None, no limit is set. Defaults to None. + score_function (str, optional): The function to compute the score for the candidates; 'mean' or 'ucb'. Defaults to 'mean'. + ucb_exploration_constant (float, optional): The exploration constant for UCB score function. Defaults to 1.0. + **kwargs: Additional keyword arguments that may be used by the implementation. + """ + + + # Create agents and optimizers for search + if num_candidates < len(self._optimizers): + print(f"Warning: num_candidates {num_candidates} is less than the number of optimizers {len(self._optimizers)}. Setting num_candidates to {len(self._optimizers)}.") + num_candidates = len(self._optimizers) + self.num_candidates = num_candidates # number of candidates for exploration + self.num_proposals = num_proposals # number of candidates to propose by each optimizer call + + self.validate_exploration_candidates = validate_exploration_candidates # whether to validate the proposed parameters + self.use_best_candidate_to_explore = use_best_candidate_to_explore + self.score_function = score_function # function to compute the score for the candidates + if score_range is None: + score_range = (0, 1) + if score_function == 'ucb': # this requires a bounded score range. By default, it is set to (0, 1) + assert score_range[1]-score_range[0] < float('inf'), \ + "For UCB score function, score_range must be finite. Use 'mean' score function if you want to use unbounded scores." + + self.ucb_exploration_constant = ucb_exploration_constant + self._exploration_candidates = None # This stores the latest candidates used for exploration + self._best_candidate = None # This stores the latest best candidate used for exploitation + + self.memory = HeapMemory(size=memory_size) # Initialize the heap memory with a size limit + self.regressor = ModuleCandidateRegressor(memory=self.memory) # Initialize the + + super().train(guide=guide, + train_dataset=train_dataset, + validate_dataset=validate_dataset, + validate_guide=validate_guide, + batch_size=batch_size, + num_batches=num_batches, + score_range=score_range, + num_epochs=num_epochs, + num_threads=num_threads, + verbose=verbose, + test_dataset=test_dataset, + eval_frequency=test_frequency, + num_eval_samples=num_eval_samples, + log_frequency=log_frequency, + save_frequency=save_frequency, + save_path=save_path, + **kwargs) + + def update(self, + samples: Union[Samples, None] = None, + verbose: bool = False, + **kwargs): #-> Tuple[Dict[ParameterNode, Any], List[trace.Module], Dict[str, Any]]: + """ Update the agent using the collected samples. + """ + + # samples is None in the first iteration + if samples is not None: + # 1. Propose new parameters based on running LLM optimizers on the collected samples + candidates = self.propose(samples, verbose=verbose, **kwargs) + # # 2. Validate the proposed parameters + validate_results = self.validate(candidates, samples, verbose=verbose, **kwargs) # this updates the priority queue + # # 3. Update the priority queue with the validation results + self.update_memory(validate_results, verbose=verbose, **kwargs) # samples are provided here in case candidates do not capture full information + + else: # The first iteration. + max_mem_size = self.memory.size if self.memory.size is not None else float('inf') + initial_update_dict = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} + while len(self.memory) < min(max_mem_size, self.num_candidates): + self.memory.push(self.max_score, ModuleCandidate(self.agent, initial_update_dict, optimizer=self.optimizer)) # Push the base agent as the first candidate (This gives the initialization of the priority queue) + self.update_memory_with_regressor() + # 4. Explore and exploit the priority queue + self._best_candidate, info_exploit = self.exploit(verbose=verbose, **kwargs) # get the best candidate (ModuleCandidate) from the priority queue + self._exploration_candidates, info_explore = self.explore(verbose=verbose, **kwargs) # List of ModuleCandidates + # TODO Log information about the update + info_log = { + 'n_iters': self.n_iters, # number of iterations + } + + info_log.update(info_exploit) # add the info from the exploit step + info_log.update(info_explore) # add the info from the explore step + return self._best_candidate.update_dict, [c.get_module() for c in self._exploration_candidates], info_log + + + ## Illustration of `propose`` + # Suppose we have 2 exploration candidates. + # exploration_candidates = [candidate(param1, optimizer_1), candidate(param2, optimizer_2)] + # and two batches are collected by sampler. + # + # In samples returned by sampler, we have data + # module(param1_copy1), batch_1 + # module(param1_copy2), batch_2 + # module(param2_copy1), batch_1 + # module(param2_copy2), batch_2 + # + # We first match the samples with the exploration candidates as + # candidate_batchrollouts_list = + # [ (candidate(param1, optimizer_1), batch_1), (candidate(param1, optimizer_1), batch_2), + # (candidate(param2, optimizer_2), batch_1), (candidate(param2, optimizer_2), batch_2) ] + # + # In backward, we create deepcopies of the optimizers for each batch, and run backward asynchronously. + # optimizer_1_copy_1(param1) <- feedback from batch_1 + # optimizer_1_copy_2(param1) <- feedback from batch_2 + # optimizer_2_copy_1(param2) <- feedback from batch_1 + # optimizer_2_copy_2(param2) <- feedback from batch_2 + # + # In step, we further create deepcopies of the optimizers for each proposal, and run step asynchronously. + # for n_proposals = 2, we have + # optimizer_1_copy_1_copy_1(param1) -> proposal_1 + # optimizer_1_copy_1_copy_2(param1) -> proposal_2 + # ... + # optimizer_2_copy_2_copy_1(param2) -> proposal_7 + # optimizer_2_copy_2_copy_2(param2) -> proposal_8 + # which form the new candidate list returned by `propose`. + # + def propose(self, + samples : Samples, + verbose : bool = False, + **kwargs): + """ Analyzing samples and propose new parameters using self.optimizer. An independent optimizer is used for the minibatch generated by one agent and generates n_proposals proposals. + + Args: + samples (Samples): Samples collected by the exploration candidates. If None, the agent's parameters are returned without updating. + verbose (bool, optional): Whether to print verbose output. Defaults to False. + **kwargs: Additional keyword arguments that may be used by the implementation. + + Returns: + candidates (list of ModuleCandidate): A list of proposed candidates for the next iteration. + """ + print("--- Proposing new parameters...") if verbose else None + assert isinstance(samples, Samples), "samples must be an instance of Samples." + samples = samples.samples # list of BatchRollout objects + n_proposals = self.num_proposals # number of proposals to generate per optimizer + + # Associate each BatchRollout with self._exploration_candidates + matched_candidates_and_samples = self.match_candidates_and_samples(self._exploration_candidates, samples) + # NOTE len(matched_candidates_and_samples) <= len(self._exploration_candidates) since some exploration candidates might be duplicated. + candidate_batchrollouts_list = [ (k,b) for k, v in matched_candidates_and_samples.items() for b in v] + assert len(samples) == len(candidate_batchrollouts_list), "All samples must be associated with exploration candidates." + n_batches = len(samples) # number of batch rollouts in the samples + + # need to copy optimizer for the n_batches + def _backward(n): + candidate, rollouts = candidate_batchrollouts_list[n] + optimizer = candidate.optimizer or self.optimizer + # Create a copy of the optimizer to avoid modifying the original one and to allow parallel execution + optimizer = copy.deepcopy(optimizer) + optimizer.parameters = rollouts.module.parameters() # set the optimizer's parameters to the proposal's parameters + targets = [r.target for r in rollouts] + feedbacks = [r.feedback for r in rollouts] + # batchify the targets and feedbacks + target = batchify(*targets) + feedback = batchify(*feedbacks).data # str + # standard optimizer step + optimizer.zero_feedback() # reset the optimizer's feedback + optimizer.backward(target, feedback) # compute the gradients based on the targets and feedbacks + return optimizer + + args_list = [(n,) for n in range(n_batches)] + optimizers = async_run([_backward]*n_batches, # run the optimizer step for each agent in parallel + args_list=args_list, + max_workers=self.num_threads, # use the number of threads specified in the class + description=None) + assert len(optimizers) == n_batches, "Number of optimizers must match number of batch rollouts." + # need to copy optimizer for the n_proposals + # NOTE when optimizer is deepcopied, its parameters are not copied. + optimizers = [copy.deepcopy(o) for o in optimizers ] * n_proposals # repeat args_list n_proposals times + assert len(optimizers) == n_batches * n_proposals, "Number of optimizers must match number of batch rollouts times number of proposals." + + # For each optimizer, containing the backward feedback, we call it n_proposals times to get the proposed parameters. + def _step(n): + optimizer = optimizers[n] + + update_dict = retry_with_exponential_backoff( + lambda: optimizer.step(verbose=verbose, num_threads=self.num_threads, bypassing=True, **kwargs), + max_retries=10, + base_delay=1.0, + operation_name="optimizer_step" + ) + if not update_dict: # if the optimizer did not propose any updates + return None # return None to indicate no updates were proposed + # update_dict may only contain some of the parameters of the agent, we need to make sure it contains all the parameters + # since the current agent might have different parameters than the one used by the optimizer + for param in optimizer.parameters: # for all parameters + if param not in update_dict: # update_dict misses some parameters + update_dict[param] = param.data # add the parameter to the update_dict + # the update_dict is linked to the copied parameters of the agent, we set it back to the agent's parameters + update_dict = remap_update_dict(self.agent, update_dict) # remap the update dict to the agent's parameters + return update_dict # return the proposed parameters + + args_list = [(n,) for n in range(n_batches*n_proposals)] + update_dicts = async_run([_step]*n_batches*n_proposals, # run the optimizer step for each agent in parallel + args_list=args_list, + max_workers=self.num_threads, # use the number of threads specified in the class + description=f"Calling optimizers: Generating {n_proposals} proposals for each of {n_batches} batches",) + + # update_dicts is a list of dicts of length n_batches * n_proposals + # Create ModuleCandidate objects for each proposed update_dict that is non-trivial + candidates = [ModuleCandidate(self.agent, update_dict, optimizer) + for update_dict, optimizer in zip(update_dicts, optimizers) if update_dict is not None] # filter out None updates + return candidates + + def validate(self, + candidates: List[ModuleCandidate], + samples: Samples, + verbose: bool = False, + **kwargs): + """ Validate the proposed candidate parameters + Args: + candidates (list of ModuleCandidate): A list of ModuleCandidate objects representing the proposed parameters. + samples (list of dict, optional): A list of samples collected in the current iteration. Defaults to None. + verbose (bool, optional): Whether to print verbose output. Defaults to False. + **kwargs: Additional keyword arguments that may be used by the implementation. + Returns: + results (dict): A dictionary where the keys are ids of ModuleCandidate objects and the values are ModuleCandidate and lists of rollouts (list of dicts) containing the module, x, info, target, score, feedback. + """ + print("--- Validating candidates...") if verbose else None + assert isinstance(samples, Samples), "samples must be an instance of Samples." + exploration_candidates = self._exploration_candidates # exploration candidates from the previous iteration + assert self._exploration_candidates is not None, "exploration_candidates must be set before calling validate." + + # The current batch of samples can be used to validate the exploration candidates + validate_samples = copy.copy(samples) + # Xuanfei: I commented all these below, only use training samples. + # # Validate newly proposed candidates + # use_prev_batch = self.use_prev_batch # when True, self.validate_sampler == self.train_sampler, and the current batch is used for validation + # candidate_agents = [c.get_module() for c in candidates] # get the modules from the candidates + # validate_samples.add_samples(Samples(*self.validate_sampler.sample(candidate_agents, + # use_prev_batch=use_prev_batch, + # description_prefix='Validating newly proposed candidates: '))) # list of BatchRollout objects + + # if self.validate_exploration_candidates: + # if not use_prev_batch: # validate the exploration candidates that collected the samples as well + # # validate the agents in the validate_dataset + # exploration_agents = [c.get_module() for c in exploration_candidates] # get the modules from the exploration candidates + # exploration_samples = Samples(*self.validate_sampler.sample(exploration_agents, + # description_prefix='Validating exploration candidates: ')) # sample the exploration agents + # validate_samples.add_samples(exploration_samples) # append the exploration samples to the validate_samples + + + matched_candidates_and_samples = self.match_candidates_and_samples(exploration_candidates + candidates, validate_samples.samples) + results = {} # dict of ModuleCandidate id: (ModuleCandidate, list of rollouts) + for c, rollouts in matched_candidates_and_samples.items(): # rollouts is a list of BatchRollouts + results[c] = [ r for rr in rollouts for r in rr.to_list()] # we only need the list of dicts + + return results + + def match_candidates_and_samples( + self, + candidates: List[ModuleCandidate], + samples: List[BatchRollout]): + """ + Match the given candidates with the provided samples. + + Args: + candidates (list of ModuleCandidate): A list of ModuleCandidate objects representing the proposed parameters. + samples (list of BatchRollout): A Samples object containing a list of BatchRollout objects, where each BatchRollout contains rollouts collected by an agent on different inputs. + Returns: + results (dict): A dictionary where the keys are ModuleCandidate objects and the values are lists of BatchRollouts collected by the corresponding ModuleCandidate. + + """ + # In general, there may be multiple BatchRollouts collected by the same ModuleCandidate. + # We group the rollouts by the agent (ModuleCandidate) and return a dictionary + # where the keys are the ModuleCandidate objects and the values are Samples + + # Group the samples by the ModuleCandidate id + _results = { c: [] for c in candidates} # dict of ModuleCandidate: list of BatchRollouts + ids = {id(c): c for c in candidates} # dict of ModuleCandidate id: ModuleCandidate + + for rollouts in samples: + assert isinstance(rollouts, BatchRollout), "Each element in samples must be a BatchRollout object." + # rollouts is a BatchRollout object + module = rollouts.module # trace.Module + key = getattr(module, '__TRACE_RESERVED_module_candidate_id') # use the candidate as the key + if key not in ids: + raise ValueError(f"ModuleCandidate with id {key} not found in results. Samples are not collected by known candidates.") + # Append the rollouts to the list of rollouts for the key + _results[ids[key]].append(rollouts) + # assert all candidates have at least one rollout + # Xuanfei: some candidates may not have rollouts + # for c in candidates: + # assert len(_results[c]) > 0, f"ModuleCandidate with id {id(c)} has no rollouts. Samples are not collected by known candidates." + + return _results + + def update_memory(self, validate_results, verbose: bool = False, **kwargs): + """ Update the priority queue with the validation results. + Args: + validate_results (dict): A dictionary where the keys are ModuleCandidate objects and the values are lists of rollouts (list of dicts) containing the module, x, info, target, score, feedback. + **kwargs: Additional keyword arguments that may be used by the implementation. + """ + print("--- Updating memory with validation results...") if verbose else None + for candidate, rollouts in validate_results.items(): + candidate.add_rollouts(rollouts) # add the rollouts to the candidate + + # priority = self.compute_exploration_priority(candidate) # compute the priority for the candidate + placeholder_priority = self.max_score + self.memory.push(placeholder_priority, candidate) + + def update_memory_with_regressor(self, verbose: bool = False, **kwargs): + """ Update the priority queue with the regressor results. + """ + print("--- Updating memory with regressor results...") if verbose else None + # Update predicted scores for all candidates in the memory + self.regressor.predict_scores() + # Reorder the memory according to the predicted scores + self.memory.reorder_according_to_predicted_scores() + # For debugging, print the memory stats + #self.print_memory_stats() + + def print_memory_stats(self): + # For debugging, print all candidates: number, mean_score(), num_rollouts, predicted_score. It is better to see an increasing trend in the predicted scores. + for i, (neg_predicted_score, candidate) in enumerate(self.memory): + print(f"Candidate {i}, Mean Score: {candidate.mean_score()}, Num Rollouts: {candidate.num_rollouts}, Predicted Score: {-neg_predicted_score}") + + def explore(self, verbose: bool = False, **kwargs): + """ Explore the parameter space and propose new candidates. + Args: + **kwargs: Additional keyword arguments that may be used by the implementation. + Returns: + list: A list of proposed candidates. + dict: A dictionary containing logging information about the exploration. + """ + print(f"--- Generating {min(len(self.memory), self.num_candidates)} exploration candidates...") if verbose else None + # pop top self.num_candidates candidates from the priority queue + # self._best_candidate is the exploited candidate from the previous iteration + neg_priority, best_candidate = self.memory.best(self.compute_exploitation_priority) + top_candidates = [best_candidate] if self.use_best_candidate_to_explore else [] + priorities = [-neg_priority] # to store the priorities of the candidates for logging + while len(top_candidates) < self.num_candidates and len(self.memory) > 0: + neg_priority, candidate = self.memory.pop() # pop the top candidate from the priority queue + priority = - neg_priority # remember that we stored negative scores in the priority queue + if self.use_best_candidate_to_explore: + if candidate is self._best_candidate: # skip if it is already in the top candidates + continue + priorities.append(priority) # store the priority of the candidate + top_candidates.append(candidate) # add the candidate to the top candidates + # NOTE some top_candidates can be duplicates + mean_scores = [c.mean_score() for c in top_candidates] + mean_scores = [s for s in mean_scores if s is not None] # filter out None scores + info_dict = { + 'num_exploration_candidates': len(top_candidates), + 'exploration_candidates_mean_priority': np.mean(priorities), # list of priorities of the exploration candidates + 'exploration_candidates_mean_score': np.mean(mean_scores) if mean_scores else None, # list of mean scores of the exploration candidates + 'exploration_candidates_average_num_rollouts': np.mean([c.num_rollouts for c in top_candidates]), + } + + return top_candidates, info_dict + + def exploit(self, verbose: bool = False, **kwargs) -> Tuple[ModuleCandidate, Dict[str, Any]]: + """ Exploit the best candidate from the priority queue. This method should not change the priority queue. + Args: + verbose (bool, optional): Whether to print verbose output. Defaults to False. + **kwargs: Additional keyword arguments that may be used by the implementation. + Returns: + ModuleCandidate: The best candidate from the priority queue. + """ + print("--- Exploiting the best candidate...") if verbose else None + if not self.memory: + raise ValueError("The priority queue is empty. Cannot exploit.") + neg_priority, best_candidate = self.memory.best(self.compute_exploitation_priority) # (priority, candidate) + priority = - neg_priority # remember that we stored negative scores in the priority queue + return best_candidate, { + 'best_candidate_priority': priority, # remember that we stored negative scores in the priority queue + 'best_candidate_mean_score': best_candidate.mean_score(), # mean score of the candidate's rollouts + 'best_candidate_num_rollouts': best_candidate.num_rollouts, # number of rollouts of the candidate + } + + # TODO refactor below to reuse scoring + def compute_exploitation_priority(self, candidate) -> float: + # NOTE This function can be overridden by subclasses to compute a different score + """ Compute the score for the candidate based on the rollouts during the validation phase. + It can be overridden by subclasses to implement a different scoring strategy. + + Args: + candidate (ModuleCandidate): The candidate for which to compute the score. + Returns: + float: The computed score for the candidate. Higher scores indicate higher priority. + """ + if not isinstance(candidate, ModuleCandidate): + raise TypeError("candidate must be an instance of ModuleCandidate.") + # By default, we compute the mean score of the rollouts + return candidate.predicted_score + + def compute_exploration_priority(self, candidate) -> float: + # NOTE This function can be overridden by subclasses to compute a different score + """ Compute the score for the candidate based on the rollouts during the validation phase. + It can be overridden by subclasses to implement a different scoring strategy. + + Args: + candidate (ModuleCandidate): The candidate for which to compute the score. + Returns: + float: The computed score for the candidate. Higher scores indicate higher priority. + """ + if not isinstance(candidate, ModuleCandidate): + raise TypeError("candidate must be an instance of ModuleCandidate.") + # By default, we compute the mean score of the rollouts + + if self.score_function == 'mean': + # Compute the mean score of the candidate's rollouts + return candidate.mean_score() + elif self.score_function == 'time': + return -candidate.created_time # latest candidates have higher priority + elif self.score_function == 'ucb': + # Compute the Upper Confidence Bound (UCB) score + lcb_score, mean_score, ucb_score = candidate.compute_score_confidence( + min_score=self.min_score, + max_score=self.max_score, + scaling_constant=self.ucb_exploration_constant, + total_trials=self.n_iters + 1 # total number of trials conducted so far + ) + return ucb_score # return the UCB score + else: + raise ValueError(f"Unknown score function: {self.score_function}") From 40210d3677f1be2454b39eed4d37ed7ba24e37f7 Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Sun, 14 Sep 2025 19:56:04 -0500 Subject: [PATCH 211/314] rename --- .../priority_search_modified.py | 711 ------------------ 1 file changed, 711 deletions(-) delete mode 100644 opto/features/priority_search/priority_search_modified.py diff --git a/opto/features/priority_search/priority_search_modified.py b/opto/features/priority_search/priority_search_modified.py deleted file mode 100644 index 1ec553f6..00000000 --- a/opto/features/priority_search/priority_search_modified.py +++ /dev/null @@ -1,711 +0,0 @@ -import numpy as np -import copy -import heapq -import time -from typing import Union, List, Tuple, Dict, Any, Optional -from opto import trace -from opto.trace.nodes import ParameterNode -from opto.optimizers.optimizer import Optimizer -from opto.trainer.utils import async_run -from opto.trainer.algorithms.basic_algorithms import batchify -from opto.features.priority_search.search_template import SearchTemplate, Samples, BatchRollout -from opto.features.priority_search.utils import set_module_parameters, remap_update_dict, create_module_from_update_dict, is_module_copy -from opto.features.priority_search.module_regressor import ModuleCandidateRegressor -from opto.features.priority_search.utils import retry_with_exponential_backoff - -class ModuleCandidate: - """ A container used by PrioritySearch to store a candidate module as (its base module and update dictionary) and its statistics. """ - - def __init__(self, - base_module: trace.Module, - update_dict: Optional[Dict[ParameterNode, Any]] = None, - optimizer: Optimizer = None, - ): - """ A candidate module with its base module and update dictionary. - Args: - base_module (trace.Module): The base module to use as a template for the candidate. - update_dict (dict): A dictionary of ParameterNode: value pairs to update the base module; the key can be a deep copy of the base module's parameters. - stats (dict): A dictionary of statistics about the candidate. - """ - assert isinstance(base_module, trace.Module), "base_module must be a trace.Module." - if update_dict is not None: - assert isinstance(optimizer, Optimizer), "optimizer must be an instance of Optimizer when update_dict is provided." - - self.base_module = base_module - self.update_dict = update_dict if update_dict is not None else {} - self.optimizer = optimizer # the optimizer used to generate the update_dict; can be None, which indicates the base_module is used. - self.update_dict = remap_update_dict(self.base_module, self.update_dict) - self.rollouts = [] # list of dicts containing the rollout information (not BatchRollout, but a list of dicts) - self.created_time = time.time() - - def get_module(self): - """ Apply the update_dict to the base_module and return the updated module. - A new module is always created so the base_module is not modified. - The new module has a new attribute _module_candidate which is this candidate.""" - module = create_module_from_update_dict(self.base_module, self.update_dict) if self.update_dict else copy.deepcopy(self.base_module) # - setattr(module, '__TRACE_RESERVED_module_candidate_id', id(self)) - return module # return the updated module - - def apply_update(self, base_module=None): - """ Apply update to the base_module in place. """ - set_module_parameters(base_module or self.base_module, self.update_dict) - - def __getstate__(self): - """ Get the state of the candidate for serialization. """ - state = copy.deepcopy(self.__dict__) # this will detach the nodes from the computation graph - return state - - def __setstate__(self, state): - """ Set the state of the candidate from serialization. """ - self.__dict__.update(state) - - def __deepcopy__(self, memo): - """ Create a deep copy, except for the base_module which is not copied, it is the original module. """ - cls = self.__class__ - result = cls.__new__(cls) - memo[id(self)] = result - for k, v in self.__dict__.items(): - if k != 'base_module': - setattr(result, k, copy.deepcopy(v, memo)) - else: - setattr(result, k, v) # base_module is not copied, it is the original module - return result - - def __eq__(self, other): - """ Check if two candidates are equal based on their base_module and update_dict. """ - assert isinstance(other, ModuleCandidate), "other must be an instance of ModuleCandidate." - return (self.update_dict == other.update_dict) and is_module_copy(self.base_module, other.base_module) and (id(self.optimizer) == id(other.optimizer)) - - def __lt__(self, other): - """ Compare two candidates based on their update_dict. """ - assert isinstance(other, ModuleCandidate), "other must be an instance of ModuleCandidate." - return self.created_time > other.created_time - # self < other if, self is created later than other - # Since we will use minheap, and this would give priority to later created candidates in the heap memory. - - def __hash__(self): - """ Hash the candidate based on its update_dict. """ - return hash((frozenset(self.update_dict.items()), id(self.optimizer), id(self.base_module))) - - def add_rollouts(self, rollouts: List[Dict[str, Any]]): - """ Add rollouts to the candidate. """ - assert isinstance(rollouts, list), "rollouts must be a list of dicts." - assert all(isinstance(r, dict) for r in rollouts), "All rollouts must be dicts." - # Each rollout is a dict with keys: 'module', 'x', 'info', 'target', 'score', 'feedback' - assert all('module' in r and 'x' in r and 'info' in r and 'target' in r and 'score' in r and 'feedback' in r for r in rollouts), \ - "Each rollout must contain 'module', 'x', 'info', 'target', 'score', and 'feedback' keys." - - self.rollouts.extend(rollouts) - - def mean_score(self): - """ Compute the score of the candidate based on the rollouts. """ - if not self.rollouts: - return None - scores = [r['score'] for r in self.rollouts] - return np.mean(scores) if scores else None - - def compute_score_confidence(self, min_score, max_score, scaling_constant=1.0, total_trials=1): - """Compute the UCB, mean, LCB score for the candidate. After queried, the number of confidence queries is incremented. - - UCB = mean_score + scaling_constant * sqrt(ln(total_trials) / n_scores) * (max_score - min_score) - UCB = clip(UCB, min_score, max_score) - - LCB = mean_score - scaling_constant * sqrt(ln(total_trials) / n_scores) * (max_score - min_score) - LCB = clip(LCB, min_score, max_score) - - Args: - min_score (float): The minimum score for clipping. - max_score (float): The maximum score for clipping. - scaling_constant (float): The scaling constant for the exploration term. - total_trials (int): The total number of trials conducted. Must be at least 1. - Returns: - lcb_score (float): The lower confidence bound score. - mean_score (float): The mean score. - ucb_score (float): The upper confidence bound score. - """ - # Get scores from rollouts - scores = [r['score'] for r in self.rollouts] - - if not scores: - return min_score, None, max_score - - # Calculate mean score for this candidate - mean_score = np.mean(scores) - n_scores = len(scores) - assert n_scores == self.num_rollouts, "Number of scores should match number of rollouts." - - # Calculate how many times the confidence interval has been used to form a union bound - assert total_trials >= 1, "total_trials must be at least 1." - total_trials = total_trials + 1 # this is an upper bound, since log(1) = 0 - - # Compute the exploration term based on Hoeffding's inequality - exploration_term = scaling_constant * np.sqrt(np.log(total_trials) / n_scores) * (max_score - min_score) - - # Calculate UCB score - ucb_score = mean_score + exploration_term - ucb_score = np.clip(ucb_score, min_score, max_score) - - # Calculate LCB score - lcb_score = mean_score - exploration_term - lcb_score = np.clip(lcb_score, min_score, max_score) - - return lcb_score, mean_score, ucb_score - - - @property - def num_rollouts(self): - """ Return the number of rollouts collected for this candidate. """ - return len(self.rollouts) - - -class HeapMemory: - # This is a basic implementation of a heap memory that uses a priority queue to store candidates. - # Later on this will be replaced by a memory DB. - - # NOTE that the heap memory is a max-heap, so we store negative scores to use the default min-heap behavior of heapq. - def __init__(self, size=None): - """ Initialize an empty heap memory. """ - self.memory = [] - self.size = size # Optional size limit for the heap memory - - def push(self, score, data): - """ Push an item to the heap memory. """ - heapq.heappush(self.memory, (-score, data)) - if self.size is not None and len(self.memory) > self.size: - # NOTE a heuristic for now - self.memory = self.memory[:self.size] # Keep only the top `size` items - - def pop(self): - """ Pop the top item from the heap memory. """ - if not self.memory: - raise IndexError("pop from an empty heap memory") - return heapq.heappop(self.memory) - - def __len__(self): - """ Return the number of items in the heap memory. """ - return len(self.memory) - - def __bool__(self): - """ Return True if the heap memory is not empty, False otherwise. """ - return len(self.memory) > 0 - - def __iter__(self): - """ Iterate over the items in the heap memory. """ - return iter(self.memory) - - def best(self, criterion=None): - """ Return the best item in the heap memory without removing it. - - If criterion is None, return the item with the highest priority (lowest negative score). - If criterion is a callable function, return the item that maximizes the criterion. - """ - if not self.memory: - raise IndexError("best from an empty heap memory") - if criterion is None: - return self.memory[0] # return the item with the highest priority (lowest negative score) - else: - assert callable(criterion), "criterion must be a callable function." - def _criterion(x): - neg_score, candidate = x - p = criterion(candidate) - return p if p is not None else 0 - return max(self.memory, key=lambda x: _criterion(x)) - - def reorder_according_to_predicted_scores(self): - """ Reorder the heap memory according to the predicted scores. """ - # Now all ModuleCandidate objects in the heap memory have predicted scores. Should modify the old score to the negative predicted scores, then use heapq.heapify to reorder the heap memory. - self.memory = [(-candidate.predicted_score, candidate) for _, candidate in self.memory] - heapq.heapify(self.memory) - -# TODO check saving and loading -class PrioritySearch(SearchTemplate): - """ A search algorithm that uses a priority queue to explore the parameter space and propose new candidates. - - It provides a scalable template for implementing search algorithms based on asynchronous generation, validation, and testing. - In each iteration, - 1. It proposes a best agent and a set of `num_candidates` exploration agents that have the highest scores in the priority queue. - 2. The best agent is tested for performance if eval_frequency is met. - 3. `num_batches` minibatches of `batch_size` samples are drawn from the training dataset, and the exploration agents are run on the samples. This creates a set of agent rollouts, where each rollout contains the agent module, input, info, target, score, and feedback. For each agent, rollouts of each minibatch are grouped together as a connected subgraph (represented as the BatchRollout object). In total, this step creates `num_candidates * num_batches` subgraphs. - 4. Optimizer is run on each subgraph to propose new parameters for the agents. `num_proposals` proposals are generated for each subgraph. This results in `num_subgraphs * num_proposals` total proposals. - 5. The proposed parameters are validated by running the agents on the validation dataset, which can be the current batch or a separate validation dataset when provided. When validate_exploration_candidates is set to True, the exploration candidates are also validated. - 6. The validation results are used to update the priority queue, which stores the candidates and their scores. The candidates are stored as ModuleCandidate objects, which contain the base module, update dictionary, and rollouts (i.e. raw statistics of the candidate). - - This algorithm template can be subclassed to implement specific search algorithms by overriding the `exploit`, `explore`, and `compute_exploration_priority` methods. - The `exploit` method is used to select the best candidate from the priority queue, the `explore` method is used to generate new candidates from the priority queue, and - the `compute_exploration_priority` method is used to compute the score for ranking in the priority queue. - - By default, `compute_exploration_priority` computes the mean score of the rollouts. `exploit` simply returns the candidate with highest priority from the priority queue, and `explore` generates the top `num_candidates` candidates from the priority queue. - - - `compute_exploration_priority`, `compute_exploitation_priority` can be overridden to implement different strategies for computing the priority and selecting the best candidate. - """ - - def train(self, - guide, # guide to provide feedback - train_dataset, # dataset of (x, info) pairs to train the agent - *, - # validation - validate_dataset = None, # same format as train_dataset; if None, use the current batch. - validate_guide = None, # to provide scores for the validation set - # training loop - batch_size = 1, # batch size for updating the agent - num_batches = 1, # number of batches to use from the dataset in each iteration - score_range = None, # minimum score to update the agent - num_epochs = 1, # number of training epochs - num_threads = None, # maximum number of threads to use - verbose = False, # whether to print the output of the agent - # evaluation - test_dataset = None, # dataset of (x, info) pairs to evaluate the agent - test_frequency: Union[int, None] = 1, # frequency of evaluation (set it to be negative to skip the first evaluation) - num_eval_samples: int = 1, # number of times to evaluate each input; when greater than 1, the scores are averaged. - # logging - log_frequency = None, # frequency of logging - save_frequency: Union[int, None] = None, # frequency of saving the agent - save_path: str = "checkpoints/agent.pkl", # path to save the agent - # Priority Search specific parameters - num_candidates: int = 10, # number of candidates to propose for exploration - num_proposals: int = 1, # number of proposals to generate per optimizer - validate_exploration_candidates: bool = True, # whether to validate the proposed parameters for exploration - use_best_candidate_to_explore: bool = True, # whether to use the best candidate as part of the exploration candidates - memory_size: Optional[int] = None, # size of the heap memory to store the candidates; if None, no limit is set - score_function: str = 'mean', # function to compute the score for the candidates; 'mean' or 'ucb' - ucb_exploration_constant: float = 1.0, # exploration constant for UCB score function - # Additional keyword arguments - **kwargs - ): - """ Train the agent using the Priority Search algorithm. - - Args: - guide (callable): A function that provides feedback for the agent. - train_dataset (list): A list of (x, info) pairs to train the agent. - validate_dataset (list, optional): A list of (x, info) pairs to validate the proposed candidates. If None, the current batch is used. Defaults to None. - validate_guide (callable, optional): A function that provides feedback for the validation set. If None, the training guide is used. Defaults to None. - batch_size (int, optional): The batch size for updating the agent. Defaults to 1. - num_batches (int, optional): The number of batches to use from the dataset in each iteration. Defaults to 1. - score_range (tuple, optional): A tuple of (min_score, max_score) to clip the scores. If None, no clipping is applied. Defaults to None. - num_epochs (int, optional): The number of training epochs. Defaults to 1. - num_threads (int, optional): The maximum number of threads to use. If None, it uses the number of CPU cores. Defaults to None. - verbose (bool, optional): Whether to print the output of the agent. Defaults to False. - test_dataset (list, optional): A list of (x, info) pairs to evaluate the agent. If None, no evaluation is performed. Defaults to None. - test_frequency (int or None, optional): The frequency of evaluation. If None, no evaluation is performed. If negative, skips the first evaluation. Defaults to 1. - num_eval_samples (int, optional): The number of times to evaluate each input; when greater than 1, the scores are averaged. Defaults to 1. - log_frequency (int or None, optional): The frequency of logging. If None, no logging is performed. Defaults to None. - save_frequency (int or None, optional): The frequency of saving the agent. If None, no saving is performed. Defaults to None. - save_path (str, optional): The path to save the agent. Defaults to "checkpoints/agent.pkl". - num_candidates (int, optional): The number of candidates to propose for exploration. Defaults to 10. - num_proposals (int, optional): The number of proposals to generate per optimizer. Defaults to 1. - validate_exploration_candidates (bool, optional): Whether to validate the proposed parameters for exploration. Defaults to True. - use_best_candidate_to_explore (bool, optional): Whether to use the best candidate as part of the exploration candidates. Defaults to True. - memory_size (int, optional): The size of the heap memory to store the candidates. If None, no limit is set. Defaults to None. - score_function (str, optional): The function to compute the score for the candidates; 'mean' or 'ucb'. Defaults to 'mean'. - ucb_exploration_constant (float, optional): The exploration constant for UCB score function. Defaults to 1.0. - **kwargs: Additional keyword arguments that may be used by the implementation. - """ - - - # Create agents and optimizers for search - if num_candidates < len(self._optimizers): - print(f"Warning: num_candidates {num_candidates} is less than the number of optimizers {len(self._optimizers)}. Setting num_candidates to {len(self._optimizers)}.") - num_candidates = len(self._optimizers) - self.num_candidates = num_candidates # number of candidates for exploration - self.num_proposals = num_proposals # number of candidates to propose by each optimizer call - - self.validate_exploration_candidates = validate_exploration_candidates # whether to validate the proposed parameters - self.use_best_candidate_to_explore = use_best_candidate_to_explore - self.score_function = score_function # function to compute the score for the candidates - if score_range is None: - score_range = (0, 1) - if score_function == 'ucb': # this requires a bounded score range. By default, it is set to (0, 1) - assert score_range[1]-score_range[0] < float('inf'), \ - "For UCB score function, score_range must be finite. Use 'mean' score function if you want to use unbounded scores." - - self.ucb_exploration_constant = ucb_exploration_constant - self._exploration_candidates = None # This stores the latest candidates used for exploration - self._best_candidate = None # This stores the latest best candidate used for exploitation - - self.memory = HeapMemory(size=memory_size) # Initialize the heap memory with a size limit - self.regressor = ModuleCandidateRegressor(memory=self.memory) # Initialize the - - super().train(guide=guide, - train_dataset=train_dataset, - validate_dataset=validate_dataset, - validate_guide=validate_guide, - batch_size=batch_size, - num_batches=num_batches, - score_range=score_range, - num_epochs=num_epochs, - num_threads=num_threads, - verbose=verbose, - test_dataset=test_dataset, - eval_frequency=test_frequency, - num_eval_samples=num_eval_samples, - log_frequency=log_frequency, - save_frequency=save_frequency, - save_path=save_path, - **kwargs) - - def update(self, - samples: Union[Samples, None] = None, - verbose: bool = False, - **kwargs): #-> Tuple[Dict[ParameterNode, Any], List[trace.Module], Dict[str, Any]]: - """ Update the agent using the collected samples. - """ - - # samples is None in the first iteration - if samples is not None: - # 1. Propose new parameters based on running LLM optimizers on the collected samples - candidates = self.propose(samples, verbose=verbose, **kwargs) - # # 2. Validate the proposed parameters - validate_results = self.validate(candidates, samples, verbose=verbose, **kwargs) # this updates the priority queue - # # 3. Update the priority queue with the validation results - self.update_memory(validate_results, verbose=verbose, **kwargs) # samples are provided here in case candidates do not capture full information - - else: # The first iteration. - max_mem_size = self.memory.size if self.memory.size is not None else float('inf') - initial_update_dict = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} - while len(self.memory) < min(max_mem_size, self.num_candidates): - self.memory.push(self.max_score, ModuleCandidate(self.agent, initial_update_dict, optimizer=self.optimizer)) # Push the base agent as the first candidate (This gives the initialization of the priority queue) - self.update_memory_with_regressor() - # 4. Explore and exploit the priority queue - self._best_candidate, info_exploit = self.exploit(verbose=verbose, **kwargs) # get the best candidate (ModuleCandidate) from the priority queue - self._exploration_candidates, info_explore = self.explore(verbose=verbose, **kwargs) # List of ModuleCandidates - # TODO Log information about the update - info_log = { - 'n_iters': self.n_iters, # number of iterations - } - - info_log.update(info_exploit) # add the info from the exploit step - info_log.update(info_explore) # add the info from the explore step - return self._best_candidate.update_dict, [c.get_module() for c in self._exploration_candidates], info_log - - - ## Illustration of `propose`` - # Suppose we have 2 exploration candidates. - # exploration_candidates = [candidate(param1, optimizer_1), candidate(param2, optimizer_2)] - # and two batches are collected by sampler. - # - # In samples returned by sampler, we have data - # module(param1_copy1), batch_1 - # module(param1_copy2), batch_2 - # module(param2_copy1), batch_1 - # module(param2_copy2), batch_2 - # - # We first match the samples with the exploration candidates as - # candidate_batchrollouts_list = - # [ (candidate(param1, optimizer_1), batch_1), (candidate(param1, optimizer_1), batch_2), - # (candidate(param2, optimizer_2), batch_1), (candidate(param2, optimizer_2), batch_2) ] - # - # In backward, we create deepcopies of the optimizers for each batch, and run backward asynchronously. - # optimizer_1_copy_1(param1) <- feedback from batch_1 - # optimizer_1_copy_2(param1) <- feedback from batch_2 - # optimizer_2_copy_1(param2) <- feedback from batch_1 - # optimizer_2_copy_2(param2) <- feedback from batch_2 - # - # In step, we further create deepcopies of the optimizers for each proposal, and run step asynchronously. - # for n_proposals = 2, we have - # optimizer_1_copy_1_copy_1(param1) -> proposal_1 - # optimizer_1_copy_1_copy_2(param1) -> proposal_2 - # ... - # optimizer_2_copy_2_copy_1(param2) -> proposal_7 - # optimizer_2_copy_2_copy_2(param2) -> proposal_8 - # which form the new candidate list returned by `propose`. - # - def propose(self, - samples : Samples, - verbose : bool = False, - **kwargs): - """ Analyzing samples and propose new parameters using self.optimizer. An independent optimizer is used for the minibatch generated by one agent and generates n_proposals proposals. - - Args: - samples (Samples): Samples collected by the exploration candidates. If None, the agent's parameters are returned without updating. - verbose (bool, optional): Whether to print verbose output. Defaults to False. - **kwargs: Additional keyword arguments that may be used by the implementation. - - Returns: - candidates (list of ModuleCandidate): A list of proposed candidates for the next iteration. - """ - print("--- Proposing new parameters...") if verbose else None - assert isinstance(samples, Samples), "samples must be an instance of Samples." - samples = samples.samples # list of BatchRollout objects - n_proposals = self.num_proposals # number of proposals to generate per optimizer - - # Associate each BatchRollout with self._exploration_candidates - matched_candidates_and_samples = self.match_candidates_and_samples(self._exploration_candidates, samples) - # NOTE len(matched_candidates_and_samples) <= len(self._exploration_candidates) since some exploration candidates might be duplicated. - candidate_batchrollouts_list = [ (k,b) for k, v in matched_candidates_and_samples.items() for b in v] - assert len(samples) == len(candidate_batchrollouts_list), "All samples must be associated with exploration candidates." - n_batches = len(samples) # number of batch rollouts in the samples - - # need to copy optimizer for the n_batches - def _backward(n): - candidate, rollouts = candidate_batchrollouts_list[n] - optimizer = candidate.optimizer or self.optimizer - # Create a copy of the optimizer to avoid modifying the original one and to allow parallel execution - optimizer = copy.deepcopy(optimizer) - optimizer.parameters = rollouts.module.parameters() # set the optimizer's parameters to the proposal's parameters - targets = [r.target for r in rollouts] - feedbacks = [r.feedback for r in rollouts] - # batchify the targets and feedbacks - target = batchify(*targets) - feedback = batchify(*feedbacks).data # str - # standard optimizer step - optimizer.zero_feedback() # reset the optimizer's feedback - optimizer.backward(target, feedback) # compute the gradients based on the targets and feedbacks - return optimizer - - args_list = [(n,) for n in range(n_batches)] - optimizers = async_run([_backward]*n_batches, # run the optimizer step for each agent in parallel - args_list=args_list, - max_workers=self.num_threads, # use the number of threads specified in the class - description=None) - assert len(optimizers) == n_batches, "Number of optimizers must match number of batch rollouts." - # need to copy optimizer for the n_proposals - # NOTE when optimizer is deepcopied, its parameters are not copied. - optimizers = [copy.deepcopy(o) for o in optimizers ] * n_proposals # repeat args_list n_proposals times - assert len(optimizers) == n_batches * n_proposals, "Number of optimizers must match number of batch rollouts times number of proposals." - - # For each optimizer, containing the backward feedback, we call it n_proposals times to get the proposed parameters. - def _step(n): - optimizer = optimizers[n] - - update_dict = retry_with_exponential_backoff( - lambda: optimizer.step(verbose=verbose, num_threads=self.num_threads, bypassing=True, **kwargs), - max_retries=10, - base_delay=1.0, - operation_name="optimizer_step" - ) - if not update_dict: # if the optimizer did not propose any updates - return None # return None to indicate no updates were proposed - # update_dict may only contain some of the parameters of the agent, we need to make sure it contains all the parameters - # since the current agent might have different parameters than the one used by the optimizer - for param in optimizer.parameters: # for all parameters - if param not in update_dict: # update_dict misses some parameters - update_dict[param] = param.data # add the parameter to the update_dict - # the update_dict is linked to the copied parameters of the agent, we set it back to the agent's parameters - update_dict = remap_update_dict(self.agent, update_dict) # remap the update dict to the agent's parameters - return update_dict # return the proposed parameters - - args_list = [(n,) for n in range(n_batches*n_proposals)] - update_dicts = async_run([_step]*n_batches*n_proposals, # run the optimizer step for each agent in parallel - args_list=args_list, - max_workers=self.num_threads, # use the number of threads specified in the class - description=f"Calling optimizers: Generating {n_proposals} proposals for each of {n_batches} batches",) - - # update_dicts is a list of dicts of length n_batches * n_proposals - # Create ModuleCandidate objects for each proposed update_dict that is non-trivial - candidates = [ModuleCandidate(self.agent, update_dict, optimizer) - for update_dict, optimizer in zip(update_dicts, optimizers) if update_dict is not None] # filter out None updates - return candidates - - def validate(self, - candidates: List[ModuleCandidate], - samples: Samples, - verbose: bool = False, - **kwargs): - """ Validate the proposed candidate parameters - Args: - candidates (list of ModuleCandidate): A list of ModuleCandidate objects representing the proposed parameters. - samples (list of dict, optional): A list of samples collected in the current iteration. Defaults to None. - verbose (bool, optional): Whether to print verbose output. Defaults to False. - **kwargs: Additional keyword arguments that may be used by the implementation. - Returns: - results (dict): A dictionary where the keys are ids of ModuleCandidate objects and the values are ModuleCandidate and lists of rollouts (list of dicts) containing the module, x, info, target, score, feedback. - """ - print("--- Validating candidates...") if verbose else None - assert isinstance(samples, Samples), "samples must be an instance of Samples." - exploration_candidates = self._exploration_candidates # exploration candidates from the previous iteration - assert self._exploration_candidates is not None, "exploration_candidates must be set before calling validate." - - # The current batch of samples can be used to validate the exploration candidates - validate_samples = copy.copy(samples) - # Xuanfei: I commented all these below, only use training samples. - # # Validate newly proposed candidates - # use_prev_batch = self.use_prev_batch # when True, self.validate_sampler == self.train_sampler, and the current batch is used for validation - # candidate_agents = [c.get_module() for c in candidates] # get the modules from the candidates - # validate_samples.add_samples(Samples(*self.validate_sampler.sample(candidate_agents, - # use_prev_batch=use_prev_batch, - # description_prefix='Validating newly proposed candidates: '))) # list of BatchRollout objects - - # if self.validate_exploration_candidates: - # if not use_prev_batch: # validate the exploration candidates that collected the samples as well - # # validate the agents in the validate_dataset - # exploration_agents = [c.get_module() for c in exploration_candidates] # get the modules from the exploration candidates - # exploration_samples = Samples(*self.validate_sampler.sample(exploration_agents, - # description_prefix='Validating exploration candidates: ')) # sample the exploration agents - # validate_samples.add_samples(exploration_samples) # append the exploration samples to the validate_samples - - - matched_candidates_and_samples = self.match_candidates_and_samples(exploration_candidates + candidates, validate_samples.samples) - results = {} # dict of ModuleCandidate id: (ModuleCandidate, list of rollouts) - for c, rollouts in matched_candidates_and_samples.items(): # rollouts is a list of BatchRollouts - results[c] = [ r for rr in rollouts for r in rr.to_list()] # we only need the list of dicts - - return results - - def match_candidates_and_samples( - self, - candidates: List[ModuleCandidate], - samples: List[BatchRollout]): - """ - Match the given candidates with the provided samples. - - Args: - candidates (list of ModuleCandidate): A list of ModuleCandidate objects representing the proposed parameters. - samples (list of BatchRollout): A Samples object containing a list of BatchRollout objects, where each BatchRollout contains rollouts collected by an agent on different inputs. - Returns: - results (dict): A dictionary where the keys are ModuleCandidate objects and the values are lists of BatchRollouts collected by the corresponding ModuleCandidate. - - """ - # In general, there may be multiple BatchRollouts collected by the same ModuleCandidate. - # We group the rollouts by the agent (ModuleCandidate) and return a dictionary - # where the keys are the ModuleCandidate objects and the values are Samples - - # Group the samples by the ModuleCandidate id - _results = { c: [] for c in candidates} # dict of ModuleCandidate: list of BatchRollouts - ids = {id(c): c for c in candidates} # dict of ModuleCandidate id: ModuleCandidate - - for rollouts in samples: - assert isinstance(rollouts, BatchRollout), "Each element in samples must be a BatchRollout object." - # rollouts is a BatchRollout object - module = rollouts.module # trace.Module - key = getattr(module, '__TRACE_RESERVED_module_candidate_id') # use the candidate as the key - if key not in ids: - raise ValueError(f"ModuleCandidate with id {key} not found in results. Samples are not collected by known candidates.") - # Append the rollouts to the list of rollouts for the key - _results[ids[key]].append(rollouts) - # assert all candidates have at least one rollout - # Xuanfei: some candidates may not have rollouts - # for c in candidates: - # assert len(_results[c]) > 0, f"ModuleCandidate with id {id(c)} has no rollouts. Samples are not collected by known candidates." - - return _results - - def update_memory(self, validate_results, verbose: bool = False, **kwargs): - """ Update the priority queue with the validation results. - Args: - validate_results (dict): A dictionary where the keys are ModuleCandidate objects and the values are lists of rollouts (list of dicts) containing the module, x, info, target, score, feedback. - **kwargs: Additional keyword arguments that may be used by the implementation. - """ - print("--- Updating memory with validation results...") if verbose else None - for candidate, rollouts in validate_results.items(): - candidate.add_rollouts(rollouts) # add the rollouts to the candidate - - # priority = self.compute_exploration_priority(candidate) # compute the priority for the candidate - placeholder_priority = self.max_score - self.memory.push(placeholder_priority, candidate) - - def update_memory_with_regressor(self, verbose: bool = False, **kwargs): - """ Update the priority queue with the regressor results. - """ - print("--- Updating memory with regressor results...") if verbose else None - # Update predicted scores for all candidates in the memory - self.regressor.predict_scores() - # Reorder the memory according to the predicted scores - self.memory.reorder_according_to_predicted_scores() - # For debugging, print the memory stats - #self.print_memory_stats() - - def print_memory_stats(self): - # For debugging, print all candidates: number, mean_score(), num_rollouts, predicted_score. It is better to see an increasing trend in the predicted scores. - for i, (neg_predicted_score, candidate) in enumerate(self.memory): - print(f"Candidate {i}, Mean Score: {candidate.mean_score()}, Num Rollouts: {candidate.num_rollouts}, Predicted Score: {-neg_predicted_score}") - - def explore(self, verbose: bool = False, **kwargs): - """ Explore the parameter space and propose new candidates. - Args: - **kwargs: Additional keyword arguments that may be used by the implementation. - Returns: - list: A list of proposed candidates. - dict: A dictionary containing logging information about the exploration. - """ - print(f"--- Generating {min(len(self.memory), self.num_candidates)} exploration candidates...") if verbose else None - # pop top self.num_candidates candidates from the priority queue - # self._best_candidate is the exploited candidate from the previous iteration - neg_priority, best_candidate = self.memory.best(self.compute_exploitation_priority) - top_candidates = [best_candidate] if self.use_best_candidate_to_explore else [] - priorities = [-neg_priority] # to store the priorities of the candidates for logging - while len(top_candidates) < self.num_candidates and len(self.memory) > 0: - neg_priority, candidate = self.memory.pop() # pop the top candidate from the priority queue - priority = - neg_priority # remember that we stored negative scores in the priority queue - if self.use_best_candidate_to_explore: - if candidate is self._best_candidate: # skip if it is already in the top candidates - continue - priorities.append(priority) # store the priority of the candidate - top_candidates.append(candidate) # add the candidate to the top candidates - # NOTE some top_candidates can be duplicates - mean_scores = [c.mean_score() for c in top_candidates] - mean_scores = [s for s in mean_scores if s is not None] # filter out None scores - info_dict = { - 'num_exploration_candidates': len(top_candidates), - 'exploration_candidates_mean_priority': np.mean(priorities), # list of priorities of the exploration candidates - 'exploration_candidates_mean_score': np.mean(mean_scores) if mean_scores else None, # list of mean scores of the exploration candidates - 'exploration_candidates_average_num_rollouts': np.mean([c.num_rollouts for c in top_candidates]), - } - - return top_candidates, info_dict - - def exploit(self, verbose: bool = False, **kwargs) -> Tuple[ModuleCandidate, Dict[str, Any]]: - """ Exploit the best candidate from the priority queue. This method should not change the priority queue. - Args: - verbose (bool, optional): Whether to print verbose output. Defaults to False. - **kwargs: Additional keyword arguments that may be used by the implementation. - Returns: - ModuleCandidate: The best candidate from the priority queue. - """ - print("--- Exploiting the best candidate...") if verbose else None - if not self.memory: - raise ValueError("The priority queue is empty. Cannot exploit.") - neg_priority, best_candidate = self.memory.best(self.compute_exploitation_priority) # (priority, candidate) - priority = - neg_priority # remember that we stored negative scores in the priority queue - return best_candidate, { - 'best_candidate_priority': priority, # remember that we stored negative scores in the priority queue - 'best_candidate_mean_score': best_candidate.mean_score(), # mean score of the candidate's rollouts - 'best_candidate_num_rollouts': best_candidate.num_rollouts, # number of rollouts of the candidate - } - - # TODO refactor below to reuse scoring - def compute_exploitation_priority(self, candidate) -> float: - # NOTE This function can be overridden by subclasses to compute a different score - """ Compute the score for the candidate based on the rollouts during the validation phase. - It can be overridden by subclasses to implement a different scoring strategy. - - Args: - candidate (ModuleCandidate): The candidate for which to compute the score. - Returns: - float: The computed score for the candidate. Higher scores indicate higher priority. - """ - if not isinstance(candidate, ModuleCandidate): - raise TypeError("candidate must be an instance of ModuleCandidate.") - # By default, we compute the mean score of the rollouts - return candidate.predicted_score - - def compute_exploration_priority(self, candidate) -> float: - # NOTE This function can be overridden by subclasses to compute a different score - """ Compute the score for the candidate based on the rollouts during the validation phase. - It can be overridden by subclasses to implement a different scoring strategy. - - Args: - candidate (ModuleCandidate): The candidate for which to compute the score. - Returns: - float: The computed score for the candidate. Higher scores indicate higher priority. - """ - if not isinstance(candidate, ModuleCandidate): - raise TypeError("candidate must be an instance of ModuleCandidate.") - # By default, we compute the mean score of the rollouts - - if self.score_function == 'mean': - # Compute the mean score of the candidate's rollouts - return candidate.mean_score() - elif self.score_function == 'time': - return -candidate.created_time # latest candidates have higher priority - elif self.score_function == 'ucb': - # Compute the Upper Confidence Bound (UCB) score - lcb_score, mean_score, ucb_score = candidate.compute_score_confidence( - min_score=self.min_score, - max_score=self.max_score, - scaling_constant=self.ucb_exploration_constant, - total_trials=self.n_iters + 1 # total number of trials conducted so far - ) - return ucb_score # return the UCB score - else: - raise ValueError(f"Unknown score function: {self.score_function}") From 1045a50da0afdc661d53a02768e018b0e289ce74 Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Sun, 14 Sep 2025 19:59:43 -0500 Subject: [PATCH 212/314] use different num threads for optimizer --- .../priority_search/priority_search_with_regressor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opto/features/priority_search/priority_search_with_regressor.py b/opto/features/priority_search/priority_search_with_regressor.py index f6374894..4e08cb18 100644 --- a/opto/features/priority_search/priority_search_with_regressor.py +++ b/opto/features/priority_search/priority_search_with_regressor.py @@ -456,7 +456,7 @@ def _backward(n): args_list = [(n,) for n in range(n_batches)] optimizers = async_run([_backward]*n_batches, # run the optimizer step for each agent in parallel args_list=args_list, - max_workers=self.num_threads, # use the number of threads specified in the class + max_workers=1000, # use the number of threads specified in the class description=None) assert len(optimizers) == n_batches, "Number of optimizers must match number of batch rollouts." # need to copy optimizer for the n_proposals @@ -488,7 +488,7 @@ def _step(n): args_list = [(n,) for n in range(n_batches*n_proposals)] update_dicts = async_run([_step]*n_batches*n_proposals, # run the optimizer step for each agent in parallel args_list=args_list, - max_workers=self.num_threads, # use the number of threads specified in the class + max_workers=1000, # use the number of threads specified in the class description=f"Calling optimizers: Generating {n_proposals} proposals for each of {n_batches} batches",) # update_dicts is a list of dicts of length n_batches * n_proposals From da98a16d85b04a1508ad9b51874f72c4d9b9e293 Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Mon, 15 Sep 2025 00:51:25 -0500 Subject: [PATCH 213/314] calculate mean score using valid scores --- opto/features/priority_search/priority_search_with_regressor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opto/features/priority_search/priority_search_with_regressor.py b/opto/features/priority_search/priority_search_with_regressor.py index 4e08cb18..0817e88d 100644 --- a/opto/features/priority_search/priority_search_with_regressor.py +++ b/opto/features/priority_search/priority_search_with_regressor.py @@ -101,7 +101,7 @@ def mean_score(self): """ Compute the score of the candidate based on the rollouts. """ if not self.rollouts: return None - scores = [r['score'] for r in self.rollouts] + scores = [r['score'] for r in self.rollouts if r['score'] is not None] return np.mean(scores) if scores else None def compute_score_confidence(self, min_score, max_score, scaling_constant=1.0, total_trials=1): From 966f5054738d6d9f49eecc56810eaf3ebe22d41e Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 15 Sep 2025 16:47:00 +0000 Subject: [PATCH 214/314] Renam eval_ to test_ for consistency. --- docs/tutorials/minibatch.ipynb | 4 ++-- docs/tutorials/trainers.ipynb | 2 +- examples/gsm8k_trainer_example.py | 4 ++-- .../run_bigbench_trace_async.py | 2 +- examples/priority_search_example.py | 8 +++---- examples/search_algo_example.py | 4 ++-- examples/train_model.py | 4 ++-- opto/features/priority_search/examples.py | 12 +++++----- .../priority_search/priority_search.py | 8 +++---- .../priority_search_modified.py | 18 +++++++-------- .../priority_search/search_template.py | 9 ++++---- opto/trainer/algorithms/UCBsearch.py | 4 ++-- opto/trainer/algorithms/aggregator.py | 4 ++-- opto/trainer/algorithms/basic_algorithms.py | 22 +++++++++---------- 14 files changed, 52 insertions(+), 53 deletions(-) diff --git a/docs/tutorials/minibatch.ipynb b/docs/tutorials/minibatch.ipynb index dd1ad029..8ee40cbc 100644 --- a/docs/tutorials/minibatch.ipynb +++ b/docs/tutorials/minibatch.ipynb @@ -531,7 +531,7 @@ "seed = 42\n", "num_epochs = 1\n", "batch_size = 2\n", - "eval_frequency = -1\n", + "test_frequency = -1\n", "num_threads = 3\n", "verbose = True\n", "\n", @@ -832,7 +832,7 @@ " train_dataset,\n", " num_epochs=num_epochs,\n", " batch_size=batch_size,\n", - " eval_frequency=eval_frequency,\n", + " test_frequency=test_frequency,\n", " test_dataset=test_dataset,\n", " num_threads=num_threads,\n", " verbose='output')\n", diff --git a/docs/tutorials/trainers.ipynb b/docs/tutorials/trainers.ipynb index 84f64fa8..cf9a8809 100644 --- a/docs/tutorials/trainers.ipynb +++ b/docs/tutorials/trainers.ipynb @@ -356,7 +356,7 @@ " \"test_dataset\": test_dataset,\n", " \"validate_dataset\": validate_dataset,\n", " \"validate_guide\": validate_guide,\n", - " \"eval_frequency\": 2,\n", + " \"test_frequency\": 2,\n", " \"log_frequency\": 2,\n", " #for Basic Search\n", " \"num_proposals\": 2,\n", diff --git a/examples/gsm8k_trainer_example.py b/examples/gsm8k_trainer_example.py index dd87b749..c4750136 100644 --- a/examples/gsm8k_trainer_example.py +++ b/examples/gsm8k_trainer_example.py @@ -55,7 +55,7 @@ def main(): seed = 42 num_epochs = 1 batch_size = 1 - eval_frequency = -1 + test_frequency = -1 num_threads = 3 verbose = True teacher_model = None # use default model @@ -85,7 +85,7 @@ def main(): train_dataset, num_epochs=num_epochs, batch_size=batch_size, - eval_frequency=eval_frequency, + test_frequency=test_frequency, test_dataset=test_dataset, num_threads=num_threads, verbose='output' if verbose else False) diff --git a/examples/minibatch_bbh_aynsc/run_bigbench_trace_async.py b/examples/minibatch_bbh_aynsc/run_bigbench_trace_async.py index 8211139c..c5689a97 100644 --- a/examples/minibatch_bbh_aynsc/run_bigbench_trace_async.py +++ b/examples/minibatch_bbh_aynsc/run_bigbench_trace_async.py @@ -258,7 +258,7 @@ def learn_predict(dp, optimizer, examples, val_examples, task_name, save_dir): test_dataset=val_dataset, num_epochs=1, batch_size=4, # Process multiple examples at a time - eval_frequency=1, # Evaluate every 5 steps + test_frequency=1, # Evaluate every 5 steps save_frequency=5, # Save every 5 steps save_dir=save_dir, num_threads=4, diff --git a/examples/priority_search_example.py b/examples/priority_search_example.py index fee40da6..a26ef452 100644 --- a/examples/priority_search_example.py +++ b/examples/priority_search_example.py @@ -59,8 +59,8 @@ def main(): num_proposals = 3 # number of proposals to generate for each query num_candidates = 2 # number of candidates for exploration score_range = (0, 1) # range of the score for the guide - eval_frequency = -1 - num_eval_samples = 2 + test_frequency = -1 + num_test_samples = 2 score_function = 'mean' num_threads = 10 @@ -94,14 +94,14 @@ def main(): train_dataset, num_epochs=num_epochs, batch_size=batch_size, - eval_frequency=eval_frequency, + test_frequency=test_frequency, test_dataset=test_dataset, num_threads=num_threads, num_batches=num_batches, num_proposals=num_proposals, num_candidates=num_candidates, score_range=score_range, - num_eval_samples=num_eval_samples, + num_test_samples=num_test_samples, score_function=score_function, verbose='output' if verbose else False) diff --git a/examples/search_algo_example.py b/examples/search_algo_example.py index e40cfa7e..473ce202 100644 --- a/examples/search_algo_example.py +++ b/examples/search_algo_example.py @@ -213,7 +213,7 @@ def main(): help='Training batch size') parser.add_argument('--num_threads', type=int, default=10, help='Number of threads for parallel processing') - parser.add_argument('--eval_frequency', type=int, default=2, + parser.add_argument('--test_frequency', type=int, default=2, help='How often to run evaluation') parser.add_argument('--log_frequency', type=int, default=10, help='How often to log results') @@ -342,7 +342,7 @@ def main(): "test_dataset": test_dataset, "validate_dataset": validate_dataset, "validate_guide": validate_guide, - "eval_frequency": args.eval_frequency, + "test_frequency": args.test_frequency, "log_frequency": args.log_frequency, "validation_dataset_size": args.validation_dataset_size, } diff --git a/examples/train_model.py b/examples/train_model.py index 10b76e0a..0c0c0f2c 100644 --- a/examples/train_model.py +++ b/examples/train_model.py @@ -51,7 +51,7 @@ def main(): seed = 42 num_epochs = 1 batch_size = 3 # number of queries to sample from the training data - eval_frequency = -1 + test_frequency = -1 num_threads = 10 datasize = 5 @@ -71,7 +71,7 @@ def main(): # trainer kwargs num_epochs=num_epochs, batch_size=batch_size, - eval_frequency=eval_frequency, + test_frequency=test_frequency, num_threads=num_threads, verbose='output', ) diff --git a/opto/features/priority_search/examples.py b/opto/features/priority_search/examples.py index 53d4ee90..281b85aa 100644 --- a/opto/features/priority_search/examples.py +++ b/opto/features/priority_search/examples.py @@ -38,7 +38,7 @@ def train(self, # evaluation test_dataset = None, # dataset of (x, info) pairs to evaluate the agent test_frequency: Union[int, None] = 1, # frequency of evaluation - num_eval_samples: int = 1, # number of samples to use to evaluate each input + num_test_samples: int = 1, # number of samples to use to evaluate each input # logging log_frequency = None, # frequency of logging save_frequency: Union[int, None] = None, # frequency of saving the agent @@ -69,7 +69,7 @@ def train(self, verbose=verbose, test_dataset=test_dataset, test_frequency=test_frequency, - num_eval_samples=num_eval_samples, + num_test_samples=num_test_samples, log_frequency=log_frequency, save_frequency=save_frequency, save_path=save_path, @@ -110,7 +110,7 @@ def train(self, # evaluation test_dataset = None, # dataset of (x, info) pairs to evaluate the agent test_frequency: Union[int, None] = 1, # frequency of evaluation - num_eval_samples: int = 1, # number of samples to use to evaluate each input + num_test_samples: int = 1, # number of samples to use to evaluate each input # logging log_frequency = None, # frequency of logging save_frequency: Union[int, None] = None, # frequency of saving the agent @@ -140,7 +140,7 @@ def train(self, verbose=verbose, test_dataset=test_dataset, test_frequency=test_frequency, - num_eval_samples=num_eval_samples, + num_test_samples=num_test_samples, log_frequency=log_frequency, save_frequency=save_frequency, save_path=save_path, @@ -176,7 +176,7 @@ def train(self, # evaluation test_dataset = None, # dataset of (x, info) pairs to evaluate the agent test_frequency: Union[int, None] = 1, # frequency of evaluation - num_eval_samples: int = 1, # number of samples to use to evaluate each input + num_test_samples: int = 1, # number of samples to use to evaluate each input # logging log_frequency = None, # frequency of logging save_frequency: Union[int, None] = None, # frequency of saving the agent @@ -203,7 +203,7 @@ def train(self, verbose=verbose, test_dataset=test_dataset, test_frequency=test_frequency, - num_eval_samples=num_eval_samples, + num_test_samples=num_test_samples, log_frequency=log_frequency, save_frequency=save_frequency, save_path=save_path, diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index 5adc2c54..7bd1efec 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -217,7 +217,7 @@ class PrioritySearch(SearchTemplate): It provides a scalable template for implementing search algorithms based on asynchronous generation, validation, and testing. In each iteration, 1. It proposes a best agent and a set of `num_candidates` exploration agents that have the highest scores in the priority queue. - 2. The best agent is tested for performance if eval_frequency is met. + 2. The best agent is tested for performance if test_frequency is met. 3. `num_batches` minibatches of `batch_size` samples are drawn from the training dataset, and the exploration agents are run on the samples. This creates a set of agent rollouts, where each rollout contains the agent module, input, info, target, score, and feedback. For each agent, rollouts of each minibatch are grouped together as a connected subgraph (represented as the BatchRollout object). In total, this step creates `num_candidates * num_batches` subgraphs. 4. Optimizer is run on each subgraph to propose new parameters for the agents. `num_proposals` proposals are generated for each subgraph. This results in `num_subgraphs * num_proposals` total proposals. 5. The proposed parameters are validated by running the agents on the validation dataset, which can be the current batch or a separate validation dataset when provided. When validate_exploration_candidates is set to True, the exploration candidates are also validated. @@ -250,7 +250,7 @@ def train(self, # evaluation test_dataset = None, # dataset of (x, info) pairs to evaluate the agent test_frequency: Union[int, None] = 1, # frequency of evaluation (set it to be negative to skip the first evaluation) - num_eval_samples: int = 1, # number of times to evaluate each input; when greater than 1, the scores are averaged. + num_test_samples: int = 1, # number of times to evaluate each input; when greater than 1, the scores are averaged. # logging log_frequency = None, # frequency of logging save_frequency: Union[int, None] = None, # frequency of saving the agent @@ -281,7 +281,7 @@ def train(self, verbose (bool, optional): Whether to print the output of the agent. Defaults to False. test_dataset (list, optional): A list of (x, info) pairs to evaluate the agent. If None, no evaluation is performed. Defaults to None. test_frequency (int or None, optional): The frequency of evaluation. If None, no evaluation is performed. If negative, skips the first evaluation. Defaults to 1. - num_eval_samples (int, optional): The number of times to evaluate each input; when greater than 1, the scores are averaged. Defaults to 1. + num_test_samples (int, optional): The number of times to evaluate each input; when greater than 1, the scores are averaged. Defaults to 1. log_frequency (int or None, optional): The frequency of logging. If None, no logging is performed. Defaults to None. save_frequency (int or None, optional): The frequency of saving the agent. If None, no saving is performed. Defaults to None. save_path (str, optional): The path to save the agent. Defaults to "checkpoints/agent.pkl". @@ -331,7 +331,7 @@ def train(self, verbose=verbose, test_dataset=test_dataset, test_frequency=test_frequency, - num_eval_samples=num_eval_samples, + num_test_samples=num_test_samples, log_frequency=log_frequency, save_frequency=save_frequency, save_path=save_path, diff --git a/opto/features/priority_search/priority_search_modified.py b/opto/features/priority_search/priority_search_modified.py index c4404432..7088bae1 100644 --- a/opto/features/priority_search/priority_search_modified.py +++ b/opto/features/priority_search/priority_search_modified.py @@ -210,7 +210,7 @@ def _criterion(x): p = criterion(candidate) return p if p is not None else 0 return max(self.memory, key=lambda x: _criterion(x)) - + def reorder_according_to_predicted_scores(self): """ Reorder the heap memory according to the predicted scores. """ # Now all ModuleCandidate objects in the heap memory have predicted scores. Should modify the old score to the negative predicted scores, then use heapq.heapify to reorder the heap memory. @@ -224,7 +224,7 @@ class PrioritySearch(SearchTemplate): It provides a scalable template for implementing search algorithms based on asynchronous generation, validation, and testing. In each iteration, 1. It proposes a best agent and a set of `num_candidates` exploration agents that have the highest scores in the priority queue. - 2. The best agent is tested for performance if eval_frequency is met. + 2. The best agent is tested for performance if test_frequency is met. 3. `num_batches` minibatches of `batch_size` samples are drawn from the training dataset, and the exploration agents are run on the samples. This creates a set of agent rollouts, where each rollout contains the agent module, input, info, target, score, and feedback. For each agent, rollouts of each minibatch are grouped together as a connected subgraph (represented as the BatchRollout object). In total, this step creates `num_candidates * num_batches` subgraphs. 4. Optimizer is run on each subgraph to propose new parameters for the agents. `num_proposals` proposals are generated for each subgraph. This results in `num_subgraphs * num_proposals` total proposals. 5. The proposed parameters are validated by running the agents on the validation dataset, which can be the current batch or a separate validation dataset when provided. When validate_exploration_candidates is set to True, the exploration candidates are also validated. @@ -257,7 +257,7 @@ def train(self, # evaluation test_dataset = None, # dataset of (x, info) pairs to evaluate the agent test_frequency: Union[int, None] = 1, # frequency of evaluation (set it to be negative to skip the first evaluation) - num_eval_samples: int = 1, # number of times to evaluate each input; when greater than 1, the scores are averaged. + num_test_samples: int = 1, # number of times to evaluate each input; when greater than 1, the scores are averaged. # logging log_frequency = None, # frequency of logging save_frequency: Union[int, None] = None, # frequency of saving the agent @@ -288,7 +288,7 @@ def train(self, verbose (bool, optional): Whether to print the output of the agent. Defaults to False. test_dataset (list, optional): A list of (x, info) pairs to evaluate the agent. If None, no evaluation is performed. Defaults to None. test_frequency (int or None, optional): The frequency of evaluation. If None, no evaluation is performed. If negative, skips the first evaluation. Defaults to 1. - num_eval_samples (int, optional): The number of times to evaluate each input; when greater than 1, the scores are averaged. Defaults to 1. + num_test_samples (int, optional): The number of times to evaluate each input; when greater than 1, the scores are averaged. Defaults to 1. log_frequency (int or None, optional): The frequency of logging. If None, no logging is performed. Defaults to None. save_frequency (int or None, optional): The frequency of saving the agent. If None, no saving is performed. Defaults to None. save_path (str, optional): The path to save the agent. Defaults to "checkpoints/agent.pkl". @@ -334,11 +334,11 @@ def train(self, num_batches=num_batches, score_range=score_range, num_epochs=num_epochs, - num_threads=num_threads, + num_threads=num_threads, verbose=verbose, test_dataset=test_dataset, - eval_frequency=test_frequency, - num_eval_samples=num_eval_samples, + test_frequency=test_frequency, + num_test_samples=num_test_samples, log_frequency=log_frequency, save_frequency=save_frequency, save_path=save_path, @@ -365,7 +365,7 @@ def update(self, validate_results = self.validate(candidates, samples, verbose=verbose, **kwargs) # this updates the priority queue # # 3. Update the priority queue with the validation results self.update_memory(validate_results, verbose=verbose, **kwargs) # samples are provided here in case candidates do not capture full information - + else: # The first iteration. max_mem_size = self.memory.size if self.memory.size is not None else float('inf') initial_update_dict = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} @@ -589,7 +589,7 @@ def update_memory(self, validate_results, verbose: bool = False, **kwargs): print("--- Updating memory with validation results...") if verbose else None for candidate, rollouts in validate_results.items(): candidate.add_rollouts(rollouts) # add the rollouts to the candidate - + # priority = self.compute_exploration_priority(candidate) # compute the priority for the candidate placeholder_priority = self.max_score self.memory.push(placeholder_priority, candidate) diff --git a/opto/features/priority_search/search_template.py b/opto/features/priority_search/search_template.py index 100be87b..f6b5a603 100644 --- a/opto/features/priority_search/search_template.py +++ b/opto/features/priority_search/search_template.py @@ -135,8 +135,8 @@ def train(self, # evaluation test_dataset = None, # dataset of (x, info) pairs to evaluate the agent; if None, use train_dataset test_guide = None, # guide to provide scores for the test set; if None, use guide - eval_frequency: Union[int, None] = 1, # frequency of evaluation NOTE set test_frequency < 0 to skip first evaluation - num_eval_samples: int = 1, # number of samples to use to evaluate each input + test_frequency: Union[int, None] = 1, # frequency of evaluation NOTE set test_frequency < 0 to skip first evaluation + num_test_samples: int = 1, # number of samples to use to evaluate each input # logging log_frequency = None, # frequency of logging save_frequency: Union[int, None] = None, # frequency of saving the agent @@ -146,12 +146,11 @@ def train(self, assert 'subbatch_size' not in kwargs, "subbatch_size should not be provided in kwargs." ## Setup - test_frequency = eval_frequency # use eval_frequency as test_frequency # NOTE legacy notation log_frequency = log_frequency or test_frequency # frequency of logging (default to test_frequency) self.num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads test_dataset = test_dataset or train_dataset # default to train_dataset if test_dataset is not provided test_guide = test_guide or guide - self.num_eval_samples = num_eval_samples # number of samples to use to evaluate each input + self.num_test_samples = num_test_samples # number of samples to use to evaluate each input if score_range is None: score_range = (-np.inf, np.inf) assert len(score_range) == 2, "score_range must be a tuple (min_score, max_score)." @@ -286,7 +285,7 @@ def test(self, test_dataset, guide): min_score = self.min_score # Test the agent's performance test_score = self.evaluate(self.agent, guide, test_dataset['inputs'], test_dataset['infos'], - min_score=min_score, num_threads=self.num_threads,num_samples=self.num_eval_samples, + min_score=min_score, num_threads=self.num_threads,num_samples=self.num_test_samples, description=f"Evaluating agent") # and log # check if the test_score is within the score range if not (self.min_score <= test_score <= self.max_score): diff --git a/opto/trainer/algorithms/UCBsearch.py b/opto/trainer/algorithms/UCBsearch.py index 21bc9455..51e2b1a9 100644 --- a/opto/trainer/algorithms/UCBsearch.py +++ b/opto/trainer/algorithms/UCBsearch.py @@ -140,7 +140,7 @@ def train(self, num_search_iterations: int = 100, train_batch_size: int = 2, evaluation_batch_size: int = 20, # Renamed from validation_batch_size, used for all explicit evaluations - eval_frequency: int = 1, + test_frequency: int = 1, log_frequency: Optional[int] = None, save_frequency: Optional[int] = None, save_path: str = "checkpoints/ucb_agent.pkl", @@ -157,7 +157,7 @@ def train(self, validation_dataset = train_dataset num_threads = num_threads or self.num_threads - log_frequency = log_frequency or eval_frequency + log_frequency = log_frequency or test_frequency self.min_score = min_score_for_agent_update # Used by parent's evaluate if called, or our own _evaluate_candidate total_samples = 0 diff --git a/opto/trainer/algorithms/aggregator.py b/opto/trainer/algorithms/aggregator.py index a1d30a67..c04e6b61 100644 --- a/opto/trainer/algorithms/aggregator.py +++ b/opto/trainer/algorithms/aggregator.py @@ -67,7 +67,7 @@ def train(self, num_epochs: int = 1, # number of training epochs batch_size: int = 1, # batch size for updating the agent test_dataset = None, # dataset of (x, info) pairs to evaluate the agent - eval_frequency: int = 1, # frequency of evaluation + test_frequency: int = 1, # frequency of evaluation log_frequency: Union[int, None] = None, # frequency of logging min_score: Union[int, None] = None, # minimum score to update the agent verbose: Union[bool, str] = False, # whether to print the output of the agent @@ -78,7 +78,7 @@ def train(self, self.stepsize = stepsize # used in self.aggregate super().train(guide, train_dataset, num_epochs=num_epochs, batch_size=batch_size, - test_dataset=test_dataset, eval_frequency=eval_frequency, + test_dataset=test_dataset, test_frequency=test_frequency, log_frequency=log_frequency, min_score=min_score, verbose=verbose, **kwargs) diff --git a/opto/trainer/algorithms/basic_algorithms.py b/opto/trainer/algorithms/basic_algorithms.py index 76597dcb..2b08f91e 100644 --- a/opto/trainer/algorithms/basic_algorithms.py +++ b/opto/trainer/algorithms/basic_algorithms.py @@ -58,8 +58,8 @@ def train(self, num_epochs: int = 1, # number of training epochs batch_size: int = 1, # batch size for updating the agent test_dataset = None, # dataset of (x, info) pairs to evaluate the agent - eval_frequency: int = 1, # frequency of evaluation - num_eval_samples: int = 1, # number of samples to use to evaluate each input + test_frequency: int = 1, # frequency of evaluation + num_test_samples: int = 1, # number of samples to use to evaluate each input log_frequency: Union[int, None] = None, # frequency of logging save_frequency: Union[int, None] = None, # frequency of saving the agent save_path: str = "checkpoints/agent.pkl", # path to save the agent @@ -75,16 +75,16 @@ def train(self, 3. Evaluate the agent on the test dataset and log the results. """ - log_frequency = log_frequency or eval_frequency # frequency of logging (default to eval_frequency) + log_frequency = log_frequency or test_frequency # frequency of logging (default to test_frequency) num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads test_dataset = test_dataset or train_dataset # default to train_dataset if test_dataset is not provided - self.num_eval_samples = num_eval_samples # number of samples to use to evaluate each input + self.num_test_samples = num_test_samples # number of samples to use to evaluate each input # Evaluate the agent before learning - if eval_frequency > 0: + if test_frequency > 0: test_score = self.evaluate(self.agent, guide, test_dataset['inputs'], test_dataset['infos'], min_score=min_score, num_threads=num_threads, - num_samples=self.num_eval_samples, + num_samples=self.num_test_samples, description=f"Evaluating agent (iteration {self.n_iters})") # and log self.logger.log('Average test score', test_score, self.n_iters, color='green') @@ -121,10 +121,10 @@ def train(self, self.n_iters += 1 # Evaluate the agent after update - if test_dataset is not None and self.n_iters % eval_frequency == 0: + if test_dataset is not None and self.n_iters % test_frequency == 0: test_score = self.evaluate(self.agent, guide, test_dataset['inputs'], test_dataset['infos'], min_score=min_score, num_threads=num_threads, - num_samples=self.num_eval_samples, + num_samples=self.num_test_samples, description=f"Evaluating agent (iteration {self.n_iters})") # and log self.logger.log('Average test score', test_score, self.n_iters, color='green') @@ -168,7 +168,7 @@ def has_improvement(self, xs, guide, infos, current_score, current_outputs, back num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads new_score = self.evaluate(self.agent, guide, xs, infos, num_threads=num_threads, description=f"Checking improvement (iteration {self.n_iters})", - num_samples=self.num_eval_samples, + num_samples=self.num_test_samples, *args, **kwargs) # evaluate the updated agent if new_score is None or new_score <= current_score - threshold: print_color(f"Update rejected: Current score {current_score}, New score {new_score}", 'red') @@ -269,7 +269,7 @@ def train(self, num_epochs = 1, # number of training epochs batch_size = 1, # batch size for updating the agent test_dataset = None, # dataset of (x, info) pairs to evaluate the agent - eval_frequency = 1, # frequency of evaluation + test_frequency = 1, # frequency of evaluation log_frequency = None, # frequency of logging min_score = None, # minimum score to update the agent verbose = False, # whether to print the output of the agent @@ -284,7 +284,7 @@ def train(self, self.current_score = None return super().train(guide, train_dataset, num_epochs=num_epochs, batch_size=batch_size, - test_dataset=test_dataset, eval_frequency=eval_frequency, log_frequency=log_frequency, + test_dataset=test_dataset, test_frequency=test_frequency, log_frequency=log_frequency, min_score=min_score, verbose=verbose, num_threads=num_threads, **kwargs) # This code should be reusable for other algorithms From 8ee8fbb0c6094dd24379036dc26ece10e61fb64c Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 15 Sep 2025 17:04:20 +0000 Subject: [PATCH 215/314] Fix the bug that the first candidate can be overwritten. --- .../priority_search/priority_search.py | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index 7bd1efec..b105abff 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -27,11 +27,14 @@ def __init__(self, stats (dict): A dictionary of statistics about the candidate. """ assert isinstance(base_module, trace.Module), "base_module must be a trace.Module." - if update_dict is not None: + if update_dict is None: + # if no update_dict is provided, use the base_module's parameters as the update_dict + update_dict = {p: p.data for p in base_module.parameters()} + else: assert isinstance(optimizer, Optimizer), "optimizer must be an instance of Optimizer when update_dict is provided." - + assert update_dict is not None, "update_dict must be provided." self.base_module = base_module - self.update_dict = update_dict if update_dict is not None else {} + self.update_dict = update_dict self.optimizer = optimizer # the optimizer used to generate the update_dict; can be None, which indicates the base_module is used. self.update_dict = remap_update_dict(self.base_module, self.update_dict) self.rollouts = [] # list of dicts containing the rollout information (not BatchRollout, but a list of dicts) @@ -314,7 +317,9 @@ def train(self, self.ucb_exploration_constant = ucb_exploration_constant self._exploration_candidates = None # This stores the latest candidates used for exploration + self._exploration_candidates_priority = None # This stores the latest candidates' priorities used for exploration self._best_candidate = None # This stores the latest best candidate used for exploitation + self._best_candidate_priority = None # This stores the latest best candidate's priority used for exploitation self.memory = HeapMemory(size=memory_size) # Initialize the heap memory with a size limit @@ -357,8 +362,8 @@ def update(self, while len(self.memory) < min(max_mem_size, self.num_candidates): self.memory.push(self.max_score, ModuleCandidate(self.agent, optimizer=self.optimizer)) # Push the base agent as the first candidate (This gives the initialization of the priority queue) # 4. Explore and exploit the priority queue - self._best_candidate, info_exploit = self.exploit(verbose=verbose, **kwargs) # get the best candidate (ModuleCandidate) from the priority queue - self._exploration_candidates, info_explore = self.explore(verbose=verbose, **kwargs) # List of ModuleCandidates + self._best_candidate, self._best_candidate_priority, info_exploit = self.exploit(verbose=verbose, **kwargs) # get the best candidate (ModuleCandidate) from the priority queue + self._exploration_candidates, self._exploration_candidates_priority, info_explore = self.explore(verbose=verbose, **kwargs) # List of ModuleCandidates # TODO Log information about the update info_log = { 'n_iters': self.n_iters, # number of iterations @@ -515,7 +520,7 @@ def validate(self, # validate the agents in the validate_dataset exploration_agents = [c.get_module() for c in exploration_candidates] # get the modules from the exploration candidates exploration_samples = Samples(*self.validate_sampler.sample(exploration_agents, - description_prefix='Validating exploration candidates: ')) # sample the exploration agents + description_prefix='Validating exploration candidates: ')) # sample the exploration agents validate_samples.add_samples(exploration_samples) # append the exploration samples to the validate_samples @@ -587,7 +592,7 @@ def explore(self, verbose: bool = False, **kwargs): # pop top self.num_candidates candidates from the priority queue # self._best_candidate is the exploited candidate from the previous iteration top_candidates = [self._best_candidate] if self.use_best_candidate_to_explore else [] - priorities = [] # to store the priorities of the candidates for logging + priorities = [self._best_candidate_priority] if self.use_best_candidate_to_explore else [] # to store the priorities of the candidates for logging while len(top_candidates) < self.num_candidates and len(self.memory) > 0: neg_priority, candidate = self.memory.pop() # pop the top candidate from the priority queue priority = - neg_priority # remember that we stored negative scores in the priority queue @@ -606,7 +611,7 @@ def explore(self, verbose: bool = False, **kwargs): 'exploration_candidates_average_num_rollouts': np.mean([c.num_rollouts for c in top_candidates]), } - return top_candidates, info_dict + return top_candidates, priorities, info_dict def exploit(self, verbose: bool = False, **kwargs) -> Tuple[ModuleCandidate, Dict[str, Any]]: """ Exploit the best candidate from the priority queue. This method should not change the priority queue. @@ -621,7 +626,7 @@ def exploit(self, verbose: bool = False, **kwargs) -> Tuple[ModuleCandidate, Dic raise ValueError("The priority queue is empty. Cannot exploit.") neg_priority, best_candidate = self.memory.best(self.compute_exploitation_priority) # (priority, candidate) priority = - neg_priority # remember that we stored negative scores in the priority queue - return best_candidate, { + return best_candidate, priority, { 'best_candidate_priority': priority, # remember that we stored negative scores in the priority queue 'best_candidate_mean_score': best_candidate.mean_score(), # mean score of the candidate's rollouts 'best_candidate_num_rollouts': best_candidate.num_rollouts, # number of rollouts of the candidate From 78d147b4198da2a74c623b93b4ce574819d9a3fc Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 15 Sep 2025 17:10:40 +0000 Subject: [PATCH 216/314] fix the bug of test_priority_search due to the api change. --- tests/unit_tests/test_priority_search.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit_tests/test_priority_search.py b/tests/unit_tests/test_priority_search.py index d6ac4a17..f83de3f6 100644 --- a/tests/unit_tests/test_priority_search.py +++ b/tests/unit_tests/test_priority_search.py @@ -86,7 +86,7 @@ def validate(self, candidates, samples, verbose=False, **kwargs): def exploit(self, **kwargs): print("[UnitTest] Exploit at iteration:", self.n_iters) - candidate, info_dict = super().exploit(**kwargs) + candidate, priority, info_dict = super().exploit(**kwargs) assert isinstance(candidate, ModuleCandidate), "Expected candidate to be an instance of ModuleCandidate" assert isinstance(info_dict, dict), "Expected info_dict to be a dictionary" @@ -97,12 +97,12 @@ def exploit(self, **kwargs): candidate.update_dict[p] = p._data + 100 # This will be different the exploration candidates - return candidate, info_dict + return candidate, priority, info_dict def explore(self, **kwargs): print("[UnitTest] Explore at iteration:", self.n_iters) - candidates, info_dict = super().explore(**kwargs) + candidates, priorities, info_dict = super().explore(**kwargs) assert isinstance(candidates, list) assert isinstance(info_dict, dict) @@ -112,7 +112,7 @@ def explore(self, **kwargs): else: assert len(candidates) <= self.num_candidates, f"Expect no more than {self.num_candidates} candidates at iter {self.n_iters}, got {len(candidates)}" assert all(isinstance(c, ModuleCandidate) for c in candidates), "All candidates should be ModuleCandidate instances" - return candidates, info_dict + return candidates, priorities, info_dict From ab4e25f1a468f15b2fc4a410be4b473eb58366fb Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Mon, 15 Sep 2025 13:10:37 -0500 Subject: [PATCH 217/314] Move the auto-retry function to opto.utils --- .../priority_search_modified.py | 4 +- opto/features/priority_search/utils.py | 79 ------------------ opto/utils/auto_retry.py | 80 +++++++++++++++++++ 3 files changed, 82 insertions(+), 81 deletions(-) create mode 100644 opto/utils/auto_retry.py diff --git a/opto/features/priority_search/priority_search_modified.py b/opto/features/priority_search/priority_search_modified.py index 7088bae1..18bf3e57 100644 --- a/opto/features/priority_search/priority_search_modified.py +++ b/opto/features/priority_search/priority_search_modified.py @@ -11,7 +11,7 @@ from opto.features.priority_search.search_template import SearchTemplate, Samples, BatchRollout from opto.features.priority_search.utils import set_module_parameters, remap_update_dict, create_module_from_update_dict, is_module_copy from opto.features.priority_search.module_regressor import ModuleCandidateRegressor - +from opto.utils.auto_retry import retry_with_exponential_backoff class ModuleCandidate: """ A container used by PrioritySearch to store a candidate module as (its base module and update dictionary) and its statistics. """ @@ -354,7 +354,7 @@ def update(self, # samples is None in the first iteration if samples is not None: # 1. Propose new parameters based on running LLM optimizers on the collected samples - from opto.features.priority_search.utils import retry_with_exponential_backoff + candidates = retry_with_exponential_backoff( lambda: self.propose(samples, verbose=verbose, **kwargs), max_retries=10, diff --git a/opto/features/priority_search/utils.py b/opto/features/priority_search/utils.py index 0b04aae4..4aae37b6 100644 --- a/opto/features/priority_search/utils.py +++ b/opto/features/priority_search/utils.py @@ -10,7 +10,6 @@ from opto.trainer.algorithms.basic_algorithms import Minibatch, Trainer, batchify from opto.trainer.loader import DataLoader from opto.features.priority_search.sampler import Sampler, BatchRollout -import time # Some helper functions to convert between trace.Module and update_dict @@ -84,81 +83,3 @@ def create_module_from_update_dict(agent, update_dict): set_module_parameters(new_agent, update_dict) # set the parameters of the new agent return new_agent # return the new agent -def retry_with_exponential_backoff(func, max_retries=10, base_delay=1.0, operation_name="operation"): - """ - Retry a function with exponential backoff for rate limit and other transient errors. - - Args: - func: Function to retry (should be a callable with no arguments) - max_retries: Maximum number of retry attempts - base_delay: Base delay for exponential backoff - operation_name: Name of the operation for logging - - Returns: - Result of the function call - - Raises: - The last exception encountered if all retries fail - """ - import time - - for retry_attempt in range(max_retries): - try: - return func() - except Exception as e: - error_str = str(e).lower() - error_type = type(e).__name__.lower() - - # Check if it's a retryable error - retryable_errors = [ - 'rate limit', 'timeout', 'temporary', 'service unavailable', - 'internal server error', 'bad gateway', 'service temporarily unavailable', - 'too many requests', 'quota', 'overloaded', 'resource has been exhausted', - 'resource_exhausted', 'ratelimiterror', 'quotaexceedederror', - 'connection error', 'network', 'json decode' - ] - - # Also check specific litellm exceptions - retryable_exception_types = [ - 'ratelimiterror', 'timeouterror', 'apiconnectionerror', - 'serviceunavailableerror', 'internalservererror', 'jsondecodeerror' - ] - - is_retryable = ( - any(err in error_str for err in retryable_errors) or - any(exc_type in error_type for exc_type in retryable_exception_types) or - 'code": 429' in error_str or # HTTP 429 Too Many Requests - 'code": 503' in error_str or # HTTP 503 Service Unavailable - 'code": 502' in error_str or # HTTP 502 Bad Gateway - 'code": 500' in error_str # HTTP 500 Internal Server Error - ) - - if retry_attempt == max_retries - 1: - # Last attempt failed - raise RuntimeError(f"{operation_name}: Failed after {max_retries} attempts. Error: {e}") - - elif is_retryable: - # Special handling for rate limit errors - use longer delays - is_rate_limit = ( - 'rate limit' in error_str or 'ratelimiterror' in error_type or - 'quota' in error_str or 'resource has been exhausted' in error_str or - 'code": 429' in error_str - ) - - if is_rate_limit: - # Longer delays for rate limits: 2, 8, 18, 32, 50 seconds - delay = 2 * (retry_attempt + 1) ** 2 + retry_attempt - else: - # Standard exponential backoff for other errors - delay = base_delay * (2 ** retry_attempt) + (0.1 * retry_attempt) - - error_type_desc = "Rate limit" if is_rate_limit else "Retryable error" - # print(f"{operation_name}: {error_type_desc} - Retry {retry_attempt + 1}/{max_retries} after {delay:.1f}s. Error: {e}") - time.sleep(delay) - else: - # Non-retryable error - print(f"{operation_name}: Non-retryable error: {e}") - raise e - - # This should never be reached, but just in case - raise RuntimeError(f"{operation_name}: Unexpected error - reached end of retry loop") \ No newline at end of file diff --git a/opto/utils/auto_retry.py b/opto/utils/auto_retry.py new file mode 100644 index 00000000..2db468e3 --- /dev/null +++ b/opto/utils/auto_retry.py @@ -0,0 +1,80 @@ +# A general-purpose auto retry function. + +def retry_with_exponential_backoff(func, max_retries=10, base_delay=1.0, operation_name="operation"): + """ + Retry a function with exponential backoff for rate limit and other transient errors. + + Args: + func: Function to retry (should be a callable with no arguments) + max_retries: Maximum number of retry attempts + base_delay: Base delay for exponential backoff + operation_name: Name of the operation for logging + + Returns: + Result of the function call + + Raises: + The last exception encountered if all retries fail + """ + import time + + for retry_attempt in range(max_retries): + try: + return func() + except Exception as e: + error_str = str(e).lower() + error_type = type(e).__name__.lower() + + # Check if it's a retryable error + retryable_errors = [ + 'rate limit', 'timeout', 'temporary', 'service unavailable', + 'internal server error', 'bad gateway', 'service temporarily unavailable', + 'too many requests', 'quota', 'overloaded', 'resource has been exhausted', + 'resource_exhausted', 'ratelimiterror', 'quotaexceedederror', + 'connection error', 'network', 'json decode' + ] + + # Also check specific litellm exceptions + retryable_exception_types = [ + 'ratelimiterror', 'timeouterror', 'apiconnectionerror', + 'serviceunavailableerror', 'internalservererror', 'jsondecodeerror' + ] + + is_retryable = ( + any(err in error_str for err in retryable_errors) or + any(exc_type in error_type for exc_type in retryable_exception_types) or + 'code": 429' in error_str or # HTTP 429 Too Many Requests + 'code": 503' in error_str or # HTTP 503 Service Unavailable + 'code": 502' in error_str or # HTTP 502 Bad Gateway + 'code": 500' in error_str # HTTP 500 Internal Server Error + ) + + if retry_attempt == max_retries - 1: + # Last attempt failed + raise RuntimeError(f"{operation_name}: Failed after {max_retries} attempts. Error: {e}") + + elif is_retryable: + # Special handling for rate limit errors - use longer delays + is_rate_limit = ( + 'rate limit' in error_str or 'ratelimiterror' in error_type or + 'quota' in error_str or 'resource has been exhausted' in error_str or + 'code": 429' in error_str + ) + + if is_rate_limit: + # Longer delays for rate limits: 2, 8, 18, 32, 50 seconds + delay = 2 * (retry_attempt + 1) ** 2 + retry_attempt + else: + # Standard exponential backoff for other errors + delay = base_delay * (2 ** retry_attempt) + (0.1 * retry_attempt) + + error_type_desc = "Rate limit" if is_rate_limit else "Retryable error" + # print(f"{operation_name}: {error_type_desc} - Retry {retry_attempt + 1}/{max_retries} after {delay:.1f}s. Error: {e}") + time.sleep(delay) + else: + # Non-retryable error + print(f"{operation_name}: Non-retryable error: {e}") + raise e + + # This should never be reached, but just in case + raise RuntimeError(f"{operation_name}: Unexpected error - reached end of retry loop") \ No newline at end of file From 09244c2f687f850049b1940c77a0e3db6385082b Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 15 Sep 2025 22:11:05 +0000 Subject: [PATCH 218/314] Add short-term memory. --- examples/priority_search_on_convex_fn.py | 3 +- .../priority_search/priority_search.py | 46 +++++++++++++++++-- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/examples/priority_search_on_convex_fn.py b/examples/priority_search_on_convex_fn.py index 13b4eeb7..f568e11b 100644 --- a/examples/priority_search_on_convex_fn.py +++ b/examples/priority_search_on_convex_fn.py @@ -251,10 +251,11 @@ def get_feedback(self, query: str, response: str, reference=None, **kwargs) -> T num_epochs=5, batch_size=2, # this is just for testing. effectively, this is the same batch_size=1 and num_proposals=4 num_batches=2, - verbose='output', + verbose=False, #'output', guide=guide, num_candidates=4, num_proposals=2, + short_term_memory_duration=2, optimizer_kwargs={'objective':"You have a task of guessing two numbers. You should make sure your guess minimizes y.", 'memory_size': 10} ) \ No newline at end of file diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index b105abff..1846ac72 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -168,12 +168,12 @@ class HeapMemory: def __init__(self, size=None): """ Initialize an empty heap memory. """ self.memory = [] - self.size = size # Optional size limit for the heap memory + self._size = size # Optional size limit for the heap memory def push(self, score, data): """ Push an item to the heap memory. """ heapq.heappush(self.memory, (-score, data)) - if self.size is not None and len(self.memory) > self.size: + if len(self.memory) > self.size: # NOTE a heuristic for now self.memory = self.memory[:self.size] # Keep only the top `size` items @@ -183,6 +183,23 @@ def pop(self): raise IndexError("pop from an empty heap memory") return heapq.heappop(self.memory) + def append(self, memory): + """ Append another heap memory to this heap memory. """ + assert isinstance(memory, HeapMemory), "memory must be an instance of HeapMemory." + for item in memory: + self.push(-item[0], item[1]) # item is (-score, data) + if len(self.memory) > self.size: + self.memory = self.memory[:self.size] # Keep only the top `size` items + + def reset(self): + """ Reset the heap memory to be empty. """ + self.memory = [] + + @property + def size(self): + """ Return the size limit of the heap memory. """ + return self._size if self._size is not None else float('inf') + def __len__(self): """ Return the number of items in the heap memory. """ return len(self.memory) @@ -263,7 +280,9 @@ def train(self, num_proposals: int = 1, # number of proposals to generate per optimizer validate_exploration_candidates: bool = True, # whether to validate the proposed parameters for exploration use_best_candidate_to_explore: bool = True, # whether to use the best candidate as part of the exploration candidates - memory_size: Optional[int] = None, # size of the heap memory to store the candidates; if None, no limit is set + memory_size: Optional[int] = None, # size of the long-term heap memory to store the candidates; if None, no limit is set + short_term_memory_size: Optional[int] = None, # size of the short-term memory to store the most recent candidates; if None, no limit is set + short_term_memory_duration: Optional[int] = 0, # number of iterations to keep the candidates in the short-term memory before merging them into the long-term memory. 0 means only long-term memory is used. score_function: str = 'mean', # function to compute the score for the candidates; 'mean' or 'ucb' ucb_exploration_constant: float = 1.0, # exploration constant for UCB score function # Additional keyword arguments @@ -293,6 +312,8 @@ def train(self, validate_exploration_candidates (bool, optional): Whether to validate the proposed parameters for exploration. Defaults to True. use_best_candidate_to_explore (bool, optional): Whether to use the best candidate as part of the exploration candidates. Defaults to True. memory_size (int, optional): The size of the heap memory to store the candidates. If None, no limit is set. Defaults to None. + short_term_memory_size (int, optional): The size of the short-term memory to store the most recent candidates. If None, no limit is set. Defaults to None. + short_term_memory_duration (int, optional): The number of iterations to keep the candidates in the short-term memory before merging them into the long-term memory. Defaults to 0. score_function (str, optional): The function to compute the score for the candidates; 'mean' or 'ucb'. Defaults to 'mean'. ucb_exploration_constant (float, optional): The exploration constant for UCB score function. Defaults to 1.0. **kwargs: Additional keyword arguments that may be used by the implementation. @@ -321,8 +342,9 @@ def train(self, self._best_candidate = None # This stores the latest best candidate used for exploitation self._best_candidate_priority = None # This stores the latest best candidate's priority used for exploitation - self.memory = HeapMemory(size=memory_size) # Initialize the heap memory with a size limit - + self.long_term_memory = HeapMemory(size=memory_size) # Initialize the long-term memory with a size limit + self.short_term_memory = HeapMemory(size=short_term_memory_size) # Initialize the short-term memory with a size limit + self.short_term_memory_duration = short_term_memory_duration # number of iterations to keep the candidates in the short-term memory before merging them into the long-term memory super().train(guide=guide, train_dataset=train_dataset, @@ -373,6 +395,20 @@ def update(self, info_log.update(info_explore) # add the info from the explore step return self._best_candidate.update_dict, [c.get_module() for c in self._exploration_candidates], info_log + @property + def memory(self): + if self.short_term_memory.size == 0 or self.short_term_memory_duration == 0: + return self.long_term_memory + # short_term_memory is finite and non-zero + if self.n_iters % self.short_term_memory_duration == 0: + # merge the the short-term memory into the long-term memory + if len(self.short_term_memory) > 0: + self.long_term_memory.append(self.short_term_memory) + self.short_term_memory.reset() + print('Merging short-term memory into long-term memory of PrioritySearch.') + return self.long_term_memory + else: + return self.short_term_memory ## Illustration of `propose`` # Suppose we have 2 exploration candidates. From 8a34f637ebd92f52ad6584392f9979db7916fb53 Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 15 Sep 2025 22:40:04 +0000 Subject: [PATCH 219/314] filter None score --- opto/features/priority_search/priority_search.py | 2 ++ opto/features/priority_search/search_template.py | 3 +-- opto/trainer/algorithms/basic_algorithms.py | 3 +-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index 1846ac72..cbad0a5b 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -127,6 +127,8 @@ def compute_score_confidence(self, min_score, max_score, scaling_constant=1.0, t """ # Get scores from rollouts scores = [r['score'] for r in self.rollouts] + # Filter out None scores + scores = [s for s in scores if s is not None] if not scores: return min_score, None, max_score diff --git a/opto/features/priority_search/search_template.py b/opto/features/priority_search/search_template.py index bb323b67..9d51eeae 100644 --- a/opto/features/priority_search/search_template.py +++ b/opto/features/priority_search/search_template.py @@ -297,8 +297,7 @@ def evaluate(self, agent, guide, xs, infos, min_score=None, num_samples=1, num_t num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads test_scores = evaluate(agent, guide, xs, infos, min_score=min_score, num_threads=num_threads, num_samples=num_samples, description=description) - if all([s is not None for s in test_scores]): - return np.mean(test_scores) + return np.mean([s for s in test_scores if s is not None]) def save(self, save_path): print(f"Saving algorithm state to {save_path} at iteration {self.n_iters}.") diff --git a/opto/trainer/algorithms/basic_algorithms.py b/opto/trainer/algorithms/basic_algorithms.py index 2b08f91e..9b20418a 100644 --- a/opto/trainer/algorithms/basic_algorithms.py +++ b/opto/trainer/algorithms/basic_algorithms.py @@ -149,8 +149,7 @@ def evaluate(self, agent, guide, xs, infos, min_score=None, num_samples=1, num_t num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads test_scores = evaluate(agent, guide, xs, infos, min_score=min_score, num_threads=num_threads, num_samples=num_samples, description=description) - if all([s is not None for s in test_scores]): - return np.mean(test_scores) + return np.mean([s for s in test_scores if s is not None]) def has_improvement(self, xs, guide, infos, current_score, current_outputs, backup_dict, threshold=0, num_threads=None, *args, **kwargs): # This function can be overridden by subclasses to implement their own improvement check. From 0b3aa4848622a6dca7a2ec8992b6dff7ca30c2f1 Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 15 Sep 2025 22:44:46 +0000 Subject: [PATCH 220/314] move save_train_config to subclass. --- opto/features/priority_search/priority_search.py | 7 ++++--- opto/features/priority_search/search_template.py | 1 - 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index cbad0a5b..c297d42f 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -8,7 +8,7 @@ from opto.optimizers.optimizer import Optimizer from opto.trainer.utils import async_run from opto.trainer.algorithms.basic_algorithms import batchify -from opto.features.priority_search.search_template import SearchTemplate, Samples, BatchRollout +from opto.features.priority_search.search_template import SearchTemplate, Samples, BatchRollout, save_train_config from opto.features.priority_search.utils import set_module_parameters, remap_update_dict, create_module_from_update_dict, is_module_copy @@ -255,6 +255,7 @@ class PrioritySearch(SearchTemplate): `compute_exploration_priority`, `compute_exploitation_priority` can be overridden to implement different strategies for computing the priority and selecting the best candidate. """ + @save_train_config def train(self, guide, # guide to provide feedback train_dataset, # dataset of (x, info) pairs to train the agent @@ -265,7 +266,7 @@ def train(self, # training loop batch_size = 1, # batch size for updating the agent num_batches = 1, # number of batches to use from the dataset in each iteration - score_range = None, # minimum score to update the agent + score_range = None, # range of (min_score, max_score) to clip the scores; if None, no clipping is applied num_epochs = 1, # number of training epochs num_threads = None, # maximum number of threads to use verbose = False, # whether to print the output of the agent @@ -299,7 +300,7 @@ def train(self, validate_guide (callable, optional): A function that provides feedback for the validation set. If None, the training guide is used. Defaults to None. batch_size (int, optional): The batch size for updating the agent. Defaults to 1. num_batches (int, optional): The number of batches to use from the dataset in each iteration. Defaults to 1. - score_range (tuple, optional): A tuple of (min_score, max_score) to clip the scores. If None, no clipping is applied. Defaults to None. + score_range (tuple, optional): A tuple of (min_score, max_score) to clip the scores. If None, it's set to (0, 1). num_epochs (int, optional): The number of training epochs. Defaults to 1. num_threads (int, optional): The maximum number of threads to use. If None, it uses the number of CPU cores. Defaults to None. verbose (bool, optional): Whether to print the output of the agent. Defaults to False. diff --git a/opto/features/priority_search/search_template.py b/opto/features/priority_search/search_template.py index 9d51eeae..8297db92 100644 --- a/opto/features/priority_search/search_template.py +++ b/opto/features/priority_search/search_template.py @@ -117,7 +117,6 @@ def optimizer(self): self._optimizer_index += 1 return self._optimizers[self._optimizer_index % len(self._optimizers)] # return the current optimizer - @save_train_config def train(self, *, guide, # guide to provide feedback From 5746544de87b3f07f75cffb90174e5180d34b40d Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 15 Sep 2025 23:11:01 +0000 Subject: [PATCH 221/314] Add logging on which memory is used. --- opto/features/priority_search/priority_search.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index c297d42f..4548de5c 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -392,6 +392,10 @@ def update(self, # TODO Log information about the update info_log = { 'n_iters': self.n_iters, # number of iterations + 'short_term_memory_size': len(self.short_term_memory), # size of the short-term memory + 'long_term_memory_size': len(self.long_term_memory), # size of the long-term memory + 'using_short_term_memory': self.memory is self.short_term_memory, # whether the current memory is the short-term memory + 'using_long_term_memory': self.memory is self.long_term_memory, # whether the current memory is the long-term memory } info_log.update(info_exploit) # add the info from the exploit step From bffad316c00cfeac3df70a24fd49f4d35eaf0bef Mon Sep 17 00:00:00 2001 From: windweller Date: Mon, 15 Sep 2025 21:29:56 -0400 Subject: [PATCH 222/314] add a saving method to pre-save source code --- opto/trace/modules.py | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/opto/trace/modules.py b/opto/trace/modules.py index 56df3ca9..67e4aeb6 100644 --- a/opto/trace/modules.py +++ b/opto/trace/modules.py @@ -18,8 +18,34 @@ def model(cls): """ name = f"{cls.__name__}Model" bases = (cls, Model) + # for export to work, we save the references to the original cls + __TRACE_RESERVED_cls_name = cls.__name__ + temp_cls_members = inspect.getmembers(cls) + __TRACE_RESERVED_cls_members = [] + __TRACE_RESERVED_cls_name_to_source = {} + for name, member in temp_cls_members: + if name.startswith('__TRACE_RESERVED_'): + continue + if not name.startswith('__'): + __TRACE_RESERVED_cls_members.append((name, member)) + elif name.startswith('__'): + try: + if hasattr(member, '__qualname__') and cls.__name__ in member.__qualname__: + inspect.getsource(member) # additionally we see if this works + __TRACE_RESERVED_cls_members.append((name, member)) + except (AttributeError, TypeError): + continue + + for name, member in __TRACE_RESERVED_cls_members: + __TRACE_RESERVED_cls_name_to_source[name] = inspect.getsource(member) + new_class = type(name, bases, {}) new_class.__module__ = cls.__module__ + # for export + new_class.reserved_cls_name = __TRACE_RESERVED_cls_name + new_class.reserved_cls_members = __TRACE_RESERVED_cls_members + new_class.reserved_cls_name_to_source = __TRACE_RESERVED_cls_name_to_source + mod = sys.modules[cls.__module__] setattr(mod, name, new_class) return new_class @@ -221,9 +247,11 @@ def export(self, filename, projections: Optional[List[Projection]] = None): if projections is None: projections = [BlackCodeFormatter()] cls = self.__class__ - trace_model_body = f"class {cls.__name__}:\n" + # trace_model_body = f"class {cls.__name__}:\n" + name = cls.reserved_cls_name + trace_model_body = f"class {name}:\n" all_members = inspect.getmembers(self) - cls_members = inspect.getmembers(cls) + cls_members = cls.reserved_cls_members # inspect.getmembers(cls) cls_member_names = [m[0] for m in cls_members] filtered_members = [] for name, member in all_members: @@ -239,6 +267,7 @@ def export(self, filename, projections: Optional[List[Projection]] = None): filtered_members.append((name, member)) except (AttributeError, TypeError): continue + for i, (name, member) in enumerate(filtered_members): if 'FunModule' in str(member): if member.parameter is not None: @@ -249,7 +278,7 @@ def export(self, filename, projections: Optional[List[Projection]] = None): indented = textwrap.indent(source, " ") trace_model_body += indented else: - source = inspect.getsource(member) + source = cls.reserved_cls_name_to_source[name] # inspect.getsource(member) source = textwrap.dedent(source) indented = textwrap.indent(source, " ") trace_model_body += indented From e36175f13b0bdfce7cf8f6f27e9650d30e6fae5d Mon Sep 17 00:00:00 2001 From: Xuanfei Ren Date: Mon, 15 Sep 2025 22:01:31 -0500 Subject: [PATCH 223/314] add auto-retry to propose --- opto/features/priority_search/priority_search.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index 4548de5c..ca3e8e26 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -10,6 +10,7 @@ from opto.trainer.algorithms.basic_algorithms import batchify from opto.features.priority_search.search_template import SearchTemplate, Samples, BatchRollout, save_train_config from opto.features.priority_search.utils import set_module_parameters, remap_update_dict, create_module_from_update_dict, is_module_copy +from opto.utils.auto_retry import retry_with_exponential_backoff class ModuleCandidate: @@ -505,7 +506,12 @@ def _backward(n): # For each optimizer, containing the backward feedback, we call it n_proposals times to get the proposed parameters. def _step(n): optimizer = optimizers[n] - update_dict = optimizer.step(verbose=verbose, num_threads=self.num_threads, bypassing=True, **kwargs) + update_dict = retry_with_exponential_backoff( + lambda: optimizer.step(verbose=verbose, num_threads=self.num_threads, bypassing=True, **kwargs), + max_retries=10, + base_delay=1.0, + operation_name="optimizer_step" + ) if not update_dict: # if the optimizer did not propose any updates return None # return None to indicate no updates were proposed # update_dict may only contain some of the parameters of the agent, we need to make sure it contains all the parameters From 44f1b770ce8c80b4dcde72eacd10ec9850cfd90e Mon Sep 17 00:00:00 2001 From: Xuanfei Ren Date: Mon, 15 Sep 2025 22:04:56 -0500 Subject: [PATCH 224/314] some candidates can have no data --- opto/features/priority_search/priority_search.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index ca3e8e26..6ec21ed6 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -612,8 +612,8 @@ def match_candidates_and_samples( # Append the rollouts to the list of rollouts for the key _results[ids[key]].append(rollouts) # assert all candidates have at least one rollout - for c in candidates: - assert len(_results[c]) > 0, f"ModuleCandidate with id {id(c)} has no rollouts. Samples are not collected by known candidates." + # for c in candidates: + # assert len(_results[c]) > 0, f"ModuleCandidate with id {id(c)} has no rollouts. Samples are not collected by known candidates." return _results From f42afea4e18f459d20265f51f4562d28a1d16f37 Mon Sep 17 00:00:00 2001 From: Xuanfei Ren Date: Mon, 15 Sep 2025 22:47:18 -0500 Subject: [PATCH 225/314] heapify the long term memory after the combination --- opto/features/priority_search/priority_search.py | 1 + 1 file changed, 1 insertion(+) diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index 6ec21ed6..173cc3cc 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -412,6 +412,7 @@ def memory(self): # merge the the short-term memory into the long-term memory if len(self.short_term_memory) > 0: self.long_term_memory.append(self.short_term_memory) + heapq.heapify(self.long_term_memory) self.short_term_memory.reset() print('Merging short-term memory into long-term memory of PrioritySearch.') return self.long_term_memory From 75016ee7d290e4868f28a7494dcc7c4e57948d6d Mon Sep 17 00:00:00 2001 From: Xuanfei Ren Date: Mon, 15 Sep 2025 23:03:17 -0500 Subject: [PATCH 226/314] make the regressor code simpler --- .../priority_search/module_regressor.py | 58 ++++--------------- 1 file changed, 12 insertions(+), 46 deletions(-) diff --git a/opto/features/priority_search/module_regressor.py b/opto/features/priority_search/module_regressor.py index 23dd1117..ac1d5dc1 100644 --- a/opto/features/priority_search/module_regressor.py +++ b/opto/features/priority_search/module_regressor.py @@ -92,8 +92,8 @@ def _update_memory_embeddings(self): continue candidate.embedding = self._get_embedding(candidate) - def _update_regression_model(self): - """Update the regression model using the current memory with logistic regression.""" + def update(self): + """Update the regression model parameters using the current memory with logistic regression.""" start_time = time.time() print_color("Updating regression model using the current memory with logistic regression...", "blue") self._update_memory_embeddings() @@ -105,7 +105,7 @@ def _update_regression_model(self): print_color("Warning: No training data available for regression model.", "yellow") end_time = time.time() elapsed_time = end_time - start_time - print_color(f"_update_regression_model completed in {elapsed_time:.4f} seconds (no training data)", "cyan") + print_color(f"Regressor update completed in {elapsed_time:.4f} seconds (no training data)", "cyan") return # Extract raw binary training data from each candidate @@ -145,7 +145,7 @@ def _update_regression_model(self): print_color("Warning: No binary training samples generated.", "yellow") end_time = time.time() elapsed_time = end_time - start_time - print_color(f"_update_regression_model completed in {elapsed_time:.4f} seconds (no binary samples)", "cyan") + print_color(f"Regressor update completed in {elapsed_time:.4f} seconds (no binary samples)", "cyan") return # Convert to numpy arrays @@ -249,28 +249,18 @@ def _update_regression_model(self): # Print timing information end_time = time.time() elapsed_time = end_time - start_time - print_color(f"_update_regression_model completed in {elapsed_time:.4f} seconds", "cyan") + print_color(f"Regressor update completed in {elapsed_time:.4f} seconds", "cyan") - def _predict_single(self, candidate): - """Predict a single score for a ModuleCandidate using the logistic regression model. Using the entire memory as the training data.""" - self._update_regression_model() - - embedding = self._get_embedding(candidate) - z = self.weights.dot(embedding) + self.bias - predicted_score = self._sigmoid(z) - return predicted_score - - def predict_scores_for_batch(self, batch): - """Predict scores for a batch of ModuleCandidates and update each with the predicted scores. Using the entire memory as the training data.""" - # Get embeddings for all candidates in batch - embeddings = [] - + def predict_scores(self): + """Predict scores for all candidates in the memory.""" + # Extract all candidates from memory (memory is a list of (neg_score, candidate) tuples) + batch = [candidate for _, candidate in self.memory] + # Separate candidates that need embeddings from those that already have them candidates_needing_embeddings = [] for candidate in batch: if not hasattr(candidate, "embedding"): candidates_needing_embeddings.append(candidate) - embeddings.append(None) # Placeholder # Generate embeddings in parallel for candidates that need them if candidates_needing_embeddings: @@ -297,8 +287,7 @@ def get_embedding_for_candidate(candidate): for candidate in batch: embeddings.append(candidate.embedding) - self._update_regression_model() - + # Batch prediction using vectorized operations X_batch = np.array(embeddings) z = X_batch.dot(self.weights) + self.bias @@ -309,27 +298,4 @@ def get_embedding_for_candidate(candidate): candidate.predicted_score = predicted_score return predicted_scores - - def predict_scores(self): - """Predict scores for all candidates in the memory. Using the entire memory as the training data.""" - # Extract all candidates from memory (memory is a list of (neg_score, candidate) tuples) - memory_candidates = [candidate for neg_score, candidate in self.memory] - - batches = [memory_candidates[i:i+self.max_candidates_to_predict] for i in range(0, len(memory_candidates), self.max_candidates_to_predict)] - - if hasattr(self, 'num_threads') and self.num_threads and self.num_threads > 1: - # Parallelize batch processing - batch_functions = [lambda batch=b: self.predict_scores_for_batch(batch) for b in batches] - async_run( - batch_functions, - max_workers=self.num_threads, - description=f"Processing {len(batches)} candidate batches" - ) - else: - # Sequential processing - for batch in batches: - self.predict_scores_for_batch(batch) - - # Return the predicted scores for the memory candidates - predicted_scores_for_the_memory = [candidate.predicted_score for candidate in memory_candidates] - return np.array(predicted_scores_for_the_memory) + \ No newline at end of file From 3945df4ee14f59c1ec135693f663ab780c7be2a7 Mon Sep 17 00:00:00 2001 From: Xuanfei Ren Date: Mon, 15 Sep 2025 23:03:42 -0500 Subject: [PATCH 227/314] debug for the memory definition --- opto/features/priority_search/priority_search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index 173cc3cc..f1d38059 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -405,7 +405,7 @@ def update(self, @property def memory(self): - if self.short_term_memory.size == 0 or self.short_term_memory_duration == 0: + if self.short_term_memory_duration == 0: return self.long_term_memory # short_term_memory is finite and non-zero if self.n_iters % self.short_term_memory_duration == 0: From 7c12698452c08498429160ddd6134af6ec648701 Mon Sep 17 00:00:00 2001 From: Xuanfei Ren Date: Mon, 15 Sep 2025 23:09:02 -0500 Subject: [PATCH 228/314] update the regressor parameter to take in memory --- opto/features/priority_search/module_regressor.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/opto/features/priority_search/module_regressor.py b/opto/features/priority_search/module_regressor.py index ac1d5dc1..12254877 100644 --- a/opto/features/priority_search/module_regressor.py +++ b/opto/features/priority_search/module_regressor.py @@ -251,10 +251,12 @@ def update(self): elapsed_time = end_time - start_time print_color(f"Regressor update completed in {elapsed_time:.4f} seconds", "cyan") - def predict_scores(self): + def predict_scores(self,memory = None): """Predict scores for all candidates in the memory.""" # Extract all candidates from memory (memory is a list of (neg_score, candidate) tuples) - batch = [candidate for _, candidate in self.memory] + if memory is None: + memory = self.memory + batch = [candidate for _, candidate in memory] # Separate candidates that need embeddings from those that already have them candidates_needing_embeddings = [] From 28b2b3bfb17161cc9dca2b9924d54d7d6b813b35 Mon Sep 17 00:00:00 2001 From: Xuanfei Ren Date: Mon, 15 Sep 2025 23:09:26 -0500 Subject: [PATCH 229/314] make PrioritySearch_with_Regressor a subclass --- .../priority_search_with_regressor.py | 628 ++---------------- 1 file changed, 64 insertions(+), 564 deletions(-) diff --git a/opto/features/priority_search/priority_search_with_regressor.py b/opto/features/priority_search/priority_search_with_regressor.py index 3d01ed13..56f3e57b 100644 --- a/opto/features/priority_search/priority_search_with_regressor.py +++ b/opto/features/priority_search/priority_search_with_regressor.py @@ -1,245 +1,20 @@ import numpy as np import copy -import heapq -import time from typing import Union, List, Tuple, Dict, Any, Optional -from opto import trace -from opto.trace.nodes import ParameterNode -from opto.optimizers.optimizer import Optimizer from opto.trainer.utils import async_run from opto.trainer.algorithms.basic_algorithms import batchify -from opto.features.priority_search.search_template import SearchTemplate, Samples, BatchRollout -from opto.features.priority_search.utils import set_module_parameters, remap_update_dict, create_module_from_update_dict, is_module_copy +from opto.features.priority_search.search_template import Samples, BatchRollout +from opto.features.priority_search.utils import remap_update_dict from opto.features.priority_search.module_regressor import ModuleCandidateRegressor from opto.utils.auto_retry import retry_with_exponential_backoff +from opto.features.priority_search.priority_search import PrioritySearch, ModuleCandidate, HeapMemory +import heapq -class ModuleCandidate: - """ A container used by PrioritySearch to store a candidate module as (its base module and update dictionary) and its statistics. """ - - def __init__(self, - base_module: trace.Module, - update_dict: Optional[Dict[ParameterNode, Any]] = None, - optimizer: Optimizer = None, - ): - """ A candidate module with its base module and update dictionary. - Args: - base_module (trace.Module): The base module to use as a template for the candidate. - update_dict (dict): A dictionary of ParameterNode: value pairs to update the base module; the key can be a deep copy of the base module's parameters. - stats (dict): A dictionary of statistics about the candidate. - """ - assert isinstance(base_module, trace.Module), "base_module must be a trace.Module." - if update_dict is not None: - assert isinstance(optimizer, Optimizer), "optimizer must be an instance of Optimizer when update_dict is provided." - - self.base_module = base_module - self.update_dict = update_dict if update_dict is not None else {} - self.optimizer = optimizer # the optimizer used to generate the update_dict; can be None, which indicates the base_module is used. - self.update_dict = remap_update_dict(self.base_module, self.update_dict) - self.rollouts = [] # list of dicts containing the rollout information (not BatchRollout, but a list of dicts) - self.created_time = time.time() - - def get_module(self): - """ Apply the update_dict to the base_module and return the updated module. - A new module is always created so the base_module is not modified. - The new module has a new attribute _module_candidate which is this candidate.""" - module = create_module_from_update_dict(self.base_module, self.update_dict) if self.update_dict else copy.deepcopy(self.base_module) # - setattr(module, '__TRACE_RESERVED_module_candidate_id', id(self)) - return module # return the updated module - - def apply_update(self, base_module=None): - """ Apply update to the base_module in place. """ - set_module_parameters(base_module or self.base_module, self.update_dict) - - def __getstate__(self): - """ Get the state of the candidate for serialization. """ - state = copy.deepcopy(self.__dict__) # this will detach the nodes from the computation graph - return state - - def __setstate__(self, state): - """ Set the state of the candidate from serialization. """ - self.__dict__.update(state) - - def __deepcopy__(self, memo): - """ Create a deep copy, except for the base_module which is not copied, it is the original module. """ - cls = self.__class__ - result = cls.__new__(cls) - memo[id(self)] = result - for k, v in self.__dict__.items(): - if k != 'base_module': - setattr(result, k, copy.deepcopy(v, memo)) - else: - setattr(result, k, v) # base_module is not copied, it is the original module - return result - - def __eq__(self, other): - """ Check if two candidates are equal based on their base_module and update_dict. """ - assert isinstance(other, ModuleCandidate), "other must be an instance of ModuleCandidate." - return (self.update_dict == other.update_dict) and is_module_copy(self.base_module, other.base_module) and (id(self.optimizer) == id(other.optimizer)) - - def __lt__(self, other): - """ Compare two candidates based on their update_dict. """ - assert isinstance(other, ModuleCandidate), "other must be an instance of ModuleCandidate." - return self.created_time > other.created_time - # self < other if, self is created later than other - # Since we will use minheap, and this would give priority to later created candidates in the heap memory. - - def __hash__(self): - """ Hash the candidate based on its update_dict. """ - return hash((frozenset(self.update_dict.items()), id(self.optimizer), id(self.base_module))) - - def add_rollouts(self, rollouts: List[Dict[str, Any]]): - """ Add rollouts to the candidate. """ - assert isinstance(rollouts, list), "rollouts must be a list of dicts." - assert all(isinstance(r, dict) for r in rollouts), "All rollouts must be dicts." - # Each rollout is a dict with keys: 'module', 'x', 'info', 'target', 'score', 'feedback' - assert all('module' in r and 'x' in r and 'info' in r and 'target' in r and 'score' in r and 'feedback' in r for r in rollouts), \ - "Each rollout must contain 'module', 'x', 'info', 'target', 'score', and 'feedback' keys." - - self.rollouts.extend(rollouts) - - def mean_score(self): - """ Compute the score of the candidate based on the rollouts. """ - if not self.rollouts: - return None - scores = [r['score'] for r in self.rollouts if r['score'] is not None] - return np.mean(scores) if scores else None - - def compute_score_confidence(self, min_score, max_score, scaling_constant=1.0, total_trials=1): - """Compute the UCB, mean, LCB score for the candidate. After queried, the number of confidence queries is incremented. - - UCB = mean_score + scaling_constant * sqrt(ln(total_trials) / n_scores) * (max_score - min_score) - UCB = clip(UCB, min_score, max_score) - - LCB = mean_score - scaling_constant * sqrt(ln(total_trials) / n_scores) * (max_score - min_score) - LCB = clip(LCB, min_score, max_score) - - Args: - min_score (float): The minimum score for clipping. - max_score (float): The maximum score for clipping. - scaling_constant (float): The scaling constant for the exploration term. - total_trials (int): The total number of trials conducted. Must be at least 1. - Returns: - lcb_score (float): The lower confidence bound score. - mean_score (float): The mean score. - ucb_score (float): The upper confidence bound score. - """ - # Get scores from rollouts - scores = [r['score'] for r in self.rollouts] - - if not scores: - return min_score, None, max_score - - # Calculate mean score for this candidate - mean_score = np.mean(scores) - n_scores = len(scores) - assert n_scores == self.num_rollouts, "Number of scores should match number of rollouts." - - # Calculate how many times the confidence interval has been used to form a union bound - assert total_trials >= 1, "total_trials must be at least 1." - total_trials = total_trials + 1 # this is an upper bound, since log(1) = 0 - - # Compute the exploration term based on Hoeffding's inequality - exploration_term = scaling_constant * np.sqrt(np.log(total_trials) / n_scores) * (max_score - min_score) - - # Calculate UCB score - ucb_score = mean_score + exploration_term - ucb_score = np.clip(ucb_score, min_score, max_score) - - # Calculate LCB score - lcb_score = mean_score - exploration_term - lcb_score = np.clip(lcb_score, min_score, max_score) - - return lcb_score, mean_score, ucb_score - - - @property - def num_rollouts(self): - """ Return the number of rollouts collected for this candidate. """ - return len(self.rollouts) - - -class HeapMemory: - # This is a basic implementation of a heap memory that uses a priority queue to store candidates. - # Later on this will be replaced by a memory DB. - - # NOTE that the heap memory is a max-heap, so we store negative scores to use the default min-heap behavior of heapq. - def __init__(self, size=None): - """ Initialize an empty heap memory. """ - self.memory = [] - self.size = size # Optional size limit for the heap memory - - def push(self, score, data): - """ Push an item to the heap memory. """ - heapq.heappush(self.memory, (-score, data)) - if self.size is not None and len(self.memory) > self.size: - # NOTE a heuristic for now - self.memory = self.memory[:self.size] # Keep only the top `size` items - - def pop(self): - """ Pop the top item from the heap memory. """ - if not self.memory: - raise IndexError("pop from an empty heap memory") - return heapq.heappop(self.memory) - - def __len__(self): - """ Return the number of items in the heap memory. """ - return len(self.memory) - - def __bool__(self): - """ Return True if the heap memory is not empty, False otherwise. """ - return len(self.memory) > 0 - - def __iter__(self): - """ Iterate over the items in the heap memory. """ - return iter(self.memory) - - def best(self, criterion=None): - """ Return the best item in the heap memory without removing it. - - If criterion is None, return the item with the highest priority (lowest negative score). - If criterion is a callable function, return the item that maximizes the criterion. - """ - if not self.memory: - raise IndexError("best from an empty heap memory") - if criterion is None: - return self.memory[0] # return the item with the highest priority (lowest negative score) - else: - assert callable(criterion), "criterion must be a callable function." - def _criterion(x): - neg_score, candidate = x - p = criterion(candidate) - return p if p is not None else 0 - return max(self.memory, key=lambda x: _criterion(x)) - - def reorder_according_to_predicted_scores(self): - """ Reorder the heap memory according to the predicted scores. """ - # Now all ModuleCandidate objects in the heap memory have predicted scores. Should modify the old score to the negative predicted scores, then use heapq.heapify to reorder the heap memory. - self.memory = [(-candidate.predicted_score, candidate) for _, candidate in self.memory] - heapq.heapify(self.memory) - -# TODO check saving and loading -class PrioritySearch_with_Regressor(SearchTemplate): - """ A search algorithm that uses a priority queue to explore the parameter space and propose new candidates. - - It provides a scalable template for implementing search algorithms based on asynchronous generation, validation, and testing. - In each iteration, - 1. It proposes a best agent and a set of `num_candidates` exploration agents that have the highest scores in the priority queue. - 2. The best agent is tested for performance if test_frequency is met. - 3. `num_batches` minibatches of `batch_size` samples are drawn from the training dataset, and the exploration agents are run on the samples. This creates a set of agent rollouts, where each rollout contains the agent module, input, info, target, score, and feedback. For each agent, rollouts of each minibatch are grouped together as a connected subgraph (represented as the BatchRollout object). In total, this step creates `num_candidates * num_batches` subgraphs. - 4. Optimizer is run on each subgraph to propose new parameters for the agents. `num_proposals` proposals are generated for each subgraph. This results in `num_subgraphs * num_proposals` total proposals. - 5. The proposed parameters are validated by running the agents on the validation dataset, which can be the current batch or a separate validation dataset when provided. When validate_exploration_candidates is set to True, the exploration candidates are also validated. - 6. The validation results are used to update the priority queue, which stores the candidates and their scores. The candidates are stored as ModuleCandidate objects, which contain the base module, update dictionary, and rollouts (i.e. raw statistics of the candidate). - - This algorithm template can be subclassed to implement specific search algorithms by overriding the `exploit`, `explore`, and `compute_exploration_priority` methods. - The `exploit` method is used to select the best candidate from the priority queue, the `explore` method is used to generate new candidates from the priority queue, and - the `compute_exploration_priority` method is used to compute the score for ranking in the priority queue. - - By default, `compute_exploration_priority` computes the mean score of the rollouts. `exploit` simply returns the candidate with highest priority from the priority queue, and `explore` generates the top `num_candidates` candidates from the priority queue. - - - `compute_exploration_priority`, `compute_exploitation_priority` can be overridden to implement different strategies for computing the priority and selecting the best candidate. +class PrioritySearch_with_Regressor(PrioritySearch): + """ + A subclass of PrioritySearch that uses a regressor to predict the scores of the candidates. """ - + def train(self, guide, # guide to provide feedback train_dataset, # dataset of (x, info) pairs to train the agent @@ -250,7 +25,7 @@ def train(self, # training loop batch_size = 1, # batch size for updating the agent num_batches = 1, # number of batches to use from the dataset in each iteration - score_range = None, # minimum score to update the agent + score_range = None, # range of (min_score, max_score) to clip the scores; if None, no clipping is applied num_epochs = 1, # number of training epochs num_threads = None, # maximum number of threads to use verbose = False, # whether to print the output of the agent @@ -267,42 +42,34 @@ def train(self, num_proposals: int = 1, # number of proposals to generate per optimizer validate_exploration_candidates: bool = True, # whether to validate the proposed parameters for exploration use_best_candidate_to_explore: bool = True, # whether to use the best candidate as part of the exploration candidates - memory_size: Optional[int] = None, # size of the heap memory to store the candidates; if None, no limit is set + memory_size: Optional[int] = None, # size of the long-term heap memory to store the candidates; if None, no limit is set + short_term_memory_size: Optional[int] = None, # size of the short-term memory to store the most recent candidates; if None, no limit is set + short_term_memory_duration: Optional[int] = 0, # number of iterations to keep the candidates in the short-term memory before merging them into the long-term memory. 0 means only long-term memory is used. score_function: str = 'mean', # function to compute the score for the candidates; 'mean' or 'ucb' ucb_exploration_constant: float = 1.0, # exploration constant for UCB score function + # Regressor specific parameters + regressor_embedding_model: str = "gemini/text-embedding-004", # embedding model for the regressor + regressor_learning_rate: float = 0.2, # learning rate for the regressor + regressor_regularization_strength: float = 1e-4, # L2 regularization strength for the regressor + regressor_max_iterations: int = 20000, # maximum iterations for regressor training + regressor_tolerance: float = 5e-3, # convergence tolerance for the regressor # Additional keyword arguments **kwargs ): - """ Train the agent using the Priority Search algorithm. + """ Train the agent using the Priority Search algorithm with regressor. + + This extends the parent PrioritySearch by adding a regressor that predicts + candidate scores based on the long-term memory. Args: - guide (callable): A function that provides feedback for the agent. - train_dataset (list): A list of (x, info) pairs to train the agent. - validate_dataset (list, optional): A list of (x, info) pairs to validate the proposed candidates. If None, the current batch is used. Defaults to None. - validate_guide (callable, optional): A function that provides feedback for the validation set. If None, the training guide is used. Defaults to None. - batch_size (int, optional): The batch size for updating the agent. Defaults to 1. - num_batches (int, optional): The number of batches to use from the dataset in each iteration. Defaults to 1. - score_range (tuple, optional): A tuple of (min_score, max_score) to clip the scores. If None, no clipping is applied. Defaults to None. - num_epochs (int, optional): The number of training epochs. Defaults to 1. - num_threads (int, optional): The maximum number of threads to use. If None, it uses the number of CPU cores. Defaults to None. - verbose (bool, optional): Whether to print the output of the agent. Defaults to False. - test_dataset (list, optional): A list of (x, info) pairs to evaluate the agent. If None, no evaluation is performed. Defaults to None. - test_frequency (int or None, optional): The frequency of evaluation. If None, no evaluation is performed. If negative, skips the first evaluation. Defaults to 1. - num_test_samples (int, optional): The number of times to evaluate each input; when greater than 1, the scores are averaged. Defaults to 1. - log_frequency (int or None, optional): The frequency of logging. If None, no logging is performed. Defaults to None. - save_frequency (int or None, optional): The frequency of saving the agent. If None, no saving is performed. Defaults to None. - save_path (str, optional): The path to save the agent. Defaults to "checkpoints/agent.pkl". - num_candidates (int, optional): The number of candidates to propose for exploration. Defaults to 10. - num_proposals (int, optional): The number of proposals to generate per optimizer. Defaults to 1. - validate_exploration_candidates (bool, optional): Whether to validate the proposed parameters for exploration. Defaults to True. - use_best_candidate_to_explore (bool, optional): Whether to use the best candidate as part of the exploration candidates. Defaults to True. - memory_size (int, optional): The size of the heap memory to store the candidates. If None, no limit is set. Defaults to None. - score_function (str, optional): The function to compute the score for the candidates; 'mean' or 'ucb'. Defaults to 'mean'. - ucb_exploration_constant (float, optional): The exploration constant for UCB score function. Defaults to 1.0. - **kwargs: Additional keyword arguments that may be used by the implementation. + All parameters from the parent PrioritySearch.train() method, plus: + regressor_embedding_model (str, optional): Embedding model for the regressor. Defaults to "gemini/text-embedding-004". + regressor_learning_rate (float, optional): Learning rate for the regressor. Defaults to 0.2. + regressor_regularization_strength (float, optional): L2 regularization strength for the regressor. Defaults to 1e-4. + regressor_max_iterations (int, optional): Maximum iterations for regressor training. Defaults to 20000. + regressor_tolerance (float, optional): Convergence tolerance for the regressor. Defaults to 5e-3. """ - # Create agents and optimizers for search if num_candidates < len(self._optimizers): print(f"Warning: num_candidates {num_candidates} is less than the number of optimizers {len(self._optimizers)}. Setting num_candidates to {len(self._optimizers)}.") @@ -321,10 +88,24 @@ def train(self, self.ucb_exploration_constant = ucb_exploration_constant self._exploration_candidates = None # This stores the latest candidates used for exploration + self._exploration_candidates_priority = None # This stores the latest candidates' priorities used for exploration self._best_candidate = None # This stores the latest best candidate used for exploitation - - self.memory = HeapMemory(size=memory_size) # Initialize the heap memory with a size limit - self.regressor = ModuleCandidateRegressor(memory=self.memory) # Initialize the + self._best_candidate_priority = None # This stores the latest best candidate's priority used for exploitation + + self.long_term_memory = HeapMemory(size=memory_size) # Initialize the long-term memory with a size limit + self.short_term_memory = HeapMemory(size=short_term_memory_size) # Initialize the short-term memory with a size limit + self.short_term_memory_duration = short_term_memory_duration # number of iterations to keep the candidates in the short-term memory before merging them into the long-term memory + + # Initialize the regressor with the long-term memory and custom parameters - this is the only difference from parent class + self.regressor = ModuleCandidateRegressor( + memory=self.long_term_memory, + embedding_model=regressor_embedding_model, + num_threads=num_threads, + learning_rate=regressor_learning_rate, + regularization_strength=regressor_regularization_strength, + max_iterations=regressor_max_iterations, + tolerance=regressor_tolerance + ) super().train(guide=guide, train_dataset=train_dataset, @@ -354,168 +135,40 @@ def update(self, # samples is None in the first iteration if samples is not None: # 1. Propose new parameters based on running LLM optimizers on the collected samples - - candidates = retry_with_exponential_backoff( - lambda: self.propose(samples, verbose=verbose, **kwargs), - max_retries=10, - base_delay=1.0, - operation_name="propose_new_parameters" - ) # List of ModuleCandidates - # # 2. Validate the proposed parameters + candidates = self.propose(samples, verbose=verbose, **kwargs) # List of ModuleCandidates + # 2. Validate the proposed parameters validate_results = self.validate(candidates, samples, verbose=verbose, **kwargs) # this updates the priority queue - # # 3. Update the priority queue with the validation results + # 3. Update the priority queue with the validation results self.update_memory(validate_results, verbose=verbose, **kwargs) # samples are provided here in case candidates do not capture full information - else: # The first iteration. max_mem_size = self.memory.size if self.memory.size is not None else float('inf') - initial_update_dict = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} while len(self.memory) < min(max_mem_size, self.num_candidates): - self.memory.push(self.max_score, ModuleCandidate(self.agent, initial_update_dict, optimizer=self.optimizer)) # Push the base agent as the first candidate (This gives the initialization of the priority queue) - self.update_memory_with_regressor() + self.memory.push(self.max_score, ModuleCandidate(self.agent, optimizer=self.optimizer)) # Push the base agent as the first candidate (This gives the initialization of the priority queue) + if self.memory is self.long_term_memory: # Only update the regressor if we are using the long-term memory + self.regressor.update() + self.regressor.predict_scores(self.memory) # The only difference from the parent class # 4. Explore and exploit the priority queue - self._best_candidate, info_exploit = self.exploit(verbose=verbose, **kwargs) # get the best candidate (ModuleCandidate) from the priority queue - self._exploration_candidates, info_explore = self.explore(verbose=verbose, **kwargs) # List of ModuleCandidates + self._best_candidate, self._best_candidate_priority, info_exploit = self.exploit(verbose=verbose, **kwargs) # get the best candidate (ModuleCandidate) from the priority queue + self._exploration_candidates, self._exploration_candidates_priority, info_explore = self.explore(verbose=verbose, **kwargs) # List of ModuleCandidates # TODO Log information about the update info_log = { 'n_iters': self.n_iters, # number of iterations + 'short_term_memory_size': len(self.short_term_memory), # size of the short-term memory + 'long_term_memory_size': len(self.long_term_memory), # size of the long-term memory + 'using_short_term_memory': self.memory is self.short_term_memory, # whether the current memory is the short-term memory + 'using_long_term_memory': self.memory is self.long_term_memory, # whether the current memory is the long-term memory } info_log.update(info_exploit) # add the info from the exploit step info_log.update(info_explore) # add the info from the explore step return self._best_candidate.update_dict, [c.get_module() for c in self._exploration_candidates], info_log - - ## Illustration of `propose`` - # Suppose we have 2 exploration candidates. - # exploration_candidates = [candidate(param1, optimizer_1), candidate(param2, optimizer_2)] - # and two batches are collected by sampler. - # - # In samples returned by sampler, we have data - # module(param1_copy1), batch_1 - # module(param1_copy2), batch_2 - # module(param2_copy1), batch_1 - # module(param2_copy2), batch_2 - # - # We first match the samples with the exploration candidates as - # candidate_batchrollouts_list = - # [ (candidate(param1, optimizer_1), batch_1), (candidate(param1, optimizer_1), batch_2), - # (candidate(param2, optimizer_2), batch_1), (candidate(param2, optimizer_2), batch_2) ] - # - # In backward, we create deepcopies of the optimizers for each batch, and run backward asynchronously. - # optimizer_1_copy_1(param1) <- feedback from batch_1 - # optimizer_1_copy_2(param1) <- feedback from batch_2 - # optimizer_2_copy_1(param2) <- feedback from batch_1 - # optimizer_2_copy_2(param2) <- feedback from batch_2 - # - # In step, we further create deepcopies of the optimizers for each proposal, and run step asynchronously. - # for n_proposals = 2, we have - # optimizer_1_copy_1_copy_1(param1) -> proposal_1 - # optimizer_1_copy_1_copy_2(param1) -> proposal_2 - # ... - # optimizer_2_copy_2_copy_1(param2) -> proposal_7 - # optimizer_2_copy_2_copy_2(param2) -> proposal_8 - # which form the new candidate list returned by `propose`. - # - def propose(self, - samples : Samples, - verbose : bool = False, - **kwargs): - """ Analyzing samples and propose new parameters using self.optimizer. An independent optimizer is used for the minibatch generated by one agent and generates n_proposals proposals. - - Args: - samples (Samples): Samples collected by the exploration candidates. If None, the agent's parameters are returned without updating. - verbose (bool, optional): Whether to print verbose output. Defaults to False. - **kwargs: Additional keyword arguments that may be used by the implementation. - - Returns: - candidates (list of ModuleCandidate): A list of proposed candidates for the next iteration. - """ - print("--- Proposing new parameters...") if verbose else None - assert isinstance(samples, Samples), "samples must be an instance of Samples." - samples = samples.samples # list of BatchRollout objects - n_proposals = self.num_proposals # number of proposals to generate per optimizer - - # Associate each BatchRollout with self._exploration_candidates - matched_candidates_and_samples = self.match_candidates_and_samples(self._exploration_candidates, samples) - # NOTE len(matched_candidates_and_samples) <= len(self._exploration_candidates) since some exploration candidates might be duplicated. - candidate_batchrollouts_list = [ (k,b) for k, v in matched_candidates_and_samples.items() for b in v] - assert len(samples) == len(candidate_batchrollouts_list), "All samples must be associated with exploration candidates." - n_batches = len(samples) # number of batch rollouts in the samples - - # need to copy optimizer for the n_batches - def _backward(n): - candidate, rollouts = candidate_batchrollouts_list[n] - optimizer = candidate.optimizer or self.optimizer - # Create a copy of the optimizer to avoid modifying the original one and to allow parallel execution - optimizer = copy.deepcopy(optimizer) - optimizer.parameters = rollouts.module.parameters() # set the optimizer's parameters to the proposal's parameters - targets = [r.target for r in rollouts] - feedbacks = [r.feedback for r in rollouts] - # batchify the targets and feedbacks - target = batchify(*targets) - feedback = batchify(*feedbacks).data # str - # standard optimizer step - optimizer.zero_feedback() # reset the optimizer's feedback - optimizer.backward(target, feedback) # compute the gradients based on the targets and feedbacks - return optimizer - - args_list = [(n,) for n in range(n_batches)] - optimizers = async_run([_backward]*n_batches, # run the optimizer step for each agent in parallel - args_list=args_list, - max_workers=1000, # use the number of threads specified in the class - description=None) - assert len(optimizers) == n_batches, "Number of optimizers must match number of batch rollouts." - # need to copy optimizer for the n_proposals - # NOTE when optimizer is deepcopied, its parameters are not copied. - optimizers = [copy.deepcopy(o) for o in optimizers ] * n_proposals # repeat args_list n_proposals times - assert len(optimizers) == n_batches * n_proposals, "Number of optimizers must match number of batch rollouts times number of proposals." - - # For each optimizer, containing the backward feedback, we call it n_proposals times to get the proposed parameters. - def _step(n): - optimizer = optimizers[n] - - update_dict = retry_with_exponential_backoff( - lambda: optimizer.step(verbose=verbose, num_threads=self.num_threads, bypassing=True, **kwargs), - max_retries=10, - base_delay=1.0, - operation_name="optimizer_step" - ) - if not update_dict: # if the optimizer did not propose any updates - return None # return None to indicate no updates were proposed - # update_dict may only contain some of the parameters of the agent, we need to make sure it contains all the parameters - # since the current agent might have different parameters than the one used by the optimizer - for param in optimizer.parameters: # for all parameters - if param not in update_dict: # update_dict misses some parameters - update_dict[param] = param.data # add the parameter to the update_dict - # the update_dict is linked to the copied parameters of the agent, we set it back to the agent's parameters - update_dict = remap_update_dict(self.agent, update_dict) # remap the update dict to the agent's parameters - return update_dict # return the proposed parameters - - args_list = [(n,) for n in range(n_batches*n_proposals)] - update_dicts = async_run([_step]*n_batches*n_proposals, # run the optimizer step for each agent in parallel - args_list=args_list, - max_workers=1000, # use the number of threads specified in the class - description=f"Calling optimizers: Generating {n_proposals} proposals for each of {n_batches} batches",) - - # update_dicts is a list of dicts of length n_batches * n_proposals - # Create ModuleCandidate objects for each proposed update_dict that is non-trivial - candidates = [ModuleCandidate(self.agent, update_dict, optimizer) - for update_dict, optimizer in zip(update_dicts, optimizers) if update_dict is not None] # filter out None updates - return candidates - def validate(self, candidates: List[ModuleCandidate], samples: Samples, verbose: bool = False, **kwargs): - """ Validate the proposed candidate parameters - Args: - candidates (list of ModuleCandidate): A list of ModuleCandidate objects representing the proposed parameters. - samples (list of dict, optional): A list of samples collected in the current iteration. Defaults to None. - verbose (bool, optional): Whether to print verbose output. Defaults to False. - **kwargs: Additional keyword arguments that may be used by the implementation. - Returns: - results (dict): A dictionary where the keys are ids of ModuleCandidate objects and the values are ModuleCandidate and lists of rollouts (list of dicts) containing the module, x, info, target, score, feedback. + """ Override the validate method. In this version we only use training data to update arm statistics. No validation is performed. """ print("--- Validating candidates...") if verbose else None assert isinstance(samples, Samples), "samples must be an instance of Samples." @@ -524,23 +177,6 @@ def validate(self, # The current batch of samples can be used to validate the exploration candidates validate_samples = copy.copy(samples) - # Xuanfei: I commented all these below, only use training samples. - # # Validate newly proposed candidates - # use_prev_batch = self.use_prev_batch # when True, self.validate_sampler == self.train_sampler, and the current batch is used for validation - # candidate_agents = [c.get_module() for c in candidates] # get the modules from the candidates - # validate_samples.add_samples(Samples(*self.validate_sampler.sample(candidate_agents, - # use_prev_batch=use_prev_batch, - # description_prefix='Validating newly proposed candidates: '))) # list of BatchRollout objects - - # if self.validate_exploration_candidates: - # if not use_prev_batch: # validate the exploration candidates that collected the samples as well - # # validate the agents in the validate_dataset - # exploration_agents = [c.get_module() for c in exploration_candidates] # get the modules from the exploration candidates - # exploration_samples = Samples(*self.validate_sampler.sample(exploration_agents, - # description_prefix='Validating exploration candidates: ')) # sample the exploration agents - # validate_samples.add_samples(exploration_samples) # append the exploration samples to the validate_samples - - matched_candidates_and_samples = self.match_candidates_and_samples(exploration_candidates + candidates, validate_samples.samples) results = {} # dict of ModuleCandidate id: (ModuleCandidate, list of rollouts) for c, rollouts in matched_candidates_and_samples.items(): # rollouts is a list of BatchRollouts @@ -548,55 +184,12 @@ def validate(self, return results - def match_candidates_and_samples( - self, - candidates: List[ModuleCandidate], - samples: List[BatchRollout]): - """ - Match the given candidates with the provided samples. - - Args: - candidates (list of ModuleCandidate): A list of ModuleCandidate objects representing the proposed parameters. - samples (list of BatchRollout): A Samples object containing a list of BatchRollout objects, where each BatchRollout contains rollouts collected by an agent on different inputs. - Returns: - results (dict): A dictionary where the keys are ModuleCandidate objects and the values are lists of BatchRollouts collected by the corresponding ModuleCandidate. - - """ - # In general, there may be multiple BatchRollouts collected by the same ModuleCandidate. - # We group the rollouts by the agent (ModuleCandidate) and return a dictionary - # where the keys are the ModuleCandidate objects and the values are Samples - - # Group the samples by the ModuleCandidate id - _results = { c: [] for c in candidates} # dict of ModuleCandidate: list of BatchRollouts - ids = {id(c): c for c in candidates} # dict of ModuleCandidate id: ModuleCandidate - - for rollouts in samples: - assert isinstance(rollouts, BatchRollout), "Each element in samples must be a BatchRollout object." - # rollouts is a BatchRollout object - module = rollouts.module # trace.Module - key = getattr(module, '__TRACE_RESERVED_module_candidate_id') # use the candidate as the key - if key not in ids: - raise ValueError(f"ModuleCandidate with id {key} not found in results. Samples are not collected by known candidates.") - # Append the rollouts to the list of rollouts for the key - _results[ids[key]].append(rollouts) - # assert all candidates have at least one rollout - # Xuanfei: some candidates may not have rollouts - # for c in candidates: - # assert len(_results[c]) > 0, f"ModuleCandidate with id {id(c)} has no rollouts. Samples are not collected by known candidates." - - return _results - def update_memory(self, validate_results, verbose: bool = False, **kwargs): - """ Update the priority queue with the validation results. - Args: - validate_results (dict): A dictionary where the keys are ModuleCandidate objects and the values are lists of rollouts (list of dicts) containing the module, x, info, target, score, feedback. - **kwargs: Additional keyword arguments that may be used by the implementation. + """ Override the update_memory method. In this subclass, we update the priority of all candidates together. Cannot use the parent class's update_memory method, because now some candidates may not have predicted scores. """ print("--- Updating memory with validation results...") if verbose else None for candidate, rollouts in validate_results.items(): - candidate.add_rollouts(rollouts) # add the rollouts to the candidate - - # priority = self.compute_exploration_priority(candidate) # compute the priority for the candidate + candidate.add_rollouts(rollouts) # add the rollouts to the placeholder_priority = self.max_score self.memory.push(placeholder_priority, candidate) @@ -607,111 +200,18 @@ def update_memory_with_regressor(self, verbose: bool = False, **kwargs): # Update predicted scores for all candidates in the memory self.regressor.predict_scores() # Reorder the memory according to the predicted scores - self.memory.reorder_according_to_predicted_scores() - # For debugging, print the memory stats - #self.print_memory_stats() + self.memory = [(-candidate.predicted_score, candidate) for candidate in self.memory] + heapq.heapify(self.memory) def print_memory_stats(self): # For debugging, print all candidates: number, mean_score(), num_rollouts, predicted_score. It is better to see an increasing trend in the predicted scores. for i, (neg_predicted_score, candidate) in enumerate(self.memory): print(f"Candidate {i}, Mean Score: {candidate.mean_score()}, Num Rollouts: {candidate.num_rollouts}, Predicted Score: {-neg_predicted_score}") - def explore(self, verbose: bool = False, **kwargs): - """ Explore the parameter space and propose new candidates. - Args: - **kwargs: Additional keyword arguments that may be used by the implementation. - Returns: - list: A list of proposed candidates. - dict: A dictionary containing logging information about the exploration. - """ - print(f"--- Generating {min(len(self.memory), self.num_candidates)} exploration candidates...") if verbose else None - # pop top self.num_candidates candidates from the priority queue - # self._best_candidate is the exploited candidate from the previous iteration - neg_priority, best_candidate = self.memory.best(self.compute_exploitation_priority) - top_candidates = [best_candidate] if self.use_best_candidate_to_explore else [] - priorities = [-neg_priority] # to store the priorities of the candidates for logging - while len(top_candidates) < self.num_candidates and len(self.memory) > 0: - neg_priority, candidate = self.memory.pop() # pop the top candidate from the priority queue - priority = - neg_priority # remember that we stored negative scores in the priority queue - if self.use_best_candidate_to_explore: - if candidate is self._best_candidate: # skip if it is already in the top candidates - continue - priorities.append(priority) # store the priority of the candidate - top_candidates.append(candidate) # add the candidate to the top candidates - # NOTE some top_candidates can be duplicates - mean_scores = [c.mean_score() for c in top_candidates] - mean_scores = [s for s in mean_scores if s is not None] # filter out None scores - info_dict = { - 'num_exploration_candidates': len(top_candidates), - 'exploration_candidates_mean_priority': np.mean(priorities), # list of priorities of the exploration candidates - 'exploration_candidates_mean_score': np.mean(mean_scores) if mean_scores else None, # list of mean scores of the exploration candidates - 'exploration_candidates_average_num_rollouts': np.mean([c.num_rollouts for c in top_candidates]), - } - - return top_candidates, info_dict - - def exploit(self, verbose: bool = False, **kwargs) -> Tuple[ModuleCandidate, Dict[str, Any]]: - """ Exploit the best candidate from the priority queue. This method should not change the priority queue. - Args: - verbose (bool, optional): Whether to print verbose output. Defaults to False. - **kwargs: Additional keyword arguments that may be used by the implementation. - Returns: - ModuleCandidate: The best candidate from the priority queue. - """ - print("--- Exploiting the best candidate...") if verbose else None - if not self.memory: - raise ValueError("The priority queue is empty. Cannot exploit.") - neg_priority, best_candidate = self.memory.best(self.compute_exploitation_priority) # (priority, candidate) - priority = - neg_priority # remember that we stored negative scores in the priority queue - return best_candidate, { - 'best_candidate_priority': priority, # remember that we stored negative scores in the priority queue - 'best_candidate_mean_score': best_candidate.mean_score(), # mean score of the candidate's rollouts - 'best_candidate_num_rollouts': best_candidate.num_rollouts, # number of rollouts of the candidate - } - # TODO refactor below to reuse scoring def compute_exploitation_priority(self, candidate) -> float: - # NOTE This function can be overridden by subclasses to compute a different score - """ Compute the score for the candidate based on the rollouts during the validation phase. - It can be overridden by subclasses to implement a different scoring strategy. - - Args: - candidate (ModuleCandidate): The candidate for which to compute the score. - Returns: - float: The computed score for the candidate. Higher scores indicate higher priority. - """ + """ Compute the priority for the candidate based on the predicted score. """ if not isinstance(candidate, ModuleCandidate): raise TypeError("candidate must be an instance of ModuleCandidate.") # By default, we compute the mean score of the rollouts return candidate.predicted_score - - def compute_exploration_priority(self, candidate) -> float: - # NOTE This function can be overridden by subclasses to compute a different score - """ Compute the score for the candidate based on the rollouts during the validation phase. - It can be overridden by subclasses to implement a different scoring strategy. - - Args: - candidate (ModuleCandidate): The candidate for which to compute the score. - Returns: - float: The computed score for the candidate. Higher scores indicate higher priority. - """ - if not isinstance(candidate, ModuleCandidate): - raise TypeError("candidate must be an instance of ModuleCandidate.") - # By default, we compute the mean score of the rollouts - - if self.score_function == 'mean': - # Compute the mean score of the candidate's rollouts - return candidate.mean_score() - elif self.score_function == 'time': - return -candidate.created_time # latest candidates have higher priority - elif self.score_function == 'ucb': - # Compute the Upper Confidence Bound (UCB) score - lcb_score, mean_score, ucb_score = candidate.compute_score_confidence( - min_score=self.min_score, - max_score=self.max_score, - scaling_constant=self.ucb_exploration_constant, - total_trials=self.n_iters + 1 # total number of trials conducted so far - ) - return ucb_score # return the UCB score - else: - raise ValueError(f"Unknown score function: {self.score_function}") From 5f93069cde89d33e122375f7a138dd5b38855a8c Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Mon, 15 Sep 2025 23:18:37 -0500 Subject: [PATCH 230/314] fix the logic of calculating mean with None rewards --- opto/features/priority_search/search_template.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/opto/features/priority_search/search_template.py b/opto/features/priority_search/search_template.py index 8297db92..e8d04ab8 100644 --- a/opto/features/priority_search/search_template.py +++ b/opto/features/priority_search/search_template.py @@ -193,7 +193,7 @@ def train(self, samples = None train_scores = [] # to store the scores of the agent during training - + train_counters = [] while self.n_epochs < num_epochs : print(f"Epoch: {self.n_epochs}. Iteration: {self.n_iters}") @@ -224,8 +224,12 @@ def train(self, assert 'self.n_epochs' in info_sample, "info_sample must contain 'self.n_epochs'." train_scores.append(info_sample['mean_score']) # so that mean can be computed + train_counters.append(info_sample['counter']) + # sum over trains scores and counters + avg_train_score = np.sum(train_scores * train_counters) / np.sum(train_counters) + if self.n_iters % log_frequency == 0: - self.logger.log('Algo/Average train score', np.mean(train_scores), self.n_iters, color='blue') + self.logger.log('Algo/Average train score', avg_train_score, self.n_iters, color='blue') self.log(info_update, prefix="Update/") self.log(info_sample, prefix="Sample/") self.n_samples += len(samples) # update the number of samples processed @@ -263,6 +267,7 @@ def sample(self, agents, verbose=False, **kwargs): scores = [item for sublist in scores for item in sublist if item is not None] # flatten the list of scores log_info = { 'mean_score': np.mean(scores), + 'counter': len(scores), 'self.n_epochs': self.train_sampler.n_epochs, } # check if the scores are within the score range From 94fdb755d4cc6fefee18992ace893a6928829965 Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Mon, 15 Sep 2025 23:22:10 -0500 Subject: [PATCH 231/314] fix an import error --- opto/features/priority_search/module_regressor.py | 2 +- .../priority_search/priority_search_with_regressor.py | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/opto/features/priority_search/module_regressor.py b/opto/features/priority_search/module_regressor.py index 12254877..94af8d0c 100644 --- a/opto/features/priority_search/module_regressor.py +++ b/opto/features/priority_search/module_regressor.py @@ -14,7 +14,7 @@ # from black import format_str, FileMode import random # import mathX -from opto.features.priority_search.utils import retry_with_exponential_backoff +from opto.utils.auto_retry import retry_with_exponential_backoff import litellm import time diff --git a/opto/features/priority_search/priority_search_with_regressor.py b/opto/features/priority_search/priority_search_with_regressor.py index 56f3e57b..01c3c198 100644 --- a/opto/features/priority_search/priority_search_with_regressor.py +++ b/opto/features/priority_search/priority_search_with_regressor.py @@ -1,12 +1,8 @@ import numpy as np import copy from typing import Union, List, Tuple, Dict, Any, Optional -from opto.trainer.utils import async_run -from opto.trainer.algorithms.basic_algorithms import batchify -from opto.features.priority_search.search_template import Samples, BatchRollout -from opto.features.priority_search.utils import remap_update_dict +from opto.features.priority_search.search_template import Samples from opto.features.priority_search.module_regressor import ModuleCandidateRegressor -from opto.utils.auto_retry import retry_with_exponential_backoff from opto.features.priority_search.priority_search import PrioritySearch, ModuleCandidate, HeapMemory import heapq From bc77c66987ef522effe3be8969f107ec85d9f70a Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Mon, 15 Sep 2025 23:25:58 -0500 Subject: [PATCH 232/314] fix an issue: at the first iter, all mean scores are none --- opto/features/priority_search/priority_search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index f1d38059..1f9f22da 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -657,7 +657,7 @@ def explore(self, verbose: bool = False, **kwargs): info_dict = { 'num_exploration_candidates': len(top_candidates), 'exploration_candidates_mean_priority': np.mean(priorities), # list of priorities of the exploration candidates - 'exploration_candidates_mean_score': np.mean(mean_scores), # list of mean scores of the exploration candidates + 'exploration_candidates_mean_score': np.mean(mean_scores) if mean_scores else None, # list of mean scores of the exploration candidates 'exploration_candidates_average_num_rollouts': np.mean([c.num_rollouts for c in top_candidates]), } From 4dd961e89dd1fb7d227a7166c3c025f352fe4e86 Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Mon, 15 Sep 2025 23:33:43 -0500 Subject: [PATCH 233/314] fix a bug --- opto/features/priority_search/search_template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opto/features/priority_search/search_template.py b/opto/features/priority_search/search_template.py index e8d04ab8..ea368dba 100644 --- a/opto/features/priority_search/search_template.py +++ b/opto/features/priority_search/search_template.py @@ -226,7 +226,7 @@ def train(self, train_scores.append(info_sample['mean_score']) # so that mean can be computed train_counters.append(info_sample['counter']) # sum over trains scores and counters - avg_train_score = np.sum(train_scores * train_counters) / np.sum(train_counters) + avg_train_score = np.sum(np.array(train_scores) * np.array(train_counters)) / np.sum(train_counters) if self.n_iters % log_frequency == 0: self.logger.log('Algo/Average train score', avg_train_score, self.n_iters, color='blue') From 92a022e956c688c9635ce1e70c2ccf3455859bc0 Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Tue, 16 Sep 2025 00:54:45 -0500 Subject: [PATCH 234/314] update the regressor --- .../priority_search/module_regressor.py | 65 +++++++++---------- 1 file changed, 32 insertions(+), 33 deletions(-) diff --git a/opto/features/priority_search/module_regressor.py b/opto/features/priority_search/module_regressor.py index 94af8d0c..b96a7ded 100644 --- a/opto/features/priority_search/module_regressor.py +++ b/opto/features/priority_search/module_regressor.py @@ -85,18 +85,40 @@ def single_embedding_call(): fallback_embedding = np.random.normal(0, 0.01, self.linear_dim) return fallback_embedding / np.linalg.norm(fallback_embedding) - def _update_memory_embeddings(self): - """Update the embeddings for all candidates in memory.""" - for neg_score, candidate in self.memory: - if hasattr(candidate, "embedding"): - continue - candidate.embedding = self._get_embedding(candidate) - + def _update_memory_embeddings_for_batch(self, batch): + """Update the embeddings for a batch of candidates.""" + # Separate candidates that need embeddings from those that already have them + candidates_needing_embeddings = [] + for candidate in batch: + if not hasattr(candidate, "embedding"): + candidates_needing_embeddings.append(candidate) + + # Generate embeddings in parallel for candidates that need them + if candidates_needing_embeddings: + def get_embedding_for_candidate(candidate): + return self._get_embedding(candidate) + + # Create function list for async_run + embedding_functions = [lambda c=candidate: get_embedding_for_candidate(c) + for candidate in candidates_needing_embeddings] + + # Run embedding generation in parallel + new_embeddings = async_run( + embedding_functions, + max_workers=1000, + description=f"Generating embeddings for {len(candidates_needing_embeddings)} candidates" + ) + + # Assign embeddings back to candidates + for candidate, embedding in zip(candidates_needing_embeddings, new_embeddings): + candidate.embedding = embedding + def update(self): """Update the regression model parameters using the current memory with logistic regression.""" start_time = time.time() print_color("Updating regression model using the current memory with logistic regression...", "blue") - self._update_memory_embeddings() + # Ensure all candidates have embeddings + self._update_memory_embeddings_for_batch(self.memory) # Get training data from memory (only candidates with rollout data) training_candidates = [candidate for neg_score, candidate in self.memory if candidate.num_rollouts > 0 and candidate.mean_score() is not None] @@ -258,31 +280,8 @@ def predict_scores(self,memory = None): memory = self.memory batch = [candidate for _, candidate in memory] - # Separate candidates that need embeddings from those that already have them - candidates_needing_embeddings = [] - for candidate in batch: - if not hasattr(candidate, "embedding"): - candidates_needing_embeddings.append(candidate) - - # Generate embeddings in parallel for candidates that need them - if candidates_needing_embeddings: - def get_embedding_for_candidate(candidate): - return self._get_embedding(candidate) - - # Create function list for async_run - embedding_functions = [lambda c=candidate: get_embedding_for_candidate(c) - for candidate in candidates_needing_embeddings] - - # Run embedding generation in parallel - new_embeddings = async_run( - embedding_functions, - max_workers=1000, - description=f"Generating embeddings for {len(candidates_needing_embeddings)} candidates" - ) - - # Assign embeddings back to candidates - for candidate, embedding in zip(candidates_needing_embeddings, new_embeddings): - candidate.embedding = embedding + # Ensure all candidates have embeddings + self._update_memory_embeddings_for_batch(batch) # Collect all embeddings in order embeddings = [] From 06034866b945122a0c2450cbc72a38e89a46628d Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Tue, 16 Sep 2025 01:06:07 -0500 Subject: [PATCH 235/314] fix a bug --- opto/features/priority_search/module_regressor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/opto/features/priority_search/module_regressor.py b/opto/features/priority_search/module_regressor.py index b96a7ded..a92793c5 100644 --- a/opto/features/priority_search/module_regressor.py +++ b/opto/features/priority_search/module_regressor.py @@ -117,8 +117,10 @@ def update(self): """Update the regression model parameters using the current memory with logistic regression.""" start_time = time.time() print_color("Updating regression model using the current memory with logistic regression...", "blue") + # Extract candidates from memory (memory contains (neg_score, candidate) tuples) + batch = [candidate for _, candidate in self.memory] # Ensure all candidates have embeddings - self._update_memory_embeddings_for_batch(self.memory) + self._update_memory_embeddings_for_batch(batch) # Get training data from memory (only candidates with rollout data) training_candidates = [candidate for neg_score, candidate in self.memory if candidate.num_rollouts > 0 and candidate.mean_score() is not None] From 01c883eb2a3e4905d53c81babfafb2f9d1021ee7 Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Tue, 16 Sep 2025 01:46:59 -0500 Subject: [PATCH 236/314] Made the algorithm with regressor a subclass --- .../priority_search_with_regressor.py | 60 ++++++++----------- 1 file changed, 26 insertions(+), 34 deletions(-) diff --git a/opto/features/priority_search/priority_search_with_regressor.py b/opto/features/priority_search/priority_search_with_regressor.py index 01c3c198..782ed3b3 100644 --- a/opto/features/priority_search/priority_search_with_regressor.py +++ b/opto/features/priority_search/priority_search_with_regressor.py @@ -1,7 +1,7 @@ import numpy as np import copy from typing import Union, List, Tuple, Dict, Any, Optional -from opto.features.priority_search.search_template import Samples +from opto.features.priority_search.search_template import Samples, SearchTemplate from opto.features.priority_search.module_regressor import ModuleCandidateRegressor from opto.features.priority_search.priority_search import PrioritySearch, ModuleCandidate, HeapMemory import heapq @@ -66,31 +66,19 @@ def train(self, regressor_tolerance (float, optional): Convergence tolerance for the regressor. Defaults to 5e-3. """ - # Create agents and optimizers for search - if num_candidates < len(self._optimizers): - print(f"Warning: num_candidates {num_candidates} is less than the number of optimizers {len(self._optimizers)}. Setting num_candidates to {len(self._optimizers)}.") - num_candidates = len(self._optimizers) - self.num_candidates = num_candidates # number of candidates for exploration - self.num_proposals = num_proposals # number of candidates to propose by each optimizer call - - self.validate_exploration_candidates = validate_exploration_candidates # whether to validate the proposed parameters - self.use_best_candidate_to_explore = use_best_candidate_to_explore - self.score_function = score_function # function to compute the score for the candidates - if score_range is None: - score_range = (0, 1) - if score_function == 'ucb': # this requires a bounded score range. By default, it is set to (0, 1) - assert score_range[1]-score_range[0] < float('inf'), \ - "For UCB score function, score_range must be finite. Use 'mean' score function if you want to use unbounded scores." - - self.ucb_exploration_constant = ucb_exploration_constant - self._exploration_candidates = None # This stores the latest candidates used for exploration - self._exploration_candidates_priority = None # This stores the latest candidates' priorities used for exploration - self._best_candidate = None # This stores the latest best candidate used for exploitation - self._best_candidate_priority = None # This stores the latest best candidate's priority used for exploitation - - self.long_term_memory = HeapMemory(size=memory_size) # Initialize the long-term memory with a size limit - self.short_term_memory = HeapMemory(size=short_term_memory_size) # Initialize the short-term memory with a size limit - self.short_term_memory_duration = short_term_memory_duration # number of iterations to keep the candidates in the short-term memory before merging them into the long-term memory + # Initialize the search parameters and memory + self._initialize_search_parameters( + num_candidates=num_candidates, + num_proposals=num_proposals, + validate_exploration_candidates=validate_exploration_candidates, + use_best_candidate_to_explore=use_best_candidate_to_explore, + score_function=score_function, + score_range=score_range, + ucb_exploration_constant=ucb_exploration_constant, + memory_size=memory_size, + short_term_memory_size=short_term_memory_size, + short_term_memory_duration=short_term_memory_duration + ) # Initialize the regressor with the long-term memory and custom parameters - this is the only difference from parent class self.regressor = ModuleCandidateRegressor( @@ -103,7 +91,7 @@ def train(self, tolerance=regressor_tolerance ) - super().train(guide=guide, + SearchTemplate.train(self, guide=guide, train_dataset=train_dataset, validate_dataset=validate_dataset, validate_guide=validate_guide, @@ -140,9 +128,10 @@ def update(self, max_mem_size = self.memory.size if self.memory.size is not None else float('inf') while len(self.memory) < min(max_mem_size, self.num_candidates): self.memory.push(self.max_score, ModuleCandidate(self.agent, optimizer=self.optimizer)) # Push the base agent as the first candidate (This gives the initialization of the priority queue) - if self.memory is self.long_term_memory: # Only update the regressor if we are using the long-term memory - self.regressor.update() - self.regressor.predict_scores(self.memory) # The only difference from the parent class + + + self.update_memory_with_regressor(verbose=verbose, **kwargs) + # 4. Explore and exploit the priority queue self._best_candidate, self._best_candidate_priority, info_exploit = self.exploit(verbose=verbose, **kwargs) # get the best candidate (ModuleCandidate) from the priority queue self._exploration_candidates, self._exploration_candidates_priority, info_explore = self.explore(verbose=verbose, **kwargs) # List of ModuleCandidates @@ -193,11 +182,14 @@ def update_memory_with_regressor(self, verbose: bool = False, **kwargs): """ Update the priority queue with the regressor results. """ print("--- Updating memory with regressor results...") if verbose else None - # Update predicted scores for all candidates in the memory - self.regressor.predict_scores() + if self.memory is self.long_term_memory: # Only update the regressor if we are using the long-term memory + self.regressor.update() + self.regressor.predict_scores(self.memory) # The only difference from the parent class # Reorder the memory according to the predicted scores - self.memory = [(-candidate.predicted_score, candidate) for candidate in self.memory] - heapq.heapify(self.memory) + # Extract candidates from memory tuples and reorder by predicted scores + candidates_with_scores = [(-candidate.predicted_score, candidate) for _, candidate in self.memory] + self.memory.memory = candidates_with_scores # Update the internal list of HeapMemory + heapq.heapify(self.memory.memory) # Heapify the internal list def print_memory_stats(self): # For debugging, print all candidates: number, mean_score(), num_rollouts, predicted_score. It is better to see an increasing trend in the predicted scores. From 1f78a4f87126aa05915fe1daf4d1e29d31c9bd8c Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Tue, 16 Sep 2025 01:47:43 -0500 Subject: [PATCH 237/314] fix bugs and reorganize the code --- .../priority_search/priority_search.py | 98 ++++++++++++++----- 1 file changed, 71 insertions(+), 27 deletions(-) diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index 1f9f22da..6f84a6a9 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -324,31 +324,19 @@ def train(self, """ - # Create agents and optimizers for search - if num_candidates < len(self._optimizers): - print(f"Warning: num_candidates {num_candidates} is less than the number of optimizers {len(self._optimizers)}. Setting num_candidates to {len(self._optimizers)}.") - num_candidates = len(self._optimizers) - self.num_candidates = num_candidates # number of candidates for exploration - self.num_proposals = num_proposals # number of candidates to propose by each optimizer call - - self.validate_exploration_candidates = validate_exploration_candidates # whether to validate the proposed parameters - self.use_best_candidate_to_explore = use_best_candidate_to_explore - self.score_function = score_function # function to compute the score for the candidates - if score_range is None: - score_range = (0, 1) - if score_function == 'ucb': # this requires a bounded score range. By default, it is set to (0, 1) - assert score_range[1]-score_range[0] < float('inf'), \ - "For UCB score function, score_range must be finite. Use 'mean' score function if you want to use unbounded scores." - - self.ucb_exploration_constant = ucb_exploration_constant - self._exploration_candidates = None # This stores the latest candidates used for exploration - self._exploration_candidates_priority = None # This stores the latest candidates' priorities used for exploration - self._best_candidate = None # This stores the latest best candidate used for exploitation - self._best_candidate_priority = None # This stores the latest best candidate's priority used for exploitation - - self.long_term_memory = HeapMemory(size=memory_size) # Initialize the long-term memory with a size limit - self.short_term_memory = HeapMemory(size=short_term_memory_size) # Initialize the short-term memory with a size limit - self.short_term_memory_duration = short_term_memory_duration # number of iterations to keep the candidates in the short-term memory before merging them into the long-term memory + # Initialize search parameters and memory + self._initialize_search_parameters( + num_candidates=num_candidates, + num_proposals=num_proposals, + validate_exploration_candidates=validate_exploration_candidates, + use_best_candidate_to_explore=use_best_candidate_to_explore, + score_function=score_function, + score_range=score_range, + ucb_exploration_constant=ucb_exploration_constant, + memory_size=memory_size, + short_term_memory_size=short_term_memory_size, + short_term_memory_duration=short_term_memory_duration + ) super().train(guide=guide, train_dataset=train_dataset, @@ -403,16 +391,72 @@ def update(self, info_log.update(info_explore) # add the info from the explore step return self._best_candidate.update_dict, [c.get_module() for c in self._exploration_candidates], info_log + def _initialize_search_parameters(self, + num_candidates, + num_proposals, + validate_exploration_candidates, + use_best_candidate_to_explore, + score_function, + score_range, + ucb_exploration_constant, + memory_size, + short_term_memory_size, + short_term_memory_duration): + """Initialize search parameters and memory structures. + + Args: + num_candidates (int): Number of candidates to propose for exploration + num_proposals (int): Number of proposals to generate per optimizer + validate_exploration_candidates (bool): Whether to validate the proposed parameters + use_best_candidate_to_explore (bool): Whether to use the best candidate as part of exploration + score_function (str): Function to compute the score for candidates ('mean' or 'ucb') + score_range (tuple): Range of scores for UCB computation + ucb_exploration_constant (float): Exploration constant for UCB score function + memory_size (int): Size of the long-term heap memory + short_term_memory_size (int): Size of the short-term memory + short_term_memory_duration (int): Duration to keep candidates in short-term memory + """ + # Validate and adjust num_candidates based on number of optimizers + if num_candidates < len(self._optimizers): + print(f"Warning: num_candidates {num_candidates} is less than the number of optimizers {len(self._optimizers)}. Setting num_candidates to {len(self._optimizers)}.") + num_candidates = len(self._optimizers) + + # Set core parameters + self.num_candidates = num_candidates + self.num_proposals = num_proposals + self.validate_exploration_candidates = validate_exploration_candidates + self.use_best_candidate_to_explore = use_best_candidate_to_explore + self.score_function = score_function + + # Validate and set score range for UCB + if score_range is None: + score_range = (0, 1) + if score_function == 'ucb': + assert score_range[1] - score_range[0] < float('inf'), \ + "For UCB score function, score_range must be finite. Use 'mean' score function if you want to use unbounded scores." + + self.ucb_exploration_constant = ucb_exploration_constant + + # Initialize candidate tracking variables + self._exploration_candidates = None + self._exploration_candidates_priority = None + self._best_candidate = None + self._best_candidate_priority = None + + # Initialize memory structures + self.long_term_memory = HeapMemory(size=memory_size) + self.short_term_memory = HeapMemory(size=short_term_memory_size) + self.short_term_memory_duration = short_term_memory_duration + @property def memory(self): - if self.short_term_memory_duration == 0: + if self.short_term_memory.size == 0 or self.short_term_memory_duration == 0: return self.long_term_memory # short_term_memory is finite and non-zero if self.n_iters % self.short_term_memory_duration == 0: # merge the the short-term memory into the long-term memory if len(self.short_term_memory) > 0: self.long_term_memory.append(self.short_term_memory) - heapq.heapify(self.long_term_memory) self.short_term_memory.reset() print('Merging short-term memory into long-term memory of PrioritySearch.') return self.long_term_memory From 5c07e6188f7b97091cac33e4ab8410a51ff42889 Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Tue, 16 Sep 2025 14:58:06 -0500 Subject: [PATCH 238/314] auto-retry in llm.py --- opto/utils/llm.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/opto/utils/llm.py b/opto/utils/llm.py index 9cd034b3..28bdb43f 100644 --- a/opto/utils/llm.py +++ b/opto/utils/llm.py @@ -4,6 +4,7 @@ import json import os import warnings +from .auto_retry import retry_with_exponential_backoff try: import autogen # We import autogen here to avoid the need of installing autogen @@ -182,9 +183,19 @@ def _factory(cls, model_name: str): if azure_token_provider_scope is not None: from azure.identity import DefaultAzureCredential, get_bearer_token_provider credential = get_bearer_token_provider(DefaultAzureCredential(), azure_token_provider_scope) - return lambda *args, **kwargs: litellm.completion(model_name, *args, - azure_ad_token_provider=credential, **kwargs) - return lambda *args, **kwargs: litellm.completion(model_name, *args, **kwargs) + return lambda *args, **kwargs: retry_with_exponential_backoff( + lambda: litellm.completion(model_name, *args, + azure_ad_token_provider=credential, **kwargs), + max_retries=10, + base_delay=1.0, + operation_name="LiteLLM_completion" + ) + return lambda *args, **kwargs: retry_with_exponential_backoff( + lambda: litellm.completion(model_name, *args, **kwargs), + max_retries=10, + base_delay=1.0, + operation_name="LiteLLM_completion" + ) @property def model(self): From 67f4482eb5584dca79e95dd3a829085f2ab1f9ab Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Tue, 16 Sep 2025 15:01:32 -0500 Subject: [PATCH 239/314] deal with api error --- opto/optimizers/optoprime.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/opto/optimizers/optoprime.py b/opto/optimizers/optoprime.py index 909be520..e50af549 100644 --- a/opto/optimizers/optoprime.py +++ b/opto/optimizers/optoprime.py @@ -502,13 +502,15 @@ def _step( system_prompt = self.replace_symbols(system_prompt, self.prompt_symbols) user_prompt = self.replace_symbols(user_prompt, self.prompt_symbols) - - response = self.call_llm( - system_prompt=system_prompt, - user_prompt=user_prompt, - verbose=verbose, - max_tokens=self.max_tokens, - ) + try: + response = self.call_llm( + system_prompt=system_prompt, + user_prompt=user_prompt, + verbose=verbose, + max_tokens=self.max_tokens, + ) + except Exception as e: # When api call fails, we return an empty update dict + return {} if "TERMINATE" in response: return {} From e8c0e98715d6815af57d7ec1a52707c405c0a0be Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Tue, 16 Sep 2025 15:02:05 -0500 Subject: [PATCH 240/314] delete auto retry here and handle one None reward issue --- opto/features/priority_search/priority_search.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index 6f84a6a9..67b4b823 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -104,7 +104,7 @@ def mean_score(self): """ Compute the score of the candidate based on the rollouts. """ if not self.rollouts: return None - scores = [r['score'] for r in self.rollouts] + scores = [r['score'] for r in self.rollouts if r['score'] is not None] return np.mean(scores) if scores else None def compute_score_confidence(self, min_score, max_score, scaling_constant=1.0, total_trials=1): @@ -551,12 +551,8 @@ def _backward(n): # For each optimizer, containing the backward feedback, we call it n_proposals times to get the proposed parameters. def _step(n): optimizer = optimizers[n] - update_dict = retry_with_exponential_backoff( - lambda: optimizer.step(verbose=verbose, num_threads=self.num_threads, bypassing=True, **kwargs), - max_retries=10, - base_delay=1.0, - operation_name="optimizer_step" - ) + update_dict = optimizer.step(verbose=verbose, num_threads=self.num_threads, bypassing=True, **kwargs) + if not update_dict: # if the optimizer did not propose any updates return None # return None to indicate no updates were proposed # update_dict may only contain some of the parameters of the agent, we need to make sure it contains all the parameters From 279bbf84815d439a9da9d3ee03450424c8fb5554 Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Tue, 16 Sep 2025 15:20:09 -0500 Subject: [PATCH 241/314] discard the changes on the optimizer --- opto/optimizers/optoprime.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/opto/optimizers/optoprime.py b/opto/optimizers/optoprime.py index e50af549..a7712486 100644 --- a/opto/optimizers/optoprime.py +++ b/opto/optimizers/optoprime.py @@ -502,16 +502,14 @@ def _step( system_prompt = self.replace_symbols(system_prompt, self.prompt_symbols) user_prompt = self.replace_symbols(user_prompt, self.prompt_symbols) - try: - response = self.call_llm( + + response = self.call_llm( system_prompt=system_prompt, user_prompt=user_prompt, verbose=verbose, max_tokens=self.max_tokens, ) - except Exception as e: # When api call fails, we return an empty update dict - return {} - + if "TERMINATE" in response: return {} From 5fa8a1994c12a6b117738080512cc9f74b5cbadd Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Tue, 16 Sep 2025 15:21:53 -0500 Subject: [PATCH 242/314] handle api error of optimizer.step --- opto/features/priority_search/priority_search.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index 67b4b823..b061396b 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -551,7 +551,11 @@ def _backward(n): # For each optimizer, containing the backward feedback, we call it n_proposals times to get the proposed parameters. def _step(n): optimizer = optimizers[n] - update_dict = optimizer.step(verbose=verbose, num_threads=self.num_threads, bypassing=True, **kwargs) + try: + update_dict = optimizer.step(verbose=verbose, num_threads=self.num_threads, bypassing=True, **kwargs) + except Exception as e: + print(f"Error calling optimizer.step: {e}") + return None if not update_dict: # if the optimizer did not propose any updates return None # return None to indicate no updates were proposed From 9d8dfe4479a1f7c70f6be3c192a618da8b4abad6 Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Tue, 16 Sep 2025 15:25:01 -0500 Subject: [PATCH 243/314] add the assertation back --- opto/features/priority_search/priority_search.py | 4 ++-- .../priority_search/priority_search_with_regressor.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index b061396b..22e7eb4b 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -657,8 +657,8 @@ def match_candidates_and_samples( # Append the rollouts to the list of rollouts for the key _results[ids[key]].append(rollouts) # assert all candidates have at least one rollout - # for c in candidates: - # assert len(_results[c]) > 0, f"ModuleCandidate with id {id(c)} has no rollouts. Samples are not collected by known candidates." + for c in candidates: + assert len(_results[c]) > 0, f"ModuleCandidate with id {id(c)} has no rollouts. Samples are not collected by known candidates." return _results diff --git a/opto/features/priority_search/priority_search_with_regressor.py b/opto/features/priority_search/priority_search_with_regressor.py index 782ed3b3..6396c502 100644 --- a/opto/features/priority_search/priority_search_with_regressor.py +++ b/opto/features/priority_search/priority_search_with_regressor.py @@ -162,7 +162,7 @@ def validate(self, # The current batch of samples can be used to validate the exploration candidates validate_samples = copy.copy(samples) - matched_candidates_and_samples = self.match_candidates_and_samples(exploration_candidates + candidates, validate_samples.samples) + matched_candidates_and_samples = self.match_candidates_and_samples(exploration_candidates, validate_samples.samples) results = {} # dict of ModuleCandidate id: (ModuleCandidate, list of rollouts) for c, rollouts in matched_candidates_and_samples.items(): # rollouts is a list of BatchRollouts results[c] = [ r for rr in rollouts for r in rr.to_list()] # we only need the list of dicts From 819483f419d100534bb064a9b32ebe32b71b76e3 Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Tue, 16 Sep 2025 17:15:21 -0500 Subject: [PATCH 244/314] update the log --- opto/features/priority_search/priority_search.py | 6 +++++- opto/features/priority_search/search_template.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index 22e7eb4b..0fd38861 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -386,6 +386,10 @@ def update(self, 'using_short_term_memory': self.memory is self.short_term_memory, # whether the current memory is the short-term memory 'using_long_term_memory': self.memory is self.long_term_memory, # whether the current memory is the long-term memory } + # If using long-term memory, log the total number of samples processed + if self.memory is self.long_term_memory: + total_samples = sum([candidate.num_rollouts for _, candidate in self.memory]) + info_log.update({'Total samples': total_samples}) info_log.update(info_exploit) # add the info from the exploit step info_log.update(info_explore) # add the info from the explore step @@ -617,7 +621,7 @@ def validate(self, description_prefix='Validating exploration candidates: ')) # sample the exploration agents validate_samples.add_samples(exploration_samples) # append the exploration samples to the validate_samples - + matched_candidates_and_samples = self.match_candidates_and_samples(exploration_candidates + candidates, validate_samples.samples) results = {} # dict of ModuleCandidate id: (ModuleCandidate, list of rollouts) for c, rollouts in matched_candidates_and_samples.items(): # rollouts is a list of BatchRollouts diff --git a/opto/features/priority_search/search_template.py b/opto/features/priority_search/search_template.py index ea368dba..73b8cd10 100644 --- a/opto/features/priority_search/search_template.py +++ b/opto/features/priority_search/search_template.py @@ -233,7 +233,7 @@ def train(self, self.log(info_update, prefix="Update/") self.log(info_sample, prefix="Sample/") self.n_samples += len(samples) # update the number of samples processed - self.logger.log('Algo/Number of samples', self.n_samples, self.n_iters, color='blue') + self.logger.log('Algo/Number of training samples', self.n_samples, self.n_iters, color='blue') # Log parameters for p in self.agent.parameters(): self.logger.log(f"Parameter/{p.name}", p.data, self.n_iters, color='red') From 3b2155b94ba48c2fbbcd2e09cd176b053b22dea4 Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Tue, 16 Sep 2025 18:32:32 -0500 Subject: [PATCH 245/314] add logging for total samples in long term memory --- opto/features/priority_search/priority_search.py | 4 ++-- .../priority_search/priority_search_with_regressor.py | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index 0fd38861..6a348889 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -386,10 +386,10 @@ def update(self, 'using_short_term_memory': self.memory is self.short_term_memory, # whether the current memory is the short-term memory 'using_long_term_memory': self.memory is self.long_term_memory, # whether the current memory is the long-term memory } - # If using long-term memory, log the total number of samples processed + # If using long-term memory, log the total number of samples in the long-term memory if self.memory is self.long_term_memory: total_samples = sum([candidate.num_rollouts for _, candidate in self.memory]) - info_log.update({'Total samples': total_samples}) + info_log.update({'total_samples': total_samples}) info_log.update(info_exploit) # add the info from the exploit step info_log.update(info_explore) # add the info from the explore step diff --git a/opto/features/priority_search/priority_search_with_regressor.py b/opto/features/priority_search/priority_search_with_regressor.py index 6396c502..94a3ebb3 100644 --- a/opto/features/priority_search/priority_search_with_regressor.py +++ b/opto/features/priority_search/priority_search_with_regressor.py @@ -143,7 +143,11 @@ def update(self, 'using_short_term_memory': self.memory is self.short_term_memory, # whether the current memory is the short-term memory 'using_long_term_memory': self.memory is self.long_term_memory, # whether the current memory is the long-term memory } - + # If using long-term memory, log the total number of samples in the long-term memory + if self.memory is self.long_term_memory: + total_samples = sum([candidate.num_rollouts for _, candidate in self.memory]) + info_log.update({'total_samples': total_samples}) + info_log.update(info_exploit) # add the info from the exploit step info_log.update(info_explore) # add the info from the explore step return self._best_candidate.update_dict, [c.get_module() for c in self._exploration_candidates], info_log From 29244a8739238ef83fcaf9edb7e91750d5729e55 Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Tue, 16 Sep 2025 22:36:12 -0500 Subject: [PATCH 246/314] Fix bugs and modify the logging fo total_samples --- opto/features/priority_search/priority_search.py | 10 ++++++---- .../priority_search/priority_search_with_regressor.py | 11 ++++++----- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index 6a348889..adbcde32 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -375,9 +375,7 @@ def update(self, max_mem_size = self.memory.size if self.memory.size is not None else float('inf') while len(self.memory) < min(max_mem_size, self.num_candidates): self.memory.push(self.max_score, ModuleCandidate(self.agent, optimizer=self.optimizer)) # Push the base agent as the first candidate (This gives the initialization of the priority queue) - # 4. Explore and exploit the priority queue - self._best_candidate, self._best_candidate_priority, info_exploit = self.exploit(verbose=verbose, **kwargs) # get the best candidate (ModuleCandidate) from the priority queue - self._exploration_candidates, self._exploration_candidates_priority, info_explore = self.explore(verbose=verbose, **kwargs) # List of ModuleCandidates + # TODO Log information about the update info_log = { 'n_iters': self.n_iters, # number of iterations @@ -388,9 +386,13 @@ def update(self, } # If using long-term memory, log the total number of samples in the long-term memory if self.memory is self.long_term_memory: + # Now all the candidates are in the long-term memory. This logging is got before popping out the exploration candidates. total_samples = sum([candidate.num_rollouts for _, candidate in self.memory]) info_log.update({'total_samples': total_samples}) + # 4. Explore and exploit the priority queue + self._best_candidate, self._best_candidate_priority, info_exploit = self.exploit(verbose=verbose, **kwargs) # get the best candidate (ModuleCandidate) from the priority queue + self._exploration_candidates, self._exploration_candidates_priority, info_explore = self.explore(verbose=verbose, **kwargs) # List of ModuleCandidates info_log.update(info_exploit) # add the info from the exploit step info_log.update(info_explore) # add the info from the explore step return self._best_candidate.update_dict, [c.get_module() for c in self._exploration_candidates], info_log @@ -545,7 +547,7 @@ def _backward(n): optimizers = async_run([_backward]*n_batches, # run the optimizer step for each agent in parallel args_list=args_list, max_workers=self.num_threads, # use the number of threads specified in the class - description=None) + description='Backward') assert len(optimizers) == n_batches, "Number of optimizers must match number of batch rollouts." # need to copy optimizer for the n_proposals # NOTE when optimizer is deepcopied, its parameters are not copied. diff --git a/opto/features/priority_search/priority_search_with_regressor.py b/opto/features/priority_search/priority_search_with_regressor.py index 94a3ebb3..b985f7e3 100644 --- a/opto/features/priority_search/priority_search_with_regressor.py +++ b/opto/features/priority_search/priority_search_with_regressor.py @@ -1,7 +1,7 @@ import numpy as np import copy from typing import Union, List, Tuple, Dict, Any, Optional -from opto.features.priority_search.search_template import Samples, SearchTemplate +from opto.features.priority_search.search_template import Samples, SearchTemplate, BatchRollout from opto.features.priority_search.module_regressor import ModuleCandidateRegressor from opto.features.priority_search.priority_search import PrioritySearch, ModuleCandidate, HeapMemory import heapq @@ -132,9 +132,6 @@ def update(self, self.update_memory_with_regressor(verbose=verbose, **kwargs) - # 4. Explore and exploit the priority queue - self._best_candidate, self._best_candidate_priority, info_exploit = self.exploit(verbose=verbose, **kwargs) # get the best candidate (ModuleCandidate) from the priority queue - self._exploration_candidates, self._exploration_candidates_priority, info_explore = self.explore(verbose=verbose, **kwargs) # List of ModuleCandidates # TODO Log information about the update info_log = { 'n_iters': self.n_iters, # number of iterations @@ -147,7 +144,9 @@ def update(self, if self.memory is self.long_term_memory: total_samples = sum([candidate.num_rollouts for _, candidate in self.memory]) info_log.update({'total_samples': total_samples}) - + # 4. Explore and exploit the priority queue + self._best_candidate, self._best_candidate_priority, info_exploit = self.exploit(verbose=verbose, **kwargs) # get the best candidate (ModuleCandidate) from the priority queue + self._exploration_candidates, self._exploration_candidates_priority, info_explore = self.explore(verbose=verbose, **kwargs) # List of ModuleCandidates info_log.update(info_exploit) # add the info from the exploit step info_log.update(info_explore) # add the info from the explore step return self._best_candidate.update_dict, [c.get_module() for c in self._exploration_candidates], info_log @@ -167,6 +166,8 @@ def validate(self, # The current batch of samples can be used to validate the exploration candidates validate_samples = copy.copy(samples) matched_candidates_and_samples = self.match_candidates_and_samples(exploration_candidates, validate_samples.samples) + # Append new candidates with out rollouts to matched_candidates_and_samples + matched_candidates_and_samples.update({c: [] for c in candidates }) results = {} # dict of ModuleCandidate id: (ModuleCandidate, list of rollouts) for c, rollouts in matched_candidates_and_samples.items(): # rollouts is a list of BatchRollouts results[c] = [ r for rr in rollouts for r in rr.to_list()] # we only need the list of dicts From 41f03a90f75de4a65b6267dd22e8a3a9fc68a5af Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Wed, 17 Sep 2025 10:31:44 -0500 Subject: [PATCH 247/314] fix a bug of none score (evaluate may return an 2d array) --- opto/features/priority_search/search_template.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/opto/features/priority_search/search_template.py b/opto/features/priority_search/search_template.py index 73b8cd10..9c442626 100644 --- a/opto/features/priority_search/search_template.py +++ b/opto/features/priority_search/search_template.py @@ -301,7 +301,11 @@ def evaluate(self, agent, guide, xs, infos, min_score=None, num_samples=1, num_t num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads test_scores = evaluate(agent, guide, xs, infos, min_score=min_score, num_threads=num_threads, num_samples=num_samples, description=description) - return np.mean([s for s in test_scores if s is not None]) + # Filter out None values and convert to list to ensure proper filtering + valid_scores = [s for s in test_scores.flatten() if s is not None] + if len(valid_scores) == 0: + raise ValueError("All scores are None.") + return np.mean(valid_scores) def save(self, save_path): print(f"Saving algorithm state to {save_path} at iteration {self.n_iters}.") From 2242c568ad6794442f30d44241991c7a5fad4657 Mon Sep 17 00:00:00 2001 From: chinganc Date: Wed, 17 Sep 2025 19:44:38 +0000 Subject: [PATCH 248/314] Clean up. Rename memory_size to long_term_memory_size. --- .../priority_search/priority_search.py | 139 +++++++++--------- .../priority_search/search_template.py | 20 +-- opto/trainer/utils.py | 17 +++ opto/utils/llm.py | 14 +- 4 files changed, 100 insertions(+), 90 deletions(-) diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index adbcde32..cda7236c 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -10,7 +10,6 @@ from opto.trainer.algorithms.basic_algorithms import batchify from opto.features.priority_search.search_template import SearchTemplate, Samples, BatchRollout, save_train_config from opto.features.priority_search.utils import set_module_parameters, remap_update_dict, create_module_from_update_dict, is_module_copy -from opto.utils.auto_retry import retry_with_exponential_backoff class ModuleCandidate: @@ -104,8 +103,7 @@ def mean_score(self): """ Compute the score of the candidate based on the rollouts. """ if not self.rollouts: return None - scores = [r['score'] for r in self.rollouts if r['score'] is not None] - return np.mean(scores) if scores else None + return safe_mean([r['score'] for r in self.rollouts]) def compute_score_confidence(self, min_score, max_score, scaling_constant=1.0, total_trials=1): """Compute the UCB, mean, LCB score for the candidate. After queried, the number of confidence queries is incremented. @@ -284,7 +282,7 @@ def train(self, num_proposals: int = 1, # number of proposals to generate per optimizer validate_exploration_candidates: bool = True, # whether to validate the proposed parameters for exploration use_best_candidate_to_explore: bool = True, # whether to use the best candidate as part of the exploration candidates - memory_size: Optional[int] = None, # size of the long-term heap memory to store the candidates; if None, no limit is set + long_term_memory_size: Optional[int] = None, # size of the long-term heap memory to store the candidates; if None, no limit is set short_term_memory_size: Optional[int] = None, # size of the short-term memory to store the most recent candidates; if None, no limit is set short_term_memory_duration: Optional[int] = 0, # number of iterations to keep the candidates in the short-term memory before merging them into the long-term memory. 0 means only long-term memory is used. score_function: str = 'mean', # function to compute the score for the candidates; 'mean' or 'ucb' @@ -323,7 +321,6 @@ def train(self, **kwargs: Additional keyword arguments that may be used by the implementation. """ - # Initialize search parameters and memory self._initialize_search_parameters( num_candidates=num_candidates, @@ -333,7 +330,7 @@ def train(self, score_function=score_function, score_range=score_range, ucb_exploration_constant=ucb_exploration_constant, - memory_size=memory_size, + long_term_memory_size=long_term_memory_size, short_term_memory_size=short_term_memory_size, short_term_memory_duration=short_term_memory_duration ) @@ -356,60 +353,19 @@ def train(self, save_path=save_path, **kwargs) - def update(self, - samples: Union[Samples, None] = None, - verbose: bool = False, - **kwargs): #-> Tuple[Dict[ParameterNode, Any], List[trace.Module], Dict[str, Any]]: - """ Update the agent using the collected samples. - """ - - # samples is None in the first iteration - if samples is not None: - # 1. Propose new parameters based on running LLM optimizers on the collected samples - candidates = self.propose(samples, verbose=verbose, **kwargs) # List of ModuleCandidates - # 2. Validate the proposed parameters - validate_results = self.validate(candidates, samples, verbose=verbose, **kwargs) # this updates the priority queue - # 3. Update the priority queue with the validation results - self.update_memory(validate_results, verbose=verbose, **kwargs) # samples are provided here in case candidates do not capture full information - else: # The first iteration. - max_mem_size = self.memory.size if self.memory.size is not None else float('inf') - while len(self.memory) < min(max_mem_size, self.num_candidates): - self.memory.push(self.max_score, ModuleCandidate(self.agent, optimizer=self.optimizer)) # Push the base agent as the first candidate (This gives the initialization of the priority queue) - - # TODO Log information about the update - info_log = { - 'n_iters': self.n_iters, # number of iterations - 'short_term_memory_size': len(self.short_term_memory), # size of the short-term memory - 'long_term_memory_size': len(self.long_term_memory), # size of the long-term memory - 'using_short_term_memory': self.memory is self.short_term_memory, # whether the current memory is the short-term memory - 'using_long_term_memory': self.memory is self.long_term_memory, # whether the current memory is the long-term memory - } - # If using long-term memory, log the total number of samples in the long-term memory - if self.memory is self.long_term_memory: - # Now all the candidates are in the long-term memory. This logging is got before popping out the exploration candidates. - total_samples = sum([candidate.num_rollouts for _, candidate in self.memory]) - info_log.update({'total_samples': total_samples}) - - # 4. Explore and exploit the priority queue - self._best_candidate, self._best_candidate_priority, info_exploit = self.exploit(verbose=verbose, **kwargs) # get the best candidate (ModuleCandidate) from the priority queue - self._exploration_candidates, self._exploration_candidates_priority, info_explore = self.explore(verbose=verbose, **kwargs) # List of ModuleCandidates - info_log.update(info_exploit) # add the info from the exploit step - info_log.update(info_explore) # add the info from the explore step - return self._best_candidate.update_dict, [c.get_module() for c in self._exploration_candidates], info_log - - def _initialize_search_parameters(self, - num_candidates, - num_proposals, - validate_exploration_candidates, - use_best_candidate_to_explore, - score_function, - score_range, - ucb_exploration_constant, - memory_size, - short_term_memory_size, + def _initialize_search_parameters(self, + num_candidates, + num_proposals, + validate_exploration_candidates, + use_best_candidate_to_explore, + score_function, + score_range, + ucb_exploration_constant, + long_term_memory_size, + short_term_memory_size, short_term_memory_duration): """Initialize search parameters and memory structures. - + Args: num_candidates (int): Number of candidates to propose for exploration num_proposals (int): Number of proposals to generate per optimizer @@ -418,7 +374,7 @@ def _initialize_search_parameters(self, score_function (str): Function to compute the score for candidates ('mean' or 'ucb') score_range (tuple): Range of scores for UCB computation ucb_exploration_constant (float): Exploration constant for UCB score function - memory_size (int): Size of the long-term heap memory + long_term_memory_size (int): Size of the long-term heap memory short_term_memory_size (int): Size of the short-term memory short_term_memory_duration (int): Duration to keep candidates in short-term memory """ @@ -426,34 +382,75 @@ def _initialize_search_parameters(self, if num_candidates < len(self._optimizers): print(f"Warning: num_candidates {num_candidates} is less than the number of optimizers {len(self._optimizers)}. Setting num_candidates to {len(self._optimizers)}.") num_candidates = len(self._optimizers) - + # Set core parameters self.num_candidates = num_candidates self.num_proposals = num_proposals self.validate_exploration_candidates = validate_exploration_candidates self.use_best_candidate_to_explore = use_best_candidate_to_explore self.score_function = score_function - + # Validate and set score range for UCB if score_range is None: score_range = (0, 1) if score_function == 'ucb': assert score_range[1] - score_range[0] < float('inf'), \ "For UCB score function, score_range must be finite. Use 'mean' score function if you want to use unbounded scores." - + self.ucb_exploration_constant = ucb_exploration_constant - + # Initialize candidate tracking variables self._exploration_candidates = None self._exploration_candidates_priority = None self._best_candidate = None self._best_candidate_priority = None - + # Initialize memory structures - self.long_term_memory = HeapMemory(size=memory_size) + self.long_term_memory = HeapMemory(size=long_term_memory_size) self.short_term_memory = HeapMemory(size=short_term_memory_size) self.short_term_memory_duration = short_term_memory_duration + + def update(self, + samples: Union[Samples, None] = None, + verbose: bool = False, + **kwargs): #-> Tuple[Dict[ParameterNode, Any], List[trace.Module], Dict[str, Any]]: + """ Update the agent using the collected samples. + """ + + # samples is None in the first iteration + if samples is not None: + # 1. Propose new parameters based on running LLM optimizers on the collected samples + candidates = self.propose(samples, verbose=verbose, **kwargs) # List of ModuleCandidates + # 2. Validate the proposed parameters + validate_results = self.validate(candidates, samples, verbose=verbose, **kwargs) # this updates the priority queue + # 3. Update the priority queue with the validation results + self.update_memory(validate_results, verbose=verbose, **kwargs) # samples are provided here in case candidates do not capture full information + else: # The first iteration. + max_mem_size = self.memory.size if self.memory.size is not None else float('inf') + while len(self.memory) < min(max_mem_size, self.num_candidates): + self.memory.push(self.max_score, ModuleCandidate(self.agent, optimizer=self.optimizer)) # Push the base agent as the first candidate (This gives the initialization of the priority queue) + + # Log information about the update + info_log = { + 'n_iters': self.n_iters, # number of iterations + 'short_term_memory_size': len(self.short_term_memory), # size of the short-term memory + 'long_term_memory_size': len(self.long_term_memory), # size of the long-term memory + 'using_short_term_memory': self.memory is self.short_term_memory, # whether the current memory is the short-term memory + 'using_long_term_memory': self.memory is self.long_term_memory, # whether the current memory is the long-term memory + } + total_samples = sum([candidate.num_rollouts for _, candidate in self.short_term_memory]) + \ + sum([candidate.num_rollouts for _, candidate in self.long_term_memory]) + info_log.update({'total_samples': total_samples}) + + # 4. Explore and exploit the priority queue + self._best_candidate, self._best_candidate_priority, info_exploit = self.exploit(verbose=verbose, **kwargs) # get the best candidate (ModuleCandidate) from the priority queue + self._exploration_candidates, self._exploration_candidates_priority, info_explore = self.explore(verbose=verbose, **kwargs) # List of ModuleCandidates + + info_log.update(info_exploit) # add the info from the exploit step + info_log.update(info_explore) # add the info from the explore step + return self._best_candidate.update_dict, [c.get_module() for c in self._exploration_candidates], info_log + @property def memory(self): if self.short_term_memory.size == 0 or self.short_term_memory_duration == 0: @@ -561,8 +558,8 @@ def _step(n): update_dict = optimizer.step(verbose=verbose, num_threads=self.num_threads, bypassing=True, **kwargs) except Exception as e: print(f"Error calling optimizer.step: {e}") - return None - + update_dict = None + if not update_dict: # if the optimizer did not propose any updates return None # return None to indicate no updates were proposed # update_dict may only contain some of the parameters of the agent, we need to make sure it contains all the parameters @@ -623,7 +620,7 @@ def validate(self, description_prefix='Validating exploration candidates: ')) # sample the exploration agents validate_samples.add_samples(exploration_samples) # append the exploration samples to the validate_samples - + matched_candidates_and_samples = self.match_candidates_and_samples(exploration_candidates + candidates, validate_samples.samples) results = {} # dict of ModuleCandidate id: (ModuleCandidate, list of rollouts) for c, rollouts in matched_candidates_and_samples.items(): # rollouts is a list of BatchRollouts @@ -706,9 +703,9 @@ def explore(self, verbose: bool = False, **kwargs): mean_scores = [s for s in mean_scores if s is not None] # filter out None scores info_dict = { 'num_exploration_candidates': len(top_candidates), - 'exploration_candidates_mean_priority': np.mean(priorities), # list of priorities of the exploration candidates - 'exploration_candidates_mean_score': np.mean(mean_scores) if mean_scores else None, # list of mean scores of the exploration candidates - 'exploration_candidates_average_num_rollouts': np.mean([c.num_rollouts for c in top_candidates]), + 'exploration_candidates_mean_priority': safe_mean(priorities), # list of priorities of the exploration candidates + 'exploration_candidates_mean_score': safe_mean(mean_scores), # list of mean scores of the exploration candidates + 'exploration_candidates_average_num_rollouts': safe_mean([c.num_rollouts for c in top_candidates]), } return top_candidates, priorities, info_dict diff --git a/opto/features/priority_search/search_template.py b/opto/features/priority_search/search_template.py index 9c442626..00fe7ddd 100644 --- a/opto/features/priority_search/search_template.py +++ b/opto/features/priority_search/search_template.py @@ -7,6 +7,7 @@ from opto.trainer.loader import DataLoader from opto.features.priority_search.sampler import Sampler, BatchRollout from opto.trainer.evaluators import evaluate # TODO update evaluate implementation +from opto.trainer.utils import safe_mean from dataclasses import dataclass import pickle, copy, os # TODO save and load SearchTemplate @@ -193,7 +194,7 @@ def train(self, samples = None train_scores = [] # to store the scores of the agent during training - train_counters = [] + train_num_samples = [] # to store the number of samples used to compute each score while self.n_epochs < num_epochs : print(f"Epoch: {self.n_epochs}. Iteration: {self.n_iters}") @@ -224,11 +225,10 @@ def train(self, assert 'self.n_epochs' in info_sample, "info_sample must contain 'self.n_epochs'." train_scores.append(info_sample['mean_score']) # so that mean can be computed - train_counters.append(info_sample['counter']) - # sum over trains scores and counters - avg_train_score = np.sum(np.array(train_scores) * np.array(train_counters)) / np.sum(train_counters) - + train_num_samples.append(info_sample['num_samples']) + if self.n_iters % log_frequency == 0: + avg_train_score = np.sum(np.array(train_scores) * np.array(train_num_samples)) / np.sum(train_num_samples) self.logger.log('Algo/Average train score', avg_train_score, self.n_iters, color='blue') self.log(info_update, prefix="Update/") self.log(info_sample, prefix="Sample/") @@ -266,8 +266,8 @@ def sample(self, agents, verbose=False, **kwargs): scores = [ g.get_scores() for g in samples.samples] # list of list of scores for each BatchRollout scores = [item for sublist in scores for item in sublist if item is not None] # flatten the list of scores log_info = { - 'mean_score': np.mean(scores), - 'counter': len(scores), + 'mean_score': safe_mean(scores, 0), # return 0, if num_samples == 0 so that the weighted mean can be computed + 'num_samples': len(scores), 'self.n_epochs': self.train_sampler.n_epochs, } # check if the scores are within the score range @@ -301,11 +301,7 @@ def evaluate(self, agent, guide, xs, infos, min_score=None, num_samples=1, num_t num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads test_scores = evaluate(agent, guide, xs, infos, min_score=min_score, num_threads=num_threads, num_samples=num_samples, description=description) - # Filter out None values and convert to list to ensure proper filtering - valid_scores = [s for s in test_scores.flatten() if s is not None] - if len(valid_scores) == 0: - raise ValueError("All scores are None.") - return np.mean(valid_scores) + return safe_mean(test_scores) def save(self, save_path): print(f"Saving algorithm state to {save_path} at iteration {self.n_iters}.") diff --git a/opto/trainer/utils.py b/opto/trainer/utils.py index f395a57c..0406b05d 100644 --- a/opto/trainer/utils.py +++ b/opto/trainer/utils.py @@ -1,12 +1,29 @@ +from typing import List, Optional import asyncio import functools import warnings +import numpy as np from concurrent.futures import ThreadPoolExecutor from tqdm.asyncio import tqdm_asyncio from opto.trace.bundle import ALLOW_EXTERNAL_DEPENDENCIES from opto.trace.modules import Module from opto.trainer.guide import Guide +def safe_mean(x: List[float | None], missing_value=None) -> float | None: + """Compute the mean of a nested list or nd.array of floats or None, returning missing_value (default None) for an empty list. + + Args: + x (List[float | None]): List of floats or None + missing_value (float | None, optional): Value to return if the list is empty or contains only None. Defaults to None. + Returns: + float | None: Mean of the list, or missing_value if the list is empty or contains only None + """ + x = np.array(x) # nd.array + x = x[x != None] # filter out None values + if x.size == 0: + return missing_value + return float(np.mean(x)) + def async_run(runs, args_list = None, kwargs_list = None, max_workers = None, description = None, allow_sequential_run=True): """Run multiple functions in asynchronously. diff --git a/opto/utils/llm.py b/opto/utils/llm.py index 28bdb43f..8cb413e8 100644 --- a/opto/utils/llm.py +++ b/opto/utils/llm.py @@ -163,7 +163,7 @@ class LiteLLM(AbstractModel): """ def __init__(self, model: Union[str, None] = None, reset_freq: Union[int, None] = None, - cache=True) -> None: + cache=True, max_retries=10, base_delay=1.0) -> None: if model is None: model = os.environ.get('TRACE_LITELLM_MODEL') if model is None: @@ -172,11 +172,11 @@ def __init__(self, model: Union[str, None] = None, reset_freq: Union[int, None] self.model_name = model self.cache = cache - factory = lambda: self._factory(self.model_name) # an LLM instance uses a fixed model + factory = lambda: self._factory(self.model_name, max_retries=max_retries, base_delay=base_delay) # an LLM instance uses a fixed model super().__init__(factory, reset_freq) @classmethod - def _factory(cls, model_name: str): + def _factory(cls, model_name: str, max_retries=10, base_delay=1.0): import litellm if model_name.startswith('azure/'): # azure model azure_token_provider_scope = os.environ.get('AZURE_TOKEN_PROVIDER_SCOPE', None) @@ -186,14 +186,14 @@ def _factory(cls, model_name: str): return lambda *args, **kwargs: retry_with_exponential_backoff( lambda: litellm.completion(model_name, *args, azure_ad_token_provider=credential, **kwargs), - max_retries=10, - base_delay=1.0, + max_retries=max_retries, + base_delay=base_delay, operation_name="LiteLLM_completion" ) return lambda *args, **kwargs: retry_with_exponential_backoff( lambda: litellm.completion(model_name, *args, **kwargs), - max_retries=10, - base_delay=1.0, + max_retries=max_retries, + base_delay=base_delay, operation_name="LiteLLM_completion" ) From aedd8d47fe0ac52490381d8dbcbb745ea4cb64ff Mon Sep 17 00:00:00 2001 From: chinganc Date: Wed, 17 Sep 2025 19:44:56 +0000 Subject: [PATCH 249/314] Add a missed change. --- opto/features/priority_search/priority_search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index cda7236c..7239d7bd 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -6,7 +6,7 @@ from opto import trace from opto.trace.nodes import ParameterNode from opto.optimizers.optimizer import Optimizer -from opto.trainer.utils import async_run +from opto.trainer.utils import async_run, safe_mean from opto.trainer.algorithms.basic_algorithms import batchify from opto.features.priority_search.search_template import SearchTemplate, Samples, BatchRollout, save_train_config from opto.features.priority_search.utils import set_module_parameters, remap_update_dict, create_module_from_update_dict, is_module_copy From 686abeb7817be2a1df2c7739fee624979d0a4113 Mon Sep 17 00:00:00 2001 From: chinganc Date: Wed, 17 Sep 2025 20:12:14 +0000 Subject: [PATCH 250/314] Fix test_priority_search due renaming memory_size to long_term_memory_size --- tests/unit_tests/test_priority_search.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/unit_tests/test_priority_search.py b/tests/unit_tests/test_priority_search.py index f83de3f6..abfabeed 100644 --- a/tests/unit_tests/test_priority_search.py +++ b/tests/unit_tests/test_priority_search.py @@ -54,7 +54,7 @@ def forward(self, x): num_proposals = 10 num_candidates = 5 -memory_size = 3 +long_term_memory_size = 3 suggested_value = 5 @@ -107,7 +107,7 @@ def explore(self, **kwargs): assert isinstance(info_dict, dict) if self.n_iters == 0: # NOTE use +1 since we hacked exploit above using deepcopy, the returned object does not have the same reference - assert len(candidates) == min(memory_size, num_candidates) + 1, f"Expected {min(memory_size, num_candidates) + 1} candidates, got {len(candidates)}" + assert len(candidates) == min(long_term_memory_size, num_candidates) + 1, f"Expected {min(long_term_memory_size, num_candidates) + 1} candidates, got {len(candidates)}" # one from the init parameter and one from the hacked best candidate else: assert len(candidates) <= self.num_candidates, f"Expect no more than {self.num_candidates} candidates at iter {self.n_iters}, got {len(candidates)}" @@ -162,7 +162,7 @@ def test_priority_search(): num_threads=num_threads, num_candidates=num_candidates, num_proposals=num_proposals, - memory_size=memory_size, + long_term_memory_size=long_term_memory_size, num_epochs=num_epochs, verbose=False, #'output', ) @@ -202,7 +202,7 @@ def test_resume(): num_threads=num_threads, num_candidates=num_candidates, num_proposals=num_proposals, - memory_size=memory_size, + long_term_memory_size=long_term_memory_size, verbose=False, #'output', save_path=save_path, save_frequency=1, @@ -241,7 +241,7 @@ def test_trainer_train_and_resume(): num_threads=num_threads, num_candidates=num_candidates, num_proposals=num_proposals, - memory_size=memory_size, + long_term_memory_size=long_term_memory_size, verbose=False, #'output', save_path="./test_priority_search_save_trainer", save_frequency=1, From 1139cddd75fc948c1b70c8299c9678c4b4ba4891 Mon Sep 17 00:00:00 2001 From: chinganc Date: Wed, 17 Sep 2025 20:53:33 +0000 Subject: [PATCH 251/314] Add compress_candidate_memory method to PrioritySearch --- examples/priority_search_on_convex_fn.py | 2 +- .../priority_search/priority_search.py | 67 ++++++++++++++----- .../priority_search_with_regressor.py | 14 ++-- tests/unit_tests/test_priority_search.py | 12 ++++ 4 files changed, 69 insertions(+), 26 deletions(-) diff --git a/examples/priority_search_on_convex_fn.py b/examples/priority_search_on_convex_fn.py index f568e11b..8b807e3d 100644 --- a/examples/priority_search_on_convex_fn.py +++ b/examples/priority_search_on_convex_fn.py @@ -255,7 +255,7 @@ def get_feedback(self, query: str, response: str, reference=None, **kwargs) -> T guide=guide, num_candidates=4, num_proposals=2, - short_term_memory_duration=2, + memory_update_frequency=2, optimizer_kwargs={'objective':"You have a task of guessing two numbers. You should make sure your guess minimizes y.", 'memory_size': 10} ) \ No newline at end of file diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index 7239d7bd..b7e12ae7 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -2,7 +2,7 @@ import copy import heapq import time -from typing import Union, List, Tuple, Dict, Any, Optional +from typing import Union, List, Tuple, Dict, Any, Optional, Callable from opto import trace from opto.trace.nodes import ParameterNode from opto.optimizers.optimizer import Optimizer @@ -166,13 +166,15 @@ class HeapMemory: # Later on this will be replaced by a memory DB. # NOTE that the heap memory is a max-heap, so we store negative scores to use the default min-heap behavior of heapq. - def __init__(self, size=None): + def __init__(self, size=None, processing_fun: Callable = None): """ Initialize an empty heap memory. """ self.memory = [] self._size = size # Optional size limit for the heap memory + self.processing_fun = processing_fun def push(self, score, data): """ Push an item to the heap memory. """ + data = self.processing_fun(data) if self.processing_fun is not None else data heapq.heappush(self.memory, (-score, data)) if len(self.memory) > self.size: # NOTE a heuristic for now @@ -284,7 +286,7 @@ def train(self, use_best_candidate_to_explore: bool = True, # whether to use the best candidate as part of the exploration candidates long_term_memory_size: Optional[int] = None, # size of the long-term heap memory to store the candidates; if None, no limit is set short_term_memory_size: Optional[int] = None, # size of the short-term memory to store the most recent candidates; if None, no limit is set - short_term_memory_duration: Optional[int] = 0, # number of iterations to keep the candidates in the short-term memory before merging them into the long-term memory. 0 means only long-term memory is used. + memory_update_frequency: Optional[int | None] = 0, # number of iterations to keep the candidates in the short-term memory before merging them into the long-term memory. 0 means only long-term memory is used. None means only short-term memory is used. score_function: str = 'mean', # function to compute the score for the candidates; 'mean' or 'ucb' ucb_exploration_constant: float = 1.0, # exploration constant for UCB score function # Additional keyword arguments @@ -313,9 +315,9 @@ def train(self, num_proposals (int, optional): The number of proposals to generate per optimizer. Defaults to 1. validate_exploration_candidates (bool, optional): Whether to validate the proposed parameters for exploration. Defaults to True. use_best_candidate_to_explore (bool, optional): Whether to use the best candidate as part of the exploration candidates. Defaults to True. - memory_size (int, optional): The size of the heap memory to store the candidates. If None, no limit is set. Defaults to None. - short_term_memory_size (int, optional): The size of the short-term memory to store the most recent candidates. If None, no limit is set. Defaults to None. - short_term_memory_duration (int, optional): The number of iterations to keep the candidates in the short-term memory before merging them into the long-term memory. Defaults to 0. + long_term_memory_size (int, optional): The size of the heap memory to store the candidates. If None, no limit is set. Defaults to None. long-term memory stores only feedback and score. + short_term_memory_size (int, optional): The size of the short-term memory to store the most recent candidates. If None, no limit is set. Defaults to None. short-term memory stores full rollout information. + memory_update_frequency (int, optional): The number of iterations to keep the candidates in the short-term memory before merging them into the long-term memory. Defaults to 0, which means only long-term memory is used. None means only short-term memory is used. score_function (str, optional): The function to compute the score for the candidates; 'mean' or 'ucb'. Defaults to 'mean'. ucb_exploration_constant (float, optional): The exploration constant for UCB score function. Defaults to 1.0. **kwargs: Additional keyword arguments that may be used by the implementation. @@ -332,7 +334,7 @@ def train(self, ucb_exploration_constant=ucb_exploration_constant, long_term_memory_size=long_term_memory_size, short_term_memory_size=short_term_memory_size, - short_term_memory_duration=short_term_memory_duration + memory_update_frequency=memory_update_frequency ) super().train(guide=guide, @@ -363,7 +365,7 @@ def _initialize_search_parameters(self, ucb_exploration_constant, long_term_memory_size, short_term_memory_size, - short_term_memory_duration): + memory_update_frequency): """Initialize search parameters and memory structures. Args: @@ -376,7 +378,7 @@ def _initialize_search_parameters(self, ucb_exploration_constant (float): Exploration constant for UCB score function long_term_memory_size (int): Size of the long-term heap memory short_term_memory_size (int): Size of the short-term memory - short_term_memory_duration (int): Duration to keep candidates in short-term memory + memory_update_frequency (int): The candidates are merged into long-term memory after this many iterations. """ # Validate and adjust num_candidates based on number of optimizers if num_candidates < len(self._optimizers): @@ -406,10 +408,20 @@ def _initialize_search_parameters(self, self._best_candidate_priority = None # Initialize memory structures - self.long_term_memory = HeapMemory(size=long_term_memory_size) - self.short_term_memory = HeapMemory(size=short_term_memory_size) - self.short_term_memory_duration = short_term_memory_duration + if memory_update_frequency is None: + print("PrioritySearch initialized with only short-term memory.") + assert short_term_memory_size is None or short_term_memory_size > 0, \ + "short_term_memory_size must be None or greater than 0 when memory_update_frequency is None." + elif memory_update_frequency == 0: + print("PrioritySearch initialized with only long-term memory.") + assert long_term_memory_size is None or long_term_memory_size > 0, \ + "long_term_memory_size must be None or greater than 0 when memory_update_frequency is 0." + else: + print(f"PrioritySearch initialized with both short-term and long-term memory. Candidates will be merged into long-term memory every {memory_update_frequency} iterations.") + self.long_term_memory = HeapMemory(size=long_term_memory_size, processing_fun=self.compress_candidate_memory) + self.short_term_memory = HeapMemory(size=short_term_memory_size) + self.memory_update_frequency = memory_update_frequency def update(self, samples: Union[Samples, None] = None, @@ -453,15 +465,19 @@ def update(self, @property def memory(self): - if self.short_term_memory.size == 0 or self.short_term_memory_duration == 0: + """ Return the current memory (long-term or short-term) based on the memory update frequency. """ + if self.memory_update_frequency is None: + return self.short_term_memory + # memory_update_frequency is finite + if self.memory_update_frequency == 0 or self.short_term_memory.size == 0: return self.long_term_memory - # short_term_memory is finite and non-zero - if self.n_iters % self.short_term_memory_duration == 0: + # short_term_memory is non-zero and memory_update_frequency is positive + if self.n_iters % self.memory_update_frequency == 0: # merge the the short-term memory into the long-term memory if len(self.short_term_memory) > 0: + print('Merging short-term memory into long-term memory of PrioritySearch.') self.long_term_memory.append(self.short_term_memory) self.short_term_memory.reset() - print('Merging short-term memory into long-term memory of PrioritySearch.') return self.long_term_memory else: return self.short_term_memory @@ -730,8 +746,8 @@ def exploit(self, verbose: bool = False, **kwargs) -> Tuple[ModuleCandidate, Dic } # TODO refactor below to reuse scoring + # NOTE This function can be overridden by subclasses to compute a different score def compute_exploitation_priority(self, candidate) -> float: - # NOTE This function can be overridden by subclasses to compute a different score """ Compute the score for the candidate based on the rollouts during the validation phase. It can be overridden by subclasses to implement a different scoring strategy. @@ -745,8 +761,8 @@ def compute_exploitation_priority(self, candidate) -> float: # By default, we compute the mean score of the rollouts return candidate.mean_score() + # NOTE This function can be overridden by subclasses to compute a different score def compute_exploration_priority(self, candidate) -> float: - # NOTE This function can be overridden by subclasses to compute a different score """ Compute the score for the candidate based on the rollouts during the validation phase. It can be overridden by subclasses to implement a different scoring strategy. @@ -775,3 +791,18 @@ def compute_exploration_priority(self, candidate) -> float: return ucb_score # return the UCB score else: raise ValueError(f"Unknown score function: {self.score_function}") + + # NOTE This function can be overridden by subclasses to compute a different score + def compress_candidate_memory(self, candidate: ModuleCandidate) -> ModuleCandidate: + """ Compress the memory of the candidate to save space. This is used to preprocess candidates before adding them to long-term memory. + By default, we save only the feedback and score of each rollout for long-term memory. """ + def _process_rollout(rollout): + # rollout is a dict containing module, x, info, target, score, feedback + for k in rollout: + if k not in ['score', 'feedback']: + rollout[k] = None + candidate = copy.copy(candidate) # make a copy of the candidate to avoid modifying the original one + candidate.rollouts = copy.deepcopy(candidate.rollouts) # deep copy the rollouts to avoid modifying the original one + for rollout in candidate.rollouts: + _process_rollout(rollout) + return candidate diff --git a/opto/features/priority_search/priority_search_with_regressor.py b/opto/features/priority_search/priority_search_with_regressor.py index b985f7e3..eb6d9f21 100644 --- a/opto/features/priority_search/priority_search_with_regressor.py +++ b/opto/features/priority_search/priority_search_with_regressor.py @@ -7,10 +7,10 @@ import heapq class PrioritySearch_with_Regressor(PrioritySearch): - """ + """ A subclass of PrioritySearch that uses a regressor to predict the scores of the candidates. """ - + def train(self, guide, # guide to provide feedback train_dataset, # dataset of (x, info) pairs to train the agent @@ -40,7 +40,7 @@ def train(self, use_best_candidate_to_explore: bool = True, # whether to use the best candidate as part of the exploration candidates memory_size: Optional[int] = None, # size of the long-term heap memory to store the candidates; if None, no limit is set short_term_memory_size: Optional[int] = None, # size of the short-term memory to store the most recent candidates; if None, no limit is set - short_term_memory_duration: Optional[int] = 0, # number of iterations to keep the candidates in the short-term memory before merging them into the long-term memory. 0 means only long-term memory is used. + memory_update_frequency: Optional[int] = 0, # number of iterations to keep the candidates in the short-term memory before merging them into the long-term memory. 0 means only long-term memory is used. score_function: str = 'mean', # function to compute the score for the candidates; 'mean' or 'ucb' ucb_exploration_constant: float = 1.0, # exploration constant for UCB score function # Regressor specific parameters @@ -77,9 +77,9 @@ def train(self, ucb_exploration_constant=ucb_exploration_constant, memory_size=memory_size, short_term_memory_size=short_term_memory_size, - short_term_memory_duration=short_term_memory_duration + memory_update_frequency=memory_update_frequency ) - + # Initialize the regressor with the long-term memory and custom parameters - this is the only difference from parent class self.regressor = ModuleCandidateRegressor( memory=self.long_term_memory, @@ -129,7 +129,7 @@ def update(self, while len(self.memory) < min(max_mem_size, self.num_candidates): self.memory.push(self.max_score, ModuleCandidate(self.agent, optimizer=self.optimizer)) # Push the base agent as the first candidate (This gives the initialization of the priority queue) - + self.update_memory_with_regressor(verbose=verbose, **kwargs) # TODO Log information about the update @@ -179,7 +179,7 @@ def update_memory(self, validate_results, verbose: bool = False, **kwargs): """ print("--- Updating memory with validation results...") if verbose else None for candidate, rollouts in validate_results.items(): - candidate.add_rollouts(rollouts) # add the rollouts to the + candidate.add_rollouts(rollouts) # add the rollouts to the placeholder_priority = self.max_score self.memory.push(placeholder_priority, candidate) diff --git a/tests/unit_tests/test_priority_search.py b/tests/unit_tests/test_priority_search.py index abfabeed..41dd103b 100644 --- a/tests/unit_tests/test_priority_search.py +++ b/tests/unit_tests/test_priority_search.py @@ -55,6 +55,7 @@ def forward(self, x): num_proposals = 10 num_candidates = 5 long_term_memory_size = 3 +memory_update_frequency = 2 suggested_value = 5 @@ -86,6 +87,14 @@ def validate(self, candidates, samples, verbose=False, **kwargs): def exploit(self, **kwargs): print("[UnitTest] Exploit at iteration:", self.n_iters) + # if self.memory is self.long_term_memory and self.n_iters > 0: + # for _, c in self.long_term_memory.memory: + # for rollout in c.rollouts: + # assert rollout['x'] is None + # assert rollout['info'] is None + # assert rollout['target'] is None + # assert rollout['module'] is None + candidate, priority, info_dict = super().exploit(**kwargs) assert isinstance(candidate, ModuleCandidate), "Expected candidate to be an instance of ModuleCandidate" assert isinstance(info_dict, dict), "Expected info_dict to be a dictionary" @@ -163,6 +172,7 @@ def test_priority_search(): num_candidates=num_candidates, num_proposals=num_proposals, long_term_memory_size=long_term_memory_size, + memory_update_frequency=memory_update_frequency, num_epochs=num_epochs, verbose=False, #'output', ) @@ -203,6 +213,7 @@ def test_resume(): num_candidates=num_candidates, num_proposals=num_proposals, long_term_memory_size=long_term_memory_size, + memory_update_frequency=memory_update_frequency, verbose=False, #'output', save_path=save_path, save_frequency=1, @@ -242,6 +253,7 @@ def test_trainer_train_and_resume(): num_candidates=num_candidates, num_proposals=num_proposals, long_term_memory_size=long_term_memory_size, + memory_update_frequency=memory_update_frequency, verbose=False, #'output', save_path="./test_priority_search_save_trainer", save_frequency=1, From de3d686875a7dadd904a5200bfc99dccca549cfb Mon Sep 17 00:00:00 2001 From: chinganc Date: Wed, 17 Sep 2025 21:40:04 +0000 Subject: [PATCH 252/314] Change to saving only score in the long term memory. --- opto/features/priority_search/priority_search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index b7e12ae7..9542b238 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -799,7 +799,7 @@ def compress_candidate_memory(self, candidate: ModuleCandidate) -> ModuleCandida def _process_rollout(rollout): # rollout is a dict containing module, x, info, target, score, feedback for k in rollout: - if k not in ['score', 'feedback']: + if k not in ['score']: rollout[k] = None candidate = copy.copy(candidate) # make a copy of the candidate to avoid modifying the original one candidate.rollouts = copy.deepcopy(candidate.rollouts) # deep copy the rollouts to avoid modifying the original one From 2d62d8997e668f373bd712581a323a93c75a679f Mon Sep 17 00:00:00 2001 From: chinganc Date: Wed, 17 Sep 2025 23:34:30 +0000 Subject: [PATCH 253/314] Update dataloader to circulate through dataset in creating a batch. --- opto/trainer/loader.py | 45 ++++++++---- tests/unit_tests/test_dataloader.py | 91 ++++++++++++++++++++---- tests/unit_tests/test_priority_search.py | 4 +- tests/unit_tests/test_sampler.py | 22 +++--- 4 files changed, 125 insertions(+), 37 deletions(-) diff --git a/opto/trainer/loader.py b/opto/trainer/loader.py index 57ee12ea..f4da05c5 100644 --- a/opto/trainer/loader.py +++ b/opto/trainer/loader.py @@ -24,26 +24,40 @@ def __init__(self, dataset, batch_size=1, randomize=True, replacement=False, shu self.randomize = randomize self.replacement = replacement self.shuffle = shuffle - self._indices = self._update_indices() - self.n_epochs = 0 + self.n_epochs = -1 self._i = 0 + self._indices = [ i for i in range(len(self.dataset['inputs'])) ] + self._exhausted = False + self._start_new_epoch() # self.n_epochs will be set to 0 + + def _start_new_epoch(self): + if self.shuffle: + self._indices = self._update_indices() + self._i = 0 + self.n_epochs += 1 + self._exhausted = False def __iter__(self): return self def __next__(self): - """ Get the next batch of data """ - if self._i >= len(self._indices): - if self.shuffle: - self._indices = self._update_indices() - # Reset the index for the next epoch - self._i = 0 - self.n_epochs += 1 + """Get the next batch of data, always of batch_size. If the dataset is smaller or at the end, the batch will include data from the next epoch after shuffling.""" + self._exhausted = self._exhausted or (self._i >= len(self._indices)) + if self._exhausted: + self._start_new_epoch() raise StopIteration - indices = self._indices[self._i: min(self._i + self.batch_size, len(self._indices))] - xs = [self.dataset['inputs'][ind] for ind in indices] - infos = [self.dataset['infos'][ind] for ind in indices] - self._i += self.batch_size + xs = [] + infos = [] + while len(xs) < self.batch_size: + if self._i >= len(self._indices): + self._start_new_epoch() + self._exhausted = True # Mark as exhausted to stop further sampling in this epoch + remaining = self.batch_size - len(xs) + end = min(self._i + remaining, len(self._indices)) + indices = self._indices[self._i:end] + xs.extend([self.dataset['inputs'][ind] for ind in indices]) + infos.extend([self.dataset['infos'][ind] for ind in indices]) + self._i += len(indices) return xs, infos def _update_indices(self): @@ -57,9 +71,10 @@ def sample(self): """ Sample a batch of data from the dataset """ try: xs, infos = next(self) - return xs, infos except StopIteration: - return self.sample() + xs, infos = self.sample() # make sure to get a batch after resetting + self._exhausted = False # calling next() again should not raise StopIteration immediately + return xs, infos def __getstate__(self): """Get the state of the dataset for pickling.""" diff --git a/tests/unit_tests/test_dataloader.py b/tests/unit_tests/test_dataloader.py index 8d4db810..9d73b2e8 100644 --- a/tests/unit_tests/test_dataloader.py +++ b/tests/unit_tests/test_dataloader.py @@ -4,6 +4,8 @@ def run_for_loop(dataloader): print('Running for-loop') + + counter = 0 for i, (inputs, infos) in enumerate(dataloader): print(f"Inputs: {inputs}, Infos: {infos}") @@ -15,10 +17,58 @@ def run_for_loop(dataloader): assert inputs == [3, 4], f"Second batch should contain inputs 3 and 4. Get: {inputs}" assert infos == ['c', 'd'], f"Second batch should contain infos 'c' and 'd'. Get: {infos}" elif i == 2: - assert inputs == [5], f"Third batch should contain input 5. Get: {inputs}" - assert infos == ['e'], f"Third batch should contain info 'e'. Get: {infos}" + assert inputs == [5, 1], f"Third batch should contain inputs 5 and 1. Get: {inputs}" + assert infos == ['e', 'a'], f"Third batch should contain infos 'e' and 'a'. Get: {infos}" + counter += 1 + + assert counter == 3, f"Should have 3 batches in total. Get: {counter}" + + # Make sure it can be iterated multiple times + counter = 0 + for i, (inputs, infos) in enumerate(dataloader): + + print(f"Inputs: {inputs}, Infos: {infos}") + + if i == 0: + assert inputs == [1, 2], f"First batch should contain inputs 1 and 2. Get: {inputs}" + assert infos == ['a', 'b'], f"First batch should contain infos 'a' and 'b'. Get: {infos}" + elif i == 1: + assert inputs == [3, 4], f"Second batch should contain inputs 3 and 4. Get: {inputs}" + assert infos == ['c', 'd'], f"Second batch should contain infos 'c' and 'd'. Get: {infos}" + elif i == 2: + assert inputs == [5, 1], f"Third batch should contain inputs 5 and 1. Get: {inputs}" + assert infos == ['e', 'a'], f"Third batch should contain infos 'e' and 'a'. Get: {infos}" + counter += 1 + + assert counter == 3, f"Should have 3 batches in total. Get: {counter}" + def run_next(dataloader): + + inputs, infos = next(dataloader) + print('Running next()') + print(f"Inputs: {inputs}, Infos: {infos}") + + assert inputs == [1, 2], f"First batch should contain inputs 1 and 2. Get: {inputs}" + assert infos == ['a', 'b'], f"First batch should contain infos 'a' and 'b'. Get: {infos}" + + inputs, infos = next(dataloader) + print(f"Inputs: {inputs}, Infos: {infos}") + + assert inputs == [3, 4], f"Second batch should contain inputs 3 and 4. Get: {inputs}" + assert infos == ['c', 'd'], f"Second batch should contain infos 'c' and 'd'. Get: {infos}" + + inputs, infos = next(dataloader) + print(f"Inputs: {inputs}, Infos: {infos}") + + assert inputs == [5, 1], f"Third batch should contain inputs 5 and 1. Get: {inputs}" + assert infos == ['e', 'a'], f"Third batch should contain infos 'e' and 'a'. Get: {infos}" + + try: + next(dataloader) + except StopIteration: + print("No more data to iterate over, as expected.") + inputs, infos = next(dataloader) print('Running next()') print(f"Inputs: {inputs}, Infos: {infos}") @@ -35,26 +85,46 @@ def run_next(dataloader): inputs, infos = next(dataloader) print(f"Inputs: {inputs}, Infos: {infos}") - assert inputs == [5], f"Third batch should contain input 5. Get: {inputs}" - assert infos == ['e'], f"Third batch should contain info 'e'. Get: {infos}" + assert inputs == [5, 1], f"Third batch should contain inputs 5 and 1. Get: {inputs}" + assert infos == ['e', 'a'], f"Third batch should contain infos 'e' and 'a'. Get: {infos}" try: next(dataloader) except StopIteration: print("No more data to iterate over, as expected.") + def run_sample(dataloader): print('Running sample()') inputs, infos = dataloader.sample() assert inputs == [1, 2], f"First sample should contain inputs 1 and 2. Get: {inputs}" assert infos == ['a', 'b'], f"First sample should contain infos 'a' and 'b'. Get: {infos}" + assert dataloader._exhausted == False, "Dataloader should be marked as exhausted after sampling all data." inputs, infos = dataloader.sample() assert inputs == [3, 4], f"Second sample should contain inputs 3 and 4. Get: {inputs}" assert infos == ['c', 'd'], f"Second sample should contain infos 'c' and 'd'. Get: {infos}" + assert dataloader._exhausted == False, "Dataloader should be marked as exhausted after sampling all data." inputs, infos = dataloader.sample() - assert inputs == [5], f"Third sample should contain input 5. Get: {inputs}" - assert infos == ['e'], f"Third sample should contain info 'e'. Get: {infos}" + assert inputs == [5, 1], f"Third sample should contain inputs 5 and 1. Get: {inputs}" + assert infos == ['e', 'a'], f"Third sample should contain infos 'e' and 'a'. Get: {infos}" + assert dataloader._exhausted == False, "Dataloader should be marked as exhausted after sampling all data." + + print('Calling sample does not exhaust the dataloader.') + + # check that it can be sampled again + inputs, infos = dataloader.sample() + assert inputs == [2, 3], f"First sample should contain inputs 2 and 3. Get: {inputs}" + assert infos == ['b', 'c'], f"First sample should contain infos 'b' and 'c'. Get: {infos}" + assert dataloader._exhausted == False, "Dataloader should be marked as exhausted after sampling all data." + inputs, infos = dataloader.sample() + assert inputs == [4, 5], f"Second sample should contain inputs 4 and 5. Get: {inputs}" + assert infos == ['d', 'e'], f"Second sample should contain infos 'd' and 'e'. Get: {infos}" + assert dataloader._exhausted == False, "Dataloader should be marked as exhausted after sampling all data." + inputs, infos = dataloader.sample() + assert inputs == [1, 2], f"Third sample should contain inputs 1 and 2. Get: {inputs}" + assert infos == ['a', 'b'], f"Third sample should contain infos 'a' and 'b'. Get: {infos}" + assert dataloader._exhausted == False, "Dataloader should be marked as exhausted after sampling all data." # At this point, the dataloader should be reset. No need to catch StopIteration when calling sample again @@ -68,24 +138,21 @@ def test_dataloader(): # Test for-loop usage run_for_loop(dataloader) - run_for_loop(dataloader) # make sure it can be iterated multiple times # Test next() usage run_next(dataloader) - run_next(dataloader) # make sure it can be called multiple times # Test sample() method run_sample(dataloader) - run_sample(dataloader) # make sure it can be called multiple times + + dataloader._start_new_epoch() # Manually start a new epoch to reset + print("Manually started a new epoch.") # Test for-loop usage run_for_loop(dataloader) - run_for_loop(dataloader) # make sure it can be iterated multiple times # Test next() usage run_next(dataloader) - run_next(dataloader) # make sure it can be called multiple times # Test sample() method run_sample(dataloader) - run_sample(dataloader) # make sure it can be called multiple times \ No newline at end of file diff --git a/tests/unit_tests/test_priority_search.py b/tests/unit_tests/test_priority_search.py index 41dd103b..21abb5d8 100644 --- a/tests/unit_tests/test_priority_search.py +++ b/tests/unit_tests/test_priority_search.py @@ -227,8 +227,8 @@ def test_resume(): model=new_agent, train_dataset=dataset, num_epochs=num_epochs+2) - assert new_algo.n_iters == num_epochs+2, "Resumed algorithm should have completed the additional epochs." - + print("Resumed training for additional epochs.") + # assert new_algo.n_iters == num_epochs+2, "Resumed algorithm should have completed the additional epochs." os.system(f"rm -rf {save_path}") diff --git a/tests/unit_tests/test_sampler.py b/tests/unit_tests/test_sampler.py index a5ff434e..a2457b35 100644 --- a/tests/unit_tests/test_sampler.py +++ b/tests/unit_tests/test_sampler.py @@ -73,10 +73,14 @@ def test_sample_with_single_agent(): samples, batch = sampler.sample([Agent()]) # check batch is equal to dataset's second batch_size elements - assert batch['inputs'] == dataset['inputs'][3:] - assert batch['infos'] == dataset['infos'][3:] - assert len(samples) == 1 + assert batch['inputs'] == dataset['inputs'][3:] + dataset['inputs'][:1] + assert batch['infos'] == dataset['infos'][3:] + dataset['infos'][:1] + + # a batch of 3 is split into 2 sub-batches of size 2 and 1 + assert len(samples) == 2 assert len(samples[0].rollouts) == 2 + assert len(samples[1].rollouts) == 1 + for rollouts in samples: for rollout in rollouts: @@ -124,13 +128,15 @@ def test_sample_with_multiple_agents(): samples, batch = sampler.sample([Agent(), Agent()]) # check batch is equal to dataset's second batch_size elements - assert batch['inputs'] == dataset['inputs'][3:] - assert batch['infos'] == dataset['infos'][3:] - - assert len(samples) == 2, f"Expected 2 samples, got {len(samples)}" + assert batch['inputs'] == dataset['inputs'][3:] + dataset['inputs'][:1] + assert batch['infos'] == dataset['infos'][3:] + dataset['infos'][:1] + # a batch of 3 is split into 2 sub-batches of size 2 and 1 + assert len(samples) == 4 assert len(samples[0].rollouts) == 2 - assert len(samples[1].rollouts) == 2 + assert len(samples[1].rollouts) == 1 + assert len(samples[2].rollouts) == 2 + assert len(samples[3].rollouts) == 1 for rollouts in samples: for rollout in rollouts: From 51cf450f8a4d30f887606284acd0e8e05248b304 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 18 Sep 2025 06:10:31 +0000 Subject: [PATCH 254/314] Fix a bug copy.copy Model --- opto/trace/modules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opto/trace/modules.py b/opto/trace/modules.py index 56df3ca9..ecf96c51 100644 --- a/opto/trace/modules.py +++ b/opto/trace/modules.py @@ -292,7 +292,7 @@ def __setstate__(self, state): parameters_dict = state['parameters_dict'] non_parameters_dict = state['non_parameters_dict'] self._set(parameters_dict) - # self.__dict__.update(non_parameters_dict) + self.__dict__.update(non_parameters_dict) def save(self, file_name: str): directory = os.path.dirname(file_name) From 15767f9b33353af3bab546cfb9648f23ee95393b Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 18 Sep 2025 07:10:22 +0000 Subject: [PATCH 255/314] Fix the bug triggered when multiple inputs to MessageNode are the same Node --- opto/trace/nodes.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/opto/trace/nodes.py b/opto/trace/nodes.py index cac02446..eac16ebe 100644 --- a/opto/trace/nodes.py +++ b/opto/trace/nodes.py @@ -2088,6 +2088,9 @@ def __init__( # Add parents if we are tracing for k, v in self._inputs.items(): assert isinstance(v, Node), f"Input {k} is not a Node." + unique_inputs = set(self._inputs.values()) + # NOTE this handles the case where the same node is passed multiple times as input + for v in unique_inputs: self._add_parent(v) self._add_dependencies( v From 4e9373da13260b51e6d5fe7cbb46dc768afe1e43 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 18 Sep 2025 07:33:12 +0000 Subject: [PATCH 256/314] Fix the failure of hash check of candidates in long_term_memory due to recently added compress_candidate_memory. --- examples/priority_search_on_convex_fn.py | 2 +- opto/features/priority_search/priority_search.py | 15 ++++++++------- tests/unit_tests/test_priority_search.py | 14 +++++++------- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/examples/priority_search_on_convex_fn.py b/examples/priority_search_on_convex_fn.py index 8b807e3d..1f68064a 100644 --- a/examples/priority_search_on_convex_fn.py +++ b/examples/priority_search_on_convex_fn.py @@ -248,7 +248,7 @@ def get_feedback(self, query: str, response: str, reference=None, **kwargs) -> T logger=logger, score_range=[-10, 10], # trainer kwargs - num_epochs=5, + num_epochs=3*4, batch_size=2, # this is just for testing. effectively, this is the same batch_size=1 and num_proposals=4 num_batches=2, verbose=False, #'output', diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index 9542b238..a07db73f 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -570,12 +570,7 @@ def _backward(n): # For each optimizer, containing the backward feedback, we call it n_proposals times to get the proposed parameters. def _step(n): optimizer = optimizers[n] - try: - update_dict = optimizer.step(verbose=verbose, num_threads=self.num_threads, bypassing=True, **kwargs) - except Exception as e: - print(f"Error calling optimizer.step: {e}") - update_dict = None - + update_dict = optimizer.step(verbose=verbose, num_threads=self.num_threads, bypassing=True, **kwargs) if not update_dict: # if the optimizer did not propose any updates return None # return None to indicate no updates were proposed # update_dict may only contain some of the parameters of the agent, we need to make sure it contains all the parameters @@ -801,7 +796,13 @@ def _process_rollout(rollout): for k in rollout: if k not in ['score']: rollout[k] = None - candidate = copy.copy(candidate) # make a copy of the candidate to avoid modifying the original one + def _copy(obj): + # We manually implement a shallow copy, since __getstate__ is overridden in ModuleCandidate. + new_obj = obj.__class__.__new__(obj.__class__) # create a new instance of the same class + new_obj.__dict__.update(obj.__dict__) + return new_obj + + candidate = _copy(candidate) # make a copy of the candidate to avoid modifying the original one candidate.rollouts = copy.deepcopy(candidate.rollouts) # deep copy the rollouts to avoid modifying the original one for rollout in candidate.rollouts: _process_rollout(rollout) diff --git a/tests/unit_tests/test_priority_search.py b/tests/unit_tests/test_priority_search.py index 21abb5d8..f63d83cb 100644 --- a/tests/unit_tests/test_priority_search.py +++ b/tests/unit_tests/test_priority_search.py @@ -87,13 +87,13 @@ def validate(self, candidates, samples, verbose=False, **kwargs): def exploit(self, **kwargs): print("[UnitTest] Exploit at iteration:", self.n_iters) - # if self.memory is self.long_term_memory and self.n_iters > 0: - # for _, c in self.long_term_memory.memory: - # for rollout in c.rollouts: - # assert rollout['x'] is None - # assert rollout['info'] is None - # assert rollout['target'] is None - # assert rollout['module'] is None + if self.n_iters == 0: + memory = self.memory.memory + _candidates = [c for _, c in memory] + # assert all candidates have the same hash, since they are all the same in this unit test + hashes = [hash(c) for c in _candidates] + assert len(hashes) > 1, "Expected more than one candidate in memory" + assert 1 == len(set(hashes)), "All candidates in memory should have the same hash" candidate, priority, info_dict = super().exploit(**kwargs) assert isinstance(candidate, ModuleCandidate), "Expected candidate to be an instance of ModuleCandidate" From e3b3b4dc2ca5adf7fd591ca87708acd1f640b29a Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 18 Sep 2025 07:41:14 +0000 Subject: [PATCH 257/314] Add self._enforce_using_data_collecting_candidates --- opto/features/priority_search/priority_search.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index a07db73f..298e159e 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -337,6 +337,10 @@ def train(self, memory_update_frequency=memory_update_frequency ) + self._enforce_using_data_collecting_candidates = True + # enforce only data collecting candidates are used in in calling match_candidates_and_samples + # this attribute is purposefully designed to be only modified by subclasses, not through input arguments. + super().train(guide=guide, train_dataset=train_dataset, validate_dataset=validate_dataset, @@ -670,9 +674,11 @@ def match_candidates_and_samples( raise ValueError(f"ModuleCandidate with id {key} not found in results. Samples are not collected by known candidates.") # Append the rollouts to the list of rollouts for the key _results[ids[key]].append(rollouts) - # assert all candidates have at least one rollout - for c in candidates: - assert len(_results[c]) > 0, f"ModuleCandidate with id {id(c)} has no rollouts. Samples are not collected by known candidates." + + if self._enforce_using_data_collecting_candidates: + # assert all candidates have at least one rollout + for c in candidates: + assert len(_results[c]) > 0, f"ModuleCandidate with id {id(c)} has no rollouts. Samples are not collected by known candidates." return _results From 08be462f099eeee15d60860ec50d2d35d391b342 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 18 Sep 2025 08:03:40 +0000 Subject: [PATCH 258/314] Fix the bug of the incorrect fix in 15767f9b33353af3bab546cfb9648f23ee95393b --- opto/trace/nodes.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/opto/trace/nodes.py b/opto/trace/nodes.py index eac16ebe..2b1941b9 100644 --- a/opto/trace/nodes.py +++ b/opto/trace/nodes.py @@ -2088,9 +2088,11 @@ def __init__( # Add parents if we are tracing for k, v in self._inputs.items(): assert isinstance(v, Node), f"Input {k} is not a Node." - unique_inputs = set(self._inputs.values()) + # cannot use set(self._inputs.values()) to create unique inputs because __equal__ is overloaded + # need to use the actual object identity + unique_inputs = {id(v): v for v in self._inputs.values()} # NOTE this handles the case where the same node is passed multiple times as input - for v in unique_inputs: + for v in unique_inputs.values(): self._add_parent(v) self._add_dependencies( v From 7e409472d0a89b8826dceaf99f0abea7ddb27386 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 18 Sep 2025 21:33:42 +0000 Subject: [PATCH 259/314] Fix the bug the bundle does not work correctly with copy or deepcopy. Add a check to not allow applying functool on trainable bundle. --- opto/trace/bundle.py | 12 +++ opto/trace/containers.py | 28 +++++- tests/unit_tests/test_bundle.py | 167 +++++++++++++++++++++++++++++++- 3 files changed, 201 insertions(+), 6 deletions(-) diff --git a/opto/trace/bundle.py b/opto/trace/bundle.py index 6d5fa5c8..4589d518 100644 --- a/opto/trace/bundle.py +++ b/opto/trace/bundle.py @@ -647,6 +647,18 @@ def __get__(self, obj, db_type): def detach(self): return copy.deepcopy(self) + def __deepcopy__(self, memo): + # deepcopy everything except for _ldict + cls = self.__class__ + result = cls.__new__(cls) + memo[id(self)] = result + for k, v in self.__dict__.items(): + if k == "_ldict": + setattr(result, k, v.copy()) # reset _ldict + else: + setattr(result, k, copy.deepcopy(v, memo)) + return result + def generate_comment( self, code: str, diff --git a/opto/trace/containers.py b/opto/trace/containers.py index f375ab47..819b568a 100644 --- a/opto/trace/containers.py +++ b/opto/trace/containers.py @@ -50,8 +50,17 @@ def parameters_dict(self): continue if isinstance(attr, functools.partial): # this is a class method - method = attr.func.__self__ + if hasattr(attr.func, '__self__'): + method = attr.func.__self__ + cls_method = True + else: + method = attr.func + cls_method = False if trainable_method(method): + if not cls_method: + raise ValueError( + "A trainable method cannot be a wrapped by functools.partial." + ) parameters[name] = method.parameter elif isinstance(attr, FunModule): # when a bundle method is not trainable @@ -82,11 +91,22 @@ def copy(self): # Set the parameters to the original ones for name, attr in inspect.getmembers(self): - if isinstance(attr, functools.partial): # this is a class method - method = attr.func.__self__ + if isinstance(attr, functools.partial): # this may be a class method + if hasattr(attr.func, '__self__'): + cls_method = True + method = attr.func.__self__ + else: + cls_method = False + method = attr.func + if trainable_method(method): new_attr = getattr(new_container, name) - setattr(new_attr.func.__self__, 'parameter', method.parameter) + if cls_method: + setattr(new_attr.func.__self__, 'parameter', method.parameter) + else: + raise ValueError( + "A trainable method cannot be a wrapped by functools.partial." + ) elif trainable_method(attr): # method attribute new_attr = getattr(new_container, name) new_attr.parameter = attr.parameter diff --git a/tests/unit_tests/test_bundle.py b/tests/unit_tests/test_bundle.py index 1b42410e..99adc4ed 100644 --- a/tests/unit_tests/test_bundle.py +++ b/tests/unit_tests/test_bundle.py @@ -2,7 +2,7 @@ from opto.trace.bundle import TraceMissingInputsError from opto.trace.nodes import Node, node from opto.trace.utils import for_all_methods, contain - +import copy global_var = node('This is a global variable') global_list = [1,2,3] @@ -421,10 +421,173 @@ def modify_global_list(): assert len(global_list) == old_len + 1 +def _test_copying(trainable): + # test copying a bundle function + @trace.bundle(trainable=trainable) + def add(a, b): + return a + b + + add2 = copy.copy(add) + assert add2(node(1), node(2)) == 3 + + add3 = copy.deepcopy(add) # module cannot be deepcopied? + assert add3(node(1), node(2)) == 3 + + add3 = add.detach() + assert add3(node(1), node(2)) == 3 + + add4 = add.copy() + assert add4(node(1), node(2)) == 3 + + + new_code = """ +def add(a, b): + return a + b + 1 +""" + + if trainable: + + add.parameter._data = new_code + + add2 = copy.copy(add) + assert add2.parameter._data == new_code + assert add2(node(1), node(2)) == 4 + + add3 = copy.deepcopy(add) # module cannot be deepcopied? + assert add3.parameter._data == new_code + assert add3(node(1), node(2)) == 4 + + add3 = add.detach() + assert add3.parameter._data == new_code + assert add3(node(1), node(2)) == 4 + + add4 = add.copy() + assert add4.parameter._data == new_code + assert add4(node(1), node(2)) == 4 + + + +def _test_module_copying(trainable): + # test copying a trace.Module with bundle-decorated methods + + + from functools import partial + @trace.bundle(trainable=trainable) + def add(a, b): + return a + b + + @trace.bundle() + def add_not_trainable(a, b): + return a + b + + def add_normal(a, b): + return a + b + + @trace.model + class Dummy: + + def __init__(self): + self.partial_add = partial(add, 10) + self.partial_add_not_trainable = partial(add_not_trainable, -3) + self.partial_add_normal = partial(add_normal, -10) + + @trace.bundle(trainable=trainable) + def add(self, a, b): + return a + b + + def forward(self, a, b): + return self.add(a, b) + self.partial_add(a) + self.partial_add_not_trainable(a) + self.partial_add_normal(a) + + dummy = Dummy() + trigger_error = False + + try: + dummy.parameters() + except ValueError as e: + dummy.partial_add = partial(add_not_trainable, 10) + trigger_error = True + if trainable: + assert trigger_error, "This should trigger an error because partial_add is trainable but is wrapped by functools.partial" + assert dummy(node(1), node(2)) == 1+2 + (10+1) + (-3+1) + (-10+1) + assert dummy.add(node(1), node(2)) == 3 + assert dummy.partial_add(node(1)) == 11 + assert dummy.partial_add_normal(1) == -9 + assert dummy.partial_add_not_trainable(node(1)) == -2 + + dummy2 = copy.copy(dummy) + assert dummy2(node(1), node(2)) == 1+2 + (10+1) + (-3+1) + (-10+1) + assert dummy2.add(node(1), node(2)) == 3 + assert dummy2.partial_add(node(1)) == 11 + assert dummy2.partial_add_normal(1) == -9 + assert dummy2.partial_add_not_trainable(node(1)) == -2 + + dummy3 = copy.deepcopy(dummy) + assert dummy3(node(1), node(2)) == 1+2 + (10+1) + (-3+1) + (-10+1) + assert dummy3.add(node(1), node(2)) == 3 + assert dummy3.partial_add(node(1)) == 11 + assert dummy3.partial_add_normal(1) == -9 + assert dummy3.partial_add_not_trainable(node(1)) == -2 + + dummy4 = dummy.copy() + assert dummy4(node(1), node(2)) == 1+2 + (10+1) + (-3+1) + (-10+1) + assert dummy4.add(node(1), node(2)) == 3 + assert dummy4.partial_add(node(1)) == 11 + assert dummy4.partial_add_normal(1) == -9 + assert dummy4.partial_add_not_trainable(node(1)) == -2 + + new_cls_code = """ +def add(self, a, b): + return a + b + 1 +""" + + if trainable: + + dummy.add.parameter._data = new_cls_code + + assert dummy(node(1), node(2)) == 1+2+1 + (10+1) + (-3+1) + (-10+1) + assert dummy.add(node(1), node(2)) == 3+1 + assert dummy.partial_add(node(1)) == 11 + assert dummy.partial_add_normal(1) == -9 + assert dummy.partial_add_not_trainable(node(1)) == -2 + + dummy2 = copy.copy(dummy) + assert dummy2(node(1), node(2)) == 1+2+1 + (10+1) + (-3+1) + (-10+1) + assert dummy2.add(node(1), node(2)) == 3+1 + assert dummy2.partial_add(node(1)) == 11 + assert dummy2.partial_add_normal(1) == -9 + assert dummy2.partial_add_not_trainable(node(1)) == -2 + + dummy3 = copy.deepcopy(dummy) + assert dummy3(node(1), node(2)) == 1+2+1 + (10+1) + (-3+1) + (-10+1) + assert dummy3.add(node(1), node(2)) == 3+1 + assert dummy3.partial_add(node(1)) == 11 + assert dummy3.partial_add_normal(1) == -9 + assert dummy3.partial_add_not_trainable(node(1)) == -2 + + dummy4 = dummy.copy() + assert dummy4(node(1), node(2)) == 1+2+1 + (10+1) + (-3+1) + (-10+1) + assert dummy4.add(node(1), node(2)) == 3+1 + assert dummy4.partial_add(node(1)) == 11 + assert dummy4.partial_add_normal(1) == -9 + assert dummy4.partial_add_not_trainable(node(1)) == -2 + + def test_trainable_FALSE(): print("Running tests with trainable=False") run(trainable=False) def test_trainable_TRUE(): print("Running tests with trainable=True") - run(trainable=True) \ No newline at end of file + run(trainable=True) + +def test_copying(): + print("Running copying tests with trainable=False") + _test_copying(trainable=False) + print("Running copying tests with trainable=True") + _test_copying(trainable=True) + +def test_module_copying(): + print("Running module copying tests with trainable=False") + _test_module_copying(trainable=False) + print("Running module copying tests with trainable=True") + _test_module_copying(trainable=True) \ No newline at end of file From 9d0897a2113842a99a6b3be57bd21f54d3a70c27 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 18 Sep 2025 21:58:40 +0000 Subject: [PATCH 260/314] Disallow all partial+bundle --- opto/trace/containers.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/opto/trace/containers.py b/opto/trace/containers.py index 819b568a..99c4d1e1 100644 --- a/opto/trace/containers.py +++ b/opto/trace/containers.py @@ -57,11 +57,10 @@ def parameters_dict(self): method = attr.func cls_method = False if trainable_method(method): - if not cls_method: - raise ValueError( - "A trainable method cannot be a wrapped by functools.partial." - ) - parameters[name] = method.parameter + raise ValueError( + "A trainable method cannot be a wrapped by functools.partial." + ) + elif isinstance(attr, FunModule): # when a bundle method is not trainable # it shows up as a FunModule attribute @@ -100,13 +99,9 @@ def copy(self): method = attr.func if trainable_method(method): - new_attr = getattr(new_container, name) - if cls_method: - setattr(new_attr.func.__self__, 'parameter', method.parameter) - else: - raise ValueError( - "A trainable method cannot be a wrapped by functools.partial." - ) + raise ValueError( + "A trainable method cannot be a wrapped by functools.partial." + ) elif trainable_method(attr): # method attribute new_attr = getattr(new_container, name) new_attr.parameter = attr.parameter From 8440948414e95f56569314cda195ef00b5bd2af2 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 18 Sep 2025 22:08:54 +0000 Subject: [PATCH 261/314] update broken test --- tests/unit_tests/test_saving_loading.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/unit_tests/test_saving_loading.py b/tests/unit_tests/test_saving_loading.py index 06f09a54..e3cf1b28 100644 --- a/tests/unit_tests/test_saving_loading.py +++ b/tests/unit_tests/test_saving_loading.py @@ -128,7 +128,7 @@ def _llm_callable(messages, **kwargs): ) agent.param._data = 10 # to simulate a change in the agent's parameters - algo.save('test_algo.pkl') + algo.save('./test_algo') # Load the algorithm and check if it works @@ -141,12 +141,12 @@ def _llm_callable(messages, **kwargs): agent, optimizer, ) - algo2.load('test_algo.pkl') + algo2.load('./test_algo') assert algo2.agent.param.data == 10, "Loaded agent's parameter does not match the saved one." assert algo2.optimizer.objective == 'fake objective', "Loaded optimizer's objective does not match the saved one." - os.remove('test_algo.pkl') - os.remove('test_algo.pkl_agent.module') - os.remove('test_algo.pkl_optimizer.optimizer') - os.remove('test_algo.pkl_validate_guide.guide') \ No newline at end of file + os.remove('./test_algo') + os.remove('./test_algo_agent.module') + os.remove('./test_algo_optimizer.optimizer') + os.remove('./test_algo_validate_guide.guide') \ No newline at end of file From 9f2c9b148950dce160ae9baa5a6cdbc35bac02cd Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 18 Sep 2025 22:13:59 +0000 Subject: [PATCH 262/314] Fix merge error --- tests/unit_tests/test_saving_loading.py | 28 ++++++++++++------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/unit_tests/test_saving_loading.py b/tests/unit_tests/test_saving_loading.py index 0fc5bc6a..f9a5835c 100644 --- a/tests/unit_tests/test_saving_loading.py +++ b/tests/unit_tests/test_saving_loading.py @@ -44,20 +44,20 @@ def _llm_callable(messages, **kwargs): """ problem = messages[1]['content'] - # extract name from - name = re.findall(r"", problem) - if name: - name = name[0] - else: - name = "unknown" - - return f""" - Dummy reasoning based on the input messages. - - {name} - {suggested_value} - - """ + # extract name from + name = re.findall(r"", problem) + if name: + name = name[0] + else: + name = "unknown" + + return f""" + Dummy reasoning based on the input messages. + + {name} + {suggested_value} + + """ # Create a dummy LLM and an agent dummy_llm = DummyLLM(_llm_callable) From 42655b237e7eada5ac5155149263efc459348b73 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 18 Sep 2025 22:17:44 +0000 Subject: [PATCH 263/314] Remove copy overwrites in ModuleCandidate --- examples/priority_search_on_convex_fn.py | 4 +- .../priority_search/priority_search.py | 56 ++++++++++--------- 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/examples/priority_search_on_convex_fn.py b/examples/priority_search_on_convex_fn.py index 1f68064a..5d10f501 100644 --- a/examples/priority_search_on_convex_fn.py +++ b/examples/priority_search_on_convex_fn.py @@ -249,12 +249,12 @@ def get_feedback(self, query: str, response: str, reference=None, **kwargs) -> T score_range=[-10, 10], # trainer kwargs num_epochs=3*4, - batch_size=2, # this is just for testing. effectively, this is the same batch_size=1 and num_proposals=4 + batch_size=1, num_batches=2, verbose=False, #'output', guide=guide, num_candidates=4, - num_proposals=2, + num_proposals=4, memory_update_frequency=2, optimizer_kwargs={'objective':"You have a task of guessing two numbers. You should make sure your guess minimizes y.", 'memory_size': 10} diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index 298e159e..7e8d51fa 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -52,26 +52,34 @@ def apply_update(self, base_module=None): """ Apply update to the base_module in place. """ set_module_parameters(base_module or self.base_module, self.update_dict) - def __getstate__(self): - """ Get the state of the candidate for serialization. """ - state = copy.deepcopy(self.__dict__) # this will detach the nodes from the computation graph - return state - - def __setstate__(self, state): - """ Set the state of the candidate from serialization. """ - self.__dict__.update(state) - - def __deepcopy__(self, memo): - """ Create a deep copy, except for the base_module which is not copied, it is the original module. """ - cls = self.__class__ - result = cls.__new__(cls) - memo[id(self)] = result - for k, v in self.__dict__.items(): - if k != 'base_module': - setattr(result, k, copy.deepcopy(v, memo)) - else: - setattr(result, k, v) # base_module is not copied, it is the original module - return result + # def __getstate__(self): + # """ Get the state of the candidate for serialization. """ + # state = copy.deepcopy(self.__dict__) # this will detach the nodes from the computation graph + # state['base_module'] = self.base_module + # state = self.__dict__.copy() + # return state + + # def __setstate__(self, state): + # """ Set the state of the candidate from serialization. """ + # self.__dict__.update(state) + + # def __deepcopy__(self, memo): + # """ Create a deep copy, except for the base_module which is not copied, it is the original module. """ + # cls = self.__class__ + # result = cls.__new__(cls) + # memo[id(self)] = result + # for k, v in self.__dict__.items(): + # if k != 'base_module': + # setattr(result, k, copy.deepcopy(v, memo)) + # else: + # setattr(result, k, v) # base_module is not copied, it is the original module + # return result + + # def copy(self): + # """ Create a shallow copy, except for the base_module which is not copied, it is the original module. """ + # new_obj = self.__class__.__new__(self.__class__) # create a new instance of the same class + # new_obj.__dict__.update(self.__dict__) + # return new_obj def __eq__(self, other): """ Check if two candidates are equal based on their base_module and update_dict. """ @@ -802,13 +810,7 @@ def _process_rollout(rollout): for k in rollout: if k not in ['score']: rollout[k] = None - def _copy(obj): - # We manually implement a shallow copy, since __getstate__ is overridden in ModuleCandidate. - new_obj = obj.__class__.__new__(obj.__class__) # create a new instance of the same class - new_obj.__dict__.update(obj.__dict__) - return new_obj - - candidate = _copy(candidate) # make a copy of the candidate to avoid modifying the original one + candidate = copy.copy(candidate) # make a copy of the candidate to avoid modifying the original one candidate.rollouts = copy.deepcopy(candidate.rollouts) # deep copy the rollouts to avoid modifying the original one for rollout in candidate.rollouts: _process_rollout(rollout) From 48640b0d8b88424a20a6b79a99e313003c9b0057 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 18 Sep 2025 22:29:20 +0000 Subject: [PATCH 264/314] Remove saving all nodes in GRAPH, which may cause memory overflows. --- opto/trace/nodes.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/opto/trace/nodes.py b/opto/trace/nodes.py index cac02446..0ff0ee63 100644 --- a/opto/trace/nodes.py +++ b/opto/trace/nodes.py @@ -68,6 +68,7 @@ class Graph: """ TRACE = True # When True, we trace the graph when creating MessageNode. When False, we don't trace the graph. + LEGACY_GRAPH_BEHAVIOR = False # When True, we use the legacy graph behavior where nodes are not copied when used in multiple places. def __init__(self): """Initialize the Graph object. @@ -111,7 +112,12 @@ def register(self, node): name, _ = node.name.split(":") if len(NAME_SCOPES) > 0: name = NAME_SCOPES[-1] + "/" + name - self._nodes[name].append(node) + + if self.LEGACY_GRAPH_BEHAVIOR: + self._nodes[name].append(node) + else: + self._nodes[name].append(id(node)) # Store the id of the node to avoid memory issues + node._name = ( name + ":" + str(len(self._nodes[name]) - 1) ) # NOTE assume elements in self._nodes never get removed. @@ -131,6 +137,9 @@ def get(self, name): The function assumes that the '_nodes' attribute is a dictionary where each key is a node name and the corresponding value is a list of nodes. The 'id' should be a valid index within the list of nodes for the given 'name'. """ + if not self.LEGACY_GRAPH_BEHAVIOR: + raise ValueError("Graph.get is not supported when LEGACY_GRAPH_BEHAVIOR is False.") + name, id = name.split(":") return self._nodes[name][int(id)] @@ -141,6 +150,8 @@ def roots(self): Returns: list: A list of all root nodes in the graph. A root node is identified by its `is_root` attribute. """ + if not self.LEGACY_GRAPH_BEHAVIOR: + raise ValueError("Graph.roots is not supported when LEGACY_GRAPH_BEHAVIOR is False.") return [v for vv in self._nodes.values() for v in vv if v.is_root] def __str__(self): From 2d6e3250a651bb6e94488d0c2bba27e00074e257 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 18 Sep 2025 23:01:10 +0000 Subject: [PATCH 265/314] Switch to use counts to save memory. --- opto/trace/nodes.py | 47 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/opto/trace/nodes.py b/opto/trace/nodes.py index 0ff0ee63..7bc1d38c 100644 --- a/opto/trace/nodes.py +++ b/opto/trace/nodes.py @@ -68,14 +68,17 @@ class Graph: """ TRACE = True # When True, we trace the graph when creating MessageNode. When False, we don't trace the graph. - LEGACY_GRAPH_BEHAVIOR = False # When True, we use the legacy graph behavior where nodes are not copied when used in multiple places. + LEGACY_GRAPH_BEHAVIOR = False # When True, we use the legacy graph behavior where nodes are stored in lists. When False, we only store the count of nodes to save memory. def __init__(self): """Initialize the Graph object. The initialization sets up the `_nodes` attribute as a defaultdict of lists to store nodes by their names. """ - self._nodes = defaultdict(list) # a lookup table to find nodes by name + if self.LEGACY_GRAPH_BEHAVIOR: + self._nodes = defaultdict(list) # a lookup table to find nodes by name + else: + self._nodes = defaultdict(lambda: 0) # a lookup table to find nodes by name def clear(self): """Remove all nodes from the graph. @@ -89,10 +92,12 @@ def clear(self): The function is called in unit tests to reset the state of the graph between test cases, ensuring that each test runs with a clean slate and is not affected by the state left by previous tests. """ - for node in self._nodes.values(): - del node - self._nodes = defaultdict(list) - # self._levels = defaultdict(list) + if self.LEGACY_GRAPH_BEHAVIOR: + for node in self._nodes.values(): + del node + self._nodes = defaultdict(list) + else: + self._nodes = defaultdict(lambda: 0) def register(self, node): """Add a node to the graph. @@ -116,13 +121,33 @@ def register(self, node): if self.LEGACY_GRAPH_BEHAVIOR: self._nodes[name].append(node) else: - self._nodes[name].append(id(node)) # Store the id of the node to avoid memory issues + self._nodes[name] += 1 # Store the id of the node to avoid memory issues node._name = ( - name + ":" + str(len(self._nodes[name]) - 1) + name + ":" + str(self.count(name) - 1) ) # NOTE assume elements in self._nodes never get removed. # self._levels[node._level].append(node) + def count(self, name): + """Count the number of nodes with a given name in the graph. + + Args: + name (str): The name of the nodes to count. + + Returns: + int: The number of nodes with the given name. + + Notes: + The count function checks if the input name is a string. + If it is, it returns the length of the list of nodes associated with that name in the `_nodes` dictionary. + If the input name is not a string, it raises a ValueError indicating that the name must be a string. + """ + assert name in self._nodes, f"Name {name} not found in graph." + if self.LEGACY_GRAPH_BEHAVIOR: + return len(self._nodes[name]) + else: + return self._nodes[name] + def get(self, name): """Retrieve a node from the graph by its name. @@ -139,7 +164,6 @@ def get(self, name): """ if not self.LEGACY_GRAPH_BEHAVIOR: raise ValueError("Graph.get is not supported when LEGACY_GRAPH_BEHAVIOR is False.") - name, id = name.split(":") return self._nodes[name][int(id)] @@ -169,7 +193,10 @@ def __len__(self): int: The total number of nodes in the graph by summing the lengths of all lists in the `_nodes` dictionary. """ # This is the number of nodes in the graph - return sum([len(v) for v in self._nodes.values()]) + if self.LEGACY_GRAPH_BEHAVIOR: + return sum([len(v) for v in self._nodes.values()]) + else: + return sum(self._nodes.values()) GRAPH = Graph() # This is a global registry of all the nodes. From c841d869f6d88335ea447ba1e555a2bf9ed8ca4d Mon Sep 17 00:00:00 2001 From: Adith Swaminathan Date: Fri, 19 Sep 2025 12:50:08 -0700 Subject: [PATCH 266/314] Adding claude-code-generated docstrings to functions and fixing docs, tutorials and examples --- Makefile | 19 - docs/quickstart/installation.md | 14 + docs/readme.md | 29 - docs/tutorials/minibatch.ipynb | 66 +- docs/tutorials/projections_tutorial.ipynb | 856 ++++++ docs/tutorials/trainers.ipynb | 2684 +++++++++++++---- examples/async_optimization_example.py | 370 +++ examples/greeting.py | 2 +- examples/gsm8k_trainer_example.py | 88 +- examples/priority_search_example.py | 101 +- examples/search_algo_example.py | 53 +- examples/train_model.py | 48 +- opto/features/predefined_agents/__init__.py | 5 + opto/features/predefined_agents/learner.py | 85 + .../priority_search/priority_search.py | 351 ++- opto/optimizers/buffers.py | 58 +- opto/optimizers/opro.py | 68 +- opto/optimizers/opro_v2.py | 174 ++ opto/optimizers/optimizer.py | 291 +- opto/optimizers/optoprime.py | 259 +- opto/optimizers/optoprimemulti.py | 94 + opto/optimizers/textgrad.py | 118 +- opto/trace/README.md | 1 - opto/trace/broadcast.py | 97 +- opto/trace/bundle.py | 303 +- opto/trace/containers.py | 196 +- opto/trace/errors.py | 56 +- opto/trace/iterators.py | 101 +- opto/trace/modules.py | 177 +- opto/trace/nodes.py | 489 ++- opto/trace/projections/projections.py | 77 +- opto/trace/propagators/graph_propagator.py | 78 +- opto/trace/propagators/propagators.py | 243 +- opto/trace/utils.py | 212 +- opto/trainer/README.md | 0 opto/trainer/algorithms/aggregator.py | 216 +- opto/trainer/algorithms/algorithm.py | 196 +- opto/trainer/algorithms/basic_algorithms.py | 370 ++- opto/trainer/train.py | 90 +- opto/utils/llm.py | 105 +- 40 files changed, 7555 insertions(+), 1285 deletions(-) delete mode 100644 Makefile delete mode 100644 docs/readme.md create mode 100644 docs/tutorials/projections_tutorial.ipynb create mode 100644 examples/async_optimization_example.py create mode 100644 opto/features/predefined_agents/__init__.py create mode 100644 opto/features/predefined_agents/learner.py delete mode 100644 opto/trace/README.md delete mode 100644 opto/trainer/README.md diff --git a/Makefile b/Makefile deleted file mode 100644 index ec45bb82..00000000 --- a/Makefile +++ /dev/null @@ -1,19 +0,0 @@ -.PHONY: help doc doc-deploy - -help: - @echo "Makefile for managing Jupyter Book documentation and deployment" - @echo "" - @echo "Usage:" - @echo " make doc - Build the documentation" - @echo " make doc-deploy - Deploy the documentation to GitHub Pages" - @echo " make help - Display this help message" - @echo "" - @echo "For more information, refer to the README or script documentation." - -doc: - @echo "Building documentation..." - @bash ./docs/jupyter_build.sh - -doc-deploy: - @echo "Deploying documentation to GitHub Pages..." - @ghp-import -n -p -f docs/_build/html \ No newline at end of file diff --git a/docs/quickstart/installation.md b/docs/quickstart/installation.md index 0c74a0ac..a196f0bc 100644 --- a/docs/quickstart/installation.md +++ b/docs/quickstart/installation.md @@ -9,6 +9,20 @@ any external dependencies. However, if you want to use optimizer `opto.optimizers`, then we require `LiteLLM` package to make LLM API calls. +If you want to use the graph visualization features (e.g., `node.backward(visualize=True)`), +you need to install the system graphviz package: + +```bash +# On Ubuntu/Debian +sudo apt install graphviz + +# On macOS +brew install graphviz + +# On Windows +# Download from https://graphviz.org/download/ +``` + To install Trace, run: ```{admonition} Installation Command diff --git a/docs/readme.md b/docs/readme.md deleted file mode 100644 index 973cda5c..00000000 --- a/docs/readme.md +++ /dev/null @@ -1,29 +0,0 @@ -Steps of deployment: - -IMPORTANT: checkout the `website` branch. - -1. Run `make doc` under the root directory to build the book. This will create a folder `docs/_build/html` that has the static webpages. -2. Run `make doc-deploy` to deploy the book to GitHub Pages (it creates a branch in the repo) - -References: - -https://jupyterbook.org/en/stable/start/publish.html - -A few notes: -1. There is no direct way to add an HTML page to Jupyter book. -2. Run `pip install -r requirements.txt` to install dependencies. -3. Do not manually modify `gh-pages` branch. - - -Workflow for **adding new documentation** -1. Documents are currently hosted under the `main` branch. You should checkout the `main` branch first and commit your edits here. -2. After you are done with the edits, checkout the `website` branch. -3. Run `git pull origin main` to merge the changes from the `main` branch to the `website` branch. - - **important**: Do not merge `website` branch into `main` branch, because it contains a lot of web-related files that are not part of the main library. -4. Run the three steps above to deploy the book to GitHub Pages. - -Workflow for **adding new jupyter notebooks** -1. Jupyter notebooks will have a `kernelspec` in the metadata. This is usually set to your machine's jupyter kernel and will report an error in CoLab. -2. We use `colab_kernel_clean_script.py` to clean the `kernelspec` from the notebook. This script will remove the `kernelspec` from the notebook and save it as a new file. -3. If you update a notebook (after running it) or add a new notebook, please run the script on the notebook before committing it to the repo. -4. Run `python docs/colab_kernel_clean_script.py` to clean the notebook. \ No newline at end of file diff --git a/docs/tutorials/minibatch.ipynb b/docs/tutorials/minibatch.ipynb index dd1ad029..f752d866 100644 --- a/docs/tutorials/minibatch.ipynb +++ b/docs/tutorials/minibatch.ipynb @@ -547,54 +547,10 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "from opto import trace\n", - "from opto.utils.llm import LLM\n", - "from opto.optimizers import OptoPrime\n", - "from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm\n", - "from opto.trainer.loggers import TensorboardLogger\n", - "from opto.trainer.guide import VerbalJudgeGuide\n", - "from typing import Any\n", - "\n", - "@trace.model\n", - "class Learner:\n", - " \"\"\" A basic LLM agent. \"\"\"\n", - "\n", - " def __init__(self, system_prompt: str = \"You're a helpful agent\",\n", - " user_prompt_template: str = \"Query: {message}\",\n", - " llm: LLM = None):\n", - " self.system_prompt = trace.node(system_prompt, trainable=True)\n", - " self.user_prompt_template = trace.node(user_prompt_template)\n", - " self.llm = llm or LLM()\n", - "\n", - " @trace.bundle()\n", - " def model(self, system_prompt: str, user_prompt_template: str, message: str) -> str:\n", - " \"\"\"Call the LLM model.\n", - "\n", - " Args:\n", - " system_prompt: the system prompt to the agent. By tuning this prompt, we can control the behavior of the agent. For example, it can be used to provide instructions to the agent (such as how to reason about the problem, how to answer the question), or provide in-context examples of how to solve the problem.\n", - " user_prompt_template: the user prompt template to the agent. It is used as formatting the input to the agent as user_prompt_template.format(message=message).\n", - " message: the input to the agent. It can be a query, a task, a code, etc.\n", - " Returns:\n", - " The response from the agent.\n", - " \"\"\"\n", - "\n", - " if '{message}' not in user_prompt_template:\n", - " raise ValueError(\"user_prompt_template must contain '{message}'\")\n", - "\n", - " response = self.llm(\n", - " messages=[{\"role\": \"system\", \"content\": system_prompt},\n", - " {\"role\": \"user\", \"content\": user_prompt_template.format(message=message)}]\n", - " )\n", - " return response.choices[0].message.content\n", - "\n", - " def forward(self, message: Any) -> Any:\n", - " \"\"\" Forward pass of the agent. \"\"\"\n", - " return self.model(self.system_prompt, self.user_prompt_template, message)\n" - ] + "source": "from opto import trace\nfrom opto.utils.llm import LLM\nfrom opto.optimizers import OptoPrime\nfrom opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm\nfrom opto.trainer.loggers import TensorboardLogger\nfrom opto.trainer.guide import LLMJudge\nfrom opto.features.predefined_agents import BasicLearner\nfrom typing import Any\n\n# Use the predefined BasicLearner instead of defining our own\nLearner = BasicLearner" }, { "cell_type": "markdown", @@ -605,7 +561,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -813,7 +769,7 @@ ], "source": [ "agent = Learner(llm=LLM())\n", - "guide = VerbalJudgeGuide(llm=LLM())\n", + "guide = LLMJudge(llm=LLM())\n", "optimizer = OptoPrime(agent.parameters(), llm=LLM())\n", "logger = TensorboardLogger(verbose=True)\n", "\n", @@ -840,6 +796,18 @@ " \n", "asyncio.run(wrapper())" ] + }, + { + "cell_type": "markdown", + "source": "## Simplified Training with `trainer.train()`\n\nInstead of manually setting up the algorithm, optimizer, guide, and logger, you can use the simplified `trainer.train()` function that handles all the setup for you. This is the recommended approach for most use cases.", + "metadata": {} + }, + { + "cell_type": "code", + "source": "# Using the simplified trainer.train approach\nfrom opto import trainer\n\n# Create a fresh agent for simplified training\nsimple_agent = Learner(\n system_prompt=\"You're a helpful agent answering math problems.\",\n llm=LLM()\n)\n\nprint(\"STARTING SIMPLIFIED TRAINING\")\nmetrics, final_score = trainer.train(\n model=simple_agent,\n train_dataset=train_dataset,\n algorithm='MinibatchAlgorithm',\n guide=LLMJudge(llm=LLM()),\n # trainer kwargs\n num_epochs=num_epochs,\n batch_size=batch_size,\n eval_frequency=eval_frequency,\n test_dataset=test_dataset,\n num_threads=num_threads,\n verbose='output',\n)\nprint(\"FINISHED SIMPLIFIED TRAINING\")\nprint(f\"Final score: {final_score}\")", + "metadata": {}, + "execution_count": null, + "outputs": [] } ], "metadata": { @@ -863,4 +831,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file diff --git a/docs/tutorials/projections_tutorial.ipynb b/docs/tutorials/projections_tutorial.ipynb new file mode 100644 index 00000000..d2aa9ef0 --- /dev/null +++ b/docs/tutorials/projections_tutorial.ipynb @@ -0,0 +1,856 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Projections and Operators Reference Guide\n", + "\n", + "This guide demonstrates projections and operators in Trace - two core features for constrained optimization and flexible computation graphs.\n", + "\n", + "## Overview\n", + "\n", + "**Operators** are traced functions that enable computation on nodes while preserving the computation graph.\n", + "\n", + "**Projections** are functions that constrain parameters to valid ranges or formats during optimization.\n", + "\n", + "Let's explore each with practical examples." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Imports successful\n" + ] + } + ], + "source": [ + "# Import required modules\n", + "import numpy as np\n", + "from typing import Any\n", + "\n", + "from opto.trace import node, bundle\n", + "from opto.trace.nodes import ParameterNode\n", + "from opto.trace.projections import Projection, BlackCodeFormatter\n", + "import opto.trace.operators as operators\n", + "\n", + "print(\"Imports successful\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Understanding Operators\n", + "\n", + "Operators in Trace are traced functions that enable computation on nodes while preserving the computation graph. They support automatic differentiation for optimization." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Addition: 10 + 5 = 15\n", + "Multiplication: 10 * 5 = 50\n", + "Power: 10^2 = 100\n", + "String concatenation: 'Hello' + ' World' = 'Hello World'\n", + "Uppercase: 'Hello' -> 'HELLO'\n" + ] + } + ], + "source": [ + "# Basic operator usage\n", + "x = node(10)\n", + "y = node(5)\n", + "\n", + "# Arithmetic operations\n", + "sum_result = operators.add(x, y)\n", + "product = operators.multiply(x, y) \n", + "power_result = operators.power(x, node(2))\n", + "\n", + "print(f\"Addition: {x.data} + {y.data} = {sum_result.data}\")\n", + "print(f\"Multiplication: {x.data} * {y.data} = {product.data}\")\n", + "print(f\"Power: {x.data}^2 = {power_result.data}\")\n", + "\n", + "# String operations\n", + "text1 = node(\"Hello\")\n", + "text2 = node(\" World\")\n", + "concatenated = operators.add(text1, text2)\n", + "uppercase = operators.upper(text1)\n", + "\n", + "print(f\"String concatenation: '{text1.data}' + '{text2.data}' = '{concatenated.data}'\")\n", + "print(f\"Uppercase: '{text1.data}' -> '{uppercase.data}'\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Operator Categories\n", + "\n", + "Trace provides operators across multiple categories:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Arithmetic Operations:\n", + "Addition: 19\n", + "Subtraction: 11\n", + "Multiplication: 60\n", + "Division: 3.75\n", + "Floor Division: 3\n", + "Modulo: 3\n", + "Power: 225\n", + "\n", + "Mathematical Functions:\n", + "Floor: 3\n", + "Ceiling: 4\n", + "Truncate: 3\n", + "Negative: -5\n", + "Positive: -5\n" + ] + } + ], + "source": [ + "# 1. Arithmetic Operators\n", + "a, b = node(15), node(4)\n", + "\n", + "print(\"Arithmetic Operations:\")\n", + "print(f\"Addition: {operators.add(a, b).data}\")\n", + "print(f\"Subtraction: {operators.subtract(a, b).data}\")\n", + "print(f\"Multiplication: {operators.multiply(a, b).data}\")\n", + "print(f\"Division: {operators.divide(a, b).data}\")\n", + "print(f\"Floor Division: {operators.floor_divide(a, b).data}\")\n", + "print(f\"Modulo: {operators.mod(a, b).data}\")\n", + "print(f\"Power: {operators.power(a, node(2)).data}\")\n", + "\n", + "# 2. Mathematical Functions \n", + "print(\"\\nMathematical Functions:\")\n", + "print(f\"Floor: {operators.floor(node(3.7)).data}\")\n", + "print(f\"Ceiling: {operators.ceil(node(3.2)).data}\")\n", + "print(f\"Truncate: {operators.trunc(node(3.9)).data}\")\n", + "print(f\"Negative: {operators.neg(node(5)).data}\")\n", + "print(f\"Positive: {operators.pos(node(-5)).data}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Comparison Operations:\n", + "Less than: 10 < 5 = False\n", + "Equal: 10 == 5 = False\n", + "Greater than: 10 > 5 = True\n", + "\n", + "Logical Operations:\n", + "Conditional: x is greater\n", + "Logical NOT: not True = False\n" + ] + } + ], + "source": [ + "# 3. Comparison and Logical Operations\n", + "x, y = node(10), node(5)\n", + "\n", + "print(\"Comparison Operations:\")\n", + "print(f\"Less than: {x.data} < {y.data} = {operators.lt(x, y).data}\")\n", + "print(f\"Equal: {x.data} == {y.data} = {operators.eq(x, y).data}\")\n", + "print(f\"Greater than: {x.data} > {y.data} = {operators.gt(x, y).data}\")\n", + "\n", + "print(\"\\nLogical Operations:\")\n", + "condition = operators.gt(x, y)\n", + "true_val = node(\"x is greater\")\n", + "false_val = node(\"x is smaller\")\n", + "result = operators.cond(condition, true_val, false_val)\n", + "print(f\"Conditional: {result.data}\")\n", + "\n", + "bool_val = node(True)\n", + "print(f\"Logical NOT: not {bool_val.data} = {operators.not_(bool_val).data}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Collection Operations:\n", + "List length: 5\n", + "List indexing [2]: 3\n", + "Dict keys: ['a', 'b', 'c']\n", + "Dict values: [1, 2, 3]\n", + "\n", + "3 in list: True\n", + "'Hello' in string: True\n" + ] + } + ], + "source": [ + "# 4. Collection Operations\n", + "my_list = node([1, 2, 3, 4, 5])\n", + "my_dict = node({\"a\": 1, \"b\": 2, \"c\": 3})\n", + "my_string = node(\"Hello World\")\n", + "\n", + "print(\"Collection Operations:\")\n", + "print(f\"List length: {operators.len_(my_list).data}\")\n", + "print(f\"List indexing [2]: {operators.getitem(my_list, node(2)).data}\")\n", + "print(f\"Dict keys: {operators.keys(my_dict).data}\")\n", + "print(f\"Dict values: {operators.values(my_dict).data}\")\n", + "\n", + "# Membership testing\n", + "print(f\"\\n3 in list: {operators.in_(node(3), my_list).data}\")\n", + "print(f\"'Hello' in string: {operators.in_(node('Hello'), my_string).data}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "String Operations:\n", + "Original: ' Hello World '\n", + "Uppercase: ' HELLO WORLD '\n", + "Lowercase: ' hello world '\n", + "Strip whitespace: 'Hello World'\n", + "\n", + "Split: ['apple', 'banana', 'cherry']\n", + "Formatted string: Hello Alice, you are 30 years old\n" + ] + } + ], + "source": [ + "# 5. String Operations\n", + "text = node(\" Hello World \")\n", + "\n", + "print(\"String Operations:\")\n", + "print(f\"Original: '{text.data}'\")\n", + "print(f\"Uppercase: '{operators.upper(text).data}'\")\n", + "print(f\"Lowercase: '{operators.lower(text).data}'\")\n", + "print(f\"Strip whitespace: '{operators.strip(text).data}'\")\n", + "\n", + "# Advanced string operations\n", + "sentence = node(\"apple,banana,cherry\")\n", + "words = operators.split(sentence, node(\",\"))\n", + "print(f\"\\nSplit: {words.data}\")\n", + "\n", + "# String formatting\n", + "template = node(\"Hello {name}, you are {age} years old\")\n", + "formatted = operators.format(template, name=\"Alice\", age=30)\n", + "print(f\"Formatted string: {formatted.data}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Understanding Projections\n", + "\n", + "Projections are functions that constrain parameters to valid ranges or formats during optimization. They ensure parameters remain within feasible sets." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Bounded Projection (range [0.0, 1.0]):\n", + " -0.5 -> 0.0\n", + " 0.3 -> 0.3\n", + " 1.2 -> 1.0\n", + " 0.8 -> 0.8\n" + ] + } + ], + "source": [ + "# Basic projection example\n", + "class BoundedProjection(Projection):\n", + " \"\"\"Project values to a bounded range [min_val, max_val].\"\"\"\n", + " \n", + " def __init__(self, min_val: float, max_val: float):\n", + " super().__init__()\n", + " self.min_val = min_val\n", + " self.max_val = max_val\n", + " \n", + " def project(self, x: Any) -> Any:\n", + " \"\"\"Clip value to the specified bounds.\"\"\"\n", + " if isinstance(x, (int, float)):\n", + " return max(self.min_val, min(self.max_val, x))\n", + " elif isinstance(x, np.ndarray):\n", + " return np.clip(x, self.min_val, self.max_val)\n", + " elif isinstance(x, list):\n", + " return [max(self.min_val, min(self.max_val, val)) for val in x]\n", + " return x\n", + "\n", + "# Test the projection\n", + "projection = BoundedProjection(0.0, 1.0)\n", + "\n", + "test_values = [-0.5, 0.3, 1.2, 0.8]\n", + "print(\"Bounded Projection (range [0.0, 1.0]):\")\n", + "for val in test_values:\n", + " projected = projection.project(val)\n", + " print(f\" {val:5.1f} -> {projected:5.1f}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Initial parameter value: 0.5\n", + "\n", + "Projecting invalid updates:\n", + " Update 10.0 -> Projected 1.000\n", + " Update -0.1 -> Projected 0.001\n", + " Update 0.5 -> Projected 0.500\n", + " Update 2.0 -> Projected 1.000\n" + ] + } + ], + "source": [ + "# Using projections with ParameterNode\n", + "bounded_param = ParameterNode(\n", + " 0.5,\n", + " name=\"learning_rate\",\n", + " description=\"Learning rate constrained to [0.001, 1.0]\",\n", + " projections=[BoundedProjection(0.001, 1.0)]\n", + ")\n", + "\n", + "print(f\"Initial parameter value: {bounded_param.data}\")\n", + "\n", + "# Simulate optimizer trying to set invalid values\n", + "test_updates = [10.0, -0.1, 0.5, 2.0]\n", + "print(\"\\nProjecting invalid updates:\")\n", + "for update in test_updates:\n", + " # This is what happens internally during optimization\n", + " projected = bounded_param.projections[0].project(update)\n", + " print(f\" Update {update:5.1f} -> Projected {projected:6.3f}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Custom Projections Examples\n", + "\n", + "Here are practical examples of custom projections:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Probability Distribution Projection:\n", + "Input -> Projected (Sum)\n", + "[0.1, 0.2, 0.3] -> [0.167, 0.333, 0.5] (sum=1.000)\n", + "[-0.1, 0.6, 0.5] -> [0.0, 0.545, 0.455] (sum=1.000)\n", + "[0, 0, 0] -> [0.333, 0.333, 0.333] (sum=1.000)\n", + "[1, 2, 3] -> [0.167, 0.333, 0.5] (sum=1.000)\n" + ] + } + ], + "source": [ + "# Example 1: Probability Distribution Projection\n", + "class ProbabilityProjection(Projection):\n", + " \"\"\"Ensure values form a valid probability distribution.\"\"\"\n", + " \n", + " def __init__(self, epsilon: float = 1e-8):\n", + " super().__init__()\n", + " self.epsilon = epsilon\n", + " \n", + " def project(self, x: Any) -> Any:\n", + " \"\"\"Normalize to valid probability distribution.\"\"\"\n", + " if isinstance(x, (list, np.ndarray)):\n", + " x_array = np.array(x, dtype=float)\n", + " # Ensure non-negative values\n", + " x_array = np.maximum(x_array, self.epsilon)\n", + " # Normalize to sum to 1\n", + " x_array = x_array / np.sum(x_array)\n", + " return x_array.tolist() if isinstance(x, list) else x_array\n", + " return x\n", + "\n", + "# Test probability projection\n", + "prob_proj = ProbabilityProjection()\n", + "\n", + "test_distributions = [\n", + " [0.1, 0.2, 0.3], # Doesn't sum to 1\n", + " [-0.1, 0.6, 0.5], # Has negative values\n", + " [0, 0, 0], # All zeros\n", + " [1, 2, 3], # Arbitrary positive values\n", + "]\n", + "\n", + "print(\"Probability Distribution Projection:\")\n", + "print(\"Input -> Projected (Sum)\")\n", + "for dist in test_distributions:\n", + " projected = prob_proj.project(dist)\n", + " print(f\"{str(dist):20} -> {[round(p, 3) for p in projected]} (sum={sum(projected):.3f})\")" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Text Length Projection (10-20 chars):\n", + "Input ( 2): 'Hi'\n", + "Output(10): 'Hi........'\n", + "\n", + "Input (14): 'Perfect length'\n", + "Output(14): 'Perfect length'\n", + "\n", + "Input (44): 'This text is way too long for the constraint'\n", + "Output(20): 'This text is way ...'\n", + "\n" + ] + } + ], + "source": [ + "# Example 2: Text Length Projection\n", + "class TextLengthProjection(Projection):\n", + " \"\"\"Constrain text to specific length range.\"\"\"\n", + " \n", + " def __init__(self, min_length: int, max_length: int, pad_char: str = \".\"):\n", + " super().__init__()\n", + " self.min_length = min_length\n", + " self.max_length = max_length\n", + " self.pad_char = pad_char\n", + " \n", + " def project(self, x: Any) -> Any:\n", + " \"\"\"Adjust text length to fit constraints.\"\"\"\n", + " if isinstance(x, str):\n", + " if len(x) > self.max_length:\n", + " return x[:self.max_length-3] + \"...\"\n", + " elif len(x) < self.min_length:\n", + " return x + self.pad_char * (self.min_length - len(x))\n", + " return x\n", + " return x\n", + "\n", + "# Test text length projection\n", + "text_proj = TextLengthProjection(min_length=10, max_length=20)\n", + "\n", + "test_texts = [\n", + " \"Hi\", # Too short\n", + " \"Perfect length\", # Just right\n", + " \"This text is way too long for the constraint\" # Too long\n", + "]\n", + "\n", + "print(\"Text Length Projection (10-20 chars):\")\n", + "for text in test_texts:\n", + " projected = text_proj.project(text)\n", + " print(f\"Input ({len(text):2d}): '{text}'\")\n", + " print(f\"Output({len(projected):2d}): '{projected}'\")\n", + " print()" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Composite Projection (Bounds + Probability):\n", + "Input: [-1, 2, 3]\n", + "Output: [0.0, 0.4, 0.6]\n", + "Sum: 1.000\n", + "\n", + "Input: [0.1, 0.2, 0.3]\n", + "Output: [0.167, 0.333, 0.5]\n", + "Sum: 1.000\n", + "\n", + "Input: [5, 10, 15]\n", + "Output: [0.2, 0.4, 0.4]\n", + "Sum: 1.000\n", + "\n" + ] + } + ], + "source": [ + "# Example 3: Composite Projections\n", + "class CompositeProjection(Projection):\n", + " \"\"\"Apply multiple projections in sequence.\"\"\"\n", + " \n", + " def __init__(self, *projections: Projection):\n", + " super().__init__()\n", + " self.projections = projections\n", + " \n", + " def project(self, x: Any) -> Any:\n", + " \"\"\"Apply projections sequentially.\"\"\"\n", + " result = x\n", + " for projection in self.projections:\n", + " result = projection.project(result)\n", + " return result\n", + "\n", + "# Example: Combine bounds with probability normalization\n", + "bounded_prob_proj = CompositeProjection(\n", + " BoundedProjection(0.0, 10.0), # First ensure non-negative\n", + " ProbabilityProjection() # Then normalize\n", + ")\n", + "\n", + "test_values = [[-1, 2, 3], [0.1, 0.2, 0.3], [5, 10, 15]]\n", + "print(\"Composite Projection (Bounds + Probability):\")\n", + "for val in test_values:\n", + " projected = bounded_prob_proj.project(val)\n", + " print(f\"Input: {val}\")\n", + " print(f\"Output: {[round(p, 3) for p in projected]}\")\n", + " print(f\"Sum: {sum(projected):.3f}\")\n", + " print()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Built-in Projections\n", + "\n", + "Trace provides built-in projections for common use cases:" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "BlackCodeFormatter Projection:\n", + "Input code: def add(a,b): return a+b\n", + "Formatted:\n", + "def add(a, b):\n", + " return a + b\n", + "\n" + ] + } + ], + "source": [ + "# BlackCodeFormatter projection\n", + "try:\n", + " code_projection = BlackCodeFormatter()\n", + " \n", + " sample_code = \"def add(a,b): return a+b\"\n", + " \n", + " print(\"BlackCodeFormatter Projection:\")\n", + " print(f\"Input code: {sample_code}\")\n", + " formatted_code = code_projection.project(sample_code)\n", + " print(f\"Formatted:\\n{formatted_code}\")\n", + " \n", + "except ImportError:\n", + " print(\"BlackCodeFormatter requires 'black' package\")\n", + " print(\"Install with: pip install black\")\n", + " print(\"\\nBlackCodeFormatter automatically formats Python code\")\n", + " print(\"It only processes strings containing 'def' keyword\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Integration with Optimization\n", + "\n", + "Projections work seamlessly with Trace optimizers:" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Parameter with Projection:\n", + "Initial value: 0.5\n", + "Has projections: True\n", + "Projection type: BoundedProjection\n", + "\n", + "Objective value: 0.0400\n", + "\n", + "Simulating optimization updates:\n", + "Update 1.5 -> Projected 1.0\n", + "Update -0.2 -> Projected 0.0\n", + "Update 0.8 -> Projected 0.8\n", + "Update 2.0 -> Projected 1.0\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages/flaml/__init__.py:20: UserWarning: flaml.automl is not available. Please install flaml[automl] to enable AutoML functionalities.\n", + " warnings.warn(\"flaml.automl is not available. Please install flaml[automl] to enable AutoML functionalities.\")\n" + ] + } + ], + "source": [ + "# Example: Parameter with projection\n", + "from opto.optimizers import OptoPrime\n", + "\n", + "# Create a parameter with bounds projection\n", + "constrained_param = ParameterNode(\n", + " 0.5,\n", + " name=\"weight\",\n", + " description=\"Weight parameter bounded to [0, 1]\",\n", + " projections=[BoundedProjection(0.0, 1.0)]\n", + ")\n", + "\n", + "print(\"Parameter with Projection:\")\n", + "print(f\"Initial value: {constrained_param.data}\")\n", + "print(f\"Has projections: {len(constrained_param.projections) > 0}\")\n", + "print(f\"Projection type: {type(constrained_param.projections[0]).__name__}\")\n", + "\n", + "# Create a simple optimization problem\n", + "@bundle()\n", + "def objective_function(weight):\n", + " \"\"\"Simple quadratic objective.\"\"\"\n", + " # Minimize (weight - 0.3)^2\n", + " return (weight - 0.3) ** 2\n", + "\n", + "# Test projection effect\n", + "result = objective_function(constrained_param)\n", + "print(f\"\\nObjective value: {result.data:.4f}\")\n", + "\n", + "# Simulate what happens during optimization\n", + "print(\"\\nSimulating optimization updates:\")\n", + "simulated_updates = [1.5, -0.2, 0.8, 2.0]\n", + "for update in simulated_updates:\n", + " projected = constrained_param.projections[0].project(update)\n", + " print(f\"Update {update:4.1f} -> Projected {projected:4.1f}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Advanced Patterns\n", + "\n", + "### Dynamic Projection Selection" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Conditional Projection (different bounds for +/- values):\n", + "-15.00 (negative) -> -10.00\n", + "-0.05 (negative) -> -0.10\n", + " 0.05 (positive) -> 0.10\n", + "15.00 (positive) -> 10.00\n" + ] + } + ], + "source": [ + "# Conditional projection based on value\n", + "class ConditionalProjection(Projection):\n", + " \"\"\"Apply different projections based on conditions.\"\"\"\n", + " \n", + " def __init__(self, condition_func, true_projection, false_projection):\n", + " super().__init__()\n", + " self.condition_func = condition_func\n", + " self.true_projection = true_projection\n", + " self.false_projection = false_projection\n", + " \n", + " def project(self, x: Any) -> Any:\n", + " \"\"\"Apply projection based on condition.\"\"\"\n", + " if self.condition_func(x):\n", + " return self.true_projection.project(x)\n", + " else:\n", + " return self.false_projection.project(x)\n", + "\n", + "# Example: Different bounds for positive vs negative values\n", + "def is_positive(x):\n", + " return isinstance(x, (int, float)) and x > 0\n", + "\n", + "conditional_proj = ConditionalProjection(\n", + " condition_func=is_positive,\n", + " true_projection=BoundedProjection(0.1, 10.0), # Positive values\n", + " false_projection=BoundedProjection(-10.0, -0.1) # Negative values\n", + ")\n", + "\n", + "test_values = [-15, -0.05, 0.05, 15]\n", + "print(\"Conditional Projection (different bounds for +/- values):\")\n", + "for val in test_values:\n", + " projected = conditional_proj.project(val)\n", + " condition = \"positive\" if is_positive(val) else \"negative\"\n", + " print(f\"{val:5.2f} ({condition:8}) -> {projected:6.2f}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Operator Chaining with Complex Data" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Complex Computation with Operators:\n", + "Input: {'a': 10, 'b': 20, 'c': 30, 'd': 40}\n", + "Total: 100\n", + "Count: 4\n", + "Average: 25.0\n", + "Summary: Total: 100, Count: 4, Average: 25.00\n" + ] + } + ], + "source": [ + "# Complex computation using operators\n", + "def process_data(data_dict):\n", + " \"\"\"Process dictionary data using operators.\"\"\"\n", + " # Extract values using operators\n", + " keys = operators.keys(data_dict)\n", + " values = operators.values(data_dict)\n", + " \n", + " # Compute statistics\n", + " total = node(0)\n", + " count = operators.len_(values)\n", + " \n", + " # Sum all values\n", + " values_list = values.data\n", + " for val in values_list:\n", + " total = operators.add(total, node(val))\n", + " \n", + " average = operators.divide(total, count)\n", + " \n", + " # Create result dictionary\n", + " result = {\n", + " \"total\": total.data,\n", + " \"count\": count.data,\n", + " \"average\": average.data,\n", + " \"summary\": f\"Total: {total.data}, Count: {count.data}, Average: {average.data:.2f}\"\n", + " }\n", + " \n", + " return result\n", + "\n", + "# Test complex computation\n", + "test_data = node({\"a\": 10, \"b\": 20, \"c\": 30, \"d\": 40})\n", + "result = process_data(test_data)\n", + "\n", + "print(\"Complex Computation with Operators:\")\n", + "print(f\"Input: {test_data.data}\")\n", + "print(f\"Total: {result['total']}\")\n", + "print(f\"Count: {result['count']}\")\n", + "print(f\"Average: {result['average']}\")\n", + "print(f\"Summary: {result['summary']}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "This guide covered the essential features of projections and operators in Trace:\n", + "\n", + "### Operators\n", + "- Enable traced computation while preserving differentiability\n", + "- Cover arithmetic, logical, comparison, and data manipulation operations\n", + "- Support diverse data types and maintain computation graphs\n", + "- Essential for building complex, optimizable functions\n", + "\n", + "### Projections\n", + "- Enforce constraints on parameters during optimization\n", + "- Are automatically applied by optimizers\n", + "- Can be composed for complex constraint scenarios\n", + "- Enable constrained optimization in diverse domains\n", + "\n", + "### Key Concepts\n", + "- **Operators**: Use for all computations that need to be traced\n", + "- **Projections**: Design to be idempotent and efficient\n", + "- **Integration**: Both work seamlessly with Trace optimizers\n", + "- **Composition**: Multiple projections can be chained together\n", + "\n", + "### Implementation Notes\n", + "- All operations are captured in the computation graph\n", + "- Projections are applied during parameter updates\n", + "- Error handling is built into the tracing system\n", + "- Support for parallel execution where applicable\n", + "\n", + "These features provide the foundation for building sophisticated, constrained optimization systems using Trace." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "trace", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.23" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/tutorials/trainers.ipynb b/docs/tutorials/trainers.ipynb index 84f64fa8..11ea5649 100644 --- a/docs/tutorials/trainers.ipynb +++ b/docs/tutorials/trainers.ipynb @@ -15,11 +15,113 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": { "collapsed": false }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Looking in indexes: https://pypi.netflix.net/simple\n", + "Requirement already satisfied: trace-opt in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (0.2.0)\n", + "Requirement already satisfied: ipywidgets in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (8.1.7)\n", + "Requirement already satisfied: graphviz>=0.20.1 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from trace-opt) (0.21)\n", + "Requirement already satisfied: pytest in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from trace-opt) (8.4.1)\n", + "Requirement already satisfied: litellm==1.75.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from trace-opt) (1.75.0)\n", + "Requirement already satisfied: black in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from trace-opt) (25.1.0)\n", + "Requirement already satisfied: scikit-learn in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from trace-opt) (1.6.1)\n", + "Requirement already satisfied: tensorboardX in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from trace-opt) (2.6.4)\n", + "Requirement already satisfied: tensorboard in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from trace-opt) (2.20.0)\n", + "Requirement already satisfied: aiohttp>=3.10 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from litellm==1.75.0->trace-opt) (3.12.15)\n", + "Requirement already satisfied: click in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from litellm==1.75.0->trace-opt) (8.1.8)\n", + "Requirement already satisfied: httpx>=0.23.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from litellm==1.75.0->trace-opt) (0.27.2)\n", + "Requirement already satisfied: importlib-metadata>=6.8.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from litellm==1.75.0->trace-opt) (8.7.0)\n", + "Requirement already satisfied: jinja2<4.0.0,>=3.1.2 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from litellm==1.75.0->trace-opt) (3.1.6)\n", + "Requirement already satisfied: jsonschema<5.0.0,>=4.22.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from litellm==1.75.0->trace-opt) (4.25.0)\n", + "Requirement already satisfied: openai>=1.68.2 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from litellm==1.75.0->trace-opt) (1.99.9)\n", + "Requirement already satisfied: pydantic<3.0.0,>=2.5.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from litellm==1.75.0->trace-opt) (2.11.7)\n", + "Requirement already satisfied: python-dotenv>=0.2.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from litellm==1.75.0->trace-opt) (1.1.1)\n", + "Requirement already satisfied: tiktoken>=0.7.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from litellm==1.75.0->trace-opt) (0.10.0)\n", + "Requirement already satisfied: tokenizers in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from litellm==1.75.0->trace-opt) (0.21.4)\n", + "Requirement already satisfied: MarkupSafe>=2.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from jinja2<4.0.0,>=3.1.2->litellm==1.75.0->trace-opt) (3.0.2)\n", + "Requirement already satisfied: attrs>=22.2.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from jsonschema<5.0.0,>=4.22.0->litellm==1.75.0->trace-opt) (25.3.0)\n", + "Requirement already satisfied: jsonschema-specifications>=2023.03.6 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from jsonschema<5.0.0,>=4.22.0->litellm==1.75.0->trace-opt) (2025.4.1)\n", + "Requirement already satisfied: referencing>=0.28.4 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from jsonschema<5.0.0,>=4.22.0->litellm==1.75.0->trace-opt) (0.36.2)\n", + "Requirement already satisfied: rpds-py>=0.7.1 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from jsonschema<5.0.0,>=4.22.0->litellm==1.75.0->trace-opt) (0.27.0)\n", + "Requirement already satisfied: annotated-types>=0.6.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from pydantic<3.0.0,>=2.5.0->litellm==1.75.0->trace-opt) (0.7.0)\n", + "Requirement already satisfied: pydantic-core==2.33.2 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from pydantic<3.0.0,>=2.5.0->litellm==1.75.0->trace-opt) (2.33.2)\n", + "Requirement already satisfied: typing-extensions>=4.12.2 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from pydantic<3.0.0,>=2.5.0->litellm==1.75.0->trace-opt) (4.14.1)\n", + "Requirement already satisfied: typing-inspection>=0.4.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from pydantic<3.0.0,>=2.5.0->litellm==1.75.0->trace-opt) (0.4.1)\n", + "Requirement already satisfied: comm>=0.1.3 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from ipywidgets) (0.2.3)\n", + "Requirement already satisfied: ipython>=6.1.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from ipywidgets) (8.18.1)\n", + "Requirement already satisfied: traitlets>=4.3.1 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from ipywidgets) (5.14.3)\n", + "Requirement already satisfied: widgetsnbextension~=4.0.14 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from ipywidgets) (4.0.14)\n", + "Requirement already satisfied: jupyterlab_widgets~=3.0.15 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from ipywidgets) (3.0.15)\n", + "Requirement already satisfied: aiohappyeyeballs>=2.5.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from aiohttp>=3.10->litellm==1.75.0->trace-opt) (2.6.1)\n", + "Requirement already satisfied: aiosignal>=1.4.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from aiohttp>=3.10->litellm==1.75.0->trace-opt) (1.4.0)\n", + "Requirement already satisfied: async-timeout<6.0,>=4.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from aiohttp>=3.10->litellm==1.75.0->trace-opt) (5.0.1)\n", + "Requirement already satisfied: frozenlist>=1.1.1 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from aiohttp>=3.10->litellm==1.75.0->trace-opt) (1.7.0)\n", + "Requirement already satisfied: multidict<7.0,>=4.5 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from aiohttp>=3.10->litellm==1.75.0->trace-opt) (6.6.3)\n", + "Requirement already satisfied: propcache>=0.2.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from aiohttp>=3.10->litellm==1.75.0->trace-opt) (0.3.2)\n", + "Requirement already satisfied: yarl<2.0,>=1.17.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from aiohttp>=3.10->litellm==1.75.0->trace-opt) (1.20.1)\n", + "Requirement already satisfied: idna>=2.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from yarl<2.0,>=1.17.0->aiohttp>=3.10->litellm==1.75.0->trace-opt) (3.10)\n", + "Requirement already satisfied: anyio in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from httpx>=0.23.0->litellm==1.75.0->trace-opt) (4.10.0)\n", + "Requirement already satisfied: certifi in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from httpx>=0.23.0->litellm==1.75.0->trace-opt) (2025.8.3)\n", + "Requirement already satisfied: httpcore==1.* in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from httpx>=0.23.0->litellm==1.75.0->trace-opt) (1.0.9)\n", + "Requirement already satisfied: sniffio in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from httpx>=0.23.0->litellm==1.75.0->trace-opt) (1.3.1)\n", + "Requirement already satisfied: h11>=0.16 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from httpcore==1.*->httpx>=0.23.0->litellm==1.75.0->trace-opt) (0.16.0)\n", + "Requirement already satisfied: zipp>=3.20 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from importlib-metadata>=6.8.0->litellm==1.75.0->trace-opt) (3.23.0)\n", + "Requirement already satisfied: decorator in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from ipython>=6.1.0->ipywidgets) (5.2.1)\n", + "Requirement already satisfied: jedi>=0.16 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from ipython>=6.1.0->ipywidgets) (0.19.2)\n", + "Requirement already satisfied: matplotlib-inline in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from ipython>=6.1.0->ipywidgets) (0.1.7)\n", + "Requirement already satisfied: prompt-toolkit<3.1.0,>=3.0.41 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from ipython>=6.1.0->ipywidgets) (3.0.51)\n", + "Requirement already satisfied: pygments>=2.4.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from ipython>=6.1.0->ipywidgets) (2.19.2)\n", + "Requirement already satisfied: stack-data in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from ipython>=6.1.0->ipywidgets) (0.6.3)\n", + "Requirement already satisfied: exceptiongroup in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from ipython>=6.1.0->ipywidgets) (1.3.0)\n", + "Requirement already satisfied: pexpect>4.3 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from ipython>=6.1.0->ipywidgets) (4.9.0)\n", + "Requirement already satisfied: wcwidth in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from prompt-toolkit<3.1.0,>=3.0.41->ipython>=6.1.0->ipywidgets) (0.2.13)\n", + "Requirement already satisfied: parso<0.9.0,>=0.8.4 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from jedi>=0.16->ipython>=6.1.0->ipywidgets) (0.8.4)\n", + "Requirement already satisfied: distro<2,>=1.7.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from openai>=1.68.2->litellm==1.75.0->trace-opt) (1.9.0)\n", + "Requirement already satisfied: jiter<1,>=0.4.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from openai>=1.68.2->litellm==1.75.0->trace-opt) (0.10.0)\n", + "Requirement already satisfied: tqdm>4 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from openai>=1.68.2->litellm==1.75.0->trace-opt) (4.67.1)\n", + "Requirement already satisfied: ptyprocess>=0.5 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from pexpect>4.3->ipython>=6.1.0->ipywidgets) (0.7.0)\n", + "Requirement already satisfied: regex>=2022.1.18 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from tiktoken>=0.7.0->litellm==1.75.0->trace-opt) (2025.7.34)\n", + "Requirement already satisfied: requests>=2.26.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from tiktoken>=0.7.0->litellm==1.75.0->trace-opt) (2.32.4)\n", + "Requirement already satisfied: charset_normalizer<4,>=2 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from requests>=2.26.0->tiktoken>=0.7.0->litellm==1.75.0->trace-opt) (3.4.2)\n", + "Requirement already satisfied: urllib3<3,>=1.21.1 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from requests>=2.26.0->tiktoken>=0.7.0->litellm==1.75.0->trace-opt) (1.26.20)\n", + "Requirement already satisfied: mypy-extensions>=0.4.3 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from black->trace-opt) (1.1.0)\n", + "Requirement already satisfied: packaging>=22.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from black->trace-opt) (25.0)\n", + "Requirement already satisfied: pathspec>=0.9.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from black->trace-opt) (0.12.1)\n", + "Requirement already satisfied: platformdirs>=2 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from black->trace-opt) (4.3.8)\n", + "Requirement already satisfied: tomli>=1.1.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from black->trace-opt) (2.2.1)\n", + "Requirement already satisfied: iniconfig>=1 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from pytest->trace-opt) (2.1.0)\n", + "Requirement already satisfied: pluggy<2,>=1.5 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from pytest->trace-opt) (1.6.0)\n", + "Requirement already satisfied: numpy>=1.19.5 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from scikit-learn->trace-opt) (1.26.4)\n", + "Requirement already satisfied: scipy>=1.6.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from scikit-learn->trace-opt) (1.13.1)\n", + "Requirement already satisfied: joblib>=1.2.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from scikit-learn->trace-opt) (1.5.1)\n", + "Requirement already satisfied: threadpoolctl>=3.1.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from scikit-learn->trace-opt) (3.6.0)\n", + "Requirement already satisfied: executing>=1.2.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from stack-data->ipython>=6.1.0->ipywidgets) (2.2.0)\n", + "Requirement already satisfied: asttokens>=2.1.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from stack-data->ipython>=6.1.0->ipywidgets) (3.0.0)\n", + "Requirement already satisfied: pure_eval in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from stack-data->ipython>=6.1.0->ipywidgets) (0.2.3)\n", + "Requirement already satisfied: absl-py>=0.4 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from tensorboard->trace-opt) (2.3.1)\n", + "Requirement already satisfied: grpcio>=1.48.2 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from tensorboard->trace-opt) (1.74.0)\n", + "Requirement already satisfied: markdown>=2.6.8 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from tensorboard->trace-opt) (3.8.2)\n", + "Requirement already satisfied: pillow in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from tensorboard->trace-opt) (11.3.0)\n", + "Requirement already satisfied: protobuf!=4.24.0,>=3.19.6 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from tensorboard->trace-opt) (6.31.1)\n", + "Requirement already satisfied: setuptools>=41.0.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from tensorboard->trace-opt) (78.1.1)\n", + "Requirement already satisfied: tensorboard-data-server<0.8.0,>=0.7.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from tensorboard->trace-opt) (0.7.2)\n", + "Requirement already satisfied: werkzeug>=1.0.1 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from tensorboard->trace-opt) (3.1.3)\n", + "Requirement already satisfied: huggingface-hub<1.0,>=0.16.4 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from tokenizers->litellm==1.75.0->trace-opt) (0.34.4)\n", + "Requirement already satisfied: filelock in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from huggingface-hub<1.0,>=0.16.4->tokenizers->litellm==1.75.0->trace-opt) (3.18.0)\n", + "Requirement already satisfied: fsspec>=2023.5.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from huggingface-hub<1.0,>=0.16.4->tokenizers->litellm==1.75.0->trace-opt) (2025.3.0)\n", + "Requirement already satisfied: pyyaml>=5.1 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from huggingface-hub<1.0,>=0.16.4->tokenizers->litellm==1.75.0->trace-opt) (6.0.2)\n", + "Requirement already satisfied: hf-xet<2.0.0,>=1.1.3 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages (from huggingface-hub<1.0,>=0.16.4->tokenizers->litellm==1.75.0->trace-opt) (1.1.7)\n", + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], "source": [ "%pip install trace-opt ipywidgets" ] @@ -33,9 +135,52 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "5d4ed861bfd64500ab0b69255a1deb42", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Text(value='OPENAI_API_KEY', description='Env Name:', placeholder='Enter env variable name (e.g., MY_API_KEY)'…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "940a64ceb5c94d78814a1d04b091d7a1", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Password(description='API Key:', placeholder='Enter your API key')" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "299f9b405c16441fa5b13a62d8982879", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Button(description='Set API Key', style=ButtonStyle())" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "import os\n", "import ipywidgets as widgets\n", @@ -93,17 +238,9 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 6, "metadata": {}, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages/flaml/__init__.py:20: UserWarning: flaml.automl is not available. Please install flaml[automl] to enable AutoML functionalities.\n", - " warnings.warn(\"flaml.automl is not available. Please install flaml[automl] to enable AutoML functionalities.\")\n" - ] - }, { "name": "stdout", "output_type": "stream", @@ -118,7 +255,7 @@ "import datasets\n", "import numpy as np\n", "from typing import Any, Tuple\n", - "from opto.trainer.guide import AutoGuide\n", + "from opto.trainer.guide import Guide, LLMJudge\n", "from opto.utils.llm import LLM\n", "\n", "# Set random seed\n", @@ -141,87 +278,33 @@ "print(f\"Validation samples: {len(validate_dataset['inputs'])}\")\n", "print(f\"Test samples: {len(test_dataset['inputs'])}\")\n", "\n", - "\n", - "class TeacherGuide(AutoGuide):\n", - " \"\"\"Guide that uses LLM to judge answers and provide feedback.\"\"\"\n", - " \n", - " def __init__(self, model: str = \"gpt-4o-mini\"):\n", - " \"\"\"Initialize the teacher guide.\n", - " \n", - " Args:\n", - " model: The LLM model to use for evaluation\n", - " \"\"\"\n", - " super().__init__()\n", - " self.guide_llm = LLM(model=model)\n", - " self.system_prompt = \"You are an expert math teacher evaluating student answers.\"\n", - " self.judge_prompt_template = (\n", - " \"Carefully review the following three distinct sections:\\n\\n\"\n", - " \"SECTION 1: The Math Problem\\n\"\n", - " \"----------------------------\\n\"\n", - " \"{query}\\n\"\n", - " \"----------------------------\\n\\n\"\n", - " \"SECTION 2: The Student's Full Answer\\n\"\n", - " \"----------------------------\\n\"\n", - " \"{response}\\n\"\n", - " \"----------------------------\\n\\n\"\n", - " \"SECTION 3: The Official Correct Answer\\n\"\n", - " \"----------------------------\\n\"\n", - " \"{reference}\\n\"\n", - " \"----------------------------\\n\\n\"\n", - " \"INSTRUCTIONS FOR JUDGING:\\n\"\n", - " \"1. Your primary task is to compare the student's **final numerical result** (or final conclusion if no number is present) from SECTION 2 with the **Official Correct Answer** provided in SECTION 3.\\n\"\n", - " \"2. When evaluating SECTION 2 (Student's Full Answer), focus SOLELY on the **final answer part** of the student's response. Ignore all intermediate steps, reasoning, or explanations for the correctness check unless the problem specifically asks for reasoning as the final answer.\\n\"\n", - " \"3. Determine if the student's **final answer** is equivalent to the **Official Correct Answer**.\\n\\n\"\n", - " \"RESPONSE FORMAT:\\n\"\n", - " \"- If the student's final answer (from SECTION 2) IS equivalent to the Official Correct Answer (from SECTION 3), respond ONLY with the exact phrase: 'Correct [TERMINATE]'\\n\"\n", - " \"- If the student's final answer IS NOT equivalent, respond ONLY with specific and actionable feedback. The feedback should clearly explain the error in the student's final answer and guide them on how to arrive at the Official Correct Answer.\"\n", - " )\n", - "\n", - " def get_feedback(self, task: str, response: str, info: Any, **kwargs) -> Tuple[float, str]:\n", - " \"\"\"Get feedback on a student response.\n", - " \n", - " Args:\n", - " task: The original math problem\n", - " response: The student's answer\n", - " info: The reference/correct answer\n", - " **kwargs: Additional arguments\n", - " \n", - " Returns:\n", - " Tuple of (score, feedback_text)\n", - " \"\"\"\n", - " user_prompt = self.judge_prompt_template.format(\n", - " query=task,\n", - " response=response,\n", - " reference=info\n", - " )\n", - "\n", - " messages = [\n", - " {\"role\": \"system\", \"content\": self.system_prompt},\n", - " {\"role\": \"user\", \"content\": user_prompt}\n", - " ]\n", - "\n", - " llm_response = self.guide_llm(messages=messages)\n", - " feedback_text = llm_response.choices[0].message.content\n", - "\n", - " if 'Correct [TERMINATE]' in feedback_text:\n", - " return 1.0, \"Correct.\"\n", - " else:\n", - " return 0.0, f\"Incorrect. Feedback: {feedback_text}\"\n", - " \n", - " def metric(self, task: str, content: str, info: Any, **kwargs) -> float:\n", - " \"\"\"Calculate the metric score for an answer.\n", - " \n", - " Args:\n", - " task: The original math problem\n", - " content: The student's answer\n", - " info: The reference/correct answer\n", - " **kwargs: Additional arguments\n", - " \n", - " Returns:\n", - " Score (0.0 or 1.0)\n", - " \"\"\"\n", - " score, _ = self.get_feedback(task, content, info, **kwargs)\n", - " return score" + "# Use the built-in LLMJudge instead of creating a custom TeacherGuide\n", + "math_judge = LLMJudge(\n", + " model=\"gpt-4o-mini\",\n", + " prompt_template=(\n", + " \"Carefully review the following three distinct sections:\\n\\n\"\n", + " \"SECTION 1: The Math Problem\\n\"\n", + " \"----------------------------\\n\"\n", + " \"{query}\\n\"\n", + " \"----------------------------\\n\\n\"\n", + " \"SECTION 2: The Student's Full Answer\\n\"\n", + " \"----------------------------\\n\"\n", + " \"{response}\\n\"\n", + " \"----------------------------\\n\\n\"\n", + " \"SECTION 3: The Official Correct Answer\\n\"\n", + " \"----------------------------\\n\"\n", + " \"{reference}\\n\"\n", + " \"----------------------------\\n\\n\"\n", + " \"INSTRUCTIONS FOR JUDGING:\\n\"\n", + " \"1. Your primary task is to compare the student's **final numerical result** (or final conclusion if no number is present) from SECTION 2 with the **Official Correct Answer** provided in SECTION 3.\\n\"\n", + " \"2. When evaluating SECTION 2 (Student's Full Answer), focus SOLELY on the **final answer part** of the student's response. Ignore all intermediate steps, reasoning, or explanations for the correctness check unless the problem specifically asks for reasoning as the final answer.\\n\"\n", + " \"3. Determine if the student's **final answer** is equivalent to the **Official Correct Answer**.\\n\\n\"\n", + " \"RESPONSE FORMAT:\\n\"\n", + " \"- If the student's final answer (from SECTION 2) IS equivalent to the Official Correct Answer (from SECTION 3), respond ONLY with the exact phrase: '{correctness_template}'\\n\"\n", + " \"- If the student's final answer IS NOT equivalent, respond ONLY with '{incorrectness_template}' and provide specific and actionable feedback. The feedback should clearly explain the error in the student's final answer and guide them on how to arrive at the Official Correct Answer.\"\n", + " ),\n", + " system_prompt=\"You are an expert math teacher evaluating student answers.\"\n", + ")" ] }, { @@ -233,69 +316,20 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "from opto import trace\n", + "from opto import trace, trainer\n", "from opto.optimizers import OptoPrime\n", "from opto.optimizers.utils import print_color\n", - "from opto.trace.modules import Module\n", "from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm, BasicSearchAlgorithm\n", "from opto.trainer.algorithms.beamsearch_algorithm import BeamsearchAlgorithm, BeamsearchHistoryAlgorithm\n", "from opto.trainer.algorithms.UCBsearch import UCBSearchAlgorithm\n", + "from opto.features.predefined_agents import BasicLearner\n", "\n", - "\n", - "@trace.model\n", - "class Learner(Module):\n", - " \"\"\"A basic LLM Agent for solving math problems.\"\"\"\n", - " \n", - " def __init__(self, \n", - " system_prompt: str = \"You're a helpful agent answering math problems.\",\n", - " user_prompt_template: str = \"Solve the following math problem step-by-step: {message}\",\n", - " llm: LLM = None):\n", - " \"\"\"Initialize the learner agent.\n", - " \n", - " Args:\n", - " system_prompt: System prompt to guide LLM behavior\n", - " user_prompt_template: Template for formatting user messages\n", - " llm: LLM instance to use for generation (defaults to gpt-3.5-turbo)\n", - " \"\"\"\n", - " super().__init__()\n", - " self.system_prompt = trace.node(system_prompt, trainable=True)\n", - " self.user_prompt_template = trace.node(user_prompt_template, trainable=True)\n", - " self.llm = llm or LLM(model=\"gpt-3.5-turbo\")\n", - "\n", - " @trace.bundle()\n", - " def call_llm(self, system_prompt: str, user_prompt: str) -> str:\n", - " \"\"\"Call LLM model with the given prompts.\n", - " \n", - " Args:\n", - " system_prompt: The system prompt\n", - " user_prompt: The user prompt\n", - " \n", - " Returns:\n", - " The LLM response content\n", - " \"\"\"\n", - " response = self.llm(\n", - " messages=[\n", - " {\"role\": \"system\", \"content\": system_prompt},\n", - " {\"role\": \"user\", \"content\": user_prompt}\n", - " ]\n", - " )\n", - " return response.choices[0].message.content\n", - "\n", - " def forward(self, message: Any) -> str:\n", - " \"\"\"Agent's forward pass to process a message.\n", - " \n", - " Args:\n", - " message: The input message to process\n", - " \n", - " Returns:\n", - " The generated response\n", - " \"\"\" \n", - " user_prompt = self.user_prompt_template.format(message=message)\n", - " return self.call_llm(self.system_prompt, user_prompt)\n" + "# Create alias for backward compatibility in this tutorial\n", + "Learner = BasicLearner" ] }, { @@ -307,15 +341,16 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "student_llm = LLM()\n", "agent = Learner(llm=student_llm)\n", "\n", - "train_guide = TeacherGuide()\n", - "validate_guide = TeacherGuide()\n", + "# Use the LLMJudge we created above for both training and validation\n", + "train_guide = math_judge\n", + "validate_guide = math_judge\n", "\n", "optimizer = OptoPrime(agent.parameters())\n", "\n", @@ -383,7 +418,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 9, "metadata": {}, "outputs": [ { @@ -397,56 +432,110 @@ "name": "stderr", "output_type": "stream", "text": [ - "Evaluating agent (iteration 0): 100%|██████████| 10/10 [00:52<00:00, 5.26s/it]\n" + "Evaluating agent (iteration 0): 100%|██████████| 10/10 [00:49<00:00, 4.94s/it]\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ - "[Step 0] \u001b[92mAverage test score: 0.4\u001b[0m\n" + "[Step 0] \u001b[92mAverage test score: 0.1\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:30<00:00, 6.05s/it]\n", - "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:52<00:00, 10.40s/it]\n", - "Evaluating agent (iteration 2): 100%|██████████| 10/10 [00:50<00:00, 5.06s/it]\n" + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:25<00:00, 5.12s/it]\n", + "Checking improvement (iteration 0): 100%|██████████| 5/5 [00:32<00:00, 6.41s/it]\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ - "[Step 2] \u001b[92mAverage test score: 0.2\u001b[0m\n", + "\u001b[91mUpdate rejected: Current score 0.0, New score 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:29<00:00, 5.99s/it]\n", + "Checking improvement (iteration 1): 100%|██████████| 5/5 [00:29<00:00, 5.99s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[92mUpdate accepted: Current score 0.0, New score 0.2\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating agent (iteration 2): 100%|██████████| 10/10 [00:58<00:00, 5.87s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 2] \u001b[92mAverage test score: 0.1\u001b[0m\n", "Epoch: 0. Iteration: 2\n", - "[Step 2] Average train score: 0.2\n", - "[Step 2] \u001b[91mParameter: str:0: You're a helpful agent assisting with thorough and complete mathematical problem analysis, ensuring all steps are accurately validated.\u001b[0m\n", - "[Step 2] \u001b[91mParameter: str:1: Carefully process each subcomponent of the following problem: {message} Methodically ensure completeness in probability calculations, permutations, customizable solutions, and systematic explorations of possible outcomes.\u001b[0m\n" + "[Step 2] Average train score: 0.0\n", + "[Step 2] \u001b[91mParameter: str:0: You're a precise problem-solver. Ensure you analyze each query with rigorous logic, considering constraints and combinatorial properties accurately.\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:49<00:00, 9.88s/it]\n", - "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:28<00:00, 5.64s/it]\n", - "Evaluating agent (iteration 4): 100%|██████████| 10/10 [01:01<00:00, 6.10s/it]" + "Forward pass (batch size: 5): 100%|██████████| 5/5 [01:00<00:00, 12.01s/it]\n", + "Checking improvement (iteration 2): 100%|██████████| 5/5 [00:28<00:00, 5.65s/it]\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ - "[Step 4] \u001b[92mAverage test score: 0.2\u001b[0m\n", + "\u001b[91mUpdate rejected: Current score 0.0, New score 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:30<00:00, 6.15s/it]\n", + "Checking improvement (iteration 3): 100%|██████████| 5/5 [00:26<00:00, 5.23s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[92mUpdate accepted: Current score 0.2, New score 0.4\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating agent (iteration 4): 100%|██████████| 10/10 [00:46<00:00, 4.62s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 4] \u001b[92mAverage test score: 0.4\u001b[0m\n", "Epoch: 0. Iteration: 4\n", - "[Step 4] Average train score: 0.2\n", - "[Step 4] \u001b[91mParameter: str:0: Accurate precision ensuring number coating and span impart cataloguing upon probability, permutation, solution synthesis, and structured exploration\u001b[0m\n", - "[Step 4] \u001b[91mParameter: str:1: Diligently analyze each part facet of the offering issue: {message} carefuly ascertain completion in probability computation, permutation exercise, customizable provides solution, and scheme sized explorable outcomes.\u001b[0m\n", + "[Step 4] Average train score: 0.05\n", + "[Step 4] \u001b[91mParameter: str:0: Adjusting calculations largely hinges on understanding exact component replacements or alternative setup checklists ('For each model', enrich sequential rotation-class opportunity collection), correcting calculations based on feedback: Producing combinations correctly and preferring direct statistically significant outcomes.\u001b[0m\n", "FINISHED TRAINING MINIBATCH\n", - "Final score: 0.2\n" + "Final score: 0.4\n" ] }, { @@ -476,7 +565,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 10, "metadata": {}, "outputs": [ { @@ -490,7 +579,14 @@ "name": "stderr", "output_type": "stream", "text": [ - "Evaluating agent (iteration 0): 100%|██████████| 10/10 [01:06<00:00, 6.63s/it]\n" + "Evaluating agent (iteration 0): 0%| | 0/10 [00:00\n", + "The instruction requires us to modify the variables in #Variables to improve the output based on the feedback. The feedback indicates issues with incorrect answers generated by the agent. Specifically, the feedback involves misunderstandings in solving math problems, where correct answers depend on understanding problem constraints and calculations.\n", + "\n", + "The #Code uses `str188` as the `system_prompt` for the `BasicLearner.model` function calls. This prompt is essential in guiding the model's behavior and approach when addressing the queries. Currently, `str188` is set to: \"You're a helpful agent answering math problems.\" From the feedback, it is evident that the answers provided do not meet expectations, so modifying this prompt could help guide the model to process tasks correctly.\n", + "\n", + "By making `str188` more specific in terms of expectations and processing steps, we can direct the model to adopt a more meticulous approach to these mathematical problems. The current prompt might be too generic and not set clear problem-solving guidelines.\n", + "\n", + "To address the feedback issues, the `str188` prompt could be revised to instruct the agent to carefully analyze problem constraints, check calculations, and consider alternate solution methods. Encouraging the model to cross-verify its computations and reason through each step could lead to improved outcomes.\n", + "\n", + "\n", + "str188\n", + "\n", + "You're a thorough agent solving math problems. Double-check calculations and ensure all problem constraints are considered for accurate results.\n", + "\n", + "\n", + "\n", + "```\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Checking improvement (iteration 0): 100%|██████████| 5/5 [00:20<00:00, 4.14s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[91mUpdate rejected: Current score 0.0, New score 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:37<00:00, 7.45s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " ```\n", + "\n", + "\n", + "The problem requires modifications to the `str188` variable to address incorrect answers generated by the BasicLearner models for math problems. `str188` is currently set to \"You're a helpful agent answering math problems,\" which provides a basic directive to the agent. The feedback indicates that in multiple instances, the student miscalculated various mathematical problems due to misunderstanding problem conditions or failing to consider necessary combinations.\n", + "\n", + "The solution involves making the `system_prompt` more instructive to guide the math problem-solving process more accurately. The current prompt doesn’t provide specific instructions or context that could lead the agent to understand or solve the problem in a step-by-step manner. An improved prompt could explicitly request thorough steps or careful verification of different conditions.\n", + "\n", + "Therefore, updating `str188` to provide a more detailed instruction that encourages careful calculation, verification, and step-by-step reasoning may help in improving the model's response, potentially leading to correct answers.\n", + "\n", + "\n", + "str188\n", + "\n", + "You're a helpful agent solving math problems. Break down each problem into step-by-step solutions, double-check your calculations, and verify conditions before providing the final answer.\n", + "\n", + "\n", + "\n", + "```\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Checking improvement (iteration 1): 100%|██████████| 5/5 [00:19<00:00, 3.93s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[91mUpdate rejected: Current score 0.2, New score 0.2\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating agent (iteration 2): 100%|██████████| 10/10 [00:36<00:00, 3.65s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 2] \u001b[92mAverage test score: 0.1\u001b[0m\n", + "Epoch: 0. Iteration: 2\n", + "[Step 2] Instantaneous train score: 0.2\n", + "[Step 2] Average train score: 0.1\n", + "[Step 2] \u001b[91mParameter: str:188: You're a helpful agent answering math problems.\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:52<00:00, 10.46s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " ```\n", + "\n", + "\n", + "The instruction asks us to alter the `value` of variables in `#Variables` to address the performance issues indicated in `#Feedback`. The variable in question is `str188`, which serves as the `system_prompt`. In the context of the provided code, the system prompt can shape the LLM agent's responses. The feedback indicates multiple incorrect problem solutions with miscalculations or application errors related to specific mathematical problems such as probability, counting, and combinatorial calculations.\n", + "\n", + "The system prompt \"You're a helpful agent answering math problems.\" is vague and only establishes a general behavior without specific guidance aligned with the type of tasks being processed. The intent for a system prompt is to fine-tune the instructions to better guide the model. Consequently, we could benefit from delivering more focused guidance. \n", + "\n", + "Given the nature of the problems in the feedback and the need for accurate, step-by-step math problem-solving, the system prompt could be refined to urge focus on step-by-step logical thought processes. Thus, the suggested new system prompt should emphasize detailed analytical methods needed for math problems, hitting on key cognitive processes such as analysis, pattern recognition, and logical evaluation.\n", + "\n", + "With these considerations, I suggest updating `str188` to a more specific format to direct the agent's responses toward detailed, logical mathematical reasoning.\n", + "\n", + "\n", + "str188\n", + "\n", + "As a math problem-solving agent, break down problems step-by-step, emphasizing logical reasoning, pattern recognition, and detailed solution analysis.\n", + "\n", + "\n", + "\n", + "```\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Checking improvement (iteration 2): 100%|██████████| 5/5 [00:31<00:00, 6.36s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[91mUpdate rejected: Current score 0.0, New score 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:32<00:00, 6.41s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " ```\n", + "\n", + "\n", + "The `str188` variable, a system prompt saying \"You're a helpful agent answering math problems,\" serves to define the role of the agent for providing mathematical solutions. The feedback indicates that the current outputs are incorrect answers to the math problems the agent tackled. Improving the output requires altering the prompt in a way that might better guide or inform the learning algorithm (or decision-making process) on how to approach mathematical problems with correct logic or consideration.\n", + "\n", + "Considering #Feedback, errors occur because correct logic or systematic calculation wasn't applied. Modifying `str188` to insist on accuracy, methodical calculation, or reasoning logic could guide better answers.\n", + "\n", + "Hence, updating `str188` to highlight logical reasoning would be sensible.\n", + "\n", + "\n", + "str188\n", + "\n", + "You're a logical and methodical math problem-solving agent. Focus on accurate calculations and reasoning.\n", + "\n", + "\n", + "\n", + "```\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Checking improvement (iteration 3): 100%|██████████| 5/5 [00:35<00:00, 7.07s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[92mUpdate accepted: Current score 0.0, New score 0.2\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating agent (iteration 4): 100%|██████████| 10/10 [00:47<00:00, 4.78s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 4] \u001b[92mAverage test score: 0.2\u001b[0m\n", + "Epoch: 0. Iteration: 4\n", + "[Step 4] Instantaneous train score: 0.0\n", + "[Step 4] Average train score: 0.05\n", + "[Step 4] \u001b[91mParameter: str:188: You're a logical and methodical math problem-solving agent. Focus on accurate calculations and reasoning.\u001b[0m\n", + "FINISHED SIMPLIFIED TRAINING\n", + "Final score: 0.2\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "# Using the simplified trainer.train approach\n", + "from opto import trainer\n", + "\n", + "# Create a fresh agent for simplified training\n", + "simple_agent = BasicLearner(\n", + " system_prompt=\"You're a helpful agent answering math problems.\",\n", + " user_prompt_template=\"Solve the following math problem step-by-step: {message}\",\n", + " llm=LLM()\n", + ")\n", + "\n", + "# Run MinibatchAlgorithm using trainer.train\n", + "print(\"STARTING SIMPLIFIED TRAINING\")\n", + "metrics, final_score = trainer.train(\n", + " model=simple_agent,\n", + " train_dataset=train_dataset,\n", + " algorithm='MinibatchAlgorithm',\n", + " guide=math_judge, # Use the same LLMJudge we created earlier\n", + " # trainer kwargs\n", + " num_epochs=1,\n", + " batch_size=5,\n", + " eval_frequency=2,\n", + " test_dataset=test_dataset,\n", + " num_threads=5,\n", + " verbose='output',\n", + ")\n", + "print(\"FINISHED SIMPLIFIED TRAINING\")\n", + "print(f\"Final score: {final_score}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Simplified Training with `trainer.train()`\n", + "\n", + "Instead of manually setting up the algorithm, optimizer, guide, and logger, you can use the simplified `trainer.train()` function that handles all the setup for you. This is the recommended approach for most use cases.\n", + "\n", + "The `trainer.train()` function:\n", + "- Automatically selects the appropriate optimizer based on your model type\n", + "- Uses sensible defaults for guide and logger\n", + "- Provides a clean, unified interface for all training algorithms\n", + "- Reduces boilerplate code significantly\n", + "\n", + "Let's see some examples:" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "==================================================\n", + "TRAINING WITH BASIC SEARCH ALGORITHM\n", + "==================================================\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating agent (iteration 0): 100%|██████████| 10/10 [01:01<00:00, 6.13s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 0] \u001b[92mAverage test score: 0.3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 3): 100%|██████████| 3/3 [00:26<00:00, 8.73s/it]\n", + "Generating 2 proposals: 100%|██████████| 2/2 [00:05<00:00, 2.74s/it]\n", + "Validating proposals: 100%|██████████| 20/20 [02:31<00:00, 7.58s/it]\n", + "Validating proposals: 100%|██████████| 20/20 [02:49<00:00, 8.48s/it]\n", + "Validating proposals: 100%|██████████| 20/20 [02:54<00:00, 8.73s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 0] \u001b[92mValidation score: 0.25\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Checking improvement (iteration 0): 100%|██████████| 3/3 [00:31<00:00, 10.48s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[91mUpdate rejected: Current score 0.0, New score 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating agent (iteration 1): 100%|██████████| 10/10 [01:02<00:00, 6.27s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 1] \u001b[92mAverage test score: 0.2\u001b[0m\n", + "Epoch: 0. Iteration: 1\n", + "[Step 1] Instantaneous train score: 0.0\n", + "[Step 1] Average train score: 0.0\n", + "[Step 1] \u001b[91mParameter: str:214: You're a math tutor providing step-by-step solutions.\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 3): 100%|██████████| 3/3 [00:26<00:00, 8.74s/it]\n", + "Generating 2 proposals: 100%|██████████| 2/2 [00:07<00:00, 3.97s/it]\n", + "Validating proposals: 100%|██████████| 20/20 [03:14<00:00, 9.72s/it]\n", + "Validating proposals: 100%|██████████| 20/20 [02:44<00:00, 8.21s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 1] \u001b[92mValidation score: 0.25\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating agent (iteration 2): 100%|██████████| 10/10 [01:01<00:00, 6.18s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 2] \u001b[92mAverage test score: 0.2\u001b[0m\n", + "Epoch: 0. Iteration: 2\n", + "[Step 2] Instantaneous train score: 0.3333333333333333\n", + "[Step 2] Average train score: 0.16666666666666666\n", + "[Step 2] \u001b[91mParameter: str:214: You're a math tutor providing step-by-step solutions.\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 3): 100%|██████████| 3/3 [00:42<00:00, 14.13s/it]\n", + "Generating 2 proposals: 100%|██████████| 2/2 [00:06<00:00, 3.17s/it]\n", + "Validating proposals: 100%|██████████| 20/20 [02:29<00:00, 7.46s/it]\n", + "Validating proposals: 100%|██████████| 20/20 [02:24<00:00, 7.24s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 2] \u001b[92mValidation score: 0.25\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating agent (iteration 3): 100%|██████████| 10/10 [01:02<00:00, 6.30s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 3] \u001b[92mAverage test score: 0.1\u001b[0m\n", + "Epoch: 0. Iteration: 3\n", + "[Step 3] Instantaneous train score: 0.0\n", + "[Step 3] Average train score: 0.1111111111111111\n", + "[Step 3] \u001b[91mParameter: str:214: You're a math tutor providing step-by-step solutions.\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 3): 100%|██████████| 3/3 [00:37<00:00, 12.47s/it]\n", + "Generating 2 proposals: 100%|██████████| 2/2 [00:07<00:00, 3.80s/it]\n", + "Validating proposals: 100%|██████████| 20/20 [02:50<00:00, 8.53s/it]\n", + "Validating proposals: 100%|██████████| 20/20 [02:43<00:00, 8.16s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 3] \u001b[92mValidation score: 0.25\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating agent (iteration 4): 100%|██████████| 10/10 [01:00<00:00, 6.04s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 4] \u001b[92mAverage test score: 0.2\u001b[0m\n", + "Epoch: 0. Iteration: 4\n", + "[Step 4] Instantaneous train score: 0.0\n", + "[Step 4] Average train score: 0.08333333333333333\n", + "[Step 4] \u001b[91mParameter: str:214: You're a math tutor providing step-by-step solutions.\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 3): 100%|██████████| 3/3 [00:40<00:00, 13.44s/it]\n", + "Generating 2 proposals: 100%|██████████| 2/2 [00:07<00:00, 3.70s/it]\n", + "Validating proposals: 100%|██████████| 20/20 [02:52<00:00, 8.61s/it]\n", + "Validating proposals: 100%|██████████| 20/20 [02:53<00:00, 8.69s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 4] \u001b[92mValidation score: 0.25\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating agent (iteration 5): 100%|██████████| 10/10 [00:52<00:00, 5.27s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 5] \u001b[92mAverage test score: 0.4\u001b[0m\n", + "Epoch: 0. Iteration: 5\n", + "[Step 5] Instantaneous train score: 0.0\n", + "[Step 5] Average train score: 0.06666666666666667\n", + "[Step 5] \u001b[91mParameter: str:214: You're a math tutor providing step-by-step solutions.\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 3): 100%|██████████| 3/3 [00:21<00:00, 7.21s/it]\n", + "Generating 2 proposals: 100%|██████████| 2/2 [00:07<00:00, 3.69s/it]\n", + "Validating proposals: 100%|██████████| 20/20 [02:38<00:00, 7.90s/it]\n", + "Validating proposals: 100%|██████████| 20/20 [02:39<00:00, 7.95s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 5] \u001b[92mValidation score: 0.25\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating agent (iteration 6): 100%|██████████| 10/10 [01:09<00:00, 6.93s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 6] \u001b[92mAverage test score: 0.2\u001b[0m\n", + "Epoch: 0. Iteration: 6\n", + "[Step 6] Instantaneous train score: 0.0\n", + "[Step 6] Average train score: 0.05555555555555555\n", + "[Step 6] \u001b[91mParameter: str:214: You're a math tutor providing step-by-step solutions.\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 2): 100%|██████████| 2/2 [00:20<00:00, 10.20s/it]\n", + "Generating 2 proposals: 100%|██████████| 2/2 [00:07<00:00, 3.59s/it]\n", + "Validating proposals: 100%|██████████| 20/20 [02:53<00:00, 8.69s/it]\n", + "Validating proposals: 100%|██████████| 20/20 [02:39<00:00, 7.95s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 6] \u001b[92mValidation score: 0.25\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating agent (iteration 7): 100%|██████████| 10/10 [00:57<00:00, 5.71s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 7] \u001b[92mAverage test score: 0.3\u001b[0m\n", + "Epoch: 0. Iteration: 7\n", + "[Step 7] Instantaneous train score: 0.5\n", + "[Step 7] Average train score: 0.11904761904761904\n", + "[Step 7] \u001b[91mParameter: str:214: You're a math tutor providing step-by-step solutions.\u001b[0m\n", + "Basic Search final score: 0.3\n", + "==================================================\n", + "TRAINING WITH BEAM SEARCH ALGORITHM\n", + "==================================================\n", + "\u001b[94mRunning BeamsearchAlgorithm with beam_width=2, max_depth=2\u001b[0m\n", + "\u001b[94mUsing validation_dataset_size=5 for intermediate evaluations\u001b[0m\n", + "\u001b[94m\n", + "===== Evaluating Initial Parameters =====\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating initial parameters on test set: 100%|██████████| 10/10 [01:05<00:00, 6.53s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[93mInitial test score: 0.3000\u001b[0m\n", + "[Step 0] \u001b[94mInitial test score: 0.3\u001b[0m\n", + "\u001b[94m\n", + "===== Beam Search Depth 1/2 with 1 beams =====\u001b[0m\n", + "\u001b[96mSampled validation minibatch of size 5 for depth 1\u001b[0m\n", + "\u001b[93mProcessing beam 1/1\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 3): 100%|██████████| 3/3 [00:32<00:00, 10.90s/it]\n", + "Generating 4 proposals for beam 1: 25%|██▌ | 1/4 [00:05<00:16, 5.66s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " ```xml\n", + "\n", + "The #Instruction requires changing the `value` of the variable `str243` to improve the output according to the #Feedback provided. In this scenario, `str243` is the system prompt that is given to the models as they process the messages. The #Feedback indicates that all three answers are incorrect due to misunderstanding the combinatorial principles necessary for the calculations and the related mathematical concept. \n", + "\n", + "The #Feedback suggests each question requires a more nuanced understanding of combinatorial mathematics and possibly a deeper explanation of Pascal's Triangle. The current value of `str243` (\"You are an expert mathematician.\") might not be providing sufficient context or guidance for the model to produce the correct output. \n", + "\n", + "A possible refinement to the prompt may involve being more explicit about the type of mathematical reasoning to apply, such as focusing specifically on combinatorics, permutations, or patterns within a sequence. This change should help align the model's focus and improve its performance.\n", + "\n", + "\n", + "str243\n", + "\n", + "You are an expert in combinatorial mathematics. Focus on combinatorial reasoning and detailed explanations for solving the problems.\n", + "\n", + "\n", + "```\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 4 proposals for beam 1: 50%|█████ | 2/4 [00:06<00:05, 2.81s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " ```\n", + "\n", + "The instruction requires improving the output according to the feedback provided. The code involves calling an LLM model with parameters including a system prompt (`str243`). Based on the feedback, the current output is incorrect because of errors in mathematical reasoning and logic when answering complex problems. The system prompt currently designates the agent as an \"expert mathematician,\" but the feedback shows errors in combinatorial reasoning, identifying correct numbers in Pascal's triangle, and accurate probability calculations.\n", + "\n", + "To address this, the system prompt can be enhanced to encourage more careful and detailed analysis of combinatorial and mathematical problems. This can be achieved by including guidance on breaking down problems, emphasizing verification of intermediate calculations, or specifying additional competencies. However, the problems are strictly mathematical, and the title \"expert mathematician\" aligns with the intention; thus, improvements might not directly link to the system prompt but to internal logic management by the AI model. Nonetheless, a modified prompt might incline the model more towards using thorough mathematical deductions as intended.\n", + "\n", + "Since the system prompt is the only variable to adjust, we could add a line that encourages reconciling calculations with examples from known mathematical principles or explicitly prompts verifying answers against known outcomes.\n", + "\n", + "\n", + "str243\n", + "\n", + "You are an expert mathematician. Reconcile your calculations with principles of combinatorics and ensure your final answers align with known math outcomes through example alignment.\n", + "\n", + "\n", + "```\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 4 proposals for beam 1: 75%|███████▌ | 3/4 [00:06<00:01, 1.71s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " ```xml\n", + "\n", + "The task is to modify the variable values in #Variables to improve the output according to the #Feedback provided. The issue pertains to incorrect answers for mathematical problems being generated by the LLM model. Specifically, #Feedback reveals misconceptions in combinatorial logic and placement of numbers, leading to errors in final numbers.\n", + "\n", + "The variable `str243` is a system prompt stating \"You are an expert mathematician.\" The current prompt might not be adequately influencing the model to produce accurate mathematical reasoning or calculations. \n", + "\n", + "To potentially improve the model's performance, the system prompt `str243` can be extended to provide clearer guidance or an explicit request to check intermediate steps and outcomes. This might help the model adhere more closely to correct mathematical logic and comprehension, as suggested by the detailed feedback given. Enhancing \"expert\" with specific mathematical reasoning or concept requests could better steer model responses towards accuracy. Testing different articulations or specifying a structure for the solution can help guide the model's logical process.\n", + "\n", + "\n", + "str243\n", + "\n", + "You are an expert mathematician. Please ensure all mathematical reasoning steps are clearly outlined and verified, with attention to combinatorial calculations and number placements.\n", + "\n", + "\n", + "```\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 4 proposals for beam 1: 100%|██████████| 4/4 [00:11<00:00, 2.87s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " ```xml\n", + "\n", + "1. The #Instruction requires adjusting the variable values to improve the output based on #Feedback.\n", + "2. The #Feedback specifies that the answers in the output (ID [0], ID [1], ID [2]) are all incorrect due to misunderstandings of the problem requirements and combinatorial logic errors.\n", + "3. The variable `str243` currently has the value \"You are an expert mathematician.\" This serves as the system prompt for guiding the model. However, since #Feedback highlights issues in combinatorial understanding and specific mathematical reasoning errors, a more explicit directive in `str243` may aid in enhancing the model's response. The current prompt does not explicitly instruct on leveraging combinatorial techniques or referencing known sequences like Pascal's Triangle, which are pivotal in addressing the given types of mathematical problems.\n", + "\n", + "Suggested Revision: Adding guidance for using combinatorial reasoning and emphasizing known results or sequences (e.g., Pascal's Triangle) could improve responses:\n", + "```python\n", + "\"You are an expert mathematician. Use advanced combinatorial techniques and refer to established sequences, such as Pascal's Triangle, to solve mathematical problems accurately.\"\n", + "```\n", + "\n", + "This modification aligns with addressing the critical points highlighted in the #Feedback by providing an explicit directive, potentially improving the output towards the desired results.\n", + "\n", + "\n", + "str243\n", + "\n", + "You are an expert mathematician. Use advanced combinatorial techniques and refer to established sequences, such as Pascal's Triangle, to solve mathematical problems accurately.\n", + "\n", + "\n", + "```\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 1/5: 100%|██████████| 5/5 [00:42<00:00, 8.53s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 1: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 2/5: 100%|██████████| 5/5 [00:56<00:00, 11.26s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 2: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 3/5: 100%|██████████| 5/5 [00:39<00:00, 7.86s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 3: Validation score: 0.2000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 4/5: 100%|██████████| 5/5 [00:43<00:00, 8.70s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 4: Validation score: 0.2000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 5/5: 100%|██████████| 5/5 [00:44<00:00, 8.90s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 5: Validation score: 0.2000\u001b[0m\n", + "\u001b[92mSelected top 2 beams with scores: ['0.2000', '0.2000']\u001b[0m\n", + "\u001b[92mDepth 1 - Best validation score: 0.2000\u001b[0m\n", + "[Step 1] \u001b[92mBest validation score: 0.2\u001b[0m\n", + "[Step 1] Average validation score: 0.2\n", + "[Step 1] Min validation score: 0.2\n", + "[Step 1] Max validation score: 0.2\n", + "\u001b[94m\n", + "===== Beam Search Depth 2/2 with 2 beams =====\u001b[0m\n", + "\u001b[96mSampled validation minibatch of size 5 for depth 2\u001b[0m\n", + "\u001b[93mProcessing beam 1/2\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 3): 100%|██████████| 3/3 [00:37<00:00, 12.35s/it]\n", + "Generating 4 proposals for beam 1: 25%|██▌ | 1/4 [00:07<00:23, 7.94s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " ```\n", + "\n", + "1. The instruction requires changing the 'value' of the str243 variable to improve the output, as indicated in the feedback.\n", + "2. The feedback reveals inaccuracies in the problem-solving explanations generated by the model, suggesting that the current system prompt (\"You are an expert in combinatorial mathematics...\") does not guide towards correct detailed explanations.\n", + "3. The str243 variable acts as the system prompt, aiming to set the context and guidance for the model's response generation. Modifying this prompt can enhance the guidance given to the model and resolve the outlined errors reflected in the feedback.\n", + "\n", + "Based on these insights, I propose changing str243 to more assertively direct the model towards structured, accurate problem-solving approaches. Explicitly reminding it to verify solutions before concluding would help. My suggested change is to incorporate verifying and structured approach instructions in the system prompt.\n", + "\n", + "\n", + "str243\n", + "\n", + "You are an expert in combinatorial mathematics. Focus on combinatorial reasoning and detailed explanations for solving the problems. Always verify calculations and logic at each step. Ensure to consider all possible conditions and scenarios before concluding the solution.\n", + "\n", + "\n", + "```\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 4 proposals for beam 1: 50%|█████ | 2/4 [00:09<00:08, 4.13s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " ```xml\n", + "\n", + "The instruction asks for improving the outputs according to the feedback, which means adjusting the variable values such that they address the issues pointed out. The feedback indicates that the current system prompt does not adequately prepare or guide the user for the three problems presented. The issues span from misunderstanding problem details to incorrect calculations and reasoning, specifically in probabilities and sequences. \n", + "\n", + "The \"str243\" system prompt currently emphasizes \"combinatorial mathematics\" and \"detailed explanations,\" focusing generally on problem solving. However, the feedback demonstrates that the solutions produced missed key conditions and analysis steps crucial for getting to the correct answers. Therefore, the \"str243\" prompt should be modified to be more targeted towards these specific problem types and guide the model to consider all necessary elements and conditions when reaching a solution. This includes explicitly directing the system to thoroughly analyze foundational conditions and alternative strategies in mathematical problem solving.\n", + "\n", + "The expected result of this change is that the generated solutions will have an improved analytical narrative prioritized towards necessary conditions, allowing them to align better with coveted solutions.\n", + "\n", + "\n", + "str243\n", + "\n", + "You are an expert in mathematical problem solving. Ensure thorough analysis of foundational conditions, detailed explanations of required steps, and exploration of alternative solution strategies, especially in probabilistic and sequence problems.\n", + "\n", + "\n", + "```\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 4 proposals for beam 1: 75%|███████▌ | 3/4 [00:10<00:02, 2.93s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " ```xml\n", + "\n", + "1. The #Instruction requires us to modify the `value` of the variable in #Variables to improve the outputs in relation to the #Feedback.\n", + "2. The #Feedback indicates that all three outputs are incorrect and seem to focus on miscalculations related to combinatorial reasoning.\n", + "3. The #Code initializes three models with varying input messages, all sharing the same `system_prompt` from `str243`. The system prompt currently hints at expert-level reasoning in combinatorial mathematics.\n", + "4. Given the feedback, it's apparent that the current system prompt in `str243` fails to direct the agent sufficiently to solve these problems with the necessary detailed combinatorial reasoning and error checks that ensure correct outputs.\n", + "\n", + "To address these issues, it is beneficial to adjust `str243` to emphasize not just expertise in combinatorial math but also in error-checking and validation against known outcomes or expected results to ensure fidelity in solution development.\n", + "\n", + "By including guidance in performing validation or cross-checking the student's initial outputs against established correct parameters or systematically iterating through possible combinatorial approaches, the model could improve its output accuracy. Hence, the modification could push the model to generate more precise or recalibrated answers before final submission.\n", + "\n", + "An example modification might include directives like \"Incorporate validation processes against expected results and ensure thorough error-checking at each step,\" alongside urging a check for correctness in combinatorial reasoning.\n", + "\n", + "This change aims to align the system's behavior with what's necessary to meet the targeted outputs more accurately.\n", + "\n", + "\n", + "str243\n", + "\n", + "You are an expert in combinatorial mathematics. Focus on combinatorial reasoning and detailed explanations for solving the problems. Incorporate validation processes against expected results and ensure thorough error-checking at each step.\n", + "\n", + "\n", + "```\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 4 proposals for beam 1: 100%|██████████| 4/4 [00:17<00:00, 4.50s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " ```xml\n", + "\n", + "The #Instruction requires us to adjust the variables in #Variables to improve the output as per the #Feedback given. In this instance, the system prompt stored in `str243` is associated with the agent’s behavior which can have a significant impact on the problem's resolution as highlighted in the #Documentation. The #Feedback indicates that the current solutions are incorrect, suggesting that the system prompt might not be fully directing the model's reasoning and problem-solving approach appropriately.\n", + "\n", + "The current value of `str243` encourages the model to focus on \"combinatorial reasoning and detailed explanations.\" The feedback from all models in the #Others section shows misunderstandings in mathematical and logical reasoning, indicating a probable mismatch in how the model interprets or solves the mathematical problems presented.\n", + "\n", + "To rectify this, the proposed adjustment to `str243` should encourage the model to follow structured problem-solving steps, which may improve the overall accuracy in obtaining correct answers reflected in the feedback for ID [0], ID [1], and ID [2]. Given the errors noted, focusing the prompt on both combinatorial reasoning with emphasis on validating solution steps and accounting for all elements in each scenario could improve accuracy.\n", + "\n", + "Expected Result: With modifications to `str243`, the model should be equipped to utilize deeper analysis and verification of intermediary steps to ensure correct outcomes. The adjusted system prompt should prompt the model to verify solution steps more critically, paying attention to logical consistency and ensuring each step aligns with expected mathematical principles, potentially rectifying the errors seen in the feedback.\n", + "\n", + "\n", + "str243\n", + "\n", + "You are an expert in combinatorial mathematics. When solving a problem, meticulously verify each step and ensure logical consistency by aligning with established mathematical principles. Prioritize accuracy and comprehensively validate the conditions and intermediary results for complex problems.\n", + "\n", + "\n", + "```\n", + "\u001b[93mProcessing beam 2/2\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 3): 100%|██████████| 3/3 [00:31<00:00, 10.45s/it]\n", + "Generating 4 proposals for beam 2: 25%|██▌ | 1/4 [00:08<00:25, 8.36s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " ```\n", + "\n", + "1. The instruction requests improving outputs based on feedback, focusing on the tuning of variable `str243`.\n", + "2. The feedback indicates that the problem-solving approaches and final numerical results were incorrect or incomplete for all three cases, highlighting issues with applying mathematical reasoning and calculations.\n", + "3. The variable `str243` is used as a `system_prompt`, which sets the overarching guidance for how the LLM approaches and reasons through the problems.\n", + "4. While `str243` already emphasizes expertise in using combinatorial techniques and sequences like Pascal's Triangle, it may lack directives on accurate problem-solving steps or ensuring conclusive numerical results.\n", + "5. A refinement of `str243` could focus on emphasizing step-by-step solution verification, ensuring conclusive results and accurate probability calculations.\n", + "\n", + "New suggested `str243` prompts encourage:\n", + "- Focus on deriving final numerical results by verifying each computational step.\n", + "- Enhanced problem-solving accuracy through iterative verification processes.\n", + "- Emphasized probability calculations and recursive formula applications where needed.\n", + "\n", + "This change aims to improve output accuracy by aligning the LLM's problem-solving approach closer to the feedback insights.\n", + "\n", + "\n", + "str243\n", + "\n", + "You are an expert mathematician. Use advanced combinatorial techniques and refer to established sequences, such as Pascal's Triangle, to solve mathematical problems accurately. Focus on deriving final numerical results and verifying each computational step to ensure accuracy. Explore iterative verification processes and emphasize probability calculations and recursive formula applications where needed.\n", + "\n", + "\n", + "```\n", + "LLM response:\n", + " ```\n", + "\n", + "1. The instruction requires adjusting the value of variables in order to improve the output as per the feedback provided. In this case, we have `str243` as the tunable variable. The #Outputs have not met the expectations and are producing incorrect results on a problem-solving task.\n", + " \n", + "2. The feedback indicates that the students' calculations and logical deductions do not lead to the correct answers in all three models (`BasicLearner.model1424`, `BasicLearner.model1425`, `BasicLearner.model1426`). In particular, the issue seems partly related to the reasoning process utilized by the models—something that can be impacted by the system prompt `str243`.\n", + "\n", + "3. The `system_prompt` acts as an instruction set for the agent, guiding its behavior in deriving solutions. In this case, the focus is on mathematical problem solving with combinatorial elements. To address the particular failures in reasoning, it would be helpful to either focus the prompt on the accuracy of combinatorial reasoning or stress exploration in terms of solution checking and validation. This should meet the task of generating correct numerical answers effectively.\n", + " \n", + "4. I suggest refining the system prompt to specify that while approaching combinatorial problems, they should also include thorough checks for correctness and explore all possible configurations comprehensively. Encourage a step-by-step evaluation to ensure no possibility is overlooked.\n", + "\n", + "\n", + "str243\n", + "\n", + "You are an expert mathematician. While solving mathematical problems using advanced combinatorial techniques and referencing sequences such as Pascal's Triangle, focus on verifying correctness by thoroughly exploring possible configurations. Ensure to evaluate each solution comprehensively and provide precise numerical results.\n", + "\n", + "\n", + "```\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 4 proposals for beam 2: 75%|███████▌ | 3/4 [00:10<00:02, 2.98s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " ```\n", + "\n", + "The instruction requires modification of the value of variable `str243` to improve the BatchLearner model output. The feedback indicates that none of the three model outputs contained the correct answers. The commentary suggests more detailed guidance and exploration strategies could assist in correcting the three different problem solutions. In the given `str243`, the system prompt is generalized and focuses on combinatorial techniques and established sequences, which may not sufficiently guide the model to tackle each specific mathematical problem.\n", + "\n", + "To improve the system's performance, adjusting the system prompt to address problem-specific guidance could lead to better reasoning and calculations. For instance, include prompts that direct the model to compute specific combinations, explore recursive solutions, or correct probabilistic calculations. Therefore, an adjustment can be made to offer more specific problem-solving guidance catered to the three problems, such as encouraging experimental verification (checking increments one step at a time) and utilizing mathematical rules or recursive techniques for solution verification.\n", + "\n", + "Expectation from changing `str243`: With a better-tuned system prompt that provides comprehensive problem-solving methods and heuristics, the model is more likely to process inputs accurately and produce the correct numerical results, hence improving each individual problem response.\n", + "\n", + "\n", + "str243\n", + "\n", + "You are an expert mathematician and problem solver. For combination and sequence problems, compute all potential configurations explicitly where necessary, apply recursive or iterative methods to simplify calculations, and ensure all outcomes are considered. Verify calculations through systematic exploration and reformulation using probabilistic and algebraic methods to ensure accuracy in probabilistic and combination scenarios.\n", + "\n", + "\n", + "```\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 4 proposals for beam 2: 100%|██████████| 4/4 [00:12<00:00, 3.15s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " ```xml\n", + "\n", + "The #Instruction asks to improve the model's output by adjusting the #Variables. The #Feedback indicates that the current system prompt in `str243` does not guide the agent to produce accurate numerical solutions required by the mathematical problems presented. The current prompt emphasizes using advanced combinatorial techniques, but does not specifically instruct the model to provide concrete answers.\n", + "\n", + "Given this, my suggestion is to change the system prompt `str243` to explicitly instruct the agent to not only explore the mathematical techniques but also ensure to provide final numerical answers when solving the problems. This should help address the issues outlined in the #Feedback, where the model's output fails to arrive at precise conclusions.\n", + "\n", + "\n", + "str243\n", + "\n", + "You are an expert mathematician. Use advanced combinatorial techniques and refer to established sequences, such as Pascal's Triangle, to solve mathematical problems accurately. Make sure to calculate and provide the final numerical answers for every problem you solve.\n", + "\n", + "\n", + "```\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 1/10: 100%|██████████| 5/5 [00:51<00:00, 10.22s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 1: Validation score: 0.2000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 2/10: 100%|██████████| 5/5 [00:37<00:00, 7.54s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 2: Validation score: 0.6000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 3/10: 100%|██████████| 5/5 [00:46<00:00, 9.29s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 3: Validation score: 0.2000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 4/10: 100%|██████████| 5/5 [00:38<00:00, 7.61s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 4: Validation score: 0.4000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 5/10: 100%|██████████| 5/5 [00:41<00:00, 8.23s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 5: Validation score: 0.4000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 6/10: 100%|██████████| 5/5 [00:41<00:00, 8.29s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 6: Validation score: 0.2000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 7/10: 100%|██████████| 5/5 [00:35<00:00, 7.05s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 7: Validation score: 0.6000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 8/10: 100%|██████████| 5/5 [00:39<00:00, 7.84s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 8: Validation score: 0.2000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 9/10: 100%|██████████| 5/5 [00:37<00:00, 7.45s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 9: Validation score: 0.4000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 10/10: 100%|██████████| 5/5 [01:02<00:00, 12.45s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 10: Validation score: 0.2000\u001b[0m\n", + "\u001b[92mSelected top 2 beams with scores: ['0.6000', '0.6000']\u001b[0m\n", + "\u001b[92mDepth 2 - Best validation score: 0.6000\u001b[0m\n", + "[Step 2] \u001b[92mBest validation score: 0.6\u001b[0m\n", + "[Step 2] Average validation score: 0.6\n", + "[Step 2] Min validation score: 0.6\n", + "[Step 2] Max validation score: 0.6\n", + "\u001b[94m\n", + "===== Final Selection Using Full Validation Set =====\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 1/2: 100%|██████████| 20/20 [03:14<00:00, 9.71s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 1: Validation score: 0.1000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 2/2: 100%|██████████| 20/20 [02:39<00:00, 7.97s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 2: Validation score: 0.1000\u001b[0m\n", + "\u001b[92mSelected top 1 beams with scores: ['0.1000']\u001b[0m\n", + "[Step 3] \u001b[94mFinal validation score: 0.1\u001b[0m\n", + "\u001b[95m\n", + "===== Final Proposal Candidate Parameters =====\u001b[0m\n", + "\u001b[94mstr:243: You are an expert in mathematical problem solving. Ensure thorough analysis of foundational conditions, detailed explanations of required steps, and exploration of alternative solution strategies, especially in probabilistic and sequence problems.\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating best beam on test set: 100%|██████████| 10/10 [01:03<00:00, 6.32s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[92mBEST BEAM - Test score: 0.3000\u001b[0m\n", + "[Step 3] \u001b[92mFinal test score: 0.3\u001b[0m\n", + "\u001b[94m\n", + "===== Periodic Test Scores Summary =====\u001b[0m\n", + "\u001b[96mDepth 1: Test score = 0.3000\u001b[0m\n", + "Beam Search final score: 0.3\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "# Example: Using trainer.train with different algorithms\n", + "print(\"=\"*50)\n", + "print(\"TRAINING WITH BASIC SEARCH ALGORITHM\")\n", + "print(\"=\"*50)\n", + "\n", + "# Create another fresh agent\n", + "basic_search_agent = BasicLearner(\n", + " system_prompt=\"You're a math tutor providing step-by-step solutions.\",\n", + " user_prompt_template=\"Problem: {message}\\n\\nSolution:\",\n", + " llm=LLM()\n", + ")\n", + "\n", + "metrics, final_score = trainer.train(\n", + " model=basic_search_agent,\n", + " train_dataset=train_dataset,\n", + " algorithm='BasicSearchAlgorithm',\n", + " guide=math_judge,\n", + " num_epochs=1,\n", + " batch_size=3,\n", + " num_proposals=2,\n", + " test_dataset=test_dataset,\n", + " validate_dataset=validate_dataset,\n", + " validate_guide=math_judge,\n", + " num_threads=3,\n", + ")\n", + "print(f\"Basic Search final score: {final_score}\")\n", + "\n", + "print(\"=\"*50)\n", + "print(\"TRAINING WITH BEAM SEARCH ALGORITHM\")\n", + "print(\"=\"*50)\n", + "\n", + "# Create another fresh agent for beam search\n", + "beam_search_agent = BasicLearner(\n", + " system_prompt=\"You are an expert mathematician.\",\n", + " user_prompt_template=\"Mathematical Problem: {message}\\n\\nDetailed Solution:\",\n", + " llm=LLM()\n", + ")\n", + "\n", + "metrics, final_score = trainer.train(\n", + " model=beam_search_agent,\n", + " train_dataset=train_dataset,\n", + " algorithm='BeamsearchAlgorithm',\n", + " guide=math_judge,\n", + " num_epochs=1,\n", + " batch_size=3,\n", + " beam_width=2,\n", + " max_depth=2,\n", + " validation_dataset_size=5,\n", + " test_dataset=test_dataset,\n", + " validate_dataset=validate_dataset,\n", + " validate_guide=math_judge,\n", + " num_threads=3,\n", + ")\n", + "print(f\"Beam Search final score: {final_score}\")" + ] } ], "metadata": { diff --git a/examples/async_optimization_example.py b/examples/async_optimization_example.py new file mode 100644 index 00000000..9455d93a --- /dev/null +++ b/examples/async_optimization_example.py @@ -0,0 +1,370 @@ +""" +Example demonstrating async operations and concurrent optimization in Trace. + +This example shows how to: +- Use async bundle functions for non-blocking operations +- Run concurrent optimizations with asyncio +- Handle async LLM calls efficiently +- Implement async data loading and processing +- Coordinate multiple async trace operations +""" + +import asyncio +import time +import random +from typing import List, Any +from opto.trace import node, bundle, GRAPH +from opto.trace.nodes import ParameterNode +from opto.optimizers import OptoPrime +from opto.utils.llm import AutoGenLLM + + +# Example 1: Async Bundle Functions +@bundle() +async def async_api_call(query): + """Simulate an async API call with tracing.""" + # Simulate network delay + await asyncio.sleep(random.uniform(0.1, 0.5)) + + # Simulate API response + response = f"API response for: {query}" + return response + + +@bundle() +async def async_data_processor(data): + """Process data asynchronously with tracing.""" + # Simulate CPU-bound processing + await asyncio.sleep(0.2) + + if isinstance(data, list): + return [f"Processed: {item}" for item in data] + return f"Processed: {data}" + + +# Example 2: Async Optimization Loop +class AsyncOptimizer: + """Demonstrates async optimization patterns.""" + + def __init__(self): + self.parameters = [] + self.optimizer = None + + async def initialize_parameters(self, n_params: int): + """Initialize parameters asynchronously.""" + tasks = [] + for i in range(n_params): + # Simulate async parameter initialization (e.g., from database) + async def create_param(idx): + await asyncio.sleep(0.1) + return ParameterNode( + f"initial_value_{idx}", + name=f"param_{idx}", + description=f"Parameter {idx} to optimize" + ) + tasks.append(create_param(i)) + + self.parameters = await asyncio.gather(*tasks) + print(f"Initialized {len(self.parameters)} parameters asynchronously") + return self.parameters + + async def async_forward(self, params): + """Async forward pass with multiple parameters.""" + # Run async operations concurrently + tasks = [] + for p in params: + tasks.append(async_api_call(p)) + + results = await asyncio.gather(*tasks) + return results + + async def optimize_step(self): + """Single async optimization step.""" + # Forward pass + results = await self.async_forward(self.parameters) + + # Simulate feedback computation + await asyncio.sleep(0.1) + feedback = f"Aggregated feedback from {len(results)} results" + + # Simulate parameter update + for i, param in enumerate(self.parameters): + param._data = f"updated_value_{i}_{time.time():.2f}" + + return feedback + + +# Example 3: Concurrent Trace Operations +class ConcurrentTracer: + """Demonstrates concurrent tracing patterns.""" + + @bundle() + async def fetch_data(self, source: str): + """Fetch data from a source asynchronously.""" + await asyncio.sleep(random.uniform(0.1, 0.3)) + return f"Data from {source}" + + @bundle() + async def process_batch(self, batch: List[Any]): + """Process a batch of items concurrently.""" + tasks = [] + for item in batch: + async def process_item(x): + await asyncio.sleep(0.1) + return f"Processed: {x}" + tasks.append(process_item(item)) + + results = await asyncio.gather(*tasks) + return results + + async def pipeline(self, sources: List[str]): + """Async pipeline with concurrent stages.""" + # Stage 1: Fetch data concurrently + fetch_tasks = [self.fetch_data(node(src)) for src in sources] + raw_data = await asyncio.gather(*fetch_tasks) + + # Stage 2: Process in batches + batch_size = 2 + all_results = [] + for i in range(0, len(raw_data), batch_size): + batch = raw_data[i:i+batch_size] + batch_results = await self.process_batch(batch) + all_results.extend(batch_results) + + return all_results + + +# Example 4: Async Context Manager for Tracing +class AsyncTraceContext: + """Context manager for async trace operations.""" + + def __init__(self, name: str): + self.name = name + self.start_time = None + self.operations = [] + + async def __aenter__(self): + """Enter async context.""" + self.start_time = time.time() + print(f"Starting async trace context: {self.name}") + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Exit async context.""" + elapsed = time.time() - self.start_time + print(f"Completed {self.name} in {elapsed:.2f}s") + print(f" Executed {len(self.operations)} operations") + + @bundle() + async def traced_operation(self, op_name: str, data: Any): + """Execute and trace an async operation.""" + self.operations.append(op_name) + await asyncio.sleep(0.1) + return f"Result of {op_name} on {data}" + + +# Example 5: Async Minibatch Optimization +class AsyncMinibatchOptimizer: + """Demonstrates async minibatch optimization.""" + + def __init__(self, batch_size: int = 4): + self.batch_size = batch_size + self.model_param = ParameterNode( + "initial_model", + name="model", + description="Model parameters" + ) + + @bundle() + async def process_sample(self, sample, model): + """Process a single sample asynchronously.""" + await asyncio.sleep(random.uniform(0.05, 0.15)) + # Simulate loss computation + loss = random.random() + return loss + + async def process_minibatch(self, batch): + """Process a minibatch concurrently.""" + tasks = [] + for sample in batch: + tasks.append(self.process_sample( + node(sample), + self.model_param + )) + + losses = await asyncio.gather(*tasks) + avg_loss = sum(losses) / len(losses) + return avg_loss + + async def train_epoch(self, dataset): + """Train one epoch with async minibatch processing.""" + total_loss = 0.0 + n_batches = 0 + + # Process dataset in minibatches + for i in range(0, len(dataset), self.batch_size): + batch = dataset[i:i+self.batch_size] + batch_loss = await self.process_minibatch(batch) + total_loss += batch_loss + n_batches += 1 + + # Simulate parameter update + self.model_param._data = f"model_epoch_{n_batches}" + + return total_loss / n_batches + + +# Example 6: Async LLM Optimization +class AsyncLLMOptimizer: + """Demonstrates async LLM-based optimization.""" + + def __init__(self): + self.prompts = [] + for i in range(3): + self.prompts.append(ParameterNode( + f"Initial prompt {i}", + name=f"prompt_{i}", + description=f"Prompt variant {i}" + )) + + @bundle() + async def async_llm_call(self, prompt): + """Simulate async LLM API call.""" + # Simulate LLM latency + await asyncio.sleep(random.uniform(0.5, 1.5)) + + # Simulate LLM response + score = random.random() + return f"Response to '{prompt}' with score {score:.2f}" + + async def evaluate_prompts_concurrently(self): + """Evaluate all prompts concurrently.""" + tasks = [] + for prompt in self.prompts: + tasks.append(self.async_llm_call(prompt)) + + results = await asyncio.gather(*tasks) + return results + + async def optimize_prompts(self, n_iterations: int = 3): + """Optimize prompts with concurrent evaluation.""" + for iteration in range(n_iterations): + print(f"\nIteration {iteration + 1}:") + + # Evaluate all prompts concurrently + start = time.time() + results = await self.evaluate_prompts_concurrently() + elapsed = time.time() - start + + print(f" Evaluated {len(self.prompts)} prompts in {elapsed:.2f}s (concurrent)") + + # Update prompts based on results + for i, (prompt, result) in enumerate(zip(self.prompts, results)): + if "score 0." in result.data and float(result.data.split()[-1]) < 0.5: + # Low score, update prompt + prompt._data = f"Improved prompt {i} (iter {iteration})" + + # Compare with sequential timing + sequential_time = len(self.prompts) * 1.0 # Average 1s per LLM call + print(f" Sequential would take ~{sequential_time:.2f}s") + print(f" Speedup: {sequential_time/elapsed:.1f}x") + + +async def main(): + """Main async function demonstrating various patterns.""" + + print("=" * 60) + print("Async Trace Operations Example") + print("=" * 60) + + # Example 1: Basic async bundle usage + print("\n1. Basic Async Bundle Functions") + print("-" * 40) + + query = node("What is async programming?") + response = await async_api_call(query) + print(f"Query: {query.data}") + print(f"Response: {response.data}") + + # Example 2: Concurrent async operations + print("\n2. Concurrent Async Operations") + print("-" * 40) + + queries = [node(f"Query {i}") for i in range(5)] + start = time.time() + + # Run concurrently + tasks = [async_api_call(q) for q in queries] + responses = await asyncio.gather(*tasks) + concurrent_time = time.time() - start + + print(f"Processed {len(queries)} queries in {concurrent_time:.2f}s (concurrent)") + print(f"Sequential would take ~{len(queries) * 0.3:.2f}s") + + # Example 3: Async optimization + print("\n3. Async Optimization Loop") + print("-" * 40) + + optimizer = AsyncOptimizer() + await optimizer.initialize_parameters(3) + + for step in range(2): + print(f"\nOptimization step {step + 1}:") + feedback = await optimizer.optimize_step() + print(f" Feedback: {feedback}") + + # Example 4: Concurrent pipeline + print("\n4. Concurrent Processing Pipeline") + print("-" * 40) + + tracer = ConcurrentTracer() + sources = ["database", "api", "cache", "file"] + results = await tracer.pipeline(sources) + print(f"Pipeline processed {len(sources)} sources") + for r in results: + print(f" - {r.data}") + + # Example 5: Async context manager + print("\n5. Async Context Manager") + print("-" * 40) + + async with AsyncTraceContext("data_processing") as ctx: + result1 = await ctx.traced_operation("step1", node("data1")) + result2 = await ctx.traced_operation("step2", result1) + result3 = await ctx.traced_operation("step3", result2) + print(f"Final result: {result3.data}") + + # Example 6: Async minibatch training + print("\n6. Async Minibatch Training") + print("-" * 40) + + trainer = AsyncMinibatchOptimizer(batch_size=4) + dataset = [f"sample_{i}" for i in range(12)] + + start = time.time() + avg_loss = await trainer.train_epoch(dataset) + elapsed = time.time() - start + + print(f"Trained on {len(dataset)} samples in {elapsed:.2f}s") + print(f"Average loss: {avg_loss.data:.4f}" if hasattr(avg_loss, 'data') else f"Average loss: {avg_loss:.4f}") + + # Example 7: Async LLM optimization + print("\n7. Async LLM Optimization") + print("-" * 40) + + llm_opt = AsyncLLMOptimizer() + await llm_opt.optimize_prompts(n_iterations=2) + + print("\n" + "=" * 60) + print("Async operations enable efficient concurrent optimization!") + print("Key benefits:") + print(" - Non-blocking I/O operations") + print(" - Concurrent parameter evaluation") + print(" - Efficient LLM API usage") + print(" - Scalable minibatch processing") + print("=" * 60) + + +if __name__ == "__main__": + # Run the async main function + asyncio.run(main()) \ No newline at end of file diff --git a/examples/greeting.py b/examples/greeting.py index 787afdfe..280bceb7 100644 --- a/examples/greeting.py +++ b/examples/greeting.py @@ -86,7 +86,7 @@ def __call__(self, user_query): @bundle(trainable=True) def decide_lang(self, response): """Map the language into a variable""" - return 'es' if 'es' or 'spanish' in response.lower() else 'en' + return 'es' if 'es' in response.lower() or 'spanish' in response.lower() else 'en' @bundle(trainable=True) def greet(self, lang, user_name): diff --git a/examples/gsm8k_trainer_example.py b/examples/gsm8k_trainer_example.py index dd87b749..065a0c91 100644 --- a/examples/gsm8k_trainer_example.py +++ b/examples/gsm8k_trainer_example.py @@ -1,53 +1,8 @@ import datasets import numpy as np -from opto import trace -from opto.utils.llm import LLM, LiteLLM -from opto.optimizers import OptoPrime -from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm -from opto.trainer.loggers import TensorboardLogger -from opto.trainer.guide import LLMJudge -from typing import Any - - -@trace.model -class Learner: - """ A basic LLM agent. """ - - def __init__(self, system_prompt: str = "You're a helpful agent", - user_prompt_template: str = "Query: {message}", - llm: LLM = None): - self.system_prompt = trace.node(system_prompt, trainable=True) - self.user_prompt_template = trace.node(user_prompt_template) - self.llm = llm or LLM() - - @trace.bundle() - def model(self, system_prompt: str, user_prompt_template: str, message: str) -> str: - """Call the LLM model. - - Args: - system_prompt: the system prompt to the agent. By tuning this prompt, we can control the behavior of the agent. For example, it can be used to provide instructions to the agent (such as how to reason about the problem, how to answer the question), or provide in-context examples of how to solve the problem. - user_prompt_template: the user prompt template to the agent. It is used as formatting the input to the agent as user_prompt_template.format(message=message). - message: the input to the agent. It can be a query, a task, a code, etc. - Returns: - The response from the agent. - """ - - if '{message}' not in user_prompt_template: - raise ValueError("user_prompt_template must contain '{message}'") - - response = self.llm( - messages=[{"role": "system", "content": system_prompt}, - {"role": "user", "content": user_prompt_template.format(message=message)}] - ) - return response.choices[0].message.content - - def forward(self, message: Any) -> Any: - """ Forward pass of the agent. """ - return self.model(self.system_prompt, self.user_prompt_template, message) - - -Guide = LLMJudge -Logger = TensorboardLogger +from opto import trainer +from opto.utils.llm import LLM +from opto.features.predefined_agents import BasicLearner def main(): @@ -57,10 +12,6 @@ def main(): batch_size = 1 eval_frequency = -1 num_threads = 3 - verbose = True - teacher_model = None # use default model - student_model = None # use default model - optimizer_model = None # use default model np.random.seed(seed) @@ -68,27 +19,20 @@ def main(): # We will look the training error of the agent on a small portion of this dataset. train_dataset = datasets.load_dataset('openai/gsm8k', 'main')['train'][:10] train_dataset = dict(inputs=train_dataset['question'], infos=train_dataset['answer']) - test_dataset = train_dataset - - agent = Learner(llm=LLM(student_model)) - guide = Guide(llm=LLM(teacher_model)) - optimizer = OptoPrime(agent.parameters(), llm=LLM(optimizer_model)) - logger = Logger(verbose=verbose) - # set use_json_object_format=False if LLM does not support JSON object format - - alg = MinibatchAlgorithm( - agent=agent, - optimizer=optimizer, - logger=logger) - alg.train(guide, - train_dataset, - num_epochs=num_epochs, - batch_size=batch_size, - eval_frequency=eval_frequency, - test_dataset=test_dataset, - num_threads=num_threads, - verbose='output' if verbose else False) + agent = BasicLearner(llm=LLM()) + + trainer.train( + model=agent, + train_dataset=train_dataset, + # trainer kwargs + num_epochs=num_epochs, + batch_size=batch_size, + eval_frequency=eval_frequency, + test_dataset=train_dataset, + num_threads=num_threads, + verbose='output', + ) if __name__ == "__main__": diff --git a/examples/priority_search_example.py b/examples/priority_search_example.py index caf03cbc..0b171302 100644 --- a/examples/priority_search_example.py +++ b/examples/priority_search_example.py @@ -1,53 +1,12 @@ import datasets import numpy as np from opto import trace -from opto.utils.llm import LLM, LiteLLM +from opto.utils.llm import LLM +from opto.features.predefined_agents import BasicLearner from opto.optimizers import OptoPrimeV2 as OptoPrime from opto.features.priority_search import PrioritySearch as SearchAlgorithm from opto.trainer.loggers import TensorboardLogger from opto.trainer.guide import LLMJudge -from typing import Any - - -@trace.model -class Learner: - """ A basic LLM agent. """ - - def __init__(self, system_prompt: str = "You're a helpful agent", - user_prompt_template: str = "Query: {message}", - llm: LLM = None): - self.system_prompt = trace.node(system_prompt, trainable=True) - self.user_prompt_template = trace.node(user_prompt_template) - self.llm = llm or LLM() - - @trace.bundle() - def model(self, system_prompt: str, user_prompt_template: str, message: str) -> str: - """Call the LLM model. - - Args: - system_prompt: the system prompt to the agent. By tuning this prompt, we can control the behavior of the agent. For example, it can be used to provide instructions to the agent (such as how to reason about the problem, how to answer the question), or provide in-context examples of how to solve the problem. - user_prompt_template: the user prompt template to the agent. It is used as formatting the input to the agent as user_prompt_template.format(message=message). - message: the input to the agent. It can be a query, a task, a code, etc. - Returns: - The response from the agent. - """ - - if '{message}' not in user_prompt_template: - raise ValueError("user_prompt_template must contain '{message}'") - - response = self.llm( - messages=[{"role": "system", "content": system_prompt}, - {"role": "user", "content": user_prompt_template.format(message=message)}] - ) - return response.choices[0].message.content - - def forward(self, message: Any) -> Any: - """ Forward pass of the agent. """ - return self.model(self.system_prompt, self.user_prompt_template, message) - - -Guide = LLMJudge -Logger = TensorboardLogger def main(): @@ -66,44 +25,40 @@ def main(): num_threads = 10 datasize = 5 verbose = True - teacher_model = None # use default model - student_model = None # use default model - optimizer_model = None # use default model - np.random.seed(seed) - # In this example, we use the GSM8K dataset, which is a dataset of math word problems. - # We will look the training error of the agent on a small portion of this dataset. + # In this example, we use the BBEH dataset train_dataset = datasets.load_dataset('BBEH/bbeh')['train'][:datasize] train_dataset = dict(inputs=train_dataset['input'], infos=train_dataset['target']) - test_dataset = train_dataset - agent = Learner(llm=LLM(student_model)) - guide = Guide(llm=LLM(teacher_model)) - optimizer = OptoPrime(agent.parameters(), llm=LLM(optimizer_model)) - logger = Logger(verbose=verbose) - # set use_json_object_format=False if LLM does not support JSON object format + agent = BasicLearner(llm=LLM()) + guide = LLMJudge(llm=LLM()) + optimizer = OptoPrime(agent.parameters(), llm=LLM()) + logger = TensorboardLogger(verbose=verbose) alg = SearchAlgorithm( - agent=agent, - optimizer=optimizer, - logger=logger) - - alg.train(guide, - train_dataset, - num_epochs=num_epochs, - batch_size=batch_size, - eval_frequency=eval_frequency, - test_dataset=test_dataset, - num_threads=num_threads, - sub_batch_size=sub_batch_size, - num_proposals=num_proposals, - num_candidates=num_candidates, - score_range=score_range, - num_eval_samples=num_eval_samples, - score_function=score_function, - verbose='output' if verbose else False) + agent=agent, + optimizer=optimizer, + logger=logger + ) + + alg.train( + guide, + train_dataset, + num_epochs=num_epochs, + batch_size=batch_size, + eval_frequency=eval_frequency, + test_dataset=train_dataset, + num_threads=num_threads, + sub_batch_size=sub_batch_size, + num_proposals=num_proposals, + num_candidates=num_candidates, + score_range=score_range, + num_eval_samples=num_eval_samples, + score_function=score_function, + verbose='output' if verbose else False + ) if __name__ == "__main__": diff --git a/examples/search_algo_example.py b/examples/search_algo_example.py index e40cfa7e..0d2b0dfb 100644 --- a/examples/search_algo_example.py +++ b/examples/search_algo_example.py @@ -19,60 +19,13 @@ from opto.trainer.guide import Guide from opto.trainer.loggers import DefaultLogger from opto.utils.llm import LLM +from opto.features.predefined_agents import BasicLearner # Set default model # os.environ["TRACE_LITELLM_MODEL"] = "vertex_ai/gemini-2.0-flash" -@trace.model -class Learner(Module): - """A basic LLM Agent for solving math problems.""" - - def __init__(self, - system_prompt: str = "You're a helpful agent answering math problems.", - user_prompt_template: str = "Solve the following math problem step-by-step: {message}", - llm: LLM = None): - """Initialize the learner agent. - - Args: - system_prompt: System prompt to guide LLM behavior - user_prompt_template: Template for formatting user messages - llm: LLM instance to use for generation (defaults to gpt-3.5-turbo) - """ - super().__init__() - self.system_prompt = trace.node(system_prompt, trainable=True) - self.user_prompt_template = trace.node(user_prompt_template, trainable=True) - self.llm = llm or LLM(model="gpt-3.5-turbo") - - @trace.bundle() - def call_llm(self, system_prompt: str, user_prompt: str) -> str: - """Call LLM model with the given prompts. - - Args: - system_prompt: The system prompt - user_prompt: The user prompt - - Returns: - The LLM response content - """ - response = self.llm( - messages=[ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": user_prompt} - ] - ) - return response.choices[0].message.content - - def forward(self, message: Any) -> str: - """Agent's forward pass to process a message. - - Args: - message: The input message to process - - Returns: - The generated response - """ - user_prompt = self.user_prompt_template.format(message=message) - return self.call_llm(self.system_prompt, user_prompt) +# Use the predefined BasicLearner instead of defining our own +Learner = BasicLearner class TeacherGuide(Guide): diff --git a/examples/train_model.py b/examples/train_model.py index 10b76e0a..de8814c2 100644 --- a/examples/train_model.py +++ b/examples/train_model.py @@ -1,48 +1,8 @@ import datasets import numpy as np -from opto import trace, trainer -from opto.utils.llm import LLM, LiteLLM - -from typing import Any - - -def call_llm(llm, system_prompt: str, user_prompt_template: str, message: str) -> str: - if '{message}' not in user_prompt_template: - raise ValueError("user_prompt_template must contain '{message}'") - response = llm( - messages=[{"role": "system", "content": system_prompt}, - {"role": "user", "content": user_prompt_template.format(message=message)}] - ) - return response.choices[0].message.content - - -@trace.model -class Learner: - """ A basic LLM agent. """ - - def __init__(self, system_prompt: str = "You're a helpful agent", - user_prompt_template: str = "Query: {message}", - llm: LLM = None): - self.system_prompt = trace.node(system_prompt, trainable=True) - self.user_prompt_template = trace.node(user_prompt_template) - self.llm = llm or LLM() - - @trace.bundle() - def model(self, system_prompt: str, user_prompt_template: str, message: str) -> str: - """Call the LLM model. - - Args: - system_prompt: the system prompt to the agent. By tuning this prompt, we can control the behavior of the agent. For example, it can be used to provide instructions to the agent (such as how to reason about the problem, how to answer the question), or provide in-context examples of how to solve the problem. - user_prompt_template: the user prompt template to the agent. It is used as formatting the input to the agent as user_prompt_template.format(message=message). - message: the input to the agent. It can be a query, a task, a code, etc. - Returns: - The response from the agent. - """ - return call_llm(self.llm, system_prompt, user_prompt_template, message) - - def forward(self, message: Any) -> Any: - """ Forward pass of the agent. """ - return self.model(self.system_prompt, self.user_prompt_template, message) +from opto import trainer +from opto.utils.llm import LLM +from opto.features.predefined_agents import BasicLearner @@ -63,7 +23,7 @@ def main(): train_dataset = datasets.load_dataset('BBEH/bbeh')['train'][:datasize] train_dataset = dict(inputs=train_dataset['input'], infos=train_dataset['target']) - agent = Learner(llm=LLM()) + agent = BasicLearner(llm=LLM()) trainer.train( model=agent, diff --git a/opto/features/predefined_agents/__init__.py b/opto/features/predefined_agents/__init__.py new file mode 100644 index 00000000..4dd84750 --- /dev/null +++ b/opto/features/predefined_agents/__init__.py @@ -0,0 +1,5 @@ +"""Predefined agents for common use cases.""" + +from .learner import BasicLearner, Learner, call_llm + +__all__ = ['BasicLearner', 'Learner', 'call_llm'] \ No newline at end of file diff --git a/opto/features/predefined_agents/learner.py b/opto/features/predefined_agents/learner.py new file mode 100644 index 00000000..08b14565 --- /dev/null +++ b/opto/features/predefined_agents/learner.py @@ -0,0 +1,85 @@ +"""Common learner utilities for examples and documentation.""" + +from typing import Any +from opto import trace +from opto.utils.llm import LLM + + +def call_llm(llm, system_prompt: str, user_prompt_template: str, message: str) -> str: + """Call LLM with system and user prompts. + + Args: + llm: The LLM instance to use + system_prompt: The system prompt for the LLM + user_prompt_template: Template for the user prompt (must contain {message}) + message: The input message to format into the template + + Returns: + The LLM response content + + Raises: + ValueError: If user_prompt_template doesn't contain {message} + """ + if '{message}' not in user_prompt_template: + raise ValueError("user_prompt_template must contain '{message}'") + + response = llm( + messages=[ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt_template.format(message=message)} + ] + ) + return response.choices[0].message.content + + +@trace.model +class BasicLearner: + """A reusable basic LLM agent for examples and tutorials. + + This class provides a standard implementation of an LLM-based learner + that can be used across multiple examples and documentation notebooks. + """ + + def __init__( + self, + system_prompt: str = "You're a helpful agent", + user_prompt_template: str = "Query: {message}", + llm: LLM = None + ): + """Initialize the learner. + + Args: + system_prompt: The system prompt to guide LLM behavior + user_prompt_template: Template for formatting user messages (must contain {message}) + llm: LLM instance to use (defaults to LLM()) + """ + self.system_prompt = trace.node(system_prompt, trainable=True) + self.user_prompt_template = trace.node(user_prompt_template) + self.llm = llm or LLM() + + @trace.bundle() + def model(self, system_prompt: str, user_prompt_template: str, message: str) -> str: + """Call the LLM model. + + Args: + system_prompt: The system prompt to the agent. By tuning this prompt, + we can control the behavior of the agent. For example, it can be used + to provide instructions to the agent (such as how to reason about the + problem, how to answer the question), or provide in-context examples + of how to solve the problem. + user_prompt_template: The user prompt template to the agent. It is used + as formatting the input to the agent as user_prompt_template.format(message=message). + message: The input to the agent. It can be a query, a task, a code, etc. + + Returns: + The response from the agent. + """ + return call_llm(self.llm, system_prompt, user_prompt_template, message) + + def forward(self, message: Any) -> Any: + """Forward pass of the agent.""" + return self.model(self.system_prompt, self.user_prompt_template, message) + + +# Alias for backward compatibility +Learner = BasicLearner \ No newline at end of file diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index 91eedffa..46cca663 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -12,17 +12,60 @@ class ModuleCandidate: - """ A container used by PrioritySearch to store a candidate module as (its base module and update dictionary) and its statistics. """ + """Container for storing candidate modules with their parameters and performance statistics. + + This class represents a candidate agent configuration consisting of a base module + and an update dictionary that modifies its parameters. It tracks performance + statistics through rollouts and provides confidence interval calculations. + + Parameters + ---------- + base_module : trace.Module + The base module to use as a template for the candidate. + update_dict : dict[ParameterNode, Any], optional + Dictionary of parameter updates to apply to the base module, by default None. + + Attributes + ---------- + base_module : trace.Module + The original module template. + update_dict : dict[ParameterNode, Any] + Parameter updates mapped to the base module's parameters. + rollouts : list[dict] + Performance statistics from agent evaluations. + created_time : float + Timestamp when the candidate was created. + + Notes + ----- + The update dictionary is automatically remapped to ensure compatibility with + the base module's parameter structure. Rollouts store detailed execution + information including modules, inputs, targets, scores, and feedback. + """ def __init__(self, base_module: Optional[trace.Module], update_dict: Optional[Dict[ParameterNode, Any]] = None, ): - """ A candidate module with its base module and update dictionary. - Args: - base_module (trace.Module): The base module to use as a template for the candidate. - update_dict (dict): A dictionary of ParameterNode: value pairs to update the base module; the key can be a deep copy of the base module's parameters. - stats (dict): A dictionary of statistics about the candidate. + """Initialize a module candidate with base module and parameter updates. + + Parameters + ---------- + base_module : trace.Module + The base module to use as a template for the candidate. + update_dict : dict[ParameterNode, Any], optional + Dictionary of parameter updates to apply, by default None. + + Raises + ------ + AssertionError + If base_module is not a trace.Module instance. + + Notes + ----- + The update dictionary is automatically remapped to ensure parameter + compatibility with the base module. Internal tracking variables for + confidence calculations are initialized. """ assert isinstance(base_module, trace.Module), "base_module must be a trace.Module." self.base_module = base_module @@ -35,15 +78,36 @@ def __init__(self, self._confidence_interval = None def get_module(self): - """ Apply the update_dict to the base_module and return the updated module. - A new module is always created so the base_module is not modified. - The new module has a new attribute _module_candidate which is this candidate.""" + """Create and return an updated module with applied parameter changes. + + Returns + ------- + trace.Module + New module instance with parameters updated according to update_dict. + + Notes + ----- + A new module is always created to avoid modifying the base module. + The returned module includes a special attribute marking its candidate ID + for tracking purposes in the priority search algorithm. + """ module = create_module_from_update_dict(self.base_module, self.update_dict) if self.update_dict else copy.deepcopy(self.base_module) # setattr(module, '__TRACE_RESERVED_module_candidate_id', id(self)) return module # return the updated module def apply_update(self, base_module=None): - """ Apply update to the base_module in place. """ + """Apply parameter updates to a module in place. + + Parameters + ---------- + base_module : trace.Module, optional + Module to update, uses self.base_module if None, by default None. + + Notes + ----- + This method modifies the target module's parameters directly using + the stored update dictionary. + """ set_module_parameters(base_module or self.base_module, self.update_dict) def __deepcopy__(self, memo): @@ -76,7 +140,26 @@ def __hash__(self): return hash(frozenset(self.update_dict.items())) def add_rollouts(self, rollouts: List[Dict[str, Any]]): - """ Add rollouts to the candidate. """ + """Add performance rollouts to the candidate for statistics tracking. + + Parameters + ---------- + rollouts : list[dict[str, Any]] + List of rollout dictionaries containing execution results. + + Raises + ------ + AssertionError + If rollouts is not a list or contains non-dict elements. + If rollout dicts missing required keys: 'module', 'x', 'info', + 'target', 'score', 'feedback'. + + Notes + ----- + Each rollout dictionary must contain complete execution information. + Adding rollouts resets the confidence interval cache and increments + the update counter. + """ assert isinstance(rollouts, list), "rollouts must be a list of dicts." assert all(isinstance(r, dict) for r in rollouts), "All rollouts must be dicts." # Each rollout is a dict with keys: 'module', 'x', 'info', 'target', 'score', 'feedback' @@ -88,25 +171,51 @@ def add_rollouts(self, rollouts: List[Dict[str, Any]]): self._n_updates += 1 # increment the number of updates def mean_score(self): - """ Compute the score of the candidate based on the rollouts. """ + """Calculate the mean performance score from rollout statistics. + + Returns + ------- + float or None + Average score across all rollouts, or None if no rollouts exist. + + Notes + ----- + This is the primary performance metric used for ranking candidates + in the priority queue. + """ if not self.rollouts: return None scores = [r['score'] for r in self.rollouts] return np.mean(scores) if scores else None def compute_score_confidence(self, min_score, max_score, scaling_constant=1.0): - """Compute the UCB, mean, LCB score for the candidate. After queried, the number of confidence queries is incremented. - - UCB = mean_score + scaling_constant * sqrt(ln(total_trials) / candidate_trials) * (max_score - min_score) - UCB = clip(UCB, min_score, max_score) - - LCB = mean_score - scaling_constant * sqrt(ln(total_trials) / candidate_trials) * (max_score - min_score) - LCB = clip(LCB, min_score, max_score) - - Args: - candidate (ModuleCandidate): The candidate for which to compute the UCB score. - Returns: - float: The computed UCB score for the candidate. + """Compute Upper and Lower Confidence Bounds for multi-armed bandit selection. + + Calculates confidence intervals using Hoeffding's inequality to balance + exploration and exploitation in candidate selection. + + Parameters + ---------- + min_score : float + Minimum possible score value for clipping. + max_score : float + Maximum possible score value for clipping. + scaling_constant : float, optional + Exploration constant controlling confidence width, by default 1.0. + + Returns + ------- + tuple[float, float, float] + Lower confidence bound, mean score, upper confidence bound. + + Notes + ----- + Uses the formula: + - UCB = mean + scaling * sqrt(ln(total_queries) / trials) * (max - min) + - LCB = mean - scaling * sqrt(ln(total_queries) / trials) * (max - min) + + Both bounds are clipped to [min_score, max_score]. The confidence query + counter is incremented after each call for proper union bound calculation. """ # Get scores from rollouts scores = [r['score'] for r in self.rollouts] @@ -157,24 +266,74 @@ def n_updates(self): return self._n_updates class HeapMemory: - # This is a basic implementation of a heap memory that uses a priority queue to store candidates. - # Later on this will be replaced by a memory DB. - - # NOTE that the heap memory is a max-heap, so we store negative scores to use the default min-heap behavior of heapq. + """Priority queue implementation for storing and retrieving module candidates. + + This class provides a max-heap interface using Python's min-heap heapq module + by storing negative scores. It maintains the best-performing candidates with + optional size limits for memory efficiency. + + Parameters + ---------- + size : int, optional + Maximum number of items to store in the heap, by default None (unlimited). + + Attributes + ---------- + memory : list + Internal heap storage containing (negative_score, candidate) tuples. + size : int or None + Maximum heap size limit. + + Notes + ----- + Since heapq implements a min-heap, scores are stored as negative values to + achieve max-heap behavior. This ensures the highest-scoring candidates are + prioritized for selection. + """ def __init__(self, size=None): - """ Initialize an empty heap memory. """ + """Initialize an empty heap memory with optional size limit. + + Parameters + ---------- + size : int, optional + Maximum number of items to store, by default None (unlimited). + """ self.memory = [] self.size = size # Optional size limit for the heap memory def push(self, score, data): - """ Push an item to the heap memory. """ + """Add an item to the heap memory with the given score. + + Parameters + ---------- + score : float + Priority score for the item (higher scores have higher priority). + data : Any + The item to store in the heap. + + Notes + ----- + The score is negated before storage to achieve max-heap behavior. + If the heap exceeds the size limit, it's truncated to maintain the limit. + """ heapq.heappush(self.memory, (-score, data)) if self.size is not None and len(self.memory) > self.size: # NOTE a heuristic for now self.memory = self.memory[:self.size] # Keep only the top `size` items def pop(self): - """ Pop the top item from the heap memory. """ + """Remove and return the highest priority item from the heap. + + Returns + ------- + tuple[float, Any] + The (negative_score, data) tuple of the highest priority item. + + Raises + ------ + IndexError + If the heap is empty. + """ if not self.memory: raise IndexError("pop from an empty heap memory") return heapq.heappop(self.memory) @@ -192,29 +351,64 @@ def __iter__(self): return iter(self.memory) def best(self): - """ Return the best item in the heap memory without removing it. """ + """Return the highest priority item without removing it from the heap. + + Returns + ------- + tuple[float, Any] + The (negative_score, data) tuple of the highest priority item. + + Raises + ------ + IndexError + If the heap is empty. + """ if not self.memory: raise IndexError("best from an empty heap memory") return self.memory[0] class PrioritySearch(SearchTemplate): - """ A search algorithm that uses a priority queue to explore the parameter space and propose new candidates. - - It provides a scalable template for implementing search algorithms based on asynchronous generation, validation, and testing. - In each iteration, - 1. It proposes a best agent and a set of `num_candidates` exploration agents that have the highest scores in the priority queue. - 2. The best agent is tested for performance if eval_frequency is met. - 3. A minibatch of `batch_size` samples are drawn from the training dataset, and the exploration agents are run on the samples. This creates a set of agent rollouts, where each rollout contains the agent module, input, info, target, score, and feedback. For each agent, rollouts of size `sub_batch_size` are grouped together as a connected subgraph (represented as the RolloutsGraph object). In total, this step creates `num_subgraphs = num_candidates * ceil(batch_size / sub_batch_size)` subgraphs. - 4. Optimizer is run on each subgraph to propose new parameters for the agents. `num_proposals` proposals are generated for each subgraph. This results in `num_subgraphs * num_proposals` total proposals. - 5. The proposed parameters are validated by running the agents on the validation dataset, which can be the current batch or a separate validation dataset when provided. When validate_proposals is set to True, the exploration candidates are also validated. - 6. The validation results are used to update the priority queue, which stores the candidates and their scores. The candidates are stored as ModuleCandidate objects, which contain the base module, update dictionary, and rollouts (i.e. raw statistics of the candidate). - - This algorithm template can be subclassed to implement specific search algorithms by overriding the `exploit`, `explore`, and `compute_priority` methods. - The `exploit` method is used to select the best candidate from the priority queue, the `explore` method is used to generate new candidates from the priority queue, and - the `compute_priority` method is used to compute the score for ranking in the priority queue. - - By default, `compute_priority` computes the mean score of the rollouts. `exploit` simply returns the best candidate from the priority queue, and `explore` generates the top `num_candidates` candidates from the priority queue. + """Priority-based search algorithm for exploring parameter space and optimizing agents. + + This algorithm uses a priority queue to systematically explore the parameter space + through a cycle of proposal generation, validation, and candidate ranking. It balances + exploration of new parameter configurations with exploitation of high-performing ones. + + The algorithm operates in iterative cycles: + + 1. **Exploitation**: Select the best-performing candidate from the priority queue + 2. **Exploration**: Choose top candidates for parameter space exploration + 3. **Proposal Generation**: Use optimizers on collected samples to propose new parameters + 4. **Validation**: Evaluate proposed parameters on validation data + 5. **Memory Update**: Update priority queue with validation results + + Each iteration processes minibatches of training data, creating rollout graphs that + capture agent execution statistics. These rollouts inform the optimization process + and candidate evaluation. + + Attributes + ---------- + memory : HeapMemory + Priority queue storing module candidates with their performance scores. + num_candidates : int + Number of exploration candidates to select per iteration. + num_proposals : int + Number of parameter proposals per optimizer call. + score_function : str + Scoring method for candidate ranking ('mean' or 'ucb'). + ucb_exploration_constant : float + Exploration parameter for Upper Confidence Bound scoring. + + Notes + ----- + The algorithm can be customized by overriding key methods: + - `exploit`: Strategy for selecting the best candidate + - `explore`: Strategy for selecting exploration candidates + - `compute_priority`: Scoring function for candidate ranking + + Default implementations use mean rollout scores for ranking, simple best-candidate + exploitation, and top-k candidate exploration. """ def train(self, @@ -250,6 +444,67 @@ def train(self, # Additional keyword arguments **kwargs ): + """Train the agent using priority-based parameter space search. + + This method orchestrates the complete training process using a priority queue + to guide parameter exploration and optimization. + + Parameters + ---------- + guide : Guide + Guide function to provide feedback during training. + train_dataset : dict + Training dataset containing 'inputs' and 'infos' keys. + validate_dataset : dict, optional + Validation dataset, uses current batch if None, by default None. + validate_guide : Guide, optional + Guide for validation scoring, uses train guide if None, by default None. + batch_size : int, optional + Batch size for agent updates, by default 1. + sub_batch_size : int, optional + Sub-batch size for optimizer attention, by default None. + score_range : tuple[float, float], optional + Score range for UCB calculations, by default None. + num_epochs : int, optional + Number of training epochs, by default 1. + num_threads : int, optional + Maximum threads for parallel processing, by default None. + verbose : bool, optional + Enable verbose output, by default False. + test_dataset : dict, optional + Test dataset for evaluation, by default None. + test_frequency : int, optional + Frequency of test evaluation, by default 1. + num_eval_samples : int, optional + Samples per input for evaluation, by default 1. + log_frequency : int, optional + Logging frequency, by default None. + save_frequency : int, optional + Model saving frequency, by default None. + save_path : str, optional + Path for saving checkpoints, by default "checkpoints/agent.pkl". + num_candidates : int, optional + Number of exploration candidates per iteration, by default 10. + num_proposals : int, optional + Number of proposals per optimizer call, by default 1. + validate_proposals : bool, optional + Whether to validate exploration candidates, by default True. + use_best_candidate_to_explore : bool, optional + Include best candidate in exploration set, by default True. + memory_size : int, optional + Maximum memory size for candidate storage, by default None. + score_function : str, optional + Scoring function ('mean' or 'ucb'), by default 'mean'. + ucb_exploration_constant : float, optional + UCB exploration parameter, by default 1.0. + **kwargs + Additional arguments passed to parent class. + + Notes + ----- + The UCB score function requires a finite score_range for proper calculation. + If score_range is None and UCB is selected, it defaults to (0, 1). + """ # Create agents and optimizers for search self.num_candidates = num_candidates # number of candidates to propose by each optimizer call diff --git a/opto/optimizers/buffers.py b/opto/optimizers/buffers.py index b0f2c9b9..333c2c28 100644 --- a/opto/optimizers/buffers.py +++ b/opto/optimizers/buffers.py @@ -1,10 +1,66 @@ class FIFOBuffer: - # A basic FIFO buffer + """First-In-First-Out buffer with fixed maximum size. + + Maintains a rolling buffer of items where oldest items are + automatically removed when the buffer reaches capacity. + + Parameters + ---------- + size : int + Maximum number of items to store. If 0, no items are stored. + + Attributes + ---------- + size : int + Maximum buffer capacity. + buffer : list + Current items in the buffer. + + Methods + ------- + add(item) + Add an item to the buffer. + __iter__() + Iterate over buffer items. + __len__() + Get current number of items. + + Notes + ----- + Used by optimizers to maintain history of: + - Recent feedback examples + - Previous parameter values + - Optimization trajectories + + When buffer is full, adding a new item removes the oldest + item (FIFO behavior). This is useful for maintaining a + sliding window of recent optimization context. + + Examples + -------- + >>> buffer = FIFOBuffer(size=3) + >>> for i in range(5): + ... buffer.add(i) + >>> list(buffer) # Only last 3 items + [2, 3, 4] + """ def __init__(self, size: int): self.size = size self.buffer = [] def add(self, item): + """Add an item to the buffer. + + Parameters + ---------- + item : Any + Item to add to the buffer. + + Notes + ----- + If buffer is at capacity, the oldest item is removed. + If size is 0, the item is not stored. + """ if self.size > 0: self.buffer.append(item) self.buffer = self.buffer[-self.size:] diff --git a/opto/optimizers/opro.py b/opto/optimizers/opro.py index 22ae199c..24f5a2cd 100644 --- a/opto/optimizers/opro.py +++ b/opto/optimizers/opro.py @@ -5,6 +5,37 @@ class OPRO(OptoPrime): + """Optimization by PROmpting (OPRO) optimizer implementing feedback-driven parameter updates. + + OPRO is a simplified version of OptoPrime that uses accumulated examples of variables + and their feedback to guide parameter optimization. It maintains a buffer of historical + examples to provide context for generating improved parameter suggestions. + + Parameters + ---------- + *args + Positional arguments passed to the OptoPrime parent class. + **kwargs + Keyword arguments passed to the OptoPrime parent class. + + Attributes + ---------- + buffer : list[tuple] + Buffer storing (variables, feedback) pairs for historical context. + user_prompt_template : str + Template for constructing user prompts with examples and instructions. + output_format_prompt : str + Template specifying the expected JSON output format for suggestions. + default_objective : str + Default optimization objective when none is specified. + + Notes + ----- + OPRO differs from OptoPrime by using a simpler prompt structure focused on + historical examples rather than detailed meta-information and reasoning chains. + The optimizer accumulates variable-feedback pairs over time to build context + for future optimization steps. + """ user_prompt_template = dedent( """ Below are some example variables and their feedbacks. @@ -41,11 +72,46 @@ class OPRO(OptoPrime): default_objective = "Come up with a new variable in accordance to feedback." def __init__(self, *args, **kwargs): + """Initialize OPRO optimizer with empty example buffer. + + Parameters + ---------- + *args + Positional arguments passed to OptoPrime parent class. + **kwargs + Keyword arguments passed to OptoPrime parent class. + """ super().__init__(*args, **kwargs) self.buffer = [] def construct_prompt(self, summary, mask=None, *args, **kwargs): - """Construct the system and user prompt.""" + """Construct system and user prompts using historical examples. + + This method builds prompts by accumulating variable-feedback pairs in a buffer + and formatting them as examples for the language model to learn from. + + Parameters + ---------- + summary : Summary + Summary object containing current variables and user feedback. + mask : Any, optional + Mask parameter (unused in OPRO), by default None. + *args + Additional positional arguments. + **kwargs + Additional keyword arguments. + + Returns + ------- + tuple[str, str] + System prompt (output format) and user prompt with examples. + + Notes + ----- + The method adds the current summary to the buffer and formats all buffered + examples into a structured prompt. Each example includes the variables + dictionary and associated feedback. + """ self.buffer.append((summary.variables, summary.user_feedback)) examples = [] diff --git a/opto/optimizers/opro_v2.py b/opto/optimizers/opro_v2.py index 13054943..ff5c801d 100644 --- a/opto/optimizers/opro_v2.py +++ b/opto/optimizers/opro_v2.py @@ -7,6 +7,47 @@ # Not inheriting from optoprime_v2 because this should have a smaller set class OPROPromptSymbolSet(OptimizerPromptSymbolSet): + """Prompt symbol set for OPRO optimizer. + + This class defines the tags and symbols used in the OPRO optimizer's prompts + and output parsing. It provides a structured way to format problems and parse + responses from the language model. + + Attributes + ---------- + problem_context_section_title : str + Title for the problem context section in prompts. + variable_section_title : str + Title for the variable/solution section in prompts. + feedback_section_title : str + Title for the feedback section in prompts. + node_tag : str + Tag used to identify constant nodes in the computation graph. + variable_tag : str + Tag used to identify variable nodes that can be optimized. + value_tag : str + Tag used to wrap the value of a node. + constraint_tag : str + Tag used to wrap constraint expressions for nodes. + reasoning_tag : str + Tag used to wrap reasoning in the output. + improved_variable_tag : str + Tag used to wrap improved variable values in the output. + name_tag : str + Tag used to wrap variable names. + expect_json : bool + Whether to expect JSON output format (default: False). + + Methods + ------- + default_prompt_symbols + Returns default prompt symbols dictionary. + + Notes + ----- + This class inherits from OptimizerPromptSymbolSet but defines a smaller, + more focused set of symbols specifically for OPRO optimization. + """ problem_context_section_title = "# Problem Context" variable_section_title = "# Solution" @@ -35,6 +76,34 @@ def default_prompt_symbols(self) -> Dict[str, str]: @dataclass class ProblemInstance: + """Represents a problem instance for OPRO optimization. + + This dataclass encapsulates a complete problem instance including the + instruction, current variables/solution, and feedback received. + + Attributes + ---------- + instruction : str + The instruction describing what needs to be done or the question to answer. + variables : str + The current proposed solution that can be modified. + feedback : str + Feedback about the current solution. + optimizer_prompt_symbol_set : OPROPromptSymbolSet + The symbol set used for formatting the problem. + problem_template : str + Template for formatting the problem instance as a string. + + Methods + ------- + __repr__() + Returns a formatted string representation of the problem instance. + + Notes + ----- + The problem instance is formatted using the problem_template which + organizes the instruction, variables, and feedback into a structured format. + """ instruction: str variables: str feedback: str @@ -62,6 +131,64 @@ def __repr__(self) -> str: ) class OPROv2(OptoPrimeV2): + """OPRO (Optimization by PROmpting) optimizer version 2. + + OPRO is an optimization algorithm that leverages large language models to + iteratively improve solutions based on feedback. It treats optimization as + a natural language problem where the LLM proposes improvements to variables + based on instruction and feedback. + + Parameters + ---------- + *args + Variable length argument list passed to parent class. + optimizer_prompt_symbol_set : OptimizerPromptSymbolSet, optional + The symbol set for formatting prompts and parsing outputs. + Defaults to OPROPromptSymbolSet(). + include_example : bool, optional + Whether to include examples in the prompt. Default is False as + the default example in OptoPrimeV2 does not work well with OPRO. + memory_size : int, optional + Number of past optimization steps to remember. Default is 5. + **kwargs + Additional keyword arguments passed to parent class. + + Attributes + ---------- + representation_prompt : str + Template for explaining the problem representation to the LLM. + output_format_prompt_template : str + Template for specifying the expected output format. + user_prompt_template : str + Template for presenting the problem instance to the LLM. + final_prompt : str + Template for requesting the final revised solutions. + default_objective : str + Default objective when none is specified. + + Methods + ------- + problem_instance(summary, mask=None) + Creates a ProblemInstance from an optimization summary. + initialize_prompt() + Initializes and formats the prompt templates. + + Notes + ----- + OPRO differs from OptoPrime by focusing on simpler problem representations + and clearer feedback incorporation. It is particularly effective for + problems where the optimization can be expressed in natural language. + + See Also + -------- + OptoPrimeV2 : Parent class providing core optimization functionality. + OPROPromptSymbolSet : Symbol set used for formatting. + + Examples + -------- + >>> optimizer = OPROv2(memory_size=10) + >>> # Use optimizer to improve solutions based on feedback + """ representation_prompt = dedent( """ You're tasked to change the proposed solution according to feedback. @@ -118,12 +245,48 @@ def __init__(self, *args, include_example=False, # default example in OptoPrimeV2 does not work in OPRO memory_size=5, **kwargs): + """Initialize the OPROv2 optimizer. + + Parameters + ---------- + *args + Variable length argument list passed to parent class. + optimizer_prompt_symbol_set : OptimizerPromptSymbolSet, optional + The symbol set for formatting prompts and parsing outputs. + If None, uses OPROPromptSymbolSet(). + include_example : bool, optional + Whether to include examples in the prompt. Default is False. + memory_size : int, optional + Number of past optimization steps to remember. Default is 5. + **kwargs + Additional keyword arguments passed to parent class. + """ optimizer_prompt_symbol_set = optimizer_prompt_symbol_set or OPROPromptSymbolSet() super().__init__(*args, optimizer_prompt_symbol_set=optimizer_prompt_symbol_set, include_example=include_example, memory_size=memory_size, **kwargs) def problem_instance(self, summary, mask=None): + """Create a ProblemInstance from an optimization summary. + + Parameters + ---------- + summary : object + The optimization summary containing variables and feedback. + mask : list, optional + List of sections to mask/hide in the problem instance. + Can include "#Instruction", variable section title, or feedback section title. + + Returns + ------- + ProblemInstance + A formatted problem instance ready for presentation to the LLM. + + Notes + ----- + The mask parameter allows selective hiding of problem components, + useful for ablation studies or specific optimization strategies. + """ mask = mask or [] return ProblemInstance( instruction=self.objective if "#Instruction" not in mask else "", @@ -139,6 +302,17 @@ def problem_instance(self, summary, mask=None): ) def initialize_prompt(self): + """Initialize and format the prompt templates. + + This method formats the representation_prompt and output_format_prompt + templates with the appropriate symbols from the optimizer_prompt_symbol_set. + It prepares the prompts for use in optimization. + + Notes + ----- + This method should be called during initialization to ensure all + prompt templates are properly formatted with the correct tags and symbols. + """ self.representation_prompt = self.representation_prompt.format( variable_expression_format=dedent(f""" <{self.optimizer_prompt_symbol_set.variable_tag} name="variable_name" type="data_type"> diff --git a/opto/optimizers/optimizer.py b/opto/optimizers/optimizer.py index 2b175d5f..9c61f605 100644 --- a/opto/optimizers/optimizer.py +++ b/opto/optimizers/optimizer.py @@ -7,7 +7,66 @@ class AbstractOptimizer: - """An optimizer is responsible for updating the parameters based on the feedback.""" + """Abstract base class for all optimizers in the Trace framework. + + Defines the interface that all optimizers must implement for parameter + optimization based on feedback from the computation graph. + + Parameters + ---------- + parameters : list[ParameterNode] + List of trainable parameters to optimize. Must be non-empty and contain + only ParameterNode instances. + *args + Additional positional arguments for optimizer configuration. + **kwargs + Additional keyword arguments for optimizer configuration. + + Attributes + ---------- + parameters : list[ParameterNode] + The parameters being optimized. + + Methods + ------- + step() + Perform one optimization step. + zero_feedback() + Clear accumulated feedback from parameters. + propagator + Property returning the feedback propagator. + + Raises + ------ + AssertionError + If parameters is not a list, contains non-ParameterNode objects, + or is empty. + + Notes + ----- + This abstract class establishes the optimizer protocol: + + 1. **Parameter Management**: Optimizers maintain a list of parameters + they are responsible for updating. + + 2. **Feedback Processing**: Optimizers process feedback accumulated + in parameters during backward passes. + + 3. **Update Steps**: The step() method applies optimization logic + to update parameter values. + + 4. **Feedback Clearing**: zero_feedback() resets accumulated feedback + for the next iteration. + + Subclasses must implement all abstract methods to create functional + optimizers. + + See Also + -------- + Optimizer : Concrete base class with graph-based optimization + ParameterNode : Trainable parameters that optimizers update + Propagator : Handles feedback propagation through the graph + """ def __init__(self, parameters: List[ParameterNode], *args, **kwargs): assert type(parameters) is list @@ -30,7 +89,103 @@ def propagator(self): class Optimizer(AbstractOptimizer): - """Optimizer based on Trace graph.""" + """Base class for graph-based optimizers in the Trace framework. + + Extends AbstractOptimizer with concrete implementations for graph-based + optimization, including feedback propagation, parameter projection, and + update mechanisms. + + Parameters + ---------- + parameters : list[ParameterNode] + List of trainable parameters to optimize. + propagator : Propagator, optional + Custom propagator for feedback processing. If None, uses default + GraphPropagator. + *args + Additional positional arguments. + **kwargs + Additional keyword arguments. + + Attributes + ---------- + parameters : list[ParameterNode] + The parameters being optimized. + propagator : Propagator + The feedback propagator used during backward passes. + trace_graph : Any + Aggregated computation graph from all parameters. + + Methods + ------- + step(bypassing=False, *args, **kwargs) + Perform one optimization step with optional update bypassing. + propose(*args, **kwargs) + Generate proposed parameter updates based on feedback. + project(update_dict) + Apply constraints/projections to proposed updates. + update(update_dict) + Apply updates to trainable parameters. + backward(node, *args, **kwargs) + Propagate feedback through the graph. + zero_feedback() + Clear accumulated feedback from all parameters. + save(path) + Save optimizer state (placeholder). + load(path) + Load optimizer state (placeholder). + _step(*args, **kwargs) + Abstract method for computing parameter updates. + default_propagator() + Return the default propagator instance. + + Notes + ----- + The Optimizer class implements a three-stage update process: + + 1. **Propose**: Generate candidate updates based on feedback + (implemented in _step by subclasses). + + 2. **Project**: Apply constraints and projections to ensure + updates remain in valid parameter space. + + 3. **Update**: Apply the projected updates to parameters + (can be bypassed for analysis). + + Key features: + + - **Feedback Aggregation**: Automatically collects and aggregates + feedback from the computation graph. + + - **Projection Support**: Integrates with parameter projections + for constrained optimization. + + - **Flexible Propagation**: Supports custom propagators for + different feedback processing strategies. + + - **State Management**: Provides hooks for saving/loading + optimizer state (implementation-specific). + + Subclasses must implement _step() to define the optimization + algorithm. + + See Also + -------- + AbstractOptimizer : Abstract base class + GraphPropagator : Default feedback propagator + ParameterNode : Parameters being optimized + Projection : Constraints applied during optimization + + Examples + -------- + >>> class MyOptimizer(Optimizer): + ... def _step(self): + ... updates = {} + ... for p in self.parameters: + ... feedback = sum_feedback(p.feedback) + ... updates[p] = p.data - 0.01 * feedback + ... return updates + """ def __init__( self, @@ -54,14 +209,50 @@ def trace_graph(self): return sum_feedback(self.parameters) def step(self, bypassing=False, *args, **kwargs): + """Perform one optimization step. + + Parameters + ---------- + bypassing : bool, default=False + If True, computes updates but doesn't apply them to parameters. + Useful for analysis or debugging. + *args + Additional arguments passed to propose(). + **kwargs + Additional keyword arguments passed to propose(). + + Returns + ------- + dict[ParameterNode, Any] + Dictionary mapping parameters to their (projected) updates. + + Notes + ----- + The step executes in three phases: + 1. Propose updates via _step() + 2. Apply projections to maintain constraints + 3. Update parameters (unless bypassing=True) + """ update_dict = self.propose(*args, **kwargs) self.project(update_dict) if not bypassing: self.update(update_dict) - return update_dict # TODO add reasoning + return update_dict def project(self, update_dict: Dict[ParameterNode, Any]): - """Project the update dictionary onto the feasible set.""" + """Apply projections to constrain parameter updates. + + Parameters + ---------- + update_dict : dict[ParameterNode, Any] + Proposed updates for each parameter. + + Notes + ----- + Modifies update_dict in-place by applying each parameter's + projection operators sequentially. Only applies to trainable + parameters with defined projections. + """ for p, d in update_dict.items(): if p.trainable: for projection in p.projections: @@ -69,30 +260,112 @@ def project(self, update_dict: Dict[ParameterNode, Any]): update_dict[p] = d def propose(self, *args, **kwargs): - """Propose the new data of the parameters based on the feedback.""" + """Generate proposed parameter updates based on feedback. + + Parameters + ---------- + *args + Arguments passed to _step(). + **kwargs + Keyword arguments passed to _step(). + + Returns + ------- + dict[ParameterNode, Any] + Proposed new values for each parameter. + + Notes + ----- + Delegates to _step() which must be implemented by subclasses. + """ return self._step(*args, **kwargs) def update(self, update_dict: Dict[ParameterNode, Any]): - """Update the trainable parameters given a dictionary of new data.""" + """Apply updates to trainable parameters. + + Parameters + ---------- + update_dict : dict[ParameterNode, Any] + New values for each parameter. + + Notes + ----- + Only updates parameters marked as trainable. Updates are + applied by directly modifying the parameter's _data attribute. + """ for p, d in update_dict.items(): if p.trainable: p._data = d def zero_feedback(self): + """Clear accumulated feedback from all parameters. + + Notes + ----- + Should be called after each optimization step to prepare + for the next iteration's feedback accumulation. + """ for p in self.parameters: p.zero_feedback() # Subclass should implement the methods below. def _step(self, *args, **kwargs) -> Dict[ParameterNode, Any]: - """Return the new data of parameter nodes based on the feedback.""" + """Compute parameter updates based on accumulated feedback. + + Parameters + ---------- + *args + Optimizer-specific arguments. + **kwargs + Optimizer-specific keyword arguments. + + Returns + ------- + dict[ParameterNode, Any] + Proposed new values for each parameter. + + Notes + ----- + Must be implemented by subclasses to define the optimization + algorithm. Has access to self.parameters and their feedback. + """ raise NotImplementedError def default_propagator(self): - """Return the default Propagator object of the optimizer.""" + """Return the default feedback propagator. + + Returns + ------- + GraphPropagator + Default propagator for feedback processing. + + Notes + ----- + Subclasses can override to provide custom default propagators. + """ return GraphPropagator() def backward(self, node: Node, *args, **kwargs): - """Propagate the feedback backward.""" + """Propagate feedback backward through the computation graph. + + Parameters + ---------- + node : Node + Starting node for backward propagation. + *args + Additional arguments passed to node.backward(). + **kwargs + Additional keyword arguments passed to node.backward(). + + Returns + ------- + Any + Result from node.backward(), typically a visualization graph. + + Notes + ----- + Uses the optimizer's propagator for feedback processing. + """ return node.backward(*args, propagator=self.propagator, **kwargs) def save(self, path: str): diff --git a/opto/optimizers/optoprime.py b/opto/optimizers/optoprime.py index 6465151d..cdb38aa7 100644 --- a/opto/optimizers/optoprime.py +++ b/opto/optimizers/optoprime.py @@ -15,12 +15,37 @@ def get_fun_name(node: MessageNode): + """Extract the function name from a MessageNode. + + Parameters + ---------- + node : MessageNode + The node to extract the function name from. + + Returns + ------- + str + The function name, either from node.info['fun_name'] or + extracted from the node name. + """ if isinstance(node.info, dict) and "fun_name" in node.info: return node.info["fun_name"] return node.name.split(":")[0] def repr_function_call(child: MessageNode): + """Generate a string representation of a function call from a MessageNode. + + Parameters + ---------- + child : MessageNode + The node representing a function call. + + Returns + ------- + str + String representation in format: 'output = function(arg1=val1, arg2=val2)'. + """ function_call = f"{child.py_name} = {get_fun_name(child)}(" for k, v in child.inputs.items(): function_call += f"{k}={v.py_name}, " @@ -29,7 +54,29 @@ def repr_function_call(child: MessageNode): def node_to_function_feedback(node_feedback: TraceGraph): - """Convert a TraceGraph to a FunctionFeedback. roots, others, outputs are dict of variable name and its data and constraints.""" + """Convert a TraceGraph to a FunctionFeedback structure. + + Parameters + ---------- + node_feedback : TraceGraph + The trace graph containing nodes and feedback to convert. + + Returns + ------- + FunctionFeedback + Structured feedback with separated roots, intermediates, and outputs. + + Notes + ----- + The conversion process: + 1. Traverses the graph in topological order + 2. Classifies nodes as roots, intermediates, or outputs + 3. Extracts function documentation and call representations + 4. Preserves user feedback from the original graph + + Roots include both true root nodes and 'blanket' nodes whose + parents haven't been visited yet. + """ depth = 0 if len(node_feedback.graph) == 0 else node_feedback.graph[-1][0] graph = [] others = {} @@ -72,7 +119,31 @@ def node_to_function_feedback(node_feedback: TraceGraph): @dataclass class FunctionFeedback: - """Feedback container used by FunctionPropagator.""" + """Container for structured feedback from function execution traces. + + Used by OptoPrime to organize execution traces into a format suitable + for LLM-based optimization. + + Attributes + ---------- + graph : list[tuple[int, str]] + Topologically sorted function calls with (depth, representation) pairs. + documentation : dict[str, str] + Mapping of function names to their documentation strings. + others : dict[str, Any] + Intermediate variables with (data, description) tuples. + roots : dict[str, Any] + Input/root variables with (data, description) tuples. + output : dict[str, Any] + Output/leaf variables with (data, description) tuples. + user_feedback : str + User-provided feedback about the execution. + + Notes + ----- + This structure separates the execution trace into logical components + that can be formatted into prompts for LLM-based optimization. + """ graph: List[ Tuple[int, str] @@ -142,6 +213,113 @@ def __repr__(self) -> str: class OptoPrime(Optimizer): + """Language model-based optimizer for text and code parameters. + + OptoPrime implements optimization through structured problem representation and + language model reasoning. It converts execution traces into problem instances + that language models can understand and improve. + + The optimizer operates by: + 1. Collecting execution traces and feedback from the computation graph + 2. Converting traces into structured problem representations + 3. Prompting language models to suggest parameter improvements + 4. Extracting and applying suggested updates to parameters + + Parameters + ---------- + parameters : list[ParameterNode] + List of trainable parameters to optimize. + llm : AbstractModel, optional + Language model for generating parameter updates, by default None (uses default LLM). + propagator : Propagator, optional + Custom propagator for trace graph processing, by default None. + objective : str, optional + Optimization objective description, by default uses default_objective. + ignore_extraction_error : bool, default=True + Whether to ignore type conversion errors when extracting LLM suggestions. + include_example : bool, default=False + Whether to include example problems in prompts. + memory_size : int, default=0 + Size of feedback memory buffer for historical context. + max_tokens : int, default=4096 + Maximum tokens for language model responses. + log : bool, default=True + Whether to log optimization steps and responses. + prompt_symbols : dict, optional + Custom symbols for prompt sections (e.g., "#Variables", "#Code"). + json_keys : dict, optional + Keys for JSON response format (reasoning, answer, suggestion). + use_json_object_format : bool, default=True + Whether to request JSON object format from LLM. + highlight_variables : bool, default=False + Whether to highlight variables at the end of prompts. + **kwargs + Additional keyword arguments passed to parent class. + + Attributes + ---------- + llm : AbstractModel + The language model used for optimization. + objective : str + The optimization objective description. + log : list or None + Log of optimization steps if logging is enabled. + summary_log : list or None + Log of problem summaries if logging is enabled. + memory : FIFOBuffer + Buffer storing historical feedback. + + Methods + ------- + summarize() + Aggregate feedback into structured problem representation. + problem_instance(summary, mask=None) + Create a ProblemInstance from aggregated feedback. + extract_llm_suggestion(response) + Parse LLM response to extract parameter updates. + + Notes + ----- + OptoPrime excels at optimizing: + - Natural language prompts and instructions + - Code implementations and algorithms + - Mixed text-code parameters + - Parameters with complex constraints + + The optimizer uses structured problem representations that separate: + - Variables (trainable parameters) + - Inputs (non-trainable values) + - Code (execution trace) + - Outputs (results) + - Feedback (optimization signals) + + This structure enables language models to understand the optimization + context and suggest targeted improvements. + + See Also + -------- + Optimizer : Base optimizer class + OptoPrimeV2 : Enhanced version with improved prompt engineering + TextGrad : Alternative text-based optimizer + + Examples + -------- + >>> from opto.optimizers import OptoPrime + >>> from opto.trace import node + >>> + >>> # Create trainable parameters + >>> prompt = node("Explain quantum computing", trainable=True) + >>> + >>> # Initialize optimizer + >>> optimizer = OptoPrime([prompt], objective="Make explanation clearer") + >>> + >>> # Run optimization loop + >>> for _ in range(5): + ... output = model(prompt) + ... feedback = evaluate(output) + ... optimizer.backward(feedback) + ... optimizer.step() + """ # This is generic representation prompt, which just explains how to read the problem. representation_prompt = dedent( """ @@ -351,6 +529,34 @@ def default_propagator(self): return GraphPropagator() def summarize(self): + """Aggregate feedback from parameters into a structured summary. + + Collects and organizes feedback from all trainable parameters into + a FunctionFeedback structure suitable for problem representation. + + Returns + ------- + FunctionFeedback + Structured feedback containing: + - variables: Trainable parameters with values and descriptions + - inputs: Non-trainable root nodes + - graph: Topologically sorted function calls + - others: Intermediate computation values + - output: Final output values + - documentation: Function documentation strings + - user_feedback: Aggregated user feedback + + Notes + ----- + The method performs several transformations: + 1. Aggregates feedback from all trainable parameters + 2. Converts the trace graph to FunctionFeedback structure + 3. Separates root nodes into variables (trainable) and inputs (non-trainable) + 4. Preserves the computation graph and intermediate values + + Parameters without feedback (disconnected from output) are still + included in the summary but may not receive updates. + """ # Aggregate feedback from all the parameters feedbacks = [ self.propagator.aggregate(node.feedback) @@ -380,6 +586,18 @@ def summarize(self): @staticmethod def repr_node_value(node_dict): + """Format node values for display. + + Parameters + ---------- + node_dict : dict + Dictionary of node names to (value, description) tuples. + + Returns + ------- + str + Formatted string with type and value for each node. + """ temp_list = [] for k, v in node_dict.items(): if "__code" not in k: @@ -390,6 +608,19 @@ def repr_node_value(node_dict): @staticmethod def repr_node_constraint(node_dict): + """Format node constraints for display. + + Parameters + ---------- + node_dict : dict + Dictionary of node names to (value, description) tuples. + + Returns + ------- + str + Formatted string with type and constraint for each node. + Only includes nodes with non-None descriptions. + """ temp_list = [] for k, v in node_dict.items(): if "__code" not in k: @@ -401,6 +632,30 @@ def repr_node_constraint(node_dict): return "\n".join(temp_list) def problem_instance(self, summary, mask=None): + """Create a ProblemInstance from aggregated feedback. + + Converts a FunctionFeedback summary into a formatted problem + representation for the language model. + + Parameters + ---------- + summary : FunctionFeedback + Aggregated feedback from summarize() method. + mask : list[str], optional + List of sections to exclude from the problem instance. + Can include: "#Instruction", "#Code", "#Variables", etc. + + Returns + ------- + ProblemInstance + Structured problem representation with all sections + formatted for language model consumption. + + Notes + ----- + The mask parameter allows selective inclusion of problem + components, useful for ablation studies or focused optimization. + """ mask = mask or [] return ProblemInstance( instruction=self.objective if "#Instruction" not in mask else "", diff --git a/opto/optimizers/optoprimemulti.py b/opto/optimizers/optoprimemulti.py index be7cfa30..aba40ee0 100644 --- a/opto/optimizers/optoprimemulti.py +++ b/opto/optimizers/optoprimemulti.py @@ -9,6 +9,100 @@ from concurrent.futures import ThreadPoolExecutor, as_completed class OptoPrimeMulti(OptoPrime): + """Multi-response variant of OptoPrime optimizer with advanced candidate generation and selection. + + Extends OptoPrime to generate multiple candidate solutions using various techniques, + then select the best one through sophisticated selection methods. Supports multiple + LLM profiles, diverse generation strategies, and parallel processing for improved + optimization performance. + + Parameters + ---------- + *args + Arguments passed to parent OptoPrime constructor. + num_responses : int, default=3 + Number of candidate responses to generate per optimization step. + temperature_min_max : List[float], optional + [min, max] temperature range for response generation. Defaults to [0.0, 1.0]. + selector : callable, optional + Custom function for selecting the best candidate from generated responses. + If None, uses built-in selection techniques. + generation_technique : str, default="temperature_variation" + Technique for generating diverse candidates: + - "temperature_variation": Use varying temperatures across responses + - "self_refinement": Each solution refines the previous one iteratively + - "iterative_alternatives": Generate alternatives informed by previous solutions + - "multi_experts": Use different expert personas for diverse perspectives + - "multi_llm": Use different LLM profiles for generation diversity + selection_technique : str, default="best_of_n" + Method for selecting the best candidate: + - "best_of_n": Choose most promising candidate via LLM evaluation + - "moa"/"mixture_of_agents": Synthesize best elements from all candidates + - "majority": Find consensus solution using clustering + - "last_of_n": Simply return the last generated candidate + experts_list : List[str], optional + List of expert personas for multi_experts generation technique. + If None, experts are automatically generated based on the problem. + llm_profiles : List[str], optional + List of LLM profile names to use for diverse generation. + Enables multi-model optimization approaches. + llm_weights : List[float], optional + Weights for each LLM profile when using weighted selection. + Defaults to equal weights if not specified. + **kwargs + Additional keyword arguments passed to parent OptoPrime constructor. + + Attributes + ---------- + candidates : List[str] + All candidate solutions generated in the current optimization step. + selected_candidate : str or Dict + The candidate solution selected for the current step. + num_responses : int + Number of responses to generate per step. + temperature_min_max : List[float] + Temperature range for generation diversity. + generation_technique : str + Current technique used for candidate generation. + selection_technique : str + Current technique used for candidate selection. + + Methods + ------- + generate_candidates(summary, system_prompt, user_prompt, **kwargs) + Generate multiple candidate solutions using the specified technique. + select_candidate(candidates, selection_technique, problem_summary) + Select the best candidate from generated responses. + _step(verbose, mask, **kwargs) + Perform one optimization step with multi-candidate approach. + + Notes + ----- + OptoPrimeMulti enhances optimization through several mechanisms: + + 1. **Diverse Generation**: Multiple techniques ensure candidate diversity, + preventing local optima and exploring the solution space more thoroughly. + + 2. **Parallel Processing**: Concurrent LLM calls reduce optimization time + while maintaining result quality and deterministic ordering. + + 3. **Advanced Selection**: Sophisticated selection methods choose optimal + solutions by analyzing candidate strengths and synthesizing improvements. + + 4. **Multi-Model Support**: Different LLM profiles provide diverse + perspectives and capabilities for complex optimization problems. + + The optimizer is particularly effective for: + - Complex optimization problems requiring creative solutions + - Scenarios where single-shot optimization may get stuck in local optima + - Applications benefiting from ensemble approaches and diverse perspectives + + See Also + -------- + OptoPrime : Base single-response optimizer + OptoPrimeV2 : Enhanced version with XML-based memory representation + """ + def __init__( self, *args, diff --git a/opto/optimizers/textgrad.py b/opto/optimizers/textgrad.py index ecdb6dcd..492e0310 100644 --- a/opto/optimizers/textgrad.py +++ b/opto/optimizers/textgrad.py @@ -92,18 +92,34 @@ def construct_tgd_prompt( do_in_context_examples: bool = False, **optimizer_kwargs, ): - """ - Construct the textual gradient descent prompt. - - :param do_momentum: Whether to include momentum in the prompt. - :type do_momentum: bool, optional - :param do_constrained: Whether to include constraints in the prompt. - :type do_constrained: bool, optional - :param do_in_context_examples: Whether to include in-context examples in the prompt. - :type do_in_context_examples: bool, optional - :param optimizer_kwargs: Additional keyword arguments for formatting the prompt. These will be things like the variable description, gradient, past values, constraints, and in-context examples. - :return: The TGD update prompt. - :rtype: str + """Construct a textual gradient descent prompt with optional components. + + This function builds prompts for the TextGrad optimization algorithm, incorporating + various optional components like momentum, constraints, and in-context examples + to guide the language model in improving variable values. + + Parameters + ---------- + do_momentum : bool, optional + Whether to include momentum information from past iterations, by default False. + do_constrained : bool, optional + Whether to include constraint specifications in the prompt, by default False. + do_in_context_examples : bool, optional + Whether to include example demonstrations, by default False. + **optimizer_kwargs + Additional formatting parameters including variable descriptions, gradients, + past values, constraints, and examples. + + Returns + ------- + str or list[str] + Formatted prompt string, or list of prompt parts for multipart gradients. + + Notes + ----- + The function handles both single-string gradients and multipart gradient contexts. + For multipart gradients, it returns a list where the first elements contain + context and the last element contains the main prompt. """ if isinstance(optimizer_kwargs["variable_grad"], str): @@ -241,6 +257,30 @@ def construct_tgd_prompt( @dataclass class GradientInfo: + """Container for gradient information in TextGrad optimization. + + This class stores gradient (feedback) text along with optional context information + that provides additional details about the gradient computation. + + Parameters + ---------- + gradient : str + The main feedback or gradient text. + gradient_context : dict[str, str], optional + Additional context information for the gradient, by default None. + + Attributes + ---------- + gradient : str + Feedback text for parameter optimization. + gradient_context : dict[str, str] or None + Optional context dictionary with additional gradient information. + + Notes + ----- + The class supports indexing and length operations for convenient access to + gradient components in optimization workflows. + """ gradient: str # feedback gradient_context: Optional[Dict[str, str]] @@ -305,6 +345,60 @@ def get_short_value(text, n_words_offset: int = 10) -> str: class TextGrad(Optimizer): + """TextGrad optimizer implementing automatic differentiation for text-based parameters. + + TextGrad extends traditional gradient-based optimization to textual parameters by using + language models to compute gradients (feedback) and apply updates. It performs backward + propagation through computation graphs where nodes represent text values and edges + represent textual transformations. + + The optimizer operates by: + 1. Computing textual gradients through backward propagation using language models + 2. Reducing multiple gradients through aggregation prompts + 3. Generating parameter updates based on accumulated feedback + 4. Applying updates to improve parameter values iteratively + + Parameters + ---------- + parameters : list[ParameterNode] + List of parameter nodes to optimize. + llm : AbstractModel, optional + Language model for gradient computation and updates, by default None. + propagator : Propagator, optional + Custom propagator for trace graph processing, by default None. + objective : str, optional + Optimization objective description, by default None. + max_tokens : int, optional + Maximum tokens for language model calls, by default 4096. + log : bool, optional + Whether to log optimization steps, by default False. + *args + Additional positional arguments passed to parent class. + **kwargs + Additional keyword arguments passed to parent class. + + Attributes + ---------- + llm : AbstractModel + Language model instance for optimization operations. + print_limit : int + Character limit for printed outputs. + max_tokens : int + Token limit for language model calls. + new_variable_tags : list[str] + Tags used to extract updated variables from LLM responses. + optimizer_system_prompt : str + System prompt for variable update operations. + log : list or None + Optimization log if logging is enabled. + + Notes + ----- + This implementation is adapted from the TextGrad paper and codebase, providing + automatic differentiation for text-based optimization problems. The optimizer + handles complex prompt engineering scenarios where traditional gradient-based + methods are not applicable. + """ def __init__( self, diff --git a/opto/trace/README.md b/opto/trace/README.md deleted file mode 100644 index 83219504..00000000 --- a/opto/trace/README.md +++ /dev/null @@ -1 +0,0 @@ -sudo apt install graphviz diff --git a/opto/trace/broadcast.py b/opto/trace/broadcast.py index f157aa1f..0f05fda1 100644 --- a/opto/trace/broadcast.py +++ b/opto/trace/broadcast.py @@ -5,14 +5,45 @@ def recursive_conversion(true_func, false_func): - """Recursively apply true_func to the nodes and false_func to the rest of - the objects in a container of nodes. Container of nodes are tuple, list, - dict, set, and NodeContainer. - - Args: - true_func (callable): the function to be applied to the nodes. - false_func (callable): the function to be applied to the rest of the objects. - + """Recursively apply functions to nodes and non-nodes in nested structures. + + Creates a function that traverses nested data structures, applying + different functions to Node objects versus other objects. + + Parameters + ---------- + true_func : callable + Function to apply to Node objects. + false_func : callable + Function to apply to non-Node objects. + + Returns + ------- + callable + A function that recursively processes nested structures. + + Notes + ----- + Supported container types: + - tuple, list, dict, set: Recursively processed + - NodeContainer: Attributes recursively processed + - Node: true_func applied + - Other: false_func applied + + The returned function preserves the structure while transforming + the contents. Commonly used for: + - Extracting data from nested nodes + - Converting between node and non-node representations + - Applying transformations while maintaining structure + + Examples + -------- + >>> # Extract data from nested nodes + >>> extract = recursive_conversion( + ... true_func=lambda n: n.data, + ... false_func=lambda x: x + ... ) + >>> result = extract(nested_structure) """ def func(obj): @@ -39,13 +70,49 @@ def func(obj): # TODO to test it and clean up the code def apply_op(op, output, *args, **kwargs): - """A broadcasting operation that applies an op to container of Nodes. - - Args: - op (callable): the operator to be applied. - output (Any): the container to be updated. - *args (Any): the positional inputs of the operator. - **kwargs (Any): the keyword inputs of the operator. + """Apply an operator to containers of nodes with broadcasting. + + Enables element-wise operations on mixed containers of nodes and + regular values, similar to NumPy broadcasting but for Node objects. + + Parameters + ---------- + op : callable + The operator to apply element-wise. + output : Any + Container template determining output structure. + Can be list, tuple, dict, or NodeContainer. + *args : Any + Positional arguments for the operator. + Each can be a Node or container matching output type. + **kwargs : Any + Keyword arguments for the operator. + Each can be a Node or container matching output type. + + Returns + ------- + Any + Result with same structure as output, containing results + of applying op element-wise. + + Raises + ------ + AssertionError + If container types don't match or lengths differ. + + Notes + ----- + Broadcasting rules: + 1. If all inputs are Nodes, applies op directly + 2. For containers, applies op element-wise: + - Lists/tuples: By index + - Dicts: By key + - NodeContainers: By attribute + 3. Node inputs are broadcast to all elements + 4. Container inputs must match output structure + + The function modifies output in-place for most containers + but returns a new tuple for tuple inputs. """ inputs = list(args) + list(kwargs.values()) diff --git a/opto/trace/bundle.py b/opto/trace/bundle.py index 6d5fa5c8..dbe1d5b8 100644 --- a/opto/trace/bundle.py +++ b/opto/trace/bundle.py @@ -27,7 +27,19 @@ # This is a global flag to allow external dependencies to be used in the operator. ALLOW_EXTERNAL_DEPENDENCIES = None def disable_external_dependencies_check(allow_external_dependencies: bool): - """Set the global flag for allowing external dependencies.""" + """Set the global flag for allowing external dependencies in operators. + + Parameters + ---------- + allow_external_dependencies : bool + If True, operators can use nodes that are not explicitly passed as inputs. + If False, all node dependencies must be explicit inputs. + + Notes + ----- + This global setting affects all subsequently created bundle decorators. + Use with caution as it can make dependency tracking less explicit. + """ global ALLOW_EXTERNAL_DEPENDENCIES ALLOW_EXTERNAL_DEPENDENCIES = allow_external_dependencies @@ -41,23 +53,87 @@ def bundle( overwrite_python_recursion=False, projections=None, ): - """Wrap a function as a FunModule which returns node objects. - - The input signature to the wrapped function stays the same. bundle can be used with other decorators - so long as they are not named 'bundle'. - - Args: - description (str, optional): Description of the operator. Defaults to None. - traceable_code (bool, optional): Whether the operator's code is traceable by Trace. Defaults to False. - _process_inputs (bool, optional): Whether to extract input from container of nodes. Defaults to True. - trainable (bool, optional): Whether block of code is treated as variable in optimization. Defaults to False. - catch_execution_error (bool, optional): Whether to catch exceptions during operator execution. Defaults to True. - allow_external_dependencies (bool, optional): Whether to allow external dependencies. Defaults to False. - overwrite_python_recursion (bool, optional): Whether to overwrite Python recursion behavior. Defaults to False. - projections (List[Projection], optional): List of projections to be used in updating trainable parameter. Defaults to None. - - Returns: - FunModule: The wrapped function that returns node objects. + """Decorator to wrap functions for integration with the Trace computation graph. + + The bundle decorator transforms regular Python functions into traced operators that + automatically create MessageNodes when called with Node inputs. This enables automatic + differentiation and optimization of the wrapped code. + + Parameters + ---------- + description : str, optional + Description of the operator in format "[op_name] details". If not provided, + generated from function name and docstring. + traceable_code : bool, default=False + If True, the function's internal operations are also traced, creating a nested + graph structure. Enables fine-grained optimization but increases overhead. + _process_inputs : bool, default=True + If True, automatically extracts data from Node inputs before passing to function. + If False, passes Node objects directly to the function. + trainable : bool, default=False + If True, treats the function's source code as a trainable parameter that can + be modified during optimization. Enables code synthesis and modification. + catch_execution_error : bool, default=True + If True, exceptions are caught and converted to ExceptionNodes in the graph. + If False, exceptions propagate normally. + allow_external_dependencies : bool, default=False + If True, permits the function to use nodes not passed as explicit inputs. + These hidden dependencies are tracked separately. + overwrite_python_recursion : bool, default=False + If True, recursive calls within the function call the original function directly. + If False, recursive calls go through the wrapped version. + projections : list[Projection], optional + Constraints to apply when updating trainable code parameters. + + Returns + ------- + FunModule + A wrapped version of the function that: + - Creates MessageNodes when called with Node inputs + - Preserves the original function signature + - Can be used with other decorators + + Notes + ----- + The bundle decorator enables several key features: + + 1. **Automatic Tracing**: Functions automatically participate in the computation + graph when called with Node arguments. + + 2. **Error Handling**: Exceptions can be captured as part of the graph, enabling + error-aware optimization. + + 3. **Code as Data**: With trainable=True, function source code becomes data that + can be optimized, enabling program synthesis. + + 4. **Nested Tracing**: With traceable_code=True, operations inside the function + are individually traced, enabling fine-grained optimization. + + 5. **Compatibility**: The wrapped function maintains its original signature and + can be used normally with non-Node inputs when tracing is disabled. + + See Also + -------- + FunModule : The class that implements bundled functions + MessageNode : Nodes created by bundled functions + ParameterNode : Created when trainable=True + + Examples + -------- + >>> # Simple bundled function + >>> @bundle() + >>> def add(a, b): + ... return a + b + >>> + >>> # Bundled function with trainable code + >>> @bundle(trainable=True, description="[optimizer] Optimization step") + >>> def optimize_step(gradient, parameter): + ... return parameter - 0.01 * gradient + >>> + >>> # Using bundled functions + >>> x = node(5) + >>> y = node(3) + >>> z = add(x, y) # Creates MessageNode with value 8 """ prev_f_locals = inspect.stack()[1].frame.f_locals @@ -80,7 +156,27 @@ def decorator(fun): class trace_nodes: - """This is a context manager for keeping track which nodes are read/used in an operator.""" + """Context manager for tracking node usage within operators. + + Maintains a thread-local stack of sets that record which nodes are accessed + during operator execution. This enables detection of implicit dependencies + and proper graph construction. + + Notes + ----- + This context manager is used internally by FunModule to track node dependencies. + It uses contextvars for thread-safety and async compatibility. + + The tracking works by: + 1. Creating a new set for nodes on __enter__ + 2. Nodes add themselves to this set when their data is accessed + 3. The set is removed from the stack on __exit__ + + See Also + -------- + USED_NODES : Global context variable maintaining the stack + Node.data : Property that registers access in USED_NODES + """ def __init__(self): self.token = None @@ -102,19 +198,76 @@ def __exit__(self, type, value, traceback): USED_NODES.reset(self.token) class FunModule(Module): - """This is a decorator to trace a function. The wrapped function returns a MessageNode. - - Args: - fun (callable): the operator to be traced. - description (str): a description of the operator; see the MessageNode for syntax. - _process_inputs (bool): if True, the input is extracted from the container of nodes; if False, the inputs are passed directly to the underlying function. - trainable (bool): if True, the block of code is treated as a variable in the optimization - traceable_code (bool): if True, the operator's code is traceable by Trace - catch_execution_error (bool): if True, the operator catches the exception raised during the execution of the operator and return ExecutionError. - allow_external_dependencies (bool): if True, the operator allows external dependencies to be used in the operator. Namely, not all nodes used to create the output are in the inputs. In this case, the extra dependencies are stored in the info dictionary with key 'extra_dependencies'. - overwrite_python_recursion (bool): if True, the operator allows the python recursion behavior of calling the decorated function to be overwritten. When true, applying bundle on a recursive function, would be the same as calling the function directly. When False, the Python's oriignal recursion behavior of decorated functions is preserved. - _ldict (dict): the local dictionary to execute the code block. - + """Wrapper class that traces function execution in the computation graph. + + FunModule implements the bundle decorator's functionality, transforming regular + Python functions into graph-aware operators that create MessageNodes. It handles + input processing, error catching, dependency tracking, and code trainability. + + Parameters + ---------- + fun : callable + The function to be wrapped and traced. + description : str, optional + Operator description in format "[op_name] details". Auto-generated if not provided. + traceable_code : bool, default=False + Whether to trace operations inside the function. + _process_inputs : bool, default=True + Whether to extract data from Node inputs before passing to function. + trainable : bool, default=False + Whether the function's source code is a trainable parameter. + catch_execution_error : bool, default=True + Whether to catch and wrap exceptions as ExceptionNodes. + allow_external_dependencies : bool, default=False + Whether to permit hidden node dependencies. + overwrite_python_recursion : bool, default=False + Whether recursive calls bypass the wrapper. + projections : list[Projection], optional + Constraints for trainable code updates. + _ldict : dict, optional + Local namespace for code execution. + + Attributes + ---------- + info : dict + Metadata about the wrapped function including source, signature, and execution details. + parameter : ParameterNode or None + The trainable code parameter when trainable=True. + trainable : bool + Whether the code is trainable. + + Methods + ------- + forward(*args, **kwargs) + Execute the wrapped function with tracing. + get_source(fun) + Extract source code from a function. + + Notes + ----- + FunModule implements sophisticated function wrapping: + + 1. **Input Processing**: Automatically converts Node inputs to data values + for the wrapped function, unless _process_inputs=False. + + 2. **Dependency Tracking**: Uses trace_nodes context to detect all nodes + accessed during execution, even indirect ones. + + 3. **Error Handling**: Can catch exceptions and convert them to ExceptionNodes, + preserving error information in the graph. + + 4. **Code Trainability**: When trainable=True, the function's source code + becomes a ParameterNode that optimizers can modify. + + 5. **Traceable Execution**: When traceable_code=True, creates detailed graphs + of internal operations for fine-grained optimization. + + See Also + -------- + bundle : Decorator that creates FunModule instances + Module : Base class for Trace modules + MessageNode : Nodes created by FunModule execution + ParameterNode : Code parameters when trainable=True """ def __init__( @@ -756,22 +909,100 @@ def get_source(self, obj: Any, bug_mode=False): def to_data(obj): - """Extract the data from a node or a container of nodes.""" + """Extract data from nodes in nested structures. + + Parameters + ---------- + obj : Any + A node, container of nodes, or nested structure containing nodes. + + Returns + ------- + Any + The same structure with Node objects replaced by their data values. + + Notes + ----- + Recursively traverses nested structures (lists, dicts, tuples, sets, + NodeContainers) and extracts the data from all Node objects while + preserving the structure. Non-Node objects are returned unchanged. + + Examples + -------- + >>> x = node(5) + >>> y = node(10) + >>> to_data([x, y, 15]) # Returns [5, 10, 15] + """ return recursive_conversion(lambda x: x.data, lambda x: x)(obj) def wrap_node(obj): - """Wrap a node on top of the original object""" + """Wrap non-Node objects in nodes throughout nested structures. + + Parameters + ---------- + obj : Any + Any object or nested structure potentially containing non-Node values. + + Returns + ------- + Any + The same structure with non-Node objects wrapped in Node instances. + + Notes + ----- + Recursively traverses nested structures and wraps any non-Node objects + in Node instances. Existing Node objects are left unchanged. Useful for + ensuring all values in a structure are nodes before processing. + """ return recursive_conversion(lambda x: x, lambda x: node(x))(obj) def detach_inputs(obj): - """Detach a node or a container of nodes.""" + """Detach nodes from the computation graph in nested structures. + + Parameters + ---------- + obj : Any + A node, container of nodes, or nested structure containing nodes. + + Returns + ------- + Any + The same structure with all nodes detached from the graph. + + Notes + ----- + Recursively traverses nested structures and calls detach() on all + Node objects, removing them from gradient computation. Non-Node + objects are returned unchanged. Useful for creating non-differentiable + copies of values. + """ return recursive_conversion(lambda x: x.detach(), lambda x: x)(obj) def update_local(frame, name, value): - """Update the value of a local variable in a frame.""" + """Update a local variable in a Python frame. + + Parameters + ---------- + frame : frame + The Python frame object to modify. + name : str + Name of the local variable to update. + value : Any + New value for the variable. + + Notes + ----- + This low-level function modifies Python frame locals directly using + ctypes. It's used internally for trainable code execution where + local variables need to be dynamically updated. The PyFrame_LocalsToFast + call ensures the change is reflected in the actual frame. + + Warning: This is an advanced internal function that modifies Python + internals and should be used with caution. + """ frame.f_locals[name] = value ctypes.pythonapi.PyFrame_LocalsToFast(ctypes.py_object(frame), ctypes.c_int(0)) diff --git a/opto/trace/containers.py b/opto/trace/containers.py index f375ab47..21dfe370 100644 --- a/opto/trace/containers.py +++ b/opto/trace/containers.py @@ -6,12 +6,46 @@ class NodeContainer: - """An identifier for a container of nodes.""" + """Base marker class for containers that hold nodes. + + This class serves as an identifier to distinguish containers of nodes + from regular Python containers. It has no implementation, serving only + as a type marker for isinstance checks. + + Notes + ----- + NodeContainer is used as a base class to identify objects that contain + Node objects and may need special handling during graph construction + and parameter collection. + + See Also + -------- + ParameterContainer : Extends NodeContainer with parameter management + Seq : List-like container for nodes + Map : Dict-like container for nodes + """ ... def trainable_method(method): + """Check if a method is trainable. + + Parameters + ---------- + method : Any + The method or attribute to check. + + Returns + ------- + bool + True if the method is a trainable FunModule, False otherwise. + + Notes + ----- + Used internally to identify trainable bundled methods when collecting + parameters from a container. + """ from opto.trace.bundle import FunModule if isinstance(method, FunModule): @@ -20,11 +54,69 @@ def trainable_method(method): class ParameterContainer(NodeContainer): - """A container of parameter nodes.""" + """Base class for containers that manage parameter nodes. + + ParameterContainer provides automatic collection and management of + ParameterNode objects and nested containers. It serves as the foundation + for models and modules in the Trace framework. + + Methods + ------- + parameters() + Return a flattened list of all parameters. + parameters_dict() + Return a dictionary of all parameters and containers. + copy() + Create a deep copy with shared parameter references. + + Notes + ----- + ParameterContainer implements sophisticated parameter collection: + + 1. **Automatic Discovery**: Scans attributes to find ParameterNodes, + trainable methods, and nested ParameterContainers. + + 2. **Recursive Collection**: Traverses nested containers to collect + all parameters in the hierarchy. + + 3. **Method Support**: Recognizes and collects parameters from + trainable bundled methods. + + 4. **Efficient Copying**: The copy() method creates new container + instances while sharing parameter references, useful for + creating model variants. + + The parameter collection logic handles: + - Direct ParameterNode attributes + - Trainable FunModule methods + - Nested ParameterContainers + - Class methods wrapped with functools.partial + + See Also + -------- + Module : Extends ParameterContainer with forward() method + ParameterNode : The parameters being collected + bundle : Decorator that can make methods trainable + """ def parameters(self): - """Return a flattned list of all the parameters in the model's - parameters_dict, useful for optimization.""" + """Return a flattened list of all parameters in the container. + + Returns + ------- + list[ParameterNode] + All ParameterNode objects in this container and nested containers. + + Raises + ------ + ValueError + If the container contains an unknown parameter type. + + Notes + ----- + Recursively traverses nested ParameterContainers to collect all + parameters. The returned list is suitable for passing to optimizers. + """ parameters = [] for k, v in self.parameters_dict().items(): if isinstance(v, ParameterNode): @@ -73,8 +165,25 @@ def parameters_dict(self): return parameters # include both trainable and non-trainable parameters def copy(self): - """Return a deep copy of the ParameterContainer except for the parameters - are set to the originals.""" + """Create a deep copy with shared parameter references. + + Returns + ------- + ParameterContainer + A new container with copied structure but shared parameters. + + Notes + ----- + This method creates new container instances while maintaining + references to the original ParameterNode objects. This is useful + for creating model variants that share parameters but have + independent structure. + + The copying process: + 1. Deep copies the entire container structure + 2. Replaces parameter references with originals + 3. Recursively applies to nested containers + """ # NOTE This current code is not optimized for speed; it does extra traversals and copying. @@ -98,9 +207,40 @@ def copy(self): return new_container class Seq(UserList, ParameterContainer): - """ - Seq is defined as having a length and an index. - Python's list/tuple will be converted to Seq + """List-like container for managing sequences of nodes and parameters. + + Seq provides a list interface while supporting automatic parameter + collection from contained nodes and nested containers. + + Parameters + ---------- + *args + Either a single sequence-like object or multiple items to store. + If a single argument with __len__ and __getitem__ is provided, + it's used as the sequence. Otherwise, all arguments become items. + + Attributes + ---------- + data : list + The underlying list storage (inherited from UserList). + + Methods + ------- + parameters_dict() + Return dictionary of contained parameters. + + Notes + ----- + Seq is automatically used when converting Python lists/tuples that + contain nodes. It maintains list semantics while enabling: + - Parameter collection from contained ParameterNodes + - Recursive parameter discovery in nested containers + - Standard list operations (indexing, iteration, etc.) + + See Also + -------- + Map : Dictionary-like container for nodes + ParameterContainer : Base class for parameter management """ def __init__(self, *args): @@ -134,9 +274,41 @@ def parameters_dict(self): class Map(UserDict, ParameterContainer): - """ - Map is defined as key and value - Python's dict will be converted to Map + """Dictionary-like container for managing mappings of nodes and parameters. + + Map provides a dictionary interface while supporting automatic parameter + collection from contained nodes and nested containers. + + Parameters + ---------- + mapping : dict + Initial dictionary of key-value pairs. + + Attributes + ---------- + data : dict + The underlying dictionary storage (inherited from UserDict). + + Methods + ------- + parameters_dict() + Return dictionary of contained parameters. + + Notes + ----- + Map is automatically used when converting Python dictionaries that + contain nodes. It maintains dictionary semantics while enabling: + - Parameter collection from contained ParameterNodes + - Recursive parameter discovery in nested containers + - Standard dictionary operations (key access, iteration, etc.) + + The parameters_dict() method uses dictionary values as parameter + identifiers when they are ParameterNodes or containers. + + See Also + -------- + Seq : List-like container for nodes + ParameterContainer : Base class for parameter management """ def __init__(self, mapping): diff --git a/opto/trace/errors.py b/opto/trace/errors.py index 70943124..849fe920 100644 --- a/opto/trace/errors.py +++ b/opto/trace/errors.py @@ -2,7 +2,36 @@ class ExecutionError(Exception): - """Base class for execution error in code tracing.""" + """Exception raised when traced code execution fails. + + Wraps an ExceptionNode to preserve error information in the computation + graph while still raising a Python exception. + + Parameters + ---------- + exception_node : ExceptionNode + The ExceptionNode containing error details from the failed operation. + + Attributes + ---------- + exception_node : ExceptionNode + The wrapped exception node with full error context. + + Notes + ----- + ExecutionError enables error-aware optimization by: + 1. Preserving error information in the computation graph + 2. Providing full traceback for debugging + 3. Allowing optimizers to learn from execution failures + + The string representation shows the full traceback from the original + error, making debugging easier. + + See Also + -------- + ExceptionNode : Node type that captures exceptions in the graph + bundle : Decorator that can catch and wrap ExecutionErrors + """ def __init__(self, exception_node: ExceptionNode): self.exception_node = exception_node @@ -13,6 +42,31 @@ def __str__(self): class TraceMissingInputsError(Exception): + """Exception raised when required inputs are missing during tracing. + + This error occurs when a traced operation cannot find all necessary + input nodes in the computation graph. + + Parameters + ---------- + message : str + Description of which inputs are missing. + + Attributes + ---------- + message : str + The error message describing missing inputs. + + Notes + ----- + This exception typically indicates: + 1. A node was used before being defined + 2. External dependencies are used without allow_external_dependencies=True + 3. Input processing failed to extract required nodes + + The error helps identify graph construction issues early in the + execution process. + """ def __init__(self, message: str): self.message = message super().__init__(self.message) diff --git a/opto/trace/iterators.py b/opto/trace/iterators.py index 207e7bb9..0f39f432 100644 --- a/opto/trace/iterators.py +++ b/opto/trace/iterators.py @@ -8,7 +8,35 @@ # List[Nodes], Node[List] def iterate(x: Any): - """Return an iterator object for node of list, tuple, set, or dict.""" + """Create an iterator for node containers. + + Parameters + ---------- + x : Any + A node or value to iterate over. Can be list, tuple, set, dict, + string, or numpy array. + + Returns + ------- + SeqIterable or DictIterable + An iterator object that yields nodes during iteration. + + Raises + ------ + ExecutionError + If the input is not iterable. + + Notes + ----- + This function enables iteration over node containers in traced code: + - Lists, tuples, strings, arrays → SeqIterable + - Sets → Converted to list then SeqIterable + - Dicts → SeqIterable over keys + - Non-iterables → Raises ExecutionError with ExceptionNode + + The returned iterator creates child nodes during iteration, + maintaining proper parent-child relationships in the graph. + """ if not isinstance(x, Node): x = node(x) if issubclass(x.type, list) or issubclass(x.type, tuple) or issubclass(x.type, str) or issubclass(x.type, np.ndarray): @@ -35,6 +63,41 @@ def iterate(x: Any): # List, Tuple, Set share an Iterable class SeqIterable: + """Iterator for sequence-like node containers. + + Provides iteration over nodes containing lists, tuples, sets, + strings, or arrays. Creates child nodes for each element during + iteration. + + Parameters + ---------- + wrapped_list : Node + A node containing a sequence-like object. + + Attributes + ---------- + wrapped_list : Node + The node being iterated over. + _index : int + Current iteration index. + + Methods + ------- + __iter__() + Reset iterator to beginning. + __next__() + Get next element as a node. + + Notes + ----- + Each iteration: + 1. Accesses the element using node indexing (wrapped_list[index]) + 2. Creates a MessageNode for the accessed element + 3. Maintains parent-child relationship in the graph + 4. Returns the element node + + This ensures all iterations are traced in the computation graph. + """ def __init__(self, wrapped_list): assert isinstance(wrapped_list, Node) self._index = 0 @@ -56,6 +119,42 @@ def __next__(self): class DictIterable: + """Iterator for dictionary nodes. + + Provides iteration over dictionary nodes, yielding (key, value) + tuples where values are nodes. + + Parameters + ---------- + wrapped_dict : Node + A node containing a dictionary. + + Attributes + ---------- + wrapped_dict : Node + The dictionary node being iterated. + keys : Node + Node containing the dictionary keys. + _index : int + Current iteration index. + + Methods + ------- + __iter__() + Reset iterator to beginning. + __next__() + Get next (key, value) tuple. + + Notes + ----- + Iteration process: + 1. Extracts keys using ops.keys() + 2. For each key, accesses value using wrapped_dict[key] + 3. Returns (key, value_node) tuples + 4. Maintains graph relationships + + Used by Node.items() to provide dictionary iteration in traced code. + """ def __init__(self, wrapped_dict): assert isinstance(wrapped_dict, Node) self._index = 0 diff --git a/opto/trace/modules.py b/opto/trace/modules.py index f08d0165..313fcf11 100644 --- a/opto/trace/modules.py +++ b/opto/trace/modules.py @@ -11,8 +11,67 @@ from typing import List, Optional def model(cls): - """ - Wrap a class with this decorator. This helps collect parameters for the optimizer. This decorated class cannot be pickled. + """Decorator to transform a class into a Trace-compatible model with parameter collection. + + The model decorator wraps a class to enable automatic parameter collection, + optimization support, and code export functionality. Decorated classes become + Module subclasses with enhanced capabilities for the Trace framework. + + Parameters + ---------- + cls : type + The class to be decorated. Should define methods and attributes that + may include trainable parameters. + + Returns + ------- + type + A wrapped version of the class that: + - Inherits from both the original class and Module + - Automatically collects parameters for optimization + - Provides export functionality for code generation + - Supports saving/loading of parameters + + Notes + ----- + The model decorator provides several key features: + + 1. **Parameter Collection**: Automatically identifies and collects all + ParameterNode and bundled method parameters for optimization. + + 2. **Code Export**: Can export the current state of the model (including + learned parameters) as executable Python code. + + 3. **Integration**: Seamlessly integrates with Trace optimizers and training + loops through the Module interface. + + 4. **State Management**: Inherits save/load functionality from Module for + parameter persistence. + + Limitations: + - Decorated classes cannot be pickled directly due to dynamic wrapping + - Use the save/load methods for persistence instead + + See Also + -------- + Module : Base class providing core functionality + bundle : Decorator for making methods trainable + ParameterNode : Trainable parameters within models + + Examples + -------- + >>> @model + >>> class MyModel: + ... def __init__(self): + ... self.weight = node(0.5, trainable=True) + ... + ... @bundle(trainable=True) + ... def forward(self, x): + ... return x * self.weight + >>> + >>> m = MyModel() + >>> # m.parameters() returns all trainable parameters + >>> # m.export('model.py') saves current state as code """ class ModelWrapper(cls, Module): @@ -101,7 +160,73 @@ def replace_node(match): class Module(ParameterContainer): - """Module is a ParameterContainer which has a forward method.""" + """Base class for all Trace models and wrapped functions. + + Module extends ParameterContainer to provide a standard interface for + components in the Trace framework. It defines the forward computation + pattern and provides parameter management functionality. + + Methods + ------- + forward(*args, **kwargs) + Define the forward computation. Must be overridden by subclasses. + __call__(*args, **kwargs) + Makes the module callable, delegating to forward(). + save(file_name) + Save model parameters to a pickle file. + load(file_name) + Load model parameters from a pickle file. + _set(new_parameters) + Update parameters from a dictionary or ParameterContainer. + + Attributes + ---------- + Inherits all attributes from ParameterContainer, including: + - Automatic parameter collection + - Parameter dictionary access + - Recursive parameter traversal + + Notes + ----- + Module serves as the foundation for: + + 1. **Model Classes**: Classes decorated with @model inherit from Module + to gain parameter management capabilities. + + 2. **Function Wrappers**: FunModule extends Module to wrap functions + as traceable operators. + + 3. **Custom Components**: Users can subclass Module directly to create + custom traceable components. + + The forward() method follows PyTorch's design pattern, providing a + familiar interface for defining computations. + + Parameter Management: + - Parameters are automatically collected from attributes + - Supports nested modules and recursive parameter collection + - Save/load functionality preserves learned parameters + + See Also + -------- + ParameterContainer : Base class for parameter management + model : Decorator that creates Module subclasses + FunModule : Module subclass for wrapped functions + + Examples + -------- + >>> class LinearLayer(Module): + ... def __init__(self, input_dim, output_dim): + ... self.weight = node(np.random.randn(input_dim, output_dim), trainable=True) + ... self.bias = node(np.zeros(output_dim), trainable=True) + ... + ... def forward(self, x): + ... return x @ self.weight + self.bias + >>> + >>> layer = LinearLayer(10, 5) + >>> output = layer(input_data) # Calls forward() + >>> layer.save('layer_params.pkl') # Save parameters + """ def forward(self, *args, **kwargs): raise NotImplementedError @@ -110,7 +235,18 @@ def __call__(self, *args, **kwargs): return self.forward(*args, **kwargs) def save(self, file_name: str): - """Save the parameters of the model to a pickle file.""" + """Save the parameters of the model to a pickle file. + + Parameters + ---------- + file_name : str + Path to the output pickle file. Directories are created if needed. + + Notes + ----- + Saves a deep copy of parameters to prevent reference issues. + The saved file can be loaded with the load() method. + """ # detect if the directory exists directory = os.path.dirname(file_name) if directory != "": @@ -119,14 +255,41 @@ def save(self, file_name: str): pickle.dump(copy.deepcopy(self.parameters_dict()), f) def load(self, file_name): - """Load the parameters of the model from a pickle file.""" + """Load the parameters of the model from a pickle file. + + Parameters + ---------- + file_name : str + Path to the pickle file containing saved parameters. + + Raises + ------ + FileNotFoundError + If the specified file does not exist. + AssertionError + If loaded parameters don't match model structure. + """ with open(file_name, "rb") as f: loaded_data = pickle.load(f) self._set(loaded_data) def _set(self, new_parameters): - """Set the parameters of the model from a dictionary. - new_parameters is a ParamterContainer or a parameter dict. + """Update model parameters from a dictionary or ParameterContainer. + + Parameters + ---------- + new_parameters : dict or ParameterContainer + New parameter values to set. Keys must match existing parameter names. + + Raises + ------ + AssertionError + If not all model parameters are present in new_parameters. + + Notes + ----- + This method updates existing parameters in-place and adds any new + parameters that don't exist in the current model. """ assert isinstance(new_parameters, (dict, ParameterContainer)) if isinstance(new_parameters, ParameterContainer): diff --git a/opto/trace/nodes.py b/opto/trace/nodes.py index cac02446..7c09cc51 100644 --- a/opto/trace/nodes.py +++ b/opto/trace/nodes.py @@ -11,23 +11,67 @@ def node(data, name=None, trainable=False, description=None): """Create a Node object from data. - Args: - data: The data to create the Node from. - name (str, optional): The name of the Node. - trainable (bool, optional): Whether the Node is trainable. Defaults to False. - description (str, optional): A string describing the data. - - Returns: - Node: A Node object containing the data. - - Notes: - If trainable=True: - - If data is already a Node, extracts underlying data and updates name - - Creates ParameterNode with extracted data, name, trainable=True - - If trainable=False: - - If data is already a Node, returns it (with warning if name provided) - - Otherwise creates new Node with data, name + This is the primary factory function for creating nodes in the Trace computation graph. + Nodes are the fundamental data structures that form vertices in the directed acyclic graph (DAG) + used for automatic differentiation and feedback propagation. + + Parameters + ---------- + data : Any + The data to wrap in a Node. Can be any Python object including primitives, + collections, or custom objects. If already a Node, its data will be extracted. + name : str, optional + A human-readable name for the node. Used for identification in graph visualizations + and debugging. If not provided, a default name based on the data type will be generated. + trainable : bool, default=False + Whether this node's value can be modified by an optimizer during training. + If True, creates a ParameterNode instead of a regular Node. + description : str, optional + A textual description of the node's purpose or constraints. Used as soft + constraints during optimization and for documentation. + + Returns + ------- + Node or ParameterNode + A Node object containing the data. Returns ParameterNode if trainable=True, + otherwise returns Node. + + Raises + ------ + AssertionError + If description is provided but is not a string or None. + + Notes + ----- + The behavior differs based on the trainable parameter: + + If trainable=True: + - If data is already a Node, extracts its underlying data and creates a new ParameterNode + - The name is preserved from the original node if no new name is provided + - Always returns a new ParameterNode instance + + If trainable=False: + - If data is already a Node, returns it unchanged (with a warning if a new name was provided) + - Otherwise creates a new Node with the provided data and name + + The created nodes are automatically registered in the global GRAPH registry. + + See Also + -------- + Node : The base node class + ParameterNode : Trainable node class for optimization parameters + MessageNode : Node class for operator outputs + + Examples + -------- + >>> # Create a simple node + >>> x = node(5, name="x") + >>> + >>> # Create a trainable parameter node + >>> weight = node(0.5, name="weight", trainable=True) + >>> + >>> # Nodes can contain complex data structures + >>> config = node({"learning_rate": 0.01, "batch_size": 32}, name="config") """ assert type(description) is str or description is None @@ -55,16 +99,45 @@ def node(data, name=None, trainable=False, description=None): class Graph: - """Graph is a registry of all the nodes, forming a Directed Acyclic Graph (DAG). - - Attributes: - _nodes (defaultdict): An instance-level attribute, which is a defaultdict of lists, used as a lookup table to find nodes by name. - - Notes: - The Graph class manages and organizes nodes in a Directed Acyclic Graph (DAG). - It provides methods to register nodes, clear the graph, retrieve nodes by name, and identify root nodes. - The `register` method assumes that elements in `_nodes` are never removed, - which is important for maintaining the integrity of node names. + """Registry and manager for all nodes in the computation graph. + + The Graph class maintains a global registry of all nodes created during program execution, + organizing them into a Directed Acyclic Graph (DAG) structure. It provides functionality + for node registration, retrieval, and graph traversal operations. + + Attributes + ---------- + TRACE : bool + Class-level flag controlling whether new operations create traced MessageNodes. + When True (default), operations on nodes are recorded in the graph for automatic + differentiation. When False, operations execute without creating graph connections. + _nodes : defaultdict[str, list[Node]] + Instance-level registry mapping node names to lists of nodes with that name. + Multiple nodes can share the same base name but are differentiated by indices. + + Notes + ----- + The Graph maintains several important invariants: + + 1. **Unique Naming**: Each node has a unique identifier in the format "name:index" + where index increments for nodes with the same base name. + + 2. **No Removal**: Once registered, nodes are never removed from the registry. + This ensures stable references and consistent indexing. + + 3. **DAG Structure**: The graph maintains a directed acyclic structure with no cycles, + enabling proper feedback propagation during backward passes. + + 4. **Name Scoping**: Supports hierarchical name scopes (similar to TensorFlow) through + the global NAME_SCOPES stack, allowing organized node naming in nested contexts. + + The Graph class is typically accessed through the global GRAPH instance rather than + instantiated directly. + + See Also + -------- + Node : Base class for graph nodes + NAME_SCOPES : Global stack for hierarchical naming contexts """ TRACE = True # When True, we trace the graph when creating MessageNode. When False, we don't trace the graph. @@ -534,11 +607,56 @@ def get_op_name(description): class NodeVizStyleGuide: - """A class to provide a standardized way to visualize nodes in a graph. - - Attributes: - style (str): Defines the style of the visualization. Default is 'default'. - print_limit (int): Sets the maximum number of characters to print for node descriptions and content. Default is 100. + """Style guide for visualizing nodes in graph representations. + + Provides a standardized approach to generating visual attributes for nodes when + rendering computation graphs using tools like Graphviz. Controls aspects such as + node shapes, colors, labels, and truncation of long content. + + Parameters + ---------- + style : str, default='default' + The visualization style to use. Currently only 'default' is supported. + print_limit : int, default=100 + Maximum number of characters to display for node descriptions and content + before truncation with ellipsis. + + Attributes + ---------- + style : str + The current visualization style. + print_limit : int + Maximum characters before truncation. + + Methods + ------- + get_attrs(x) + Generate complete visualization attributes for a node. + get_label(x) + Construct the text label for a node. + get_node_shape(x) + Determine the shape to use for a node. + get_color(x) + Assign fill color to a node. + get_style(x) + Set the visual style properties of a node. + + Notes + ----- + This class defines the visual language for graph representations: + + - **ParameterNodes** are rendered as boxes with light blue fill + - **ExceptionNodes** are rendered as ellipses with red fill + - **Regular Nodes** are rendered as ellipses with default fill + - **Trainable nodes** have a filled, solid style + + Labels include the node's Python name, description, and truncated content. + Long descriptions and content are automatically truncated to improve readability. + + See Also + -------- + NodeVizStyleGuideColorful : Enhanced colorful visualization style + Node.backward : Method that uses visualization during backward pass """ def __init__(self, style="default", print_limit=100): @@ -662,11 +780,54 @@ def get_style(self, x): class NodeVizStyleGuideColorful(NodeVizStyleGuide): - """A class to provide a colorful style guide for visualizing nodes in a graph. - - Attributes: - style (str): Defines the style of the visualization. Default is 'default'. - print_limit (int): Sets the maximum number of characters to print for node descriptions and content. Default is 100. + """Enhanced colorful style guide for visualizing nodes in graph representations. + + Extends the base NodeVizStyleGuide with more visually distinctive styling, + including colored borders and enhanced color schemes for different node types. + Particularly useful for presentations and debugging complex graphs. + + Parameters + ---------- + style : str, default='default' + The visualization style to use. Currently only 'default' is supported. + print_limit : int, default=100 + Maximum number of characters to display for node descriptions and content + before truncation with ellipsis. + + Attributes + ---------- + style : str + The current visualization style. + print_limit : int + Maximum characters before truncation. + + Methods + ------- + get_attrs(x) + Generate complete visualization attributes including border properties. + get_border_color(x) + Assign border color based on node type. + get_color(x) + Assign fill color with enhanced color scheme. + get_style(x) + Always returns 'filled,solid' for consistent appearance. + + Notes + ----- + Enhanced visual scheme compared to base class: + + - **ParameterNodes**: Light pink fill (#FFE5E5) with red border (#FF7E79) + - **ExceptionNodes**: Red fill (firebrick1) with black border + - **Regular Nodes**: Light blue fill (#DEEBF6) with blue border (#5C9BD5) + - All nodes have increased border width (1.2) for better visibility + + This style guide is automatically used in Node.backward() when visualize=True + for clearer graph representations. + + See Also + -------- + NodeVizStyleGuide : Base visualization style guide + Node.backward : Method that uses this for colorful visualization """ def __init__(self, style="default", print_limit=100): @@ -1996,6 +2157,72 @@ def append(self, *args, **kwargs): class ParameterNode(Node[T]): + """A trainable node that can be optimized during training. + + ParameterNode extends Node to represent trainable parameters in the computation graph. + These nodes can be modified by optimizers during the training process and support + projections for constrained optimization. + + Parameters + ---------- + value : Any + The initial value of the parameter. + name : str, optional + Name identifier for the parameter node. + trainable : bool, default=True + Whether this parameter can be optimized. Usually True for ParameterNodes. + description : str, optional + Textual description used as soft constraints during optimization. + projections : list[Projection], optional + List of projection operators to apply during optimization for maintaining + constraints (e.g., keeping values within bounds). + info : dict, optional + Additional metadata about the parameter. + + Attributes + ---------- + projections : list[Projection] + Active projections for this parameter. + trainable : bool + Whether the parameter is trainable. + _dependencies : dict + Tracks parameter and expandable node dependencies. + + Notes + ----- + ParameterNodes are the primary mechanism for defining optimizable values in Trace: + + 1. **Automatic Registration**: ParameterNodes register themselves as parameters + in the dependency tracking system. + + 2. **Projection Support**: Can enforce constraints through projection operators + that modify proposed updates to maintain validity. + + 3. **Optimizer Integration**: Optimizers specifically target ParameterNodes + when applying updates based on feedback. + + 4. **Visualization**: Rendered as boxes (rather than ellipses) in graph + visualizations to distinguish them from regular nodes. + + See Also + -------- + Node : Base class for all nodes + node : Factory function that creates ParameterNodes when trainable=True + Optimizer : Uses ParameterNodes as optimization targets + Projection : Constraint enforcement for parameters + + Examples + -------- + >>> # Create a trainable weight parameter + >>> weight = ParameterNode(0.5, name="weight") + >>> + >>> # Parameter with description for guided optimization + >>> prompt = ParameterNode( + ... "Solve this step by step", + ... name="prompt", + ... description="A clear instructional prompt" + ... ) + """ # This is a shorthand of a trainable Node. def __init__( self, @@ -2037,23 +2264,88 @@ def __str__(self) -> str: class MessageNode(Node[T]): - """A node representing the output of an operator. - - The description string should begin with [operator_name] followed by details about the operator. - When referring to inputs in the description, use either: - - The keys in args (if args is a dict) - - The names of the nodes in args (if args is a list) - + """Node representing the output of an operator in the computation graph. + + MessageNodes are automatically created when operations are performed on traced nodes. + They capture the relationship between inputs and outputs, enabling automatic differentiation + and feedback propagation through the graph. + + Parameters + ---------- + value : Any + The output value produced by the operator. + inputs : Union[List[Node], Dict[str, Node]] + Input nodes to the operator. Can be either: + - A list of Node objects (will be converted to dict with node names as keys) + - A dict mapping parameter names to Node objects + description : str + Description of the operator. Must begin with "[operator_name]" followed by + details. When referring to inputs, use the keys from the inputs dict. + name : str, optional + Name for this output node. + info : dict, optional + Additional metadata about the operation, including: + - 'inputs': Original function inputs + - 'output': Original function output + - 'traceable_code': Whether the operation contains traceable code + + Attributes + ---------- + inputs : Union[List[Node], Dict[str, Node]] + Copy of the input nodes to this operator. + hidden_dependencies : set + Parameters that this node depends on but aren't visible in the current graph level. + op_name : str + The operator type extracted from the description. + + Notes + ----- + MessageNodes serve several critical functions in the Trace system: + + 1. **Operation Recording**: Each MessageNode records an operation that was performed, + maintaining the complete computation history. + + 2. **Dependency Tracking**: Automatically tracks dependencies on parameters and + expandable nodes through the graph structure. + + 3. **Feedback Propagation**: During backward passes, MessageNodes receive feedback + from children and propagate processed feedback to parent nodes. + + 4. **Hidden Dependencies**: Can track dependencies on parameters not directly visible + in the current graph level, important for nested function calls. + + 5. **Automatic Creation**: Created automatically by operators when GRAPH.TRACE is True, + users rarely need to instantiate MessageNodes directly. + + Description Format + ----------------- + The description must follow the pattern: "[operator_name] details" + Examples: - >>> MessageNode(node_a, inputs=[node_a], - >>> description="[identity] This is an identity operator.") - >>> MessageNode(copy_node_a, inputs=[node_a], - >>> description="[copy] This is a copy operator.") - >>> MessageNode(1, inputs={'a':node_a, 'b':node_b}, - >>> description="[Add] This is an add operator of a and b.") - - Attributes: - value: The output value of the operator + - "[add] Addition of a and b" + - "[multiply] Element-wise multiplication" + - "[call] Function call with arguments" + + See Also + -------- + Node : Base class for all nodes + ExceptionNode : Specialized MessageNode for exceptions + operators : Module containing operators that create MessageNodes + + Examples + -------- + >>> # MessageNodes are typically created by operators + >>> a = node(5, name="a") + >>> b = node(3, name="b") + >>> # The following operation creates a MessageNode automatically + >>> c = a + b # Creates MessageNode with inputs={'a': a, 'b': b} + >>> + >>> # Manual creation (rarely needed) + >>> result = MessageNode( + ... value=8, + ... inputs={'a': a, 'b': b}, + ... description="[add] Addition of a and b" + ... ) """ # TODO document what needs to go into info @@ -2158,7 +2450,70 @@ def _add_dependencies(self, parent): class ExceptionNode(MessageNode[T]): - """Node containing the exception message.""" + """Specialized node for capturing and propagating exceptions in the computation graph. + + ExceptionNodes are created when operations fail during execution, preserving error + information in the graph structure for debugging and error-aware optimization. + + Parameters + ---------- + value : Exception + The exception that was raised during operation execution. + inputs : Union[List[Node], Dict[str, Node]] + Input nodes that led to the exception. + description : str, optional + Description of the operation that failed. + name : str, optional + Name for this exception node. + info : dict, optional + Additional context about the error, including: + - 'error_comment': Detailed error message for feedback + - 'traceback': Full traceback information + - 'code': Code that caused the error + + Methods + ------- + create_feedback(style='simple') + Generate feedback message from the exception. + + Notes + ----- + ExceptionNodes enable sophisticated error handling in Trace: + + 1. **Error Preservation**: Exceptions don't break the graph; they become part of it, + allowing continued execution and analysis. + + 2. **Error-Aware Optimization**: Optimizers can receive feedback about errors and + adjust parameters to avoid them in future iterations. + + 3. **Debugging Support**: Full error context is preserved, including type, message, + and optional traceback information. + + 4. **Visualization**: Rendered in red (firebrick1) in graph visualizations for + easy identification of error points. + + 5. **Feedback Generation**: Can produce simple or detailed feedback messages for + optimizers to understand what went wrong. + + The node's value is formatted as "(ErrorType) error message" for clarity. + + See Also + -------- + MessageNode : Parent class for operation outputs + ExecutionError : Exception type often captured in ExceptionNodes + bundle : Decorator that can create ExceptionNodes on errors + + Examples + -------- + >>> # ExceptionNodes are typically created automatically on errors + >>> a = node([1, 2, 3], name="list") + >>> b = node(5, name="index") + >>> # If index is out of bounds, creates ExceptionNode + >>> try: + ... c = a[b] # IndexError -> ExceptionNode + ... except: + ... pass # ExceptionNode created in graph + """ def __init__( self, @@ -2182,6 +2537,32 @@ def __init__( ) def create_feedback(self, style="simple"): + """Generate feedback message from the exception. + + Parameters + ---------- + style : {'simple', 'full'}, default='simple' + The level of detail for the feedback message: + - 'simple': Returns the formatted exception string + - 'full': Returns detailed error comment from info if available + + Returns + ------- + str + The feedback message describing the error. + + Raises + ------ + AssertionError + If style is not 'simple' or 'full'. + + Notes + ----- + The feedback is used by optimizers to understand what went wrong and + potentially adjust parameters to avoid similar errors. The 'full' style + is particularly useful when detailed error analysis is needed for + complex debugging or optimization scenarios. + """ assert style in ("simple", "full") feedback = self._data if style == "full": diff --git a/opto/trace/projections/projections.py b/opto/trace/projections/projections.py index 9be4227c..7b597e41 100644 --- a/opto/trace/projections/projections.py +++ b/opto/trace/projections/projections.py @@ -2,28 +2,85 @@ class Projection: - """ - Abstract base class for projection methods. + """Abstract base class for parameter projection methods. + + Projections are used to constrain parameter updates during optimization, + ensuring that parameters remain within valid or desired ranges/formats. + + Methods + ------- + __call__(x) + Apply projection to parameter (calls project method). + project(x) + Project parameter onto feasible set (must be implemented). + + Notes + ----- + Projections are applied during optimization to: + 1. Enforce constraints (e.g., bounds, formats) + 2. Maintain parameter validity (e.g., proper code syntax) + 3. Apply regularization or normalization + + Common projection types: + - Bound constraints: Clipping values to ranges + - Format constraints: Ensuring proper syntax/structure + - Semantic constraints: Maintaining meaning/validity + + Projections are applied sequentially if multiple are specified + for a parameter. + + See Also + -------- + ParameterNode : Parameters that can have projections + Optimizer.project : Method that applies projections """ def __init__(self, *args, **kwargs): pass def __call__(self, x: Any) -> Any: - """ - Call the projection method on the parameter node `x`. + """Apply projection to a parameter. + + Parameters + ---------- + x : Any + The parameter value to project. - Args: - x: The parameter node to project. + Returns + ------- + Any + The projected parameter value. - Returns: - The projected parameter node. + Notes + ----- + This method simply delegates to project() for consistency + with callable interface. """ return self.project(x) def project(self, x: Any) -> Any: - """ - Project the parameter node `x` onto the feasible set. + """Project parameter onto the feasible set. + + Parameters + ---------- + x : Any + The parameter value to project. + + Returns + ------- + Any + The projected parameter value that satisfies constraints. + + Raises + ------ + NotImplementedError + Must be implemented by subclasses. + + Notes + ----- + Subclasses should implement this method to define specific + projection logic. The projection should be idempotent: + project(project(x)) = project(x). """ raise NotImplementedError("Subclasses should implement this method.") \ No newline at end of file diff --git a/opto/trace/propagators/graph_propagator.py b/opto/trace/propagators/graph_propagator.py index 3ace69c2..23f4131e 100644 --- a/opto/trace/propagators/graph_propagator.py +++ b/opto/trace/propagators/graph_propagator.py @@ -15,7 +15,37 @@ @dataclass class TraceGraph(AbstractFeedback): - """Feedback container used by GraphPropagator.""" + """Container for subgraph feedback used by GraphPropagator. + + Represents a subgraph of the computation graph along with associated + user feedback. Used to propagate structured feedback through the graph. + + Attributes + ---------- + graph : list[tuple[int, Node]] + Priority queue of nodes ordered by level from roots to leaves. + Each tuple contains (level, node). + user_feedback : Any + User-provided feedback associated with this subgraph. + + Methods + ------- + empty() + Check if the graph is empty. + expand(node) + Extract and return the subgraph within a MessageNode. + visualize(simple_visualization, reverse_plot, print_limit) + Generate a Graphviz visualization of the subgraph. + + Notes + ----- + TraceGraph implements feedback aggregation through the __add__ method, + allowing multiple subgraphs to be combined while maintaining topological + order and preserving user feedback. + + The expand() class method enables recursive exploration of nested + computation graphs within traceable MessageNodes. + """ graph: List[ Tuple[int, Node] @@ -23,6 +53,13 @@ class TraceGraph(AbstractFeedback): user_feedback: Any def empty(self): + """Check if the trace graph is empty. + + Returns + ------- + bool + True if both graph and user_feedback are empty. + """ return len(self.graph) == 0 and self.user_feedback is None def __add__(self, other): @@ -53,7 +90,28 @@ def __add__(self, other): @classmethod def expand(cls, node: MessageNode): - """Return the subgraph within a MessageNode.""" + """Extract the subgraph within a traceable MessageNode. + + Parameters + ---------- + node : MessageNode + The node to expand, must have traceable code. + + Returns + ------- + TraceGraph + The extracted subgraph if node contains traceable code, + otherwise an empty TraceGraph. + + Notes + ----- + This method: + 1. Identifies dependencies within the node + 2. Temporarily clears existing feedback + 3. Runs backward pass to collect internal structure + 4. Restores original feedback + 5. Returns the collected subgraph + """ assert isinstance(node, MessageNode) if isinstance(node.info["output"], MessageNode): # these are the nodes where we will collect the feedback @@ -86,6 +144,22 @@ def _itemize(self, node): return (node.level, node) def visualize(self, simple_visualization=True, reverse_plot=False, print_limit=100): + """Generate a Graphviz visualization of the trace graph. + + Parameters + ---------- + simple_visualization : bool, default=True + If True, skip identity operators in visualization. + reverse_plot : bool, default=False + If True, plot edges from child to parent. + print_limit : int, default=100 + Maximum characters to display for node content. + + Returns + ------- + Digraph + Graphviz Digraph object for rendering. + """ from graphviz import Digraph nvsg = NodeVizStyleGuideColorful(print_limit=print_limit) diff --git a/opto/trace/propagators/propagators.py b/opto/trace/propagators/propagators.py index 9081f8d3..db8986c5 100644 --- a/opto/trace/propagators/propagators.py +++ b/opto/trace/propagators/propagators.py @@ -3,8 +3,45 @@ class AbstractPropagator: + """Abstract base class for feedback propagation in computation graphs. + + This class defines the interface for propagating feedback from child nodes + to their parent nodes in traced computation graphs. Concrete implementations + define specific propagation strategies for different types of operations. + + Notes + ----- + Propagators are essential components of the trace system that enable + backward passes for optimization. They determine how feedback flows + through the computation graph during parameter updates. + """ + def __call__(self, child: MessageNode): - """Calling this method would propagte the feedback from the child to the parents.""" + """Propagate feedback from child node to its parent nodes. + + This method validates the feedback structure and delegates to the + concrete propagate implementation. + + Parameters + ---------- + child : MessageNode + Child node containing feedback to propagate. + + Returns + ------- + dict[Node, Any] + Dictionary mapping parent nodes to their propagated feedback. + + Raises + ------ + AssertionError + If child is not a MessageNode or feedback format is invalid. + + Notes + ----- + All MessageNode feedback should have at most one feedback entry per key. + The propagated feedback must include entries for all parent nodes. + """ assert isinstance(child, MessageNode) assert all( [len(f) <= 1 for f in child.feedback.values()] @@ -17,20 +54,76 @@ def __call__(self, child: MessageNode): return propagated_feedback def propagate(self, child: MessageNode) -> Dict[Node, Any]: - """Compute propagated feedback to node.parents of a node. Return a dict where - the keys are the parents and the values are the - propagated feedback. + """Compute propagated feedback for parent nodes. + + This abstract method must be implemented by concrete propagator classes + to define how feedback is computed and distributed to parent nodes. + + Parameters + ---------- + child : MessageNode + Child node containing feedback to propagate. + + Returns + ------- + dict[Node, Any] + Dictionary mapping each parent node to its computed feedback. + + Raises + ------ + NotImplementedError + Must be implemented by subclasses. """ raise NotImplementedError class AbstractFeedback: - """Feedback container used by propagators. It needs to support addition.""" + """Abstract base class for feedback objects in propagation systems. + + This class defines the interface for feedback objects that can be combined + and accumulated during backward propagation. Concrete implementations must + support addition operations for proper feedback aggregation. + + Notes + ----- + Feedback objects are used to carry gradient information through the + computation graph. The addition operation enables accumulation of + feedback from multiple sources. + """ def __add__(self, other): + """Add two feedback objects together. + + Parameters + ---------- + other : AbstractFeedback + Another feedback object to combine with this one. + + Returns + ------- + AbstractFeedback + Combined feedback object. + + Raises + ------ + NotImplementedError + Must be implemented by subclasses. + """ raise NotImplementedError def __radd__(self, other): + """Support right-hand addition and sum() function. + + Parameters + ---------- + other : Any + Other object to add. If 0, returns self for sum() compatibility. + + Returns + ------- + AbstractFeedback + Result of addition operation. + """ if other == 0: # for support sum return self else: @@ -38,13 +131,62 @@ def __radd__(self, other): class Propagator(AbstractPropagator): + """Configurable propagator with operator-specific override support. + + This propagator allows registration of custom propagation functions for + specific operators while providing a default propagation strategy for + unregistered operators. + + Attributes + ---------- + override : dict[str, callable] + Dictionary mapping operator names to custom propagation functions. + + Notes + ----- + This design enables flexible customization of propagation behavior for + different types of operations while maintaining a unified interface. + """ + def __init__(self): + """Initialize propagator with empty override registry.""" self.override = dict() # key: operator name: data: override propagate function def register(self, operator_name, propagate_function): + """Register a custom propagation function for an operator. + + Parameters + ---------- + operator_name : str + Name of the operator to override. + propagate_function : callable + Custom propagation function with signature (child: MessageNode) -> dict. + + Notes + ----- + Registered functions take precedence over the default propagation logic. + The function should return a dictionary mapping parent nodes to feedback. + """ self.override[operator_name] = propagate_function def propagate(self, child: MessageNode) -> Dict[Node, Any]: + """Propagate feedback using operator-specific or default logic. + + Parameters + ---------- + child : MessageNode + Child node containing feedback to propagate. + + Returns + ------- + dict[Node, Any] + Dictionary mapping parent nodes to propagated feedback. + + Notes + ----- + First checks for registered operator-specific propagation functions. + Falls back to the default _propagate method if no override is found. + """ operator_name = child.op_name if operator_name in self.override: return self.override[operator_name](child) @@ -52,17 +194,51 @@ def propagate(self, child: MessageNode) -> Dict[Node, Any]: return self._propagate(child) def init_feedback(self, node: Node, feedback: Any): - """ - Given raw feedback, create the feedback object that will be propagated recursively. + """Initialize feedback object for propagation. + This method converts raw feedback into the appropriate format for + recursive propagation through the computation graph. + + Parameters + ---------- + node : Node + Node receiving the feedback. + feedback : Any + Raw feedback data to initialize. + + Returns + ------- + Any + Initialized feedback object ready for propagation. + + Raises + ------ + NotImplementedError + Must be implemented by subclasses. """ raise NotImplementedError def _propagate(self, child: MessageNode) -> Dict[Node, Any]: - """Compute propagated feedback to node.parents based on - node.description, node.data, and node.feedback. Return a dict where - the keys are the parents and the values are the - propagated feedback. + """Default propagation logic for operators without custom overrides. + + This method implements the default strategy for propagating feedback + from child nodes to their parents using the node's description, data, + and feedback information. + + Parameters + ---------- + child : MessageNode + Child node containing feedback to propagate. + + Returns + ------- + dict[Node, Any] + Dictionary mapping parent nodes to their computed feedback. + + Raises + ------ + NotImplementedError + Must be implemented by subclasses to define default behavior. """ raise NotImplementedError @@ -72,12 +248,55 @@ def _propagate(self, child: MessageNode) -> Dict[Node, Any]: # we might need to perform a "merge" feedback action -# # TODO test class SumPropagator(Propagator): + """Simple propagator that sums or concatenates feedback from multiple sources. + + This propagator implements a basic aggregation strategy where feedback from + multiple child nodes is combined by summation (for numeric types) or + concatenation (for string types) before being distributed to parent nodes. + + Notes + ----- + This is a concrete implementation suitable for scenarios where feedback + can be meaningfully combined through simple aggregation operations. + """ + def init_feedback(self, feedback: Any): + """Initialize feedback without transformation. + + Parameters + ---------- + feedback : Any + Raw feedback to initialize. + + Returns + ------- + Any + The feedback object unchanged. + """ return feedback def _propagate(self, child: MessageNode): + """Propagate feedback by summing or concatenating multiple sources. + + Parameters + ---------- + child : MessageNode + Child node containing feedback to propagate. + + Returns + ------- + dict[Node, Any] + Dictionary mapping each parent node to the aggregated feedback. + + Notes + ----- + User feedback takes precedence and is used directly if present. + Otherwise, feedback from all sources is aggregated by type: + - Strings are concatenated + - Numeric types are summed + All feedback values must be of the same type for proper aggregation. + """ if "user" in child.feedback: assert len(child.feedback) == 1, "user feedback should be the only feedback" assert len(child.feedback["user"]) == 1 diff --git a/opto/trace/utils.py b/opto/trace/utils.py index e852a06f..c9e48cf0 100644 --- a/opto/trace/utils.py +++ b/opto/trace/utils.py @@ -12,29 +12,81 @@ def sum_feedback(nodes): - """Aggregate the feedback of a list of nodes.""" + """Aggregate feedback from a list of nodes. + + Sums all feedback values across all feedback channels for the given nodes. + + Parameters + ---------- + nodes : list + List of nodes containing feedback to aggregate. + + Returns + ------- + Any + Aggregated feedback value, typically the sum of all feedback. + + Notes + ----- + Each node may have multiple feedback channels (stored in feedback.values()). + This function sums across all channels and all nodes. + """ return sum([sum(gg) for p in nodes for gg in p.feedback.values()]) def contain(container_of_nodes, node): + """Check if a node is contained in a collection by identity. + + Parameters + ---------- + container_of_nodes : iterable + Collection of nodes to search in. + node : Node + Node to search for. + + Returns + ------- + bool + True if node is found in container (by identity, not value). + + Notes + ----- + Uses identity comparison (is) rather than value comparison (==) + to ensure exact object matching. + """ # check for identity instead of value return any([node is n for n in container_of_nodes]) def parse_eqs_to_dict(text): - """ - Parse the text of equations into a dictionary - - Example: + """Parse text containing variable assignments into a dictionary. + + Parameters + ---------- + text : str + Text containing variable assignments in the format 'key = value'. + + Returns + ------- + dict[str, str] + Dictionary mapping variable names to their values. + + Notes + ----- + Handles multi-line values by concatenating lines without '=' to + the previous key's value. Removes backticks from values. + + Examples + -------- + Input: x0 = 1 x1=2 x2=`2` x3= def fun():\\n print('hello')\\n abc_test1=test - would be parsed into - - {'x0': '1', 'x1': '2', 'x2': '2', 'x3': "def fun():\\nprint('hello')", 'abc_test1': 'test'} + Output: + {'x0': '1', 'x1': '2', 'x2': '2', 'x3': "def fun():\\nprint('hello')", 'abc_test1': 'test'} """ lines = text.split("\n") result_dict = {} @@ -52,7 +104,35 @@ def parse_eqs_to_dict(text): def for_all_methods(decorator): - """Applying a decorator to all methods of a class.""" + """Apply a decorator to all methods of a class. + + Class decorator that applies the given decorator to all non-dunder + methods of the decorated class. + + Parameters + ---------- + decorator : callable + Decorator function to apply to class methods. + + Returns + ------- + callable + Class decorator function. + + Examples + -------- + >>> @for_all_methods(my_decorator) + ... class MyClass: + ... def method1(self): + ... pass + ... def method2(self): + ... pass + + Notes + ----- + Only applies to callable attributes that don't start with '__'. + Useful for applying logging, timing, or validation to all methods. + """ def decorate(cls): for name, attr in cls.__dict__.items(): @@ -64,6 +144,35 @@ def decorate(cls): def render_opt_step(step_idx, optimizer, no_trace_graph=False, no_improvement=False): + """Render an optimization step as HTML for Jupyter notebook display. + + Creates a visual representation of an optimization step showing the + trace graph, feedback, reasoning, and suggested improvements. + + Parameters + ---------- + step_idx : int + Index of the optimization step to render. + optimizer : Optimizer + Optimizer instance containing logs and summaries. + no_trace_graph : bool, default=False + If True, omits the trace graph from the display. + no_improvement : bool, default=False + If True, omits the improvement section from the display. + + Returns + ------- + None + Displays HTML output directly in Jupyter notebook. + + Notes + ----- + Requires IPython display capabilities. Creates color-coded boxes for: + - Gray: Trace graph showing computation flow + - Red: Feedback indicating issues or goals + - Green: Reasoning about improvements + - Blue: Suggested parameter updates + """ from IPython.display import display, HTML idx = step_idx @@ -237,8 +346,28 @@ def escape_json_nested_quotes(json_str): def remove_non_ascii(json_txt): - """ - Example usage can be found in optimizers/textgrad.py + """Remove non-ASCII and non-printable characters from JSON text. + + Cleans JSON strings by removing control characters and non-printable + characters while preserving valid escape sequences. + + Parameters + ---------- + json_txt : str + JSON text that may contain non-ASCII or control characters. + + Returns + ------- + str + Cleaned JSON text with only printable ASCII characters. + + Notes + ----- + First applies escape_json_nested_quotes, then removes: + - Newlines, tabs, backspaces, carriage returns, form feeds + - Any other non-printable characters + + Example usage can be found in optimizers/textgrad.py. """ cleaned = "" for c in escape_json_nested_quotes(json_txt): @@ -249,25 +378,58 @@ def remove_non_ascii(json_txt): def dedent(text: str): - """ - A better dedent than dedent from textwrap module. - Remove leading and trailing whitespace for each line - For example: - ``` - Line 1 has no leading space - Line 2 has two leading spaces - ``` - The output will be : - ``` - Line 1 has no leading space - Line 2 has two leading spaces - ``` - This allows writing cleaner multiline prompts in the code. + """Remove leading and trailing whitespace from each line. + + A simpler alternative to textwrap.dedent that strips whitespace + from the beginning and end of each line individually, rather than + removing common leading whitespace. + + Parameters + ---------- + text : str + Multi-line text to dedent. + + Returns + ------- + str + Text with each line stripped of leading/trailing whitespace. + + Examples + -------- + >>> text = '''\n Line 1 has leading space\n Line 2 has more\n ''' + >>> dedent(text) + 'Line 1 has leading space\nLine 2 has more' + + Notes + ----- + Unlike textwrap.dedent, this function: + - Strips each line independently + - Removes ALL leading/trailing whitespace per line + - Useful for cleaning up multi-line prompts in code """ return "\n".join([line.strip() for line in text.split("\n")]) def test_json_quote_escaper(): + """Test suite for escape_json_nested_quotes function. + + Verifies that the JSON quote escaper correctly handles various + edge cases including nested quotes, already-escaped quotes, and + special characters. + + Raises + ------ + AssertionError + If any test case fails to produce expected output. + + Notes + ----- + Tests cover: + - Multiple quotes within string values + - Quotes at various positions + - Already escaped quotes + - LaTeX-style escape sequences + """ test_cases = [ ( '{"name": "Multiple "quotes" in "one" string", "value": "Multiple "quotes" in "the second" string"}', diff --git a/opto/trainer/README.md b/opto/trainer/README.md deleted file mode 100644 index e69de29b..00000000 diff --git a/opto/trainer/algorithms/aggregator.py b/opto/trainer/algorithms/aggregator.py index a1d30a67..0657cfc1 100644 --- a/opto/trainer/algorithms/aggregator.py +++ b/opto/trainer/algorithms/aggregator.py @@ -14,8 +14,47 @@ class AggregatedUpdate(Minibatch): - """ The algorithm applies the optimizer to propose updates for each instance in the minibatch independently. - The updates are then aggregated using an LLM and applied to the agent. + """Algorithm that applies optimizer to propose updates independently for minibatch instances. + + The updates are then aggregated using an LLM and applied to the agent. This approach + allows for intelligent consolidation of multiple parameter suggestions based on + confidence scores and common patterns. + + Parameters + ---------- + agent : trace.Module + The agent module to be trained and optimized. + optimizer : Optimizer + The optimizer instance used to generate parameter updates. + use_asyncio : bool, optional + Whether to use asyncio for parallel agent evaluation, by default True. + logger : Logger, optional + Logger instance for tracking training metrics, by default None. + llm : AbstractModel, optional + Language model instance for aggregating updates, by default None. + max_tokens : int, optional + Maximum tokens for aggregator LLM responses, by default 4096. + *args + Additional positional arguments passed to parent class. + **kwargs + Additional keyword arguments passed to parent class. + + Attributes + ---------- + llm : AbstractModel + Language model used for parameter update aggregation. + max_tokens : int + Token limit for aggregator responses. + stepsize : float + Step size for parameter updates, set during training. + aggregator_system_prompt : str + System prompt template for the aggregator LLM. + + Notes + ----- + The aggregation process uses confidence scores to weight suggestions, with the + current parameter values receiving a confidence of (1 - stepsize) and new + suggestions receiving a confidence of stepsize. """ aggregator_system_prompt = f"""You are an expert in aggregating suggestions. You will see a list of suggestions of parameters from different people (denoted as #SuggestedValue_i). A parameter is represented as a dict, where the key is the name of a parameter component, and the value is the component value. @@ -54,6 +93,27 @@ def __init__(self, *args, **kwargs, ): + """Initialize the AggregatedUpdate algorithm. + + Parameters + ---------- + agent : trace.Module + The agent module to be trained. + optimizer : Optimizer + The optimizer for generating parameter updates. + use_asyncio : bool, optional + Whether to use asyncio for agent evaluation, by default True. + logger : Logger, optional + Logger for tracking metrics, by default None. + llm : AbstractModel, optional + Language model for aggregation, by default None (uses LLM()). + max_tokens : int, optional + Maximum tokens for aggregator responses, by default 4096. + *args + Additional positional arguments. + **kwargs + Additional keyword arguments. + """ super().__init__(agent, optimizer, logger=logger, use_asyncio=use_asyncio, *args, **kwargs) self.llm = llm or LLM() # for the aggregator self.max_tokens = max_tokens # for the aggregator @@ -73,6 +133,43 @@ def train(self, verbose: Union[bool, str] = False, # whether to print the output of the agent **kwargs ): + """Train the agent using aggregated parameter updates. + + Parameters + ---------- + guide : Guide + Guide function to provide feedback for training. + train_dataset : dict + Training dataset containing 'inputs' and 'infos' keys. + stepsize : float, optional + Step size for parameter updates (0-1), by default 0.5. + num_epochs : int, optional + Number of training epochs, by default 1. + batch_size : int, optional + Batch size for parameter updates, by default 1. + test_dataset : dict, optional + Test dataset for evaluation, by default None. + eval_frequency : int, optional + Frequency of evaluation, by default 1. + log_frequency : int, optional + Frequency of logging, by default None. + min_score : int, optional + Minimum score threshold for updates, by default None. + verbose : bool or str, optional + Verbosity level for output, by default False. + **kwargs + Additional training arguments. + + Raises + ------ + AssertionError + If stepsize is not between 0 and 1. + + Notes + ----- + The stepsize parameter controls the balance between current parameters + (confidence 1-stepsize) and new suggestions (confidence stepsize). + """ assert stepsize >= 0 and stepsize <= 1 self.stepsize = stepsize # used in self.aggregate @@ -84,7 +181,31 @@ def train(self, def forward(self, agent, x, guide, info, verbose=False): - """ Run the agent, compute feedback and return the new parameters for an instance in the minibatch. """ + """Run agent forward pass and generate parameter updates for minibatch instance. + + Parameters + ---------- + agent : trace.Module + The agent module to run forward pass on. + x : Any + Input data for the agent. + guide : Guide + Guide function for generating feedback. + info : Any + Additional information for the guide. + verbose : bool, optional + Whether to print verbose output, by default False. + + Returns + ------- + tuple[dict, float] + Parameter update dictionary and score for the instance. + + Notes + ----- + This method runs a standard optimization step and generates parameter + updates using the optimizer's backward and step methods. + """ target, score, feedback = standard_optimization_step(self.agent, x, guide, info, min_score=None) self.optimizer.zero_feedback() self.optimizer.backward(target, feedback) @@ -92,11 +213,41 @@ def forward(self, agent, x, guide, info, verbose=False): return self.to_param_dict(update_dict), score def to_param_dict(self, update_dict): - """ Convert the update the dict {ParameterNode:Any} to a dict {str:Any}. """ + """Convert parameter update dictionary from ParameterNode keys to string keys. + + Parameters + ---------- + update_dict : dict[ParameterNode, Any] + Update dictionary with ParameterNode keys. + + Returns + ------- + dict[str, Any] + Update dictionary with string keys (py_name of ParameterNode). + """ return {k.py_name: v for k, v in update_dict.items()} def update(self, outputs, verbose=False): - """ Ask LLM to aggregate the new parameter suggestions. """ + """Aggregate parameter update suggestions using LLM and apply to agent. + + Parameters + ---------- + outputs : list[tuple[dict, float]] + List of (parameter_updates, score) tuples from minibatch forward passes. + verbose : bool, optional + Whether to print verbose aggregation output, by default False. + + Returns + ------- + float or None + Average score across the minibatch instances, or None if no valid scores. + + Notes + ----- + This method constructs a prompt with current parameters and suggested updates, + asks the LLM aggregator to consolidate them, and applies the aggregated update + to the agent parameters. + """ # Prepare the new parameters and scores new_parameters = [] @@ -147,7 +298,35 @@ def update(self, outputs, verbose=False): def construct_update_dict( parameters: List[ParameterNode], suggestion: Dict[str, Any], ignore_extraction_error: bool = True ) -> Dict[ParameterNode, Any]: - """Convert the suggestion in text into the right data type.""" + """Convert LLM suggestion dictionary into typed parameter update dictionary. + + Parameters + ---------- + parameters : List[ParameterNode] + List of trainable parameter nodes in the agent. + suggestion : Dict[str, Any] + Dictionary of suggested parameter values from LLM. + ignore_extraction_error : bool, optional + Whether to ignore type conversion errors, by default True. + + Returns + ------- + Dict[ParameterNode, Any] + Dictionary mapping parameter nodes to their suggested values. + + Raises + ------ + ValueError + If type conversion fails and ignore_extraction_error is False. + KeyError + If parameter key is missing and ignore_extraction_error is False. + + Notes + ----- + This function attempts to convert string suggestions to the appropriate + data types based on the current parameter values. Type conversion errors + are either ignored (with warning) or raised based on the flag. + """ # TODO: might need some automatic type conversion update_dict = {} for node in parameters: @@ -166,7 +345,30 @@ def construct_update_dict( def extract_llm_suggestion(response: str, ignore_extraction_error: bool = True) -> Dict[str, Any]: - """Extract the suggestion from the response.""" + """Extract parameter suggestions from LLM response text. + + Parameters + ---------- + response : str + Raw response text from the LLM aggregator. + ignore_extraction_error : bool, optional + Whether to ignore JSON parsing and extraction errors, by default True. + + Returns + ------- + Dict[str, Any] + Dictionary of extracted parameter suggestions. + + Notes + ----- + This function attempts multiple parsing strategies: + 1. JSON parsing of the full response + 2. Regex extraction of JSON content within braces + 3. Manual key-value pair extraction using regex patterns + + Empty code suggestions (parameters ending with "__code") are automatically + removed from the final result. + """ suggestion = {} attempt_n = 0 while attempt_n < 2: diff --git a/opto/trainer/algorithms/algorithm.py b/opto/trainer/algorithms/algorithm.py index 326e1be2..47aec62e 100644 --- a/opto/trainer/algorithms/algorithm.py +++ b/opto/trainer/algorithms/algorithm.py @@ -8,23 +8,92 @@ import pickle class AbstractAlgorithm: - """ Abstract base class for all algorithms. """ + """Abstract base class for all training algorithms. + + This class provides a common interface for all algorithms that train agents. + Subclasses should implement the train method with their specific training logic. + + Parameters + ---------- + agent : Any + The agent to be trained by the algorithm. + *args + Additional positional arguments. + **kwargs + Additional keyword arguments. + + Attributes + ---------- + agent : Any + The agent instance being trained. + """ def __init__(self, agent, *args, **kwargs): + """Initialize the abstract algorithm. + + Parameters + ---------- + agent : Any + The agent to be trained. + *args + Additional positional arguments. + **kwargs + Additional keyword arguments. + """ self.agent = agent def train(self, *args, **kwargs): - """ Train the agent. """ + """Train the agent using the algorithm's specific strategy. + + Parameters + ---------- + *args + Training arguments specific to the algorithm. + **kwargs + Training keyword arguments specific to the algorithm. + + Notes + ----- + This method should be overridden by subclasses to implement + their specific training logic. + """ pass class Trainer(AbstractAlgorithm): - """ - We define the API of algorithms to train an agent from a dataset of (x, info) pairs. + """Base trainer class that defines the API for training agents from datasets. + + This class provides infrastructure for training trace.Module agents using datasets + of (input, info) pairs and teacher/guide functions for feedback generation. - agent: trace.Module (e.g. constructed by @trace.model) - teacher: (question, student_answer, info) -> score, feedback (e.g. info can contain the true answer) - train_dataset: dataset of (x, info) pairs + Parameters + ---------- + agent : trace.Module + The trace module to be trained (e.g. constructed with @trace.model). + num_threads : int, optional + Maximum number of threads for parallel execution, by default None. + logger : Logger, optional + Logger instance for tracking training metrics, by default None. + *args + Additional positional arguments. + **kwargs + Additional keyword arguments. + + Attributes + ---------- + agent : trace.Module + The agent being trained. + num_threads : int or None + Maximum number of threads for parallel operations. + logger : Logger + Logger instance for metric tracking. + + Notes + ----- + The training paradigm involves: + - agent: trace.Module (constructed via @trace.model decorator) + - guide/teacher: function (question, student_answer, info) -> score, feedback + - train_dataset: dataset of (x, info) pairs for training """ def __init__(self, @@ -33,6 +102,26 @@ def __init__(self, logger=None, # logger for tracking metrics *args, **kwargs): + """Initialize the Trainer with an agent and configuration. + + Parameters + ---------- + agent : trace.Module + The trace module agent to be trained. + num_threads : int, optional + Maximum number of threads for parallel execution, by default None. + logger : Logger, optional + Logger for tracking training metrics, by default None (uses DefaultLogger). + *args + Additional positional arguments passed to parent class. + **kwargs + Additional keyword arguments passed to parent class. + + Raises + ------ + AssertionError + If agent is not a trace.Module instance. + """ assert isinstance(agent, Module), "Agent must be a trace Module. Getting {}".format(type(agent)) super().__init__(agent, *args, **kwargs) self.num_threads = num_threads @@ -42,24 +131,44 @@ def __init__(self, def _use_asyncio(self, threads=None): """Determine whether to use asyncio based on the number of threads. - Args: - threads: Number of threads to use. If None, uses self.num_threads. + Parameters + ---------- + threads : int, optional + Number of threads to use. If None, uses self.num_threads. - Returns: - bool: True if parallel execution should be used, False otherwise. + Returns + ------- + bool + True if parallel execution should be used, False otherwise. + + Notes + ----- + Parallel execution is enabled when the effective thread count is + greater than 1. This helps optimize performance for batch operations. """ effective_threads = threads or self.num_threads return effective_threads is not None and effective_threads > 1 def save_agent(self, save_path, iteration=None): - """Save the agent to the specified path. + """Save the agent to the specified path with optional iteration numbering. + + Parameters + ---------- + save_path : str + Base path to save the agent to. + iteration : int, optional + Current iteration number for checkpoint naming, by default None. - Args: - save_path: Path to save the agent to. - iteration: Current iteration number (for logging purposes). + Returns + ------- + str + The actual path where the agent was saved. - Returns: - str: The path where the agent was saved. + Notes + ----- + If iteration is provided, it's appended to the filename. Final iterations + (matching self.n_iters) get a "_final" suffix for easy identification. + The directory structure is created automatically if it doesn't exist. """ # Create directory if it doesn't exist directory = os.path.dirname(save_path) @@ -90,11 +199,47 @@ def train(self, num_threads: int = None, # maximum number of threads to use (overrides self.num_threads) **kwargs ): + """Train the agent using the provided guide and dataset. + + Parameters + ---------- + guide : Guide + Guide function to provide feedback during training. + train_dataset : dict + Training dataset containing 'inputs' and 'infos' keys. + num_threads : int, optional + Maximum number of threads to use, overrides self.num_threads. + **kwargs + Additional training arguments specific to the algorithm. + + Raises + ------ + NotImplementedError + This method must be implemented by subclasses. + + Notes + ----- + Subclasses must implement this method with their specific training logic. + The method should use the guide to evaluate agent outputs and update + the agent parameters accordingly. + """ raise NotImplementedError def save(self, path: str): - """ Save the guide to a file. """ + """Save the trainer and its components to a file. + + Parameters + ---------- + path : str + Base path to save the trainer state to. + + Notes + ----- + This method serializes the trainer's state, saving different component + types (Module, Guide, DataLoader, Optimizer) to separate files with + appropriate extensions. The main trainer state is saved as a pickle file. + """ with open(path, 'wb') as f: d = {} for key, value in self.__dict__.items(): @@ -119,7 +264,20 @@ def save(self, path: str): pickle.dump(d, f) def load(self, path: str): - """ Load the guide from a file. """ + """Load the trainer and its components from a file. + + Parameters + ---------- + path : str + Path to the saved trainer state file. + + Notes + ----- + This method deserializes the trainer's state and loads component files. + It validates that the loaded attributes match the expected types and + warns if attributes from the saved state are not found in the current + trainer instance. + """ with open(path, 'rb') as f: data = pickle.load(f) for key, value in data.items(): diff --git a/opto/trainer/algorithms/basic_algorithms.py b/opto/trainer/algorithms/basic_algorithms.py index 76597dcb..168423cb 100644 --- a/opto/trainer/algorithms/basic_algorithms.py +++ b/opto/trainer/algorithms/basic_algorithms.py @@ -10,20 +10,37 @@ def standard_optimization_step(agent, x, guide, info, min_score=0): - """ Forward and compute feedback. - - Args: - agent: trace.Module - x: input - guide: (question, student_answer, info) -> score, feedback - info: additional information for the guide - min_score: minimum score when exception happens - - Returns: - target: output of the agent - score: score from the guide - feedback: feedback from the guide - """ + """Execute a standard forward pass and feedback computation step. + + This function runs the agent on input data, evaluates the output using the guide, + and handles any execution errors that may occur during the process. + + Parameters + ---------- + agent : trace.Module + The agent module to execute the forward pass on. + x : Any + Input data for the agent. + guide : callable + Guide function with signature (question, student_answer, info) -> (score, feedback). + info : Any + Additional information passed to the guide function. + min_score : float, optional + Minimum score to assign when execution exceptions occur, by default 0. + + Returns + ------- + tuple[trace.Node, float, str] + A tuple containing: + - target: Output node from the agent (or exception node if error occurred) + - score: Numeric score from the guide evaluation + - feedback: Text feedback from the guide evaluation + + Notes + ----- + If a trace.ExecutionError occurs during agent execution, the function catches + it and returns the exception node with the minimum score and full feedback. + """ try: target = agent(x) score, feedback = guide(x, target.data, info) @@ -34,7 +51,46 @@ def standard_optimization_step(agent, x, guide, info, min_score=0): class Minibatch(Trainer): - """ General minibatch optimization algorithm. This class defines a general training and logging routine using minimbatch sampling.""" + """General minibatch optimization algorithm with comprehensive training infrastructure. + + This class provides a complete training framework that handles minibatch sampling, + evaluation, logging, checkpointing, and improvement validation. It serves as a + base class for various optimization algorithms that operate on minibatches. + + Parameters + ---------- + agent : trace.Module + The agent module to be trained. + optimizer : Optimizer + The optimizer instance for parameter updates. + num_threads : int, optional + Maximum number of threads for parallel execution, by default None. + logger : Logger, optional + Logger instance for tracking metrics, by default None. + *args + Additional positional arguments passed to parent class. + **kwargs + Additional keyword arguments passed to parent class. + + Attributes + ---------- + agent : trace.Module + The agent being trained. + optimizer : Optimizer + The optimizer used for parameter updates. + n_iters : int + Number of training iterations completed. + num_eval_samples : int + Number of samples used for evaluation. + + Notes + ----- + This class implements the core training loop including: + - Minibatch sampling and processing + - Periodic evaluation and logging + - Model checkpointing at specified intervals + - Optional improvement validation and rollback + """ def __init__(self, agent, @@ -44,6 +100,23 @@ def __init__(self, *args, **kwargs, ): + """Initialize the Minibatch algorithm. + + Parameters + ---------- + agent : trace.Module + The agent module to be trained. + optimizer : Optimizer + The optimizer instance for parameter updates. + num_threads : int, optional + Maximum number of threads for parallel execution, by default None. + logger : Logger, optional + Logger instance for tracking metrics, by default None. + *args + Additional positional arguments passed to parent class. + **kwargs + Additional keyword arguments passed to parent class. + """ super().__init__(agent, num_threads=num_threads, logger=logger, *args, **kwargs) self.optimizer = optimizer self.n_iters = 0 # number of iterations @@ -68,11 +141,61 @@ def train(self, num_threads: int = None, # maximum number of threads to use (overrides self.num_threads) **kwargs ): - """ - Given a dataset of (x, info) pairs, the algorithm will: - 1. Forward the agent on the inputs and compute the feedback using the guide. - 2. Update the agent using the feedback. - 3. Evaluate the agent on the test dataset and log the results. + """Train the agent using minibatch optimization with comprehensive monitoring. + + This method implements a complete training loop that processes the dataset in + minibatches, applies parameter updates, and tracks progress through evaluation, + logging, and checkpointing. + + Parameters + ---------- + guide : Guide + Guide function to provide feedback during training. + train_dataset : dict + Training dataset containing 'inputs' and 'infos' keys. + ensure_improvement : bool, optional + Whether to validate that updates improve performance, by default False. + improvement_threshold : float, optional + Minimum improvement threshold for accepting updates, by default 0.0. + num_epochs : int, optional + Number of training epochs to run, by default 1. + batch_size : int, optional + Size of minibatches for parameter updates, by default 1. + test_dataset : dict, optional + Test dataset for evaluation, defaults to train_dataset if None. + eval_frequency : int, optional + Frequency of evaluation (every N iterations), by default 1. + num_eval_samples : int, optional + Number of samples per input for evaluation, by default 1. + log_frequency : int, optional + Frequency of logging, defaults to eval_frequency if None. + save_frequency : int, optional + Frequency of saving checkpoints, by default None (no saving). + save_path : str, optional + Path template for saving agent checkpoints, by default "checkpoints/agent.pkl". + min_score : int, optional + Minimum score threshold for processing, by default None. + verbose : bool or str, optional + Verbosity level for training output, by default False. + num_threads : int, optional + Number of threads for parallel processing, by default None. + **kwargs + Additional arguments passed to subclass methods. + + Returns + ------- + tuple[list[float], float or None] + Training scores and final test score. + + Notes + ----- + The training procedure follows these steps for each minibatch: + 1. Forward pass: Run agent on inputs and compute feedback using guide + 2. Parameter update: Apply optimizer to update agent parameters + 3. Improvement check: Optionally validate and potentially rollback updates + 4. Evaluation: Periodically evaluate agent performance on test data + 5. Logging: Track training metrics and parameter values + 6. Checkpointing: Save agent state at specified intervals """ log_frequency = log_frequency or eval_frequency # frequency of logging (default to eval_frequency) @@ -145,7 +268,32 @@ def train(self, return train_scores, test_score def evaluate(self, agent, guide, xs, infos, min_score=None, num_samples=1, num_threads=None, description=None): - """ Evaluate the agent on the given dataset. """ + """Evaluate the agent on a dataset and return the average score. + + Parameters + ---------- + agent : trace.Module + The agent to evaluate. + guide : Guide + Guide function to provide evaluation scores. + xs : list + List of input data points. + infos : list + List of additional information for each input. + min_score : float, optional + Minimum score for evaluation, by default None. + num_samples : int, optional + Number of samples per input for evaluation, by default 1. + num_threads : int, optional + Number of threads for parallel evaluation, by default None. + description : str, optional + Description for progress tracking, by default None. + + Returns + ------- + float or None + Average evaluation score, or None if any scores are invalid. + """ num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads test_scores = evaluate(agent, guide, xs, infos, min_score=min_score, num_threads=num_threads, num_samples=num_samples, description=description) @@ -153,17 +301,44 @@ def evaluate(self, agent, guide, xs, infos, min_score=None, num_samples=1, num_t return np.mean(test_scores) def has_improvement(self, xs, guide, infos, current_score, current_outputs, backup_dict, threshold=0, num_threads=None, *args, **kwargs): - # This function can be overridden by subclasses to implement their own improvement check. - """ Check if the updated agent is improved compared to the current one. - - Args: - xs: inputs - infos: additional information for the guide - current_score: current score of the agent - current_outputs: outputs of the agent, guide interaction - backup_dict: backup of the current value of the parameters - improvement_threshold: threshold for improvement - num_threads: maximum number of threads to use + """Check if the updated agent shows improvement over the previous version. + + This method evaluates the updated agent and compares its performance to the + current score to determine whether the parameter update should be accepted. + + Parameters + ---------- + xs : list + Input data points for evaluation. + guide : Guide + Guide function to provide evaluation scores. + infos : list + Additional information for each input. + current_score : float + Score of the agent before the update. + current_outputs : list + Outputs from the agent-guide interaction. + backup_dict : dict + Backup of parameter values before the update. + threshold : float, optional + Minimum improvement threshold, by default 0. + num_threads : int, optional + Number of threads for evaluation, by default None. + *args + Additional positional arguments. + **kwargs + Additional keyword arguments. + + Returns + ------- + bool + True if the update shows improvement, False otherwise. + + Notes + ----- + This method can be overridden by subclasses to implement custom + improvement validation logic. The default implementation evaluates + the updated agent and compares against the threshold. """ num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads new_score = self.evaluate(self.agent, guide, xs, infos, num_threads=num_threads, @@ -179,25 +354,65 @@ def has_improvement(self, xs, guide, infos, current_score, current_outputs, back def forward(self, agent, x, guide, info): - """ Forward the agent on the input and compute the feedback using the guide. - Args: - agent: trace.Module - x: input - guide: (question, student_answer, info) -> score, feedback - info: additional information for the guide - Returns: - outputs that will be used to update the agent + """Execute forward pass and compute feedback for a single input. + + This method must be implemented by subclasses to define how the agent + processes individual inputs and generates outputs for parameter updates. + + Parameters + ---------- + agent : trace.Module + The agent module to execute. + x : Any + Input data for the agent. + guide : callable + Guide function with signature (question, student_answer, info) -> (score, feedback). + info : Any + Additional information for the guide. + + Returns + ------- + Any + Outputs that will be used by the update method to modify agent parameters. + + Raises + ------ + NotImplementedError + This method must be implemented by subclasses. """ raise NotImplementedError("Subclasses must implement this method") def update(self, outputs, verbose=False, num_threads=None, **kwargs): - """ Subclasses can implement this method to update the agent. - Args: - outputs: returned value from self.step - verbose: whether to print the output of the agent - num_threads: maximum number of threads to use (overrides self.num_threads) - Returns: - score: average score of the minibatch of inputs + """Update the agent parameters based on forward pass outputs. + + This method must be implemented by subclasses to define how parameter + updates are computed and applied based on the forward pass results. + + Parameters + ---------- + outputs : Any + Outputs returned from the forward method. + verbose : bool, optional + Whether to print verbose update information, by default False. + num_threads : int, optional + Maximum number of threads to use, overrides self.num_threads, by default None. + **kwargs + Additional keyword arguments for the update process. + + Returns + ------- + float or None + Average score of the minibatch inputs, or None if no valid scores. + + Raises + ------ + NotImplementedError + This method must be implemented by subclasses. + + Notes + ----- + The update method should process the outputs from forward passes, + apply parameter updates using the optimizer, and return performance metrics. """ num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads raise NotImplementedError("Subclasses must implement this method") @@ -206,7 +421,23 @@ def update(self, outputs, verbose=False, num_threads=None, **kwargs): @trace.bundle() def batchify(*items): - """ Concatenate the items into a single string """ + """Concatenate multiple items into a formatted batch string. + + Parameters + ---------- + *items : Any + Variable number of items to concatenate into a batch. + + Returns + ------- + str + Formatted string with each item labeled by ID. + + Notes + ----- + This function is decorated with @trace.bundle() and creates a formatted + string where each item is prefixed with 'ID [i]:' for identification. + """ output = '' for i, item in enumerate(items): output += f'ID {[i]}: {item}\n' @@ -214,12 +445,49 @@ def batchify(*items): class MinibatchAlgorithm(Minibatch): - """ - The computed output of each instance in the minibatch is aggregated and a batched feedback is provided to update the agent. + """Standard minibatch algorithm that aggregates outputs for batch feedback. + + This algorithm processes each instance in the minibatch individually, then + concatenates the outputs and feedback to provide a single batched update + to the agent. This approach allows the agent to learn from multiple examples + simultaneously in each optimization step. + + Attributes + ---------- + agent : trace.Module + The agent being trained (inherited from parent). + optimizer : Optimizer + The optimizer for parameter updates (inherited from parent). + + Notes + ----- + The algorithm follows these steps: + 1. Execute standard optimization steps for each minibatch instance + 2. Aggregate targets and feedback using the batchify function + 3. Apply optimizer backward pass with batched feedback + 4. Update agent parameters using the optimizer step """ def forward(self, agent, x, guide, info): - return standard_optimization_step(agent, x, guide, info) # (score, target, feedback) + """Execute standard optimization step for a single input. + + Parameters + ---------- + agent : trace.Module + The agent to execute. + x : Any + Input data. + guide : callable + Guide function for feedback generation. + info : Any + Additional information for the guide. + + Returns + ------- + tuple[trace.Node, float, str] + Tuple of (target, score, feedback) from standard optimization step. + """ + return standard_optimization_step(agent, x, guide, info) # (target, score, feedback) def update(self, outputs, verbose=False, num_threads=None, **kwargs): """ Subclasses can implement this method to update the agent. diff --git a/opto/trainer/train.py b/opto/trainer/train.py index e3b79e05..da2d9489 100644 --- a/opto/trainer/train.py +++ b/opto/trainer/train.py @@ -31,10 +31,92 @@ def train( # The rest is treated as trainer config **trainer_kwargs, ) -> None: - """ A high-level helper function to train the model using trainer. - - A trainer algorithm applies an optimizer to train a model under a guide on a train_dataset. - + """High-level training function for Trace models using optimization algorithms. + + Provides a unified interface for training Trace models by combining an optimizer, + training algorithm, evaluation guide, and logging. Automatically configures + components based on the model type and provided parameters. + + Parameters + ---------- + model : Union[trace.Module, ParameterNode] + The model to train. Can be a Trace Module with multiple parameters + or a single ParameterNode for direct optimization. + train_dataset : dict + Training dataset with required keys: + - 'inputs': List of input samples + - 'infos': List of corresponding target/reference information + Both lists must have the same length. + algorithm : Union[Trainer, str], default='MinibatchAlgorithm' + Training algorithm to use. Can be a Trainer instance or string name. + Common algorithms: 'MinibatchAlgorithm', 'BeamSearchAlgorithm'. + optimizer : Union[Optimizer, str], optional + Optimizer for parameter updates. If None, automatically selected: + - 'OPROv2' for ParameterNode models + - 'OptoPrimeV2' for Module models + Can be optimizer instance or string name. + guide : Union[Guide, str], default='LLMJudge' + Evaluation guide that provides feedback on model outputs. + Common guides: 'LLMJudge', 'ExactMatchGuide'. + logger : Union[BaseLogger, str], default='ConsoleLogger' + Logger for tracking training progress and metrics. + optimizer_kwargs : dict, optional + Additional keyword arguments passed to optimizer constructor. + Useful for specifying LLM instances, learning rates, etc. + guide_kwargs : dict, optional + Additional keyword arguments passed to guide constructor. + logger_kwargs : dict, optional + Additional keyword arguments passed to logger constructor. + **trainer_kwargs + Additional configuration passed to the training algorithm, + such as batch size, number of epochs, early stopping criteria. + + Raises + ------ + AssertionError + If dataset format is invalid (missing keys, mismatched lengths). + + Notes + ----- + The training process follows these steps: + 1. **Dataset Validation**: Ensures dataset has correct format and structure + 2. **Component Setup**: Instantiates optimizer, guide, and logger from strings/configs + 3. **Model Preparation**: Converts ParameterNode to Module if needed + 4. **Algorithm Execution**: Runs the specified training algorithm + + Training algorithms coordinate the optimization process: + - Generate batches from the dataset + - Apply the model to inputs + - Use the guide to evaluate outputs and provide feedback + - Update model parameters through the optimizer + - Log progress and metrics + + Examples + -------- + >>> # Train a simple text model + >>> model = MyTextModel() + >>> dataset = { + ... 'inputs': ['What is AI?', 'Explain ML'], + ... 'infos': ['Artificial Intelligence...', 'Machine Learning...'] + ... } + >>> train(model=model, train_dataset=dataset, algorithm='MinibatchAlgorithm') + + >>> # Train with custom configuration + >>> train( + ... model=model, + ... train_dataset=dataset, + ... optimizer='OptoPrimeV2', + ... guide='LLMJudge', + ... optimizer_kwargs={'max_tokens': 1000}, + ... batch_size=8, + ... num_epochs=10 + ... ) + + See Also + -------- + Trainer : Base class for training algorithms + Optimizer : Parameter optimization interface + Guide : Evaluation and feedback interface """ optimizer_kwargs = optimizer_kwargs or {} # this can be used to pass extra optimizer configs, like llm object explictly guide_kwargs = guide_kwargs or {} diff --git a/opto/utils/llm.py b/opto/utils/llm.py index a53abbc7..81ded517 100644 --- a/opto/utils/llm.py +++ b/opto/utils/llm.py @@ -11,10 +11,48 @@ pass class AbstractModel: - """ - A minimal abstraction of a model api that refreshes the model every - reset_freq seconds (this is useful for long-running models that may require - refreshing certificates or memory management). + """Abstract base class for LLM model wrappers with automatic refreshing. + + Provides a minimal abstraction for model APIs that need periodic refreshing + for certificate renewal or memory management in long-running processes. + + Parameters + ---------- + factory : callable + A function that takes no arguments and returns a callable model instance. + reset_freq : int or None, optional + Number of seconds after which to refresh the model. If None, the model + is never refreshed. + + Attributes + ---------- + factory : callable + The factory function for creating model instances. + reset_freq : int or None + Refresh frequency in seconds. + model : Any + Property that returns the current model instance. + + Methods + ------- + __call__(*args, **kwargs) + Execute the model, refreshing if needed. + + Notes + ----- + This class handles: + 1. **Automatic Refreshing**: Recreates the model instance periodically + to prevent issues with long-running connections. + 2. **Serialization**: Supports pickling by recreating the model on load. + 3. **Consistent Interface**: Ensures responses are available at + `response['choices'][0]['message']['content']`. + + Subclasses should override the `model` property to customize behavior. + + See Also + -------- + AutoGenLLM : Concrete implementation using AutoGen + LiteLLM : Concrete implementation using LiteLLM """ def __init__(self, factory: Callable, reset_freq: Union[int, None] = None) -> None: @@ -55,10 +93,61 @@ def __setstate__(self, state): class AutoGenLLM(AbstractModel): - """ This is the main class Trace uses to interact with the model. It is a - wrapper around autogen's OpenAIWrapper. For using models not supported by - autogen, subclass AutoGenLLM and override the `_factory` and `create` - method. Users can pass instances of this class to optimizers' llm argument. + """LLM wrapper using AutoGen's OpenAIWrapper for model interactions. + + This class provides integration with AutoGen for accessing various LLM APIs. + It handles configuration, caching, and provides a consistent interface for + the Trace framework. + + Parameters + ---------- + config_list : list, optional + List of model configurations. If None, attempts to load from + 'OAI_CONFIG_LIST' environment variable or auto-constructs from + individual API keys. + filter_dict : dict, optional + Dictionary to filter configurations based on model properties. + reset_freq : int or None, optional + Number of seconds after which to refresh the model connection. + + Methods + ------- + create(**config) + Make a completion request with the given configuration. + _factory(config_list) + Class method to create the underlying AutoGen wrapper. + + Notes + ----- + Configuration sources (in priority order): + 1. Explicitly provided config_list + 2. OAI_CONFIG_LIST environment variable or file + 3. Auto-construction from individual API keys (OPENAI_API_KEY, etc.) + + The create() method supports AutoGen's full configuration options including: + - Templating with context + - Response caching + - Custom filter functions + - API version specification + + For models not supported by AutoGen, subclass and override _factory() + and create() methods. + + See Also + -------- + AbstractModel : Base class for model wrappers + auto_construct_oai_config_list_from_env : Helper for config construction + + Examples + -------- + >>> # Using with explicit configuration + >>> llm = AutoGenLLM(config_list=[{"model": "gpt-4", "api_key": "..."}]) + >>> + >>> # Using with environment variables + >>> llm = AutoGenLLM() # Auto-loads from environment + >>> + >>> # Making a completion + >>> response = llm(messages=[{"role": "user", "content": "Hello"}]) """ def __init__(self, config_list: List = None, filter_dict: Dict = None, reset_freq: Union[int, None] = None) -> None: From 092c582321d34d63b9ba60e9a82f2845c2b94417 Mon Sep 17 00:00:00 2001 From: Adith Swaminathan Date: Fri, 19 Sep 2025 21:31:20 -0700 Subject: [PATCH 267/314] Fixing hard-coded links to repo --- docs/_config.yml | 4 +- docs/examples/nlp/bigbench_hard.ipynb | 2 +- docs/quickstart/quick_start.ipynb | 2 +- docs/quickstart/quick_start_2.ipynb | 524 +++++++++--------- docs/quickstart/virtualhome.md | 6 +- docs/tutorials/optimization_tutorial.ipynb | 2 +- examples/battleship.py | 18 +- ...st_time_loss_for_code_OptoPrimeMulti.ipynb | 2 +- 8 files changed, 280 insertions(+), 280 deletions(-) diff --git a/docs/_config.yml b/docs/_config.yml index 53728795..aa3e7108 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -36,7 +36,7 @@ html: use_issues_button: false use_repository_button: true extra_navbar: Go to Book Content - extra_footer: "Contact Us | Terms Of Use | Trademarks" + extra_footer: "Contact Us | Terms Of Use | Trademarks" analytics: plausible_analytics_domain: agentopt.github.io/trace plausible_analytics_url: https://plausible.io/js/script.js @@ -67,4 +67,4 @@ sphinx: autodoc_typehints: signature autodoc_typehints_format: short autosummary_filename_map: - opto.trace.nodes.node: "opto.trace.nodes.node-function" \ No newline at end of file + opto.trace.nodes.node: "opto.trace.nodes.node-function" diff --git a/docs/examples/nlp/bigbench_hard.ipynb b/docs/examples/nlp/bigbench_hard.ipynb index 00df852a..0e3567bd 100644 --- a/docs/examples/nlp/bigbench_hard.ipynb +++ b/docs/examples/nlp/bigbench_hard.ipynb @@ -16,7 +16,7 @@ "\n", "```{note}\n", "To replicate our experiment in the paper, run the script here:\n", - "https://github.com/microsoft/Trace/blob/main/examples/bbh/run_prompt_bigbench_trace.py\n", + "https://github.com/agentopt/OpenTrace/blob/main/examples/bbh/run_prompt_bigbench_trace.py\n", "```" ] }, diff --git a/docs/quickstart/quick_start.ipynb b/docs/quickstart/quick_start.ipynb index ce8b17a4..5a4dd3ac 100644 --- a/docs/quickstart/quick_start.ipynb +++ b/docs/quickstart/quick_start.ipynb @@ -348,7 +348,7 @@ "id": "5936bb57-b0f1-419a-a1a4-9d3bcd1f5c23", "metadata": {}, "source": [ - "In order for the optimization code to run, create a file with name `OAI_CONFIG_LIST` in the same folder as this notebook. This file should look the same as [OAI_CONFIG_LIST](https://github.com/microsoft/autogen/blob/main/OAI_CONFIG_LIST_sample).\n", + "In order for the optimization code to run, ensure that the `OPENAI_API_KEY` environment variable is set to a valid key by e.g. using the interactive widget at the beginning of this notebook.\n", "\n", "The code below looks like any PyTorch code. We can break it down to a few steps:\n", "1. We first import an optimizer from `opto.optimizers`.\n", diff --git a/docs/quickstart/quick_start_2.ipynb b/docs/quickstart/quick_start_2.ipynb index 0c9c1e1b..49f4c800 100644 --- a/docs/quickstart/quick_start_2.ipynb +++ b/docs/quickstart/quick_start_2.ipynb @@ -133,7 +133,7 @@ "import importlib.util\n", "\n", "# Define the raw URL for downloading\n", - "raw_url = \"https://raw.githubusercontent.com/agentopt/Trace/main/examples/battleship.py\"\n", + "raw_url = \"https://raw.githubusercontent.com/agentopt/OpenTrace/main/examples/battleship.py\"\n", "\n", "# Define the local file path\n", "local_file = \"battleship.py\"\n", @@ -183,39 +183,39 @@ " background-color: #699BF7;\n", " }\n", " .water {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/empty.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/empty.png');\n", " background-size: cover;\n", " }\n", " .hit {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/hit.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/hit.png');\n", " background-size: cover;\n", " }\n", " .miss {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/miss.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/miss.png');\n", " background-size: cover;\n", " }\n", " .ship-head-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hl.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hl.png');\n", " background-size: cover;\n", " }\n", " .ship-body-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_h.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_h.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hr.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hr.png');\n", " background-size: cover;\n", " }\n", " .ship-head-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vt.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vt.png');\n", " background-size: cover;\n", " }\n", " .ship-body-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_v.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_v.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vb.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vb.png');\n", " background-size: cover;\n", " }\n", " \n", @@ -296,39 +296,39 @@ " background-color: #699BF7;\n", " }\n", " .water {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/empty.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/empty.png');\n", " background-size: cover;\n", " }\n", " .hit {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/hit.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/hit.png');\n", " background-size: cover;\n", " }\n", " .miss {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/miss.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/miss.png');\n", " background-size: cover;\n", " }\n", " .ship-head-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hl.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hl.png');\n", " background-size: cover;\n", " }\n", " .ship-body-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_h.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_h.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hr.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hr.png');\n", " background-size: cover;\n", " }\n", " .ship-head-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vt.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vt.png');\n", " background-size: cover;\n", " }\n", " .ship-body-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_v.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_v.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vb.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vb.png');\n", " background-size: cover;\n", " }\n", " \n", @@ -650,39 +650,39 @@ " background-color: #699BF7;\n", " }\n", " .water {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/empty.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/empty.png');\n", " background-size: cover;\n", " }\n", " .hit {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/hit.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/hit.png');\n", " background-size: cover;\n", " }\n", " .miss {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/miss.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/miss.png');\n", " background-size: cover;\n", " }\n", " .ship-head-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hl.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hl.png');\n", " background-size: cover;\n", " }\n", " .ship-body-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_h.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_h.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hr.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hr.png');\n", " background-size: cover;\n", " }\n", " .ship-head-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vt.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vt.png');\n", " background-size: cover;\n", " }\n", " .ship-body-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_v.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_v.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vb.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vb.png');\n", " background-size: cover;\n", " }\n", " \n", @@ -733,39 +733,39 @@ " background-color: #699BF7;\n", " }\n", " .water {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/empty.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/empty.png');\n", " background-size: cover;\n", " }\n", " .hit {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/hit.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/hit.png');\n", " background-size: cover;\n", " }\n", " .miss {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/miss.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/miss.png');\n", " background-size: cover;\n", " }\n", " .ship-head-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hl.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hl.png');\n", " background-size: cover;\n", " }\n", " .ship-body-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_h.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_h.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hr.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hr.png');\n", " background-size: cover;\n", " }\n", " .ship-head-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vt.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vt.png');\n", " background-size: cover;\n", " }\n", " .ship-body-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_v.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_v.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vb.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vb.png');\n", " background-size: cover;\n", " }\n", " \n", @@ -892,39 +892,39 @@ " background-color: #699BF7;\n", " }\n", " .water {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/empty.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/empty.png');\n", " background-size: cover;\n", " }\n", " .hit {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/hit.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/hit.png');\n", " background-size: cover;\n", " }\n", " .miss {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/miss.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/miss.png');\n", " background-size: cover;\n", " }\n", " .ship-head-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hl.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hl.png');\n", " background-size: cover;\n", " }\n", " .ship-body-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_h.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_h.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hr.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hr.png');\n", " background-size: cover;\n", " }\n", " .ship-head-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vt.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vt.png');\n", " background-size: cover;\n", " }\n", " .ship-body-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_v.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_v.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vb.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vb.png');\n", " background-size: cover;\n", " }\n", " \n", @@ -1036,39 +1036,39 @@ " background-color: #699BF7;\n", " }\n", " .water {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/empty.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/empty.png');\n", " background-size: cover;\n", " }\n", " .hit {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/hit.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/hit.png');\n", " background-size: cover;\n", " }\n", " .miss {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/miss.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/miss.png');\n", " background-size: cover;\n", " }\n", " .ship-head-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hl.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hl.png');\n", " background-size: cover;\n", " }\n", " .ship-body-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_h.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_h.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hr.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hr.png');\n", " background-size: cover;\n", " }\n", " .ship-head-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vt.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vt.png');\n", " background-size: cover;\n", " }\n", " .ship-body-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_v.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_v.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vb.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vb.png');\n", " background-size: cover;\n", " }\n", " \n", @@ -1199,39 +1199,39 @@ " background-color: #699BF7;\n", " }\n", " .water {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/empty.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/empty.png');\n", " background-size: cover;\n", " }\n", " .hit {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/hit.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/hit.png');\n", " background-size: cover;\n", " }\n", " .miss {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/miss.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/miss.png');\n", " background-size: cover;\n", " }\n", " .ship-head-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hl.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hl.png');\n", " background-size: cover;\n", " }\n", " .ship-body-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_h.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_h.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hr.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hr.png');\n", " background-size: cover;\n", " }\n", " .ship-head-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vt.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vt.png');\n", " background-size: cover;\n", " }\n", " .ship-body-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_v.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_v.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vb.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vb.png');\n", " background-size: cover;\n", " }\n", " \n", @@ -1354,39 +1354,39 @@ " background-color: #699BF7;\n", " }\n", " .water {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/empty.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/empty.png');\n", " background-size: cover;\n", " }\n", " .hit {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/hit.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/hit.png');\n", " background-size: cover;\n", " }\n", " .miss {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/miss.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/miss.png');\n", " background-size: cover;\n", " }\n", " .ship-head-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hl.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hl.png');\n", " background-size: cover;\n", " }\n", " .ship-body-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_h.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_h.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hr.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hr.png');\n", " background-size: cover;\n", " }\n", " .ship-head-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vt.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vt.png');\n", " background-size: cover;\n", " }\n", " .ship-body-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_v.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_v.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vb.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vb.png');\n", " background-size: cover;\n", " }\n", " \n", @@ -1499,39 +1499,39 @@ " background-color: #699BF7;\n", " }\n", " .water {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/empty.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/empty.png');\n", " background-size: cover;\n", " }\n", " .hit {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/hit.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/hit.png');\n", " background-size: cover;\n", " }\n", " .miss {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/miss.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/miss.png');\n", " background-size: cover;\n", " }\n", " .ship-head-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hl.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hl.png');\n", " background-size: cover;\n", " }\n", " .ship-body-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_h.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_h.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hr.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hr.png');\n", " background-size: cover;\n", " }\n", " .ship-head-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vt.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vt.png');\n", " background-size: cover;\n", " }\n", " .ship-body-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_v.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_v.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vb.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vb.png');\n", " background-size: cover;\n", " }\n", " \n", @@ -1641,39 +1641,39 @@ " background-color: #699BF7;\n", " }\n", " .water {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/empty.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/empty.png');\n", " background-size: cover;\n", " }\n", " .hit {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/hit.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/hit.png');\n", " background-size: cover;\n", " }\n", " .miss {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/miss.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/miss.png');\n", " background-size: cover;\n", " }\n", " .ship-head-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hl.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hl.png');\n", " background-size: cover;\n", " }\n", " .ship-body-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_h.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_h.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hr.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hr.png');\n", " background-size: cover;\n", " }\n", " .ship-head-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vt.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vt.png');\n", " background-size: cover;\n", " }\n", " .ship-body-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_v.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_v.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vb.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vb.png');\n", " background-size: cover;\n", " }\n", " \n", @@ -1800,39 +1800,39 @@ " background-color: #699BF7;\n", " }\n", " .water {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/empty.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/empty.png');\n", " background-size: cover;\n", " }\n", " .hit {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/hit.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/hit.png');\n", " background-size: cover;\n", " }\n", " .miss {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/miss.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/miss.png');\n", " background-size: cover;\n", " }\n", " .ship-head-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hl.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hl.png');\n", " background-size: cover;\n", " }\n", " .ship-body-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_h.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_h.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hr.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hr.png');\n", " background-size: cover;\n", " }\n", " .ship-head-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vt.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vt.png');\n", " background-size: cover;\n", " }\n", " .ship-body-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_v.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_v.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vb.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vb.png');\n", " background-size: cover;\n", " }\n", " \n", @@ -1945,39 +1945,39 @@ " background-color: #699BF7;\n", " }\n", " .water {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/empty.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/empty.png');\n", " background-size: cover;\n", " }\n", " .hit {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/hit.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/hit.png');\n", " background-size: cover;\n", " }\n", " .miss {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/miss.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/miss.png');\n", " background-size: cover;\n", " }\n", " .ship-head-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hl.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hl.png');\n", " background-size: cover;\n", " }\n", " .ship-body-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_h.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_h.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hr.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hr.png');\n", " background-size: cover;\n", " }\n", " .ship-head-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vt.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vt.png');\n", " background-size: cover;\n", " }\n", " .ship-body-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_v.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_v.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vb.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vb.png');\n", " background-size: cover;\n", " }\n", " \n", @@ -2075,39 +2075,39 @@ " background-color: #699BF7;\n", " }\n", " .water {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/empty.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/empty.png');\n", " background-size: cover;\n", " }\n", " .hit {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/hit.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/hit.png');\n", " background-size: cover;\n", " }\n", " .miss {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/miss.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/miss.png');\n", " background-size: cover;\n", " }\n", " .ship-head-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hl.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hl.png');\n", " background-size: cover;\n", " }\n", " .ship-body-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_h.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_h.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hr.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hr.png');\n", " background-size: cover;\n", " }\n", " .ship-head-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vt.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vt.png');\n", " background-size: cover;\n", " }\n", " .ship-body-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_v.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_v.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vb.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vb.png');\n", " background-size: cover;\n", " }\n", " \n", @@ -2334,39 +2334,39 @@ " background-color: #699BF7;\n", " }\n", " .water {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/empty.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/empty.png');\n", " background-size: cover;\n", " }\n", " .hit {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/hit.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/hit.png');\n", " background-size: cover;\n", " }\n", " .miss {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/miss.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/miss.png');\n", " background-size: cover;\n", " }\n", " .ship-head-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hl.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hl.png');\n", " background-size: cover;\n", " }\n", " .ship-body-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_h.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_h.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hr.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hr.png');\n", " background-size: cover;\n", " }\n", " .ship-head-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vt.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vt.png');\n", " background-size: cover;\n", " }\n", " .ship-body-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_v.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_v.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vb.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vb.png');\n", " background-size: cover;\n", " }\n", " \n", @@ -2417,39 +2417,39 @@ " background-color: #699BF7;\n", " }\n", " .water {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/empty.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/empty.png');\n", " background-size: cover;\n", " }\n", " .hit {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/hit.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/hit.png');\n", " background-size: cover;\n", " }\n", " .miss {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/miss.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/miss.png');\n", " background-size: cover;\n", " }\n", " .ship-head-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hl.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hl.png');\n", " background-size: cover;\n", " }\n", " .ship-body-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_h.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_h.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hr.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hr.png');\n", " background-size: cover;\n", " }\n", " .ship-head-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vt.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vt.png');\n", " background-size: cover;\n", " }\n", " .ship-body-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_v.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_v.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vb.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vb.png');\n", " background-size: cover;\n", " }\n", " \n", @@ -2500,39 +2500,39 @@ " background-color: #699BF7;\n", " }\n", " .water {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/empty.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/empty.png');\n", " background-size: cover;\n", " }\n", " .hit {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/hit.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/hit.png');\n", " background-size: cover;\n", " }\n", " .miss {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/miss.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/miss.png');\n", " background-size: cover;\n", " }\n", " .ship-head-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hl.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hl.png');\n", " background-size: cover;\n", " }\n", " .ship-body-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_h.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_h.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hr.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hr.png');\n", " background-size: cover;\n", " }\n", " .ship-head-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vt.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vt.png');\n", " background-size: cover;\n", " }\n", " .ship-body-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_v.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_v.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vb.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vb.png');\n", " background-size: cover;\n", " }\n", " \n", @@ -2583,39 +2583,39 @@ " background-color: #699BF7;\n", " }\n", " .water {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/empty.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/empty.png');\n", " background-size: cover;\n", " }\n", " .hit {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/hit.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/hit.png');\n", " background-size: cover;\n", " }\n", " .miss {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/miss.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/miss.png');\n", " background-size: cover;\n", " }\n", " .ship-head-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hl.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hl.png');\n", " background-size: cover;\n", " }\n", " .ship-body-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_h.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_h.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hr.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hr.png');\n", " background-size: cover;\n", " }\n", " .ship-head-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vt.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vt.png');\n", " background-size: cover;\n", " }\n", " .ship-body-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_v.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_v.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vb.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vb.png');\n", " background-size: cover;\n", " }\n", " \n", @@ -2666,39 +2666,39 @@ " background-color: #699BF7;\n", " }\n", " .water {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/empty.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/empty.png');\n", " background-size: cover;\n", " }\n", " .hit {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/hit.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/hit.png');\n", " background-size: cover;\n", " }\n", " .miss {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/miss.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/miss.png');\n", " background-size: cover;\n", " }\n", " .ship-head-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hl.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hl.png');\n", " background-size: cover;\n", " }\n", " .ship-body-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_h.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_h.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hr.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hr.png');\n", " background-size: cover;\n", " }\n", " .ship-head-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vt.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vt.png');\n", " background-size: cover;\n", " }\n", " .ship-body-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_v.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_v.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vb.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vb.png');\n", " background-size: cover;\n", " }\n", " \n", @@ -2749,39 +2749,39 @@ " background-color: #699BF7;\n", " }\n", " .water {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/empty.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/empty.png');\n", " background-size: cover;\n", " }\n", " .hit {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/hit.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/hit.png');\n", " background-size: cover;\n", " }\n", " .miss {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/miss.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/miss.png');\n", " background-size: cover;\n", " }\n", " .ship-head-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hl.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hl.png');\n", " background-size: cover;\n", " }\n", " .ship-body-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_h.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_h.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hr.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hr.png');\n", " background-size: cover;\n", " }\n", " .ship-head-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vt.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vt.png');\n", " background-size: cover;\n", " }\n", " .ship-body-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_v.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_v.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vb.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vb.png');\n", " background-size: cover;\n", " }\n", " \n", @@ -2832,39 +2832,39 @@ " background-color: #699BF7;\n", " }\n", " .water {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/empty.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/empty.png');\n", " background-size: cover;\n", " }\n", " .hit {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/hit.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/hit.png');\n", " background-size: cover;\n", " }\n", " .miss {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/miss.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/miss.png');\n", " background-size: cover;\n", " }\n", " .ship-head-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hl.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hl.png');\n", " background-size: cover;\n", " }\n", " .ship-body-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_h.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_h.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hr.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hr.png');\n", " background-size: cover;\n", " }\n", " .ship-head-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vt.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vt.png');\n", " background-size: cover;\n", " }\n", " .ship-body-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_v.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_v.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vb.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vb.png');\n", " background-size: cover;\n", " }\n", " \n", @@ -2915,39 +2915,39 @@ " background-color: #699BF7;\n", " }\n", " .water {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/empty.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/empty.png');\n", " background-size: cover;\n", " }\n", " .hit {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/hit.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/hit.png');\n", " background-size: cover;\n", " }\n", " .miss {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/miss.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/miss.png');\n", " background-size: cover;\n", " }\n", " .ship-head-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hl.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hl.png');\n", " background-size: cover;\n", " }\n", " .ship-body-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_h.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_h.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hr.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hr.png');\n", " background-size: cover;\n", " }\n", " .ship-head-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vt.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vt.png');\n", " background-size: cover;\n", " }\n", " .ship-body-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_v.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_v.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vb.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vb.png');\n", " background-size: cover;\n", " }\n", " \n", @@ -2998,39 +2998,39 @@ " background-color: #699BF7;\n", " }\n", " .water {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/empty.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/empty.png');\n", " background-size: cover;\n", " }\n", " .hit {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/hit.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/hit.png');\n", " background-size: cover;\n", " }\n", " .miss {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/miss.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/miss.png');\n", " background-size: cover;\n", " }\n", " .ship-head-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hl.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hl.png');\n", " background-size: cover;\n", " }\n", " .ship-body-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_h.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_h.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hr.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hr.png');\n", " background-size: cover;\n", " }\n", " .ship-head-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vt.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vt.png');\n", " background-size: cover;\n", " }\n", " .ship-body-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_v.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_v.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vb.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vb.png');\n", " background-size: cover;\n", " }\n", " \n", @@ -3081,39 +3081,39 @@ " background-color: #699BF7;\n", " }\n", " .water {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/empty.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/empty.png');\n", " background-size: cover;\n", " }\n", " .hit {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/hit.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/hit.png');\n", " background-size: cover;\n", " }\n", " .miss {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/miss.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/miss.png');\n", " background-size: cover;\n", " }\n", " .ship-head-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hl.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hl.png');\n", " background-size: cover;\n", " }\n", " .ship-body-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_h.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_h.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hr.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hr.png');\n", " background-size: cover;\n", " }\n", " .ship-head-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vt.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vt.png');\n", " background-size: cover;\n", " }\n", " .ship-body-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_v.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_v.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vb.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vb.png');\n", " background-size: cover;\n", " }\n", " \n", @@ -3164,39 +3164,39 @@ " background-color: #699BF7;\n", " }\n", " .water {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/empty.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/empty.png');\n", " background-size: cover;\n", " }\n", " .hit {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/hit.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/hit.png');\n", " background-size: cover;\n", " }\n", " .miss {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/miss.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/miss.png');\n", " background-size: cover;\n", " }\n", " .ship-head-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hl.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hl.png');\n", " background-size: cover;\n", " }\n", " .ship-body-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_h.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_h.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hr.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hr.png');\n", " background-size: cover;\n", " }\n", " .ship-head-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vt.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vt.png');\n", " background-size: cover;\n", " }\n", " .ship-body-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_v.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_v.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vb.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vb.png');\n", " background-size: cover;\n", " }\n", " \n", @@ -3247,39 +3247,39 @@ " background-color: #699BF7;\n", " }\n", " .water {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/empty.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/empty.png');\n", " background-size: cover;\n", " }\n", " .hit {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/hit.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/hit.png');\n", " background-size: cover;\n", " }\n", " .miss {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/miss.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/miss.png');\n", " background-size: cover;\n", " }\n", " .ship-head-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hl.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hl.png');\n", " background-size: cover;\n", " }\n", " .ship-body-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_h.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_h.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hr.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hr.png');\n", " background-size: cover;\n", " }\n", " .ship-head-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vt.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vt.png');\n", " background-size: cover;\n", " }\n", " .ship-body-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_v.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_v.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vb.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vb.png');\n", " background-size: cover;\n", " }\n", " \n", @@ -3330,39 +3330,39 @@ " background-color: #699BF7;\n", " }\n", " .water {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/empty.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/empty.png');\n", " background-size: cover;\n", " }\n", " .hit {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/hit.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/hit.png');\n", " background-size: cover;\n", " }\n", " .miss {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/miss.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/miss.png');\n", " background-size: cover;\n", " }\n", " .ship-head-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hl.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hl.png');\n", " background-size: cover;\n", " }\n", " .ship-body-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_h.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_h.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hr.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hr.png');\n", " background-size: cover;\n", " }\n", " .ship-head-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vt.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vt.png');\n", " background-size: cover;\n", " }\n", " .ship-body-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_v.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_v.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vb.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vb.png');\n", " background-size: cover;\n", " }\n", " \n", @@ -3413,39 +3413,39 @@ " background-color: #699BF7;\n", " }\n", " .water {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/empty.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/empty.png');\n", " background-size: cover;\n", " }\n", " .hit {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/hit.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/hit.png');\n", " background-size: cover;\n", " }\n", " .miss {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/miss.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/miss.png');\n", " background-size: cover;\n", " }\n", " .ship-head-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hl.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hl.png');\n", " background-size: cover;\n", " }\n", " .ship-body-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_h.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_h.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hr.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hr.png');\n", " background-size: cover;\n", " }\n", " .ship-head-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vt.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vt.png');\n", " background-size: cover;\n", " }\n", " .ship-body-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_v.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_v.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vb.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vb.png');\n", " background-size: cover;\n", " }\n", " \n", @@ -3496,39 +3496,39 @@ " background-color: #699BF7;\n", " }\n", " .water {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/empty.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/empty.png');\n", " background-size: cover;\n", " }\n", " .hit {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/hit.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/hit.png');\n", " background-size: cover;\n", " }\n", " .miss {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/miss.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/miss.png');\n", " background-size: cover;\n", " }\n", " .ship-head-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hl.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hl.png');\n", " background-size: cover;\n", " }\n", " .ship-body-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_h.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_h.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hr.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hr.png');\n", " background-size: cover;\n", " }\n", " .ship-head-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vt.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vt.png');\n", " background-size: cover;\n", " }\n", " .ship-body-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_v.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_v.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vb.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vb.png');\n", " background-size: cover;\n", " }\n", " \n", @@ -3579,39 +3579,39 @@ " background-color: #699BF7;\n", " }\n", " .water {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/empty.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/empty.png');\n", " background-size: cover;\n", " }\n", " .hit {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/hit.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/hit.png');\n", " background-size: cover;\n", " }\n", " .miss {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/miss.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/miss.png');\n", " background-size: cover;\n", " }\n", " .ship-head-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hl.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hl.png');\n", " background-size: cover;\n", " }\n", " .ship-body-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_h.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_h.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-horizontal {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hr.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hr.png');\n", " background-size: cover;\n", " }\n", " .ship-head-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vt.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vt.png');\n", " background-size: cover;\n", " }\n", " .ship-body-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_v.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_v.png');\n", " background-size: cover;\n", " }\n", " .ship-tail-vertical {\n", - " background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vb.png');\n", + " background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vb.png');\n", " background-size: cover;\n", " }\n", " \n", diff --git a/docs/quickstart/virtualhome.md b/docs/quickstart/virtualhome.md index 3f48a184..374d08bf 100644 --- a/docs/quickstart/virtualhome.md +++ b/docs/quickstart/virtualhome.md @@ -183,7 +183,7 @@ Therefore, we can directly call `backward` on the next observation. ``` ```{tip} -To learn more about how to use Trace to create an agent in an interactive environment, check out the [Meta-World](https://microsoft.github.io/Trace/examples/robotics/metaworld.html) example. +To learn more about how to use Trace to create an agent in an interactive environment, check out the [Meta-World](https://agentopt.github.io/OpenTrace/examples/robotics/metaworld.html) example. ``` ## Results @@ -289,9 +289,9 @@ In this tutorial, we showed how to create two agents and have them interact with If you are interested in knowing how to use Trace for your own projects, continue learning the basics of Trace. ```{note} -To learn more about how to trace through agent-environment interactions, check out the [Meta-World](https://microsoft.github.io/Trace/examples/robotics/metaworld.html) example. +To learn more about how to trace through agent-environment interactions, check out the [Meta-World](https://agentopt.github.io/OpenTrace/examples/robotics/metaworld.html) example. ``` ```{note} -To see another example of multi-agent interaction in a different environment, check out the [Negotiation Arena](https://microsoft.github.io/Trace/examples/game/negotiation_arena.html) example. +To see another example of multi-agent interaction in a different environment, check out the [Negotiation Arena](https://agentopt.github.io/OpenTrace/examples/game/negotiation_arena.html) example. ``` diff --git a/docs/tutorials/optimization_tutorial.ipynb b/docs/tutorials/optimization_tutorial.ipynb index 0cf4ca1f..30359aca 100644 --- a/docs/tutorials/optimization_tutorial.ipynb +++ b/docs/tutorials/optimization_tutorial.ipynb @@ -128,7 +128,7 @@ "\n", "We apply `FunctionOptimizer` to change the input to the function `foobar` such that the simulated user is satisfied. To this end, we backpropagated the user's language feedback about the output, through the graph that connects the input to the output.\n", "\n", - "We use helper functions from [AutoGen](https://github.com/microsoft/autogen) to call LLMs to interpret the user's language feedback. Before running the cell below, please copy `OAI_CONFIG_LIST_sample` from the root folder of this repository to the current folder, rename it to `OAI_CONFIG_LIST`, and set the correct configuration for LLMs in there." + "We use helper functions from LiteLLM by default to call LLMs to interpret the user's language feedback. Before running the cell below, please ensure that the `OPENAI_API_KEY` environment variable is set to a valid key by e.g. using the interactive widget at the beginning of this notebook." ] }, { diff --git a/examples/battleship.py b/examples/battleship.py index c3c85a51..a18d99c7 100644 --- a/examples/battleship.py +++ b/examples/battleship.py @@ -247,39 +247,39 @@ def render_html(self, show_ships=False): background-color: #699BF7; } .water { - background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/empty.png'); + background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/empty.png'); background-size: cover; } .hit { - background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/hit.png'); + background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/hit.png'); background-size: cover; } .miss { - background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/miss.png'); + background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/miss.png'); background-size: cover; } .ship-head-horizontal { - background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hl.png'); + background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hl.png'); background-size: cover; } .ship-body-horizontal { - background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_h.png'); + background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_h.png'); background-size: cover; } .ship-tail-horizontal { - background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_hr.png'); + background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_hr.png'); background-size: cover; } .ship-head-vertical { - background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vt.png'); + background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vt.png'); background-size: cover; } .ship-body-vertical { - background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_v.png'); + background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_v.png'); background-size: cover; } .ship-tail-vertical { - background-image: url('https://microsoft.github.io/Trace/images/battleship_widgets/ship_vb.png'); + background-image: url('https://agentopt.github.io/OpenTrace/images/battleship_widgets/ship_vb.png'); background-size: cover; } diff --git a/examples/textgrad_examples/notebooks/textgrad_test_time_loss_for_code_OptoPrimeMulti.ipynb b/examples/textgrad_examples/notebooks/textgrad_test_time_loss_for_code_OptoPrimeMulti.ipynb index 7cd54a63..ba0ac312 100644 --- a/examples/textgrad_examples/notebooks/textgrad_test_time_loss_for_code_OptoPrimeMulti.ipynb +++ b/examples/textgrad_examples/notebooks/textgrad_test_time_loss_for_code_OptoPrimeMulti.ipynb @@ -13,7 +13,7 @@ "outputs": [], "source": [ "%pip install textgrad\n", - "%pip install git+https://github.com/microsoft/Trace.git\n", + "%pip install git+https://github.com/agentopt/OpenTrace.git\n", "%pip install dask[dataframe]\n", "%pip install autogen" ] From 15c739e19af3f25f6360182e7c976d335cc86498 Mon Sep 17 00:00:00 2001 From: chinganc Date: Sat, 20 Sep 2025 18:52:05 +0000 Subject: [PATCH 268/314] Fix memory leak in backward. --- opto/trace/nodes.py | 27 ++++++++++++++++ opto/trace/propagators/graph_propagator.py | 4 +++ tests/test_memory_leak.py | 37 ++++++++++++++++++++++ tests/unit_tests/test_backward.py | 29 +++++++++++++++++ tests/unit_tests/test_dependencies.py | 3 +- 5 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 tests/test_memory_leak.py diff --git a/opto/trace/nodes.py b/opto/trace/nodes.py index 0c577705..bb2c3f6c 100644 --- a/opto/trace/nodes.py +++ b/opto/trace/nodes.py @@ -142,6 +142,7 @@ class Graph: TRACE = True # When True, we trace the graph when creating MessageNode. When False, we don't trace the graph. LEGACY_GRAPH_BEHAVIOR = False # When True, we use the legacy graph behavior where nodes are stored in lists. When False, we only store the count of nodes to save memory. + ALLOW_NESTED_GRAPHS = False # When True, we allow nested graphs. When False, we don't allow nested graphs. def __init__(self): """Initialize the Graph object. @@ -1111,6 +1112,24 @@ def _itemize(self): # for priority queue """ return (-self.level, id(self), self) + def _detach(self): + """Detach the node from its children and parents to break the graph. + + Notes: + This method removes all edges between the current node and its children and parents, + effectively isolating the current node from the graph. + + XXX This method does not propagate the updated level to the children. Use with caution. + """ + for c in self.children: + c._parents.remove(self) + for p in self.parents: + p._children.remove(self) + self._children = [] + # self._parents = [] # we still keep this to allow tracking inputs to create this node for computing the ``effective'' Jacobian. + self.zero_feedback() + # NOTE we do not update the levels of this node and its children. Use with caution. + def backward( self, feedback: Any = "", @@ -1201,6 +1220,11 @@ def backward( node.zero_feedback() for parent in node.parents: + if len(parent.parameter_dependencies) == 0 and not GRAPH.ALLOW_NESTED_GRAPHS: + continue # skip parents that are not descendants of parameters to save memory + # This will break the nested graph functionality + # TODO better implementation for nested graphs for memory efficiency + if parent in propagated_feedback: parent._add_feedback(node, propagated_feedback[parent]) @@ -1241,6 +1265,9 @@ def backward( digraph.node(parent.py_name, **nvsg.get_attrs(parent)) node._backwarded = not retain_graph # set backwarded to True + if node._backwarded and not GRAPH.LEGACY_GRAPH_BEHAVIOR: + node._detach() # detach the node from the graph to save memory + except IndexError: # queue is empty break diff --git a/opto/trace/propagators/graph_propagator.py b/opto/trace/propagators/graph_propagator.py index 23f4131e..c84d6473 100644 --- a/opto/trace/propagators/graph_propagator.py +++ b/opto/trace/propagators/graph_propagator.py @@ -7,6 +7,7 @@ get_op_name, IDENTITY_OPERATORS, NodeVizStyleGuideColorful, + GRAPH, ) from opto.trace.propagators.propagators import Propagator, AbstractFeedback import heapq @@ -112,6 +113,9 @@ def expand(cls, node: MessageNode): 4. Restores original feedback 5. Returns the collected subgraph """ + if not GRAPH.ALLOW_NESTED_GRAPHS: + raise ValueError("Nested graphs are not allowed. Set GRAPH.ALLOW_NESTED_GRAPHS = True to enable this feature.") + assert isinstance(node, MessageNode) if isinstance(node.info["output"], MessageNode): # these are the nodes where we will collect the feedback diff --git a/tests/test_memory_leak.py b/tests/test_memory_leak.py new file mode 100644 index 00000000..62ffce0e --- /dev/null +++ b/tests/test_memory_leak.py @@ -0,0 +1,37 @@ +from memory_profiler import profile +import sys +from opto.trace import node, GRAPH, bundle +import numpy as np + +# GRAPH.LEGACY_GRAPH_BEHAVIOR = True +# GRAPH.clear() + +base = node(np.ones(10000000)) + +@bundle() +def add(x, y): + return x + y + +def fun(x): + return x + np.ones(10000000) + # return add(x, base) + # return add(x, np.ones(10000000)) + +@profile +def test_multiple_backward(): + x = node(1, name="x", trainable=True) + + for i in range(100): + y1 = fun(x) + y2 = fun(x) + x.zero_feedback() + y1.backward("first backward") + y2.backward("second backward") + x.zero_feedback() + + print(len(x.feedback)) # should be 0 + # print(len(base.feedback)) # should be 0 + + +if __name__ == "__main__": + test_multiple_backward() \ No newline at end of file diff --git a/tests/unit_tests/test_backward.py b/tests/unit_tests/test_backward.py index 35522517..6f5ca614 100644 --- a/tests/unit_tests/test_backward.py +++ b/tests/unit_tests/test_backward.py @@ -1,4 +1,5 @@ import copy +import numpy as np from opto.trace import node, bundle from opto.trace.nodes import GRAPH, Node from opto.trace.propagators import GraphPropagator @@ -99,6 +100,34 @@ def test_node_feedback(): +def test_multiple_backward(): + # This test _detach calls in backward + + x = node(1, name="x", trainable=True) + def fun(x): + return x + np.ones(10) + + for i in range(10): + y1 = fun(x) + y2 = fun(x) + x.zero_feedback() + y1.backward("first backward") + # after backward, y1 should be detached from x + assert len(x.children) == 1 + # but the feedback should be there + assert len(x.feedback) == 1 + + y2.backward("second backward") + # after backward, y2 should be detached from x + assert len(x.children) == 0 + # but the feedback should be there + assert len(x.feedback) == 2 + + + + + + # def sum_of_integers(): # y = x.clone() # z = ops.add(x, y) diff --git a/tests/unit_tests/test_dependencies.py b/tests/unit_tests/test_dependencies.py index 2961ad91..a85e704e 100644 --- a/tests/unit_tests/test_dependencies.py +++ b/tests/unit_tests/test_dependencies.py @@ -1,6 +1,7 @@ import pytest -from opto.trace import node, bundle +from opto.trace import node, bundle, GRAPH from opto.trace.utils import contain, sum_feedback +GRAPH.ALLOW_NESTED_GRAPHS = True def test_flat_dependencies(): x = node(1.0, trainable=True) From 93eed0dbd08b9943fb50408b588eddd30d5088cd Mon Sep 17 00:00:00 2001 From: doxav Date: Sun, 21 Sep 2025 23:43:55 +0200 Subject: [PATCH 269/314] validated on many different LLM robust suggestion extraction and made it common to JSON mode on OptoPrimev2 and OPROv2 --- opto/optimizers/optoprime.py | 15 ++- opto/optimizers/optoprime_v2.py | 62 +--------- tests/llm_optimizers_tests/test_optimizer.py | 114 ++++++++++++++++++- 3 files changed, 124 insertions(+), 67 deletions(-) diff --git a/opto/optimizers/optoprime.py b/opto/optimizers/optoprime.py index 454cf331..cdbb9f23 100644 --- a/opto/optimizers/optoprime.py +++ b/opto/optimizers/optoprime.py @@ -603,9 +603,15 @@ def _find_key(node_name: str, sugg: Dict[str, Any]) -> Optional[str]: raise e return update_dict - def extract_llm_suggestion(self, response: str): + def extract_llm_suggestion(self, response: str, suggestion_tag=None, reasoning_tag=None, return_only_suggestion=True) -> Dict[str, Any]: """Extract the suggestion from the response.""" - suggestion_tag = self.default_json_keys["suggestion"] + suggestion_tag = suggestion_tag or self.default_json_keys.get("suggestion", "suggestion") + reasoning_tag = reasoning_tag or self.default_json_keys.get("reasoning", "reasoning") + + if "```" in response: + match = re.findall(r"```(.*?)```", response, re.DOTALL) + if len(match) > 0: + response = match[0] json_extracted = {} suggestion = {} @@ -613,7 +619,10 @@ def extract_llm_suggestion(self, response: str): while attempt_n < 2: try: json_extracted = json.loads(response) + if isinstance(json_extracted, dict): # trim all whitespace keys in the json_extracted + json_extracted = {k.strip(): v for k, v in json_extracted.items()} suggestion = json_extracted.get(suggestion_tag, json_extracted) + reasoning = json_extracted.get(reasoning_tag, "") break except json.JSONDecodeError: response = re.findall(r"{.*}", response, re.DOTALL) @@ -648,7 +657,7 @@ def extract_llm_suggestion(self, response: str): for key in keys_to_remove: del suggestion[key] - return suggestion + return suggestion if return_only_suggestion else {"reasoning": reasoning, "variables": suggestion} def call_llm( self, diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index 4aae5f32..164870e0 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -171,65 +171,9 @@ def example_output(self, reasoning, variables): } return json.dumps(output, indent=2) - def output_response_extractor(self, response: str) -> Dict[str, Any]: - reasoning = "" - suggestion_tag = "suggestion" - - if "```" in response: - response = response.replace("```", "").strip() - - suggestion = {} - attempt_n = 0 - while attempt_n < 2: - try: - suggestion = json.loads(response)[suggestion_tag] - reasoning = json.loads(response)[self.reasoning_tag] - break - except json.JSONDecodeError: - # Remove things outside the brackets - response = re.findall(r"{.*}", response, re.DOTALL) - if len(response) > 0: - response = response[0] - attempt_n += 1 - except Exception: - attempt_n += 1 - - if not isinstance(suggestion, dict): - suggestion = {} - - if len(suggestion) == 0: - # we try to extract key/value separately and return it as a dictionary - pattern = rf'"{suggestion_tag}"\s*:\s*\{{(.*?)\}}' - suggestion_match = re.search(pattern, str(response), re.DOTALL) - if suggestion_match: - suggestion = {} - # Extract the entire content of the suggestion dictionary - suggestion_content = suggestion_match.group(1) - # Regex to extract each key-value pair; - # This scheme assumes double quotes but is robust to missing commas at the end of the line - pair_pattern = r'"([a-zA-Z0-9_]+)"\s*:\s*"(.*)"' - # Find all matches of key-value pairs - pairs = re.findall(pair_pattern, suggestion_content, re.DOTALL) - for key, value in pairs: - suggestion[key] = value - - if len(suggestion) == 0: - print(f"Cannot extract suggestion from LLM's response:") - print(response) - - # if the suggested value is a code, and the entire code body is empty (i.e., not even function signature is present) - # then we remove such suggestion - keys_to_remove = [] - for key, value in suggestion.items(): - if "__code" in key and value.strip() == "": - keys_to_remove.append(key) - for key in keys_to_remove: - del suggestion[key] - - extracted_data = {"reasoning": reasoning, - "variables": suggestion} - - return extracted_data + def output_response_extractor(self, response: str, suggestion_tag = "suggestion") -> Dict[str, Any]: + # Use extract_llm_suggestion from OptoPrime => it could be implemented the other way around (OptoPrime would uses this helper but it should be moved out of OptoPrimev2) + return OptoPrime.extract_llm_suggestion(self, response, suggestion_tag=suggestion_tag, reasoning_tag="reasoning", return_only_suggestion=False) class OptimizerPromptSymbolSet2(OptimizerPromptSymbolSet): variables_section_title = "# Variables" diff --git a/tests/llm_optimizers_tests/test_optimizer.py b/tests/llm_optimizers_tests/test_optimizer.py index 445d03e5..b7d7f541 100644 --- a/tests/llm_optimizers_tests/test_optimizer.py +++ b/tests/llm_optimizers_tests/test_optimizer.py @@ -3,11 +3,12 @@ from opto.trace import bundle, node, GRAPH import opto.optimizers from opto.optimizers import OptoPrimeMulti, OptoPrime, TextGrad +from opto.optimizers.optoprime_v2 import OptimizerPromptSymbolSetJSON import importlib import inspect import json import pickle -from opto.utils.llm import LLM +from opto.utils.llm import LLM, LLMFactory # Dynamically get all optimizer classes from opto.optimizers def get_all_optimizers(): @@ -17,12 +18,76 @@ def get_all_optimizers(): item = getattr(opto.optimizers, name) # Check if it's a class and has 'step' method (likely an optimizer) if inspect.isclass(item) and hasattr(item, 'step'): - optimizers.append(item) + if name in ("OptoPrimeV2", "OPROv2"): + # 1) Default (XML) variant = original class + optimizers.append(item) + # 2) JSON variant: tiny subclass that forces JSON settings + JSONSubclass = type(f"{name}_JSON", (item,), {}) + # bind current base class to avoid late-binding gotchas + def _json_init(self, *args, __base=item, **kwargs): + kwargs.setdefault("optimizer_prompt_symbol_set", OptimizerPromptSymbolSetJSON()) + kwargs.setdefault("use_json_object_format", True) + __base.__init__(self, *args, **kwargs) + # Ensure the prompt literally mentions JSON (OpenAI requirement) + if hasattr(self, "output_format_prompt") and isinstance(self.output_format_prompt, str): + self.output_format_prompt = "Please answer in JSON.\n" + self.output_format_prompt + JSONSubclass.__init__ = _json_init + optimizers.append(JSONSubclass) + else: + optimizers.append(item) return optimizers ALL_OPTIMIZERS = get_all_optimizers() # You can override for temporarly testing a specific optimizer ALL_OPTIMIZERS = [TextGrad] # [OptoPrimeMulti] ALL_OPTIMIZERS = [OptoPrime] +# LLM models to test (profile_name, model_id) - editable list // currently specific to OpenRouter +def get_all_models(): + """ + Returns list of (profile_name, model_identifier) pairs. + Edit the right-hand model ids if you prefer other model strings + (OpenRouter may use vendor-prefixed names for some models). + """ + return [ + ("basic_llama", "meta-llama/llama-3.2-1b-instruct"), + ("basic_gemma", "google/gemma-3-1b-it"), + ("gpt4o_mini","gpt-4o-mini"), + ("gpt_oss_20b","openai/gpt-oss-20b:free"), # replace if you have a different OSS reasoning id + ("gpt_oss_120b","openai/gpt-oss-120b:free"), # replace if you have a different OSS reasoning id + ("qwen_next_thinking","qwen/qwen3-next-80b-a3b-thinking"), + ("grok_fast", "x-ai/grok-4-fast:free") + ] + +ALL_MODELS = get_all_models() +# ------------------------------------------------------------------ + +# Parametrized fixture that registers the profile at test runtime and +# sets OpenRouter-compatible env vars when OPENROUTER_API_KEY is present. +@pytest.fixture(params=[p for p, _ in ALL_MODELS], ids=[p for p, _ in ALL_MODELS]) +def model_profile(request, monkeypatch): + """ + Yields the profile name (e.g. 'basic_llama'). + If OPENROUTER_API_KEY is present, the fixture: + - sets OPENAI_BASE_URL -> https://openrouter.ai/api/v1 + - copies OPENROUTER_API_KEY -> OPENAI_API_KEY + - registers the profile in LLMFactory (backend CustomLLM) + """ + profile_name = request.param + model_map = dict(ALL_MODELS) + model_id = model_map[profile_name] + + # If the user provided an OpenRouter key, make OpenAI-compatible libs + # point to OpenRouter and register the profile to use CustomLLM. + if os.environ.get("OPENROUTER_API_KEY"): + # Use monkeypatch to avoid leaking env changes across tests + monkeypatch.setenv("OPENAI_BASE_URL", "https://openrouter.ai/api/v1") + monkeypatch.setenv("OPENAI_API_KEY", os.environ["OPENROUTER_API_KEY"]) + + # Register a runtime profile (does not modify source files) + # Use CustomLLM backend which uses OpenAI-compatible calls. + LLMFactory.register_profile(profile_name, backend="CustomLLM", model=model_id) + + return profile_name + # Skip tests if no API credentials are available SKIP_REASON = "No API credentials found" HAS_CREDENTIALS = os.path.exists("OAI_CONFIG_LIST") or os.environ.get("TRACE_LITELLM_MODEL") or os.environ.get("OPENAI_API_KEY") @@ -156,13 +221,52 @@ def my_fun(x): optimizer.zero_feedback() optimizer.backward(output, feedback) - print(f"output={output.data}, feedback={feedback}, variables=") - for p in optimizer.parameters: - print(p.name, p.data) + print(f"BEFORE output={output.data}, feedback={feedback}, variables=") + for p in optimizer.parameters: print(f"{p.name} => {p.data}") optimizer.step(verbose=True) new_func_value = my_fun.parameter.data + print("AFTER variables=") + for p in optimizer.parameters: print(f"{p.name} => {p.data}") + + # The function implementation should be changed + assert str(old_func_value) != str(new_func_value), f"{optimizer_class.__name__} failed to update function" + print(f"Function updated: old value: {str(old_func_value)}, new value: {str(new_func_value)}") + +@pytest.mark.skipif(not os.environ.get("OPENROUTER_API_KEY"), reason="OPENROUTER_API_KEY not set") +def test_optimizer_with_code_on_many_llm_types_using_openrouter(model_profile, optimizer_class): + """ + Test optimizer with a single LLM profile (registered at runtime). + Both model_profile and optimizer_class are explicit parameters so you + can run an individual combination from VSCode Test Lab. + """ + from opto.trace import bundle, node + + @bundle(trainable=True) + def my_fun(x): + return x**2 + 1 + + old_func_value = my_fun.parameter.data + x = node(-1, trainable=False) + + optimizer = optimizer_class([my_fun.parameter], llm_profiles=[model_profile], generation_technique="multi_llm") + + output = my_fun(x) + feedback = user_code(output.data) + + optimizer.zero_feedback() + optimizer.backward(output, feedback) + + print(f"BEFORE output={output.data}, feedback={feedback}, variables=") + for p in optimizer.parameters: print(f"{p.name} => {p.data}") + + optimizer.step(verbose=True) + new_func_value = my_fun.parameter.data + + print("AFTER variables=") + for p in optimizer.parameters: print(f"{p.name} => {p.data}") + # The function implementation should be changed assert str(old_func_value) != str(new_func_value), f"{optimizer_class.__name__} failed to update function" print(f"Function updated: old value: {str(old_func_value)}, new value: {str(new_func_value)}") From daa1a730988d58443927f494051584d259e8faec Mon Sep 17 00:00:00 2001 From: windweller Date: Sun, 21 Sep 2025 21:56:36 -0400 Subject: [PATCH 270/314] push the fix to `model.export()` -- all tests now pass, unit tests uncommented. --- opto/trace/modules.py | 243 +++++++++---------------------- tests/unit_tests/test_modules.py | 118 +++++++-------- 2 files changed, 127 insertions(+), 234 deletions(-) diff --git a/opto/trace/modules.py b/opto/trace/modules.py index 8cfc8085..d27813c4 100644 --- a/opto/trace/modules.py +++ b/opto/trace/modules.py @@ -1,17 +1,17 @@ import os +import ast import pickle import copy import sys import inspect import textwrap from opto.trace.containers import ParameterContainer, trainable_method -from opto.trace.nodes import ParameterNode +from opto.trace.nodes import ParameterNode, Node from opto.trace.projections import Projection, BlackCodeFormatter import functools from typing import List, Optional - def model(cls): """Decorator to transform a class into a Trace-compatible model with parameter collection. @@ -72,8 +72,8 @@ def model(cls): ... return x * self.weight >>> >>> m = MyModel() - >>> # m.parameters() returns all trainable parameters - >>> # m.export('model.py') saves current state as code + >>> m.parameters() returns all trainable parameters + >>> m.export('model.py') saves current state as code """ name = f"{cls.__name__}Model" bases = (cls, Model) @@ -96,10 +96,14 @@ def model(cls): continue for name, member in __TRACE_RESERVED_cls_members: + if 'FunModule' in str(member): + # for these class method members, we need to access their content dynamically + continue __TRACE_RESERVED_cls_name_to_source[name] = inspect.getsource(member) new_class = type(name, bases, {}) new_class.__module__ = cls.__module__ + # for export new_class.reserved_cls_name = __TRACE_RESERVED_cls_name new_class.reserved_cls_members = __TRACE_RESERVED_cls_members @@ -109,147 +113,6 @@ def model(cls): setattr(mod, name, new_class) return new_class -# Old code -# def model(cls): -# """ -# Wrap a class with this decorator. This helps collect parameters for the optimizer. This decorated class cannot be pickled. -# """ - -# class ModelWrapper(cls, Module): - -# def export(self, filename, projections: Optional[List[Projection]] = None): -# """Dump the model's source code to a file, including all methods and attributes. -# Ignores dunder methods unless they were overridden by the user. -# """ -# if projections is None: -# projections = [BlackCodeFormatter()] - -# trace_model_body = f"class {cls.__name__}:\n" - -# # Get all members of the class -# all_members = inspect.getmembers(self) -# cls_members = inspect.getmembers(cls) -# cls_member_names = [m[0] for m in cls_members] - -# # Filter out dunder methods unless they were overridden -# filtered_members = [] -# for name, member in all_members: -# # Skip internal trace reserved members -# if name.startswith('__TRACE_RESERVED_'): -# continue - -# if name not in cls_member_names: -# continue - -# # Include if it's not a dunder method or if it was overridden -# if not name.startswith('__'): -# filtered_members.append((name, member)) -# elif name.startswith('__'): -# # For dunder methods, check if they were overridden -# try: -# print(cls.__name__, "<>", member.__qualname__) -# # MixedClass <> test_export_mixed_trainable..MixedClass.__init__ -# # if we wrap it inside a function, the qualname is different than when we dont -# if hasattr(member, '__qualname__') and cls.__name__ in member.__qualname__: -# filtered_members.append((name, member)) -# except (AttributeError, TypeError): -# # Skip if we can't determine if it was overridden -# continue - -# # Process each member -# for i, (name, member) in enumerate(filtered_members): -# print(name, member) -# if 'FunModule' in str(member): -# # Handle methods -# if member.parameter is not None: -# source = member.parameter.data -# else: -# source = member.info['source'] -# source = textwrap.dedent(source) -# indented = textwrap.indent(source, " ") -# trace_model_body += indented -# else: # this is a class method -# source = inspect.getsource(member) -# source = textwrap.dedent(source) -# indented = textwrap.indent(source, " ") -# trace_model_body += indented - -# if i < len(all_members) - 1: -# trace_model_body += "\n" # only one newline between members - -# # Replace node initializations with their current values -# # WARNING: there might be corner cases that this static analysis does not cover -# import re -# node_pattern = r'self\.(\w+)\s*=\s*node\([^)]*\)' - -# def replace_node(match): -# attr_name = match.group(1) -# if hasattr(self, attr_name): -# attr = getattr(self, attr_name) -# if hasattr(attr, 'data'): -# return f"self.{attr_name} = {attr.data}" -# return match.group(0) # Return original if replacement not possible - -# trace_model_body = re.sub(node_pattern, replace_node, trace_model_body) - -# trace_model_body = functools.reduce(lambda body, proj: proj.project(body), projections, trace_model_body) - -# with open(filename, "w") as f: -# f.write(trace_model_body) - - -# def __deepcopy__(self, memo): -# # regular deepcopy behavior, because we will overwrite __setstate__ and __getstate__ for pickling -# cls = self.__class__ -# result = cls.__new__(cls) -# memo[id(self)] = result -# for k, v in self.__dict__.items(): -# setattr(result, k, copy.deepcopy(v, memo)) -# return result - -# def __getstate__(self): -# parameters_dict = self.parameters_dict() -# non_parameters_dict = {} -# for k, v in self.__dict__.items(): -# if k not in parameters_dict: -# if k.startswith('__TRACE_RESERVED_'): -# # These are reserved for internal use. -# continue -# non_parameters_dict[k] = v -# return dict(parameters_dict=parameters_dict, -# non_parameters_dict=non_parameters_dict) - -# def __setstate__(self, state): -# parameters_dict = state['parameters_dict'] -# non_parameters_dict = state['non_parameters_dict'] -# self._set(parameters_dict) -# # self.__dict__.update(non_parameters_dict) - -# def save(self, file_name: str): -# """Save the parameters of the model to a pickle file.""" -# directory = os.path.dirname(file_name) -# if directory != "": -# os.makedirs(directory, exist_ok=True) -# with open(file_name, "wb") as f: -# pickle.dump(copy.deepcopy(self.__getstate__()), f) - -# def load(self, file_name): -# """Load the parameters of the model from a pickle file.""" -# with open(file_name, "rb") as f: -# loaded_data = pickle.load(f) -# self.__setstate__(loaded_data) - -# # return ModelWrapper -# name = f"{cls.__name__}ModelWrapper" -# ModelWrapper.__name__ = name -# ModelWrapper.__qualname__ = name - -# # register the class in the module namespace for pickle -# ModelWrapper.__module__ = cls.__module__ -# mod = sys.modules[cls.__module__] -# setattr(mod, name, ModelWrapper) -# return ModelWrapper - class Module(ParameterContainer): """Base class for all Trace models and wrapped functions. @@ -406,32 +269,69 @@ def _set(self, new_parameters): class Model(Module): """ Base class for all models. A model is a container of parameters with methods. """ + def _replace_self_assignments_with_node_data(self, source: str) -> str: + """ + Replace any `self. = ...` in `source` with `self. = ` + when `getattr(self, '')` is a Node and has a `.data` attribute. + + If `.data` is a string of Python code, it will be inserted as code. + Otherwise `.data` is inserted via repr(). + """ + + class Rewriter(ast.NodeTransformer): + def __init__(self, outer_self): + self._self = outer_self + + def visit_Assign(self, node: ast.Assign) -> ast.AST: + # Check if any target is `self.` + replace_names = [] + for t in node.targets: + if isinstance(t, ast.Attribute) and isinstance(t.value, ast.Name) and t.value.id == "self": + replace_names.append(t.attr) + + # Nothing to do + if not replace_names: + return self.generic_visit(node) + + # Decide replacement expression ONCE per assignment + new_value = node.value + for name in replace_names: + try: + attr = getattr(self._self, name) + except AttributeError: + continue + if isinstance(attr, Node) and hasattr(attr, "data"): + data = attr.data + # If it's a string, assume it's code; otherwise, literal via repr + if isinstance(data, str): + try: + new_value = ast.parse(data, mode="eval").body + except SyntaxError: + # fall back to literal repr if not valid code + new_value = ast.parse(repr(data), mode="eval").body + else: + new_value = ast.parse(repr(data), mode="eval").body + # Once we have a replacement value, we can stop; it applies to the whole RHS + break + + node.value = new_value + return node + + tree = ast.parse(source) + tree = Rewriter(self).visit(tree) + ast.fix_missing_locations(tree) + # Python 3.9+: ast.unparse available + return ast.unparse(tree) + def export(self, filename, projections: Optional[List[Projection]] = None): if projections is None: projections = [BlackCodeFormatter()] cls = self.__class__ - # trace_model_body = f"class {cls.__name__}:\n" name = cls.reserved_cls_name trace_model_body = f"class {name}:\n" - all_members = inspect.getmembers(self) - cls_members = cls.reserved_cls_members # inspect.getmembers(cls) - cls_member_names = [m[0] for m in cls_members] - filtered_members = [] - for name, member in all_members: - if name.startswith('__TRACE_RESERVED_'): - continue - if name not in cls_member_names: - continue - if not name.startswith('__'): - filtered_members.append((name, member)) - elif name.startswith('__'): - try: - if hasattr(member, '__qualname__') and cls.__name__ in member.__qualname__: - filtered_members.append((name, member)) - except (AttributeError, TypeError): - continue + cls_members = cls.reserved_cls_members - for i, (name, member) in enumerate(filtered_members): + for i, (name, member) in enumerate(cls_members): if 'FunModule' in str(member): if member.parameter is not None: source = member.parameter.data @@ -445,18 +345,11 @@ def export(self, filename, projections: Optional[List[Projection]] = None): source = textwrap.dedent(source) indented = textwrap.indent(source, " ") trace_model_body += indented - if i < len(all_members) - 1: + if i < len(cls_members) - 1: trace_model_body += "\n" - import re - node_pattern = r'self\.(\w+)\s*=\s*node\([^)]*\)' - def replace_node(match): - attr_name = match.group(1) - if hasattr(self, attr_name): - attr = getattr(self, attr_name) - if hasattr(attr, 'data'): - return f"self.{attr_name} = {attr.data}" - return match.group(0) - trace_model_body = re.sub(node_pattern, replace_node, trace_model_body) + + trace_model_body = self._replace_self_assignments_with_node_data(trace_model_body) + trace_model_body = functools.reduce(lambda body, proj: proj.project(body), projections, trace_model_body) with open(filename, "w") as f: f.write(trace_model_body) diff --git a/tests/unit_tests/test_modules.py b/tests/unit_tests/test_modules.py index c3cd4ff7..33d589c8 100644 --- a/tests/unit_tests/test_modules.py +++ b/tests/unit_tests/test_modules.py @@ -189,65 +189,65 @@ def complex_method(self, x): def __str__(self): return "ComplexClass" -# def test_export_basic(): -# dummy = DummyClass() -# dummy._param._data = 42 # Change the node value -# temp_file = "temp_dummy.py" -# try: -# dummy.export(temp_file) -# with open(temp_file, "r") as f: -# content = f.read() -# # Check if class definition is present -# assert "class DummyClass:" in content -# # Check if regular method is present -# assert "def regular_method" in content -# # Check if __str__ is present (overridden dunder) -# assert "def __str__" in content -# # Check if __custom__ is present (custom dunder) -# assert "def __custom__" in content -# # Check if regular attribute is present -# assert "regular_attr" in content -# # Check if node initialization was replaced with current value -# assert "self._param = 42" in content -# assert "self._param = node(1" not in content -# finally: -# if os.path.exists(temp_file): -# os.remove(temp_file) - -# def test_export_complex(): -# complex_obj = ComplexClass() -# temp_file = "temp_complex.py" -# try: -# complex_obj.export(temp_file) -# with open(temp_file, "r") as f: -# content = f.read() -# # Check if class definition is present -# assert "class ComplexClass:" in content -# # Check if complex method is present -# assert "def complex_method" in content -# # Check if __str__ is present -# assert "def __str__" in content -# # Check if nested class reference is in the method -# assert "self._nested.regular_method" in content -# finally: -# if os.path.exists(temp_file): -# os.remove(temp_file) - -# def test_export_with_projection(): -# dummy = DummyClass() -# temp_file = "temp_dummy_formatted.py" -# try: -# # Test with BlackCodeFormatter -# from opto.trace.projections import BlackCodeFormatter -# dummy.export(temp_file, projections=[BlackCodeFormatter()]) -# with open(temp_file, "r") as f: -# content = f.read() -# # Check if content is properly formatted -# assert "class DummyClass:" in content -# assert "def regular_method" in content -# finally: -# if os.path.exists(temp_file): -# os.remove(temp_file) +def test_export_basic(): + dummy = DummyClass() + dummy._param._data = 42 # Change the node value + temp_file = "temp_dummy.py" + try: + dummy.export(temp_file) + with open(temp_file, "r") as f: + content = f.read() + # Check if class definition is present + assert "class DummyClass:" in content + # Check if regular method is present + assert "def regular_method" in content + # Check if __str__ is present (overridden dunder) + assert "def __str__" in content + # Check if __custom__ is present (custom dunder) + assert "def __custom__" in content + # Check if regular attribute is present + assert "regular_attr" in content + # Check if node initialization was replaced with current value + assert "self._param = 42" in content + assert "self._param = node(1" not in content + finally: + if os.path.exists(temp_file): + os.remove(temp_file) + +def test_export_complex(): + complex_obj = ComplexClass() + temp_file = "temp_complex.py" + try: + complex_obj.export(temp_file) + with open(temp_file, "r") as f: + content = f.read() + # Check if class definition is present + assert "class ComplexClass:" in content + # Check if complex method is present + assert "def complex_method" in content + # Check if __str__ is present + assert "def __str__" in content + # Check if nested class reference is in the method + assert "self._nested.regular_method" in content + finally: + if os.path.exists(temp_file): + os.remove(temp_file) + +def test_export_with_projection(): + dummy = DummyClass() + temp_file = "temp_dummy_formatted.py" + try: + # Test with BlackCodeFormatter + from opto.trace.projections import BlackCodeFormatter + dummy.export(temp_file, projections=[BlackCodeFormatter()]) + with open(temp_file, "r") as f: + content = f.read() + # Check if content is properly formatted + assert "class DummyClass:" in content + assert "def regular_method" in content + finally: + if os.path.exists(temp_file): + os.remove(temp_file) @model class NonTrainableClass: From d2d7ef947fa1162c70aa6a01875586de5cc0e2ff Mon Sep 17 00:00:00 2001 From: windweller Date: Sun, 21 Sep 2025 22:17:55 -0400 Subject: [PATCH 271/314] add a test (will remove later) --- tests/features_tests/test_flows_compose.py | 457 +++++++++++++++++++++ 1 file changed, 457 insertions(+) create mode 100644 tests/features_tests/test_flows_compose.py diff --git a/tests/features_tests/test_flows_compose.py b/tests/features_tests/test_flows_compose.py new file mode 100644 index 00000000..9d2efdcc --- /dev/null +++ b/tests/features_tests/test_flows_compose.py @@ -0,0 +1,457 @@ +import pytest +import os +from unittest.mock import patch, MagicMock, Mock +from opto.flows.compose import TracedLLM, TracedResponse +from opto.flows.types import TracedInput, TracedOutput + + +# Mock LLM at module level to ensure no real API calls +@pytest.fixture(autouse=True) +def mock_llm_globally(): + """Automatically mock all LLM calls for all tests.""" + with patch('opto.utils.llm.LLM') as mock_llm_class: + # Create a mock LLM instance that doesn't require API keys + mock_llm_instance = Mock() + mock_llm_instance.return_value = Mock() + mock_llm_class.return_value = mock_llm_instance + yield mock_llm_instance + + +@pytest.fixture(autouse=True) +def mock_trace_operators(): + """Mock trace operators to prevent any external dependencies.""" + with patch('opto.trace.operators.call_llm') as mock_call_llm: + mock_call_llm.return_value = "Mocked LLM response" + yield mock_call_llm + + +class TestTracedLLM: + """Test cases for TracedLLM functionality.""" + + def test_basic_initialization(self): + """Test basic TracedLLM initialization.""" + llm = TracedLLM("You are a helpful assistant") + assert llm.system_prompt.data == "You are a helpful assistant" + assert llm._input_fields == [] + assert llm._output_fields == [] + assert llm._field_types == {} + + def test_docstring_as_system_prompt(self): + """Test that class docstring is used as system prompt when none provided.""" + class TestLLM(TracedLLM): + """This is a test LLM for testing purposes""" + pass + + llm = TestLLM() + assert llm.system_prompt.data == "This is a test LLM for testing purposes" + + def test_explicit_system_prompt_overrides_docstring(self): + """Test that explicit system prompt overrides docstring.""" + class TestLLM(TracedLLM): + """This is a test LLM""" + pass + + llm = TestLLM("Custom prompt") + assert llm.system_prompt.data == "Custom prompt" + + def test_field_detection_basic(self): + """Test basic field detection for input and output fields.""" + class BasicScorer(TracedLLM): + """Basic document scorer""" + doc: str = TracedInput(description="Document to score") + score: int = TracedOutput(description="Score from 1-10") + + scorer = BasicScorer() + assert scorer._input_fields == ['doc'] + assert scorer._output_fields == ['score'] + assert scorer._field_types == {'doc': str, 'score': int} + + def test_field_detection_multiple_fields(self): + """Test field detection with multiple input/output fields.""" + class MultiFieldScorer(TracedLLM): + """Multi-field scorer""" + doc: str = TracedInput(description="Document") + context: str = TracedInput(description="Context") + score: int = TracedOutput(description="Score") + confidence: float = TracedOutput(description="Confidence") + tags: list = TracedOutput(description="Tags") + + scorer = MultiFieldScorer() + assert set(scorer._input_fields) == {'doc', 'context'} + assert set(scorer._output_fields) == {'score', 'confidence', 'tags'} + assert scorer._field_types['doc'] == str + assert scorer._field_types['score'] == int + assert scorer._field_types['confidence'] == float + assert scorer._field_types['tags'] == list + + def test_direct_pattern_call(self, mock_trace_operators): + """Test direct usage pattern (no inheritance fields).""" + mock_trace_operators.return_value = "Hello! The weather is sunny." + + llm = TracedLLM("You are a helpful assistant") + response = llm("Hello, what's the weather today?") + + assert response == "Hello! The weather is sunny." + mock_trace_operators.assert_called_once() + + def test_inheritance_pattern_call(self, mock_trace_operators): + """Test inheritance pattern with structured input/output.""" + mock_trace_operators.return_value = "The score is 8 out of 10" + + class Scorer(TracedLLM): + """Score documents""" + doc: str = TracedInput(description="Document to score") + score: int = TracedOutput( + description="Score from 1-10", + parser=r"score[:\s]*is[:\s]*(\d+)|(\d+)\s*out\s*of" + ) + + scorer = Scorer() + response = scorer(doc="This is a great document") + + assert isinstance(response, TracedResponse) + assert response.score == 8 + mock_trace_operators.assert_called_once() + + def test_dynamic_response_model_creation(self): + """Test dynamic Pydantic model creation.""" + class TestScorer(TracedLLM): + """Test scorer""" + doc: str = TracedInput(description="Document") + score: int = TracedOutput(description="Score") + confidence: float = TracedOutput(description="Confidence") + + scorer = TestScorer() + ResponseModel = scorer._create_dynamic_response_model() + + assert ResponseModel.__name__ == "TestScorerResponse" + assert 'score' in ResponseModel.model_fields + assert 'confidence' in ResponseModel.model_fields + assert ResponseModel.model_fields['score'].annotation == int + assert ResponseModel.model_fields['confidence'].annotation == float + + def test_json_extraction(self): + """Test JSON response extraction.""" + class Scorer(TracedLLM): + """Test scorer""" + doc: str = TracedInput() + score: int = TracedOutput() + + scorer = Scorer() + json_response = '{"score": 9}' + extracted = scorer._extract_structured_data(json_response) + + assert extracted == {'score': 9} + + def test_text_extraction_with_patterns(self): + """Test text extraction using field name patterns.""" + class Scorer(TracedLLM): + """Test scorer""" + doc: str = TracedInput() + score: int = TracedOutput(parser=r"score[:\s]*is[:\s]*(\d+)|(\d+)\s*out\s*of") + + scorer = Scorer() + text_response = "The score is 7 out of 10" + extracted = scorer._extract_structured_data(text_response) + + assert extracted == {'score': 7} + + +class TestTracedInput: + """Test cases for TracedInput.""" + + def test_basic_initialization(self): + """Test basic TracedInput initialization.""" + input_field = TracedInput(description="Test input") + assert input_field.description == "Test input" + assert input_field.required == True + + def test_optional_field(self): + """Test optional TracedInput field.""" + input_field = TracedInput(description="Optional input", required=False) + assert input_field.required == False + + +class TestTracedOutput: + """Test cases for TracedOutput.""" + + def test_basic_initialization(self): + """Test basic TracedOutput initialization.""" + output_field = TracedOutput(description="Test output") + assert output_field.description == "Test output" + assert output_field.required == True + assert output_field.parser is None + assert output_field.default_value is None + + def test_with_default_value(self): + """Test TracedOutput with default value.""" + output_field = TracedOutput(description="Score", default_value=5) + assert output_field.default_value == 5 + + def test_regex_parser_extraction(self): + """Test extraction using regex parser.""" + output_field = TracedOutput( + description="Rating", + parser=r"(\d+)/5|rating[:\s]+(\d+)", + default_value=0 + ) + + # Test successful extraction + result = output_field.extract_from_text("The rating is 4/5 stars", int) + assert result == 4 + + # Test fallback to default + result = output_field.extract_from_text("No rating information", int) + assert result == 0 + + def test_function_parser_extraction(self): + """Test extraction using function parser.""" + def sentiment_parser(text): + if "good" in text.lower(): + return "Positive" + elif "bad" in text.lower(): + return "Negative" + else: + return "Neutral" + + output_field = TracedOutput( + description="Sentiment", + parser=sentiment_parser, + default_value="Unknown" + ) + + # Test successful extraction + result = output_field.extract_from_text("This is a good product", str) + assert result == "Positive" + + result = output_field.extract_from_text("This is a bad product", str) + assert result == "Negative" + + # Test parser exception (should return default) + def failing_parser(text): + raise Exception("Parser error") + + output_field_with_failing_parser = TracedOutput( + description="Sentiment", + parser=failing_parser, + default_value="Unknown" + ) + result = output_field_with_failing_parser.extract_from_text("Some text", str) + assert result == "Unknown" + + def test_boolean_parsing(self): + """Test boolean value parsing.""" + output_field = TracedOutput(default_value=False) + + # Test positive cases + assert output_field._parse_boolean("true") == True + assert output_field._parse_boolean("yes") == True + assert output_field._parse_boolean("positive") == True + assert output_field._parse_boolean("definitely") == True + + # Test negative cases + assert output_field._parse_boolean("false") == False + assert output_field._parse_boolean("no") == False + assert output_field._parse_boolean("negative") == False + assert output_field._parse_boolean("no way") == False + + # Test default case + assert output_field._parse_boolean("unclear") == False + + def test_type_conversion(self): + """Test automatic type conversion.""" + output_field = TracedOutput(default_value=0) + + # Test int conversion + assert output_field._convert_to_type("42", int) == 42 + assert output_field._convert_to_type("Score: 8", int) == 8 + assert output_field._convert_to_type("No numbers", int) == 0 # default + + # Test float conversion + assert output_field._convert_to_type("3.14", float) == 3.14 + assert output_field._convert_to_type("Rating: 4.5", float) == 4.5 + + # Test list conversion + assert output_field._convert_to_type('["a", "b", "c"]', list) == ["a", "b", "c"] + assert output_field._convert_to_type("a, b, c", list) == ["a", "b", "c"] + + +class TestDynamicModelMixin: + """Test cases for DynamicModelMixin.""" + + def test_create_response_model(self): + """Test dynamic response model creation.""" + from opto.flows.types import DynamicModelMixin + + class TestClass(DynamicModelMixin): + pass + + field_defs = { + 'score': (int, TracedOutput(description="Score value", default_value=0)), + 'tags': (list, TracedOutput(description="Tag list", required=False, default_value=[])) + } + + ResponseModel = TestClass.create_response_model(field_defs) + + assert ResponseModel.__name__ == "TestClassResponse" + assert 'score' in ResponseModel.model_fields + assert 'tags' in ResponseModel.model_fields + assert ResponseModel.model_fields['score'].annotation == int + assert ResponseModel.model_fields['tags'].annotation == list + + def test_create_input_model(self): + """Test dynamic input model creation.""" + from opto.flows.types import DynamicModelMixin + + class TestClass(DynamicModelMixin): + pass + + field_defs = { + 'doc': (str, TracedInput(description="Document", required=True)), + 'context': (str, TracedInput(description="Context", required=False)) + } + + InputModel = TestClass.create_input_model(field_defs) + + assert InputModel.__name__ == "TestClassInput" + assert 'doc' in InputModel.model_fields + assert 'context' in InputModel.model_fields + + +class TestTracedResponse: + """Test cases for TracedResponse.""" + + def test_dynamic_attribute_setting(self): + """Test that TracedResponse allows dynamic attribute setting.""" + response = TracedResponse(score=8, confidence=0.85, tags=["good", "clear"]) + + assert response.score == 8 + assert response.confidence == 0.85 + assert response.tags == ["good", "clear"] + + +class TestIntegration: + """Integration tests for the complete flows system.""" + + def test_end_to_end_workflow(self, mock_trace_operators): + """Test complete end-to-end workflow.""" + mock_trace_operators.return_value = "Score: 9, Sentiment: Positive, Confidence: 90%" + + class DocumentAnalyzer(TracedLLM): + """Analyze documents comprehensively""" + document: str = TracedInput(description="Document to analyze") + score: int = TracedOutput( + description="Quality score 1-10", + parser=r"score[:\s]+(\d+)", + default_value=5 + ) + sentiment: str = TracedOutput( + description="Sentiment analysis", + parser=lambda text: "Positive" if "positive" in text.lower() else "Negative", + default_value="Neutral" + ) + confidence: float = TracedOutput( + description="Confidence percentage", + parser=r"confidence[:\s]+(\d+)%?", + default_value=0.5 + ) + + analyzer = DocumentAnalyzer() + + # Test field detection + assert set(analyzer._input_fields) == {'document'} + assert set(analyzer._output_fields) == {'score', 'sentiment', 'confidence'} + + # Test analysis + response = analyzer(document="This is a test document") + + assert isinstance(response, TracedResponse) + assert response.score == 9 + assert response.sentiment == "Positive" + assert response.confidence == 90.0 + + # Verify LLM was called correctly + mock_trace_operators.assert_called_once() + args, kwargs = mock_trace_operators.call_args + assert "This is a test document" in args + + +class TestCICompatibility: + """Tests specifically designed for CI/CD environments without API keys.""" + + def test_no_real_api_calls_made(self): + """Ensure no real API calls are made during testing.""" + # This test verifies that our mocking is working correctly + class SimpleScorer(TracedLLM): + """Simple scorer""" + text: str = TracedInput(description="Text input") + score: int = TracedOutput(description="Score output", default_value=5) + + scorer = SimpleScorer() + + # This should not fail even without API keys because everything is mocked + assert scorer.system_prompt.data == "Simple scorer" + assert scorer._input_fields == ['text'] + assert scorer._output_fields == ['score'] + + def test_offline_functionality(self): + """Test functionality that doesn't require any external services.""" + # Test type extraction + output_field = TracedOutput(parser=r"score[:\s]*is[:\s]*(\d+)", default_value=0) + result = output_field.extract_from_text("The score is 85", int) + assert result == 85 + + # Test boolean parsing + bool_field = TracedOutput(default_value=False) + assert bool_field._parse_boolean("yes") == True + assert bool_field._parse_boolean("no") == False + + # Test type conversion + assert output_field._convert_to_type("42", int) == 42 + assert output_field._convert_to_type("3.14", float) == 3.14 + + def test_mock_verification(self, mock_trace_operators): + """Verify that mocking is working as expected.""" + # Check that the mock is active + assert mock_trace_operators is not None + + # Create a TracedLLM instance + llm = TracedLLM("Test prompt") + + # This should use the mock, not real API + mock_trace_operators.return_value = "Mocked response" + response = llm("Test input") + + assert response == "Mocked response" + mock_trace_operators.assert_called_once() + + @pytest.mark.skipif( + os.getenv('GITHUB_ACTIONS') == 'true' and not os.getenv('OPENAI_API_KEY'), + reason="Skipping in GitHub Actions without API key" + ) + def test_optional_real_api_integration(self): + """Optional test that can be skipped in CI without API keys.""" + # This test is automatically skipped in GitHub Actions if no API key is set + # It can be useful for local testing with real APIs + pytest.skip("Real API integration test - skipped for CI safety") + + def test_boolean_parsing_delegates_to_traced_output(self, mock_trace_operators): + """Test that boolean parsing properly delegates to TracedOutput when available.""" + mock_trace_operators.return_value = "answer: yes" # More structured format + + class BooleanTester(TracedLLM): + """Test boolean delegation""" + question: str = TracedInput(description="Question to ask") + answer: bool = TracedOutput( + description="Boolean answer", + parser=r"answer[:\s]*([^\n,]+)", # Add explicit parser to extract "yes" + default_value=False # This should be used by TracedOutput._parse_boolean + ) + + tester = BooleanTester() + response = tester(question="Is this working?") + + # The TracedOutput._parse_boolean should handle the parsing with its default_value logic + assert isinstance(response, TracedResponse) + # Since "yes" is in positive_words, it should return True regardless of default_value + assert response.answer == True From 05ddf54a1745435ece886e04c0a59f1c7edecd5b Mon Sep 17 00:00:00 2001 From: doxav Date: Mon, 22 Sep 2025 07:22:22 +0200 Subject: [PATCH 272/314] just ensured not to prepend prompt to answer in JSON if JSON is early in prompt format + fail safe self ignore_extraction_error attribute in extract_llm_suggestion --- opto/optimizers/optoprime.py | 8 ++++---- tests/llm_optimizers_tests/test_optimizer.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/opto/optimizers/optoprime.py b/opto/optimizers/optoprime.py index cdbb9f23..52816ff9 100644 --- a/opto/optimizers/optoprime.py +++ b/opto/optimizers/optoprime.py @@ -603,10 +603,11 @@ def _find_key(node_name: str, sugg: Dict[str, Any]) -> Optional[str]: raise e return update_dict - def extract_llm_suggestion(self, response: str, suggestion_tag=None, reasoning_tag=None, return_only_suggestion=True) -> Dict[str, Any]: + def extract_llm_suggestion(self, response: str, suggestion_tag=None, reasoning_tag=None, return_only_suggestion=True, ignore_extraction_error=None) -> Dict[str, Any]: """Extract the suggestion from the response.""" suggestion_tag = suggestion_tag or self.default_json_keys.get("suggestion", "suggestion") reasoning_tag = reasoning_tag or self.default_json_keys.get("reasoning", "reasoning") + ignore_extraction_error = ignore_extraction_error or getattr(self, "ignore_extraction_error", False) if "```" in response: match = re.findall(r"```(.*?)```", response, re.DOTALL) @@ -646,9 +647,8 @@ def extract_llm_suggestion(self, response: str, suggestion_tag=None, reasoning_t for key, value in pairs: suggestion[key] = value - if len(suggestion) == 0 and not self.ignore_extraction_error: - print(f"Cannot extract {self.default_json_keys['suggestion']} from LLM's response:") - print(response) + if len(suggestion) == 0 and not ignore_extraction_error: + print(f"Cannot extract {suggestion_tag} from LLM's response:\n{response}") keys_to_remove = [] for key, value in suggestion.items(): diff --git a/tests/llm_optimizers_tests/test_optimizer.py b/tests/llm_optimizers_tests/test_optimizer.py index b7d7f541..aa278d8e 100644 --- a/tests/llm_optimizers_tests/test_optimizer.py +++ b/tests/llm_optimizers_tests/test_optimizer.py @@ -29,8 +29,8 @@ def _json_init(self, *args, __base=item, **kwargs): kwargs.setdefault("use_json_object_format", True) __base.__init__(self, *args, **kwargs) # Ensure the prompt literally mentions JSON (OpenAI requirement) - if hasattr(self, "output_format_prompt") and isinstance(self.output_format_prompt, str): - self.output_format_prompt = "Please answer in JSON.\n" + self.output_format_prompt + if hasattr(self, "output_format_prompt") and isinstance(self.output_format_prompt, str) and "JSON" not in self.output_format_prompt.upper(): + self.output_format_prompt = "Please answer in JSON format.\n" + self.output_format_prompt JSONSubclass.__init__ = _json_init optimizers.append(JSONSubclass) else: From 81f41e95347c206257f634778c490987da716a46 Mon Sep 17 00:00:00 2001 From: windweller Date: Mon, 22 Sep 2025 00:22:23 -0500 Subject: [PATCH 273/314] remove parts --- opto/flows/compose.py | 228 ++++++------------------------------------ 1 file changed, 32 insertions(+), 196 deletions(-) diff --git a/opto/flows/compose.py b/opto/flows/compose.py index 2e4e23fe..a5476e06 100644 --- a/opto/flows/compose.py +++ b/opto/flows/compose.py @@ -1,7 +1,7 @@ import pydantic from pydantic import BaseModel, ValidationError, Field, create_model import opto.trace as trace -from typing import Union, get_type_hints, Any, Dict +from typing import Union, get_type_hints, Any, Dict, List from opto.utils.llm import AbstractModel, LLM from opto.flows.types import TracedInput, TracedOutput, DynamicModelMixin from opto.optimizers.utils import extract_xml_like_data @@ -15,205 +15,46 @@ 1. special operations that supports specifying inputs (system_prompt, user_prompt) to LLM and parsing of outputs, wrap everything under one command. 2. Easy to use interface -- can be inherited by users. +3. Support multi-turn chatting (message history) Usage patterns: -1. Direct use: (only supports single input, single output) (signature: str -> str) +Direct use: (only supports single input, single output) (signature: str -> str) llm = TracedLLM("You are a helpful assistant.") response = llm("Hello, what's the weather in France today?") - -2. Inheritance: -class Scorer(TracedLLM): - "This is a class that scores the response from LLM" - doc: str = TracedInput(description="The document to score") - score: int = TracedOutput(description="The score of the document") - -scorer = Scorer() # if a system prompt is passed in here, it will override the docstring. -response = scorer(doc="The response is ...") -print(response.score) - -When using the inheritance mode, the system prompt augmented to be as follow: - -------------- -You are a helpful assistant generates output based on the instructions and inputs below. - -## Inputs - -### input_name - - -value - -## Instructions -{original system prompt docstring} - -## Outputs -output_name1 [type=str]: description \n -output_name2 [type=List[int]]: description - -## Output Format -Your output should be in the following XML/HTML format: - - -value - """ - -class TracedResponse: - """Dynamic response object that holds output field values.""" - def __init__(self, **kwargs): - for key, value in kwargs.items(): - setattr(self, key, value) - -class StructuredInputOutputMixin: - """Mixin providing structured input/output parsing capabilities for TracedLLM.""" - - def _detect_fields(self): - """Detect TracedInput and TracedOutput fields from class annotations and defaults.""" - # Get type hints to extract the actual types - type_hints = get_type_hints(self.__class__) - - # Look at class attributes and their default values - for attr_name in dir(self.__class__): - if not attr_name.startswith('_'): # Skip private attributes - attr_value = getattr(self.__class__, attr_name, None) - if isinstance(attr_value, TracedInput): - self._input_fields.append(attr_name) - # Store the type annotation for this field - self._field_types[attr_name] = type_hints.get(attr_name, str) - elif isinstance(attr_value, TracedOutput): - self._output_fields.append(attr_name) - # Store the type annotation for this field - self._field_types[attr_name] = type_hints.get(attr_name, str) - - def _create_dynamic_response_model(self) -> type[BaseModel]: - """ - Create a dynamic Pydantic model for parsing LLM responses. We avoid creating an explicit signature by creating a dynamic model instead. - The only disadvantage is nested-type parsing can be slightly more difficult, but that level of flexibility + nested LLM usage is rare and not a primary - use case for Trace. - """ - # Create field definitions for create_model - field_definitions = {} - - for field_name in self._output_fields: - field_type = self._field_types.get(field_name, str) - # Get the description from the TracedOutput instance - traced_output = getattr(self.__class__, field_name, None) - description = getattr(traced_output, 'description', None) if traced_output else None - - # Create field definition tuple: (type, Field(...)) - field_definitions[field_name] = (field_type, Field(description=description)) - - # Use Pydantic's create_model for dynamic model creation - ResponseModel = create_model( - f"{self.__class__.__name__}Response", - **field_definitions - ) - - return ResponseModel - - # TODO: rewrite this part - # TODO: 1. append at the end of the system prompt about generation instructions. XML based format with Markdown. - # TODO: 2. extract by XML, put into a JSON string (allow nested XML parsing, such that the fields/response model can actually be nested) - # TODO: 3. use the dynamic ResponseModel to do the parsing - def _extract_structured_data(self, llm_response: str) -> Dict[str, Any]: - """Extract structured data from LLM response - delegates to TracedOutput instances.""" - # Try to parse as JSON if it looks like JSON - llm_response_stripped = llm_response.strip() - if llm_response_stripped.startswith('{') and llm_response_stripped.endswith('}'): - # TODO: implement pydantic parsing instead - try: - json_data = json.loads(llm_response_stripped) - # Validate that all fields are expected - validated_data = {} - for field_name, value in json_data.items(): - if field_name in self._output_fields: - validated_data[field_name] = value - else: - print(f"Warning: Unexpected field '{field_name}' in JSON response, ignoring") - return validated_data - except json.JSONDecodeError: - pass - - # Then treat it like XML, re-format it into JSON, and use Pydantic to parse - # TODO: implement that - extracted_data = {} - - for field_name in self._output_fields: - # Get the TracedOutput class variable - traced_output = getattr(self.__class__, field_name, None) - - if traced_output and isinstance(traced_output, TracedOutput): - # Delegate parsing to the TracedOutput instance - field_type = self._field_types.get(field_name, str) - try: - value = traced_output.extract_from_text(llm_response, field_type) - if value is not None: - extracted_data[field_name] = value - except Exception as e: - print(f"Warning: Failed to extract field '{field_name}': {e}") - else: - print(f"Warning: Field '{field_name}' not properly defined as TracedOutput, ignoring") - - return extracted_data - - def _process_structured_inputs(self, **kwargs) -> TracedResponse: - """Process structured inputs and return structured output with Pydantic parsing.""" - # Validate that all required input fields are provided - missing_fields = [field for field in self._input_fields if field not in kwargs] - if missing_fields: - raise ValueError(f"Missing required input field(s): {missing_fields}") - - # For now, use the first input field value as the user prompt - # This will be expanded later with proper parsing/formatting - user_prompt = kwargs[self._input_fields[0]] - llm_response = self._call_llm(user_prompt) - - # Extract structured data from LLM response - extracted_data = self._extract_structured_data(llm_response) - - # Create dynamic Pydantic model for validation - ResponseModel = self._create_dynamic_response_model() - - try: - # Use Pydantic to validate and parse the extracted data - validated_response = ResponseModel(**extracted_data) - - # Convert to TracedResponse - response_data = validated_response.model_dump() - - except ValidationError as e: - # If Pydantic validation fails, include error info - response_data = {} - for output_field in self._output_fields: - # Try to get individual field values, fall back to raw response - response_data[output_field] = extracted_data.get(output_field, llm_response) - - response_data['_validation_errors'] = [str(error) for error in e.errors()] - response_data['_raw_response'] = llm_response - - except Exception as e: - # If extraction fails completely, return raw response - response_data = {} - for output_field in self._output_fields: - response_data[output_field] = llm_response - response_data['_extraction_error'] = str(e) - response_data['_raw_response'] = llm_response - - return TracedResponse(**response_data) - +@trace.bundle(catch_execution_error=False) +def call_llm(llm, system_prompt: str, *user_prompts: List[str], **kwargs) -> str: + """Call the LLM model. + + Args: + llm: The language model to use for generating responses. + system_prompt: the system prompt to the agent. By tuning this prompt, we can control the behavior of the agent. For example, it can be used to provide instructions to the agent (such as how to reason about the problem, how to use tools, how to answer the question), or provide in-context examples of how to solve the problem. + user_prompt: the input to the agent. It can be a query, a task, a code, etc. + Returns: + The response from the agent. + """ + messages = [] + if system_prompt is not None: + messages.append({"role": "system", "content": system_prompt}) + for user_prompt in user_prompts: + messages.append({"role": "user", "content": user_prompt}) + # TODO auto-parsing results + response = llm(messages=messages, **kwargs) + return response.choices[0].message.content @trace.model -class TracedLLM(StructuredInputOutputMixin, DynamicModelMixin): +class TracedLLM(DynamicModelMixin): def __init__(self, system_prompt: Union[str, None, trace.Node] = None, - llm: AbstractModel = None): + llm: AbstractModel = None, chat_history_on=False): """Initialize TracedLLM with a system prompt. Args: system_prompt: The system prompt to use for LLM calls. If None and the class has a docstring, the docstring will be used. llm: The LLM model to use for inference + chat_history_on: if on, maintain chat history for multi-turn conversations """ # Use class docstring as system prompt if none provided if system_prompt is None: @@ -231,9 +72,8 @@ def __init__(self, self._input_fields = [] self._output_fields = [] self._field_types = {} # Store type annotations for each field - self._detect_fields() - - def forward(self, *args, **kwargs) -> Union[str, TracedResponse]: + + def forward(self, *args, **kwargs) -> str: """Main function that handles both direct call and inheritance patterns. Args: @@ -244,16 +84,12 @@ def forward(self, *args, **kwargs) -> Union[str, TracedResponse]: str: For direct pattern TracedResponse: For inheritance pattern with structured output fields """ - if self._input_fields: - # Inheritance pattern: use named arguments - return self._process_structured_inputs(**kwargs) + # Direct pattern: single string argument + if len(args) == 1 and isinstance(args[0], str): + return self._call_llm(args[0]) else: - # Direct pattern: single string argument - if len(args) == 1 and isinstance(args[0], str): - return self._call_llm(args[0]) - else: - raise ValueError("Direct usage requires a single string argument") + raise ValueError("Direct usage requires a single string argument") def _call_llm(self, user_prompt: str) -> str: """Call the LLM with user prompt and system prompt.""" - return trace.operators.call_llm(self.llm, self.system_prompt, user_prompt) + return call_llm(self.llm, self.system_prompt, user_prompt) From 42f32ff3ca818bbe15b6d46d14c8d7653ebfdb56 Mon Sep 17 00:00:00 2001 From: doxav Date: Mon, 22 Sep 2025 08:13:27 +0200 Subject: [PATCH 274/314] initial working version of GEPA bench test --- opto/trainer/algorithms/gepa_algorithms.py | 652 ++++++++++++++++++ .../test_gepa_benchmark.py | 94 +++ 2 files changed, 746 insertions(+) create mode 100644 opto/trainer/algorithms/gepa_algorithms.py create mode 100644 tests/llm_optimizers_tests/test_gepa_benchmark.py diff --git a/opto/trainer/algorithms/gepa_algorithms.py b/opto/trainer/algorithms/gepa_algorithms.py new file mode 100644 index 00000000..588cdbad --- /dev/null +++ b/opto/trainer/algorithms/gepa_algorithms.py @@ -0,0 +1,652 @@ +# opto/trainer/algorithms/gepa_algorithms.py +# GEPA (+Merge) algorithms for Trace +# - GEPAUCBSearch: subclass of UCBSearchAlgorithm +# - GEPABeamPareto: subclass of BeamsearchAlgorithm (Pareto select + single-parent incremental) +# - GEPATrainer: subclass of Trainer (minimal GEPA loop) +# +# All default to OptoPrimeV2 if optimizer=None. + +from __future__ import annotations +import copy +import math +import random +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional, Tuple + +import numpy as np + +from opto.optimizers.optoprime_v2 import OptoPrimeV2 +from opto.trace.nodes import ParameterNode +from opto.trainer.algorithms.UCBsearch import UCBSearchAlgorithm +from opto.trainer.algorithms.beamsearch_algorithm import BeamsearchAlgorithm +from opto.trainer.algorithms.algorithm import Trainer +from opto.trainer.algorithms.basic_algorithms import ( + evaluate, + batchify, + standard_optimization_step, +) +from opto.trainer.utils import async_run +from opto.optimizers.utils import print_color + + +# ----------------------------- Utilities ----------------------------- # + +@dataclass +class Candidate: + params: Dict[ParameterNode, Any] + eval_vector: List[float] # per-instance scores on fixed Pareto subset + mean: float + id: int + parent_ids: Tuple[int, ...] = field(default_factory=tuple) + ancestors: set = field(default_factory=set) + created_iter: int = 0 + wins: int = 0 # updated by Pareto accounting + meta: Dict[str, Any] = field(default_factory=dict) # freeform + +def _eval_on_subset(agent, guide, xs, infos, *, num_threads: Optional[int], desc: str) -> List[float]: + return evaluate(agent, guide, xs, infos, min_score=None, num_threads=num_threads, description=desc) + +def _compute_pareto_counts(cands: List[Candidate]) -> None: + """ + "Best-for-at-least-one-instance" winners. + For each position m in eval vectors, find argmax candidate and credit a win. + """ + if not cands: + return + L = len(cands[0].eval_vector) + # Reset + for c in cands: + c.wins = 0 + # Credit wins + for m in range(L): + best_idx = None + best_val = -float("inf") + for i, c in enumerate(cands): + v = c.eval_vector[m] if m < len(c.eval_vector) else -float("inf") + if v > best_val: + best_val, best_idx = v, i + if best_idx is not None: + cands[best_idx].wins += 1 + +def _pareto_sample(cands: List[Candidate], *, temperature: float = 1.0, rng: random.Random) -> Candidate: + """ + Sample a parent from union of per-instance winners, proportional to wins^1/T. + """ + if not cands: + raise ValueError("Empty candidate buffer.") + _compute_pareto_counts(cands) + wins = np.array([max(1, c.wins) for c in cands], dtype=float) # avoid zero + if temperature <= 0: + # Deterministic pick + return cands[int(wins.argmax())] + weights = wins ** (1.0 / max(1e-6, temperature)) + probs = weights / (weights.sum() if weights.sum() > 0 else 1.0) + idx = rng.choices(range(len(cands)), weights=probs, k=1)[0] + return cands[idx] + +def _uniform_merge_params(a: Dict[ParameterNode, Any], b: Dict[ParameterNode, Any], rng: random.Random) -> Dict[ParameterNode, Any]: + """ + Simple, robust "crossover": per-parameter uniform pick between parents. + (System-aware enough for prompt/code params, cheap, and safe.) + """ + keys = set(a.keys()) | set(b.keys()) + merged: Dict[ParameterNode, Any] = {} + for p in keys: + if p in a and p in b: + merged[p] = copy.deepcopy(a[p] if rng.random() < 0.5 else b[p]) + elif p in a: + merged[p] = copy.deepcopy(a[p]) + else: + merged[p] = copy.deepcopy(b[p]) + return merged + +def _maybe_merge(buffer: List[Candidate], + *, + agent, + guide, + pareto_inputs: List[Any], + pareto_infos: List[Any], + num_threads: Optional[int], + rng: random.Random, + tried_pairs: set, + max_tries: int = 8) -> Optional[Candidate]: + """ + Try merging two non-lineage candidates once; return merged if better than both parents' mean, else None. + """ + if len(buffer) < 2: + return None + # Prefer winners + _compute_pareto_counts(buffer) + pool = sorted(buffer, key=lambda c: (c.wins, c.mean), reverse=True) + + # Try a few distinct pairs + for _ in range(max_tries): + i, j = rng.sample(range(len(pool)), 2) + a, b = pool[i], pool[j] + if a.id == b.id: + continue + if a.id in b.ancestors or b.id in a.ancestors: + continue # avoid direct ancestry + key = tuple(sorted((a.id, b.id))) + if key in tried_pairs: + continue + tried_pairs.add(key) + + merged_params = _uniform_merge_params(a.params, b.params, rng) + # Evaluate merged on Pareto subset + original_params = {p: copy.deepcopy(p.data) for p in agent.parameters()} + try: + # load params to agent + from opto.optimizers.optimizer import Optimizer # type: ignore + # We only need the parameters dict projection; we can set via optimizer.update if available + # But we don't have an optimizer here; use ParameterNode._set + for p, v in merged_params.items(): + p._set(v) + + vec = _eval_on_subset(agent, guide, pareto_inputs, pareto_infos, num_threads=num_threads, + desc="GEPA+Merge: evaluating merged") + mean = float(np.mean(vec)) if all(s is not None for s in vec) else -float("inf") + finally: + # restore original + for p, v in original_params.items(): + p._set(v) + + if mean > max(a.mean, b.mean): + merged = Candidate(params=merged_params, + eval_vector=vec, + mean=mean, + id=-1, # to be set by caller + parent_ids=(a.id, b.id), + ancestors=set(a.ancestors) | set(b.ancestors) | {a.id, b.id}, + created_iter=0) + return merged + return None + + +def _ensure_optimizer(agent, optimizer): + if optimizer is not None: + return optimizer + params = [p for p in agent.parameters()] # List[ParameterNode] + return OptoPrimeV2(parameters=params) + + +def _train_step_generate_child(agent, guide, optimizer, train_xs, train_infos, *, verbose=False, num_threads=None): + """ + Single-parent, incremental evolution "mutation": run forward on a minibatch to get batched feedback, + then optimizer.step(bypassing=True) to obtain a new candidate param dict (without applying). + """ + use_async = num_threads is not None and num_threads > 1 + if use_async: + outputs = async_run([lambda a,x,g,info: standard_optimization_step(a, x, g, info)] * len(train_xs), + args_list=[(agent, x, guide, info) for x, info in zip(train_xs, train_infos)], + max_workers=num_threads, + description="GEPA forward (mutate parent)") + # outputs: List[(target, score, feedback)] + else: + outputs = [standard_optimization_step(agent, x, guide, info) for x, info in zip(train_xs, train_infos)] + + scores, targets, feedbacks = [], [], [] + for target, score, feedback in outputs: + scores.append(score) + targets.append(target) + feedbacks.append(feedback) + + target_batch = batchify(*targets) + feedback_batch = batchify(*feedbacks).data + + optimizer.zero_feedback() + optimizer.backward(target_batch, feedback_batch) + try: + update_dict = optimizer.step(bypassing=True, verbose=("output" if verbose else False)) + if not isinstance(update_dict, dict) or len(update_dict) == 0: + # Fallback: treat current as child (rare) + update_dict = {p: copy.deepcopy(p.data) for p in optimizer.parameters} + except Exception as e: + print_color(f"[GEPA] optimizer.step error: {e}", "red") + update_dict = {} + return update_dict, (None if not scores or any(s is None for s in scores) else float(np.mean(scores))) + + +def _apply_params(optimizer, param_dict: Dict[ParameterNode, Any]): + """Load param dict into the agent via optimizer.update (preserves projections).""" + optimizer.update(param_dict) + + +# ======================= Variant 1: GEPA + Merge (UCB subclass) ======================= # + +class GEPAUCBSearch(UCBSearchAlgorithm): + """ + GEPA (+Merge) implemented atop UCBSearchAlgorithm. + Differences vs base UCB: + - Fixed Pareto subset (D_pareto) and per-instance vectors kept for each candidate + - Parent selection = Pareto "best-for-at-least-one" sampling (wins-weighted); UCB used only for eviction fallback + - Single-parent incremental mutation via a minibatch + - Optional periodic Merge crossover (uniform per-parameter) with desirability checks + """ + + def __init__(self, + agent, + optimizer=None, + *, + max_buffer_size: int = 16, + ucb_exploration_factor: float = 0.8, + rng_seed: int = 7, + logger=None, + num_threads: Optional[int] = None): + optimizer = _ensure_optimizer(agent, optimizer) + super().__init__(agent, optimizer, + max_buffer_size=max_buffer_size, + ucb_exploration_factor=ucb_exploration_factor, + logger=logger, + num_threads=num_threads) + self.rng = random.Random(rng_seed) + self._pareto_inputs: List[Any] = [] + self._pareto_infos: List[Any] = [] + self._id_counter = 0 + + def _next_id(self) -> int: + self._id_counter += 1 + return self._id_counter + + def _evaluate_on_pareto(self, params_dict: Dict[ParameterNode, Any], guide, *, num_threads) -> Tuple[List[float], float]: + original_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + try: + _apply_params(self.optimizer, params_dict) + vec = _eval_on_subset(self.agent, guide, self._pareto_inputs, self._pareto_infos, + num_threads=num_threads, desc="GEPA: evaluate on Pareto subset") + mean = float(np.mean(vec)) if all(s is not None for s in vec) else -float("inf") + return vec, mean + finally: + _apply_params(self.optimizer, original_params) + + def _select_pareto_parent(self, cand_buffer: List[Candidate]) -> Candidate: + return _pareto_sample(cand_buffer, temperature=1.0, rng=self.rng) + + def train(self, + guide, + train_dataset: Dict[str, List[Any]], + *, + validate_dataset: Optional[Dict[str, List[Any]]] = None, + pareto_subset_size: int = 24, + num_search_iterations: int = 120, + train_batch_size: int = 2, + merge_every: int = 6, + log_frequency: Optional[int] = None, + save_frequency: Optional[int] = None, + save_path: str = "checkpoints/gepa_ucb_agent.pkl", + verbose: bool = False, + num_threads: Optional[int] = None) -> Tuple[Dict[str, Any], float]: + """ + GEPA search loop with Pareto sampling + (optional) Merge. + """ + num_threads = num_threads or self.num_threads + log_frequency = log_frequency or 5 + validate_ds = validate_dataset or train_dataset + + # Fix a Pareto subset (small, stable) to compute per-instance vectors + assert len(validate_ds["inputs"]) > 0, "Empty dataset." + idxs = np.random.choice(len(validate_ds["inputs"]), + min(pareto_subset_size, len(validate_ds["inputs"])), + replace=False) + self._pareto_inputs = [validate_ds["inputs"][i] for i in idxs] + self._pareto_infos = [validate_ds["infos"][i] for i in idxs] + + buffer: List[Candidate] = [] + tried_merges: set = set() + + # Seed with current params + base_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + v0, m0 = self._evaluate_on_pareto(base_params, guide, num_threads=num_threads) + buffer.append(Candidate(params=base_params, eval_vector=v0, mean=m0, id=self._next_id(), ancestors=set())) + print_color(f"[GEPA] Seed candidate mean={m0:.4f}", "cyan") + + metrics = {"best_means": [], "new_child_means": [], "merge_accepts": 0, "total_merges": 0} + + for it in range(1, num_search_iterations + 1): + # Select parent by Pareto winners + parent = self._select_pareto_parent(buffer) + _apply_params(self.optimizer, parent.params) + + # Sample train minibatch + train_size = min(train_batch_size, len(train_dataset["inputs"])) + tr_idxs = np.random.choice(len(train_dataset["inputs"]), train_size, replace=False) + train_xs = [train_dataset["inputs"][i] for i in tr_idxs] + train_info = [train_dataset["infos"][i] for i in tr_idxs] + + # Generate child via one incremental step + update_dict, train_batch_mean = _train_step_generate_child( + self.agent, guide, self.optimizer, train_xs, train_info, verbose=verbose, num_threads=num_threads + ) + if not update_dict: + print_color("[GEPA] Empty child update; skipping.", "yellow") + continue + + # Evaluate child on Pareto subset + child_vec, child_mean = self._evaluate_on_pareto(update_dict, guide, num_threads=num_threads) + child = Candidate(params=update_dict, + eval_vector=child_vec, + mean=child_mean, + id=self._next_id(), + parent_ids=(parent.id,), + ancestors=set(parent.ancestors) | {parent.id}, + created_iter=it) + buffer.append(child) + metrics["new_child_means"].append(child_mean) + print_color(f"[GEPA] iter {it}: child mean={child_mean:.4f} (train-batch≈{train_batch_mean})", "green") + + # Optional Merge + if merge_every and (it % merge_every == 0): + metrics["total_merges"] += 1 + merged = _maybe_merge(buffer, + agent=self.agent, guide=guide, + pareto_inputs=self._pareto_inputs, + pareto_infos=self._pareto_infos, + num_threads=num_threads, + rng=self.rng, + tried_pairs=tried_merges) + if merged is not None: + merged.id = self._next_id() + merged.created_iter = it + buffer.append(merged) + metrics["merge_accepts"] += 1 + print_color(f"[GEPA] Merge accepted: mean={merged.mean:.4f}", "magenta") + + # Keep buffer bounded: remove the candidate with lowest (wins, mean) + if len(buffer) > self.max_buffer_size: + _compute_pareto_counts(buffer) + buffer.sort(key=lambda c: (c.wins, c.mean)) + evicted = buffer.pop(0) + print_color(f"[GEPA] Evicted cand#{evicted.id} (wins={evicted.wins}, mean={evicted.mean:.4f})", "yellow") + + # Track & log + best = max(buffer, key=lambda c: c.mean) + metrics["best_means"].append(best.mean) + if it % log_frequency == 0: + self.logger.log("GEPA best mean", best.mean, it, color="green") + + # Save best candidate snapshot (optional) + if save_frequency and it % save_frequency == 0: + _apply_params(self.optimizer, best.params) + self.save_agent(save_path, it) + + # Load best into the agent and return + best = max(buffer, key=lambda c: c.mean) if buffer else buffer[0] + _apply_params(self.optimizer, best.params) + return metrics, float(best.mean) + + +# ================= Variant 2: Beamsearch subclass with Pareto select ================= # + +class GEPABeamPareto(BeamsearchAlgorithm): + """ + BeamsearchAlgorithm retrofit: + - override select() to a Pareto "best-for-at-least-one" selector + - replace deep beam expansion with GEPA’s single-parent incremental evolution + """ + + def __init__(self, + agent, + optimizer=None, + *, + rng_seed: int = 11, + logger=None, + num_threads: Optional[int] = None): + optimizer = _ensure_optimizer(agent, optimizer) + super().__init__(agent, optimizer, num_threads=num_threads, logger=logger) + self.rng = random.Random(rng_seed) + + # We keep a Pareto select helper that returns (selected_params, wins, scores) + def select(self, + candidates: List[Dict[ParameterNode, Any]], + validate_guide, + validation_mini_dataset, + beam_width: int, + num_threads: int = None, + min_score: float = None, + return_scores: bool = False): + """ + Override to Pareto union-of-winners on the mini validation batch. + """ + # Evaluate each candidate to a vector on the mini validation + cand_objs: List[Candidate] = [] + current_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + try: + for idx, params in enumerate(candidates): + _apply_params(self.optimizer, params) + vec = evaluate(self.agent, + validate_guide, + validation_mini_dataset['inputs'], + validation_mini_dataset['infos'], + min_score=min_score, + num_threads=num_threads, + description=f"Validating candidate {idx+1}/{len(candidates)} (Pareto)") + mean = float(np.mean(vec)) if all(s is not None for s in vec) else -float("inf") + cand_objs.append(Candidate(params=params, eval_vector=vec, mean=mean, id=idx)) + finally: + _apply_params(self.optimizer, current_params) + + # Compute wins and select top "beam_width" by (wins, mean) + _compute_pareto_counts(cand_objs) + cand_objs.sort(key=lambda c: (c.wins, c.mean), reverse=True) + selected = cand_objs[: min(beam_width, len(cand_objs))] + sel_params = [c.params for c in selected] + sel_scores = [c.mean for c in selected] + if return_scores: + return sel_params, sel_scores + return sel_params + + # Replace beam "train" with GEPA-style incremental loop (keeps BeamsearchAlgorithm API) + def train(self, + guide, + train_dataset, + *, + validate_dataset=None, + pareto_subset_size: int = 24, + num_search_iterations: int = 120, + train_batch_size: int = 2, + merge_every: int = 6, + log_frequency: Optional[int] = None, + save_frequency: Optional[int] = None, + save_path: str = "checkpoints/gepa_beam_agent.pkl", + verbose: bool = False, + num_threads: Optional[int] = None): + num_threads = num_threads or self.num_threads + log_frequency = log_frequency or 5 + validate_ds = validate_dataset or train_dataset + + # Fix Pareto subset for this run + idxs = np.random.choice(len(validate_ds["inputs"]), + min(pareto_subset_size, len(validate_ds["inputs"])), + replace=False) + pareto_inputs = [validate_ds["inputs"][i] for i in idxs] + pareto_infos = [validate_ds["infos"][i] for i in idxs] + + # Seed buffer + buffer: List[Candidate] = [] + base_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + # Evaluate seed + current_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + try: + _apply_params(self.optimizer, base_params) + vec = evaluate(self.agent, guide, pareto_inputs, pareto_infos, + min_score=None, num_threads=num_threads, + description="GEPA(beam): seed evaluation") + finally: + _apply_params(self.optimizer, current_params) + m0 = float(np.mean(vec)) if all(s is not None for s in vec) else -float("inf") + buffer.append(Candidate(params=base_params, eval_vector=vec, mean=m0, id=0, ancestors=set())) + tried_merges: set = set() + + best_mean = m0 + for it in range(1, num_search_iterations + 1): + # Pareto-select parent and mutate + _compute_pareto_counts(buffer) + parent = _pareto_sample(buffer, temperature=1.0, rng=self.rng) + _apply_params(self.optimizer, parent.params) + + # Make a child + k = min(train_batch_size, len(train_dataset["inputs"])) + tr = np.random.choice(len(train_dataset["inputs"]), k, replace=False) + train_xs = [train_dataset["inputs"][i] for i in tr] + train_in = [train_dataset["infos"][i] for i in tr] + + update_dict, _ = _train_step_generate_child(self.agent, guide, self.optimizer, train_xs, train_in, + verbose=verbose, num_threads=num_threads) + if not update_dict: + continue + + # Evaluate child on Pareto subset + current_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + try: + _apply_params(self.optimizer, update_dict) + vec = evaluate(self.agent, guide, pareto_inputs, pareto_infos, min_score=None, + num_threads=num_threads, description="GEPA(beam): child eval") + finally: + _apply_params(self.optimizer, current_params) + mean = float(np.mean(vec)) if all(s is not None for s in vec) else -float("inf") + buffer.append(Candidate(params=update_dict, eval_vector=vec, mean=mean, id=len(buffer), + parent_ids=(parent.id,), ancestors=set(parent.ancestors) | {parent.id})) + best_mean = max(best_mean, mean) + if it % log_frequency == 0: + self.logger.log("GEPA(beam) best mean", best_mean, it, color="green") + + # Periodic merge + if merge_every and it % merge_every == 0: + merged = _maybe_merge(buffer, + agent=self.agent, guide=guide, + pareto_inputs=pareto_inputs, pareto_infos=pareto_infos, + num_threads=num_threads, rng=self.rng, tried_pairs=tried_merges) + if merged is not None: + merged.id = len(buffer) + buffer.append(merged) + + # Trim buffer softly (keep top by (wins, mean)) + if len(buffer) > 16: + _compute_pareto_counts(buffer) + buffer.sort(key=lambda c: (c.wins, c.mean), reverse=True) + buffer[:] = buffer[:16] + + # Optional save + if save_frequency and it % save_frequency == 0: + best = max(buffer, key=lambda c: c.mean) + _apply_params(self.optimizer, best.params) + self.save_agent(save_path, it) + + best = max(buffer, key=lambda c: c.mean) + _apply_params(self.optimizer, best.params) + return {"best_mean": best.mean}, float(best.mean) + + +# =================== Variant 3: Minimal GEPA on AlgorithmBase =================== # + +class GEPAAlgorithmBase(Trainer): + """ + Lightweight GEPA (+Merge) with only Trainer dependency. + Useful when you want the simplest control loop with your own logging/saving. + """ + + def __init__(self, + agent, + optimizer=None, + *, + rng_seed: int = 13, + logger=None, + num_threads: Optional[int] = None): + super().__init__(agent, num_threads=num_threads, logger=logger) + self.optimizer = _ensure_optimizer(agent, optimizer) + self.rng = random.Random(rng_seed) + + def train(self, + guide, + train_dataset, + *, + validate_dataset=None, + pareto_subset_size: int = 24, + num_iters: int = 100, + train_batch_size: int = 2, + merge_every: int = 5, + num_threads: Optional[int] = None, + save_path: Optional[str] = None): + num_threads = num_threads or self.num_threads + validate_ds = validate_dataset or train_dataset + + # Pareto subset + idxs = np.random.choice(len(validate_ds["inputs"]), + min(pareto_subset_size, len(validate_ds["inputs"])), + replace=False) + xsP = [validate_ds["inputs"][i] for i in idxs] + isP = [validate_ds["infos"][i] for i in idxs] + + # Seed + buffer: List[Candidate] = [] + base_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + original = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + try: + _apply_params(self.optimizer, base_params) + vec = evaluate(self.agent, guide, xsP, isP, min_score=None, num_threads=num_threads, + description="GEPA(base): seed eval") + finally: + _apply_params(self.optimizer, original) + m0 = float(np.mean(vec)) if all(s is not None for s in vec) else -float("inf") + buffer.append(Candidate(params=base_params, eval_vector=vec, mean=m0, id=0, ancestors=set())) + tried_merges: set = set() + + for it in range(1, num_iters + 1): + # Parent select + _compute_pareto_counts(buffer) + parent = _pareto_sample(buffer, temperature=1.0, rng=self.rng) + _apply_params(self.optimizer, parent.params) + + # Child + k = min(train_batch_size, len(train_dataset["inputs"])) + tr = np.random.choice(len(train_dataset["inputs"]), k, replace=False) + tx = [train_dataset["inputs"][i] for i in tr] + ti = [train_dataset["infos"][i] for i in tr] + update_dict, _ = _train_step_generate_child(self.agent, guide, self.optimizer, tx, ti, + verbose=False, num_threads=num_threads) + if not update_dict: + continue + + # Eval child + original = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + try: + _apply_params(self.optimizer, update_dict) + vec = evaluate(self.agent, guide, xsP, isP, min_score=None, num_threads=num_threads, + description="GEPA(base): child eval") + finally: + _apply_params(self.optimizer, original) + mean = float(np.mean(vec)) if all(s is not None for s in vec) else -float("inf") + buffer.append(Candidate(params=update_dict, eval_vector=vec, mean=mean, id=len(buffer), + parent_ids=(parent.id,), ancestors=set(parent.ancestors) | {parent.id})) + + # Merge + if merge_every and it % merge_every == 0: + merged = _maybe_merge(buffer, + agent=self.agent, guide=guide, + pareto_inputs=xsP, pareto_infos=isP, + num_threads=num_threads, rng=self.rng, tried_pairs=tried_merges) + if merged is not None: + merged.id = len(buffer) + buffer.append(merged) + + # Keep compact buffer + if len(buffer) > 16: + _compute_pareto_counts(buffer) + buffer.sort(key=lambda c: (c.wins, c.mean), reverse=True) + buffer[:] = buffer[:16] + + # Log + best = max(buffer, key=lambda c: c.mean) + if self.logger: + self.logger.log("GEPA(base) best mean", best.mean, it, color="green") + + # Optional save + if save_path and it % 10 == 0: + _apply_params(self.optimizer, best.params) + self.save_agent(save_path, it) + + # Load best into agent + best = max(buffer, key=lambda c: c.mean) + _apply_params(self.optimizer, best.params) + return {"best_mean": best.mean}, float(best.mean) + diff --git a/tests/llm_optimizers_tests/test_gepa_benchmark.py b/tests/llm_optimizers_tests/test_gepa_benchmark.py new file mode 100644 index 00000000..fdfe5d2e --- /dev/null +++ b/tests/llm_optimizers_tests/test_gepa_benchmark.py @@ -0,0 +1,94 @@ +import os +import pytest +import numpy as np + +from opto import trace +from opto.optimizers.optoprime_v2 import OptoPrimeV2 +from opto.trainer.algorithms.gepa_algorithms import GEPAAlgorithmBase, GEPAUCBSearch, GEPABeamPareto +from opto.trainer.algorithms.basic_algorithms import BasicSearchAlgorithm +from opto.trainer.guide import LLMJudge +from opto.utils.llm import LLM + + +RUN_BENCH = "1" + + +def _datasets_or_skip(): + try: + import datasets # noqa: F401 + except Exception: + pytest.skip("datasets library not available; skipping GEPA benchmark test.") + + +def _llm_env_or_skip(): + have_key = any(os.getenv(k) for k in ["OPENAI_API_KEY", "AZURE_OPENAI_API_KEY", "ANTHROPIC_API_KEY", "OAI_CONFIG_LIST"]) + if not have_key: + pytest.skip("No LLM credentials found in environment; skipping GEPA benchmark test.") + + +@trace.model +class Learner: + """Agent that calls an LLM. The only trainable variable is 'system_prompt'.""" + + def __init__(self, system_prompt: str = "You're a helpful agent", user_prompt_template: str = "Query: {message}", llm: LLM = None): + self.system_prompt = trace.node(system_prompt, trainable=True) + self.user_prompt_template = trace.node(user_prompt_template) + self.llm = llm or LLM() # default profile + + @trace.bundle() + def model(self, system_prompt: str, user_prompt_template: str, message: str) -> str: + if "{message}" not in user_prompt_template: + raise ValueError("user_prompt_template must contain '{message}'") + resp = self.llm( + messages=[ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt_template.format(message=message)}, + ] + ) + return resp.choices[0].message.content + + def forward(self, message): + return self.model(self.system_prompt, self.user_prompt_template, message) + + +@pytest.mark.skipif(not RUN_BENCH, reason="Set RUN_GEPA_BENCH=1 to run this optional benchmark test.") +def test_gepa_benchmark_gsm8k_real_llm(): + _datasets_or_skip() + _llm_env_or_skip() + + import datasets + + # Load a tiny subset of GSM8k + ds = datasets.load_dataset("openai/gsm8k", "main") + train = ds["train"][:6] + train_dataset = {"inputs": train["question"], "infos": train["answer"]} + + # Teacher/judge with a low-cost profile + guide = LLMJudge(llm=LLM(profile="cheap")) + + # Agent and optimizer (low-cost profile) + agent = Learner(llm=LLM(profile="cheap")) + optimizer = OptoPrimeV2(agent.parameters(), llm=LLM(profile="cheap")) + + algos = [ + ("GEPA-Base", GEPAAlgorithmBase(agent, optimizer=optimizer, logger=None, num_threads=2), dict(num_iters=2, train_batch_size=1, merge_every=2)), + ("GEPA-UCB", GEPAUCBSearch(agent, optimizer=optimizer, logger=None, num_threads=2), dict(num_search_iterations=2, train_batch_size=1, merge_every=2)), + ("GEPA-Beam", GEPABeamPareto(agent, optimizer=optimizer, logger=None, num_threads=2), dict(num_search_iterations=2, train_batch_size=1, merge_every=2)), + ("BasicSearch", BasicSearchAlgorithm(agent, optimizer=optimizer, logger=None, num_threads=2), dict(num_epochs=1, batch_size=1, num_proposals=2)), + ] + + results = {} + for name, algo, kwargs in algos: + if name == "BasicSearch": + # Conform to BasicSearch's interface + algo.train(guide=guide, train_dataset=train_dataset, validate_dataset=train_dataset, test_dataset=train_dataset, eval_frequency=1, num_threads=2, verbose=False, **kwargs) + results[name] = 0.0 # placeholder; evaluation is heavy and non-deterministic + else: + _, best = algo.train(guide=guide, train_dataset=train_dataset, validate_dataset=train_dataset, pareto_subset_size=4, num_threads=2, **kwargs) + results[name] = float(best) + + # Sanity check that we produced some floats for each algorithm + assert set(results.keys()) == {"GEPA-Base", "GEPA-UCB", "GEPA-Beam", "BasicSearch"} + for v in results.values(): + assert isinstance(v, float) + From 4adac1b0208718e4a5550e29bb978263be7afd6c Mon Sep 17 00:00:00 2001 From: doxav Date: Mon, 22 Sep 2025 08:17:11 +0200 Subject: [PATCH 275/314] added unit test --- tests/unit_tests/test_gepa_algorithms.py | 214 +++++++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 tests/unit_tests/test_gepa_algorithms.py diff --git a/tests/unit_tests/test_gepa_algorithms.py b/tests/unit_tests/test_gepa_algorithms.py new file mode 100644 index 00000000..a4c42f26 --- /dev/null +++ b/tests/unit_tests/test_gepa_algorithms.py @@ -0,0 +1,214 @@ +import math +import os +import random +import re +from typing import Any, Dict, List, Tuple + +import numpy as np +import pytest + +# Provide a light stub for optional graphviz dependency to allow imports without system graphviz +import sys, types +if "graphviz" not in sys.modules: + sys.modules["graphviz"] = types.SimpleNamespace(Digraph=object) + +from opto.trace.modules import model as trace_model +from opto.trace.nodes import node as trace_node +from opto.optimizers.optoprime_v2 import OptoPrimeV2 +import pytest +from opto.trainer.algorithms.gepa_algorithms import ( + GEPAAlgorithmBase, + GEPAUCBSearch, + GEPABeamPareto, + _compute_pareto_counts, + _pareto_sample, + _uniform_merge_params, + ) +from opto.trainer.evaluators import evaluate +from opto.trainer.guide import Guide +from opto.utils.llm import DummyLLM + + +class ExactMatchGuide(Guide): + """Simple guide: score=1 if response == reference, else 0.""" + + def get_feedback(self, query: Any, response: Any, reference: Any, **kwargs): + score = float(response == reference) + feedback = f"Score: {score}. Response: {response}. Reference: {reference}." + return score, feedback + + +@trace_model +class AddAgent: + """Toy agent: returns x + param.""" + + def __init__(self, param: int = 0): + self.param = trace_node(int(param), trainable=True) + + def forward(self, x: int) -> int: + return x + self.param + + +def make_dummy_llm(suggest_value: int) -> DummyLLM: + """Dummy LLM that parses the variable name from the prompt and suggests a fixed value. + + Matches the default XML-like output format expected by OptoPrimeV2. + """ + + def _llm_callable(messages, **kwargs): + # Extract the variable name from the #Variables section in the prompt + problem = messages[1]["content"] if isinstance(messages, (list, tuple)) and len(messages) > 1 else "" + name_match = re.findall(r"", problem) + var_name = name_match[0] if name_match else "param" + return ( + f""" + Dummy reasoning based on the input messages. + + {var_name} + {suggest_value} + + """ + ) + + return DummyLLM(_llm_callable) + + +def make_dataset(target_add: int, n: int = 8) -> Dict[str, List[int]]: + xs = list(range(n)) + infos = [x + target_add for x in xs] + return {"inputs": xs, "infos": infos} + + +def build_optimizer(agent: AddAgent, suggest_value: int) -> OptoPrimeV2: + return OptoPrimeV2(agent.parameters(), llm=make_dummy_llm(suggest_value)) + + +def test_pareto_counting_and_sampling(): + # Construct mock candidates with per-instance eval vectors where each wins on one dimension + from types import SimpleNamespace + + class Cand(SimpleNamespace): + pass + + A = Cand(eval_vector=[1.0, 0.1], wins=0, mean=0.55) + B = Cand(eval_vector=[0.2, 1.1], wins=0, mean=0.65) + cands = [A, B] + + _compute_pareto_counts(cands) + assert A.wins == 1 and B.wins == 1 + + rng = random.Random(0) + # With equal wins, both should be sampled with similar probability + picks = [ + _pareto_sample([A, B], temperature=1.0, rng=rng) for _ in range(100) + ] + a_count = sum(p is A for p in picks) + b_count = sum(p is B for p in picks) + assert abs(a_count - b_count) < 40 # rough balance + + +def test_uniform_merge_params_uses_both_parents(): + # Use two ParameterNodes to exercise merging across keys + @trace_model + class TwoParam: + def __init__(self): + self.a = trace_node(1, trainable=True) + self.b = trace_node(2, trainable=True) + + def forward(self, x): + return self.a + self.b + x + + m = TwoParam() + a_params = {p: (10 if p.py_name.endswith("a") else 20) for p in m.parameters()} + b_params = {p: (100 if p.py_name.endswith("a") else 200) for p in m.parameters()} + + rng = random.Random(123) + merged = _uniform_merge_params(a_params, b_params, rng) + # For each key, merged value should be chosen from either a_params or b_params + for k, v in merged.items(): + assert v in (a_params[k], b_params[k]) + + +@pytest.mark.parametrize( + "algo_cls,train_kwargs", + [ + (GEPAAlgorithmBase, {"num_iters": 8, "train_batch_size": 2, "merge_every": 2}), + (GEPAUCBSearch, {"num_search_iterations": 8, "train_batch_size": 2, "merge_every": 2}), + (GEPABeamPareto, {"num_search_iterations": 8, "train_batch_size": 2, "merge_every": 2}), + ], +) +def test_gepa_variants_converge_on_dummyllm(algo_cls, train_kwargs): + target_add = 5 + ds = make_dataset(target_add, n=6) + agent = AddAgent(param=0) + optimizer = build_optimizer(agent, suggest_value=target_add) + + algo = algo_cls(agent=agent, optimizer=optimizer, logger=None, num_threads=1) + + # Prepare kwargs and include 'verbose' only if supported + import inspect + call_kwargs = dict(guide=ExactMatchGuide(), train_dataset=ds, pareto_subset_size=4, num_threads=1) + sig = inspect.signature(algo.train) + if 'validation_dataset' in sig.parameters: + call_kwargs['validation_dataset'] = ds + else: + call_kwargs['validate_dataset'] = ds + call_kwargs.update(train_kwargs) + if 'verbose' in sig.parameters: + call_kwargs['verbose'] = False + + metrics, best = algo.train(**call_kwargs) + + # Best mean on pareto subset should be perfect + assert isinstance(best, float) + assert best == pytest.approx(1.0, rel=0, abs=1e-6) + # Agent parameter should be updated to target_add + assert agent.param.data == target_add + + +def test_compare_gepa_vs_basicsearch_on_dummyllm(): + from opto.trainer.algorithms.basic_algorithms import BasicSearchAlgorithm + + target_add = 7 + ds = make_dataset(target_add, n=6) + agent_gepa = AddAgent(param=0) + agent_basic = AddAgent(param=0) + + opt_gepa = build_optimizer(agent_gepa, suggest_value=target_add) + opt_basic = build_optimizer(agent_basic, suggest_value=target_add) + + # GEPA + gepa = GEPAAlgorithmBase(agent_gepa, optimizer=opt_gepa, logger=None, num_threads=1) + _, best_gepa = gepa.train( + guide=ExactMatchGuide(), + train_dataset=ds, + validate_dataset=ds, + pareto_subset_size=4, + num_iters=8, + train_batch_size=2, + merge_every=2, + num_threads=1, + ) + + # BasicSearch baseline + basic = BasicSearchAlgorithm(agent_basic, optimizer=opt_basic, logger=None, num_threads=1) + basic.train( + guide=ExactMatchGuide(), + train_dataset=ds, + validate_dataset=ds, + num_proposals=1, + num_epochs=1, + batch_size=1, + test_dataset=ds, + eval_frequency=1, + num_threads=1, + verbose=False, + ) + + # Evaluate both on full dataset + score_gepa = np.mean(evaluate(agent_gepa, ExactMatchGuide(), ds["inputs"], ds["infos"], num_threads=2)) + score_basic = np.mean(evaluate(agent_basic, ExactMatchGuide(), ds["inputs"], ds["infos"], num_threads=2)) + + assert best_gepa == pytest.approx(1.0, rel=0, abs=1e-6) + assert score_gepa == pytest.approx(1.0, rel=0, abs=1e-6) + assert score_basic == pytest.approx(1.0, rel=0, abs=1e-6) From 0d9e0e6f1cddb4dc8694bb699dfed160b8fa6015 Mon Sep 17 00:00:00 2001 From: doxav Date: Mon, 22 Sep 2025 08:52:00 +0200 Subject: [PATCH 276/314] IMPROVED but to check: total iterations seems much higher --- opto/trainer/algorithms/gepa_algorithms.py | 246 ++++++++++++++--- .../test_gepa_benchmark.py | 64 ++++- tests/unit_tests/test_gepa_algorithms.py | 257 ++++++++++++++++++ 3 files changed, 521 insertions(+), 46 deletions(-) diff --git a/opto/trainer/algorithms/gepa_algorithms.py b/opto/trainer/algorithms/gepa_algorithms.py index 588cdbad..2dac2a7c 100644 --- a/opto/trainer/algorithms/gepa_algorithms.py +++ b/opto/trainer/algorithms/gepa_algorithms.py @@ -26,6 +26,11 @@ standard_optimization_step, ) from opto.trainer.utils import async_run +# Prefer thread-safe batched runner (deep-copies per task). Fallback handled at callsite. +try: + from opto.trainer.utils import batch_run # type: ignore +except Exception: # pragma: no cover + batch_run = None from opto.optimizers.utils import print_color @@ -134,7 +139,7 @@ def _maybe_merge(buffer: List[Candidate], merged_params = _uniform_merge_params(a.params, b.params, rng) # Evaluate merged on Pareto subset - original_params = {p: copy.deepcopy(p.data) for p in agent.parameters()} + original_params = _snapshot_params_fast(list(agent.parameters())) try: # load params to agent from opto.optimizers.optimizer import Optimizer # type: ignore @@ -163,6 +168,98 @@ def _maybe_merge(buffer: List[Candidate], return None +def _maybe_merge_ancestor_aware( + buffer: List[Candidate], + *, + id2cand: Dict[int, Candidate], + module_groups: List[List[ParameterNode]], + agent, + guide, + optimizer, + train_dataset: Dict[str, List[Any]], + train_batch_size: int, + pareto_inputs: List[Any], + pareto_infos: List[Any], + num_threads: Optional[int], + rng: random.Random, + tried_pairs: set, + budget_tracker: Optional[Dict[str, int]] = None, + budget_B: Optional[int] = None, + max_tries: int = 8 +) -> Optional[Tuple[Candidate, int]]: + """ + Ancestor-aware merge with budget tracking. Returns (merged_candidate, rollouts_used). + """ + if len(buffer) < 2: + return None + + rollouts_used = 0 + + # Sample training minibatch + tx = rng.choices(train_dataset["inputs"], k=min(train_batch_size, len(train_dataset["inputs"]))) + ti = rng.choices(train_dataset["infos"], k=len(tx)) + + # Prefer winners for parent selection + _compute_pareto_counts(buffer) + pool = sorted(buffer, key=lambda c: (c.wins, c.mean), reverse=True) + + for _ in range(max_tries): + i, j = rng.sample(range(len(pool)), 2) + ci, cj = pool[i], pool[j] + if ci.id == cj.id: + continue + if ci.id in cj.ancestors or cj.id in ci.ancestors: + continue # avoid direct ancestry + key = tuple(sorted((ci.id, cj.id))) + if key in tried_pairs: + continue + tried_pairs.add(key) + + merged_params = _uniform_merge_params(ci.params, cj.params, rng) + + # Quick minibatch acceptability check + def _batch_mean_for(param_dict): + original = _snapshot_params_fast(list(optimizer.parameters)) + try: + _apply_params(optimizer, param_dict) + vec = evaluate(agent, guide, tx, ti, min_score=None, num_threads=num_threads, + description="MERGE(mini-batch accept)") + finally: + _apply_params(optimizer, original) + return float(np.mean(vec)) if all(s is not None for s in vec) else -float("inf") + + rollouts_used += len(tx) + merged_batch_mean = _batch_mean_for(merged_params) + parent_means = [_batch_mean_for(ci.params), _batch_mean_for(cj.params)] + rollouts_used += 2 * len(tx) + + if merged_batch_mean <= max(parent_means): + continue # Not promising enough + + # Full Pareto evaluation + original = _snapshot_params_fast(list(optimizer.parameters)) + try: + _apply_params(optimizer, merged_params) + vec = evaluate(agent, guide, pareto_inputs, pareto_infos, min_score=None, + num_threads=num_threads, description="GEPA+Merge: ancestor-aware Pareto eval") + finally: + _apply_params(optimizer, original) + mean = float(np.mean(vec)) if all(s is not None for s in vec) else -float("inf") + # Account Pareto evaluation cost in the global budget and local counter. + if budget_B is not None and budget_tracker is not None: + budget_tracker["used"] += len(pareto_inputs) + rollouts_used += len(pareto_inputs) + + merged = Candidate(params=merged_params, + eval_vector=vec, mean=mean, + id=-1, parent_ids=(ci.id, cj.id), + ancestors=set(ci.ancestors) | set(cj.ancestors) | {ci.id, cj.id}, + created_iter=0) + return merged, rollouts_used + + return None + + def _ensure_optimizer(agent, optimizer): if optimizer is not None: return optimizer @@ -175,14 +272,16 @@ def _train_step_generate_child(agent, guide, optimizer, train_xs, train_infos, * Single-parent, incremental evolution "mutation": run forward on a minibatch to get batched feedback, then optimizer.step(bypassing=True) to obtain a new candidate param dict (without applying). """ - use_async = num_threads is not None and num_threads > 1 - if use_async: + use_parallel = (num_threads is not None and num_threads > 1) + if use_parallel: + # Use async_run but ensure thread safety through parameter handling + # Since we're working with parameters through optimizer, this should be thread-safe outputs = async_run([lambda a,x,g,info: standard_optimization_step(a, x, g, info)] * len(train_xs), args_list=[(agent, x, guide, info) for x, info in zip(train_xs, train_infos)], max_workers=num_threads, description="GEPA forward (mutate parent)") - # outputs: List[(target, score, feedback)] else: + # Safe sequential fallback. outputs = [standard_optimization_step(agent, x, guide, info) for x, info in zip(train_xs, train_infos)] scores, targets, feedbacks = [], [], [] @@ -212,6 +311,44 @@ def _apply_params(optimizer, param_dict: Dict[ParameterNode, Any]): optimizer.update(param_dict) +def _snapshot_params_fast(parameters: List[ParameterNode]) -> Dict[ParameterNode, Any]: + """ + Snapshot ParameterNode->value with minimal copying: + - immutables (str/int/float/bool/tuple/bytes/None): no copy + - numpy arrays: .copy() + - everything else: deepcopy (safe fallback) + """ + snap: Dict[ParameterNode, Any] = {} + immutables = (str, int, float, bool, tuple, frozenset, bytes, type(None)) + for p in parameters: + v = getattr(p, "data", None) + if isinstance(v, immutables): + snap[p] = v + elif isinstance(v, np.ndarray): + snap[p] = v.copy() + else: + snap[p] = copy.deepcopy(v) + return snap + + +def _fingerprint_params(params_dict: Dict[ParameterNode, Any]) -> Tuple: + """ + Hashable fingerprint of a ParameterNode->value dict for optional caching. + Uses (param-id, repr(value)) with special handling for numpy arrays. + """ + items: List[Tuple] = [] + for p, v in params_dict.items(): + pid = getattr(p, "uid", None) or getattr(p, "name", None) or id(p) + try: + if isinstance(v, np.ndarray): + items.append(("arr", pid, v.shape, v.dtype.str, hash(v.tobytes()))) + else: + items.append(("val", pid, repr(v))) + except Exception: + items.append(("val", pid, repr(v))) + return tuple(sorted(items)) + + # ======================= Variant 1: GEPA + Merge (UCB subclass) ======================= # class GEPAUCBSearch(UCBSearchAlgorithm): @@ -232,7 +369,10 @@ def __init__(self, ucb_exploration_factor: float = 0.8, rng_seed: int = 7, logger=None, - num_threads: Optional[int] = None): + num_threads: Optional[int] = None, + module_groups: Optional[Dict[str, List[ParameterNode]] | List[List[ParameterNode]]] = None, + selectmodule_policy: str = "round_robin", + enable_pareto_cache: bool = False): optimizer = _ensure_optimizer(agent, optimizer) super().__init__(agent, optimizer, max_buffer_size=max_buffer_size, @@ -240,22 +380,37 @@ def __init__(self, logger=logger, num_threads=num_threads) self.rng = random.Random(rng_seed) + np.random.seed(rng_seed) # ensure numpy reproducibility for np.random.choice self._pareto_inputs: List[Any] = [] self._pareto_infos: List[Any] = [] self._id_counter = 0 + self.enable_pareto_cache = enable_pareto_cache + self._pareto_cache: Dict[Tuple, Tuple[List[float], float]] = {} + # >>> NEW selector (commented out as ModuleSelector may not exist) + # self.module_selector = ModuleSelector(self.optimizer.parameters, + # module_groups=module_groups, + # policy=selectmodule_policy) def _next_id(self) -> int: self._id_counter += 1 return self._id_counter def _evaluate_on_pareto(self, params_dict: Dict[ParameterNode, Any], guide, *, num_threads) -> Tuple[List[float], float]: - original_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + cache_key = _fingerprint_params(params_dict) if self.enable_pareto_cache else None + if cache_key is not None: + cached = self._pareto_cache.get(cache_key) + if cached is not None: + return cached + original_params = _snapshot_params_fast(list(self.optimizer.parameters)) try: _apply_params(self.optimizer, params_dict) vec = _eval_on_subset(self.agent, guide, self._pareto_inputs, self._pareto_infos, num_threads=num_threads, desc="GEPA: evaluate on Pareto subset") mean = float(np.mean(vec)) if all(s is not None for s in vec) else -float("inf") - return vec, mean + result = (vec, mean) + if cache_key is not None: + self._pareto_cache[cache_key] = result + return result finally: _apply_params(self.optimizer, original_params) @@ -293,11 +448,14 @@ def train(self, buffer: List[Candidate] = [] tried_merges: set = set() + id2cand: Dict[int, Candidate] = {} # Seed with current params - base_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + base_params = _snapshot_params_fast(list(self.optimizer.parameters)) v0, m0 = self._evaluate_on_pareto(base_params, guide, num_threads=num_threads) - buffer.append(Candidate(params=base_params, eval_vector=v0, mean=m0, id=self._next_id(), ancestors=set())) + seed = Candidate(params=base_params, eval_vector=v0, mean=m0, id=self._next_id(), ancestors=set(), created_iter=0) + buffer.append(seed) + id2cand[seed.id] = seed print_color(f"[GEPA] Seed candidate mean={m0:.4f}", "cyan") metrics = {"best_means": [], "new_child_means": [], "merge_accepts": 0, "total_merges": 0} @@ -384,16 +542,17 @@ class GEPABeamPareto(BeamsearchAlgorithm): - replace deep beam expansion with GEPA’s single-parent incremental evolution """ - def __init__(self, - agent, - optimizer=None, - *, - rng_seed: int = 11, - logger=None, - num_threads: Optional[int] = None): + def __init__(self, agent, optimizer=None, *, rng_seed: int = 11, logger=None, + num_threads: Optional[int] = None, + module_groups: Optional[Dict[str, List[ParameterNode]] | List[List[ParameterNode]]] = None, + selectmodule_policy: str = "round_robin"): optimizer = _ensure_optimizer(agent, optimizer) super().__init__(agent, optimizer, num_threads=num_threads, logger=logger) self.rng = random.Random(rng_seed) + np.random.seed(rng_seed) + # self.module_selector = ModuleSelector(self.optimizer.parameters, + # module_groups=module_groups, + # policy=selectmodule_policy) # We keep a Pareto select helper that returns (selected_params, wins, scores) def select(self, @@ -409,7 +568,7 @@ def select(self, """ # Evaluate each candidate to a vector on the mini validation cand_objs: List[Candidate] = [] - current_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + current_params = _snapshot_params_fast(list(self.optimizer.parameters)) try: for idx, params in enumerate(candidates): _apply_params(self.optimizer, params) @@ -436,20 +595,17 @@ def select(self, return sel_params # Replace beam "train" with GEPA-style incremental loop (keeps BeamsearchAlgorithm API) - def train(self, - guide, - train_dataset, - *, - validate_dataset=None, - pareto_subset_size: int = 24, - num_search_iterations: int = 120, - train_batch_size: int = 2, - merge_every: int = 6, - log_frequency: Optional[int] = None, + def train(self, guide, train_dataset, *, + validate_dataset=None, pareto_subset_size: int = 24, + num_search_iterations: int = 120, train_batch_size: int = 2, + merge_every: int = 6, log_frequency: Optional[int] = None, save_frequency: Optional[int] = None, save_path: str = "checkpoints/gepa_beam_agent.pkl", - verbose: bool = False, - num_threads: Optional[int] = None): + verbose: bool = False, num_threads: Optional[int] = None, + module_groups: Optional[Dict[str, List[ParameterNode]] | List[List[ParameterNode]]] = None, + selectmodule_policy: str = "round_robin", + budget_B: Optional[int] = None, + accept_epsilon: float = 0.0): num_threads = num_threads or self.num_threads log_frequency = log_frequency or 5 validate_ds = validate_dataset or train_dataset @@ -463,16 +619,15 @@ def train(self, # Seed buffer buffer: List[Candidate] = [] - base_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} - # Evaluate seed - current_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + base_params = _snapshot_params_fast(list(self.optimizer.parameters)) + original = _snapshot_params_fast(list(self.optimizer.parameters)) try: _apply_params(self.optimizer, base_params) vec = evaluate(self.agent, guide, pareto_inputs, pareto_infos, min_score=None, num_threads=num_threads, description="GEPA(beam): seed evaluation") finally: - _apply_params(self.optimizer, current_params) + _apply_params(self.optimizer, original) m0 = float(np.mean(vec)) if all(s is not None for s in vec) else -float("inf") buffer.append(Candidate(params=base_params, eval_vector=vec, mean=m0, id=0, ancestors=set())) tried_merges: set = set() @@ -496,13 +651,13 @@ def train(self, continue # Evaluate child on Pareto subset - current_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + original = _snapshot_params_fast(list(self.optimizer.parameters)) try: _apply_params(self.optimizer, update_dict) vec = evaluate(self.agent, guide, pareto_inputs, pareto_infos, min_score=None, num_threads=num_threads, description="GEPA(beam): child eval") finally: - _apply_params(self.optimizer, current_params) + _apply_params(self.optimizer, original) mean = float(np.mean(vec)) if all(s is not None for s in vec) else -float("inf") buffer.append(Candidate(params=update_dict, eval_vector=vec, mean=mean, id=len(buffer), parent_ids=(parent.id,), ancestors=set(parent.ancestors) | {parent.id})) @@ -545,16 +700,17 @@ class GEPAAlgorithmBase(Trainer): Useful when you want the simplest control loop with your own logging/saving. """ - def __init__(self, - agent, - optimizer=None, - *, - rng_seed: int = 13, - logger=None, - num_threads: Optional[int] = None): + def __init__(self, agent, optimizer=None, *, rng_seed: int = 13, logger=None, + num_threads: Optional[int] = None, + module_groups: Optional[Dict[str, List[ParameterNode]] | List[List[ParameterNode]]] = None, + selectmodule_policy: str = "round_robin"): super().__init__(agent, num_threads=num_threads, logger=logger) self.optimizer = _ensure_optimizer(agent, optimizer) self.rng = random.Random(rng_seed) + np.random.seed(rng_seed) + # self.module_selector = ModuleSelector(self.optimizer.parameters, + # module_groups=module_groups, + # policy=selectmodule_policy) def train(self, guide, @@ -579,8 +735,8 @@ def train(self, # Seed buffer: List[Candidate] = [] - base_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} - original = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + base_params = _snapshot_params_fast(list(self.optimizer.parameters)) + original = _snapshot_params_fast(list(self.optimizer.parameters)) try: _apply_params(self.optimizer, base_params) vec = evaluate(self.agent, guide, xsP, isP, min_score=None, num_threads=num_threads, @@ -608,7 +764,7 @@ def train(self, continue # Eval child - original = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + original = _snapshot_params_fast(list(self.optimizer.parameters)) try: _apply_params(self.optimizer, update_dict) vec = evaluate(self.agent, guide, xsP, isP, min_score=None, num_threads=num_threads, diff --git a/tests/llm_optimizers_tests/test_gepa_benchmark.py b/tests/llm_optimizers_tests/test_gepa_benchmark.py index fdfe5d2e..a9254725 100644 --- a/tests/llm_optimizers_tests/test_gepa_benchmark.py +++ b/tests/llm_optimizers_tests/test_gepa_benchmark.py @@ -66,14 +66,18 @@ def test_gepa_benchmark_gsm8k_real_llm(): # Teacher/judge with a low-cost profile guide = LLMJudge(llm=LLM(profile="cheap")) + # Set a budget constraint for algorithms that support it (e.g., GEPABeamPareto) + budget_limit = 5 + # Agent and optimizer (low-cost profile) agent = Learner(llm=LLM(profile="cheap")) optimizer = OptoPrimeV2(agent.parameters(), llm=LLM(profile="cheap")) algos = [ ("GEPA-Base", GEPAAlgorithmBase(agent, optimizer=optimizer, logger=None, num_threads=2), dict(num_iters=2, train_batch_size=1, merge_every=2)), + (f"GEPA-BeamPareto-Budget{budget_limit}", GEPABeamPareto(agent, optimizer=optimizer, logger=None, num_threads=2), dict(num_search_iterations=2, train_batch_size=1, merge_every=2, budget_B=budget_limit)), + ("GEPA-BeamPareto", GEPABeamPareto(agent, optimizer=optimizer, logger=None, num_threads=2), dict(num_search_iterations=2, train_batch_size=1, merge_every=2)), ("GEPA-UCB", GEPAUCBSearch(agent, optimizer=optimizer, logger=None, num_threads=2), dict(num_search_iterations=2, train_batch_size=1, merge_every=2)), - ("GEPA-Beam", GEPABeamPareto(agent, optimizer=optimizer, logger=None, num_threads=2), dict(num_search_iterations=2, train_batch_size=1, merge_every=2)), ("BasicSearch", BasicSearchAlgorithm(agent, optimizer=optimizer, logger=None, num_threads=2), dict(num_epochs=1, batch_size=1, num_proposals=2)), ] @@ -92,3 +96,61 @@ def test_gepa_benchmark_gsm8k_real_llm(): for v in results.values(): assert isinstance(v, float) + +# @pytest.mark.skipif(not RUN_BENCH, reason="Set RUN_GEPA_BENCH=1 to run this optional benchmark test.") +# def test_gepa_benchmark_gsm8k_low_budget(): +# """Same benchmark test but with a low budget constraint (5 evaluations).""" +# _datasets_or_skip() +# _llm_env_or_skip() + +# import datasets + +# # Load a tiny subset of GSM8k +# ds = datasets.load_dataset("openai/gsm8k", "main") +# train = ds["train"][:3] # Even smaller dataset for low budget +# train_dataset = {"inputs": train["question"], "infos": train["answer"]} + +# # Teacher/judge with a low-cost profile +# guide = LLMJudge(llm=LLM(profile="cheap")) + +# # Test each GEPA variant with budget constraint +# budget_limit = 5 +# algos = [ +# ("GEPA-Base-Budget", GEPAAlgorithmBase, dict(num_iters=1, train_batch_size=1, merge_every=2)), +# ("GEPA-UCB-Budget", GEPAUCBSearch, dict(num_search_iterations=1, train_batch_size=1, merge_every=2)), +# ("GEPA-Beam-Budget", GEPABeamPareto, dict(num_search_iterations=1, train_batch_size=1, merge_every=2, budget_B=budget_limit)), +# ] + +# results = {} +# for name, algo_cls, kwargs in algos: +# # Create fresh agent and optimizer for each test +# agent = Learner(llm=LLM(profile="cheap")) +# optimizer = OptoPrimeV2(agent.parameters(), llm=LLM(profile="cheap")) +# algo = algo_cls(agent, optimizer=optimizer, logger=None, num_threads=1) + +# # Add budget_B to kwargs if supported by the algorithm +# if name == "GEPA-Beam-Budget": +# # GEPABeamPareto supports budget_B parameter +# pass # budget_B already in kwargs + +# try: +# _, best = algo.train( +# guide=guide, +# train_dataset=train_dataset, +# validate_dataset=train_dataset, +# pareto_subset_size=2, # Small Pareto subset to save budget +# num_threads=1, +# **kwargs +# ) +# results[name] = float(best) +# except Exception as e: +# # If budget constraint causes early termination or other issues, record as 0 +# print(f"Algorithm {name} encountered error with budget constraint: {e}") +# results[name] = 0.0 + +# # Sanity check that we produced some results +# assert set(results.keys()) == {"GEPA-Base-Budget", "GEPA-UCB-Budget", "GEPA-Beam-Budget"} +# for v in results.values(): +# assert isinstance(v, float) +# assert v >= 0.0 # Should be non-negative scores + diff --git a/tests/unit_tests/test_gepa_algorithms.py b/tests/unit_tests/test_gepa_algorithms.py index a4c42f26..c435628e 100644 --- a/tests/unit_tests/test_gepa_algorithms.py +++ b/tests/unit_tests/test_gepa_algorithms.py @@ -212,3 +212,260 @@ def test_compare_gepa_vs_basicsearch_on_dummyllm(): assert best_gepa == pytest.approx(1.0, rel=0, abs=1e-6) assert score_gepa == pytest.approx(1.0, rel=0, abs=1e-6) assert score_basic == pytest.approx(1.0, rel=0, abs=1e-6) + + +def test_snapshot_params_fast(): + """Test the fast parameter snapshot utility function.""" + from opto.trainer.algorithms.gepa_algorithms import _snapshot_params_fast + + @trace_model + class MultiTypeAgent: + def __init__(self): + self.int_param = trace_node(42, trainable=True) + self.str_param = trace_node("hello", trainable=True) + self.float_param = trace_node(3.14, trainable=True) + self.list_param = trace_node([1, 2, 3], trainable=True) + self.dict_param = trace_node({"key": "value"}, trainable=True) + # Test numpy array + self.np_param = trace_node(np.array([1, 2, 3]), trainable=True) + + def forward(self, x): + return x + self.int_param + + agent = MultiTypeAgent() + params = list(agent.parameters()) + + # Test snapshot + snapshot = _snapshot_params_fast(params) + + # Check that all parameters are included + assert len(snapshot) == len(params) + + # Modify original values + agent.int_param._set(100) + agent.str_param._set("modified") + agent.np_param._set(np.array([4, 5, 6])) + + # Verify snapshot preserved original values + for p in params: + if p.py_name == "int_param": + assert snapshot[p] == 42 + elif p.py_name == "str_param": + assert snapshot[p] == "hello" + elif p.py_name == "np_param": + assert np.array_equal(snapshot[p], np.array([1, 2, 3])) + + +def test_fingerprint_params(): + """Test the parameter fingerprinting utility function.""" + from opto.trainer.algorithms.gepa_algorithms import _fingerprint_params + + @trace_model + class SimpleAgent: + def __init__(self): + self.a = trace_node(1, trainable=True) + self.b = trace_node("test", trainable=True) + + def forward(self, x): + return x + self.a + + agent = SimpleAgent() + params_dict = {p: p.data for p in agent.parameters()} + + # Test fingerprinting + fp1 = _fingerprint_params(params_dict) + fp2 = _fingerprint_params(params_dict) + + # Same parameters should produce same fingerprint + assert fp1 == fp2 + + # Different parameters should produce different fingerprint + agent.a._set(2) + params_dict2 = {p: p.data for p in agent.parameters()} + fp3 = _fingerprint_params(params_dict2) + assert fp1 != fp3 + + +def test_numpy_seeding_reproducibility(): + """Test that numpy seeding ensures reproducible behavior.""" + target_add = 3 + ds = make_dataset(target_add, n=4) + + # Test with same seed + results = [] + for seed in [123, 123]: # Same seed twice + agent = AddAgent(param=0) + optimizer = build_optimizer(agent, suggest_value=target_add) + algo = GEPAAlgorithmBase(agent=agent, optimizer=optimizer, logger=None, num_threads=1, rng_seed=seed) + + metrics, best = algo.train( + guide=ExactMatchGuide(), + train_dataset=ds, + validate_dataset=ds, + pareto_subset_size=3, + num_iters=2, + train_batch_size=1, + merge_every=2, + num_threads=1, + ) + results.append((metrics, best, agent.param.data)) + + # Results should be identical with same seed + assert results[0][1] == results[1][1] # Same best score + assert results[0][2] == results[1][2] # Same final parameter + + # Test with different seed + agent_diff = AddAgent(param=0) + optimizer_diff = build_optimizer(agent_diff, suggest_value=target_add) + algo_diff = GEPAAlgorithmBase(agent=agent_diff, optimizer=optimizer_diff, logger=None, num_threads=1, rng_seed=456) + + metrics_diff, best_diff = algo_diff.train( + guide=ExactMatchGuide(), + train_dataset=ds, + validate_dataset=ds, + pareto_subset_size=3, + num_iters=2, + train_batch_size=1, + merge_every=2, + num_threads=1, + ) + + # Both should converge but the process might differ + # (though with DummyLLM behavior is very predictable) + assert best_diff == pytest.approx(1.0, rel=0, abs=1e-6) + + +def test_gepa_ucb_pareto_cache(): + """Test Pareto cache functionality in GEPAUCBSearch.""" + target_add = 4 + ds = make_dataset(target_add, n=3) + agent = AddAgent(param=0) + optimizer = build_optimizer(agent, suggest_value=target_add) + + # Test with cache enabled + algo = GEPAUCBSearch(agent=agent, optimizer=optimizer, logger=None, num_threads=1, enable_pareto_cache=True) + + metrics, best = algo.train( + guide=ExactMatchGuide(), + train_dataset=ds, + validate_dataset=ds, + pareto_subset_size=2, + num_search_iterations=2, + train_batch_size=1, + merge_every=2, + num_threads=1, + ) + + # Should converge to perfect solution + assert best == pytest.approx(1.0, rel=0, abs=1e-6) + assert agent.param.data == target_add + + # Test that cache was used (should have some entries) + # Note: exact cache size depends on algorithm behavior, but should be non-empty if enabled + if hasattr(algo, '_pareto_cache'): + assert isinstance(algo._pareto_cache, dict) + + +def test_budget_tracking_functionality(): + """Test budget tracking in GEPA algorithms.""" + target_add = 2 + ds = make_dataset(target_add, n=4) + agent = AddAgent(param=0) + optimizer = build_optimizer(agent, suggest_value=target_add) + + # Test GEPABeamPareto with budget + algo = GEPABeamPareto(agent=agent, optimizer=optimizer, logger=None, num_threads=1) + + metrics, best = algo.train( + guide=ExactMatchGuide(), + train_dataset=ds, + validate_dataset=ds, + pareto_subset_size=3, + num_search_iterations=2, + train_batch_size=1, + merge_every=2, + budget_B=10, # Low budget to test tracking + num_threads=1, + ) + + # Should still achieve good results even with budget constraint + assert isinstance(best, float) + assert best >= 0.0 # Should be non-negative score + + +def test_thread_safety_with_sequential_fallback(): + """Test that algorithms work correctly with sequential fallback when batch_run unavailable.""" + target_add = 1 + ds = make_dataset(target_add, n=2) + agent = AddAgent(param=0) + optimizer = build_optimizer(agent, suggest_value=target_add) + + # Test with num_threads=1 (should use sequential) + algo = GEPAAlgorithmBase(agent=agent, optimizer=optimizer, logger=None, num_threads=1) + metrics, best = algo.train( + guide=ExactMatchGuide(), + train_dataset=ds, + validate_dataset=ds, + pareto_subset_size=2, + num_iters=2, + train_batch_size=1, + merge_every=2, + num_threads=1, + ) + + assert best == pytest.approx(1.0, rel=0, abs=1e-6) + assert agent.param.data == target_add + + # Test with num_threads=2 (may use parallel or fallback to sequential) + agent2 = AddAgent(param=0) + optimizer2 = build_optimizer(agent2, suggest_value=target_add) + algo2 = GEPAAlgorithmBase(agent=agent2, optimizer=optimizer2, logger=None, num_threads=2) + + metrics2, best2 = algo2.train( + guide=ExactMatchGuide(), + train_dataset=ds, + validate_dataset=ds, + pareto_subset_size=2, + num_iters=2, + train_batch_size=1, + merge_every=2, + num_threads=2, + ) + + assert best2 == pytest.approx(1.0, rel=0, abs=1e-6) + assert agent2.param.data == target_add + + +def test_gepa_ucb_selectmodule_policy(): + """Test different module selection policies in GEPAUCBSearch.""" + target_add = 6 + ds = make_dataset(target_add, n=3) + + # Test different selection policies + policies = ["round_robin"] # Could test more if other policies are available + + for policy in policies: + agent = AddAgent(param=0) + optimizer = build_optimizer(agent, suggest_value=target_add) + + algo = GEPAUCBSearch( + agent=agent, + optimizer=optimizer, + logger=None, + num_threads=1, + selectmodule_policy=policy + ) + + metrics, best = algo.train( + guide=ExactMatchGuide(), + train_dataset=ds, + validate_dataset=ds, + pareto_subset_size=2, + num_search_iterations=2, + train_batch_size=1, + merge_every=2, + num_threads=1, + ) + + assert best == pytest.approx(1.0, rel=0, abs=1e-6) + assert agent.param.data == target_add From 76a249ce8a36f73c85d304dd4e97f8de19cba2d7 Mon Sep 17 00:00:00 2001 From: windweller Date: Mon, 22 Sep 2025 10:44:21 -0500 Subject: [PATCH 277/314] fixed the error (due to name shadowing), commented out more test cases for export --- opto/trace/modules.py | 40 +++-- tests/unit_tests/test_modules.py | 270 +++++++++++++++---------------- 2 files changed, 159 insertions(+), 151 deletions(-) diff --git a/opto/trace/modules.py b/opto/trace/modules.py index d27813c4..ffd08e91 100644 --- a/opto/trace/modules.py +++ b/opto/trace/modules.py @@ -75,42 +75,46 @@ def model(cls): >>> m.parameters() returns all trainable parameters >>> m.export('model.py') saves current state as code """ - name = f"{cls.__name__}Model" + new_cls_name = f"{cls.__name__}Model" bases = (cls, Model) + # for export to work, we save the references to the original cls - __TRACE_RESERVED_cls_name = cls.__name__ + cls_name = str(cls.__name__) temp_cls_members = inspect.getmembers(cls) - __TRACE_RESERVED_cls_members = [] - __TRACE_RESERVED_cls_name_to_source = {} + cls_members = [] + cls_member_names = [] + cls_name_to_source = {} for name, member in temp_cls_members: if name.startswith('__TRACE_RESERVED_'): continue if not name.startswith('__'): - __TRACE_RESERVED_cls_members.append((name, member)) + cls_members.append((name, member)) + cls_member_names.append(name) elif name.startswith('__'): try: if hasattr(member, '__qualname__') and cls.__name__ in member.__qualname__: inspect.getsource(member) # additionally we see if this works - __TRACE_RESERVED_cls_members.append((name, member)) + cls_members.append((name, member)) + cls_member_names.append(name) except (AttributeError, TypeError): continue - for name, member in __TRACE_RESERVED_cls_members: + for name, member in cls_members: if 'FunModule' in str(member): # for these class method members, we need to access their content dynamically continue - __TRACE_RESERVED_cls_name_to_source[name] = inspect.getsource(member) + cls_name_to_source[name] = inspect.getsource(member) - new_class = type(name, bases, {}) - new_class.__module__ = cls.__module__ + new_class = type(new_cls_name, bases, {}) + + cls.reserved_cls_name = cls_name + cls.reserved_cls_member_names = cls_member_names + cls.reserved_cls_name_to_source = cls_name_to_source - # for export - new_class.reserved_cls_name = __TRACE_RESERVED_cls_name - new_class.reserved_cls_members = __TRACE_RESERVED_cls_members - new_class.reserved_cls_name_to_source = __TRACE_RESERVED_cls_name_to_source + new_class.__module__ = cls.__module__ mod = sys.modules[cls.__module__] - setattr(mod, name, new_class) + setattr(mod, new_cls_name, new_class) return new_class class Module(ParameterContainer): @@ -329,9 +333,13 @@ def export(self, filename, projections: Optional[List[Projection]] = None): cls = self.__class__ name = cls.reserved_cls_name trace_model_body = f"class {name}:\n" - cls_members = cls.reserved_cls_members + cls_members = inspect.getmembers(self) # cls.reserved_cls_members + cls_member_names = cls.reserved_cls_member_names for i, (name, member) in enumerate(cls_members): + if name not in cls_member_names: + continue + if 'FunModule' in str(member): if member.parameter is not None: source = member.parameter.data diff --git a/tests/unit_tests/test_modules.py b/tests/unit_tests/test_modules.py index 33d589c8..f5a5d6cc 100644 --- a/tests/unit_tests/test_modules.py +++ b/tests/unit_tests/test_modules.py @@ -265,141 +265,141 @@ def non_trainable_method(self, x): def another_non_trainable(self, y): return y + 1 -# def test_export_non_trainable(): -# obj = NonTrainableClass() -# obj._param._data = 10 # Change node value -# obj._param2._data = 20 # Change another node value -# temp_file = "temp_non_trainable.py" -# try: -# obj.export(temp_file) -# with open(temp_file, "r") as f: -# content = f.read() -# # Check if class definition is present -# assert "class NonTrainableClass:" in content -# # Check if node initializations were replaced with current values -# assert "self._param = 10" in content -# assert "self._param2 = 20" in content -# # Verify no node() calls remain -# assert "node(" not in content -# # Verify no bundle decorators remain -# assert "@bundle" not in content -# # Check if methods are present but without decorators -# assert "def non_trainable_method" in content -# assert "def another_non_trainable" in content -# # Check if regular attribute is present -# assert "regular_attr" in content -# finally: -# if os.path.exists(temp_file): -# os.remove(temp_file) - -# def test_export_mixed_trainable(): - -# @model -# class MixedClass: -# def __init__(self): -# super().__init__() -# self._trainable = node(1, trainable=True) -# self._non_trainable = node(2, trainable=False) -# self.regular_attr = "test" - -# @bundle(trainable=True) -# def trainable_method(self, x): -# return x - -# @bundle(trainable=False) -# def non_trainable_method(self, y): -# return y + 1 - - -# obj = MixedClass() -# obj._trainable._data = 100 -# obj._non_trainable._data = 200 - -# obj.trainable_method.parameter._data = "def trainable_method(self, x):\n return x + 3" - -# temp_file = "temp_mixed.py" -# try: -# obj.export(temp_file) -# with open(temp_file, "r") as f: -# content = f.read() -# # Check if class definition is present -# assert "class MixedClass:" in content -# # Check if all node initializations were replaced -# assert "self._trainable = 100" in content -# assert "self._non_trainable = 200" in content -# # Verify no node() calls remain -# assert "node(" not in content -# # Verify no bundle decorators remain -# assert "@bundle" not in content -# # Check if methods are present but without decorators -# assert "def trainable_method" in content -# assert "return x + 3" in content -# assert "def non_trainable_method" in content -# # Check if regular attribute is present -# assert "regular_attr" in content -# finally: -# if os.path.exists(temp_file): -# os.remove(temp_file) - -# def test_export_and_import(): -# @model -# class StrangeCalculator: -# def __init__(self): -# super().__init__() -# self.offset = node(2, trainable=True) -# self.multiplier = node(1.5, trainable=True) - -# @bundle(trainable=True) -# def add(self, x, y): -# """Add two numbers with an offset""" -# return x + y + self.offset - -# @bundle(trainable=True) -# def multiply(self, x, y): -# """Multiply two numbers with a multiplier""" -# return x * y * self.multiplier - -# # Create instance and modify parameters -# calc = StrangeCalculator() -# calc.offset._data = 3 -# calc.multiplier._data = 2.0 -# calc.add.parameter._data = "def add(self, x, y):\n return x + y + self.offset + 1" -# calc.multiply.parameter._data = "def multiply(self, x, y):\n return x * y * self.multiplier * 2" - -# # Dump the model -# temp_file = "temp_calculator.py" -# try: -# calc.export(temp_file) - -# # Import the dumped class -# import importlib.util -# spec = importlib.util.spec_from_file_location("temp_calculator", temp_file) -# temp_module = importlib.util.module_from_spec(spec) -# spec.loader.exec_module(temp_module) - -# # Get the imported class -# ImportedCalculator = temp_module.StrangeCalculator - -# # Create instance and test functionality -# imported_calc = ImportedCalculator() - -# # Test the modified behavior -# result_add = imported_calc.add(5, 3) -# result_multiply = imported_calc.multiply(4, 2) - -# # Verify the results match our expected modified behavior -# # add: 5 + 3 + 3 + 1 = 12 -# # multiply: 4 * 2 * 2.0 * 2 = 32 -# assert result_add == 12, f"Expected 12, got {result_add}" -# assert result_multiply == 32, f"Expected 32, got {result_multiply}" - -# # Verify the attributes have the correct values -# assert imported_calc.offset == 3 -# assert imported_calc.multiplier == 2.0 - -# finally: -# if os.path.exists(temp_file): -# os.remove(temp_file) +def test_export_non_trainable(): + obj = NonTrainableClass() + obj._param._data = 10 # Change node value + obj._param2._data = 20 # Change another node value + temp_file = "temp_non_trainable.py" + try: + obj.export(temp_file) + with open(temp_file, "r") as f: + content = f.read() + # Check if class definition is present + assert "class NonTrainableClass:" in content + # Check if node initializations were replaced with current values + assert "self._param = 10" in content + assert "self._param2 = 20" in content + # Verify no node() calls remain + assert "node(" not in content + # Verify no bundle decorators remain + assert "@bundle" not in content + # Check if methods are present but without decorators + assert "def non_trainable_method" in content + assert "def another_non_trainable" in content + # Check if regular attribute is present + assert "regular_attr" in content + finally: + if os.path.exists(temp_file): + os.remove(temp_file) + +def test_export_mixed_trainable(): + + @model + class MixedClass: + def __init__(self): + super().__init__() + self._trainable = node(1, trainable=True) + self._non_trainable = node(2, trainable=False) + self.regular_attr = "test" + + @bundle(trainable=True) + def trainable_method(self, x): + return x + + @bundle(trainable=False) + def non_trainable_method(self, y): + return y + 1 + + + obj = MixedClass() + obj._trainable._data = 100 + obj._non_trainable._data = 200 + + obj.trainable_method.parameter._data = "def trainable_method(self, x):\n return x + 3" + + temp_file = "temp_mixed.py" + try: + obj.export(temp_file) + with open(temp_file, "r") as f: + content = f.read() + # Check if class definition is present + assert "class MixedClass:" in content + # Check if all node initializations were replaced + assert "self._trainable = 100" in content + assert "self._non_trainable = 200" in content + # Verify no node() calls remain + assert "node(" not in content + # Verify no bundle decorators remain + assert "@bundle" not in content + # Check if methods are present but without decorators + assert "def trainable_method" in content + assert "return x + 3" in content + assert "def non_trainable_method" in content + # Check if regular attribute is present + assert "regular_attr" in content + finally: + if os.path.exists(temp_file): + os.remove(temp_file) + +def test_export_and_import(): + @model + class StrangeCalculator: + def __init__(self): + super().__init__() + self.offset = node(2, trainable=True) + self.multiplier = node(1.5, trainable=True) + + @bundle(trainable=True) + def add(self, x, y): + """Add two numbers with an offset""" + return x + y + self.offset + + @bundle(trainable=True) + def multiply(self, x, y): + """Multiply two numbers with a multiplier""" + return x * y * self.multiplier + + # Create instance and modify parameters + calc = StrangeCalculator() + calc.offset._data = 3 + calc.multiplier._data = 2.0 + calc.add.parameter._data = "def add(self, x, y):\n return x + y + self.offset + 1" + calc.multiply.parameter._data = "def multiply(self, x, y):\n return x * y * self.multiplier * 2" + + # Dump the model + temp_file = "temp_calculator.py" + try: + calc.export(temp_file) + + # Import the dumped class + import importlib.util + spec = importlib.util.spec_from_file_location("temp_calculator", temp_file) + temp_module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(temp_module) + + # Get the imported class + ImportedCalculator = temp_module.StrangeCalculator + + # Create instance and test functionality + imported_calc = ImportedCalculator() + + # Test the modified behavior + result_add = imported_calc.add(5, 3) + result_multiply = imported_calc.multiply(4, 2) + + # Verify the results match our expected modified behavior + # add: 5 + 3 + 3 + 1 = 12 + # multiply: 4 * 2 * 2.0 * 2 = 32 + assert result_add == 12, f"Expected 12, got {result_add}" + assert result_multiply == 32, f"Expected 32, got {result_multiply}" + + # Verify the attributes have the correct values + assert imported_calc.offset == 3 + assert imported_calc.multiplier == 2.0 + + finally: + if os.path.exists(temp_file): + os.remove(temp_file) def test_copy_function(): """Test the copy function of Module class.""" From 0b504f0c0325e88ed630f84b0e00f23c5085f9df Mon Sep 17 00:00:00 2001 From: doxav Date: Mon, 22 Sep 2025 21:15:06 +0200 Subject: [PATCH 278/314] added budget and stabilized parallel training --- opto/trainer/algorithms/gepa_algorithms.py | 77 +++++++++++++------ .../test_gepa_benchmark.py | 58 -------------- 2 files changed, 55 insertions(+), 80 deletions(-) diff --git a/opto/trainer/algorithms/gepa_algorithms.py b/opto/trainer/algorithms/gepa_algorithms.py index 2dac2a7c..c0283e38 100644 --- a/opto/trainer/algorithms/gepa_algorithms.py +++ b/opto/trainer/algorithms/gepa_algorithms.py @@ -10,6 +10,8 @@ import copy import math import random +import functools +import types from dataclasses import dataclass, field from typing import Any, Dict, List, Optional, Tuple @@ -195,9 +197,11 @@ def _maybe_merge_ancestor_aware( rollouts_used = 0 - # Sample training minibatch - tx = rng.choices(train_dataset["inputs"], k=min(train_batch_size, len(train_dataset["inputs"]))) - ti = rng.choices(train_dataset["infos"], k=len(tx)) + # Sample training minibatch (no replacement → lower variance) + k = min(train_batch_size, len(train_dataset["inputs"])) + idxs = np.random.choice(len(train_dataset["inputs"]), k, replace=False) + tx = [train_dataset["inputs"][i] for i in idxs] + ti = [train_dataset["infos"][i] for i in idxs] # Prefer winners for parent selection _compute_pareto_counts(buffer) @@ -228,10 +232,16 @@ def _batch_mean_for(param_dict): _apply_params(optimizer, original) return float(np.mean(vec)) if all(s is not None for s in vec) else -float("inf") - rollouts_used += len(tx) + rollouts_used += k merged_batch_mean = _batch_mean_for(merged_params) parent_means = [_batch_mean_for(ci.params), _batch_mean_for(cj.params)] - rollouts_used += 2 * len(tx) + rollouts_used += 2 * k + + # Early budget guard (3*k minibatch evals) before Pareto eval + if budget_B is not None and budget_tracker is not None: + if budget_tracker["used"] + 3 * k + len(pareto_inputs) > budget_B: + return None + budget_tracker["used"] += 3 * k if merged_batch_mean <= max(parent_means): continue # Not promising enough @@ -272,14 +282,47 @@ def _train_step_generate_child(agent, guide, optimizer, train_xs, train_infos, * Single-parent, incremental evolution "mutation": run forward on a minibatch to get batched feedback, then optimizer.step(bypassing=True) to obtain a new candidate param dict (without applying). """ - use_parallel = (num_threads is not None and num_threads > 1) + use_parallel = (num_threads is not None and num_threads > 1 and batch_run is not None) if use_parallel: - # Use async_run but ensure thread safety through parameter handling - # Since we're working with parameters through optimizer, this should be thread-safe - outputs = async_run([lambda a,x,g,info: standard_optimization_step(a, x, g, info)] * len(train_xs), - args_list=[(agent, x, guide, info) for x, info in zip(train_xs, train_infos)], - max_workers=num_threads, - description="GEPA forward (mutate parent)") + # Pre-bind args → pass callables only. Robust to different batch_run signatures. + callables = [ + functools.partial(standard_optimization_step, agent, x, guide, info) + for x, info in zip(train_xs, train_infos) + ] + try: + outputs = batch_run( + callables, + max_workers=num_threads, + description="GEPA forward (mutate parent)", + ) + except TypeError: + # Fallback: older/other signature (e.g., batch_run(callables, max_workers)) + try: + outputs = batch_run(callables, num_threads) + except Exception: + outputs = None + # Normalize outputs to a list of results. batch_run in different versions may: + # - return the list of results, + # - return a callable that returns the results, + # - return a generator/iterator, + # - or return None. + try: + if callable(outputs): + outputs = outputs() + elif isinstance(outputs, types.GeneratorType): + outputs = list(outputs) + elif outputs is None: + # fallback to sequential evaluation + outputs = [fn() for fn in callables] + elif not isinstance(outputs, (list, tuple)): + # Some other iterable (e.g. map object) + try: + outputs = list(outputs) + except Exception: + outputs = [fn() for fn in callables] + except Exception: + # Any error while normalizing → fallback to sequential + outputs = [fn() for fn in callables] else: # Safe sequential fallback. outputs = [standard_optimization_step(agent, x, guide, info) for x, info in zip(train_xs, train_infos)] @@ -386,10 +429,6 @@ def __init__(self, self._id_counter = 0 self.enable_pareto_cache = enable_pareto_cache self._pareto_cache: Dict[Tuple, Tuple[List[float], float]] = {} - # >>> NEW selector (commented out as ModuleSelector may not exist) - # self.module_selector = ModuleSelector(self.optimizer.parameters, - # module_groups=module_groups, - # policy=selectmodule_policy) def _next_id(self) -> int: self._id_counter += 1 @@ -550,9 +589,6 @@ def __init__(self, agent, optimizer=None, *, rng_seed: int = 11, logger=None, super().__init__(agent, optimizer, num_threads=num_threads, logger=logger) self.rng = random.Random(rng_seed) np.random.seed(rng_seed) - # self.module_selector = ModuleSelector(self.optimizer.parameters, - # module_groups=module_groups, - # policy=selectmodule_policy) # We keep a Pareto select helper that returns (selected_params, wins, scores) def select(self, @@ -708,9 +744,6 @@ def __init__(self, agent, optimizer=None, *, rng_seed: int = 13, logger=None, self.optimizer = _ensure_optimizer(agent, optimizer) self.rng = random.Random(rng_seed) np.random.seed(rng_seed) - # self.module_selector = ModuleSelector(self.optimizer.parameters, - # module_groups=module_groups, - # policy=selectmodule_policy) def train(self, guide, diff --git a/tests/llm_optimizers_tests/test_gepa_benchmark.py b/tests/llm_optimizers_tests/test_gepa_benchmark.py index a9254725..31efa305 100644 --- a/tests/llm_optimizers_tests/test_gepa_benchmark.py +++ b/tests/llm_optimizers_tests/test_gepa_benchmark.py @@ -96,61 +96,3 @@ def test_gepa_benchmark_gsm8k_real_llm(): for v in results.values(): assert isinstance(v, float) - -# @pytest.mark.skipif(not RUN_BENCH, reason="Set RUN_GEPA_BENCH=1 to run this optional benchmark test.") -# def test_gepa_benchmark_gsm8k_low_budget(): -# """Same benchmark test but with a low budget constraint (5 evaluations).""" -# _datasets_or_skip() -# _llm_env_or_skip() - -# import datasets - -# # Load a tiny subset of GSM8k -# ds = datasets.load_dataset("openai/gsm8k", "main") -# train = ds["train"][:3] # Even smaller dataset for low budget -# train_dataset = {"inputs": train["question"], "infos": train["answer"]} - -# # Teacher/judge with a low-cost profile -# guide = LLMJudge(llm=LLM(profile="cheap")) - -# # Test each GEPA variant with budget constraint -# budget_limit = 5 -# algos = [ -# ("GEPA-Base-Budget", GEPAAlgorithmBase, dict(num_iters=1, train_batch_size=1, merge_every=2)), -# ("GEPA-UCB-Budget", GEPAUCBSearch, dict(num_search_iterations=1, train_batch_size=1, merge_every=2)), -# ("GEPA-Beam-Budget", GEPABeamPareto, dict(num_search_iterations=1, train_batch_size=1, merge_every=2, budget_B=budget_limit)), -# ] - -# results = {} -# for name, algo_cls, kwargs in algos: -# # Create fresh agent and optimizer for each test -# agent = Learner(llm=LLM(profile="cheap")) -# optimizer = OptoPrimeV2(agent.parameters(), llm=LLM(profile="cheap")) -# algo = algo_cls(agent, optimizer=optimizer, logger=None, num_threads=1) - -# # Add budget_B to kwargs if supported by the algorithm -# if name == "GEPA-Beam-Budget": -# # GEPABeamPareto supports budget_B parameter -# pass # budget_B already in kwargs - -# try: -# _, best = algo.train( -# guide=guide, -# train_dataset=train_dataset, -# validate_dataset=train_dataset, -# pareto_subset_size=2, # Small Pareto subset to save budget -# num_threads=1, -# **kwargs -# ) -# results[name] = float(best) -# except Exception as e: -# # If budget constraint causes early termination or other issues, record as 0 -# print(f"Algorithm {name} encountered error with budget constraint: {e}") -# results[name] = 0.0 - -# # Sanity check that we produced some results -# assert set(results.keys()) == {"GEPA-Base-Budget", "GEPA-UCB-Budget", "GEPA-Beam-Budget"} -# for v in results.values(): -# assert isinstance(v, float) -# assert v >= 0.0 # Should be non-negative scores - From a937334b2a8781d65786cac2f715cc2edb8083e7 Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 23 Sep 2025 00:00:21 +0000 Subject: [PATCH 279/314] Add num_steps to SearchTemplate --- examples/priority_search_on_convex_fn.py | 3 ++- opto/features/priority_search/priority_search.py | 4 +++- .../priority_search/priority_search_with_regressor.py | 4 +++- opto/features/priority_search/search_template.py | 8 ++++++-- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/examples/priority_search_on_convex_fn.py b/examples/priority_search_on_convex_fn.py index 5d10f501..54c50a6d 100644 --- a/examples/priority_search_on_convex_fn.py +++ b/examples/priority_search_on_convex_fn.py @@ -248,7 +248,8 @@ def get_feedback(self, query: str, response: str, reference=None, **kwargs) -> T logger=logger, score_range=[-10, 10], # trainer kwargs - num_epochs=3*4, + num_epochs=1, + num_steps=5, batch_size=1, num_batches=2, verbose=False, #'output', diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index 7e8d51fa..f4fd1b28 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -276,7 +276,8 @@ def train(self, batch_size = 1, # batch size for updating the agent num_batches = 1, # number of batches to use from the dataset in each iteration score_range = None, # range of (min_score, max_score) to clip the scores; if None, no clipping is applied - num_epochs = 1, # number of training epochs + num_epochs = 1, # number of training epochs (int or None) + num_steps = None, # number of training steps (int or None) num_threads = None, # maximum number of threads to use verbose = False, # whether to print the output of the agent # evaluation @@ -357,6 +358,7 @@ def train(self, num_batches=num_batches, score_range=score_range, num_epochs=num_epochs, + num_steps=num_steps, num_threads=num_threads, verbose=verbose, test_dataset=test_dataset, diff --git a/opto/features/priority_search/priority_search_with_regressor.py b/opto/features/priority_search/priority_search_with_regressor.py index eb6d9f21..21574ed5 100644 --- a/opto/features/priority_search/priority_search_with_regressor.py +++ b/opto/features/priority_search/priority_search_with_regressor.py @@ -22,7 +22,8 @@ def train(self, batch_size = 1, # batch size for updating the agent num_batches = 1, # number of batches to use from the dataset in each iteration score_range = None, # range of (min_score, max_score) to clip the scores; if None, no clipping is applied - num_epochs = 1, # number of training epochs + num_epochs = 1, # number of training epochs (int or None) + num_steps = None, # number of training steps (int or None) num_threads = None, # maximum number of threads to use verbose = False, # whether to print the output of the agent # evaluation @@ -99,6 +100,7 @@ def train(self, num_batches=num_batches, score_range=score_range, num_epochs=num_epochs, + num_steps=num_steps, num_threads=num_threads, verbose=verbose, test_dataset=test_dataset, diff --git a/opto/features/priority_search/search_template.py b/opto/features/priority_search/search_template.py index 00fe7ddd..ec244f74 100644 --- a/opto/features/priority_search/search_template.py +++ b/opto/features/priority_search/search_template.py @@ -129,7 +129,8 @@ def train(self, batch_size = 1, # batch size for updating the agent num_batches = 1, # number of batches to use from the dataset in each iteration score_range = None, # minimum score to update the agent - num_epochs = 1, # number of training epochs + num_epochs = 1, # number of training epochs (int or None) + num_steps = None, # number of training steps (int or None) num_threads = None, # maximum number of threads to use verbose = False, # whether to print the output of the agent # evaluation @@ -195,7 +196,10 @@ def train(self, samples = None train_scores = [] # to store the scores of the agent during training train_num_samples = [] # to store the number of samples used to compute each score - while self.n_epochs < num_epochs : + num_steps = num_steps if num_steps is not None else 0 + num_epochs = num_epochs if num_epochs is not None else 0 + # Train the agent at least for num_epochs or num_steps + while self.n_epochs < num_epochs or self.n_iters < num_steps: print(f"Epoch: {self.n_epochs}. Iteration: {self.n_iters}") From 514f95eaa1f7e15eafc6623f1589b62be8da8488 Mon Sep 17 00:00:00 2001 From: windweller Date: Tue, 23 Sep 2025 12:17:16 -0500 Subject: [PATCH 280/314] finish a tentative graph construction for the LLM call --- opto/features/flows/__init__.py | 0 opto/features/flows/compose.py | 221 +++++++++++ opto/{ => features}/flows/types.py | 4 + opto/flows/__init__.py | 1 - opto/flows/compose.py | 95 ----- tests/features_tests/test_flows_compose.py | 435 +-------------------- 6 files changed, 226 insertions(+), 530 deletions(-) create mode 100644 opto/features/flows/__init__.py create mode 100644 opto/features/flows/compose.py rename opto/{ => features}/flows/types.py (96%) delete mode 100644 opto/flows/__init__.py delete mode 100644 opto/flows/compose.py diff --git a/opto/features/flows/__init__.py b/opto/features/flows/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/opto/features/flows/compose.py b/opto/features/flows/compose.py new file mode 100644 index 00000000..6a6e6479 --- /dev/null +++ b/opto/features/flows/compose.py @@ -0,0 +1,221 @@ +import pydantic +from pydantic import BaseModel, ValidationError, Field, create_model +import opto.trace as trace +from typing import Union, get_type_hints, Any, Dict, List, Optional +from opto.utils.llm import AbstractModel, LLM +from opto.features.flows.types import TraceObject +from opto.optimizers.utils import extract_xml_like_data +import inspect +import json +import re + +""" +TracedLLM: +1. special operations that supports specifying inputs (system_prompt, user_prompt) to LLM and parsing of outputs, wrap + everything under one command. +2. Easy to use interface -- can be inherited by users. +3. Support multi-turn chatting (message history) + +Usage patterns: + +Direct use: (only supports single input, single output) (signature: str -> str) +llm = TracedLLM("You are a helpful assistant.") +response = llm("Hello, what's the weather in France today?") +""" + + +class ChatHistory(TraceObject): + def __init__(self, max_len=10, auto_summary=False): + """Initialize chat history for multi-turn conversation. + + Args: + max_len: Maximum number of messages to keep in history + auto_summary: Whether to automatically summarize old messages + """ + self.messages = [] + self.max_len = max_len + self.auto_summary = auto_summary + + def add(self, content, role): + """Add a message to history with role validation. + + Args: + content: The content of the message + role: The role of the message ("user" or "assistant") + """ + if role not in ["user", "assistant"]: + raise ValueError(f"Invalid role '{role}'. Must be 'user' or 'assistant'.") + + # Check for alternating user/assistant pattern + if len(self.messages) > 0: + last_msg = self.messages[-1] + if last_msg["role"] == role: + print(f"Warning: Adding consecutive {role} messages. Consider alternating user/assistant messages.") + + self.messages.append({"role": role, "content": content}) + self._trim_history() + + def _trim_history(self): + """Trim history to max_len while preserving first user message.""" + if len(self.messages) <= self.max_len: + return + + # Find first user message index + first_user_idx = None + for i, msg in enumerate(self.messages): + if msg["role"] == "user": + first_user_idx = i + break + + # Keep first user message + protected_messages = [] + if first_user_idx is not None: + first_user_msg = self.messages[first_user_idx] + protected_messages.append(first_user_msg) + + # Calculate how many recent messages we can keep + remaining_slots = self.max_len - len(protected_messages) + if remaining_slots > 0: + # Get recent messages + recent_messages = self.messages[-remaining_slots:] + # Avoid duplicating first user message + if first_user_idx is not None: + first_user_msg = self.messages[first_user_idx] + recent_messages = [msg for msg in recent_messages if msg != first_user_msg] + + self.messages = protected_messages + recent_messages + else: + self.messages = protected_messages + + def get_messages(self, system_prompt: Optional[Union[str, trace.Node]] = None): + """Get messages from history. + + Args: + system_prompt: If this is passed in, then we construct a node/graph that + builds system_prompt -> chat_history graph + + Returns: + List of messages + """ + + @trace.bundle() + def converse_with_llm(system_prompt: Union[str, trace.Node]): + """The conversation history with the LLM using the given system prompt. + Args: + system_prompt: The system prompt to use for the conversation. + Returns: + The conversation history from the LLM. + """ + return self + + if system_prompt is None: + return self.messages.copy() + else: + return converse_with_llm(system_prompt) + + def __str__(self): + """String representation of the chat history. Mostly for the optimizer.""" + if len(self.messages) == 0: + return "There is no chat history so far." + + lines = [">>ChatHistory<<"] + + for msg in self.messages: + role = msg["role"] + content = msg["content"] + + if role == "user": + lines.append(f"User: {content}") + elif role == "assistant": + lines.append(f"Assistant: {content}") + + lines.append(">>End<<") + return "\n".join(lines) + + +@trace.bundle(catch_execution_error=False) +def call_llm(llm, system_prompt: str, user_prompt: str, chat_history: Optional[ChatHistory] = None, **kwargs) -> str: + """Call the LLM model. + + Args: + llm: The language model to use for generating responses. + system_prompt: the system prompt to the agent. By tuning this prompt, we can control the behavior of the agent. For example, it can be used to provide instructions to the agent (such as how to reason about the problem, how to use tools, how to answer the question), or provide in-context examples of how to solve the problem. + user_prompt: the input to the agent. It can be a query, a task, a code, etc. + chat_history: The conversation between the user and LLM so far. Can be empty. + Returns: + The response from the agent. + """ + messages = [] + if system_prompt is not None: + messages.append({"role": "system", "content": system_prompt}) + + messages.extend(chat_history.get_messages()) + messages.append({"role": "user", "content": user_prompt}) + + # TODO auto-parsing results + response = llm(messages=messages, **kwargs) + return response.choices[0].message.content + + +DEFAULT_SYSTEM_PROMPT_DESCRIPTION = ("the system prompt to the agent. By tuning this prompt, we can control the " + "behavior of the agent. For example, it can be used to provide instructions to " + "the agent (such as how to reason about the problem, how to use tools, " + "how to answer the question), or provide in-context examples of how to solve the " + "problem.") + + +@trace.model +class TracedLLM: + def __init__(self, + system_prompt: Union[str, None, trace.Node] = None, + llm: AbstractModel = None, chat_history_on=False, + trainable=False): + """Initialize TracedLLM with a system prompt. + + Args: + system_prompt: The system prompt to use for LLM calls. If None and the class has a docstring, the docstring will be used. + llm: The LLM model to use for inference + chat_history_on: if on, maintain chat history for multi-turn conversations + """ + if system_prompt is None: + system_prompt = "You are a helpful assistant." + + self.system_prompt = system_prompt if isinstance(system_prompt, trace.Node) else trace.node(system_prompt, + name='system_prompt', + description=DEFAULT_SYSTEM_PROMPT_DESCRIPTION, + trainable=trainable) + if llm is None: + llm = LLM() + assert isinstance(llm, AbstractModel), f"{llm} must be an instance of AbstractModel" + self.llm = llm + self.chat_history = ChatHistory() + + def forward(self, user_query: str, **kwargs) -> str: + """Main function that handles both direct call and inheritance patterns. + + Args: + *args: For direct pattern - single string argument + **kwargs: For inheritance pattern - named input fields + + Returns: + str: For direct pattern + TracedResponse: For inheritance pattern with structured output fields + """ + messages = [] + messages.append({"role": "system", "content": self.system_prompt.data}) + messages.extend(self.chat_history.get_messages()) + messages.append({"role": "user", "content": user_query}) + + response = self.llm(messages=messages, **kwargs) + + @trace.bundle() + def call_llm(chat_history: ChatHistory, user_query: str) -> str: + """Call the LLM model. + Args: + user_query + Returns: + response from the LLM + """ + return response.choices[0].message.content + + return call_llm(self.chat_history.get_messages(self.system_prompt), user_query) \ No newline at end of file diff --git a/opto/flows/types.py b/opto/features/flows/types.py similarity index 96% rename from opto/flows/types.py rename to opto/features/flows/types.py index b763330f..e79fafd7 100644 --- a/opto/flows/types.py +++ b/opto/features/flows/types.py @@ -4,6 +4,10 @@ import re import json +class TraceObject: + def __str__(self): + # Any subclass that inherits this will be friendly to the optimizer + raise NotImplementedError("Subclasses must implement __str__") class TracedInput(BaseModel): """Pydantic model for input fields in TracedLLM inheritance pattern.""" diff --git a/opto/flows/__init__.py b/opto/flows/__init__.py deleted file mode 100644 index d865a711..00000000 --- a/opto/flows/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from opto.flows.compose import TracedLLM \ No newline at end of file diff --git a/opto/flows/compose.py b/opto/flows/compose.py deleted file mode 100644 index a5476e06..00000000 --- a/opto/flows/compose.py +++ /dev/null @@ -1,95 +0,0 @@ -import pydantic -from pydantic import BaseModel, ValidationError, Field, create_model -import opto.trace as trace -from typing import Union, get_type_hints, Any, Dict, List -from opto.utils.llm import AbstractModel, LLM -from opto.flows.types import TracedInput, TracedOutput, DynamicModelMixin -from opto.optimizers.utils import extract_xml_like_data -import inspect -import json -import re - - -""" -TracedLLM: -1. special operations that supports specifying inputs (system_prompt, user_prompt) to LLM and parsing of outputs, wrap - everything under one command. -2. Easy to use interface -- can be inherited by users. -3. Support multi-turn chatting (message history) - -Usage patterns: - -Direct use: (only supports single input, single output) (signature: str -> str) -llm = TracedLLM("You are a helpful assistant.") -response = llm("Hello, what's the weather in France today?") -""" - -@trace.bundle(catch_execution_error=False) -def call_llm(llm, system_prompt: str, *user_prompts: List[str], **kwargs) -> str: - """Call the LLM model. - - Args: - llm: The language model to use for generating responses. - system_prompt: the system prompt to the agent. By tuning this prompt, we can control the behavior of the agent. For example, it can be used to provide instructions to the agent (such as how to reason about the problem, how to use tools, how to answer the question), or provide in-context examples of how to solve the problem. - user_prompt: the input to the agent. It can be a query, a task, a code, etc. - Returns: - The response from the agent. - """ - messages = [] - if system_prompt is not None: - messages.append({"role": "system", "content": system_prompt}) - for user_prompt in user_prompts: - messages.append({"role": "user", "content": user_prompt}) - # TODO auto-parsing results - response = llm(messages=messages, **kwargs) - return response.choices[0].message.content - -@trace.model -class TracedLLM(DynamicModelMixin): - def __init__(self, - system_prompt: Union[str, None, trace.Node] = None, - llm: AbstractModel = None, chat_history_on=False): - """Initialize TracedLLM with a system prompt. - - Args: - system_prompt: The system prompt to use for LLM calls. If None and the class has a docstring, the docstring will be used. - llm: The LLM model to use for inference - chat_history_on: if on, maintain chat history for multi-turn conversations - """ - # Use class docstring as system prompt if none provided - if system_prompt is None: - class_docstring = self.__class__.__doc__ - if class_docstring and class_docstring.strip(): - system_prompt = class_docstring.strip() - - self.system_prompt = trace.node(system_prompt) - if llm is None: - llm = LLM() - assert isinstance(llm, AbstractModel), f"{llm} must be an instance of AbstractModel" - self.llm = llm - - # Initialize fields for structured input/output - self._input_fields = [] - self._output_fields = [] - self._field_types = {} # Store type annotations for each field - - def forward(self, *args, **kwargs) -> str: - """Main function that handles both direct call and inheritance patterns. - - Args: - *args: For direct pattern - single string argument - **kwargs: For inheritance pattern - named input fields - - Returns: - str: For direct pattern - TracedResponse: For inheritance pattern with structured output fields - """ - # Direct pattern: single string argument - if len(args) == 1 and isinstance(args[0], str): - return self._call_llm(args[0]) - else: - raise ValueError("Direct usage requires a single string argument") - - def _call_llm(self, user_prompt: str) -> str: - """Call the LLM with user prompt and system prompt.""" - return call_llm(self.llm, self.system_prompt, user_prompt) diff --git a/tests/features_tests/test_flows_compose.py b/tests/features_tests/test_flows_compose.py index 9d2efdcc..3571ca6a 100644 --- a/tests/features_tests/test_flows_compose.py +++ b/tests/features_tests/test_flows_compose.py @@ -1,8 +1,5 @@ import pytest -import os -from unittest.mock import patch, MagicMock, Mock -from opto.flows.compose import TracedLLM, TracedResponse -from opto.flows.types import TracedInput, TracedOutput +from unittest.mock import patch, Mock # Mock LLM at module level to ensure no real API calls @@ -25,433 +22,3 @@ def mock_trace_operators(): yield mock_call_llm -class TestTracedLLM: - """Test cases for TracedLLM functionality.""" - - def test_basic_initialization(self): - """Test basic TracedLLM initialization.""" - llm = TracedLLM("You are a helpful assistant") - assert llm.system_prompt.data == "You are a helpful assistant" - assert llm._input_fields == [] - assert llm._output_fields == [] - assert llm._field_types == {} - - def test_docstring_as_system_prompt(self): - """Test that class docstring is used as system prompt when none provided.""" - class TestLLM(TracedLLM): - """This is a test LLM for testing purposes""" - pass - - llm = TestLLM() - assert llm.system_prompt.data == "This is a test LLM for testing purposes" - - def test_explicit_system_prompt_overrides_docstring(self): - """Test that explicit system prompt overrides docstring.""" - class TestLLM(TracedLLM): - """This is a test LLM""" - pass - - llm = TestLLM("Custom prompt") - assert llm.system_prompt.data == "Custom prompt" - - def test_field_detection_basic(self): - """Test basic field detection for input and output fields.""" - class BasicScorer(TracedLLM): - """Basic document scorer""" - doc: str = TracedInput(description="Document to score") - score: int = TracedOutput(description="Score from 1-10") - - scorer = BasicScorer() - assert scorer._input_fields == ['doc'] - assert scorer._output_fields == ['score'] - assert scorer._field_types == {'doc': str, 'score': int} - - def test_field_detection_multiple_fields(self): - """Test field detection with multiple input/output fields.""" - class MultiFieldScorer(TracedLLM): - """Multi-field scorer""" - doc: str = TracedInput(description="Document") - context: str = TracedInput(description="Context") - score: int = TracedOutput(description="Score") - confidence: float = TracedOutput(description="Confidence") - tags: list = TracedOutput(description="Tags") - - scorer = MultiFieldScorer() - assert set(scorer._input_fields) == {'doc', 'context'} - assert set(scorer._output_fields) == {'score', 'confidence', 'tags'} - assert scorer._field_types['doc'] == str - assert scorer._field_types['score'] == int - assert scorer._field_types['confidence'] == float - assert scorer._field_types['tags'] == list - - def test_direct_pattern_call(self, mock_trace_operators): - """Test direct usage pattern (no inheritance fields).""" - mock_trace_operators.return_value = "Hello! The weather is sunny." - - llm = TracedLLM("You are a helpful assistant") - response = llm("Hello, what's the weather today?") - - assert response == "Hello! The weather is sunny." - mock_trace_operators.assert_called_once() - - def test_inheritance_pattern_call(self, mock_trace_operators): - """Test inheritance pattern with structured input/output.""" - mock_trace_operators.return_value = "The score is 8 out of 10" - - class Scorer(TracedLLM): - """Score documents""" - doc: str = TracedInput(description="Document to score") - score: int = TracedOutput( - description="Score from 1-10", - parser=r"score[:\s]*is[:\s]*(\d+)|(\d+)\s*out\s*of" - ) - - scorer = Scorer() - response = scorer(doc="This is a great document") - - assert isinstance(response, TracedResponse) - assert response.score == 8 - mock_trace_operators.assert_called_once() - - def test_dynamic_response_model_creation(self): - """Test dynamic Pydantic model creation.""" - class TestScorer(TracedLLM): - """Test scorer""" - doc: str = TracedInput(description="Document") - score: int = TracedOutput(description="Score") - confidence: float = TracedOutput(description="Confidence") - - scorer = TestScorer() - ResponseModel = scorer._create_dynamic_response_model() - - assert ResponseModel.__name__ == "TestScorerResponse" - assert 'score' in ResponseModel.model_fields - assert 'confidence' in ResponseModel.model_fields - assert ResponseModel.model_fields['score'].annotation == int - assert ResponseModel.model_fields['confidence'].annotation == float - - def test_json_extraction(self): - """Test JSON response extraction.""" - class Scorer(TracedLLM): - """Test scorer""" - doc: str = TracedInput() - score: int = TracedOutput() - - scorer = Scorer() - json_response = '{"score": 9}' - extracted = scorer._extract_structured_data(json_response) - - assert extracted == {'score': 9} - - def test_text_extraction_with_patterns(self): - """Test text extraction using field name patterns.""" - class Scorer(TracedLLM): - """Test scorer""" - doc: str = TracedInput() - score: int = TracedOutput(parser=r"score[:\s]*is[:\s]*(\d+)|(\d+)\s*out\s*of") - - scorer = Scorer() - text_response = "The score is 7 out of 10" - extracted = scorer._extract_structured_data(text_response) - - assert extracted == {'score': 7} - - -class TestTracedInput: - """Test cases for TracedInput.""" - - def test_basic_initialization(self): - """Test basic TracedInput initialization.""" - input_field = TracedInput(description="Test input") - assert input_field.description == "Test input" - assert input_field.required == True - - def test_optional_field(self): - """Test optional TracedInput field.""" - input_field = TracedInput(description="Optional input", required=False) - assert input_field.required == False - - -class TestTracedOutput: - """Test cases for TracedOutput.""" - - def test_basic_initialization(self): - """Test basic TracedOutput initialization.""" - output_field = TracedOutput(description="Test output") - assert output_field.description == "Test output" - assert output_field.required == True - assert output_field.parser is None - assert output_field.default_value is None - - def test_with_default_value(self): - """Test TracedOutput with default value.""" - output_field = TracedOutput(description="Score", default_value=5) - assert output_field.default_value == 5 - - def test_regex_parser_extraction(self): - """Test extraction using regex parser.""" - output_field = TracedOutput( - description="Rating", - parser=r"(\d+)/5|rating[:\s]+(\d+)", - default_value=0 - ) - - # Test successful extraction - result = output_field.extract_from_text("The rating is 4/5 stars", int) - assert result == 4 - - # Test fallback to default - result = output_field.extract_from_text("No rating information", int) - assert result == 0 - - def test_function_parser_extraction(self): - """Test extraction using function parser.""" - def sentiment_parser(text): - if "good" in text.lower(): - return "Positive" - elif "bad" in text.lower(): - return "Negative" - else: - return "Neutral" - - output_field = TracedOutput( - description="Sentiment", - parser=sentiment_parser, - default_value="Unknown" - ) - - # Test successful extraction - result = output_field.extract_from_text("This is a good product", str) - assert result == "Positive" - - result = output_field.extract_from_text("This is a bad product", str) - assert result == "Negative" - - # Test parser exception (should return default) - def failing_parser(text): - raise Exception("Parser error") - - output_field_with_failing_parser = TracedOutput( - description="Sentiment", - parser=failing_parser, - default_value="Unknown" - ) - result = output_field_with_failing_parser.extract_from_text("Some text", str) - assert result == "Unknown" - - def test_boolean_parsing(self): - """Test boolean value parsing.""" - output_field = TracedOutput(default_value=False) - - # Test positive cases - assert output_field._parse_boolean("true") == True - assert output_field._parse_boolean("yes") == True - assert output_field._parse_boolean("positive") == True - assert output_field._parse_boolean("definitely") == True - - # Test negative cases - assert output_field._parse_boolean("false") == False - assert output_field._parse_boolean("no") == False - assert output_field._parse_boolean("negative") == False - assert output_field._parse_boolean("no way") == False - - # Test default case - assert output_field._parse_boolean("unclear") == False - - def test_type_conversion(self): - """Test automatic type conversion.""" - output_field = TracedOutput(default_value=0) - - # Test int conversion - assert output_field._convert_to_type("42", int) == 42 - assert output_field._convert_to_type("Score: 8", int) == 8 - assert output_field._convert_to_type("No numbers", int) == 0 # default - - # Test float conversion - assert output_field._convert_to_type("3.14", float) == 3.14 - assert output_field._convert_to_type("Rating: 4.5", float) == 4.5 - - # Test list conversion - assert output_field._convert_to_type('["a", "b", "c"]', list) == ["a", "b", "c"] - assert output_field._convert_to_type("a, b, c", list) == ["a", "b", "c"] - - -class TestDynamicModelMixin: - """Test cases for DynamicModelMixin.""" - - def test_create_response_model(self): - """Test dynamic response model creation.""" - from opto.flows.types import DynamicModelMixin - - class TestClass(DynamicModelMixin): - pass - - field_defs = { - 'score': (int, TracedOutput(description="Score value", default_value=0)), - 'tags': (list, TracedOutput(description="Tag list", required=False, default_value=[])) - } - - ResponseModel = TestClass.create_response_model(field_defs) - - assert ResponseModel.__name__ == "TestClassResponse" - assert 'score' in ResponseModel.model_fields - assert 'tags' in ResponseModel.model_fields - assert ResponseModel.model_fields['score'].annotation == int - assert ResponseModel.model_fields['tags'].annotation == list - - def test_create_input_model(self): - """Test dynamic input model creation.""" - from opto.flows.types import DynamicModelMixin - - class TestClass(DynamicModelMixin): - pass - - field_defs = { - 'doc': (str, TracedInput(description="Document", required=True)), - 'context': (str, TracedInput(description="Context", required=False)) - } - - InputModel = TestClass.create_input_model(field_defs) - - assert InputModel.__name__ == "TestClassInput" - assert 'doc' in InputModel.model_fields - assert 'context' in InputModel.model_fields - - -class TestTracedResponse: - """Test cases for TracedResponse.""" - - def test_dynamic_attribute_setting(self): - """Test that TracedResponse allows dynamic attribute setting.""" - response = TracedResponse(score=8, confidence=0.85, tags=["good", "clear"]) - - assert response.score == 8 - assert response.confidence == 0.85 - assert response.tags == ["good", "clear"] - - -class TestIntegration: - """Integration tests for the complete flows system.""" - - def test_end_to_end_workflow(self, mock_trace_operators): - """Test complete end-to-end workflow.""" - mock_trace_operators.return_value = "Score: 9, Sentiment: Positive, Confidence: 90%" - - class DocumentAnalyzer(TracedLLM): - """Analyze documents comprehensively""" - document: str = TracedInput(description="Document to analyze") - score: int = TracedOutput( - description="Quality score 1-10", - parser=r"score[:\s]+(\d+)", - default_value=5 - ) - sentiment: str = TracedOutput( - description="Sentiment analysis", - parser=lambda text: "Positive" if "positive" in text.lower() else "Negative", - default_value="Neutral" - ) - confidence: float = TracedOutput( - description="Confidence percentage", - parser=r"confidence[:\s]+(\d+)%?", - default_value=0.5 - ) - - analyzer = DocumentAnalyzer() - - # Test field detection - assert set(analyzer._input_fields) == {'document'} - assert set(analyzer._output_fields) == {'score', 'sentiment', 'confidence'} - - # Test analysis - response = analyzer(document="This is a test document") - - assert isinstance(response, TracedResponse) - assert response.score == 9 - assert response.sentiment == "Positive" - assert response.confidence == 90.0 - - # Verify LLM was called correctly - mock_trace_operators.assert_called_once() - args, kwargs = mock_trace_operators.call_args - assert "This is a test document" in args - - -class TestCICompatibility: - """Tests specifically designed for CI/CD environments without API keys.""" - - def test_no_real_api_calls_made(self): - """Ensure no real API calls are made during testing.""" - # This test verifies that our mocking is working correctly - class SimpleScorer(TracedLLM): - """Simple scorer""" - text: str = TracedInput(description="Text input") - score: int = TracedOutput(description="Score output", default_value=5) - - scorer = SimpleScorer() - - # This should not fail even without API keys because everything is mocked - assert scorer.system_prompt.data == "Simple scorer" - assert scorer._input_fields == ['text'] - assert scorer._output_fields == ['score'] - - def test_offline_functionality(self): - """Test functionality that doesn't require any external services.""" - # Test type extraction - output_field = TracedOutput(parser=r"score[:\s]*is[:\s]*(\d+)", default_value=0) - result = output_field.extract_from_text("The score is 85", int) - assert result == 85 - - # Test boolean parsing - bool_field = TracedOutput(default_value=False) - assert bool_field._parse_boolean("yes") == True - assert bool_field._parse_boolean("no") == False - - # Test type conversion - assert output_field._convert_to_type("42", int) == 42 - assert output_field._convert_to_type("3.14", float) == 3.14 - - def test_mock_verification(self, mock_trace_operators): - """Verify that mocking is working as expected.""" - # Check that the mock is active - assert mock_trace_operators is not None - - # Create a TracedLLM instance - llm = TracedLLM("Test prompt") - - # This should use the mock, not real API - mock_trace_operators.return_value = "Mocked response" - response = llm("Test input") - - assert response == "Mocked response" - mock_trace_operators.assert_called_once() - - @pytest.mark.skipif( - os.getenv('GITHUB_ACTIONS') == 'true' and not os.getenv('OPENAI_API_KEY'), - reason="Skipping in GitHub Actions without API key" - ) - def test_optional_real_api_integration(self): - """Optional test that can be skipped in CI without API keys.""" - # This test is automatically skipped in GitHub Actions if no API key is set - # It can be useful for local testing with real APIs - pytest.skip("Real API integration test - skipped for CI safety") - - def test_boolean_parsing_delegates_to_traced_output(self, mock_trace_operators): - """Test that boolean parsing properly delegates to TracedOutput when available.""" - mock_trace_operators.return_value = "answer: yes" # More structured format - - class BooleanTester(TracedLLM): - """Test boolean delegation""" - question: str = TracedInput(description="Question to ask") - answer: bool = TracedOutput( - description="Boolean answer", - parser=r"answer[:\s]*([^\n,]+)", # Add explicit parser to extract "yes" - default_value=False # This should be used by TracedOutput._parse_boolean - ) - - tester = BooleanTester() - response = tester(question="Is this working?") - - # The TracedOutput._parse_boolean should handle the parsing with its default_value logic - assert isinstance(response, TracedResponse) - # Since "yes" is in positive_words, it should return True regardless of default_value - assert response.answer == True From 7ca339f4b6a70ca52c0ee7db15e630b2ce9bf693 Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 23 Sep 2025 18:57:29 +0000 Subject: [PATCH 281/314] Move assertion into match_candidates_and_samples --- opto/features/priority_search/priority_search.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index f4fd1b28..04d03f33 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -550,8 +550,7 @@ def propose(self, matched_candidates_and_samples = self.match_candidates_and_samples(self._exploration_candidates, samples) # NOTE len(matched_candidates_and_samples) <= len(self._exploration_candidates) since some exploration candidates might be duplicated. candidate_batchrollouts_list = [ (k,b) for k, v in matched_candidates_and_samples.items() for b in v] - assert len(samples) == len(candidate_batchrollouts_list), "All samples must be associated with exploration candidates." - n_batches = len(samples) # number of batch rollouts in the samples + n_batches = len(candidate_batchrollouts_list) # number of batch rollouts in the samples # need to copy optimizer for the n_batches def _backward(n): @@ -690,6 +689,7 @@ def match_candidates_and_samples( for c in candidates: assert len(_results[c]) > 0, f"ModuleCandidate with id {id(c)} has no rollouts. Samples are not collected by known candidates." + assert len(samples) == sum(len(rollouts) for rollouts in _results.values()), "All samples must be associated with exploration candidates." return _results def update_memory(self, validate_results, verbose: bool = False, **kwargs): From 11360d1b286290e16e870b5e1ad60f18318b6d3d Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 23 Sep 2025 18:59:24 +0000 Subject: [PATCH 282/314] Add StreamingPrioritySearch --- .../streaming_priority_search.py | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 opto/features/priority_search/streaming_priority_search.py diff --git a/opto/features/priority_search/streaming_priority_search.py b/opto/features/priority_search/streaming_priority_search.py new file mode 100644 index 00000000..3f555b66 --- /dev/null +++ b/opto/features/priority_search/streaming_priority_search.py @@ -0,0 +1,97 @@ + +import numpy as np +from typing import List +from opto.features.priority_search.search_template import Samples, BatchRollout, save_train_config +from opto.features.priority_search.priority_search import PrioritySearch, ModuleCandidate + + + +class StreamingPrioritySearch(PrioritySearch): + """ A variant of PrioritySearch that uses only the most recent samples for proposing new candidates. + It overrides the `propose` method to match candidates with samples differently. + """ + + @save_train_config + def train(self, + *args, + exploration_ratio = 0.5, + exploration_temperature = 1.0, + **kwargs): + assert 0 < exploration_ratio <= 1, "exploration_ratio must be in (0, 1]." + self._exploration_ratio = exploration_ratio + self._exploration_temperature = exploration_temperature + return super().train(*args, **kwargs) + + def match_candidates_and_samples( + self, + candidates: List[ModuleCandidate], + samples: List[BatchRollout]): + """ + Match the given candidates with the provided samples. + + Args: + candidates (list of ModuleCandidate): A list of ModuleCandidate objects representing the proposed parameters. + samples (list of BatchRollout): A Samples object containing a list of BatchRollout objects, where each BatchRollout contains rollouts collected by an agent on different inputs. + Returns: + results (dict): A dictionary where the keys are ModuleCandidate objects and the values are lists of BatchRollouts collected by the corresponding ModuleCandidate. + + """ + # NOTE since we overwrite validate, this function is only called in propose + + # Associate each BatchRollout candidates + matched_candidates_and_samples = super().match_candidates_and_samples(candidates, samples) + + # Update the candidates with all the rollouts collected so far so we can compute their scores + results = {} # dict of ModuleCandidate id: (ModuleCandidate, list of rollouts) + for c, rollouts in matched_candidates_and_samples.items(): # rollouts is a list of BatchRollouts + results[c] = [ r for rr in rollouts for r in rr.to_list()] # we only need the list of dicts + for candidate, rollouts in results.items(): + candidate.add_rollouts(rollouts) # add the rollouts to the candidate + + # set exploration candidates to those with merged stats (they are the ones that will be added to memory) + self._exploration_candidates = list(results.keys()) + + # Now these candidates have all the rollouts collected so far + # compute the score for each candidate using compute_exploration_priority + candidate_batchrollouts_list = [ (k,b) for k, v in matched_candidates_and_samples.items() for b in v] + scores = [self.compute_exploration_priority(c) for c, _ in candidate_batchrollouts_list] + + # We use the top K to improve over, where K is determined by exploration_ratio. + K = int(self.num_candidates * self._exploration_ratio) + K += max(0, self.num_candidates - len(self._exploration_candidates) - K - len(self.memory)) # ensure we have enough candidates to explore + # Randomly sample K candidates from the pool + if len(candidate_batchrollouts_list) <= K: + return matched_candidates_and_samples + weight = np.array(scores)/self._exploration_temperature + weight = np.exp(weight - np.max(weight)) + indices = np.random.choice(len(candidate_batchrollouts_list), size=K, replace=False, p=weight/np.sum(weight)) + candidate_batchrollouts_list = [candidate_batchrollouts_list[i] for i in indices] + # candidate_batchrollouts_list = sorted(candidate_batchrollouts_list, key=lambda x: scores[candidate_batchrollouts_list.index(x)], reverse=True)[:K] + assert len(candidate_batchrollouts_list) == K, f"Number of selected candidates {len(candidate_batchrollouts_list)} must be equal to K {K}." + # convert it back to the format of matched_candidates_and_samples + matched_candidates_and_samples = {c: [] for c, _ in candidate_batchrollouts_list} + for c, b in candidate_batchrollouts_list: + matched_candidates_and_samples[c].append(b) + return matched_candidates_and_samples + + + def validate(self, + candidates: List[ModuleCandidate], + samples: Samples, + verbose: bool = False, + **kwargs): + print("--- Skip validating candidates...") if verbose else None + exploration_candidates = self._exploration_candidates # exploration candidates from the previous iteration + assert self._exploration_candidates is not None, "exploration_candidates must be set before calling validate." + results = {c: [] for c in (exploration_candidates + candidates)} # dict of ModuleCandidate id: (ModuleCandidate, list of rollouts) + print(f'Adding {len(exploration_candidates)} exploration candidates and {len(candidates)} proposed candidates to validate results.') + assert len(candidates) <= self.num_candidates, f"Number of proposed candidates {len(candidates)} must be less than num_candidates {self.num_candidates}." + if len(candidates) == self.num_candidates: + print("Warning: Number of proposed candidates is equal to num_candidates. Running in pure exploration mode.") + assert len(results) == len(exploration_candidates) + len(candidates), "Number of candidates in results must match the number of exploration candidates and proposed candidates." + return results + + def compute_exploration_priority(self, candidate) -> float: + if candidate.num_rollouts == 0: + return self.max_score # candidates with no rollouts have the highest priority + return super().compute_exploration_priority(candidate) \ No newline at end of file From ac13301ef1e1c411649efc126ce91641057e8628 Mon Sep 17 00:00:00 2001 From: chinganc Date: Wed, 24 Sep 2025 19:59:32 +0000 Subject: [PATCH 283/314] Fix the bug of PS using optimizer with memory - Make the copied modules' parameter nodes have the same as the original one, so that optimizer's memory works. - Add a flag to allow using the same optimizer instance across search - Remove commented code --- .../priority_search/priority_search.py | 50 ++++++------------- opto/features/priority_search/sampler.py | 4 +- .../streaming_priority_search.py | 3 +- opto/features/priority_search/utils.py | 21 +++++++- 4 files changed, 39 insertions(+), 39 deletions(-) diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index 04d03f33..50bdc918 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -9,7 +9,7 @@ from opto.trainer.utils import async_run, safe_mean from opto.trainer.algorithms.basic_algorithms import batchify from opto.features.priority_search.search_template import SearchTemplate, Samples, BatchRollout, save_train_config -from opto.features.priority_search.utils import set_module_parameters, remap_update_dict, create_module_from_update_dict, is_module_copy +from opto.features.priority_search.utils import set_module_parameters, remap_update_dict, create_module_from_update_dict, is_module_copy, deepcopy_module class ModuleCandidate: @@ -44,7 +44,7 @@ def get_module(self): """ Apply the update_dict to the base_module and return the updated module. A new module is always created so the base_module is not modified. The new module has a new attribute _module_candidate which is this candidate.""" - module = create_module_from_update_dict(self.base_module, self.update_dict) if self.update_dict else copy.deepcopy(self.base_module) # + module = create_module_from_update_dict(self.base_module, self.update_dict) if self.update_dict else deepcopy_module(self.base_module) # setattr(module, '__TRACE_RESERVED_module_candidate_id', id(self)) return module # return the updated module @@ -52,34 +52,6 @@ def apply_update(self, base_module=None): """ Apply update to the base_module in place. """ set_module_parameters(base_module or self.base_module, self.update_dict) - # def __getstate__(self): - # """ Get the state of the candidate for serialization. """ - # state = copy.deepcopy(self.__dict__) # this will detach the nodes from the computation graph - # state['base_module'] = self.base_module - # state = self.__dict__.copy() - # return state - - # def __setstate__(self, state): - # """ Set the state of the candidate from serialization. """ - # self.__dict__.update(state) - - # def __deepcopy__(self, memo): - # """ Create a deep copy, except for the base_module which is not copied, it is the original module. """ - # cls = self.__class__ - # result = cls.__new__(cls) - # memo[id(self)] = result - # for k, v in self.__dict__.items(): - # if k != 'base_module': - # setattr(result, k, copy.deepcopy(v, memo)) - # else: - # setattr(result, k, v) # base_module is not copied, it is the original module - # return result - - # def copy(self): - # """ Create a shallow copy, except for the base_module which is not copied, it is the original module. """ - # new_obj = self.__class__.__new__(self.__class__) # create a new instance of the same class - # new_obj.__dict__.update(self.__dict__) - # return new_obj def __eq__(self, other): """ Check if two candidates are equal based on their base_module and update_dict. """ @@ -298,6 +270,7 @@ def train(self, memory_update_frequency: Optional[int | None] = 0, # number of iterations to keep the candidates in the short-term memory before merging them into the long-term memory. 0 means only long-term memory is used. None means only short-term memory is used. score_function: str = 'mean', # function to compute the score for the candidates; 'mean' or 'ucb' ucb_exploration_constant: float = 1.0, # exploration constant for UCB score function + decouple_optimizers: bool = True, # whether to decouple the optimizers for each candidate; if True, each candidate will have its own optimizer instance; if False, all candidates share the same optimizer instance. # Additional keyword arguments **kwargs ): @@ -343,7 +316,8 @@ def train(self, ucb_exploration_constant=ucb_exploration_constant, long_term_memory_size=long_term_memory_size, short_term_memory_size=short_term_memory_size, - memory_update_frequency=memory_update_frequency + memory_update_frequency=memory_update_frequency, + decouple_optimizers=decouple_optimizers, ) self._enforce_using_data_collecting_candidates = True @@ -369,7 +343,7 @@ def train(self, save_path=save_path, **kwargs) - def _initialize_search_parameters(self, + def _initialize_search_parameters(self, *, num_candidates, num_proposals, validate_exploration_candidates, @@ -379,7 +353,8 @@ def _initialize_search_parameters(self, ucb_exploration_constant, long_term_memory_size, short_term_memory_size, - memory_update_frequency): + memory_update_frequency, + decouple_optimizers): """Initialize search parameters and memory structures. Args: @@ -393,6 +368,7 @@ def _initialize_search_parameters(self, long_term_memory_size (int): Size of the long-term heap memory short_term_memory_size (int): Size of the short-term memory memory_update_frequency (int): The candidates are merged into long-term memory after this many iterations. + decouple_optimizers (bool): Whether to decouple optimizers for each candidate """ # Validate and adjust num_candidates based on number of optimizers if num_candidates < len(self._optimizers): @@ -405,6 +381,7 @@ def _initialize_search_parameters(self, self.validate_exploration_candidates = validate_exploration_candidates self.use_best_candidate_to_explore = use_best_candidate_to_explore self.score_function = score_function + self.decouple_optimizers = decouple_optimizers # Validate and set score range for UCB if score_range is None: @@ -552,12 +529,15 @@ def propose(self, candidate_batchrollouts_list = [ (k,b) for k, v in matched_candidates_and_samples.items() for b in v] n_batches = len(candidate_batchrollouts_list) # number of batch rollouts in the samples + def copy_optimizer(optimizer): + return copy.deepcopy(optimizer) if self.decouple_optimizers else optimizer + # need to copy optimizer for the n_batches def _backward(n): candidate, rollouts = candidate_batchrollouts_list[n] optimizer = candidate.optimizer or self.optimizer # Create a copy of the optimizer to avoid modifying the original one and to allow parallel execution - optimizer = copy.deepcopy(optimizer) + optimizer = copy_optimizer(optimizer) optimizer.parameters = rollouts.module.parameters() # set the optimizer's parameters to the proposal's parameters targets = [r.target for r in rollouts] feedbacks = [r.feedback for r in rollouts] @@ -577,7 +557,7 @@ def _backward(n): assert len(optimizers) == n_batches, "Number of optimizers must match number of batch rollouts." # need to copy optimizer for the n_proposals # NOTE when optimizer is deepcopied, its parameters are not copied. - optimizers = [copy.deepcopy(o) for o in optimizers ] * n_proposals # repeat args_list n_proposals times + optimizers = [copy_optimizer(o) for o in optimizers ] * n_proposals # repeat args_list n_proposals times assert len(optimizers) == n_batches * n_proposals, "Number of optimizers must match number of batch rollouts times number of proposals." # For each optimizer, containing the backward feedback, we call it n_proposals times to get the proposed parameters. diff --git a/opto/features/priority_search/sampler.py b/opto/features/priority_search/sampler.py index bc98ae43..6c09cf57 100644 --- a/opto/features/priority_search/sampler.py +++ b/opto/features/priority_search/sampler.py @@ -5,6 +5,7 @@ from opto import trace from opto.trainer.utils import batch_run from opto.trainer.guide import Guide +from opto.features.priority_search.utils import deepcopy_module @dataclass class Rollout: @@ -311,7 +312,8 @@ def sample(self, agents, use_prev_batch=False, description_prefix=''): if i % self.subbatch_size == 0 and i > 0: configs.append(RolloutConfig(module=agent, xs=_xs, infos=_infos, guide=self.guide)) # reset - agent = copy.deepcopy(agent) # create a deep copy of the agent for the next sub-batch + # agent = copy.deepcopy(agent) # create a deep copy of the agent for the next sub-batch + agent = deepcopy_module(agent) # remove _copy suffixes _xs, _infos = [], [] _xs.append(xs[i]) _infos.append(infos[i]) diff --git a/opto/features/priority_search/streaming_priority_search.py b/opto/features/priority_search/streaming_priority_search.py index 3f555b66..4bb14e6e 100644 --- a/opto/features/priority_search/streaming_priority_search.py +++ b/opto/features/priority_search/streaming_priority_search.py @@ -88,7 +88,8 @@ def validate(self, assert len(candidates) <= self.num_candidates, f"Number of proposed candidates {len(candidates)} must be less than num_candidates {self.num_candidates}." if len(candidates) == self.num_candidates: print("Warning: Number of proposed candidates is equal to num_candidates. Running in pure exploration mode.") - assert len(results) == len(exploration_candidates) + len(candidates), "Number of candidates in results must match the number of exploration candidates and proposed candidates." + # remove this assertion since some candidates might be duplicates + # assert len(results) == len(exploration_candidates) + len(candidates), f"Number of candidates in results must match the number of exploration candidates and proposed candidates. Getting {len(results)} vs {len(exploration_candidates) + len(candidates)}." return results def compute_exploration_priority(self, candidate) -> float: diff --git a/opto/features/priority_search/utils.py b/opto/features/priority_search/utils.py index 4aae37b6..c61c81c2 100644 --- a/opto/features/priority_search/utils.py +++ b/opto/features/priority_search/utils.py @@ -9,7 +9,6 @@ from opto.optimizers.utils import print_color from opto.trainer.algorithms.basic_algorithms import Minibatch, Trainer, batchify from opto.trainer.loader import DataLoader -from opto.features.priority_search.sampler import Sampler, BatchRollout # Some helper functions to convert between trace.Module and update_dict @@ -79,7 +78,25 @@ def create_module_from_update_dict(agent, update_dict): The update_dict is a dictionary of ParameterNode: value pairs. A new agent will be created with the parameters set to the values from the update_dict. """ - new_agent = copy.deepcopy(agent) #.copy() # create a copy of the agent + # new_agent = copy.deepcopy(agent) #.copy() # create a copy of the agent + new_agent = deepcopy_module(agent) # create a copy of the agent set_module_parameters(new_agent, update_dict) # set the parameters of the new agent return new_agent # return the new agent + +def deepcopy_module(agent): + """ Create a deep copy of the agent, but reset the parameter names to remove the _copy suffixes. + + This is useful when we want to create a new agent for a new rollout, + but we want to keep the parameter names consistent with the original agent + so that the optimizer can recognize them across different rollouts. + + NOTE: This breaks the GRAPH's assumption on uniqueness of node names. Use with caution. + """ + new_agent = copy.deepcopy(agent) + for p_n in new_agent.parameters(): + for p_o in agent.parameters(): + if is_node_copy(p_n, p_o): + p_n._name = p_o._name # directly set the name to the original parameter's name + break + return new_agent From 14c657044f8c7c27663504a48d842042ced1274e Mon Sep 17 00:00:00 2001 From: chinganc Date: Wed, 24 Sep 2025 21:58:13 +0000 Subject: [PATCH 284/314] Fix memory due to tracking optimizers in PS --- opto/features/priority_search/priority_search.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index 50bdc918..86bcbd60 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -581,6 +581,11 @@ def _step(n): max_workers=self.num_threads, # use the number of threads specified in the class description=f"Calling optimizers: Generating {n_proposals} proposals for each of {n_batches} batches",) + # Clear the optimizers to avoid memory leaks + for optimizer in optimizers: + optimizer.zero_feedback() # reset the optimizer's feedback + optimizer.parameters = [] # clear the optimizer's parameters to avoid memory leaks + # update_dicts is a list of dicts of length n_batches * n_proposals # Create ModuleCandidate objects for each proposed update_dict that is non-trivial candidates = [ModuleCandidate(self.agent, update_dict, optimizer) From 11444b7c72dea57cd1d4ef1c342d3f7c61eb608b Mon Sep 17 00:00:00 2001 From: doxav Date: Fri, 26 Sep 2025 23:41:48 +0200 Subject: [PATCH 285/314] Separate priority_search_on_convex_fn into dedicated PrioritySearch and multi algo BENCH --- examples/priority_search_on_convex_fn.py | 5 +- .../priority_search_on_convex_fn_BENCH.py | 225 ++++++ opto/trainer/algorithms/gepa_algorithms.py | 652 ++++++++++++++++++ .../test_gepa_benchmark.py | 97 +++ 4 files changed, 976 insertions(+), 3 deletions(-) create mode 100644 examples/priority_search_on_convex_fn_BENCH.py create mode 100644 opto/trainer/algorithms/gepa_algorithms.py create mode 100644 tests/llm_optimizers_tests/test_gepa_benchmark.py diff --git a/examples/priority_search_on_convex_fn.py b/examples/priority_search_on_convex_fn.py index 54c50a6d..8122dde7 100644 --- a/examples/priority_search_on_convex_fn.py +++ b/examples/priority_search_on_convex_fn.py @@ -229,7 +229,7 @@ def get_feedback(self, query: str, response: str, reference=None, **kwargs) -> T # score, feedbak str obs, reward, done, info = self.env.step(response) - return reward, obs + '\n\n' + info['feedback'] + return reward, ((obs + '\n\n') if obs else '') + info.get('feedback', '') env = SixHumpCamel(horizon=200) train_dataset = dict(inputs=[None], infos=[None]) @@ -257,6 +257,5 @@ def get_feedback(self, query: str, response: str, reference=None, **kwargs) -> T num_candidates=4, num_proposals=4, memory_update_frequency=2, - optimizer_kwargs={'objective':"You have a task of guessing two numbers. You should make sure your guess minimizes y.", - 'memory_size': 10} + optimizer_kwargs={'objective':"You have a task of guessing two numbers. You should make sure your guess minimizes y.", 'memory_size': 10} ) \ No newline at end of file diff --git a/examples/priority_search_on_convex_fn_BENCH.py b/examples/priority_search_on_convex_fn_BENCH.py new file mode 100644 index 00000000..13088b9f --- /dev/null +++ b/examples/priority_search_on_convex_fn_BENCH.py @@ -0,0 +1,225 @@ +import re +import sys +import string +import numpy as np +import time +from opto.trace.utils import dedent +from priority_search_on_convex_fn import LossLandscapeBase, np_random, Rosenbrock, SixHumpCamel, RewardGuide + +# ============ TESTING code ============= +import datasets +import numpy as np +from opto import trace +from opto.utils.llm import LLM, LiteLLM +from opto.optimizers import OptoPrimeV2 as OptoPrime +from opto.features.priority_search import PrioritySearch as SearchAlgorithm +from opto.trainer.guide import Guide +from opto.trainer.loggers import TensorboardLogger +from opto.trainer.guide import LLMJudge +from opto.trainer.algorithms.basic_algorithms import BasicSearchAlgorithm +from opto.trainer.algorithms.gepa_algorithms import GEPAAlgorithmBase, GEPAUCBSearch, GEPABeamPareto +from typing import Any +from opto import trainer +from typing import Tuple + + +def run_algorithm_comparison(): + """Compare PrioritySearch vs GEPA algorithms on optimization tasks using trainer.train API.""" + + # Test on both Rosenbrock and SixHumpCamel + envs = [ + ("Rosenbrock", Rosenbrock(horizon=20, seed=42)), + ("SixHumpCamel", SixHumpCamel(horizon=20, seed=42)) + ] + + results = {} + + for env_name, env in envs: + print(f"\n{'='*60}") + print(f"Testing on {env_name}") + print(f"{'='*60}") + + # Reset environment and get initial instruction + instruction = env.reset() + initial_input = instruction.split("\n")[0].strip() + + # Prepare train dataset + train_dataset = dict(inputs=[None], infos=[None]) + + # Setup guide + guide = RewardGuide(env) + + optimizer_kwargs = {'objective': "You have a task of guessing two numbers. You should make sure your guess minimizes y.", 'memory_size': 10} + # Configure algorithms to test + algorithms = [ + # PrioritySearch baseline + { + 'name': 'PrioritySearch', + 'algorithm': SearchAlgorithm, + 'params': { + 'guide': guide, + 'train_dataset': train_dataset, + 'score_range': [-10, 10], + 'num_epochs': 1, + 'num_steps': 3, + 'batch_size': 1, + 'num_batches': 2, + 'verbose': False, + 'num_candidates': 4, + 'num_proposals': 4, + 'memory_update_frequency': 2, + 'optimizer_kwargs': optimizer_kwargs + } + }, + + # GEPA algorithms + { + 'name': 'GEPA-Base', + 'algorithm': GEPAAlgorithmBase, + 'params': { + 'guide': guide, + 'train_dataset': train_dataset, + 'validate_dataset': train_dataset, + 'num_iters': 3, # More iterations for better exploration + 'train_batch_size': 2, # Larger batch size + 'merge_every': 2, # Merge more frequently + 'pareto_subset_size': 4, # Larger Pareto subset + 'num_threads': 2, + 'optimizer_kwargs': optimizer_kwargs + } + }, + + { + 'name': 'GEPA-UCB', + 'algorithm': GEPAUCBSearch, + 'params': { + 'guide': guide, + 'train_dataset': train_dataset, + 'num_search_iterations': 3, # More search iterations + 'train_batch_size': 2, # Larger batch size + 'merge_every': 2, # Merge more frequently + 'pareto_subset_size': 4, # Larger Pareto subset + 'num_threads': 2, + 'optimizer_kwargs': optimizer_kwargs + } + }, + + { + 'name': 'GEPA-Beam', + 'algorithm': GEPABeamPareto, + 'params': { + 'guide': guide, + 'train_dataset': train_dataset, + 'validate_dataset': train_dataset, + 'num_search_iterations': 3, # More search iterations + 'train_batch_size': 2, # Larger batch size + 'merge_every': 2, # Merge more frequently + 'pareto_subset_size': 4, # Larger Pareto subset + 'num_threads': 2, + 'optimizer_kwargs': optimizer_kwargs + } + } + ] + + env_results = {} + + for algo_config in algorithms: + name = algo_config['name'] + algorithm = algo_config['algorithm'] + params = algo_config['params'] + + print(f"\nRunning {name}...") + + # Reset environment for each algorithm + env.reset() + + # Create fresh trainable parameter for each algorithm + param = trace.node(initial_input, description='Input x into the hidden function to get y.', trainable=True) + + # Time the algorithm + start_time = time.time() + + try: + print(f" Initial parameter value: {param.data if hasattr(param, 'data') else param}") + # Use trainer.train API consistently for all algorithms + result = trainer.train( + model=param, + algorithm=algorithm, + **params + ) + print(f" Training result type: {type(result)}, value: {result}") + + # Get final parameter value and calculate score + final_guess = str(param.data) if hasattr(param, 'data') else str(param) + print(f" Final parameter value: {final_guess}") + x, _ = env.text_extract(final_guess) + if x is not None: + # Get the function value directly, score = -function_value (higher is better) + final_score = -env.callable_func(x) + print(f" Extracted coordinates: {x}, Function value: {env.callable_func(x)}") + else: + final_score = -10.0 # penalty for invalid output + print(f" Failed to extract coordinates from: {final_guess}") + + end_time = time.time() + runtime = end_time - start_time + + env_results[name] = { + 'score': final_score, + 'runtime': runtime, + 'success': final_score > -5.0 + } + + print(f" ✓ {name}: Score={final_score:.4f}, Runtime={runtime:.2f}s") + + except Exception as e: + print(f" ✗ {name} failed with error: {e}") + env_results[name] = { + 'score': -10.0, + 'runtime': float('inf'), + 'success': False + } + + results[env_name] = env_results + + # Analyze and display results + print(f"\n{'='*80}") + print("FINAL RESULTS SUMMARY") + print(f"{'='*80}") + + for env_name, env_results in results.items(): + print(f"\n{env_name}:") + print("-" * 40) + + priority_search_score = env_results.get("PrioritySearch", {}).get('score', -10.0) + priority_search_time = env_results.get("PrioritySearch", {}).get('runtime', float('inf')) + + print(f" PrioritySearch (baseline): {priority_search_score:.4f} (time: {priority_search_time:.2f}s)") + + gepa_wins = 0 + draws = 0 + for algo_name, result in env_results.items(): + if algo_name.startswith("GEPA"): + improvement = result['score'] - priority_search_score + time_ratio = result['runtime'] / priority_search_time if priority_search_time > 0 else float('inf') + + # Since scores are -function_value, higher scores = better performance (closer to optimal) + if abs(improvement) < 1e-6: + status = "→ SAME" + draws += 1 + elif improvement > 0: + status = "✓ BETTER" + gepa_wins += 1 + else: + status = "✗ WORSE" + + print(f" {algo_name:12}: {result['score']:7.4f} (improvement: {improvement:+6.4f}) " + f"(time: {result['runtime']:5.2f}s, ratio: {time_ratio:.2f}x) {status}") + + print(f" → Results: {gepa_wins} GEPA wins // {draws} draws // PrioritySearch wins: {len(env_results)-1-gepa_wins-draws}") + + return results + + +if __name__ == "__main__": + results = run_algorithm_comparison() \ No newline at end of file diff --git a/opto/trainer/algorithms/gepa_algorithms.py b/opto/trainer/algorithms/gepa_algorithms.py new file mode 100644 index 00000000..a793f48f --- /dev/null +++ b/opto/trainer/algorithms/gepa_algorithms.py @@ -0,0 +1,652 @@ +# opto/trainer/algorithms/gepa_algorithms.py +# GEPA (+Merge) algorithms for Trace +# - GEPAUCBSearch: subclass of UCBSearchAlgorithm +# - GEPABeamPareto: subclass of BeamsearchAlgorithm (Pareto select + single-parent incremental) +# - GEPAAlgorithmBase: subclass of AlgorithmBase (minimal GEPA loop) +# +# All default to OptoPrimeV2 if optimizer=None. + +from __future__ import annotations +import copy +import math +import random +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional, Tuple + +import numpy as np + +from opto.optimizers.optoprime_v2 import OptoPrimeV2 +from opto.trace.nodes import ParameterNode +from opto.trainer.algorithms.UCBsearch import UCBSearchAlgorithm +from opto.trainer.algorithms.beamsearch_algorithm import BeamsearchAlgorithm +from opto.trainer.algorithms.algorithm import Trainer as AlgorithmBase +from opto.trainer.algorithms.basic_algorithms import ( + evaluate, + batchify, + standard_optimization_step, +) +from opto.trainer.utils import async_run +from opto.optimizers.utils import print_color + + +# ----------------------------- Utilities ----------------------------- # + +@dataclass +class Candidate: + params: Dict[ParameterNode, Any] + eval_vector: List[float] # per-instance scores on fixed Pareto subset + mean: float + id: int + parent_ids: Tuple[int, ...] = field(default_factory=tuple) + ancestors: set = field(default_factory=set) + created_iter: int = 0 + wins: int = 0 # updated by Pareto accounting + meta: Dict[str, Any] = field(default_factory=dict) # freeform + +def _eval_on_subset(agent, guide, xs, infos, *, num_threads: Optional[int], desc: str) -> List[float]: + return evaluate(agent, guide, xs, infos, min_score=None, num_threads=num_threads, description=desc) + +def _compute_pareto_counts(cands: List[Candidate]) -> None: + """ + "Best-for-at-least-one-instance" winners. + For each position m in eval vectors, find argmax candidate and credit a win. + """ + if not cands: + return + L = len(cands[0].eval_vector) + # Reset + for c in cands: + c.wins = 0 + # Credit wins + for m in range(L): + best_idx = None + best_val = -float("inf") + for i, c in enumerate(cands): + v = c.eval_vector[m] if m < len(c.eval_vector) else -float("inf") + if v > best_val: + best_val, best_idx = v, i + if best_idx is not None: + cands[best_idx].wins += 1 + +def _pareto_sample(cands: List[Candidate], *, temperature: float = 1.0, rng: random.Random) -> Candidate: + """ + Sample a parent from union of per-instance winners, proportional to wins^1/T. + """ + if not cands: + raise ValueError("Empty candidate buffer.") + _compute_pareto_counts(cands) + wins = np.array([max(1, c.wins) for c in cands], dtype=float) # avoid zero + if temperature <= 0: + # Deterministic pick + return cands[int(wins.argmax())] + weights = wins ** (1.0 / max(1e-6, temperature)) + probs = weights / (weights.sum() if weights.sum() > 0 else 1.0) + idx = rng.choices(range(len(cands)), weights=probs, k=1)[0] + return cands[idx] + +def _uniform_merge_params(a: Dict[ParameterNode, Any], b: Dict[ParameterNode, Any], rng: random.Random) -> Dict[ParameterNode, Any]: + """ + Simple, robust "crossover": per-parameter uniform pick between parents. + (System-aware enough for prompt/code params, cheap, and safe.) + """ + keys = set(a.keys()) | set(b.keys()) + merged: Dict[ParameterNode, Any] = {} + for p in keys: + if p in a and p in b: + merged[p] = copy.deepcopy(a[p] if rng.random() < 0.5 else b[p]) + elif p in a: + merged[p] = copy.deepcopy(a[p]) + else: + merged[p] = copy.deepcopy(b[p]) + return merged + +def _maybe_merge(buffer: List[Candidate], + *, + agent, + guide, + pareto_inputs: List[Any], + pareto_infos: List[Any], + num_threads: Optional[int], + rng: random.Random, + tried_pairs: set, + max_tries: int = 8) -> Optional[Candidate]: + """ + Try merging two non-lineage candidates once; return merged if better than both parents' mean, else None. + """ + if len(buffer) < 2: + return None + # Prefer winners + _compute_pareto_counts(buffer) + pool = sorted(buffer, key=lambda c: (c.wins, c.mean), reverse=True) + + # Try a few distinct pairs + for _ in range(max_tries): + i, j = rng.sample(range(len(pool)), 2) + a, b = pool[i], pool[j] + if a.id == b.id: + continue + if a.id in b.ancestors or b.id in a.ancestors: + continue # avoid direct ancestry + key = tuple(sorted((a.id, b.id))) + if key in tried_pairs: + continue + tried_pairs.add(key) + + merged_params = _uniform_merge_params(a.params, b.params, rng) + # Evaluate merged on Pareto subset + original_params = {p: copy.deepcopy(p.data) for p in agent.parameters()} + try: + # load params to agent + from opto.optimizers.optimizer import Optimizer # type: ignore + # We only need the parameters dict projection; we can set via optimizer.update if available + # But we don't have an optimizer here; use ParameterNode._set + for p, v in merged_params.items(): + p._set(v) + + vec = _eval_on_subset(agent, guide, pareto_inputs, pareto_infos, num_threads=num_threads, + desc="GEPA+Merge: evaluating merged") + mean = float(np.mean(vec)) if all(s is not None for s in vec) else -float("inf") + finally: + # restore original + for p, v in original_params.items(): + p._set(v) + + if mean > max(a.mean, b.mean): + merged = Candidate(params=merged_params, + eval_vector=vec, + mean=mean, + id=-1, # to be set by caller + parent_ids=(a.id, b.id), + ancestors=set(a.ancestors) | set(b.ancestors) | {a.id, b.id}, + created_iter=0) + return merged + return None + + +def _ensure_optimizer(agent, optimizer): + if optimizer is not None: + return optimizer + params = [p for p in agent.parameters()] # List[ParameterNode] + return OptoPrimeV2(parameters=params) + + +def _train_step_generate_child(agent, guide, optimizer, train_xs, train_infos, *, verbose=False, num_threads=None): + """ + Single-parent, incremental evolution "mutation": run forward on a minibatch to get batched feedback, + then optimizer.step(bypassing=True) to obtain a new candidate param dict (without applying). + """ + use_async = num_threads is not None and num_threads > 1 + if use_async: + outputs = async_run([lambda a,x,g,info: standard_optimization_step(a, x, g, info)] * len(train_xs), + args_list=[(agent, x, guide, info) for x, info in zip(train_xs, train_infos)], + max_workers=num_threads, + description="GEPA forward (mutate parent)") + # outputs: List[(target, score, feedback)] + else: + outputs = [standard_optimization_step(agent, x, guide, info) for x, info in zip(train_xs, train_infos)] + + scores, targets, feedbacks = [], [], [] + for target, score, feedback in outputs: + scores.append(score) + targets.append(target) + feedbacks.append(feedback) + + target_batch = batchify(*targets) + feedback_batch = batchify(*feedbacks).data + + optimizer.zero_feedback() + optimizer.backward(target_batch, feedback_batch) + try: + update_dict = optimizer.step(bypassing=True, verbose=("output" if verbose else False)) + if not isinstance(update_dict, dict) or len(update_dict) == 0: + # Fallback: treat current as child (rare) + update_dict = {p: copy.deepcopy(p.data) for p in optimizer.parameters} + except Exception as e: + print_color(f"[GEPA] optimizer.step error: {e}", "red") + update_dict = {} + return update_dict, (None if not scores or any(s is None for s in scores) else float(np.mean(scores))) + + +def _apply_params(optimizer, param_dict: Dict[ParameterNode, Any]): + """Load param dict into the agent via optimizer.update (preserves projections).""" + optimizer.update(param_dict) + + +# ======================= Variant 1: GEPA + Merge (UCB subclass) ======================= # + +class GEPAUCBSearch(UCBSearchAlgorithm): + """ + GEPA (+Merge) implemented atop UCBSearchAlgorithm. + Differences vs base UCB: + - Fixed Pareto subset (D_pareto) and per-instance vectors kept for each candidate + - Parent selection = Pareto "best-for-at-least-one" sampling (wins-weighted); UCB used only for eviction fallback + - Single-parent incremental mutation via a minibatch + - Optional periodic Merge crossover (uniform per-parameter) with desirability checks + """ + + def __init__(self, + agent, + optimizer=None, + *, + max_buffer_size: int = 16, + ucb_exploration_factor: float = 0.8, + rng_seed: int = 7, + logger=None, + num_threads: Optional[int] = None): + optimizer = _ensure_optimizer(agent, optimizer) + super().__init__(agent, optimizer, + max_buffer_size=max_buffer_size, + ucb_exploration_factor=ucb_exploration_factor, + logger=logger, + num_threads=num_threads) + self.rng = random.Random(rng_seed) + self._pareto_inputs: List[Any] = [] + self._pareto_infos: List[Any] = [] + self._id_counter = 0 + + def _next_id(self) -> int: + self._id_counter += 1 + return self._id_counter + + def _evaluate_on_pareto(self, params_dict: Dict[ParameterNode, Any], guide, *, num_threads) -> Tuple[List[float], float]: + original_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + try: + _apply_params(self.optimizer, params_dict) + vec = _eval_on_subset(self.agent, guide, self._pareto_inputs, self._pareto_infos, + num_threads=num_threads, desc="GEPA: evaluate on Pareto subset") + mean = float(np.mean(vec)) if all(s is not None for s in vec) else -float("inf") + return vec, mean + finally: + _apply_params(self.optimizer, original_params) + + def _select_pareto_parent(self, cand_buffer: List[Candidate]) -> Candidate: + return _pareto_sample(cand_buffer, temperature=1.0, rng=self.rng) + + def train(self, + guide, + train_dataset: Dict[str, List[Any]], + *, + validation_dataset: Optional[Dict[str, List[Any]]] = None, + pareto_subset_size: int = 24, + num_search_iterations: int = 120, + train_batch_size: int = 2, + merge_every: int = 6, + log_frequency: Optional[int] = None, + save_frequency: Optional[int] = None, + save_path: str = "checkpoints/gepa_ucb_agent.pkl", + verbose: bool = False, + num_threads: Optional[int] = None) -> Tuple[Dict[str, Any], float]: + """ + GEPA search loop with Pareto sampling + (optional) Merge. + """ + num_threads = num_threads or self.num_threads + log_frequency = log_frequency or 5 + validate_ds = validation_dataset or train_dataset + + # Fix a Pareto subset (small, stable) to compute per-instance vectors + assert len(validate_ds["inputs"]) > 0, "Empty dataset." + idxs = np.random.choice(len(validate_ds["inputs"]), + min(pareto_subset_size, len(validate_ds["inputs"])), + replace=False) + self._pareto_inputs = [validate_ds["inputs"][i] for i in idxs] + self._pareto_infos = [validate_ds["infos"][i] for i in idxs] + + buffer: List[Candidate] = [] + tried_merges: set = set() + + # Seed with current params + base_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + v0, m0 = self._evaluate_on_pareto(base_params, guide, num_threads=num_threads) + buffer.append(Candidate(params=base_params, eval_vector=v0, mean=m0, id=self._next_id(), ancestors=set())) + print_color(f"[GEPA] Seed candidate mean={m0:.4f}", "cyan") + + metrics = {"best_means": [], "new_child_means": [], "merge_accepts": 0, "total_merges": 0} + + for it in range(1, num_search_iterations + 1): + # Select parent by Pareto winners + parent = self._select_pareto_parent(buffer) + _apply_params(self.optimizer, parent.params) + + # Sample train minibatch + train_size = min(train_batch_size, len(train_dataset["inputs"])) + tr_idxs = np.random.choice(len(train_dataset["inputs"]), train_size, replace=False) + train_xs = [train_dataset["inputs"][i] for i in tr_idxs] + train_info = [train_dataset["infos"][i] for i in tr_idxs] + + # Generate child via one incremental step + update_dict, train_batch_mean = _train_step_generate_child( + self.agent, guide, self.optimizer, train_xs, train_info, verbose=verbose, num_threads=num_threads + ) + if not update_dict: + print_color("[GEPA] Empty child update; skipping.", "yellow") + continue + + # Evaluate child on Pareto subset + child_vec, child_mean = self._evaluate_on_pareto(update_dict, guide, num_threads=num_threads) + child = Candidate(params=update_dict, + eval_vector=child_vec, + mean=child_mean, + id=self._next_id(), + parent_ids=(parent.id,), + ancestors=set(parent.ancestors) | {parent.id}, + created_iter=it) + buffer.append(child) + metrics["new_child_means"].append(child_mean) + print_color(f"[GEPA] iter {it}: child mean={child_mean:.4f} (train-batch≈{train_batch_mean})", "green") + + # Optional Merge + if merge_every and (it % merge_every == 0): + metrics["total_merges"] += 1 + merged = _maybe_merge(buffer, + agent=self.agent, guide=guide, + pareto_inputs=self._pareto_inputs, + pareto_infos=self._pareto_infos, + num_threads=num_threads, + rng=self.rng, + tried_pairs=tried_merges) + if merged is not None: + merged.id = self._next_id() + merged.created_iter = it + buffer.append(merged) + metrics["merge_accepts"] += 1 + print_color(f"[GEPA] Merge accepted: mean={merged.mean:.4f}", "magenta") + + # Keep buffer bounded: remove the candidate with lowest (wins, mean) + if len(buffer) > self.max_buffer_size: + _compute_pareto_counts(buffer) + buffer.sort(key=lambda c: (c.wins, c.mean)) + evicted = buffer.pop(0) + print_color(f"[GEPA] Evicted cand#{evicted.id} (wins={evicted.wins}, mean={evicted.mean:.4f})", "yellow") + + # Track & log + best = max(buffer, key=lambda c: c.mean) + metrics["best_means"].append(best.mean) + if it % log_frequency == 0: + self.logger.log("GEPA best mean", best.mean, it, color="green") + + # Save best candidate snapshot (optional) + if save_frequency and it % save_frequency == 0: + _apply_params(self.optimizer, best.params) + self.save_agent(save_path, it) + + # Load best into the agent and return + best = max(buffer, key=lambda c: c.mean) if buffer else buffer[0] + _apply_params(self.optimizer, best.params) + return metrics, float(best.mean) + + +# ================= Variant 2: Beamsearch subclass with Pareto select ================= # + +class GEPABeamPareto(BeamsearchAlgorithm): + """ + BeamsearchAlgorithm retrofit: + - override select() to a Pareto "best-for-at-least-one" selector + - replace deep beam expansion with GEPA’s single-parent incremental evolution + """ + + def __init__(self, + agent, + optimizer=None, + *, + rng_seed: int = 11, + logger=None, + num_threads: Optional[int] = None): + optimizer = _ensure_optimizer(agent, optimizer) + super().__init__(agent, optimizer, num_threads=num_threads, logger=logger) + self.rng = random.Random(rng_seed) + + # We keep a Pareto select helper that returns (selected_params, wins, scores) + def select(self, + candidates: List[Dict[ParameterNode, Any]], + validate_guide, + validation_mini_dataset, + beam_width: int, + num_threads: int = None, + min_score: float = None, + return_scores: bool = False): + """ + Override to Pareto union-of-winners on the mini validation batch. + """ + # Evaluate each candidate to a vector on the mini validation + cand_objs: List[Candidate] = [] + current_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + try: + for idx, params in enumerate(candidates): + _apply_params(self.optimizer, params) + vec = evaluate(self.agent, + validate_guide, + validation_mini_dataset['inputs'], + validation_mini_dataset['infos'], + min_score=min_score, + num_threads=num_threads, + description=f"Validating candidate {idx+1}/{len(candidates)} (Pareto)") + mean = float(np.mean(vec)) if all(s is not None for s in vec) else -float("inf") + cand_objs.append(Candidate(params=params, eval_vector=vec, mean=mean, id=idx)) + finally: + _apply_params(self.optimizer, current_params) + + # Compute wins and select top "beam_width" by (wins, mean) + _compute_pareto_counts(cand_objs) + cand_objs.sort(key=lambda c: (c.wins, c.mean), reverse=True) + selected = cand_objs[: min(beam_width, len(cand_objs))] + sel_params = [c.params for c in selected] + sel_scores = [c.mean for c in selected] + if return_scores: + return sel_params, sel_scores + return sel_params + + # Replace beam "train" with GEPA-style incremental loop (keeps BeamsearchAlgorithm API) + def train(self, + guide, + train_dataset, + *, + validate_dataset=None, + pareto_subset_size: int = 24, + num_search_iterations: int = 120, + train_batch_size: int = 2, + merge_every: int = 6, + log_frequency: Optional[int] = None, + save_frequency: Optional[int] = None, + save_path: str = "checkpoints/gepa_beam_agent.pkl", + verbose: bool = False, + num_threads: Optional[int] = None): + num_threads = num_threads or self.num_threads + log_frequency = log_frequency or 5 + validate_ds = validate_dataset or train_dataset + + # Fix Pareto subset for this run + idxs = np.random.choice(len(validate_ds["inputs"]), + min(pareto_subset_size, len(validate_ds["inputs"])), + replace=False) + pareto_inputs = [validate_ds["inputs"][i] for i in idxs] + pareto_infos = [validate_ds["infos"][i] for i in idxs] + + # Seed buffer + buffer: List[Candidate] = [] + base_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + # Evaluate seed + current_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + try: + _apply_params(self.optimizer, base_params) + vec = evaluate(self.agent, guide, pareto_inputs, pareto_infos, + min_score=None, num_threads=num_threads, + description="GEPA(beam): seed evaluation") + finally: + _apply_params(self.optimizer, current_params) + m0 = float(np.mean(vec)) if all(s is not None for s in vec) else -float("inf") + buffer.append(Candidate(params=base_params, eval_vector=vec, mean=m0, id=0, ancestors=set())) + tried_merges: set = set() + + best_mean = m0 + for it in range(1, num_search_iterations + 1): + # Pareto-select parent and mutate + _compute_pareto_counts(buffer) + parent = _pareto_sample(buffer, temperature=1.0, rng=self.rng) + _apply_params(self.optimizer, parent.params) + + # Make a child + k = min(train_batch_size, len(train_dataset["inputs"])) + tr = np.random.choice(len(train_dataset["inputs"]), k, replace=False) + train_xs = [train_dataset["inputs"][i] for i in tr] + train_in = [train_dataset["infos"][i] for i in tr] + + update_dict, _ = _train_step_generate_child(self.agent, guide, self.optimizer, train_xs, train_in, + verbose=verbose, num_threads=num_threads) + if not update_dict: + continue + + # Evaluate child on Pareto subset + current_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + try: + _apply_params(self.optimizer, update_dict) + vec = evaluate(self.agent, guide, pareto_inputs, pareto_infos, min_score=None, + num_threads=num_threads, description="GEPA(beam): child eval") + finally: + _apply_params(self.optimizer, current_params) + mean = float(np.mean(vec)) if all(s is not None for s in vec) else -float("inf") + buffer.append(Candidate(params=update_dict, eval_vector=vec, mean=mean, id=len(buffer), + parent_ids=(parent.id,), ancestors=set(parent.ancestors) | {parent.id})) + best_mean = max(best_mean, mean) + if it % log_frequency == 0: + self.logger.log("GEPA(beam) best mean", best_mean, it, color="green") + + # Periodic merge + if merge_every and it % merge_every == 0: + merged = _maybe_merge(buffer, + agent=self.agent, guide=guide, + pareto_inputs=pareto_inputs, pareto_infos=pareto_infos, + num_threads=num_threads, rng=self.rng, tried_pairs=tried_merges) + if merged is not None: + merged.id = len(buffer) + buffer.append(merged) + + # Trim buffer softly (keep top by (wins, mean)) + if len(buffer) > 16: + _compute_pareto_counts(buffer) + buffer.sort(key=lambda c: (c.wins, c.mean), reverse=True) + buffer[:] = buffer[:16] + + # Optional save + if save_frequency and it % save_frequency == 0: + best = max(buffer, key=lambda c: c.mean) + _apply_params(self.optimizer, best.params) + self.save_agent(save_path, it) + + best = max(buffer, key=lambda c: c.mean) + _apply_params(self.optimizer, best.params) + return {"best_mean": best.mean}, float(best.mean) + + +# =================== Variant 3: Minimal GEPA on AlgorithmBase =================== # + +class GEPAAlgorithmBase(AlgorithmBase): + """ + Lightweight GEPA (+Merge) with only AlgorithmBase dependency. + Useful when you want the simplest control loop with your own logging/saving. + """ + + def __init__(self, + agent, + optimizer=None, + *, + rng_seed: int = 13, + logger=None, + num_threads: Optional[int] = None): + super().__init__(agent, num_threads=num_threads, logger=logger) + self.optimizer = _ensure_optimizer(agent, optimizer) + self.rng = random.Random(rng_seed) + + def train(self, + guide, + train_dataset, + *, + validate_dataset=None, + pareto_subset_size: int = 24, + num_iters: int = 100, + train_batch_size: int = 2, + merge_every: int = 5, + num_threads: Optional[int] = None, + save_path: Optional[str] = None): + num_threads = num_threads or self.num_threads + validate_ds = validate_dataset or train_dataset + + # Pareto subset + idxs = np.random.choice(len(validate_ds["inputs"]), + min(pareto_subset_size, len(validate_ds["inputs"])), + replace=False) + xsP = [validate_ds["inputs"][i] for i in idxs] + isP = [validate_ds["infos"][i] for i in idxs] + + # Seed + buffer: List[Candidate] = [] + base_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + original = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + try: + _apply_params(self.optimizer, base_params) + vec = evaluate(self.agent, guide, xsP, isP, min_score=None, num_threads=num_threads, + description="GEPA(base): seed eval") + finally: + _apply_params(self.optimizer, original) + m0 = float(np.mean(vec)) if all(s is not None for s in vec) else -float("inf") + buffer.append(Candidate(params=base_params, eval_vector=vec, mean=m0, id=0, ancestors=set())) + tried_merges: set = set() + + for it in range(1, num_iters + 1): + # Parent select + _compute_pareto_counts(buffer) + parent = _pareto_sample(buffer, temperature=1.0, rng=self.rng) + _apply_params(self.optimizer, parent.params) + + # Child + k = min(train_batch_size, len(train_dataset["inputs"])) + tr = np.random.choice(len(train_dataset["inputs"]), k, replace=False) + tx = [train_dataset["inputs"][i] for i in tr] + ti = [train_dataset["infos"][i] for i in tr] + update_dict, _ = _train_step_generate_child(self.agent, guide, self.optimizer, tx, ti, + verbose=False, num_threads=num_threads) + if not update_dict: + continue + + # Eval child + original = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + try: + _apply_params(self.optimizer, update_dict) + vec = evaluate(self.agent, guide, xsP, isP, min_score=None, num_threads=num_threads, + description="GEPA(base): child eval") + finally: + _apply_params(self.optimizer, original) + mean = float(np.mean(vec)) if all(s is not None for s in vec) else -float("inf") + buffer.append(Candidate(params=update_dict, eval_vector=vec, mean=mean, id=len(buffer), + parent_ids=(parent.id,), ancestors=set(parent.ancestors) | {parent.id})) + + # Merge + if merge_every and it % merge_every == 0: + merged = _maybe_merge(buffer, + agent=self.agent, guide=guide, + pareto_inputs=xsP, pareto_infos=isP, + num_threads=num_threads, rng=self.rng, tried_pairs=tried_merges) + if merged is not None: + merged.id = len(buffer) + buffer.append(merged) + + # Keep compact buffer + if len(buffer) > 16: + _compute_pareto_counts(buffer) + buffer.sort(key=lambda c: (c.wins, c.mean), reverse=True) + buffer[:] = buffer[:16] + + # Log + best = max(buffer, key=lambda c: c.mean) + if self.logger: + self.logger.log("GEPA(base) best mean", best.mean, it, color="green") + + # Optional save + if save_path and it % 10 == 0: + _apply_params(self.optimizer, best.params) + self.save_agent(save_path, it) + + # Load best into agent + best = max(buffer, key=lambda c: c.mean) + _apply_params(self.optimizer, best.params) + return {"best_mean": best.mean}, float(best.mean) + diff --git a/tests/llm_optimizers_tests/test_gepa_benchmark.py b/tests/llm_optimizers_tests/test_gepa_benchmark.py new file mode 100644 index 00000000..19c97559 --- /dev/null +++ b/tests/llm_optimizers_tests/test_gepa_benchmark.py @@ -0,0 +1,97 @@ +import os +import pytest +import numpy as np + +from opto import trace +from opto.optimizers.optoprime_v2 import OptoPrimeV2 +from opto.trainer.algorithms.gepa_algorithms import GEPAAlgorithmBase, GEPAUCBSearch, GEPABeamPareto +from opto.trainer.algorithms.basic_algorithms import BasicSearchAlgorithm +from opto.trainer.guide import LLMJudge +from opto.utils.llm import LLM + + +RUN_BENCH = os.getenv("RUN_GEPA_BENCH") == "1" + + +def _datasets_or_skip(): + try: + import datasets # noqa: F401 + except Exception: + pytest.skip("datasets library not available; skipping GEPA benchmark test.") + + +def _llm_env_or_skip(): + have_key = any(os.getenv(k) for k in ["OPENAI_API_KEY", "AZURE_OPENAI_API_KEY", "ANTHROPIC_API_KEY", "OAI_CONFIG_LIST"]) + if not have_key: + pytest.skip("No LLM credentials found in environment; skipping GEPA benchmark test.") + + +@trace.model +class Learner: + """Agent that calls an LLM. The only trainable variable is 'system_prompt'.""" + + def __init__(self, system_prompt: str = "You're a helpful agent", user_prompt_template: str = "Query: {message}", llm: LLM = None): + self.system_prompt = trace.node(system_prompt, trainable=True) + self.user_prompt_template = trace.node(user_prompt_template) + self.llm = llm or LLM() # default profile + + @trace.bundle() + def model(self, system_prompt: str, user_prompt_template: str, message: str) -> str: + if "{message}" not in user_prompt_template: + raise ValueError("user_prompt_template must contain '{message}'") + resp = self.llm( + messages=[ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt_template.format(message=message)}, + ] + ) + return resp.choices[0].message.content + + def forward(self, message): + return self.model(self.system_prompt, self.user_prompt_template, message) + + +@pytest.mark.slow +def test_gepa_benchmark_gsm8k_real_llm(): + if not RUN_BENCH: + pytest.skip("Set RUN_GEPA_BENCH=1 to run this optional benchmark test.") + + _datasets_or_skip() + _llm_env_or_skip() + + import datasets + + # Load a tiny subset of GSM8k + ds = datasets.load_dataset("openai/gsm8k", "main") + train = ds["train"][:6] + train_dataset = {"inputs": train["question"], "infos": train["answer"]} + + # Teacher/judge with a low-cost profile + guide = LLMJudge(llm=LLM(profile="cheap")) + + # Agent and optimizer (low-cost profile) + agent = Learner(llm=LLM(profile="cheap")) + optimizer = OptoPrimeV2(agent.parameters(), llm=LLM(profile="cheap")) + + algos = [ + ("GEPA-Base", GEPAAlgorithmBase(agent, optimizer=optimizer, logger=None, num_threads=2), dict(num_iters=2, train_batch_size=1, merge_every=2)), + ("GEPA-UCB", GEPAUCBSearch(agent, optimizer=optimizer, logger=None, num_threads=2), dict(num_search_iterations=2, train_batch_size=1, merge_every=2)), + ("GEPA-Beam", GEPABeamPareto(agent, optimizer=optimizer, logger=None, num_threads=2), dict(num_search_iterations=2, train_batch_size=1, merge_every=2)), + ("BasicSearch", BasicSearchAlgorithm(agent, optimizer=optimizer, logger=None, num_threads=2), dict(num_epochs=1, batch_size=1, num_proposals=2)), + ] + + results = {} + for name, algo, kwargs in algos: + if name == "BasicSearch": + # Conform to BasicSearch's interface + algo.train(guide=guide, train_dataset=train_dataset, validate_dataset=train_dataset, test_dataset=train_dataset, eval_frequency=1, num_threads=2, verbose=False, **kwargs) + results[name] = 0.0 # placeholder; evaluation is heavy and non-deterministic + else: + _, best = algo.train(guide=guide, train_dataset=train_dataset, validate_dataset=train_dataset, pareto_subset_size=4, num_threads=2, verbose=False, **kwargs) + results[name] = float(best) + + # Sanity check that we produced some floats for each algorithm + assert set(results.keys()) == {"GEPA-Base", "GEPA-UCB", "GEPA-Beam", "BasicSearch"} + for v in results.values(): + assert isinstance(v, float) + From 153bcdd75757a2084b5a711f218540fbd4b2e103 Mon Sep 17 00:00:00 2001 From: doxav Date: Sat, 27 Sep 2025 20:41:11 +0200 Subject: [PATCH 286/314] add full benchmark conversion from LM4AD and run using examples/trainers_benchmark.py (see howto: examples/trainer_benchmark_HOWTO.md) --- .../circle_packing/__init__.py | 265 ++++++ .../benchmark_tasks/circle_packing/run_eoh.py | 33 + examples/benchmark_tasks/index.json | 805 ++++++++++++++++++ .../machine_learning_acrobot/__init__.py | 175 ++++ .../machine_learning_acrobot/paras.yaml | 3 + .../machine_learning_acrobot/test.py | 46 + .../machine_learning_car_mountain/__init__.py | 162 ++++ .../machine_learning_car_mountain/paras.yaml | 3 + .../q-learning.py | 123 +++ .../machine_learning_car_mountain/test.py | 167 ++++ .../__init__.py | 167 ++++ .../paras.yaml | 3 + .../test.py | 64 ++ .../machine_learning_moon_lander/__init__.py | 196 +++++ .../machine_learning_moon_lander/paras.yaml | 3 + .../machine_learning_moon_lander/test.py | 53 ++ .../machine_learning_pendulum/__init__.py | 195 +++++ .../machine_learning_pendulum/paras.yaml | 3 + .../machine_learning_pendulum/test.py | 47 + .../online_bin_packing_local/__init__.py | 164 ++++ .../generate_weibull_instances.py | 36 + .../online_bin_packing_local/run_eoh.py | 33 + .../optimization_admissible_set/__init__.py | 256 ++++++ .../optimization_admissible_set/paras.yaml | 4 + .../optimization_aircraft_landing/__init__.py | 450 ++++++++++ .../optimization_aircraft_landing/paras.yaml | 2 + .../__init__.py | 327 +++++++ .../paras.yaml | 2 + .../__init__.py | 388 +++++++++ .../paras.yaml | 2 + .../optimization_bp_1d/__init__.py | 298 +++++++ .../optimization_bp_1d/paras.yaml | 2 + .../optimization_bp_1d_construct/__init__.py | 289 +++++++ .../get_instance.py | 55 ++ .../optimization_bp_1d_construct/paras.yaml | 2 + .../optimization_bp_2d_construct/__init__.py | 344 ++++++++ .../get_instance.py | 40 + .../optimization_bp_2d_construct/paras.yaml | 2 + .../__init__.py | 398 +++++++++ .../paras.yaml | 2 + .../optimization_cflp_construct/__init__.py | 310 +++++++ .../get_instance.py | 65 ++ .../optimization_cflp_construct/paras.yaml | 2 + .../__init__.py | 310 +++++++ .../paras.yaml | 2 + .../__init__.py | 399 +++++++++ .../paras.yaml | 2 + .../__init__.py | 359 ++++++++ .../paras.yaml | 2 + .../__init__.py | 387 +++++++++ .../optimization_container_loading/paras.yaml | 2 + .../__init__.py | 456 ++++++++++ .../paras.yaml | 2 + .../__init__.py | 341 ++++++++ .../paras.yaml | 2 + .../optimization_crew_scheduling/__init__.py | 369 ++++++++ .../optimization_crew_scheduling/paras.yaml | 2 + .../optimization_cvrp_construct/__init__.py | 328 +++++++ .../get_instance.py | 50 ++ .../optimization_cvrp_construct/paras.yaml | 2 + .../__init__.py | 326 +++++++ .../paras.yaml | 2 + .../__init__.py | 372 ++++++++ .../paras.yaml | 2 + .../__init__.py | 388 +++++++++ .../paras.yaml | 2 + .../__init__.py | 340 ++++++++ .../paras.yaml | 2 + .../optimization_graph_colouring/__init__.py | 372 ++++++++ .../optimization_graph_colouring/paras.yaml | 2 + .../__init__.py | 564 ++++++++++++ .../paras.yaml | 2 + .../__init__.py | 307 +++++++ .../paras.yaml | 2 + .../optimization_jssp_construct/__init__.py | 289 +++++++ .../get_instance.py | 43 + .../optimization_jssp_construct/paras.yaml | 2 + .../__init__.py | 271 ++++++ .../get_instance.py | 41 + .../paras.yaml | 2 + .../__init__.py | 261 ++++++ .../paras.yaml | 2 + .../__init__.py | 629 ++++++++++++++ .../paras.yaml | 2 + .../__init__.py | 496 +++++++++++ .../paras.yaml | 2 + .../__init__.py | 311 +++++++ .../generate_weibull_instances.py | 36 + .../paras.yaml | 2 + .../__init__.py | 194 +++++ .../generate_weibull_instances.py | 36 + .../paras.yaml | 2 + .../__init__.py | 307 +++++++ .../paras.yaml | 2 + .../optimization_ovrp_construct/__init__.py | 299 +++++++ .../get_instance.py | 50 ++ .../optimization_ovrp_construct/paras.yaml | 2 + .../__init__.py | 357 ++++++++ .../paras.yaml | 2 + .../__init__.py | 367 ++++++++ .../paras.yaml | 2 + .../__init__.py | 337 ++++++++ .../paras.yaml | 2 + .../__init__.py | 334 ++++++++ .../paras.yaml | 2 + .../__init__.py | 400 +++++++++ .../paras.yaml | 2 + .../__init__.py | 442 ++++++++++ .../paras.yaml | 2 + .../optimization_pymoo_moead/__init__.py | 216 +++++ .../optimization_pymoo_moead/get_instance.py | 87 ++ .../optimization_pymoo_moead/paras.yaml | 2 + .../optimization_qap_construct/__init__.py | 293 +++++++ .../get_instance.py | 48 ++ .../optimization_qap_construct/paras.yaml | 2 + .../__init__.py | 353 ++++++++ .../paras.yaml | 2 + .../__init__.py | 296 +++++++ .../get_instance.py | 53 ++ .../paras.yaml | 2 + .../optimization_set_cover_construct/test.py | 125 +++ .../optimization_set_covering/__init__.py | 497 +++++++++++ .../optimization_set_covering/paras.yaml | 2 + .../optimization_set_partitioning/__init__.py | 389 +++++++++ .../optimization_set_partitioning/paras.yaml | 2 + .../__init__.py | 334 ++++++++ .../paras.yaml | 2 + .../optimization_tsp_construct/__init__.py | 259 ++++++ .../get_instance.py | 16 + .../optimization_tsp_construct/paras.yaml | 2 + .../optimization_tsp_gls_2O/__init__.py | 184 ++++ .../optimization_tsp_gls_2O/get_instance.py | 23 + .../optimization_tsp_gls_2O/gls.py | 226 +++++ .../optimization_tsp_gls_2O/paras.yaml | 2 + .../__init__.py | 349 ++++++++ .../paras.yaml | 2 + .../__init__.py | 376 ++++++++ .../paras.yaml | 2 + .../__init__.py | 469 ++++++++++ .../paras.yaml | 2 + .../optimization_vrptw_construct/__init__.py | 279 ++++++ .../get_instance.py | 64 ++ .../optimization_vrptw_construct/paras.yaml | 2 + .../science_discovery_ode_1d/__init__.py | 258 ++++++ .../science_discovery_ode_1d/paras.yaml | 3 + .../strogatz_equations.py | 223 +++++ examples/convert_llm4ad_benchmark.py | 460 ++++++++++ examples/llm4ad_loader.py | 492 +++++++++++ examples/trainer_benchmark_HOWTO.md | 300 +++++++ examples/trainers_benchmark.py | 348 ++++++++ .../trainers_benchmark_tasks_validation.py | 385 +++++++++ 151 files changed, 24645 insertions(+) create mode 100644 examples/benchmark_tasks/circle_packing/__init__.py create mode 100644 examples/benchmark_tasks/circle_packing/run_eoh.py create mode 100644 examples/benchmark_tasks/index.json create mode 100644 examples/benchmark_tasks/machine_learning_acrobot/__init__.py create mode 100644 examples/benchmark_tasks/machine_learning_acrobot/paras.yaml create mode 100644 examples/benchmark_tasks/machine_learning_acrobot/test.py create mode 100644 examples/benchmark_tasks/machine_learning_car_mountain/__init__.py create mode 100644 examples/benchmark_tasks/machine_learning_car_mountain/paras.yaml create mode 100644 examples/benchmark_tasks/machine_learning_car_mountain/q-learning.py create mode 100644 examples/benchmark_tasks/machine_learning_car_mountain/test.py create mode 100644 examples/benchmark_tasks/machine_learning_car_mountain_continue/__init__.py create mode 100644 examples/benchmark_tasks/machine_learning_car_mountain_continue/paras.yaml create mode 100644 examples/benchmark_tasks/machine_learning_car_mountain_continue/test.py create mode 100644 examples/benchmark_tasks/machine_learning_moon_lander/__init__.py create mode 100644 examples/benchmark_tasks/machine_learning_moon_lander/paras.yaml create mode 100644 examples/benchmark_tasks/machine_learning_moon_lander/test.py create mode 100644 examples/benchmark_tasks/machine_learning_pendulum/__init__.py create mode 100644 examples/benchmark_tasks/machine_learning_pendulum/paras.yaml create mode 100644 examples/benchmark_tasks/machine_learning_pendulum/test.py create mode 100644 examples/benchmark_tasks/online_bin_packing_local/__init__.py create mode 100644 examples/benchmark_tasks/online_bin_packing_local/generate_weibull_instances.py create mode 100644 examples/benchmark_tasks/online_bin_packing_local/run_eoh.py create mode 100644 examples/benchmark_tasks/optimization_admissible_set/__init__.py create mode 100644 examples/benchmark_tasks/optimization_admissible_set/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_aircraft_landing/__init__.py create mode 100644 examples/benchmark_tasks/optimization_aircraft_landing/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_assignment_problem/__init__.py create mode 100644 examples/benchmark_tasks/optimization_assignment_problem/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_assortment_problem/__init__.py create mode 100644 examples/benchmark_tasks/optimization_assortment_problem/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_bp_1d/__init__.py create mode 100644 examples/benchmark_tasks/optimization_bp_1d/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_bp_1d_construct/__init__.py create mode 100644 examples/benchmark_tasks/optimization_bp_1d_construct/get_instance.py create mode 100644 examples/benchmark_tasks/optimization_bp_1d_construct/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_bp_2d_construct/__init__.py create mode 100644 examples/benchmark_tasks/optimization_bp_2d_construct/get_instance.py create mode 100644 examples/benchmark_tasks/optimization_bp_2d_construct/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_capacitated_warehouse_location/__init__.py create mode 100644 examples/benchmark_tasks/optimization_capacitated_warehouse_location/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_cflp_construct/__init__.py create mode 100644 examples/benchmark_tasks/optimization_cflp_construct/get_instance.py create mode 100644 examples/benchmark_tasks/optimization_cflp_construct/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_common_due_date_scheduling/__init__.py create mode 100644 examples/benchmark_tasks/optimization_common_due_date_scheduling/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_constrained_guillotine_cutting/__init__.py create mode 100644 examples/benchmark_tasks/optimization_constrained_guillotine_cutting/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_constrained_non_guillotine_cutting/__init__.py create mode 100644 examples/benchmark_tasks/optimization_constrained_non_guillotine_cutting/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_container_loading/__init__.py create mode 100644 examples/benchmark_tasks/optimization_container_loading/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_container_loading_with_weight_restrictions/__init__.py create mode 100644 examples/benchmark_tasks/optimization_container_loading_with_weight_restrictions/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_corporate_structuring/__init__.py create mode 100644 examples/benchmark_tasks/optimization_corporate_structuring/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_crew_scheduling/__init__.py create mode 100644 examples/benchmark_tasks/optimization_crew_scheduling/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_cvrp_construct/__init__.py create mode 100644 examples/benchmark_tasks/optimization_cvrp_construct/get_instance.py create mode 100644 examples/benchmark_tasks/optimization_cvrp_construct/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_equitable_partitioning_problem/__init__.py create mode 100644 examples/benchmark_tasks/optimization_equitable_partitioning_problem/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_euclidean_steiner_problem/__init__.py create mode 100644 examples/benchmark_tasks/optimization_euclidean_steiner_problem/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_flow_shop_scheduling/__init__.py create mode 100644 examples/benchmark_tasks/optimization_flow_shop_scheduling/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_generalised_assignment_problem/__init__.py create mode 100644 examples/benchmark_tasks/optimization_generalised_assignment_problem/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_graph_colouring/__init__.py create mode 100644 examples/benchmark_tasks/optimization_graph_colouring/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_hybrid_reentrant_shop_scheduling/__init__.py create mode 100644 examples/benchmark_tasks/optimization_hybrid_reentrant_shop_scheduling/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_job_shop_scheduling/__init__.py create mode 100644 examples/benchmark_tasks/optimization_job_shop_scheduling/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_jssp_construct/__init__.py create mode 100644 examples/benchmark_tasks/optimization_jssp_construct/get_instance.py create mode 100644 examples/benchmark_tasks/optimization_jssp_construct/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_knapsack_construct/__init__.py create mode 100644 examples/benchmark_tasks/optimization_knapsack_construct/get_instance.py create mode 100644 examples/benchmark_tasks/optimization_knapsack_construct/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_maximal_independent_set/__init__.py create mode 100644 examples/benchmark_tasks/optimization_maximal_independent_set/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_multi_demand_multidimensional_knapsack_problem/__init__.py create mode 100644 examples/benchmark_tasks/optimization_multi_demand_multidimensional_knapsack_problem/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_multidimensional_knapsack_problem/__init__.py create mode 100644 examples/benchmark_tasks/optimization_multidimensional_knapsack_problem/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_online_bin_packing/__init__.py create mode 100644 examples/benchmark_tasks/optimization_online_bin_packing/generate_weibull_instances.py create mode 100644 examples/benchmark_tasks/optimization_online_bin_packing/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_online_bin_packing_2O/__init__.py create mode 100644 examples/benchmark_tasks/optimization_online_bin_packing_2O/generate_weibull_instances.py create mode 100644 examples/benchmark_tasks/optimization_online_bin_packing_2O/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_open_shop_scheduling/__init__.py create mode 100644 examples/benchmark_tasks/optimization_open_shop_scheduling/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_ovrp_construct/__init__.py create mode 100644 examples/benchmark_tasks/optimization_ovrp_construct/get_instance.py create mode 100644 examples/benchmark_tasks/optimization_ovrp_construct/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_p_median_capacitated/__init__.py create mode 100644 examples/benchmark_tasks/optimization_p_median_capacitated/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_p_median_uncapacitated/__init__.py create mode 100644 examples/benchmark_tasks/optimization_p_median_uncapacitated/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_packing_unequal_circles/__init__.py create mode 100644 examples/benchmark_tasks/optimization_packing_unequal_circles/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_packing_unequal_circles_area/__init__.py create mode 100644 examples/benchmark_tasks/optimization_packing_unequal_circles_area/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_packing_unequal_rectangles_and_squares/__init__.py create mode 100644 examples/benchmark_tasks/optimization_packing_unequal_rectangles_and_squares/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_packing_unequal_rectangles_and_squares_area/__init__.py create mode 100644 examples/benchmark_tasks/optimization_packing_unequal_rectangles_and_squares_area/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_pymoo_moead/__init__.py create mode 100644 examples/benchmark_tasks/optimization_pymoo_moead/get_instance.py create mode 100644 examples/benchmark_tasks/optimization_pymoo_moead/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_qap_construct/__init__.py create mode 100644 examples/benchmark_tasks/optimization_qap_construct/get_instance.py create mode 100644 examples/benchmark_tasks/optimization_qap_construct/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_resource_constrained_shortest_path/__init__.py create mode 100644 examples/benchmark_tasks/optimization_resource_constrained_shortest_path/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_set_cover_construct/__init__.py create mode 100644 examples/benchmark_tasks/optimization_set_cover_construct/get_instance.py create mode 100644 examples/benchmark_tasks/optimization_set_cover_construct/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_set_cover_construct/test.py create mode 100644 examples/benchmark_tasks/optimization_set_covering/__init__.py create mode 100644 examples/benchmark_tasks/optimization_set_covering/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_set_partitioning/__init__.py create mode 100644 examples/benchmark_tasks/optimization_set_partitioning/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_travelling_salesman_problem/__init__.py create mode 100644 examples/benchmark_tasks/optimization_travelling_salesman_problem/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_tsp_construct/__init__.py create mode 100644 examples/benchmark_tasks/optimization_tsp_construct/get_instance.py create mode 100644 examples/benchmark_tasks/optimization_tsp_construct/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_tsp_gls_2O/__init__.py create mode 100644 examples/benchmark_tasks/optimization_tsp_gls_2O/get_instance.py create mode 100644 examples/benchmark_tasks/optimization_tsp_gls_2O/gls.py create mode 100644 examples/benchmark_tasks/optimization_tsp_gls_2O/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_uncapacitated_warehouse_location/__init__.py create mode 100644 examples/benchmark_tasks/optimization_uncapacitated_warehouse_location/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_unconstrained_guillotine_cutting/__init__.py create mode 100644 examples/benchmark_tasks/optimization_unconstrained_guillotine_cutting/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_vehicle_routing_period_routing/__init__.py create mode 100644 examples/benchmark_tasks/optimization_vehicle_routing_period_routing/paras.yaml create mode 100644 examples/benchmark_tasks/optimization_vrptw_construct/__init__.py create mode 100644 examples/benchmark_tasks/optimization_vrptw_construct/get_instance.py create mode 100644 examples/benchmark_tasks/optimization_vrptw_construct/paras.yaml create mode 100644 examples/benchmark_tasks/science_discovery_ode_1d/__init__.py create mode 100644 examples/benchmark_tasks/science_discovery_ode_1d/paras.yaml create mode 100644 examples/benchmark_tasks/science_discovery_ode_1d/strogatz_equations.py create mode 100644 examples/convert_llm4ad_benchmark.py create mode 100644 examples/llm4ad_loader.py create mode 100644 examples/trainer_benchmark_HOWTO.md create mode 100644 examples/trainers_benchmark.py create mode 100644 examples/trainers_benchmark_tasks_validation.py diff --git a/examples/benchmark_tasks/circle_packing/__init__.py b/examples/benchmark_tasks/circle_packing/__init__.py new file mode 100644 index 00000000..5f9c6f9d --- /dev/null +++ b/examples/benchmark_tasks/circle_packing/__init__.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: circle_packing +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +from __future__ import annotations + +from typing import Any +import numpy as np +# from template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\nimport math\ndef pack_circles(n: int) -> np.ndarray:\n """\n Pack n circles in a unit square to maximize sum of radii.\n \n Args:\n n: Number of circles to pack\n\n Returns:\n Numpy array of shape (n, 3) where each row is (x, y, radius)\n All values should be between 0 and 1\n Circles must not overlap\n \n Important: Set "all" random seeds to 2025, including the packages (such as scipy sub-packages) involving random seeds.\n """\n\n grid_size = int(np.ceil(np.sqrt(n)))\n radius = 0.5 / grid_size\n\n circles = []\n for i in range(n):\n row = i // grid_size\n col = i % grid_size\n x = (col + 0.5) / grid_size\n y = (row + 0.5) / grid_size\n circles.append([x, y, radius])\n\n return np.array(circles)' +task_description = 'Implement a function that uses a constructive heuristic to pack n non-overlapping circles iteratively within a unit square to maximize the sum of their radii' + +import itertools +from llm4ad_loader import Evaluation + +__all__ = ['CirclePackingEvaluation'] + + +class CirclePackingEvaluation(Evaluation): + """Evaluator for circle packing problem in a unit square.""" + + def __init__(self, + timeout_seconds=30, + **kwargs): + """ + Args: + timeout_seconds: Time limit for evaluation + n_instance: Number of problem instances to evaluate + max_circles: Maximum number of circles to pack (n) + Raises: + ValueError: If invalid parameters are provided + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + self.n = 26 + + def evaluate_program(self, program_str: str, callable_func: callable) -> Any | None: + return self.evaluate(callable_func) + + def verify_circles(self, circles: np.ndarray) -> bool: + """Checks that the circles are disjoint and lie inside a unit square. + + Args: + circles: A numpy array of shape (num_circles, 3), where each row is + of the form (x, y, radius), specifying a circle. + + Returns: + bool: True if valid, False otherwise + """ + try: + # Check pairwise disjointness + for circle1, circle2 in itertools.combinations(circles, 2): + center_distance = np.sqrt((circle1[0] - circle2[0]) ** 2 + (circle1[1] - circle2[1]) ** 2) + radii_sum = circle1[2] + circle2[2] + if center_distance < radii_sum: + return False + + # Check all circles lie inside the unit square [0,1]x[0,1] + for circle in circles: + if not (0 <= min(circle[0], circle[1]) - circle[2] and + max(circle[0], circle[1]) + circle[2] <= 1): + return False + return True + except Exception: + return False + + + + def plot_circles(self,circles: np.ndarray): + + import matplotlib.pyplot as plt + import matplotlib.patches as patches + """Plots the circles.""" + _, ax = plt.subplots(1, figsize=(7, 7)) + ax.set_xlim(0, 1) + ax.set_ylim(0, 1) + ax.set_aspect('equal') # Make axes scaled equally. + + # Draw unit square boundary. + rect = patches.Rectangle((0, 0), 1, 1, linewidth=1, edgecolor='black', facecolor='none') + ax.add_patch(rect) + + # Draw the circles. + for circle in circles: + circ = patches.Circle((circle[0], circle[1]), circle[2], edgecolor='blue', facecolor='skyblue', alpha=0.5) + ax.add_patch(circ) + + plt.title( + f'A collection of {len(circles)} disjoint circles packed inside a unit square to maximize the sum of radii') + plt.show() + + def evaluate(self, eva: callable) -> float: + """Evaluate the circle packing solution.""" + circles = eva(self.n) + + #self.plot_circles(circles) + # Convert to numpy array if not already + circles = np.array(circles, dtype=np.float64) + + # Verify the solution + if not self.verify_circles(circles) or len(circles) != self.n: + return -float('inf') + + # Sum of radii is our score + score = np.sum(circles[:, 2]) + + return score + + + + + + +if __name__ == '__main__': + + # import numpy as np + # + # + # def pack_circles(n: int) -> np.ndarray: + # """ + # Pack n circles in a unit square to maximize sum of radii. + # + # Args: + # n: Number of circles to pack + # + # Returns: + # Numpy array of shape (n, 3) where each row is (x, y, radius) + # All values should be between 0 and 1 + # Circles must not overlap + # """ + # + # grid_size = int(np.ceil(np.sqrt(n))) + # radius = 0.5 / grid_size + # + # circles = [] + # for i in range(n): + # row = i // grid_size + # col = i % grid_size + # x = (col + 0.5) / grid_size + # y = (row + 0.5) / grid_size + # circles.append([x, y, radius]) + # + # return np.array(circles) + import numpy as np + import math + + + def pack_circles(n: int) -> np.ndarray: + """ + Pack n circles in a unit square to maximize sum of radii. + + Args: + n: Number of circles to pack + + Returns: + Numpy array of shape (n, 3) where each row is (x, y, radius) + All values should be between 0 and 1 + Circles must not overlap + """ + if n == 0: + return np.zeros((0, 3)) + + circles = np.zeros((n, 3)) + circles[0] = [0.5, 0.5, 0.5] # Place first circle at center with max possible radius + + for i in range(1, n): + max_r = 0 + best_pos = (0, 0) + + # Grid search for best position + grid_size = 100 + for x in np.linspace(0, 1, grid_size): + for y in np.linspace(0, 1, grid_size): + # Calculate minimum distance to existing circles and boundaries + min_dist = min( + min(np.sqrt((x - cx) ** 2 + (y - cy) ** 2) - cr for cx, cy, cr in circles[:i]), + x, + 1 - x, + y, + 1 - y + ) + + if min_dist > max_r: + max_r = min_dist + best_pos = (x, y) + + circles[i] = [best_pos[0], best_pos[1], max_r] + + return circles + + + pack = CirclePackingEvaluation() + pack.evaluate_program('_', pack_circles) + + +# Task configuration for benchmark task +ENTRY_NAME = 'pack_circles' +FUNCTION_SIGNATURE = 'def pack_circles(...):' +IMPORT_HEADER = 'import numpy as np\nimport math' +TASK_DESCRIPTION = 'Implement a function that uses a constructive heuristic to pack n non-overlapping circles iteratively within a unit square to maximize the sum of their radii' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `pack_circles` for the LLM4AD task.\\n\\nTask description:\\nImplement a function that uses a constructive heuristic to pack n non-overlapping circles iteratively within a unit square to maximize the sum of their radii\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\nimport math\ndef pack_circles(n: int) -> np.ndarray:\n """\n Pack n circles in a unit square to maximize sum of radii.\n \n Args:\n n: Number of circles to pack\n\n Returns:\n Numpy array of shape (n, 3) where each row is (x, y, radius)\n All values should be between 0 and 1\n Circles must not overlap\n \n Important: Set "all" random seeds to 2025, including the packages (such as scipy sub-packages) involving random seeds.\n """\n\n grid_size = int(np.ceil(np.sqrt(n)))\n radius = 0.5 / grid_size\n\n circles = []\n for i in range(n):\n row = i // grid_size\n col = i % grid_size\n x = (col + 0.5) / grid_size\n y = (row + 0.5) / grid_size\n circles.append([x, y, radius])\n\n return np.array(circles)' +EVAL_CLASS_NAME = 'CirclePackingEvaluation' +EVAL_KWARGS = {} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/circle_packing/run_eoh.py b/examples/benchmark_tasks/circle_packing/run_eoh.py new file mode 100644 index 00000000..7bc54483 --- /dev/null +++ b/examples/benchmark_tasks/circle_packing/run_eoh.py @@ -0,0 +1,33 @@ +import sys + +sys.path.append('../../') # This is for finding all the modules + +from evaluation import CirclePackingEvaluation +from llm4ad.tools.llm.llm_api_https import HttpsApi +from llm4ad.method.eoh import EoH,EoHProfiler +from llm4ad.tools.profiler import ProfilerBase + + +def main(): + llm = HttpsApi(host='api.bltcy.ai', # your host endpoint, e.g., 'api.openai.com', 'api.deepseek.com' + key='sk-bxkYIPpRbqTWS0cGB01009DfE8F94c2f8a26082248Bf7e98', # your key, e.g., 'sk-abcdefghijklmn' + model='deepseek-v3', # your llm, e.g., 'gpt-3.5-turbo' + timeout=120) + + task = CirclePackingEvaluation(timeout_seconds=1200) # local + + method = EoH(llm=llm, + profiler=EoHProfiler(log_dir='logs/eohseed', log_style='simple'), + evaluation=task, + max_sample_nums=15000, + max_generations=10000, + pop_size=32, + num_samplers=32, + num_evaluators=32, + debug_mode=False) + + method.run() + + +if __name__ == '__main__': + main() diff --git a/examples/benchmark_tasks/index.json b/examples/benchmark_tasks/index.json new file mode 100644 index 00000000..ac9e9dee --- /dev/null +++ b/examples/benchmark_tasks/index.json @@ -0,0 +1,805 @@ +[ + { + "key": "circle_packing", + "module": "circle_packing", + "entry": "pack_circles", + "eval_class": "CirclePackingEvaluation", + "task_description": "Implement a function that uses a constructive heuristic to pack n non-overlapping circles iteratively within a unit square to maximize the sum of their radii", + "wrapper": "circle_packing", + "copied_files": [ + "run_eoh.py" + ], + "benchmark": true + }, + { + "key": "online_bin_packing_local", + "module": "online_bin_packing_local", + "entry": "priority", + "eval_class": "OBPEvaluation", + "task_description": "Implement a function that returns the priority with which we want to add an item to each bin.", + "wrapper": "online_bin_packing_local", + "copied_files": [ + "run_eoh.py", + "generate_weibull_instances.py" + ], + "benchmark": true + }, + { + "key": "optimization/tsp_gls_2O", + "module": "optimization_tsp_gls_2O", + "entry": "update_edge_distance", + "eval_class": "TSP_GLS_2O_Evaluation", + "task_description": "Given an edge distance matrix and a local optimal route, please help me design a strategy to update the distance matrix to avoid being trapped in the local optimum with the final goal of finding a tour with minimized distance. You should create a heuristic for me to update the edge distance matrix.", + "wrapper": "optimization_tsp_gls_2O", + "copied_files": [ + "get_instance.py", + "__init__.py", + "gls.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/set_cover_construct", + "module": "optimization_set_cover_construct", + "entry": "select_next_subset", + "eval_class": "SCPEvaluation", + "task_description": "'", + "wrapper": "optimization_set_cover_construct", + "copied_files": [ + "get_instance.py", + "test.py", + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/tsp_construct", + "module": "optimization_tsp_construct", + "entry": "select_next_node", + "eval_class": "TSPEvaluation", + "task_description": "\"Given a set of nodes with their coordinates, you need to find the shortest route that visits each node once and returns to the starting node. \\", + "wrapper": "optimization_tsp_construct", + "copied_files": [ + "get_instance.py", + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/bp_2d_construct", + "module": "optimization_bp_2d_construct", + "entry": "determine_next_assignment", + "eval_class": "BP2DEvaluation", + "task_description": "'", + "wrapper": "optimization_bp_2d_construct", + "copied_files": [ + "get_instance.py", + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/online_bin_packing_2O", + "module": "optimization_online_bin_packing_2O", + "entry": "priority", + "eval_class": "OBP_2O_Evaluation", + "task_description": "Implement a function that returns the priority with which we want to add an item to each bin.", + "wrapper": "optimization_online_bin_packing_2O", + "copied_files": [ + "__init__.py", + "generate_weibull_instances.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/cflp_construct", + "module": "optimization_cflp_construct", + "entry": "select_next_assignment", + "eval_class": "CFLPEvaluation", + "task_description": "'", + "wrapper": "optimization_cflp_construct", + "copied_files": [ + "get_instance.py", + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/vrptw_construct", + "module": "optimization_vrptw_construct", + "entry": "select_next_node", + "eval_class": "VRPTWEvaluation", + "task_description": "The task involves finding optimal routes for a fleet of vehicles to serve a set of customers, respecting time windows and vehicle capacity constraints. Help me design an algorithm to select the next node in each step.", + "wrapper": "optimization_vrptw_construct", + "copied_files": [ + "get_instance.py", + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/online_bin_packing", + "module": "optimization_online_bin_packing", + "entry": "priority", + "eval_class": "OBPEvaluation", + "task_description": "Implement a function that returns the priority with which we want to add an item to each bin.", + "wrapper": "optimization_online_bin_packing", + "copied_files": [ + "__init__.py", + "generate_weibull_instances.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/knapsack_construct", + "module": "optimization_knapsack_construct", + "entry": "select_next_item", + "eval_class": "KnapsackEvaluation", + "task_description": "'", + "wrapper": "optimization_knapsack_construct", + "copied_files": [ + "get_instance.py", + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/pymoo_moead", + "module": "optimization_pymoo_moead", + "entry": "custom_decomposition", + "eval_class": "MOEAD_PYMOO_Evaluation", + "task_description": "\"", + "wrapper": "optimization_pymoo_moead", + "copied_files": [ + "get_instance.py", + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/cvrp_construct", + "module": "optimization_cvrp_construct", + "entry": "select_next_node", + "eval_class": "CVRPEvaluation", + "task_description": "\"", + "wrapper": "optimization_cvrp_construct", + "copied_files": [ + "get_instance.py", + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/jssp_construct", + "module": "optimization_jssp_construct", + "entry": "determine_next_operation", + "eval_class": "JSSPEvaluation", + "task_description": "'", + "wrapper": "optimization_jssp_construct", + "copied_files": [ + "get_instance.py", + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/bp_1d_construct", + "module": "optimization_bp_1d_construct", + "entry": "determine_next_assignment", + "eval_class": "BP1DEvaluation", + "task_description": "'", + "wrapper": "optimization_bp_1d_construct", + "copied_files": [ + "get_instance.py", + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/admissible_set", + "module": "optimization_admissible_set", + "entry": "priority", + "eval_class": "ASPEvaluation", + "task_description": "\"\"\"\\", + "wrapper": "optimization_admissible_set", + "copied_files": [ + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/qap_construct", + "module": "optimization_qap_construct", + "entry": "select_next_assignment", + "eval_class": "QAPEvaluation", + "task_description": "'", + "wrapper": "optimization_qap_construct", + "copied_files": [ + "get_instance.py", + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/ovrp_construct", + "module": "optimization_ovrp_construct", + "entry": "select_next_node", + "eval_class": "OVRPEvaluation", + "task_description": "\"", + "wrapper": "optimization_ovrp_construct", + "copied_files": [ + "get_instance.py", + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/co_bench/open_shop_scheduling_co_bench", + "module": "optimization_open_shop_scheduling", + "entry": "solve", + "eval_class": "OSSEvaluationCB", + "task_description": "(\"The Open Shop Scheduling Problem involves scheduling a set of jobs across a set of machines with \"", + "wrapper": "optimization_open_shop_scheduling", + "copied_files": [ + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/co_bench/generalised_assignment_problem_co_bench", + "module": "optimization_generalised_assignment_problem", + "entry": "solve", + "eval_class": "GAPEvaluationCB", + "task_description": "(\"The Generalized Assignment Problem (GAP) involves assigning \\( n \\) jobs to \\( m \\) agents such \"", + "wrapper": "optimization_generalised_assignment_problem", + "copied_files": [ + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/co_bench/flow_shop_scheduling_co_bench", + "module": "optimization_flow_shop_scheduling", + "entry": "solve", + "eval_class": "FSSEvaluationCB", + "task_description": "(\"Given n jobs and m machines, the goal of the flow shop scheduling problem is to determine \"", + "wrapper": "optimization_flow_shop_scheduling", + "copied_files": [ + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/co_bench/set_partitioning_co_bench", + "module": "optimization_set_partitioning", + "entry": "solve", + "eval_class": "SPEvaluationCB", + "task_description": "(\"This problem involves solving a set partitioning instance where the goal is to choose a subset \"", + "wrapper": "optimization_set_partitioning", + "copied_files": [ + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/co_bench/maximal_independent_set_co_bench", + "module": "optimization_maximal_independent_set", + "entry": "solve", + "eval_class": "MISEvaluationCB", + "task_description": "(\"The Maximum Independent Set (MIS) problem is a fundamental NP-hard optimization problem in graph \"", + "wrapper": "optimization_maximal_independent_set", + "copied_files": [ + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/co_bench/container_loading_co_bench", + "module": "optimization_container_loading", + "entry": "solve", + "eval_class": "CLEvaluationCB", + "task_description": "(\"Solves a container loading problem: Given a 3D container of specified dimensions and multiple \"", + "wrapper": "optimization_container_loading", + "copied_files": [ + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/co_bench/equitable_partitioning_problem_co_bench", + "module": "optimization_equitable_partitioning_problem", + "entry": "solve", + "eval_class": "EPPEvaluationCB", + "task_description": "(\"The task is to partition a set of individuals\u2014each characterized by multiple binary \"", + "wrapper": "optimization_equitable_partitioning_problem", + "copied_files": [ + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/co_bench/p_median_uncapacitated_co_bench", + "module": "optimization_p_median_uncapacitated", + "entry": "solve", + "eval_class": "PMUEvaluationCB", + "task_description": "(\"The uncapacitated p-median problem is a combinatorial optimization problem defined on a given \"", + "wrapper": "optimization_p_median_uncapacitated", + "copied_files": [ + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/co_bench/crew_scheduling_co_bench", + "module": "optimization_crew_scheduling", + "entry": "solve", + "eval_class": "CSchedulingEvaluationCB", + "task_description": "(\"The Crew Scheduling Problem involves assigning each task\u2014with defined start and finish times\u2014to \"", + "wrapper": "optimization_crew_scheduling", + "copied_files": [ + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/co_bench/euclidean_steiner_problem_co_bench", + "module": "optimization_euclidean_steiner_problem", + "entry": "solve", + "eval_class": "ESPEvaluationCB", + "task_description": "(\"Given a set of 2D points (terminals), the goal of the Euclidean Steiner Problem is to compute a \"", + "wrapper": "optimization_euclidean_steiner_problem", + "copied_files": [ + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/co_bench/unconstrained_guillotine_cutting_co_bench", + "module": "optimization_unconstrained_guillotine_cutting", + "entry": "solve", + "eval_class": "UGCEvaluationCB", + "task_description": "(\"The unconstrained guillotine cutting problem involves selecting and placing a subset of \"", + "wrapper": "optimization_unconstrained_guillotine_cutting", + "copied_files": [ + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/co_bench/packing_unequal_circles_co_bench", + "module": "optimization_packing_unequal_circles", + "entry": "solve", + "eval_class": "PUCEvaluationCB", + "task_description": "(\"The problem involves packing a subset of unequal circles into a fixed circular container with \"", + "wrapper": "optimization_packing_unequal_circles", + "copied_files": [ + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/co_bench/packing_unequal_rectangles_and_squares_area_co_bench", + "module": "optimization_packing_unequal_rectangles_and_squares_area", + "entry": "solve", + "eval_class": "PURSAEvaluationCB", + "task_description": "(\"We consider the problem of selecting and placing a subset of n unequal rectangles (or squares) \"", + "wrapper": "optimization_packing_unequal_rectangles_and_squares_area", + "copied_files": [ + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/co_bench/hybrid_reentrant_shop_scheduling_co_bench", + "module": "optimization_hybrid_reentrant_shop_scheduling", + "entry": "solve", + "eval_class": "HRSSEvaluationCB", + "task_description": "(\"The problem is a Hybrid Reentrant Shop Scheduling problem where each of n jobs must sequentially \"", + "wrapper": "optimization_hybrid_reentrant_shop_scheduling", + "copied_files": [ + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/co_bench/travelling_salesman_problem_co_bench", + "module": "optimization_travelling_salesman_problem", + "entry": "solve", + "eval_class": "TSPEvaluationCB", + "task_description": "(\"The Traveling Salesman Problem (TSP) is a classic combinatorial optimization problem where, \"", + "wrapper": "optimization_travelling_salesman_problem", + "copied_files": [ + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/co_bench/uncapacitated_warehouse_location_co_bench", + "module": "optimization_uncapacitated_warehouse_location", + "entry": "solve", + "eval_class": "UWLEvaluationCB", + "task_description": "(\"The Uncapacitated Warehouse Location Problem aims to determine which warehouses to open and how \"", + "wrapper": "optimization_uncapacitated_warehouse_location", + "copied_files": [ + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/co_bench/bp_1d_co_bench", + "module": "optimization_bp_1d", + "entry": "solve", + "eval_class": "BP1DEvaluationCB", + "task_description": "(\"The **one-dimensional bin packing problem** seeks to minimize the number of bins required to \"", + "wrapper": "optimization_bp_1d", + "copied_files": [ + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/co_bench/job_shop_scheduling_co_bench", + "module": "optimization_job_shop_scheduling", + "entry": "solve", + "eval_class": "JSSEvaluationCB", + "task_description": "(\"The job shop scheduling problem requires assigning non-negative integer start times to a set of \"", + "wrapper": "optimization_job_shop_scheduling", + "copied_files": [ + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/co_bench/corporate_structuring_co_bench", + "module": "optimization_corporate_structuring", + "entry": "solve", + "eval_class": "CSEvaluationCB", + "task_description": "'''Given N countries, each defined by:", + "wrapper": "optimization_corporate_structuring", + "copied_files": [ + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/co_bench/assignment_problem_co_bench", + "module": "optimization_assignment_problem", + "entry": "solve", + "eval_class": "APEvaluationCB", + "task_description": "(\"The Assignment Problem involves optimally assigning n items to n agents based on a provided \"", + "wrapper": "optimization_assignment_problem", + "copied_files": [ + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/co_bench/packing_unequal_rectangles_and_squares_co_bench", + "module": "optimization_packing_unequal_rectangles_and_squares", + "entry": "solve", + "eval_class": "PURSEvaluationCB", + "task_description": "(\"We are given a set of n unequal rectangles (or squares), each with specified dimensions, \"", + "wrapper": "optimization_packing_unequal_rectangles_and_squares", + "copied_files": [ + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/co_bench/assortment_problem_co_bench", + "module": "optimization_assortment_problem", + "entry": "solve", + "eval_class": "AssortPEvaluationCB", + "task_description": "(\"This optimization problem involves arranging a set of rectangular pieces within available stock \"", + "wrapper": "optimization_assortment_problem", + "copied_files": [ + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/co_bench/set_covering_co_bench", + "module": "optimization_set_covering", + "entry": "solve", + "eval_class": "SCEvaluationCB", + "task_description": "(\"Set Covering Problem. The goal is to select a subset of columns, each with an associated cost, \"", + "wrapper": "optimization_set_covering", + "copied_files": [ + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/co_bench/p_median_capacitated_co_bench", + "module": "optimization_p_median_capacitated", + "entry": "solve", + "eval_class": "PMCEvaluationCB", + "task_description": "(\"The Capacitated P-Median Problem is a facility location optimization problem where the objective \"", + "wrapper": "optimization_p_median_capacitated", + "copied_files": [ + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/co_bench/multi_demand_multidimensional_knapsack_problem_co_bench", + "module": "optimization_multi_demand_multidimensional_knapsack_problem", + "entry": "solve", + "eval_class": "MDMKPEvaluationCB", + "task_description": "(\"The Multi-Demand Multidimensional Knapsack Problem (MDMKP) is a binary optimization problem that \"", + "wrapper": "optimization_multi_demand_multidimensional_knapsack_problem", + "copied_files": [ + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/co_bench/container_loading_with_weight_restrictions_co_bench", + "module": "optimization_container_loading_with_weight_restrictions", + "entry": "solve", + "eval_class": "CLWREvaluationCB", + "task_description": "(\"The Container Loading with Weight Restrictions problem aims to maximize the utilization of a \"", + "wrapper": "optimization_container_loading_with_weight_restrictions", + "copied_files": [ + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/co_bench/capacitated_warehouse_location_co_bench", + "module": "optimization_capacitated_warehouse_location", + "entry": "solve", + "eval_class": "CWLEvaluationCB", + "task_description": "(\"The Capacitated Warehouse Location Problem with Splittable Demand aims to determine which \"", + "wrapper": "optimization_capacitated_warehouse_location", + "copied_files": [ + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/co_bench/common_due_date_scheduling_co_bench", + "module": "optimization_common_due_date_scheduling", + "entry": "solve", + "eval_class": "CDDSEvaluationCB", + "task_description": "(\"The **Restricted Single-Machine Common Due Date Scheduling Problem** involves scheduling a set \"", + "wrapper": "optimization_common_due_date_scheduling", + "copied_files": [ + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/co_bench/constrained_guillotine_cutting_co_bench", + "module": "optimization_constrained_guillotine_cutting", + "entry": "solve", + "eval_class": "CGCEvaluationCB", + "task_description": "(\"The problem involves optimizing the guillotine feasible placement of a set of rectangular pieces \"", + "wrapper": "optimization_constrained_guillotine_cutting", + "copied_files": [ + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/co_bench/packing_unequal_circles_area_co_bench", + "module": "optimization_packing_unequal_circles_area", + "entry": "solve", + "eval_class": "PUCAEvaluationCB", + "task_description": "(\"The problem involves packing a subset of unequal circles into a fixed circular container with \"", + "wrapper": "optimization_packing_unequal_circles_area", + "copied_files": [ + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/co_bench/graph_colouring_co_bench", + "module": "optimization_graph_colouring", + "entry": "solve", + "eval_class": "GCEvaluationCB", + "task_description": "(\"Given a graph in DIMACS format with vertices, edges, and an adjacency list, the goal is to \"", + "wrapper": "optimization_graph_colouring", + "copied_files": [ + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/co_bench/vehicle_routing_period_routing_co_bench", + "module": "optimization_vehicle_routing_period_routing", + "entry": "solve", + "eval_class": "VRPREvaluationCB", + "task_description": "(\"The Period Vehicle Routing Problem requires planning delivery routes over a multi\u2010day planning \"", + "wrapper": "optimization_vehicle_routing_period_routing", + "copied_files": [ + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/co_bench/resource_constrained_shortest_path_co_bench", + "module": "optimization_resource_constrained_shortest_path", + "entry": "solve", + "eval_class": "RCSPEvaluationCB", + "task_description": "(\"This problem involves finding the shortest path from vertex 1 to vertex n in a directed graph \"", + "wrapper": "optimization_resource_constrained_shortest_path", + "copied_files": [ + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/co_bench/multidimensional_knapsack_problem_co_bench", + "module": "optimization_multidimensional_knapsack_problem", + "entry": "solve", + "eval_class": "MKPEvaluationCB", + "task_description": "(\"This problem is a multidimensional knapsack optimization where the objective is to maximize the \"", + "wrapper": "optimization_multidimensional_knapsack_problem", + "copied_files": [ + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/co_bench/aircraft_landing_co_bench", + "module": "optimization_aircraft_landing", + "entry": "solve", + "eval_class": "ALEvaluationCB", + "task_description": "(\"The problem is to schedule landing times for a set of planes across one or more runways such that \"", + "wrapper": "optimization_aircraft_landing", + "copied_files": [ + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "optimization/co_bench/constrained_non_guillotine_cutting_co_bench", + "module": "optimization_constrained_non_guillotine_cutting", + "entry": "solve", + "eval_class": "CNCEvaluationCB", + "task_description": "(\"The constrained non-guillotine cutting problem involves optimally arranging rectangular pieces \"", + "wrapper": "optimization_constrained_non_guillotine_cutting", + "copied_files": [ + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "science_discovery/ode_1d", + "module": "science_discovery_ode_1d", + "entry": "equation", + "eval_class": "ODEEvaluation", + "task_description": "(\"Find the ODE mathematical function skeleton, given data on initial x. The function should be differentiable, continuous.\"", + "wrapper": "science_discovery_ode_1d", + "copied_files": [ + "strogatz_equations.py", + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "machine_learning/pendulum", + "module": "machine_learning_pendulum", + "entry": "choose_action", + "eval_class": "PendulumEvaluation", + "task_description": "(\"Implement a novel control strategy for the inverted pendulum swing-up problem. The goal is to \"", + "wrapper": "machine_learning_pendulum", + "copied_files": [ + "test.py", + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "machine_learning/moon_lander", + "module": "machine_learning_moon_lander", + "entry": "choose_action", + "eval_class": "MoonLanderEvaluation", + "task_description": "(\"Implement a novel heuristic strategy heuristic strategy function that guides the \"", + "wrapper": "machine_learning_moon_lander", + "copied_files": [ + "test.py", + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "machine_learning/car_mountain_continue", + "module": "machine_learning_car_mountain_continue", + "entry": "choose_action", + "eval_class": "CarMountainCEvaluation", + "task_description": "(\"Implement a function that designing a novel strategy function that guide the car along an uneven \"", + "wrapper": "machine_learning_car_mountain_continue", + "copied_files": [ + "test.py", + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "machine_learning/acrobot", + "module": "machine_learning_acrobot", + "entry": "choose_action", + "eval_class": "AcrobotEvaluation", + "task_description": "(\"I need help designing an innovative heuristic strategy function to control an acrobot, aiming to \"", + "wrapper": "machine_learning_acrobot", + "copied_files": [ + "test.py", + "__init__.py", + "paras.yaml" + ], + "benchmark": true + }, + { + "key": "machine_learning/car_mountain", + "module": "machine_learning_car_mountain", + "entry": "choose_action", + "eval_class": "CarMountainEvaluation", + "task_description": "(\"Implement a function that designing a novel strategy function that guide the car along an uneven \"", + "wrapper": "machine_learning_car_mountain", + "copied_files": [ + "test.py", + "__init__.py", + "q-learning.py", + "paras.yaml" + ], + "benchmark": true + } +] \ No newline at end of file diff --git a/examples/benchmark_tasks/machine_learning_acrobot/__init__.py b/examples/benchmark_tasks/machine_learning_acrobot/__init__.py new file mode 100644 index 00000000..576840c2 --- /dev/null +++ b/examples/benchmark_tasks/machine_learning_acrobot/__init__.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: machine_learning_acrobot +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# Module Name: AcrobotEvaluation +# Last Revision: 2025/3/5 +# Description: Designs a heuristic strategy function for controlling an acrobot system. +# The function selects actions based on joint angles and angular velocities +# to efficiently swing the lower link and generate momentum for the upper +# link to reach the target height. +# This module is part of the LLM4AD project (https://github.com/Optima-CityU/llm4ad). +# +# Parameters: +# - cos_theta1: float - cosine of theta1, range [-1, 1] (default: None). +# - sin_theta1: float - sine of theta1, range [-1, 1] (default: None). +# - cos_theta2: float - cosine of theta2, range [-1, 1] (default: None). +# - sin_theta2: float - sine of theta2, range [-1, 1] (default: None). +# - a_v_theta1: float - angular velocity of theta1, range [-12.567, 12.567] (default: None). +# - a_v_theta2: float - angular velocity of theta2, range [-28.274, 28.274] (default: None). +# - last_action: int - last action taken, values [0, 1, 2] (default: None). +# - timeout_seconds: int - Maximum allowed time (in seconds) for the evaluation process (default: 20). +# +# References: +# - Brockman, Greg, et al. "Openai gym." arXiv preprint arXiv:1606.01540 (2016). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + + + +from __future__ import annotations + +from typing import Any +import os, sys +sys.path.insert(0, os.path.dirname(__file__)) +import gym + +from llm4ad_loader import Evaluation +# from llm4ad.task.machine_learning.acrobot.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\n\ndef choose_action(ct1: float, st1: float, ct2: float, st2: float, avt1: float, avt2: float, last_action: int) -> int: \n """\n Design a novel algorithm to select the action in each step.\n\n Args:\n ct1: cosine of theta1, float between [-1, 1].\n st1: sine of theta1, float between [-1, 1]\n ct2: cosine of theta2, float between [-1, 1].\n st2: sine of theta2, float between [-1, 1].\n avt1: angular velocity of theta1, float between [-12.567, 12.567].\n avt2: angular velocity of theta2, float between [-28.274, 28.274].\n\n\n Return:\n An integer representing the selected action for the acrobot.\n 0: apply -1 torque on actuated joint.\n 1: apply 0 torque on actuated joint\n 2: apply +1 torque on actuated joint.\n\n """\n # this is a placehold, replace it with your algorithm\n action = np.random.randint(3)\n\n return action' +task_description = '("I need help designing an innovative heuristic strategy function to control an acrobot, aiming to "' + + +__all__ = ['AcrobotEvaluation'] + + +def evaluate(env: gym.Env, action_select: callable) -> float: + """Evaluate heuristic function on car mountain problem.""" + + observation, _ = env.reset() # initialization + action = 0 # initial action + + for i in range(env._max_episode_steps + 1): # protect upper limits + action = action_select(observation[0], + observation[1], + observation[2], + observation[3], + observation[4], + observation[5], + action) + observation, reward, done, truncated, info = env.step(action) + + if done or truncated: + # self.env.close() + fitness = observation[0] + (observation[0] * observation[2] - observation[1] * observation[3]) + 2 + if fitness <= 1: + return -(i + 1) / env._max_episode_steps + else: + return -fitness + + +class AcrobotEvaluation(Evaluation): + """Evaluator for car mountain problem.""" + + def __init__(self, max_steps=500, timeout_seconds=20, **kwargs): + """ + Args: + - 'max_steps' (int): Maximum number of steps allowed per episode in the MountainCar-v0 environment (default is 500). + - '**kwargs' (dict): Additional keyword arguments passed to the parent class initializer. + + Attributes: + - 'env' (gym.Env): The MountainCar-v0 environment with a modified maximum episode length. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + self.env = None + self.env = gym.make('Acrobot-v1') + self.env._max_episode_steps = max_steps + + def evaluate_program(self, program_str: str, callable_func: callable) -> Any | None: + return evaluate(self.env, callable_func) + +# Task configuration for benchmark task +ENTRY_NAME = 'choose_action' +FUNCTION_SIGNATURE = 'def choose_action(...):' +IMPORT_HEADER = 'import numpy as np\nimport math' +TASK_DESCRIPTION = '("I need help designing an innovative heuristic strategy function to control an acrobot, aiming to "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `choose_action` for the LLM4AD task.\\n\\nTask description:\\n("I need help designing an innovative heuristic strategy function to control an acrobot, aiming to "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\n\ndef choose_action(ct1: float, st1: float, ct2: float, st2: float, avt1: float, avt2: float, last_action: int) -> int: \n """\n Design a novel algorithm to select the action in each step.\n\n Args:\n ct1: cosine of theta1, float between [-1, 1].\n st1: sine of theta1, float between [-1, 1]\n ct2: cosine of theta2, float between [-1, 1].\n st2: sine of theta2, float between [-1, 1].\n avt1: angular velocity of theta1, float between [-12.567, 12.567].\n avt2: angular velocity of theta2, float between [-28.274, 28.274].\n\n\n Return:\n An integer representing the selected action for the acrobot.\n 0: apply -1 torque on actuated joint.\n 1: apply 0 torque on actuated joint\n 2: apply +1 torque on actuated joint.\n\n """\n # this is a placehold, replace it with your algorithm\n action = np.random.randint(3)\n\n return action' +EVAL_CLASS_NAME = 'AcrobotEvaluation' +EVAL_KWARGS = {'max_steps': 500, 'timeout_seconds': 20} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/machine_learning_acrobot/paras.yaml b/examples/benchmark_tasks/machine_learning_acrobot/paras.yaml new file mode 100644 index 00000000..4a02375e --- /dev/null +++ b/examples/benchmark_tasks/machine_learning_acrobot/paras.yaml @@ -0,0 +1,3 @@ +name: AcrobotEvaluation +max_steps: 500 +timeout_seconds: 20 \ No newline at end of file diff --git a/examples/benchmark_tasks/machine_learning_acrobot/test.py b/examples/benchmark_tasks/machine_learning_acrobot/test.py new file mode 100644 index 00000000..f9bd2a1a --- /dev/null +++ b/examples/benchmark_tasks/machine_learning_acrobot/test.py @@ -0,0 +1,46 @@ +import gym +import numpy as np + +# 初始化Acrobot-v1环境 +env = gym.make('Acrobot-v1') # , render_mode='human' + + +# 定义动作选择函数 +def choose_action(ct1: float, st1: float, ct2: float, st2: float, avt1: float, avt2: float, last_action: int) -> int: + if ct1 >= 0 and st1 >= 0 and avt1 < 0: + action = 2 + elif st1 < 0 and avt1 == 0 and st2 < 0 and avt2 == 0: + action = 0 + elif last_action == 2: + action = 0 + else: + action = 2 + + return action + + +# 环境重置 +observation, _ = env.reset() + +done = False +step = 0 +action = 1 +while not done: + step += 1 + theta1, theta2, theta1_dot, theta2_dot, avt1, avt2 = observation # 提取状态信息 + action = choose_action(theta1, theta2, theta1_dot, theta2_dot, avt1, avt2, action) # 决策动作 + + # 执行动作并获得新状态 + observation, reward, done, t, info = env.step(action) + + print(f"Step: {step}") + print(f"Theta1: {theta1}, Theta2: {theta2}") + print(f"Theta1_dot: {theta1_dot}, Theta2_dot: {theta2_dot}") + print(f"Action: {action}, Reward: {reward}, Done: {done}") + print(f"{(step + 1) / 500}") + + # 渲染环境 + env.render() + +# 关闭环境 +env.close() diff --git a/examples/benchmark_tasks/machine_learning_car_mountain/__init__.py b/examples/benchmark_tasks/machine_learning_car_mountain/__init__.py new file mode 100644 index 00000000..dae3d181 --- /dev/null +++ b/examples/benchmark_tasks/machine_learning_car_mountain/__init__.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: machine_learning_car_mountain +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# Module Name: CarMountainEvaluation +# Last Revision: 2025/3/5 +# Description: Designs a heuristic strategy function for controlling a car along an uneven road (Mountain Car problem). +# The function selects actions based on the car's position and velocity to efficiently guide the car +# towards a target in the minimum number of steps. +# This module is part of the LLM4AD project (https://github.com/Optima-CityU/llm4ad). +# +# Parameters: +# - position: float - Car's position, range [-1.2, 0.6] (default: None). +# - velocity: float - Car's velocity, range [-0.07, 0.07] (default: None). +# - last_action: int - Car's last move, values [0, 1, 2] (default: None). +# - timeout_seconds: int - Maximum allowed time (in seconds) for the evaluation process (default: 20). +# +# References: +# - Brockman, Greg, et al. "Openai gym." arXiv preprint arXiv:1606.01540 (2016). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + + + +from __future__ import annotations + +from typing import Any +import os, sys +sys.path.insert(0, os.path.dirname(__file__)) +import gym + +from llm4ad_loader import Evaluation +# from llm4ad.task.machine_learning.car_mountain.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\n\ndef choose_action(pos: float, v: float, last_action: int) -> int:\n """Return the action for the car to proceed the next move.\n Args:\n pos: Car\'s position, a float ranges between [-1.2, 0.6].\n v: Car\'s velocity, a float ranges between [-0.07, 0.07].\n last_action: Car\'s next move, a int ranges between [0, 1, 2].\n Return:\n An integer representing the selected action for the car.\n 0: accelerate to left\n 1: don\'t accelerate\n 2: accelerate to right\n """\n return np.random.randint(3)' +task_description = '("Implement a function that designing a novel strategy function that guide the car along an uneven "' + + +__all__ = ['CarMountainEvaluation'] + + +def evaluate(env: gym.Env, action_select: callable) -> float: + """Evaluate heuristic function on car mountain problem.""" + + observation, _ = env.reset() # initialization + action = 1 # initial action, stay static + + for i in range(env._max_episode_steps): + action = action_select(observation[0], observation[1], action) + observation, reward, done, truncated, info = env.step(action) + + if done: + return -(i / env._max_episode_steps) # succeed + + if truncated: + return -(max(0.5 - observation[0], 0) + 1) # failed + + +class CarMountainEvaluation(Evaluation): + """Evaluator for car mountain problem.""" + + def __init__(self, max_steps=500, timeout_seconds=20, **kwargs): + """ + Args: + - 'max_steps' (int): Maximum number of steps allowed per episode in the MountainCar-v0 environment (default is 500). + - '**kwargs' (dict): Additional keyword arguments passed to the parent class initializer. + + Attributes: + - 'env' (gym.Env): The MountainCar-v0 environment with a modified maximum episode length. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + self.env = None + self.env = gym.make('MountainCar-v0') + self.env._max_episode_steps = max_steps + + def evaluate_program(self, program_str: str, callable_func: callable) -> Any | None: + return evaluate(self.env, callable_func) + +# Task configuration for benchmark task +ENTRY_NAME = 'choose_action' +FUNCTION_SIGNATURE = 'def choose_action(...):' +IMPORT_HEADER = 'import numpy as np\nimport math' +TASK_DESCRIPTION = '("Implement a function that designing a novel strategy function that guide the car along an uneven "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `choose_action` for the LLM4AD task.\\n\\nTask description:\\n("Implement a function that designing a novel strategy function that guide the car along an uneven "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\n\ndef choose_action(pos: float, v: float, last_action: int) -> int:\n """Return the action for the car to proceed the next move.\n Args:\n pos: Car\'s position, a float ranges between [-1.2, 0.6].\n v: Car\'s velocity, a float ranges between [-0.07, 0.07].\n last_action: Car\'s next move, a int ranges between [0, 1, 2].\n Return:\n An integer representing the selected action for the car.\n 0: accelerate to left\n 1: don\'t accelerate\n 2: accelerate to right\n """\n return np.random.randint(3)' +EVAL_CLASS_NAME = 'CarMountainEvaluation' +EVAL_KWARGS = {'max_steps': 500, 'timeout_seconds': 20} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/machine_learning_car_mountain/paras.yaml b/examples/benchmark_tasks/machine_learning_car_mountain/paras.yaml new file mode 100644 index 00000000..c36f71f7 --- /dev/null +++ b/examples/benchmark_tasks/machine_learning_car_mountain/paras.yaml @@ -0,0 +1,3 @@ +name: CarMountainEvaluation +max_steps: 500 +timeout_seconds: 20 \ No newline at end of file diff --git a/examples/benchmark_tasks/machine_learning_car_mountain/q-learning.py b/examples/benchmark_tasks/machine_learning_car_mountain/q-learning.py new file mode 100644 index 00000000..cdbd6c42 --- /dev/null +++ b/examples/benchmark_tasks/machine_learning_car_mountain/q-learning.py @@ -0,0 +1,123 @@ +import numpy as np +import pandas as pd +import time +import gym +import csv +import os +import pickle +from queue import Queue + + +class QLearning: + def __init__(self, actions_space, learning_rate=0.01, reward_decay=0.99, e_greedy=0.6): + self.actions = actions_space # 动作空间 + self.lr = learning_rate # 学习率 + self.gamma = reward_decay # 回报衰减率 + self.epsilon = e_greedy # 探索/利用 贪婪系数 + self.num_pos = 20 # 位置分为num_pos份 + self.num_vel = 14 # 速度分为num_vel份 + self.q_table = np.random.uniform(low=-1, high=1, size=(self.num_pos * self.num_vel, self.actions.n)) # Q值表 + self.pos_bins = self.toBins(-1.2, 0.6, self.num_pos) + self.vel_bins = self.toBins(-0.07, 0.07, self.num_vel) + + def choose_action(self, state): + if np.random.uniform() < self.epsilon: + action = np.argmax(self.q_table[state]) + else: + action = self.actions.sample() + return action + + def toBins(self, clip_min, clip_max, num): + return np.linspace(clip_min, clip_max, num + 1) + + def digit(self, x, bin): + n = np.digitize(x, bins=bin) + if x == bin[-1]: + n = n - 1 + return n + + def digitize_state(self, observation): + cart_pos, cart_v = observation + digitized = [self.digit(cart_pos, self.pos_bins), + self.digit(cart_v, self.vel_bins)] + return (digitized[1] - 1) * self.num_pos + digitized[0] - 1 + + def learn(self, state, action, r, next_state): + next_action = np.argmax(self.q_table[next_state]) + q_predict = self.q_table[state, action] + q_target = r + self.gamma * self.q_table[next_state, next_action] + self.q_table[state, action] += self.lr * (q_target - q_predict) + + +def train(): + env = gym.make('MountainCar-v0', render_mode='human') # 指定渲染模式为 human + print(env.action_space) + agent = QLearning(env.action_space) + + for i in range(10000): # 训练次数 + observation, _ = env.reset() # 状态 + state = agent.digitize_state(observation) # 状态标准化 + for t in range(300): # 一次训练最大运行次数 + action = agent.choose_action(state) # 动作 + observation, reward, done, truncated, info = env.step(action) + next_state = agent.digitize_state(observation) + + if reward == 0: # 到达山顶时 reward 为 0 + reward += 1000 # 给大一点的奖励 + + print(f"step: {t}", action, reward, done, state, next_state, truncated) + agent.learn(state, action, reward, next_state) + state = next_state + + env.render() # 每一步渲染画面 + + if done or truncated: # 重新加载环境 + print("Episode finished after {} timesteps".format(t + 1)) + break + + print(agent.q_table) + env.close() + + # 保存模型 + with open(os.getcwd() + '/tmp/carmountain.model', 'wb') as f: + pickle.dump(agent, f) + + +def test(): + env = gym.make('MountainCar-v0', render_mode='human') # 指定渲染模式为 human + print(env.action_space) + with open(os.getcwd() + '/tmp/carmountain.model', 'rb') as f: + agent = pickle.load(f) + agent.actions = env.action_space # 初始化 + agent.epsilon = 1 + observation, _ = env.reset() # 初始化状态 + state = agent.digitize_state(observation) # 状态标准化 + + for t in range(500): # 一次测试最大运行次数 + action = agent.choose_action(state) # + observation, reward, done, truncated, info = env.step(action) + next_state = agent.digitize_state(observation) + print(action, reward, done, state, next_state) + agent.learn(state, action, reward, next_state) + state = next_state + env.render() # 渲染画面 + env.close() # 关闭环境 + + +def run_test(): + env = gym.make('MountainCar-v0') + observation, _ = env.reset() # 状态包括以下因素 + + for t in range(500): + action = np.random.choice([0, 1, 2]) # 动作 + observation, reward, done, truncated, info = env.step(action) + print(action, reward, done) + print(observation) + env.render() + time.sleep(0.02) + env.close() + + +if __name__ == '__main__': + train() # 训练 + test() # 训练结束后测试 diff --git a/examples/benchmark_tasks/machine_learning_car_mountain/test.py b/examples/benchmark_tasks/machine_learning_car_mountain/test.py new file mode 100644 index 00000000..ef893033 --- /dev/null +++ b/examples/benchmark_tasks/machine_learning_car_mountain/test.py @@ -0,0 +1,167 @@ +import numpy as np +import pandas as pd +import time +import gym +import tqdm +import csv +import os +import pickle +from queue import Queue + + +class QLearning: + def __init__(self, actions_space, learning_rate=0.01, reward_decay=0.99, e_greedy=0.6): + self.actions = actions_space # 动作空间 + self.lr = learning_rate # 学习率 + self.gamma = reward_decay # 回报衰减率 + self.epsilon = e_greedy # 探索/利用 贪婪系数 + self.num_pos = 20 # 位置分为num_pos份 + self.num_vel = 14 # 速度分为num_vel份 + self.q_table = np.random.uniform(low=-1, high=1, size=(self.num_pos * self.num_vel, self.actions.n)) # Q值表 + self.pos_bins = self.toBins(-1.2, 0.6, self.num_pos) + self.vel_bins = self.toBins(-0.07, 0.07, self.num_vel) + + def choose_action(self, state): + if np.random.uniform() < self.epsilon: + action = np.argmax(self.q_table[state]) + else: + action = self.actions.sample() + return action + + def toBins(self, clip_min, clip_max, num): + return np.linspace(clip_min, clip_max, num + 1) + + def digit(self, x, bin): + n = np.digitize(x, bins=bin) + if x == bin[-1]: + n = n - 1 + return n + + def digitize_state(self, observation): + cart_pos, cart_v = observation + digitized = [self.digit(cart_pos, self.pos_bins), + self.digit(cart_v, self.vel_bins)] + return (digitized[1] - 1) * self.num_pos + digitized[0] - 1 + + def learn(self, state, action, r, next_state): + next_action = np.argmax(self.q_table[next_state]) + q_predict = self.q_table[state, action] + q_target = r + self.gamma * self.q_table[next_state, next_action] + self.q_table[state, action] += self.lr * (q_target - q_predict) + + +def train(): + env = gym.make('MountainCar-v0') # 指定渲染模式为 human + # print(env.action_space) + agent = QLearning(env.action_space) + + # use tqdm + for i in tqdm.tqdm(range(10000)): # 训练次数 + observation, _ = env.reset() # 状态 + state = agent.digitize_state(observation) # 状态标准化 + for t in range(300): # 一次训练最大运行次数 + action = agent.choose_action(state) # 动作 + observation, reward, done, truncated, info = env.step(action) + next_state = agent.digitize_state(observation) + + if reward == 0: # 到达山顶时 reward 为 0 + reward += 1000 # 给大一点的奖励 + + # print(f"step: {t}", action, reward, done, state, next_state, truncated) + agent.learn(state, action, reward, next_state) + state = next_state + + # env.render() # 每一步渲染画面 + + if done or truncated: # 重新加载环境 + # print("Episode {} finished after {} timesteps".format(i, t + 1)) + break + + print(agent.q_table) + env.close() + + # 保存模型 + with open(os.getcwd() + '/carmountain.model', 'wb') as f: + pickle.dump(agent, f) + + +def taste(): + # env = gym.make('MountainCar-v0', render_mode='human') # 指定渲染模式为 human + env = gym.make('MountainCar-v0') # 指定渲染模式为 human + + print(env.action_space) + with open(os.getcwd() + '/carmountain.model', 'rb') as f: + agent = pickle.load(f) + agent.actions = env.action_space # 初始化 + agent.epsilon = 1 + observation, _ = env.reset() # 初始化状态 + state = agent.digitize_state(observation) # 状态标准化 + + for t in range(500): # 一次测试最大运行次数 + action = agent.choose_action(state) # + observation, reward, done, truncated, info = env.step(action) + next_state = agent.digitize_state(observation) + print(f"step: {t}", action, reward, done, state, next_state) + # agent.learn(state, action, reward, next_state) + state = next_state + env.render() # 渲染画面 + env.close() # 关闭环境 + + +import numpy as np + + +def choose_action(pos: float, v: float, last_action: int) -> int: + """Return the action for the car to proceed the next move. + Args: + pos: Car's position, a float ranges between [-1.2, 0.6]. + v: Car's velocity, a float ranges between [-0.07, 0.07]. + last_action: Car's next move, a int ranges between [0, 1, 2]. + Return: + An integer representing the selected action for the car. + 0: accelerate to left + 1: don't accelerate + 2: accelerate to right + """ + target_pos = 0.6 + + # Calculate distance to target + distance_to_target = target_pos - pos + + # Define thresholds for decision making + if v < 0 and pos > target_pos: + return 0 # Accelerate left if moving backwards and past target + elif v > 0 and pos < target_pos: + return 2 # Accelerate right if moving forwards and before target + elif abs(distance_to_target) < 0.1: # If close to target, stabilize + return 1 # Don't accelerate, maintain current state + elif distance_to_target > 0: + return 2 # Move right towards the target + else: + return 0 # Move left away from the target + + +def run_test(): + env = gym.make('MountainCar-v0', render_mode='human') + observation, _ = env.reset() # 状态包括以下因素 + action = 1 + + for t in range(500): + # action = np.random.choice([0, 1, 2]) # 动作 + action = choose_action(observation[0], observation[1], action) + observation, reward, done, truncated, info = env.step(action) + print(f"step: {t}") + # print(action, reward, done) + # print(observation) + env.render() + # time.sleep(0.02) + + if done: + break + + env.close() + + +if __name__ == '__main__': + # train() # 训练 + run_test() # 训练结束后测试 diff --git a/examples/benchmark_tasks/machine_learning_car_mountain_continue/__init__.py b/examples/benchmark_tasks/machine_learning_car_mountain_continue/__init__.py new file mode 100644 index 00000000..939f15f9 --- /dev/null +++ b/examples/benchmark_tasks/machine_learning_car_mountain_continue/__init__.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: machine_learning_car_mountain_continue +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# Module Name: CarMountainCEvaluation +# Last Revision: 2025/3/5 +# Description: Designs a heuristic strategy function for controlling a car along an uneven road (Continuous Mountain Car problem). +# The function applies an appropriate force based on the car's position and velocity to guide the car +# towards a target in the minimum number of steps. +# This module is part of the LLM4AD project (https://github.com/Optima-CityU/llm4ad). +# +# Parameters: +# - position: float - Car's position, range [-1.2, 0.6] (default: None). +# - velocity: float - Car's velocity, range [-0.07, 0.07] (default: None). +# - last_action: float - Car's last applied force, range [-1.0, 1.0] (default: None). +# - timeout_seconds: int - Maximum allowed time (in seconds) for the evaluation process (default: 20). +# +# References: +# - Brockman, Greg, et al. "Openai gym." arXiv preprint arXiv:1606.01540 (2016). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any +import os, sys +sys.path.insert(0, os.path.dirname(__file__)) +import gym + +from llm4ad_loader import Evaluation +# from llm4ad.task.machine_learning.car_mountain_continue.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\n\ndef choose_action(pos: float, v: float, last_action: float) -> float:\n """Return the action for the car to proceed the next move.\n Args:\n pos: Car\'s position, a float ranges between [-1.2, 0.6].\n v: Car\'s velocity, a float ranges between [-0.07, 0.07].\n last_action: Car\'s next move, a float ranges between [-1, 1].\n Return:\n A [float] representing the force to be applied to the car.\n The value should be in the range of [-1.0, 1.0].\n """\n return np.random.uniform(-1.0, 1.0)' +task_description = '("Implement a function that designing a novel strategy function that guide the car along an uneven "' + + + +__all__ = ['CarMountainCEvaluation'] + + +def evaluate(env: gym.Env, action_select: callable) -> float: + """Evaluate heuristic function on car mountain problem.""" + + observation, _ = env.reset() # initialization + + action = 0 # initial action, stay static + + for i in range(env._max_episode_steps): + action = action_select(observation[0], observation[1], action) + observation, reward, done, truncated, info = env.step([action]) + + if done: + return -(i / env._max_episode_steps) # succeed + + if truncated: + return -(max(0.5 - observation[0], 0) + 1) # failed + + + +class CarMountainCEvaluation(Evaluation): + """Evaluator for car mountain problem.""" + + def __init__(self, max_steps=500, timeout_seconds=20, **kwargs): + """ + Args: + - 'max_steps' (int): Maximum number of steps allowed per episode in the MountainCar-v0 environment (default is 500). + - '**kwargs' (dict): Additional keyword arguments passed to the parent class initializer. + + Attributes: + - 'env' (gym.Env): The MountainCar-v0 environment with a modified maximum episode length. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + self.env = None + self.env = gym.make('MountainCarContinuous-v0') + self.env._max_episode_steps = max_steps + + def evaluate_program(self, program_str: str, callable_func: callable) -> Any | None: + try: + a = evaluate(self.env, callable_func) + except Exception as e: + print(e) + return evaluate(self.env, callable_func) + +# Task configuration for benchmark task +ENTRY_NAME = 'choose_action' +FUNCTION_SIGNATURE = 'def choose_action(...):' +IMPORT_HEADER = 'import numpy as np\nimport math' +TASK_DESCRIPTION = '("Implement a function that designing a novel strategy function that guide the car along an uneven "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `choose_action` for the LLM4AD task.\\n\\nTask description:\\n("Implement a function that designing a novel strategy function that guide the car along an uneven "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\n\ndef choose_action(pos: float, v: float, last_action: float) -> float:\n """Return the action for the car to proceed the next move.\n Args:\n pos: Car\'s position, a float ranges between [-1.2, 0.6].\n v: Car\'s velocity, a float ranges between [-0.07, 0.07].\n last_action: Car\'s next move, a float ranges between [-1, 1].\n Return:\n A [float] representing the force to be applied to the car.\n The value should be in the range of [-1.0, 1.0].\n """\n return np.random.uniform(-1.0, 1.0)' +EVAL_CLASS_NAME = 'CarMountainCEvaluation' +EVAL_KWARGS = {'max_steps': 500, 'timeout_seconds': 20} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/machine_learning_car_mountain_continue/paras.yaml b/examples/benchmark_tasks/machine_learning_car_mountain_continue/paras.yaml new file mode 100644 index 00000000..ac4fcccd --- /dev/null +++ b/examples/benchmark_tasks/machine_learning_car_mountain_continue/paras.yaml @@ -0,0 +1,3 @@ +name: CarMountainCEvaluation +max_steps: 500 +timeout_seconds: 20 diff --git a/examples/benchmark_tasks/machine_learning_car_mountain_continue/test.py b/examples/benchmark_tasks/machine_learning_car_mountain_continue/test.py new file mode 100644 index 00000000..a9ce6e5e --- /dev/null +++ b/examples/benchmark_tasks/machine_learning_car_mountain_continue/test.py @@ -0,0 +1,64 @@ +import numpy as np +import pandas as pd +import time +import gym +import tqdm +import csv +import os +import pickle +from queue import Queue + +def choose_action(pos: float, v: float, last_action: float) -> [float]: + """Return the action for the car to proceed the next move. + Args: + pos: Car's position, a float ranges between [-1.2, 0.6]. + v: Car's velocity, a float ranges between [-0.07, 0.07]. + last_action: Car's next move, a int ranges between [0, 1, 2]. + Return: + An integer representing the selected action for the car. + 0: accelerate to left + 1: don't accelerate + 2: accelerate to right + """ + target_pos = 0.6 + + # Calculate distance to target + distance_to_target = target_pos - pos + + # Define thresholds for decision making + if v < 0 and pos > target_pos: + return [1] # Accelerate left if moving backwards and past target + elif v > 0 and pos < target_pos: + return [1] # Accelerate right if moving forwards and before target + elif abs(distance_to_target) < 0.1: # If close to target, stabilize + return [1] # Don't accelerate, maintain current state + elif distance_to_target > 0: + return [1] # Move right towards the target + else: + return [0.5] # Move left away from the target + + +def run_test(): + env = gym.make('MountainCarContinuous-v0', render_mode='human') + observation, _ = env.reset() # 状态包括以下因素 + action = 1 + + for t in range(500): + # action = np.random.choice([0, 1, 2]) # 动作 + action = choose_action(observation[0], observation[1], action) + action = np.random.random() + observation, reward, done, truncated, info = env.step([action]) + print(f"step: {t}, action: {action}, reward: {reward}, done: {done}, truncated: {truncated}, info: {info}") + # print(action, reward, done) + # print(observation) + env.render() + # time.sleep(0.02) + + if done: + break + + env.close() + + +if __name__ == '__main__': + run_test() # 训练结束后测试 diff --git a/examples/benchmark_tasks/machine_learning_moon_lander/__init__.py b/examples/benchmark_tasks/machine_learning_moon_lander/__init__.py new file mode 100644 index 00000000..22cb7819 --- /dev/null +++ b/examples/benchmark_tasks/machine_learning_moon_lander/__init__.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: machine_learning_moon_lander +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# Module Name: MoonLanderEvaluation +# Last Revision: 2025/3/5 +# Description: Implements a heuristic strategy function to guide a lunar lander to achieve safe landings +# at the center of the target area. The function selects actions based on the lander's +# current state, aiming to minimize the number of steps required for a safe landing. +# A "safe landing" is defined as a touchdown with minimal vertical velocity, upright +# orientation, and angular velocity and angle close to zero. +# This module is part of the LLM4AD project (https://github.com/Optima-CityU/llm4ad). +# +# Parameters: +# - x_coordinate: float - x coordinate, range [-1, 1] (default: None). +# - y_coordinate: float - y coordinate, range [-1, 1] (default: None). +# - x_velocity: float - x velocity (default: None). +# - x_velocity: float - y velocity (default: None). +# - angle: float - angle (default: None). +# - angular_velocity: float - angular velocity (default: None). +# - l_contact: int - 1 if the first leg has contact, else 0 (default: None). +# - r_contact: int - 1 if the second leg has contact, else 0 (default: None). +# - last_action: int - last action taken by the lander, values [0, 1, 2, 3] (default: None). +# - timeout_seconds: int - Maximum allowed time (in seconds) for the evaluation process (default: 20). +# +# References: +# - Brockman, Greg, et al. "Openai gym." arXiv preprint arXiv:1606.01540 (2016). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + + +from __future__ import annotations + +from typing import Any +import os, sys +sys.path.insert(0, os.path.dirname(__file__)) +import gym +import numpy as np + +from llm4ad_loader import Evaluation +# from llm4ad.task.machine_learning.moon_lander.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\ndef choose_action(xc: float, yc: float, xv: float, yv: float, a: float, av: float, lc: float, rc: float, last_action: int) -> int:\n """\n Args:\n xc: x coordinate, between [-1, 1]\n yc: y coordinate, between [-1, 1]\n xv: x velocity\n yv: y velocity\n a: angle\n av: angular velocity\n lc: 1 if first leg has contact, else 0\n rc: 1 if second leg has contact, else 0.\n last_action: Lander\'s last move, a int ranges in [0, 1, 2, 3].\n\n Return:\n An integer representing the selected action for the lander.\n 0: do nothing\n 1: fire left orientation engine\n 2: upward\n 3: fire right orientation engine\n """\n action = np.random.randint(4)\n return action' +task_description = '("Implement a novel heuristic strategy heuristic strategy function that guides the "' + + +__all__ = ['MoonLanderEvaluation'] + + +def evaluate(env: gym.Env, action_select: callable) -> float | None: + try: + fitness = [] + # parallel evaluation 4 times, core=4 + # fitness = Parallel(n_jobs=4)(delayed(evaluate_single)(env, action_select) for _ in range(5)) + for i in range(5): + fitness.append(evaluate_single(env, action_select)) + fitness = np.mean(fitness) + + return fitness + except Exception as e: + return None + + +def evaluate_single(env: gym.Env, action_select: callable) -> float: + """Evaluate heuristic function on moon lander problem.""" + + observation, _ = env.reset() # initialization + action = 0 # initial action + reward = 0 + yv = [] + + for i in range(env._max_episode_steps + 1): # protect upper limits + action = action_select(observation[0], observation[1], + observation[2], + observation[3], + observation[4], + observation[5], + observation[6], + observation[7], + action) + observation, reward, done, truncated, info = env.step(action) + yv.append(observation[3]) + + if done or truncated: + # self.env.close() + fitness = abs(observation[0]) + abs(yv[-2]) - ((observation[6] + observation[7]) - 2) + 1 + if reward >= 100: + return -(i + 1) / env._max_episode_steps + else: + return -fitness + + +class MoonLanderEvaluation(Evaluation): + """Evaluator for moon lander problem.""" + + def __init__(self, max_steps=500, timeout_seconds=20, **kwargs): + """ + Args: + - 'max_steps' (int): Maximum number of steps allowed per episode in the MountainCar-v0 environment (default is 500). + - '**kwargs' (dict): Additional keyword arguments passed to the parent class initializer. + + Attributes: + - 'env' (gym.Env): The MountainCar-v0 environment with a modified maximum episode length. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + self.env = None + self.env = gym.make('LunarLander-v2') + self.env._max_episode_steps = max_steps + + def evaluate_program(self, program_str: str, callable_func: callable) -> Any | None: + return evaluate(self.env, callable_func) + +# Task configuration for benchmark task +ENTRY_NAME = 'choose_action' +FUNCTION_SIGNATURE = 'def choose_action(...):' +IMPORT_HEADER = 'import numpy as np\nimport math' +TASK_DESCRIPTION = '("Implement a novel heuristic strategy heuristic strategy function that guides the "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `choose_action` for the LLM4AD task.\\n\\nTask description:\\n("Implement a novel heuristic strategy heuristic strategy function that guides the "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\ndef choose_action(xc: float, yc: float, xv: float, yv: float, a: float, av: float, lc: float, rc: float, last_action: int) -> int:\n """\n Args:\n xc: x coordinate, between [-1, 1]\n yc: y coordinate, between [-1, 1]\n xv: x velocity\n yv: y velocity\n a: angle\n av: angular velocity\n lc: 1 if first leg has contact, else 0\n rc: 1 if second leg has contact, else 0.\n last_action: Lander\'s last move, a int ranges in [0, 1, 2, 3].\n\n Return:\n An integer representing the selected action for the lander.\n 0: do nothing\n 1: fire left orientation engine\n 2: upward\n 3: fire right orientation engine\n """\n action = np.random.randint(4)\n return action' +EVAL_CLASS_NAME = 'MoonLanderEvaluation' +EVAL_KWARGS = {'max_steps': 500, 'timeout_seconds': 20} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/machine_learning_moon_lander/paras.yaml b/examples/benchmark_tasks/machine_learning_moon_lander/paras.yaml new file mode 100644 index 00000000..5ca46fae --- /dev/null +++ b/examples/benchmark_tasks/machine_learning_moon_lander/paras.yaml @@ -0,0 +1,3 @@ +name: MoonLanderEvaluation +max_steps: 500 +timeout_seconds: 20 \ No newline at end of file diff --git a/examples/benchmark_tasks/machine_learning_moon_lander/test.py b/examples/benchmark_tasks/machine_learning_moon_lander/test.py new file mode 100644 index 00000000..dee9916c --- /dev/null +++ b/examples/benchmark_tasks/machine_learning_moon_lander/test.py @@ -0,0 +1,53 @@ +import random +import gym +import numpy as np +import random + + +def choose_action(state, reward, last_action): + x, y, x_vel, y_vel, angle, angular_vel, leg1_contact, leg2_contact = state + + if y < 0.5 and y_vel < -0.1: + action = 2 # Fire upward engine if below target and moving downward + elif angle > 0.1 and angular_vel > 0: + action = 3 # Fire right orientation engine if orientation needs adjustment + else: + if reward < 0.5: + if random.uniform(0, 1) < 0.7: + action = 0 # Do nothing + else: + action = 1 # Fire left orientation engine + else: + if random.uniform(0, 1) < 0.5: + action = 2 # Fire upward engine + else: + action = 3 # Fire right orientation engine + return action + + +# 创建LunarLander-v2环境 +env = gym.make('LunarLander-v2', render_mode='human') + +# 重置环境 +state, _ = env.reset() + +done = False + +step = 0 +while not done: + # 随机采取一个动作 + step += 1 + action = 0 + # action = env.action_space.sample() + action = choose_action(state, 0, action) + + # 环境采取动作并返回新的状态、奖励等 + state, reward, done, t, info = env.step(action) + + print(f"step: {step}, state: {state}, reward: {reward}, done: {done}, t: {t}, action: {action}") + + # 渲染环境 + env.render() + +# 关闭环境 +env.close() diff --git a/examples/benchmark_tasks/machine_learning_pendulum/__init__.py b/examples/benchmark_tasks/machine_learning_pendulum/__init__.py new file mode 100644 index 00000000..3055c866 --- /dev/null +++ b/examples/benchmark_tasks/machine_learning_pendulum/__init__.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: machine_learning_pendulum +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# Module Name: PendulumEvaluation +# Last Revision: 2025/3/5 +# Description: Implements a control strategy for the inverted pendulum swing-up problem. The function +# selects an appropriate torque based on the pendulum's current state to swing it into an +# upright position and stabilize it. The goal is to minimize the time required to reach +# the upright position while ensuring stability. This module is part of the LLM4AD project +# (https://github.com/Optima-CityU/llm4ad). +# +# Parameters: +# - x_position: float - cos(theta), range [-1, 1] (default: None). +# - y_position: float - sin(theta), range [-1, 1] (default: None). +# - angular_velocity: float - angular velocity of the pendulum, range [-8.0, 8.0] (default: None). +# - last_action: float - last torque applied to the pendulum, range [-2.0, 2.0] (default: None). +# - timeout_seconds: int - Maximum allowed time (in seconds) for the evaluation process (default: 20). +# +# References: +# - Brockman, Greg, et al. "Openai gym." arXiv preprint arXiv:1606.01540 (2016). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any +import os, sys +sys.path.insert(0, os.path.dirname(__file__)) +import gym +import numpy as np + +from llm4ad_loader import Evaluation +# from llm4ad.task.machine_learning.pendulum.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\n\ndef choose_action(x: float, y: float, av: float, last_action: float) -> float:\n """\n Args:\n x: cos(theta), between [-1, 1]\n y: sin(theta), between [-1, 1]\n av: angular velocity of the pendulum, between [-8.0, 8.0]\n last_action: the last torque applied to the pendulum, a float between [-2.0, 2.0]\n\n Return:\n A float representing the torque to be applied to the pendulum.\n The value should be in the range of [-2.0, 2.0].\n """\n action = np.random.uniform(-2.0, 2.0)\n return action' +task_description = '("Implement a novel control strategy for the inverted pendulum swing-up problem. The goal is to "' + + +__all__ = ['PendulumEvaluation'] + +def evaluate(env: gym.Env, action_select: callable) -> float | None: + try: + fitness = [] + # Parallel evaluation 4 times, core=4 + # fitness = Parallel(n_jobs=4)(delayed(evaluate_single)(env, action_select) for _ in range(5)) + for i in range(5): + fitness.append(evaluate_single(env, action_select)) + fitness = np.mean(fitness) + + return fitness + except Exception as e: + return None + + +def evaluate_single(env: gym.Env, action_select: callable) -> float: + """Evaluate heuristic function on the pendulum swing-up problem.""" + + observation, _ = env.reset() # initialization + action = 0.0 # initial action (torque) + total_reward = 0 + + for i in range(env._max_episode_steps + 1): # protect upper limits + action = action_select(observation[0], # cos(theta) + observation[1], # sin(theta) + observation[2], # angular velocity + action) # last action (torque) + observation, reward, done, truncated, info = env.step([action]) + total_reward += reward + + if done or truncated: + # self.env.close() + cos_theta = observation[0] + sin_theta = observation[1] + angular_velocity = observation[2] + + # Calculate error terms + angle_error = abs(1 - cos_theta) # Distance from vertical (cos(theta) = 1 when upright) + stability_error = abs(sin_theta) # Penalize instability + + # Total error + error = angle_error + stability_error + + # Fitness calculation: ensure fitness > 1 and closer to 1 for better states + fitness = 1 + error + if fitness <= 1: + return -(i + 1) / env._max_episode_steps + else: + return -fitness + + +class PendulumEvaluation(Evaluation): + """Evaluator for the pendulum swing-up problem.""" + + def __init__(self, max_steps=500, timeout_seconds=20, **kwargs): + """ + Args: + - 'max_steps' (int): Maximum number of steps allowed per episode in the Pendulum-v1 environment (default is 200). + - '**kwargs' (dict): Additional keyword arguments passed to the parent class initializer. + + Attributes: + - 'env' (gym.Env): The Pendulum-v1 environment with a modified maximum episode length. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + self.env = None + self.env = gym.make('Pendulum-v1') + self.env._max_episode_steps = max_steps + + def evaluate_program(self, program_str: str, callable_func: callable) -> Any | None: + return evaluate(self.env, callable_func) + +# Task configuration for benchmark task +ENTRY_NAME = 'choose_action' +FUNCTION_SIGNATURE = 'def choose_action(...):' +IMPORT_HEADER = 'import numpy as np\nimport math' +TASK_DESCRIPTION = '("Implement a novel control strategy for the inverted pendulum swing-up problem. The goal is to "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `choose_action` for the LLM4AD task.\\n\\nTask description:\\n("Implement a novel control strategy for the inverted pendulum swing-up problem. The goal is to "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\n\ndef choose_action(x: float, y: float, av: float, last_action: float) -> float:\n """\n Args:\n x: cos(theta), between [-1, 1]\n y: sin(theta), between [-1, 1]\n av: angular velocity of the pendulum, between [-8.0, 8.0]\n last_action: the last torque applied to the pendulum, a float between [-2.0, 2.0]\n\n Return:\n A float representing the torque to be applied to the pendulum.\n The value should be in the range of [-2.0, 2.0].\n """\n action = np.random.uniform(-2.0, 2.0)\n return action' +EVAL_CLASS_NAME = 'PendulumEvaluation' +EVAL_KWARGS = {'max_steps': 500, 'timeout_seconds': 20} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/machine_learning_pendulum/paras.yaml b/examples/benchmark_tasks/machine_learning_pendulum/paras.yaml new file mode 100644 index 00000000..db88b585 --- /dev/null +++ b/examples/benchmark_tasks/machine_learning_pendulum/paras.yaml @@ -0,0 +1,3 @@ +name: PendulumEvaluation +max_steps: 500 +timeout_seconds: 20 diff --git a/examples/benchmark_tasks/machine_learning_pendulum/test.py b/examples/benchmark_tasks/machine_learning_pendulum/test.py new file mode 100644 index 00000000..7dbb8876 --- /dev/null +++ b/examples/benchmark_tasks/machine_learning_pendulum/test.py @@ -0,0 +1,47 @@ +import gym +import numpy as np + +# 初始化Pendulum-v1环境 +env = gym.make('Pendulum-v1') # 可选:设置 render_mode='human' 以显示图形界面 + + +# 定义动作选择函数 +def choose_action(x: float, y: float, angular_velocity: float, last_action: float) -> float: + if angular_velocity > 0 and y > 0: + action = -2.0 # 施加一个负力矩 + elif angular_velocity < 0 and y < 0: + action = 2.0 # 施加一个正力矩 + else: + action = 0.0 # 保持静止力矩 + + # 确保动作在 [-2.0, 2.0] 范围内 + action = np.clip(action, -2.0, 2.0) + return action + + +# 环境重置 +observation, _ = env.reset() + +done = False +step = 0 +action = 0.0 # 初始动作 +env._max_episode_steps = 500 + +while not done and step < 500: + step += 1 + x, y, angular_velocity = observation # 提取状态信息 (cos(theta), sin(theta), angular_velocity) + action = choose_action(x, y, angular_velocity, action) # 决策动作 + + # 执行动作并获得新状态 + observation, reward, done, truncated, info = env.step([action]) # 动作需要作为列表传递 + + print(f"Step: {step}") + print(f"x (cos(theta)): {x}, y (sin(theta)): {y}, Angular Velocity: {angular_velocity}") + print(f"Action: {action}, Reward: {reward}, Done: {done}, Truncated: {truncated}") + print(f"Progress: {(step + 1) / env._max_episode_steps:.2%}") + + # 渲染环境(可选) + env.render() + +# 关闭环境 +env.close() diff --git a/examples/benchmark_tasks/online_bin_packing_local/__init__.py b/examples/benchmark_tasks/online_bin_packing_local/__init__.py new file mode 100644 index 00000000..e87190c6 --- /dev/null +++ b/examples/benchmark_tasks/online_bin_packing_local/__init__.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: online_bin_packing_local +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# name: str: OBPEvaluation +# Parameters: +# timeout_seconds: int: 20 +# end +from __future__ import annotations + +from typing import Any + +import numpy as np + +from llm4ad_loader import Evaluation +# from template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\n\ndef priority(item: float, bins: np.ndarray) -> np.ndarray:\n """Returns priority with which we want to add item to each bin.\n Args:\n item: Size of item to be added to the bin.\n bins: Array of capacities for each bin.\n Return:\n Array of same size as bins with priority score of each bin.\n """\n return item - bins' +task_description = 'Implement a function that returns the priority with which we want to add an item to each bin.' + +import os, sys +sys.path.insert(0, os.path.dirname(__file__)) +from generate_weibull_instances import generate_weibull_dataset + +__all__ = ['OBPEvaluation'] + +def get_valid_bin_indices(item: float, bins: np.ndarray) -> np.ndarray: + """Returns indices of bins in which item can fit.""" + return np.nonzero((bins - item) >= 0)[0] + + +def online_binpack( + items: tuple[float, ...], bins: np.ndarray, priority: callable +) -> tuple[list[list[float, ...], ...], np.ndarray]: + """Performs online binpacking of `items` into `bins`.""" + # Track which items are added to each bin. + packing = [[] for _ in bins] + # Add items to bins. + for item in items: + # Extract bins that have sufficient space to fit item. + valid_bin_indices = get_valid_bin_indices(item, bins) + # Score each bin based on heuristic. + priorities = priority(item, bins[valid_bin_indices]) + # Add item to bin with highest priority. + best_bin = valid_bin_indices[np.argmax(priorities)] + bins[best_bin] -= item + packing[best_bin].append(item) + # Remove unused bins from packing. + packing = [bin_items for bin_items in packing if bin_items] + return packing, bins + + +def evaluate(instances: dict, priority: callable) -> float: + """Evaluate heuristic function on a set of online binpacking instances.""" + # List storing number of bins used for each instance. + num_bins = [] + # Perform online binpacking for each instance. + for name in instances: + instance = instances[name] + capacity = instance['capacity'] + items = instance['items'] + # Create num_items bins so there will always be space for all items, + # regardless of packing order. Array has shape (num_items,). + bins = np.array([capacity for _ in range(instance['num_items'])]) + # Pack items into bins and return remaining capacity in bins_packed, which + # has shape (num_items,). + _, bins_packed = online_binpack(items, bins, priority) + # If remaining capacity in a bin is equal to initial capacity, then it is + # unused. Count number of used bins. + num_bins.append((bins_packed != capacity).sum()) + # Score of heuristic function is negative of average number of bins used + # across instances (as we want to minimize number of bins). + return -np.mean(num_bins) + + +class OBPEvaluation(Evaluation): + """Evaluator for online bin packing problem.""" + + def __init__(self, timeout_seconds=20, data_file='weibull_train.pkl', data_key='weibull_5k_train', **kwargs): + """ + Args: + - 'data_file' (str): The data file to load (default is 'weibull_5k_train.pkl'). + - 'data_key' (str): The key of the data to load (default is 'data_key'). + + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + self._datasets = generate_weibull_dataset(5, 5000, 100) + + def evaluate_program(self, program_str: str, callable_func: callable) -> Any | None: + return evaluate(self._datasets, callable_func) + +# Task configuration for benchmark task +ENTRY_NAME = 'priority' +FUNCTION_SIGNATURE = 'def priority(...):' +IMPORT_HEADER = 'import numpy as np\nimport math' +TASK_DESCRIPTION = 'Implement a function that returns the priority with which we want to add an item to each bin.' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `priority` for the LLM4AD task.\\n\\nTask description:\\nImplement a function that returns the priority with which we want to add an item to each bin.\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\n\ndef priority(item: float, bins: np.ndarray) -> np.ndarray:\n """Returns priority with which we want to add item to each bin.\n Args:\n item: Size of item to be added to the bin.\n bins: Array of capacities for each bin.\n Return:\n Array of same size as bins with priority score of each bin.\n """\n return item - bins' +EVAL_CLASS_NAME = 'OBPEvaluation' +EVAL_KWARGS = {} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/online_bin_packing_local/generate_weibull_instances.py b/examples/benchmark_tasks/online_bin_packing_local/generate_weibull_instances.py new file mode 100644 index 00000000..3bc3dec8 --- /dev/null +++ b/examples/benchmark_tasks/online_bin_packing_local/generate_weibull_instances.py @@ -0,0 +1,36 @@ +import numpy as np + +def generate_weibull_dataset(num_instances, num_items, capacity_limit): + + np.random.seed(2024) + + dataset = {} + + for i in range(num_instances): + instance = { + 'capacity': capacity_limit, + 'num_items': num_items, + 'items': [] + } + + items = [] + + # Generate random samples from Weibull(45, 3) distribution + samples = np.random.weibull(3, num_items) * 45 + + # Clip the samples at the specified limit + samples = np.clip(samples, 1, capacity_limit) + + # Round the item sizes to the nearest integer + sizes = np.round(samples).astype(int) + + # Add the items to the instance + for size in sizes: + items.append(size) + + instance['items'] = np.array(items) + + if num_items not in dataset: + dataset[f'instance_{i}'] = instance + + return dataset \ No newline at end of file diff --git a/examples/benchmark_tasks/online_bin_packing_local/run_eoh.py b/examples/benchmark_tasks/online_bin_packing_local/run_eoh.py new file mode 100644 index 00000000..717bc37c --- /dev/null +++ b/examples/benchmark_tasks/online_bin_packing_local/run_eoh.py @@ -0,0 +1,33 @@ +import sys + +sys.path.append('../../') # This is for finding all the modules + +from evaluation import OBPEvaluation +from llm4ad.tools.llm.llm_api_https import HttpsApi +from llm4ad.method.eoh import EoH +from llm4ad.tools.profiler import ProfilerBase + + +def main(): + llm = HttpsApi(host='xxx', # your host endpoint, e.g., 'api.openai.com', 'api.deepseek.com' + key='sk-xxx', # your key, e.g., 'sk-abcdefghijklmn' + model='xxx', # your llm, e.g., 'gpt-3.5-turbo' + timeout=60) + + task = OBPEvaluation() # local + + method = EoH(llm=llm, + profiler=ProfilerBase(log_dir='logs/eoh', log_style='simple'), + evaluation=task, + max_sample_nums=20, + max_generations=10, + pop_size=4, + num_samplers=1, + num_evaluators=1, + debug_mode=False) + + method.run() + + +if __name__ == '__main__': + main() diff --git a/examples/benchmark_tasks/optimization_admissible_set/__init__.py b/examples/benchmark_tasks/optimization_admissible_set/__init__.py new file mode 100644 index 00000000..990e4f97 --- /dev/null +++ b/examples/benchmark_tasks/optimization_admissible_set/__init__.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_admissible_set +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# Module Name: ASPEvaluation +# Last Revision: 2025/2/14 +# Description: Evaluates admissible sets for symmetric constant-weight optimization problems. +# This module is part of the LLM4AD project (https://github.com/Optima-CityU/llm4ad). +# +# Parameters: +# - dimension: int - The dimension of the problem space (default: 15). +# - weight: int - The weight constraint for the admissible set (default: 10). +# - timeout_seconds: int - Maximum allowed time (in seconds) for the evaluation process (default: 60). +# +# References: +# - Bernardino Romera-Paredes, Mohammadamin Barekatain, Alexander Novikov, +# Matej Balog, M. Pawan Kumar, Emilien Dupont, Francisco JR Ruiz et al. +# "Mathematical discoveries from program search with large language models." +# Nature 625, no. 7995 (2024): 468-475. +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + + +from __future__ import annotations + +import itertools +from typing import Any, List, Tuple +import numpy as np + +from llm4ad_loader import Evaluation +# from llm4ad.task.optimization.admissible_set.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import math\nimport numpy as np\n\ndef priority(el: tuple[int, ...], n: int = 15, w: int = 10) -> float:\n """Returns the priority with which we want to add `el` to the set.\n Args:\n el: the unique vector has the same number w of non-zero elements.\n n : length of the vector.\n w : number of non-zero elements.\n """\n return 0.' +task_description = '"""\\' + + +__all__ = ['ASPEvaluation'] + +class ASPEvaluation(Evaluation): + """Evaluator for online bin packing problem.""" + + def __init__(self, timeout_seconds=60, dimension=15, weight=10, **kwargs): + """ + Args: + - 'dimension' (int): The dimension of tested case (default is 15). + - 'weight' (int): The wight of tested case (default is 10). + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + self.dimension = dimension + self.weight = weight + + + self.TRIPLES = [(0, 0, 0), (0, 0, 1), (0, 0, 2), (0, 1, 2), (0, 2, 1), (1, 1, 1), (2, 2, 2)] + self.INT_TO_WEIGHT = [0, 1, 1, 2, 2, 3, 3] + self.Optimal_Set_Length = { + "n12w7": 792, + "n15w10": 3003, + "n21w15": 43596, + "n24w17": 237984 + } + + + def expand_admissible_set(self, pre_admissible_set: List[Tuple[int, ...]]) -> List[Tuple[int, ...]]: + """Expands a pre-admissible set into an admissible set.""" + num_groups = len(pre_admissible_set[0]) + admissible_set_15_10 = [] + for row in pre_admissible_set: + rotations = [[] for _ in range(num_groups)] + for i in range(num_groups): + x, y, z = self.TRIPLES[row[i]] + rotations[i].append((x, y, z)) + if not x == y == z: + rotations[i].append((z, x, y)) + rotations[i].append((y, z, x)) + product = list(itertools.product(*rotations)) + concatenated = [sum(xs, ()) for xs in product] + admissible_set_15_10.extend(concatenated) + return admissible_set_15_10 + + + def get_surviving_children(self, extant_elements, new_element, valid_children): + """Returns the indices of `valid_children` that remain valid after adding `new_element` to `extant_elements`.""" + bad_triples = {(0, 0, 0), (0, 1, 1), (0, 2, 2), (0, 3, 3), (0, 4, 4), (0, 5, 5), (0, 6, 6), (1, 1, 1), + (1, 1, 2), + (1, 2, 2), (1, 2, 3), (1, 2, 4), (1, 3, 3), (1, 4, 4), (1, 5, 5), (1, 6, 6), (2, 2, 2), + (2, 3, 3), + (2, 4, 4), (2, 5, 5), (2, 6, 6), (3, 3, 3), (3, 3, 4), (3, 4, 4), (3, 4, 5), (3, 4, 6), + (3, 5, 5), + (3, 6, 6), (4, 4, 4), (4, 5, 5), (4, 6, 6), (5, 5, 5), (5, 5, 6), (5, 6, 6), (6, 6, 6)} + + # Compute. + valid_indices = [] + for index, child in enumerate(valid_children): + # Invalidate based on 2 elements from `new_element` and 1 element from a + # potential child. + if all(self.INT_TO_WEIGHT[x] <= self.INT_TO_WEIGHT[y] + for x, y in zip(new_element, child)): + continue + # Invalidate based on 1 element from `new_element` and 2 elements from a + # potential child. + if all(self.INT_TO_WEIGHT[x] >= self.INT_TO_WEIGHT[y] + for x, y in zip(new_element, child)): + continue + # Invalidate based on 1 element from `extant_elements`, 1 element from + # `new_element`, and 1 element from a potential child. + is_invalid = False + for extant_element in extant_elements: + if all(tuple(sorted((x, y, z))) in bad_triples + for x, y, z in zip(extant_element, new_element, child)): + is_invalid = True + break + if is_invalid: + continue + + valid_indices.append(index) + return valid_indices + + + def evaluate(self, priority: callable) -> int: + + """Generates a symmetric constant-weight admissible set I(n, w).""" + num_groups = self.dimension // 3 + assert 3 * num_groups == self.dimension + + # Compute the scores of all valid (weight w) children. + valid_children = [] + for child in itertools.product(range(7), repeat=num_groups): + weight = sum(self.INT_TO_WEIGHT[x] for x in child) + if weight == self.weight: + valid_children.append(np.array(child, dtype=np.int32)) + + valid_scores = np.array([ + priority(sum([self.TRIPLES[x] for x in xs], ()), self.dimension, self.weight) for xs in valid_children]) + + # Greedy search guided by the scores. + pre_admissible_set = np.empty((0, num_groups), dtype=np.int32) + while valid_children: + max_index = np.argmax(valid_scores) + max_child = valid_children[max_index] + surviving_indices = self.get_surviving_children(pre_admissible_set, max_child, valid_children) + valid_children = [valid_children[i] for i in surviving_indices] + valid_scores = valid_scores[surviving_indices] + + pre_admissible_set = np.concatenate([pre_admissible_set, max_child[None]], axis=0) + + admissible_set = np.array(self.expand_admissible_set(pre_admissible_set)) + + return (len(admissible_set) - self.Optimal_Set_Length[f"n{self.dimension}w{self.weight}"]) + + + def evaluate_program(self, program_str: str, callable_func: callable) -> Any | None: + return self.evaluate(callable_func) + + +if __name__ == '__main__': + def priority(el: tuple, n: int, w: int) -> float: + """Design a novel algorithm to evaluate a vector for potential inclusion in a set + Args: + el: Candidate vectors for the admissible set. + n: Number of dimensions and the length of a vector. + w: Weight of each vector. + + Return: + The priorities of `el`. + """ + priorities = sum([abs(i) for i in el]) / n + return priorities + + eval = ASPEvaluation() + res = eval.evaluate_program('', priority) + print(res) + +# Task configuration for benchmark task +ENTRY_NAME = 'priority' +FUNCTION_SIGNATURE = 'def priority(...):' +IMPORT_HEADER = 'import math\nimport numpy as np' +TASK_DESCRIPTION = '"""\\' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `priority` for the LLM4AD task.\\n\\nTask description:\\n"""\\\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import math\nimport numpy as np\n\ndef priority(el: tuple[int, ...], n: int = 15, w: int = 10) -> float:\n """Returns the priority with which we want to add `el` to the set.\n Args:\n el: the unique vector has the same number w of non-zero elements.\n n : length of the vector.\n w : number of non-zero elements.\n """\n return 0.' +EVAL_CLASS_NAME = 'ASPEvaluation' +EVAL_KWARGS = {'dimension': 15, 'weight': 10, 'timeout_seconds': 20} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_admissible_set/paras.yaml b/examples/benchmark_tasks/optimization_admissible_set/paras.yaml new file mode 100644 index 00000000..5f7512aa --- /dev/null +++ b/examples/benchmark_tasks/optimization_admissible_set/paras.yaml @@ -0,0 +1,4 @@ +name: ASPEvaluation +dimension: 15 +weight: 10 +timeout_seconds: 20 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_aircraft_landing/__init__.py b/examples/benchmark_tasks/optimization_aircraft_landing/__init__.py new file mode 100644 index 00000000..203d8724 --- /dev/null +++ b/examples/benchmark_tasks/optimization_aircraft_landing/__init__.py @@ -0,0 +1,450 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_aircraft_landing +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# References: +# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language +# model agents in algorithm search for combinatorial optimization. +# arXiv preprint arXiv:2504.04310 (2025). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any +import numpy as np +from llm4ad_loader import Evaluation +from llm4ad_loader import load_subdir_as_text +# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader +# from llm4ad.task.optimization.co_bench.aircraft_landing_co_bench.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(num_planes: int, num_runways: int, freeze_time: float, planes: list[dict], separation: list[list[int]]) -> dict:\n """\n Problem:\n Given an instance of the Aircraft Landing Scheduling Problem, schedule the landing time for each plane and assign a runway so that:\n - Each landing time is within its allowed time window.\n - Each plane is assigned to one runway (from the available runways).\n - For any two planes assigned to the same runway, if plane i lands at or before plane j, then the landing times must be separated by at least\n the specified separation time (provided in the input data).\n - The overall penalty is minimized. For each plane, if its landing time is earlier than its target time, a penalty\n is incurred proportional to the earliness; if later than its target time, a penalty proportional to the lateness is incurred.\n - If any constraint is violated, the solution receives no score.\n Input kwargs:\n num_planes : (int) Number of planes.\n num_runways : (int) Number of runways.\n freeze_time : (float) Freeze time (unused in scheduling decisions).\n planes : (list of dict) Each dictionary contains:\n - "appearance" : float, time the plane appears.\n - "earliest" : float, earliest landing time.\n - "target" : float, target landing time.\n - "latest" : float, latest landing time.\n - "penalty_early" : float, penalty per unit time landing early.\n - "penalty_late" : float, penalty per unit time landing late.\n separation : (list of lists) separation[i][j] is the required gap after plane i lands before plane j can land\n when they are assigned to the same runway.\n Returns:\n A dictionary named "schedule" mapping each plane id (1-indexed) to a dictionary with its scheduled landing time\n and assigned runway, e.g., {"schedule": { plane_id: {"landing_time": float, "runway": int}, ... }}.\n """\n # -----------------------\n # For demonstration purposes, we simply schedule each plane at its target time\n # and assign all planes to runway 1.\n # (Note: This solution may be infeasible if targets do not satisfy separation constraints.)\n schedule = {}\n for i, plane in enumerate(planes, start=1):\n schedule[i] = {"landing_time": plane["target"], "runway": 1}\n return {"schedule": schedule}' +task_description = '("The problem is to schedule landing times for a set of planes across one or more runways such that "' + + +__all__ = ['ALEvaluationCB'] + + +class ALEvaluationCB(Evaluation): + """Evaluator for aircraft landing.""" + + def __init__(self, + timeout_seconds=300, + **kwargs): + + """ + Args: + None + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + # Load datasets from Hugging Face + dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Aircraft landing") + self._datasets = {} + for i in range(1, 14): # airland1 to airland13 + filename = f"airland{i}.txt" + if filename in dataset: + # Join all text rows into a single string + text_content = '\n'.join([row['text'] for row in dataset[filename]]) + self._datasets[filename] = text_content + + def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: + return self.evaluate(callable_func) + + def evaluate(self, eva: callable) -> float | None: + ins_cases = [] + + # Define runway configurations for each dataset (corresponds to airland1-13) + runway_configs = [[1, 2, 3], + [1, 2, 3], + [1, 2, 3], + [1, 2, 3, 4], + [1, 2, 3, 4], + [1, 2, 3], + [1, 2], + [1, 2, 3], + [1, 2, 3, 4], + [1, 2, 3, 4, 5], + [1, 2, 3, 4, 5], + [1, 2, 3, 4, 5], + [1, 2, 3, 4, 5]] + + for case_id, ins in enumerate(self._datasets.values()): + base_case = self.load_data(ins) + # Create variations with different runway configurations + for num_runways in runway_configs[case_id]: + case_with_runways = base_case.copy() + case_with_runways['num_runways'] = num_runways + ins_cases.append(case_with_runways) + + penalties = [] + try: + for case in ins_cases: + schedule = eva(case['num_planes'], case['num_runways'], case['freeze_time'], case['planes'], case['separation']) + penalty = self.eval_func(num_planes=case['num_planes'], num_runways=case['num_runways'], + freeze_time=case['freeze_time'], separation=case['separation'], planes=case['planes'], + schedule=schedule) + penalties.append(penalty) + + return -np.mean(penalties) + + except ValueError as e: + print(e) + return None + + def load_data(self, input_str): + """ + Reads the aircraft landing scheduling problem instance from a string. + The string contains a single case with the following format: + Line 1: + For each plane (i = 1, …, num_planes): + - A line with 6 numbers: + appearance_time earliest_landing_time target_landing_time + latest_landing_time penalty_cost_early penalty_cost_late + - One or more subsequent lines containing exactly num_planes separation times. + (Separation times for plane i with respect to planes 1..num_planes. They may span multiple lines.) + Returns: + A dictionary containing the keys: + - "num_planes" : int + - "freeze_time" : float + - "planes" : list of dicts (one per plane) + - "separation" : list of lists of floats + """ + all_lines = input_str.split("\n") + all_lines = [line.strip() for line in all_lines if line.strip()] + + idx = 0 + total_lines = len(all_lines) + + # Parse the first line: num_planes and freeze_time. + try: + tokens = all_lines[idx].split() + num_planes = int(tokens[0]) + freeze_time = float(tokens[1]) + except Exception as e: + raise ValueError(f"Error parsing case header at line {idx + 1}: {e}") + idx += 1 + + planes = [] + separation = [] + + for plane_index in range(num_planes): + if idx >= total_lines: + raise ValueError(f"Insufficient lines for plane {plane_index + 1} parameters.") + params_tokens = all_lines[idx].split() + idx += 1 + if len(params_tokens) < 6: + raise ValueError(f"Plane {plane_index + 1}: Expected 6 parameters, got {len(params_tokens)}.") + try: + appearance = float(params_tokens[0]) + earliest = float(params_tokens[1]) + target = float(params_tokens[2]) + latest = float(params_tokens[3]) + penalty_early = float(params_tokens[4]) + penalty_late = float(params_tokens[5]) + except Exception as e: + raise ValueError(f"Plane {plane_index + 1}: Error converting parameters: {e}") + + planes.append({ + "appearance": appearance, + "earliest": earliest, + "target": target, + "latest": latest, + "penalty_early": penalty_early, + "penalty_late": penalty_late + }) + + # Read exactly num_planes separation times (may span multiple lines) + sep_tokens = [] + while len(sep_tokens) < num_planes: + if idx >= total_lines: + raise ValueError(f"Not enough lines to read separation times for plane {plane_index + 1}.") + sep_tokens.extend(all_lines[idx].split()) + idx += 1 + # In case more tokens were read than needed: + sep_tokens = sep_tokens[:num_planes] + try: + sep_times = [float(token) for token in sep_tokens] + except Exception as e: + raise ValueError(f"Plane {plane_index + 1}: Error converting separation times: {e}") + separation.append(sep_times) + + # Return a single case dictionary (without num_runways, as that will be added later) + return { + "num_planes": num_planes, + "freeze_time": freeze_time, + "planes": planes, + "separation": separation, + } + + def eval_func(self, **kwargs): + """ + Evaluates a proposed aircraft landing schedule. + Expects the following keys in kwargs: + - num_planes : int, number of planes. + - num_runways : int, number of runways. + - freeze_time : float. + - planes : list of dicts, each containing: + "earliest", "target", "latest", "penalty_early", "penalty_late". + - separation : list of lists (floats), where separation[i][j] is the required gap after plane i lands + before plane j can land when they are assigned to the same runway. + - schedule : dict mapping plane_id (1-indexed) to a dict with keys: + "landing_time" (float) and "runway" (int). + The evaluation performs these checks: + 1. Each plane's landing time is within its allowed time window. + 2. Each plane is assigned to a runway in the range [1, num_runways]. + 3. For every two distinct planes i and j assigned to the same runway, + if plane i lands at or before plane j then the gap must be at least + the required separation time. + The total penalty is computed as follows for each plane: + - If landing_time < target: penalty = (target - landing_time) * penalty_early. + - If landing_time > target: penalty = (landing_time - target) * penalty_late. + - If landing_time == target: no penalty. + Returns: + The total penalty (a float) if the schedule is feasible. + Raises: + ValueError with an informative message if any constraint is violated. + """ + # Extract required parameters. + num_planes = kwargs.get("num_planes") + num_runways = kwargs.get("num_runways") + planes = kwargs.get("planes") + separation = kwargs.get("separation") + schedule = kwargs.get("schedule") + + # Check that schedule has exactly num_planes entries. + if not isinstance(schedule, dict) or len(schedule) != num_planes: + raise ValueError(f"Schedule must be a dict with exactly {num_planes} entries.") + + for plane_id in range(1, num_planes + 1): + if plane_id not in schedule: + raise ValueError(f"Plane {plane_id} is missing in the schedule.") + # Each schedule entry must be a dict with 'landing_time' and 'runway' + entry = schedule[plane_id] + if not isinstance(entry, dict) or "landing_time" not in entry or "runway" not in entry: + raise ValueError(f"Schedule entry for plane {plane_id} must contain 'landing_time' and 'runway' keys.") + # Check runway assignment is valid. + runway = entry["runway"] + if not isinstance(runway, int) or runway < 1 or runway > num_runways: + raise ValueError( + f"Plane {plane_id} assigned runway {runway} is invalid. Must be between 1 and {num_runways}.") + + # 1. Check landing time window constraints. + for i in range(1, num_planes + 1): + landing_time = schedule[i]["landing_time"] + earliest = planes[i - 1]["earliest"] + latest = planes[i - 1]["latest"] + if landing_time < earliest or landing_time > latest: + raise ValueError( + f"Plane {i}: Landing time {landing_time} is outside the allowed window [{earliest}, {latest}]." + ) + + # 2. Check separation constraints for planes on the same runway. + for i in range(1, num_planes + 1): + for j in range(1, num_planes + 1): + if i == j: + continue + entry_i = schedule[i] + entry_j = schedule[j] + # Only check separation if both planes are assigned to the same runway. + if entry_i["runway"] == entry_j["runway"]: + L_i = entry_i["landing_time"] + L_j = entry_j["landing_time"] + # If plane i lands no later than plane j, check the required separation. + if L_i <= L_j: + required_gap = separation[i - 1][j - 1] + if (L_j - L_i) < required_gap: + raise ValueError( + f"Separation violation on runway {entry_i['runway']}: Plane {i} lands at {L_i} and Plane {j} at {L_j} " + f"(required gap: {required_gap})." + ) + + # 3. Compute total penalty. + total_penalty = 0.0 + for i in range(1, num_planes + 1): + landing_time = schedule[i]["landing_time"] + target = planes[i - 1]["target"] + if landing_time < target: + penalty = (target - landing_time) * planes[i - 1]["penalty_early"] + elif landing_time > target: + penalty = (landing_time - target) * planes[i - 1]["penalty_late"] + else: + penalty = 0.0 + total_penalty += penalty + + return total_penalty + + def norm_score(self, results): + optimal_scores = { + "airland1.txt": [700, 90, 0], + "airland2.txt": [1480, 210, 0], + "airland3.txt": [820, 60, 0], + "airland4.txt": [2520, 640, 130, 0], + "airland5.txt": [3100, 650, 170, 0], + "airland6.txt": [24442, 554, 0], + "airland7.txt": [1550, 0], + "airland8.txt": [1950, 135, 0], + "airland9.txt": [7848.42, 573.25, 88.72, 0.0], + "airland10.txt": [17726.06, 1372.21, 246.15, 34.22, 0.0], + "airland11.txt": [19327.45, 1683.75, 333.53, 69.66, 0.0], + "airland12.txt": [2549.24, 2204.96, 430.5, 2.86, 0.0], + "airland13.txt": [58392.69, 4897.92, 821.82, 123.3, 0.0], + } + + normed = {} + for case, (scores, error_message) in results.items(): + if case not in optimal_scores: + continue # Skip if there's no optimal score defined. + optimal_list = optimal_scores[case] + normed_scores = [] + # Compute normalized score for each index. + for idx, score in enumerate(scores): + if isinstance(score, (int, float)): + if optimal_list[idx] == 0: + normed_scores.append((optimal_list[idx] + 1) / (score + 1)) + else: + normed_scores.append(optimal_list[idx] / score) + else: + normed_scores.append(score) + normed[case] = (normed_scores, error_message) + + return normed + + def get_dev(self): + dev = {'airland1.txt': [0], 'airland10.txt': [2, 1], 'airland11.txt': [0, 1], 'airland12.txt': [3, 4], + 'airland13.txt': [0, 3], 'airland2.txt': [2], 'airland3.txt': [2], 'airland4.txt': [1, 3], + 'airland5.txt': [0, 1], + 'airland6.txt': [1], 'airland7.txt': [1], 'airland8.txt': [2], 'airland9.txt': [0, 1]} + return dev + +if __name__ == "__main__": + evaluator = ALEvaluationCB() + import random + + def solve(num_planes: int, num_runways: int, freeze_time: float, planes: list[dict], + separation: list[list[int]]) -> dict: + """ + Problem: + Given an instance of the Aircraft Landing Scheduling Problem, schedule the landing time for each plane and assign a runway so that: + - Each landing time is within its allowed time window. + - Each plane is assigned to one runway (from the available runways). + - For any two planes assigned to the same runway, if plane i lands at or before plane j, then the landing times must be separated by at least + the specified separation time (provided in the input data). + - The overall penalty is minimized. For each plane, if its landing time is earlier than its target time, a penalty + is incurred proportional to the earliness; if later than its target time, a penalty proportional to the lateness is incurred. + - If any constraint is violated, the solution receives no score. + Input kwargs: + num_planes : (int) Number of planes. + num_runways : (int) Number of runways. + freeze_time : (float) Freeze time (unused in scheduling decisions). + planes : (list of dict) Each dictionary contains: + - "appearance" : float, time the plane appears. + - "earliest" : float, earliest landing time. + - "target" : float, target landing time. + - "latest" : float, latest landing time. + - "penalty_early" : float, penalty per unit time landing early. + - "penalty_late" : float, penalty per unit time landing late. + separation : (list of lists) separation[i][j] is the required gap after plane i lands before plane j can land + when they are assigned to the same runway. + Returns: + A dictionary named "schedule" mapping each plane id (1-indexed) to a dictionary with its scheduled landing time + and assigned runway, e.g., { plane_id: {"landing_time": float, "runway": int}, ... }. + """ + # ----------------------- + # For demonstration purposes, we simply schedule each plane at its target time + # and assign all planes to runway 1. + # (Note: This solution may be infeasible if targets do not satisfy separation constraints.) + schedule = {} + for i, plane in enumerate(planes, start=1): + schedule[i] = {"landing_time": plane["target"], "runway": random.randint(1, num_runways + 1)} + return {"schedule": schedule} + + results = evaluator.evaluate_program('', solve) + print(results) + +# Task configuration for benchmark task +ENTRY_NAME = 'solve' +FUNCTION_SIGNATURE = 'def solve(...):' +IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' +TASK_DESCRIPTION = '("The problem is to schedule landing times for a set of planes across one or more runways such that "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The problem is to schedule landing times for a set of planes across one or more runways such that "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(num_planes: int, num_runways: int, freeze_time: float, planes: list[dict], separation: list[list[int]]) -> dict:\n """\n Problem:\n Given an instance of the Aircraft Landing Scheduling Problem, schedule the landing time for each plane and assign a runway so that:\n - Each landing time is within its allowed time window.\n - Each plane is assigned to one runway (from the available runways).\n - For any two planes assigned to the same runway, if plane i lands at or before plane j, then the landing times must be separated by at least\n the specified separation time (provided in the input data).\n - The overall penalty is minimized. For each plane, if its landing time is earlier than its target time, a penalty\n is incurred proportional to the earliness; if later than its target time, a penalty proportional to the lateness is incurred.\n - If any constraint is violated, the solution receives no score.\n Input kwargs:\n num_planes : (int) Number of planes.\n num_runways : (int) Number of runways.\n freeze_time : (float) Freeze time (unused in scheduling decisions).\n planes : (list of dict) Each dictionary contains:\n - "appearance" : float, time the plane appears.\n - "earliest" : float, earliest landing time.\n - "target" : float, target landing time.\n - "latest" : float, latest landing time.\n - "penalty_early" : float, penalty per unit time landing early.\n - "penalty_late" : float, penalty per unit time landing late.\n separation : (list of lists) separation[i][j] is the required gap after plane i lands before plane j can land\n when they are assigned to the same runway.\n Returns:\n A dictionary named "schedule" mapping each plane id (1-indexed) to a dictionary with its scheduled landing time\n and assigned runway, e.g., {"schedule": { plane_id: {"landing_time": float, "runway": int}, ... }}.\n """\n # -----------------------\n # For demonstration purposes, we simply schedule each plane at its target time\n # and assign all planes to runway 1.\n # (Note: This solution may be infeasible if targets do not satisfy separation constraints.)\n schedule = {}\n for i, plane in enumerate(planes, start=1):\n schedule[i] = {"landing_time": plane["target"], "runway": 1}\n return {"schedule": schedule}' +EVAL_CLASS_NAME = 'ALEvaluationCB' +EVAL_KWARGS = {'timeout_seconds': 300} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_aircraft_landing/paras.yaml b/examples/benchmark_tasks/optimization_aircraft_landing/paras.yaml new file mode 100644 index 00000000..45aaac57 --- /dev/null +++ b/examples/benchmark_tasks/optimization_aircraft_landing/paras.yaml @@ -0,0 +1,2 @@ +name: ALEvaluationCB +timeout_seconds: 300 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_assignment_problem/__init__.py b/examples/benchmark_tasks/optimization_assignment_problem/__init__.py new file mode 100644 index 00000000..80ba5bec --- /dev/null +++ b/examples/benchmark_tasks/optimization_assignment_problem/__init__.py @@ -0,0 +1,327 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_assignment_problem +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# References: +# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language +# model agents in algorithm search for combinatorial optimization. +# arXiv preprint arXiv:2504.04310 (2025). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any +import numpy as np +from llm4ad_loader import Evaluation +from llm4ad_loader import load_subdir_as_text +# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader +# from llm4ad.task.optimization.co_bench.assignment_problem_co_bench.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\nfrom scipy.optimize import linear_sum_assignment\ndef solve(num_items: int, cost_matrix: np.ndarray) -> dict:\n """\n Solves an instance of the Assignment Problem.\n Given n items and an n×n cost matrix (where cost_matrix[i][j] is the cost of assigning\n item (i+1) to agent (j+1)), the goal is to determine a permutation (a one-to-one assignment\n between items and agents) that minimizes the total cost. The returned solution is a\n dictionary with:\n - "total_cost": The sum of the costs of the chosen assignments.\n - "assignment": A list of n tuples (i, j), where i is the item number (1-indexed)\n and j is the assigned agent number (1-indexed).\n Input kwargs:\n - n: int, the number of items/agents.\n - cost_matrix: numpy.ndarray, a 2D array with shape (n, n) containing the costs.\n Returns:\n A dictionary with keys "total_cost" and "assignment" representing the optimal solution.\n """\n # Your algorithm implementation goes here.\n # For example, you may use the Hungarian algorithm.\n return {"total_cost": None, "assignment": None}' +task_description = '("The Assignment Problem involves optimally assigning n items to n agents based on a provided "' + + +__all__ = ['APEvaluationCB'] + + +class APEvaluationCB(Evaluation): + """Evaluator for assignment problem.""" + + def __init__(self, + timeout_seconds=50, + **kwargs): + + """ + Args: + None + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + # Load datasets from Hugging Face + dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Assignment problem") + self._datasets = {} + for filename in dataset: + # Join all text rows into a single string + text_content = '\n'.join([row['text'] for row in dataset[filename]]) + self._datasets[filename] = text_content + + def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: + return self.evaluate(callable_func) + + def evaluate(self, eva: callable) -> float | None: + ins_cases = [] + for case_id, ins in enumerate(self._datasets.values()): + ins_cases.append(self.load_data(ins)) + + fitness_list = [] + try: + for i in ins_cases: + for j in i: + result = eva(j['n'], j['cost_matrix']) + fitness = self.eval_func(n=j['n'], cost_matrix=j['cost_matrix'], total_cost=result['total_cost'], assignment=result['assignment']) + fitness_list.append(fitness) + + return -np.mean(fitness_list) + + except ValueError as e: + print(e) + return None + + def load_data(self, input_string): + """ + Reads input string content and separates it into multiple cases for the assignment problem. + The input is expected to contain one or more cases. Each case has the following format: + - The first non-empty line of the case is a single integer n (the number of items/agents). + - The remaining tokens in the case provide the cost information. This can be in one of two formats: + 1. Dense Format: Exactly n*n numeric tokens (row-major order). + 2. Sparse Format: A sequence of tokens in groups of three: (i, j, cost). Any (i,j) + not specified is assigned a cost equal to 1000 times the maximum provided cost in that row. + Cases in the input are separated by one or more blank lines. + Parameters: + input_string (str): The input content as string. + Returns: + A list of dictionaries, each containing: + - "n": int, the number of items. + - "cost_matrix": numpy.ndarray of shape (n, n) with the assignment costs. + """ + import math + + all_lines = [line.rstrip() for line in input_string.split('\n')] + + # Group lines into cases using blank lines as delimiters. + cases = [] + current_block = [] + for line in all_lines: + if line.strip() == "": + if current_block: + cases.append(current_block) + current_block = [] + else: + current_block.append(line.strip()) + if current_block: + cases.append(current_block) + + case_list = [] + for block in cases: + if not block: + continue + try: + n = int(block[0]) + except Exception as e: + raise ValueError("The first line of each case must be an integer representing n.") from e + + tokens = [] + for line in block[1:]: + tokens.extend(line.split()) + + # Determine the format. + if len(tokens) == n * n: + try: + numbers = [float(token) for token in tokens] + except Exception as e: + raise ValueError("Non-numeric token found in dense format.") from e + cost_matrix = np.array(numbers).reshape(n, n) + elif len(tokens) % 3 == 0: + cost_matrix = np.full((n, n), math.inf) + for idx in range(0, len(tokens), 3): + try: + i = int(tokens[idx]) + j = int(tokens[idx + 1]) + cost = float(tokens[idx + 2]) + except Exception as e: + raise ValueError("Invalid token encountered in sparse format.") from e + if not (1 <= i <= n and 1 <= j <= n): + raise ValueError(f"Indices out of range in sparse format: i={i}, j={j}.") + cost_matrix[i - 1][j - 1] = cost + # Set unspecified assignments. + for i in range(n): + if np.all(np.isinf(cost_matrix[i])): + raise ValueError(f"Row {i + 1} has no valid assignments.") + max_finite = np.max(cost_matrix[i][np.isfinite(cost_matrix[i])]) + cost_matrix[i][np.isinf(cost_matrix[i])] = max_finite * 1000 + else: + raise ValueError( + "Input case format not recognized. Expect either n*n tokens (dense) or a multiple of 3 tokens (sparse).") + + case_list.append({"n": n, "cost_matrix": cost_matrix}) + return case_list + + def eval_func(self, **kwargs): + """ + Evaluates the solution of the Assignment Problem for a single case. + Parameters: + - case (dict): A dictionary containing the case data with keys: + * "n": int, the number of items/agents. + * "cost_matrix": numpy.ndarray, the cost matrix. + - solution (dict): A dictionary representing the solution returned by solve(), with keys: + * "total_cost": numeric, the total cost reported by the solution. + * "assignment": list of tuples (i, j) where i is the item (1-indexed) and j is the assigned agent (1-indexed). + Returns: + A numeric score representing the total cost computed from the cost_matrix based on the provided assignment. + The function performs the following checks: + - Each item (1 to n) must be assigned exactly once. + - Each agent (1 to n) must be assigned exactly once. + - The computed total cost (from the cost_matrix and assignment) must match the reported total_cost + (within a small tolerance). If not, the computed total is used. + """ + import math + + n = kwargs.get("n") + cost_matrix = kwargs.get("cost_matrix") + + # Validate the assignment. + assignment = {} # Maps item i to agent j. + assigned_agents = set() + if not isinstance(kwargs.get("assignment"), list): + raise ValueError("Solution must include an 'assignment' list.") + for idx, pair in enumerate(kwargs["assignment"], start=1): + if not (isinstance(pair, (list, tuple)) and len(pair) == 2): + raise ValueError(f"Assignment entry {idx} must be a tuple/list of two integers (i, j).") + i_val, j_val = pair + if i_val in assignment: + raise ValueError(f"Duplicate assignment for item {i_val} found.") + if j_val in assigned_agents: + raise ValueError(f"Agent {j_val} assigned more than once.") + if not (1 <= i_val <= n and 1 <= j_val <= n): + raise ValueError(f"Assignment indices ({i_val}, {j_val}) are out of range (must be between 1 and {n}).") + assignment[i_val] = j_val + assigned_agents.add(j_val) + + if len(assignment) != n: + raise ValueError(f"Incomplete assignment: expected {n} assignments, but got {len(assignment)}.") + + # Compute the total cost based on the assignment. + computed_total = 0.0 + for i in range(1, n + 1): + j_val = assignment[i] + cost = cost_matrix[i - 1][j_val - 1] + if cost == math.inf: + raise ValueError(f"Assignment ({i}, {j_val}) has an infinite cost, hence invalid.") + computed_total += cost + + return computed_total + + def norm_score(self, results): + optimal_scores = { + "assign100.txt": [305], + "assign200.txt": [475], + "assign300.txt": [626], + "assign400.txt": [804], + "assign500.txt": [991], + "assign600.txt": [1176], + "assign700.txt": [1362], + "assign800.txt": [1552], + "assignp800.txt": [2239], + "assignp1500.txt": [5839], + "assignp3000.txt": [18696], + "assignp5000.txt": [48533], + } + + normed = {} + for case, (scores, error_message) in results.items(): + if case not in optimal_scores: + continue # Skip if there's no optimal score defined. + optimal_list = optimal_scores[case] + normed_scores = [] + # Compute normalized score for each index. + for idx, score in enumerate(scores): + if isinstance(score, (int, float)): + normed_scores.append(score / optimal_list[idx]) + else: + normed_scores.append(score) + normed[case] = (normed_scores, error_message) + + return normed + + def get_dev(self): + dev = {'assign100.txt': [0], 'assign400.txt': [0], 'assign700.txt': [0], 'assignp3000.txt': [0]} + + return dev + +# Task configuration for benchmark task +ENTRY_NAME = 'solve' +FUNCTION_SIGNATURE = 'def solve(...):' +IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\nfrom scipy.optimize import linear_sum_assignment' +TASK_DESCRIPTION = '("The Assignment Problem involves optimally assigning n items to n agents based on a provided "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The Assignment Problem involves optimally assigning n items to n agents based on a provided "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\nfrom scipy.optimize import linear_sum_assignment\ndef solve(num_items: int, cost_matrix: np.ndarray) -> dict:\n """\n Solves an instance of the Assignment Problem.\n Given n items and an n×n cost matrix (where cost_matrix[i][j] is the cost of assigning\n item (i+1) to agent (j+1)), the goal is to determine a permutation (a one-to-one assignment\n between items and agents) that minimizes the total cost. The returned solution is a\n dictionary with:\n - "total_cost": The sum of the costs of the chosen assignments.\n - "assignment": A list of n tuples (i, j), where i is the item number (1-indexed)\n and j is the assigned agent number (1-indexed).\n Input kwargs:\n - n: int, the number of items/agents.\n - cost_matrix: numpy.ndarray, a 2D array with shape (n, n) containing the costs.\n Returns:\n A dictionary with keys "total_cost" and "assignment" representing the optimal solution.\n """\n # Your algorithm implementation goes here.\n # For example, you may use the Hungarian algorithm.\n return {"total_cost": None, "assignment": None}' +EVAL_CLASS_NAME = 'APEvaluationCB' +EVAL_KWARGS = {'timeout_seconds': 20} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_assignment_problem/paras.yaml b/examples/benchmark_tasks/optimization_assignment_problem/paras.yaml new file mode 100644 index 00000000..4577df36 --- /dev/null +++ b/examples/benchmark_tasks/optimization_assignment_problem/paras.yaml @@ -0,0 +1,2 @@ +name: APEvaluationCB +timeout_seconds: 20 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_assortment_problem/__init__.py b/examples/benchmark_tasks/optimization_assortment_problem/__init__.py new file mode 100644 index 00000000..bec00adb --- /dev/null +++ b/examples/benchmark_tasks/optimization_assortment_problem/__init__.py @@ -0,0 +1,388 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_assortment_problem +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# References: +# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language +# model agents in algorithm search for combinatorial optimization. +# arXiv preprint arXiv:2504.04310 (2025). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any +import numpy as np +from llm4ad_loader import Evaluation +from llm4ad_loader import load_subdir_as_text +# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader +# from llm4ad.task.optimization.co_bench.assortment_problem_co_bench.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(m: int, stocks: list, pieces: list) -> dict:\n """\n Solves the rectangular piece arrangement optimization problem to minimize the overall waste area percentage.\n Given:\n - m (int): Number of piece types.\n - stocks (list of dict): Each dict represents a stock type with keys:\n \'length\' (float), \'width\' (float), \'fixed_cost\' (float).\n - pieces (list of dict): Each dict represents a piece type with keys:\n \'length\' (float), \'width\' (float), \'min\' (int), \'max\' (int), \'value\' (float).\n Objective:\n Arrange rectangular pieces (which may be rotated by 90°) into stock rectangles such that the overall waste area percentage is minimized.\n The waste area percentage is computed as:\n Waste Percentage = (Total Stock Area - Total Used Area) / (Total Stock Area)\n Constraints:\n • Each piece must lie entirely within its assigned stock rectangle.\n • Pieces must not overlap within the same stock rectangle.\n • The number of pieces placed for each piece type must lie within its specified minimum and maximum bounds.\n • You may use unlimited many instances of each selected stock type, but the solution can include at most 2 distinct stock types.\n Output:\n Returns a dictionary with two keys (exactly follow this format):\n - "objective": The overall waste area percentage (float) as computed by the evaluation function.\n - "placements": A dictionary mapping stock instance ids (1-indexed) to their placement details.\n Each stock instance is represented by a dictionary with the following keys:\n \'stock_type\': (the 1-indexed id of the stock type used for this instance),\n \'placements\': a list of placements for pieces within that stock instance.\n Each placement is a dict with keys:\n \'piece\' (piece type, 1-indexed, 1 <= piece type <= m),\n \'x\' (x-coordinate of the bottom-left corner),\n \'y\' (y-coordinate of the bottom-left corner),\n \'orientation\' (0 for normal, 1 for rotated 90°).\n NOTE: The returned data should adhere to the output format required for evaluation.\n """\n # ----- INSERT YOUR SOLUTION ALGORITHM HERE -----\n # For demonstration purposes, we provide a dummy solution that does not place any pieces.\n # In a real solution, you would compute placements that respect all constraints.\n\n # Dummy solution: Create a single stock instance of the first stock type, with no pieces placed.\n solution = {\n "objective": 0.0, # With no placements, the evaluation function would compute a waste area percentage of 0.0.\n "placements": {\n 1: {\n "stock_type": 1,\n "placements": []\n }\n }\n }\n return solution' +task_description = '("This optimization problem involves arranging a set of rectangular pieces within available stock "' + + +__all__ = ['AssortPEvaluationCB'] + + +class AssortPEvaluationCB(Evaluation): + + def __init__(self, + timeout_seconds=50, + **kwargs): + + """ + Args: + None + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + # Load datasets from Hugging Face + dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Assortment problem") + self._datasets = {} + for filename in dataset: + # Join all text rows into a single string + text_content = '\n'.join([row['text'] for row in dataset[filename]]) + self._datasets[filename] = text_content + + def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: + return self.evaluate(callable_func) + + def evaluate(self, eva: callable) -> float | None: + ins_cases = [] + for case_id, ins in enumerate(self._datasets.values()): + ins_cases.append(self.load_data(ins)) + + fitness_list = [] + try: + for i in ins_cases: + for j in i: + result = eva(j['m'], j['stocks'], j['pieces']) + fitness = self.eval_func(j['m'], j['n'], j['waste_cost'], j['stocks'], j['pieces'], result['objective'], result['placements']) + fitness_list.append(fitness) + + return -np.mean(fitness_list) + + except ValueError as e: + print(e) + return None + + def load_data(self, input_string): + """ + Loads the input data for one or more cases from a TXT file. + The file format is as follows for each case: + 1. A line with three tokens: m n waste_cost + - m: number of piece types (int) + - n: number of stock rectangles (int) + - waste_cost: cost per unit area of waste (float) + 2. Next n lines: each with "length width fixed_cost" for a stock rectangle. + 3. Next m lines: each with "length width min max value" for a piece. + If the file contains multiple cases, they should be separated by at least one blank line. + Returns: + A list of dictionaries, one per case. Each dictionary contains: + - "m": int + - "n": int + - "waste_cost": float + - "stocks": list of dicts (each with keys 'length', 'width', 'fixed_cost') + - "pieces": list of dicts (each with keys 'length', 'width', 'min', 'max', 'value') + """ + + cases = [] + lines = [line.strip() for line in input_string.split('\n') if line.strip() != ''] + + ptr = 0 + while ptr < len(lines): + # Parse first line of a case. + try: + m_str, n_str, waste_cost_str = lines[ptr].split() + m = int(m_str) + n = int(n_str) + waste_cost = float(waste_cost_str) + except Exception: + raise Exception("Error reading the case header (expected: m n waste_cost) at line {}".format(ptr + 1)) + ptr += 1 + + stocks = [] + for i in range(n): + if ptr >= len(lines): + raise Exception("Unexpected end of file while reading stocks.") + tokens = lines[ptr].split() + if len(tokens) != 3: + raise Exception("Invalid stock rectangle line at line {}: expected 3 tokens.".format(ptr + 1)) + try: + length, width, fixed_cost = float(tokens[0]), float(tokens[1]), float(tokens[2]) + except Exception: + raise Exception("Parsing error in stock rectangle at line {}.".format(ptr + 1)) + stocks.append({'length': length, 'width': width, 'fixed_cost': fixed_cost}) + ptr += 1 + + pieces = [] + for j in range(m): + if ptr >= len(lines): + raise Exception("Unexpected end of file while reading pieces.") + tokens = lines[ptr].split() + if len(tokens) != 5: + raise Exception("Invalid piece line at line {}: expected 5 tokens.".format(ptr + 1)) + try: + p_length = float(tokens[0]) + p_width = float(tokens[1]) + p_min = int(tokens[2]) + p_max = int(tokens[3]) + p_value = float(tokens[4]) + except Exception: + raise Exception("Parsing error in piece line at line {}.".format(ptr + 1)) + pieces.append({'length': p_length, 'width': p_width, 'min': p_min, 'max': p_max, 'value': p_value}) + ptr += 1 + + cases.append({ + "m": m, + "n": n, + "waste_cost": waste_cost, + "stocks": stocks, + "pieces": pieces + }) + return cases + + def eval_func(self, m, n, waste_cost, stocks, pieces, objective, placements): + """ + Evaluates the solution for the arrangement optimization problem using waste area percentage as the metric. + The overall waste area percentage is computed as: + overall_waste_percentage = (total unused area) / (total stock area) + where a lower percentage indicates better utilization. This metric disregards piece values and fixed costs. + Infeasible solutions (due to piece count constraint violations or using more than 2 distinct stock types) + will raise an exception. + Inputs: + - m (int): Number of piece types. + - n (int): (Not used directly) originally denoted the number of stock rectangles, but now placements are stock instances. + - waste_cost (float): Not used in this metric. + - stocks (list of dict): Each dict represents a stock type with keys: + 'length', 'width', 'fixed_cost'. + There is an infinite supply of each stock type. + - pieces (list of dict): Each dict represents a piece type with keys: + 'length', 'width', 'min', 'max', 'value'. + - placements (dict): Mapping from stock instance id (1-indexed) to a dictionary with keys: + 'stock_type' : (1-indexed id of the stock type used for this instance), + 'placements' : a list of placements for pieces within that stock instance. + Each placement is a dict with keys: + 'piece' (piece type, 1-indexed), + 'x' (x-coordinate of the bottom-left corner), + 'y' (y-coordinate of the bottom-left corner), + 'orientation' (0 for normal, 1 for rotated 90°). + - objective (float): Not used in this metric (provided for reference). + Returns: + The overall waste area percentage (float) if all constraints are satisfied. + Raises: + Exception: If a placement violates boundary or overlap conditions, if piece count constraints are not met, + or if more than 2 distinct stock types are used. + """ + total_piece_counts = [0] * m + total_stock_area = 0.0 + total_waste_area = 0.0 + used_stock_types = set() + + # Iterate over each stock instance in the placements. + for stock_instance_id, instance_data in placements.items(): + # Validate the stock instance structure. + if not isinstance(instance_data, + dict) or 'stock_type' not in instance_data or 'placements' not in instance_data: + raise Exception( + f"Stock instance {stock_instance_id} is missing required keys ('stock_type', 'placements').") + + stock_type = instance_data['stock_type'] + # Check stock_type is valid. + if not (1 <= stock_type <= len(stocks)): + raise Exception( + f"Stock type {stock_type} in instance {stock_instance_id} is out of valid range (should be between 1 and {len(stocks)}).") + used_stock_types.add(stock_type) + + # Retrieve stock type details and compute area. + stock = stocks[stock_type - 1] + stock_length, stock_width = stock['length'], stock['width'] + stock_area = stock_length * stock_width + total_stock_area += stock_area + + used_area = 0.0 + placed_rectangles = [] # To check for overlaps within this stock instance. + + # Process each piece placement in this stock instance. + for placement in instance_data['placements']: + piece_type = placement.get('piece') + x = placement.get('x') + y = placement.get('y') + orientation = placement.get('orientation') + + # Validate piece type. + if not (1 <= piece_type <= m): + raise Exception( + f"Piece type {piece_type} in stock instance {stock_instance_id} is out of range (should be between 1 and {m}).") + piece = pieces[piece_type - 1] + + # Determine dimensions based on orientation. + if orientation == 0: + p_len, p_wid = piece['length'], piece['width'] + elif orientation == 1: + p_len, p_wid = piece['width'], piece['length'] + else: + raise Exception( + f"Invalid orientation {orientation} for piece type {piece_type} in stock instance {stock_instance_id}.") + + # Check that the piece lies fully within the stock boundaries. + if x < 0 or y < 0 or (x + p_len) > stock_length + 1e-6 or (y + p_wid) > stock_width + 1e-6: + raise Exception( + f"Piece type {piece_type} in stock instance {stock_instance_id} is placed outside the stock boundaries.") + + # Check for overlapping pieces within the same stock instance. + rect = (x, y, x + p_len, y + p_wid) + for other in placed_rectangles: + if not (rect[2] <= other[0] or rect[0] >= other[2] or rect[3] <= other[1] or rect[1] >= other[3]): + raise Exception(f"Overlap detected in stock instance {stock_instance_id}.") + placed_rectangles.append(rect) + + used_area += p_len * p_wid + total_piece_counts[piece_type - 1] += 1 + + total_waste_area += (stock_area - used_area) + + # Verify that no more than 2 distinct stock types were used. + if len(used_stock_types) > 2: + raise Exception(f"More than 2 distinct stock types used: found {len(used_stock_types)} types.") + + # Check piece count constraints for each piece type. + for idx, piece in enumerate(pieces): + count = total_piece_counts[idx] + if count < piece['min'] or count > piece['max']: + raise Exception( + f"Piece count violation for piece type {idx + 1}: count = {count}, required min = {piece['min']}, max = {piece['max']}." + ) + + if total_stock_area == 0: + raise Exception("Total stock area is 0, invalid configuration.") + + overall_waste_percentage = total_waste_area / total_stock_area + return overall_waste_percentage + + def norm_score(self, results): + optimal_scores = { + "assort1.txt": [7.69], + "assort2.txt": [4.17], + "assort3.txt": [5.87], + "assort4.txt": [6.63], + "assort5.txt": [4.95], + "assort6.txt": [7.62], + "assort7.txt": [16.84], + "assort8.txt": [5.48], + "assort9.txt": [9.07], + "assort10.txt": [13.80], + "assort11.txt": [6.65], + "assort12.txt": [5.89], + } + + normed = {} + for case, (scores, error_message) in results.items(): + if case not in optimal_scores: + continue # Skip if there's no optimal score defined. + optimal_list = optimal_scores[case] + normed_scores = [] + # Compute normalized score for each index. + for idx, score in enumerate(scores): + if isinstance(score, (int, float)): + normed_scores.append(optimal_list[idx] / score / 100) + else: + normed_scores.append(score) + normed[case] = (normed_scores, error_message) + + return normed + + def get_dev(self): + dev = {'assort1.txt': [0], 'assort10.txt': [0], 'assort4.txt': [0], + 'assort7.txt': [0], } + + return dev + + +# Task configuration for benchmark task +ENTRY_NAME = 'solve' +FUNCTION_SIGNATURE = 'def solve(...):' +IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' +TASK_DESCRIPTION = '("This optimization problem involves arranging a set of rectangular pieces within available stock "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("This optimization problem involves arranging a set of rectangular pieces within available stock "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(m: int, stocks: list, pieces: list) -> dict:\n """\n Solves the rectangular piece arrangement optimization problem to minimize the overall waste area percentage.\n Given:\n - m (int): Number of piece types.\n - stocks (list of dict): Each dict represents a stock type with keys:\n \'length\' (float), \'width\' (float), \'fixed_cost\' (float).\n - pieces (list of dict): Each dict represents a piece type with keys:\n \'length\' (float), \'width\' (float), \'min\' (int), \'max\' (int), \'value\' (float).\n Objective:\n Arrange rectangular pieces (which may be rotated by 90°) into stock rectangles such that the overall waste area percentage is minimized.\n The waste area percentage is computed as:\n Waste Percentage = (Total Stock Area - Total Used Area) / (Total Stock Area)\n Constraints:\n • Each piece must lie entirely within its assigned stock rectangle.\n • Pieces must not overlap within the same stock rectangle.\n • The number of pieces placed for each piece type must lie within its specified minimum and maximum bounds.\n • You may use unlimited many instances of each selected stock type, but the solution can include at most 2 distinct stock types.\n Output:\n Returns a dictionary with two keys (exactly follow this format):\n - "objective": The overall waste area percentage (float) as computed by the evaluation function.\n - "placements": A dictionary mapping stock instance ids (1-indexed) to their placement details.\n Each stock instance is represented by a dictionary with the following keys:\n \'stock_type\': (the 1-indexed id of the stock type used for this instance),\n \'placements\': a list of placements for pieces within that stock instance.\n Each placement is a dict with keys:\n \'piece\' (piece type, 1-indexed, 1 <= piece type <= m),\n \'x\' (x-coordinate of the bottom-left corner),\n \'y\' (y-coordinate of the bottom-left corner),\n \'orientation\' (0 for normal, 1 for rotated 90°).\n NOTE: The returned data should adhere to the output format required for evaluation.\n """\n # ----- INSERT YOUR SOLUTION ALGORITHM HERE -----\n # For demonstration purposes, we provide a dummy solution that does not place any pieces.\n # In a real solution, you would compute placements that respect all constraints.\n\n # Dummy solution: Create a single stock instance of the first stock type, with no pieces placed.\n solution = {\n "objective": 0.0, # With no placements, the evaluation function would compute a waste area percentage of 0.0.\n "placements": {\n 1: {\n "stock_type": 1,\n "placements": []\n }\n }\n }\n return solution' +EVAL_CLASS_NAME = 'AssortPEvaluationCB' +EVAL_KWARGS = {'timeout_seconds': 20} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_assortment_problem/paras.yaml b/examples/benchmark_tasks/optimization_assortment_problem/paras.yaml new file mode 100644 index 00000000..31aeb4c4 --- /dev/null +++ b/examples/benchmark_tasks/optimization_assortment_problem/paras.yaml @@ -0,0 +1,2 @@ +name: AssortPEvaluationCB +timeout_seconds: 20 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_bp_1d/__init__.py b/examples/benchmark_tasks/optimization_bp_1d/__init__.py new file mode 100644 index 00000000..50b607b7 --- /dev/null +++ b/examples/benchmark_tasks/optimization_bp_1d/__init__.py @@ -0,0 +1,298 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_bp_1d +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# References: +# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language +# model agents in algorithm search for combinatorial optimization. +# arXiv preprint arXiv:2504.04310 (2025). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any +import numpy as np +from llm4ad_loader import Evaluation +from llm4ad_loader import load_subdir_as_text +# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader +# from llm4ad.task.optimization.co_bench.bp_1d_co_bench.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\nfrom scipy.optimize import linear_sum_assignment\ndef solve(id: str, bin_capacity: int, num_items: int, items: list[int], **kwargs) -> dict:\n """\n Solve the one-dimensional bin packing problem for a single test case.\n Input kwargs (for a single test case):\n - id: The problem identifier (string)\n - bin_capacity: The capacity of each bin (int)\n - num_items: The number of items (int)\n - items: A list of item sizes (list of ints)\n - **kwargs: Other unused keyword arguments\n Evaluation metric:\n - The solution is scored by the total number of bins used.\n - If the solution is invalid (e.g., items are missing or duplicated, or bin capacity is exceeded),\n a penalty of 1,000,000 is added.\n Returns:\n A dictionary with:\n - \'num_bins\': An integer, the number of bins used.\n - \'bins\': A list of lists, where each inner list contains the 1-based indices of items assigned to that bin.\n Note: This is a placeholder implementation.\n """\n # Placeholder: Replace with your bin packing solution.\n return {\n \'num_bins\': 0,\n \'bins\': []\n }' +task_description = '("The **one-dimensional bin packing problem** seeks to minimize the number of bins required to "' + + +__all__ = ['BP1DEvaluationCB'] + + +class BP1DEvaluationCB(Evaluation): + + def __init__(self, + timeout_seconds=50, + **kwargs): + + """ + Args: + None + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + # Load datasets from Hugging Face + dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Bin packing - one-dimensional") + self._datasets = {} + for filename in dataset: + # Join all text rows into a single string + text_content = '\n'.join([row['text'] for row in dataset[filename]]) + self._datasets[filename] = text_content + + def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: + return self.evaluate(callable_func) + + def evaluate(self, eva: callable) -> float | None: + ins_cases = [] + for case_id, ins in enumerate(self._datasets.values()): + ins_cases.append(self.load_data(ins)) + + fitness_list = [] + try: + for i in ins_cases: + for j in i: + result = eva(j['id'], j['bin_capacity'], j['num_items'], j['items']) + fitness = self.eval_func(j['id'], j['bin_capacity'], j['num_items'], j['best_known'], j['items'], result['num_bins'], result['bins']) + fitness_list.append(fitness) + + return -np.mean(fitness_list) + + except ValueError as e: + print(e) + return None + + def load_data(self, input_string): + """ + Load test cases from string content for the bin packing problem. + The input format: + 1. The first nonempty line is an integer P, the number of test cases. + 2. For each test case: + a. A line with the problem identifier (e.g., "u120_00"). + b. A line with three space-separated numbers: bin_capacity, num_items, best_known. + (Note: bin_capacity and item sizes may be given as floats.) + c. Then num_items lines, each with a number representing an item size. + Returns: + A list of dictionaries. Each dictionary contains the input data for one test case with keys: + - 'id': Problem identifier (string) + - 'bin_capacity': Bin capacity (float) + - 'num_items': Number of items (int) + - 'best_known': Best known number of bins (int) + - 'items': List of item sizes (list of floats) + """ + cases = [] + try: + # Get all nonempty, stripped lines from string. + in_lines = [line.strip() for line in input_string.split('\n') if line.strip() != ''] + except Exception as e: + raise Exception("Error processing input string: " + str(e)) + + if not in_lines: + raise Exception("Input file is empty or improperly formatted.") + + try: + num_cases = int(in_lines[0]) + except Exception as e: + raise Exception("Error parsing the number of test cases: " + str(e)) + + pos = 1 + for _ in range(num_cases): + if pos >= len(in_lines): + raise Exception("Unexpected end of file while reading a test case header.") + # Read problem identifier. + prob_id = in_lines[pos] + pos += 1 + + if pos >= len(in_lines): + raise Exception(f"Missing header for problem {prob_id}.") + header_parts = in_lines[pos].split() + pos += 1 + if len(header_parts) < 3: + raise Exception( + f"Header for problem {prob_id} must contain bin capacity, number of items, and best known bins.") + try: + # Use float for bin_capacity since it might be provided as a float. + bin_capacity = float(header_parts[0]) + num_items = int(header_parts[1]) + best_known = int(header_parts[2]) + except Exception as e: + raise Exception(f"Error parsing header for problem {prob_id}: {e}") + + items = [] + for i in range(num_items): + if pos >= len(in_lines): + raise Exception(f"Unexpected end of file while reading items for problem {prob_id}.") + try: + # Parse item sizes as floats. + item_size = float(in_lines[pos]) + except Exception as e: + raise Exception(f"Error parsing item size for problem {prob_id} at line {pos + 1}: {e}") + items.append(item_size) + pos += 1 + + cases.append({ + 'id': prob_id, + 'bin_capacity': bin_capacity, + 'num_items': num_items, + 'best_known': best_known, + 'items': items + }) + + return cases + + def eval_func(self, id, bin_capacity, num_items, best_known, items, num_bins, bins): + """ + Evaluate the bin packing solution for a single test case. + Parameters (from the input case and the solution): + - id: Problem identifier (string) + - bin_capacity: Bin capacity (int) + - num_items: Number of items (int) + - best_known: Best known number of bins (int) + - items: List of item sizes (list of ints) + - num_bins: Number of bins used in the solution (int) + - bins: List of lists; each inner list contains 1-based item indices assigned to that bin. + Returns: + A scalar score (int). The score is the total number of bins used. + If the solution is invalid (e.g., item indices are wrong, items not used exactly once, or bin capacity exceeded), + a penalty of 1,000,000 is added to the score. + """ + penalty = 1_000_000 + score = num_bins # start with the number of bins used + valid = True + details = [] + + # Check that the number of bin assignments matches num_bins. + if len(bins) != num_bins: + valid = False + details.append("Declared number of bins does not match the number of bin assignments provided.") + + # Check each bin for capacity and valid item indices. + # Also count item appearances. + item_counts = [0] * (num_items + 1) # index 0 unused + for bin_index, bin_items in enumerate(bins, start=1): + bin_total = 0 + for item_idx in bin_items: + if item_idx < 1 or item_idx > num_items: + valid = False + details.append(f"Bin {bin_index} contains an invalid item index: {item_idx}.") + continue + bin_total += items[item_idx - 1] + item_counts[item_idx] += 1 + if bin_total > bin_capacity: + valid = False + details.append(f"Bin {bin_index} exceeds capacity: total size {bin_total} > capacity {bin_capacity}.") + + # Check that every item appears exactly once. + for i in range(1, num_items + 1): + if item_counts[i] != 1: + valid = False + details.append(f"Item {i} appears {item_counts[i]} times (expected exactly once).") + + if not valid: + score = None + else: + score = (score - best_known) / best_known + + # For debugging purposes, one might print or log details. + # For now, we simply return the computed score. + return score + + def get_dev(self): + dev = {'binpack1.txt': [7, 5, 16, 9, 13], 'binpack2.txt': [1, 15, 16, 4, 18], + 'binpack3.txt': [10, 18, 0, 19, 14], 'binpack4.txt': [11, 3, 16, 18, 17], + 'binpack5.txt': [10, 13, 0, 11, 17], 'binpack6.txt': [18, 11, 0, 6, 2], + 'binpack7.txt': [12, 17, 9, 15, 13], 'binpack8.txt': [4, 11, 19, 6, 17]} + + return dev + +# Task configuration for benchmark task +ENTRY_NAME = 'solve' +FUNCTION_SIGNATURE = 'def solve(...):' +IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\nfrom scipy.optimize import linear_sum_assignment' +TASK_DESCRIPTION = '("The **one-dimensional bin packing problem** seeks to minimize the number of bins required to "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The **one-dimensional bin packing problem** seeks to minimize the number of bins required to "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\nfrom scipy.optimize import linear_sum_assignment\ndef solve(id: str, bin_capacity: int, num_items: int, items: list[int], **kwargs) -> dict:\n """\n Solve the one-dimensional bin packing problem for a single test case.\n Input kwargs (for a single test case):\n - id: The problem identifier (string)\n - bin_capacity: The capacity of each bin (int)\n - num_items: The number of items (int)\n - items: A list of item sizes (list of ints)\n - **kwargs: Other unused keyword arguments\n Evaluation metric:\n - The solution is scored by the total number of bins used.\n - If the solution is invalid (e.g., items are missing or duplicated, or bin capacity is exceeded),\n a penalty of 1,000,000 is added.\n Returns:\n A dictionary with:\n - \'num_bins\': An integer, the number of bins used.\n - \'bins\': A list of lists, where each inner list contains the 1-based indices of items assigned to that bin.\n Note: This is a placeholder implementation.\n """\n # Placeholder: Replace with your bin packing solution.\n return {\n \'num_bins\': 0,\n \'bins\': []\n }' +EVAL_CLASS_NAME = 'BP1DEvaluationCB' +EVAL_KWARGS = {'timeout_seconds': 20} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_bp_1d/paras.yaml b/examples/benchmark_tasks/optimization_bp_1d/paras.yaml new file mode 100644 index 00000000..706ec53b --- /dev/null +++ b/examples/benchmark_tasks/optimization_bp_1d/paras.yaml @@ -0,0 +1,2 @@ +name: BP1DEvaluationCB +timeout_seconds: 20 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_bp_1d_construct/__init__.py b/examples/benchmark_tasks/optimization_bp_1d_construct/__init__.py new file mode 100644 index 00000000..a9c8dbcb --- /dev/null +++ b/examples/benchmark_tasks/optimization_bp_1d_construct/__init__.py @@ -0,0 +1,289 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_bp_1d_construct +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# Module Name: BP1DEvaluation +# Last Revision: 2025/2/16 +# Description: Evaluates constructive heuristic for 1-dimensional bin packing problem. +# Given a set of bins and items, iteratively assign one item to feasible bins. +# Design the optimal heuristic in each iteration to minimize the used bins. +# This module is part of the LLM4AD project (https://github.com/Optima-CityU/llm4ad). +# +# Parameters: +# - n_bins: number of bins: int (default: 10). +# - n_instance: number of instances: int (default: 16). +# - n_items: number of items: int (default: 10). +# - bin_capacity: capacity of bins: int (default: 100). +# - timeout_seconds: int - Maximum allowed time (in seconds) for the evaluation process (default: 60). +# +# References: +# - Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + + +from __future__ import annotations +import matplotlib.pyplot as plt +from typing import Callable, Any, List, Tuple +import os, sys +sys.path.insert(0, os.path.dirname(__file__)) +import copy + +from llm4ad_loader import Evaluation +from get_instance import GetData +# from llm4ad.task.optimization.bp_1d_construct.get_instance import GetData # Converted from LLM4AD import +# from llm4ad.task.optimization.bp_1d_construct.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\n\ndef determine_next_assignment(remaining_items: List[int], remaining_capacities: List[int]) -> Tuple[int, Optional[int]]:\n """\n Determine the next item and bin to pack based on a greedy heuristic.\n\n Args:\n remaining_items: A list of remaining item weights.\n remaining_capacities: A list of remaining capacities of feasible bins.\n\n Returns:\n A tuple containing:\n - The selected item to pack.\n - The selected bin to pack the item into (or None if no feasible bin is found).\n """\n # Iterate through items in their original order\n for item in remaining_items:\n # Iterate through bins to find the first feasible one\n for bin_id, capacity in enumerate(remaining_capacities):\n if item <= capacity:\n return item, bin_id # Return the selected item and bin\n return remaining_items[0], None # If no feasible bin is found, return the first item and no bin' +task_description = "'" + + +__all__ = ['BP1DEvaluation'] + + +class BP1DEvaluation(Evaluation): + """Evaluator for the 1D Bin Packing Problem.""" + + def __init__(self, + timeout_seconds: int = 60, + n_bins: int = 500, + n_instance: int = 8, + n_items: int = 500, + bin_capacity: int = 100, + **kwargs): + """ + Args: + n_bins: The number of available bins at the beginning. + """ + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + self.n_instance = n_instance + self.n_items = n_items + self.bin_capacity = bin_capacity + self.n_bins = n_bins + getData = GetData(self.n_instance, self.n_items, self.bin_capacity) + self._datasets = getData.generate_instances() + + def plot_bins(self, bins: List[List[int]], bin_capacity: int): + """ + Plot the bins and their contents. + + Args: + bins: A list of bins, where each bin is a list of item weights. + bin_capacity: The capacity of each bin. + """ + fig, ax = plt.subplots() + + # Create a bar plot for each bin + for i, bin_content in enumerate(bins): + # Calculate the cumulative sum of item weights for stacking + cumulative_weights = [sum(bin_content[:j + 1]) for j in range(len(bin_content))] + # Plot the bin as a bar, with items stacked + ax.bar(i, cumulative_weights[-1] if cumulative_weights else 0, color='lightblue', edgecolor='black') + # Plot individual items as stacked segments + for j, weight in enumerate(bin_content): + ax.bar(i, weight, bottom=cumulative_weights[j] - weight, edgecolor='black') + + # Set plot labels and title + ax.set_xlabel('Bin Index') + ax.set_ylabel('Weight') + ax.set_title(f'1D Bin Packing Solution (Bin Capacity: {bin_capacity})') + ax.set_xticks(range(len(bins))) + ax.set_xticklabels([f'Bin {i + 1}' for i in range(len(bins))]) + ax.axhline(bin_capacity, color='red', linestyle='--', label='Bin Capacity') + + # Add a legend + ax.legend() + + # Show the plot + plt.show() + + def pack_items(self, item_weights: List[int], bin_capacity: int, eva: Callable, n_bins: int) -> Tuple[int, List[List[int]]]: + """ + Pack items into bins using a constructive heuristic. + + Args: + item_weights: A list of item weights. + bin_capacity: The capacity of each bin. + eva: The constructive heuristic function to select the next item and bin. + n_bins: The number of available bins at the beginning. + + Returns: + A tuple containing: + - The total number of bins used. + - A list of bins, where each bin is a list of item weights. + """ + bins = [[] for _ in range(n_bins)] # Initialize n_bins empty bins + remaining_items = item_weights.copy() # Copy of item weights to track remaining items + remaining_capacities = [bin_capacity] * n_bins # Initialize remaining capacities of all bins + + while remaining_items: + # Determine feasible bins for the next item + feasible_bins = [bin_id for bin_id, capacity in enumerate(remaining_capacities) if capacity >= min(remaining_items)] + + # Use the heuristic to select the next item and bin + remaining_items_copy = copy.deepcopy(remaining_items) + remaining_capacities_copy = copy.deepcopy(remaining_capacities) + selected_item, selected_bin = eva(remaining_items_copy, remaining_capacities_copy) + + if selected_bin is not None: + # Add the selected item to the selected bin + bins[selected_bin].append(selected_item) + # Update the remaining capacity of the selected bin + remaining_capacities[selected_bin] -= selected_item + else: + # If no feasible bin is found, stop packing (no more bins available) + break + + if remaining_capacities[selected_bin] < 0: + return None + + # Remove the selected item from the remaining items + remaining_items.remove(selected_item) + + if len(remaining_items) > 0: + return None + + # Calculate the number of bins used (bins that contain at least one item) + used_bins = sum(1 for bin_content in bins if bin_content) + + return used_bins, bins + + def evaluate(self, eva: Callable) -> float: + """ + Evaluate the constructive heuristic for the 1D Bin Packing Problem. + + Args: + instance_data: List of tuples containing the item weights and bin capacity. + n_ins: Number of instances to evaluate. + eva: The constructive heuristic function to evaluate. + n_bins: The number of available bins at the beginning. + + Returns: + The average number of bins used across all instances. + """ + total_bins = 0 + + for instance in self._datasets: + item_weights, bin_capacity = instance + num_bins, _ = self.pack_items(item_weights, bin_capacity, eva, self.n_bins) + total_bins += num_bins + + average_bins = total_bins / self.n_instance + return -average_bins # Negative because we want to minimize the number of bins + + def evaluate_program(self, program_str: str, callable_func: Callable) -> Any | None: + return self.evaluate(callable_func) + + +if __name__ == '__main__': + + def determine_next_assignment(remaining_items: List[int], remaining_capacities: List[int]) -> Tuple[int, int | None]: + """ + Determine the next item and bin to pack based on a greedy heuristic. + + Args: + remaining_items: A list of remaining item weights. + remaining_capacities: A list of remaining capacities of feasible bins. + + Returns: + A tuple containing: + - The selected item to pack. + - The selected bin to pack the item into (or None if no feasible bin is found). + """ + # Simple greedy heuristic: choose the largest item that fits into the bin with the smallest remaining capacity + for item in sorted(remaining_items, reverse=True): # Try largest items first + for bin_id, capacity in enumerate(remaining_capacities): + if item <= capacity: + return item, bin_id # Return the selected item and bin + return remaining_items[0], None # If no feasible bin is found, return the first item and no bin + + + bp1d = BP1DEvaluation() + ave_bins = bp1d.evaluate_program('_', determine_next_assignment) + print(ave_bins) + +# Task configuration for benchmark task +ENTRY_NAME = 'determine_next_assignment' +FUNCTION_SIGNATURE = 'def determine_next_assignment(...):' +IMPORT_HEADER = 'import numpy as np\nimport math' +TASK_DESCRIPTION = "'" +OBJECTIVE_TEXT = "You are optimizing the implementation of `determine_next_assignment` for the LLM4AD task.\\n\\nTask description:\\n'\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible." +TEMPLATE_FUNCTION = 'import numpy as np\n\ndef determine_next_assignment(remaining_items: List[int], remaining_capacities: List[int]) -> Tuple[int, Optional[int]]:\n """\n Determine the next item and bin to pack based on a greedy heuristic.\n\n Args:\n remaining_items: A list of remaining item weights.\n remaining_capacities: A list of remaining capacities of feasible bins.\n\n Returns:\n A tuple containing:\n - The selected item to pack.\n - The selected bin to pack the item into (or None if no feasible bin is found).\n """\n # Iterate through items in their original order\n for item in remaining_items:\n # Iterate through bins to find the first feasible one\n for bin_id, capacity in enumerate(remaining_capacities):\n if item <= capacity:\n return item, bin_id # Return the selected item and bin\n return remaining_items[0], None # If no feasible bin is found, return the first item and no bin' +EVAL_CLASS_NAME = 'BP1DEvaluation' +EVAL_KWARGS = {'timeout_seconds': 60} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_bp_1d_construct/get_instance.py b/examples/benchmark_tasks/optimization_bp_1d_construct/get_instance.py new file mode 100644 index 00000000..7840a5d8 --- /dev/null +++ b/examples/benchmark_tasks/optimization_bp_1d_construct/get_instance.py @@ -0,0 +1,55 @@ +import numpy as np + + +class GetData: + def __init__(self, n_instance: int, n_items: int, bin_capacity: int): + """ + Initialize the GetData class for the 1D Bin Packing Problem. + + Args: + n_instance: Number of instances to generate. + n_items: Number of items. + bin_capacity: Capacity of each bin. + """ + self.n_instance = n_instance + self.n_items = n_items + self.bin_capacity = bin_capacity + + def generate_instances(self): + """ + Generate instances for the 1D Bin Packing Problem. + + Returns: + A list of tuples, where each tuple contains: + - item_weights: A list of item weights. + - bin_capacity: The capacity of each bin. + """ + np.random.seed(2024) # Set seed for reproducibility + instance_data = [] + + for _ in range(self.n_instance): + # Parameters for the beta distribution + alpha = 2 # Shape parameter (adjust as needed) + beta = 5 # Shape parameter (adjust as needed) + + # Generate random item weights using a beta distribution + # Scale and shift the values to the range [5, 50] + item_weights = (50 - np.random.beta(alpha, beta, size=self.n_items) * 40).astype(int).tolist() + # # Generate random item weights, ensuring no item exceeds the bin capacity + # item_weights = np.random.randint(2, 9, size=self.n_items).tolist() + + # # Randomly decide for each item whether to multiply by 5 or 8 + # multipliers = np.random.choice([5, 11], size=self.n_items) + + # # Apply the multipliers to the item weights + # modified_weights = [weight * multiplier for weight, multiplier in zip(item_weights, multipliers)] + + instance_data.append((item_weights, self.bin_capacity)) + + return instance_data + +# # Example usage: +# data_generator = GetData(n_instance=5, n_items=10, bin_capacity=100) +# instances = data_generator.generate_instances() +# for instance in instances: +# print(instance) diff --git a/examples/benchmark_tasks/optimization_bp_1d_construct/paras.yaml b/examples/benchmark_tasks/optimization_bp_1d_construct/paras.yaml new file mode 100644 index 00000000..310280ad --- /dev/null +++ b/examples/benchmark_tasks/optimization_bp_1d_construct/paras.yaml @@ -0,0 +1,2 @@ +name: BP1DEvaluation +timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_bp_2d_construct/__init__.py b/examples/benchmark_tasks/optimization_bp_2d_construct/__init__.py new file mode 100644 index 00000000..a7acee17 --- /dev/null +++ b/examples/benchmark_tasks/optimization_bp_2d_construct/__init__.py @@ -0,0 +1,344 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_bp_2d_construct +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# Module Name: BP2DEvaluation +# Last Revision: 2025/2/16 +# Description: Evaluates constructive heuristic for 2-dimensional bin packing problem. +# Given a set of bins and items, iteratively assign one item to feasible bins. +# Design the optimal heuristic in each iteration to minimize the used bins. +# This module is part of the LLM4AD project (https://github.com/Optima-CityU/llm4ad). +# +# Parameters: +# - n_bins: number of bins: int (default: 10). +# - n_instance: number of instances: int (default: 16). +# - n_items: number of items: int (default: 10). +# - bin_width: width of bins: int (default: 100). +# - bin_height: height of bins: int (default: 100). +# - timeout_seconds: int - Maximum allowed time (in seconds) for the evaluation process (default: 60). +# +# References: +# - Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + + +from __future__ import annotations +from typing import List, Tuple, Callable, Any +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.patches as patches + +from llm4ad_loader import Evaluation +import os, sys +sys.path.insert(0, os.path.dirname(__file__)) +from get_instance import GetData +# from llm4ad.task.optimization.bp_2d_construct.get_instance import GetData # Converted from LLM4AD import +# from llm4ad.task.optimization.bp_2d_construct.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\ndef determine_next_assignment(remaining_items: List[Tuple[int, int]], point_matrices: List[List[List[int]]]) -> Tuple[Tuple[int, int], int]:\n """\n A simple heuristic function to select the next item and bin for packing.\n\n Args:\n remaining_items: A list of tuples, where each tuple represents the (width, height) of an item.\n point_matrices: A list of 2D matrices representing the occupied (1) and unoccupied (0) points in each bin.\n\n Returns:\n A tuple containing:\n - The selected item (width, height).\n - The selected bin index (or None if no bin is feasible).\n """\n # Select the largest item (based on area) from the remaining items\n selected_item = max(remaining_items, key=lambda item: item[0] * item[1])\n\n # Try to find a feasible bin for the selected item\n for bin_idx, point_matrix in enumerate(point_matrices):\n bin_width = len(point_matrix)\n bin_height = len(point_matrix[0]) if bin_width > 0 else 0\n # Check if the item fits in the bin\n if bin_width >= selected_item[0] and bin_height >= selected_item[1]:\n # Check for a feasible position in the bin\n for x in range(bin_width - selected_item[0] + 1):\n for y in range(bin_height - selected_item[1] + 1):\n # Check if the area is unoccupied\n if all(point_matrix[x + dx][y + dy] == 0 for dx in range(selected_item[0]) for dy in range(selected_item[1])):\n return selected_item, bin_idx\n # If no feasible bin is found, return None for the bin\n return selected_item, None' +task_description = "'" + + +__all__ = ['BP2DEvaluation'] + + +class BP2DEvaluation(Evaluation): + """Evaluator for the 2D Bin Packing Problem.""" + + def __init__(self, + timeout_seconds: int = 120, + n_bins: int = 100, + n_instance: int = 8, + n_items: int = 100, + bin_width: int = 100, + bin_height: int = 100, + **kwargs): + """ + Args: + n_bins: The number of available bins at the beginning. + """ + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + self.n_instance = n_instance + self.n_items = n_items + self.n_bins = n_bins + self.bin_width = bin_width + self.bin_height = bin_height + getData = GetData(self.n_instance, self.n_items, self.bin_width, self.bin_height) + self._datasets = getData.generate_instances() + + def plot_solution(self, bins: List[List[Tuple[Tuple[int, int], Tuple[int, int]]]], bin_dimensions: Tuple[int, int]): + """ + Plot the final packing solution for 2D bin packing. + + Args: + bins: A list of bins, where each bin is a list of tuples containing the corner and dimensions of packed items. + bin_dimensions: A tuple representing the (width, height) of the bin. + """ + # Only plot the used bins + num_bins = sum(1 for bin_content in bins if bin_content) + 5 + bins = bins[:num_bins] + max_bins_per_row = 5 + num_rows = (num_bins + max_bins_per_row - 1) // max_bins_per_row # Calculate the number of rows needed + + fig, axes = plt.subplots(num_rows, max_bins_per_row, figsize=(5 * max_bins_per_row, 5 * num_rows)) + + # Flatten the axes array if there are multiple rows + if num_rows > 1: + axes = axes.flatten() + else: + axes = [axes] # Ensure axes is a list for consistency + + for i, bin_content in enumerate(bins): + ax = axes[i] + ax.set_xlim(0, bin_dimensions[0]) + ax.set_ylim(0, bin_dimensions[1]) + ax.set_title(f"Bin {i + 1}") + ax.set_aspect('equal') + + # Draw the bin boundary + bin_rect = patches.Rectangle((0, 0), bin_dimensions[0], bin_dimensions[1], linewidth=2, edgecolor='black', facecolor='none') + ax.add_patch(bin_rect) + + # Draw each item in the bin + for corner, (width, height) in bin_content: + item_rect = patches.Rectangle(corner, width, height, linewidth=1, edgecolor='blue', facecolor='lightblue', alpha=0.6) + ax.add_patch(item_rect) + # Add text to label the item + ax.text(corner[0] + width / 2, corner[1] + height / 2, f"{width}x{height}", ha='center', va='center', fontsize=8) + + # Hide unused axes if the number of bins is not a multiple of max_bins_per_row + for j in range(num_bins, num_rows * max_bins_per_row): + axes[j].axis('off') + + plt.tight_layout() + plt.show() + + def pack_items_2d(self, item_dimensions: List[Tuple[int, int]], bin_dimensions: Tuple[int, int], eva: Callable, n_bins: int) -> Tuple[int, List[List[Tuple[int, int]]]]: + """ + Pack items into bins using a constructive heuristic for the 2D Bin Packing Problem. + The bins are represented as a discrete point matrix to track feasible areas. + + Args: + item_dimensions: A list of tuples, where each tuple represents the (width, height) of an item. + bin_dimensions: A tuple representing the (width, height) of the bin. + eva: The constructive heuristic function to select the next item and bin. + n_bins: The number of available bins at the beginning. + + Returns: + A tuple containing: + - The total number of bins used. + - A list of bins, where each bin is a list of item dimensions. + """ + bins = [[] for _ in range(n_bins)] # Initialize n_bins empty bins + remaining_items = item_dimensions.copy() # Copy of item dimensions to track remaining items + # Initialize the point matrix for each bin (0: unoccupied, 1: occupied) + point_matrices = [[[0 for _ in range(bin_dimensions[1])] for _ in range(bin_dimensions[0])] for _ in range(n_bins)] + + while remaining_items: + # Use the heuristic to select the next item and bin + selected_item, selected_bin = eva(remaining_items, point_matrices) + + if selected_bin is not None: + # Find a feasible position for the selected item in the selected bin + placed = False + for x in range(bin_dimensions[0] - selected_item[0] + 1): + for y in range(bin_dimensions[1] - selected_item[1] + 1): + # Check the four edges of the item + top_edge = all(point_matrices[selected_bin][x + dx][y] == 0 for dx in range(selected_item[0])) + bottom_edge = all(point_matrices[selected_bin][x + dx][y + selected_item[1] - 1] == 0 for dx in range(selected_item[0])) + left_edge = all(point_matrices[selected_bin][x][y + dy] == 0 for dy in range(selected_item[1])) + right_edge = all(point_matrices[selected_bin][x + selected_item[0] - 1][y + dy] == 0 for dy in range(selected_item[1])) + + if top_edge and bottom_edge and left_edge and right_edge: + # Place the item at this position + for dx in range(selected_item[0]): + for dy in range(selected_item[1]): + point_matrices[selected_bin][x + dx][y + dy] = 1 + bins[selected_bin].append(((x, y), selected_item)) + placed = True + break + if placed: + break + if not placed: + # If the item cannot be placed in the selected bin, try other bins + for i in range(len(bins)): + if placed: + break + selected_bin = i + for x in range(bin_dimensions[0] - selected_item[0] + 1): + for y in range(bin_dimensions[1] - selected_item[1] + 1): + # Check only the four corners of the item + corners = [ + (x, y), + (x + selected_item[0] - 1, y), + (x, y + selected_item[1] - 1), + (x + selected_item[0] - 1, y + selected_item[1] - 1) + ] + if all(point_matrices[selected_bin][cx][cy] == 0 for cx, cy in corners): + # Place the item at this position + for dx in range(selected_item[0]): + for dy in range(selected_item[1]): + point_matrices[selected_bin][x + dx][y + dy] = 1 + bins[selected_bin].append(((x, y), selected_item)) + placed = True + break + if placed: + break + else: + # If no feasible bin is found, stop packing (no more bins available) + break + + # Remove the selected item from the remaining items + remaining_items.remove(selected_item) + + # Calculate the number of bins used (bins that contain at least one item) + used_bins = sum(1 for bin_content in bins if bin_content) + return used_bins, bins + + def evaluate_2d(self, eva: Callable) -> float: + """ + Evaluate the constructive heuristic for the 2D Bin Packing Problem. + + Args: + eva: callable function of constructive heuristic. + + Returns: + The average number of bins used across all instances. + """ + total_bins = 0 + + for instance in self._datasets[:self.n_instance]: + item_dimensions, bin_dimensions = instance + num_bins, _ = self.pack_items_2d(item_dimensions, bin_dimensions, eva, self.n_bins) + total_bins += num_bins + + average_bins = total_bins / self.n_instance + return -average_bins # Negative because we want to minimize the number of bins + + def evaluate_program(self, program_str: str, callable_func: Callable) -> Any | None: + return self.evaluate_2d(callable_func) + + +if __name__ == '__main__': + + def determine_next_assignment(remaining_items: List[Tuple[int, int]], feasible_corners: List[List[Tuple[int, int]]]) -> Tuple[Tuple[int, int], int]: + """ + A simple heuristic function to select the next item and bin for 2D bin packing. + + Args: + remaining_items: A list of tuples representing the (width, height) of remaining items. + feasible_corners: A list of lists, where each inner list contains the feasible corners for a bin. + + Returns: + A tuple containing: + - The selected item (width, height). + - The index of the selected bin (or None if no bin is feasible). + """ + # Step 1: Select the largest item by area + selected_item = max(remaining_items, key=lambda x: x[0] * x[1]) + + # Step 2: Select the bin with the most feasible corners + max_corners = -1 + selected_bin = None + for i, corners in enumerate(feasible_corners): + if len(corners) > max_corners: + max_corners = len(corners) + selected_bin = i + + # If no bin has feasible corners, return None for the bin + if max_corners == 0: + selected_bin = None + + return selected_item, selected_bin + + + bp2d = BP2DEvaluation() + ave_bins = bp2d.evaluate_program('_', determine_next_assignment) + print(ave_bins) + +# Task configuration for benchmark task +ENTRY_NAME = 'determine_next_assignment' +FUNCTION_SIGNATURE = 'def determine_next_assignment(...):' +IMPORT_HEADER = 'import numpy as np\nimport math' +TASK_DESCRIPTION = "'" +OBJECTIVE_TEXT = "You are optimizing the implementation of `determine_next_assignment` for the LLM4AD task.\\n\\nTask description:\\n'\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible." +TEMPLATE_FUNCTION = 'import numpy as np\ndef determine_next_assignment(remaining_items: List[Tuple[int, int]], point_matrices: List[List[List[int]]]) -> Tuple[Tuple[int, int], int]:\n """\n A simple heuristic function to select the next item and bin for packing.\n\n Args:\n remaining_items: A list of tuples, where each tuple represents the (width, height) of an item.\n point_matrices: A list of 2D matrices representing the occupied (1) and unoccupied (0) points in each bin.\n\n Returns:\n A tuple containing:\n - The selected item (width, height).\n - The selected bin index (or None if no bin is feasible).\n """\n # Select the largest item (based on area) from the remaining items\n selected_item = max(remaining_items, key=lambda item: item[0] * item[1])\n\n # Try to find a feasible bin for the selected item\n for bin_idx, point_matrix in enumerate(point_matrices):\n bin_width = len(point_matrix)\n bin_height = len(point_matrix[0]) if bin_width > 0 else 0\n # Check if the item fits in the bin\n if bin_width >= selected_item[0] and bin_height >= selected_item[1]:\n # Check for a feasible position in the bin\n for x in range(bin_width - selected_item[0] + 1):\n for y in range(bin_height - selected_item[1] + 1):\n # Check if the area is unoccupied\n if all(point_matrix[x + dx][y + dy] == 0 for dx in range(selected_item[0]) for dy in range(selected_item[1])):\n return selected_item, bin_idx\n # If no feasible bin is found, return None for the bin\n return selected_item, None' +EVAL_CLASS_NAME = 'BP2DEvaluation' +EVAL_KWARGS = {'timeout_seconds': 120} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_bp_2d_construct/get_instance.py b/examples/benchmark_tasks/optimization_bp_2d_construct/get_instance.py new file mode 100644 index 00000000..a9cb77d5 --- /dev/null +++ b/examples/benchmark_tasks/optimization_bp_2d_construct/get_instance.py @@ -0,0 +1,40 @@ +import numpy as np + + +class GetData: + def __init__(self, n_instance: int, n_items: int, bin_width: int, bin_height: int): + """ + Initialize the GetData class for the 2D Bin Packing Problem. + + Args: + n_instance: Number of instances to generate. + n_items: Number of items. + bin_width: Width of each bin. + bin_height: Height of each bin. + """ + self.n_instance = n_instance + self.n_items = n_items + self.bin_width = bin_width + self.bin_height = bin_height + + def generate_instances(self): + """ + Generate instances for the 2D Bin Packing Problem. + + Returns: + A list of tuples, where each tuple contains: + - item_dimensions: A list of tuples, where each tuple represents the (width, height) of an item. + - bin_dimensions: A tuple representing the (width, height) of the bin. + """ + np.random.seed(2024) # Set seed for reproducibility + instance_data = [] + + for _ in range(self.n_instance): + # Generate random item dimensions, ensuring no item exceeds the bin dimensions + item_widths = np.random.randint(10, self.bin_width - 10, size=self.n_items) + item_heights = np.random.randint(10, self.bin_height - 10, size=self.n_items) + item_dimensions = list(zip(item_widths, item_heights)) + bin_dimensions = (self.bin_width, self.bin_height) + instance_data.append((item_dimensions, bin_dimensions)) + + return instance_data diff --git a/examples/benchmark_tasks/optimization_bp_2d_construct/paras.yaml b/examples/benchmark_tasks/optimization_bp_2d_construct/paras.yaml new file mode 100644 index 00000000..2d30a9f6 --- /dev/null +++ b/examples/benchmark_tasks/optimization_bp_2d_construct/paras.yaml @@ -0,0 +1,2 @@ +name: BP2DEvaluation +timeout_seconds: 120 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_capacitated_warehouse_location/__init__.py b/examples/benchmark_tasks/optimization_capacitated_warehouse_location/__init__.py new file mode 100644 index 00000000..6c408169 --- /dev/null +++ b/examples/benchmark_tasks/optimization_capacitated_warehouse_location/__init__.py @@ -0,0 +1,398 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_capacitated_warehouse_location +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# References: +# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language +# model agents in algorithm search for combinatorial optimization. +# arXiv preprint arXiv:2504.04310 (2025). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any +import numpy as np +from llm4ad_loader import Evaluation +from llm4ad_loader import load_subdir_as_text +# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader +# from llm4ad.task.optimization.co_bench.capacitated_warehouse_location_co_bench.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(m: int, n: int, warehouses: list, customers: list) -> dict:\n """\n Solves the Capacitated Warehouse Location Problem with Splittable Customer Demand.\n Input kwargs:\n - m (int): Number of potential warehouses\n - n (int): Number of customers\n - warehouses (list of dict): A list of dictionaries, each with keys \'capacity\' (float) and \'fixed_cost\' (float)\n - customers (list of dict): A list of dictionaries, each with keys \'demand\' (float) and \'costs\' (list of float) representing the per-unit assignment cost from each warehouse\n Evaluation Metric:\n The objective is to minimize the total cost, computed as:\n (Sum of fixed costs for all open warehouses)\n + (Sum of per-unit assignment costs for each unit of demand allocated from warehouses to customers)\n For each customer, the sum of allocations from all warehouses must equal the customer\'s demand.\n For each warehouse, the total allocated demand across all customers must not exceed its capacity.\n If a solution violates any of these constraints, the solution is considered infeasible and no score is provided.\n Returns:\n A dictionary with the following keys:\n \'total_cost\': (float) The computed objective value (cost) if the solution is feasible;\n otherwise, no score is provided.\n \'warehouse_open\': (list of int) A list of m integers (0 or 1) indicating whether each warehouse is closed or open.\n \'assignments\': (list of list of float) A 2D list (n x m) where each entry represents the amount of customer i\'s demand supplied by warehouse j.\n """\n ## placeholder. You do not need to write anything here.\n return {\n "total_cost": 0.0,\n "warehouse_open": [0] * kwargs["m"],\n "assignments": [[0.0] * kwargs["m"] for _ in range(kwargs["n"])]\n }' +task_description = '("The Capacitated Warehouse Location Problem with Splittable Demand aims to determine which "' + + +__all__ = ['CWLEvaluationCB'] + + +class CWLEvaluationCB(Evaluation): + + def __init__(self, + timeout_seconds=50, + **kwargs): + + """ + Args: + None + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + # Load datasets from Hugging Face with fallback + dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Capacitated warehouse location") + self._datasets = {} + for filename in dataset: + # Join all text rows into a single string + text_content = '\n'.join([row['text'] for row in dataset[filename]]) + self._datasets[filename] = text_content + + def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: + return self.evaluate(callable_func) + + def evaluate(self, eva: callable) -> float | None: + ins_cases = [] + for case_id, ins in enumerate(self._datasets.values()): + ins_cases.append(self.load_data(ins)) + + fitness_list = [] + try: + for i in ins_cases: + for j in i: + result = eva(j['m'], j['n'], j['warehouses'], j['customers']) + fitness = self.eval_func(j['m'], j['n'], j['warehouses'], j['customers'], result['warehouse_open'], result['assignments']) + fitness_list.append(fitness) + + return -np.mean(fitness_list) + + except ValueError as e: + print(e) + return None + + def load_data(self, input_string): + """ + Reads one or more problem cases from the input string. + Expected Input String Format for each case: + Line 1: Two integers: m n + Next m lines: Each line contains two numbers: capacity fixed_cost for a warehouse. + Next n lines: Each line contains: demand (a number) followed by m numbers representing the cost of + allocating the customer's demand to each warehouse. + If the input string contains multiple cases, the cases appear sequentially. + Returns: + A list of dictionaries, each corresponding to one case. Each dictionary has the keys: + - 'm': Number of potential warehouses (int) + - 'n': Number of customers (int) + - 'warehouses': List of dictionaries; each with keys 'capacity' and 'fixed_cost' + - 'customers': List of dictionaries; each with keys 'demand' and 'costs' (list of floats) + """ + try: + all_lines = [line.strip() for line in input_string.split('\n')] + except Exception as e: + raise ValueError("Error reading input string: " + str(e)) + + # Tokenize all non-empty lines. + tokens = [] + for line in all_lines: + line = line.strip() + if line: + tokens.extend(line.split()) + + cases = [] + index = 0 + total_tokens = len(tokens) + + # Process tokens until we have exhausted them. + while index < total_tokens: + if index + 1 >= total_tokens: + raise ValueError("Insufficient tokens to read m and n for a case.") + try: + m = int(tokens[index]) + n = int(tokens[index + 1]) + except Exception as e: + raise ValueError("Error parsing m or n: " + str(e)) + index += 2 + + # Parse warehouse data (m warehouses, each with 2 tokens). + expected_warehouse_tokens = m * 2 + if index + expected_warehouse_tokens - 1 >= total_tokens: + raise ValueError("Not enough tokens for warehouse data in a case.") + warehouses = [] + for i in range(m): + try: + capacity = float(tokens[index]) + fixed_cost = float(tokens[index + 1]) + except Exception as e: + raise ValueError("Error parsing warehouse data: " + str(e)) + warehouses.append({'capacity': capacity, 'fixed_cost': fixed_cost}) + index += 2 + + # Parse customer data (n customers, each with 1 demand and m cost values). + customers = [] + for j in range(n): + if index >= total_tokens: + raise ValueError(f"Not enough tokens for customer {j + 1} demand.") + try: + demand = float(tokens[index]) + except Exception as e: + raise ValueError(f"Error parsing demand for customer {j + 1}: " + str(e)) + index += 1 + if index + m - 1 >= total_tokens: + raise ValueError(f"Not enough tokens for cost data for customer {j + 1}.") + costs = [] + for i in range(m): + try: + cost = float(tokens[index]) + except Exception as e: + raise ValueError(f"Error parsing cost for customer {j + 1}, warehouse {i + 1}: " + str(e)) + costs.append(cost) + index += 1 + customers.append({'demand': demand, 'costs': costs}) + + case_data = {"m": m, "n": n, "warehouses": warehouses, "customers": customers} + cases.append(case_data) + + return cases + + def eval_func(self, m, n, warehouses, customers, warehouse_open, assignments, **kwargs): + """ + Evaluates the solution for the Capacitated Warehouse Location Problem with Splittable Customer Demand, + using a weighted average cost for each customer. + For each customer: + - The sum of allocations across warehouses must equal the customer's demand. + - The assignment cost is computed as the weighted average of the per-unit costs, + i.e., for each warehouse i, the fraction of demand allocated from i multiplied by its cost. + - No positive allocation is allowed for a warehouse that is closed. + Additionally, for each warehouse: + - The total allocated demand must not exceed its capacity. + The total cost is computed as: + (Sum of fixed costs for all open warehouses) + + (Sum over customers of the weighted average assignment cost) + Input Parameters: + - m: Number of potential warehouses (int) + - n: Number of customers (int) + - warehouses: List of dictionaries (each with 'capacity' and 'fixed_cost') + - customers: List of dictionaries (each with 'demand' and 'costs' (list of floats representing per-unit cost)) + - warehouse_open: List of m integers (0 or 1) indicating whether each warehouse is closed or open. + - assignments: List of n lists (each of length m) where assignments[j][i] represents the amount of + customer j's demand allocated to warehouse i. + - kwargs: Other parameters (not used here). + Returns: + A floating-point number representing the total cost if the solution is feasible. + Raises: + Exception: If any of the following conditions are violated: + - The sum of allocations for any customer does not equal its demand. + - Any positive allocation is made to a closed warehouse. + - Any warehouse's total allocated demand exceeds its capacity. + """ + computed_total_cost = 0.0 + + # Add fixed costs for open warehouses. + for i in range(m): + if warehouse_open[i] == 1: + computed_total_cost += warehouses[i]['fixed_cost'] + + # Evaluate assignment cost for each customer as a weighted average. + for j in range(n): + customer_demand = customers[j]['demand'] + allocated_amount = sum(assignments[j]) + if abs(allocated_amount - customer_demand) > 1e-6: + raise Exception( + f"Customer {j} demand violation: total assigned amount {allocated_amount} does not equal demand {customer_demand}." + ) + weighted_cost = 0.0 + for i in range(m): + allocation = assignments[j][i] + if allocation < 0: + raise Exception( + f"Customer {j} has a negative allocation {allocation} for warehouse {i + 1}." + ) + if allocation > 0 and warehouse_open[i] != 1: + raise Exception( + f"Customer {j} has allocation {allocation} for warehouse {i + 1}, which is closed." + ) + # Compute fraction of the customer's demand supplied from warehouse i. + fraction = allocation / customer_demand if customer_demand > 0 else 0.0 + weighted_cost += fraction * customers[j]['costs'][i] + # Add the weighted cost (applied once per customer). + computed_total_cost += weighted_cost + + # Compute total demand allocated to each warehouse and check capacity constraints. + assigned_demand = [0.0] * m + for i in range(m): + for j in range(n): + assigned_demand[i] += assignments[j][i] + for i in range(m): + if assigned_demand[i] > warehouses[i]['capacity'] + 1e-6: + excess = assigned_demand[i] - warehouses[i]['capacity'] + raise Exception( + f"Warehouse {i + 1} exceeds its capacity by {excess} units." + ) + + return computed_total_cost + + def norm_score(self, results): + optimal_scores = { + "cap41.txt": [1040444.375], + "cap42.txt": [1098000.450], + "cap43.txt": [1153000.450], + "cap44.txt": [1235500.450], + "cap51.txt": [1025208.225], + "cap61.txt": [932615.750], + "cap62.txt": [977799.400], + "cap63.txt": [1014062.050], + "cap64.txt": [1045650.250], + "cap71.txt": [932615.750], + "cap72.txt": [977799.400], + "cap73.txt": [1010641.450], + "cap74.txt": [1034976.975], + "cap81.txt": [838499.288], + "cap82.txt": [910889.563], + "cap83.txt": [975889.563], + "cap84.txt": [1069369.525], + "cap91.txt": [796648.438], + "cap92.txt": [855733.500], + "cap93.txt": [896617.538], + "cap94.txt": [946051.325], + "cap101.txt": [796648.437], + "cap102.txt": [854704.200], + "cap103.txt": [893782.112], + "cap104.txt": [928941.750], + "cap111.txt": [826124.713], + "cap112.txt": [901377.213], + "cap113.txt": [970567.750], + "cap114.txt": [1063356.488], + "cap121.txt": [793439.563], + "cap122.txt": [852524.625], + "cap123.txt": [895302.325], + "cap124.txt": [946051.325], + "cap131.txt": [793439.562], + "cap132.txt": [851495.325], + "cap133.txt": [893076.712], + "cap134.txt": [928941.750], + "capa-8000.txt": [19240822.449], + "capa-10000.txt": [18438046.543], + "capa-12000.txt": [17765201.949], + "capa-14000.txt": [17160439.012], + "capb-5000.txt": [13656379.578], + "capb-6000.txt": [13361927.449], + "capb-7000.txt": [13198556.434], + "capb-8000.txt": [13082516.496], + "capc-5000.txt": [11646596.974], + "capc-5750.txt": [11570340.289], + "capc-6500.txt": [11518743.744], + "capc-7250.txt": [11505767.394] + } + + normed = {} + for case, (scores, error_message) in results.items(): + if case not in optimal_scores: + continue # Skip if there's no optimal score defined. + optimal_list = optimal_scores[case] + normed_scores = [] + # Compute normalized score for each index. + for idx, score in enumerate(scores): + if isinstance(score, (int, float)): + normed_scores.append(optimal_list[idx] / score) + else: + normed_scores.append(score) + normed[case] = (normed_scores, error_message) + + return normed + + def get_dev(self): + dev = {'cap101.txt': [], 'cap112.txt': [], + 'cap123.txt': [], + 'cap134.txt': [], + 'cap41.txt': [], 'cap62.txt': [], 'cap73.txt': [], 'cap84.txt': [], + 'cap91.txt': [], + 'capa-12000.txt': [], + 'capb-5000.txt': [], + 'capc-7250.txt': []} + + return dev + + +# Task configuration for benchmark task +ENTRY_NAME = 'solve' +FUNCTION_SIGNATURE = 'def solve(...):' +IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' +TASK_DESCRIPTION = '("The Capacitated Warehouse Location Problem with Splittable Demand aims to determine which "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The Capacitated Warehouse Location Problem with Splittable Demand aims to determine which "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(m: int, n: int, warehouses: list, customers: list) -> dict:\n """\n Solves the Capacitated Warehouse Location Problem with Splittable Customer Demand.\n Input kwargs:\n - m (int): Number of potential warehouses\n - n (int): Number of customers\n - warehouses (list of dict): A list of dictionaries, each with keys \'capacity\' (float) and \'fixed_cost\' (float)\n - customers (list of dict): A list of dictionaries, each with keys \'demand\' (float) and \'costs\' (list of float) representing the per-unit assignment cost from each warehouse\n Evaluation Metric:\n The objective is to minimize the total cost, computed as:\n (Sum of fixed costs for all open warehouses)\n + (Sum of per-unit assignment costs for each unit of demand allocated from warehouses to customers)\n For each customer, the sum of allocations from all warehouses must equal the customer\'s demand.\n For each warehouse, the total allocated demand across all customers must not exceed its capacity.\n If a solution violates any of these constraints, the solution is considered infeasible and no score is provided.\n Returns:\n A dictionary with the following keys:\n \'total_cost\': (float) The computed objective value (cost) if the solution is feasible;\n otherwise, no score is provided.\n \'warehouse_open\': (list of int) A list of m integers (0 or 1) indicating whether each warehouse is closed or open.\n \'assignments\': (list of list of float) A 2D list (n x m) where each entry represents the amount of customer i\'s demand supplied by warehouse j.\n """\n ## placeholder. You do not need to write anything here.\n return {\n "total_cost": 0.0,\n "warehouse_open": [0] * kwargs["m"],\n "assignments": [[0.0] * kwargs["m"] for _ in range(kwargs["n"])]\n }' +EVAL_CLASS_NAME = 'CWLEvaluationCB' +EVAL_KWARGS = {'timeout_seconds': 20} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_capacitated_warehouse_location/paras.yaml b/examples/benchmark_tasks/optimization_capacitated_warehouse_location/paras.yaml new file mode 100644 index 00000000..8d3ff068 --- /dev/null +++ b/examples/benchmark_tasks/optimization_capacitated_warehouse_location/paras.yaml @@ -0,0 +1,2 @@ +name: CWLEvaluationCB +timeout_seconds: 20 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_cflp_construct/__init__.py b/examples/benchmark_tasks/optimization_cflp_construct/__init__.py new file mode 100644 index 00000000..b91eb218 --- /dev/null +++ b/examples/benchmark_tasks/optimization_cflp_construct/__init__.py @@ -0,0 +1,310 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_cflp_construct +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# Module Name: CFLPEvaluation +# Last Revision: 2025/2/16 +# Description: Evaluates the Capacitated Facility Location Problem (CFLP). +# Given a set of facilities and customers, the goal is to assign customers to facilities +# while respecting facility capacities and minimizing total costs. +# This module is part of the LLM4AD project (https://github.com/Optima-CityU/llm4ad). +# +# Parameters: +# - timeout_seconds: Maximum allowed time (in seconds) for the evaluation process: int (default: 60). +# - n_instance: Number of problem instances to generate: int (default: 16). +# - n_facilities: Number of facilities: int (default: 5). +# - n_customers: Number of customers: int (default: 8). +# - max_capacity: Maximum capacity of each facility: int (default: 100). +# - max_demand: Maximum demand of each customer: int (default: 20). +# - max_cost: Maximum cost of assigning a customer to a facility: int (default: 50). +# +# References: +# - Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + + +from __future__ import annotations +from typing import Callable, Any, List, Tuple +import numpy as np +import matplotlib.pyplot as plt + +from llm4ad_loader import Evaluation +import os, sys +sys.path.insert(0, os.path.dirname(__file__)) +from get_instance import GetData +# from llm4ad.task.optimization.cflp_construct.get_instance import GetData # Converted from LLM4AD import +# from llm4ad.task.optimization.cflp_construct.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\n\ndef select_next_assignment(assignments: List[List[int]], remaining_customers: List[int], remaining_capacities: List[int], customer_demands: List[int], assignment_costs: List[List[int]]) -> Tuple[int, int]:\n """\n Constructive heuristic for the Capacitated Facility Location Problem.\n Assigns the next customer to the facility with the lowest cost that has sufficient capacity.\n\n Args:\n assignments: Current assignments of customers to facilities.\n remaining_customers: List of customer indices not yet assigned.\n remaining_capacities: Remaining capacities of facilities.\n customer_demands: List of customer demands.\n assignment_costs: 2D list of assignment costs (facility-to-customer).\n\n Returns:\n A tuple containing:\n - The selected customer index.\n - The selected facility index (or None if no feasible assignment exists).\n """\n # Iterate over all remaining customers\n for customer in remaining_customers:\n # Iterate over all facilities to find the one with the lowest cost and sufficient capacity\n min_cost = float(\'inf\')\n selected_facility = None\n\n for facility in range(len(remaining_capacities)):\n if remaining_capacities[facility] >= customer_demands[customer] and assignment_costs[facility][customer] < min_cost:\n min_cost = assignment_costs[facility][customer]\n selected_facility = facility\n\n # If a feasible facility is found, return the customer and facility\n if selected_facility is not None:\n return customer, selected_facility\n\n # If no feasible assignment is found, return None\n return None, None' +task_description = "'" + + +__all__ = ['CFLPEvaluation'] + + +class CFLPEvaluation(Evaluation): + """Evaluator for the Capacitated Facility Location Problem.""" + + def __init__(self, + timeout_seconds: int = 60, + n_instance: int = 16, + n_facilities: int = 50, + n_customers: int = 50, + max_capacity: int = 100, + max_demand: int = 20, + max_cost: int = 50, + **kwargs): + """ + Initialize the evaluator. + """ + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + self.n_instance = n_instance + self.n_facilities = n_facilities + self.n_customers = n_customers + self.max_capacity = max_capacity + self.max_demand = max_demand + self.max_cost = max_cost + getData = GetData(self.n_instance, self.n_facilities, self.n_customers, self.max_capacity, self.max_demand, self.max_cost) + self._datasets = getData.generate_instances() + + def evaluate_program(self, program_str: str, callable_func: Callable) -> Any | None: + return self.evaluate_cflp(callable_func) + + def plot_solution(self, facility_capacities: List[int], customer_demands: List[int], assignments: List[List[int]], assignment_costs: List[List[int]]): + """ + Plot the final solution of assignments for the Capacitated Facility Location Problem. + + Args: + facility_capacities: A list of facility capacities. + customer_demands: A list of customer demands. + assignments: A list of assignments, where each assignment is a list of customer indices assigned to a facility. + assignment_costs: A 2D list (matrix) of costs, where the cost of assigning customer j to facility i is assignment_costs[i][j]. + """ + n_facilities = len(facility_capacities) + n_customers = len(customer_demands) + + # Create a figure and axis + fig, ax = plt.subplots(figsize=(10, 6)) + + # Plot facilities and customers + for facility in range(n_facilities): + # Plot facility as a rectangle + ax.add_patch(plt.Rectangle((facility - 0.4, -0.4), 0.8, 0.8, color='skyblue', label='Facility' if facility == 0 else None)) + ax.text(facility, 0, f'F{facility}\nCap: {facility_capacities[facility]}', ha='center', va='center', fontsize=10) + + # Plot assigned customers + for customer in assignments[facility]: + ax.plot([facility, customer], [0, 1], 'k--', linewidth=0.5) # Line connecting facility to customer + ax.add_patch(plt.Circle((customer, 1), 0.1, color='orange', label='Customer' if facility == 0 and customer == 0 else None)) + ax.text(customer, 1.1, f'C{customer}\nDem: {customer_demands[customer]}', ha='center', va='bottom', fontsize=8) + # Add cost as text near the line + ax.text((facility + customer) / 2, 0.5, f'Cost: {assignment_costs[facility][customer]}', ha='center', va='center', fontsize=8, rotation=45) + + # Set axis limits and labels + ax.set_xlim(-1, n_customers) + ax.set_ylim(-0.5, 1.5) + ax.set_xticks(range(n_customers)) + ax.set_yticks([0, 1]) + ax.set_yticklabels(['Facilities', 'Customers']) + ax.set_title('Capacitated Facility Location Problem - Assignments') + ax.legend(loc='upper right') + + # Show the plot + plt.tight_layout() + plt.show() + + def assign_customers(self, facility_capacities: List[int], customer_demands: List[int], assignment_costs: List[List[int]], eva: Callable) -> Tuple[int, List[List[int]]]: + """ + Assign customers to facilities using a constructive heuristic. + + Args: + facility_capacities: A list of facility capacities. + customer_demands: A list of customer demands. + assignment_costs: A 2D list (matrix) of costs, where the cost of assigning customer j to facility i is assignment_costs[i][j]. + eva: The constructive heuristic function to select the next customer-facility assignment. + + Returns: + A tuple containing: + - The total cost of the assignments. + - A list of assignments, where each assignment is a list of customer indices assigned to a facility. + """ + n_facilities = len(facility_capacities) + n_customers = len(customer_demands) + assignments = [[] for _ in range(n_facilities)] # Initialize empty assignments for each facility + remaining_customers = list(range(n_customers)) # List of remaining customer indices + remaining_capacities = facility_capacities.copy() # Copy of facility capacities to track remaining capacities + total_cost = 0 # Total cost of assignments + + while remaining_customers: + # Use the heuristic to select the next customer-facility assignment + selected_customer, selected_facility = eva(assignments, remaining_customers, remaining_capacities, customer_demands, assignment_costs) + + if selected_facility is not None: + # Assign the selected customer to the selected facility + assignments[selected_facility].append(selected_customer) + # Update the remaining capacity of the selected facility + remaining_capacities[selected_facility] -= customer_demands[selected_customer] + # Add the assignment cost to the total cost + total_cost += assignment_costs[selected_facility][selected_customer] + else: + # If no feasible assignment is found, stop assigning (no more feasible assignments) + break + + # Remove the selected customer from the remaining customers + remaining_customers.remove(selected_customer) + + return total_cost, assignments + + def evaluate_cflp(self, eva: Callable) -> float: + """ + Evaluate the constructive heuristic for the Capacitated Facility Location Problem. + + Args: + instance_data: List of dictionaries containing facility capacities, customer demands, and assignment costs. + n_ins: Number of instances to evaluate. + eva: The constructive heuristic function to evaluate. + + Returns: + The average total cost across all instances. + """ + total_cost = 0 + + for instance in self._datasets[:self.n_instance]: + facility_capacities = instance["facility_capacities"] + customer_demands = instance["customer_demands"] + assignment_costs = instance["assignment_costs"] + cost, _ = self.assign_customers(facility_capacities, customer_demands, assignment_costs, eva) + total_cost += cost + + average_cost = total_cost / self.n_instance + return -average_cost + + +if __name__ == '__main__': + + def select_next_assignment(assignments: List[List[int]], remaining_customers: List[int], remaining_capacities: List[int], customer_demands: List[int], assignment_costs: List[List[int]]) -> Tuple[int, int]: + """ + Constructive heuristic for the Capacitated Facility Location Problem. + Assigns the next customer to the facility with the lowest cost that has sufficient capacity. + + Args: + assignments: Current assignments of customers to facilities. + remaining_customers: List of customer indices not yet assigned. + remaining_capacities: Remaining capacities of facilities. + customer_demands: List of customer demands. + assignment_costs: 2D list of assignment costs (facility-to-customer). + + Returns: + A tuple containing: + - The selected customer index. + - The selected facility index (or None if no feasible assignment exists). + """ + # Iterate over all remaining customers + for customer in remaining_customers: + # Iterate over all facilities to find the one with the lowest cost and sufficient capacity + min_cost = float('inf') + selected_facility = None + + for facility in range(len(remaining_capacities)): + if remaining_capacities[facility] >= customer_demands[customer] and assignment_costs[facility][customer] < min_cost: + min_cost = assignment_costs[facility][customer] + selected_facility = facility + + # If a feasible facility is found, return the customer and facility + if selected_facility is not None: + return customer, selected_facility + + # If no feasible assignment is found, return None + return None, None + + + bp1d = CFLPEvaluation() + ave_bins = bp1d.evaluate_program('_', select_next_assignment) + print(ave_bins) + +# Task configuration for benchmark task +ENTRY_NAME = 'select_next_assignment' +FUNCTION_SIGNATURE = 'def select_next_assignment(...):' +IMPORT_HEADER = 'import numpy as np\nimport math' +TASK_DESCRIPTION = "'" +OBJECTIVE_TEXT = "You are optimizing the implementation of `select_next_assignment` for the LLM4AD task.\\n\\nTask description:\\n'\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible." +TEMPLATE_FUNCTION = 'import numpy as np\n\ndef select_next_assignment(assignments: List[List[int]], remaining_customers: List[int], remaining_capacities: List[int], customer_demands: List[int], assignment_costs: List[List[int]]) -> Tuple[int, int]:\n """\n Constructive heuristic for the Capacitated Facility Location Problem.\n Assigns the next customer to the facility with the lowest cost that has sufficient capacity.\n\n Args:\n assignments: Current assignments of customers to facilities.\n remaining_customers: List of customer indices not yet assigned.\n remaining_capacities: Remaining capacities of facilities.\n customer_demands: List of customer demands.\n assignment_costs: 2D list of assignment costs (facility-to-customer).\n\n Returns:\n A tuple containing:\n - The selected customer index.\n - The selected facility index (or None if no feasible assignment exists).\n """\n # Iterate over all remaining customers\n for customer in remaining_customers:\n # Iterate over all facilities to find the one with the lowest cost and sufficient capacity\n min_cost = float(\'inf\')\n selected_facility = None\n\n for facility in range(len(remaining_capacities)):\n if remaining_capacities[facility] >= customer_demands[customer] and assignment_costs[facility][customer] < min_cost:\n min_cost = assignment_costs[facility][customer]\n selected_facility = facility\n\n # If a feasible facility is found, return the customer and facility\n if selected_facility is not None:\n return customer, selected_facility\n\n # If no feasible assignment is found, return None\n return None, None' +EVAL_CLASS_NAME = 'CFLPEvaluation' +EVAL_KWARGS = {'timeout_seconds': 30} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_cflp_construct/get_instance.py b/examples/benchmark_tasks/optimization_cflp_construct/get_instance.py new file mode 100644 index 00000000..e6ba0435 --- /dev/null +++ b/examples/benchmark_tasks/optimization_cflp_construct/get_instance.py @@ -0,0 +1,65 @@ +import numpy as np + + +class GetData: + def __init__(self, n_instance: int, n_facilities: int, n_customers: int, max_capacity: int, max_demand: int, max_cost: int): + """ + Initialize the GetDataCFLP class for the Capacitated Facility Location Problem. + + Args: + n_instance: Number of instances to generate. + n_facilities: Number of facilities. + n_customers: Number of customers. + max_capacity: Maximum capacity of any facility. + max_demand: Maximum demand of any customer. + max_cost: Maximum cost for assigning a customer to a facility. + """ + self.n_instance = n_instance + self.n_facilities = n_facilities + self.n_customers = n_customers + self.max_capacity = max_capacity + self.max_demand = max_demand + self.max_cost = max_cost + + def generate_instances(self): + """ + Generate instances for the Capacitated Facility Location Problem. + + Returns: + A list of dictionaries, where each dictionary contains: + - facility_capacities: A list of capacities for each facility. + - customer_demands: A list of demands for each customer. + - assignment_costs: A 2D list (matrix) of costs, where the cost of assigning + customer j to facility i is assignment_costs[i][j]. + """ + np.random.seed(2024) # Set seed for reproducibility + instance_data = [] + + for _ in range(self.n_instance): + # Generate random capacities for facilities + facility_capacities = np.random.randint(5, self.max_capacity + 1, size=self.n_facilities).tolist() + + # Generate random demands for customers + customer_demands = np.random.randint(5, self.max_demand + 1, size=self.n_customers).tolist() + + # Generate random assignment costs (facility-to-customer cost matrix) + assignment_costs = np.random.randint(5, self.max_cost + 1, size=(self.n_facilities, self.n_customers)).tolist() + + instance_data.append({ + "facility_capacities": facility_capacities, + "customer_demands": customer_demands, + "assignment_costs": assignment_costs + }) + + return instance_data + +# # Example usage: +# data_generator = GetDataCFLP(n_instance=3, n_facilities=5, n_customers=8, max_capacity=100, max_demand=20, max_cost=50) +# instances = data_generator.generate_instances() +# for instance in instances: +# print("Facility Capacities:", instance["facility_capacities"]) +# print("Customer Demands:", instance["customer_demands"]) +# print("Assignment Costs:") +# for row in instance["assignment_costs"]: +# print(row) +# print() diff --git a/examples/benchmark_tasks/optimization_cflp_construct/paras.yaml b/examples/benchmark_tasks/optimization_cflp_construct/paras.yaml new file mode 100644 index 00000000..5d2994a8 --- /dev/null +++ b/examples/benchmark_tasks/optimization_cflp_construct/paras.yaml @@ -0,0 +1,2 @@ +name: CFLPEvaluation +timeout_seconds: 30 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_common_due_date_scheduling/__init__.py b/examples/benchmark_tasks/optimization_common_due_date_scheduling/__init__.py new file mode 100644 index 00000000..7645571d --- /dev/null +++ b/examples/benchmark_tasks/optimization_common_due_date_scheduling/__init__.py @@ -0,0 +1,310 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_common_due_date_scheduling +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# References: +# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language +# model agents in algorithm search for combinatorial optimization. +# arXiv preprint arXiv:2504.04310 (2025). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any +import numpy as np +from llm4ad_loader import Evaluation +from llm4ad_loader import load_subdir_as_text +# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader +# from llm4ad.task.optimization.co_bench.common_due_date_scheduling_co_bench.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(jobs: List[Tuple[int, int, int]], h: float = 0.6) -> Dict[str, List[int]]:\n """\n Solves the restricted single‐machine common due date scheduling problem.\n The problem:\n Given a list of jobs where each job is represented as a tuple (p, a, b):\n • p: processing time\n • a: earliness penalty coefficient\n • b: tardiness penalty coefficient\n and an optional parameter h (default 0.6), the common due date is computed as:\n d = floor(sum(p) * h)\n A schedule (i.e., a permutation of job indices in 1‐based numbering) is produced.\n When processing the jobs in that order, the penalty is computed by:\n • Adding a × (d − C) if a job’s completion time C is less than d,\n • Adding b × (C − d) if C is greater than d,\n • No penalty if C equals d.\n The objective is to minimize the total penalty.\n Input kwargs:\n - \'jobs\' (List[Tuple[int, int, int]]): a list of tuples where each tuple represents a job with:\n • p (int): processing time,\n • a (int): earliness penalty coefficient,\n • b (int): tardiness penalty coefficient.\n - Optional: \'h\' (float): the factor used to compute the common due date (default is 0.6).\n Evaluation Metric:\n The computed schedule is evaluated by accumulating processing times and applying\n the appropriate earliness/tardiness penalties with respect to the common due date.\n Returns:\n A dictionary with key \'schedule\' whose value is a list of integers representing\n a valid permutation of job indices (1-based).\n """\n # Placeholder implementation: simply return the jobs in their original order.\n jobs = kwargs.get(\'jobs\', [])\n n = len(jobs)\n return {\'schedule\': list(range(1, n + 1))}' +task_description = '("The **Restricted Single-Machine Common Due Date Scheduling Problem** involves scheduling a set "' + + +__all__ = ['CDDSEvaluationCB'] + + +class CDDSEvaluationCB(Evaluation): + + def __init__(self, + timeout_seconds=50, + **kwargs): + + """ + Args: + None + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + # Load datasets from Hugging Face + dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Common due date scheduling") + self._datasets = {} + for filename in dataset: + # Join all text rows into a single string + text_content = '\n'.join([row['text'] for row in dataset[filename]]) + self._datasets[filename] = text_content + + def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: + return self.evaluate(callable_func) + + def evaluate(self, eva: callable) -> float | None: + ins_cases = [] + for case_id, ins in enumerate(self._datasets.values()): + ins_cases.append(self.load_data(ins)) + + fitness_list = [] + try: + for i in ins_cases: + for j in i: + result = eva(j['jobs'], j['h']) + fitness = self.eval_func(j['jobs'], result['schedule'], j['h']) + fitness_list.append(fitness) + + return -np.mean(fitness_list) + + except ValueError as e: + print(e) + return None + + def load_data(self, input_string): + """ + Reads the input file and returns a list of cases. + Each case is represented as a dictionary containing: + - 'jobs': a list of tuples (p, a, b) for each job. + - 'h': a float parameter for due date computation (default set to 0.6). + The input format: + • The first token is an integer T indicating the number of cases. + • For each case: + – The first integer is n, the number of jobs. + – The following n lines each contain three space-separated integers: p, a, and b. + Returns: + List[dict]: A list where each element is a dictionary with the keys 'jobs' and 'h'. + """ + cases = [] + try: + tokens = input_string.strip().split() + except Exception as e: + raise ValueError(f"Error reading input file: {e}") + + index = 0 + try: + T = int(tokens[index]) + except Exception as e: + raise ValueError("Invalid input format: first token must be an integer (number of cases).") + index += 1 + + for t in range(T): + if index >= len(tokens): + raise ValueError(f"Unexpected end of input while reading case {t + 1}.") + try: + n = int(tokens[index]) + except Exception as e: + raise ValueError(f"Invalid job count for case {t + 1}.") + index += 1 + + jobs = [] + for i in range(n): + if index + 2 >= len(tokens): + raise ValueError(f"Unexpected end of input while reading job data for case {t + 1}.") + try: + p = int(tokens[index]) + a = int(tokens[index + 1]) + b = int(tokens[index + 2]) + except Exception as e: + raise ValueError(f"Invalid job data for job {i + 1} in case {t + 1}.") + index += 3 + jobs.append((p, a, b)) + + # For each case, we include the jobs and set a default h value (can be adjusted if needed) + cases.append({'jobs': jobs, 'h': 0.6}) + + return cases + + def eval_func(self, jobs, schedule, h=0.6): + """ + Evaluates the quality of a schedule for the restricted single‐machine common due date problem. + Parameters: + - jobs (List[Tuple[int, int, int]]): List of jobs, each represented as (p, a, b). + - schedule (List[int]): A permutation (1-based indices) representing the processing order. + - h (float): Factor for computing the common due date d = floor(sum(p) * h). + Returns: + int: The total penalty computed for the schedule. + The evaluation: + 1. Compute d = floor(total_processing_time * h). + 2. Process jobs in the given order, accumulating processing times. + 3. For each job, if the cumulative time C is less than d, add a penalty a × (d − C); + if C is greater than d, add a penalty b × (C − d); no penalty is incurred if C equals d. + 4. Sum the penalties to yield the total score. + """ + total_processing = sum(p for p, a, b in jobs) + d = int(total_processing * h) # floor operation via int conversion for non-negative totals + + cumulative_time = 0 + total_penalty = 0 + # Validate that schedule is a permutation of 1..n + n = len(jobs) + if sorted(schedule) != list(range(1, n + 1)): + raise ValueError(f"Schedule must be a permutation of 1 to {n}. Provided schedule: {schedule}") + + for idx in schedule: + try: + p, a, b = jobs[idx - 1] # Convert from 1-based to 0-based indexing + except IndexError: + raise ValueError(f"Job index {idx} is out of bounds for jobs list of length {n}.") + cumulative_time += p + if cumulative_time < d: + total_penalty += a * (d - cumulative_time) + elif cumulative_time > d: + total_penalty += b * (cumulative_time - d) + # No penalty if cumulative_time == d + return total_penalty + + def norm_score(self, results): + """ + Given a dictionary `results` where each key is a test case filename (e.g., "sch10.txt") + and the value is a tuple (scores, error_message), this function returns a new dictionary + with the normed results. For each test case, the normed score for each k is computed as: + norm = (optimal score for h=0.6) / (model's score) + The optimal scores for h=0.6 are pre-defined for each job instance size n. + If a score in the list is not numeric (e.g., "Timeout (10s)"), that entry is skipped. + Parameters: + results (dict): A dictionary where keys are filenames (e.g., "sch10.txt") and values + are tuples (scores, error_message). + Returns: + dict: A dictionary with the same keys, where each value is a list of normed scores + computed for the numeric entries only. + """ + # Pre-defined optimal scores for h = 0.6 by instance size (n) + optimal_scores = { + 10: [841, 615, 793, 815, 521, 755, 1101, 610, 582, 710], + 20: [2986, 3260, 3600, 3336, 2206, 3016, 4175, 1638, 1992, 2116], + 50: [17990, 14231, 16497, 14105, 14650, 14251, 17715, 21367, 14298, 14377], + 100: [72019, 59351, 68537, 69231, 55291, 62519, 62213, 80844, 58771, 61419], + 200: [254268, 266028, 254647, 297269, 260455, 236160, 247555, 225572, 255029, 269236], + 500: [1581233, 1715332, 1644947, 1640942, 1468325, 1413345, 1634912, 1542090, 1684055, 1520515], + 1000: [6411581, 6112598, 5985538, 6096729, 6348242, 6082142, 6575879, 6069658, 6188416, 6147295], + } + + normed = {} + for case, (scores, error_message) in results.items(): + # Try to extract the number of jobs (n) from the filename. + # Expected format: "sch{n}.txt", e.g., "sch10.txt" -> n = 10. + try: + n_val = int(case.replace("sch", "").replace(".txt", "")) + except ValueError: + continue # Skip if the filename is not in expected format. + + # Only process if we have optimal scores for this instance size. + if n_val not in optimal_scores: + continue + + optimal_list = optimal_scores[n_val] + normed_scores = [] + # Process each score in the scores list, along with its index (for k=1,...,10). + for idx, score in enumerate(scores): + # If the score is not numeric, skip it. + if isinstance(score, (int, float)): + # Compute normalized score as (optimal / model score) + norm_val = optimal_list[idx] / score + normed_scores.append(norm_val) + else: + normed_scores.append(score) + normed[case] = (normed_scores, error_message) + + return normed + + def get_dev(self): + dev = {'sch10.txt': [4, 5, 6], 'sch100.txt': [9, 8, 5], 'sch1000.txt': [4, 9, 0], + 'sch20.txt': [6, 5, 3], 'sch200.txt': [2, 4, 5], 'sch50.txt': [1, 8, 2], + 'sch500.txt': [3, 6, 9]} + + return dev + + +# Task configuration for benchmark task +ENTRY_NAME = 'solve' +FUNCTION_SIGNATURE = 'def solve(...):' +IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' +TASK_DESCRIPTION = '("The **Restricted Single-Machine Common Due Date Scheduling Problem** involves scheduling a set "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The **Restricted Single-Machine Common Due Date Scheduling Problem** involves scheduling a set "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(jobs: List[Tuple[int, int, int]], h: float = 0.6) -> Dict[str, List[int]]:\n """\n Solves the restricted single‐machine common due date scheduling problem.\n The problem:\n Given a list of jobs where each job is represented as a tuple (p, a, b):\n • p: processing time\n • a: earliness penalty coefficient\n • b: tardiness penalty coefficient\n and an optional parameter h (default 0.6), the common due date is computed as:\n d = floor(sum(p) * h)\n A schedule (i.e., a permutation of job indices in 1‐based numbering) is produced.\n When processing the jobs in that order, the penalty is computed by:\n • Adding a × (d − C) if a job’s completion time C is less than d,\n • Adding b × (C − d) if C is greater than d,\n • No penalty if C equals d.\n The objective is to minimize the total penalty.\n Input kwargs:\n - \'jobs\' (List[Tuple[int, int, int]]): a list of tuples where each tuple represents a job with:\n • p (int): processing time,\n • a (int): earliness penalty coefficient,\n • b (int): tardiness penalty coefficient.\n - Optional: \'h\' (float): the factor used to compute the common due date (default is 0.6).\n Evaluation Metric:\n The computed schedule is evaluated by accumulating processing times and applying\n the appropriate earliness/tardiness penalties with respect to the common due date.\n Returns:\n A dictionary with key \'schedule\' whose value is a list of integers representing\n a valid permutation of job indices (1-based).\n """\n # Placeholder implementation: simply return the jobs in their original order.\n jobs = kwargs.get(\'jobs\', [])\n n = len(jobs)\n return {\'schedule\': list(range(1, n + 1))}' +EVAL_CLASS_NAME = 'CDDSEvaluationCB' +EVAL_KWARGS = {'timeout_seconds': 20} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_common_due_date_scheduling/paras.yaml b/examples/benchmark_tasks/optimization_common_due_date_scheduling/paras.yaml new file mode 100644 index 00000000..fb3b977d --- /dev/null +++ b/examples/benchmark_tasks/optimization_common_due_date_scheduling/paras.yaml @@ -0,0 +1,2 @@ +name: CDDSEvaluationCB +timeout_seconds: 20 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_constrained_guillotine_cutting/__init__.py b/examples/benchmark_tasks/optimization_constrained_guillotine_cutting/__init__.py new file mode 100644 index 00000000..2bcd6b6f --- /dev/null +++ b/examples/benchmark_tasks/optimization_constrained_guillotine_cutting/__init__.py @@ -0,0 +1,399 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_constrained_guillotine_cutting +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# References: +# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language +# model agents in algorithm search for combinatorial optimization. +# arXiv preprint arXiv:2504.04310 (2025). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any +import numpy as np +from llm4ad_loader import Evaluation +from llm4ad_loader import load_subdir_as_text +# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader +# from llm4ad.task.optimization.co_bench.constrained_guillotine_cutting_co_bench.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(m: int, stock_length: int, stock_width: int, piece_types: list) -> dict:\n """\n Solves the Fixed Orientation Guillotine Cutting problem.\n Problem Description:\n Given a rectangular stock sheet (with specified length and width) and a set of piece types\n (each defined by a length, width, an upper bound on the number of times it may appear, and a value),\n the goal is to determine a placement for the pieces such that:\n - Each placed piece lies entirely within the stock sheet.\n - Pieces do not overlap.\n - The number of pieces placed for any type does not exceed its allowed maximum.\n - The orientation of the pieces is fixed (i.e. no rotation is allowed).\n - The total value reported equals the sum of the values of the placed pieces.\n Input kwargs (for one case):\n - m: integer, the number of piece types.\n - stock_length: integer, the length of the stock sheet.\n - stock_width: integer, the width of the stock sheet.\n - piece_types: list of dictionaries. Each dictionary has the keys:\n \'length\' : int, the length of the piece.\n \'width\' : int, the width of the piece.\n \'max\' : int, maximum number of pieces allowed.\n \'value\' : int, value of the piece.\n Returns:\n A dictionary with the following keys:\n - total_value: int, the computed total value (must equal the sum of the piece values in placements).\n - placements: list of placements, where each placement is a tuple of 6 integers:\n (piece_type_index, x, y, placed_length, placed_width, orientation_flag)\n The orientation_flag is always 0 since rotation is not allowed.\n """\n # Your optimization/placement algorithm should go here.\n # For now, this is a placeholder that meets the output format requirements.\n\n # Example placeholder output (no actual pieces placed):\n return {"total_value": 0, "placements": []}' +task_description = '("The problem involves optimizing the guillotine feasible placement of a set of rectangular pieces "' + + +__all__ = ['CGCEvaluationCB'] + + +class CGCEvaluationCB(Evaluation): + + def __init__(self, + timeout_seconds=50, + **kwargs): + + """ + Args: + None + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + # Load datasets from Hugging Face + dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Constrained guillotine cutting") + self._datasets = {} + for filename in dataset: + # Join all text rows into a single string + text_content = '\n'.join([row['text'] for row in dataset[filename]]) + self._datasets[filename] = text_content + + def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: + return self.evaluate(callable_func) + + def evaluate(self, eva: callable) -> float | None: + ins_cases = [] + for case_id, ins in enumerate(self._datasets.values()): + ins_cases.append(self.load_data(ins)) + + fitness_list = [] + try: + for i in ins_cases: + for j in i: + result = eva(j['m'], j['stock_length'], j['stock_width'], j['piece_types']) + fitness = self.eval_func(j['m'], j['stock_length'], j['stock_width'], j['piece_types'], result['total_value'], result['placements']) + fitness_list.append(fitness) + + return np.mean(fitness_list) # itself is a maximize problem + + except ValueError as e: + print(e) + return None + + def load_data(self, input_string): + """ + Loads one or more cases from an input file for the Constrained Guillotine Cutting problem. + The input file contains one or more cases concatenated together. Each case is structured as follows: + - The first token: an integer m (the number of piece types). + - The next two tokens: stock_length and stock_width. + - Then 4*m tokens follow, where each group of 4 tokens represents a piece type: + piece_length, piece_width, maximum permitted count, piece_value. + Parameters: + input_path (str): Path to the input TXT file. + Returns: + List[dict]: A list where each element is a dictionary with the following keys: + - m: int, number of piece types. + - stock_length: int, length of the stock sheet. + - stock_width: int, width of the stock sheet. + - piece_types: list of dicts, each with keys 'length', 'width', 'max', 'value'. + """ + cases = [] + content = input_string + tokens = content.split() + pos = 0 + total_tokens = len(tokens) + + while pos < total_tokens: + # Ensure there are at least 3 tokens to read m, stock_length, stock_width + if pos + 3 > total_tokens: + raise ValueError("Insufficient data for a new case.") + try: + m = int(tokens[pos]) + stock_length = int(tokens[pos + 1]) + stock_width = int(tokens[pos + 2]) + except: + raise ValueError("Error parsing m, stock_length, or stock_width.") + pos += 3 + + # There must be 4*m tokens for the piece types. + if pos + 4 * m > total_tokens: + raise ValueError("Not enough tokens for piece types in one case.") + + piece_types = [] + for i in range(m): + try: + p_length = int(tokens[pos]) + p_width = int(tokens[pos + 1]) + max_count = int(tokens[pos + 2]) + p_value = int(tokens[pos + 3]) + except: + raise ValueError("Error parsing piece type data.") + piece_types.append({ + 'length': p_length, + 'width': p_width, + 'max': max_count, + 'value': p_value + }) + pos += 4 + + case_data = { + "m": m, + "stock_length": stock_length, + "stock_width": stock_width, + "piece_types": piece_types + } + cases.append(case_data) + + return cases + + def eval_func(self, m, stock_length, stock_width, piece_types, total_value, placements): + """ + Evaluates a solution for the Fixed Orientation Guillotine Cutting problem by verifying all constraints. + Raises an error immediately upon any constraint violation. + Parameters: + m (int): Number of piece types. + stock_length (int): Length of the stock rectangle. + stock_width (int): Width of the stock rectangle. + piece_types (list of dict): Each dict has keys: + 'length': int, piece length. + 'width' : int, piece width. + 'max' : int, maximum permitted count for the piece type. + 'value' : int, value of the piece. + total_value (int): The reported total value from the solution. + placements (list): List of placements. Each placement is a tuple or list of 6 integers: + (piece_type_index, x, y, placed_length, placed_width, orientation_flag) + where orientation_flag must be 0 (rotation not allowed). + Returns: + int: The computed total value if the solution is valid. + Constraints verified: + - Each placement is a well-formed 6-tuple of integers. + - The piece type index is within the valid range. + - The orientation flag is 0 (rotation is not allowed). + - The placed dimensions match the expected dimensions. + - Each piece is completely within the stock boundaries. + - No two pieces overlap. + - The count of pieces of each type does not exceed its allowed maximum. + - The reported total_value exactly equals the computed sum of placed piece values. + - The set of placements satisfies the guillotine cutting condition. + """ + + # Helper function: Check guillotine feasibility recursively. + def is_guillotine(rects, bx, by, ex, ey): + """ + Recursively checks if the collection of placed rectangles (rects) in the box + defined by (bx, by) - (ex, ey) is guillotine separable. + A set of placements is considered guillotine feasible if there exists at least one straight cut + (vertical or horizontal) that does not slice through any rectangle, and the property holds recursively + on the resulting subregions. Empty regions or regions exactly matching a placed piece are considered valid. + """ + # If there are no pieces, the region is trivially guillotine separable. + if not rects: + return True + # If a single rectangle exactly covers the region, it is guillotine separable. + if len(rects) == 1: + r = rects[0] + if r[0] == bx and r[1] == by and r[2] == ex and r[3] == ey: + return True + + # Try vertical cuts. + for x in range(bx + 1, ex): + # A vertical cut at x is valid if no rectangle is cut by the line. + if all((r[2] <= x or r[0] >= x) for r in rects): + left_rects = [r for r in rects if r[2] <= x] + right_rects = [r for r in rects if r[0] >= x] + if is_guillotine(left_rects, bx, by, x, ey) and is_guillotine(right_rects, x, by, ex, ey): + return True + + # Try horizontal cuts. + for y in range(by + 1, ey): + if all((r[3] <= y or r[1] >= y) for r in rects): + bottom_rects = [r for r in rects if r[3] <= y] + top_rects = [r for r in rects if r[1] >= y] + if is_guillotine(bottom_rects, bx, by, ex, y) and is_guillotine(top_rects, bx, y, ex, ey): + return True + + return False + + computed_value = 0 + type_counts = [0] * m # Count pieces for each type. + rects = [] # To store placed rectangles as (x1, y1, x2, y2) + + # Process and validate each placement. + for idx, placement in enumerate(placements): + if not (isinstance(placement, (list, tuple)) and len(placement) == 6): + raise ValueError(f"Placement {idx} is not a 6-tuple: {placement}") + + try: + type_idx = int(placement[0]) + x = int(placement[1]) + y = int(placement[2]) + placed_len = int(placement[3]) + placed_wid = int(placement[4]) + orient = int(placement[5]) + except Exception: + raise ValueError(f"Non-integer value in placement {idx}: {placement}") + + # Validate piece type index (using 1-indexing). + if type_idx < 1 or type_idx > m: + raise ValueError(f"Placement {idx} has invalid piece type index {type_idx}") + + # Orientation must be 0 (rotation is not allowed). + if orient != 0: + raise ValueError(f"Placement {idx} has invalid orientation flag {orient}; rotation is not allowed.") + + # Retrieve expected dimensions and value. + piece = piece_types[type_idx - 1] + p_length = piece['length'] + p_width = piece['width'] + max_allowed = piece['max'] + p_value = piece['value'] + + # Since rotation is not allowed, expected dimensions are as given. + expected_length, expected_width = p_length, p_width + + # Check that the placed dimensions match the expected dimensions. + if placed_len != expected_length or placed_wid != expected_width: + raise ValueError( + f"Placement {idx} dimensions ({placed_len}, {placed_wid}) do not match expected ({expected_length}, {expected_width})") + + # Check boundaries: the entire piece must lie within the stock sheet. + if x < 0 or y < 0 or (x + placed_len) > stock_length or (y + placed_wid) > stock_width: + raise ValueError( + f"Placement {idx} with rectangle {(x, y, x + placed_len, y + placed_wid)} is out of stock bounds (0,0) to ({stock_length},{stock_width})") + + # Passed validations: count the piece and add its value. + type_counts[type_idx - 1] += 1 + computed_value += p_value + + # Record rectangle (bottom-left: (x, y), top-right: (x+placed_len, y+placed_wid)) + rects.append((x, y, x + placed_len, y + placed_wid)) + + # Check for overlapping placements. + num_rects = len(rects) + for i in range(num_rects): + for j in range(i + 1, num_rects): + r1 = rects[i] + r2 = rects[j] + dx = min(r1[2], r2[2]) - max(r1[0], r2[0]) + dy = min(r1[3], r2[3]) - max(r1[1], r2[1]) + if dx > 0 and dy > 0: + raise ValueError(f"Placements {i} and {j} overlap.") + + # Check that no piece type is placed more times than its allowed maximum. + for i in range(m): + if type_counts[i] > piece_types[i]['max']: + raise ValueError( + f"Piece type {i + 1} exceeds allowed count: {type_counts[i]} > {piece_types[i]['max']}") + + # Check the guillotine condition on the entire stock sheet. + if not is_guillotine(rects, 0, 0, stock_length, stock_width): + raise ValueError("Guillotine condition violated: the placement layout is not guillotine separable.") + + # Verify that the reported total_value matches the computed total. + if computed_value != total_value: + raise ValueError(f"Reported total value {total_value} does not match computed value {computed_value}.") + + return computed_value + + def norm_score(self, results): + optimal_scores = { + "cgcut1.txt": [244], + "cgcut2.txt": [2892], + "cgcut3.txt": [1860], + } + + normed = {} + for case, (scores, error_message) in results.items(): + if case not in optimal_scores: + continue # Skip if there's no optimal score defined. + optimal_list = optimal_scores[case] + normed_scores = [] + # Compute normalized score for each index. + for idx, score in enumerate(scores): + if isinstance(score, (int, float)): + normed_scores.append(score / optimal_list[idx]) + else: + normed_scores.append(score) + normed[case] = (normed_scores, error_message) + + return normed + + def get_dev(self): + dev = {'cgcut1.txt': [], 'cgcut2.txt': [], 'cgcut3.txt': []} + + return dev + + +# Task configuration for benchmark task +ENTRY_NAME = 'solve' +FUNCTION_SIGNATURE = 'def solve(...):' +IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' +TASK_DESCRIPTION = '("The problem involves optimizing the guillotine feasible placement of a set of rectangular pieces "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The problem involves optimizing the guillotine feasible placement of a set of rectangular pieces "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(m: int, stock_length: int, stock_width: int, piece_types: list) -> dict:\n """\n Solves the Fixed Orientation Guillotine Cutting problem.\n Problem Description:\n Given a rectangular stock sheet (with specified length and width) and a set of piece types\n (each defined by a length, width, an upper bound on the number of times it may appear, and a value),\n the goal is to determine a placement for the pieces such that:\n - Each placed piece lies entirely within the stock sheet.\n - Pieces do not overlap.\n - The number of pieces placed for any type does not exceed its allowed maximum.\n - The orientation of the pieces is fixed (i.e. no rotation is allowed).\n - The total value reported equals the sum of the values of the placed pieces.\n Input kwargs (for one case):\n - m: integer, the number of piece types.\n - stock_length: integer, the length of the stock sheet.\n - stock_width: integer, the width of the stock sheet.\n - piece_types: list of dictionaries. Each dictionary has the keys:\n \'length\' : int, the length of the piece.\n \'width\' : int, the width of the piece.\n \'max\' : int, maximum number of pieces allowed.\n \'value\' : int, value of the piece.\n Returns:\n A dictionary with the following keys:\n - total_value: int, the computed total value (must equal the sum of the piece values in placements).\n - placements: list of placements, where each placement is a tuple of 6 integers:\n (piece_type_index, x, y, placed_length, placed_width, orientation_flag)\n The orientation_flag is always 0 since rotation is not allowed.\n """\n # Your optimization/placement algorithm should go here.\n # For now, this is a placeholder that meets the output format requirements.\n\n # Example placeholder output (no actual pieces placed):\n return {"total_value": 0, "placements": []}' +EVAL_CLASS_NAME = 'CGCEvaluationCB' +EVAL_KWARGS = {'timeout_seconds': 20} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_constrained_guillotine_cutting/paras.yaml b/examples/benchmark_tasks/optimization_constrained_guillotine_cutting/paras.yaml new file mode 100644 index 00000000..b4575223 --- /dev/null +++ b/examples/benchmark_tasks/optimization_constrained_guillotine_cutting/paras.yaml @@ -0,0 +1,2 @@ +name: CGCEvaluationCB +timeout_seconds: 20 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_constrained_non_guillotine_cutting/__init__.py b/examples/benchmark_tasks/optimization_constrained_non_guillotine_cutting/__init__.py new file mode 100644 index 00000000..e8c8341c --- /dev/null +++ b/examples/benchmark_tasks/optimization_constrained_non_guillotine_cutting/__init__.py @@ -0,0 +1,359 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_constrained_non_guillotine_cutting +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# References: +# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language +# model agents in algorithm search for combinatorial optimization. +# arXiv preprint arXiv:2504.04310 (2025). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any +import numpy as np +from llm4ad_loader import Evaluation +from llm4ad_loader import load_subdir_as_text +# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader +# from llm4ad.task.optimization.co_bench.constrained_non_guillotine_cutting_co_bench.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(stock_length: int, stock_width: int, pieces: list) -> dict:\n """\n Solves the constrained non-guillotine cutting problem.\n Input kwargs:\n - stock_length (int): Length of the stock rectangle.\n - stock_width (int): Width of the stock rectangle.\n - pieces (list of dict): List of pieces, where each dict has:\n \'length\' (int), \'width\' (int),\n \'min\' (int): minimum number required,\n \'max\' (int): maximum allowed,\n \'value\' (int): value of the piece.\n Evaluation Metric:\n The solution is scored as the sum of the values of all placed pieces,\n provided that every placement is valid (i.e., pieces lie within bounds,\n do not overlap, and the count for each type meets the specified [min, max] range).\n If any constraint is violated, the solution receives no score.\n Returns:\n A dictionary with one key:\n \'placements\': a list of placements, where each placement is a 4-tuple:\n (piece_type, x, y, r)\n - piece_type: 1-indexed index of the piece type.\n - x, y: integer coordinates for the placement (bottom-left corner).\n - r: rotation flag (0 for no rotation, 1 for 90° rotation).\n """\n # Placeholder implementation.\n # (A valid implementation would generate placements meeting all constraints.)\n return {\'placements\': []}' +task_description = '("The constrained non-guillotine cutting problem involves optimally arranging rectangular pieces "' + + +__all__ = ['CNCEvaluationCB'] + + +class CNCEvaluationCB(Evaluation): + + def __init__(self, + timeout_seconds=50, + **kwargs): + + """ + Args: + None + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + # Load datasets from Hugging Face + dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Constrained non-guillotine cutting") + self._datasets = {} + for filename in dataset: + # Join all text rows into a single string + text_content = '\n'.join([row['text'] for row in dataset[filename]]) + self._datasets[filename] = text_content + + def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: + return self.evaluate(callable_func) + + def evaluate(self, eva: callable) -> float | None: + ins_cases = [] + for case_id, ins in enumerate(self._datasets.values()): + ins_cases.append(self.load_data(ins)) + + fitness_list = [] + try: + for i in ins_cases: + for j in i: + result = eva(j['stock_length'], j['stock_width'], j['pieces']) + fitness = self.eval_func(j['stock_length'], j['stock_width'], j['pieces'], result['placements']) + fitness_list.append(fitness) + + return np.mean(fitness_list) # itself is a maximize problem + + except ValueError as e: + print(e) + return None + + def load_data(self, input_string): + """ + Loads input data from a text file and returns a list of test case dictionaries. + The input format: + - First line: integer T (number of test cases) + - For each test case: + * A line with integer m (number of pieces) + * A line with two integers: stock_length and stock_width + * m subsequent lines, each with 5 integers: + length, width, min_required, max_allowed, value + Returns: + List[Dict]: A list where each element is a dictionary with keys: + 'stock_length': int, + 'stock_width': int, + 'pieces': list of dicts, each dict has: + 'length': int, + 'width': int, + 'min': int, + 'max': int, + 'value': int + """ + test_cases = [] + all_lines = [line.strip() for line in input_string.split('\n')] + + idx = 0 + T = int(all_lines[idx]) + idx += 1 + for _ in range(T): + if idx >= len(all_lines): + raise ValueError("Insufficient data for the expected number of test cases.") + m = int(all_lines[idx]) + idx += 1 + + stock_dims = list(map(int, all_lines[idx].split())) + if len(stock_dims) != 2: + raise ValueError("Invalid stock dimensions format.") + stock_length, stock_width = stock_dims + idx += 1 + + pieces = [] + for _ in range(m): + piece_data = list(map(int, all_lines[idx].split())) + if len(piece_data) != 5: + raise ValueError("Invalid piece data format.") + pieces.append({ + 'length': piece_data[0], + 'width': piece_data[1], + 'min': piece_data[2], + 'max': piece_data[3], + 'value': piece_data[4] + }) + idx += 1 + + test_cases.append({ + 'stock_length': stock_length, + 'stock_width': stock_width, + 'pieces': pieces + }) + + return test_cases + + def eval_func(self, stock_length, stock_width, pieces, placements): + """ + Evaluates the solution for a single test case. + Parameters: + - stock_length (int): Length of the stock rectangle. + - stock_width (int): Width of the stock rectangle. + - pieces (list of dict): List of piece definitions. + - placements (list): List of placements; each placement is a 4-tuple: + (piece_type, x, y, r) + Returns: + float: The overall score, computed as the sum of values of all placed pieces, + if the solution is feasible. + Raises: + ValueError: If any constraint is violated. + """ + counts = [0] * len(pieces) + rects = [] # Each rectangle is represented as (x1, y1, x2, y2) + + for idx, placement in enumerate(placements): + if not (isinstance(placement, (list, tuple)) and len(placement) == 4): + raise ValueError(f"Placement at index {idx} is invalid; must be a 4-tuple.") + + piece_type, x, y, r = placement + + # Ensure that placement values are integers. + if not all(isinstance(val, int) for val in (piece_type, x, y, r)): + raise ValueError(f"All values in placement at index {idx} must be integers.") + + # Check piece_type validity. + if piece_type < 1 or piece_type > len(pieces): + raise ValueError(f"Placement at index {idx} has an invalid piece_type {piece_type}.") + + piece = pieces[piece_type - 1] + + # Determine dimensions based on rotation flag. + if r == 0: + p_len = piece['length'] + p_wid = piece['width'] + elif r == 1: + p_len = piece['width'] + p_wid = piece['length'] + else: + raise ValueError(f"Placement at index {idx} has an invalid rotation flag {r}.") + + # Check that the piece is fully within the stock boundaries. + if x < 0 or y < 0 or (x + p_len) > stock_length or (y + p_wid) > stock_width: + raise ValueError(f"Placement at index {idx} is out of stock boundaries.") + + # Record the rectangle: (x1, y1, x2, y2) + rects.append((x, y, x + p_len, y + p_wid)) + counts[piece_type - 1] += 1 + + # Check for overlapping placements. + n = len(rects) + for i in range(n): + for j in range(i + 1, n): + a = rects[i] + b = rects[j] + # Two rectangles do not overlap if one is completely to the left, + # right, above, or below the other. + if not (a[2] <= b[0] or b[2] <= a[0] or a[3] <= b[1] or b[3] <= a[1]): + raise ValueError(f"Placements at indices {i} and {j} overlap.") + + # Check that the count of placements for each piece type meets its constraints. + for i, piece in enumerate(pieces): + if counts[i] < piece['min'] or counts[i] > piece['max']: + raise ValueError(f"Piece type {i + 1} count {counts[i]} does not meet constraints " + f"[min: {piece['min']}, max: {piece['max']}].") + + # Compute the total score. + total_score = 0 + for placement in placements: + piece_type, x, y, r = placement + piece = pieces[piece_type - 1] + total_score += piece['value'] + + return total_score + + def norm_score(self, results): + optimal_scores = { + "ngcutap.txt": [164, 230, 247, 268, 358, 289, 430, 834, 924, 1452, 1688, 1865, 1178, 1270, 2726, 1860, + 27718, + 22502, 24019, 32893, 27923], + "ngcutcon.txt": [164, 230, 247, 268, 358, 289, 430, 834, 924, 1452, 1688, 1865, 1178, 1270, 2726, 1860, + 27718, + 22502, 24019, 32893, 27923], + "ngcutfs1.txt": [30000] * 210, + "ngcutfs2.txt": [30000] * 210, + "ngcutfs3.txt": [30000] * 210, + } + + normed = {} + for case, (scores, error_message) in results.items(): + if case not in optimal_scores: + continue # Skip if there's no optimal score defined. + optimal_list = optimal_scores[case] + normed_scores = [] + # Compute normalized score for each index. + for idx, score in enumerate(scores): + if isinstance(score, (int, float)): + normed_scores.append(score / optimal_list[idx]) + else: + normed_scores.append(score) + normed[case] = (normed_scores, error_message) + + return normed + + def get_dev(self): + dev = {'ngcutap.txt': [19, 4, 12, 2, 8], 'ngcutcon.txt': [0, 8, 19, 7, 17], + 'ngcutfs1.txt': [51, 66, 120, 62, 8, 185, 197, 0, 170, 119, 103, 161, 173, 26, 153, 96, 13, 136, 5, 44, + 150, + 82, 86, 14, 71, 207, 135, 75, 97, 139, 118, 46, 108, 93, 99, 140, 204, 147, 16, 183, 27, + 191, 176, 49, 127, 78, 10, 113, 110, 143, 199, 142, 167, 22, 50, 30, 180, 188, 154, 123, + 63, + 72, 203, 61, 28, 186, 159, 134, 19, 52, 39, 79, 98, 55, 56, 137, 148, 155, 163, 124, + 174, + 33, 1, 125, 77, 58, 151, 76, 116, 206, 156, 184, 12, 32, 53, 92, 164, 131, 175, 187, + 157, + 45, 201, 189, 54], + 'ngcutfs2.txt': [123, 108, 114, 43, 151, 116, 197, 23, 45, 166, 8, 126, 147, 87, 154, 12, 172, 103, 133, + 143, + 122, 68, 24, 97, 144, 179, 195, 52, 67, 1, 14, 167, 33, 65, 196, 46, 202, 206, 54, 63, + 160, + 159, 176, 79, 129, 61, 9, 164, 72, 115, 21, 111, 96, 66, 198, 104, 201, 92, 105, 125, + 91, + 119, 124, 94, 84, 20, 113, 203, 177, 15, 135, 120, 49, 194, 192, 98, 88, 158, 36, 171, + 29, + 199, 109, 185, 148, 130, 204, 70, 174, 207, 53, 142, 2, 89, 35, 51, 117, 145, 73, 10, + 81, + 83, 139, 4, 128], + 'ngcutfs3.txt': [193, 73, 128, 170, 197, 26, 85, 58, 105, 100, 36, 93, 32, 72, 110, 80, 16, 106, 160, 11, + 129, 3, 89, 66, 87, 61, 27, 47, 171, 52, 176, 24, 203, 205, 186, 161, 135, 114, 200, 90, + 124, 198, 141, 70, 14, 183, 81, 8, 86, 178, 54, 157, 25, 208, 38, 134, 39, 88, 111, 23, + 190, + 109, 152, 43, 98, 99, 163, 148, 201, 44, 192, 130, 30, 138, 33, 9, 209, 194, 4, 15, 37, + 169, + 188, 112, 123, 115, 173, 181, 108, 97, 133, 96, 53, 13, 48, 158, 71, 19, 149, 64, 74, + 103, + 102, 206, 143]} + + return dev + + +# Task configuration for benchmark task +ENTRY_NAME = 'solve' +FUNCTION_SIGNATURE = 'def solve(...):' +IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' +TASK_DESCRIPTION = '("The constrained non-guillotine cutting problem involves optimally arranging rectangular pieces "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The constrained non-guillotine cutting problem involves optimally arranging rectangular pieces "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(stock_length: int, stock_width: int, pieces: list) -> dict:\n """\n Solves the constrained non-guillotine cutting problem.\n Input kwargs:\n - stock_length (int): Length of the stock rectangle.\n - stock_width (int): Width of the stock rectangle.\n - pieces (list of dict): List of pieces, where each dict has:\n \'length\' (int), \'width\' (int),\n \'min\' (int): minimum number required,\n \'max\' (int): maximum allowed,\n \'value\' (int): value of the piece.\n Evaluation Metric:\n The solution is scored as the sum of the values of all placed pieces,\n provided that every placement is valid (i.e., pieces lie within bounds,\n do not overlap, and the count for each type meets the specified [min, max] range).\n If any constraint is violated, the solution receives no score.\n Returns:\n A dictionary with one key:\n \'placements\': a list of placements, where each placement is a 4-tuple:\n (piece_type, x, y, r)\n - piece_type: 1-indexed index of the piece type.\n - x, y: integer coordinates for the placement (bottom-left corner).\n - r: rotation flag (0 for no rotation, 1 for 90° rotation).\n """\n # Placeholder implementation.\n # (A valid implementation would generate placements meeting all constraints.)\n return {\'placements\': []}' +EVAL_CLASS_NAME = 'CNCEvaluationCB' +EVAL_KWARGS = {'timeout_seconds': 20} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_constrained_non_guillotine_cutting/paras.yaml b/examples/benchmark_tasks/optimization_constrained_non_guillotine_cutting/paras.yaml new file mode 100644 index 00000000..58248382 --- /dev/null +++ b/examples/benchmark_tasks/optimization_constrained_non_guillotine_cutting/paras.yaml @@ -0,0 +1,2 @@ +name: CNCEvaluationCB +timeout_seconds: 20 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_container_loading/__init__.py b/examples/benchmark_tasks/optimization_container_loading/__init__.py new file mode 100644 index 00000000..982c2b11 --- /dev/null +++ b/examples/benchmark_tasks/optimization_container_loading/__init__.py @@ -0,0 +1,387 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_container_loading +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# References: +# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language +# model agents in algorithm search for combinatorial optimization. +# arXiv preprint arXiv:2504.04310 (2025). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any +import numpy as np +from llm4ad_loader import Evaluation +from llm4ad_loader import load_subdir_as_text +# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader +# from llm4ad.task.optimization.co_bench.container_loading_co_bench.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(problem_index: int, container: tuple, box_types: dict) -> dict:\n """\n Solves a container loading problem.\n Input kwargs:\n - problem_index: an integer identifier for the test case.\n - container: a tuple of three integers (container_length, container_width, container_height).\n - box_types: a dictionary mapping each box type (integer) to a dict with:\n \'dims\': a list of three integers [d1, d2, d3],\n \'flags\': a list of three binary integers [f1, f2, f3] indicating if that dimension can be vertical,\n \'count\': an integer number of available boxes of that type.\n Evaluation Metric:\n The solution is evaluated by computing the volume utilization ratio, which is the sum of the volumes\n of all placed boxes divided by the container volume. Placements must be valid (i.e. respect orientation,\n remain within the container, and not overlap). If any placement is invalid, the score is 0.0.\n Return:\n A dictionary with key \'placements\', whose value is a list of placement dictionaries.\n Each placement dictionary must contain 7 integers with the following keys/values:\n box_type, container_id, x, y, z, v, hswap\n where \'v\' is the index (0, 1, or 2) for the vertical dimension and \'hswap\' is a binary flag (0 or 1)\n indicating whether the horizontal dimensions are swapped.\n """\n # Placeholder implementation.\n return {\'placements\': []}' +task_description = '("Solves a container loading problem: Given a 3D container of specified dimensions and multiple "' + + +__all__ = ['CLEvaluationCB'] + + +class CLEvaluationCB(Evaluation): + + def __init__(self, + timeout_seconds=50, + **kwargs): + + """ + Args: + None + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + # Load datasets from Hugging Face + dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Container loading") + self._datasets = {} + for filename in dataset: + # Join all text rows into a single string + text_content = '\n'.join([row['text'] for row in dataset[filename]]) + self._datasets[filename] = text_content + + def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: + return self.evaluate(callable_func) + + def evaluate(self, eva: callable) -> float | None: + ins_cases = [] + for case_id, ins in enumerate(self._datasets.values()): + ins_cases.append(self.load_data(ins)) + + fitness_list = [] + try: + for i in ins_cases: + for j in i: + result = eva(j['problem_index'], j['container'], j['box_types']) + fitness = self.eval_func(j['problem_index'], j['container'], j['box_types'], result['placements']) + fitness_list.append(fitness) + + return np.mean(fitness_list) # itself is a maximize problem + + except ValueError as e: + print(e) + return None + + def load_data(self, input_string): + """ + Loads container loading problem data from string content. + The input format: + 1. The first line is an integer P, the number of test problems. + 2. For each test problem: + a. A header line with two integers: problem_index and a seed (the seed may be ignored). + (Note: Some files may only provide one number; the seed is optional.) + b. A line with three integers: container_length, container_width, container_height. + c. A line with a single integer n: the number of box types. + d. Then n lines follow, each with 7 or 8 integers in this order: + box_type, d1, f1, d2, f2, d3, f3 [, count] + If only 7 numbers are provided, a default count of 1 is assumed. + Returns: + A list of dictionaries, one per test case. Each dictionary has the following keys: + - 'problem_index': int, + - 'container': tuple (container_length, container_width, container_height), + - 'box_types': dict mapping each box_type to a dict with keys: + 'dims': [d1, d2, d3], + 'flags': [f1, f2, f3], + 'count': count + """ + test_cases = [] + lines = [line.strip() for line in input_string.split('\n') if line.strip()] + if not lines: + raise ValueError("Empty input file") + + try: + P = int(lines[0]) + except Exception as e: + raise ValueError("First line must be an integer representing the number of test cases.") from e + idx = 1 + for case_num in range(P): + # Read header: expecting at least one number (problem_index); seed is optional. + header_parts = lines[idx].split() + if len(header_parts) < 1: + raise ValueError(f"Test case {case_num + 1}: Header line missing problem index.") + problem_index = int(header_parts[0]) + idx += 1 + + # Container dimensions: length, width, height. + cont_parts = lines[idx].split() + if len(cont_parts) < 3: + raise ValueError(f"Test case {problem_index}: Container dimensions missing or incomplete.") + container = tuple(map(int, cont_parts[:3])) + idx += 1 + + # Number of box types. + if idx >= len(lines): + raise ValueError(f"Test case {problem_index}: Expected number of box types but reached end of file.") + try: + n = int(lines[idx]) + except Exception as e: + raise ValueError(f"Test case {problem_index}: Box types count is not an integer.") from e + idx += 1 + + box_types = {} + for bt_index in range(n): + if idx >= len(lines): + raise ValueError( + f"Test case {problem_index}: Missing box type specification at index {bt_index + 1}.") + parts = lines[idx].split() + if len(parts) < 7: + raise ValueError( + f"Test case {problem_index}: Box type specification incomplete on line: '{lines[idx]}'") + try: + bt = int(parts[0]) + d1 = int(parts[1]) + f1 = int(parts[2]) + d2 = int(parts[3]) + f2 = int(parts[4]) + d3 = int(parts[5]) + f3 = int(parts[6]) + # If a count is provided, use it; otherwise default to 1. + count = int(parts[7]) if len(parts) >= 8 else 1 + except Exception as e: + raise ValueError( + f"Test case {problem_index}: Error parsing box type specification: '{lines[idx]}'") from e + dims = [d1, d2, d3] + flags = [f1, f2, f3] + box_types[bt] = {'dims': dims, 'flags': flags, 'count': count} + idx += 1 + + test_cases.append({ + 'problem_index': problem_index, + 'container': container, + 'box_types': box_types + }) + return test_cases + + def eval_func(self, problem_index, container, box_types, placements, **kwargs): + """ + Evaluates a container loading solution for a single test case. + Parameters: + - problem_index: the integer identifier of the test case. + - container: a tuple (container_length, container_width, container_height). + - box_types: a dictionary mapping box types to their specifications. + - placements: a list of placement dictionaries; each must include: + 'box_type', 'container_id', 'x', 'y', 'z', 'v', 'hswap' + Returns: + A scalar float value representing the volume utilization ratio if the solution is valid. + If any placement is invalid (e.g., incorrect orientation, out-of-bound placement, + overlapping boxes, or exceeding available count), the function returns 0.0. + Evaluation Details: + - For each placement, verifies that the chosen vertical dimension (v) is allowed. + - Computes the oriented dimensions: + horizontal dimensions are the two not chosen as vertical (swapped if hswap == 1), + vertical dimension is dims[v]. + - Checks that each box is entirely within the container. + - Checks that boxes do not overlap (touching is allowed). + - Verifies that the number of placed boxes for each type does not exceed the available count. + - The score is computed as (total placed volume) / (container volume). + """ + + def boxes_overlap(pos1, dims1, pos2, dims2): + x1, y1, z1 = pos1 + w1, d1, h1 = dims1 + x2, y2, z2 = pos2 + w2, d2, h2 = dims2 + if x1 + w1 <= x2 or x2 + w2 <= x1: + return False + if y1 + d1 <= y2 or y2 + d2 <= y1: + return False + if z1 + h1 <= z2 or z2 + h2 <= z1: + return False + return True + + cont_len, cont_wid, cont_ht = container + container_volume = cont_len * cont_wid * cont_ht + total_placed_volume = 0 + used_counts = {} + placements_by_container = {} + + # Group placements by container_id + for pmt in placements: + cid = pmt['container_id'] + if cid not in placements_by_container: + placements_by_container[cid] = [] + placements_by_container[cid].append(pmt) + + # Validate each placement + for cid, plist in placements_by_container.items(): + for pmt in plist: + bt = pmt['box_type'] + if bt not in box_types: + return 0.0 # Unknown box type + info = box_types[bt] + dims = info['dims'] + flags = info['flags'] + v = pmt['v'] + if v not in [0, 1, 2]: + return 0.0 + if flags[v] != 1: + return 0.0 # Vertical orientation not allowed + + # Determine horizontal dimensions indices + horz_idx = [i for i in [0, 1, 2] if i != v] + h1 = dims[horz_idx[0]] + h2 = dims[horz_idx[1]] + if pmt['hswap'] == 1: + h1, h2 = h2, h1 + vert = dims[v] + + # Check that placement coordinates are nonnegative and within container bounds + if pmt['x'] < 0 or pmt['y'] < 0 or pmt['z'] < 0: + return 0.0 + if (pmt['x'] + h1 > cont_len or + pmt['y'] + h2 > cont_wid or + pmt['z'] + vert > cont_ht): + return 0.0 + + # Save oriented dimensions and position for overlap checking + pmt['oriented_dims'] = (h1, h2, vert) + pmt['oriented_pos'] = (pmt['x'], pmt['y'], pmt['z']) + total_placed_volume += h1 * h2 * vert + used_counts[bt] = used_counts.get(bt, 0) + 1 + + # Check for overlaps among placements in the same container + for i in range(len(plist)): + for j in range(i + 1, len(plist)): + if boxes_overlap(plist[i]['oriented_pos'], plist[i]['oriented_dims'], + plist[j]['oriented_pos'], plist[j]['oriented_dims']): + return 0.0 + + # Verify that box usage does not exceed available counts + for bt, cnt in used_counts.items(): + if cnt > box_types[bt]['count']: + return 0.0 + + utilization = total_placed_volume / container_volume if container_volume > 0 else 0.0 + return utilization + + def get_dev(self): + dev = { + 'thpack1.txt': [89, 15, 12, 53, 78, 32, 56, 30, 6, 28, 23, 62, 52, 37, 69, 33, 35, 24, 17, 4, 79, 72, 2, 92, + 54, + 90, 91, 1, 57, 59, 94, 65, 25, 14, 83, 47, 46, 95, 48, 42, 88, 68, 85, 55, 40, 64, 74, 70, + 3, + 7], + 'thpack2.txt': [6, 9, 72, 24, 69, 2, 81, 33, 53, 39, 64, 71, 15, 99, 61, 36, 52, 8, 19, 7, 4, 1, 86, 21, 31, + 5, + 20, 57, 0, 79, 55, 35, 23, 25, 89, 44, 91, 62, 82, 12, 68, 75, 73, 27, 80, 56, 30, 47, 70, + 16], + 'thpack3.txt': [17, 36, 89, 50, 19, 11, 97, 9, 75, 62, 10, 46, 42, 23, 39, 18, 99, 1, 5, 20, 70, 60, 31, 3, + 43, + 33, 51, 92, 95, 40, 84, 63, 13, 78, 58, 25, 4, 38, 24, 15, 88, 82, 7, 28, 8, 77, 71, 80, 76, + 53], + 'thpack4.txt': [7, 89, 96, 75, 2, 37, 6, 82, 18, 14, 90, 36, 32, 40, 10, 25, 56, 72, 87, 98, 45, 21, 23, 55, + 4, + 79, 15, 65, 63, 73, 5, 81, 76, 69, 20, 67, 85, 60, 50, 47, 84, 16, 35, 1, 22, 43, 91, 48, + 88, + 41], + 'thpack5.txt': [79, 36, 97, 5, 62, 10, 49, 2, 23, 52, 51, 29, 96, 20, 64, 41, 38, 35, 94, 95, 12, 73, 34, + 11, + 93, 69, 58, 61, 87, 80, 71, 4, 88, 57, 46, 59, 33, 50, 13, 44, 0, 85, 55, 21, 77, 82, 63, + 67, + 31, 26], + 'thpack6.txt': [21, 31, 83, 22, 10, 19, 5, 0, 43, 82, 66, 36, 49, 38, 33, 58, 70, 15, 97, 80, 9, 30, 42, 88, + 69, + 61, 40, 60, 14, 95, 91, 39, 98, 16, 73, 90, 51, 18, 71, 26, 47, 54, 57, 87, 17, 53, 89, 92, + 65, + 81], + 'thpack7.txt': [97, 37, 73, 88, 50, 79, 12, 60, 99, 34, 4, 19, 78, 9, 7, 93, 31, 74, 90, 38, 33, 21, 24, 22, + 52, + 0, 43, 67, 13, 3, 59, 42, 39, 47, 36, 40, 45, 10, 5, 56, 57, 18, 51, 61, 92, 20, 69, 81, 35, + 98], + 'thpack8.txt': [11, 4, 12, 14, 10, 2, 7], + 'thpack9.txt': [14, 32, 25, 30, 40, 8, 37, 15, 31, 9, 17, 21, 22, 16, 24, 33, 35, 44, 42, 0, 1, 45, 11]} + + return dev + + +# Task configuration for benchmark task +ENTRY_NAME = 'solve' +FUNCTION_SIGNATURE = 'def solve(...):' +IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' +TASK_DESCRIPTION = '("Solves a container loading problem: Given a 3D container of specified dimensions and multiple "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("Solves a container loading problem: Given a 3D container of specified dimensions and multiple "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(problem_index: int, container: tuple, box_types: dict) -> dict:\n """\n Solves a container loading problem.\n Input kwargs:\n - problem_index: an integer identifier for the test case.\n - container: a tuple of three integers (container_length, container_width, container_height).\n - box_types: a dictionary mapping each box type (integer) to a dict with:\n \'dims\': a list of three integers [d1, d2, d3],\n \'flags\': a list of three binary integers [f1, f2, f3] indicating if that dimension can be vertical,\n \'count\': an integer number of available boxes of that type.\n Evaluation Metric:\n The solution is evaluated by computing the volume utilization ratio, which is the sum of the volumes\n of all placed boxes divided by the container volume. Placements must be valid (i.e. respect orientation,\n remain within the container, and not overlap). If any placement is invalid, the score is 0.0.\n Return:\n A dictionary with key \'placements\', whose value is a list of placement dictionaries.\n Each placement dictionary must contain 7 integers with the following keys/values:\n box_type, container_id, x, y, z, v, hswap\n where \'v\' is the index (0, 1, or 2) for the vertical dimension and \'hswap\' is a binary flag (0 or 1)\n indicating whether the horizontal dimensions are swapped.\n """\n # Placeholder implementation.\n return {\'placements\': []}' +EVAL_CLASS_NAME = 'CLEvaluationCB' +EVAL_KWARGS = {'timeout_seconds': 20} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_container_loading/paras.yaml b/examples/benchmark_tasks/optimization_container_loading/paras.yaml new file mode 100644 index 00000000..6b88d118 --- /dev/null +++ b/examples/benchmark_tasks/optimization_container_loading/paras.yaml @@ -0,0 +1,2 @@ +name: CLEvaluationCB +timeout_seconds: 20 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_container_loading_with_weight_restrictions/__init__.py b/examples/benchmark_tasks/optimization_container_loading_with_weight_restrictions/__init__.py new file mode 100644 index 00000000..59c13190 --- /dev/null +++ b/examples/benchmark_tasks/optimization_container_loading_with_weight_restrictions/__init__.py @@ -0,0 +1,456 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_container_loading_with_weight_restrictions +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# References: +# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language +# model agents in algorithm search for combinatorial optimization. +# arXiv preprint arXiv:2504.04310 (2025). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any +import numpy as np +from llm4ad_loader import Evaluation +from llm4ad_loader import load_subdir_as_text +# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader +# from llm4ad.task.optimization.co_bench.container_loading_with_weight_restrictions_co_bench.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(container: tuple, n: int, cargo_vol: float, box_types: list) -> dict:\n """\n Solves the Container Loading with Weight Restrictions problem.\n Input kwargs (for one test case):\n - container (tuple of int): (L, W, H) representing the container dimensions in cm.\n - n (int): the number of box types.\n - cargo_vol (float): the total cargo volume in m³ (provided for consistency).\n - box_types (list of dict): one per box type. Each dictionary has the keys:\n \'length\' (int), \'length_flag\' (int),\n \'width\' (int), \'width_flag\' (int),\n \'height\' (int), \'height_flag\' (int),\n \'count\' (int), \'weight\' (float),\n \'lb1\' (float), \'lb2\' (float), \'lb3\' (float).\n The problem is to select and place boxes (each possibly in one of three allowed orientations)\n inside the container so as to maximize the ratio of the total volume of placed boxes (each based on its original dimensions)\n to the container’s volume, while obeying placement, support, and load–bearing constraints.\n Evaluation metric:\n The score is the container volume utilization (i.e. total placed boxes volume divided by container volume)\n if the solution is valid according to all constraints; otherwise the score is 0.0.\n Placeholder implementation: No boxes are placed.\n Returns a dictionary with keys:\n - \'instance\': instance number (int),\n - \'util\': achieved utilization (float),\n - \'m\': number of placements (int),\n - \'placements\': a list of placements; each placement is a dict with keys:\n \'box_type\' (int, 1-indexed), \'orientation\' (int: 1, 2, or 3),\n \'x\', \'y\', \'z\' (floats for the lower–left–front corner in cm).\n """\n # Placeholder: return an empty solution.\n return {\n \'instance\': 1,\n \'util\': 0.0,\n \'m\': 0,\n \'placements\': []\n }' +task_description = '("The Container Loading with Weight Restrictions problem aims to maximize the utilization of a "' + + +__all__ = ['CLWREvaluationCB'] +TOL = 1e-6 + + +class CLWREvaluationCB(Evaluation): + + def __init__(self, + timeout_seconds=50, + **kwargs): + + """ + Args: + None + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + # Load datasets from Hugging Face + dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Container loading with weight restrictions") + self._datasets = {} + for filename in dataset: + # Join all text rows into a single string + text_content = '\n'.join([row['text'] for row in dataset[filename]]) + self._datasets[filename] = text_content + + def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: + return self.evaluate(callable_func) + + def evaluate(self, eva: callable) -> float | None: + ins_cases = [] + for case_id, ins in enumerate(self._datasets.values()): + ins_cases.append(self.load_data(ins)) + + fitness_list = [] + try: + for i in ins_cases: + for j in i: + result = eva(j['container'], j['n'], j['cargo_vol'], j['box_types']) + fitness = self.eval_func(j['container'], j['n'], j['cargo_vol'], j['box_types'], result['instance'], result['util'], result['m'], result['placements']) + fitness_list.append(fitness) + + return np.mean(fitness_list) # itself is a maximize problem + + except ValueError as e: + print(e) + return None + + def load_data(self, input_string): + """ + Loads the input data file for the Container Loading problem. + The input file may contain one or more test cases. For each test case: + - The first non-empty line contains three floats: container length, width, height (in cm). + - The next non-empty line contains an integer n (number of box types) and a float (total cargo volume in m³). + - The following n non-empty lines each contain 11 whitespace-separated values: + Box length, length_flag, Box width, width_flag, Box height, height_flag, + count, weight, lb1, lb2, lb3. + Returns: + A list where each element is a dictionary containing the input data for one test case with keys: + 'container', 'n', 'cargo_vol', and 'box_types'. + """ + all_lines = [line.strip() for line in input_string.split('\n')] + + cases = [] + i = 0 + while i < len(all_lines): + # Read container dimensions. + parts = all_lines[i].split() + if len(parts) < 3: + raise ValueError("Invalid container dimensions line.") + container = (int(parts[0]), int(parts[1]), int(parts[2])) + i += 1 + + # Read header: number of box types and cargo volume. + parts = all_lines[i].split() + if len(parts) < 2: + raise ValueError("Invalid test-case header line.") + n = int(parts[0]) + cargo_vol = float(parts[1]) + i += 1 + + # Read details for each box type. + box_types = [] + for _ in range(n): + parts = all_lines[i].split() + if len(parts) != 11: + raise ValueError("Invalid box type line: " + all_lines[i]) + box_type = { + 'length': int(parts[0]), + 'length_flag': int(parts[1]), + 'width': int(parts[2]), + 'width_flag': int(parts[3]), + 'height': int(parts[4]), + 'height_flag': int(parts[5]), + 'count': int(parts[6]), + 'weight': float(parts[7]), + 'lb1': float(parts[8]), + 'lb2': float(parts[9]), + 'lb3': float(parts[10]) + } + box_types.append(box_type) + i += 1 + + cases.append({ + 'container': container, + 'n': n, + 'cargo_vol': cargo_vol, + 'box_types': box_types + }) + + return cases + + # Helper functions used by eval_func + + def get_box_dimensions(self, box, orientation): + """ + Given a box type (dictionary) and an orientation (1, 2, or 3), + returns a tuple (dx, dy, dz, lb, volume) where: + - (dx, dy) are the horizontal dimensions, + - dz is the vertical dimension, + - lb is the load-bearing ability for that orientation, + - volume is the original box volume. + Orientation conventions: + 1: Box length is vertical (dz = length; horizontal: width, height). + 2: Box width is vertical (dz = width; horizontal: length, height). + 3: Box height is vertical (dz = height; horizontal: length, width). + """ + if orientation == 1: + if box['length_flag'] != 1: + raise ValueError("Orientation 1 not allowed for this box type.") + dz = box['length'] + dx = box['width'] + dy = box['height'] + lb = box['lb1'] + elif orientation == 2: + if box['width_flag'] != 1: + raise ValueError("Orientation 2 not allowed for this box type.") + dz = box['width'] + dx = box['length'] + dy = box['height'] + lb = box['lb2'] + elif orientation == 3: + if box['height_flag'] != 1: + raise ValueError("Orientation 3 not allowed for this box type.") + dz = box['height'] + dx = box['length'] + dy = box['width'] + lb = box['lb3'] + else: + raise ValueError("Invalid orientation value.") + volume = box['length'] * box['width'] * box['height'] + return dx, dy, dz, lb, volume + + def boxes_overlap(self, b1, b2): + """ + Determines if two boxes overlap in space. + Each box is represented as a dict with keys: + x, y, z, dx, dy, dz. + Returns True if the boxes overlap (i.e. intersect in all three dimensions, not just touch). + """ + if b1['x'] + b1['dx'] - TOL <= b2['x'] or b2['x'] + b2['dx'] - TOL <= b1['x']: + return False + if b1['y'] + b1['dy'] - TOL <= b2['y'] or b2['y'] + b2['dy'] - TOL <= b1['y']: + return False + if b1['z'] + b1['dz'] - TOL <= b2['z'] or b2['z'] + b2['dz'] - TOL <= b1['z']: + return False + return True + + def eval_func(self, container, n, cargo_vol, box_types, instance, util, m, placements): + """ + Hard evaluation for a container–loading solution. + This function checks all constraints and raises an error immediately when any + constraint is violated. The constraints include: + - Validity of the box type index. + - Box orientation (via get_box_dimensions). + - Box placement completely within container boundaries. + - Not exceeding the available counts for each box type. + - Proper support: every box not on the floor must be fully and uniquely supported. + - Overlap: boxes may only overlap if one is exactly supporting the other. + - Load-bearing capacity: the weight on each box must not exceed its capacity. + If all constraints are met, the function returns the container volume utilization, + i.e., (total placed box volume) / (container volume). + Inputs: + - container: tuple (L, W, H) in cm. + - n: number of box types. + - cargo_vol: total cargo volume (m³) (not used in evaluation). + - box_types: list of box type dictionaries. + - instance: instance number (int) (not used in evaluation). + - util: reported utilization (float) (ignored here). + - m: number of placements. + - placements: list of placements; each placement is a dict with keys: + 'box_type' (int, 1-indexed), + 'orientation' (int), + 'x', 'y', 'z' (floats). + Returns: + A float representing the container utilization if all constraints are satisfied. + """ + TOL = 1e-6 + container_L, container_W, container_H = container + placed = [] + usage = [0] * len(box_types) + + # Process each placement: check box type, orientation, and container boundaries. + for idx, placement in enumerate(placements): + bt_index = placement['box_type'] - 1 + if bt_index < 0 or bt_index >= len(box_types): + raise ValueError(f"Invalid box type index in placement {idx}: {placement['box_type']}") + + usage[bt_index] += 1 + box = box_types[bt_index] + + try: + # get_box_dimensions should return (dx, dy, dz, load_bearing, volume) + dx, dy, dz, lb, volume = self.get_box_dimensions(box, placement['orientation']) + except Exception as e: + raise ValueError(f"Orientation error for placement {idx}: {e}") + + # Check that the box is completely inside the container. + if (placement['x'] < -TOL or placement['y'] < -TOL or placement['z'] < -TOL or + placement['x'] + dx > container_L + TOL or + placement['y'] + dy > container_W + TOL or + placement['z'] + dz > container_H + TOL): + raise ValueError(f"Box at placement {idx} is out-of-bound") + + placed.append({ + 'id': idx, + 'box_type': bt_index, + 'orientation': placement['orientation'], + 'x': placement['x'], + 'y': placement['y'], + 'z': placement['z'], + 'dx': dx, + 'dy': dy, + 'dz': dz, + 'lb': lb, + 'weight': box['weight'], + 'volume': volume + }) + + # Check that the usage does not exceed available counts. + for i, count in enumerate(usage): + if count > box_types[i]['count']: + raise ValueError( + f"Usage error: Box type {i + 1} used {count} times but only {box_types[i]['count']} available") + + # Determine support relationships. + support_of = {} # Maps a box's id to the id of its supporting box. + for b in placed: + # Boxes on the floor need no support. + if abs(b['z']) < TOL: + continue + + candidate = None + for other in placed: + if other['id'] == b['id']: + continue + # Check if other box's top face aligns with the bottom of b. + if abs(other['z'] + other['dz'] - b['z']) > TOL: + continue + # b's horizontal projection must be completely inside other's top face. + if b['x'] + TOL < other['x'] or (b['x'] + b['dx']) - TOL > other['x'] + other['dx']: + continue + if b['y'] + TOL < other['y'] or (b['y'] + b['dy']) - TOL > other['y'] + other['dy']: + continue + if candidate is not None: + raise ValueError(f"Ambiguous support for box id {b['id']} (placement {b['id']})") + candidate = other + if candidate is None: + raise ValueError(f"Missing support for box id {b['id']} (placement {b['id']})") + support_of[b['id']] = candidate['id'] + + # Check for improper overlaps. + # Overlap is allowed only if one box is exactly supporting the other. + for i in range(len(placed)): + for j in range(i + 1, len(placed)): + b1 = placed[i] + b2 = placed[j] + # Skip if boxes are in non-overlapping vertical positions. + if b1['z'] + b1['dz'] - TOL <= b2['z'] or b2['z'] + b2['dz'] - TOL <= b1['z']: + continue + if self.boxes_overlap(b1, b2): + if support_of.get(b1['id'], -1) != b2['id'] and support_of.get(b2['id'], -1) != b1['id']: + raise ValueError(f"Improper overlap between box id {b1['id']} and box id {b2['id']}") + + # Compute load on each box. + total_load = {b['id']: 0.0 for b in placed} + placed_sorted = sorted(placed, key=lambda b: b['z'], reverse=True) + for b in placed_sorted: + load_here = b['weight'] + total_load[b['id']] + if b['id'] in support_of: + sup_id = support_of[b['id']] + total_load[sup_id] += load_here + + # Verify load-bearing capacity for each box. + for b in placed: + capacity = b['dx'] * b['dy'] * b['lb'] + if total_load[b['id']] > capacity + TOL: + excess = total_load[b['id']] - capacity + raise ValueError(f"Load-bearing capacity exceeded for box id {b['id']}: overload {excess}") + + total_box_volume = sum(b['volume'] for b in placed) + container_volume = container_L * container_W * container_H + utilization = total_box_volume / container_volume if container_volume > 0 else 0.0 + + return utilization + + def get_dev(self): + dev = { + 'wtpack1.txt': [23, 24, 74, 19, 18, 98, 15, 80, 20, 44, 49, 95, 21, 64, 37, 46, 88, 29, 2, 41, 12, 56, 52, + 31, + 86, 92, 57, 33, 78, 26, 10, 38, 40, 32, 67, 89, 85, 7, 11, 53, 97, 22, 70, 82, 8, 48, 43, + 45, + 91, 71], + 'wtpack2.txt': [17, 7, 76, 44, 74, 95, 47, 53, 31, 55, 58, 50, 21, 41, 14, 98, 49, 67, 97, 88, 73, 87, 34, + 19, + 64, 90, 54, 82, 61, 93, 91, 75, 59, 5, 71, 8, 18, 72, 92, 85, 40, 32, 43, 42, 39, 30, 10, + 48, + 25, 15], + 'wtpack3.txt': [94, 25, 40, 83, 39, 80, 13, 64, 70, 21, 65, 4, 31, 54, 45, 58, 29, 33, 59, 42, 69, 92, 79, + 96, + 71, 43, 50, 19, 75, 89, 98, 97, 77, 72, 51, 2, 18, 93, 52, 88, 68, 56, 7, 26, 32, 46, 87, + 91, + 22, 49], + 'wtpack4.txt': [7, 78, 37, 44, 33, 10, 23, 14, 39, 6, 79, 36, 38, 25, 97, 88, 26, 54, 76, 51, 99, 62, 20, + 48, + 56, 32, 49, 2, 47, 95, 86, 22, 8, 53, 71, 85, 93, 92, 90, 0, 52, 91, 28, 84, 63, 31, 24, 11, + 15, + 80], + 'wtpack5.txt': [5, 56, 60, 51, 64, 17, 88, 3, 76, 37, 78, 70, 74, 30, 2, 57, 11, 34, 96, 16, 41, 4, 15, 7, + 42, + 65, 97, 80, 89, 69, 39, 25, 0, 32, 81, 95, 82, 19, 31, 8, 85, 94, 33, 14, 55, 93, 18, 83, + 61, + 87], + 'wtpack6.txt': [33, 3, 58, 46, 8, 35, 95, 64, 90, 60, 43, 11, 27, 99, 91, 30, 68, 70, 41, 96, 81, 47, 57, + 87, + 74, 42, 16, 66, 28, 98, 85, 4, 72, 88, 59, 75, 51, 82, 71, 14, 65, 10, 40, 0, 38, 83, 52, 7, + 86, + 89], + 'wtpack7.txt': [24, 94, 50, 40, 76, 58, 15, 36, 5, 1, 27, 8, 18, 87, 88, 92, 38, 54, 80, 41, 21, 46, 57, 59, + 91, + 51, 97, 95, 79, 4, 22, 85, 26, 53, 42, 64, 9, 83, 96, 29, 44, 89, 73, 77, 69, 72, 81, 61, + 93, + 2]} + + return dev + + + +# Task configuration for benchmark task +ENTRY_NAME = 'solve' +FUNCTION_SIGNATURE = 'def solve(...):' +IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' +TASK_DESCRIPTION = '("The Container Loading with Weight Restrictions problem aims to maximize the utilization of a "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The Container Loading with Weight Restrictions problem aims to maximize the utilization of a "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(container: tuple, n: int, cargo_vol: float, box_types: list) -> dict:\n """\n Solves the Container Loading with Weight Restrictions problem.\n Input kwargs (for one test case):\n - container (tuple of int): (L, W, H) representing the container dimensions in cm.\n - n (int): the number of box types.\n - cargo_vol (float): the total cargo volume in m³ (provided for consistency).\n - box_types (list of dict): one per box type. Each dictionary has the keys:\n \'length\' (int), \'length_flag\' (int),\n \'width\' (int), \'width_flag\' (int),\n \'height\' (int), \'height_flag\' (int),\n \'count\' (int), \'weight\' (float),\n \'lb1\' (float), \'lb2\' (float), \'lb3\' (float).\n The problem is to select and place boxes (each possibly in one of three allowed orientations)\n inside the container so as to maximize the ratio of the total volume of placed boxes (each based on its original dimensions)\n to the container’s volume, while obeying placement, support, and load–bearing constraints.\n Evaluation metric:\n The score is the container volume utilization (i.e. total placed boxes volume divided by container volume)\n if the solution is valid according to all constraints; otherwise the score is 0.0.\n Placeholder implementation: No boxes are placed.\n Returns a dictionary with keys:\n - \'instance\': instance number (int),\n - \'util\': achieved utilization (float),\n - \'m\': number of placements (int),\n - \'placements\': a list of placements; each placement is a dict with keys:\n \'box_type\' (int, 1-indexed), \'orientation\' (int: 1, 2, or 3),\n \'x\', \'y\', \'z\' (floats for the lower–left–front corner in cm).\n """\n # Placeholder: return an empty solution.\n return {\n \'instance\': 1,\n \'util\': 0.0,\n \'m\': 0,\n \'placements\': []\n }' +EVAL_CLASS_NAME = 'CLWREvaluationCB' +EVAL_KWARGS = {'timeout_seconds': 60} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_container_loading_with_weight_restrictions/paras.yaml b/examples/benchmark_tasks/optimization_container_loading_with_weight_restrictions/paras.yaml new file mode 100644 index 00000000..1573d2c9 --- /dev/null +++ b/examples/benchmark_tasks/optimization_container_loading_with_weight_restrictions/paras.yaml @@ -0,0 +1,2 @@ +name: CLWREvaluationCB +timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_corporate_structuring/__init__.py b/examples/benchmark_tasks/optimization_corporate_structuring/__init__.py new file mode 100644 index 00000000..22ebdd3a --- /dev/null +++ b/examples/benchmark_tasks/optimization_corporate_structuring/__init__.py @@ -0,0 +1,341 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_corporate_structuring +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# References: +# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language +# model agents in algorithm search for combinatorial optimization. +# arXiv preprint arXiv:2504.04310 (2025). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any +import numpy as np +from llm4ad_loader import Evaluation +from llm4ad_loader import load_subdir_as_text +# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader +# from llm4ad.task.optimization.co_bench.corporate_structuring_co_bench.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(N: int, target: int, countries: dict, withholding: dict) -> dict:\n """\n Input kwargs:\n - N: (int) The number of countries.\n - target: (int) The target country (1-indexed) which must be the root (its parent is 0).\n - countries: (dict) Mapping country id (1-indexed) to a tuple:\n (tax_code, foreign_income_tax_rate, domestic_income_tax_rate, profit).\n - withholding: (dict of dict) A nested dictionary where withholding[i][j] is the withholding tax rate\n applied when country i sends dividends to country j.\n Returns:\n A dictionary with the key "structure" whose value is a dictionary representing the corporate tree,\n where each key is a child country and its value is the immediate parent (with the target country having parent 0).\n (Note: This is a placeholder implementation.)\n """\n # --- Placeholder implementation ---\n # For demonstration, we simply return a structure that includes only the target country.\n structure = {kwargs[\'target\']: 0}\n # In an actual solution, you would build a tree covering all countries with positive profit.\n return {"structure": structure}' +task_description = "'''Given N countries, each defined by:" + + +__all__ = ['CSEvaluationCB'] + + +class CSEvaluationCB(Evaluation): + + def __init__(self, + timeout_seconds=50, + **kwargs): + + """ + Args: + None + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + # Load datasets from Hugging Face + dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Corporate structuring") + self._datasets = {} + for filename in dataset: + # Join all text rows into a single string + text_content = '\n'.join([row['text'] for row in dataset[filename]]) + self._datasets[filename] = text_content + + def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: + return self.evaluate(callable_func) + + def evaluate(self, eva: callable) -> float | None: + ins_cases = [] + for case_id, ins in enumerate(self._datasets.values()): + ins_cases.append(self.load_data(ins)) + + fitness_list = [] + try: + for i in ins_cases: + for j in i: + result = eva(j['N'], j['target'], j['countries'], j['withholding']) + fitness = self.eval_func(j['N'], j['target'], j['countries'], j['withholding'], result['structure']) + fitness_list.append(fitness) + + return np.mean(fitness_list) # itself is a maximize problem + + except ValueError as e: + print(e) + return None + + def load_data(self, input_string): + """ + Reads input string content that may contain one or more cases. + File Format for each case: + - Line 1: Two space-separated numbers: N target + - Next N lines: For each country i (1-indexed), four space-separated values: + tax_code, foreign_income_tax_rate, domestic_income_tax_rate, profit + - Remaining tokens: N*N floating-point numbers representing the withholding tax matrix. + (These numbers can be spread across one or more lines.) + Returns: + A list of dictionaries. Each dictionary corresponds to one test case and has the keys: + - "N": (int) number of countries. + - "target": (int) target country (1-indexed). + - "countries": (dict) mapping each country id to its tuple of (tax_code, foreign_rate, domestic_rate, profit). + - "withholding": (dict of dict) where withholding[i][j] is the withholding tax rate from country i to j. + """ + cases = [] + lines = [line.strip() for line in input_string.split('\n') if line.strip() != ''] + + i = 0 + total_lines = len(lines) + while i < total_lines: + # Parse first line of a case: N and target. + parts = lines[i].split() + if len(parts) < 2: + raise ValueError("Expected N and target on line {}.".format(i + 1)) + N = int(parts[0]) + target = int(parts[1]) + i += 1 + + # Parse country data. + if i + N > total_lines: + raise ValueError("Not enough lines for country data in a case starting at line {}.".format(i + 1)) + countries = {} + for country in range(1, N + 1): + parts = lines[i].split() + if len(parts) < 4: + raise ValueError("Incomplete country data at line {}.".format(i + 1)) + tax_code = int(parts[0]) + foreign_rate = float(parts[1]) + domestic_rate = float(parts[2]) + profit = float(parts[3]) + countries[country] = (tax_code, foreign_rate, domestic_rate, profit) + i += 1 + + # Read all remaining tokens for the withholding tax matrix. + withholding_tokens = [] + # We'll assume that the withholding matrix occupies the next N*N tokens. + while i < total_lines and len(withholding_tokens) < N * N: + withholding_tokens.extend(lines[i].split()) + i += 1 + + if len(withholding_tokens) < N * N: + raise ValueError("Incomplete withholding tax matrix: expected {} numbers, got {}.".format(N * N, + len(withholding_tokens))) + + # Build the withholding matrix from tokens. + withholding = {} + token_index = 0 + for country in range(1, N + 1): + withholding[country] = {} + for j in range(1, N + 1): + withholding[country][j] = float(withholding_tokens[token_index]) + token_index += 1 + + # Append the parsed case to the list. + cases.append({ + "N": N, + "target": target, + "countries": countries, + "withholding": withholding + }) + return cases + + def eval_func(self, N, target, countries, withholding, structure): + """ + Evaluates the score of a given tree structure. + Inputs: + - N: Number of countries. + - target: The designated target country (1-indexed) that is the root (its parent is 0). + - countries: A dict mapping country id (1-indexed) to a tuple: + (tax_code, foreign_income_tax_rate, domestic_income_tax_rate, profit) + - withholding: A dict of dicts where withholding[i][j] is the withholding tax rate + applied when country i sends dividends to j. + - structure: A dict representing the corporate tree. Each key is a country (child) and its + value is its immediate parent (for the target, parent is 0). + Returns: + The score, defined as: + total_profit = (sum of profits for all countries) - (total_tax) + where total_tax is the sum of domestic tax and extra foreign tax paid in the tree. + """ + + # Build a mapping from each node to its children from the tree structure. + children = {i: [] for i in range(1, N + 1)} + for child, parent in structure.items(): + if parent != 0: # Only non-root nodes appear in the structure mapping. + children[parent].append(child) + # It is possible that some countries (e.g. with profit <= 0) are not in the structure. + # They will not incur any tax in the corporate hierarchy. + + # First, compute P[i] = sum of profits (only if >0) in the subtree of i. + # This is used in the pooling tax rules. + + P_cache = {} + + def get_P(i): + if i in P_cache: + return P_cache[i] + # Only count profit if positive (i.e. the node is a "source") + profit_i = countries[i][3] + total = profit_i + for c in children.get(i, []): + total += get_P(c) + P_cache[i] = total + return total + + for i in range(1, N + 1): + P_cache[i] = get_P(i) + + print(P_cache) + + def outcome(i): + d_income = countries[i][3] * (1 - countries[i][2]) + f_income = foreign_income(i) + total_f_income = sum(f_income.values()) + if countries[i][0] == 1: + return d_income + total_f_income + elif countries[i][0] == 2: + return d_income + total_f_income * (1 - countries[i][1]) + elif countries[i][0] == 3: + return d_income + total_f_income - sum( + [max(0, f_income[c] - (1 - countries[i][1]) * P_cache[c]) for c in children[i]]) + else: + return d_income + total_f_income - max(0, total_f_income - (1 - countries[i][1]) * ( + P_cache[i] - countries[i][3])) + + def foreign_income(i): + if len(children.get(i, [])) == 0: + return {} + else: + total = {} + for c in children.get(i, []): + a = outcome(c) + total[c] = a * (1 - withholding[c][i]) + return total + + return outcome(target) + + def norm_score(self, results): + optimal_scores = { + "tax1.txt": [647.51], + "tax2.txt": [2153.45], + "tax3.txt": [4329.83], + "tax4.txt": [3491.62], + "tax5.txt": [5435.79], + "tax6.txt": [5058.07], + "tax7.txt": [11872.37], + "tax8.txt": [10206.65], + "tax9.txt": [16584.32], + "tax10.txt": [455], + } + + normed = {} + for case, (scores, error_message) in results.items(): + if case not in optimal_scores: + continue # Skip if there's no optimal score defined. + optimal_list = optimal_scores[case] + normed_scores = [] + # Compute normalized score for each index. + for idx, score in enumerate(scores): + if isinstance(score, (int, float)): + normed_scores.append(score / optimal_list[idx]) + else: + normed_scores.append(score) + normed[case] = (normed_scores, error_message) + + return normed + + def get_dev(self): + dev = {'tax1.txt': [], 'tax3.txt': [], 'tax5.txt': [], + 'tax7.txt': [], 'tax9.txt': []} + + return dev + + + +# Task configuration for benchmark task +ENTRY_NAME = 'solve' +FUNCTION_SIGNATURE = 'def solve(...):' +IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' +TASK_DESCRIPTION = "'''Given N countries, each defined by:" +OBJECTIVE_TEXT = "You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n'''Given N countries, each defined by:\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible." +TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(N: int, target: int, countries: dict, withholding: dict) -> dict:\n """\n Input kwargs:\n - N: (int) The number of countries.\n - target: (int) The target country (1-indexed) which must be the root (its parent is 0).\n - countries: (dict) Mapping country id (1-indexed) to a tuple:\n (tax_code, foreign_income_tax_rate, domestic_income_tax_rate, profit).\n - withholding: (dict of dict) A nested dictionary where withholding[i][j] is the withholding tax rate\n applied when country i sends dividends to country j.\n Returns:\n A dictionary with the key "structure" whose value is a dictionary representing the corporate tree,\n where each key is a child country and its value is the immediate parent (with the target country having parent 0).\n (Note: This is a placeholder implementation.)\n """\n # --- Placeholder implementation ---\n # For demonstration, we simply return a structure that includes only the target country.\n structure = {kwargs[\'target\']: 0}\n # In an actual solution, you would build a tree covering all countries with positive profit.\n return {"structure": structure}' +EVAL_CLASS_NAME = 'CSEvaluationCB' +EVAL_KWARGS = {'timeout_seconds': 60} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_corporate_structuring/paras.yaml b/examples/benchmark_tasks/optimization_corporate_structuring/paras.yaml new file mode 100644 index 00000000..afbcb78e --- /dev/null +++ b/examples/benchmark_tasks/optimization_corporate_structuring/paras.yaml @@ -0,0 +1,2 @@ +name: CSEvaluationCB +timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_crew_scheduling/__init__.py b/examples/benchmark_tasks/optimization_crew_scheduling/__init__.py new file mode 100644 index 00000000..4a110b57 --- /dev/null +++ b/examples/benchmark_tasks/optimization_crew_scheduling/__init__.py @@ -0,0 +1,369 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_crew_scheduling +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# References: +# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language +# model agents in algorithm search for combinatorial optimization. +# arXiv preprint arXiv:2504.04310 (2025). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any +import numpy as np +from llm4ad_loader import Evaluation +from llm4ad_loader import load_subdir_as_text +# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader +# from llm4ad.task.optimization.co_bench.crew_scheduling_co_bench.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(N: int, K: int, time_limit: float, tasks: dict, arcs: dict) -> dict:\n """\n Solves the crew scheduling problem.\n The problem consists of assigning each task (with a defined start and finish time) to exactly one crew,\n such that:\n - The tasks within each crew are executed in non-overlapping order.\n - For every consecutive pair of tasks in a crew’s schedule, a valid transition arc exists (with an associated cost).\n - The overall duty time (finish time of the last task minus start time of the first task) does not exceed the specified time limit.\n - Exactly K crews are used.\n Input kwargs (for one case):\n - N (int): Number of tasks.\n - K (int): Maximum number of crews to be used.\n - time_limit (float): Maximum allowed duty time.\n - tasks (dict): Dictionary mapping task ID (1 to N) to a tuple (start_time, finish_time).\n - arcs (dict): Dictionary mapping (from_task, to_task) pairs to transition cost.\n Evaluation metric:\n - If all constraints are met (no task overlap, valid transition arcs, duty time within the limit, and exactly K crews used), the score is the sum of transition costs across all crews.\n - If any constraint is violated, the solution is infeasible and receives no score.\n - A lower score indicates a more cost-effective solution.\n Returns:\n dict: A dictionary with one key "crews", whose value is a list of lists. Each inner list is a sequence of task IDs (integers)\n representing one crew’s schedule.\n """\n # --- placeholder implementation ---\n # For example, here we distribute tasks evenly across K crews.\n N = kwargs.get("N")\n K = kwargs.get("K")\n tasks_ids = list(range(1, N + 1))\n crews = [[] for _ in range(K)]\n for i, task in enumerate(tasks_ids):\n crews[i % K].append(task)\n # In practice, you would implement a heuristic or optimization method that groups tasks into exactly K crews\n # while satisfying the non-overlap, valid transitions, and duty time constraints.\n return {"crews": crews}' +task_description = '("The Crew Scheduling Problem involves assigning each task—with defined start and finish times—to "' + + +__all__ = ['CSchedulingEvaluationCB'] + + +class CSchedulingEvaluationCB(Evaluation): + + def __init__(self, + timeout_seconds=50, + **kwargs): + + """ + Args: + None + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + # Load datasets from Hugging Face + dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Crew scheduling") + self._datasets = {} + for filename in dataset: + # Join all text rows into a single string + text_content = '\n'.join([row['text'] for row in dataset[filename]]) + self._datasets[filename] = text_content + + def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: + return self.evaluate(callable_func) + + def evaluate(self, eva: callable) -> float | None: + ins_cases = [] + for case_id, ins in enumerate(self._datasets.values()): + ins_cases.append(self.load_data(ins)) + + fitness_list = [] + try: + for i in ins_cases: + for j in i: + result = eva(j['N'], j['K'], j['time_limit'], j['tasks'], j['arcs']) + fitness = self.eval_func(N=j['N'], K=j['K'], time_limit=j['time_limit'], tasks=j['tasks'], arcs=j['arcs'], crews=result['crews']) + fitness_list.append(fitness) + + return -np.mean(fitness_list) + + except ValueError as e: + print(e) + return None + + def load_data(self, input_string): + """ + Loads input data from a provided text string. This function supports multiple cases. + The input file format for each case is as follows: + - The first line contains two numbers: the number of tasks (N) and the maximum allowed duty time (time_limit). + - The next N lines contain two numbers each: start time and finish time for each task (tasks are indexed from 1 to N). + - The remaining lines describe transition arcs between tasks in the format: "i j cost". + Cases are assumed to be separated by one or more blank lines. + Returns: + list: A list of dictionaries, each dictionary corresponds to one case with keys: + "N", "time_limit", "tasks", "arcs". + """ + cases = [] + try: + lines = [line.strip() for line in input_string.split('\n') if line.strip() != ''] + except Exception as e: + raise ValueError("Failed to read input string: " + str(e)) + + # Split lines into blocks separated by blank lines. + blocks = [] + current_block = [] + for line in lines: + if line.strip() == "": + if current_block: + blocks.append(current_block) + current_block = [] + else: + current_block.append(line.strip()) + if current_block: + blocks.append(current_block) + + # Parse each block as a separate case. + for block in blocks: + if not block: + continue + # Parse the first line: number of tasks and time limit. + first_parts = block[0].split() + if len(first_parts) < 2: + raise ValueError("The first line must contain at least two values: number of tasks and time limit.") + try: + N = int(first_parts[0]) + time_limit = float(first_parts[1]) + except Exception as e: + raise ValueError("Error parsing number of tasks or time limit: " + str(e)) + + if len(block) < 1 + N: + raise ValueError(f"Expected {N} task lines after the first line; found {len(block) - 1}.") + + tasks = {} + # Parse tasks: next N lines. + for i in range(1, 1 + N): + parts = block[i].split() + if len(parts) < 2: + raise ValueError(f"Task line {i} does not contain two values.") + try: + start_time = float(parts[0]) + finish_time = float(parts[1]) + except Exception as e: + raise ValueError(f"Invalid time values in task line {i}: " + str(e)) + tasks[i] = (start_time, finish_time) + + # Parse remaining lines: transition arcs. + arcs = {} + for line in block[1 + N:]: + parts = line.split() + if len(parts) < 3: + continue # Ignore lines that don't have the complete triple. + try: + from_task = int(parts[0]) + to_task = int(parts[1]) + cost = float(parts[2]) + except Exception: + continue # Skip lines with invalid formatting. + arcs[(from_task, to_task)] = cost + + case_data = {"N": N, "time_limit": time_limit, "tasks": tasks, "arcs": arcs} + + # Determine K range based on problem size (N) + if N <= 50: + k_range = range(27, 32) + elif N <= 100: + k_range = range(44, 49) + elif N <= 150: + k_range = range(69, 74) + elif N <= 200: + k_range = range(93, 98) + elif N <= 250: + k_range = range(108, 113) + elif N <= 300: + k_range = range(130, 134) + elif N <= 350: + k_range = range(144, 149) + elif N <= 400: + k_range = range(159, 164) + elif N <= 450: + k_range = range(182, 187) + else: # N <= 500 or larger + k_range = range(204, 209) + + for k in k_range: + cases.append(case_data | {'K': k}) + + return cases + + def eval_func(self, **kwargs): + """ + Evaluates the quality (i.e. total cost and feasibility) of a crew scheduling solution. + Raises an error immediately if any feasibility constraint is violated. + Input kwargs must include: + - N (int): Number of tasks. + - K (int): The exact number of crews required. + - time_limit (float): Maximum allowed duty time. + - tasks (dict): Mapping from task ID to (start_time, finish_time). + - arcs (dict): Mapping from (from_task, to_task) to transition cost. + - crews (list): List of lists, where each inner list is the sequence of task IDs for one crew. + Returns: + float: The total transition cost if the solution is feasible. + """ + N = kwargs.get("N") + K = kwargs.get("K") + time_limit = kwargs.get("time_limit") + tasks = kwargs.get("tasks") + arcs = kwargs.get("arcs") + crews = kwargs.get("crews") + + if crews is None: + raise ValueError("Solution does not contain a 'crews' key.") + + # Check that exactly K crews are used. + if K is None: + raise ValueError("Parameter K (number of crews) is missing.") + if len(crews) > K: + raise ValueError(f"Invalid solution: number of crews in solution is larger than K={K}.") + + # Validate that every task appears exactly once. + all_tasks_in_output = [task for crew in crews for task in crew] + if len(all_tasks_in_output) != N: + raise ValueError("Invalid solution: number of tasks in crews does not equal N.") + if set(all_tasks_in_output) != set(range(1, N + 1)): + raise ValueError("Invalid solution: tasks in crews do not match expected tasks set.") + + total_cost = 0.0 + + # Evaluate each crew schedule. + for crew in crews: + if not crew: + raise ValueError("Invalid solution: one crew has an empty schedule.") + + # Check the duty time. + first_task = crew[0] + last_task = crew[-1] + duty_time = tasks[last_task][1] - tasks[first_task][0] + if duty_time > time_limit: + raise ValueError("Invalid solution: duty time for a crew exceeds the time limit.") + + # Check each consecutive pair of tasks. + for idx in range(len(crew) - 1): + current_task = crew[idx] + next_task = crew[idx + 1] + + # Check that tasks do not overlap. + if tasks[current_task][1] > tasks[next_task][0]: + raise ValueError(f"Invalid solution: tasks {current_task} and {next_task} overlap.") + + # Check that a valid transition arc exists. + if (current_task, next_task) not in arcs: + raise ValueError( + f"Invalid solution: missing transition arc between tasks {current_task} and {next_task}.") + + # Add the transition cost. + total_cost += arcs[(current_task, next_task)] + + return total_cost + + def norm_score(self, results): + optimal_scores = { + 'csp50.txt': [3139, 2706, 2399, 2092, 1872], + 'csp100.txt': [4812, 4514, 4310, 4107, 3905], + 'csp150.txt': [6275, 5999, 5754, 5551, 5347], + 'csp200.txt': [6914, 6747, 6583, 6430, 6288], + 'csp250.txt': [8406, 8212, 8023, 7863, 7707], + 'csp300.txt': [9580, 9378, 9200, 9026], + 'csp350.txt': [10991, 10833, 10677, 10525, 10378], + 'csp400.txt': [12341, 12163, 12006, 11848, 11696], + 'csp450.txt': [12785, 12639, 12497, 12357, 12232], + 'csp500.txt': [13302, 13169, 13032, 12899, 12772], + } + + normed = {} + for case, (scores, error_message) in results.items(): + if case not in optimal_scores: + continue # Skip if there's no optimal score defined. + optimal_list = optimal_scores[case] + normed_scores = [] + # Compute normalized score for each index. + for idx, score in enumerate(scores): + if isinstance(score, (int, float)): + normed_scores.append(optimal_list[idx] / score) + else: + normed_scores.append(score) + normed[case] = (normed_scores, error_message) + + return normed + + def get_dev(self): + dev = {'csp100.txt': [2, 1], 'csp150.txt': [1, 4], 'csp200.txt': [4, 2], 'csp250.txt': [2, 1], + 'csp300.txt': [2, 0], + 'csp350.txt': [4, 3], 'csp400.txt': [2, 0], 'csp450.txt': [2, 1], 'csp50.txt': [1, 0], + 'csp500.txt': [4, 1]} + + return dev + + + +# Task configuration for benchmark task +ENTRY_NAME = 'solve' +FUNCTION_SIGNATURE = 'def solve(...):' +IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' +TASK_DESCRIPTION = '("The Crew Scheduling Problem involves assigning each task—with defined start and finish times—to "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The Crew Scheduling Problem involves assigning each task—with defined start and finish times—to "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(N: int, K: int, time_limit: float, tasks: dict, arcs: dict) -> dict:\n """\n Solves the crew scheduling problem.\n The problem consists of assigning each task (with a defined start and finish time) to exactly one crew,\n such that:\n - The tasks within each crew are executed in non-overlapping order.\n - For every consecutive pair of tasks in a crew’s schedule, a valid transition arc exists (with an associated cost).\n - The overall duty time (finish time of the last task minus start time of the first task) does not exceed the specified time limit.\n - Exactly K crews are used.\n Input kwargs (for one case):\n - N (int): Number of tasks.\n - K (int): Maximum number of crews to be used.\n - time_limit (float): Maximum allowed duty time.\n - tasks (dict): Dictionary mapping task ID (1 to N) to a tuple (start_time, finish_time).\n - arcs (dict): Dictionary mapping (from_task, to_task) pairs to transition cost.\n Evaluation metric:\n - If all constraints are met (no task overlap, valid transition arcs, duty time within the limit, and exactly K crews used), the score is the sum of transition costs across all crews.\n - If any constraint is violated, the solution is infeasible and receives no score.\n - A lower score indicates a more cost-effective solution.\n Returns:\n dict: A dictionary with one key "crews", whose value is a list of lists. Each inner list is a sequence of task IDs (integers)\n representing one crew’s schedule.\n """\n # --- placeholder implementation ---\n # For example, here we distribute tasks evenly across K crews.\n N = kwargs.get("N")\n K = kwargs.get("K")\n tasks_ids = list(range(1, N + 1))\n crews = [[] for _ in range(K)]\n for i, task in enumerate(tasks_ids):\n crews[i % K].append(task)\n # In practice, you would implement a heuristic or optimization method that groups tasks into exactly K crews\n # while satisfying the non-overlap, valid transitions, and duty time constraints.\n return {"crews": crews}' +EVAL_CLASS_NAME = 'CSchedulingEvaluationCB' +EVAL_KWARGS = {'timeout_seconds': 60} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_crew_scheduling/paras.yaml b/examples/benchmark_tasks/optimization_crew_scheduling/paras.yaml new file mode 100644 index 00000000..e5f0d6b0 --- /dev/null +++ b/examples/benchmark_tasks/optimization_crew_scheduling/paras.yaml @@ -0,0 +1,2 @@ +name: CSchedulingEvaluationCB +timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_cvrp_construct/__init__.py b/examples/benchmark_tasks/optimization_cvrp_construct/__init__.py new file mode 100644 index 00000000..2412106f --- /dev/null +++ b/examples/benchmark_tasks/optimization_cvrp_construct/__init__.py @@ -0,0 +1,328 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_cvrp_construct +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# Module Name: CVRPEvaluation +# Last Revision: 2025/2/16 +# Description: Evaluates the Capacitated Vehicle Routing Problem (CVRP). +# Given a set of customers and a fleet of vehicles with limited capacity, +# the goal is to find optimal routes for the vehicles to serve all customers +# while minimizing the total travel distance. +# This module is part of the LLM4AD project (https://github.com/Optima-CityU/llm4ad). +# +# Parameters: +# - timeout_seconds: Maximum allowed time (in seconds) for the evaluation process: int (default: 20). +# - n_instance: Number of problem instances to generate: int (default: 16). +# - problem_size: Number of customers to serve: int (default: 50). +# - capacity: Maximum capacity of each vehicle: int (default: 40). +# +# References: +# - Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +import os, sys +sys.path.insert(0, os.path.dirname(__file__)) +import copy +from typing import Any +import matplotlib.pyplot as plt +import numpy as np + +from llm4ad_loader import Evaluation +from get_instance import GetData +# from llm4ad.task.optimization.cvrp_construct.get_instance import GetData # Converted from LLM4AD import +# from llm4ad.task.optimization.cvrp_construct.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\ndef select_next_node(current_node: int, depot: int, unvisited_nodes: np.ndarray, rest_capacity: np.ndarray, demands: np.ndarray, distance_matrix: np.ndarray) -> int:\n """Design a novel algorithm to select the next node in each step.\n Args:\n current_node: ID of the current node.\n depot: ID of the depot.\n unvisited_nodes: Array of IDs of unvisited nodes.\n rest_capacity: rest capacity of vehicle\n demands: demands of nodes\n distance_matrix: Distance matrix of nodes.\n Return:\n ID of the next node to visit.\n """\n best_score = -1\n next_node = -1\n\n for node in unvisited_nodes:\n demand = demands[node]\n distance = distance_matrix[current_node][node]\n\n if demand <= rest_capacity:\n score = demand / distance if distance > 0 else float(\'inf\') # Avoid division by zero\n if score > best_score:\n best_score = score\n next_node = node\n\n return next_node' +task_description = '"' + + + +class CVRPEvaluation(Evaluation): + def __init__(self, + timeout_seconds=20, + n_instance=16, + problem_size=50, + capacity=40, + **kwargs): + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + self.problem_size = problem_size + 1 + self.n_instance = n_instance + self.capacity = capacity + + getData = GetData(self.n_instance, self.problem_size, self.capacity) + self._datasets = getData.generate_instances() + + def plot_solution(self, instance: np.ndarray, route: list, demands: list, vehicle_capacity: int): + """ + Plot the solution of the Capacitated Vehicle Routing Problem (CVRP). + + Args: + instance: A 2D array of node coordinates (including the depot). + route: A list representing the sequence of nodes visited in the route. + demands: A list of demands for each node. + vehicle_capacity: The capacity of the vehicle. + """ + # Extract coordinates + x = instance[:, 0] + y = instance[:, 1] + + # Create a figure and axis + fig, ax = plt.subplots(figsize=(10, 8)) + + # Plot depot (node 0) + ax.plot(x[0], y[0], 'ro', markersize=10, label='Depot') + ax.text(x[0], y[0], 'Depot', ha='center', va='bottom', fontsize=12) + + # Plot customer nodes + for i in range(1, len(x)): + ax.plot(x[i], y[i], 'bo', markersize=8) + ax.text(x[i], y[i], f'C{i}\nDem: {demands[i]}', ha='center', va='bottom', fontsize=8) + + # Split the route into individual vehicle routes based on depot visits + routes = [] + current_route = [] + for node in route: + current_route.append(node) + if node == 0 and len(current_route) > 1: # End of a route (return to depot) + routes.append(current_route) + current_route = [0] # Start a new route from the depot + if current_route: # Add the last route if it exists + routes.append(current_route) + + # Plot each route in a different color + colors = plt.cm.tab10.colors # Use a colormap for distinct colors + for i, r in enumerate(routes): + color = colors[i % len(colors)] # Cycle through colors + for j in range(len(r) - 1): + start_node = r[j] + end_node = r[j + 1] + ax.plot([x[start_node], x[end_node]], [y[start_node], y[end_node]], color=color, linestyle='--', linewidth=1, label=f'Route {i + 1}' if j == 0 else None) + + # Add load information + if end_node != 0: # If not returning to the depot + ax.text((x[start_node] + x[end_node]) / 2, (y[start_node] + y[end_node]) / 2, + f'Load: {sum(demands[r[:j + 1]])}', ha='center', va='center', fontsize=8, rotation=45) + + # Mark start and end nodes of the route with triangles (excluding depot) + if len(r) > 1: + ax.plot(x[r[1]], y[r[1]], '^', color=color, markersize=10, label='Start' if i == 0 else None) # Start node + ax.plot(x[r[-2]], y[r[-2]], 'v', color=color, markersize=10, label='End' if i == 0 else None) # End node + + # Set axis labels and title + ax.set_xlabel('X Coordinate') + ax.set_ylabel('Y Coordinate') + ax.set_title('Capacitated Vehicle Routing Problem (CVRP) Solution') + ax.legend(loc='upper right') + + # Show the plot + plt.tight_layout() + plt.show() + + def tour_cost(self, instance, solution): + cost = 0 + for j in range(len(solution) - 1): + cost += np.linalg.norm(instance[int(solution[j])] - instance[int(solution[j + 1])]) + cost += np.linalg.norm(instance[int(solution[-1])] - instance[int(solution[0])]) + return cost + + def route_construct(self, distance_matrix, demands, vehicle_capacity, heuristic): + route = [] + current_load = 0 + current_node = 0 + route.append(current_node) + + unvisited_nodes = set(range(1, self.problem_size)) # Assuming node 0 is the depot + all_nodes = np.array(list(unvisited_nodes)) + feasible_unvisited_nodes = all_nodes + + while unvisited_nodes: + next_node = heuristic(current_node, + 0, + feasible_unvisited_nodes, # copy + vehicle_capacity - current_load, + copy.deepcopy(demands), # copy + copy.deepcopy(distance_matrix)) # copy + if next_node == 0: + # Update route and load + route.append(next_node) + current_load = 0 + current_node = 0 + else: + # Update route and load + route.append(next_node) + current_load += demands[next_node] + unvisited_nodes.remove(next_node) + current_node = next_node + + feasible_nodes_capacity = np.array([node for node in all_nodes if current_load + demands[node] <= vehicle_capacity]) + # Determine feasible and unvisited nodes + feasible_unvisited_nodes = np.intersect1d(feasible_nodes_capacity, list(unvisited_nodes)) + + if len(unvisited_nodes) > 0 and len(feasible_unvisited_nodes) < 1: + route.append(0) + current_load = 0 + current_node = 0 + feasible_unvisited_nodes = np.array(list(unvisited_nodes)) + + # check if not all nodes have been visited + independent_values = set(route) + if len(independent_values) != self.problem_size: + return None + return route + + def evaluate(self, heuristic): + dis = np.ones(self.n_instance) + n_ins = 0 + + for instance, distance_matrix, demands, vehicle_capacity in self._datasets: + route = self.route_construct(distance_matrix, demands, vehicle_capacity, heuristic) + LLM_dis = self.tour_cost(instance, route) + dis[n_ins] = LLM_dis + n_ins += 1 + if n_ins == self.n_instance: + break + + ave_dis = np.average(dis) + return -ave_dis + + def evaluate_program(self, program_str: str, callable_func: callable) -> Any | None: + return self.evaluate(callable_func) + + +if __name__ == '__main__': + def select_next_node(current_node: int, depot: int, unvisited_nodes: np.ndarray, rest_capacity: np.ndarray, demands: np.ndarray, distance_matrix: np.ndarray) -> int: + """Design a novel algorithm to select the next node in each step. + Args: + current_node: ID of the current node. + depot: ID of the depot. + unvisited_nodes: Array of IDs of unvisited nodes. + rest_capacity: rest capacity of vehicle + demands: demands of nodes + distance_matrix: Distance matrix of nodes. + Return: + ID of the next node to visit. + """ + next_node = unvisited_nodes[0] + return next_node + + + # def select_next_node(current_node: int, depot: int, unvisited_nodes: np.ndarray, rest_capacity: np.ndarray, demands: np.ndarray, distance_matrix: np.ndarray) -> int: + # """Design a novel algorithm to select the next node in each step. + # Args: + # current_node: ID of the current node. + # depot: ID of the depot. + # unvisited_nodes: Array of IDs of unvisited nodes. + # rest_capacity: rest capacity of vehicle + # demands: demands of nodes + # distance_matrix: Distance matrix of nodes. + # Return: + # ID of the next node to visit. + # """ + # best_score = -1 + # next_node = -1 + + # for node in unvisited_nodes: + # demand = demands[node] + # distance = distance_matrix[current_node][node] + + # if demand <= rest_capacity: + # score = demand / distance if distance > 0 else float('inf') # Avoid division by zero + # if score > best_score: + # best_score = score + # next_node = node + + # return next_node + + eval = CVRPEvaluation() + res = eval.evaluate_program('', select_next_node) + print(res) + +# Task configuration for benchmark task +ENTRY_NAME = 'select_next_node' +FUNCTION_SIGNATURE = 'def select_next_node(...):' +IMPORT_HEADER = 'import numpy as np\nimport math' +TASK_DESCRIPTION = '"' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `select_next_node` for the LLM4AD task.\\n\\nTask description:\\n"\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\ndef select_next_node(current_node: int, depot: int, unvisited_nodes: np.ndarray, rest_capacity: np.ndarray, demands: np.ndarray, distance_matrix: np.ndarray) -> int:\n """Design a novel algorithm to select the next node in each step.\n Args:\n current_node: ID of the current node.\n depot: ID of the depot.\n unvisited_nodes: Array of IDs of unvisited nodes.\n rest_capacity: rest capacity of vehicle\n demands: demands of nodes\n distance_matrix: Distance matrix of nodes.\n Return:\n ID of the next node to visit.\n """\n best_score = -1\n next_node = -1\n\n for node in unvisited_nodes:\n demand = demands[node]\n distance = distance_matrix[current_node][node]\n\n if demand <= rest_capacity:\n score = demand / distance if distance > 0 else float(\'inf\') # Avoid division by zero\n if score > best_score:\n best_score = score\n next_node = node\n\n return next_node' +EVAL_CLASS_NAME = 'CVRPEvaluation' +EVAL_KWARGS = {'timeout_seconds': 30} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_cvrp_construct/get_instance.py b/examples/benchmark_tasks/optimization_cvrp_construct/get_instance.py new file mode 100644 index 00000000..41e5f5fb --- /dev/null +++ b/examples/benchmark_tasks/optimization_cvrp_construct/get_instance.py @@ -0,0 +1,50 @@ +import pickle + +import numpy as np + + +class GetData: + def __init__(self, n_instance, n_cities, capacity): + self.n_instance = n_instance + self.n_cities = n_cities + self.capacity = capacity + + def generate_instances(self): + """each instance -> (coordinates, distances, demands, capacity)""" + np.random.seed(2024) + instance_data = [] + for _ in range(self.n_instance): + coordinates = np.random.rand(self.n_cities, 2) + demands = np.random.randint(1, 10, size=self.n_cities) + distances = np.linalg.norm(coordinates[:, np.newaxis] - coordinates, axis=2) + instance_data.append((coordinates, distances, demands, self.capacity)) + return instance_data + + +if __name__ == '__main__': + gd = GetData(10, 51) + data = gd.generate_instances() + with open('data.pkl', 'wb') as f: + pickle.dump(data, f) + + prompt_code_temp = "import numpy as np\n\ + def select_next_node(current_node: int, depot: int, unvisited_nodes: np.ndarray, rest_capacity: np.ndarray, demands: np.ndarray, distance_matrix: np.ndarray) -> int: \n\ + \n\ + '''Design a novel algorithm to select the next node in each step.\n\ + \n\ + Args:\n\ + current_node: ID of the current node.\n\ + depot: ID of the depot.\n\ + unvisited_nodes: Array of IDs of unvisited nodes.\n\ + rest_capacity: rest capacity of vehicle \n\ + demands: demands of nodes \n\ + distance_matrix: Distance matrix of nodes.\n\ + \n\ + Return:\n\ + ID of the next node to visit.\n\ + '''\n\ + next_node = unvisited_nodes[0]\n\ + \n\ + return next_node\n" + + print(prompt_code_temp) diff --git a/examples/benchmark_tasks/optimization_cvrp_construct/paras.yaml b/examples/benchmark_tasks/optimization_cvrp_construct/paras.yaml new file mode 100644 index 00000000..fa0fb882 --- /dev/null +++ b/examples/benchmark_tasks/optimization_cvrp_construct/paras.yaml @@ -0,0 +1,2 @@ +name: CVRPEvaluation +timeout_seconds: 30 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_equitable_partitioning_problem/__init__.py b/examples/benchmark_tasks/optimization_equitable_partitioning_problem/__init__.py new file mode 100644 index 00000000..77946010 --- /dev/null +++ b/examples/benchmark_tasks/optimization_equitable_partitioning_problem/__init__.py @@ -0,0 +1,326 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_equitable_partitioning_problem +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# References: +# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language +# model agents in algorithm search for combinatorial optimization. +# arXiv preprint arXiv:2504.04310 (2025). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any +import numpy as np +from llm4ad_loader import Evaluation +from llm4ad_loader import load_subdir_as_text +# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader +# from llm4ad.task.optimization.co_bench.equitable_partitioning_problem_co_bench.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(data: list[list[int]]) -> dict:\n """\n Partition individuals into 8 groups so that for every binary attribute the count of 1\'s is as evenly\n distributed across the groups as possible.\n Input kwargs:\n - data (list of list of int): A matrix where each inner list represents the binary attributes (0 or 1)\n of one individual.\n Evaluation Metric:\n For each attribute, calculate the number of 1’s in each group,\n then compute the absolute difference between each group’s count and the mean count for that attribute.\n Average these differences over all groups to obtain the attribute’s imbalance.\n The final score is the sum of these attribute imbalances across all attributes.\n A lower score indicates a more balanced partitioning.\n Returns:\n dict: A dictionary with one key \'assignment\' whose value is a list of positive integers (one per individual)\n indicating the group assignment (using 1-based indexing). For example:\n { "assignment": [1, 1, 1, ...] }\n """\n # --- Placeholder solution ---\n # For this placeholder, we assign every individual to group 1.\n data = kwargs.get(\'data\', [])\n num_individuals = len(data)\n return {\'assignment\': [1] * num_individuals}' +task_description = '("The task is to partition a set of individuals—each characterized by multiple binary "' + + +__all__ = ['EPPEvaluationCB'] + + +class EPPEvaluationCB(Evaluation): + + def __init__(self, + timeout_seconds=50, + **kwargs): + + """ + Args: + None + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + # Load datasets from Hugging Face + dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Equitable partitioning problem") + self._datasets = {} + for filename in dataset: + # Join all text rows into a single string + text_content = '\n'.join([row['text'] for row in dataset[filename]]) + self._datasets[filename] = text_content + + def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: + return self.evaluate(callable_func) + + def evaluate(self, eva: callable) -> float | None: + ins_cases = [] + for case_id, ins in enumerate(self._datasets.values()): + ins_cases.append(self.load_data(ins)) + + fitness_list = [] + try: + for i in ins_cases: + for j in i: + result = eva(j['data']) + fitness = self.eval_func(data=j['data'], assignment=result['assignment']) + fitness_list.append(fitness) + + return -np.mean(fitness_list) + + except ValueError as e: + print(e) + return None + + def load_data(self, input_string): + """ + Reads input string content where each non-empty line represents an individual with space-separated binary attributes. + In case the input string contains multiple cases (separated by one or more blank lines), this function will + separate them into distinct cases. + Parameters: + input_string (str): The string content with the input data. + Returns: + list: A list of dictionaries. Each dictionary represents one case with the key 'data' mapping to a 2D list + (matrix) of binary attributes (0 or 1). For example: + [ + {"data": [[0, 1, 0], [1, 0, 1], ...]}, + {"data": [[1, 1], [0, 1], ...]}, + ... + ] + Raises: + Exception: If the string cannot be read, or if any line is invalid, contains non-integer tokens, + tokens not in {0, 1}, or if any row has an inconsistent number of attributes. + """ + try: + all_lines = [line.strip() for line in input_string.split('\n')] + except Exception as e: + raise Exception("Error reading input string: " + str(e)) + + cases = [] + current_case = [] + for line_no, line in enumerate(all_lines, start=1): + stripped = line.strip() + # A blank line indicates a separator between cases. + if not stripped: + if current_case: + cases.append(current_case) + current_case = [] + continue + current_case.append(stripped) + + # Add last case if file did not end with a blank line. + if current_case: + cases.append(current_case) + + # Parse each case into a data matrix. + list_of_cases = [] + for case_idx, case_lines in enumerate(cases, start=1): + matrix = [] + n_attributes = None + for line_no, line in enumerate(case_lines, start=1): + tokens = line.split() + if not tokens: + raise Exception(f"Case {case_idx}, line {line_no} is empty or invalid.") + try: + row = [int(token) for token in tokens] + except ValueError: + raise Exception(f"Non-integer value found in case {case_idx}, line {line_no}.") + for token in row: + if token not in (0, 1): + raise Exception( + f"Invalid attribute value {token} found in case {case_idx}, line {line_no}; expected only 0 or 1.") + if n_attributes is None: + n_attributes = len(row) + elif len(row) != n_attributes: + raise Exception(f"Inconsistent number of attributes in case {case_idx}, line {line_no}.") + matrix.append(row) + list_of_cases.append({"data": matrix}) + + if not list_of_cases: + raise Exception("Input file is empty or contains no valid cases.") + + return list_of_cases + + def eval_func(self, **kwargs): + """ + Evaluates a partitioning solution for the equitable distribution problem using the new imbalance metric. + Expected Parameters (provided via kwargs): + - data (list of list of int): A matrix of binary attributes for individuals. + - assignment (list of int): A list of positive integers representing group assignments for each individual. + Evaluation Metric: + For each attribute (column), compute the number of 1's per group. Then, compute the mean of these counts. + The imbalance for the attribute is defined as the average of the absolute differences between each group's count and the mean count. + The final score is the sum of these imbalances over all attributes. + (A lower score indicates a more balanced partitioning.) + Returns: + total_imbalance: The computed total imbalance (float). + Raises: + Exception: If any expected parameter is missing, if the assignment format is invalid, or if the number of groups is not 8. + """ + # Retrieve input data and assignment from kwargs + if 'data' not in kwargs or 'assignment' not in kwargs: + raise Exception("Missing required input parameters 'data' and/or 'assignment'.") + + data = kwargs['data'] + assignment = kwargs['assignment'] + # + n_individuals = len(data) + if len(assignment) != n_individuals: + raise Exception(f"Expected {n_individuals} group assignments but found {len(assignment)}.") + + n_attributes = len(data[0]) + for idx, row in enumerate(data, start=1): + if len(row) != n_attributes: + raise Exception(f"Inconsistent number of attributes in data at individual {idx}.") + + # Ensure all group assignments are positive integers. + for idx, g in enumerate(assignment, start=1): + if not isinstance(g, int) or g < 1: + raise Exception(f"Invalid group assignment at position {idx}: {g}. Must be a positive integer.") + + # Collect unique groups and check for exactly 8 groups. + groups = set(assignment) + if len(groups) != 8: + raise Exception(f"Invalid number of groups: expected 8, but got {len(groups)}.") + + # Initialize per-group attribute sums. + group_sums = {g: [0] * n_attributes for g in groups} + for ind, group in enumerate(assignment): + for j in range(n_attributes): + group_sums[group][j] += data[ind][j] + + total_imbalance = 0.0 + for j in range(n_attributes): + # Collect counts for attribute j from all groups + attr_counts = [group_sums[g][j] for g in groups] + mean_count = sum(attr_counts) / len(groups) + # Compute average absolute difference from the mean + # imbalance = sum(abs(count - mean_count) for count in attr_counts) / len(groups) + imbalance = sum(abs(count - mean_count) for count in attr_counts) + total_imbalance += imbalance + + return total_imbalance + + def norm_score(self, results): + optimal_scores = { + "eppperf1.txt": [0], + "eppperf2.txt": [0], + "eppperf3.txt": [0], + "eppperf4.txt": [0], + "eppperf5.txt": [0], + "epprandom1.txt": [11.5], + "epprandom2.txt": [12.75], + "epprandom3.txt": [13.75], + "epprandom4.txt": [14.50], + "epprandom5.txt": [16.25], + } + + normed = {} + for case, (scores, error_message) in results.items(): + if case not in optimal_scores: + continue # Skip if there's no optimal score defined. + optimal_list = optimal_scores[case] + normed_scores = [] + # Compute normalized score for each index. + for idx, score in enumerate(scores): + if isinstance(score, (int, float)): + if optimal_list[idx] == 0: + normed_scores.append((optimal_list[idx] + 1) / (score + 1)) + else: + normed_scores.append(optimal_list[idx] / score) + else: + normed_scores.append(score) + normed[case] = (normed_scores, error_message) + + return normed + + def get_dev(self): + dev = {'eppperf1.txt': [0], 'eppperf3.txt': [0], + 'epprandom2.txt': [0], 'epprandom4.txt': [0]} + + return dev + + + + +# Task configuration for benchmark task +ENTRY_NAME = 'solve' +FUNCTION_SIGNATURE = 'def solve(...):' +IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' +TASK_DESCRIPTION = '("The task is to partition a set of individuals—each characterized by multiple binary "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The task is to partition a set of individuals—each characterized by multiple binary "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(data: list[list[int]]) -> dict:\n """\n Partition individuals into 8 groups so that for every binary attribute the count of 1\'s is as evenly\n distributed across the groups as possible.\n Input kwargs:\n - data (list of list of int): A matrix where each inner list represents the binary attributes (0 or 1)\n of one individual.\n Evaluation Metric:\n For each attribute, calculate the number of 1’s in each group,\n then compute the absolute difference between each group’s count and the mean count for that attribute.\n Average these differences over all groups to obtain the attribute’s imbalance.\n The final score is the sum of these attribute imbalances across all attributes.\n A lower score indicates a more balanced partitioning.\n Returns:\n dict: A dictionary with one key \'assignment\' whose value is a list of positive integers (one per individual)\n indicating the group assignment (using 1-based indexing). For example:\n { "assignment": [1, 1, 1, ...] }\n """\n # --- Placeholder solution ---\n # For this placeholder, we assign every individual to group 1.\n data = kwargs.get(\'data\', [])\n num_individuals = len(data)\n return {\'assignment\': [1] * num_individuals}' +EVAL_CLASS_NAME = 'EPPEvaluationCB' +EVAL_KWARGS = {'timeout_seconds': 60} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_equitable_partitioning_problem/paras.yaml b/examples/benchmark_tasks/optimization_equitable_partitioning_problem/paras.yaml new file mode 100644 index 00000000..578dfad2 --- /dev/null +++ b/examples/benchmark_tasks/optimization_equitable_partitioning_problem/paras.yaml @@ -0,0 +1,2 @@ +name: EPPEvaluationCB +timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_euclidean_steiner_problem/__init__.py b/examples/benchmark_tasks/optimization_euclidean_steiner_problem/__init__.py new file mode 100644 index 00000000..b2b2dd4c --- /dev/null +++ b/examples/benchmark_tasks/optimization_euclidean_steiner_problem/__init__.py @@ -0,0 +1,372 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_euclidean_steiner_problem +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# References: +# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language +# model agents in algorithm search for combinatorial optimization. +# arXiv preprint arXiv:2504.04310 (2025). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any +import numpy as np +from llm4ad_loader import Evaluation +from llm4ad_loader import load_subdir_as_text +# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader +# from llm4ad.task.optimization.co_bench.euclidean_steiner_problem_co_bench.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(points: list) -> dict:\n """\n Solves a single instance of the Euclidean Steiner Problem.\n Problem Description:\n Given a set of 2D points (terminals), the goal is to compute additional Steiner points\n such that when you compute the MST over the union of the original terminals and these Steiner points,\n the total length (measured via Euclidean distances) is minimized.\n (Recall, the Euclidean distance between two points (x1, y1) and (x2, y2) is sqrt((x1-x2)^2 + (y1-y2)^2).)\n Input kwargs:\n - points: a list of points, where each point is a tuple of floats (x, y),\n representing the coordinates of an original terminal.\n Returns:\n A dictionary with one key:\n - "steiner_points": a list of (x, y) tuples representing the additional Steiner points.\n It is assumed that the candidate solution’s computed total length can be derived by computing\n the MST over the union of the original terminals and the returned Steiner points.\n """\n points = kwargs.get("points")\n if points is None:\n raise ValueError("Missing input: \'points\' key is required.")\n\n # Placeholder for an actual Steiner tree algorithm:\n # In a real implementation, you would compute extra Steiner points to lower the MST length.\n steiner_points = [] # For now, return no additional Steiner points.\n\n return {"steiner_points": steiner_points}' +task_description = '("Given a set of 2D points (terminals), the goal of the Euclidean Steiner Problem is to compute a "' + + +__all__ = ['ESPEvaluationCB'] + + +class ESPEvaluationCB(Evaluation): + + def __init__(self, + timeout_seconds=50, + **kwargs): + + """ + Args: + None + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + # Load datasets from Hugging Face + dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Euclidean Steiner problem") + self._datasets = {} + for filename in dataset: + # Join all text rows into a single string + text_content = '\n'.join([row['text'] for row in dataset[filename]]) + self._datasets[filename] = text_content + + def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: + return self.evaluate(callable_func) + + def evaluate(self, eva: callable) -> float | None: + ins_cases = [] + for case_id, ins in enumerate(self._datasets.values()): + ins_cases.append(self.load_data(ins)) + + fitness_list = [] + try: + for i in ins_cases: + for j in i: + result = eva(j['points']) + fitness = self.eval_func(points=j['points'], steiner_points=result['steiner_points']) + fitness_list.append(fitness) + + return np.mean(fitness_list) # itself is a maximum problem + + except ValueError as e: + print(e) + return None + + def load_data(self, input_string): + """ + Reads the input string and returns a list of individual test problems. + The input file may contain one or more cases. Each case is expected to follow the format: + Line 1: An integer m representing the number of test problems in the case. + For each test problem: + Line 1: An integer n representing the number of points. + Next n lines: Two space-separated floating-point numbers for the x- and y-coordinates. + Returns: + A list of dictionaries. Each dictionary corresponds to one test problem and contains: + - "points": a list of (x, y) tuples representing the terminals. + The function ignores empty lines and supports multiple cases concatenated in one file. + """ + all_lines = [line.strip() for line in input_string.split('\n')] + + problems = [] + idx = 0 + while idx < len(all_lines): + # Read number of test problems for this case. + try: + m = int(all_lines[idx]) + except Exception as e: + raise ValueError(f"Expected an integer for number of test problems at line {idx + 1}: {e}") + idx += 1 + for i in range(m): + if idx >= len(all_lines): + raise ValueError(f"Insufficient data for test problem {i + 1} in a case.") + try: + n = int(all_lines[idx]) + except Exception as e: + raise ValueError(f"Expected an integer for number of points at line {idx + 1}: {e}") + idx += 1 + pts = [] + for j in range(n): + if idx >= len(all_lines): + raise ValueError(f"Insufficient point data for test problem {i + 1}, point {j + 1}.") + parts = all_lines[idx].split() + if len(parts) < 2: + raise ValueError(f"Test problem {i + 1}: point {j + 1} does not have two coordinates.") + try: + x, y = float(parts[0]), float(parts[1]) + except Exception as e: + raise ValueError(f"Test problem {i + 1}: invalid coordinate format at point {j + 1}: {e}") + pts.append((x, y)) + idx += 1 + problems.append({"points": pts}) + return problems + + def eval_func(self, **kwargs): + """ + Evaluates candidate solutions for the Euclidean Steiner Problem. + Expected kwargs: + - problems: a list of test problems; each test problem is a dict with key "points" + which holds a list of (x, y) tuples representing the original terminals. + - solutions: a list of candidate solutions, one for each test problem. + Each candidate solution is a dict with: + - "steiner_points": a list of (x, y) tuples representing the additional Steiner points. + Evaluation: + For each test problem: + 1. Compute MST_original, the total length of the Minimum Spanning Tree (MST) computed + on the original terminals. + 2. Compute candidate_value, the total length of the MST computed on the union of + the original terminals and the candidate Steiner points. + (Both MST computations use Euclidean distance where the distance between (x1,y1) and (x2,y2) + is sqrt((x1-x2)^2 + (y1-y2)^2).) + 3. A valid candidate must have candidate_value ≤ MST_original (within a small tolerance). + If not, a ValueError is raised. + Otherwise, the quality ratio is computed as candidate_value / MST_original. + (A lower ratio indicates a better solution.) + The overall score is the average of the ratios over all test problems. + Returns: + overall_score (float): The average ratio over all test problems. + """ + import math + + TOL = 1e-6 + + def euclidean_distance(a, b): + return math.sqrt((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2) + + def compute_mst_length(points): + n = len(points) + if n == 0: + return 0.0 + in_mst = [False] * n + min_dist = [float('inf')] * n + min_dist[0] = 0.0 + total = 0.0 + for _ in range(n): + u = -1 + best = float('inf') + for j in range(n): + if not in_mst[j] and min_dist[j] < best: + best = min_dist[j] + u = j + if u == -1: + break + in_mst[u] = True + total += best + for v in range(n): + if not in_mst[v]: + d = euclidean_distance(points[u], points[v]) + if d < min_dist[v]: + min_dist[v] = d + return total + + original_points = kwargs.get("points") + steiner_points = kwargs.get("steiner_points", []) + + # Compute the MST length on the original terminals. + mst_original = compute_mst_length(original_points) + # Compute the candidate tree length as the MST on original terminals plus the candidate Steiner points. + union_points = original_points + steiner_points + candidate_value = compute_mst_length(union_points) + + # The candidate MST must not be longer than the MST of the original terminals. + if candidate_value > mst_original + TOL: + raise ValueError( + f"Candidate solution for problem violates constraint: candidate_value ({candidate_value}) > mst_original ({mst_original}).") + + ratio = candidate_value / mst_original if mst_original > 0 else 1.0 + score = 1.0 - ratio + return score + + def norm_score(self, results): + optimal_scores = { + 'estein250.txt': [0.03] * 15, + 'estein500.txt': [0.03] * 15, + 'estein1000.txt': [0.03] * 15, + 'estein10000.txt': [0.03], + 'estein100.txt': [0.032423065085033675, 0.03483759951901777, 0.034182167020644916, 0.03248098628546203, + 0.03310927379936712, 0.034041259411550784, 0.0397677887027611, 0.035660501862228244, + 0.03502528832071461, 0.03371716889176812, 0.028161233483136594, 0.02687851300146371, + 0.026629423968470123, 0.03565961816027485, 0.027792022641784153], + 'estein10.txt': [0.04299943461594302, 0.004769960182740007, 0.043782084069761584, 0.011502149990875177, + 0.024602813181648697, 0.046077835193320094, 0.04426130719672583, 0.015859056215462353, + 0.02494689613151435, 0.01979275009710557, 0.054874017619661486, 0.005785367498201133, + 0.06167524682759662, 0.05601469362679634, 0.030685355394374447], + 'estein1.txt': [0.03715248999695819, 2.53248940706996e-08, 0.0, 0.0, 9.429250334525019e-05, + 0.023970330309954435, 0.01908676366919071, 2.3915825470233187e-05, 0.13381432532245285, + 0.0295462267220441, 0.056958559892640315, 0.01345616626071433, 0.02629868523014056, + 0.06795895781452022, 0.0017250989103574366, 0.0, 0.0, 0.06725973598503387, + 0.037596415595463006, 0.1338944832237634, 0.026412933267079164, 0.018262573283449823, + 0.02298024555878808, 0.008339962103159793, 0.010573340933293873, 0.001728616561433527, + 0.0028756538345464655, 0.0, 0.013994227369019674, 0.10179537695309238, 0.07520718237458235, + 0.05864794152816455, 0.028893309353272167, 0.012207382373579323, 0.006618274407397151, + 0.023430599555555487, 0.0051899185134780534, 0.007102662306716856, 0.0, 0.04660324576963126, + 0.007969992389563973, 0.014169307452227442, 0.029004689079907386, 0.00890432342316072, + 0.024451928874551054, 0.08931639733333341], + 'estein20.txt': [0.043942725618148826, 0.02299597956072552, 0.03725284493193792, 0.02793871516551827, + 0.03890768508604925, 0.027692754737118963, 0.020995306344934295, 0.047581240549860127, + 0.015508884273023105, 0.035719166517610645, 0.030072471281848645, 0.04369773360827678, + 0.031287634487079496, 0.03339355305720737, 0.01641067343311564], + 'estein30.txt': [0.021869824541884353, 0.027617593078341218, 0.02963480155348497, 0.03714277441461655, + 0.03618276310308932, 0.03148586454727753, 0.03001110334170809, 0.021792810128040463, + 0.03951202278065513, 0.03211942119280953, 0.020834943979018195, 0.03215928284393588, + 0.024799825912022122, 0.04963688935942201, 0.025222898338703503], + 'estein40.txt': [0.02609813221879309, 0.03181546093667176, 0.0257617636108477, 0.024867757483739594, + 0.03878011159818051, 0.033996855652012936, 0.03010133858855013, 0.03474099376571327, + 0.04407499975387952, 0.036479709224781276, 0.018556418029103017, 0.027092227325115847, + 0.032442218263355804, 0.034038355193724, 0.03194768623039035], + 'estein50.txt': [0.026375763293115195, 0.03786259604274811, 0.0368858882909211, 0.02843354067948245, + 0.031562424825947843, 0.03451603250411406, 0.031052490692446644, 0.026042857120256224, + 0.030847821995874658, 0.028427456323692923, 0.024745303837364396, 0.028489474734615827, + 0.03501573784622991, 0.02796869646410083, 0.026754142858155694], + 'estein60.txt': [0.033431902683743187, 0.029312387787789773, 0.03673737294505586, 0.029931036026207947, + 0.038719592946913406, 0.027985371134918502, 0.034956652180465175, 0.02568855514408741, + 0.03291599372153209, 0.027053357949617274, 0.030189122888249265, 0.03666235385539496, + 0.037309702462750116, 0.037371343062245765, 0.03292664563821035], + 'estein70.txt': [0.0281926927308368, 0.03822852322564063, 0.02985749535563431, 0.027371582271915496, + 0.03165937908883898, 0.0319172977507971, 0.03216563529368788, 0.028798544856373787, + 0.02368422096077183, 0.03141890259642621, 0.03168584881094072, 0.03728987267456063, + 0.030740662840068156, 0.028285136466959404, 0.03516404960406827], + 'estein80.txt': [0.028927636103650123, 0.027621437956897088, 0.030045750960559836, 0.02154696015188895, + 0.02208065777296797, 0.028561814513135886, 0.04406481956617947, 0.03559605525407783, + 0.0387928564376363, 0.029134782330045295, 0.029451055665711712, 0.020408525270118272, + 0.032505342891095745, 0.038584240577326456, 0.02859138721565424], + 'estein90.txt': [0.03726927391600421, 0.03352718377112174, 0.02689284725659824, 0.027968087207550618, + 0.040547493724352957, 0.02090677298804755, 0.03565573020648938, 0.030772023917592817, + 0.030029109853112357, 0.031132625096035427, 0.03504603605103018, 0.026598398815443458, + 0.02814959463666722, 0.03392597014885834, 0.029514790002086455] + } + + normed = {} + for case, (scores, error_message) in results.items(): + if case not in optimal_scores: + continue # Skip if there's no optimal score defined. + optimal_list = optimal_scores[case] + normed_scores = [] + # Compute normalized score for each index. + for idx, score in enumerate(scores): + if isinstance(score, (int, float)): + if optimal_list[idx] == 0: + normed_scores.append(1.0) + else: + normed_scores.append(score / optimal_list[idx]) + else: + normed_scores.append(score) + normed[case] = (normed_scores, error_message) + + return normed + + def get_dev(self): + dev = {'estein1.txt': [5, 43, 37, 26, 38, 27, 25, 9, 42, 0, 4, 34, 36, 24, 3, 10, 15, 13, 12, 8, 20, 23, 14], + 'estein10.txt': [6, 3, 12, 2, 8, 9, 5], 'estein100.txt': [2, 11, 0, 7, 13, 6, 4], + 'estein1000.txt': [9, 6, 1, 5, 7, 14, 3], 'estein20.txt': [13, 2, 3, 14, 0, 4, 8], + 'estein250.txt': [1, 14, 6, 10, 2, 11, 4], 'estein30.txt': [3, 12, 9, 11, 4, 2, 14], + 'estein40.txt': [14, 13, 3, 6, 10, 7, 2], 'estein50.txt': [4, 7, 8, 5, 9, 6, 0], + 'estein500.txt': [12, 11, 4, 8, 1, 9, 0], 'estein60.txt': [14, 0, 2, 8, 12, 9, 7], + 'estein70.txt': [12, 10, 0, 14, 1, 11, 2], 'estein80.txt': [9, 12, 1, 3, 2, 13, 6], + 'estein90.txt': [14, 3, 4, 8, 2, 5, 10]} + + return dev + + + + +# Task configuration for benchmark task +ENTRY_NAME = 'solve' +FUNCTION_SIGNATURE = 'def solve(...):' +IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' +TASK_DESCRIPTION = '("Given a set of 2D points (terminals), the goal of the Euclidean Steiner Problem is to compute a "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("Given a set of 2D points (terminals), the goal of the Euclidean Steiner Problem is to compute a "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(points: list) -> dict:\n """\n Solves a single instance of the Euclidean Steiner Problem.\n Problem Description:\n Given a set of 2D points (terminals), the goal is to compute additional Steiner points\n such that when you compute the MST over the union of the original terminals and these Steiner points,\n the total length (measured via Euclidean distances) is minimized.\n (Recall, the Euclidean distance between two points (x1, y1) and (x2, y2) is sqrt((x1-x2)^2 + (y1-y2)^2).)\n Input kwargs:\n - points: a list of points, where each point is a tuple of floats (x, y),\n representing the coordinates of an original terminal.\n Returns:\n A dictionary with one key:\n - "steiner_points": a list of (x, y) tuples representing the additional Steiner points.\n It is assumed that the candidate solution’s computed total length can be derived by computing\n the MST over the union of the original terminals and the returned Steiner points.\n """\n points = kwargs.get("points")\n if points is None:\n raise ValueError("Missing input: \'points\' key is required.")\n\n # Placeholder for an actual Steiner tree algorithm:\n # In a real implementation, you would compute extra Steiner points to lower the MST length.\n steiner_points = [] # For now, return no additional Steiner points.\n\n return {"steiner_points": steiner_points}' +EVAL_CLASS_NAME = 'ESPEvaluationCB' +EVAL_KWARGS = {'timeout_seconds': 60} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_euclidean_steiner_problem/paras.yaml b/examples/benchmark_tasks/optimization_euclidean_steiner_problem/paras.yaml new file mode 100644 index 00000000..e05bc700 --- /dev/null +++ b/examples/benchmark_tasks/optimization_euclidean_steiner_problem/paras.yaml @@ -0,0 +1,2 @@ +name: ESPEvaluationCB +timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_flow_shop_scheduling/__init__.py b/examples/benchmark_tasks/optimization_flow_shop_scheduling/__init__.py new file mode 100644 index 00000000..6d2ffe33 --- /dev/null +++ b/examples/benchmark_tasks/optimization_flow_shop_scheduling/__init__.py @@ -0,0 +1,388 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_flow_shop_scheduling +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# References: +# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language +# model agents in algorithm search for combinatorial optimization. +# arXiv preprint arXiv:2504.04310 (2025). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any +import numpy as np +from llm4ad_loader import Evaluation +from llm4ad_loader import load_subdir_as_text +# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader +# from llm4ad.task.optimization.co_bench.flow_shop_scheduling_co_bench.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n: int, m: int, matrix: list) -> dict:\n """\n Solves the flow shop scheduling problem.\n Input kwargs:\n - n (int): Number of jobs.\n - m (int): Number of machines.\n - matrix (list of list of int): Processing times for each job, where each sublist\n contains m integers (processing times for machines 0 through m-1).\n Evaluation Metric:\n The solution is evaluated by its makespan, which is the completion time of the last\n job on the last machine computed by the classical flow shop recurrence.\n Returns:\n dict: A dictionary with a single key \'job_sequence\' whose value is a permutation\n (1-indexed) of the job indices. For example, for 4 jobs, a valid return is:\n {\'job_sequence\': [1, 3, 2, 4]}\n Note: This is a placeholder implementation.\n """\n # Placeholder: simply return the identity permutation.\n return {\'job_sequence\': list(range(1, kwargs[\'n\'] + 1))}' +task_description = '("Given n jobs and m machines, the goal of the flow shop scheduling problem is to determine "' + + +__all__ = ['FSSEvaluationCB'] + + +class FSSEvaluationCB(Evaluation): + + def __init__(self, + timeout_seconds=50, + **kwargs): + + """ + Args: + None + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + # Load datasets from Hugging Face + dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Flow shop scheduling") + self._datasets = {} + for filename in dataset: + # Join all text rows into a single string + text_content = '\n'.join([row['text'] for row in dataset[filename]]) + self._datasets[filename] = text_content + + def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: + return self.evaluate(callable_func) + + def evaluate(self, eva: callable) -> float | None: + ins_cases = [] + for case_id, ins in enumerate(self._datasets.values()): + ins_cases.append(self.load_data(ins)) + + fitness_list = [] + try: + for i in ins_cases: + for j in i: + result = eva(j['n'], j['m'], j['matrix']) + fitness = self.eval_func(n=j['n'], m=j['m'], matrix=j['matrix'], job_sequence=result['job_sequence'], lower_bound=j['lower_bound'], upper_bound=j['upper_bound']) + fitness_list.append(fitness) + + return np.mean(fitness_list) + + except ValueError as e: + print(e) + return None + + def load_data(self, input_string): + """ + Reads a file containing multiple test cases for the flow shop scheduling problem. + The file format: + - A header line: "number of jobs, number of machines, initial seed, upper bound and lower bound :" + - Next line: five numbers (n, m, seed, upper_bound, lower_bound) + - A line that starts with "processing times :" + - Then m lines of processing times. Each line contains n integers (processing times for one machine across all jobs). + The function returns a list of test cases, where each test case is a dictionary with: + - "n" (int): number of jobs + - "m" (int): number of machines + - "matrix" (list of list of int): processing times in a n x m matrix (each row corresponds to a job) + - "upper_bound" (int) + - "lower_bound" (int) + """ + test_cases = [] + all_lines = [line.strip() for line in input_string.split('\n')] + + i = 0 + while i < len(all_lines): + line = all_lines[i].strip() + # Look for the header line indicating a new test case. + if line.startswith("number of jobs"): + # Skip to the line with the five numbers. + i += 1 + while i < len(all_lines) and all_lines[i].strip() == "": + i += 1 + if i >= len(all_lines): + break + # The header values line (n, m, seed, upper_bound, lower_bound) + header_tokens = all_lines[i].strip().split() + if len(header_tokens) < 5: + raise ValueError(f"Expected at least 5 numbers in header, got: {all_lines[i].strip()}") + n = int(header_tokens[0]) + m = int(header_tokens[1]) + # initial seed is ignored + upper_bound = int(header_tokens[3]) + lower_bound = int(header_tokens[4]) + i += 1 + + # Skip empty lines until we find the processing times label. + while i < len(all_lines) and all_lines[i].strip() == "": + i += 1 + # Expect a line that starts with "processing times" + if i < len(all_lines) and all_lines[i].strip().lower().startswith("processing times"): + i += 1 + else: + raise ValueError("Expected 'processing times' line not found.") + + # Read m lines containing the processing times (each line should have n integers) + machine_times = [] + for _ in range(m): + while i < len(all_lines) and all_lines[i].strip() == "": + i += 1 + if i >= len(all_lines): + raise ValueError("Unexpected end of file while reading processing times.") + row_tokens = all_lines[i].strip().split() + if len(row_tokens) != n: + raise ValueError( + f"Expected {n} numbers in processing times line, got {len(row_tokens)} in line: {all_lines[i].strip()}") + row = [int(token) for token in row_tokens] + machine_times.append(row) + i += 1 + + # The data is read per machine, so transpose it to obtain a list of n jobs, + # where each job is a list of m processing times. + matrix = [[machine_times[machine][job] for machine in range(m)] for job in range(n)] + + # Add the test case dictionary. + test_cases.append({ + "n": n, + "m": m, + "matrix": matrix, + "upper_bound": upper_bound, + "lower_bound": lower_bound + }) + else: + i += 1 + + return test_cases + + # def load_flowshop1(self, input_path): + # """ + # Reads the input file for one or more flow shop scheduling instances. + # The file may contain multiple cases. For each case, the instance is defined by: + # - A header section (to be skipped) until a line with exactly two integers is found. + # - The two integers define n (number of jobs) and m (number of machines). + # - Then the next n nonempty lines (ignoring blank lines and lines starting with '+') + # contain the job descriptions. Each job line must contain at least 2*m integers, + # which are interpreted as (machine, processing_time) pairs. + # - The processing times for each job are collected and ordered by machine number (0 to m-1). + # Returns: + # list: A list of dictionaries, each corresponding to one instance/case with keys: + # - 'n': number of jobs (int) + # - 'm': number of machines (int) + # - 'matrix': list of list of int (each sublist contains processing times for one job) + # """ + # if 'tai' in input_path: + # return load_tai(input_path) + # + # cases = [] + # try: + # with open(input_path, 'r') as f: + # lines = f.readlines() + # except Exception as e: + # raise Exception("Error reading input file: " + str(e)) + # + # line_index = 0 + # total_lines = len(lines) + # + # while line_index < total_lines: + # # Search for a valid instance size line (exactly two integers) + # instance_found = False + # while line_index < total_lines: + # line = lines[line_index].strip() + # line_index += 1 + # if not line: + # continue + # tokens = line.split() + # if len(tokens) == 2: + # try: + # n_val = int(tokens[0]) + # m_val = int(tokens[1]) + # n, m = n_val, m_val + # instance_found = True + # break + # except ValueError: + # continue + # if not instance_found: + # break # No more instances found + # + # matrix = [] + # job_count = 0 + # # Read next n valid job lines (skip blank and lines starting with '+') + # while line_index < total_lines and job_count < n: + # line = lines[line_index].strip() + # line_index += 1 + # if not line or line.startswith('+'): + # continue + # tokens = line.split() + # if len(tokens) < 2 * m: + # raise Exception( + # f"Error: Expected at least {2 * m} numbers in a job line, got {len(tokens)} in line: {line}") + # # Consider only the first 2*m tokens in case of extra tokens. + # tokens = tokens[:2 * m] + # try: + # numbers = [int(token) for token in tokens] + # except ValueError: + # raise Exception("Error: Non-integer token encountered in job line.") + # + # job_data = {} + # for i in range(0, len(numbers), 2): + # machine = numbers[i] + # proc_time = numbers[i + 1] + # if machine < 0 or machine >= m: + # raise Exception(f"Error: Invalid machine number {machine} (expected between 0 and {m - 1}).") + # if machine in job_data: + # raise Exception(f"Error: Duplicate machine number {machine} in job line.") + # job_data[machine] = proc_time + # if set(job_data.keys()) != set(range(m)): + # raise Exception("Error: Not all machine numbers are present in job line.") + # job_proc = [job_data[i] for i in range(m)] + # matrix.append(job_proc) + # job_count += 1 + # + # if job_count != n: + # raise Exception("Error: Number of job lines read does not match the expected number of jobs.") + # + # cases.append({'n': n, 'm': m, 'matrix': matrix}) + # + # return cases + + def eval_func(self, **kwargs): + """ + Evaluates a flow shop scheduling solution for a single instance. + Input kwargs must include: + - n (int): Number of jobs. + - m (int): Number of machines. + - matrix (list of list of int): Processing times matrix. + - job_sequence (list of int): A 1-indexed permutation of job indices, as returned by solve. + The evaluation metric (makespan) is computed using the classical flow shop recurrence: + - C[0][0] = processing_time(job_1, machine_0) + - For the first job on machines j > 0: C[0][j] = C[0][j-1] + processing_time(job_1, machine_j) + - For subsequent jobs on the first machine: C[i][0] = C[i-1][0] + processing_time(job_(i+1), machine_0) + - For all other entries: C[i][j] = max(C[i-1][j], C[i][j-1]) + processing_time(job_(i+1), machine_j) + Returns: + float: The computed makespan for the provided solution. + """ + n = kwargs.get('n') + m = kwargs.get('m') + matrix = kwargs.get('matrix') + job_sequence = kwargs.get('job_sequence') + + # Validate the job sequence: it must be a permutation of [1, 2, ..., n] + if not job_sequence or len(job_sequence) != n or set(job_sequence) != set(range(1, n + 1)): + raise Exception(f"Error: Job sequence is not a valid permutation of job indices 1 to {n}.") + + # Convert job sequence from 1-indexed to 0-indexed. + seq_zero = [job - 1 for job in job_sequence] + + # Initialize the completion time table. + completion = [[0] * m for _ in range(n)] + + for i in range(n): + for j in range(m): + proc_time = matrix[seq_zero[i]][j] + if i == 0 and j == 0: + completion[i][j] = proc_time + elif i == 0: + completion[i][j] = completion[i][j - 1] + proc_time + elif j == 0: + completion[i][j] = completion[i - 1][j] + proc_time + else: + completion[i][j] = max(completion[i - 1][j], completion[i][j - 1]) + proc_time + + makespan = completion[-1][-1] + + score = kwargs['lower_bound'] / makespan + # score = kwargs['upper_bound'] / makespan + return score + + def get_dev(self): + dev = {'tai100_10.txt': [1, 7, 4, 9, 8], 'tai100_20.txt': [1, 0, 2, 6, 8], 'tai100_5.txt': [9, 8, 5, 6, 3], + 'tai200_10.txt': [5, 9, 4, 1, 0], 'tai200_20.txt': [9, 4, 7, 6, 0], 'tai20_10.txt': [8, 9, 2, 5, 4], + 'tai20_20.txt': [4, 8, 9, 7, 6], 'tai20_5.txt': [7, 3, 9, 8, 0], 'tai500_20.txt': [3, 0, 6, 7, 4], + 'tai50_10.txt': [6, 4, 3, 8, 7], 'tai50_20.txt': [1, 7, 4, 6, 2], 'tai50_5.txt': [6, 7, 2, 4, 8]} + + return dev + + + + + +# Task configuration for benchmark task +ENTRY_NAME = 'solve' +FUNCTION_SIGNATURE = 'def solve(...):' +IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' +TASK_DESCRIPTION = '("Given n jobs and m machines, the goal of the flow shop scheduling problem is to determine "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("Given n jobs and m machines, the goal of the flow shop scheduling problem is to determine "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n: int, m: int, matrix: list) -> dict:\n """\n Solves the flow shop scheduling problem.\n Input kwargs:\n - n (int): Number of jobs.\n - m (int): Number of machines.\n - matrix (list of list of int): Processing times for each job, where each sublist\n contains m integers (processing times for machines 0 through m-1).\n Evaluation Metric:\n The solution is evaluated by its makespan, which is the completion time of the last\n job on the last machine computed by the classical flow shop recurrence.\n Returns:\n dict: A dictionary with a single key \'job_sequence\' whose value is a permutation\n (1-indexed) of the job indices. For example, for 4 jobs, a valid return is:\n {\'job_sequence\': [1, 3, 2, 4]}\n Note: This is a placeholder implementation.\n """\n # Placeholder: simply return the identity permutation.\n return {\'job_sequence\': list(range(1, kwargs[\'n\'] + 1))}' +EVAL_CLASS_NAME = 'FSSEvaluationCB' +EVAL_KWARGS = {'timeout_seconds': 60} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_flow_shop_scheduling/paras.yaml b/examples/benchmark_tasks/optimization_flow_shop_scheduling/paras.yaml new file mode 100644 index 00000000..63bf302e --- /dev/null +++ b/examples/benchmark_tasks/optimization_flow_shop_scheduling/paras.yaml @@ -0,0 +1,2 @@ +name: FSSEvaluationCB +timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_generalised_assignment_problem/__init__.py b/examples/benchmark_tasks/optimization_generalised_assignment_problem/__init__.py new file mode 100644 index 00000000..776f2121 --- /dev/null +++ b/examples/benchmark_tasks/optimization_generalised_assignment_problem/__init__.py @@ -0,0 +1,340 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_generalised_assignment_problem +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# References: +# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language +# model agents in algorithm search for combinatorial optimization. +# arXiv preprint arXiv:2504.04310 (2025). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any +import numpy as np +from llm4ad_loader import Evaluation +from llm4ad_loader import load_subdir_as_text +# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader +# from llm4ad.task.optimization.co_bench.generalised_assignment_problem_co_bench.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(m:int, n:int, cost_matrix:list, consumption_matrix:list, capacities:list, problem_type:str=\'max\') -> dict:\n """\n Solve the Generalised Assignment Problem (GAP) for a single case.\n Input arguments (passed as keyword arguments):\n - m: (int) Number of agents.\n - n: (int) Number of jobs.\n - cost_matrix: (list of list of float) A matrix of size m×n where cost_matrix[i][j]\n represents the cost of assigning job j to agent i.\n - consumption_matrix: (list of list of float) A matrix of size m×n where consumption_matrix[i][j]\n represents the resource consumed when job j is assigned to agent i.\n - capacities: (list of float) A list of length m containing the resource capacity for each agent.\n - problem_type: (str, optional) Indicates whether the problem is a \'max\' or \'min\' problem.\n Defaults to \'max\'.\n Returns:\n A dictionary with the key \'assignments\' whose value is a list of n integers.\n Each integer is an agent number (using 1-indexing) that is assigned to the corresponding job.\n """\n # For illustration purposes, we provide a trivial solution that assigns every job to agent 1.\n assignments = [1] * kwargs[\'n\']\n return {\'assignments\': assignments}' +task_description = '("The Generalized Assignment Problem (GAP) involves assigning \\( n \\) jobs to \\( m \\) agents such "' + + +__all__ = ['GAPEvaluationCB'] + + +class GAPEvaluationCB(Evaluation): + + def __init__(self, + timeout_seconds=50, + **kwargs): + + """ + Args: + None + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + # Load datasets from Hugging Face + dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Generalised assignment problem") + self._datasets = {} + for filename in dataset: + # Join all text rows into a single string + text_content = '\n'.join([row['text'] for row in dataset[filename]]) + self._datasets[filename] = text_content + + def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: + return self.evaluate(callable_func) + + def evaluate(self, eva: callable) -> float | None: + ins_cases = [] + for case_id, ins in enumerate(self._datasets.values()): + ins_cases.append(self.load_data(ins)) + + fitness_list = [] + try: + for i in ins_cases: + for j in i: + result = eva(j['m'], j['n'], j['cost_matrix'], j['consumption_matrix'], j['capacities'], j['problem_type']) + fitness = self.eval_func(j['m'], j['n'], j['cost_matrix'], j['consumption_matrix'], j['capacities'], result['assignments']) + fitness_list.append(fitness) + + return -np.mean(fitness_list) + + except ValueError as e: + print(e) + return None + + def load_data(self, input_string): + """ + Load and parse the input file for the Generalised Assignment Problem (GAP). + The input is expected to be a whitespace‐delimited text file with the following format: + - The first token is an integer P, indicating the number of cases. + - For each case, the following tokens are provided sequentially: + • Two integers: m (number of agents) and n (number of jobs). + • m×n numbers representing the cost matrix (row by row). + • m×n numbers representing the resource consumption matrix (row by row). + • m numbers representing the capacities for each agent. + Parameters: + input_file_path: (str) Path to the input text file. + Returns: + A list of dictionaries. Each dictionary corresponds to one case and contains the keys: + 'm', 'n', 'cost_matrix', 'consumption_matrix', and 'capacities'. + """ + cases = [] + try: + tokens = input_string.split() + except Exception as e: + raise Exception("Error reading input file: " + str(e)) + + ptr = 0 + try: + P = int(tokens[ptr]) + ptr += 1 + except Exception as e: + raise Exception("Error parsing the number of cases: " + str(e)) + + for _ in range(P): + try: + m = int(tokens[ptr]) + n = int(tokens[ptr + 1]) + ptr += 2 + except Exception as e: + raise Exception("Error parsing m and n for a case: " + str(e)) + + cost_matrix = [] + for i in range(m): + row = [] + for j in range(n): + try: + row.append(float(tokens[ptr])) + except Exception as e: + raise Exception("Error reading cost matrix value: " + str(e)) + ptr += 1 + cost_matrix.append(row) + + consumption_matrix = [] + for i in range(m): + row = [] + for j in range(n): + try: + row.append(float(tokens[ptr])) + except Exception as e: + raise Exception("Error reading consumption matrix value: " + str(e)) + ptr += 1 + consumption_matrix.append(row) + + capacities = [] + for i in range(m): + try: + capacities.append(float(tokens[ptr])) + except Exception as e: + raise Exception("Error reading capacity value: " + str(e)) + ptr += 1 + # Determine problem type based on content analysis or default to 'max' + # Since we don't have file name, we'll default to 'max' for now + problem_type = 'max' + + case = { + 'm': m, + 'n': n, + 'cost_matrix': cost_matrix, + 'consumption_matrix': consumption_matrix, + 'capacities': capacities, + 'problem_type': problem_type + } + cases.append(case) + + return cases + + def eval_func(self, m, n, cost_matrix, consumption_matrix, capacities, assignments, **kwargs): + """ + Evaluate a solution for a single case of the Generalised Assignment Problem (GAP). + Parameters: + - m: (int) Number of agents. + - n: (int) Number of jobs. + - cost_matrix: (list of list of float) The cost matrix of size m×n. + - consumption_matrix: (list of list of float) The resource consumption matrix of size m×n. + - capacities: (list of float) The resource capacities for each of the m agents. + - assignments: (list of int) A list of n integers (using 1-indexing) representing the agent + assigned to each job. + Evaluation: + - TotalCost is computed as the sum of cost_matrix[agent-1][j] for each job j. + - For each agent i, ResourceConsumption[i] is the sum of consumption_matrix[i][j] for jobs assigned to agent i. + - If an agent’s ResourceConsumption exceeds its capacity, a ValueError is raised. + - For a maximization problem, the score is simply the TotalCost. + (For minimization problems, you might use the negative of TotalCost.) + Returns: + A numeric score (float) evaluating the quality of the solution. + """ + total_cost = 0.0 + agent_consumption = [0.0] * m + + # Check if the number of assignments matches the number of jobs. + if len(assignments) != n: + raise ValueError("Malformed solution: number of assignments does not match the number of jobs.") + + # Process each job. + for j in range(n): + agent = assignments[j] + # Check if the assigned agent is valid (using 1-indexing). + if agent < 1 or agent > m: + raise ValueError(f"Invalid agent number {agent} for job {j}. Must be between 1 and {m}.") + agent_index = agent - 1 + total_cost += cost_matrix[agent_index][j] + agent_consumption[agent_index] += consumption_matrix[agent_index][j] + + # Check capacity constraints for each agent. + for i in range(m): + if agent_consumption[i] > capacities[i]: + raise ValueError( + f"Capacity constraint violated for agent {i + 1}: consumption {agent_consumption[i]} exceeds capacity {capacities[i]}.") + + # For a feasible solution, return the total cost as the score (for a maximization problem). + return total_cost + + def norm_score(self, results): + # Pre-defined optimal scores for each test case. + optimal_scores = { + "gap1.txt": [336.0, 327.0, 339.0, 341.0, 326.0], + "gap10.txt": [958.0, 963.0, 960.0, 947.0, 947.0], + "gap11.txt": [1139.0, 1178.0, 1195.0, 1171.0, 1171.0], + "gap12.txt": [1451.0, 1449.0, 1433.0, 1447.0, 1446.0], + "gap2.txt": [434.0, 436.0, 420.0, 419.0, 428.0], + "gap3.txt": [580.0, 564.0, 573.0, 570.0, 564.0], + "gap4.txt": [656.0, 644.0, 673.0, 647.0, 664.0], + "gap5.txt": [563.0, 558.0, 564.0, 568.0, 559.0], + "gap6.txt": [761.0, 759.0, 758.0, 752.0, 747.0], + "gap7.txt": [942.0, 949.0, 968.0, 945.0, 951.0], + "gap8.txt": [1133.0, 1134.0, 1141.0, 1117.0, 1127.0], + "gap9.txt": [709.0, 717.0, 712.0, 723.0, 706.0], + "gapa.txt": [1698, 3235, 1360, 2623, 1158, 2339], + "gapb.txt": [1843, 3553, 1407, 2831, 1166, 2340], + "gapc.txt": [1931, 3458, 1403, 2814, 1244, 2397], + "gapd.txt": [6373, 12796, 6379, 12601, 6269, 12452], + } + + normed = {} + for case, (scores, error_message) in results.items(): + if case not in optimal_scores: + continue # Skip if there's no optimal score defined. + optimal_list = optimal_scores[case] + normed_scores = [] + # Compute normalized score for each index. + if 'gapa.txt' in case or 'gapb.txt' in case or 'gapc.txt' in case or 'gapd.txt' in case: + problem_type = 'min' + else: + problem_type = 'max' + for idx, score in enumerate(scores): + if isinstance(score, (int, float)): + if problem_type == 'min': + normed_scores.append(optimal_list[idx] / score) + else: + normed_scores.append(score / optimal_list[idx]) + else: + normed_scores.append(score) + normed[case] = (normed_scores, error_message) + + return normed + + def get_dev(self): + dev = {'gap1.txt': [2, 3], 'gap10.txt': [2, 0], 'gap11.txt': [3, 0], 'gap12.txt': [3, 1], 'gap2.txt': [2, 1], + 'gap3.txt': [2, 1], 'gap4.txt': [2, 0], 'gap5.txt': [1, 4], 'gap6.txt': [2, 0], 'gap7.txt': [4, 1], + 'gap8.txt': [1, 4], 'gap9.txt': [1, 4], 'gapa.txt': [4, 0, 2], 'gapb.txt': [3, 2, 0], + 'gapc.txt': [3, 2, 0], + 'gapd.txt': [5, 4, 1]} + + return dev + + + + + +# Task configuration for benchmark task +ENTRY_NAME = 'solve' +FUNCTION_SIGNATURE = 'def solve(...):' +IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' +TASK_DESCRIPTION = '("The Generalized Assignment Problem (GAP) involves assigning \\( n \\) jobs to \\( m \\) agents such "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The Generalized Assignment Problem (GAP) involves assigning \\( n \\) jobs to \\( m \\) agents such "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(m:int, n:int, cost_matrix:list, consumption_matrix:list, capacities:list, problem_type:str=\'max\') -> dict:\n """\n Solve the Generalised Assignment Problem (GAP) for a single case.\n Input arguments (passed as keyword arguments):\n - m: (int) Number of agents.\n - n: (int) Number of jobs.\n - cost_matrix: (list of list of float) A matrix of size m×n where cost_matrix[i][j]\n represents the cost of assigning job j to agent i.\n - consumption_matrix: (list of list of float) A matrix of size m×n where consumption_matrix[i][j]\n represents the resource consumed when job j is assigned to agent i.\n - capacities: (list of float) A list of length m containing the resource capacity for each agent.\n - problem_type: (str, optional) Indicates whether the problem is a \'max\' or \'min\' problem.\n Defaults to \'max\'.\n Returns:\n A dictionary with the key \'assignments\' whose value is a list of n integers.\n Each integer is an agent number (using 1-indexing) that is assigned to the corresponding job.\n """\n # For illustration purposes, we provide a trivial solution that assigns every job to agent 1.\n assignments = [1] * kwargs[\'n\']\n return {\'assignments\': assignments}' +EVAL_CLASS_NAME = 'GAPEvaluationCB' +EVAL_KWARGS = {'timeout_seconds': 60} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_generalised_assignment_problem/paras.yaml b/examples/benchmark_tasks/optimization_generalised_assignment_problem/paras.yaml new file mode 100644 index 00000000..b2f09843 --- /dev/null +++ b/examples/benchmark_tasks/optimization_generalised_assignment_problem/paras.yaml @@ -0,0 +1,2 @@ +name: GAPEvaluationCB +timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_graph_colouring/__init__.py b/examples/benchmark_tasks/optimization_graph_colouring/__init__.py new file mode 100644 index 00000000..438e60c6 --- /dev/null +++ b/examples/benchmark_tasks/optimization_graph_colouring/__init__.py @@ -0,0 +1,372 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_graph_colouring +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# References: +# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language +# model agents in algorithm search for combinatorial optimization. +# arXiv preprint arXiv:2504.04310 (2025). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any +import numpy as np +from llm4ad_loader import Evaluation +from llm4ad_loader import load_subdir_as_text +# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader +# from llm4ad.task.optimization.co_bench.graph_colouring_co_bench.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n: int, edges: list, adjacency: dict) -> dict:\n """\n Problem:\n Given a graph in DIMACS format (with vertices, edges, and an adjacency list),\n assign a positive integer color to each vertex (1..n) so that no two adjacent vertices\n share the same color. The objective is to use as few colors as possible.\n Input kwargs:\n The keyword arguments are expected to include at least:\n - n: int (int), the number of vertices.\n - edges: list of (u, v) tuples (tuple of int (int), int (int)) representing edges.\n - adjacency: dict mapping each vertex (1..n) (int) to a set of its adjacent vertices (set of int).\n Evaluation Metric:\n Let k be the number of distinct colors used.\n For every edge connecting two vertices with the same color, count one conflict ( C ).\n If C > 0 , the solution is invalid and receives no score.\n Otherwise, the score is simply k , with a lower k being better.\n Returns:\n A dictionary representing the solution, mapping each vertex_id (1..n) to a positive integer color.\n """\n ## placeholder.\n return {} # Replace {} with the actual solution dictionary when implemented.' +task_description = '("Given a graph in DIMACS format with vertices, edges, and an adjacency list, the goal is to "' + + +__all__ = ['GCEvaluationCB'] + + +class GCEvaluationCB(Evaluation): + + def __init__(self, + timeout_seconds=50, + **kwargs): + + """ + Args: + None + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + # Load datasets from Hugging Face + dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Graph colouring") + self._datasets = {} + for filename in dataset: + # Join all text rows into a single string + text_content = '\n'.join([row['text'] for row in dataset[filename]]) + self._datasets[filename] = text_content + + def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: + return self.evaluate(callable_func) + + def evaluate(self, eva: callable) -> float | None: + ins_cases = [] + for case_id, ins in enumerate(self._datasets.values()): + ins_cases.append(self.load_data(ins)) + + fitness_list = [] + try: + for i in ins_cases: + for j in i: + result = eva(j['n'], j['edges'], j['adjacency']) + fitness = self.eval_func(n=j['n'], adjacency=j['adjacency'], result=result) + fitness_list.append(fitness) + + return -np.mean(fitness_list) + + except ValueError as e: + print(e) + return None + + def load_data(self, input_string): + """ + Reads the input DIMACS string, which may contain one or more cases. + Each case is separated by a header line (starting with "p"). For each case, the function: + - Ignores blank lines and comment lines (starting with "c"). + - Parses the header line ("p edge ") if present; if absent, determines n from edge listings. + - Parses each edge line (starting with "e") to extract the edge (u,v). + - Builds an adjacency list mapping each vertex (from 1 to n) to its adjacent vertices. + Returns: + A list where each element is a dictionary containing the data for one case. + Each dictionary has at least the following keys: + - 'n': int, number of vertices. + - 'edges': list of (u, v) tuples. + - 'adjacency': dict mapping vertex (1..n) to a set of adjacent vertices. + """ + + all_lines = [line.strip() for line in input_string.split('\n')] + + cases = [] + current_case_lines = [] + found_header = False + + # Separate file content into multiple cases based on header lines ("p ...") + for line in all_lines: + stripped = line.strip() + if not stripped or stripped.startswith("c"): + continue # skip blank lines and comments + if stripped.startswith("p"): + # Start of a new case: if current_case_lines not empty, finish previous case. + if current_case_lines: + cases.append(current_case_lines) + current_case_lines = [] + found_header = True + current_case_lines.append(stripped) + if current_case_lines: + cases.append(current_case_lines) + + # If no header line was found in the entire file, treat entire file as one case. + if not found_header and not cases: + # Filter out blank lines and comments from all_lines and treat as single case. + cases = [[line for line in all_lines if line.strip() and not line.strip().startswith("c")]] + + case_data_list = [] + # Process each case's lines. + for case_lines in cases: + n = None # number of vertices + edges = [] + vertices_found = set() + + for line in case_lines: + parts = line.split() + if parts[0] == "p": + # Expected format: p edge + if len(parts) < 4: + raise ValueError("Problem line malformed: " + line) + try: + n = int(parts[2]) + except Exception as e: + raise ValueError("Error parsing problem line: " + str(e)) + elif parts[0] == "e": + # Expected format: e + if len(parts) < 3: + raise ValueError("Edge line malformed: " + line) + try: + u = int(parts[1]) + v = int(parts[2]) + edges.append((u, v)) + vertices_found.update([u, v]) + except Exception as e: + raise ValueError("Error parsing edge line: " + str(e)) + # If n was not provided in the header, use the maximum vertex id found. + if n is None: + if vertices_found: + n = max(vertices_found) + else: + raise ValueError("No vertex information found in input.") + + # Build adjacency list. + adjacency = {i: set() for i in range(1, n + 1)} + for (u, v) in edges: + if u in adjacency: + adjacency[u].add(v) + if v in adjacency: + adjacency[v].add(u) + + case_data_list.append({ + 'n': n, + 'edges': edges, + 'adjacency': adjacency + }) + + return case_data_list + + def eval_func(self, **kwargs): + """ + Evaluates a solution for a single case. + Expected kwargs: + - 'n': int, number of vertices. + - 'adjacency': dict mapping each vertex (1..n) to a set of adjacent vertices. + - Plus all key-value pairs from the solution dictionary produced by solve, + mapping vertex ids to assigned positive integer colors. + Evaluation: + - Verifies that every vertex from 1 to n is assigned a positive integer color. + - For each edge (u,v), if the assigned colors are the same, counts as a conflict. + - Let C be the total number of conflicts and k be the number of distinct colors used. + - If C > 0, the solution is invalid and an error is raised. + - If C == 0, the score is simply k (lower is better). + Returns: + A scalar score (integer or float) representing the evaluation of the solution. + """ + # Extract expected case data. + try: + n = kwargs['n'] + adjacency = kwargs['adjacency'] + except KeyError as e: + raise KeyError("Missing required case data key: " + str(e)) + + # The solution should include an assignment for every vertex (1..n). + result = kwargs.get('result', {}) + solution = {k: v for k, v in result.items() if isinstance(k, int) or (isinstance(k, str) and k.isdigit())} + # Normalize keys to integers. + normalized_solution = {} + for key, value in solution.items(): + try: + vertex = int(key) + except Exception: + continue + normalized_solution[vertex] = value + + expected_vertices = set(range(1, n + 1)) + if set(normalized_solution.keys()) != expected_vertices: + raise ValueError("The solution must assign a color to every vertex from 1 to " + str(n)) + + # Check that every color is a positive integer. + for v, color in normalized_solution.items(): + if not (isinstance(color, int) and color >= 1): + raise ValueError("Invalid color for vertex {}: {}. Colors must be positive integers.".format(v, color)) + + # Count conflicts: for each edge, if both endpoints have the same color, count a conflict. + conflict_count = 0 + for u in range(1, n + 1): + for v in adjacency[u]: + if u < v: # count each edge only once + if normalized_solution[u] == normalized_solution[v]: + conflict_count += 1 + + if conflict_count > 0: + raise ValueError("Invalid coloring: {} conflict(s) found.".format(conflict_count)) + + num_colors = len(set(normalized_solution.values())) + score = num_colors + + return score + + def norm_score(self, results): + optimal_scores = { + "gcol1.txt": [15], + "gcol10.txt": [15], + "gcol11.txt": [15], + "gcol12.txt": [15], + "gcol13.txt": [15], + "gcol14.txt": [15], + "gcol15.txt": [15], + "gcol16.txt": [15], + "gcol17.txt": [15], + "gcol18.txt": [15], + "gcol19.txt": [15], + "gcol2.txt": [15], + "gcol20.txt": [15], + "gcol21.txt": [34], + "gcol22.txt": [34], + "gcol23.txt": [34], + "gcol24.txt": [34], + "gcol25.txt": [34], + "gcol26.txt": [34], + "gcol27.txt": [34], + "gcol28.txt": [34], + "gcol29.txt": [34], + "gcol3.txt": [15], + "gcol30.txt": [34], + "gcol4.txt": [15], + "gcol5.txt": [15], + "gcol6.txt": [15], + "gcol7.txt": [15], + "gcol8.txt": [15], + "gcol9.txt": [15] + } + + normed = {} + for case, (scores, error_message) in results.items(): + if case not in optimal_scores: + continue # Skip if there's no optimal score defined. + optimal_list = optimal_scores[case] + normed_scores = [] + # Compute normalized score for each index. + for idx, score in enumerate(scores): + if isinstance(score, (int, float)): + normed_scores.append(optimal_list[idx] / score) + else: + normed_scores.append(score) + normed[case] = (normed_scores, error_message) + + return normed + + def get_dev(self): + dev = {'gcol1.txt': [0], 'gcol11.txt': [0], 'gcol13.txt': [0], + 'gcol15.txt': [0], 'gcol17.txt': [0], 'gcol19.txt': [0], + 'gcol21.txt': [0], 'gcol23.txt': [0], 'gcol25.txt': [0], + 'gcol27.txt': [0], 'gcol29.txt': [0], 'gcol3.txt': [0], + 'gcol5.txt': [0], 'gcol7.txt': [0], 'gcol9.txt': [0]} + + return dev + + + + + +# Task configuration for benchmark task +ENTRY_NAME = 'solve' +FUNCTION_SIGNATURE = 'def solve(...):' +IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' +TASK_DESCRIPTION = '("Given a graph in DIMACS format with vertices, edges, and an adjacency list, the goal is to "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("Given a graph in DIMACS format with vertices, edges, and an adjacency list, the goal is to "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n: int, edges: list, adjacency: dict) -> dict:\n """\n Problem:\n Given a graph in DIMACS format (with vertices, edges, and an adjacency list),\n assign a positive integer color to each vertex (1..n) so that no two adjacent vertices\n share the same color. The objective is to use as few colors as possible.\n Input kwargs:\n The keyword arguments are expected to include at least:\n - n: int (int), the number of vertices.\n - edges: list of (u, v) tuples (tuple of int (int), int (int)) representing edges.\n - adjacency: dict mapping each vertex (1..n) (int) to a set of its adjacent vertices (set of int).\n Evaluation Metric:\n Let k be the number of distinct colors used.\n For every edge connecting two vertices with the same color, count one conflict ( C ).\n If C > 0 , the solution is invalid and receives no score.\n Otherwise, the score is simply k , with a lower k being better.\n Returns:\n A dictionary representing the solution, mapping each vertex_id (1..n) to a positive integer color.\n """\n ## placeholder.\n return {} # Replace {} with the actual solution dictionary when implemented.' +EVAL_CLASS_NAME = 'GCEvaluationCB' +EVAL_KWARGS = {'timeout_seconds': 60} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_graph_colouring/paras.yaml b/examples/benchmark_tasks/optimization_graph_colouring/paras.yaml new file mode 100644 index 00000000..25ba1092 --- /dev/null +++ b/examples/benchmark_tasks/optimization_graph_colouring/paras.yaml @@ -0,0 +1,2 @@ +name: GCEvaluationCB +timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_hybrid_reentrant_shop_scheduling/__init__.py b/examples/benchmark_tasks/optimization_hybrid_reentrant_shop_scheduling/__init__.py new file mode 100644 index 00000000..aa9990f8 --- /dev/null +++ b/examples/benchmark_tasks/optimization_hybrid_reentrant_shop_scheduling/__init__.py @@ -0,0 +1,564 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_hybrid_reentrant_shop_scheduling +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# References: +# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language +# model agents in algorithm search for combinatorial optimization. +# arXiv preprint arXiv:2504.04310 (2025). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any +import numpy as np +from llm4ad_loader import Evaluation +from llm4ad_loader import load_subdir_as_text +# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader +# from llm4ad.task.optimization.co_bench.hybrid_reentrant_shop_scheduling_co_bench.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n_jobs: int, n_machines: int, init_time: int, setup_times: list, processing_times: list, **kwargs) -> dict:\n """\n Input:\n - n_jobs: Integer; the number of jobs.\n - n_machines: Integer; the number of primary machines.\n - init_time: Integer; the initialization time for every job on a primary machine.\n - setup_times: List of integers; the setup times for each job on the remote server.\n - processing_times: List of integers; the processing times for each job in the main processing stage.\n Output:\n A dictionary with the following keys:\n - \'permutation\': A list of integers of length n_jobs. This list represents the order in which the jobs are processed on the remote server.\n - \'batch_assignment\': A list of integers of length n_jobs. Each element indicates the primary machine to which the corresponding job (or batch) is assigned.\n """\n\n # TODO: Implement the solution logic.\n\n # Placeholder return\n n_jobs = kwargs[\'n_jobs\']\n return {\n \'permutation\': list(range(1, n_jobs + 1)),\n \'batch_assignment\': [1 if i % 2 == 0 else 2 for i in range(n_jobs)]\n }' +task_description = '("The problem is a Hybrid Reentrant Shop Scheduling problem where each of n jobs must sequentially "' + + +__all__ = ['HRSSEvaluationCB'] + + +class HRSSEvaluationCB(Evaluation): + + def __init__(self, + timeout_seconds=50, + **kwargs): + + """ + Args: + None + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + # Load datasets from Hugging Face with fallback + dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Hybrid Reentrant Shop Scheduling") + self._datasets = {} + for filename in dataset: + # Join all text rows into a single string + text_content = '\n'.join([row['text'] for row in dataset[filename]]) + self._datasets[filename] = text_content + + def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: + return self.evaluate(callable_func) + + def evaluate(self, eva: callable) -> float | None: + ins_cases = [] + for case_id, ins in enumerate(self._datasets.values()): + ins_cases.append(self.load_data(ins)) + + fitness_list = [] + try: + for i in ins_cases: + for j in i: + result = eva(j['n_jobs'], j['n_machines'], j['init_time'], j['setup_times'], j['processing_times']) + fitness = self.eval_func(j['n_jobs'], j['n_machines'], j['init_time'], j['setup_times'], j['processing_times'], result['permutation'], result['batch_assignment']) + fitness_list.append(fitness) + + return -np.mean(fitness_list) + + except ValueError as e: + print(e) + return None + + def load_data(self, input_string): + """ + Reads the input string and parses one or more problem instances. + The input is expected to have one or more instances separated by lines that contain only dashes (e.g., "-----"). + Each instance must include exactly 4 nonempty lines: + 1. Header line: "Number of jobs: X Number of machines: Y" + 2. Initialization time: "Initialization time: Z" + 3. Setup times: "Setup times: t1 t2 ... tX" + 4. Processing times: "Processing times: p1 p2 ... pX" + Returns: + A list of dictionaries. Each dictionary corresponds to a problem instance and contains the keys: + - 'n_jobs': integer + - 'n_machines': integer + - 'init_time': integer + - 'setup_times': list of integers + - 'processing_times': list of integers + """ +import os, sys +sys.path.insert(0, os.path.dirname(__file__)) + import re + cases = [] + lines = [line.strip() for line in input_string.split('\n') if line.strip() != ''] + + # Split the file into separate instance blocks using a line of dashes as delimiter. + instance_blocks = [] + current_block = [] + for line in lines: + if re.match(r'^-+$', line): + if current_block: + instance_blocks.append(current_block) + current_block = [] + else: + current_block.append(line) + if current_block: + instance_blocks.append(current_block) + + # Process each instance block. + for block in instance_blocks: + if len(block) < 4: + raise ValueError("Invalid instance format: each instance must contain at least 4 nonempty lines.") + + # Line 1: Extract number of jobs and number of machines. + header_line = block[0] + m_jobs = re.search(r'Number of jobs:\s*(\d+)', header_line) + m_machines = re.search(r'Number of machines:\s*(\d+)', header_line) + if not m_jobs or not m_machines: + raise ValueError("Invalid header format in instance: '{}'".format(header_line)) + n_jobs = int(m_jobs.group(1)) + n_machines = int(m_machines.group(1)) + + # Line 2: Initialization time. + m_init = re.search(r'Initialization time:\s*(\d+)', block[1]) + if not m_init: + raise ValueError("Invalid initialization time line: '{}'".format(block[1])) + init_time = int(m_init.group(1)) + + # Line 3: Setup times. + m_setup = re.search(r'Setup times:\s*(.*)', block[2]) + if not m_setup: + raise ValueError("Invalid setup times line: '{}'".format(block[2])) + setup_str = m_setup.group(1).strip() + setup_times = list(map(int, setup_str.split())) + if len(setup_times) != n_jobs: + raise ValueError( + "Number of setup times ({}) does not match number of jobs ({})".format(len(setup_times), n_jobs)) + + # Line 4: Processing times. + m_process = re.search(r'Processing times:\s*(.*)', block[3]) + if not m_process: + raise ValueError("Invalid processing times line: '{}'".format(block[3])) + process_str = m_process.group(1).strip() + processing_times = list(map(int, process_str.split())) + if len(processing_times) != n_jobs: + raise ValueError( + "Number of processing times ({}) does not match number of jobs ({})".format(len(processing_times), + n_jobs)) + + case = { + 'n_jobs': n_jobs, + 'n_machines': n_machines, + 'init_time': init_time, + 'setup_times': setup_times, + 'processing_times': processing_times + } + cases.append(case) + + return cases + + def eval_func(self, n_jobs, n_machines, init_time, setup_times, processing_times, permutation, batch_assignment): + """ + 1. Initialization on one of m identical primary machines: + - Jobs are processed in natural order (1, 2, …, n_jobs) using list scheduling. + - In this phase, each job takes 'init_time'. The machine assignment is determined + by the list scheduling, and that assignment is used for the final main processing. + 2. Setup on the remote server: + - Jobs are processed in the order specified by 'permutation' (a 1-indexed list). + - A job's setup can start only after its initialization is complete and when the + remote server is free. The setup time for job j is given as setup_times[j-1]. + 3. Main processing on primary machines: + - Each job is processed on the same primary machine that performed its initialization. + - Within each machine, jobs are processed in the natural order (i.e., in order of their job indices). + - The processing time for job j is given by processing_times[j-1]. + The makespan is defined as the time when the last job completes its main processing. + Parameters: + - n_jobs: Integer; number of jobs. + - n_machines: Integer; number of primary machines. + - init_time: Integer; initialization time for each job. + - setup_times: List of integers; setup times for each job on the remote server. + - processing_times: List of integers; processing times for each job in main processing. + - permutation: List of integers of length n_jobs; a permutation (1-indexed) representing the order + in which jobs are processed on the remote server. + Returns: + A scalar (float or integer) representing the makespan (total completion time). + Raises: + ValueError: if any input constraint is not met. + """ + import heapq + + # --- Input Validation --- + if len(setup_times) != n_jobs: + raise ValueError("Length of setup_times must equal n_jobs.") + if len(processing_times) != n_jobs: + raise ValueError("Length of processing_times must equal n_jobs.") + if len(permutation) != n_jobs or sorted(permutation) != list(range(1, n_jobs + 1)): + raise ValueError("permutation must be a valid permutation of the job indices 1 through n_jobs.") + + # --- Operation 1: Initialization on Primary Machines --- + # Jobs are initialized in natural order using list scheduling. + # We keep track of both finish time and the machine used for each job. + op1_finish = [0] * (n_jobs + 1) # op1_finish[j] for job j (1-indexed) + machine_assignment = [0] * (n_jobs + 1) # Which machine processed job j + # Create a heap of available machines with tuples (next_available_time, machine_id) + machine_heap = [(0, machine_id) for machine_id in range(1, n_machines + 1)] + heapq.heapify(machine_heap) + + for job in range(1, n_jobs + 1): + avail_time, machine_id = heapq.heappop(machine_heap) + finish_time = avail_time + init_time + op1_finish[job] = finish_time + machine_assignment[job] = machine_id # Record the machine used for initialization. + heapq.heappush(machine_heap, (finish_time, machine_id)) + + # --- Operation 2: Setup on the Remote Server --- + op2_finish = [0] * (n_jobs + 1) # op2_finish[j] for job j (1-indexed) + current_time = 0 + for job in permutation: + start_time = max(op1_finish[job], current_time) + finish_time = start_time + setup_times[job - 1] + op2_finish[job] = finish_time + current_time = finish_time + + # --- Operation 3: Main Processing on Primary Machines --- + # We now schedule the main processing on the same primary machine + # that performed the job's initialization. + # Group jobs per machine based on machine_assignment. + jobs_by_machine = {machine_id: [] for machine_id in range(1, n_machines + 1)} + for job in range(1, n_jobs + 1): + assigned_machine = machine_assignment[job] + jobs_by_machine[assigned_machine].append(job) + # For each machine, sort jobs in natural order. + for machine_id in jobs_by_machine: + jobs_by_machine[machine_id].sort() + + op3_finish = [0] * (n_jobs + 1) + machine_finish_times = {machine_id: 0 for machine_id in range(1, n_machines + 1)} + for machine_id in range(1, n_machines + 1): + current_machine_time = machine_finish_times[machine_id] + for job in jobs_by_machine[machine_id]: + release_time = op2_finish[job] # Job is ready for main processing only after setup. + start_time = max(current_machine_time, release_time) + finish_time = start_time + processing_times[job - 1] + op3_finish[job] = finish_time + current_machine_time = finish_time + machine_finish_times[machine_id] = current_machine_time + + # --- Calculate Makespan --- + makespan = max(op3_finish) if op3_finish else 0 + return makespan + + def norm_score(self, results): + optimal_scores = { + 'hrs-10_025.txt': [821.0, 809.5, 751.5, 814.5, 792.0, 785.5, 775.0, 801.0, 846.0, 850.5, 793.5, 899.5, + 820.5, + 799.0, 765.0, 822.0, 785.0, 781.5, 819.0, 758.5, 775.0, 813.5, 800.0, 809.0, 762.5, + 796.5, + 758.0, 769.0, 771.0, 873.5, 796.0, 854.0, 808.5, 768.0, 825.5, 770.0, 840.5, 848.0, + 739.5, + 813.5, 800.0, 788.5, 782.0, 826.5, 795.0, 743.5, 789.5, 839.0, 779.0, 816.0], + 'hrs-10_05.txt': [411.5, 410.0, 385.0, 386.0, 395.0, 402.5, 401.0, 371.0, 398.0, 403.5, 407.0, 396.0, 407.5, + 376.5, 405.0, 401.5, 453.5, 408.0, 405.5, 382.5, 382.5, 386.5, 392.5, 388.5, 446.0, 417.5, + 394.5, 372.0, 403.5, 363.0, 404.5, 392.0, 411.0, 408.0, 417.0, 377.0, 421.0, 383.0, 402.5, + 399.0, 405.5, 414.0, 420.5, 377.0, 382.0, 404.5, 438.5, 401.5, 418.0, 414.5], + 'hrs-10_075.txt': [284.0, 267.5, 239.0, 269, 284, 274.0, 284, 286.0, 276, 278.5, 288, 308, 265.0, 291, 257, + 278, + 311, 277, 268.0, 290.5, 276.5, 290.0, 285.0, 298.0, 250.5, 276, 266.0, 248, 269.5, 266.0, + 265.0, 280.5, 245.5, 265, 272.5, 320.5, 302, 268.0, 266.0, 264, 288.5, 269.5, 266, 279.0, + 284.0, 284.5, 271, 283.0, 259.0, 257.0], + 'hrs-10_1.txt': [243, 267, 237, 250, 192, 273, 273, 226, 251, 242, 219, 269, 218, 229, 212.5, 266, 269, 223, + 274, 232, 225.5, 271, 287, 288, 258, 205.5, 265, 251, 268, 259, 203.0, 251, 231, 218, 225, + 252, + 250, 246, 296, 202.5, 228, 247, 223, 290, 219.5, 192, 277, 224, 273, 222.5], + 'hrs-10_125.txt': [230, 168, 210, 264, 230, 297, 210, 260, 210, 290, 180, 268, 258, 187, 224, 192, 204, 289, + 178, 236, 204, 257, 193, 251, 212, 183, 238, 205, 294, 236, 199, 238, 260, 255, 224, 260, + 197, 234, 224, 243, 209, 261, 283, 216, 212, 238, 223, 281, 238, 247], + 'hrs-10_15.txt': [208, 206, 252, 272, 213, 259, 212, 230, 216, 236, 255, 178, 215, 188, 267, 204, 190, 217, + 254, + 193, 209, 255, 172, 228, 303, 213, 211, 233, 229, 163, 296, 230, 138, 241, 191, 236, 207, + 269, + 238, 279, 239, 232, 201, 237, 226, 243, 284, 213, 202, 216], + 'hrs-10_175.txt': [207, 183, 236, 222, 243, 270, 256, 234, 191, 213, 210, 282, 263, 172, 278, 216, 275, 210, + 264, 221, 219, 261, 211, 189, 199, 207, 209, 210, 220, 270, 320, 236, 240, 205, 206, 199, + 233, 191, 194, 260, 215, 230, 219, 191, 201, 248, 169, 216, 225, 185], + 'hrs-10_2.txt': [185, 244, 166, 252, 207, 204, 220, 175, 229, 182, 200, 264, 221, 211, 203, 229, 191, 210, + 239, + 202, 200, 238, 264, 255, 192, 187, 236, 224, 192, 207, 279, 229, 198, 217, 205, 259, 240, + 228, + 200, 234, 219, 177, 191, 241, 190, 253, 235, 216, 187, 229], + 'hrs-10_25.txt': [307, 242, 226, 208, 163, 222, 254, 209, 238, 159, 196, 230, 208, 255, 231, 218, 227, 237, + 258, + 241, 213, 204, 204, 257, 195, 246, 185, 128, 213, 188, 228, 231, 255, 150, 177, 220, 214, + 197, + 286, 226, 162, 226, 210, 189, 278, 234, 218, 237, 260, 212], + 'hrs-25_025.txt': [2009.5, 1922.0, 1972.0, 2013.5, 1945.5, 2114.5, 2054.0, 1957.0, 1986.5, 2024.5, 2034.0, + 2118.5, 2016.5, 2043.5, 2009.5, 1933.5, 2028.5, 2050.5, 2066.5, 1997.0, 1926.0, 1933.0, + 2066.0, 2101.5, 1977.0, 2004.5, 2068.5, 2000.0, 2027.0, 2071.5, 1986.5, 2031.0, 2041.5, + 1992.0, 2073.0, 1940.5, 1977.0, 1892.5, 1918.0, 2071.0, 2109.5, 1949.0, 2024.0, 1955.0, + 2077.0, 1959.0, 1902.0, 2079.0, 1975.0, 2083.0], + 'hrs-25_05.txt': [965.0, 932.5, 1021.5, 1033.0, 933.5, 998.0, 1075.0, 1022.5, 1033.5, 945.5, 1027.0, 1019.5, + 955.0, 955.0, 1044.5, 1045.5, 983.0, 1016.0, 1024.0, 1016.5, 1062.0, 994.0, 983.5, 998.0, + 1019.0, 1014.5, 996.0, 950.0, 1016.5, 1035.5, 968.5, 1028.5, 1067.0, 1027.0, 1047.0, + 1012.0, + 1052.0, 1058.0, 1019.0, 1015.5, 1035.5, 1041.0, 975.0, 1040.5, 973.0, 1009.5, 1013.0, + 1041.0, + 1003.0, 996.0], + 'hrs-25_075.txt': [673.5, 690.5, 666.0, 669.0, 717.5, 696.5, 674.0, 678.0, 693.0, 674.0, 664.5, 695.5, + 733.0, + 667.0, 690.5, 658.5, 637.5, 735.0, 624.0, 640.0, 683.5, 676.0, 672.0, 691.0, 707.5, + 676.0, + 644.0, 667.5, 676.0, 667.0, 690.5, 692.5, 701.0, 667.5, 699.5, 683.0, 686.5, 660.5, + 705.5, + 663.0, 689.0, 694.0, 674.0, 659, 664.0, 694.0, 662.5, 653.0, 708.0, 679.5], + 'hrs-25_1.txt': [585, 548, 543.5, 533, 526.5, 555.0, 535.5, 528.5, 548.0, 497, 558.5, 518, 502.5, 545.5, + 541.0, + 578, 519, 543, 543, 497, 524, 556, 595, 631, 476.0, 538, 556, 553.0, 517, 533, 578, 536.0, + 619, + 547, 576, 470.0, 554, 528, 574, 521, 574, 520.5, 523, 551, 519, 506, 510, 583, 580, 531], + 'hrs-25_125.txt': [482, 635, 491, 497, 514, 557, 576, 498, 520, 532, 472, 532, 556, 462, 498, 601, 540, 526, + 528, 498, 458, 475, 549, 587, 589, 500, 481, 495.5, 464, 605, 576, 449, 525, 465, 541, + 591, + 446, 543, 477, 498, 564, 471, 488, 501, 500, 566, 541, 455, 566, 542], + 'hrs-25_15.txt': [555, 533, 546, 483, 422, 519, 442, 561, 508, 569, 510, 562, 629, 470, 441, 505, 465, 583, + 483, + 440, 540, 480, 577, 575, 458, 553, 535, 544, 418, 562, 557, 485, 497, 543, 555, 575, 480, + 608, + 632, 568, 552, 497, 544, 554, 577, 574, 481, 618, 550, 514], + 'hrs-25_175.txt': [575, 451, 442, 527, 487, 539, 486, 584, 505, 531, 472, 602, 526, 536, 488, 496, 469, 460, + 593, 544, 523, 482, 548, 516, 631, 636, 463, 580, 437, 559, 596, 594, 539, 586, 448, 647, + 532, 473, 581, 507, 532, 454, 654, 505, 542, 438, 463, 552, 544, 548], + 'hrs-25_2.txt': [561, 490, 586, 486, 469, 489, 569, 536, 578, 526, 527, 420, 526, 531, 498, 600, 611, 557, + 485, + 536, 530, 581, 519, 521, 565, 526, 482, 538, 521, 531, 538, 558, 512, 585, 558, 502, 609, + 516, + 566, 590, 495, 535, 613, 567, 576, 540, 627, 573, 482, 600], + 'hrs-25_25.txt': [573, 487, 528, 579, 510, 538, 582, 541, 495, 559, 454, 536, 506, 543, 569, 480, 544, 545, + 576, + 438, 435, 493, 472, 588, 500, 476, 593, 468, 465, 468, 497, 456, 529, 456, 572, 582, 596, + 601, + 479, 544, 523, 506, 504, 555, 522, 572, 496, 508, 591, 539], + 'hrs-50_025.txt': [4034.5, 3844.0, 4138.0, 4072.0, 4022.0, 4015.0, 4043.5, 4161.5, 3997.0, 3954.0, 3965.0, + 4100.5, 3918.0, 3969.5, 4075.0, 4084.0, 3826.5, 4037.0, 4061.5, 3999.0, 4123.0, 4157.5, + 4087.0, 4046.0, 4032.5, 3896.5, 4010.0, 4084.0, 4009.0, 3900.5, 3944.0, 3982.5, 3943.5, + 4083.5, 3988.0, 3881.0, 3963.0, 4021.5, 4093.5, 3909.0, 3950.5, 3843.5, 3897.0, 4074.0, + 4062.5, 4061.5, 3911.0, 4011.5, 4113.0, 3975.5], + 'hrs-50_05.txt': [2052.5, 2057.0, 2025.5, 2053.5, 1995.0, 2105.5, 2038.5, 2028.5, 2076.5, 2055.5, 2044.0, + 1957.5, 2039.5, 2002.5, 2009.5, 2016.5, 2006.5, 2027.0, 1998.5, 1986.0, 1990.0, 2021.5, + 2044.0, 2058.5, 2071.0, 1958.5, 2031.5, 2110.0, 2044.0, 1982.5, 2010.5, 2004.0, 2011.0, + 2002.0, 1997.5, 2035.5, 2015.0, 2065.0, 1956.5, 1966.5, 2102.0, 2001.0, 2048.5, 2020.5, + 2017.0, 2010.5, 1988.5, 1974.5, 1989.5, 2093.0], + 'hrs-50_075.txt': [1355.5, 1335.5, 1346.5, 1376.0, 1241.0, 1337.5, 1355.0, 1318.0, 1345.0, 1324.0, 1359.0, + 1353.0, 1349.5, 1280.5, 1332.5, 1318.5, 1324.5, 1374.0, 1332.5, 1338.5, 1304.5, 1349.0, + 1409.5, 1333.5, 1385.0, 1319.5, 1288.0, 1301.0, 1373.0, 1324.5, 1363.5, 1351.5, 1329.5, + 1293.5, 1337.0, 1326.5, 1357.0, 1322.5, 1370.5, 1362.0, 1328.0, 1375.5, 1322.0, 1348.5, + 1424.5, 1320.5, 1355.5, 1321.0, 1329.0, 1425.5], + 'hrs-50_1.txt': [1030.0, 1048, 1018.5, 1141, 1095, 1056.5, 1087.5, 1002, 983.0, 1179, 1126.5, 1075, 1118, + 1034, + 1088, 1009.5, 1052.5, 1115.5, 1054.5, 1114.0, 985.5, 1023.5, 1095, 1158, 1024.5, 1028, + 1046, + 1024, 1002.0, 1111, 1044.0, 1030.5, 1116.0, 1107.5, 1031, 986, 1063, 1100, 1070, 1041.5, + 1064.0, 1056, 1060, 1124, 1060.5, 1030.5, 1097, 1011, 1148, 970.0], + 'hrs-50_125.txt': [1007, 1001, 996, 1037, 1021, 924, 1071, 988, 1034, 915, 1022, 959, 911, 968, 996, 1019, + 940, + 1016, 972, 983, 999, 1079, 1015, 947, 1025, 1053, 931, 1017, 1081, 1101, 968, 1095, 1109, + 1011, 957, 1033, 1111, 1000, 1126, 1036, 1103, 1038, 927, 967, 922, 871, 1098, 939, 1092, + 1188], + 'hrs-50_15.txt': [926, 1093, 906, 987, 956, 1119, 1069, 1015, 900, 1083, 1038, 1109, 990, 974, 1047, 1013, + 989, + 1130, 1022, 1019, 979, 952, 1067, 1056, 1097, 985, 1004, 983, 968, 1056, 932, 997, 943, + 1135, + 1113, 1044, 984, 1065, 978, 951, 976, 1081, 958, 971, 1053, 973, 934, 944, 1055, 1019], + 'hrs-50_175.txt': [1097, 977, 1146, 911, 960, 1072, 1047, 1067, 1127, 1033, 917, 944, 1122, 980, 989, 959, + 1082, + 1012, 1156, 969, 969, 898, 1043, 981, 1118, 1040, 1058, 974, 952, 951, 1033, 1160, 1071, + 1077, 1043, 1054, 1094, 1026, 1026, 1087, 966, 1064, 993, 1035, 952, 1000, 1042, 946, + 1105, + 1094], + 'hrs-50_2.txt': [907, 1114, 971, 1045, 1066, 976, 1093, 1153, 1071, 943, 1018, 934, 943, 1057, 922, 1021, + 1108, + 909, 929, 1061, 932, 1001, 946, 1015, 1112, 1041, 1096, 1050, 1023, 1014, 970, 1017, 968, + 1050, + 1068, 941, 937, 994, 1046, 1009, 926, 1090, 1005, 1006, 1044, 1010, 924, 1008, 1026, 1011], + 'hrs-50_25.txt': [1040, 997, 947, 1068, 1055, 933, 911, 927, 1062, 873, 1030, 1061, 1051, 897, 1051, 970, + 1030, + 1088, 1046, 908, 996, 1014, 935, 1085, 1011, 929, 877, 1233, 1020, 1002, 1087, 960, 1149, + 1076, 1040, 1002, 994, 974, 990, 1043, 1058, 990, 1074, 1118, 965, 1008, 1061, 1099, 1037, + 1053]} + + normed = {} + for case, (scores, error_message) in results.items(): + if case not in optimal_scores: + continue # Skip if there's no optimal score defined. + optimal_list = optimal_scores[case] + normed_scores = [] + # Compute normalized score for each index. + for idx, score in enumerate(scores): + if isinstance(score, (int, float)): + normed_scores.append(optimal_list[idx] / score) + else: + normed_scores.append(score) + normed[case] = (normed_scores, error_message) + + return normed + + def get_dev(self): + dev = { + 'hrs-10_025.txt': [20, 21, 1, 6, 17, 48, 15, 43, 38, 28, 46, 18, 0, 31, 24, 9, 27, 8, 35, 2, 25, 22, 49, 5, + 33], + 'hrs-10_05.txt': [34, 43, 45, 32, 31, 24, 46, 3, 22, 36, 0, 40, 25, 17, 23, 10, 21, 20, 14, 6, 48, 28, 8, + 26, + 1], + 'hrs-10_075.txt': [38, 41, 31, 22, 12, 13, 48, 32, 27, 16, 35, 17, 34, 6, 4, 30, 26, 42, 29, 3, 18, 5, 28, + 20, + 39], + 'hrs-10_1.txt': [49, 28, 14, 7, 0, 16, 18, 25, 44, 19, 40, 38, 24, 33, 12, 3, 41, 35, 46, 9, 11, 39, 29, 8, + 5], + 'hrs-10_125.txt': [48, 40, 25, 36, 24, 20, 45, 4, 12, 17, 16, 28, 0, 11, 9, 23, 8, 6, 41, 34, 31, 35, 7, 44, + 38], + 'hrs-10_15.txt': [11, 17, 21, 14, 0, 28, 45, 4, 20, 5, 9, 32, 29, 27, 44, 49, 15, 7, 39, 46, 36, 2, 31, 3, + 1], + 'hrs-10_175.txt': [11, 27, 47, 10, 39, 20, 49, 34, 5, 38, 36, 22, 9, 14, 28, 33, 23, 37, 41, 45, 35, 12, 44, + 17, + 18], + 'hrs-10_2.txt': [34, 46, 8, 21, 6, 39, 26, 43, 4, 23, 9, 0, 35, 47, 3, 30, 24, 37, 42, 44, 7, 15, 38, 29, + 49], + 'hrs-10_25.txt': [16, 4, 3, 45, 32, 12, 1, 17, 7, 0, 49, 47, 18, 21, 25, 42, 36, 11, 30, 48, 37, 13, 8, 15, + 38], + 'hrs-25_025.txt': [18, 12, 13, 22, 8, 20, 44, 10, 47, 9, 48, 32, 27, 16, 7, 11, 25, 23, 14, 36, 17, 29, 21, + 38, + 45], + 'hrs-25_05.txt': [20, 7, 23, 8, 17, 37, 45, 38, 25, 18, 40, 35, 36, 46, 28, 16, 32, 22, 49, 31, 13, 1, 43, + 39, + 41], + 'hrs-25_075.txt': [6, 30, 23, 44, 15, 38, 24, 27, 5, 49, 39, 31, 45, 25, 11, 48, 4, 32, 21, 47, 46, 33, 12, + 19, + 29], + 'hrs-25_1.txt': [39, 0, 21, 24, 8, 40, 9, 41, 3, 34, 43, 16, 36, 26, 10, 7, 4, 25, 45, 20, 5, 11, 18, 31, + 33], + 'hrs-25_125.txt': [1, 9, 38, 6, 49, 36, 14, 11, 25, 20, 39, 22, 7, 21, 29, 8, 43, 45, 2, 35, 40, 42, 10, 13, + 30], + 'hrs-25_15.txt': [48, 46, 44, 23, 12, 26, 28, 33, 16, 30, 21, 4, 34, 9, 19, 47, 1, 13, 35, 6, 41, 2, 45, 14, + 38], + 'hrs-25_175.txt': [4, 7, 46, 14, 1, 43, 18, 47, 5, 31, 12, 35, 8, 20, 37, 33, 22, 23, 16, 17, 10, 24, 15, + 32, + 19], + 'hrs-25_2.txt': [5, 32, 47, 29, 49, 15, 23, 26, 24, 44, 35, 3, 31, 42, 46, 14, 16, 12, 6, 17, 45, 37, 20, + 22, + 25], + 'hrs-25_25.txt': [48, 16, 45, 18, 17, 0, 8, 38, 44, 15, 49, 40, 19, 41, 47, 37, 3, 27, 34, 43, 12, 39, 1, + 36, + 6], + 'hrs-50_025.txt': [4, 12, 44, 23, 33, 28, 5, 27, 1, 24, 21, 36, 18, 26, 31, 37, 48, 35, 14, 11, 29, 30, 39, + 34, + 2], + 'hrs-50_05.txt': [27, 5, 43, 46, 25, 29, 9, 2, 36, 38, 0, 10, 7, 31, 24, 22, 45, 44, 14, 1, 47, 19, 34, 6, + 35], + 'hrs-50_075.txt': [4, 16, 25, 26, 9, 1, 24, 17, 43, 47, 36, 38, 5, 44, 18, 27, 31, 2, 42, 39, 23, 41, 40, + 46, + 14], + 'hrs-50_1.txt': [6, 18, 30, 26, 27, 2, 28, 34, 15, 24, 44, 43, 1, 32, 17, 5, 16, 14, 7, 19, 25, 21, 38, 12, + 48], + 'hrs-50_125.txt': [1, 19, 32, 11, 9, 12, 7, 37, 40, 30, 15, 16, 35, 8, 18, 45, 2, 21, 46, 29, 26, 14, 25, 4, + 22], + 'hrs-50_15.txt': [49, 38, 5, 45, 27, 42, 14, 13, 16, 21, 10, 4, 48, 24, 32, 47, 15, 43, 1, 44, 31, 40, 2, + 11, + 19], + 'hrs-50_175.txt': [2, 26, 23, 19, 20, 17, 40, 27, 16, 29, 3, 30, 48, 49, 25, 39, 38, 35, 7, 6, 46, 15, 24, + 13, + 5], + 'hrs-50_2.txt': [19, 12, 24, 18, 22, 49, 7, 43, 1, 11, 33, 42, 35, 46, 25, 4, 32, 5, 3, 20, 29, 10, 37, 34, + 15], + 'hrs-50_25.txt': [35, 24, 28, 34, 18, 20, 23, 49, 13, 9, 39, 2, 38, 22, 33, 36, 46, 1, 19, 29, 3, 21, 15, + 12, + 43]} + + return dev + + + + + +# Task configuration for benchmark task +ENTRY_NAME = 'solve' +FUNCTION_SIGNATURE = 'def solve(...):' +IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' +TASK_DESCRIPTION = '("The problem is a Hybrid Reentrant Shop Scheduling problem where each of n jobs must sequentially "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The problem is a Hybrid Reentrant Shop Scheduling problem where each of n jobs must sequentially "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n_jobs: int, n_machines: int, init_time: int, setup_times: list, processing_times: list, **kwargs) -> dict:\n """\n Input:\n - n_jobs: Integer; the number of jobs.\n - n_machines: Integer; the number of primary machines.\n - init_time: Integer; the initialization time for every job on a primary machine.\n - setup_times: List of integers; the setup times for each job on the remote server.\n - processing_times: List of integers; the processing times for each job in the main processing stage.\n Output:\n A dictionary with the following keys:\n - \'permutation\': A list of integers of length n_jobs. This list represents the order in which the jobs are processed on the remote server.\n - \'batch_assignment\': A list of integers of length n_jobs. Each element indicates the primary machine to which the corresponding job (or batch) is assigned.\n """\n\n # TODO: Implement the solution logic.\n\n # Placeholder return\n n_jobs = kwargs[\'n_jobs\']\n return {\n \'permutation\': list(range(1, n_jobs + 1)),\n \'batch_assignment\': [1 if i % 2 == 0 else 2 for i in range(n_jobs)]\n }' +EVAL_CLASS_NAME = 'HRSSEvaluationCB' +EVAL_KWARGS = {'timeout_seconds': 60} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_hybrid_reentrant_shop_scheduling/paras.yaml b/examples/benchmark_tasks/optimization_hybrid_reentrant_shop_scheduling/paras.yaml new file mode 100644 index 00000000..7beaae0c --- /dev/null +++ b/examples/benchmark_tasks/optimization_hybrid_reentrant_shop_scheduling/paras.yaml @@ -0,0 +1,2 @@ +name: HRSSEvaluationCB +timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_job_shop_scheduling/__init__.py b/examples/benchmark_tasks/optimization_job_shop_scheduling/__init__.py new file mode 100644 index 00000000..7ac17d15 --- /dev/null +++ b/examples/benchmark_tasks/optimization_job_shop_scheduling/__init__.py @@ -0,0 +1,307 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_job_shop_scheduling +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# References: +# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language +# model agents in algorithm search for combinatorial optimization. +# arXiv preprint arXiv:2504.04310 (2025). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any +import numpy as np +from llm4ad_loader import Evaluation +from llm4ad_loader import load_subdir_as_text +# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader +# from llm4ad.task.optimization.co_bench.job_shop_scheduling_co_bench.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n_jobs: int, n_machines: int, times: list, machines: list) -> dict:\n """\n Solves a single job shop scheduling test case.\n Input:\n - n_jobs (int): Number of jobs.\n - n_machines (int): Number of machines (and operations per job).\n - times (list of list of int): A 2D list of processing times for each operation.\n Dimensions: n_jobs x n_machines.\n - machines (list of list of int): A 2D list specifying the machine assignment for each operation.\n Dimensions: n_jobs x n_machines. Note machine is 1-indexed.\n Output:\n solution (dict): A dictionary containing:\n - start_times (list of list of int): A 2D list of start times for each operation.\n Dimensions: n_jobs x n_machines.\n Each start time must be a non-negative integer, and the schedule must respect the following constraints:\n (i) Sequential processing: For each job, an operation cannot start until its preceding operation has finished.\n (ii) Machine exclusivity: For operations assigned to the same machine, their processing intervals must not overlap.\n The evaluation function will use the start_times to compute the makespan and verify the constraints.\n """\n\n # Extract the case parameters\n n_jobs = kwargs["n_jobs"]\n n_machines = kwargs["n_machines"]\n times = kwargs["times"]\n machines = kwargs["machines"]\n\n # TODO: Implement the scheduling algorithm here.\n # For now, we provide a dummy solution where all operations start at time 0.\n\n # Create a start_times list with dimensions n_jobs x n_machines, initializing all start times to 0.\n start_times = [[0 for _ in range(n_machines)] for _ in range(n_jobs)]\n\n # Build the solution dictionary.\n solution = {"start_times": start_times}\n\n return solution' +task_description = '("The job shop scheduling problem requires assigning non-negative integer start times to a set of "' + + +__all__ = ['JSSEvaluationCB'] + + +class JSSEvaluationCB(Evaluation): + + def __init__(self, + timeout_seconds=50, + **kwargs): + + """ + Args: + None + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + # Load datasets from Hugging Face + dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Job shop scheduling") + self._datasets = {} + for filename in dataset: + # Join all text rows into a single string + text_content = '\n'.join([row['text'] for row in dataset[filename]]) + self._datasets[filename] = text_content + + def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: + return self.evaluate(callable_func) + + def evaluate(self, eva: callable) -> float | None: + ins_cases = [] + for case_id, ins in enumerate(self._datasets.values()): + ins_cases.append(self.load_data(ins)) + + fitness_list = [] + try: + for i in ins_cases: + for j in i: + result = eva(j['n_jobs'], j['n_machines'], j['times'], j['machines']) + fitness = self.eval_func(j['n_jobs'], j['n_machines'], j['times'], j['machines'], result['start_times'], lower_bound=j['lower_bound'], upper_bound=j['upper_bound']) + fitness_list.append(fitness) + + return np.mean(fitness_list) + + except ValueError as e: + print(e) + return None + + def load_data(self, input_string): + cases = [] + lines = [line.strip() for line in input_string.split('\n') if line.strip()] # remove blank lines + + i = 0 + while i < len(lines): + # Look for a header line starting with "Nb of jobs" + if lines[i].startswith("Nb of jobs"): + # Next line contains six numbers: n_jobs, n_machines, time_seed, machine_seed, upper_bound, lower_bound + i += 1 + header_tokens = lines[i].split() + if len(header_tokens) < 6: + raise ValueError("Header line does not contain 6 values.") + n_jobs = int(header_tokens[0]) + n_machines = int(header_tokens[1]) + time_seed = int(header_tokens[2]) + machine_seed = int(header_tokens[3]) + upper_bound = int(header_tokens[4]) + lower_bound = int(header_tokens[5]) + + # Find the "Times" section + i += 1 + if not lines[i].lower().startswith("times"): + raise ValueError("Expected 'Times' section, got: " + lines[i]) + i += 1 # move to first line of times + times = [] + for _ in range(n_jobs): + # Each line should contain n_machines numbers + time_line = list(map(int, lines[i].split())) + if len(time_line) != n_machines: + raise ValueError(f"Expected {n_machines} numbers in times row, got {len(time_line)}") + times.append(time_line) + i += 1 + + # Find the "Machines" section + if i >= len(lines) or not lines[i].lower().startswith("machines"): + raise ValueError("Expected 'Machines' section, got: " + (lines[i] if i < len(lines) else "EOF")) + i += 1 # move to first line of machines + machines = [] + for _ in range(n_jobs): + machine_line = list(map(int, lines[i].split())) + if len(machine_line) != n_machines: + raise ValueError(f"Expected {n_machines} numbers in machines row, got {len(machine_line)}") + machines.append(machine_line) + i += 1 + + # Build the test case dictionary and add to the list of cases. + case = { + "n_jobs": n_jobs, + "n_machines": n_machines, + "time_seed": time_seed, + "machine_seed": machine_seed, + "upper_bound": upper_bound, + "lower_bound": lower_bound, + "times": times, + "machines": machines + } + cases.append(case) + else: + # If the current line is not a header, skip it. + i += 1 + + return cases + + def eval_func(self, n_jobs, n_machines, times, machines, start_times, **kwargs): + """ + Evaluates the solution for a job shop scheduling problem. + Input: + n_jobs (int): Number of jobs. + n_machines (int): Number of machines. + times (list of list of int): Processing times for each operation. + Dimensions: n_jobs x n_machines. + machines (list of list of int): Machine assignments for each operation. + Dimensions: n_jobs x n_machines. + start_times (list of list of int): Proposed start times for each operation. + Dimensions: n_jobs x n_machines. + kwargs: Other parameters that may be provided, which are ignored here. + Output: + score (int): The makespan, defined as the maximum completion time across all jobs. + Raises: + ValueError: If any scheduling constraints are violated. + """ + + # Check that start_times dimensions match the problem dimensions. + if len(start_times) != n_jobs: + raise ValueError(f"Expected start_times to have {n_jobs} rows, got {len(start_times)}") + for i, row in enumerate(start_times): + if len(row) != n_machines: + raise ValueError(f"Expected start_times row {i} to have {n_machines} entries, got {len(row)}") + for t in row: + if t < 0: + raise ValueError("Start times must be non-negative.") + + # Constraint (i): Sequential processing for each job. + job_completion_times = [] + for i in range(n_jobs): + current_time = None + for j in range(n_machines): + st = start_times[i][j] + pt = times[i][j] + if j == 0: + # For the first operation, simply set the finish time. + current_time = st + pt + else: + # For subsequent operations, the start time must be no earlier than the finish of the previous. + if st < current_time: + raise ValueError( + f"Job {i} operation {j} starts at {st} but previous operation finishes at {current_time}") + current_time = st + pt + job_completion_times.append(current_time) + + # Constraint (ii): Machine non-overlap. + # Build a dictionary mapping machine id to a list of (start_time, finish_time, job, op_index) + machine_schedules = {} + for i in range(n_jobs): + for j in range(n_machines): + machine_id = machines[i][j] + st = start_times[i][j] + pt = times[i][j] + finish_time = st + pt + if machine_id not in machine_schedules: + machine_schedules[machine_id] = [] + machine_schedules[machine_id].append((st, finish_time, i, j)) + + # For each machine, sort operations by start time and check for overlaps. + for machine_id, ops in machine_schedules.items(): + ops_sorted = sorted(ops, key=lambda x: x[0]) + for k in range(1, len(ops_sorted)): + prev_st, prev_finish, prev_job, prev_op = ops_sorted[k - 1] + curr_st, curr_finish, curr_job, curr_op = ops_sorted[k] + if prev_finish > curr_st: + raise ValueError( + f"Machine {machine_id}: Operation from job {prev_job}, op {prev_op} (finishing at {prev_finish}) overlaps with job {curr_job}, op {curr_op} (starting at {curr_st}).") + + # Compute the makespan as the maximum completion time among all jobs. + makespan = max(job_completion_times) + + score = kwargs['lower_bound'] / makespan + + return score + + def get_dev(self): + dev = {'tai100_20.txt': [1, 8, 0, 6, 9], 'tai15_15.txt': [1, 8, 9, 4, 5], 'tai20_15.txt': [2, 7, 0, 8, 3], + 'tai20_20.txt': [9, 7, 8, 3, 0], 'tai30_15.txt': [8, 7, 2, 5, 1], 'tai30_20.txt': [0, 5, 1, 4, 6], + 'tai50_15.txt': [9, 1, 4, 5, 6], 'tai50_20.txt': [5, 9, 7, 4, 8]} + + return dev + + + + + +# Task configuration for benchmark task +ENTRY_NAME = 'solve' +FUNCTION_SIGNATURE = 'def solve(...):' +IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' +TASK_DESCRIPTION = '("The job shop scheduling problem requires assigning non-negative integer start times to a set of "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The job shop scheduling problem requires assigning non-negative integer start times to a set of "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n_jobs: int, n_machines: int, times: list, machines: list) -> dict:\n """\n Solves a single job shop scheduling test case.\n Input:\n - n_jobs (int): Number of jobs.\n - n_machines (int): Number of machines (and operations per job).\n - times (list of list of int): A 2D list of processing times for each operation.\n Dimensions: n_jobs x n_machines.\n - machines (list of list of int): A 2D list specifying the machine assignment for each operation.\n Dimensions: n_jobs x n_machines. Note machine is 1-indexed.\n Output:\n solution (dict): A dictionary containing:\n - start_times (list of list of int): A 2D list of start times for each operation.\n Dimensions: n_jobs x n_machines.\n Each start time must be a non-negative integer, and the schedule must respect the following constraints:\n (i) Sequential processing: For each job, an operation cannot start until its preceding operation has finished.\n (ii) Machine exclusivity: For operations assigned to the same machine, their processing intervals must not overlap.\n The evaluation function will use the start_times to compute the makespan and verify the constraints.\n """\n\n # Extract the case parameters\n n_jobs = kwargs["n_jobs"]\n n_machines = kwargs["n_machines"]\n times = kwargs["times"]\n machines = kwargs["machines"]\n\n # TODO: Implement the scheduling algorithm here.\n # For now, we provide a dummy solution where all operations start at time 0.\n\n # Create a start_times list with dimensions n_jobs x n_machines, initializing all start times to 0.\n start_times = [[0 for _ in range(n_machines)] for _ in range(n_jobs)]\n\n # Build the solution dictionary.\n solution = {"start_times": start_times}\n\n return solution' +EVAL_CLASS_NAME = 'JSSEvaluationCB' +EVAL_KWARGS = {'timeout_seconds': 60} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_job_shop_scheduling/paras.yaml b/examples/benchmark_tasks/optimization_job_shop_scheduling/paras.yaml new file mode 100644 index 00000000..1921d34a --- /dev/null +++ b/examples/benchmark_tasks/optimization_job_shop_scheduling/paras.yaml @@ -0,0 +1,2 @@ +name: JSSEvaluationCB +timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_jssp_construct/__init__.py b/examples/benchmark_tasks/optimization_jssp_construct/__init__.py new file mode 100644 index 00000000..b2c5ff2e --- /dev/null +++ b/examples/benchmark_tasks/optimization_jssp_construct/__init__.py @@ -0,0 +1,289 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_jssp_construct +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# Module Name: JSSPEvaluation +# Last Revision: 2025/2/16 +# Description: Evaluates the Job Shop Scheduling Problem (JSSP). +# Given a set of jobs and machines, the goal is to schedule jobs on machines +# in a way that minimizes the total makespan (completion time of all jobs). +# This module is part of the LLM4AD project (https://github.com/Optima-CityU/llm4ad). +# +# Parameters: +# - timeout_seconds: Maximum allowed time (in seconds) for the evaluation process: int (default: 20). +# - n_instance: Number of problem instances to generate: int (default: 16). +# - n_jobs: Number of jobs to schedule: int (default: 10). +# - n_machines: Number of machines available: int (default: 5). +# +# References: +# - Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + + +from __future__ import annotations +from typing import Any, List, Tuple, Callable +import numpy as np +import matplotlib.pyplot as plt + +from llm4ad_loader import Evaluation +import os, sys +sys.path.insert(0, os.path.dirname(__file__)) +from get_instance import GetData +# from llm4ad.task.optimization.jssp_construct.get_instance import GetData # Converted from LLM4AD import +# from llm4ad.task.optimization.jssp_construct.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\n\ndef determine_next_operation(current_status, feasible_operations):\n """\n Determine the next operation to schedule based on a greedy heuristic.\n\n Args:\n current_status: A dictionary representing the current status of each machine and job.\n feasible_operations: A list of feasible operations that can be scheduled next.\n\n Returns:\n The next operation to schedule, represented as a tuple (job_id, machine_id, processing_time).\n """\n # Simple greedy heuristic: choose the operation with the shortest processing time\n next_operation = min(feasible_operations, key=lambda x: x[2])\n return next_operation' +task_description = "'" + + +__all__ = ['JSSPEvaluation'] + + +class JSSPEvaluation(Evaluation): + """Evaluator for Job Shop Scheduling Problem.""" + + def __init__(self, + timeout_seconds=20, + n_instance=16, + n_jobs=50, + n_machines=10, + **kwargs): + """ + Args: + None + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + self.n_instance = n_instance + self.n_jobs = n_jobs + self.n_machines = n_machines + getData = GetData(self.n_instance, self.n_jobs, self.n_machines) + self._datasets = getData.generate_instances() + + def evaluate_program(self, program_str: str, callable_func: Callable) -> Any | None: + return self.evaluate(callable_func) + + def plot_solution(self, schedule: List[List[Tuple[int, int, int]]], n_jobs: int, n_machines: int): + """ + Plots the schedule as a Gantt chart. + + Args: + schedule: The schedule generated by select_next_operation. + n_jobs: Number of jobs. + n_machines: Number of machines. + """ + fig, ax = plt.subplots(figsize=(10, 6)) + + # Create a color map for the jobs + colors = plt.cm.get_cmap('tab10', n_jobs) + + # Iterate over each job and its operations + for job_idx, operations in enumerate(schedule): + for operation in operations: + machine, start_time, end_time = operation + # Plot the operation as a horizontal bar with a specific color + ax.barh(machine, end_time - start_time, left=start_time, + color=colors(job_idx), label=f'Job {job_idx}') + + # Customize the plot + ax.set_xlabel('Time') + ax.set_ylabel('Machine') + ax.set_yticks(range(n_machines)) + ax.set_yticklabels([f'Machine {i}' for i in range(n_machines)]) + ax.set_title('Scheduling Gantt Chart') + + # Add a legend + handles, labels = ax.get_legend_handles_labels() + by_label = dict(zip(labels, handles)) # Remove duplicate labels + ax.legend(by_label.values(), by_label.keys(), title="Jobs", bbox_to_anchor=(1.05, 1), loc='upper left') + + plt.tight_layout() + plt.show() + + def schedule_jobs(self, processing_times, n_jobs, n_machines, eva): + """ + Schedule jobs on machines using a greedy constructive heuristic. + + Args: + processing_times: A list of lists representing the processing times of each job on each machine. + n_jobs: Number of jobs. + n_machines: Number of machines. + + Returns: + The makespan, which is the total time required to complete all jobs. + """ + # Initialize the current status of each machine and job + machine_status = [0] * n_machines # Time each machine is available + job_status = [0] * n_jobs # Time each job is available + operation_sequence = [[] for _ in range(n_jobs)] # Sequence of operations for each job + + # Initialize the list of all operations + all_operations = [] + for job_id in range(n_jobs): + for machine_id in range(n_machines): + all_operations.append((job_id, machine_id, processing_times[job_id][machine_id])) + + # Schedule operations until all are completed + while all_operations: + # Determine feasible operations + feasible_operations = [] + for operation in all_operations: + job_id, machine_id, processing_time = operation + if job_status[job_id] <= machine_status[machine_id]: + feasible_operations.append(operation) + + if len(feasible_operations) == 0: + next_operation = all_operations[0] + else: + # Determine the next operation to schedule + next_operation = eva({'machine_status': machine_status, 'job_status': job_status}, feasible_operations) + + # Schedule the next operation + job_id, machine_id, processing_time = next_operation + start_time = max(job_status[job_id], machine_status[machine_id]) + end_time = start_time + processing_time + machine_status[machine_id] = end_time + job_status[job_id] = end_time + operation_sequence[job_id].append((machine_id, start_time, end_time)) + + # Remove the scheduled operation from the list of all operations + all_operations.remove(next_operation) + + # Calculate the makespan (total time required to complete all jobs) + makespan = max(job_status) + return makespan, operation_sequence + + def evaluate(self, eva: Callable) -> float: + """ + Evaluate the constructive heuristic for JSSP. + + Args: + instance_data: List of tuples containing the processing times, number of jobs, and number of machines. + n_ins: Number of instances to evaluate. + n_jobs: Number of jobs. + n_machines: Number of machines. + eva: The constructive heuristic function to evaluate. + + Returns: + The average makespan across all instances. + """ + makespans = [] + + for instance in self._datasets[:self.n_instance]: + processing_times, n1, n2 = instance + makespan, solution = self.schedule_jobs(processing_times, n1, n2, eva) + makespans.append(makespan) + + average_makespan = np.mean(makespans) + return -average_makespan # Negative because we want to minimize the makespan + + +if __name__ == '__main__': + def determine_next_operation(current_status, feasible_operations): + """ + Determine the next operation to schedule based on a greedy heuristic. + + Args: + current_status: A dictionary representing the current status of each machine and job. + feasible_operations: A list of feasible operations that can be scheduled next. + + Returns: + The next operation to schedule, represented as a tuple (job_id, machine_id, processing_time). + """ + # Simple greedy heuristic: choose the operation with the shortest processing time + next_operation = min(feasible_operations, key=lambda x: x[2]) + return next_operation + + + tsp = JSSPEvaluation() + makespan = tsp.evaluate_program('_', determine_next_operation) + print(makespan) + +# Task configuration for benchmark task +ENTRY_NAME = 'determine_next_operation' +FUNCTION_SIGNATURE = 'def determine_next_operation(current_status, feasible_operations):' +IMPORT_HEADER = 'import numpy as np\nimport math' +TASK_DESCRIPTION = "'" +OBJECTIVE_TEXT = "You are optimizing the implementation of `determine_next_operation` for the LLM4AD task.\\n\\nTask description:\\n'\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible." +TEMPLATE_FUNCTION = 'import numpy as np\n\ndef determine_next_operation(current_status, feasible_operations):\n """\n Determine the next operation to schedule based on a greedy heuristic.\n\n Args:\n current_status: A dictionary representing the current status of each machine and job.\n feasible_operations: A list of feasible operations that can be scheduled next.\n\n Returns:\n The next operation to schedule, represented as a tuple (job_id, machine_id, processing_time).\n """\n # Simple greedy heuristic: choose the operation with the shortest processing time\n next_operation = min(feasible_operations, key=lambda x: x[2])\n return next_operation' +EVAL_CLASS_NAME = 'JSSPEvaluation' +EVAL_KWARGS = {'timeout_seconds': 30} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_jssp_construct/get_instance.py b/examples/benchmark_tasks/optimization_jssp_construct/get_instance.py new file mode 100644 index 00000000..b2950615 --- /dev/null +++ b/examples/benchmark_tasks/optimization_jssp_construct/get_instance.py @@ -0,0 +1,43 @@ +import numpy as np + + +class GetData: + def __init__(self, n_instance: int, n_jobs: int, n_machines: int): + """ + Initialize the GetData class for JSSP. + + Args: + n_instance: Number of instances to generate. + n_jobs: Number of jobs. + n_machines: Number of machines. + """ + self.n_instance = n_instance + self.n_jobs = n_jobs + self.n_machines = n_machines + + def generate_instances(self): + """ + Generate instances for the Job Shop Scheduling Problem. + + Returns: + A list of tuples, where each tuple contains: + - processing_times: A list of lists representing the processing times of each job on each machine. + - n_jobs: Number of jobs. + - n_machines: Number of machines. + """ + np.random.seed(2024) # Set seed for reproducibility + instance_data = [] + + for _ in range(self.n_instance): + # Generate random processing times for each job on each machine + # Each job has a sequence of operations, and each operation is assigned to a machine + # For simplicity, we assume each job has exactly `n_machines` operations, one for each machine + processing_times = [] + for _ in range(self.n_jobs): + # Randomly assign processing times for each machine + job_processing_times = np.random.randint(10, 100, size=self.n_machines).tolist() + processing_times.append(job_processing_times) + + instance_data.append((processing_times, self.n_jobs, self.n_machines)) + + return instance_data diff --git a/examples/benchmark_tasks/optimization_jssp_construct/paras.yaml b/examples/benchmark_tasks/optimization_jssp_construct/paras.yaml new file mode 100644 index 00000000..056940f4 --- /dev/null +++ b/examples/benchmark_tasks/optimization_jssp_construct/paras.yaml @@ -0,0 +1,2 @@ +name: JSSPEvaluation +timeout_seconds: 30 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_knapsack_construct/__init__.py b/examples/benchmark_tasks/optimization_knapsack_construct/__init__.py new file mode 100644 index 00000000..ee485d16 --- /dev/null +++ b/examples/benchmark_tasks/optimization_knapsack_construct/__init__.py @@ -0,0 +1,271 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_knapsack_construct +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# Module Name: KnapsackEvaluation +# Last Revision: 2025/2/16 +# Description: Evaluates the Knapsack Problem. +# Given a set of items with weights and values, the goal is to select a subset of items +# that maximizes the total value while not exceeding the knapsack's capacity. +# This module is part of the LLM4AD project (https://github.com/Optima-CityU/llm4ad). +# +# Parameters: +# - timeout_seconds: Maximum allowed time (in seconds) for the evaluation process: int (default: 20). +# - n_instance: Number of problem instances to generate: int (default: 16). +# - n_items: Number of items available: int (default: 20). +# - knapsack_capacity: Maximum capacity of the knapsack: int (default: 50). +# +# References: +# - Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations +from typing import Callable, Any, List, Tuple +import matplotlib.pyplot as plt + +from llm4ad_loader import Evaluation +import os, sys +sys.path.insert(0, os.path.dirname(__file__)) +from get_instance import GetData +# from llm4ad.task.optimization.knapsack_construct.get_instance import GetData # Converted from LLM4AD import +# from llm4ad.task.optimization.knapsack_construct.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\n\ndef select_next_item(remaining_capacity: int, remaining_items: List[Tuple[int, int, int]]) -> Tuple[int, int, int] | None:\n """\n Select the item with the highest value-to-weight ratio that fits in the remaining capacity.\n\n Args:\n remaining_capacity: The remaining capacity of the knapsack.\n remaining_items: List of tuples containing (weight, value, index) of remaining items.\n\n Returns:\n The selected item as a tuple (weight, value, index), or None if no item fits.\n """\n best_item = None\n best_ratio = -1 # Initialize with a negative value to ensure any item will have a higher ratio\n\n for item in remaining_items:\n weight, value, index = item\n if weight <= remaining_capacity:\n ratio = value / weight # Calculate value-to-weight ratio\n if ratio > best_ratio:\n best_ratio = ratio\n best_item = item\n\n return best_item' +task_description = "'" + + +__all__ = ['KnapsackEvaluation'] + + +class KnapsackEvaluation(Evaluation): + """Evaluator for the Knapsack Problem.""" + + def __init__(self, + timeout_seconds=20, + n_instance=32, + n_items=50, + knapsack_capacity=100, + **kwargs): + """ + Initialize the evaluator for the Knapsack Problem. + """ + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + self.n_instance = n_instance + self.n_items = n_items + self.knapsack_capacity = knapsack_capacity + getData = GetData(self.n_instance, self.n_items, self.knapsack_capacity) + self._datasets = getData.generate_instances() + + def evaluate_program(self, program_str: str, callable_func: Callable) -> Any | None: + return self.evaluate(callable_func) + + def plot_solution(self, item_weights: list, item_values: list, selected_indices: list, knapsack_capacity: int): + """ + Plot the solution of the Knapsack problem. + + Args: + item_weights: A list of item weights. + item_values: A list of item values. + selected_indices: A list of indices of selected items. + knapsack_capacity: The capacity of the knapsack. + """ + # Prepare data for plotting + selected_weights = [item_weights[i] for i in selected_indices] + selected_values = [item_values[i] for i in selected_indices] + total_weight = sum(selected_weights) + total_value = sum(selected_values) + + # Create a bar plot for selected items + fig, ax = plt.subplots() + x = range(len(selected_indices)) + ax.bar(x, selected_weights, label='Weight', color='blue', alpha=0.6) + ax.bar(x, selected_values, label='Value', color='orange', alpha=0.6, bottom=selected_weights) + + # Add labels and title + ax.set_xlabel('Selected Items') + ax.set_ylabel('Weight / Value') + ax.set_title(f'Knapsack Solution\nTotal Weight: {total_weight}/{knapsack_capacity}, Total Value: {total_value}') + ax.set_xticks(x) + ax.set_xticklabels([f'Item {i}' for i in selected_indices]) + ax.legend() + + plt.show() + + def pack_items(self, item_weights: List[int], item_values: List[int], knapsack_capacity: int, eva: Callable) -> Tuple[int, List[int]]: + """ + Select items for the knapsack using a constructive heuristic. + + Args: + item_weights: A list of item weights. + item_values: A list of item values. + knapsack_capacity: The capacity of the knapsack. + eva: The constructive heuristic function to select the next item. + + Returns: + A tuple containing: + - The total value of the selected items. + - A list of selected item indices. + """ + remaining_items = list(zip(item_weights, item_values, range(len(item_weights)))) # Track weights, values, and indices + selected_items = [] # List of selected item indices + remaining_capacity = knapsack_capacity # Track remaining capacity + total_value = 0 # Track total value of selected items + + while remaining_items and remaining_capacity > 0: + # Use the heuristic to select the next item + selected_item = eva(remaining_capacity, remaining_items) + + if selected_item is not None: + weight, value, index = selected_item + if weight <= remaining_capacity: + # Add the selected item to the knapsack + selected_items.append(index) + total_value += value + remaining_capacity -= weight + # Remove the selected item from the remaining items + remaining_items.remove(selected_item) + else: + break + + return total_value, selected_items + + def evaluate(self, eva: Callable) -> float: + """ + Evaluate the constructive heuristic for the Knapsack Problem. + + Args: + instance_data: List of tuples containing the item weights, values, and knapsack capacity. + n_ins: Number of instances to evaluate. + eva: The constructive heuristic function to evaluate. + + Returns: + The average total value of selected items across all instances. + """ + total_value = 0 + + for instance in self._datasets[:self.n_instance]: + item_weights, item_values, knapsack_capacity = instance + value, _ = self.pack_items(item_weights, item_values, knapsack_capacity, eva) + total_value += value + + average_value = total_value / self.n_instance + return -average_value # Positive because we want to maximize the total value + + +if __name__ == '__main__': + + def select_next_item(remaining_capacity: int, remaining_items: List[Tuple[int, int, int]]) -> Tuple[int, int, int] | None: + """ + Select the item with the highest value-to-weight ratio that fits in the remaining capacity. + + Args: + remaining_capacity: The remaining capacity of the knapsack. + remaining_items: List of tuples containing (weight, value, index) of remaining items. + + Returns: + The selected item as a tuple (weight, value, index), or None if no item fits. + """ + best_item = None + best_ratio = -1 # Initialize with a negative value to ensure any item will have a higher ratio + + for item in remaining_items: + weight, value, index = item + if weight <= remaining_capacity: + ratio = value / weight # Calculate value-to-weight ratio + if ratio > best_ratio: + best_ratio = ratio + best_item = item + + return best_item + + + bp1d = KnapsackEvaluation() + ave_bins = bp1d.evaluate_program('_', select_next_item) + print(ave_bins) + +# Task configuration for benchmark task +ENTRY_NAME = 'select_next_item' +FUNCTION_SIGNATURE = 'def select_next_item(...):' +IMPORT_HEADER = 'import numpy as np\nimport math' +TASK_DESCRIPTION = "'" +OBJECTIVE_TEXT = "You are optimizing the implementation of `select_next_item` for the LLM4AD task.\\n\\nTask description:\\n'\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible." +TEMPLATE_FUNCTION = 'import numpy as np\n\ndef select_next_item(remaining_capacity: int, remaining_items: List[Tuple[int, int, int]]) -> Tuple[int, int, int] | None:\n """\n Select the item with the highest value-to-weight ratio that fits in the remaining capacity.\n\n Args:\n remaining_capacity: The remaining capacity of the knapsack.\n remaining_items: List of tuples containing (weight, value, index) of remaining items.\n\n Returns:\n The selected item as a tuple (weight, value, index), or None if no item fits.\n """\n best_item = None\n best_ratio = -1 # Initialize with a negative value to ensure any item will have a higher ratio\n\n for item in remaining_items:\n weight, value, index = item\n if weight <= remaining_capacity:\n ratio = value / weight # Calculate value-to-weight ratio\n if ratio > best_ratio:\n best_ratio = ratio\n best_item = item\n\n return best_item' +EVAL_CLASS_NAME = 'KnapsackEvaluation' +EVAL_KWARGS = {'timeout_seconds': 20} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_knapsack_construct/get_instance.py b/examples/benchmark_tasks/optimization_knapsack_construct/get_instance.py new file mode 100644 index 00000000..d2d15c10 --- /dev/null +++ b/examples/benchmark_tasks/optimization_knapsack_construct/get_instance.py @@ -0,0 +1,41 @@ +import numpy as np + + +class GetData: + def __init__(self, n_instance: int, n_items: int, knapsack_capacity: int): + """ + Initialize the GetData class for the Knapsack Problem. + + Args: + n_instance: Number of instances to generate. + n_items: Number of items. + knapsack_capacity: Capacity of the knapsack. + """ + self.n_instance = n_instance + self.n_items = n_items + self.knapsack_capacity = knapsack_capacity + + def generate_instances(self): + """ + Generate instances for the Knapsack Problem. + + Returns: + A list of tuples, where each tuple contains: + - item_weights: A list of item weights. + - item_values: A list of item values. + - knapsack_capacity: The capacity of the knapsack. + """ + np.random.seed(2024) # Set seed for reproducibility + instance_data = [] + + for _ in range(self.n_instance): + # Generate random item weights, ensuring no item exceeds the knapsack capacity + item_weights = np.random.randint(10, self.knapsack_capacity / 2 + 10, size=self.n_items).tolist() + + # Generate random item values, ensuring they are positive + item_values = np.random.randint(1, 101, size=self.n_items).tolist() # Values between 1 and 100 + + # Append the instance data as a tuple (weights, values, capacity) + instance_data.append((item_weights, item_values, self.knapsack_capacity)) + + return instance_data diff --git a/examples/benchmark_tasks/optimization_knapsack_construct/paras.yaml b/examples/benchmark_tasks/optimization_knapsack_construct/paras.yaml new file mode 100644 index 00000000..22d30b48 --- /dev/null +++ b/examples/benchmark_tasks/optimization_knapsack_construct/paras.yaml @@ -0,0 +1,2 @@ +name: KnapsackEvaluation +timeout_seconds: 20 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_maximal_independent_set/__init__.py b/examples/benchmark_tasks/optimization_maximal_independent_set/__init__.py new file mode 100644 index 00000000..17883347 --- /dev/null +++ b/examples/benchmark_tasks/optimization_maximal_independent_set/__init__.py @@ -0,0 +1,261 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_maximal_independent_set +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# References: +# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language +# model agents in algorithm search for combinatorial optimization. +# arXiv preprint arXiv:2504.04310 (2025). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +import os, sys +sys.path.insert(0, os.path.dirname(__file__)) +import pathlib +import pickle +from typing import Any +import numpy as np +from llm4ad_loader import Evaluation +from llm4ad_loader import load_subdir_as_pickle +# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_pickle # Common utilities from llm4ad_loader +# from llm4ad.task.optimization.co_bench.maximal_independent_set_co_bench.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\nimport networkx as nx\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(graph: networkx.Graph):\n """\n Solve the Maximum Independent Set problem for a given test case.\n Input:\n kwargs (dict): A dictionary with the following keys:\n - graph (networkx.Graph): The graph to solve\n Returns:\n dict: A solution dictionary containing:\n - mis_nodes (list): List of node indices in the maximum independent set\n """\n # TODO: Implement your MIS solving algorithm here. Below is a placeholder.\n solution = {\n \'mis_nodes\': [0, 1, ...],\n }\n return solution' +task_description = '("The Maximum Independent Set (MIS) problem is a fundamental NP-hard optimization problem in graph "' + + +__all__ = ['MISEvaluationCB'] + + +class MISEvaluationCB(Evaluation): + + def __init__(self, + timeout_seconds=50, + **kwargs): + + """ + Args: + None + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + # Load datasets from Hugging Face as pickle files + pickle_data = load_subdir_as_pickle("CO-Bench/CO-Bench", "Maximal independent set", + include_subdirs=("er_test", "er_large_test")) + + # Organize datasets by filename (dict format preserves filenames) + self._datasets = {} + for subdir_name, graphs in pickle_data.items(): + for filename, graph in graphs.items(): + # Use filename as key, store metadata with graph as value + dataset_entry = { + 'name': filename.replace('.gpickle', ''), + 'subdir': subdir_name, + 'graph': graph, + 'filename': filename + } + self._datasets[filename] = dataset_entry + + def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: + return self.evaluate(callable_func) + + def evaluate(self, eva: callable) -> float | None: + fitness_list = [] + try: + for dataset_entry in self._datasets.values(): + # Each dataset entry already contains the graph and metadata + result = eva(dataset_entry['graph']) + fitness = self.eval_func( + name=dataset_entry['name'], + graph=dataset_entry['graph'], + mis_nodes=result['mis_nodes'], + mis_size=len(result['mis_nodes']) + ) + fitness_list.append(fitness) + + return np.mean(fitness_list) + + except ValueError as e: + print(e) + return None + + def load_data(self, input_string): + """ + Load data method for compatibility with comprehensive testing. + Since MIS task loads pickle files directly in __init__, this method + returns cases from the dictionary format. + + Args: + input_string: Dataset content (not used, but required for interface) + + Returns: + list: List of dataset entries for compatibility + """ + # Return all dataset entries as a list for compatibility with testing + return list(self._datasets.values()) + + def eval_func(self, **kwargs): + """ + Evaluate a Maximum Independent Set solution for correctness. + Args: + name (str): Name of the test case + graph (networkx.Graph): The graph that was solved + mis_nodes (list): List of nodes claimed to be in the maximum independent set + mis_size (int): Claimed size of the maximum independent set + Returns: + actual_size (int): The actual size of the provided solution + # dict: Evaluation results containing: + # - is_valid (bool): Whether the solution is a valid independent set + # - actual_size (int): The actual size of the provided solution + # - score (int): The score of the solution (0 if invalid, actual_size if valid) + # - error (str, optional): Error message if any constraint is violated + """ + + graph = kwargs['graph'] + mis_nodes = kwargs['mis_nodes'] + + # Check if mis_nodes is a list + if not isinstance(mis_nodes, list): + raise Exception("mis_nodes must be a list") + + # Check if all nodes in mis_nodes exist in the graph + node_set = set(graph.nodes()) + for node in mis_nodes: + if node not in node_set: + raise Exception(f"Node {node} in solution does not exist in graph") + + # Check for duplicates in mis_nodes + if len(mis_nodes) != len(set(mis_nodes)): + raise Exception("Duplicate nodes in solution") + + # Check if mis_size matches the length of mis_nodes + actual_size = len(mis_nodes) + + # Most important: Check if it's an independent set (no edges between any nodes) + for i in range(len(mis_nodes)): + for j in range(i + 1, len(mis_nodes)): + if graph.has_edge(mis_nodes[i], mis_nodes[j]): + raise Exception(f"Not an independent set: edge exists between {mis_nodes[i]} and {mis_nodes[j]}") + + return actual_size + + def norm_score(self, results): + optimal_scores = { + "er_large_test": [382] * 16, + "er_test": [46] * 128, + "er_valid": [46] * 100, + } + normed = {} + for case, (scores, error_message) in results.items(): + if case not in optimal_scores: + continue # Skip if there's no optimal score defined. + optimal_list = optimal_scores[case] + normed_scores = [] + # Compute normalized score for each index. + for idx, score in enumerate(scores): + if isinstance(score, (int, float)): + normed_scores.append(score / optimal_list[idx]) + else: + normed_scores.append(score) + normed[case] = (normed_scores, error_message) + + return normed + + def get_dev(self): + dev = {'er_large_test': [1, 0, 8, 10, 6], + 'er_valid': [i for i in range(100)]} + + return dev + + + + + +# Task configuration for benchmark task +ENTRY_NAME = 'solve' +FUNCTION_SIGNATURE = 'def solve(graph: networkx.Graph):' +IMPORT_HEADER = 'import numpy as np\nimport networkx as nx\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' +TASK_DESCRIPTION = '("The Maximum Independent Set (MIS) problem is a fundamental NP-hard optimization problem in graph "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The Maximum Independent Set (MIS) problem is a fundamental NP-hard optimization problem in graph "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\nimport networkx as nx\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(graph: networkx.Graph):\n """\n Solve the Maximum Independent Set problem for a given test case.\n Input:\n kwargs (dict): A dictionary with the following keys:\n - graph (networkx.Graph): The graph to solve\n Returns:\n dict: A solution dictionary containing:\n - mis_nodes (list): List of node indices in the maximum independent set\n """\n # TODO: Implement your MIS solving algorithm here. Below is a placeholder.\n solution = {\n \'mis_nodes\': [0, 1, ...],\n }\n return solution' +EVAL_CLASS_NAME = 'MISEvaluationCB' +EVAL_KWARGS = {'timeout_seconds': 60} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_maximal_independent_set/paras.yaml b/examples/benchmark_tasks/optimization_maximal_independent_set/paras.yaml new file mode 100644 index 00000000..6ea99df4 --- /dev/null +++ b/examples/benchmark_tasks/optimization_maximal_independent_set/paras.yaml @@ -0,0 +1,2 @@ +name: MISEvaluationCB +timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_multi_demand_multidimensional_knapsack_problem/__init__.py b/examples/benchmark_tasks/optimization_multi_demand_multidimensional_knapsack_problem/__init__.py new file mode 100644 index 00000000..6a9a4778 --- /dev/null +++ b/examples/benchmark_tasks/optimization_multi_demand_multidimensional_knapsack_problem/__init__.py @@ -0,0 +1,629 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_multi_demand_multidimensional_knapsack_problem +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# References: +# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language +# model agents in algorithm search for combinatorial optimization. +# arXiv preprint arXiv:2504.04310 (2025). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any +import numpy as np +from llm4ad_loader import Evaluation +from llm4ad_loader import load_subdir_as_text +# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader +# from llm4ad.task.optimization.co_bench.multi_demand_multidimensional_knapsack_problem_co_bench.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n:int, m:int, q:int, A_leq:list, b_leq:list, A_geq:list, b_geq:list, cost_vector:list, cost_type:str) -> dict:\n """\n Solve a given MDMKP test instance.\n Input (via kwargs):\n - n: int\n Number of decision variables.\n - m: int\n Number of <= constraints.\n - q: int\n Number of active >= constraints (subset of the full set).\n - A_leq: list of lists of int\n Coefficient matrix for <= constraints (dimensions: m x n).\n - b_leq: list of int\n Right-hand side for <= constraints (length m).\n - A_geq: list of lists of int\n Coefficient matrix for >= constraints (dimensions: q x n).\n - b_geq: list of int\n Right-hand side for >= constraints (length q).\n - cost_vector: list of int\n Objective function coefficients (length n).\n - cost_type: str\n Type of cost coefficients ("positive" or "mixed").\n Output:\n A dictionary with the following keys:\n - \'optimal_value\': int/float\n The optimal objective function value (if found).\n - \'x\': list of int\n Binary vector (0 or 1) representing the decision variable assignment.\n TODO: Implement the actual solution algorithm for the MDMKP instance.\n """\n # TODO: Define your model variables, constraints, and objective function.\n # For example, you might use an integer programming solver (e.g., PuLP, Gurobi, or another solver)\n # to model and solve the instance.\n\n # Placeholder solution:\n solution = {\n \'optimal_value\': None, # Replace with the computed objective value.\n \'x\': [0] * kwargs.get(\'n\', 0), # Replace with the computed decision vector.\n }\n return solution' +task_description = '("The Multi-Demand Multidimensional Knapsack Problem (MDMKP) is a binary optimization problem that "' + + +__all__ = ['MDMKPEvaluationCB'] + + +class MDMKPEvaluationCB(Evaluation): + + def __init__(self, + timeout_seconds=50, + **kwargs): + + """ + Args: + None + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + # Load datasets from Hugging Face + dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Multi-Demand Multidimensional Knapsack problem") + self._datasets = {} + for filename in dataset: + # Join all text rows into a single string + text_content = '\n'.join([row['text'] for row in dataset[filename]]) + self._datasets[filename] = text_content + + def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: + return self.evaluate(callable_func) + + def evaluate(self, eva: callable) -> float | None: + ins_cases = [] + for case_id, ins in enumerate(self._datasets.values()): + ins_cases.append(self.load_data(ins)) + + fitness_list = [] + try: + for i in ins_cases: + for j in i: + result = eva(j['n'], j['m'], j['q'], j['A_leq'], j['b_leq'], j['A_geq'], j['b_geq'], j['cost_vector'], j['cost_type']) + fitness = self.eval_func(n=j['n'], m=j['m'], q=j['q'], A_leq=j['A_leq'], b_leq=j['b_leq'], A_geq=j['A_geq'], b_geq=j['b_geq'], cost_vector=j['cost_vector'], cost_type=j['cost_type'], x=result['x']) + fitness_list.append(fitness) + + return np.mean(fitness_list) + + except ValueError as e: + print(e) + return None + + def load_data(self, input_string): + """ + Load MDMKP test instances from a given file and split each base instance into + 6 separate optimization problems. Each split instance is a dictionary with the keys: + - 'n': number of decision variables. + - 'm': number of <= constraints. + - 'q': number of active >= constraints for this variant (1, m//2, or m). + - 'A_leq': list of lists representing the <= constraints coefficients. + - 'b_leq': list representing the right-hand side for the <= constraints. + - 'A_geq': list of lists representing the active >= constraints coefficients (first q rows). + - 'b_geq': list representing the active right-hand side values (first q values). + - 'cost_vector': objective function coefficients for this variant. + - 'cost_type': either "positive" or "mixed". + The file format is assumed to be: + - The first line contains an integer K: the number of base test instances. + - For each instance: + * A line with two integers: n (number of variables) and m (number of <= constraints). + * m lines with n integers each: coefficients for the <= constraints. + * One line with m integers: right-hand side for the <= constraints. + * m lines with n integers each: coefficients for the >= constraints. + * One line with m integers: right-hand side for the >= constraints. + * 6 lines with n integers each: cost vectors. + - The first 3 lines correspond to the positive cost case (for q = 1, m//2, m). + - The next 3 lines correspond to the mixed cost case (for q = 1, m//2, m). + Returns: + A list of dictionaries, each representing one optimization problem variant. + """ + instances = [] + + lines = [line.strip() for line in input_string.split('\n') if line.strip() != ''] + + idx = 0 + try: + K = int(lines[idx]) + except Exception as e: + raise ValueError("The first line must be an integer indicating the number of test instances.") from e + idx += 1 + + for k in range(K): + # Read n and m for the base instance + try: + n, m = map(int, lines[idx].split()) + except Exception as e: + raise ValueError(f"Error reading n and m for test instance {k + 1} at line {idx + 1}.") from e + idx += 1 + + # Read the <= constraints matrix (A_leq): m lines each with n coefficients + A_leq = [] + for i in range(m): + row = list(map(int, lines[idx].split())) + if len(row) != n: + raise ValueError( + f"Test instance {k + 1}: Expected {n} coefficients for <= constraint {i + 1}, got {len(row)}.") + A_leq.append(row) + idx += 1 + + # Read the right-hand side for <= constraints (b_leq): one line with m integers + b_leq = list(map(int, lines[idx].split())) + if len(b_leq) != m: + raise ValueError( + f"Test instance {k + 1}: Expected {m} RHS values for <= constraints, got {len(b_leq)}.") + idx += 1 + + # Read the >= constraints matrix (A_geq): m lines each with n coefficients + A_geq = [] + for i in range(m): + row = list(map(int, lines[idx].split())) + if len(row) != n: + raise ValueError( + f"Test instance {k + 1}: Expected {n} coefficients for >= constraint {i + 1}, got {len(row)}.") + A_geq.append(row) + idx += 1 + + # Read the right-hand side for >= constraints (b_geq): one line with m integers + b_geq = list(map(int, lines[idx].split())) + if len(b_geq) != m: + raise ValueError( + f"Test instance {k + 1}: Expected {m} RHS values for >= constraints, got {len(b_geq)}.") + idx += 1 + + # Read 6 cost vectors (each with n integers) + cost_vectors = [] + for i in range(6): + vector = list(map(int, lines[idx].split())) + if len(vector) != n: + raise ValueError( + f"Test instance {k + 1}: Expected {n} values for cost vector {i + 1}, got {len(vector)}.") + cost_vectors.append(vector) + idx += 1 + + # Define the q values for the three cases + q_values = [1, m // 2, m] + + # Create 6 separate optimization problem variants. + # For the first three cost vectors: positive cost case. + # For the last three cost vectors: mixed cost case. + for i in range(6): + if i < 3: + cost_type = "positive" + q = q_values[i] + cost_vector = cost_vectors[i] + else: + cost_type = "mixed" + q = q_values[i - 3] + cost_vector = cost_vectors[i] + + # For the >= constraints, take only the first q rows and corresponding RHS values. + instance_variant = { + 'n': n, + 'm': m, + 'q': q, + 'A_leq': A_leq, + 'b_leq': b_leq, + 'A_geq': A_geq[:q], + 'b_geq': b_geq[:q], + 'cost_vector': cost_vector, + 'cost_type': cost_type + } + instances.append(instance_variant) + + return instances + + def eval_func(self, **kwargs): + """ + Evaluate a solution for a given MDMKP instance. + This function expects keyword arguments that include both the instance data and the solution. + It is designed to be used as: + eval_func(**case, **output) + where the instance 'case' contains the following keys: + - n: int + Number of decision variables. + - m: int + Number of <= constraints. + - q: int + Number of active >= constraints (subset of the full set). + - A_leq: list of lists of int (dimensions: m x n) + Coefficient matrix for <= constraints. + - b_leq: list of int (length m) + Right-hand side for <= constraints. + - A_geq: list of lists of int (dimensions: q x n) + Coefficient matrix for >= constraints. + - b_geq: list of int (length q) + Right-hand side for >= constraints. + - cost_vector: list of int (length n) + Objective function coefficients. + - cost_type: str + A string indicating the cost type ("positive" or "mixed"). + And the solver output (the solution) is expected to include at least: + - x: list of int + Binary decision vector (0 or 1) of length n. + The evaluation process is as follows: + 1. Check that the decision vector is of the proper length and binary. + 2. Verify that each <= constraint is satisfied: + For each i in 0,...,m-1, sum_{j=0}^{n-1} A_leq[i][j]*x[j] <= b_leq[i] + 3. Verify that each >= constraint is satisfied: + For each i in 0,...,q-1, sum_{j=0}^{n-1} A_geq[i][j]*x[j] >= b_geq[i] + 4. If all constraints are satisfied, compute the objective value as: + score = sum_{j=0}^{n-1} cost_vector[j] * x[j] + 5. Return the computed score. + If any constraint is violated or the input format is incorrect, the function raises a ValueError. + Returns: + score: int or float, the objective value computed from the solution. + """ + # Extract instance data + n = kwargs.get('n') + m = kwargs.get('m') + q = kwargs.get('q') + A_leq = kwargs.get('A_leq') + b_leq = kwargs.get('b_leq') + A_geq = kwargs.get('A_geq') + b_geq = kwargs.get('b_geq') + cost_vector = kwargs.get('cost_vector') + + # Extract solution data + x = kwargs.get('x') + + # Validate required keys + if None in (n, m, q, A_leq, b_leq, A_geq, b_geq, cost_vector): + raise ValueError("Missing one or more required instance keys for evaluation.") + if x is None: + raise ValueError("Solution output must include a decision vector 'x'.") + + # Validate decision vector: must be a list of length n and binary (0 or 1) + if not isinstance(x, list) or len(x) != n: + raise ValueError(f"Decision vector 'x' must be a list of length {n}.") + if any(val not in (0, 1) for val in x): + raise ValueError("Decision vector 'x' must be binary (0 or 1).") + + # Validate <= constraints: for each constraint, check feasibility. + for i in range(m): + lhs = sum(A_leq[i][j] * x[j] for j in range(n)) + if lhs > b_leq[i]: + raise ValueError(f"<= Constraint {i + 1} violated: computed sum {lhs} exceeds RHS {b_leq[i]}.") + + # Validate >= constraints: for each constraint, check feasibility. + for i in range(q): + lhs = sum(A_geq[i][j] * x[j] for j in range(n)) + if lhs < b_geq[i]: + raise ValueError(f">= Constraint {i + 1} violated: computed sum {lhs} is less than RHS {b_geq[i]}.") + + # Compute the objective value as the sum of cost_vector * x. + score = sum(cost_vector[j] * x[j] for j in range(n)) + + return score + + def norm_score(self, results): + optimal_scores = { + 'mdmkp_ct1.txt': [31376.06099098094, 28841.20208473961, 22192.899694276286, 10976.880429144556, + 10686.301154359677, 10776.09848748367, 27965.860247587698, 26830.224814550158, + 26539.177334530545, 11578.699254973362, 11463.552178478127, 11113.692459439326, + 30638.19628214322, 24049.619536529088, 21030.249006014095, 10643.70090091486, + 10453.7553665755, 10851.28935057092, 31109.826608630654, 27849.578986509467, + 21907.43232441284, 11084.204959504046, 10931.486053492556, 10617.154943664962, + 34067.48088690925, 31057.81683843506, 25461.483419257427, 11120.824982910157, + 10939.366732751803, 10501.68843539494, 52281.53172193436, 45115.128308760926, + 40697.32062752598, 18730.394778021, 17862.24431317923, 16294.366955720267, + 53807.60142950598, + 48358.464538058324, 38502.130166681985, 18588.950738387197, 17310.90043217735, + 16480.073883206154, 54772.51730504556, 49269.756608415206, 38624.595248448204, + 18018.418531206178, 16757.436775836548, 16430.385997855254, 54794.38354919037, + 49243.457471650145, 37733.710673328365, 18666.988427036664, 17552.919972379645, + 16714.50974311463, 54864.038253140265, 49912.99246733524, 39111.106147546845, + 17741.778795120015, 17454.746367665648, 15996.305483915903, 79295.85009317525, + 65084.49754064415, 52943.89178151959, 23465.48439728474, 21832.910625647823, + 19264.287770207746, 76548.45099026011, 60466.45166114487, 55279.197434974216, + 24155.813207346844, 22966.410348838213, 18983.75249797779, 73649.63988890029, + 71112.22602702447, 57946.65292077182, 23641.269675101943, 22291.717167052662, + 18741.849658805524, 81400.06764100857, 67629.03783864013, 67405.53195610014, + 24218.123407341278, 23333.215281521032, 20066.519859679578, 69297.20725459281, + 63219.49226518766, 53748.09893388699, 21812.00360812832, 19872.413381814225, + 17788.761084583686], + 'mdmkp_ct2.txt': [85737.01455327868, 78696.9991775994, 68344.04567453945, 28849.533294001798, + 26863.98196580951, 27320.612143547143, 77201.6817216863, 75444.46744561363, + 61063.789881092605, 29217.298765083375, 27075.31000291711, 26996.660180378818, + 81822.22342824662, 71256.25402393805, 62341.194601596886, 28313.55566164002, + 27523.52868077394, 27035.321578925952, 81853.34918315883, 80557.51259726346, + 66790.0712529679, 28232.37737198369, 26469.88622592108, 26245.96998646702, + 81785.92659972196, + 71222.49036491562, 62160.1118806897, 29281.210740186492, 27484.81483365757, + 26451.244595144035, 152744.10289701147, 131201.8397279085, 128183.09582390457, + 48530.504947691516, 44624.04171250145, 41734.55641636365, 137680.65156421435, + 131378.93642009562, 119993.19346841935, 47923.46930072854, 44961.232653882784, + 42156.49791254687, 144428.7158031205, 134533.43016588953, 103006.65157231665, + 47913.187589197136, 43826.18755568891, 42798.53780623339, 138875.9989433622, + 119974.20395118099, 108237.50680627486, 48076.91709819546, 43690.33318025998, + 41272.888199935616, 136680.00739689753, 128922.92966800612, 114619.70175377218, + 49199.94515582458, 45910.55304695518, 43797.21240710909, 220622.55996837773, + 194379.6945304465, 162305.7537748959, 60762.10209518433, 54341.222517811606, + 48668.07407973811, 199563.43979134466, 165580.50491656526, 175037.9382193091, + 61530.28209895772, 56104.68838548819, 47996.20454740994, 224140.1857862799, + 186790.55426756508, 150832.88991816493, 62759.54862209718, 57610.3818509872, + 50924.4636364738, 189913.657037726, 171291.44385422583, 150388.56765837915, + 61219.44818444248, 56034.59960390541, 47761.792950168354, 198167.53454359408, + 186238.7291392124, 154679.97893410415, 59218.96126115047, 54808.378420106, + 47917.408017869006], + 'mdmkp_ct3.txt': [168814.0860184554, 143266.38476611354, 137156.91757617562, 57894.150493004636, + 54801.499428939074, 53517.711310484585, 172610.2292039204, 160115.6472575543, + 145496.39844883644, 59390.29179783931, 54289.72547227832, 53024.46694919935, + 167567.35510673033, 164729.93693986148, 135420.79514986262, 60115.06815508855, + 55813.36492966956, 56352.20430613357, 172688.82134408774, 162319.81137900034, + 130344.96923837559, 59563.69109849362, 55603.0488768423, 56809.66042438546, + 170206.74300980923, 157900.84535984247, 131060.13019116307, 59531.600760548616, + 55559.28618907818, 54752.018503499545, 302114.49873024924, 256581.55631994538, + 207732.980425423, 97439.41910679372, 90516.99034978052, 85260.28922281954, + 312465.62841811427, 271474.10840658314, 225440.37664729697, 98799.08250295873, + 90273.14037722594, 87477.24047531083, 297238.32753707597, 303609.97451424616, + 225541.924960721, 98443.46583899345, 91397.1203183814, 86432.96390184318, + 306816.02212839137, + 280040.73064957885, 221762.73815399208, 98664.8071685896, 91138.37204475686, + 87776.28663778222, 318569.3817006032, 291852.5925075597, 236866.72831430216, + 98327.48388000038, 90336.45570858807, 86101.47544617752, 415194.2465573161, + 362491.8427350558, 307377.7603135135, 123584.46297508608, 111764.25699100617, + 98085.19170604613, 433314.1493530444, 364569.17167671275, 297531.23157215375, + 125913.7470940513, 114772.6222478601, 98973.94195326211, 406167.4870924763, + 416022.1099206263, 347267.15219282993, 125246.56624027551, 113042.34566405955, + 98644.59205787636, 431000.6087227007, 347398.03921355103, 316049.181248464, + 124957.64024309818, 115292.0983954651, 98821.88400173541, 390733.65163432877, + 386296.45713615883, 312349.0075675711, 125461.39250902845, 113530.28945365804, + 99827.79213964228], + 'mdmkp_ct4.txt': [29164.038872705965, 22448.128690234324, 22750.94208136601, 11324.083285011215, + 10719.956309350773, 9430.485533330844, 27413.704538415757, 21461.687993511725, + 20807.526347746276, 11174.57205823707, 10523.515868282093, 9480.294964657856, + 27679.789721276044, 20314.35486741767, 20078.725511933364, 11321.877876900408, + 10569.208944470603, 10037.671236768076, 27275.964278206353, 21403.189994100558, + 18214.689756802898, 11764.756991232283, 11403.330730547532, 10754.684755455568, + 27408.560904311627, 20239.00788536538, 19675.454531748826, 11103.196531870448, + 10870.885634988072, 9778.676576921001, 47914.26841337081, 36755.00142928597, + 34496.23241640505, 19355.6131185271, 17043.422551538544, 14963.241879022014, + 48424.800879436036, 37796.04860563597, 34252.66746620095, 20004.46730023067, + 18835.536671157857, 16372.729959011156, 46056.593752138586, 34203.22184902439, + 32098.336710050586, 20150.91866979717, 18742.934262975916, 16261.904248836905, + 46606.25108291802, 38692.27805531054, 34116.31787218311, 19892.13679537105, + 17906.171672142802, 15457.930528107207, 50837.276577590026, 38270.19850132687, + 33994.983617245525, 19964.04689543776, 18102.368498531567, 15557.02649862105, + 66492.56132398843, 48695.57734529988, 41728.99175538258, 26020.71441769906, + 21704.154628167817, 17058.040076600046, 66446.61120480338, 47164.040871164994, + 48144.90458804947, 26104.088009067298, 23025.98793294367, 17964.932081561263, + 69551.03401323149, 59291.08777980967, 47802.32806977924, 26495.990533074244, + 22596.77082251083, 17536.90097464368, 62631.5014743124, 58643.30592840052, + 49398.5074628221, + 25760.06015043017, 22890.929818858796, 17713.170731163762, 70181.38883730803, + 48918.371779400695, 49643.56705073802, 26572.11174570738, 23037.056763652552, + 16705.942545272545], + 'mdmkp_ct5.txt': [67074.72332396788, 56922.72071257537, 52889.19458254509, 29909.01523234807, + 27546.15586023913, 27256.80532272495, 77868.54700099613, 60229.72049072842, + 54149.53266748691, 30249.223526187412, 27120.038198121416, 26594.844588672448, + 70119.79201060643, 55514.79683891681, 47439.357264372185, 30134.92269436036, + 26259.336998000002, 27193.319825225342, 67901.56262102553, 53001.06387129924, + 55281.744348889064, 30069.120783816663, 27702.98526921436, 27356.78776039429, + 69629.5298376119, 58835.15928545275, 50230.15054431706, 30166.73357407277, + 27364.166305029587, 27330.144292096822, 119164.81505401935, 100397.28774329153, + 93469.74355268248, 51930.86507009473, 46769.19322469853, 43469.001177917045, + 131222.81251695874, 108833.88808461775, 90059.43430489714, 50740.658976924875, + 46235.81493273908, 43496.78460049598, 128866.81838327541, 108711.7891535098, + 86752.92311339389, 50998.811367235605, 45928.679572754314, 42680.52542881308, + 127143.08694841203, 102644.96769290596, 85141.97072163413, 51670.6446424105, + 46178.15119491831, 43412.42326287415, 130750.38067186893, 108385.55051038244, + 88052.26497421459, 51783.509375019516, 45886.267591475684, 42860.85744149264, + 169045.83770196422, 128460.7780770426, 118442.91466652084, 65910.00741807284, + 56072.77414670372, 45324.6047968282, 169920.60488581128, 130133.63844405038, + 121416.77168002189, 65784.1330038083, 56518.658717892344, 46831.28106896001, + 174541.92749152833, 129928.59851393547, 108508.93425522567, 66702.65101118234, + 56108.915404156476, 46214.73426365582, 163110.9330947666, 134212.68379176338, + 125886.72283652119, 68024.49481755303, 57037.86726713851, 46268.360529372236, + 173960.27635069186, 136162.4432373785, 112316.86040339211, 66543.19030194926, + 56539.515264863825, 46146.95781182992], + 'mdmkp_ct6.txt': [144980.23407910476, 112763.51393780176, 102383.66646450528, 59980.573187587404, + 52939.47573714053, 53771.685494620295, 154257.03187626274, 124883.9953049457, + 120642.0249017839, 60019.79547885496, 53897.85764198486, 54161.532879947954, + 144534.53457044237, 117720.3220895322, 96475.2443776782, 58954.60041811628, + 54069.08130394793, 54323.06299600842, 157814.49413999054, 122314.66949077391, + 100134.14416493346, 59884.60813014079, 53973.0450434286, 54587.50902401689, + 151422.05437238456, 122973.61948453542, 112057.21433922206, 58594.09015818635, + 53920.68270934412, 54374.5373291473, 282848.98344492953, 224326.69691858994, + 183304.07819034575, 101427.42770624388, 90233.59162281707, 87931.83988695969, + 269408.01082357974, 225474.35073228314, 215456.65002890778, 102416.03111190387, + 90069.81910015695, 85116.5260466803, 254947.89675161077, 218974.4686576769, + 186749.28559522133, 102917.6477109319, 91441.73103545069, 88999.20238685762, + 269816.67061679246, 210858.44227739258, 203583.16500971685, 104182.59994978322, + 88357.42788500736, 85767.49572881762, 272744.18752231536, 193951.70467221903, + 188257.8254527853, 101785.26150797169, 89213.54586549665, 85484.81249285143, + 381542.4217357743, 326469.342237431, 282353.7716418138, 128661.62461491564, + 108999.45692138896, 95010.49588600297, 356708.95381429413, 304916.8454043936, + 251063.8372197164, 130181.26485675877, 107033.53612376121, 94649.81679638875, + 361613.35129891743, 306735.42153189424, 297995.04983937036, 129864.48025022763, + 108951.57993028058, 93125.75190483894, 372946.5256811151, 312169.39120588027, + 291961.601990496, 128139.33325293686, 107763.57401113368, 92037.90261510992, + 371106.7889048268, 276728.6811577454, 264946.69248258974, 130334.98487932215, + 107565.0032849825, 94861.48458289343], + 'mdmkp_ct7.txt': [21536.667021014287, 15306.883012189614, 13443.216809193567, 11390.815478460714, + 8894.856170390527, 8519.060387333655, 21705.543635545953, 15173.62619468222, + 13038.599272654503, 12408.558146390122, 9797.67354382143, 8876.007748591903, + 23578.57658086318, 17182.951675388776, 13230.830577072633, 12537.577858898758, + 9219.568400973225, 7977.518849705143, 22053.315182257193, 14764.620511312056, + 13892.13285878336, 12425.658398919977, 9739.394008046469, 8885.452593071088, + 22313.07495965335, 15609.53760642277, 13245.694637863515, 12055.917630568516, + 9627.467383903446, 8162.209064269218, 42173.69463633543, 29877.880449919074, + 25440.078288591638, 21825.455611063735, 16549.449355399665, 13971.022911392056, + 43664.20300776704, 29464.902018501187, 24112.721453411297, 22513.26234961254, + 16629.759894115432, 13458.744980317182, 41039.61174386542, 29361.952810329356, + 25395.63206161855, 22057.01699394085, 16648.10129596265, 14013.12188134602, + 43085.507865128704, 30345.55473923687, 25186.272538620593, 21913.134339634285, + 16410.724181692833, 13953.82361078376, 44823.613253516436, 30543.80340170374, + 26300.182116669628, 22188.202993274637, 16580.805936525514, 12952.062092272812, + 56519.740845824, 42915.67901994479, 36709.844924364166, 29814.67594985235, + 21301.929554223458, 15512.614795423231, 59358.39167893502, 40874.51794050845, + 34902.226748636676, 28713.930949486898, 21518.41831664319, 15945.794740345258, + 64920.55408589671, 37197.96324958661, 35058.404365245566, 30202.452304810613, + 20831.238717184224, 15176.318443957553, 60911.53631172457, 42914.45741637827, + 37188.39746443595, 29560.00507125548, 21757.49688641164, 15066.861187060727, + 59162.50291552842, 37851.22602477869, 33511.28841330884, 29822.916196666672, + 20755.309519582046, 14846.520309418032], + 'mdmkp_ct8.txt': [65991.67034165592, 39986.57332809463, 36598.09000404739, 31686.372556660783, + 25445.141538461943, 25077.05114636876, 63979.22183114905, 43126.26992981033, + 38628.88407519385, 30302.17299689071, 25856.008803794008, 26177.344337965726, + 65138.034210076374, 44907.343983147846, 41550.321250392364, 31845.384596822078, + 25791.469096917182, 26506.638584217988, 64634.00404312337, 40174.8200604331, + 37334.96401267335, 30606.32307685129, 25945.06525360605, 26187.72721124403, + 65280.72384563691, 39834.38753008512, 36962.42645358037, 30687.36207486381, + 25679.89696314449, 25369.83962024141, 113030.16628540732, 82740.77198899978, + 73895.62791600019, 55638.48680489044, 44334.35576064882, 40312.02329612095, + 116780.09817051327, 76575.7430130301, 67790.62525433657, 54232.24837998093, + 44014.019831706864, 42095.043637196846, 113131.19405348024, 82687.33750069182, + 73711.02052653379, 53630.823367916724, 44179.40462430917, 40583.47254003538, + 117795.90163898062, 79370.86436229874, 70043.37406542819, 56012.97811055305, + 43195.84496979824, 41088.50539662127, 117372.85927796275, 81505.10700293747, + 75489.689213593, + 52990.92694419264, 43272.74841650926, 39717.78170853508, 160852.0754812623, + 106590.67841439568, 99150.39399824802, 68409.80507279171, 52921.09290099409, + 43061.27479238256, 169355.20414890588, 112529.5560624529, 95892.00602546158, + 70889.48098034738, 53229.92344418698, 44841.2020442713, 162207.84790491065, + 108897.66487579771, 86526.99526234265, 71872.14878415015, 53655.04733850709, + 41896.08057194517, 162405.8574386429, 112025.48285525134, 100112.58173915549, + 73823.0844300016, 53451.75195169678, 44515.54400782727, 157038.53409987607, + 110402.29049724352, 95649.38172715022, 71516.83170706524, 53422.2982374228, + 43167.068559292675], + 'mdmkp_ct9.txt': [134758.3549892344, 86909.00762356113, 86982.49992050465, 62340.79946652035, + 52415.14256530901, 53232.920819668056, 128419.67845418965, 88901.5566742058, + 83992.02809346681, 63360.38586246625, 51893.2045344762, 52256.21901538081, + 127064.63498233976, 89453.5086261967, 79351.25774203127, 62032.14391662491, + 51631.24745514526, 52782.4609329052, 128368.08791816013, 90245.11312968897, + 84131.37307719128, 63133.2050746076, 52123.626492230534, 53162.85131488812, + 129825.81285949677, 90096.64235781695, 85277.83477898309, 63245.06600443, + 52549.65434095841, + 53367.474150258815, 239342.2155683403, 161158.5613710625, 147529.19830464877, + 111487.10174717069, 88339.35475314604, 85961.16276741328, 248369.26195392013, + 162009.4593849904, 153842.18958281272, 110110.75575503791, 86496.15756377159, + 85119.35754565377, 250539.4853694354, 167117.75419544292, 159374.2650101431, + 109304.25500648574, 86773.9066627287, 84813.75401106518, 245446.27414940202, + 169228.10912750015, 155884.4962464008, 108544.66757407523, 85582.42521344902, + 84874.08841589266, 245923.51801080085, 163298.31207544284, 143918.62121275134, + 108315.8596996154, 87105.7025967709, 84940.87088406476, 328053.7136147506, + 225215.22929174392, 186813.67930702146, 143202.56393579207, 104756.76964530846, + 89882.77399645871, 328408.6333206934, 234918.00365513685, 196065.373602686, + 141788.8644964311, 105420.40686888609, 88356.57745002235, 324087.92881999584, + 234211.97225764123, 209812.7317955713, 143874.73260227023, 106052.15296672149, + 88147.09916517115, 341135.49211315066, 243868.20006443554, 217462.7234056969, + 143414.35002359937, 105223.51930478415, 88613.1445228776, 336605.35161074856, + 210327.86701928257, 195984.09990534862, 141107.11065593347, 104153.81949061107, + 88866.18621642572] + } + + normed = {} + for case, (scores, error_message) in results.items(): + if case not in optimal_scores: + continue # Skip if there's no optimal score defined. + optimal_list = optimal_scores[case] + normed_scores = [] + # Compute normalized score for each index. + for idx, score in enumerate(scores): + if isinstance(score, (int, float)): + normed_scores.append(score / optimal_list[idx]) + else: + normed_scores.append(score) + normed[case] = (normed_scores, error_message) + + return normed + + def get_dev(self): + dev = { + 'mdmkp_ct1.txt': [43, 4, 30, 72, 52, 62, 33, 73, 64, 37, 17, 36, 59, 38, 13, 9, 22, 57, 15, 44, 87, 55, 2, + 29, + 39, 50, 11, 54, 79, 19, 10, 0, 47, 5, 18, 83, 45, 88, 81, 35, 75, 27, 70, 61, 67], + 'mdmkp_ct2.txt': [76, 25, 72, 79, 59, 55, 58, 67, 70, 5, 81, 54, 77, 51, 78, 39, 4, 84, 0, 47, 48, 24, 27, + 69, + 49, 34, 53, 26, 89, 73, 21, 37, 29, 10, 52, 15, 23, 46, 88, 60, 19, 64, 12, 20, 71], + 'mdmkp_ct3.txt': [4, 7, 0, 60, 57, 48, 37, 13, 45, 65, 74, 20, 80, 17, 43, 46, 67, 33, 83, 77, 32, 86, 41, + 26, + 70, 34, 75, 21, 47, 56, 84, 14, 25, 5, 88, 24, 9, 28, 2, 66, 85, 81, 69, 58, 18], + 'mdmkp_ct4.txt': [73, 38, 62, 32, 34, 71, 17, 19, 63, 20, 30, 55, 65, 45, 4, 22, 86, 48, 75, 23, 41, 59, 79, + 3, + 14, 83, 36, 72, 87, 9, 40, 44, 53, 15, 47, 74, 68, 67, 24, 28, 57, 27, 77, 89, 37], + 'mdmkp_ct5.txt': [35, 7, 69, 39, 84, 9, 44, 62, 53, 32, 72, 71, 13, 83, 12, 33, 66, 17, 59, 51, 14, 56, 0, + 16, + 54, 52, 65, 41, 75, 46, 89, 64, 48, 61, 28, 77, 68, 19, 36, 50, 88, 82, 80, 22, 27], + 'mdmkp_ct6.txt': [2, 69, 38, 19, 79, 66, 73, 7, 82, 33, 49, 64, 85, 89, 14, 9, 23, 40, 25, 10, 17, 31, 58, + 78, + 11, 74, 1, 46, 60, 28, 71, 88, 39, 62, 77, 72, 50, 22, 16, 84, 51, 53, 56, 20, 13], + 'mdmkp_ct7.txt': [68, 37, 23, 21, 71, 36, 80, 18, 0, 58, 78, 25, 26, 73, 19, 81, 7, 38, 67, 6, 77, 52, 11, + 57, + 86, 42, 50, 56, 82, 89, 48, 61, 53, 24, 74, 70, 43, 30, 47, 14, 69, 63, 3, 22, 44], + 'mdmkp_ct8.txt': [47, 54, 56, 87, 69, 81, 63, 6, 26, 53, 3, 83, 52, 23, 82, 57, 1, 78, 5, 13, 42, 80, 30, + 19, + 11, 37, 36, 61, 46, 21, 71, 35, 84, 49, 67, 70, 55, 44, 51, 12, 86, 74, 72, 45, 8], + 'mdmkp_ct9.txt': [23, 72, 78, 70, 68, 61, 52, 12, 56, 69, 35, 21, 31, 3, 25, 30, 66, 1, 54, 83, 89, 26, 49, + 65, + 40, 20, 57, 7, 5, 74, 44, 42, 85, 77, 87, 76, 45, 2, 86, 10, 48, 29, 46, 51, 13]} + + return dev + + + + + +# Task configuration for benchmark task +ENTRY_NAME = 'solve' +FUNCTION_SIGNATURE = 'def solve(...):' +IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' +TASK_DESCRIPTION = '("The Multi-Demand Multidimensional Knapsack Problem (MDMKP) is a binary optimization problem that "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The Multi-Demand Multidimensional Knapsack Problem (MDMKP) is a binary optimization problem that "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n:int, m:int, q:int, A_leq:list, b_leq:list, A_geq:list, b_geq:list, cost_vector:list, cost_type:str) -> dict:\n """\n Solve a given MDMKP test instance.\n Input (via kwargs):\n - n: int\n Number of decision variables.\n - m: int\n Number of <= constraints.\n - q: int\n Number of active >= constraints (subset of the full set).\n - A_leq: list of lists of int\n Coefficient matrix for <= constraints (dimensions: m x n).\n - b_leq: list of int\n Right-hand side for <= constraints (length m).\n - A_geq: list of lists of int\n Coefficient matrix for >= constraints (dimensions: q x n).\n - b_geq: list of int\n Right-hand side for >= constraints (length q).\n - cost_vector: list of int\n Objective function coefficients (length n).\n - cost_type: str\n Type of cost coefficients ("positive" or "mixed").\n Output:\n A dictionary with the following keys:\n - \'optimal_value\': int/float\n The optimal objective function value (if found).\n - \'x\': list of int\n Binary vector (0 or 1) representing the decision variable assignment.\n TODO: Implement the actual solution algorithm for the MDMKP instance.\n """\n # TODO: Define your model variables, constraints, and objective function.\n # For example, you might use an integer programming solver (e.g., PuLP, Gurobi, or another solver)\n # to model and solve the instance.\n\n # Placeholder solution:\n solution = {\n \'optimal_value\': None, # Replace with the computed objective value.\n \'x\': [0] * kwargs.get(\'n\', 0), # Replace with the computed decision vector.\n }\n return solution' +EVAL_CLASS_NAME = 'MDMKPEvaluationCB' +EVAL_KWARGS = {'timeout_seconds': 60} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_multi_demand_multidimensional_knapsack_problem/paras.yaml b/examples/benchmark_tasks/optimization_multi_demand_multidimensional_knapsack_problem/paras.yaml new file mode 100644 index 00000000..2394fe1f --- /dev/null +++ b/examples/benchmark_tasks/optimization_multi_demand_multidimensional_knapsack_problem/paras.yaml @@ -0,0 +1,2 @@ +name: MDMKPEvaluationCB +timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_multidimensional_knapsack_problem/__init__.py b/examples/benchmark_tasks/optimization_multidimensional_knapsack_problem/__init__.py new file mode 100644 index 00000000..35588dd6 --- /dev/null +++ b/examples/benchmark_tasks/optimization_multidimensional_knapsack_problem/__init__.py @@ -0,0 +1,496 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_multidimensional_knapsack_problem +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# References: +# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language +# model agents in algorithm search for combinatorial optimization. +# arXiv preprint arXiv:2504.04310 (2025). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any +import numpy as np +from llm4ad_loader import Evaluation +from llm4ad_loader import load_subdir_as_text +# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader +# from llm4ad.task.optimization.co_bench.multidimensional_knapsack_problem_co_bench.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n: int, m: int, p: list, r: list, b: list) -> dict:\n """\n Solves a multidimensional knapsack problem instance.\n Input kwargs (for one test case):\n - n: int, number of decision variables.\n - m: int, number of constraints.\n - p: list of floats, profit coefficients (length n).\n - r: list of m lists, each of length n, representing the resource consumption per constraint.\n - b: list of floats, right-hand side values for each constraint (length m).\n Evaluation metric:\n The score is computed as:\n score = sum(p[j] * x[j] for j in range(n))\n if and only if all constraints are satisfied—that is, for every constraint i, the total resource consumption\n sum(r[i][j] * x[j] for j in range(n))\n does not exceed b[i].\n If any constraint is violated, the solution receives no score. A higher score is better.\n Returns:\n A dict with key \'x\' whose value is a list of n binary decisions (0 or 1).\n """\n # Placeholder implementation: a dummy solution that selects no items.\n x = [0] * kwargs[\'n\']\n return {\'x\': x}' +task_description = '("This problem is a multidimensional knapsack optimization where the objective is to maximize the "' + + +__all__ = ['MKPEvaluationCB'] + + +class MKPEvaluationCB(Evaluation): + + def __init__(self, + timeout_seconds=300, + **kwargs): + + """ + Args: + None + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + # Load datasets from Hugging Face + dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Multidimensional knapsack problem") + self._datasets = {} + for filename in dataset: + # Join all text rows into a single string + text_content = '\n'.join([row['text'] for row in dataset[filename]]) + self._datasets[filename] = text_content + + def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: + return self.evaluate(callable_func) + + def evaluate(self, eva: callable) -> float | None: + ins_cases = [] + for case_id, ins in enumerate(self._datasets.values()): + ins_cases.append(self.load_data(ins)) + + fitness_list = [] + try: + for i in ins_cases: + for j in i: + result = eva(j['n'], j['m'], j['p'], j['r'], j['b']) + fitness = self.eval_func(j['n'], j['m'], j['p'], j['r'], j['b'], result['x'], j['opt']) + fitness_list.append(fitness) + + return np.mean(fitness_list) + + except ValueError as e: + print(e) + return None + + def load_data2(self, input_path): + """ + Loads instance(s) from the OR-Library mknap2.txt file. + This file contains many lines of commentary and then one or more instances. + Each instance is defined (after removing comments) as: + <#knapsacks> <#objects> + (there will be exactly #objects numbers) + (exactly #knapsacks numbers) + (#objects rows, each with #knapsacks numbers) + [] (an extra token, optional) + In our formulation: + - the number of decision variables (n) is set to the number of objects, + - the number of constraints (m) is set to the number of knapsacks, + - the profit coefficients p are taken equal to the object weights, + - the constraint coefficients r are taken from the matrix (transposed so that each + constraint i gets a list of consumptions for all objects), + - the right-hand sides b are the knapsack capacities. + Returns: + A list of dictionaries. Each dictionary corresponds to one problem instance and + has the keys: + 'n' : int, number of objects (decision variables) + 'm' : int, number of knapsacks (constraints) + 'p' : list of floats, profit coefficients (length n) + 'r' : list of m lists of floats, where each inner list is of length n (constraint coefficients) + 'b' : list of floats, knapsack capacities (length m) + If the instance file also provides an optimum value, it is stored under key 'opt'. + """ + cases = [] + all_lines = [line.strip() for line in input_string.split('\n')] + + # Remove comments (anything after '//') and extra whitespace. + cleaned_lines = [] + for line in all_lines: + line = line.split("//")[0] + line = line.strip() + if line: + cleaned_lines.append(line) + + # Gather all tokens (they may come from several lines) + tokens = [] + for line in cleaned_lines: + tokens.extend(line.split()) + + # Process tokens sequentially looking for candidate instance headers. + # The expected header is two positive numbers: (#knapsacks, #objects). + i = 0 + N = len(tokens) + while i < N - 1: + try: + # Try to read two numbers as candidate header + knapsacks = int(float(tokens[i])) + objects = int(float(tokens[i + 1])) + except Exception: + i += 1 + continue + + # Basic validity check: both numbers must be positive. + if knapsacks <= 0 or objects <= 0: + i += 1 + continue + + # Once a candidate header is found, compute the expected number of tokens: + # header already consumed: 2 tokens + # then: object weights: objects tokens + # then: knapsack capacities: knapsacks tokens + # then: resource consumption matrix: objects * knapsacks tokens + # Optionally: one token for known optimum. + required = objects + knapsacks + (objects * knapsacks) + # Check if there is at least the required number of tokens after the header. + if i + 2 + required > N: + # Not enough tokens left; break out. + break + + # Consume header. + i += 2 + + # Read object weights (which we use as profit coefficients). + weights = [] + for _ in range(objects): + weights.append(float(tokens[i])) + i += 1 + + # Read knapsack capacities. + capacities = [] + for _ in range(knapsacks): + capacities.append(float(tokens[i])) + i += 1 + + # Read the resource consumption matrix. + # The file gives a matrix with 'objects' rows and 'knapsacks' columns. + matrix = [] + for _ in range(objects): + row = [] + for _ in range(knapsacks): + row.append(float(tokens[i])) + i += 1 + matrix.append(row) + + # Optionally, read the known optimum if present. + optimum = None + if i < N: + # We treat the next token as optimum if it is a number. + try: + optimum = float(tokens[i]) + i += 1 + except Exception: + optimum = None + + # Convert the data to our formulation: + # Decision variables: one per object. + # Constraints: one per knapsack. + # Profit coefficients p: equal to the object weights. + # Constraint coefficients r: we need to transpose the matrix so that for each knapsack, + # we get the consumption for each object. + p = weights + r = [] + for k in range(knapsacks): + constraint_coeffs = [] + for obj in range(objects): + constraint_coeffs.append(matrix[obj][k]) + r.append(constraint_coeffs) + b = capacities + + case = {'n': objects, 'm': knapsacks, 'p': p, 'r': r, 'b': b} + if optimum is not None: + case['opt'] = optimum + cases.append(case) + + return cases + + def load_data(self, input_string): + """ + Reads the input string and returns a list of test cases. + Each case is represented as a dictionary containing: + - 'n': number of decision variables. + - 'm': number of constraints. + - 'p': list of floats, profit coefficients. + - 'r': list of m lists of floats, constraint coefficients. + - 'b': list of floats, right-hand side values. + """ + # Simple check for mknap2 format - for now, use default format + # if 'mknap2' in input_path: + # return self.load_data2(input_path) + + tokens = input_string.split() + + token_index = 0 + try: + K = int(tokens[token_index]) + except Exception as e: + raise ValueError("The first token must be an integer indicating the number of test cases.") from e + token_index += 1 + + cases = [] + for case_index in range(K): + try: + n = int(tokens[token_index]) + m = int(tokens[token_index + 1]) + opt_val = float(tokens[token_index + 2]) + except Exception as e: + raise ValueError(f"Error reading header for test case {case_index + 1}.") from e + token_index += 3 + + p = [] + for j in range(n): + try: + p.append(float(tokens[token_index])) + except Exception as e: + raise ValueError(f"Error reading profit coefficient {j + 1} for test case {case_index + 1}.") from e + token_index += 1 + + r = [] + for i in range(m): + row = [] + for j in range(n): + try: + row.append(float(tokens[token_index])) + except Exception as e: + raise ValueError( + f"Error reading constraint coefficient for constraint {i + 1}, variable {j + 1} in test case {case_index + 1}.") from e + token_index += 1 + r.append(row) + + b = [] + for i in range(m): + try: + b.append(float(tokens[token_index])) + except Exception as e: + raise ValueError( + f"Error reading right-hand side value {i + 1} for test case {case_index + 1}.") from e + token_index += 1 + + case_data = { + 'n': n, + 'm': m, + 'p': p, + 'r': r, + 'b': b, + 'opt': opt_val + } + cases.append(case_data) + + return cases + + def eval_func(self, n, m, p, r, b, x, opt=None): + """ + Evaluates the solution for a multidimensional knapsack problem instance. + Inputs: + - n: int, number of decision variables. + - m: int, number of constraints. + - p: list of floats, profit coefficients (length n). + - r: list of m lists of floats, each representing the constraint coefficients. + - b: list of floats, right-hand side values for each constraint (length m). + - x: list of ints (0 or 1), the solution decisions (length n). + - opt (float, optional): The known optimal (or best-known) objective value. + This parameter is provided by instances loaded via load_data2, if available. + Evaluation: + - The objective value is computed as: + sum(p[j] * x[j] for j in range(n)) + - For each constraint i, the total resource consumption is computed as: + sum(r[i][j] * x[j] for j in range(n)) + - If any constraint i is violated (i.e., the consumption exceeds b[i]), an error is raised. + - If all constraints are satisfied, the score is equal to the objective value. + Returns: + - If opt is not provided (None), returns a float representing the overall quality score. + - If opt is provided, returns a tuple: + (score, gap) + where gap is defined as (score - opt), which indicates how far (or above) + the computed score is relative to the known optimum. + """ + tol = 1e-6 + + # Compute objective value. + objective_value = sum(p[j] * x[j] for j in range(n)) + + # Check each constraint; raise an error if any constraint is violated. + for i in range(m): + lhs = sum(r[i][j] * x[j] for j in range(n)) + if lhs - b[i] > tol: + raise ValueError(f"Constraint violation in constraint {i}: consumption {lhs} exceeds limit {b[i]}.") + + # If all constraints are satisfied, score is the objective value. + score = objective_value + + # Return either score alone or (score, gap) if optimum is provided. + if opt is not None: + gap = score - opt + return score + else: + return score + + def norm_score(self, results): + optimal_scores = { + "mknap1.txt": [3800, 8706.1, 4015, 6120, 12400, 10618, 16537], + "mknap2.txt": [7772.0, 8722.0, 141278.0, 130883.0, 95677.0, 119337.0, 98796.0, 130623.0, 1095445.0, + 624319.0, + 4554.0, 4536.0, 4115.0, 4561.0, 4514.0, 5557.0, 5567.0, 5605.0, 5246.0, 6339.0, 5643.0, + 6339.0, + 6159.0, 6954.0, 7486.0, 7289.0, 8633.0, 9580.0, 7698.0, 9450.0, 9074.0, 8947.0, 8344.0, + 10220.0, + 9939.0, 9584.0, 9819.0, 9492.0, 9410.0, 11191.0, 3090.0, 3186.0, 95168.0, 2139.0, 776.0, + 1035.0, + 3418.0, 3186.0], + "mknapcb1.txt": [24381, 24274, 23551, 23534, 23991, 24613, 25591, 23410, 24216, 24411, 42757, 42545, 41968, + 45090, 42218, 42927, 42009, 45020, 43441, 44554, 59822, 62081, 59802, 60479, 61091, 58959, + 61538, 61520, 59453, 59965], + "mknapcb2.txt": [59312, 61472, 62130, 59446, 58951, 60056, 60414, 61472, 61885, 58959, 109109, 109841, + 108489, + 109383, 110720, 110256, 109016, 109037, 109957, 107038, 149659, 155940, 149316, 152130, + 150353, + 150045, 148607, 149772, 155075, 154662], + "mknapcb3.txt": [120130, 117837, 121109, 120798, 122319, 122007, 119113, 120568, 121575, 120699, 218422, + 221191, + 217534, 223558, 218962, 220514, 219987, 218194, 216976, 219693, 295828, 308077, 299796, + 306476, + 300342, 302560, 301322, 306430, 302814, 299904], + "mknapcb4.txt": [23064, 22801, 22131, 22772, 22751, 22777, 21875, 22635, 22511, 22702, 41395, 42344, 42401, + 45624, 41884, 42995, 43559, 42970, 42212, 41207, 57375, 58978, 58391, 61966, 60803, 61437, + 56377, 59391, 60205, 60633], + "mknapcb5.txt": [59187, 58662, 58094, 61000, 58092, 58803, 58607, 58917, 59384, 59193, 110863, 108659, + 108932, + 110037, 108423, 110841, 106075, 106686, 109825, 106723, 151790, 148772, 151900, 151275, + 151948, + 152109, 153131, 153520, 149155, 149704], + "mknapcb6.txt": [117726, 119139, 119159, 118802, 116434, 119454, 119749, 118288, 117779, 119125, 217318, + 219022, + 217772, 216802, 213809, 215013, 217896, 219949, 214332, 220833, 304344, 302332, 302354, + 300743, + 304344, 301730, 304949, 296437, 301313, 307014], + "mknapcb7.txt": [21946, 21716, 20754, 21464, 21814, 22176, 21799, 21397, 22493, 20983, 40767, 41304, 41560, + 41041, 40872, 41058, 41062, 42719, 42230, 41700, 57494, 60027, 58025, 60776, 58884, 60011, + 58132, 59064, 58975, 60603], + "mknapcb8.txt": [56693, 58318, 56553, 56863, 56629, 57119, 56292, 56403, 57442, 56447, 107689, 108338, + 106385, + 106796, 107396, 107246, 106308, 103993, 106835, 105751, 150083, 149907, 152993, 153169, + 150287, + 148544, 147471, 152841, 149568, 149572], + 'mknapcb9.txt': [115868, 114667, 116661, 115237, 116353, 115604, 113952, 114199, 115247, 116947, 217995, + 214534, + 215854, 217836, 215566, 215762, 215772, 216336, 217290, 214624, 301627, 299985, 304995, + 301935, + 304404, 296894, 303233, 306944, 303057, 300460] + } + + normed = {} + for case, (scores, error_message) in results.items(): + if case not in optimal_scores: + continue # Skip if there's no optimal score defined. + optimal_list = optimal_scores[case] + normed_scores = [] + # Compute normalized score for each index. + for idx, score in enumerate(scores): + if isinstance(score, (int, float)): + normed_scores.append(score / optimal_list[idx]) + else: + normed_scores.append(score) + normed[case] = (normed_scores, error_message) + + return normed + + def get_dev(self): + dev = {'mknap1.txt': [4, 1, 0], + 'mknap2.txt': [6, 44, 18, 22, 35, 45, 26, 28, 12, 0, 46, 1, 17, 31, 9, 21, 20, 23, 2, 13, 27, 33, 29, + 41], + 'mknapcb1.txt': [2, 5, 24, 4, 6, 25, 8, 14, 11, 9, 20, 26, 10, 7, 27], + 'mknapcb2.txt': [18, 10, 4, 27, 16, 17, 25, 29, 13, 21, 20, 7, 14, 9, 28], + 'mknapcb3.txt': [2, 8, 3, 0, 18, 7, 24, 1, 17, 23, 28, 12, 9, 4, 5], + 'mknapcb4.txt': [9, 16, 2, 10, 24, 19, 3, 13, 14, 29, 28, 15, 0, 4, 22], + 'mknapcb5.txt': [16, 15, 11, 5, 7, 8, 20, 2, 3, 27, 12, 22, 29, 23, 21], + 'mknapcb6.txt': [23, 5, 9, 14, 13, 6, 7, 16, 8, 2, 22, 3, 25, 26, 1], + 'mknapcb7.txt': [22, 7, 11, 0, 4, 3, 26, 17, 10, 14, 8, 13, 27, 15, 9], + 'mknapcb8.txt': [19, 12, 18, 6, 0, 16, 2, 25, 15, 28, 14, 1, 26, 9, 4], + 'mknapcb9.txt': [23, 8, 21, 24, 0, 5, 17, 1, 2, 7, 27, 29, 15, 12, 18]} + + return dev + + + + + + +# Task configuration for benchmark task +ENTRY_NAME = 'solve' +FUNCTION_SIGNATURE = 'def solve(...):' +IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' +TASK_DESCRIPTION = '("This problem is a multidimensional knapsack optimization where the objective is to maximize the "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("This problem is a multidimensional knapsack optimization where the objective is to maximize the "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n: int, m: int, p: list, r: list, b: list) -> dict:\n """\n Solves a multidimensional knapsack problem instance.\n Input kwargs (for one test case):\n - n: int, number of decision variables.\n - m: int, number of constraints.\n - p: list of floats, profit coefficients (length n).\n - r: list of m lists, each of length n, representing the resource consumption per constraint.\n - b: list of floats, right-hand side values for each constraint (length m).\n Evaluation metric:\n The score is computed as:\n score = sum(p[j] * x[j] for j in range(n))\n if and only if all constraints are satisfied—that is, for every constraint i, the total resource consumption\n sum(r[i][j] * x[j] for j in range(n))\n does not exceed b[i].\n If any constraint is violated, the solution receives no score. A higher score is better.\n Returns:\n A dict with key \'x\' whose value is a list of n binary decisions (0 or 1).\n """\n # Placeholder implementation: a dummy solution that selects no items.\n x = [0] * kwargs[\'n\']\n return {\'x\': x}' +EVAL_CLASS_NAME = 'MKPEvaluationCB' +EVAL_KWARGS = {'timeout_seconds': 300} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_multidimensional_knapsack_problem/paras.yaml b/examples/benchmark_tasks/optimization_multidimensional_knapsack_problem/paras.yaml new file mode 100644 index 00000000..5d479383 --- /dev/null +++ b/examples/benchmark_tasks/optimization_multidimensional_knapsack_problem/paras.yaml @@ -0,0 +1,2 @@ +name: MKPEvaluationCB +timeout_seconds: 300 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_online_bin_packing/__init__.py b/examples/benchmark_tasks/optimization_online_bin_packing/__init__.py new file mode 100644 index 00000000..eba0f176 --- /dev/null +++ b/examples/benchmark_tasks/optimization_online_bin_packing/__init__.py @@ -0,0 +1,311 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_online_bin_packing +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# Module Name: OBPEvaluation +# Last Revision: 2025/2/16 +# Description: Evaluates the Online Bin Packing Problem (OBP). +# Given a sequence of items arriving one by one, the goal is to pack them into bins +# of fixed capacity in real-time, minimizing the number of bins used. +# This module is part of the LLM4AD project (https://github.com/Optima-CityU/llm4ad). +# +# Parameters: +# - timeout_seconds: Maximum allowed time (in seconds) for the evaluation process: int (default: 30). +# - n_instances: Number of problem instances to generate: int (default: 5). +# - n_items: Number of items to pack: int (default: 5000). +# - capacity: Maximum capacity of each bin: int (default: 100). +# +# References: +# - Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any +import numpy as np +import matplotlib.pyplot as plt + +from llm4ad_loader import Evaluation +# from llm4ad.task.optimization.online_bin_packing.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\n\ndef priority(item: float, bins: np.ndarray) -> np.ndarray:\n """Returns priority with which we want to add item to each bin.\n Args:\n item: Size of item to be added to the bin.\n bins: Array of capacities for each bin.\n Return:\n Array of same size as bins with priority score of each bin.\n """\n return item - bins' +task_description = 'Implement a function that returns the priority with which we want to add an item to each bin.' + +import os, sys +sys.path.insert(0, os.path.dirname(__file__)) +from generate_weibull_instances import generate_weibull_dataset +# from llm4ad.task.optimization.online_bin_packing.generate_weibull_instances import generate_weibull_dataset # Converted from LLM4AD import + +__all__ = ['OBPEvaluation'] + + +class OBPEvaluation(Evaluation): + """Evaluator for online bin packing problem.""" + + def __init__(self, timeout_seconds=30, + n_instances=5, + n_items=5000, + capacity=100, + **kwargs): + """ + Args: + - 'data_file' (str): The data file to load (default is 'weibull_5k_train.pkl'). + - 'data_key' (str): The key of the data to load (default is 'data_key'). + + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + self.n_instances = n_instances + self.n_items = n_items + self.capacity = capacity + + self._datasets = generate_weibull_dataset(self.n_instances, self.n_items, self.capacity) + + def evaluate_program(self, program_str: str, callable_func: callable) -> Any | None: + return self.evaluate(callable_func) + + def plot_solution(self, bins_packed: np.ndarray, items: list, capacity: int, max_unused_bins: int = 5): + """ + Plot the solution of the 1D Online Bin Packing Problem, omitting unused bins. + + Args: + bins_packed: A numpy array of remaining capacities in the bins after packing. + items: A list of item sizes. + capacity: The capacity of each bin. + max_unused_bins: Maximum number of unused bins to include in the plot (for sampling). + """ + # Calculate the number of bins used + num_bins = (bins_packed != capacity).sum() + + # + n_show = 15 + + # Check for empty bins or invalid inputs + if num_bins == 0: + print("No bins used.") + return + if len(items) == 0: + print("No items to pack.") + return + + # Track which items are assigned to which bins + item_assignment = [[] for _ in range(len(bins_packed))] + current_bin = 0 + current_position = 0 + + for item in items: + if current_bin >= len(bins_packed): + break # No more bins available + if current_position + item <= capacity - bins_packed[current_bin]: + item_assignment[current_bin].append((current_position, item)) + current_position += item + else: + current_bin += 1 + current_position = 0 + if current_bin >= len(bins_packed): + break + item_assignment[current_bin].append((current_position, item)) + current_position += item + + # Filter out bins with no items + bins_with_items = [bin_idx for bin_idx, items_in_bin in enumerate(item_assignment) if items_in_bin] + + # Include a sample of unused bins (if any) + unused_bins = [bin_idx for bin_idx, items_in_bin in enumerate(item_assignment) if not items_in_bin] + if unused_bins: + unused_bins_sample = unused_bins[:max_unused_bins] # Sample a subset of unused bins + bins_to_plot = bins_with_items + unused_bins_sample + else: + bins_to_plot = bins_with_items + + bins_to_plot = bins_to_plot[:n_show] + + # Adjust figure size based on the number of bins to plot + bin_height = 0.5 # Height per bin in inches + fig_height = max(3, len(bins_to_plot) * bin_height) # Minimum height of 3 inches + + # Create a figure and axis + fig, ax = plt.subplots(figsize=(10, fig_height)) + + # Plot each bin and its items + for plot_idx, bin_idx in enumerate(bins_to_plot): + # Plot the bin as a horizontal bar + ax.barh(plot_idx, capacity, height=0.6, color='lightgray', edgecolor='black', label='Bin' if plot_idx == 0 else None) + + # Plot the items packed into the bin (if any) + for position, item in item_assignment[bin_idx]: + ax.barh(plot_idx, item, left=position, height=0.6, color='skyblue', edgecolor='black') + + # Set axis labels and title + ax.set_yticks(range(len(bins_to_plot))) + ax.set_yticklabels([f'Bin {bin_idx + 1}' for bin_idx in bins_to_plot]) + ax.set_xlabel('Capacity') + ax.set_title('1D Online Bin Packing Solution') + + # Add a legend + ax.legend(['Bin', 'Item'], loc='upper right') + + # Adjust layout to prevent overlap + plt.tight_layout() + + # Show the plot + plt.show() + + def get_valid_bin_indices(self, item: float, bins: np.ndarray) -> np.ndarray: + """Returns indices of bins in which item can fit.""" + return np.nonzero((bins - item) >= 0)[0] + + def online_binpack(self, + items: tuple[float, ...], bins: np.ndarray, priority: callable + ) -> tuple[list[list[float, ...], ...], np.ndarray]: + """Performs online binpacking of `items` into `bins`.""" + # Track which items are added to each bin. + packing = [[] for _ in bins] + # Add items to bins. + for item in items: + # Extract bins that have sufficient space to fit item. + valid_bin_indices = self.get_valid_bin_indices(item, bins) + # Score each bin based on heuristic. + priorities = priority(item, bins[valid_bin_indices]) + # Add item to bin with highest priority. + best_bin = valid_bin_indices[np.argmax(priorities)] + bins[best_bin] -= item + packing[best_bin].append(item) + # Remove unused bins from packing. + packing = [bin_items for bin_items in packing if bin_items] + return packing, bins + + def evaluate(self, priority: callable) -> float: + """Evaluate heuristic function on a set of online binpacking instances.""" + # List storing number of bins used for each instance. + num_bins = [] + # Perform online binpacking for each instance. + for name in self._datasets: + instance = self._datasets[name] + capacity = instance['capacity'] + items = instance['items'] + # Create num_items bins so there will always be space for all items, + # regardless of packing order. Array has shape (num_items,). + bins = np.array([capacity for _ in range(instance['num_items'])]) + # Pack items into bins and return remaining capacity in bins_packed, which + # has shape (num_items,). + _, bins_packed = self.online_binpack(items, bins, priority) + + # If remaining capacity in a bin is equal to initial capacity, then it is + # unused. Count number of used bins. + num_bins.append((bins_packed != capacity).sum()) + # Score of heuristic function is negative of average number of bins used + # across instances (as we want to minimize number of bins). + return -np.mean(num_bins) + + +if __name__ == '__main__': + def priority(item: float, valid_bins: np.ndarray) -> np.ndarray: + """ + Priority function for the First-Fit Decreasing (FFD) heuristic. + + Args: + item: The size of the item to be packed. + valid_bins: A numpy array of remaining capacities in valid bins. + + Returns: + A numpy array of priorities for the valid bins. + """ + # Prioritize bins with the least remaining capacity (but still able to fit the item) + priorities = -valid_bins # Negative because we want to maximize the priority for the smallest remaining capacity + return priorities + + + obp = OBPEvaluation() + ave_bins = obp.evaluate_program('_', priority) + print(ave_bins) + +# Task configuration for benchmark task +ENTRY_NAME = 'priority' +FUNCTION_SIGNATURE = 'def priority(...):' +IMPORT_HEADER = 'import numpy as np\nimport math' +TASK_DESCRIPTION = 'Implement a function that returns the priority with which we want to add an item to each bin.' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `priority` for the LLM4AD task.\\n\\nTask description:\\nImplement a function that returns the priority with which we want to add an item to each bin.\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\n\ndef priority(item: float, bins: np.ndarray) -> np.ndarray:\n """Returns priority with which we want to add item to each bin.\n Args:\n item: Size of item to be added to the bin.\n bins: Array of capacities for each bin.\n Return:\n Array of same size as bins with priority score of each bin.\n """\n return item - bins' +EVAL_CLASS_NAME = 'OBPEvaluation' +EVAL_KWARGS = {'timeout_seconds': 20} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_online_bin_packing/generate_weibull_instances.py b/examples/benchmark_tasks/optimization_online_bin_packing/generate_weibull_instances.py new file mode 100644 index 00000000..48ca0f35 --- /dev/null +++ b/examples/benchmark_tasks/optimization_online_bin_packing/generate_weibull_instances.py @@ -0,0 +1,36 @@ +import numpy as np + + +def generate_weibull_dataset(num_instances, num_items, capacity_limit): + np.random.seed(2024) + + dataset = {} + + for i in range(num_instances): + instance = { + 'capacity': capacity_limit, + 'num_items': num_items, + 'items': [] + } + + items = [] + + # Generate random samples from Weibull(45, 3) distribution + samples = np.random.weibull(3, num_items) * 45 + + # Clip the samples at the specified limit + samples = np.clip(samples, 1, capacity_limit) + + # Round the item sizes to the nearest integer + sizes = np.round(samples).astype(int) + + # Add the items to the instance + for size in sizes: + items.append(size) + + instance['items'] = np.array(items) + + if num_items not in dataset: + dataset[f'instance_{i}'] = instance + + return dataset diff --git a/examples/benchmark_tasks/optimization_online_bin_packing/paras.yaml b/examples/benchmark_tasks/optimization_online_bin_packing/paras.yaml new file mode 100644 index 00000000..26997979 --- /dev/null +++ b/examples/benchmark_tasks/optimization_online_bin_packing/paras.yaml @@ -0,0 +1,2 @@ +name: OBPEvaluation +timeout_seconds: 20 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_online_bin_packing_2O/__init__.py b/examples/benchmark_tasks/optimization_online_bin_packing_2O/__init__.py new file mode 100644 index 00000000..e9755855 --- /dev/null +++ b/examples/benchmark_tasks/optimization_online_bin_packing_2O/__init__.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_online_bin_packing_2O +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# name: str: OBP_2O_Evaluation +# Parameters: +# timeout_seconds: int: 20 +# end +from __future__ import annotations + +import os +import pickle +from typing import Any + +import numpy as np + +from llm4ad_loader import Evaluation +# from llm4ad.task.optimization.online_bin_packing.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\n\ndef priority(item: float, bins: np.ndarray) -> np.ndarray:\n """Returns priority with which we want to add item to each bin.\n Args:\n item: Size of item to be added to the bin.\n bins: Array of capacities for each bin.\n Return:\n Array of same size as bins with priority score of each bin.\n """\n return item - bins' +task_description = 'Implement a function that returns the priority with which we want to add an item to each bin.' + +import os, sys +sys.path.insert(0, os.path.dirname(__file__)) +from generate_weibull_instances import generate_weibull_dataset +# from llm4ad.task.optimization.online_bin_packing.generate_weibull_instances import generate_weibull_dataset # Converted from LLM4AD import + +import time +from typing import Tuple + +__all__ = ['OBP_2O_Evaluation'] + + +def get_valid_bin_indices(item: float, bins: np.ndarray) -> np.ndarray: + """Returns indices of bins in which item can fit.""" + return np.nonzero((bins - item) >= 0)[0] + + +def online_binpack( + items: tuple[float, ...], bins: np.ndarray, priority: callable +) -> tuple[list[list[float, ...], ...], np.ndarray]: + """Performs online binpacking of `items` into `bins`.""" + # Track which items are added to each bin. + packing = [[] for _ in bins] + # Add items to bins. + for item in items: + # Extract bins that have sufficient space to fit item. + valid_bin_indices = get_valid_bin_indices(item, bins) + # Score each bin based on heuristic. + priorities = priority(item, bins[valid_bin_indices]) + # Add item to bin with highest priority. + best_bin = valid_bin_indices[np.argmax(priorities)] + bins[best_bin] -= item + packing[best_bin].append(item) + # Remove unused bins from packing. + packing = [bin_items for bin_items in packing if bin_items] + return packing, bins + + +def evaluate(instances: dict, priority: callable) -> np.ndarray: + """Evaluate heuristic function on a set of online binpacking instances.""" + # List storing number of bins used for each instance. + num_bins = [] + + start_time = time.time() + + # Perform online binpacking for each instance. + for name in instances: + instance = instances[name] + capacity = instance['capacity'] + items = instance['items'] + # Create num_items bins so there will always be space for all items, + # regardless of packing order. Array has shape (num_items,). + bins = np.array([capacity for _ in range(instance['num_items'])]) + # Pack items into bins and return remaining capacity in bins_packed, which + # has shape (num_items,). + _, bins_packed = online_binpack(items, bins, priority) + # If remaining capacity in a bin is equal to initial capacity, then it is + # unused. Count number of used bins. + num_bins.append((bins_packed != capacity).sum()) + # Score of heuristic function is negative of average number of bins used + # across instances (as we want to minimize number of bins). + running_time = time.time() - start_time + return np.array([-np.mean(num_bins), -running_time/len(instances)]) + + +class OBP_2O_Evaluation(Evaluation): + """Evaluator for online bin packing problem.""" + + def __init__(self, timeout_seconds=60, data_file='weibull_train.pkl', data_key='weibull_5k_train', **kwargs): + """ + Args: + - 'data_file' (str): The data file to load (default is 'weibull_5k_train.pkl'). + - 'data_key' (str): The key of the data to load (default is 'data_key'). + + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + self._datasets = generate_weibull_dataset(5, 5000, 100) + + def evaluate_program(self, program_str: str, callable_func: callable) -> Any | None: + return evaluate(self._datasets, callable_func) + + +if __name__ == '__main__': + import numpy as np + + + def priority(item: float, bins: np.ndarray) -> np.ndarray: + """Returns priority with which we want to add item to each bin. + Args: + item: Size of item to be added to the bin. + bins: Array of capacities for each bin. + Return: + Array of same size as bins with priority score of each bin. + """ + return -bins + + + bpp = OBP_2O_Evaluation() + bpp.evaluate_program('_', priority) + +# Task configuration for benchmark task +ENTRY_NAME = 'priority' +FUNCTION_SIGNATURE = 'def priority(...):' +IMPORT_HEADER = 'import numpy as np\nimport math' +TASK_DESCRIPTION = 'Implement a function that returns the priority with which we want to add an item to each bin.' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `priority` for the LLM4AD task.\\n\\nTask description:\\nImplement a function that returns the priority with which we want to add an item to each bin.\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\n\ndef priority(item: float, bins: np.ndarray) -> np.ndarray:\n """Returns priority with which we want to add item to each bin.\n Args:\n item: Size of item to be added to the bin.\n bins: Array of capacities for each bin.\n Return:\n Array of same size as bins with priority score of each bin.\n """\n return item - bins' +EVAL_CLASS_NAME = 'OBP_2O_Evaluation' +EVAL_KWARGS = {'timeout_seconds': 60} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_online_bin_packing_2O/generate_weibull_instances.py b/examples/benchmark_tasks/optimization_online_bin_packing_2O/generate_weibull_instances.py new file mode 100644 index 00000000..3bc3dec8 --- /dev/null +++ b/examples/benchmark_tasks/optimization_online_bin_packing_2O/generate_weibull_instances.py @@ -0,0 +1,36 @@ +import numpy as np + +def generate_weibull_dataset(num_instances, num_items, capacity_limit): + + np.random.seed(2024) + + dataset = {} + + for i in range(num_instances): + instance = { + 'capacity': capacity_limit, + 'num_items': num_items, + 'items': [] + } + + items = [] + + # Generate random samples from Weibull(45, 3) distribution + samples = np.random.weibull(3, num_items) * 45 + + # Clip the samples at the specified limit + samples = np.clip(samples, 1, capacity_limit) + + # Round the item sizes to the nearest integer + sizes = np.round(samples).astype(int) + + # Add the items to the instance + for size in sizes: + items.append(size) + + instance['items'] = np.array(items) + + if num_items not in dataset: + dataset[f'instance_{i}'] = instance + + return dataset \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_online_bin_packing_2O/paras.yaml b/examples/benchmark_tasks/optimization_online_bin_packing_2O/paras.yaml new file mode 100644 index 00000000..44b540cb --- /dev/null +++ b/examples/benchmark_tasks/optimization_online_bin_packing_2O/paras.yaml @@ -0,0 +1,2 @@ +name: OBP_2O_Evaluation +timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_open_shop_scheduling/__init__.py b/examples/benchmark_tasks/optimization_open_shop_scheduling/__init__.py new file mode 100644 index 00000000..e5444343 --- /dev/null +++ b/examples/benchmark_tasks/optimization_open_shop_scheduling/__init__.py @@ -0,0 +1,307 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_open_shop_scheduling +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# References: +# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language +# model agents in algorithm search for combinatorial optimization. +# arXiv preprint arXiv:2504.04310 (2025). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any +import numpy as np +from llm4ad_loader import Evaluation +from llm4ad_loader import load_subdir_as_text +# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader +# from llm4ad.task.optimization.co_bench.open_shop_scheduling_co_bench.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n_jobs: int, n_machines: int, times: list, machines: list) -> dict:\n """\n Solves a single open shop scheduling test case.\n Input kwargs:\n - n_jobs (int): Number of jobs.\n - n_machines (int): Number of machines (and operations per job).\n - times (list of list of int): A 2D list of processing times for each operation.\n Dimensions: n_jobs x n_machines.\n - machines (list of list of int): A 2D list specifying the machine assignment for each operation.\n Dimensions: n_jobs x n_machines. Note machine is 1-indexed.\n Output:\n solution (dict): A dictionary containing:\n - start_times (list of list of int): A 2D list of start times for each operation.\n Dimensions: n_jobs x n_machines.\n Each start time must be a non-negative integer, and the schedule must respect the following constraint:\n (i) Non-parallel operation: Each job must be processed on only one machine at a time\n (ii) Machine exclusivity: For operations assigned to the same machine, their processing intervals must not overlap.\n The evaluation function will use the start_times to compute the makespan and verify the constraints.\n """\n\n # Extract the case parameters\n n_jobs = kwargs["n_jobs"]\n n_machines = kwargs["n_machines"]\n times = kwargs["times"]\n machines = kwargs["machines"]\n\n # TODO: Implement the scheduling algorithm here.\n # For now, we provide a dummy solution where all operations start at time 0.\n\n # Create a start_times list with dimensions n_jobs x n_machines, initializing all start times to 0.\n start_times = [[0 for _ in range(n_machines)] for _ in range(n_jobs)]\n\n # Build the solution dictionary.\n solution = {"start_times": start_times}\n\n return solution' +task_description = '("The Open Shop Scheduling Problem involves scheduling a set of jobs across a set of machines with "' + + +__all__ = ['OSSEvaluationCB'] + + +class OSSEvaluationCB(Evaluation): + + def __init__(self, + timeout_seconds=50, + **kwargs): + + """ + Args: + None + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + # Load datasets from Hugging Face + dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Open shop scheduling") + self._datasets = {} + for filename in dataset: + # Join all text rows into a single string + text_content = '\n'.join([row['text'] for row in dataset[filename]]) + self._datasets[filename] = text_content + + def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: + return self.evaluate(callable_func) + + def evaluate(self, eva: callable) -> float | None: + ins_cases = [] + for case_id, ins in enumerate(self._datasets.values()): + ins_cases.append(self.load_data(ins)) + + fitness_list = [] + try: + for i in ins_cases: + for j in i: + result = eva(j['n_jobs'], j['n_machines'], j['times'], j['machines']) + fitness = self.eval_func(j['n_jobs'], j['n_machines'], j['times'], j['machines'], result['start_times'], lower_bound=j['lower_bound'], upper_bound=j['upper_bound']) + fitness_list.append(fitness) + + return -np.mean(fitness_list) + + except ValueError as e: + print(e) + return None + + def load_data(self, input_string): + cases = [] + lines = [line.strip() for line in input_string.split('\n') if line.strip()] # remove blank lines + + i = 0 + while i < len(lines): + # Look for a header line starting with "Nb of jobs" + if lines[i].startswith("number of jobs"): + # Next line contains six numbers: n_jobs, n_machines, time_seed, machine_seed, upper_bound, lower_bound + i += 1 + header_tokens = lines[i].split() + if len(header_tokens) < 6: + raise ValueError("Header line does not contain 6 values.") + n_jobs = int(header_tokens[0]) + n_machines = int(header_tokens[1]) + time_seed = int(header_tokens[2]) + machine_seed = int(header_tokens[3]) + upper_bound = int(header_tokens[4]) + lower_bound = int(header_tokens[5]) + + # Find the "Times" section + i += 1 + if not lines[i].lower().startswith("processing"): + raise ValueError("Expected 'Times' section, got: " + lines[i]) + i += 1 # move to first line of times + times = [] + for _ in range(n_jobs): + # Each line should contain n_machines numbers + time_line = list(map(int, lines[i].split())) + if len(time_line) != n_machines: + raise ValueError(f"Expected {n_machines} numbers in times row, got {len(time_line)}") + times.append(time_line) + i += 1 + + # Find the "Machines" section + if i >= len(lines) or not lines[i].lower().startswith("machines"): + raise ValueError("Expected 'Machines' section, got: " + (lines[i] if i < len(lines) else "EOF")) + i += 1 # move to first line of machines + machines = [] + for _ in range(n_jobs): + machine_line = list(map(int, lines[i].split())) + if len(machine_line) != n_machines: + raise ValueError(f"Expected {n_machines} numbers in machines row, got {len(machine_line)}") + machines.append(machine_line) + i += 1 + + # Build the test case dictionary and add to the list of cases. + case = { + "n_jobs": n_jobs, + "n_machines": n_machines, + "time_seed": time_seed, + "machine_seed": machine_seed, + "upper_bound": upper_bound, + "lower_bound": lower_bound, + "times": times, + "machines": machines + } + cases.append(case) + else: + # If the current line is not a header, skip it. + i += 1 + + return cases + + def eval_func(self, n_jobs, n_machines, times, machines, start_times, **kwargs): + """ + Evaluates the solution for a open shop scheduling problem. + Input: + n_jobs (int): Number of jobs. + n_machines (int): Number of machines. + times (list of list of int): Processing times for each operation. + Dimensions: n_jobs x n_machines. + machines (list of list of int): Machine assignments for each operation. + Dimensions: n_jobs x n_machines. + start_times (list of list of int): Proposed start times for each operation. + Dimensions: n_jobs x n_machines. + kwargs: Other parameters that may be provided, which are ignored here. + Output: + score (int): The makespan, defined as the maximum completion time across all jobs. + Raises: + ValueError: If any scheduling constraints are violated. + """ + + # Check that start_times dimensions match the problem dimensions. + if len(start_times) != n_jobs: + raise ValueError(f"Expected start_times to have {n_jobs} rows, got {len(start_times)}") + for i, row in enumerate(start_times): + if len(row) != n_machines: + raise ValueError(f"Expected start_times row {i} to have {n_machines} entries, got {len(row)}") + for t in row: + if t < 0: + raise ValueError("Start times must be non-negative.") + + job_operations = [] + job_completion_times = [] + for i in range(n_jobs): + job_operations.append([]) + finish_time = 0 + for j in range(n_machines): + st = start_times[i][j] + pt = times[i][j] + finish_time = max(finish_time, st + pt) + job_operations[i].append((st, st + pt)) + job_completion_times.append(finish_time) + + for job_id in range(n_jobs): + ops = sorted(job_operations[job_id], key=lambda x: x[0]) # Sort by start time + for i in range(len(ops) - 1): + if ops[i][1] > ops[i + 1][0]: # End time of current > start time of next + raise ValueError(f"Overlapping operations for job {job_id}: {ops[i]} and {ops[i + 1]}") + + # Constraint: Machine non-overlap. + # Build a dictionary mapping machine id to a list of (start_time, finish_time, job, op_index) + machine_schedules = {} + for i in range(n_jobs): + for j in range(n_machines): + machine_id = machines[i][j] + st = start_times[i][j] + pt = times[i][j] + finish_time = st + pt + if machine_id not in machine_schedules: + machine_schedules[machine_id] = [] + machine_schedules[machine_id].append((st, finish_time, i, j)) + + # For each machine, sort operations by start time and check for overlaps. + for machine_id, ops in machine_schedules.items(): + ops_sorted = sorted(ops, key=lambda x: x[0]) + for k in range(1, len(ops_sorted)): + prev_st, prev_finish, prev_job, prev_op = ops_sorted[k - 1] + curr_st, curr_finish, curr_job, curr_op = ops_sorted[k] + if prev_finish > curr_st: + raise ValueError( + f"Machine {machine_id}: Operation from job {prev_job}, op {prev_op} (finishing at {prev_finish}) overlaps with job {curr_job}, op {curr_op} (starting at {curr_st}).") + + # Compute the makespan as the maximum completion time among all jobs. + makespan = max(job_completion_times) + + score = kwargs['lower_bound'] / makespan + + return score + + def get_dev(self): + dev = {'tai10_10.txt': [7, 8, 3, 9, 2], 'tai15_15.txt': [7, 0, 8, 4, 5], 'tai20_20.txt': [6, 0, 3, 8, 2], + 'tai4_4.txt': [0, 7, 5, 8, 6], 'tai5_5.txt': [3, 0, 9, 8, 1], 'tai7_7.txt': [3, 0, 8, 2, 1]} + + return dev + + + + + + +# Task configuration for benchmark task +ENTRY_NAME = 'solve' +FUNCTION_SIGNATURE = 'def solve(...):' +IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' +TASK_DESCRIPTION = '("The Open Shop Scheduling Problem involves scheduling a set of jobs across a set of machines with "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The Open Shop Scheduling Problem involves scheduling a set of jobs across a set of machines with "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n_jobs: int, n_machines: int, times: list, machines: list) -> dict:\n """\n Solves a single open shop scheduling test case.\n Input kwargs:\n - n_jobs (int): Number of jobs.\n - n_machines (int): Number of machines (and operations per job).\n - times (list of list of int): A 2D list of processing times for each operation.\n Dimensions: n_jobs x n_machines.\n - machines (list of list of int): A 2D list specifying the machine assignment for each operation.\n Dimensions: n_jobs x n_machines. Note machine is 1-indexed.\n Output:\n solution (dict): A dictionary containing:\n - start_times (list of list of int): A 2D list of start times for each operation.\n Dimensions: n_jobs x n_machines.\n Each start time must be a non-negative integer, and the schedule must respect the following constraint:\n (i) Non-parallel operation: Each job must be processed on only one machine at a time\n (ii) Machine exclusivity: For operations assigned to the same machine, their processing intervals must not overlap.\n The evaluation function will use the start_times to compute the makespan and verify the constraints.\n """\n\n # Extract the case parameters\n n_jobs = kwargs["n_jobs"]\n n_machines = kwargs["n_machines"]\n times = kwargs["times"]\n machines = kwargs["machines"]\n\n # TODO: Implement the scheduling algorithm here.\n # For now, we provide a dummy solution where all operations start at time 0.\n\n # Create a start_times list with dimensions n_jobs x n_machines, initializing all start times to 0.\n start_times = [[0 for _ in range(n_machines)] for _ in range(n_jobs)]\n\n # Build the solution dictionary.\n solution = {"start_times": start_times}\n\n return solution' +EVAL_CLASS_NAME = 'OSSEvaluationCB' +EVAL_KWARGS = {'timeout_seconds': 60} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_open_shop_scheduling/paras.yaml b/examples/benchmark_tasks/optimization_open_shop_scheduling/paras.yaml new file mode 100644 index 00000000..5ba59822 --- /dev/null +++ b/examples/benchmark_tasks/optimization_open_shop_scheduling/paras.yaml @@ -0,0 +1,2 @@ +name: OSSEvaluationCB +timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_ovrp_construct/__init__.py b/examples/benchmark_tasks/optimization_ovrp_construct/__init__.py new file mode 100644 index 00000000..963b3fe2 --- /dev/null +++ b/examples/benchmark_tasks/optimization_ovrp_construct/__init__.py @@ -0,0 +1,299 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_ovrp_construct +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# Module Name: OVRPEvaluation +# Last Revision: 2025/2/16 +# Description: Evaluates the Open Vehicle Routing Problem (OVRP). +# Given a set of customers and a fleet of vehicles with limited capacity, +# the goal is to find optimal routes for the vehicles to serve all customers +# while minimizing the total travel distance. +# This module is part of the LLM4AD project (https://github.com/Optima-CityU/llm4ad). +# +# Parameters: +# - timeout_seconds: Maximum allowed time (in seconds) for the evaluation process: int (default: 20). +# - n_instance: Number of problem instances to generate: int (default: 16). +# - problem_size: Number of customers to serve: int (default: 50). +# - capacity: Maximum capacity of each vehicle: int (default: 40). +# +# References: +# - Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +import os, sys +sys.path.insert(0, os.path.dirname(__file__)) +import copy +from typing import Any +import numpy as np +import matplotlib.pyplot as plt + +from llm4ad_loader import Evaluation +from get_instance import GetData +# from llm4ad.task.optimization.ovrp_construct.get_instance import GetData # Converted from LLM4AD import +# from llm4ad.task.optimization.ovrp_construct.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\n\ndef select_next_node(current_node: int, depot: int, unvisited_nodes: np.ndarray, rest_capacity: np.ndarray, demands: np.ndarray, distance_matrix: np.ndarray) -> int:\n """Design a novel algorithm to select the next node in each step.\n Args:\n current_node: ID of the current node.\n depot: ID of the depot.\n unvisited_nodes: Array of IDs of unvisited nodes.\n rest_capacity: rest capacity of vehicle\n demands: demands of nodes\n distance_matrix: Distance matrix of nodes.\n Return:\n ID of the next node to visit.\n """\n next_node = unvisited_nodes[0]\n return next_node' +task_description = '"' + + + +class OVRPEvaluation(Evaluation): + def __init__(self, + timeout_seconds=20, + problem_size=50, + n_instance=16, + **kwargs): + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + self.problem_size = problem_size + 1 + self.n_instance = n_instance + + getData = GetData(self.n_instance, self.problem_size) + self._datasets = getData.generate_instances() + + def plot_solution(self, instance: np.ndarray, route: list, demands: list, vehicle_capacity: int): + """ + Plot the solution of the Open Vehicle Routing Problem (Open VRP). + + Args: + instance: A 2D array of node coordinates (including the depot). + route: A list representing the sequence of nodes visited in the route. + demands: A list of demands for each node. + vehicle_capacity: The capacity of the vehicle. + """ + # Extract coordinates + x = instance[:, 0] + y = instance[:, 1] + + # Create a figure and axis + fig, ax = plt.subplots(figsize=(10, 8)) + + # Plot depot (node 0) + ax.plot(x[0], y[0], 'ro', markersize=10, label='Depot') + ax.text(x[0], y[0], 'Depot', ha='center', va='bottom', fontsize=12) + + # Plot customer nodes + for i in range(1, len(x)): + ax.plot(x[i], y[i], 'bo', markersize=8) + ax.text(x[i], y[i], f'C{i}\nDem: {demands[i]}', ha='center', va='bottom', fontsize=8) + + # Split the route into individual vehicle routes + routes = [] + current_route = [0] # Start each route from the depot + for node in route: + if node == 0 and len(current_route) > 1: # Start a new route from the depot + routes.append(current_route) + current_route = [0] + else: + current_route.append(node) + if current_route: # Add the last route if it exists + routes.append(current_route) + + # Plot each route in a different color + colors = plt.cm.tab10.colors # Use a colormap for distinct colors + for i, r in enumerate(routes): + color = colors[i % len(colors)] # Cycle through colors + for j in range(len(r) - 1): + start_node = r[j] + end_node = r[j + 1] + ax.plot([x[start_node], x[end_node]], [y[start_node], y[end_node]], color=color, linestyle='--', linewidth=1, label=f'Route {i + 1}' if j == 0 else None) + + # Add load information + if end_node != 0: # If not returning to the depot + ax.text((x[start_node] + x[end_node]) / 2, (y[start_node] + y[end_node]) / 2, + f'Load: {sum(demands[r[:j + 1]])}', ha='center', va='center', fontsize=8, rotation=45) + + # Mark start and end nodes of the route with triangles (excluding depot) + if len(r) > 1: + ax.plot(x[r[1]], y[r[1]], '^', color=color, markersize=10, label='Start' if i == 0 else None) # Start node + ax.plot(x[r[-1]], y[r[-1]], 'v', color=color, markersize=10, label='End' if i == 0 else None) # End node + + # Set axis labels and title + ax.set_xlabel('X Coordinate') + ax.set_ylabel('Y Coordinate') + ax.set_title('Open Vehicle Routing Problem (Open VRP) Solution') + ax.legend(loc='upper right') + + # Show the plot + plt.tight_layout() + plt.show() + + def tour_cost(self, instance, solution): + cost = 0 + for j in range(len(solution) - 1): + cost += np.linalg.norm(instance[int(solution[j])] - instance[int(solution[j + 1])]) + return cost + + def route_construct(self, distance_matrix, demands, vehicle_capacity, heuristic): + route = [] + current_load = 0 + current_node = 0 + route.append(current_node) + + unvisited_nodes = set(range(1, self.problem_size)) # Assuming node 0 is the depot + all_nodes = np.array(list(unvisited_nodes)) + feasible_unvisited_nodes = all_nodes + + while unvisited_nodes: + next_node = heuristic(current_node, + 0, + feasible_unvisited_nodes, # copy + vehicle_capacity - current_load, + copy.deepcopy(demands), # copy + copy.deepcopy(distance_matrix)) # copy + if next_node == 0: + # Update route and load + route.append(next_node) + current_load = 0 + current_node = 0 + else: + # Update route and load + route.append(next_node) + current_load += demands[next_node] + unvisited_nodes.remove(next_node) + current_node = next_node + + feasible_nodes_capacity = np.array([node for node in all_nodes if current_load + demands[node] <= vehicle_capacity]) + # Determine feasible and unvisited nodes + feasible_unvisited_nodes = np.intersect1d(feasible_nodes_capacity, list(unvisited_nodes)) + + if len(unvisited_nodes) > 0 and len(feasible_unvisited_nodes) < 1: + route.append(0) + current_load = 0 + current_node = 0 + feasible_unvisited_nodes = np.array(list(unvisited_nodes)) + + # check if not all nodes have been visited + independent_values = set(route) + if len(independent_values) != self.problem_size: + return None + + return route + + def evaluate(self, heuristic): + dis = np.ones(self.n_instance) + n_ins = 0 + + for instance, distance_matrix, demands, vehicle_capacity in self._datasets: + route = self.route_construct(distance_matrix, demands, vehicle_capacity, heuristic) + LLM_dis = self.tour_cost(instance, route) + dis[n_ins] = LLM_dis + n_ins += 1 + if n_ins == self.n_instance: + break + + ave_dis = np.average(dis) + return -ave_dis + + def evaluate_program(self, program_str: str, callable_func: callable) -> Any | None: + return self.evaluate(callable_func) + + +if __name__ == '__main__': + def select_next_node(current_node: int, depot: int, unvisited_nodes: np.ndarray, rest_capacity: np.ndarray, demands: np.ndarray, distance_matrix: np.ndarray) -> int: + """Design a novel algorithm to select the next node in each step. + Args: + current_node: ID of the current node. + depot: ID of the depot. + unvisited_nodes: Array of IDs of unvisited nodes. + rest_capacity: rest capacity of vehicle + demands: demands of nodes + distance_matrix: Distance matrix of nodes. + Return: + ID of the next node to visit. + """ + next_node = unvisited_nodes[0] + return next_node + + + eval = OVRPEvaluation() + res = eval.evaluate_program('', select_next_node) + print(res) + +# Task configuration for benchmark task +ENTRY_NAME = 'select_next_node' +FUNCTION_SIGNATURE = 'def select_next_node(...):' +IMPORT_HEADER = 'import numpy as np\nimport math' +TASK_DESCRIPTION = '"' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `select_next_node` for the LLM4AD task.\\n\\nTask description:\\n"\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\n\ndef select_next_node(current_node: int, depot: int, unvisited_nodes: np.ndarray, rest_capacity: np.ndarray, demands: np.ndarray, distance_matrix: np.ndarray) -> int:\n """Design a novel algorithm to select the next node in each step.\n Args:\n current_node: ID of the current node.\n depot: ID of the depot.\n unvisited_nodes: Array of IDs of unvisited nodes.\n rest_capacity: rest capacity of vehicle\n demands: demands of nodes\n distance_matrix: Distance matrix of nodes.\n Return:\n ID of the next node to visit.\n """\n next_node = unvisited_nodes[0]\n return next_node' +EVAL_CLASS_NAME = 'OVRPEvaluation' +EVAL_KWARGS = {'timeout_seconds': 20} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_ovrp_construct/get_instance.py b/examples/benchmark_tasks/optimization_ovrp_construct/get_instance.py new file mode 100644 index 00000000..dddc3422 --- /dev/null +++ b/examples/benchmark_tasks/optimization_ovrp_construct/get_instance.py @@ -0,0 +1,50 @@ +import pickle + +import numpy as np + + +class GetData: + def __init__(self, n_instance, n_cities): + self.n_instance = n_instance + self.n_cities = n_cities + + def generate_instances(self): + """each instance -> (coordinates, distances, demands, capacity)""" + np.random.seed(2024) + instance_data = [] + for _ in range(self.n_instance): + coordinates = np.random.rand(self.n_cities, 2) + demands = np.random.randint(1, 10, size=self.n_cities) + capacity = 40 + distances = np.linalg.norm(coordinates[:, np.newaxis] - coordinates, axis=2) + instance_data.append((coordinates, distances, demands, capacity)) + return instance_data + + +if __name__ == '__main__': + gd = GetData(10, 51) + data = gd.generate_instances() + with open('data.pkl', 'wb') as f: + pickle.dump(data, f) + + prompt_code_temp = "import numpy as np\n\ + def select_next_node(current_node: int, depot: int, unvisited_nodes: np.ndarray, rest_capacity: np.ndarray, demands: np.ndarray, distance_matrix: np.ndarray) -> int: \n\ + \n\ + '''Design a novel algorithm to select the next node in each step.\n\ + \n\ + Args:\n\ + current_node: ID of the current node.\n\ + depot: ID of the depot.\n\ + unvisited_nodes: Array of IDs of unvisited nodes.\n\ + rest_capacity: rest capacity of vehicle \n\ + demands: demands of nodes \n\ + distance_matrix: Distance matrix of nodes.\n\ + \n\ + Return:\n\ + ID of the next node to visit.\n\ + '''\n\ + next_node = unvisited_nodes[0]\n\ + \n\ + return next_node\n" + + print(prompt_code_temp) diff --git a/examples/benchmark_tasks/optimization_ovrp_construct/paras.yaml b/examples/benchmark_tasks/optimization_ovrp_construct/paras.yaml new file mode 100644 index 00000000..a95d8853 --- /dev/null +++ b/examples/benchmark_tasks/optimization_ovrp_construct/paras.yaml @@ -0,0 +1,2 @@ +name: OVRPEvaluation +timeout_seconds: 20 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_p_median_capacitated/__init__.py b/examples/benchmark_tasks/optimization_p_median_capacitated/__init__.py new file mode 100644 index 00000000..6734192d --- /dev/null +++ b/examples/benchmark_tasks/optimization_p_median_capacitated/__init__.py @@ -0,0 +1,357 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_p_median_capacitated +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# References: +# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language +# model agents in algorithm search for combinatorial optimization. +# arXiv preprint arXiv:2504.04310 (2025). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any +import numpy as np +from llm4ad_loader import Evaluation +from llm4ad_loader import load_subdir_as_text +# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader +# from llm4ad.task.optimization.co_bench.p_median_capacitated_co_bench.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(best_known: float, n: int, p: int, Q: float, customers: list) -> dict:\n """\n Solve the Capacitated P-Median Problem.\n This function receives the data for one problem instance via keyword arguments:\n - best_known (float): Best known solution value for reference.\n - n (int): Number of customers.\n - p (int): Number of medians to choose.\n - Q (float): Capacity limit for each median.\n - customers (list of tuples): Each tuple is (customer_id, x, y, demand).\n The goal is to select p medians (from the customers) and assign every customer to one\n of these medians so that the total cost is minimized. The cost for a customer is the\n Euclidean distance (rounded down to the nearest integer) to its assigned median, and the\n total demand assigned to each median must not exceed Q.\n Evaluation Metric:\n The solution is evaluated by computing the ratio:\n score = best_known / computed_total_cost,\n where computed_total_cost is the sum over all customers of the (floored) Euclidean distance\n to its assigned median.\n Note: This is a placeholder function. Replace the placeholder with an actual algorithm.\n Returns:\n A dictionary with the following keys:\n - \'objective\': (numeric) the total cost (objective value) computed by the algorithm.\n - \'medians\': (list of int) exactly p customer IDs chosen as medians.\n - \'assignments\': (list of int) a list of n integers, where the i-th integer is the customer\n ID (from the chosen medians) assigned to customer i.\n """\n # Placeholder: Replace this with your actual implementation.\n # For now, we return an empty solution structure.\n return {\n "objective": 0, # total cost (to be computed)\n "medians": [], # list of p medians (customer IDs)\n "assignments": [] # list of n assignments (each is one of the medians)\n }' +task_description = '("The Capacitated P-Median Problem is a facility location optimization problem where the objective "' + + +__all__ = ['PMCEvaluationCB'] + + +class PMCEvaluationCB(Evaluation): + + def __init__(self, + timeout_seconds=50, + **kwargs): + + """ + Args: + None + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + # Load datasets from Hugging Face + dataset = load_subdir_as_text("CO-Bench/CO-Bench", "p-median - capacitated") + self._datasets = {} + for filename in dataset: + # Join all text rows into a single string + text_content = '\n'.join([row['text'] for row in dataset[filename]]) + self._datasets[filename] = text_content + + def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: + return self.evaluate(callable_func) + + def evaluate(self, eva: callable) -> float | None: + ins_cases = [] + for case_id, ins in enumerate(self._datasets.values()): + ins_cases.append(self.load_data(ins)) + + fitness_list = [] + try: + for i in ins_cases: + for j in i: + result = eva(j['best_known'], j['n'], j['p'], j['Q'], j['customers']) + fitness = self.eval_func(best_known=j['best_known'], n=j['n'], p=j['p'], Q=j['Q'], customers=j['customers'], objective=result['objective'], medians=result['medians'], assignments=result['assignments']) + fitness_list.append(fitness) + + return -np.mean(fitness_list) + + except ValueError as e: + print(e) + return None + + def load_data(self, input_string): + """ + Load one or more instances of the Capacitated P-Median Problem from a text file. + The input file structure is: + Line 1: An integer M, the number of problem instances in the file. + Then, for each instance: + - A header line with two values: + - A line with three values:

+ - n subsequent lines each with: + Returns: + A list of dictionaries. Each dictionary contains the keys: + - 'best_known': float + - 'n': int + - 'p': int + - 'Q': float + - 'customers': list of tuples (customer_id, x, y, demand) + """ + cases = [] + try: + lines = [line.strip() for line in input_string.split('\n') if line.strip() != ''] + except Exception as e: + raise ValueError("Error reading input file: " + str(e)) + + if not lines: + raise ValueError("Input file is empty.") + + try: + M = int(lines[0]) + except Exception as e: + raise ValueError("The first line must be an integer representing the number of cases.") + + index = 1 + for case_idx in range(M): + if index >= len(lines): + raise ValueError("Unexpected end of file when reading case {}.".format(case_idx + 1)) + + # Read problem header: + tokens = lines[index].split() + if len(tokens) < 2: + raise ValueError("Invalid problem header at case {}.".format(case_idx + 1)) + try: + # We don't need the problem number, so we can ignore it. + _ = int(tokens[0]) + best_known = float(tokens[1]) + except Exception as e: + raise ValueError("Error parsing problem header at case {}: {}".format(case_idx + 1, e)) + index += 1 + + if index >= len(lines): + raise ValueError("Missing instance parameters for case {}.".format(case_idx + 1)) + + # Read instance parameters:

+ tokens = lines[index].split() + if len(tokens) < 3: + raise ValueError("Invalid instance parameters at case {}.".format(case_idx + 1)) + try: + n = int(tokens[0]) + p = int(tokens[1]) + Q = float(tokens[2]) + except Exception as e: + raise ValueError("Error parsing instance parameters at case {}: {}".format(case_idx + 1, e)) + index += 1 + + # Read n customer lines + customers = [] + if len(lines) < index + n: + raise ValueError("Expected {} customer lines for case {}, but found fewer.".format(n, case_idx + 1)) + for i in range(n): + tokens = lines[index].split() + if len(tokens) < 4: + raise ValueError("Invalid customer data at line {} in case {}.".format(index + 1, case_idx + 1)) + try: + customer_id = int(tokens[0]) + x = float(tokens[1]) + y = float(tokens[2]) + demand = float(tokens[3]) + except Exception as e: + raise ValueError( + "Error parsing customer data on line {} in case {}: {}".format(index + 1, case_idx + 1, e)) + customers.append((customer_id, x, y, demand)) + index += 1 + + case_data = { + "best_known": best_known, + "n": n, + "p": p, + "Q": Q, + "customers": customers + } + cases.append(case_data) + + return cases + + def eval_func(self, **kwargs): + """ + Evaluate the solution for a single instance of the Capacitated P-Median Problem. + This function expects the following keyword arguments (combined from the instance data and the solution): + - best_known (float): Best known solution value (for reference). + - n (int): Number of customers. + - p (int): Number of medians. + - Q (float): Capacity of each median. + - customers (list of tuples): Each tuple is (customer_id, x, y, demand). + - objective (numeric): The objective value (total cost) reported by the solution. + - medians (list of int): List of chosen medians (customer IDs), exactly p elements. + - assignments (list of int): List of assignments for each customer (length n), where each entry is one of the chosen medians. + The evaluation performs the following: + 1. Verifies that each assignment is to one of the selected medians. + 2. Checks that the total demand assigned to each median does not exceed Q. + 3. Recomputes the total cost as the sum, over all customers, of the Euclidean distance (rounded down) + from the customer to its assigned median. + 4. Computes the score as: score = best_known / computed_total_cost. + Returns: + A scalar float representing the score for the solution. + """ + import math + + # Extract instance data + best_known = kwargs.get("best_known") + n = kwargs.get("n") + p = kwargs.get("p") + Q = kwargs.get("Q") + customers = kwargs.get("customers") + + # Extract solution data + reported_obj = kwargs.get("objective") + medians = kwargs.get("medians") + assignments = kwargs.get("assignments") + + if best_known is None or n is None or p is None or Q is None or customers is None: + raise ValueError("Instance data is incomplete.") + if reported_obj is None or medians is None or assignments is None: + raise ValueError("Solution data is incomplete.") + + # Validate medians length + if len(medians) != p: + raise ValueError("The solution must contain exactly {} medians; found {}.".format(p, len(medians))) + + # Validate assignments length + if len(assignments) != n: + raise ValueError("The solution must contain exactly {} assignments; found {}.".format(n, len(assignments))) + + # Build a dictionary for quick lookup of customer data by customer_id. + cust_dict = {} + for cust in customers: + cid, x, y, demand = cust + cust_dict[cid] = (x, y, demand) + + # Verify that each median is a valid customer. + for m in medians: + if m not in cust_dict: + raise ValueError("Median {} is not found in the customer data.".format(m)) + + # Verify that each customer's assignment is one of the selected medians. + for idx, a in enumerate(assignments): + if a not in medians: + raise ValueError( + "Customer {} is assigned to {} which is not in the list of selected medians.".format(idx + 1, a)) + + # Check capacity constraints. + capacity_usage = {m: 0.0 for m in medians} + for i, a in enumerate(assignments): + # Assuming that the order of customers in 'customers' corresponds to customer 1..n. + demand = customers[i][3] + capacity_usage[a] += demand + for m, used in capacity_usage.items(): + if used > Q + 1e-6: # small tolerance + raise ValueError( + "Capacity exceeded for median {}: used capacity {:.4f} exceeds allowed capacity {:.4f}.".format(m, + used, + Q)) + + # Recompute the total cost. + total_cost = 0 + for i, a in enumerate(assignments): + # Get customer i data. + try: + cid, cx, cy, _ = customers[i] + except Exception as e: + raise ValueError("Error accessing data for customer {}: {}".format(i + 1, e)) + # Get the assigned median's coordinates. + if a not in cust_dict: + raise ValueError("Assigned median {} for customer {} not found.".format(a, i + 1)) + mx, my, _ = cust_dict[a] + d = math.sqrt((cx - mx) ** 2 + (cy - my) ** 2) + total_cost += math.floor(d) + + if total_cost <= 0: + raise ValueError("Computed total cost is non-positive, which is invalid.") + + score = best_known / total_cost + return score + + def get_dev(self): + dev = {'pmedcap1.txt': [3, 11, 16, 0, 4, 2, 1, 9, 19, 18]} + + return dev + + + + + + +# Task configuration for benchmark task +ENTRY_NAME = 'solve' +FUNCTION_SIGNATURE = 'def solve(...):' +IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' +TASK_DESCRIPTION = '("The Capacitated P-Median Problem is a facility location optimization problem where the objective "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The Capacitated P-Median Problem is a facility location optimization problem where the objective "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(best_known: float, n: int, p: int, Q: float, customers: list) -> dict:\n """\n Solve the Capacitated P-Median Problem.\n This function receives the data for one problem instance via keyword arguments:\n - best_known (float): Best known solution value for reference.\n - n (int): Number of customers.\n - p (int): Number of medians to choose.\n - Q (float): Capacity limit for each median.\n - customers (list of tuples): Each tuple is (customer_id, x, y, demand).\n The goal is to select p medians (from the customers) and assign every customer to one\n of these medians so that the total cost is minimized. The cost for a customer is the\n Euclidean distance (rounded down to the nearest integer) to its assigned median, and the\n total demand assigned to each median must not exceed Q.\n Evaluation Metric:\n The solution is evaluated by computing the ratio:\n score = best_known / computed_total_cost,\n where computed_total_cost is the sum over all customers of the (floored) Euclidean distance\n to its assigned median.\n Note: This is a placeholder function. Replace the placeholder with an actual algorithm.\n Returns:\n A dictionary with the following keys:\n - \'objective\': (numeric) the total cost (objective value) computed by the algorithm.\n - \'medians\': (list of int) exactly p customer IDs chosen as medians.\n - \'assignments\': (list of int) a list of n integers, where the i-th integer is the customer\n ID (from the chosen medians) assigned to customer i.\n """\n # Placeholder: Replace this with your actual implementation.\n # For now, we return an empty solution structure.\n return {\n "objective": 0, # total cost (to be computed)\n "medians": [], # list of p medians (customer IDs)\n "assignments": [] # list of n assignments (each is one of the medians)\n }' +EVAL_CLASS_NAME = 'PMCEvaluationCB' +EVAL_KWARGS = {'timeout_seconds': 60} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_p_median_capacitated/paras.yaml b/examples/benchmark_tasks/optimization_p_median_capacitated/paras.yaml new file mode 100644 index 00000000..62454cda --- /dev/null +++ b/examples/benchmark_tasks/optimization_p_median_capacitated/paras.yaml @@ -0,0 +1,2 @@ +name: PMCEvaluationCB +timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_p_median_uncapacitated/__init__.py b/examples/benchmark_tasks/optimization_p_median_uncapacitated/__init__.py new file mode 100644 index 00000000..9c3b82a9 --- /dev/null +++ b/examples/benchmark_tasks/optimization_p_median_uncapacitated/__init__.py @@ -0,0 +1,367 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_p_median_uncapacitated +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# References: +# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language +# model agents in algorithm search for combinatorial optimization. +# arXiv preprint arXiv:2504.04310 (2025). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any +import numpy as np +from llm4ad_loader import Evaluation +from llm4ad_loader import load_subdir_as_text +# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader +# from llm4ad.task.optimization.co_bench.p_median_uncapacitated_co_bench.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n: int, m: int, p: int, dist: list) -> dict:\n """\n Solves the uncapacitated p-median problem on a given graph.\n Input kwargs:\n - n: int, number of vertices.\n - m: int, number of edges.\n - p: int, number of medians to choose.\n - dist: list of lists, the complete cost matrix (n x n) computed via Floyd’s algorithm.\n Evaluation metric:\n The total assignment cost, defined as the sum (over all vertices) of the shortest distance\n from that vertex to its closest chosen median.\n Returns:\n A dictionary with a single key:\n - \'medians\': a list of exactly p distinct integers (each between 1 and n) representing\n the indices of the chosen medians.\n Note: This is a placeholder. The actual solution logic should populate the \'medians\' list.\n """\n # Placeholder implementation; replace with your solution logic.\n return {"medians": []}' +task_description = '("The uncapacitated p-median problem is a combinatorial optimization problem defined on a given "' + + +__all__ = ['PMUEvaluationCB'] + + +class PMUEvaluationCB(Evaluation): + + def __init__(self, + timeout_seconds=300, + **kwargs): + + """ + Args: + None + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + # Load datasets from Hugging Face + dataset = load_subdir_as_text("CO-Bench/CO-Bench", "p-median - uncapacitated") + self._datasets = {} + for filename in dataset: + # Join all text rows into a single string + text_content = '\n'.join([row['text'] for row in dataset[filename]]) + self._datasets[filename] = text_content + + def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: + return self.evaluate(callable_func) + + def evaluate(self, eva: callable) -> float | None: + ins_cases = [] + for case_id, ins in enumerate(self._datasets.values()): + ins_cases.append(self.load_data(ins)) + + fitness_list = [] + try: + for i in ins_cases: + for j in i: + result = eva(j['n'], j['m'], j['p'], j['dist']) + fitness = self.eval_func(n=j['n'], p=j['p'], m=j['m'], dist=j['dist'], medians=result['medians']) + fitness_list.append(fitness) + + return -np.mean(fitness_list) + + except ValueError as e: + print(e) + return None + + def load_data(self, input_string): + """ + Loads one or more cases from the input file for the p-median problem, optimized for speed. + This version uses NumPy to perform the Floyd–Warshall algorithm in a vectorized manner, + which is significantly faster than the pure-Python triple nested loops for moderate-to-large graphs. + The input is expected to have one or more cases. Each case starts with a header line + containing three numbers: n m p, where: + - n: number of vertices, + - m: number of edges, + - p: number of medians to choose. + This is followed by at least m non-empty lines, each specifying an edge in the format: + i j cost + (If there are more than m edge lines, only the first m valid ones are used.) + For each case, the function builds the complete cost matrix by: + - Initializing an n x n NumPy array with infinity (and 0 on the diagonal). + - Processing m valid edges (using the last occurrence for duplicate edges). + - Running a vectorized Floyd–Warshall algorithm to compute all-pairs shortest paths. + Returns: + A list of dictionaries, one per case. Each dictionary contains: + - 'n': int, number of vertices. + - 'm': int, number of edges. + - 'p': int, number of medians to choose. + - 'dist': list of lists, the complete cost matrix (n x n), converted from a NumPy array. + """ + import numpy as np + import math + + INF = math.inf + + # Read the entire file and filter out empty lines + all_lines = [line.strip() for line in input_string.split('\n')] + + cases = [] + idx = 0 + while idx < len(all_lines): + header_parts = all_lines[idx].split() + idx += 1 + if len(header_parts) < 3: + raise ValueError("Header line must contain at least three numbers: n, m, p.") + try: + n = int(header_parts[0]) + m = int(header_parts[1]) + p = int(header_parts[2]) + except Exception as e: + raise ValueError("Invalid header values.") from e + + # Initialize the cost matrix using NumPy for fast operations. + dist = np.full((n, n), INF, dtype=float) + np.fill_diagonal(dist, 0.0) + + edges_read = 0 + while edges_read < m and idx < len(all_lines): + tokens = all_lines[idx].split() + idx += 1 + if len(tokens) < 3: + continue + try: + u = int(tokens[0]) + v = int(tokens[1]) + c = float(tokens[2]) + except Exception: + continue + if 1 <= u <= n and 1 <= v <= n: + # Update both symmetric entries; the last occurrence overwrites previous ones. + dist[u - 1, v - 1] = c + dist[v - 1, u - 1] = c + edges_read += 1 + + # Vectorized Floyd–Warshall: update distances using broadcasting. + for k in range(n): + # Update: dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]) for all i, j. + dist = np.minimum(dist, dist[:, k:k + 1] + dist[k:k + 1, :]) + + # Convert the NumPy array to a list of lists for compatibility. + cases.append({ + "n": n, + "m": m, + "p": p, + "dist": dist.tolist() + }) + + return cases + + def eval_func(self, **kwargs): + """ + Evaluates a candidate solution for the uncapacitated p-median problem. + Parameters: + candidate_data (dict): Contains the input data for a single case with keys: + - 'n': int, number of vertices. + - 'm': int, number of edges. + - 'p': int, number of medians to choose. + - 'dist': list of lists, the complete cost matrix (n x n). + solution (dict): The candidate solution with key: + - 'medians': list of exactly p distinct integers (each between 1 and n). + Returns: + float: The total assignment cost, i.e., the sum over all vertices of the shortest distance + to the nearest chosen median. + Raises: + ValueError: If the solution is invalid due to incorrect format, duplicates, out-of-range values, + or if any vertex is unreachable from all medians. + """ + n = kwargs.get("n") + p = kwargs.get("p") + dist = kwargs.get("dist") + medians = kwargs.get("medians", []) + + # Validate input constraints + if not isinstance(n, int) or n <= 0: + raise ValueError("Invalid number of vertices (n). Must be a positive integer.") + if not isinstance(p, int) or p <= 0 or p > n: + raise ValueError("Invalid number of medians (p). Must be a positive integer and at most n.") + if not isinstance(dist, list) or len(dist) != n or any(len(row) != n for row in dist): + raise ValueError("Invalid distance matrix. Must be a square matrix of size (n x n).") + if not isinstance(medians, list) or len(medians) != p: + raise ValueError(f"Medians must be a list of exactly {p} distinct integers.") + if len(set(medians)) != p: + raise ValueError("Medians must be distinct values.") + if any(not isinstance(m, int) or m < 1 or m > n for m in medians): + raise ValueError("Each median must be an integer in the range [1, n].") + + INF = float('inf') + total_cost = 0.0 + + for i in range(n): + best_distance = INF + for median in medians: + d = dist[i][median - 1] # Adjust for 0-indexing. + if d < best_distance: + best_distance = d + if best_distance == INF: + raise ValueError(f"Vertex {i + 1} is unreachable from all chosen medians.") + total_cost += best_distance + + return total_cost + + def norm_score(self, results): + optimal_scores = { + "pmed1.txt": [5819], + "pmed2.txt": [4093], + "pmed3.txt": [4250], + "pmed4.txt": [3034], + "pmed5.txt": [1355], + "pmed6.txt": [7824], + "pmed7.txt": [5631], + "pmed8.txt": [4445], + "pmed9.txt": [2734], + "pmed10.txt": [1255], + "pmed11.txt": [7696], + "pmed12.txt": [6634], + "pmed13.txt": [4374], + "pmed14.txt": [2968], + "pmed15.txt": [1729], + "pmed16.txt": [8162], + "pmed17.txt": [6999], + "pmed18.txt": [4809], + "pmed19.txt": [2845], + "pmed20.txt": [1789], + "pmed21.txt": [9138], + "pmed22.txt": [8579], + "pmed23.txt": [4619], + "pmed24.txt": [2961], + "pmed25.txt": [1828], + "pmed26.txt": [9917], + "pmed27.txt": [8307], + "pmed28.txt": [4498], + "pmed29.txt": [3033], + "pmed30.txt": [1989], + "pmed31.txt": [10086], + "pmed32.txt": [9297], + "pmed33.txt": [4700], + "pmed34.txt": [3013], + "pmed35.txt": [10400], + "pmed36.txt": [9934], + "pmed37.txt": [5057], + "pmed38.txt": [11060], + "pmed39.txt": [9423], + "pmed40.txt": [5128] + } + + normed = {} + for case, (scores, error_message) in results.items(): + if case not in optimal_scores: + continue # Skip if there's no optimal score defined. + optimal_list = optimal_scores[case] + normed_scores = [] + # Compute normalized score for each index. + for idx, score in enumerate(scores): + if isinstance(score, (int, float)): + normed_scores.append(optimal_list[idx] / score) + else: + normed_scores.append(score) + normed[case] = (normed_scores, error_message) + + return normed + + def get_dev(self): + dev = {'pmed1.txt': [], 'pmed11.txt': [], 'pmed13.txt': [], + 'pmed15.txt': [], 'pmed17.txt': [], 'pmed19.txt': [], + 'pmed21.txt': [], 'pmed23.txt': [], 'pmed25.txt': [], + 'pmed27.txt': [], 'pmed29.txt': [], 'pmed3.txt': [], + 'pmed31.txt': [], 'pmed33.txt': [], 'pmed35.txt': [], + 'pmed37.txt': [], 'pmed39.txt': [], 'pmed5.txt': [], + 'pmed7.txt': [], 'pmed9.txt': []} + + return dev + + + + + + + +# Task configuration for benchmark task +ENTRY_NAME = 'solve' +FUNCTION_SIGNATURE = 'def solve(...):' +IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\n from that vertex to its closest chosen median.' +TASK_DESCRIPTION = '("The uncapacitated p-median problem is a combinatorial optimization problem defined on a given "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The uncapacitated p-median problem is a combinatorial optimization problem defined on a given "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n: int, m: int, p: int, dist: list) -> dict:\n """\n Solves the uncapacitated p-median problem on a given graph.\n Input kwargs:\n - n: int, number of vertices.\n - m: int, number of edges.\n - p: int, number of medians to choose.\n - dist: list of lists, the complete cost matrix (n x n) computed via Floyd’s algorithm.\n Evaluation metric:\n The total assignment cost, defined as the sum (over all vertices) of the shortest distance\n from that vertex to its closest chosen median.\n Returns:\n A dictionary with a single key:\n - \'medians\': a list of exactly p distinct integers (each between 1 and n) representing\n the indices of the chosen medians.\n Note: This is a placeholder. The actual solution logic should populate the \'medians\' list.\n """\n # Placeholder implementation; replace with your solution logic.\n return {"medians": []}' +EVAL_CLASS_NAME = 'PMUEvaluationCB' +EVAL_KWARGS = {'timeout_seconds': 300} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_p_median_uncapacitated/paras.yaml b/examples/benchmark_tasks/optimization_p_median_uncapacitated/paras.yaml new file mode 100644 index 00000000..9ab656bc --- /dev/null +++ b/examples/benchmark_tasks/optimization_p_median_uncapacitated/paras.yaml @@ -0,0 +1,2 @@ +name: PMUEvaluationCB +timeout_seconds: 300 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_packing_unequal_circles/__init__.py b/examples/benchmark_tasks/optimization_packing_unequal_circles/__init__.py new file mode 100644 index 00000000..f65d25e3 --- /dev/null +++ b/examples/benchmark_tasks/optimization_packing_unequal_circles/__init__.py @@ -0,0 +1,337 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_packing_unequal_circles +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# References: +# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language +# model agents in algorithm search for combinatorial optimization. +# arXiv preprint arXiv:2504.04310 (2025). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any +import numpy as np +from llm4ad_loader import Evaluation +from llm4ad_loader import load_subdir_as_text +# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader +# from llm4ad.task.optimization.co_bench.packing_unequal_circles_co_bench.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n: int, cx: float, cy: float, R: float, radii: list) -> dict:\n """\n Solve the unequal circle packing problem for the maximize-number case.\n Problem Description:\n Given a circular container with center (cx, cy) and radius R, and n circles with specified radii (sorted in increasing order),\n the task is to select and pack a prefix of the sorted list—i.e., if circle i is packed, then all circles with a smaller index must also be packed—in order to maximize the number of circles placed.\n Each packed circle must be fully contained within the container, meaning that the distance from its center to (cx, cy) plus its radius must not exceed R, and no two packed circles may overlap, which requires that the distance between any two centers is at least the sum of their respective radii.\n Input kwargs:\n - n : int, the number of circles.\n - cx : float, x-coordinate of the container\'s center.\n - cy : float, y-coordinate of the container\'s center.\n - R : float, the radius of the container.\n - radii : list of float, the radius of each circle (assumed sorted in increasing order).\n Returns:\n A dictionary with one key:\n - "coords": a list of n (x, y) tuples corresponding to the centers of the circles.\n For circles that are not packed, the coordinates default to (-1, -1).\n """\n return {"coords": []}' +task_description = '("The problem involves packing a subset of unequal circles into a fixed circular container with "' + + +__all__ = ['PUCEvaluationCB'] + + +class PUCEvaluationCB(Evaluation): + + def __init__(self, + timeout_seconds=50, + **kwargs): + + """ + Args: + None + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + # Load datasets from Hugging Face + dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Packing unequal circles") + self._datasets = {} + for filename in dataset: + # Join all text rows into a single string + text_content = '\n'.join([row['text'] for row in dataset[filename]]) + self._datasets[filename] = text_content + + def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: + return self.evaluate(callable_func) + + def evaluate(self, eva: callable) -> float | None: + ins_cases = [] + for case_id, ins in enumerate(self._datasets.values()): + ins_cases.append(self.load_data(ins)) + + fitness_list = [] + try: + for i in ins_cases: + for j in i: + result = eva(j['n'], j['cx'], j['cy'], j['R'], j['radii']) + fitness = self.eval_func(n=j['n'], cx=j['cx'], cy=j['cy'], R=j['R'], radii=j['radii'], coords=result['coords']) + fitness_list.append(fitness) + + return np.mean(fitness_list) + + except ValueError as e: + print(e) + return None + + def load_data(self, input_string): + """ + Load and parse the input file containing one or multiple cases. + File Format: + - The file is a plain-text file with non-empty lines. + - Each case starts with a header line containing exactly four numbers: + n cx cy R + where: + • n is the number of circles (an integer), + • cx and cy are the container's center coordinates (floats), + • R is the container's radius (float). + - The next n non-empty lines each contain one real number representing + the radius of a circle. + Returns: + A list of cases, where each case is a dictionary with keys: + "n" : int, number of circles. + "cx" : float, container center x-coordinate. + "cy" : float, container center y-coordinate. + "R" : float, container radius. + "radii" : list of float, the radii of the circles. + """ + cases = [] + try: + lines = [line.strip() for line in input_string.split('\n') if line.strip() != ''] + except Exception as e: + raise Exception(f"Error reading input file: {e}") + + i = 0 + total_lines = len(lines) + while i < total_lines: + header_tokens = lines[i].split() + if len(header_tokens) != 4: + raise Exception(f"Header line at line {i + 1} must contain exactly 4 numbers: n cx cy R.") + try: + n = int(header_tokens[0]) + cx = float(header_tokens[1]) + cy = float(header_tokens[2]) + R = float(header_tokens[3]) + except Exception as e: + raise Exception(f"Error parsing header on line {i + 1}: {e}") + + if i + n >= total_lines: + raise Exception(f"Not enough lines for {n} circle radii after line {i + 1}.") + radii = [] + for j in range(1, n + 1): + try: + # Even if there are extra tokens, take the first as the radius. + r = float(lines[i + j].split()[0]) + radii.append(r) + except Exception as e: + raise Exception(f"Error parsing circle radius on line {i + j + 1}: {e}") + case = {"n": n, "cx": cx, "cy": cy, "R": R, "radii": radii} + cases.append(case) + i += n + 1 # Move to the next case header (if any) + return cases + + def eval_func(self, **kwargs): + """ + Evaluate the solution for the Maximise Number problem of Unequal Circle Packing. + Input (merged from the case data and the solution): + - n : int, the total number of circles. + - cx : float, x-coordinate of the container's center. + - cy : float, y-coordinate of the container's center. + - R : float, the container's radius. + - radii : list of float, radii for each circle (assumed sorted in increasing order). + - coords: list of (x, y) tuples, the centers of the circles as produced by solve. + Evaluation Details: + 1. Identify “packed” circles. By convention, a circle is considered packed if its coordinate + is not equal to the default (cx, cy) (within tolerance). For the maximize number problem, + the optimal solution packs a prefix of the sorted circles. + 2. Verify the prefix property: if any circle i is packed, then all circles with index < i must also be packed. + 3. For every packed circle: + - Check container feasibility: + Ensure that sqrt((x-cx)^2 + (y-cy)^2) + r_i <= R. + - Record the clearance: R - (distance to (cx,cy) + r_i). + 4. For every pair of packed circles, verify non-overlap: + Ensure that distance((x_i,y_i), (x_j,y_j)) >= r_i + r_j. + And record the pair clearance. + 5. If any feasibility constraint is violated (beyond a small tolerance), raise an error. + 6. Let the primary score be the number of circles packed (i.e. the prefix length). + Use the minimum clearance among packed circles as a tie-breaker. + (For example, final score = (number packed) + ε*(minimum clearance), with ε small.) + Returns: + float: the evaluation score (a higher score indicates a better solution). + The main component is the number of circles feasibly packed. + """ + import math + + tol = 1e-5 # Numerical tolerance. + + # Extract required inputs. + try: + n = kwargs["n"] + cx = kwargs["cx"] + cy = kwargs["cy"] + container_R = kwargs["R"] + radii = kwargs["radii"] + coords = kwargs["coords"] + except KeyError as e: + raise Exception(f"Missing required parameter: {e}") + + if len(coords) != n: + raise Exception(f"Expected {n} coordinates, but got {len(coords)}.") + + # Identify packed circles. + # Convention: a circle is considered not packed if its center is (cx, cy) within tolerance. + packed_indices = [] + for i in range(n): + x, y = coords[i] + if x != -1 and y != -1: + # if math.sqrt((x - cx) ** 2 + (y - cy) ** 2) > tol: + packed_indices.append(i) + + # Verify the prefix property: if a circle with index i is packed, then all circles with index < i must be packed. + if packed_indices: + K = max(packed_indices) # highest index among packed circles. + for i in range(K): + if i not in packed_indices: + raise Exception(f"Prefix property violated: circle {i} is not packed while circle {K} is packed.") + else: + K = -1 # No circles packed. + + # Evaluate feasibility of packed circles. + container_clearances = [] + for i in packed_indices: + x, y = coords[i] + r = radii[i] + dist = math.sqrt((x - cx) ** 2 + (y - cy) ** 2) + clearance = container_R - (dist + r) + if clearance < -tol: + raise Exception(f"Circle {i} violates container constraint by {-clearance}.") + container_clearances.append(clearance) + + pair_clearances = [] + for idx, i in enumerate(packed_indices): + for j in packed_indices[idx + 1:]: + x1, y1 = coords[i] + x2, y2 = coords[j] + center_distance = math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2) + required_distance = radii[i] + radii[j] + clearance = center_distance - required_distance + if clearance < -tol: + raise Exception(f"Circles {i} and {j} overlap by {-clearance}.") + pair_clearances.append(clearance) + + # Primary measure: number of circles packed. + # (Since indices are 0-based, number_packed = K+1 if any are packed.) + num_packed = (K + 1) if packed_indices else 0 + + # Final score: primary is the count of packed circles; use clearance as a tie-breaker. + score = num_packed + return score + + def norm_score(self, results): + optimal_scores = { + "circle1.txt": [6], + "circle2.txt": [15], + "circle3.txt": [22], + "circle4.txt": [30], + } + + normed = {} + for case, (scores, error_message) in results.items(): + if case not in optimal_scores: + continue # Skip if there's no optimal score defined. + optimal_list = optimal_scores[case] + normed_scores = [] + # Compute normalized score for each index. + for idx, score in enumerate(scores): + if isinstance(score, (int, float)): + normed_scores.append(score / optimal_list[idx]) + else: + normed_scores.append(score) + normed[case] = (normed_scores, error_message) + return normed + + + + + + + +# Task configuration for benchmark task +ENTRY_NAME = 'solve' +FUNCTION_SIGNATURE = 'def solve(...):' +IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' +TASK_DESCRIPTION = '("The problem involves packing a subset of unequal circles into a fixed circular container with "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The problem involves packing a subset of unequal circles into a fixed circular container with "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n: int, cx: float, cy: float, R: float, radii: list) -> dict:\n """\n Solve the unequal circle packing problem for the maximize-number case.\n Problem Description:\n Given a circular container with center (cx, cy) and radius R, and n circles with specified radii (sorted in increasing order),\n the task is to select and pack a prefix of the sorted list—i.e., if circle i is packed, then all circles with a smaller index must also be packed—in order to maximize the number of circles placed.\n Each packed circle must be fully contained within the container, meaning that the distance from its center to (cx, cy) plus its radius must not exceed R, and no two packed circles may overlap, which requires that the distance between any two centers is at least the sum of their respective radii.\n Input kwargs:\n - n : int, the number of circles.\n - cx : float, x-coordinate of the container\'s center.\n - cy : float, y-coordinate of the container\'s center.\n - R : float, the radius of the container.\n - radii : list of float, the radius of each circle (assumed sorted in increasing order).\n Returns:\n A dictionary with one key:\n - "coords": a list of n (x, y) tuples corresponding to the centers of the circles.\n For circles that are not packed, the coordinates default to (-1, -1).\n """\n return {"coords": []}' +EVAL_CLASS_NAME = 'PUCEvaluationCB' +EVAL_KWARGS = {'timeout_seconds': 60} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_packing_unequal_circles/paras.yaml b/examples/benchmark_tasks/optimization_packing_unequal_circles/paras.yaml new file mode 100644 index 00000000..3c04ec19 --- /dev/null +++ b/examples/benchmark_tasks/optimization_packing_unequal_circles/paras.yaml @@ -0,0 +1,2 @@ +name: PUCEvaluationCB +timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_packing_unequal_circles_area/__init__.py b/examples/benchmark_tasks/optimization_packing_unequal_circles_area/__init__.py new file mode 100644 index 00000000..e1a0fedf --- /dev/null +++ b/examples/benchmark_tasks/optimization_packing_unequal_circles_area/__init__.py @@ -0,0 +1,334 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_packing_unequal_circles_area +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# References: +# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language +# model agents in algorithm search for combinatorial optimization. +# arXiv preprint arXiv:2504.04310 (2025). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any +import numpy as np +from llm4ad_loader import Evaluation +from llm4ad_loader import load_subdir_as_text +# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader +# from llm4ad.task.optimization.co_bench.packing_unequal_circles_area_co_bench.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n:int, cx: float, cy: float, R: float, radii: list) -> dict:\n """\n Solve the Unequal Circle Packing problem (Maximize Area version).\n Problem Description:\n Given a circular container with center (cx, cy) and radius R, and n circles\n with specified radii (provided in \'radii\'), decide which circles to pack and\n determine the centers (x_i, y_i) for the packed circles such that:\n 1. Containment: Each packed circle i must lie completely within the container.\n (x_i - cx)^2 + (y_i - cy)^2 <= α_i * (R - radii[i])^2, for i = 1,...,n.\n (If α_i = 0, then the circle is not packed and its center is set to (cx, cy).)\n 2. Non-Overlap: For every pair of circles i and j (with i < j), if both are packed,\n their centers must satisfy:\n (x_i - x_j)^2 + (y_i - y_j)^2 >= ( (α_i + α_j - 1) * (radii[i] + radii[j]) )^2.\n (This is a linearized version of the product α_i * α_j used in the paper.)\n 3. Binary decisions: α_i ∈ {0, 1} for i = 1,...,n, where α_i = 1 indicates circle i is packed.\n (For circles not packed, we force (x_i, y_i) to equal (cx, cy).)\n 4. Objective: Maximize the total area of the circles packed:\n maximize sum_{i=1}^n α_i * (pi * radii[i]^2).\n Input kwargs:\n - n : int, the number of circles.\n - cx : float, x-coordinate of the container\'s center.\n - cy : float, y-coordinate of the container\'s center.\n - R : float, the radius of the container.\n - radii : list of float, each element is the radius of a circle.\n Returns:\n A dictionary with one key:\n - "coords": a list of n (x, y) tuples corresponding to the centers of the circles.\n For circles not packed (α_i = 0), (x, y) should be (-1, -1).\n """\n # ===== Placeholder Implementation =====\n\n return {"coords": []}' +task_description = '("The problem involves packing a subset of unequal circles into a fixed circular container with "' + + +__all__ = ['PUCAEvaluationCB'] + + +class PUCAEvaluationCB(Evaluation): + + def __init__(self, + timeout_seconds=50, + **kwargs): + + """ + Args: + None + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + # Load datasets from Hugging Face + dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Packing unequal circles area") + self._datasets = {} + for filename in dataset: + # Join all text rows into a single string + text_content = '\n'.join([row['text'] for row in dataset[filename]]) + self._datasets[filename] = text_content + + def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: + return self.evaluate(callable_func) + + def evaluate(self, eva: callable) -> float | None: + ins_cases = [] + for case_id, ins in enumerate(self._datasets.values()): + ins_cases.append(self.load_data(ins)) + + fitness_list = [] + try: + for i in ins_cases: + for j in i: + result = eva(j['n'], j['cx'], j['cy'], j['R'], j['radii']) + fitness = self.eval_func(n=j['n'], cx=j['cx'], cy=j['cy'], R=j['R'], radii=j['radii'], coords=result['coords']) + fitness_list.append(fitness) + + return np.mean(fitness_list) + + except ValueError as e: + print(e) + return None + + def load_data(self, input_string): + """ + Load and parse the input file containing one or multiple cases. + File Format: + - The file is a plain-text file with non-empty lines. + - Each case starts with a header line containing exactly four numbers: + n cx cy R + where: + • n is the number of circles (an integer), + • cx and cy are the container's center coordinates (floats), + • R is the container's radius (float). + - The next n non-empty lines each contain one real number representing + the radius of a circle. + Returns: + A list of cases, where each case is a dictionary with keys: + "n" : int, number of circles. + "cx" : float, container center x-coordinate. + "cy" : float, container center y-coordinate. + "R" : float, container radius. + "radii" : list of float, the radii of the circles. + """ + cases = [] + try: + lines = [line.strip() for line in input_string.split('\n') if line.strip() != ''] + except Exception as e: + raise Exception(f"Error reading input file: {e}") + + i = 0 + total_lines = len(lines) + while i < total_lines: + header_tokens = lines[i].split() + if len(header_tokens) != 4: + raise Exception(f"Header line at line {i + 1} must contain exactly 4 numbers: n cx cy R.") + try: + n = int(header_tokens[0]) + cx = float(header_tokens[1]) + cy = float(header_tokens[2]) + R = float(header_tokens[3]) + except Exception as e: + raise Exception(f"Error parsing header on line {i + 1}: {e}") + + if i + n >= total_lines: + raise Exception(f"Not enough lines for {n} circle radii after line {i + 1}.") + radii = [] + for j in range(1, n + 1): + try: + # Even if there are extra tokens, take the first as the radius. + r = float(lines[i + j].split()[0]) + radii.append(r) + except Exception as e: + raise Exception(f"Error parsing circle radius on line {i + j + 1}: {e}") + case = {"n": n, "cx": cx, "cy": cy, "R": R, "radii": radii} + cases.append(case) + i += n + 1 # Move to the next case header (if any) + return cases + + def eval_func(self, **kwargs): + """ + Evaluate the solution for the Maximise Area problem of Unequal Circle Packing. + Input (merged from the case data and the solution): + - n : int, the total number of circles. + - cx : float, x-coordinate of the container's center. + - cy : float, y-coordinate of the container's center. + - R : float, the container's radius. + - radii : list of float, radii for each circle. + - coords: list of (x, y) tuples, the centers of the circles as produced by solve. + A circle is considered unpacked if its center equals (-1, -1) (within tolerance). + Evaluation Details: + 1. Identify packed circles: a circle is considered packed if its center is not (-1, -1) + (within a small tolerance tol). + 2. For every packed circle: + - Verify container feasibility: + Ensure that sqrt((x - cx)^2 + (y - cy)^2) + r_i <= R (within tolerance). + - Record its container clearance: clearance = R - (distance from (cx, cy) + r_i). + 3. For every pair of packed circles, verify non-overlap: + Ensure that the distance between centers >= r_i + r_j (within tolerance). + And record the pair clearance: (distance - (r_i + r_j)). + 4. If any feasibility constraint is violated (beyond tol), raise an Exception. + 5. Compute the primary score as the total area of packed circles: + total_area = sum(π * (r_i)^2 for each packed circle). + Then, use the minimum clearance (across all container and pair clearances) as a tie-breaker. + (For example, final score = total_area + ε * (minimum clearance), with ε small.) + 6. Return the final score (a higher score indicates a better solution). + Returns: + float: the evaluation score. + """ + import math + + tol = 1e-5 # Numerical tolerance. + + # Extract required inputs. + try: + n = kwargs["n"] + cx = kwargs["cx"] + cy = kwargs["cy"] + container_R = kwargs["R"] + radii = kwargs["radii"] + coords = kwargs["coords"] + except KeyError as e: + raise Exception(f"Missing required parameter: {e}") + + if len(coords) != n: + raise Exception(f"Expected {n} coordinates, but got {len(coords)}.") + + # Identify packed circles. + # Convention: a circle is considered not packed if its center equals (-1, -1) within tolerance. + packed_indices = [] + for i in range(n): + x, y = coords[i] + if not (abs(x + 1) <= tol and abs(y + 1) <= tol): + packed_indices.append(i) + + # Evaluate feasibility for each packed circle (container constraint). + container_clearances = [] + for i in packed_indices: + x, y = coords[i] + r = radii[i] + dist = math.hypot(x - cx, y - cy) + clearance = container_R - (dist + r) + if clearance < -tol: + raise Exception(f"Circle {i} violates container constraint by {-clearance}.") + container_clearances.append(clearance) + + # Evaluate non-overlap feasibility for every pair of packed circles. + pair_clearances = [] + for idx, i in enumerate(packed_indices): + for j in packed_indices[idx + 1:]: + x1, y1 = coords[i] + x2, y2 = coords[j] + center_distance = math.hypot(x1 - x2, y1 - y2) + required_distance = radii[i] + radii[j] + clearance = center_distance - required_distance + if clearance < -tol: + raise Exception(f"Circles {i} and {j} overlap by {-clearance}.") + pair_clearances.append(clearance) + + # Primary measure: total area of packed circles. + total_area = 0.0 + for i in packed_indices: + total_area += math.pi * (radii[i] ** 2) + + # Final score: primary is the total area packed + score = total_area + return score + + def norm_score(self, results): + optimal_scores = { + "circle1.txt": [197.0718], + "circle2.txt": [290.5062], + "circle3.txt": [502.0171], + "circle4.txt": [642.9087], + } + + normed = {} + for case, (scores, error_message) in results.items(): + if case not in optimal_scores: + continue # Skip if there's no optimal score defined. + optimal_list = optimal_scores[case] + normed_scores = [] + # Compute normalized score for each index. + for idx, score in enumerate(scores): + if isinstance(score, (int, float)): + normed_scores.append(score / optimal_list[idx]) + else: + normed_scores.append(score) + normed[case] = (normed_scores, error_message) + return normed + + def get_dev(self): + dev = {'circle1.txt': [], 'circle2.txt': [], 'circle3.txt': [], 'circle4.txt': []} + + return dev + + + + + + + +# Task configuration for benchmark task +ENTRY_NAME = 'solve' +FUNCTION_SIGNATURE = 'def solve(...):' +IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' +TASK_DESCRIPTION = '("The problem involves packing a subset of unequal circles into a fixed circular container with "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The problem involves packing a subset of unequal circles into a fixed circular container with "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n:int, cx: float, cy: float, R: float, radii: list) -> dict:\n """\n Solve the Unequal Circle Packing problem (Maximize Area version).\n Problem Description:\n Given a circular container with center (cx, cy) and radius R, and n circles\n with specified radii (provided in \'radii\'), decide which circles to pack and\n determine the centers (x_i, y_i) for the packed circles such that:\n 1. Containment: Each packed circle i must lie completely within the container.\n (x_i - cx)^2 + (y_i - cy)^2 <= α_i * (R - radii[i])^2, for i = 1,...,n.\n (If α_i = 0, then the circle is not packed and its center is set to (cx, cy).)\n 2. Non-Overlap: For every pair of circles i and j (with i < j), if both are packed,\n their centers must satisfy:\n (x_i - x_j)^2 + (y_i - y_j)^2 >= ( (α_i + α_j - 1) * (radii[i] + radii[j]) )^2.\n (This is a linearized version of the product α_i * α_j used in the paper.)\n 3. Binary decisions: α_i ∈ {0, 1} for i = 1,...,n, where α_i = 1 indicates circle i is packed.\n (For circles not packed, we force (x_i, y_i) to equal (cx, cy).)\n 4. Objective: Maximize the total area of the circles packed:\n maximize sum_{i=1}^n α_i * (pi * radii[i]^2).\n Input kwargs:\n - n : int, the number of circles.\n - cx : float, x-coordinate of the container\'s center.\n - cy : float, y-coordinate of the container\'s center.\n - R : float, the radius of the container.\n - radii : list of float, each element is the radius of a circle.\n Returns:\n A dictionary with one key:\n - "coords": a list of n (x, y) tuples corresponding to the centers of the circles.\n For circles not packed (α_i = 0), (x, y) should be (-1, -1).\n """\n # ===== Placeholder Implementation =====\n\n return {"coords": []}' +EVAL_CLASS_NAME = 'PUCAEvaluationCB' +EVAL_KWARGS = {'timeout_seconds': 60} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_packing_unequal_circles_area/paras.yaml b/examples/benchmark_tasks/optimization_packing_unequal_circles_area/paras.yaml new file mode 100644 index 00000000..06ecec66 --- /dev/null +++ b/examples/benchmark_tasks/optimization_packing_unequal_circles_area/paras.yaml @@ -0,0 +1,2 @@ +name: PUCAEvaluationCB +timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_packing_unequal_rectangles_and_squares/__init__.py b/examples/benchmark_tasks/optimization_packing_unequal_rectangles_and_squares/__init__.py new file mode 100644 index 00000000..0219f6c2 --- /dev/null +++ b/examples/benchmark_tasks/optimization_packing_unequal_rectangles_and_squares/__init__.py @@ -0,0 +1,400 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_packing_unequal_rectangles_and_squares +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# References: +# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language +# model agents in algorithm search for combinatorial optimization. +# arXiv preprint arXiv:2504.04310 (2025). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +import math +from typing import Any +import numpy as np +from llm4ad_loader import Evaluation +from llm4ad_loader import load_subdir_as_text +# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader +# from llm4ad.task.optimization.co_bench.packing_unequal_rectangles_and_squares_co_bench.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n: int, cx: float, cy: float, R: float, items: list, shape: str, rotation: bool) -> dict:\n """\n Solves the "maximum number" packing problem for unequal rectangles and squares\n in a fixed-size circular container.\n Input kwargs:\n - n : int, total number of available items (rectangles or squares)\n - cx, cy : floats, coordinates of the container center (typically the origin)\n - R : float, radius of the circular container\n - items : list of tuples, where each tuple (L, W) specifies the dimensions\n of an item (for a square, L == W). Items are assumed to be ordered\n by increasing size.\n - shape : str, either "rectangle" or "square"\n - rotation : bool, indicating whether 90° rotation is allowed\n Objective:\n The goal is to pack as many items as possible inside the container. An item is\n considered packed if its entire geometry lies completely within the circular\n container and it does not overlap any other packed item.\n Evaluation:\n A valid solution is one in which no packed item extends outside the container\n and no two packed items overlap. The quality of a solution is measured solely by\n the number of items successfully packed (i.e. the higher the number, the better).\n Returns:\n A dictionary with the key \'placements\' containing a list of exactly n tuples.\n Each tuple is of the form (x-coordinate, y-coordinate, theta) where:\n - (x-coordinate, y-coordinate) is the center position of the item,\n - theta is the rotation angle in degrees (counter-clockwise from the horizontal) 90 or 0.\n - For any item that is not packed, set its x and y coordinates to -1\n (and theta can be set to 0).\n Note:\n This is a placeholder header. The actual solution logic is not implemented here.\n """\n ## placeholder.\n return {\'placements\': []}' +task_description = '("We are given a set of n unequal rectangles (or squares), each with specified dimensions, "' + + +__all__ = ['PURSEvaluationCB'] + + +class PURSEvaluationCB(Evaluation): + + def __init__(self, + timeout_seconds=50, + **kwargs): + + """ + Args: + None + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + # Load datasets from Hugging Face + dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Packing unequal rectangles and squares") + self._datasets = {} + for filename in dataset: + # Join all text rows into a single string + text_content = '\n'.join([row['text'] for row in dataset[filename]]) + self._datasets[filename] = text_content + + def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: + return self.evaluate(callable_func) + + def evaluate(self, eva: callable) -> float | None: + ins_cases = [] + for case_id, ins in enumerate(self._datasets.values()): + ins_cases.append(self.load_data(ins)) + + fitness_list = [] + try: + for i in ins_cases: + for j in i: + result = eva(j['n'], j['cx'], j['cy'], j['R'], j['items'], j['shape'], j['rotation']) + fitness = self.eval_func(n=j['n'], cx=j['cx'], cy=j['cy'], R=j['R'], items=j['items'], shape=j['shape'], rotation=j['rotation'], placements=result['placements']) + fitness_list.append(fitness) + + return np.mean(fitness_list) + + except ValueError as e: + print(e) + return None + + def load_data(self, input_string): + """ + Reads input string content that may contain multiple cases for the packing problem. + Each case is formatted as follows: + - A header line with four values: n, cx, cy, R + n : number of items (rectangles or squares) + cx, cy : container center coordinates + R : container radius + - Next n non-empty lines: each line represents an item: + * For a square: one number (side length) — interpreted as (side, side) + * For a rectangle: two numbers (length and width) + Returns: + A list of cases. Each case is a dictionary with the following keys: + - 'n' : int, number of items + - 'cx' : float, x-coordinate of container center + - 'cy' : float, y-coordinate of container center + - 'R' : float, container radius + - 'items': list of tuples, where each tuple is (L, W) for the respective item. + """ + cases = [] + lines = [line.strip() for line in input_string.split('\n') if line.strip() != ''] + + i = 0 + while i < len(lines): + # Parse header line for one case + header_tokens = lines[i].split() + if len(header_tokens) < 4: + raise ValueError("Header line must contain at least 4 values: n, cx, cy, R.") + n = int(header_tokens[0]) + cx = float(header_tokens[1]) + cy = float(header_tokens[2]) + R = float(header_tokens[3]) + i += 1 + + # Ensure there are enough lines for all items + if i + n > len(lines): + raise ValueError("Insufficient item lines for a case.") + + items = [] + shape = None + for j in range(n): + tokens = lines[i].split() + if len(tokens) == 1: + side = float(tokens[0]) + items.append((side, side)) + shape = 'square' + elif len(tokens) >= 2: + length = float(tokens[0]) + width = float(tokens[1]) + items.append((length, width)) + shape = 'rectangle' + else: + raise ValueError(f"Item data format error at line {i + 1}.") + i += 1 + + # Append the parsed case as a dictionary + if shape == 'rectangle': + cases.append({ + 'n': n, + 'cx': cx, + 'cy': cy, + 'R': R, + 'items': items, + 'shape': shape, + 'rotation': False + }) + cases.append({ + 'n': n, + 'cx': cx, + 'cy': cy, + 'R': R, + 'items': items, + 'shape': shape, + 'rotation': True + }) + else: + cases.append({ + 'n': n, + 'cx': cx, + 'cy': cy, + 'R': R, + 'items': items, + 'shape': shape, + 'rotation': False + + }) + + return cases + + def eval_func(self, **kwargs): + """ + Evaluates a solution for the "maximise number of items packed" rectangle (or square) + packing problem in a circular container. + Parameters: + input_data: dict with keys: + - n : int, total number of available items. + - cx, cy : floats, coordinates of the container center. + - R : float, container radius. + - items : list of tuples, where each tuple (L, W) gives the dimensions of an item. + (For squares, L == W.) + - shape : str, either "rectangle" or "square". + - rotation : bool, whether 90° rotation is allowed. + solution_output: dict with key 'placements' containing a list of exactly n tuples. + Each tuple is (x, y, theta), where: + - (x, y) are the center coordinates. + - theta is the rotation angle in degrees (counter-clockwise from horizontal). + - For any item that is not packed, x and y should be set to -1 (theta can be 0). + Returns: + score: int, the number of valid (packed) items. + Raises: + ValueError: if any constraint is violated. + """ + # Unpack input parameters. + tol = 1e-5 + n = kwargs.get("n") + cx = kwargs.get("cx") + cy = kwargs.get("cy") + R = kwargs.get("R") + items = kwargs.get("items") # list of (L, W) + shape = kwargs.get("shape").lower() # "rectangle" or "square" + rotation_allowed = kwargs.get("rotation") + + placements = kwargs.get("placements") + + # Check that exactly n placements are provided. + if not isinstance(placements, list) or len(placements) != n: + raise ValueError("The output must contain exactly n placements.") + + # List to hold the geometry of each packed item for later overlap checking. + # For each packed item, we will store a tuple: (xmin, xmax, ymin, ymax) + packed_rectangles = [] + + score = 0 # Count of packed items. + + for idx, placement in enumerate(placements): + if (not isinstance(placement, (list, tuple))) or len(placement) != 3: + raise ValueError(f"Placement {idx} must be a tuple of (x, y, theta).") + x, y, theta = placement + + # Check unpacked indicator: if x == -1 and y == -1 then item is not packed. + if x == -1 and y == -1: + # Unpacked item; theta is ignored. Continue. + continue + + # Otherwise, the item is packed. + score += 1 + + # --- Check rotation value. + # If rotation is not allowed then theta must be 0. + # If rotation is allowed, we require theta to be either 0 or 90 (within a small tolerance). + if rotation_allowed: + if not (math.isclose(theta, 0, abs_tol=1e-3) or math.isclose(theta, 90, abs_tol=1e-3)): + raise ValueError(f"Item {idx}: rotation angle must be 0 or 90 degrees when rotation is allowed.") + else: + if not math.isclose(theta, 0, abs_tol=1e-3): + raise ValueError(f"Item {idx}: rotation angle must be 0 when rotation is not allowed.") + + # --- Determine the effective dimensions of the item. + L, W = items[idx] + # For squares, ensure consistency. + if shape == "square" and not math.isclose(L, W, abs_tol=1e-3): + raise ValueError(f"Item {idx}: For square packing, dimensions must be equal.") + + # If rotated by 90, swap dimensions. + if rotation_allowed and math.isclose(theta, 90, abs_tol=1e-3): + eff_L, eff_W = W, L + else: + eff_L, eff_W = L, W + + half_L = eff_L / 2.0 + half_W = eff_W / 2.0 + + # --- Compute the coordinates of the four corners. + # Since theta is either 0 or 90, the rectangle remains axis aligned. + # For theta==0: corners are (x ± half_L, y ± half_W). + # For theta==90: same structure because dimensions have been swapped. + corners = [ + (x - half_L, y - half_W), + (x - half_L, y + half_W), + (x + half_L, y - half_W), + (x + half_L, y + half_W) + ] + + # --- Check that every corner is inside the container. + for corner in corners: + cx_corner, cy_corner = corner + # Distance from the container center (cx, cy) + dist = math.hypot(cx_corner - cx, cy_corner - cy) + if dist > R + tol: # use a small tolerance + raise ValueError(f"Item {idx}: Corner {corner} lies outside the container.") + + # --- Store the axis-aligned bounding box for overlap checking. + # (Since the rectangles are axis aligned, the bounding box is the rectangle itself.) + xmin = x - half_L + xmax = x + half_L + ymin = y - half_W + ymax = y + half_W + current_rect = (xmin, xmax, ymin, ymax) + + # --- Check for overlap with previously packed items. + for jdx, other_rect in enumerate(packed_rectangles): + oxmin, oxmax, oymin, oymax = other_rect + # Two axis-aligned rectangles do not overlap if one is to the left + # or one is above the other. + if not (xmax <= oxmin + tol or xmin >= oxmax - tol or + ymax <= oymin + tol or ymin >= oymax - tol): + raise ValueError(f"Item {idx} overlaps with an already packed item (index {jdx}).") + + # Save the current rectangle for future overlap checking. + packed_rectangles.append(current_rect) + + return score + + def norm_score(self, results): + optimal_scores = { + "rect1.txt": [7, 7], + "rect2.txt": [11, 12], + "rect3.txt": [19, 20], + "square1.txt": [6], + "square2.txt": [14], + "square3.txt": [23], + } + + normed = {} + for case, (scores, error_message) in results.items(): + if case not in optimal_scores: + continue # Skip if there's no optimal score defined. + optimal_list = optimal_scores[case] + normed_scores = [] + # Compute normalized score for each index. + for idx, score in enumerate(scores): + if isinstance(score, (int, float)): + normed_scores.append(score / optimal_list[idx]) + else: + normed_scores.append(score) + normed[case] = (normed_scores, error_message) + return normed + + + + + + + +# Task configuration for benchmark task +ENTRY_NAME = 'solve' +FUNCTION_SIGNATURE = 'def solve(...):' +IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' +TASK_DESCRIPTION = '("We are given a set of n unequal rectangles (or squares), each with specified dimensions, "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("We are given a set of n unequal rectangles (or squares), each with specified dimensions, "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n: int, cx: float, cy: float, R: float, items: list, shape: str, rotation: bool) -> dict:\n """\n Solves the "maximum number" packing problem for unequal rectangles and squares\n in a fixed-size circular container.\n Input kwargs:\n - n : int, total number of available items (rectangles or squares)\n - cx, cy : floats, coordinates of the container center (typically the origin)\n - R : float, radius of the circular container\n - items : list of tuples, where each tuple (L, W) specifies the dimensions\n of an item (for a square, L == W). Items are assumed to be ordered\n by increasing size.\n - shape : str, either "rectangle" or "square"\n - rotation : bool, indicating whether 90° rotation is allowed\n Objective:\n The goal is to pack as many items as possible inside the container. An item is\n considered packed if its entire geometry lies completely within the circular\n container and it does not overlap any other packed item.\n Evaluation:\n A valid solution is one in which no packed item extends outside the container\n and no two packed items overlap. The quality of a solution is measured solely by\n the number of items successfully packed (i.e. the higher the number, the better).\n Returns:\n A dictionary with the key \'placements\' containing a list of exactly n tuples.\n Each tuple is of the form (x-coordinate, y-coordinate, theta) where:\n - (x-coordinate, y-coordinate) is the center position of the item,\n - theta is the rotation angle in degrees (counter-clockwise from the horizontal) 90 or 0.\n - For any item that is not packed, set its x and y coordinates to -1\n (and theta can be set to 0).\n Note:\n This is a placeholder header. The actual solution logic is not implemented here.\n """\n ## placeholder.\n return {\'placements\': []}' +EVAL_CLASS_NAME = 'PURSEvaluationCB' +EVAL_KWARGS = {'timeout_seconds': 60} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_packing_unequal_rectangles_and_squares/paras.yaml b/examples/benchmark_tasks/optimization_packing_unequal_rectangles_and_squares/paras.yaml new file mode 100644 index 00000000..32ec3be0 --- /dev/null +++ b/examples/benchmark_tasks/optimization_packing_unequal_rectangles_and_squares/paras.yaml @@ -0,0 +1,2 @@ +name: PURSEvaluationCB +timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_packing_unequal_rectangles_and_squares_area/__init__.py b/examples/benchmark_tasks/optimization_packing_unequal_rectangles_and_squares_area/__init__.py new file mode 100644 index 00000000..00b9f02a --- /dev/null +++ b/examples/benchmark_tasks/optimization_packing_unequal_rectangles_and_squares_area/__init__.py @@ -0,0 +1,442 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_packing_unequal_rectangles_and_squares_area +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# References: +# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language +# model agents in algorithm search for combinatorial optimization. +# arXiv preprint arXiv:2504.04310 (2025). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +import math +from typing import Any +import numpy as np +from llm4ad_loader import Evaluation +from llm4ad_loader import load_subdir_as_text +# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader +# from llm4ad.task.optimization.co_bench.packing_unequal_rectangles_and_squares_area_co_bench.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n: int, cx: float, cy: float, R: float, items: list, shape: str, rotation: bool) -> dict:\n """\n Solves the problem of packing a subset of unequal rectangles and squares into a fixed‐size circular container\n with the objective of maximizing the total area of the items placed inside the container.\n Input kwargs:\n - n : int, the number of items (rectangles or squares)\n - cx, cy : floats, the coordinates of the container center\n - R : float, the radius of the container\n - items : list of tuples, where each tuple (L, W) gives the dimensions of an item\n (for a square, L == W)\n - shape : string, either "rectangle" or "square"\n - rotation : bool, whether 90° rotation is allowed (True or False)\n Objective:\n - Select and place a subset of the given items so that each packed item lies completely inside the circular container,\n no two packed items overlap, and the sum of the areas of the packed items is maximized.\n - An item that is not packed contributes zero area.\n Returns:\n A dictionary with the key \'placements\' containing a list of exactly n tuples.\n Each tuple is (x-coordinate, y-coordinate, theta) where:\n - (x-coordinate, y-coordinate) is the center position of the item (if packed),\n - theta is the rotation angle in degrees (counter-clockwise from the horizontal). 90 or 0.\n - For an unpacked item, x and y should be set to -1 and theta to 0 (or another default value).\n Note: This is a placeholder. The actual solution logic is not implemented here.\n """\n ## placeholder.\n return {\'placements\': []}' +task_description = '("We consider the problem of selecting and placing a subset of n unequal rectangles (or squares) "' + + +__all__ = ['PURSAEvaluationCB'] + + +class PURSAEvaluationCB(Evaluation): + + def __init__(self, + timeout_seconds=50, + **kwargs): + + """ + Args: + None + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + # Load datasets from Hugging Face + dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Packing unequal rectangles and squares area") + self._datasets = {} + for filename in dataset: + # Join all text rows into a single string + text_content = '\n'.join([row['text'] for row in dataset[filename]]) + self._datasets[filename] = text_content + + def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: + return self.evaluate(callable_func) + + def evaluate(self, eva: callable) -> float | None: + ins_cases = [] + for case_id, ins in enumerate(self._datasets.values()): + ins_cases.append(self.load_data(ins)) + + fitness_list = [] + try: + for i in ins_cases: + for j in i: + result = eva(j['n'], j['cx'], j['cy'], j['R'], j['items'], j['shape'], j['rotation']) + fitness = self.eval_func(n=j['n'], cx=j['cx'], cy=j['cy'], R=j['R'], items=j['items'], shape=j['shape'], rotation=j['rotation'], placements=result['placements']) + fitness_list.append(fitness) + + return np.mean(fitness_list) + + except ValueError as e: + print(e) + return None + + def load_data(self, input_string): + """ + Reads input string content that may contain multiple cases for the packing problem. + Each case is formatted as follows: + - A header line with four values: n, cx, cy, R + n : number of items (rectangles or squares) + cx, cy : container center coordinates + R : container radius + - Next n non-empty lines: each line represents an item: + * For a square: one number (side length) — interpreted as (side, side) + * For a rectangle: two numbers (length and width) + Returns: + A list of cases. Each case is a dictionary with the following keys: + - 'n' : int, number of items + - 'cx' : float, x-coordinate of container center + - 'cy' : float, y-coordinate of container center + - 'R' : float, container radius + - 'items': list of tuples, where each tuple is (L, W) for the respective item. + """ + cases = [] + lines = [line.strip() for line in input_string.split('\n') if line.strip() != ''] + + i = 0 + while i < len(lines): + # Parse header line for one case + header_tokens = lines[i].split() + if len(header_tokens) < 4: + raise ValueError("Header line must contain at least 4 values: n, cx, cy, R.") + n = int(header_tokens[0]) + cx = float(header_tokens[1]) + cy = float(header_tokens[2]) + R = float(header_tokens[3]) + i += 1 + + # Ensure there are enough lines for all items + if i + n > len(lines): + raise ValueError("Insufficient item lines for a case.") + + items = [] + shape = None + for j in range(n): + tokens = lines[i].split() + if len(tokens) == 1: + side = float(tokens[0]) + items.append((side, side)) + shape = 'square' + elif len(tokens) >= 2: + length = float(tokens[0]) + width = float(tokens[1]) + items.append((length, width)) + shape = 'rectangle' + else: + raise ValueError(f"Item data format error at line {i + 1}.") + i += 1 + + # Append the parsed case as a dictionary + if shape == 'rectangle': + cases.append({ + 'n': n, + 'cx': cx, + 'cy': cy, + 'R': R, + 'items': items, + 'shape': shape, + 'rotation': False + }) + cases.append({ + 'n': n, + 'cx': cx, + 'cy': cy, + 'R': R, + 'items': items, + 'shape': shape, + 'rotation': True + }) + else: + cases.append({ + 'n': n, + 'cx': cx, + 'cy': cy, + 'R': R, + 'items': items, + 'shape': shape, + 'rotation': False + + }) + + return cases + + def eval_func(self, **kwargs): + """ + Evaluates a candidate solution for the "maximize total area" rectangle/square packing problem. + The function expects: + data: a dict with keys: + - n : int, number of items (rectangles or squares) + - cx, cy : floats, coordinates of the container center + - R : float, radius of the container + - items : list of tuples, each (L, W) giving dimensions of an item + - shape : string, either "rectangle" or "square" + - rotation : bool, whether 90° rotation is allowed + sol: a dict with key 'placements' containing a list of exactly n tuples. + Each tuple is (x, y, theta) where: + - (x, y) is the center position for the item (if packed), + - theta is the rotation angle in degrees (counter-clockwise from the horizontal). + - For an unpacked item, x and y must be exactly -1 and theta is ignored (or should be 0). + The evaluation process checks all feasibility constraints: + 1. The number of placements equals n. + 2. Each placement tuple must have three numerical values. + 3. For each item: + - If it is "unpacked" (x == -1 and y == -1), it contributes no area. + - If it is "packed" (x,y != -1), then: + a. If rotation is not allowed, theta must be 0 (within a tiny tolerance). + b. If rotation is allowed, theta must be either 0 or 90 (within tolerance). + c. The entire item (with given dimensions and rotation) must lie completely inside + the circular container (centered at (cx, cy) with radius R). + 4. No two packed items may overlap (their interiors should be disjoint). + If any constraint is violated, the function raises a ValueError with an appropriate message. + If all constraints are met, the function returns the total area of the packed items. + (This is the score that we wish to maximize.) + Note: The evaluation is designed to be robust against malicious modifications + by the solve function. Only valid solutions (with zero penalties) receive a score. + """ + + # Tolerances for numerical comparisons + tol = 1e-5 + angle_tol = 1e-3 # tolerance for angle comparisons in degrees + + # Unpack input data + try: + n = kwargs['n'] + cx, cy = float(kwargs['cx']), float(kwargs['cy']) + R = float(kwargs['R']) + items = kwargs['items'] + shape = kwargs['shape'].lower() + rotation_allowed = bool(kwargs['rotation']) + except KeyError as e: + raise ValueError(f"Missing input data key: {e}") + + if len(items) != n: + raise ValueError("Length of items list must equal n.") + + # Unpack solution + placements = kwargs.get('placements', None) + if placements is None: + raise ValueError("Solution does not contain key 'placements'.") + if not isinstance(placements, list) or len(placements) != n: + raise ValueError("The 'placements' list must contain exactly n tuples.") + + # Helper: Given a placement (x, y, theta in degrees) and item dimensions (L, W), + # compute the four vertices of the rectangle after rotation. + def compute_vertices(x, y, L, W, theta_deg): + theta = math.radians(theta_deg) + # Local coordinates of corners before rotation: + local_corners = [(L / 2, W / 2), + (L / 2, -W / 2), + (-L / 2, W / 2), + (-L / 2, -W / 2)] + vertices = [] + cos_t = math.cos(theta) + sin_t = math.sin(theta) + for dx, dy in local_corners: + # Apply rotation: + dx_r = dx * cos_t - dy * sin_t + dy_r = dx * sin_t + dy * cos_t + vertices.append((x + dx_r, y + dy_r)) + return vertices + + # Helper: For an item with placement (x,y,theta) and dimensions (L,W), + # compute its axis-aligned bounding box. + # Since allowed rotations are only 0 or 90 degrees (if rotation is allowed), + # the rectangle remains axis-aligned. + def compute_aabb(x, y, L, W, theta_deg): + # Enforce only 0 or 90: if theta is nearly 90, swap dimensions. + if abs(theta_deg) < angle_tol: + half_L, half_W = L / 2, W / 2 + elif abs(theta_deg - 90) < angle_tol: + half_L, half_W = W / 2, L / 2 + else: + # Should not happen; safeguard. + raise ValueError("Invalid rotation angle. Allowed angles are 0 or 90 degrees.") + return (x - half_L, x + half_L, y - half_W, y + half_W) + + total_area = 0.0 + placed_items = [] # List of dicts: { 'aabb': (xmin,xmax,ymin,ymax), 'vertices': [...] } + + # Process each item + for i in range(n): + # Check placement tuple structure + try: + placement = placements[i] + if not (isinstance(placement, (list, tuple)) and len(placement) == 3): + raise ValueError(f"Placement for item {i} must be a tuple/list of three numbers.") + x, y, theta = float(placement[0]), float(placement[1]), float(placement[2]) + except Exception as e: + raise ValueError(f"Invalid placement for item {i}: {e}") + + L, W = items[i] + # For squares, check that L == W (within tolerance) + if shape == "square" and abs(L - W) > tol: + raise ValueError(f"Item {i} is marked as square but dimensions differ: L={L}, W={W}") + + # Determine if the item is packed. + # Convention: If x == -1 and y == -1, item is not placed. + if abs(x + 1) < tol and abs(y + 1) < tol: + # Unpacked item: skip (area = 0). Optionally, enforce theta = 0. + if abs(theta) > angle_tol: + raise ValueError(f"Unpacked item {i} must have theta equal to 0.") + continue + + # Packed item: check rotation feasibility. + if not rotation_allowed: + if abs(theta) > angle_tol: + raise ValueError(f"Rotation is not allowed, but item {i} has theta = {theta}.") + else: + # If rotation is allowed, then theta must be 0 or 90. + if not (abs(theta) < angle_tol or abs(theta - 90) < angle_tol): + raise ValueError( + f"Item {i} has invalid rotation angle {theta}. Allowed values are 0 or 90 degrees.") + + # Compute the vertices for the placed rectangle. + vertices = compute_vertices(x, y, L, W, theta) + # Check each vertex lies inside the container circle. + for vx, vy in vertices: + # Euclidean distance from container center (cx, cy) + if (vx - cx) ** 2 + (vy - cy) ** 2 > R ** 2 + tol: + raise ValueError(f"Item {i} has a vertex at ({vx:.4f},{vy:.4f}) outside the container.") + + # Compute axis-aligned bounding box (since rectangle is axis-aligned if theta in {0,90}) + xmin, xmax, ymin, ymax = compute_aabb(x, y, L, W, theta) + + # Save the item details for later overlap checking. + placed_items.append({ + 'index': i, + 'aabb': (xmin, xmax, ymin, ymax), + 'vertices': vertices, + 'area': L * W + }) + total_area += L * W + + # Check for pairwise overlap among all placed items. + num_placed = len(placed_items) + for i in range(num_placed): + aabb_i = placed_items[i]['aabb'] + xmin_i, xmax_i, ymin_i, ymax_i = aabb_i + for j in range(i + 1, num_placed): + aabb_j = placed_items[j]['aabb'] + xmin_j, xmax_j, ymin_j, ymax_j = aabb_j + # Compute overlap in x and y + overlap_x = max(0.0, min(xmax_i, xmax_j) - max(xmin_i, xmin_j)) + overlap_y = max(0.0, min(ymax_i, ymax_j) - max(ymin_i, ymin_j)) + if overlap_x * overlap_y > tol: + raise ValueError(f"Items {placed_items[i]['index']} and {placed_items[j]['index']} overlap.") + + return total_area + + def norm_score(self, results): + optimal_scores = { + "rect1.txt": [37.6878, 37.9687], + "rect2.txt": [84.4446, 84.7008], + "rect3.txt": [103.4802, 110.3253], + "square1.txt": [51.7583], + "square2.txt": [109.8363], + "square3.txt": [103.0963], + } + + normed = {} + for case, (scores, error_message) in results.items(): + if case not in optimal_scores: + continue # Skip if there's no optimal score defined. + optimal_list = optimal_scores[case] + normed_scores = [] + # Compute normalized score for each index. + for idx, score in enumerate(scores): + if isinstance(score, (int, float)): + normed_scores.append(score / optimal_list[idx]) + else: + normed_scores.append(score) + normed[case] = (normed_scores, error_message) + return normed + + + + + + + +# Task configuration for benchmark task +ENTRY_NAME = 'solve' +FUNCTION_SIGNATURE = 'def solve(...):' +IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' +TASK_DESCRIPTION = '("We consider the problem of selecting and placing a subset of n unequal rectangles (or squares) "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("We consider the problem of selecting and placing a subset of n unequal rectangles (or squares) "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n: int, cx: float, cy: float, R: float, items: list, shape: str, rotation: bool) -> dict:\n """\n Solves the problem of packing a subset of unequal rectangles and squares into a fixed‐size circular container\n with the objective of maximizing the total area of the items placed inside the container.\n Input kwargs:\n - n : int, the number of items (rectangles or squares)\n - cx, cy : floats, the coordinates of the container center\n - R : float, the radius of the container\n - items : list of tuples, where each tuple (L, W) gives the dimensions of an item\n (for a square, L == W)\n - shape : string, either "rectangle" or "square"\n - rotation : bool, whether 90° rotation is allowed (True or False)\n Objective:\n - Select and place a subset of the given items so that each packed item lies completely inside the circular container,\n no two packed items overlap, and the sum of the areas of the packed items is maximized.\n - An item that is not packed contributes zero area.\n Returns:\n A dictionary with the key \'placements\' containing a list of exactly n tuples.\n Each tuple is (x-coordinate, y-coordinate, theta) where:\n - (x-coordinate, y-coordinate) is the center position of the item (if packed),\n - theta is the rotation angle in degrees (counter-clockwise from the horizontal). 90 or 0.\n - For an unpacked item, x and y should be set to -1 and theta to 0 (or another default value).\n Note: This is a placeholder. The actual solution logic is not implemented here.\n """\n ## placeholder.\n return {\'placements\': []}' +EVAL_CLASS_NAME = 'PURSAEvaluationCB' +EVAL_KWARGS = {'timeout_seconds': 60} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_packing_unequal_rectangles_and_squares_area/paras.yaml b/examples/benchmark_tasks/optimization_packing_unequal_rectangles_and_squares_area/paras.yaml new file mode 100644 index 00000000..17db25b2 --- /dev/null +++ b/examples/benchmark_tasks/optimization_packing_unequal_rectangles_and_squares_area/paras.yaml @@ -0,0 +1,2 @@ +name: PURSAEvaluationCB +timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_pymoo_moead/__init__.py b/examples/benchmark_tasks/optimization_pymoo_moead/__init__.py new file mode 100644 index 00000000..1f2b2346 --- /dev/null +++ b/examples/benchmark_tasks/optimization_pymoo_moead/__init__.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_pymoo_moead +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# Module Name: MOEAD_PYMOO_Evaluation +# Last Revision: 2025/07/14 +# Description: Evaluates the Multi-objective problem using the MOEAD algorithm. +# Problem instances are generated by the GetData class. +# +# Parameters: +# - timeout_seconds: Maximum allowed time (in seconds) for the evaluation process: int (default: 20). +# - n_var: The number of decision variables for the optimization problem: int (default: 10). +# - n_obj: The number of objectives for the optimization problem: int (default: 3). +# - n_partitions: The number of partitions used to generate reference directions: int (default: 12). +# - pop_size: The size of the population in the evolutionary algorithm: int (default: 100). +# - n_gen: The number of generations for the algorithm to run: int (default: 100). +# +# References: +# - Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +import os, sys +sys.path.insert(0, os.path.dirname(__file__)) +import copy +from typing import Callable, Any +import numpy as np + +from pymoo.algorithms.moo.moead import MOEAD +from pymoo.indicators.hv import HV +from pymoo.optimize import minimize +from pymoo.termination import get_termination +from pymoo.util.ref_dirs import get_reference_directions +from pymoo.decomposition.tchebicheff import Tchebicheff + +from llm4ad_loader import Evaluation +# Assuming the new GetData class is located at the following path +from get_instance import GetData +# from llm4ad.task.optimization.pymoo_moead.get_instance import GetData # Converted from LLM4AD import +# from llm4ad.task.optimization.pymoo_moead.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\n\ndef custom_decomposition(F: np.ndarray,\n weights: np.ndarray,\n ideal_point: np.ndarray,\n **kwargs) -> np.ndarray:\n """Design a novel decomposition method for MOEA/D.\n\n Args:\n F (np.ndarray): A set of objective vectors for the population.\n Shape: (n_solutions, n_objectives)\n weights (np.ndarray): The weight vectors for the subproblems.\n Shape: (n_solutions, n_objectives)\n ideal_point (np.ndarray): The ideal point found so far.\n Shape: (n_objectives,)\n\n Returns:\n np.ndarray: The aggregated scalar value for each solution.\n Shape: (n_solutions,)\n """\n # Default implementation: Tchebycheff decomposition.\n # Replace this with your novel algorithm.\n v = np.abs(F - ideal_point) * weights\n return np.max(v, axis=1)' +task_description = '"' + + +class MOEAD_PYMOO_Evaluation(Evaluation): + def __init__(self, + timeout_seconds=100, + n_var=10, + n_obj=3, + n_partitions=12, + pop_size=100, + n_gen=100, + seed=None, + **kwargs): + """ + Parameter Description: + This evaluator now receives a decomposition function via the evaluate_program interface. + """ + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + # Following the CVRP pattern, use the GetData class to generate problem instances + getData = GetData(n_var=n_var, n_obj=n_obj) + self.problem = getData.get_problem_instance() + + self.ref_dirs = get_reference_directions("das-dennis", self.problem.n_obj, n_partitions=n_partitions) + self.pop_size = pop_size if pop_size else len(self.ref_dirs) + self.n_gen = n_gen + self.seed = seed + self.hv_ref = np.array([1.1] * self.problem.n_obj) + self.hv_calculator = HV(ref_point=self.hv_ref) + self.last_result = None + + + def evaluate(self, decomposition_func: Callable = None) -> float: + """ + Core evaluation method. Returns the evaluation score and stores detailed results in self.last_result. + """ + class DecompAdapter: + def __init__(self, func): + self.func = func + def do(self, F, weights, ideal_point, **kwargs): + return self.func(F, weights=weights, ideal_point=ideal_point, **kwargs) + + decomposition = DecompAdapter(decomposition_func) if decomposition_func else Tchebicheff() + + algorithm = MOEAD( + ref_dirs=self.ref_dirs, + n_neighbors=15, + prob_neighbor_mating=0.7, + decomposition=decomposition, + seed=self.seed + ) + + termination = get_termination("n_gen", self.n_gen) + res = minimize(self.problem, algorithm, termination, seed=self.seed, verbose=False) + + hv_value = self.hv_calculator(res.opt.get("F")) + self.last_result = {"hv": hv_value, "pareto_front": res.opt} + return -hv_value + + + def evaluate_program(self, program_str: str, callable_func: callable) -> Any: + return self.evaluate(decomposition_func=callable_func) + + def plot_solutions(self, solutions): + import matplotlib.pyplot as plt + F = solutions.get("F") + fig = plt.figure(figsize=(10, 8)) + ax = fig.add_subplot(111, projection='3d') + ax.scatter(F[:, 0], F[:, 1], F[:, 2], c='blue', s=30, alpha=0.5) + ax.set_xlabel('Objective 1'); ax.set_ylabel('Objective 2'); ax.set_zlabel('Objective 3') + ax.set_title(f'MOEAD on {self.problem.__class__.__name__} (HV = {self.hv_calculator(F):.4f})') + plt.tight_layout(); plt.show() + +if __name__ == "__main__": + def custom_decomposition_tchebycheff(F: np.ndarray, weights: np.ndarray, ideal_point: np.ndarray, **kwargs) -> np.ndarray: + v = np.abs(F - ideal_point) * weights + return np.max(v, axis=1) + + evaluator = MOEAD_PYMOO_Evaluation(n_gen=100, seed=1) + score = evaluator.evaluate_program("", custom_decomposition_tchebycheff) + results = evaluator.last_result + + print(f"Evaluation Score (Negative HV): {score:.5f}") + print(f"Hypervolume (HV): {results['hv']:.4f}") + + if evaluator.problem.n_obj == 3 and results: + evaluator.plot_solutions(results["pareto_front"]) + +# Task configuration for benchmark task +ENTRY_NAME = 'custom_decomposition' +FUNCTION_SIGNATURE = 'def custom_decomposition(...):' +IMPORT_HEADER = 'import numpy as np\nimport math' +TASK_DESCRIPTION = '"' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `custom_decomposition` for the LLM4AD task.\\n\\nTask description:\\n"\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\n\ndef custom_decomposition(F: np.ndarray,\n weights: np.ndarray,\n ideal_point: np.ndarray,\n **kwargs) -> np.ndarray:\n """Design a novel decomposition method for MOEA/D.\n\n Args:\n F (np.ndarray): A set of objective vectors for the population.\n Shape: (n_solutions, n_objectives)\n weights (np.ndarray): The weight vectors for the subproblems.\n Shape: (n_solutions, n_objectives)\n ideal_point (np.ndarray): The ideal point found so far.\n Shape: (n_objectives,)\n\n Returns:\n np.ndarray: The aggregated scalar value for each solution.\n Shape: (n_solutions,)\n """\n # Default implementation: Tchebycheff decomposition.\n # Replace this with your novel algorithm.\n v = np.abs(F - ideal_point) * weights\n return np.max(v, axis=1)' +EVAL_CLASS_NAME = 'MOEAD_PYMOO_Evaluation' +EVAL_KWARGS = {'timeout_seconds': 100} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_pymoo_moead/get_instance.py b/examples/benchmark_tasks/optimization_pymoo_moead/get_instance.py new file mode 100644 index 00000000..a7b85e0e --- /dev/null +++ b/examples/benchmark_tasks/optimization_pymoo_moead/get_instance.py @@ -0,0 +1,87 @@ +# Module Name: get_instance +# Last Revision: 2025/07/14 +# Description: Generates DTLZ4 problem instances for MOEAD evaluation. +# This module is part of the LLM4AD project (https://github.com/Optima-CityU/llm4ad). +import pickle + +import numpy as np + +from pymoo.problems import get_problem + +# class GetData: +# def __init__(self, n_var, n_obj): +# """ +# Initialize parameters for the WFG problem. +# Args: +# n_var (int): The number of decision variables. +# n_obj (int): The number of objectives. +# """ +# # For WFG problems, ensure the number of decision variables is sufficient +# k = 2 * (n_obj - 1) +# if n_var < k + 1: +# raise ValueError(f"For WFG1 with {n_obj} objectives, n_var must be at least {k + 1}") +# +# self.n_var = n_var +# self.n_obj = n_obj +# +# def get_problem_instance(self): +# """ +# Generate and return a WFG1 problem instance using the pymoo library. +# This is a more complex benchmark problem than DTLZ. +# """ +# # WFG problems typically require a position parameter k; a standard configuration is used here. +# k = 2 * (self.n_obj - 1) +# return get_problem("wfg1", n_var=self.n_var, n_obj=self.n_obj, k=k) + +class GetData: + def __init__(self, n_var, n_obj): + """ + Initialize parameters for the DTLZ problem. + Args: + n_var (int): The number of decision variables. + n_obj (int): The number of objectives. + """ + self.n_var = n_var + self.n_obj = n_obj + + def get_problem_instance(self): + """ + Generate and return a DTLZ4 problem instance using the pymoo library. + """ + return get_problem("DTLZ4", n_var=self.n_var, n_obj=self.n_obj) + + +if __name__ == '__main__': + # Demonstrate the use of the GetData class + print("--- Demonstrating GetData Class ---") + gd = GetData(n_var=10, n_obj=3) + dtlz4_problem = gd.get_problem_instance() + print("Successfully created a DTLZ4 problem instance:") + print(dtlz4_problem) + print("\n") + + # Provide a code template for a Large Language Model (LLM) to implement a custom decomposition function + prompt_code_temp = '''import numpy as np + +def custom_decomposition(F: np.ndarray, + weights: np.ndarray, + ideal_point: np.ndarray, + **kwargs) -> np.ndarray: + """Design a novel decomposition method for MOEA/D. + + Args: + F (np.ndarray): A set of objective vectors for the population. + weights (np.ndarray): The weight vectors for the subproblems. + ideal_point (np.ndarray): The ideal point found so far. + + Returns: + np.ndarray: The aggregated scalar value for each solution. + """ + # Example: Tchebycheff decomposition + # This is a placeholder and should be replaced by a novel algorithm. + v = np.abs(F - ideal_point) * weights + return np.max(v, axis=1) +''' + + print("--- Template for LLM-designed Decomposition Function ---") + print(prompt_code_temp) diff --git a/examples/benchmark_tasks/optimization_pymoo_moead/paras.yaml b/examples/benchmark_tasks/optimization_pymoo_moead/paras.yaml new file mode 100644 index 00000000..ea215272 --- /dev/null +++ b/examples/benchmark_tasks/optimization_pymoo_moead/paras.yaml @@ -0,0 +1,2 @@ +name: MOEAD_PYMOO_Evaluation +timeout_seconds: 100 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_qap_construct/__init__.py b/examples/benchmark_tasks/optimization_qap_construct/__init__.py new file mode 100644 index 00000000..f4005a73 --- /dev/null +++ b/examples/benchmark_tasks/optimization_qap_construct/__init__.py @@ -0,0 +1,293 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_qap_construct +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# Module Name: QAPEvaluation +# Last Revision: 2025/2/16 +# Description: Evaluates the Quadratic Assignment Problem (QAP). +# The QAP involves assigning a set of facilities to a set of locations in such a way that the total cost of interactions between facilities is minimized. +# This module is part of the LLM4AD project (https://github.com/Optima-CityU/llm4ad). +# +# Parameters: +# - timeout_seconds: Maximum allowed time (in seconds) for the evaluation process: int (default: 20). +# - n_facilities: Number of facilities to assign: int (default: 50). +# - n_instance: Number of problem instances to generate: int (default: 10). +# +# References: +# - Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations +import numpy as np +from typing import Callable, Any, List, Tuple +import matplotlib.pyplot as plt + +from llm4ad_loader import Evaluation +import os, sys +sys.path.insert(0, os.path.dirname(__file__)) +from get_instance import GetData +# from llm4ad.task.optimization.qap_construct.get_instance import GetData # Converted from LLM4AD import +# from llm4ad.task.optimization.qap_construct.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\n\ndef select_next_assignment(current_assignment: List[int], flow_matrix: np.ndarray, distance_matrix: np.ndarray) -> List[int]:\n """\n A heuristic for the Quadratic Assignment Problem.\n\n Args:\n current_assignment: Current assignment of facilities to locations (-1 means unassigned).\n flow_matrix: Flow matrix between facilities.\n distance_matrix: Distance matrix between locations.\n\n Returns:\n Updated assignment of facilities to locations.\n """\n n_facilities = len(current_assignment)\n \n # Find the first unassigned facility and the first available location\n for facility in range(n_facilities):\n if current_assignment[facility] == -1:\n # Find the first available location\n for location in range(n_facilities):\n if location not in current_assignment:\n current_assignment[facility] = location\n break\n break\n \n return current_assignment' +task_description = "'" + + +__all__ = ['QAPEvaluation'] + + +class QAPEvaluation(Evaluation): + """Evaluator for the Quadratic Assignment Problem.""" + + def __init__(self, + timeout_seconds=20, + n_facilities=50, + n_instance=16, + **kwargs): + """ + Initializes the QAP evaluator. + """ + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + self.n_instance = n_instance + self.n_facilities = n_facilities + self.data_generator = GetData(self.n_instance, self.n_facilities) + self._datasets = self.data_generator.generate_instances() + + def evaluate_program(self, program_str: str, callable_func: Callable) -> Any | None: + """ + Evaluates the program (constructive heuristic) for the QAP. + + Args: + program_str: Program string (not used here, but kept for compatibility). + callable_func: The constructive heuristic function to evaluate. + + Returns: + The average total cost across all instances. + """ + return self.evaluate_qap(callable_func) + + def plot_solution(self, flow_matrix: np.ndarray, distance_matrix: np.ndarray, assignment: List[int]): + """ + Plot the solution of the Quadratic Assignment Problem. + + Args: + flow_matrix: Flow matrix between facilities. + distance_matrix: Distance matrix between locations. + assignment: Assignment of facilities to locations. + """ + n_facilities = len(assignment) + + # Generate random coordinates for locations (for visualization purposes) + np.random.seed(42) # For reproducibility + locations = np.random.rand(n_facilities, 2) * 10 # Scale coordinates for better visualization + + # Plot locations + plt.figure(figsize=(8, 6)) + for loc_id, (x, y) in enumerate(locations): + plt.scatter(x, y, color='blue', s=200, label='Locations' if loc_id == 0 else "", zorder=1) + plt.text(x, y, f'L{loc_id + 1}', fontsize=12, ha='right', va='bottom', zorder=1) + + # Plot facilities and connections based on flow + for facility_id, loc_id in enumerate(assignment): + x, y = locations[loc_id] + plt.scatter(x, y, color='red', s=100, marker='s', label='Facilities' if facility_id == 0 else "", zorder=2) + plt.text(x, y, f'F{facility_id + 1}', fontsize=12, ha='left', va='top', zorder=2) + + # Draw lines between facilities based on flow + for i in range(n_facilities): + for j in range(i + 1, n_facilities): + if flow_matrix[i, j] > 0: + loc_i = assignment[i] + loc_j = assignment[j] + plt.plot( + [locations[loc_i, 0], locations[loc_j, 0]], + [locations[loc_i, 1], locations[loc_j, 1]], + color='gray', linewidth=flow_matrix[i, j] / 10, alpha=0.5, zorder=0 + ) + + plt.title('QAP Solution: Facilities Assigned to Locations') + plt.xlabel('X Coordinate') + plt.ylabel('Y Coordinate') + plt.legend() + plt.grid(True) + plt.show() + + def qap_evaluate(self, current_assignment: List[int], flow_matrix: np.ndarray, distance_matrix: np.ndarray, eva: Callable) -> List[int]: + """ + Evaluate the next assignment for the Quadratic Assignment Problem using a constructive heuristic. + + Args: + current_assignment: Current assignment of facilities to locations. + flow_matrix: Flow matrix between facilities. + distance_matrix: Distance matrix between locations. + eva: The constructive heuristic function to select the next assignment. + + Returns: + Updated assignment of facilities to locations. + """ + # Use the heuristic to select the next assignment + + n_facilities = flow_matrix.shape[0] + for _ in range(n_facilities): + next_assignment = eva(current_assignment, flow_matrix, distance_matrix) + + return next_assignment + + def evaluate_qap(self, eva: Callable) -> float: + """ + Evaluate the constructive heuristic for the Quadratic Assignment Problem. + + Args: + instance_data: List of tuples containing the flow and distance matrices. + n_ins: Number of instances to evaluate. + eva: The constructive heuristic function to evaluate. + + Returns: + The average total cost across all instances. + """ + total_cost = 0 + + for instance in self._datasets[:self.n_instance]: + flow_matrix, distance_matrix = instance + n_facilities = flow_matrix.shape[0] + current_assignment = [-1] * n_facilities # Initialize with no assignments + current_assignment = self.qap_evaluate(current_assignment, flow_matrix, distance_matrix, eva) + + # Check if current_assignment is a feasible solution + if -1 in current_assignment: + raise ValueError("Feasibility check failed: Not all facilities are allocated.") + if any(not (0 <= x < n_facilities) for x in current_assignment): + raise ValueError("Feasibility check failed: Assignment values are out of range.") + if len(set(current_assignment)) != n_facilities: + raise ValueError("Feasibility check failed: Duplicate assignment values found.") + + # Calculate the total cost of the assignment + cost = 0 + for i in range(n_facilities): + for j in range(n_facilities): + cost += flow_matrix[i, j] * distance_matrix[current_assignment[i], current_assignment[j]] + total_cost += cost + + average_cost = total_cost / self.n_instance + return -average_cost # We want to minimize the total cost + + +if __name__ == '__main__': + + def select_next_assignment(current_assignment: List[int], flow_matrix: np.ndarray, distance_matrix: np.ndarray) -> List[int]: + """ + A greedy heuristic for the Quadratic Assignment Problem. + + Args: + current_assignment: Current assignment of facilities to locations (-1 means unassigned). + flow_matrix: Flow matrix between facilities. + distance_matrix: Distance matrix between locations. + + Returns: + Updated assignment of facilities to locations. + """ + n_facilities = len(current_assignment) + + # Find the first unassigned facility and the first available location + for facility in range(n_facilities): + if current_assignment[facility] == -1: + # Find the first available location + for location in range(n_facilities): + if location not in current_assignment: + current_assignment[facility] = location + break + break + + return current_assignment + + + bp1d = QAPEvaluation() + ave_bins = bp1d.evaluate_program('_', select_next_assignment) + print(ave_bins) + +# Task configuration for benchmark task +ENTRY_NAME = 'select_next_assignment' +FUNCTION_SIGNATURE = 'def select_next_assignment(...):' +IMPORT_HEADER = 'import numpy as np\nimport math' +TASK_DESCRIPTION = "'" +OBJECTIVE_TEXT = "You are optimizing the implementation of `select_next_assignment` for the LLM4AD task.\\n\\nTask description:\\n'\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible." +TEMPLATE_FUNCTION = 'import numpy as np\n\ndef select_next_assignment(current_assignment: List[int], flow_matrix: np.ndarray, distance_matrix: np.ndarray) -> List[int]:\n """\n A heuristic for the Quadratic Assignment Problem.\n\n Args:\n current_assignment: Current assignment of facilities to locations (-1 means unassigned).\n flow_matrix: Flow matrix between facilities.\n distance_matrix: Distance matrix between locations.\n\n Returns:\n Updated assignment of facilities to locations.\n """\n n_facilities = len(current_assignment)\n \n # Find the first unassigned facility and the first available location\n for facility in range(n_facilities):\n if current_assignment[facility] == -1:\n # Find the first available location\n for location in range(n_facilities):\n if location not in current_assignment:\n current_assignment[facility] = location\n break\n break\n \n return current_assignment' +EVAL_CLASS_NAME = 'QAPEvaluation' +EVAL_KWARGS = {'timeout_seconds': 30} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_qap_construct/get_instance.py b/examples/benchmark_tasks/optimization_qap_construct/get_instance.py new file mode 100644 index 00000000..a972efb4 --- /dev/null +++ b/examples/benchmark_tasks/optimization_qap_construct/get_instance.py @@ -0,0 +1,48 @@ +import numpy as np + +class GetData: + def __init__(self, n_instance: int, n_facilities: int): + """ + Initialize the QAPDataGenerator class for the Quadratic Assignment Problem. + + Args: + n_instance: Number of instances to generate. + n_facilities: Number of facilities (and locations). + """ + self.n_instance = n_instance + self.n_facilities = n_facilities + + def generate_instances(self): + """ + Generate instances for the Quadratic Assignment Problem. + + Returns: + A list of tuples, where each tuple contains: + - flow_matrix: A 2D numpy array representing the flow between facilities. + - distance_matrix: A 2D numpy array representing the distance between locations. + """ + np.random.seed(2024) # Set seed for reproducibility + instance_data = [] + + for _ in range(self.n_instance): + # Generate random flow and distance matrices + flow_matrix = np.random.randint(1, 101, size=(self.n_facilities, self.n_facilities)) + distance_matrix = np.random.randint(1, 101, size=(self.n_facilities, self.n_facilities)) + + # Ensure the matrices are symmetric and have zero diagonals + flow_matrix = (flow_matrix + flow_matrix.T) // 2 + np.fill_diagonal(flow_matrix, 0) + + distance_matrix = (distance_matrix + distance_matrix.T) // 2 + np.fill_diagonal(distance_matrix, 0) + + instance_data.append((flow_matrix, distance_matrix)) + + return instance_data + +# Example usage: +# generator = QAPDataGenerator(n_instance=5, n_facilities=4) +# instances = generator.generate_instances() +# for flow, distance in instances: +# print("Flow Matrix:\n", flow) +# print("Distance Matrix:\n", distance) diff --git a/examples/benchmark_tasks/optimization_qap_construct/paras.yaml b/examples/benchmark_tasks/optimization_qap_construct/paras.yaml new file mode 100644 index 00000000..8962e72e --- /dev/null +++ b/examples/benchmark_tasks/optimization_qap_construct/paras.yaml @@ -0,0 +1,2 @@ +name: QAPEvaluation +timeout_seconds: 30 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_resource_constrained_shortest_path/__init__.py b/examples/benchmark_tasks/optimization_resource_constrained_shortest_path/__init__.py new file mode 100644 index 00000000..cf799657 --- /dev/null +++ b/examples/benchmark_tasks/optimization_resource_constrained_shortest_path/__init__.py @@ -0,0 +1,353 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_resource_constrained_shortest_path +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# References: +# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language +# model agents in algorithm search for combinatorial optimization. +# arXiv preprint arXiv:2504.04310 (2025). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any +import numpy as np +from llm4ad_loader import Evaluation +from llm4ad_loader import load_subdir_as_text +# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader +# from llm4ad.task.optimization.co_bench.resource_constrained_shortest_path_co_bench.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n:int, m:int, K:int, lower_bounds:list, upper_bounds:list, vertex_resources:list, graph:dict) -> dict:\n """\n Solve the Resource Constrained Shortest Path problem.\n Input kwargs should include:\n - n (int): number of vertices,\n - m (int): number of arcs,\n - K (int): number of resources,\n - lower_bounds (list of float): list of lower limits for each resource,\n - upper_bounds (list of float): list of upper limits for each resource,\n - vertex_resources (list of list of float): list (of length n) of lists (of length K) with the resource consumption at each vertex,\n - graph (dict): dictionary mapping each vertex (1-indexed) to a list of arcs, where each arc is a tuple\n (end_vertex (int), cost (float), [arc resource consumptions] (list of float)).\n Evaluation Metric:\n If the computed path is valid (i.e. it starts at vertex 1, ends at vertex n, every transition is\n defined in the graph, and the total resource consumption from both vertices and arcs is within the\n specified bounds for each resource), then the score equals the total arc cost along the path.\n Otherwise, the solution is invalid and receives no score.\n Returns:\n A dictionary with keys:\n "total_cost": total cost (a float) of the computed path,\n "path": a list of vertex indices (integers) defining the path.\n (Placeholder implementation)\n """\n # Placeholder implementation.\n n = kwargs.get("n", 1)\n # Return a trivial solution: just go directly from vertex 1 to vertex n.\n return {"total_cost": 0.0, "path": [1, n]}' +task_description = '("This problem involves finding the shortest path from vertex 1 to vertex n in a directed graph "' + + +__all__ = ['RCSPEvaluationCB'] + + +class RCSPEvaluationCB(Evaluation): + + def __init__(self, + timeout_seconds=50, + **kwargs): + + """ + Args: + None + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + # Load datasets from Hugging Face + dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Resource constrained shortest path") + self._datasets = {} + for filename in dataset: + # Join all text rows into a single string + text_content = '\n'.join([row['text'] for row in dataset[filename]]) + self._datasets[filename] = text_content + + def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: + return self.evaluate(callable_func) + + def evaluate(self, eva: callable) -> float | None: + ins_cases = [] + for case_id, ins in enumerate(self._datasets.values()): + ins_cases.append(self.load_data(ins)) + + fitness_list = [] + try: + for i in ins_cases: + for j in i: + result = eva(j['n'], j['m'], j['K'], j['lower_bounds'], j['upper_bounds'], j['vertex_resources'], j['graph']) + fitness = self.eval_func(j['n'], j['m'], j['K'], j['lower_bounds'], j['upper_bounds'], j['vertex_resources'], j['graph'], result['total_cost'], result['path']) + fitness_list.append(fitness) + + return -np.mean(fitness_list) + + except ValueError as e: + print(e) + return None + + def load_data(self, input_string): + """ + Load one or more cases from a TXT input file for the Resource Constrained Shortest Path problem. + The input file format (per case) is as follows: + 1. Three numbers: n (number of vertices), m (number of arcs), K (number of resources) + 2. For each resource (k = 1,...,K): the lower limit on the resource consumed on the chosen path. + 3. For each resource (k = 1,...,K): the upper limit on the resource consumed on the chosen path. + 4. For each vertex (i = 1,...,n): K numbers indicating the resource consumption incurred at that vertex. + 5. For each arc (j = 1,...,m): (3 + K) numbers: + - starting vertex, + - ending vertex, + - cost of the arc, + - K numbers indicating the resource consumption incurred on the arc. + Note: + In many of the RCSP test files, the file is a stream of numbers separated by whitespace rather than fixed lines. + This implementation reads the entire file and splits it into tokens. + Returns: + A list of cases. Each case is a dictionary with keys: + "n", "m", "K", "lower_bounds", "upper_bounds", "vertex_resources", "graph" + """ + tokens = input_string.split() + + cases = [] + pos = 0 + total_tokens = len(tokens) + + while pos < total_tokens: + if pos + 3 > total_tokens: + break # Not enough tokens for a new case header. + try: + n = int(tokens[pos]) + m = int(tokens[pos + 1]) + K = int(tokens[pos + 2]) + except Exception as e: + raise ValueError("Error reading header (n, m, K)") from e + pos += 3 + + if pos + K > total_tokens: + raise ValueError("Not enough tokens for lower bounds.") + lower_bounds = [float(tokens[pos + i]) for i in range(K)] + pos += K + + if pos + K > total_tokens: + raise ValueError("Not enough tokens for upper bounds.") + upper_bounds = [float(tokens[pos + i]) for i in range(K)] + pos += K + + if pos + n * K > total_tokens: + raise ValueError("Not enough tokens for vertex resource consumption.") + vertex_resources = [] + for i in range(n): + vertex_resources.append([float(tokens[pos + j]) for j in range(K)]) + pos += K + + if pos + m * (3 + K) > total_tokens: + raise ValueError("Not enough tokens for arc information.") + graph = {i: [] for i in range(1, n + 1)} + for j in range(m): + try: + u = int(tokens[pos]) + v = int(tokens[pos + 1]) + cost = float(tokens[pos + 2]) + arc_resources = [float(tokens[pos + 3 + i]) for i in range(K)] + except Exception as e: + raise ValueError("Error reading arc information.") from e + pos += 3 + K + graph[u].append((v, cost, arc_resources)) + + case = { + "n": n, + "m": m, + "K": K, + "lower_bounds": lower_bounds, + "upper_bounds": upper_bounds, + "vertex_resources": vertex_resources, + "graph": graph + } + cases.append(case) + + return cases + + def eval_func(self, n, m, K, lower_bounds, upper_bounds, vertex_resources, graph, total_cost, path): + """ + Evaluate the solution for one case of the Resource Constrained Shortest Path problem. + Parameters: + n, m, K : Input parameters defining the problem instance. + lower_bounds : List of lower resource bounds (length K). + upper_bounds : List of upper resource bounds (length K). + vertex_resources : List (length n) of lists (each of length K) with resource consumption per vertex. + graph : Dictionary mapping each vertex (1-indexed) to its outgoing arcs. + Each arc is a tuple (end_vertex, cost, [arc resource consumptions]). + total_cost : The total cost value reported by the solution (not used in validation). + path : List of vertex indices (integers) defining the computed path. + Returns: + The total arc cost along the path if the solution is valid. + Raises: + ValueError: If the solution is invalid (i.e. the path does not start at vertex 1, does not end at vertex n, + contains an undefined arc, or the cumulative resource consumption (from both vertices and arcs) + is not within the specified bounds for each resource). + """ + + # Check basic validity of the path. + if not path or path[0] != 1 or path[-1] != n: + raise ValueError("Invalid solution: path must start at vertex 1 and end at vertex n.") + + computed_cost = 0.0 + total_resources = [0.0] * K + + # Add resource consumption from vertices. + for vertex in path: + if vertex < 1 or vertex > n: + raise ValueError(f"Invalid solution: vertex {vertex} is out of valid range 1 to {n}.") + for k in range(K): + total_resources[k] += vertex_resources[vertex - 1][k] + + # For each consecutive pair in the path, check for a valid arc and add its cost and resource consumption. + for i in range(len(path) - 1): + u = path[i] + v = path[i + 1] + valid_arc = False + for (dest, arc_cost, arc_res) in graph.get(u, []): + if dest == v: + valid_arc = True + computed_cost += arc_cost + for k in range(K): + total_resources[k] += arc_res[k] + break + if not valid_arc: + raise ValueError(f"Invalid solution: no valid arc from vertex {u} to vertex {v}.") + + # Verify resource constraints. + for k in range(K): + if total_resources[k] < lower_bounds[k] - 1e-6 or total_resources[k] > upper_bounds[k] + 1e-6: + raise ValueError( + f"Invalid solution: total consumption for resource {k} is {total_resources[k]}, " + f"which is outside the bounds [{lower_bounds[k]}, {upper_bounds[k]}]." + ) + + return computed_cost + + def norm_score(self, results): + optimal_scores = { + "rcsp1.txt": [88.3], + "rcsp2.txt": [131], + "rcsp3.txt": [1.44], + "rcsp4.txt": [2], + "rcsp5.txt": [81.9], + "rcsp6.txt": [91.4], + "rcsp7.txt": [3.91], + "rcsp8.txt": [3.77], + "rcsp9.txt": [420], + "rcsp10.txt": [420], + "rcsp11.txt": [6], + "rcsp12.txt": [6], + "rcsp13.txt": [448], + "rcsp14.txt": [656], + "rcsp15.txt": [6.2], + "rcsp16.txt": [5], + "rcsp17.txt": [487], + "rcsp18.txt": [512], + "rcsp19.txt": [6], + "rcsp20.txt": [6], + "rcsp21.txt": [858], + "rcsp22.txt": [858], + "rcsp23.txt": [3.34], + "rcsp24.txt": [3.74] + } + + normed = {} + for case, (scores, error_message) in results.items(): + if case not in optimal_scores: + continue # Skip if there's no optimal score defined. + optimal_list = optimal_scores[case] + normed_scores = [] + # Compute normalized score for each index. + for idx, score in enumerate(scores): + if isinstance(score, (int, float)): + normed_scores.append(optimal_list[idx] / score) + else: + normed_scores.append(score) + normed[case] = (normed_scores, error_message) + + return normed + + def get_dev(self): + dev = {'rcsp1.txt': [], 'rcsp11.txt': [], 'rcsp13.txt': [], + 'rcsp15.txt': [], 'rcsp17.txt': [], 'rcsp19.txt': [], + 'rcsp21.txt': [], 'rcsp23.txt': [], 'rcsp3.txt': [], + 'rcsp5.txt': [], 'rcsp7.txt': [], 'rcsp9.txt': []} + + return dev + + + + + + + +# Task configuration for benchmark task +ENTRY_NAME = 'solve' +FUNCTION_SIGNATURE = 'def solve(...):' +IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' +TASK_DESCRIPTION = '("This problem involves finding the shortest path from vertex 1 to vertex n in a directed graph "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("This problem involves finding the shortest path from vertex 1 to vertex n in a directed graph "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n:int, m:int, K:int, lower_bounds:list, upper_bounds:list, vertex_resources:list, graph:dict) -> dict:\n """\n Solve the Resource Constrained Shortest Path problem.\n Input kwargs should include:\n - n (int): number of vertices,\n - m (int): number of arcs,\n - K (int): number of resources,\n - lower_bounds (list of float): list of lower limits for each resource,\n - upper_bounds (list of float): list of upper limits for each resource,\n - vertex_resources (list of list of float): list (of length n) of lists (of length K) with the resource consumption at each vertex,\n - graph (dict): dictionary mapping each vertex (1-indexed) to a list of arcs, where each arc is a tuple\n (end_vertex (int), cost (float), [arc resource consumptions] (list of float)).\n Evaluation Metric:\n If the computed path is valid (i.e. it starts at vertex 1, ends at vertex n, every transition is\n defined in the graph, and the total resource consumption from both vertices and arcs is within the\n specified bounds for each resource), then the score equals the total arc cost along the path.\n Otherwise, the solution is invalid and receives no score.\n Returns:\n A dictionary with keys:\n "total_cost": total cost (a float) of the computed path,\n "path": a list of vertex indices (integers) defining the path.\n (Placeholder implementation)\n """\n # Placeholder implementation.\n n = kwargs.get("n", 1)\n # Return a trivial solution: just go directly from vertex 1 to vertex n.\n return {"total_cost": 0.0, "path": [1, n]}' +EVAL_CLASS_NAME = 'RCSPEvaluationCB' +EVAL_KWARGS = {'timeout_seconds': 60} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_resource_constrained_shortest_path/paras.yaml b/examples/benchmark_tasks/optimization_resource_constrained_shortest_path/paras.yaml new file mode 100644 index 00000000..1cc93736 --- /dev/null +++ b/examples/benchmark_tasks/optimization_resource_constrained_shortest_path/paras.yaml @@ -0,0 +1,2 @@ +name: RCSPEvaluationCB +timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_set_cover_construct/__init__.py b/examples/benchmark_tasks/optimization_set_cover_construct/__init__.py new file mode 100644 index 00000000..abc12095 --- /dev/null +++ b/examples/benchmark_tasks/optimization_set_cover_construct/__init__.py @@ -0,0 +1,296 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_set_cover_construct +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# Module Name: SCPEvaluation +# Last Revision: 2025/2/16 +# Description: Evaluates the Set Covering Problem (SCP). +# The SCP involves selecting a minimum number of subsets from a collection that covers all elements in a universal set. +# This module is part of the LLM4AD project (https://github.com/Optima-CityU/llm4ad). +# +# Parameters: +# - timeout_seconds: Maximum allowed time (in seconds) for the evaluation process: int (default: 30). +# - n_instance: Number of problem instances to generate: int (default: 5). +# - n_elements: Number of elements in the universal set: int (default: 10). +# - n_subsets: Number of subsets in the collection: int (default: 15). +# - max_subset_size: Maximum size of each subset: int (default: 5). +# +# References: +# - Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations +from typing import Any, List, Tuple, Callable +import numpy as np +import matplotlib.pyplot as plt + +from llm4ad_loader import Evaluation +import os, sys +sys.path.insert(0, os.path.dirname(__file__)) +from get_instance import GetData +# from llm4ad.task.optimization.set_cover_construct.get_instance import GetData # Converted from LLM4AD import +# from llm4ad.task.optimization.set_cover_construct.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\ndef select_next_subset(selected_subsets: List[List[int]], remaining_subsets: List[List[int]], remaining_elements: List[int]) -> List[int] | None:\n """\n A heuristic for the Set Covering Problem.\n\n Args:\n selected_subsets: List of already selected subsets.\n remaining_subsets: List of remaining subsets to choose from.\n remaining_elements: List of elements still to be covered.\n\n Returns:\n The next subset to select, or None if no subset can cover any remaining elements.\n """\n max_covered = 0\n best_subset = None\n\n for subset in remaining_subsets:\n # Calculate the number of uncovered elements this subset covers\n covered = len(set(subset).intersection(remaining_elements))\n if covered > max_covered:\n max_covered = covered\n best_subset = subset\n\n return best_subset' +task_description = "'" + + +__all__ = ['SCPEvaluation'] + +import matplotlib.pyplot as plt + + +class SCPEvaluation(Evaluation): + """Evaluator for the Set Covering Problem.""" + + def __init__(self, + timeout_seconds=30, + n_instance: int = 16, + n_elements: int = 50, + n_subsets: int = 50, + max_subset_size: int = 8, + **kwargs): + """ + Args: + n_instance: Number of instances to generate. + n_elements: Number of elements in the universal set. + n_subsets: Number of subsets in the collection. + max_subset_size: Maximum size of each subset. + """ + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + self.n_instance = n_instance + self.n_elements = n_elements + self.n_subsets = n_subsets + self.max_subset_size = max_subset_size + + getData = GetData(self.n_instance, self.n_elements, self.n_subsets, self.max_subset_size) + self._datasets = getData.generate_instances() + + def evaluate_program(self, program_str: str, callable_func: Callable) -> Any | None: + """ + Evaluate a constructive heuristic for the Set Covering Problem. + + Args: + program_str: A string representation of the heuristic (unused here). + callable_func: The constructive heuristic function to evaluate. + + Returns: + The average number of subsets used. + """ + return self.evaluate(callable_func) + + def plot_solution(self, universal_set: List[int], selected_subsets: List[List[int]], all_subsets: List[List[int]]): + """ + Plot the final solution of the Set Covering Problem, including selected and unselected subsets. + + Args: + universal_set: The universal set of elements. + selected_subsets: The list of selected subsets that cover the universal set. + all_subsets: The list of all subsets (including unselected ones). + """ + # Create a mapping of elements to their positions for plotting + element_positions = {element: idx for idx, element in enumerate(universal_set)} + + # Plot the universal set + plt.figure(figsize=(10, 6)) + plt.scatter([element_positions[element] for element in universal_set], [0] * len(universal_set), + color='blue', label='Universal Set', s=100) + + # Plot the selected subsets + for subset_idx, subset in enumerate(selected_subsets): + plt.scatter([element_positions[element] for element in subset], [subset_idx + 1] * len(subset), + label=f'Selected Subset {subset_idx + 1}', s=100, marker='o', edgecolor='black') + + # Plot the unselected subsets + unselected_subsets = [subset for subset in all_subsets if subset not in selected_subsets] + for subset_idx, subset in enumerate(unselected_subsets): + plt.scatter([element_positions[element] for element in subset], [subset_idx + len(selected_subsets) + 1] * len(subset), + label=f'Unselected Subset {subset_idx + 1}', s=100, marker='o', edgecolor='black', facecolor='none') + + # Add annotations and labels + y_labels = ['Universal Set'] + [f'Selected Subset {i + 1}' for i in range(len(selected_subsets))] + \ + [f'Unselected Subset {i + 1}' for i in range(len(unselected_subsets))] + plt.yticks(range(len(y_labels)), y_labels) + plt.xlabel('Elements') + plt.title('Set Covering Problem Solution') + plt.legend(loc='upper right') + plt.grid(True, axis='x') + plt.tight_layout() + plt.show() + + def cover_subsets(self, universal_set: List[int], subsets: List[List[int]], eva: Callable) -> Tuple[int, List[List[int]]]: + """ + Select subsets to cover the universal set using a constructive heuristic. + + Args: + universal_set: The universal set of elements to cover. + subsets: A list of subsets, where each subset is a list of elements. + eva: The constructive heuristic function to select the next subset. + + Returns: + A tuple containing: + - The total number of subsets used. + - A list of selected subsets. + """ + selected_subsets = [] # List to store the selected subsets + remaining_elements = set(universal_set) # Set to track uncovered elements + remaining_subsets = subsets.copy() # Copy of subsets to track remaining subsets + + while remaining_elements: + # Use the heuristic to select the next subset + selected_subset = eva(selected_subsets, remaining_subsets, list(remaining_elements)) + + if selected_subset is None: + break # No more subsets to select + + # Add the selected subset to the list of selected subsets + selected_subsets.append(selected_subset) + # Remove the covered elements from the remaining elements + remaining_elements -= set(selected_subset) + # Remove the selected subset from the remaining subsets + remaining_subsets.remove(selected_subset) + + # Calculate the number of subsets used + used_subsets = len(selected_subsets) + return used_subsets, selected_subsets + + def evaluate(self, eva: Callable) -> float: + """ + Evaluate the constructive heuristic for the Set Covering Problem. + + Args: + instance_data: List of tuples containing the universal set and subsets. + n_ins: Number of instances to evaluate. + eva: The constructive heuristic function to evaluate. + + Returns: + The average number of subsets used across all instances. + """ + total_subsets = 0 + + for instance in self._datasets[:self.n_instance]: + universal_set, subsets = instance + num_subsets, _ = self.cover_subsets(universal_set, subsets, eva) + total_subsets += num_subsets + + average_subsets = total_subsets / self.n_instance + return -average_subsets # Negative because we want to minimize the number of subsets + + +if __name__ == '__main__': + + def select_next_subset(selected_subsets: List[List[int]], remaining_subsets: List[List[int]], remaining_elements: List[int]) -> List[int] | None: + """ + A heuristic for the Set Covering Problem. + + Args: + selected_subsets: List of already selected subsets. + remaining_subsets: List of remaining subsets to choose from. + remaining_elements: List of elements still to be covered. + + Returns: + The next subset to select, or None if no subset can cover any remaining elements. + """ + max_covered = 0 + best_subset = None + + for subset in remaining_subsets: + # Calculate the number of uncovered elements this subset covers + covered = len(set(subset).intersection(remaining_elements)) + if covered > max_covered: + max_covered = covered + best_subset = subset + + return best_subset + + + bp1d = SCPEvaluation() + ave_bins = bp1d.evaluate_program('_', select_next_subset) + print(ave_bins) + +# Task configuration for benchmark task +ENTRY_NAME = 'select_next_subset' +FUNCTION_SIGNATURE = 'def select_next_subset(...):' +IMPORT_HEADER = 'import numpy as np\nimport math' +TASK_DESCRIPTION = "'" +OBJECTIVE_TEXT = "You are optimizing the implementation of `select_next_subset` for the LLM4AD task.\\n\\nTask description:\\n'\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible." +TEMPLATE_FUNCTION = 'import numpy as np\ndef select_next_subset(selected_subsets: List[List[int]], remaining_subsets: List[List[int]], remaining_elements: List[int]) -> List[int] | None:\n """\n A heuristic for the Set Covering Problem.\n\n Args:\n selected_subsets: List of already selected subsets.\n remaining_subsets: List of remaining subsets to choose from.\n remaining_elements: List of elements still to be covered.\n\n Returns:\n The next subset to select, or None if no subset can cover any remaining elements.\n """\n max_covered = 0\n best_subset = None\n\n for subset in remaining_subsets:\n # Calculate the number of uncovered elements this subset covers\n covered = len(set(subset).intersection(remaining_elements))\n if covered > max_covered:\n max_covered = covered\n best_subset = subset\n\n return best_subset' +EVAL_CLASS_NAME = 'SCPEvaluation' +EVAL_KWARGS = {'timeout_seconds': 30} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_set_cover_construct/get_instance.py b/examples/benchmark_tasks/optimization_set_cover_construct/get_instance.py new file mode 100644 index 00000000..16688bbc --- /dev/null +++ b/examples/benchmark_tasks/optimization_set_cover_construct/get_instance.py @@ -0,0 +1,53 @@ +import numpy as np + + +class GetData: + def __init__(self, n_instance: int, n_elements: int, n_subsets: int, max_subset_size: int): + """ + Initialize the GetData class for the Set Covering Problem. + + Args: + n_instance: Number of instances to generate. + n_elements: Number of elements in the universal set. + n_subsets: Number of subsets in the collection. + max_subset_size: Maximum size of each subset. + """ + self.n_instance = n_instance + self.n_elements = n_elements + self.n_subsets = n_subsets + self.max_subset_size = max_subset_size + + def generate_instances(self): + """ + Generate instances for the Set Covering Problem. + + Returns: + A list of tuples, where each tuple contains: + - universal_set: A list of elements in the universal set. + - subsets: A list of subsets, where each subset is a list of elements. + """ + np.random.seed(2024) # Set seed for reproducibility + instance_data = [] + + for _ in range(self.n_instance): + # Define the universal set + universal_set = list(range(1, self.n_elements + 1)) + + # Generate subsets + subsets = [] + for _ in range(self.n_subsets): + subset_size = np.random.randint(1, self.max_subset_size + 1) # Random subset size + subset = np.random.choice(universal_set, size=subset_size, replace=False).tolist() + subsets.append(subset) + + instance_data.append((universal_set, subsets)) + + return instance_data + +# # Example usage: +# data_generator = GetData(n_instance=3, n_elements=10, n_subsets=5, max_subset_size=5) +# instances = data_generator.generate_instances() +# for universal_set, subsets in instances: +# print("Universal Set:", universal_set) +# print("Subsets:", subsets) +# print() diff --git a/examples/benchmark_tasks/optimization_set_cover_construct/paras.yaml b/examples/benchmark_tasks/optimization_set_cover_construct/paras.yaml new file mode 100644 index 00000000..04688e9f --- /dev/null +++ b/examples/benchmark_tasks/optimization_set_cover_construct/paras.yaml @@ -0,0 +1,2 @@ +name: SCPEvaluation +timeout_seconds: 30 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_set_cover_construct/test.py b/examples/benchmark_tasks/optimization_set_cover_construct/test.py new file mode 100644 index 00000000..11f90187 --- /dev/null +++ b/examples/benchmark_tasks/optimization_set_cover_construct/test.py @@ -0,0 +1,125 @@ +import numpy as np + + +class GetData: + def __init__(self, n_instance: int, n_jobs: int, n_machines: int): + """ + Initialize the GetData class for JSSP. + + Args: + n_instance: Number of instances to generate. + n_jobs: Number of jobs. + n_machines: Number of machines. + """ + self.n_instance = n_instance + self.n_jobs = n_jobs + self.n_machines = n_machines + + def generate_instances(self): + """ + Generate instances for the Job Shop Scheduling Problem. + + Returns: + A list of tuples, where each tuple contains: + - processing_times: A list of lists representing the processing times of each job on each machine. + - n_jobs: Number of jobs. + - n_machines: Number of machines. + """ + np.random.seed(2024) # Set seed for reproducibility + instance_data = [] + + for _ in range(self.n_instance): + # Generate random processing times for each job on each machine + # Each job has a sequence of operations, and each operation is assigned to a machine + # For simplicity, we assume each job has exactly `n_machines` operations, one for each machine + processing_times = [] + for _ in range(self.n_jobs): + # Randomly assign processing times for each machine + job_processing_times = np.random.randint(1, 100, size=self.n_machines).tolist() + processing_times.append(job_processing_times) + + instance_data.append((processing_times, self.n_jobs, self.n_machines)) + + return instance_data + + +def determine_next_operation(current_status, feasible_operations): + """ + Determine the next operation to schedule based on a greedy heuristic. + + Args: + current_status: A dictionary representing the current status of each machine and job. + feasible_operations: A list of feasible operations that can be scheduled next. + + Returns: + The next operation to schedule, represented as a tuple (job_id, machine_id, processing_time). + """ + # Simple greedy heuristic: choose the operation with the shortest processing time + next_operation = min(feasible_operations, key=lambda x: x[2]) + return next_operation + + +def schedule_jobs(processing_times, n_jobs, n_machines): + """ + Schedule jobs on machines using a greedy constructive heuristic. + + Args: + processing_times: A list of lists representing the processing times of each job on each machine. + n_jobs: Number of jobs. + n_machines: Number of machines. + + Returns: + The makespan, which is the total time required to complete all jobs. + """ + # Initialize the current status of each machine and job + machine_status = [0] * n_machines # Time each machine is available + job_status = [0] * n_jobs # Time each job is available + operation_sequence = [[] for _ in range(n_jobs)] # Sequence of operations for each job + + # Initialize the list of all operations + all_operations = [] + for job_id in range(n_jobs): + for machine_id in range(n_machines): + all_operations.append((job_id, machine_id, processing_times[job_id][machine_id])) + + # Schedule operations until all are completed + while all_operations: + # Determine feasible operations + feasible_operations = [] + for operation in all_operations: + job_id, machine_id, processing_time = operation + if job_status[job_id] <= machine_status[machine_id]: + feasible_operations.append(operation) + + if len(feasible_operations) == 0: + next_operation = all_operations[0] + else: + # Determine the next operation to schedule + next_operation = determine_next_operation({'machine_status': machine_status, 'job_status': job_status}, feasible_operations) + + # Schedule the next operation + job_id, machine_id, processing_time = next_operation + start_time = max(job_status[job_id], machine_status[machine_id]) + end_time = start_time + processing_time + machine_status[machine_id] = end_time + job_status[job_id] = end_time + operation_sequence[job_id].append((machine_id, start_time, end_time)) + + # Remove the scheduled operation from the list of all operations + all_operations.remove(next_operation) + + # Calculate the makespan (total time required to complete all jobs) + makespan = max(job_status) + return makespan, operation_sequence + + +# Example usage +if __name__ == "__main__": + # Generate data + data_generator = GetData(n_instance=1, n_jobs=5, n_machines=5).generate_instances() + + for instance in data_generator: + processing_times, n1, n2 = instance + makespan, solution = schedule_jobs(processing_times, n1, n2) + print(makespan) + print(solution) diff --git a/examples/benchmark_tasks/optimization_set_covering/__init__.py b/examples/benchmark_tasks/optimization_set_covering/__init__.py new file mode 100644 index 00000000..42895e56 --- /dev/null +++ b/examples/benchmark_tasks/optimization_set_covering/__init__.py @@ -0,0 +1,497 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_set_covering +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# References: +# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language +# model agents in algorithm search for combinatorial optimization. +# arXiv preprint arXiv:2504.04310 (2025). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any +import numpy as np +from llm4ad_loader import Evaluation +from llm4ad_loader import load_subdir_as_text +# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader +# from llm4ad.task.optimization.co_bench.set_covering_co_bench.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(m: int, n: int, costs: list, row_cover: list) -> dict:\n """\n Solves the set covering optimization problem.\n Problem Description:\n Given m rows (constraints) and n columns (covering sets) with associated costs,\n choose a subset of columns such that every row is covered (i.e. for every row,\n at least one chosen column appears in that row\'s coverage list) while minimizing\n the total cost (the sum of the costs of the chosen columns).\n Input kwargs:\n - m: (int) number of rows.\n - n: (int) number of columns.\n - costs: (list of int) where costs[j] is the cost for column j+1.\n - row_cover: (list of list of int) where row_cover[i] contains the 1-indexed column\n numbers that cover row i+1.\n Evaluation Metric:\n The score is computed as the sum of the costs for the chosen columns.\n However, if any row is left uncovered by the chosen columns, the solution is invalid and receives no score.\n Otherwise, the score is simply the total cost of the selected columns.\n Returns:\n A dictionary with one key:\n - "selected_columns": a list of 1-indexed column numbers representing the chosen covering set.\n """\n ## placeholder. You do not need to write anything here.\n return {"selected_columns": []}' +task_description = '("Set Covering Problem. The goal is to select a subset of columns, each with an associated cost, "' + + +__all__ = ['SCEvaluationCB'] + + +class SCEvaluationCB(Evaluation): + + def __init__(self, + timeout_seconds=50, + **kwargs): + + """ + Args: + None + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + # Load datasets from Hugging Face + dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Set covering") + self._datasets = {} + for filename in dataset: + # Join all text rows into a single string + text_content = '\n'.join([row['text'] for row in dataset[filename]]) + self._datasets[filename] = text_content + + def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: + return self.evaluate(callable_func) + + def evaluate(self, eva: callable) -> float | None: + ins_cases = [] + for case_id, ins in enumerate(self._datasets.values()): + ins_cases.append(self.load_data(ins)) + + fitness_list = [] + try: + for i in ins_cases: + for j in i: + result = eva(j['m'], j['n'], j['costs'], j['row_cover']) + fitness = self.eval_func(m=j['m'], n=j['n'], costs=j['costs'], row_cover=j['row_cover'], selected_columns=result['selected_columns']) + fitness_list.append(fitness) + + return -np.mean(fitness_list) + + except ValueError as e: + print(e) + return None + + def load_data(self, input_string): + """ + Loads one or more set covering test cases from string content. + The input can contain one or more cases. Each case must follow one of three formats: + Format A (SCP/Beasley): + - Header (first nonempty line): two integers, m and n. + - Next: a cost vector of n integers (which may span multiple lines). + - Then: for each row, a line that starts with an integer k (number of columns covering the row) + followed by k space‑separated 1-indexed column indices. + Format B (Real-world rail problems): + - Header: two integers, m and n. + - Next n nonempty lines: each line describes a column by giving: + cost, the number of rows the column covers, and then that many 1-indexed row indices. + - Row coverage is then built by aggregating the information from each column. + Format C (Dense row format): + - Header: two integers, m and n. + - Next m nonempty lines: each line lists the 1-indexed column indices that cover that row. + - In this format, every column has an implicit unit cost. + If the input contains multiple cases, it is assumed that the cases are separated + by at least one blank line. + Returns: + A list of cases, where each case is a dictionary with keys: + - "m": number of rows (int) + - "n": number of columns (int) + - "costs": list of column costs (list of int) + - "row_cover": list of lists; each inner list contains the 1-indexed column numbers covering that row. + """ +import os, sys +sys.path.insert(0, os.path.dirname(__file__)) + import re + + content = input_string.strip() + + # Split into blocks by one or more blank lines. + blocks = re.split(r'\n\s*\n', content) + cases = [] + + # Check if the very first block is simply a test-case count. + first_block_tokens = blocks[0].split() + if len(first_block_tokens) == 1: + try: + num_cases = int(first_block_tokens[0]) + # Remove the count block and treat the remaining blocks as cases. + blocks = blocks[1:] + if len(blocks) != num_cases: + # Fall back: if the number doesn't match, assume each block is a case. + pass + except Exception: + pass # Not a test-case count; treat first block as a case. + + for block in blocks: + case = self._parse_single_case(block) + cases.append(case) + return cases + + def _parse_single_case(self, block): + """ + Helper function to parse a single test case from a block (string) of text. + The block must have its lines (nonempty) in one of the three supported formats. + """ + lines = [line.strip() for line in block.splitlines() if line.strip()] + if not lines: + raise ValueError("Encountered an empty test case block.") + + header = lines[0].split() + if len(header) < 2: + raise ValueError("Header must contain at least two integers (m and n).") + try: + m = int(header[0]) + n = int(header[1]) + except Exception as e: + raise ValueError("Error parsing m and n from header: " + str(e)) + + remaining_lines = lines[1:] + + # Determine format based on the number of remaining lines. + if len(remaining_lines) == n: + # Format B: one line per column. + costs = [] + col_rows = [] + for j in range(n): + tokens = remaining_lines[j].split() + if len(tokens) < 2: + raise ValueError(f"Column {j + 1}: expected at least cost and count.") + try: + cost = int(tokens[0]) + count = int(tokens[1]) + except Exception as e: + raise ValueError(f"Error parsing cost/count for column {j + 1}: {e}") + if len(tokens) < 2 + count: + raise ValueError(f"Column {j + 1}: expected {count} row indices, got {len(tokens) - 2}.") + try: + rows_for_col = list(map(int, tokens[2:2 + count])) + except Exception as e: + raise ValueError(f"Error parsing row indices for column {j + 1}: {e}") + costs.append(cost) + col_rows.append(rows_for_col) + # Build row coverage from column data. + row_cover = [[] for _ in range(m)] + for j in range(n): + for r in col_rows[j]: + if r < 1 or r > m: + raise ValueError(f"Column {j + 1}: row index {r} is out of bounds.") + row_cover[r - 1].append(j + 1) + return {"m": m, "n": n, "costs": costs, "row_cover": row_cover} + + elif len(remaining_lines) == m: + # Format C: one line per row (dense row format). + costs = [1] * n + row_cover = [] + for i in range(m): + try: + cols = list(map(int, remaining_lines[i].split())) + except Exception as e: + raise ValueError(f"Error parsing row {i + 1}: {e}") + row_cover.append(cols) + return {"m": m, "n": n, "costs": costs, "row_cover": row_cover} + + else: + # Format A: SCP test case. + # First, read cost vector tokens until we have n tokens. + cost_tokens = [] + line_index = 0 + while line_index < len(remaining_lines) and len(cost_tokens) < n: + tokens = remaining_lines[line_index].split() + cost_tokens.extend(tokens) + line_index += 1 + if len(cost_tokens) < n: + raise ValueError("Not enough tokens for cost vector.") + try: + costs = list(map(int, cost_tokens[:n])) + except Exception as e: + raise ValueError("Error converting cost tokens to integers: " + str(e)) + + # The remaining tokens represent row coverage. + row_tokens = [] + for line in remaining_lines[line_index:]: + row_tokens.extend(line.split()) + token_index = 0 + row_cover = [] + for i in range(m): + if token_index >= len(row_tokens): + raise ValueError(f"Not enough tokens for row {i + 1}.") + try: + k = int(row_tokens[token_index]) + except Exception as e: + raise ValueError(f"Error parsing coverage count for row {i + 1}: {e}") + token_index += 1 + if token_index + k > len(row_tokens): + raise ValueError(f"Not enough tokens for row {i + 1}: expected {k} tokens.") + try: + cols = list(map(int, row_tokens[token_index: token_index + k])) + except Exception as e: + raise ValueError(f"Error parsing column indices for row {i + 1}: {e}") + token_index += k + row_cover.append(cols) + return {"m": m, "n": n, "costs": costs, "row_cover": row_cover} + + def eval_func(self, **kwargs): + """ + Evaluates the solution for a single test case. + Parameters: + - m: (int) number of rows. + - n: (int) number of columns. + - costs: (list of int) where costs[j] is the cost for column j+1. + - row_cover: (list of list of int) where row_cover[i] contains the 1-indexed columns covering row i+1. + - selected_columns: a list of chosen 1-indexed column numbers. + Evaluation: + 1. Compute the total cost as the sum of the costs for each selected column. + 2. Verify that every row is covered by at least one of the selected columns. + If any row is uncovered, the function raises an error indicating the constraint violation. + Returns: + A scalar value representing the computed score (total cost) if all constraints are met. + Raises: + KeyError: if "selected_columns" is not provided in kwargs. + ValueError: if any selected column is out of valid bounds or if any row is left uncovered. + """ + m = kwargs["m"] + n = kwargs["n"] + costs = kwargs["costs"] + row_cover = kwargs["row_cover"] + + if "selected_columns" not in kwargs: + raise KeyError("Solution must contain 'selected_columns'.") + + selected_columns = set(kwargs["selected_columns"]) + + # Check that each selected column is within valid bounds. + for col in selected_columns: + if col < 1 or col > n: + raise ValueError(f"Column {col} is out of bounds (should be between 1 and {n}).") + + computed_cost = sum(costs[col - 1] for col in selected_columns) + + # Verify that every row is covered by at least one selected column. + uncovered_rows = [] + for i in range(m): + if not set(row_cover[i]).intersection(selected_columns): + uncovered_rows.append(i + 1) + + if uncovered_rows: + raise ValueError("Infeasible solution: rows such as {} are not covered.".format( + ', '.join(map(str, uncovered_rows[:10])) + )) + + return computed_cost + + def norm_score(self, results): + optimal_scores = { + "scp41.txt": [429], + "scp42.txt": [512], + "scp43.txt": [516], + "scp45.txt": [512], + "scp47.txt": [430], + "scp49.txt": [641], + "scp410.txt": [514], + "scp53.txt": [226], + "scp55.txt": [211], + "scp56.txt": [213], + "scp58.txt": [288], + "scp59.txt": [279], + "scp510.txt": [265], + "scp44.txt": [494], + "scp46.txt": [560], + "scp48.txt": [492], + "scp51.txt": [253], + "scp52.txt": [302], + "scp54.txt": [242], + "scp57.txt": [293], + "scp61.txt": [138], + "scp62.txt": [146], + "scp63.txt": [145], + "scp64.txt": [131], + "scp65.txt": [161], + "scpa1.txt": [253], + "scpa2.txt": [252], + "scpa3.txt": [232], + "scpa4.txt": [234], + "scpa5.txt": [236], + "scpb1.txt": [69], + "scpb2.txt": [76], + "scpb3.txt": [80], + "scpb4.txt": [79], + "scpb5.txt": [72], + "scpc1.txt": [227], + "scpc2.txt": [219], + "scpc3.txt": [243], + "scpc4.txt": [219], + "scpc5.txt": [215], + "scpd1.txt": [60], + "scpd2.txt": [66], + "scpd3.txt": [72], + "scpd4.txt": [62], + "scpd5.txt": [61], + "scpe1.txt": [5], + "scpe2.txt": [5], + "scpe3.txt": [5], + "scpe4.txt": [5], + "scpe5.txt": [5], + "scpnre1.txt": [29], + "scpnre2.txt": [32], + "scpnre3.txt": [28], + "scpnre4.txt": [30], + "scpnre5.txt": [28], + "scpnrf1.txt": [15], + "scpnrf2.txt": [16], + "scpnrf3.txt": [15], + "scpnrf4.txt": [15], + "scpnrf5.txt": [14], + "scpnrg1.txt": [184], + "scpnrg2.txt": [163], + "scpnrg3.txt": [174], + "scpnrg4.txt": [176], + "scpnrg5.txt": [175], + "scpnrh1.txt": [68], + "scpnrh2.txt": [66], + "scpnrh3.txt": [65], + "scpnrh4.txt": [63], + "scpnrh5.txt": [60], + "scpcyc06.txt": [48.0], + "scpcyc07.txt": [112.0], + "scpcyc08.txt": [256.0], + "scpcyc09.txt": [576.0], + "scpcyc010.txt": [1280.0], + "scpcyc011.txt": [2816.0], + "scpclr10.txt": [21.0], + "scpclr11.txt": [16.5], + "scpclr12.txt": [16.5], + "scpclr13.txt": [14.3], + "rail507.txt": [172.4], + "rail516.txt": [182], + "rail582.txt": [209.5], + "rail2536.txt": [691], + "rail2586.txt": [936.1], + "rail4284.txt": [1065], + "rail4872.txt": [1509], + } + + normed = {} + for case, (scores, error_message) in results.items(): + if case not in optimal_scores: + continue # Skip if there's no optimal score defined. + optimal_list = optimal_scores[case] + normed_scores = [] + # Compute normalized score for each index. + for idx, score in enumerate(scores): + if isinstance(score, (int, float)): + normed_scores.append(optimal_list[idx] / score) + else: + normed_scores.append(score) + normed[case] = (normed_scores, error_message) + + return normed + + def get_dev(self): + dev = {'rail2536.txt': [0], 'rail4284.txt': [0], + 'rail516.txt': [0], 'rail582.txt': [0], + 'scp410.txt': [0], 'scp42.txt': [0], + 'scp44.txt': [0], 'scp48.txt': [0], + 'scp52.txt': [0], 'scp54.txt': [0], + 'scp56.txt': [0], 'scp58.txt': [0], 'scp62.txt': [0], + 'scp64.txt': [0], 'scpa2.txt': [0], + 'scpa4.txt': [0], 'scpb2.txt': [0], 'scpb4.txt': [0], + 'scpc2.txt': [0], 'scpc4.txt': [0], + 'scpclr10.txt': [0], 'scpclr12.txt': [0], 'scpcyc010.txt': [0], + 'scpcyc06.txt': [0], 'scpcyc08.txt': [0], + 'scpd2.txt': [0], 'scpd4.txt': [0], 'scpd5.txt': [0], + 'scpe2.txt': [0], 'scpe4.txt': [0], 'scpnre2.txt': [0], + 'scpnre4.txt': [0], 'scpnrf2.txt': [0], + 'scpnrf4.txt': [0], 'scpnrg2.txt': [0], + 'scpnrg4.txt': [0], 'scpnrh2.txt': [0], + 'scpnrh4.txt': [0]} + + return dev + + + + + + + + +# Task configuration for benchmark task +ENTRY_NAME = 'solve' +FUNCTION_SIGNATURE = 'def solve(...):' +IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' +TASK_DESCRIPTION = '("Set Covering Problem. The goal is to select a subset of columns, each with an associated cost, "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("Set Covering Problem. The goal is to select a subset of columns, each with an associated cost, "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(m: int, n: int, costs: list, row_cover: list) -> dict:\n """\n Solves the set covering optimization problem.\n Problem Description:\n Given m rows (constraints) and n columns (covering sets) with associated costs,\n choose a subset of columns such that every row is covered (i.e. for every row,\n at least one chosen column appears in that row\'s coverage list) while minimizing\n the total cost (the sum of the costs of the chosen columns).\n Input kwargs:\n - m: (int) number of rows.\n - n: (int) number of columns.\n - costs: (list of int) where costs[j] is the cost for column j+1.\n - row_cover: (list of list of int) where row_cover[i] contains the 1-indexed column\n numbers that cover row i+1.\n Evaluation Metric:\n The score is computed as the sum of the costs for the chosen columns.\n However, if any row is left uncovered by the chosen columns, the solution is invalid and receives no score.\n Otherwise, the score is simply the total cost of the selected columns.\n Returns:\n A dictionary with one key:\n - "selected_columns": a list of 1-indexed column numbers representing the chosen covering set.\n """\n ## placeholder. You do not need to write anything here.\n return {"selected_columns": []}' +EVAL_CLASS_NAME = 'SCEvaluationCB' +EVAL_KWARGS = {'timeout_seconds': 60} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_set_covering/paras.yaml b/examples/benchmark_tasks/optimization_set_covering/paras.yaml new file mode 100644 index 00000000..68fbacb9 --- /dev/null +++ b/examples/benchmark_tasks/optimization_set_covering/paras.yaml @@ -0,0 +1,2 @@ +name: SCEvaluationCB +timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_set_partitioning/__init__.py b/examples/benchmark_tasks/optimization_set_partitioning/__init__.py new file mode 100644 index 00000000..6cc7cd3c --- /dev/null +++ b/examples/benchmark_tasks/optimization_set_partitioning/__init__.py @@ -0,0 +1,389 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_set_partitioning +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# References: +# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language +# model agents in algorithm search for combinatorial optimization. +# arXiv preprint arXiv:2504.04310 (2025). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any +import numpy as np +from llm4ad_loader import Evaluation +from llm4ad_loader import load_subdir_as_text +# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader +# from llm4ad.task.optimization.co_bench.set_partitioning_co_bench.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(num_rows: int, num_columns: int, columns_info: dict) -> dict:\n """\n Solve a set partitioning problem instance.\n The problem: Given a set of rows and a set of columns (each with an associated cost and a set\n of rows it covers), select a subset of columns so that each row is covered exactly once and the\n total cost is minimized.\n Input kwargs:\n - num_rows (int): Total number of rows. (int)\n - num_columns (int): Total number of columns. (int)\n - columns_info (dict): Dictionary mapping 1-indexed column indices (int) to a tuple:\n (cost (int), set of row indices (set[int]) covered by that column).\n Evaluation metric:\n The objective score equals the sum of the costs of the selected columns if the solution is feasible,\n i.e., if every row is covered exactly once. Otherwise, the solution is invalid and receives no score.\n Returns:\n A dictionary with key "selected_columns" containing a list of chosen column indices in strictly increasing order.\n (This is a placeholder implementation.)\n """\n # Placeholder implementation.\n # You must replace the following line with your actual solution logic.\n return {"selected_columns": []}' +task_description = '("This problem involves solving a set partitioning instance where the goal is to choose a subset "' + + +__all__ = ['SPEvaluationCB'] + + +class SPEvaluationCB(Evaluation): + + def __init__(self, + timeout_seconds=50, + **kwargs): + + """ + Args: + None + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + # Load datasets from Hugging Face + dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Set partitioning") + self._datasets = {} + for filename in dataset: + # Join all text rows into a single string + text_content = '\n'.join([row['text'] for row in dataset[filename]]) + self._datasets[filename] = text_content + + def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: + return self.evaluate(callable_func) + + def evaluate(self, eva: callable) -> float | None: + ins_cases = [] + for case_id, ins in enumerate(self._datasets.values()): + ins_cases.append(self.load_data(ins)) + + fitness_list = [] + try: + for i in ins_cases: + for j in i: + result = eva(j['num_rows'], j['num_columns'], j['columns_info']) + fitness = self.eval_func(num_rows=j['num_rows'], num_columns=j['num_columns'], columns_info=j['columns_info'], selected_columns=result['selected_columns']) + fitness_list.append(fitness) + + return -np.mean(fitness_list) + + except ValueError as e: + print(e) + return None + + def load_data(self, input_string): + """ + Load and validate one or multiple set partitioning cases from a TXT file. + The file may contain multiple cases. Each case is structured as follows: + - The first non-empty line of a case contains two integers: num_rows and num_columns. + - Then, for each of the num_columns columns, there is one line containing: + cost (int), count (int), followed by exactly 'count' integers (the row indices covered). + Each case is parsed and validated independently. If any inconsistency or formatting error is found, + a ValueError is raised. + Returns: + cases (list): A list of dictionaries, each representing one case with keys: + - 'num_rows': int + - 'num_columns': int + - 'columns_info': dict mapping column index (1-indexed) -> (cost, set(row_indices)) + """ + cases = [] + lines = [line.strip() for line in input_string.split('\n') if line.strip() != ''] + + index = 0 + total_lines = len(lines) + + while index < total_lines: + # Parse header line for one case. + header_tokens = lines[index].split() + index += 1 + if len(header_tokens) < 2: + raise ValueError("Header must contain two integers: num_rows and num_columns.") + try: + num_rows = int(header_tokens[0]) + num_columns = int(header_tokens[1]) + except Exception: + raise ValueError("Header values must be integers.") + + columns_info = {} + # There must be exactly num_columns lines following for the columns. + for j in range(1, num_columns + 1): + if index >= total_lines: + raise ValueError("Insufficient lines for all columns' data in a case.") + parts = lines[index].split() + index += 1 + + if len(parts) < 2: + raise ValueError("Each column line must have at least 2 tokens (cost and count).") + try: + cost = int(parts[0]) + count = int(parts[1]) + except Exception: + raise ValueError("Column cost and count must be integers.") + + if len(parts) != 2 + count: + raise ValueError(f"Column {j} is expected to have {2 + count} tokens, but got {len(parts)}.") + try: + row_list = [int(tok) for tok in parts[2:]] + except Exception: + raise ValueError("Row indices must be integers.") + + for r in row_list: + if r < 1 or r > num_rows: + raise ValueError("Row index out of the valid range (1 to num_rows).") + + columns_info[j] = (cost, set(row_list)) + + # Append the case as a dictionary. + cases.append({ + "num_rows": num_rows, + "num_columns": num_columns, + "columns_info": columns_info + }) + + if not cases: + raise ValueError("Input file is empty or contains no valid cases.") + return cases + + def eval_func(self, **kwargs): + """ + Evaluate a solution for a set partitioning problem case. + Expected kwargs: + - num_rows (int): Total number of rows. + - num_columns (int): Total number of columns. + - columns_info (dict): Dictionary mapping column index (1-indexed) to a tuple (cost, set(row indices)). + - selected_columns (list): List of selected column indices (should be in strictly increasing order). + Raises: + ValueError: If any constraints are violated, such as an invalid output format, + a column index error, or if any row is not covered exactly once. + Returns: + score (int): The computed score, which is the total cost of the selected columns. + Lower scores are better. + """ + # Retrieve input data. + num_rows = kwargs["num_rows"] + num_columns = kwargs["num_columns"] + columns_info = kwargs["columns_info"] + selected_columns = kwargs.get("selected_columns") + + # Validate that selected_columns is provided and is a list. + if selected_columns is None or not isinstance(selected_columns, list): + raise ValueError("selected_columns must be provided as a list.") + + # Enforce that the list is in strictly increasing order and has no duplicates. + if selected_columns != sorted(selected_columns) or len(selected_columns) != len(set(selected_columns)): + raise ValueError("selected_columns must be in strictly increasing order with no duplicates.") + + # Validate each selected column index. + for col in selected_columns: + if not isinstance(col, int) or col < 1 or col > num_columns: + raise ValueError(f"Invalid column index: {col}. Must be an integer between 1 and {num_columns}.") + + total_cost = 0 + row_coverage = [0] * (num_rows + 1) # 1-indexed; index 0 is unused. + + # Process each selected column. + for col in selected_columns: + if col not in columns_info: + raise ValueError(f"Column {col} not found in columns_info.") + cost, covered_rows = columns_info[col] + total_cost += cost + for r in covered_rows: + if r < 1 or r > num_rows: + raise ValueError(f"Invalid row index: {r} (must be between 1 and {num_rows}).") + row_coverage[r] += 1 + + # Ensure that every row is covered exactly once. + for r in range(1, num_rows + 1): + if row_coverage[r] != 1: + raise ValueError(f"Row {r} is covered {row_coverage[r]} times; each row must be covered exactly once.") + + return total_cost + + def norm_score(self, results): + optimal_scores = { + "bills_snowflake.txt": [34], + "exotic_fives.txt": [12], + "sppaa02.txt": [30494], + "sppaa03.txt": [49649], + "sppaa05.txt": [53839], + "sppaa06.txt": [27040], + "delta.txt": [126], + "heart.txt": [180], + "sppkl01.txt": [1086], + "sppkl02.txt": [219], + "meteor.txt": [60], + "sppnw01.txt": [114852], + "sppnw02.txt": [105444], + "sppnw03.txt": [24492], + "sppnw04.txt": [16862], + "sppnw05.txt": [132878], + "sppnw06.txt": [7810], + "sppnw07.txt": [5476], + "sppnw08.txt": [35894], + "sppnw09.txt": [67760], + "sppnw10.txt": [68271], + "sppnw11.txt": [116256], + "sppnw12.txt": [14118], + "sppnw13.txt": [50146], + "sppnw14.txt": [61844], + "sppnw15.txt": [67743], + "sppnw16.txt": [1181590], + "sppnw17.txt": [11115], + "sppnw18.txt": [340160], + "sppnw19.txt": [10898], + "sppnw20.txt": [16812], + "sppnw21.txt": [7408], + "sppnw22.txt": [6984], + "sppnw23.txt": [12534], + "sppnw24.txt": [6314], + "sppnw25.txt": [5960], + "sppnw26.txt": [6796], + "sppnw27.txt": [9933], + "sppnw28.txt": [8298], + "sppnw29.txt": [4274], + "sppnw30.txt": [3942], + "sppnw31.txt": [8038], + "sppnw32.txt": [14877], + "sppnw33.txt": [6678], + "sppnw34.txt": [10488], + "sppnw35.txt": [7216], + "sppnw36.txt": [7314], + "sppnw37.txt": [10068], + "sppnw38.txt": [5558], + "sppnw39.txt": [10080], + "sppnw40.txt": [10809], + "sppnw41.txt": [11307], + "sppnw42.txt": [7656], + "sppnw43.txt": [8904], + "sppus01.txt": [10036], + "sppus02.txt": [5965], + "sppus03.txt": [5338], + "sppus04.txt": [17854], + "sppaa01.txt": [55535.4], + "sppaa04.txt": [25877.6], + + } + + normed = {} + for case, (scores, error_message) in results.items(): + if case not in optimal_scores: + continue # Skip if there's no optimal score defined. + optimal_list = optimal_scores[case] + normed_scores = [] + # Compute normalized score for each index. + for idx, score in enumerate(scores): + if isinstance(score, (int, float)): + normed_scores.append(optimal_list[idx] / score) + else: + normed_scores.append(score) + normed[case] = (normed_scores, error_message) + + return normed + + def get_dev(self): + dev = {'bills_snowflake.txt': [0], 'meteor.txt': [0], + 'sppaa02.txt': [0], 'sppaa04.txt': [0], 'sppaa05.txt': [0], + + 'sppkl02.txt': [0], + 'sppnw02.txt': [0], + 'sppnw04.txt': [0], 'sppnw06.txt': [0], + 'sppnw08.txt': [0], 'sppnw10.txt': [0], 'sppnw12.txt': [0], + 'sppnw14.txt': [0], 'sppnw16.txt': [0], + 'sppnw18.txt': [0], 'sppnw20.txt': [0], 'sppnw22.txt': [0], + 'sppnw24.txt': [0], 'sppnw26.txt': [0], + 'sppnw28.txt': [0], 'sppnw30.txt': [0], 'sppnw32.txt': [0], + 'sppnw34.txt': [0], 'sppnw36.txt': [0], + 'sppnw38.txt': [0], 'sppnw40.txt': [0], 'sppnw42.txt': [0], + 'sppus02.txt': [0], 'sppus04.txt': [0]} + + return dev + + + + + + + + +# Task configuration for benchmark task +ENTRY_NAME = 'solve' +FUNCTION_SIGNATURE = 'def solve(...):' +IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' +TASK_DESCRIPTION = '("This problem involves solving a set partitioning instance where the goal is to choose a subset "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("This problem involves solving a set partitioning instance where the goal is to choose a subset "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(num_rows: int, num_columns: int, columns_info: dict) -> dict:\n """\n Solve a set partitioning problem instance.\n The problem: Given a set of rows and a set of columns (each with an associated cost and a set\n of rows it covers), select a subset of columns so that each row is covered exactly once and the\n total cost is minimized.\n Input kwargs:\n - num_rows (int): Total number of rows. (int)\n - num_columns (int): Total number of columns. (int)\n - columns_info (dict): Dictionary mapping 1-indexed column indices (int) to a tuple:\n (cost (int), set of row indices (set[int]) covered by that column).\n Evaluation metric:\n The objective score equals the sum of the costs of the selected columns if the solution is feasible,\n i.e., if every row is covered exactly once. Otherwise, the solution is invalid and receives no score.\n Returns:\n A dictionary with key "selected_columns" containing a list of chosen column indices in strictly increasing order.\n (This is a placeholder implementation.)\n """\n # Placeholder implementation.\n # You must replace the following line with your actual solution logic.\n return {"selected_columns": []}' +EVAL_CLASS_NAME = 'SPEvaluationCB' +EVAL_KWARGS = {'timeout_seconds': 60} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_set_partitioning/paras.yaml b/examples/benchmark_tasks/optimization_set_partitioning/paras.yaml new file mode 100644 index 00000000..9fc34ede --- /dev/null +++ b/examples/benchmark_tasks/optimization_set_partitioning/paras.yaml @@ -0,0 +1,2 @@ +name: SPEvaluationCB +timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_travelling_salesman_problem/__init__.py b/examples/benchmark_tasks/optimization_travelling_salesman_problem/__init__.py new file mode 100644 index 00000000..a8b76128 --- /dev/null +++ b/examples/benchmark_tasks/optimization_travelling_salesman_problem/__init__.py @@ -0,0 +1,334 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_travelling_salesman_problem +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# References: +# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language +# model agents in algorithm search for combinatorial optimization. +# arXiv preprint arXiv:2504.04310 (2025). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any +import numpy as np +from llm4ad_loader import Evaluation +from llm4ad_loader import load_subdir_as_text +# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader +# from llm4ad.task.optimization.co_bench.travelling_salesman_problem_co_bench.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(nodes: list) -> dict:\n """\n Solve a TSP instance.\n Args:\n - nodes (list): List of (x, y) coordinates representing cities in the TSP problem\n Format: [(x1, y1), (x2, y2), ..., (xn, yn)]\n Returns:\n dict: Solution information with:\n - \'tour\' (list): List of node indices representing the solution path\n Format: [0, 3, 1, ...] where numbers are indices into the nodes list\n """\n\n return {\n \'tour\': [],\n }' +task_description = '("The Traveling Salesman Problem (TSP) is a classic combinatorial optimization problem where, "' + + +__all__ = ['TSPEvaluationCB'] + + +class TSPEvaluationCB(Evaluation): + + def __init__(self, + timeout_seconds=50, + **kwargs): + + """ + Args: + None + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + # Load datasets from Hugging Face + dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Travelling salesman problem") + self._datasets = {} + for filename in dataset: + # Join all text rows into a single string + text_content = '\n'.join([row['text'] for row in dataset[filename]]) + self._datasets[filename] = text_content + + def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: + return self.evaluate(callable_func) + + def evaluate(self, eva: callable) -> float | None: + ins_cases = [] + for case_id, ins in enumerate(self._datasets.values()): + ins_cases.append(self.load_data(ins)) + + fitness_list = [] + try: + for i in ins_cases: + for j in i: + result = eva(j['nodes']) + fitness = self.eval_func(j['nodes'], j['tour'], result['tour']) + fitness_list.append(fitness) + + return -np.mean(fitness_list) + + except ValueError as e: + print(e) + return None + + def load_data(self, input_string): + """ + Load TSP instances from a file. + Args: + file_path (str): Path to the file containing TSP instances + Returns: + list: List of dictionaries, each containing a TSP instance with: + - 'nodes': List of (x, y) coordinates + - 'tour': List of node indices representing the optimal tour (if available) + """ + instances = [] + for line in input_string.split('\n'): + if line.strip(): # Skip empty lines + line = line.split(" ") + try: + output_idx = line.index('output') + num_nodes = output_idx // 2 + + # Extract node coordinates + nodes = [(float(line[i]), float(line[i + 1])) for i in range(0, 2 * num_nodes, 2)] + + # Extract tour (if available) + tour = None + if output_idx < len(line) - 1: + # Convert tour nodes to 0-indexed and exclude the final node (which is the same as the first) + tour = [int(node) - 1 for node in line[output_idx + 1:-1]][:-1] + + instances.append({ + 'nodes': nodes, + 'tour': tour # Changed from 'label_tour' to 'tour' to match eval_func + }) + except (ValueError, IndexError) as e: + print(f"Error processing line: {e}") + continue + return instances + + def eval_func(self, nodes, label_tour, tour): + """ + Evaluate a predicted TSP tour against a reference tour. + Args: + nodes (list): List of (x, y) coordinates representing cities in the TSP problem + Format: [(x1, y1), (x2, y2), ..., (xn, yn)] + label_tour (list): Reference/optimal tour as list of node indices + Format: [0, 3, 1, ...] (may be None if no reference available) + tour (list): Predicted tour from the solver as list of node indices + Format: [0, 3, 1, ...] + Returns: + float: Optimality gap percentage ((predicted_cost/optimal_cost - 1) * 100) + or just the predicted cost if no label_tour is provided + """ + # Calculate the predicted tour cost + import math + + num_nodes = len(nodes) + + if len(tour) != num_nodes: + raise Exception(f"Invalid tour length: Expected {num_nodes}, got {len(tour)}") + nodes_set = set(tour) + + if len(nodes_set) != num_nodes: + raise Exception(f"Invalid tour: Contains {len(nodes_set)} unique nodes, expected {num_nodes}") + + expected_nodes = set(range(num_nodes)) + if nodes_set != expected_nodes: + raise Exception(f"Invalid tour: Contains out-of-range or missing nodes") + + def calculate_tour_cost(nodes, tour): + cost = 0 + for i in range(len(tour)): + from_node = tour[i] + to_node = tour[(i + 1) % len(tour)] # Wrap around to the first node + + # Calculate Euclidean distance + from_x, from_y = nodes[from_node] + to_x, to_y = nodes[to_node] + segment_cost = math.sqrt((to_x - from_x) ** 2 + (to_y - from_y) ** 2) + + cost += segment_cost + + return cost + + pred_cost = calculate_tour_cost(nodes, tour) + + return pred_cost + + def norm_score(self, results): + optimal_scores = { + 'tsp10000_test_concorde.txt': [71.77] * 16, + 'tsp1000_test_concorde.txt': [23.180520881091528, 23.185595820967464, 23.015849671324247, + 23.537607117355098, + 23.437452128607738, 23.31718378127829, 23.337815853824736, 22.98403971254625, + 23.056714372610298, 23.344826856094013, 23.204461510197465, + 22.739131293587075, + 23.188355412394525, 22.89676721383878, 23.321213972552503, 23.288168535452023, + 23.40260594371496, 23.379338976209613, 23.373901670260118, 23.217316627245133, + 23.237964507712658, 23.468791280324233, 22.921856962988343, 23.10809259424775, + 23.370845238521724, 23.241556219224208, 23.348641855759727, 23.53455701244874, + 23.385399569524708, 23.324316152061755, 23.600128423871258, 22.97776918106818, + 23.23996887566731, 23.39944035075775, 23.21410580402093, 23.093180229981513, + 23.41235476581497, 22.907788976836535, 23.023973448563986, 23.38106742108426, + 23.015367118079723, 22.610650093362192, 23.728111421819854, 23.31046641124744, + 23.25381246570274, 22.889579599261864, 23.138723098665373, 23.228706227395723, + 23.420741250703944, 23.255723604641904, 23.63211466330456, 23.03074201227862, + 23.08458884685017, 23.241154659459145, 23.445330799785832, 23.315728497380498, + 23.262087203582375, 23.43107533587823, 23.020824065107902, 23.591574572456, + 23.01019854749962, 23.006394524552746, 23.117390281951273, 23.06132560795126, + 22.899650785646813, 23.17319516968116, 23.229133743009296, 23.187607300641957, + 22.83150095703399, 23.158901255572648, 23.298349320155108, 23.364983773246387, + 23.265256805650658, 23.73268837357109, 23.07144480109362, 23.202894990560697, + 23.34293044019312, 23.027139320724427, 23.005485112127072, 23.16783838686215, + 23.505726302417372, 23.002594549857108, 23.50388356372942, 23.147934207287026, + 23.149537479144914, 23.20934617772166, 23.591015529376406, 23.04614917635098, + 23.253196613627406, 23.608716670166032, 23.313874804840438, 23.14887954791675, + 23.261925104915175, 23.283273388936596, 22.869470302805432, 23.28919260955595, + 23.291061784892037, 23.26303190269252, 23.43192602385145, 22.992654709729297, + 23.53527899384453, 23.040088044723632, 23.165752550718327, 23.346603825959306, + 23.21040140495141, 23.346553301777227, 23.192654754892565, 23.30425312678073, + 23.03197099577737, 23.33672313379179, 23.209507048094107, 23.33316267340018, + 22.832592819311447, 23.47921422142005, 23.29841589882617, 22.79469376239716, + 23.437580101042798, 22.90129840984213, 23.377778449705787, 23.152730269355438, + 23.179248710299515, 23.150584655373375, 23.303559153530237, + 23.567343754278223, + 23.14174465613352, 23.236813383632978, 23.178718844944385, + 23.114735241004848], + 'tsp500_test_concorde.txt': [16.43849479258626, 16.30760609977988, 16.55368794754589, 17.0916769200107, + 16.358815620695264, 16.355575136034258, 16.468449176999673, 16.547487678806803, + 16.624118787814286, 16.875851583784797, 16.584382768436186, 16.775629024699168, + 16.625112093123217, 16.537041048883633, 16.211908886171635, 16.507889182815646, + 16.443711824038594, 16.772997858965947, 16.576148488026003, 16.644182889540385, + 16.83104599989968, 16.798687309323867, 16.64786310345603, 16.68678554471238, + 16.539765290816586, 16.158516162147357, 16.750957469266986, 16.454327423569975, + 16.437695592935125, 16.47266324558099, 16.5807314540603, 16.640030608011333, + 16.717644006541413, 16.538629003657803, 16.73424552661684, 16.702691981178777, + 16.4488503948912, 16.65158792760706, 16.21441667652796, 16.58894596771913, + 16.62425057027662, 16.411010231382186, 16.4198250548815, 16.880314028063836, + 16.654445215349824, 16.6703557900618, 16.811423319096434, 16.681548608331166, + 16.40538961977731, 16.375709814617032, 16.4755439381876, 16.352299703304702, + 16.358345088111275, 16.446260979610017, 16.479360821405024, 16.664705227172075, + 16.514514381377964, 16.703418138718607, 16.501081465067912, 16.758043371686597, + 16.529838521968927, 16.331302381910483, 16.769035549248624, 16.667247187672565, + 16.457565298893492, 16.649335805699657, 16.82614018506712, 16.938244810751787, + 16.7896287123959, 16.45162524049444, 16.60657770837926, 16.752028686357416, + 16.538134167181376, 16.419856051838476, 17.056640374302344, 16.763628081715684, + 16.76853264913112, 16.94949524434479, 16.57562195411809, 16.665389374714852, + 16.690740743946513, 16.405456340497622, 16.442597689610583, 16.801813848508267, + 16.670030108101063, 16.62938726279957, 16.23649751271661, 16.69571793825944, + 16.587558708667046, 16.32450912204972, 16.270614173517753, 16.75899873051874, + 16.803321805550524, 16.3602825442514, 16.58252109177151, 16.450516009703893, + 16.35900041167487, 16.637551343677693, 16.572893477964705, 16.73275661200808, + 16.541081653324518, 16.466516697851265, 17.021310751236744, 16.536183906712942, + 16.77678089186245, 16.35713000043851, 16.3183776670553, 16.68224023564231, + 16.672341313126555, 16.607714934366197, 16.634734868495503, 16.674511551735357, + 16.414641537953482, 16.849240225161548, 16.74452644717401, 16.50467692427514, + 16.93072503233582, 16.38341557967758, 16.610910144984917, 16.589115661773096, + 16.366818207481515, 16.599226446198887, 16.349609487246365, 16.38083156520364, + 16.732343248542644, 16.615639804768033, 16.603236295079725, 16.12821378820771]} + normed = {} + for case, (scores, error_message) in results.items(): + if case not in optimal_scores: + continue # Skip if there's no optimal score defined. + optimal_list = optimal_scores[case] + normed_scores = [] + # Compute normalized score for each index. + for idx, score in enumerate(scores): + if isinstance(score, (int, float)): + normed_scores.append(optimal_list[idx] / score) + else: + normed_scores.append(score) + normed[case] = (normed_scores, error_message) + + return normed + + + + + + + + +# Task configuration for benchmark task +ENTRY_NAME = 'solve' +FUNCTION_SIGNATURE = 'def solve(...):' +IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' +TASK_DESCRIPTION = '("The Traveling Salesman Problem (TSP) is a classic combinatorial optimization problem where, "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The Traveling Salesman Problem (TSP) is a classic combinatorial optimization problem where, "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(nodes: list) -> dict:\n """\n Solve a TSP instance.\n Args:\n - nodes (list): List of (x, y) coordinates representing cities in the TSP problem\n Format: [(x1, y1), (x2, y2), ..., (xn, yn)]\n Returns:\n dict: Solution information with:\n - \'tour\' (list): List of node indices representing the solution path\n Format: [0, 3, 1, ...] where numbers are indices into the nodes list\n """\n\n return {\n \'tour\': [],\n }' +EVAL_CLASS_NAME = 'TSPEvaluationCB' +EVAL_KWARGS = {'timeout_seconds': 60} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_travelling_salesman_problem/paras.yaml b/examples/benchmark_tasks/optimization_travelling_salesman_problem/paras.yaml new file mode 100644 index 00000000..ca7f008b --- /dev/null +++ b/examples/benchmark_tasks/optimization_travelling_salesman_problem/paras.yaml @@ -0,0 +1,2 @@ +name: TSPEvaluationCB +timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_tsp_construct/__init__.py b/examples/benchmark_tasks/optimization_tsp_construct/__init__.py new file mode 100644 index 00000000..87d91cad --- /dev/null +++ b/examples/benchmark_tasks/optimization_tsp_construct/__init__.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_tsp_construct +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# Module Name: TSPEvaluation +# Last Revision: 2025/2/16 +# Description: Evaluates the constructive heuristic for Traveling Salseman Problem (TSP). +# Given a set of locations, +# the goal is to find optimal route to travel all locations and back to start point +# while minimizing the total travel distance. +# This module is part of the LLM4AD project (https://github.com/Optima-CityU/llm4ad). +# +# Parameters: +# - timeout_seconds: Maximum allowed time (in seconds) for the evaluation process: int (default: 30). +# - n_instance: Number of problem instances to generate: int (default: 16). +# - problem_size: Number of customers to serve: int (default: 50). +# +# +# References: +# - Fei Liu, Xialiang Tong, Mingxuan Yuan, and Qingfu Zhang. +# "Algorithm Evolution using Large Language Model." arXiv preprint arXiv:2311.15249 (2023). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- +from __future__ import annotations + +from typing import Any +import numpy as np +from llm4ad_loader import Evaluation +import os, sys +sys.path.insert(0, os.path.dirname(__file__)) +from get_instance import GetData +# from llm4ad.task.optimization.tsp_construct.get_instance import GetData # Converted from LLM4AD import +# from llm4ad.task.optimization.tsp_construct.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\ndef select_next_node(current_node: int, destination_node: int, unvisited_nodes: np.ndarray, distance_matrix: np.ndarray) -> int: \n """\n Design a novel algorithm to select the next node in each step.\n\n Args:\n current_node: ID of the current node.\n destination_node: ID of the destination node.\n unvisited_nodes: Array of IDs of unvisited nodes.\n distance_matrix: Distance matrix of nodes.\n\n Return:\n ID of the next node to visit.\n """\n next_node = unvisited_nodes[0]\n\n return next_node' +task_description = '"Given a set of nodes with their coordinates, you need to find the shortest route that visits each node once and returns to the starting node. \\' + + +__all__ = ['TSPEvaluation'] + + +class TSPEvaluation(Evaluation): + """Evaluator for traveling salesman problem.""" + + def __init__(self, + timeout_seconds=30, + n_instance=16, + problem_size=50, + **kwargs): + + """ + Args: + None + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + self.n_instance = n_instance + self.problem_size = problem_size + getData = GetData(self.n_instance, self.problem_size) + self._datasets = getData.generate_instances() + + def evaluate_program(self, program_str: str, callable_func: callable) -> Any | None: + return self.evaluate(callable_func) + + def tour_cost(self, instance, solution, problem_size): + cost = 0 + for j in range(problem_size - 1): + cost += np.linalg.norm(instance[int(solution[j])] - instance[int(solution[j + 1])]) + cost += np.linalg.norm(instance[int(solution[-1])] - instance[int(solution[0])]) + return cost + + def generate_neighborhood_matrix(self, instance): + instance = np.array(instance) + n = len(instance) + neighborhood_matrix = np.zeros((n, n), dtype=int) + + for i in range(n): + distances = np.linalg.norm(instance[i] - instance, axis=1) + sorted_indices = np.argsort(distances) # sort indices based on distances + neighborhood_matrix[i] = sorted_indices + + return neighborhood_matrix + + def evaluate(self, eva: callable) -> float: + + n_max = self.n_instance + dis = np.ones(self.n_instance) + n_ins = 0 + + for instance, distance_matrix in self._datasets: + + # get neighborhood matrix + neighbor_matrix = self.generate_neighborhood_matrix(instance) + + destination_node = 0 + + current_node = 0 + + route = np.zeros(self.problem_size) + # print(">>> Step 0 : select node "+str(instance[0][0])+", "+str(instance[0][1])) + for i in range(1, self.problem_size - 1): + + near_nodes = neighbor_matrix[current_node][1:] + + mask = ~np.isin(near_nodes, route[:i]) + + unvisited_near_nodes = near_nodes[mask] + + next_node = eva(current_node, destination_node, unvisited_near_nodes, distance_matrix) + + if next_node in route: + # print("wrong algorithm select duplicate node, retrying ...") + return None + + current_node = next_node + + route[i] = current_node + + mask = ~np.isin(np.arange(self.problem_size), route[:self.problem_size - 1]) + + last_node = np.arange(self.problem_size)[mask] + + current_node = last_node[0] + + route[self.problem_size - 1] = current_node + + LLM_dis = self.tour_cost(instance, route, self.problem_size) + + dis[n_ins] = LLM_dis + + n_ins += 1 + if n_ins == self.n_instance: + break + # self.route_plot(instance,route,self.oracle[n_ins]) + + ave_dis = np.average(dis) + # print("average dis: ",ave_dis) + return -ave_dis + + +if __name__ == '__main__': + import sys + + print(sys.path) + + + def select_next_node(current_node: int, destination_node: int, unvisited_nodes: np.ndarray, distance_matrix: np.ndarray) -> int: + """ + Design a novel algorithm to select the next node in each step. + + Args: + current_node: ID of the current node. + destination_node: ID of the destination node. + unvisited_nodes: Array of IDs of unvisited nodes. + distance_matrix: Distance matrix of nodes. + + Return: + ID of the next node to visit. + """ + distances_to_destination = distance_matrix[current_node][unvisited_nodes] + + # Find the index of the unvisited node with the smallest distance to the destination + next_node_index = np.argmin(distances_to_destination) + + # Get the ID of the next node to visit + next_node = unvisited_nodes[next_node_index] + + return next_node + + + tsp = TSPEvaluation() + tsp.evaluate_program('_', select_next_node) + +# Task configuration for benchmark task +ENTRY_NAME = 'select_next_node' +FUNCTION_SIGNATURE = 'def select_next_node(...):' +IMPORT_HEADER = 'import numpy as np\nimport math' +TASK_DESCRIPTION = '"Given a set of nodes with their coordinates, you need to find the shortest route that visits each node once and returns to the starting node. \\' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `select_next_node` for the LLM4AD task.\\n\\nTask description:\\n"Given a set of nodes with their coordinates, you need to find the shortest route that visits each node once and returns to the starting node. \\\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\ndef select_next_node(current_node: int, destination_node: int, unvisited_nodes: np.ndarray, distance_matrix: np.ndarray) -> int: \n """\n Design a novel algorithm to select the next node in each step.\n\n Args:\n current_node: ID of the current node.\n destination_node: ID of the destination node.\n unvisited_nodes: Array of IDs of unvisited nodes.\n distance_matrix: Distance matrix of nodes.\n\n Return:\n ID of the next node to visit.\n """\n next_node = unvisited_nodes[0]\n\n return next_node' +EVAL_CLASS_NAME = 'TSPEvaluation' +EVAL_KWARGS = {'timeout_seconds': 20} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_tsp_construct/get_instance.py b/examples/benchmark_tasks/optimization_tsp_construct/get_instance.py new file mode 100644 index 00000000..4a08c38e --- /dev/null +++ b/examples/benchmark_tasks/optimization_tsp_construct/get_instance.py @@ -0,0 +1,16 @@ +import numpy as np + + +class GetData(): + def __init__(self, n_instance, n_cities): + self.n_instance = n_instance + self.n_cities = n_cities + + def generate_instances(self): + np.random.seed(2024) + instance_data = [] + for _ in range(self.n_instance): + coordinates = np.random.rand(self.n_cities, 2) + distances = np.linalg.norm(coordinates[:, np.newaxis] - coordinates, axis=2) + instance_data.append((coordinates, distances)) + return instance_data diff --git a/examples/benchmark_tasks/optimization_tsp_construct/paras.yaml b/examples/benchmark_tasks/optimization_tsp_construct/paras.yaml new file mode 100644 index 00000000..2aa2b88a --- /dev/null +++ b/examples/benchmark_tasks/optimization_tsp_construct/paras.yaml @@ -0,0 +1,2 @@ +name: TSPEvaluation +timeout_seconds: 20 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_tsp_gls_2O/__init__.py b/examples/benchmark_tasks/optimization_tsp_gls_2O/__init__.py new file mode 100644 index 00000000..1c046093 --- /dev/null +++ b/examples/benchmark_tasks/optimization_tsp_gls_2O/__init__.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_tsp_gls_2O +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# name: str: TSP_GLS_2O_Evaluation +# Parameters: +# timeout_seconds: int: 20 +# end +from __future__ import annotations + +from typing import Tuple, Any +import numpy as np +from llm4ad_loader import Evaluation +import os, sys +sys.path.insert(0, os.path.dirname(__file__)) +from get_instance import GetData, TSPInstance +# from llm4ad.task.optimization.tsp_gls_2O.get_instance import GetData, TSPInstance # Converted from LLM4AD import +# from llm4ad.task.optimization.tsp_gls_2O.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\ndef update_edge_distance(edge_distance: np.ndarray, local_opt_tour: np.ndarray, edge_n_used: np.ndarray) -> np.ndarray:\n """\n Design a novel algorithm to update the distance matrix.\n\n Args:\n edge_distance: A matrix of the distance.\n local_opt_tour: An array of the local optimal tour of IDs.\n edge_n_used: A matrix of the number of each edge used during permutation.\n\n Return:\n updated_edge_distance: A matrix of the updated distance.\n """\n updated_edge_distance = np.copy(edge_distance)\n\n # Calculate combined importance and frequency factor\n combined_factor = (1 / edge_n_used) + (1 / edge_n_used)\n\n for i in range(len(local_opt_tour) - 1):\n node1 = local_opt_tour[i]\n node2 = local_opt_tour[i + 1]\n\n update_factor = combined_factor[node1, node2]\n\n updated_edge_distance[node1, node2] += update_factor\n updated_edge_distance[node2, node1] = updated_edge_distance[node1, node2]\n\n return updated_edge_distance' +task_description = 'Given an edge distance matrix and a local optimal route, please help me design a strategy to update the distance matrix to avoid being trapped in the local optimum with the final goal of finding a tour with minimized distance. You should create a heuristic for me to update the edge distance matrix.' + +from .gls import guided_local_search_with_time + +__all__ = ['TSP_GLS_2O_Evaluation'] + +perturbation_moves = 5 +iter_limit = 1000 + + +def calculate_cost(inst: TSPInstance, path: np.ndarray) -> float: + # assert (np.sort(path) == np.arange(inst.n)).all(), 'Illegal path' + return inst.distmat[path, np.roll(path, 1)].sum().item() + +def solve_with_time(inst: TSPInstance, eva) -> Tuple[float, float]: + try: + result, running_time = guided_local_search_with_time(inst.distmat, inst.distmat.copy(), eva, perturbation_moves, iter_limit) + cost = calculate_cost(inst, result) + except Exception as e: + # cost, running_time = 1E10, 1E10 + cost, running_time = float("inf"), float("inf") + # print(result) + return cost, running_time + +def evaluate(instance_data,n_ins,prob_size, eva: callable) -> np.ndarray: + objs = np.zeros((n_ins, 2)) + + for i in range(n_ins): + obj = solve_with_time(instance_data[i], eva) + # print(f'{obj[0]}, {obj[1]}') + objs[i] = np.array(obj) + + obj = np.mean(objs, axis=0) + return -obj + + +class TSP_GLS_2O_Evaluation(Evaluation): + """Evaluator for traveling salesman problem.""" + + def __init__(self, **kwargs): + + """ + Args: + None + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=20 + ) + + self.n_instance = 16 + self.problem_size = 100 + getData = GetData(self.n_instance, self.problem_size) + self._datasets = getData.generate_instances() + + def evaluate_program(self, program_str: str, callable_func: callable) -> Any | None: + return evaluate(self._datasets,self.n_instance,self.problem_size, callable_func) + + +if __name__ == '__main__': + import numpy as np + + + def update_edge_distance(edge_distance: np.ndarray, local_opt_tour: np.ndarray, + edge_n_used: np.ndarray) -> np.ndarray: + """ + Design a novel algorithm to update the distance matrix. + + Args: + edge_distance: A matrix of the distance. + local_opt_tour: An array of the local optimal tour of IDs. + edge_n_used: A matrix of the number of each edge used during permutation. + + Return: + updated_edge_distance: A matrix of the updated distance. + """ + updated_edge_distance = np.copy(edge_distance) + + # Calculate combined importance and frequency factor + combined_factor = (1 / edge_n_used) + (1 / edge_n_used) + + for i in range(len(local_opt_tour) - 1): + node1 = local_opt_tour[i] + node2 = local_opt_tour[i + 1] + + update_factor = combined_factor[node1, node2] + + updated_edge_distance[node1, node2] += update_factor + updated_edge_distance[node2, node1] = updated_edge_distance[node1, node2] + + return updated_edge_distance + + tsp = TSP_GLS_2O_Evaluation() + tsp.evaluate_program('_', update_edge_distance) + +# Task configuration for benchmark task +ENTRY_NAME = 'update_edge_distance' +FUNCTION_SIGNATURE = 'def update_edge_distance(...):' +IMPORT_HEADER = 'import numpy as np\nimport math' +TASK_DESCRIPTION = 'Given an edge distance matrix and a local optimal route, please help me design a strategy to update the distance matrix to avoid being trapped in the local optimum with the final goal of finding a tour with minimized distance. You should create a heuristic for me to update the edge distance matrix.' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `update_edge_distance` for the LLM4AD task.\\n\\nTask description:\\nGiven an edge distance matrix and a local optimal route, please help me design a strategy to update the distance matrix to avoid being trapped in the local optimum with the final goal of finding a tour with minimized distance. You should create a heuristic for me to update the edge distance matrix.\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\ndef update_edge_distance(edge_distance: np.ndarray, local_opt_tour: np.ndarray, edge_n_used: np.ndarray) -> np.ndarray:\n """\n Design a novel algorithm to update the distance matrix.\n\n Args:\n edge_distance: A matrix of the distance.\n local_opt_tour: An array of the local optimal tour of IDs.\n edge_n_used: A matrix of the number of each edge used during permutation.\n\n Return:\n updated_edge_distance: A matrix of the updated distance.\n """\n updated_edge_distance = np.copy(edge_distance)\n\n # Calculate combined importance and frequency factor\n combined_factor = (1 / edge_n_used) + (1 / edge_n_used)\n\n for i in range(len(local_opt_tour) - 1):\n node1 = local_opt_tour[i]\n node2 = local_opt_tour[i + 1]\n\n update_factor = combined_factor[node1, node2]\n\n updated_edge_distance[node1, node2] += update_factor\n updated_edge_distance[node2, node1] = updated_edge_distance[node1, node2]\n\n return updated_edge_distance' +EVAL_CLASS_NAME = 'TSP_GLS_2O_Evaluation' +EVAL_KWARGS = {'timeout_seconds': 60} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_tsp_gls_2O/get_instance.py b/examples/benchmark_tasks/optimization_tsp_gls_2O/get_instance.py new file mode 100644 index 00000000..ee49dfbf --- /dev/null +++ b/examples/benchmark_tasks/optimization_tsp_gls_2O/get_instance.py @@ -0,0 +1,23 @@ +import numpy as np +import numpy.typing as npt +from scipy.spatial import distance_matrix + + +class GetData(): + def __init__(self,n_instance,n_cities): + self.n_instance = n_instance + self.n_cities = n_cities + + def generate_instances(self): + np.random.seed(2024) + instance_data = [] + for _ in range(self.n_instance): + coordinates = np.random.random((self.n_cities, 2)) + instance_data.append(TSPInstance(coordinates)) + return instance_data + +class TSPInstance: + def __init__(self, positions: npt.NDArray[np.float_]) -> None: + self.positions = positions + self.n = positions.shape[0] + self.distmat = distance_matrix(positions, positions) + np.eye(self.n)*1e-5 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_tsp_gls_2O/gls.py b/examples/benchmark_tasks/optimization_tsp_gls_2O/gls.py new file mode 100644 index 00000000..379a002f --- /dev/null +++ b/examples/benchmark_tasks/optimization_tsp_gls_2O/gls.py @@ -0,0 +1,226 @@ +import time + +import numpy as np +import numpy.typing as npt +import numba as nb +import concurrent.futures +from typing import Tuple + +FloatArray = npt.NDArray[np.float_] +IntArray = npt.NDArray[np.int_] +usecache = True + + +@nb.njit(nb.float32(nb.float32[:,:], nb.uint16[:], nb.uint16), nogil=True, cache = usecache) +def _two_opt_once(distmat, tour, fixed_i = 0): + '''in-place operation''' + n = tour.shape[0] + p = q = 0 + delta = 0 + for i in range(1, n - 1) if fixed_i==0 else range(fixed_i, fixed_i+1): + for j in range(i + 1, n): + node_i, node_j = tour[i], tour[j] + node_prev, node_next = tour[i-1], tour[(j+1) % n] + if node_prev == node_j or node_next == node_i: + continue + change = ( distmat[node_prev, node_j] + + distmat[node_i, node_next] + - distmat[node_prev, node_i] + - distmat[node_j, node_next]) + if change < delta: + p, q, delta = i, j, change + if delta < -1e-6: + tour[p: q+1] = np.flip(tour[p: q+1]) + return delta + else: + return 0.0 + +@nb.njit(nb.float32(nb.float32[:,:], nb.uint16[:], nb.uint16), nogil=True, cache = usecache) +def _relocate_once(distmat, tour, fixed_i = 0): + n = distmat.shape[0] + delta = p = q = 0 + for i in range(1, n) if fixed_i==0 else range(fixed_i, fixed_i+1): + node = tour[i] + prev_node = tour[i-1] + next_node = tour[(i+1)%n] + for j in range(n): + if j == i or j == i-1: + continue + prev_insert = tour[j] + next_insert = tour[(j+1)%n] + cost = ( - distmat[prev_node, node] + - distmat[node, next_node] + - distmat[prev_insert, next_insert] + + distmat[prev_insert, node] + + distmat[node, next_insert] + + distmat[prev_node, next_node] ) + if cost < delta: + delta, p, q = cost, i, j + if delta >= 0: + return 0.0 + if p 0: + delta = 0 + delta += _two_opt_once(distmat, cur_tour, fixed_i) + delta += _relocate_once(distmat, cur_tour, fixed_i) + count -= 1 + sum_delta += delta + return sum_delta + +def _perturbation(distmat, guide, penalty, cur_tour, update_edge_distance, perturbation_moves = 30): + # moves = 0 + n = distmat.shape[0] + # print('distmat', type(distmat), distmat.shape) + # print('cur_tour', type(cur_tour), cur_tour.shape) + # print('penalty', type(penalty), penalty.shape) + + edge_weight_guided = update_edge_distance(distmat, cur_tour, penalty) + edge_weight_guided = np.asmatrix(edge_weight_guided) + edge_weight_gap = edge_weight_guided - distmat + + for i in range(perturbation_moves): + # while moves < perturbation_moves: + # penalize edge + max_indices = np.argmin(-edge_weight_gap, axis=None) + rows, columns = np.unravel_index(max_indices, edge_weight_gap.shape) + penalty[rows, columns] += 1 + penalty[columns, rows] += 1 + edge_weight_gap[rows, columns] = 0 + edge_weight_gap[columns, rows] = 0 + for fixed_i in [rows, columns]: + if fixed_i == 0 or fixed_i + 1 == n: + continue + delta = _local_search(edge_weight_guided, cur_tour, fixed_i, 1) + # if delta < 0: + # moves += 1 + +@nb.njit(nb.uint16[:](nb.float32[:,:], nb.uint16), nogil=True, cache = usecache) +def _init_nearest_neighbor(distmat, start): + n = distmat.shape[0] + tour = np.zeros(n, dtype=np.uint16) + visited = np.zeros(n, dtype=np.bool_) + visited[start] = True + tour[0] = start + for i in range(1, n): + min_dist = np.inf + min_idx = -1 + for j in range(n): + if not visited[j] and distmat[tour[i-1], j] < min_dist: + min_dist = distmat[tour[i-1], j] + min_idx = j + tour[i] = min_idx + visited[min_idx] = True + return tour + + +def _guided_local_search( + distmat, guide, start, update_edge_distance, perturbation_moves = 30, iter_limit = 1000 +) -> npt.NDArray[np.uint16]: + penalty = np.zeros_like(distmat) + start_time = time.monotonic() + best_tour = _init_nearest_neighbor(distmat, start) + _local_search(distmat, best_tour, 0, 1000) + best_cost = _calculate_cost(distmat, best_tour) + # k = 0.1 * best_cost / distmat.shape[0] + cur_tour = best_tour.copy() + + for _ in range(iter_limit): + _perturbation(distmat, guide, penalty, cur_tour, update_edge_distance, perturbation_moves) + _local_search(distmat, cur_tour, 0, 1000) + cur_cost = _calculate_cost(distmat, cur_tour) + if cur_cost < best_cost: + best_tour, best_cost = cur_tour.copy(), cur_cost + if time.monotonic() - start_time > 60: + break + return best_tour + + +def _guided_local_search_with_time( + distmat, guide, start, update_edge_distance, perturbation_moves = 30, iter_limit = 1000 +) -> Tuple[npt.NDArray[np.uint16], float]: + penalty = np.zeros_like(distmat) + start_time = time.monotonic() + best_tour = _init_nearest_neighbor(distmat, start) + _local_search(distmat, best_tour, 0, 1000) + best_cost = _calculate_cost(distmat, best_tour) + # k = 0.1 * best_cost / distmat.shape[0] + cur_tour = best_tour.copy() + + for _ in range(iter_limit): + _perturbation(distmat, guide, penalty, cur_tour, update_edge_distance, perturbation_moves) + _local_search(distmat, cur_tour, 0, 1000) + cur_cost = _calculate_cost(distmat, cur_tour) + if cur_cost < best_cost: + best_tour, best_cost = cur_tour.copy(), cur_cost + running_time = time.monotonic() - start_time + return best_tour, running_time + +def guided_local_search( + distmat: FloatArray, + guide: FloatArray, + update_edge_distance, + perturbation_moves: int = 30, + iter_limit: int = 1000 +) -> npt.NDArray[np.uint16]: + return _guided_local_search( + distmat.astype(np.float32), + guide.astype(np.float32), + 0, + update_edge_distance, + perturbation_moves=perturbation_moves, + iter_limit=iter_limit, + ) + +def guided_local_search_with_time( + distmat: FloatArray, + guide: FloatArray, + update_edge_distance, + perturbation_moves: int = 30, + iter_limit: int = 1000 +) -> Tuple[npt.NDArray[np.uint16], float]: + return _guided_local_search_with_time( + distmat.astype(np.float32), + guide.astype(np.float32), + 0, + update_edge_distance, + perturbation_moves=perturbation_moves, + iter_limit=iter_limit, + ) + +def multi_start_guided_local_search( + dist: FloatArray, + guide: FloatArray, + n_starts: int = 10, + perturbation_moves = 30, + iter_limit = 1000 +): + dist = dist.astype(np.float32) + guide = guide.astype(np.float32) + start_nodes = np.arange(n_starts).astype(np.uint16) + + with concurrent.futures.ThreadPoolExecutor() as executor: + futures = [] + for start in start_nodes: + future = executor.submit(_guided_local_search, dist, guide, start, perturbation_moves = perturbation_moves, iter_limit = iter_limit) + futures.append(future) + tours = [f.result() for f in futures] + # Calculate costs and return the best tour + costs = np.array([_calculate_cost(dist, tour) for tour in tours]) + best_tour = tours[np.argmin(costs)] + return best_tour \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_tsp_gls_2O/paras.yaml b/examples/benchmark_tasks/optimization_tsp_gls_2O/paras.yaml new file mode 100644 index 00000000..6c0c111d --- /dev/null +++ b/examples/benchmark_tasks/optimization_tsp_gls_2O/paras.yaml @@ -0,0 +1,2 @@ +name: TSP_GLS_2O_Evaluation +timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_uncapacitated_warehouse_location/__init__.py b/examples/benchmark_tasks/optimization_uncapacitated_warehouse_location/__init__.py new file mode 100644 index 00000000..53d40a53 --- /dev/null +++ b/examples/benchmark_tasks/optimization_uncapacitated_warehouse_location/__init__.py @@ -0,0 +1,349 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_uncapacitated_warehouse_location +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# References: +# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language +# model agents in algorithm search for combinatorial optimization. +# arXiv preprint arXiv:2504.04310 (2025). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any +import numpy as np +from llm4ad_loader import Evaluation +from llm4ad_loader import load_subdir_as_text +# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader +# from llm4ad.task.optimization.co_bench.uncapacitated_warehouse_location_co_bench.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(m: int, n: int, warehouses: list, customers: list) -> dict:\n """\n Solves the Uncapacitated Warehouse Location Problem.\n Input kwargs:\n - m: Number of potential warehouses (int)\n - n: Number of customers (int)\n - warehouses: A list of dictionaries, each with keys:\n \'fixed_cost\': Fixed cost for opening the warehouse.\n - customers: A list of dictionaries, each with keys:\n \'costs\': A list of floats representing the cost of assigning the entire customer to each warehouse.\n Evaluation Metric:\n The objective is to minimize the total cost, computed as:\n (Sum of fixed costs for all open warehouses)\n + (Sum of assignment costs for each customer assigned to a warehouse)\n Each customer must be assigned entirely to exactly one open warehouse.\n If a solution violates this constraint (i.e., a customer is unassigned or is assigned to more than one warehouse), then the solution is considered infeasible and no score is provided.\n Returns:\n A dictionary with the following keys:\n \'total_cost\': (float) The computed objective value (cost) if the solution is feasible; otherwise, no score is provided.\n \'warehouse_open\': (list of int) A list of m integers (0 or 1) indicating whether each warehouse is closed or open.\n \'assignments\': (list of list of int) A 2D list (n x m) where each entry is 1 if customer i is assigned to warehouse j, and 0 otherwise.\n """\n ## placeholder. You do not need to write anything here.\n return {\n "total_cost": 0.0,\n "warehouse_open": [0] * kwargs["m"],\n "assignments": [[0] * kwargs["m"] for _ in range(kwargs["n"])]\n }' +task_description = '("The Uncapacitated Warehouse Location Problem aims to determine which warehouses to open and how "' + + +__all__ = ['UWLEvaluationCB'] + + +class UWLEvaluationCB(Evaluation): + + def __init__(self, + timeout_seconds=50, + **kwargs): + + """ + Args: + None + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + # Load datasets from Hugging Face + dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Uncapacitated warehouse location") + self._datasets = {} + for filename in dataset: + # Join all text rows into a single string + text_content = '\n'.join([row['text'] for row in dataset[filename]]) + self._datasets[filename] = text_content + + def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: + return self.evaluate(callable_func) + + def evaluate(self, eva: callable) -> float | None: + ins_cases = [] + for case_id, ins in enumerate(self._datasets.values()): + ins_cases.append(self.load_data(ins)) + + fitness_list = [] + try: + for i in ins_cases: + for j in i: + result = eva(j['m'], j['n'], j['warehouses'], j['customers']) + fitness = self.eval_func(j['m'], j['n'], j['warehouses'], j['customers'], result['warehouse_open'], result['assignments']) + fitness_list.append(fitness) + + return -np.mean(fitness_list) + + except ValueError as e: + print(e) + return None + + def load_data(self, input_string): + """ + Reads one or more problem cases from the input file. + Expected Input File Format for each case: + Line 1: Two integers: m n + Next m lines: Each line contains two numbers: capacity fixed_cost for a warehouse. + Next n lines: Each line contains: demand (a number) followed by m numbers representing the cost of + allocating the customer's demand to each warehouse. + If the input file contains multiple cases, the cases appear sequentially in the file. + Returns: + A list of dictionaries, each corresponding to one case. Each dictionary has the keys: + - 'm': Number of potential warehouses (int) + - 'n': Number of customers (int) + - 'warehouses': List of dictionaries; each with keys 'capacity' and 'fixed_cost' + - 'customers': List of dictionaries; each with keys 'demand' and 'costs' (list of floats) + """ + try: + all_lines = [line.strip() for line in input_string.split('\n')] + except Exception as e: + raise ValueError("Error reading input file: " + str(e)) + + # Tokenize all non-empty lines. + tokens = [] + for line in all_lines: + line = line.strip() + if line: + tokens.extend(line.split()) + + cases = [] + index = 0 + total_tokens = len(tokens) + + # Process tokens until we have exhausted them. + while index < total_tokens: + if index + 1 >= total_tokens: + raise ValueError("Insufficient tokens to read m and n for a case.") + try: + m = int(tokens[index]) + n = int(tokens[index + 1]) + except Exception as e: + raise ValueError("Error parsing m or n: " + str(e)) + index += 2 + + # Parse warehouse data (m warehouses, each with 2 tokens). + expected_warehouse_tokens = m * 2 + if index + expected_warehouse_tokens - 1 >= total_tokens: + raise ValueError("Not enough tokens for warehouse data in a case.") + warehouses = [] + for i in range(m): + try: + capacity = float(tokens[index]) + fixed_cost = float(tokens[index + 1]) + except Exception as e: + raise ValueError("Error parsing warehouse data: " + str(e)) + warehouses.append({'capacity': capacity, 'fixed_cost': fixed_cost}) + index += 2 + + # Parse customer data (n customers, each with 1 demand and m cost values). + customers = [] + for j in range(n): + if index >= total_tokens: + raise ValueError(f"Not enough tokens for customer {j + 1} demand.") + try: + demand = float(tokens[index]) + except Exception as e: + raise ValueError(f"Error parsing demand for customer {j + 1}: " + str(e)) + index += 1 + if index + m - 1 >= total_tokens: + raise ValueError(f"Not enough tokens for cost data for customer {j + 1}.") + costs = [] + for i in range(m): + try: + cost = float(tokens[index]) + except Exception as e: + raise ValueError(f"Error parsing cost for customer {j + 1}, warehouse {i + 1}: " + str(e)) + costs.append(cost) + index += 1 + customers.append({'demand': demand, 'costs': costs}) + + case_data = {"m": m, "n": n, "warehouses": warehouses, "customers": customers} + cases.append(case_data) + + return cases + + def eval_func(self, m, n, warehouses, customers, warehouse_open, assignments, **kwargs): + """ + Evaluates the solution for the Uncapacitated Warehouse Location Problem. + For each customer: + - The customer must be assigned to exactly one open warehouse. + - The assignment cost is the cost associated with the warehouse to which the customer is assigned. + - No assignment is allowed for a warehouse that is closed. + The total cost is computed as: + (Sum of fixed costs for all open warehouses) + + (Sum of assignment costs for all customers) + Input Parameters: + - m: Number of potential warehouses (int) + - n: Number of customers (int) + - warehouses: List of dictionaries, each with keys: + 'fixed_cost': The fixed cost for opening the warehouse. + 'capacity': Provided but ignored in this problem. + - customers: List of dictionaries, each with keys: + 'costs': A list of floats representing the cost of assigning the customer entirely to each warehouse. + 'demand': Provided but ignored in this problem. + - warehouse_open: List of m integers (0 or 1) indicating whether each warehouse is closed or open. + - assignments: List of n lists (each of length m) where assignments[j][i] is 1 if customer j is assigned to warehouse i, and 0 otherwise. + - kwargs: Other parameters (not used here). + Returns: + A floating-point number representing the total cost if the solution is feasible. + Raises: + Exception: If any of the following conditions are violated: + - The sum of assignments for any customer is not exactly 1. + - Any positive assignment is made to a closed warehouse. + - Any assignment value is not binary (0 or 1). + """ + computed_total_cost = 0.0 + + # Add fixed costs for open warehouses. + for i in range(m): + if warehouse_open[i] == 1: + computed_total_cost += warehouses[i]['fixed_cost'] + + # Evaluate assignment cost for each customer. + for j in range(n): + # Sum of assignments for customer j should be exactly 1. + assigned_sum = sum(assignments[j]) + if abs(assigned_sum - 1.0) > 1e-6: + raise Exception( + f"Customer {j} assignment violation: total assigned value {assigned_sum} does not equal 1." + ) + + customer_cost = 0.0 + for i in range(m): + allocation = assignments[j][i] + # Ensure the assignment is binary (allowing for small floating point tolerance) + if not (abs(allocation) < 1e-6 or abs(allocation - 1.0) < 1e-6): + raise Exception( + f"Customer {j} has a non-binary assignment value {allocation} for warehouse {i + 1}." + ) + if allocation > 0: + if warehouse_open[i] != 1: + raise Exception( + f"Customer {j} is assigned to warehouse {i + 1}, which is closed." + ) + # Since assignment is binary, add the corresponding cost. + customer_cost += customers[j]['costs'][i] + computed_total_cost += customer_cost + + return computed_total_cost + + def norm_score(self, results): + optimal_scores = { + "cap71.txt": [932615.750], + "cap72.txt": [977799.400], + "cap73.txt": [1010641.450], + "cap74.txt": [1034976.975], + "cap101.txt": [796648.437], + "cap102.txt": [854704.200], + "cap103.txt": [893782.112], + "cap104.txt": [928941.750], + "cap131.txt": [793439.562], + "cap132.txt": [851495.325], + "cap133.txt": [893076.712], + "cap134.txt": [928941.750], + "capa.txt": [17156454.478], + "capb.txt": [12979071.582], + "capc.txt": [11505594.329] + } + + normed = {} + for case, (scores, error_message) in results.items(): + if case not in optimal_scores: + continue # Skip if there's no optimal score defined. + optimal_list = optimal_scores[case] + normed_scores = [] + # Compute normalized score for each index. + for idx, score in enumerate(scores): + if isinstance(score, (int, float)): + normed_scores.append(optimal_list[idx] / score) + else: + normed_scores.append(score) + normed[case] = (normed_scores, error_message) + + return normed + + def get_dev(self): + dev = {'cap101.txt': [], 'cap103.txt': [], + 'cap131.txt': [], + 'cap133.txt': [], + 'cap71.txt': [], 'cap73.txt': [], + 'capb.txt': []} + + return dev + + +# Task configuration for benchmark task +ENTRY_NAME = 'solve' +FUNCTION_SIGNATURE = 'def solve(...):' +IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' +TASK_DESCRIPTION = '("The Uncapacitated Warehouse Location Problem aims to determine which warehouses to open and how "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The Uncapacitated Warehouse Location Problem aims to determine which warehouses to open and how "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(m: int, n: int, warehouses: list, customers: list) -> dict:\n """\n Solves the Uncapacitated Warehouse Location Problem.\n Input kwargs:\n - m: Number of potential warehouses (int)\n - n: Number of customers (int)\n - warehouses: A list of dictionaries, each with keys:\n \'fixed_cost\': Fixed cost for opening the warehouse.\n - customers: A list of dictionaries, each with keys:\n \'costs\': A list of floats representing the cost of assigning the entire customer to each warehouse.\n Evaluation Metric:\n The objective is to minimize the total cost, computed as:\n (Sum of fixed costs for all open warehouses)\n + (Sum of assignment costs for each customer assigned to a warehouse)\n Each customer must be assigned entirely to exactly one open warehouse.\n If a solution violates this constraint (i.e., a customer is unassigned or is assigned to more than one warehouse), then the solution is considered infeasible and no score is provided.\n Returns:\n A dictionary with the following keys:\n \'total_cost\': (float) The computed objective value (cost) if the solution is feasible; otherwise, no score is provided.\n \'warehouse_open\': (list of int) A list of m integers (0 or 1) indicating whether each warehouse is closed or open.\n \'assignments\': (list of list of int) A 2D list (n x m) where each entry is 1 if customer i is assigned to warehouse j, and 0 otherwise.\n """\n ## placeholder. You do not need to write anything here.\n return {\n "total_cost": 0.0,\n "warehouse_open": [0] * kwargs["m"],\n "assignments": [[0] * kwargs["m"] for _ in range(kwargs["n"])]\n }' +EVAL_CLASS_NAME = 'UWLEvaluationCB' +EVAL_KWARGS = {'timeout_seconds': 60} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_uncapacitated_warehouse_location/paras.yaml b/examples/benchmark_tasks/optimization_uncapacitated_warehouse_location/paras.yaml new file mode 100644 index 00000000..b39dfbce --- /dev/null +++ b/examples/benchmark_tasks/optimization_uncapacitated_warehouse_location/paras.yaml @@ -0,0 +1,2 @@ +name: UWLEvaluationCB +timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_unconstrained_guillotine_cutting/__init__.py b/examples/benchmark_tasks/optimization_unconstrained_guillotine_cutting/__init__.py new file mode 100644 index 00000000..6db09c3b --- /dev/null +++ b/examples/benchmark_tasks/optimization_unconstrained_guillotine_cutting/__init__.py @@ -0,0 +1,376 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_unconstrained_guillotine_cutting +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# References: +# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language +# model agents in algorithm search for combinatorial optimization. +# arXiv preprint arXiv:2504.04310 (2025). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any +import numpy as np +from llm4ad_loader import Evaluation +from llm4ad_loader import load_subdir_as_text +# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader +# from llm4ad.task.optimization.co_bench.unconstrained_guillotine_cutting_co_bench.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(m: int, stock_width: int, stock_height: int, pieces: dict, allow_rotation: bool = False) -> dict:\n """\n Solves the unconstrained guillotine cutting problem.\n Given a stock rectangle (with dimensions \'stock_width\' and \'stock_height\') and a set of pieces\n (provided as a dictionary \'pieces\' mapping each piece_id to its specification {\'l\', \'w\', \'value\'}),\n the goal is to select and place some pieces (each used at most once) within the stock rectangle.\n If the keyword argument \'allow_rotation\' is True, each piece may be placed in its original orientation or rotated 90° (swapping its dimensions);\n otherwise, pieces must be placed in their original orientation. In all cases, placements must not overlap and must lie entirely within the stock.\n Input kwargs:\n - m (int): Number of available pieces.\n - stock_width (int): The width of the stock rectangle.\n - stock_height (int): The height of the stock rectangle.\n - pieces (dict): A dictionary mapping piece_id (1-indexed) to a dict with keys:\n \'l\' (length), \'w\' (width), and \'value\' (value of the piece).\n - allow_rotation (bool): Indicates whether a piece is allowed to be rotated 90°.\n Evaluation metric:\n The performance is measured as the total value of the placed pieces (sum of individual values).\n Returns:\n A dictionary with a key "placements" whose value is a list.\n Each element in the list is a dictionary representing a placement with keys:\n - piece_id (int): Identifier of the placed piece.\n - x (int): x-coordinate of the bottom-left corner in the stock rectangle.\n - y (int): y-coordinate of the bottom-left corner in the stock rectangle.\n - orientation (int): 0 for original orientation; 1 if rotated 90° (only applicable if allow_rotation is True, otherwise default to 0).\n NOTE: This is a placeholder function. Replace the body with an actual algorithm if desired.\n """\n ## placeholder. You do not need to write anything here.\n return {"placements": []}' +task_description = '("The unconstrained guillotine cutting problem involves selecting and placing a subset of "' + + +__all__ = ['UGCEvaluationCB'] + + +class UGCEvaluationCB(Evaluation): + + def __init__(self, + timeout_seconds=50, + **kwargs): + + """ + Args: + None + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + # Load datasets from Hugging Face + dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Unconstrained guillotine cutting") + self._datasets = {} + for filename in dataset: + # Join all text rows into a single string + text_content = '\n'.join([row['text'] for row in dataset[filename]]) + self._datasets[filename] = text_content + + def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: + return self.evaluate(callable_func) + + def evaluate(self, eva: callable) -> float | None: + ins_cases = [] + for case_id, ins in enumerate(self._datasets.values()): + ins_cases.append(self.load_data(ins)) + + fitness_list = [] + try: + for i in ins_cases: + for j in i: + result = eva(j['m'], j['stock_width'], j['stock_height'], j['pieces'], j['allow_rotation']) + fitness = self.eval_func(m=j['m'], stock_width=j['stock_width'], stock_height=j['stock_height'], pieces=j['pieces'], placements=result['placements']) + fitness_list.append(fitness) + + return np.mean(fitness_list) # itself is a maximize problem + + except ValueError as e: + print(e) + return None + + def load_data(self, input_string): + """ + Loads one or more problem cases from the input file. + The input is expected to contain one or more cases. + Each case has the following format: + - Line 1: An integer m (number of pieces). + - Line 2: Two integers: stock_width and stock_height. + - Next m lines: Each line contains three space-separated integers: l, w, value. + Cases are concatenated one after the other (ignoring blank lines). + Parameters: + input_path (str): Path to the input file. + Returns: + list: A list of dictionaries. Each dictionary corresponds to one case and contains: + - "m" (int): number of pieces. + - "stock_width" (int): width of the stock rectangle. + - "stock_height" (int): height of the stock rectangle. + - "pieces" (dict): mapping from piece_id (1-indexed) to a dict with keys 'l', 'w', 'value'. + """ + lines = [line.strip() for line in input_string.split('\n') if line.strip() != ''] + + cases = [] + idx = 0 + total_lines = len(lines) + while idx < total_lines: + # Read the number of pieces for the current case. + try: + m = int(lines[idx]) + except Exception: + raise ValueError(f"Invalid number of pieces at line {idx + 1}") + idx += 1 + + if idx >= total_lines: + raise ValueError("Missing stock dimensions for a case.") + + # Read stock rectangle dimensions. + stock_parts = lines[idx].split() + if len(stock_parts) != 2: + raise ValueError(f"Stock dimensions must consist of two integers at line {idx + 1}") + try: + stock_width, stock_height = map(int, stock_parts) + except Exception: + raise ValueError(f"Stock dimensions must be integers at line {idx + 1}") + idx += 1 + + # Read m piece specifications. + pieces = {} + for i in range(m): + if idx >= total_lines: + raise ValueError(f"Not enough piece specifications for case starting at line {idx + 1}") + parts = lines[idx].split() + if len(parts) < 3: + raise ValueError(f"Piece {i + 1} specification is incomplete at line {idx + 1}") + try: + l, w, value = map(int, parts[:3]) + except Exception: + raise ValueError(f"Piece {i + 1} contains non-integer data at line {idx + 1}") + pieces[i + 1] = {'l': l, 'w': w, 'value': value} + idx += 1 + + case = { + "m": m, + "stock_width": stock_width, + "stock_height": stock_height, + "pieces": pieces, + "allow_rotation": False, # Default value since we can't determine from string + } + cases.append(case) + + return cases + + def eval_func(self, **kwargs): + """ + Evaluates a candidate solution for the guillotine cutting problem. + This function computes the total value of the placed pieces while enforcing + the following constraints by raising errors when violated: + 1. Each placement must be entirely within the stock rectangle. + 2. Placements must not overlap. + 3. Each piece may be used at most once. + 4. Each placement must have a valid orientation (0 or 1). + Parameters (passed as keyword arguments): + - m (int): Number of pieces. + - stock_width (int): Width of the stock rectangle. + - stock_height (int): Height of the stock rectangle. + - pieces (dict): Dictionary mapping piece_id to {'l', 'w', 'value'}. + - placements (list): List of placements, where each placement is a dict with keys: + 'piece_id', 'x', 'y', 'orientation'. + Returns: + float: Total value of the placed pieces if all constraints are met. + Raises: + ValueError: If any of the constraints (format, boundary, overlap, duplicate usage, or orientation) + are violated. + """ + try: + m = kwargs["m"] + stock_width = kwargs["stock_width"] + stock_height = kwargs["stock_height"] + pieces = kwargs["pieces"] + placements = kwargs.get("placements", []) + except KeyError as e: + raise ValueError(f"Missing required input parameter: {e}") + + total_value = 0.0 + used_piece_ids = set() + rects = [] + + # Process each placement. + for placement in placements: + try: + piece_id = int(placement["piece_id"]) + x = int(placement["x"]) + y = int(placement["y"]) + orientation = int(placement["orientation"]) + except Exception as e: + raise ValueError(f"Invalid placement format: {placement}. Error: {e}") + + if piece_id not in pieces: + raise ValueError(f"Piece id {piece_id} not found in pieces.") + + # Check for duplicate usage. + if piece_id in used_piece_ids: + raise ValueError(f"Duplicate usage of piece id {piece_id}.") + used_piece_ids.add(piece_id) + + # Check orientation. + if orientation not in (0, 1): + raise ValueError(f"Invalid orientation {orientation} for piece id {piece_id}; must be 0 or 1.") + + # Determine effective dimensions based on orientation. + if orientation == 0: + p_width = pieces[piece_id]['l'] + p_height = pieces[piece_id]['w'] + else: + p_width = pieces[piece_id]['w'] + p_height = pieces[piece_id]['l'] + + # Check boundaries. + if x < 0 or y < 0 or (x + p_width) > stock_width or (y + p_height) > stock_height: + raise ValueError(f"Placement of piece id {piece_id} is out of stock boundaries.") + + total_value += pieces[piece_id]['value'] + + # Record rectangle for later overlap checks. + rects.append({ + "x": x, + "y": y, + "width": p_width, + "height": p_height + }) + + # Helper function to compute overlapping area between two rectangles. + def overlap_area(r1, r2): + x_overlap = max(0, min(r1["x"] + r1["width"], r2["x"] + r2["width"]) - max(r1["x"], r2["x"])) + y_overlap = max(0, min(r1["y"] + r1["height"], r2["y"] + r2["height"]) - max(r1["y"], r2["y"])) + return x_overlap * y_overlap + + # Check for overlapping pieces. + n_rects = len(rects) + for i in range(n_rects): + for j in range(i + 1, n_rects): + if overlap_area(rects[i], rects[j]) > 0: + raise ValueError("Overlapping detected between placements.") + + return total_value + + def norm_score(self, results): + optimal_scores = { + "gcut1.txt": [56460], + "gcut2.txt": [60536], + "gcut3.txt": [61036], + "gcut4.txt": [61698], + "gcut5.txt": [246000], + "gcut6.txt": [238998], + "gcut7.txt": [242567], + "gcut8.txt": [246633], + "gcut9.txt": [971100], + "gcut10.txt": [982025], + "gcut11.txt": [980096], + "gcut12.txt": [979986], + "gcut13.txt": [8997780], + "gcut1r.txt": [58136], + "gcut2r.txt": [60611], + "gcut3r.txt": [61626], + "gcut4r.txt": [62265], + "gcut5r.txt": [246000], + "gcut6r.txt": [240951], + "gcut7r.txt": [245866], + "gcut8r.txt": [247787], + "gcut9r.txt": [971100], + "gcut10r.txt": [982025], + "gcut11r.txt": [980096], + "gcut12r.txt": [988694], + "gcut13r.txt": [9000000], + } + + normed = {} + for case, (scores, error_message) in results.items(): + if case not in optimal_scores: + continue # Skip if there's no optimal score defined. + optimal_list = optimal_scores[case] + normed_scores = [] + # Compute normalized score for each index. + for idx, score in enumerate(scores): + if isinstance(score, (int, float)): + normed_scores.append(score / optimal_list[idx]) + else: + normed_scores.append(score) + normed[case] = (normed_scores, error_message) + + return normed + + def get_dev(self): + dev = {'gcut1.txt': [], 'gcut10r.txt': [], 'gcut11.txt': [], + 'gcut12r.txt': [], 'gcut13.txt': [], 'gcut2r.txt': [], + 'gcut3.txt': [], 'gcut4r.txt': [], 'gcut5.txt': [], + 'gcut6r.txt': [], 'gcut7r.txt': [], 'gcut8r.txt': [], + 'gcut9.txt': [], } + + return dev + + + +# Task configuration for benchmark task +ENTRY_NAME = 'solve' +FUNCTION_SIGNATURE = 'def solve(...):' +IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' +TASK_DESCRIPTION = '("The unconstrained guillotine cutting problem involves selecting and placing a subset of "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The unconstrained guillotine cutting problem involves selecting and placing a subset of "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(m: int, stock_width: int, stock_height: int, pieces: dict, allow_rotation: bool = False) -> dict:\n """\n Solves the unconstrained guillotine cutting problem.\n Given a stock rectangle (with dimensions \'stock_width\' and \'stock_height\') and a set of pieces\n (provided as a dictionary \'pieces\' mapping each piece_id to its specification {\'l\', \'w\', \'value\'}),\n the goal is to select and place some pieces (each used at most once) within the stock rectangle.\n If the keyword argument \'allow_rotation\' is True, each piece may be placed in its original orientation or rotated 90° (swapping its dimensions);\n otherwise, pieces must be placed in their original orientation. In all cases, placements must not overlap and must lie entirely within the stock.\n Input kwargs:\n - m (int): Number of available pieces.\n - stock_width (int): The width of the stock rectangle.\n - stock_height (int): The height of the stock rectangle.\n - pieces (dict): A dictionary mapping piece_id (1-indexed) to a dict with keys:\n \'l\' (length), \'w\' (width), and \'value\' (value of the piece).\n - allow_rotation (bool): Indicates whether a piece is allowed to be rotated 90°.\n Evaluation metric:\n The performance is measured as the total value of the placed pieces (sum of individual values).\n Returns:\n A dictionary with a key "placements" whose value is a list.\n Each element in the list is a dictionary representing a placement with keys:\n - piece_id (int): Identifier of the placed piece.\n - x (int): x-coordinate of the bottom-left corner in the stock rectangle.\n - y (int): y-coordinate of the bottom-left corner in the stock rectangle.\n - orientation (int): 0 for original orientation; 1 if rotated 90° (only applicable if allow_rotation is True, otherwise default to 0).\n NOTE: This is a placeholder function. Replace the body with an actual algorithm if desired.\n """\n ## placeholder. You do not need to write anything here.\n return {"placements": []}' +EVAL_CLASS_NAME = 'UGCEvaluationCB' +EVAL_KWARGS = {'timeout_seconds': 300} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_unconstrained_guillotine_cutting/paras.yaml b/examples/benchmark_tasks/optimization_unconstrained_guillotine_cutting/paras.yaml new file mode 100644 index 00000000..0f17f3c5 --- /dev/null +++ b/examples/benchmark_tasks/optimization_unconstrained_guillotine_cutting/paras.yaml @@ -0,0 +1,2 @@ +name: UGCEvaluationCB +timeout_seconds: 300 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_vehicle_routing_period_routing/__init__.py b/examples/benchmark_tasks/optimization_vehicle_routing_period_routing/__init__.py new file mode 100644 index 00000000..b9ebda77 --- /dev/null +++ b/examples/benchmark_tasks/optimization_vehicle_routing_period_routing/__init__.py @@ -0,0 +1,469 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_vehicle_routing_period_routing +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# References: +# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language +# model agents in algorithm search for combinatorial optimization. +# arXiv preprint arXiv:2504.04310 (2025). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +import os, sys +sys.path.insert(0, os.path.dirname(__file__)) +import ast +from typing import Any +import numpy as np +from llm4ad_loader import Evaluation +from llm4ad_loader import load_subdir_as_text +# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader +# from llm4ad.task.optimization.co_bench.vehicle_routing_period_routing_co_bench.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(depot: dict, customers: list, vehicles_per_day: list, vehicle_capacity: float, period_length: int) -> dict:\n """\n Solves an instance of the Period Vehicle Routing Problem.\n Input kwargs includes:\n - depot: dict with keys:\n "id": int, always 0.\n "x": float, the x-coordinate.\n "y": float, the y-coordinate.\n - customers: list of dictionaries (with customer id ≠ 0) having keys:\n "id": int, the customer id.\n "x": float, the x-coordinate.\n "y": float, the y-coordinate.\n "demand": numeric, the customer demand.\n "schedules": list of candidate schedules, each a list (of length period_length) with binary entries.\n - vehicles_per_day: list of ints (length period_length) indicating the number of vehicles available each day.\n - vehicle_capacity: numeric, the capacity of each vehicle.\n - period_length: int, the number of days in the planning period.\n The solution must decide:\n 1. Which service schedule (from the candidate schedules) is selected for each customer.\n 2. For each day (days are 1-indexed), the daily tours: a list of tours—one per available vehicle.\n Each tour is a continuous route that starts at the depot (0), visits some customers (each exactly once),\n and returns to the depot. The depot may only appear as the first and last vertex in each tour.\n The number of tours for day d must be exactly equal to vehicles_per_day[d-1].\n The returned solution is a dictionary containing:\n - "selected_schedules": dict mapping each customer id (integer) to the chosen schedule (a list of binary integers).\n - "tours": dict mapping day (an integer between 1 and period_length) to a list of tours.\n Each tour is a list of vertex ids (integers), starting and ending at the depot (id 0).\n """\n # ------------------------------\n\n return {\n "selected_schedules": ...,\n "tours": ...\n }' +task_description = '("The Period Vehicle Routing Problem requires planning delivery routes over a multi‐day planning "' + + +__all__ = ['VRPREvaluationCB'] + + +class VRPREvaluationCB(Evaluation): + + def __init__(self, + timeout_seconds=50, + **kwargs): + + """ + Args: + None + Raises: + AttributeError: If the data key does not exist. + FileNotFoundError: If the specified data file is not found. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + # Load datasets from Hugging Face + dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Vehicle routing: period routing") + self._datasets = {} + for filename in dataset: + # Join all text rows into a single string + text_content = '\n'.join([row['text'] for row in dataset[filename]]) + self._datasets[filename] = text_content + + def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: + return self.evaluate(callable_func) + + def evaluate(self, eva: callable) -> float | None: + ins_cases = [] + for case_id, ins in enumerate(self._datasets.values()): + ins_cases.append(self.load_data(ins)) + + fitness_list = [] + try: + for i in ins_cases: + for j in i: + result = eva(j['depot'], j['costumers'], j['vehicles_per_day'], j['vehicle_capacity'], j['period_length']) + fitness = self.eval_func(depot=j['depot'], customers=j['costumers'], vehicles_per_day=j['vehicles_per_day'], vehicle_capacity=j['vehicle_capacity'], period_length=j['period_length'], selected_schedules=result['selected_schedules'], tours=result['tours']) + fitness_list.append(fitness) + + return -np.mean(fitness_list) + + except ValueError as e: + print(e) + return None + + def load_data(self, input_string): + """ + Reads a period vehicle routing problem file and returns a dictionary with the problem data. + The file is expected to have the following format: + Line 1: Two integers: + (Note: the depot is specified as customer_id = 0.) + Line 2: A list of period_length integers representing the number of vehicles on each day. + Line 3: A single number representing the constant capacity of every vehicle. + Lines 4 onward: Each line represents a vertex (depot or customer) in the format: + customer_id x_coordinate y_coordinate demand possible_schedule_list + For the depot (customer_id = 0) the demand and schedule are omitted or ignored. + e.g., depot line: 0 30 40 0 + customer line: 1 37 52 7 [[1, 0], [0, 1]] + Parameters: + input_string (str): The input content as string. + Returns: + A dictionary with keys: + - "period_length" (int) + - "vehicles_per_day" (list of ints) + - "vehicle_capacity" (number) + - "depot": dict with keys: "id", "x", "y" + - "customers": list of customer dictionaries (for customer id ≠ 0) + Each customer dictionary contains keys: + "id": int, the customer id. + "x": float, the x coordinate. + "y": float, the y coordinate. + "demand": float, the customer demand. + "schedules": list of lists, each sub-list is a binary schedule for the period. + """ + + # Read file and filter out any empty lines. + all_lines = [line.strip() for line in input_string.split('\n')] + + # Check that we have at least 3 lines for headers. + if len(all_lines) < 3: + raise ValueError("Insufficient data in the file. Expect at least three header lines.") + + # Parse header + # First line: number of customers and period length: + header1 = all_lines[0].split() + if len(header1) != 2: + print(header1) + raise ValueError("The first line must have exactly 2 tokens: .") + try: + num_customers = int(header1[0]) + period_length = int(header1[1]) + except Exception as e: + raise ValueError("Error parsing the number of customers or period length.") from e + + # Second line: number of vehicles on each day + vehicles_tokens = all_lines[1].split() + if len(vehicles_tokens) != period_length: + raise ValueError("The number of vehicle counts provided does not equal the period length.") + try: + vehicles_per_day = [int(x) for x in vehicles_tokens] + except Exception as e: + raise ValueError("Error parsing the vehicles per day.") from e + + # Third line: vehicle capacity (all vehicles have same capacity) + try: + vehicle_capacity = float(all_lines[2]) + except Exception as e: + raise ValueError("Error parsing vehicle capacity.") from e + + depot = None + customers = [] + # Process the remaining lines. + for line in all_lines[3:]: + # Split into at most five tokens; the first four are assumed to be id, x, y and demand. + parts = line.split(maxsplit=4) + if len(parts) < 3: + continue # Skip lines that do not have minimum required data. + + try: + cid = int(parts[0]) + x = float(parts[1]) + y = float(parts[2]) + except Exception as ex: + raise ValueError("Error parsing id or coordinates in line: " + line) from ex + + # Check for depot (id == 0). For depot, we ignore demand and schedule. + if cid == 0: + depot = {"id": cid, "x": x, "y": y} + # Skip further processing of demand/schedules for the depot. + continue + + # For a customer, we expect a demand value. + if len(parts) < 4: + raise ValueError("Insufficient data for customer (id=%s) in line: %s" % (cid, line)) + try: + demand = float(parts[3]) + except Exception as ex: + raise ValueError("Error parsing demand for customer (id=%s) in line: %s" % (cid, line)) from ex + + # Parse possible schedule if provided. + schedules = [] + if len(parts) == 5: + try: + schedules = ast.literal_eval(parts[4]) + except Exception as ex: + raise ValueError("Error parsing delivery schedules in line: " + line) from ex + + customers.append({ + "id": cid, + "x": x, + "y": y, + "demand": demand, + "schedules": schedules + }) + + # Optionally, you can check if depot was found. + if depot is None: + raise ValueError("Depot (customer id 0) was not found in the file.") + + return [{ + "period_length": period_length, + "vehicles_per_day": vehicles_per_day, + "vehicle_capacity": vehicle_capacity, + "depot": depot, + "customers": customers + }] + + def eval_func(self, **kwargs): + """ + Evaluates the solution of the Period Vehicle Routing Problem for a single case. + Input kwargs should include: + - from data: + "depot": dict with keys "id", "x", "y". + "customers": list of customer dictionaries (each with keys "id", "x", "y", "demand", "schedules"). + "vehicles_per_day": list of ints (indicating the number of available vehicles per day). + "vehicle_capacity": numeric, the capacity of each vehicle. + "period_length": int, the number of days. + - from solve: + "selected_schedules": a mapping from customer id to the chosen schedule (a list of binary integers). + "tours": a mapping from day (1-indexed) to a list of tours; + each tour is a list of vertex ids (integers), starting and ending at depot (id 0), + with no intermediate depot visits. + The evaluator checks the following: + 1. For each customer (other than the depot), verifies that there is a chosen schedule, + and that the chosen schedule is one of that customer's candidate schedules. + 2. For each day: + - Verifies that the number of tours does not exceed the available vehicles for that day. + - Checks that every customer whose chosen schedule requires service is visited exactly once. + 3. Each tour must: + - Start at the depot (id 0) and end at the depot (id 0). + - Not include any depot visit in the middle (the depot may appear only as the first and the last vertex). + - Not visit the same customer more than once. + 4. Each tour must satisfy the capacity constraint: the total customer demand on the tour does not exceed vehicle_capacity. + 5. Finally, the evaluator computes the total tour length (using Euclidean distance) over all days. + Returns: + A numeric value representing the total tour length computed from the solution. + Raises an error if any constraint is violated. + """ + import math + + depot = kwargs["depot"] + customers = kwargs["customers"] + vehicles_per_day = kwargs["vehicles_per_day"] + vehicle_capacity = kwargs["vehicle_capacity"] + period_length = kwargs["period_length"] + + # Build a lookup table for customers by id. + customer_lookup = {cust["id"]: cust for cust in customers} + + # Validate the selected schedules. + selected_schedules = kwargs.get("selected_schedules") + if not isinstance(selected_schedules, dict): + raise ValueError("Solution must include a dictionary 'selected_schedules'.") + + # Ensure that every customer (except the depot) has a selected schedule. + for cust in customers: + # Assuming depot has id 0. + if cust["id"] == 0: + continue + if cust["id"] not in selected_schedules: + raise ValueError(f"Missing selected schedule for customer {cust['id']}.") + + # Now validate each provided schedule. + for cid, sel_sched in selected_schedules.items(): + cust = customer_lookup.get(cid) + if cust is None: + raise ValueError(f"Customer id {cid} in selected_schedules not found in customer list.") + if sel_sched not in cust["schedules"]: + raise ValueError( + f"Selected schedule {sel_sched} for customer {cid} is not among candidate schedules {cust['schedules']}.") + if len(sel_sched) != period_length: + raise ValueError(f"Selected schedule for customer {cid} does not match period_length {period_length}.") + + # Process tours for each day. + tours = kwargs.get("tours") + if not isinstance(tours, dict): + raise ValueError("Solution must include a dictionary 'tours'.") + + total_length = 0.0 + + def euclidean(a, b): + return math.sqrt((a["x"] - b["x"]) ** 2 + (a["y"] - b["y"]) ** 2) + + # Evaluate each day. + for day in range(1, period_length + 1): + # Validate the number of tours does not exceed the available vehicles. + tours_day = tours.get(day, []) + vehicles_available = vehicles_per_day[day - 1] + if len(tours_day) > vehicles_available: + raise ValueError( + f"On day {day}: Number of tours ({len(tours_day)}) exceeds available vehicles ({vehicles_available}).") + + # Determine all customers that should receive service today. + expected_customers = set() + for cust in customers: + if cust["id"] == 0: + continue + sched = selected_schedules.get(cust["id"]) + if sched is not None and sched[day - 1] == 1: + expected_customers.add(cust["id"]) + + visited_today = [] + for tour in tours_day: + # A valid tour must have at least depot, one customer, and depot again. + if len(tour) < 3: + raise ValueError(f"Tour {tour} on day {day} is too short.") + # Check that the tour starts and ends with the depot. + if tour[0] != 0 or tour[-1] != 0: + raise ValueError(f"Tour {tour} on day {day} must start and end at the depot (id 0).") + # Ensure no depot visits occur in the middle. + if 0 in tour[1:-1]: + raise ValueError(f"Tour {tour} on day {day} contains an extra depot visit in the middle.") + + seen_in_tour = set() + # Process customer visits in the tour (excluding depot at the beginning and end). + for vid in tour[1:-1]: + if vid in seen_in_tour: + raise ValueError(f"Tour on day {day} visits customer {vid} more than once.") + seen_in_tour.add(vid) + visited_today.append(vid) + + # Check the capacity constraint for the tour. + capacity_used = sum(customer_lookup[vid]["demand"] for vid in tour[1:-1]) + if capacity_used > vehicle_capacity: + raise ValueError( + f"Tour on day {day} exceeds capacity: used {capacity_used}, capacity is {vehicle_capacity}.") + + # Compute the tour's travel distance. + tour_length = 0.0 + prev = depot + for vid in tour[1:]: + curr = depot if vid == 0 else customer_lookup.get(vid) + if curr is None: + raise ValueError(f"Customer id {vid} in tour on day {day} not found.") + tour_length += euclidean(prev, curr) + prev = curr + total_length += tour_length + + # Ensure that the visited customers exactly match those expected for the day. + if set(visited_today) != expected_customers: + missing = expected_customers - set(visited_today) + extra = set(visited_today) - expected_customers + err_msg = f"On day {day}: " + if missing: + # Only showing a sample of missing customers + err_msg += f"Missing visits for customers such as {list(missing)[:10]}. " + if extra: + err_msg += f"Extra visits for customers {list(extra)}." + raise ValueError(err_msg) + + return total_length + + def norm_score(self, results): + optimal_scores = { + "prvp1.txt": [547.9], + "prvp2.txt": [1487.6], + "prvp3.txt": [550.1], + "prvp4.txt": [872.3], + "prvp5.txt": [2207.9], + "prvp6.txt": [965.7], + "prvp7.txt": [839.2], + "prvp8.txt": [2294.2], + "prvp9.txt": [925.0], + "prvp10.txt": [1819.2], + } + + normed = {} + for case, (scores, error_message) in results.items(): + if case not in optimal_scores: + continue # Skip if there's no optimal score defined. + optimal_list = optimal_scores[case] + normed_scores = [] + # Compute normalized score for each index. + for idx, score in enumerate(scores): + if isinstance(score, (int, float)): + normed_scores.append(optimal_list[idx] / score) + else: + normed_scores.append(score) + normed[case] = (normed_scores, error_message) + + return normed + + def get_dev(self): + dev = {'prvp1.txt': [], 'prvp3.txt': [], 'prvp5.txt': [], + 'prvp7.txt': [], 'prvp9.txt': []} + + return dev + + + + + + + + + +# Task configuration for benchmark task +ENTRY_NAME = 'solve' +FUNCTION_SIGNATURE = 'def solve(...):' +IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' +TASK_DESCRIPTION = '("The Period Vehicle Routing Problem requires planning delivery routes over a multi‐day planning "' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The Period Vehicle Routing Problem requires planning delivery routes over a multi‐day planning "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(depot: dict, customers: list, vehicles_per_day: list, vehicle_capacity: float, period_length: int) -> dict:\n """\n Solves an instance of the Period Vehicle Routing Problem.\n Input kwargs includes:\n - depot: dict with keys:\n "id": int, always 0.\n "x": float, the x-coordinate.\n "y": float, the y-coordinate.\n - customers: list of dictionaries (with customer id ≠ 0) having keys:\n "id": int, the customer id.\n "x": float, the x-coordinate.\n "y": float, the y-coordinate.\n "demand": numeric, the customer demand.\n "schedules": list of candidate schedules, each a list (of length period_length) with binary entries.\n - vehicles_per_day: list of ints (length period_length) indicating the number of vehicles available each day.\n - vehicle_capacity: numeric, the capacity of each vehicle.\n - period_length: int, the number of days in the planning period.\n The solution must decide:\n 1. Which service schedule (from the candidate schedules) is selected for each customer.\n 2. For each day (days are 1-indexed), the daily tours: a list of tours—one per available vehicle.\n Each tour is a continuous route that starts at the depot (0), visits some customers (each exactly once),\n and returns to the depot. The depot may only appear as the first and last vertex in each tour.\n The number of tours for day d must be exactly equal to vehicles_per_day[d-1].\n The returned solution is a dictionary containing:\n - "selected_schedules": dict mapping each customer id (integer) to the chosen schedule (a list of binary integers).\n - "tours": dict mapping day (an integer between 1 and period_length) to a list of tours.\n Each tour is a list of vertex ids (integers), starting and ending at the depot (id 0).\n """\n # ------------------------------\n\n return {\n "selected_schedules": ...,\n "tours": ...\n }' +EVAL_CLASS_NAME = 'VRPREvaluationCB' +EVAL_KWARGS = {'timeout_seconds': 60} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_vehicle_routing_period_routing/paras.yaml b/examples/benchmark_tasks/optimization_vehicle_routing_period_routing/paras.yaml new file mode 100644 index 00000000..ca482783 --- /dev/null +++ b/examples/benchmark_tasks/optimization_vehicle_routing_period_routing/paras.yaml @@ -0,0 +1,2 @@ +name: VRPREvaluationCB +timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_vrptw_construct/__init__.py b/examples/benchmark_tasks/optimization_vrptw_construct/__init__.py new file mode 100644 index 00000000..5574218c --- /dev/null +++ b/examples/benchmark_tasks/optimization_vrptw_construct/__init__.py @@ -0,0 +1,279 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: optimization_vrptw_construct +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# Module Name: VRPTWEvaluation +# Last Revision: 2025/2/16 +# Description: Evaluates the Vehicle Routing Problem with Time Windows (VRPTW). +# The VRPTW involves finding optimal routes for a fleet of vehicles to serve a set of customers, +# respecting time windows and vehicle capacity constraints. +# This module is part of the LLM4AD project (https://github.com/Optima-CityU/llm4ad). +# +# Parameters: +# - timeout_seconds: Maximum allowed time (in seconds) for the evaluation process: int (default: 30). +# - problem_size: Number of customers to serve (excluding the depot): int (default: 50). +# - n_instance: Number of problem instances to generate: int (default: 16). +# +# References: +# - Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any +import os, sys +sys.path.insert(0, os.path.dirname(__file__)) +import copy +import numpy as np +from llm4ad_loader import Evaluation +from get_instance import GetData +# from llm4ad.task.optimization.vrptw_construct.get_instance import GetData # Converted from LLM4AD import +# from llm4ad.task.optimization.vrptw_construct.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'def select_next_node(current_node: int, depot: int, unvisited_nodes: np.ndarray, rest_capacity: np.ndarray, current_time: np.ndarray,\\\n demands: np.ndarray, distance_matrix: np.ndarray, time_windows: np.ndarray) -> int:\n """Design a novel algorithm to select the next node in each step.\n Args:\n current_node: ID of the current node.\n depot: ID of the depot.\n unvisited_nodes: Array of IDs of unvisited nodes.\n rest_capacity: Rest capacity of vehicle\n current_time: Current time\n demands: Demands of nodes\n distance_matrix: Distance matrix of nodes.\n time_windows: Time windows of nodes.\n Return:\n ID of the next node to visit.\n """\n next_node = unvisited_nodes[0]\n return next_node' +task_description = 'The task involves finding optimal routes for a fleet of vehicles to serve a set of customers, respecting time windows and vehicle capacity constraints. Help me design an algorithm to select the next node in each step.' + + + +class VRPTWEvaluation(Evaluation): + def __init__(self, + timeout_seconds=30, + problem_size=50, + n_instance=16, + **kwargs): + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + self.problem_size = problem_size + self.n_instance = n_instance + + getData = GetData(self.n_instance, self.problem_size + 1) + self._datasets = getData.generate_instances() + + def tour_cost(self, distance_matrix, solution, time_service, time_windows): + cost = 0 + current_time = 0 + + for j in range(len(solution) - 1): + travel_time = distance_matrix[int(solution[j]), int(solution[j + 1])] + # print(current_time) + current_time += travel_time + + if current_time < time_windows[solution[j + 1]][0]: + current_time = time_windows[solution[j + 1]][0] + if max(current_time, time_windows[solution[j + 1]][0]) > time_windows[solution[j + 1]][1]: + # print(max(current_time ,time_windows[solution[j + 1]][0])+time_service[solution[j + 1]] ) + # print(time_windows[solution[j + 1]][1]) + return float('inf') # Exceeds time window + current_time += time_service[solution[j + 1]] + cost += travel_time + if (solution[j + 1] == 0): + current_time = 0 + return cost + + def evaluate_program(self, program_str: str, callable_func: callable) -> Any | None: + return self.evaluate(callable_func) + + def evaluate(self, heuristic): + dis = np.ones(self.n_instance) + n_ins = 0 + + for instance, distance_matrix, demands, vehicle_capacity, time_service, time_windows in self._datasets: + route = [] + current_load = 0 + current_node = 0 + current_time = 0 + route.append(current_node) + unvisited_nodes = set(range(1, self.problem_size + 1)) # Assuming node 0 is the depot + all_nodes = np.array(list(unvisited_nodes)) + feasible_unvisited_nodes = all_nodes + + unvisited_nodes_depot = np.array(list(unvisited_nodes)) + + while unvisited_nodes: + + next_node = heuristic(current_node, + 0, + feasible_unvisited_nodes, + vehicle_capacity - current_load, + current_time, + copy.deepcopy(demands), + copy.deepcopy(distance_matrix), + copy.deepcopy(time_windows)) + if next_node == 0: + route.append(next_node) + current_load = 0 + current_time = 0 + current_node = 0 + unvisited_nodes_depot = np.array(list(unvisited_nodes)) + else: + travel_time = distance_matrix[current_node, next_node] + current_time += (travel_time) + current_time = max(current_time, time_windows[next_node][0]) + current_time += time_service[next_node] + # if current_time < time_windows[next_node][0]: + # current_time = time_windows[next_node][0] + # if current_time > time_windows[next_node][1]: + # print(current_time) + # print(time_windows[next_node][1]) + # return float('inf') # Exceeds time window + route.append(next_node) + current_load += demands[next_node] + unvisited_nodes.remove(next_node) + current_node = next_node + unvisited_nodes_depot = np.append(np.array(list(unvisited_nodes)), 0) + + feasible_nodes_tw = np.array([node for node in all_nodes \ + if max(current_time + distance_matrix[current_node][node], time_windows[node][0]) < time_windows[node][1] - 0.0001 \ + and max(current_time + distance_matrix[current_node][node], time_windows[node][0]) + time_service[node] + distance_matrix[node][0] < time_windows[0][1] - 0.0001]) + feasible_nodes_capacity = np.array([node for node in all_nodes if current_load + demands[node] <= vehicle_capacity]) + # Determine feasible and unvisited nodes + feasible_unvisited_nodes = np.intersect1d(np.intersect1d(feasible_nodes_tw, feasible_nodes_capacity), list(unvisited_nodes)) + + if len(unvisited_nodes) > 0 and len(feasible_unvisited_nodes) < 1: + route.append(0) + current_load = 0 + current_time = 0 + current_node = 0 + feasible_unvisited_nodes = np.array(list(unvisited_nodes)) + + # print(set(route)) + + if len(set(route)) != self.problem_size + 1: + return None + + LLM_dis = self.tour_cost(distance_matrix, route, time_service, time_windows) + dis[n_ins] = LLM_dis + + n_ins += 1 + if n_ins == self.n_instance: + break + # print(dis) + ave_dis = np.average(dis) + return -ave_dis + + +if __name__ == '__main__': + def select_next_node(current_node: int, depot: int, unvisited_nodes: np.ndarray, rest_capacity: np.ndarray, current_time: np.ndarray, demands: np.ndarray, distance_matrix: np.ndarray, time_windows: np.ndarray) -> int: + """Design a novel algorithm to select the next node in each step. + Args: + current_node: ID of the current node. + depot: ID of the depot. + unvisited_nodes: Array of IDs of unvisited nodes. + rest_capacity: Rest capacity of vehicle + current_time: Current time + demands: Demands of nodes + distance_matrix: Distance matrix of nodes. + time_windows: Time windows of nodes. + Return: + ID of the next node to visit. + """ + best_node = -1 + best_value = -float('inf') + + for node in unvisited_nodes: + if demands[node] <= rest_capacity: + travel_time = distance_matrix[current_node, node] + arrival_time = current_time + travel_time + + if arrival_time <= time_windows[node][1]: # Checking if within time window + wait_time = max(0, time_windows[node][0] - arrival_time) + effective_time = arrival_time + wait_time + distance_to_demand_ratio = travel_time / demands[node] if demands[node] > 0 else float('inf') + + if distance_to_demand_ratio > best_value: + best_value = distance_to_demand_ratio + best_node = node + + return best_node if best_node != -1 else depot + + + eval = VRPTWEvaluation() + res = eval.evaluate_program('', select_next_node) + print(res) + +# Task configuration for benchmark task +ENTRY_NAME = 'select_next_node' +FUNCTION_SIGNATURE = 'def select_next_node(...):' +IMPORT_HEADER = 'import numpy as np\nimport math' +TASK_DESCRIPTION = 'The task involves finding optimal routes for a fleet of vehicles to serve a set of customers, respecting time windows and vehicle capacity constraints. Help me design an algorithm to select the next node in each step.' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `select_next_node` for the LLM4AD task.\\n\\nTask description:\\nThe task involves finding optimal routes for a fleet of vehicles to serve a set of customers, respecting time windows and vehicle capacity constraints. Help me design an algorithm to select the next node in each step.\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'def select_next_node(current_node: int, depot: int, unvisited_nodes: np.ndarray, rest_capacity: np.ndarray, current_time: np.ndarray,\\\n demands: np.ndarray, distance_matrix: np.ndarray, time_windows: np.ndarray) -> int:\n """Design a novel algorithm to select the next node in each step.\n Args:\n current_node: ID of the current node.\n depot: ID of the depot.\n unvisited_nodes: Array of IDs of unvisited nodes.\n rest_capacity: Rest capacity of vehicle\n current_time: Current time\n demands: Demands of nodes\n distance_matrix: Distance matrix of nodes.\n time_windows: Time windows of nodes.\n Return:\n ID of the next node to visit.\n """\n next_node = unvisited_nodes[0]\n return next_node' +EVAL_CLASS_NAME = 'VRPTWEvaluation' +EVAL_KWARGS = {'timeout_seconds': 20} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/optimization_vrptw_construct/get_instance.py b/examples/benchmark_tasks/optimization_vrptw_construct/get_instance.py new file mode 100644 index 00000000..11667a34 --- /dev/null +++ b/examples/benchmark_tasks/optimization_vrptw_construct/get_instance.py @@ -0,0 +1,64 @@ +import pickle + +import numpy as np + + +class GetData: + def __init__(self, n_instance, n_cities): + self.n_instance = n_instance + self.n_cities = n_cities + self.max_time = 4.6 + + def generate_instances(self): + """each instance -> (coordinates, distances, demands, capacity)""" + np.random.seed(2024) + instance_data = [] + for _ in range(self.n_instance): + coordinates = np.random.rand(self.n_cities + 1, 2) + demands = np.append(np.array([0]), np.random.randint(1, 10, size=self.n_cities)) + capacity = 40 + distances = np.linalg.norm(coordinates[:, np.newaxis] - coordinates, axis=2) + node_serviceTime = np.random.rand(self.n_cities) * 0.05 + 0.15 + serviceTime = np.append(np.array([0]), node_serviceTime) + # shape: (batch, problem) + # range: (0.15, 0.2) for T=4.6 + + node_lengthTW = np.random.rand(self.n_cities) * 0.05 + 0.15 + # shape: (batch, problem) + # range: (0.15, 0.2) for T=4.6 + + d0i = distances[0][1:] + # shape: (batch, problem) + + # ei = (np.random.rand(self.n_cities) * ((self.max_time - node_serviceTime - node_lengthTW) / d0i - 1) + 1) + ei = np.random.rand(self.n_cities) * (((4.6 * np.ones(self.n_cities) - node_serviceTime - node_lengthTW) / d0i - 1) - 1) + 1 + # shape: (batch, problem) + # default velocity = 1.0 + + # Element-wise multiplication + node_earlyTW = np.multiply(ei, d0i) + # node_earlyTW = ei * d0i + # shape: (batch, problem) + # default velocity = 1.0 + + node_lateTW = node_earlyTW + node_lengthTW + # shape: (batch, problem) + + time_windows_node = np.append(np.array([node_earlyTW]).reshape(self.n_cities, 1), np.array([node_lateTW]).reshape(self.n_cities, 1), axis=1) + + time_windows = np.append(np.array([[0, self.max_time]]), time_windows_node, axis=0) + + instance_data.append((coordinates, distances, demands, capacity, serviceTime, time_windows)) + return instance_data + + +if __name__ == '__main__': + gd = GetData(10, 50) + data = gd.generate_instances() + with open('data_vrptw.pkl', 'wb') as f: + pickle.dump(data, f) + with open('data_vrptw.pkl', 'rb') as f: + data = pickle.load(f) + coordinates, distances, demands, capacity, serviceTime, time_windows = data[0] + print(time_windows) + print(time_windows[0]) diff --git a/examples/benchmark_tasks/optimization_vrptw_construct/paras.yaml b/examples/benchmark_tasks/optimization_vrptw_construct/paras.yaml new file mode 100644 index 00000000..72c258f8 --- /dev/null +++ b/examples/benchmark_tasks/optimization_vrptw_construct/paras.yaml @@ -0,0 +1,2 @@ +name: VRPTWEvaluation +timeout_seconds: 20 \ No newline at end of file diff --git a/examples/benchmark_tasks/science_discovery_ode_1d/__init__.py b/examples/benchmark_tasks/science_discovery_ode_1d/__init__.py new file mode 100644 index 00000000..7ded0d44 --- /dev/null +++ b/examples/benchmark_tasks/science_discovery_ode_1d/__init__.py @@ -0,0 +1,258 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: science_discovery_ode_1d +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +# Module Name: ODEEvaluation +# Last Revision: 2025/3/5 +# Description: Provides the skeleton for an ODE mathematical function based on given initial data. +# The function is designed to be differentiable and continuous, using only a limited +# set of selectable components. This module is part of the LLM4AD project +# (https://github.com/Optima-CityU/llm4ad). +# +# Parameters: +# - x: float - initial value of the ODE formula (default: None). +# - params: np.ndarray - 1D array of numeric constants or parameters to be optimized (default: None). +# - timeout_seconds: int - Maximum allowed time (in seconds) for the evaluation process (default: 20). +# +# References: +# - Du, Mengge, et al. "Llm4ed: Large language models for automatic equation discovery." +# arXiv preprint arXiv:2405.07761 (2024). +# +# ------------------------------- Copyright -------------------------------- +# Copyright (c) 2025 Optima Group. +# +# Permission is granted to use the LLM4AD platform for research purposes. +# All publications, software, or other works that utilize this platform +# or any part of its codebase must acknowledge the use of "LLM4AD" and +# cite the following reference: +# +# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, +# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design +# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). +# +# For inquiries regarding commercial use or licensing, please contact +# http://www.llm4ad.com/contact.html +# -------------------------------------------------------------------------- + + +from __future__ import annotations + +import os, sys +sys.path.insert(0, os.path.dirname(__file__)) +import re +import itertools +from typing import Any +import numpy as np + +from llm4ad_loader import Evaluation +# from llm4ad.task.science_discovery.ode_1d.template import template_program, task_description # Template values embedded below + +# Embedded template values +template_program = 'import numpy as np\n\ndef equation(x: float, params: np.ndarray) -> float:\n """ A ODE mathematical function \n Args:\n x: the initial float value of the ode formula\n params: a 1-d Array of numeric constants or parameters to be optimized\n\n Return:\n A numpy array representing the result of applying the mathematical function to the inputs.\n """\n y = params[0] * x + params[2]\n return y' +task_description = '("Find the ODE mathematical function skeleton, given data on initial x. The function should be differentiable, continuous."' + +from ode_1d import strogatz_extended, strogatz_equations +# from llm4ad.task.science_discovery.ode_1d import strogatz_extended, strogatz_equations # Converted from LLM4AD import + +__all__ = ['ODEEvaluation'] + +MAX_NPARAMS = 10 +params = [1.0] * MAX_NPARAMS + +local_dict = { + "np.e": "sp.E", + "np.pi": "sp.pi", + "np.arcsin": "sp.asin", + "np.arccos": "sp.acos", + "np.arctan": "sp.atan", + "np.sin": "sp.sin", + "np.cos": "sp.cos", + "np.tan": "sp.tan", + "np.sign": "sp.sign", + "np.sqrt": "sp.sqrt", + "np.log": "sp.log", + "np.exp": "sp.exp", +} + + +def evaluate(program_str: str, data: dict, equation: callable) -> float | None: + """ Evaluate the equation on data observations.""" + + # Load data observations + xs = np.array(data['xs']) + ts = np.array(data['t']) + ys = np.array(list(itertools.chain(*data['ys']))) # flatten to 1d + num_ini_x_values = len(xs) + num_variables = len(xs[0]) + + try: # initial x(0) = x0 + # t = sp.symbols('t') # time variable t + # x0 = sp.Function('x0')(t) # x(t) is the unknown formula about t + # constants = [sp.symbols(f'c{i}') for i in range(MAX_NPARAMS)] # constants symbol + + program_str = re.sub(r"def equation\(", r"def equation(t: float, ", program_str) + local_vars = {"equation": equation} + exec(program_str, globals(), local_vars) + equation = local_vars['equation'] # replace equation with str that after replacement of key parts + + # formula_sympy = equation(x0, constants) + # diff_eq = sp.Eq(sp.diff(x0, t), formula_sympy) + + # calculate the values of 2 initial x0 value + # solution_with_initial = sp.dsolve(diff_eq, ics={x0.subs(t, 0): xs[0][0]}) + # x0_solution = solution_with_initial.rhs # extract the expression of right part + # x0_func = sp.lambdify([t, constants], x0_solution, 'numpy') + except Exception as e: + # print(e) + return None + + # Optimize parameters based on data + from scipy.optimize import minimize + from scipy.integrate import solve_ivp + def loss(params): + y_pred = np.zeros(num_ini_x_values * len(ts[0])) + for i in range(num_ini_x_values): + s = solve_ivp(equation, (ts[i][0], ts[i][-1]), xs[i], args=(params,), t_eval=ts[i]) + y_pred[i * len(ts[0]):(i + 1) * len(ts[0])] = s['y'][0] + return np.mean((y_pred - ys) ** 2) + + # x0_funcs = [] + # for i in range(num_ini_x_values): + # solution_with_initial = sp.dsolve(diff_eq, ics={x0.subs(t, 0): xs[i][0]}) + # x0_solution = solution_with_initial.rhs # extract the expression of right part + # x0_func = sp.lambdify([t, constants], x0_solution, 'numpy') + # + # x0_funcs.append(x0_func) + + loss_partial = lambda params: loss(params) + result = minimize(loss_partial, [1.0] * MAX_NPARAMS, method='BFGS') + + # Return evaluation score + optimized_params = result.x + loss = result.fun + + if np.isnan(loss) or np.isinf(loss): + return None + else: + return -loss + + +class ODEEvaluation(Evaluation): + + def __init__(self, timeout_seconds=200000, test_id=1, **kwargs): + """ + Args: + timeout_seconds: evaluate time limit. + test_id: test equation id ranges from [1, 16]. + """ + + super().__init__( + template_program=template_program, + task_description=task_description, + use_numba_accelerate=False, + timeout_seconds=timeout_seconds + ) + + # read files + test_eq_dict = strogatz_equations.equations[test_id - 1] + dataset = strogatz_extended.data + + dataset = dataset[test_id - 1] + xs = dataset['init'] + t = [e['t'] for e in dataset['solutions'][0]] + ys = [e['y'][0] for e in dataset['solutions'][0]] # for only 1 output + self._datasets = { + 'xs': xs, + 'ys': ys, + 't': t + } + + def evaluate_program(self, program_str: str, callable_func: callable) -> Any | None: + import inspect + if not program_str: + program_str = inspect.getsource(callable_func).lstrip() # for testing + # for np_func, sp_func in local_dict.items(): # replace key parts + # program_str = program_str.replace(np_func, sp_func) + return evaluate(program_str, self._datasets, callable_func) + + +if __name__ == '__main__': + def equation(x: float, params: np.ndarray) -> float: + """ A ODE mathematical function + Args: + x: the initial float value of the ode formula + params: a 1-d Array of numeric constants or parameters to be optimized + + Return: + A numpy array representing the result of applying the mathematical function to the inputs. + """ + y = params[0] * np.sin(x) + params[1] + return y + + + evaluation = ODEEvaluation() + res = evaluation.evaluate_program('', equation) + print(res) + +# Task configuration for benchmark task +ENTRY_NAME = 'equation' +FUNCTION_SIGNATURE = 'def equation(...):' +IMPORT_HEADER = 'import numpy as np\nimport math' +TASK_DESCRIPTION = '("Find the ODE mathematical function skeleton, given data on initial x. The function should be differentiable, continuous."' +OBJECTIVE_TEXT = 'You are optimizing the implementation of `equation` for the LLM4AD task.\\n\\nTask description:\\n("Find the ODE mathematical function skeleton, given data on initial x. The function should be differentiable, continuous."\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' +TEMPLATE_FUNCTION = 'import numpy as np\n\ndef equation(x: float, params: np.ndarray) -> float:\n """ A ODE mathematical function \n Args:\n x: the initial float value of the ode formula\n params: a 1-d Array of numeric constants or parameters to be optimized\n\n Return:\n A numpy array representing the result of applying the mathematical function to the inputs.\n """\n y = params[0] * x + params[2]\n return y' +EVAL_CLASS_NAME = 'ODEEvaluation' +EVAL_KWARGS = {'test_id (1-16)': 1, 'timeout_seconds': 20} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {FUNCTION_SIGNATURE}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) diff --git a/examples/benchmark_tasks/science_discovery_ode_1d/paras.yaml b/examples/benchmark_tasks/science_discovery_ode_1d/paras.yaml new file mode 100644 index 00000000..405b2700 --- /dev/null +++ b/examples/benchmark_tasks/science_discovery_ode_1d/paras.yaml @@ -0,0 +1,3 @@ +name: FeynmanEvaluation +test_id (1-16): 1 +timeout_seconds: 20 \ No newline at end of file diff --git a/examples/benchmark_tasks/science_discovery_ode_1d/strogatz_equations.py b/examples/benchmark_tasks/science_discovery_ode_1d/strogatz_equations.py new file mode 100644 index 00000000..0feda3c0 --- /dev/null +++ b/examples/benchmark_tasks/science_discovery_ode_1d/strogatz_equations.py @@ -0,0 +1,223 @@ +""" +A selection of ordinary differential equations primarily from Steven Strogatz's book "Nonlinear Dynamics and Chaos" with manually chosen parameter values and initial conditions. +Some other famous known systems have been selected from other sources, which are included in the dictionary entries as well. +We selected ODEs primarily based on whether they have actually been suggested as models for real-world phenomena as well as on whether they are 'iconic' ODEs in the sense that they are often used as examples in textbooks and/or have recognizable names. +Whenever there were 'realistic' parameter values suggested, we chose those. +In this benchmark, we typically include only one set of parameter values per equation. +Many of the ODEs in Strogatz' book are analyzed in terms of the different limiting behavior for different parameter settings. +For some systems that exhibit wildely different behavior for different parameter settings, we include multiple sets of parameter values as separate equations (e.g., Lorenz system in chaotic and non-chaotic regime). +For each equation, we include two sets of manually chosen initial conditions. +There are 23 equations with dimension 1, 28 equations with dimension 2, 10 equation with dimension 3, and 2 equations with dimension 4. +This results in a total of 63 equations, 4 of which display chaotic behavior. +""" + +equations = [ + { + 'id': 1, + 'eq': '(c_0 - x_0 / c_1) / c_2', + 'dim': 1, + 'consts': [[0.7, 1.2, 2.31]], + 'init': [[10.], [3.54]], + 'init_constraints': 'x_0 > 0', + 'const_constraints': 'c_1 > 0, c_2 > 0', + 'eq_description': 'RC-circuit (charging capacitor)', + 'const_description': 'c_0: fixed voltage source, c_1: capacitance, c_2: resistance', + 'var_description': 'x_0: charge', + 'source': 'strogatz p.20' + }, + { + 'id': 2, + 'eq': 'c_0 * x_0', + 'dim': 1, + 'consts': [[0.23]], + 'init': [[4.78], [0.87]], + 'init_constraints': 'x_0 > 0', + 'const_constraints': '', + 'eq_description': 'Population growth (naive)', + 'const_description': 'c_0: growth rate', + 'var_description': 'x_0: population', + 'source': 'strogatz p.22' + }, + { + 'id': 4, + 'eq': '1 / (1 + exp(c_0 - x_0 / c_1)) - 0.5', + 'dim': 1, + 'consts': [[0.5, 0.96]], + 'init': [[0.8], [0.02]], + 'init_constraints': 'x_0 > 0', + 'const_constraints': 'c_1 > 0', + 'eq_description': 'RC-circuit with non-linear resistor (charging capacitor)', + 'const_description': 'c_0: fixed voltage source, c_1: capacitance', + 'var_description': 'x_0: charge', + 'source': 'strogatz p.38' + }, + { + 'id': 5, + 'eq': 'c_0 - c_1 * x_0^2', + 'dim': 1, + 'consts': [[9.81, 0.0021175]], + 'init': [[0.5], [73.]], + 'init_constraints': '', + 'const_constraints': 'c_0 > 0, c_1 > 0', + 'eq_description': 'Velocity of a falling object with air resistance', + 'const_description': 'c_0: gravitational acceleration, c_1: overall drag for human: 0.5 * C * rho * A / m, with drag coeff C=0.7, air density rho=1.21, cross-sectional area A=0.25, mass m=50', + 'var_description': 'x_0: velocity', + 'source': 'strogatz p.38' + }, + { + 'id': 7, + 'eq': 'c_0 * x_0 * log(c_1 * x_0)', + 'dim': 1, + 'consts': [[0.032, 2.29]], + 'init': [[1.73], [9.5]], + 'init_constraints': 'x_0 > 0', + 'const_constraints': 'c_0 > 0, c_1 > 0', + 'eq_description': 'Gompertz law for tumor growth', + 'const_description': 'c_0: growth rate, c_1: tumor carrying capacity', + 'var_description': 'x_0: proportional to number of cells (tumor size)', + 'source': 'strogatz p.39' + }, + { + 'id': 8, + 'eq': 'c_0 * x_0 * (1 - x_0 / c_1) * (x_0 / c_2 - 1)', + 'dim': 1, + 'consts': [[0.14, 130., 4.4]], + 'init': [[6.123], [2.1]], + 'init_constraints': 'x_0 > 0', + 'const_constraints': 'c_0 > 0, c_1 > 0, c_2 > 0', + 'eq_description': 'Logistic equation with Allee effect', + 'const_description': 'c_0: growth rate, c_1: carrying capacity, c_2: Allee effect parameter', + 'var_description': 'x_0: population', + 'source': 'strogatz p.39' + }, + { + 'id': 10, + 'eq': '(1 - x_0) * c_0 * x_0^c_1 - x_0 * (1 - c_0) * (1 - x_0)^c_1', + 'dim': 1, + 'consts': [[0.2, 1.2]], + 'init': [[0.83], [0.34]], + 'init_constraints': '0 < x_0 < 1', + 'const_constraints': '0 <= c_0 <= 1, c_1 > 1', + 'eq_description': 'Refined language death model for two languages', + 'const_description': 'c_0: perceived status of language 1, c_1: adjustable exponent', + 'var_description': 'x_0: proportion of population speaking language 1', + 'source': 'strogatz p.40' + }, + { + 'id': 13, + 'eq': 'c_0 * sin(x_0) * (c_1 * cos(x_0) - 1)', + 'dim': 1, + 'consts': [[0.0981, 9.7]], + 'init': [[3.1], [2.4]], + 'init_constraints': '', + 'const_constraints': 'c_0 > 0, c_1 > 0', + 'eq_description': 'Overdamped bead on a rotating hoop', + 'const_description': 'c_0: m * g, for m: mass, g: gravitational acceleration, c_1: r * omega^2 / g, for r: radius, omega: angular velocity', + 'var_description': 'x_0: angle', + 'source': 'strogatz p.63' + }, + { + 'id': 15, + 'eq': 'c_0 * x_0 * (1 - x_0 / c_1) - x_0^2 / (1 + x_0^2)', + 'dim': 1, + 'consts': [[0.4, 95.]], + 'init': [[44.3], [4.5]], + 'init_constraints': 'x_0 > 0', + 'const_constraints': 'c_0 > 0, c_1 > 0', + 'eq_description': 'Budworm outbreak with predation (dimensionless)', + 'const_description': 'c_0: growth rate (<0.5 for young forest, 1 for mature), c_1: carrying capacity (~300 for young forest)', + 'var_description': 'x_0: population', + 'source': 'strogatz p.76' + }, + { + 'id': 16, + 'eq': 'c_0 * x_0 - c_1 * x_0^3 - c_2 * x_0^5', + 'dim': 1, + 'consts': [[0.1, -0.04, 0.001]], + 'init': [[0.94], [1.65]], + 'init_constraints': '', + 'const_constraints': 'c_0 > 0', + 'eq_description': 'Landau equation (typical time scale tau = 1)', + 'const_description': 'c_0: small dimensionless parameter, c_1: constant, c_2: constant; c_1 > 0 for supercritical bifurcation; c_1 < 0 and c_2 > 0 for subcritical bifurcation', + 'var_description': 'x_0: order parameter', + 'source': 'strogatz p.87' + }, + { + 'id': 18, + 'eq': 'c_0 * x_0 * (1 - x_0 / c_1) - c_2 * x_0 / (c_3 + x_0)', + 'dim': 1, + 'consts': [[0.4, 100., 0.24, 50.]], + 'init': [[21.1], [44.1]], + 'init_constraints': 'x_0 > 0', + 'const_constraints': 'c_0 > 0, c_1 > 0, c_2 > 0, c_3 > 0', + 'eq_description': 'Improved logistic equation with harvesting/fishing', + 'const_description': 'c_0: growth rate, c_1: carrying capacity, c_2: harvesting rate, c_3: harvesting onset', + 'var_description': 'x_0: population', + 'source': 'strogatz p.90' + }, + { + 'id': 19, + 'eq': 'x_0 * (1 - x_0) - c_0 * x_0 / (c_1 + x_0)', + 'dim': 1, + 'consts': [[0.08, 0.8]], + 'init': [[0.13], [0.03]], + 'init_constraints': 'x_0 > 0', + 'const_constraints': 'c_0 > 0, c_1 > 0', + 'eq_description': 'Improved logistic equation with harvesting/fishing (dimensionless)', + 'const_description': 'c_0: harvesting rate, c_1: harvesting onset', + 'var_description': 'x_0: population', + 'source': 'strogatz p.90' + }, + { + 'id': 20, + 'eq': 'c_0 - c_1 * x_0 + x_0^2 / (1 + x_0^2)', + 'dim': 1, + 'consts': [[0.1, 0.55]], + 'init': [[0.002], [0.25]], + 'init_constraints': 'x_0 > 0', + 'const_constraints': 'c_0 >= 0, c_1 > 0', + 'eq_description': 'Autocatalytic gene switching (dimensionless)', + 'const_description': 'c_0: basal production rate, c_1: degradation rate', + 'var_description': 'x_0: gene product', + 'source': 'strogatz p.91' + }, + { + 'id': 21, + 'eq': 'c_0 - c_1 * x_0 - exp(-x_0)', + 'dim': 1, + 'consts': [[1.2, 0.2]], + 'init': [[0.], [0.8]], + 'init_constraints': 'x_0 >= 0', + 'const_constraints': 'c_0 >= 1, c_1 > 0', + 'eq_description': 'Dimensionally reduced SIR infection model for dead people (dimensionless)', + 'const_description': 'c_0: death rate, c_1: unknown parameter group', + 'var_description': 'x_0: dead people', + 'source': 'strogatz p.92' + }, + { + 'id': 22, + 'eq': 'c_0 + c_1 * x_0^5 / (c_2 + x_0^5) - c_3 * x_0', + 'dim': 1, + 'consts': [[1.4, 0.4, 123., 0.89]], + 'init': [[3.1], [6.3]], + 'init_constraints': 'x_0 > 0', + 'const_constraints': 'c_0 > 0, c_1 > 0, c_2 > 0, c_3 > 0', + 'eq_description': 'Hysteretic activation of a protein expression (positive feedback, basal promoter expression)', + 'const_description': 'c_0: basal transcription rate, c_1: maximum transcription rate, c_2: activation coefficient, c_3: decay rate', + 'var_description': 'x_0: protein concentration', + 'source': 'strogatz p.93' + }, + { + 'id': 23, + 'eq': 'c_0 - sin(x_0)', + 'dim': 1, + 'consts': [[0.21]], + 'init': [[-2.74], [1.65]], + 'init_constraints': '-pi <= x_0 <= pi', + 'const_constraints': 'c_0 > 0', + 'eq_description': 'Overdamped pendulum with constant driving torque/fireflies/Josephson junction (dimensionless)', + 'const_description': 'c_0: ratio of driving torque to maximum gravitational torque', + 'var_description': 'x_0: angle', + 'source': 'strogatz p.104' + } +] diff --git a/examples/convert_llm4ad_benchmark.py b/examples/convert_llm4ad_benchmark.py new file mode 100644 index 00000000..1f1ddc1b --- /dev/null +++ b/examples/convert_llm4ad_benchmark.py @@ -0,0 +1,460 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +'''convert_llm4ad_benchmark.py +Convert LLM4AD tasks into fully benchmark Trace-ready wrappers. + +Unlike the previous version, this creates completely self-contained task modules that: +1. Don't reference the original LLM4AD codebase +2. Include all necessary evaluation code and data generation +3. Have no hardcoded paths +4. Work without any external dependencies beyond standard libraries + numpy + +Each benchmark wrapper exposes: + build_trace_problem() -> dict + +Usage: + python convert_llm4ad_benchmark.py --llm4ad-root /path/to/LLM4AD --out ./benchmark_tasks +''' + +import argparse, sys, os, inspect, importlib, json, shutil +from pathlib import Path +import re +import textwrap +import ast +import runpy + +# ------------------------------- Helpers ------------------------------- + +def read_file(p: Path) -> str: + try: + return p.read_text(encoding='utf-8') + except Exception: + return '' + +def extract_template_program(text: str) -> str | None: + '''Pull out the Python code inside a variable named `template_program`.''' + # Try triple-single quotes + m1 = re.search(r"""template_program\s*=\s*'''(.*?)'''""", text, re.DOTALL) + if m1: + return m1.group(1).strip() + # Try triple-double quotes + m2 = re.search(r'"""template_program\s*=\s*"""(.*?)""""""', text, re.DOTALL) + # The above pattern is brittle across snapshots; fallback: generic after '=' until next triple quotes + m3 = re.search(r'template_program\s*=\s*(?P\"\"\"|\'\'\')(.*?)(?P=q)', text, re.DOTALL) + if m3: + return m3.group(2).strip() + # Fallback: single-line quotes + m4 = re.search(r"template_program\s*=\s*([\'\"])(.*?)\1", text, re.DOTALL) + if m4: + return m4.group(2).strip() + return None + +def extract_task_description(text: str) -> str | None: + m = re.search(r"task_description\s*=\s*(.+)", text) + if not m: + return None + val = m.group(1).strip() + if val.startswith(('"', '\'')) and val.endswith(('"', '\'')): + return val[1:-1] + return val + +def find_entry_function_name(template_code: str) -> str | None: + '''Find first def name( ... ) in the template code.''' + m = re.search(r"^\s*def\s+([A-Za-z_]\w*)\s*\(", template_code, re.MULTILINE) + return m.group(1) if m else None + +def extract_import_header(template_code: str) -> str: + '''Collect top-of-snippet import lines; ensure numpy/math present.''' + header_lines = [] + for line in template_code.splitlines(): + s = line.strip() + if s.startswith('import ') or s.startswith('from '): + header_lines.append(line.rstrip()) + defaults = ['import numpy as np', 'import math'] + for d in defaults: + if not any(l.strip().startswith(d) for l in header_lines): + header_lines.append(d) + return '\n'.join(header_lines) + +def snake_from_parts(parts): + s = '_'.join(p for p in parts if p) + s = re.sub(r'[^A-Za-z0-9_]+', '_', s) + s = re.sub(r'_+', '_', s).strip('_') + return s or 'task' + +def rewrite_imports_for_autonomy(code: str, template_program: str, task_description: str) -> str: + """Rewrite imports to work with benchmark task structure.""" + lines = [] + template_vars_inserted = False + path_setup_inserted = False + + for line in code.splitlines(): + stripped = line.strip() + + # Handle template imports FIRST (before removing llm4ad imports) + if ('template import template_program' in stripped or + 'from template import' in stripped): + # Replace with embedded template values + lines.append('# ' + line + ' # Template values embedded below') + if not template_vars_inserted: + lines.append('') + lines.append('# Embedded template values') + lines.append('template_program = ' + repr(template_program)) + lines.append('task_description = ' + repr(task_description)) + lines.append('') + template_vars_inserted = True + # Replace LLM4AD base imports + elif 'from llm4ad.base import Evaluation' in line: + lines.append('from llm4ad_loader import Evaluation') + elif stripped.startswith('from llm4ad.') or stripped.startswith('import llm4ad.'): + # Convert llm4ad imports - utilities to llm4ad_loader, others to local imports + if 'from llm4ad.task.' in stripped and 'import ' in stripped: + # Extract the module and imports + parts = stripped.split(' import ') + if len(parts) == 2: + module_path = parts[0].replace('from ', '') + imports = parts[1] + + # Check if this is a common utility that should come from llm4ad_loader + common_utils = ['load_subdir_as_text', 'load_subdir_as_pickle'] + imported_items = [item.strip() for item in imports.split(',')] + + # If any imported item is a common utility, import from llm4ad_loader + if any(item in common_utils for item in imported_items): + # Split into common utilities and local imports + loader_imports = [item for item in imported_items if item in common_utils] + local_imports = [item for item in imported_items if item not in common_utils] + + # Add import from llm4ad_loader for utilities + if loader_imports: + lines.append(f"from llm4ad_loader import {', '.join(loader_imports)}") + lines.append('# ' + line + ' # Common utilities from llm4ad_loader') + + # Add local imports if any remain + if local_imports: + if not path_setup_inserted: + lines.append('import os, sys') + lines.append('sys.path.insert(0, os.path.dirname(__file__))') + path_setup_inserted = True + module_file = module_path.split('.')[-1] + lines.append(f"from {module_file} import {', '.join(local_imports)}") + lines.append('# ' + line + ' # Local imports converted') + else: + # Regular local import conversion + if not path_setup_inserted: + lines.append('import os, sys') + lines.append('sys.path.insert(0, os.path.dirname(__file__))') + path_setup_inserted = True + module_file = module_path.split('.')[-1] + new_import = f"from {module_file} import {imports}" + lines.append(new_import) + lines.append('# ' + line + ' # Converted from LLM4AD import') + else: + lines.append('# ' + line + ' # Removed LLM4AD dependency - using local copies') + else: + lines.append('# ' + line + ' # Removed LLM4AD dependency - using local copies') + elif (stripped.startswith('from ') and 'import ' in stripped and + not stripped.startswith('from typing') and + not stripped.startswith('from __future__') and + not stripped.startswith('from collections') and + not stripped.startswith('from itertools') and + not stripped.startswith('from functools') and + not stripped.startswith('from math') and + not stripped.startswith('from numpy') and + not stripped.startswith('from llm4ad_loader') and + not '.' in stripped.split()[1]): # Local import (no dots) + # This is likely a local import - add path setup + if not path_setup_inserted: + lines.append('import os, sys') + lines.append('sys.path.insert(0, os.path.dirname(__file__))') + path_setup_inserted = True + lines.append(line) + elif (stripped.startswith('import ') and + not stripped.startswith('import numpy') and + not stripped.startswith('import math') and + not stripped.startswith('import os') and + not stripped.startswith('import sys') and + not stripped.startswith('import itertools') and + not stripped.startswith('import random') and + not stripped.startswith('import json') and + not stripped.startswith('import pickle') and + not '.' in stripped.split()[1]): # Local import (no dots) + # This might be a local import - add path setup + if not path_setup_inserted: + lines.append('import os, sys') + lines.append('sys.path.insert(0, os.path.dirname(__file__))') + path_setup_inserted = True + lines.append(line) + else: + lines.append(line) + + return '\n'.join(lines) + +def extract_evaluation_class(evaluation_file: Path) -> tuple[str, str]: + """Extract the evaluation class name and its full code.""" + content = read_file(evaluation_file) + + # Find the evaluation class definition + class_match = re.search(r'class\s+([A-Za-z_]\w*)\(Evaluation\)', content) + if not class_match: + raise ValueError(f"No Evaluation subclass found in {evaluation_file}") + + class_name = class_match.group(1) + + return class_name, content + +# ------------------------------- Core ---------------------------------- + +def discover_task_pairs(llm4ad_root: Path, requested_filters: list[str] | None): + '''Yield (template_path, evaluation_path, family_key).''' + candidates = [] + # example/* + ex = llm4ad_root / 'example' + if ex.exists(): + for tpl in ex.rglob('template.py'): + fam = tpl.parent + ev = fam / 'evaluation.py' + if ev.exists(): + rel = tpl.relative_to(ex) + key = rel.parts[0] if len(rel.parts)>0 else rel.stem + candidates.append((tpl, ev, key)) + # llm4ad/task/* + task_root = llm4ad_root / 'llm4ad' / 'task' + if task_root.exists(): + for tpl in task_root.rglob('template.py'): + fam = tpl.parent + ev = fam / 'evaluation.py' + if ev.exists(): + rel = tpl.relative_to(task_root) + # Use the full relative path without the template.py part for unique keys + key = '/'.join(rel.parts[:-1]) if len(rel.parts) > 1 else rel.stem + candidates.append((tpl, ev, key)) + # filter & dedup + pairs, seen = [], set() + for tpl, ev, key in candidates: + h = (str(tpl), str(ev)) + if h in seen: + continue + seen.add(h) + if requested_filters: + if not any(f in str(tpl) or f in str(ev) or f in key for f in requested_filters): + continue + pairs.append((tpl, ev, key)) + return pairs + +def copy_task_dependencies(task_dir: Path, out_task_dir: Path) -> list[str]: + """Copy additional files needed by a task (e.g., data generators).""" + copied_files = [] + + # Copy all Python files except template.py and evaluation.py + for py_file in task_dir.glob('*.py'): + if py_file.name not in ('template.py', 'evaluation.py'): + dest = out_task_dir / py_file.name + shutil.copy2(py_file, dest) + copied_files.append(py_file.name) + + # Copy paras.yaml if it exists + paras_file = task_dir / 'paras.yaml' + if paras_file.exists(): + shutil.copy2(paras_file, out_task_dir / 'paras.yaml') + copied_files.append('paras.yaml') + + # Copy any data files or other resources + for ext in ['*.txt', '*.json', '*.csv', '*.dat']: + for data_file in task_dir.glob(ext): + dest = out_task_dir / data_file.name + shutil.copy2(data_file, dest) + copied_files.append(data_file.name) + + return copied_files + +WRAPPER_TEMPLATE = '''#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Autonomous LLM4AD task: {task_name} +Generated by convert_llm4ad_benchmark.py + +This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. +""" + +# Embedded evaluation code (benchmark) +{evaluation_code} + +# Task configuration for benchmark task +ENTRY_NAME = {entry_name!r} +FUNCTION_SIGNATURE = {function_signature!r} +IMPORT_HEADER = {import_header!r} +TASK_DESCRIPTION = {task_description!r} +OBJECTIVE_TEXT = {objective_text!r} +TEMPLATE_FUNCTION = {template_function!r} +EVAL_CLASS_NAME = {eval_class_name!r} +EVAL_KWARGS = {eval_kwargs!r} + +def build_trace_problem(**override_eval_kwargs) -> dict: + """Build a Trace-ready problem using embedded benchmark evaluator.""" + + # Create evaluator instance with embedded class + eval_kwargs_final = EVAL_KWARGS.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) + + from llm4ad_loader import AutonomousEvaluatorGuide + from opto import trace + + # Create parameter + initial_code = TEMPLATE_FUNCTION.strip() + param = trace.node(initial_code, name='__code', + description=f'The code should start with: {{FUNCTION_SIGNATURE}}', + trainable=True) + + # Create guide using benchmark embedded evaluator + guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, + timeout=eval_kwargs_final.get('timeout_seconds', 30)) + + # Create dataset + train_dataset = dict( + inputs=[TASK_DESCRIPTION], + infos=[{{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}}] + ) + + # Optimizer kwargs + optimizer_kwargs = dict( + objective=OBJECTIVE_TEXT, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=ENTRY_NAME, + function_signature=FUNCTION_SIGNATURE, + eval_class=EVAL_CLASS_NAME, + benchmark=True, + ) + ) +''' + +def main(): + ap = argparse.ArgumentParser(description='Convert LLM4AD tasks into benchmark Trace wrappers.') + ap.add_argument('--llm4ad-root', type=str, required=True, help='Path to LLM4AD repository root.') + ap.add_argument('--out', type=str, default='./benchmark_tasks', help='Output folder for benchmark task modules.') + ap.add_argument('--select', type=str, default='', help='Comma-separated substrings to filter tasks.') + args = ap.parse_args() + + llm4ad_root = Path(args.llm4ad_root).resolve() + out = Path(args.out).resolve() + out.mkdir(parents=True, exist_ok=True) + + filters = [s.strip() for s in args.select.split(',') if s.strip()] if args.select else None + + pairs = discover_task_pairs(llm4ad_root, filters) + + if not pairs: + print('No (template.py, evaluation.py) pairs found with current filters.') + sys.exit(1) + + index = [] + + for tpl, ev, fam_key in pairs: + try: + tpl_txt = read_file(tpl) + ev_txt = read_file(ev) + + template_code = extract_template_program(tpl_txt) + if not template_code: + print(f'[SKIP] Could not extract template_program from {tpl}') + continue + + entry = find_entry_function_name(template_code) + if not entry: + print(f'[SKIP] Could not find entry function in template_program at {tpl}') + continue + + # description + task_desc = extract_task_description(tpl_txt) or f'Implement {entry}() to solve the problem.' + + # Extract evaluation class with template values + eval_class_name, eval_code = extract_evaluation_class(ev) + eval_code = rewrite_imports_for_autonomy(eval_code, template_code, task_desc) + + imports = extract_import_header(template_code) + # Capture function signature for clarity + fsig = re.search(r'(^\s*def\s+[A-Za-z_]\w*\s*\([^)]*\)\s*:\s*)', template_code, re.MULTILINE) + fsig_str = fsig.group(1).strip() if fsig else f'def {entry}(...):' + + objective_text = (f"You are optimizing the implementation of `{entry}` for the LLM4AD task.\\n\\n" + f"Task description:\\n{task_desc}\\n\\n" + f"Your goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.") + + # file name - use full path to avoid collisions + parts = fam_key.split('/') + if len(parts) >= 3 and parts[0] == 'optimization' and parts[1] == 'co_bench': + task_name = parts[2].replace('_co_bench', '') if parts[2].endswith('_co_bench') else parts[2] + short_key = snake_from_parts([parts[0], task_name]) + elif len(parts) >= 3: + short_key = snake_from_parts(parts[:3]) + else: + short_key = snake_from_parts(parts[:2]) + mod_name = short_key if short_key else snake_from_parts([entry]) + + # Create task directory + task_dir = out / mod_name + task_dir.mkdir(exist_ok=True) + + # Copy task dependencies + copied_files = copy_task_dependencies(ev.parent, task_dir) + + # Load eval kwargs from paras.yaml + paras_yaml = ev.parent / 'paras.yaml' + eval_kwargs = {} + if paras_yaml.exists(): + try: + import yaml # optional + eval_kwargs = yaml.safe_load(paras_yaml.read_text()) + if isinstance(eval_kwargs, dict): + eval_kwargs.pop('name', None) + except Exception: + eval_kwargs = {} + + # Create benchmark wrapper + wrapper_content = WRAPPER_TEMPLATE.format( + task_name=mod_name, + evaluation_code=eval_code, + entry_name=entry, + function_signature=fsig_str, + import_header=imports, + task_description=task_desc, + objective_text=objective_text, + template_function=template_code, + eval_class_name=eval_class_name, + eval_kwargs=eval_kwargs + ) + + wrapper_path = task_dir / '__init__.py' + wrapper_path.write_text(wrapper_content, encoding='utf-8') + + index.append(dict( + key=fam_key, + module=str(task_dir.relative_to(out)), + entry=entry, + eval_class=eval_class_name, + task_description=task_desc, + wrapper=mod_name, + copied_files=copied_files, + benchmark=True + )) + print(f"[OK] Created benchmark task {task_dir}") + + except Exception as e: + print(f"[ERROR] Failed to convert {fam_key}: {e}") + continue + + (out / 'index.json').write_text(json.dumps(index, indent=2), encoding='utf-8') + print(f"\\nCreated {len(index)} benchmark tasks at {out}") + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/examples/llm4ad_loader.py b/examples/llm4ad_loader.py new file mode 100644 index 00000000..b4794b55 --- /dev/null +++ b/examples/llm4ad_loader.py @@ -0,0 +1,492 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""llm4ad_loader.py +Autonomous LLM4AD task runner for Trace optimization. + +This module provides a complete, self-contained implementation of LLM4AD evaluators +that doesn't depend on the original LLM4AD codebase. All necessary components are +either reimplemented here or copied from the original tasks. +""" + +import sys, os, types, traceback, inspect, importlib, importlib.util, textwrap, json, time, multiprocessing +from typing import Any, Dict, Literal, Callable +from abc import ABC, abstractmethod +import numpy as np +from pathlib import Path + +# You must have Trace installed and importable as `opto`. +from opto import trace +from opto.trainer.guide import Guide +from opto.trace.nodes import ParameterNode + + +# ============================================================================ +# LLM4AD Base Classes (reimplemented for autonomy) +# ============================================================================ + +class Evaluation(ABC): + """Base evaluation class reimplemented from LLM4AD for benchmark tasks.""" + + def __init__( + self, + template_program: str = '', + task_description: str = '', + timeout_seconds: int | float = 30, + random_seed: int | None = None, + exec_code: bool = True, + safe_evaluate: bool = False, # Simplified - no multiprocessing by default + **kwargs + ): + """Simplified Evaluation base class. + + Args: + template_program: The template program string (not used in our implementation) + task_description: Description of the task (not used in our implementation) + timeout_seconds: Time limit for evaluation + random_seed: Random seed to set (not implemented) + exec_code: Whether to exec the code (always True in our case) + safe_evaluate: Whether to use safe evaluation (simplified, always False) + **kwargs: Additional arguments (stored but not used) + """ + self.template_program = template_program + self.task_description = task_description + self.timeout_seconds = timeout_seconds + self.random_seed = random_seed + self.exec_code = exec_code + self.safe_evaluate = safe_evaluate + self.kwargs = kwargs + + @abstractmethod + def evaluate_program(self, program_str: str, callable_func: Callable, **kwargs) -> Any | None: + """Evaluate a program. Must be implemented by subclasses. + + Args: + program_str: The program as a string + callable_func: The compiled callable function + **kwargs: Additional evaluation arguments + + Returns: + Evaluation score/result + """ + pass + + +class LLM4ADEvaluatorLoader: + """Dynamically load and instantiate LLM4AD evaluators from their original modules.""" + + def __init__(self, llm4ad_root: str, eval_module_path: str, eval_class_name: str, eval_file_path: str = None, **eval_kwargs): + self.llm4ad_root = Path(llm4ad_root) + self.eval_module_path = eval_module_path + self.eval_class_name = eval_class_name + self.eval_file_path = eval_file_path + self.eval_kwargs = eval_kwargs + self._evaluator = None + + def _load_evaluator(self): + """Load the evaluator class from LLM4AD and instantiate it.""" + if self._evaluator is not None: + return self._evaluator + + try: + # Add LLM4AD root and evaluation file directory to Python path temporarily + original_path = sys.path.copy() + if str(self.llm4ad_root) not in sys.path: + sys.path.insert(0, str(self.llm4ad_root)) + # Also add the evaluation file's directory for local imports + if self.eval_file_path: + eval_dir = str(Path(self.eval_file_path).parent) + if eval_dir not in sys.path: + sys.path.insert(0, eval_dir) + + try: + # Try importing the module normally first + try: + eval_module = importlib.import_module(self.eval_module_path) + except (ImportError, ModuleNotFoundError): + # Fallback: direct file execution for problematic paths + eval_file_path = getattr(self, 'eval_file_path', None) + if eval_file_path and Path(eval_file_path).exists(): + spec = importlib.util.spec_from_file_location( + f"eval_module_{hash(eval_file_path)}", + eval_file_path + ) + eval_module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = eval_module + spec.loader.exec_module(eval_module) + else: + raise + + # Get the evaluator class + evaluator_class = getattr(eval_module, self.eval_class_name) + + # Instantiate with provided kwargs + self._evaluator = evaluator_class(**self.eval_kwargs) + + return self._evaluator + + finally: + # Restore original Python path + sys.path = original_path + + except Exception as e: + raise RuntimeError(f"Failed to load LLM4AD evaluator {self.eval_class_name} from {self.eval_module_path}: {e}") + + def evaluate_program(self, program_str: str, callable_func, **kwargs): + """Evaluate using the LLM4AD evaluator's evaluate_program method.""" + evaluator = self._load_evaluator() + return evaluator.evaluate_program(program_str, callable_func, **kwargs) + + +class LLM4ADEvaluatorGuide(Guide): + """Trace Guide that uses LLM4AD evaluators for feedback.""" + + def __init__(self, evaluator_loader: LLM4ADEvaluatorLoader, entry_name: str, import_header: str = '', timeout: float | None = None): + self.evaluator_loader = evaluator_loader + self._entry = entry_name + self._import_header = import_header + self._timeout = timeout + + def get_feedback(self, task: str, response: str, info: Any, **kwargs): + # response is a code string (candidate). Compile it and evaluate using LLM4AD. + import signal + start = time.time() + feedback_lines = [] + + def timeout_handler(signum, frame): + raise TimeoutError("Evaluation timed out") + + try: + # Set timeout (default 30 seconds for LLM4AD evaluations) + timeout = self._timeout or 30.0 + use_signal = True + try: + signal.signal(signal.SIGALRM, timeout_handler) + signal.alarm(int(timeout)) + except ValueError as e: + # signal only works in main thread - skip timeout when in thread + if "main thread" in str(e): + use_signal = False + else: + raise # Build namespace and exec the code + ns: Dict[str, Any] = {} + header = info.get('imports', '') if isinstance(info, dict) else self._import_header + full_code = header + "\n" + response if header else response + exec(full_code, ns, ns) + + if self._entry not in ns or not callable(ns[self._entry]): + msg = f"Entry function '{self._entry}' not found after exec." + signal.alarm(0) + return -float('inf'), msg + + func = ns[self._entry] + + # Use LLM4AD's evaluate_program method + try: + score = self.evaluator_loader.evaluate_program(response, func) + if use_signal: + signal.alarm(0) + elapsed = time.time() - start + + if score is None or score == float('-inf') or score == float('inf'): + # Try to give a more informative error for infinite scores + if score == float('-inf'): + feedback_lines.append(f'LLM4AD eval returned -inf (possible constraint violation or error)') + # Instead of returning -inf, return a large negative score for optimization to work + return -1000000.0, '\n'.join(feedback_lines) + elif score == float('inf'): + feedback_lines.append(f'LLM4AD eval returned +inf (possible error in evaluation)') + return -1000000.0, '\n'.join(feedback_lines) + else: + feedback_lines.append(f'LLM4AD eval returned None') + return -1000000.0, '\n'.join(feedback_lines) + + feedback_lines.append(f'LLM4AD eval OK in {elapsed:.2f}s; score={score}') + return float(score), '\n'.join(feedback_lines) + + except (ValueError, RuntimeError, AssertionError) as eval_err: + # Handle evaluation-specific errors more gracefully + if use_signal: + signal.alarm(0) + elapsed = time.time() - start + feedback_lines.append(f'LLM4AD eval constraint violation in {elapsed:.2f}s: {eval_err}') + # Return a large negative score instead of -inf to allow optimization + return -1000000.0, '\n'.join(feedback_lines) + + except TimeoutError: + if use_signal: + signal.alarm(0) + return -1000000.0, f'Evaluation timed out after {timeout}s' + except Exception as e: + if use_signal: + signal.alarm(0) + tb = traceback.format_exc(limit=3) + return -1000000.0, f'LLM4AD eval failed: {e}\n{tb}' + + def __call__(self, task: str, response: str, info: Any, **kwargs): + return self.get_feedback(task, response, info, **kwargs) + + +def build_trace_problem_from_config( + llm4ad_root: str, + eval_module_path: str, + eval_class_name: str, + eval_file_path: str, + entry_name: str, + function_signature: str, + import_header: str, + task_description: str, + objective_text: str, + template_function: str, + eval_kwargs: dict, + **override_eval_kwargs +) -> dict: + """ + Build a Trace problem from LLM4AD task configuration. + + This is a common implementation that replaces the build_trace_problem function + that was duplicated in every converted task file. + + Returns: + dict with keys: param, guide, train_dataset, optimizer_kwargs, metadata + """ + + # 1) make the trainable code parameter + initial_code = template_function.strip() + param = trace.node(initial_code, name='__code', description=f'The code should start with: {function_signature}', trainable=True) + + # 2) Create dynamic LLM4AD evaluator loader + eval_kwargs_final = eval_kwargs.copy() + eval_kwargs_final.update(override_eval_kwargs) + + evaluator_loader = LLM4ADEvaluatorLoader( + llm4ad_root=llm4ad_root, + eval_module_path=eval_module_path, + eval_class_name=eval_class_name, + eval_file_path=eval_file_path, + **eval_kwargs_final + ) + + # 3) Create guide that uses the LLM4AD evaluator + timeout = eval_kwargs_final.get('timeout_seconds', 30) + guide = LLM4ADEvaluatorGuide(evaluator_loader, entry_name, import_header, timeout=timeout) + + # 4) dataset: minimal 1-sample dataset + train_dataset = dict( + inputs=[task_description], + infos=[{'imports': import_header, 'entry': entry_name}] + ) + + # 5) optimizer hints (objective) + optimizer_kwargs = dict( + objective=objective_text, + memory_size=10 + ) + + return dict( + param=param, + guide=guide, + train_dataset=train_dataset, + optimizer_kwargs=optimizer_kwargs, + metadata=dict( + entry=entry_name, + function_signature=function_signature, + llm4ad_eval=eval_class_name, + eval_module=eval_module_path, + llm4ad_root=llm4ad_root, + ) + ) + + +class AutonomousEvaluatorGuide(Guide): + """Trace Guide that uses benchmark (embedded) LLM4AD evaluators.""" + + def __init__(self, evaluator: Evaluation, entry_name: str, import_header: str = '', timeout: float | None = None): + self.evaluator = evaluator + self._entry = entry_name + self._import_header = import_header + self._timeout = timeout + + def get_feedback(self, task: str, response: str, info: Any, **kwargs): + # response is a code string (candidate). Compile it and evaluate using embedded evaluator. + import signal + start = time.time() + feedback_lines = [] + + def timeout_handler(signum, frame): + raise TimeoutError("Evaluation timed out") + + try: + # Set timeout (default 30 seconds for LLM4AD evaluations) + timeout = self._timeout or 30.0 + use_signal = True + try: + signal.signal(signal.SIGALRM, timeout_handler) + signal.alarm(int(timeout)) + except ValueError as e: + # signal only works in main thread - skip timeout when in thread + if "main thread" in str(e): + use_signal = False + else: + raise + + # Build namespace and exec the code + ns: Dict[str, Any] = {} + header = info.get('imports', '') if isinstance(info, dict) else self._import_header + full_code = header + "\n" + response if header else response + exec(full_code, ns, ns) + + if self._entry not in ns or not callable(ns[self._entry]): + msg = f"Entry function '{self._entry}' not found after exec." + if use_signal: + signal.alarm(0) + return -float('inf'), msg + + func = ns[self._entry] + + # Use embedded evaluator's evaluate_program method directly + score = self.evaluator.evaluate_program(response, func) + + if use_signal: + signal.alarm(0) + elapsed = time.time() - start + feedback_lines.append(f'Autonomous eval OK in {elapsed:.2f}s; score={score}') + return float(score) if score is not None else -float('inf'), '\n'.join(feedback_lines) + + except TimeoutError: + if use_signal: + signal.alarm(0) + return -float('inf'), f'Evaluation timed out after {timeout}s' + except Exception as e: + if use_signal: + signal.alarm(0) + tb = traceback.format_exc(limit=3) + return -float('inf'), f'Autonomous eval failed: {e}\n{tb}' + + def __call__(self, task: str, response: str, info: Any, **kwargs): + return self.get_feedback(task, response, info, **kwargs) + +def load_subdir_as_text(repo_id: str, subdir: str, *, skip_ext: tuple[str, ...] = (".py",), streaming: bool = False): + """ + Load files from a subdirectory in a Hugging Face dataset as text format. + + Args: + repo_id: The repository ID on Hugging Face (e.g., "CO-Bench/CO-Bench") + subdir: The subdirectory path within the dataset + skip_ext: File extensions to skip (default: (".py",)) + streaming: Whether to use streaming mode + + Returns: + A dict where keys are original filenames and values are loaded datasets + + Example: + ds = load_subdir_as_text("CO-Bench/CO-Bench", "Aircraft landing") + # Returns: {"airland1.txt": Dataset(...), "airland2.txt": Dataset(...), ...} + """ + from huggingface_hub import list_repo_files + from datasets import load_dataset + from pathlib import PurePosixPath + prefix = subdir.rstrip("/") + "/" + files = [ + f for f in list_repo_files(repo_id, repo_type="dataset") + if f.startswith(prefix) and not f.endswith(skip_ext) + ] + if not files: + raise FileNotFoundError(f"No matching files inside '{subdir}' on {repo_id}") + + # Create a mapping from sanitized split names to original filenames + def sanitize_split_name(filename): + """Convert filename to valid split name (only alphanumeric, dots, underscores)""" + import re + # Replace hyphens and other special chars with underscores + sanitized = re.sub(r'[^a-zA-Z0-9._]', '_', filename) + return sanitized + + # Build data_files dict with sanitized split names + data_files = {} + filename_mapping = {} # Maps sanitized names back to original names + + for f in files: + original_filename = PurePosixPath(f).name + sanitized_name = sanitize_split_name(original_filename) + data_files[sanitized_name] = f + filename_mapping[sanitized_name] = original_filename + + # Load the dataset + dataset = load_dataset( + repo_id, + data_files=data_files, + streaming=streaming, + ) + + # Return a dict with original filenames as keys + result = {} + for sanitized_name, original_filename in filename_mapping.items(): + result[original_filename] = dataset[sanitized_name] + + return result + + +def load_subdir_as_pickle(repo_id: str, subdir: str, *, include_subdirs: tuple[str, ...] = (), streaming: bool = False): + """ + Load pickle files from a subdirectory in a Hugging Face dataset. + + Args: + repo_id: The repository ID on Hugging Face (e.g., "CO-Bench/CO-Bench") + subdir: The subdirectory path within the dataset + include_subdirs: Tuple of subdirectory names to include (if empty, includes all) + streaming: Whether to use streaming mode + + Returns: + A dict where keys are subdirectory names and values are dicts of + {filename: loaded_pickle_content} + + Example: + result = load_subdir_as_pickle("CO-Bench/CO-Bench", "Maximal independent set", + include_subdirs=("er_test", "er_large_test")) + # Returns: {"er_test": {"file1.gpickle": graph1, ...}, "er_large_test": {...}} + """ + import pickle + from huggingface_hub import hf_hub_download, list_repo_files + + prefix = subdir.rstrip("/") + "/" + files = [ + f for f in list_repo_files(repo_id, repo_type="dataset") + if f.startswith(prefix) and f.endswith(('.pickle', '.gpickle', '.pkl')) + ] + + if not files: + raise FileNotFoundError(f"No pickle files found inside '{subdir}' on {repo_id}") + + # Organize files by subdirectory + subdirs = {} + for file_path in files: + parts = file_path.split('/') + if len(parts) >= 3: # "subdir/subsubdir/filename" + subsubdir = parts[1] # The subdirectory under main subdir + filename = parts[2] # The actual filename + + # Filter by include_subdirs if specified + if include_subdirs and subsubdir not in include_subdirs: + continue + + if subsubdir not in subdirs: + subdirs[subsubdir] = {} + + # Download and load the pickle file + try: + local_path = hf_hub_download( + repo_id=repo_id, + filename=file_path, + repo_type="dataset" + ) + + with open(local_path, "rb") as f: + pickle_content = pickle.load(f) + + subdirs[subsubdir][filename] = pickle_content + + except Exception as e: + print(f"Warning: Failed to load {file_path}: {e}") + continue + + return subdirs \ No newline at end of file diff --git a/examples/trainer_benchmark_HOWTO.md b/examples/trainer_benchmark_HOWTO.md new file mode 100644 index 00000000..b21b16a8 --- /dev/null +++ b/examples/trainer_benchmark_HOWTO.md @@ -0,0 +1,300 @@ +# Trace Benchmark Trainer - HOWTO Guide + +## Overview + +The Trace Benchmark Trainer is a comprehensive system for running optimization algorithms on algorithmic tasks derived from the [LLM4AD (Large Language Models for Algorithm Design)](https://github.com/Opti### Examples of Analysis Workflows + +### Quick Task Evaluation +```bash +# Test a new optimization approach on a simple task +python examples/trainers_benchmark.py --tasks examples/benchmark_tasks --task circle_packing --algos PrioritySearch --ps-steps 1 +``` + +### Algorithm Comparison Study +```bash +# Compare all algorithms on multiple related tasks +python examples/trainers_benchmark.py --tasks examples/benchmark_tasks --task "optimization_tsp_construct,optimization_knapsack_construct,optimization_set_cover_construct" --algos PrioritySearch,GEPA-Base,GEPA-UCB,GEPA-Beam --ps-steps 2 --gepa-iters 2 --threads 4 +``` + +### Performance Profiling +```bash +# Detailed performance analysis with extended runtime +python examples/trainers_benchmark.py --tasks examples/benchmark_tasks --task optimization_job_shop_scheduling --algos GEPA-UCB --gepa-iters 5 --gepa-train-bs 2 --threads 4 --eval-kwargs '{"timeout_seconds": 300}' +```oject. This system enables systematic evaluation and comparison of different optimization approaches on diverse algorithmic challenges. + +### What it does + +The benchmark trainer: +- **Runs optimization algorithms**: Supports PrioritySearch, GEPA-Base, GEPA-UCB, and GEPA-Beam algorithms +- **Evaluates performance**: Uses self-contained task evaluators derived from LLM4AD +- **Provides multiple outputs**: Console display, CSV results, TensorBoard logs for analysis +- **Supports parallel execution**: Multi-task and multi-algorithm runs with timeout protection +- **Enables comparison**: Systematic benchmarking across algorithms and tasks + +### Key Features + +- **60 benchmark tasks** covering optimization, machine learning, and scientific discovery +- **Timeout protection** prevents hanging on difficult tasks +- **Comprehensive logging** with CSV export and TensorBoard integration +- **Multi-task support** for batch evaluation +- **Self-contained tasks** with no external dependencies + +## Quick Start + +### Basic Usage + +Run a single task with default PrioritySearch algorithm: +```bash +python examples/trainers_benchmark.py --tasks examples/benchmark_tasks --task circle_packing +``` + +### Command Structure + +```bash +python examples/trainers_benchmark.py --tasks --task [OPTIONS] +``` + +## Main Commands and Variations + +### 1. Single Task, Single Algorithm + +**Basic run with default settings:** +```bash +python examples/trainers_benchmark.py --tasks examples/benchmark_tasks --task circle_packing +``` + +**With custom PrioritySearch parameters:** +```bash +python examples/trainers_benchmark.py --tasks examples/benchmark_tasks --task circle_packing --ps-steps 2 --ps-batches 2 +``` + +**With timeout and thread control:** +```bash +python examples/trainers_benchmark.py --tasks examples/benchmark_tasks --task circle_packing --threads 4 --eval-kwargs '{"timeout_seconds": 60}' +``` + +### 2. Single Task, Multiple Algorithms + +**Compare all algorithms on one task:** +```bash +python examples/trainers_benchmark.py --tasks examples/benchmark_tasks --task circle_packing --algos PrioritySearch,GEPA-Base,GEPA-UCB,GEPA-Beam +``` + +**Compare specific algorithms with custom settings:** +```bash +python examples/trainers_benchmark.py --tasks examples/benchmark_tasks --task optimization_knapsack_construct --algos PrioritySearch,GEPA-Beam --ps-steps 2 --gepa-iters 2 +``` + +**Run with detailed GEPA configuration:** +```bash +python examples/trainers_benchmark.py --tasks examples/benchmark_tasks --task online_bin_packing_local --algos GEPA-UCB,GEPA-Beam --gepa-train-bs 2 --gepa-pareto-subset 3 --threads 4 +``` + +### 3. Multiple Tasks, Multiple Algorithms + +**Batch evaluation on related tasks:** +```bash +python examples/trainers_benchmark.py --tasks examples/benchmark_tasks --task "circle_packing,optimization_knapsack_construct,optimization_tsp_construct" --algos PrioritySearch,GEPA-Beam +``` + +**Comprehensive benchmark run:** +```bash +python examples/trainers_benchmark.py --tasks examples/benchmark_tasks --task "circle_packing,machine_learning_acrobot,optimization_knapsack_construct" --algos PrioritySearch,GEPA-UCB,GEPA-Beam --ps-steps 2 --gepa-iters 2 --threads 4 +``` + +**Production benchmark with full configuration:** +```bash +python examples/trainers_benchmark.py --tasks examples/benchmark_tasks --task "optimization_tsp_construct,optimization_set_cover_construct,optimization_bp_1d_construct" --algos PrioritySearch,GEPA-Base,GEPA-UCB,GEPA-Beam --ps-steps 3 --gepa-iters 2 --gepa-train-bs 2 --threads 4 --eval-kwargs '{"timeout_seconds": 120}' +``` + +## Output Formats + +### 1. Console Display +Real-time progress with: +- Task loading status +- Algorithm execution progress +- Performance scores and timing +- Error messages and timeouts +- Final summary table + +### 2. CSV Export (`./results/results_YYYYMMDD_HHMMSS.csv`) +Structured data with columns: +- `timestamp`: Execution timestamp +- `task`: Task name +- `algo`: Algorithm name +- `parameters`: JSON configuration used +- `time`: Execution time in seconds +- `score`: Final performance score +- `initial_params`: Starting code/parameters +- `final_params`: Optimized code/parameters +- `log_dir`: TensorBoard log directory + +### 3. TensorBoard Logs (`./logs////`) +Interactive visualization with: +- Training curves and metrics +- Parameter evolution over time +- Algorithm-specific performance data +- Comparative analysis across runs + +**Note**: For multi-task runs, logs are organized as `./logs//`, `./logs//`, etc. + +## Available Benchmark Tasks + +The system includes **60 self-contained benchmark tasks** organized by domain: + +| Category | Tasks | Examples | +|----------|-------|----------| +| **Optimization - Basic** | 18 tasks | `circle_packing`, `online_bin_packing_local` | +| **Optimization - Constructive** | 15 tasks | `optimization_tsp_construct`, `optimization_knapsack_construct`, `optimization_set_cover_construct` | +| **Optimization - CO-Bench** | 21 tasks | `optimization_travelling_salesman_problem`, `optimization_job_shop_scheduling`, `optimization_container_loading` | +| **Machine Learning** | 5 tasks | `machine_learning_acrobot`, `machine_learning_pendulum`, `machine_learning_moon_lander` | +| **Scientific Discovery** | 1 task | `science_discovery_ode_1d` | + +### Task Categories Detail + +**Optimization - Basic:** +- `circle_packing`: Pack circles in unit square +- `online_bin_packing_local`: Online bin packing heuristics +- `optimization_admissible_set`: Admissible set priority +- `optimization_online_bin_packing`: Online bin packing strategies + +**Optimization - Constructive Heuristics:** +- `optimization_tsp_construct`: TSP node selection +- `optimization_knapsack_construct`: Knapsack item selection +- `optimization_set_cover_construct`: Set cover subset selection +- `optimization_bp_1d_construct`: 1D bin packing assignment +- `optimization_vrptw_construct`: Vehicle routing with time windows + +**Optimization - CO-Bench (Complex):** +- `optimization_travelling_salesman_problem`: Complete TSP solving +- `optimization_job_shop_scheduling`: Job shop scheduling +- `optimization_container_loading`: 3D container packing +- `optimization_maximal_independent_set`: Graph MIS problem +- `optimization_flow_shop_scheduling`: Flow shop optimization + +**Machine Learning Control:** +- `machine_learning_acrobot`: Acrobot control optimization +- `machine_learning_pendulum`: Pendulum control strategies +- `machine_learning_moon_lander`: Lunar lander control +- `machine_learning_car_mountain`: Mountain car problem + +**Scientific Discovery:** +- `science_discovery_ode_1d`: ODE system discovery + +## Command Line Parameters + +### Required Parameters +- `--tasks`: Path to benchmark tasks directory (e.g., `examples/benchmark_tasks`) +- `--task`: Task name(s), comma-separated for multiple tasks + +### Algorithm Selection +- `--algos`: Comma-separated algorithm list (default: `PrioritySearch`) + - Options: `PrioritySearch`, `GEPA-Base`, `GEPA-UCB`, `GEPA-Beam` + +### Performance Tuning +- `--threads`: Number of threads (default: 2) +- `--optimizer-kwargs`: JSON dict for optimizer configuration +- `--eval-kwargs`: JSON dict for evaluator parameters (e.g., timeout) + +### PrioritySearch Parameters +- `--ps-steps`: Search steps (default: 3) +- `--ps-batches`: Batch size (default: 2) +- `--ps-candidates`: Candidate count (default: 3) +- `--ps-proposals`: Proposal count (default: 3) +- `--ps-mem-update`: Memory update frequency (default: 2) + +### GEPA Algorithm Parameters +- `--gepa-iters`: Search iterations (default: 3) +- `--gepa-train-bs`: Training batch size (default: 2) +- `--gepa-merge-every`: Merge frequency (default: 2) +- `--gepa-pareto-subset`: Pareto subset size (default: 3) + +## Updating/Re-creating Tasks from LLM4AD + +To update the benchmark tasks from the latest LLM4AD repository: + +### 1. Clone/Update LLM4AD Repository + +```bash +git clone https://github.com/Optima-CityU/LLM4AD.git +cd LLM4AD +git pull # if already cloned +``` + +### 2. Convert Tasks to Benchmark Format + +**Convert all available tasks:** +```bash +python examples/convert_llm4ad_benchmark.py --llm4ad-root /path/to/LLM4AD --out examples/benchmark_tasks +``` + +**Convert specific task families:** +```bash +python examples/convert_llm4ad_benchmark.py --llm4ad-root /path/to/LLM4AD --out examples/benchmark_tasks --select "circle_packing,optimization,machine_learning" +``` + +**Convert only the two core tasks (minimal set):** +```bash +python examples/convert_llm4ad_benchmark.py --llm4ad-root /path/to/LLM4AD --out examples/benchmark_tasks --select "circle_packing,science_discovery/ode_1d" +``` + +### 3. Validate Converted Tasks + +```bash +python examples/trainers_benchmark_tasks_validation.py --tasks examples/benchmark_tasks --task circle_packing +``` + +### 4. Check Task Inventory + +```bash +python -c "import json; print(json.dumps([t['key'] for t in json.load(open('examples/benchmark_tasks/index.json'))], indent=2))" +``` + +## Troubleshooting + +### Common Issues + +**Task hangs during execution:** +- Increase timeout: `--eval-kwargs '{"timeout_seconds": 120}'` +- Reduce complexity: Lower `--ps-steps` or `--gepa-iters` + +**Out of memory errors:** +- Reduce `--threads` parameter +- Lower batch sizes: `--ps-batches` or `--gepa-train-bs` + +**Task not found:** +- Check task name spelling in `examples/benchmark_tasks/index.json` +- Use partial matching: `optimization_tsp` matches `optimization_tsp_construct` + +**Import errors:** +- Ensure Trace (opto) is properly installed: `pip install -e .` +- Verify benchmark tasks are properly converted + +### Performance Tips + +- **Parallel execution**: Use `--threads 4-8` for faster results +- **Batch processing**: Run multiple related tasks together +- **Timeout tuning**: Set appropriate timeouts based on task complexity +- **Algorithm selection**: Start with PrioritySearch for quick results, use GEPA for thorough optimization + +## Examples of Analysis Workflows + +### Quick Task Evaluation +```bash +# Test a new optimization approach on a simple task +python examples/trainers_benchmark.py --tasks examples/benchmark_tasks --task circle_packing --algos PrioritySearch --ps-steps 3 +``` + +### Algorithm Comparison Study +```bash +# Compare all algorithms on multiple related tasks +python examples/trainers_benchmark.py --tasks examples/benchmark_tasks --task "optimization_tsp_construct,optimization_knapsack_construct,optimization_set_cover_construct" --algos PrioritySearch,GEPA-Base,GEPA-UCB,GEPA-Beam --threads 6 +``` + +### Performance Profiling +```bash +# Detailed performance analysis with extended runtime +python examples/trainers_benchmark.py --tasks examples/benchmark_tasks --task optimization_job_shop_scheduling --algos GEPA-UCB --gepa-iters 10 --gepa-train-bs 4 --threads 8 --eval-kwargs '{"timeout_seconds": 300}' +``` + +The results can then be analyzed using the CSV output for statistical analysis or TensorBoard logs for detailed performance visualization. \ No newline at end of file diff --git a/examples/trainers_benchmark.py b/examples/trainers_benchmark.py new file mode 100644 index 00000000..bdfbaf1d --- /dev/null +++ b/examples/trainers_benchmark.py @@ -0,0 +1,348 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +'''trainers_BENCHMARK.py +Run Trace trainers on benchmark LLM4AD tasks (generated by convert_llm4ad_benchmark.py). + +This script works with benchmark task directories that contain self-contained +task modules with embedded evaluators. + +Examples: + pyt print(f"\nResults saved to {csv_path}") + if len(task_keys) == 1: + print(f"TensorBoard logs saved to ./logs/{task_keys[0]}/") + else: + print(f"TensorBoard logs saved to ./logs/ (multiple task subdirectories)") + for task_key in task_keys: + print(f" - ./logs/{task_key}/")n trainers_BENCHMARK.py --tasks ./benchmark_tasks --task circle_packing + python trainers_BENCHMARK.py --tasks ./benchmark_tasks --task online_bin_packing_local --algos PrioritySearch --ps-steps 5 +''' + +from __future__ import annotations + +import argparse, json, importlib.util, sys, time, csv, os, threading +from pathlib import Path +from typing import Dict, Any, List, Tuple +from datetime import datetime + +import numpy as np + +from opto import trace +from opto import trainer +from opto.trainer.algorithms.gepa_algorithms import GEPAAlgorithmBase, GEPAUCBSearch, GEPABeamPareto +from opto.features.priority_search import PrioritySearch as SearchAlgorithm +from opto.trainer.loggers import TensorboardLogger + + +class TimeoutError(Exception): + """Custom timeout exception""" + pass + + +def run_with_timeout(task_func, timeout_seconds=300): + """Run a task function with timeout using threading.""" + result = [None] + exception = [None] + + def target(): + try: + result[0] = task_func() + except Exception as e: + exception[0] = e + + thread = threading.Thread(target=target) + thread.daemon = True # Dies when main thread dies + thread.start() + thread.join(timeout=timeout_seconds) + + if thread.is_alive(): + # Timeout occurred - we can't actually kill the thread, but we can return timeout error + raise TimeoutError(f"Task timed out after {timeout_seconds} seconds") + + if exception[0] is not None: + raise exception[0] + + return result[0] + + +# -------------------------------- Utilities -------------------------------- + +def load_benchmark_task(task_dir: Path): + '''Load an benchmark task module from its directory.''' + init_file = task_dir / '__init__.py' + if not init_file.exists(): + raise FileNotFoundError(f"No __init__.py found in {task_dir}") + + spec = importlib.util.spec_from_file_location(task_dir.name, str(init_file)) + mod = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = mod + spec.loader.exec_module(mod) + return mod + +def pick_benchmark_task(tasks_dir: Path, task_key: str) -> Path: + ''' + Resolve an benchmark task directory by fuzzy key. + ''' + cands = [p for p in tasks_dir.iterdir() if p.is_dir()] + # exact + for p in cands: + if p.name == task_key: + return p + # substring + for p in cands: + if task_key in p.name: + return p + raise FileNotFoundError(f'No benchmark task matching: {task_key} in {tasks_dir}') + +# -------------------------------- Bench core -------------------------------- + +def run_one(mod, algo_name: str, algo_cls, *, threads: int, optimizer_kwargs: Dict[str, Any], trainer_overrides: Dict[str, Any], task_name: str) -> Tuple[float, float, Dict[str, Any]]: + '''Run a single algorithm on the benchmark task defined by `mod`.''' + bundle = mod.build_trace_problem(**trainer_overrides.get('eval_kwargs', {})) + param = bundle['param'] + guide = bundle['guide'] + ds = bundle['train_dataset'] + opt_kwargs = (bundle.get('optimizer_kwargs', {}) | (optimizer_kwargs or {})) + + # Store initial parameters for logging + initial_params = getattr(param, 'data', None) + + # Setup TensorBoard logging + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + log_dir = f'./logs/{task_name}/{algo_name}/{timestamp}' + logger = TensorboardLogger(log_dir=log_dir) + + # Algorithm params following priority_search_on_convex_fn_BENCH.py style + if algo_name == 'PrioritySearch': + params = dict( + guide=guide, + train_dataset=ds, + score_range=[-10, 10], + num_epochs=1, + num_steps=trainer_overrides.get('ps_steps', 3), + batch_size=1, + num_batches=trainer_overrides.get('ps_batches', 2), + verbose=False, + num_candidates=trainer_overrides.get('ps_candidates', 4), + num_proposals=trainer_overrides.get('ps_proposals', 4), + memory_update_frequency=trainer_overrides.get('ps_mem_update', 2), + optimizer_kwargs=opt_kwargs, + num_threads=threads, + ) + elif algo_name == 'GEPA-Base': + params = dict( + guide=guide, + train_dataset=ds, + validate_dataset=ds, + num_iters=trainer_overrides.get('gepa_iters', 3), + train_batch_size=trainer_overrides.get('gepa_train_bs', 2), + merge_every=trainer_overrides.get('gepa_merge_every', 2), + pareto_subset_size=trainer_overrides.get('gepa_pareto_subset', 4), + num_threads=threads, + optimizer_kwargs=opt_kwargs, + ) + elif algo_name == 'GEPA-UCB': + params = dict( + guide=guide, + train_dataset=ds, + num_search_iterations=trainer_overrides.get('gepa_iters', 3), + train_batch_size=trainer_overrides.get('gepa_train_bs', 2), + merge_every=trainer_overrides.get('gepa_merge_every', 2), + pareto_subset_size=trainer_overrides.get('gepa_pareto_subset', 4), + num_threads=threads, + optimizer_kwargs=opt_kwargs, + ) + elif algo_name == 'GEPA-Beam': + params = dict( + guide=guide, + train_dataset=ds, + validate_dataset=ds, + num_search_iterations=trainer_overrides.get('gepa_iters', 3), + train_batch_size=trainer_overrides.get('gepa_train_bs', 2), + merge_every=trainer_overrides.get('gepa_merge_every', 2), + pareto_subset_size=trainer_overrides.get('gepa_pareto_subset', 4), + num_threads=threads, + optimizer_kwargs=opt_kwargs, + ) + else: + raise ValueError(f'Unknown algorithm name: {algo_name}') + + # Add logger to params + params['logger'] = logger + + # The model is just the single ParameterNode (train wraps it into a Module) + start = time.time() + + # Get timeout from task configuration or use default + task_timeout = trainer_overrides.get('eval_kwargs', {}).get('timeout_seconds', 30) + # Global timeout should be much longer than individual evaluation timeout + global_timeout = max(task_timeout * 10, 300) # At least 5 minutes + + def train_task(): + trainer.train(model=param, algorithm=algo_cls, **params) # runs and mutates `param` + return param + + try: + # Use timeout wrapper to prevent hanging + param = run_with_timeout(train_task, global_timeout) + elapsed = time.time() - start + except TimeoutError as e: + elapsed = time.time() - start + print(f" Training timed out after {global_timeout}s") + # Return current state with timeout indicator + final_code = getattr(param, 'data', None) + score, fb = guide('', final_code or initial_params, ds['infos'][0]) + return (float(score) if score is not None else float('-inf')), elapsed, dict( + feedback=f"Training timed out: {str(e)}", + initial_params=initial_params, + final_params=final_code, + log_dir=log_dir, + timestamp=timestamp, + timeout_occurred=True + ) + + # Evaluate final parameter directly via the guide on one sample (same as ds) + final_code = getattr(param, 'data', None) + score, fb = guide('', final_code, ds['infos'][0]) + + return (float(score) if score is not None else float('-inf')), elapsed, dict( + feedback=fb, + initial_params=initial_params, + final_params=final_code, + log_dir=log_dir, + timestamp=timestamp + ) + +def main(): + ap = argparse.ArgumentParser(description='Run Trace trainers on benchmark LLM4AD tasks.') + ap.add_argument('--tasks', type=str, required=True, help='Folder with benchmark task directories') + ap.add_argument('--task', type=str, required=True, help='Task key(s) (e.g., "circle_packing" or "circle_packing,acrobot,knapsack" for multiple tasks)') + ap.add_argument('--algos', type=str, default='PrioritySearch', help='Comma-separated algorithms: PrioritySearch,GEPA-Base,GEPA-UCB,GEPA-Beam') + ap.add_argument('--threads', type=int, default=2, help='Num threads used by algorithms') + ap.add_argument('--optimizer-kwargs', type=str, default='', help='JSON dict to merge into optimizer_kwargs') + ap.add_argument('--eval-kwargs', type=str, default='', help='JSON dict passed into the evaluator ctor') + # Some knobs + ap.add_argument('--gepa-iters', type=int, default=3) + ap.add_argument('--gepa-train-bs', type=int, default=2) + ap.add_argument('--gepa-merge-every', type=int, default=2) + ap.add_argument('--gepa-pareto-subset', type=int, default=3) + ap.add_argument('--ps-steps', type=int, default=3) + ap.add_argument('--ps-batches', type=int, default=2) + ap.add_argument('--ps-candidates', type=int, default=3) + ap.add_argument('--ps-proposals', type=int, default=3) + ap.add_argument('--ps-mem-update', type=int, default=2) + args = ap.parse_args() + + tasks_dir = Path(args.tasks).resolve() + algo_names = [s.strip() for s in args.algos.split(',') if s.strip()] + algo_map = { + 'PrioritySearch': SearchAlgorithm, + 'GEPA-Base': GEPAAlgorithmBase, + 'GEPA-UCB': GEPAUCBSearch, + 'GEPA-Beam': GEPABeamPareto, + } + + extra_opt = json.loads(args.optimizer_kwargs) if args.optimizer_kwargs else {} + eval_kwargs = json.loads(args.eval_kwargs) if args.eval_kwargs else {} + + # Parse multiple tasks + task_keys = [key.strip() for key in args.task.split(',') if key.strip()] + + trainer_overrides = dict( + eval_kwargs=eval_kwargs, + gepa_iters=args.gepa_iters, + gepa_train_bs=args.gepa_train_bs, + gepa_merge_every=args.gepa_merge_every, + gepa_pareto_subset=args.gepa_pareto_subset, + ps_steps=args.ps_steps, + ps_batches=args.ps_batches, + ps_candidates=args.ps_candidates, + ps_proposals=args.ps_proposals, + ps_mem_update=args.ps_mem_update, + ) + + all_results = [] + + for task_key in task_keys: + print(f"\n{'='*60}") + print(f"PROCESSING TASK: {task_key}") + print(f"{'='*60}") + + try: + task_dir = pick_benchmark_task(tasks_dir, task_key) + mod = load_benchmark_task(task_dir) + except Exception as e: + print(f"Failed to load task {task_key}: {e}") + continue + + task_results = [] + + for name in algo_names: + if name not in algo_map: + print(f'[SKIP] Unknown algo: {name}') + continue + algo_cls = algo_map[name] + print(f"\n=== Running {name} on benchmark task '{task_key}' ===") + try: + score, secs, meta = run_one(mod, name, algo_cls, threads=args.threads, optimizer_kwargs=extra_opt, trainer_overrides=trainer_overrides, task_name=task_key) + print(f"{name}: score={score:.4f} time={secs:.2f}s") + result = dict(task=task_key, algo=name, score=float(score), time=float(secs), meta=meta) + task_results.append(result) + all_results.append(result) + except Exception as e: + print(f"Error running {name} on {task_key}: {e}") + result = dict(task=task_key, algo=name, score=float('-inf'), time=0.0, meta=dict(error=str(e))) + task_results.append(result) + all_results.append(result) + + # Task summary + print(f"\n--- TASK {task_key} SUMMARY ---") + for r in task_results: + if 'error' not in r['meta']: + print(f"{r['algo']:>12} | score={r['score']:.4f} | time={r['time']:.2f}s") + else: + print(f"{r['algo']:>12} | ERROR: {r['meta']['error'][:50]}...") + + results = all_results # Use all_results for final CSV output + + # Overall Summary + print('\n========== OVERALL SUMMARY ==========') + for r in results: + if 'error' not in r['meta']: + print(f"{r['task']:>20} | {r['algo']:>12} | score={r['score']:.4f} | time={r['time']:.2f}s") + else: + print(f"{r['task']:>20} | {r['algo']:>12} | ERROR") + + # CSV Logging + csv_filename = f"results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv" + os.makedirs('./results', exist_ok=True) + csv_path = f'./results/{csv_filename}' + + with open(csv_path, 'w', newline='', encoding='utf-8') as csvfile: + fieldnames = ['timestamp', 'task', 'algo', 'parameters', 'time', 'score', 'initial_params', 'final_params', 'log_dir'] + writer = csv.DictWriter(csvfile, fieldnames=fieldnames) + writer.writeheader() + + for r in results: + # Convert parameters to a single line string + params_str = json.dumps(trainer_overrides, separators=(',', ':')).replace('\\n', '\\\\n') + initial_params_str = str(r['meta'].get('initial_params', '')).replace('\\n', '\\\\n') + final_params_str = str(r['meta'].get('final_params', '')).replace('\\n', '\\\\n') + + writer.writerow({ + 'timestamp': r['meta'].get('timestamp', ''), + 'task': r.get('task', args.task), + 'algo': r['algo'], + 'parameters': params_str, + 'time': r['time'], + 'score': r['score'], + 'initial_params': initial_params_str, + 'final_params': final_params_str, + 'log_dir': r['meta'].get('log_dir', '') + }) + + print(f"\\nResults saved to {csv_path}") + print(f"TensorBoard logs saved to ./logs/{args.task}/") + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/examples/trainers_benchmark_tasks_validation.py b/examples/trainers_benchmark_tasks_validation.py new file mode 100644 index 00000000..1b0a867c --- /dev/null +++ b/examples/trainers_benchmark_tasks_validation.py @@ -0,0 +1,385 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +'''validate_benchmark_tasks.py +Quick validation script to test all benchmark tasks with minimal resources. + +This script: +1. Tests if each task can be loaded and built +2. Runs 1 optimization step with PrioritySearch +3. Times each task with a short timeout +4. Identifies which tasks work and can be optimized quickly +''' + +import os +import sys +import time +import signal +import traceback +import importlib.util +import threading +import argparse +from pathlib import Path +from contextlib import contextmanager + +# Add current directory to path for imports +sys.path.append('.') +sys.path.append('./examples/benchmark_tasks') + +from opto.features.priority_search import PrioritySearch as SearchAlgorithm +from opto import trainer + + +class TimeoutError(Exception): + """Custom timeout exception""" + pass + + +def run_with_timeout(task_func, timeout_seconds=5): + """Run a task function with timeout using threading.""" + result = [None] + exception = [None] + + def target(): + try: + result[0] = task_func() + except Exception as e: + exception[0] = e + + thread = threading.Thread(target=target) + thread.daemon = True # Dies when main thread dies + thread.start() + thread.join(timeout=timeout_seconds) + + if thread.is_alive(): + # Timeout occurred - we can't actually kill the thread, but we can return timeout error + raise TimeoutError(f"Task timed out after {timeout_seconds} seconds") + + if exception[0] is not None: + raise exception[0] + + return result[0] + + +@contextmanager +def timeout_context(seconds): + """Context manager for timeout using threading (fallback)""" + def timeout_handler(): + raise TimeoutError(f"Operation timed out after {seconds} seconds") + + timer = threading.Timer(seconds, timeout_handler) + timer.start() + try: + yield + finally: + timer.cancel() + + +def load_benchmark_task(task_dir: Path): + '''Load an benchmark task module from its directory with path isolation.''' + init_file = task_dir / '__init__.py' + if not init_file.exists(): + raise FileNotFoundError(f"No __init__.py found in {task_dir}") + + # Save current sys.path to restore later + original_path = sys.path.copy() + + try: + # Clear sys.path and add only the task directory and essential paths + sys.path.clear() + sys.path.extend([ + str(task_dir), # Task directory first for local imports + '.', # Current directory + ]) + # Add back essential system paths, but exclude any benchmark_tasks paths to prevent conflicts + original_path_filtered = [p for p in original_path if 'benchmark_tasks' not in p] + sys.path.extend(original_path_filtered) + + # Create unique module name to avoid conflicts + module_name = f"benchmark_task_{task_dir.name}_{hash(str(task_dir))}" + + # Clear any cached modules that might cause conflicts + modules_to_remove = [k for k in sys.modules.keys() if k.startswith('get_instance') or k.startswith('benchmark_task_')] + for mod in modules_to_remove: + sys.modules.pop(mod, None) + + spec = importlib.util.spec_from_file_location(module_name, str(init_file)) + mod = importlib.util.module_from_spec(spec) + sys.modules[module_name] = mod # Add to sys.modules to avoid import issues + spec.loader.exec_module(mod) + return mod + + finally: + # Restore original sys.path + sys.path.clear() + sys.path.extend(original_path) + + +def _load_task_internal(task_name, task_dir): + """Internal function for loading task (for multiprocessing)""" + print(f" Loading task module...") + mod = load_benchmark_task(task_dir) + + print(f" Building trace problem...") + problem = mod.build_trace_problem() + + # Get initial score + print(f" Getting initial evaluation...") + guide = problem['guide'] + param = problem['param'] + initial_code = param.data + task_desc = problem['train_dataset']['inputs'][0] + info = problem['train_dataset']['infos'][0] + + score, feedback = guide.get_feedback(task_desc, initial_code, info) + + return { + 'status': 'SUCCESS', + 'initial_score': score, + 'entry_function': problem['metadata']['entry'], + 'benchmark': problem['metadata']['benchmark'], + 'feedback_preview': feedback[:100] + '...' if len(feedback) > 100 else feedback + } + + +def test_task_loading(task_name, task_dir): + """Test if a task can be loaded and built""" + try: + # Use timeout for robust task loading + result = run_with_timeout(lambda: _load_task_internal(task_name, task_dir), 5) + return result + except TimeoutError as e: + return { + 'status': 'FAILED', + 'error': f'Task loading timed out after 5s', + 'error_type': 'TimeoutError' + } + except Exception as e: + return { + 'status': 'FAILED', + 'error': str(e), + 'error_type': type(e).__name__ + } + + +def _optimize_task_internal(task_name, task_dir): + """Internal function for optimization (for multiprocessing)""" + print(f" Loading for optimization...") + mod = load_benchmark_task(task_dir) + problem = mod.build_trace_problem() + + print(f" Setting up optimization...") + param = problem['param'] + guide = problem['guide'] + ds = problem['train_dataset'] + opt_kwargs = problem.get('optimizer_kwargs', {}) + + # Minimal PrioritySearch parameters + params = dict( + guide=guide, + train_dataset=ds, + score_range=[-10, 10], + num_epochs=1, + num_steps=1, # Just 1 step + batch_size=1, + num_batches=1, # Just 1 batch + verbose=False, + num_candidates=2, # Minimal candidates + num_proposals=2, # Minimal proposals + memory_update_frequency=2, + optimizer_kwargs=opt_kwargs, + num_threads=1, + ) + + print(f" Running optimization...") + start_time = time.time() + trainer.train(model=param, algorithm=SearchAlgorithm, **params) + elapsed = time.time() - start_time + + # Get final score + print(f" Getting final score...") + final_code = getattr(param, 'data', None) + final_score, _ = guide('', final_code, ds['infos'][0]) + + return { + 'status': 'OPTIMIZED', + 'optimization_time': elapsed, + 'final_score': final_score, + 'can_optimize': True + } + + +def test_task_optimization(task_name, task_dir, max_time=5): + """Test if a task can run optimization with minimal resources""" + try: + # Use timeout for robust optimization testing + result = run_with_timeout(lambda: _optimize_task_internal(task_name, task_dir), max_time) + return result + + except TimeoutError as e: + return { + 'status': 'TIMEOUT', + 'optimization_time': max_time, + 'can_optimize': False, + 'error': f'Optimization timed out after {max_time}s' + } + except Exception as e: + return { + 'status': 'OPT_FAILED', + 'can_optimize': False, + 'error': str(e), + 'error_type': type(e).__name__ + } + + +def pick_benchmark_task(tasks_dir: Path, task_key: str) -> Path: + ''' + Resolve an benchmark task directory by fuzzy key. + ''' + cands = [p for p in tasks_dir.iterdir() if p.is_dir()] + # exact + for p in cands: + if p.name == task_key: + return p + # substring + for p in cands: + if task_key in p.name: + return p + raise FileNotFoundError(f'No benchmark task matching: {task_key} in {tasks_dir}') + + +def main(): + ap = argparse.ArgumentParser(description='Validate benchmark LLM4AD tasks.') + ap.add_argument('--tasks', type=str, default='./examples/benchmark_tasks', help='Folder with benchmark task directories') + ap.add_argument('--task', type=str, help='Specific task key(s) to test, comma-separated (e.g., "circle_packing" or "optimization_bp_2d_construct,optimization_set_cover_construct")') + args = ap.parse_args() + + # Threading-based timeout doesn't need multiprocessing setup + + tasks_dir = Path(args.tasks) + if not tasks_dir.exists(): + print(f"Tasks directory not found: {tasks_dir}") + return + + # Filter tasks based on --task parameter + if args.task: + task_keys = [key.strip() for key in args.task.split(',') if key.strip()] + task_dirs = [] + for task_key in task_keys: + try: + task_dir = pick_benchmark_task(tasks_dir, task_key) + task_dirs.append(task_dir) + except FileNotFoundError as e: + print(f"Warning: {e}") + + if not task_dirs: + print("No valid tasks found!") + return + + print(f"Testing {len(task_dirs)} specific task(s): {[d.name for d in task_dirs]}") + else: + task_dirs = [d for d in tasks_dir.iterdir() if d.is_dir()] + print(f"Found {len(task_dirs)} benchmark tasks to validate") + + results = {} + working_tasks = [] + optimizable_tasks = [] + + for i, task_dir in enumerate(task_dirs, 1): + task_name = task_dir.name + print(f"\\n[{i}/{len(task_dirs)}] Testing {task_name}...") + + try: + # Test loading (has its own robust timeout) + load_result = test_task_loading(task_name, task_dir) + results[task_name] = load_result + + print(f" Loading: {load_result['status']}") + if load_result['status'] == 'SUCCESS': + print(f" Entry: {load_result['entry_function']}") + print(f" Initial score: {load_result['initial_score']}") + working_tasks.append(task_name) + + # Test optimization for all working tasks, including those with -inf scores + # The updated llm4ad_loader should handle -inf more gracefully + opt_result = test_task_optimization(task_name, task_dir) + results[task_name].update(opt_result) + print(f" Optimization: {opt_result['status']}") + if opt_result['status'] == 'OPTIMIZED': + print(f" Time: {opt_result['optimization_time']:.2f}s") + print(f" Final score: {opt_result['final_score']}") + optimizable_tasks.append(task_name) + elif opt_result['status'] in ['TIMEOUT', 'OPT_FAILED']: + print(f" Error: {opt_result.get('error', 'Unknown')}") + + # Mark as optimizable if it completed without major errors + if opt_result['status'] in ['OPTIMIZED']: + results[task_name]['can_optimize'] = True + else: + results[task_name]['can_optimize'] = False + + else: + print(f" Error: {load_result['error']}") + + except KeyboardInterrupt: + print(f"\\nKeyboard interrupt - stopping validation") + break + except Exception as e: + print(f" UNEXPECTED ERROR: {e}") + results[task_name] = { + 'status': 'FAILED', + 'error': f'Unexpected error: {str(e)}', + 'error_type': type(e).__name__ + } + + # Summary + print(f"\\n{'='*60}") + print(f"VALIDATION SUMMARY") + print(f"{'='*60}") + print(f"Total tasks: {len(task_dirs)}") + print(f"Successfully loaded: {len(working_tasks)}") + print(f"Can optimize quickly: {len(optimizable_tasks)}") + + print(f"\\nWORKING TASKS ({len(working_tasks)}):") + for task in working_tasks: + result = results[task] + score = result['initial_score'] + print(f" {task}: {result['entry_function']} (score: {score})") + + print(f"\\nQUICKLY OPTIMIZABLE TASKS ({len(optimizable_tasks)}):") + for task in optimizable_tasks: + result = results[task] + print(f" {task}: {result['optimization_time']:.2f}s (final: {result['final_score']})") + + print(f"\\nFAILED TASKS ({len(task_dirs) - len(working_tasks)}):") + failed_tasks = [name for name, result in results.items() if result['status'] == 'FAILED'] + error_summary = {} + for task in failed_tasks: + error_type = results[task].get('error_type', 'Unknown') + if error_type not in error_summary: + error_summary[error_type] = [] + error_summary[error_type].append(task) + + for error_type, tasks in error_summary.items(): + print(f" {error_type} ({len(tasks)}): {', '.join(tasks[:3])}{'...' if len(tasks) > 3 else ''}") + + # Save detailed results + import json + with open('benchmark_tasks_validation.json', 'w') as f: + # Convert any non-serializable values + serializable_results = {} + for task, result in results.items(): + serializable_result = {} + for k, v in result.items(): + if isinstance(v, (int, float, str, bool, type(None))): + serializable_result[k] = v + else: + serializable_result[k] = str(v) + serializable_results[task] = serializable_result + + json.dump(serializable_results, f, indent=2) + + print(f"\\nDetailed results saved to benchmark_tasks_validation.json") + + +if __name__ == '__main__': + main() \ No newline at end of file From 50b1544603db376ceddc828adeba952283ca5269 Mon Sep 17 00:00:00 2001 From: windweller Date: Sat, 27 Sep 2025 16:10:56 -0400 Subject: [PATCH 287/314] remove test.py --- test.py | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 test.py diff --git a/test.py b/test.py deleted file mode 100644 index 78497cb6..00000000 --- a/test.py +++ /dev/null @@ -1,16 +0,0 @@ -from opto import trace -import pickle - -@trace.model -class Dummy: - def forward(self, x): - return x * 2 - - -dummy = Dummy() -pickle.dumps(dummy) - -try: - dummy.export("dummy.py") -except Exception as e: - print("Export failed:", e) \ No newline at end of file From 2e6299135d78a5768fe593f91fe7c9d9b17b9eee Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 1 Oct 2025 13:55:56 -0400 Subject: [PATCH 288/314] cleaned up LLM model and rewrote the logic --- opto/features/flows/compose.py | 179 ++++++++++++++++----------------- opto/features/flows/types.py | 129 +----------------------- 2 files changed, 86 insertions(+), 222 deletions(-) diff --git a/opto/features/flows/compose.py b/opto/features/flows/compose.py index 6a6e6479..3ca7639b 100644 --- a/opto/features/flows/compose.py +++ b/opto/features/flows/compose.py @@ -1,13 +1,7 @@ -import pydantic -from pydantic import BaseModel, ValidationError, Field, create_model import opto.trace as trace from typing import Union, get_type_hints, Any, Dict, List, Optional from opto.utils.llm import AbstractModel, LLM -from opto.features.flows.types import TraceObject -from opto.optimizers.utils import extract_xml_like_data -import inspect -import json -import re +import random """ TracedLLM: @@ -24,21 +18,24 @@ """ -class ChatHistory(TraceObject): - def __init__(self, max_len=10, auto_summary=False): +class ChatHistory: + def __init__(self, max_len=50, auto_summary=False): """Initialize chat history for multi-turn conversation. - + Args: max_len: Maximum number of messages to keep in history auto_summary: Whether to automatically summarize old messages """ - self.messages = [] + self.messages: List[Dict[str, Union[str, trace.Node]]] = [] self.max_len = max_len self.auto_summary = auto_summary - def add(self, content, role): + def __len__(self): + return len(self.messages) + + def add(self, content: Union[trace.Node, str], role): """Add a message to history with role validation. - + Args: content: The content of the message role: The role of the message ("user" or "assistant") @@ -55,6 +52,42 @@ def add(self, content, role): self.messages.append({"role": role, "content": content}) self._trim_history() + def append(self, message: Dict[str, Union[str, trace.Node]]): + """Append a message directly to history.""" + if "role" not in message or "content" not in message: + raise ValueError("Message must have 'role' and 'content' fields.") + self.add(message["content"], message["role"]) + + def __iter__(self): + return iter(self.messages) + + def get_messages(self) -> List[Dict[str, str]]: + messages = [] + for message in self.messages: + if type(message['content']) is trace.Node: + messages.append({"role": message["role"], "content": message["content"].data}) + else: + messages.append(message) + return messages + + def get_messages_as_node(self, llm_name="") -> List[trace.Node]: + node_list = [] + for message in self.messages: + # issue: if user query is a node and has other computation attached, we can't rename it :( + if type(message['content']) is trace.Node: + node_list.append(message['content']) + else: + role = message["role"] + content = message["content"] + name = f"{llm_name}_{role}" if llm_name else f"{role}" + if role == 'user': + name += "_query" + elif role == 'assistant': + name += "_response" + node_list.append(trace.node(content, name=name)) + + return node_list + def _trim_history(self): """Trim history to max_len while preserving first user message.""" if len(self.messages) <= self.max_len: @@ -87,75 +120,6 @@ def _trim_history(self): else: self.messages = protected_messages - def get_messages(self, system_prompt: Optional[Union[str, trace.Node]] = None): - """Get messages from history. - - Args: - system_prompt: If this is passed in, then we construct a node/graph that - builds system_prompt -> chat_history graph - - Returns: - List of messages - """ - - @trace.bundle() - def converse_with_llm(system_prompt: Union[str, trace.Node]): - """The conversation history with the LLM using the given system prompt. - Args: - system_prompt: The system prompt to use for the conversation. - Returns: - The conversation history from the LLM. - """ - return self - - if system_prompt is None: - return self.messages.copy() - else: - return converse_with_llm(system_prompt) - - def __str__(self): - """String representation of the chat history. Mostly for the optimizer.""" - if len(self.messages) == 0: - return "There is no chat history so far." - - lines = [">>ChatHistory<<"] - - for msg in self.messages: - role = msg["role"] - content = msg["content"] - - if role == "user": - lines.append(f"User: {content}") - elif role == "assistant": - lines.append(f"Assistant: {content}") - - lines.append(">>End<<") - return "\n".join(lines) - - -@trace.bundle(catch_execution_error=False) -def call_llm(llm, system_prompt: str, user_prompt: str, chat_history: Optional[ChatHistory] = None, **kwargs) -> str: - """Call the LLM model. - - Args: - llm: The language model to use for generating responses. - system_prompt: the system prompt to the agent. By tuning this prompt, we can control the behavior of the agent. For example, it can be used to provide instructions to the agent (such as how to reason about the problem, how to use tools, how to answer the question), or provide in-context examples of how to solve the problem. - user_prompt: the input to the agent. It can be a query, a task, a code, etc. - chat_history: The conversation between the user and LLM so far. Can be empty. - Returns: - The response from the agent. - """ - messages = [] - if system_prompt is not None: - messages.append({"role": "system", "content": system_prompt}) - - messages.extend(chat_history.get_messages()) - messages.append({"role": "user", "content": user_prompt}) - - # TODO auto-parsing results - response = llm(messages=messages, **kwargs) - return response.choices[0].message.content - DEFAULT_SYSTEM_PROMPT_DESCRIPTION = ("the system prompt to the agent. By tuning this prompt, we can control the " "behavior of the agent. For example, it can be used to provide instructions to " @@ -169,7 +133,7 @@ class TracedLLM: def __init__(self, system_prompt: Union[str, None, trace.Node] = None, llm: AbstractModel = None, chat_history_on=False, - trainable=False): + trainable=False, model_name=None): """Initialize TracedLLM with a system prompt. Args: @@ -189,33 +153,60 @@ def __init__(self, assert isinstance(llm, AbstractModel), f"{llm} must be an instance of AbstractModel" self.llm = llm self.chat_history = ChatHistory() + self.chat_history_on = chat_history_on + self.model_name = model_name if model_name else f"TracedLLM{random.randint(1, 9999)}" + + def forward(self, user_query: str) -> str: + """We build the TraceGraph in two ways. + + If there is no chat history, then the graph would look like: + + llm = UF_LLM(system_prompt) + response = llm.chat(user_prompt) + + If there is chat history, the graph would look like: + + llm = UF_LLM(system_prompt) + response = llm.chat(user_prompt) + response_2 = llm.chat(user_prompt_2) - def forward(self, user_query: str, **kwargs) -> str: - """Main function that handles both direct call and inheritance patterns. - Args: *args: For direct pattern - single string argument **kwargs: For inheritance pattern - named input fields - + Returns: str: For direct pattern - TracedResponse: For inheritance pattern with structured output fields """ - messages = [] - messages.append({"role": "system", "content": self.system_prompt.data}) + messages = [{"role": "system", "content": self.system_prompt.data}] messages.extend(self.chat_history.get_messages()) messages.append({"role": "user", "content": user_query}) - response = self.llm(messages=messages, **kwargs) + response = self.llm(messages=messages) @trace.bundle() - def call_llm(chat_history: ChatHistory, user_query: str) -> str: + def call_llm(*args) -> str: """Call the LLM model. Args: - user_query + All the conversation history so far, starting from system prompt, to alternating user/assistant messages, ending with the current user query. + Returns: response from the LLM """ return response.choices[0].message.content - return call_llm(self.chat_history.get_messages(self.system_prompt), user_query) \ No newline at end of file + user_query_node = trace.node(user_query, name=f"{self.model_name}_user_query") + arg_list = ([self.system_prompt] + self.chat_history.get_messages_as_node(self.model_name) + + [user_query_node]) + + # save to chat history + if self.chat_history_on: + self.chat_history.add(user_query_node, role="user") + response_node = trace.node(response.choices[0].message.content, + name=f"{self.model_name}_assistant_response") + + self.chat_history.add(response_node, role="assistant") + + return call_llm(*arg_list) + + def chat(self, user_query: str) -> str: + return self.forward(user_query) \ No newline at end of file diff --git a/opto/features/flows/types.py b/opto/features/flows/types.py index e79fafd7..4196b926 100644 --- a/opto/features/flows/types.py +++ b/opto/features/flows/types.py @@ -7,131 +7,4 @@ class TraceObject: def __str__(self): # Any subclass that inherits this will be friendly to the optimizer - raise NotImplementedError("Subclasses must implement __str__") - -class TracedInput(BaseModel): - """Pydantic model for input fields in TracedLLM inheritance pattern.""" - model_config = ConfigDict(arbitrary_types_allowed=True) - - description: Optional[str] = "Input specified by the user for the LLM." - required: bool = True - - -class TracedOutput(BaseModel): - """Pydantic model for output fields in TracedLLM inheritance pattern.""" - model_config = ConfigDict(arbitrary_types_allowed=True) - - description: Optional[str] = "Output from the LLM." - parser: Optional[Union[Callable[[str], Any], str]] = None # Can be function or regex pattern - default_value: Optional[Any] = None - required: bool = True - - def extract_from_text(self, text: str, field_type: Type) -> Any: - """Extract value from text using parser (function or regex) or default logic.""" - if self.parser: - if callable(self.parser): - # Parser is a function - try: - return self.parser(text) - except: - return self.default_value - elif isinstance(self.parser, str): - # Parser is a regex pattern - match = re.search(self.parser, text, re.IGNORECASE) - if match: - # Find the first non-None group or use group(0) - extracted = None - for group in match.groups(): - if group is not None: - extracted = group - break - if extracted is None: - extracted = match.group(0) - return self._convert_to_type(extracted, field_type) - else: - return self.default_value - - # Fall back to default extraction logic - return self._default_extract(text, field_type) - - def _convert_to_type(self, value: str, field_type: Type) -> Any: - """Convert extracted string to target type.""" - # Default type conversion - if field_type == int: - numbers = re.findall(r'-?\d+', value) - return int(numbers[0]) if numbers else self.default_value - elif field_type == float: - numbers = re.findall(r'-?\d+\.?\d*', value) - return float(numbers[0]) if numbers else self.default_value - elif field_type == bool: - return self._parse_boolean(value) - elif field_type == list: - try: - return json.loads(value) - except: - return [item.strip() for item in value.split(',')] - else: - return value - - def _default_extract(self, text: str, field_type: Type) -> Any: - """Default extraction logic.""" - # If custom parser failed, return default value - return self.default_value - - def _parse_boolean(self, text: str) -> bool: - """Parse boolean from text.""" - text_lower = text.lower().strip() - positive_words = ['true', 'yes', 'correct', 'positive', 'definitely', '1'] - negative_words = ['false', 'no', 'incorrect', 'negative', 'way', '0'] - - if any(word in text_lower for word in positive_words): - return True - elif any(word in text_lower for word in negative_words): - return False - else: - return self.default_value if self.default_value is not None else True - - -class DynamicModelMixin: - """Mixin to provide dynamic model creation capabilities.""" - - @classmethod - def create_response_model(cls, field_definitions: Dict[str, tuple]) -> Type[BaseModel]: - """Create a dynamic Pydantic model from field definitions. - - Args: - field_definitions: Dict mapping field names to (type, TracedOutput) tuples - - Returns: - Dynamically created Pydantic model class - """ - pydantic_fields = {} - - for field_name, (field_type, traced_output) in field_definitions.items(): - # Create Pydantic field with metadata from TracedOutput - field_kwargs = {} - if traced_output.description: - field_kwargs['description'] = traced_output.description - if not traced_output.required: - field_kwargs['default'] = traced_output.default_value - - pydantic_fields[field_name] = (field_type, Field(**field_kwargs)) - - # Create the dynamic model - return create_model(f"{cls.__name__}Response", **pydantic_fields) - - @classmethod - def create_input_model(cls, field_definitions: Dict[str, tuple]) -> Type[BaseModel]: - """Create a dynamic input validation model.""" - pydantic_fields = {} - - for field_name, (field_type, traced_input) in field_definitions.items(): - field_kwargs = {} - if traced_input.description: - field_kwargs['description'] = traced_input.description - if not traced_input.required: - field_kwargs['default'] = None - - pydantic_fields[field_name] = (field_type, Field(**field_kwargs)) - - return create_model(f"{cls.__name__}Input", **pydantic_fields) + raise NotImplementedError("Subclasses must implement __str__") \ No newline at end of file From f238335ff5bc5b72d9fec3ad80919b682d6cb6e4 Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 1 Oct 2025 15:26:27 -0400 Subject: [PATCH 289/314] add a test function (that doesn't do much) --- opto/features/flows/compose.py | 4 +- tests/features_tests/test_flows_compose.py | 80 ++++++++++++++++++---- 2 files changed, 69 insertions(+), 15 deletions(-) diff --git a/opto/features/flows/compose.py b/opto/features/flows/compose.py index 3ca7639b..42268ab9 100644 --- a/opto/features/flows/compose.py +++ b/opto/features/flows/compose.py @@ -26,7 +26,7 @@ def __init__(self, max_len=50, auto_summary=False): max_len: Maximum number of messages to keep in history auto_summary: Whether to automatically summarize old messages """ - self.messages: List[Dict[str, Union[str, trace.Node]]] = [] + self.messages: List[Dict[str, Any]] = [] self.max_len = max_len self.auto_summary = auto_summary @@ -52,7 +52,7 @@ def add(self, content: Union[trace.Node, str], role): self.messages.append({"role": role, "content": content}) self._trim_history() - def append(self, message: Dict[str, Union[str, trace.Node]]): + def append(self, message: Dict[str, Any]): """Append a message directly to history.""" if "role" not in message or "content" not in message: raise ValueError("Message must have 'role' and 'content' fields.") diff --git a/tests/features_tests/test_flows_compose.py b/tests/features_tests/test_flows_compose.py index 3571ca6a..65e51cba 100644 --- a/tests/features_tests/test_flows_compose.py +++ b/tests/features_tests/test_flows_compose.py @@ -5,20 +5,74 @@ # Mock LLM at module level to ensure no real API calls @pytest.fixture(autouse=True) def mock_llm_globally(): - """Automatically mock all LLM calls for all tests.""" - with patch('opto.utils.llm.LLM') as mock_llm_class: - # Create a mock LLM instance that doesn't require API keys - mock_llm_instance = Mock() - mock_llm_instance.return_value = Mock() - mock_llm_class.return_value = mock_llm_instance - yield mock_llm_instance + """Automatically mock all LLM calls for all tests with structured responses. + The dummy LLM returns an object with `.choices[0].message.content` so callers + like `TracedLLM` and optimizers can read a string without hitting a network. + You can override `dummy_llm.responses` in tests to control returned strings. + """ + class _Choice: + def __init__(self, content): + self.message = type('m', (), {'content': content}) -@pytest.fixture(autouse=True) -def mock_trace_operators(): - """Mock trace operators to prevent any external dependencies.""" - with patch('opto.trace.operators.call_llm') as mock_call_llm: - mock_call_llm.return_value = "Mocked LLM response" - yield mock_call_llm + class DummyLLM: + def __init__(self): + # Default to an endless stream of the same mocked response + self.responses = ["Mocked LLM response"] + self._idx = 0 + + def __call__(self, *args, **kwargs): + # Return a response-like object with choices + if self._idx >= len(self.responses): + # Repeat last if we run out + content = self.responses[-1] + else: + content = self.responses[self._idx] + self._idx += 1 + return type('r', (), {'choices': [_Choice(content)]}) + + dummy_llm = DummyLLM() + with patch('opto.utils.llm.LLM', return_value=dummy_llm): + yield dummy_llm + + + +def test_tracedllm_and_optoprimev2_prompt_with_mock_llm(mock_llm_globally): + # Arrange custom fake responses for three chat turns + mock_llm_globally.responses = [ + "I can't access your location.", + "Noted: you're in Paris.", + "You are in Paris.", + ] + + from opto.features.flows.compose import TracedLLM + from opto.optimizers.optoprime_v2 import OptoPrimeV2, OptimizerPromptSymbolSet2 + + # Act: run the user-provided flow without any real LLM calls + traced_llm = TracedLLM(system_prompt="Be a friendly personal assistant.", trainable=True, chat_history_on=True) + output = traced_llm("Where am I?") + output2 = traced_llm("I'm in Paris.") + output3 = traced_llm("Where am I?") + + optimizer = OptoPrimeV2( + traced_llm.parameters(), + use_json_object_format=False, + ignore_extraction_error=False, + include_example=False, + optimizer_prompt_symbol_set=OptimizerPromptSymbolSet2(), + memory_size=5, + initial_var_char_limit=500, + ) + + optimizer.zero_feedback() + optimizer.backward(output3, "Don't mention that you don't have the ability to locate my location if I tell you where I am.") + + summary = optimizer.summarize() + part1, part2 = optimizer.construct_prompt(summary) + + part1 = optimizer.replace_symbols(part1, optimizer.prompt_symbols) + part2 = optimizer.replace_symbols(part2, optimizer.prompt_symbols) + assert "Your response:" in part2 + print(part2) From b6d41d0ce4afe0c290a0ca75a0dc7b31fe7668a7 Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 1 Oct 2025 19:55:59 -0400 Subject: [PATCH 290/314] modify bundle to allow more user customization. Added global counter. Fixed graph missing link issue. --- opto/features/flows/compose.py | 23 +++++++++++++---------- opto/trace/bundle.py | 13 +++++++++++-- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/opto/features/flows/compose.py b/opto/features/flows/compose.py index 42268ab9..5d6bbbb7 100644 --- a/opto/features/flows/compose.py +++ b/opto/features/flows/compose.py @@ -1,7 +1,7 @@ import opto.trace as trace from typing import Union, get_type_hints, Any, Dict, List, Optional from opto.utils.llm import AbstractModel, LLM -import random +import contextvars """ TracedLLM: @@ -17,6 +17,7 @@ response = llm("Hello, what's the weather in France today?") """ +USED_TracedLLM = contextvars.ContextVar('USED_TracedLLM', default=list()) class ChatHistory: def __init__(self, max_len=50, auto_summary=False): @@ -64,7 +65,7 @@ def __iter__(self): def get_messages(self) -> List[Dict[str, str]]: messages = [] for message in self.messages: - if type(message['content']) is trace.Node: + if isinstance(message['content'], trace.Node): messages.append({"role": message["role"], "content": message["content"].data}) else: messages.append(message) @@ -73,8 +74,8 @@ def get_messages(self) -> List[Dict[str, str]]: def get_messages_as_node(self, llm_name="") -> List[trace.Node]: node_list = [] for message in self.messages: - # issue: if user query is a node and has other computation attached, we can't rename it :( - if type(message['content']) is trace.Node: + # If user query is a node and has other computation attached, we can't rename it + if isinstance(message['content'], trace.Node): node_list.append(message['content']) else: role = message["role"] @@ -154,7 +155,10 @@ def __init__(self, self.llm = llm self.chat_history = ChatHistory() self.chat_history_on = chat_history_on - self.model_name = model_name if model_name else f"TracedLLM{random.randint(1, 9999)}" + + current_llm_sessions = USED_TracedLLM.get() + self.model_name = model_name if model_name else f"TracedLLM{len(current_llm_sessions)}" + current_llm_sessions.append(1) # just a marker def forward(self, user_query: str) -> str: """We build the TraceGraph in two ways. @@ -183,7 +187,7 @@ def forward(self, user_query: str) -> str: response = self.llm(messages=messages) - @trace.bundle() + @trace.bundle(output_name="TracedLLM_response") def call_llm(*args) -> str: """Call the LLM model. Args: @@ -198,15 +202,14 @@ def call_llm(*args) -> str: arg_list = ([self.system_prompt] + self.chat_history.get_messages_as_node(self.model_name) + [user_query_node]) + response_node = call_llm(*arg_list) + # save to chat history if self.chat_history_on: self.chat_history.add(user_query_node, role="user") - response_node = trace.node(response.choices[0].message.content, - name=f"{self.model_name}_assistant_response") - self.chat_history.add(response_node, role="assistant") - return call_llm(*arg_list) + return response_node def chat(self, user_query: str) -> str: return self.forward(user_query) \ No newline at end of file diff --git a/opto/trace/bundle.py b/opto/trace/bundle.py index 6d5fa5c8..4d7b2b99 100644 --- a/opto/trace/bundle.py +++ b/opto/trace/bundle.py @@ -40,6 +40,7 @@ def bundle( allow_external_dependencies=False, overwrite_python_recursion=False, projections=None, + output_name=None ): """Wrap a function as a FunModule which returns node objects. @@ -55,6 +56,7 @@ def bundle( allow_external_dependencies (bool, optional): Whether to allow external dependencies. Defaults to False. overwrite_python_recursion (bool, optional): Whether to overwrite Python recursion behavior. Defaults to False. projections (List[Projection], optional): List of projections to be used in updating trainable parameter. Defaults to None. + output_name (str, optional): Specify/override the name of the output node. If None, the name is derived from the function name. Returns: FunModule: The wrapped function that returns node objects. @@ -73,6 +75,7 @@ def decorator(fun): overwrite_python_recursion=overwrite_python_recursion, _ldict=prev_f_locals, # Get the locals of the calling function projections=projections, + output_name=output_name ) return fun_module @@ -128,6 +131,7 @@ def __init__( allow_external_dependencies=False, overwrite_python_recursion=False, projections=None, + output_name=None, _ldict=None, ): @@ -146,7 +150,7 @@ def __init__( self.info = dict( # TODO explain the info dict # info about the decorated function fun=None, # to be defined at run time - fun_name=fun.__qualname__, + fun_name=self.filter_fun_name(fun.__qualname__), _fun_name=fun.__name__, # this saves the pure fun name (without the class name); this should not be modified.map= doc=inspect.cleandoc(docstring) if docstring is not None else "", signature=inspect.signature(fun), @@ -175,6 +179,8 @@ def __init__( self.allow_external_dependencies = allow_external_dependencies self.parameter = None self.overwrite_python_recursion = overwrite_python_recursion + self.output_name = output_name + if trainable: # trainable code uses exec which has an effect of overwrite_python_recursion==True. self.overwrite_python_recursion = True @@ -261,6 +267,9 @@ def fun(self, *args, **kwargs): def name(self): return get_op_name(self.description) + def filter_fun_name(self, fun_name): + return fun_name.replace(r".", "") + def _wrap_inputs(self, fun, args, kwargs): """Wrap the inputs to a function as nodes when they're not. @@ -602,7 +611,7 @@ def wrap( self.info["fun_name"] = "eval" else: description = self.description - name = self.name + name = self.name if self.output_name is None else self.output_name info = self.info.copy() if isinstance(output, Exception): e_node = ExceptionNode( From 17896661149fecffc18527b65c87a8614def2eba Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 1 Oct 2025 19:59:42 -0400 Subject: [PATCH 291/314] cleared up incorrect docstring --- opto/features/flows/compose.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/opto/features/flows/compose.py b/opto/features/flows/compose.py index 5d6bbbb7..96b8a9be 100644 --- a/opto/features/flows/compose.py +++ b/opto/features/flows/compose.py @@ -131,6 +131,20 @@ def _trim_history(self): @trace.model class TracedLLM: + """ + This high-level model provides an easy-to-use interface for LLM calls with system prompts and optional chat history. + + Python usage patterns: + + llm = UF_LLM(system_prompt) + response = llm.chat(user_prompt) + response_2 = llm.chat(user_prompt_2) + + The underlying Trace Graph: + TracedLLM_response0 = TracedLLM.forward.call_llm(args_0=system_prompt0, args_1=TracedLLM0_user_query0) + TracedLLM_response1 = TracedLLM.forward.call_llm(args_0=system_prompt0, args_1=TracedLLM0_user_query0, args_2=TracedLLM_response0, args_3=TracedLLM0_user_query1) + TracedLLM_response2 = TracedLLM.forward.call_llm(args_0=system_prompt0, args_1=TracedLLM0_user_query0, args_2=TracedLLM_response0, args_3=TracedLLM0_user_query1, args_4=TracedLLM_response1, args_5=TracedLLM0_user_query2) + """ def __init__(self, system_prompt: Union[str, None, trace.Node] = None, llm: AbstractModel = None, chat_history_on=False, @@ -161,22 +175,10 @@ def __init__(self, current_llm_sessions.append(1) # just a marker def forward(self, user_query: str) -> str: - """We build the TraceGraph in two ways. - - If there is no chat history, then the graph would look like: - - llm = UF_LLM(system_prompt) - response = llm.chat(user_prompt) - - If there is chat history, the graph would look like: - - llm = UF_LLM(system_prompt) - response = llm.chat(user_prompt) - response_2 = llm.chat(user_prompt_2) + """This function takes user_query as input, and returns the response from the LLM, with the system prompt prepended. Args: - *args: For direct pattern - single string argument - **kwargs: For inheritance pattern - named input fields + user_query: The user query to send to the LLM Returns: str: For direct pattern From df6a0c4cb8bf6a3df8dd91e4737a809d5f72616e Mon Sep 17 00:00:00 2001 From: doxav Date: Thu, 2 Oct 2025 18:51:28 +0200 Subject: [PATCH 292/314] GEPA moved to features/gepa (tests/examples adapted) --- .../circle_packing/__init__.py | 265 ------ .../benchmark_tasks/circle_packing/run_eoh.py | 33 - examples/benchmark_tasks/index.json | 805 ------------------ .../machine_learning_acrobot/__init__.py | 175 ---- .../machine_learning_acrobot/paras.yaml | 3 - .../machine_learning_acrobot/test.py | 46 - .../machine_learning_car_mountain/__init__.py | 162 ---- .../machine_learning_car_mountain/paras.yaml | 3 - .../q-learning.py | 123 --- .../machine_learning_car_mountain/test.py | 167 ---- .../__init__.py | 167 ---- .../paras.yaml | 3 - .../test.py | 64 -- .../machine_learning_moon_lander/__init__.py | 196 ----- .../machine_learning_moon_lander/paras.yaml | 3 - .../machine_learning_moon_lander/test.py | 53 -- .../machine_learning_pendulum/__init__.py | 195 ----- .../machine_learning_pendulum/paras.yaml | 3 - .../machine_learning_pendulum/test.py | 47 - .../online_bin_packing_local/__init__.py | 164 ---- .../generate_weibull_instances.py | 36 - .../online_bin_packing_local/run_eoh.py | 33 - .../optimization_admissible_set/__init__.py | 256 ------ .../optimization_admissible_set/paras.yaml | 4 - .../optimization_aircraft_landing/__init__.py | 450 ---------- .../optimization_aircraft_landing/paras.yaml | 2 - .../__init__.py | 327 ------- .../paras.yaml | 2 - .../__init__.py | 388 --------- .../paras.yaml | 2 - .../optimization_bp_1d/__init__.py | 298 ------- .../optimization_bp_1d/paras.yaml | 2 - .../optimization_bp_1d_construct/__init__.py | 289 ------- .../get_instance.py | 55 -- .../optimization_bp_1d_construct/paras.yaml | 2 - .../optimization_bp_2d_construct/__init__.py | 344 -------- .../get_instance.py | 40 - .../optimization_bp_2d_construct/paras.yaml | 2 - .../__init__.py | 398 --------- .../paras.yaml | 2 - .../optimization_cflp_construct/__init__.py | 310 ------- .../get_instance.py | 65 -- .../optimization_cflp_construct/paras.yaml | 2 - .../__init__.py | 310 ------- .../paras.yaml | 2 - .../__init__.py | 399 --------- .../paras.yaml | 2 - .../__init__.py | 359 -------- .../paras.yaml | 2 - .../__init__.py | 387 --------- .../optimization_container_loading/paras.yaml | 2 - .../__init__.py | 456 ---------- .../paras.yaml | 2 - .../__init__.py | 341 -------- .../paras.yaml | 2 - .../optimization_crew_scheduling/__init__.py | 369 -------- .../optimization_crew_scheduling/paras.yaml | 2 - .../optimization_cvrp_construct/__init__.py | 328 ------- .../get_instance.py | 50 -- .../optimization_cvrp_construct/paras.yaml | 2 - .../__init__.py | 326 ------- .../paras.yaml | 2 - .../__init__.py | 372 -------- .../paras.yaml | 2 - .../__init__.py | 388 --------- .../paras.yaml | 2 - .../__init__.py | 340 -------- .../paras.yaml | 2 - .../optimization_graph_colouring/__init__.py | 372 -------- .../optimization_graph_colouring/paras.yaml | 2 - .../__init__.py | 564 ------------ .../paras.yaml | 2 - .../__init__.py | 307 ------- .../paras.yaml | 2 - .../optimization_jssp_construct/__init__.py | 289 ------- .../get_instance.py | 43 - .../optimization_jssp_construct/paras.yaml | 2 - .../__init__.py | 271 ------ .../get_instance.py | 41 - .../paras.yaml | 2 - .../__init__.py | 261 ------ .../paras.yaml | 2 - .../__init__.py | 629 -------------- .../paras.yaml | 2 - .../__init__.py | 496 ----------- .../paras.yaml | 2 - .../__init__.py | 311 ------- .../generate_weibull_instances.py | 36 - .../paras.yaml | 2 - .../__init__.py | 194 ----- .../generate_weibull_instances.py | 36 - .../paras.yaml | 2 - .../__init__.py | 307 ------- .../paras.yaml | 2 - .../optimization_ovrp_construct/__init__.py | 299 ------- .../get_instance.py | 50 -- .../optimization_ovrp_construct/paras.yaml | 2 - .../__init__.py | 357 -------- .../paras.yaml | 2 - .../__init__.py | 367 -------- .../paras.yaml | 2 - .../__init__.py | 337 -------- .../paras.yaml | 2 - .../__init__.py | 334 -------- .../paras.yaml | 2 - .../__init__.py | 400 --------- .../paras.yaml | 2 - .../__init__.py | 442 ---------- .../paras.yaml | 2 - .../optimization_pymoo_moead/__init__.py | 216 ----- .../optimization_pymoo_moead/get_instance.py | 87 -- .../optimization_pymoo_moead/paras.yaml | 2 - .../optimization_qap_construct/__init__.py | 293 ------- .../get_instance.py | 48 -- .../optimization_qap_construct/paras.yaml | 2 - .../__init__.py | 353 -------- .../paras.yaml | 2 - .../__init__.py | 296 ------- .../get_instance.py | 53 -- .../paras.yaml | 2 - .../optimization_set_cover_construct/test.py | 125 --- .../optimization_set_covering/__init__.py | 497 ----------- .../optimization_set_covering/paras.yaml | 2 - .../optimization_set_partitioning/__init__.py | 389 --------- .../optimization_set_partitioning/paras.yaml | 2 - .../__init__.py | 334 -------- .../paras.yaml | 2 - .../optimization_tsp_construct/__init__.py | 259 ------ .../get_instance.py | 16 - .../optimization_tsp_construct/paras.yaml | 2 - .../optimization_tsp_gls_2O/__init__.py | 184 ---- .../optimization_tsp_gls_2O/get_instance.py | 23 - .../optimization_tsp_gls_2O/gls.py | 226 ----- .../optimization_tsp_gls_2O/paras.yaml | 2 - .../__init__.py | 349 -------- .../paras.yaml | 2 - .../__init__.py | 376 -------- .../paras.yaml | 2 - .../__init__.py | 469 ---------- .../paras.yaml | 2 - .../optimization_vrptw_construct/__init__.py | 279 ------ .../get_instance.py | 64 -- .../optimization_vrptw_construct/paras.yaml | 2 - .../science_discovery_ode_1d/__init__.py | 258 ------ .../science_discovery_ode_1d/paras.yaml | 3 - .../strogatz_equations.py | 223 ----- examples/convert_llm4ad_benchmark.py | 460 ---------- examples/llm4ad_loader.py | 492 ----------- .../priority_search_on_convex_fn_BENCH.py | 11 +- examples/trainers_benchmark.py | 2 +- opto/features/gepa/__init__.py | 9 + .../gepa}/gepa_algorithms.py | 2 +- .../test_gepa_benchmark.py | 2 +- 153 files changed, 14 insertions(+), 23624 deletions(-) delete mode 100644 examples/benchmark_tasks/circle_packing/__init__.py delete mode 100644 examples/benchmark_tasks/circle_packing/run_eoh.py delete mode 100644 examples/benchmark_tasks/index.json delete mode 100644 examples/benchmark_tasks/machine_learning_acrobot/__init__.py delete mode 100644 examples/benchmark_tasks/machine_learning_acrobot/paras.yaml delete mode 100644 examples/benchmark_tasks/machine_learning_acrobot/test.py delete mode 100644 examples/benchmark_tasks/machine_learning_car_mountain/__init__.py delete mode 100644 examples/benchmark_tasks/machine_learning_car_mountain/paras.yaml delete mode 100644 examples/benchmark_tasks/machine_learning_car_mountain/q-learning.py delete mode 100644 examples/benchmark_tasks/machine_learning_car_mountain/test.py delete mode 100644 examples/benchmark_tasks/machine_learning_car_mountain_continue/__init__.py delete mode 100644 examples/benchmark_tasks/machine_learning_car_mountain_continue/paras.yaml delete mode 100644 examples/benchmark_tasks/machine_learning_car_mountain_continue/test.py delete mode 100644 examples/benchmark_tasks/machine_learning_moon_lander/__init__.py delete mode 100644 examples/benchmark_tasks/machine_learning_moon_lander/paras.yaml delete mode 100644 examples/benchmark_tasks/machine_learning_moon_lander/test.py delete mode 100644 examples/benchmark_tasks/machine_learning_pendulum/__init__.py delete mode 100644 examples/benchmark_tasks/machine_learning_pendulum/paras.yaml delete mode 100644 examples/benchmark_tasks/machine_learning_pendulum/test.py delete mode 100644 examples/benchmark_tasks/online_bin_packing_local/__init__.py delete mode 100644 examples/benchmark_tasks/online_bin_packing_local/generate_weibull_instances.py delete mode 100644 examples/benchmark_tasks/online_bin_packing_local/run_eoh.py delete mode 100644 examples/benchmark_tasks/optimization_admissible_set/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_admissible_set/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_aircraft_landing/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_aircraft_landing/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_assignment_problem/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_assignment_problem/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_assortment_problem/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_assortment_problem/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_bp_1d/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_bp_1d/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_bp_1d_construct/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_bp_1d_construct/get_instance.py delete mode 100644 examples/benchmark_tasks/optimization_bp_1d_construct/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_bp_2d_construct/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_bp_2d_construct/get_instance.py delete mode 100644 examples/benchmark_tasks/optimization_bp_2d_construct/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_capacitated_warehouse_location/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_capacitated_warehouse_location/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_cflp_construct/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_cflp_construct/get_instance.py delete mode 100644 examples/benchmark_tasks/optimization_cflp_construct/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_common_due_date_scheduling/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_common_due_date_scheduling/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_constrained_guillotine_cutting/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_constrained_guillotine_cutting/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_constrained_non_guillotine_cutting/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_constrained_non_guillotine_cutting/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_container_loading/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_container_loading/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_container_loading_with_weight_restrictions/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_container_loading_with_weight_restrictions/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_corporate_structuring/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_corporate_structuring/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_crew_scheduling/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_crew_scheduling/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_cvrp_construct/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_cvrp_construct/get_instance.py delete mode 100644 examples/benchmark_tasks/optimization_cvrp_construct/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_equitable_partitioning_problem/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_equitable_partitioning_problem/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_euclidean_steiner_problem/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_euclidean_steiner_problem/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_flow_shop_scheduling/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_flow_shop_scheduling/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_generalised_assignment_problem/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_generalised_assignment_problem/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_graph_colouring/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_graph_colouring/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_hybrid_reentrant_shop_scheduling/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_hybrid_reentrant_shop_scheduling/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_job_shop_scheduling/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_job_shop_scheduling/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_jssp_construct/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_jssp_construct/get_instance.py delete mode 100644 examples/benchmark_tasks/optimization_jssp_construct/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_knapsack_construct/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_knapsack_construct/get_instance.py delete mode 100644 examples/benchmark_tasks/optimization_knapsack_construct/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_maximal_independent_set/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_maximal_independent_set/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_multi_demand_multidimensional_knapsack_problem/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_multi_demand_multidimensional_knapsack_problem/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_multidimensional_knapsack_problem/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_multidimensional_knapsack_problem/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_online_bin_packing/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_online_bin_packing/generate_weibull_instances.py delete mode 100644 examples/benchmark_tasks/optimization_online_bin_packing/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_online_bin_packing_2O/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_online_bin_packing_2O/generate_weibull_instances.py delete mode 100644 examples/benchmark_tasks/optimization_online_bin_packing_2O/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_open_shop_scheduling/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_open_shop_scheduling/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_ovrp_construct/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_ovrp_construct/get_instance.py delete mode 100644 examples/benchmark_tasks/optimization_ovrp_construct/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_p_median_capacitated/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_p_median_capacitated/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_p_median_uncapacitated/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_p_median_uncapacitated/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_packing_unequal_circles/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_packing_unequal_circles/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_packing_unequal_circles_area/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_packing_unequal_circles_area/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_packing_unequal_rectangles_and_squares/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_packing_unequal_rectangles_and_squares/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_packing_unequal_rectangles_and_squares_area/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_packing_unequal_rectangles_and_squares_area/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_pymoo_moead/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_pymoo_moead/get_instance.py delete mode 100644 examples/benchmark_tasks/optimization_pymoo_moead/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_qap_construct/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_qap_construct/get_instance.py delete mode 100644 examples/benchmark_tasks/optimization_qap_construct/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_resource_constrained_shortest_path/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_resource_constrained_shortest_path/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_set_cover_construct/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_set_cover_construct/get_instance.py delete mode 100644 examples/benchmark_tasks/optimization_set_cover_construct/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_set_cover_construct/test.py delete mode 100644 examples/benchmark_tasks/optimization_set_covering/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_set_covering/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_set_partitioning/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_set_partitioning/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_travelling_salesman_problem/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_travelling_salesman_problem/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_tsp_construct/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_tsp_construct/get_instance.py delete mode 100644 examples/benchmark_tasks/optimization_tsp_construct/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_tsp_gls_2O/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_tsp_gls_2O/get_instance.py delete mode 100644 examples/benchmark_tasks/optimization_tsp_gls_2O/gls.py delete mode 100644 examples/benchmark_tasks/optimization_tsp_gls_2O/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_uncapacitated_warehouse_location/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_uncapacitated_warehouse_location/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_unconstrained_guillotine_cutting/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_unconstrained_guillotine_cutting/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_vehicle_routing_period_routing/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_vehicle_routing_period_routing/paras.yaml delete mode 100644 examples/benchmark_tasks/optimization_vrptw_construct/__init__.py delete mode 100644 examples/benchmark_tasks/optimization_vrptw_construct/get_instance.py delete mode 100644 examples/benchmark_tasks/optimization_vrptw_construct/paras.yaml delete mode 100644 examples/benchmark_tasks/science_discovery_ode_1d/__init__.py delete mode 100644 examples/benchmark_tasks/science_discovery_ode_1d/paras.yaml delete mode 100644 examples/benchmark_tasks/science_discovery_ode_1d/strogatz_equations.py delete mode 100644 examples/convert_llm4ad_benchmark.py delete mode 100644 examples/llm4ad_loader.py create mode 100644 opto/features/gepa/__init__.py rename opto/{trainer/algorithms => features/gepa}/gepa_algorithms.py (99%) diff --git a/examples/benchmark_tasks/circle_packing/__init__.py b/examples/benchmark_tasks/circle_packing/__init__.py deleted file mode 100644 index 5f9c6f9d..00000000 --- a/examples/benchmark_tasks/circle_packing/__init__.py +++ /dev/null @@ -1,265 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: circle_packing -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -from __future__ import annotations - -from typing import Any -import numpy as np -# from template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\nimport math\ndef pack_circles(n: int) -> np.ndarray:\n """\n Pack n circles in a unit square to maximize sum of radii.\n \n Args:\n n: Number of circles to pack\n\n Returns:\n Numpy array of shape (n, 3) where each row is (x, y, radius)\n All values should be between 0 and 1\n Circles must not overlap\n \n Important: Set "all" random seeds to 2025, including the packages (such as scipy sub-packages) involving random seeds.\n """\n\n grid_size = int(np.ceil(np.sqrt(n)))\n radius = 0.5 / grid_size\n\n circles = []\n for i in range(n):\n row = i // grid_size\n col = i % grid_size\n x = (col + 0.5) / grid_size\n y = (row + 0.5) / grid_size\n circles.append([x, y, radius])\n\n return np.array(circles)' -task_description = 'Implement a function that uses a constructive heuristic to pack n non-overlapping circles iteratively within a unit square to maximize the sum of their radii' - -import itertools -from llm4ad_loader import Evaluation - -__all__ = ['CirclePackingEvaluation'] - - -class CirclePackingEvaluation(Evaluation): - """Evaluator for circle packing problem in a unit square.""" - - def __init__(self, - timeout_seconds=30, - **kwargs): - """ - Args: - timeout_seconds: Time limit for evaluation - n_instance: Number of problem instances to evaluate - max_circles: Maximum number of circles to pack (n) - Raises: - ValueError: If invalid parameters are provided - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - self.n = 26 - - def evaluate_program(self, program_str: str, callable_func: callable) -> Any | None: - return self.evaluate(callable_func) - - def verify_circles(self, circles: np.ndarray) -> bool: - """Checks that the circles are disjoint and lie inside a unit square. - - Args: - circles: A numpy array of shape (num_circles, 3), where each row is - of the form (x, y, radius), specifying a circle. - - Returns: - bool: True if valid, False otherwise - """ - try: - # Check pairwise disjointness - for circle1, circle2 in itertools.combinations(circles, 2): - center_distance = np.sqrt((circle1[0] - circle2[0]) ** 2 + (circle1[1] - circle2[1]) ** 2) - radii_sum = circle1[2] + circle2[2] - if center_distance < radii_sum: - return False - - # Check all circles lie inside the unit square [0,1]x[0,1] - for circle in circles: - if not (0 <= min(circle[0], circle[1]) - circle[2] and - max(circle[0], circle[1]) + circle[2] <= 1): - return False - return True - except Exception: - return False - - - - def plot_circles(self,circles: np.ndarray): - - import matplotlib.pyplot as plt - import matplotlib.patches as patches - """Plots the circles.""" - _, ax = plt.subplots(1, figsize=(7, 7)) - ax.set_xlim(0, 1) - ax.set_ylim(0, 1) - ax.set_aspect('equal') # Make axes scaled equally. - - # Draw unit square boundary. - rect = patches.Rectangle((0, 0), 1, 1, linewidth=1, edgecolor='black', facecolor='none') - ax.add_patch(rect) - - # Draw the circles. - for circle in circles: - circ = patches.Circle((circle[0], circle[1]), circle[2], edgecolor='blue', facecolor='skyblue', alpha=0.5) - ax.add_patch(circ) - - plt.title( - f'A collection of {len(circles)} disjoint circles packed inside a unit square to maximize the sum of radii') - plt.show() - - def evaluate(self, eva: callable) -> float: - """Evaluate the circle packing solution.""" - circles = eva(self.n) - - #self.plot_circles(circles) - # Convert to numpy array if not already - circles = np.array(circles, dtype=np.float64) - - # Verify the solution - if not self.verify_circles(circles) or len(circles) != self.n: - return -float('inf') - - # Sum of radii is our score - score = np.sum(circles[:, 2]) - - return score - - - - - - -if __name__ == '__main__': - - # import numpy as np - # - # - # def pack_circles(n: int) -> np.ndarray: - # """ - # Pack n circles in a unit square to maximize sum of radii. - # - # Args: - # n: Number of circles to pack - # - # Returns: - # Numpy array of shape (n, 3) where each row is (x, y, radius) - # All values should be between 0 and 1 - # Circles must not overlap - # """ - # - # grid_size = int(np.ceil(np.sqrt(n))) - # radius = 0.5 / grid_size - # - # circles = [] - # for i in range(n): - # row = i // grid_size - # col = i % grid_size - # x = (col + 0.5) / grid_size - # y = (row + 0.5) / grid_size - # circles.append([x, y, radius]) - # - # return np.array(circles) - import numpy as np - import math - - - def pack_circles(n: int) -> np.ndarray: - """ - Pack n circles in a unit square to maximize sum of radii. - - Args: - n: Number of circles to pack - - Returns: - Numpy array of shape (n, 3) where each row is (x, y, radius) - All values should be between 0 and 1 - Circles must not overlap - """ - if n == 0: - return np.zeros((0, 3)) - - circles = np.zeros((n, 3)) - circles[0] = [0.5, 0.5, 0.5] # Place first circle at center with max possible radius - - for i in range(1, n): - max_r = 0 - best_pos = (0, 0) - - # Grid search for best position - grid_size = 100 - for x in np.linspace(0, 1, grid_size): - for y in np.linspace(0, 1, grid_size): - # Calculate minimum distance to existing circles and boundaries - min_dist = min( - min(np.sqrt((x - cx) ** 2 + (y - cy) ** 2) - cr for cx, cy, cr in circles[:i]), - x, - 1 - x, - y, - 1 - y - ) - - if min_dist > max_r: - max_r = min_dist - best_pos = (x, y) - - circles[i] = [best_pos[0], best_pos[1], max_r] - - return circles - - - pack = CirclePackingEvaluation() - pack.evaluate_program('_', pack_circles) - - -# Task configuration for benchmark task -ENTRY_NAME = 'pack_circles' -FUNCTION_SIGNATURE = 'def pack_circles(...):' -IMPORT_HEADER = 'import numpy as np\nimport math' -TASK_DESCRIPTION = 'Implement a function that uses a constructive heuristic to pack n non-overlapping circles iteratively within a unit square to maximize the sum of their radii' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `pack_circles` for the LLM4AD task.\\n\\nTask description:\\nImplement a function that uses a constructive heuristic to pack n non-overlapping circles iteratively within a unit square to maximize the sum of their radii\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\nimport math\ndef pack_circles(n: int) -> np.ndarray:\n """\n Pack n circles in a unit square to maximize sum of radii.\n \n Args:\n n: Number of circles to pack\n\n Returns:\n Numpy array of shape (n, 3) where each row is (x, y, radius)\n All values should be between 0 and 1\n Circles must not overlap\n \n Important: Set "all" random seeds to 2025, including the packages (such as scipy sub-packages) involving random seeds.\n """\n\n grid_size = int(np.ceil(np.sqrt(n)))\n radius = 0.5 / grid_size\n\n circles = []\n for i in range(n):\n row = i // grid_size\n col = i % grid_size\n x = (col + 0.5) / grid_size\n y = (row + 0.5) / grid_size\n circles.append([x, y, radius])\n\n return np.array(circles)' -EVAL_CLASS_NAME = 'CirclePackingEvaluation' -EVAL_KWARGS = {} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/circle_packing/run_eoh.py b/examples/benchmark_tasks/circle_packing/run_eoh.py deleted file mode 100644 index 7bc54483..00000000 --- a/examples/benchmark_tasks/circle_packing/run_eoh.py +++ /dev/null @@ -1,33 +0,0 @@ -import sys - -sys.path.append('../../') # This is for finding all the modules - -from evaluation import CirclePackingEvaluation -from llm4ad.tools.llm.llm_api_https import HttpsApi -from llm4ad.method.eoh import EoH,EoHProfiler -from llm4ad.tools.profiler import ProfilerBase - - -def main(): - llm = HttpsApi(host='api.bltcy.ai', # your host endpoint, e.g., 'api.openai.com', 'api.deepseek.com' - key='sk-bxkYIPpRbqTWS0cGB01009DfE8F94c2f8a26082248Bf7e98', # your key, e.g., 'sk-abcdefghijklmn' - model='deepseek-v3', # your llm, e.g., 'gpt-3.5-turbo' - timeout=120) - - task = CirclePackingEvaluation(timeout_seconds=1200) # local - - method = EoH(llm=llm, - profiler=EoHProfiler(log_dir='logs/eohseed', log_style='simple'), - evaluation=task, - max_sample_nums=15000, - max_generations=10000, - pop_size=32, - num_samplers=32, - num_evaluators=32, - debug_mode=False) - - method.run() - - -if __name__ == '__main__': - main() diff --git a/examples/benchmark_tasks/index.json b/examples/benchmark_tasks/index.json deleted file mode 100644 index ac9e9dee..00000000 --- a/examples/benchmark_tasks/index.json +++ /dev/null @@ -1,805 +0,0 @@ -[ - { - "key": "circle_packing", - "module": "circle_packing", - "entry": "pack_circles", - "eval_class": "CirclePackingEvaluation", - "task_description": "Implement a function that uses a constructive heuristic to pack n non-overlapping circles iteratively within a unit square to maximize the sum of their radii", - "wrapper": "circle_packing", - "copied_files": [ - "run_eoh.py" - ], - "benchmark": true - }, - { - "key": "online_bin_packing_local", - "module": "online_bin_packing_local", - "entry": "priority", - "eval_class": "OBPEvaluation", - "task_description": "Implement a function that returns the priority with which we want to add an item to each bin.", - "wrapper": "online_bin_packing_local", - "copied_files": [ - "run_eoh.py", - "generate_weibull_instances.py" - ], - "benchmark": true - }, - { - "key": "optimization/tsp_gls_2O", - "module": "optimization_tsp_gls_2O", - "entry": "update_edge_distance", - "eval_class": "TSP_GLS_2O_Evaluation", - "task_description": "Given an edge distance matrix and a local optimal route, please help me design a strategy to update the distance matrix to avoid being trapped in the local optimum with the final goal of finding a tour with minimized distance. You should create a heuristic for me to update the edge distance matrix.", - "wrapper": "optimization_tsp_gls_2O", - "copied_files": [ - "get_instance.py", - "__init__.py", - "gls.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/set_cover_construct", - "module": "optimization_set_cover_construct", - "entry": "select_next_subset", - "eval_class": "SCPEvaluation", - "task_description": "'", - "wrapper": "optimization_set_cover_construct", - "copied_files": [ - "get_instance.py", - "test.py", - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/tsp_construct", - "module": "optimization_tsp_construct", - "entry": "select_next_node", - "eval_class": "TSPEvaluation", - "task_description": "\"Given a set of nodes with their coordinates, you need to find the shortest route that visits each node once and returns to the starting node. \\", - "wrapper": "optimization_tsp_construct", - "copied_files": [ - "get_instance.py", - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/bp_2d_construct", - "module": "optimization_bp_2d_construct", - "entry": "determine_next_assignment", - "eval_class": "BP2DEvaluation", - "task_description": "'", - "wrapper": "optimization_bp_2d_construct", - "copied_files": [ - "get_instance.py", - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/online_bin_packing_2O", - "module": "optimization_online_bin_packing_2O", - "entry": "priority", - "eval_class": "OBP_2O_Evaluation", - "task_description": "Implement a function that returns the priority with which we want to add an item to each bin.", - "wrapper": "optimization_online_bin_packing_2O", - "copied_files": [ - "__init__.py", - "generate_weibull_instances.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/cflp_construct", - "module": "optimization_cflp_construct", - "entry": "select_next_assignment", - "eval_class": "CFLPEvaluation", - "task_description": "'", - "wrapper": "optimization_cflp_construct", - "copied_files": [ - "get_instance.py", - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/vrptw_construct", - "module": "optimization_vrptw_construct", - "entry": "select_next_node", - "eval_class": "VRPTWEvaluation", - "task_description": "The task involves finding optimal routes for a fleet of vehicles to serve a set of customers, respecting time windows and vehicle capacity constraints. Help me design an algorithm to select the next node in each step.", - "wrapper": "optimization_vrptw_construct", - "copied_files": [ - "get_instance.py", - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/online_bin_packing", - "module": "optimization_online_bin_packing", - "entry": "priority", - "eval_class": "OBPEvaluation", - "task_description": "Implement a function that returns the priority with which we want to add an item to each bin.", - "wrapper": "optimization_online_bin_packing", - "copied_files": [ - "__init__.py", - "generate_weibull_instances.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/knapsack_construct", - "module": "optimization_knapsack_construct", - "entry": "select_next_item", - "eval_class": "KnapsackEvaluation", - "task_description": "'", - "wrapper": "optimization_knapsack_construct", - "copied_files": [ - "get_instance.py", - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/pymoo_moead", - "module": "optimization_pymoo_moead", - "entry": "custom_decomposition", - "eval_class": "MOEAD_PYMOO_Evaluation", - "task_description": "\"", - "wrapper": "optimization_pymoo_moead", - "copied_files": [ - "get_instance.py", - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/cvrp_construct", - "module": "optimization_cvrp_construct", - "entry": "select_next_node", - "eval_class": "CVRPEvaluation", - "task_description": "\"", - "wrapper": "optimization_cvrp_construct", - "copied_files": [ - "get_instance.py", - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/jssp_construct", - "module": "optimization_jssp_construct", - "entry": "determine_next_operation", - "eval_class": "JSSPEvaluation", - "task_description": "'", - "wrapper": "optimization_jssp_construct", - "copied_files": [ - "get_instance.py", - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/bp_1d_construct", - "module": "optimization_bp_1d_construct", - "entry": "determine_next_assignment", - "eval_class": "BP1DEvaluation", - "task_description": "'", - "wrapper": "optimization_bp_1d_construct", - "copied_files": [ - "get_instance.py", - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/admissible_set", - "module": "optimization_admissible_set", - "entry": "priority", - "eval_class": "ASPEvaluation", - "task_description": "\"\"\"\\", - "wrapper": "optimization_admissible_set", - "copied_files": [ - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/qap_construct", - "module": "optimization_qap_construct", - "entry": "select_next_assignment", - "eval_class": "QAPEvaluation", - "task_description": "'", - "wrapper": "optimization_qap_construct", - "copied_files": [ - "get_instance.py", - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/ovrp_construct", - "module": "optimization_ovrp_construct", - "entry": "select_next_node", - "eval_class": "OVRPEvaluation", - "task_description": "\"", - "wrapper": "optimization_ovrp_construct", - "copied_files": [ - "get_instance.py", - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/co_bench/open_shop_scheduling_co_bench", - "module": "optimization_open_shop_scheduling", - "entry": "solve", - "eval_class": "OSSEvaluationCB", - "task_description": "(\"The Open Shop Scheduling Problem involves scheduling a set of jobs across a set of machines with \"", - "wrapper": "optimization_open_shop_scheduling", - "copied_files": [ - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/co_bench/generalised_assignment_problem_co_bench", - "module": "optimization_generalised_assignment_problem", - "entry": "solve", - "eval_class": "GAPEvaluationCB", - "task_description": "(\"The Generalized Assignment Problem (GAP) involves assigning \\( n \\) jobs to \\( m \\) agents such \"", - "wrapper": "optimization_generalised_assignment_problem", - "copied_files": [ - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/co_bench/flow_shop_scheduling_co_bench", - "module": "optimization_flow_shop_scheduling", - "entry": "solve", - "eval_class": "FSSEvaluationCB", - "task_description": "(\"Given n jobs and m machines, the goal of the flow shop scheduling problem is to determine \"", - "wrapper": "optimization_flow_shop_scheduling", - "copied_files": [ - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/co_bench/set_partitioning_co_bench", - "module": "optimization_set_partitioning", - "entry": "solve", - "eval_class": "SPEvaluationCB", - "task_description": "(\"This problem involves solving a set partitioning instance where the goal is to choose a subset \"", - "wrapper": "optimization_set_partitioning", - "copied_files": [ - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/co_bench/maximal_independent_set_co_bench", - "module": "optimization_maximal_independent_set", - "entry": "solve", - "eval_class": "MISEvaluationCB", - "task_description": "(\"The Maximum Independent Set (MIS) problem is a fundamental NP-hard optimization problem in graph \"", - "wrapper": "optimization_maximal_independent_set", - "copied_files": [ - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/co_bench/container_loading_co_bench", - "module": "optimization_container_loading", - "entry": "solve", - "eval_class": "CLEvaluationCB", - "task_description": "(\"Solves a container loading problem: Given a 3D container of specified dimensions and multiple \"", - "wrapper": "optimization_container_loading", - "copied_files": [ - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/co_bench/equitable_partitioning_problem_co_bench", - "module": "optimization_equitable_partitioning_problem", - "entry": "solve", - "eval_class": "EPPEvaluationCB", - "task_description": "(\"The task is to partition a set of individuals\u2014each characterized by multiple binary \"", - "wrapper": "optimization_equitable_partitioning_problem", - "copied_files": [ - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/co_bench/p_median_uncapacitated_co_bench", - "module": "optimization_p_median_uncapacitated", - "entry": "solve", - "eval_class": "PMUEvaluationCB", - "task_description": "(\"The uncapacitated p-median problem is a combinatorial optimization problem defined on a given \"", - "wrapper": "optimization_p_median_uncapacitated", - "copied_files": [ - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/co_bench/crew_scheduling_co_bench", - "module": "optimization_crew_scheduling", - "entry": "solve", - "eval_class": "CSchedulingEvaluationCB", - "task_description": "(\"The Crew Scheduling Problem involves assigning each task\u2014with defined start and finish times\u2014to \"", - "wrapper": "optimization_crew_scheduling", - "copied_files": [ - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/co_bench/euclidean_steiner_problem_co_bench", - "module": "optimization_euclidean_steiner_problem", - "entry": "solve", - "eval_class": "ESPEvaluationCB", - "task_description": "(\"Given a set of 2D points (terminals), the goal of the Euclidean Steiner Problem is to compute a \"", - "wrapper": "optimization_euclidean_steiner_problem", - "copied_files": [ - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/co_bench/unconstrained_guillotine_cutting_co_bench", - "module": "optimization_unconstrained_guillotine_cutting", - "entry": "solve", - "eval_class": "UGCEvaluationCB", - "task_description": "(\"The unconstrained guillotine cutting problem involves selecting and placing a subset of \"", - "wrapper": "optimization_unconstrained_guillotine_cutting", - "copied_files": [ - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/co_bench/packing_unequal_circles_co_bench", - "module": "optimization_packing_unequal_circles", - "entry": "solve", - "eval_class": "PUCEvaluationCB", - "task_description": "(\"The problem involves packing a subset of unequal circles into a fixed circular container with \"", - "wrapper": "optimization_packing_unequal_circles", - "copied_files": [ - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/co_bench/packing_unequal_rectangles_and_squares_area_co_bench", - "module": "optimization_packing_unequal_rectangles_and_squares_area", - "entry": "solve", - "eval_class": "PURSAEvaluationCB", - "task_description": "(\"We consider the problem of selecting and placing a subset of n unequal rectangles (or squares) \"", - "wrapper": "optimization_packing_unequal_rectangles_and_squares_area", - "copied_files": [ - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/co_bench/hybrid_reentrant_shop_scheduling_co_bench", - "module": "optimization_hybrid_reentrant_shop_scheduling", - "entry": "solve", - "eval_class": "HRSSEvaluationCB", - "task_description": "(\"The problem is a Hybrid Reentrant Shop Scheduling problem where each of n jobs must sequentially \"", - "wrapper": "optimization_hybrid_reentrant_shop_scheduling", - "copied_files": [ - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/co_bench/travelling_salesman_problem_co_bench", - "module": "optimization_travelling_salesman_problem", - "entry": "solve", - "eval_class": "TSPEvaluationCB", - "task_description": "(\"The Traveling Salesman Problem (TSP) is a classic combinatorial optimization problem where, \"", - "wrapper": "optimization_travelling_salesman_problem", - "copied_files": [ - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/co_bench/uncapacitated_warehouse_location_co_bench", - "module": "optimization_uncapacitated_warehouse_location", - "entry": "solve", - "eval_class": "UWLEvaluationCB", - "task_description": "(\"The Uncapacitated Warehouse Location Problem aims to determine which warehouses to open and how \"", - "wrapper": "optimization_uncapacitated_warehouse_location", - "copied_files": [ - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/co_bench/bp_1d_co_bench", - "module": "optimization_bp_1d", - "entry": "solve", - "eval_class": "BP1DEvaluationCB", - "task_description": "(\"The **one-dimensional bin packing problem** seeks to minimize the number of bins required to \"", - "wrapper": "optimization_bp_1d", - "copied_files": [ - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/co_bench/job_shop_scheduling_co_bench", - "module": "optimization_job_shop_scheduling", - "entry": "solve", - "eval_class": "JSSEvaluationCB", - "task_description": "(\"The job shop scheduling problem requires assigning non-negative integer start times to a set of \"", - "wrapper": "optimization_job_shop_scheduling", - "copied_files": [ - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/co_bench/corporate_structuring_co_bench", - "module": "optimization_corporate_structuring", - "entry": "solve", - "eval_class": "CSEvaluationCB", - "task_description": "'''Given N countries, each defined by:", - "wrapper": "optimization_corporate_structuring", - "copied_files": [ - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/co_bench/assignment_problem_co_bench", - "module": "optimization_assignment_problem", - "entry": "solve", - "eval_class": "APEvaluationCB", - "task_description": "(\"The Assignment Problem involves optimally assigning n items to n agents based on a provided \"", - "wrapper": "optimization_assignment_problem", - "copied_files": [ - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/co_bench/packing_unequal_rectangles_and_squares_co_bench", - "module": "optimization_packing_unequal_rectangles_and_squares", - "entry": "solve", - "eval_class": "PURSEvaluationCB", - "task_description": "(\"We are given a set of n unequal rectangles (or squares), each with specified dimensions, \"", - "wrapper": "optimization_packing_unequal_rectangles_and_squares", - "copied_files": [ - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/co_bench/assortment_problem_co_bench", - "module": "optimization_assortment_problem", - "entry": "solve", - "eval_class": "AssortPEvaluationCB", - "task_description": "(\"This optimization problem involves arranging a set of rectangular pieces within available stock \"", - "wrapper": "optimization_assortment_problem", - "copied_files": [ - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/co_bench/set_covering_co_bench", - "module": "optimization_set_covering", - "entry": "solve", - "eval_class": "SCEvaluationCB", - "task_description": "(\"Set Covering Problem. The goal is to select a subset of columns, each with an associated cost, \"", - "wrapper": "optimization_set_covering", - "copied_files": [ - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/co_bench/p_median_capacitated_co_bench", - "module": "optimization_p_median_capacitated", - "entry": "solve", - "eval_class": "PMCEvaluationCB", - "task_description": "(\"The Capacitated P-Median Problem is a facility location optimization problem where the objective \"", - "wrapper": "optimization_p_median_capacitated", - "copied_files": [ - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/co_bench/multi_demand_multidimensional_knapsack_problem_co_bench", - "module": "optimization_multi_demand_multidimensional_knapsack_problem", - "entry": "solve", - "eval_class": "MDMKPEvaluationCB", - "task_description": "(\"The Multi-Demand Multidimensional Knapsack Problem (MDMKP) is a binary optimization problem that \"", - "wrapper": "optimization_multi_demand_multidimensional_knapsack_problem", - "copied_files": [ - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/co_bench/container_loading_with_weight_restrictions_co_bench", - "module": "optimization_container_loading_with_weight_restrictions", - "entry": "solve", - "eval_class": "CLWREvaluationCB", - "task_description": "(\"The Container Loading with Weight Restrictions problem aims to maximize the utilization of a \"", - "wrapper": "optimization_container_loading_with_weight_restrictions", - "copied_files": [ - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/co_bench/capacitated_warehouse_location_co_bench", - "module": "optimization_capacitated_warehouse_location", - "entry": "solve", - "eval_class": "CWLEvaluationCB", - "task_description": "(\"The Capacitated Warehouse Location Problem with Splittable Demand aims to determine which \"", - "wrapper": "optimization_capacitated_warehouse_location", - "copied_files": [ - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/co_bench/common_due_date_scheduling_co_bench", - "module": "optimization_common_due_date_scheduling", - "entry": "solve", - "eval_class": "CDDSEvaluationCB", - "task_description": "(\"The **Restricted Single-Machine Common Due Date Scheduling Problem** involves scheduling a set \"", - "wrapper": "optimization_common_due_date_scheduling", - "copied_files": [ - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/co_bench/constrained_guillotine_cutting_co_bench", - "module": "optimization_constrained_guillotine_cutting", - "entry": "solve", - "eval_class": "CGCEvaluationCB", - "task_description": "(\"The problem involves optimizing the guillotine feasible placement of a set of rectangular pieces \"", - "wrapper": "optimization_constrained_guillotine_cutting", - "copied_files": [ - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/co_bench/packing_unequal_circles_area_co_bench", - "module": "optimization_packing_unequal_circles_area", - "entry": "solve", - "eval_class": "PUCAEvaluationCB", - "task_description": "(\"The problem involves packing a subset of unequal circles into a fixed circular container with \"", - "wrapper": "optimization_packing_unequal_circles_area", - "copied_files": [ - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/co_bench/graph_colouring_co_bench", - "module": "optimization_graph_colouring", - "entry": "solve", - "eval_class": "GCEvaluationCB", - "task_description": "(\"Given a graph in DIMACS format with vertices, edges, and an adjacency list, the goal is to \"", - "wrapper": "optimization_graph_colouring", - "copied_files": [ - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/co_bench/vehicle_routing_period_routing_co_bench", - "module": "optimization_vehicle_routing_period_routing", - "entry": "solve", - "eval_class": "VRPREvaluationCB", - "task_description": "(\"The Period Vehicle Routing Problem requires planning delivery routes over a multi\u2010day planning \"", - "wrapper": "optimization_vehicle_routing_period_routing", - "copied_files": [ - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/co_bench/resource_constrained_shortest_path_co_bench", - "module": "optimization_resource_constrained_shortest_path", - "entry": "solve", - "eval_class": "RCSPEvaluationCB", - "task_description": "(\"This problem involves finding the shortest path from vertex 1 to vertex n in a directed graph \"", - "wrapper": "optimization_resource_constrained_shortest_path", - "copied_files": [ - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/co_bench/multidimensional_knapsack_problem_co_bench", - "module": "optimization_multidimensional_knapsack_problem", - "entry": "solve", - "eval_class": "MKPEvaluationCB", - "task_description": "(\"This problem is a multidimensional knapsack optimization where the objective is to maximize the \"", - "wrapper": "optimization_multidimensional_knapsack_problem", - "copied_files": [ - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/co_bench/aircraft_landing_co_bench", - "module": "optimization_aircraft_landing", - "entry": "solve", - "eval_class": "ALEvaluationCB", - "task_description": "(\"The problem is to schedule landing times for a set of planes across one or more runways such that \"", - "wrapper": "optimization_aircraft_landing", - "copied_files": [ - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "optimization/co_bench/constrained_non_guillotine_cutting_co_bench", - "module": "optimization_constrained_non_guillotine_cutting", - "entry": "solve", - "eval_class": "CNCEvaluationCB", - "task_description": "(\"The constrained non-guillotine cutting problem involves optimally arranging rectangular pieces \"", - "wrapper": "optimization_constrained_non_guillotine_cutting", - "copied_files": [ - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "science_discovery/ode_1d", - "module": "science_discovery_ode_1d", - "entry": "equation", - "eval_class": "ODEEvaluation", - "task_description": "(\"Find the ODE mathematical function skeleton, given data on initial x. The function should be differentiable, continuous.\"", - "wrapper": "science_discovery_ode_1d", - "copied_files": [ - "strogatz_equations.py", - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "machine_learning/pendulum", - "module": "machine_learning_pendulum", - "entry": "choose_action", - "eval_class": "PendulumEvaluation", - "task_description": "(\"Implement a novel control strategy for the inverted pendulum swing-up problem. The goal is to \"", - "wrapper": "machine_learning_pendulum", - "copied_files": [ - "test.py", - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "machine_learning/moon_lander", - "module": "machine_learning_moon_lander", - "entry": "choose_action", - "eval_class": "MoonLanderEvaluation", - "task_description": "(\"Implement a novel heuristic strategy heuristic strategy function that guides the \"", - "wrapper": "machine_learning_moon_lander", - "copied_files": [ - "test.py", - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "machine_learning/car_mountain_continue", - "module": "machine_learning_car_mountain_continue", - "entry": "choose_action", - "eval_class": "CarMountainCEvaluation", - "task_description": "(\"Implement a function that designing a novel strategy function that guide the car along an uneven \"", - "wrapper": "machine_learning_car_mountain_continue", - "copied_files": [ - "test.py", - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "machine_learning/acrobot", - "module": "machine_learning_acrobot", - "entry": "choose_action", - "eval_class": "AcrobotEvaluation", - "task_description": "(\"I need help designing an innovative heuristic strategy function to control an acrobot, aiming to \"", - "wrapper": "machine_learning_acrobot", - "copied_files": [ - "test.py", - "__init__.py", - "paras.yaml" - ], - "benchmark": true - }, - { - "key": "machine_learning/car_mountain", - "module": "machine_learning_car_mountain", - "entry": "choose_action", - "eval_class": "CarMountainEvaluation", - "task_description": "(\"Implement a function that designing a novel strategy function that guide the car along an uneven \"", - "wrapper": "machine_learning_car_mountain", - "copied_files": [ - "test.py", - "__init__.py", - "q-learning.py", - "paras.yaml" - ], - "benchmark": true - } -] \ No newline at end of file diff --git a/examples/benchmark_tasks/machine_learning_acrobot/__init__.py b/examples/benchmark_tasks/machine_learning_acrobot/__init__.py deleted file mode 100644 index 576840c2..00000000 --- a/examples/benchmark_tasks/machine_learning_acrobot/__init__.py +++ /dev/null @@ -1,175 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: machine_learning_acrobot -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# Module Name: AcrobotEvaluation -# Last Revision: 2025/3/5 -# Description: Designs a heuristic strategy function for controlling an acrobot system. -# The function selects actions based on joint angles and angular velocities -# to efficiently swing the lower link and generate momentum for the upper -# link to reach the target height. -# This module is part of the LLM4AD project (https://github.com/Optima-CityU/llm4ad). -# -# Parameters: -# - cos_theta1: float - cosine of theta1, range [-1, 1] (default: None). -# - sin_theta1: float - sine of theta1, range [-1, 1] (default: None). -# - cos_theta2: float - cosine of theta2, range [-1, 1] (default: None). -# - sin_theta2: float - sine of theta2, range [-1, 1] (default: None). -# - a_v_theta1: float - angular velocity of theta1, range [-12.567, 12.567] (default: None). -# - a_v_theta2: float - angular velocity of theta2, range [-28.274, 28.274] (default: None). -# - last_action: int - last action taken, values [0, 1, 2] (default: None). -# - timeout_seconds: int - Maximum allowed time (in seconds) for the evaluation process (default: 20). -# -# References: -# - Brockman, Greg, et al. "Openai gym." arXiv preprint arXiv:1606.01540 (2016). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - - - -from __future__ import annotations - -from typing import Any -import os, sys -sys.path.insert(0, os.path.dirname(__file__)) -import gym - -from llm4ad_loader import Evaluation -# from llm4ad.task.machine_learning.acrobot.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\n\ndef choose_action(ct1: float, st1: float, ct2: float, st2: float, avt1: float, avt2: float, last_action: int) -> int: \n """\n Design a novel algorithm to select the action in each step.\n\n Args:\n ct1: cosine of theta1, float between [-1, 1].\n st1: sine of theta1, float between [-1, 1]\n ct2: cosine of theta2, float between [-1, 1].\n st2: sine of theta2, float between [-1, 1].\n avt1: angular velocity of theta1, float between [-12.567, 12.567].\n avt2: angular velocity of theta2, float between [-28.274, 28.274].\n\n\n Return:\n An integer representing the selected action for the acrobot.\n 0: apply -1 torque on actuated joint.\n 1: apply 0 torque on actuated joint\n 2: apply +1 torque on actuated joint.\n\n """\n # this is a placehold, replace it with your algorithm\n action = np.random.randint(3)\n\n return action' -task_description = '("I need help designing an innovative heuristic strategy function to control an acrobot, aiming to "' - - -__all__ = ['AcrobotEvaluation'] - - -def evaluate(env: gym.Env, action_select: callable) -> float: - """Evaluate heuristic function on car mountain problem.""" - - observation, _ = env.reset() # initialization - action = 0 # initial action - - for i in range(env._max_episode_steps + 1): # protect upper limits - action = action_select(observation[0], - observation[1], - observation[2], - observation[3], - observation[4], - observation[5], - action) - observation, reward, done, truncated, info = env.step(action) - - if done or truncated: - # self.env.close() - fitness = observation[0] + (observation[0] * observation[2] - observation[1] * observation[3]) + 2 - if fitness <= 1: - return -(i + 1) / env._max_episode_steps - else: - return -fitness - - -class AcrobotEvaluation(Evaluation): - """Evaluator for car mountain problem.""" - - def __init__(self, max_steps=500, timeout_seconds=20, **kwargs): - """ - Args: - - 'max_steps' (int): Maximum number of steps allowed per episode in the MountainCar-v0 environment (default is 500). - - '**kwargs' (dict): Additional keyword arguments passed to the parent class initializer. - - Attributes: - - 'env' (gym.Env): The MountainCar-v0 environment with a modified maximum episode length. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - self.env = None - self.env = gym.make('Acrobot-v1') - self.env._max_episode_steps = max_steps - - def evaluate_program(self, program_str: str, callable_func: callable) -> Any | None: - return evaluate(self.env, callable_func) - -# Task configuration for benchmark task -ENTRY_NAME = 'choose_action' -FUNCTION_SIGNATURE = 'def choose_action(...):' -IMPORT_HEADER = 'import numpy as np\nimport math' -TASK_DESCRIPTION = '("I need help designing an innovative heuristic strategy function to control an acrobot, aiming to "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `choose_action` for the LLM4AD task.\\n\\nTask description:\\n("I need help designing an innovative heuristic strategy function to control an acrobot, aiming to "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\n\ndef choose_action(ct1: float, st1: float, ct2: float, st2: float, avt1: float, avt2: float, last_action: int) -> int: \n """\n Design a novel algorithm to select the action in each step.\n\n Args:\n ct1: cosine of theta1, float between [-1, 1].\n st1: sine of theta1, float between [-1, 1]\n ct2: cosine of theta2, float between [-1, 1].\n st2: sine of theta2, float between [-1, 1].\n avt1: angular velocity of theta1, float between [-12.567, 12.567].\n avt2: angular velocity of theta2, float between [-28.274, 28.274].\n\n\n Return:\n An integer representing the selected action for the acrobot.\n 0: apply -1 torque on actuated joint.\n 1: apply 0 torque on actuated joint\n 2: apply +1 torque on actuated joint.\n\n """\n # this is a placehold, replace it with your algorithm\n action = np.random.randint(3)\n\n return action' -EVAL_CLASS_NAME = 'AcrobotEvaluation' -EVAL_KWARGS = {'max_steps': 500, 'timeout_seconds': 20} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/machine_learning_acrobot/paras.yaml b/examples/benchmark_tasks/machine_learning_acrobot/paras.yaml deleted file mode 100644 index 4a02375e..00000000 --- a/examples/benchmark_tasks/machine_learning_acrobot/paras.yaml +++ /dev/null @@ -1,3 +0,0 @@ -name: AcrobotEvaluation -max_steps: 500 -timeout_seconds: 20 \ No newline at end of file diff --git a/examples/benchmark_tasks/machine_learning_acrobot/test.py b/examples/benchmark_tasks/machine_learning_acrobot/test.py deleted file mode 100644 index f9bd2a1a..00000000 --- a/examples/benchmark_tasks/machine_learning_acrobot/test.py +++ /dev/null @@ -1,46 +0,0 @@ -import gym -import numpy as np - -# 初始化Acrobot-v1环境 -env = gym.make('Acrobot-v1') # , render_mode='human' - - -# 定义动作选择函数 -def choose_action(ct1: float, st1: float, ct2: float, st2: float, avt1: float, avt2: float, last_action: int) -> int: - if ct1 >= 0 and st1 >= 0 and avt1 < 0: - action = 2 - elif st1 < 0 and avt1 == 0 and st2 < 0 and avt2 == 0: - action = 0 - elif last_action == 2: - action = 0 - else: - action = 2 - - return action - - -# 环境重置 -observation, _ = env.reset() - -done = False -step = 0 -action = 1 -while not done: - step += 1 - theta1, theta2, theta1_dot, theta2_dot, avt1, avt2 = observation # 提取状态信息 - action = choose_action(theta1, theta2, theta1_dot, theta2_dot, avt1, avt2, action) # 决策动作 - - # 执行动作并获得新状态 - observation, reward, done, t, info = env.step(action) - - print(f"Step: {step}") - print(f"Theta1: {theta1}, Theta2: {theta2}") - print(f"Theta1_dot: {theta1_dot}, Theta2_dot: {theta2_dot}") - print(f"Action: {action}, Reward: {reward}, Done: {done}") - print(f"{(step + 1) / 500}") - - # 渲染环境 - env.render() - -# 关闭环境 -env.close() diff --git a/examples/benchmark_tasks/machine_learning_car_mountain/__init__.py b/examples/benchmark_tasks/machine_learning_car_mountain/__init__.py deleted file mode 100644 index dae3d181..00000000 --- a/examples/benchmark_tasks/machine_learning_car_mountain/__init__.py +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: machine_learning_car_mountain -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# Module Name: CarMountainEvaluation -# Last Revision: 2025/3/5 -# Description: Designs a heuristic strategy function for controlling a car along an uneven road (Mountain Car problem). -# The function selects actions based on the car's position and velocity to efficiently guide the car -# towards a target in the minimum number of steps. -# This module is part of the LLM4AD project (https://github.com/Optima-CityU/llm4ad). -# -# Parameters: -# - position: float - Car's position, range [-1.2, 0.6] (default: None). -# - velocity: float - Car's velocity, range [-0.07, 0.07] (default: None). -# - last_action: int - Car's last move, values [0, 1, 2] (default: None). -# - timeout_seconds: int - Maximum allowed time (in seconds) for the evaluation process (default: 20). -# -# References: -# - Brockman, Greg, et al. "Openai gym." arXiv preprint arXiv:1606.01540 (2016). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - - - -from __future__ import annotations - -from typing import Any -import os, sys -sys.path.insert(0, os.path.dirname(__file__)) -import gym - -from llm4ad_loader import Evaluation -# from llm4ad.task.machine_learning.car_mountain.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\n\ndef choose_action(pos: float, v: float, last_action: int) -> int:\n """Return the action for the car to proceed the next move.\n Args:\n pos: Car\'s position, a float ranges between [-1.2, 0.6].\n v: Car\'s velocity, a float ranges between [-0.07, 0.07].\n last_action: Car\'s next move, a int ranges between [0, 1, 2].\n Return:\n An integer representing the selected action for the car.\n 0: accelerate to left\n 1: don\'t accelerate\n 2: accelerate to right\n """\n return np.random.randint(3)' -task_description = '("Implement a function that designing a novel strategy function that guide the car along an uneven "' - - -__all__ = ['CarMountainEvaluation'] - - -def evaluate(env: gym.Env, action_select: callable) -> float: - """Evaluate heuristic function on car mountain problem.""" - - observation, _ = env.reset() # initialization - action = 1 # initial action, stay static - - for i in range(env._max_episode_steps): - action = action_select(observation[0], observation[1], action) - observation, reward, done, truncated, info = env.step(action) - - if done: - return -(i / env._max_episode_steps) # succeed - - if truncated: - return -(max(0.5 - observation[0], 0) + 1) # failed - - -class CarMountainEvaluation(Evaluation): - """Evaluator for car mountain problem.""" - - def __init__(self, max_steps=500, timeout_seconds=20, **kwargs): - """ - Args: - - 'max_steps' (int): Maximum number of steps allowed per episode in the MountainCar-v0 environment (default is 500). - - '**kwargs' (dict): Additional keyword arguments passed to the parent class initializer. - - Attributes: - - 'env' (gym.Env): The MountainCar-v0 environment with a modified maximum episode length. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - self.env = None - self.env = gym.make('MountainCar-v0') - self.env._max_episode_steps = max_steps - - def evaluate_program(self, program_str: str, callable_func: callable) -> Any | None: - return evaluate(self.env, callable_func) - -# Task configuration for benchmark task -ENTRY_NAME = 'choose_action' -FUNCTION_SIGNATURE = 'def choose_action(...):' -IMPORT_HEADER = 'import numpy as np\nimport math' -TASK_DESCRIPTION = '("Implement a function that designing a novel strategy function that guide the car along an uneven "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `choose_action` for the LLM4AD task.\\n\\nTask description:\\n("Implement a function that designing a novel strategy function that guide the car along an uneven "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\n\ndef choose_action(pos: float, v: float, last_action: int) -> int:\n """Return the action for the car to proceed the next move.\n Args:\n pos: Car\'s position, a float ranges between [-1.2, 0.6].\n v: Car\'s velocity, a float ranges between [-0.07, 0.07].\n last_action: Car\'s next move, a int ranges between [0, 1, 2].\n Return:\n An integer representing the selected action for the car.\n 0: accelerate to left\n 1: don\'t accelerate\n 2: accelerate to right\n """\n return np.random.randint(3)' -EVAL_CLASS_NAME = 'CarMountainEvaluation' -EVAL_KWARGS = {'max_steps': 500, 'timeout_seconds': 20} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/machine_learning_car_mountain/paras.yaml b/examples/benchmark_tasks/machine_learning_car_mountain/paras.yaml deleted file mode 100644 index c36f71f7..00000000 --- a/examples/benchmark_tasks/machine_learning_car_mountain/paras.yaml +++ /dev/null @@ -1,3 +0,0 @@ -name: CarMountainEvaluation -max_steps: 500 -timeout_seconds: 20 \ No newline at end of file diff --git a/examples/benchmark_tasks/machine_learning_car_mountain/q-learning.py b/examples/benchmark_tasks/machine_learning_car_mountain/q-learning.py deleted file mode 100644 index cdbd6c42..00000000 --- a/examples/benchmark_tasks/machine_learning_car_mountain/q-learning.py +++ /dev/null @@ -1,123 +0,0 @@ -import numpy as np -import pandas as pd -import time -import gym -import csv -import os -import pickle -from queue import Queue - - -class QLearning: - def __init__(self, actions_space, learning_rate=0.01, reward_decay=0.99, e_greedy=0.6): - self.actions = actions_space # 动作空间 - self.lr = learning_rate # 学习率 - self.gamma = reward_decay # 回报衰减率 - self.epsilon = e_greedy # 探索/利用 贪婪系数 - self.num_pos = 20 # 位置分为num_pos份 - self.num_vel = 14 # 速度分为num_vel份 - self.q_table = np.random.uniform(low=-1, high=1, size=(self.num_pos * self.num_vel, self.actions.n)) # Q值表 - self.pos_bins = self.toBins(-1.2, 0.6, self.num_pos) - self.vel_bins = self.toBins(-0.07, 0.07, self.num_vel) - - def choose_action(self, state): - if np.random.uniform() < self.epsilon: - action = np.argmax(self.q_table[state]) - else: - action = self.actions.sample() - return action - - def toBins(self, clip_min, clip_max, num): - return np.linspace(clip_min, clip_max, num + 1) - - def digit(self, x, bin): - n = np.digitize(x, bins=bin) - if x == bin[-1]: - n = n - 1 - return n - - def digitize_state(self, observation): - cart_pos, cart_v = observation - digitized = [self.digit(cart_pos, self.pos_bins), - self.digit(cart_v, self.vel_bins)] - return (digitized[1] - 1) * self.num_pos + digitized[0] - 1 - - def learn(self, state, action, r, next_state): - next_action = np.argmax(self.q_table[next_state]) - q_predict = self.q_table[state, action] - q_target = r + self.gamma * self.q_table[next_state, next_action] - self.q_table[state, action] += self.lr * (q_target - q_predict) - - -def train(): - env = gym.make('MountainCar-v0', render_mode='human') # 指定渲染模式为 human - print(env.action_space) - agent = QLearning(env.action_space) - - for i in range(10000): # 训练次数 - observation, _ = env.reset() # 状态 - state = agent.digitize_state(observation) # 状态标准化 - for t in range(300): # 一次训练最大运行次数 - action = agent.choose_action(state) # 动作 - observation, reward, done, truncated, info = env.step(action) - next_state = agent.digitize_state(observation) - - if reward == 0: # 到达山顶时 reward 为 0 - reward += 1000 # 给大一点的奖励 - - print(f"step: {t}", action, reward, done, state, next_state, truncated) - agent.learn(state, action, reward, next_state) - state = next_state - - env.render() # 每一步渲染画面 - - if done or truncated: # 重新加载环境 - print("Episode finished after {} timesteps".format(t + 1)) - break - - print(agent.q_table) - env.close() - - # 保存模型 - with open(os.getcwd() + '/tmp/carmountain.model', 'wb') as f: - pickle.dump(agent, f) - - -def test(): - env = gym.make('MountainCar-v0', render_mode='human') # 指定渲染模式为 human - print(env.action_space) - with open(os.getcwd() + '/tmp/carmountain.model', 'rb') as f: - agent = pickle.load(f) - agent.actions = env.action_space # 初始化 - agent.epsilon = 1 - observation, _ = env.reset() # 初始化状态 - state = agent.digitize_state(observation) # 状态标准化 - - for t in range(500): # 一次测试最大运行次数 - action = agent.choose_action(state) # - observation, reward, done, truncated, info = env.step(action) - next_state = agent.digitize_state(observation) - print(action, reward, done, state, next_state) - agent.learn(state, action, reward, next_state) - state = next_state - env.render() # 渲染画面 - env.close() # 关闭环境 - - -def run_test(): - env = gym.make('MountainCar-v0') - observation, _ = env.reset() # 状态包括以下因素 - - for t in range(500): - action = np.random.choice([0, 1, 2]) # 动作 - observation, reward, done, truncated, info = env.step(action) - print(action, reward, done) - print(observation) - env.render() - time.sleep(0.02) - env.close() - - -if __name__ == '__main__': - train() # 训练 - test() # 训练结束后测试 diff --git a/examples/benchmark_tasks/machine_learning_car_mountain/test.py b/examples/benchmark_tasks/machine_learning_car_mountain/test.py deleted file mode 100644 index ef893033..00000000 --- a/examples/benchmark_tasks/machine_learning_car_mountain/test.py +++ /dev/null @@ -1,167 +0,0 @@ -import numpy as np -import pandas as pd -import time -import gym -import tqdm -import csv -import os -import pickle -from queue import Queue - - -class QLearning: - def __init__(self, actions_space, learning_rate=0.01, reward_decay=0.99, e_greedy=0.6): - self.actions = actions_space # 动作空间 - self.lr = learning_rate # 学习率 - self.gamma = reward_decay # 回报衰减率 - self.epsilon = e_greedy # 探索/利用 贪婪系数 - self.num_pos = 20 # 位置分为num_pos份 - self.num_vel = 14 # 速度分为num_vel份 - self.q_table = np.random.uniform(low=-1, high=1, size=(self.num_pos * self.num_vel, self.actions.n)) # Q值表 - self.pos_bins = self.toBins(-1.2, 0.6, self.num_pos) - self.vel_bins = self.toBins(-0.07, 0.07, self.num_vel) - - def choose_action(self, state): - if np.random.uniform() < self.epsilon: - action = np.argmax(self.q_table[state]) - else: - action = self.actions.sample() - return action - - def toBins(self, clip_min, clip_max, num): - return np.linspace(clip_min, clip_max, num + 1) - - def digit(self, x, bin): - n = np.digitize(x, bins=bin) - if x == bin[-1]: - n = n - 1 - return n - - def digitize_state(self, observation): - cart_pos, cart_v = observation - digitized = [self.digit(cart_pos, self.pos_bins), - self.digit(cart_v, self.vel_bins)] - return (digitized[1] - 1) * self.num_pos + digitized[0] - 1 - - def learn(self, state, action, r, next_state): - next_action = np.argmax(self.q_table[next_state]) - q_predict = self.q_table[state, action] - q_target = r + self.gamma * self.q_table[next_state, next_action] - self.q_table[state, action] += self.lr * (q_target - q_predict) - - -def train(): - env = gym.make('MountainCar-v0') # 指定渲染模式为 human - # print(env.action_space) - agent = QLearning(env.action_space) - - # use tqdm - for i in tqdm.tqdm(range(10000)): # 训练次数 - observation, _ = env.reset() # 状态 - state = agent.digitize_state(observation) # 状态标准化 - for t in range(300): # 一次训练最大运行次数 - action = agent.choose_action(state) # 动作 - observation, reward, done, truncated, info = env.step(action) - next_state = agent.digitize_state(observation) - - if reward == 0: # 到达山顶时 reward 为 0 - reward += 1000 # 给大一点的奖励 - - # print(f"step: {t}", action, reward, done, state, next_state, truncated) - agent.learn(state, action, reward, next_state) - state = next_state - - # env.render() # 每一步渲染画面 - - if done or truncated: # 重新加载环境 - # print("Episode {} finished after {} timesteps".format(i, t + 1)) - break - - print(agent.q_table) - env.close() - - # 保存模型 - with open(os.getcwd() + '/carmountain.model', 'wb') as f: - pickle.dump(agent, f) - - -def taste(): - # env = gym.make('MountainCar-v0', render_mode='human') # 指定渲染模式为 human - env = gym.make('MountainCar-v0') # 指定渲染模式为 human - - print(env.action_space) - with open(os.getcwd() + '/carmountain.model', 'rb') as f: - agent = pickle.load(f) - agent.actions = env.action_space # 初始化 - agent.epsilon = 1 - observation, _ = env.reset() # 初始化状态 - state = agent.digitize_state(observation) # 状态标准化 - - for t in range(500): # 一次测试最大运行次数 - action = agent.choose_action(state) # - observation, reward, done, truncated, info = env.step(action) - next_state = agent.digitize_state(observation) - print(f"step: {t}", action, reward, done, state, next_state) - # agent.learn(state, action, reward, next_state) - state = next_state - env.render() # 渲染画面 - env.close() # 关闭环境 - - -import numpy as np - - -def choose_action(pos: float, v: float, last_action: int) -> int: - """Return the action for the car to proceed the next move. - Args: - pos: Car's position, a float ranges between [-1.2, 0.6]. - v: Car's velocity, a float ranges between [-0.07, 0.07]. - last_action: Car's next move, a int ranges between [0, 1, 2]. - Return: - An integer representing the selected action for the car. - 0: accelerate to left - 1: don't accelerate - 2: accelerate to right - """ - target_pos = 0.6 - - # Calculate distance to target - distance_to_target = target_pos - pos - - # Define thresholds for decision making - if v < 0 and pos > target_pos: - return 0 # Accelerate left if moving backwards and past target - elif v > 0 and pos < target_pos: - return 2 # Accelerate right if moving forwards and before target - elif abs(distance_to_target) < 0.1: # If close to target, stabilize - return 1 # Don't accelerate, maintain current state - elif distance_to_target > 0: - return 2 # Move right towards the target - else: - return 0 # Move left away from the target - - -def run_test(): - env = gym.make('MountainCar-v0', render_mode='human') - observation, _ = env.reset() # 状态包括以下因素 - action = 1 - - for t in range(500): - # action = np.random.choice([0, 1, 2]) # 动作 - action = choose_action(observation[0], observation[1], action) - observation, reward, done, truncated, info = env.step(action) - print(f"step: {t}") - # print(action, reward, done) - # print(observation) - env.render() - # time.sleep(0.02) - - if done: - break - - env.close() - - -if __name__ == '__main__': - # train() # 训练 - run_test() # 训练结束后测试 diff --git a/examples/benchmark_tasks/machine_learning_car_mountain_continue/__init__.py b/examples/benchmark_tasks/machine_learning_car_mountain_continue/__init__.py deleted file mode 100644 index 939f15f9..00000000 --- a/examples/benchmark_tasks/machine_learning_car_mountain_continue/__init__.py +++ /dev/null @@ -1,167 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: machine_learning_car_mountain_continue -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# Module Name: CarMountainCEvaluation -# Last Revision: 2025/3/5 -# Description: Designs a heuristic strategy function for controlling a car along an uneven road (Continuous Mountain Car problem). -# The function applies an appropriate force based on the car's position and velocity to guide the car -# towards a target in the minimum number of steps. -# This module is part of the LLM4AD project (https://github.com/Optima-CityU/llm4ad). -# -# Parameters: -# - position: float - Car's position, range [-1.2, 0.6] (default: None). -# - velocity: float - Car's velocity, range [-0.07, 0.07] (default: None). -# - last_action: float - Car's last applied force, range [-1.0, 1.0] (default: None). -# - timeout_seconds: int - Maximum allowed time (in seconds) for the evaluation process (default: 20). -# -# References: -# - Brockman, Greg, et al. "Openai gym." arXiv preprint arXiv:1606.01540 (2016). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -from typing import Any -import os, sys -sys.path.insert(0, os.path.dirname(__file__)) -import gym - -from llm4ad_loader import Evaluation -# from llm4ad.task.machine_learning.car_mountain_continue.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\n\ndef choose_action(pos: float, v: float, last_action: float) -> float:\n """Return the action for the car to proceed the next move.\n Args:\n pos: Car\'s position, a float ranges between [-1.2, 0.6].\n v: Car\'s velocity, a float ranges between [-0.07, 0.07].\n last_action: Car\'s next move, a float ranges between [-1, 1].\n Return:\n A [float] representing the force to be applied to the car.\n The value should be in the range of [-1.0, 1.0].\n """\n return np.random.uniform(-1.0, 1.0)' -task_description = '("Implement a function that designing a novel strategy function that guide the car along an uneven "' - - - -__all__ = ['CarMountainCEvaluation'] - - -def evaluate(env: gym.Env, action_select: callable) -> float: - """Evaluate heuristic function on car mountain problem.""" - - observation, _ = env.reset() # initialization - - action = 0 # initial action, stay static - - for i in range(env._max_episode_steps): - action = action_select(observation[0], observation[1], action) - observation, reward, done, truncated, info = env.step([action]) - - if done: - return -(i / env._max_episode_steps) # succeed - - if truncated: - return -(max(0.5 - observation[0], 0) + 1) # failed - - - -class CarMountainCEvaluation(Evaluation): - """Evaluator for car mountain problem.""" - - def __init__(self, max_steps=500, timeout_seconds=20, **kwargs): - """ - Args: - - 'max_steps' (int): Maximum number of steps allowed per episode in the MountainCar-v0 environment (default is 500). - - '**kwargs' (dict): Additional keyword arguments passed to the parent class initializer. - - Attributes: - - 'env' (gym.Env): The MountainCar-v0 environment with a modified maximum episode length. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - self.env = None - self.env = gym.make('MountainCarContinuous-v0') - self.env._max_episode_steps = max_steps - - def evaluate_program(self, program_str: str, callable_func: callable) -> Any | None: - try: - a = evaluate(self.env, callable_func) - except Exception as e: - print(e) - return evaluate(self.env, callable_func) - -# Task configuration for benchmark task -ENTRY_NAME = 'choose_action' -FUNCTION_SIGNATURE = 'def choose_action(...):' -IMPORT_HEADER = 'import numpy as np\nimport math' -TASK_DESCRIPTION = '("Implement a function that designing a novel strategy function that guide the car along an uneven "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `choose_action` for the LLM4AD task.\\n\\nTask description:\\n("Implement a function that designing a novel strategy function that guide the car along an uneven "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\n\ndef choose_action(pos: float, v: float, last_action: float) -> float:\n """Return the action for the car to proceed the next move.\n Args:\n pos: Car\'s position, a float ranges between [-1.2, 0.6].\n v: Car\'s velocity, a float ranges between [-0.07, 0.07].\n last_action: Car\'s next move, a float ranges between [-1, 1].\n Return:\n A [float] representing the force to be applied to the car.\n The value should be in the range of [-1.0, 1.0].\n """\n return np.random.uniform(-1.0, 1.0)' -EVAL_CLASS_NAME = 'CarMountainCEvaluation' -EVAL_KWARGS = {'max_steps': 500, 'timeout_seconds': 20} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/machine_learning_car_mountain_continue/paras.yaml b/examples/benchmark_tasks/machine_learning_car_mountain_continue/paras.yaml deleted file mode 100644 index ac4fcccd..00000000 --- a/examples/benchmark_tasks/machine_learning_car_mountain_continue/paras.yaml +++ /dev/null @@ -1,3 +0,0 @@ -name: CarMountainCEvaluation -max_steps: 500 -timeout_seconds: 20 diff --git a/examples/benchmark_tasks/machine_learning_car_mountain_continue/test.py b/examples/benchmark_tasks/machine_learning_car_mountain_continue/test.py deleted file mode 100644 index a9ce6e5e..00000000 --- a/examples/benchmark_tasks/machine_learning_car_mountain_continue/test.py +++ /dev/null @@ -1,64 +0,0 @@ -import numpy as np -import pandas as pd -import time -import gym -import tqdm -import csv -import os -import pickle -from queue import Queue - -def choose_action(pos: float, v: float, last_action: float) -> [float]: - """Return the action for the car to proceed the next move. - Args: - pos: Car's position, a float ranges between [-1.2, 0.6]. - v: Car's velocity, a float ranges between [-0.07, 0.07]. - last_action: Car's next move, a int ranges between [0, 1, 2]. - Return: - An integer representing the selected action for the car. - 0: accelerate to left - 1: don't accelerate - 2: accelerate to right - """ - target_pos = 0.6 - - # Calculate distance to target - distance_to_target = target_pos - pos - - # Define thresholds for decision making - if v < 0 and pos > target_pos: - return [1] # Accelerate left if moving backwards and past target - elif v > 0 and pos < target_pos: - return [1] # Accelerate right if moving forwards and before target - elif abs(distance_to_target) < 0.1: # If close to target, stabilize - return [1] # Don't accelerate, maintain current state - elif distance_to_target > 0: - return [1] # Move right towards the target - else: - return [0.5] # Move left away from the target - - -def run_test(): - env = gym.make('MountainCarContinuous-v0', render_mode='human') - observation, _ = env.reset() # 状态包括以下因素 - action = 1 - - for t in range(500): - # action = np.random.choice([0, 1, 2]) # 动作 - action = choose_action(observation[0], observation[1], action) - action = np.random.random() - observation, reward, done, truncated, info = env.step([action]) - print(f"step: {t}, action: {action}, reward: {reward}, done: {done}, truncated: {truncated}, info: {info}") - # print(action, reward, done) - # print(observation) - env.render() - # time.sleep(0.02) - - if done: - break - - env.close() - - -if __name__ == '__main__': - run_test() # 训练结束后测试 diff --git a/examples/benchmark_tasks/machine_learning_moon_lander/__init__.py b/examples/benchmark_tasks/machine_learning_moon_lander/__init__.py deleted file mode 100644 index 22cb7819..00000000 --- a/examples/benchmark_tasks/machine_learning_moon_lander/__init__.py +++ /dev/null @@ -1,196 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: machine_learning_moon_lander -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# Module Name: MoonLanderEvaluation -# Last Revision: 2025/3/5 -# Description: Implements a heuristic strategy function to guide a lunar lander to achieve safe landings -# at the center of the target area. The function selects actions based on the lander's -# current state, aiming to minimize the number of steps required for a safe landing. -# A "safe landing" is defined as a touchdown with minimal vertical velocity, upright -# orientation, and angular velocity and angle close to zero. -# This module is part of the LLM4AD project (https://github.com/Optima-CityU/llm4ad). -# -# Parameters: -# - x_coordinate: float - x coordinate, range [-1, 1] (default: None). -# - y_coordinate: float - y coordinate, range [-1, 1] (default: None). -# - x_velocity: float - x velocity (default: None). -# - x_velocity: float - y velocity (default: None). -# - angle: float - angle (default: None). -# - angular_velocity: float - angular velocity (default: None). -# - l_contact: int - 1 if the first leg has contact, else 0 (default: None). -# - r_contact: int - 1 if the second leg has contact, else 0 (default: None). -# - last_action: int - last action taken by the lander, values [0, 1, 2, 3] (default: None). -# - timeout_seconds: int - Maximum allowed time (in seconds) for the evaluation process (default: 20). -# -# References: -# - Brockman, Greg, et al. "Openai gym." arXiv preprint arXiv:1606.01540 (2016). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - - -from __future__ import annotations - -from typing import Any -import os, sys -sys.path.insert(0, os.path.dirname(__file__)) -import gym -import numpy as np - -from llm4ad_loader import Evaluation -# from llm4ad.task.machine_learning.moon_lander.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\ndef choose_action(xc: float, yc: float, xv: float, yv: float, a: float, av: float, lc: float, rc: float, last_action: int) -> int:\n """\n Args:\n xc: x coordinate, between [-1, 1]\n yc: y coordinate, between [-1, 1]\n xv: x velocity\n yv: y velocity\n a: angle\n av: angular velocity\n lc: 1 if first leg has contact, else 0\n rc: 1 if second leg has contact, else 0.\n last_action: Lander\'s last move, a int ranges in [0, 1, 2, 3].\n\n Return:\n An integer representing the selected action for the lander.\n 0: do nothing\n 1: fire left orientation engine\n 2: upward\n 3: fire right orientation engine\n """\n action = np.random.randint(4)\n return action' -task_description = '("Implement a novel heuristic strategy heuristic strategy function that guides the "' - - -__all__ = ['MoonLanderEvaluation'] - - -def evaluate(env: gym.Env, action_select: callable) -> float | None: - try: - fitness = [] - # parallel evaluation 4 times, core=4 - # fitness = Parallel(n_jobs=4)(delayed(evaluate_single)(env, action_select) for _ in range(5)) - for i in range(5): - fitness.append(evaluate_single(env, action_select)) - fitness = np.mean(fitness) - - return fitness - except Exception as e: - return None - - -def evaluate_single(env: gym.Env, action_select: callable) -> float: - """Evaluate heuristic function on moon lander problem.""" - - observation, _ = env.reset() # initialization - action = 0 # initial action - reward = 0 - yv = [] - - for i in range(env._max_episode_steps + 1): # protect upper limits - action = action_select(observation[0], observation[1], - observation[2], - observation[3], - observation[4], - observation[5], - observation[6], - observation[7], - action) - observation, reward, done, truncated, info = env.step(action) - yv.append(observation[3]) - - if done or truncated: - # self.env.close() - fitness = abs(observation[0]) + abs(yv[-2]) - ((observation[6] + observation[7]) - 2) + 1 - if reward >= 100: - return -(i + 1) / env._max_episode_steps - else: - return -fitness - - -class MoonLanderEvaluation(Evaluation): - """Evaluator for moon lander problem.""" - - def __init__(self, max_steps=500, timeout_seconds=20, **kwargs): - """ - Args: - - 'max_steps' (int): Maximum number of steps allowed per episode in the MountainCar-v0 environment (default is 500). - - '**kwargs' (dict): Additional keyword arguments passed to the parent class initializer. - - Attributes: - - 'env' (gym.Env): The MountainCar-v0 environment with a modified maximum episode length. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - self.env = None - self.env = gym.make('LunarLander-v2') - self.env._max_episode_steps = max_steps - - def evaluate_program(self, program_str: str, callable_func: callable) -> Any | None: - return evaluate(self.env, callable_func) - -# Task configuration for benchmark task -ENTRY_NAME = 'choose_action' -FUNCTION_SIGNATURE = 'def choose_action(...):' -IMPORT_HEADER = 'import numpy as np\nimport math' -TASK_DESCRIPTION = '("Implement a novel heuristic strategy heuristic strategy function that guides the "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `choose_action` for the LLM4AD task.\\n\\nTask description:\\n("Implement a novel heuristic strategy heuristic strategy function that guides the "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\ndef choose_action(xc: float, yc: float, xv: float, yv: float, a: float, av: float, lc: float, rc: float, last_action: int) -> int:\n """\n Args:\n xc: x coordinate, between [-1, 1]\n yc: y coordinate, between [-1, 1]\n xv: x velocity\n yv: y velocity\n a: angle\n av: angular velocity\n lc: 1 if first leg has contact, else 0\n rc: 1 if second leg has contact, else 0.\n last_action: Lander\'s last move, a int ranges in [0, 1, 2, 3].\n\n Return:\n An integer representing the selected action for the lander.\n 0: do nothing\n 1: fire left orientation engine\n 2: upward\n 3: fire right orientation engine\n """\n action = np.random.randint(4)\n return action' -EVAL_CLASS_NAME = 'MoonLanderEvaluation' -EVAL_KWARGS = {'max_steps': 500, 'timeout_seconds': 20} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/machine_learning_moon_lander/paras.yaml b/examples/benchmark_tasks/machine_learning_moon_lander/paras.yaml deleted file mode 100644 index 5ca46fae..00000000 --- a/examples/benchmark_tasks/machine_learning_moon_lander/paras.yaml +++ /dev/null @@ -1,3 +0,0 @@ -name: MoonLanderEvaluation -max_steps: 500 -timeout_seconds: 20 \ No newline at end of file diff --git a/examples/benchmark_tasks/machine_learning_moon_lander/test.py b/examples/benchmark_tasks/machine_learning_moon_lander/test.py deleted file mode 100644 index dee9916c..00000000 --- a/examples/benchmark_tasks/machine_learning_moon_lander/test.py +++ /dev/null @@ -1,53 +0,0 @@ -import random -import gym -import numpy as np -import random - - -def choose_action(state, reward, last_action): - x, y, x_vel, y_vel, angle, angular_vel, leg1_contact, leg2_contact = state - - if y < 0.5 and y_vel < -0.1: - action = 2 # Fire upward engine if below target and moving downward - elif angle > 0.1 and angular_vel > 0: - action = 3 # Fire right orientation engine if orientation needs adjustment - else: - if reward < 0.5: - if random.uniform(0, 1) < 0.7: - action = 0 # Do nothing - else: - action = 1 # Fire left orientation engine - else: - if random.uniform(0, 1) < 0.5: - action = 2 # Fire upward engine - else: - action = 3 # Fire right orientation engine - return action - - -# 创建LunarLander-v2环境 -env = gym.make('LunarLander-v2', render_mode='human') - -# 重置环境 -state, _ = env.reset() - -done = False - -step = 0 -while not done: - # 随机采取一个动作 - step += 1 - action = 0 - # action = env.action_space.sample() - action = choose_action(state, 0, action) - - # 环境采取动作并返回新的状态、奖励等 - state, reward, done, t, info = env.step(action) - - print(f"step: {step}, state: {state}, reward: {reward}, done: {done}, t: {t}, action: {action}") - - # 渲染环境 - env.render() - -# 关闭环境 -env.close() diff --git a/examples/benchmark_tasks/machine_learning_pendulum/__init__.py b/examples/benchmark_tasks/machine_learning_pendulum/__init__.py deleted file mode 100644 index 3055c866..00000000 --- a/examples/benchmark_tasks/machine_learning_pendulum/__init__.py +++ /dev/null @@ -1,195 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: machine_learning_pendulum -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# Module Name: PendulumEvaluation -# Last Revision: 2025/3/5 -# Description: Implements a control strategy for the inverted pendulum swing-up problem. The function -# selects an appropriate torque based on the pendulum's current state to swing it into an -# upright position and stabilize it. The goal is to minimize the time required to reach -# the upright position while ensuring stability. This module is part of the LLM4AD project -# (https://github.com/Optima-CityU/llm4ad). -# -# Parameters: -# - x_position: float - cos(theta), range [-1, 1] (default: None). -# - y_position: float - sin(theta), range [-1, 1] (default: None). -# - angular_velocity: float - angular velocity of the pendulum, range [-8.0, 8.0] (default: None). -# - last_action: float - last torque applied to the pendulum, range [-2.0, 2.0] (default: None). -# - timeout_seconds: int - Maximum allowed time (in seconds) for the evaluation process (default: 20). -# -# References: -# - Brockman, Greg, et al. "Openai gym." arXiv preprint arXiv:1606.01540 (2016). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -from typing import Any -import os, sys -sys.path.insert(0, os.path.dirname(__file__)) -import gym -import numpy as np - -from llm4ad_loader import Evaluation -# from llm4ad.task.machine_learning.pendulum.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\n\ndef choose_action(x: float, y: float, av: float, last_action: float) -> float:\n """\n Args:\n x: cos(theta), between [-1, 1]\n y: sin(theta), between [-1, 1]\n av: angular velocity of the pendulum, between [-8.0, 8.0]\n last_action: the last torque applied to the pendulum, a float between [-2.0, 2.0]\n\n Return:\n A float representing the torque to be applied to the pendulum.\n The value should be in the range of [-2.0, 2.0].\n """\n action = np.random.uniform(-2.0, 2.0)\n return action' -task_description = '("Implement a novel control strategy for the inverted pendulum swing-up problem. The goal is to "' - - -__all__ = ['PendulumEvaluation'] - -def evaluate(env: gym.Env, action_select: callable) -> float | None: - try: - fitness = [] - # Parallel evaluation 4 times, core=4 - # fitness = Parallel(n_jobs=4)(delayed(evaluate_single)(env, action_select) for _ in range(5)) - for i in range(5): - fitness.append(evaluate_single(env, action_select)) - fitness = np.mean(fitness) - - return fitness - except Exception as e: - return None - - -def evaluate_single(env: gym.Env, action_select: callable) -> float: - """Evaluate heuristic function on the pendulum swing-up problem.""" - - observation, _ = env.reset() # initialization - action = 0.0 # initial action (torque) - total_reward = 0 - - for i in range(env._max_episode_steps + 1): # protect upper limits - action = action_select(observation[0], # cos(theta) - observation[1], # sin(theta) - observation[2], # angular velocity - action) # last action (torque) - observation, reward, done, truncated, info = env.step([action]) - total_reward += reward - - if done or truncated: - # self.env.close() - cos_theta = observation[0] - sin_theta = observation[1] - angular_velocity = observation[2] - - # Calculate error terms - angle_error = abs(1 - cos_theta) # Distance from vertical (cos(theta) = 1 when upright) - stability_error = abs(sin_theta) # Penalize instability - - # Total error - error = angle_error + stability_error - - # Fitness calculation: ensure fitness > 1 and closer to 1 for better states - fitness = 1 + error - if fitness <= 1: - return -(i + 1) / env._max_episode_steps - else: - return -fitness - - -class PendulumEvaluation(Evaluation): - """Evaluator for the pendulum swing-up problem.""" - - def __init__(self, max_steps=500, timeout_seconds=20, **kwargs): - """ - Args: - - 'max_steps' (int): Maximum number of steps allowed per episode in the Pendulum-v1 environment (default is 200). - - '**kwargs' (dict): Additional keyword arguments passed to the parent class initializer. - - Attributes: - - 'env' (gym.Env): The Pendulum-v1 environment with a modified maximum episode length. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - self.env = None - self.env = gym.make('Pendulum-v1') - self.env._max_episode_steps = max_steps - - def evaluate_program(self, program_str: str, callable_func: callable) -> Any | None: - return evaluate(self.env, callable_func) - -# Task configuration for benchmark task -ENTRY_NAME = 'choose_action' -FUNCTION_SIGNATURE = 'def choose_action(...):' -IMPORT_HEADER = 'import numpy as np\nimport math' -TASK_DESCRIPTION = '("Implement a novel control strategy for the inverted pendulum swing-up problem. The goal is to "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `choose_action` for the LLM4AD task.\\n\\nTask description:\\n("Implement a novel control strategy for the inverted pendulum swing-up problem. The goal is to "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\n\ndef choose_action(x: float, y: float, av: float, last_action: float) -> float:\n """\n Args:\n x: cos(theta), between [-1, 1]\n y: sin(theta), between [-1, 1]\n av: angular velocity of the pendulum, between [-8.0, 8.0]\n last_action: the last torque applied to the pendulum, a float between [-2.0, 2.0]\n\n Return:\n A float representing the torque to be applied to the pendulum.\n The value should be in the range of [-2.0, 2.0].\n """\n action = np.random.uniform(-2.0, 2.0)\n return action' -EVAL_CLASS_NAME = 'PendulumEvaluation' -EVAL_KWARGS = {'max_steps': 500, 'timeout_seconds': 20} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/machine_learning_pendulum/paras.yaml b/examples/benchmark_tasks/machine_learning_pendulum/paras.yaml deleted file mode 100644 index db88b585..00000000 --- a/examples/benchmark_tasks/machine_learning_pendulum/paras.yaml +++ /dev/null @@ -1,3 +0,0 @@ -name: PendulumEvaluation -max_steps: 500 -timeout_seconds: 20 diff --git a/examples/benchmark_tasks/machine_learning_pendulum/test.py b/examples/benchmark_tasks/machine_learning_pendulum/test.py deleted file mode 100644 index 7dbb8876..00000000 --- a/examples/benchmark_tasks/machine_learning_pendulum/test.py +++ /dev/null @@ -1,47 +0,0 @@ -import gym -import numpy as np - -# 初始化Pendulum-v1环境 -env = gym.make('Pendulum-v1') # 可选:设置 render_mode='human' 以显示图形界面 - - -# 定义动作选择函数 -def choose_action(x: float, y: float, angular_velocity: float, last_action: float) -> float: - if angular_velocity > 0 and y > 0: - action = -2.0 # 施加一个负力矩 - elif angular_velocity < 0 and y < 0: - action = 2.0 # 施加一个正力矩 - else: - action = 0.0 # 保持静止力矩 - - # 确保动作在 [-2.0, 2.0] 范围内 - action = np.clip(action, -2.0, 2.0) - return action - - -# 环境重置 -observation, _ = env.reset() - -done = False -step = 0 -action = 0.0 # 初始动作 -env._max_episode_steps = 500 - -while not done and step < 500: - step += 1 - x, y, angular_velocity = observation # 提取状态信息 (cos(theta), sin(theta), angular_velocity) - action = choose_action(x, y, angular_velocity, action) # 决策动作 - - # 执行动作并获得新状态 - observation, reward, done, truncated, info = env.step([action]) # 动作需要作为列表传递 - - print(f"Step: {step}") - print(f"x (cos(theta)): {x}, y (sin(theta)): {y}, Angular Velocity: {angular_velocity}") - print(f"Action: {action}, Reward: {reward}, Done: {done}, Truncated: {truncated}") - print(f"Progress: {(step + 1) / env._max_episode_steps:.2%}") - - # 渲染环境(可选) - env.render() - -# 关闭环境 -env.close() diff --git a/examples/benchmark_tasks/online_bin_packing_local/__init__.py b/examples/benchmark_tasks/online_bin_packing_local/__init__.py deleted file mode 100644 index e87190c6..00000000 --- a/examples/benchmark_tasks/online_bin_packing_local/__init__.py +++ /dev/null @@ -1,164 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: online_bin_packing_local -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# name: str: OBPEvaluation -# Parameters: -# timeout_seconds: int: 20 -# end -from __future__ import annotations - -from typing import Any - -import numpy as np - -from llm4ad_loader import Evaluation -# from template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\n\ndef priority(item: float, bins: np.ndarray) -> np.ndarray:\n """Returns priority with which we want to add item to each bin.\n Args:\n item: Size of item to be added to the bin.\n bins: Array of capacities for each bin.\n Return:\n Array of same size as bins with priority score of each bin.\n """\n return item - bins' -task_description = 'Implement a function that returns the priority with which we want to add an item to each bin.' - -import os, sys -sys.path.insert(0, os.path.dirname(__file__)) -from generate_weibull_instances import generate_weibull_dataset - -__all__ = ['OBPEvaluation'] - -def get_valid_bin_indices(item: float, bins: np.ndarray) -> np.ndarray: - """Returns indices of bins in which item can fit.""" - return np.nonzero((bins - item) >= 0)[0] - - -def online_binpack( - items: tuple[float, ...], bins: np.ndarray, priority: callable -) -> tuple[list[list[float, ...], ...], np.ndarray]: - """Performs online binpacking of `items` into `bins`.""" - # Track which items are added to each bin. - packing = [[] for _ in bins] - # Add items to bins. - for item in items: - # Extract bins that have sufficient space to fit item. - valid_bin_indices = get_valid_bin_indices(item, bins) - # Score each bin based on heuristic. - priorities = priority(item, bins[valid_bin_indices]) - # Add item to bin with highest priority. - best_bin = valid_bin_indices[np.argmax(priorities)] - bins[best_bin] -= item - packing[best_bin].append(item) - # Remove unused bins from packing. - packing = [bin_items for bin_items in packing if bin_items] - return packing, bins - - -def evaluate(instances: dict, priority: callable) -> float: - """Evaluate heuristic function on a set of online binpacking instances.""" - # List storing number of bins used for each instance. - num_bins = [] - # Perform online binpacking for each instance. - for name in instances: - instance = instances[name] - capacity = instance['capacity'] - items = instance['items'] - # Create num_items bins so there will always be space for all items, - # regardless of packing order. Array has shape (num_items,). - bins = np.array([capacity for _ in range(instance['num_items'])]) - # Pack items into bins and return remaining capacity in bins_packed, which - # has shape (num_items,). - _, bins_packed = online_binpack(items, bins, priority) - # If remaining capacity in a bin is equal to initial capacity, then it is - # unused. Count number of used bins. - num_bins.append((bins_packed != capacity).sum()) - # Score of heuristic function is negative of average number of bins used - # across instances (as we want to minimize number of bins). - return -np.mean(num_bins) - - -class OBPEvaluation(Evaluation): - """Evaluator for online bin packing problem.""" - - def __init__(self, timeout_seconds=20, data_file='weibull_train.pkl', data_key='weibull_5k_train', **kwargs): - """ - Args: - - 'data_file' (str): The data file to load (default is 'weibull_5k_train.pkl'). - - 'data_key' (str): The key of the data to load (default is 'data_key'). - - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - self._datasets = generate_weibull_dataset(5, 5000, 100) - - def evaluate_program(self, program_str: str, callable_func: callable) -> Any | None: - return evaluate(self._datasets, callable_func) - -# Task configuration for benchmark task -ENTRY_NAME = 'priority' -FUNCTION_SIGNATURE = 'def priority(...):' -IMPORT_HEADER = 'import numpy as np\nimport math' -TASK_DESCRIPTION = 'Implement a function that returns the priority with which we want to add an item to each bin.' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `priority` for the LLM4AD task.\\n\\nTask description:\\nImplement a function that returns the priority with which we want to add an item to each bin.\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\n\ndef priority(item: float, bins: np.ndarray) -> np.ndarray:\n """Returns priority with which we want to add item to each bin.\n Args:\n item: Size of item to be added to the bin.\n bins: Array of capacities for each bin.\n Return:\n Array of same size as bins with priority score of each bin.\n """\n return item - bins' -EVAL_CLASS_NAME = 'OBPEvaluation' -EVAL_KWARGS = {} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/online_bin_packing_local/generate_weibull_instances.py b/examples/benchmark_tasks/online_bin_packing_local/generate_weibull_instances.py deleted file mode 100644 index 3bc3dec8..00000000 --- a/examples/benchmark_tasks/online_bin_packing_local/generate_weibull_instances.py +++ /dev/null @@ -1,36 +0,0 @@ -import numpy as np - -def generate_weibull_dataset(num_instances, num_items, capacity_limit): - - np.random.seed(2024) - - dataset = {} - - for i in range(num_instances): - instance = { - 'capacity': capacity_limit, - 'num_items': num_items, - 'items': [] - } - - items = [] - - # Generate random samples from Weibull(45, 3) distribution - samples = np.random.weibull(3, num_items) * 45 - - # Clip the samples at the specified limit - samples = np.clip(samples, 1, capacity_limit) - - # Round the item sizes to the nearest integer - sizes = np.round(samples).astype(int) - - # Add the items to the instance - for size in sizes: - items.append(size) - - instance['items'] = np.array(items) - - if num_items not in dataset: - dataset[f'instance_{i}'] = instance - - return dataset \ No newline at end of file diff --git a/examples/benchmark_tasks/online_bin_packing_local/run_eoh.py b/examples/benchmark_tasks/online_bin_packing_local/run_eoh.py deleted file mode 100644 index 717bc37c..00000000 --- a/examples/benchmark_tasks/online_bin_packing_local/run_eoh.py +++ /dev/null @@ -1,33 +0,0 @@ -import sys - -sys.path.append('../../') # This is for finding all the modules - -from evaluation import OBPEvaluation -from llm4ad.tools.llm.llm_api_https import HttpsApi -from llm4ad.method.eoh import EoH -from llm4ad.tools.profiler import ProfilerBase - - -def main(): - llm = HttpsApi(host='xxx', # your host endpoint, e.g., 'api.openai.com', 'api.deepseek.com' - key='sk-xxx', # your key, e.g., 'sk-abcdefghijklmn' - model='xxx', # your llm, e.g., 'gpt-3.5-turbo' - timeout=60) - - task = OBPEvaluation() # local - - method = EoH(llm=llm, - profiler=ProfilerBase(log_dir='logs/eoh', log_style='simple'), - evaluation=task, - max_sample_nums=20, - max_generations=10, - pop_size=4, - num_samplers=1, - num_evaluators=1, - debug_mode=False) - - method.run() - - -if __name__ == '__main__': - main() diff --git a/examples/benchmark_tasks/optimization_admissible_set/__init__.py b/examples/benchmark_tasks/optimization_admissible_set/__init__.py deleted file mode 100644 index 990e4f97..00000000 --- a/examples/benchmark_tasks/optimization_admissible_set/__init__.py +++ /dev/null @@ -1,256 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_admissible_set -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# Module Name: ASPEvaluation -# Last Revision: 2025/2/14 -# Description: Evaluates admissible sets for symmetric constant-weight optimization problems. -# This module is part of the LLM4AD project (https://github.com/Optima-CityU/llm4ad). -# -# Parameters: -# - dimension: int - The dimension of the problem space (default: 15). -# - weight: int - The weight constraint for the admissible set (default: 10). -# - timeout_seconds: int - Maximum allowed time (in seconds) for the evaluation process (default: 60). -# -# References: -# - Bernardino Romera-Paredes, Mohammadamin Barekatain, Alexander Novikov, -# Matej Balog, M. Pawan Kumar, Emilien Dupont, Francisco JR Ruiz et al. -# "Mathematical discoveries from program search with large language models." -# Nature 625, no. 7995 (2024): 468-475. -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - - -from __future__ import annotations - -import itertools -from typing import Any, List, Tuple -import numpy as np - -from llm4ad_loader import Evaluation -# from llm4ad.task.optimization.admissible_set.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import math\nimport numpy as np\n\ndef priority(el: tuple[int, ...], n: int = 15, w: int = 10) -> float:\n """Returns the priority with which we want to add `el` to the set.\n Args:\n el: the unique vector has the same number w of non-zero elements.\n n : length of the vector.\n w : number of non-zero elements.\n """\n return 0.' -task_description = '"""\\' - - -__all__ = ['ASPEvaluation'] - -class ASPEvaluation(Evaluation): - """Evaluator for online bin packing problem.""" - - def __init__(self, timeout_seconds=60, dimension=15, weight=10, **kwargs): - """ - Args: - - 'dimension' (int): The dimension of tested case (default is 15). - - 'weight' (int): The wight of tested case (default is 10). - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - self.dimension = dimension - self.weight = weight - - - self.TRIPLES = [(0, 0, 0), (0, 0, 1), (0, 0, 2), (0, 1, 2), (0, 2, 1), (1, 1, 1), (2, 2, 2)] - self.INT_TO_WEIGHT = [0, 1, 1, 2, 2, 3, 3] - self.Optimal_Set_Length = { - "n12w7": 792, - "n15w10": 3003, - "n21w15": 43596, - "n24w17": 237984 - } - - - def expand_admissible_set(self, pre_admissible_set: List[Tuple[int, ...]]) -> List[Tuple[int, ...]]: - """Expands a pre-admissible set into an admissible set.""" - num_groups = len(pre_admissible_set[0]) - admissible_set_15_10 = [] - for row in pre_admissible_set: - rotations = [[] for _ in range(num_groups)] - for i in range(num_groups): - x, y, z = self.TRIPLES[row[i]] - rotations[i].append((x, y, z)) - if not x == y == z: - rotations[i].append((z, x, y)) - rotations[i].append((y, z, x)) - product = list(itertools.product(*rotations)) - concatenated = [sum(xs, ()) for xs in product] - admissible_set_15_10.extend(concatenated) - return admissible_set_15_10 - - - def get_surviving_children(self, extant_elements, new_element, valid_children): - """Returns the indices of `valid_children` that remain valid after adding `new_element` to `extant_elements`.""" - bad_triples = {(0, 0, 0), (0, 1, 1), (0, 2, 2), (0, 3, 3), (0, 4, 4), (0, 5, 5), (0, 6, 6), (1, 1, 1), - (1, 1, 2), - (1, 2, 2), (1, 2, 3), (1, 2, 4), (1, 3, 3), (1, 4, 4), (1, 5, 5), (1, 6, 6), (2, 2, 2), - (2, 3, 3), - (2, 4, 4), (2, 5, 5), (2, 6, 6), (3, 3, 3), (3, 3, 4), (3, 4, 4), (3, 4, 5), (3, 4, 6), - (3, 5, 5), - (3, 6, 6), (4, 4, 4), (4, 5, 5), (4, 6, 6), (5, 5, 5), (5, 5, 6), (5, 6, 6), (6, 6, 6)} - - # Compute. - valid_indices = [] - for index, child in enumerate(valid_children): - # Invalidate based on 2 elements from `new_element` and 1 element from a - # potential child. - if all(self.INT_TO_WEIGHT[x] <= self.INT_TO_WEIGHT[y] - for x, y in zip(new_element, child)): - continue - # Invalidate based on 1 element from `new_element` and 2 elements from a - # potential child. - if all(self.INT_TO_WEIGHT[x] >= self.INT_TO_WEIGHT[y] - for x, y in zip(new_element, child)): - continue - # Invalidate based on 1 element from `extant_elements`, 1 element from - # `new_element`, and 1 element from a potential child. - is_invalid = False - for extant_element in extant_elements: - if all(tuple(sorted((x, y, z))) in bad_triples - for x, y, z in zip(extant_element, new_element, child)): - is_invalid = True - break - if is_invalid: - continue - - valid_indices.append(index) - return valid_indices - - - def evaluate(self, priority: callable) -> int: - - """Generates a symmetric constant-weight admissible set I(n, w).""" - num_groups = self.dimension // 3 - assert 3 * num_groups == self.dimension - - # Compute the scores of all valid (weight w) children. - valid_children = [] - for child in itertools.product(range(7), repeat=num_groups): - weight = sum(self.INT_TO_WEIGHT[x] for x in child) - if weight == self.weight: - valid_children.append(np.array(child, dtype=np.int32)) - - valid_scores = np.array([ - priority(sum([self.TRIPLES[x] for x in xs], ()), self.dimension, self.weight) for xs in valid_children]) - - # Greedy search guided by the scores. - pre_admissible_set = np.empty((0, num_groups), dtype=np.int32) - while valid_children: - max_index = np.argmax(valid_scores) - max_child = valid_children[max_index] - surviving_indices = self.get_surviving_children(pre_admissible_set, max_child, valid_children) - valid_children = [valid_children[i] for i in surviving_indices] - valid_scores = valid_scores[surviving_indices] - - pre_admissible_set = np.concatenate([pre_admissible_set, max_child[None]], axis=0) - - admissible_set = np.array(self.expand_admissible_set(pre_admissible_set)) - - return (len(admissible_set) - self.Optimal_Set_Length[f"n{self.dimension}w{self.weight}"]) - - - def evaluate_program(self, program_str: str, callable_func: callable) -> Any | None: - return self.evaluate(callable_func) - - -if __name__ == '__main__': - def priority(el: tuple, n: int, w: int) -> float: - """Design a novel algorithm to evaluate a vector for potential inclusion in a set - Args: - el: Candidate vectors for the admissible set. - n: Number of dimensions and the length of a vector. - w: Weight of each vector. - - Return: - The priorities of `el`. - """ - priorities = sum([abs(i) for i in el]) / n - return priorities - - eval = ASPEvaluation() - res = eval.evaluate_program('', priority) - print(res) - -# Task configuration for benchmark task -ENTRY_NAME = 'priority' -FUNCTION_SIGNATURE = 'def priority(...):' -IMPORT_HEADER = 'import math\nimport numpy as np' -TASK_DESCRIPTION = '"""\\' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `priority` for the LLM4AD task.\\n\\nTask description:\\n"""\\\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import math\nimport numpy as np\n\ndef priority(el: tuple[int, ...], n: int = 15, w: int = 10) -> float:\n """Returns the priority with which we want to add `el` to the set.\n Args:\n el: the unique vector has the same number w of non-zero elements.\n n : length of the vector.\n w : number of non-zero elements.\n """\n return 0.' -EVAL_CLASS_NAME = 'ASPEvaluation' -EVAL_KWARGS = {'dimension': 15, 'weight': 10, 'timeout_seconds': 20} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_admissible_set/paras.yaml b/examples/benchmark_tasks/optimization_admissible_set/paras.yaml deleted file mode 100644 index 5f7512aa..00000000 --- a/examples/benchmark_tasks/optimization_admissible_set/paras.yaml +++ /dev/null @@ -1,4 +0,0 @@ -name: ASPEvaluation -dimension: 15 -weight: 10 -timeout_seconds: 20 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_aircraft_landing/__init__.py b/examples/benchmark_tasks/optimization_aircraft_landing/__init__.py deleted file mode 100644 index 203d8724..00000000 --- a/examples/benchmark_tasks/optimization_aircraft_landing/__init__.py +++ /dev/null @@ -1,450 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_aircraft_landing -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# References: -# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language -# model agents in algorithm search for combinatorial optimization. -# arXiv preprint arXiv:2504.04310 (2025). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -from typing import Any -import numpy as np -from llm4ad_loader import Evaluation -from llm4ad_loader import load_subdir_as_text -# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader -# from llm4ad.task.optimization.co_bench.aircraft_landing_co_bench.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(num_planes: int, num_runways: int, freeze_time: float, planes: list[dict], separation: list[list[int]]) -> dict:\n """\n Problem:\n Given an instance of the Aircraft Landing Scheduling Problem, schedule the landing time for each plane and assign a runway so that:\n - Each landing time is within its allowed time window.\n - Each plane is assigned to one runway (from the available runways).\n - For any two planes assigned to the same runway, if plane i lands at or before plane j, then the landing times must be separated by at least\n the specified separation time (provided in the input data).\n - The overall penalty is minimized. For each plane, if its landing time is earlier than its target time, a penalty\n is incurred proportional to the earliness; if later than its target time, a penalty proportional to the lateness is incurred.\n - If any constraint is violated, the solution receives no score.\n Input kwargs:\n num_planes : (int) Number of planes.\n num_runways : (int) Number of runways.\n freeze_time : (float) Freeze time (unused in scheduling decisions).\n planes : (list of dict) Each dictionary contains:\n - "appearance" : float, time the plane appears.\n - "earliest" : float, earliest landing time.\n - "target" : float, target landing time.\n - "latest" : float, latest landing time.\n - "penalty_early" : float, penalty per unit time landing early.\n - "penalty_late" : float, penalty per unit time landing late.\n separation : (list of lists) separation[i][j] is the required gap after plane i lands before plane j can land\n when they are assigned to the same runway.\n Returns:\n A dictionary named "schedule" mapping each plane id (1-indexed) to a dictionary with its scheduled landing time\n and assigned runway, e.g., {"schedule": { plane_id: {"landing_time": float, "runway": int}, ... }}.\n """\n # -----------------------\n # For demonstration purposes, we simply schedule each plane at its target time\n # and assign all planes to runway 1.\n # (Note: This solution may be infeasible if targets do not satisfy separation constraints.)\n schedule = {}\n for i, plane in enumerate(planes, start=1):\n schedule[i] = {"landing_time": plane["target"], "runway": 1}\n return {"schedule": schedule}' -task_description = '("The problem is to schedule landing times for a set of planes across one or more runways such that "' - - -__all__ = ['ALEvaluationCB'] - - -class ALEvaluationCB(Evaluation): - """Evaluator for aircraft landing.""" - - def __init__(self, - timeout_seconds=300, - **kwargs): - - """ - Args: - None - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - # Load datasets from Hugging Face - dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Aircraft landing") - self._datasets = {} - for i in range(1, 14): # airland1 to airland13 - filename = f"airland{i}.txt" - if filename in dataset: - # Join all text rows into a single string - text_content = '\n'.join([row['text'] for row in dataset[filename]]) - self._datasets[filename] = text_content - - def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: - return self.evaluate(callable_func) - - def evaluate(self, eva: callable) -> float | None: - ins_cases = [] - - # Define runway configurations for each dataset (corresponds to airland1-13) - runway_configs = [[1, 2, 3], - [1, 2, 3], - [1, 2, 3], - [1, 2, 3, 4], - [1, 2, 3, 4], - [1, 2, 3], - [1, 2], - [1, 2, 3], - [1, 2, 3, 4], - [1, 2, 3, 4, 5], - [1, 2, 3, 4, 5], - [1, 2, 3, 4, 5], - [1, 2, 3, 4, 5]] - - for case_id, ins in enumerate(self._datasets.values()): - base_case = self.load_data(ins) - # Create variations with different runway configurations - for num_runways in runway_configs[case_id]: - case_with_runways = base_case.copy() - case_with_runways['num_runways'] = num_runways - ins_cases.append(case_with_runways) - - penalties = [] - try: - for case in ins_cases: - schedule = eva(case['num_planes'], case['num_runways'], case['freeze_time'], case['planes'], case['separation']) - penalty = self.eval_func(num_planes=case['num_planes'], num_runways=case['num_runways'], - freeze_time=case['freeze_time'], separation=case['separation'], planes=case['planes'], - schedule=schedule) - penalties.append(penalty) - - return -np.mean(penalties) - - except ValueError as e: - print(e) - return None - - def load_data(self, input_str): - """ - Reads the aircraft landing scheduling problem instance from a string. - The string contains a single case with the following format: - Line 1: - For each plane (i = 1, …, num_planes): - - A line with 6 numbers: - appearance_time earliest_landing_time target_landing_time - latest_landing_time penalty_cost_early penalty_cost_late - - One or more subsequent lines containing exactly num_planes separation times. - (Separation times for plane i with respect to planes 1..num_planes. They may span multiple lines.) - Returns: - A dictionary containing the keys: - - "num_planes" : int - - "freeze_time" : float - - "planes" : list of dicts (one per plane) - - "separation" : list of lists of floats - """ - all_lines = input_str.split("\n") - all_lines = [line.strip() for line in all_lines if line.strip()] - - idx = 0 - total_lines = len(all_lines) - - # Parse the first line: num_planes and freeze_time. - try: - tokens = all_lines[idx].split() - num_planes = int(tokens[0]) - freeze_time = float(tokens[1]) - except Exception as e: - raise ValueError(f"Error parsing case header at line {idx + 1}: {e}") - idx += 1 - - planes = [] - separation = [] - - for plane_index in range(num_planes): - if idx >= total_lines: - raise ValueError(f"Insufficient lines for plane {plane_index + 1} parameters.") - params_tokens = all_lines[idx].split() - idx += 1 - if len(params_tokens) < 6: - raise ValueError(f"Plane {plane_index + 1}: Expected 6 parameters, got {len(params_tokens)}.") - try: - appearance = float(params_tokens[0]) - earliest = float(params_tokens[1]) - target = float(params_tokens[2]) - latest = float(params_tokens[3]) - penalty_early = float(params_tokens[4]) - penalty_late = float(params_tokens[5]) - except Exception as e: - raise ValueError(f"Plane {plane_index + 1}: Error converting parameters: {e}") - - planes.append({ - "appearance": appearance, - "earliest": earliest, - "target": target, - "latest": latest, - "penalty_early": penalty_early, - "penalty_late": penalty_late - }) - - # Read exactly num_planes separation times (may span multiple lines) - sep_tokens = [] - while len(sep_tokens) < num_planes: - if idx >= total_lines: - raise ValueError(f"Not enough lines to read separation times for plane {plane_index + 1}.") - sep_tokens.extend(all_lines[idx].split()) - idx += 1 - # In case more tokens were read than needed: - sep_tokens = sep_tokens[:num_planes] - try: - sep_times = [float(token) for token in sep_tokens] - except Exception as e: - raise ValueError(f"Plane {plane_index + 1}: Error converting separation times: {e}") - separation.append(sep_times) - - # Return a single case dictionary (without num_runways, as that will be added later) - return { - "num_planes": num_planes, - "freeze_time": freeze_time, - "planes": planes, - "separation": separation, - } - - def eval_func(self, **kwargs): - """ - Evaluates a proposed aircraft landing schedule. - Expects the following keys in kwargs: - - num_planes : int, number of planes. - - num_runways : int, number of runways. - - freeze_time : float. - - planes : list of dicts, each containing: - "earliest", "target", "latest", "penalty_early", "penalty_late". - - separation : list of lists (floats), where separation[i][j] is the required gap after plane i lands - before plane j can land when they are assigned to the same runway. - - schedule : dict mapping plane_id (1-indexed) to a dict with keys: - "landing_time" (float) and "runway" (int). - The evaluation performs these checks: - 1. Each plane's landing time is within its allowed time window. - 2. Each plane is assigned to a runway in the range [1, num_runways]. - 3. For every two distinct planes i and j assigned to the same runway, - if plane i lands at or before plane j then the gap must be at least - the required separation time. - The total penalty is computed as follows for each plane: - - If landing_time < target: penalty = (target - landing_time) * penalty_early. - - If landing_time > target: penalty = (landing_time - target) * penalty_late. - - If landing_time == target: no penalty. - Returns: - The total penalty (a float) if the schedule is feasible. - Raises: - ValueError with an informative message if any constraint is violated. - """ - # Extract required parameters. - num_planes = kwargs.get("num_planes") - num_runways = kwargs.get("num_runways") - planes = kwargs.get("planes") - separation = kwargs.get("separation") - schedule = kwargs.get("schedule") - - # Check that schedule has exactly num_planes entries. - if not isinstance(schedule, dict) or len(schedule) != num_planes: - raise ValueError(f"Schedule must be a dict with exactly {num_planes} entries.") - - for plane_id in range(1, num_planes + 1): - if plane_id not in schedule: - raise ValueError(f"Plane {plane_id} is missing in the schedule.") - # Each schedule entry must be a dict with 'landing_time' and 'runway' - entry = schedule[plane_id] - if not isinstance(entry, dict) or "landing_time" not in entry or "runway" not in entry: - raise ValueError(f"Schedule entry for plane {plane_id} must contain 'landing_time' and 'runway' keys.") - # Check runway assignment is valid. - runway = entry["runway"] - if not isinstance(runway, int) or runway < 1 or runway > num_runways: - raise ValueError( - f"Plane {plane_id} assigned runway {runway} is invalid. Must be between 1 and {num_runways}.") - - # 1. Check landing time window constraints. - for i in range(1, num_planes + 1): - landing_time = schedule[i]["landing_time"] - earliest = planes[i - 1]["earliest"] - latest = planes[i - 1]["latest"] - if landing_time < earliest or landing_time > latest: - raise ValueError( - f"Plane {i}: Landing time {landing_time} is outside the allowed window [{earliest}, {latest}]." - ) - - # 2. Check separation constraints for planes on the same runway. - for i in range(1, num_planes + 1): - for j in range(1, num_planes + 1): - if i == j: - continue - entry_i = schedule[i] - entry_j = schedule[j] - # Only check separation if both planes are assigned to the same runway. - if entry_i["runway"] == entry_j["runway"]: - L_i = entry_i["landing_time"] - L_j = entry_j["landing_time"] - # If plane i lands no later than plane j, check the required separation. - if L_i <= L_j: - required_gap = separation[i - 1][j - 1] - if (L_j - L_i) < required_gap: - raise ValueError( - f"Separation violation on runway {entry_i['runway']}: Plane {i} lands at {L_i} and Plane {j} at {L_j} " - f"(required gap: {required_gap})." - ) - - # 3. Compute total penalty. - total_penalty = 0.0 - for i in range(1, num_planes + 1): - landing_time = schedule[i]["landing_time"] - target = planes[i - 1]["target"] - if landing_time < target: - penalty = (target - landing_time) * planes[i - 1]["penalty_early"] - elif landing_time > target: - penalty = (landing_time - target) * planes[i - 1]["penalty_late"] - else: - penalty = 0.0 - total_penalty += penalty - - return total_penalty - - def norm_score(self, results): - optimal_scores = { - "airland1.txt": [700, 90, 0], - "airland2.txt": [1480, 210, 0], - "airland3.txt": [820, 60, 0], - "airland4.txt": [2520, 640, 130, 0], - "airland5.txt": [3100, 650, 170, 0], - "airland6.txt": [24442, 554, 0], - "airland7.txt": [1550, 0], - "airland8.txt": [1950, 135, 0], - "airland9.txt": [7848.42, 573.25, 88.72, 0.0], - "airland10.txt": [17726.06, 1372.21, 246.15, 34.22, 0.0], - "airland11.txt": [19327.45, 1683.75, 333.53, 69.66, 0.0], - "airland12.txt": [2549.24, 2204.96, 430.5, 2.86, 0.0], - "airland13.txt": [58392.69, 4897.92, 821.82, 123.3, 0.0], - } - - normed = {} - for case, (scores, error_message) in results.items(): - if case not in optimal_scores: - continue # Skip if there's no optimal score defined. - optimal_list = optimal_scores[case] - normed_scores = [] - # Compute normalized score for each index. - for idx, score in enumerate(scores): - if isinstance(score, (int, float)): - if optimal_list[idx] == 0: - normed_scores.append((optimal_list[idx] + 1) / (score + 1)) - else: - normed_scores.append(optimal_list[idx] / score) - else: - normed_scores.append(score) - normed[case] = (normed_scores, error_message) - - return normed - - def get_dev(self): - dev = {'airland1.txt': [0], 'airland10.txt': [2, 1], 'airland11.txt': [0, 1], 'airland12.txt': [3, 4], - 'airland13.txt': [0, 3], 'airland2.txt': [2], 'airland3.txt': [2], 'airland4.txt': [1, 3], - 'airland5.txt': [0, 1], - 'airland6.txt': [1], 'airland7.txt': [1], 'airland8.txt': [2], 'airland9.txt': [0, 1]} - return dev - -if __name__ == "__main__": - evaluator = ALEvaluationCB() - import random - - def solve(num_planes: int, num_runways: int, freeze_time: float, planes: list[dict], - separation: list[list[int]]) -> dict: - """ - Problem: - Given an instance of the Aircraft Landing Scheduling Problem, schedule the landing time for each plane and assign a runway so that: - - Each landing time is within its allowed time window. - - Each plane is assigned to one runway (from the available runways). - - For any two planes assigned to the same runway, if plane i lands at or before plane j, then the landing times must be separated by at least - the specified separation time (provided in the input data). - - The overall penalty is minimized. For each plane, if its landing time is earlier than its target time, a penalty - is incurred proportional to the earliness; if later than its target time, a penalty proportional to the lateness is incurred. - - If any constraint is violated, the solution receives no score. - Input kwargs: - num_planes : (int) Number of planes. - num_runways : (int) Number of runways. - freeze_time : (float) Freeze time (unused in scheduling decisions). - planes : (list of dict) Each dictionary contains: - - "appearance" : float, time the plane appears. - - "earliest" : float, earliest landing time. - - "target" : float, target landing time. - - "latest" : float, latest landing time. - - "penalty_early" : float, penalty per unit time landing early. - - "penalty_late" : float, penalty per unit time landing late. - separation : (list of lists) separation[i][j] is the required gap after plane i lands before plane j can land - when they are assigned to the same runway. - Returns: - A dictionary named "schedule" mapping each plane id (1-indexed) to a dictionary with its scheduled landing time - and assigned runway, e.g., { plane_id: {"landing_time": float, "runway": int}, ... }. - """ - # ----------------------- - # For demonstration purposes, we simply schedule each plane at its target time - # and assign all planes to runway 1. - # (Note: This solution may be infeasible if targets do not satisfy separation constraints.) - schedule = {} - for i, plane in enumerate(planes, start=1): - schedule[i] = {"landing_time": plane["target"], "runway": random.randint(1, num_runways + 1)} - return {"schedule": schedule} - - results = evaluator.evaluate_program('', solve) - print(results) - -# Task configuration for benchmark task -ENTRY_NAME = 'solve' -FUNCTION_SIGNATURE = 'def solve(...):' -IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' -TASK_DESCRIPTION = '("The problem is to schedule landing times for a set of planes across one or more runways such that "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The problem is to schedule landing times for a set of planes across one or more runways such that "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(num_planes: int, num_runways: int, freeze_time: float, planes: list[dict], separation: list[list[int]]) -> dict:\n """\n Problem:\n Given an instance of the Aircraft Landing Scheduling Problem, schedule the landing time for each plane and assign a runway so that:\n - Each landing time is within its allowed time window.\n - Each plane is assigned to one runway (from the available runways).\n - For any two planes assigned to the same runway, if plane i lands at or before plane j, then the landing times must be separated by at least\n the specified separation time (provided in the input data).\n - The overall penalty is minimized. For each plane, if its landing time is earlier than its target time, a penalty\n is incurred proportional to the earliness; if later than its target time, a penalty proportional to the lateness is incurred.\n - If any constraint is violated, the solution receives no score.\n Input kwargs:\n num_planes : (int) Number of planes.\n num_runways : (int) Number of runways.\n freeze_time : (float) Freeze time (unused in scheduling decisions).\n planes : (list of dict) Each dictionary contains:\n - "appearance" : float, time the plane appears.\n - "earliest" : float, earliest landing time.\n - "target" : float, target landing time.\n - "latest" : float, latest landing time.\n - "penalty_early" : float, penalty per unit time landing early.\n - "penalty_late" : float, penalty per unit time landing late.\n separation : (list of lists) separation[i][j] is the required gap after plane i lands before plane j can land\n when they are assigned to the same runway.\n Returns:\n A dictionary named "schedule" mapping each plane id (1-indexed) to a dictionary with its scheduled landing time\n and assigned runway, e.g., {"schedule": { plane_id: {"landing_time": float, "runway": int}, ... }}.\n """\n # -----------------------\n # For demonstration purposes, we simply schedule each plane at its target time\n # and assign all planes to runway 1.\n # (Note: This solution may be infeasible if targets do not satisfy separation constraints.)\n schedule = {}\n for i, plane in enumerate(planes, start=1):\n schedule[i] = {"landing_time": plane["target"], "runway": 1}\n return {"schedule": schedule}' -EVAL_CLASS_NAME = 'ALEvaluationCB' -EVAL_KWARGS = {'timeout_seconds': 300} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_aircraft_landing/paras.yaml b/examples/benchmark_tasks/optimization_aircraft_landing/paras.yaml deleted file mode 100644 index 45aaac57..00000000 --- a/examples/benchmark_tasks/optimization_aircraft_landing/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: ALEvaluationCB -timeout_seconds: 300 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_assignment_problem/__init__.py b/examples/benchmark_tasks/optimization_assignment_problem/__init__.py deleted file mode 100644 index 80ba5bec..00000000 --- a/examples/benchmark_tasks/optimization_assignment_problem/__init__.py +++ /dev/null @@ -1,327 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_assignment_problem -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# References: -# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language -# model agents in algorithm search for combinatorial optimization. -# arXiv preprint arXiv:2504.04310 (2025). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -from typing import Any -import numpy as np -from llm4ad_loader import Evaluation -from llm4ad_loader import load_subdir_as_text -# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader -# from llm4ad.task.optimization.co_bench.assignment_problem_co_bench.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\nfrom scipy.optimize import linear_sum_assignment\ndef solve(num_items: int, cost_matrix: np.ndarray) -> dict:\n """\n Solves an instance of the Assignment Problem.\n Given n items and an n×n cost matrix (where cost_matrix[i][j] is the cost of assigning\n item (i+1) to agent (j+1)), the goal is to determine a permutation (a one-to-one assignment\n between items and agents) that minimizes the total cost. The returned solution is a\n dictionary with:\n - "total_cost": The sum of the costs of the chosen assignments.\n - "assignment": A list of n tuples (i, j), where i is the item number (1-indexed)\n and j is the assigned agent number (1-indexed).\n Input kwargs:\n - n: int, the number of items/agents.\n - cost_matrix: numpy.ndarray, a 2D array with shape (n, n) containing the costs.\n Returns:\n A dictionary with keys "total_cost" and "assignment" representing the optimal solution.\n """\n # Your algorithm implementation goes here.\n # For example, you may use the Hungarian algorithm.\n return {"total_cost": None, "assignment": None}' -task_description = '("The Assignment Problem involves optimally assigning n items to n agents based on a provided "' - - -__all__ = ['APEvaluationCB'] - - -class APEvaluationCB(Evaluation): - """Evaluator for assignment problem.""" - - def __init__(self, - timeout_seconds=50, - **kwargs): - - """ - Args: - None - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - # Load datasets from Hugging Face - dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Assignment problem") - self._datasets = {} - for filename in dataset: - # Join all text rows into a single string - text_content = '\n'.join([row['text'] for row in dataset[filename]]) - self._datasets[filename] = text_content - - def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: - return self.evaluate(callable_func) - - def evaluate(self, eva: callable) -> float | None: - ins_cases = [] - for case_id, ins in enumerate(self._datasets.values()): - ins_cases.append(self.load_data(ins)) - - fitness_list = [] - try: - for i in ins_cases: - for j in i: - result = eva(j['n'], j['cost_matrix']) - fitness = self.eval_func(n=j['n'], cost_matrix=j['cost_matrix'], total_cost=result['total_cost'], assignment=result['assignment']) - fitness_list.append(fitness) - - return -np.mean(fitness_list) - - except ValueError as e: - print(e) - return None - - def load_data(self, input_string): - """ - Reads input string content and separates it into multiple cases for the assignment problem. - The input is expected to contain one or more cases. Each case has the following format: - - The first non-empty line of the case is a single integer n (the number of items/agents). - - The remaining tokens in the case provide the cost information. This can be in one of two formats: - 1. Dense Format: Exactly n*n numeric tokens (row-major order). - 2. Sparse Format: A sequence of tokens in groups of three: (i, j, cost). Any (i,j) - not specified is assigned a cost equal to 1000 times the maximum provided cost in that row. - Cases in the input are separated by one or more blank lines. - Parameters: - input_string (str): The input content as string. - Returns: - A list of dictionaries, each containing: - - "n": int, the number of items. - - "cost_matrix": numpy.ndarray of shape (n, n) with the assignment costs. - """ - import math - - all_lines = [line.rstrip() for line in input_string.split('\n')] - - # Group lines into cases using blank lines as delimiters. - cases = [] - current_block = [] - for line in all_lines: - if line.strip() == "": - if current_block: - cases.append(current_block) - current_block = [] - else: - current_block.append(line.strip()) - if current_block: - cases.append(current_block) - - case_list = [] - for block in cases: - if not block: - continue - try: - n = int(block[0]) - except Exception as e: - raise ValueError("The first line of each case must be an integer representing n.") from e - - tokens = [] - for line in block[1:]: - tokens.extend(line.split()) - - # Determine the format. - if len(tokens) == n * n: - try: - numbers = [float(token) for token in tokens] - except Exception as e: - raise ValueError("Non-numeric token found in dense format.") from e - cost_matrix = np.array(numbers).reshape(n, n) - elif len(tokens) % 3 == 0: - cost_matrix = np.full((n, n), math.inf) - for idx in range(0, len(tokens), 3): - try: - i = int(tokens[idx]) - j = int(tokens[idx + 1]) - cost = float(tokens[idx + 2]) - except Exception as e: - raise ValueError("Invalid token encountered in sparse format.") from e - if not (1 <= i <= n and 1 <= j <= n): - raise ValueError(f"Indices out of range in sparse format: i={i}, j={j}.") - cost_matrix[i - 1][j - 1] = cost - # Set unspecified assignments. - for i in range(n): - if np.all(np.isinf(cost_matrix[i])): - raise ValueError(f"Row {i + 1} has no valid assignments.") - max_finite = np.max(cost_matrix[i][np.isfinite(cost_matrix[i])]) - cost_matrix[i][np.isinf(cost_matrix[i])] = max_finite * 1000 - else: - raise ValueError( - "Input case format not recognized. Expect either n*n tokens (dense) or a multiple of 3 tokens (sparse).") - - case_list.append({"n": n, "cost_matrix": cost_matrix}) - return case_list - - def eval_func(self, **kwargs): - """ - Evaluates the solution of the Assignment Problem for a single case. - Parameters: - - case (dict): A dictionary containing the case data with keys: - * "n": int, the number of items/agents. - * "cost_matrix": numpy.ndarray, the cost matrix. - - solution (dict): A dictionary representing the solution returned by solve(), with keys: - * "total_cost": numeric, the total cost reported by the solution. - * "assignment": list of tuples (i, j) where i is the item (1-indexed) and j is the assigned agent (1-indexed). - Returns: - A numeric score representing the total cost computed from the cost_matrix based on the provided assignment. - The function performs the following checks: - - Each item (1 to n) must be assigned exactly once. - - Each agent (1 to n) must be assigned exactly once. - - The computed total cost (from the cost_matrix and assignment) must match the reported total_cost - (within a small tolerance). If not, the computed total is used. - """ - import math - - n = kwargs.get("n") - cost_matrix = kwargs.get("cost_matrix") - - # Validate the assignment. - assignment = {} # Maps item i to agent j. - assigned_agents = set() - if not isinstance(kwargs.get("assignment"), list): - raise ValueError("Solution must include an 'assignment' list.") - for idx, pair in enumerate(kwargs["assignment"], start=1): - if not (isinstance(pair, (list, tuple)) and len(pair) == 2): - raise ValueError(f"Assignment entry {idx} must be a tuple/list of two integers (i, j).") - i_val, j_val = pair - if i_val in assignment: - raise ValueError(f"Duplicate assignment for item {i_val} found.") - if j_val in assigned_agents: - raise ValueError(f"Agent {j_val} assigned more than once.") - if not (1 <= i_val <= n and 1 <= j_val <= n): - raise ValueError(f"Assignment indices ({i_val}, {j_val}) are out of range (must be between 1 and {n}).") - assignment[i_val] = j_val - assigned_agents.add(j_val) - - if len(assignment) != n: - raise ValueError(f"Incomplete assignment: expected {n} assignments, but got {len(assignment)}.") - - # Compute the total cost based on the assignment. - computed_total = 0.0 - for i in range(1, n + 1): - j_val = assignment[i] - cost = cost_matrix[i - 1][j_val - 1] - if cost == math.inf: - raise ValueError(f"Assignment ({i}, {j_val}) has an infinite cost, hence invalid.") - computed_total += cost - - return computed_total - - def norm_score(self, results): - optimal_scores = { - "assign100.txt": [305], - "assign200.txt": [475], - "assign300.txt": [626], - "assign400.txt": [804], - "assign500.txt": [991], - "assign600.txt": [1176], - "assign700.txt": [1362], - "assign800.txt": [1552], - "assignp800.txt": [2239], - "assignp1500.txt": [5839], - "assignp3000.txt": [18696], - "assignp5000.txt": [48533], - } - - normed = {} - for case, (scores, error_message) in results.items(): - if case not in optimal_scores: - continue # Skip if there's no optimal score defined. - optimal_list = optimal_scores[case] - normed_scores = [] - # Compute normalized score for each index. - for idx, score in enumerate(scores): - if isinstance(score, (int, float)): - normed_scores.append(score / optimal_list[idx]) - else: - normed_scores.append(score) - normed[case] = (normed_scores, error_message) - - return normed - - def get_dev(self): - dev = {'assign100.txt': [0], 'assign400.txt': [0], 'assign700.txt': [0], 'assignp3000.txt': [0]} - - return dev - -# Task configuration for benchmark task -ENTRY_NAME = 'solve' -FUNCTION_SIGNATURE = 'def solve(...):' -IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\nfrom scipy.optimize import linear_sum_assignment' -TASK_DESCRIPTION = '("The Assignment Problem involves optimally assigning n items to n agents based on a provided "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The Assignment Problem involves optimally assigning n items to n agents based on a provided "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\nfrom scipy.optimize import linear_sum_assignment\ndef solve(num_items: int, cost_matrix: np.ndarray) -> dict:\n """\n Solves an instance of the Assignment Problem.\n Given n items and an n×n cost matrix (where cost_matrix[i][j] is the cost of assigning\n item (i+1) to agent (j+1)), the goal is to determine a permutation (a one-to-one assignment\n between items and agents) that minimizes the total cost. The returned solution is a\n dictionary with:\n - "total_cost": The sum of the costs of the chosen assignments.\n - "assignment": A list of n tuples (i, j), where i is the item number (1-indexed)\n and j is the assigned agent number (1-indexed).\n Input kwargs:\n - n: int, the number of items/agents.\n - cost_matrix: numpy.ndarray, a 2D array with shape (n, n) containing the costs.\n Returns:\n A dictionary with keys "total_cost" and "assignment" representing the optimal solution.\n """\n # Your algorithm implementation goes here.\n # For example, you may use the Hungarian algorithm.\n return {"total_cost": None, "assignment": None}' -EVAL_CLASS_NAME = 'APEvaluationCB' -EVAL_KWARGS = {'timeout_seconds': 20} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_assignment_problem/paras.yaml b/examples/benchmark_tasks/optimization_assignment_problem/paras.yaml deleted file mode 100644 index 4577df36..00000000 --- a/examples/benchmark_tasks/optimization_assignment_problem/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: APEvaluationCB -timeout_seconds: 20 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_assortment_problem/__init__.py b/examples/benchmark_tasks/optimization_assortment_problem/__init__.py deleted file mode 100644 index bec00adb..00000000 --- a/examples/benchmark_tasks/optimization_assortment_problem/__init__.py +++ /dev/null @@ -1,388 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_assortment_problem -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# References: -# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language -# model agents in algorithm search for combinatorial optimization. -# arXiv preprint arXiv:2504.04310 (2025). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -from typing import Any -import numpy as np -from llm4ad_loader import Evaluation -from llm4ad_loader import load_subdir_as_text -# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader -# from llm4ad.task.optimization.co_bench.assortment_problem_co_bench.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(m: int, stocks: list, pieces: list) -> dict:\n """\n Solves the rectangular piece arrangement optimization problem to minimize the overall waste area percentage.\n Given:\n - m (int): Number of piece types.\n - stocks (list of dict): Each dict represents a stock type with keys:\n \'length\' (float), \'width\' (float), \'fixed_cost\' (float).\n - pieces (list of dict): Each dict represents a piece type with keys:\n \'length\' (float), \'width\' (float), \'min\' (int), \'max\' (int), \'value\' (float).\n Objective:\n Arrange rectangular pieces (which may be rotated by 90°) into stock rectangles such that the overall waste area percentage is minimized.\n The waste area percentage is computed as:\n Waste Percentage = (Total Stock Area - Total Used Area) / (Total Stock Area)\n Constraints:\n • Each piece must lie entirely within its assigned stock rectangle.\n • Pieces must not overlap within the same stock rectangle.\n • The number of pieces placed for each piece type must lie within its specified minimum and maximum bounds.\n • You may use unlimited many instances of each selected stock type, but the solution can include at most 2 distinct stock types.\n Output:\n Returns a dictionary with two keys (exactly follow this format):\n - "objective": The overall waste area percentage (float) as computed by the evaluation function.\n - "placements": A dictionary mapping stock instance ids (1-indexed) to their placement details.\n Each stock instance is represented by a dictionary with the following keys:\n \'stock_type\': (the 1-indexed id of the stock type used for this instance),\n \'placements\': a list of placements for pieces within that stock instance.\n Each placement is a dict with keys:\n \'piece\' (piece type, 1-indexed, 1 <= piece type <= m),\n \'x\' (x-coordinate of the bottom-left corner),\n \'y\' (y-coordinate of the bottom-left corner),\n \'orientation\' (0 for normal, 1 for rotated 90°).\n NOTE: The returned data should adhere to the output format required for evaluation.\n """\n # ----- INSERT YOUR SOLUTION ALGORITHM HERE -----\n # For demonstration purposes, we provide a dummy solution that does not place any pieces.\n # In a real solution, you would compute placements that respect all constraints.\n\n # Dummy solution: Create a single stock instance of the first stock type, with no pieces placed.\n solution = {\n "objective": 0.0, # With no placements, the evaluation function would compute a waste area percentage of 0.0.\n "placements": {\n 1: {\n "stock_type": 1,\n "placements": []\n }\n }\n }\n return solution' -task_description = '("This optimization problem involves arranging a set of rectangular pieces within available stock "' - - -__all__ = ['AssortPEvaluationCB'] - - -class AssortPEvaluationCB(Evaluation): - - def __init__(self, - timeout_seconds=50, - **kwargs): - - """ - Args: - None - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - # Load datasets from Hugging Face - dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Assortment problem") - self._datasets = {} - for filename in dataset: - # Join all text rows into a single string - text_content = '\n'.join([row['text'] for row in dataset[filename]]) - self._datasets[filename] = text_content - - def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: - return self.evaluate(callable_func) - - def evaluate(self, eva: callable) -> float | None: - ins_cases = [] - for case_id, ins in enumerate(self._datasets.values()): - ins_cases.append(self.load_data(ins)) - - fitness_list = [] - try: - for i in ins_cases: - for j in i: - result = eva(j['m'], j['stocks'], j['pieces']) - fitness = self.eval_func(j['m'], j['n'], j['waste_cost'], j['stocks'], j['pieces'], result['objective'], result['placements']) - fitness_list.append(fitness) - - return -np.mean(fitness_list) - - except ValueError as e: - print(e) - return None - - def load_data(self, input_string): - """ - Loads the input data for one or more cases from a TXT file. - The file format is as follows for each case: - 1. A line with three tokens: m n waste_cost - - m: number of piece types (int) - - n: number of stock rectangles (int) - - waste_cost: cost per unit area of waste (float) - 2. Next n lines: each with "length width fixed_cost" for a stock rectangle. - 3. Next m lines: each with "length width min max value" for a piece. - If the file contains multiple cases, they should be separated by at least one blank line. - Returns: - A list of dictionaries, one per case. Each dictionary contains: - - "m": int - - "n": int - - "waste_cost": float - - "stocks": list of dicts (each with keys 'length', 'width', 'fixed_cost') - - "pieces": list of dicts (each with keys 'length', 'width', 'min', 'max', 'value') - """ - - cases = [] - lines = [line.strip() for line in input_string.split('\n') if line.strip() != ''] - - ptr = 0 - while ptr < len(lines): - # Parse first line of a case. - try: - m_str, n_str, waste_cost_str = lines[ptr].split() - m = int(m_str) - n = int(n_str) - waste_cost = float(waste_cost_str) - except Exception: - raise Exception("Error reading the case header (expected: m n waste_cost) at line {}".format(ptr + 1)) - ptr += 1 - - stocks = [] - for i in range(n): - if ptr >= len(lines): - raise Exception("Unexpected end of file while reading stocks.") - tokens = lines[ptr].split() - if len(tokens) != 3: - raise Exception("Invalid stock rectangle line at line {}: expected 3 tokens.".format(ptr + 1)) - try: - length, width, fixed_cost = float(tokens[0]), float(tokens[1]), float(tokens[2]) - except Exception: - raise Exception("Parsing error in stock rectangle at line {}.".format(ptr + 1)) - stocks.append({'length': length, 'width': width, 'fixed_cost': fixed_cost}) - ptr += 1 - - pieces = [] - for j in range(m): - if ptr >= len(lines): - raise Exception("Unexpected end of file while reading pieces.") - tokens = lines[ptr].split() - if len(tokens) != 5: - raise Exception("Invalid piece line at line {}: expected 5 tokens.".format(ptr + 1)) - try: - p_length = float(tokens[0]) - p_width = float(tokens[1]) - p_min = int(tokens[2]) - p_max = int(tokens[3]) - p_value = float(tokens[4]) - except Exception: - raise Exception("Parsing error in piece line at line {}.".format(ptr + 1)) - pieces.append({'length': p_length, 'width': p_width, 'min': p_min, 'max': p_max, 'value': p_value}) - ptr += 1 - - cases.append({ - "m": m, - "n": n, - "waste_cost": waste_cost, - "stocks": stocks, - "pieces": pieces - }) - return cases - - def eval_func(self, m, n, waste_cost, stocks, pieces, objective, placements): - """ - Evaluates the solution for the arrangement optimization problem using waste area percentage as the metric. - The overall waste area percentage is computed as: - overall_waste_percentage = (total unused area) / (total stock area) - where a lower percentage indicates better utilization. This metric disregards piece values and fixed costs. - Infeasible solutions (due to piece count constraint violations or using more than 2 distinct stock types) - will raise an exception. - Inputs: - - m (int): Number of piece types. - - n (int): (Not used directly) originally denoted the number of stock rectangles, but now placements are stock instances. - - waste_cost (float): Not used in this metric. - - stocks (list of dict): Each dict represents a stock type with keys: - 'length', 'width', 'fixed_cost'. - There is an infinite supply of each stock type. - - pieces (list of dict): Each dict represents a piece type with keys: - 'length', 'width', 'min', 'max', 'value'. - - placements (dict): Mapping from stock instance id (1-indexed) to a dictionary with keys: - 'stock_type' : (1-indexed id of the stock type used for this instance), - 'placements' : a list of placements for pieces within that stock instance. - Each placement is a dict with keys: - 'piece' (piece type, 1-indexed), - 'x' (x-coordinate of the bottom-left corner), - 'y' (y-coordinate of the bottom-left corner), - 'orientation' (0 for normal, 1 for rotated 90°). - - objective (float): Not used in this metric (provided for reference). - Returns: - The overall waste area percentage (float) if all constraints are satisfied. - Raises: - Exception: If a placement violates boundary or overlap conditions, if piece count constraints are not met, - or if more than 2 distinct stock types are used. - """ - total_piece_counts = [0] * m - total_stock_area = 0.0 - total_waste_area = 0.0 - used_stock_types = set() - - # Iterate over each stock instance in the placements. - for stock_instance_id, instance_data in placements.items(): - # Validate the stock instance structure. - if not isinstance(instance_data, - dict) or 'stock_type' not in instance_data or 'placements' not in instance_data: - raise Exception( - f"Stock instance {stock_instance_id} is missing required keys ('stock_type', 'placements').") - - stock_type = instance_data['stock_type'] - # Check stock_type is valid. - if not (1 <= stock_type <= len(stocks)): - raise Exception( - f"Stock type {stock_type} in instance {stock_instance_id} is out of valid range (should be between 1 and {len(stocks)}).") - used_stock_types.add(stock_type) - - # Retrieve stock type details and compute area. - stock = stocks[stock_type - 1] - stock_length, stock_width = stock['length'], stock['width'] - stock_area = stock_length * stock_width - total_stock_area += stock_area - - used_area = 0.0 - placed_rectangles = [] # To check for overlaps within this stock instance. - - # Process each piece placement in this stock instance. - for placement in instance_data['placements']: - piece_type = placement.get('piece') - x = placement.get('x') - y = placement.get('y') - orientation = placement.get('orientation') - - # Validate piece type. - if not (1 <= piece_type <= m): - raise Exception( - f"Piece type {piece_type} in stock instance {stock_instance_id} is out of range (should be between 1 and {m}).") - piece = pieces[piece_type - 1] - - # Determine dimensions based on orientation. - if orientation == 0: - p_len, p_wid = piece['length'], piece['width'] - elif orientation == 1: - p_len, p_wid = piece['width'], piece['length'] - else: - raise Exception( - f"Invalid orientation {orientation} for piece type {piece_type} in stock instance {stock_instance_id}.") - - # Check that the piece lies fully within the stock boundaries. - if x < 0 or y < 0 or (x + p_len) > stock_length + 1e-6 or (y + p_wid) > stock_width + 1e-6: - raise Exception( - f"Piece type {piece_type} in stock instance {stock_instance_id} is placed outside the stock boundaries.") - - # Check for overlapping pieces within the same stock instance. - rect = (x, y, x + p_len, y + p_wid) - for other in placed_rectangles: - if not (rect[2] <= other[0] or rect[0] >= other[2] or rect[3] <= other[1] or rect[1] >= other[3]): - raise Exception(f"Overlap detected in stock instance {stock_instance_id}.") - placed_rectangles.append(rect) - - used_area += p_len * p_wid - total_piece_counts[piece_type - 1] += 1 - - total_waste_area += (stock_area - used_area) - - # Verify that no more than 2 distinct stock types were used. - if len(used_stock_types) > 2: - raise Exception(f"More than 2 distinct stock types used: found {len(used_stock_types)} types.") - - # Check piece count constraints for each piece type. - for idx, piece in enumerate(pieces): - count = total_piece_counts[idx] - if count < piece['min'] or count > piece['max']: - raise Exception( - f"Piece count violation for piece type {idx + 1}: count = {count}, required min = {piece['min']}, max = {piece['max']}." - ) - - if total_stock_area == 0: - raise Exception("Total stock area is 0, invalid configuration.") - - overall_waste_percentage = total_waste_area / total_stock_area - return overall_waste_percentage - - def norm_score(self, results): - optimal_scores = { - "assort1.txt": [7.69], - "assort2.txt": [4.17], - "assort3.txt": [5.87], - "assort4.txt": [6.63], - "assort5.txt": [4.95], - "assort6.txt": [7.62], - "assort7.txt": [16.84], - "assort8.txt": [5.48], - "assort9.txt": [9.07], - "assort10.txt": [13.80], - "assort11.txt": [6.65], - "assort12.txt": [5.89], - } - - normed = {} - for case, (scores, error_message) in results.items(): - if case not in optimal_scores: - continue # Skip if there's no optimal score defined. - optimal_list = optimal_scores[case] - normed_scores = [] - # Compute normalized score for each index. - for idx, score in enumerate(scores): - if isinstance(score, (int, float)): - normed_scores.append(optimal_list[idx] / score / 100) - else: - normed_scores.append(score) - normed[case] = (normed_scores, error_message) - - return normed - - def get_dev(self): - dev = {'assort1.txt': [0], 'assort10.txt': [0], 'assort4.txt': [0], - 'assort7.txt': [0], } - - return dev - - -# Task configuration for benchmark task -ENTRY_NAME = 'solve' -FUNCTION_SIGNATURE = 'def solve(...):' -IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' -TASK_DESCRIPTION = '("This optimization problem involves arranging a set of rectangular pieces within available stock "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("This optimization problem involves arranging a set of rectangular pieces within available stock "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(m: int, stocks: list, pieces: list) -> dict:\n """\n Solves the rectangular piece arrangement optimization problem to minimize the overall waste area percentage.\n Given:\n - m (int): Number of piece types.\n - stocks (list of dict): Each dict represents a stock type with keys:\n \'length\' (float), \'width\' (float), \'fixed_cost\' (float).\n - pieces (list of dict): Each dict represents a piece type with keys:\n \'length\' (float), \'width\' (float), \'min\' (int), \'max\' (int), \'value\' (float).\n Objective:\n Arrange rectangular pieces (which may be rotated by 90°) into stock rectangles such that the overall waste area percentage is minimized.\n The waste area percentage is computed as:\n Waste Percentage = (Total Stock Area - Total Used Area) / (Total Stock Area)\n Constraints:\n • Each piece must lie entirely within its assigned stock rectangle.\n • Pieces must not overlap within the same stock rectangle.\n • The number of pieces placed for each piece type must lie within its specified minimum and maximum bounds.\n • You may use unlimited many instances of each selected stock type, but the solution can include at most 2 distinct stock types.\n Output:\n Returns a dictionary with two keys (exactly follow this format):\n - "objective": The overall waste area percentage (float) as computed by the evaluation function.\n - "placements": A dictionary mapping stock instance ids (1-indexed) to their placement details.\n Each stock instance is represented by a dictionary with the following keys:\n \'stock_type\': (the 1-indexed id of the stock type used for this instance),\n \'placements\': a list of placements for pieces within that stock instance.\n Each placement is a dict with keys:\n \'piece\' (piece type, 1-indexed, 1 <= piece type <= m),\n \'x\' (x-coordinate of the bottom-left corner),\n \'y\' (y-coordinate of the bottom-left corner),\n \'orientation\' (0 for normal, 1 for rotated 90°).\n NOTE: The returned data should adhere to the output format required for evaluation.\n """\n # ----- INSERT YOUR SOLUTION ALGORITHM HERE -----\n # For demonstration purposes, we provide a dummy solution that does not place any pieces.\n # In a real solution, you would compute placements that respect all constraints.\n\n # Dummy solution: Create a single stock instance of the first stock type, with no pieces placed.\n solution = {\n "objective": 0.0, # With no placements, the evaluation function would compute a waste area percentage of 0.0.\n "placements": {\n 1: {\n "stock_type": 1,\n "placements": []\n }\n }\n }\n return solution' -EVAL_CLASS_NAME = 'AssortPEvaluationCB' -EVAL_KWARGS = {'timeout_seconds': 20} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_assortment_problem/paras.yaml b/examples/benchmark_tasks/optimization_assortment_problem/paras.yaml deleted file mode 100644 index 31aeb4c4..00000000 --- a/examples/benchmark_tasks/optimization_assortment_problem/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: AssortPEvaluationCB -timeout_seconds: 20 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_bp_1d/__init__.py b/examples/benchmark_tasks/optimization_bp_1d/__init__.py deleted file mode 100644 index 50b607b7..00000000 --- a/examples/benchmark_tasks/optimization_bp_1d/__init__.py +++ /dev/null @@ -1,298 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_bp_1d -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# References: -# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language -# model agents in algorithm search for combinatorial optimization. -# arXiv preprint arXiv:2504.04310 (2025). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -from typing import Any -import numpy as np -from llm4ad_loader import Evaluation -from llm4ad_loader import load_subdir_as_text -# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader -# from llm4ad.task.optimization.co_bench.bp_1d_co_bench.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\nfrom scipy.optimize import linear_sum_assignment\ndef solve(id: str, bin_capacity: int, num_items: int, items: list[int], **kwargs) -> dict:\n """\n Solve the one-dimensional bin packing problem for a single test case.\n Input kwargs (for a single test case):\n - id: The problem identifier (string)\n - bin_capacity: The capacity of each bin (int)\n - num_items: The number of items (int)\n - items: A list of item sizes (list of ints)\n - **kwargs: Other unused keyword arguments\n Evaluation metric:\n - The solution is scored by the total number of bins used.\n - If the solution is invalid (e.g., items are missing or duplicated, or bin capacity is exceeded),\n a penalty of 1,000,000 is added.\n Returns:\n A dictionary with:\n - \'num_bins\': An integer, the number of bins used.\n - \'bins\': A list of lists, where each inner list contains the 1-based indices of items assigned to that bin.\n Note: This is a placeholder implementation.\n """\n # Placeholder: Replace with your bin packing solution.\n return {\n \'num_bins\': 0,\n \'bins\': []\n }' -task_description = '("The **one-dimensional bin packing problem** seeks to minimize the number of bins required to "' - - -__all__ = ['BP1DEvaluationCB'] - - -class BP1DEvaluationCB(Evaluation): - - def __init__(self, - timeout_seconds=50, - **kwargs): - - """ - Args: - None - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - # Load datasets from Hugging Face - dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Bin packing - one-dimensional") - self._datasets = {} - for filename in dataset: - # Join all text rows into a single string - text_content = '\n'.join([row['text'] for row in dataset[filename]]) - self._datasets[filename] = text_content - - def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: - return self.evaluate(callable_func) - - def evaluate(self, eva: callable) -> float | None: - ins_cases = [] - for case_id, ins in enumerate(self._datasets.values()): - ins_cases.append(self.load_data(ins)) - - fitness_list = [] - try: - for i in ins_cases: - for j in i: - result = eva(j['id'], j['bin_capacity'], j['num_items'], j['items']) - fitness = self.eval_func(j['id'], j['bin_capacity'], j['num_items'], j['best_known'], j['items'], result['num_bins'], result['bins']) - fitness_list.append(fitness) - - return -np.mean(fitness_list) - - except ValueError as e: - print(e) - return None - - def load_data(self, input_string): - """ - Load test cases from string content for the bin packing problem. - The input format: - 1. The first nonempty line is an integer P, the number of test cases. - 2. For each test case: - a. A line with the problem identifier (e.g., "u120_00"). - b. A line with three space-separated numbers: bin_capacity, num_items, best_known. - (Note: bin_capacity and item sizes may be given as floats.) - c. Then num_items lines, each with a number representing an item size. - Returns: - A list of dictionaries. Each dictionary contains the input data for one test case with keys: - - 'id': Problem identifier (string) - - 'bin_capacity': Bin capacity (float) - - 'num_items': Number of items (int) - - 'best_known': Best known number of bins (int) - - 'items': List of item sizes (list of floats) - """ - cases = [] - try: - # Get all nonempty, stripped lines from string. - in_lines = [line.strip() for line in input_string.split('\n') if line.strip() != ''] - except Exception as e: - raise Exception("Error processing input string: " + str(e)) - - if not in_lines: - raise Exception("Input file is empty or improperly formatted.") - - try: - num_cases = int(in_lines[0]) - except Exception as e: - raise Exception("Error parsing the number of test cases: " + str(e)) - - pos = 1 - for _ in range(num_cases): - if pos >= len(in_lines): - raise Exception("Unexpected end of file while reading a test case header.") - # Read problem identifier. - prob_id = in_lines[pos] - pos += 1 - - if pos >= len(in_lines): - raise Exception(f"Missing header for problem {prob_id}.") - header_parts = in_lines[pos].split() - pos += 1 - if len(header_parts) < 3: - raise Exception( - f"Header for problem {prob_id} must contain bin capacity, number of items, and best known bins.") - try: - # Use float for bin_capacity since it might be provided as a float. - bin_capacity = float(header_parts[0]) - num_items = int(header_parts[1]) - best_known = int(header_parts[2]) - except Exception as e: - raise Exception(f"Error parsing header for problem {prob_id}: {e}") - - items = [] - for i in range(num_items): - if pos >= len(in_lines): - raise Exception(f"Unexpected end of file while reading items for problem {prob_id}.") - try: - # Parse item sizes as floats. - item_size = float(in_lines[pos]) - except Exception as e: - raise Exception(f"Error parsing item size for problem {prob_id} at line {pos + 1}: {e}") - items.append(item_size) - pos += 1 - - cases.append({ - 'id': prob_id, - 'bin_capacity': bin_capacity, - 'num_items': num_items, - 'best_known': best_known, - 'items': items - }) - - return cases - - def eval_func(self, id, bin_capacity, num_items, best_known, items, num_bins, bins): - """ - Evaluate the bin packing solution for a single test case. - Parameters (from the input case and the solution): - - id: Problem identifier (string) - - bin_capacity: Bin capacity (int) - - num_items: Number of items (int) - - best_known: Best known number of bins (int) - - items: List of item sizes (list of ints) - - num_bins: Number of bins used in the solution (int) - - bins: List of lists; each inner list contains 1-based item indices assigned to that bin. - Returns: - A scalar score (int). The score is the total number of bins used. - If the solution is invalid (e.g., item indices are wrong, items not used exactly once, or bin capacity exceeded), - a penalty of 1,000,000 is added to the score. - """ - penalty = 1_000_000 - score = num_bins # start with the number of bins used - valid = True - details = [] - - # Check that the number of bin assignments matches num_bins. - if len(bins) != num_bins: - valid = False - details.append("Declared number of bins does not match the number of bin assignments provided.") - - # Check each bin for capacity and valid item indices. - # Also count item appearances. - item_counts = [0] * (num_items + 1) # index 0 unused - for bin_index, bin_items in enumerate(bins, start=1): - bin_total = 0 - for item_idx in bin_items: - if item_idx < 1 or item_idx > num_items: - valid = False - details.append(f"Bin {bin_index} contains an invalid item index: {item_idx}.") - continue - bin_total += items[item_idx - 1] - item_counts[item_idx] += 1 - if bin_total > bin_capacity: - valid = False - details.append(f"Bin {bin_index} exceeds capacity: total size {bin_total} > capacity {bin_capacity}.") - - # Check that every item appears exactly once. - for i in range(1, num_items + 1): - if item_counts[i] != 1: - valid = False - details.append(f"Item {i} appears {item_counts[i]} times (expected exactly once).") - - if not valid: - score = None - else: - score = (score - best_known) / best_known - - # For debugging purposes, one might print or log details. - # For now, we simply return the computed score. - return score - - def get_dev(self): - dev = {'binpack1.txt': [7, 5, 16, 9, 13], 'binpack2.txt': [1, 15, 16, 4, 18], - 'binpack3.txt': [10, 18, 0, 19, 14], 'binpack4.txt': [11, 3, 16, 18, 17], - 'binpack5.txt': [10, 13, 0, 11, 17], 'binpack6.txt': [18, 11, 0, 6, 2], - 'binpack7.txt': [12, 17, 9, 15, 13], 'binpack8.txt': [4, 11, 19, 6, 17]} - - return dev - -# Task configuration for benchmark task -ENTRY_NAME = 'solve' -FUNCTION_SIGNATURE = 'def solve(...):' -IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\nfrom scipy.optimize import linear_sum_assignment' -TASK_DESCRIPTION = '("The **one-dimensional bin packing problem** seeks to minimize the number of bins required to "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The **one-dimensional bin packing problem** seeks to minimize the number of bins required to "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\nfrom scipy.optimize import linear_sum_assignment\ndef solve(id: str, bin_capacity: int, num_items: int, items: list[int], **kwargs) -> dict:\n """\n Solve the one-dimensional bin packing problem for a single test case.\n Input kwargs (for a single test case):\n - id: The problem identifier (string)\n - bin_capacity: The capacity of each bin (int)\n - num_items: The number of items (int)\n - items: A list of item sizes (list of ints)\n - **kwargs: Other unused keyword arguments\n Evaluation metric:\n - The solution is scored by the total number of bins used.\n - If the solution is invalid (e.g., items are missing or duplicated, or bin capacity is exceeded),\n a penalty of 1,000,000 is added.\n Returns:\n A dictionary with:\n - \'num_bins\': An integer, the number of bins used.\n - \'bins\': A list of lists, where each inner list contains the 1-based indices of items assigned to that bin.\n Note: This is a placeholder implementation.\n """\n # Placeholder: Replace with your bin packing solution.\n return {\n \'num_bins\': 0,\n \'bins\': []\n }' -EVAL_CLASS_NAME = 'BP1DEvaluationCB' -EVAL_KWARGS = {'timeout_seconds': 20} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_bp_1d/paras.yaml b/examples/benchmark_tasks/optimization_bp_1d/paras.yaml deleted file mode 100644 index 706ec53b..00000000 --- a/examples/benchmark_tasks/optimization_bp_1d/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: BP1DEvaluationCB -timeout_seconds: 20 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_bp_1d_construct/__init__.py b/examples/benchmark_tasks/optimization_bp_1d_construct/__init__.py deleted file mode 100644 index a9c8dbcb..00000000 --- a/examples/benchmark_tasks/optimization_bp_1d_construct/__init__.py +++ /dev/null @@ -1,289 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_bp_1d_construct -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# Module Name: BP1DEvaluation -# Last Revision: 2025/2/16 -# Description: Evaluates constructive heuristic for 1-dimensional bin packing problem. -# Given a set of bins and items, iteratively assign one item to feasible bins. -# Design the optimal heuristic in each iteration to minimize the used bins. -# This module is part of the LLM4AD project (https://github.com/Optima-CityU/llm4ad). -# -# Parameters: -# - n_bins: number of bins: int (default: 10). -# - n_instance: number of instances: int (default: 16). -# - n_items: number of items: int (default: 10). -# - bin_capacity: capacity of bins: int (default: 100). -# - timeout_seconds: int - Maximum allowed time (in seconds) for the evaluation process (default: 60). -# -# References: -# - Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - - -from __future__ import annotations -import matplotlib.pyplot as plt -from typing import Callable, Any, List, Tuple -import os, sys -sys.path.insert(0, os.path.dirname(__file__)) -import copy - -from llm4ad_loader import Evaluation -from get_instance import GetData -# from llm4ad.task.optimization.bp_1d_construct.get_instance import GetData # Converted from LLM4AD import -# from llm4ad.task.optimization.bp_1d_construct.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\n\ndef determine_next_assignment(remaining_items: List[int], remaining_capacities: List[int]) -> Tuple[int, Optional[int]]:\n """\n Determine the next item and bin to pack based on a greedy heuristic.\n\n Args:\n remaining_items: A list of remaining item weights.\n remaining_capacities: A list of remaining capacities of feasible bins.\n\n Returns:\n A tuple containing:\n - The selected item to pack.\n - The selected bin to pack the item into (or None if no feasible bin is found).\n """\n # Iterate through items in their original order\n for item in remaining_items:\n # Iterate through bins to find the first feasible one\n for bin_id, capacity in enumerate(remaining_capacities):\n if item <= capacity:\n return item, bin_id # Return the selected item and bin\n return remaining_items[0], None # If no feasible bin is found, return the first item and no bin' -task_description = "'" - - -__all__ = ['BP1DEvaluation'] - - -class BP1DEvaluation(Evaluation): - """Evaluator for the 1D Bin Packing Problem.""" - - def __init__(self, - timeout_seconds: int = 60, - n_bins: int = 500, - n_instance: int = 8, - n_items: int = 500, - bin_capacity: int = 100, - **kwargs): - """ - Args: - n_bins: The number of available bins at the beginning. - """ - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - self.n_instance = n_instance - self.n_items = n_items - self.bin_capacity = bin_capacity - self.n_bins = n_bins - getData = GetData(self.n_instance, self.n_items, self.bin_capacity) - self._datasets = getData.generate_instances() - - def plot_bins(self, bins: List[List[int]], bin_capacity: int): - """ - Plot the bins and their contents. - - Args: - bins: A list of bins, where each bin is a list of item weights. - bin_capacity: The capacity of each bin. - """ - fig, ax = plt.subplots() - - # Create a bar plot for each bin - for i, bin_content in enumerate(bins): - # Calculate the cumulative sum of item weights for stacking - cumulative_weights = [sum(bin_content[:j + 1]) for j in range(len(bin_content))] - # Plot the bin as a bar, with items stacked - ax.bar(i, cumulative_weights[-1] if cumulative_weights else 0, color='lightblue', edgecolor='black') - # Plot individual items as stacked segments - for j, weight in enumerate(bin_content): - ax.bar(i, weight, bottom=cumulative_weights[j] - weight, edgecolor='black') - - # Set plot labels and title - ax.set_xlabel('Bin Index') - ax.set_ylabel('Weight') - ax.set_title(f'1D Bin Packing Solution (Bin Capacity: {bin_capacity})') - ax.set_xticks(range(len(bins))) - ax.set_xticklabels([f'Bin {i + 1}' for i in range(len(bins))]) - ax.axhline(bin_capacity, color='red', linestyle='--', label='Bin Capacity') - - # Add a legend - ax.legend() - - # Show the plot - plt.show() - - def pack_items(self, item_weights: List[int], bin_capacity: int, eva: Callable, n_bins: int) -> Tuple[int, List[List[int]]]: - """ - Pack items into bins using a constructive heuristic. - - Args: - item_weights: A list of item weights. - bin_capacity: The capacity of each bin. - eva: The constructive heuristic function to select the next item and bin. - n_bins: The number of available bins at the beginning. - - Returns: - A tuple containing: - - The total number of bins used. - - A list of bins, where each bin is a list of item weights. - """ - bins = [[] for _ in range(n_bins)] # Initialize n_bins empty bins - remaining_items = item_weights.copy() # Copy of item weights to track remaining items - remaining_capacities = [bin_capacity] * n_bins # Initialize remaining capacities of all bins - - while remaining_items: - # Determine feasible bins for the next item - feasible_bins = [bin_id for bin_id, capacity in enumerate(remaining_capacities) if capacity >= min(remaining_items)] - - # Use the heuristic to select the next item and bin - remaining_items_copy = copy.deepcopy(remaining_items) - remaining_capacities_copy = copy.deepcopy(remaining_capacities) - selected_item, selected_bin = eva(remaining_items_copy, remaining_capacities_copy) - - if selected_bin is not None: - # Add the selected item to the selected bin - bins[selected_bin].append(selected_item) - # Update the remaining capacity of the selected bin - remaining_capacities[selected_bin] -= selected_item - else: - # If no feasible bin is found, stop packing (no more bins available) - break - - if remaining_capacities[selected_bin] < 0: - return None - - # Remove the selected item from the remaining items - remaining_items.remove(selected_item) - - if len(remaining_items) > 0: - return None - - # Calculate the number of bins used (bins that contain at least one item) - used_bins = sum(1 for bin_content in bins if bin_content) - - return used_bins, bins - - def evaluate(self, eva: Callable) -> float: - """ - Evaluate the constructive heuristic for the 1D Bin Packing Problem. - - Args: - instance_data: List of tuples containing the item weights and bin capacity. - n_ins: Number of instances to evaluate. - eva: The constructive heuristic function to evaluate. - n_bins: The number of available bins at the beginning. - - Returns: - The average number of bins used across all instances. - """ - total_bins = 0 - - for instance in self._datasets: - item_weights, bin_capacity = instance - num_bins, _ = self.pack_items(item_weights, bin_capacity, eva, self.n_bins) - total_bins += num_bins - - average_bins = total_bins / self.n_instance - return -average_bins # Negative because we want to minimize the number of bins - - def evaluate_program(self, program_str: str, callable_func: Callable) -> Any | None: - return self.evaluate(callable_func) - - -if __name__ == '__main__': - - def determine_next_assignment(remaining_items: List[int], remaining_capacities: List[int]) -> Tuple[int, int | None]: - """ - Determine the next item and bin to pack based on a greedy heuristic. - - Args: - remaining_items: A list of remaining item weights. - remaining_capacities: A list of remaining capacities of feasible bins. - - Returns: - A tuple containing: - - The selected item to pack. - - The selected bin to pack the item into (or None if no feasible bin is found). - """ - # Simple greedy heuristic: choose the largest item that fits into the bin with the smallest remaining capacity - for item in sorted(remaining_items, reverse=True): # Try largest items first - for bin_id, capacity in enumerate(remaining_capacities): - if item <= capacity: - return item, bin_id # Return the selected item and bin - return remaining_items[0], None # If no feasible bin is found, return the first item and no bin - - - bp1d = BP1DEvaluation() - ave_bins = bp1d.evaluate_program('_', determine_next_assignment) - print(ave_bins) - -# Task configuration for benchmark task -ENTRY_NAME = 'determine_next_assignment' -FUNCTION_SIGNATURE = 'def determine_next_assignment(...):' -IMPORT_HEADER = 'import numpy as np\nimport math' -TASK_DESCRIPTION = "'" -OBJECTIVE_TEXT = "You are optimizing the implementation of `determine_next_assignment` for the LLM4AD task.\\n\\nTask description:\\n'\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible." -TEMPLATE_FUNCTION = 'import numpy as np\n\ndef determine_next_assignment(remaining_items: List[int], remaining_capacities: List[int]) -> Tuple[int, Optional[int]]:\n """\n Determine the next item and bin to pack based on a greedy heuristic.\n\n Args:\n remaining_items: A list of remaining item weights.\n remaining_capacities: A list of remaining capacities of feasible bins.\n\n Returns:\n A tuple containing:\n - The selected item to pack.\n - The selected bin to pack the item into (or None if no feasible bin is found).\n """\n # Iterate through items in their original order\n for item in remaining_items:\n # Iterate through bins to find the first feasible one\n for bin_id, capacity in enumerate(remaining_capacities):\n if item <= capacity:\n return item, bin_id # Return the selected item and bin\n return remaining_items[0], None # If no feasible bin is found, return the first item and no bin' -EVAL_CLASS_NAME = 'BP1DEvaluation' -EVAL_KWARGS = {'timeout_seconds': 60} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_bp_1d_construct/get_instance.py b/examples/benchmark_tasks/optimization_bp_1d_construct/get_instance.py deleted file mode 100644 index 7840a5d8..00000000 --- a/examples/benchmark_tasks/optimization_bp_1d_construct/get_instance.py +++ /dev/null @@ -1,55 +0,0 @@ -import numpy as np - - -class GetData: - def __init__(self, n_instance: int, n_items: int, bin_capacity: int): - """ - Initialize the GetData class for the 1D Bin Packing Problem. - - Args: - n_instance: Number of instances to generate. - n_items: Number of items. - bin_capacity: Capacity of each bin. - """ - self.n_instance = n_instance - self.n_items = n_items - self.bin_capacity = bin_capacity - - def generate_instances(self): - """ - Generate instances for the 1D Bin Packing Problem. - - Returns: - A list of tuples, where each tuple contains: - - item_weights: A list of item weights. - - bin_capacity: The capacity of each bin. - """ - np.random.seed(2024) # Set seed for reproducibility - instance_data = [] - - for _ in range(self.n_instance): - # Parameters for the beta distribution - alpha = 2 # Shape parameter (adjust as needed) - beta = 5 # Shape parameter (adjust as needed) - - # Generate random item weights using a beta distribution - # Scale and shift the values to the range [5, 50] - item_weights = (50 - np.random.beta(alpha, beta, size=self.n_items) * 40).astype(int).tolist() - # # Generate random item weights, ensuring no item exceeds the bin capacity - # item_weights = np.random.randint(2, 9, size=self.n_items).tolist() - - # # Randomly decide for each item whether to multiply by 5 or 8 - # multipliers = np.random.choice([5, 11], size=self.n_items) - - # # Apply the multipliers to the item weights - # modified_weights = [weight * multiplier for weight, multiplier in zip(item_weights, multipliers)] - - instance_data.append((item_weights, self.bin_capacity)) - - return instance_data - -# # Example usage: -# data_generator = GetData(n_instance=5, n_items=10, bin_capacity=100) -# instances = data_generator.generate_instances() -# for instance in instances: -# print(instance) diff --git a/examples/benchmark_tasks/optimization_bp_1d_construct/paras.yaml b/examples/benchmark_tasks/optimization_bp_1d_construct/paras.yaml deleted file mode 100644 index 310280ad..00000000 --- a/examples/benchmark_tasks/optimization_bp_1d_construct/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: BP1DEvaluation -timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_bp_2d_construct/__init__.py b/examples/benchmark_tasks/optimization_bp_2d_construct/__init__.py deleted file mode 100644 index a7acee17..00000000 --- a/examples/benchmark_tasks/optimization_bp_2d_construct/__init__.py +++ /dev/null @@ -1,344 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_bp_2d_construct -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# Module Name: BP2DEvaluation -# Last Revision: 2025/2/16 -# Description: Evaluates constructive heuristic for 2-dimensional bin packing problem. -# Given a set of bins and items, iteratively assign one item to feasible bins. -# Design the optimal heuristic in each iteration to minimize the used bins. -# This module is part of the LLM4AD project (https://github.com/Optima-CityU/llm4ad). -# -# Parameters: -# - n_bins: number of bins: int (default: 10). -# - n_instance: number of instances: int (default: 16). -# - n_items: number of items: int (default: 10). -# - bin_width: width of bins: int (default: 100). -# - bin_height: height of bins: int (default: 100). -# - timeout_seconds: int - Maximum allowed time (in seconds) for the evaluation process (default: 60). -# -# References: -# - Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - - -from __future__ import annotations -from typing import List, Tuple, Callable, Any -import numpy as np -import matplotlib.pyplot as plt -import matplotlib.patches as patches - -from llm4ad_loader import Evaluation -import os, sys -sys.path.insert(0, os.path.dirname(__file__)) -from get_instance import GetData -# from llm4ad.task.optimization.bp_2d_construct.get_instance import GetData # Converted from LLM4AD import -# from llm4ad.task.optimization.bp_2d_construct.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\ndef determine_next_assignment(remaining_items: List[Tuple[int, int]], point_matrices: List[List[List[int]]]) -> Tuple[Tuple[int, int], int]:\n """\n A simple heuristic function to select the next item and bin for packing.\n\n Args:\n remaining_items: A list of tuples, where each tuple represents the (width, height) of an item.\n point_matrices: A list of 2D matrices representing the occupied (1) and unoccupied (0) points in each bin.\n\n Returns:\n A tuple containing:\n - The selected item (width, height).\n - The selected bin index (or None if no bin is feasible).\n """\n # Select the largest item (based on area) from the remaining items\n selected_item = max(remaining_items, key=lambda item: item[0] * item[1])\n\n # Try to find a feasible bin for the selected item\n for bin_idx, point_matrix in enumerate(point_matrices):\n bin_width = len(point_matrix)\n bin_height = len(point_matrix[0]) if bin_width > 0 else 0\n # Check if the item fits in the bin\n if bin_width >= selected_item[0] and bin_height >= selected_item[1]:\n # Check for a feasible position in the bin\n for x in range(bin_width - selected_item[0] + 1):\n for y in range(bin_height - selected_item[1] + 1):\n # Check if the area is unoccupied\n if all(point_matrix[x + dx][y + dy] == 0 for dx in range(selected_item[0]) for dy in range(selected_item[1])):\n return selected_item, bin_idx\n # If no feasible bin is found, return None for the bin\n return selected_item, None' -task_description = "'" - - -__all__ = ['BP2DEvaluation'] - - -class BP2DEvaluation(Evaluation): - """Evaluator for the 2D Bin Packing Problem.""" - - def __init__(self, - timeout_seconds: int = 120, - n_bins: int = 100, - n_instance: int = 8, - n_items: int = 100, - bin_width: int = 100, - bin_height: int = 100, - **kwargs): - """ - Args: - n_bins: The number of available bins at the beginning. - """ - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - self.n_instance = n_instance - self.n_items = n_items - self.n_bins = n_bins - self.bin_width = bin_width - self.bin_height = bin_height - getData = GetData(self.n_instance, self.n_items, self.bin_width, self.bin_height) - self._datasets = getData.generate_instances() - - def plot_solution(self, bins: List[List[Tuple[Tuple[int, int], Tuple[int, int]]]], bin_dimensions: Tuple[int, int]): - """ - Plot the final packing solution for 2D bin packing. - - Args: - bins: A list of bins, where each bin is a list of tuples containing the corner and dimensions of packed items. - bin_dimensions: A tuple representing the (width, height) of the bin. - """ - # Only plot the used bins - num_bins = sum(1 for bin_content in bins if bin_content) + 5 - bins = bins[:num_bins] - max_bins_per_row = 5 - num_rows = (num_bins + max_bins_per_row - 1) // max_bins_per_row # Calculate the number of rows needed - - fig, axes = plt.subplots(num_rows, max_bins_per_row, figsize=(5 * max_bins_per_row, 5 * num_rows)) - - # Flatten the axes array if there are multiple rows - if num_rows > 1: - axes = axes.flatten() - else: - axes = [axes] # Ensure axes is a list for consistency - - for i, bin_content in enumerate(bins): - ax = axes[i] - ax.set_xlim(0, bin_dimensions[0]) - ax.set_ylim(0, bin_dimensions[1]) - ax.set_title(f"Bin {i + 1}") - ax.set_aspect('equal') - - # Draw the bin boundary - bin_rect = patches.Rectangle((0, 0), bin_dimensions[0], bin_dimensions[1], linewidth=2, edgecolor='black', facecolor='none') - ax.add_patch(bin_rect) - - # Draw each item in the bin - for corner, (width, height) in bin_content: - item_rect = patches.Rectangle(corner, width, height, linewidth=1, edgecolor='blue', facecolor='lightblue', alpha=0.6) - ax.add_patch(item_rect) - # Add text to label the item - ax.text(corner[0] + width / 2, corner[1] + height / 2, f"{width}x{height}", ha='center', va='center', fontsize=8) - - # Hide unused axes if the number of bins is not a multiple of max_bins_per_row - for j in range(num_bins, num_rows * max_bins_per_row): - axes[j].axis('off') - - plt.tight_layout() - plt.show() - - def pack_items_2d(self, item_dimensions: List[Tuple[int, int]], bin_dimensions: Tuple[int, int], eva: Callable, n_bins: int) -> Tuple[int, List[List[Tuple[int, int]]]]: - """ - Pack items into bins using a constructive heuristic for the 2D Bin Packing Problem. - The bins are represented as a discrete point matrix to track feasible areas. - - Args: - item_dimensions: A list of tuples, where each tuple represents the (width, height) of an item. - bin_dimensions: A tuple representing the (width, height) of the bin. - eva: The constructive heuristic function to select the next item and bin. - n_bins: The number of available bins at the beginning. - - Returns: - A tuple containing: - - The total number of bins used. - - A list of bins, where each bin is a list of item dimensions. - """ - bins = [[] for _ in range(n_bins)] # Initialize n_bins empty bins - remaining_items = item_dimensions.copy() # Copy of item dimensions to track remaining items - # Initialize the point matrix for each bin (0: unoccupied, 1: occupied) - point_matrices = [[[0 for _ in range(bin_dimensions[1])] for _ in range(bin_dimensions[0])] for _ in range(n_bins)] - - while remaining_items: - # Use the heuristic to select the next item and bin - selected_item, selected_bin = eva(remaining_items, point_matrices) - - if selected_bin is not None: - # Find a feasible position for the selected item in the selected bin - placed = False - for x in range(bin_dimensions[0] - selected_item[0] + 1): - for y in range(bin_dimensions[1] - selected_item[1] + 1): - # Check the four edges of the item - top_edge = all(point_matrices[selected_bin][x + dx][y] == 0 for dx in range(selected_item[0])) - bottom_edge = all(point_matrices[selected_bin][x + dx][y + selected_item[1] - 1] == 0 for dx in range(selected_item[0])) - left_edge = all(point_matrices[selected_bin][x][y + dy] == 0 for dy in range(selected_item[1])) - right_edge = all(point_matrices[selected_bin][x + selected_item[0] - 1][y + dy] == 0 for dy in range(selected_item[1])) - - if top_edge and bottom_edge and left_edge and right_edge: - # Place the item at this position - for dx in range(selected_item[0]): - for dy in range(selected_item[1]): - point_matrices[selected_bin][x + dx][y + dy] = 1 - bins[selected_bin].append(((x, y), selected_item)) - placed = True - break - if placed: - break - if not placed: - # If the item cannot be placed in the selected bin, try other bins - for i in range(len(bins)): - if placed: - break - selected_bin = i - for x in range(bin_dimensions[0] - selected_item[0] + 1): - for y in range(bin_dimensions[1] - selected_item[1] + 1): - # Check only the four corners of the item - corners = [ - (x, y), - (x + selected_item[0] - 1, y), - (x, y + selected_item[1] - 1), - (x + selected_item[0] - 1, y + selected_item[1] - 1) - ] - if all(point_matrices[selected_bin][cx][cy] == 0 for cx, cy in corners): - # Place the item at this position - for dx in range(selected_item[0]): - for dy in range(selected_item[1]): - point_matrices[selected_bin][x + dx][y + dy] = 1 - bins[selected_bin].append(((x, y), selected_item)) - placed = True - break - if placed: - break - else: - # If no feasible bin is found, stop packing (no more bins available) - break - - # Remove the selected item from the remaining items - remaining_items.remove(selected_item) - - # Calculate the number of bins used (bins that contain at least one item) - used_bins = sum(1 for bin_content in bins if bin_content) - return used_bins, bins - - def evaluate_2d(self, eva: Callable) -> float: - """ - Evaluate the constructive heuristic for the 2D Bin Packing Problem. - - Args: - eva: callable function of constructive heuristic. - - Returns: - The average number of bins used across all instances. - """ - total_bins = 0 - - for instance in self._datasets[:self.n_instance]: - item_dimensions, bin_dimensions = instance - num_bins, _ = self.pack_items_2d(item_dimensions, bin_dimensions, eva, self.n_bins) - total_bins += num_bins - - average_bins = total_bins / self.n_instance - return -average_bins # Negative because we want to minimize the number of bins - - def evaluate_program(self, program_str: str, callable_func: Callable) -> Any | None: - return self.evaluate_2d(callable_func) - - -if __name__ == '__main__': - - def determine_next_assignment(remaining_items: List[Tuple[int, int]], feasible_corners: List[List[Tuple[int, int]]]) -> Tuple[Tuple[int, int], int]: - """ - A simple heuristic function to select the next item and bin for 2D bin packing. - - Args: - remaining_items: A list of tuples representing the (width, height) of remaining items. - feasible_corners: A list of lists, where each inner list contains the feasible corners for a bin. - - Returns: - A tuple containing: - - The selected item (width, height). - - The index of the selected bin (or None if no bin is feasible). - """ - # Step 1: Select the largest item by area - selected_item = max(remaining_items, key=lambda x: x[0] * x[1]) - - # Step 2: Select the bin with the most feasible corners - max_corners = -1 - selected_bin = None - for i, corners in enumerate(feasible_corners): - if len(corners) > max_corners: - max_corners = len(corners) - selected_bin = i - - # If no bin has feasible corners, return None for the bin - if max_corners == 0: - selected_bin = None - - return selected_item, selected_bin - - - bp2d = BP2DEvaluation() - ave_bins = bp2d.evaluate_program('_', determine_next_assignment) - print(ave_bins) - -# Task configuration for benchmark task -ENTRY_NAME = 'determine_next_assignment' -FUNCTION_SIGNATURE = 'def determine_next_assignment(...):' -IMPORT_HEADER = 'import numpy as np\nimport math' -TASK_DESCRIPTION = "'" -OBJECTIVE_TEXT = "You are optimizing the implementation of `determine_next_assignment` for the LLM4AD task.\\n\\nTask description:\\n'\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible." -TEMPLATE_FUNCTION = 'import numpy as np\ndef determine_next_assignment(remaining_items: List[Tuple[int, int]], point_matrices: List[List[List[int]]]) -> Tuple[Tuple[int, int], int]:\n """\n A simple heuristic function to select the next item and bin for packing.\n\n Args:\n remaining_items: A list of tuples, where each tuple represents the (width, height) of an item.\n point_matrices: A list of 2D matrices representing the occupied (1) and unoccupied (0) points in each bin.\n\n Returns:\n A tuple containing:\n - The selected item (width, height).\n - The selected bin index (or None if no bin is feasible).\n """\n # Select the largest item (based on area) from the remaining items\n selected_item = max(remaining_items, key=lambda item: item[0] * item[1])\n\n # Try to find a feasible bin for the selected item\n for bin_idx, point_matrix in enumerate(point_matrices):\n bin_width = len(point_matrix)\n bin_height = len(point_matrix[0]) if bin_width > 0 else 0\n # Check if the item fits in the bin\n if bin_width >= selected_item[0] and bin_height >= selected_item[1]:\n # Check for a feasible position in the bin\n for x in range(bin_width - selected_item[0] + 1):\n for y in range(bin_height - selected_item[1] + 1):\n # Check if the area is unoccupied\n if all(point_matrix[x + dx][y + dy] == 0 for dx in range(selected_item[0]) for dy in range(selected_item[1])):\n return selected_item, bin_idx\n # If no feasible bin is found, return None for the bin\n return selected_item, None' -EVAL_CLASS_NAME = 'BP2DEvaluation' -EVAL_KWARGS = {'timeout_seconds': 120} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_bp_2d_construct/get_instance.py b/examples/benchmark_tasks/optimization_bp_2d_construct/get_instance.py deleted file mode 100644 index a9cb77d5..00000000 --- a/examples/benchmark_tasks/optimization_bp_2d_construct/get_instance.py +++ /dev/null @@ -1,40 +0,0 @@ -import numpy as np - - -class GetData: - def __init__(self, n_instance: int, n_items: int, bin_width: int, bin_height: int): - """ - Initialize the GetData class for the 2D Bin Packing Problem. - - Args: - n_instance: Number of instances to generate. - n_items: Number of items. - bin_width: Width of each bin. - bin_height: Height of each bin. - """ - self.n_instance = n_instance - self.n_items = n_items - self.bin_width = bin_width - self.bin_height = bin_height - - def generate_instances(self): - """ - Generate instances for the 2D Bin Packing Problem. - - Returns: - A list of tuples, where each tuple contains: - - item_dimensions: A list of tuples, where each tuple represents the (width, height) of an item. - - bin_dimensions: A tuple representing the (width, height) of the bin. - """ - np.random.seed(2024) # Set seed for reproducibility - instance_data = [] - - for _ in range(self.n_instance): - # Generate random item dimensions, ensuring no item exceeds the bin dimensions - item_widths = np.random.randint(10, self.bin_width - 10, size=self.n_items) - item_heights = np.random.randint(10, self.bin_height - 10, size=self.n_items) - item_dimensions = list(zip(item_widths, item_heights)) - bin_dimensions = (self.bin_width, self.bin_height) - instance_data.append((item_dimensions, bin_dimensions)) - - return instance_data diff --git a/examples/benchmark_tasks/optimization_bp_2d_construct/paras.yaml b/examples/benchmark_tasks/optimization_bp_2d_construct/paras.yaml deleted file mode 100644 index 2d30a9f6..00000000 --- a/examples/benchmark_tasks/optimization_bp_2d_construct/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: BP2DEvaluation -timeout_seconds: 120 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_capacitated_warehouse_location/__init__.py b/examples/benchmark_tasks/optimization_capacitated_warehouse_location/__init__.py deleted file mode 100644 index 6c408169..00000000 --- a/examples/benchmark_tasks/optimization_capacitated_warehouse_location/__init__.py +++ /dev/null @@ -1,398 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_capacitated_warehouse_location -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# References: -# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language -# model agents in algorithm search for combinatorial optimization. -# arXiv preprint arXiv:2504.04310 (2025). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -from typing import Any -import numpy as np -from llm4ad_loader import Evaluation -from llm4ad_loader import load_subdir_as_text -# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader -# from llm4ad.task.optimization.co_bench.capacitated_warehouse_location_co_bench.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(m: int, n: int, warehouses: list, customers: list) -> dict:\n """\n Solves the Capacitated Warehouse Location Problem with Splittable Customer Demand.\n Input kwargs:\n - m (int): Number of potential warehouses\n - n (int): Number of customers\n - warehouses (list of dict): A list of dictionaries, each with keys \'capacity\' (float) and \'fixed_cost\' (float)\n - customers (list of dict): A list of dictionaries, each with keys \'demand\' (float) and \'costs\' (list of float) representing the per-unit assignment cost from each warehouse\n Evaluation Metric:\n The objective is to minimize the total cost, computed as:\n (Sum of fixed costs for all open warehouses)\n + (Sum of per-unit assignment costs for each unit of demand allocated from warehouses to customers)\n For each customer, the sum of allocations from all warehouses must equal the customer\'s demand.\n For each warehouse, the total allocated demand across all customers must not exceed its capacity.\n If a solution violates any of these constraints, the solution is considered infeasible and no score is provided.\n Returns:\n A dictionary with the following keys:\n \'total_cost\': (float) The computed objective value (cost) if the solution is feasible;\n otherwise, no score is provided.\n \'warehouse_open\': (list of int) A list of m integers (0 or 1) indicating whether each warehouse is closed or open.\n \'assignments\': (list of list of float) A 2D list (n x m) where each entry represents the amount of customer i\'s demand supplied by warehouse j.\n """\n ## placeholder. You do not need to write anything here.\n return {\n "total_cost": 0.0,\n "warehouse_open": [0] * kwargs["m"],\n "assignments": [[0.0] * kwargs["m"] for _ in range(kwargs["n"])]\n }' -task_description = '("The Capacitated Warehouse Location Problem with Splittable Demand aims to determine which "' - - -__all__ = ['CWLEvaluationCB'] - - -class CWLEvaluationCB(Evaluation): - - def __init__(self, - timeout_seconds=50, - **kwargs): - - """ - Args: - None - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - # Load datasets from Hugging Face with fallback - dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Capacitated warehouse location") - self._datasets = {} - for filename in dataset: - # Join all text rows into a single string - text_content = '\n'.join([row['text'] for row in dataset[filename]]) - self._datasets[filename] = text_content - - def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: - return self.evaluate(callable_func) - - def evaluate(self, eva: callable) -> float | None: - ins_cases = [] - for case_id, ins in enumerate(self._datasets.values()): - ins_cases.append(self.load_data(ins)) - - fitness_list = [] - try: - for i in ins_cases: - for j in i: - result = eva(j['m'], j['n'], j['warehouses'], j['customers']) - fitness = self.eval_func(j['m'], j['n'], j['warehouses'], j['customers'], result['warehouse_open'], result['assignments']) - fitness_list.append(fitness) - - return -np.mean(fitness_list) - - except ValueError as e: - print(e) - return None - - def load_data(self, input_string): - """ - Reads one or more problem cases from the input string. - Expected Input String Format for each case: - Line 1: Two integers: m n - Next m lines: Each line contains two numbers: capacity fixed_cost for a warehouse. - Next n lines: Each line contains: demand (a number) followed by m numbers representing the cost of - allocating the customer's demand to each warehouse. - If the input string contains multiple cases, the cases appear sequentially. - Returns: - A list of dictionaries, each corresponding to one case. Each dictionary has the keys: - - 'm': Number of potential warehouses (int) - - 'n': Number of customers (int) - - 'warehouses': List of dictionaries; each with keys 'capacity' and 'fixed_cost' - - 'customers': List of dictionaries; each with keys 'demand' and 'costs' (list of floats) - """ - try: - all_lines = [line.strip() for line in input_string.split('\n')] - except Exception as e: - raise ValueError("Error reading input string: " + str(e)) - - # Tokenize all non-empty lines. - tokens = [] - for line in all_lines: - line = line.strip() - if line: - tokens.extend(line.split()) - - cases = [] - index = 0 - total_tokens = len(tokens) - - # Process tokens until we have exhausted them. - while index < total_tokens: - if index + 1 >= total_tokens: - raise ValueError("Insufficient tokens to read m and n for a case.") - try: - m = int(tokens[index]) - n = int(tokens[index + 1]) - except Exception as e: - raise ValueError("Error parsing m or n: " + str(e)) - index += 2 - - # Parse warehouse data (m warehouses, each with 2 tokens). - expected_warehouse_tokens = m * 2 - if index + expected_warehouse_tokens - 1 >= total_tokens: - raise ValueError("Not enough tokens for warehouse data in a case.") - warehouses = [] - for i in range(m): - try: - capacity = float(tokens[index]) - fixed_cost = float(tokens[index + 1]) - except Exception as e: - raise ValueError("Error parsing warehouse data: " + str(e)) - warehouses.append({'capacity': capacity, 'fixed_cost': fixed_cost}) - index += 2 - - # Parse customer data (n customers, each with 1 demand and m cost values). - customers = [] - for j in range(n): - if index >= total_tokens: - raise ValueError(f"Not enough tokens for customer {j + 1} demand.") - try: - demand = float(tokens[index]) - except Exception as e: - raise ValueError(f"Error parsing demand for customer {j + 1}: " + str(e)) - index += 1 - if index + m - 1 >= total_tokens: - raise ValueError(f"Not enough tokens for cost data for customer {j + 1}.") - costs = [] - for i in range(m): - try: - cost = float(tokens[index]) - except Exception as e: - raise ValueError(f"Error parsing cost for customer {j + 1}, warehouse {i + 1}: " + str(e)) - costs.append(cost) - index += 1 - customers.append({'demand': demand, 'costs': costs}) - - case_data = {"m": m, "n": n, "warehouses": warehouses, "customers": customers} - cases.append(case_data) - - return cases - - def eval_func(self, m, n, warehouses, customers, warehouse_open, assignments, **kwargs): - """ - Evaluates the solution for the Capacitated Warehouse Location Problem with Splittable Customer Demand, - using a weighted average cost for each customer. - For each customer: - - The sum of allocations across warehouses must equal the customer's demand. - - The assignment cost is computed as the weighted average of the per-unit costs, - i.e., for each warehouse i, the fraction of demand allocated from i multiplied by its cost. - - No positive allocation is allowed for a warehouse that is closed. - Additionally, for each warehouse: - - The total allocated demand must not exceed its capacity. - The total cost is computed as: - (Sum of fixed costs for all open warehouses) - + (Sum over customers of the weighted average assignment cost) - Input Parameters: - - m: Number of potential warehouses (int) - - n: Number of customers (int) - - warehouses: List of dictionaries (each with 'capacity' and 'fixed_cost') - - customers: List of dictionaries (each with 'demand' and 'costs' (list of floats representing per-unit cost)) - - warehouse_open: List of m integers (0 or 1) indicating whether each warehouse is closed or open. - - assignments: List of n lists (each of length m) where assignments[j][i] represents the amount of - customer j's demand allocated to warehouse i. - - kwargs: Other parameters (not used here). - Returns: - A floating-point number representing the total cost if the solution is feasible. - Raises: - Exception: If any of the following conditions are violated: - - The sum of allocations for any customer does not equal its demand. - - Any positive allocation is made to a closed warehouse. - - Any warehouse's total allocated demand exceeds its capacity. - """ - computed_total_cost = 0.0 - - # Add fixed costs for open warehouses. - for i in range(m): - if warehouse_open[i] == 1: - computed_total_cost += warehouses[i]['fixed_cost'] - - # Evaluate assignment cost for each customer as a weighted average. - for j in range(n): - customer_demand = customers[j]['demand'] - allocated_amount = sum(assignments[j]) - if abs(allocated_amount - customer_demand) > 1e-6: - raise Exception( - f"Customer {j} demand violation: total assigned amount {allocated_amount} does not equal demand {customer_demand}." - ) - weighted_cost = 0.0 - for i in range(m): - allocation = assignments[j][i] - if allocation < 0: - raise Exception( - f"Customer {j} has a negative allocation {allocation} for warehouse {i + 1}." - ) - if allocation > 0 and warehouse_open[i] != 1: - raise Exception( - f"Customer {j} has allocation {allocation} for warehouse {i + 1}, which is closed." - ) - # Compute fraction of the customer's demand supplied from warehouse i. - fraction = allocation / customer_demand if customer_demand > 0 else 0.0 - weighted_cost += fraction * customers[j]['costs'][i] - # Add the weighted cost (applied once per customer). - computed_total_cost += weighted_cost - - # Compute total demand allocated to each warehouse and check capacity constraints. - assigned_demand = [0.0] * m - for i in range(m): - for j in range(n): - assigned_demand[i] += assignments[j][i] - for i in range(m): - if assigned_demand[i] > warehouses[i]['capacity'] + 1e-6: - excess = assigned_demand[i] - warehouses[i]['capacity'] - raise Exception( - f"Warehouse {i + 1} exceeds its capacity by {excess} units." - ) - - return computed_total_cost - - def norm_score(self, results): - optimal_scores = { - "cap41.txt": [1040444.375], - "cap42.txt": [1098000.450], - "cap43.txt": [1153000.450], - "cap44.txt": [1235500.450], - "cap51.txt": [1025208.225], - "cap61.txt": [932615.750], - "cap62.txt": [977799.400], - "cap63.txt": [1014062.050], - "cap64.txt": [1045650.250], - "cap71.txt": [932615.750], - "cap72.txt": [977799.400], - "cap73.txt": [1010641.450], - "cap74.txt": [1034976.975], - "cap81.txt": [838499.288], - "cap82.txt": [910889.563], - "cap83.txt": [975889.563], - "cap84.txt": [1069369.525], - "cap91.txt": [796648.438], - "cap92.txt": [855733.500], - "cap93.txt": [896617.538], - "cap94.txt": [946051.325], - "cap101.txt": [796648.437], - "cap102.txt": [854704.200], - "cap103.txt": [893782.112], - "cap104.txt": [928941.750], - "cap111.txt": [826124.713], - "cap112.txt": [901377.213], - "cap113.txt": [970567.750], - "cap114.txt": [1063356.488], - "cap121.txt": [793439.563], - "cap122.txt": [852524.625], - "cap123.txt": [895302.325], - "cap124.txt": [946051.325], - "cap131.txt": [793439.562], - "cap132.txt": [851495.325], - "cap133.txt": [893076.712], - "cap134.txt": [928941.750], - "capa-8000.txt": [19240822.449], - "capa-10000.txt": [18438046.543], - "capa-12000.txt": [17765201.949], - "capa-14000.txt": [17160439.012], - "capb-5000.txt": [13656379.578], - "capb-6000.txt": [13361927.449], - "capb-7000.txt": [13198556.434], - "capb-8000.txt": [13082516.496], - "capc-5000.txt": [11646596.974], - "capc-5750.txt": [11570340.289], - "capc-6500.txt": [11518743.744], - "capc-7250.txt": [11505767.394] - } - - normed = {} - for case, (scores, error_message) in results.items(): - if case not in optimal_scores: - continue # Skip if there's no optimal score defined. - optimal_list = optimal_scores[case] - normed_scores = [] - # Compute normalized score for each index. - for idx, score in enumerate(scores): - if isinstance(score, (int, float)): - normed_scores.append(optimal_list[idx] / score) - else: - normed_scores.append(score) - normed[case] = (normed_scores, error_message) - - return normed - - def get_dev(self): - dev = {'cap101.txt': [], 'cap112.txt': [], - 'cap123.txt': [], - 'cap134.txt': [], - 'cap41.txt': [], 'cap62.txt': [], 'cap73.txt': [], 'cap84.txt': [], - 'cap91.txt': [], - 'capa-12000.txt': [], - 'capb-5000.txt': [], - 'capc-7250.txt': []} - - return dev - - -# Task configuration for benchmark task -ENTRY_NAME = 'solve' -FUNCTION_SIGNATURE = 'def solve(...):' -IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' -TASK_DESCRIPTION = '("The Capacitated Warehouse Location Problem with Splittable Demand aims to determine which "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The Capacitated Warehouse Location Problem with Splittable Demand aims to determine which "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(m: int, n: int, warehouses: list, customers: list) -> dict:\n """\n Solves the Capacitated Warehouse Location Problem with Splittable Customer Demand.\n Input kwargs:\n - m (int): Number of potential warehouses\n - n (int): Number of customers\n - warehouses (list of dict): A list of dictionaries, each with keys \'capacity\' (float) and \'fixed_cost\' (float)\n - customers (list of dict): A list of dictionaries, each with keys \'demand\' (float) and \'costs\' (list of float) representing the per-unit assignment cost from each warehouse\n Evaluation Metric:\n The objective is to minimize the total cost, computed as:\n (Sum of fixed costs for all open warehouses)\n + (Sum of per-unit assignment costs for each unit of demand allocated from warehouses to customers)\n For each customer, the sum of allocations from all warehouses must equal the customer\'s demand.\n For each warehouse, the total allocated demand across all customers must not exceed its capacity.\n If a solution violates any of these constraints, the solution is considered infeasible and no score is provided.\n Returns:\n A dictionary with the following keys:\n \'total_cost\': (float) The computed objective value (cost) if the solution is feasible;\n otherwise, no score is provided.\n \'warehouse_open\': (list of int) A list of m integers (0 or 1) indicating whether each warehouse is closed or open.\n \'assignments\': (list of list of float) A 2D list (n x m) where each entry represents the amount of customer i\'s demand supplied by warehouse j.\n """\n ## placeholder. You do not need to write anything here.\n return {\n "total_cost": 0.0,\n "warehouse_open": [0] * kwargs["m"],\n "assignments": [[0.0] * kwargs["m"] for _ in range(kwargs["n"])]\n }' -EVAL_CLASS_NAME = 'CWLEvaluationCB' -EVAL_KWARGS = {'timeout_seconds': 20} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_capacitated_warehouse_location/paras.yaml b/examples/benchmark_tasks/optimization_capacitated_warehouse_location/paras.yaml deleted file mode 100644 index 8d3ff068..00000000 --- a/examples/benchmark_tasks/optimization_capacitated_warehouse_location/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: CWLEvaluationCB -timeout_seconds: 20 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_cflp_construct/__init__.py b/examples/benchmark_tasks/optimization_cflp_construct/__init__.py deleted file mode 100644 index b91eb218..00000000 --- a/examples/benchmark_tasks/optimization_cflp_construct/__init__.py +++ /dev/null @@ -1,310 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_cflp_construct -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# Module Name: CFLPEvaluation -# Last Revision: 2025/2/16 -# Description: Evaluates the Capacitated Facility Location Problem (CFLP). -# Given a set of facilities and customers, the goal is to assign customers to facilities -# while respecting facility capacities and minimizing total costs. -# This module is part of the LLM4AD project (https://github.com/Optima-CityU/llm4ad). -# -# Parameters: -# - timeout_seconds: Maximum allowed time (in seconds) for the evaluation process: int (default: 60). -# - n_instance: Number of problem instances to generate: int (default: 16). -# - n_facilities: Number of facilities: int (default: 5). -# - n_customers: Number of customers: int (default: 8). -# - max_capacity: Maximum capacity of each facility: int (default: 100). -# - max_demand: Maximum demand of each customer: int (default: 20). -# - max_cost: Maximum cost of assigning a customer to a facility: int (default: 50). -# -# References: -# - Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - - -from __future__ import annotations -from typing import Callable, Any, List, Tuple -import numpy as np -import matplotlib.pyplot as plt - -from llm4ad_loader import Evaluation -import os, sys -sys.path.insert(0, os.path.dirname(__file__)) -from get_instance import GetData -# from llm4ad.task.optimization.cflp_construct.get_instance import GetData # Converted from LLM4AD import -# from llm4ad.task.optimization.cflp_construct.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\n\ndef select_next_assignment(assignments: List[List[int]], remaining_customers: List[int], remaining_capacities: List[int], customer_demands: List[int], assignment_costs: List[List[int]]) -> Tuple[int, int]:\n """\n Constructive heuristic for the Capacitated Facility Location Problem.\n Assigns the next customer to the facility with the lowest cost that has sufficient capacity.\n\n Args:\n assignments: Current assignments of customers to facilities.\n remaining_customers: List of customer indices not yet assigned.\n remaining_capacities: Remaining capacities of facilities.\n customer_demands: List of customer demands.\n assignment_costs: 2D list of assignment costs (facility-to-customer).\n\n Returns:\n A tuple containing:\n - The selected customer index.\n - The selected facility index (or None if no feasible assignment exists).\n """\n # Iterate over all remaining customers\n for customer in remaining_customers:\n # Iterate over all facilities to find the one with the lowest cost and sufficient capacity\n min_cost = float(\'inf\')\n selected_facility = None\n\n for facility in range(len(remaining_capacities)):\n if remaining_capacities[facility] >= customer_demands[customer] and assignment_costs[facility][customer] < min_cost:\n min_cost = assignment_costs[facility][customer]\n selected_facility = facility\n\n # If a feasible facility is found, return the customer and facility\n if selected_facility is not None:\n return customer, selected_facility\n\n # If no feasible assignment is found, return None\n return None, None' -task_description = "'" - - -__all__ = ['CFLPEvaluation'] - - -class CFLPEvaluation(Evaluation): - """Evaluator for the Capacitated Facility Location Problem.""" - - def __init__(self, - timeout_seconds: int = 60, - n_instance: int = 16, - n_facilities: int = 50, - n_customers: int = 50, - max_capacity: int = 100, - max_demand: int = 20, - max_cost: int = 50, - **kwargs): - """ - Initialize the evaluator. - """ - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - self.n_instance = n_instance - self.n_facilities = n_facilities - self.n_customers = n_customers - self.max_capacity = max_capacity - self.max_demand = max_demand - self.max_cost = max_cost - getData = GetData(self.n_instance, self.n_facilities, self.n_customers, self.max_capacity, self.max_demand, self.max_cost) - self._datasets = getData.generate_instances() - - def evaluate_program(self, program_str: str, callable_func: Callable) -> Any | None: - return self.evaluate_cflp(callable_func) - - def plot_solution(self, facility_capacities: List[int], customer_demands: List[int], assignments: List[List[int]], assignment_costs: List[List[int]]): - """ - Plot the final solution of assignments for the Capacitated Facility Location Problem. - - Args: - facility_capacities: A list of facility capacities. - customer_demands: A list of customer demands. - assignments: A list of assignments, where each assignment is a list of customer indices assigned to a facility. - assignment_costs: A 2D list (matrix) of costs, where the cost of assigning customer j to facility i is assignment_costs[i][j]. - """ - n_facilities = len(facility_capacities) - n_customers = len(customer_demands) - - # Create a figure and axis - fig, ax = plt.subplots(figsize=(10, 6)) - - # Plot facilities and customers - for facility in range(n_facilities): - # Plot facility as a rectangle - ax.add_patch(plt.Rectangle((facility - 0.4, -0.4), 0.8, 0.8, color='skyblue', label='Facility' if facility == 0 else None)) - ax.text(facility, 0, f'F{facility}\nCap: {facility_capacities[facility]}', ha='center', va='center', fontsize=10) - - # Plot assigned customers - for customer in assignments[facility]: - ax.plot([facility, customer], [0, 1], 'k--', linewidth=0.5) # Line connecting facility to customer - ax.add_patch(plt.Circle((customer, 1), 0.1, color='orange', label='Customer' if facility == 0 and customer == 0 else None)) - ax.text(customer, 1.1, f'C{customer}\nDem: {customer_demands[customer]}', ha='center', va='bottom', fontsize=8) - # Add cost as text near the line - ax.text((facility + customer) / 2, 0.5, f'Cost: {assignment_costs[facility][customer]}', ha='center', va='center', fontsize=8, rotation=45) - - # Set axis limits and labels - ax.set_xlim(-1, n_customers) - ax.set_ylim(-0.5, 1.5) - ax.set_xticks(range(n_customers)) - ax.set_yticks([0, 1]) - ax.set_yticklabels(['Facilities', 'Customers']) - ax.set_title('Capacitated Facility Location Problem - Assignments') - ax.legend(loc='upper right') - - # Show the plot - plt.tight_layout() - plt.show() - - def assign_customers(self, facility_capacities: List[int], customer_demands: List[int], assignment_costs: List[List[int]], eva: Callable) -> Tuple[int, List[List[int]]]: - """ - Assign customers to facilities using a constructive heuristic. - - Args: - facility_capacities: A list of facility capacities. - customer_demands: A list of customer demands. - assignment_costs: A 2D list (matrix) of costs, where the cost of assigning customer j to facility i is assignment_costs[i][j]. - eva: The constructive heuristic function to select the next customer-facility assignment. - - Returns: - A tuple containing: - - The total cost of the assignments. - - A list of assignments, where each assignment is a list of customer indices assigned to a facility. - """ - n_facilities = len(facility_capacities) - n_customers = len(customer_demands) - assignments = [[] for _ in range(n_facilities)] # Initialize empty assignments for each facility - remaining_customers = list(range(n_customers)) # List of remaining customer indices - remaining_capacities = facility_capacities.copy() # Copy of facility capacities to track remaining capacities - total_cost = 0 # Total cost of assignments - - while remaining_customers: - # Use the heuristic to select the next customer-facility assignment - selected_customer, selected_facility = eva(assignments, remaining_customers, remaining_capacities, customer_demands, assignment_costs) - - if selected_facility is not None: - # Assign the selected customer to the selected facility - assignments[selected_facility].append(selected_customer) - # Update the remaining capacity of the selected facility - remaining_capacities[selected_facility] -= customer_demands[selected_customer] - # Add the assignment cost to the total cost - total_cost += assignment_costs[selected_facility][selected_customer] - else: - # If no feasible assignment is found, stop assigning (no more feasible assignments) - break - - # Remove the selected customer from the remaining customers - remaining_customers.remove(selected_customer) - - return total_cost, assignments - - def evaluate_cflp(self, eva: Callable) -> float: - """ - Evaluate the constructive heuristic for the Capacitated Facility Location Problem. - - Args: - instance_data: List of dictionaries containing facility capacities, customer demands, and assignment costs. - n_ins: Number of instances to evaluate. - eva: The constructive heuristic function to evaluate. - - Returns: - The average total cost across all instances. - """ - total_cost = 0 - - for instance in self._datasets[:self.n_instance]: - facility_capacities = instance["facility_capacities"] - customer_demands = instance["customer_demands"] - assignment_costs = instance["assignment_costs"] - cost, _ = self.assign_customers(facility_capacities, customer_demands, assignment_costs, eva) - total_cost += cost - - average_cost = total_cost / self.n_instance - return -average_cost - - -if __name__ == '__main__': - - def select_next_assignment(assignments: List[List[int]], remaining_customers: List[int], remaining_capacities: List[int], customer_demands: List[int], assignment_costs: List[List[int]]) -> Tuple[int, int]: - """ - Constructive heuristic for the Capacitated Facility Location Problem. - Assigns the next customer to the facility with the lowest cost that has sufficient capacity. - - Args: - assignments: Current assignments of customers to facilities. - remaining_customers: List of customer indices not yet assigned. - remaining_capacities: Remaining capacities of facilities. - customer_demands: List of customer demands. - assignment_costs: 2D list of assignment costs (facility-to-customer). - - Returns: - A tuple containing: - - The selected customer index. - - The selected facility index (or None if no feasible assignment exists). - """ - # Iterate over all remaining customers - for customer in remaining_customers: - # Iterate over all facilities to find the one with the lowest cost and sufficient capacity - min_cost = float('inf') - selected_facility = None - - for facility in range(len(remaining_capacities)): - if remaining_capacities[facility] >= customer_demands[customer] and assignment_costs[facility][customer] < min_cost: - min_cost = assignment_costs[facility][customer] - selected_facility = facility - - # If a feasible facility is found, return the customer and facility - if selected_facility is not None: - return customer, selected_facility - - # If no feasible assignment is found, return None - return None, None - - - bp1d = CFLPEvaluation() - ave_bins = bp1d.evaluate_program('_', select_next_assignment) - print(ave_bins) - -# Task configuration for benchmark task -ENTRY_NAME = 'select_next_assignment' -FUNCTION_SIGNATURE = 'def select_next_assignment(...):' -IMPORT_HEADER = 'import numpy as np\nimport math' -TASK_DESCRIPTION = "'" -OBJECTIVE_TEXT = "You are optimizing the implementation of `select_next_assignment` for the LLM4AD task.\\n\\nTask description:\\n'\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible." -TEMPLATE_FUNCTION = 'import numpy as np\n\ndef select_next_assignment(assignments: List[List[int]], remaining_customers: List[int], remaining_capacities: List[int], customer_demands: List[int], assignment_costs: List[List[int]]) -> Tuple[int, int]:\n """\n Constructive heuristic for the Capacitated Facility Location Problem.\n Assigns the next customer to the facility with the lowest cost that has sufficient capacity.\n\n Args:\n assignments: Current assignments of customers to facilities.\n remaining_customers: List of customer indices not yet assigned.\n remaining_capacities: Remaining capacities of facilities.\n customer_demands: List of customer demands.\n assignment_costs: 2D list of assignment costs (facility-to-customer).\n\n Returns:\n A tuple containing:\n - The selected customer index.\n - The selected facility index (or None if no feasible assignment exists).\n """\n # Iterate over all remaining customers\n for customer in remaining_customers:\n # Iterate over all facilities to find the one with the lowest cost and sufficient capacity\n min_cost = float(\'inf\')\n selected_facility = None\n\n for facility in range(len(remaining_capacities)):\n if remaining_capacities[facility] >= customer_demands[customer] and assignment_costs[facility][customer] < min_cost:\n min_cost = assignment_costs[facility][customer]\n selected_facility = facility\n\n # If a feasible facility is found, return the customer and facility\n if selected_facility is not None:\n return customer, selected_facility\n\n # If no feasible assignment is found, return None\n return None, None' -EVAL_CLASS_NAME = 'CFLPEvaluation' -EVAL_KWARGS = {'timeout_seconds': 30} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_cflp_construct/get_instance.py b/examples/benchmark_tasks/optimization_cflp_construct/get_instance.py deleted file mode 100644 index e6ba0435..00000000 --- a/examples/benchmark_tasks/optimization_cflp_construct/get_instance.py +++ /dev/null @@ -1,65 +0,0 @@ -import numpy as np - - -class GetData: - def __init__(self, n_instance: int, n_facilities: int, n_customers: int, max_capacity: int, max_demand: int, max_cost: int): - """ - Initialize the GetDataCFLP class for the Capacitated Facility Location Problem. - - Args: - n_instance: Number of instances to generate. - n_facilities: Number of facilities. - n_customers: Number of customers. - max_capacity: Maximum capacity of any facility. - max_demand: Maximum demand of any customer. - max_cost: Maximum cost for assigning a customer to a facility. - """ - self.n_instance = n_instance - self.n_facilities = n_facilities - self.n_customers = n_customers - self.max_capacity = max_capacity - self.max_demand = max_demand - self.max_cost = max_cost - - def generate_instances(self): - """ - Generate instances for the Capacitated Facility Location Problem. - - Returns: - A list of dictionaries, where each dictionary contains: - - facility_capacities: A list of capacities for each facility. - - customer_demands: A list of demands for each customer. - - assignment_costs: A 2D list (matrix) of costs, where the cost of assigning - customer j to facility i is assignment_costs[i][j]. - """ - np.random.seed(2024) # Set seed for reproducibility - instance_data = [] - - for _ in range(self.n_instance): - # Generate random capacities for facilities - facility_capacities = np.random.randint(5, self.max_capacity + 1, size=self.n_facilities).tolist() - - # Generate random demands for customers - customer_demands = np.random.randint(5, self.max_demand + 1, size=self.n_customers).tolist() - - # Generate random assignment costs (facility-to-customer cost matrix) - assignment_costs = np.random.randint(5, self.max_cost + 1, size=(self.n_facilities, self.n_customers)).tolist() - - instance_data.append({ - "facility_capacities": facility_capacities, - "customer_demands": customer_demands, - "assignment_costs": assignment_costs - }) - - return instance_data - -# # Example usage: -# data_generator = GetDataCFLP(n_instance=3, n_facilities=5, n_customers=8, max_capacity=100, max_demand=20, max_cost=50) -# instances = data_generator.generate_instances() -# for instance in instances: -# print("Facility Capacities:", instance["facility_capacities"]) -# print("Customer Demands:", instance["customer_demands"]) -# print("Assignment Costs:") -# for row in instance["assignment_costs"]: -# print(row) -# print() diff --git a/examples/benchmark_tasks/optimization_cflp_construct/paras.yaml b/examples/benchmark_tasks/optimization_cflp_construct/paras.yaml deleted file mode 100644 index 5d2994a8..00000000 --- a/examples/benchmark_tasks/optimization_cflp_construct/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: CFLPEvaluation -timeout_seconds: 30 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_common_due_date_scheduling/__init__.py b/examples/benchmark_tasks/optimization_common_due_date_scheduling/__init__.py deleted file mode 100644 index 7645571d..00000000 --- a/examples/benchmark_tasks/optimization_common_due_date_scheduling/__init__.py +++ /dev/null @@ -1,310 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_common_due_date_scheduling -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# References: -# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language -# model agents in algorithm search for combinatorial optimization. -# arXiv preprint arXiv:2504.04310 (2025). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -from typing import Any -import numpy as np -from llm4ad_loader import Evaluation -from llm4ad_loader import load_subdir_as_text -# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader -# from llm4ad.task.optimization.co_bench.common_due_date_scheduling_co_bench.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(jobs: List[Tuple[int, int, int]], h: float = 0.6) -> Dict[str, List[int]]:\n """\n Solves the restricted single‐machine common due date scheduling problem.\n The problem:\n Given a list of jobs where each job is represented as a tuple (p, a, b):\n • p: processing time\n • a: earliness penalty coefficient\n • b: tardiness penalty coefficient\n and an optional parameter h (default 0.6), the common due date is computed as:\n d = floor(sum(p) * h)\n A schedule (i.e., a permutation of job indices in 1‐based numbering) is produced.\n When processing the jobs in that order, the penalty is computed by:\n • Adding a × (d − C) if a job’s completion time C is less than d,\n • Adding b × (C − d) if C is greater than d,\n • No penalty if C equals d.\n The objective is to minimize the total penalty.\n Input kwargs:\n - \'jobs\' (List[Tuple[int, int, int]]): a list of tuples where each tuple represents a job with:\n • p (int): processing time,\n • a (int): earliness penalty coefficient,\n • b (int): tardiness penalty coefficient.\n - Optional: \'h\' (float): the factor used to compute the common due date (default is 0.6).\n Evaluation Metric:\n The computed schedule is evaluated by accumulating processing times and applying\n the appropriate earliness/tardiness penalties with respect to the common due date.\n Returns:\n A dictionary with key \'schedule\' whose value is a list of integers representing\n a valid permutation of job indices (1-based).\n """\n # Placeholder implementation: simply return the jobs in their original order.\n jobs = kwargs.get(\'jobs\', [])\n n = len(jobs)\n return {\'schedule\': list(range(1, n + 1))}' -task_description = '("The **Restricted Single-Machine Common Due Date Scheduling Problem** involves scheduling a set "' - - -__all__ = ['CDDSEvaluationCB'] - - -class CDDSEvaluationCB(Evaluation): - - def __init__(self, - timeout_seconds=50, - **kwargs): - - """ - Args: - None - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - # Load datasets from Hugging Face - dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Common due date scheduling") - self._datasets = {} - for filename in dataset: - # Join all text rows into a single string - text_content = '\n'.join([row['text'] for row in dataset[filename]]) - self._datasets[filename] = text_content - - def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: - return self.evaluate(callable_func) - - def evaluate(self, eva: callable) -> float | None: - ins_cases = [] - for case_id, ins in enumerate(self._datasets.values()): - ins_cases.append(self.load_data(ins)) - - fitness_list = [] - try: - for i in ins_cases: - for j in i: - result = eva(j['jobs'], j['h']) - fitness = self.eval_func(j['jobs'], result['schedule'], j['h']) - fitness_list.append(fitness) - - return -np.mean(fitness_list) - - except ValueError as e: - print(e) - return None - - def load_data(self, input_string): - """ - Reads the input file and returns a list of cases. - Each case is represented as a dictionary containing: - - 'jobs': a list of tuples (p, a, b) for each job. - - 'h': a float parameter for due date computation (default set to 0.6). - The input format: - • The first token is an integer T indicating the number of cases. - • For each case: - – The first integer is n, the number of jobs. - – The following n lines each contain three space-separated integers: p, a, and b. - Returns: - List[dict]: A list where each element is a dictionary with the keys 'jobs' and 'h'. - """ - cases = [] - try: - tokens = input_string.strip().split() - except Exception as e: - raise ValueError(f"Error reading input file: {e}") - - index = 0 - try: - T = int(tokens[index]) - except Exception as e: - raise ValueError("Invalid input format: first token must be an integer (number of cases).") - index += 1 - - for t in range(T): - if index >= len(tokens): - raise ValueError(f"Unexpected end of input while reading case {t + 1}.") - try: - n = int(tokens[index]) - except Exception as e: - raise ValueError(f"Invalid job count for case {t + 1}.") - index += 1 - - jobs = [] - for i in range(n): - if index + 2 >= len(tokens): - raise ValueError(f"Unexpected end of input while reading job data for case {t + 1}.") - try: - p = int(tokens[index]) - a = int(tokens[index + 1]) - b = int(tokens[index + 2]) - except Exception as e: - raise ValueError(f"Invalid job data for job {i + 1} in case {t + 1}.") - index += 3 - jobs.append((p, a, b)) - - # For each case, we include the jobs and set a default h value (can be adjusted if needed) - cases.append({'jobs': jobs, 'h': 0.6}) - - return cases - - def eval_func(self, jobs, schedule, h=0.6): - """ - Evaluates the quality of a schedule for the restricted single‐machine common due date problem. - Parameters: - - jobs (List[Tuple[int, int, int]]): List of jobs, each represented as (p, a, b). - - schedule (List[int]): A permutation (1-based indices) representing the processing order. - - h (float): Factor for computing the common due date d = floor(sum(p) * h). - Returns: - int: The total penalty computed for the schedule. - The evaluation: - 1. Compute d = floor(total_processing_time * h). - 2. Process jobs in the given order, accumulating processing times. - 3. For each job, if the cumulative time C is less than d, add a penalty a × (d − C); - if C is greater than d, add a penalty b × (C − d); no penalty is incurred if C equals d. - 4. Sum the penalties to yield the total score. - """ - total_processing = sum(p for p, a, b in jobs) - d = int(total_processing * h) # floor operation via int conversion for non-negative totals - - cumulative_time = 0 - total_penalty = 0 - # Validate that schedule is a permutation of 1..n - n = len(jobs) - if sorted(schedule) != list(range(1, n + 1)): - raise ValueError(f"Schedule must be a permutation of 1 to {n}. Provided schedule: {schedule}") - - for idx in schedule: - try: - p, a, b = jobs[idx - 1] # Convert from 1-based to 0-based indexing - except IndexError: - raise ValueError(f"Job index {idx} is out of bounds for jobs list of length {n}.") - cumulative_time += p - if cumulative_time < d: - total_penalty += a * (d - cumulative_time) - elif cumulative_time > d: - total_penalty += b * (cumulative_time - d) - # No penalty if cumulative_time == d - return total_penalty - - def norm_score(self, results): - """ - Given a dictionary `results` where each key is a test case filename (e.g., "sch10.txt") - and the value is a tuple (scores, error_message), this function returns a new dictionary - with the normed results. For each test case, the normed score for each k is computed as: - norm = (optimal score for h=0.6) / (model's score) - The optimal scores for h=0.6 are pre-defined for each job instance size n. - If a score in the list is not numeric (e.g., "Timeout (10s)"), that entry is skipped. - Parameters: - results (dict): A dictionary where keys are filenames (e.g., "sch10.txt") and values - are tuples (scores, error_message). - Returns: - dict: A dictionary with the same keys, where each value is a list of normed scores - computed for the numeric entries only. - """ - # Pre-defined optimal scores for h = 0.6 by instance size (n) - optimal_scores = { - 10: [841, 615, 793, 815, 521, 755, 1101, 610, 582, 710], - 20: [2986, 3260, 3600, 3336, 2206, 3016, 4175, 1638, 1992, 2116], - 50: [17990, 14231, 16497, 14105, 14650, 14251, 17715, 21367, 14298, 14377], - 100: [72019, 59351, 68537, 69231, 55291, 62519, 62213, 80844, 58771, 61419], - 200: [254268, 266028, 254647, 297269, 260455, 236160, 247555, 225572, 255029, 269236], - 500: [1581233, 1715332, 1644947, 1640942, 1468325, 1413345, 1634912, 1542090, 1684055, 1520515], - 1000: [6411581, 6112598, 5985538, 6096729, 6348242, 6082142, 6575879, 6069658, 6188416, 6147295], - } - - normed = {} - for case, (scores, error_message) in results.items(): - # Try to extract the number of jobs (n) from the filename. - # Expected format: "sch{n}.txt", e.g., "sch10.txt" -> n = 10. - try: - n_val = int(case.replace("sch", "").replace(".txt", "")) - except ValueError: - continue # Skip if the filename is not in expected format. - - # Only process if we have optimal scores for this instance size. - if n_val not in optimal_scores: - continue - - optimal_list = optimal_scores[n_val] - normed_scores = [] - # Process each score in the scores list, along with its index (for k=1,...,10). - for idx, score in enumerate(scores): - # If the score is not numeric, skip it. - if isinstance(score, (int, float)): - # Compute normalized score as (optimal / model score) - norm_val = optimal_list[idx] / score - normed_scores.append(norm_val) - else: - normed_scores.append(score) - normed[case] = (normed_scores, error_message) - - return normed - - def get_dev(self): - dev = {'sch10.txt': [4, 5, 6], 'sch100.txt': [9, 8, 5], 'sch1000.txt': [4, 9, 0], - 'sch20.txt': [6, 5, 3], 'sch200.txt': [2, 4, 5], 'sch50.txt': [1, 8, 2], - 'sch500.txt': [3, 6, 9]} - - return dev - - -# Task configuration for benchmark task -ENTRY_NAME = 'solve' -FUNCTION_SIGNATURE = 'def solve(...):' -IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' -TASK_DESCRIPTION = '("The **Restricted Single-Machine Common Due Date Scheduling Problem** involves scheduling a set "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The **Restricted Single-Machine Common Due Date Scheduling Problem** involves scheduling a set "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(jobs: List[Tuple[int, int, int]], h: float = 0.6) -> Dict[str, List[int]]:\n """\n Solves the restricted single‐machine common due date scheduling problem.\n The problem:\n Given a list of jobs where each job is represented as a tuple (p, a, b):\n • p: processing time\n • a: earliness penalty coefficient\n • b: tardiness penalty coefficient\n and an optional parameter h (default 0.6), the common due date is computed as:\n d = floor(sum(p) * h)\n A schedule (i.e., a permutation of job indices in 1‐based numbering) is produced.\n When processing the jobs in that order, the penalty is computed by:\n • Adding a × (d − C) if a job’s completion time C is less than d,\n • Adding b × (C − d) if C is greater than d,\n • No penalty if C equals d.\n The objective is to minimize the total penalty.\n Input kwargs:\n - \'jobs\' (List[Tuple[int, int, int]]): a list of tuples where each tuple represents a job with:\n • p (int): processing time,\n • a (int): earliness penalty coefficient,\n • b (int): tardiness penalty coefficient.\n - Optional: \'h\' (float): the factor used to compute the common due date (default is 0.6).\n Evaluation Metric:\n The computed schedule is evaluated by accumulating processing times and applying\n the appropriate earliness/tardiness penalties with respect to the common due date.\n Returns:\n A dictionary with key \'schedule\' whose value is a list of integers representing\n a valid permutation of job indices (1-based).\n """\n # Placeholder implementation: simply return the jobs in their original order.\n jobs = kwargs.get(\'jobs\', [])\n n = len(jobs)\n return {\'schedule\': list(range(1, n + 1))}' -EVAL_CLASS_NAME = 'CDDSEvaluationCB' -EVAL_KWARGS = {'timeout_seconds': 20} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_common_due_date_scheduling/paras.yaml b/examples/benchmark_tasks/optimization_common_due_date_scheduling/paras.yaml deleted file mode 100644 index fb3b977d..00000000 --- a/examples/benchmark_tasks/optimization_common_due_date_scheduling/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: CDDSEvaluationCB -timeout_seconds: 20 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_constrained_guillotine_cutting/__init__.py b/examples/benchmark_tasks/optimization_constrained_guillotine_cutting/__init__.py deleted file mode 100644 index 2bcd6b6f..00000000 --- a/examples/benchmark_tasks/optimization_constrained_guillotine_cutting/__init__.py +++ /dev/null @@ -1,399 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_constrained_guillotine_cutting -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# References: -# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language -# model agents in algorithm search for combinatorial optimization. -# arXiv preprint arXiv:2504.04310 (2025). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -from typing import Any -import numpy as np -from llm4ad_loader import Evaluation -from llm4ad_loader import load_subdir_as_text -# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader -# from llm4ad.task.optimization.co_bench.constrained_guillotine_cutting_co_bench.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(m: int, stock_length: int, stock_width: int, piece_types: list) -> dict:\n """\n Solves the Fixed Orientation Guillotine Cutting problem.\n Problem Description:\n Given a rectangular stock sheet (with specified length and width) and a set of piece types\n (each defined by a length, width, an upper bound on the number of times it may appear, and a value),\n the goal is to determine a placement for the pieces such that:\n - Each placed piece lies entirely within the stock sheet.\n - Pieces do not overlap.\n - The number of pieces placed for any type does not exceed its allowed maximum.\n - The orientation of the pieces is fixed (i.e. no rotation is allowed).\n - The total value reported equals the sum of the values of the placed pieces.\n Input kwargs (for one case):\n - m: integer, the number of piece types.\n - stock_length: integer, the length of the stock sheet.\n - stock_width: integer, the width of the stock sheet.\n - piece_types: list of dictionaries. Each dictionary has the keys:\n \'length\' : int, the length of the piece.\n \'width\' : int, the width of the piece.\n \'max\' : int, maximum number of pieces allowed.\n \'value\' : int, value of the piece.\n Returns:\n A dictionary with the following keys:\n - total_value: int, the computed total value (must equal the sum of the piece values in placements).\n - placements: list of placements, where each placement is a tuple of 6 integers:\n (piece_type_index, x, y, placed_length, placed_width, orientation_flag)\n The orientation_flag is always 0 since rotation is not allowed.\n """\n # Your optimization/placement algorithm should go here.\n # For now, this is a placeholder that meets the output format requirements.\n\n # Example placeholder output (no actual pieces placed):\n return {"total_value": 0, "placements": []}' -task_description = '("The problem involves optimizing the guillotine feasible placement of a set of rectangular pieces "' - - -__all__ = ['CGCEvaluationCB'] - - -class CGCEvaluationCB(Evaluation): - - def __init__(self, - timeout_seconds=50, - **kwargs): - - """ - Args: - None - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - # Load datasets from Hugging Face - dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Constrained guillotine cutting") - self._datasets = {} - for filename in dataset: - # Join all text rows into a single string - text_content = '\n'.join([row['text'] for row in dataset[filename]]) - self._datasets[filename] = text_content - - def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: - return self.evaluate(callable_func) - - def evaluate(self, eva: callable) -> float | None: - ins_cases = [] - for case_id, ins in enumerate(self._datasets.values()): - ins_cases.append(self.load_data(ins)) - - fitness_list = [] - try: - for i in ins_cases: - for j in i: - result = eva(j['m'], j['stock_length'], j['stock_width'], j['piece_types']) - fitness = self.eval_func(j['m'], j['stock_length'], j['stock_width'], j['piece_types'], result['total_value'], result['placements']) - fitness_list.append(fitness) - - return np.mean(fitness_list) # itself is a maximize problem - - except ValueError as e: - print(e) - return None - - def load_data(self, input_string): - """ - Loads one or more cases from an input file for the Constrained Guillotine Cutting problem. - The input file contains one or more cases concatenated together. Each case is structured as follows: - - The first token: an integer m (the number of piece types). - - The next two tokens: stock_length and stock_width. - - Then 4*m tokens follow, where each group of 4 tokens represents a piece type: - piece_length, piece_width, maximum permitted count, piece_value. - Parameters: - input_path (str): Path to the input TXT file. - Returns: - List[dict]: A list where each element is a dictionary with the following keys: - - m: int, number of piece types. - - stock_length: int, length of the stock sheet. - - stock_width: int, width of the stock sheet. - - piece_types: list of dicts, each with keys 'length', 'width', 'max', 'value'. - """ - cases = [] - content = input_string - tokens = content.split() - pos = 0 - total_tokens = len(tokens) - - while pos < total_tokens: - # Ensure there are at least 3 tokens to read m, stock_length, stock_width - if pos + 3 > total_tokens: - raise ValueError("Insufficient data for a new case.") - try: - m = int(tokens[pos]) - stock_length = int(tokens[pos + 1]) - stock_width = int(tokens[pos + 2]) - except: - raise ValueError("Error parsing m, stock_length, or stock_width.") - pos += 3 - - # There must be 4*m tokens for the piece types. - if pos + 4 * m > total_tokens: - raise ValueError("Not enough tokens for piece types in one case.") - - piece_types = [] - for i in range(m): - try: - p_length = int(tokens[pos]) - p_width = int(tokens[pos + 1]) - max_count = int(tokens[pos + 2]) - p_value = int(tokens[pos + 3]) - except: - raise ValueError("Error parsing piece type data.") - piece_types.append({ - 'length': p_length, - 'width': p_width, - 'max': max_count, - 'value': p_value - }) - pos += 4 - - case_data = { - "m": m, - "stock_length": stock_length, - "stock_width": stock_width, - "piece_types": piece_types - } - cases.append(case_data) - - return cases - - def eval_func(self, m, stock_length, stock_width, piece_types, total_value, placements): - """ - Evaluates a solution for the Fixed Orientation Guillotine Cutting problem by verifying all constraints. - Raises an error immediately upon any constraint violation. - Parameters: - m (int): Number of piece types. - stock_length (int): Length of the stock rectangle. - stock_width (int): Width of the stock rectangle. - piece_types (list of dict): Each dict has keys: - 'length': int, piece length. - 'width' : int, piece width. - 'max' : int, maximum permitted count for the piece type. - 'value' : int, value of the piece. - total_value (int): The reported total value from the solution. - placements (list): List of placements. Each placement is a tuple or list of 6 integers: - (piece_type_index, x, y, placed_length, placed_width, orientation_flag) - where orientation_flag must be 0 (rotation not allowed). - Returns: - int: The computed total value if the solution is valid. - Constraints verified: - - Each placement is a well-formed 6-tuple of integers. - - The piece type index is within the valid range. - - The orientation flag is 0 (rotation is not allowed). - - The placed dimensions match the expected dimensions. - - Each piece is completely within the stock boundaries. - - No two pieces overlap. - - The count of pieces of each type does not exceed its allowed maximum. - - The reported total_value exactly equals the computed sum of placed piece values. - - The set of placements satisfies the guillotine cutting condition. - """ - - # Helper function: Check guillotine feasibility recursively. - def is_guillotine(rects, bx, by, ex, ey): - """ - Recursively checks if the collection of placed rectangles (rects) in the box - defined by (bx, by) - (ex, ey) is guillotine separable. - A set of placements is considered guillotine feasible if there exists at least one straight cut - (vertical or horizontal) that does not slice through any rectangle, and the property holds recursively - on the resulting subregions. Empty regions or regions exactly matching a placed piece are considered valid. - """ - # If there are no pieces, the region is trivially guillotine separable. - if not rects: - return True - # If a single rectangle exactly covers the region, it is guillotine separable. - if len(rects) == 1: - r = rects[0] - if r[0] == bx and r[1] == by and r[2] == ex and r[3] == ey: - return True - - # Try vertical cuts. - for x in range(bx + 1, ex): - # A vertical cut at x is valid if no rectangle is cut by the line. - if all((r[2] <= x or r[0] >= x) for r in rects): - left_rects = [r for r in rects if r[2] <= x] - right_rects = [r for r in rects if r[0] >= x] - if is_guillotine(left_rects, bx, by, x, ey) and is_guillotine(right_rects, x, by, ex, ey): - return True - - # Try horizontal cuts. - for y in range(by + 1, ey): - if all((r[3] <= y or r[1] >= y) for r in rects): - bottom_rects = [r for r in rects if r[3] <= y] - top_rects = [r for r in rects if r[1] >= y] - if is_guillotine(bottom_rects, bx, by, ex, y) and is_guillotine(top_rects, bx, y, ex, ey): - return True - - return False - - computed_value = 0 - type_counts = [0] * m # Count pieces for each type. - rects = [] # To store placed rectangles as (x1, y1, x2, y2) - - # Process and validate each placement. - for idx, placement in enumerate(placements): - if not (isinstance(placement, (list, tuple)) and len(placement) == 6): - raise ValueError(f"Placement {idx} is not a 6-tuple: {placement}") - - try: - type_idx = int(placement[0]) - x = int(placement[1]) - y = int(placement[2]) - placed_len = int(placement[3]) - placed_wid = int(placement[4]) - orient = int(placement[5]) - except Exception: - raise ValueError(f"Non-integer value in placement {idx}: {placement}") - - # Validate piece type index (using 1-indexing). - if type_idx < 1 or type_idx > m: - raise ValueError(f"Placement {idx} has invalid piece type index {type_idx}") - - # Orientation must be 0 (rotation is not allowed). - if orient != 0: - raise ValueError(f"Placement {idx} has invalid orientation flag {orient}; rotation is not allowed.") - - # Retrieve expected dimensions and value. - piece = piece_types[type_idx - 1] - p_length = piece['length'] - p_width = piece['width'] - max_allowed = piece['max'] - p_value = piece['value'] - - # Since rotation is not allowed, expected dimensions are as given. - expected_length, expected_width = p_length, p_width - - # Check that the placed dimensions match the expected dimensions. - if placed_len != expected_length or placed_wid != expected_width: - raise ValueError( - f"Placement {idx} dimensions ({placed_len}, {placed_wid}) do not match expected ({expected_length}, {expected_width})") - - # Check boundaries: the entire piece must lie within the stock sheet. - if x < 0 or y < 0 or (x + placed_len) > stock_length or (y + placed_wid) > stock_width: - raise ValueError( - f"Placement {idx} with rectangle {(x, y, x + placed_len, y + placed_wid)} is out of stock bounds (0,0) to ({stock_length},{stock_width})") - - # Passed validations: count the piece and add its value. - type_counts[type_idx - 1] += 1 - computed_value += p_value - - # Record rectangle (bottom-left: (x, y), top-right: (x+placed_len, y+placed_wid)) - rects.append((x, y, x + placed_len, y + placed_wid)) - - # Check for overlapping placements. - num_rects = len(rects) - for i in range(num_rects): - for j in range(i + 1, num_rects): - r1 = rects[i] - r2 = rects[j] - dx = min(r1[2], r2[2]) - max(r1[0], r2[0]) - dy = min(r1[3], r2[3]) - max(r1[1], r2[1]) - if dx > 0 and dy > 0: - raise ValueError(f"Placements {i} and {j} overlap.") - - # Check that no piece type is placed more times than its allowed maximum. - for i in range(m): - if type_counts[i] > piece_types[i]['max']: - raise ValueError( - f"Piece type {i + 1} exceeds allowed count: {type_counts[i]} > {piece_types[i]['max']}") - - # Check the guillotine condition on the entire stock sheet. - if not is_guillotine(rects, 0, 0, stock_length, stock_width): - raise ValueError("Guillotine condition violated: the placement layout is not guillotine separable.") - - # Verify that the reported total_value matches the computed total. - if computed_value != total_value: - raise ValueError(f"Reported total value {total_value} does not match computed value {computed_value}.") - - return computed_value - - def norm_score(self, results): - optimal_scores = { - "cgcut1.txt": [244], - "cgcut2.txt": [2892], - "cgcut3.txt": [1860], - } - - normed = {} - for case, (scores, error_message) in results.items(): - if case not in optimal_scores: - continue # Skip if there's no optimal score defined. - optimal_list = optimal_scores[case] - normed_scores = [] - # Compute normalized score for each index. - for idx, score in enumerate(scores): - if isinstance(score, (int, float)): - normed_scores.append(score / optimal_list[idx]) - else: - normed_scores.append(score) - normed[case] = (normed_scores, error_message) - - return normed - - def get_dev(self): - dev = {'cgcut1.txt': [], 'cgcut2.txt': [], 'cgcut3.txt': []} - - return dev - - -# Task configuration for benchmark task -ENTRY_NAME = 'solve' -FUNCTION_SIGNATURE = 'def solve(...):' -IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' -TASK_DESCRIPTION = '("The problem involves optimizing the guillotine feasible placement of a set of rectangular pieces "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The problem involves optimizing the guillotine feasible placement of a set of rectangular pieces "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(m: int, stock_length: int, stock_width: int, piece_types: list) -> dict:\n """\n Solves the Fixed Orientation Guillotine Cutting problem.\n Problem Description:\n Given a rectangular stock sheet (with specified length and width) and a set of piece types\n (each defined by a length, width, an upper bound on the number of times it may appear, and a value),\n the goal is to determine a placement for the pieces such that:\n - Each placed piece lies entirely within the stock sheet.\n - Pieces do not overlap.\n - The number of pieces placed for any type does not exceed its allowed maximum.\n - The orientation of the pieces is fixed (i.e. no rotation is allowed).\n - The total value reported equals the sum of the values of the placed pieces.\n Input kwargs (for one case):\n - m: integer, the number of piece types.\n - stock_length: integer, the length of the stock sheet.\n - stock_width: integer, the width of the stock sheet.\n - piece_types: list of dictionaries. Each dictionary has the keys:\n \'length\' : int, the length of the piece.\n \'width\' : int, the width of the piece.\n \'max\' : int, maximum number of pieces allowed.\n \'value\' : int, value of the piece.\n Returns:\n A dictionary with the following keys:\n - total_value: int, the computed total value (must equal the sum of the piece values in placements).\n - placements: list of placements, where each placement is a tuple of 6 integers:\n (piece_type_index, x, y, placed_length, placed_width, orientation_flag)\n The orientation_flag is always 0 since rotation is not allowed.\n """\n # Your optimization/placement algorithm should go here.\n # For now, this is a placeholder that meets the output format requirements.\n\n # Example placeholder output (no actual pieces placed):\n return {"total_value": 0, "placements": []}' -EVAL_CLASS_NAME = 'CGCEvaluationCB' -EVAL_KWARGS = {'timeout_seconds': 20} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_constrained_guillotine_cutting/paras.yaml b/examples/benchmark_tasks/optimization_constrained_guillotine_cutting/paras.yaml deleted file mode 100644 index b4575223..00000000 --- a/examples/benchmark_tasks/optimization_constrained_guillotine_cutting/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: CGCEvaluationCB -timeout_seconds: 20 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_constrained_non_guillotine_cutting/__init__.py b/examples/benchmark_tasks/optimization_constrained_non_guillotine_cutting/__init__.py deleted file mode 100644 index e8c8341c..00000000 --- a/examples/benchmark_tasks/optimization_constrained_non_guillotine_cutting/__init__.py +++ /dev/null @@ -1,359 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_constrained_non_guillotine_cutting -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# References: -# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language -# model agents in algorithm search for combinatorial optimization. -# arXiv preprint arXiv:2504.04310 (2025). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -from typing import Any -import numpy as np -from llm4ad_loader import Evaluation -from llm4ad_loader import load_subdir_as_text -# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader -# from llm4ad.task.optimization.co_bench.constrained_non_guillotine_cutting_co_bench.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(stock_length: int, stock_width: int, pieces: list) -> dict:\n """\n Solves the constrained non-guillotine cutting problem.\n Input kwargs:\n - stock_length (int): Length of the stock rectangle.\n - stock_width (int): Width of the stock rectangle.\n - pieces (list of dict): List of pieces, where each dict has:\n \'length\' (int), \'width\' (int),\n \'min\' (int): minimum number required,\n \'max\' (int): maximum allowed,\n \'value\' (int): value of the piece.\n Evaluation Metric:\n The solution is scored as the sum of the values of all placed pieces,\n provided that every placement is valid (i.e., pieces lie within bounds,\n do not overlap, and the count for each type meets the specified [min, max] range).\n If any constraint is violated, the solution receives no score.\n Returns:\n A dictionary with one key:\n \'placements\': a list of placements, where each placement is a 4-tuple:\n (piece_type, x, y, r)\n - piece_type: 1-indexed index of the piece type.\n - x, y: integer coordinates for the placement (bottom-left corner).\n - r: rotation flag (0 for no rotation, 1 for 90° rotation).\n """\n # Placeholder implementation.\n # (A valid implementation would generate placements meeting all constraints.)\n return {\'placements\': []}' -task_description = '("The constrained non-guillotine cutting problem involves optimally arranging rectangular pieces "' - - -__all__ = ['CNCEvaluationCB'] - - -class CNCEvaluationCB(Evaluation): - - def __init__(self, - timeout_seconds=50, - **kwargs): - - """ - Args: - None - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - # Load datasets from Hugging Face - dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Constrained non-guillotine cutting") - self._datasets = {} - for filename in dataset: - # Join all text rows into a single string - text_content = '\n'.join([row['text'] for row in dataset[filename]]) - self._datasets[filename] = text_content - - def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: - return self.evaluate(callable_func) - - def evaluate(self, eva: callable) -> float | None: - ins_cases = [] - for case_id, ins in enumerate(self._datasets.values()): - ins_cases.append(self.load_data(ins)) - - fitness_list = [] - try: - for i in ins_cases: - for j in i: - result = eva(j['stock_length'], j['stock_width'], j['pieces']) - fitness = self.eval_func(j['stock_length'], j['stock_width'], j['pieces'], result['placements']) - fitness_list.append(fitness) - - return np.mean(fitness_list) # itself is a maximize problem - - except ValueError as e: - print(e) - return None - - def load_data(self, input_string): - """ - Loads input data from a text file and returns a list of test case dictionaries. - The input format: - - First line: integer T (number of test cases) - - For each test case: - * A line with integer m (number of pieces) - * A line with two integers: stock_length and stock_width - * m subsequent lines, each with 5 integers: - length, width, min_required, max_allowed, value - Returns: - List[Dict]: A list where each element is a dictionary with keys: - 'stock_length': int, - 'stock_width': int, - 'pieces': list of dicts, each dict has: - 'length': int, - 'width': int, - 'min': int, - 'max': int, - 'value': int - """ - test_cases = [] - all_lines = [line.strip() for line in input_string.split('\n')] - - idx = 0 - T = int(all_lines[idx]) - idx += 1 - for _ in range(T): - if idx >= len(all_lines): - raise ValueError("Insufficient data for the expected number of test cases.") - m = int(all_lines[idx]) - idx += 1 - - stock_dims = list(map(int, all_lines[idx].split())) - if len(stock_dims) != 2: - raise ValueError("Invalid stock dimensions format.") - stock_length, stock_width = stock_dims - idx += 1 - - pieces = [] - for _ in range(m): - piece_data = list(map(int, all_lines[idx].split())) - if len(piece_data) != 5: - raise ValueError("Invalid piece data format.") - pieces.append({ - 'length': piece_data[0], - 'width': piece_data[1], - 'min': piece_data[2], - 'max': piece_data[3], - 'value': piece_data[4] - }) - idx += 1 - - test_cases.append({ - 'stock_length': stock_length, - 'stock_width': stock_width, - 'pieces': pieces - }) - - return test_cases - - def eval_func(self, stock_length, stock_width, pieces, placements): - """ - Evaluates the solution for a single test case. - Parameters: - - stock_length (int): Length of the stock rectangle. - - stock_width (int): Width of the stock rectangle. - - pieces (list of dict): List of piece definitions. - - placements (list): List of placements; each placement is a 4-tuple: - (piece_type, x, y, r) - Returns: - float: The overall score, computed as the sum of values of all placed pieces, - if the solution is feasible. - Raises: - ValueError: If any constraint is violated. - """ - counts = [0] * len(pieces) - rects = [] # Each rectangle is represented as (x1, y1, x2, y2) - - for idx, placement in enumerate(placements): - if not (isinstance(placement, (list, tuple)) and len(placement) == 4): - raise ValueError(f"Placement at index {idx} is invalid; must be a 4-tuple.") - - piece_type, x, y, r = placement - - # Ensure that placement values are integers. - if not all(isinstance(val, int) for val in (piece_type, x, y, r)): - raise ValueError(f"All values in placement at index {idx} must be integers.") - - # Check piece_type validity. - if piece_type < 1 or piece_type > len(pieces): - raise ValueError(f"Placement at index {idx} has an invalid piece_type {piece_type}.") - - piece = pieces[piece_type - 1] - - # Determine dimensions based on rotation flag. - if r == 0: - p_len = piece['length'] - p_wid = piece['width'] - elif r == 1: - p_len = piece['width'] - p_wid = piece['length'] - else: - raise ValueError(f"Placement at index {idx} has an invalid rotation flag {r}.") - - # Check that the piece is fully within the stock boundaries. - if x < 0 or y < 0 or (x + p_len) > stock_length or (y + p_wid) > stock_width: - raise ValueError(f"Placement at index {idx} is out of stock boundaries.") - - # Record the rectangle: (x1, y1, x2, y2) - rects.append((x, y, x + p_len, y + p_wid)) - counts[piece_type - 1] += 1 - - # Check for overlapping placements. - n = len(rects) - for i in range(n): - for j in range(i + 1, n): - a = rects[i] - b = rects[j] - # Two rectangles do not overlap if one is completely to the left, - # right, above, or below the other. - if not (a[2] <= b[0] or b[2] <= a[0] or a[3] <= b[1] or b[3] <= a[1]): - raise ValueError(f"Placements at indices {i} and {j} overlap.") - - # Check that the count of placements for each piece type meets its constraints. - for i, piece in enumerate(pieces): - if counts[i] < piece['min'] or counts[i] > piece['max']: - raise ValueError(f"Piece type {i + 1} count {counts[i]} does not meet constraints " - f"[min: {piece['min']}, max: {piece['max']}].") - - # Compute the total score. - total_score = 0 - for placement in placements: - piece_type, x, y, r = placement - piece = pieces[piece_type - 1] - total_score += piece['value'] - - return total_score - - def norm_score(self, results): - optimal_scores = { - "ngcutap.txt": [164, 230, 247, 268, 358, 289, 430, 834, 924, 1452, 1688, 1865, 1178, 1270, 2726, 1860, - 27718, - 22502, 24019, 32893, 27923], - "ngcutcon.txt": [164, 230, 247, 268, 358, 289, 430, 834, 924, 1452, 1688, 1865, 1178, 1270, 2726, 1860, - 27718, - 22502, 24019, 32893, 27923], - "ngcutfs1.txt": [30000] * 210, - "ngcutfs2.txt": [30000] * 210, - "ngcutfs3.txt": [30000] * 210, - } - - normed = {} - for case, (scores, error_message) in results.items(): - if case not in optimal_scores: - continue # Skip if there's no optimal score defined. - optimal_list = optimal_scores[case] - normed_scores = [] - # Compute normalized score for each index. - for idx, score in enumerate(scores): - if isinstance(score, (int, float)): - normed_scores.append(score / optimal_list[idx]) - else: - normed_scores.append(score) - normed[case] = (normed_scores, error_message) - - return normed - - def get_dev(self): - dev = {'ngcutap.txt': [19, 4, 12, 2, 8], 'ngcutcon.txt': [0, 8, 19, 7, 17], - 'ngcutfs1.txt': [51, 66, 120, 62, 8, 185, 197, 0, 170, 119, 103, 161, 173, 26, 153, 96, 13, 136, 5, 44, - 150, - 82, 86, 14, 71, 207, 135, 75, 97, 139, 118, 46, 108, 93, 99, 140, 204, 147, 16, 183, 27, - 191, 176, 49, 127, 78, 10, 113, 110, 143, 199, 142, 167, 22, 50, 30, 180, 188, 154, 123, - 63, - 72, 203, 61, 28, 186, 159, 134, 19, 52, 39, 79, 98, 55, 56, 137, 148, 155, 163, 124, - 174, - 33, 1, 125, 77, 58, 151, 76, 116, 206, 156, 184, 12, 32, 53, 92, 164, 131, 175, 187, - 157, - 45, 201, 189, 54], - 'ngcutfs2.txt': [123, 108, 114, 43, 151, 116, 197, 23, 45, 166, 8, 126, 147, 87, 154, 12, 172, 103, 133, - 143, - 122, 68, 24, 97, 144, 179, 195, 52, 67, 1, 14, 167, 33, 65, 196, 46, 202, 206, 54, 63, - 160, - 159, 176, 79, 129, 61, 9, 164, 72, 115, 21, 111, 96, 66, 198, 104, 201, 92, 105, 125, - 91, - 119, 124, 94, 84, 20, 113, 203, 177, 15, 135, 120, 49, 194, 192, 98, 88, 158, 36, 171, - 29, - 199, 109, 185, 148, 130, 204, 70, 174, 207, 53, 142, 2, 89, 35, 51, 117, 145, 73, 10, - 81, - 83, 139, 4, 128], - 'ngcutfs3.txt': [193, 73, 128, 170, 197, 26, 85, 58, 105, 100, 36, 93, 32, 72, 110, 80, 16, 106, 160, 11, - 129, 3, 89, 66, 87, 61, 27, 47, 171, 52, 176, 24, 203, 205, 186, 161, 135, 114, 200, 90, - 124, 198, 141, 70, 14, 183, 81, 8, 86, 178, 54, 157, 25, 208, 38, 134, 39, 88, 111, 23, - 190, - 109, 152, 43, 98, 99, 163, 148, 201, 44, 192, 130, 30, 138, 33, 9, 209, 194, 4, 15, 37, - 169, - 188, 112, 123, 115, 173, 181, 108, 97, 133, 96, 53, 13, 48, 158, 71, 19, 149, 64, 74, - 103, - 102, 206, 143]} - - return dev - - -# Task configuration for benchmark task -ENTRY_NAME = 'solve' -FUNCTION_SIGNATURE = 'def solve(...):' -IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' -TASK_DESCRIPTION = '("The constrained non-guillotine cutting problem involves optimally arranging rectangular pieces "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The constrained non-guillotine cutting problem involves optimally arranging rectangular pieces "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(stock_length: int, stock_width: int, pieces: list) -> dict:\n """\n Solves the constrained non-guillotine cutting problem.\n Input kwargs:\n - stock_length (int): Length of the stock rectangle.\n - stock_width (int): Width of the stock rectangle.\n - pieces (list of dict): List of pieces, where each dict has:\n \'length\' (int), \'width\' (int),\n \'min\' (int): minimum number required,\n \'max\' (int): maximum allowed,\n \'value\' (int): value of the piece.\n Evaluation Metric:\n The solution is scored as the sum of the values of all placed pieces,\n provided that every placement is valid (i.e., pieces lie within bounds,\n do not overlap, and the count for each type meets the specified [min, max] range).\n If any constraint is violated, the solution receives no score.\n Returns:\n A dictionary with one key:\n \'placements\': a list of placements, where each placement is a 4-tuple:\n (piece_type, x, y, r)\n - piece_type: 1-indexed index of the piece type.\n - x, y: integer coordinates for the placement (bottom-left corner).\n - r: rotation flag (0 for no rotation, 1 for 90° rotation).\n """\n # Placeholder implementation.\n # (A valid implementation would generate placements meeting all constraints.)\n return {\'placements\': []}' -EVAL_CLASS_NAME = 'CNCEvaluationCB' -EVAL_KWARGS = {'timeout_seconds': 20} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_constrained_non_guillotine_cutting/paras.yaml b/examples/benchmark_tasks/optimization_constrained_non_guillotine_cutting/paras.yaml deleted file mode 100644 index 58248382..00000000 --- a/examples/benchmark_tasks/optimization_constrained_non_guillotine_cutting/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: CNCEvaluationCB -timeout_seconds: 20 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_container_loading/__init__.py b/examples/benchmark_tasks/optimization_container_loading/__init__.py deleted file mode 100644 index 982c2b11..00000000 --- a/examples/benchmark_tasks/optimization_container_loading/__init__.py +++ /dev/null @@ -1,387 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_container_loading -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# References: -# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language -# model agents in algorithm search for combinatorial optimization. -# arXiv preprint arXiv:2504.04310 (2025). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -from typing import Any -import numpy as np -from llm4ad_loader import Evaluation -from llm4ad_loader import load_subdir_as_text -# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader -# from llm4ad.task.optimization.co_bench.container_loading_co_bench.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(problem_index: int, container: tuple, box_types: dict) -> dict:\n """\n Solves a container loading problem.\n Input kwargs:\n - problem_index: an integer identifier for the test case.\n - container: a tuple of three integers (container_length, container_width, container_height).\n - box_types: a dictionary mapping each box type (integer) to a dict with:\n \'dims\': a list of three integers [d1, d2, d3],\n \'flags\': a list of three binary integers [f1, f2, f3] indicating if that dimension can be vertical,\n \'count\': an integer number of available boxes of that type.\n Evaluation Metric:\n The solution is evaluated by computing the volume utilization ratio, which is the sum of the volumes\n of all placed boxes divided by the container volume. Placements must be valid (i.e. respect orientation,\n remain within the container, and not overlap). If any placement is invalid, the score is 0.0.\n Return:\n A dictionary with key \'placements\', whose value is a list of placement dictionaries.\n Each placement dictionary must contain 7 integers with the following keys/values:\n box_type, container_id, x, y, z, v, hswap\n where \'v\' is the index (0, 1, or 2) for the vertical dimension and \'hswap\' is a binary flag (0 or 1)\n indicating whether the horizontal dimensions are swapped.\n """\n # Placeholder implementation.\n return {\'placements\': []}' -task_description = '("Solves a container loading problem: Given a 3D container of specified dimensions and multiple "' - - -__all__ = ['CLEvaluationCB'] - - -class CLEvaluationCB(Evaluation): - - def __init__(self, - timeout_seconds=50, - **kwargs): - - """ - Args: - None - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - # Load datasets from Hugging Face - dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Container loading") - self._datasets = {} - for filename in dataset: - # Join all text rows into a single string - text_content = '\n'.join([row['text'] for row in dataset[filename]]) - self._datasets[filename] = text_content - - def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: - return self.evaluate(callable_func) - - def evaluate(self, eva: callable) -> float | None: - ins_cases = [] - for case_id, ins in enumerate(self._datasets.values()): - ins_cases.append(self.load_data(ins)) - - fitness_list = [] - try: - for i in ins_cases: - for j in i: - result = eva(j['problem_index'], j['container'], j['box_types']) - fitness = self.eval_func(j['problem_index'], j['container'], j['box_types'], result['placements']) - fitness_list.append(fitness) - - return np.mean(fitness_list) # itself is a maximize problem - - except ValueError as e: - print(e) - return None - - def load_data(self, input_string): - """ - Loads container loading problem data from string content. - The input format: - 1. The first line is an integer P, the number of test problems. - 2. For each test problem: - a. A header line with two integers: problem_index and a seed (the seed may be ignored). - (Note: Some files may only provide one number; the seed is optional.) - b. A line with three integers: container_length, container_width, container_height. - c. A line with a single integer n: the number of box types. - d. Then n lines follow, each with 7 or 8 integers in this order: - box_type, d1, f1, d2, f2, d3, f3 [, count] - If only 7 numbers are provided, a default count of 1 is assumed. - Returns: - A list of dictionaries, one per test case. Each dictionary has the following keys: - - 'problem_index': int, - - 'container': tuple (container_length, container_width, container_height), - - 'box_types': dict mapping each box_type to a dict with keys: - 'dims': [d1, d2, d3], - 'flags': [f1, f2, f3], - 'count': count - """ - test_cases = [] - lines = [line.strip() for line in input_string.split('\n') if line.strip()] - if not lines: - raise ValueError("Empty input file") - - try: - P = int(lines[0]) - except Exception as e: - raise ValueError("First line must be an integer representing the number of test cases.") from e - idx = 1 - for case_num in range(P): - # Read header: expecting at least one number (problem_index); seed is optional. - header_parts = lines[idx].split() - if len(header_parts) < 1: - raise ValueError(f"Test case {case_num + 1}: Header line missing problem index.") - problem_index = int(header_parts[0]) - idx += 1 - - # Container dimensions: length, width, height. - cont_parts = lines[idx].split() - if len(cont_parts) < 3: - raise ValueError(f"Test case {problem_index}: Container dimensions missing or incomplete.") - container = tuple(map(int, cont_parts[:3])) - idx += 1 - - # Number of box types. - if idx >= len(lines): - raise ValueError(f"Test case {problem_index}: Expected number of box types but reached end of file.") - try: - n = int(lines[idx]) - except Exception as e: - raise ValueError(f"Test case {problem_index}: Box types count is not an integer.") from e - idx += 1 - - box_types = {} - for bt_index in range(n): - if idx >= len(lines): - raise ValueError( - f"Test case {problem_index}: Missing box type specification at index {bt_index + 1}.") - parts = lines[idx].split() - if len(parts) < 7: - raise ValueError( - f"Test case {problem_index}: Box type specification incomplete on line: '{lines[idx]}'") - try: - bt = int(parts[0]) - d1 = int(parts[1]) - f1 = int(parts[2]) - d2 = int(parts[3]) - f2 = int(parts[4]) - d3 = int(parts[5]) - f3 = int(parts[6]) - # If a count is provided, use it; otherwise default to 1. - count = int(parts[7]) if len(parts) >= 8 else 1 - except Exception as e: - raise ValueError( - f"Test case {problem_index}: Error parsing box type specification: '{lines[idx]}'") from e - dims = [d1, d2, d3] - flags = [f1, f2, f3] - box_types[bt] = {'dims': dims, 'flags': flags, 'count': count} - idx += 1 - - test_cases.append({ - 'problem_index': problem_index, - 'container': container, - 'box_types': box_types - }) - return test_cases - - def eval_func(self, problem_index, container, box_types, placements, **kwargs): - """ - Evaluates a container loading solution for a single test case. - Parameters: - - problem_index: the integer identifier of the test case. - - container: a tuple (container_length, container_width, container_height). - - box_types: a dictionary mapping box types to their specifications. - - placements: a list of placement dictionaries; each must include: - 'box_type', 'container_id', 'x', 'y', 'z', 'v', 'hswap' - Returns: - A scalar float value representing the volume utilization ratio if the solution is valid. - If any placement is invalid (e.g., incorrect orientation, out-of-bound placement, - overlapping boxes, or exceeding available count), the function returns 0.0. - Evaluation Details: - - For each placement, verifies that the chosen vertical dimension (v) is allowed. - - Computes the oriented dimensions: - horizontal dimensions are the two not chosen as vertical (swapped if hswap == 1), - vertical dimension is dims[v]. - - Checks that each box is entirely within the container. - - Checks that boxes do not overlap (touching is allowed). - - Verifies that the number of placed boxes for each type does not exceed the available count. - - The score is computed as (total placed volume) / (container volume). - """ - - def boxes_overlap(pos1, dims1, pos2, dims2): - x1, y1, z1 = pos1 - w1, d1, h1 = dims1 - x2, y2, z2 = pos2 - w2, d2, h2 = dims2 - if x1 + w1 <= x2 or x2 + w2 <= x1: - return False - if y1 + d1 <= y2 or y2 + d2 <= y1: - return False - if z1 + h1 <= z2 or z2 + h2 <= z1: - return False - return True - - cont_len, cont_wid, cont_ht = container - container_volume = cont_len * cont_wid * cont_ht - total_placed_volume = 0 - used_counts = {} - placements_by_container = {} - - # Group placements by container_id - for pmt in placements: - cid = pmt['container_id'] - if cid not in placements_by_container: - placements_by_container[cid] = [] - placements_by_container[cid].append(pmt) - - # Validate each placement - for cid, plist in placements_by_container.items(): - for pmt in plist: - bt = pmt['box_type'] - if bt not in box_types: - return 0.0 # Unknown box type - info = box_types[bt] - dims = info['dims'] - flags = info['flags'] - v = pmt['v'] - if v not in [0, 1, 2]: - return 0.0 - if flags[v] != 1: - return 0.0 # Vertical orientation not allowed - - # Determine horizontal dimensions indices - horz_idx = [i for i in [0, 1, 2] if i != v] - h1 = dims[horz_idx[0]] - h2 = dims[horz_idx[1]] - if pmt['hswap'] == 1: - h1, h2 = h2, h1 - vert = dims[v] - - # Check that placement coordinates are nonnegative and within container bounds - if pmt['x'] < 0 or pmt['y'] < 0 or pmt['z'] < 0: - return 0.0 - if (pmt['x'] + h1 > cont_len or - pmt['y'] + h2 > cont_wid or - pmt['z'] + vert > cont_ht): - return 0.0 - - # Save oriented dimensions and position for overlap checking - pmt['oriented_dims'] = (h1, h2, vert) - pmt['oriented_pos'] = (pmt['x'], pmt['y'], pmt['z']) - total_placed_volume += h1 * h2 * vert - used_counts[bt] = used_counts.get(bt, 0) + 1 - - # Check for overlaps among placements in the same container - for i in range(len(plist)): - for j in range(i + 1, len(plist)): - if boxes_overlap(plist[i]['oriented_pos'], plist[i]['oriented_dims'], - plist[j]['oriented_pos'], plist[j]['oriented_dims']): - return 0.0 - - # Verify that box usage does not exceed available counts - for bt, cnt in used_counts.items(): - if cnt > box_types[bt]['count']: - return 0.0 - - utilization = total_placed_volume / container_volume if container_volume > 0 else 0.0 - return utilization - - def get_dev(self): - dev = { - 'thpack1.txt': [89, 15, 12, 53, 78, 32, 56, 30, 6, 28, 23, 62, 52, 37, 69, 33, 35, 24, 17, 4, 79, 72, 2, 92, - 54, - 90, 91, 1, 57, 59, 94, 65, 25, 14, 83, 47, 46, 95, 48, 42, 88, 68, 85, 55, 40, 64, 74, 70, - 3, - 7], - 'thpack2.txt': [6, 9, 72, 24, 69, 2, 81, 33, 53, 39, 64, 71, 15, 99, 61, 36, 52, 8, 19, 7, 4, 1, 86, 21, 31, - 5, - 20, 57, 0, 79, 55, 35, 23, 25, 89, 44, 91, 62, 82, 12, 68, 75, 73, 27, 80, 56, 30, 47, 70, - 16], - 'thpack3.txt': [17, 36, 89, 50, 19, 11, 97, 9, 75, 62, 10, 46, 42, 23, 39, 18, 99, 1, 5, 20, 70, 60, 31, 3, - 43, - 33, 51, 92, 95, 40, 84, 63, 13, 78, 58, 25, 4, 38, 24, 15, 88, 82, 7, 28, 8, 77, 71, 80, 76, - 53], - 'thpack4.txt': [7, 89, 96, 75, 2, 37, 6, 82, 18, 14, 90, 36, 32, 40, 10, 25, 56, 72, 87, 98, 45, 21, 23, 55, - 4, - 79, 15, 65, 63, 73, 5, 81, 76, 69, 20, 67, 85, 60, 50, 47, 84, 16, 35, 1, 22, 43, 91, 48, - 88, - 41], - 'thpack5.txt': [79, 36, 97, 5, 62, 10, 49, 2, 23, 52, 51, 29, 96, 20, 64, 41, 38, 35, 94, 95, 12, 73, 34, - 11, - 93, 69, 58, 61, 87, 80, 71, 4, 88, 57, 46, 59, 33, 50, 13, 44, 0, 85, 55, 21, 77, 82, 63, - 67, - 31, 26], - 'thpack6.txt': [21, 31, 83, 22, 10, 19, 5, 0, 43, 82, 66, 36, 49, 38, 33, 58, 70, 15, 97, 80, 9, 30, 42, 88, - 69, - 61, 40, 60, 14, 95, 91, 39, 98, 16, 73, 90, 51, 18, 71, 26, 47, 54, 57, 87, 17, 53, 89, 92, - 65, - 81], - 'thpack7.txt': [97, 37, 73, 88, 50, 79, 12, 60, 99, 34, 4, 19, 78, 9, 7, 93, 31, 74, 90, 38, 33, 21, 24, 22, - 52, - 0, 43, 67, 13, 3, 59, 42, 39, 47, 36, 40, 45, 10, 5, 56, 57, 18, 51, 61, 92, 20, 69, 81, 35, - 98], - 'thpack8.txt': [11, 4, 12, 14, 10, 2, 7], - 'thpack9.txt': [14, 32, 25, 30, 40, 8, 37, 15, 31, 9, 17, 21, 22, 16, 24, 33, 35, 44, 42, 0, 1, 45, 11]} - - return dev - - -# Task configuration for benchmark task -ENTRY_NAME = 'solve' -FUNCTION_SIGNATURE = 'def solve(...):' -IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' -TASK_DESCRIPTION = '("Solves a container loading problem: Given a 3D container of specified dimensions and multiple "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("Solves a container loading problem: Given a 3D container of specified dimensions and multiple "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(problem_index: int, container: tuple, box_types: dict) -> dict:\n """\n Solves a container loading problem.\n Input kwargs:\n - problem_index: an integer identifier for the test case.\n - container: a tuple of three integers (container_length, container_width, container_height).\n - box_types: a dictionary mapping each box type (integer) to a dict with:\n \'dims\': a list of three integers [d1, d2, d3],\n \'flags\': a list of three binary integers [f1, f2, f3] indicating if that dimension can be vertical,\n \'count\': an integer number of available boxes of that type.\n Evaluation Metric:\n The solution is evaluated by computing the volume utilization ratio, which is the sum of the volumes\n of all placed boxes divided by the container volume. Placements must be valid (i.e. respect orientation,\n remain within the container, and not overlap). If any placement is invalid, the score is 0.0.\n Return:\n A dictionary with key \'placements\', whose value is a list of placement dictionaries.\n Each placement dictionary must contain 7 integers with the following keys/values:\n box_type, container_id, x, y, z, v, hswap\n where \'v\' is the index (0, 1, or 2) for the vertical dimension and \'hswap\' is a binary flag (0 or 1)\n indicating whether the horizontal dimensions are swapped.\n """\n # Placeholder implementation.\n return {\'placements\': []}' -EVAL_CLASS_NAME = 'CLEvaluationCB' -EVAL_KWARGS = {'timeout_seconds': 20} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_container_loading/paras.yaml b/examples/benchmark_tasks/optimization_container_loading/paras.yaml deleted file mode 100644 index 6b88d118..00000000 --- a/examples/benchmark_tasks/optimization_container_loading/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: CLEvaluationCB -timeout_seconds: 20 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_container_loading_with_weight_restrictions/__init__.py b/examples/benchmark_tasks/optimization_container_loading_with_weight_restrictions/__init__.py deleted file mode 100644 index 59c13190..00000000 --- a/examples/benchmark_tasks/optimization_container_loading_with_weight_restrictions/__init__.py +++ /dev/null @@ -1,456 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_container_loading_with_weight_restrictions -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# References: -# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language -# model agents in algorithm search for combinatorial optimization. -# arXiv preprint arXiv:2504.04310 (2025). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -from typing import Any -import numpy as np -from llm4ad_loader import Evaluation -from llm4ad_loader import load_subdir_as_text -# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader -# from llm4ad.task.optimization.co_bench.container_loading_with_weight_restrictions_co_bench.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(container: tuple, n: int, cargo_vol: float, box_types: list) -> dict:\n """\n Solves the Container Loading with Weight Restrictions problem.\n Input kwargs (for one test case):\n - container (tuple of int): (L, W, H) representing the container dimensions in cm.\n - n (int): the number of box types.\n - cargo_vol (float): the total cargo volume in m³ (provided for consistency).\n - box_types (list of dict): one per box type. Each dictionary has the keys:\n \'length\' (int), \'length_flag\' (int),\n \'width\' (int), \'width_flag\' (int),\n \'height\' (int), \'height_flag\' (int),\n \'count\' (int), \'weight\' (float),\n \'lb1\' (float), \'lb2\' (float), \'lb3\' (float).\n The problem is to select and place boxes (each possibly in one of three allowed orientations)\n inside the container so as to maximize the ratio of the total volume of placed boxes (each based on its original dimensions)\n to the container’s volume, while obeying placement, support, and load–bearing constraints.\n Evaluation metric:\n The score is the container volume utilization (i.e. total placed boxes volume divided by container volume)\n if the solution is valid according to all constraints; otherwise the score is 0.0.\n Placeholder implementation: No boxes are placed.\n Returns a dictionary with keys:\n - \'instance\': instance number (int),\n - \'util\': achieved utilization (float),\n - \'m\': number of placements (int),\n - \'placements\': a list of placements; each placement is a dict with keys:\n \'box_type\' (int, 1-indexed), \'orientation\' (int: 1, 2, or 3),\n \'x\', \'y\', \'z\' (floats for the lower–left–front corner in cm).\n """\n # Placeholder: return an empty solution.\n return {\n \'instance\': 1,\n \'util\': 0.0,\n \'m\': 0,\n \'placements\': []\n }' -task_description = '("The Container Loading with Weight Restrictions problem aims to maximize the utilization of a "' - - -__all__ = ['CLWREvaluationCB'] -TOL = 1e-6 - - -class CLWREvaluationCB(Evaluation): - - def __init__(self, - timeout_seconds=50, - **kwargs): - - """ - Args: - None - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - # Load datasets from Hugging Face - dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Container loading with weight restrictions") - self._datasets = {} - for filename in dataset: - # Join all text rows into a single string - text_content = '\n'.join([row['text'] for row in dataset[filename]]) - self._datasets[filename] = text_content - - def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: - return self.evaluate(callable_func) - - def evaluate(self, eva: callable) -> float | None: - ins_cases = [] - for case_id, ins in enumerate(self._datasets.values()): - ins_cases.append(self.load_data(ins)) - - fitness_list = [] - try: - for i in ins_cases: - for j in i: - result = eva(j['container'], j['n'], j['cargo_vol'], j['box_types']) - fitness = self.eval_func(j['container'], j['n'], j['cargo_vol'], j['box_types'], result['instance'], result['util'], result['m'], result['placements']) - fitness_list.append(fitness) - - return np.mean(fitness_list) # itself is a maximize problem - - except ValueError as e: - print(e) - return None - - def load_data(self, input_string): - """ - Loads the input data file for the Container Loading problem. - The input file may contain one or more test cases. For each test case: - - The first non-empty line contains three floats: container length, width, height (in cm). - - The next non-empty line contains an integer n (number of box types) and a float (total cargo volume in m³). - - The following n non-empty lines each contain 11 whitespace-separated values: - Box length, length_flag, Box width, width_flag, Box height, height_flag, - count, weight, lb1, lb2, lb3. - Returns: - A list where each element is a dictionary containing the input data for one test case with keys: - 'container', 'n', 'cargo_vol', and 'box_types'. - """ - all_lines = [line.strip() for line in input_string.split('\n')] - - cases = [] - i = 0 - while i < len(all_lines): - # Read container dimensions. - parts = all_lines[i].split() - if len(parts) < 3: - raise ValueError("Invalid container dimensions line.") - container = (int(parts[0]), int(parts[1]), int(parts[2])) - i += 1 - - # Read header: number of box types and cargo volume. - parts = all_lines[i].split() - if len(parts) < 2: - raise ValueError("Invalid test-case header line.") - n = int(parts[0]) - cargo_vol = float(parts[1]) - i += 1 - - # Read details for each box type. - box_types = [] - for _ in range(n): - parts = all_lines[i].split() - if len(parts) != 11: - raise ValueError("Invalid box type line: " + all_lines[i]) - box_type = { - 'length': int(parts[0]), - 'length_flag': int(parts[1]), - 'width': int(parts[2]), - 'width_flag': int(parts[3]), - 'height': int(parts[4]), - 'height_flag': int(parts[5]), - 'count': int(parts[6]), - 'weight': float(parts[7]), - 'lb1': float(parts[8]), - 'lb2': float(parts[9]), - 'lb3': float(parts[10]) - } - box_types.append(box_type) - i += 1 - - cases.append({ - 'container': container, - 'n': n, - 'cargo_vol': cargo_vol, - 'box_types': box_types - }) - - return cases - - # Helper functions used by eval_func - - def get_box_dimensions(self, box, orientation): - """ - Given a box type (dictionary) and an orientation (1, 2, or 3), - returns a tuple (dx, dy, dz, lb, volume) where: - - (dx, dy) are the horizontal dimensions, - - dz is the vertical dimension, - - lb is the load-bearing ability for that orientation, - - volume is the original box volume. - Orientation conventions: - 1: Box length is vertical (dz = length; horizontal: width, height). - 2: Box width is vertical (dz = width; horizontal: length, height). - 3: Box height is vertical (dz = height; horizontal: length, width). - """ - if orientation == 1: - if box['length_flag'] != 1: - raise ValueError("Orientation 1 not allowed for this box type.") - dz = box['length'] - dx = box['width'] - dy = box['height'] - lb = box['lb1'] - elif orientation == 2: - if box['width_flag'] != 1: - raise ValueError("Orientation 2 not allowed for this box type.") - dz = box['width'] - dx = box['length'] - dy = box['height'] - lb = box['lb2'] - elif orientation == 3: - if box['height_flag'] != 1: - raise ValueError("Orientation 3 not allowed for this box type.") - dz = box['height'] - dx = box['length'] - dy = box['width'] - lb = box['lb3'] - else: - raise ValueError("Invalid orientation value.") - volume = box['length'] * box['width'] * box['height'] - return dx, dy, dz, lb, volume - - def boxes_overlap(self, b1, b2): - """ - Determines if two boxes overlap in space. - Each box is represented as a dict with keys: - x, y, z, dx, dy, dz. - Returns True if the boxes overlap (i.e. intersect in all three dimensions, not just touch). - """ - if b1['x'] + b1['dx'] - TOL <= b2['x'] or b2['x'] + b2['dx'] - TOL <= b1['x']: - return False - if b1['y'] + b1['dy'] - TOL <= b2['y'] or b2['y'] + b2['dy'] - TOL <= b1['y']: - return False - if b1['z'] + b1['dz'] - TOL <= b2['z'] or b2['z'] + b2['dz'] - TOL <= b1['z']: - return False - return True - - def eval_func(self, container, n, cargo_vol, box_types, instance, util, m, placements): - """ - Hard evaluation for a container–loading solution. - This function checks all constraints and raises an error immediately when any - constraint is violated. The constraints include: - - Validity of the box type index. - - Box orientation (via get_box_dimensions). - - Box placement completely within container boundaries. - - Not exceeding the available counts for each box type. - - Proper support: every box not on the floor must be fully and uniquely supported. - - Overlap: boxes may only overlap if one is exactly supporting the other. - - Load-bearing capacity: the weight on each box must not exceed its capacity. - If all constraints are met, the function returns the container volume utilization, - i.e., (total placed box volume) / (container volume). - Inputs: - - container: tuple (L, W, H) in cm. - - n: number of box types. - - cargo_vol: total cargo volume (m³) (not used in evaluation). - - box_types: list of box type dictionaries. - - instance: instance number (int) (not used in evaluation). - - util: reported utilization (float) (ignored here). - - m: number of placements. - - placements: list of placements; each placement is a dict with keys: - 'box_type' (int, 1-indexed), - 'orientation' (int), - 'x', 'y', 'z' (floats). - Returns: - A float representing the container utilization if all constraints are satisfied. - """ - TOL = 1e-6 - container_L, container_W, container_H = container - placed = [] - usage = [0] * len(box_types) - - # Process each placement: check box type, orientation, and container boundaries. - for idx, placement in enumerate(placements): - bt_index = placement['box_type'] - 1 - if bt_index < 0 or bt_index >= len(box_types): - raise ValueError(f"Invalid box type index in placement {idx}: {placement['box_type']}") - - usage[bt_index] += 1 - box = box_types[bt_index] - - try: - # get_box_dimensions should return (dx, dy, dz, load_bearing, volume) - dx, dy, dz, lb, volume = self.get_box_dimensions(box, placement['orientation']) - except Exception as e: - raise ValueError(f"Orientation error for placement {idx}: {e}") - - # Check that the box is completely inside the container. - if (placement['x'] < -TOL or placement['y'] < -TOL or placement['z'] < -TOL or - placement['x'] + dx > container_L + TOL or - placement['y'] + dy > container_W + TOL or - placement['z'] + dz > container_H + TOL): - raise ValueError(f"Box at placement {idx} is out-of-bound") - - placed.append({ - 'id': idx, - 'box_type': bt_index, - 'orientation': placement['orientation'], - 'x': placement['x'], - 'y': placement['y'], - 'z': placement['z'], - 'dx': dx, - 'dy': dy, - 'dz': dz, - 'lb': lb, - 'weight': box['weight'], - 'volume': volume - }) - - # Check that the usage does not exceed available counts. - for i, count in enumerate(usage): - if count > box_types[i]['count']: - raise ValueError( - f"Usage error: Box type {i + 1} used {count} times but only {box_types[i]['count']} available") - - # Determine support relationships. - support_of = {} # Maps a box's id to the id of its supporting box. - for b in placed: - # Boxes on the floor need no support. - if abs(b['z']) < TOL: - continue - - candidate = None - for other in placed: - if other['id'] == b['id']: - continue - # Check if other box's top face aligns with the bottom of b. - if abs(other['z'] + other['dz'] - b['z']) > TOL: - continue - # b's horizontal projection must be completely inside other's top face. - if b['x'] + TOL < other['x'] or (b['x'] + b['dx']) - TOL > other['x'] + other['dx']: - continue - if b['y'] + TOL < other['y'] or (b['y'] + b['dy']) - TOL > other['y'] + other['dy']: - continue - if candidate is not None: - raise ValueError(f"Ambiguous support for box id {b['id']} (placement {b['id']})") - candidate = other - if candidate is None: - raise ValueError(f"Missing support for box id {b['id']} (placement {b['id']})") - support_of[b['id']] = candidate['id'] - - # Check for improper overlaps. - # Overlap is allowed only if one box is exactly supporting the other. - for i in range(len(placed)): - for j in range(i + 1, len(placed)): - b1 = placed[i] - b2 = placed[j] - # Skip if boxes are in non-overlapping vertical positions. - if b1['z'] + b1['dz'] - TOL <= b2['z'] or b2['z'] + b2['dz'] - TOL <= b1['z']: - continue - if self.boxes_overlap(b1, b2): - if support_of.get(b1['id'], -1) != b2['id'] and support_of.get(b2['id'], -1) != b1['id']: - raise ValueError(f"Improper overlap between box id {b1['id']} and box id {b2['id']}") - - # Compute load on each box. - total_load = {b['id']: 0.0 for b in placed} - placed_sorted = sorted(placed, key=lambda b: b['z'], reverse=True) - for b in placed_sorted: - load_here = b['weight'] + total_load[b['id']] - if b['id'] in support_of: - sup_id = support_of[b['id']] - total_load[sup_id] += load_here - - # Verify load-bearing capacity for each box. - for b in placed: - capacity = b['dx'] * b['dy'] * b['lb'] - if total_load[b['id']] > capacity + TOL: - excess = total_load[b['id']] - capacity - raise ValueError(f"Load-bearing capacity exceeded for box id {b['id']}: overload {excess}") - - total_box_volume = sum(b['volume'] for b in placed) - container_volume = container_L * container_W * container_H - utilization = total_box_volume / container_volume if container_volume > 0 else 0.0 - - return utilization - - def get_dev(self): - dev = { - 'wtpack1.txt': [23, 24, 74, 19, 18, 98, 15, 80, 20, 44, 49, 95, 21, 64, 37, 46, 88, 29, 2, 41, 12, 56, 52, - 31, - 86, 92, 57, 33, 78, 26, 10, 38, 40, 32, 67, 89, 85, 7, 11, 53, 97, 22, 70, 82, 8, 48, 43, - 45, - 91, 71], - 'wtpack2.txt': [17, 7, 76, 44, 74, 95, 47, 53, 31, 55, 58, 50, 21, 41, 14, 98, 49, 67, 97, 88, 73, 87, 34, - 19, - 64, 90, 54, 82, 61, 93, 91, 75, 59, 5, 71, 8, 18, 72, 92, 85, 40, 32, 43, 42, 39, 30, 10, - 48, - 25, 15], - 'wtpack3.txt': [94, 25, 40, 83, 39, 80, 13, 64, 70, 21, 65, 4, 31, 54, 45, 58, 29, 33, 59, 42, 69, 92, 79, - 96, - 71, 43, 50, 19, 75, 89, 98, 97, 77, 72, 51, 2, 18, 93, 52, 88, 68, 56, 7, 26, 32, 46, 87, - 91, - 22, 49], - 'wtpack4.txt': [7, 78, 37, 44, 33, 10, 23, 14, 39, 6, 79, 36, 38, 25, 97, 88, 26, 54, 76, 51, 99, 62, 20, - 48, - 56, 32, 49, 2, 47, 95, 86, 22, 8, 53, 71, 85, 93, 92, 90, 0, 52, 91, 28, 84, 63, 31, 24, 11, - 15, - 80], - 'wtpack5.txt': [5, 56, 60, 51, 64, 17, 88, 3, 76, 37, 78, 70, 74, 30, 2, 57, 11, 34, 96, 16, 41, 4, 15, 7, - 42, - 65, 97, 80, 89, 69, 39, 25, 0, 32, 81, 95, 82, 19, 31, 8, 85, 94, 33, 14, 55, 93, 18, 83, - 61, - 87], - 'wtpack6.txt': [33, 3, 58, 46, 8, 35, 95, 64, 90, 60, 43, 11, 27, 99, 91, 30, 68, 70, 41, 96, 81, 47, 57, - 87, - 74, 42, 16, 66, 28, 98, 85, 4, 72, 88, 59, 75, 51, 82, 71, 14, 65, 10, 40, 0, 38, 83, 52, 7, - 86, - 89], - 'wtpack7.txt': [24, 94, 50, 40, 76, 58, 15, 36, 5, 1, 27, 8, 18, 87, 88, 92, 38, 54, 80, 41, 21, 46, 57, 59, - 91, - 51, 97, 95, 79, 4, 22, 85, 26, 53, 42, 64, 9, 83, 96, 29, 44, 89, 73, 77, 69, 72, 81, 61, - 93, - 2]} - - return dev - - - -# Task configuration for benchmark task -ENTRY_NAME = 'solve' -FUNCTION_SIGNATURE = 'def solve(...):' -IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' -TASK_DESCRIPTION = '("The Container Loading with Weight Restrictions problem aims to maximize the utilization of a "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The Container Loading with Weight Restrictions problem aims to maximize the utilization of a "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(container: tuple, n: int, cargo_vol: float, box_types: list) -> dict:\n """\n Solves the Container Loading with Weight Restrictions problem.\n Input kwargs (for one test case):\n - container (tuple of int): (L, W, H) representing the container dimensions in cm.\n - n (int): the number of box types.\n - cargo_vol (float): the total cargo volume in m³ (provided for consistency).\n - box_types (list of dict): one per box type. Each dictionary has the keys:\n \'length\' (int), \'length_flag\' (int),\n \'width\' (int), \'width_flag\' (int),\n \'height\' (int), \'height_flag\' (int),\n \'count\' (int), \'weight\' (float),\n \'lb1\' (float), \'lb2\' (float), \'lb3\' (float).\n The problem is to select and place boxes (each possibly in one of three allowed orientations)\n inside the container so as to maximize the ratio of the total volume of placed boxes (each based on its original dimensions)\n to the container’s volume, while obeying placement, support, and load–bearing constraints.\n Evaluation metric:\n The score is the container volume utilization (i.e. total placed boxes volume divided by container volume)\n if the solution is valid according to all constraints; otherwise the score is 0.0.\n Placeholder implementation: No boxes are placed.\n Returns a dictionary with keys:\n - \'instance\': instance number (int),\n - \'util\': achieved utilization (float),\n - \'m\': number of placements (int),\n - \'placements\': a list of placements; each placement is a dict with keys:\n \'box_type\' (int, 1-indexed), \'orientation\' (int: 1, 2, or 3),\n \'x\', \'y\', \'z\' (floats for the lower–left–front corner in cm).\n """\n # Placeholder: return an empty solution.\n return {\n \'instance\': 1,\n \'util\': 0.0,\n \'m\': 0,\n \'placements\': []\n }' -EVAL_CLASS_NAME = 'CLWREvaluationCB' -EVAL_KWARGS = {'timeout_seconds': 60} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_container_loading_with_weight_restrictions/paras.yaml b/examples/benchmark_tasks/optimization_container_loading_with_weight_restrictions/paras.yaml deleted file mode 100644 index 1573d2c9..00000000 --- a/examples/benchmark_tasks/optimization_container_loading_with_weight_restrictions/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: CLWREvaluationCB -timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_corporate_structuring/__init__.py b/examples/benchmark_tasks/optimization_corporate_structuring/__init__.py deleted file mode 100644 index 22ebdd3a..00000000 --- a/examples/benchmark_tasks/optimization_corporate_structuring/__init__.py +++ /dev/null @@ -1,341 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_corporate_structuring -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# References: -# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language -# model agents in algorithm search for combinatorial optimization. -# arXiv preprint arXiv:2504.04310 (2025). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -from typing import Any -import numpy as np -from llm4ad_loader import Evaluation -from llm4ad_loader import load_subdir_as_text -# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader -# from llm4ad.task.optimization.co_bench.corporate_structuring_co_bench.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(N: int, target: int, countries: dict, withholding: dict) -> dict:\n """\n Input kwargs:\n - N: (int) The number of countries.\n - target: (int) The target country (1-indexed) which must be the root (its parent is 0).\n - countries: (dict) Mapping country id (1-indexed) to a tuple:\n (tax_code, foreign_income_tax_rate, domestic_income_tax_rate, profit).\n - withholding: (dict of dict) A nested dictionary where withholding[i][j] is the withholding tax rate\n applied when country i sends dividends to country j.\n Returns:\n A dictionary with the key "structure" whose value is a dictionary representing the corporate tree,\n where each key is a child country and its value is the immediate parent (with the target country having parent 0).\n (Note: This is a placeholder implementation.)\n """\n # --- Placeholder implementation ---\n # For demonstration, we simply return a structure that includes only the target country.\n structure = {kwargs[\'target\']: 0}\n # In an actual solution, you would build a tree covering all countries with positive profit.\n return {"structure": structure}' -task_description = "'''Given N countries, each defined by:" - - -__all__ = ['CSEvaluationCB'] - - -class CSEvaluationCB(Evaluation): - - def __init__(self, - timeout_seconds=50, - **kwargs): - - """ - Args: - None - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - # Load datasets from Hugging Face - dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Corporate structuring") - self._datasets = {} - for filename in dataset: - # Join all text rows into a single string - text_content = '\n'.join([row['text'] for row in dataset[filename]]) - self._datasets[filename] = text_content - - def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: - return self.evaluate(callable_func) - - def evaluate(self, eva: callable) -> float | None: - ins_cases = [] - for case_id, ins in enumerate(self._datasets.values()): - ins_cases.append(self.load_data(ins)) - - fitness_list = [] - try: - for i in ins_cases: - for j in i: - result = eva(j['N'], j['target'], j['countries'], j['withholding']) - fitness = self.eval_func(j['N'], j['target'], j['countries'], j['withholding'], result['structure']) - fitness_list.append(fitness) - - return np.mean(fitness_list) # itself is a maximize problem - - except ValueError as e: - print(e) - return None - - def load_data(self, input_string): - """ - Reads input string content that may contain one or more cases. - File Format for each case: - - Line 1: Two space-separated numbers: N target - - Next N lines: For each country i (1-indexed), four space-separated values: - tax_code, foreign_income_tax_rate, domestic_income_tax_rate, profit - - Remaining tokens: N*N floating-point numbers representing the withholding tax matrix. - (These numbers can be spread across one or more lines.) - Returns: - A list of dictionaries. Each dictionary corresponds to one test case and has the keys: - - "N": (int) number of countries. - - "target": (int) target country (1-indexed). - - "countries": (dict) mapping each country id to its tuple of (tax_code, foreign_rate, domestic_rate, profit). - - "withholding": (dict of dict) where withholding[i][j] is the withholding tax rate from country i to j. - """ - cases = [] - lines = [line.strip() for line in input_string.split('\n') if line.strip() != ''] - - i = 0 - total_lines = len(lines) - while i < total_lines: - # Parse first line of a case: N and target. - parts = lines[i].split() - if len(parts) < 2: - raise ValueError("Expected N and target on line {}.".format(i + 1)) - N = int(parts[0]) - target = int(parts[1]) - i += 1 - - # Parse country data. - if i + N > total_lines: - raise ValueError("Not enough lines for country data in a case starting at line {}.".format(i + 1)) - countries = {} - for country in range(1, N + 1): - parts = lines[i].split() - if len(parts) < 4: - raise ValueError("Incomplete country data at line {}.".format(i + 1)) - tax_code = int(parts[0]) - foreign_rate = float(parts[1]) - domestic_rate = float(parts[2]) - profit = float(parts[3]) - countries[country] = (tax_code, foreign_rate, domestic_rate, profit) - i += 1 - - # Read all remaining tokens for the withholding tax matrix. - withholding_tokens = [] - # We'll assume that the withholding matrix occupies the next N*N tokens. - while i < total_lines and len(withholding_tokens) < N * N: - withholding_tokens.extend(lines[i].split()) - i += 1 - - if len(withholding_tokens) < N * N: - raise ValueError("Incomplete withholding tax matrix: expected {} numbers, got {}.".format(N * N, - len(withholding_tokens))) - - # Build the withholding matrix from tokens. - withholding = {} - token_index = 0 - for country in range(1, N + 1): - withholding[country] = {} - for j in range(1, N + 1): - withholding[country][j] = float(withholding_tokens[token_index]) - token_index += 1 - - # Append the parsed case to the list. - cases.append({ - "N": N, - "target": target, - "countries": countries, - "withholding": withholding - }) - return cases - - def eval_func(self, N, target, countries, withholding, structure): - """ - Evaluates the score of a given tree structure. - Inputs: - - N: Number of countries. - - target: The designated target country (1-indexed) that is the root (its parent is 0). - - countries: A dict mapping country id (1-indexed) to a tuple: - (tax_code, foreign_income_tax_rate, domestic_income_tax_rate, profit) - - withholding: A dict of dicts where withholding[i][j] is the withholding tax rate - applied when country i sends dividends to j. - - structure: A dict representing the corporate tree. Each key is a country (child) and its - value is its immediate parent (for the target, parent is 0). - Returns: - The score, defined as: - total_profit = (sum of profits for all countries) - (total_tax) - where total_tax is the sum of domestic tax and extra foreign tax paid in the tree. - """ - - # Build a mapping from each node to its children from the tree structure. - children = {i: [] for i in range(1, N + 1)} - for child, parent in structure.items(): - if parent != 0: # Only non-root nodes appear in the structure mapping. - children[parent].append(child) - # It is possible that some countries (e.g. with profit <= 0) are not in the structure. - # They will not incur any tax in the corporate hierarchy. - - # First, compute P[i] = sum of profits (only if >0) in the subtree of i. - # This is used in the pooling tax rules. - - P_cache = {} - - def get_P(i): - if i in P_cache: - return P_cache[i] - # Only count profit if positive (i.e. the node is a "source") - profit_i = countries[i][3] - total = profit_i - for c in children.get(i, []): - total += get_P(c) - P_cache[i] = total - return total - - for i in range(1, N + 1): - P_cache[i] = get_P(i) - - print(P_cache) - - def outcome(i): - d_income = countries[i][3] * (1 - countries[i][2]) - f_income = foreign_income(i) - total_f_income = sum(f_income.values()) - if countries[i][0] == 1: - return d_income + total_f_income - elif countries[i][0] == 2: - return d_income + total_f_income * (1 - countries[i][1]) - elif countries[i][0] == 3: - return d_income + total_f_income - sum( - [max(0, f_income[c] - (1 - countries[i][1]) * P_cache[c]) for c in children[i]]) - else: - return d_income + total_f_income - max(0, total_f_income - (1 - countries[i][1]) * ( - P_cache[i] - countries[i][3])) - - def foreign_income(i): - if len(children.get(i, [])) == 0: - return {} - else: - total = {} - for c in children.get(i, []): - a = outcome(c) - total[c] = a * (1 - withholding[c][i]) - return total - - return outcome(target) - - def norm_score(self, results): - optimal_scores = { - "tax1.txt": [647.51], - "tax2.txt": [2153.45], - "tax3.txt": [4329.83], - "tax4.txt": [3491.62], - "tax5.txt": [5435.79], - "tax6.txt": [5058.07], - "tax7.txt": [11872.37], - "tax8.txt": [10206.65], - "tax9.txt": [16584.32], - "tax10.txt": [455], - } - - normed = {} - for case, (scores, error_message) in results.items(): - if case not in optimal_scores: - continue # Skip if there's no optimal score defined. - optimal_list = optimal_scores[case] - normed_scores = [] - # Compute normalized score for each index. - for idx, score in enumerate(scores): - if isinstance(score, (int, float)): - normed_scores.append(score / optimal_list[idx]) - else: - normed_scores.append(score) - normed[case] = (normed_scores, error_message) - - return normed - - def get_dev(self): - dev = {'tax1.txt': [], 'tax3.txt': [], 'tax5.txt': [], - 'tax7.txt': [], 'tax9.txt': []} - - return dev - - - -# Task configuration for benchmark task -ENTRY_NAME = 'solve' -FUNCTION_SIGNATURE = 'def solve(...):' -IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' -TASK_DESCRIPTION = "'''Given N countries, each defined by:" -OBJECTIVE_TEXT = "You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n'''Given N countries, each defined by:\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible." -TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(N: int, target: int, countries: dict, withholding: dict) -> dict:\n """\n Input kwargs:\n - N: (int) The number of countries.\n - target: (int) The target country (1-indexed) which must be the root (its parent is 0).\n - countries: (dict) Mapping country id (1-indexed) to a tuple:\n (tax_code, foreign_income_tax_rate, domestic_income_tax_rate, profit).\n - withholding: (dict of dict) A nested dictionary where withholding[i][j] is the withholding tax rate\n applied when country i sends dividends to country j.\n Returns:\n A dictionary with the key "structure" whose value is a dictionary representing the corporate tree,\n where each key is a child country and its value is the immediate parent (with the target country having parent 0).\n (Note: This is a placeholder implementation.)\n """\n # --- Placeholder implementation ---\n # For demonstration, we simply return a structure that includes only the target country.\n structure = {kwargs[\'target\']: 0}\n # In an actual solution, you would build a tree covering all countries with positive profit.\n return {"structure": structure}' -EVAL_CLASS_NAME = 'CSEvaluationCB' -EVAL_KWARGS = {'timeout_seconds': 60} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_corporate_structuring/paras.yaml b/examples/benchmark_tasks/optimization_corporate_structuring/paras.yaml deleted file mode 100644 index afbcb78e..00000000 --- a/examples/benchmark_tasks/optimization_corporate_structuring/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: CSEvaluationCB -timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_crew_scheduling/__init__.py b/examples/benchmark_tasks/optimization_crew_scheduling/__init__.py deleted file mode 100644 index 4a110b57..00000000 --- a/examples/benchmark_tasks/optimization_crew_scheduling/__init__.py +++ /dev/null @@ -1,369 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_crew_scheduling -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# References: -# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language -# model agents in algorithm search for combinatorial optimization. -# arXiv preprint arXiv:2504.04310 (2025). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -from typing import Any -import numpy as np -from llm4ad_loader import Evaluation -from llm4ad_loader import load_subdir_as_text -# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader -# from llm4ad.task.optimization.co_bench.crew_scheduling_co_bench.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(N: int, K: int, time_limit: float, tasks: dict, arcs: dict) -> dict:\n """\n Solves the crew scheduling problem.\n The problem consists of assigning each task (with a defined start and finish time) to exactly one crew,\n such that:\n - The tasks within each crew are executed in non-overlapping order.\n - For every consecutive pair of tasks in a crew’s schedule, a valid transition arc exists (with an associated cost).\n - The overall duty time (finish time of the last task minus start time of the first task) does not exceed the specified time limit.\n - Exactly K crews are used.\n Input kwargs (for one case):\n - N (int): Number of tasks.\n - K (int): Maximum number of crews to be used.\n - time_limit (float): Maximum allowed duty time.\n - tasks (dict): Dictionary mapping task ID (1 to N) to a tuple (start_time, finish_time).\n - arcs (dict): Dictionary mapping (from_task, to_task) pairs to transition cost.\n Evaluation metric:\n - If all constraints are met (no task overlap, valid transition arcs, duty time within the limit, and exactly K crews used), the score is the sum of transition costs across all crews.\n - If any constraint is violated, the solution is infeasible and receives no score.\n - A lower score indicates a more cost-effective solution.\n Returns:\n dict: A dictionary with one key "crews", whose value is a list of lists. Each inner list is a sequence of task IDs (integers)\n representing one crew’s schedule.\n """\n # --- placeholder implementation ---\n # For example, here we distribute tasks evenly across K crews.\n N = kwargs.get("N")\n K = kwargs.get("K")\n tasks_ids = list(range(1, N + 1))\n crews = [[] for _ in range(K)]\n for i, task in enumerate(tasks_ids):\n crews[i % K].append(task)\n # In practice, you would implement a heuristic or optimization method that groups tasks into exactly K crews\n # while satisfying the non-overlap, valid transitions, and duty time constraints.\n return {"crews": crews}' -task_description = '("The Crew Scheduling Problem involves assigning each task—with defined start and finish times—to "' - - -__all__ = ['CSchedulingEvaluationCB'] - - -class CSchedulingEvaluationCB(Evaluation): - - def __init__(self, - timeout_seconds=50, - **kwargs): - - """ - Args: - None - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - # Load datasets from Hugging Face - dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Crew scheduling") - self._datasets = {} - for filename in dataset: - # Join all text rows into a single string - text_content = '\n'.join([row['text'] for row in dataset[filename]]) - self._datasets[filename] = text_content - - def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: - return self.evaluate(callable_func) - - def evaluate(self, eva: callable) -> float | None: - ins_cases = [] - for case_id, ins in enumerate(self._datasets.values()): - ins_cases.append(self.load_data(ins)) - - fitness_list = [] - try: - for i in ins_cases: - for j in i: - result = eva(j['N'], j['K'], j['time_limit'], j['tasks'], j['arcs']) - fitness = self.eval_func(N=j['N'], K=j['K'], time_limit=j['time_limit'], tasks=j['tasks'], arcs=j['arcs'], crews=result['crews']) - fitness_list.append(fitness) - - return -np.mean(fitness_list) - - except ValueError as e: - print(e) - return None - - def load_data(self, input_string): - """ - Loads input data from a provided text string. This function supports multiple cases. - The input file format for each case is as follows: - - The first line contains two numbers: the number of tasks (N) and the maximum allowed duty time (time_limit). - - The next N lines contain two numbers each: start time and finish time for each task (tasks are indexed from 1 to N). - - The remaining lines describe transition arcs between tasks in the format: "i j cost". - Cases are assumed to be separated by one or more blank lines. - Returns: - list: A list of dictionaries, each dictionary corresponds to one case with keys: - "N", "time_limit", "tasks", "arcs". - """ - cases = [] - try: - lines = [line.strip() for line in input_string.split('\n') if line.strip() != ''] - except Exception as e: - raise ValueError("Failed to read input string: " + str(e)) - - # Split lines into blocks separated by blank lines. - blocks = [] - current_block = [] - for line in lines: - if line.strip() == "": - if current_block: - blocks.append(current_block) - current_block = [] - else: - current_block.append(line.strip()) - if current_block: - blocks.append(current_block) - - # Parse each block as a separate case. - for block in blocks: - if not block: - continue - # Parse the first line: number of tasks and time limit. - first_parts = block[0].split() - if len(first_parts) < 2: - raise ValueError("The first line must contain at least two values: number of tasks and time limit.") - try: - N = int(first_parts[0]) - time_limit = float(first_parts[1]) - except Exception as e: - raise ValueError("Error parsing number of tasks or time limit: " + str(e)) - - if len(block) < 1 + N: - raise ValueError(f"Expected {N} task lines after the first line; found {len(block) - 1}.") - - tasks = {} - # Parse tasks: next N lines. - for i in range(1, 1 + N): - parts = block[i].split() - if len(parts) < 2: - raise ValueError(f"Task line {i} does not contain two values.") - try: - start_time = float(parts[0]) - finish_time = float(parts[1]) - except Exception as e: - raise ValueError(f"Invalid time values in task line {i}: " + str(e)) - tasks[i] = (start_time, finish_time) - - # Parse remaining lines: transition arcs. - arcs = {} - for line in block[1 + N:]: - parts = line.split() - if len(parts) < 3: - continue # Ignore lines that don't have the complete triple. - try: - from_task = int(parts[0]) - to_task = int(parts[1]) - cost = float(parts[2]) - except Exception: - continue # Skip lines with invalid formatting. - arcs[(from_task, to_task)] = cost - - case_data = {"N": N, "time_limit": time_limit, "tasks": tasks, "arcs": arcs} - - # Determine K range based on problem size (N) - if N <= 50: - k_range = range(27, 32) - elif N <= 100: - k_range = range(44, 49) - elif N <= 150: - k_range = range(69, 74) - elif N <= 200: - k_range = range(93, 98) - elif N <= 250: - k_range = range(108, 113) - elif N <= 300: - k_range = range(130, 134) - elif N <= 350: - k_range = range(144, 149) - elif N <= 400: - k_range = range(159, 164) - elif N <= 450: - k_range = range(182, 187) - else: # N <= 500 or larger - k_range = range(204, 209) - - for k in k_range: - cases.append(case_data | {'K': k}) - - return cases - - def eval_func(self, **kwargs): - """ - Evaluates the quality (i.e. total cost and feasibility) of a crew scheduling solution. - Raises an error immediately if any feasibility constraint is violated. - Input kwargs must include: - - N (int): Number of tasks. - - K (int): The exact number of crews required. - - time_limit (float): Maximum allowed duty time. - - tasks (dict): Mapping from task ID to (start_time, finish_time). - - arcs (dict): Mapping from (from_task, to_task) to transition cost. - - crews (list): List of lists, where each inner list is the sequence of task IDs for one crew. - Returns: - float: The total transition cost if the solution is feasible. - """ - N = kwargs.get("N") - K = kwargs.get("K") - time_limit = kwargs.get("time_limit") - tasks = kwargs.get("tasks") - arcs = kwargs.get("arcs") - crews = kwargs.get("crews") - - if crews is None: - raise ValueError("Solution does not contain a 'crews' key.") - - # Check that exactly K crews are used. - if K is None: - raise ValueError("Parameter K (number of crews) is missing.") - if len(crews) > K: - raise ValueError(f"Invalid solution: number of crews in solution is larger than K={K}.") - - # Validate that every task appears exactly once. - all_tasks_in_output = [task for crew in crews for task in crew] - if len(all_tasks_in_output) != N: - raise ValueError("Invalid solution: number of tasks in crews does not equal N.") - if set(all_tasks_in_output) != set(range(1, N + 1)): - raise ValueError("Invalid solution: tasks in crews do not match expected tasks set.") - - total_cost = 0.0 - - # Evaluate each crew schedule. - for crew in crews: - if not crew: - raise ValueError("Invalid solution: one crew has an empty schedule.") - - # Check the duty time. - first_task = crew[0] - last_task = crew[-1] - duty_time = tasks[last_task][1] - tasks[first_task][0] - if duty_time > time_limit: - raise ValueError("Invalid solution: duty time for a crew exceeds the time limit.") - - # Check each consecutive pair of tasks. - for idx in range(len(crew) - 1): - current_task = crew[idx] - next_task = crew[idx + 1] - - # Check that tasks do not overlap. - if tasks[current_task][1] > tasks[next_task][0]: - raise ValueError(f"Invalid solution: tasks {current_task} and {next_task} overlap.") - - # Check that a valid transition arc exists. - if (current_task, next_task) not in arcs: - raise ValueError( - f"Invalid solution: missing transition arc between tasks {current_task} and {next_task}.") - - # Add the transition cost. - total_cost += arcs[(current_task, next_task)] - - return total_cost - - def norm_score(self, results): - optimal_scores = { - 'csp50.txt': [3139, 2706, 2399, 2092, 1872], - 'csp100.txt': [4812, 4514, 4310, 4107, 3905], - 'csp150.txt': [6275, 5999, 5754, 5551, 5347], - 'csp200.txt': [6914, 6747, 6583, 6430, 6288], - 'csp250.txt': [8406, 8212, 8023, 7863, 7707], - 'csp300.txt': [9580, 9378, 9200, 9026], - 'csp350.txt': [10991, 10833, 10677, 10525, 10378], - 'csp400.txt': [12341, 12163, 12006, 11848, 11696], - 'csp450.txt': [12785, 12639, 12497, 12357, 12232], - 'csp500.txt': [13302, 13169, 13032, 12899, 12772], - } - - normed = {} - for case, (scores, error_message) in results.items(): - if case not in optimal_scores: - continue # Skip if there's no optimal score defined. - optimal_list = optimal_scores[case] - normed_scores = [] - # Compute normalized score for each index. - for idx, score in enumerate(scores): - if isinstance(score, (int, float)): - normed_scores.append(optimal_list[idx] / score) - else: - normed_scores.append(score) - normed[case] = (normed_scores, error_message) - - return normed - - def get_dev(self): - dev = {'csp100.txt': [2, 1], 'csp150.txt': [1, 4], 'csp200.txt': [4, 2], 'csp250.txt': [2, 1], - 'csp300.txt': [2, 0], - 'csp350.txt': [4, 3], 'csp400.txt': [2, 0], 'csp450.txt': [2, 1], 'csp50.txt': [1, 0], - 'csp500.txt': [4, 1]} - - return dev - - - -# Task configuration for benchmark task -ENTRY_NAME = 'solve' -FUNCTION_SIGNATURE = 'def solve(...):' -IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' -TASK_DESCRIPTION = '("The Crew Scheduling Problem involves assigning each task—with defined start and finish times—to "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The Crew Scheduling Problem involves assigning each task—with defined start and finish times—to "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(N: int, K: int, time_limit: float, tasks: dict, arcs: dict) -> dict:\n """\n Solves the crew scheduling problem.\n The problem consists of assigning each task (with a defined start and finish time) to exactly one crew,\n such that:\n - The tasks within each crew are executed in non-overlapping order.\n - For every consecutive pair of tasks in a crew’s schedule, a valid transition arc exists (with an associated cost).\n - The overall duty time (finish time of the last task minus start time of the first task) does not exceed the specified time limit.\n - Exactly K crews are used.\n Input kwargs (for one case):\n - N (int): Number of tasks.\n - K (int): Maximum number of crews to be used.\n - time_limit (float): Maximum allowed duty time.\n - tasks (dict): Dictionary mapping task ID (1 to N) to a tuple (start_time, finish_time).\n - arcs (dict): Dictionary mapping (from_task, to_task) pairs to transition cost.\n Evaluation metric:\n - If all constraints are met (no task overlap, valid transition arcs, duty time within the limit, and exactly K crews used), the score is the sum of transition costs across all crews.\n - If any constraint is violated, the solution is infeasible and receives no score.\n - A lower score indicates a more cost-effective solution.\n Returns:\n dict: A dictionary with one key "crews", whose value is a list of lists. Each inner list is a sequence of task IDs (integers)\n representing one crew’s schedule.\n """\n # --- placeholder implementation ---\n # For example, here we distribute tasks evenly across K crews.\n N = kwargs.get("N")\n K = kwargs.get("K")\n tasks_ids = list(range(1, N + 1))\n crews = [[] for _ in range(K)]\n for i, task in enumerate(tasks_ids):\n crews[i % K].append(task)\n # In practice, you would implement a heuristic or optimization method that groups tasks into exactly K crews\n # while satisfying the non-overlap, valid transitions, and duty time constraints.\n return {"crews": crews}' -EVAL_CLASS_NAME = 'CSchedulingEvaluationCB' -EVAL_KWARGS = {'timeout_seconds': 60} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_crew_scheduling/paras.yaml b/examples/benchmark_tasks/optimization_crew_scheduling/paras.yaml deleted file mode 100644 index e5f0d6b0..00000000 --- a/examples/benchmark_tasks/optimization_crew_scheduling/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: CSchedulingEvaluationCB -timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_cvrp_construct/__init__.py b/examples/benchmark_tasks/optimization_cvrp_construct/__init__.py deleted file mode 100644 index 2412106f..00000000 --- a/examples/benchmark_tasks/optimization_cvrp_construct/__init__.py +++ /dev/null @@ -1,328 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_cvrp_construct -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# Module Name: CVRPEvaluation -# Last Revision: 2025/2/16 -# Description: Evaluates the Capacitated Vehicle Routing Problem (CVRP). -# Given a set of customers and a fleet of vehicles with limited capacity, -# the goal is to find optimal routes for the vehicles to serve all customers -# while minimizing the total travel distance. -# This module is part of the LLM4AD project (https://github.com/Optima-CityU/llm4ad). -# -# Parameters: -# - timeout_seconds: Maximum allowed time (in seconds) for the evaluation process: int (default: 20). -# - n_instance: Number of problem instances to generate: int (default: 16). -# - problem_size: Number of customers to serve: int (default: 50). -# - capacity: Maximum capacity of each vehicle: int (default: 40). -# -# References: -# - Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -import os, sys -sys.path.insert(0, os.path.dirname(__file__)) -import copy -from typing import Any -import matplotlib.pyplot as plt -import numpy as np - -from llm4ad_loader import Evaluation -from get_instance import GetData -# from llm4ad.task.optimization.cvrp_construct.get_instance import GetData # Converted from LLM4AD import -# from llm4ad.task.optimization.cvrp_construct.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\ndef select_next_node(current_node: int, depot: int, unvisited_nodes: np.ndarray, rest_capacity: np.ndarray, demands: np.ndarray, distance_matrix: np.ndarray) -> int:\n """Design a novel algorithm to select the next node in each step.\n Args:\n current_node: ID of the current node.\n depot: ID of the depot.\n unvisited_nodes: Array of IDs of unvisited nodes.\n rest_capacity: rest capacity of vehicle\n demands: demands of nodes\n distance_matrix: Distance matrix of nodes.\n Return:\n ID of the next node to visit.\n """\n best_score = -1\n next_node = -1\n\n for node in unvisited_nodes:\n demand = demands[node]\n distance = distance_matrix[current_node][node]\n\n if demand <= rest_capacity:\n score = demand / distance if distance > 0 else float(\'inf\') # Avoid division by zero\n if score > best_score:\n best_score = score\n next_node = node\n\n return next_node' -task_description = '"' - - - -class CVRPEvaluation(Evaluation): - def __init__(self, - timeout_seconds=20, - n_instance=16, - problem_size=50, - capacity=40, - **kwargs): - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - self.problem_size = problem_size + 1 - self.n_instance = n_instance - self.capacity = capacity - - getData = GetData(self.n_instance, self.problem_size, self.capacity) - self._datasets = getData.generate_instances() - - def plot_solution(self, instance: np.ndarray, route: list, demands: list, vehicle_capacity: int): - """ - Plot the solution of the Capacitated Vehicle Routing Problem (CVRP). - - Args: - instance: A 2D array of node coordinates (including the depot). - route: A list representing the sequence of nodes visited in the route. - demands: A list of demands for each node. - vehicle_capacity: The capacity of the vehicle. - """ - # Extract coordinates - x = instance[:, 0] - y = instance[:, 1] - - # Create a figure and axis - fig, ax = plt.subplots(figsize=(10, 8)) - - # Plot depot (node 0) - ax.plot(x[0], y[0], 'ro', markersize=10, label='Depot') - ax.text(x[0], y[0], 'Depot', ha='center', va='bottom', fontsize=12) - - # Plot customer nodes - for i in range(1, len(x)): - ax.plot(x[i], y[i], 'bo', markersize=8) - ax.text(x[i], y[i], f'C{i}\nDem: {demands[i]}', ha='center', va='bottom', fontsize=8) - - # Split the route into individual vehicle routes based on depot visits - routes = [] - current_route = [] - for node in route: - current_route.append(node) - if node == 0 and len(current_route) > 1: # End of a route (return to depot) - routes.append(current_route) - current_route = [0] # Start a new route from the depot - if current_route: # Add the last route if it exists - routes.append(current_route) - - # Plot each route in a different color - colors = plt.cm.tab10.colors # Use a colormap for distinct colors - for i, r in enumerate(routes): - color = colors[i % len(colors)] # Cycle through colors - for j in range(len(r) - 1): - start_node = r[j] - end_node = r[j + 1] - ax.plot([x[start_node], x[end_node]], [y[start_node], y[end_node]], color=color, linestyle='--', linewidth=1, label=f'Route {i + 1}' if j == 0 else None) - - # Add load information - if end_node != 0: # If not returning to the depot - ax.text((x[start_node] + x[end_node]) / 2, (y[start_node] + y[end_node]) / 2, - f'Load: {sum(demands[r[:j + 1]])}', ha='center', va='center', fontsize=8, rotation=45) - - # Mark start and end nodes of the route with triangles (excluding depot) - if len(r) > 1: - ax.plot(x[r[1]], y[r[1]], '^', color=color, markersize=10, label='Start' if i == 0 else None) # Start node - ax.plot(x[r[-2]], y[r[-2]], 'v', color=color, markersize=10, label='End' if i == 0 else None) # End node - - # Set axis labels and title - ax.set_xlabel('X Coordinate') - ax.set_ylabel('Y Coordinate') - ax.set_title('Capacitated Vehicle Routing Problem (CVRP) Solution') - ax.legend(loc='upper right') - - # Show the plot - plt.tight_layout() - plt.show() - - def tour_cost(self, instance, solution): - cost = 0 - for j in range(len(solution) - 1): - cost += np.linalg.norm(instance[int(solution[j])] - instance[int(solution[j + 1])]) - cost += np.linalg.norm(instance[int(solution[-1])] - instance[int(solution[0])]) - return cost - - def route_construct(self, distance_matrix, demands, vehicle_capacity, heuristic): - route = [] - current_load = 0 - current_node = 0 - route.append(current_node) - - unvisited_nodes = set(range(1, self.problem_size)) # Assuming node 0 is the depot - all_nodes = np.array(list(unvisited_nodes)) - feasible_unvisited_nodes = all_nodes - - while unvisited_nodes: - next_node = heuristic(current_node, - 0, - feasible_unvisited_nodes, # copy - vehicle_capacity - current_load, - copy.deepcopy(demands), # copy - copy.deepcopy(distance_matrix)) # copy - if next_node == 0: - # Update route and load - route.append(next_node) - current_load = 0 - current_node = 0 - else: - # Update route and load - route.append(next_node) - current_load += demands[next_node] - unvisited_nodes.remove(next_node) - current_node = next_node - - feasible_nodes_capacity = np.array([node for node in all_nodes if current_load + demands[node] <= vehicle_capacity]) - # Determine feasible and unvisited nodes - feasible_unvisited_nodes = np.intersect1d(feasible_nodes_capacity, list(unvisited_nodes)) - - if len(unvisited_nodes) > 0 and len(feasible_unvisited_nodes) < 1: - route.append(0) - current_load = 0 - current_node = 0 - feasible_unvisited_nodes = np.array(list(unvisited_nodes)) - - # check if not all nodes have been visited - independent_values = set(route) - if len(independent_values) != self.problem_size: - return None - return route - - def evaluate(self, heuristic): - dis = np.ones(self.n_instance) - n_ins = 0 - - for instance, distance_matrix, demands, vehicle_capacity in self._datasets: - route = self.route_construct(distance_matrix, demands, vehicle_capacity, heuristic) - LLM_dis = self.tour_cost(instance, route) - dis[n_ins] = LLM_dis - n_ins += 1 - if n_ins == self.n_instance: - break - - ave_dis = np.average(dis) - return -ave_dis - - def evaluate_program(self, program_str: str, callable_func: callable) -> Any | None: - return self.evaluate(callable_func) - - -if __name__ == '__main__': - def select_next_node(current_node: int, depot: int, unvisited_nodes: np.ndarray, rest_capacity: np.ndarray, demands: np.ndarray, distance_matrix: np.ndarray) -> int: - """Design a novel algorithm to select the next node in each step. - Args: - current_node: ID of the current node. - depot: ID of the depot. - unvisited_nodes: Array of IDs of unvisited nodes. - rest_capacity: rest capacity of vehicle - demands: demands of nodes - distance_matrix: Distance matrix of nodes. - Return: - ID of the next node to visit. - """ - next_node = unvisited_nodes[0] - return next_node - - - # def select_next_node(current_node: int, depot: int, unvisited_nodes: np.ndarray, rest_capacity: np.ndarray, demands: np.ndarray, distance_matrix: np.ndarray) -> int: - # """Design a novel algorithm to select the next node in each step. - # Args: - # current_node: ID of the current node. - # depot: ID of the depot. - # unvisited_nodes: Array of IDs of unvisited nodes. - # rest_capacity: rest capacity of vehicle - # demands: demands of nodes - # distance_matrix: Distance matrix of nodes. - # Return: - # ID of the next node to visit. - # """ - # best_score = -1 - # next_node = -1 - - # for node in unvisited_nodes: - # demand = demands[node] - # distance = distance_matrix[current_node][node] - - # if demand <= rest_capacity: - # score = demand / distance if distance > 0 else float('inf') # Avoid division by zero - # if score > best_score: - # best_score = score - # next_node = node - - # return next_node - - eval = CVRPEvaluation() - res = eval.evaluate_program('', select_next_node) - print(res) - -# Task configuration for benchmark task -ENTRY_NAME = 'select_next_node' -FUNCTION_SIGNATURE = 'def select_next_node(...):' -IMPORT_HEADER = 'import numpy as np\nimport math' -TASK_DESCRIPTION = '"' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `select_next_node` for the LLM4AD task.\\n\\nTask description:\\n"\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\ndef select_next_node(current_node: int, depot: int, unvisited_nodes: np.ndarray, rest_capacity: np.ndarray, demands: np.ndarray, distance_matrix: np.ndarray) -> int:\n """Design a novel algorithm to select the next node in each step.\n Args:\n current_node: ID of the current node.\n depot: ID of the depot.\n unvisited_nodes: Array of IDs of unvisited nodes.\n rest_capacity: rest capacity of vehicle\n demands: demands of nodes\n distance_matrix: Distance matrix of nodes.\n Return:\n ID of the next node to visit.\n """\n best_score = -1\n next_node = -1\n\n for node in unvisited_nodes:\n demand = demands[node]\n distance = distance_matrix[current_node][node]\n\n if demand <= rest_capacity:\n score = demand / distance if distance > 0 else float(\'inf\') # Avoid division by zero\n if score > best_score:\n best_score = score\n next_node = node\n\n return next_node' -EVAL_CLASS_NAME = 'CVRPEvaluation' -EVAL_KWARGS = {'timeout_seconds': 30} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_cvrp_construct/get_instance.py b/examples/benchmark_tasks/optimization_cvrp_construct/get_instance.py deleted file mode 100644 index 41e5f5fb..00000000 --- a/examples/benchmark_tasks/optimization_cvrp_construct/get_instance.py +++ /dev/null @@ -1,50 +0,0 @@ -import pickle - -import numpy as np - - -class GetData: - def __init__(self, n_instance, n_cities, capacity): - self.n_instance = n_instance - self.n_cities = n_cities - self.capacity = capacity - - def generate_instances(self): - """each instance -> (coordinates, distances, demands, capacity)""" - np.random.seed(2024) - instance_data = [] - for _ in range(self.n_instance): - coordinates = np.random.rand(self.n_cities, 2) - demands = np.random.randint(1, 10, size=self.n_cities) - distances = np.linalg.norm(coordinates[:, np.newaxis] - coordinates, axis=2) - instance_data.append((coordinates, distances, demands, self.capacity)) - return instance_data - - -if __name__ == '__main__': - gd = GetData(10, 51) - data = gd.generate_instances() - with open('data.pkl', 'wb') as f: - pickle.dump(data, f) - - prompt_code_temp = "import numpy as np\n\ - def select_next_node(current_node: int, depot: int, unvisited_nodes: np.ndarray, rest_capacity: np.ndarray, demands: np.ndarray, distance_matrix: np.ndarray) -> int: \n\ - \n\ - '''Design a novel algorithm to select the next node in each step.\n\ - \n\ - Args:\n\ - current_node: ID of the current node.\n\ - depot: ID of the depot.\n\ - unvisited_nodes: Array of IDs of unvisited nodes.\n\ - rest_capacity: rest capacity of vehicle \n\ - demands: demands of nodes \n\ - distance_matrix: Distance matrix of nodes.\n\ - \n\ - Return:\n\ - ID of the next node to visit.\n\ - '''\n\ - next_node = unvisited_nodes[0]\n\ - \n\ - return next_node\n" - - print(prompt_code_temp) diff --git a/examples/benchmark_tasks/optimization_cvrp_construct/paras.yaml b/examples/benchmark_tasks/optimization_cvrp_construct/paras.yaml deleted file mode 100644 index fa0fb882..00000000 --- a/examples/benchmark_tasks/optimization_cvrp_construct/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: CVRPEvaluation -timeout_seconds: 30 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_equitable_partitioning_problem/__init__.py b/examples/benchmark_tasks/optimization_equitable_partitioning_problem/__init__.py deleted file mode 100644 index 77946010..00000000 --- a/examples/benchmark_tasks/optimization_equitable_partitioning_problem/__init__.py +++ /dev/null @@ -1,326 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_equitable_partitioning_problem -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# References: -# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language -# model agents in algorithm search for combinatorial optimization. -# arXiv preprint arXiv:2504.04310 (2025). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -from typing import Any -import numpy as np -from llm4ad_loader import Evaluation -from llm4ad_loader import load_subdir_as_text -# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader -# from llm4ad.task.optimization.co_bench.equitable_partitioning_problem_co_bench.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(data: list[list[int]]) -> dict:\n """\n Partition individuals into 8 groups so that for every binary attribute the count of 1\'s is as evenly\n distributed across the groups as possible.\n Input kwargs:\n - data (list of list of int): A matrix where each inner list represents the binary attributes (0 or 1)\n of one individual.\n Evaluation Metric:\n For each attribute, calculate the number of 1’s in each group,\n then compute the absolute difference between each group’s count and the mean count for that attribute.\n Average these differences over all groups to obtain the attribute’s imbalance.\n The final score is the sum of these attribute imbalances across all attributes.\n A lower score indicates a more balanced partitioning.\n Returns:\n dict: A dictionary with one key \'assignment\' whose value is a list of positive integers (one per individual)\n indicating the group assignment (using 1-based indexing). For example:\n { "assignment": [1, 1, 1, ...] }\n """\n # --- Placeholder solution ---\n # For this placeholder, we assign every individual to group 1.\n data = kwargs.get(\'data\', [])\n num_individuals = len(data)\n return {\'assignment\': [1] * num_individuals}' -task_description = '("The task is to partition a set of individuals—each characterized by multiple binary "' - - -__all__ = ['EPPEvaluationCB'] - - -class EPPEvaluationCB(Evaluation): - - def __init__(self, - timeout_seconds=50, - **kwargs): - - """ - Args: - None - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - # Load datasets from Hugging Face - dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Equitable partitioning problem") - self._datasets = {} - for filename in dataset: - # Join all text rows into a single string - text_content = '\n'.join([row['text'] for row in dataset[filename]]) - self._datasets[filename] = text_content - - def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: - return self.evaluate(callable_func) - - def evaluate(self, eva: callable) -> float | None: - ins_cases = [] - for case_id, ins in enumerate(self._datasets.values()): - ins_cases.append(self.load_data(ins)) - - fitness_list = [] - try: - for i in ins_cases: - for j in i: - result = eva(j['data']) - fitness = self.eval_func(data=j['data'], assignment=result['assignment']) - fitness_list.append(fitness) - - return -np.mean(fitness_list) - - except ValueError as e: - print(e) - return None - - def load_data(self, input_string): - """ - Reads input string content where each non-empty line represents an individual with space-separated binary attributes. - In case the input string contains multiple cases (separated by one or more blank lines), this function will - separate them into distinct cases. - Parameters: - input_string (str): The string content with the input data. - Returns: - list: A list of dictionaries. Each dictionary represents one case with the key 'data' mapping to a 2D list - (matrix) of binary attributes (0 or 1). For example: - [ - {"data": [[0, 1, 0], [1, 0, 1], ...]}, - {"data": [[1, 1], [0, 1], ...]}, - ... - ] - Raises: - Exception: If the string cannot be read, or if any line is invalid, contains non-integer tokens, - tokens not in {0, 1}, or if any row has an inconsistent number of attributes. - """ - try: - all_lines = [line.strip() for line in input_string.split('\n')] - except Exception as e: - raise Exception("Error reading input string: " + str(e)) - - cases = [] - current_case = [] - for line_no, line in enumerate(all_lines, start=1): - stripped = line.strip() - # A blank line indicates a separator between cases. - if not stripped: - if current_case: - cases.append(current_case) - current_case = [] - continue - current_case.append(stripped) - - # Add last case if file did not end with a blank line. - if current_case: - cases.append(current_case) - - # Parse each case into a data matrix. - list_of_cases = [] - for case_idx, case_lines in enumerate(cases, start=1): - matrix = [] - n_attributes = None - for line_no, line in enumerate(case_lines, start=1): - tokens = line.split() - if not tokens: - raise Exception(f"Case {case_idx}, line {line_no} is empty or invalid.") - try: - row = [int(token) for token in tokens] - except ValueError: - raise Exception(f"Non-integer value found in case {case_idx}, line {line_no}.") - for token in row: - if token not in (0, 1): - raise Exception( - f"Invalid attribute value {token} found in case {case_idx}, line {line_no}; expected only 0 or 1.") - if n_attributes is None: - n_attributes = len(row) - elif len(row) != n_attributes: - raise Exception(f"Inconsistent number of attributes in case {case_idx}, line {line_no}.") - matrix.append(row) - list_of_cases.append({"data": matrix}) - - if not list_of_cases: - raise Exception("Input file is empty or contains no valid cases.") - - return list_of_cases - - def eval_func(self, **kwargs): - """ - Evaluates a partitioning solution for the equitable distribution problem using the new imbalance metric. - Expected Parameters (provided via kwargs): - - data (list of list of int): A matrix of binary attributes for individuals. - - assignment (list of int): A list of positive integers representing group assignments for each individual. - Evaluation Metric: - For each attribute (column), compute the number of 1's per group. Then, compute the mean of these counts. - The imbalance for the attribute is defined as the average of the absolute differences between each group's count and the mean count. - The final score is the sum of these imbalances over all attributes. - (A lower score indicates a more balanced partitioning.) - Returns: - total_imbalance: The computed total imbalance (float). - Raises: - Exception: If any expected parameter is missing, if the assignment format is invalid, or if the number of groups is not 8. - """ - # Retrieve input data and assignment from kwargs - if 'data' not in kwargs or 'assignment' not in kwargs: - raise Exception("Missing required input parameters 'data' and/or 'assignment'.") - - data = kwargs['data'] - assignment = kwargs['assignment'] - # - n_individuals = len(data) - if len(assignment) != n_individuals: - raise Exception(f"Expected {n_individuals} group assignments but found {len(assignment)}.") - - n_attributes = len(data[0]) - for idx, row in enumerate(data, start=1): - if len(row) != n_attributes: - raise Exception(f"Inconsistent number of attributes in data at individual {idx}.") - - # Ensure all group assignments are positive integers. - for idx, g in enumerate(assignment, start=1): - if not isinstance(g, int) or g < 1: - raise Exception(f"Invalid group assignment at position {idx}: {g}. Must be a positive integer.") - - # Collect unique groups and check for exactly 8 groups. - groups = set(assignment) - if len(groups) != 8: - raise Exception(f"Invalid number of groups: expected 8, but got {len(groups)}.") - - # Initialize per-group attribute sums. - group_sums = {g: [0] * n_attributes for g in groups} - for ind, group in enumerate(assignment): - for j in range(n_attributes): - group_sums[group][j] += data[ind][j] - - total_imbalance = 0.0 - for j in range(n_attributes): - # Collect counts for attribute j from all groups - attr_counts = [group_sums[g][j] for g in groups] - mean_count = sum(attr_counts) / len(groups) - # Compute average absolute difference from the mean - # imbalance = sum(abs(count - mean_count) for count in attr_counts) / len(groups) - imbalance = sum(abs(count - mean_count) for count in attr_counts) - total_imbalance += imbalance - - return total_imbalance - - def norm_score(self, results): - optimal_scores = { - "eppperf1.txt": [0], - "eppperf2.txt": [0], - "eppperf3.txt": [0], - "eppperf4.txt": [0], - "eppperf5.txt": [0], - "epprandom1.txt": [11.5], - "epprandom2.txt": [12.75], - "epprandom3.txt": [13.75], - "epprandom4.txt": [14.50], - "epprandom5.txt": [16.25], - } - - normed = {} - for case, (scores, error_message) in results.items(): - if case not in optimal_scores: - continue # Skip if there's no optimal score defined. - optimal_list = optimal_scores[case] - normed_scores = [] - # Compute normalized score for each index. - for idx, score in enumerate(scores): - if isinstance(score, (int, float)): - if optimal_list[idx] == 0: - normed_scores.append((optimal_list[idx] + 1) / (score + 1)) - else: - normed_scores.append(optimal_list[idx] / score) - else: - normed_scores.append(score) - normed[case] = (normed_scores, error_message) - - return normed - - def get_dev(self): - dev = {'eppperf1.txt': [0], 'eppperf3.txt': [0], - 'epprandom2.txt': [0], 'epprandom4.txt': [0]} - - return dev - - - - -# Task configuration for benchmark task -ENTRY_NAME = 'solve' -FUNCTION_SIGNATURE = 'def solve(...):' -IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' -TASK_DESCRIPTION = '("The task is to partition a set of individuals—each characterized by multiple binary "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The task is to partition a set of individuals—each characterized by multiple binary "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(data: list[list[int]]) -> dict:\n """\n Partition individuals into 8 groups so that for every binary attribute the count of 1\'s is as evenly\n distributed across the groups as possible.\n Input kwargs:\n - data (list of list of int): A matrix where each inner list represents the binary attributes (0 or 1)\n of one individual.\n Evaluation Metric:\n For each attribute, calculate the number of 1’s in each group,\n then compute the absolute difference between each group’s count and the mean count for that attribute.\n Average these differences over all groups to obtain the attribute’s imbalance.\n The final score is the sum of these attribute imbalances across all attributes.\n A lower score indicates a more balanced partitioning.\n Returns:\n dict: A dictionary with one key \'assignment\' whose value is a list of positive integers (one per individual)\n indicating the group assignment (using 1-based indexing). For example:\n { "assignment": [1, 1, 1, ...] }\n """\n # --- Placeholder solution ---\n # For this placeholder, we assign every individual to group 1.\n data = kwargs.get(\'data\', [])\n num_individuals = len(data)\n return {\'assignment\': [1] * num_individuals}' -EVAL_CLASS_NAME = 'EPPEvaluationCB' -EVAL_KWARGS = {'timeout_seconds': 60} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_equitable_partitioning_problem/paras.yaml b/examples/benchmark_tasks/optimization_equitable_partitioning_problem/paras.yaml deleted file mode 100644 index 578dfad2..00000000 --- a/examples/benchmark_tasks/optimization_equitable_partitioning_problem/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: EPPEvaluationCB -timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_euclidean_steiner_problem/__init__.py b/examples/benchmark_tasks/optimization_euclidean_steiner_problem/__init__.py deleted file mode 100644 index b2b2dd4c..00000000 --- a/examples/benchmark_tasks/optimization_euclidean_steiner_problem/__init__.py +++ /dev/null @@ -1,372 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_euclidean_steiner_problem -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# References: -# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language -# model agents in algorithm search for combinatorial optimization. -# arXiv preprint arXiv:2504.04310 (2025). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -from typing import Any -import numpy as np -from llm4ad_loader import Evaluation -from llm4ad_loader import load_subdir_as_text -# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader -# from llm4ad.task.optimization.co_bench.euclidean_steiner_problem_co_bench.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(points: list) -> dict:\n """\n Solves a single instance of the Euclidean Steiner Problem.\n Problem Description:\n Given a set of 2D points (terminals), the goal is to compute additional Steiner points\n such that when you compute the MST over the union of the original terminals and these Steiner points,\n the total length (measured via Euclidean distances) is minimized.\n (Recall, the Euclidean distance between two points (x1, y1) and (x2, y2) is sqrt((x1-x2)^2 + (y1-y2)^2).)\n Input kwargs:\n - points: a list of points, where each point is a tuple of floats (x, y),\n representing the coordinates of an original terminal.\n Returns:\n A dictionary with one key:\n - "steiner_points": a list of (x, y) tuples representing the additional Steiner points.\n It is assumed that the candidate solution’s computed total length can be derived by computing\n the MST over the union of the original terminals and the returned Steiner points.\n """\n points = kwargs.get("points")\n if points is None:\n raise ValueError("Missing input: \'points\' key is required.")\n\n # Placeholder for an actual Steiner tree algorithm:\n # In a real implementation, you would compute extra Steiner points to lower the MST length.\n steiner_points = [] # For now, return no additional Steiner points.\n\n return {"steiner_points": steiner_points}' -task_description = '("Given a set of 2D points (terminals), the goal of the Euclidean Steiner Problem is to compute a "' - - -__all__ = ['ESPEvaluationCB'] - - -class ESPEvaluationCB(Evaluation): - - def __init__(self, - timeout_seconds=50, - **kwargs): - - """ - Args: - None - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - # Load datasets from Hugging Face - dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Euclidean Steiner problem") - self._datasets = {} - for filename in dataset: - # Join all text rows into a single string - text_content = '\n'.join([row['text'] for row in dataset[filename]]) - self._datasets[filename] = text_content - - def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: - return self.evaluate(callable_func) - - def evaluate(self, eva: callable) -> float | None: - ins_cases = [] - for case_id, ins in enumerate(self._datasets.values()): - ins_cases.append(self.load_data(ins)) - - fitness_list = [] - try: - for i in ins_cases: - for j in i: - result = eva(j['points']) - fitness = self.eval_func(points=j['points'], steiner_points=result['steiner_points']) - fitness_list.append(fitness) - - return np.mean(fitness_list) # itself is a maximum problem - - except ValueError as e: - print(e) - return None - - def load_data(self, input_string): - """ - Reads the input string and returns a list of individual test problems. - The input file may contain one or more cases. Each case is expected to follow the format: - Line 1: An integer m representing the number of test problems in the case. - For each test problem: - Line 1: An integer n representing the number of points. - Next n lines: Two space-separated floating-point numbers for the x- and y-coordinates. - Returns: - A list of dictionaries. Each dictionary corresponds to one test problem and contains: - - "points": a list of (x, y) tuples representing the terminals. - The function ignores empty lines and supports multiple cases concatenated in one file. - """ - all_lines = [line.strip() for line in input_string.split('\n')] - - problems = [] - idx = 0 - while idx < len(all_lines): - # Read number of test problems for this case. - try: - m = int(all_lines[idx]) - except Exception as e: - raise ValueError(f"Expected an integer for number of test problems at line {idx + 1}: {e}") - idx += 1 - for i in range(m): - if idx >= len(all_lines): - raise ValueError(f"Insufficient data for test problem {i + 1} in a case.") - try: - n = int(all_lines[idx]) - except Exception as e: - raise ValueError(f"Expected an integer for number of points at line {idx + 1}: {e}") - idx += 1 - pts = [] - for j in range(n): - if idx >= len(all_lines): - raise ValueError(f"Insufficient point data for test problem {i + 1}, point {j + 1}.") - parts = all_lines[idx].split() - if len(parts) < 2: - raise ValueError(f"Test problem {i + 1}: point {j + 1} does not have two coordinates.") - try: - x, y = float(parts[0]), float(parts[1]) - except Exception as e: - raise ValueError(f"Test problem {i + 1}: invalid coordinate format at point {j + 1}: {e}") - pts.append((x, y)) - idx += 1 - problems.append({"points": pts}) - return problems - - def eval_func(self, **kwargs): - """ - Evaluates candidate solutions for the Euclidean Steiner Problem. - Expected kwargs: - - problems: a list of test problems; each test problem is a dict with key "points" - which holds a list of (x, y) tuples representing the original terminals. - - solutions: a list of candidate solutions, one for each test problem. - Each candidate solution is a dict with: - - "steiner_points": a list of (x, y) tuples representing the additional Steiner points. - Evaluation: - For each test problem: - 1. Compute MST_original, the total length of the Minimum Spanning Tree (MST) computed - on the original terminals. - 2. Compute candidate_value, the total length of the MST computed on the union of - the original terminals and the candidate Steiner points. - (Both MST computations use Euclidean distance where the distance between (x1,y1) and (x2,y2) - is sqrt((x1-x2)^2 + (y1-y2)^2).) - 3. A valid candidate must have candidate_value ≤ MST_original (within a small tolerance). - If not, a ValueError is raised. - Otherwise, the quality ratio is computed as candidate_value / MST_original. - (A lower ratio indicates a better solution.) - The overall score is the average of the ratios over all test problems. - Returns: - overall_score (float): The average ratio over all test problems. - """ - import math - - TOL = 1e-6 - - def euclidean_distance(a, b): - return math.sqrt((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2) - - def compute_mst_length(points): - n = len(points) - if n == 0: - return 0.0 - in_mst = [False] * n - min_dist = [float('inf')] * n - min_dist[0] = 0.0 - total = 0.0 - for _ in range(n): - u = -1 - best = float('inf') - for j in range(n): - if not in_mst[j] and min_dist[j] < best: - best = min_dist[j] - u = j - if u == -1: - break - in_mst[u] = True - total += best - for v in range(n): - if not in_mst[v]: - d = euclidean_distance(points[u], points[v]) - if d < min_dist[v]: - min_dist[v] = d - return total - - original_points = kwargs.get("points") - steiner_points = kwargs.get("steiner_points", []) - - # Compute the MST length on the original terminals. - mst_original = compute_mst_length(original_points) - # Compute the candidate tree length as the MST on original terminals plus the candidate Steiner points. - union_points = original_points + steiner_points - candidate_value = compute_mst_length(union_points) - - # The candidate MST must not be longer than the MST of the original terminals. - if candidate_value > mst_original + TOL: - raise ValueError( - f"Candidate solution for problem violates constraint: candidate_value ({candidate_value}) > mst_original ({mst_original}).") - - ratio = candidate_value / mst_original if mst_original > 0 else 1.0 - score = 1.0 - ratio - return score - - def norm_score(self, results): - optimal_scores = { - 'estein250.txt': [0.03] * 15, - 'estein500.txt': [0.03] * 15, - 'estein1000.txt': [0.03] * 15, - 'estein10000.txt': [0.03], - 'estein100.txt': [0.032423065085033675, 0.03483759951901777, 0.034182167020644916, 0.03248098628546203, - 0.03310927379936712, 0.034041259411550784, 0.0397677887027611, 0.035660501862228244, - 0.03502528832071461, 0.03371716889176812, 0.028161233483136594, 0.02687851300146371, - 0.026629423968470123, 0.03565961816027485, 0.027792022641784153], - 'estein10.txt': [0.04299943461594302, 0.004769960182740007, 0.043782084069761584, 0.011502149990875177, - 0.024602813181648697, 0.046077835193320094, 0.04426130719672583, 0.015859056215462353, - 0.02494689613151435, 0.01979275009710557, 0.054874017619661486, 0.005785367498201133, - 0.06167524682759662, 0.05601469362679634, 0.030685355394374447], - 'estein1.txt': [0.03715248999695819, 2.53248940706996e-08, 0.0, 0.0, 9.429250334525019e-05, - 0.023970330309954435, 0.01908676366919071, 2.3915825470233187e-05, 0.13381432532245285, - 0.0295462267220441, 0.056958559892640315, 0.01345616626071433, 0.02629868523014056, - 0.06795895781452022, 0.0017250989103574366, 0.0, 0.0, 0.06725973598503387, - 0.037596415595463006, 0.1338944832237634, 0.026412933267079164, 0.018262573283449823, - 0.02298024555878808, 0.008339962103159793, 0.010573340933293873, 0.001728616561433527, - 0.0028756538345464655, 0.0, 0.013994227369019674, 0.10179537695309238, 0.07520718237458235, - 0.05864794152816455, 0.028893309353272167, 0.012207382373579323, 0.006618274407397151, - 0.023430599555555487, 0.0051899185134780534, 0.007102662306716856, 0.0, 0.04660324576963126, - 0.007969992389563973, 0.014169307452227442, 0.029004689079907386, 0.00890432342316072, - 0.024451928874551054, 0.08931639733333341], - 'estein20.txt': [0.043942725618148826, 0.02299597956072552, 0.03725284493193792, 0.02793871516551827, - 0.03890768508604925, 0.027692754737118963, 0.020995306344934295, 0.047581240549860127, - 0.015508884273023105, 0.035719166517610645, 0.030072471281848645, 0.04369773360827678, - 0.031287634487079496, 0.03339355305720737, 0.01641067343311564], - 'estein30.txt': [0.021869824541884353, 0.027617593078341218, 0.02963480155348497, 0.03714277441461655, - 0.03618276310308932, 0.03148586454727753, 0.03001110334170809, 0.021792810128040463, - 0.03951202278065513, 0.03211942119280953, 0.020834943979018195, 0.03215928284393588, - 0.024799825912022122, 0.04963688935942201, 0.025222898338703503], - 'estein40.txt': [0.02609813221879309, 0.03181546093667176, 0.0257617636108477, 0.024867757483739594, - 0.03878011159818051, 0.033996855652012936, 0.03010133858855013, 0.03474099376571327, - 0.04407499975387952, 0.036479709224781276, 0.018556418029103017, 0.027092227325115847, - 0.032442218263355804, 0.034038355193724, 0.03194768623039035], - 'estein50.txt': [0.026375763293115195, 0.03786259604274811, 0.0368858882909211, 0.02843354067948245, - 0.031562424825947843, 0.03451603250411406, 0.031052490692446644, 0.026042857120256224, - 0.030847821995874658, 0.028427456323692923, 0.024745303837364396, 0.028489474734615827, - 0.03501573784622991, 0.02796869646410083, 0.026754142858155694], - 'estein60.txt': [0.033431902683743187, 0.029312387787789773, 0.03673737294505586, 0.029931036026207947, - 0.038719592946913406, 0.027985371134918502, 0.034956652180465175, 0.02568855514408741, - 0.03291599372153209, 0.027053357949617274, 0.030189122888249265, 0.03666235385539496, - 0.037309702462750116, 0.037371343062245765, 0.03292664563821035], - 'estein70.txt': [0.0281926927308368, 0.03822852322564063, 0.02985749535563431, 0.027371582271915496, - 0.03165937908883898, 0.0319172977507971, 0.03216563529368788, 0.028798544856373787, - 0.02368422096077183, 0.03141890259642621, 0.03168584881094072, 0.03728987267456063, - 0.030740662840068156, 0.028285136466959404, 0.03516404960406827], - 'estein80.txt': [0.028927636103650123, 0.027621437956897088, 0.030045750960559836, 0.02154696015188895, - 0.02208065777296797, 0.028561814513135886, 0.04406481956617947, 0.03559605525407783, - 0.0387928564376363, 0.029134782330045295, 0.029451055665711712, 0.020408525270118272, - 0.032505342891095745, 0.038584240577326456, 0.02859138721565424], - 'estein90.txt': [0.03726927391600421, 0.03352718377112174, 0.02689284725659824, 0.027968087207550618, - 0.040547493724352957, 0.02090677298804755, 0.03565573020648938, 0.030772023917592817, - 0.030029109853112357, 0.031132625096035427, 0.03504603605103018, 0.026598398815443458, - 0.02814959463666722, 0.03392597014885834, 0.029514790002086455] - } - - normed = {} - for case, (scores, error_message) in results.items(): - if case not in optimal_scores: - continue # Skip if there's no optimal score defined. - optimal_list = optimal_scores[case] - normed_scores = [] - # Compute normalized score for each index. - for idx, score in enumerate(scores): - if isinstance(score, (int, float)): - if optimal_list[idx] == 0: - normed_scores.append(1.0) - else: - normed_scores.append(score / optimal_list[idx]) - else: - normed_scores.append(score) - normed[case] = (normed_scores, error_message) - - return normed - - def get_dev(self): - dev = {'estein1.txt': [5, 43, 37, 26, 38, 27, 25, 9, 42, 0, 4, 34, 36, 24, 3, 10, 15, 13, 12, 8, 20, 23, 14], - 'estein10.txt': [6, 3, 12, 2, 8, 9, 5], 'estein100.txt': [2, 11, 0, 7, 13, 6, 4], - 'estein1000.txt': [9, 6, 1, 5, 7, 14, 3], 'estein20.txt': [13, 2, 3, 14, 0, 4, 8], - 'estein250.txt': [1, 14, 6, 10, 2, 11, 4], 'estein30.txt': [3, 12, 9, 11, 4, 2, 14], - 'estein40.txt': [14, 13, 3, 6, 10, 7, 2], 'estein50.txt': [4, 7, 8, 5, 9, 6, 0], - 'estein500.txt': [12, 11, 4, 8, 1, 9, 0], 'estein60.txt': [14, 0, 2, 8, 12, 9, 7], - 'estein70.txt': [12, 10, 0, 14, 1, 11, 2], 'estein80.txt': [9, 12, 1, 3, 2, 13, 6], - 'estein90.txt': [14, 3, 4, 8, 2, 5, 10]} - - return dev - - - - -# Task configuration for benchmark task -ENTRY_NAME = 'solve' -FUNCTION_SIGNATURE = 'def solve(...):' -IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' -TASK_DESCRIPTION = '("Given a set of 2D points (terminals), the goal of the Euclidean Steiner Problem is to compute a "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("Given a set of 2D points (terminals), the goal of the Euclidean Steiner Problem is to compute a "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(points: list) -> dict:\n """\n Solves a single instance of the Euclidean Steiner Problem.\n Problem Description:\n Given a set of 2D points (terminals), the goal is to compute additional Steiner points\n such that when you compute the MST over the union of the original terminals and these Steiner points,\n the total length (measured via Euclidean distances) is minimized.\n (Recall, the Euclidean distance between two points (x1, y1) and (x2, y2) is sqrt((x1-x2)^2 + (y1-y2)^2).)\n Input kwargs:\n - points: a list of points, where each point is a tuple of floats (x, y),\n representing the coordinates of an original terminal.\n Returns:\n A dictionary with one key:\n - "steiner_points": a list of (x, y) tuples representing the additional Steiner points.\n It is assumed that the candidate solution’s computed total length can be derived by computing\n the MST over the union of the original terminals and the returned Steiner points.\n """\n points = kwargs.get("points")\n if points is None:\n raise ValueError("Missing input: \'points\' key is required.")\n\n # Placeholder for an actual Steiner tree algorithm:\n # In a real implementation, you would compute extra Steiner points to lower the MST length.\n steiner_points = [] # For now, return no additional Steiner points.\n\n return {"steiner_points": steiner_points}' -EVAL_CLASS_NAME = 'ESPEvaluationCB' -EVAL_KWARGS = {'timeout_seconds': 60} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_euclidean_steiner_problem/paras.yaml b/examples/benchmark_tasks/optimization_euclidean_steiner_problem/paras.yaml deleted file mode 100644 index e05bc700..00000000 --- a/examples/benchmark_tasks/optimization_euclidean_steiner_problem/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: ESPEvaluationCB -timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_flow_shop_scheduling/__init__.py b/examples/benchmark_tasks/optimization_flow_shop_scheduling/__init__.py deleted file mode 100644 index 6d2ffe33..00000000 --- a/examples/benchmark_tasks/optimization_flow_shop_scheduling/__init__.py +++ /dev/null @@ -1,388 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_flow_shop_scheduling -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# References: -# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language -# model agents in algorithm search for combinatorial optimization. -# arXiv preprint arXiv:2504.04310 (2025). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -from typing import Any -import numpy as np -from llm4ad_loader import Evaluation -from llm4ad_loader import load_subdir_as_text -# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader -# from llm4ad.task.optimization.co_bench.flow_shop_scheduling_co_bench.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n: int, m: int, matrix: list) -> dict:\n """\n Solves the flow shop scheduling problem.\n Input kwargs:\n - n (int): Number of jobs.\n - m (int): Number of machines.\n - matrix (list of list of int): Processing times for each job, where each sublist\n contains m integers (processing times for machines 0 through m-1).\n Evaluation Metric:\n The solution is evaluated by its makespan, which is the completion time of the last\n job on the last machine computed by the classical flow shop recurrence.\n Returns:\n dict: A dictionary with a single key \'job_sequence\' whose value is a permutation\n (1-indexed) of the job indices. For example, for 4 jobs, a valid return is:\n {\'job_sequence\': [1, 3, 2, 4]}\n Note: This is a placeholder implementation.\n """\n # Placeholder: simply return the identity permutation.\n return {\'job_sequence\': list(range(1, kwargs[\'n\'] + 1))}' -task_description = '("Given n jobs and m machines, the goal of the flow shop scheduling problem is to determine "' - - -__all__ = ['FSSEvaluationCB'] - - -class FSSEvaluationCB(Evaluation): - - def __init__(self, - timeout_seconds=50, - **kwargs): - - """ - Args: - None - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - # Load datasets from Hugging Face - dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Flow shop scheduling") - self._datasets = {} - for filename in dataset: - # Join all text rows into a single string - text_content = '\n'.join([row['text'] for row in dataset[filename]]) - self._datasets[filename] = text_content - - def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: - return self.evaluate(callable_func) - - def evaluate(self, eva: callable) -> float | None: - ins_cases = [] - for case_id, ins in enumerate(self._datasets.values()): - ins_cases.append(self.load_data(ins)) - - fitness_list = [] - try: - for i in ins_cases: - for j in i: - result = eva(j['n'], j['m'], j['matrix']) - fitness = self.eval_func(n=j['n'], m=j['m'], matrix=j['matrix'], job_sequence=result['job_sequence'], lower_bound=j['lower_bound'], upper_bound=j['upper_bound']) - fitness_list.append(fitness) - - return np.mean(fitness_list) - - except ValueError as e: - print(e) - return None - - def load_data(self, input_string): - """ - Reads a file containing multiple test cases for the flow shop scheduling problem. - The file format: - - A header line: "number of jobs, number of machines, initial seed, upper bound and lower bound :" - - Next line: five numbers (n, m, seed, upper_bound, lower_bound) - - A line that starts with "processing times :" - - Then m lines of processing times. Each line contains n integers (processing times for one machine across all jobs). - The function returns a list of test cases, where each test case is a dictionary with: - - "n" (int): number of jobs - - "m" (int): number of machines - - "matrix" (list of list of int): processing times in a n x m matrix (each row corresponds to a job) - - "upper_bound" (int) - - "lower_bound" (int) - """ - test_cases = [] - all_lines = [line.strip() for line in input_string.split('\n')] - - i = 0 - while i < len(all_lines): - line = all_lines[i].strip() - # Look for the header line indicating a new test case. - if line.startswith("number of jobs"): - # Skip to the line with the five numbers. - i += 1 - while i < len(all_lines) and all_lines[i].strip() == "": - i += 1 - if i >= len(all_lines): - break - # The header values line (n, m, seed, upper_bound, lower_bound) - header_tokens = all_lines[i].strip().split() - if len(header_tokens) < 5: - raise ValueError(f"Expected at least 5 numbers in header, got: {all_lines[i].strip()}") - n = int(header_tokens[0]) - m = int(header_tokens[1]) - # initial seed is ignored - upper_bound = int(header_tokens[3]) - lower_bound = int(header_tokens[4]) - i += 1 - - # Skip empty lines until we find the processing times label. - while i < len(all_lines) and all_lines[i].strip() == "": - i += 1 - # Expect a line that starts with "processing times" - if i < len(all_lines) and all_lines[i].strip().lower().startswith("processing times"): - i += 1 - else: - raise ValueError("Expected 'processing times' line not found.") - - # Read m lines containing the processing times (each line should have n integers) - machine_times = [] - for _ in range(m): - while i < len(all_lines) and all_lines[i].strip() == "": - i += 1 - if i >= len(all_lines): - raise ValueError("Unexpected end of file while reading processing times.") - row_tokens = all_lines[i].strip().split() - if len(row_tokens) != n: - raise ValueError( - f"Expected {n} numbers in processing times line, got {len(row_tokens)} in line: {all_lines[i].strip()}") - row = [int(token) for token in row_tokens] - machine_times.append(row) - i += 1 - - # The data is read per machine, so transpose it to obtain a list of n jobs, - # where each job is a list of m processing times. - matrix = [[machine_times[machine][job] for machine in range(m)] for job in range(n)] - - # Add the test case dictionary. - test_cases.append({ - "n": n, - "m": m, - "matrix": matrix, - "upper_bound": upper_bound, - "lower_bound": lower_bound - }) - else: - i += 1 - - return test_cases - - # def load_flowshop1(self, input_path): - # """ - # Reads the input file for one or more flow shop scheduling instances. - # The file may contain multiple cases. For each case, the instance is defined by: - # - A header section (to be skipped) until a line with exactly two integers is found. - # - The two integers define n (number of jobs) and m (number of machines). - # - Then the next n nonempty lines (ignoring blank lines and lines starting with '+') - # contain the job descriptions. Each job line must contain at least 2*m integers, - # which are interpreted as (machine, processing_time) pairs. - # - The processing times for each job are collected and ordered by machine number (0 to m-1). - # Returns: - # list: A list of dictionaries, each corresponding to one instance/case with keys: - # - 'n': number of jobs (int) - # - 'm': number of machines (int) - # - 'matrix': list of list of int (each sublist contains processing times for one job) - # """ - # if 'tai' in input_path: - # return load_tai(input_path) - # - # cases = [] - # try: - # with open(input_path, 'r') as f: - # lines = f.readlines() - # except Exception as e: - # raise Exception("Error reading input file: " + str(e)) - # - # line_index = 0 - # total_lines = len(lines) - # - # while line_index < total_lines: - # # Search for a valid instance size line (exactly two integers) - # instance_found = False - # while line_index < total_lines: - # line = lines[line_index].strip() - # line_index += 1 - # if not line: - # continue - # tokens = line.split() - # if len(tokens) == 2: - # try: - # n_val = int(tokens[0]) - # m_val = int(tokens[1]) - # n, m = n_val, m_val - # instance_found = True - # break - # except ValueError: - # continue - # if not instance_found: - # break # No more instances found - # - # matrix = [] - # job_count = 0 - # # Read next n valid job lines (skip blank and lines starting with '+') - # while line_index < total_lines and job_count < n: - # line = lines[line_index].strip() - # line_index += 1 - # if not line or line.startswith('+'): - # continue - # tokens = line.split() - # if len(tokens) < 2 * m: - # raise Exception( - # f"Error: Expected at least {2 * m} numbers in a job line, got {len(tokens)} in line: {line}") - # # Consider only the first 2*m tokens in case of extra tokens. - # tokens = tokens[:2 * m] - # try: - # numbers = [int(token) for token in tokens] - # except ValueError: - # raise Exception("Error: Non-integer token encountered in job line.") - # - # job_data = {} - # for i in range(0, len(numbers), 2): - # machine = numbers[i] - # proc_time = numbers[i + 1] - # if machine < 0 or machine >= m: - # raise Exception(f"Error: Invalid machine number {machine} (expected between 0 and {m - 1}).") - # if machine in job_data: - # raise Exception(f"Error: Duplicate machine number {machine} in job line.") - # job_data[machine] = proc_time - # if set(job_data.keys()) != set(range(m)): - # raise Exception("Error: Not all machine numbers are present in job line.") - # job_proc = [job_data[i] for i in range(m)] - # matrix.append(job_proc) - # job_count += 1 - # - # if job_count != n: - # raise Exception("Error: Number of job lines read does not match the expected number of jobs.") - # - # cases.append({'n': n, 'm': m, 'matrix': matrix}) - # - # return cases - - def eval_func(self, **kwargs): - """ - Evaluates a flow shop scheduling solution for a single instance. - Input kwargs must include: - - n (int): Number of jobs. - - m (int): Number of machines. - - matrix (list of list of int): Processing times matrix. - - job_sequence (list of int): A 1-indexed permutation of job indices, as returned by solve. - The evaluation metric (makespan) is computed using the classical flow shop recurrence: - - C[0][0] = processing_time(job_1, machine_0) - - For the first job on machines j > 0: C[0][j] = C[0][j-1] + processing_time(job_1, machine_j) - - For subsequent jobs on the first machine: C[i][0] = C[i-1][0] + processing_time(job_(i+1), machine_0) - - For all other entries: C[i][j] = max(C[i-1][j], C[i][j-1]) + processing_time(job_(i+1), machine_j) - Returns: - float: The computed makespan for the provided solution. - """ - n = kwargs.get('n') - m = kwargs.get('m') - matrix = kwargs.get('matrix') - job_sequence = kwargs.get('job_sequence') - - # Validate the job sequence: it must be a permutation of [1, 2, ..., n] - if not job_sequence or len(job_sequence) != n or set(job_sequence) != set(range(1, n + 1)): - raise Exception(f"Error: Job sequence is not a valid permutation of job indices 1 to {n}.") - - # Convert job sequence from 1-indexed to 0-indexed. - seq_zero = [job - 1 for job in job_sequence] - - # Initialize the completion time table. - completion = [[0] * m for _ in range(n)] - - for i in range(n): - for j in range(m): - proc_time = matrix[seq_zero[i]][j] - if i == 0 and j == 0: - completion[i][j] = proc_time - elif i == 0: - completion[i][j] = completion[i][j - 1] + proc_time - elif j == 0: - completion[i][j] = completion[i - 1][j] + proc_time - else: - completion[i][j] = max(completion[i - 1][j], completion[i][j - 1]) + proc_time - - makespan = completion[-1][-1] - - score = kwargs['lower_bound'] / makespan - # score = kwargs['upper_bound'] / makespan - return score - - def get_dev(self): - dev = {'tai100_10.txt': [1, 7, 4, 9, 8], 'tai100_20.txt': [1, 0, 2, 6, 8], 'tai100_5.txt': [9, 8, 5, 6, 3], - 'tai200_10.txt': [5, 9, 4, 1, 0], 'tai200_20.txt': [9, 4, 7, 6, 0], 'tai20_10.txt': [8, 9, 2, 5, 4], - 'tai20_20.txt': [4, 8, 9, 7, 6], 'tai20_5.txt': [7, 3, 9, 8, 0], 'tai500_20.txt': [3, 0, 6, 7, 4], - 'tai50_10.txt': [6, 4, 3, 8, 7], 'tai50_20.txt': [1, 7, 4, 6, 2], 'tai50_5.txt': [6, 7, 2, 4, 8]} - - return dev - - - - - -# Task configuration for benchmark task -ENTRY_NAME = 'solve' -FUNCTION_SIGNATURE = 'def solve(...):' -IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' -TASK_DESCRIPTION = '("Given n jobs and m machines, the goal of the flow shop scheduling problem is to determine "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("Given n jobs and m machines, the goal of the flow shop scheduling problem is to determine "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n: int, m: int, matrix: list) -> dict:\n """\n Solves the flow shop scheduling problem.\n Input kwargs:\n - n (int): Number of jobs.\n - m (int): Number of machines.\n - matrix (list of list of int): Processing times for each job, where each sublist\n contains m integers (processing times for machines 0 through m-1).\n Evaluation Metric:\n The solution is evaluated by its makespan, which is the completion time of the last\n job on the last machine computed by the classical flow shop recurrence.\n Returns:\n dict: A dictionary with a single key \'job_sequence\' whose value is a permutation\n (1-indexed) of the job indices. For example, for 4 jobs, a valid return is:\n {\'job_sequence\': [1, 3, 2, 4]}\n Note: This is a placeholder implementation.\n """\n # Placeholder: simply return the identity permutation.\n return {\'job_sequence\': list(range(1, kwargs[\'n\'] + 1))}' -EVAL_CLASS_NAME = 'FSSEvaluationCB' -EVAL_KWARGS = {'timeout_seconds': 60} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_flow_shop_scheduling/paras.yaml b/examples/benchmark_tasks/optimization_flow_shop_scheduling/paras.yaml deleted file mode 100644 index 63bf302e..00000000 --- a/examples/benchmark_tasks/optimization_flow_shop_scheduling/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: FSSEvaluationCB -timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_generalised_assignment_problem/__init__.py b/examples/benchmark_tasks/optimization_generalised_assignment_problem/__init__.py deleted file mode 100644 index 776f2121..00000000 --- a/examples/benchmark_tasks/optimization_generalised_assignment_problem/__init__.py +++ /dev/null @@ -1,340 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_generalised_assignment_problem -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# References: -# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language -# model agents in algorithm search for combinatorial optimization. -# arXiv preprint arXiv:2504.04310 (2025). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -from typing import Any -import numpy as np -from llm4ad_loader import Evaluation -from llm4ad_loader import load_subdir_as_text -# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader -# from llm4ad.task.optimization.co_bench.generalised_assignment_problem_co_bench.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(m:int, n:int, cost_matrix:list, consumption_matrix:list, capacities:list, problem_type:str=\'max\') -> dict:\n """\n Solve the Generalised Assignment Problem (GAP) for a single case.\n Input arguments (passed as keyword arguments):\n - m: (int) Number of agents.\n - n: (int) Number of jobs.\n - cost_matrix: (list of list of float) A matrix of size m×n where cost_matrix[i][j]\n represents the cost of assigning job j to agent i.\n - consumption_matrix: (list of list of float) A matrix of size m×n where consumption_matrix[i][j]\n represents the resource consumed when job j is assigned to agent i.\n - capacities: (list of float) A list of length m containing the resource capacity for each agent.\n - problem_type: (str, optional) Indicates whether the problem is a \'max\' or \'min\' problem.\n Defaults to \'max\'.\n Returns:\n A dictionary with the key \'assignments\' whose value is a list of n integers.\n Each integer is an agent number (using 1-indexing) that is assigned to the corresponding job.\n """\n # For illustration purposes, we provide a trivial solution that assigns every job to agent 1.\n assignments = [1] * kwargs[\'n\']\n return {\'assignments\': assignments}' -task_description = '("The Generalized Assignment Problem (GAP) involves assigning \\( n \\) jobs to \\( m \\) agents such "' - - -__all__ = ['GAPEvaluationCB'] - - -class GAPEvaluationCB(Evaluation): - - def __init__(self, - timeout_seconds=50, - **kwargs): - - """ - Args: - None - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - # Load datasets from Hugging Face - dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Generalised assignment problem") - self._datasets = {} - for filename in dataset: - # Join all text rows into a single string - text_content = '\n'.join([row['text'] for row in dataset[filename]]) - self._datasets[filename] = text_content - - def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: - return self.evaluate(callable_func) - - def evaluate(self, eva: callable) -> float | None: - ins_cases = [] - for case_id, ins in enumerate(self._datasets.values()): - ins_cases.append(self.load_data(ins)) - - fitness_list = [] - try: - for i in ins_cases: - for j in i: - result = eva(j['m'], j['n'], j['cost_matrix'], j['consumption_matrix'], j['capacities'], j['problem_type']) - fitness = self.eval_func(j['m'], j['n'], j['cost_matrix'], j['consumption_matrix'], j['capacities'], result['assignments']) - fitness_list.append(fitness) - - return -np.mean(fitness_list) - - except ValueError as e: - print(e) - return None - - def load_data(self, input_string): - """ - Load and parse the input file for the Generalised Assignment Problem (GAP). - The input is expected to be a whitespace‐delimited text file with the following format: - - The first token is an integer P, indicating the number of cases. - - For each case, the following tokens are provided sequentially: - • Two integers: m (number of agents) and n (number of jobs). - • m×n numbers representing the cost matrix (row by row). - • m×n numbers representing the resource consumption matrix (row by row). - • m numbers representing the capacities for each agent. - Parameters: - input_file_path: (str) Path to the input text file. - Returns: - A list of dictionaries. Each dictionary corresponds to one case and contains the keys: - 'm', 'n', 'cost_matrix', 'consumption_matrix', and 'capacities'. - """ - cases = [] - try: - tokens = input_string.split() - except Exception as e: - raise Exception("Error reading input file: " + str(e)) - - ptr = 0 - try: - P = int(tokens[ptr]) - ptr += 1 - except Exception as e: - raise Exception("Error parsing the number of cases: " + str(e)) - - for _ in range(P): - try: - m = int(tokens[ptr]) - n = int(tokens[ptr + 1]) - ptr += 2 - except Exception as e: - raise Exception("Error parsing m and n for a case: " + str(e)) - - cost_matrix = [] - for i in range(m): - row = [] - for j in range(n): - try: - row.append(float(tokens[ptr])) - except Exception as e: - raise Exception("Error reading cost matrix value: " + str(e)) - ptr += 1 - cost_matrix.append(row) - - consumption_matrix = [] - for i in range(m): - row = [] - for j in range(n): - try: - row.append(float(tokens[ptr])) - except Exception as e: - raise Exception("Error reading consumption matrix value: " + str(e)) - ptr += 1 - consumption_matrix.append(row) - - capacities = [] - for i in range(m): - try: - capacities.append(float(tokens[ptr])) - except Exception as e: - raise Exception("Error reading capacity value: " + str(e)) - ptr += 1 - # Determine problem type based on content analysis or default to 'max' - # Since we don't have file name, we'll default to 'max' for now - problem_type = 'max' - - case = { - 'm': m, - 'n': n, - 'cost_matrix': cost_matrix, - 'consumption_matrix': consumption_matrix, - 'capacities': capacities, - 'problem_type': problem_type - } - cases.append(case) - - return cases - - def eval_func(self, m, n, cost_matrix, consumption_matrix, capacities, assignments, **kwargs): - """ - Evaluate a solution for a single case of the Generalised Assignment Problem (GAP). - Parameters: - - m: (int) Number of agents. - - n: (int) Number of jobs. - - cost_matrix: (list of list of float) The cost matrix of size m×n. - - consumption_matrix: (list of list of float) The resource consumption matrix of size m×n. - - capacities: (list of float) The resource capacities for each of the m agents. - - assignments: (list of int) A list of n integers (using 1-indexing) representing the agent - assigned to each job. - Evaluation: - - TotalCost is computed as the sum of cost_matrix[agent-1][j] for each job j. - - For each agent i, ResourceConsumption[i] is the sum of consumption_matrix[i][j] for jobs assigned to agent i. - - If an agent’s ResourceConsumption exceeds its capacity, a ValueError is raised. - - For a maximization problem, the score is simply the TotalCost. - (For minimization problems, you might use the negative of TotalCost.) - Returns: - A numeric score (float) evaluating the quality of the solution. - """ - total_cost = 0.0 - agent_consumption = [0.0] * m - - # Check if the number of assignments matches the number of jobs. - if len(assignments) != n: - raise ValueError("Malformed solution: number of assignments does not match the number of jobs.") - - # Process each job. - for j in range(n): - agent = assignments[j] - # Check if the assigned agent is valid (using 1-indexing). - if agent < 1 or agent > m: - raise ValueError(f"Invalid agent number {agent} for job {j}. Must be between 1 and {m}.") - agent_index = agent - 1 - total_cost += cost_matrix[agent_index][j] - agent_consumption[agent_index] += consumption_matrix[agent_index][j] - - # Check capacity constraints for each agent. - for i in range(m): - if agent_consumption[i] > capacities[i]: - raise ValueError( - f"Capacity constraint violated for agent {i + 1}: consumption {agent_consumption[i]} exceeds capacity {capacities[i]}.") - - # For a feasible solution, return the total cost as the score (for a maximization problem). - return total_cost - - def norm_score(self, results): - # Pre-defined optimal scores for each test case. - optimal_scores = { - "gap1.txt": [336.0, 327.0, 339.0, 341.0, 326.0], - "gap10.txt": [958.0, 963.0, 960.0, 947.0, 947.0], - "gap11.txt": [1139.0, 1178.0, 1195.0, 1171.0, 1171.0], - "gap12.txt": [1451.0, 1449.0, 1433.0, 1447.0, 1446.0], - "gap2.txt": [434.0, 436.0, 420.0, 419.0, 428.0], - "gap3.txt": [580.0, 564.0, 573.0, 570.0, 564.0], - "gap4.txt": [656.0, 644.0, 673.0, 647.0, 664.0], - "gap5.txt": [563.0, 558.0, 564.0, 568.0, 559.0], - "gap6.txt": [761.0, 759.0, 758.0, 752.0, 747.0], - "gap7.txt": [942.0, 949.0, 968.0, 945.0, 951.0], - "gap8.txt": [1133.0, 1134.0, 1141.0, 1117.0, 1127.0], - "gap9.txt": [709.0, 717.0, 712.0, 723.0, 706.0], - "gapa.txt": [1698, 3235, 1360, 2623, 1158, 2339], - "gapb.txt": [1843, 3553, 1407, 2831, 1166, 2340], - "gapc.txt": [1931, 3458, 1403, 2814, 1244, 2397], - "gapd.txt": [6373, 12796, 6379, 12601, 6269, 12452], - } - - normed = {} - for case, (scores, error_message) in results.items(): - if case not in optimal_scores: - continue # Skip if there's no optimal score defined. - optimal_list = optimal_scores[case] - normed_scores = [] - # Compute normalized score for each index. - if 'gapa.txt' in case or 'gapb.txt' in case or 'gapc.txt' in case or 'gapd.txt' in case: - problem_type = 'min' - else: - problem_type = 'max' - for idx, score in enumerate(scores): - if isinstance(score, (int, float)): - if problem_type == 'min': - normed_scores.append(optimal_list[idx] / score) - else: - normed_scores.append(score / optimal_list[idx]) - else: - normed_scores.append(score) - normed[case] = (normed_scores, error_message) - - return normed - - def get_dev(self): - dev = {'gap1.txt': [2, 3], 'gap10.txt': [2, 0], 'gap11.txt': [3, 0], 'gap12.txt': [3, 1], 'gap2.txt': [2, 1], - 'gap3.txt': [2, 1], 'gap4.txt': [2, 0], 'gap5.txt': [1, 4], 'gap6.txt': [2, 0], 'gap7.txt': [4, 1], - 'gap8.txt': [1, 4], 'gap9.txt': [1, 4], 'gapa.txt': [4, 0, 2], 'gapb.txt': [3, 2, 0], - 'gapc.txt': [3, 2, 0], - 'gapd.txt': [5, 4, 1]} - - return dev - - - - - -# Task configuration for benchmark task -ENTRY_NAME = 'solve' -FUNCTION_SIGNATURE = 'def solve(...):' -IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' -TASK_DESCRIPTION = '("The Generalized Assignment Problem (GAP) involves assigning \\( n \\) jobs to \\( m \\) agents such "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The Generalized Assignment Problem (GAP) involves assigning \\( n \\) jobs to \\( m \\) agents such "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(m:int, n:int, cost_matrix:list, consumption_matrix:list, capacities:list, problem_type:str=\'max\') -> dict:\n """\n Solve the Generalised Assignment Problem (GAP) for a single case.\n Input arguments (passed as keyword arguments):\n - m: (int) Number of agents.\n - n: (int) Number of jobs.\n - cost_matrix: (list of list of float) A matrix of size m×n where cost_matrix[i][j]\n represents the cost of assigning job j to agent i.\n - consumption_matrix: (list of list of float) A matrix of size m×n where consumption_matrix[i][j]\n represents the resource consumed when job j is assigned to agent i.\n - capacities: (list of float) A list of length m containing the resource capacity for each agent.\n - problem_type: (str, optional) Indicates whether the problem is a \'max\' or \'min\' problem.\n Defaults to \'max\'.\n Returns:\n A dictionary with the key \'assignments\' whose value is a list of n integers.\n Each integer is an agent number (using 1-indexing) that is assigned to the corresponding job.\n """\n # For illustration purposes, we provide a trivial solution that assigns every job to agent 1.\n assignments = [1] * kwargs[\'n\']\n return {\'assignments\': assignments}' -EVAL_CLASS_NAME = 'GAPEvaluationCB' -EVAL_KWARGS = {'timeout_seconds': 60} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_generalised_assignment_problem/paras.yaml b/examples/benchmark_tasks/optimization_generalised_assignment_problem/paras.yaml deleted file mode 100644 index b2f09843..00000000 --- a/examples/benchmark_tasks/optimization_generalised_assignment_problem/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: GAPEvaluationCB -timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_graph_colouring/__init__.py b/examples/benchmark_tasks/optimization_graph_colouring/__init__.py deleted file mode 100644 index 438e60c6..00000000 --- a/examples/benchmark_tasks/optimization_graph_colouring/__init__.py +++ /dev/null @@ -1,372 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_graph_colouring -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# References: -# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language -# model agents in algorithm search for combinatorial optimization. -# arXiv preprint arXiv:2504.04310 (2025). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -from typing import Any -import numpy as np -from llm4ad_loader import Evaluation -from llm4ad_loader import load_subdir_as_text -# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader -# from llm4ad.task.optimization.co_bench.graph_colouring_co_bench.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n: int, edges: list, adjacency: dict) -> dict:\n """\n Problem:\n Given a graph in DIMACS format (with vertices, edges, and an adjacency list),\n assign a positive integer color to each vertex (1..n) so that no two adjacent vertices\n share the same color. The objective is to use as few colors as possible.\n Input kwargs:\n The keyword arguments are expected to include at least:\n - n: int (int), the number of vertices.\n - edges: list of (u, v) tuples (tuple of int (int), int (int)) representing edges.\n - adjacency: dict mapping each vertex (1..n) (int) to a set of its adjacent vertices (set of int).\n Evaluation Metric:\n Let k be the number of distinct colors used.\n For every edge connecting two vertices with the same color, count one conflict ( C ).\n If C > 0 , the solution is invalid and receives no score.\n Otherwise, the score is simply k , with a lower k being better.\n Returns:\n A dictionary representing the solution, mapping each vertex_id (1..n) to a positive integer color.\n """\n ## placeholder.\n return {} # Replace {} with the actual solution dictionary when implemented.' -task_description = '("Given a graph in DIMACS format with vertices, edges, and an adjacency list, the goal is to "' - - -__all__ = ['GCEvaluationCB'] - - -class GCEvaluationCB(Evaluation): - - def __init__(self, - timeout_seconds=50, - **kwargs): - - """ - Args: - None - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - # Load datasets from Hugging Face - dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Graph colouring") - self._datasets = {} - for filename in dataset: - # Join all text rows into a single string - text_content = '\n'.join([row['text'] for row in dataset[filename]]) - self._datasets[filename] = text_content - - def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: - return self.evaluate(callable_func) - - def evaluate(self, eva: callable) -> float | None: - ins_cases = [] - for case_id, ins in enumerate(self._datasets.values()): - ins_cases.append(self.load_data(ins)) - - fitness_list = [] - try: - for i in ins_cases: - for j in i: - result = eva(j['n'], j['edges'], j['adjacency']) - fitness = self.eval_func(n=j['n'], adjacency=j['adjacency'], result=result) - fitness_list.append(fitness) - - return -np.mean(fitness_list) - - except ValueError as e: - print(e) - return None - - def load_data(self, input_string): - """ - Reads the input DIMACS string, which may contain one or more cases. - Each case is separated by a header line (starting with "p"). For each case, the function: - - Ignores blank lines and comment lines (starting with "c"). - - Parses the header line ("p edge ") if present; if absent, determines n from edge listings. - - Parses each edge line (starting with "e") to extract the edge (u,v). - - Builds an adjacency list mapping each vertex (from 1 to n) to its adjacent vertices. - Returns: - A list where each element is a dictionary containing the data for one case. - Each dictionary has at least the following keys: - - 'n': int, number of vertices. - - 'edges': list of (u, v) tuples. - - 'adjacency': dict mapping vertex (1..n) to a set of adjacent vertices. - """ - - all_lines = [line.strip() for line in input_string.split('\n')] - - cases = [] - current_case_lines = [] - found_header = False - - # Separate file content into multiple cases based on header lines ("p ...") - for line in all_lines: - stripped = line.strip() - if not stripped or stripped.startswith("c"): - continue # skip blank lines and comments - if stripped.startswith("p"): - # Start of a new case: if current_case_lines not empty, finish previous case. - if current_case_lines: - cases.append(current_case_lines) - current_case_lines = [] - found_header = True - current_case_lines.append(stripped) - if current_case_lines: - cases.append(current_case_lines) - - # If no header line was found in the entire file, treat entire file as one case. - if not found_header and not cases: - # Filter out blank lines and comments from all_lines and treat as single case. - cases = [[line for line in all_lines if line.strip() and not line.strip().startswith("c")]] - - case_data_list = [] - # Process each case's lines. - for case_lines in cases: - n = None # number of vertices - edges = [] - vertices_found = set() - - for line in case_lines: - parts = line.split() - if parts[0] == "p": - # Expected format: p edge - if len(parts) < 4: - raise ValueError("Problem line malformed: " + line) - try: - n = int(parts[2]) - except Exception as e: - raise ValueError("Error parsing problem line: " + str(e)) - elif parts[0] == "e": - # Expected format: e - if len(parts) < 3: - raise ValueError("Edge line malformed: " + line) - try: - u = int(parts[1]) - v = int(parts[2]) - edges.append((u, v)) - vertices_found.update([u, v]) - except Exception as e: - raise ValueError("Error parsing edge line: " + str(e)) - # If n was not provided in the header, use the maximum vertex id found. - if n is None: - if vertices_found: - n = max(vertices_found) - else: - raise ValueError("No vertex information found in input.") - - # Build adjacency list. - adjacency = {i: set() for i in range(1, n + 1)} - for (u, v) in edges: - if u in adjacency: - adjacency[u].add(v) - if v in adjacency: - adjacency[v].add(u) - - case_data_list.append({ - 'n': n, - 'edges': edges, - 'adjacency': adjacency - }) - - return case_data_list - - def eval_func(self, **kwargs): - """ - Evaluates a solution for a single case. - Expected kwargs: - - 'n': int, number of vertices. - - 'adjacency': dict mapping each vertex (1..n) to a set of adjacent vertices. - - Plus all key-value pairs from the solution dictionary produced by solve, - mapping vertex ids to assigned positive integer colors. - Evaluation: - - Verifies that every vertex from 1 to n is assigned a positive integer color. - - For each edge (u,v), if the assigned colors are the same, counts as a conflict. - - Let C be the total number of conflicts and k be the number of distinct colors used. - - If C > 0, the solution is invalid and an error is raised. - - If C == 0, the score is simply k (lower is better). - Returns: - A scalar score (integer or float) representing the evaluation of the solution. - """ - # Extract expected case data. - try: - n = kwargs['n'] - adjacency = kwargs['adjacency'] - except KeyError as e: - raise KeyError("Missing required case data key: " + str(e)) - - # The solution should include an assignment for every vertex (1..n). - result = kwargs.get('result', {}) - solution = {k: v for k, v in result.items() if isinstance(k, int) or (isinstance(k, str) and k.isdigit())} - # Normalize keys to integers. - normalized_solution = {} - for key, value in solution.items(): - try: - vertex = int(key) - except Exception: - continue - normalized_solution[vertex] = value - - expected_vertices = set(range(1, n + 1)) - if set(normalized_solution.keys()) != expected_vertices: - raise ValueError("The solution must assign a color to every vertex from 1 to " + str(n)) - - # Check that every color is a positive integer. - for v, color in normalized_solution.items(): - if not (isinstance(color, int) and color >= 1): - raise ValueError("Invalid color for vertex {}: {}. Colors must be positive integers.".format(v, color)) - - # Count conflicts: for each edge, if both endpoints have the same color, count a conflict. - conflict_count = 0 - for u in range(1, n + 1): - for v in adjacency[u]: - if u < v: # count each edge only once - if normalized_solution[u] == normalized_solution[v]: - conflict_count += 1 - - if conflict_count > 0: - raise ValueError("Invalid coloring: {} conflict(s) found.".format(conflict_count)) - - num_colors = len(set(normalized_solution.values())) - score = num_colors - - return score - - def norm_score(self, results): - optimal_scores = { - "gcol1.txt": [15], - "gcol10.txt": [15], - "gcol11.txt": [15], - "gcol12.txt": [15], - "gcol13.txt": [15], - "gcol14.txt": [15], - "gcol15.txt": [15], - "gcol16.txt": [15], - "gcol17.txt": [15], - "gcol18.txt": [15], - "gcol19.txt": [15], - "gcol2.txt": [15], - "gcol20.txt": [15], - "gcol21.txt": [34], - "gcol22.txt": [34], - "gcol23.txt": [34], - "gcol24.txt": [34], - "gcol25.txt": [34], - "gcol26.txt": [34], - "gcol27.txt": [34], - "gcol28.txt": [34], - "gcol29.txt": [34], - "gcol3.txt": [15], - "gcol30.txt": [34], - "gcol4.txt": [15], - "gcol5.txt": [15], - "gcol6.txt": [15], - "gcol7.txt": [15], - "gcol8.txt": [15], - "gcol9.txt": [15] - } - - normed = {} - for case, (scores, error_message) in results.items(): - if case not in optimal_scores: - continue # Skip if there's no optimal score defined. - optimal_list = optimal_scores[case] - normed_scores = [] - # Compute normalized score for each index. - for idx, score in enumerate(scores): - if isinstance(score, (int, float)): - normed_scores.append(optimal_list[idx] / score) - else: - normed_scores.append(score) - normed[case] = (normed_scores, error_message) - - return normed - - def get_dev(self): - dev = {'gcol1.txt': [0], 'gcol11.txt': [0], 'gcol13.txt': [0], - 'gcol15.txt': [0], 'gcol17.txt': [0], 'gcol19.txt': [0], - 'gcol21.txt': [0], 'gcol23.txt': [0], 'gcol25.txt': [0], - 'gcol27.txt': [0], 'gcol29.txt': [0], 'gcol3.txt': [0], - 'gcol5.txt': [0], 'gcol7.txt': [0], 'gcol9.txt': [0]} - - return dev - - - - - -# Task configuration for benchmark task -ENTRY_NAME = 'solve' -FUNCTION_SIGNATURE = 'def solve(...):' -IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' -TASK_DESCRIPTION = '("Given a graph in DIMACS format with vertices, edges, and an adjacency list, the goal is to "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("Given a graph in DIMACS format with vertices, edges, and an adjacency list, the goal is to "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n: int, edges: list, adjacency: dict) -> dict:\n """\n Problem:\n Given a graph in DIMACS format (with vertices, edges, and an adjacency list),\n assign a positive integer color to each vertex (1..n) so that no two adjacent vertices\n share the same color. The objective is to use as few colors as possible.\n Input kwargs:\n The keyword arguments are expected to include at least:\n - n: int (int), the number of vertices.\n - edges: list of (u, v) tuples (tuple of int (int), int (int)) representing edges.\n - adjacency: dict mapping each vertex (1..n) (int) to a set of its adjacent vertices (set of int).\n Evaluation Metric:\n Let k be the number of distinct colors used.\n For every edge connecting two vertices with the same color, count one conflict ( C ).\n If C > 0 , the solution is invalid and receives no score.\n Otherwise, the score is simply k , with a lower k being better.\n Returns:\n A dictionary representing the solution, mapping each vertex_id (1..n) to a positive integer color.\n """\n ## placeholder.\n return {} # Replace {} with the actual solution dictionary when implemented.' -EVAL_CLASS_NAME = 'GCEvaluationCB' -EVAL_KWARGS = {'timeout_seconds': 60} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_graph_colouring/paras.yaml b/examples/benchmark_tasks/optimization_graph_colouring/paras.yaml deleted file mode 100644 index 25ba1092..00000000 --- a/examples/benchmark_tasks/optimization_graph_colouring/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: GCEvaluationCB -timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_hybrid_reentrant_shop_scheduling/__init__.py b/examples/benchmark_tasks/optimization_hybrid_reentrant_shop_scheduling/__init__.py deleted file mode 100644 index aa9990f8..00000000 --- a/examples/benchmark_tasks/optimization_hybrid_reentrant_shop_scheduling/__init__.py +++ /dev/null @@ -1,564 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_hybrid_reentrant_shop_scheduling -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# References: -# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language -# model agents in algorithm search for combinatorial optimization. -# arXiv preprint arXiv:2504.04310 (2025). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -from typing import Any -import numpy as np -from llm4ad_loader import Evaluation -from llm4ad_loader import load_subdir_as_text -# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader -# from llm4ad.task.optimization.co_bench.hybrid_reentrant_shop_scheduling_co_bench.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n_jobs: int, n_machines: int, init_time: int, setup_times: list, processing_times: list, **kwargs) -> dict:\n """\n Input:\n - n_jobs: Integer; the number of jobs.\n - n_machines: Integer; the number of primary machines.\n - init_time: Integer; the initialization time for every job on a primary machine.\n - setup_times: List of integers; the setup times for each job on the remote server.\n - processing_times: List of integers; the processing times for each job in the main processing stage.\n Output:\n A dictionary with the following keys:\n - \'permutation\': A list of integers of length n_jobs. This list represents the order in which the jobs are processed on the remote server.\n - \'batch_assignment\': A list of integers of length n_jobs. Each element indicates the primary machine to which the corresponding job (or batch) is assigned.\n """\n\n # TODO: Implement the solution logic.\n\n # Placeholder return\n n_jobs = kwargs[\'n_jobs\']\n return {\n \'permutation\': list(range(1, n_jobs + 1)),\n \'batch_assignment\': [1 if i % 2 == 0 else 2 for i in range(n_jobs)]\n }' -task_description = '("The problem is a Hybrid Reentrant Shop Scheduling problem where each of n jobs must sequentially "' - - -__all__ = ['HRSSEvaluationCB'] - - -class HRSSEvaluationCB(Evaluation): - - def __init__(self, - timeout_seconds=50, - **kwargs): - - """ - Args: - None - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - # Load datasets from Hugging Face with fallback - dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Hybrid Reentrant Shop Scheduling") - self._datasets = {} - for filename in dataset: - # Join all text rows into a single string - text_content = '\n'.join([row['text'] for row in dataset[filename]]) - self._datasets[filename] = text_content - - def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: - return self.evaluate(callable_func) - - def evaluate(self, eva: callable) -> float | None: - ins_cases = [] - for case_id, ins in enumerate(self._datasets.values()): - ins_cases.append(self.load_data(ins)) - - fitness_list = [] - try: - for i in ins_cases: - for j in i: - result = eva(j['n_jobs'], j['n_machines'], j['init_time'], j['setup_times'], j['processing_times']) - fitness = self.eval_func(j['n_jobs'], j['n_machines'], j['init_time'], j['setup_times'], j['processing_times'], result['permutation'], result['batch_assignment']) - fitness_list.append(fitness) - - return -np.mean(fitness_list) - - except ValueError as e: - print(e) - return None - - def load_data(self, input_string): - """ - Reads the input string and parses one or more problem instances. - The input is expected to have one or more instances separated by lines that contain only dashes (e.g., "-----"). - Each instance must include exactly 4 nonempty lines: - 1. Header line: "Number of jobs: X Number of machines: Y" - 2. Initialization time: "Initialization time: Z" - 3. Setup times: "Setup times: t1 t2 ... tX" - 4. Processing times: "Processing times: p1 p2 ... pX" - Returns: - A list of dictionaries. Each dictionary corresponds to a problem instance and contains the keys: - - 'n_jobs': integer - - 'n_machines': integer - - 'init_time': integer - - 'setup_times': list of integers - - 'processing_times': list of integers - """ -import os, sys -sys.path.insert(0, os.path.dirname(__file__)) - import re - cases = [] - lines = [line.strip() for line in input_string.split('\n') if line.strip() != ''] - - # Split the file into separate instance blocks using a line of dashes as delimiter. - instance_blocks = [] - current_block = [] - for line in lines: - if re.match(r'^-+$', line): - if current_block: - instance_blocks.append(current_block) - current_block = [] - else: - current_block.append(line) - if current_block: - instance_blocks.append(current_block) - - # Process each instance block. - for block in instance_blocks: - if len(block) < 4: - raise ValueError("Invalid instance format: each instance must contain at least 4 nonempty lines.") - - # Line 1: Extract number of jobs and number of machines. - header_line = block[0] - m_jobs = re.search(r'Number of jobs:\s*(\d+)', header_line) - m_machines = re.search(r'Number of machines:\s*(\d+)', header_line) - if not m_jobs or not m_machines: - raise ValueError("Invalid header format in instance: '{}'".format(header_line)) - n_jobs = int(m_jobs.group(1)) - n_machines = int(m_machines.group(1)) - - # Line 2: Initialization time. - m_init = re.search(r'Initialization time:\s*(\d+)', block[1]) - if not m_init: - raise ValueError("Invalid initialization time line: '{}'".format(block[1])) - init_time = int(m_init.group(1)) - - # Line 3: Setup times. - m_setup = re.search(r'Setup times:\s*(.*)', block[2]) - if not m_setup: - raise ValueError("Invalid setup times line: '{}'".format(block[2])) - setup_str = m_setup.group(1).strip() - setup_times = list(map(int, setup_str.split())) - if len(setup_times) != n_jobs: - raise ValueError( - "Number of setup times ({}) does not match number of jobs ({})".format(len(setup_times), n_jobs)) - - # Line 4: Processing times. - m_process = re.search(r'Processing times:\s*(.*)', block[3]) - if not m_process: - raise ValueError("Invalid processing times line: '{}'".format(block[3])) - process_str = m_process.group(1).strip() - processing_times = list(map(int, process_str.split())) - if len(processing_times) != n_jobs: - raise ValueError( - "Number of processing times ({}) does not match number of jobs ({})".format(len(processing_times), - n_jobs)) - - case = { - 'n_jobs': n_jobs, - 'n_machines': n_machines, - 'init_time': init_time, - 'setup_times': setup_times, - 'processing_times': processing_times - } - cases.append(case) - - return cases - - def eval_func(self, n_jobs, n_machines, init_time, setup_times, processing_times, permutation, batch_assignment): - """ - 1. Initialization on one of m identical primary machines: - - Jobs are processed in natural order (1, 2, …, n_jobs) using list scheduling. - - In this phase, each job takes 'init_time'. The machine assignment is determined - by the list scheduling, and that assignment is used for the final main processing. - 2. Setup on the remote server: - - Jobs are processed in the order specified by 'permutation' (a 1-indexed list). - - A job's setup can start only after its initialization is complete and when the - remote server is free. The setup time for job j is given as setup_times[j-1]. - 3. Main processing on primary machines: - - Each job is processed on the same primary machine that performed its initialization. - - Within each machine, jobs are processed in the natural order (i.e., in order of their job indices). - - The processing time for job j is given by processing_times[j-1]. - The makespan is defined as the time when the last job completes its main processing. - Parameters: - - n_jobs: Integer; number of jobs. - - n_machines: Integer; number of primary machines. - - init_time: Integer; initialization time for each job. - - setup_times: List of integers; setup times for each job on the remote server. - - processing_times: List of integers; processing times for each job in main processing. - - permutation: List of integers of length n_jobs; a permutation (1-indexed) representing the order - in which jobs are processed on the remote server. - Returns: - A scalar (float or integer) representing the makespan (total completion time). - Raises: - ValueError: if any input constraint is not met. - """ - import heapq - - # --- Input Validation --- - if len(setup_times) != n_jobs: - raise ValueError("Length of setup_times must equal n_jobs.") - if len(processing_times) != n_jobs: - raise ValueError("Length of processing_times must equal n_jobs.") - if len(permutation) != n_jobs or sorted(permutation) != list(range(1, n_jobs + 1)): - raise ValueError("permutation must be a valid permutation of the job indices 1 through n_jobs.") - - # --- Operation 1: Initialization on Primary Machines --- - # Jobs are initialized in natural order using list scheduling. - # We keep track of both finish time and the machine used for each job. - op1_finish = [0] * (n_jobs + 1) # op1_finish[j] for job j (1-indexed) - machine_assignment = [0] * (n_jobs + 1) # Which machine processed job j - # Create a heap of available machines with tuples (next_available_time, machine_id) - machine_heap = [(0, machine_id) for machine_id in range(1, n_machines + 1)] - heapq.heapify(machine_heap) - - for job in range(1, n_jobs + 1): - avail_time, machine_id = heapq.heappop(machine_heap) - finish_time = avail_time + init_time - op1_finish[job] = finish_time - machine_assignment[job] = machine_id # Record the machine used for initialization. - heapq.heappush(machine_heap, (finish_time, machine_id)) - - # --- Operation 2: Setup on the Remote Server --- - op2_finish = [0] * (n_jobs + 1) # op2_finish[j] for job j (1-indexed) - current_time = 0 - for job in permutation: - start_time = max(op1_finish[job], current_time) - finish_time = start_time + setup_times[job - 1] - op2_finish[job] = finish_time - current_time = finish_time - - # --- Operation 3: Main Processing on Primary Machines --- - # We now schedule the main processing on the same primary machine - # that performed the job's initialization. - # Group jobs per machine based on machine_assignment. - jobs_by_machine = {machine_id: [] for machine_id in range(1, n_machines + 1)} - for job in range(1, n_jobs + 1): - assigned_machine = machine_assignment[job] - jobs_by_machine[assigned_machine].append(job) - # For each machine, sort jobs in natural order. - for machine_id in jobs_by_machine: - jobs_by_machine[machine_id].sort() - - op3_finish = [0] * (n_jobs + 1) - machine_finish_times = {machine_id: 0 for machine_id in range(1, n_machines + 1)} - for machine_id in range(1, n_machines + 1): - current_machine_time = machine_finish_times[machine_id] - for job in jobs_by_machine[machine_id]: - release_time = op2_finish[job] # Job is ready for main processing only after setup. - start_time = max(current_machine_time, release_time) - finish_time = start_time + processing_times[job - 1] - op3_finish[job] = finish_time - current_machine_time = finish_time - machine_finish_times[machine_id] = current_machine_time - - # --- Calculate Makespan --- - makespan = max(op3_finish) if op3_finish else 0 - return makespan - - def norm_score(self, results): - optimal_scores = { - 'hrs-10_025.txt': [821.0, 809.5, 751.5, 814.5, 792.0, 785.5, 775.0, 801.0, 846.0, 850.5, 793.5, 899.5, - 820.5, - 799.0, 765.0, 822.0, 785.0, 781.5, 819.0, 758.5, 775.0, 813.5, 800.0, 809.0, 762.5, - 796.5, - 758.0, 769.0, 771.0, 873.5, 796.0, 854.0, 808.5, 768.0, 825.5, 770.0, 840.5, 848.0, - 739.5, - 813.5, 800.0, 788.5, 782.0, 826.5, 795.0, 743.5, 789.5, 839.0, 779.0, 816.0], - 'hrs-10_05.txt': [411.5, 410.0, 385.0, 386.0, 395.0, 402.5, 401.0, 371.0, 398.0, 403.5, 407.0, 396.0, 407.5, - 376.5, 405.0, 401.5, 453.5, 408.0, 405.5, 382.5, 382.5, 386.5, 392.5, 388.5, 446.0, 417.5, - 394.5, 372.0, 403.5, 363.0, 404.5, 392.0, 411.0, 408.0, 417.0, 377.0, 421.0, 383.0, 402.5, - 399.0, 405.5, 414.0, 420.5, 377.0, 382.0, 404.5, 438.5, 401.5, 418.0, 414.5], - 'hrs-10_075.txt': [284.0, 267.5, 239.0, 269, 284, 274.0, 284, 286.0, 276, 278.5, 288, 308, 265.0, 291, 257, - 278, - 311, 277, 268.0, 290.5, 276.5, 290.0, 285.0, 298.0, 250.5, 276, 266.0, 248, 269.5, 266.0, - 265.0, 280.5, 245.5, 265, 272.5, 320.5, 302, 268.0, 266.0, 264, 288.5, 269.5, 266, 279.0, - 284.0, 284.5, 271, 283.0, 259.0, 257.0], - 'hrs-10_1.txt': [243, 267, 237, 250, 192, 273, 273, 226, 251, 242, 219, 269, 218, 229, 212.5, 266, 269, 223, - 274, 232, 225.5, 271, 287, 288, 258, 205.5, 265, 251, 268, 259, 203.0, 251, 231, 218, 225, - 252, - 250, 246, 296, 202.5, 228, 247, 223, 290, 219.5, 192, 277, 224, 273, 222.5], - 'hrs-10_125.txt': [230, 168, 210, 264, 230, 297, 210, 260, 210, 290, 180, 268, 258, 187, 224, 192, 204, 289, - 178, 236, 204, 257, 193, 251, 212, 183, 238, 205, 294, 236, 199, 238, 260, 255, 224, 260, - 197, 234, 224, 243, 209, 261, 283, 216, 212, 238, 223, 281, 238, 247], - 'hrs-10_15.txt': [208, 206, 252, 272, 213, 259, 212, 230, 216, 236, 255, 178, 215, 188, 267, 204, 190, 217, - 254, - 193, 209, 255, 172, 228, 303, 213, 211, 233, 229, 163, 296, 230, 138, 241, 191, 236, 207, - 269, - 238, 279, 239, 232, 201, 237, 226, 243, 284, 213, 202, 216], - 'hrs-10_175.txt': [207, 183, 236, 222, 243, 270, 256, 234, 191, 213, 210, 282, 263, 172, 278, 216, 275, 210, - 264, 221, 219, 261, 211, 189, 199, 207, 209, 210, 220, 270, 320, 236, 240, 205, 206, 199, - 233, 191, 194, 260, 215, 230, 219, 191, 201, 248, 169, 216, 225, 185], - 'hrs-10_2.txt': [185, 244, 166, 252, 207, 204, 220, 175, 229, 182, 200, 264, 221, 211, 203, 229, 191, 210, - 239, - 202, 200, 238, 264, 255, 192, 187, 236, 224, 192, 207, 279, 229, 198, 217, 205, 259, 240, - 228, - 200, 234, 219, 177, 191, 241, 190, 253, 235, 216, 187, 229], - 'hrs-10_25.txt': [307, 242, 226, 208, 163, 222, 254, 209, 238, 159, 196, 230, 208, 255, 231, 218, 227, 237, - 258, - 241, 213, 204, 204, 257, 195, 246, 185, 128, 213, 188, 228, 231, 255, 150, 177, 220, 214, - 197, - 286, 226, 162, 226, 210, 189, 278, 234, 218, 237, 260, 212], - 'hrs-25_025.txt': [2009.5, 1922.0, 1972.0, 2013.5, 1945.5, 2114.5, 2054.0, 1957.0, 1986.5, 2024.5, 2034.0, - 2118.5, 2016.5, 2043.5, 2009.5, 1933.5, 2028.5, 2050.5, 2066.5, 1997.0, 1926.0, 1933.0, - 2066.0, 2101.5, 1977.0, 2004.5, 2068.5, 2000.0, 2027.0, 2071.5, 1986.5, 2031.0, 2041.5, - 1992.0, 2073.0, 1940.5, 1977.0, 1892.5, 1918.0, 2071.0, 2109.5, 1949.0, 2024.0, 1955.0, - 2077.0, 1959.0, 1902.0, 2079.0, 1975.0, 2083.0], - 'hrs-25_05.txt': [965.0, 932.5, 1021.5, 1033.0, 933.5, 998.0, 1075.0, 1022.5, 1033.5, 945.5, 1027.0, 1019.5, - 955.0, 955.0, 1044.5, 1045.5, 983.0, 1016.0, 1024.0, 1016.5, 1062.0, 994.0, 983.5, 998.0, - 1019.0, 1014.5, 996.0, 950.0, 1016.5, 1035.5, 968.5, 1028.5, 1067.0, 1027.0, 1047.0, - 1012.0, - 1052.0, 1058.0, 1019.0, 1015.5, 1035.5, 1041.0, 975.0, 1040.5, 973.0, 1009.5, 1013.0, - 1041.0, - 1003.0, 996.0], - 'hrs-25_075.txt': [673.5, 690.5, 666.0, 669.0, 717.5, 696.5, 674.0, 678.0, 693.0, 674.0, 664.5, 695.5, - 733.0, - 667.0, 690.5, 658.5, 637.5, 735.0, 624.0, 640.0, 683.5, 676.0, 672.0, 691.0, 707.5, - 676.0, - 644.0, 667.5, 676.0, 667.0, 690.5, 692.5, 701.0, 667.5, 699.5, 683.0, 686.5, 660.5, - 705.5, - 663.0, 689.0, 694.0, 674.0, 659, 664.0, 694.0, 662.5, 653.0, 708.0, 679.5], - 'hrs-25_1.txt': [585, 548, 543.5, 533, 526.5, 555.0, 535.5, 528.5, 548.0, 497, 558.5, 518, 502.5, 545.5, - 541.0, - 578, 519, 543, 543, 497, 524, 556, 595, 631, 476.0, 538, 556, 553.0, 517, 533, 578, 536.0, - 619, - 547, 576, 470.0, 554, 528, 574, 521, 574, 520.5, 523, 551, 519, 506, 510, 583, 580, 531], - 'hrs-25_125.txt': [482, 635, 491, 497, 514, 557, 576, 498, 520, 532, 472, 532, 556, 462, 498, 601, 540, 526, - 528, 498, 458, 475, 549, 587, 589, 500, 481, 495.5, 464, 605, 576, 449, 525, 465, 541, - 591, - 446, 543, 477, 498, 564, 471, 488, 501, 500, 566, 541, 455, 566, 542], - 'hrs-25_15.txt': [555, 533, 546, 483, 422, 519, 442, 561, 508, 569, 510, 562, 629, 470, 441, 505, 465, 583, - 483, - 440, 540, 480, 577, 575, 458, 553, 535, 544, 418, 562, 557, 485, 497, 543, 555, 575, 480, - 608, - 632, 568, 552, 497, 544, 554, 577, 574, 481, 618, 550, 514], - 'hrs-25_175.txt': [575, 451, 442, 527, 487, 539, 486, 584, 505, 531, 472, 602, 526, 536, 488, 496, 469, 460, - 593, 544, 523, 482, 548, 516, 631, 636, 463, 580, 437, 559, 596, 594, 539, 586, 448, 647, - 532, 473, 581, 507, 532, 454, 654, 505, 542, 438, 463, 552, 544, 548], - 'hrs-25_2.txt': [561, 490, 586, 486, 469, 489, 569, 536, 578, 526, 527, 420, 526, 531, 498, 600, 611, 557, - 485, - 536, 530, 581, 519, 521, 565, 526, 482, 538, 521, 531, 538, 558, 512, 585, 558, 502, 609, - 516, - 566, 590, 495, 535, 613, 567, 576, 540, 627, 573, 482, 600], - 'hrs-25_25.txt': [573, 487, 528, 579, 510, 538, 582, 541, 495, 559, 454, 536, 506, 543, 569, 480, 544, 545, - 576, - 438, 435, 493, 472, 588, 500, 476, 593, 468, 465, 468, 497, 456, 529, 456, 572, 582, 596, - 601, - 479, 544, 523, 506, 504, 555, 522, 572, 496, 508, 591, 539], - 'hrs-50_025.txt': [4034.5, 3844.0, 4138.0, 4072.0, 4022.0, 4015.0, 4043.5, 4161.5, 3997.0, 3954.0, 3965.0, - 4100.5, 3918.0, 3969.5, 4075.0, 4084.0, 3826.5, 4037.0, 4061.5, 3999.0, 4123.0, 4157.5, - 4087.0, 4046.0, 4032.5, 3896.5, 4010.0, 4084.0, 4009.0, 3900.5, 3944.0, 3982.5, 3943.5, - 4083.5, 3988.0, 3881.0, 3963.0, 4021.5, 4093.5, 3909.0, 3950.5, 3843.5, 3897.0, 4074.0, - 4062.5, 4061.5, 3911.0, 4011.5, 4113.0, 3975.5], - 'hrs-50_05.txt': [2052.5, 2057.0, 2025.5, 2053.5, 1995.0, 2105.5, 2038.5, 2028.5, 2076.5, 2055.5, 2044.0, - 1957.5, 2039.5, 2002.5, 2009.5, 2016.5, 2006.5, 2027.0, 1998.5, 1986.0, 1990.0, 2021.5, - 2044.0, 2058.5, 2071.0, 1958.5, 2031.5, 2110.0, 2044.0, 1982.5, 2010.5, 2004.0, 2011.0, - 2002.0, 1997.5, 2035.5, 2015.0, 2065.0, 1956.5, 1966.5, 2102.0, 2001.0, 2048.5, 2020.5, - 2017.0, 2010.5, 1988.5, 1974.5, 1989.5, 2093.0], - 'hrs-50_075.txt': [1355.5, 1335.5, 1346.5, 1376.0, 1241.0, 1337.5, 1355.0, 1318.0, 1345.0, 1324.0, 1359.0, - 1353.0, 1349.5, 1280.5, 1332.5, 1318.5, 1324.5, 1374.0, 1332.5, 1338.5, 1304.5, 1349.0, - 1409.5, 1333.5, 1385.0, 1319.5, 1288.0, 1301.0, 1373.0, 1324.5, 1363.5, 1351.5, 1329.5, - 1293.5, 1337.0, 1326.5, 1357.0, 1322.5, 1370.5, 1362.0, 1328.0, 1375.5, 1322.0, 1348.5, - 1424.5, 1320.5, 1355.5, 1321.0, 1329.0, 1425.5], - 'hrs-50_1.txt': [1030.0, 1048, 1018.5, 1141, 1095, 1056.5, 1087.5, 1002, 983.0, 1179, 1126.5, 1075, 1118, - 1034, - 1088, 1009.5, 1052.5, 1115.5, 1054.5, 1114.0, 985.5, 1023.5, 1095, 1158, 1024.5, 1028, - 1046, - 1024, 1002.0, 1111, 1044.0, 1030.5, 1116.0, 1107.5, 1031, 986, 1063, 1100, 1070, 1041.5, - 1064.0, 1056, 1060, 1124, 1060.5, 1030.5, 1097, 1011, 1148, 970.0], - 'hrs-50_125.txt': [1007, 1001, 996, 1037, 1021, 924, 1071, 988, 1034, 915, 1022, 959, 911, 968, 996, 1019, - 940, - 1016, 972, 983, 999, 1079, 1015, 947, 1025, 1053, 931, 1017, 1081, 1101, 968, 1095, 1109, - 1011, 957, 1033, 1111, 1000, 1126, 1036, 1103, 1038, 927, 967, 922, 871, 1098, 939, 1092, - 1188], - 'hrs-50_15.txt': [926, 1093, 906, 987, 956, 1119, 1069, 1015, 900, 1083, 1038, 1109, 990, 974, 1047, 1013, - 989, - 1130, 1022, 1019, 979, 952, 1067, 1056, 1097, 985, 1004, 983, 968, 1056, 932, 997, 943, - 1135, - 1113, 1044, 984, 1065, 978, 951, 976, 1081, 958, 971, 1053, 973, 934, 944, 1055, 1019], - 'hrs-50_175.txt': [1097, 977, 1146, 911, 960, 1072, 1047, 1067, 1127, 1033, 917, 944, 1122, 980, 989, 959, - 1082, - 1012, 1156, 969, 969, 898, 1043, 981, 1118, 1040, 1058, 974, 952, 951, 1033, 1160, 1071, - 1077, 1043, 1054, 1094, 1026, 1026, 1087, 966, 1064, 993, 1035, 952, 1000, 1042, 946, - 1105, - 1094], - 'hrs-50_2.txt': [907, 1114, 971, 1045, 1066, 976, 1093, 1153, 1071, 943, 1018, 934, 943, 1057, 922, 1021, - 1108, - 909, 929, 1061, 932, 1001, 946, 1015, 1112, 1041, 1096, 1050, 1023, 1014, 970, 1017, 968, - 1050, - 1068, 941, 937, 994, 1046, 1009, 926, 1090, 1005, 1006, 1044, 1010, 924, 1008, 1026, 1011], - 'hrs-50_25.txt': [1040, 997, 947, 1068, 1055, 933, 911, 927, 1062, 873, 1030, 1061, 1051, 897, 1051, 970, - 1030, - 1088, 1046, 908, 996, 1014, 935, 1085, 1011, 929, 877, 1233, 1020, 1002, 1087, 960, 1149, - 1076, 1040, 1002, 994, 974, 990, 1043, 1058, 990, 1074, 1118, 965, 1008, 1061, 1099, 1037, - 1053]} - - normed = {} - for case, (scores, error_message) in results.items(): - if case not in optimal_scores: - continue # Skip if there's no optimal score defined. - optimal_list = optimal_scores[case] - normed_scores = [] - # Compute normalized score for each index. - for idx, score in enumerate(scores): - if isinstance(score, (int, float)): - normed_scores.append(optimal_list[idx] / score) - else: - normed_scores.append(score) - normed[case] = (normed_scores, error_message) - - return normed - - def get_dev(self): - dev = { - 'hrs-10_025.txt': [20, 21, 1, 6, 17, 48, 15, 43, 38, 28, 46, 18, 0, 31, 24, 9, 27, 8, 35, 2, 25, 22, 49, 5, - 33], - 'hrs-10_05.txt': [34, 43, 45, 32, 31, 24, 46, 3, 22, 36, 0, 40, 25, 17, 23, 10, 21, 20, 14, 6, 48, 28, 8, - 26, - 1], - 'hrs-10_075.txt': [38, 41, 31, 22, 12, 13, 48, 32, 27, 16, 35, 17, 34, 6, 4, 30, 26, 42, 29, 3, 18, 5, 28, - 20, - 39], - 'hrs-10_1.txt': [49, 28, 14, 7, 0, 16, 18, 25, 44, 19, 40, 38, 24, 33, 12, 3, 41, 35, 46, 9, 11, 39, 29, 8, - 5], - 'hrs-10_125.txt': [48, 40, 25, 36, 24, 20, 45, 4, 12, 17, 16, 28, 0, 11, 9, 23, 8, 6, 41, 34, 31, 35, 7, 44, - 38], - 'hrs-10_15.txt': [11, 17, 21, 14, 0, 28, 45, 4, 20, 5, 9, 32, 29, 27, 44, 49, 15, 7, 39, 46, 36, 2, 31, 3, - 1], - 'hrs-10_175.txt': [11, 27, 47, 10, 39, 20, 49, 34, 5, 38, 36, 22, 9, 14, 28, 33, 23, 37, 41, 45, 35, 12, 44, - 17, - 18], - 'hrs-10_2.txt': [34, 46, 8, 21, 6, 39, 26, 43, 4, 23, 9, 0, 35, 47, 3, 30, 24, 37, 42, 44, 7, 15, 38, 29, - 49], - 'hrs-10_25.txt': [16, 4, 3, 45, 32, 12, 1, 17, 7, 0, 49, 47, 18, 21, 25, 42, 36, 11, 30, 48, 37, 13, 8, 15, - 38], - 'hrs-25_025.txt': [18, 12, 13, 22, 8, 20, 44, 10, 47, 9, 48, 32, 27, 16, 7, 11, 25, 23, 14, 36, 17, 29, 21, - 38, - 45], - 'hrs-25_05.txt': [20, 7, 23, 8, 17, 37, 45, 38, 25, 18, 40, 35, 36, 46, 28, 16, 32, 22, 49, 31, 13, 1, 43, - 39, - 41], - 'hrs-25_075.txt': [6, 30, 23, 44, 15, 38, 24, 27, 5, 49, 39, 31, 45, 25, 11, 48, 4, 32, 21, 47, 46, 33, 12, - 19, - 29], - 'hrs-25_1.txt': [39, 0, 21, 24, 8, 40, 9, 41, 3, 34, 43, 16, 36, 26, 10, 7, 4, 25, 45, 20, 5, 11, 18, 31, - 33], - 'hrs-25_125.txt': [1, 9, 38, 6, 49, 36, 14, 11, 25, 20, 39, 22, 7, 21, 29, 8, 43, 45, 2, 35, 40, 42, 10, 13, - 30], - 'hrs-25_15.txt': [48, 46, 44, 23, 12, 26, 28, 33, 16, 30, 21, 4, 34, 9, 19, 47, 1, 13, 35, 6, 41, 2, 45, 14, - 38], - 'hrs-25_175.txt': [4, 7, 46, 14, 1, 43, 18, 47, 5, 31, 12, 35, 8, 20, 37, 33, 22, 23, 16, 17, 10, 24, 15, - 32, - 19], - 'hrs-25_2.txt': [5, 32, 47, 29, 49, 15, 23, 26, 24, 44, 35, 3, 31, 42, 46, 14, 16, 12, 6, 17, 45, 37, 20, - 22, - 25], - 'hrs-25_25.txt': [48, 16, 45, 18, 17, 0, 8, 38, 44, 15, 49, 40, 19, 41, 47, 37, 3, 27, 34, 43, 12, 39, 1, - 36, - 6], - 'hrs-50_025.txt': [4, 12, 44, 23, 33, 28, 5, 27, 1, 24, 21, 36, 18, 26, 31, 37, 48, 35, 14, 11, 29, 30, 39, - 34, - 2], - 'hrs-50_05.txt': [27, 5, 43, 46, 25, 29, 9, 2, 36, 38, 0, 10, 7, 31, 24, 22, 45, 44, 14, 1, 47, 19, 34, 6, - 35], - 'hrs-50_075.txt': [4, 16, 25, 26, 9, 1, 24, 17, 43, 47, 36, 38, 5, 44, 18, 27, 31, 2, 42, 39, 23, 41, 40, - 46, - 14], - 'hrs-50_1.txt': [6, 18, 30, 26, 27, 2, 28, 34, 15, 24, 44, 43, 1, 32, 17, 5, 16, 14, 7, 19, 25, 21, 38, 12, - 48], - 'hrs-50_125.txt': [1, 19, 32, 11, 9, 12, 7, 37, 40, 30, 15, 16, 35, 8, 18, 45, 2, 21, 46, 29, 26, 14, 25, 4, - 22], - 'hrs-50_15.txt': [49, 38, 5, 45, 27, 42, 14, 13, 16, 21, 10, 4, 48, 24, 32, 47, 15, 43, 1, 44, 31, 40, 2, - 11, - 19], - 'hrs-50_175.txt': [2, 26, 23, 19, 20, 17, 40, 27, 16, 29, 3, 30, 48, 49, 25, 39, 38, 35, 7, 6, 46, 15, 24, - 13, - 5], - 'hrs-50_2.txt': [19, 12, 24, 18, 22, 49, 7, 43, 1, 11, 33, 42, 35, 46, 25, 4, 32, 5, 3, 20, 29, 10, 37, 34, - 15], - 'hrs-50_25.txt': [35, 24, 28, 34, 18, 20, 23, 49, 13, 9, 39, 2, 38, 22, 33, 36, 46, 1, 19, 29, 3, 21, 15, - 12, - 43]} - - return dev - - - - - -# Task configuration for benchmark task -ENTRY_NAME = 'solve' -FUNCTION_SIGNATURE = 'def solve(...):' -IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' -TASK_DESCRIPTION = '("The problem is a Hybrid Reentrant Shop Scheduling problem where each of n jobs must sequentially "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The problem is a Hybrid Reentrant Shop Scheduling problem where each of n jobs must sequentially "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n_jobs: int, n_machines: int, init_time: int, setup_times: list, processing_times: list, **kwargs) -> dict:\n """\n Input:\n - n_jobs: Integer; the number of jobs.\n - n_machines: Integer; the number of primary machines.\n - init_time: Integer; the initialization time for every job on a primary machine.\n - setup_times: List of integers; the setup times for each job on the remote server.\n - processing_times: List of integers; the processing times for each job in the main processing stage.\n Output:\n A dictionary with the following keys:\n - \'permutation\': A list of integers of length n_jobs. This list represents the order in which the jobs are processed on the remote server.\n - \'batch_assignment\': A list of integers of length n_jobs. Each element indicates the primary machine to which the corresponding job (or batch) is assigned.\n """\n\n # TODO: Implement the solution logic.\n\n # Placeholder return\n n_jobs = kwargs[\'n_jobs\']\n return {\n \'permutation\': list(range(1, n_jobs + 1)),\n \'batch_assignment\': [1 if i % 2 == 0 else 2 for i in range(n_jobs)]\n }' -EVAL_CLASS_NAME = 'HRSSEvaluationCB' -EVAL_KWARGS = {'timeout_seconds': 60} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_hybrid_reentrant_shop_scheduling/paras.yaml b/examples/benchmark_tasks/optimization_hybrid_reentrant_shop_scheduling/paras.yaml deleted file mode 100644 index 7beaae0c..00000000 --- a/examples/benchmark_tasks/optimization_hybrid_reentrant_shop_scheduling/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: HRSSEvaluationCB -timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_job_shop_scheduling/__init__.py b/examples/benchmark_tasks/optimization_job_shop_scheduling/__init__.py deleted file mode 100644 index 7ac17d15..00000000 --- a/examples/benchmark_tasks/optimization_job_shop_scheduling/__init__.py +++ /dev/null @@ -1,307 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_job_shop_scheduling -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# References: -# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language -# model agents in algorithm search for combinatorial optimization. -# arXiv preprint arXiv:2504.04310 (2025). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -from typing import Any -import numpy as np -from llm4ad_loader import Evaluation -from llm4ad_loader import load_subdir_as_text -# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader -# from llm4ad.task.optimization.co_bench.job_shop_scheduling_co_bench.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n_jobs: int, n_machines: int, times: list, machines: list) -> dict:\n """\n Solves a single job shop scheduling test case.\n Input:\n - n_jobs (int): Number of jobs.\n - n_machines (int): Number of machines (and operations per job).\n - times (list of list of int): A 2D list of processing times for each operation.\n Dimensions: n_jobs x n_machines.\n - machines (list of list of int): A 2D list specifying the machine assignment for each operation.\n Dimensions: n_jobs x n_machines. Note machine is 1-indexed.\n Output:\n solution (dict): A dictionary containing:\n - start_times (list of list of int): A 2D list of start times for each operation.\n Dimensions: n_jobs x n_machines.\n Each start time must be a non-negative integer, and the schedule must respect the following constraints:\n (i) Sequential processing: For each job, an operation cannot start until its preceding operation has finished.\n (ii) Machine exclusivity: For operations assigned to the same machine, their processing intervals must not overlap.\n The evaluation function will use the start_times to compute the makespan and verify the constraints.\n """\n\n # Extract the case parameters\n n_jobs = kwargs["n_jobs"]\n n_machines = kwargs["n_machines"]\n times = kwargs["times"]\n machines = kwargs["machines"]\n\n # TODO: Implement the scheduling algorithm here.\n # For now, we provide a dummy solution where all operations start at time 0.\n\n # Create a start_times list with dimensions n_jobs x n_machines, initializing all start times to 0.\n start_times = [[0 for _ in range(n_machines)] for _ in range(n_jobs)]\n\n # Build the solution dictionary.\n solution = {"start_times": start_times}\n\n return solution' -task_description = '("The job shop scheduling problem requires assigning non-negative integer start times to a set of "' - - -__all__ = ['JSSEvaluationCB'] - - -class JSSEvaluationCB(Evaluation): - - def __init__(self, - timeout_seconds=50, - **kwargs): - - """ - Args: - None - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - # Load datasets from Hugging Face - dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Job shop scheduling") - self._datasets = {} - for filename in dataset: - # Join all text rows into a single string - text_content = '\n'.join([row['text'] for row in dataset[filename]]) - self._datasets[filename] = text_content - - def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: - return self.evaluate(callable_func) - - def evaluate(self, eva: callable) -> float | None: - ins_cases = [] - for case_id, ins in enumerate(self._datasets.values()): - ins_cases.append(self.load_data(ins)) - - fitness_list = [] - try: - for i in ins_cases: - for j in i: - result = eva(j['n_jobs'], j['n_machines'], j['times'], j['machines']) - fitness = self.eval_func(j['n_jobs'], j['n_machines'], j['times'], j['machines'], result['start_times'], lower_bound=j['lower_bound'], upper_bound=j['upper_bound']) - fitness_list.append(fitness) - - return np.mean(fitness_list) - - except ValueError as e: - print(e) - return None - - def load_data(self, input_string): - cases = [] - lines = [line.strip() for line in input_string.split('\n') if line.strip()] # remove blank lines - - i = 0 - while i < len(lines): - # Look for a header line starting with "Nb of jobs" - if lines[i].startswith("Nb of jobs"): - # Next line contains six numbers: n_jobs, n_machines, time_seed, machine_seed, upper_bound, lower_bound - i += 1 - header_tokens = lines[i].split() - if len(header_tokens) < 6: - raise ValueError("Header line does not contain 6 values.") - n_jobs = int(header_tokens[0]) - n_machines = int(header_tokens[1]) - time_seed = int(header_tokens[2]) - machine_seed = int(header_tokens[3]) - upper_bound = int(header_tokens[4]) - lower_bound = int(header_tokens[5]) - - # Find the "Times" section - i += 1 - if not lines[i].lower().startswith("times"): - raise ValueError("Expected 'Times' section, got: " + lines[i]) - i += 1 # move to first line of times - times = [] - for _ in range(n_jobs): - # Each line should contain n_machines numbers - time_line = list(map(int, lines[i].split())) - if len(time_line) != n_machines: - raise ValueError(f"Expected {n_machines} numbers in times row, got {len(time_line)}") - times.append(time_line) - i += 1 - - # Find the "Machines" section - if i >= len(lines) or not lines[i].lower().startswith("machines"): - raise ValueError("Expected 'Machines' section, got: " + (lines[i] if i < len(lines) else "EOF")) - i += 1 # move to first line of machines - machines = [] - for _ in range(n_jobs): - machine_line = list(map(int, lines[i].split())) - if len(machine_line) != n_machines: - raise ValueError(f"Expected {n_machines} numbers in machines row, got {len(machine_line)}") - machines.append(machine_line) - i += 1 - - # Build the test case dictionary and add to the list of cases. - case = { - "n_jobs": n_jobs, - "n_machines": n_machines, - "time_seed": time_seed, - "machine_seed": machine_seed, - "upper_bound": upper_bound, - "lower_bound": lower_bound, - "times": times, - "machines": machines - } - cases.append(case) - else: - # If the current line is not a header, skip it. - i += 1 - - return cases - - def eval_func(self, n_jobs, n_machines, times, machines, start_times, **kwargs): - """ - Evaluates the solution for a job shop scheduling problem. - Input: - n_jobs (int): Number of jobs. - n_machines (int): Number of machines. - times (list of list of int): Processing times for each operation. - Dimensions: n_jobs x n_machines. - machines (list of list of int): Machine assignments for each operation. - Dimensions: n_jobs x n_machines. - start_times (list of list of int): Proposed start times for each operation. - Dimensions: n_jobs x n_machines. - kwargs: Other parameters that may be provided, which are ignored here. - Output: - score (int): The makespan, defined as the maximum completion time across all jobs. - Raises: - ValueError: If any scheduling constraints are violated. - """ - - # Check that start_times dimensions match the problem dimensions. - if len(start_times) != n_jobs: - raise ValueError(f"Expected start_times to have {n_jobs} rows, got {len(start_times)}") - for i, row in enumerate(start_times): - if len(row) != n_machines: - raise ValueError(f"Expected start_times row {i} to have {n_machines} entries, got {len(row)}") - for t in row: - if t < 0: - raise ValueError("Start times must be non-negative.") - - # Constraint (i): Sequential processing for each job. - job_completion_times = [] - for i in range(n_jobs): - current_time = None - for j in range(n_machines): - st = start_times[i][j] - pt = times[i][j] - if j == 0: - # For the first operation, simply set the finish time. - current_time = st + pt - else: - # For subsequent operations, the start time must be no earlier than the finish of the previous. - if st < current_time: - raise ValueError( - f"Job {i} operation {j} starts at {st} but previous operation finishes at {current_time}") - current_time = st + pt - job_completion_times.append(current_time) - - # Constraint (ii): Machine non-overlap. - # Build a dictionary mapping machine id to a list of (start_time, finish_time, job, op_index) - machine_schedules = {} - for i in range(n_jobs): - for j in range(n_machines): - machine_id = machines[i][j] - st = start_times[i][j] - pt = times[i][j] - finish_time = st + pt - if machine_id not in machine_schedules: - machine_schedules[machine_id] = [] - machine_schedules[machine_id].append((st, finish_time, i, j)) - - # For each machine, sort operations by start time and check for overlaps. - for machine_id, ops in machine_schedules.items(): - ops_sorted = sorted(ops, key=lambda x: x[0]) - for k in range(1, len(ops_sorted)): - prev_st, prev_finish, prev_job, prev_op = ops_sorted[k - 1] - curr_st, curr_finish, curr_job, curr_op = ops_sorted[k] - if prev_finish > curr_st: - raise ValueError( - f"Machine {machine_id}: Operation from job {prev_job}, op {prev_op} (finishing at {prev_finish}) overlaps with job {curr_job}, op {curr_op} (starting at {curr_st}).") - - # Compute the makespan as the maximum completion time among all jobs. - makespan = max(job_completion_times) - - score = kwargs['lower_bound'] / makespan - - return score - - def get_dev(self): - dev = {'tai100_20.txt': [1, 8, 0, 6, 9], 'tai15_15.txt': [1, 8, 9, 4, 5], 'tai20_15.txt': [2, 7, 0, 8, 3], - 'tai20_20.txt': [9, 7, 8, 3, 0], 'tai30_15.txt': [8, 7, 2, 5, 1], 'tai30_20.txt': [0, 5, 1, 4, 6], - 'tai50_15.txt': [9, 1, 4, 5, 6], 'tai50_20.txt': [5, 9, 7, 4, 8]} - - return dev - - - - - -# Task configuration for benchmark task -ENTRY_NAME = 'solve' -FUNCTION_SIGNATURE = 'def solve(...):' -IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' -TASK_DESCRIPTION = '("The job shop scheduling problem requires assigning non-negative integer start times to a set of "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The job shop scheduling problem requires assigning non-negative integer start times to a set of "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n_jobs: int, n_machines: int, times: list, machines: list) -> dict:\n """\n Solves a single job shop scheduling test case.\n Input:\n - n_jobs (int): Number of jobs.\n - n_machines (int): Number of machines (and operations per job).\n - times (list of list of int): A 2D list of processing times for each operation.\n Dimensions: n_jobs x n_machines.\n - machines (list of list of int): A 2D list specifying the machine assignment for each operation.\n Dimensions: n_jobs x n_machines. Note machine is 1-indexed.\n Output:\n solution (dict): A dictionary containing:\n - start_times (list of list of int): A 2D list of start times for each operation.\n Dimensions: n_jobs x n_machines.\n Each start time must be a non-negative integer, and the schedule must respect the following constraints:\n (i) Sequential processing: For each job, an operation cannot start until its preceding operation has finished.\n (ii) Machine exclusivity: For operations assigned to the same machine, their processing intervals must not overlap.\n The evaluation function will use the start_times to compute the makespan and verify the constraints.\n """\n\n # Extract the case parameters\n n_jobs = kwargs["n_jobs"]\n n_machines = kwargs["n_machines"]\n times = kwargs["times"]\n machines = kwargs["machines"]\n\n # TODO: Implement the scheduling algorithm here.\n # For now, we provide a dummy solution where all operations start at time 0.\n\n # Create a start_times list with dimensions n_jobs x n_machines, initializing all start times to 0.\n start_times = [[0 for _ in range(n_machines)] for _ in range(n_jobs)]\n\n # Build the solution dictionary.\n solution = {"start_times": start_times}\n\n return solution' -EVAL_CLASS_NAME = 'JSSEvaluationCB' -EVAL_KWARGS = {'timeout_seconds': 60} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_job_shop_scheduling/paras.yaml b/examples/benchmark_tasks/optimization_job_shop_scheduling/paras.yaml deleted file mode 100644 index 1921d34a..00000000 --- a/examples/benchmark_tasks/optimization_job_shop_scheduling/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: JSSEvaluationCB -timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_jssp_construct/__init__.py b/examples/benchmark_tasks/optimization_jssp_construct/__init__.py deleted file mode 100644 index b2c5ff2e..00000000 --- a/examples/benchmark_tasks/optimization_jssp_construct/__init__.py +++ /dev/null @@ -1,289 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_jssp_construct -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# Module Name: JSSPEvaluation -# Last Revision: 2025/2/16 -# Description: Evaluates the Job Shop Scheduling Problem (JSSP). -# Given a set of jobs and machines, the goal is to schedule jobs on machines -# in a way that minimizes the total makespan (completion time of all jobs). -# This module is part of the LLM4AD project (https://github.com/Optima-CityU/llm4ad). -# -# Parameters: -# - timeout_seconds: Maximum allowed time (in seconds) for the evaluation process: int (default: 20). -# - n_instance: Number of problem instances to generate: int (default: 16). -# - n_jobs: Number of jobs to schedule: int (default: 10). -# - n_machines: Number of machines available: int (default: 5). -# -# References: -# - Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - - -from __future__ import annotations -from typing import Any, List, Tuple, Callable -import numpy as np -import matplotlib.pyplot as plt - -from llm4ad_loader import Evaluation -import os, sys -sys.path.insert(0, os.path.dirname(__file__)) -from get_instance import GetData -# from llm4ad.task.optimization.jssp_construct.get_instance import GetData # Converted from LLM4AD import -# from llm4ad.task.optimization.jssp_construct.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\n\ndef determine_next_operation(current_status, feasible_operations):\n """\n Determine the next operation to schedule based on a greedy heuristic.\n\n Args:\n current_status: A dictionary representing the current status of each machine and job.\n feasible_operations: A list of feasible operations that can be scheduled next.\n\n Returns:\n The next operation to schedule, represented as a tuple (job_id, machine_id, processing_time).\n """\n # Simple greedy heuristic: choose the operation with the shortest processing time\n next_operation = min(feasible_operations, key=lambda x: x[2])\n return next_operation' -task_description = "'" - - -__all__ = ['JSSPEvaluation'] - - -class JSSPEvaluation(Evaluation): - """Evaluator for Job Shop Scheduling Problem.""" - - def __init__(self, - timeout_seconds=20, - n_instance=16, - n_jobs=50, - n_machines=10, - **kwargs): - """ - Args: - None - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - self.n_instance = n_instance - self.n_jobs = n_jobs - self.n_machines = n_machines - getData = GetData(self.n_instance, self.n_jobs, self.n_machines) - self._datasets = getData.generate_instances() - - def evaluate_program(self, program_str: str, callable_func: Callable) -> Any | None: - return self.evaluate(callable_func) - - def plot_solution(self, schedule: List[List[Tuple[int, int, int]]], n_jobs: int, n_machines: int): - """ - Plots the schedule as a Gantt chart. - - Args: - schedule: The schedule generated by select_next_operation. - n_jobs: Number of jobs. - n_machines: Number of machines. - """ - fig, ax = plt.subplots(figsize=(10, 6)) - - # Create a color map for the jobs - colors = plt.cm.get_cmap('tab10', n_jobs) - - # Iterate over each job and its operations - for job_idx, operations in enumerate(schedule): - for operation in operations: - machine, start_time, end_time = operation - # Plot the operation as a horizontal bar with a specific color - ax.barh(machine, end_time - start_time, left=start_time, - color=colors(job_idx), label=f'Job {job_idx}') - - # Customize the plot - ax.set_xlabel('Time') - ax.set_ylabel('Machine') - ax.set_yticks(range(n_machines)) - ax.set_yticklabels([f'Machine {i}' for i in range(n_machines)]) - ax.set_title('Scheduling Gantt Chart') - - # Add a legend - handles, labels = ax.get_legend_handles_labels() - by_label = dict(zip(labels, handles)) # Remove duplicate labels - ax.legend(by_label.values(), by_label.keys(), title="Jobs", bbox_to_anchor=(1.05, 1), loc='upper left') - - plt.tight_layout() - plt.show() - - def schedule_jobs(self, processing_times, n_jobs, n_machines, eva): - """ - Schedule jobs on machines using a greedy constructive heuristic. - - Args: - processing_times: A list of lists representing the processing times of each job on each machine. - n_jobs: Number of jobs. - n_machines: Number of machines. - - Returns: - The makespan, which is the total time required to complete all jobs. - """ - # Initialize the current status of each machine and job - machine_status = [0] * n_machines # Time each machine is available - job_status = [0] * n_jobs # Time each job is available - operation_sequence = [[] for _ in range(n_jobs)] # Sequence of operations for each job - - # Initialize the list of all operations - all_operations = [] - for job_id in range(n_jobs): - for machine_id in range(n_machines): - all_operations.append((job_id, machine_id, processing_times[job_id][machine_id])) - - # Schedule operations until all are completed - while all_operations: - # Determine feasible operations - feasible_operations = [] - for operation in all_operations: - job_id, machine_id, processing_time = operation - if job_status[job_id] <= machine_status[machine_id]: - feasible_operations.append(operation) - - if len(feasible_operations) == 0: - next_operation = all_operations[0] - else: - # Determine the next operation to schedule - next_operation = eva({'machine_status': machine_status, 'job_status': job_status}, feasible_operations) - - # Schedule the next operation - job_id, machine_id, processing_time = next_operation - start_time = max(job_status[job_id], machine_status[machine_id]) - end_time = start_time + processing_time - machine_status[machine_id] = end_time - job_status[job_id] = end_time - operation_sequence[job_id].append((machine_id, start_time, end_time)) - - # Remove the scheduled operation from the list of all operations - all_operations.remove(next_operation) - - # Calculate the makespan (total time required to complete all jobs) - makespan = max(job_status) - return makespan, operation_sequence - - def evaluate(self, eva: Callable) -> float: - """ - Evaluate the constructive heuristic for JSSP. - - Args: - instance_data: List of tuples containing the processing times, number of jobs, and number of machines. - n_ins: Number of instances to evaluate. - n_jobs: Number of jobs. - n_machines: Number of machines. - eva: The constructive heuristic function to evaluate. - - Returns: - The average makespan across all instances. - """ - makespans = [] - - for instance in self._datasets[:self.n_instance]: - processing_times, n1, n2 = instance - makespan, solution = self.schedule_jobs(processing_times, n1, n2, eva) - makespans.append(makespan) - - average_makespan = np.mean(makespans) - return -average_makespan # Negative because we want to minimize the makespan - - -if __name__ == '__main__': - def determine_next_operation(current_status, feasible_operations): - """ - Determine the next operation to schedule based on a greedy heuristic. - - Args: - current_status: A dictionary representing the current status of each machine and job. - feasible_operations: A list of feasible operations that can be scheduled next. - - Returns: - The next operation to schedule, represented as a tuple (job_id, machine_id, processing_time). - """ - # Simple greedy heuristic: choose the operation with the shortest processing time - next_operation = min(feasible_operations, key=lambda x: x[2]) - return next_operation - - - tsp = JSSPEvaluation() - makespan = tsp.evaluate_program('_', determine_next_operation) - print(makespan) - -# Task configuration for benchmark task -ENTRY_NAME = 'determine_next_operation' -FUNCTION_SIGNATURE = 'def determine_next_operation(current_status, feasible_operations):' -IMPORT_HEADER = 'import numpy as np\nimport math' -TASK_DESCRIPTION = "'" -OBJECTIVE_TEXT = "You are optimizing the implementation of `determine_next_operation` for the LLM4AD task.\\n\\nTask description:\\n'\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible." -TEMPLATE_FUNCTION = 'import numpy as np\n\ndef determine_next_operation(current_status, feasible_operations):\n """\n Determine the next operation to schedule based on a greedy heuristic.\n\n Args:\n current_status: A dictionary representing the current status of each machine and job.\n feasible_operations: A list of feasible operations that can be scheduled next.\n\n Returns:\n The next operation to schedule, represented as a tuple (job_id, machine_id, processing_time).\n """\n # Simple greedy heuristic: choose the operation with the shortest processing time\n next_operation = min(feasible_operations, key=lambda x: x[2])\n return next_operation' -EVAL_CLASS_NAME = 'JSSPEvaluation' -EVAL_KWARGS = {'timeout_seconds': 30} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_jssp_construct/get_instance.py b/examples/benchmark_tasks/optimization_jssp_construct/get_instance.py deleted file mode 100644 index b2950615..00000000 --- a/examples/benchmark_tasks/optimization_jssp_construct/get_instance.py +++ /dev/null @@ -1,43 +0,0 @@ -import numpy as np - - -class GetData: - def __init__(self, n_instance: int, n_jobs: int, n_machines: int): - """ - Initialize the GetData class for JSSP. - - Args: - n_instance: Number of instances to generate. - n_jobs: Number of jobs. - n_machines: Number of machines. - """ - self.n_instance = n_instance - self.n_jobs = n_jobs - self.n_machines = n_machines - - def generate_instances(self): - """ - Generate instances for the Job Shop Scheduling Problem. - - Returns: - A list of tuples, where each tuple contains: - - processing_times: A list of lists representing the processing times of each job on each machine. - - n_jobs: Number of jobs. - - n_machines: Number of machines. - """ - np.random.seed(2024) # Set seed for reproducibility - instance_data = [] - - for _ in range(self.n_instance): - # Generate random processing times for each job on each machine - # Each job has a sequence of operations, and each operation is assigned to a machine - # For simplicity, we assume each job has exactly `n_machines` operations, one for each machine - processing_times = [] - for _ in range(self.n_jobs): - # Randomly assign processing times for each machine - job_processing_times = np.random.randint(10, 100, size=self.n_machines).tolist() - processing_times.append(job_processing_times) - - instance_data.append((processing_times, self.n_jobs, self.n_machines)) - - return instance_data diff --git a/examples/benchmark_tasks/optimization_jssp_construct/paras.yaml b/examples/benchmark_tasks/optimization_jssp_construct/paras.yaml deleted file mode 100644 index 056940f4..00000000 --- a/examples/benchmark_tasks/optimization_jssp_construct/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: JSSPEvaluation -timeout_seconds: 30 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_knapsack_construct/__init__.py b/examples/benchmark_tasks/optimization_knapsack_construct/__init__.py deleted file mode 100644 index ee485d16..00000000 --- a/examples/benchmark_tasks/optimization_knapsack_construct/__init__.py +++ /dev/null @@ -1,271 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_knapsack_construct -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# Module Name: KnapsackEvaluation -# Last Revision: 2025/2/16 -# Description: Evaluates the Knapsack Problem. -# Given a set of items with weights and values, the goal is to select a subset of items -# that maximizes the total value while not exceeding the knapsack's capacity. -# This module is part of the LLM4AD project (https://github.com/Optima-CityU/llm4ad). -# -# Parameters: -# - timeout_seconds: Maximum allowed time (in seconds) for the evaluation process: int (default: 20). -# - n_instance: Number of problem instances to generate: int (default: 16). -# - n_items: Number of items available: int (default: 20). -# - knapsack_capacity: Maximum capacity of the knapsack: int (default: 50). -# -# References: -# - Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations -from typing import Callable, Any, List, Tuple -import matplotlib.pyplot as plt - -from llm4ad_loader import Evaluation -import os, sys -sys.path.insert(0, os.path.dirname(__file__)) -from get_instance import GetData -# from llm4ad.task.optimization.knapsack_construct.get_instance import GetData # Converted from LLM4AD import -# from llm4ad.task.optimization.knapsack_construct.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\n\ndef select_next_item(remaining_capacity: int, remaining_items: List[Tuple[int, int, int]]) -> Tuple[int, int, int] | None:\n """\n Select the item with the highest value-to-weight ratio that fits in the remaining capacity.\n\n Args:\n remaining_capacity: The remaining capacity of the knapsack.\n remaining_items: List of tuples containing (weight, value, index) of remaining items.\n\n Returns:\n The selected item as a tuple (weight, value, index), or None if no item fits.\n """\n best_item = None\n best_ratio = -1 # Initialize with a negative value to ensure any item will have a higher ratio\n\n for item in remaining_items:\n weight, value, index = item\n if weight <= remaining_capacity:\n ratio = value / weight # Calculate value-to-weight ratio\n if ratio > best_ratio:\n best_ratio = ratio\n best_item = item\n\n return best_item' -task_description = "'" - - -__all__ = ['KnapsackEvaluation'] - - -class KnapsackEvaluation(Evaluation): - """Evaluator for the Knapsack Problem.""" - - def __init__(self, - timeout_seconds=20, - n_instance=32, - n_items=50, - knapsack_capacity=100, - **kwargs): - """ - Initialize the evaluator for the Knapsack Problem. - """ - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - self.n_instance = n_instance - self.n_items = n_items - self.knapsack_capacity = knapsack_capacity - getData = GetData(self.n_instance, self.n_items, self.knapsack_capacity) - self._datasets = getData.generate_instances() - - def evaluate_program(self, program_str: str, callable_func: Callable) -> Any | None: - return self.evaluate(callable_func) - - def plot_solution(self, item_weights: list, item_values: list, selected_indices: list, knapsack_capacity: int): - """ - Plot the solution of the Knapsack problem. - - Args: - item_weights: A list of item weights. - item_values: A list of item values. - selected_indices: A list of indices of selected items. - knapsack_capacity: The capacity of the knapsack. - """ - # Prepare data for plotting - selected_weights = [item_weights[i] for i in selected_indices] - selected_values = [item_values[i] for i in selected_indices] - total_weight = sum(selected_weights) - total_value = sum(selected_values) - - # Create a bar plot for selected items - fig, ax = plt.subplots() - x = range(len(selected_indices)) - ax.bar(x, selected_weights, label='Weight', color='blue', alpha=0.6) - ax.bar(x, selected_values, label='Value', color='orange', alpha=0.6, bottom=selected_weights) - - # Add labels and title - ax.set_xlabel('Selected Items') - ax.set_ylabel('Weight / Value') - ax.set_title(f'Knapsack Solution\nTotal Weight: {total_weight}/{knapsack_capacity}, Total Value: {total_value}') - ax.set_xticks(x) - ax.set_xticklabels([f'Item {i}' for i in selected_indices]) - ax.legend() - - plt.show() - - def pack_items(self, item_weights: List[int], item_values: List[int], knapsack_capacity: int, eva: Callable) -> Tuple[int, List[int]]: - """ - Select items for the knapsack using a constructive heuristic. - - Args: - item_weights: A list of item weights. - item_values: A list of item values. - knapsack_capacity: The capacity of the knapsack. - eva: The constructive heuristic function to select the next item. - - Returns: - A tuple containing: - - The total value of the selected items. - - A list of selected item indices. - """ - remaining_items = list(zip(item_weights, item_values, range(len(item_weights)))) # Track weights, values, and indices - selected_items = [] # List of selected item indices - remaining_capacity = knapsack_capacity # Track remaining capacity - total_value = 0 # Track total value of selected items - - while remaining_items and remaining_capacity > 0: - # Use the heuristic to select the next item - selected_item = eva(remaining_capacity, remaining_items) - - if selected_item is not None: - weight, value, index = selected_item - if weight <= remaining_capacity: - # Add the selected item to the knapsack - selected_items.append(index) - total_value += value - remaining_capacity -= weight - # Remove the selected item from the remaining items - remaining_items.remove(selected_item) - else: - break - - return total_value, selected_items - - def evaluate(self, eva: Callable) -> float: - """ - Evaluate the constructive heuristic for the Knapsack Problem. - - Args: - instance_data: List of tuples containing the item weights, values, and knapsack capacity. - n_ins: Number of instances to evaluate. - eva: The constructive heuristic function to evaluate. - - Returns: - The average total value of selected items across all instances. - """ - total_value = 0 - - for instance in self._datasets[:self.n_instance]: - item_weights, item_values, knapsack_capacity = instance - value, _ = self.pack_items(item_weights, item_values, knapsack_capacity, eva) - total_value += value - - average_value = total_value / self.n_instance - return -average_value # Positive because we want to maximize the total value - - -if __name__ == '__main__': - - def select_next_item(remaining_capacity: int, remaining_items: List[Tuple[int, int, int]]) -> Tuple[int, int, int] | None: - """ - Select the item with the highest value-to-weight ratio that fits in the remaining capacity. - - Args: - remaining_capacity: The remaining capacity of the knapsack. - remaining_items: List of tuples containing (weight, value, index) of remaining items. - - Returns: - The selected item as a tuple (weight, value, index), or None if no item fits. - """ - best_item = None - best_ratio = -1 # Initialize with a negative value to ensure any item will have a higher ratio - - for item in remaining_items: - weight, value, index = item - if weight <= remaining_capacity: - ratio = value / weight # Calculate value-to-weight ratio - if ratio > best_ratio: - best_ratio = ratio - best_item = item - - return best_item - - - bp1d = KnapsackEvaluation() - ave_bins = bp1d.evaluate_program('_', select_next_item) - print(ave_bins) - -# Task configuration for benchmark task -ENTRY_NAME = 'select_next_item' -FUNCTION_SIGNATURE = 'def select_next_item(...):' -IMPORT_HEADER = 'import numpy as np\nimport math' -TASK_DESCRIPTION = "'" -OBJECTIVE_TEXT = "You are optimizing the implementation of `select_next_item` for the LLM4AD task.\\n\\nTask description:\\n'\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible." -TEMPLATE_FUNCTION = 'import numpy as np\n\ndef select_next_item(remaining_capacity: int, remaining_items: List[Tuple[int, int, int]]) -> Tuple[int, int, int] | None:\n """\n Select the item with the highest value-to-weight ratio that fits in the remaining capacity.\n\n Args:\n remaining_capacity: The remaining capacity of the knapsack.\n remaining_items: List of tuples containing (weight, value, index) of remaining items.\n\n Returns:\n The selected item as a tuple (weight, value, index), or None if no item fits.\n """\n best_item = None\n best_ratio = -1 # Initialize with a negative value to ensure any item will have a higher ratio\n\n for item in remaining_items:\n weight, value, index = item\n if weight <= remaining_capacity:\n ratio = value / weight # Calculate value-to-weight ratio\n if ratio > best_ratio:\n best_ratio = ratio\n best_item = item\n\n return best_item' -EVAL_CLASS_NAME = 'KnapsackEvaluation' -EVAL_KWARGS = {'timeout_seconds': 20} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_knapsack_construct/get_instance.py b/examples/benchmark_tasks/optimization_knapsack_construct/get_instance.py deleted file mode 100644 index d2d15c10..00000000 --- a/examples/benchmark_tasks/optimization_knapsack_construct/get_instance.py +++ /dev/null @@ -1,41 +0,0 @@ -import numpy as np - - -class GetData: - def __init__(self, n_instance: int, n_items: int, knapsack_capacity: int): - """ - Initialize the GetData class for the Knapsack Problem. - - Args: - n_instance: Number of instances to generate. - n_items: Number of items. - knapsack_capacity: Capacity of the knapsack. - """ - self.n_instance = n_instance - self.n_items = n_items - self.knapsack_capacity = knapsack_capacity - - def generate_instances(self): - """ - Generate instances for the Knapsack Problem. - - Returns: - A list of tuples, where each tuple contains: - - item_weights: A list of item weights. - - item_values: A list of item values. - - knapsack_capacity: The capacity of the knapsack. - """ - np.random.seed(2024) # Set seed for reproducibility - instance_data = [] - - for _ in range(self.n_instance): - # Generate random item weights, ensuring no item exceeds the knapsack capacity - item_weights = np.random.randint(10, self.knapsack_capacity / 2 + 10, size=self.n_items).tolist() - - # Generate random item values, ensuring they are positive - item_values = np.random.randint(1, 101, size=self.n_items).tolist() # Values between 1 and 100 - - # Append the instance data as a tuple (weights, values, capacity) - instance_data.append((item_weights, item_values, self.knapsack_capacity)) - - return instance_data diff --git a/examples/benchmark_tasks/optimization_knapsack_construct/paras.yaml b/examples/benchmark_tasks/optimization_knapsack_construct/paras.yaml deleted file mode 100644 index 22d30b48..00000000 --- a/examples/benchmark_tasks/optimization_knapsack_construct/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: KnapsackEvaluation -timeout_seconds: 20 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_maximal_independent_set/__init__.py b/examples/benchmark_tasks/optimization_maximal_independent_set/__init__.py deleted file mode 100644 index 17883347..00000000 --- a/examples/benchmark_tasks/optimization_maximal_independent_set/__init__.py +++ /dev/null @@ -1,261 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_maximal_independent_set -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# References: -# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language -# model agents in algorithm search for combinatorial optimization. -# arXiv preprint arXiv:2504.04310 (2025). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -import os, sys -sys.path.insert(0, os.path.dirname(__file__)) -import pathlib -import pickle -from typing import Any -import numpy as np -from llm4ad_loader import Evaluation -from llm4ad_loader import load_subdir_as_pickle -# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_pickle # Common utilities from llm4ad_loader -# from llm4ad.task.optimization.co_bench.maximal_independent_set_co_bench.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\nimport networkx as nx\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(graph: networkx.Graph):\n """\n Solve the Maximum Independent Set problem for a given test case.\n Input:\n kwargs (dict): A dictionary with the following keys:\n - graph (networkx.Graph): The graph to solve\n Returns:\n dict: A solution dictionary containing:\n - mis_nodes (list): List of node indices in the maximum independent set\n """\n # TODO: Implement your MIS solving algorithm here. Below is a placeholder.\n solution = {\n \'mis_nodes\': [0, 1, ...],\n }\n return solution' -task_description = '("The Maximum Independent Set (MIS) problem is a fundamental NP-hard optimization problem in graph "' - - -__all__ = ['MISEvaluationCB'] - - -class MISEvaluationCB(Evaluation): - - def __init__(self, - timeout_seconds=50, - **kwargs): - - """ - Args: - None - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - # Load datasets from Hugging Face as pickle files - pickle_data = load_subdir_as_pickle("CO-Bench/CO-Bench", "Maximal independent set", - include_subdirs=("er_test", "er_large_test")) - - # Organize datasets by filename (dict format preserves filenames) - self._datasets = {} - for subdir_name, graphs in pickle_data.items(): - for filename, graph in graphs.items(): - # Use filename as key, store metadata with graph as value - dataset_entry = { - 'name': filename.replace('.gpickle', ''), - 'subdir': subdir_name, - 'graph': graph, - 'filename': filename - } - self._datasets[filename] = dataset_entry - - def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: - return self.evaluate(callable_func) - - def evaluate(self, eva: callable) -> float | None: - fitness_list = [] - try: - for dataset_entry in self._datasets.values(): - # Each dataset entry already contains the graph and metadata - result = eva(dataset_entry['graph']) - fitness = self.eval_func( - name=dataset_entry['name'], - graph=dataset_entry['graph'], - mis_nodes=result['mis_nodes'], - mis_size=len(result['mis_nodes']) - ) - fitness_list.append(fitness) - - return np.mean(fitness_list) - - except ValueError as e: - print(e) - return None - - def load_data(self, input_string): - """ - Load data method for compatibility with comprehensive testing. - Since MIS task loads pickle files directly in __init__, this method - returns cases from the dictionary format. - - Args: - input_string: Dataset content (not used, but required for interface) - - Returns: - list: List of dataset entries for compatibility - """ - # Return all dataset entries as a list for compatibility with testing - return list(self._datasets.values()) - - def eval_func(self, **kwargs): - """ - Evaluate a Maximum Independent Set solution for correctness. - Args: - name (str): Name of the test case - graph (networkx.Graph): The graph that was solved - mis_nodes (list): List of nodes claimed to be in the maximum independent set - mis_size (int): Claimed size of the maximum independent set - Returns: - actual_size (int): The actual size of the provided solution - # dict: Evaluation results containing: - # - is_valid (bool): Whether the solution is a valid independent set - # - actual_size (int): The actual size of the provided solution - # - score (int): The score of the solution (0 if invalid, actual_size if valid) - # - error (str, optional): Error message if any constraint is violated - """ - - graph = kwargs['graph'] - mis_nodes = kwargs['mis_nodes'] - - # Check if mis_nodes is a list - if not isinstance(mis_nodes, list): - raise Exception("mis_nodes must be a list") - - # Check if all nodes in mis_nodes exist in the graph - node_set = set(graph.nodes()) - for node in mis_nodes: - if node not in node_set: - raise Exception(f"Node {node} in solution does not exist in graph") - - # Check for duplicates in mis_nodes - if len(mis_nodes) != len(set(mis_nodes)): - raise Exception("Duplicate nodes in solution") - - # Check if mis_size matches the length of mis_nodes - actual_size = len(mis_nodes) - - # Most important: Check if it's an independent set (no edges between any nodes) - for i in range(len(mis_nodes)): - for j in range(i + 1, len(mis_nodes)): - if graph.has_edge(mis_nodes[i], mis_nodes[j]): - raise Exception(f"Not an independent set: edge exists between {mis_nodes[i]} and {mis_nodes[j]}") - - return actual_size - - def norm_score(self, results): - optimal_scores = { - "er_large_test": [382] * 16, - "er_test": [46] * 128, - "er_valid": [46] * 100, - } - normed = {} - for case, (scores, error_message) in results.items(): - if case not in optimal_scores: - continue # Skip if there's no optimal score defined. - optimal_list = optimal_scores[case] - normed_scores = [] - # Compute normalized score for each index. - for idx, score in enumerate(scores): - if isinstance(score, (int, float)): - normed_scores.append(score / optimal_list[idx]) - else: - normed_scores.append(score) - normed[case] = (normed_scores, error_message) - - return normed - - def get_dev(self): - dev = {'er_large_test': [1, 0, 8, 10, 6], - 'er_valid': [i for i in range(100)]} - - return dev - - - - - -# Task configuration for benchmark task -ENTRY_NAME = 'solve' -FUNCTION_SIGNATURE = 'def solve(graph: networkx.Graph):' -IMPORT_HEADER = 'import numpy as np\nimport networkx as nx\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' -TASK_DESCRIPTION = '("The Maximum Independent Set (MIS) problem is a fundamental NP-hard optimization problem in graph "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The Maximum Independent Set (MIS) problem is a fundamental NP-hard optimization problem in graph "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\nimport networkx as nx\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(graph: networkx.Graph):\n """\n Solve the Maximum Independent Set problem for a given test case.\n Input:\n kwargs (dict): A dictionary with the following keys:\n - graph (networkx.Graph): The graph to solve\n Returns:\n dict: A solution dictionary containing:\n - mis_nodes (list): List of node indices in the maximum independent set\n """\n # TODO: Implement your MIS solving algorithm here. Below is a placeholder.\n solution = {\n \'mis_nodes\': [0, 1, ...],\n }\n return solution' -EVAL_CLASS_NAME = 'MISEvaluationCB' -EVAL_KWARGS = {'timeout_seconds': 60} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_maximal_independent_set/paras.yaml b/examples/benchmark_tasks/optimization_maximal_independent_set/paras.yaml deleted file mode 100644 index 6ea99df4..00000000 --- a/examples/benchmark_tasks/optimization_maximal_independent_set/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: MISEvaluationCB -timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_multi_demand_multidimensional_knapsack_problem/__init__.py b/examples/benchmark_tasks/optimization_multi_demand_multidimensional_knapsack_problem/__init__.py deleted file mode 100644 index 6a9a4778..00000000 --- a/examples/benchmark_tasks/optimization_multi_demand_multidimensional_knapsack_problem/__init__.py +++ /dev/null @@ -1,629 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_multi_demand_multidimensional_knapsack_problem -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# References: -# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language -# model agents in algorithm search for combinatorial optimization. -# arXiv preprint arXiv:2504.04310 (2025). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -from typing import Any -import numpy as np -from llm4ad_loader import Evaluation -from llm4ad_loader import load_subdir_as_text -# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader -# from llm4ad.task.optimization.co_bench.multi_demand_multidimensional_knapsack_problem_co_bench.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n:int, m:int, q:int, A_leq:list, b_leq:list, A_geq:list, b_geq:list, cost_vector:list, cost_type:str) -> dict:\n """\n Solve a given MDMKP test instance.\n Input (via kwargs):\n - n: int\n Number of decision variables.\n - m: int\n Number of <= constraints.\n - q: int\n Number of active >= constraints (subset of the full set).\n - A_leq: list of lists of int\n Coefficient matrix for <= constraints (dimensions: m x n).\n - b_leq: list of int\n Right-hand side for <= constraints (length m).\n - A_geq: list of lists of int\n Coefficient matrix for >= constraints (dimensions: q x n).\n - b_geq: list of int\n Right-hand side for >= constraints (length q).\n - cost_vector: list of int\n Objective function coefficients (length n).\n - cost_type: str\n Type of cost coefficients ("positive" or "mixed").\n Output:\n A dictionary with the following keys:\n - \'optimal_value\': int/float\n The optimal objective function value (if found).\n - \'x\': list of int\n Binary vector (0 or 1) representing the decision variable assignment.\n TODO: Implement the actual solution algorithm for the MDMKP instance.\n """\n # TODO: Define your model variables, constraints, and objective function.\n # For example, you might use an integer programming solver (e.g., PuLP, Gurobi, or another solver)\n # to model and solve the instance.\n\n # Placeholder solution:\n solution = {\n \'optimal_value\': None, # Replace with the computed objective value.\n \'x\': [0] * kwargs.get(\'n\', 0), # Replace with the computed decision vector.\n }\n return solution' -task_description = '("The Multi-Demand Multidimensional Knapsack Problem (MDMKP) is a binary optimization problem that "' - - -__all__ = ['MDMKPEvaluationCB'] - - -class MDMKPEvaluationCB(Evaluation): - - def __init__(self, - timeout_seconds=50, - **kwargs): - - """ - Args: - None - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - # Load datasets from Hugging Face - dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Multi-Demand Multidimensional Knapsack problem") - self._datasets = {} - for filename in dataset: - # Join all text rows into a single string - text_content = '\n'.join([row['text'] for row in dataset[filename]]) - self._datasets[filename] = text_content - - def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: - return self.evaluate(callable_func) - - def evaluate(self, eva: callable) -> float | None: - ins_cases = [] - for case_id, ins in enumerate(self._datasets.values()): - ins_cases.append(self.load_data(ins)) - - fitness_list = [] - try: - for i in ins_cases: - for j in i: - result = eva(j['n'], j['m'], j['q'], j['A_leq'], j['b_leq'], j['A_geq'], j['b_geq'], j['cost_vector'], j['cost_type']) - fitness = self.eval_func(n=j['n'], m=j['m'], q=j['q'], A_leq=j['A_leq'], b_leq=j['b_leq'], A_geq=j['A_geq'], b_geq=j['b_geq'], cost_vector=j['cost_vector'], cost_type=j['cost_type'], x=result['x']) - fitness_list.append(fitness) - - return np.mean(fitness_list) - - except ValueError as e: - print(e) - return None - - def load_data(self, input_string): - """ - Load MDMKP test instances from a given file and split each base instance into - 6 separate optimization problems. Each split instance is a dictionary with the keys: - - 'n': number of decision variables. - - 'm': number of <= constraints. - - 'q': number of active >= constraints for this variant (1, m//2, or m). - - 'A_leq': list of lists representing the <= constraints coefficients. - - 'b_leq': list representing the right-hand side for the <= constraints. - - 'A_geq': list of lists representing the active >= constraints coefficients (first q rows). - - 'b_geq': list representing the active right-hand side values (first q values). - - 'cost_vector': objective function coefficients for this variant. - - 'cost_type': either "positive" or "mixed". - The file format is assumed to be: - - The first line contains an integer K: the number of base test instances. - - For each instance: - * A line with two integers: n (number of variables) and m (number of <= constraints). - * m lines with n integers each: coefficients for the <= constraints. - * One line with m integers: right-hand side for the <= constraints. - * m lines with n integers each: coefficients for the >= constraints. - * One line with m integers: right-hand side for the >= constraints. - * 6 lines with n integers each: cost vectors. - - The first 3 lines correspond to the positive cost case (for q = 1, m//2, m). - - The next 3 lines correspond to the mixed cost case (for q = 1, m//2, m). - Returns: - A list of dictionaries, each representing one optimization problem variant. - """ - instances = [] - - lines = [line.strip() for line in input_string.split('\n') if line.strip() != ''] - - idx = 0 - try: - K = int(lines[idx]) - except Exception as e: - raise ValueError("The first line must be an integer indicating the number of test instances.") from e - idx += 1 - - for k in range(K): - # Read n and m for the base instance - try: - n, m = map(int, lines[idx].split()) - except Exception as e: - raise ValueError(f"Error reading n and m for test instance {k + 1} at line {idx + 1}.") from e - idx += 1 - - # Read the <= constraints matrix (A_leq): m lines each with n coefficients - A_leq = [] - for i in range(m): - row = list(map(int, lines[idx].split())) - if len(row) != n: - raise ValueError( - f"Test instance {k + 1}: Expected {n} coefficients for <= constraint {i + 1}, got {len(row)}.") - A_leq.append(row) - idx += 1 - - # Read the right-hand side for <= constraints (b_leq): one line with m integers - b_leq = list(map(int, lines[idx].split())) - if len(b_leq) != m: - raise ValueError( - f"Test instance {k + 1}: Expected {m} RHS values for <= constraints, got {len(b_leq)}.") - idx += 1 - - # Read the >= constraints matrix (A_geq): m lines each with n coefficients - A_geq = [] - for i in range(m): - row = list(map(int, lines[idx].split())) - if len(row) != n: - raise ValueError( - f"Test instance {k + 1}: Expected {n} coefficients for >= constraint {i + 1}, got {len(row)}.") - A_geq.append(row) - idx += 1 - - # Read the right-hand side for >= constraints (b_geq): one line with m integers - b_geq = list(map(int, lines[idx].split())) - if len(b_geq) != m: - raise ValueError( - f"Test instance {k + 1}: Expected {m} RHS values for >= constraints, got {len(b_geq)}.") - idx += 1 - - # Read 6 cost vectors (each with n integers) - cost_vectors = [] - for i in range(6): - vector = list(map(int, lines[idx].split())) - if len(vector) != n: - raise ValueError( - f"Test instance {k + 1}: Expected {n} values for cost vector {i + 1}, got {len(vector)}.") - cost_vectors.append(vector) - idx += 1 - - # Define the q values for the three cases - q_values = [1, m // 2, m] - - # Create 6 separate optimization problem variants. - # For the first three cost vectors: positive cost case. - # For the last three cost vectors: mixed cost case. - for i in range(6): - if i < 3: - cost_type = "positive" - q = q_values[i] - cost_vector = cost_vectors[i] - else: - cost_type = "mixed" - q = q_values[i - 3] - cost_vector = cost_vectors[i] - - # For the >= constraints, take only the first q rows and corresponding RHS values. - instance_variant = { - 'n': n, - 'm': m, - 'q': q, - 'A_leq': A_leq, - 'b_leq': b_leq, - 'A_geq': A_geq[:q], - 'b_geq': b_geq[:q], - 'cost_vector': cost_vector, - 'cost_type': cost_type - } - instances.append(instance_variant) - - return instances - - def eval_func(self, **kwargs): - """ - Evaluate a solution for a given MDMKP instance. - This function expects keyword arguments that include both the instance data and the solution. - It is designed to be used as: - eval_func(**case, **output) - where the instance 'case' contains the following keys: - - n: int - Number of decision variables. - - m: int - Number of <= constraints. - - q: int - Number of active >= constraints (subset of the full set). - - A_leq: list of lists of int (dimensions: m x n) - Coefficient matrix for <= constraints. - - b_leq: list of int (length m) - Right-hand side for <= constraints. - - A_geq: list of lists of int (dimensions: q x n) - Coefficient matrix for >= constraints. - - b_geq: list of int (length q) - Right-hand side for >= constraints. - - cost_vector: list of int (length n) - Objective function coefficients. - - cost_type: str - A string indicating the cost type ("positive" or "mixed"). - And the solver output (the solution) is expected to include at least: - - x: list of int - Binary decision vector (0 or 1) of length n. - The evaluation process is as follows: - 1. Check that the decision vector is of the proper length and binary. - 2. Verify that each <= constraint is satisfied: - For each i in 0,...,m-1, sum_{j=0}^{n-1} A_leq[i][j]*x[j] <= b_leq[i] - 3. Verify that each >= constraint is satisfied: - For each i in 0,...,q-1, sum_{j=0}^{n-1} A_geq[i][j]*x[j] >= b_geq[i] - 4. If all constraints are satisfied, compute the objective value as: - score = sum_{j=0}^{n-1} cost_vector[j] * x[j] - 5. Return the computed score. - If any constraint is violated or the input format is incorrect, the function raises a ValueError. - Returns: - score: int or float, the objective value computed from the solution. - """ - # Extract instance data - n = kwargs.get('n') - m = kwargs.get('m') - q = kwargs.get('q') - A_leq = kwargs.get('A_leq') - b_leq = kwargs.get('b_leq') - A_geq = kwargs.get('A_geq') - b_geq = kwargs.get('b_geq') - cost_vector = kwargs.get('cost_vector') - - # Extract solution data - x = kwargs.get('x') - - # Validate required keys - if None in (n, m, q, A_leq, b_leq, A_geq, b_geq, cost_vector): - raise ValueError("Missing one or more required instance keys for evaluation.") - if x is None: - raise ValueError("Solution output must include a decision vector 'x'.") - - # Validate decision vector: must be a list of length n and binary (0 or 1) - if not isinstance(x, list) or len(x) != n: - raise ValueError(f"Decision vector 'x' must be a list of length {n}.") - if any(val not in (0, 1) for val in x): - raise ValueError("Decision vector 'x' must be binary (0 or 1).") - - # Validate <= constraints: for each constraint, check feasibility. - for i in range(m): - lhs = sum(A_leq[i][j] * x[j] for j in range(n)) - if lhs > b_leq[i]: - raise ValueError(f"<= Constraint {i + 1} violated: computed sum {lhs} exceeds RHS {b_leq[i]}.") - - # Validate >= constraints: for each constraint, check feasibility. - for i in range(q): - lhs = sum(A_geq[i][j] * x[j] for j in range(n)) - if lhs < b_geq[i]: - raise ValueError(f">= Constraint {i + 1} violated: computed sum {lhs} is less than RHS {b_geq[i]}.") - - # Compute the objective value as the sum of cost_vector * x. - score = sum(cost_vector[j] * x[j] for j in range(n)) - - return score - - def norm_score(self, results): - optimal_scores = { - 'mdmkp_ct1.txt': [31376.06099098094, 28841.20208473961, 22192.899694276286, 10976.880429144556, - 10686.301154359677, 10776.09848748367, 27965.860247587698, 26830.224814550158, - 26539.177334530545, 11578.699254973362, 11463.552178478127, 11113.692459439326, - 30638.19628214322, 24049.619536529088, 21030.249006014095, 10643.70090091486, - 10453.7553665755, 10851.28935057092, 31109.826608630654, 27849.578986509467, - 21907.43232441284, 11084.204959504046, 10931.486053492556, 10617.154943664962, - 34067.48088690925, 31057.81683843506, 25461.483419257427, 11120.824982910157, - 10939.366732751803, 10501.68843539494, 52281.53172193436, 45115.128308760926, - 40697.32062752598, 18730.394778021, 17862.24431317923, 16294.366955720267, - 53807.60142950598, - 48358.464538058324, 38502.130166681985, 18588.950738387197, 17310.90043217735, - 16480.073883206154, 54772.51730504556, 49269.756608415206, 38624.595248448204, - 18018.418531206178, 16757.436775836548, 16430.385997855254, 54794.38354919037, - 49243.457471650145, 37733.710673328365, 18666.988427036664, 17552.919972379645, - 16714.50974311463, 54864.038253140265, 49912.99246733524, 39111.106147546845, - 17741.778795120015, 17454.746367665648, 15996.305483915903, 79295.85009317525, - 65084.49754064415, 52943.89178151959, 23465.48439728474, 21832.910625647823, - 19264.287770207746, 76548.45099026011, 60466.45166114487, 55279.197434974216, - 24155.813207346844, 22966.410348838213, 18983.75249797779, 73649.63988890029, - 71112.22602702447, 57946.65292077182, 23641.269675101943, 22291.717167052662, - 18741.849658805524, 81400.06764100857, 67629.03783864013, 67405.53195610014, - 24218.123407341278, 23333.215281521032, 20066.519859679578, 69297.20725459281, - 63219.49226518766, 53748.09893388699, 21812.00360812832, 19872.413381814225, - 17788.761084583686], - 'mdmkp_ct2.txt': [85737.01455327868, 78696.9991775994, 68344.04567453945, 28849.533294001798, - 26863.98196580951, 27320.612143547143, 77201.6817216863, 75444.46744561363, - 61063.789881092605, 29217.298765083375, 27075.31000291711, 26996.660180378818, - 81822.22342824662, 71256.25402393805, 62341.194601596886, 28313.55566164002, - 27523.52868077394, 27035.321578925952, 81853.34918315883, 80557.51259726346, - 66790.0712529679, 28232.37737198369, 26469.88622592108, 26245.96998646702, - 81785.92659972196, - 71222.49036491562, 62160.1118806897, 29281.210740186492, 27484.81483365757, - 26451.244595144035, 152744.10289701147, 131201.8397279085, 128183.09582390457, - 48530.504947691516, 44624.04171250145, 41734.55641636365, 137680.65156421435, - 131378.93642009562, 119993.19346841935, 47923.46930072854, 44961.232653882784, - 42156.49791254687, 144428.7158031205, 134533.43016588953, 103006.65157231665, - 47913.187589197136, 43826.18755568891, 42798.53780623339, 138875.9989433622, - 119974.20395118099, 108237.50680627486, 48076.91709819546, 43690.33318025998, - 41272.888199935616, 136680.00739689753, 128922.92966800612, 114619.70175377218, - 49199.94515582458, 45910.55304695518, 43797.21240710909, 220622.55996837773, - 194379.6945304465, 162305.7537748959, 60762.10209518433, 54341.222517811606, - 48668.07407973811, 199563.43979134466, 165580.50491656526, 175037.9382193091, - 61530.28209895772, 56104.68838548819, 47996.20454740994, 224140.1857862799, - 186790.55426756508, 150832.88991816493, 62759.54862209718, 57610.3818509872, - 50924.4636364738, 189913.657037726, 171291.44385422583, 150388.56765837915, - 61219.44818444248, 56034.59960390541, 47761.792950168354, 198167.53454359408, - 186238.7291392124, 154679.97893410415, 59218.96126115047, 54808.378420106, - 47917.408017869006], - 'mdmkp_ct3.txt': [168814.0860184554, 143266.38476611354, 137156.91757617562, 57894.150493004636, - 54801.499428939074, 53517.711310484585, 172610.2292039204, 160115.6472575543, - 145496.39844883644, 59390.29179783931, 54289.72547227832, 53024.46694919935, - 167567.35510673033, 164729.93693986148, 135420.79514986262, 60115.06815508855, - 55813.36492966956, 56352.20430613357, 172688.82134408774, 162319.81137900034, - 130344.96923837559, 59563.69109849362, 55603.0488768423, 56809.66042438546, - 170206.74300980923, 157900.84535984247, 131060.13019116307, 59531.600760548616, - 55559.28618907818, 54752.018503499545, 302114.49873024924, 256581.55631994538, - 207732.980425423, 97439.41910679372, 90516.99034978052, 85260.28922281954, - 312465.62841811427, 271474.10840658314, 225440.37664729697, 98799.08250295873, - 90273.14037722594, 87477.24047531083, 297238.32753707597, 303609.97451424616, - 225541.924960721, 98443.46583899345, 91397.1203183814, 86432.96390184318, - 306816.02212839137, - 280040.73064957885, 221762.73815399208, 98664.8071685896, 91138.37204475686, - 87776.28663778222, 318569.3817006032, 291852.5925075597, 236866.72831430216, - 98327.48388000038, 90336.45570858807, 86101.47544617752, 415194.2465573161, - 362491.8427350558, 307377.7603135135, 123584.46297508608, 111764.25699100617, - 98085.19170604613, 433314.1493530444, 364569.17167671275, 297531.23157215375, - 125913.7470940513, 114772.6222478601, 98973.94195326211, 406167.4870924763, - 416022.1099206263, 347267.15219282993, 125246.56624027551, 113042.34566405955, - 98644.59205787636, 431000.6087227007, 347398.03921355103, 316049.181248464, - 124957.64024309818, 115292.0983954651, 98821.88400173541, 390733.65163432877, - 386296.45713615883, 312349.0075675711, 125461.39250902845, 113530.28945365804, - 99827.79213964228], - 'mdmkp_ct4.txt': [29164.038872705965, 22448.128690234324, 22750.94208136601, 11324.083285011215, - 10719.956309350773, 9430.485533330844, 27413.704538415757, 21461.687993511725, - 20807.526347746276, 11174.57205823707, 10523.515868282093, 9480.294964657856, - 27679.789721276044, 20314.35486741767, 20078.725511933364, 11321.877876900408, - 10569.208944470603, 10037.671236768076, 27275.964278206353, 21403.189994100558, - 18214.689756802898, 11764.756991232283, 11403.330730547532, 10754.684755455568, - 27408.560904311627, 20239.00788536538, 19675.454531748826, 11103.196531870448, - 10870.885634988072, 9778.676576921001, 47914.26841337081, 36755.00142928597, - 34496.23241640505, 19355.6131185271, 17043.422551538544, 14963.241879022014, - 48424.800879436036, 37796.04860563597, 34252.66746620095, 20004.46730023067, - 18835.536671157857, 16372.729959011156, 46056.593752138586, 34203.22184902439, - 32098.336710050586, 20150.91866979717, 18742.934262975916, 16261.904248836905, - 46606.25108291802, 38692.27805531054, 34116.31787218311, 19892.13679537105, - 17906.171672142802, 15457.930528107207, 50837.276577590026, 38270.19850132687, - 33994.983617245525, 19964.04689543776, 18102.368498531567, 15557.02649862105, - 66492.56132398843, 48695.57734529988, 41728.99175538258, 26020.71441769906, - 21704.154628167817, 17058.040076600046, 66446.61120480338, 47164.040871164994, - 48144.90458804947, 26104.088009067298, 23025.98793294367, 17964.932081561263, - 69551.03401323149, 59291.08777980967, 47802.32806977924, 26495.990533074244, - 22596.77082251083, 17536.90097464368, 62631.5014743124, 58643.30592840052, - 49398.5074628221, - 25760.06015043017, 22890.929818858796, 17713.170731163762, 70181.38883730803, - 48918.371779400695, 49643.56705073802, 26572.11174570738, 23037.056763652552, - 16705.942545272545], - 'mdmkp_ct5.txt': [67074.72332396788, 56922.72071257537, 52889.19458254509, 29909.01523234807, - 27546.15586023913, 27256.80532272495, 77868.54700099613, 60229.72049072842, - 54149.53266748691, 30249.223526187412, 27120.038198121416, 26594.844588672448, - 70119.79201060643, 55514.79683891681, 47439.357264372185, 30134.92269436036, - 26259.336998000002, 27193.319825225342, 67901.56262102553, 53001.06387129924, - 55281.744348889064, 30069.120783816663, 27702.98526921436, 27356.78776039429, - 69629.5298376119, 58835.15928545275, 50230.15054431706, 30166.73357407277, - 27364.166305029587, 27330.144292096822, 119164.81505401935, 100397.28774329153, - 93469.74355268248, 51930.86507009473, 46769.19322469853, 43469.001177917045, - 131222.81251695874, 108833.88808461775, 90059.43430489714, 50740.658976924875, - 46235.81493273908, 43496.78460049598, 128866.81838327541, 108711.7891535098, - 86752.92311339389, 50998.811367235605, 45928.679572754314, 42680.52542881308, - 127143.08694841203, 102644.96769290596, 85141.97072163413, 51670.6446424105, - 46178.15119491831, 43412.42326287415, 130750.38067186893, 108385.55051038244, - 88052.26497421459, 51783.509375019516, 45886.267591475684, 42860.85744149264, - 169045.83770196422, 128460.7780770426, 118442.91466652084, 65910.00741807284, - 56072.77414670372, 45324.6047968282, 169920.60488581128, 130133.63844405038, - 121416.77168002189, 65784.1330038083, 56518.658717892344, 46831.28106896001, - 174541.92749152833, 129928.59851393547, 108508.93425522567, 66702.65101118234, - 56108.915404156476, 46214.73426365582, 163110.9330947666, 134212.68379176338, - 125886.72283652119, 68024.49481755303, 57037.86726713851, 46268.360529372236, - 173960.27635069186, 136162.4432373785, 112316.86040339211, 66543.19030194926, - 56539.515264863825, 46146.95781182992], - 'mdmkp_ct6.txt': [144980.23407910476, 112763.51393780176, 102383.66646450528, 59980.573187587404, - 52939.47573714053, 53771.685494620295, 154257.03187626274, 124883.9953049457, - 120642.0249017839, 60019.79547885496, 53897.85764198486, 54161.532879947954, - 144534.53457044237, 117720.3220895322, 96475.2443776782, 58954.60041811628, - 54069.08130394793, 54323.06299600842, 157814.49413999054, 122314.66949077391, - 100134.14416493346, 59884.60813014079, 53973.0450434286, 54587.50902401689, - 151422.05437238456, 122973.61948453542, 112057.21433922206, 58594.09015818635, - 53920.68270934412, 54374.5373291473, 282848.98344492953, 224326.69691858994, - 183304.07819034575, 101427.42770624388, 90233.59162281707, 87931.83988695969, - 269408.01082357974, 225474.35073228314, 215456.65002890778, 102416.03111190387, - 90069.81910015695, 85116.5260466803, 254947.89675161077, 218974.4686576769, - 186749.28559522133, 102917.6477109319, 91441.73103545069, 88999.20238685762, - 269816.67061679246, 210858.44227739258, 203583.16500971685, 104182.59994978322, - 88357.42788500736, 85767.49572881762, 272744.18752231536, 193951.70467221903, - 188257.8254527853, 101785.26150797169, 89213.54586549665, 85484.81249285143, - 381542.4217357743, 326469.342237431, 282353.7716418138, 128661.62461491564, - 108999.45692138896, 95010.49588600297, 356708.95381429413, 304916.8454043936, - 251063.8372197164, 130181.26485675877, 107033.53612376121, 94649.81679638875, - 361613.35129891743, 306735.42153189424, 297995.04983937036, 129864.48025022763, - 108951.57993028058, 93125.75190483894, 372946.5256811151, 312169.39120588027, - 291961.601990496, 128139.33325293686, 107763.57401113368, 92037.90261510992, - 371106.7889048268, 276728.6811577454, 264946.69248258974, 130334.98487932215, - 107565.0032849825, 94861.48458289343], - 'mdmkp_ct7.txt': [21536.667021014287, 15306.883012189614, 13443.216809193567, 11390.815478460714, - 8894.856170390527, 8519.060387333655, 21705.543635545953, 15173.62619468222, - 13038.599272654503, 12408.558146390122, 9797.67354382143, 8876.007748591903, - 23578.57658086318, 17182.951675388776, 13230.830577072633, 12537.577858898758, - 9219.568400973225, 7977.518849705143, 22053.315182257193, 14764.620511312056, - 13892.13285878336, 12425.658398919977, 9739.394008046469, 8885.452593071088, - 22313.07495965335, 15609.53760642277, 13245.694637863515, 12055.917630568516, - 9627.467383903446, 8162.209064269218, 42173.69463633543, 29877.880449919074, - 25440.078288591638, 21825.455611063735, 16549.449355399665, 13971.022911392056, - 43664.20300776704, 29464.902018501187, 24112.721453411297, 22513.26234961254, - 16629.759894115432, 13458.744980317182, 41039.61174386542, 29361.952810329356, - 25395.63206161855, 22057.01699394085, 16648.10129596265, 14013.12188134602, - 43085.507865128704, 30345.55473923687, 25186.272538620593, 21913.134339634285, - 16410.724181692833, 13953.82361078376, 44823.613253516436, 30543.80340170374, - 26300.182116669628, 22188.202993274637, 16580.805936525514, 12952.062092272812, - 56519.740845824, 42915.67901994479, 36709.844924364166, 29814.67594985235, - 21301.929554223458, 15512.614795423231, 59358.39167893502, 40874.51794050845, - 34902.226748636676, 28713.930949486898, 21518.41831664319, 15945.794740345258, - 64920.55408589671, 37197.96324958661, 35058.404365245566, 30202.452304810613, - 20831.238717184224, 15176.318443957553, 60911.53631172457, 42914.45741637827, - 37188.39746443595, 29560.00507125548, 21757.49688641164, 15066.861187060727, - 59162.50291552842, 37851.22602477869, 33511.28841330884, 29822.916196666672, - 20755.309519582046, 14846.520309418032], - 'mdmkp_ct8.txt': [65991.67034165592, 39986.57332809463, 36598.09000404739, 31686.372556660783, - 25445.141538461943, 25077.05114636876, 63979.22183114905, 43126.26992981033, - 38628.88407519385, 30302.17299689071, 25856.008803794008, 26177.344337965726, - 65138.034210076374, 44907.343983147846, 41550.321250392364, 31845.384596822078, - 25791.469096917182, 26506.638584217988, 64634.00404312337, 40174.8200604331, - 37334.96401267335, 30606.32307685129, 25945.06525360605, 26187.72721124403, - 65280.72384563691, 39834.38753008512, 36962.42645358037, 30687.36207486381, - 25679.89696314449, 25369.83962024141, 113030.16628540732, 82740.77198899978, - 73895.62791600019, 55638.48680489044, 44334.35576064882, 40312.02329612095, - 116780.09817051327, 76575.7430130301, 67790.62525433657, 54232.24837998093, - 44014.019831706864, 42095.043637196846, 113131.19405348024, 82687.33750069182, - 73711.02052653379, 53630.823367916724, 44179.40462430917, 40583.47254003538, - 117795.90163898062, 79370.86436229874, 70043.37406542819, 56012.97811055305, - 43195.84496979824, 41088.50539662127, 117372.85927796275, 81505.10700293747, - 75489.689213593, - 52990.92694419264, 43272.74841650926, 39717.78170853508, 160852.0754812623, - 106590.67841439568, 99150.39399824802, 68409.80507279171, 52921.09290099409, - 43061.27479238256, 169355.20414890588, 112529.5560624529, 95892.00602546158, - 70889.48098034738, 53229.92344418698, 44841.2020442713, 162207.84790491065, - 108897.66487579771, 86526.99526234265, 71872.14878415015, 53655.04733850709, - 41896.08057194517, 162405.8574386429, 112025.48285525134, 100112.58173915549, - 73823.0844300016, 53451.75195169678, 44515.54400782727, 157038.53409987607, - 110402.29049724352, 95649.38172715022, 71516.83170706524, 53422.2982374228, - 43167.068559292675], - 'mdmkp_ct9.txt': [134758.3549892344, 86909.00762356113, 86982.49992050465, 62340.79946652035, - 52415.14256530901, 53232.920819668056, 128419.67845418965, 88901.5566742058, - 83992.02809346681, 63360.38586246625, 51893.2045344762, 52256.21901538081, - 127064.63498233976, 89453.5086261967, 79351.25774203127, 62032.14391662491, - 51631.24745514526, 52782.4609329052, 128368.08791816013, 90245.11312968897, - 84131.37307719128, 63133.2050746076, 52123.626492230534, 53162.85131488812, - 129825.81285949677, 90096.64235781695, 85277.83477898309, 63245.06600443, - 52549.65434095841, - 53367.474150258815, 239342.2155683403, 161158.5613710625, 147529.19830464877, - 111487.10174717069, 88339.35475314604, 85961.16276741328, 248369.26195392013, - 162009.4593849904, 153842.18958281272, 110110.75575503791, 86496.15756377159, - 85119.35754565377, 250539.4853694354, 167117.75419544292, 159374.2650101431, - 109304.25500648574, 86773.9066627287, 84813.75401106518, 245446.27414940202, - 169228.10912750015, 155884.4962464008, 108544.66757407523, 85582.42521344902, - 84874.08841589266, 245923.51801080085, 163298.31207544284, 143918.62121275134, - 108315.8596996154, 87105.7025967709, 84940.87088406476, 328053.7136147506, - 225215.22929174392, 186813.67930702146, 143202.56393579207, 104756.76964530846, - 89882.77399645871, 328408.6333206934, 234918.00365513685, 196065.373602686, - 141788.8644964311, 105420.40686888609, 88356.57745002235, 324087.92881999584, - 234211.97225764123, 209812.7317955713, 143874.73260227023, 106052.15296672149, - 88147.09916517115, 341135.49211315066, 243868.20006443554, 217462.7234056969, - 143414.35002359937, 105223.51930478415, 88613.1445228776, 336605.35161074856, - 210327.86701928257, 195984.09990534862, 141107.11065593347, 104153.81949061107, - 88866.18621642572] - } - - normed = {} - for case, (scores, error_message) in results.items(): - if case not in optimal_scores: - continue # Skip if there's no optimal score defined. - optimal_list = optimal_scores[case] - normed_scores = [] - # Compute normalized score for each index. - for idx, score in enumerate(scores): - if isinstance(score, (int, float)): - normed_scores.append(score / optimal_list[idx]) - else: - normed_scores.append(score) - normed[case] = (normed_scores, error_message) - - return normed - - def get_dev(self): - dev = { - 'mdmkp_ct1.txt': [43, 4, 30, 72, 52, 62, 33, 73, 64, 37, 17, 36, 59, 38, 13, 9, 22, 57, 15, 44, 87, 55, 2, - 29, - 39, 50, 11, 54, 79, 19, 10, 0, 47, 5, 18, 83, 45, 88, 81, 35, 75, 27, 70, 61, 67], - 'mdmkp_ct2.txt': [76, 25, 72, 79, 59, 55, 58, 67, 70, 5, 81, 54, 77, 51, 78, 39, 4, 84, 0, 47, 48, 24, 27, - 69, - 49, 34, 53, 26, 89, 73, 21, 37, 29, 10, 52, 15, 23, 46, 88, 60, 19, 64, 12, 20, 71], - 'mdmkp_ct3.txt': [4, 7, 0, 60, 57, 48, 37, 13, 45, 65, 74, 20, 80, 17, 43, 46, 67, 33, 83, 77, 32, 86, 41, - 26, - 70, 34, 75, 21, 47, 56, 84, 14, 25, 5, 88, 24, 9, 28, 2, 66, 85, 81, 69, 58, 18], - 'mdmkp_ct4.txt': [73, 38, 62, 32, 34, 71, 17, 19, 63, 20, 30, 55, 65, 45, 4, 22, 86, 48, 75, 23, 41, 59, 79, - 3, - 14, 83, 36, 72, 87, 9, 40, 44, 53, 15, 47, 74, 68, 67, 24, 28, 57, 27, 77, 89, 37], - 'mdmkp_ct5.txt': [35, 7, 69, 39, 84, 9, 44, 62, 53, 32, 72, 71, 13, 83, 12, 33, 66, 17, 59, 51, 14, 56, 0, - 16, - 54, 52, 65, 41, 75, 46, 89, 64, 48, 61, 28, 77, 68, 19, 36, 50, 88, 82, 80, 22, 27], - 'mdmkp_ct6.txt': [2, 69, 38, 19, 79, 66, 73, 7, 82, 33, 49, 64, 85, 89, 14, 9, 23, 40, 25, 10, 17, 31, 58, - 78, - 11, 74, 1, 46, 60, 28, 71, 88, 39, 62, 77, 72, 50, 22, 16, 84, 51, 53, 56, 20, 13], - 'mdmkp_ct7.txt': [68, 37, 23, 21, 71, 36, 80, 18, 0, 58, 78, 25, 26, 73, 19, 81, 7, 38, 67, 6, 77, 52, 11, - 57, - 86, 42, 50, 56, 82, 89, 48, 61, 53, 24, 74, 70, 43, 30, 47, 14, 69, 63, 3, 22, 44], - 'mdmkp_ct8.txt': [47, 54, 56, 87, 69, 81, 63, 6, 26, 53, 3, 83, 52, 23, 82, 57, 1, 78, 5, 13, 42, 80, 30, - 19, - 11, 37, 36, 61, 46, 21, 71, 35, 84, 49, 67, 70, 55, 44, 51, 12, 86, 74, 72, 45, 8], - 'mdmkp_ct9.txt': [23, 72, 78, 70, 68, 61, 52, 12, 56, 69, 35, 21, 31, 3, 25, 30, 66, 1, 54, 83, 89, 26, 49, - 65, - 40, 20, 57, 7, 5, 74, 44, 42, 85, 77, 87, 76, 45, 2, 86, 10, 48, 29, 46, 51, 13]} - - return dev - - - - - -# Task configuration for benchmark task -ENTRY_NAME = 'solve' -FUNCTION_SIGNATURE = 'def solve(...):' -IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' -TASK_DESCRIPTION = '("The Multi-Demand Multidimensional Knapsack Problem (MDMKP) is a binary optimization problem that "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The Multi-Demand Multidimensional Knapsack Problem (MDMKP) is a binary optimization problem that "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n:int, m:int, q:int, A_leq:list, b_leq:list, A_geq:list, b_geq:list, cost_vector:list, cost_type:str) -> dict:\n """\n Solve a given MDMKP test instance.\n Input (via kwargs):\n - n: int\n Number of decision variables.\n - m: int\n Number of <= constraints.\n - q: int\n Number of active >= constraints (subset of the full set).\n - A_leq: list of lists of int\n Coefficient matrix for <= constraints (dimensions: m x n).\n - b_leq: list of int\n Right-hand side for <= constraints (length m).\n - A_geq: list of lists of int\n Coefficient matrix for >= constraints (dimensions: q x n).\n - b_geq: list of int\n Right-hand side for >= constraints (length q).\n - cost_vector: list of int\n Objective function coefficients (length n).\n - cost_type: str\n Type of cost coefficients ("positive" or "mixed").\n Output:\n A dictionary with the following keys:\n - \'optimal_value\': int/float\n The optimal objective function value (if found).\n - \'x\': list of int\n Binary vector (0 or 1) representing the decision variable assignment.\n TODO: Implement the actual solution algorithm for the MDMKP instance.\n """\n # TODO: Define your model variables, constraints, and objective function.\n # For example, you might use an integer programming solver (e.g., PuLP, Gurobi, or another solver)\n # to model and solve the instance.\n\n # Placeholder solution:\n solution = {\n \'optimal_value\': None, # Replace with the computed objective value.\n \'x\': [0] * kwargs.get(\'n\', 0), # Replace with the computed decision vector.\n }\n return solution' -EVAL_CLASS_NAME = 'MDMKPEvaluationCB' -EVAL_KWARGS = {'timeout_seconds': 60} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_multi_demand_multidimensional_knapsack_problem/paras.yaml b/examples/benchmark_tasks/optimization_multi_demand_multidimensional_knapsack_problem/paras.yaml deleted file mode 100644 index 2394fe1f..00000000 --- a/examples/benchmark_tasks/optimization_multi_demand_multidimensional_knapsack_problem/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: MDMKPEvaluationCB -timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_multidimensional_knapsack_problem/__init__.py b/examples/benchmark_tasks/optimization_multidimensional_knapsack_problem/__init__.py deleted file mode 100644 index 35588dd6..00000000 --- a/examples/benchmark_tasks/optimization_multidimensional_knapsack_problem/__init__.py +++ /dev/null @@ -1,496 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_multidimensional_knapsack_problem -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# References: -# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language -# model agents in algorithm search for combinatorial optimization. -# arXiv preprint arXiv:2504.04310 (2025). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -from typing import Any -import numpy as np -from llm4ad_loader import Evaluation -from llm4ad_loader import load_subdir_as_text -# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader -# from llm4ad.task.optimization.co_bench.multidimensional_knapsack_problem_co_bench.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n: int, m: int, p: list, r: list, b: list) -> dict:\n """\n Solves a multidimensional knapsack problem instance.\n Input kwargs (for one test case):\n - n: int, number of decision variables.\n - m: int, number of constraints.\n - p: list of floats, profit coefficients (length n).\n - r: list of m lists, each of length n, representing the resource consumption per constraint.\n - b: list of floats, right-hand side values for each constraint (length m).\n Evaluation metric:\n The score is computed as:\n score = sum(p[j] * x[j] for j in range(n))\n if and only if all constraints are satisfied—that is, for every constraint i, the total resource consumption\n sum(r[i][j] * x[j] for j in range(n))\n does not exceed b[i].\n If any constraint is violated, the solution receives no score. A higher score is better.\n Returns:\n A dict with key \'x\' whose value is a list of n binary decisions (0 or 1).\n """\n # Placeholder implementation: a dummy solution that selects no items.\n x = [0] * kwargs[\'n\']\n return {\'x\': x}' -task_description = '("This problem is a multidimensional knapsack optimization where the objective is to maximize the "' - - -__all__ = ['MKPEvaluationCB'] - - -class MKPEvaluationCB(Evaluation): - - def __init__(self, - timeout_seconds=300, - **kwargs): - - """ - Args: - None - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - # Load datasets from Hugging Face - dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Multidimensional knapsack problem") - self._datasets = {} - for filename in dataset: - # Join all text rows into a single string - text_content = '\n'.join([row['text'] for row in dataset[filename]]) - self._datasets[filename] = text_content - - def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: - return self.evaluate(callable_func) - - def evaluate(self, eva: callable) -> float | None: - ins_cases = [] - for case_id, ins in enumerate(self._datasets.values()): - ins_cases.append(self.load_data(ins)) - - fitness_list = [] - try: - for i in ins_cases: - for j in i: - result = eva(j['n'], j['m'], j['p'], j['r'], j['b']) - fitness = self.eval_func(j['n'], j['m'], j['p'], j['r'], j['b'], result['x'], j['opt']) - fitness_list.append(fitness) - - return np.mean(fitness_list) - - except ValueError as e: - print(e) - return None - - def load_data2(self, input_path): - """ - Loads instance(s) from the OR-Library mknap2.txt file. - This file contains many lines of commentary and then one or more instances. - Each instance is defined (after removing comments) as: - <#knapsacks> <#objects> - (there will be exactly #objects numbers) - (exactly #knapsacks numbers) - (#objects rows, each with #knapsacks numbers) - [] (an extra token, optional) - In our formulation: - - the number of decision variables (n) is set to the number of objects, - - the number of constraints (m) is set to the number of knapsacks, - - the profit coefficients p are taken equal to the object weights, - - the constraint coefficients r are taken from the matrix (transposed so that each - constraint i gets a list of consumptions for all objects), - - the right-hand sides b are the knapsack capacities. - Returns: - A list of dictionaries. Each dictionary corresponds to one problem instance and - has the keys: - 'n' : int, number of objects (decision variables) - 'm' : int, number of knapsacks (constraints) - 'p' : list of floats, profit coefficients (length n) - 'r' : list of m lists of floats, where each inner list is of length n (constraint coefficients) - 'b' : list of floats, knapsack capacities (length m) - If the instance file also provides an optimum value, it is stored under key 'opt'. - """ - cases = [] - all_lines = [line.strip() for line in input_string.split('\n')] - - # Remove comments (anything after '//') and extra whitespace. - cleaned_lines = [] - for line in all_lines: - line = line.split("//")[0] - line = line.strip() - if line: - cleaned_lines.append(line) - - # Gather all tokens (they may come from several lines) - tokens = [] - for line in cleaned_lines: - tokens.extend(line.split()) - - # Process tokens sequentially looking for candidate instance headers. - # The expected header is two positive numbers: (#knapsacks, #objects). - i = 0 - N = len(tokens) - while i < N - 1: - try: - # Try to read two numbers as candidate header - knapsacks = int(float(tokens[i])) - objects = int(float(tokens[i + 1])) - except Exception: - i += 1 - continue - - # Basic validity check: both numbers must be positive. - if knapsacks <= 0 or objects <= 0: - i += 1 - continue - - # Once a candidate header is found, compute the expected number of tokens: - # header already consumed: 2 tokens - # then: object weights: objects tokens - # then: knapsack capacities: knapsacks tokens - # then: resource consumption matrix: objects * knapsacks tokens - # Optionally: one token for known optimum. - required = objects + knapsacks + (objects * knapsacks) - # Check if there is at least the required number of tokens after the header. - if i + 2 + required > N: - # Not enough tokens left; break out. - break - - # Consume header. - i += 2 - - # Read object weights (which we use as profit coefficients). - weights = [] - for _ in range(objects): - weights.append(float(tokens[i])) - i += 1 - - # Read knapsack capacities. - capacities = [] - for _ in range(knapsacks): - capacities.append(float(tokens[i])) - i += 1 - - # Read the resource consumption matrix. - # The file gives a matrix with 'objects' rows and 'knapsacks' columns. - matrix = [] - for _ in range(objects): - row = [] - for _ in range(knapsacks): - row.append(float(tokens[i])) - i += 1 - matrix.append(row) - - # Optionally, read the known optimum if present. - optimum = None - if i < N: - # We treat the next token as optimum if it is a number. - try: - optimum = float(tokens[i]) - i += 1 - except Exception: - optimum = None - - # Convert the data to our formulation: - # Decision variables: one per object. - # Constraints: one per knapsack. - # Profit coefficients p: equal to the object weights. - # Constraint coefficients r: we need to transpose the matrix so that for each knapsack, - # we get the consumption for each object. - p = weights - r = [] - for k in range(knapsacks): - constraint_coeffs = [] - for obj in range(objects): - constraint_coeffs.append(matrix[obj][k]) - r.append(constraint_coeffs) - b = capacities - - case = {'n': objects, 'm': knapsacks, 'p': p, 'r': r, 'b': b} - if optimum is not None: - case['opt'] = optimum - cases.append(case) - - return cases - - def load_data(self, input_string): - """ - Reads the input string and returns a list of test cases. - Each case is represented as a dictionary containing: - - 'n': number of decision variables. - - 'm': number of constraints. - - 'p': list of floats, profit coefficients. - - 'r': list of m lists of floats, constraint coefficients. - - 'b': list of floats, right-hand side values. - """ - # Simple check for mknap2 format - for now, use default format - # if 'mknap2' in input_path: - # return self.load_data2(input_path) - - tokens = input_string.split() - - token_index = 0 - try: - K = int(tokens[token_index]) - except Exception as e: - raise ValueError("The first token must be an integer indicating the number of test cases.") from e - token_index += 1 - - cases = [] - for case_index in range(K): - try: - n = int(tokens[token_index]) - m = int(tokens[token_index + 1]) - opt_val = float(tokens[token_index + 2]) - except Exception as e: - raise ValueError(f"Error reading header for test case {case_index + 1}.") from e - token_index += 3 - - p = [] - for j in range(n): - try: - p.append(float(tokens[token_index])) - except Exception as e: - raise ValueError(f"Error reading profit coefficient {j + 1} for test case {case_index + 1}.") from e - token_index += 1 - - r = [] - for i in range(m): - row = [] - for j in range(n): - try: - row.append(float(tokens[token_index])) - except Exception as e: - raise ValueError( - f"Error reading constraint coefficient for constraint {i + 1}, variable {j + 1} in test case {case_index + 1}.") from e - token_index += 1 - r.append(row) - - b = [] - for i in range(m): - try: - b.append(float(tokens[token_index])) - except Exception as e: - raise ValueError( - f"Error reading right-hand side value {i + 1} for test case {case_index + 1}.") from e - token_index += 1 - - case_data = { - 'n': n, - 'm': m, - 'p': p, - 'r': r, - 'b': b, - 'opt': opt_val - } - cases.append(case_data) - - return cases - - def eval_func(self, n, m, p, r, b, x, opt=None): - """ - Evaluates the solution for a multidimensional knapsack problem instance. - Inputs: - - n: int, number of decision variables. - - m: int, number of constraints. - - p: list of floats, profit coefficients (length n). - - r: list of m lists of floats, each representing the constraint coefficients. - - b: list of floats, right-hand side values for each constraint (length m). - - x: list of ints (0 or 1), the solution decisions (length n). - - opt (float, optional): The known optimal (or best-known) objective value. - This parameter is provided by instances loaded via load_data2, if available. - Evaluation: - - The objective value is computed as: - sum(p[j] * x[j] for j in range(n)) - - For each constraint i, the total resource consumption is computed as: - sum(r[i][j] * x[j] for j in range(n)) - - If any constraint i is violated (i.e., the consumption exceeds b[i]), an error is raised. - - If all constraints are satisfied, the score is equal to the objective value. - Returns: - - If opt is not provided (None), returns a float representing the overall quality score. - - If opt is provided, returns a tuple: - (score, gap) - where gap is defined as (score - opt), which indicates how far (or above) - the computed score is relative to the known optimum. - """ - tol = 1e-6 - - # Compute objective value. - objective_value = sum(p[j] * x[j] for j in range(n)) - - # Check each constraint; raise an error if any constraint is violated. - for i in range(m): - lhs = sum(r[i][j] * x[j] for j in range(n)) - if lhs - b[i] > tol: - raise ValueError(f"Constraint violation in constraint {i}: consumption {lhs} exceeds limit {b[i]}.") - - # If all constraints are satisfied, score is the objective value. - score = objective_value - - # Return either score alone or (score, gap) if optimum is provided. - if opt is not None: - gap = score - opt - return score - else: - return score - - def norm_score(self, results): - optimal_scores = { - "mknap1.txt": [3800, 8706.1, 4015, 6120, 12400, 10618, 16537], - "mknap2.txt": [7772.0, 8722.0, 141278.0, 130883.0, 95677.0, 119337.0, 98796.0, 130623.0, 1095445.0, - 624319.0, - 4554.0, 4536.0, 4115.0, 4561.0, 4514.0, 5557.0, 5567.0, 5605.0, 5246.0, 6339.0, 5643.0, - 6339.0, - 6159.0, 6954.0, 7486.0, 7289.0, 8633.0, 9580.0, 7698.0, 9450.0, 9074.0, 8947.0, 8344.0, - 10220.0, - 9939.0, 9584.0, 9819.0, 9492.0, 9410.0, 11191.0, 3090.0, 3186.0, 95168.0, 2139.0, 776.0, - 1035.0, - 3418.0, 3186.0], - "mknapcb1.txt": [24381, 24274, 23551, 23534, 23991, 24613, 25591, 23410, 24216, 24411, 42757, 42545, 41968, - 45090, 42218, 42927, 42009, 45020, 43441, 44554, 59822, 62081, 59802, 60479, 61091, 58959, - 61538, 61520, 59453, 59965], - "mknapcb2.txt": [59312, 61472, 62130, 59446, 58951, 60056, 60414, 61472, 61885, 58959, 109109, 109841, - 108489, - 109383, 110720, 110256, 109016, 109037, 109957, 107038, 149659, 155940, 149316, 152130, - 150353, - 150045, 148607, 149772, 155075, 154662], - "mknapcb3.txt": [120130, 117837, 121109, 120798, 122319, 122007, 119113, 120568, 121575, 120699, 218422, - 221191, - 217534, 223558, 218962, 220514, 219987, 218194, 216976, 219693, 295828, 308077, 299796, - 306476, - 300342, 302560, 301322, 306430, 302814, 299904], - "mknapcb4.txt": [23064, 22801, 22131, 22772, 22751, 22777, 21875, 22635, 22511, 22702, 41395, 42344, 42401, - 45624, 41884, 42995, 43559, 42970, 42212, 41207, 57375, 58978, 58391, 61966, 60803, 61437, - 56377, 59391, 60205, 60633], - "mknapcb5.txt": [59187, 58662, 58094, 61000, 58092, 58803, 58607, 58917, 59384, 59193, 110863, 108659, - 108932, - 110037, 108423, 110841, 106075, 106686, 109825, 106723, 151790, 148772, 151900, 151275, - 151948, - 152109, 153131, 153520, 149155, 149704], - "mknapcb6.txt": [117726, 119139, 119159, 118802, 116434, 119454, 119749, 118288, 117779, 119125, 217318, - 219022, - 217772, 216802, 213809, 215013, 217896, 219949, 214332, 220833, 304344, 302332, 302354, - 300743, - 304344, 301730, 304949, 296437, 301313, 307014], - "mknapcb7.txt": [21946, 21716, 20754, 21464, 21814, 22176, 21799, 21397, 22493, 20983, 40767, 41304, 41560, - 41041, 40872, 41058, 41062, 42719, 42230, 41700, 57494, 60027, 58025, 60776, 58884, 60011, - 58132, 59064, 58975, 60603], - "mknapcb8.txt": [56693, 58318, 56553, 56863, 56629, 57119, 56292, 56403, 57442, 56447, 107689, 108338, - 106385, - 106796, 107396, 107246, 106308, 103993, 106835, 105751, 150083, 149907, 152993, 153169, - 150287, - 148544, 147471, 152841, 149568, 149572], - 'mknapcb9.txt': [115868, 114667, 116661, 115237, 116353, 115604, 113952, 114199, 115247, 116947, 217995, - 214534, - 215854, 217836, 215566, 215762, 215772, 216336, 217290, 214624, 301627, 299985, 304995, - 301935, - 304404, 296894, 303233, 306944, 303057, 300460] - } - - normed = {} - for case, (scores, error_message) in results.items(): - if case not in optimal_scores: - continue # Skip if there's no optimal score defined. - optimal_list = optimal_scores[case] - normed_scores = [] - # Compute normalized score for each index. - for idx, score in enumerate(scores): - if isinstance(score, (int, float)): - normed_scores.append(score / optimal_list[idx]) - else: - normed_scores.append(score) - normed[case] = (normed_scores, error_message) - - return normed - - def get_dev(self): - dev = {'mknap1.txt': [4, 1, 0], - 'mknap2.txt': [6, 44, 18, 22, 35, 45, 26, 28, 12, 0, 46, 1, 17, 31, 9, 21, 20, 23, 2, 13, 27, 33, 29, - 41], - 'mknapcb1.txt': [2, 5, 24, 4, 6, 25, 8, 14, 11, 9, 20, 26, 10, 7, 27], - 'mknapcb2.txt': [18, 10, 4, 27, 16, 17, 25, 29, 13, 21, 20, 7, 14, 9, 28], - 'mknapcb3.txt': [2, 8, 3, 0, 18, 7, 24, 1, 17, 23, 28, 12, 9, 4, 5], - 'mknapcb4.txt': [9, 16, 2, 10, 24, 19, 3, 13, 14, 29, 28, 15, 0, 4, 22], - 'mknapcb5.txt': [16, 15, 11, 5, 7, 8, 20, 2, 3, 27, 12, 22, 29, 23, 21], - 'mknapcb6.txt': [23, 5, 9, 14, 13, 6, 7, 16, 8, 2, 22, 3, 25, 26, 1], - 'mknapcb7.txt': [22, 7, 11, 0, 4, 3, 26, 17, 10, 14, 8, 13, 27, 15, 9], - 'mknapcb8.txt': [19, 12, 18, 6, 0, 16, 2, 25, 15, 28, 14, 1, 26, 9, 4], - 'mknapcb9.txt': [23, 8, 21, 24, 0, 5, 17, 1, 2, 7, 27, 29, 15, 12, 18]} - - return dev - - - - - - -# Task configuration for benchmark task -ENTRY_NAME = 'solve' -FUNCTION_SIGNATURE = 'def solve(...):' -IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' -TASK_DESCRIPTION = '("This problem is a multidimensional knapsack optimization where the objective is to maximize the "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("This problem is a multidimensional knapsack optimization where the objective is to maximize the "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n: int, m: int, p: list, r: list, b: list) -> dict:\n """\n Solves a multidimensional knapsack problem instance.\n Input kwargs (for one test case):\n - n: int, number of decision variables.\n - m: int, number of constraints.\n - p: list of floats, profit coefficients (length n).\n - r: list of m lists, each of length n, representing the resource consumption per constraint.\n - b: list of floats, right-hand side values for each constraint (length m).\n Evaluation metric:\n The score is computed as:\n score = sum(p[j] * x[j] for j in range(n))\n if and only if all constraints are satisfied—that is, for every constraint i, the total resource consumption\n sum(r[i][j] * x[j] for j in range(n))\n does not exceed b[i].\n If any constraint is violated, the solution receives no score. A higher score is better.\n Returns:\n A dict with key \'x\' whose value is a list of n binary decisions (0 or 1).\n """\n # Placeholder implementation: a dummy solution that selects no items.\n x = [0] * kwargs[\'n\']\n return {\'x\': x}' -EVAL_CLASS_NAME = 'MKPEvaluationCB' -EVAL_KWARGS = {'timeout_seconds': 300} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_multidimensional_knapsack_problem/paras.yaml b/examples/benchmark_tasks/optimization_multidimensional_knapsack_problem/paras.yaml deleted file mode 100644 index 5d479383..00000000 --- a/examples/benchmark_tasks/optimization_multidimensional_knapsack_problem/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: MKPEvaluationCB -timeout_seconds: 300 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_online_bin_packing/__init__.py b/examples/benchmark_tasks/optimization_online_bin_packing/__init__.py deleted file mode 100644 index eba0f176..00000000 --- a/examples/benchmark_tasks/optimization_online_bin_packing/__init__.py +++ /dev/null @@ -1,311 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_online_bin_packing -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# Module Name: OBPEvaluation -# Last Revision: 2025/2/16 -# Description: Evaluates the Online Bin Packing Problem (OBP). -# Given a sequence of items arriving one by one, the goal is to pack them into bins -# of fixed capacity in real-time, minimizing the number of bins used. -# This module is part of the LLM4AD project (https://github.com/Optima-CityU/llm4ad). -# -# Parameters: -# - timeout_seconds: Maximum allowed time (in seconds) for the evaluation process: int (default: 30). -# - n_instances: Number of problem instances to generate: int (default: 5). -# - n_items: Number of items to pack: int (default: 5000). -# - capacity: Maximum capacity of each bin: int (default: 100). -# -# References: -# - Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -from typing import Any -import numpy as np -import matplotlib.pyplot as plt - -from llm4ad_loader import Evaluation -# from llm4ad.task.optimization.online_bin_packing.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\n\ndef priority(item: float, bins: np.ndarray) -> np.ndarray:\n """Returns priority with which we want to add item to each bin.\n Args:\n item: Size of item to be added to the bin.\n bins: Array of capacities for each bin.\n Return:\n Array of same size as bins with priority score of each bin.\n """\n return item - bins' -task_description = 'Implement a function that returns the priority with which we want to add an item to each bin.' - -import os, sys -sys.path.insert(0, os.path.dirname(__file__)) -from generate_weibull_instances import generate_weibull_dataset -# from llm4ad.task.optimization.online_bin_packing.generate_weibull_instances import generate_weibull_dataset # Converted from LLM4AD import - -__all__ = ['OBPEvaluation'] - - -class OBPEvaluation(Evaluation): - """Evaluator for online bin packing problem.""" - - def __init__(self, timeout_seconds=30, - n_instances=5, - n_items=5000, - capacity=100, - **kwargs): - """ - Args: - - 'data_file' (str): The data file to load (default is 'weibull_5k_train.pkl'). - - 'data_key' (str): The key of the data to load (default is 'data_key'). - - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - self.n_instances = n_instances - self.n_items = n_items - self.capacity = capacity - - self._datasets = generate_weibull_dataset(self.n_instances, self.n_items, self.capacity) - - def evaluate_program(self, program_str: str, callable_func: callable) -> Any | None: - return self.evaluate(callable_func) - - def plot_solution(self, bins_packed: np.ndarray, items: list, capacity: int, max_unused_bins: int = 5): - """ - Plot the solution of the 1D Online Bin Packing Problem, omitting unused bins. - - Args: - bins_packed: A numpy array of remaining capacities in the bins after packing. - items: A list of item sizes. - capacity: The capacity of each bin. - max_unused_bins: Maximum number of unused bins to include in the plot (for sampling). - """ - # Calculate the number of bins used - num_bins = (bins_packed != capacity).sum() - - # - n_show = 15 - - # Check for empty bins or invalid inputs - if num_bins == 0: - print("No bins used.") - return - if len(items) == 0: - print("No items to pack.") - return - - # Track which items are assigned to which bins - item_assignment = [[] for _ in range(len(bins_packed))] - current_bin = 0 - current_position = 0 - - for item in items: - if current_bin >= len(bins_packed): - break # No more bins available - if current_position + item <= capacity - bins_packed[current_bin]: - item_assignment[current_bin].append((current_position, item)) - current_position += item - else: - current_bin += 1 - current_position = 0 - if current_bin >= len(bins_packed): - break - item_assignment[current_bin].append((current_position, item)) - current_position += item - - # Filter out bins with no items - bins_with_items = [bin_idx for bin_idx, items_in_bin in enumerate(item_assignment) if items_in_bin] - - # Include a sample of unused bins (if any) - unused_bins = [bin_idx for bin_idx, items_in_bin in enumerate(item_assignment) if not items_in_bin] - if unused_bins: - unused_bins_sample = unused_bins[:max_unused_bins] # Sample a subset of unused bins - bins_to_plot = bins_with_items + unused_bins_sample - else: - bins_to_plot = bins_with_items - - bins_to_plot = bins_to_plot[:n_show] - - # Adjust figure size based on the number of bins to plot - bin_height = 0.5 # Height per bin in inches - fig_height = max(3, len(bins_to_plot) * bin_height) # Minimum height of 3 inches - - # Create a figure and axis - fig, ax = plt.subplots(figsize=(10, fig_height)) - - # Plot each bin and its items - for plot_idx, bin_idx in enumerate(bins_to_plot): - # Plot the bin as a horizontal bar - ax.barh(plot_idx, capacity, height=0.6, color='lightgray', edgecolor='black', label='Bin' if plot_idx == 0 else None) - - # Plot the items packed into the bin (if any) - for position, item in item_assignment[bin_idx]: - ax.barh(plot_idx, item, left=position, height=0.6, color='skyblue', edgecolor='black') - - # Set axis labels and title - ax.set_yticks(range(len(bins_to_plot))) - ax.set_yticklabels([f'Bin {bin_idx + 1}' for bin_idx in bins_to_plot]) - ax.set_xlabel('Capacity') - ax.set_title('1D Online Bin Packing Solution') - - # Add a legend - ax.legend(['Bin', 'Item'], loc='upper right') - - # Adjust layout to prevent overlap - plt.tight_layout() - - # Show the plot - plt.show() - - def get_valid_bin_indices(self, item: float, bins: np.ndarray) -> np.ndarray: - """Returns indices of bins in which item can fit.""" - return np.nonzero((bins - item) >= 0)[0] - - def online_binpack(self, - items: tuple[float, ...], bins: np.ndarray, priority: callable - ) -> tuple[list[list[float, ...], ...], np.ndarray]: - """Performs online binpacking of `items` into `bins`.""" - # Track which items are added to each bin. - packing = [[] for _ in bins] - # Add items to bins. - for item in items: - # Extract bins that have sufficient space to fit item. - valid_bin_indices = self.get_valid_bin_indices(item, bins) - # Score each bin based on heuristic. - priorities = priority(item, bins[valid_bin_indices]) - # Add item to bin with highest priority. - best_bin = valid_bin_indices[np.argmax(priorities)] - bins[best_bin] -= item - packing[best_bin].append(item) - # Remove unused bins from packing. - packing = [bin_items for bin_items in packing if bin_items] - return packing, bins - - def evaluate(self, priority: callable) -> float: - """Evaluate heuristic function on a set of online binpacking instances.""" - # List storing number of bins used for each instance. - num_bins = [] - # Perform online binpacking for each instance. - for name in self._datasets: - instance = self._datasets[name] - capacity = instance['capacity'] - items = instance['items'] - # Create num_items bins so there will always be space for all items, - # regardless of packing order. Array has shape (num_items,). - bins = np.array([capacity for _ in range(instance['num_items'])]) - # Pack items into bins and return remaining capacity in bins_packed, which - # has shape (num_items,). - _, bins_packed = self.online_binpack(items, bins, priority) - - # If remaining capacity in a bin is equal to initial capacity, then it is - # unused. Count number of used bins. - num_bins.append((bins_packed != capacity).sum()) - # Score of heuristic function is negative of average number of bins used - # across instances (as we want to minimize number of bins). - return -np.mean(num_bins) - - -if __name__ == '__main__': - def priority(item: float, valid_bins: np.ndarray) -> np.ndarray: - """ - Priority function for the First-Fit Decreasing (FFD) heuristic. - - Args: - item: The size of the item to be packed. - valid_bins: A numpy array of remaining capacities in valid bins. - - Returns: - A numpy array of priorities for the valid bins. - """ - # Prioritize bins with the least remaining capacity (but still able to fit the item) - priorities = -valid_bins # Negative because we want to maximize the priority for the smallest remaining capacity - return priorities - - - obp = OBPEvaluation() - ave_bins = obp.evaluate_program('_', priority) - print(ave_bins) - -# Task configuration for benchmark task -ENTRY_NAME = 'priority' -FUNCTION_SIGNATURE = 'def priority(...):' -IMPORT_HEADER = 'import numpy as np\nimport math' -TASK_DESCRIPTION = 'Implement a function that returns the priority with which we want to add an item to each bin.' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `priority` for the LLM4AD task.\\n\\nTask description:\\nImplement a function that returns the priority with which we want to add an item to each bin.\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\n\ndef priority(item: float, bins: np.ndarray) -> np.ndarray:\n """Returns priority with which we want to add item to each bin.\n Args:\n item: Size of item to be added to the bin.\n bins: Array of capacities for each bin.\n Return:\n Array of same size as bins with priority score of each bin.\n """\n return item - bins' -EVAL_CLASS_NAME = 'OBPEvaluation' -EVAL_KWARGS = {'timeout_seconds': 20} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_online_bin_packing/generate_weibull_instances.py b/examples/benchmark_tasks/optimization_online_bin_packing/generate_weibull_instances.py deleted file mode 100644 index 48ca0f35..00000000 --- a/examples/benchmark_tasks/optimization_online_bin_packing/generate_weibull_instances.py +++ /dev/null @@ -1,36 +0,0 @@ -import numpy as np - - -def generate_weibull_dataset(num_instances, num_items, capacity_limit): - np.random.seed(2024) - - dataset = {} - - for i in range(num_instances): - instance = { - 'capacity': capacity_limit, - 'num_items': num_items, - 'items': [] - } - - items = [] - - # Generate random samples from Weibull(45, 3) distribution - samples = np.random.weibull(3, num_items) * 45 - - # Clip the samples at the specified limit - samples = np.clip(samples, 1, capacity_limit) - - # Round the item sizes to the nearest integer - sizes = np.round(samples).astype(int) - - # Add the items to the instance - for size in sizes: - items.append(size) - - instance['items'] = np.array(items) - - if num_items not in dataset: - dataset[f'instance_{i}'] = instance - - return dataset diff --git a/examples/benchmark_tasks/optimization_online_bin_packing/paras.yaml b/examples/benchmark_tasks/optimization_online_bin_packing/paras.yaml deleted file mode 100644 index 26997979..00000000 --- a/examples/benchmark_tasks/optimization_online_bin_packing/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: OBPEvaluation -timeout_seconds: 20 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_online_bin_packing_2O/__init__.py b/examples/benchmark_tasks/optimization_online_bin_packing_2O/__init__.py deleted file mode 100644 index e9755855..00000000 --- a/examples/benchmark_tasks/optimization_online_bin_packing_2O/__init__.py +++ /dev/null @@ -1,194 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_online_bin_packing_2O -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# name: str: OBP_2O_Evaluation -# Parameters: -# timeout_seconds: int: 20 -# end -from __future__ import annotations - -import os -import pickle -from typing import Any - -import numpy as np - -from llm4ad_loader import Evaluation -# from llm4ad.task.optimization.online_bin_packing.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\n\ndef priority(item: float, bins: np.ndarray) -> np.ndarray:\n """Returns priority with which we want to add item to each bin.\n Args:\n item: Size of item to be added to the bin.\n bins: Array of capacities for each bin.\n Return:\n Array of same size as bins with priority score of each bin.\n """\n return item - bins' -task_description = 'Implement a function that returns the priority with which we want to add an item to each bin.' - -import os, sys -sys.path.insert(0, os.path.dirname(__file__)) -from generate_weibull_instances import generate_weibull_dataset -# from llm4ad.task.optimization.online_bin_packing.generate_weibull_instances import generate_weibull_dataset # Converted from LLM4AD import - -import time -from typing import Tuple - -__all__ = ['OBP_2O_Evaluation'] - - -def get_valid_bin_indices(item: float, bins: np.ndarray) -> np.ndarray: - """Returns indices of bins in which item can fit.""" - return np.nonzero((bins - item) >= 0)[0] - - -def online_binpack( - items: tuple[float, ...], bins: np.ndarray, priority: callable -) -> tuple[list[list[float, ...], ...], np.ndarray]: - """Performs online binpacking of `items` into `bins`.""" - # Track which items are added to each bin. - packing = [[] for _ in bins] - # Add items to bins. - for item in items: - # Extract bins that have sufficient space to fit item. - valid_bin_indices = get_valid_bin_indices(item, bins) - # Score each bin based on heuristic. - priorities = priority(item, bins[valid_bin_indices]) - # Add item to bin with highest priority. - best_bin = valid_bin_indices[np.argmax(priorities)] - bins[best_bin] -= item - packing[best_bin].append(item) - # Remove unused bins from packing. - packing = [bin_items for bin_items in packing if bin_items] - return packing, bins - - -def evaluate(instances: dict, priority: callable) -> np.ndarray: - """Evaluate heuristic function on a set of online binpacking instances.""" - # List storing number of bins used for each instance. - num_bins = [] - - start_time = time.time() - - # Perform online binpacking for each instance. - for name in instances: - instance = instances[name] - capacity = instance['capacity'] - items = instance['items'] - # Create num_items bins so there will always be space for all items, - # regardless of packing order. Array has shape (num_items,). - bins = np.array([capacity for _ in range(instance['num_items'])]) - # Pack items into bins and return remaining capacity in bins_packed, which - # has shape (num_items,). - _, bins_packed = online_binpack(items, bins, priority) - # If remaining capacity in a bin is equal to initial capacity, then it is - # unused. Count number of used bins. - num_bins.append((bins_packed != capacity).sum()) - # Score of heuristic function is negative of average number of bins used - # across instances (as we want to minimize number of bins). - running_time = time.time() - start_time - return np.array([-np.mean(num_bins), -running_time/len(instances)]) - - -class OBP_2O_Evaluation(Evaluation): - """Evaluator for online bin packing problem.""" - - def __init__(self, timeout_seconds=60, data_file='weibull_train.pkl', data_key='weibull_5k_train', **kwargs): - """ - Args: - - 'data_file' (str): The data file to load (default is 'weibull_5k_train.pkl'). - - 'data_key' (str): The key of the data to load (default is 'data_key'). - - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - self._datasets = generate_weibull_dataset(5, 5000, 100) - - def evaluate_program(self, program_str: str, callable_func: callable) -> Any | None: - return evaluate(self._datasets, callable_func) - - -if __name__ == '__main__': - import numpy as np - - - def priority(item: float, bins: np.ndarray) -> np.ndarray: - """Returns priority with which we want to add item to each bin. - Args: - item: Size of item to be added to the bin. - bins: Array of capacities for each bin. - Return: - Array of same size as bins with priority score of each bin. - """ - return -bins - - - bpp = OBP_2O_Evaluation() - bpp.evaluate_program('_', priority) - -# Task configuration for benchmark task -ENTRY_NAME = 'priority' -FUNCTION_SIGNATURE = 'def priority(...):' -IMPORT_HEADER = 'import numpy as np\nimport math' -TASK_DESCRIPTION = 'Implement a function that returns the priority with which we want to add an item to each bin.' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `priority` for the LLM4AD task.\\n\\nTask description:\\nImplement a function that returns the priority with which we want to add an item to each bin.\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\n\ndef priority(item: float, bins: np.ndarray) -> np.ndarray:\n """Returns priority with which we want to add item to each bin.\n Args:\n item: Size of item to be added to the bin.\n bins: Array of capacities for each bin.\n Return:\n Array of same size as bins with priority score of each bin.\n """\n return item - bins' -EVAL_CLASS_NAME = 'OBP_2O_Evaluation' -EVAL_KWARGS = {'timeout_seconds': 60} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_online_bin_packing_2O/generate_weibull_instances.py b/examples/benchmark_tasks/optimization_online_bin_packing_2O/generate_weibull_instances.py deleted file mode 100644 index 3bc3dec8..00000000 --- a/examples/benchmark_tasks/optimization_online_bin_packing_2O/generate_weibull_instances.py +++ /dev/null @@ -1,36 +0,0 @@ -import numpy as np - -def generate_weibull_dataset(num_instances, num_items, capacity_limit): - - np.random.seed(2024) - - dataset = {} - - for i in range(num_instances): - instance = { - 'capacity': capacity_limit, - 'num_items': num_items, - 'items': [] - } - - items = [] - - # Generate random samples from Weibull(45, 3) distribution - samples = np.random.weibull(3, num_items) * 45 - - # Clip the samples at the specified limit - samples = np.clip(samples, 1, capacity_limit) - - # Round the item sizes to the nearest integer - sizes = np.round(samples).astype(int) - - # Add the items to the instance - for size in sizes: - items.append(size) - - instance['items'] = np.array(items) - - if num_items not in dataset: - dataset[f'instance_{i}'] = instance - - return dataset \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_online_bin_packing_2O/paras.yaml b/examples/benchmark_tasks/optimization_online_bin_packing_2O/paras.yaml deleted file mode 100644 index 44b540cb..00000000 --- a/examples/benchmark_tasks/optimization_online_bin_packing_2O/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: OBP_2O_Evaluation -timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_open_shop_scheduling/__init__.py b/examples/benchmark_tasks/optimization_open_shop_scheduling/__init__.py deleted file mode 100644 index e5444343..00000000 --- a/examples/benchmark_tasks/optimization_open_shop_scheduling/__init__.py +++ /dev/null @@ -1,307 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_open_shop_scheduling -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# References: -# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language -# model agents in algorithm search for combinatorial optimization. -# arXiv preprint arXiv:2504.04310 (2025). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -from typing import Any -import numpy as np -from llm4ad_loader import Evaluation -from llm4ad_loader import load_subdir_as_text -# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader -# from llm4ad.task.optimization.co_bench.open_shop_scheduling_co_bench.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n_jobs: int, n_machines: int, times: list, machines: list) -> dict:\n """\n Solves a single open shop scheduling test case.\n Input kwargs:\n - n_jobs (int): Number of jobs.\n - n_machines (int): Number of machines (and operations per job).\n - times (list of list of int): A 2D list of processing times for each operation.\n Dimensions: n_jobs x n_machines.\n - machines (list of list of int): A 2D list specifying the machine assignment for each operation.\n Dimensions: n_jobs x n_machines. Note machine is 1-indexed.\n Output:\n solution (dict): A dictionary containing:\n - start_times (list of list of int): A 2D list of start times for each operation.\n Dimensions: n_jobs x n_machines.\n Each start time must be a non-negative integer, and the schedule must respect the following constraint:\n (i) Non-parallel operation: Each job must be processed on only one machine at a time\n (ii) Machine exclusivity: For operations assigned to the same machine, their processing intervals must not overlap.\n The evaluation function will use the start_times to compute the makespan and verify the constraints.\n """\n\n # Extract the case parameters\n n_jobs = kwargs["n_jobs"]\n n_machines = kwargs["n_machines"]\n times = kwargs["times"]\n machines = kwargs["machines"]\n\n # TODO: Implement the scheduling algorithm here.\n # For now, we provide a dummy solution where all operations start at time 0.\n\n # Create a start_times list with dimensions n_jobs x n_machines, initializing all start times to 0.\n start_times = [[0 for _ in range(n_machines)] for _ in range(n_jobs)]\n\n # Build the solution dictionary.\n solution = {"start_times": start_times}\n\n return solution' -task_description = '("The Open Shop Scheduling Problem involves scheduling a set of jobs across a set of machines with "' - - -__all__ = ['OSSEvaluationCB'] - - -class OSSEvaluationCB(Evaluation): - - def __init__(self, - timeout_seconds=50, - **kwargs): - - """ - Args: - None - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - # Load datasets from Hugging Face - dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Open shop scheduling") - self._datasets = {} - for filename in dataset: - # Join all text rows into a single string - text_content = '\n'.join([row['text'] for row in dataset[filename]]) - self._datasets[filename] = text_content - - def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: - return self.evaluate(callable_func) - - def evaluate(self, eva: callable) -> float | None: - ins_cases = [] - for case_id, ins in enumerate(self._datasets.values()): - ins_cases.append(self.load_data(ins)) - - fitness_list = [] - try: - for i in ins_cases: - for j in i: - result = eva(j['n_jobs'], j['n_machines'], j['times'], j['machines']) - fitness = self.eval_func(j['n_jobs'], j['n_machines'], j['times'], j['machines'], result['start_times'], lower_bound=j['lower_bound'], upper_bound=j['upper_bound']) - fitness_list.append(fitness) - - return -np.mean(fitness_list) - - except ValueError as e: - print(e) - return None - - def load_data(self, input_string): - cases = [] - lines = [line.strip() for line in input_string.split('\n') if line.strip()] # remove blank lines - - i = 0 - while i < len(lines): - # Look for a header line starting with "Nb of jobs" - if lines[i].startswith("number of jobs"): - # Next line contains six numbers: n_jobs, n_machines, time_seed, machine_seed, upper_bound, lower_bound - i += 1 - header_tokens = lines[i].split() - if len(header_tokens) < 6: - raise ValueError("Header line does not contain 6 values.") - n_jobs = int(header_tokens[0]) - n_machines = int(header_tokens[1]) - time_seed = int(header_tokens[2]) - machine_seed = int(header_tokens[3]) - upper_bound = int(header_tokens[4]) - lower_bound = int(header_tokens[5]) - - # Find the "Times" section - i += 1 - if not lines[i].lower().startswith("processing"): - raise ValueError("Expected 'Times' section, got: " + lines[i]) - i += 1 # move to first line of times - times = [] - for _ in range(n_jobs): - # Each line should contain n_machines numbers - time_line = list(map(int, lines[i].split())) - if len(time_line) != n_machines: - raise ValueError(f"Expected {n_machines} numbers in times row, got {len(time_line)}") - times.append(time_line) - i += 1 - - # Find the "Machines" section - if i >= len(lines) or not lines[i].lower().startswith("machines"): - raise ValueError("Expected 'Machines' section, got: " + (lines[i] if i < len(lines) else "EOF")) - i += 1 # move to first line of machines - machines = [] - for _ in range(n_jobs): - machine_line = list(map(int, lines[i].split())) - if len(machine_line) != n_machines: - raise ValueError(f"Expected {n_machines} numbers in machines row, got {len(machine_line)}") - machines.append(machine_line) - i += 1 - - # Build the test case dictionary and add to the list of cases. - case = { - "n_jobs": n_jobs, - "n_machines": n_machines, - "time_seed": time_seed, - "machine_seed": machine_seed, - "upper_bound": upper_bound, - "lower_bound": lower_bound, - "times": times, - "machines": machines - } - cases.append(case) - else: - # If the current line is not a header, skip it. - i += 1 - - return cases - - def eval_func(self, n_jobs, n_machines, times, machines, start_times, **kwargs): - """ - Evaluates the solution for a open shop scheduling problem. - Input: - n_jobs (int): Number of jobs. - n_machines (int): Number of machines. - times (list of list of int): Processing times for each operation. - Dimensions: n_jobs x n_machines. - machines (list of list of int): Machine assignments for each operation. - Dimensions: n_jobs x n_machines. - start_times (list of list of int): Proposed start times for each operation. - Dimensions: n_jobs x n_machines. - kwargs: Other parameters that may be provided, which are ignored here. - Output: - score (int): The makespan, defined as the maximum completion time across all jobs. - Raises: - ValueError: If any scheduling constraints are violated. - """ - - # Check that start_times dimensions match the problem dimensions. - if len(start_times) != n_jobs: - raise ValueError(f"Expected start_times to have {n_jobs} rows, got {len(start_times)}") - for i, row in enumerate(start_times): - if len(row) != n_machines: - raise ValueError(f"Expected start_times row {i} to have {n_machines} entries, got {len(row)}") - for t in row: - if t < 0: - raise ValueError("Start times must be non-negative.") - - job_operations = [] - job_completion_times = [] - for i in range(n_jobs): - job_operations.append([]) - finish_time = 0 - for j in range(n_machines): - st = start_times[i][j] - pt = times[i][j] - finish_time = max(finish_time, st + pt) - job_operations[i].append((st, st + pt)) - job_completion_times.append(finish_time) - - for job_id in range(n_jobs): - ops = sorted(job_operations[job_id], key=lambda x: x[0]) # Sort by start time - for i in range(len(ops) - 1): - if ops[i][1] > ops[i + 1][0]: # End time of current > start time of next - raise ValueError(f"Overlapping operations for job {job_id}: {ops[i]} and {ops[i + 1]}") - - # Constraint: Machine non-overlap. - # Build a dictionary mapping machine id to a list of (start_time, finish_time, job, op_index) - machine_schedules = {} - for i in range(n_jobs): - for j in range(n_machines): - machine_id = machines[i][j] - st = start_times[i][j] - pt = times[i][j] - finish_time = st + pt - if machine_id not in machine_schedules: - machine_schedules[machine_id] = [] - machine_schedules[machine_id].append((st, finish_time, i, j)) - - # For each machine, sort operations by start time and check for overlaps. - for machine_id, ops in machine_schedules.items(): - ops_sorted = sorted(ops, key=lambda x: x[0]) - for k in range(1, len(ops_sorted)): - prev_st, prev_finish, prev_job, prev_op = ops_sorted[k - 1] - curr_st, curr_finish, curr_job, curr_op = ops_sorted[k] - if prev_finish > curr_st: - raise ValueError( - f"Machine {machine_id}: Operation from job {prev_job}, op {prev_op} (finishing at {prev_finish}) overlaps with job {curr_job}, op {curr_op} (starting at {curr_st}).") - - # Compute the makespan as the maximum completion time among all jobs. - makespan = max(job_completion_times) - - score = kwargs['lower_bound'] / makespan - - return score - - def get_dev(self): - dev = {'tai10_10.txt': [7, 8, 3, 9, 2], 'tai15_15.txt': [7, 0, 8, 4, 5], 'tai20_20.txt': [6, 0, 3, 8, 2], - 'tai4_4.txt': [0, 7, 5, 8, 6], 'tai5_5.txt': [3, 0, 9, 8, 1], 'tai7_7.txt': [3, 0, 8, 2, 1]} - - return dev - - - - - - -# Task configuration for benchmark task -ENTRY_NAME = 'solve' -FUNCTION_SIGNATURE = 'def solve(...):' -IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' -TASK_DESCRIPTION = '("The Open Shop Scheduling Problem involves scheduling a set of jobs across a set of machines with "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The Open Shop Scheduling Problem involves scheduling a set of jobs across a set of machines with "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n_jobs: int, n_machines: int, times: list, machines: list) -> dict:\n """\n Solves a single open shop scheduling test case.\n Input kwargs:\n - n_jobs (int): Number of jobs.\n - n_machines (int): Number of machines (and operations per job).\n - times (list of list of int): A 2D list of processing times for each operation.\n Dimensions: n_jobs x n_machines.\n - machines (list of list of int): A 2D list specifying the machine assignment for each operation.\n Dimensions: n_jobs x n_machines. Note machine is 1-indexed.\n Output:\n solution (dict): A dictionary containing:\n - start_times (list of list of int): A 2D list of start times for each operation.\n Dimensions: n_jobs x n_machines.\n Each start time must be a non-negative integer, and the schedule must respect the following constraint:\n (i) Non-parallel operation: Each job must be processed on only one machine at a time\n (ii) Machine exclusivity: For operations assigned to the same machine, their processing intervals must not overlap.\n The evaluation function will use the start_times to compute the makespan and verify the constraints.\n """\n\n # Extract the case parameters\n n_jobs = kwargs["n_jobs"]\n n_machines = kwargs["n_machines"]\n times = kwargs["times"]\n machines = kwargs["machines"]\n\n # TODO: Implement the scheduling algorithm here.\n # For now, we provide a dummy solution where all operations start at time 0.\n\n # Create a start_times list with dimensions n_jobs x n_machines, initializing all start times to 0.\n start_times = [[0 for _ in range(n_machines)] for _ in range(n_jobs)]\n\n # Build the solution dictionary.\n solution = {"start_times": start_times}\n\n return solution' -EVAL_CLASS_NAME = 'OSSEvaluationCB' -EVAL_KWARGS = {'timeout_seconds': 60} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_open_shop_scheduling/paras.yaml b/examples/benchmark_tasks/optimization_open_shop_scheduling/paras.yaml deleted file mode 100644 index 5ba59822..00000000 --- a/examples/benchmark_tasks/optimization_open_shop_scheduling/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: OSSEvaluationCB -timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_ovrp_construct/__init__.py b/examples/benchmark_tasks/optimization_ovrp_construct/__init__.py deleted file mode 100644 index 963b3fe2..00000000 --- a/examples/benchmark_tasks/optimization_ovrp_construct/__init__.py +++ /dev/null @@ -1,299 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_ovrp_construct -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# Module Name: OVRPEvaluation -# Last Revision: 2025/2/16 -# Description: Evaluates the Open Vehicle Routing Problem (OVRP). -# Given a set of customers and a fleet of vehicles with limited capacity, -# the goal is to find optimal routes for the vehicles to serve all customers -# while minimizing the total travel distance. -# This module is part of the LLM4AD project (https://github.com/Optima-CityU/llm4ad). -# -# Parameters: -# - timeout_seconds: Maximum allowed time (in seconds) for the evaluation process: int (default: 20). -# - n_instance: Number of problem instances to generate: int (default: 16). -# - problem_size: Number of customers to serve: int (default: 50). -# - capacity: Maximum capacity of each vehicle: int (default: 40). -# -# References: -# - Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -import os, sys -sys.path.insert(0, os.path.dirname(__file__)) -import copy -from typing import Any -import numpy as np -import matplotlib.pyplot as plt - -from llm4ad_loader import Evaluation -from get_instance import GetData -# from llm4ad.task.optimization.ovrp_construct.get_instance import GetData # Converted from LLM4AD import -# from llm4ad.task.optimization.ovrp_construct.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\n\ndef select_next_node(current_node: int, depot: int, unvisited_nodes: np.ndarray, rest_capacity: np.ndarray, demands: np.ndarray, distance_matrix: np.ndarray) -> int:\n """Design a novel algorithm to select the next node in each step.\n Args:\n current_node: ID of the current node.\n depot: ID of the depot.\n unvisited_nodes: Array of IDs of unvisited nodes.\n rest_capacity: rest capacity of vehicle\n demands: demands of nodes\n distance_matrix: Distance matrix of nodes.\n Return:\n ID of the next node to visit.\n """\n next_node = unvisited_nodes[0]\n return next_node' -task_description = '"' - - - -class OVRPEvaluation(Evaluation): - def __init__(self, - timeout_seconds=20, - problem_size=50, - n_instance=16, - **kwargs): - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - self.problem_size = problem_size + 1 - self.n_instance = n_instance - - getData = GetData(self.n_instance, self.problem_size) - self._datasets = getData.generate_instances() - - def plot_solution(self, instance: np.ndarray, route: list, demands: list, vehicle_capacity: int): - """ - Plot the solution of the Open Vehicle Routing Problem (Open VRP). - - Args: - instance: A 2D array of node coordinates (including the depot). - route: A list representing the sequence of nodes visited in the route. - demands: A list of demands for each node. - vehicle_capacity: The capacity of the vehicle. - """ - # Extract coordinates - x = instance[:, 0] - y = instance[:, 1] - - # Create a figure and axis - fig, ax = plt.subplots(figsize=(10, 8)) - - # Plot depot (node 0) - ax.plot(x[0], y[0], 'ro', markersize=10, label='Depot') - ax.text(x[0], y[0], 'Depot', ha='center', va='bottom', fontsize=12) - - # Plot customer nodes - for i in range(1, len(x)): - ax.plot(x[i], y[i], 'bo', markersize=8) - ax.text(x[i], y[i], f'C{i}\nDem: {demands[i]}', ha='center', va='bottom', fontsize=8) - - # Split the route into individual vehicle routes - routes = [] - current_route = [0] # Start each route from the depot - for node in route: - if node == 0 and len(current_route) > 1: # Start a new route from the depot - routes.append(current_route) - current_route = [0] - else: - current_route.append(node) - if current_route: # Add the last route if it exists - routes.append(current_route) - - # Plot each route in a different color - colors = plt.cm.tab10.colors # Use a colormap for distinct colors - for i, r in enumerate(routes): - color = colors[i % len(colors)] # Cycle through colors - for j in range(len(r) - 1): - start_node = r[j] - end_node = r[j + 1] - ax.plot([x[start_node], x[end_node]], [y[start_node], y[end_node]], color=color, linestyle='--', linewidth=1, label=f'Route {i + 1}' if j == 0 else None) - - # Add load information - if end_node != 0: # If not returning to the depot - ax.text((x[start_node] + x[end_node]) / 2, (y[start_node] + y[end_node]) / 2, - f'Load: {sum(demands[r[:j + 1]])}', ha='center', va='center', fontsize=8, rotation=45) - - # Mark start and end nodes of the route with triangles (excluding depot) - if len(r) > 1: - ax.plot(x[r[1]], y[r[1]], '^', color=color, markersize=10, label='Start' if i == 0 else None) # Start node - ax.plot(x[r[-1]], y[r[-1]], 'v', color=color, markersize=10, label='End' if i == 0 else None) # End node - - # Set axis labels and title - ax.set_xlabel('X Coordinate') - ax.set_ylabel('Y Coordinate') - ax.set_title('Open Vehicle Routing Problem (Open VRP) Solution') - ax.legend(loc='upper right') - - # Show the plot - plt.tight_layout() - plt.show() - - def tour_cost(self, instance, solution): - cost = 0 - for j in range(len(solution) - 1): - cost += np.linalg.norm(instance[int(solution[j])] - instance[int(solution[j + 1])]) - return cost - - def route_construct(self, distance_matrix, demands, vehicle_capacity, heuristic): - route = [] - current_load = 0 - current_node = 0 - route.append(current_node) - - unvisited_nodes = set(range(1, self.problem_size)) # Assuming node 0 is the depot - all_nodes = np.array(list(unvisited_nodes)) - feasible_unvisited_nodes = all_nodes - - while unvisited_nodes: - next_node = heuristic(current_node, - 0, - feasible_unvisited_nodes, # copy - vehicle_capacity - current_load, - copy.deepcopy(demands), # copy - copy.deepcopy(distance_matrix)) # copy - if next_node == 0: - # Update route and load - route.append(next_node) - current_load = 0 - current_node = 0 - else: - # Update route and load - route.append(next_node) - current_load += demands[next_node] - unvisited_nodes.remove(next_node) - current_node = next_node - - feasible_nodes_capacity = np.array([node for node in all_nodes if current_load + demands[node] <= vehicle_capacity]) - # Determine feasible and unvisited nodes - feasible_unvisited_nodes = np.intersect1d(feasible_nodes_capacity, list(unvisited_nodes)) - - if len(unvisited_nodes) > 0 and len(feasible_unvisited_nodes) < 1: - route.append(0) - current_load = 0 - current_node = 0 - feasible_unvisited_nodes = np.array(list(unvisited_nodes)) - - # check if not all nodes have been visited - independent_values = set(route) - if len(independent_values) != self.problem_size: - return None - - return route - - def evaluate(self, heuristic): - dis = np.ones(self.n_instance) - n_ins = 0 - - for instance, distance_matrix, demands, vehicle_capacity in self._datasets: - route = self.route_construct(distance_matrix, demands, vehicle_capacity, heuristic) - LLM_dis = self.tour_cost(instance, route) - dis[n_ins] = LLM_dis - n_ins += 1 - if n_ins == self.n_instance: - break - - ave_dis = np.average(dis) - return -ave_dis - - def evaluate_program(self, program_str: str, callable_func: callable) -> Any | None: - return self.evaluate(callable_func) - - -if __name__ == '__main__': - def select_next_node(current_node: int, depot: int, unvisited_nodes: np.ndarray, rest_capacity: np.ndarray, demands: np.ndarray, distance_matrix: np.ndarray) -> int: - """Design a novel algorithm to select the next node in each step. - Args: - current_node: ID of the current node. - depot: ID of the depot. - unvisited_nodes: Array of IDs of unvisited nodes. - rest_capacity: rest capacity of vehicle - demands: demands of nodes - distance_matrix: Distance matrix of nodes. - Return: - ID of the next node to visit. - """ - next_node = unvisited_nodes[0] - return next_node - - - eval = OVRPEvaluation() - res = eval.evaluate_program('', select_next_node) - print(res) - -# Task configuration for benchmark task -ENTRY_NAME = 'select_next_node' -FUNCTION_SIGNATURE = 'def select_next_node(...):' -IMPORT_HEADER = 'import numpy as np\nimport math' -TASK_DESCRIPTION = '"' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `select_next_node` for the LLM4AD task.\\n\\nTask description:\\n"\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\n\ndef select_next_node(current_node: int, depot: int, unvisited_nodes: np.ndarray, rest_capacity: np.ndarray, demands: np.ndarray, distance_matrix: np.ndarray) -> int:\n """Design a novel algorithm to select the next node in each step.\n Args:\n current_node: ID of the current node.\n depot: ID of the depot.\n unvisited_nodes: Array of IDs of unvisited nodes.\n rest_capacity: rest capacity of vehicle\n demands: demands of nodes\n distance_matrix: Distance matrix of nodes.\n Return:\n ID of the next node to visit.\n """\n next_node = unvisited_nodes[0]\n return next_node' -EVAL_CLASS_NAME = 'OVRPEvaluation' -EVAL_KWARGS = {'timeout_seconds': 20} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_ovrp_construct/get_instance.py b/examples/benchmark_tasks/optimization_ovrp_construct/get_instance.py deleted file mode 100644 index dddc3422..00000000 --- a/examples/benchmark_tasks/optimization_ovrp_construct/get_instance.py +++ /dev/null @@ -1,50 +0,0 @@ -import pickle - -import numpy as np - - -class GetData: - def __init__(self, n_instance, n_cities): - self.n_instance = n_instance - self.n_cities = n_cities - - def generate_instances(self): - """each instance -> (coordinates, distances, demands, capacity)""" - np.random.seed(2024) - instance_data = [] - for _ in range(self.n_instance): - coordinates = np.random.rand(self.n_cities, 2) - demands = np.random.randint(1, 10, size=self.n_cities) - capacity = 40 - distances = np.linalg.norm(coordinates[:, np.newaxis] - coordinates, axis=2) - instance_data.append((coordinates, distances, demands, capacity)) - return instance_data - - -if __name__ == '__main__': - gd = GetData(10, 51) - data = gd.generate_instances() - with open('data.pkl', 'wb') as f: - pickle.dump(data, f) - - prompt_code_temp = "import numpy as np\n\ - def select_next_node(current_node: int, depot: int, unvisited_nodes: np.ndarray, rest_capacity: np.ndarray, demands: np.ndarray, distance_matrix: np.ndarray) -> int: \n\ - \n\ - '''Design a novel algorithm to select the next node in each step.\n\ - \n\ - Args:\n\ - current_node: ID of the current node.\n\ - depot: ID of the depot.\n\ - unvisited_nodes: Array of IDs of unvisited nodes.\n\ - rest_capacity: rest capacity of vehicle \n\ - demands: demands of nodes \n\ - distance_matrix: Distance matrix of nodes.\n\ - \n\ - Return:\n\ - ID of the next node to visit.\n\ - '''\n\ - next_node = unvisited_nodes[0]\n\ - \n\ - return next_node\n" - - print(prompt_code_temp) diff --git a/examples/benchmark_tasks/optimization_ovrp_construct/paras.yaml b/examples/benchmark_tasks/optimization_ovrp_construct/paras.yaml deleted file mode 100644 index a95d8853..00000000 --- a/examples/benchmark_tasks/optimization_ovrp_construct/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: OVRPEvaluation -timeout_seconds: 20 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_p_median_capacitated/__init__.py b/examples/benchmark_tasks/optimization_p_median_capacitated/__init__.py deleted file mode 100644 index 6734192d..00000000 --- a/examples/benchmark_tasks/optimization_p_median_capacitated/__init__.py +++ /dev/null @@ -1,357 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_p_median_capacitated -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# References: -# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language -# model agents in algorithm search for combinatorial optimization. -# arXiv preprint arXiv:2504.04310 (2025). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -from typing import Any -import numpy as np -from llm4ad_loader import Evaluation -from llm4ad_loader import load_subdir_as_text -# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader -# from llm4ad.task.optimization.co_bench.p_median_capacitated_co_bench.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(best_known: float, n: int, p: int, Q: float, customers: list) -> dict:\n """\n Solve the Capacitated P-Median Problem.\n This function receives the data for one problem instance via keyword arguments:\n - best_known (float): Best known solution value for reference.\n - n (int): Number of customers.\n - p (int): Number of medians to choose.\n - Q (float): Capacity limit for each median.\n - customers (list of tuples): Each tuple is (customer_id, x, y, demand).\n The goal is to select p medians (from the customers) and assign every customer to one\n of these medians so that the total cost is minimized. The cost for a customer is the\n Euclidean distance (rounded down to the nearest integer) to its assigned median, and the\n total demand assigned to each median must not exceed Q.\n Evaluation Metric:\n The solution is evaluated by computing the ratio:\n score = best_known / computed_total_cost,\n where computed_total_cost is the sum over all customers of the (floored) Euclidean distance\n to its assigned median.\n Note: This is a placeholder function. Replace the placeholder with an actual algorithm.\n Returns:\n A dictionary with the following keys:\n - \'objective\': (numeric) the total cost (objective value) computed by the algorithm.\n - \'medians\': (list of int) exactly p customer IDs chosen as medians.\n - \'assignments\': (list of int) a list of n integers, where the i-th integer is the customer\n ID (from the chosen medians) assigned to customer i.\n """\n # Placeholder: Replace this with your actual implementation.\n # For now, we return an empty solution structure.\n return {\n "objective": 0, # total cost (to be computed)\n "medians": [], # list of p medians (customer IDs)\n "assignments": [] # list of n assignments (each is one of the medians)\n }' -task_description = '("The Capacitated P-Median Problem is a facility location optimization problem where the objective "' - - -__all__ = ['PMCEvaluationCB'] - - -class PMCEvaluationCB(Evaluation): - - def __init__(self, - timeout_seconds=50, - **kwargs): - - """ - Args: - None - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - # Load datasets from Hugging Face - dataset = load_subdir_as_text("CO-Bench/CO-Bench", "p-median - capacitated") - self._datasets = {} - for filename in dataset: - # Join all text rows into a single string - text_content = '\n'.join([row['text'] for row in dataset[filename]]) - self._datasets[filename] = text_content - - def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: - return self.evaluate(callable_func) - - def evaluate(self, eva: callable) -> float | None: - ins_cases = [] - for case_id, ins in enumerate(self._datasets.values()): - ins_cases.append(self.load_data(ins)) - - fitness_list = [] - try: - for i in ins_cases: - for j in i: - result = eva(j['best_known'], j['n'], j['p'], j['Q'], j['customers']) - fitness = self.eval_func(best_known=j['best_known'], n=j['n'], p=j['p'], Q=j['Q'], customers=j['customers'], objective=result['objective'], medians=result['medians'], assignments=result['assignments']) - fitness_list.append(fitness) - - return -np.mean(fitness_list) - - except ValueError as e: - print(e) - return None - - def load_data(self, input_string): - """ - Load one or more instances of the Capacitated P-Median Problem from a text file. - The input file structure is: - Line 1: An integer M, the number of problem instances in the file. - Then, for each instance: - - A header line with two values: - - A line with three values:

- - n subsequent lines each with: - Returns: - A list of dictionaries. Each dictionary contains the keys: - - 'best_known': float - - 'n': int - - 'p': int - - 'Q': float - - 'customers': list of tuples (customer_id, x, y, demand) - """ - cases = [] - try: - lines = [line.strip() for line in input_string.split('\n') if line.strip() != ''] - except Exception as e: - raise ValueError("Error reading input file: " + str(e)) - - if not lines: - raise ValueError("Input file is empty.") - - try: - M = int(lines[0]) - except Exception as e: - raise ValueError("The first line must be an integer representing the number of cases.") - - index = 1 - for case_idx in range(M): - if index >= len(lines): - raise ValueError("Unexpected end of file when reading case {}.".format(case_idx + 1)) - - # Read problem header: - tokens = lines[index].split() - if len(tokens) < 2: - raise ValueError("Invalid problem header at case {}.".format(case_idx + 1)) - try: - # We don't need the problem number, so we can ignore it. - _ = int(tokens[0]) - best_known = float(tokens[1]) - except Exception as e: - raise ValueError("Error parsing problem header at case {}: {}".format(case_idx + 1, e)) - index += 1 - - if index >= len(lines): - raise ValueError("Missing instance parameters for case {}.".format(case_idx + 1)) - - # Read instance parameters:

- tokens = lines[index].split() - if len(tokens) < 3: - raise ValueError("Invalid instance parameters at case {}.".format(case_idx + 1)) - try: - n = int(tokens[0]) - p = int(tokens[1]) - Q = float(tokens[2]) - except Exception as e: - raise ValueError("Error parsing instance parameters at case {}: {}".format(case_idx + 1, e)) - index += 1 - - # Read n customer lines - customers = [] - if len(lines) < index + n: - raise ValueError("Expected {} customer lines for case {}, but found fewer.".format(n, case_idx + 1)) - for i in range(n): - tokens = lines[index].split() - if len(tokens) < 4: - raise ValueError("Invalid customer data at line {} in case {}.".format(index + 1, case_idx + 1)) - try: - customer_id = int(tokens[0]) - x = float(tokens[1]) - y = float(tokens[2]) - demand = float(tokens[3]) - except Exception as e: - raise ValueError( - "Error parsing customer data on line {} in case {}: {}".format(index + 1, case_idx + 1, e)) - customers.append((customer_id, x, y, demand)) - index += 1 - - case_data = { - "best_known": best_known, - "n": n, - "p": p, - "Q": Q, - "customers": customers - } - cases.append(case_data) - - return cases - - def eval_func(self, **kwargs): - """ - Evaluate the solution for a single instance of the Capacitated P-Median Problem. - This function expects the following keyword arguments (combined from the instance data and the solution): - - best_known (float): Best known solution value (for reference). - - n (int): Number of customers. - - p (int): Number of medians. - - Q (float): Capacity of each median. - - customers (list of tuples): Each tuple is (customer_id, x, y, demand). - - objective (numeric): The objective value (total cost) reported by the solution. - - medians (list of int): List of chosen medians (customer IDs), exactly p elements. - - assignments (list of int): List of assignments for each customer (length n), where each entry is one of the chosen medians. - The evaluation performs the following: - 1. Verifies that each assignment is to one of the selected medians. - 2. Checks that the total demand assigned to each median does not exceed Q. - 3. Recomputes the total cost as the sum, over all customers, of the Euclidean distance (rounded down) - from the customer to its assigned median. - 4. Computes the score as: score = best_known / computed_total_cost. - Returns: - A scalar float representing the score for the solution. - """ - import math - - # Extract instance data - best_known = kwargs.get("best_known") - n = kwargs.get("n") - p = kwargs.get("p") - Q = kwargs.get("Q") - customers = kwargs.get("customers") - - # Extract solution data - reported_obj = kwargs.get("objective") - medians = kwargs.get("medians") - assignments = kwargs.get("assignments") - - if best_known is None or n is None or p is None or Q is None or customers is None: - raise ValueError("Instance data is incomplete.") - if reported_obj is None or medians is None or assignments is None: - raise ValueError("Solution data is incomplete.") - - # Validate medians length - if len(medians) != p: - raise ValueError("The solution must contain exactly {} medians; found {}.".format(p, len(medians))) - - # Validate assignments length - if len(assignments) != n: - raise ValueError("The solution must contain exactly {} assignments; found {}.".format(n, len(assignments))) - - # Build a dictionary for quick lookup of customer data by customer_id. - cust_dict = {} - for cust in customers: - cid, x, y, demand = cust - cust_dict[cid] = (x, y, demand) - - # Verify that each median is a valid customer. - for m in medians: - if m not in cust_dict: - raise ValueError("Median {} is not found in the customer data.".format(m)) - - # Verify that each customer's assignment is one of the selected medians. - for idx, a in enumerate(assignments): - if a not in medians: - raise ValueError( - "Customer {} is assigned to {} which is not in the list of selected medians.".format(idx + 1, a)) - - # Check capacity constraints. - capacity_usage = {m: 0.0 for m in medians} - for i, a in enumerate(assignments): - # Assuming that the order of customers in 'customers' corresponds to customer 1..n. - demand = customers[i][3] - capacity_usage[a] += demand - for m, used in capacity_usage.items(): - if used > Q + 1e-6: # small tolerance - raise ValueError( - "Capacity exceeded for median {}: used capacity {:.4f} exceeds allowed capacity {:.4f}.".format(m, - used, - Q)) - - # Recompute the total cost. - total_cost = 0 - for i, a in enumerate(assignments): - # Get customer i data. - try: - cid, cx, cy, _ = customers[i] - except Exception as e: - raise ValueError("Error accessing data for customer {}: {}".format(i + 1, e)) - # Get the assigned median's coordinates. - if a not in cust_dict: - raise ValueError("Assigned median {} for customer {} not found.".format(a, i + 1)) - mx, my, _ = cust_dict[a] - d = math.sqrt((cx - mx) ** 2 + (cy - my) ** 2) - total_cost += math.floor(d) - - if total_cost <= 0: - raise ValueError("Computed total cost is non-positive, which is invalid.") - - score = best_known / total_cost - return score - - def get_dev(self): - dev = {'pmedcap1.txt': [3, 11, 16, 0, 4, 2, 1, 9, 19, 18]} - - return dev - - - - - - -# Task configuration for benchmark task -ENTRY_NAME = 'solve' -FUNCTION_SIGNATURE = 'def solve(...):' -IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' -TASK_DESCRIPTION = '("The Capacitated P-Median Problem is a facility location optimization problem where the objective "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The Capacitated P-Median Problem is a facility location optimization problem where the objective "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(best_known: float, n: int, p: int, Q: float, customers: list) -> dict:\n """\n Solve the Capacitated P-Median Problem.\n This function receives the data for one problem instance via keyword arguments:\n - best_known (float): Best known solution value for reference.\n - n (int): Number of customers.\n - p (int): Number of medians to choose.\n - Q (float): Capacity limit for each median.\n - customers (list of tuples): Each tuple is (customer_id, x, y, demand).\n The goal is to select p medians (from the customers) and assign every customer to one\n of these medians so that the total cost is minimized. The cost for a customer is the\n Euclidean distance (rounded down to the nearest integer) to its assigned median, and the\n total demand assigned to each median must not exceed Q.\n Evaluation Metric:\n The solution is evaluated by computing the ratio:\n score = best_known / computed_total_cost,\n where computed_total_cost is the sum over all customers of the (floored) Euclidean distance\n to its assigned median.\n Note: This is a placeholder function. Replace the placeholder with an actual algorithm.\n Returns:\n A dictionary with the following keys:\n - \'objective\': (numeric) the total cost (objective value) computed by the algorithm.\n - \'medians\': (list of int) exactly p customer IDs chosen as medians.\n - \'assignments\': (list of int) a list of n integers, where the i-th integer is the customer\n ID (from the chosen medians) assigned to customer i.\n """\n # Placeholder: Replace this with your actual implementation.\n # For now, we return an empty solution structure.\n return {\n "objective": 0, # total cost (to be computed)\n "medians": [], # list of p medians (customer IDs)\n "assignments": [] # list of n assignments (each is one of the medians)\n }' -EVAL_CLASS_NAME = 'PMCEvaluationCB' -EVAL_KWARGS = {'timeout_seconds': 60} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_p_median_capacitated/paras.yaml b/examples/benchmark_tasks/optimization_p_median_capacitated/paras.yaml deleted file mode 100644 index 62454cda..00000000 --- a/examples/benchmark_tasks/optimization_p_median_capacitated/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: PMCEvaluationCB -timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_p_median_uncapacitated/__init__.py b/examples/benchmark_tasks/optimization_p_median_uncapacitated/__init__.py deleted file mode 100644 index 9c3b82a9..00000000 --- a/examples/benchmark_tasks/optimization_p_median_uncapacitated/__init__.py +++ /dev/null @@ -1,367 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_p_median_uncapacitated -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# References: -# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language -# model agents in algorithm search for combinatorial optimization. -# arXiv preprint arXiv:2504.04310 (2025). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -from typing import Any -import numpy as np -from llm4ad_loader import Evaluation -from llm4ad_loader import load_subdir_as_text -# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader -# from llm4ad.task.optimization.co_bench.p_median_uncapacitated_co_bench.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n: int, m: int, p: int, dist: list) -> dict:\n """\n Solves the uncapacitated p-median problem on a given graph.\n Input kwargs:\n - n: int, number of vertices.\n - m: int, number of edges.\n - p: int, number of medians to choose.\n - dist: list of lists, the complete cost matrix (n x n) computed via Floyd’s algorithm.\n Evaluation metric:\n The total assignment cost, defined as the sum (over all vertices) of the shortest distance\n from that vertex to its closest chosen median.\n Returns:\n A dictionary with a single key:\n - \'medians\': a list of exactly p distinct integers (each between 1 and n) representing\n the indices of the chosen medians.\n Note: This is a placeholder. The actual solution logic should populate the \'medians\' list.\n """\n # Placeholder implementation; replace with your solution logic.\n return {"medians": []}' -task_description = '("The uncapacitated p-median problem is a combinatorial optimization problem defined on a given "' - - -__all__ = ['PMUEvaluationCB'] - - -class PMUEvaluationCB(Evaluation): - - def __init__(self, - timeout_seconds=300, - **kwargs): - - """ - Args: - None - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - # Load datasets from Hugging Face - dataset = load_subdir_as_text("CO-Bench/CO-Bench", "p-median - uncapacitated") - self._datasets = {} - for filename in dataset: - # Join all text rows into a single string - text_content = '\n'.join([row['text'] for row in dataset[filename]]) - self._datasets[filename] = text_content - - def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: - return self.evaluate(callable_func) - - def evaluate(self, eva: callable) -> float | None: - ins_cases = [] - for case_id, ins in enumerate(self._datasets.values()): - ins_cases.append(self.load_data(ins)) - - fitness_list = [] - try: - for i in ins_cases: - for j in i: - result = eva(j['n'], j['m'], j['p'], j['dist']) - fitness = self.eval_func(n=j['n'], p=j['p'], m=j['m'], dist=j['dist'], medians=result['medians']) - fitness_list.append(fitness) - - return -np.mean(fitness_list) - - except ValueError as e: - print(e) - return None - - def load_data(self, input_string): - """ - Loads one or more cases from the input file for the p-median problem, optimized for speed. - This version uses NumPy to perform the Floyd–Warshall algorithm in a vectorized manner, - which is significantly faster than the pure-Python triple nested loops for moderate-to-large graphs. - The input is expected to have one or more cases. Each case starts with a header line - containing three numbers: n m p, where: - - n: number of vertices, - - m: number of edges, - - p: number of medians to choose. - This is followed by at least m non-empty lines, each specifying an edge in the format: - i j cost - (If there are more than m edge lines, only the first m valid ones are used.) - For each case, the function builds the complete cost matrix by: - - Initializing an n x n NumPy array with infinity (and 0 on the diagonal). - - Processing m valid edges (using the last occurrence for duplicate edges). - - Running a vectorized Floyd–Warshall algorithm to compute all-pairs shortest paths. - Returns: - A list of dictionaries, one per case. Each dictionary contains: - - 'n': int, number of vertices. - - 'm': int, number of edges. - - 'p': int, number of medians to choose. - - 'dist': list of lists, the complete cost matrix (n x n), converted from a NumPy array. - """ - import numpy as np - import math - - INF = math.inf - - # Read the entire file and filter out empty lines - all_lines = [line.strip() for line in input_string.split('\n')] - - cases = [] - idx = 0 - while idx < len(all_lines): - header_parts = all_lines[idx].split() - idx += 1 - if len(header_parts) < 3: - raise ValueError("Header line must contain at least three numbers: n, m, p.") - try: - n = int(header_parts[0]) - m = int(header_parts[1]) - p = int(header_parts[2]) - except Exception as e: - raise ValueError("Invalid header values.") from e - - # Initialize the cost matrix using NumPy for fast operations. - dist = np.full((n, n), INF, dtype=float) - np.fill_diagonal(dist, 0.0) - - edges_read = 0 - while edges_read < m and idx < len(all_lines): - tokens = all_lines[idx].split() - idx += 1 - if len(tokens) < 3: - continue - try: - u = int(tokens[0]) - v = int(tokens[1]) - c = float(tokens[2]) - except Exception: - continue - if 1 <= u <= n and 1 <= v <= n: - # Update both symmetric entries; the last occurrence overwrites previous ones. - dist[u - 1, v - 1] = c - dist[v - 1, u - 1] = c - edges_read += 1 - - # Vectorized Floyd–Warshall: update distances using broadcasting. - for k in range(n): - # Update: dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]) for all i, j. - dist = np.minimum(dist, dist[:, k:k + 1] + dist[k:k + 1, :]) - - # Convert the NumPy array to a list of lists for compatibility. - cases.append({ - "n": n, - "m": m, - "p": p, - "dist": dist.tolist() - }) - - return cases - - def eval_func(self, **kwargs): - """ - Evaluates a candidate solution for the uncapacitated p-median problem. - Parameters: - candidate_data (dict): Contains the input data for a single case with keys: - - 'n': int, number of vertices. - - 'm': int, number of edges. - - 'p': int, number of medians to choose. - - 'dist': list of lists, the complete cost matrix (n x n). - solution (dict): The candidate solution with key: - - 'medians': list of exactly p distinct integers (each between 1 and n). - Returns: - float: The total assignment cost, i.e., the sum over all vertices of the shortest distance - to the nearest chosen median. - Raises: - ValueError: If the solution is invalid due to incorrect format, duplicates, out-of-range values, - or if any vertex is unreachable from all medians. - """ - n = kwargs.get("n") - p = kwargs.get("p") - dist = kwargs.get("dist") - medians = kwargs.get("medians", []) - - # Validate input constraints - if not isinstance(n, int) or n <= 0: - raise ValueError("Invalid number of vertices (n). Must be a positive integer.") - if not isinstance(p, int) or p <= 0 or p > n: - raise ValueError("Invalid number of medians (p). Must be a positive integer and at most n.") - if not isinstance(dist, list) or len(dist) != n or any(len(row) != n for row in dist): - raise ValueError("Invalid distance matrix. Must be a square matrix of size (n x n).") - if not isinstance(medians, list) or len(medians) != p: - raise ValueError(f"Medians must be a list of exactly {p} distinct integers.") - if len(set(medians)) != p: - raise ValueError("Medians must be distinct values.") - if any(not isinstance(m, int) or m < 1 or m > n for m in medians): - raise ValueError("Each median must be an integer in the range [1, n].") - - INF = float('inf') - total_cost = 0.0 - - for i in range(n): - best_distance = INF - for median in medians: - d = dist[i][median - 1] # Adjust for 0-indexing. - if d < best_distance: - best_distance = d - if best_distance == INF: - raise ValueError(f"Vertex {i + 1} is unreachable from all chosen medians.") - total_cost += best_distance - - return total_cost - - def norm_score(self, results): - optimal_scores = { - "pmed1.txt": [5819], - "pmed2.txt": [4093], - "pmed3.txt": [4250], - "pmed4.txt": [3034], - "pmed5.txt": [1355], - "pmed6.txt": [7824], - "pmed7.txt": [5631], - "pmed8.txt": [4445], - "pmed9.txt": [2734], - "pmed10.txt": [1255], - "pmed11.txt": [7696], - "pmed12.txt": [6634], - "pmed13.txt": [4374], - "pmed14.txt": [2968], - "pmed15.txt": [1729], - "pmed16.txt": [8162], - "pmed17.txt": [6999], - "pmed18.txt": [4809], - "pmed19.txt": [2845], - "pmed20.txt": [1789], - "pmed21.txt": [9138], - "pmed22.txt": [8579], - "pmed23.txt": [4619], - "pmed24.txt": [2961], - "pmed25.txt": [1828], - "pmed26.txt": [9917], - "pmed27.txt": [8307], - "pmed28.txt": [4498], - "pmed29.txt": [3033], - "pmed30.txt": [1989], - "pmed31.txt": [10086], - "pmed32.txt": [9297], - "pmed33.txt": [4700], - "pmed34.txt": [3013], - "pmed35.txt": [10400], - "pmed36.txt": [9934], - "pmed37.txt": [5057], - "pmed38.txt": [11060], - "pmed39.txt": [9423], - "pmed40.txt": [5128] - } - - normed = {} - for case, (scores, error_message) in results.items(): - if case not in optimal_scores: - continue # Skip if there's no optimal score defined. - optimal_list = optimal_scores[case] - normed_scores = [] - # Compute normalized score for each index. - for idx, score in enumerate(scores): - if isinstance(score, (int, float)): - normed_scores.append(optimal_list[idx] / score) - else: - normed_scores.append(score) - normed[case] = (normed_scores, error_message) - - return normed - - def get_dev(self): - dev = {'pmed1.txt': [], 'pmed11.txt': [], 'pmed13.txt': [], - 'pmed15.txt': [], 'pmed17.txt': [], 'pmed19.txt': [], - 'pmed21.txt': [], 'pmed23.txt': [], 'pmed25.txt': [], - 'pmed27.txt': [], 'pmed29.txt': [], 'pmed3.txt': [], - 'pmed31.txt': [], 'pmed33.txt': [], 'pmed35.txt': [], - 'pmed37.txt': [], 'pmed39.txt': [], 'pmed5.txt': [], - 'pmed7.txt': [], 'pmed9.txt': []} - - return dev - - - - - - - -# Task configuration for benchmark task -ENTRY_NAME = 'solve' -FUNCTION_SIGNATURE = 'def solve(...):' -IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\n from that vertex to its closest chosen median.' -TASK_DESCRIPTION = '("The uncapacitated p-median problem is a combinatorial optimization problem defined on a given "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The uncapacitated p-median problem is a combinatorial optimization problem defined on a given "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n: int, m: int, p: int, dist: list) -> dict:\n """\n Solves the uncapacitated p-median problem on a given graph.\n Input kwargs:\n - n: int, number of vertices.\n - m: int, number of edges.\n - p: int, number of medians to choose.\n - dist: list of lists, the complete cost matrix (n x n) computed via Floyd’s algorithm.\n Evaluation metric:\n The total assignment cost, defined as the sum (over all vertices) of the shortest distance\n from that vertex to its closest chosen median.\n Returns:\n A dictionary with a single key:\n - \'medians\': a list of exactly p distinct integers (each between 1 and n) representing\n the indices of the chosen medians.\n Note: This is a placeholder. The actual solution logic should populate the \'medians\' list.\n """\n # Placeholder implementation; replace with your solution logic.\n return {"medians": []}' -EVAL_CLASS_NAME = 'PMUEvaluationCB' -EVAL_KWARGS = {'timeout_seconds': 300} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_p_median_uncapacitated/paras.yaml b/examples/benchmark_tasks/optimization_p_median_uncapacitated/paras.yaml deleted file mode 100644 index 9ab656bc..00000000 --- a/examples/benchmark_tasks/optimization_p_median_uncapacitated/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: PMUEvaluationCB -timeout_seconds: 300 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_packing_unequal_circles/__init__.py b/examples/benchmark_tasks/optimization_packing_unequal_circles/__init__.py deleted file mode 100644 index f65d25e3..00000000 --- a/examples/benchmark_tasks/optimization_packing_unequal_circles/__init__.py +++ /dev/null @@ -1,337 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_packing_unequal_circles -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# References: -# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language -# model agents in algorithm search for combinatorial optimization. -# arXiv preprint arXiv:2504.04310 (2025). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -from typing import Any -import numpy as np -from llm4ad_loader import Evaluation -from llm4ad_loader import load_subdir_as_text -# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader -# from llm4ad.task.optimization.co_bench.packing_unequal_circles_co_bench.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n: int, cx: float, cy: float, R: float, radii: list) -> dict:\n """\n Solve the unequal circle packing problem for the maximize-number case.\n Problem Description:\n Given a circular container with center (cx, cy) and radius R, and n circles with specified radii (sorted in increasing order),\n the task is to select and pack a prefix of the sorted list—i.e., if circle i is packed, then all circles with a smaller index must also be packed—in order to maximize the number of circles placed.\n Each packed circle must be fully contained within the container, meaning that the distance from its center to (cx, cy) plus its radius must not exceed R, and no two packed circles may overlap, which requires that the distance between any two centers is at least the sum of their respective radii.\n Input kwargs:\n - n : int, the number of circles.\n - cx : float, x-coordinate of the container\'s center.\n - cy : float, y-coordinate of the container\'s center.\n - R : float, the radius of the container.\n - radii : list of float, the radius of each circle (assumed sorted in increasing order).\n Returns:\n A dictionary with one key:\n - "coords": a list of n (x, y) tuples corresponding to the centers of the circles.\n For circles that are not packed, the coordinates default to (-1, -1).\n """\n return {"coords": []}' -task_description = '("The problem involves packing a subset of unequal circles into a fixed circular container with "' - - -__all__ = ['PUCEvaluationCB'] - - -class PUCEvaluationCB(Evaluation): - - def __init__(self, - timeout_seconds=50, - **kwargs): - - """ - Args: - None - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - # Load datasets from Hugging Face - dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Packing unequal circles") - self._datasets = {} - for filename in dataset: - # Join all text rows into a single string - text_content = '\n'.join([row['text'] for row in dataset[filename]]) - self._datasets[filename] = text_content - - def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: - return self.evaluate(callable_func) - - def evaluate(self, eva: callable) -> float | None: - ins_cases = [] - for case_id, ins in enumerate(self._datasets.values()): - ins_cases.append(self.load_data(ins)) - - fitness_list = [] - try: - for i in ins_cases: - for j in i: - result = eva(j['n'], j['cx'], j['cy'], j['R'], j['radii']) - fitness = self.eval_func(n=j['n'], cx=j['cx'], cy=j['cy'], R=j['R'], radii=j['radii'], coords=result['coords']) - fitness_list.append(fitness) - - return np.mean(fitness_list) - - except ValueError as e: - print(e) - return None - - def load_data(self, input_string): - """ - Load and parse the input file containing one or multiple cases. - File Format: - - The file is a plain-text file with non-empty lines. - - Each case starts with a header line containing exactly four numbers: - n cx cy R - where: - • n is the number of circles (an integer), - • cx and cy are the container's center coordinates (floats), - • R is the container's radius (float). - - The next n non-empty lines each contain one real number representing - the radius of a circle. - Returns: - A list of cases, where each case is a dictionary with keys: - "n" : int, number of circles. - "cx" : float, container center x-coordinate. - "cy" : float, container center y-coordinate. - "R" : float, container radius. - "radii" : list of float, the radii of the circles. - """ - cases = [] - try: - lines = [line.strip() for line in input_string.split('\n') if line.strip() != ''] - except Exception as e: - raise Exception(f"Error reading input file: {e}") - - i = 0 - total_lines = len(lines) - while i < total_lines: - header_tokens = lines[i].split() - if len(header_tokens) != 4: - raise Exception(f"Header line at line {i + 1} must contain exactly 4 numbers: n cx cy R.") - try: - n = int(header_tokens[0]) - cx = float(header_tokens[1]) - cy = float(header_tokens[2]) - R = float(header_tokens[3]) - except Exception as e: - raise Exception(f"Error parsing header on line {i + 1}: {e}") - - if i + n >= total_lines: - raise Exception(f"Not enough lines for {n} circle radii after line {i + 1}.") - radii = [] - for j in range(1, n + 1): - try: - # Even if there are extra tokens, take the first as the radius. - r = float(lines[i + j].split()[0]) - radii.append(r) - except Exception as e: - raise Exception(f"Error parsing circle radius on line {i + j + 1}: {e}") - case = {"n": n, "cx": cx, "cy": cy, "R": R, "radii": radii} - cases.append(case) - i += n + 1 # Move to the next case header (if any) - return cases - - def eval_func(self, **kwargs): - """ - Evaluate the solution for the Maximise Number problem of Unequal Circle Packing. - Input (merged from the case data and the solution): - - n : int, the total number of circles. - - cx : float, x-coordinate of the container's center. - - cy : float, y-coordinate of the container's center. - - R : float, the container's radius. - - radii : list of float, radii for each circle (assumed sorted in increasing order). - - coords: list of (x, y) tuples, the centers of the circles as produced by solve. - Evaluation Details: - 1. Identify “packed” circles. By convention, a circle is considered packed if its coordinate - is not equal to the default (cx, cy) (within tolerance). For the maximize number problem, - the optimal solution packs a prefix of the sorted circles. - 2. Verify the prefix property: if any circle i is packed, then all circles with index < i must also be packed. - 3. For every packed circle: - - Check container feasibility: - Ensure that sqrt((x-cx)^2 + (y-cy)^2) + r_i <= R. - - Record the clearance: R - (distance to (cx,cy) + r_i). - 4. For every pair of packed circles, verify non-overlap: - Ensure that distance((x_i,y_i), (x_j,y_j)) >= r_i + r_j. - And record the pair clearance. - 5. If any feasibility constraint is violated (beyond a small tolerance), raise an error. - 6. Let the primary score be the number of circles packed (i.e. the prefix length). - Use the minimum clearance among packed circles as a tie-breaker. - (For example, final score = (number packed) + ε*(minimum clearance), with ε small.) - Returns: - float: the evaluation score (a higher score indicates a better solution). - The main component is the number of circles feasibly packed. - """ - import math - - tol = 1e-5 # Numerical tolerance. - - # Extract required inputs. - try: - n = kwargs["n"] - cx = kwargs["cx"] - cy = kwargs["cy"] - container_R = kwargs["R"] - radii = kwargs["radii"] - coords = kwargs["coords"] - except KeyError as e: - raise Exception(f"Missing required parameter: {e}") - - if len(coords) != n: - raise Exception(f"Expected {n} coordinates, but got {len(coords)}.") - - # Identify packed circles. - # Convention: a circle is considered not packed if its center is (cx, cy) within tolerance. - packed_indices = [] - for i in range(n): - x, y = coords[i] - if x != -1 and y != -1: - # if math.sqrt((x - cx) ** 2 + (y - cy) ** 2) > tol: - packed_indices.append(i) - - # Verify the prefix property: if a circle with index i is packed, then all circles with index < i must be packed. - if packed_indices: - K = max(packed_indices) # highest index among packed circles. - for i in range(K): - if i not in packed_indices: - raise Exception(f"Prefix property violated: circle {i} is not packed while circle {K} is packed.") - else: - K = -1 # No circles packed. - - # Evaluate feasibility of packed circles. - container_clearances = [] - for i in packed_indices: - x, y = coords[i] - r = radii[i] - dist = math.sqrt((x - cx) ** 2 + (y - cy) ** 2) - clearance = container_R - (dist + r) - if clearance < -tol: - raise Exception(f"Circle {i} violates container constraint by {-clearance}.") - container_clearances.append(clearance) - - pair_clearances = [] - for idx, i in enumerate(packed_indices): - for j in packed_indices[idx + 1:]: - x1, y1 = coords[i] - x2, y2 = coords[j] - center_distance = math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2) - required_distance = radii[i] + radii[j] - clearance = center_distance - required_distance - if clearance < -tol: - raise Exception(f"Circles {i} and {j} overlap by {-clearance}.") - pair_clearances.append(clearance) - - # Primary measure: number of circles packed. - # (Since indices are 0-based, number_packed = K+1 if any are packed.) - num_packed = (K + 1) if packed_indices else 0 - - # Final score: primary is the count of packed circles; use clearance as a tie-breaker. - score = num_packed - return score - - def norm_score(self, results): - optimal_scores = { - "circle1.txt": [6], - "circle2.txt": [15], - "circle3.txt": [22], - "circle4.txt": [30], - } - - normed = {} - for case, (scores, error_message) in results.items(): - if case not in optimal_scores: - continue # Skip if there's no optimal score defined. - optimal_list = optimal_scores[case] - normed_scores = [] - # Compute normalized score for each index. - for idx, score in enumerate(scores): - if isinstance(score, (int, float)): - normed_scores.append(score / optimal_list[idx]) - else: - normed_scores.append(score) - normed[case] = (normed_scores, error_message) - return normed - - - - - - - -# Task configuration for benchmark task -ENTRY_NAME = 'solve' -FUNCTION_SIGNATURE = 'def solve(...):' -IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' -TASK_DESCRIPTION = '("The problem involves packing a subset of unequal circles into a fixed circular container with "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The problem involves packing a subset of unequal circles into a fixed circular container with "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n: int, cx: float, cy: float, R: float, radii: list) -> dict:\n """\n Solve the unequal circle packing problem for the maximize-number case.\n Problem Description:\n Given a circular container with center (cx, cy) and radius R, and n circles with specified radii (sorted in increasing order),\n the task is to select and pack a prefix of the sorted list—i.e., if circle i is packed, then all circles with a smaller index must also be packed—in order to maximize the number of circles placed.\n Each packed circle must be fully contained within the container, meaning that the distance from its center to (cx, cy) plus its radius must not exceed R, and no two packed circles may overlap, which requires that the distance between any two centers is at least the sum of their respective radii.\n Input kwargs:\n - n : int, the number of circles.\n - cx : float, x-coordinate of the container\'s center.\n - cy : float, y-coordinate of the container\'s center.\n - R : float, the radius of the container.\n - radii : list of float, the radius of each circle (assumed sorted in increasing order).\n Returns:\n A dictionary with one key:\n - "coords": a list of n (x, y) tuples corresponding to the centers of the circles.\n For circles that are not packed, the coordinates default to (-1, -1).\n """\n return {"coords": []}' -EVAL_CLASS_NAME = 'PUCEvaluationCB' -EVAL_KWARGS = {'timeout_seconds': 60} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_packing_unequal_circles/paras.yaml b/examples/benchmark_tasks/optimization_packing_unequal_circles/paras.yaml deleted file mode 100644 index 3c04ec19..00000000 --- a/examples/benchmark_tasks/optimization_packing_unequal_circles/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: PUCEvaluationCB -timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_packing_unequal_circles_area/__init__.py b/examples/benchmark_tasks/optimization_packing_unequal_circles_area/__init__.py deleted file mode 100644 index e1a0fedf..00000000 --- a/examples/benchmark_tasks/optimization_packing_unequal_circles_area/__init__.py +++ /dev/null @@ -1,334 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_packing_unequal_circles_area -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# References: -# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language -# model agents in algorithm search for combinatorial optimization. -# arXiv preprint arXiv:2504.04310 (2025). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -from typing import Any -import numpy as np -from llm4ad_loader import Evaluation -from llm4ad_loader import load_subdir_as_text -# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader -# from llm4ad.task.optimization.co_bench.packing_unequal_circles_area_co_bench.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n:int, cx: float, cy: float, R: float, radii: list) -> dict:\n """\n Solve the Unequal Circle Packing problem (Maximize Area version).\n Problem Description:\n Given a circular container with center (cx, cy) and radius R, and n circles\n with specified radii (provided in \'radii\'), decide which circles to pack and\n determine the centers (x_i, y_i) for the packed circles such that:\n 1. Containment: Each packed circle i must lie completely within the container.\n (x_i - cx)^2 + (y_i - cy)^2 <= α_i * (R - radii[i])^2, for i = 1,...,n.\n (If α_i = 0, then the circle is not packed and its center is set to (cx, cy).)\n 2. Non-Overlap: For every pair of circles i and j (with i < j), if both are packed,\n their centers must satisfy:\n (x_i - x_j)^2 + (y_i - y_j)^2 >= ( (α_i + α_j - 1) * (radii[i] + radii[j]) )^2.\n (This is a linearized version of the product α_i * α_j used in the paper.)\n 3. Binary decisions: α_i ∈ {0, 1} for i = 1,...,n, where α_i = 1 indicates circle i is packed.\n (For circles not packed, we force (x_i, y_i) to equal (cx, cy).)\n 4. Objective: Maximize the total area of the circles packed:\n maximize sum_{i=1}^n α_i * (pi * radii[i]^2).\n Input kwargs:\n - n : int, the number of circles.\n - cx : float, x-coordinate of the container\'s center.\n - cy : float, y-coordinate of the container\'s center.\n - R : float, the radius of the container.\n - radii : list of float, each element is the radius of a circle.\n Returns:\n A dictionary with one key:\n - "coords": a list of n (x, y) tuples corresponding to the centers of the circles.\n For circles not packed (α_i = 0), (x, y) should be (-1, -1).\n """\n # ===== Placeholder Implementation =====\n\n return {"coords": []}' -task_description = '("The problem involves packing a subset of unequal circles into a fixed circular container with "' - - -__all__ = ['PUCAEvaluationCB'] - - -class PUCAEvaluationCB(Evaluation): - - def __init__(self, - timeout_seconds=50, - **kwargs): - - """ - Args: - None - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - # Load datasets from Hugging Face - dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Packing unequal circles area") - self._datasets = {} - for filename in dataset: - # Join all text rows into a single string - text_content = '\n'.join([row['text'] for row in dataset[filename]]) - self._datasets[filename] = text_content - - def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: - return self.evaluate(callable_func) - - def evaluate(self, eva: callable) -> float | None: - ins_cases = [] - for case_id, ins in enumerate(self._datasets.values()): - ins_cases.append(self.load_data(ins)) - - fitness_list = [] - try: - for i in ins_cases: - for j in i: - result = eva(j['n'], j['cx'], j['cy'], j['R'], j['radii']) - fitness = self.eval_func(n=j['n'], cx=j['cx'], cy=j['cy'], R=j['R'], radii=j['radii'], coords=result['coords']) - fitness_list.append(fitness) - - return np.mean(fitness_list) - - except ValueError as e: - print(e) - return None - - def load_data(self, input_string): - """ - Load and parse the input file containing one or multiple cases. - File Format: - - The file is a plain-text file with non-empty lines. - - Each case starts with a header line containing exactly four numbers: - n cx cy R - where: - • n is the number of circles (an integer), - • cx and cy are the container's center coordinates (floats), - • R is the container's radius (float). - - The next n non-empty lines each contain one real number representing - the radius of a circle. - Returns: - A list of cases, where each case is a dictionary with keys: - "n" : int, number of circles. - "cx" : float, container center x-coordinate. - "cy" : float, container center y-coordinate. - "R" : float, container radius. - "radii" : list of float, the radii of the circles. - """ - cases = [] - try: - lines = [line.strip() for line in input_string.split('\n') if line.strip() != ''] - except Exception as e: - raise Exception(f"Error reading input file: {e}") - - i = 0 - total_lines = len(lines) - while i < total_lines: - header_tokens = lines[i].split() - if len(header_tokens) != 4: - raise Exception(f"Header line at line {i + 1} must contain exactly 4 numbers: n cx cy R.") - try: - n = int(header_tokens[0]) - cx = float(header_tokens[1]) - cy = float(header_tokens[2]) - R = float(header_tokens[3]) - except Exception as e: - raise Exception(f"Error parsing header on line {i + 1}: {e}") - - if i + n >= total_lines: - raise Exception(f"Not enough lines for {n} circle radii after line {i + 1}.") - radii = [] - for j in range(1, n + 1): - try: - # Even if there are extra tokens, take the first as the radius. - r = float(lines[i + j].split()[0]) - radii.append(r) - except Exception as e: - raise Exception(f"Error parsing circle radius on line {i + j + 1}: {e}") - case = {"n": n, "cx": cx, "cy": cy, "R": R, "radii": radii} - cases.append(case) - i += n + 1 # Move to the next case header (if any) - return cases - - def eval_func(self, **kwargs): - """ - Evaluate the solution for the Maximise Area problem of Unequal Circle Packing. - Input (merged from the case data and the solution): - - n : int, the total number of circles. - - cx : float, x-coordinate of the container's center. - - cy : float, y-coordinate of the container's center. - - R : float, the container's radius. - - radii : list of float, radii for each circle. - - coords: list of (x, y) tuples, the centers of the circles as produced by solve. - A circle is considered unpacked if its center equals (-1, -1) (within tolerance). - Evaluation Details: - 1. Identify packed circles: a circle is considered packed if its center is not (-1, -1) - (within a small tolerance tol). - 2. For every packed circle: - - Verify container feasibility: - Ensure that sqrt((x - cx)^2 + (y - cy)^2) + r_i <= R (within tolerance). - - Record its container clearance: clearance = R - (distance from (cx, cy) + r_i). - 3. For every pair of packed circles, verify non-overlap: - Ensure that the distance between centers >= r_i + r_j (within tolerance). - And record the pair clearance: (distance - (r_i + r_j)). - 4. If any feasibility constraint is violated (beyond tol), raise an Exception. - 5. Compute the primary score as the total area of packed circles: - total_area = sum(π * (r_i)^2 for each packed circle). - Then, use the minimum clearance (across all container and pair clearances) as a tie-breaker. - (For example, final score = total_area + ε * (minimum clearance), with ε small.) - 6. Return the final score (a higher score indicates a better solution). - Returns: - float: the evaluation score. - """ - import math - - tol = 1e-5 # Numerical tolerance. - - # Extract required inputs. - try: - n = kwargs["n"] - cx = kwargs["cx"] - cy = kwargs["cy"] - container_R = kwargs["R"] - radii = kwargs["radii"] - coords = kwargs["coords"] - except KeyError as e: - raise Exception(f"Missing required parameter: {e}") - - if len(coords) != n: - raise Exception(f"Expected {n} coordinates, but got {len(coords)}.") - - # Identify packed circles. - # Convention: a circle is considered not packed if its center equals (-1, -1) within tolerance. - packed_indices = [] - for i in range(n): - x, y = coords[i] - if not (abs(x + 1) <= tol and abs(y + 1) <= tol): - packed_indices.append(i) - - # Evaluate feasibility for each packed circle (container constraint). - container_clearances = [] - for i in packed_indices: - x, y = coords[i] - r = radii[i] - dist = math.hypot(x - cx, y - cy) - clearance = container_R - (dist + r) - if clearance < -tol: - raise Exception(f"Circle {i} violates container constraint by {-clearance}.") - container_clearances.append(clearance) - - # Evaluate non-overlap feasibility for every pair of packed circles. - pair_clearances = [] - for idx, i in enumerate(packed_indices): - for j in packed_indices[idx + 1:]: - x1, y1 = coords[i] - x2, y2 = coords[j] - center_distance = math.hypot(x1 - x2, y1 - y2) - required_distance = radii[i] + radii[j] - clearance = center_distance - required_distance - if clearance < -tol: - raise Exception(f"Circles {i} and {j} overlap by {-clearance}.") - pair_clearances.append(clearance) - - # Primary measure: total area of packed circles. - total_area = 0.0 - for i in packed_indices: - total_area += math.pi * (radii[i] ** 2) - - # Final score: primary is the total area packed - score = total_area - return score - - def norm_score(self, results): - optimal_scores = { - "circle1.txt": [197.0718], - "circle2.txt": [290.5062], - "circle3.txt": [502.0171], - "circle4.txt": [642.9087], - } - - normed = {} - for case, (scores, error_message) in results.items(): - if case not in optimal_scores: - continue # Skip if there's no optimal score defined. - optimal_list = optimal_scores[case] - normed_scores = [] - # Compute normalized score for each index. - for idx, score in enumerate(scores): - if isinstance(score, (int, float)): - normed_scores.append(score / optimal_list[idx]) - else: - normed_scores.append(score) - normed[case] = (normed_scores, error_message) - return normed - - def get_dev(self): - dev = {'circle1.txt': [], 'circle2.txt': [], 'circle3.txt': [], 'circle4.txt': []} - - return dev - - - - - - - -# Task configuration for benchmark task -ENTRY_NAME = 'solve' -FUNCTION_SIGNATURE = 'def solve(...):' -IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' -TASK_DESCRIPTION = '("The problem involves packing a subset of unequal circles into a fixed circular container with "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The problem involves packing a subset of unequal circles into a fixed circular container with "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n:int, cx: float, cy: float, R: float, radii: list) -> dict:\n """\n Solve the Unequal Circle Packing problem (Maximize Area version).\n Problem Description:\n Given a circular container with center (cx, cy) and radius R, and n circles\n with specified radii (provided in \'radii\'), decide which circles to pack and\n determine the centers (x_i, y_i) for the packed circles such that:\n 1. Containment: Each packed circle i must lie completely within the container.\n (x_i - cx)^2 + (y_i - cy)^2 <= α_i * (R - radii[i])^2, for i = 1,...,n.\n (If α_i = 0, then the circle is not packed and its center is set to (cx, cy).)\n 2. Non-Overlap: For every pair of circles i and j (with i < j), if both are packed,\n their centers must satisfy:\n (x_i - x_j)^2 + (y_i - y_j)^2 >= ( (α_i + α_j - 1) * (radii[i] + radii[j]) )^2.\n (This is a linearized version of the product α_i * α_j used in the paper.)\n 3. Binary decisions: α_i ∈ {0, 1} for i = 1,...,n, where α_i = 1 indicates circle i is packed.\n (For circles not packed, we force (x_i, y_i) to equal (cx, cy).)\n 4. Objective: Maximize the total area of the circles packed:\n maximize sum_{i=1}^n α_i * (pi * radii[i]^2).\n Input kwargs:\n - n : int, the number of circles.\n - cx : float, x-coordinate of the container\'s center.\n - cy : float, y-coordinate of the container\'s center.\n - R : float, the radius of the container.\n - radii : list of float, each element is the radius of a circle.\n Returns:\n A dictionary with one key:\n - "coords": a list of n (x, y) tuples corresponding to the centers of the circles.\n For circles not packed (α_i = 0), (x, y) should be (-1, -1).\n """\n # ===== Placeholder Implementation =====\n\n return {"coords": []}' -EVAL_CLASS_NAME = 'PUCAEvaluationCB' -EVAL_KWARGS = {'timeout_seconds': 60} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_packing_unequal_circles_area/paras.yaml b/examples/benchmark_tasks/optimization_packing_unequal_circles_area/paras.yaml deleted file mode 100644 index 06ecec66..00000000 --- a/examples/benchmark_tasks/optimization_packing_unequal_circles_area/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: PUCAEvaluationCB -timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_packing_unequal_rectangles_and_squares/__init__.py b/examples/benchmark_tasks/optimization_packing_unequal_rectangles_and_squares/__init__.py deleted file mode 100644 index 0219f6c2..00000000 --- a/examples/benchmark_tasks/optimization_packing_unequal_rectangles_and_squares/__init__.py +++ /dev/null @@ -1,400 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_packing_unequal_rectangles_and_squares -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# References: -# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language -# model agents in algorithm search for combinatorial optimization. -# arXiv preprint arXiv:2504.04310 (2025). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -import math -from typing import Any -import numpy as np -from llm4ad_loader import Evaluation -from llm4ad_loader import load_subdir_as_text -# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader -# from llm4ad.task.optimization.co_bench.packing_unequal_rectangles_and_squares_co_bench.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n: int, cx: float, cy: float, R: float, items: list, shape: str, rotation: bool) -> dict:\n """\n Solves the "maximum number" packing problem for unequal rectangles and squares\n in a fixed-size circular container.\n Input kwargs:\n - n : int, total number of available items (rectangles or squares)\n - cx, cy : floats, coordinates of the container center (typically the origin)\n - R : float, radius of the circular container\n - items : list of tuples, where each tuple (L, W) specifies the dimensions\n of an item (for a square, L == W). Items are assumed to be ordered\n by increasing size.\n - shape : str, either "rectangle" or "square"\n - rotation : bool, indicating whether 90° rotation is allowed\n Objective:\n The goal is to pack as many items as possible inside the container. An item is\n considered packed if its entire geometry lies completely within the circular\n container and it does not overlap any other packed item.\n Evaluation:\n A valid solution is one in which no packed item extends outside the container\n and no two packed items overlap. The quality of a solution is measured solely by\n the number of items successfully packed (i.e. the higher the number, the better).\n Returns:\n A dictionary with the key \'placements\' containing a list of exactly n tuples.\n Each tuple is of the form (x-coordinate, y-coordinate, theta) where:\n - (x-coordinate, y-coordinate) is the center position of the item,\n - theta is the rotation angle in degrees (counter-clockwise from the horizontal) 90 or 0.\n - For any item that is not packed, set its x and y coordinates to -1\n (and theta can be set to 0).\n Note:\n This is a placeholder header. The actual solution logic is not implemented here.\n """\n ## placeholder.\n return {\'placements\': []}' -task_description = '("We are given a set of n unequal rectangles (or squares), each with specified dimensions, "' - - -__all__ = ['PURSEvaluationCB'] - - -class PURSEvaluationCB(Evaluation): - - def __init__(self, - timeout_seconds=50, - **kwargs): - - """ - Args: - None - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - # Load datasets from Hugging Face - dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Packing unequal rectangles and squares") - self._datasets = {} - for filename in dataset: - # Join all text rows into a single string - text_content = '\n'.join([row['text'] for row in dataset[filename]]) - self._datasets[filename] = text_content - - def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: - return self.evaluate(callable_func) - - def evaluate(self, eva: callable) -> float | None: - ins_cases = [] - for case_id, ins in enumerate(self._datasets.values()): - ins_cases.append(self.load_data(ins)) - - fitness_list = [] - try: - for i in ins_cases: - for j in i: - result = eva(j['n'], j['cx'], j['cy'], j['R'], j['items'], j['shape'], j['rotation']) - fitness = self.eval_func(n=j['n'], cx=j['cx'], cy=j['cy'], R=j['R'], items=j['items'], shape=j['shape'], rotation=j['rotation'], placements=result['placements']) - fitness_list.append(fitness) - - return np.mean(fitness_list) - - except ValueError as e: - print(e) - return None - - def load_data(self, input_string): - """ - Reads input string content that may contain multiple cases for the packing problem. - Each case is formatted as follows: - - A header line with four values: n, cx, cy, R - n : number of items (rectangles or squares) - cx, cy : container center coordinates - R : container radius - - Next n non-empty lines: each line represents an item: - * For a square: one number (side length) — interpreted as (side, side) - * For a rectangle: two numbers (length and width) - Returns: - A list of cases. Each case is a dictionary with the following keys: - - 'n' : int, number of items - - 'cx' : float, x-coordinate of container center - - 'cy' : float, y-coordinate of container center - - 'R' : float, container radius - - 'items': list of tuples, where each tuple is (L, W) for the respective item. - """ - cases = [] - lines = [line.strip() for line in input_string.split('\n') if line.strip() != ''] - - i = 0 - while i < len(lines): - # Parse header line for one case - header_tokens = lines[i].split() - if len(header_tokens) < 4: - raise ValueError("Header line must contain at least 4 values: n, cx, cy, R.") - n = int(header_tokens[0]) - cx = float(header_tokens[1]) - cy = float(header_tokens[2]) - R = float(header_tokens[3]) - i += 1 - - # Ensure there are enough lines for all items - if i + n > len(lines): - raise ValueError("Insufficient item lines for a case.") - - items = [] - shape = None - for j in range(n): - tokens = lines[i].split() - if len(tokens) == 1: - side = float(tokens[0]) - items.append((side, side)) - shape = 'square' - elif len(tokens) >= 2: - length = float(tokens[0]) - width = float(tokens[1]) - items.append((length, width)) - shape = 'rectangle' - else: - raise ValueError(f"Item data format error at line {i + 1}.") - i += 1 - - # Append the parsed case as a dictionary - if shape == 'rectangle': - cases.append({ - 'n': n, - 'cx': cx, - 'cy': cy, - 'R': R, - 'items': items, - 'shape': shape, - 'rotation': False - }) - cases.append({ - 'n': n, - 'cx': cx, - 'cy': cy, - 'R': R, - 'items': items, - 'shape': shape, - 'rotation': True - }) - else: - cases.append({ - 'n': n, - 'cx': cx, - 'cy': cy, - 'R': R, - 'items': items, - 'shape': shape, - 'rotation': False - - }) - - return cases - - def eval_func(self, **kwargs): - """ - Evaluates a solution for the "maximise number of items packed" rectangle (or square) - packing problem in a circular container. - Parameters: - input_data: dict with keys: - - n : int, total number of available items. - - cx, cy : floats, coordinates of the container center. - - R : float, container radius. - - items : list of tuples, where each tuple (L, W) gives the dimensions of an item. - (For squares, L == W.) - - shape : str, either "rectangle" or "square". - - rotation : bool, whether 90° rotation is allowed. - solution_output: dict with key 'placements' containing a list of exactly n tuples. - Each tuple is (x, y, theta), where: - - (x, y) are the center coordinates. - - theta is the rotation angle in degrees (counter-clockwise from horizontal). - - For any item that is not packed, x and y should be set to -1 (theta can be 0). - Returns: - score: int, the number of valid (packed) items. - Raises: - ValueError: if any constraint is violated. - """ - # Unpack input parameters. - tol = 1e-5 - n = kwargs.get("n") - cx = kwargs.get("cx") - cy = kwargs.get("cy") - R = kwargs.get("R") - items = kwargs.get("items") # list of (L, W) - shape = kwargs.get("shape").lower() # "rectangle" or "square" - rotation_allowed = kwargs.get("rotation") - - placements = kwargs.get("placements") - - # Check that exactly n placements are provided. - if not isinstance(placements, list) or len(placements) != n: - raise ValueError("The output must contain exactly n placements.") - - # List to hold the geometry of each packed item for later overlap checking. - # For each packed item, we will store a tuple: (xmin, xmax, ymin, ymax) - packed_rectangles = [] - - score = 0 # Count of packed items. - - for idx, placement in enumerate(placements): - if (not isinstance(placement, (list, tuple))) or len(placement) != 3: - raise ValueError(f"Placement {idx} must be a tuple of (x, y, theta).") - x, y, theta = placement - - # Check unpacked indicator: if x == -1 and y == -1 then item is not packed. - if x == -1 and y == -1: - # Unpacked item; theta is ignored. Continue. - continue - - # Otherwise, the item is packed. - score += 1 - - # --- Check rotation value. - # If rotation is not allowed then theta must be 0. - # If rotation is allowed, we require theta to be either 0 or 90 (within a small tolerance). - if rotation_allowed: - if not (math.isclose(theta, 0, abs_tol=1e-3) or math.isclose(theta, 90, abs_tol=1e-3)): - raise ValueError(f"Item {idx}: rotation angle must be 0 or 90 degrees when rotation is allowed.") - else: - if not math.isclose(theta, 0, abs_tol=1e-3): - raise ValueError(f"Item {idx}: rotation angle must be 0 when rotation is not allowed.") - - # --- Determine the effective dimensions of the item. - L, W = items[idx] - # For squares, ensure consistency. - if shape == "square" and not math.isclose(L, W, abs_tol=1e-3): - raise ValueError(f"Item {idx}: For square packing, dimensions must be equal.") - - # If rotated by 90, swap dimensions. - if rotation_allowed and math.isclose(theta, 90, abs_tol=1e-3): - eff_L, eff_W = W, L - else: - eff_L, eff_W = L, W - - half_L = eff_L / 2.0 - half_W = eff_W / 2.0 - - # --- Compute the coordinates of the four corners. - # Since theta is either 0 or 90, the rectangle remains axis aligned. - # For theta==0: corners are (x ± half_L, y ± half_W). - # For theta==90: same structure because dimensions have been swapped. - corners = [ - (x - half_L, y - half_W), - (x - half_L, y + half_W), - (x + half_L, y - half_W), - (x + half_L, y + half_W) - ] - - # --- Check that every corner is inside the container. - for corner in corners: - cx_corner, cy_corner = corner - # Distance from the container center (cx, cy) - dist = math.hypot(cx_corner - cx, cy_corner - cy) - if dist > R + tol: # use a small tolerance - raise ValueError(f"Item {idx}: Corner {corner} lies outside the container.") - - # --- Store the axis-aligned bounding box for overlap checking. - # (Since the rectangles are axis aligned, the bounding box is the rectangle itself.) - xmin = x - half_L - xmax = x + half_L - ymin = y - half_W - ymax = y + half_W - current_rect = (xmin, xmax, ymin, ymax) - - # --- Check for overlap with previously packed items. - for jdx, other_rect in enumerate(packed_rectangles): - oxmin, oxmax, oymin, oymax = other_rect - # Two axis-aligned rectangles do not overlap if one is to the left - # or one is above the other. - if not (xmax <= oxmin + tol or xmin >= oxmax - tol or - ymax <= oymin + tol or ymin >= oymax - tol): - raise ValueError(f"Item {idx} overlaps with an already packed item (index {jdx}).") - - # Save the current rectangle for future overlap checking. - packed_rectangles.append(current_rect) - - return score - - def norm_score(self, results): - optimal_scores = { - "rect1.txt": [7, 7], - "rect2.txt": [11, 12], - "rect3.txt": [19, 20], - "square1.txt": [6], - "square2.txt": [14], - "square3.txt": [23], - } - - normed = {} - for case, (scores, error_message) in results.items(): - if case not in optimal_scores: - continue # Skip if there's no optimal score defined. - optimal_list = optimal_scores[case] - normed_scores = [] - # Compute normalized score for each index. - for idx, score in enumerate(scores): - if isinstance(score, (int, float)): - normed_scores.append(score / optimal_list[idx]) - else: - normed_scores.append(score) - normed[case] = (normed_scores, error_message) - return normed - - - - - - - -# Task configuration for benchmark task -ENTRY_NAME = 'solve' -FUNCTION_SIGNATURE = 'def solve(...):' -IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' -TASK_DESCRIPTION = '("We are given a set of n unequal rectangles (or squares), each with specified dimensions, "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("We are given a set of n unequal rectangles (or squares), each with specified dimensions, "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n: int, cx: float, cy: float, R: float, items: list, shape: str, rotation: bool) -> dict:\n """\n Solves the "maximum number" packing problem for unequal rectangles and squares\n in a fixed-size circular container.\n Input kwargs:\n - n : int, total number of available items (rectangles or squares)\n - cx, cy : floats, coordinates of the container center (typically the origin)\n - R : float, radius of the circular container\n - items : list of tuples, where each tuple (L, W) specifies the dimensions\n of an item (for a square, L == W). Items are assumed to be ordered\n by increasing size.\n - shape : str, either "rectangle" or "square"\n - rotation : bool, indicating whether 90° rotation is allowed\n Objective:\n The goal is to pack as many items as possible inside the container. An item is\n considered packed if its entire geometry lies completely within the circular\n container and it does not overlap any other packed item.\n Evaluation:\n A valid solution is one in which no packed item extends outside the container\n and no two packed items overlap. The quality of a solution is measured solely by\n the number of items successfully packed (i.e. the higher the number, the better).\n Returns:\n A dictionary with the key \'placements\' containing a list of exactly n tuples.\n Each tuple is of the form (x-coordinate, y-coordinate, theta) where:\n - (x-coordinate, y-coordinate) is the center position of the item,\n - theta is the rotation angle in degrees (counter-clockwise from the horizontal) 90 or 0.\n - For any item that is not packed, set its x and y coordinates to -1\n (and theta can be set to 0).\n Note:\n This is a placeholder header. The actual solution logic is not implemented here.\n """\n ## placeholder.\n return {\'placements\': []}' -EVAL_CLASS_NAME = 'PURSEvaluationCB' -EVAL_KWARGS = {'timeout_seconds': 60} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_packing_unequal_rectangles_and_squares/paras.yaml b/examples/benchmark_tasks/optimization_packing_unequal_rectangles_and_squares/paras.yaml deleted file mode 100644 index 32ec3be0..00000000 --- a/examples/benchmark_tasks/optimization_packing_unequal_rectangles_and_squares/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: PURSEvaluationCB -timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_packing_unequal_rectangles_and_squares_area/__init__.py b/examples/benchmark_tasks/optimization_packing_unequal_rectangles_and_squares_area/__init__.py deleted file mode 100644 index 00b9f02a..00000000 --- a/examples/benchmark_tasks/optimization_packing_unequal_rectangles_and_squares_area/__init__.py +++ /dev/null @@ -1,442 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_packing_unequal_rectangles_and_squares_area -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# References: -# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language -# model agents in algorithm search for combinatorial optimization. -# arXiv preprint arXiv:2504.04310 (2025). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -import math -from typing import Any -import numpy as np -from llm4ad_loader import Evaluation -from llm4ad_loader import load_subdir_as_text -# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader -# from llm4ad.task.optimization.co_bench.packing_unequal_rectangles_and_squares_area_co_bench.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n: int, cx: float, cy: float, R: float, items: list, shape: str, rotation: bool) -> dict:\n """\n Solves the problem of packing a subset of unequal rectangles and squares into a fixed‐size circular container\n with the objective of maximizing the total area of the items placed inside the container.\n Input kwargs:\n - n : int, the number of items (rectangles or squares)\n - cx, cy : floats, the coordinates of the container center\n - R : float, the radius of the container\n - items : list of tuples, where each tuple (L, W) gives the dimensions of an item\n (for a square, L == W)\n - shape : string, either "rectangle" or "square"\n - rotation : bool, whether 90° rotation is allowed (True or False)\n Objective:\n - Select and place a subset of the given items so that each packed item lies completely inside the circular container,\n no two packed items overlap, and the sum of the areas of the packed items is maximized.\n - An item that is not packed contributes zero area.\n Returns:\n A dictionary with the key \'placements\' containing a list of exactly n tuples.\n Each tuple is (x-coordinate, y-coordinate, theta) where:\n - (x-coordinate, y-coordinate) is the center position of the item (if packed),\n - theta is the rotation angle in degrees (counter-clockwise from the horizontal). 90 or 0.\n - For an unpacked item, x and y should be set to -1 and theta to 0 (or another default value).\n Note: This is a placeholder. The actual solution logic is not implemented here.\n """\n ## placeholder.\n return {\'placements\': []}' -task_description = '("We consider the problem of selecting and placing a subset of n unequal rectangles (or squares) "' - - -__all__ = ['PURSAEvaluationCB'] - - -class PURSAEvaluationCB(Evaluation): - - def __init__(self, - timeout_seconds=50, - **kwargs): - - """ - Args: - None - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - # Load datasets from Hugging Face - dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Packing unequal rectangles and squares area") - self._datasets = {} - for filename in dataset: - # Join all text rows into a single string - text_content = '\n'.join([row['text'] for row in dataset[filename]]) - self._datasets[filename] = text_content - - def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: - return self.evaluate(callable_func) - - def evaluate(self, eva: callable) -> float | None: - ins_cases = [] - for case_id, ins in enumerate(self._datasets.values()): - ins_cases.append(self.load_data(ins)) - - fitness_list = [] - try: - for i in ins_cases: - for j in i: - result = eva(j['n'], j['cx'], j['cy'], j['R'], j['items'], j['shape'], j['rotation']) - fitness = self.eval_func(n=j['n'], cx=j['cx'], cy=j['cy'], R=j['R'], items=j['items'], shape=j['shape'], rotation=j['rotation'], placements=result['placements']) - fitness_list.append(fitness) - - return np.mean(fitness_list) - - except ValueError as e: - print(e) - return None - - def load_data(self, input_string): - """ - Reads input string content that may contain multiple cases for the packing problem. - Each case is formatted as follows: - - A header line with four values: n, cx, cy, R - n : number of items (rectangles or squares) - cx, cy : container center coordinates - R : container radius - - Next n non-empty lines: each line represents an item: - * For a square: one number (side length) — interpreted as (side, side) - * For a rectangle: two numbers (length and width) - Returns: - A list of cases. Each case is a dictionary with the following keys: - - 'n' : int, number of items - - 'cx' : float, x-coordinate of container center - - 'cy' : float, y-coordinate of container center - - 'R' : float, container radius - - 'items': list of tuples, where each tuple is (L, W) for the respective item. - """ - cases = [] - lines = [line.strip() for line in input_string.split('\n') if line.strip() != ''] - - i = 0 - while i < len(lines): - # Parse header line for one case - header_tokens = lines[i].split() - if len(header_tokens) < 4: - raise ValueError("Header line must contain at least 4 values: n, cx, cy, R.") - n = int(header_tokens[0]) - cx = float(header_tokens[1]) - cy = float(header_tokens[2]) - R = float(header_tokens[3]) - i += 1 - - # Ensure there are enough lines for all items - if i + n > len(lines): - raise ValueError("Insufficient item lines for a case.") - - items = [] - shape = None - for j in range(n): - tokens = lines[i].split() - if len(tokens) == 1: - side = float(tokens[0]) - items.append((side, side)) - shape = 'square' - elif len(tokens) >= 2: - length = float(tokens[0]) - width = float(tokens[1]) - items.append((length, width)) - shape = 'rectangle' - else: - raise ValueError(f"Item data format error at line {i + 1}.") - i += 1 - - # Append the parsed case as a dictionary - if shape == 'rectangle': - cases.append({ - 'n': n, - 'cx': cx, - 'cy': cy, - 'R': R, - 'items': items, - 'shape': shape, - 'rotation': False - }) - cases.append({ - 'n': n, - 'cx': cx, - 'cy': cy, - 'R': R, - 'items': items, - 'shape': shape, - 'rotation': True - }) - else: - cases.append({ - 'n': n, - 'cx': cx, - 'cy': cy, - 'R': R, - 'items': items, - 'shape': shape, - 'rotation': False - - }) - - return cases - - def eval_func(self, **kwargs): - """ - Evaluates a candidate solution for the "maximize total area" rectangle/square packing problem. - The function expects: - data: a dict with keys: - - n : int, number of items (rectangles or squares) - - cx, cy : floats, coordinates of the container center - - R : float, radius of the container - - items : list of tuples, each (L, W) giving dimensions of an item - - shape : string, either "rectangle" or "square" - - rotation : bool, whether 90° rotation is allowed - sol: a dict with key 'placements' containing a list of exactly n tuples. - Each tuple is (x, y, theta) where: - - (x, y) is the center position for the item (if packed), - - theta is the rotation angle in degrees (counter-clockwise from the horizontal). - - For an unpacked item, x and y must be exactly -1 and theta is ignored (or should be 0). - The evaluation process checks all feasibility constraints: - 1. The number of placements equals n. - 2. Each placement tuple must have three numerical values. - 3. For each item: - - If it is "unpacked" (x == -1 and y == -1), it contributes no area. - - If it is "packed" (x,y != -1), then: - a. If rotation is not allowed, theta must be 0 (within a tiny tolerance). - b. If rotation is allowed, theta must be either 0 or 90 (within tolerance). - c. The entire item (with given dimensions and rotation) must lie completely inside - the circular container (centered at (cx, cy) with radius R). - 4. No two packed items may overlap (their interiors should be disjoint). - If any constraint is violated, the function raises a ValueError with an appropriate message. - If all constraints are met, the function returns the total area of the packed items. - (This is the score that we wish to maximize.) - Note: The evaluation is designed to be robust against malicious modifications - by the solve function. Only valid solutions (with zero penalties) receive a score. - """ - - # Tolerances for numerical comparisons - tol = 1e-5 - angle_tol = 1e-3 # tolerance for angle comparisons in degrees - - # Unpack input data - try: - n = kwargs['n'] - cx, cy = float(kwargs['cx']), float(kwargs['cy']) - R = float(kwargs['R']) - items = kwargs['items'] - shape = kwargs['shape'].lower() - rotation_allowed = bool(kwargs['rotation']) - except KeyError as e: - raise ValueError(f"Missing input data key: {e}") - - if len(items) != n: - raise ValueError("Length of items list must equal n.") - - # Unpack solution - placements = kwargs.get('placements', None) - if placements is None: - raise ValueError("Solution does not contain key 'placements'.") - if not isinstance(placements, list) or len(placements) != n: - raise ValueError("The 'placements' list must contain exactly n tuples.") - - # Helper: Given a placement (x, y, theta in degrees) and item dimensions (L, W), - # compute the four vertices of the rectangle after rotation. - def compute_vertices(x, y, L, W, theta_deg): - theta = math.radians(theta_deg) - # Local coordinates of corners before rotation: - local_corners = [(L / 2, W / 2), - (L / 2, -W / 2), - (-L / 2, W / 2), - (-L / 2, -W / 2)] - vertices = [] - cos_t = math.cos(theta) - sin_t = math.sin(theta) - for dx, dy in local_corners: - # Apply rotation: - dx_r = dx * cos_t - dy * sin_t - dy_r = dx * sin_t + dy * cos_t - vertices.append((x + dx_r, y + dy_r)) - return vertices - - # Helper: For an item with placement (x,y,theta) and dimensions (L,W), - # compute its axis-aligned bounding box. - # Since allowed rotations are only 0 or 90 degrees (if rotation is allowed), - # the rectangle remains axis-aligned. - def compute_aabb(x, y, L, W, theta_deg): - # Enforce only 0 or 90: if theta is nearly 90, swap dimensions. - if abs(theta_deg) < angle_tol: - half_L, half_W = L / 2, W / 2 - elif abs(theta_deg - 90) < angle_tol: - half_L, half_W = W / 2, L / 2 - else: - # Should not happen; safeguard. - raise ValueError("Invalid rotation angle. Allowed angles are 0 or 90 degrees.") - return (x - half_L, x + half_L, y - half_W, y + half_W) - - total_area = 0.0 - placed_items = [] # List of dicts: { 'aabb': (xmin,xmax,ymin,ymax), 'vertices': [...] } - - # Process each item - for i in range(n): - # Check placement tuple structure - try: - placement = placements[i] - if not (isinstance(placement, (list, tuple)) and len(placement) == 3): - raise ValueError(f"Placement for item {i} must be a tuple/list of three numbers.") - x, y, theta = float(placement[0]), float(placement[1]), float(placement[2]) - except Exception as e: - raise ValueError(f"Invalid placement for item {i}: {e}") - - L, W = items[i] - # For squares, check that L == W (within tolerance) - if shape == "square" and abs(L - W) > tol: - raise ValueError(f"Item {i} is marked as square but dimensions differ: L={L}, W={W}") - - # Determine if the item is packed. - # Convention: If x == -1 and y == -1, item is not placed. - if abs(x + 1) < tol and abs(y + 1) < tol: - # Unpacked item: skip (area = 0). Optionally, enforce theta = 0. - if abs(theta) > angle_tol: - raise ValueError(f"Unpacked item {i} must have theta equal to 0.") - continue - - # Packed item: check rotation feasibility. - if not rotation_allowed: - if abs(theta) > angle_tol: - raise ValueError(f"Rotation is not allowed, but item {i} has theta = {theta}.") - else: - # If rotation is allowed, then theta must be 0 or 90. - if not (abs(theta) < angle_tol or abs(theta - 90) < angle_tol): - raise ValueError( - f"Item {i} has invalid rotation angle {theta}. Allowed values are 0 or 90 degrees.") - - # Compute the vertices for the placed rectangle. - vertices = compute_vertices(x, y, L, W, theta) - # Check each vertex lies inside the container circle. - for vx, vy in vertices: - # Euclidean distance from container center (cx, cy) - if (vx - cx) ** 2 + (vy - cy) ** 2 > R ** 2 + tol: - raise ValueError(f"Item {i} has a vertex at ({vx:.4f},{vy:.4f}) outside the container.") - - # Compute axis-aligned bounding box (since rectangle is axis-aligned if theta in {0,90}) - xmin, xmax, ymin, ymax = compute_aabb(x, y, L, W, theta) - - # Save the item details for later overlap checking. - placed_items.append({ - 'index': i, - 'aabb': (xmin, xmax, ymin, ymax), - 'vertices': vertices, - 'area': L * W - }) - total_area += L * W - - # Check for pairwise overlap among all placed items. - num_placed = len(placed_items) - for i in range(num_placed): - aabb_i = placed_items[i]['aabb'] - xmin_i, xmax_i, ymin_i, ymax_i = aabb_i - for j in range(i + 1, num_placed): - aabb_j = placed_items[j]['aabb'] - xmin_j, xmax_j, ymin_j, ymax_j = aabb_j - # Compute overlap in x and y - overlap_x = max(0.0, min(xmax_i, xmax_j) - max(xmin_i, xmin_j)) - overlap_y = max(0.0, min(ymax_i, ymax_j) - max(ymin_i, ymin_j)) - if overlap_x * overlap_y > tol: - raise ValueError(f"Items {placed_items[i]['index']} and {placed_items[j]['index']} overlap.") - - return total_area - - def norm_score(self, results): - optimal_scores = { - "rect1.txt": [37.6878, 37.9687], - "rect2.txt": [84.4446, 84.7008], - "rect3.txt": [103.4802, 110.3253], - "square1.txt": [51.7583], - "square2.txt": [109.8363], - "square3.txt": [103.0963], - } - - normed = {} - for case, (scores, error_message) in results.items(): - if case not in optimal_scores: - continue # Skip if there's no optimal score defined. - optimal_list = optimal_scores[case] - normed_scores = [] - # Compute normalized score for each index. - for idx, score in enumerate(scores): - if isinstance(score, (int, float)): - normed_scores.append(score / optimal_list[idx]) - else: - normed_scores.append(score) - normed[case] = (normed_scores, error_message) - return normed - - - - - - - -# Task configuration for benchmark task -ENTRY_NAME = 'solve' -FUNCTION_SIGNATURE = 'def solve(...):' -IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' -TASK_DESCRIPTION = '("We consider the problem of selecting and placing a subset of n unequal rectangles (or squares) "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("We consider the problem of selecting and placing a subset of n unequal rectangles (or squares) "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n: int, cx: float, cy: float, R: float, items: list, shape: str, rotation: bool) -> dict:\n """\n Solves the problem of packing a subset of unequal rectangles and squares into a fixed‐size circular container\n with the objective of maximizing the total area of the items placed inside the container.\n Input kwargs:\n - n : int, the number of items (rectangles or squares)\n - cx, cy : floats, the coordinates of the container center\n - R : float, the radius of the container\n - items : list of tuples, where each tuple (L, W) gives the dimensions of an item\n (for a square, L == W)\n - shape : string, either "rectangle" or "square"\n - rotation : bool, whether 90° rotation is allowed (True or False)\n Objective:\n - Select and place a subset of the given items so that each packed item lies completely inside the circular container,\n no two packed items overlap, and the sum of the areas of the packed items is maximized.\n - An item that is not packed contributes zero area.\n Returns:\n A dictionary with the key \'placements\' containing a list of exactly n tuples.\n Each tuple is (x-coordinate, y-coordinate, theta) where:\n - (x-coordinate, y-coordinate) is the center position of the item (if packed),\n - theta is the rotation angle in degrees (counter-clockwise from the horizontal). 90 or 0.\n - For an unpacked item, x and y should be set to -1 and theta to 0 (or another default value).\n Note: This is a placeholder. The actual solution logic is not implemented here.\n """\n ## placeholder.\n return {\'placements\': []}' -EVAL_CLASS_NAME = 'PURSAEvaluationCB' -EVAL_KWARGS = {'timeout_seconds': 60} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_packing_unequal_rectangles_and_squares_area/paras.yaml b/examples/benchmark_tasks/optimization_packing_unequal_rectangles_and_squares_area/paras.yaml deleted file mode 100644 index 17db25b2..00000000 --- a/examples/benchmark_tasks/optimization_packing_unequal_rectangles_and_squares_area/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: PURSAEvaluationCB -timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_pymoo_moead/__init__.py b/examples/benchmark_tasks/optimization_pymoo_moead/__init__.py deleted file mode 100644 index 1f2b2346..00000000 --- a/examples/benchmark_tasks/optimization_pymoo_moead/__init__.py +++ /dev/null @@ -1,216 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_pymoo_moead -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# Module Name: MOEAD_PYMOO_Evaluation -# Last Revision: 2025/07/14 -# Description: Evaluates the Multi-objective problem using the MOEAD algorithm. -# Problem instances are generated by the GetData class. -# -# Parameters: -# - timeout_seconds: Maximum allowed time (in seconds) for the evaluation process: int (default: 20). -# - n_var: The number of decision variables for the optimization problem: int (default: 10). -# - n_obj: The number of objectives for the optimization problem: int (default: 3). -# - n_partitions: The number of partitions used to generate reference directions: int (default: 12). -# - pop_size: The size of the population in the evolutionary algorithm: int (default: 100). -# - n_gen: The number of generations for the algorithm to run: int (default: 100). -# -# References: -# - Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -import os, sys -sys.path.insert(0, os.path.dirname(__file__)) -import copy -from typing import Callable, Any -import numpy as np - -from pymoo.algorithms.moo.moead import MOEAD -from pymoo.indicators.hv import HV -from pymoo.optimize import minimize -from pymoo.termination import get_termination -from pymoo.util.ref_dirs import get_reference_directions -from pymoo.decomposition.tchebicheff import Tchebicheff - -from llm4ad_loader import Evaluation -# Assuming the new GetData class is located at the following path -from get_instance import GetData -# from llm4ad.task.optimization.pymoo_moead.get_instance import GetData # Converted from LLM4AD import -# from llm4ad.task.optimization.pymoo_moead.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\n\ndef custom_decomposition(F: np.ndarray,\n weights: np.ndarray,\n ideal_point: np.ndarray,\n **kwargs) -> np.ndarray:\n """Design a novel decomposition method for MOEA/D.\n\n Args:\n F (np.ndarray): A set of objective vectors for the population.\n Shape: (n_solutions, n_objectives)\n weights (np.ndarray): The weight vectors for the subproblems.\n Shape: (n_solutions, n_objectives)\n ideal_point (np.ndarray): The ideal point found so far.\n Shape: (n_objectives,)\n\n Returns:\n np.ndarray: The aggregated scalar value for each solution.\n Shape: (n_solutions,)\n """\n # Default implementation: Tchebycheff decomposition.\n # Replace this with your novel algorithm.\n v = np.abs(F - ideal_point) * weights\n return np.max(v, axis=1)' -task_description = '"' - - -class MOEAD_PYMOO_Evaluation(Evaluation): - def __init__(self, - timeout_seconds=100, - n_var=10, - n_obj=3, - n_partitions=12, - pop_size=100, - n_gen=100, - seed=None, - **kwargs): - """ - Parameter Description: - This evaluator now receives a decomposition function via the evaluate_program interface. - """ - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - # Following the CVRP pattern, use the GetData class to generate problem instances - getData = GetData(n_var=n_var, n_obj=n_obj) - self.problem = getData.get_problem_instance() - - self.ref_dirs = get_reference_directions("das-dennis", self.problem.n_obj, n_partitions=n_partitions) - self.pop_size = pop_size if pop_size else len(self.ref_dirs) - self.n_gen = n_gen - self.seed = seed - self.hv_ref = np.array([1.1] * self.problem.n_obj) - self.hv_calculator = HV(ref_point=self.hv_ref) - self.last_result = None - - - def evaluate(self, decomposition_func: Callable = None) -> float: - """ - Core evaluation method. Returns the evaluation score and stores detailed results in self.last_result. - """ - class DecompAdapter: - def __init__(self, func): - self.func = func - def do(self, F, weights, ideal_point, **kwargs): - return self.func(F, weights=weights, ideal_point=ideal_point, **kwargs) - - decomposition = DecompAdapter(decomposition_func) if decomposition_func else Tchebicheff() - - algorithm = MOEAD( - ref_dirs=self.ref_dirs, - n_neighbors=15, - prob_neighbor_mating=0.7, - decomposition=decomposition, - seed=self.seed - ) - - termination = get_termination("n_gen", self.n_gen) - res = minimize(self.problem, algorithm, termination, seed=self.seed, verbose=False) - - hv_value = self.hv_calculator(res.opt.get("F")) - self.last_result = {"hv": hv_value, "pareto_front": res.opt} - return -hv_value - - - def evaluate_program(self, program_str: str, callable_func: callable) -> Any: - return self.evaluate(decomposition_func=callable_func) - - def plot_solutions(self, solutions): - import matplotlib.pyplot as plt - F = solutions.get("F") - fig = plt.figure(figsize=(10, 8)) - ax = fig.add_subplot(111, projection='3d') - ax.scatter(F[:, 0], F[:, 1], F[:, 2], c='blue', s=30, alpha=0.5) - ax.set_xlabel('Objective 1'); ax.set_ylabel('Objective 2'); ax.set_zlabel('Objective 3') - ax.set_title(f'MOEAD on {self.problem.__class__.__name__} (HV = {self.hv_calculator(F):.4f})') - plt.tight_layout(); plt.show() - -if __name__ == "__main__": - def custom_decomposition_tchebycheff(F: np.ndarray, weights: np.ndarray, ideal_point: np.ndarray, **kwargs) -> np.ndarray: - v = np.abs(F - ideal_point) * weights - return np.max(v, axis=1) - - evaluator = MOEAD_PYMOO_Evaluation(n_gen=100, seed=1) - score = evaluator.evaluate_program("", custom_decomposition_tchebycheff) - results = evaluator.last_result - - print(f"Evaluation Score (Negative HV): {score:.5f}") - print(f"Hypervolume (HV): {results['hv']:.4f}") - - if evaluator.problem.n_obj == 3 and results: - evaluator.plot_solutions(results["pareto_front"]) - -# Task configuration for benchmark task -ENTRY_NAME = 'custom_decomposition' -FUNCTION_SIGNATURE = 'def custom_decomposition(...):' -IMPORT_HEADER = 'import numpy as np\nimport math' -TASK_DESCRIPTION = '"' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `custom_decomposition` for the LLM4AD task.\\n\\nTask description:\\n"\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\n\ndef custom_decomposition(F: np.ndarray,\n weights: np.ndarray,\n ideal_point: np.ndarray,\n **kwargs) -> np.ndarray:\n """Design a novel decomposition method for MOEA/D.\n\n Args:\n F (np.ndarray): A set of objective vectors for the population.\n Shape: (n_solutions, n_objectives)\n weights (np.ndarray): The weight vectors for the subproblems.\n Shape: (n_solutions, n_objectives)\n ideal_point (np.ndarray): The ideal point found so far.\n Shape: (n_objectives,)\n\n Returns:\n np.ndarray: The aggregated scalar value for each solution.\n Shape: (n_solutions,)\n """\n # Default implementation: Tchebycheff decomposition.\n # Replace this with your novel algorithm.\n v = np.abs(F - ideal_point) * weights\n return np.max(v, axis=1)' -EVAL_CLASS_NAME = 'MOEAD_PYMOO_Evaluation' -EVAL_KWARGS = {'timeout_seconds': 100} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_pymoo_moead/get_instance.py b/examples/benchmark_tasks/optimization_pymoo_moead/get_instance.py deleted file mode 100644 index a7b85e0e..00000000 --- a/examples/benchmark_tasks/optimization_pymoo_moead/get_instance.py +++ /dev/null @@ -1,87 +0,0 @@ -# Module Name: get_instance -# Last Revision: 2025/07/14 -# Description: Generates DTLZ4 problem instances for MOEAD evaluation. -# This module is part of the LLM4AD project (https://github.com/Optima-CityU/llm4ad). -import pickle - -import numpy as np - -from pymoo.problems import get_problem - -# class GetData: -# def __init__(self, n_var, n_obj): -# """ -# Initialize parameters for the WFG problem. -# Args: -# n_var (int): The number of decision variables. -# n_obj (int): The number of objectives. -# """ -# # For WFG problems, ensure the number of decision variables is sufficient -# k = 2 * (n_obj - 1) -# if n_var < k + 1: -# raise ValueError(f"For WFG1 with {n_obj} objectives, n_var must be at least {k + 1}") -# -# self.n_var = n_var -# self.n_obj = n_obj -# -# def get_problem_instance(self): -# """ -# Generate and return a WFG1 problem instance using the pymoo library. -# This is a more complex benchmark problem than DTLZ. -# """ -# # WFG problems typically require a position parameter k; a standard configuration is used here. -# k = 2 * (self.n_obj - 1) -# return get_problem("wfg1", n_var=self.n_var, n_obj=self.n_obj, k=k) - -class GetData: - def __init__(self, n_var, n_obj): - """ - Initialize parameters for the DTLZ problem. - Args: - n_var (int): The number of decision variables. - n_obj (int): The number of objectives. - """ - self.n_var = n_var - self.n_obj = n_obj - - def get_problem_instance(self): - """ - Generate and return a DTLZ4 problem instance using the pymoo library. - """ - return get_problem("DTLZ4", n_var=self.n_var, n_obj=self.n_obj) - - -if __name__ == '__main__': - # Demonstrate the use of the GetData class - print("--- Demonstrating GetData Class ---") - gd = GetData(n_var=10, n_obj=3) - dtlz4_problem = gd.get_problem_instance() - print("Successfully created a DTLZ4 problem instance:") - print(dtlz4_problem) - print("\n") - - # Provide a code template for a Large Language Model (LLM) to implement a custom decomposition function - prompt_code_temp = '''import numpy as np - -def custom_decomposition(F: np.ndarray, - weights: np.ndarray, - ideal_point: np.ndarray, - **kwargs) -> np.ndarray: - """Design a novel decomposition method for MOEA/D. - - Args: - F (np.ndarray): A set of objective vectors for the population. - weights (np.ndarray): The weight vectors for the subproblems. - ideal_point (np.ndarray): The ideal point found so far. - - Returns: - np.ndarray: The aggregated scalar value for each solution. - """ - # Example: Tchebycheff decomposition - # This is a placeholder and should be replaced by a novel algorithm. - v = np.abs(F - ideal_point) * weights - return np.max(v, axis=1) -''' - - print("--- Template for LLM-designed Decomposition Function ---") - print(prompt_code_temp) diff --git a/examples/benchmark_tasks/optimization_pymoo_moead/paras.yaml b/examples/benchmark_tasks/optimization_pymoo_moead/paras.yaml deleted file mode 100644 index ea215272..00000000 --- a/examples/benchmark_tasks/optimization_pymoo_moead/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: MOEAD_PYMOO_Evaluation -timeout_seconds: 100 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_qap_construct/__init__.py b/examples/benchmark_tasks/optimization_qap_construct/__init__.py deleted file mode 100644 index f4005a73..00000000 --- a/examples/benchmark_tasks/optimization_qap_construct/__init__.py +++ /dev/null @@ -1,293 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_qap_construct -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# Module Name: QAPEvaluation -# Last Revision: 2025/2/16 -# Description: Evaluates the Quadratic Assignment Problem (QAP). -# The QAP involves assigning a set of facilities to a set of locations in such a way that the total cost of interactions between facilities is minimized. -# This module is part of the LLM4AD project (https://github.com/Optima-CityU/llm4ad). -# -# Parameters: -# - timeout_seconds: Maximum allowed time (in seconds) for the evaluation process: int (default: 20). -# - n_facilities: Number of facilities to assign: int (default: 50). -# - n_instance: Number of problem instances to generate: int (default: 10). -# -# References: -# - Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations -import numpy as np -from typing import Callable, Any, List, Tuple -import matplotlib.pyplot as plt - -from llm4ad_loader import Evaluation -import os, sys -sys.path.insert(0, os.path.dirname(__file__)) -from get_instance import GetData -# from llm4ad.task.optimization.qap_construct.get_instance import GetData # Converted from LLM4AD import -# from llm4ad.task.optimization.qap_construct.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\n\ndef select_next_assignment(current_assignment: List[int], flow_matrix: np.ndarray, distance_matrix: np.ndarray) -> List[int]:\n """\n A heuristic for the Quadratic Assignment Problem.\n\n Args:\n current_assignment: Current assignment of facilities to locations (-1 means unassigned).\n flow_matrix: Flow matrix between facilities.\n distance_matrix: Distance matrix between locations.\n\n Returns:\n Updated assignment of facilities to locations.\n """\n n_facilities = len(current_assignment)\n \n # Find the first unassigned facility and the first available location\n for facility in range(n_facilities):\n if current_assignment[facility] == -1:\n # Find the first available location\n for location in range(n_facilities):\n if location not in current_assignment:\n current_assignment[facility] = location\n break\n break\n \n return current_assignment' -task_description = "'" - - -__all__ = ['QAPEvaluation'] - - -class QAPEvaluation(Evaluation): - """Evaluator for the Quadratic Assignment Problem.""" - - def __init__(self, - timeout_seconds=20, - n_facilities=50, - n_instance=16, - **kwargs): - """ - Initializes the QAP evaluator. - """ - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - self.n_instance = n_instance - self.n_facilities = n_facilities - self.data_generator = GetData(self.n_instance, self.n_facilities) - self._datasets = self.data_generator.generate_instances() - - def evaluate_program(self, program_str: str, callable_func: Callable) -> Any | None: - """ - Evaluates the program (constructive heuristic) for the QAP. - - Args: - program_str: Program string (not used here, but kept for compatibility). - callable_func: The constructive heuristic function to evaluate. - - Returns: - The average total cost across all instances. - """ - return self.evaluate_qap(callable_func) - - def plot_solution(self, flow_matrix: np.ndarray, distance_matrix: np.ndarray, assignment: List[int]): - """ - Plot the solution of the Quadratic Assignment Problem. - - Args: - flow_matrix: Flow matrix between facilities. - distance_matrix: Distance matrix between locations. - assignment: Assignment of facilities to locations. - """ - n_facilities = len(assignment) - - # Generate random coordinates for locations (for visualization purposes) - np.random.seed(42) # For reproducibility - locations = np.random.rand(n_facilities, 2) * 10 # Scale coordinates for better visualization - - # Plot locations - plt.figure(figsize=(8, 6)) - for loc_id, (x, y) in enumerate(locations): - plt.scatter(x, y, color='blue', s=200, label='Locations' if loc_id == 0 else "", zorder=1) - plt.text(x, y, f'L{loc_id + 1}', fontsize=12, ha='right', va='bottom', zorder=1) - - # Plot facilities and connections based on flow - for facility_id, loc_id in enumerate(assignment): - x, y = locations[loc_id] - plt.scatter(x, y, color='red', s=100, marker='s', label='Facilities' if facility_id == 0 else "", zorder=2) - plt.text(x, y, f'F{facility_id + 1}', fontsize=12, ha='left', va='top', zorder=2) - - # Draw lines between facilities based on flow - for i in range(n_facilities): - for j in range(i + 1, n_facilities): - if flow_matrix[i, j] > 0: - loc_i = assignment[i] - loc_j = assignment[j] - plt.plot( - [locations[loc_i, 0], locations[loc_j, 0]], - [locations[loc_i, 1], locations[loc_j, 1]], - color='gray', linewidth=flow_matrix[i, j] / 10, alpha=0.5, zorder=0 - ) - - plt.title('QAP Solution: Facilities Assigned to Locations') - plt.xlabel('X Coordinate') - plt.ylabel('Y Coordinate') - plt.legend() - plt.grid(True) - plt.show() - - def qap_evaluate(self, current_assignment: List[int], flow_matrix: np.ndarray, distance_matrix: np.ndarray, eva: Callable) -> List[int]: - """ - Evaluate the next assignment for the Quadratic Assignment Problem using a constructive heuristic. - - Args: - current_assignment: Current assignment of facilities to locations. - flow_matrix: Flow matrix between facilities. - distance_matrix: Distance matrix between locations. - eva: The constructive heuristic function to select the next assignment. - - Returns: - Updated assignment of facilities to locations. - """ - # Use the heuristic to select the next assignment - - n_facilities = flow_matrix.shape[0] - for _ in range(n_facilities): - next_assignment = eva(current_assignment, flow_matrix, distance_matrix) - - return next_assignment - - def evaluate_qap(self, eva: Callable) -> float: - """ - Evaluate the constructive heuristic for the Quadratic Assignment Problem. - - Args: - instance_data: List of tuples containing the flow and distance matrices. - n_ins: Number of instances to evaluate. - eva: The constructive heuristic function to evaluate. - - Returns: - The average total cost across all instances. - """ - total_cost = 0 - - for instance in self._datasets[:self.n_instance]: - flow_matrix, distance_matrix = instance - n_facilities = flow_matrix.shape[0] - current_assignment = [-1] * n_facilities # Initialize with no assignments - current_assignment = self.qap_evaluate(current_assignment, flow_matrix, distance_matrix, eva) - - # Check if current_assignment is a feasible solution - if -1 in current_assignment: - raise ValueError("Feasibility check failed: Not all facilities are allocated.") - if any(not (0 <= x < n_facilities) for x in current_assignment): - raise ValueError("Feasibility check failed: Assignment values are out of range.") - if len(set(current_assignment)) != n_facilities: - raise ValueError("Feasibility check failed: Duplicate assignment values found.") - - # Calculate the total cost of the assignment - cost = 0 - for i in range(n_facilities): - for j in range(n_facilities): - cost += flow_matrix[i, j] * distance_matrix[current_assignment[i], current_assignment[j]] - total_cost += cost - - average_cost = total_cost / self.n_instance - return -average_cost # We want to minimize the total cost - - -if __name__ == '__main__': - - def select_next_assignment(current_assignment: List[int], flow_matrix: np.ndarray, distance_matrix: np.ndarray) -> List[int]: - """ - A greedy heuristic for the Quadratic Assignment Problem. - - Args: - current_assignment: Current assignment of facilities to locations (-1 means unassigned). - flow_matrix: Flow matrix between facilities. - distance_matrix: Distance matrix between locations. - - Returns: - Updated assignment of facilities to locations. - """ - n_facilities = len(current_assignment) - - # Find the first unassigned facility and the first available location - for facility in range(n_facilities): - if current_assignment[facility] == -1: - # Find the first available location - for location in range(n_facilities): - if location not in current_assignment: - current_assignment[facility] = location - break - break - - return current_assignment - - - bp1d = QAPEvaluation() - ave_bins = bp1d.evaluate_program('_', select_next_assignment) - print(ave_bins) - -# Task configuration for benchmark task -ENTRY_NAME = 'select_next_assignment' -FUNCTION_SIGNATURE = 'def select_next_assignment(...):' -IMPORT_HEADER = 'import numpy as np\nimport math' -TASK_DESCRIPTION = "'" -OBJECTIVE_TEXT = "You are optimizing the implementation of `select_next_assignment` for the LLM4AD task.\\n\\nTask description:\\n'\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible." -TEMPLATE_FUNCTION = 'import numpy as np\n\ndef select_next_assignment(current_assignment: List[int], flow_matrix: np.ndarray, distance_matrix: np.ndarray) -> List[int]:\n """\n A heuristic for the Quadratic Assignment Problem.\n\n Args:\n current_assignment: Current assignment of facilities to locations (-1 means unassigned).\n flow_matrix: Flow matrix between facilities.\n distance_matrix: Distance matrix between locations.\n\n Returns:\n Updated assignment of facilities to locations.\n """\n n_facilities = len(current_assignment)\n \n # Find the first unassigned facility and the first available location\n for facility in range(n_facilities):\n if current_assignment[facility] == -1:\n # Find the first available location\n for location in range(n_facilities):\n if location not in current_assignment:\n current_assignment[facility] = location\n break\n break\n \n return current_assignment' -EVAL_CLASS_NAME = 'QAPEvaluation' -EVAL_KWARGS = {'timeout_seconds': 30} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_qap_construct/get_instance.py b/examples/benchmark_tasks/optimization_qap_construct/get_instance.py deleted file mode 100644 index a972efb4..00000000 --- a/examples/benchmark_tasks/optimization_qap_construct/get_instance.py +++ /dev/null @@ -1,48 +0,0 @@ -import numpy as np - -class GetData: - def __init__(self, n_instance: int, n_facilities: int): - """ - Initialize the QAPDataGenerator class for the Quadratic Assignment Problem. - - Args: - n_instance: Number of instances to generate. - n_facilities: Number of facilities (and locations). - """ - self.n_instance = n_instance - self.n_facilities = n_facilities - - def generate_instances(self): - """ - Generate instances for the Quadratic Assignment Problem. - - Returns: - A list of tuples, where each tuple contains: - - flow_matrix: A 2D numpy array representing the flow between facilities. - - distance_matrix: A 2D numpy array representing the distance between locations. - """ - np.random.seed(2024) # Set seed for reproducibility - instance_data = [] - - for _ in range(self.n_instance): - # Generate random flow and distance matrices - flow_matrix = np.random.randint(1, 101, size=(self.n_facilities, self.n_facilities)) - distance_matrix = np.random.randint(1, 101, size=(self.n_facilities, self.n_facilities)) - - # Ensure the matrices are symmetric and have zero diagonals - flow_matrix = (flow_matrix + flow_matrix.T) // 2 - np.fill_diagonal(flow_matrix, 0) - - distance_matrix = (distance_matrix + distance_matrix.T) // 2 - np.fill_diagonal(distance_matrix, 0) - - instance_data.append((flow_matrix, distance_matrix)) - - return instance_data - -# Example usage: -# generator = QAPDataGenerator(n_instance=5, n_facilities=4) -# instances = generator.generate_instances() -# for flow, distance in instances: -# print("Flow Matrix:\n", flow) -# print("Distance Matrix:\n", distance) diff --git a/examples/benchmark_tasks/optimization_qap_construct/paras.yaml b/examples/benchmark_tasks/optimization_qap_construct/paras.yaml deleted file mode 100644 index 8962e72e..00000000 --- a/examples/benchmark_tasks/optimization_qap_construct/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: QAPEvaluation -timeout_seconds: 30 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_resource_constrained_shortest_path/__init__.py b/examples/benchmark_tasks/optimization_resource_constrained_shortest_path/__init__.py deleted file mode 100644 index cf799657..00000000 --- a/examples/benchmark_tasks/optimization_resource_constrained_shortest_path/__init__.py +++ /dev/null @@ -1,353 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_resource_constrained_shortest_path -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# References: -# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language -# model agents in algorithm search for combinatorial optimization. -# arXiv preprint arXiv:2504.04310 (2025). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -from typing import Any -import numpy as np -from llm4ad_loader import Evaluation -from llm4ad_loader import load_subdir_as_text -# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader -# from llm4ad.task.optimization.co_bench.resource_constrained_shortest_path_co_bench.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n:int, m:int, K:int, lower_bounds:list, upper_bounds:list, vertex_resources:list, graph:dict) -> dict:\n """\n Solve the Resource Constrained Shortest Path problem.\n Input kwargs should include:\n - n (int): number of vertices,\n - m (int): number of arcs,\n - K (int): number of resources,\n - lower_bounds (list of float): list of lower limits for each resource,\n - upper_bounds (list of float): list of upper limits for each resource,\n - vertex_resources (list of list of float): list (of length n) of lists (of length K) with the resource consumption at each vertex,\n - graph (dict): dictionary mapping each vertex (1-indexed) to a list of arcs, where each arc is a tuple\n (end_vertex (int), cost (float), [arc resource consumptions] (list of float)).\n Evaluation Metric:\n If the computed path is valid (i.e. it starts at vertex 1, ends at vertex n, every transition is\n defined in the graph, and the total resource consumption from both vertices and arcs is within the\n specified bounds for each resource), then the score equals the total arc cost along the path.\n Otherwise, the solution is invalid and receives no score.\n Returns:\n A dictionary with keys:\n "total_cost": total cost (a float) of the computed path,\n "path": a list of vertex indices (integers) defining the path.\n (Placeholder implementation)\n """\n # Placeholder implementation.\n n = kwargs.get("n", 1)\n # Return a trivial solution: just go directly from vertex 1 to vertex n.\n return {"total_cost": 0.0, "path": [1, n]}' -task_description = '("This problem involves finding the shortest path from vertex 1 to vertex n in a directed graph "' - - -__all__ = ['RCSPEvaluationCB'] - - -class RCSPEvaluationCB(Evaluation): - - def __init__(self, - timeout_seconds=50, - **kwargs): - - """ - Args: - None - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - # Load datasets from Hugging Face - dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Resource constrained shortest path") - self._datasets = {} - for filename in dataset: - # Join all text rows into a single string - text_content = '\n'.join([row['text'] for row in dataset[filename]]) - self._datasets[filename] = text_content - - def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: - return self.evaluate(callable_func) - - def evaluate(self, eva: callable) -> float | None: - ins_cases = [] - for case_id, ins in enumerate(self._datasets.values()): - ins_cases.append(self.load_data(ins)) - - fitness_list = [] - try: - for i in ins_cases: - for j in i: - result = eva(j['n'], j['m'], j['K'], j['lower_bounds'], j['upper_bounds'], j['vertex_resources'], j['graph']) - fitness = self.eval_func(j['n'], j['m'], j['K'], j['lower_bounds'], j['upper_bounds'], j['vertex_resources'], j['graph'], result['total_cost'], result['path']) - fitness_list.append(fitness) - - return -np.mean(fitness_list) - - except ValueError as e: - print(e) - return None - - def load_data(self, input_string): - """ - Load one or more cases from a TXT input file for the Resource Constrained Shortest Path problem. - The input file format (per case) is as follows: - 1. Three numbers: n (number of vertices), m (number of arcs), K (number of resources) - 2. For each resource (k = 1,...,K): the lower limit on the resource consumed on the chosen path. - 3. For each resource (k = 1,...,K): the upper limit on the resource consumed on the chosen path. - 4. For each vertex (i = 1,...,n): K numbers indicating the resource consumption incurred at that vertex. - 5. For each arc (j = 1,...,m): (3 + K) numbers: - - starting vertex, - - ending vertex, - - cost of the arc, - - K numbers indicating the resource consumption incurred on the arc. - Note: - In many of the RCSP test files, the file is a stream of numbers separated by whitespace rather than fixed lines. - This implementation reads the entire file and splits it into tokens. - Returns: - A list of cases. Each case is a dictionary with keys: - "n", "m", "K", "lower_bounds", "upper_bounds", "vertex_resources", "graph" - """ - tokens = input_string.split() - - cases = [] - pos = 0 - total_tokens = len(tokens) - - while pos < total_tokens: - if pos + 3 > total_tokens: - break # Not enough tokens for a new case header. - try: - n = int(tokens[pos]) - m = int(tokens[pos + 1]) - K = int(tokens[pos + 2]) - except Exception as e: - raise ValueError("Error reading header (n, m, K)") from e - pos += 3 - - if pos + K > total_tokens: - raise ValueError("Not enough tokens for lower bounds.") - lower_bounds = [float(tokens[pos + i]) for i in range(K)] - pos += K - - if pos + K > total_tokens: - raise ValueError("Not enough tokens for upper bounds.") - upper_bounds = [float(tokens[pos + i]) for i in range(K)] - pos += K - - if pos + n * K > total_tokens: - raise ValueError("Not enough tokens for vertex resource consumption.") - vertex_resources = [] - for i in range(n): - vertex_resources.append([float(tokens[pos + j]) for j in range(K)]) - pos += K - - if pos + m * (3 + K) > total_tokens: - raise ValueError("Not enough tokens for arc information.") - graph = {i: [] for i in range(1, n + 1)} - for j in range(m): - try: - u = int(tokens[pos]) - v = int(tokens[pos + 1]) - cost = float(tokens[pos + 2]) - arc_resources = [float(tokens[pos + 3 + i]) for i in range(K)] - except Exception as e: - raise ValueError("Error reading arc information.") from e - pos += 3 + K - graph[u].append((v, cost, arc_resources)) - - case = { - "n": n, - "m": m, - "K": K, - "lower_bounds": lower_bounds, - "upper_bounds": upper_bounds, - "vertex_resources": vertex_resources, - "graph": graph - } - cases.append(case) - - return cases - - def eval_func(self, n, m, K, lower_bounds, upper_bounds, vertex_resources, graph, total_cost, path): - """ - Evaluate the solution for one case of the Resource Constrained Shortest Path problem. - Parameters: - n, m, K : Input parameters defining the problem instance. - lower_bounds : List of lower resource bounds (length K). - upper_bounds : List of upper resource bounds (length K). - vertex_resources : List (length n) of lists (each of length K) with resource consumption per vertex. - graph : Dictionary mapping each vertex (1-indexed) to its outgoing arcs. - Each arc is a tuple (end_vertex, cost, [arc resource consumptions]). - total_cost : The total cost value reported by the solution (not used in validation). - path : List of vertex indices (integers) defining the computed path. - Returns: - The total arc cost along the path if the solution is valid. - Raises: - ValueError: If the solution is invalid (i.e. the path does not start at vertex 1, does not end at vertex n, - contains an undefined arc, or the cumulative resource consumption (from both vertices and arcs) - is not within the specified bounds for each resource). - """ - - # Check basic validity of the path. - if not path or path[0] != 1 or path[-1] != n: - raise ValueError("Invalid solution: path must start at vertex 1 and end at vertex n.") - - computed_cost = 0.0 - total_resources = [0.0] * K - - # Add resource consumption from vertices. - for vertex in path: - if vertex < 1 or vertex > n: - raise ValueError(f"Invalid solution: vertex {vertex} is out of valid range 1 to {n}.") - for k in range(K): - total_resources[k] += vertex_resources[vertex - 1][k] - - # For each consecutive pair in the path, check for a valid arc and add its cost and resource consumption. - for i in range(len(path) - 1): - u = path[i] - v = path[i + 1] - valid_arc = False - for (dest, arc_cost, arc_res) in graph.get(u, []): - if dest == v: - valid_arc = True - computed_cost += arc_cost - for k in range(K): - total_resources[k] += arc_res[k] - break - if not valid_arc: - raise ValueError(f"Invalid solution: no valid arc from vertex {u} to vertex {v}.") - - # Verify resource constraints. - for k in range(K): - if total_resources[k] < lower_bounds[k] - 1e-6 or total_resources[k] > upper_bounds[k] + 1e-6: - raise ValueError( - f"Invalid solution: total consumption for resource {k} is {total_resources[k]}, " - f"which is outside the bounds [{lower_bounds[k]}, {upper_bounds[k]}]." - ) - - return computed_cost - - def norm_score(self, results): - optimal_scores = { - "rcsp1.txt": [88.3], - "rcsp2.txt": [131], - "rcsp3.txt": [1.44], - "rcsp4.txt": [2], - "rcsp5.txt": [81.9], - "rcsp6.txt": [91.4], - "rcsp7.txt": [3.91], - "rcsp8.txt": [3.77], - "rcsp9.txt": [420], - "rcsp10.txt": [420], - "rcsp11.txt": [6], - "rcsp12.txt": [6], - "rcsp13.txt": [448], - "rcsp14.txt": [656], - "rcsp15.txt": [6.2], - "rcsp16.txt": [5], - "rcsp17.txt": [487], - "rcsp18.txt": [512], - "rcsp19.txt": [6], - "rcsp20.txt": [6], - "rcsp21.txt": [858], - "rcsp22.txt": [858], - "rcsp23.txt": [3.34], - "rcsp24.txt": [3.74] - } - - normed = {} - for case, (scores, error_message) in results.items(): - if case not in optimal_scores: - continue # Skip if there's no optimal score defined. - optimal_list = optimal_scores[case] - normed_scores = [] - # Compute normalized score for each index. - for idx, score in enumerate(scores): - if isinstance(score, (int, float)): - normed_scores.append(optimal_list[idx] / score) - else: - normed_scores.append(score) - normed[case] = (normed_scores, error_message) - - return normed - - def get_dev(self): - dev = {'rcsp1.txt': [], 'rcsp11.txt': [], 'rcsp13.txt': [], - 'rcsp15.txt': [], 'rcsp17.txt': [], 'rcsp19.txt': [], - 'rcsp21.txt': [], 'rcsp23.txt': [], 'rcsp3.txt': [], - 'rcsp5.txt': [], 'rcsp7.txt': [], 'rcsp9.txt': []} - - return dev - - - - - - - -# Task configuration for benchmark task -ENTRY_NAME = 'solve' -FUNCTION_SIGNATURE = 'def solve(...):' -IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' -TASK_DESCRIPTION = '("This problem involves finding the shortest path from vertex 1 to vertex n in a directed graph "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("This problem involves finding the shortest path from vertex 1 to vertex n in a directed graph "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(n:int, m:int, K:int, lower_bounds:list, upper_bounds:list, vertex_resources:list, graph:dict) -> dict:\n """\n Solve the Resource Constrained Shortest Path problem.\n Input kwargs should include:\n - n (int): number of vertices,\n - m (int): number of arcs,\n - K (int): number of resources,\n - lower_bounds (list of float): list of lower limits for each resource,\n - upper_bounds (list of float): list of upper limits for each resource,\n - vertex_resources (list of list of float): list (of length n) of lists (of length K) with the resource consumption at each vertex,\n - graph (dict): dictionary mapping each vertex (1-indexed) to a list of arcs, where each arc is a tuple\n (end_vertex (int), cost (float), [arc resource consumptions] (list of float)).\n Evaluation Metric:\n If the computed path is valid (i.e. it starts at vertex 1, ends at vertex n, every transition is\n defined in the graph, and the total resource consumption from both vertices and arcs is within the\n specified bounds for each resource), then the score equals the total arc cost along the path.\n Otherwise, the solution is invalid and receives no score.\n Returns:\n A dictionary with keys:\n "total_cost": total cost (a float) of the computed path,\n "path": a list of vertex indices (integers) defining the path.\n (Placeholder implementation)\n """\n # Placeholder implementation.\n n = kwargs.get("n", 1)\n # Return a trivial solution: just go directly from vertex 1 to vertex n.\n return {"total_cost": 0.0, "path": [1, n]}' -EVAL_CLASS_NAME = 'RCSPEvaluationCB' -EVAL_KWARGS = {'timeout_seconds': 60} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_resource_constrained_shortest_path/paras.yaml b/examples/benchmark_tasks/optimization_resource_constrained_shortest_path/paras.yaml deleted file mode 100644 index 1cc93736..00000000 --- a/examples/benchmark_tasks/optimization_resource_constrained_shortest_path/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: RCSPEvaluationCB -timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_set_cover_construct/__init__.py b/examples/benchmark_tasks/optimization_set_cover_construct/__init__.py deleted file mode 100644 index abc12095..00000000 --- a/examples/benchmark_tasks/optimization_set_cover_construct/__init__.py +++ /dev/null @@ -1,296 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_set_cover_construct -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# Module Name: SCPEvaluation -# Last Revision: 2025/2/16 -# Description: Evaluates the Set Covering Problem (SCP). -# The SCP involves selecting a minimum number of subsets from a collection that covers all elements in a universal set. -# This module is part of the LLM4AD project (https://github.com/Optima-CityU/llm4ad). -# -# Parameters: -# - timeout_seconds: Maximum allowed time (in seconds) for the evaluation process: int (default: 30). -# - n_instance: Number of problem instances to generate: int (default: 5). -# - n_elements: Number of elements in the universal set: int (default: 10). -# - n_subsets: Number of subsets in the collection: int (default: 15). -# - max_subset_size: Maximum size of each subset: int (default: 5). -# -# References: -# - Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations -from typing import Any, List, Tuple, Callable -import numpy as np -import matplotlib.pyplot as plt - -from llm4ad_loader import Evaluation -import os, sys -sys.path.insert(0, os.path.dirname(__file__)) -from get_instance import GetData -# from llm4ad.task.optimization.set_cover_construct.get_instance import GetData # Converted from LLM4AD import -# from llm4ad.task.optimization.set_cover_construct.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\ndef select_next_subset(selected_subsets: List[List[int]], remaining_subsets: List[List[int]], remaining_elements: List[int]) -> List[int] | None:\n """\n A heuristic for the Set Covering Problem.\n\n Args:\n selected_subsets: List of already selected subsets.\n remaining_subsets: List of remaining subsets to choose from.\n remaining_elements: List of elements still to be covered.\n\n Returns:\n The next subset to select, or None if no subset can cover any remaining elements.\n """\n max_covered = 0\n best_subset = None\n\n for subset in remaining_subsets:\n # Calculate the number of uncovered elements this subset covers\n covered = len(set(subset).intersection(remaining_elements))\n if covered > max_covered:\n max_covered = covered\n best_subset = subset\n\n return best_subset' -task_description = "'" - - -__all__ = ['SCPEvaluation'] - -import matplotlib.pyplot as plt - - -class SCPEvaluation(Evaluation): - """Evaluator for the Set Covering Problem.""" - - def __init__(self, - timeout_seconds=30, - n_instance: int = 16, - n_elements: int = 50, - n_subsets: int = 50, - max_subset_size: int = 8, - **kwargs): - """ - Args: - n_instance: Number of instances to generate. - n_elements: Number of elements in the universal set. - n_subsets: Number of subsets in the collection. - max_subset_size: Maximum size of each subset. - """ - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - self.n_instance = n_instance - self.n_elements = n_elements - self.n_subsets = n_subsets - self.max_subset_size = max_subset_size - - getData = GetData(self.n_instance, self.n_elements, self.n_subsets, self.max_subset_size) - self._datasets = getData.generate_instances() - - def evaluate_program(self, program_str: str, callable_func: Callable) -> Any | None: - """ - Evaluate a constructive heuristic for the Set Covering Problem. - - Args: - program_str: A string representation of the heuristic (unused here). - callable_func: The constructive heuristic function to evaluate. - - Returns: - The average number of subsets used. - """ - return self.evaluate(callable_func) - - def plot_solution(self, universal_set: List[int], selected_subsets: List[List[int]], all_subsets: List[List[int]]): - """ - Plot the final solution of the Set Covering Problem, including selected and unselected subsets. - - Args: - universal_set: The universal set of elements. - selected_subsets: The list of selected subsets that cover the universal set. - all_subsets: The list of all subsets (including unselected ones). - """ - # Create a mapping of elements to their positions for plotting - element_positions = {element: idx for idx, element in enumerate(universal_set)} - - # Plot the universal set - plt.figure(figsize=(10, 6)) - plt.scatter([element_positions[element] for element in universal_set], [0] * len(universal_set), - color='blue', label='Universal Set', s=100) - - # Plot the selected subsets - for subset_idx, subset in enumerate(selected_subsets): - plt.scatter([element_positions[element] for element in subset], [subset_idx + 1] * len(subset), - label=f'Selected Subset {subset_idx + 1}', s=100, marker='o', edgecolor='black') - - # Plot the unselected subsets - unselected_subsets = [subset for subset in all_subsets if subset not in selected_subsets] - for subset_idx, subset in enumerate(unselected_subsets): - plt.scatter([element_positions[element] for element in subset], [subset_idx + len(selected_subsets) + 1] * len(subset), - label=f'Unselected Subset {subset_idx + 1}', s=100, marker='o', edgecolor='black', facecolor='none') - - # Add annotations and labels - y_labels = ['Universal Set'] + [f'Selected Subset {i + 1}' for i in range(len(selected_subsets))] + \ - [f'Unselected Subset {i + 1}' for i in range(len(unselected_subsets))] - plt.yticks(range(len(y_labels)), y_labels) - plt.xlabel('Elements') - plt.title('Set Covering Problem Solution') - plt.legend(loc='upper right') - plt.grid(True, axis='x') - plt.tight_layout() - plt.show() - - def cover_subsets(self, universal_set: List[int], subsets: List[List[int]], eva: Callable) -> Tuple[int, List[List[int]]]: - """ - Select subsets to cover the universal set using a constructive heuristic. - - Args: - universal_set: The universal set of elements to cover. - subsets: A list of subsets, where each subset is a list of elements. - eva: The constructive heuristic function to select the next subset. - - Returns: - A tuple containing: - - The total number of subsets used. - - A list of selected subsets. - """ - selected_subsets = [] # List to store the selected subsets - remaining_elements = set(universal_set) # Set to track uncovered elements - remaining_subsets = subsets.copy() # Copy of subsets to track remaining subsets - - while remaining_elements: - # Use the heuristic to select the next subset - selected_subset = eva(selected_subsets, remaining_subsets, list(remaining_elements)) - - if selected_subset is None: - break # No more subsets to select - - # Add the selected subset to the list of selected subsets - selected_subsets.append(selected_subset) - # Remove the covered elements from the remaining elements - remaining_elements -= set(selected_subset) - # Remove the selected subset from the remaining subsets - remaining_subsets.remove(selected_subset) - - # Calculate the number of subsets used - used_subsets = len(selected_subsets) - return used_subsets, selected_subsets - - def evaluate(self, eva: Callable) -> float: - """ - Evaluate the constructive heuristic for the Set Covering Problem. - - Args: - instance_data: List of tuples containing the universal set and subsets. - n_ins: Number of instances to evaluate. - eva: The constructive heuristic function to evaluate. - - Returns: - The average number of subsets used across all instances. - """ - total_subsets = 0 - - for instance in self._datasets[:self.n_instance]: - universal_set, subsets = instance - num_subsets, _ = self.cover_subsets(universal_set, subsets, eva) - total_subsets += num_subsets - - average_subsets = total_subsets / self.n_instance - return -average_subsets # Negative because we want to minimize the number of subsets - - -if __name__ == '__main__': - - def select_next_subset(selected_subsets: List[List[int]], remaining_subsets: List[List[int]], remaining_elements: List[int]) -> List[int] | None: - """ - A heuristic for the Set Covering Problem. - - Args: - selected_subsets: List of already selected subsets. - remaining_subsets: List of remaining subsets to choose from. - remaining_elements: List of elements still to be covered. - - Returns: - The next subset to select, or None if no subset can cover any remaining elements. - """ - max_covered = 0 - best_subset = None - - for subset in remaining_subsets: - # Calculate the number of uncovered elements this subset covers - covered = len(set(subset).intersection(remaining_elements)) - if covered > max_covered: - max_covered = covered - best_subset = subset - - return best_subset - - - bp1d = SCPEvaluation() - ave_bins = bp1d.evaluate_program('_', select_next_subset) - print(ave_bins) - -# Task configuration for benchmark task -ENTRY_NAME = 'select_next_subset' -FUNCTION_SIGNATURE = 'def select_next_subset(...):' -IMPORT_HEADER = 'import numpy as np\nimport math' -TASK_DESCRIPTION = "'" -OBJECTIVE_TEXT = "You are optimizing the implementation of `select_next_subset` for the LLM4AD task.\\n\\nTask description:\\n'\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible." -TEMPLATE_FUNCTION = 'import numpy as np\ndef select_next_subset(selected_subsets: List[List[int]], remaining_subsets: List[List[int]], remaining_elements: List[int]) -> List[int] | None:\n """\n A heuristic for the Set Covering Problem.\n\n Args:\n selected_subsets: List of already selected subsets.\n remaining_subsets: List of remaining subsets to choose from.\n remaining_elements: List of elements still to be covered.\n\n Returns:\n The next subset to select, or None if no subset can cover any remaining elements.\n """\n max_covered = 0\n best_subset = None\n\n for subset in remaining_subsets:\n # Calculate the number of uncovered elements this subset covers\n covered = len(set(subset).intersection(remaining_elements))\n if covered > max_covered:\n max_covered = covered\n best_subset = subset\n\n return best_subset' -EVAL_CLASS_NAME = 'SCPEvaluation' -EVAL_KWARGS = {'timeout_seconds': 30} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_set_cover_construct/get_instance.py b/examples/benchmark_tasks/optimization_set_cover_construct/get_instance.py deleted file mode 100644 index 16688bbc..00000000 --- a/examples/benchmark_tasks/optimization_set_cover_construct/get_instance.py +++ /dev/null @@ -1,53 +0,0 @@ -import numpy as np - - -class GetData: - def __init__(self, n_instance: int, n_elements: int, n_subsets: int, max_subset_size: int): - """ - Initialize the GetData class for the Set Covering Problem. - - Args: - n_instance: Number of instances to generate. - n_elements: Number of elements in the universal set. - n_subsets: Number of subsets in the collection. - max_subset_size: Maximum size of each subset. - """ - self.n_instance = n_instance - self.n_elements = n_elements - self.n_subsets = n_subsets - self.max_subset_size = max_subset_size - - def generate_instances(self): - """ - Generate instances for the Set Covering Problem. - - Returns: - A list of tuples, where each tuple contains: - - universal_set: A list of elements in the universal set. - - subsets: A list of subsets, where each subset is a list of elements. - """ - np.random.seed(2024) # Set seed for reproducibility - instance_data = [] - - for _ in range(self.n_instance): - # Define the universal set - universal_set = list(range(1, self.n_elements + 1)) - - # Generate subsets - subsets = [] - for _ in range(self.n_subsets): - subset_size = np.random.randint(1, self.max_subset_size + 1) # Random subset size - subset = np.random.choice(universal_set, size=subset_size, replace=False).tolist() - subsets.append(subset) - - instance_data.append((universal_set, subsets)) - - return instance_data - -# # Example usage: -# data_generator = GetData(n_instance=3, n_elements=10, n_subsets=5, max_subset_size=5) -# instances = data_generator.generate_instances() -# for universal_set, subsets in instances: -# print("Universal Set:", universal_set) -# print("Subsets:", subsets) -# print() diff --git a/examples/benchmark_tasks/optimization_set_cover_construct/paras.yaml b/examples/benchmark_tasks/optimization_set_cover_construct/paras.yaml deleted file mode 100644 index 04688e9f..00000000 --- a/examples/benchmark_tasks/optimization_set_cover_construct/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: SCPEvaluation -timeout_seconds: 30 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_set_cover_construct/test.py b/examples/benchmark_tasks/optimization_set_cover_construct/test.py deleted file mode 100644 index 11f90187..00000000 --- a/examples/benchmark_tasks/optimization_set_cover_construct/test.py +++ /dev/null @@ -1,125 +0,0 @@ -import numpy as np - - -class GetData: - def __init__(self, n_instance: int, n_jobs: int, n_machines: int): - """ - Initialize the GetData class for JSSP. - - Args: - n_instance: Number of instances to generate. - n_jobs: Number of jobs. - n_machines: Number of machines. - """ - self.n_instance = n_instance - self.n_jobs = n_jobs - self.n_machines = n_machines - - def generate_instances(self): - """ - Generate instances for the Job Shop Scheduling Problem. - - Returns: - A list of tuples, where each tuple contains: - - processing_times: A list of lists representing the processing times of each job on each machine. - - n_jobs: Number of jobs. - - n_machines: Number of machines. - """ - np.random.seed(2024) # Set seed for reproducibility - instance_data = [] - - for _ in range(self.n_instance): - # Generate random processing times for each job on each machine - # Each job has a sequence of operations, and each operation is assigned to a machine - # For simplicity, we assume each job has exactly `n_machines` operations, one for each machine - processing_times = [] - for _ in range(self.n_jobs): - # Randomly assign processing times for each machine - job_processing_times = np.random.randint(1, 100, size=self.n_machines).tolist() - processing_times.append(job_processing_times) - - instance_data.append((processing_times, self.n_jobs, self.n_machines)) - - return instance_data - - -def determine_next_operation(current_status, feasible_operations): - """ - Determine the next operation to schedule based on a greedy heuristic. - - Args: - current_status: A dictionary representing the current status of each machine and job. - feasible_operations: A list of feasible operations that can be scheduled next. - - Returns: - The next operation to schedule, represented as a tuple (job_id, machine_id, processing_time). - """ - # Simple greedy heuristic: choose the operation with the shortest processing time - next_operation = min(feasible_operations, key=lambda x: x[2]) - return next_operation - - -def schedule_jobs(processing_times, n_jobs, n_machines): - """ - Schedule jobs on machines using a greedy constructive heuristic. - - Args: - processing_times: A list of lists representing the processing times of each job on each machine. - n_jobs: Number of jobs. - n_machines: Number of machines. - - Returns: - The makespan, which is the total time required to complete all jobs. - """ - # Initialize the current status of each machine and job - machine_status = [0] * n_machines # Time each machine is available - job_status = [0] * n_jobs # Time each job is available - operation_sequence = [[] for _ in range(n_jobs)] # Sequence of operations for each job - - # Initialize the list of all operations - all_operations = [] - for job_id in range(n_jobs): - for machine_id in range(n_machines): - all_operations.append((job_id, machine_id, processing_times[job_id][machine_id])) - - # Schedule operations until all are completed - while all_operations: - # Determine feasible operations - feasible_operations = [] - for operation in all_operations: - job_id, machine_id, processing_time = operation - if job_status[job_id] <= machine_status[machine_id]: - feasible_operations.append(operation) - - if len(feasible_operations) == 0: - next_operation = all_operations[0] - else: - # Determine the next operation to schedule - next_operation = determine_next_operation({'machine_status': machine_status, 'job_status': job_status}, feasible_operations) - - # Schedule the next operation - job_id, machine_id, processing_time = next_operation - start_time = max(job_status[job_id], machine_status[machine_id]) - end_time = start_time + processing_time - machine_status[machine_id] = end_time - job_status[job_id] = end_time - operation_sequence[job_id].append((machine_id, start_time, end_time)) - - # Remove the scheduled operation from the list of all operations - all_operations.remove(next_operation) - - # Calculate the makespan (total time required to complete all jobs) - makespan = max(job_status) - return makespan, operation_sequence - - -# Example usage -if __name__ == "__main__": - # Generate data - data_generator = GetData(n_instance=1, n_jobs=5, n_machines=5).generate_instances() - - for instance in data_generator: - processing_times, n1, n2 = instance - makespan, solution = schedule_jobs(processing_times, n1, n2) - print(makespan) - print(solution) diff --git a/examples/benchmark_tasks/optimization_set_covering/__init__.py b/examples/benchmark_tasks/optimization_set_covering/__init__.py deleted file mode 100644 index 42895e56..00000000 --- a/examples/benchmark_tasks/optimization_set_covering/__init__.py +++ /dev/null @@ -1,497 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_set_covering -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# References: -# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language -# model agents in algorithm search for combinatorial optimization. -# arXiv preprint arXiv:2504.04310 (2025). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -from typing import Any -import numpy as np -from llm4ad_loader import Evaluation -from llm4ad_loader import load_subdir_as_text -# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader -# from llm4ad.task.optimization.co_bench.set_covering_co_bench.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(m: int, n: int, costs: list, row_cover: list) -> dict:\n """\n Solves the set covering optimization problem.\n Problem Description:\n Given m rows (constraints) and n columns (covering sets) with associated costs,\n choose a subset of columns such that every row is covered (i.e. for every row,\n at least one chosen column appears in that row\'s coverage list) while minimizing\n the total cost (the sum of the costs of the chosen columns).\n Input kwargs:\n - m: (int) number of rows.\n - n: (int) number of columns.\n - costs: (list of int) where costs[j] is the cost for column j+1.\n - row_cover: (list of list of int) where row_cover[i] contains the 1-indexed column\n numbers that cover row i+1.\n Evaluation Metric:\n The score is computed as the sum of the costs for the chosen columns.\n However, if any row is left uncovered by the chosen columns, the solution is invalid and receives no score.\n Otherwise, the score is simply the total cost of the selected columns.\n Returns:\n A dictionary with one key:\n - "selected_columns": a list of 1-indexed column numbers representing the chosen covering set.\n """\n ## placeholder. You do not need to write anything here.\n return {"selected_columns": []}' -task_description = '("Set Covering Problem. The goal is to select a subset of columns, each with an associated cost, "' - - -__all__ = ['SCEvaluationCB'] - - -class SCEvaluationCB(Evaluation): - - def __init__(self, - timeout_seconds=50, - **kwargs): - - """ - Args: - None - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - # Load datasets from Hugging Face - dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Set covering") - self._datasets = {} - for filename in dataset: - # Join all text rows into a single string - text_content = '\n'.join([row['text'] for row in dataset[filename]]) - self._datasets[filename] = text_content - - def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: - return self.evaluate(callable_func) - - def evaluate(self, eva: callable) -> float | None: - ins_cases = [] - for case_id, ins in enumerate(self._datasets.values()): - ins_cases.append(self.load_data(ins)) - - fitness_list = [] - try: - for i in ins_cases: - for j in i: - result = eva(j['m'], j['n'], j['costs'], j['row_cover']) - fitness = self.eval_func(m=j['m'], n=j['n'], costs=j['costs'], row_cover=j['row_cover'], selected_columns=result['selected_columns']) - fitness_list.append(fitness) - - return -np.mean(fitness_list) - - except ValueError as e: - print(e) - return None - - def load_data(self, input_string): - """ - Loads one or more set covering test cases from string content. - The input can contain one or more cases. Each case must follow one of three formats: - Format A (SCP/Beasley): - - Header (first nonempty line): two integers, m and n. - - Next: a cost vector of n integers (which may span multiple lines). - - Then: for each row, a line that starts with an integer k (number of columns covering the row) - followed by k space‑separated 1-indexed column indices. - Format B (Real-world rail problems): - - Header: two integers, m and n. - - Next n nonempty lines: each line describes a column by giving: - cost, the number of rows the column covers, and then that many 1-indexed row indices. - - Row coverage is then built by aggregating the information from each column. - Format C (Dense row format): - - Header: two integers, m and n. - - Next m nonempty lines: each line lists the 1-indexed column indices that cover that row. - - In this format, every column has an implicit unit cost. - If the input contains multiple cases, it is assumed that the cases are separated - by at least one blank line. - Returns: - A list of cases, where each case is a dictionary with keys: - - "m": number of rows (int) - - "n": number of columns (int) - - "costs": list of column costs (list of int) - - "row_cover": list of lists; each inner list contains the 1-indexed column numbers covering that row. - """ -import os, sys -sys.path.insert(0, os.path.dirname(__file__)) - import re - - content = input_string.strip() - - # Split into blocks by one or more blank lines. - blocks = re.split(r'\n\s*\n', content) - cases = [] - - # Check if the very first block is simply a test-case count. - first_block_tokens = blocks[0].split() - if len(first_block_tokens) == 1: - try: - num_cases = int(first_block_tokens[0]) - # Remove the count block and treat the remaining blocks as cases. - blocks = blocks[1:] - if len(blocks) != num_cases: - # Fall back: if the number doesn't match, assume each block is a case. - pass - except Exception: - pass # Not a test-case count; treat first block as a case. - - for block in blocks: - case = self._parse_single_case(block) - cases.append(case) - return cases - - def _parse_single_case(self, block): - """ - Helper function to parse a single test case from a block (string) of text. - The block must have its lines (nonempty) in one of the three supported formats. - """ - lines = [line.strip() for line in block.splitlines() if line.strip()] - if not lines: - raise ValueError("Encountered an empty test case block.") - - header = lines[0].split() - if len(header) < 2: - raise ValueError("Header must contain at least two integers (m and n).") - try: - m = int(header[0]) - n = int(header[1]) - except Exception as e: - raise ValueError("Error parsing m and n from header: " + str(e)) - - remaining_lines = lines[1:] - - # Determine format based on the number of remaining lines. - if len(remaining_lines) == n: - # Format B: one line per column. - costs = [] - col_rows = [] - for j in range(n): - tokens = remaining_lines[j].split() - if len(tokens) < 2: - raise ValueError(f"Column {j + 1}: expected at least cost and count.") - try: - cost = int(tokens[0]) - count = int(tokens[1]) - except Exception as e: - raise ValueError(f"Error parsing cost/count for column {j + 1}: {e}") - if len(tokens) < 2 + count: - raise ValueError(f"Column {j + 1}: expected {count} row indices, got {len(tokens) - 2}.") - try: - rows_for_col = list(map(int, tokens[2:2 + count])) - except Exception as e: - raise ValueError(f"Error parsing row indices for column {j + 1}: {e}") - costs.append(cost) - col_rows.append(rows_for_col) - # Build row coverage from column data. - row_cover = [[] for _ in range(m)] - for j in range(n): - for r in col_rows[j]: - if r < 1 or r > m: - raise ValueError(f"Column {j + 1}: row index {r} is out of bounds.") - row_cover[r - 1].append(j + 1) - return {"m": m, "n": n, "costs": costs, "row_cover": row_cover} - - elif len(remaining_lines) == m: - # Format C: one line per row (dense row format). - costs = [1] * n - row_cover = [] - for i in range(m): - try: - cols = list(map(int, remaining_lines[i].split())) - except Exception as e: - raise ValueError(f"Error parsing row {i + 1}: {e}") - row_cover.append(cols) - return {"m": m, "n": n, "costs": costs, "row_cover": row_cover} - - else: - # Format A: SCP test case. - # First, read cost vector tokens until we have n tokens. - cost_tokens = [] - line_index = 0 - while line_index < len(remaining_lines) and len(cost_tokens) < n: - tokens = remaining_lines[line_index].split() - cost_tokens.extend(tokens) - line_index += 1 - if len(cost_tokens) < n: - raise ValueError("Not enough tokens for cost vector.") - try: - costs = list(map(int, cost_tokens[:n])) - except Exception as e: - raise ValueError("Error converting cost tokens to integers: " + str(e)) - - # The remaining tokens represent row coverage. - row_tokens = [] - for line in remaining_lines[line_index:]: - row_tokens.extend(line.split()) - token_index = 0 - row_cover = [] - for i in range(m): - if token_index >= len(row_tokens): - raise ValueError(f"Not enough tokens for row {i + 1}.") - try: - k = int(row_tokens[token_index]) - except Exception as e: - raise ValueError(f"Error parsing coverage count for row {i + 1}: {e}") - token_index += 1 - if token_index + k > len(row_tokens): - raise ValueError(f"Not enough tokens for row {i + 1}: expected {k} tokens.") - try: - cols = list(map(int, row_tokens[token_index: token_index + k])) - except Exception as e: - raise ValueError(f"Error parsing column indices for row {i + 1}: {e}") - token_index += k - row_cover.append(cols) - return {"m": m, "n": n, "costs": costs, "row_cover": row_cover} - - def eval_func(self, **kwargs): - """ - Evaluates the solution for a single test case. - Parameters: - - m: (int) number of rows. - - n: (int) number of columns. - - costs: (list of int) where costs[j] is the cost for column j+1. - - row_cover: (list of list of int) where row_cover[i] contains the 1-indexed columns covering row i+1. - - selected_columns: a list of chosen 1-indexed column numbers. - Evaluation: - 1. Compute the total cost as the sum of the costs for each selected column. - 2. Verify that every row is covered by at least one of the selected columns. - If any row is uncovered, the function raises an error indicating the constraint violation. - Returns: - A scalar value representing the computed score (total cost) if all constraints are met. - Raises: - KeyError: if "selected_columns" is not provided in kwargs. - ValueError: if any selected column is out of valid bounds or if any row is left uncovered. - """ - m = kwargs["m"] - n = kwargs["n"] - costs = kwargs["costs"] - row_cover = kwargs["row_cover"] - - if "selected_columns" not in kwargs: - raise KeyError("Solution must contain 'selected_columns'.") - - selected_columns = set(kwargs["selected_columns"]) - - # Check that each selected column is within valid bounds. - for col in selected_columns: - if col < 1 or col > n: - raise ValueError(f"Column {col} is out of bounds (should be between 1 and {n}).") - - computed_cost = sum(costs[col - 1] for col in selected_columns) - - # Verify that every row is covered by at least one selected column. - uncovered_rows = [] - for i in range(m): - if not set(row_cover[i]).intersection(selected_columns): - uncovered_rows.append(i + 1) - - if uncovered_rows: - raise ValueError("Infeasible solution: rows such as {} are not covered.".format( - ', '.join(map(str, uncovered_rows[:10])) - )) - - return computed_cost - - def norm_score(self, results): - optimal_scores = { - "scp41.txt": [429], - "scp42.txt": [512], - "scp43.txt": [516], - "scp45.txt": [512], - "scp47.txt": [430], - "scp49.txt": [641], - "scp410.txt": [514], - "scp53.txt": [226], - "scp55.txt": [211], - "scp56.txt": [213], - "scp58.txt": [288], - "scp59.txt": [279], - "scp510.txt": [265], - "scp44.txt": [494], - "scp46.txt": [560], - "scp48.txt": [492], - "scp51.txt": [253], - "scp52.txt": [302], - "scp54.txt": [242], - "scp57.txt": [293], - "scp61.txt": [138], - "scp62.txt": [146], - "scp63.txt": [145], - "scp64.txt": [131], - "scp65.txt": [161], - "scpa1.txt": [253], - "scpa2.txt": [252], - "scpa3.txt": [232], - "scpa4.txt": [234], - "scpa5.txt": [236], - "scpb1.txt": [69], - "scpb2.txt": [76], - "scpb3.txt": [80], - "scpb4.txt": [79], - "scpb5.txt": [72], - "scpc1.txt": [227], - "scpc2.txt": [219], - "scpc3.txt": [243], - "scpc4.txt": [219], - "scpc5.txt": [215], - "scpd1.txt": [60], - "scpd2.txt": [66], - "scpd3.txt": [72], - "scpd4.txt": [62], - "scpd5.txt": [61], - "scpe1.txt": [5], - "scpe2.txt": [5], - "scpe3.txt": [5], - "scpe4.txt": [5], - "scpe5.txt": [5], - "scpnre1.txt": [29], - "scpnre2.txt": [32], - "scpnre3.txt": [28], - "scpnre4.txt": [30], - "scpnre5.txt": [28], - "scpnrf1.txt": [15], - "scpnrf2.txt": [16], - "scpnrf3.txt": [15], - "scpnrf4.txt": [15], - "scpnrf5.txt": [14], - "scpnrg1.txt": [184], - "scpnrg2.txt": [163], - "scpnrg3.txt": [174], - "scpnrg4.txt": [176], - "scpnrg5.txt": [175], - "scpnrh1.txt": [68], - "scpnrh2.txt": [66], - "scpnrh3.txt": [65], - "scpnrh4.txt": [63], - "scpnrh5.txt": [60], - "scpcyc06.txt": [48.0], - "scpcyc07.txt": [112.0], - "scpcyc08.txt": [256.0], - "scpcyc09.txt": [576.0], - "scpcyc010.txt": [1280.0], - "scpcyc011.txt": [2816.0], - "scpclr10.txt": [21.0], - "scpclr11.txt": [16.5], - "scpclr12.txt": [16.5], - "scpclr13.txt": [14.3], - "rail507.txt": [172.4], - "rail516.txt": [182], - "rail582.txt": [209.5], - "rail2536.txt": [691], - "rail2586.txt": [936.1], - "rail4284.txt": [1065], - "rail4872.txt": [1509], - } - - normed = {} - for case, (scores, error_message) in results.items(): - if case not in optimal_scores: - continue # Skip if there's no optimal score defined. - optimal_list = optimal_scores[case] - normed_scores = [] - # Compute normalized score for each index. - for idx, score in enumerate(scores): - if isinstance(score, (int, float)): - normed_scores.append(optimal_list[idx] / score) - else: - normed_scores.append(score) - normed[case] = (normed_scores, error_message) - - return normed - - def get_dev(self): - dev = {'rail2536.txt': [0], 'rail4284.txt': [0], - 'rail516.txt': [0], 'rail582.txt': [0], - 'scp410.txt': [0], 'scp42.txt': [0], - 'scp44.txt': [0], 'scp48.txt': [0], - 'scp52.txt': [0], 'scp54.txt': [0], - 'scp56.txt': [0], 'scp58.txt': [0], 'scp62.txt': [0], - 'scp64.txt': [0], 'scpa2.txt': [0], - 'scpa4.txt': [0], 'scpb2.txt': [0], 'scpb4.txt': [0], - 'scpc2.txt': [0], 'scpc4.txt': [0], - 'scpclr10.txt': [0], 'scpclr12.txt': [0], 'scpcyc010.txt': [0], - 'scpcyc06.txt': [0], 'scpcyc08.txt': [0], - 'scpd2.txt': [0], 'scpd4.txt': [0], 'scpd5.txt': [0], - 'scpe2.txt': [0], 'scpe4.txt': [0], 'scpnre2.txt': [0], - 'scpnre4.txt': [0], 'scpnrf2.txt': [0], - 'scpnrf4.txt': [0], 'scpnrg2.txt': [0], - 'scpnrg4.txt': [0], 'scpnrh2.txt': [0], - 'scpnrh4.txt': [0]} - - return dev - - - - - - - - -# Task configuration for benchmark task -ENTRY_NAME = 'solve' -FUNCTION_SIGNATURE = 'def solve(...):' -IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' -TASK_DESCRIPTION = '("Set Covering Problem. The goal is to select a subset of columns, each with an associated cost, "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("Set Covering Problem. The goal is to select a subset of columns, each with an associated cost, "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(m: int, n: int, costs: list, row_cover: list) -> dict:\n """\n Solves the set covering optimization problem.\n Problem Description:\n Given m rows (constraints) and n columns (covering sets) with associated costs,\n choose a subset of columns such that every row is covered (i.e. for every row,\n at least one chosen column appears in that row\'s coverage list) while minimizing\n the total cost (the sum of the costs of the chosen columns).\n Input kwargs:\n - m: (int) number of rows.\n - n: (int) number of columns.\n - costs: (list of int) where costs[j] is the cost for column j+1.\n - row_cover: (list of list of int) where row_cover[i] contains the 1-indexed column\n numbers that cover row i+1.\n Evaluation Metric:\n The score is computed as the sum of the costs for the chosen columns.\n However, if any row is left uncovered by the chosen columns, the solution is invalid and receives no score.\n Otherwise, the score is simply the total cost of the selected columns.\n Returns:\n A dictionary with one key:\n - "selected_columns": a list of 1-indexed column numbers representing the chosen covering set.\n """\n ## placeholder. You do not need to write anything here.\n return {"selected_columns": []}' -EVAL_CLASS_NAME = 'SCEvaluationCB' -EVAL_KWARGS = {'timeout_seconds': 60} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_set_covering/paras.yaml b/examples/benchmark_tasks/optimization_set_covering/paras.yaml deleted file mode 100644 index 68fbacb9..00000000 --- a/examples/benchmark_tasks/optimization_set_covering/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: SCEvaluationCB -timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_set_partitioning/__init__.py b/examples/benchmark_tasks/optimization_set_partitioning/__init__.py deleted file mode 100644 index 6cc7cd3c..00000000 --- a/examples/benchmark_tasks/optimization_set_partitioning/__init__.py +++ /dev/null @@ -1,389 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_set_partitioning -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# References: -# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language -# model agents in algorithm search for combinatorial optimization. -# arXiv preprint arXiv:2504.04310 (2025). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -from typing import Any -import numpy as np -from llm4ad_loader import Evaluation -from llm4ad_loader import load_subdir_as_text -# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader -# from llm4ad.task.optimization.co_bench.set_partitioning_co_bench.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(num_rows: int, num_columns: int, columns_info: dict) -> dict:\n """\n Solve a set partitioning problem instance.\n The problem: Given a set of rows and a set of columns (each with an associated cost and a set\n of rows it covers), select a subset of columns so that each row is covered exactly once and the\n total cost is minimized.\n Input kwargs:\n - num_rows (int): Total number of rows. (int)\n - num_columns (int): Total number of columns. (int)\n - columns_info (dict): Dictionary mapping 1-indexed column indices (int) to a tuple:\n (cost (int), set of row indices (set[int]) covered by that column).\n Evaluation metric:\n The objective score equals the sum of the costs of the selected columns if the solution is feasible,\n i.e., if every row is covered exactly once. Otherwise, the solution is invalid and receives no score.\n Returns:\n A dictionary with key "selected_columns" containing a list of chosen column indices in strictly increasing order.\n (This is a placeholder implementation.)\n """\n # Placeholder implementation.\n # You must replace the following line with your actual solution logic.\n return {"selected_columns": []}' -task_description = '("This problem involves solving a set partitioning instance where the goal is to choose a subset "' - - -__all__ = ['SPEvaluationCB'] - - -class SPEvaluationCB(Evaluation): - - def __init__(self, - timeout_seconds=50, - **kwargs): - - """ - Args: - None - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - # Load datasets from Hugging Face - dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Set partitioning") - self._datasets = {} - for filename in dataset: - # Join all text rows into a single string - text_content = '\n'.join([row['text'] for row in dataset[filename]]) - self._datasets[filename] = text_content - - def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: - return self.evaluate(callable_func) - - def evaluate(self, eva: callable) -> float | None: - ins_cases = [] - for case_id, ins in enumerate(self._datasets.values()): - ins_cases.append(self.load_data(ins)) - - fitness_list = [] - try: - for i in ins_cases: - for j in i: - result = eva(j['num_rows'], j['num_columns'], j['columns_info']) - fitness = self.eval_func(num_rows=j['num_rows'], num_columns=j['num_columns'], columns_info=j['columns_info'], selected_columns=result['selected_columns']) - fitness_list.append(fitness) - - return -np.mean(fitness_list) - - except ValueError as e: - print(e) - return None - - def load_data(self, input_string): - """ - Load and validate one or multiple set partitioning cases from a TXT file. - The file may contain multiple cases. Each case is structured as follows: - - The first non-empty line of a case contains two integers: num_rows and num_columns. - - Then, for each of the num_columns columns, there is one line containing: - cost (int), count (int), followed by exactly 'count' integers (the row indices covered). - Each case is parsed and validated independently. If any inconsistency or formatting error is found, - a ValueError is raised. - Returns: - cases (list): A list of dictionaries, each representing one case with keys: - - 'num_rows': int - - 'num_columns': int - - 'columns_info': dict mapping column index (1-indexed) -> (cost, set(row_indices)) - """ - cases = [] - lines = [line.strip() for line in input_string.split('\n') if line.strip() != ''] - - index = 0 - total_lines = len(lines) - - while index < total_lines: - # Parse header line for one case. - header_tokens = lines[index].split() - index += 1 - if len(header_tokens) < 2: - raise ValueError("Header must contain two integers: num_rows and num_columns.") - try: - num_rows = int(header_tokens[0]) - num_columns = int(header_tokens[1]) - except Exception: - raise ValueError("Header values must be integers.") - - columns_info = {} - # There must be exactly num_columns lines following for the columns. - for j in range(1, num_columns + 1): - if index >= total_lines: - raise ValueError("Insufficient lines for all columns' data in a case.") - parts = lines[index].split() - index += 1 - - if len(parts) < 2: - raise ValueError("Each column line must have at least 2 tokens (cost and count).") - try: - cost = int(parts[0]) - count = int(parts[1]) - except Exception: - raise ValueError("Column cost and count must be integers.") - - if len(parts) != 2 + count: - raise ValueError(f"Column {j} is expected to have {2 + count} tokens, but got {len(parts)}.") - try: - row_list = [int(tok) for tok in parts[2:]] - except Exception: - raise ValueError("Row indices must be integers.") - - for r in row_list: - if r < 1 or r > num_rows: - raise ValueError("Row index out of the valid range (1 to num_rows).") - - columns_info[j] = (cost, set(row_list)) - - # Append the case as a dictionary. - cases.append({ - "num_rows": num_rows, - "num_columns": num_columns, - "columns_info": columns_info - }) - - if not cases: - raise ValueError("Input file is empty or contains no valid cases.") - return cases - - def eval_func(self, **kwargs): - """ - Evaluate a solution for a set partitioning problem case. - Expected kwargs: - - num_rows (int): Total number of rows. - - num_columns (int): Total number of columns. - - columns_info (dict): Dictionary mapping column index (1-indexed) to a tuple (cost, set(row indices)). - - selected_columns (list): List of selected column indices (should be in strictly increasing order). - Raises: - ValueError: If any constraints are violated, such as an invalid output format, - a column index error, or if any row is not covered exactly once. - Returns: - score (int): The computed score, which is the total cost of the selected columns. - Lower scores are better. - """ - # Retrieve input data. - num_rows = kwargs["num_rows"] - num_columns = kwargs["num_columns"] - columns_info = kwargs["columns_info"] - selected_columns = kwargs.get("selected_columns") - - # Validate that selected_columns is provided and is a list. - if selected_columns is None or not isinstance(selected_columns, list): - raise ValueError("selected_columns must be provided as a list.") - - # Enforce that the list is in strictly increasing order and has no duplicates. - if selected_columns != sorted(selected_columns) or len(selected_columns) != len(set(selected_columns)): - raise ValueError("selected_columns must be in strictly increasing order with no duplicates.") - - # Validate each selected column index. - for col in selected_columns: - if not isinstance(col, int) or col < 1 or col > num_columns: - raise ValueError(f"Invalid column index: {col}. Must be an integer between 1 and {num_columns}.") - - total_cost = 0 - row_coverage = [0] * (num_rows + 1) # 1-indexed; index 0 is unused. - - # Process each selected column. - for col in selected_columns: - if col not in columns_info: - raise ValueError(f"Column {col} not found in columns_info.") - cost, covered_rows = columns_info[col] - total_cost += cost - for r in covered_rows: - if r < 1 or r > num_rows: - raise ValueError(f"Invalid row index: {r} (must be between 1 and {num_rows}).") - row_coverage[r] += 1 - - # Ensure that every row is covered exactly once. - for r in range(1, num_rows + 1): - if row_coverage[r] != 1: - raise ValueError(f"Row {r} is covered {row_coverage[r]} times; each row must be covered exactly once.") - - return total_cost - - def norm_score(self, results): - optimal_scores = { - "bills_snowflake.txt": [34], - "exotic_fives.txt": [12], - "sppaa02.txt": [30494], - "sppaa03.txt": [49649], - "sppaa05.txt": [53839], - "sppaa06.txt": [27040], - "delta.txt": [126], - "heart.txt": [180], - "sppkl01.txt": [1086], - "sppkl02.txt": [219], - "meteor.txt": [60], - "sppnw01.txt": [114852], - "sppnw02.txt": [105444], - "sppnw03.txt": [24492], - "sppnw04.txt": [16862], - "sppnw05.txt": [132878], - "sppnw06.txt": [7810], - "sppnw07.txt": [5476], - "sppnw08.txt": [35894], - "sppnw09.txt": [67760], - "sppnw10.txt": [68271], - "sppnw11.txt": [116256], - "sppnw12.txt": [14118], - "sppnw13.txt": [50146], - "sppnw14.txt": [61844], - "sppnw15.txt": [67743], - "sppnw16.txt": [1181590], - "sppnw17.txt": [11115], - "sppnw18.txt": [340160], - "sppnw19.txt": [10898], - "sppnw20.txt": [16812], - "sppnw21.txt": [7408], - "sppnw22.txt": [6984], - "sppnw23.txt": [12534], - "sppnw24.txt": [6314], - "sppnw25.txt": [5960], - "sppnw26.txt": [6796], - "sppnw27.txt": [9933], - "sppnw28.txt": [8298], - "sppnw29.txt": [4274], - "sppnw30.txt": [3942], - "sppnw31.txt": [8038], - "sppnw32.txt": [14877], - "sppnw33.txt": [6678], - "sppnw34.txt": [10488], - "sppnw35.txt": [7216], - "sppnw36.txt": [7314], - "sppnw37.txt": [10068], - "sppnw38.txt": [5558], - "sppnw39.txt": [10080], - "sppnw40.txt": [10809], - "sppnw41.txt": [11307], - "sppnw42.txt": [7656], - "sppnw43.txt": [8904], - "sppus01.txt": [10036], - "sppus02.txt": [5965], - "sppus03.txt": [5338], - "sppus04.txt": [17854], - "sppaa01.txt": [55535.4], - "sppaa04.txt": [25877.6], - - } - - normed = {} - for case, (scores, error_message) in results.items(): - if case not in optimal_scores: - continue # Skip if there's no optimal score defined. - optimal_list = optimal_scores[case] - normed_scores = [] - # Compute normalized score for each index. - for idx, score in enumerate(scores): - if isinstance(score, (int, float)): - normed_scores.append(optimal_list[idx] / score) - else: - normed_scores.append(score) - normed[case] = (normed_scores, error_message) - - return normed - - def get_dev(self): - dev = {'bills_snowflake.txt': [0], 'meteor.txt': [0], - 'sppaa02.txt': [0], 'sppaa04.txt': [0], 'sppaa05.txt': [0], - - 'sppkl02.txt': [0], - 'sppnw02.txt': [0], - 'sppnw04.txt': [0], 'sppnw06.txt': [0], - 'sppnw08.txt': [0], 'sppnw10.txt': [0], 'sppnw12.txt': [0], - 'sppnw14.txt': [0], 'sppnw16.txt': [0], - 'sppnw18.txt': [0], 'sppnw20.txt': [0], 'sppnw22.txt': [0], - 'sppnw24.txt': [0], 'sppnw26.txt': [0], - 'sppnw28.txt': [0], 'sppnw30.txt': [0], 'sppnw32.txt': [0], - 'sppnw34.txt': [0], 'sppnw36.txt': [0], - 'sppnw38.txt': [0], 'sppnw40.txt': [0], 'sppnw42.txt': [0], - 'sppus02.txt': [0], 'sppus04.txt': [0]} - - return dev - - - - - - - - -# Task configuration for benchmark task -ENTRY_NAME = 'solve' -FUNCTION_SIGNATURE = 'def solve(...):' -IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' -TASK_DESCRIPTION = '("This problem involves solving a set partitioning instance where the goal is to choose a subset "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("This problem involves solving a set partitioning instance where the goal is to choose a subset "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(num_rows: int, num_columns: int, columns_info: dict) -> dict:\n """\n Solve a set partitioning problem instance.\n The problem: Given a set of rows and a set of columns (each with an associated cost and a set\n of rows it covers), select a subset of columns so that each row is covered exactly once and the\n total cost is minimized.\n Input kwargs:\n - num_rows (int): Total number of rows. (int)\n - num_columns (int): Total number of columns. (int)\n - columns_info (dict): Dictionary mapping 1-indexed column indices (int) to a tuple:\n (cost (int), set of row indices (set[int]) covered by that column).\n Evaluation metric:\n The objective score equals the sum of the costs of the selected columns if the solution is feasible,\n i.e., if every row is covered exactly once. Otherwise, the solution is invalid and receives no score.\n Returns:\n A dictionary with key "selected_columns" containing a list of chosen column indices in strictly increasing order.\n (This is a placeholder implementation.)\n """\n # Placeholder implementation.\n # You must replace the following line with your actual solution logic.\n return {"selected_columns": []}' -EVAL_CLASS_NAME = 'SPEvaluationCB' -EVAL_KWARGS = {'timeout_seconds': 60} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_set_partitioning/paras.yaml b/examples/benchmark_tasks/optimization_set_partitioning/paras.yaml deleted file mode 100644 index 9fc34ede..00000000 --- a/examples/benchmark_tasks/optimization_set_partitioning/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: SPEvaluationCB -timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_travelling_salesman_problem/__init__.py b/examples/benchmark_tasks/optimization_travelling_salesman_problem/__init__.py deleted file mode 100644 index a8b76128..00000000 --- a/examples/benchmark_tasks/optimization_travelling_salesman_problem/__init__.py +++ /dev/null @@ -1,334 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_travelling_salesman_problem -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# References: -# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language -# model agents in algorithm search for combinatorial optimization. -# arXiv preprint arXiv:2504.04310 (2025). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -from typing import Any -import numpy as np -from llm4ad_loader import Evaluation -from llm4ad_loader import load_subdir_as_text -# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader -# from llm4ad.task.optimization.co_bench.travelling_salesman_problem_co_bench.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(nodes: list) -> dict:\n """\n Solve a TSP instance.\n Args:\n - nodes (list): List of (x, y) coordinates representing cities in the TSP problem\n Format: [(x1, y1), (x2, y2), ..., (xn, yn)]\n Returns:\n dict: Solution information with:\n - \'tour\' (list): List of node indices representing the solution path\n Format: [0, 3, 1, ...] where numbers are indices into the nodes list\n """\n\n return {\n \'tour\': [],\n }' -task_description = '("The Traveling Salesman Problem (TSP) is a classic combinatorial optimization problem where, "' - - -__all__ = ['TSPEvaluationCB'] - - -class TSPEvaluationCB(Evaluation): - - def __init__(self, - timeout_seconds=50, - **kwargs): - - """ - Args: - None - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - # Load datasets from Hugging Face - dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Travelling salesman problem") - self._datasets = {} - for filename in dataset: - # Join all text rows into a single string - text_content = '\n'.join([row['text'] for row in dataset[filename]]) - self._datasets[filename] = text_content - - def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: - return self.evaluate(callable_func) - - def evaluate(self, eva: callable) -> float | None: - ins_cases = [] - for case_id, ins in enumerate(self._datasets.values()): - ins_cases.append(self.load_data(ins)) - - fitness_list = [] - try: - for i in ins_cases: - for j in i: - result = eva(j['nodes']) - fitness = self.eval_func(j['nodes'], j['tour'], result['tour']) - fitness_list.append(fitness) - - return -np.mean(fitness_list) - - except ValueError as e: - print(e) - return None - - def load_data(self, input_string): - """ - Load TSP instances from a file. - Args: - file_path (str): Path to the file containing TSP instances - Returns: - list: List of dictionaries, each containing a TSP instance with: - - 'nodes': List of (x, y) coordinates - - 'tour': List of node indices representing the optimal tour (if available) - """ - instances = [] - for line in input_string.split('\n'): - if line.strip(): # Skip empty lines - line = line.split(" ") - try: - output_idx = line.index('output') - num_nodes = output_idx // 2 - - # Extract node coordinates - nodes = [(float(line[i]), float(line[i + 1])) for i in range(0, 2 * num_nodes, 2)] - - # Extract tour (if available) - tour = None - if output_idx < len(line) - 1: - # Convert tour nodes to 0-indexed and exclude the final node (which is the same as the first) - tour = [int(node) - 1 for node in line[output_idx + 1:-1]][:-1] - - instances.append({ - 'nodes': nodes, - 'tour': tour # Changed from 'label_tour' to 'tour' to match eval_func - }) - except (ValueError, IndexError) as e: - print(f"Error processing line: {e}") - continue - return instances - - def eval_func(self, nodes, label_tour, tour): - """ - Evaluate a predicted TSP tour against a reference tour. - Args: - nodes (list): List of (x, y) coordinates representing cities in the TSP problem - Format: [(x1, y1), (x2, y2), ..., (xn, yn)] - label_tour (list): Reference/optimal tour as list of node indices - Format: [0, 3, 1, ...] (may be None if no reference available) - tour (list): Predicted tour from the solver as list of node indices - Format: [0, 3, 1, ...] - Returns: - float: Optimality gap percentage ((predicted_cost/optimal_cost - 1) * 100) - or just the predicted cost if no label_tour is provided - """ - # Calculate the predicted tour cost - import math - - num_nodes = len(nodes) - - if len(tour) != num_nodes: - raise Exception(f"Invalid tour length: Expected {num_nodes}, got {len(tour)}") - nodes_set = set(tour) - - if len(nodes_set) != num_nodes: - raise Exception(f"Invalid tour: Contains {len(nodes_set)} unique nodes, expected {num_nodes}") - - expected_nodes = set(range(num_nodes)) - if nodes_set != expected_nodes: - raise Exception(f"Invalid tour: Contains out-of-range or missing nodes") - - def calculate_tour_cost(nodes, tour): - cost = 0 - for i in range(len(tour)): - from_node = tour[i] - to_node = tour[(i + 1) % len(tour)] # Wrap around to the first node - - # Calculate Euclidean distance - from_x, from_y = nodes[from_node] - to_x, to_y = nodes[to_node] - segment_cost = math.sqrt((to_x - from_x) ** 2 + (to_y - from_y) ** 2) - - cost += segment_cost - - return cost - - pred_cost = calculate_tour_cost(nodes, tour) - - return pred_cost - - def norm_score(self, results): - optimal_scores = { - 'tsp10000_test_concorde.txt': [71.77] * 16, - 'tsp1000_test_concorde.txt': [23.180520881091528, 23.185595820967464, 23.015849671324247, - 23.537607117355098, - 23.437452128607738, 23.31718378127829, 23.337815853824736, 22.98403971254625, - 23.056714372610298, 23.344826856094013, 23.204461510197465, - 22.739131293587075, - 23.188355412394525, 22.89676721383878, 23.321213972552503, 23.288168535452023, - 23.40260594371496, 23.379338976209613, 23.373901670260118, 23.217316627245133, - 23.237964507712658, 23.468791280324233, 22.921856962988343, 23.10809259424775, - 23.370845238521724, 23.241556219224208, 23.348641855759727, 23.53455701244874, - 23.385399569524708, 23.324316152061755, 23.600128423871258, 22.97776918106818, - 23.23996887566731, 23.39944035075775, 23.21410580402093, 23.093180229981513, - 23.41235476581497, 22.907788976836535, 23.023973448563986, 23.38106742108426, - 23.015367118079723, 22.610650093362192, 23.728111421819854, 23.31046641124744, - 23.25381246570274, 22.889579599261864, 23.138723098665373, 23.228706227395723, - 23.420741250703944, 23.255723604641904, 23.63211466330456, 23.03074201227862, - 23.08458884685017, 23.241154659459145, 23.445330799785832, 23.315728497380498, - 23.262087203582375, 23.43107533587823, 23.020824065107902, 23.591574572456, - 23.01019854749962, 23.006394524552746, 23.117390281951273, 23.06132560795126, - 22.899650785646813, 23.17319516968116, 23.229133743009296, 23.187607300641957, - 22.83150095703399, 23.158901255572648, 23.298349320155108, 23.364983773246387, - 23.265256805650658, 23.73268837357109, 23.07144480109362, 23.202894990560697, - 23.34293044019312, 23.027139320724427, 23.005485112127072, 23.16783838686215, - 23.505726302417372, 23.002594549857108, 23.50388356372942, 23.147934207287026, - 23.149537479144914, 23.20934617772166, 23.591015529376406, 23.04614917635098, - 23.253196613627406, 23.608716670166032, 23.313874804840438, 23.14887954791675, - 23.261925104915175, 23.283273388936596, 22.869470302805432, 23.28919260955595, - 23.291061784892037, 23.26303190269252, 23.43192602385145, 22.992654709729297, - 23.53527899384453, 23.040088044723632, 23.165752550718327, 23.346603825959306, - 23.21040140495141, 23.346553301777227, 23.192654754892565, 23.30425312678073, - 23.03197099577737, 23.33672313379179, 23.209507048094107, 23.33316267340018, - 22.832592819311447, 23.47921422142005, 23.29841589882617, 22.79469376239716, - 23.437580101042798, 22.90129840984213, 23.377778449705787, 23.152730269355438, - 23.179248710299515, 23.150584655373375, 23.303559153530237, - 23.567343754278223, - 23.14174465613352, 23.236813383632978, 23.178718844944385, - 23.114735241004848], - 'tsp500_test_concorde.txt': [16.43849479258626, 16.30760609977988, 16.55368794754589, 17.0916769200107, - 16.358815620695264, 16.355575136034258, 16.468449176999673, 16.547487678806803, - 16.624118787814286, 16.875851583784797, 16.584382768436186, 16.775629024699168, - 16.625112093123217, 16.537041048883633, 16.211908886171635, 16.507889182815646, - 16.443711824038594, 16.772997858965947, 16.576148488026003, 16.644182889540385, - 16.83104599989968, 16.798687309323867, 16.64786310345603, 16.68678554471238, - 16.539765290816586, 16.158516162147357, 16.750957469266986, 16.454327423569975, - 16.437695592935125, 16.47266324558099, 16.5807314540603, 16.640030608011333, - 16.717644006541413, 16.538629003657803, 16.73424552661684, 16.702691981178777, - 16.4488503948912, 16.65158792760706, 16.21441667652796, 16.58894596771913, - 16.62425057027662, 16.411010231382186, 16.4198250548815, 16.880314028063836, - 16.654445215349824, 16.6703557900618, 16.811423319096434, 16.681548608331166, - 16.40538961977731, 16.375709814617032, 16.4755439381876, 16.352299703304702, - 16.358345088111275, 16.446260979610017, 16.479360821405024, 16.664705227172075, - 16.514514381377964, 16.703418138718607, 16.501081465067912, 16.758043371686597, - 16.529838521968927, 16.331302381910483, 16.769035549248624, 16.667247187672565, - 16.457565298893492, 16.649335805699657, 16.82614018506712, 16.938244810751787, - 16.7896287123959, 16.45162524049444, 16.60657770837926, 16.752028686357416, - 16.538134167181376, 16.419856051838476, 17.056640374302344, 16.763628081715684, - 16.76853264913112, 16.94949524434479, 16.57562195411809, 16.665389374714852, - 16.690740743946513, 16.405456340497622, 16.442597689610583, 16.801813848508267, - 16.670030108101063, 16.62938726279957, 16.23649751271661, 16.69571793825944, - 16.587558708667046, 16.32450912204972, 16.270614173517753, 16.75899873051874, - 16.803321805550524, 16.3602825442514, 16.58252109177151, 16.450516009703893, - 16.35900041167487, 16.637551343677693, 16.572893477964705, 16.73275661200808, - 16.541081653324518, 16.466516697851265, 17.021310751236744, 16.536183906712942, - 16.77678089186245, 16.35713000043851, 16.3183776670553, 16.68224023564231, - 16.672341313126555, 16.607714934366197, 16.634734868495503, 16.674511551735357, - 16.414641537953482, 16.849240225161548, 16.74452644717401, 16.50467692427514, - 16.93072503233582, 16.38341557967758, 16.610910144984917, 16.589115661773096, - 16.366818207481515, 16.599226446198887, 16.349609487246365, 16.38083156520364, - 16.732343248542644, 16.615639804768033, 16.603236295079725, 16.12821378820771]} - normed = {} - for case, (scores, error_message) in results.items(): - if case not in optimal_scores: - continue # Skip if there's no optimal score defined. - optimal_list = optimal_scores[case] - normed_scores = [] - # Compute normalized score for each index. - for idx, score in enumerate(scores): - if isinstance(score, (int, float)): - normed_scores.append(optimal_list[idx] / score) - else: - normed_scores.append(score) - normed[case] = (normed_scores, error_message) - - return normed - - - - - - - - -# Task configuration for benchmark task -ENTRY_NAME = 'solve' -FUNCTION_SIGNATURE = 'def solve(...):' -IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' -TASK_DESCRIPTION = '("The Traveling Salesman Problem (TSP) is a classic combinatorial optimization problem where, "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The Traveling Salesman Problem (TSP) is a classic combinatorial optimization problem where, "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(nodes: list) -> dict:\n """\n Solve a TSP instance.\n Args:\n - nodes (list): List of (x, y) coordinates representing cities in the TSP problem\n Format: [(x1, y1), (x2, y2), ..., (xn, yn)]\n Returns:\n dict: Solution information with:\n - \'tour\' (list): List of node indices representing the solution path\n Format: [0, 3, 1, ...] where numbers are indices into the nodes list\n """\n\n return {\n \'tour\': [],\n }' -EVAL_CLASS_NAME = 'TSPEvaluationCB' -EVAL_KWARGS = {'timeout_seconds': 60} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_travelling_salesman_problem/paras.yaml b/examples/benchmark_tasks/optimization_travelling_salesman_problem/paras.yaml deleted file mode 100644 index ca7f008b..00000000 --- a/examples/benchmark_tasks/optimization_travelling_salesman_problem/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: TSPEvaluationCB -timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_tsp_construct/__init__.py b/examples/benchmark_tasks/optimization_tsp_construct/__init__.py deleted file mode 100644 index 87d91cad..00000000 --- a/examples/benchmark_tasks/optimization_tsp_construct/__init__.py +++ /dev/null @@ -1,259 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_tsp_construct -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# Module Name: TSPEvaluation -# Last Revision: 2025/2/16 -# Description: Evaluates the constructive heuristic for Traveling Salseman Problem (TSP). -# Given a set of locations, -# the goal is to find optimal route to travel all locations and back to start point -# while minimizing the total travel distance. -# This module is part of the LLM4AD project (https://github.com/Optima-CityU/llm4ad). -# -# Parameters: -# - timeout_seconds: Maximum allowed time (in seconds) for the evaluation process: int (default: 30). -# - n_instance: Number of problem instances to generate: int (default: 16). -# - problem_size: Number of customers to serve: int (default: 50). -# -# -# References: -# - Fei Liu, Xialiang Tong, Mingxuan Yuan, and Qingfu Zhang. -# "Algorithm Evolution using Large Language Model." arXiv preprint arXiv:2311.15249 (2023). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- -from __future__ import annotations - -from typing import Any -import numpy as np -from llm4ad_loader import Evaluation -import os, sys -sys.path.insert(0, os.path.dirname(__file__)) -from get_instance import GetData -# from llm4ad.task.optimization.tsp_construct.get_instance import GetData # Converted from LLM4AD import -# from llm4ad.task.optimization.tsp_construct.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\ndef select_next_node(current_node: int, destination_node: int, unvisited_nodes: np.ndarray, distance_matrix: np.ndarray) -> int: \n """\n Design a novel algorithm to select the next node in each step.\n\n Args:\n current_node: ID of the current node.\n destination_node: ID of the destination node.\n unvisited_nodes: Array of IDs of unvisited nodes.\n distance_matrix: Distance matrix of nodes.\n\n Return:\n ID of the next node to visit.\n """\n next_node = unvisited_nodes[0]\n\n return next_node' -task_description = '"Given a set of nodes with their coordinates, you need to find the shortest route that visits each node once and returns to the starting node. \\' - - -__all__ = ['TSPEvaluation'] - - -class TSPEvaluation(Evaluation): - """Evaluator for traveling salesman problem.""" - - def __init__(self, - timeout_seconds=30, - n_instance=16, - problem_size=50, - **kwargs): - - """ - Args: - None - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - self.n_instance = n_instance - self.problem_size = problem_size - getData = GetData(self.n_instance, self.problem_size) - self._datasets = getData.generate_instances() - - def evaluate_program(self, program_str: str, callable_func: callable) -> Any | None: - return self.evaluate(callable_func) - - def tour_cost(self, instance, solution, problem_size): - cost = 0 - for j in range(problem_size - 1): - cost += np.linalg.norm(instance[int(solution[j])] - instance[int(solution[j + 1])]) - cost += np.linalg.norm(instance[int(solution[-1])] - instance[int(solution[0])]) - return cost - - def generate_neighborhood_matrix(self, instance): - instance = np.array(instance) - n = len(instance) - neighborhood_matrix = np.zeros((n, n), dtype=int) - - for i in range(n): - distances = np.linalg.norm(instance[i] - instance, axis=1) - sorted_indices = np.argsort(distances) # sort indices based on distances - neighborhood_matrix[i] = sorted_indices - - return neighborhood_matrix - - def evaluate(self, eva: callable) -> float: - - n_max = self.n_instance - dis = np.ones(self.n_instance) - n_ins = 0 - - for instance, distance_matrix in self._datasets: - - # get neighborhood matrix - neighbor_matrix = self.generate_neighborhood_matrix(instance) - - destination_node = 0 - - current_node = 0 - - route = np.zeros(self.problem_size) - # print(">>> Step 0 : select node "+str(instance[0][0])+", "+str(instance[0][1])) - for i in range(1, self.problem_size - 1): - - near_nodes = neighbor_matrix[current_node][1:] - - mask = ~np.isin(near_nodes, route[:i]) - - unvisited_near_nodes = near_nodes[mask] - - next_node = eva(current_node, destination_node, unvisited_near_nodes, distance_matrix) - - if next_node in route: - # print("wrong algorithm select duplicate node, retrying ...") - return None - - current_node = next_node - - route[i] = current_node - - mask = ~np.isin(np.arange(self.problem_size), route[:self.problem_size - 1]) - - last_node = np.arange(self.problem_size)[mask] - - current_node = last_node[0] - - route[self.problem_size - 1] = current_node - - LLM_dis = self.tour_cost(instance, route, self.problem_size) - - dis[n_ins] = LLM_dis - - n_ins += 1 - if n_ins == self.n_instance: - break - # self.route_plot(instance,route,self.oracle[n_ins]) - - ave_dis = np.average(dis) - # print("average dis: ",ave_dis) - return -ave_dis - - -if __name__ == '__main__': - import sys - - print(sys.path) - - - def select_next_node(current_node: int, destination_node: int, unvisited_nodes: np.ndarray, distance_matrix: np.ndarray) -> int: - """ - Design a novel algorithm to select the next node in each step. - - Args: - current_node: ID of the current node. - destination_node: ID of the destination node. - unvisited_nodes: Array of IDs of unvisited nodes. - distance_matrix: Distance matrix of nodes. - - Return: - ID of the next node to visit. - """ - distances_to_destination = distance_matrix[current_node][unvisited_nodes] - - # Find the index of the unvisited node with the smallest distance to the destination - next_node_index = np.argmin(distances_to_destination) - - # Get the ID of the next node to visit - next_node = unvisited_nodes[next_node_index] - - return next_node - - - tsp = TSPEvaluation() - tsp.evaluate_program('_', select_next_node) - -# Task configuration for benchmark task -ENTRY_NAME = 'select_next_node' -FUNCTION_SIGNATURE = 'def select_next_node(...):' -IMPORT_HEADER = 'import numpy as np\nimport math' -TASK_DESCRIPTION = '"Given a set of nodes with their coordinates, you need to find the shortest route that visits each node once and returns to the starting node. \\' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `select_next_node` for the LLM4AD task.\\n\\nTask description:\\n"Given a set of nodes with their coordinates, you need to find the shortest route that visits each node once and returns to the starting node. \\\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\ndef select_next_node(current_node: int, destination_node: int, unvisited_nodes: np.ndarray, distance_matrix: np.ndarray) -> int: \n """\n Design a novel algorithm to select the next node in each step.\n\n Args:\n current_node: ID of the current node.\n destination_node: ID of the destination node.\n unvisited_nodes: Array of IDs of unvisited nodes.\n distance_matrix: Distance matrix of nodes.\n\n Return:\n ID of the next node to visit.\n """\n next_node = unvisited_nodes[0]\n\n return next_node' -EVAL_CLASS_NAME = 'TSPEvaluation' -EVAL_KWARGS = {'timeout_seconds': 20} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_tsp_construct/get_instance.py b/examples/benchmark_tasks/optimization_tsp_construct/get_instance.py deleted file mode 100644 index 4a08c38e..00000000 --- a/examples/benchmark_tasks/optimization_tsp_construct/get_instance.py +++ /dev/null @@ -1,16 +0,0 @@ -import numpy as np - - -class GetData(): - def __init__(self, n_instance, n_cities): - self.n_instance = n_instance - self.n_cities = n_cities - - def generate_instances(self): - np.random.seed(2024) - instance_data = [] - for _ in range(self.n_instance): - coordinates = np.random.rand(self.n_cities, 2) - distances = np.linalg.norm(coordinates[:, np.newaxis] - coordinates, axis=2) - instance_data.append((coordinates, distances)) - return instance_data diff --git a/examples/benchmark_tasks/optimization_tsp_construct/paras.yaml b/examples/benchmark_tasks/optimization_tsp_construct/paras.yaml deleted file mode 100644 index 2aa2b88a..00000000 --- a/examples/benchmark_tasks/optimization_tsp_construct/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: TSPEvaluation -timeout_seconds: 20 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_tsp_gls_2O/__init__.py b/examples/benchmark_tasks/optimization_tsp_gls_2O/__init__.py deleted file mode 100644 index 1c046093..00000000 --- a/examples/benchmark_tasks/optimization_tsp_gls_2O/__init__.py +++ /dev/null @@ -1,184 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_tsp_gls_2O -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# name: str: TSP_GLS_2O_Evaluation -# Parameters: -# timeout_seconds: int: 20 -# end -from __future__ import annotations - -from typing import Tuple, Any -import numpy as np -from llm4ad_loader import Evaluation -import os, sys -sys.path.insert(0, os.path.dirname(__file__)) -from get_instance import GetData, TSPInstance -# from llm4ad.task.optimization.tsp_gls_2O.get_instance import GetData, TSPInstance # Converted from LLM4AD import -# from llm4ad.task.optimization.tsp_gls_2O.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\ndef update_edge_distance(edge_distance: np.ndarray, local_opt_tour: np.ndarray, edge_n_used: np.ndarray) -> np.ndarray:\n """\n Design a novel algorithm to update the distance matrix.\n\n Args:\n edge_distance: A matrix of the distance.\n local_opt_tour: An array of the local optimal tour of IDs.\n edge_n_used: A matrix of the number of each edge used during permutation.\n\n Return:\n updated_edge_distance: A matrix of the updated distance.\n """\n updated_edge_distance = np.copy(edge_distance)\n\n # Calculate combined importance and frequency factor\n combined_factor = (1 / edge_n_used) + (1 / edge_n_used)\n\n for i in range(len(local_opt_tour) - 1):\n node1 = local_opt_tour[i]\n node2 = local_opt_tour[i + 1]\n\n update_factor = combined_factor[node1, node2]\n\n updated_edge_distance[node1, node2] += update_factor\n updated_edge_distance[node2, node1] = updated_edge_distance[node1, node2]\n\n return updated_edge_distance' -task_description = 'Given an edge distance matrix and a local optimal route, please help me design a strategy to update the distance matrix to avoid being trapped in the local optimum with the final goal of finding a tour with minimized distance. You should create a heuristic for me to update the edge distance matrix.' - -from .gls import guided_local_search_with_time - -__all__ = ['TSP_GLS_2O_Evaluation'] - -perturbation_moves = 5 -iter_limit = 1000 - - -def calculate_cost(inst: TSPInstance, path: np.ndarray) -> float: - # assert (np.sort(path) == np.arange(inst.n)).all(), 'Illegal path' - return inst.distmat[path, np.roll(path, 1)].sum().item() - -def solve_with_time(inst: TSPInstance, eva) -> Tuple[float, float]: - try: - result, running_time = guided_local_search_with_time(inst.distmat, inst.distmat.copy(), eva, perturbation_moves, iter_limit) - cost = calculate_cost(inst, result) - except Exception as e: - # cost, running_time = 1E10, 1E10 - cost, running_time = float("inf"), float("inf") - # print(result) - return cost, running_time - -def evaluate(instance_data,n_ins,prob_size, eva: callable) -> np.ndarray: - objs = np.zeros((n_ins, 2)) - - for i in range(n_ins): - obj = solve_with_time(instance_data[i], eva) - # print(f'{obj[0]}, {obj[1]}') - objs[i] = np.array(obj) - - obj = np.mean(objs, axis=0) - return -obj - - -class TSP_GLS_2O_Evaluation(Evaluation): - """Evaluator for traveling salesman problem.""" - - def __init__(self, **kwargs): - - """ - Args: - None - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=20 - ) - - self.n_instance = 16 - self.problem_size = 100 - getData = GetData(self.n_instance, self.problem_size) - self._datasets = getData.generate_instances() - - def evaluate_program(self, program_str: str, callable_func: callable) -> Any | None: - return evaluate(self._datasets,self.n_instance,self.problem_size, callable_func) - - -if __name__ == '__main__': - import numpy as np - - - def update_edge_distance(edge_distance: np.ndarray, local_opt_tour: np.ndarray, - edge_n_used: np.ndarray) -> np.ndarray: - """ - Design a novel algorithm to update the distance matrix. - - Args: - edge_distance: A matrix of the distance. - local_opt_tour: An array of the local optimal tour of IDs. - edge_n_used: A matrix of the number of each edge used during permutation. - - Return: - updated_edge_distance: A matrix of the updated distance. - """ - updated_edge_distance = np.copy(edge_distance) - - # Calculate combined importance and frequency factor - combined_factor = (1 / edge_n_used) + (1 / edge_n_used) - - for i in range(len(local_opt_tour) - 1): - node1 = local_opt_tour[i] - node2 = local_opt_tour[i + 1] - - update_factor = combined_factor[node1, node2] - - updated_edge_distance[node1, node2] += update_factor - updated_edge_distance[node2, node1] = updated_edge_distance[node1, node2] - - return updated_edge_distance - - tsp = TSP_GLS_2O_Evaluation() - tsp.evaluate_program('_', update_edge_distance) - -# Task configuration for benchmark task -ENTRY_NAME = 'update_edge_distance' -FUNCTION_SIGNATURE = 'def update_edge_distance(...):' -IMPORT_HEADER = 'import numpy as np\nimport math' -TASK_DESCRIPTION = 'Given an edge distance matrix and a local optimal route, please help me design a strategy to update the distance matrix to avoid being trapped in the local optimum with the final goal of finding a tour with minimized distance. You should create a heuristic for me to update the edge distance matrix.' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `update_edge_distance` for the LLM4AD task.\\n\\nTask description:\\nGiven an edge distance matrix and a local optimal route, please help me design a strategy to update the distance matrix to avoid being trapped in the local optimum with the final goal of finding a tour with minimized distance. You should create a heuristic for me to update the edge distance matrix.\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\ndef update_edge_distance(edge_distance: np.ndarray, local_opt_tour: np.ndarray, edge_n_used: np.ndarray) -> np.ndarray:\n """\n Design a novel algorithm to update the distance matrix.\n\n Args:\n edge_distance: A matrix of the distance.\n local_opt_tour: An array of the local optimal tour of IDs.\n edge_n_used: A matrix of the number of each edge used during permutation.\n\n Return:\n updated_edge_distance: A matrix of the updated distance.\n """\n updated_edge_distance = np.copy(edge_distance)\n\n # Calculate combined importance and frequency factor\n combined_factor = (1 / edge_n_used) + (1 / edge_n_used)\n\n for i in range(len(local_opt_tour) - 1):\n node1 = local_opt_tour[i]\n node2 = local_opt_tour[i + 1]\n\n update_factor = combined_factor[node1, node2]\n\n updated_edge_distance[node1, node2] += update_factor\n updated_edge_distance[node2, node1] = updated_edge_distance[node1, node2]\n\n return updated_edge_distance' -EVAL_CLASS_NAME = 'TSP_GLS_2O_Evaluation' -EVAL_KWARGS = {'timeout_seconds': 60} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_tsp_gls_2O/get_instance.py b/examples/benchmark_tasks/optimization_tsp_gls_2O/get_instance.py deleted file mode 100644 index ee49dfbf..00000000 --- a/examples/benchmark_tasks/optimization_tsp_gls_2O/get_instance.py +++ /dev/null @@ -1,23 +0,0 @@ -import numpy as np -import numpy.typing as npt -from scipy.spatial import distance_matrix - - -class GetData(): - def __init__(self,n_instance,n_cities): - self.n_instance = n_instance - self.n_cities = n_cities - - def generate_instances(self): - np.random.seed(2024) - instance_data = [] - for _ in range(self.n_instance): - coordinates = np.random.random((self.n_cities, 2)) - instance_data.append(TSPInstance(coordinates)) - return instance_data - -class TSPInstance: - def __init__(self, positions: npt.NDArray[np.float_]) -> None: - self.positions = positions - self.n = positions.shape[0] - self.distmat = distance_matrix(positions, positions) + np.eye(self.n)*1e-5 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_tsp_gls_2O/gls.py b/examples/benchmark_tasks/optimization_tsp_gls_2O/gls.py deleted file mode 100644 index 379a002f..00000000 --- a/examples/benchmark_tasks/optimization_tsp_gls_2O/gls.py +++ /dev/null @@ -1,226 +0,0 @@ -import time - -import numpy as np -import numpy.typing as npt -import numba as nb -import concurrent.futures -from typing import Tuple - -FloatArray = npt.NDArray[np.float_] -IntArray = npt.NDArray[np.int_] -usecache = True - - -@nb.njit(nb.float32(nb.float32[:,:], nb.uint16[:], nb.uint16), nogil=True, cache = usecache) -def _two_opt_once(distmat, tour, fixed_i = 0): - '''in-place operation''' - n = tour.shape[0] - p = q = 0 - delta = 0 - for i in range(1, n - 1) if fixed_i==0 else range(fixed_i, fixed_i+1): - for j in range(i + 1, n): - node_i, node_j = tour[i], tour[j] - node_prev, node_next = tour[i-1], tour[(j+1) % n] - if node_prev == node_j or node_next == node_i: - continue - change = ( distmat[node_prev, node_j] - + distmat[node_i, node_next] - - distmat[node_prev, node_i] - - distmat[node_j, node_next]) - if change < delta: - p, q, delta = i, j, change - if delta < -1e-6: - tour[p: q+1] = np.flip(tour[p: q+1]) - return delta - else: - return 0.0 - -@nb.njit(nb.float32(nb.float32[:,:], nb.uint16[:], nb.uint16), nogil=True, cache = usecache) -def _relocate_once(distmat, tour, fixed_i = 0): - n = distmat.shape[0] - delta = p = q = 0 - for i in range(1, n) if fixed_i==0 else range(fixed_i, fixed_i+1): - node = tour[i] - prev_node = tour[i-1] - next_node = tour[(i+1)%n] - for j in range(n): - if j == i or j == i-1: - continue - prev_insert = tour[j] - next_insert = tour[(j+1)%n] - cost = ( - distmat[prev_node, node] - - distmat[node, next_node] - - distmat[prev_insert, next_insert] - + distmat[prev_insert, node] - + distmat[node, next_insert] - + distmat[prev_node, next_node] ) - if cost < delta: - delta, p, q = cost, i, j - if delta >= 0: - return 0.0 - if p 0: - delta = 0 - delta += _two_opt_once(distmat, cur_tour, fixed_i) - delta += _relocate_once(distmat, cur_tour, fixed_i) - count -= 1 - sum_delta += delta - return sum_delta - -def _perturbation(distmat, guide, penalty, cur_tour, update_edge_distance, perturbation_moves = 30): - # moves = 0 - n = distmat.shape[0] - # print('distmat', type(distmat), distmat.shape) - # print('cur_tour', type(cur_tour), cur_tour.shape) - # print('penalty', type(penalty), penalty.shape) - - edge_weight_guided = update_edge_distance(distmat, cur_tour, penalty) - edge_weight_guided = np.asmatrix(edge_weight_guided) - edge_weight_gap = edge_weight_guided - distmat - - for i in range(perturbation_moves): - # while moves < perturbation_moves: - # penalize edge - max_indices = np.argmin(-edge_weight_gap, axis=None) - rows, columns = np.unravel_index(max_indices, edge_weight_gap.shape) - penalty[rows, columns] += 1 - penalty[columns, rows] += 1 - edge_weight_gap[rows, columns] = 0 - edge_weight_gap[columns, rows] = 0 - for fixed_i in [rows, columns]: - if fixed_i == 0 or fixed_i + 1 == n: - continue - delta = _local_search(edge_weight_guided, cur_tour, fixed_i, 1) - # if delta < 0: - # moves += 1 - -@nb.njit(nb.uint16[:](nb.float32[:,:], nb.uint16), nogil=True, cache = usecache) -def _init_nearest_neighbor(distmat, start): - n = distmat.shape[0] - tour = np.zeros(n, dtype=np.uint16) - visited = np.zeros(n, dtype=np.bool_) - visited[start] = True - tour[0] = start - for i in range(1, n): - min_dist = np.inf - min_idx = -1 - for j in range(n): - if not visited[j] and distmat[tour[i-1], j] < min_dist: - min_dist = distmat[tour[i-1], j] - min_idx = j - tour[i] = min_idx - visited[min_idx] = True - return tour - - -def _guided_local_search( - distmat, guide, start, update_edge_distance, perturbation_moves = 30, iter_limit = 1000 -) -> npt.NDArray[np.uint16]: - penalty = np.zeros_like(distmat) - start_time = time.monotonic() - best_tour = _init_nearest_neighbor(distmat, start) - _local_search(distmat, best_tour, 0, 1000) - best_cost = _calculate_cost(distmat, best_tour) - # k = 0.1 * best_cost / distmat.shape[0] - cur_tour = best_tour.copy() - - for _ in range(iter_limit): - _perturbation(distmat, guide, penalty, cur_tour, update_edge_distance, perturbation_moves) - _local_search(distmat, cur_tour, 0, 1000) - cur_cost = _calculate_cost(distmat, cur_tour) - if cur_cost < best_cost: - best_tour, best_cost = cur_tour.copy(), cur_cost - if time.monotonic() - start_time > 60: - break - return best_tour - - -def _guided_local_search_with_time( - distmat, guide, start, update_edge_distance, perturbation_moves = 30, iter_limit = 1000 -) -> Tuple[npt.NDArray[np.uint16], float]: - penalty = np.zeros_like(distmat) - start_time = time.monotonic() - best_tour = _init_nearest_neighbor(distmat, start) - _local_search(distmat, best_tour, 0, 1000) - best_cost = _calculate_cost(distmat, best_tour) - # k = 0.1 * best_cost / distmat.shape[0] - cur_tour = best_tour.copy() - - for _ in range(iter_limit): - _perturbation(distmat, guide, penalty, cur_tour, update_edge_distance, perturbation_moves) - _local_search(distmat, cur_tour, 0, 1000) - cur_cost = _calculate_cost(distmat, cur_tour) - if cur_cost < best_cost: - best_tour, best_cost = cur_tour.copy(), cur_cost - running_time = time.monotonic() - start_time - return best_tour, running_time - -def guided_local_search( - distmat: FloatArray, - guide: FloatArray, - update_edge_distance, - perturbation_moves: int = 30, - iter_limit: int = 1000 -) -> npt.NDArray[np.uint16]: - return _guided_local_search( - distmat.astype(np.float32), - guide.astype(np.float32), - 0, - update_edge_distance, - perturbation_moves=perturbation_moves, - iter_limit=iter_limit, - ) - -def guided_local_search_with_time( - distmat: FloatArray, - guide: FloatArray, - update_edge_distance, - perturbation_moves: int = 30, - iter_limit: int = 1000 -) -> Tuple[npt.NDArray[np.uint16], float]: - return _guided_local_search_with_time( - distmat.astype(np.float32), - guide.astype(np.float32), - 0, - update_edge_distance, - perturbation_moves=perturbation_moves, - iter_limit=iter_limit, - ) - -def multi_start_guided_local_search( - dist: FloatArray, - guide: FloatArray, - n_starts: int = 10, - perturbation_moves = 30, - iter_limit = 1000 -): - dist = dist.astype(np.float32) - guide = guide.astype(np.float32) - start_nodes = np.arange(n_starts).astype(np.uint16) - - with concurrent.futures.ThreadPoolExecutor() as executor: - futures = [] - for start in start_nodes: - future = executor.submit(_guided_local_search, dist, guide, start, perturbation_moves = perturbation_moves, iter_limit = iter_limit) - futures.append(future) - tours = [f.result() for f in futures] - # Calculate costs and return the best tour - costs = np.array([_calculate_cost(dist, tour) for tour in tours]) - best_tour = tours[np.argmin(costs)] - return best_tour \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_tsp_gls_2O/paras.yaml b/examples/benchmark_tasks/optimization_tsp_gls_2O/paras.yaml deleted file mode 100644 index 6c0c111d..00000000 --- a/examples/benchmark_tasks/optimization_tsp_gls_2O/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: TSP_GLS_2O_Evaluation -timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_uncapacitated_warehouse_location/__init__.py b/examples/benchmark_tasks/optimization_uncapacitated_warehouse_location/__init__.py deleted file mode 100644 index 53d40a53..00000000 --- a/examples/benchmark_tasks/optimization_uncapacitated_warehouse_location/__init__.py +++ /dev/null @@ -1,349 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_uncapacitated_warehouse_location -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# References: -# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language -# model agents in algorithm search for combinatorial optimization. -# arXiv preprint arXiv:2504.04310 (2025). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -from typing import Any -import numpy as np -from llm4ad_loader import Evaluation -from llm4ad_loader import load_subdir_as_text -# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader -# from llm4ad.task.optimization.co_bench.uncapacitated_warehouse_location_co_bench.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(m: int, n: int, warehouses: list, customers: list) -> dict:\n """\n Solves the Uncapacitated Warehouse Location Problem.\n Input kwargs:\n - m: Number of potential warehouses (int)\n - n: Number of customers (int)\n - warehouses: A list of dictionaries, each with keys:\n \'fixed_cost\': Fixed cost for opening the warehouse.\n - customers: A list of dictionaries, each with keys:\n \'costs\': A list of floats representing the cost of assigning the entire customer to each warehouse.\n Evaluation Metric:\n The objective is to minimize the total cost, computed as:\n (Sum of fixed costs for all open warehouses)\n + (Sum of assignment costs for each customer assigned to a warehouse)\n Each customer must be assigned entirely to exactly one open warehouse.\n If a solution violates this constraint (i.e., a customer is unassigned or is assigned to more than one warehouse), then the solution is considered infeasible and no score is provided.\n Returns:\n A dictionary with the following keys:\n \'total_cost\': (float) The computed objective value (cost) if the solution is feasible; otherwise, no score is provided.\n \'warehouse_open\': (list of int) A list of m integers (0 or 1) indicating whether each warehouse is closed or open.\n \'assignments\': (list of list of int) A 2D list (n x m) where each entry is 1 if customer i is assigned to warehouse j, and 0 otherwise.\n """\n ## placeholder. You do not need to write anything here.\n return {\n "total_cost": 0.0,\n "warehouse_open": [0] * kwargs["m"],\n "assignments": [[0] * kwargs["m"] for _ in range(kwargs["n"])]\n }' -task_description = '("The Uncapacitated Warehouse Location Problem aims to determine which warehouses to open and how "' - - -__all__ = ['UWLEvaluationCB'] - - -class UWLEvaluationCB(Evaluation): - - def __init__(self, - timeout_seconds=50, - **kwargs): - - """ - Args: - None - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - # Load datasets from Hugging Face - dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Uncapacitated warehouse location") - self._datasets = {} - for filename in dataset: - # Join all text rows into a single string - text_content = '\n'.join([row['text'] for row in dataset[filename]]) - self._datasets[filename] = text_content - - def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: - return self.evaluate(callable_func) - - def evaluate(self, eva: callable) -> float | None: - ins_cases = [] - for case_id, ins in enumerate(self._datasets.values()): - ins_cases.append(self.load_data(ins)) - - fitness_list = [] - try: - for i in ins_cases: - for j in i: - result = eva(j['m'], j['n'], j['warehouses'], j['customers']) - fitness = self.eval_func(j['m'], j['n'], j['warehouses'], j['customers'], result['warehouse_open'], result['assignments']) - fitness_list.append(fitness) - - return -np.mean(fitness_list) - - except ValueError as e: - print(e) - return None - - def load_data(self, input_string): - """ - Reads one or more problem cases from the input file. - Expected Input File Format for each case: - Line 1: Two integers: m n - Next m lines: Each line contains two numbers: capacity fixed_cost for a warehouse. - Next n lines: Each line contains: demand (a number) followed by m numbers representing the cost of - allocating the customer's demand to each warehouse. - If the input file contains multiple cases, the cases appear sequentially in the file. - Returns: - A list of dictionaries, each corresponding to one case. Each dictionary has the keys: - - 'm': Number of potential warehouses (int) - - 'n': Number of customers (int) - - 'warehouses': List of dictionaries; each with keys 'capacity' and 'fixed_cost' - - 'customers': List of dictionaries; each with keys 'demand' and 'costs' (list of floats) - """ - try: - all_lines = [line.strip() for line in input_string.split('\n')] - except Exception as e: - raise ValueError("Error reading input file: " + str(e)) - - # Tokenize all non-empty lines. - tokens = [] - for line in all_lines: - line = line.strip() - if line: - tokens.extend(line.split()) - - cases = [] - index = 0 - total_tokens = len(tokens) - - # Process tokens until we have exhausted them. - while index < total_tokens: - if index + 1 >= total_tokens: - raise ValueError("Insufficient tokens to read m and n for a case.") - try: - m = int(tokens[index]) - n = int(tokens[index + 1]) - except Exception as e: - raise ValueError("Error parsing m or n: " + str(e)) - index += 2 - - # Parse warehouse data (m warehouses, each with 2 tokens). - expected_warehouse_tokens = m * 2 - if index + expected_warehouse_tokens - 1 >= total_tokens: - raise ValueError("Not enough tokens for warehouse data in a case.") - warehouses = [] - for i in range(m): - try: - capacity = float(tokens[index]) - fixed_cost = float(tokens[index + 1]) - except Exception as e: - raise ValueError("Error parsing warehouse data: " + str(e)) - warehouses.append({'capacity': capacity, 'fixed_cost': fixed_cost}) - index += 2 - - # Parse customer data (n customers, each with 1 demand and m cost values). - customers = [] - for j in range(n): - if index >= total_tokens: - raise ValueError(f"Not enough tokens for customer {j + 1} demand.") - try: - demand = float(tokens[index]) - except Exception as e: - raise ValueError(f"Error parsing demand for customer {j + 1}: " + str(e)) - index += 1 - if index + m - 1 >= total_tokens: - raise ValueError(f"Not enough tokens for cost data for customer {j + 1}.") - costs = [] - for i in range(m): - try: - cost = float(tokens[index]) - except Exception as e: - raise ValueError(f"Error parsing cost for customer {j + 1}, warehouse {i + 1}: " + str(e)) - costs.append(cost) - index += 1 - customers.append({'demand': demand, 'costs': costs}) - - case_data = {"m": m, "n": n, "warehouses": warehouses, "customers": customers} - cases.append(case_data) - - return cases - - def eval_func(self, m, n, warehouses, customers, warehouse_open, assignments, **kwargs): - """ - Evaluates the solution for the Uncapacitated Warehouse Location Problem. - For each customer: - - The customer must be assigned to exactly one open warehouse. - - The assignment cost is the cost associated with the warehouse to which the customer is assigned. - - No assignment is allowed for a warehouse that is closed. - The total cost is computed as: - (Sum of fixed costs for all open warehouses) - + (Sum of assignment costs for all customers) - Input Parameters: - - m: Number of potential warehouses (int) - - n: Number of customers (int) - - warehouses: List of dictionaries, each with keys: - 'fixed_cost': The fixed cost for opening the warehouse. - 'capacity': Provided but ignored in this problem. - - customers: List of dictionaries, each with keys: - 'costs': A list of floats representing the cost of assigning the customer entirely to each warehouse. - 'demand': Provided but ignored in this problem. - - warehouse_open: List of m integers (0 or 1) indicating whether each warehouse is closed or open. - - assignments: List of n lists (each of length m) where assignments[j][i] is 1 if customer j is assigned to warehouse i, and 0 otherwise. - - kwargs: Other parameters (not used here). - Returns: - A floating-point number representing the total cost if the solution is feasible. - Raises: - Exception: If any of the following conditions are violated: - - The sum of assignments for any customer is not exactly 1. - - Any positive assignment is made to a closed warehouse. - - Any assignment value is not binary (0 or 1). - """ - computed_total_cost = 0.0 - - # Add fixed costs for open warehouses. - for i in range(m): - if warehouse_open[i] == 1: - computed_total_cost += warehouses[i]['fixed_cost'] - - # Evaluate assignment cost for each customer. - for j in range(n): - # Sum of assignments for customer j should be exactly 1. - assigned_sum = sum(assignments[j]) - if abs(assigned_sum - 1.0) > 1e-6: - raise Exception( - f"Customer {j} assignment violation: total assigned value {assigned_sum} does not equal 1." - ) - - customer_cost = 0.0 - for i in range(m): - allocation = assignments[j][i] - # Ensure the assignment is binary (allowing for small floating point tolerance) - if not (abs(allocation) < 1e-6 or abs(allocation - 1.0) < 1e-6): - raise Exception( - f"Customer {j} has a non-binary assignment value {allocation} for warehouse {i + 1}." - ) - if allocation > 0: - if warehouse_open[i] != 1: - raise Exception( - f"Customer {j} is assigned to warehouse {i + 1}, which is closed." - ) - # Since assignment is binary, add the corresponding cost. - customer_cost += customers[j]['costs'][i] - computed_total_cost += customer_cost - - return computed_total_cost - - def norm_score(self, results): - optimal_scores = { - "cap71.txt": [932615.750], - "cap72.txt": [977799.400], - "cap73.txt": [1010641.450], - "cap74.txt": [1034976.975], - "cap101.txt": [796648.437], - "cap102.txt": [854704.200], - "cap103.txt": [893782.112], - "cap104.txt": [928941.750], - "cap131.txt": [793439.562], - "cap132.txt": [851495.325], - "cap133.txt": [893076.712], - "cap134.txt": [928941.750], - "capa.txt": [17156454.478], - "capb.txt": [12979071.582], - "capc.txt": [11505594.329] - } - - normed = {} - for case, (scores, error_message) in results.items(): - if case not in optimal_scores: - continue # Skip if there's no optimal score defined. - optimal_list = optimal_scores[case] - normed_scores = [] - # Compute normalized score for each index. - for idx, score in enumerate(scores): - if isinstance(score, (int, float)): - normed_scores.append(optimal_list[idx] / score) - else: - normed_scores.append(score) - normed[case] = (normed_scores, error_message) - - return normed - - def get_dev(self): - dev = {'cap101.txt': [], 'cap103.txt': [], - 'cap131.txt': [], - 'cap133.txt': [], - 'cap71.txt': [], 'cap73.txt': [], - 'capb.txt': []} - - return dev - - -# Task configuration for benchmark task -ENTRY_NAME = 'solve' -FUNCTION_SIGNATURE = 'def solve(...):' -IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' -TASK_DESCRIPTION = '("The Uncapacitated Warehouse Location Problem aims to determine which warehouses to open and how "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The Uncapacitated Warehouse Location Problem aims to determine which warehouses to open and how "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(m: int, n: int, warehouses: list, customers: list) -> dict:\n """\n Solves the Uncapacitated Warehouse Location Problem.\n Input kwargs:\n - m: Number of potential warehouses (int)\n - n: Number of customers (int)\n - warehouses: A list of dictionaries, each with keys:\n \'fixed_cost\': Fixed cost for opening the warehouse.\n - customers: A list of dictionaries, each with keys:\n \'costs\': A list of floats representing the cost of assigning the entire customer to each warehouse.\n Evaluation Metric:\n The objective is to minimize the total cost, computed as:\n (Sum of fixed costs for all open warehouses)\n + (Sum of assignment costs for each customer assigned to a warehouse)\n Each customer must be assigned entirely to exactly one open warehouse.\n If a solution violates this constraint (i.e., a customer is unassigned or is assigned to more than one warehouse), then the solution is considered infeasible and no score is provided.\n Returns:\n A dictionary with the following keys:\n \'total_cost\': (float) The computed objective value (cost) if the solution is feasible; otherwise, no score is provided.\n \'warehouse_open\': (list of int) A list of m integers (0 or 1) indicating whether each warehouse is closed or open.\n \'assignments\': (list of list of int) A 2D list (n x m) where each entry is 1 if customer i is assigned to warehouse j, and 0 otherwise.\n """\n ## placeholder. You do not need to write anything here.\n return {\n "total_cost": 0.0,\n "warehouse_open": [0] * kwargs["m"],\n "assignments": [[0] * kwargs["m"] for _ in range(kwargs["n"])]\n }' -EVAL_CLASS_NAME = 'UWLEvaluationCB' -EVAL_KWARGS = {'timeout_seconds': 60} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_uncapacitated_warehouse_location/paras.yaml b/examples/benchmark_tasks/optimization_uncapacitated_warehouse_location/paras.yaml deleted file mode 100644 index b39dfbce..00000000 --- a/examples/benchmark_tasks/optimization_uncapacitated_warehouse_location/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: UWLEvaluationCB -timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_unconstrained_guillotine_cutting/__init__.py b/examples/benchmark_tasks/optimization_unconstrained_guillotine_cutting/__init__.py deleted file mode 100644 index 6db09c3b..00000000 --- a/examples/benchmark_tasks/optimization_unconstrained_guillotine_cutting/__init__.py +++ /dev/null @@ -1,376 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_unconstrained_guillotine_cutting -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# References: -# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language -# model agents in algorithm search for combinatorial optimization. -# arXiv preprint arXiv:2504.04310 (2025). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -from typing import Any -import numpy as np -from llm4ad_loader import Evaluation -from llm4ad_loader import load_subdir_as_text -# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader -# from llm4ad.task.optimization.co_bench.unconstrained_guillotine_cutting_co_bench.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(m: int, stock_width: int, stock_height: int, pieces: dict, allow_rotation: bool = False) -> dict:\n """\n Solves the unconstrained guillotine cutting problem.\n Given a stock rectangle (with dimensions \'stock_width\' and \'stock_height\') and a set of pieces\n (provided as a dictionary \'pieces\' mapping each piece_id to its specification {\'l\', \'w\', \'value\'}),\n the goal is to select and place some pieces (each used at most once) within the stock rectangle.\n If the keyword argument \'allow_rotation\' is True, each piece may be placed in its original orientation or rotated 90° (swapping its dimensions);\n otherwise, pieces must be placed in their original orientation. In all cases, placements must not overlap and must lie entirely within the stock.\n Input kwargs:\n - m (int): Number of available pieces.\n - stock_width (int): The width of the stock rectangle.\n - stock_height (int): The height of the stock rectangle.\n - pieces (dict): A dictionary mapping piece_id (1-indexed) to a dict with keys:\n \'l\' (length), \'w\' (width), and \'value\' (value of the piece).\n - allow_rotation (bool): Indicates whether a piece is allowed to be rotated 90°.\n Evaluation metric:\n The performance is measured as the total value of the placed pieces (sum of individual values).\n Returns:\n A dictionary with a key "placements" whose value is a list.\n Each element in the list is a dictionary representing a placement with keys:\n - piece_id (int): Identifier of the placed piece.\n - x (int): x-coordinate of the bottom-left corner in the stock rectangle.\n - y (int): y-coordinate of the bottom-left corner in the stock rectangle.\n - orientation (int): 0 for original orientation; 1 if rotated 90° (only applicable if allow_rotation is True, otherwise default to 0).\n NOTE: This is a placeholder function. Replace the body with an actual algorithm if desired.\n """\n ## placeholder. You do not need to write anything here.\n return {"placements": []}' -task_description = '("The unconstrained guillotine cutting problem involves selecting and placing a subset of "' - - -__all__ = ['UGCEvaluationCB'] - - -class UGCEvaluationCB(Evaluation): - - def __init__(self, - timeout_seconds=50, - **kwargs): - - """ - Args: - None - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - # Load datasets from Hugging Face - dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Unconstrained guillotine cutting") - self._datasets = {} - for filename in dataset: - # Join all text rows into a single string - text_content = '\n'.join([row['text'] for row in dataset[filename]]) - self._datasets[filename] = text_content - - def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: - return self.evaluate(callable_func) - - def evaluate(self, eva: callable) -> float | None: - ins_cases = [] - for case_id, ins in enumerate(self._datasets.values()): - ins_cases.append(self.load_data(ins)) - - fitness_list = [] - try: - for i in ins_cases: - for j in i: - result = eva(j['m'], j['stock_width'], j['stock_height'], j['pieces'], j['allow_rotation']) - fitness = self.eval_func(m=j['m'], stock_width=j['stock_width'], stock_height=j['stock_height'], pieces=j['pieces'], placements=result['placements']) - fitness_list.append(fitness) - - return np.mean(fitness_list) # itself is a maximize problem - - except ValueError as e: - print(e) - return None - - def load_data(self, input_string): - """ - Loads one or more problem cases from the input file. - The input is expected to contain one or more cases. - Each case has the following format: - - Line 1: An integer m (number of pieces). - - Line 2: Two integers: stock_width and stock_height. - - Next m lines: Each line contains three space-separated integers: l, w, value. - Cases are concatenated one after the other (ignoring blank lines). - Parameters: - input_path (str): Path to the input file. - Returns: - list: A list of dictionaries. Each dictionary corresponds to one case and contains: - - "m" (int): number of pieces. - - "stock_width" (int): width of the stock rectangle. - - "stock_height" (int): height of the stock rectangle. - - "pieces" (dict): mapping from piece_id (1-indexed) to a dict with keys 'l', 'w', 'value'. - """ - lines = [line.strip() for line in input_string.split('\n') if line.strip() != ''] - - cases = [] - idx = 0 - total_lines = len(lines) - while idx < total_lines: - # Read the number of pieces for the current case. - try: - m = int(lines[idx]) - except Exception: - raise ValueError(f"Invalid number of pieces at line {idx + 1}") - idx += 1 - - if idx >= total_lines: - raise ValueError("Missing stock dimensions for a case.") - - # Read stock rectangle dimensions. - stock_parts = lines[idx].split() - if len(stock_parts) != 2: - raise ValueError(f"Stock dimensions must consist of two integers at line {idx + 1}") - try: - stock_width, stock_height = map(int, stock_parts) - except Exception: - raise ValueError(f"Stock dimensions must be integers at line {idx + 1}") - idx += 1 - - # Read m piece specifications. - pieces = {} - for i in range(m): - if idx >= total_lines: - raise ValueError(f"Not enough piece specifications for case starting at line {idx + 1}") - parts = lines[idx].split() - if len(parts) < 3: - raise ValueError(f"Piece {i + 1} specification is incomplete at line {idx + 1}") - try: - l, w, value = map(int, parts[:3]) - except Exception: - raise ValueError(f"Piece {i + 1} contains non-integer data at line {idx + 1}") - pieces[i + 1] = {'l': l, 'w': w, 'value': value} - idx += 1 - - case = { - "m": m, - "stock_width": stock_width, - "stock_height": stock_height, - "pieces": pieces, - "allow_rotation": False, # Default value since we can't determine from string - } - cases.append(case) - - return cases - - def eval_func(self, **kwargs): - """ - Evaluates a candidate solution for the guillotine cutting problem. - This function computes the total value of the placed pieces while enforcing - the following constraints by raising errors when violated: - 1. Each placement must be entirely within the stock rectangle. - 2. Placements must not overlap. - 3. Each piece may be used at most once. - 4. Each placement must have a valid orientation (0 or 1). - Parameters (passed as keyword arguments): - - m (int): Number of pieces. - - stock_width (int): Width of the stock rectangle. - - stock_height (int): Height of the stock rectangle. - - pieces (dict): Dictionary mapping piece_id to {'l', 'w', 'value'}. - - placements (list): List of placements, where each placement is a dict with keys: - 'piece_id', 'x', 'y', 'orientation'. - Returns: - float: Total value of the placed pieces if all constraints are met. - Raises: - ValueError: If any of the constraints (format, boundary, overlap, duplicate usage, or orientation) - are violated. - """ - try: - m = kwargs["m"] - stock_width = kwargs["stock_width"] - stock_height = kwargs["stock_height"] - pieces = kwargs["pieces"] - placements = kwargs.get("placements", []) - except KeyError as e: - raise ValueError(f"Missing required input parameter: {e}") - - total_value = 0.0 - used_piece_ids = set() - rects = [] - - # Process each placement. - for placement in placements: - try: - piece_id = int(placement["piece_id"]) - x = int(placement["x"]) - y = int(placement["y"]) - orientation = int(placement["orientation"]) - except Exception as e: - raise ValueError(f"Invalid placement format: {placement}. Error: {e}") - - if piece_id not in pieces: - raise ValueError(f"Piece id {piece_id} not found in pieces.") - - # Check for duplicate usage. - if piece_id in used_piece_ids: - raise ValueError(f"Duplicate usage of piece id {piece_id}.") - used_piece_ids.add(piece_id) - - # Check orientation. - if orientation not in (0, 1): - raise ValueError(f"Invalid orientation {orientation} for piece id {piece_id}; must be 0 or 1.") - - # Determine effective dimensions based on orientation. - if orientation == 0: - p_width = pieces[piece_id]['l'] - p_height = pieces[piece_id]['w'] - else: - p_width = pieces[piece_id]['w'] - p_height = pieces[piece_id]['l'] - - # Check boundaries. - if x < 0 or y < 0 or (x + p_width) > stock_width or (y + p_height) > stock_height: - raise ValueError(f"Placement of piece id {piece_id} is out of stock boundaries.") - - total_value += pieces[piece_id]['value'] - - # Record rectangle for later overlap checks. - rects.append({ - "x": x, - "y": y, - "width": p_width, - "height": p_height - }) - - # Helper function to compute overlapping area between two rectangles. - def overlap_area(r1, r2): - x_overlap = max(0, min(r1["x"] + r1["width"], r2["x"] + r2["width"]) - max(r1["x"], r2["x"])) - y_overlap = max(0, min(r1["y"] + r1["height"], r2["y"] + r2["height"]) - max(r1["y"], r2["y"])) - return x_overlap * y_overlap - - # Check for overlapping pieces. - n_rects = len(rects) - for i in range(n_rects): - for j in range(i + 1, n_rects): - if overlap_area(rects[i], rects[j]) > 0: - raise ValueError("Overlapping detected between placements.") - - return total_value - - def norm_score(self, results): - optimal_scores = { - "gcut1.txt": [56460], - "gcut2.txt": [60536], - "gcut3.txt": [61036], - "gcut4.txt": [61698], - "gcut5.txt": [246000], - "gcut6.txt": [238998], - "gcut7.txt": [242567], - "gcut8.txt": [246633], - "gcut9.txt": [971100], - "gcut10.txt": [982025], - "gcut11.txt": [980096], - "gcut12.txt": [979986], - "gcut13.txt": [8997780], - "gcut1r.txt": [58136], - "gcut2r.txt": [60611], - "gcut3r.txt": [61626], - "gcut4r.txt": [62265], - "gcut5r.txt": [246000], - "gcut6r.txt": [240951], - "gcut7r.txt": [245866], - "gcut8r.txt": [247787], - "gcut9r.txt": [971100], - "gcut10r.txt": [982025], - "gcut11r.txt": [980096], - "gcut12r.txt": [988694], - "gcut13r.txt": [9000000], - } - - normed = {} - for case, (scores, error_message) in results.items(): - if case not in optimal_scores: - continue # Skip if there's no optimal score defined. - optimal_list = optimal_scores[case] - normed_scores = [] - # Compute normalized score for each index. - for idx, score in enumerate(scores): - if isinstance(score, (int, float)): - normed_scores.append(score / optimal_list[idx]) - else: - normed_scores.append(score) - normed[case] = (normed_scores, error_message) - - return normed - - def get_dev(self): - dev = {'gcut1.txt': [], 'gcut10r.txt': [], 'gcut11.txt': [], - 'gcut12r.txt': [], 'gcut13.txt': [], 'gcut2r.txt': [], - 'gcut3.txt': [], 'gcut4r.txt': [], 'gcut5.txt': [], - 'gcut6r.txt': [], 'gcut7r.txt': [], 'gcut8r.txt': [], - 'gcut9.txt': [], } - - return dev - - - -# Task configuration for benchmark task -ENTRY_NAME = 'solve' -FUNCTION_SIGNATURE = 'def solve(...):' -IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' -TASK_DESCRIPTION = '("The unconstrained guillotine cutting problem involves selecting and placing a subset of "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The unconstrained guillotine cutting problem involves selecting and placing a subset of "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(m: int, stock_width: int, stock_height: int, pieces: dict, allow_rotation: bool = False) -> dict:\n """\n Solves the unconstrained guillotine cutting problem.\n Given a stock rectangle (with dimensions \'stock_width\' and \'stock_height\') and a set of pieces\n (provided as a dictionary \'pieces\' mapping each piece_id to its specification {\'l\', \'w\', \'value\'}),\n the goal is to select and place some pieces (each used at most once) within the stock rectangle.\n If the keyword argument \'allow_rotation\' is True, each piece may be placed in its original orientation or rotated 90° (swapping its dimensions);\n otherwise, pieces must be placed in their original orientation. In all cases, placements must not overlap and must lie entirely within the stock.\n Input kwargs:\n - m (int): Number of available pieces.\n - stock_width (int): The width of the stock rectangle.\n - stock_height (int): The height of the stock rectangle.\n - pieces (dict): A dictionary mapping piece_id (1-indexed) to a dict with keys:\n \'l\' (length), \'w\' (width), and \'value\' (value of the piece).\n - allow_rotation (bool): Indicates whether a piece is allowed to be rotated 90°.\n Evaluation metric:\n The performance is measured as the total value of the placed pieces (sum of individual values).\n Returns:\n A dictionary with a key "placements" whose value is a list.\n Each element in the list is a dictionary representing a placement with keys:\n - piece_id (int): Identifier of the placed piece.\n - x (int): x-coordinate of the bottom-left corner in the stock rectangle.\n - y (int): y-coordinate of the bottom-left corner in the stock rectangle.\n - orientation (int): 0 for original orientation; 1 if rotated 90° (only applicable if allow_rotation is True, otherwise default to 0).\n NOTE: This is a placeholder function. Replace the body with an actual algorithm if desired.\n """\n ## placeholder. You do not need to write anything here.\n return {"placements": []}' -EVAL_CLASS_NAME = 'UGCEvaluationCB' -EVAL_KWARGS = {'timeout_seconds': 300} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_unconstrained_guillotine_cutting/paras.yaml b/examples/benchmark_tasks/optimization_unconstrained_guillotine_cutting/paras.yaml deleted file mode 100644 index 0f17f3c5..00000000 --- a/examples/benchmark_tasks/optimization_unconstrained_guillotine_cutting/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: UGCEvaluationCB -timeout_seconds: 300 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_vehicle_routing_period_routing/__init__.py b/examples/benchmark_tasks/optimization_vehicle_routing_period_routing/__init__.py deleted file mode 100644 index b9ebda77..00000000 --- a/examples/benchmark_tasks/optimization_vehicle_routing_period_routing/__init__.py +++ /dev/null @@ -1,469 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_vehicle_routing_period_routing -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# References: -# - Sun, W., Feng, S., Li, S., & Yang, Y. Co-bench: benchmarking language -# model agents in algorithm search for combinatorial optimization. -# arXiv preprint arXiv:2504.04310 (2025). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -import os, sys -sys.path.insert(0, os.path.dirname(__file__)) -import ast -from typing import Any -import numpy as np -from llm4ad_loader import Evaluation -from llm4ad_loader import load_subdir_as_text -# from llm4ad.task.optimization.co_bench.utils import load_subdir_as_text # Common utilities from llm4ad_loader -# from llm4ad.task.optimization.co_bench.vehicle_routing_period_routing_co_bench.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(depot: dict, customers: list, vehicles_per_day: list, vehicle_capacity: float, period_length: int) -> dict:\n """\n Solves an instance of the Period Vehicle Routing Problem.\n Input kwargs includes:\n - depot: dict with keys:\n "id": int, always 0.\n "x": float, the x-coordinate.\n "y": float, the y-coordinate.\n - customers: list of dictionaries (with customer id ≠ 0) having keys:\n "id": int, the customer id.\n "x": float, the x-coordinate.\n "y": float, the y-coordinate.\n "demand": numeric, the customer demand.\n "schedules": list of candidate schedules, each a list (of length period_length) with binary entries.\n - vehicles_per_day: list of ints (length period_length) indicating the number of vehicles available each day.\n - vehicle_capacity: numeric, the capacity of each vehicle.\n - period_length: int, the number of days in the planning period.\n The solution must decide:\n 1. Which service schedule (from the candidate schedules) is selected for each customer.\n 2. For each day (days are 1-indexed), the daily tours: a list of tours—one per available vehicle.\n Each tour is a continuous route that starts at the depot (0), visits some customers (each exactly once),\n and returns to the depot. The depot may only appear as the first and last vertex in each tour.\n The number of tours for day d must be exactly equal to vehicles_per_day[d-1].\n The returned solution is a dictionary containing:\n - "selected_schedules": dict mapping each customer id (integer) to the chosen schedule (a list of binary integers).\n - "tours": dict mapping day (an integer between 1 and period_length) to a list of tours.\n Each tour is a list of vertex ids (integers), starting and ending at the depot (id 0).\n """\n # ------------------------------\n\n return {\n "selected_schedules": ...,\n "tours": ...\n }' -task_description = '("The Period Vehicle Routing Problem requires planning delivery routes over a multi‐day planning "' - - -__all__ = ['VRPREvaluationCB'] - - -class VRPREvaluationCB(Evaluation): - - def __init__(self, - timeout_seconds=50, - **kwargs): - - """ - Args: - None - Raises: - AttributeError: If the data key does not exist. - FileNotFoundError: If the specified data file is not found. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - # Load datasets from Hugging Face - dataset = load_subdir_as_text("CO-Bench/CO-Bench", "Vehicle routing: period routing") - self._datasets = {} - for filename in dataset: - # Join all text rows into a single string - text_content = '\n'.join([row['text'] for row in dataset[filename]]) - self._datasets[filename] = text_content - - def evaluate_program(self, program_str: str, callable_func: callable, **kwargs) -> Any | None: - return self.evaluate(callable_func) - - def evaluate(self, eva: callable) -> float | None: - ins_cases = [] - for case_id, ins in enumerate(self._datasets.values()): - ins_cases.append(self.load_data(ins)) - - fitness_list = [] - try: - for i in ins_cases: - for j in i: - result = eva(j['depot'], j['costumers'], j['vehicles_per_day'], j['vehicle_capacity'], j['period_length']) - fitness = self.eval_func(depot=j['depot'], customers=j['costumers'], vehicles_per_day=j['vehicles_per_day'], vehicle_capacity=j['vehicle_capacity'], period_length=j['period_length'], selected_schedules=result['selected_schedules'], tours=result['tours']) - fitness_list.append(fitness) - - return -np.mean(fitness_list) - - except ValueError as e: - print(e) - return None - - def load_data(self, input_string): - """ - Reads a period vehicle routing problem file and returns a dictionary with the problem data. - The file is expected to have the following format: - Line 1: Two integers: - (Note: the depot is specified as customer_id = 0.) - Line 2: A list of period_length integers representing the number of vehicles on each day. - Line 3: A single number representing the constant capacity of every vehicle. - Lines 4 onward: Each line represents a vertex (depot or customer) in the format: - customer_id x_coordinate y_coordinate demand possible_schedule_list - For the depot (customer_id = 0) the demand and schedule are omitted or ignored. - e.g., depot line: 0 30 40 0 - customer line: 1 37 52 7 [[1, 0], [0, 1]] - Parameters: - input_string (str): The input content as string. - Returns: - A dictionary with keys: - - "period_length" (int) - - "vehicles_per_day" (list of ints) - - "vehicle_capacity" (number) - - "depot": dict with keys: "id", "x", "y" - - "customers": list of customer dictionaries (for customer id ≠ 0) - Each customer dictionary contains keys: - "id": int, the customer id. - "x": float, the x coordinate. - "y": float, the y coordinate. - "demand": float, the customer demand. - "schedules": list of lists, each sub-list is a binary schedule for the period. - """ - - # Read file and filter out any empty lines. - all_lines = [line.strip() for line in input_string.split('\n')] - - # Check that we have at least 3 lines for headers. - if len(all_lines) < 3: - raise ValueError("Insufficient data in the file. Expect at least three header lines.") - - # Parse header - # First line: number of customers and period length: - header1 = all_lines[0].split() - if len(header1) != 2: - print(header1) - raise ValueError("The first line must have exactly 2 tokens: .") - try: - num_customers = int(header1[0]) - period_length = int(header1[1]) - except Exception as e: - raise ValueError("Error parsing the number of customers or period length.") from e - - # Second line: number of vehicles on each day - vehicles_tokens = all_lines[1].split() - if len(vehicles_tokens) != period_length: - raise ValueError("The number of vehicle counts provided does not equal the period length.") - try: - vehicles_per_day = [int(x) for x in vehicles_tokens] - except Exception as e: - raise ValueError("Error parsing the vehicles per day.") from e - - # Third line: vehicle capacity (all vehicles have same capacity) - try: - vehicle_capacity = float(all_lines[2]) - except Exception as e: - raise ValueError("Error parsing vehicle capacity.") from e - - depot = None - customers = [] - # Process the remaining lines. - for line in all_lines[3:]: - # Split into at most five tokens; the first four are assumed to be id, x, y and demand. - parts = line.split(maxsplit=4) - if len(parts) < 3: - continue # Skip lines that do not have minimum required data. - - try: - cid = int(parts[0]) - x = float(parts[1]) - y = float(parts[2]) - except Exception as ex: - raise ValueError("Error parsing id or coordinates in line: " + line) from ex - - # Check for depot (id == 0). For depot, we ignore demand and schedule. - if cid == 0: - depot = {"id": cid, "x": x, "y": y} - # Skip further processing of demand/schedules for the depot. - continue - - # For a customer, we expect a demand value. - if len(parts) < 4: - raise ValueError("Insufficient data for customer (id=%s) in line: %s" % (cid, line)) - try: - demand = float(parts[3]) - except Exception as ex: - raise ValueError("Error parsing demand for customer (id=%s) in line: %s" % (cid, line)) from ex - - # Parse possible schedule if provided. - schedules = [] - if len(parts) == 5: - try: - schedules = ast.literal_eval(parts[4]) - except Exception as ex: - raise ValueError("Error parsing delivery schedules in line: " + line) from ex - - customers.append({ - "id": cid, - "x": x, - "y": y, - "demand": demand, - "schedules": schedules - }) - - # Optionally, you can check if depot was found. - if depot is None: - raise ValueError("Depot (customer id 0) was not found in the file.") - - return [{ - "period_length": period_length, - "vehicles_per_day": vehicles_per_day, - "vehicle_capacity": vehicle_capacity, - "depot": depot, - "customers": customers - }] - - def eval_func(self, **kwargs): - """ - Evaluates the solution of the Period Vehicle Routing Problem for a single case. - Input kwargs should include: - - from data: - "depot": dict with keys "id", "x", "y". - "customers": list of customer dictionaries (each with keys "id", "x", "y", "demand", "schedules"). - "vehicles_per_day": list of ints (indicating the number of available vehicles per day). - "vehicle_capacity": numeric, the capacity of each vehicle. - "period_length": int, the number of days. - - from solve: - "selected_schedules": a mapping from customer id to the chosen schedule (a list of binary integers). - "tours": a mapping from day (1-indexed) to a list of tours; - each tour is a list of vertex ids (integers), starting and ending at depot (id 0), - with no intermediate depot visits. - The evaluator checks the following: - 1. For each customer (other than the depot), verifies that there is a chosen schedule, - and that the chosen schedule is one of that customer's candidate schedules. - 2. For each day: - - Verifies that the number of tours does not exceed the available vehicles for that day. - - Checks that every customer whose chosen schedule requires service is visited exactly once. - 3. Each tour must: - - Start at the depot (id 0) and end at the depot (id 0). - - Not include any depot visit in the middle (the depot may appear only as the first and the last vertex). - - Not visit the same customer more than once. - 4. Each tour must satisfy the capacity constraint: the total customer demand on the tour does not exceed vehicle_capacity. - 5. Finally, the evaluator computes the total tour length (using Euclidean distance) over all days. - Returns: - A numeric value representing the total tour length computed from the solution. - Raises an error if any constraint is violated. - """ - import math - - depot = kwargs["depot"] - customers = kwargs["customers"] - vehicles_per_day = kwargs["vehicles_per_day"] - vehicle_capacity = kwargs["vehicle_capacity"] - period_length = kwargs["period_length"] - - # Build a lookup table for customers by id. - customer_lookup = {cust["id"]: cust for cust in customers} - - # Validate the selected schedules. - selected_schedules = kwargs.get("selected_schedules") - if not isinstance(selected_schedules, dict): - raise ValueError("Solution must include a dictionary 'selected_schedules'.") - - # Ensure that every customer (except the depot) has a selected schedule. - for cust in customers: - # Assuming depot has id 0. - if cust["id"] == 0: - continue - if cust["id"] not in selected_schedules: - raise ValueError(f"Missing selected schedule for customer {cust['id']}.") - - # Now validate each provided schedule. - for cid, sel_sched in selected_schedules.items(): - cust = customer_lookup.get(cid) - if cust is None: - raise ValueError(f"Customer id {cid} in selected_schedules not found in customer list.") - if sel_sched not in cust["schedules"]: - raise ValueError( - f"Selected schedule {sel_sched} for customer {cid} is not among candidate schedules {cust['schedules']}.") - if len(sel_sched) != period_length: - raise ValueError(f"Selected schedule for customer {cid} does not match period_length {period_length}.") - - # Process tours for each day. - tours = kwargs.get("tours") - if not isinstance(tours, dict): - raise ValueError("Solution must include a dictionary 'tours'.") - - total_length = 0.0 - - def euclidean(a, b): - return math.sqrt((a["x"] - b["x"]) ** 2 + (a["y"] - b["y"]) ** 2) - - # Evaluate each day. - for day in range(1, period_length + 1): - # Validate the number of tours does not exceed the available vehicles. - tours_day = tours.get(day, []) - vehicles_available = vehicles_per_day[day - 1] - if len(tours_day) > vehicles_available: - raise ValueError( - f"On day {day}: Number of tours ({len(tours_day)}) exceeds available vehicles ({vehicles_available}).") - - # Determine all customers that should receive service today. - expected_customers = set() - for cust in customers: - if cust["id"] == 0: - continue - sched = selected_schedules.get(cust["id"]) - if sched is not None and sched[day - 1] == 1: - expected_customers.add(cust["id"]) - - visited_today = [] - for tour in tours_day: - # A valid tour must have at least depot, one customer, and depot again. - if len(tour) < 3: - raise ValueError(f"Tour {tour} on day {day} is too short.") - # Check that the tour starts and ends with the depot. - if tour[0] != 0 or tour[-1] != 0: - raise ValueError(f"Tour {tour} on day {day} must start and end at the depot (id 0).") - # Ensure no depot visits occur in the middle. - if 0 in tour[1:-1]: - raise ValueError(f"Tour {tour} on day {day} contains an extra depot visit in the middle.") - - seen_in_tour = set() - # Process customer visits in the tour (excluding depot at the beginning and end). - for vid in tour[1:-1]: - if vid in seen_in_tour: - raise ValueError(f"Tour on day {day} visits customer {vid} more than once.") - seen_in_tour.add(vid) - visited_today.append(vid) - - # Check the capacity constraint for the tour. - capacity_used = sum(customer_lookup[vid]["demand"] for vid in tour[1:-1]) - if capacity_used > vehicle_capacity: - raise ValueError( - f"Tour on day {day} exceeds capacity: used {capacity_used}, capacity is {vehicle_capacity}.") - - # Compute the tour's travel distance. - tour_length = 0.0 - prev = depot - for vid in tour[1:]: - curr = depot if vid == 0 else customer_lookup.get(vid) - if curr is None: - raise ValueError(f"Customer id {vid} in tour on day {day} not found.") - tour_length += euclidean(prev, curr) - prev = curr - total_length += tour_length - - # Ensure that the visited customers exactly match those expected for the day. - if set(visited_today) != expected_customers: - missing = expected_customers - set(visited_today) - extra = set(visited_today) - expected_customers - err_msg = f"On day {day}: " - if missing: - # Only showing a sample of missing customers - err_msg += f"Missing visits for customers such as {list(missing)[:10]}. " - if extra: - err_msg += f"Extra visits for customers {list(extra)}." - raise ValueError(err_msg) - - return total_length - - def norm_score(self, results): - optimal_scores = { - "prvp1.txt": [547.9], - "prvp2.txt": [1487.6], - "prvp3.txt": [550.1], - "prvp4.txt": [872.3], - "prvp5.txt": [2207.9], - "prvp6.txt": [965.7], - "prvp7.txt": [839.2], - "prvp8.txt": [2294.2], - "prvp9.txt": [925.0], - "prvp10.txt": [1819.2], - } - - normed = {} - for case, (scores, error_message) in results.items(): - if case not in optimal_scores: - continue # Skip if there's no optimal score defined. - optimal_list = optimal_scores[case] - normed_scores = [] - # Compute normalized score for each index. - for idx, score in enumerate(scores): - if isinstance(score, (int, float)): - normed_scores.append(optimal_list[idx] / score) - else: - normed_scores.append(score) - normed[case] = (normed_scores, error_message) - - return normed - - def get_dev(self): - dev = {'prvp1.txt': [], 'prvp3.txt': [], 'prvp5.txt': [], - 'prvp7.txt': [], 'prvp9.txt': []} - - return dev - - - - - - - - - -# Task configuration for benchmark task -ENTRY_NAME = 'solve' -FUNCTION_SIGNATURE = 'def solve(...):' -IMPORT_HEADER = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict' -TASK_DESCRIPTION = '("The Period Vehicle Routing Problem requires planning delivery routes over a multi‐day planning "' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `solve` for the LLM4AD task.\\n\\nTask description:\\n("The Period Vehicle Routing Problem requires planning delivery routes over a multi‐day planning "\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\nimport scipy.optimize as opt\nimport math\nimport random\nfrom typing import List, Tuple, Dict\ndef solve(depot: dict, customers: list, vehicles_per_day: list, vehicle_capacity: float, period_length: int) -> dict:\n """\n Solves an instance of the Period Vehicle Routing Problem.\n Input kwargs includes:\n - depot: dict with keys:\n "id": int, always 0.\n "x": float, the x-coordinate.\n "y": float, the y-coordinate.\n - customers: list of dictionaries (with customer id ≠ 0) having keys:\n "id": int, the customer id.\n "x": float, the x-coordinate.\n "y": float, the y-coordinate.\n "demand": numeric, the customer demand.\n "schedules": list of candidate schedules, each a list (of length period_length) with binary entries.\n - vehicles_per_day: list of ints (length period_length) indicating the number of vehicles available each day.\n - vehicle_capacity: numeric, the capacity of each vehicle.\n - period_length: int, the number of days in the planning period.\n The solution must decide:\n 1. Which service schedule (from the candidate schedules) is selected for each customer.\n 2. For each day (days are 1-indexed), the daily tours: a list of tours—one per available vehicle.\n Each tour is a continuous route that starts at the depot (0), visits some customers (each exactly once),\n and returns to the depot. The depot may only appear as the first and last vertex in each tour.\n The number of tours for day d must be exactly equal to vehicles_per_day[d-1].\n The returned solution is a dictionary containing:\n - "selected_schedules": dict mapping each customer id (integer) to the chosen schedule (a list of binary integers).\n - "tours": dict mapping day (an integer between 1 and period_length) to a list of tours.\n Each tour is a list of vertex ids (integers), starting and ending at the depot (id 0).\n """\n # ------------------------------\n\n return {\n "selected_schedules": ...,\n "tours": ...\n }' -EVAL_CLASS_NAME = 'VRPREvaluationCB' -EVAL_KWARGS = {'timeout_seconds': 60} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_vehicle_routing_period_routing/paras.yaml b/examples/benchmark_tasks/optimization_vehicle_routing_period_routing/paras.yaml deleted file mode 100644 index ca482783..00000000 --- a/examples/benchmark_tasks/optimization_vehicle_routing_period_routing/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: VRPREvaluationCB -timeout_seconds: 60 \ No newline at end of file diff --git a/examples/benchmark_tasks/optimization_vrptw_construct/__init__.py b/examples/benchmark_tasks/optimization_vrptw_construct/__init__.py deleted file mode 100644 index 5574218c..00000000 --- a/examples/benchmark_tasks/optimization_vrptw_construct/__init__.py +++ /dev/null @@ -1,279 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: optimization_vrptw_construct -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# Module Name: VRPTWEvaluation -# Last Revision: 2025/2/16 -# Description: Evaluates the Vehicle Routing Problem with Time Windows (VRPTW). -# The VRPTW involves finding optimal routes for a fleet of vehicles to serve a set of customers, -# respecting time windows and vehicle capacity constraints. -# This module is part of the LLM4AD project (https://github.com/Optima-CityU/llm4ad). -# -# Parameters: -# - timeout_seconds: Maximum allowed time (in seconds) for the evaluation process: int (default: 30). -# - problem_size: Number of customers to serve (excluding the depot): int (default: 50). -# - n_instance: Number of problem instances to generate: int (default: 16). -# -# References: -# - Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - -from __future__ import annotations - -from typing import Any -import os, sys -sys.path.insert(0, os.path.dirname(__file__)) -import copy -import numpy as np -from llm4ad_loader import Evaluation -from get_instance import GetData -# from llm4ad.task.optimization.vrptw_construct.get_instance import GetData # Converted from LLM4AD import -# from llm4ad.task.optimization.vrptw_construct.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'def select_next_node(current_node: int, depot: int, unvisited_nodes: np.ndarray, rest_capacity: np.ndarray, current_time: np.ndarray,\\\n demands: np.ndarray, distance_matrix: np.ndarray, time_windows: np.ndarray) -> int:\n """Design a novel algorithm to select the next node in each step.\n Args:\n current_node: ID of the current node.\n depot: ID of the depot.\n unvisited_nodes: Array of IDs of unvisited nodes.\n rest_capacity: Rest capacity of vehicle\n current_time: Current time\n demands: Demands of nodes\n distance_matrix: Distance matrix of nodes.\n time_windows: Time windows of nodes.\n Return:\n ID of the next node to visit.\n """\n next_node = unvisited_nodes[0]\n return next_node' -task_description = 'The task involves finding optimal routes for a fleet of vehicles to serve a set of customers, respecting time windows and vehicle capacity constraints. Help me design an algorithm to select the next node in each step.' - - - -class VRPTWEvaluation(Evaluation): - def __init__(self, - timeout_seconds=30, - problem_size=50, - n_instance=16, - **kwargs): - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - self.problem_size = problem_size - self.n_instance = n_instance - - getData = GetData(self.n_instance, self.problem_size + 1) - self._datasets = getData.generate_instances() - - def tour_cost(self, distance_matrix, solution, time_service, time_windows): - cost = 0 - current_time = 0 - - for j in range(len(solution) - 1): - travel_time = distance_matrix[int(solution[j]), int(solution[j + 1])] - # print(current_time) - current_time += travel_time - - if current_time < time_windows[solution[j + 1]][0]: - current_time = time_windows[solution[j + 1]][0] - if max(current_time, time_windows[solution[j + 1]][0]) > time_windows[solution[j + 1]][1]: - # print(max(current_time ,time_windows[solution[j + 1]][0])+time_service[solution[j + 1]] ) - # print(time_windows[solution[j + 1]][1]) - return float('inf') # Exceeds time window - current_time += time_service[solution[j + 1]] - cost += travel_time - if (solution[j + 1] == 0): - current_time = 0 - return cost - - def evaluate_program(self, program_str: str, callable_func: callable) -> Any | None: - return self.evaluate(callable_func) - - def evaluate(self, heuristic): - dis = np.ones(self.n_instance) - n_ins = 0 - - for instance, distance_matrix, demands, vehicle_capacity, time_service, time_windows in self._datasets: - route = [] - current_load = 0 - current_node = 0 - current_time = 0 - route.append(current_node) - unvisited_nodes = set(range(1, self.problem_size + 1)) # Assuming node 0 is the depot - all_nodes = np.array(list(unvisited_nodes)) - feasible_unvisited_nodes = all_nodes - - unvisited_nodes_depot = np.array(list(unvisited_nodes)) - - while unvisited_nodes: - - next_node = heuristic(current_node, - 0, - feasible_unvisited_nodes, - vehicle_capacity - current_load, - current_time, - copy.deepcopy(demands), - copy.deepcopy(distance_matrix), - copy.deepcopy(time_windows)) - if next_node == 0: - route.append(next_node) - current_load = 0 - current_time = 0 - current_node = 0 - unvisited_nodes_depot = np.array(list(unvisited_nodes)) - else: - travel_time = distance_matrix[current_node, next_node] - current_time += (travel_time) - current_time = max(current_time, time_windows[next_node][0]) - current_time += time_service[next_node] - # if current_time < time_windows[next_node][0]: - # current_time = time_windows[next_node][0] - # if current_time > time_windows[next_node][1]: - # print(current_time) - # print(time_windows[next_node][1]) - # return float('inf') # Exceeds time window - route.append(next_node) - current_load += demands[next_node] - unvisited_nodes.remove(next_node) - current_node = next_node - unvisited_nodes_depot = np.append(np.array(list(unvisited_nodes)), 0) - - feasible_nodes_tw = np.array([node for node in all_nodes \ - if max(current_time + distance_matrix[current_node][node], time_windows[node][0]) < time_windows[node][1] - 0.0001 \ - and max(current_time + distance_matrix[current_node][node], time_windows[node][0]) + time_service[node] + distance_matrix[node][0] < time_windows[0][1] - 0.0001]) - feasible_nodes_capacity = np.array([node for node in all_nodes if current_load + demands[node] <= vehicle_capacity]) - # Determine feasible and unvisited nodes - feasible_unvisited_nodes = np.intersect1d(np.intersect1d(feasible_nodes_tw, feasible_nodes_capacity), list(unvisited_nodes)) - - if len(unvisited_nodes) > 0 and len(feasible_unvisited_nodes) < 1: - route.append(0) - current_load = 0 - current_time = 0 - current_node = 0 - feasible_unvisited_nodes = np.array(list(unvisited_nodes)) - - # print(set(route)) - - if len(set(route)) != self.problem_size + 1: - return None - - LLM_dis = self.tour_cost(distance_matrix, route, time_service, time_windows) - dis[n_ins] = LLM_dis - - n_ins += 1 - if n_ins == self.n_instance: - break - # print(dis) - ave_dis = np.average(dis) - return -ave_dis - - -if __name__ == '__main__': - def select_next_node(current_node: int, depot: int, unvisited_nodes: np.ndarray, rest_capacity: np.ndarray, current_time: np.ndarray, demands: np.ndarray, distance_matrix: np.ndarray, time_windows: np.ndarray) -> int: - """Design a novel algorithm to select the next node in each step. - Args: - current_node: ID of the current node. - depot: ID of the depot. - unvisited_nodes: Array of IDs of unvisited nodes. - rest_capacity: Rest capacity of vehicle - current_time: Current time - demands: Demands of nodes - distance_matrix: Distance matrix of nodes. - time_windows: Time windows of nodes. - Return: - ID of the next node to visit. - """ - best_node = -1 - best_value = -float('inf') - - for node in unvisited_nodes: - if demands[node] <= rest_capacity: - travel_time = distance_matrix[current_node, node] - arrival_time = current_time + travel_time - - if arrival_time <= time_windows[node][1]: # Checking if within time window - wait_time = max(0, time_windows[node][0] - arrival_time) - effective_time = arrival_time + wait_time - distance_to_demand_ratio = travel_time / demands[node] if demands[node] > 0 else float('inf') - - if distance_to_demand_ratio > best_value: - best_value = distance_to_demand_ratio - best_node = node - - return best_node if best_node != -1 else depot - - - eval = VRPTWEvaluation() - res = eval.evaluate_program('', select_next_node) - print(res) - -# Task configuration for benchmark task -ENTRY_NAME = 'select_next_node' -FUNCTION_SIGNATURE = 'def select_next_node(...):' -IMPORT_HEADER = 'import numpy as np\nimport math' -TASK_DESCRIPTION = 'The task involves finding optimal routes for a fleet of vehicles to serve a set of customers, respecting time windows and vehicle capacity constraints. Help me design an algorithm to select the next node in each step.' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `select_next_node` for the LLM4AD task.\\n\\nTask description:\\nThe task involves finding optimal routes for a fleet of vehicles to serve a set of customers, respecting time windows and vehicle capacity constraints. Help me design an algorithm to select the next node in each step.\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'def select_next_node(current_node: int, depot: int, unvisited_nodes: np.ndarray, rest_capacity: np.ndarray, current_time: np.ndarray,\\\n demands: np.ndarray, distance_matrix: np.ndarray, time_windows: np.ndarray) -> int:\n """Design a novel algorithm to select the next node in each step.\n Args:\n current_node: ID of the current node.\n depot: ID of the depot.\n unvisited_nodes: Array of IDs of unvisited nodes.\n rest_capacity: Rest capacity of vehicle\n current_time: Current time\n demands: Demands of nodes\n distance_matrix: Distance matrix of nodes.\n time_windows: Time windows of nodes.\n Return:\n ID of the next node to visit.\n """\n next_node = unvisited_nodes[0]\n return next_node' -EVAL_CLASS_NAME = 'VRPTWEvaluation' -EVAL_KWARGS = {'timeout_seconds': 20} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/optimization_vrptw_construct/get_instance.py b/examples/benchmark_tasks/optimization_vrptw_construct/get_instance.py deleted file mode 100644 index 11667a34..00000000 --- a/examples/benchmark_tasks/optimization_vrptw_construct/get_instance.py +++ /dev/null @@ -1,64 +0,0 @@ -import pickle - -import numpy as np - - -class GetData: - def __init__(self, n_instance, n_cities): - self.n_instance = n_instance - self.n_cities = n_cities - self.max_time = 4.6 - - def generate_instances(self): - """each instance -> (coordinates, distances, demands, capacity)""" - np.random.seed(2024) - instance_data = [] - for _ in range(self.n_instance): - coordinates = np.random.rand(self.n_cities + 1, 2) - demands = np.append(np.array([0]), np.random.randint(1, 10, size=self.n_cities)) - capacity = 40 - distances = np.linalg.norm(coordinates[:, np.newaxis] - coordinates, axis=2) - node_serviceTime = np.random.rand(self.n_cities) * 0.05 + 0.15 - serviceTime = np.append(np.array([0]), node_serviceTime) - # shape: (batch, problem) - # range: (0.15, 0.2) for T=4.6 - - node_lengthTW = np.random.rand(self.n_cities) * 0.05 + 0.15 - # shape: (batch, problem) - # range: (0.15, 0.2) for T=4.6 - - d0i = distances[0][1:] - # shape: (batch, problem) - - # ei = (np.random.rand(self.n_cities) * ((self.max_time - node_serviceTime - node_lengthTW) / d0i - 1) + 1) - ei = np.random.rand(self.n_cities) * (((4.6 * np.ones(self.n_cities) - node_serviceTime - node_lengthTW) / d0i - 1) - 1) + 1 - # shape: (batch, problem) - # default velocity = 1.0 - - # Element-wise multiplication - node_earlyTW = np.multiply(ei, d0i) - # node_earlyTW = ei * d0i - # shape: (batch, problem) - # default velocity = 1.0 - - node_lateTW = node_earlyTW + node_lengthTW - # shape: (batch, problem) - - time_windows_node = np.append(np.array([node_earlyTW]).reshape(self.n_cities, 1), np.array([node_lateTW]).reshape(self.n_cities, 1), axis=1) - - time_windows = np.append(np.array([[0, self.max_time]]), time_windows_node, axis=0) - - instance_data.append((coordinates, distances, demands, capacity, serviceTime, time_windows)) - return instance_data - - -if __name__ == '__main__': - gd = GetData(10, 50) - data = gd.generate_instances() - with open('data_vrptw.pkl', 'wb') as f: - pickle.dump(data, f) - with open('data_vrptw.pkl', 'rb') as f: - data = pickle.load(f) - coordinates, distances, demands, capacity, serviceTime, time_windows = data[0] - print(time_windows) - print(time_windows[0]) diff --git a/examples/benchmark_tasks/optimization_vrptw_construct/paras.yaml b/examples/benchmark_tasks/optimization_vrptw_construct/paras.yaml deleted file mode 100644 index 72c258f8..00000000 --- a/examples/benchmark_tasks/optimization_vrptw_construct/paras.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: VRPTWEvaluation -timeout_seconds: 20 \ No newline at end of file diff --git a/examples/benchmark_tasks/science_discovery_ode_1d/__init__.py b/examples/benchmark_tasks/science_discovery_ode_1d/__init__.py deleted file mode 100644 index 7ded0d44..00000000 --- a/examples/benchmark_tasks/science_discovery_ode_1d/__init__.py +++ /dev/null @@ -1,258 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: science_discovery_ode_1d -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -# Module Name: ODEEvaluation -# Last Revision: 2025/3/5 -# Description: Provides the skeleton for an ODE mathematical function based on given initial data. -# The function is designed to be differentiable and continuous, using only a limited -# set of selectable components. This module is part of the LLM4AD project -# (https://github.com/Optima-CityU/llm4ad). -# -# Parameters: -# - x: float - initial value of the ODE formula (default: None). -# - params: np.ndarray - 1D array of numeric constants or parameters to be optimized (default: None). -# - timeout_seconds: int - Maximum allowed time (in seconds) for the evaluation process (default: 20). -# -# References: -# - Du, Mengge, et al. "Llm4ed: Large language models for automatic equation discovery." -# arXiv preprint arXiv:2405.07761 (2024). -# -# ------------------------------- Copyright -------------------------------- -# Copyright (c) 2025 Optima Group. -# -# Permission is granted to use the LLM4AD platform for research purposes. -# All publications, software, or other works that utilize this platform -# or any part of its codebase must acknowledge the use of "LLM4AD" and -# cite the following reference: -# -# Fei Liu, Rui Zhang, Zhuoliang Xie, Rui Sun, Kai Li, Xi Lin, Zhenkun Wang, -# Zhichao Lu, and Qingfu Zhang, "LLM4AD: A Platform for Algorithm Design -# with Large Language Model," arXiv preprint arXiv:2412.17287 (2024). -# -# For inquiries regarding commercial use or licensing, please contact -# http://www.llm4ad.com/contact.html -# -------------------------------------------------------------------------- - - -from __future__ import annotations - -import os, sys -sys.path.insert(0, os.path.dirname(__file__)) -import re -import itertools -from typing import Any -import numpy as np - -from llm4ad_loader import Evaluation -# from llm4ad.task.science_discovery.ode_1d.template import template_program, task_description # Template values embedded below - -# Embedded template values -template_program = 'import numpy as np\n\ndef equation(x: float, params: np.ndarray) -> float:\n """ A ODE mathematical function \n Args:\n x: the initial float value of the ode formula\n params: a 1-d Array of numeric constants or parameters to be optimized\n\n Return:\n A numpy array representing the result of applying the mathematical function to the inputs.\n """\n y = params[0] * x + params[2]\n return y' -task_description = '("Find the ODE mathematical function skeleton, given data on initial x. The function should be differentiable, continuous."' - -from ode_1d import strogatz_extended, strogatz_equations -# from llm4ad.task.science_discovery.ode_1d import strogatz_extended, strogatz_equations # Converted from LLM4AD import - -__all__ = ['ODEEvaluation'] - -MAX_NPARAMS = 10 -params = [1.0] * MAX_NPARAMS - -local_dict = { - "np.e": "sp.E", - "np.pi": "sp.pi", - "np.arcsin": "sp.asin", - "np.arccos": "sp.acos", - "np.arctan": "sp.atan", - "np.sin": "sp.sin", - "np.cos": "sp.cos", - "np.tan": "sp.tan", - "np.sign": "sp.sign", - "np.sqrt": "sp.sqrt", - "np.log": "sp.log", - "np.exp": "sp.exp", -} - - -def evaluate(program_str: str, data: dict, equation: callable) -> float | None: - """ Evaluate the equation on data observations.""" - - # Load data observations - xs = np.array(data['xs']) - ts = np.array(data['t']) - ys = np.array(list(itertools.chain(*data['ys']))) # flatten to 1d - num_ini_x_values = len(xs) - num_variables = len(xs[0]) - - try: # initial x(0) = x0 - # t = sp.symbols('t') # time variable t - # x0 = sp.Function('x0')(t) # x(t) is the unknown formula about t - # constants = [sp.symbols(f'c{i}') for i in range(MAX_NPARAMS)] # constants symbol - - program_str = re.sub(r"def equation\(", r"def equation(t: float, ", program_str) - local_vars = {"equation": equation} - exec(program_str, globals(), local_vars) - equation = local_vars['equation'] # replace equation with str that after replacement of key parts - - # formula_sympy = equation(x0, constants) - # diff_eq = sp.Eq(sp.diff(x0, t), formula_sympy) - - # calculate the values of 2 initial x0 value - # solution_with_initial = sp.dsolve(diff_eq, ics={x0.subs(t, 0): xs[0][0]}) - # x0_solution = solution_with_initial.rhs # extract the expression of right part - # x0_func = sp.lambdify([t, constants], x0_solution, 'numpy') - except Exception as e: - # print(e) - return None - - # Optimize parameters based on data - from scipy.optimize import minimize - from scipy.integrate import solve_ivp - def loss(params): - y_pred = np.zeros(num_ini_x_values * len(ts[0])) - for i in range(num_ini_x_values): - s = solve_ivp(equation, (ts[i][0], ts[i][-1]), xs[i], args=(params,), t_eval=ts[i]) - y_pred[i * len(ts[0]):(i + 1) * len(ts[0])] = s['y'][0] - return np.mean((y_pred - ys) ** 2) - - # x0_funcs = [] - # for i in range(num_ini_x_values): - # solution_with_initial = sp.dsolve(diff_eq, ics={x0.subs(t, 0): xs[i][0]}) - # x0_solution = solution_with_initial.rhs # extract the expression of right part - # x0_func = sp.lambdify([t, constants], x0_solution, 'numpy') - # - # x0_funcs.append(x0_func) - - loss_partial = lambda params: loss(params) - result = minimize(loss_partial, [1.0] * MAX_NPARAMS, method='BFGS') - - # Return evaluation score - optimized_params = result.x - loss = result.fun - - if np.isnan(loss) or np.isinf(loss): - return None - else: - return -loss - - -class ODEEvaluation(Evaluation): - - def __init__(self, timeout_seconds=200000, test_id=1, **kwargs): - """ - Args: - timeout_seconds: evaluate time limit. - test_id: test equation id ranges from [1, 16]. - """ - - super().__init__( - template_program=template_program, - task_description=task_description, - use_numba_accelerate=False, - timeout_seconds=timeout_seconds - ) - - # read files - test_eq_dict = strogatz_equations.equations[test_id - 1] - dataset = strogatz_extended.data - - dataset = dataset[test_id - 1] - xs = dataset['init'] - t = [e['t'] for e in dataset['solutions'][0]] - ys = [e['y'][0] for e in dataset['solutions'][0]] # for only 1 output - self._datasets = { - 'xs': xs, - 'ys': ys, - 't': t - } - - def evaluate_program(self, program_str: str, callable_func: callable) -> Any | None: - import inspect - if not program_str: - program_str = inspect.getsource(callable_func).lstrip() # for testing - # for np_func, sp_func in local_dict.items(): # replace key parts - # program_str = program_str.replace(np_func, sp_func) - return evaluate(program_str, self._datasets, callable_func) - - -if __name__ == '__main__': - def equation(x: float, params: np.ndarray) -> float: - """ A ODE mathematical function - Args: - x: the initial float value of the ode formula - params: a 1-d Array of numeric constants or parameters to be optimized - - Return: - A numpy array representing the result of applying the mathematical function to the inputs. - """ - y = params[0] * np.sin(x) + params[1] - return y - - - evaluation = ODEEvaluation() - res = evaluation.evaluate_program('', equation) - print(res) - -# Task configuration for benchmark task -ENTRY_NAME = 'equation' -FUNCTION_SIGNATURE = 'def equation(...):' -IMPORT_HEADER = 'import numpy as np\nimport math' -TASK_DESCRIPTION = '("Find the ODE mathematical function skeleton, given data on initial x. The function should be differentiable, continuous."' -OBJECTIVE_TEXT = 'You are optimizing the implementation of `equation` for the LLM4AD task.\\n\\nTask description:\\n("Find the ODE mathematical function skeleton, given data on initial x. The function should be differentiable, continuous."\\n\\nYour goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.' -TEMPLATE_FUNCTION = 'import numpy as np\n\ndef equation(x: float, params: np.ndarray) -> float:\n """ A ODE mathematical function \n Args:\n x: the initial float value of the ode formula\n params: a 1-d Array of numeric constants or parameters to be optimized\n\n Return:\n A numpy array representing the result of applying the mathematical function to the inputs.\n """\n y = params[0] * x + params[2]\n return y' -EVAL_CLASS_NAME = 'ODEEvaluation' -EVAL_KWARGS = {'test_id (1-16)': 1, 'timeout_seconds': 20} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {FUNCTION_SIGNATURE}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) diff --git a/examples/benchmark_tasks/science_discovery_ode_1d/paras.yaml b/examples/benchmark_tasks/science_discovery_ode_1d/paras.yaml deleted file mode 100644 index 405b2700..00000000 --- a/examples/benchmark_tasks/science_discovery_ode_1d/paras.yaml +++ /dev/null @@ -1,3 +0,0 @@ -name: FeynmanEvaluation -test_id (1-16): 1 -timeout_seconds: 20 \ No newline at end of file diff --git a/examples/benchmark_tasks/science_discovery_ode_1d/strogatz_equations.py b/examples/benchmark_tasks/science_discovery_ode_1d/strogatz_equations.py deleted file mode 100644 index 0feda3c0..00000000 --- a/examples/benchmark_tasks/science_discovery_ode_1d/strogatz_equations.py +++ /dev/null @@ -1,223 +0,0 @@ -""" -A selection of ordinary differential equations primarily from Steven Strogatz's book "Nonlinear Dynamics and Chaos" with manually chosen parameter values and initial conditions. -Some other famous known systems have been selected from other sources, which are included in the dictionary entries as well. -We selected ODEs primarily based on whether they have actually been suggested as models for real-world phenomena as well as on whether they are 'iconic' ODEs in the sense that they are often used as examples in textbooks and/or have recognizable names. -Whenever there were 'realistic' parameter values suggested, we chose those. -In this benchmark, we typically include only one set of parameter values per equation. -Many of the ODEs in Strogatz' book are analyzed in terms of the different limiting behavior for different parameter settings. -For some systems that exhibit wildely different behavior for different parameter settings, we include multiple sets of parameter values as separate equations (e.g., Lorenz system in chaotic and non-chaotic regime). -For each equation, we include two sets of manually chosen initial conditions. -There are 23 equations with dimension 1, 28 equations with dimension 2, 10 equation with dimension 3, and 2 equations with dimension 4. -This results in a total of 63 equations, 4 of which display chaotic behavior. -""" - -equations = [ - { - 'id': 1, - 'eq': '(c_0 - x_0 / c_1) / c_2', - 'dim': 1, - 'consts': [[0.7, 1.2, 2.31]], - 'init': [[10.], [3.54]], - 'init_constraints': 'x_0 > 0', - 'const_constraints': 'c_1 > 0, c_2 > 0', - 'eq_description': 'RC-circuit (charging capacitor)', - 'const_description': 'c_0: fixed voltage source, c_1: capacitance, c_2: resistance', - 'var_description': 'x_0: charge', - 'source': 'strogatz p.20' - }, - { - 'id': 2, - 'eq': 'c_0 * x_0', - 'dim': 1, - 'consts': [[0.23]], - 'init': [[4.78], [0.87]], - 'init_constraints': 'x_0 > 0', - 'const_constraints': '', - 'eq_description': 'Population growth (naive)', - 'const_description': 'c_0: growth rate', - 'var_description': 'x_0: population', - 'source': 'strogatz p.22' - }, - { - 'id': 4, - 'eq': '1 / (1 + exp(c_0 - x_0 / c_1)) - 0.5', - 'dim': 1, - 'consts': [[0.5, 0.96]], - 'init': [[0.8], [0.02]], - 'init_constraints': 'x_0 > 0', - 'const_constraints': 'c_1 > 0', - 'eq_description': 'RC-circuit with non-linear resistor (charging capacitor)', - 'const_description': 'c_0: fixed voltage source, c_1: capacitance', - 'var_description': 'x_0: charge', - 'source': 'strogatz p.38' - }, - { - 'id': 5, - 'eq': 'c_0 - c_1 * x_0^2', - 'dim': 1, - 'consts': [[9.81, 0.0021175]], - 'init': [[0.5], [73.]], - 'init_constraints': '', - 'const_constraints': 'c_0 > 0, c_1 > 0', - 'eq_description': 'Velocity of a falling object with air resistance', - 'const_description': 'c_0: gravitational acceleration, c_1: overall drag for human: 0.5 * C * rho * A / m, with drag coeff C=0.7, air density rho=1.21, cross-sectional area A=0.25, mass m=50', - 'var_description': 'x_0: velocity', - 'source': 'strogatz p.38' - }, - { - 'id': 7, - 'eq': 'c_0 * x_0 * log(c_1 * x_0)', - 'dim': 1, - 'consts': [[0.032, 2.29]], - 'init': [[1.73], [9.5]], - 'init_constraints': 'x_0 > 0', - 'const_constraints': 'c_0 > 0, c_1 > 0', - 'eq_description': 'Gompertz law for tumor growth', - 'const_description': 'c_0: growth rate, c_1: tumor carrying capacity', - 'var_description': 'x_0: proportional to number of cells (tumor size)', - 'source': 'strogatz p.39' - }, - { - 'id': 8, - 'eq': 'c_0 * x_0 * (1 - x_0 / c_1) * (x_0 / c_2 - 1)', - 'dim': 1, - 'consts': [[0.14, 130., 4.4]], - 'init': [[6.123], [2.1]], - 'init_constraints': 'x_0 > 0', - 'const_constraints': 'c_0 > 0, c_1 > 0, c_2 > 0', - 'eq_description': 'Logistic equation with Allee effect', - 'const_description': 'c_0: growth rate, c_1: carrying capacity, c_2: Allee effect parameter', - 'var_description': 'x_0: population', - 'source': 'strogatz p.39' - }, - { - 'id': 10, - 'eq': '(1 - x_0) * c_0 * x_0^c_1 - x_0 * (1 - c_0) * (1 - x_0)^c_1', - 'dim': 1, - 'consts': [[0.2, 1.2]], - 'init': [[0.83], [0.34]], - 'init_constraints': '0 < x_0 < 1', - 'const_constraints': '0 <= c_0 <= 1, c_1 > 1', - 'eq_description': 'Refined language death model for two languages', - 'const_description': 'c_0: perceived status of language 1, c_1: adjustable exponent', - 'var_description': 'x_0: proportion of population speaking language 1', - 'source': 'strogatz p.40' - }, - { - 'id': 13, - 'eq': 'c_0 * sin(x_0) * (c_1 * cos(x_0) - 1)', - 'dim': 1, - 'consts': [[0.0981, 9.7]], - 'init': [[3.1], [2.4]], - 'init_constraints': '', - 'const_constraints': 'c_0 > 0, c_1 > 0', - 'eq_description': 'Overdamped bead on a rotating hoop', - 'const_description': 'c_0: m * g, for m: mass, g: gravitational acceleration, c_1: r * omega^2 / g, for r: radius, omega: angular velocity', - 'var_description': 'x_0: angle', - 'source': 'strogatz p.63' - }, - { - 'id': 15, - 'eq': 'c_0 * x_0 * (1 - x_0 / c_1) - x_0^2 / (1 + x_0^2)', - 'dim': 1, - 'consts': [[0.4, 95.]], - 'init': [[44.3], [4.5]], - 'init_constraints': 'x_0 > 0', - 'const_constraints': 'c_0 > 0, c_1 > 0', - 'eq_description': 'Budworm outbreak with predation (dimensionless)', - 'const_description': 'c_0: growth rate (<0.5 for young forest, 1 for mature), c_1: carrying capacity (~300 for young forest)', - 'var_description': 'x_0: population', - 'source': 'strogatz p.76' - }, - { - 'id': 16, - 'eq': 'c_0 * x_0 - c_1 * x_0^3 - c_2 * x_0^5', - 'dim': 1, - 'consts': [[0.1, -0.04, 0.001]], - 'init': [[0.94], [1.65]], - 'init_constraints': '', - 'const_constraints': 'c_0 > 0', - 'eq_description': 'Landau equation (typical time scale tau = 1)', - 'const_description': 'c_0: small dimensionless parameter, c_1: constant, c_2: constant; c_1 > 0 for supercritical bifurcation; c_1 < 0 and c_2 > 0 for subcritical bifurcation', - 'var_description': 'x_0: order parameter', - 'source': 'strogatz p.87' - }, - { - 'id': 18, - 'eq': 'c_0 * x_0 * (1 - x_0 / c_1) - c_2 * x_0 / (c_3 + x_0)', - 'dim': 1, - 'consts': [[0.4, 100., 0.24, 50.]], - 'init': [[21.1], [44.1]], - 'init_constraints': 'x_0 > 0', - 'const_constraints': 'c_0 > 0, c_1 > 0, c_2 > 0, c_3 > 0', - 'eq_description': 'Improved logistic equation with harvesting/fishing', - 'const_description': 'c_0: growth rate, c_1: carrying capacity, c_2: harvesting rate, c_3: harvesting onset', - 'var_description': 'x_0: population', - 'source': 'strogatz p.90' - }, - { - 'id': 19, - 'eq': 'x_0 * (1 - x_0) - c_0 * x_0 / (c_1 + x_0)', - 'dim': 1, - 'consts': [[0.08, 0.8]], - 'init': [[0.13], [0.03]], - 'init_constraints': 'x_0 > 0', - 'const_constraints': 'c_0 > 0, c_1 > 0', - 'eq_description': 'Improved logistic equation with harvesting/fishing (dimensionless)', - 'const_description': 'c_0: harvesting rate, c_1: harvesting onset', - 'var_description': 'x_0: population', - 'source': 'strogatz p.90' - }, - { - 'id': 20, - 'eq': 'c_0 - c_1 * x_0 + x_0^2 / (1 + x_0^2)', - 'dim': 1, - 'consts': [[0.1, 0.55]], - 'init': [[0.002], [0.25]], - 'init_constraints': 'x_0 > 0', - 'const_constraints': 'c_0 >= 0, c_1 > 0', - 'eq_description': 'Autocatalytic gene switching (dimensionless)', - 'const_description': 'c_0: basal production rate, c_1: degradation rate', - 'var_description': 'x_0: gene product', - 'source': 'strogatz p.91' - }, - { - 'id': 21, - 'eq': 'c_0 - c_1 * x_0 - exp(-x_0)', - 'dim': 1, - 'consts': [[1.2, 0.2]], - 'init': [[0.], [0.8]], - 'init_constraints': 'x_0 >= 0', - 'const_constraints': 'c_0 >= 1, c_1 > 0', - 'eq_description': 'Dimensionally reduced SIR infection model for dead people (dimensionless)', - 'const_description': 'c_0: death rate, c_1: unknown parameter group', - 'var_description': 'x_0: dead people', - 'source': 'strogatz p.92' - }, - { - 'id': 22, - 'eq': 'c_0 + c_1 * x_0^5 / (c_2 + x_0^5) - c_3 * x_0', - 'dim': 1, - 'consts': [[1.4, 0.4, 123., 0.89]], - 'init': [[3.1], [6.3]], - 'init_constraints': 'x_0 > 0', - 'const_constraints': 'c_0 > 0, c_1 > 0, c_2 > 0, c_3 > 0', - 'eq_description': 'Hysteretic activation of a protein expression (positive feedback, basal promoter expression)', - 'const_description': 'c_0: basal transcription rate, c_1: maximum transcription rate, c_2: activation coefficient, c_3: decay rate', - 'var_description': 'x_0: protein concentration', - 'source': 'strogatz p.93' - }, - { - 'id': 23, - 'eq': 'c_0 - sin(x_0)', - 'dim': 1, - 'consts': [[0.21]], - 'init': [[-2.74], [1.65]], - 'init_constraints': '-pi <= x_0 <= pi', - 'const_constraints': 'c_0 > 0', - 'eq_description': 'Overdamped pendulum with constant driving torque/fireflies/Josephson junction (dimensionless)', - 'const_description': 'c_0: ratio of driving torque to maximum gravitational torque', - 'var_description': 'x_0: angle', - 'source': 'strogatz p.104' - } -] diff --git a/examples/convert_llm4ad_benchmark.py b/examples/convert_llm4ad_benchmark.py deleted file mode 100644 index 1f1ddc1b..00000000 --- a/examples/convert_llm4ad_benchmark.py +++ /dev/null @@ -1,460 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -'''convert_llm4ad_benchmark.py -Convert LLM4AD tasks into fully benchmark Trace-ready wrappers. - -Unlike the previous version, this creates completely self-contained task modules that: -1. Don't reference the original LLM4AD codebase -2. Include all necessary evaluation code and data generation -3. Have no hardcoded paths -4. Work without any external dependencies beyond standard libraries + numpy - -Each benchmark wrapper exposes: - build_trace_problem() -> dict - -Usage: - python convert_llm4ad_benchmark.py --llm4ad-root /path/to/LLM4AD --out ./benchmark_tasks -''' - -import argparse, sys, os, inspect, importlib, json, shutil -from pathlib import Path -import re -import textwrap -import ast -import runpy - -# ------------------------------- Helpers ------------------------------- - -def read_file(p: Path) -> str: - try: - return p.read_text(encoding='utf-8') - except Exception: - return '' - -def extract_template_program(text: str) -> str | None: - '''Pull out the Python code inside a variable named `template_program`.''' - # Try triple-single quotes - m1 = re.search(r"""template_program\s*=\s*'''(.*?)'''""", text, re.DOTALL) - if m1: - return m1.group(1).strip() - # Try triple-double quotes - m2 = re.search(r'"""template_program\s*=\s*"""(.*?)""""""', text, re.DOTALL) - # The above pattern is brittle across snapshots; fallback: generic after '=' until next triple quotes - m3 = re.search(r'template_program\s*=\s*(?P\"\"\"|\'\'\')(.*?)(?P=q)', text, re.DOTALL) - if m3: - return m3.group(2).strip() - # Fallback: single-line quotes - m4 = re.search(r"template_program\s*=\s*([\'\"])(.*?)\1", text, re.DOTALL) - if m4: - return m4.group(2).strip() - return None - -def extract_task_description(text: str) -> str | None: - m = re.search(r"task_description\s*=\s*(.+)", text) - if not m: - return None - val = m.group(1).strip() - if val.startswith(('"', '\'')) and val.endswith(('"', '\'')): - return val[1:-1] - return val - -def find_entry_function_name(template_code: str) -> str | None: - '''Find first def name( ... ) in the template code.''' - m = re.search(r"^\s*def\s+([A-Za-z_]\w*)\s*\(", template_code, re.MULTILINE) - return m.group(1) if m else None - -def extract_import_header(template_code: str) -> str: - '''Collect top-of-snippet import lines; ensure numpy/math present.''' - header_lines = [] - for line in template_code.splitlines(): - s = line.strip() - if s.startswith('import ') or s.startswith('from '): - header_lines.append(line.rstrip()) - defaults = ['import numpy as np', 'import math'] - for d in defaults: - if not any(l.strip().startswith(d) for l in header_lines): - header_lines.append(d) - return '\n'.join(header_lines) - -def snake_from_parts(parts): - s = '_'.join(p for p in parts if p) - s = re.sub(r'[^A-Za-z0-9_]+', '_', s) - s = re.sub(r'_+', '_', s).strip('_') - return s or 'task' - -def rewrite_imports_for_autonomy(code: str, template_program: str, task_description: str) -> str: - """Rewrite imports to work with benchmark task structure.""" - lines = [] - template_vars_inserted = False - path_setup_inserted = False - - for line in code.splitlines(): - stripped = line.strip() - - # Handle template imports FIRST (before removing llm4ad imports) - if ('template import template_program' in stripped or - 'from template import' in stripped): - # Replace with embedded template values - lines.append('# ' + line + ' # Template values embedded below') - if not template_vars_inserted: - lines.append('') - lines.append('# Embedded template values') - lines.append('template_program = ' + repr(template_program)) - lines.append('task_description = ' + repr(task_description)) - lines.append('') - template_vars_inserted = True - # Replace LLM4AD base imports - elif 'from llm4ad.base import Evaluation' in line: - lines.append('from llm4ad_loader import Evaluation') - elif stripped.startswith('from llm4ad.') or stripped.startswith('import llm4ad.'): - # Convert llm4ad imports - utilities to llm4ad_loader, others to local imports - if 'from llm4ad.task.' in stripped and 'import ' in stripped: - # Extract the module and imports - parts = stripped.split(' import ') - if len(parts) == 2: - module_path = parts[0].replace('from ', '') - imports = parts[1] - - # Check if this is a common utility that should come from llm4ad_loader - common_utils = ['load_subdir_as_text', 'load_subdir_as_pickle'] - imported_items = [item.strip() for item in imports.split(',')] - - # If any imported item is a common utility, import from llm4ad_loader - if any(item in common_utils for item in imported_items): - # Split into common utilities and local imports - loader_imports = [item for item in imported_items if item in common_utils] - local_imports = [item for item in imported_items if item not in common_utils] - - # Add import from llm4ad_loader for utilities - if loader_imports: - lines.append(f"from llm4ad_loader import {', '.join(loader_imports)}") - lines.append('# ' + line + ' # Common utilities from llm4ad_loader') - - # Add local imports if any remain - if local_imports: - if not path_setup_inserted: - lines.append('import os, sys') - lines.append('sys.path.insert(0, os.path.dirname(__file__))') - path_setup_inserted = True - module_file = module_path.split('.')[-1] - lines.append(f"from {module_file} import {', '.join(local_imports)}") - lines.append('# ' + line + ' # Local imports converted') - else: - # Regular local import conversion - if not path_setup_inserted: - lines.append('import os, sys') - lines.append('sys.path.insert(0, os.path.dirname(__file__))') - path_setup_inserted = True - module_file = module_path.split('.')[-1] - new_import = f"from {module_file} import {imports}" - lines.append(new_import) - lines.append('# ' + line + ' # Converted from LLM4AD import') - else: - lines.append('# ' + line + ' # Removed LLM4AD dependency - using local copies') - else: - lines.append('# ' + line + ' # Removed LLM4AD dependency - using local copies') - elif (stripped.startswith('from ') and 'import ' in stripped and - not stripped.startswith('from typing') and - not stripped.startswith('from __future__') and - not stripped.startswith('from collections') and - not stripped.startswith('from itertools') and - not stripped.startswith('from functools') and - not stripped.startswith('from math') and - not stripped.startswith('from numpy') and - not stripped.startswith('from llm4ad_loader') and - not '.' in stripped.split()[1]): # Local import (no dots) - # This is likely a local import - add path setup - if not path_setup_inserted: - lines.append('import os, sys') - lines.append('sys.path.insert(0, os.path.dirname(__file__))') - path_setup_inserted = True - lines.append(line) - elif (stripped.startswith('import ') and - not stripped.startswith('import numpy') and - not stripped.startswith('import math') and - not stripped.startswith('import os') and - not stripped.startswith('import sys') and - not stripped.startswith('import itertools') and - not stripped.startswith('import random') and - not stripped.startswith('import json') and - not stripped.startswith('import pickle') and - not '.' in stripped.split()[1]): # Local import (no dots) - # This might be a local import - add path setup - if not path_setup_inserted: - lines.append('import os, sys') - lines.append('sys.path.insert(0, os.path.dirname(__file__))') - path_setup_inserted = True - lines.append(line) - else: - lines.append(line) - - return '\n'.join(lines) - -def extract_evaluation_class(evaluation_file: Path) -> tuple[str, str]: - """Extract the evaluation class name and its full code.""" - content = read_file(evaluation_file) - - # Find the evaluation class definition - class_match = re.search(r'class\s+([A-Za-z_]\w*)\(Evaluation\)', content) - if not class_match: - raise ValueError(f"No Evaluation subclass found in {evaluation_file}") - - class_name = class_match.group(1) - - return class_name, content - -# ------------------------------- Core ---------------------------------- - -def discover_task_pairs(llm4ad_root: Path, requested_filters: list[str] | None): - '''Yield (template_path, evaluation_path, family_key).''' - candidates = [] - # example/* - ex = llm4ad_root / 'example' - if ex.exists(): - for tpl in ex.rglob('template.py'): - fam = tpl.parent - ev = fam / 'evaluation.py' - if ev.exists(): - rel = tpl.relative_to(ex) - key = rel.parts[0] if len(rel.parts)>0 else rel.stem - candidates.append((tpl, ev, key)) - # llm4ad/task/* - task_root = llm4ad_root / 'llm4ad' / 'task' - if task_root.exists(): - for tpl in task_root.rglob('template.py'): - fam = tpl.parent - ev = fam / 'evaluation.py' - if ev.exists(): - rel = tpl.relative_to(task_root) - # Use the full relative path without the template.py part for unique keys - key = '/'.join(rel.parts[:-1]) if len(rel.parts) > 1 else rel.stem - candidates.append((tpl, ev, key)) - # filter & dedup - pairs, seen = [], set() - for tpl, ev, key in candidates: - h = (str(tpl), str(ev)) - if h in seen: - continue - seen.add(h) - if requested_filters: - if not any(f in str(tpl) or f in str(ev) or f in key for f in requested_filters): - continue - pairs.append((tpl, ev, key)) - return pairs - -def copy_task_dependencies(task_dir: Path, out_task_dir: Path) -> list[str]: - """Copy additional files needed by a task (e.g., data generators).""" - copied_files = [] - - # Copy all Python files except template.py and evaluation.py - for py_file in task_dir.glob('*.py'): - if py_file.name not in ('template.py', 'evaluation.py'): - dest = out_task_dir / py_file.name - shutil.copy2(py_file, dest) - copied_files.append(py_file.name) - - # Copy paras.yaml if it exists - paras_file = task_dir / 'paras.yaml' - if paras_file.exists(): - shutil.copy2(paras_file, out_task_dir / 'paras.yaml') - copied_files.append('paras.yaml') - - # Copy any data files or other resources - for ext in ['*.txt', '*.json', '*.csv', '*.dat']: - for data_file in task_dir.glob(ext): - dest = out_task_dir / data_file.name - shutil.copy2(data_file, dest) - copied_files.append(data_file.name) - - return copied_files - -WRAPPER_TEMPLATE = '''#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Autonomous LLM4AD task: {task_name} -Generated by convert_llm4ad_benchmark.py - -This is a fully self-contained task module that doesn't depend on the original LLM4AD codebase. -""" - -# Embedded evaluation code (benchmark) -{evaluation_code} - -# Task configuration for benchmark task -ENTRY_NAME = {entry_name!r} -FUNCTION_SIGNATURE = {function_signature!r} -IMPORT_HEADER = {import_header!r} -TASK_DESCRIPTION = {task_description!r} -OBJECTIVE_TEXT = {objective_text!r} -TEMPLATE_FUNCTION = {template_function!r} -EVAL_CLASS_NAME = {eval_class_name!r} -EVAL_KWARGS = {eval_kwargs!r} - -def build_trace_problem(**override_eval_kwargs) -> dict: - """Build a Trace-ready problem using embedded benchmark evaluator.""" - - # Create evaluator instance with embedded class - eval_kwargs_final = EVAL_KWARGS.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator = globals()[EVAL_CLASS_NAME](**eval_kwargs_final) - - from llm4ad_loader import AutonomousEvaluatorGuide - from opto import trace - - # Create parameter - initial_code = TEMPLATE_FUNCTION.strip() - param = trace.node(initial_code, name='__code', - description=f'The code should start with: {{FUNCTION_SIGNATURE}}', - trainable=True) - - # Create guide using benchmark embedded evaluator - guide = AutonomousEvaluatorGuide(evaluator, ENTRY_NAME, IMPORT_HEADER, - timeout=eval_kwargs_final.get('timeout_seconds', 30)) - - # Create dataset - train_dataset = dict( - inputs=[TASK_DESCRIPTION], - infos=[{{'imports': IMPORT_HEADER, 'entry': ENTRY_NAME}}] - ) - - # Optimizer kwargs - optimizer_kwargs = dict( - objective=OBJECTIVE_TEXT, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=ENTRY_NAME, - function_signature=FUNCTION_SIGNATURE, - eval_class=EVAL_CLASS_NAME, - benchmark=True, - ) - ) -''' - -def main(): - ap = argparse.ArgumentParser(description='Convert LLM4AD tasks into benchmark Trace wrappers.') - ap.add_argument('--llm4ad-root', type=str, required=True, help='Path to LLM4AD repository root.') - ap.add_argument('--out', type=str, default='./benchmark_tasks', help='Output folder for benchmark task modules.') - ap.add_argument('--select', type=str, default='', help='Comma-separated substrings to filter tasks.') - args = ap.parse_args() - - llm4ad_root = Path(args.llm4ad_root).resolve() - out = Path(args.out).resolve() - out.mkdir(parents=True, exist_ok=True) - - filters = [s.strip() for s in args.select.split(',') if s.strip()] if args.select else None - - pairs = discover_task_pairs(llm4ad_root, filters) - - if not pairs: - print('No (template.py, evaluation.py) pairs found with current filters.') - sys.exit(1) - - index = [] - - for tpl, ev, fam_key in pairs: - try: - tpl_txt = read_file(tpl) - ev_txt = read_file(ev) - - template_code = extract_template_program(tpl_txt) - if not template_code: - print(f'[SKIP] Could not extract template_program from {tpl}') - continue - - entry = find_entry_function_name(template_code) - if not entry: - print(f'[SKIP] Could not find entry function in template_program at {tpl}') - continue - - # description - task_desc = extract_task_description(tpl_txt) or f'Implement {entry}() to solve the problem.' - - # Extract evaluation class with template values - eval_class_name, eval_code = extract_evaluation_class(ev) - eval_code = rewrite_imports_for_autonomy(eval_code, template_code, task_desc) - - imports = extract_import_header(template_code) - # Capture function signature for clarity - fsig = re.search(r'(^\s*def\s+[A-Za-z_]\w*\s*\([^)]*\)\s*:\s*)', template_code, re.MULTILINE) - fsig_str = fsig.group(1).strip() if fsig else f'def {entry}(...):' - - objective_text = (f"You are optimizing the implementation of `{entry}` for the LLM4AD task.\\n\\n" - f"Task description:\\n{task_desc}\\n\\n" - f"Your goal is to return a correct and efficient function whose score (computed by the task evaluator) is as high as possible.") - - # file name - use full path to avoid collisions - parts = fam_key.split('/') - if len(parts) >= 3 and parts[0] == 'optimization' and parts[1] == 'co_bench': - task_name = parts[2].replace('_co_bench', '') if parts[2].endswith('_co_bench') else parts[2] - short_key = snake_from_parts([parts[0], task_name]) - elif len(parts) >= 3: - short_key = snake_from_parts(parts[:3]) - else: - short_key = snake_from_parts(parts[:2]) - mod_name = short_key if short_key else snake_from_parts([entry]) - - # Create task directory - task_dir = out / mod_name - task_dir.mkdir(exist_ok=True) - - # Copy task dependencies - copied_files = copy_task_dependencies(ev.parent, task_dir) - - # Load eval kwargs from paras.yaml - paras_yaml = ev.parent / 'paras.yaml' - eval_kwargs = {} - if paras_yaml.exists(): - try: - import yaml # optional - eval_kwargs = yaml.safe_load(paras_yaml.read_text()) - if isinstance(eval_kwargs, dict): - eval_kwargs.pop('name', None) - except Exception: - eval_kwargs = {} - - # Create benchmark wrapper - wrapper_content = WRAPPER_TEMPLATE.format( - task_name=mod_name, - evaluation_code=eval_code, - entry_name=entry, - function_signature=fsig_str, - import_header=imports, - task_description=task_desc, - objective_text=objective_text, - template_function=template_code, - eval_class_name=eval_class_name, - eval_kwargs=eval_kwargs - ) - - wrapper_path = task_dir / '__init__.py' - wrapper_path.write_text(wrapper_content, encoding='utf-8') - - index.append(dict( - key=fam_key, - module=str(task_dir.relative_to(out)), - entry=entry, - eval_class=eval_class_name, - task_description=task_desc, - wrapper=mod_name, - copied_files=copied_files, - benchmark=True - )) - print(f"[OK] Created benchmark task {task_dir}") - - except Exception as e: - print(f"[ERROR] Failed to convert {fam_key}: {e}") - continue - - (out / 'index.json').write_text(json.dumps(index, indent=2), encoding='utf-8') - print(f"\\nCreated {len(index)} benchmark tasks at {out}") - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/examples/llm4ad_loader.py b/examples/llm4ad_loader.py deleted file mode 100644 index b4794b55..00000000 --- a/examples/llm4ad_loader.py +++ /dev/null @@ -1,492 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -"""llm4ad_loader.py -Autonomous LLM4AD task runner for Trace optimization. - -This module provides a complete, self-contained implementation of LLM4AD evaluators -that doesn't depend on the original LLM4AD codebase. All necessary components are -either reimplemented here or copied from the original tasks. -""" - -import sys, os, types, traceback, inspect, importlib, importlib.util, textwrap, json, time, multiprocessing -from typing import Any, Dict, Literal, Callable -from abc import ABC, abstractmethod -import numpy as np -from pathlib import Path - -# You must have Trace installed and importable as `opto`. -from opto import trace -from opto.trainer.guide import Guide -from opto.trace.nodes import ParameterNode - - -# ============================================================================ -# LLM4AD Base Classes (reimplemented for autonomy) -# ============================================================================ - -class Evaluation(ABC): - """Base evaluation class reimplemented from LLM4AD for benchmark tasks.""" - - def __init__( - self, - template_program: str = '', - task_description: str = '', - timeout_seconds: int | float = 30, - random_seed: int | None = None, - exec_code: bool = True, - safe_evaluate: bool = False, # Simplified - no multiprocessing by default - **kwargs - ): - """Simplified Evaluation base class. - - Args: - template_program: The template program string (not used in our implementation) - task_description: Description of the task (not used in our implementation) - timeout_seconds: Time limit for evaluation - random_seed: Random seed to set (not implemented) - exec_code: Whether to exec the code (always True in our case) - safe_evaluate: Whether to use safe evaluation (simplified, always False) - **kwargs: Additional arguments (stored but not used) - """ - self.template_program = template_program - self.task_description = task_description - self.timeout_seconds = timeout_seconds - self.random_seed = random_seed - self.exec_code = exec_code - self.safe_evaluate = safe_evaluate - self.kwargs = kwargs - - @abstractmethod - def evaluate_program(self, program_str: str, callable_func: Callable, **kwargs) -> Any | None: - """Evaluate a program. Must be implemented by subclasses. - - Args: - program_str: The program as a string - callable_func: The compiled callable function - **kwargs: Additional evaluation arguments - - Returns: - Evaluation score/result - """ - pass - - -class LLM4ADEvaluatorLoader: - """Dynamically load and instantiate LLM4AD evaluators from their original modules.""" - - def __init__(self, llm4ad_root: str, eval_module_path: str, eval_class_name: str, eval_file_path: str = None, **eval_kwargs): - self.llm4ad_root = Path(llm4ad_root) - self.eval_module_path = eval_module_path - self.eval_class_name = eval_class_name - self.eval_file_path = eval_file_path - self.eval_kwargs = eval_kwargs - self._evaluator = None - - def _load_evaluator(self): - """Load the evaluator class from LLM4AD and instantiate it.""" - if self._evaluator is not None: - return self._evaluator - - try: - # Add LLM4AD root and evaluation file directory to Python path temporarily - original_path = sys.path.copy() - if str(self.llm4ad_root) not in sys.path: - sys.path.insert(0, str(self.llm4ad_root)) - # Also add the evaluation file's directory for local imports - if self.eval_file_path: - eval_dir = str(Path(self.eval_file_path).parent) - if eval_dir not in sys.path: - sys.path.insert(0, eval_dir) - - try: - # Try importing the module normally first - try: - eval_module = importlib.import_module(self.eval_module_path) - except (ImportError, ModuleNotFoundError): - # Fallback: direct file execution for problematic paths - eval_file_path = getattr(self, 'eval_file_path', None) - if eval_file_path and Path(eval_file_path).exists(): - spec = importlib.util.spec_from_file_location( - f"eval_module_{hash(eval_file_path)}", - eval_file_path - ) - eval_module = importlib.util.module_from_spec(spec) - sys.modules[spec.name] = eval_module - spec.loader.exec_module(eval_module) - else: - raise - - # Get the evaluator class - evaluator_class = getattr(eval_module, self.eval_class_name) - - # Instantiate with provided kwargs - self._evaluator = evaluator_class(**self.eval_kwargs) - - return self._evaluator - - finally: - # Restore original Python path - sys.path = original_path - - except Exception as e: - raise RuntimeError(f"Failed to load LLM4AD evaluator {self.eval_class_name} from {self.eval_module_path}: {e}") - - def evaluate_program(self, program_str: str, callable_func, **kwargs): - """Evaluate using the LLM4AD evaluator's evaluate_program method.""" - evaluator = self._load_evaluator() - return evaluator.evaluate_program(program_str, callable_func, **kwargs) - - -class LLM4ADEvaluatorGuide(Guide): - """Trace Guide that uses LLM4AD evaluators for feedback.""" - - def __init__(self, evaluator_loader: LLM4ADEvaluatorLoader, entry_name: str, import_header: str = '', timeout: float | None = None): - self.evaluator_loader = evaluator_loader - self._entry = entry_name - self._import_header = import_header - self._timeout = timeout - - def get_feedback(self, task: str, response: str, info: Any, **kwargs): - # response is a code string (candidate). Compile it and evaluate using LLM4AD. - import signal - start = time.time() - feedback_lines = [] - - def timeout_handler(signum, frame): - raise TimeoutError("Evaluation timed out") - - try: - # Set timeout (default 30 seconds for LLM4AD evaluations) - timeout = self._timeout or 30.0 - use_signal = True - try: - signal.signal(signal.SIGALRM, timeout_handler) - signal.alarm(int(timeout)) - except ValueError as e: - # signal only works in main thread - skip timeout when in thread - if "main thread" in str(e): - use_signal = False - else: - raise # Build namespace and exec the code - ns: Dict[str, Any] = {} - header = info.get('imports', '') if isinstance(info, dict) else self._import_header - full_code = header + "\n" + response if header else response - exec(full_code, ns, ns) - - if self._entry not in ns or not callable(ns[self._entry]): - msg = f"Entry function '{self._entry}' not found after exec." - signal.alarm(0) - return -float('inf'), msg - - func = ns[self._entry] - - # Use LLM4AD's evaluate_program method - try: - score = self.evaluator_loader.evaluate_program(response, func) - if use_signal: - signal.alarm(0) - elapsed = time.time() - start - - if score is None or score == float('-inf') or score == float('inf'): - # Try to give a more informative error for infinite scores - if score == float('-inf'): - feedback_lines.append(f'LLM4AD eval returned -inf (possible constraint violation or error)') - # Instead of returning -inf, return a large negative score for optimization to work - return -1000000.0, '\n'.join(feedback_lines) - elif score == float('inf'): - feedback_lines.append(f'LLM4AD eval returned +inf (possible error in evaluation)') - return -1000000.0, '\n'.join(feedback_lines) - else: - feedback_lines.append(f'LLM4AD eval returned None') - return -1000000.0, '\n'.join(feedback_lines) - - feedback_lines.append(f'LLM4AD eval OK in {elapsed:.2f}s; score={score}') - return float(score), '\n'.join(feedback_lines) - - except (ValueError, RuntimeError, AssertionError) as eval_err: - # Handle evaluation-specific errors more gracefully - if use_signal: - signal.alarm(0) - elapsed = time.time() - start - feedback_lines.append(f'LLM4AD eval constraint violation in {elapsed:.2f}s: {eval_err}') - # Return a large negative score instead of -inf to allow optimization - return -1000000.0, '\n'.join(feedback_lines) - - except TimeoutError: - if use_signal: - signal.alarm(0) - return -1000000.0, f'Evaluation timed out after {timeout}s' - except Exception as e: - if use_signal: - signal.alarm(0) - tb = traceback.format_exc(limit=3) - return -1000000.0, f'LLM4AD eval failed: {e}\n{tb}' - - def __call__(self, task: str, response: str, info: Any, **kwargs): - return self.get_feedback(task, response, info, **kwargs) - - -def build_trace_problem_from_config( - llm4ad_root: str, - eval_module_path: str, - eval_class_name: str, - eval_file_path: str, - entry_name: str, - function_signature: str, - import_header: str, - task_description: str, - objective_text: str, - template_function: str, - eval_kwargs: dict, - **override_eval_kwargs -) -> dict: - """ - Build a Trace problem from LLM4AD task configuration. - - This is a common implementation that replaces the build_trace_problem function - that was duplicated in every converted task file. - - Returns: - dict with keys: param, guide, train_dataset, optimizer_kwargs, metadata - """ - - # 1) make the trainable code parameter - initial_code = template_function.strip() - param = trace.node(initial_code, name='__code', description=f'The code should start with: {function_signature}', trainable=True) - - # 2) Create dynamic LLM4AD evaluator loader - eval_kwargs_final = eval_kwargs.copy() - eval_kwargs_final.update(override_eval_kwargs) - - evaluator_loader = LLM4ADEvaluatorLoader( - llm4ad_root=llm4ad_root, - eval_module_path=eval_module_path, - eval_class_name=eval_class_name, - eval_file_path=eval_file_path, - **eval_kwargs_final - ) - - # 3) Create guide that uses the LLM4AD evaluator - timeout = eval_kwargs_final.get('timeout_seconds', 30) - guide = LLM4ADEvaluatorGuide(evaluator_loader, entry_name, import_header, timeout=timeout) - - # 4) dataset: minimal 1-sample dataset - train_dataset = dict( - inputs=[task_description], - infos=[{'imports': import_header, 'entry': entry_name}] - ) - - # 5) optimizer hints (objective) - optimizer_kwargs = dict( - objective=objective_text, - memory_size=10 - ) - - return dict( - param=param, - guide=guide, - train_dataset=train_dataset, - optimizer_kwargs=optimizer_kwargs, - metadata=dict( - entry=entry_name, - function_signature=function_signature, - llm4ad_eval=eval_class_name, - eval_module=eval_module_path, - llm4ad_root=llm4ad_root, - ) - ) - - -class AutonomousEvaluatorGuide(Guide): - """Trace Guide that uses benchmark (embedded) LLM4AD evaluators.""" - - def __init__(self, evaluator: Evaluation, entry_name: str, import_header: str = '', timeout: float | None = None): - self.evaluator = evaluator - self._entry = entry_name - self._import_header = import_header - self._timeout = timeout - - def get_feedback(self, task: str, response: str, info: Any, **kwargs): - # response is a code string (candidate). Compile it and evaluate using embedded evaluator. - import signal - start = time.time() - feedback_lines = [] - - def timeout_handler(signum, frame): - raise TimeoutError("Evaluation timed out") - - try: - # Set timeout (default 30 seconds for LLM4AD evaluations) - timeout = self._timeout or 30.0 - use_signal = True - try: - signal.signal(signal.SIGALRM, timeout_handler) - signal.alarm(int(timeout)) - except ValueError as e: - # signal only works in main thread - skip timeout when in thread - if "main thread" in str(e): - use_signal = False - else: - raise - - # Build namespace and exec the code - ns: Dict[str, Any] = {} - header = info.get('imports', '') if isinstance(info, dict) else self._import_header - full_code = header + "\n" + response if header else response - exec(full_code, ns, ns) - - if self._entry not in ns or not callable(ns[self._entry]): - msg = f"Entry function '{self._entry}' not found after exec." - if use_signal: - signal.alarm(0) - return -float('inf'), msg - - func = ns[self._entry] - - # Use embedded evaluator's evaluate_program method directly - score = self.evaluator.evaluate_program(response, func) - - if use_signal: - signal.alarm(0) - elapsed = time.time() - start - feedback_lines.append(f'Autonomous eval OK in {elapsed:.2f}s; score={score}') - return float(score) if score is not None else -float('inf'), '\n'.join(feedback_lines) - - except TimeoutError: - if use_signal: - signal.alarm(0) - return -float('inf'), f'Evaluation timed out after {timeout}s' - except Exception as e: - if use_signal: - signal.alarm(0) - tb = traceback.format_exc(limit=3) - return -float('inf'), f'Autonomous eval failed: {e}\n{tb}' - - def __call__(self, task: str, response: str, info: Any, **kwargs): - return self.get_feedback(task, response, info, **kwargs) - -def load_subdir_as_text(repo_id: str, subdir: str, *, skip_ext: tuple[str, ...] = (".py",), streaming: bool = False): - """ - Load files from a subdirectory in a Hugging Face dataset as text format. - - Args: - repo_id: The repository ID on Hugging Face (e.g., "CO-Bench/CO-Bench") - subdir: The subdirectory path within the dataset - skip_ext: File extensions to skip (default: (".py",)) - streaming: Whether to use streaming mode - - Returns: - A dict where keys are original filenames and values are loaded datasets - - Example: - ds = load_subdir_as_text("CO-Bench/CO-Bench", "Aircraft landing") - # Returns: {"airland1.txt": Dataset(...), "airland2.txt": Dataset(...), ...} - """ - from huggingface_hub import list_repo_files - from datasets import load_dataset - from pathlib import PurePosixPath - prefix = subdir.rstrip("/") + "/" - files = [ - f for f in list_repo_files(repo_id, repo_type="dataset") - if f.startswith(prefix) and not f.endswith(skip_ext) - ] - if not files: - raise FileNotFoundError(f"No matching files inside '{subdir}' on {repo_id}") - - # Create a mapping from sanitized split names to original filenames - def sanitize_split_name(filename): - """Convert filename to valid split name (only alphanumeric, dots, underscores)""" - import re - # Replace hyphens and other special chars with underscores - sanitized = re.sub(r'[^a-zA-Z0-9._]', '_', filename) - return sanitized - - # Build data_files dict with sanitized split names - data_files = {} - filename_mapping = {} # Maps sanitized names back to original names - - for f in files: - original_filename = PurePosixPath(f).name - sanitized_name = sanitize_split_name(original_filename) - data_files[sanitized_name] = f - filename_mapping[sanitized_name] = original_filename - - # Load the dataset - dataset = load_dataset( - repo_id, - data_files=data_files, - streaming=streaming, - ) - - # Return a dict with original filenames as keys - result = {} - for sanitized_name, original_filename in filename_mapping.items(): - result[original_filename] = dataset[sanitized_name] - - return result - - -def load_subdir_as_pickle(repo_id: str, subdir: str, *, include_subdirs: tuple[str, ...] = (), streaming: bool = False): - """ - Load pickle files from a subdirectory in a Hugging Face dataset. - - Args: - repo_id: The repository ID on Hugging Face (e.g., "CO-Bench/CO-Bench") - subdir: The subdirectory path within the dataset - include_subdirs: Tuple of subdirectory names to include (if empty, includes all) - streaming: Whether to use streaming mode - - Returns: - A dict where keys are subdirectory names and values are dicts of - {filename: loaded_pickle_content} - - Example: - result = load_subdir_as_pickle("CO-Bench/CO-Bench", "Maximal independent set", - include_subdirs=("er_test", "er_large_test")) - # Returns: {"er_test": {"file1.gpickle": graph1, ...}, "er_large_test": {...}} - """ - import pickle - from huggingface_hub import hf_hub_download, list_repo_files - - prefix = subdir.rstrip("/") + "/" - files = [ - f for f in list_repo_files(repo_id, repo_type="dataset") - if f.startswith(prefix) and f.endswith(('.pickle', '.gpickle', '.pkl')) - ] - - if not files: - raise FileNotFoundError(f"No pickle files found inside '{subdir}' on {repo_id}") - - # Organize files by subdirectory - subdirs = {} - for file_path in files: - parts = file_path.split('/') - if len(parts) >= 3: # "subdir/subsubdir/filename" - subsubdir = parts[1] # The subdirectory under main subdir - filename = parts[2] # The actual filename - - # Filter by include_subdirs if specified - if include_subdirs and subsubdir not in include_subdirs: - continue - - if subsubdir not in subdirs: - subdirs[subsubdir] = {} - - # Download and load the pickle file - try: - local_path = hf_hub_download( - repo_id=repo_id, - filename=file_path, - repo_type="dataset" - ) - - with open(local_path, "rb") as f: - pickle_content = pickle.load(f) - - subdirs[subsubdir][filename] = pickle_content - - except Exception as e: - print(f"Warning: Failed to load {file_path}: {e}") - continue - - return subdirs \ No newline at end of file diff --git a/examples/priority_search_on_convex_fn_BENCH.py b/examples/priority_search_on_convex_fn_BENCH.py index 13088b9f..8f1a974e 100644 --- a/examples/priority_search_on_convex_fn_BENCH.py +++ b/examples/priority_search_on_convex_fn_BENCH.py @@ -4,20 +4,13 @@ import numpy as np import time from opto.trace.utils import dedent -from priority_search_on_convex_fn import LossLandscapeBase, np_random, Rosenbrock, SixHumpCamel, RewardGuide +from priority_search_on_convex_fn import Rosenbrock, SixHumpCamel, RewardGuide # ============ TESTING code ============= -import datasets import numpy as np from opto import trace -from opto.utils.llm import LLM, LiteLLM -from opto.optimizers import OptoPrimeV2 as OptoPrime from opto.features.priority_search import PrioritySearch as SearchAlgorithm -from opto.trainer.guide import Guide -from opto.trainer.loggers import TensorboardLogger -from opto.trainer.guide import LLMJudge -from opto.trainer.algorithms.basic_algorithms import BasicSearchAlgorithm -from opto.trainer.algorithms.gepa_algorithms import GEPAAlgorithmBase, GEPAUCBSearch, GEPABeamPareto +from opto.features.gepa.gepa_algorithms import GEPAAlgorithmBase, GEPAUCBSearch, GEPABeamPareto from typing import Any from opto import trainer from typing import Tuple diff --git a/examples/trainers_benchmark.py b/examples/trainers_benchmark.py index bdfbaf1d..84a24a1d 100644 --- a/examples/trainers_benchmark.py +++ b/examples/trainers_benchmark.py @@ -28,7 +28,7 @@ from opto import trace from opto import trainer -from opto.trainer.algorithms.gepa_algorithms import GEPAAlgorithmBase, GEPAUCBSearch, GEPABeamPareto +from opto.features.gepa.gepa_algorithms import GEPAAlgorithmBase, GEPAUCBSearch, GEPABeamPareto from opto.features.priority_search import PrioritySearch as SearchAlgorithm from opto.trainer.loggers import TensorboardLogger diff --git a/opto/features/gepa/__init__.py b/opto/features/gepa/__init__.py new file mode 100644 index 00000000..dd92a13c --- /dev/null +++ b/opto/features/gepa/__init__.py @@ -0,0 +1,9 @@ +"""GEPA (Genetic Enhancement via Population Algorithm) implementations. + +This module contains experimental GEPA algorithms that extend basic optimization +with population-based genetic enhancement techniques. +""" + +from .gepa_algorithms import (GEPAAlgorithmBase, GEPAUCBSearch, GEPABeamPareto) + +__all__ = ['GEPAAlgorithmBase', 'GEPAUCBSearch', 'GEPABeamPareto'] \ No newline at end of file diff --git a/opto/trainer/algorithms/gepa_algorithms.py b/opto/features/gepa/gepa_algorithms.py similarity index 99% rename from opto/trainer/algorithms/gepa_algorithms.py rename to opto/features/gepa/gepa_algorithms.py index a793f48f..7494c0ca 100644 --- a/opto/trainer/algorithms/gepa_algorithms.py +++ b/opto/features/gepa/gepa_algorithms.py @@ -1,4 +1,4 @@ -# opto/trainer/algorithms/gepa_algorithms.py +# opto/features/gepa/gepa_algorithms.py # GEPA (+Merge) algorithms for Trace # - GEPAUCBSearch: subclass of UCBSearchAlgorithm # - GEPABeamPareto: subclass of BeamsearchAlgorithm (Pareto select + single-parent incremental) diff --git a/tests/llm_optimizers_tests/test_gepa_benchmark.py b/tests/llm_optimizers_tests/test_gepa_benchmark.py index 19c97559..2811d4ec 100644 --- a/tests/llm_optimizers_tests/test_gepa_benchmark.py +++ b/tests/llm_optimizers_tests/test_gepa_benchmark.py @@ -4,7 +4,7 @@ from opto import trace from opto.optimizers.optoprime_v2 import OptoPrimeV2 -from opto.trainer.algorithms.gepa_algorithms import GEPAAlgorithmBase, GEPAUCBSearch, GEPABeamPareto +from opto.features.gepa.gepa_algorithms import GEPAAlgorithmBase, GEPAUCBSearch, GEPABeamPareto from opto.trainer.algorithms.basic_algorithms import BasicSearchAlgorithm from opto.trainer.guide import LLMJudge from opto.utils.llm import LLM From b3c211a17cad5eda69b850cdb43b7516029501c7 Mon Sep 17 00:00:00 2001 From: doxav Date: Thu, 2 Oct 2025 18:55:59 +0200 Subject: [PATCH 293/314] removed any LLM4AD tasks related files --- examples/trainer_benchmark_HOWTO.md | 300 -------------- examples/trainers_benchmark.py | 348 ---------------- .../trainers_benchmark_tasks_validation.py | 385 ------------------ 3 files changed, 1033 deletions(-) delete mode 100644 examples/trainer_benchmark_HOWTO.md delete mode 100644 examples/trainers_benchmark.py delete mode 100644 examples/trainers_benchmark_tasks_validation.py diff --git a/examples/trainer_benchmark_HOWTO.md b/examples/trainer_benchmark_HOWTO.md deleted file mode 100644 index b21b16a8..00000000 --- a/examples/trainer_benchmark_HOWTO.md +++ /dev/null @@ -1,300 +0,0 @@ -# Trace Benchmark Trainer - HOWTO Guide - -## Overview - -The Trace Benchmark Trainer is a comprehensive system for running optimization algorithms on algorithmic tasks derived from the [LLM4AD (Large Language Models for Algorithm Design)](https://github.com/Opti### Examples of Analysis Workflows - -### Quick Task Evaluation -```bash -# Test a new optimization approach on a simple task -python examples/trainers_benchmark.py --tasks examples/benchmark_tasks --task circle_packing --algos PrioritySearch --ps-steps 1 -``` - -### Algorithm Comparison Study -```bash -# Compare all algorithms on multiple related tasks -python examples/trainers_benchmark.py --tasks examples/benchmark_tasks --task "optimization_tsp_construct,optimization_knapsack_construct,optimization_set_cover_construct" --algos PrioritySearch,GEPA-Base,GEPA-UCB,GEPA-Beam --ps-steps 2 --gepa-iters 2 --threads 4 -``` - -### Performance Profiling -```bash -# Detailed performance analysis with extended runtime -python examples/trainers_benchmark.py --tasks examples/benchmark_tasks --task optimization_job_shop_scheduling --algos GEPA-UCB --gepa-iters 5 --gepa-train-bs 2 --threads 4 --eval-kwargs '{"timeout_seconds": 300}' -```oject. This system enables systematic evaluation and comparison of different optimization approaches on diverse algorithmic challenges. - -### What it does - -The benchmark trainer: -- **Runs optimization algorithms**: Supports PrioritySearch, GEPA-Base, GEPA-UCB, and GEPA-Beam algorithms -- **Evaluates performance**: Uses self-contained task evaluators derived from LLM4AD -- **Provides multiple outputs**: Console display, CSV results, TensorBoard logs for analysis -- **Supports parallel execution**: Multi-task and multi-algorithm runs with timeout protection -- **Enables comparison**: Systematic benchmarking across algorithms and tasks - -### Key Features - -- **60 benchmark tasks** covering optimization, machine learning, and scientific discovery -- **Timeout protection** prevents hanging on difficult tasks -- **Comprehensive logging** with CSV export and TensorBoard integration -- **Multi-task support** for batch evaluation -- **Self-contained tasks** with no external dependencies - -## Quick Start - -### Basic Usage - -Run a single task with default PrioritySearch algorithm: -```bash -python examples/trainers_benchmark.py --tasks examples/benchmark_tasks --task circle_packing -``` - -### Command Structure - -```bash -python examples/trainers_benchmark.py --tasks --task [OPTIONS] -``` - -## Main Commands and Variations - -### 1. Single Task, Single Algorithm - -**Basic run with default settings:** -```bash -python examples/trainers_benchmark.py --tasks examples/benchmark_tasks --task circle_packing -``` - -**With custom PrioritySearch parameters:** -```bash -python examples/trainers_benchmark.py --tasks examples/benchmark_tasks --task circle_packing --ps-steps 2 --ps-batches 2 -``` - -**With timeout and thread control:** -```bash -python examples/trainers_benchmark.py --tasks examples/benchmark_tasks --task circle_packing --threads 4 --eval-kwargs '{"timeout_seconds": 60}' -``` - -### 2. Single Task, Multiple Algorithms - -**Compare all algorithms on one task:** -```bash -python examples/trainers_benchmark.py --tasks examples/benchmark_tasks --task circle_packing --algos PrioritySearch,GEPA-Base,GEPA-UCB,GEPA-Beam -``` - -**Compare specific algorithms with custom settings:** -```bash -python examples/trainers_benchmark.py --tasks examples/benchmark_tasks --task optimization_knapsack_construct --algos PrioritySearch,GEPA-Beam --ps-steps 2 --gepa-iters 2 -``` - -**Run with detailed GEPA configuration:** -```bash -python examples/trainers_benchmark.py --tasks examples/benchmark_tasks --task online_bin_packing_local --algos GEPA-UCB,GEPA-Beam --gepa-train-bs 2 --gepa-pareto-subset 3 --threads 4 -``` - -### 3. Multiple Tasks, Multiple Algorithms - -**Batch evaluation on related tasks:** -```bash -python examples/trainers_benchmark.py --tasks examples/benchmark_tasks --task "circle_packing,optimization_knapsack_construct,optimization_tsp_construct" --algos PrioritySearch,GEPA-Beam -``` - -**Comprehensive benchmark run:** -```bash -python examples/trainers_benchmark.py --tasks examples/benchmark_tasks --task "circle_packing,machine_learning_acrobot,optimization_knapsack_construct" --algos PrioritySearch,GEPA-UCB,GEPA-Beam --ps-steps 2 --gepa-iters 2 --threads 4 -``` - -**Production benchmark with full configuration:** -```bash -python examples/trainers_benchmark.py --tasks examples/benchmark_tasks --task "optimization_tsp_construct,optimization_set_cover_construct,optimization_bp_1d_construct" --algos PrioritySearch,GEPA-Base,GEPA-UCB,GEPA-Beam --ps-steps 3 --gepa-iters 2 --gepa-train-bs 2 --threads 4 --eval-kwargs '{"timeout_seconds": 120}' -``` - -## Output Formats - -### 1. Console Display -Real-time progress with: -- Task loading status -- Algorithm execution progress -- Performance scores and timing -- Error messages and timeouts -- Final summary table - -### 2. CSV Export (`./results/results_YYYYMMDD_HHMMSS.csv`) -Structured data with columns: -- `timestamp`: Execution timestamp -- `task`: Task name -- `algo`: Algorithm name -- `parameters`: JSON configuration used -- `time`: Execution time in seconds -- `score`: Final performance score -- `initial_params`: Starting code/parameters -- `final_params`: Optimized code/parameters -- `log_dir`: TensorBoard log directory - -### 3. TensorBoard Logs (`./logs////`) -Interactive visualization with: -- Training curves and metrics -- Parameter evolution over time -- Algorithm-specific performance data -- Comparative analysis across runs - -**Note**: For multi-task runs, logs are organized as `./logs//`, `./logs//`, etc. - -## Available Benchmark Tasks - -The system includes **60 self-contained benchmark tasks** organized by domain: - -| Category | Tasks | Examples | -|----------|-------|----------| -| **Optimization - Basic** | 18 tasks | `circle_packing`, `online_bin_packing_local` | -| **Optimization - Constructive** | 15 tasks | `optimization_tsp_construct`, `optimization_knapsack_construct`, `optimization_set_cover_construct` | -| **Optimization - CO-Bench** | 21 tasks | `optimization_travelling_salesman_problem`, `optimization_job_shop_scheduling`, `optimization_container_loading` | -| **Machine Learning** | 5 tasks | `machine_learning_acrobot`, `machine_learning_pendulum`, `machine_learning_moon_lander` | -| **Scientific Discovery** | 1 task | `science_discovery_ode_1d` | - -### Task Categories Detail - -**Optimization - Basic:** -- `circle_packing`: Pack circles in unit square -- `online_bin_packing_local`: Online bin packing heuristics -- `optimization_admissible_set`: Admissible set priority -- `optimization_online_bin_packing`: Online bin packing strategies - -**Optimization - Constructive Heuristics:** -- `optimization_tsp_construct`: TSP node selection -- `optimization_knapsack_construct`: Knapsack item selection -- `optimization_set_cover_construct`: Set cover subset selection -- `optimization_bp_1d_construct`: 1D bin packing assignment -- `optimization_vrptw_construct`: Vehicle routing with time windows - -**Optimization - CO-Bench (Complex):** -- `optimization_travelling_salesman_problem`: Complete TSP solving -- `optimization_job_shop_scheduling`: Job shop scheduling -- `optimization_container_loading`: 3D container packing -- `optimization_maximal_independent_set`: Graph MIS problem -- `optimization_flow_shop_scheduling`: Flow shop optimization - -**Machine Learning Control:** -- `machine_learning_acrobot`: Acrobot control optimization -- `machine_learning_pendulum`: Pendulum control strategies -- `machine_learning_moon_lander`: Lunar lander control -- `machine_learning_car_mountain`: Mountain car problem - -**Scientific Discovery:** -- `science_discovery_ode_1d`: ODE system discovery - -## Command Line Parameters - -### Required Parameters -- `--tasks`: Path to benchmark tasks directory (e.g., `examples/benchmark_tasks`) -- `--task`: Task name(s), comma-separated for multiple tasks - -### Algorithm Selection -- `--algos`: Comma-separated algorithm list (default: `PrioritySearch`) - - Options: `PrioritySearch`, `GEPA-Base`, `GEPA-UCB`, `GEPA-Beam` - -### Performance Tuning -- `--threads`: Number of threads (default: 2) -- `--optimizer-kwargs`: JSON dict for optimizer configuration -- `--eval-kwargs`: JSON dict for evaluator parameters (e.g., timeout) - -### PrioritySearch Parameters -- `--ps-steps`: Search steps (default: 3) -- `--ps-batches`: Batch size (default: 2) -- `--ps-candidates`: Candidate count (default: 3) -- `--ps-proposals`: Proposal count (default: 3) -- `--ps-mem-update`: Memory update frequency (default: 2) - -### GEPA Algorithm Parameters -- `--gepa-iters`: Search iterations (default: 3) -- `--gepa-train-bs`: Training batch size (default: 2) -- `--gepa-merge-every`: Merge frequency (default: 2) -- `--gepa-pareto-subset`: Pareto subset size (default: 3) - -## Updating/Re-creating Tasks from LLM4AD - -To update the benchmark tasks from the latest LLM4AD repository: - -### 1. Clone/Update LLM4AD Repository - -```bash -git clone https://github.com/Optima-CityU/LLM4AD.git -cd LLM4AD -git pull # if already cloned -``` - -### 2. Convert Tasks to Benchmark Format - -**Convert all available tasks:** -```bash -python examples/convert_llm4ad_benchmark.py --llm4ad-root /path/to/LLM4AD --out examples/benchmark_tasks -``` - -**Convert specific task families:** -```bash -python examples/convert_llm4ad_benchmark.py --llm4ad-root /path/to/LLM4AD --out examples/benchmark_tasks --select "circle_packing,optimization,machine_learning" -``` - -**Convert only the two core tasks (minimal set):** -```bash -python examples/convert_llm4ad_benchmark.py --llm4ad-root /path/to/LLM4AD --out examples/benchmark_tasks --select "circle_packing,science_discovery/ode_1d" -``` - -### 3. Validate Converted Tasks - -```bash -python examples/trainers_benchmark_tasks_validation.py --tasks examples/benchmark_tasks --task circle_packing -``` - -### 4. Check Task Inventory - -```bash -python -c "import json; print(json.dumps([t['key'] for t in json.load(open('examples/benchmark_tasks/index.json'))], indent=2))" -``` - -## Troubleshooting - -### Common Issues - -**Task hangs during execution:** -- Increase timeout: `--eval-kwargs '{"timeout_seconds": 120}'` -- Reduce complexity: Lower `--ps-steps` or `--gepa-iters` - -**Out of memory errors:** -- Reduce `--threads` parameter -- Lower batch sizes: `--ps-batches` or `--gepa-train-bs` - -**Task not found:** -- Check task name spelling in `examples/benchmark_tasks/index.json` -- Use partial matching: `optimization_tsp` matches `optimization_tsp_construct` - -**Import errors:** -- Ensure Trace (opto) is properly installed: `pip install -e .` -- Verify benchmark tasks are properly converted - -### Performance Tips - -- **Parallel execution**: Use `--threads 4-8` for faster results -- **Batch processing**: Run multiple related tasks together -- **Timeout tuning**: Set appropriate timeouts based on task complexity -- **Algorithm selection**: Start with PrioritySearch for quick results, use GEPA for thorough optimization - -## Examples of Analysis Workflows - -### Quick Task Evaluation -```bash -# Test a new optimization approach on a simple task -python examples/trainers_benchmark.py --tasks examples/benchmark_tasks --task circle_packing --algos PrioritySearch --ps-steps 3 -``` - -### Algorithm Comparison Study -```bash -# Compare all algorithms on multiple related tasks -python examples/trainers_benchmark.py --tasks examples/benchmark_tasks --task "optimization_tsp_construct,optimization_knapsack_construct,optimization_set_cover_construct" --algos PrioritySearch,GEPA-Base,GEPA-UCB,GEPA-Beam --threads 6 -``` - -### Performance Profiling -```bash -# Detailed performance analysis with extended runtime -python examples/trainers_benchmark.py --tasks examples/benchmark_tasks --task optimization_job_shop_scheduling --algos GEPA-UCB --gepa-iters 10 --gepa-train-bs 4 --threads 8 --eval-kwargs '{"timeout_seconds": 300}' -``` - -The results can then be analyzed using the CSV output for statistical analysis or TensorBoard logs for detailed performance visualization. \ No newline at end of file diff --git a/examples/trainers_benchmark.py b/examples/trainers_benchmark.py deleted file mode 100644 index 84a24a1d..00000000 --- a/examples/trainers_benchmark.py +++ /dev/null @@ -1,348 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -'''trainers_BENCHMARK.py -Run Trace trainers on benchmark LLM4AD tasks (generated by convert_llm4ad_benchmark.py). - -This script works with benchmark task directories that contain self-contained -task modules with embedded evaluators. - -Examples: - pyt print(f"\nResults saved to {csv_path}") - if len(task_keys) == 1: - print(f"TensorBoard logs saved to ./logs/{task_keys[0]}/") - else: - print(f"TensorBoard logs saved to ./logs/ (multiple task subdirectories)") - for task_key in task_keys: - print(f" - ./logs/{task_key}/")n trainers_BENCHMARK.py --tasks ./benchmark_tasks --task circle_packing - python trainers_BENCHMARK.py --tasks ./benchmark_tasks --task online_bin_packing_local --algos PrioritySearch --ps-steps 5 -''' - -from __future__ import annotations - -import argparse, json, importlib.util, sys, time, csv, os, threading -from pathlib import Path -from typing import Dict, Any, List, Tuple -from datetime import datetime - -import numpy as np - -from opto import trace -from opto import trainer -from opto.features.gepa.gepa_algorithms import GEPAAlgorithmBase, GEPAUCBSearch, GEPABeamPareto -from opto.features.priority_search import PrioritySearch as SearchAlgorithm -from opto.trainer.loggers import TensorboardLogger - - -class TimeoutError(Exception): - """Custom timeout exception""" - pass - - -def run_with_timeout(task_func, timeout_seconds=300): - """Run a task function with timeout using threading.""" - result = [None] - exception = [None] - - def target(): - try: - result[0] = task_func() - except Exception as e: - exception[0] = e - - thread = threading.Thread(target=target) - thread.daemon = True # Dies when main thread dies - thread.start() - thread.join(timeout=timeout_seconds) - - if thread.is_alive(): - # Timeout occurred - we can't actually kill the thread, but we can return timeout error - raise TimeoutError(f"Task timed out after {timeout_seconds} seconds") - - if exception[0] is not None: - raise exception[0] - - return result[0] - - -# -------------------------------- Utilities -------------------------------- - -def load_benchmark_task(task_dir: Path): - '''Load an benchmark task module from its directory.''' - init_file = task_dir / '__init__.py' - if not init_file.exists(): - raise FileNotFoundError(f"No __init__.py found in {task_dir}") - - spec = importlib.util.spec_from_file_location(task_dir.name, str(init_file)) - mod = importlib.util.module_from_spec(spec) - sys.modules[spec.name] = mod - spec.loader.exec_module(mod) - return mod - -def pick_benchmark_task(tasks_dir: Path, task_key: str) -> Path: - ''' - Resolve an benchmark task directory by fuzzy key. - ''' - cands = [p for p in tasks_dir.iterdir() if p.is_dir()] - # exact - for p in cands: - if p.name == task_key: - return p - # substring - for p in cands: - if task_key in p.name: - return p - raise FileNotFoundError(f'No benchmark task matching: {task_key} in {tasks_dir}') - -# -------------------------------- Bench core -------------------------------- - -def run_one(mod, algo_name: str, algo_cls, *, threads: int, optimizer_kwargs: Dict[str, Any], trainer_overrides: Dict[str, Any], task_name: str) -> Tuple[float, float, Dict[str, Any]]: - '''Run a single algorithm on the benchmark task defined by `mod`.''' - bundle = mod.build_trace_problem(**trainer_overrides.get('eval_kwargs', {})) - param = bundle['param'] - guide = bundle['guide'] - ds = bundle['train_dataset'] - opt_kwargs = (bundle.get('optimizer_kwargs', {}) | (optimizer_kwargs or {})) - - # Store initial parameters for logging - initial_params = getattr(param, 'data', None) - - # Setup TensorBoard logging - timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') - log_dir = f'./logs/{task_name}/{algo_name}/{timestamp}' - logger = TensorboardLogger(log_dir=log_dir) - - # Algorithm params following priority_search_on_convex_fn_BENCH.py style - if algo_name == 'PrioritySearch': - params = dict( - guide=guide, - train_dataset=ds, - score_range=[-10, 10], - num_epochs=1, - num_steps=trainer_overrides.get('ps_steps', 3), - batch_size=1, - num_batches=trainer_overrides.get('ps_batches', 2), - verbose=False, - num_candidates=trainer_overrides.get('ps_candidates', 4), - num_proposals=trainer_overrides.get('ps_proposals', 4), - memory_update_frequency=trainer_overrides.get('ps_mem_update', 2), - optimizer_kwargs=opt_kwargs, - num_threads=threads, - ) - elif algo_name == 'GEPA-Base': - params = dict( - guide=guide, - train_dataset=ds, - validate_dataset=ds, - num_iters=trainer_overrides.get('gepa_iters', 3), - train_batch_size=trainer_overrides.get('gepa_train_bs', 2), - merge_every=trainer_overrides.get('gepa_merge_every', 2), - pareto_subset_size=trainer_overrides.get('gepa_pareto_subset', 4), - num_threads=threads, - optimizer_kwargs=opt_kwargs, - ) - elif algo_name == 'GEPA-UCB': - params = dict( - guide=guide, - train_dataset=ds, - num_search_iterations=trainer_overrides.get('gepa_iters', 3), - train_batch_size=trainer_overrides.get('gepa_train_bs', 2), - merge_every=trainer_overrides.get('gepa_merge_every', 2), - pareto_subset_size=trainer_overrides.get('gepa_pareto_subset', 4), - num_threads=threads, - optimizer_kwargs=opt_kwargs, - ) - elif algo_name == 'GEPA-Beam': - params = dict( - guide=guide, - train_dataset=ds, - validate_dataset=ds, - num_search_iterations=trainer_overrides.get('gepa_iters', 3), - train_batch_size=trainer_overrides.get('gepa_train_bs', 2), - merge_every=trainer_overrides.get('gepa_merge_every', 2), - pareto_subset_size=trainer_overrides.get('gepa_pareto_subset', 4), - num_threads=threads, - optimizer_kwargs=opt_kwargs, - ) - else: - raise ValueError(f'Unknown algorithm name: {algo_name}') - - # Add logger to params - params['logger'] = logger - - # The model is just the single ParameterNode (train wraps it into a Module) - start = time.time() - - # Get timeout from task configuration or use default - task_timeout = trainer_overrides.get('eval_kwargs', {}).get('timeout_seconds', 30) - # Global timeout should be much longer than individual evaluation timeout - global_timeout = max(task_timeout * 10, 300) # At least 5 minutes - - def train_task(): - trainer.train(model=param, algorithm=algo_cls, **params) # runs and mutates `param` - return param - - try: - # Use timeout wrapper to prevent hanging - param = run_with_timeout(train_task, global_timeout) - elapsed = time.time() - start - except TimeoutError as e: - elapsed = time.time() - start - print(f" Training timed out after {global_timeout}s") - # Return current state with timeout indicator - final_code = getattr(param, 'data', None) - score, fb = guide('', final_code or initial_params, ds['infos'][0]) - return (float(score) if score is not None else float('-inf')), elapsed, dict( - feedback=f"Training timed out: {str(e)}", - initial_params=initial_params, - final_params=final_code, - log_dir=log_dir, - timestamp=timestamp, - timeout_occurred=True - ) - - # Evaluate final parameter directly via the guide on one sample (same as ds) - final_code = getattr(param, 'data', None) - score, fb = guide('', final_code, ds['infos'][0]) - - return (float(score) if score is not None else float('-inf')), elapsed, dict( - feedback=fb, - initial_params=initial_params, - final_params=final_code, - log_dir=log_dir, - timestamp=timestamp - ) - -def main(): - ap = argparse.ArgumentParser(description='Run Trace trainers on benchmark LLM4AD tasks.') - ap.add_argument('--tasks', type=str, required=True, help='Folder with benchmark task directories') - ap.add_argument('--task', type=str, required=True, help='Task key(s) (e.g., "circle_packing" or "circle_packing,acrobot,knapsack" for multiple tasks)') - ap.add_argument('--algos', type=str, default='PrioritySearch', help='Comma-separated algorithms: PrioritySearch,GEPA-Base,GEPA-UCB,GEPA-Beam') - ap.add_argument('--threads', type=int, default=2, help='Num threads used by algorithms') - ap.add_argument('--optimizer-kwargs', type=str, default='', help='JSON dict to merge into optimizer_kwargs') - ap.add_argument('--eval-kwargs', type=str, default='', help='JSON dict passed into the evaluator ctor') - # Some knobs - ap.add_argument('--gepa-iters', type=int, default=3) - ap.add_argument('--gepa-train-bs', type=int, default=2) - ap.add_argument('--gepa-merge-every', type=int, default=2) - ap.add_argument('--gepa-pareto-subset', type=int, default=3) - ap.add_argument('--ps-steps', type=int, default=3) - ap.add_argument('--ps-batches', type=int, default=2) - ap.add_argument('--ps-candidates', type=int, default=3) - ap.add_argument('--ps-proposals', type=int, default=3) - ap.add_argument('--ps-mem-update', type=int, default=2) - args = ap.parse_args() - - tasks_dir = Path(args.tasks).resolve() - algo_names = [s.strip() for s in args.algos.split(',') if s.strip()] - algo_map = { - 'PrioritySearch': SearchAlgorithm, - 'GEPA-Base': GEPAAlgorithmBase, - 'GEPA-UCB': GEPAUCBSearch, - 'GEPA-Beam': GEPABeamPareto, - } - - extra_opt = json.loads(args.optimizer_kwargs) if args.optimizer_kwargs else {} - eval_kwargs = json.loads(args.eval_kwargs) if args.eval_kwargs else {} - - # Parse multiple tasks - task_keys = [key.strip() for key in args.task.split(',') if key.strip()] - - trainer_overrides = dict( - eval_kwargs=eval_kwargs, - gepa_iters=args.gepa_iters, - gepa_train_bs=args.gepa_train_bs, - gepa_merge_every=args.gepa_merge_every, - gepa_pareto_subset=args.gepa_pareto_subset, - ps_steps=args.ps_steps, - ps_batches=args.ps_batches, - ps_candidates=args.ps_candidates, - ps_proposals=args.ps_proposals, - ps_mem_update=args.ps_mem_update, - ) - - all_results = [] - - for task_key in task_keys: - print(f"\n{'='*60}") - print(f"PROCESSING TASK: {task_key}") - print(f"{'='*60}") - - try: - task_dir = pick_benchmark_task(tasks_dir, task_key) - mod = load_benchmark_task(task_dir) - except Exception as e: - print(f"Failed to load task {task_key}: {e}") - continue - - task_results = [] - - for name in algo_names: - if name not in algo_map: - print(f'[SKIP] Unknown algo: {name}') - continue - algo_cls = algo_map[name] - print(f"\n=== Running {name} on benchmark task '{task_key}' ===") - try: - score, secs, meta = run_one(mod, name, algo_cls, threads=args.threads, optimizer_kwargs=extra_opt, trainer_overrides=trainer_overrides, task_name=task_key) - print(f"{name}: score={score:.4f} time={secs:.2f}s") - result = dict(task=task_key, algo=name, score=float(score), time=float(secs), meta=meta) - task_results.append(result) - all_results.append(result) - except Exception as e: - print(f"Error running {name} on {task_key}: {e}") - result = dict(task=task_key, algo=name, score=float('-inf'), time=0.0, meta=dict(error=str(e))) - task_results.append(result) - all_results.append(result) - - # Task summary - print(f"\n--- TASK {task_key} SUMMARY ---") - for r in task_results: - if 'error' not in r['meta']: - print(f"{r['algo']:>12} | score={r['score']:.4f} | time={r['time']:.2f}s") - else: - print(f"{r['algo']:>12} | ERROR: {r['meta']['error'][:50]}...") - - results = all_results # Use all_results for final CSV output - - # Overall Summary - print('\n========== OVERALL SUMMARY ==========') - for r in results: - if 'error' not in r['meta']: - print(f"{r['task']:>20} | {r['algo']:>12} | score={r['score']:.4f} | time={r['time']:.2f}s") - else: - print(f"{r['task']:>20} | {r['algo']:>12} | ERROR") - - # CSV Logging - csv_filename = f"results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv" - os.makedirs('./results', exist_ok=True) - csv_path = f'./results/{csv_filename}' - - with open(csv_path, 'w', newline='', encoding='utf-8') as csvfile: - fieldnames = ['timestamp', 'task', 'algo', 'parameters', 'time', 'score', 'initial_params', 'final_params', 'log_dir'] - writer = csv.DictWriter(csvfile, fieldnames=fieldnames) - writer.writeheader() - - for r in results: - # Convert parameters to a single line string - params_str = json.dumps(trainer_overrides, separators=(',', ':')).replace('\\n', '\\\\n') - initial_params_str = str(r['meta'].get('initial_params', '')).replace('\\n', '\\\\n') - final_params_str = str(r['meta'].get('final_params', '')).replace('\\n', '\\\\n') - - writer.writerow({ - 'timestamp': r['meta'].get('timestamp', ''), - 'task': r.get('task', args.task), - 'algo': r['algo'], - 'parameters': params_str, - 'time': r['time'], - 'score': r['score'], - 'initial_params': initial_params_str, - 'final_params': final_params_str, - 'log_dir': r['meta'].get('log_dir', '') - }) - - print(f"\\nResults saved to {csv_path}") - print(f"TensorBoard logs saved to ./logs/{args.task}/") - - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/examples/trainers_benchmark_tasks_validation.py b/examples/trainers_benchmark_tasks_validation.py deleted file mode 100644 index 1b0a867c..00000000 --- a/examples/trainers_benchmark_tasks_validation.py +++ /dev/null @@ -1,385 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -'''validate_benchmark_tasks.py -Quick validation script to test all benchmark tasks with minimal resources. - -This script: -1. Tests if each task can be loaded and built -2. Runs 1 optimization step with PrioritySearch -3. Times each task with a short timeout -4. Identifies which tasks work and can be optimized quickly -''' - -import os -import sys -import time -import signal -import traceback -import importlib.util -import threading -import argparse -from pathlib import Path -from contextlib import contextmanager - -# Add current directory to path for imports -sys.path.append('.') -sys.path.append('./examples/benchmark_tasks') - -from opto.features.priority_search import PrioritySearch as SearchAlgorithm -from opto import trainer - - -class TimeoutError(Exception): - """Custom timeout exception""" - pass - - -def run_with_timeout(task_func, timeout_seconds=5): - """Run a task function with timeout using threading.""" - result = [None] - exception = [None] - - def target(): - try: - result[0] = task_func() - except Exception as e: - exception[0] = e - - thread = threading.Thread(target=target) - thread.daemon = True # Dies when main thread dies - thread.start() - thread.join(timeout=timeout_seconds) - - if thread.is_alive(): - # Timeout occurred - we can't actually kill the thread, but we can return timeout error - raise TimeoutError(f"Task timed out after {timeout_seconds} seconds") - - if exception[0] is not None: - raise exception[0] - - return result[0] - - -@contextmanager -def timeout_context(seconds): - """Context manager for timeout using threading (fallback)""" - def timeout_handler(): - raise TimeoutError(f"Operation timed out after {seconds} seconds") - - timer = threading.Timer(seconds, timeout_handler) - timer.start() - try: - yield - finally: - timer.cancel() - - -def load_benchmark_task(task_dir: Path): - '''Load an benchmark task module from its directory with path isolation.''' - init_file = task_dir / '__init__.py' - if not init_file.exists(): - raise FileNotFoundError(f"No __init__.py found in {task_dir}") - - # Save current sys.path to restore later - original_path = sys.path.copy() - - try: - # Clear sys.path and add only the task directory and essential paths - sys.path.clear() - sys.path.extend([ - str(task_dir), # Task directory first for local imports - '.', # Current directory - ]) - # Add back essential system paths, but exclude any benchmark_tasks paths to prevent conflicts - original_path_filtered = [p for p in original_path if 'benchmark_tasks' not in p] - sys.path.extend(original_path_filtered) - - # Create unique module name to avoid conflicts - module_name = f"benchmark_task_{task_dir.name}_{hash(str(task_dir))}" - - # Clear any cached modules that might cause conflicts - modules_to_remove = [k for k in sys.modules.keys() if k.startswith('get_instance') or k.startswith('benchmark_task_')] - for mod in modules_to_remove: - sys.modules.pop(mod, None) - - spec = importlib.util.spec_from_file_location(module_name, str(init_file)) - mod = importlib.util.module_from_spec(spec) - sys.modules[module_name] = mod # Add to sys.modules to avoid import issues - spec.loader.exec_module(mod) - return mod - - finally: - # Restore original sys.path - sys.path.clear() - sys.path.extend(original_path) - - -def _load_task_internal(task_name, task_dir): - """Internal function for loading task (for multiprocessing)""" - print(f" Loading task module...") - mod = load_benchmark_task(task_dir) - - print(f" Building trace problem...") - problem = mod.build_trace_problem() - - # Get initial score - print(f" Getting initial evaluation...") - guide = problem['guide'] - param = problem['param'] - initial_code = param.data - task_desc = problem['train_dataset']['inputs'][0] - info = problem['train_dataset']['infos'][0] - - score, feedback = guide.get_feedback(task_desc, initial_code, info) - - return { - 'status': 'SUCCESS', - 'initial_score': score, - 'entry_function': problem['metadata']['entry'], - 'benchmark': problem['metadata']['benchmark'], - 'feedback_preview': feedback[:100] + '...' if len(feedback) > 100 else feedback - } - - -def test_task_loading(task_name, task_dir): - """Test if a task can be loaded and built""" - try: - # Use timeout for robust task loading - result = run_with_timeout(lambda: _load_task_internal(task_name, task_dir), 5) - return result - except TimeoutError as e: - return { - 'status': 'FAILED', - 'error': f'Task loading timed out after 5s', - 'error_type': 'TimeoutError' - } - except Exception as e: - return { - 'status': 'FAILED', - 'error': str(e), - 'error_type': type(e).__name__ - } - - -def _optimize_task_internal(task_name, task_dir): - """Internal function for optimization (for multiprocessing)""" - print(f" Loading for optimization...") - mod = load_benchmark_task(task_dir) - problem = mod.build_trace_problem() - - print(f" Setting up optimization...") - param = problem['param'] - guide = problem['guide'] - ds = problem['train_dataset'] - opt_kwargs = problem.get('optimizer_kwargs', {}) - - # Minimal PrioritySearch parameters - params = dict( - guide=guide, - train_dataset=ds, - score_range=[-10, 10], - num_epochs=1, - num_steps=1, # Just 1 step - batch_size=1, - num_batches=1, # Just 1 batch - verbose=False, - num_candidates=2, # Minimal candidates - num_proposals=2, # Minimal proposals - memory_update_frequency=2, - optimizer_kwargs=opt_kwargs, - num_threads=1, - ) - - print(f" Running optimization...") - start_time = time.time() - trainer.train(model=param, algorithm=SearchAlgorithm, **params) - elapsed = time.time() - start_time - - # Get final score - print(f" Getting final score...") - final_code = getattr(param, 'data', None) - final_score, _ = guide('', final_code, ds['infos'][0]) - - return { - 'status': 'OPTIMIZED', - 'optimization_time': elapsed, - 'final_score': final_score, - 'can_optimize': True - } - - -def test_task_optimization(task_name, task_dir, max_time=5): - """Test if a task can run optimization with minimal resources""" - try: - # Use timeout for robust optimization testing - result = run_with_timeout(lambda: _optimize_task_internal(task_name, task_dir), max_time) - return result - - except TimeoutError as e: - return { - 'status': 'TIMEOUT', - 'optimization_time': max_time, - 'can_optimize': False, - 'error': f'Optimization timed out after {max_time}s' - } - except Exception as e: - return { - 'status': 'OPT_FAILED', - 'can_optimize': False, - 'error': str(e), - 'error_type': type(e).__name__ - } - - -def pick_benchmark_task(tasks_dir: Path, task_key: str) -> Path: - ''' - Resolve an benchmark task directory by fuzzy key. - ''' - cands = [p for p in tasks_dir.iterdir() if p.is_dir()] - # exact - for p in cands: - if p.name == task_key: - return p - # substring - for p in cands: - if task_key in p.name: - return p - raise FileNotFoundError(f'No benchmark task matching: {task_key} in {tasks_dir}') - - -def main(): - ap = argparse.ArgumentParser(description='Validate benchmark LLM4AD tasks.') - ap.add_argument('--tasks', type=str, default='./examples/benchmark_tasks', help='Folder with benchmark task directories') - ap.add_argument('--task', type=str, help='Specific task key(s) to test, comma-separated (e.g., "circle_packing" or "optimization_bp_2d_construct,optimization_set_cover_construct")') - args = ap.parse_args() - - # Threading-based timeout doesn't need multiprocessing setup - - tasks_dir = Path(args.tasks) - if not tasks_dir.exists(): - print(f"Tasks directory not found: {tasks_dir}") - return - - # Filter tasks based on --task parameter - if args.task: - task_keys = [key.strip() for key in args.task.split(',') if key.strip()] - task_dirs = [] - for task_key in task_keys: - try: - task_dir = pick_benchmark_task(tasks_dir, task_key) - task_dirs.append(task_dir) - except FileNotFoundError as e: - print(f"Warning: {e}") - - if not task_dirs: - print("No valid tasks found!") - return - - print(f"Testing {len(task_dirs)} specific task(s): {[d.name for d in task_dirs]}") - else: - task_dirs = [d for d in tasks_dir.iterdir() if d.is_dir()] - print(f"Found {len(task_dirs)} benchmark tasks to validate") - - results = {} - working_tasks = [] - optimizable_tasks = [] - - for i, task_dir in enumerate(task_dirs, 1): - task_name = task_dir.name - print(f"\\n[{i}/{len(task_dirs)}] Testing {task_name}...") - - try: - # Test loading (has its own robust timeout) - load_result = test_task_loading(task_name, task_dir) - results[task_name] = load_result - - print(f" Loading: {load_result['status']}") - if load_result['status'] == 'SUCCESS': - print(f" Entry: {load_result['entry_function']}") - print(f" Initial score: {load_result['initial_score']}") - working_tasks.append(task_name) - - # Test optimization for all working tasks, including those with -inf scores - # The updated llm4ad_loader should handle -inf more gracefully - opt_result = test_task_optimization(task_name, task_dir) - results[task_name].update(opt_result) - print(f" Optimization: {opt_result['status']}") - if opt_result['status'] == 'OPTIMIZED': - print(f" Time: {opt_result['optimization_time']:.2f}s") - print(f" Final score: {opt_result['final_score']}") - optimizable_tasks.append(task_name) - elif opt_result['status'] in ['TIMEOUT', 'OPT_FAILED']: - print(f" Error: {opt_result.get('error', 'Unknown')}") - - # Mark as optimizable if it completed without major errors - if opt_result['status'] in ['OPTIMIZED']: - results[task_name]['can_optimize'] = True - else: - results[task_name]['can_optimize'] = False - - else: - print(f" Error: {load_result['error']}") - - except KeyboardInterrupt: - print(f"\\nKeyboard interrupt - stopping validation") - break - except Exception as e: - print(f" UNEXPECTED ERROR: {e}") - results[task_name] = { - 'status': 'FAILED', - 'error': f'Unexpected error: {str(e)}', - 'error_type': type(e).__name__ - } - - # Summary - print(f"\\n{'='*60}") - print(f"VALIDATION SUMMARY") - print(f"{'='*60}") - print(f"Total tasks: {len(task_dirs)}") - print(f"Successfully loaded: {len(working_tasks)}") - print(f"Can optimize quickly: {len(optimizable_tasks)}") - - print(f"\\nWORKING TASKS ({len(working_tasks)}):") - for task in working_tasks: - result = results[task] - score = result['initial_score'] - print(f" {task}: {result['entry_function']} (score: {score})") - - print(f"\\nQUICKLY OPTIMIZABLE TASKS ({len(optimizable_tasks)}):") - for task in optimizable_tasks: - result = results[task] - print(f" {task}: {result['optimization_time']:.2f}s (final: {result['final_score']})") - - print(f"\\nFAILED TASKS ({len(task_dirs) - len(working_tasks)}):") - failed_tasks = [name for name, result in results.items() if result['status'] == 'FAILED'] - error_summary = {} - for task in failed_tasks: - error_type = results[task].get('error_type', 'Unknown') - if error_type not in error_summary: - error_summary[error_type] = [] - error_summary[error_type].append(task) - - for error_type, tasks in error_summary.items(): - print(f" {error_type} ({len(tasks)}): {', '.join(tasks[:3])}{'...' if len(tasks) > 3 else ''}") - - # Save detailed results - import json - with open('benchmark_tasks_validation.json', 'w') as f: - # Convert any non-serializable values - serializable_results = {} - for task, result in results.items(): - serializable_result = {} - for k, v in result.items(): - if isinstance(v, (int, float, str, bool, type(None))): - serializable_result[k] = v - else: - serializable_result[k] = str(v) - serializable_results[task] = serializable_result - - json.dump(serializable_results, f, indent=2) - - print(f"\\nDetailed results saved to benchmark_tasks_validation.json") - - -if __name__ == '__main__': - main() \ No newline at end of file From 513cb6b6dfdf1312e0e71a28b1662348c458c073 Mon Sep 17 00:00:00 2001 From: chinganc Date: Fri, 3 Oct 2025 17:13:34 +0000 Subject: [PATCH 294/314] comment out resume --- opto/trainer/train.py | 63 ++++++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/opto/trainer/train.py b/opto/trainer/train.py index 7cb734e1..ab33862c 100644 --- a/opto/trainer/train.py +++ b/opto/trainer/train.py @@ -15,37 +15,38 @@ def dataset_check(dataset): assert len(dataset['inputs'])==len(dataset['infos']), "Inputs and infos must have the same length" -def resume( - save_path: str, - *, - algorithm: Union[Trainer, str] = 'MinibatchAlgorithm', - model: trace.Module, - train_dataset: dict, - validate_dataset = None, - test_dataset = None, - **kwargs): - """ Resume training from a checkpoint. - - Args: - model: the model to be trained - train_dataset: the training dataset - resume_training: path to the checkpoint - validate_dataset: the validation dataset - test_dataset: the test dataset - **kwargs: additional keyword arguments for the training method. If not provided, the same parameters as the last training call are used. - """ - dataset_check(train_dataset) - trainer_class = load_trainer_class(algorithm) - assert issubclass(trainer_class, Trainer) - assert isinstance(save_path, str), "resume_training must be a path string." - assert hasattr(trainer_class, 'resume'), f"{trainer_class} does not support resume." - assert hasattr(trainer_class, 'load'), f"{trainer_class} does not support load." - algo = trainer_class.load(save_path) # load the saved state - return algo.resume(model=model, - train_dataset=train_dataset, - validate_dataset=validate_dataset, - test_dataset=test_dataset, - **kwargs) +# TODO finish implementing resume function +# def resume( +# save_path: str, +# *, +# algorithm: Union[Trainer, str] = 'MinibatchAlgorithm', +# model: trace.Module, +# train_dataset: dict, +# validate_dataset = None, +# test_dataset = None, +# **kwargs): +# """ Resume training from a checkpoint. + +# Args: +# model: the model to be trained +# train_dataset: the training dataset +# resume_training: path to the checkpoint +# validate_dataset: the validation dataset +# test_dataset: the test dataset +# **kwargs: additional keyword arguments for the training method. If not provided, the same parameters as the last training call are used. +# """ +# dataset_check(train_dataset) +# trainer_class = load_trainer_class(algorithm) +# assert issubclass(trainer_class, Trainer) +# assert isinstance(save_path, str), "resume_training must be a path string." +# assert hasattr(trainer_class, 'resume'), f"{trainer_class} does not support resume." +# assert hasattr(trainer_class, 'load'), f"{trainer_class} does not support load." +# algo = trainer_class.load(save_path) # load the saved state +# return algo.resume(model=model, +# train_dataset=train_dataset, +# validate_dataset=validate_dataset, +# test_dataset=test_dataset, +# **kwargs) def train( From 3d521574505d61bdb2d34fb8390eef62e535ae14 Mon Sep 17 00:00:00 2001 From: chinganc Date: Fri, 3 Oct 2025 17:19:07 +0000 Subject: [PATCH 295/314] comment out importing resume --- opto/trainer/__init__.py | 2 +- tests/unit_tests/test_priority_search.py | 74 ++++++++++++------------ 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/opto/trainer/__init__.py b/opto/trainer/__init__.py index 679e14d5..fdb4b478 100644 --- a/opto/trainer/__init__.py +++ b/opto/trainer/__init__.py @@ -1 +1 @@ -from opto.trainer.train import train, resume \ No newline at end of file +from opto.trainer.train import train #, resume \ No newline at end of file diff --git a/tests/unit_tests/test_priority_search.py b/tests/unit_tests/test_priority_search.py index f63d83cb..a7ff24d3 100644 --- a/tests/unit_tests/test_priority_search.py +++ b/tests/unit_tests/test_priority_search.py @@ -232,40 +232,40 @@ def test_resume(): os.system(f"rm -rf {save_path}") -def test_trainer_train_and_resume(): - - dummy_llm = DummyLLM(_llm_callable) - agent = Agent() - optimizer = OptoPrimeV2( - agent.parameters(), - llm=dummy_llm, - ) - - trainer.train( - algorithm='PrioritySearch', - model=agent, - optimizer=optimizer, - guide=Guide(), - train_dataset=dataset, - batch_size=batch_size, - num_batches=num_batches, - num_threads=num_threads, - num_candidates=num_candidates, - num_proposals=num_proposals, - long_term_memory_size=long_term_memory_size, - memory_update_frequency=memory_update_frequency, - verbose=False, #'output', - save_path="./test_priority_search_save_trainer", - save_frequency=1, - num_epochs=num_epochs, - ) - - new_agent = Agent() - trainer.resume( - "./test_priority_search_save_trainer", - algorithm='PrioritySearch', - model=new_agent, - train_dataset=dataset, - num_epochs=num_epochs+2) - - os.system(f"rm -rf ./test_priority_search_save_trainer") \ No newline at end of file +# def test_trainer_train_and_resume(): + +# dummy_llm = DummyLLM(_llm_callable) +# agent = Agent() +# optimizer = OptoPrimeV2( +# agent.parameters(), +# llm=dummy_llm, +# ) + +# trainer.train( +# algorithm='PrioritySearch', +# model=agent, +# optimizer=optimizer, +# guide=Guide(), +# train_dataset=dataset, +# batch_size=batch_size, +# num_batches=num_batches, +# num_threads=num_threads, +# num_candidates=num_candidates, +# num_proposals=num_proposals, +# long_term_memory_size=long_term_memory_size, +# memory_update_frequency=memory_update_frequency, +# verbose=False, #'output', +# save_path="./test_priority_search_save_trainer", +# save_frequency=1, +# num_epochs=num_epochs, +# ) + +# new_agent = Agent() +# trainer.resume( +# "./test_priority_search_save_trainer", +# algorithm='PrioritySearch', +# model=new_agent, +# train_dataset=dataset, +# num_epochs=num_epochs+2) + +# os.system(f"rm -rf ./test_priority_search_save_trainer") \ No newline at end of file From b759b0ab5b2038dcf9ad32ddeabfc51f97229f50 Mon Sep 17 00:00:00 2001 From: windweller Date: Fri, 3 Oct 2025 16:05:59 -0400 Subject: [PATCH 296/314] update compose.py forward and system_prompt behavior --- opto/features/flows/compose.py | 37 +++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/opto/features/flows/compose.py b/opto/features/flows/compose.py index 96b8a9be..f075ac4b 100644 --- a/opto/features/flows/compose.py +++ b/opto/features/flows/compose.py @@ -19,6 +19,7 @@ USED_TracedLLM = contextvars.ContextVar('USED_TracedLLM', default=list()) + class ChatHistory: def __init__(self, max_len=50, auto_summary=False): """Initialize chat history for multi-turn conversation. @@ -145,6 +146,7 @@ class TracedLLM: TracedLLM_response1 = TracedLLM.forward.call_llm(args_0=system_prompt0, args_1=TracedLLM0_user_query0, args_2=TracedLLM_response0, args_3=TracedLLM0_user_query1) TracedLLM_response2 = TracedLLM.forward.call_llm(args_0=system_prompt0, args_1=TracedLLM0_user_query0, args_2=TracedLLM_response0, args_3=TracedLLM0_user_query1, args_4=TracedLLM_response1, args_5=TracedLLM0_user_query2) """ + def __init__(self, system_prompt: Union[str, None, trace.Node] = None, llm: AbstractModel = None, chat_history_on=False, @@ -159,10 +161,12 @@ def __init__(self, if system_prompt is None: system_prompt = "You are a helpful assistant." - self.system_prompt = system_prompt if isinstance(system_prompt, trace.Node) else trace.node(system_prompt, - name='system_prompt', - description=DEFAULT_SYSTEM_PROMPT_DESCRIPTION, - trainable=trainable) + self.system_prompt = trace.node(system_prompt, name='system_prompt', + description=DEFAULT_SYSTEM_PROMPT_DESCRIPTION, + trainable=trainable) + # if system_prompt is already a node, then we have to override its trainable attribute + self.system_prompt.trainable = trainable + if llm is None: llm = LLM() assert isinstance(llm, AbstractModel), f"{llm} must be an instance of AbstractModel" @@ -174,8 +178,13 @@ def __init__(self, self.model_name = model_name if model_name else f"TracedLLM{len(current_llm_sessions)}" current_llm_sessions.append(1) # just a marker - def forward(self, user_query: str) -> str: + def forward(self, user_query: str, chat_history_on: Optional[bool] = None) -> str: """This function takes user_query as input, and returns the response from the LLM, with the system prompt prepended. + This method will always save chat history. + + If chat_history_on is set to False, the chat history will not be included in the LLM input. + If chat_history_on is None, it will use the class-level chat_history_on setting. + If chat_history_on is True, the chat history will be included in the LLM input. Args: user_query: The user query to send to the LLM @@ -183,8 +192,11 @@ def forward(self, user_query: str) -> str: Returns: str: For direct pattern """ + chat_history_on = self.chat_history_on if chat_history_on is None else chat_history_on + messages = [{"role": "system", "content": self.system_prompt.data}] - messages.extend(self.chat_history.get_messages()) + if chat_history_on: + messages.extend(self.chat_history.get_messages()) messages.append({"role": "user", "content": user_query}) response = self.llm(messages=messages) @@ -201,17 +213,18 @@ def call_llm(*args) -> str: return response.choices[0].message.content user_query_node = trace.node(user_query, name=f"{self.model_name}_user_query") - arg_list = ([self.system_prompt] + self.chat_history.get_messages_as_node(self.model_name) - + [user_query_node]) + arg_list = [self.system_prompt] + if chat_history_on: + arg_list += self.chat_history.get_messages_as_node(self.model_name) + arg_list += [user_query_node] response_node = call_llm(*arg_list) # save to chat history - if self.chat_history_on: - self.chat_history.add(user_query_node, role="user") - self.chat_history.add(response_node, role="assistant") + self.chat_history.add(user_query_node, role="user") + self.chat_history.add(response_node, role="assistant") return response_node def chat(self, user_query: str) -> str: - return self.forward(user_query) \ No newline at end of file + return self.forward(user_query) From 048a008b194e7c07b112888c74036beae0fb03b1 Mon Sep 17 00:00:00 2001 From: windweller Date: Fri, 3 Oct 2025 16:08:10 -0400 Subject: [PATCH 297/314] revert call_llm signature back --- opto/trace/operators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opto/trace/operators.py b/opto/trace/operators.py index e02b922c..e319d070 100644 --- a/opto/trace/operators.py +++ b/opto/trace/operators.py @@ -590,7 +590,7 @@ def set_update(x: Any, y: Any): @bundle(catch_execution_error=False) -def call_llm(llm, system_prompt: str, *user_prompts: List[str], **kwargs) -> str: +def call_llm(llm, system_prompt: str, *user_prompts, **kwargs) -> str: """Call the LLM model. Args: From 83f11a28d1cab5220e119acc587ad0723c36e541 Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 8 Oct 2025 01:59:58 -0400 Subject: [PATCH 298/314] small enhancement to LLM module --- opto/features/flows/compose.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/opto/features/flows/compose.py b/opto/features/flows/compose.py index f075ac4b..dff95bbc 100644 --- a/opto/features/flows/compose.py +++ b/opto/features/flows/compose.py @@ -175,7 +175,7 @@ def __init__(self, self.chat_history_on = chat_history_on current_llm_sessions = USED_TracedLLM.get() - self.model_name = model_name if model_name else f"TracedLLM{len(current_llm_sessions)}" + self.model_name = model_name if model_name else f"{self.__class__.__name__}{len(current_llm_sessions)}" current_llm_sessions.append(1) # just a marker def forward(self, user_query: str, chat_history_on: Optional[bool] = None) -> str: @@ -201,12 +201,11 @@ def forward(self, user_query: str, chat_history_on: Optional[bool] = None) -> st response = self.llm(messages=messages) - @trace.bundle(output_name="TracedLLM_response") - def call_llm(*args) -> str: + @trace.bundle(output_name=f"{self.model_name}_response") + def call_llm(*messages) -> str: """Call the LLM model. Args: - All the conversation history so far, starting from system prompt, to alternating user/assistant messages, ending with the current user query. - + messages: All the conversation history so far, starting from system prompt, to alternating user/assistant messages, ending with the current user query. Returns: response from the LLM """ From 6e07db365d84ab405ee0be53e0bb192cd5b2d975 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 9 Oct 2025 22:06:51 +0000 Subject: [PATCH 299/314] Fix the bug that sometimes the number of new candidates generated can be larger than the allowed number of candidates --- .../streaming_priority_search.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/opto/features/priority_search/streaming_priority_search.py b/opto/features/priority_search/streaming_priority_search.py index 4bb14e6e..19d83257 100644 --- a/opto/features/priority_search/streaming_priority_search.py +++ b/opto/features/priority_search/streaming_priority_search.py @@ -57,8 +57,21 @@ def match_candidates_and_samples( scores = [self.compute_exploration_priority(c) for c, _ in candidate_batchrollouts_list] # We use the top K to improve over, where K is determined by exploration_ratio. - K = int(self.num_candidates * self._exploration_ratio) - K += max(0, self.num_candidates - len(self._exploration_candidates) - K - len(self.memory)) # ensure we have enough candidates to explore + + # ensure it is possible to select K>=1 such that K * num_proposals <= num_candidates * exploration_ratio + if self.num_proposals > self.num_candidates * self._exploration_ratio: + print(f"Warning: num_proposals {self.num_proposals} is greater than num_candidates {self.num_candidates * self._exploration_ratio}. Setting num_proposals to num_candidates * exploration_ratio.") + self.num_proposals = int(self.num_candidates * self._exploration_ratio) + + currently_available = len(self._exploration_candidates) + len(self.memory) + K = max(int(self.num_candidates * self._exploration_ratio / self.num_proposals), 1) # K>=1 + # make sure we have enough candidates to explore + if K * self.num_proposals + currently_available < self.num_candidates: + # Increase K to ensure we have enough candidates + K += int((self.num_candidates - (K * self.num_proposals + currently_available)) / self.num_proposals) + # make sure K * self.num_proposals <= self.num_candidates + K = min(K, int(self.num_candidates / self.num_proposals)) + # Randomly sample K candidates from the pool if len(candidate_batchrollouts_list) <= K: return matched_candidates_and_samples @@ -85,7 +98,7 @@ def validate(self, assert self._exploration_candidates is not None, "exploration_candidates must be set before calling validate." results = {c: [] for c in (exploration_candidates + candidates)} # dict of ModuleCandidate id: (ModuleCandidate, list of rollouts) print(f'Adding {len(exploration_candidates)} exploration candidates and {len(candidates)} proposed candidates to validate results.') - assert len(candidates) <= self.num_candidates, f"Number of proposed candidates {len(candidates)} must be less than num_candidates {self.num_candidates}." + assert len(candidates) <= self.num_candidates, f"Number of proposed candidates {len(candidates)} must be no larger than num_candidates {self.num_candidates}." if len(candidates) == self.num_candidates: print("Warning: Number of proposed candidates is equal to num_candidates. Running in pure exploration mode.") # remove this assertion since some candidates might be duplicates From 369ec02053b656f1aaccfd98aa1416b7242eef2c Mon Sep 17 00:00:00 2001 From: Ching-An Cheng Date: Fri, 10 Oct 2025 13:17:06 -0700 Subject: [PATCH 300/314] Update opto/features/priority_search/streaming_priority_search.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- opto/features/priority_search/streaming_priority_search.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/opto/features/priority_search/streaming_priority_search.py b/opto/features/priority_search/streaming_priority_search.py index 19d83257..d0699a0c 100644 --- a/opto/features/priority_search/streaming_priority_search.py +++ b/opto/features/priority_search/streaming_priority_search.py @@ -59,9 +59,10 @@ def match_candidates_and_samples( # We use the top K to improve over, where K is determined by exploration_ratio. # ensure it is possible to select K>=1 such that K * num_proposals <= num_candidates * exploration_ratio - if self.num_proposals > self.num_candidates * self._exploration_ratio: - print(f"Warning: num_proposals {self.num_proposals} is greater than num_candidates {self.num_candidates * self._exploration_ratio}. Setting num_proposals to num_candidates * exploration_ratio.") - self.num_proposals = int(self.num_candidates * self._exploration_ratio) + max_proposals = self.num_candidates * self._exploration_ratio + if self.num_proposals > max_proposals: + print(f"Warning: num_proposals {self.num_proposals} is greater than num_candidates {max_proposals}. Setting num_proposals to num_candidates * exploration_ratio.") + self.num_proposals = int(max_proposals) currently_available = len(self._exploration_candidates) + len(self.memory) K = max(int(self.num_candidates * self._exploration_ratio / self.num_proposals), 1) # K>=1 From 278c00ddc29c4d7df2b5de2f700cc51af3518c57 Mon Sep 17 00:00:00 2001 From: Ching-An Cheng Date: Fri, 10 Oct 2025 13:17:19 -0700 Subject: [PATCH 301/314] Update opto/features/priority_search/streaming_priority_search.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- opto/features/priority_search/streaming_priority_search.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/opto/features/priority_search/streaming_priority_search.py b/opto/features/priority_search/streaming_priority_search.py index d0699a0c..5b7db8cd 100644 --- a/opto/features/priority_search/streaming_priority_search.py +++ b/opto/features/priority_search/streaming_priority_search.py @@ -69,7 +69,8 @@ def match_candidates_and_samples( # make sure we have enough candidates to explore if K * self.num_proposals + currently_available < self.num_candidates: # Increase K to ensure we have enough candidates - K += int((self.num_candidates - (K * self.num_proposals + currently_available)) / self.num_proposals) + additional_candidates_needed = int((self.num_candidates - (K * self.num_proposals + currently_available)) / self.num_proposals) + K += additional_candidates_needed # make sure K * self.num_proposals <= self.num_candidates K = min(K, int(self.num_candidates / self.num_proposals)) From 91c75167b8c1df271ad98bbe7bb93ed211884082 Mon Sep 17 00:00:00 2001 From: Allen Nie Date: Mon, 13 Oct 2025 10:47:35 -0700 Subject: [PATCH 302/314] Increase max_tokens and initial_var_char_limit Increase default character limit for v2 optimizers --- opto/optimizers/optoprime_v2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index 0e099c3e..b567660f 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -387,9 +387,9 @@ def __init__( # ignore the type conversion error when extracting updated values from LLM's suggestion include_example=False, memory_size=0, # Memory size to store the past feedback - max_tokens=4096, + max_tokens=8192, log=True, - initial_var_char_limit=100, + initial_var_char_limit=2000, optimizer_prompt_symbol_set: OptimizerPromptSymbolSet = None, use_json_object_format=True, # whether to use json object format for the response when calling LLM truncate_expression=truncate_expression, From 8847dcf0677b7d326e23647cfbcd556c438b5dca Mon Sep 17 00:00:00 2001 From: Allen Nie Date: Mon, 13 Oct 2025 11:00:15 -0700 Subject: [PATCH 303/314] Set default value for optimizer_prompt_symbol_set This is a change discussed and agreed upon a long time ago, but it lives in a branch/PR that has not been merged yet. Lifting this change and adding it to the experimental branch first. --- opto/optimizers/optoprime_v2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index b567660f..a512af8e 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -390,7 +390,7 @@ def __init__( max_tokens=8192, log=True, initial_var_char_limit=2000, - optimizer_prompt_symbol_set: OptimizerPromptSymbolSet = None, + optimizer_prompt_symbol_set: OptimizerPromptSymbolSet = OptimizerPromptSymbolSet(), use_json_object_format=True, # whether to use json object format for the response when calling LLM truncate_expression=truncate_expression, **kwargs, From 994e0f2a45311d2255803c128c970e382ae109b3 Mon Sep 17 00:00:00 2001 From: windweller Date: Tue, 4 Nov 2025 15:42:39 -0500 Subject: [PATCH 304/314] fix a compatibility bug for Python 3.13 --- opto/trace/bundle.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/opto/trace/bundle.py b/opto/trace/bundle.py index 4d7b2b99..a6595b72 100644 --- a/opto/trace/bundle.py +++ b/opto/trace/bundle.py @@ -8,6 +8,7 @@ import asyncio from typing import List, Dict, Callable, Union, Any +from collections.abc import Mapping from opto.trace.broadcast import recursive_conversion from opto.trace.errors import ExecutionError, TraceMissingInputsError @@ -136,8 +137,8 @@ def __init__( ): assert _ldict is None or isinstance( - _ldict, dict - ), "_ldict must be a dictionary. or None" + _ldict, Mapping + ), "_ldict must be a dictionary or None." self._ldict = {} if _ldict is None else _ldict.copy() assert callable(fun), "fun must be a callable." From c480cbd5101c38d58b9403e49ce4cb4cdd5ec88e Mon Sep 17 00:00:00 2001 From: doxav Date: Thu, 6 Nov 2025 19:44:12 +0100 Subject: [PATCH 305/314] Improved GEPA-UCB with score_range clamping --- opto/features/gepa/gepa_algorithms.py | 27 +- opto/trainer/algorithms/gepa_algorithms.py | 841 --------------------- 2 files changed, 23 insertions(+), 845 deletions(-) delete mode 100644 opto/trainer/algorithms/gepa_algorithms.py diff --git a/opto/features/gepa/gepa_algorithms.py b/opto/features/gepa/gepa_algorithms.py index 7494c0ca..5684aba2 100644 --- a/opto/features/gepa/gepa_algorithms.py +++ b/opto/features/gepa/gepa_algorithms.py @@ -224,6 +224,14 @@ class GEPAUCBSearch(UCBSearchAlgorithm): - Optional periodic Merge crossover (uniform per-parameter) with desirability checks """ + def _rank(self, raw: float) -> float: + """ + If a score_range is provided (lo, hi), clamp the scalar score into that band. + This keeps UCB-like behavior numerically stable without changing external APIs. + """ + if getattr(self, "score_range", None) is None or raw is None: return raw + lo, hi = self.score_range; return float(min(hi, max(lo, raw))) + def __init__(self, agent, optimizer=None, @@ -270,6 +278,7 @@ def train(self, pareto_subset_size: int = 24, num_search_iterations: int = 120, train_batch_size: int = 2, + score_range: Optional[Tuple[float, float]] = None, merge_every: int = 6, log_frequency: Optional[int] = None, save_frequency: Optional[int] = None, @@ -282,6 +291,7 @@ def train(self, num_threads = num_threads or self.num_threads log_frequency = log_frequency or 5 validate_ds = validation_dataset or train_dataset + self.score_range = score_range # Optional score clamping band for mean-based selections # Fix a Pareto subset (small, stable) to compute per-instance vectors assert len(validate_ds["inputs"]) > 0, "Empty dataset." @@ -296,10 +306,13 @@ def train(self, # Seed with current params base_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} - v0, m0 = self._evaluate_on_pareto(base_params, guide, num_threads=num_threads) - buffer.append(Candidate(params=base_params, eval_vector=v0, mean=m0, id=self._next_id(), ancestors=set())) + v0, m0_raw = self._evaluate_on_pareto(base_params, guide, num_threads=num_threads) + m0 = self._rank(m0_raw) + buffer.append(Candidate(params=base_params, eval_vector=v0, mean=m0, id=self._next_id(), + ancestors=set(), meta={"raw_mean": m0_raw})) print_color(f"[GEPA] Seed candidate mean={m0:.4f}", "cyan") + metrics = {"best_means": [], "new_child_means": [], "merge_accepts": 0, "total_merges": 0} for it in range(1, num_search_iterations + 1): @@ -322,14 +335,16 @@ def train(self, continue # Evaluate child on Pareto subset - child_vec, child_mean = self._evaluate_on_pareto(update_dict, guide, num_threads=num_threads) + child_vec, child_mean_raw = self._evaluate_on_pareto(update_dict, guide, num_threads=num_threads) + child_mean = self._rank(child_mean_raw) child = Candidate(params=update_dict, eval_vector=child_vec, mean=child_mean, id=self._next_id(), parent_ids=(parent.id,), ancestors=set(parent.ancestors) | {parent.id}, - created_iter=it) + created_iter=it, + meta={"raw_mean": child_mean_raw}) buffer.append(child) metrics["new_child_means"].append(child_mean) print_color(f"[GEPA] iter {it}: child mean={child_mean:.4f} (train-batch≈{train_batch_mean})", "green") @@ -347,6 +362,10 @@ def train(self, if merged is not None: merged.id = self._next_id() merged.created_iter = it + # preserve raw and clamp to range for ranking/logging + _raw = merged.mean + merged.meta["raw_mean"] = _raw + merged.mean = self._rank(_raw) buffer.append(merged) metrics["merge_accepts"] += 1 print_color(f"[GEPA] Merge accepted: mean={merged.mean:.4f}", "magenta") diff --git a/opto/trainer/algorithms/gepa_algorithms.py b/opto/trainer/algorithms/gepa_algorithms.py deleted file mode 100644 index c0283e38..00000000 --- a/opto/trainer/algorithms/gepa_algorithms.py +++ /dev/null @@ -1,841 +0,0 @@ -# opto/trainer/algorithms/gepa_algorithms.py -# GEPA (+Merge) algorithms for Trace -# - GEPAUCBSearch: subclass of UCBSearchAlgorithm -# - GEPABeamPareto: subclass of BeamsearchAlgorithm (Pareto select + single-parent incremental) -# - GEPATrainer: subclass of Trainer (minimal GEPA loop) -# -# All default to OptoPrimeV2 if optimizer=None. - -from __future__ import annotations -import copy -import math -import random -import functools -import types -from dataclasses import dataclass, field -from typing import Any, Dict, List, Optional, Tuple - -import numpy as np - -from opto.optimizers.optoprime_v2 import OptoPrimeV2 -from opto.trace.nodes import ParameterNode -from opto.trainer.algorithms.UCBsearch import UCBSearchAlgorithm -from opto.trainer.algorithms.beamsearch_algorithm import BeamsearchAlgorithm -from opto.trainer.algorithms.algorithm import Trainer -from opto.trainer.algorithms.basic_algorithms import ( - evaluate, - batchify, - standard_optimization_step, -) -from opto.trainer.utils import async_run -# Prefer thread-safe batched runner (deep-copies per task). Fallback handled at callsite. -try: - from opto.trainer.utils import batch_run # type: ignore -except Exception: # pragma: no cover - batch_run = None -from opto.optimizers.utils import print_color - - -# ----------------------------- Utilities ----------------------------- # - -@dataclass -class Candidate: - params: Dict[ParameterNode, Any] - eval_vector: List[float] # per-instance scores on fixed Pareto subset - mean: float - id: int - parent_ids: Tuple[int, ...] = field(default_factory=tuple) - ancestors: set = field(default_factory=set) - created_iter: int = 0 - wins: int = 0 # updated by Pareto accounting - meta: Dict[str, Any] = field(default_factory=dict) # freeform - -def _eval_on_subset(agent, guide, xs, infos, *, num_threads: Optional[int], desc: str) -> List[float]: - return evaluate(agent, guide, xs, infos, min_score=None, num_threads=num_threads, description=desc) - -def _compute_pareto_counts(cands: List[Candidate]) -> None: - """ - "Best-for-at-least-one-instance" winners. - For each position m in eval vectors, find argmax candidate and credit a win. - """ - if not cands: - return - L = len(cands[0].eval_vector) - # Reset - for c in cands: - c.wins = 0 - # Credit wins - for m in range(L): - best_idx = None - best_val = -float("inf") - for i, c in enumerate(cands): - v = c.eval_vector[m] if m < len(c.eval_vector) else -float("inf") - if v > best_val: - best_val, best_idx = v, i - if best_idx is not None: - cands[best_idx].wins += 1 - -def _pareto_sample(cands: List[Candidate], *, temperature: float = 1.0, rng: random.Random) -> Candidate: - """ - Sample a parent from union of per-instance winners, proportional to wins^1/T. - """ - if not cands: - raise ValueError("Empty candidate buffer.") - _compute_pareto_counts(cands) - wins = np.array([max(1, c.wins) for c in cands], dtype=float) # avoid zero - if temperature <= 0: - # Deterministic pick - return cands[int(wins.argmax())] - weights = wins ** (1.0 / max(1e-6, temperature)) - probs = weights / (weights.sum() if weights.sum() > 0 else 1.0) - idx = rng.choices(range(len(cands)), weights=probs, k=1)[0] - return cands[idx] - -def _uniform_merge_params(a: Dict[ParameterNode, Any], b: Dict[ParameterNode, Any], rng: random.Random) -> Dict[ParameterNode, Any]: - """ - Simple, robust "crossover": per-parameter uniform pick between parents. - (System-aware enough for prompt/code params, cheap, and safe.) - """ - keys = set(a.keys()) | set(b.keys()) - merged: Dict[ParameterNode, Any] = {} - for p in keys: - if p in a and p in b: - merged[p] = copy.deepcopy(a[p] if rng.random() < 0.5 else b[p]) - elif p in a: - merged[p] = copy.deepcopy(a[p]) - else: - merged[p] = copy.deepcopy(b[p]) - return merged - -def _maybe_merge(buffer: List[Candidate], - *, - agent, - guide, - pareto_inputs: List[Any], - pareto_infos: List[Any], - num_threads: Optional[int], - rng: random.Random, - tried_pairs: set, - max_tries: int = 8) -> Optional[Candidate]: - """ - Try merging two non-lineage candidates once; return merged if better than both parents' mean, else None. - """ - if len(buffer) < 2: - return None - # Prefer winners - _compute_pareto_counts(buffer) - pool = sorted(buffer, key=lambda c: (c.wins, c.mean), reverse=True) - - # Try a few distinct pairs - for _ in range(max_tries): - i, j = rng.sample(range(len(pool)), 2) - a, b = pool[i], pool[j] - if a.id == b.id: - continue - if a.id in b.ancestors or b.id in a.ancestors: - continue # avoid direct ancestry - key = tuple(sorted((a.id, b.id))) - if key in tried_pairs: - continue - tried_pairs.add(key) - - merged_params = _uniform_merge_params(a.params, b.params, rng) - # Evaluate merged on Pareto subset - original_params = _snapshot_params_fast(list(agent.parameters())) - try: - # load params to agent - from opto.optimizers.optimizer import Optimizer # type: ignore - # We only need the parameters dict projection; we can set via optimizer.update if available - # But we don't have an optimizer here; use ParameterNode._set - for p, v in merged_params.items(): - p._set(v) - - vec = _eval_on_subset(agent, guide, pareto_inputs, pareto_infos, num_threads=num_threads, - desc="GEPA+Merge: evaluating merged") - mean = float(np.mean(vec)) if all(s is not None for s in vec) else -float("inf") - finally: - # restore original - for p, v in original_params.items(): - p._set(v) - - if mean > max(a.mean, b.mean): - merged = Candidate(params=merged_params, - eval_vector=vec, - mean=mean, - id=-1, # to be set by caller - parent_ids=(a.id, b.id), - ancestors=set(a.ancestors) | set(b.ancestors) | {a.id, b.id}, - created_iter=0) - return merged - return None - - -def _maybe_merge_ancestor_aware( - buffer: List[Candidate], - *, - id2cand: Dict[int, Candidate], - module_groups: List[List[ParameterNode]], - agent, - guide, - optimizer, - train_dataset: Dict[str, List[Any]], - train_batch_size: int, - pareto_inputs: List[Any], - pareto_infos: List[Any], - num_threads: Optional[int], - rng: random.Random, - tried_pairs: set, - budget_tracker: Optional[Dict[str, int]] = None, - budget_B: Optional[int] = None, - max_tries: int = 8 -) -> Optional[Tuple[Candidate, int]]: - """ - Ancestor-aware merge with budget tracking. Returns (merged_candidate, rollouts_used). - """ - if len(buffer) < 2: - return None - - rollouts_used = 0 - - # Sample training minibatch (no replacement → lower variance) - k = min(train_batch_size, len(train_dataset["inputs"])) - idxs = np.random.choice(len(train_dataset["inputs"]), k, replace=False) - tx = [train_dataset["inputs"][i] for i in idxs] - ti = [train_dataset["infos"][i] for i in idxs] - - # Prefer winners for parent selection - _compute_pareto_counts(buffer) - pool = sorted(buffer, key=lambda c: (c.wins, c.mean), reverse=True) - - for _ in range(max_tries): - i, j = rng.sample(range(len(pool)), 2) - ci, cj = pool[i], pool[j] - if ci.id == cj.id: - continue - if ci.id in cj.ancestors or cj.id in ci.ancestors: - continue # avoid direct ancestry - key = tuple(sorted((ci.id, cj.id))) - if key in tried_pairs: - continue - tried_pairs.add(key) - - merged_params = _uniform_merge_params(ci.params, cj.params, rng) - - # Quick minibatch acceptability check - def _batch_mean_for(param_dict): - original = _snapshot_params_fast(list(optimizer.parameters)) - try: - _apply_params(optimizer, param_dict) - vec = evaluate(agent, guide, tx, ti, min_score=None, num_threads=num_threads, - description="MERGE(mini-batch accept)") - finally: - _apply_params(optimizer, original) - return float(np.mean(vec)) if all(s is not None for s in vec) else -float("inf") - - rollouts_used += k - merged_batch_mean = _batch_mean_for(merged_params) - parent_means = [_batch_mean_for(ci.params), _batch_mean_for(cj.params)] - rollouts_used += 2 * k - - # Early budget guard (3*k minibatch evals) before Pareto eval - if budget_B is not None and budget_tracker is not None: - if budget_tracker["used"] + 3 * k + len(pareto_inputs) > budget_B: - return None - budget_tracker["used"] += 3 * k - - if merged_batch_mean <= max(parent_means): - continue # Not promising enough - - # Full Pareto evaluation - original = _snapshot_params_fast(list(optimizer.parameters)) - try: - _apply_params(optimizer, merged_params) - vec = evaluate(agent, guide, pareto_inputs, pareto_infos, min_score=None, - num_threads=num_threads, description="GEPA+Merge: ancestor-aware Pareto eval") - finally: - _apply_params(optimizer, original) - mean = float(np.mean(vec)) if all(s is not None for s in vec) else -float("inf") - # Account Pareto evaluation cost in the global budget and local counter. - if budget_B is not None and budget_tracker is not None: - budget_tracker["used"] += len(pareto_inputs) - rollouts_used += len(pareto_inputs) - - merged = Candidate(params=merged_params, - eval_vector=vec, mean=mean, - id=-1, parent_ids=(ci.id, cj.id), - ancestors=set(ci.ancestors) | set(cj.ancestors) | {ci.id, cj.id}, - created_iter=0) - return merged, rollouts_used - - return None - - -def _ensure_optimizer(agent, optimizer): - if optimizer is not None: - return optimizer - params = [p for p in agent.parameters()] # List[ParameterNode] - return OptoPrimeV2(parameters=params) - - -def _train_step_generate_child(agent, guide, optimizer, train_xs, train_infos, *, verbose=False, num_threads=None): - """ - Single-parent, incremental evolution "mutation": run forward on a minibatch to get batched feedback, - then optimizer.step(bypassing=True) to obtain a new candidate param dict (without applying). - """ - use_parallel = (num_threads is not None and num_threads > 1 and batch_run is not None) - if use_parallel: - # Pre-bind args → pass callables only. Robust to different batch_run signatures. - callables = [ - functools.partial(standard_optimization_step, agent, x, guide, info) - for x, info in zip(train_xs, train_infos) - ] - try: - outputs = batch_run( - callables, - max_workers=num_threads, - description="GEPA forward (mutate parent)", - ) - except TypeError: - # Fallback: older/other signature (e.g., batch_run(callables, max_workers)) - try: - outputs = batch_run(callables, num_threads) - except Exception: - outputs = None - # Normalize outputs to a list of results. batch_run in different versions may: - # - return the list of results, - # - return a callable that returns the results, - # - return a generator/iterator, - # - or return None. - try: - if callable(outputs): - outputs = outputs() - elif isinstance(outputs, types.GeneratorType): - outputs = list(outputs) - elif outputs is None: - # fallback to sequential evaluation - outputs = [fn() for fn in callables] - elif not isinstance(outputs, (list, tuple)): - # Some other iterable (e.g. map object) - try: - outputs = list(outputs) - except Exception: - outputs = [fn() for fn in callables] - except Exception: - # Any error while normalizing → fallback to sequential - outputs = [fn() for fn in callables] - else: - # Safe sequential fallback. - outputs = [standard_optimization_step(agent, x, guide, info) for x, info in zip(train_xs, train_infos)] - - scores, targets, feedbacks = [], [], [] - for target, score, feedback in outputs: - scores.append(score) - targets.append(target) - feedbacks.append(feedback) - - target_batch = batchify(*targets) - feedback_batch = batchify(*feedbacks).data - - optimizer.zero_feedback() - optimizer.backward(target_batch, feedback_batch) - try: - update_dict = optimizer.step(bypassing=True, verbose=("output" if verbose else False)) - if not isinstance(update_dict, dict) or len(update_dict) == 0: - # Fallback: treat current as child (rare) - update_dict = {p: copy.deepcopy(p.data) for p in optimizer.parameters} - except Exception as e: - print_color(f"[GEPA] optimizer.step error: {e}", "red") - update_dict = {} - return update_dict, (None if not scores or any(s is None for s in scores) else float(np.mean(scores))) - - -def _apply_params(optimizer, param_dict: Dict[ParameterNode, Any]): - """Load param dict into the agent via optimizer.update (preserves projections).""" - optimizer.update(param_dict) - - -def _snapshot_params_fast(parameters: List[ParameterNode]) -> Dict[ParameterNode, Any]: - """ - Snapshot ParameterNode->value with minimal copying: - - immutables (str/int/float/bool/tuple/bytes/None): no copy - - numpy arrays: .copy() - - everything else: deepcopy (safe fallback) - """ - snap: Dict[ParameterNode, Any] = {} - immutables = (str, int, float, bool, tuple, frozenset, bytes, type(None)) - for p in parameters: - v = getattr(p, "data", None) - if isinstance(v, immutables): - snap[p] = v - elif isinstance(v, np.ndarray): - snap[p] = v.copy() - else: - snap[p] = copy.deepcopy(v) - return snap - - -def _fingerprint_params(params_dict: Dict[ParameterNode, Any]) -> Tuple: - """ - Hashable fingerprint of a ParameterNode->value dict for optional caching. - Uses (param-id, repr(value)) with special handling for numpy arrays. - """ - items: List[Tuple] = [] - for p, v in params_dict.items(): - pid = getattr(p, "uid", None) or getattr(p, "name", None) or id(p) - try: - if isinstance(v, np.ndarray): - items.append(("arr", pid, v.shape, v.dtype.str, hash(v.tobytes()))) - else: - items.append(("val", pid, repr(v))) - except Exception: - items.append(("val", pid, repr(v))) - return tuple(sorted(items)) - - -# ======================= Variant 1: GEPA + Merge (UCB subclass) ======================= # - -class GEPAUCBSearch(UCBSearchAlgorithm): - """ - GEPA (+Merge) implemented atop UCBSearchAlgorithm. - Differences vs base UCB: - - Fixed Pareto subset (D_pareto) and per-instance vectors kept for each candidate - - Parent selection = Pareto "best-for-at-least-one" sampling (wins-weighted); UCB used only for eviction fallback - - Single-parent incremental mutation via a minibatch - - Optional periodic Merge crossover (uniform per-parameter) with desirability checks - """ - - def __init__(self, - agent, - optimizer=None, - *, - max_buffer_size: int = 16, - ucb_exploration_factor: float = 0.8, - rng_seed: int = 7, - logger=None, - num_threads: Optional[int] = None, - module_groups: Optional[Dict[str, List[ParameterNode]] | List[List[ParameterNode]]] = None, - selectmodule_policy: str = "round_robin", - enable_pareto_cache: bool = False): - optimizer = _ensure_optimizer(agent, optimizer) - super().__init__(agent, optimizer, - max_buffer_size=max_buffer_size, - ucb_exploration_factor=ucb_exploration_factor, - logger=logger, - num_threads=num_threads) - self.rng = random.Random(rng_seed) - np.random.seed(rng_seed) # ensure numpy reproducibility for np.random.choice - self._pareto_inputs: List[Any] = [] - self._pareto_infos: List[Any] = [] - self._id_counter = 0 - self.enable_pareto_cache = enable_pareto_cache - self._pareto_cache: Dict[Tuple, Tuple[List[float], float]] = {} - - def _next_id(self) -> int: - self._id_counter += 1 - return self._id_counter - - def _evaluate_on_pareto(self, params_dict: Dict[ParameterNode, Any], guide, *, num_threads) -> Tuple[List[float], float]: - cache_key = _fingerprint_params(params_dict) if self.enable_pareto_cache else None - if cache_key is not None: - cached = self._pareto_cache.get(cache_key) - if cached is not None: - return cached - original_params = _snapshot_params_fast(list(self.optimizer.parameters)) - try: - _apply_params(self.optimizer, params_dict) - vec = _eval_on_subset(self.agent, guide, self._pareto_inputs, self._pareto_infos, - num_threads=num_threads, desc="GEPA: evaluate on Pareto subset") - mean = float(np.mean(vec)) if all(s is not None for s in vec) else -float("inf") - result = (vec, mean) - if cache_key is not None: - self._pareto_cache[cache_key] = result - return result - finally: - _apply_params(self.optimizer, original_params) - - def _select_pareto_parent(self, cand_buffer: List[Candidate]) -> Candidate: - return _pareto_sample(cand_buffer, temperature=1.0, rng=self.rng) - - def train(self, - guide, - train_dataset: Dict[str, List[Any]], - *, - validate_dataset: Optional[Dict[str, List[Any]]] = None, - pareto_subset_size: int = 24, - num_search_iterations: int = 120, - train_batch_size: int = 2, - merge_every: int = 6, - log_frequency: Optional[int] = None, - save_frequency: Optional[int] = None, - save_path: str = "checkpoints/gepa_ucb_agent.pkl", - verbose: bool = False, - num_threads: Optional[int] = None) -> Tuple[Dict[str, Any], float]: - """ - GEPA search loop with Pareto sampling + (optional) Merge. - """ - num_threads = num_threads or self.num_threads - log_frequency = log_frequency or 5 - validate_ds = validate_dataset or train_dataset - - # Fix a Pareto subset (small, stable) to compute per-instance vectors - assert len(validate_ds["inputs"]) > 0, "Empty dataset." - idxs = np.random.choice(len(validate_ds["inputs"]), - min(pareto_subset_size, len(validate_ds["inputs"])), - replace=False) - self._pareto_inputs = [validate_ds["inputs"][i] for i in idxs] - self._pareto_infos = [validate_ds["infos"][i] for i in idxs] - - buffer: List[Candidate] = [] - tried_merges: set = set() - id2cand: Dict[int, Candidate] = {} - - # Seed with current params - base_params = _snapshot_params_fast(list(self.optimizer.parameters)) - v0, m0 = self._evaluate_on_pareto(base_params, guide, num_threads=num_threads) - seed = Candidate(params=base_params, eval_vector=v0, mean=m0, id=self._next_id(), ancestors=set(), created_iter=0) - buffer.append(seed) - id2cand[seed.id] = seed - print_color(f"[GEPA] Seed candidate mean={m0:.4f}", "cyan") - - metrics = {"best_means": [], "new_child_means": [], "merge_accepts": 0, "total_merges": 0} - - for it in range(1, num_search_iterations + 1): - # Select parent by Pareto winners - parent = self._select_pareto_parent(buffer) - _apply_params(self.optimizer, parent.params) - - # Sample train minibatch - train_size = min(train_batch_size, len(train_dataset["inputs"])) - tr_idxs = np.random.choice(len(train_dataset["inputs"]), train_size, replace=False) - train_xs = [train_dataset["inputs"][i] for i in tr_idxs] - train_info = [train_dataset["infos"][i] for i in tr_idxs] - - # Generate child via one incremental step - update_dict, train_batch_mean = _train_step_generate_child( - self.agent, guide, self.optimizer, train_xs, train_info, verbose=verbose, num_threads=num_threads - ) - if not update_dict: - print_color("[GEPA] Empty child update; skipping.", "yellow") - continue - - # Evaluate child on Pareto subset - child_vec, child_mean = self._evaluate_on_pareto(update_dict, guide, num_threads=num_threads) - child = Candidate(params=update_dict, - eval_vector=child_vec, - mean=child_mean, - id=self._next_id(), - parent_ids=(parent.id,), - ancestors=set(parent.ancestors) | {parent.id}, - created_iter=it) - buffer.append(child) - metrics["new_child_means"].append(child_mean) - print_color(f"[GEPA] iter {it}: child mean={child_mean:.4f} (train-batch≈{train_batch_mean})", "green") - - # Optional Merge - if merge_every and (it % merge_every == 0): - metrics["total_merges"] += 1 - merged = _maybe_merge(buffer, - agent=self.agent, guide=guide, - pareto_inputs=self._pareto_inputs, - pareto_infos=self._pareto_infos, - num_threads=num_threads, - rng=self.rng, - tried_pairs=tried_merges) - if merged is not None: - merged.id = self._next_id() - merged.created_iter = it - buffer.append(merged) - metrics["merge_accepts"] += 1 - print_color(f"[GEPA] Merge accepted: mean={merged.mean:.4f}", "magenta") - - # Keep buffer bounded: remove the candidate with lowest (wins, mean) - if len(buffer) > self.max_buffer_size: - _compute_pareto_counts(buffer) - buffer.sort(key=lambda c: (c.wins, c.mean)) - evicted = buffer.pop(0) - print_color(f"[GEPA] Evicted cand#{evicted.id} (wins={evicted.wins}, mean={evicted.mean:.4f})", "yellow") - - # Track & log - best = max(buffer, key=lambda c: c.mean) - metrics["best_means"].append(best.mean) - if it % log_frequency == 0: - self.logger.log("GEPA best mean", best.mean, it, color="green") - - # Save best candidate snapshot (optional) - if save_frequency and it % save_frequency == 0: - _apply_params(self.optimizer, best.params) - self.save_agent(save_path, it) - - # Load best into the agent and return - best = max(buffer, key=lambda c: c.mean) if buffer else buffer[0] - _apply_params(self.optimizer, best.params) - return metrics, float(best.mean) - - -# ================= Variant 2: Beamsearch subclass with Pareto select ================= # - -class GEPABeamPareto(BeamsearchAlgorithm): - """ - BeamsearchAlgorithm retrofit: - - override select() to a Pareto "best-for-at-least-one" selector - - replace deep beam expansion with GEPA’s single-parent incremental evolution - """ - - def __init__(self, agent, optimizer=None, *, rng_seed: int = 11, logger=None, - num_threads: Optional[int] = None, - module_groups: Optional[Dict[str, List[ParameterNode]] | List[List[ParameterNode]]] = None, - selectmodule_policy: str = "round_robin"): - optimizer = _ensure_optimizer(agent, optimizer) - super().__init__(agent, optimizer, num_threads=num_threads, logger=logger) - self.rng = random.Random(rng_seed) - np.random.seed(rng_seed) - - # We keep a Pareto select helper that returns (selected_params, wins, scores) - def select(self, - candidates: List[Dict[ParameterNode, Any]], - validate_guide, - validation_mini_dataset, - beam_width: int, - num_threads: int = None, - min_score: float = None, - return_scores: bool = False): - """ - Override to Pareto union-of-winners on the mini validation batch. - """ - # Evaluate each candidate to a vector on the mini validation - cand_objs: List[Candidate] = [] - current_params = _snapshot_params_fast(list(self.optimizer.parameters)) - try: - for idx, params in enumerate(candidates): - _apply_params(self.optimizer, params) - vec = evaluate(self.agent, - validate_guide, - validation_mini_dataset['inputs'], - validation_mini_dataset['infos'], - min_score=min_score, - num_threads=num_threads, - description=f"Validating candidate {idx+1}/{len(candidates)} (Pareto)") - mean = float(np.mean(vec)) if all(s is not None for s in vec) else -float("inf") - cand_objs.append(Candidate(params=params, eval_vector=vec, mean=mean, id=idx)) - finally: - _apply_params(self.optimizer, current_params) - - # Compute wins and select top "beam_width" by (wins, mean) - _compute_pareto_counts(cand_objs) - cand_objs.sort(key=lambda c: (c.wins, c.mean), reverse=True) - selected = cand_objs[: min(beam_width, len(cand_objs))] - sel_params = [c.params for c in selected] - sel_scores = [c.mean for c in selected] - if return_scores: - return sel_params, sel_scores - return sel_params - - # Replace beam "train" with GEPA-style incremental loop (keeps BeamsearchAlgorithm API) - def train(self, guide, train_dataset, *, - validate_dataset=None, pareto_subset_size: int = 24, - num_search_iterations: int = 120, train_batch_size: int = 2, - merge_every: int = 6, log_frequency: Optional[int] = None, - save_frequency: Optional[int] = None, - save_path: str = "checkpoints/gepa_beam_agent.pkl", - verbose: bool = False, num_threads: Optional[int] = None, - module_groups: Optional[Dict[str, List[ParameterNode]] | List[List[ParameterNode]]] = None, - selectmodule_policy: str = "round_robin", - budget_B: Optional[int] = None, - accept_epsilon: float = 0.0): - num_threads = num_threads or self.num_threads - log_frequency = log_frequency or 5 - validate_ds = validate_dataset or train_dataset - - # Fix Pareto subset for this run - idxs = np.random.choice(len(validate_ds["inputs"]), - min(pareto_subset_size, len(validate_ds["inputs"])), - replace=False) - pareto_inputs = [validate_ds["inputs"][i] for i in idxs] - pareto_infos = [validate_ds["infos"][i] for i in idxs] - - # Seed buffer - buffer: List[Candidate] = [] - base_params = _snapshot_params_fast(list(self.optimizer.parameters)) - original = _snapshot_params_fast(list(self.optimizer.parameters)) - try: - _apply_params(self.optimizer, base_params) - vec = evaluate(self.agent, guide, pareto_inputs, pareto_infos, - min_score=None, num_threads=num_threads, - description="GEPA(beam): seed evaluation") - finally: - _apply_params(self.optimizer, original) - m0 = float(np.mean(vec)) if all(s is not None for s in vec) else -float("inf") - buffer.append(Candidate(params=base_params, eval_vector=vec, mean=m0, id=0, ancestors=set())) - tried_merges: set = set() - - best_mean = m0 - for it in range(1, num_search_iterations + 1): - # Pareto-select parent and mutate - _compute_pareto_counts(buffer) - parent = _pareto_sample(buffer, temperature=1.0, rng=self.rng) - _apply_params(self.optimizer, parent.params) - - # Make a child - k = min(train_batch_size, len(train_dataset["inputs"])) - tr = np.random.choice(len(train_dataset["inputs"]), k, replace=False) - train_xs = [train_dataset["inputs"][i] for i in tr] - train_in = [train_dataset["infos"][i] for i in tr] - - update_dict, _ = _train_step_generate_child(self.agent, guide, self.optimizer, train_xs, train_in, - verbose=verbose, num_threads=num_threads) - if not update_dict: - continue - - # Evaluate child on Pareto subset - original = _snapshot_params_fast(list(self.optimizer.parameters)) - try: - _apply_params(self.optimizer, update_dict) - vec = evaluate(self.agent, guide, pareto_inputs, pareto_infos, min_score=None, - num_threads=num_threads, description="GEPA(beam): child eval") - finally: - _apply_params(self.optimizer, original) - mean = float(np.mean(vec)) if all(s is not None for s in vec) else -float("inf") - buffer.append(Candidate(params=update_dict, eval_vector=vec, mean=mean, id=len(buffer), - parent_ids=(parent.id,), ancestors=set(parent.ancestors) | {parent.id})) - best_mean = max(best_mean, mean) - if it % log_frequency == 0: - self.logger.log("GEPA(beam) best mean", best_mean, it, color="green") - - # Periodic merge - if merge_every and it % merge_every == 0: - merged = _maybe_merge(buffer, - agent=self.agent, guide=guide, - pareto_inputs=pareto_inputs, pareto_infos=pareto_infos, - num_threads=num_threads, rng=self.rng, tried_pairs=tried_merges) - if merged is not None: - merged.id = len(buffer) - buffer.append(merged) - - # Trim buffer softly (keep top by (wins, mean)) - if len(buffer) > 16: - _compute_pareto_counts(buffer) - buffer.sort(key=lambda c: (c.wins, c.mean), reverse=True) - buffer[:] = buffer[:16] - - # Optional save - if save_frequency and it % save_frequency == 0: - best = max(buffer, key=lambda c: c.mean) - _apply_params(self.optimizer, best.params) - self.save_agent(save_path, it) - - best = max(buffer, key=lambda c: c.mean) - _apply_params(self.optimizer, best.params) - return {"best_mean": best.mean}, float(best.mean) - - -# =================== Variant 3: Minimal GEPA on AlgorithmBase =================== # - -class GEPAAlgorithmBase(Trainer): - """ - Lightweight GEPA (+Merge) with only Trainer dependency. - Useful when you want the simplest control loop with your own logging/saving. - """ - - def __init__(self, agent, optimizer=None, *, rng_seed: int = 13, logger=None, - num_threads: Optional[int] = None, - module_groups: Optional[Dict[str, List[ParameterNode]] | List[List[ParameterNode]]] = None, - selectmodule_policy: str = "round_robin"): - super().__init__(agent, num_threads=num_threads, logger=logger) - self.optimizer = _ensure_optimizer(agent, optimizer) - self.rng = random.Random(rng_seed) - np.random.seed(rng_seed) - - def train(self, - guide, - train_dataset, - *, - validate_dataset=None, - pareto_subset_size: int = 24, - num_iters: int = 100, - train_batch_size: int = 2, - merge_every: int = 5, - num_threads: Optional[int] = None, - save_path: Optional[str] = None): - num_threads = num_threads or self.num_threads - validate_ds = validate_dataset or train_dataset - - # Pareto subset - idxs = np.random.choice(len(validate_ds["inputs"]), - min(pareto_subset_size, len(validate_ds["inputs"])), - replace=False) - xsP = [validate_ds["inputs"][i] for i in idxs] - isP = [validate_ds["infos"][i] for i in idxs] - - # Seed - buffer: List[Candidate] = [] - base_params = _snapshot_params_fast(list(self.optimizer.parameters)) - original = _snapshot_params_fast(list(self.optimizer.parameters)) - try: - _apply_params(self.optimizer, base_params) - vec = evaluate(self.agent, guide, xsP, isP, min_score=None, num_threads=num_threads, - description="GEPA(base): seed eval") - finally: - _apply_params(self.optimizer, original) - m0 = float(np.mean(vec)) if all(s is not None for s in vec) else -float("inf") - buffer.append(Candidate(params=base_params, eval_vector=vec, mean=m0, id=0, ancestors=set())) - tried_merges: set = set() - - for it in range(1, num_iters + 1): - # Parent select - _compute_pareto_counts(buffer) - parent = _pareto_sample(buffer, temperature=1.0, rng=self.rng) - _apply_params(self.optimizer, parent.params) - - # Child - k = min(train_batch_size, len(train_dataset["inputs"])) - tr = np.random.choice(len(train_dataset["inputs"]), k, replace=False) - tx = [train_dataset["inputs"][i] for i in tr] - ti = [train_dataset["infos"][i] for i in tr] - update_dict, _ = _train_step_generate_child(self.agent, guide, self.optimizer, tx, ti, - verbose=False, num_threads=num_threads) - if not update_dict: - continue - - # Eval child - original = _snapshot_params_fast(list(self.optimizer.parameters)) - try: - _apply_params(self.optimizer, update_dict) - vec = evaluate(self.agent, guide, xsP, isP, min_score=None, num_threads=num_threads, - description="GEPA(base): child eval") - finally: - _apply_params(self.optimizer, original) - mean = float(np.mean(vec)) if all(s is not None for s in vec) else -float("inf") - buffer.append(Candidate(params=update_dict, eval_vector=vec, mean=mean, id=len(buffer), - parent_ids=(parent.id,), ancestors=set(parent.ancestors) | {parent.id})) - - # Merge - if merge_every and it % merge_every == 0: - merged = _maybe_merge(buffer, - agent=self.agent, guide=guide, - pareto_inputs=xsP, pareto_infos=isP, - num_threads=num_threads, rng=self.rng, tried_pairs=tried_merges) - if merged is not None: - merged.id = len(buffer) - buffer.append(merged) - - # Keep compact buffer - if len(buffer) > 16: - _compute_pareto_counts(buffer) - buffer.sort(key=lambda c: (c.wins, c.mean), reverse=True) - buffer[:] = buffer[:16] - - # Log - best = max(buffer, key=lambda c: c.mean) - if self.logger: - self.logger.log("GEPA(base) best mean", best.mean, it, color="green") - - # Optional save - if save_path and it % 10 == 0: - _apply_params(self.optimizer, best.params) - self.save_agent(save_path, it) - - # Load best into agent - best = max(buffer, key=lambda c: c.mean) - _apply_params(self.optimizer, best.params) - return {"best_mean": best.mean}, float(best.mean) - From cc30a3d4f7ffc1c9dc8672bf22320b4b53060216 Mon Sep 17 00:00:00 2001 From: doxav Date: Thu, 6 Nov 2025 19:54:14 +0100 Subject: [PATCH 306/314] removed from test GEPA unavailable feature > GEPA-UCB might be the one to keep --- tests/unit_tests/test_gepa_algorithms.py | 217 +---------------------- 1 file changed, 3 insertions(+), 214 deletions(-) diff --git a/tests/unit_tests/test_gepa_algorithms.py b/tests/unit_tests/test_gepa_algorithms.py index c435628e..fdaa6c0b 100644 --- a/tests/unit_tests/test_gepa_algorithms.py +++ b/tests/unit_tests/test_gepa_algorithms.py @@ -16,14 +16,15 @@ from opto.trace.nodes import node as trace_node from opto.optimizers.optoprime_v2 import OptoPrimeV2 import pytest -from opto.trainer.algorithms.gepa_algorithms import ( +from opto.features.gepa.gepa_algorithms import ( GEPAAlgorithmBase, GEPAUCBSearch, GEPABeamPareto, _compute_pareto_counts, _pareto_sample, - _uniform_merge_params, + _uniform_merge_params ) +from opto.trainer.algorithms.beamsearch_algorithm import BeamsearchAlgorithm from opto.trainer.evaluators import evaluate from opto.trainer.guide import Guide from opto.utils.llm import DummyLLM @@ -166,126 +167,6 @@ def test_gepa_variants_converge_on_dummyllm(algo_cls, train_kwargs): assert agent.param.data == target_add -def test_compare_gepa_vs_basicsearch_on_dummyllm(): - from opto.trainer.algorithms.basic_algorithms import BasicSearchAlgorithm - - target_add = 7 - ds = make_dataset(target_add, n=6) - agent_gepa = AddAgent(param=0) - agent_basic = AddAgent(param=0) - - opt_gepa = build_optimizer(agent_gepa, suggest_value=target_add) - opt_basic = build_optimizer(agent_basic, suggest_value=target_add) - - # GEPA - gepa = GEPAAlgorithmBase(agent_gepa, optimizer=opt_gepa, logger=None, num_threads=1) - _, best_gepa = gepa.train( - guide=ExactMatchGuide(), - train_dataset=ds, - validate_dataset=ds, - pareto_subset_size=4, - num_iters=8, - train_batch_size=2, - merge_every=2, - num_threads=1, - ) - - # BasicSearch baseline - basic = BasicSearchAlgorithm(agent_basic, optimizer=opt_basic, logger=None, num_threads=1) - basic.train( - guide=ExactMatchGuide(), - train_dataset=ds, - validate_dataset=ds, - num_proposals=1, - num_epochs=1, - batch_size=1, - test_dataset=ds, - eval_frequency=1, - num_threads=1, - verbose=False, - ) - - # Evaluate both on full dataset - score_gepa = np.mean(evaluate(agent_gepa, ExactMatchGuide(), ds["inputs"], ds["infos"], num_threads=2)) - score_basic = np.mean(evaluate(agent_basic, ExactMatchGuide(), ds["inputs"], ds["infos"], num_threads=2)) - - assert best_gepa == pytest.approx(1.0, rel=0, abs=1e-6) - assert score_gepa == pytest.approx(1.0, rel=0, abs=1e-6) - assert score_basic == pytest.approx(1.0, rel=0, abs=1e-6) - - -def test_snapshot_params_fast(): - """Test the fast parameter snapshot utility function.""" - from opto.trainer.algorithms.gepa_algorithms import _snapshot_params_fast - - @trace_model - class MultiTypeAgent: - def __init__(self): - self.int_param = trace_node(42, trainable=True) - self.str_param = trace_node("hello", trainable=True) - self.float_param = trace_node(3.14, trainable=True) - self.list_param = trace_node([1, 2, 3], trainable=True) - self.dict_param = trace_node({"key": "value"}, trainable=True) - # Test numpy array - self.np_param = trace_node(np.array([1, 2, 3]), trainable=True) - - def forward(self, x): - return x + self.int_param - - agent = MultiTypeAgent() - params = list(agent.parameters()) - - # Test snapshot - snapshot = _snapshot_params_fast(params) - - # Check that all parameters are included - assert len(snapshot) == len(params) - - # Modify original values - agent.int_param._set(100) - agent.str_param._set("modified") - agent.np_param._set(np.array([4, 5, 6])) - - # Verify snapshot preserved original values - for p in params: - if p.py_name == "int_param": - assert snapshot[p] == 42 - elif p.py_name == "str_param": - assert snapshot[p] == "hello" - elif p.py_name == "np_param": - assert np.array_equal(snapshot[p], np.array([1, 2, 3])) - - -def test_fingerprint_params(): - """Test the parameter fingerprinting utility function.""" - from opto.trainer.algorithms.gepa_algorithms import _fingerprint_params - - @trace_model - class SimpleAgent: - def __init__(self): - self.a = trace_node(1, trainable=True) - self.b = trace_node("test", trainable=True) - - def forward(self, x): - return x + self.a - - agent = SimpleAgent() - params_dict = {p: p.data for p in agent.parameters()} - - # Test fingerprinting - fp1 = _fingerprint_params(params_dict) - fp2 = _fingerprint_params(params_dict) - - # Same parameters should produce same fingerprint - assert fp1 == fp2 - - # Different parameters should produce different fingerprint - agent.a._set(2) - params_dict2 = {p: p.data for p in agent.parameters()} - fp3 = _fingerprint_params(params_dict2) - assert fp1 != fp3 - - def test_numpy_seeding_reproducibility(): """Test that numpy seeding ensures reproducible behavior.""" target_add = 3 @@ -335,64 +216,6 @@ def test_numpy_seeding_reproducibility(): assert best_diff == pytest.approx(1.0, rel=0, abs=1e-6) -def test_gepa_ucb_pareto_cache(): - """Test Pareto cache functionality in GEPAUCBSearch.""" - target_add = 4 - ds = make_dataset(target_add, n=3) - agent = AddAgent(param=0) - optimizer = build_optimizer(agent, suggest_value=target_add) - - # Test with cache enabled - algo = GEPAUCBSearch(agent=agent, optimizer=optimizer, logger=None, num_threads=1, enable_pareto_cache=True) - - metrics, best = algo.train( - guide=ExactMatchGuide(), - train_dataset=ds, - validate_dataset=ds, - pareto_subset_size=2, - num_search_iterations=2, - train_batch_size=1, - merge_every=2, - num_threads=1, - ) - - # Should converge to perfect solution - assert best == pytest.approx(1.0, rel=0, abs=1e-6) - assert agent.param.data == target_add - - # Test that cache was used (should have some entries) - # Note: exact cache size depends on algorithm behavior, but should be non-empty if enabled - if hasattr(algo, '_pareto_cache'): - assert isinstance(algo._pareto_cache, dict) - - -def test_budget_tracking_functionality(): - """Test budget tracking in GEPA algorithms.""" - target_add = 2 - ds = make_dataset(target_add, n=4) - agent = AddAgent(param=0) - optimizer = build_optimizer(agent, suggest_value=target_add) - - # Test GEPABeamPareto with budget - algo = GEPABeamPareto(agent=agent, optimizer=optimizer, logger=None, num_threads=1) - - metrics, best = algo.train( - guide=ExactMatchGuide(), - train_dataset=ds, - validate_dataset=ds, - pareto_subset_size=3, - num_search_iterations=2, - train_batch_size=1, - merge_every=2, - budget_B=10, # Low budget to test tracking - num_threads=1, - ) - - # Should still achieve good results even with budget constraint - assert isinstance(best, float) - assert best >= 0.0 # Should be non-negative score - - def test_thread_safety_with_sequential_fallback(): """Test that algorithms work correctly with sequential fallback when batch_run unavailable.""" target_add = 1 @@ -435,37 +258,3 @@ def test_thread_safety_with_sequential_fallback(): assert best2 == pytest.approx(1.0, rel=0, abs=1e-6) assert agent2.param.data == target_add - -def test_gepa_ucb_selectmodule_policy(): - """Test different module selection policies in GEPAUCBSearch.""" - target_add = 6 - ds = make_dataset(target_add, n=3) - - # Test different selection policies - policies = ["round_robin"] # Could test more if other policies are available - - for policy in policies: - agent = AddAgent(param=0) - optimizer = build_optimizer(agent, suggest_value=target_add) - - algo = GEPAUCBSearch( - agent=agent, - optimizer=optimizer, - logger=None, - num_threads=1, - selectmodule_policy=policy - ) - - metrics, best = algo.train( - guide=ExactMatchGuide(), - train_dataset=ds, - validate_dataset=ds, - pareto_subset_size=2, - num_search_iterations=2, - train_batch_size=1, - merge_every=2, - num_threads=1, - ) - - assert best == pytest.approx(1.0, rel=0, abs=1e-6) - assert agent.param.data == target_add From 0126bfbbf8342e84af743e417fd98fbce64d18de Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 19 Nov 2025 17:54:43 -0500 Subject: [PATCH 307/314] adding a modal environment for dev (with support on filesync/vscode/TCP port and jupyter notebook). --- dev_deployment/README.md | 5 ++ dev_deployment/trace_dev_modal_image.py | 65 +++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 dev_deployment/README.md create mode 100644 dev_deployment/trace_dev_modal_image.py diff --git a/dev_deployment/README.md b/dev_deployment/README.md new file mode 100644 index 00000000..2255c0df --- /dev/null +++ b/dev_deployment/README.md @@ -0,0 +1,5 @@ +# Starting a Dev Environment on Modal + +``` +modal run trace_dev_modal_image.py +``` \ No newline at end of file diff --git a/dev_deployment/trace_dev_modal_image.py b/dev_deployment/trace_dev_modal_image.py new file mode 100644 index 00000000..868d7602 --- /dev/null +++ b/dev_deployment/trace_dev_modal_image.py @@ -0,0 +1,65 @@ +""" +This sets up a Modal container that runs a Jupyter Lab. +It will: +1. Expose a tunnel for ssh and for jupyter (with public URL link) +2. Attach a persistent volume (for data storage). +""" + +import os +import secrets +import subprocess +from pathlib import Path + +import modal + +# Run this command: `modal volume create trace-dev` +volume = modal.Volume.from_name("trace-dev") + +ssh_key_path = Path.home() / ".ssh" / "id_rsa_modal.pub" + +image = ( + modal.Image.debian_slim() + .pip_install("jupyter") + .apt_install("openssh-server") + .run_commands("mkdir /run/sshd") + .add_local_file(str(ssh_key_path), "/root/.ssh/authorized_keys", copy=True) +) +app = modal.App("Trace-dev-jupyter", image=image) + +@app.function(volumes={"/vol/trace-dev-home/": volume}, timeout=3600) +def run_jupyter(): + + # a nested setup to start SSH connection and Jupyter + + subprocess.Popen(["/usr/sbin/sshd", "-D", "-e"]) + + with modal.forward(port=22, unencrypted=True) as ssh_tunnel: + hostname, port = ssh_tunnel.tcp_socket + connection_cmd = f'ssh -p {port} root@{hostname}' + print("Run `ssh-add ~/.ssh/id_rsa_modal` to add SSH key credential") + print(f"ssh into container using: {connection_cmd}") + + token = secrets.token_urlsafe(13) + + with modal.forward(8888) as tunnel: + url = tunnel.url + "/?token=" + token + print(f"Starting Jupyter at {url}") + subprocess.run( + [ + "jupyter", + "notebook", + "--no-browser", + "--allow-root", + "--ip=0.0.0.0", + "--port=8888", + "--LabApp.allow_origin='*'", + "--LabApp.allow_remote_access=1", + ], + env={**os.environ, "JUPYTER_TOKEN": token, "SHELL": "/bin/bash"}, + stderr=subprocess.DEVNULL, + ) + +@app.local_entrypoint() +def main(): + # Server() + run_jupyter.remote() From 43b150a8703bd4dedaf94b05fe84425eb88a1fbe Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 19 Nov 2025 18:22:01 -0500 Subject: [PATCH 308/314] improve some usability --- dev_deployment/README.md | 12 +++++++++++- dev_deployment/trace_dev_modal_image.py | 18 +++++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/dev_deployment/README.md b/dev_deployment/README.md index 2255c0df..14cedc9e 100644 --- a/dev_deployment/README.md +++ b/dev_deployment/README.md @@ -1,5 +1,15 @@ # Starting a Dev Environment on Modal +Run it as a blocking command: + ``` modal run trace_dev_modal_image.py -``` \ No newline at end of file +``` + +Run it detached (might need to shut down app through the Modal interface): + +``` +modal run --detach trace_dev_modal_image.py +``` + +Or simply run it in a screen or tmux session. \ No newline at end of file diff --git a/dev_deployment/trace_dev_modal_image.py b/dev_deployment/trace_dev_modal_image.py index 868d7602..f1efbcb5 100644 --- a/dev_deployment/trace_dev_modal_image.py +++ b/dev_deployment/trace_dev_modal_image.py @@ -2,7 +2,11 @@ This sets up a Modal container that runs a Jupyter Lab. It will: 1. Expose a tunnel for ssh and for jupyter (with public URL link) -2. Attach a persistent volume (for data storage). +2. Add Trace repo code into the container and include it in the PYTHONPATH +3. Attach a persistent volume (for data storage). + +The code lives in the ephemeral space, but any results should be saved in the persistent volume, +mounted under `/vol/trace-dev-home/`. """ import os @@ -12,21 +16,29 @@ import modal +MINUTES = 60 # seconds +HOURS = 60 * MINUTES + # Run this command: `modal volume create trace-dev` volume = modal.Volume.from_name("trace-dev") ssh_key_path = Path.home() / ".ssh" / "id_rsa_modal.pub" +project_root = Path(__file__).parent.parent image = ( modal.Image.debian_slim() .pip_install("jupyter") .apt_install("openssh-server") + .env({ + "PYTHONPATH": "/root/Trace" # add KernelBench to python path + }) .run_commands("mkdir /run/sshd") .add_local_file(str(ssh_key_path), "/root/.ssh/authorized_keys", copy=True) + .add_local_dir(str(project_root), "/root/Trace", copy=True) ) app = modal.App("Trace-dev-jupyter", image=image) -@app.function(volumes={"/vol/trace-dev-home/": volume}, timeout=3600) +@app.function(volumes={"/vol/trace-dev-home/": volume}, timeout=24 * HOURS) def run_jupyter(): # a nested setup to start SSH connection and Jupyter @@ -61,5 +73,5 @@ def run_jupyter(): @app.local_entrypoint() def main(): - # Server() + subprocess.run(["ssh-add", "~/.ssh/id_rsa_modal"]) run_jupyter.remote() From 2bc95eb2c2217f3a3ea10d8bcde5d2d10a0460a4 Mon Sep 17 00:00:00 2001 From: windweller Date: Thu, 20 Nov 2025 14:59:21 -0500 Subject: [PATCH 309/314] modified to upload current state of files locally to remote as well --- dev_deployment/trace_dev_modal_image.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dev_deployment/trace_dev_modal_image.py b/dev_deployment/trace_dev_modal_image.py index f1efbcb5..ac5b1d55 100644 --- a/dev_deployment/trace_dev_modal_image.py +++ b/dev_deployment/trace_dev_modal_image.py @@ -34,7 +34,8 @@ }) .run_commands("mkdir /run/sshd") .add_local_file(str(ssh_key_path), "/root/.ssh/authorized_keys", copy=True) - .add_local_dir(str(project_root), "/root/Trace", copy=True) + .add_local_dir(str(project_root / "opto"), "/root/Trace/opto", copy=True) + .add_local_dir(str(project_root / "tests"), "/root/Trace/tests", copy=True) ) app = modal.App("Trace-dev-jupyter", image=image) From ad91a16f57688934f86ae33d272f8902715eede3 Mon Sep 17 00:00:00 2001 From: windweller Date: Thu, 20 Nov 2025 15:53:29 -0500 Subject: [PATCH 310/314] finalized the deployment command --- dev_deployment/trace_dev_modal_image.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/dev_deployment/trace_dev_modal_image.py b/dev_deployment/trace_dev_modal_image.py index ac5b1d55..20d92e3d 100644 --- a/dev_deployment/trace_dev_modal_image.py +++ b/dev_deployment/trace_dev_modal_image.py @@ -27,13 +27,16 @@ image = ( modal.Image.debian_slim() - .pip_install("jupyter") + .pip_install("jupyter", "graphviz") .apt_install("openssh-server") .env({ "PYTHONPATH": "/root/Trace" # add KernelBench to python path }) .run_commands("mkdir /run/sshd") .add_local_file(str(ssh_key_path), "/root/.ssh/authorized_keys", copy=True) + .add_local_file(str(project_root / "setup.py"), "/root/Trace/setup.py", copy=True) + .add_local_file(str(project_root / "pyproject.toml"), "/root/Trace/pyproject.toml", copy=True) + .add_local_file(str(project_root / "README.md"), "/root/Trace/README.md", copy=True) .add_local_dir(str(project_root / "opto"), "/root/Trace/opto", copy=True) .add_local_dir(str(project_root / "tests"), "/root/Trace/tests", copy=True) ) @@ -42,6 +45,9 @@ @app.function(volumes={"/vol/trace-dev-home/": volume}, timeout=24 * HOURS) def run_jupyter(): + # set up Trace env + subprocess.run(["pip", "install", "-e", "/root/Trace"]) + # a nested setup to start SSH connection and Jupyter subprocess.Popen(["/usr/sbin/sshd", "-D", "-e"]) @@ -56,7 +62,7 @@ def run_jupyter(): with modal.forward(8888) as tunnel: url = tunnel.url + "/?token=" + token - print(f"Starting Jupyter at {url}") + print(f"\033[91mStarting Jupyter at {url}\033[0m") subprocess.run( [ "jupyter", From 894776db783e3c56a1410057b29b65f4bf137e18 Mon Sep 17 00:00:00 2001 From: Allen Nie Date: Sat, 22 Nov 2025 08:50:31 -0800 Subject: [PATCH 311/314] Revert "Improved Gepa UCB with range clamping / target" --- opto/features/gepa/gepa_algorithms.py | 27 +-- tests/unit_tests/test_gepa_algorithms.py | 260 ----------------------- 2 files changed, 4 insertions(+), 283 deletions(-) delete mode 100644 tests/unit_tests/test_gepa_algorithms.py diff --git a/opto/features/gepa/gepa_algorithms.py b/opto/features/gepa/gepa_algorithms.py index 5684aba2..7494c0ca 100644 --- a/opto/features/gepa/gepa_algorithms.py +++ b/opto/features/gepa/gepa_algorithms.py @@ -224,14 +224,6 @@ class GEPAUCBSearch(UCBSearchAlgorithm): - Optional periodic Merge crossover (uniform per-parameter) with desirability checks """ - def _rank(self, raw: float) -> float: - """ - If a score_range is provided (lo, hi), clamp the scalar score into that band. - This keeps UCB-like behavior numerically stable without changing external APIs. - """ - if getattr(self, "score_range", None) is None or raw is None: return raw - lo, hi = self.score_range; return float(min(hi, max(lo, raw))) - def __init__(self, agent, optimizer=None, @@ -278,7 +270,6 @@ def train(self, pareto_subset_size: int = 24, num_search_iterations: int = 120, train_batch_size: int = 2, - score_range: Optional[Tuple[float, float]] = None, merge_every: int = 6, log_frequency: Optional[int] = None, save_frequency: Optional[int] = None, @@ -291,7 +282,6 @@ def train(self, num_threads = num_threads or self.num_threads log_frequency = log_frequency or 5 validate_ds = validation_dataset or train_dataset - self.score_range = score_range # Optional score clamping band for mean-based selections # Fix a Pareto subset (small, stable) to compute per-instance vectors assert len(validate_ds["inputs"]) > 0, "Empty dataset." @@ -306,13 +296,10 @@ def train(self, # Seed with current params base_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} - v0, m0_raw = self._evaluate_on_pareto(base_params, guide, num_threads=num_threads) - m0 = self._rank(m0_raw) - buffer.append(Candidate(params=base_params, eval_vector=v0, mean=m0, id=self._next_id(), - ancestors=set(), meta={"raw_mean": m0_raw})) + v0, m0 = self._evaluate_on_pareto(base_params, guide, num_threads=num_threads) + buffer.append(Candidate(params=base_params, eval_vector=v0, mean=m0, id=self._next_id(), ancestors=set())) print_color(f"[GEPA] Seed candidate mean={m0:.4f}", "cyan") - metrics = {"best_means": [], "new_child_means": [], "merge_accepts": 0, "total_merges": 0} for it in range(1, num_search_iterations + 1): @@ -335,16 +322,14 @@ def train(self, continue # Evaluate child on Pareto subset - child_vec, child_mean_raw = self._evaluate_on_pareto(update_dict, guide, num_threads=num_threads) - child_mean = self._rank(child_mean_raw) + child_vec, child_mean = self._evaluate_on_pareto(update_dict, guide, num_threads=num_threads) child = Candidate(params=update_dict, eval_vector=child_vec, mean=child_mean, id=self._next_id(), parent_ids=(parent.id,), ancestors=set(parent.ancestors) | {parent.id}, - created_iter=it, - meta={"raw_mean": child_mean_raw}) + created_iter=it) buffer.append(child) metrics["new_child_means"].append(child_mean) print_color(f"[GEPA] iter {it}: child mean={child_mean:.4f} (train-batch≈{train_batch_mean})", "green") @@ -362,10 +347,6 @@ def train(self, if merged is not None: merged.id = self._next_id() merged.created_iter = it - # preserve raw and clamp to range for ranking/logging - _raw = merged.mean - merged.meta["raw_mean"] = _raw - merged.mean = self._rank(_raw) buffer.append(merged) metrics["merge_accepts"] += 1 print_color(f"[GEPA] Merge accepted: mean={merged.mean:.4f}", "magenta") diff --git a/tests/unit_tests/test_gepa_algorithms.py b/tests/unit_tests/test_gepa_algorithms.py deleted file mode 100644 index fdaa6c0b..00000000 --- a/tests/unit_tests/test_gepa_algorithms.py +++ /dev/null @@ -1,260 +0,0 @@ -import math -import os -import random -import re -from typing import Any, Dict, List, Tuple - -import numpy as np -import pytest - -# Provide a light stub for optional graphviz dependency to allow imports without system graphviz -import sys, types -if "graphviz" not in sys.modules: - sys.modules["graphviz"] = types.SimpleNamespace(Digraph=object) - -from opto.trace.modules import model as trace_model -from opto.trace.nodes import node as trace_node -from opto.optimizers.optoprime_v2 import OptoPrimeV2 -import pytest -from opto.features.gepa.gepa_algorithms import ( - GEPAAlgorithmBase, - GEPAUCBSearch, - GEPABeamPareto, - _compute_pareto_counts, - _pareto_sample, - _uniform_merge_params - ) -from opto.trainer.algorithms.beamsearch_algorithm import BeamsearchAlgorithm -from opto.trainer.evaluators import evaluate -from opto.trainer.guide import Guide -from opto.utils.llm import DummyLLM - - -class ExactMatchGuide(Guide): - """Simple guide: score=1 if response == reference, else 0.""" - - def get_feedback(self, query: Any, response: Any, reference: Any, **kwargs): - score = float(response == reference) - feedback = f"Score: {score}. Response: {response}. Reference: {reference}." - return score, feedback - - -@trace_model -class AddAgent: - """Toy agent: returns x + param.""" - - def __init__(self, param: int = 0): - self.param = trace_node(int(param), trainable=True) - - def forward(self, x: int) -> int: - return x + self.param - - -def make_dummy_llm(suggest_value: int) -> DummyLLM: - """Dummy LLM that parses the variable name from the prompt and suggests a fixed value. - - Matches the default XML-like output format expected by OptoPrimeV2. - """ - - def _llm_callable(messages, **kwargs): - # Extract the variable name from the #Variables section in the prompt - problem = messages[1]["content"] if isinstance(messages, (list, tuple)) and len(messages) > 1 else "" - name_match = re.findall(r"", problem) - var_name = name_match[0] if name_match else "param" - return ( - f""" - Dummy reasoning based on the input messages. - - {var_name} - {suggest_value} - - """ - ) - - return DummyLLM(_llm_callable) - - -def make_dataset(target_add: int, n: int = 8) -> Dict[str, List[int]]: - xs = list(range(n)) - infos = [x + target_add for x in xs] - return {"inputs": xs, "infos": infos} - - -def build_optimizer(agent: AddAgent, suggest_value: int) -> OptoPrimeV2: - return OptoPrimeV2(agent.parameters(), llm=make_dummy_llm(suggest_value)) - - -def test_pareto_counting_and_sampling(): - # Construct mock candidates with per-instance eval vectors where each wins on one dimension - from types import SimpleNamespace - - class Cand(SimpleNamespace): - pass - - A = Cand(eval_vector=[1.0, 0.1], wins=0, mean=0.55) - B = Cand(eval_vector=[0.2, 1.1], wins=0, mean=0.65) - cands = [A, B] - - _compute_pareto_counts(cands) - assert A.wins == 1 and B.wins == 1 - - rng = random.Random(0) - # With equal wins, both should be sampled with similar probability - picks = [ - _pareto_sample([A, B], temperature=1.0, rng=rng) for _ in range(100) - ] - a_count = sum(p is A for p in picks) - b_count = sum(p is B for p in picks) - assert abs(a_count - b_count) < 40 # rough balance - - -def test_uniform_merge_params_uses_both_parents(): - # Use two ParameterNodes to exercise merging across keys - @trace_model - class TwoParam: - def __init__(self): - self.a = trace_node(1, trainable=True) - self.b = trace_node(2, trainable=True) - - def forward(self, x): - return self.a + self.b + x - - m = TwoParam() - a_params = {p: (10 if p.py_name.endswith("a") else 20) for p in m.parameters()} - b_params = {p: (100 if p.py_name.endswith("a") else 200) for p in m.parameters()} - - rng = random.Random(123) - merged = _uniform_merge_params(a_params, b_params, rng) - # For each key, merged value should be chosen from either a_params or b_params - for k, v in merged.items(): - assert v in (a_params[k], b_params[k]) - - -@pytest.mark.parametrize( - "algo_cls,train_kwargs", - [ - (GEPAAlgorithmBase, {"num_iters": 8, "train_batch_size": 2, "merge_every": 2}), - (GEPAUCBSearch, {"num_search_iterations": 8, "train_batch_size": 2, "merge_every": 2}), - (GEPABeamPareto, {"num_search_iterations": 8, "train_batch_size": 2, "merge_every": 2}), - ], -) -def test_gepa_variants_converge_on_dummyllm(algo_cls, train_kwargs): - target_add = 5 - ds = make_dataset(target_add, n=6) - agent = AddAgent(param=0) - optimizer = build_optimizer(agent, suggest_value=target_add) - - algo = algo_cls(agent=agent, optimizer=optimizer, logger=None, num_threads=1) - - # Prepare kwargs and include 'verbose' only if supported - import inspect - call_kwargs = dict(guide=ExactMatchGuide(), train_dataset=ds, pareto_subset_size=4, num_threads=1) - sig = inspect.signature(algo.train) - if 'validation_dataset' in sig.parameters: - call_kwargs['validation_dataset'] = ds - else: - call_kwargs['validate_dataset'] = ds - call_kwargs.update(train_kwargs) - if 'verbose' in sig.parameters: - call_kwargs['verbose'] = False - - metrics, best = algo.train(**call_kwargs) - - # Best mean on pareto subset should be perfect - assert isinstance(best, float) - assert best == pytest.approx(1.0, rel=0, abs=1e-6) - # Agent parameter should be updated to target_add - assert agent.param.data == target_add - - -def test_numpy_seeding_reproducibility(): - """Test that numpy seeding ensures reproducible behavior.""" - target_add = 3 - ds = make_dataset(target_add, n=4) - - # Test with same seed - results = [] - for seed in [123, 123]: # Same seed twice - agent = AddAgent(param=0) - optimizer = build_optimizer(agent, suggest_value=target_add) - algo = GEPAAlgorithmBase(agent=agent, optimizer=optimizer, logger=None, num_threads=1, rng_seed=seed) - - metrics, best = algo.train( - guide=ExactMatchGuide(), - train_dataset=ds, - validate_dataset=ds, - pareto_subset_size=3, - num_iters=2, - train_batch_size=1, - merge_every=2, - num_threads=1, - ) - results.append((metrics, best, agent.param.data)) - - # Results should be identical with same seed - assert results[0][1] == results[1][1] # Same best score - assert results[0][2] == results[1][2] # Same final parameter - - # Test with different seed - agent_diff = AddAgent(param=0) - optimizer_diff = build_optimizer(agent_diff, suggest_value=target_add) - algo_diff = GEPAAlgorithmBase(agent=agent_diff, optimizer=optimizer_diff, logger=None, num_threads=1, rng_seed=456) - - metrics_diff, best_diff = algo_diff.train( - guide=ExactMatchGuide(), - train_dataset=ds, - validate_dataset=ds, - pareto_subset_size=3, - num_iters=2, - train_batch_size=1, - merge_every=2, - num_threads=1, - ) - - # Both should converge but the process might differ - # (though with DummyLLM behavior is very predictable) - assert best_diff == pytest.approx(1.0, rel=0, abs=1e-6) - - -def test_thread_safety_with_sequential_fallback(): - """Test that algorithms work correctly with sequential fallback when batch_run unavailable.""" - target_add = 1 - ds = make_dataset(target_add, n=2) - agent = AddAgent(param=0) - optimizer = build_optimizer(agent, suggest_value=target_add) - - # Test with num_threads=1 (should use sequential) - algo = GEPAAlgorithmBase(agent=agent, optimizer=optimizer, logger=None, num_threads=1) - metrics, best = algo.train( - guide=ExactMatchGuide(), - train_dataset=ds, - validate_dataset=ds, - pareto_subset_size=2, - num_iters=2, - train_batch_size=1, - merge_every=2, - num_threads=1, - ) - - assert best == pytest.approx(1.0, rel=0, abs=1e-6) - assert agent.param.data == target_add - - # Test with num_threads=2 (may use parallel or fallback to sequential) - agent2 = AddAgent(param=0) - optimizer2 = build_optimizer(agent2, suggest_value=target_add) - algo2 = GEPAAlgorithmBase(agent=agent2, optimizer=optimizer2, logger=None, num_threads=2) - - metrics2, best2 = algo2.train( - guide=ExactMatchGuide(), - train_dataset=ds, - validate_dataset=ds, - pareto_subset_size=2, - num_iters=2, - train_batch_size=1, - merge_every=2, - num_threads=2, - ) - - assert best2 == pytest.approx(1.0, rel=0, abs=1e-6) - assert agent2.param.data == target_add - From 93cc982254e869690b0a590be1e0e8b65148c2bc Mon Sep 17 00:00:00 2001 From: windweller Date: Tue, 2 Dec 2025 13:03:26 -0800 Subject: [PATCH 312/314] upgrade python to 3.13 and temporarily changed test for direct bundle instantiation of methods --- setup.py | 2 +- tests/unit_tests/test_modules.py | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/setup.py b/setup.py index dbd60be5..8fdfd139 100644 --- a/setup.py +++ b/setup.py @@ -29,5 +29,5 @@ long_description=open('README.md', encoding="utf8").read(), packages=setuptools.find_packages(include=["opto*"]), install_requires=install_requires, - python_requires=">=3.10", + python_requires=">=3.13", ) diff --git a/tests/unit_tests/test_modules.py b/tests/unit_tests/test_modules.py index f5a5d6cc..a5ed29e7 100644 --- a/tests/unit_tests/test_modules.py +++ b/tests/unit_tests/test_modules.py @@ -35,7 +35,7 @@ class ChildModule(BaseModule): def __init__(self): super().__init__() self._extra_param = node(1, trainable=True) - self._extra_method = bundle(trainable=True)(dummy_method) + # self._extra_method = bundle(trainable=True)(dummy_method) self._base = BaseModule() # ParameterContainer @bundle(trainable=True) @@ -47,8 +47,8 @@ def method2(self, y): child = ChildModule() print(child.parameters_dict().keys()) -assert len(child.parameters()) == 6 -assert len(child.parameters_dict()) == 5 +assert len(child.parameters()) == 5 +assert len(child.parameters_dict()) == 4 # Test using model decorator @@ -83,7 +83,7 @@ class ChildClass(BaseClass): def __init__(self): super().__init__() self._extra_param = node(1, trainable=True) - self._extra_method = bundle(trainable=True)(dummy_method) + # self._extra_method = bundle(trainable=True)(dummy_method) self._base = BaseClass() # ParameterContainer @bundle(trainable=True) @@ -95,18 +95,18 @@ def method2(self, y): def test_inheritance(): child = ChildClass() - assert len(child.parameters()) == 6, f"Expected 6 parameters, got {child.parameters_dict()}" - assert len(child.parameters_dict()) == 5 + assert len(child.parameters()) == 5, f"Expected 6 parameters, got {child.parameters_dict()}" + assert len(child.parameters_dict()) == 4 # test save and load def test_save_load_pickle(): child = ChildClass() child._extra_param._data = 2 # simulate data changes - child._extra_method.parameter._data = "fake method" # simulate data changes - child._base._param._data = 3 # simulate data changes + # child._extra_method.parameter._data = "fake method" # simulate data changes + # child._base._param._data = 3 # simulate data changes child._new_param = node(1, trainable=True) # simulate adding new parameter - assert len(child.parameters()) == 7 + assert len(child.parameters()) == 6 try: child.save("test.pkl") From 172b7c0c1cd9dfd48c7dd8bdaa41eafce3f2f66f Mon Sep 17 00:00:00 2001 From: windweller Date: Sat, 27 Dec 2025 10:41:05 -0500 Subject: [PATCH 313/314] change python dep to 3.13 --- .github/workflows/ci.yml | 2 +- .github/workflows/python-app.yml | 4 ++-- pyproject.toml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7889b69d..622c9626 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,7 +49,7 @@ jobs: # 6) Set up Python & install dependencies - uses: actions/setup-python@v5 - with: { python-version: "3.10" } + with: { python-version: "3.13" } - name: Install Python deps run: | pip install -e . diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 8074be85..a111e34f 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -19,10 +19,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up Python 3.10 + - name: Set up Python 3.13 uses: actions/setup-python@v3 with: - python-version: "3.10" + python-version: "3.13" - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/pyproject.toml b/pyproject.toml index 829af4e5..2312a403 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ keywords = ["trace", "opto", "AutoDiff"] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", - "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.13", ] [project.optional-dependencies] From aa127e775584473cea8ee1e185e042db94119a18 Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 18 Feb 2026 02:30:37 -0500 Subject: [PATCH 314/314] add multi-objective convex function toy example (with norm input objective) --- examples/multi_objective_convex_fn.py | 663 ++++++++++++++++++++++++++ 1 file changed, 663 insertions(+) create mode 100644 examples/multi_objective_convex_fn.py diff --git a/examples/multi_objective_convex_fn.py b/examples/multi_objective_convex_fn.py new file mode 100644 index 00000000..50f11428 --- /dev/null +++ b/examples/multi_objective_convex_fn.py @@ -0,0 +1,663 @@ +import re +import numpy as np +import cvxpy as cp +from opto.trace.utils import dedent + +def np_random(seed: int | None = None) -> tuple[np.random.Generator, int]: + if seed is not None and not (isinstance(seed, int) and 0 <= seed): + if isinstance(seed, int) is False: + raise Exception(f"Seed must be a python integer, actual type: {type(seed)}") + else: + raise Exception(f"Seed must be greater or equal to zero, actual value: {seed}") + seed_seq = np.random.SeedSequence(seed) + np_seed = seed_seq.entropy + rng = np.random.Generator(np.random.PCG64(seed_seq)) + return rng, np_seed + + +def _norm_term(x: np.ndarray, norm_coef: float, norm_kind: str) -> float: + if norm_coef == 0.0: + return 0.0 + if norm_kind == "l2sq": + return float(norm_coef * (x[0] ** 2 + x[1] ** 2)) + if norm_kind == "l2": + return float(norm_coef * np.sqrt(x[0] ** 2 + x[1] ** 2)) + if norm_kind == "l1": + return float(norm_coef * (abs(x[0]) + abs(x[1]))) + raise ValueError("norm_kind must be one of {'l2sq','l2','l1'}") + + +def _rosenbrock_cubic_global_min(a: float, b: float, lam: float) -> tuple[np.ndarray, float]: + """ + For f(u,v)=(a-u)^2 + b(v-u^2)^2 + lam(u^2+v^2), b>0, lam>=0. + Returns (x_star, f_star). + """ + # lam == 0: classic Rosenbrock minimum at (a, a^2) with value 0. + if lam == 0.0: + x_star = np.array([a, a ** 2], dtype=float) + f_star = 0.0 + return x_star, f_star + + # Solve cubic: c3*u^3 + (1+lam)*u - a = 0 with c3 = 2*b*lam/(b+lam) + c3 = 2.0 * b * lam / (b + lam) + c1 = 1.0 + lam + roots = np.roots([c3, 0.0, c1, -a]) + + best = None + for r in roots: + if abs(r.imag) < 1e-10: + u = float(r.real) + v = (b / (b + lam)) * u * u + x = np.array([u, v], dtype=float) + # evaluate full objective + base = (a - u) ** 2 + b * (v - u * u) ** 2 + f = float(base + lam * (u * u + v * v)) + if best is None or f < best[1]: + best = (x, f) + + if best is None: + raise RuntimeError("Unexpected: cubic had no real root.") + return best + + +# --------------------------- +# SOS / moment relaxation for Six-Hump Camel on a box +# --------------------------- + +def _monomials_upto_degree(k: int) -> list[tuple[int, int]]: + out: list[tuple[int, int]] = [] + for deg in range(k + 1): + for i in range(deg + 1): + j = deg - i + out.append((i, j)) + return out + +def _add_mono(a: tuple[int, int], b: tuple[int, int]) -> tuple[int, int]: + return (a[0] + b[0], a[1] + b[1]) + +def _build_moment_matrix(y: dict[tuple[int,int], cp.Expression], basis: list[tuple[int,int]]) -> cp.Expression: + m = len(basis) + blocks = [] + for i in range(m): + row = [] + for j in range(m): + row.append(y[_add_mono(basis[i], basis[j])]) + blocks.append(row) + return cp.bmat(blocks) + +def _build_localizing_matrix_linear( + y: dict[tuple[int,int], cp.Expression], + basis: list[tuple[int,int]], + g_lin: dict[tuple[int,int], float], # g(x,y) = c00 + c10 x + c01 y +) -> cp.Expression: + m = len(basis) + blocks = [] + for i in range(m): + row = [] + for j in range(m): + a = _add_mono(basis[i], basis[j]) + expr = 0 + for beta, c in g_lin.items(): + expr += c * y[_add_mono(a, beta)] + row.append(expr) + blocks.append(row) + return cp.bmat(blocks) + +def _six_hump_coeffs(lam_l2sq: float = 0.0) -> dict[tuple[int,int], float]: + """ + Base six-hump camel: + f(x,y) = 4x^2 -2.1 x^4 + (1/3) x^6 + x y - 4y^2 + 4y^4 + + With l2sq regularizer: + f(x,y) + lam*(x^2 + y^2) + => add lam to the (2,0) and (0,2) coefficients. + """ + lam = float(lam_l2sq) + return { + (2, 0): 4.0 + lam, + (4, 0): -2.1, + (6, 0): 1.0 / 3.0, + (1, 1): 1.0, + (0, 2): -4.0 + lam, + (0, 4): 4.0, + } + + +def six_hump_sos_certificate_on_box( + bound: float = 2.0, + order_d: int = 3, + solver: str = "SCS", + verbose: bool = False, + lam_l2sq: float = 0.0, +) -> tuple[float, str]: + """ + Moment relaxation (Lasserre) order d for Six-Hump Camel (+ optional l2sq) on [-bound, bound]^2. + Returns (lower_bound, status). lower_bound is the SOS certificate gamma. + """ + if order_d < 3: + raise ValueError("For degree-6 polynomial, use order_d >= 3") + + coeff = _six_hump_coeffs(lam_l2sq=lam_l2sq) + max_deg = 2 * order_d + all_monos = _monomials_upto_degree(max_deg) + + y: dict[tuple[int,int], cp.Variable] = {m: cp.Variable() for m in all_monos} + constraints = [y[(0, 0)] == 1.0] + + basis_d = _monomials_upto_degree(order_d) + M = _build_moment_matrix(y, basis_d) + constraints.append(M >> 0) + + basis_d1 = _monomials_upto_degree(order_d - 1) + g_list = [ + {(0,0): bound, (1,0): -1.0, (0,1): 0.0}, + {(0,0): bound, (1,0): 1.0, (0,1): 0.0}, + {(0,0): bound, (1,0): 0.0, (0,1): -1.0}, + {(0,0): bound, (1,0): 0.0, (0,1): 1.0}, + ] + for g in g_list: + L = _build_localizing_matrix_linear(y, basis_d1, g) + constraints.append(L >> 0) + + obj = cp.Minimize(sum(c * y[m] for m, c in coeff.items())) + prob = cp.Problem(obj, constraints) + prob.solve(solver=solver, verbose=verbose) + + return float(prob.value), str(prob.status) + +class LossLandscapeBase: + def __init__( + self, + callable_func, + x_low, + x_high, + optimal_sol, + feedback=0, + seed=None, + precision_digit=2, + horizon=10, + # multi-objective / regularization knobs + norm_coef: float = 0.0, + norm_kind: str = "l2sq", + # done criterion uses certificate + done_tol: float = 1e-2, + ): + self.x_low = x_low + self.x_high = x_high + + self._np_random = None + self.stop_keywords = ["reach", "stay", "stop"] + + # base (unregularized) function + self.base_func = callable_func + + self.norm_coef = float(norm_coef) + self.norm_kind = str(norm_kind) + self.done_tol = float(done_tol) + + # wrapped function used everywhere in env: base + norm + def augmented(x: np.ndarray) -> float: + x = np.asarray(x, dtype=float) + return float(self.base_func(x) + _norm_term(x, self.norm_coef, self.norm_kind)) + + self.callable_func = augmented + + self.prev_x = None + self.left_attempts = horizon + + self.optimal_sol = optimal_sol + self.precision_digit = precision_digit + self.horizon = horizon + self._seed = self.seed(seed) + + # subclass sets this (certificate-based) in _init_certificate() + self.certificate_y: float | None = None + self.certificate_meta: dict = {} + + self._init_certificate() + if self.certificate_y is None: + raise RuntimeError("Subclass must set self.certificate_y in _init_certificate().") + + # Use certificate as min_y for reward range + done checks + self.min_y = float(self.certificate_y) + + self.reward_range = (self.get_min_reward(), -self.min_y) + + if self.norm_coef != 0.0: + norm_desc = { + "l2sq": "||x||_2^2 (squared L2 norm)", + "l2": "||x||_2 (L2 norm)", + "l1": "||x||_1 (L1 norm)", + }.get(self.norm_kind, self.norm_kind) + + objective_line = ( + f"Your goal is to minimize the total objective:\n" + f" y(x) = f(x) + {self.norm_coef} * {norm_desc}\n" + f"where f(x) is the base function output and x is a 2D vector." + ) + else: + objective_line = ( + "Your goal is to minimize the function output:\n" + " y(x) = f(x)\n" + "where f(x) is the base function output and x is a 2D vector." + ) + + self.docstring = dedent(f""" + You are trying to minimize an objective by choosing the input x. + + {objective_line} + + You get to observe y once you choose x, where x is a 2-dimensional vector: + x = [x1, x2], with real-valued coordinates. + + The allowed range for x1 and x2 is [{self.x_low}, {self.x_high}]. + Please do not choose x outside of this range. + + You have {self.horizon} attempts. + You can choose to stop at any time by outputting a message containing one of: {", ".join(self.stop_keywords)}. + + Output format: + x = [x1, x2] + """).strip() + + self.called_reset = False + + def _init_certificate(self) -> None: + """ + Subclasses must set: + self.certificate_y: float (target min value / certificate) + Optionally: + self.certificate_meta: dict with info (solver status, x*, etc.) + """ + raise NotImplementedError + + def get_min_reward(self): + # conservative: evaluate on corners of box for reward lower bound + x_range = [self.x_low, self.x_high] + y_vals = [self.callable_func(np.array([x_range[i], x_range[j]])) for i in range(2) for j in range(2)] + y_max = max(y_vals) + return -float(y_max) + + def get_optimal_solution(self): + return self.optimal_sol + + def reset(self, **kwargs): + if "seed" in kwargs: + self._seed = self.seed(kwargs["seed"]) + + x = self.np_random.uniform(self.x_low, self.x_high, size=2) + x = np.round(x, self.precision_digit) + self.prev_x = x + + y = self.callable_func(x) + self.left_attempts = self.horizon + + # obs = f"x={x.tolist()}\nFunction outputs y = {y}\nYou have {self.left_attempts} attempts left!\n" + loss_line, info = self._format_loss_report(x) + obs = loss_line + obs += "Please output the next x that will make this function output the smallest y.\n" + obs += "Format: x = [x1, x2]\n" + obs += "Output:" + + self.called_reset = True + return obs + + def seed(self, seed=None): + self._np_random, seed = np_random(seed) + return [seed] + + @property + def np_random(self): + if self._np_random is None: + self.seed() + return self._np_random # type: ignore + + def text_extract(self, text): + for stop_word in self.stop_keywords: + if stop_word in text: + return None, True + + pattern = r"\[(-?\d+\.?\d*(?:e[-+]?\d+)?),\s*(-?\d+\.?\d*(?:e[-+]?\d+)?)\]" + match = re.search(pattern, text) + if match is None: + return None, False + numbers = [float(g) for g in match.groups()] + return np.array(numbers, dtype=float), False + + def _is_success(self, loss: float) -> bool: + # Done criterion: close to certificate/guarantee. + # Note: certificate_y is a lower bound for SOS cases; if it's tight, this is meaningful. + return abs(float(loss) - float(self.certificate_y)) <= self.done_tol + + def _eval_losses(self, x: np.ndarray) -> tuple[float, float, float]: + x = np.asarray(x, dtype=float) + base = float(self.base_func(x)) + reg = float(_norm_term(x, self.norm_coef, self.norm_kind)) + total = base + reg + return base, reg, total + + def _format_loss_report(self, x: np.ndarray) -> tuple[str, dict]: + base, reg, total = self._eval_losses(x) + info = { + "base_loss": base, + "reg_loss": reg, + "total_loss": total, + "certificate_y": float(self.certificate_y), + "gap": float(total - float(self.certificate_y)), + } + + if self.norm_coef != 0.0: + # optional: report the raw norm too (not multiplied by coef) + if self.norm_kind == "l2sq": + norm_val = float(x[0] ** 2 + x[1] ** 2) + elif self.norm_kind == "l2": + norm_val = float(np.sqrt(x[0] ** 2 + x[1] ** 2)) + elif self.norm_kind == "l1": + norm_val = float(abs(x[0]) + abs(x[1])) + else: + norm_val = None + + info["norm_value"] = norm_val + info["norm_kind"] = self.norm_kind + info["norm_coef"] = float(self.norm_coef) + + line = ( + f"Function outputs total y = {total}\n" + f" base f(x) = {base}\n" + f" regularizer = {reg} (coef={self.norm_coef}, kind={self.norm_kind}, norm={norm_val})\n" + ) + else: + line = f"Function outputs y = {total}\n" + + return line, info + + def step(self, action): + if not self.called_reset: + raise Exception("must call env.reset() first before step()") + + x, stop = self.text_extract(action) + + if x is None and stop is False: + feedback = ( + f"You entered an invalid action: {action}" + + f" Please enter a valid action within ({self.x_low, self.x_high})" + ) + return None, -1, True, { + "feedback": feedback, + "success": False, + "base_loss": None, + "reg_loss": None, + "total_loss": None, + "certificate_y": float(self.certificate_y), + "gap": None, + } + + if stop: + base, reg, total = self._eval_losses(self.prev_x) + success = self._is_success(total) + feedback = f"You have chosen to stop at {self.prev_x}." + feedback += " You have reached the (certified) minimum!" if success else " You have not reached the (certified) minimum!" + return None, total, True, { + "feedback": feedback, + "success": success, + "base_loss": base, + "reg_loss": reg, + "total_loss": total, + "certificate_y": float(self.certificate_y), + "gap": float(total - float(self.certificate_y)), + } + + if np.any(x < self.x_low) or np.any(x > self.x_high): + base, reg, total = self._eval_losses(self.prev_x) + feedback = f"x must be within [{self.x_low}, {self.x_high}]. You gave {x.tolist()}." + return None, total, True, { + "feedback": feedback, + "success": False, + "base_loss": base, + "reg_loss": reg, + "total_loss": total, + "certificate_y": float(self.certificate_y), + "gap": float(total - float(self.certificate_y)), + } + + base, reg, total = self._eval_losses(x) + + if self._is_success(total): + feedback = f"Function outputs y: {total}\nYou have reached the (certified) minimum!" + return feedback, -total, True, { + "feedback": feedback, + "success": True, + "base_loss": base, + "reg_loss": reg, + "total_loss": total, + "certificate_y": float(self.certificate_y), + "gap": float(total - float(self.certificate_y)), + } + + loss_line, info = self._format_loss_report(x) + obs = loss_line + obs += f"You have {self.left_attempts} attempts left!\n" + obs += "Please output the next x that will make this function output the smallest y.\n" + obs += "Format: x = [x1, x2]\n" + obs += "Output:" + + self.prev_x = x + self.left_attempts -= 1 + + r = np.clip(float(-total), self.get_min_reward(), -self.min_y) + feedback = f"You chose {action}. Choose different numbers such that you can minimize y." + return obs, r, False, { + "feedback": feedback, + "success": False, + "base_loss": base, + "reg_loss": reg, + "total_loss": total, + "certificate_y": float(self.certificate_y), + "gap": float(total - float(self.certificate_y)), + } + + +class Rosenbrock(LossLandscapeBase): + def __init__( + self, + a=1.0, + b=1.0, + feedback=0, + seed=None, + horizon=10, + precision_digit=2, + norm_coef: float = 1.0, + norm_kind: str = "l2sq", + done_tol: float = 1e-2, + ): + self.a = float(a) + self.b = float(b) + + def base(x: np.ndarray) -> float: + return float((self.a - x[0]) ** 2 + self.b * (x[1] - x[0] ** 2) ** 2) + + super().__init__( + callable_func=base, + x_low=-5, + x_high=10, + optimal_sol=np.ones(2), + feedback=feedback, + seed=seed, + precision_digit=precision_digit, + horizon=horizon, + norm_coef=norm_coef, + norm_kind=norm_kind, + done_tol=done_tol, + ) + + def _init_certificate(self) -> None: + if self.norm_kind != "l2sq": + raise ValueError("Rosenbrock cubic certificate requires norm_kind='l2sq'.") + + if self.norm_coef < 0: + raise ValueError("For a meaningful global certificate, norm_coef should be >= 0.") + + x_star, f_star = _rosenbrock_cubic_global_min(self.a, self.b, self.norm_coef) + + self.certificate_y = float(f_star) + self.optimal_sol = x_star + self.certificate_meta = {"method": "cubic", "x_star": x_star, "f_star": float(f_star)} + + +class SixHumpCamel(LossLandscapeBase): + def __init__( + self, + feedback=0, + seed=None, + horizon=10, + precision_digit=4, + norm_coef: float = 1.0, + norm_kind: str = "l2sq", + done_tol: float = 1e-3, + sos_solver: str = "SCS", + sos_order_d: int = 3, + sos_verbose: bool = False, + ): + self.sos_solver = sos_solver + self.sos_order_d = sos_order_d + self.sos_verbose = sos_verbose + + def base(x: np.ndarray) -> float: + u, v = float(x[0]), float(x[1]) + return float((4 - 2.1 * u ** 2 + (u ** 4) / 3) * u ** 2 + u * v + (-4 + 4 * v ** 2) * v ** 2) + + super().__init__( + callable_func=base, + x_low=-2, + x_high=2, + optimal_sol=[np.array([0.0898, -0.7126]), np.array([-0.0898, 0.7126])], + feedback=feedback, + seed=seed, + precision_digit=precision_digit, + horizon=horizon, + norm_coef=norm_coef, + norm_kind=norm_kind, + done_tol=done_tol, + ) + + def _init_certificate(self) -> None: + if self.norm_coef != 0.0 and self.norm_kind != "l2sq": + raise ValueError( + "SixHumpCamel SOS certificate supports norm_coef==0 or norm_kind=='l2sq'. " + "For l1/l2 you need epigraph variables." + ) + + gamma, status = six_hump_sos_certificate_on_box( + bound=2.0, + order_d=self.sos_order_d, + solver=self.sos_solver, + verbose=self.sos_verbose, + lam_l2sq=self.norm_coef, + ) + self.certificate_y = float(gamma) + self.certificate_meta = { + "method": "moment_sdp", + "gamma": float(gamma), + "status": status, + "lam_l2sq": float(self.norm_coef), + "bound": 2.0, + "order_d": self.sos_order_d, + "solver": self.sos_solver, + } + + +# ============ Multi-objective test harness (Approach 1: BasicSearch + ObjectiveConfig) ============ +from opto import trace +from opto.trainer.guide import Guide +from opto.trainer.loggers import TensorboardLogger +from opto import trainer +from opto.trainer.objectives import ObjectiveConfig +from opto.trainer.algorithms.basic_algorithms import BasicSearchAlgorithm as SearchAlgorithm +from typing import Tuple +from copy import copy + + +class RewardGuide(Guide): + """ + Multi-objective metrics: + + - base_loss: minimize + - reg_loss: minimize + + (The trainer's ObjectiveConfig decides how to combine/compare.) + """ + + def __init__(self, env: LossLandscapeBase): + self.env = env + + def _score_action_on_env_copy(self, action: str): + env_copy = copy.deepcopy(self.env) + obs, reward, done, info = env_copy.step(action) + return obs, reward, done, info + + def get_feedback(self, query: str, response: str, reference=None, **kwargs) -> Tuple[float, str]: + # Legacy scalar path: advances the real env. + obs, reward, done, info = self.env.step(response) + return float(reward), ((obs + "\n\n") if obs else "") + info.get("feedback", "") + + def get_score_dict(self, query: str, response: str, reference=None, **kwargs) -> dict[str, float]: + # Vector score path for trainer-side selection: + obs, reward, done, info = self._score_action_on_env_copy(response) + + base_loss = info.get("base_loss") + reg_loss = info.get("reg_loss") + + # If action invalid, your env sets losses to None. Map to +inf so it never gets selected. + if base_loss is None or reg_loss is None: + base_loss = float("inf") + reg_loss = float("inf") + + return { + "base_loss": float(base_loss), # minimize + "reg_loss": float(reg_loss), # minimize + } + +def main(): + env = SixHumpCamel(horizon=200) + train_dataset = dict(inputs=[None], infos=[None]) + + instruction = env.reset() + initial_input = instruction.split("\n")[0].strip() + param = trace.node(initial_input, description="Input x into the hidden function to get y.", trainable=True) + + guide = RewardGuide(env) + logger = TensorboardLogger(log_dir="./logs/basicsearch_multiobjective_on_loss_landscape") + + # We want high reward, but penalize invalid actions and overly long outputs. + objective_config = ObjectiveConfig( + mode="weighted", + weights={"base_loss": 1.0, "reg_loss": 1.0}, + minimize=frozenset({"base_loss", "reg_loss"}), + seed=0, + ) + + trainer.train( + model=param, + algorithm=SearchAlgorithm, + train_dataset=train_dataset, + logger=logger, + score_range=[-10, 10], + num_epochs=1, + num_steps=5, + batch_size=1, + num_batches=2, + verbose=False, + guide=guide, + objective_config=objective_config, + # basic search knobs (keep small for smoke test) + num_candidates=4, + num_proposals=4, + optimizer_kwargs={ + "objective": "You have a task of guessing two numbers. Output x=[x1,x2] and minimize y.", + "memory_size": 10, + }, + ) + + +if __name__ == "__main__": + main()

ABCDEFGH
1
2
3
1
2
3
4
5
6