# file name: optimizer.py (updated with fixes for SearchAgent initialization)
import numpy as np
import logging
import math
from typing import Dict, List, Optional, Tuple
from collections import deque

from .config import DPO_Config
from .agent import SearchAgent
from ..architecture.gene import ArchitectureGene
from ..evaluation.ensemble import EnsembleEstimator
from ..constraints.handler import AdvancedConstraintHandler
from ..utils.logger import get_logger

class DPO_NAS:
    def __init__(self,
                 config: DPO_Config,
                 estimator: Optional[EnsembleEstimator] = None,
                 constraint_handler: Optional[AdvancedConstraintHandler] = None,
                 logger: Optional[logging.Logger] = None):
        self.config = config
        
        # Validate configuration
        config.validate()
        if config.w_loss > 0:
            if logger:
                logger.warning("w_loss > 0: Using legacy loss-based fitness. For accuracy-aware mode, set w_loss = 0.0")
        
        self.estimator = estimator or EnsembleEstimator()
        self.constraint_handler = constraint_handler or AdvancedConstraintHandler(config)
        self.logger = logger or get_logger('DPO-NAS', self.config.verbose)
        self.islands = [[] for _ in range(config.num_islands if config.island_model else 1)]
        self.seen_hashes = set()  # Track seen architectures to penalize clones
        self.best_agent: Optional[SearchAgent] = None
        self.best_accuracy_agent: Optional[SearchAgent] = None  # Agent with highest raw accuracy
        self.best_fitness: float = float('inf')
        self.best_accuracy: float = 0.0  # Track best accuracy separately
        self.best_per_island = [None] * len(self.islands)
        
        # Pareto archive for optional Pareto-assisted mode
        self.pareto_archive: List[SearchAgent] = []
        
        # Temperature for probabilistic acceptance
        self.temperature: float = config.temperature_start
        
        # History tracking with accuracy focus
        self.history = {
            'iterations': [],
            'best_fitness': [],
            'best_accuracy': [],  # Track accuracy progression
            'avg_fitness': [],
            'worst_fitness': [],
            'improvement_rate': [],
            'temperature': [],  # Track temperature decay
            'debt_norms': [],  # Track average debt magnitude
            'diversity_scores': [],  # Track population diversity
            'acceptance_rates': [],  # Track acceptance probabilities
            'best_architecture': None,
            'auc_10': 0.0,
            'auc_25': 0.0,
            'auc_50': 0.0,
            'time_to_95': 0,
            'time_to_99': 0,
        }
        
        # Improvement tracking with accuracy awareness
        self.improvement_tracker = deque(maxlen=30)
        self.accuracy_improvement_tracker = deque(maxlen=30)
        
        # Population statistics for normalization
        self.population_stats = {
            'latency_range': (0.0, 1.0),
            'flops_range': (0.0, 1.0),
            'memory_range': (0.0, 1.0),
            'fitness_std': 1.0,
        }
        
        # Acceptance statistics
        self.acceptance_stats = {
            'total_candidates': 0,
            'accepted_better': 0,
            'accepted_worse': 0,
            'rejected': 0
        }
        
        # Pre-calculate bounds for enforcement
        self._ops_len = len(ArchitectureGene.OPERATIONS) - 0.01
        self._kers_len = len(ArchitectureGene.KERNELS) - 0.01
        
        # Adaptive acceptance scaling
        self._recent_fitness_deltas = deque(maxlen=50)
        self.target_acceptance_rate = 0.25
        self.acceptance_scale_bounds = (0.5, 2.5)
        self._effective_elite_ratio = self.config.elite_ratio
        self._component_flags = {
            "debt": True,
            "acceptance": True,
            "diversity": True,
        }
        self._last_acceptance_rate = 0.0
        self._last_diversity = 0.0
        self._last_improvement = 0.0
        
        # SOTA: Island-specific temperatures for population-based annealing
        self.island_temperatures = [config.temperature_start] * len(self.islands)
        
        # SOTA: Island specialization biases (accuracy, cost, balanced)
        island_biases = ['balanced'] * len(self.islands)
        if len(self.islands) >= 3:
            island_biases[0] = 'accuracy'   # Island 0: accuracy-heavy
            island_biases[1] = 'cost'       # Island 1: cost-heavy
            island_biases[2] = 'balanced'   # Island 2: balanced
        self.island_biases = island_biases
        
        # SOTA: Fitness smoothing for noise reduction
        self.fitness_history = [deque(maxlen=5) for _ in range(len(self.islands))]
        
        # SOTA: Age-based tracking for Regularized Evolution
        self.generation_counter = 0

    def initialize_population(self) -> None:
        agents_per_island = max(1, self.config.population_size // len(self.islands))
        self.logger.info(f"Initializing population: {self.config.population_size} agents")
        
        cont   = getattr(self.config, 'continuous_mode', False)
        n_dims = getattr(self.config, 'n_continuous_params', 0)
        for island_idx, island in enumerate(self.islands):
            for agent_idx in range(agents_per_island):
                # In continuous mode pass n_dims so the gene is sized exactly
                # to the problem dimensionality (not the default NAS D=43).
                gene = ArchitectureGene(
                    continuous_mode=cont,
                    n_dims=n_dims,
                )
                if cont and agents_per_island > 1:
                    # Low-discrepancy population seeding improves early coverage
                    # versus i.i.d. uniform draws on categorical-encoded HPO spaces.
                    frac = (agent_idx + np.random.random()) / agents_per_island
                    offsets = (0.61803398875 * np.arange(gene.D) + np.random.random(gene.D)) % 1.0
                    gene.gene = ((frac + offsets) % 1.0).astype(np.float32)
                # Create agent with persistent debt vector
                agent = SearchAgent(
                    gene=gene, 
                    island_id=island_idx,
                    debt_vector=np.zeros_like(gene.gene)
                )
                # Evaluate with accuracy-aware fitness - add warmup bias
                self._evaluate_agent(agent, iteration=0, is_initial=True)
                island.append(agent)
                
                # Update bests (accuracy tracked separately)
                if agent.fitness < self.best_fitness:
                    self.best_fitness = agent.fitness
                    self.best_agent = agent
                
                if agent.accuracy > self.best_accuracy:
                    self.best_accuracy = agent.accuracy
                
                if self.best_per_island[island_idx] is None or agent.fitness < self.best_per_island[island_idx].fitness:
                    self.best_per_island[island_idx] = agent
                
                # Initialize Pareto archive if enabled
                if self.config.fitness_mode == 'pareto_assisted':
                    self._update_pareto_archive(agent)

    # =========================================================================
    # SOTA ENHANCEMENT METHODS - NSGA-II, Local Search, Objective-Aware Debt
    # =========================================================================
    
    def _nsga_ii_sort_and_select(self, island_idx: int, target_size: int) -> None:
        """
        NSGA-II based selection: Pareto sorting + crowding distance.
        Replaces worst fitness-based selection with true multi-objective survival.
        """
        island = self.islands[island_idx]
        if len(island) <= target_size:
            return
        
        # Non-dominated sorting
        fronts = self._non_dominated_sort(island)
        
        # Select from fronts until we have target_size
        selected = []
        for front in fronts:
            if len(selected) + len(front) <= target_size:
                selected.extend(front)
            else:
                # Need crowding distance for this front
                self._compute_crowding_distance(front)
                # Sort by crowding distance (descending)
                front.sort(key=lambda a: a.crowding_distance, reverse=True)
                remaining = target_size - len(selected)
                selected.extend(front[:remaining])
                break
        
        self.islands[island_idx] = selected
    
    def _non_dominated_sort(self, agents: List[SearchAgent]) -> List[List[SearchAgent]]:
        """
        NSGA-II non-dominated sorting.
        Returns list of fronts, where front[0] is fully non-dominated.
        """
        fronts = []
        
        # Calculate domination relationships
        domination_count = [0] * len(agents)
        dominated_solutions = [[] for _ in range(len(agents))]
        
        for i in range(len(agents)):
            for j in range(i + 1, len(agents)):
                if agents[i].is_dominated(agents[j]):
                    dominated_solutions[j].append(i)
                    domination_count[i] += 1
                elif agents[j].is_dominated(agents[i]):
                    dominated_solutions[i].append(j)
                    domination_count[j] += 1
        
        # Front 0: all non-dominated solutions
        current_front = [i for i in range(len(agents)) if domination_count[i] == 0]
        rank = 0
        
        while current_front:
            front = [agents[i] for i in current_front]
            fronts.append(front)
            
            # Assign rank
            for i in current_front:
                agents[i].pareto_rank = rank
            
            # Find next front
            next_front = set()
            for i in current_front:
                for j in dominated_solutions[i]:
                    domination_count[j] -= 1
                    if domination_count[j] == 0:
                        next_front.add(j)
            
            current_front = list(next_front)
            rank += 1
        
        return fronts
    
    def _compute_crowding_distance(self, front: List[SearchAgent]) -> None:
        """
        NSGA-II crowding distance: agents on corner and low-density areas get higher distance.
        """
        n = len(front)
        for agent in front:
            agent.crowding_distance = 0.0
        
        if n <= 2:
            for agent in front:
                agent.crowding_distance = float('inf')
            return
        
        # Distance for each objective
        objectives = [
            ('accuracy', lambda a: -a.accuracy),  # Negate for minimization
            ('cost', lambda a: sum(a.cost_metrics.values()))
        ]
        
        for obj_name, obj_func in objectives:
            # Sort by objective
            front.sort(key=obj_func)
            
            # Distance contribution
            front[0].crowding_distance = float('inf')
            front[-1].crowding_distance = float('inf')
            
            max_val = obj_func(front[-1]) - obj_func(front[0])
            if max_val == 0:
                continue
            
            for i in range(1, n - 1):
                contribution = (obj_func(front[i + 1]) - obj_func(front[i - 1])) / max_val
                front[i].crowding_distance += contribution
    
    def _compute_objective_aware_debt(self, current: SearchAgent, candidate: SearchAgent) -> Tuple[np.ndarray, np.ndarray]:
        """
        SOTA: Compute objective-aware debt (accuracy and cost debt separately).
        Projects debt into multi-objective space.
        """
        delta_vec = candidate.gene.gene - current.gene.gene
        
        # Accuracy component: direction of accuracy change
        accuracy_change = candidate.accuracy - current.accuracy
        if accuracy_change < 0:  # Got worse
            accuracy_debt = delta_vec.copy()
        else:
            accuracy_debt = np.zeros_like(delta_vec)
        
        # Cost component: direction of cost change
        current_cost = sum(current.cost_metrics.values())
        candidate_cost = sum(candidate.cost_metrics.values())
        cost_change = candidate_cost - current_cost
        if cost_change > 0:  # Got worse (higher cost)
            cost_debt = delta_vec.copy()
        else:
            cost_debt = np.zeros_like(delta_vec)
        
        return accuracy_debt, cost_debt
    
    def _apply_discrete_hybrid_mutation(self, gene_arr: np.ndarray, num_layers: int, 
                                       num_cells: int, strength: float = 0.1) -> np.ndarray:
        """
        SOTA discrete mutation: Mix of categorical switches and Gaussian perturbation.
        Operations and kernels use categorical; multipliers use Gaussian.
        
        CONVERGENCE FIX: Force categorical jumps when strength becomes too small
        to guarantee discrete changes in late phase (prevents stagnation trap).
        """
        mutated = gene_arr.copy()

        # In continuous mode all positions are real-valued ∈ [0, 1].
        # Replace discrete categorical mutations with pure Gaussian + clip.
        if getattr(self.config, 'continuous_mode', False):
            mutated += np.random.randn(len(mutated)) * strength
            np.clip(mutated, 0.0, 1.0, out=mutated)
            return mutated

        # Operations: categorical with GUARANTEED minimum probability
        # Even at high iteration count, ensure at least 1% chance to flip
        op_prob = max(strength * 0.3, 0.01)
        for i in range(num_layers):
            if np.random.random() < op_prob:
                # CONVERGENCE FIX: Force different operation (not just random)
                current_op = int(mutated[i])
                available_ops = [x for x in range(int(self._ops_len)) if x != current_op]
                if available_ops:
                    mutated[i] = float(np.random.choice(available_ops))
        
        # Kernels: categorical with same guarantee
        kern_prob = max(strength * 0.3, 0.01)
        for i in range(num_layers, 2 * num_layers):
            if np.random.random() < kern_prob:
                # CONVERGENCE FIX: Force different kernel (not just random)
                current_kernel = int(mutated[i])
                available_kernels = [x for x in range(int(self._kers_len)) if x != current_kernel]
                if available_kernels:
                    mutated[i] = float(np.random.choice(available_kernels))
        
        # Skip connections: Gaussian + clip
        skip_start = 2 * num_layers
        skip_end = skip_start + num_cells
        mutated[skip_start:skip_end] += np.random.randn(num_cells) * strength
        np.clip(mutated[skip_start:skip_end], 0, 1, out=mutated[skip_start:skip_end])
        
        # Multipliers: Gaussian only
        mutated[-2:] += np.random.randn(2) * strength * 0.5
        np.clip(mutated[-2:], 0.3, 2.0, out=mutated[-2:])
        
        return mutated
    
    def _local_elite_search(self, island_idx: int, iteration: int, top_k: int = 3) -> None:
        """
        SOTA local refinement: Intensive local search on top agents when stagnating.
        Inspired by Regularized Evolution.
        """
        island = self.islands[island_idx]
        if len(island) < 3:
            return
        
        # Only trigger on stagnation
        if iteration < 0.5 * self.config.max_iterations:
            return
        
        # Get top-k agents
        island.sort(key=lambda a: a.fitness)
        top_agents = island[:top_k]
        
        new_members = []
        for parent in top_agents:
            # Keep local refinement lightweight to preserve wall-time budget.
            for _ in range(6):
                # Small mutation
                gene_arr = parent.gene.gene.copy()
                gene_arr = self._apply_discrete_hybrid_mutation(
                    gene_arr, parent.gene.num_layers, parent.gene.num_cells, strength=0.05
                )
                
                new_gene = ArchitectureGene(
                    parent.gene.num_layers,
                    parent.gene.num_cells,
                    continuous_mode=parent.gene.continuous_mode,
                )
                new_gene.gene = gene_arr
                
                # Evaluate and accept only if better
                child = SearchAgent(
                    gene=new_gene,
                    island_id=island_idx,
                    debt_vector=parent.debt_vector.copy() if parent.debt_vector is not None else None,
                    mutation_magnitude=parent.mutation_magnitude * 0.8,
                    acceptance_rate=parent.acceptance_rate
                )
                
                self._evaluate_agent(child, iteration)
                
                # Accept only if strictly better
                if child.fitness < parent.fitness:
                    new_members.append(child)
                    parent = child  # Greedy hill-climbing
        
        # Add successful micro-mutations to island
        if new_members:
            island.extend(new_members)
            self._nsga_ii_sort_and_select(island_idx, self.config.population_size // len(self.islands))
    
    def _apply_island_specialization_bias(self, agent: SearchAgent, island_idx: int) -> float:
        """
        SOTA island specialization: Bias fitness based on island role.
        Island 0 (accuracy): reward high accuracy
        Island 1 (cost): reward low cost
        Island 2 (balanced): no bias
        """
        bias = self.island_biases[island_idx]
        
        if bias == 'accuracy':
            # Accuracy island: penalize cost more
            return agent.fitness * 0.85 + 0.15 * sum(agent.cost_metrics.values())
        elif bias == 'cost':
            # Cost island: penalize low accuracy more
            return agent.fitness * 0.7 + 0.3 * (1.0 - agent.accuracy)
        else:
            # Balanced: use original fitness
            return agent.fitness
    
    def _smooth_fitness(self, agent: SearchAgent, island_idx: int, alpha: float = 0.7) -> float:
        """
        SOTA fitness smoothing: Exponential moving average to reduce evaluation noise.
        """
        history = self.fitness_history[island_idx]
        history.append(agent.fitness)
        
        if len(history) < 2:
            return agent.fitness
        
        # EMA: new_smooth = alpha * current + (1-alpha) * old_average
        old_avg = np.mean(list(history)[:-1])
        smoothed = alpha * agent.fitness + (1.0 - alpha) * old_avg
        
        agent.smoothed_fitness = smoothed
        return smoothed
    
    def _apply_opposition_based_learning(self, gene_arr: np.ndarray, bounds: Tuple[float, float]) -> np.ndarray:
        """
        SOTA Opposition-Based Learning: If current doesn't work, try opposite direction.
        For discrete/continuous hybrid spaces.
        Returns the opposition point in search space.
        """
        min_val, max_val = bounds
        if max_val - min_val < 1e-6:
            return gene_arr
        
        # Opposition: x_opp = min + max - x
        opposition = min_val + max_val - gene_arr
        
        # Clip to bounds
        opposition = np.clip(opposition, min_val, max_val)
        
        return opposition
    
    def _update_debt_reservoir(self, agent: SearchAgent, new_debt: np.ndarray) -> None:
        """
        SOTA Debt Reservoir: Non-Markovian memory of failed directions.
        Stores history of failed debt vectors for backflow mechanism.
        Allows optimizer to "remember" bad architectural patterns.
        """
        if len(agent.debt_reservoir) >= 5:
            agent.debt_reservoir.pop(0)
        
        agent.debt_reservoir.append(new_debt.copy())
    
    def _apply_debt_backflow(self, agent: SearchAgent, params: Dict) -> np.ndarray:
        """
        SOTA Debt Backflow: Weighted sum of historical failed directions.
        Prevents repeating the same mistakes.
        Uses recent failures more heavily (inverse exponential weighting).
        """
        if not agent.debt_reservoir or len(agent.debt_reservoir) == 0:
            return np.zeros_like(agent.debt_vector)
        
        # Inverse exponential weights: recent failures weighted more
        weights = np.array([0.5 ** i for i in range(len(agent.debt_reservoir) - 1, -1, -1)])
        weights = weights / np.sum(weights)  # Normalize
        
        backflow = np.zeros_like(agent.debt_vector)
        for i, past_debt in enumerate(agent.debt_reservoir):
            backflow += weights[i] * past_debt
        
        return backflow * 0.15  # Scale: 15% contribution from history
    
    def _update_variable_interest_rate(self, agent: SearchAgent, improved: bool, iteration: int) -> None:
        """
        SOTA Variable Interest Rate: Repayment force grows exponentially if stagnating.
        Philosophy: "The longer Lann ister takes to pay, the more they must overshoot."
        Forces jumps out of local optima when stuck.
        """
        if improved:
            agent.stagnation_counter = 0
            agent.interest_rate = 1.0  # Reset interest rate on improvement
        else:
            agent.stagnation_counter += 1
            # Exponential growth: rate = 1.0 * (1.1 ^ stagnation_counter)
            agent.interest_rate = 1.0 * (1.1 ** min(agent.stagnation_counter, 10))
            agent.interest_rate = min(agent.interest_rate, 3.0)  # Cap at 3x

    def _evaluate_agent(self, agent: SearchAgent, iteration: int, is_initial: bool = False) -> None:

        """Evaluate agent with accuracy-aware fitness calculation."""
        arch_dict = agent.gene.to_architecture_dict()

        # Always expose the raw gene so ProblemBasedEstimator (continuous mode)
        # can map it to the true parameter bounds without going through the NAS
        # architecture encoding.
        arch_dict['_raw_gene'] = agent.gene.gene.tolist()

        # Add iteration info for warmup clamping
        arch_dict['iter'] = iteration
        
        # During search, disable caching or add noise to cached results
        # For initial evaluation, also add warmup bias
        use_cache = self.config.cache_evaluations and not is_initial and iteration > 10
        search_mode = True  # Always in search mode during optimization
        
        # Detect stagnation for noise modulation
        stagnation_detected = self.best_agent is not None and (iteration - self.best_agent.last_improvement) > 10
        
        loss, metrics = self.estimator.estimate(
            arch_dict, 
            use_cache=use_cache,
            search_mode=search_mode,
            iteration=iteration,
            max_iterations=self.config.max_iterations,
            stagnation_detected=stagnation_detected
        )
        
        # Extract accuracy if available, otherwise convert loss to pseudo-accuracy
        accuracy = metrics.get('accuracy', 1.0 - min(loss, 1.0))
        agent.accuracy = accuracy
        
        # Store cost metrics
        agent.cost_metrics = {
            'latency': metrics['latency_ms'],
            'flops': metrics['flops_m'],
            'memory': metrics['memory_mb']
        }

        # Continuous problems are single-objective in this codebase.
        # Optimize the true objective directly (loss) and skip NAS cost shaping.
        if getattr(self.config, 'continuous_mode', False):
            agent.fitness = float(loss)
            agent.metrics = metrics
            agent.metrics['accuracy'] = accuracy

            gene_hash = agent.gene.get_hash()
            if gene_hash in self.seen_hashes:
                agent.fitness += 1e-6
            self.seen_hashes.add(gene_hash)
            return
        
        # Dynamic normalization using population statistics
        if not is_initial and iteration > 5:
            lat_norm = self._normalize_cost(metrics['latency_ms'], 'latency')
            flop_norm = self._normalize_cost(metrics['flops_m'], 'flops')
            mem_norm = self._normalize_cost(metrics['memory_mb'], 'memory')
        else:
            # Initial normalization using constraints
            lat_norm = metrics['latency_ms'] / max(self.config.latency_constraint, 1.0)
            flop_norm = metrics['flops_m'] / max(self.config.flops_constraint, 1.0)
            mem_norm = metrics['memory_mb'] / max(self.config.memory_constraint, 1.0)
        
        # Combined cost term
        total_cost = (lat_norm + flop_norm + mem_norm) / 3.0
        
        # Constraint penalty
        penalty = self.constraint_handler.compute_adaptive_penalty(metrics, iteration, self.config.max_iterations)
        
        # FIX 1: Use accuracy-aware fitness by default
        # Only use loss-based fitness if explicitly configured
        if self.config.w_loss > 0:
            # Legacy mode: loss-based fitness (causes flat accuracy)
            loss_component = self.config.w_loss * loss
            cost_component = (
                self.config.w_latency * lat_norm +
                self.config.w_memory * mem_norm +
                self.config.w_flops * flop_norm
            )
            agent.fitness = loss_component + cost_component + penalty
        else:
            # Accuracy-aware fitness (recommended)
            agent.fitness = (
                self.config.w_accuracy * (1.0 - accuracy) +  # Lower is better for fitness
                self.config.w_cost * total_cost +
                self.config.w_penalty * penalty
            )
        
        agent.metrics = metrics
        agent.metrics['accuracy'] = accuracy  # Ensure accuracy is in metrics
        
        # Pareto dominance is only used in explicit pareto-assisted mode.
        # In scalar mode, this extra O(N) scan per evaluation adds overhead and
        # can flatten useful fitness differences.
        if self.config.fitness_mode == 'pareto_assisted' and not getattr(self.config, 'continuous_mode', False):
            self._compute_pareto_rank(agent)
        
        # Pareto rank if enabled
        if self.config.fitness_mode == 'pareto_assisted':
            # Blend Pareto rank into fitness (normalized)
            if agent.pareto_rank > 0:
                normalized_rank = agent.pareto_rank / max(1, len(self.pareto_archive))
                agent.fitness += self.config.pareto_weight * normalized_rank
        
        # Keep scalar fitness surface clean; avoid extra regret shaping in default mode.
        if self.config.fitness_mode == 'pareto_assisted' and self.best_fitness < float('inf'):
            agent.fitness += 0.01 * max(0, agent.fitness - self.best_fitness)
        
        # B. Penalize architecture clones
        gene_hash = agent.gene.get_hash()
        if gene_hash in self.seen_hashes:
            agent.fitness += 0.02  # Penalize clones to encourage diversity
        self.seen_hashes.add(gene_hash)

    def _normalize_cost(self, value: float, cost_type: str) -> float:
        """Normalize cost using population statistics."""
        min_val, max_val = self.population_stats.get(f'{cost_type}_range', (0.0, 1.0))
        if max_val - min_val > 1e-6:
            return (value - min_val) / (max_val - min_val)
        return value / max(1.0, max_val)

    def _update_population_stats(self) -> None:
        """Update population statistics for dynamic normalization."""
        all_agents = [a for island in self.islands for a in island]
        
        if len(all_agents) > 5:  # Wait until we have enough samples
            latencies = [a.cost_metrics['latency'] for a in all_agents]
            flops_list = [a.cost_metrics['flops'] for a in all_agents]
            memories = [a.cost_metrics['memory'] for a in all_agents]
            fitnesses = [a.fitness for a in all_agents]
            
            self.population_stats['latency_range'] = (min(latencies), max(latencies))
            self.population_stats['flops_range'] = (min(flops_list), max(flops_list))
            self.population_stats['memory_range'] = (min(memories), max(memories))
            self.population_stats['fitness_std'] = np.std(fitnesses) + 1e-8

    def _compute_pareto_rank(self, agent: SearchAgent) -> None:
        """Compute Pareto dominance rank for an agent."""
        all_agents = [a for island in self.islands for a in island]
        dominated_count = 0
        
        for other in all_agents:
            if other is agent:
                continue
            if other.is_dominated(agent):
                dominated_count += 1
        
        agent.pareto_rank = dominated_count

    def _update_pareto_archive(self, agent: SearchAgent) -> None:
        """Update Pareto archive with non-dominated solutions."""
        # Remove dominated solutions
        self.pareto_archive = [a for a in self.pareto_archive if not agent.is_dominated(a)]
        
        # Add if not dominated by any in archive
        if not any(a.is_dominated(agent) for a in self.pareto_archive):
            self.pareto_archive.append(agent)
        
        # Limit archive size
        if len(self.pareto_archive) > self.config.pareto_archive_size:
            # Remove solutions with worst combined metrics
            self.pareto_archive.sort(key=lambda a: a.fitness)
            self.pareto_archive = self.pareto_archive[:self.config.pareto_archive_size]

    def _debt_memory_schedule(self, iteration: int) -> float:
        start = self.config.debt_persistence_start
        end = self.config.debt_persistence_end
        ratio = min(1.0, max(0.0, iteration / max(1, self.config.max_iterations)))

        if self.config.debt_persistence_decay == 'exponential':
            value = start * ((end / max(start, 1e-6)) ** ratio)
        elif self.config.debt_persistence_decay == 'cosine':
            value = end + (start - end) * 0.5 * (1 + np.cos(np.pi * ratio))
        else:
            value = start + (end - start) * ratio

        return float(np.clip(value, self.config.min_debt_memory, self.config.max_debt_memory))

    def _get_adaptive_parameters(self, iteration: int) -> Dict[str, float]:
        """Get DPO parameters with exploration phase control."""
        t = iteration / max(1, self.config.max_iterations)
        
        # FIX: Power-law decay for slower parameter reduction
        decay = (1 - t) ** self.config.decay_power

        # Continuous mode: smooth schedule without phase discontinuities
        if getattr(self.config, 'continuous_mode', False):
            alpha = self.config.alpha_0 * decay
            beta = self.config.beta_0
            gamma = self.config.gamma_0
            delta = self.config.delta_0 * (0.5 + 0.5 * t)  # Increasing global pull
            # Stagnation boost: increase exploration when stuck
            if self.config.adaptive_alpha and len(self.improvement_tracker) > 5:
                recent_avg = sum(list(self.improvement_tracker)[-5:]) / 5.0
                if recent_avg < 1e-8:
                    alpha = min(alpha * 2.0, self.config.alpha_0 * 1.5)
            return {
                'alpha': float(np.clip(alpha, 0.02, 0.5)),
                'beta': float(np.clip(beta, 0.3, 2.5)),
                'gamma': float(np.clip(gamma, 0.0, 2.0)),
                'delta': float(np.clip(delta, 0.05, 0.8)),
                'debt_memory': self._debt_memory_schedule(iteration),
            }

        # Phase-based parameter adjustment
        if t < self.config.exploration_phase_ratio:
            # Early phase: favor debt accumulation and exploration
            alpha = self.config.alpha_0 * 1.2  # Slightly higher exploration
            beta = self.config.beta_0 * 0.8    # Lower repayment
            gamma = self.config.gamma_0 * 0.9  # Lower overshoot
            delta = self.config.delta_0 * 0.8  # Lower global pull
        elif t < self.config.debt_accumulation_phase:
            # Middle phase: balance accumulation and repayment
            alpha = self.config.alpha_0 * decay
            beta = self.config.beta_0 + 0.3 * t
            gamma = self.config.gamma_0 * (1 - 0.3 * t)
            delta = self.config.delta_0 * (1 - 0.2 * t)
        else:
            # CONVERGENCE MODE: Late phase - aggressive repayment and refinement (>60%)
            alpha = self.config.alpha_0 * decay * 0.4  # CONVERGENCE FIX: Tighter alpha (0.7 → 0.4)
            beta = self.config.beta_0 + 1.2 * t         # CONVERGENCE FIX: Massive repayment force increase
            # CONVERGENCE FIX: Force gamma to strictly zero (not 0.2x at end anymore)
            gamma = float(np.clip(self.config.gamma_0 * (1.0 - 1.1 * t), 0.0, 1.8))
            # CONVERGENCE FIX: Increase global pull strength late phase (accelerate toward best)
            delta = self.config.delta_0 * (1 + 0.5 * t)  # Changed from (1 - 0.1 * t)
        
        # Adaptive adjustment based on improvement rate
        if self.config.adaptive_alpha and len(self.improvement_tracker) > 5:
            recent_avg = sum(list(self.improvement_tracker)[-5:]) / 5.0
            if recent_avg < 1e-6:  # Stagnation
                alpha *= 1.3  # Increase exploration
                beta *= 0.9   # Reduce repayment
            elif recent_avg > 0.01:  # Rapid improvement
                alpha *= 0.9  # Reduce noise
                beta *= 1.1   # Increase repayment
        
        return {
            'alpha': float(np.clip(alpha, 0.05, 0.3)),
            'beta': float(np.clip(beta, 0.6, 2.8)),   # Raised cap: late-phase formula reaches ~2.9
            'gamma': float(np.clip(gamma, 0.2, 1.8)),  # Allow lower gamma but keep >0 for overshoot
            'delta': float(np.clip(delta, 0.05, 0.65)), # Raised cap: late delta_0*(1+0.5*t) needs room
            'debt_memory': self._debt_memory_schedule(iteration),
        }

    def _update_adaptive_controls(self, iteration: int, acceptance_rate: float, avg_diversity: float) -> None:
        """Adjust acceptance scaling and elite ratio without changing DPO semantics."""
        if self.config.adaptive_acceptance_scaling:
            delta = self.target_acceptance_rate - acceptance_rate
            updated = self.config.acceptance_scaling_factor - 0.5 * delta
            low, high = self.acceptance_scale_bounds
            self.config.acceptance_scaling_factor = float(np.clip(updated, low, high))

        # Dynamic elite ratio: lower when diversity is low, higher late when stable
        elite = self.config.elite_ratio
        if avg_diversity < self.config.min_diversity_threshold:
            elite *= 0.7
        elif iteration > 0.6 * self.config.max_iterations:
            elite *= 1.1
        self._effective_elite_ratio = float(np.clip(elite, 0.05, 0.25))

        stagnated = self._last_improvement <= self.config.convergence_threshold
        self._component_flags["debt"] = stagnated or iteration > 0.5 * self.config.max_iterations
        self._component_flags["acceptance"] = acceptance_rate < 0.35 or stagnated
        self._component_flags["diversity"] = avg_diversity < self.config.min_diversity_threshold

    def _clip_gene_array(self, gene_arr: np.ndarray, num_layers: int, num_cells: int) -> None:
        """Helper to enforce bounds in-place on numpy array"""
        # In continuous mode all positions live in [0, 1]; skip NAS-specific struct
        if getattr(self.config, 'continuous_mode', False):
            np.clip(gene_arr, 0.0, 1.0, out=gene_arr)
            return
        # Operations
        np.clip(gene_arr[:num_layers], 0, self._ops_len, out=gene_arr[:num_layers])
        # Kernels
        np.clip(gene_arr[num_layers:2*num_layers], 0, self._kers_len, out=gene_arr[num_layers:2*num_layers])
        # Skip connections
        np.clip(gene_arr[2*num_layers:2*num_layers+num_cells], 0, 1, out=gene_arr[2*num_layers:2*num_layers+num_cells])
        # Multipliers
        np.clip(gene_arr[-2:], 0.3, 2.0, out=gene_arr[-2:])

    def _uniform_crossover(self, gene_a: np.ndarray, gene_b: np.ndarray) -> np.ndarray:
        mask = np.random.rand(len(gene_a)) < 0.5
        return np.where(mask, gene_a, gene_b)

    def _inject_elite_offspring(self, island_idx: int, iteration: int) -> None:
        island = self.islands[island_idx]
        if len(island) < 3:
            return

        island.sort(key=lambda a: a.fitness)
        elite_count = max(2, int(len(island) * self._effective_elite_ratio))
        elites = island[:elite_count]
        num_offspring = max(1, len(island) // 6)

        for _ in range(num_offspring):
            parent_a, parent_b = np.random.choice(elites, size=2, replace=False)
            if np.random.random() < self.config.beta_0:
                child_gene_arr = self._uniform_crossover(parent_a.gene.gene, parent_b.gene.gene)
            else:
                child_gene_arr = parent_a.gene.gene.copy()

            mutation_strength = max(parent_a.mutation_magnitude, parent_b.mutation_magnitude)
            mutation_strength = min(0.35, mutation_strength * 1.1)
            child_gene_arr = child_gene_arr + np.random.randn(parent_a.gene.D) * mutation_strength
            self._clip_gene_array(child_gene_arr, parent_a.gene.num_layers, parent_a.gene.num_cells)

            new_gene = ArchitectureGene(
                parent_a.gene.num_layers,
                parent_a.gene.num_cells,
                continuous_mode=parent_a.gene.continuous_mode,
            )
            new_gene.gene = child_gene_arr

            if parent_a.debt_vector is not None and parent_b.debt_vector is not None:
                debt = 0.5 * (parent_a.debt_vector + parent_b.debt_vector)
            else:
                debt = np.zeros_like(child_gene_arr)

            new_agent = SearchAgent(
                gene=new_gene,
                island_id=island_idx,
                debt_vector=debt,
                mutation_magnitude=mutation_strength,
                acceptance_rate=(parent_a.acceptance_rate + parent_b.acceptance_rate) * 0.5,
            )
            self._evaluate_agent(new_agent, iteration)

            worst = max(island, key=lambda a: a.fitness)
            if new_agent.fitness < worst.fitness:
                island[island.index(worst)] = new_agent

    def _accept_candidate(self, current: SearchAgent, candidate: SearchAgent) -> bool:
        """SOTA acceptance: debt-aware, phase-dependent, with ruthless late-phase greedy (65%+)."""
        self.acceptance_stats['total_candidates'] += 1
        
        t = self.history['iterations'][-1] / self.config.max_iterations if self.history['iterations'] else 0.0
        
        # Always accept better candidates
        if candidate.fitness < current.fitness:
            self.acceptance_stats['accepted_better'] += 1
            current.success_streak += 1
            return True
        
        # Late greedy phase only near the end.
        if t > 0.80:
            self.acceptance_stats['rejected'] += 1
            current.success_streak = 0
            return False
        
        # Ablation check: disable worse-move acceptance (DPO-NoAccept variant)
        if not self.config.enable_worse_acceptance:
            self.acceptance_stats['rejected'] += 1
            current.success_streak = 0
            return False
        
        if not self._component_flags["acceptance"]:
            self.acceptance_stats['rejected'] += 1
            current.success_streak = 0
            return False
        
        # Worse candidate: consider with debt-aware probability
        delta_fitness = candidate.fitness - current.fitness
        
        # Track delta
        self._recent_fitness_deltas.append(delta_fitness)
        
        if delta_fitness > 0 and self.temperature > 0:
            # Guardrail: avoid accepting candidates that significantly hurt
            # accuracy, especially after early exploration.
            if (candidate.accuracy + 0.002) < current.accuracy and t > 0.25:
                self.acceptance_stats['rejected'] += 1
                current.success_streak = 0
                return False

            # DEBT-AWARE ACCEPTANCE: Higher debt = more likely to accept worse moves
            debt_factor = 1.0
            if current.debt_vector is not None:
                debt_norm = np.linalg.norm(current.debt_vector)
                debt_factor = 1.0 + min(2.0, debt_norm * 5.0)  # Up to 3x more likely with high debt
            
            # VARIABLE INTEREST RATE: If stagnating, apply higher repayment force
            interest_factor = current.interest_rate if hasattr(current, 'interest_rate') else 1.0
            
            # PHASE-AWARE ACCEPTANCE: More accepting early, less late
            phase_factor = 1.4 - t  # 1.4 early → 0.4 late
            
            # DIVERSITY-AWARE ACCEPTANCE: Accept diverse candidates
            diversity_factor = 1.0 + current.diversity_score * 0.5
            
            # Calculate acceptance probability
            if self.config.adaptive_acceptance_scaling and len(self._recent_fitness_deltas) > 10:
                delta_std = np.std(list(self._recent_fitness_deltas)) + 1e-8
                scaled_delta = (delta_fitness / delta_std) * self.config.acceptance_scaling_factor
            else:
                scaled_delta = delta_fitness * self.config.acceptance_scaling_factor
            
            # Apply all factors
            scaled_delta = scaled_delta / (debt_factor * phase_factor * diversity_factor * interest_factor)
            if t > 0.5:
                # Earlier increase of exploitation pressure
                late_exploit = 1.0 + 1.8 * ((t - 0.5) / 0.5)  # Increased from 1.5
                scaled_delta *= late_exploit
            scaled_delta = min(scaled_delta, 10.0)  # Cap to avoid underflow
            
            # Calculate probability
            probability = math.exp(-scaled_delta / max(self.temperature, 1e-8))
            
            # Phase-dependent minimum acceptance probability
            if t < 0.3:
                min_probability = 0.04
            elif t < 0.7:
                min_probability = 0.02
            else:
                min_probability = 0.005
            probability = max(probability, min_probability)
            
            accept = np.random.random() < probability
            
            if accept:
                self.acceptance_stats['accepted_worse'] += 1
                # Update agent's acceptance rate with smoothing
                current.acceptance_rate = 0.95 * current.acceptance_rate + 0.05 * probability
                # Soft clamp acceptance rate
                current.acceptance_rate = np.clip(current.acceptance_rate, 0.2, 0.6)
            else:
                self.acceptance_stats['rejected'] += 1
                current.acceptance_rate = 0.95 * current.acceptance_rate + 0.05 * 0.0
                # Soft clamp acceptance rate
                current.acceptance_rate = np.clip(current.acceptance_rate, 0.2, 0.6)
            
            return accept
        
        self.acceptance_stats['rejected'] += 1
        return False

    # =========================================================================
    # CLEAN CONTINUOUS-MODE DPO — Faithful 4-step debt philosophy
    # =========================================================================

    def _dpo_step_continuous(self, agent: SearchAgent, params: Dict, iteration: int) -> SearchAgent:
        """
        Clean DPO step for continuous optimisation.
        Faithfully implements the core 4-step debt philosophy:

          1. Debt Move      — α·random perturbation (exploration)
          2. Repayment      — β·reverse accumulated debt direction
          3. Double Pay     — γ·overshoot past the repayment point
          4. Smart Learning — δ·pull toward global best

        Uses 1 evaluation per step plus 1 conditional evaluation when an
        improvement is found and repayable debt exists.
        """
        current_vec = agent.gene.gene.copy()
        t = iteration / max(1, self.config.max_iterations)
        alpha = params['alpha']
        beta = params['beta']
        gamma = params['gamma']
        delta = params['delta']
        debt_memory = params['debt_memory']

        # ══════════ STEP 1: DEBT MOVE (exploration perturbation) ══════════
        perturbation = np.random.randn(agent.gene.D) * alpha

        # Debt-guided exploration: bias away from known bad directions
        debt_vec = agent.debt_vector if agent.debt_vector is not None else np.zeros(agent.gene.D)
        debt_norm = np.linalg.norm(debt_vec)
        if debt_norm > 0.01:
            debt_dir = debt_vec / debt_norm
            perturbation -= 0.25 * alpha * debt_dir  # Avoid past bad directions

        # Light best-guidance: small pull toward global best for directed search
        if self.best_agent is not None and self.best_agent is not agent:
            diff = self.best_agent.gene.gene - current_vec
            perturbation += 0.1 * delta * diff  # Very light exploitation hint

        candidate_vec = current_vec + perturbation
        self._clip_gene_array(candidate_vec, agent.gene.num_layers, agent.gene.num_cells)

        # Create and evaluate candidate
        candidate_gene = ArchitectureGene(
            agent.gene.num_layers,
            agent.gene.num_cells,
            continuous_mode=True,
        )
        candidate_gene.gene = candidate_vec
        candidate = SearchAgent(
            gene=candidate_gene,
            island_id=agent.island_id,
            debt_vector=agent.debt_vector.copy() if agent.debt_vector is not None else np.zeros(agent.gene.D),
            mutation_magnitude=agent.mutation_magnitude,
            acceptance_rate=agent.acceptance_rate,
        )
        self._evaluate_agent(candidate, iteration)
        self.acceptance_stats['total_candidates'] += 1

        # ══════════ STEP 2: ACCEPT / REJECT ══════════
        improved = candidate.fitness < agent.fitness

        if improved:
            accepted = True
            self.acceptance_stats['accepted_better'] += 1
        elif not self.config.enable_worse_acceptance:
            accepted = False
            self.acceptance_stats['rejected'] += 1
        elif t > 0.85:
            # Late phase: greedy — only accept improvements
            accepted = False
            self.acceptance_stats['rejected'] += 1
        else:
            # SA-style probabilistic acceptance for worse moves
            # Normalize delta by fitness scale for scale-invariant acceptance
            delta_f = candidate.fitness - agent.fitness
            self._recent_fitness_deltas.append(delta_f)
            if len(self._recent_fitness_deltas) > 10:
                delta_std = np.std(list(self._recent_fitness_deltas)) + 1e-8
                normalized_delta = delta_f / delta_std
            else:
                fitness_std = self.population_stats.get('fitness_std', 1.0)
                normalized_delta = delta_f / max(fitness_std, 1e-8)
            prob = math.exp(-min(normalized_delta / max(self.temperature, 1e-8), 10.0))
            accepted = np.random.random() < prob
            if accepted:
                self.acceptance_stats['accepted_worse'] += 1
            else:
                self.acceptance_stats['rejected'] += 1

        if not accepted:
            agent.age += 1
            return agent

        # ══════════ STEPS 3+4: REPAY + DOUBLE PAY + SMART LEARNING ══════════
        if improved and self.config.enable_debt_repayment:
            debt_vec = agent.debt_vector if agent.debt_vector is not None else np.zeros(agent.gene.D)
            debt_norm = np.linalg.norm(debt_vec)

            if debt_norm > 0.01:
                # Streak-boosted repayment: consecutive improvements increase force
                streak = getattr(agent, 'success_streak', 0)
                streak_factor = min(1.5, 1.0 + 0.1 * streak)

                # REPAY: move opposite to accumulated debt
                repay_vec = candidate_vec - beta * streak_factor * debt_vec

                # DOUBLE PAY: overshoot further in repayment direction
                double_vec = repay_vec - gamma * streak_factor * debt_vec

                # SMART LEARNING: pull toward global best
                if self.best_agent is not None:
                    best_vec = self.best_agent.gene.gene
                    final_vec = double_vec + delta * (best_vec - double_vec)
                else:
                    final_vec = double_vec

                self._clip_gene_array(final_vec, agent.gene.num_layers, agent.gene.num_cells)

                # Evaluate the repaid position (2nd evaluation this step)
                final_gene = ArchitectureGene(
                    agent.gene.num_layers,
                    agent.gene.num_cells,
                    continuous_mode=True,
                )
                final_gene.gene = final_vec
                final_agent = SearchAgent(
                    gene=final_gene,
                    island_id=agent.island_id,
                    debt_vector=debt_vec.copy(),
                    mutation_magnitude=agent.mutation_magnitude,
                    acceptance_rate=agent.acceptance_rate,
                )
                self._evaluate_agent(final_agent, iteration)

                if final_agent.fitness <= candidate.fitness:
                    # Repayment succeeded — large debt reduction
                    final_agent.debt_vector = debt_vec * debt_memory * 0.3
                    final_agent.last_improvement = iteration
                    final_agent.improvements = agent.improvements + 1
                    final_agent.success_streak = getattr(agent, 'success_streak', 0) + 1
                    if self.config.adaptive_mutation:
                        final_agent.update_mutation_magnitude(improvement=True)
                    return final_agent
                else:
                    # Repayment did not improve — keep candidate, moderate debt reduction
                    candidate.debt_vector = debt_vec * debt_memory
                    candidate.last_improvement = iteration
                    candidate.improvements = agent.improvements + 1
                    candidate.success_streak = getattr(agent, 'success_streak', 0) + 1
                    if self.config.adaptive_mutation:
                        candidate.update_mutation_magnitude(improvement=True)
                    return candidate
            else:
                # Negligible debt — no repayment needed
                candidate.debt_vector = np.zeros(agent.gene.D)
                candidate.last_improvement = iteration
                candidate.improvements = agent.improvements + 1
                candidate.success_streak = getattr(agent, 'success_streak', 0) + 1
                if self.config.adaptive_mutation:
                    candidate.update_mutation_magnitude(improvement=True)
                return candidate

        elif improved:
            # Improvement found but debt repayment disabled (ablation variant)
            candidate.debt_vector = (agent.debt_vector * debt_memory
                                     if agent.debt_vector is not None
                                     else np.zeros(agent.gene.D))
            candidate.last_improvement = iteration
            candidate.improvements = agent.improvements + 1
            candidate.success_streak = getattr(agent, 'success_streak', 0) + 1
            if self.config.adaptive_mutation:
                candidate.update_mutation_magnitude(improvement=True)
            return candidate

        else:
            # Worse but accepted — accumulate debt (track the bad move)
            if self.config.enable_debt_accumulation:
                worse_move = candidate_vec - current_vec
                old_debt = agent.debt_vector if agent.debt_vector is not None else np.zeros(agent.gene.D)
                candidate.debt_vector = debt_memory * old_debt + (1.0 - debt_memory) * worse_move
            else:
                candidate.debt_vector = np.zeros(agent.gene.D)
            candidate.success_streak = 0  # Reset streak on worse move
            if self.config.adaptive_mutation:
                candidate.update_mutation_magnitude(improvement=False)
            return candidate

    def _dpo_step(self, agent: SearchAgent, params: Dict, iteration: int) -> SearchAgent:
        """
        SOTA DPO step with simplified momentum-based debt and objective awareness.
        Lannister Philosophy: Debt accumulation → Delayed Repayment → Momentum Overshoot → Global Pull.
        
        CONVERGENCE FIXES:
        - Curvature-adaptive repayment based on fitness improvements
        - Nesterov second-order momentum
        - Debt forgiveness for micro-debts in late phase
        - Mutation reduction in convergence mode
        """
        # Continuous mode: use clean 4-step DPO implementation
        if getattr(self.config, 'continuous_mode', False):
            return self._dpo_step_continuous(agent, params, iteration)

        current_vec = agent.gene.gene
        t = iteration / max(1, self.config.max_iterations)
        
        # ====== 0. UPDATE VARIABLE INTEREST RATE ======
        # Increases exponentially if agent stagnating (non-Markovian memory effect)
        previous_improved = agent.age - agent.last_improvement == 0
        self._update_variable_interest_rate(agent, previous_improved, iteration)
        
        # ====== 1. SIMPLIFIED MOMENTUM-BASED DEBT ACCUMULATION ======
        # Momentum velocity tracks successful repayment direction
        base_alpha = params['alpha']
        
        # CONVERGENCE MODE: Reduce mutation in late exploitation phase (>75%)
        if t > 0.75:
            base_alpha *= 0.4  # Heavy reduction to stabilize convergence
        
        # SOTA: Apply Variable Interest Rate to exploration (higher rate = more aggressive)
        interest_boost = 1.0 + (agent.interest_rate - 1.0) * 0.3
        base_alpha *= interest_boost
        
        # Debt-aware exploration: high debt = more aggressive mutations
        debt_norm = np.linalg.norm(agent.debt_vector) if agent.debt_vector is not None else 0.0
        if debt_norm > 0.15:
            base_alpha *= 1.3
        
        # Stagnation awareness: boost exploration
        stagnation = iteration - agent.last_improvement
        if stagnation > self.config.stagnation_threshold:
            base_alpha *= 1.2
        
        # Base exploration perturbation
        perturbation = np.random.randn(agent.gene.D) * base_alpha
        
        # SOTA: Add Debt Backflow (non-Markovian memory of failed directions)
        backflow = self._apply_debt_backflow(agent, params)
        perturbation -= backflow  # Negative: avoid past failures
        
        # CONVERGENCE FIX: Nesterov-style second-order momentum
        # Captures curvature information for better convergence
        move_vec_placeholder = perturbation.copy()  # Will be updated after evaluation
        agent.momentum_velocity = (
            0.85 * agent.momentum_velocity +
            0.15 * move_vec_placeholder +
            0.05 * (move_vec_placeholder - agent.prev_move_vec)
        )
        
        # Add upgraded momentum velocity
        momentum_factor = 0.5
        perturbation += momentum_factor * agent.momentum_velocity
        
        # ====== 2. BEST EXPLOITATION WITH DIRECTIONAL DEBT ======
        # Pull toward best solution found
        best_local = self.best_per_island[agent.island_id] or self.best_agent
        if best_local is not None and best_local is not agent:
            direction = best_local.gene.gene - current_vec
            dir_norm = np.linalg.norm(direction)
            if dir_norm > 1e-8:
                # Scale with debt level (high debt = stronger pull)
                exploit_scale = 0.1 + 0.2 * min(1.0, debt_norm / 0.3)
                perturbation += (direction / dir_norm) * exploit_scale
        
        # ====== 3. HYBRID DE-STYLE DIFFERENTIAL EVOLUTION FOR STRONGER EXPLOITATION ======
        island = self.islands[agent.island_id]
        if len(island) >= 3:
            candidates = [a for a in island if a is not agent]
            if len(candidates) >= 2:
                a, b = np.random.choice(candidates, size=2, replace=False)
                f_scale = 0.1
                perturbation += f_scale * (a.gene.gene - b.gene.gene)
        
        # ====== 4. OPPOSITION-BASED LEARNING ======
        # If stagnating significantly, try opposition point
        if stagnation > self.config.stagnation_threshold * 1.5 and np.random.random() < 0.15:
            opposition = self._apply_opposition_based_learning(current_vec, (0.0, 1.0))
            perturbation = opposition - current_vec
        
        # Create candidate gene
        debt_vec = current_vec + perturbation
        self._clip_gene_array(debt_vec, agent.gene.num_layers, agent.gene.num_cells)
        
        # Create candidate
        smart_gene = ArchitectureGene(
            agent.gene.num_layers,
            agent.gene.num_cells,
            continuous_mode=agent.gene.continuous_mode,
        )
        smart_gene.gene = debt_vec
        candidate = SearchAgent(
            gene=smart_gene,
            island_id=agent.island_id,
            debt_vector=agent.debt_vector.copy() if agent.debt_vector is not None else None,
            mutation_magnitude=agent.mutation_magnitude,
            acceptance_rate=agent.acceptance_rate
        )
        
        # Evaluate candidate
        self._evaluate_agent(candidate, iteration)
        
        # ====== 5. ACCEPTANCE AND DEBT UPDATE ======
        
        if self._accept_candidate(agent, candidate):
            if candidate.fitness < agent.fitness:
                # ===== ADAPTIVE LOCAL CONTRACTION: fine-grained exploitation near optimum =====
                # Inspired by JADE/CMA-ES contraction: when close to best, shrink step size
                # to converge precisely instead of overshooting past the optimum region.
                if self.best_agent is not None:
                    distance_to_best = np.linalg.norm(
                        self.best_agent.gene.gene - candidate.gene.gene
                    )
                    # Skip local contraction refinement in continuous mode:
                    # the extra evaluation inflates total function calls vs baselines.
                    if distance_to_best < 0.15 and not getattr(self.config, 'continuous_mode', False):
                        # Contraction strength decays to zero as iterations progress
                        contraction_strength = 0.05 * (1.0 - iteration / self.config.max_iterations)
                        local_refine = candidate.gene.gene + contraction_strength * (
                            self.best_agent.gene.gene - candidate.gene.gene
                        )
                        self._clip_gene_array(
                            local_refine, agent.gene.num_layers, agent.gene.num_cells
                        )
                        refine_gene = ArchitectureGene(
                            agent.gene.num_layers,
                            agent.gene.num_cells,
                            continuous_mode=agent.gene.continuous_mode,
                        )
                        refine_gene.gene = local_refine
                        refined_agent = SearchAgent(
                            gene=refine_gene,
                            island_id=agent.island_id,
                            debt_vector=(
                                np.zeros_like(agent.debt_vector)
                                if agent.debt_vector is not None
                                else None
                            ),
                        )
                        self._evaluate_agent(refined_agent, iteration)
                        if refined_agent.fitness < candidate.fitness:
                            candidate = refined_agent

                # ===== IMPROVEMENT: Winning Streak + Objective-Aware Debt =====
                agent.improvements += 1
                agent.last_improvement = iteration
                agent.success_streak += 1
                
                # Compute objective-aware debt
                accuracy_debt, cost_debt = self._compute_objective_aware_debt(agent, candidate)
                
                # Update objective debts
                agent.accuracy_debt = 0.8 * agent.accuracy_debt + 0.2 * accuracy_debt
                agent.cost_debt = 0.8 * agent.cost_debt + 0.2 * cost_debt
                
                # Update momentum velocity for successful move
                move_vec = candidate.gene.gene - agent.gene.gene
                
                # CONVERGENCE FIX: Nesterov second-order momentum update
                agent.momentum_velocity = (
                    0.85 * agent.momentum_velocity +
                    0.15 * move_vec +
                    0.05 * (move_vec - agent.prev_move_vec)
                )
                agent.prev_move_vec = move_vec.copy()  # Store for next iteration
                
                # ===== CONVERGENCE FIX: CURVATURE-ADAPTIVE REPAYMENT SCALING =====
                # If fitness improves significantly, boost repayment force
                fitness_gain = agent.fitness - candidate.fitness
                if fitness_gain > 0:
                    # Exponential boost: 1.0 + up to 2.5x multiplier
                    curvature_boost = min(2.5, 1.0 + fitness_gain * 8.0)
                else:
                    curvature_boost = 1.0
                
                effective_beta = params['beta'] * curvature_boost
                effective_gamma = params['gamma'] * curvature_boost

                # ===== CONTINUOUS MODE: Lazy repayment — no extra evaluation =====
                # The overshoot step on a NAS gene encodes a valid discrete arch.
                # In continuous mode the extra _evaluate_agent call burns budget
                # (double-counting vs baselines) with no benefit.  Instead we:
                #   (a) compute a clipped repayment direction that cannot
                #       overshoot past the best point found so far, and
                #   (b) store it as a directional bias in candidate.debt_vector
                #       so the *next* _dpo_step adds it cheaply as momentum
                #       (lazy / deferred repayment).
                if getattr(self.config, 'continuous_mode', False):
                    debt_vec = agent.debt_vector if agent.debt_vector is not None else np.zeros_like(candidate.gene.gene)
                    # Repayment direction (scaled, streak-aware, no gamma overshoot)
                    streak_mult = 2.0 if agent.success_streak >= 3 else 1.0
                    repay_vec = candidate.gene.gene - (effective_beta * streak_mult * debt_vec)

                    # Clip to not overshoot past best along each dimension
                    # (CMA-ES-style: honour the trust region around the best)
                    if self.best_agent is not None:
                        best_vec = self.best_agent.gene.gene
                        cand_vec = candidate.gene.gene
                        # If repay_vec overshoots best (crosses best from cand side), clamp to best
                        overshot = (repay_vec - best_vec) * (cand_vec - best_vec) < 0
                        repay_vec = np.where(overshot, best_vec, repay_vec)

                    self._clip_gene_array(repay_vec, agent.gene.num_layers, agent.gene.num_cells)

                    # Deferred bias: becomes the exploration perturbation anchor next step
                    repayment_bias = repay_vec - candidate.gene.gene
                    candidate.debt_vector = (
                        params['debt_memory'] * debt_vec
                        + (1.0 - params['debt_memory']) * repayment_bias
                    )

                    # Propagate trajectory metadata
                    candidate.success_streak = agent.success_streak
                    candidate.momentum_velocity = agent.momentum_velocity.copy()
                    candidate.prev_move_vec = agent.prev_move_vec.copy()

                    if self.config.adaptive_mutation:
                        candidate.update_mutation_magnitude(improvement=True)
                    return candidate

                # ===== NAS / DISCRETE MODE: Full overshoot + extra evaluation =====
                # Ablation check: disable debt repayment (DPO-NoRepay variant)
                if not self.config.enable_debt_repayment:
                    # Skip repayment, just use candidate as-is
                    final_agent = candidate
                    if self.config.adaptive_mutation:
                        final_agent.update_mutation_magnitude(improvement=True)
                    return final_agent
                
                # SOTA: Winning Streak Overshoot
                # If 3+ consecutive improvements, apply aggressive repayment
                if agent.success_streak >= 3:
                    # Double overshoot: repay debt twice
                    repay_vec = candidate.gene.gene - (effective_beta * 2.0 * agent.debt_vector)
                    overshoot = repay_vec - (effective_gamma * 1.5 * agent.debt_vector)
                else:
                    # Normal repayment with curvature adaptation
                    repay_vec = candidate.gene.gene - (effective_beta * agent.debt_vector)
                    overshoot = repay_vec - (effective_gamma * agent.debt_vector)
                
                # Global best attraction (anchor)
                if self.best_agent is not None:
                    delta_best = self.best_agent.gene.gene - overshoot
                    global_pull = params['delta'] * delta_best
                    final_vec = overshoot + global_pull
                else:
                    final_vec = overshoot
                
                # Clip and create final agent
                self._clip_gene_array(final_vec, agent.gene.num_layers, agent.gene.num_cells)

                # ===== CONTINUOUS MODE: LAZY REPAYMENT — no extra evaluation =====
                # In continuous single-objective mode evaluating final_agent burns
                # extra function calls that make DPO appear 2-3× slower than
                # baselines on equal budgets.  Instead:
                #   1. Clamp the overshoot so it never goes past the running best
                #      (prevents jumping over Rosenbrock valleys / Rastrigin basins).
                #   2. Encode the clamped direction as candidate's debt_vector so
                #      the NEXT step still gets the repayment pull (lazy repayment).
                if getattr(self.config, 'continuous_mode', False):
                    if self.best_agent is not None:
                        best_vec  = self.best_agent.gene.gene
                        diff      = final_vec - best_vec
                        cand_dist = np.linalg.norm(candidate.gene.gene - best_vec)
                        over_dist = np.linalg.norm(diff)
                        # Clamp: don't move further from best than the accepted candidate
                        if over_dist > cand_dist and over_dist > 1e-9:
                            final_vec = best_vec + diff * (cand_dist / over_dist)
                            self._clip_gene_array(
                                final_vec, agent.gene.num_layers, agent.gene.num_cells
                            )
                    # Lazy repayment: store residual direction into candidate's debt
                    pending = final_vec - candidate.gene.gene
                    candidate.debt_vector = (
                        0.7 * candidate.debt_vector + 0.3 * pending
                        if candidate.debt_vector is not None
                        else pending.copy()
                    )
                    candidate.success_streak     = agent.success_streak
                    candidate.momentum_velocity  = agent.momentum_velocity.copy()
                    candidate.prev_move_vec       = agent.prev_move_vec.copy()
                    if self.config.adaptive_mutation:
                        candidate.update_mutation_magnitude(improvement=True)
                    return candidate

                # ===== NAS / DISCRETE MODE: evaluate the overshoot position =====
                final_gene = ArchitectureGene(
                    agent.gene.num_layers,
                    agent.gene.num_cells,
                    continuous_mode=agent.gene.continuous_mode,
                )
                final_gene.gene = final_vec

                # Evaluate final agent
                final_agent = SearchAgent(
                    gene=final_gene,
                    island_id=agent.island_id,
                    debt_vector=agent.debt_vector.copy() if agent.debt_vector is not None else None,
                    mutation_magnitude=agent.mutation_magnitude,
                    acceptance_rate=agent.acceptance_rate,
                )
                final_agent.success_streak = agent.success_streak
                final_agent.momentum_velocity = agent.momentum_velocity.copy()
                final_agent.prev_move_vec = agent.prev_move_vec.copy()
                self._evaluate_agent(final_agent, iteration)

                # SOTA: Jubilee on massive improvements (clear debt)
                if candidate.fitness < agent.fitness - self.population_stats['fitness_std'] * 2:
                    final_agent.debt_vector = np.zeros_like(final_agent.debt_vector)
                    final_agent.success_streak = 0

                if self.config.adaptive_mutation:
                    final_agent.update_mutation_magnitude(improvement=True)

                return final_agent
            else:
                # Worse but accepted move: accumulate debt
                agent.success_streak = 0
                
                # Ablation check: disable debt accumulation (DPO-NoDebt variant)
                if not self.config.enable_debt_accumulation:
                    # No debt accumulation, just use candidate as-is
                    candidate.success_streak = 0
                    if self.config.adaptive_mutation:
                        candidate.update_mutation_magnitude(improvement=False)
                    return candidate
                
                worse_move = candidate.gene.gene - agent.gene.gene
                
                # Simple momentum debt: debt ← λ·debt + (1-λ)·worse_move
                debt_memory = params['debt_memory']
                
                # CONVERGENCE FIX: DEBT FORGIVENESS - Clear micro-debts in late phase
                debt_norm = np.linalg.norm(agent.debt_vector) if agent.debt_vector is not None else 0.0
                if debt_norm < 0.05 and t > 0.75:
                    # Late phase + micro-debt = forgive it entirely (no oscillation)
                    agent.debt_vector = np.zeros_like(agent.debt_vector)
                else:
                    # Normal debt accumulation
                    agent.debt_vector = (debt_memory * agent.debt_vector + 
                                       (1.0 - debt_memory) * worse_move)
                
                if self.config.adaptive_mutation:
                    agent.update_mutation_magnitude(improvement=False)
                
                return candidate
        
        # Rejected: Small debt injection to prevent stagnation
        agent.success_streak = 0
        agent.age += 1
        if agent.age % 5 == 0 and agent.debt_vector is not None:
            agent.debt_vector += np.random.randn(agent.gene.D) * 0.04
        
        return agent

    def _compute_diversity_scores(self) -> None:
        """Compute diversity scores for all agents."""
        all_agents = [a for island in self.islands for a in island]
        
        if len(all_agents) < 2:
            return
        
        # Normalize gene vectors
        gene_vectors = np.array([a.gene.gene for a in all_agents])
        gene_norms = np.linalg.norm(gene_vectors, axis=1, keepdims=True)
        gene_norms[gene_norms == 0] = 1.0
        normalized_vectors = gene_vectors / gene_norms
        
        # Compute pairwise distances
        for i, agent in enumerate(all_agents):
            distances = np.linalg.norm(normalized_vectors[i] - normalized_vectors, axis=1)
            agent.diversity_score = np.mean(distances)

    def _migrate_between_islands(self, iteration: int) -> None:
        """Enhanced migration with debt diversity preservation."""
        if not self.config.island_model or len(self.islands) < 2:
            return
            
        t = iteration / self.config.max_iterations
        num_migrate = max(1, self.config.population_size // 15)  # More frequent migration
        
        for island_idx in range(len(self.islands)):
            # Find diverse candidates from other islands (not just best)
            migration_candidates = []
            for other_idx in range(len(self.islands)):
                if other_idx == island_idx:
                    continue
                    
                other_island = self.islands[other_idx]
                if not other_island:
                    continue
                    
                # Select agents based on diversity to current island
                for agent in other_island[:3]:  # Consider top 3 from each island
                    # Check if agent brings diversity
                    similarity_score = 0.0
                    for local_agent in self.islands[island_idx][:5]:  # Compare to local top 5
                        # Compare genes and debt directions
                        gene_sim = np.dot(
                            agent.gene.gene / (np.linalg.norm(agent.gene.gene) + 1e-8),
                            local_agent.gene.gene / (np.linalg.norm(local_agent.gene.gene) + 1e-8)
                        )
                        
                        # Compare debt directions if available
                        debt_sim = 0.0
                        if (agent.debt_vector is not None and local_agent.debt_vector is not None and
                            np.linalg.norm(agent.debt_vector) > 1e-6 and np.linalg.norm(local_agent.debt_vector) > 1e-6):
                            debt_sim = np.dot(
                                agent.debt_vector / (np.linalg.norm(agent.debt_vector) + 1e-8),
                                local_agent.debt_vector / (np.linalg.norm(local_agent.debt_vector) + 1e-8)
                            )
                        
                        similarity_score += 0.7 * gene_sim + 0.3 * debt_sim
                    
                    # Lower similarity = more diverse
                    diversity_score = 1.0 - (similarity_score / 5.0)  # Normalize
                    migration_candidates.append((agent, diversity_score))
            
            if not migration_candidates:
                continue
                
            # Sort by diversity (most diverse first)
            migration_candidates.sort(key=lambda x: x[1], reverse=True)
            
            # Sort current island (worst first)
            self.islands[island_idx].sort(key=lambda a: a.fitness, reverse=True)
            
            # Replace worst agents with diverse migrants
            for i in range(min(num_migrate, len(migration_candidates), len(self.islands[island_idx]))):
                migrant, diversity = migration_candidates[i]
                
                # Create migrated agent with debt inheritance
                new_gene = migrant.gene.copy()
                
                # Inherit debt with phase-dependent adjustment
                if migrant.debt_vector is not None:
                    if t < 0.5:  # Early phase: preserve debt for exploration
                        debt_inheritance = 1.0
                    else:  # Late phase: reduce inherited debt
                        debt_inheritance = 0.7
                    new_debt = migrant.debt_vector.copy() * debt_inheritance
                else:
                    new_debt = np.zeros_like(new_gene.gene)
                
                # Add migration noise to debt
                new_debt += np.random.randn(*new_debt.shape) * 0.1
                
                # FIX #3: Orthogonalize migrated debt
                island_debts = []
                for agent in self.islands[island_idx]:
                    if agent.debt_vector is not None and np.linalg.norm(agent.debt_vector) > 1e-6:
                        island_debts.append(agent.debt_vector)
                
                if island_debts:
                    new_debt = self._orthogonalize_debt(new_debt, island_debts)
                
                new_agent = SearchAgent(
                    gene=new_gene, 
                    island_id=island_idx,
                    debt_vector=new_debt,
                    accuracy=migrant.accuracy,
                    cost_metrics=migrant.cost_metrics.copy(),
                    mutation_magnitude=migrant.mutation_magnitude,
                    acceptance_rate=migrant.acceptance_rate
                )
                
                # Evaluate in new context
                self._evaluate_agent(new_agent, iteration)
                self.islands[island_idx][i] = new_agent

    def _inject_diversity(self, iteration: int) -> None:
        """Enhanced diversity injection with debt-guided exploration."""
        t = iteration / self.config.max_iterations
        
        for island_idx, island in enumerate(self.islands):
            n = len(island)
            if n < 2:
                continue
            
            # Calculate diversity with debt-aware metric
            island_agents = island
            if len(island_agents) < 2:
                continue
            
            # Compute diversity including debt directions
            diversity_scores = []
            for i, agent in enumerate(island_agents):
                # Combine gene and debt for diversity calculation
                if agent.debt_vector is not None and np.linalg.norm(agent.debt_vector) > 1e-6:
                    # Normalize debt vector
                    debt_dir = agent.debt_vector / (np.linalg.norm(agent.debt_vector) + 1e-8)
                    agent_vector = np.concatenate([agent.gene.gene, debt_dir * 0.3])
                else:
                    agent_vector = agent.gene.gene
                
                # Calculate distance to other agents
                distances = []
                for j, other in enumerate(island_agents):
                    if i == j:
                        continue
                    if other.debt_vector is not None and np.linalg.norm(other.debt_vector) > 1e-6:
                        other_debt_dir = other.debt_vector / (np.linalg.norm(other.debt_vector) + 1e-8)
                        other_vector = np.concatenate([other.gene.gene, other_debt_dir * 0.3])
                    else:
                        other_vector = other.gene.gene
                    
                    # Ensure vectors have same length
                    min_len = min(len(agent_vector), len(other_vector))
                    dist = np.linalg.norm(agent_vector[:min_len] - other_vector[:min_len])
                    distances.append(dist)
                
                diversity_scores.append(np.mean(distances) if distances else 0.0)
            
            avg_diversity = np.mean(diversity_scores)
            
            # Dynamic diversity threshold based on phase
            phase_threshold = self.config.min_diversity_threshold
            if t < 0.3:  # Early: require less diversity
                phase_threshold *= 0.7
            elif t > 0.7:  # Late: require more diversity
                phase_threshold *= 1.3
            
            if avg_diversity > phase_threshold:
                continue  # Island is diverse enough
            
            # Enhanced diversity injection
            elite_count = max(1, int(n * self._effective_elite_ratio))
            island.sort(key=lambda a: a.fitness)
            elites = island[:elite_count]
            
            # Inject into worst-performing agents
            for i in range(elite_count, n):
                if np.random.random() < 0.6:  # 60% chance to inject
                    # Select parent (prefer elites with high debt for exploration)
                    eligible_parents = []
                    for elite in elites:
                        if elite.debt_vector is not None and np.linalg.norm(elite.debt_vector) > 0.1:
                            eligible_parents.append((elite, 2.0))  # Higher weight for indebted elites
                        else:
                            eligible_parents.append((elite, 1.0))
                    
                    if eligible_parents:
                        parents, weights = zip(*eligible_parents)
                        parent = np.random.choice(parents, p=np.array(weights)/sum(weights))
                    else:
                        parent = island[np.random.randint(0, n)]
                    
                    # Create mutant with debt-guided exploration
                    parent_vec = parent.gene.gene
                    
                    # Use parent's debt direction for exploration
                    if parent.debt_vector is not None and np.linalg.norm(parent.debt_vector) > 1e-6:
                        # Explore in debt direction (repayment direction)
                        exploration_dir = -parent.debt_vector  # Opposite of debt = repayment direction
                        exploration_dir = exploration_dir / (np.linalg.norm(exploration_dir) + 1e-8)
                    else:
                        # Random exploration
                        exploration_dir = np.random.randn(len(parent_vec))
                        exploration_dir = exploration_dir / (np.linalg.norm(exploration_dir) + 1e-8)
                    
                    # Add orthogonal noise
                    noise = np.random.randn(len(parent_vec))
                    if np.linalg.norm(exploration_dir) > 1e-6:
                        # Make noise orthogonal to exploration direction
                        noise = noise - np.dot(noise, exploration_dir) * noise
                    
                    # Adaptive mutation strength
                    mutation_strength = island[i].mutation_magnitude
                    if t > 0.7:  # Late phase: stronger exploration
                        mutation_strength *= self.config.late_phase_exploration_boost
                    
                    # Combine exploration direction and orthogonal noise
                    mutant_gene = parent_vec + mutation_strength * (
                        0.7 * exploration_dir + 0.3 * noise / (np.linalg.norm(noise) + 1e-8)
                    )
                    
                    # Clip
                    self._clip_gene_array(mutant_gene, parent.gene.num_layers, parent.gene.num_cells)
                    
                    # Create new agent with inherited debt
                    new_gene = ArchitectureGene(
                        parent.gene.num_layers,
                        parent.gene.num_cells,
                        continuous_mode=parent.gene.continuous_mode,
                    )
                    new_gene.gene = mutant_gene
                    
                    # Inherit and amplify debt for exploration
                    new_debt = parent.debt_vector.copy() if parent.debt_vector is not None else np.zeros_like(mutant_gene)
                    new_debt = new_debt * 0.8 + np.random.randn(*new_debt.shape) * 0.2
                    
                    # FIX #3: Orthogonalize debt against island's existing debts
                    island_debts = []
                    for agent in island:
                        if agent.debt_vector is not None and np.linalg.norm(agent.debt_vector) > 1e-6:
                            island_debts.append(agent.debt_vector)
                    
                    if island_debts:
                        new_debt = self._orthogonalize_debt(new_debt, island_debts)
                    
                    new_agent = SearchAgent(
                        gene=new_gene, 
                        island_id=island_idx,
                        debt_vector=new_debt,
                        mutation_magnitude=min(0.3, mutation_strength * 1.1),
                        acceptance_rate=island[i].acceptance_rate * 0.9
                    )
                    self._evaluate_agent(new_agent, iteration)
                    island[i] = new_agent

    def _force_diversity(self, island_idx: int, iteration: int) -> None:
        """Force diversity in a stagnated island."""
        island = self.islands[island_idx]
        if len(island) < 3:
            return
        
        # Keep best agent
        island.sort(key=lambda a: a.fitness)
        best_agent = island[0]
        
        # Generate completely new agents with strategic debt
        new_agents = [best_agent]
        for i in range(1, len(island)):
            # Create new gene
            gene = ArchitectureGene(
                best_agent.gene.num_layers,
                best_agent.gene.num_cells,
                continuous_mode=best_agent.gene.continuous_mode,
            )
            
            # Strategic debt: opposite direction of best agent's debt
            if best_agent.debt_vector is not None and np.linalg.norm(best_agent.debt_vector) > 1e-6:
                # Explore in opposite direction of current debt
                debt_dir = best_agent.debt_vector / np.linalg.norm(best_agent.debt_vector)
                exploration_dir = -debt_dir  # Opposite direction
            else:
                exploration_dir = np.random.randn(gene.D)
                exploration_dir = exploration_dir / (np.linalg.norm(exploration_dir) + 1e-8)
            
            # Apply exploration
            mutation_strength = 0.3  # Strong mutation for restart
            gene.gene = best_agent.gene.gene + mutation_strength * exploration_dir
            self._clip_gene_array(gene.gene, gene.num_layers, gene.num_cells)
            
            # Create new agent with fresh debt
            new_debt = exploration_dir * 0.5  # Debt in exploration direction
            
            # FIX #3: Orthogonalize restart debt
            island_debts = []
            for agent in self.islands[island_idx]:
                if agent.debt_vector is not None and np.linalg.norm(agent.debt_vector) > 1e-6:
                    island_debts.append(agent.debt_vector)
            
            if island_debts:
                new_debt = self._orthogonalize_debt(new_debt, island_debts)
            
            new_agent = SearchAgent(
                gene=gene,
                island_id=island_idx,
                debt_vector=new_debt,
                mutation_magnitude=0.25,  # Reset to moderate
                acceptance_rate=0.4  # Reset acceptance
            )
            self._evaluate_agent(new_agent, iteration)
            new_agents.append(new_agent)
        
        self.islands[island_idx] = new_agents

    def _compute_auc_metrics(self) -> None:
        """Compute AUC metrics for accuracy progression."""
        if not self.history['iterations']:
            return
            
        iterations = self.history['iterations']
        accuracies = self.history['best_accuracy']
        
        # AUC@10: Area under curve for first 10 iterations
        if len(iterations) >= 10:
            idx_10 = min(10, len(iterations))
            self.history['auc_10'] = np.trapz(accuracies[:idx_10], iterations[:idx_10])
        
        # AUC@25
        if len(iterations) >= 25:
            idx_25 = min(25, len(iterations))
            self.history['auc_25'] = np.trapz(accuracies[:idx_25], iterations[:idx_25])
        
        # AUC@50
        if len(iterations) >= 50:
            idx_50 = min(50, len(iterations))
            self.history['auc_50'] = np.trapz(accuracies[:idx_50], iterations[:idx_50])
        
        # Time to 95% and 99% of best accuracy
        if accuracies:
            best_acc = max(accuracies)
            target_95 = best_acc * 0.95
            target_99 = best_acc * 0.99
            
            # Find first iteration where accuracy reaches target
            for i, acc in enumerate(accuracies):
                if acc >= target_95 and self.history['time_to_95'] == 0:
                    self.history['time_to_95'] = i
                if acc >= target_99 and self.history['time_to_99'] == 0:
                    self.history['time_to_99'] = i
                    break

    def optimize(self) -> Dict:
        self.logger.info("=" * 70)
        self.logger.info("Starting DPO-NAS Optimization (Accuracy-Aware)")
        self.logger.info(f"Strategy: {self.config.eval_strategy}")
        self.logger.info(f"Fitness Mode: {self.config.fitness_mode}")
        self.logger.info(f"Accuracy-aware: {self.config.w_loss == 0.0}")
        self.logger.info(f"Population: {self.config.population_size} | Islands: {len(self.islands)}")
        self.logger.info("=" * 70)
        
        # Set estimator to search mode
        if hasattr(self.estimator, 'set_search_mode'):
            self.estimator.set_search_mode(True)
        
        self.initialize_population()
        self.temperature = self.config.temperature_start
        continuous_mode = bool(getattr(self.config, 'continuous_mode', False))
        
        # Track stagnation per island
        island_stagnation = [0] * len(self.islands)
        
        for iteration in range(self.config.max_iterations):
            # Update temperature with phase-dependent decay
            # Faster early cooling → less bad-solution acceptance → better AUC
            t = iteration / self.config.max_iterations
            if t < 0.3:
                decay = self.config.temperature_decay  # Use config rate – no artificial slowdown
            elif t < 0.7:
                decay = self.config.temperature_decay
            else:
                decay = max(self.config.temperature_decay, 0.96)  # Ensure adequate cooling late
            
            self.temperature = max(
                self.config.temperature_min,
                self.temperature * decay
            )
            
            # Update population statistics for normalization
            self._update_population_stats()

            # Diversity scores are only used by NAS-specific shaping.
            if not continuous_mode:
                self._compute_diversity_scores()
            
            # B. Penalize over-confidence - reward diversity (NAS-only)
            if not continuous_mode:
                for island in self.islands:
                    for agent in island:
                        if agent.diversity_score < 0.05:
                            agent.fitness += 0.005
            
            # Get adaptive parameters for current phase
            params = self._get_adaptive_parameters(iteration)
            
            # Process each island with stagnation awareness
            for island_idx, island in enumerate(self.islands):
                island_improved = False
                
                for idx, agent in enumerate(island):
                    # Check for stagnation-driven debt injection
                    if (agent.debt_vector is not None and 
                        np.linalg.norm(agent.debt_vector) < self.config.min_debt_for_exploration and
                        iteration - agent.last_improvement > self.config.stagnation_threshold):
                        # Inject exploration debt
                        exploration_debt = np.random.randn(agent.gene.D) * self.config.stagnation_debt_injection
                        agent.debt_vector = agent.debt_vector * 0.5 + exploration_debt * 0.5
                    
                    new_agent = self._dpo_step(agent, params, iteration)
                    
                    # Keep raw evaluated fitness in scalar mode; additional
                    # shaping (island bias / smoothing) can blur ranking signal.
                    if (not continuous_mode) and self.config.fitness_mode == 'pareto_assisted':
                        biased_fitness = self._apply_island_specialization_bias(new_agent, island_idx)
                        new_agent.fitness = biased_fitness
                        smoothed_fit = self._smooth_fitness(new_agent, island_idx, alpha=0.7)
                        new_agent.fitness = smoothed_fit
                    
                    # SOTA: Age-based survival (track generation with CrossoverCount)
                    new_agent.age = self.generation_counter
                    
                    island[idx] = new_agent
                    
                    # Check if island improved
                    if new_agent.fitness < agent.fitness:
                        island_improved = True
                    
                    # Update best agents
                    if new_agent.fitness < self.best_fitness:
                        self.best_fitness = new_agent.fitness
                        self.best_agent = new_agent
                        self.logger.info(f"[Iter {iteration}] New best fitness: {self.best_fitness:.4f}")
                    
                    if new_agent.accuracy > self.best_accuracy:
                        self.best_accuracy = new_agent.accuracy
                        self.best_accuracy_agent = new_agent
                        self.logger.info(f"[Iter {iteration}] New best accuracy: {self.best_accuracy:.4f}")
                
                if not continuous_mode:
                    # SOTA: NSGA-II based survival with crowding distance
                    self._nsga_ii_sort_and_select(island_idx, self.config.population_size // len(self.islands))

                    # Trigger local search only when an island is stagnating.
                    if (iteration + 1) % 6 == 0 and t > 0.5 and island_stagnation[island_idx] > 4:
                        self._local_elite_search(island_idx, iteration, top_k=2)

                    if t > 0.8 and (iteration + 1) % 4 == 0 and island_stagnation[island_idx] > 6:
                        self._local_elite_search(island_idx, iteration, top_k=3)
                
                # SOTA: Population-based annealing per island
                if island_improved:
                    self.island_temperatures[island_idx] *= 0.96
                    island_stagnation[island_idx] = 0
                else:
                    self.island_temperatures[island_idx] *= 1.02  # Increase when stagnated
                    island_stagnation[island_idx] += 1
                
                # Clip island temperature
                self.island_temperatures[island_idx] = max(
                    self.config.temperature_min,
                    min(self.config.temperature_start, self.island_temperatures[island_idx])
                )
                
                # If island severely stagnated, force diversity (NAS-only)
                if (not continuous_mode) and island_stagnation[island_idx] > 20:
                    self.logger.info(f"Island {island_idx} stagnated, forcing diversity")
                    self._force_diversity(island_idx, iteration)
                    island_stagnation[island_idx] = 0
                
                self.generation_counter += len(island)
            
            # Late elite re-optimisation (NAS-only): periodic and lighter.
            if (not continuous_mode) and t > 0.8 and (iteration + 1) % 3 == 0:
                for island_idx_e in range(len(self.islands)):
                    island_e = self.islands[island_idx_e]
                    elite_count = max(2, int(len(island_e) * 0.10))
                    elites = sorted(island_e, key=lambda a: a.fitness)[:elite_count]
                    for elite in elites:
                        refined = self._dpo_step(elite, params, iteration)
                        if refined.fitness < elite.fitness:
                            pos = island_e.index(elite)
                            island_e[pos] = refined
                            if refined.fitness < self.best_fitness:
                                self.best_fitness = refined.fitness
                                self.best_agent = refined
                            if refined.accuracy > self.best_accuracy:
                                self.best_accuracy = refined.accuracy
                                self.best_accuracy_agent = refined

            # CONVERGENCE FIX: Exploitation Lockdown - disable chaos-inducing ops at 85%
            # NAS-only; continuous mode keeps strict one-pass-per-agent updates.
            if (not continuous_mode) and t < 0.85:
                # Periodic operations
                if (iteration + 1) % self.config.migration_freq == 0 and self.config.island_model:
                    self._migrate_between_islands(iteration)
                    
                if (iteration + 1) % self.config.diversity_inject_freq == 0:
                    if self._component_flags["diversity"]:
                        self._inject_diversity(iteration)
                    for island_idx in range(len(self.islands)):
                        self._inject_elite_offspring(island_idx, iteration)
            
            # Update Pareto archive if enabled
            if self.config.fitness_mode == 'pareto_assisted':
                for island in self.islands:
                    for agent in island:
                        self._update_pareto_archive(agent)
            
            # Tracking and statistics
            all_agents = [a for island in self.islands for a in island]
            all_fitness = np.array([a.fitness for a in all_agents])
            
            # Flush all sub-iteration improvements into self.best_accuracy BEFORE
            # recording history.  _local_elite_search, _inject_elite_offspring,
            # migration, and diversity injection all modify islands but do NOT
            # update self.best_accuracy, so without this scan those improvements
            # are silently lost for the current AUC data point.
            for _a in all_agents:
                if _a.accuracy > self.best_accuracy:
                    self.best_accuracy = _a.accuracy
                    self.best_accuracy_agent = _a
                if _a.fitness < self.best_fitness:
                    self.best_fitness = _a.fitness
                    self.best_agent = _a
            
            # Track improvements
            if self.history['best_fitness']:
                improvement = self.history['best_fitness'][-1] - self.best_fitness
                accuracy_improvement = self.best_accuracy - (self.history['best_accuracy'][-1] if self.history['best_accuracy'] else 0.0)
            else:
                improvement = 0.0
                accuracy_improvement = 0.0
            
            self.improvement_tracker.append(improvement)
            self.accuracy_improvement_tracker.append(accuracy_improvement)
            
            # Compute debt statistics
            debt_norms = [float(np.linalg.norm(a.debt_vector)) for a in all_agents if a.debt_vector is not None]
            avg_debt_norm = np.mean(debt_norms) if debt_norms else 0.0
            
            # Compute acceptance statistics
            total = self.acceptance_stats['total_candidates']
            acceptance_rate = (self.acceptance_stats['accepted_better'] + self.acceptance_stats['accepted_worse']) / max(total, 1)
            
            # Update history
            self.history['iterations'].append(iteration)
            self.history['best_fitness'].append(self.best_fitness)
            self.history['best_accuracy'].append(self.best_accuracy)
            self.history['avg_fitness'].append(float(np.mean(all_fitness)))
            self.history['worst_fitness'].append(float(np.max(all_fitness)))
            self.history['improvement_rate'].append(float(improvement))
            self.history['temperature'].append(self.temperature)
            self.history['debt_norms'].append(avg_debt_norm)
            avg_diversity = float(np.mean([a.diversity_score for a in all_agents]))
            self.history['diversity_scores'].append(avg_diversity)
            self.history['acceptance_rates'].append(acceptance_rate)

            # Adaptive acceptance scaling and elite ratio
            self._update_adaptive_controls(iteration, acceptance_rate, avg_diversity)
            self._last_acceptance_rate = acceptance_rate
            self._last_diversity = avg_diversity
            self._last_improvement = improvement
            
            # Acceptance rate sanity band: adjust temperature
            if acceptance_rate < 0.2:
                self.temperature *= 1.05  # increase slightly if too low
            elif acceptance_rate > 0.6:
                self.temperature *= 0.95  # decay faster if too high
            self.temperature = max(self.config.temperature_min, min(self.config.temperature_start, self.temperature))
            
            # Logging
            if iteration % 10 == 0 or iteration == self.config.max_iterations - 1:
                self.logger.info(
                    f"[Iter {iteration:3d}] "
                    f"BestFit: {self.best_fitness:.4f} | "
                    f"BestAcc: {self.best_accuracy:.4f} | "
                    f"AvgFit: {np.mean(all_fitness):.4f} | "
                    f"Temp: {self.temperature:.3f} | "
                    f"Debt: {avg_debt_norm:.3f} | "
                    f"Accept: {acceptance_rate:.3f}"
                )
            
            # Early stopping check
            if self.config.adaptive_early_stop and iteration > 50:
                recent_improvements = list(self.improvement_tracker)[-20:]
                if all(abs(imp) < self.config.convergence_threshold for imp in recent_improvements):
                    self.logger.info(f"Early stopping at iteration {iteration}")
                    break

        # Finalize with accurate evaluation
        if self.best_agent:
            # Re-evaluate best agent with ensemble averaging and caching
            if hasattr(self.estimator, 'set_search_mode'):
                self.estimator.set_search_mode(False)
            
            # Use the accuracy-best agent for final eval (not fitness-best, which may differ
            # after smoothing and island-bias modifications)
            eval_agent = self.best_accuracy_agent or self.best_agent
            
            # Final evaluation with ensemble averaging
            arch_dict = eval_agent.gene.to_architecture_dict()
            arch_dict['_raw_gene'] = eval_agent.gene.gene.tolist()
            loss, metrics = self.estimator.estimate(
                arch_dict, 
                use_cache=True,
                search_mode=False,
                iteration=self.config.max_iterations,
                max_iterations=self.config.max_iterations,
                stagnation_detected=False  # No noise in final evaluation
            )
            
            # Update best agent with final metrics
            # CRITICAL: use max() — never let noisy re-eval clobber the tracked peak
            accuracy = metrics.get('accuracy', 1.0 - min(loss, 1.0))
            eval_agent.accuracy = accuracy
            eval_agent.metrics = metrics
            self.best_accuracy = max(self.best_accuracy, accuracy)
            # Sync best_agent pointer to accuracy-best agent
            self.best_agent = eval_agent
            
            self.history['best_architecture'] = self.best_agent.gene.to_architecture_dict()
            
            # Compute AUC metrics
            self._compute_auc_metrics()
            
            self.logger.info(f"Final Best Accuracy: {self.best_accuracy:.4f}")
            self.logger.info(f"Final Best Fitness: {self.best_fitness:.4f}")
            self.logger.info(f"AUC@10: {self.history['auc_10']:.4f}")
            self.logger.info(f"AUC@25: {self.history['auc_25']:.4f}")
            self.logger.info(f"Time to 95%: {self.history['time_to_95']} iterations")

        self.logger.info("=" * 70)
        self.logger.info("Optimization Complete!")
        self.logger.info(f"Best Accuracy: {self.best_accuracy:.4f}")
        self.logger.info(f"Best Fitness: {self.best_fitness:.4f}")
        if self.config.fitness_mode == 'pareto_assisted':
            self.logger.info(f"Pareto Archive Size: {len(self.pareto_archive)}")
        self.logger.info(f"Final Acceptance Rate: {acceptance_rate:.3f}")
        self.logger.info(f"Worse moves accepted: {self.acceptance_stats['accepted_worse']}")
        self.logger.info("=" * 70)

        return {
            'best_fitness': self.best_fitness,
            'best_accuracy': self.best_accuracy,
            'best_architecture': self.history.get('best_architecture'),
            'best_metrics': getattr(self.best_agent, 'metrics', {}),
            'history': self.history,
            'pareto_archive_size': len(self.pareto_archive),
            'acceptance_stats': self.acceptance_stats,
            'config': {
                'population_size': self.config.population_size,
                'islands': len(self.islands),
                'eval_strategy': self.config.eval_strategy,
                'fitness_mode': self.config.fitness_mode,
                'w_loss': self.config.w_loss,  # Indicate which fitness mode was used
                'max_iterations': len(self.history['iterations']),
            }
        }

    def _orthogonalize_debt(self, new_debt: np.ndarray, reference_debts: List[np.ndarray]) -> np.ndarray:
        """Make new debt orthogonal to mean of reference debts."""
        if not reference_debts:
            return new_debt
        
        # Compute mean debt direction
        mean_debt = np.zeros_like(new_debt)
        valid_debts = 0
        
        for ref_debt in reference_debts:
            if ref_debt is not None and np.linalg.norm(ref_debt) > 1e-6:
                # Normalize each debt vector
                norm_ref = np.linalg.norm(ref_debt)
                mean_debt += ref_debt / norm_ref
                valid_debts += 1
        
        if valid_debts == 0:
            return new_debt
        
        mean_debt /= valid_debts
        
        # Orthogonalize: remove projection onto mean debt direction
        if np.linalg.norm(mean_debt) > 1e-6:
            mean_debt_norm = mean_debt / np.linalg.norm(mean_debt)
            projection = np.dot(new_debt, mean_debt_norm) * mean_debt_norm
            orthogonal_debt = new_debt - projection
            
            # Preserve original magnitude
            original_norm = np.linalg.norm(new_debt)
            if np.linalg.norm(orthogonal_debt) > 1e-6:
                orthogonal_debt = orthogonal_debt / np.linalg.norm(orthogonal_debt) * original_norm
            
            return orthogonal_debt
        
        return new_debt