1use crate::architecture::{Architecture, ComponentType};
8use crate::error::{OptimError, Result};
9use scirs2_core::ndarray::{Array1, ScalarOperand, Zip};
10use scirs2_core::numeric::Float;
11use std::collections::HashMap;
12use std::fmt::Debug;
13
14#[derive(Debug, Clone, Copy)]
16pub enum AggregationMethod {
17 Mean,
19 WeightedSum,
21 AttentionPooling,
23}
24
25#[derive(Debug, Clone)]
30pub struct ArchitectureEmbedder<T: Float + Debug + Send + Sync + 'static + ScalarOperand> {
31 pub embedding_dim: usize,
33 pub component_embeddings: HashMap<ComponentType, Array1<T>>,
35 pub aggregation_method: AggregationMethod,
37 pub performance_weights: HashMap<String, T>,
39}
40
41const ALL_COMPONENT_TYPES: &[ComponentType] = &[
43 ComponentType::SGD,
44 ComponentType::Adam,
45 ComponentType::AdamW,
46 ComponentType::RMSprop,
47 ComponentType::AdaGrad,
48 ComponentType::AdaDelta,
49 ComponentType::Momentum,
50 ComponentType::Nesterov,
51 ComponentType::LRScheduler,
52 ComponentType::GradientClipping,
53 ComponentType::BatchNorm,
54 ComponentType::Dropout,
55 ComponentType::LAMB,
56 ComponentType::LARS,
57 ComponentType::Lion,
58 ComponentType::RAdam,
59 ComponentType::Lookahead,
60 ComponentType::SAM,
61 ComponentType::LBFGS,
62 ComponentType::SparseAdam,
63 ComponentType::GroupedAdam,
64 ComponentType::MAML,
65 ComponentType::L1Regularizer,
66 ComponentType::L2Regularizer,
67 ComponentType::ElasticNetRegularizer,
68 ComponentType::DropoutRegularizer,
69 ComponentType::WeightDecay,
70 ComponentType::AdaptiveLR,
71 ComponentType::AdaptiveMomentum,
72 ComponentType::AdaptiveRegularization,
73 ComponentType::LSTMOptimizer,
74 ComponentType::TransformerOptimizer,
75 ComponentType::AttentionOptimizer,
76 ComponentType::MetaSGD,
77 ComponentType::ConstantLR,
78 ComponentType::ExponentialLR,
79 ComponentType::StepLR,
80 ComponentType::CosineAnnealingLR,
81 ComponentType::OneCycleLR,
82 ComponentType::CyclicLR,
83 ComponentType::Reptile,
84 ComponentType::Custom,
85];
86
87impl<T: Float + Debug + Send + Sync + 'static + ScalarOperand> ArchitectureEmbedder<T> {
88 pub fn new(embedding_dim: usize, aggregation: AggregationMethod) -> Self {
92 let mut component_embeddings = HashMap::new();
93
94 for (idx, &comp_type) in ALL_COMPONENT_TYPES.iter().enumerate() {
95 let embedding = Self::sincos_encoding(idx, embedding_dim);
96 component_embeddings.insert(comp_type, embedding);
97 }
98
99 Self {
100 embedding_dim,
101 component_embeddings,
102 aggregation_method: aggregation,
103 performance_weights: HashMap::new(),
104 }
105 }
106
107 fn sincos_encoding(index: usize, dim: usize) -> Array1<T> {
109 let mut values = Vec::with_capacity(dim);
110 let idx_f64 = index as f64;
111
112 for d in 0..dim {
113 let denom = (10000.0_f64).powf(2.0 * (d / 2) as f64 / dim as f64);
114 let val = if d % 2 == 0 {
115 (idx_f64 / denom).sin()
116 } else {
117 (idx_f64 / denom).cos()
118 };
119 let t_val: T = scirs2_core::numeric::NumCast::from(val).unwrap_or_else(|| T::zero());
120 values.push(t_val);
121 }
122
123 Array1::from_vec(values)
124 }
125
126 pub fn embed(&self, architecture: &Architecture) -> Result<Array1<T>> {
128 if architecture.components.is_empty() {
129 return Err(OptimError::ArchitectureError(
130 "Cannot embed architecture with no components".to_string(),
131 ));
132 }
133
134 let mut embeddings: Vec<Array1<T>> = Vec::new();
136 for component in &architecture.components {
137 if !component.enabled {
138 continue;
139 }
140 let emb = self
141 .component_embeddings
142 .get(&component.component_type)
143 .ok_or_else(|| {
144 OptimError::ArchitectureError(format!(
145 "No embedding found for component type {:?}",
146 component.component_type
147 ))
148 })?;
149 embeddings.push(emb.clone());
150 }
151
152 if embeddings.is_empty() {
153 return Err(OptimError::ArchitectureError(
154 "No enabled components to embed".to_string(),
155 ));
156 }
157
158 match self.aggregation_method {
159 AggregationMethod::Mean => self.aggregate_mean(&embeddings),
160 AggregationMethod::WeightedSum => {
161 self.aggregate_weighted_sum(&embeddings, architecture)
162 }
163 AggregationMethod::AttentionPooling => self.aggregate_attention(&embeddings),
164 }
165 }
166
167 fn aggregate_mean(&self, embeddings: &[Array1<T>]) -> Result<Array1<T>> {
169 let n: T =
170 scirs2_core::numeric::NumCast::from(embeddings.len()).unwrap_or_else(|| T::one());
171 let mut result = Array1::zeros(self.embedding_dim);
172
173 for emb in embeddings {
174 Zip::from(&mut result).and(emb).for_each(|r, &e| {
175 *r = *r + e;
176 });
177 }
178
179 result.mapv_inplace(|v| v / n);
180 Ok(result)
181 }
182
183 fn aggregate_weighted_sum(
185 &self,
186 embeddings: &[Array1<T>],
187 architecture: &Architecture,
188 ) -> Result<Array1<T>> {
189 let mut type_counts: HashMap<ComponentType, usize> = HashMap::new();
191 for comp in &architecture.components {
192 if comp.enabled {
193 *type_counts.entry(comp.component_type).or_insert(0) += 1;
194 }
195 }
196
197 let total_count: usize = type_counts.values().sum();
198 let total_t: T =
199 scirs2_core::numeric::NumCast::from(total_count.max(1)).unwrap_or_else(|| T::one());
200
201 let mut result = Array1::zeros(self.embedding_dim);
202 let mut emb_idx = 0;
203
204 for comp in &architecture.components {
205 if !comp.enabled {
206 continue;
207 }
208 if emb_idx >= embeddings.len() {
209 break;
210 }
211
212 let count = type_counts.get(&comp.component_type).copied().unwrap_or(1);
213 let weight: T = scirs2_core::numeric::NumCast::from(count).unwrap_or_else(|| T::one());
214
215 let emb = &embeddings[emb_idx];
216 Zip::from(&mut result).and(emb).for_each(|r, &e| {
217 *r = *r + e * weight;
218 });
219 emb_idx += 1;
220 }
221
222 result.mapv_inplace(|v| v / total_t);
224 Ok(result)
225 }
226
227 fn aggregate_attention(&self, embeddings: &[Array1<T>]) -> Result<Array1<T>> {
229 let mut scores: Vec<T> = Vec::with_capacity(embeddings.len());
231 for emb in embeddings {
232 let norm_sq = emb.iter().fold(T::zero(), |acc, &v| acc + v * v);
233 scores.push(norm_sq.sqrt());
234 }
235
236 let max_score = scores
238 .iter()
239 .copied()
240 .fold(T::neg_infinity(), |a, b| if b > a { b } else { a });
241
242 let mut exp_scores: Vec<T> = scores.iter().map(|&s| (s - max_score).exp()).collect();
243
244 let sum_exp: T = exp_scores.iter().copied().fold(T::zero(), |a, b| a + b);
245 if sum_exp > T::zero() {
246 for s in &mut exp_scores {
247 *s = *s / sum_exp;
248 }
249 }
250
251 let mut result = Array1::zeros(self.embedding_dim);
253 for (emb, &weight) in embeddings.iter().zip(exp_scores.iter()) {
254 Zip::from(&mut result).and(emb).for_each(|r, &e| {
255 *r = *r + e * weight;
256 });
257 }
258
259 Ok(result)
260 }
261
262 pub fn compute_similarity(&self, arch1: &Architecture, arch2: &Architecture) -> Result<T> {
264 let emb1 = self.embed(arch1)?;
265 let emb2 = self.embed(arch2)?;
266
267 let dot: T = Zip::from(&emb1)
268 .and(&emb2)
269 .fold(T::zero(), |acc, &a, &b| acc + a * b);
270
271 let norm1 = emb1.iter().fold(T::zero(), |acc, &v| acc + v * v).sqrt();
272 let norm2 = emb2.iter().fold(T::zero(), |acc, &v| acc + v * v).sqrt();
273
274 let denom = norm1 * norm2;
275 if denom <= T::zero() {
276 return Ok(T::zero());
277 }
278
279 Ok(dot / denom)
280 }
281
282 pub fn find_similar(
286 &self,
287 query: &Architecture,
288 candidates: &[Architecture],
289 k: usize,
290 ) -> Result<Vec<(usize, T)>> {
291 if candidates.is_empty() {
292 return Ok(Vec::new());
293 }
294
295 let mut similarities: Vec<(usize, T)> = Vec::with_capacity(candidates.len());
296 for (idx, candidate) in candidates.iter().enumerate() {
297 let sim = self.compute_similarity(query, candidate)?;
298 similarities.push((idx, sim));
299 }
300
301 similarities.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
303
304 similarities.truncate(k);
305 Ok(similarities)
306 }
307
308 pub fn update_embeddings(
314 &mut self,
315 architectures: &[Architecture],
316 performances: &[T],
317 ) -> Result<()> {
318 if architectures.len() != performances.len() {
319 return Err(OptimError::InvalidParameter(format!(
320 "Architectures count ({}) must match performances count ({})",
321 architectures.len(),
322 performances.len()
323 )));
324 }
325
326 if architectures.is_empty() {
327 return Ok(());
328 }
329
330 let n: T =
332 scirs2_core::numeric::NumCast::from(performances.len()).unwrap_or_else(|| T::one());
333 let mean_perf = performances.iter().copied().fold(T::zero(), |a, b| a + b) / n;
334
335 let learning_rate: T =
336 scirs2_core::numeric::NumCast::from(0.01).unwrap_or_else(|| T::zero());
337
338 for (arch, &perf) in architectures.iter().zip(performances.iter()) {
340 let relative_perf = perf - mean_perf;
341
342 for component in &arch.components {
343 if !component.enabled {
344 continue;
345 }
346 if let Some(emb) = self.component_embeddings.get_mut(&component.component_type) {
347 let scale = T::one() + learning_rate * relative_perf;
349 emb.mapv_inplace(|v| v * scale);
350 }
351 }
352 }
353
354 for emb in self.component_embeddings.values_mut() {
356 let norm = emb.iter().fold(T::zero(), |acc, &v| acc + v * v).sqrt();
357 if norm > T::zero() {
358 emb.mapv_inplace(|v| v / norm);
359 }
360 }
361
362 Ok(())
363 }
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369 use crate::architecture::{ComponentPosition, Connection, ConnectionType, OptimizerComponent};
370
371 fn make_test_architecture(id: &str, component_types: &[ComponentType]) -> Architecture {
372 let components: Vec<OptimizerComponent> = component_types
373 .iter()
374 .enumerate()
375 .map(|(i, ct)| OptimizerComponent {
376 id: format!("comp_{}", i),
377 component_type: *ct,
378 hyperparameters: HashMap::new(),
379 position: ComponentPosition {
380 layer: i as u32,
381 index: 0,
382 x: i as f32 * 100.0,
383 y: 0.0,
384 },
385 enabled: true,
386 })
387 .collect();
388
389 let mut connections = Vec::new();
390 for i in 0..components.len().saturating_sub(1) {
391 connections.push(Connection {
392 from: components[i].id.clone(),
393 to: components[i + 1].id.clone(),
394 weight: 1.0,
395 connection_type: ConnectionType::Gradient,
396 enabled: true,
397 });
398 }
399
400 Architecture {
401 id: id.to_string(),
402 components,
403 connections,
404 hyperparameters: HashMap::new(),
405 performance: None,
406 generation: 0,
407 }
408 }
409
410 #[test]
411 fn test_embed_architecture() {
412 let embedder = ArchitectureEmbedder::<f64>::new(16, AggregationMethod::Mean);
413
414 let arch = make_test_architecture(
415 "test_arch",
416 &[
417 ComponentType::Adam,
418 ComponentType::SGD,
419 ComponentType::BatchNorm,
420 ],
421 );
422
423 let embedding = embedder.embed(&arch);
424 assert!(embedding.is_ok());
425 let embedding = embedding.ok().unwrap_or_else(|| Array1::zeros(0));
426 assert_eq!(embedding.len(), 16);
427
428 let sum: f64 = embedding.iter().map(|v| v.abs()).sum();
430 assert!(sum > 0.0, "Embedding should be non-zero");
431 }
432
433 #[test]
434 fn test_compute_similarity() {
435 let embedder = ArchitectureEmbedder::<f64>::new(32, AggregationMethod::Mean);
436
437 let arch1 = make_test_architecture("arch1", &[ComponentType::Adam, ComponentType::SGD]);
439 let arch2 = make_test_architecture("arch2", &[ComponentType::Adam, ComponentType::SGD]);
440
441 let sim = embedder.compute_similarity(&arch1, &arch2);
442 assert!(sim.is_ok());
443 let sim = sim.ok().unwrap_or(0.0);
444 assert!(
445 (sim - 1.0).abs() < 1e-6,
446 "Same components should have similarity ~1.0, got {}",
447 sim
448 );
449
450 let arch3 = make_test_architecture(
452 "arch3",
453 &[
454 ComponentType::LBFGS,
455 ComponentType::Nesterov,
456 ComponentType::BatchNorm,
457 ],
458 );
459 let sim_diff = embedder.compute_similarity(&arch1, &arch3);
460 assert!(sim_diff.is_ok());
461 let sim_diff = sim_diff.ok().unwrap_or(1.0);
462 assert!(
463 sim_diff < sim,
464 "Different architectures should be less similar: {} vs {}",
465 sim_diff,
466 sim
467 );
468 }
469
470 #[test]
471 fn test_find_similar_architectures() {
472 let embedder = ArchitectureEmbedder::<f64>::new(16, AggregationMethod::Mean);
473
474 let query = make_test_architecture("query", &[ComponentType::Adam, ComponentType::SGD]);
475
476 let candidates = vec![
477 make_test_architecture("c0", &[ComponentType::Adam, ComponentType::SGD]),
478 make_test_architecture(
479 "c1",
480 &[
481 ComponentType::LBFGS,
482 ComponentType::Nesterov,
483 ComponentType::BatchNorm,
484 ],
485 ),
486 make_test_architecture("c2", &[ComponentType::Adam]),
487 make_test_architecture(
488 "c3",
489 &[
490 ComponentType::RMSprop,
491 ComponentType::AdaDelta,
492 ComponentType::Dropout,
493 ],
494 ),
495 ];
496
497 let result = embedder.find_similar(&query, &candidates, 2);
498 assert!(result.is_ok());
499 let top_k = result.ok().unwrap_or_default();
500 assert_eq!(top_k.len(), 2);
501
502 assert_eq!(
504 top_k[0].0, 0,
505 "Candidate 0 (identical components) should be most similar"
506 );
507 assert!(
509 top_k[0].1 >= top_k[1].1,
510 "Results should be sorted by similarity descending"
511 );
512 }
513
514 #[test]
515 fn test_update_embeddings() {
516 let mut embedder = ArchitectureEmbedder::<f64>::new(16, AggregationMethod::Mean);
517
518 let arch_good =
519 make_test_architecture("good", &[ComponentType::Adam, ComponentType::BatchNorm]);
520 let arch_bad = make_test_architecture("bad", &[ComponentType::SGD, ComponentType::Dropout]);
521
522 let adam_emb_before = embedder
524 .component_embeddings
525 .get(&ComponentType::Adam)
526 .cloned()
527 .unwrap_or_else(|| Array1::zeros(16));
528
529 let architectures = vec![arch_good, arch_bad];
530 let performances = vec![0.95, 0.3];
531
532 let result = embedder.update_embeddings(&architectures, &performances);
533 assert!(result.is_ok());
534
535 let adam_emb_after = embedder
537 .component_embeddings
538 .get(&ComponentType::Adam)
539 .cloned()
540 .unwrap_or_else(|| Array1::zeros(16));
541
542 let norm: f64 = adam_emb_after.iter().map(|v| v * v).sum::<f64>().sqrt();
543 assert!(
544 (norm - 1.0).abs() < 1e-6,
545 "Embeddings should be normalized after update, norm = {}",
546 norm
547 );
548
549 let diff: f64 = Zip::from(&adam_emb_before)
551 .and(&adam_emb_after)
552 .fold(0.0, |acc, &a, &b| acc + (a - b).abs());
553 assert!(diff > 1e-10, "Embeddings should change after update");
554 }
555
556 #[test]
557 fn test_aggregation_methods() {
558 let arch = make_test_architecture(
559 "agg_test",
560 &[
561 ComponentType::Adam,
562 ComponentType::SGD,
563 ComponentType::BatchNorm,
564 ComponentType::GradientClipping,
565 ],
566 );
567
568 for &method in &[
570 AggregationMethod::Mean,
571 AggregationMethod::WeightedSum,
572 AggregationMethod::AttentionPooling,
573 ] {
574 let embedder = ArchitectureEmbedder::<f64>::new(8, method);
575 let result = embedder.embed(&arch);
576 assert!(result.is_ok(), "Aggregation {:?} should succeed", method);
577 let emb = result.ok().unwrap_or_else(|| Array1::zeros(0));
578 assert_eq!(emb.len(), 8, "Embedding dim should match for {:?}", method);
579
580 let sum: f64 = emb.iter().map(|v| v.abs()).sum();
581 assert!(sum > 0.0, "Embedding from {:?} should be non-zero", method);
582 }
583
584 let arch_repeated = make_test_architecture(
587 "repeated",
588 &[
589 ComponentType::Adam,
590 ComponentType::Adam,
591 ComponentType::SGD,
592 ComponentType::BatchNorm,
593 ],
594 );
595 let mean_embedder = ArchitectureEmbedder::<f64>::new(8, AggregationMethod::Mean);
596 let ws_embedder = ArchitectureEmbedder::<f64>::new(8, AggregationMethod::WeightedSum);
597
598 let emb_mean = mean_embedder
599 .embed(&arch_repeated)
600 .ok()
601 .unwrap_or_else(|| Array1::zeros(8));
602 let emb_ws = ws_embedder
603 .embed(&arch_repeated)
604 .ok()
605 .unwrap_or_else(|| Array1::zeros(8));
606
607 let diff: f64 = Zip::from(&emb_mean)
608 .and(&emb_ws)
609 .fold(0.0, |acc, &a, &b| acc + (a - b).abs());
610 assert!(
611 diff > 1e-10,
612 "Mean and WeightedSum should produce different embeddings for repeated components"
613 );
614 }
615}