Skip to main content

optirs_nas/
architecture.rs

1//! Architecture representation and search space definition for Neural Architecture Search.
2//!
3//! This module provides the core architecture representation used in NAS,
4//! including the search space definition, architecture encoding/decoding,
5//! and mutation/crossover operations for evolutionary algorithms.
6
7use scirs2_core::RngExt;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::fmt;
11
12/// Represents an optimizer architecture in the search space
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct Architecture {
15    /// Unique identifier for this architecture
16    pub id: String,
17
18    /// List of optimizer components in the architecture
19    pub components: Vec<OptimizerComponent>,
20
21    /// Connections between components
22    pub connections: Vec<Connection>,
23
24    /// Architecture-level hyperparameters
25    pub hyperparameters: HashMap<String, f64>,
26
27    /// Performance metrics from evaluation
28    pub performance: Option<PerformanceMetrics>,
29
30    /// Generation number (for evolutionary algorithms)
31    pub generation: u32,
32}
33
34/// Individual optimizer component within an architecture
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct OptimizerComponent {
37    /// Unique ID within the architecture
38    pub id: String,
39
40    /// Type of optimizer component
41    pub component_type: ComponentType,
42
43    /// Component-specific hyperparameters
44    pub hyperparameters: HashMap<String, f64>,
45
46    /// Position in the architecture graph
47    pub position: ComponentPosition,
48
49    /// Whether this component is enabled
50    pub enabled: bool,
51}
52
53/// Types of optimizer components that can be used in architectures
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
55pub enum ComponentType {
56    /// Stochastic Gradient Descent
57    SGD,
58    /// Adam optimizer
59    Adam,
60    /// AdamW (Adam with weight decay)
61    AdamW,
62    /// RMSprop optimizer
63    RMSprop,
64    /// AdaGrad optimizer
65    AdaGrad,
66    /// AdaDelta optimizer
67    AdaDelta,
68    /// Momentum component
69    Momentum,
70    /// Nesterov momentum
71    Nesterov,
72    /// Learning rate scheduler
73    LRScheduler,
74    /// Gradient clipping
75    GradientClipping,
76    /// Batch normalization
77    BatchNorm,
78    /// Dropout regularization
79    Dropout,
80    /// LAMB optimizer
81    LAMB,
82    /// LARS optimizer
83    LARS,
84    /// Lion optimizer
85    Lion,
86    /// RAdam optimizer
87    RAdam,
88    /// Lookahead optimizer
89    Lookahead,
90    /// SAM optimizer
91    SAM,
92    /// L-BFGS optimizer
93    LBFGS,
94    /// Sparse Adam optimizer
95    SparseAdam,
96    /// Grouped Adam optimizer
97    GroupedAdam,
98    /// MAML optimizer
99    MAML,
100    /// L1 regularizer
101    L1Regularizer,
102    /// L2 regularizer
103    L2Regularizer,
104    /// Elastic Net regularizer
105    ElasticNetRegularizer,
106    /// Dropout regularizer
107    DropoutRegularizer,
108    /// Weight decay
109    WeightDecay,
110    /// Adaptive learning rate
111    AdaptiveLR,
112    /// Adaptive momentum
113    AdaptiveMomentum,
114    /// Adaptive regularization
115    AdaptiveRegularization,
116    /// LSTM optimizer
117    LSTMOptimizer,
118    /// Transformer optimizer
119    TransformerOptimizer,
120    /// Attention optimizer
121    AttentionOptimizer,
122    /// Meta SGD
123    MetaSGD,
124    /// Constant learning rate
125    ConstantLR,
126    /// Exponential learning rate
127    ExponentialLR,
128    /// Step learning rate
129    StepLR,
130    /// Cosine annealing learning rate
131    CosineAnnealingLR,
132    /// One cycle learning rate
133    OneCycleLR,
134    /// Cyclic learning rate
135    CyclicLR,
136    /// Reptile optimizer
137    Reptile,
138    /// Custom component
139    Custom,
140}
141
142/// Position and layout information for a component
143#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct ComponentPosition {
145    /// Layer index in the architecture
146    pub layer: u32,
147    /// Position within the layer
148    pub index: u32,
149    /// Spatial coordinates (for visualization)
150    pub x: f32,
151    pub y: f32,
152}
153
154/// Connection between two components
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct Connection {
157    /// Source component ID
158    pub from: String,
159    /// Target component ID
160    pub to: String,
161    /// Connection weight/strength
162    pub weight: f64,
163    /// Connection type
164    pub connection_type: ConnectionType,
165    /// Whether this connection is enabled
166    pub enabled: bool,
167}
168
169/// Types of connections between components
170#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
171pub enum ConnectionType {
172    /// Direct gradient flow
173    Gradient,
174    /// Parameter update flow
175    Parameter,
176    /// Skip connection
177    Skip,
178    /// Residual connection
179    Residual,
180    /// Information flow (for meta-learning)
181    Information,
182}
183
184/// Performance metrics for an evaluated architecture
185#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct PerformanceMetrics {
187    /// Final optimization performance
188    pub final_performance: f64,
189    /// Convergence speed (iterations to convergence)
190    pub convergence_speed: f64,
191    /// Stability (variance in performance)
192    pub stability: f64,
193    /// Computational efficiency
194    pub efficiency: f64,
195    /// Memory usage
196    pub memory_usage: f64,
197    /// Robustness across problem instances
198    pub robustness: f64,
199    /// Overall score (weighted combination)
200    pub overall_score: f64,
201}
202
203/// Search space definition for architecture generation
204#[derive(Debug, Clone)]
205pub struct ArchitectureSpace {
206    /// Available component types
207    pub component_types: Vec<ComponentType>,
208
209    /// Minimum number of components
210    pub min_components: usize,
211
212    /// Maximum number of components
213    pub max_components: usize,
214
215    /// Maximum number of connections
216    pub max_connections: usize,
217
218    /// Whether cycles are allowed in the architecture graph
219    pub allow_cycles: bool,
220
221    /// Maximum depth of the architecture
222    pub max_depth: u32,
223
224    /// Hyperparameter ranges for each component type
225    pub hyperparameter_ranges: HashMap<ComponentType, HashMap<String, (f64, f64)>>,
226
227    /// Probability of each component type being selected
228    pub component_probabilities: HashMap<ComponentType, f64>,
229
230    /// Connection probability between components
231    pub connection_probability: f64,
232}
233
234impl ArchitectureSpace {
235    /// Create a new architecture space with default settings
236    pub fn new() -> Self {
237        let component_types = vec![
238            ComponentType::SGD,
239            ComponentType::Adam,
240            ComponentType::AdamW,
241            ComponentType::RMSprop,
242            ComponentType::Momentum,
243        ];
244
245        let mut component_probabilities = HashMap::new();
246        component_probabilities.insert(ComponentType::Adam, 0.3);
247        component_probabilities.insert(ComponentType::AdamW, 0.25);
248        component_probabilities.insert(ComponentType::SGD, 0.2);
249        component_probabilities.insert(ComponentType::RMSprop, 0.15);
250        component_probabilities.insert(ComponentType::Momentum, 0.1);
251
252        let mut hyperparameter_ranges = HashMap::new();
253
254        // Adam hyperparameters
255        let mut adam_ranges = HashMap::new();
256        adam_ranges.insert("learning_rate".to_string(), (1e-5, 1e-1));
257        adam_ranges.insert("beta1".to_string(), (0.8, 0.99));
258        adam_ranges.insert("beta2".to_string(), (0.9, 0.9999));
259        adam_ranges.insert("epsilon".to_string(), (1e-8, 1e-6));
260        hyperparameter_ranges.insert(ComponentType::Adam, adam_ranges);
261
262        // SGD hyperparameters
263        let mut sgd_ranges = HashMap::new();
264        sgd_ranges.insert("learning_rate".to_string(), (1e-4, 1e-1));
265        sgd_ranges.insert("momentum".to_string(), (0.0, 0.99));
266        hyperparameter_ranges.insert(ComponentType::SGD, sgd_ranges);
267
268        // RMSprop hyperparameters
269        let mut rmsprop_ranges = HashMap::new();
270        rmsprop_ranges.insert("learning_rate".to_string(), (1e-5, 1e-1));
271        rmsprop_ranges.insert("alpha".to_string(), (0.9, 0.999));
272        rmsprop_ranges.insert("epsilon".to_string(), (1e-8, 1e-6));
273        hyperparameter_ranges.insert(ComponentType::RMSprop, rmsprop_ranges);
274
275        Self {
276            component_types,
277            min_components: 1,
278            max_components: 10,
279            max_connections: 20,
280            allow_cycles: false,
281            max_depth: 5,
282            hyperparameter_ranges,
283            component_probabilities,
284            connection_probability: 0.3,
285        }
286    }
287
288    /// Generate a random architecture within this search space
289    pub fn generate_random_architecture(&self) -> Architecture {
290        let mut rng = scirs2_core::random::Random::default();
291
292        let num_components = rng.gen_range(self.min_components..=self.max_components);
293        let mut components = Vec::new();
294
295        for i in 0..num_components {
296            let component_type = self.component_types[rng.gen_range(0..self.component_types.len())];
297            let mut hyperparameters = HashMap::new();
298
299            if let Some(ranges) = self.hyperparameter_ranges.get(&component_type) {
300                for (param_name, (min_val, max_val)) in ranges {
301                    let value = rng.gen_range(*min_val..=*max_val);
302                    hyperparameters.insert(param_name.clone(), value);
303                }
304            }
305
306            components.push(OptimizerComponent {
307                id: format!("comp_{}", i),
308                component_type,
309                hyperparameters,
310                position: ComponentPosition {
311                    layer: i as u32,
312                    index: 0,
313                    x: i as f32 * 100.0,
314                    y: 0.0,
315                },
316                enabled: true,
317            });
318        }
319
320        let mut connections = Vec::new();
321        let max_connections = self
322            .max_connections
323            .min(num_components * (num_components - 1));
324        let num_connections = rng.gen_range(0..=max_connections);
325
326        for _ in 0..num_connections {
327            if rng.random::<f64>() < self.connection_probability {
328                let from_idx = rng.gen_range(0..num_components);
329                let to_idx = rng.gen_range(0..num_components);
330
331                // For acyclic graphs, only allow forward connections (from lower index to higher)
332                if !self.allow_cycles {
333                    if from_idx < to_idx {
334                        connections.push(Connection {
335                            from: components[from_idx].id.clone(),
336                            to: components[to_idx].id.clone(),
337                            weight: rng.gen_range(0.1..=1.0),
338                            connection_type: ConnectionType::Gradient,
339                            enabled: true,
340                        });
341                    }
342                } else if from_idx != to_idx {
343                    connections.push(Connection {
344                        from: components[from_idx].id.clone(),
345                        to: components[to_idx].id.clone(),
346                        weight: rng.gen_range(0.1..=1.0),
347                        connection_type: ConnectionType::Gradient,
348                        enabled: true,
349                    });
350                }
351            }
352        }
353
354        Architecture {
355            id: format!("arch_{}", rng.random::<u64>()),
356            components,
357            connections,
358            hyperparameters: HashMap::new(),
359            performance: None,
360            generation: 0,
361        }
362    }
363
364    /// Mutate an architecture
365    pub fn mutate_architecture(
366        &self,
367        architecture: &Architecture,
368        mutation_rate: f64,
369    ) -> Architecture {
370        let mut rng = scirs2_core::random::Random::default();
371        let mut mutated = architecture.clone();
372        mutated.id = format!("{}_{}_mut", architecture.id, rng.random::<u32>());
373        mutated.generation = architecture.generation + 1;
374        mutated.performance = None; // Reset performance after mutation
375
376        // Mutate component hyperparameters
377        for component in &mut mutated.components {
378            if rng.random::<f64>() < mutation_rate {
379                if let Some(ranges) = self.hyperparameter_ranges.get(&component.component_type) {
380                    for (param_name, value) in &mut component.hyperparameters {
381                        if let Some((min_val, max_val)) = ranges.get(param_name) {
382                            // Gaussian mutation around current value
383                            let std_dev = (max_val - min_val) * 0.1;
384                            let mutation = rng.random::<f64>() * std_dev - std_dev / 2.0;
385                            *value = (*value + mutation).clamp(*min_val, *max_val);
386                        }
387                    }
388                }
389            }
390        }
391
392        // Randomly add or remove components
393        if rng.random::<f64>() < mutation_rate * 0.1 {
394            if mutated.components.len() < self.max_components && rng.random::<bool>() {
395                // Add component
396                let component_type =
397                    self.component_types[rng.gen_range(0..self.component_types.len())];
398                let mut hyperparameters = HashMap::new();
399
400                if let Some(ranges) = self.hyperparameter_ranges.get(&component_type) {
401                    for (param_name, (min_val, max_val)) in ranges {
402                        let value = rng.gen_range(*min_val..=*max_val);
403                        hyperparameters.insert(param_name.clone(), value);
404                    }
405                }
406
407                mutated.components.push(OptimizerComponent {
408                    id: format!("comp_{}", mutated.components.len()),
409                    component_type,
410                    hyperparameters,
411                    position: ComponentPosition {
412                        layer: mutated.components.len() as u32,
413                        index: 0,
414                        x: mutated.components.len() as f32 * 100.0,
415                        y: 0.0,
416                    },
417                    enabled: true,
418                });
419            } else if mutated.components.len() > self.min_components {
420                // Remove component only if we'll still have at least min_components
421                let remove_idx = rng.gen_range(0..mutated.components.len());
422                let removed_id = mutated.components[remove_idx].id.clone();
423                mutated.components.remove(remove_idx);
424
425                // Remove connections involving the removed component
426                mutated
427                    .connections
428                    .retain(|conn| conn.from != removed_id && conn.to != removed_id);
429            }
430        }
431
432        mutated
433    }
434
435    /// Crossover two architectures to create offspring
436    pub fn crossover_architectures(
437        &self,
438        parent1: &Architecture,
439        parent2: &Architecture,
440    ) -> (Architecture, Architecture) {
441        let mut rng = scirs2_core::random::Random::default();
442
443        let mut child1 = parent1.clone();
444        let mut child2 = parent2.clone();
445
446        child1.id = format!("{}_{}_cross", parent1.id, rng.random::<u32>());
447        child2.id = format!("{}_{}_cross", parent2.id, rng.random::<u32>());
448        child1.generation = parent1.generation.max(parent2.generation) + 1;
449        child2.generation = parent1.generation.max(parent2.generation) + 1;
450        child1.performance = None;
451        child2.performance = None;
452
453        // Single-point crossover for components
454        if !parent1.components.is_empty() && !parent2.components.is_empty() {
455            let crossover_point1 = rng.gen_range(0..parent1.components.len());
456            let crossover_point2 = rng.gen_range(0..parent2.components.len());
457
458            let mut new_components1 = parent1.components[..crossover_point1].to_vec();
459            new_components1.extend_from_slice(&parent2.components[crossover_point2..]);
460
461            let mut new_components2 = parent2.components[..crossover_point2].to_vec();
462            new_components2.extend_from_slice(&parent1.components[crossover_point1..]);
463
464            // Ensure components respect min/max constraints
465            if new_components1.len() > self.max_components {
466                new_components1.truncate(self.max_components);
467            } else if new_components1.len() < self.min_components {
468                // Pad with random components from parents if too few
469                while new_components1.len() < self.min_components {
470                    let source = if rng.random_bool() {
471                        &parent1.components
472                    } else {
473                        &parent2.components
474                    };
475                    if !source.is_empty() {
476                        let idx = rng.gen_range(0..source.len());
477                        new_components1.push(source[idx].clone());
478                    } else {
479                        break;
480                    }
481                }
482            }
483
484            if new_components2.len() > self.max_components {
485                new_components2.truncate(self.max_components);
486            } else if new_components2.len() < self.min_components {
487                // Pad with random components from parents if too few
488                while new_components2.len() < self.min_components {
489                    let source = if rng.random_bool() {
490                        &parent1.components
491                    } else {
492                        &parent2.components
493                    };
494                    if !source.is_empty() {
495                        let idx = rng.gen_range(0..source.len());
496                        new_components2.push(source[idx].clone());
497                    } else {
498                        break;
499                    }
500                }
501            }
502
503            child1.components = new_components1;
504            child2.components = new_components2;
505
506            // Clean up connections - remove connections to non-existent components
507            let child1_ids: std::collections::HashSet<_> =
508                child1.components.iter().map(|c| c.id.clone()).collect();
509            let child2_ids: std::collections::HashSet<_> =
510                child2.components.iter().map(|c| c.id.clone()).collect();
511
512            child1
513                .connections
514                .retain(|conn| child1_ids.contains(&conn.from) && child1_ids.contains(&conn.to));
515
516            child2
517                .connections
518                .retain(|conn| child2_ids.contains(&conn.from) && child2_ids.contains(&conn.to));
519        }
520
521        (child1, child2)
522    }
523
524    /// Validate that an architecture satisfies search space constraints
525    pub fn validate_architecture(&self, architecture: &Architecture) -> Result<(), String> {
526        if architecture.components.len() < self.min_components {
527            return Err(format!(
528                "Too few components: {} < {}",
529                architecture.components.len(),
530                self.min_components
531            ));
532        }
533
534        if architecture.components.len() > self.max_components {
535            return Err(format!(
536                "Too many components: {} > {}",
537                architecture.components.len(),
538                self.max_components
539            ));
540        }
541
542        if architecture.connections.len() > self.max_connections {
543            return Err(format!(
544                "Too many connections: {} > {}",
545                architecture.connections.len(),
546                self.max_connections
547            ));
548        }
549
550        // Check for cycles if not allowed
551        if !self.allow_cycles && self.has_cycles(architecture) {
552            return Err("Architecture contains cycles, but cycles are not allowed".to_string());
553        }
554
555        // Validate hyperparameters are within ranges
556        for component in &architecture.components {
557            if let Some(ranges) = self.hyperparameter_ranges.get(&component.component_type) {
558                for (param_name, value) in &component.hyperparameters {
559                    if let Some((min_val, max_val)) = ranges.get(param_name) {
560                        if *value < *min_val || *value > *max_val {
561                            return Err(format!(
562                                "Hyperparameter {} = {} is out of range [{}, {}]",
563                                param_name, value, min_val, max_val
564                            ));
565                        }
566                    }
567                }
568            }
569        }
570
571        Ok(())
572    }
573
574    /// Check if the architecture contains cycles
575    fn has_cycles(&self, architecture: &Architecture) -> bool {
576        let mut visited = std::collections::HashSet::new();
577        let mut rec_stack = std::collections::HashSet::new();
578
579        for component in &architecture.components {
580            if !visited.contains(&component.id)
581                && Self::has_cycle_util(architecture, &component.id, &mut visited, &mut rec_stack)
582            {
583                return true;
584            }
585        }
586
587        false
588    }
589
590    fn has_cycle_util(
591        architecture: &Architecture,
592        node: &str,
593        visited: &mut std::collections::HashSet<String>,
594        rec_stack: &mut std::collections::HashSet<String>,
595    ) -> bool {
596        visited.insert(node.to_string());
597        rec_stack.insert(node.to_string());
598
599        // Find all outgoing connections from this node
600        for connection in &architecture.connections {
601            if connection.from == node && connection.enabled {
602                if !visited.contains(&connection.to) {
603                    if Self::has_cycle_util(architecture, &connection.to, visited, rec_stack) {
604                        return true;
605                    }
606                } else if rec_stack.contains(&connection.to) {
607                    return true;
608                }
609            }
610        }
611
612        rec_stack.remove(node);
613        false
614    }
615}
616
617impl Default for ArchitectureSpace {
618    fn default() -> Self {
619        Self::new()
620    }
621}
622
623impl fmt::Display for Architecture {
624    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
625        writeln!(f, "Architecture: {}", self.id)?;
626        writeln!(f, "  Components: {}", self.components.len())?;
627        writeln!(f, "  Connections: {}", self.connections.len())?;
628        writeln!(f, "  Generation: {}", self.generation)?;
629
630        if let Some(ref perf) = self.performance {
631            writeln!(f, "  Performance: {:.4}", perf.overall_score)?;
632        }
633
634        for component in &self.components {
635            writeln!(
636                f,
637                "    - {:?}: {} params",
638                component.component_type,
639                component.hyperparameters.len()
640            )?;
641        }
642
643        Ok(())
644    }
645}
646
647impl fmt::Display for ComponentType {
648    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
649        match self {
650            ComponentType::SGD => write!(f, "SGD"),
651            ComponentType::Adam => write!(f, "Adam"),
652            ComponentType::AdamW => write!(f, "AdamW"),
653            ComponentType::RMSprop => write!(f, "RMSprop"),
654            ComponentType::AdaGrad => write!(f, "AdaGrad"),
655            ComponentType::AdaDelta => write!(f, "AdaDelta"),
656            ComponentType::Momentum => write!(f, "Momentum"),
657            ComponentType::Nesterov => write!(f, "Nesterov"),
658            ComponentType::LRScheduler => write!(f, "LRScheduler"),
659            ComponentType::GradientClipping => write!(f, "GradientClipping"),
660            ComponentType::BatchNorm => write!(f, "BatchNorm"),
661            ComponentType::Dropout => write!(f, "Dropout"),
662            ComponentType::LAMB => write!(f, "LAMB"),
663            ComponentType::LARS => write!(f, "LARS"),
664            ComponentType::Lion => write!(f, "Lion"),
665            ComponentType::RAdam => write!(f, "RAdam"),
666            ComponentType::Lookahead => write!(f, "Lookahead"),
667            ComponentType::SAM => write!(f, "SAM"),
668            ComponentType::LBFGS => write!(f, "LBFGS"),
669            ComponentType::SparseAdam => write!(f, "SparseAdam"),
670            ComponentType::GroupedAdam => write!(f, "GroupedAdam"),
671            ComponentType::MAML => write!(f, "MAML"),
672            ComponentType::L1Regularizer => write!(f, "L1Regularizer"),
673            ComponentType::L2Regularizer => write!(f, "L2Regularizer"),
674            ComponentType::ElasticNetRegularizer => write!(f, "ElasticNetRegularizer"),
675            ComponentType::DropoutRegularizer => write!(f, "DropoutRegularizer"),
676            ComponentType::WeightDecay => write!(f, "WeightDecay"),
677            ComponentType::AdaptiveLR => write!(f, "AdaptiveLR"),
678            ComponentType::AdaptiveMomentum => write!(f, "AdaptiveMomentum"),
679            ComponentType::AdaptiveRegularization => write!(f, "AdaptiveRegularization"),
680            ComponentType::LSTMOptimizer => write!(f, "LSTMOptimizer"),
681            ComponentType::TransformerOptimizer => write!(f, "TransformerOptimizer"),
682            ComponentType::AttentionOptimizer => write!(f, "AttentionOptimizer"),
683            ComponentType::MetaSGD => write!(f, "MetaSGD"),
684            ComponentType::ConstantLR => write!(f, "ConstantLR"),
685            ComponentType::ExponentialLR => write!(f, "ExponentialLR"),
686            ComponentType::StepLR => write!(f, "StepLR"),
687            ComponentType::CosineAnnealingLR => write!(f, "CosineAnnealingLR"),
688            ComponentType::OneCycleLR => write!(f, "OneCycleLR"),
689            ComponentType::CyclicLR => write!(f, "CyclicLR"),
690            ComponentType::Reptile => write!(f, "Reptile"),
691            ComponentType::Custom => write!(f, "Custom"),
692        }
693    }
694}
695
696#[cfg(test)]
697mod tests {
698    use super::*;
699
700    #[test]
701    fn test_architecture_space_creation() {
702        let space = ArchitectureSpace::new();
703        assert!(space.min_components <= space.max_components);
704        assert!(!space.component_types.is_empty());
705        assert!(space.connection_probability >= 0.0 && space.connection_probability <= 1.0);
706    }
707
708    #[test]
709    fn test_random_architecture_generation() {
710        let space = ArchitectureSpace::new();
711        let arch = space.generate_random_architecture();
712
713        assert!(arch.components.len() >= space.min_components);
714        assert!(arch.components.len() <= space.max_components);
715        assert!(space.validate_architecture(&arch).is_ok());
716    }
717
718    #[test]
719    fn test_architecture_mutation() {
720        let space = ArchitectureSpace::new();
721        let original = space.generate_random_architecture();
722        let mutated = space.mutate_architecture(&original, 0.5);
723
724        assert_ne!(original.id, mutated.id);
725        assert_eq!(mutated.generation, original.generation + 1);
726
727        // Debug info for failures
728        if let Err(e) = space.validate_architecture(&mutated) {
729            eprintln!("Validation failed: {}", e);
730            eprintln!("Original components: {}", original.components.len());
731            eprintln!("Mutated components: {}", mutated.components.len());
732            eprintln!("Min components: {}", space.min_components);
733            eprintln!("Max components: {}", space.max_components);
734        }
735
736        assert!(space.validate_architecture(&mutated).is_ok());
737    }
738
739    #[test]
740    fn test_architecture_crossover() {
741        let space = ArchitectureSpace::new();
742        let parent1 = space.generate_random_architecture();
743        let parent2 = space.generate_random_architecture();
744
745        let (child1, child2) = space.crossover_architectures(&parent1, &parent2);
746
747        assert_ne!(child1.id, parent1.id);
748        assert_ne!(child2.id, parent2.id);
749        assert!(space.validate_architecture(&child1).is_ok());
750        assert!(space.validate_architecture(&child2).is_ok());
751    }
752
753    #[test]
754    fn test_architecture_validation() {
755        let space = ArchitectureSpace::new();
756        let valid_arch = space.generate_random_architecture();
757        assert!(space.validate_architecture(&valid_arch).is_ok());
758
759        // Test invalid architecture with too many components
760        let mut invalid_arch = valid_arch.clone();
761        for i in 0..space.max_components + 5 {
762            invalid_arch.components.push(OptimizerComponent {
763                id: format!("invalid_{}", i),
764                component_type: ComponentType::Adam,
765                hyperparameters: HashMap::new(),
766                position: ComponentPosition {
767                    layer: i as u32,
768                    index: 0,
769                    x: 0.0,
770                    y: 0.0,
771                },
772                enabled: true,
773            });
774        }
775
776        assert!(space.validate_architecture(&invalid_arch).is_err());
777    }
778}