1use crate::autodiff::optimizers::Optimizer;
8use crate::error::{MLError, Result};
9use crate::optimization::OptimizationMethod;
10use crate::qnn::{QNNLayerType, QuantumNeuralNetwork};
11use ndarray::{s, Array1, Array2, Array3, Axis};
12use quantrs2_circuit::builder::{Circuit, Simulator};
13use quantrs2_core::gate::{
14 single::{RotationX, RotationY, RotationZ},
15 GateOp,
16};
17use quantrs2_sim::statevector::StateVectorSimulator;
18use std::collections::{HashMap, HashSet};
19use std::fmt;
20
21#[derive(Debug, Clone, Copy)]
23pub enum SearchStrategy {
24 Evolutionary {
26 population_size: usize,
27 mutation_rate: f64,
28 crossover_rate: f64,
29 elitism_ratio: f64,
30 },
31
32 ReinforcementLearning {
34 agent_type: RLAgentType,
35 exploration_rate: f64,
36 learning_rate: f64,
37 },
38
39 Random { num_samples: usize },
41
42 BayesianOptimization {
44 acquisition_function: AcquisitionFunction,
45 num_initial_points: usize,
46 },
47
48 DARTS {
50 learning_rate: f64,
51 weight_decay: f64,
52 },
53}
54
55#[derive(Debug, Clone, Copy)]
57pub enum RLAgentType {
58 DQN,
60 PolicyGradient,
62 ActorCritic,
64}
65
66#[derive(Debug, Clone, Copy)]
68pub enum AcquisitionFunction {
69 ExpectedImprovement,
71 UpperConfidenceBound,
73 ProbabilityOfImprovement,
75}
76
77#[derive(Debug, Clone)]
79pub struct SearchSpace {
80 pub layer_types: Vec<QNNLayerType>,
82
83 pub depth_range: (usize, usize),
85
86 pub qubit_constraints: QubitConstraints,
88
89 pub param_ranges: HashMap<String, (usize, usize)>,
91
92 pub connectivity_patterns: Vec<String>,
94
95 pub measurement_bases: Vec<String>,
97}
98
99#[derive(Debug, Clone)]
101pub struct QubitConstraints {
102 pub min_qubits: usize,
104
105 pub max_qubits: usize,
107
108 pub topology: Option<QuantumTopology>,
110}
111
112#[derive(Debug, Clone)]
114pub enum QuantumTopology {
115 Linear,
117 Ring,
119 Grid { width: usize, height: usize },
121 Complete,
123 Custom { edges: Vec<(usize, usize)> },
125}
126
127#[derive(Debug, Clone)]
129pub struct ArchitectureCandidate {
130 pub id: String,
132
133 pub layers: Vec<QNNLayerType>,
135
136 pub num_qubits: usize,
138
139 pub metrics: ArchitectureMetrics,
141
142 pub properties: ArchitectureProperties,
144}
145
146#[derive(Debug, Clone)]
148pub struct ArchitectureMetrics {
149 pub accuracy: Option<f64>,
151
152 pub loss: Option<f64>,
154
155 pub circuit_depth: usize,
157
158 pub parameter_count: usize,
160
161 pub training_time: Option<f64>,
163
164 pub memory_usage: Option<usize>,
166
167 pub hardware_efficiency: Option<f64>,
169}
170
171#[derive(Debug, Clone)]
173pub struct ArchitectureProperties {
174 pub expressivity: Option<f64>,
176
177 pub entanglement_capability: Option<f64>,
179
180 pub gradient_variance: Option<f64>,
182
183 pub barren_plateau_score: Option<f64>,
185
186 pub noise_resilience: Option<f64>,
188}
189
190pub struct QuantumNAS {
192 strategy: SearchStrategy,
194
195 search_space: SearchSpace,
197
198 eval_data: Option<(Array2<f64>, Array1<usize>)>,
200
201 best_architectures: Vec<ArchitectureCandidate>,
203
204 search_history: Vec<ArchitectureCandidate>,
206
207 current_generation: usize,
209
210 rl_state: Option<RLSearchState>,
212
213 pareto_front: Vec<ArchitectureCandidate>,
215}
216
217#[derive(Debug, Clone)]
219pub struct RLSearchState {
220 q_values: HashMap<String, f64>,
222
223 policy_params: Array1<f64>,
225
226 replay_buffer: Vec<RLExperience>,
228
229 current_state: Array1<f64>,
231}
232
233#[derive(Debug, Clone)]
235pub struct RLExperience {
236 pub state: Array1<f64>,
238
239 pub action: ArchitectureAction,
241
242 pub reward: f64,
244
245 pub next_state: Array1<f64>,
247
248 pub done: bool,
250}
251
252#[derive(Debug, Clone)]
254pub enum ArchitectureAction {
255 AddLayer(QNNLayerType),
257
258 RemoveLayer(usize),
260
261 ModifyLayer(usize, HashMap<String, f64>),
263
264 ChangeConnectivity(String),
266
267 Finish,
269}
270
271impl QuantumNAS {
272 pub fn new(strategy: SearchStrategy, search_space: SearchSpace) -> Self {
274 Self {
275 strategy,
276 search_space,
277 eval_data: None,
278 best_architectures: Vec::new(),
279 search_history: Vec::new(),
280 current_generation: 0,
281 rl_state: None,
282 pareto_front: Vec::new(),
283 }
284 }
285
286 pub fn set_evaluation_data(&mut self, data: Array2<f64>, labels: Array1<usize>) {
288 self.eval_data = Some((data, labels));
289 }
290
291 pub fn search(&mut self, max_iterations: usize) -> Result<Vec<ArchitectureCandidate>> {
293 println!("Starting quantum neural architecture search...");
294
295 match self.strategy {
296 SearchStrategy::Evolutionary { .. } => self.evolutionary_search(max_iterations),
297 SearchStrategy::ReinforcementLearning { .. } => self.rl_search(max_iterations),
298 SearchStrategy::Random { .. } => self.random_search(max_iterations),
299 SearchStrategy::BayesianOptimization { .. } => self.bayesian_search(max_iterations),
300 SearchStrategy::DARTS { .. } => self.darts_search(max_iterations),
301 }
302 }
303
304 fn evolutionary_search(
306 &mut self,
307 max_generations: usize,
308 ) -> Result<Vec<ArchitectureCandidate>> {
309 let (population_size, mutation_rate, crossover_rate, elitism_ratio) = match self.strategy {
310 SearchStrategy::Evolutionary {
311 population_size,
312 mutation_rate,
313 crossover_rate,
314 elitism_ratio,
315 } => (
316 population_size,
317 mutation_rate,
318 crossover_rate,
319 elitism_ratio,
320 ),
321 _ => unreachable!(),
322 };
323
324 let mut population = self.initialize_population(population_size)?;
326
327 for generation in 0..max_generations {
328 self.current_generation = generation;
329
330 for candidate in &mut population {
332 if candidate.metrics.accuracy.is_none() {
333 self.evaluate_architecture(candidate)?;
334 }
335 }
336
337 population.sort_by(|a, b| {
339 let fitness_a = self.compute_fitness(a);
340 let fitness_b = self.compute_fitness(b);
341 fitness_b.partial_cmp(&fitness_a).unwrap()
342 });
343
344 self.update_best_architectures(&population);
346
347 self.update_pareto_front(&population);
349
350 println!(
351 "Generation {}: Best fitness = {:.4}",
352 generation,
353 self.compute_fitness(&population[0])
354 );
355
356 let elite_count = (population_size as f64 * elitism_ratio) as usize;
358 let mut next_generation = population[..elite_count].to_vec();
359
360 while next_generation.len() < population_size {
361 let parent1 = self.tournament_selection(&population, 3)?;
363 let parent2 = self.tournament_selection(&population, 3)?;
364
365 let mut offspring = if rand::random::<f64>() < crossover_rate {
367 self.crossover(&parent1, &parent2)?
368 } else {
369 parent1.clone()
370 };
371
372 if rand::random::<f64>() < mutation_rate {
374 self.mutate(&mut offspring)?;
375 }
376
377 next_generation.push(offspring);
378 }
379
380 population = next_generation;
381
382 self.search_history.extend(population.clone());
384 }
385
386 Ok(self.best_architectures.clone())
387 }
388
389 fn rl_search(&mut self, max_episodes: usize) -> Result<Vec<ArchitectureCandidate>> {
391 let (agent_type, exploration_rate, learning_rate) = match self.strategy {
392 SearchStrategy::ReinforcementLearning {
393 agent_type,
394 exploration_rate,
395 learning_rate,
396 } => (agent_type, exploration_rate, learning_rate),
397 _ => unreachable!(),
398 };
399
400 self.initialize_rl_agent(agent_type, learning_rate)?;
402
403 for episode in 0..max_episodes {
404 let mut current_architecture = self.create_empty_architecture();
405 let mut episode_reward = 0.0;
406 let mut step = 0;
407
408 loop {
409 let state = self.architecture_to_state(¤t_architecture)?;
411
412 let action = if rand::random::<f64>() < exploration_rate {
414 self.sample_random_action(¤t_architecture)?
415 } else {
416 self.choose_best_action(&state)?
417 };
418
419 let (next_architecture, reward, done) =
421 self.apply_action(¤t_architecture, &action)?;
422
423 let next_state = self.architecture_to_state(&next_architecture)?;
425 let experience = RLExperience {
426 state: state.clone(),
427 action: action.clone(),
428 reward,
429 next_state: next_state.clone(),
430 done,
431 };
432
433 if let Some(ref mut rl_state) = self.rl_state {
434 rl_state.replay_buffer.push(experience);
435 }
436
437 if step % 10 == 0 {
439 self.train_rl_agent()?;
440 }
441
442 episode_reward += reward;
443 current_architecture = next_architecture;
444 step += 1;
445
446 if done || step > 20 {
447 break;
448 }
449 }
450
451 let mut final_candidate = current_architecture;
453 self.evaluate_architecture(&mut final_candidate)?;
454 self.search_history.push(final_candidate.clone());
455 self.update_best_architectures(&[final_candidate]);
456
457 if episode % 100 == 0 {
458 println!("Episode {}: Reward = {:.4}", episode, episode_reward);
459 }
460 }
461
462 Ok(self.best_architectures.clone())
463 }
464
465 fn random_search(&mut self, num_samples: usize) -> Result<Vec<ArchitectureCandidate>> {
467 for i in 0..num_samples {
468 let mut candidate = self.sample_random_architecture()?;
469 self.evaluate_architecture(&mut candidate)?;
470
471 self.search_history.push(candidate.clone());
472 self.update_best_architectures(&[candidate]);
473
474 if i % 100 == 0 {
475 println!("Evaluated {} random architectures", i + 1);
476 }
477 }
478
479 Ok(self.best_architectures.clone())
480 }
481
482 fn bayesian_search(&mut self, max_iterations: usize) -> Result<Vec<ArchitectureCandidate>> {
484 let (acquisition_fn, num_initial) = match self.strategy {
485 SearchStrategy::BayesianOptimization {
486 acquisition_function,
487 num_initial_points,
488 } => (acquisition_function, num_initial_points),
489 _ => unreachable!(),
490 };
491
492 let mut candidates = Vec::new();
494 for _ in 0..num_initial {
495 let mut candidate = self.sample_random_architecture()?;
496 self.evaluate_architecture(&mut candidate)?;
497 candidates.push(candidate);
498 }
499
500 for iteration in num_initial..max_iterations {
502 let surrogate = self.fit_surrogate_model(&candidates)?;
504
505 let next_candidate = self.optimize_acquisition(&surrogate, acquisition_fn)?;
507
508 let mut evaluated_candidate = next_candidate;
510 self.evaluate_architecture(&mut evaluated_candidate)?;
511
512 candidates.push(evaluated_candidate.clone());
513 self.search_history.push(evaluated_candidate.clone());
514 self.update_best_architectures(&[evaluated_candidate]);
515
516 if iteration % 50 == 0 {
517 let best_acc = self.best_architectures[0].metrics.accuracy.unwrap_or(0.0);
518 println!("Iteration {}: Best accuracy = {:.4}", iteration, best_acc);
519 }
520 }
521
522 Ok(self.best_architectures.clone())
523 }
524
525 fn darts_search(&mut self, max_epochs: usize) -> Result<Vec<ArchitectureCandidate>> {
527 let (learning_rate, weight_decay) = match self.strategy {
528 SearchStrategy::DARTS {
529 learning_rate,
530 weight_decay,
531 } => (learning_rate, weight_decay),
532 _ => unreachable!(),
533 };
534
535 let num_layers = 8; let num_ops = self.search_space.layer_types.len();
538 let mut alpha = Array2::zeros((num_layers, num_ops));
539
540 for i in 0..num_layers {
542 for j in 0..num_ops {
543 alpha[[i, j]] = 1.0 / num_ops as f64;
544 }
545 }
546
547 for epoch in 0..max_epochs {
548 let alpha_grad = self.compute_architecture_gradients(&alpha)?;
550 alpha = alpha - learning_rate * &alpha_grad;
551
552 for i in 0..num_layers {
554 let row_sum: f64 = alpha.row(i).iter().map(|x| x.exp()).sum();
555 for j in 0..num_ops {
556 alpha[[i, j]] = alpha[[i, j]].exp() / row_sum;
557 }
558 }
559
560 if epoch % 100 == 0 {
561 println!("DARTS epoch {}: Architecture weights updated", epoch);
562 }
563 }
564
565 let final_architecture = self.derive_architecture_from_weights(&alpha)?;
567 let mut candidate = final_architecture;
568 self.evaluate_architecture(&mut candidate)?;
569
570 self.search_history.push(candidate.clone());
571 self.update_best_architectures(&[candidate]);
572
573 Ok(self.best_architectures.clone())
574 }
575
576 fn initialize_population(&self, size: usize) -> Result<Vec<ArchitectureCandidate>> {
578 let mut population = Vec::new();
579 for i in 0..size {
580 let candidate = self.sample_random_architecture()?;
581 population.push(candidate);
582 }
583 Ok(population)
584 }
585
586 fn sample_random_architecture(&self) -> Result<ArchitectureCandidate> {
588 let depth =
589 fastrand::usize(self.search_space.depth_range.0..=self.search_space.depth_range.1);
590 let num_qubits = fastrand::usize(
591 self.search_space.qubit_constraints.min_qubits
592 ..=self.search_space.qubit_constraints.max_qubits,
593 );
594
595 let mut layers = Vec::new();
596
597 layers.push(QNNLayerType::EncodingLayer {
599 num_features: fastrand::usize(2..8),
600 });
601
602 for _ in 0..depth {
604 let layer_type_idx = fastrand::usize(0..self.search_space.layer_types.len());
605 let layer_type = self.search_space.layer_types[layer_type_idx].clone();
606 layers.push(layer_type);
607 }
608
609 let basis_idx = fastrand::usize(0..self.search_space.measurement_bases.len());
611 layers.push(QNNLayerType::MeasurementLayer {
612 measurement_basis: self.search_space.measurement_bases[basis_idx].clone(),
613 });
614
615 Ok(ArchitectureCandidate {
616 id: format!("arch_{}", fastrand::u64(..)),
617 layers,
618 num_qubits,
619 metrics: ArchitectureMetrics {
620 accuracy: None,
621 loss: None,
622 circuit_depth: 0,
623 parameter_count: 0,
624 training_time: None,
625 memory_usage: None,
626 hardware_efficiency: None,
627 },
628 properties: ArchitectureProperties {
629 expressivity: None,
630 entanglement_capability: None,
631 gradient_variance: None,
632 barren_plateau_score: None,
633 noise_resilience: None,
634 },
635 })
636 }
637
638 fn evaluate_architecture(&self, candidate: &mut ArchitectureCandidate) -> Result<()> {
640 let qnn = QuantumNeuralNetwork::new(
642 candidate.layers.clone(),
643 candidate.num_qubits,
644 4, 2, )?;
647
648 candidate.metrics.parameter_count = qnn.parameters.len();
650 candidate.metrics.circuit_depth = self.estimate_circuit_depth(&candidate.layers);
651
652 if let Some((data, labels)) = &self.eval_data {
654 let (accuracy, loss) = self.evaluate_on_dataset(&qnn, data, labels)?;
655 candidate.metrics.accuracy = Some(accuracy);
656 candidate.metrics.loss = Some(loss);
657 } else {
658 candidate.metrics.accuracy = Some(0.5 + 0.4 * rand::random::<f64>());
660 candidate.metrics.loss = Some(0.5 + 0.5 * rand::random::<f64>());
661 }
662
663 self.compute_architecture_properties(candidate)?;
665
666 Ok(())
667 }
668
669 fn compute_fitness(&self, candidate: &ArchitectureCandidate) -> f64 {
671 let accuracy = candidate.metrics.accuracy.unwrap_or(0.0);
672 let param_penalty = candidate.metrics.parameter_count as f64 / 1000.0;
673 let depth_penalty = candidate.metrics.circuit_depth as f64 / 100.0;
674
675 accuracy - 0.1 * param_penalty - 0.05 * depth_penalty
677 }
678
679 fn tournament_selection(
681 &self,
682 population: &[ArchitectureCandidate],
683 tournament_size: usize,
684 ) -> Result<ArchitectureCandidate> {
685 let mut best = None;
686 let mut best_fitness = f64::NEG_INFINITY;
687
688 for _ in 0..tournament_size {
689 let idx = fastrand::usize(0..population.len());
690 let candidate = &population[idx];
691 let fitness = self.compute_fitness(candidate);
692
693 if fitness > best_fitness {
694 best_fitness = fitness;
695 best = Some(candidate.clone());
696 }
697 }
698
699 Ok(best.unwrap())
700 }
701
702 fn crossover(
704 &self,
705 parent1: &ArchitectureCandidate,
706 parent2: &ArchitectureCandidate,
707 ) -> Result<ArchitectureCandidate> {
708 let mut child_layers = Vec::new();
710 let max_len = parent1.layers.len().max(parent2.layers.len());
711
712 for i in 0..max_len {
713 if rand::random::<bool>() {
714 if i < parent1.layers.len() {
715 child_layers.push(parent1.layers[i].clone());
716 }
717 } else {
718 if i < parent2.layers.len() {
719 child_layers.push(parent2.layers[i].clone());
720 }
721 }
722 }
723
724 let num_qubits = if rand::random::<bool>() {
725 parent1.num_qubits
726 } else {
727 parent2.num_qubits
728 };
729
730 Ok(ArchitectureCandidate {
731 id: format!("crossover_{}", fastrand::u64(..)),
732 layers: child_layers,
733 num_qubits,
734 metrics: ArchitectureMetrics {
735 accuracy: None,
736 loss: None,
737 circuit_depth: 0,
738 parameter_count: 0,
739 training_time: None,
740 memory_usage: None,
741 hardware_efficiency: None,
742 },
743 properties: ArchitectureProperties {
744 expressivity: None,
745 entanglement_capability: None,
746 gradient_variance: None,
747 barren_plateau_score: None,
748 noise_resilience: None,
749 },
750 })
751 }
752
753 fn mutate(&self, candidate: &mut ArchitectureCandidate) -> Result<()> {
755 let mutation_type = fastrand::usize(0..4);
756
757 match mutation_type {
758 0 => {
759 if candidate.layers.len() < self.search_space.depth_range.1 + 2 {
761 let layer_idx = fastrand::usize(0..self.search_space.layer_types.len());
762 let new_layer = self.search_space.layer_types[layer_idx].clone();
763 let insert_pos = fastrand::usize(1..candidate.layers.len()); candidate.layers.insert(insert_pos, new_layer);
765 }
766 }
767 1 => {
768 if candidate.layers.len() > 3 {
770 let remove_pos = fastrand::usize(1..candidate.layers.len() - 1);
772 candidate.layers.remove(remove_pos);
773 }
774 }
775 2 => {
776 if candidate.layers.len() > 2 {
778 let layer_idx = fastrand::usize(1..candidate.layers.len() - 1);
779 let new_layer_idx = fastrand::usize(0..self.search_space.layer_types.len());
780 candidate.layers[layer_idx] =
781 self.search_space.layer_types[new_layer_idx].clone();
782 }
783 }
784 3 => {
785 candidate.num_qubits = fastrand::usize(
787 self.search_space.qubit_constraints.min_qubits
788 ..=self.search_space.qubit_constraints.max_qubits,
789 );
790 }
791 _ => {}
792 }
793
794 candidate.metrics.accuracy = None;
796 candidate.metrics.loss = None;
797
798 Ok(())
799 }
800
801 fn update_best_architectures(&mut self, candidates: &[ArchitectureCandidate]) {
803 for candidate in candidates {
804 if candidate.metrics.accuracy.is_some() {
805 self.best_architectures.push(candidate.clone());
806 }
807 }
808
809 let mut fitness_scores: Vec<(usize, f64)> = self
811 .best_architectures
812 .iter()
813 .enumerate()
814 .map(|(i, arch)| (i, self.compute_fitness(arch)))
815 .collect();
816
817 fitness_scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
819
820 let sorted_architectures: Vec<_> = fitness_scores
822 .into_iter()
823 .take(10)
824 .map(|(i, _)| self.best_architectures[i].clone())
825 .collect();
826
827 self.best_architectures = sorted_architectures;
828 }
829
830 fn update_pareto_front(&mut self, candidates: &[ArchitectureCandidate]) {
832 for candidate in candidates {
833 let is_dominated = self
834 .pareto_front
835 .iter()
836 .any(|other| self.dominates(other, candidate));
837
838 if !is_dominated {
839 let mut to_remove = Vec::new();
841 for (i, other) in self.pareto_front.iter().enumerate() {
842 if self.dominates(candidate, other) {
843 to_remove.push(i);
844 }
845 }
846
847 for &i in to_remove.iter().rev() {
849 self.pareto_front.remove(i);
850 }
851
852 self.pareto_front.push(candidate.clone());
854 }
855 }
856 }
857
858 fn dominates(&self, a: &ArchitectureCandidate, b: &ArchitectureCandidate) -> bool {
860 let acc_a = a.metrics.accuracy.unwrap_or(0.0);
861 let acc_b = b.metrics.accuracy.unwrap_or(0.0);
862 let params_a = a.metrics.parameter_count as f64;
863 let params_b = b.metrics.parameter_count as f64;
864
865 (acc_a >= acc_b && params_a <= params_b) && (acc_a > acc_b || params_a < params_b)
866 }
867
868 fn estimate_circuit_depth(&self, layers: &[QNNLayerType]) -> usize {
870 layers
871 .iter()
872 .map(|layer| match layer {
873 QNNLayerType::EncodingLayer { .. } => 1,
874 QNNLayerType::VariationalLayer { num_params } => num_params / 3, QNNLayerType::EntanglementLayer { .. } => 1,
876 QNNLayerType::MeasurementLayer { .. } => 1,
877 })
878 .sum()
879 }
880
881 fn evaluate_on_dataset(
883 &self,
884 qnn: &QuantumNeuralNetwork,
885 data: &Array2<f64>,
886 labels: &Array1<usize>,
887 ) -> Result<(f64, f64)> {
888 let accuracy = 0.6 + 0.3 * rand::random::<f64>();
890 let loss = 0.2 + 0.8 * rand::random::<f64>();
891 Ok((accuracy, loss))
892 }
893
894 fn compute_architecture_properties(&self, candidate: &mut ArchitectureCandidate) -> Result<()> {
896 let expressivity = (candidate.metrics.parameter_count as f64).ln()
898 * (candidate.metrics.circuit_depth as f64).sqrt()
899 / 100.0;
900 candidate.properties.expressivity = Some(expressivity.min(1.0));
901
902 let entanglement_layers = candidate
904 .layers
905 .iter()
906 .filter(|layer| matches!(layer, QNNLayerType::EntanglementLayer { .. }))
907 .count();
908 candidate.properties.entanglement_capability =
909 Some((entanglement_layers as f64 / candidate.layers.len() as f64).min(1.0));
910
911 candidate.properties.gradient_variance = Some(0.1 + 0.3 * rand::random::<f64>());
913 candidate.properties.barren_plateau_score = Some(0.2 + 0.6 * rand::random::<f64>());
914 candidate.properties.noise_resilience = Some(0.3 + 0.4 * rand::random::<f64>());
915
916 Ok(())
917 }
918
919 fn initialize_rl_agent(&mut self, agent_type: RLAgentType, learning_rate: f64) -> Result<()> {
921 let state_dim = 64; self.rl_state = Some(RLSearchState {
924 q_values: HashMap::new(),
925 policy_params: Array1::zeros(state_dim),
926 replay_buffer: Vec::new(),
927 current_state: Array1::zeros(state_dim),
928 });
929
930 Ok(())
931 }
932
933 fn architecture_to_state(&self, arch: &ArchitectureCandidate) -> Result<Array1<f64>> {
935 let mut state = Array1::zeros(64);
936
937 state[0] = arch.layers.len() as f64 / 20.0; state[1] = arch.num_qubits as f64 / 16.0; for (i, layer) in arch.layers.iter().enumerate().take(30) {
943 let layer_code = match layer {
944 QNNLayerType::EncodingLayer { .. } => 0.1,
945 QNNLayerType::VariationalLayer { .. } => 0.3,
946 QNNLayerType::EntanglementLayer { .. } => 0.5,
947 QNNLayerType::MeasurementLayer { .. } => 0.7,
948 };
949 state[2 + i] = layer_code;
950 }
951
952 Ok(state)
953 }
954
955 fn sample_random_action(&self, arch: &ArchitectureCandidate) -> Result<ArchitectureAction> {
957 let action_type = fastrand::usize(0..5);
958
959 match action_type {
960 0 => {
961 let layer_idx = fastrand::usize(0..self.search_space.layer_types.len());
962 Ok(ArchitectureAction::AddLayer(
963 self.search_space.layer_types[layer_idx].clone(),
964 ))
965 }
966 1 => {
967 if arch.layers.len() > 3 {
968 let layer_idx = fastrand::usize(1..arch.layers.len() - 1);
969 Ok(ArchitectureAction::RemoveLayer(layer_idx))
970 } else {
971 self.sample_random_action(arch)
972 }
973 }
974 2 => {
975 let layer_idx = fastrand::usize(0..arch.layers.len());
976 Ok(ArchitectureAction::ModifyLayer(layer_idx, HashMap::new()))
977 }
978 3 => {
979 let conn_idx = fastrand::usize(0..self.search_space.connectivity_patterns.len());
980 Ok(ArchitectureAction::ChangeConnectivity(
981 self.search_space.connectivity_patterns[conn_idx].clone(),
982 ))
983 }
984 _ => Ok(ArchitectureAction::Finish),
985 }
986 }
987
988 fn choose_best_action(&self, state: &Array1<f64>) -> Result<ArchitectureAction> {
990 Ok(ArchitectureAction::Finish)
992 }
993
994 fn apply_action(
996 &self,
997 arch: &ArchitectureCandidate,
998 action: &ArchitectureAction,
999 ) -> Result<(ArchitectureCandidate, f64, bool)> {
1000 let mut new_arch = arch.clone();
1001 let mut reward = 0.0;
1002 let mut done = false;
1003
1004 match action {
1005 ArchitectureAction::AddLayer(layer) => {
1006 if new_arch.layers.len() < self.search_space.depth_range.1 + 2 {
1007 let insert_pos = fastrand::usize(1..new_arch.layers.len());
1008 new_arch.layers.insert(insert_pos, layer.clone());
1009 reward = 0.1;
1010 } else {
1011 reward = -0.1;
1012 }
1013 }
1014 ArchitectureAction::RemoveLayer(idx) => {
1015 if new_arch.layers.len() > 3 && *idx < new_arch.layers.len() {
1016 new_arch.layers.remove(*idx);
1017 reward = 0.05;
1018 } else {
1019 reward = -0.1;
1020 }
1021 }
1022 ArchitectureAction::Finish => {
1023 done = true;
1024 reward = 1.0; }
1026 _ => {
1027 reward = 0.0;
1028 }
1029 }
1030
1031 new_arch.id = format!("rl_{}", fastrand::u64(..));
1032 Ok((new_arch, reward, done))
1033 }
1034
1035 fn train_rl_agent(&mut self) -> Result<()> {
1037 Ok(())
1039 }
1040
1041 fn create_empty_architecture(&self) -> ArchitectureCandidate {
1043 ArchitectureCandidate {
1044 id: format!("empty_{}", fastrand::u64(..)),
1045 layers: vec![
1046 QNNLayerType::EncodingLayer { num_features: 4 },
1047 QNNLayerType::MeasurementLayer {
1048 measurement_basis: "computational".to_string(),
1049 },
1050 ],
1051 num_qubits: 4,
1052 metrics: ArchitectureMetrics {
1053 accuracy: None,
1054 loss: None,
1055 circuit_depth: 0,
1056 parameter_count: 0,
1057 training_time: None,
1058 memory_usage: None,
1059 hardware_efficiency: None,
1060 },
1061 properties: ArchitectureProperties {
1062 expressivity: None,
1063 entanglement_capability: None,
1064 gradient_variance: None,
1065 barren_plateau_score: None,
1066 noise_resilience: None,
1067 },
1068 }
1069 }
1070
1071 fn fit_surrogate_model(&self, candidates: &[ArchitectureCandidate]) -> Result<SurrogateModel> {
1073 Ok(SurrogateModel {
1075 mean_prediction: 0.7,
1076 uncertainty: 0.1,
1077 })
1078 }
1079
1080 fn optimize_acquisition(
1082 &self,
1083 surrogate: &SurrogateModel,
1084 acquisition_fn: AcquisitionFunction,
1085 ) -> Result<ArchitectureCandidate> {
1086 self.sample_random_architecture()
1088 }
1089
1090 fn compute_architecture_gradients(&self, alpha: &Array2<f64>) -> Result<Array2<f64>> {
1092 Ok(Array2::zeros(alpha.raw_dim()))
1094 }
1095
1096 fn derive_architecture_from_weights(
1098 &self,
1099 alpha: &Array2<f64>,
1100 ) -> Result<ArchitectureCandidate> {
1101 let mut layers = vec![QNNLayerType::EncodingLayer { num_features: 4 }];
1102
1103 for i in 0..alpha.nrows() {
1104 let mut best_op = 0;
1106 let mut best_weight = alpha[[i, 0]];
1107
1108 for j in 1..alpha.ncols() {
1109 if alpha[[i, j]] > best_weight {
1110 best_weight = alpha[[i, j]];
1111 best_op = j;
1112 }
1113 }
1114
1115 if best_op < self.search_space.layer_types.len() {
1116 layers.push(self.search_space.layer_types[best_op].clone());
1117 }
1118 }
1119
1120 layers.push(QNNLayerType::MeasurementLayer {
1121 measurement_basis: "computational".to_string(),
1122 });
1123
1124 Ok(ArchitectureCandidate {
1125 id: format!("darts_{}", fastrand::u64(..)),
1126 layers,
1127 num_qubits: 4,
1128 metrics: ArchitectureMetrics {
1129 accuracy: None,
1130 loss: None,
1131 circuit_depth: 0,
1132 parameter_count: 0,
1133 training_time: None,
1134 memory_usage: None,
1135 hardware_efficiency: None,
1136 },
1137 properties: ArchitectureProperties {
1138 expressivity: None,
1139 entanglement_capability: None,
1140 gradient_variance: None,
1141 barren_plateau_score: None,
1142 noise_resilience: None,
1143 },
1144 })
1145 }
1146
1147 pub fn get_search_summary(&self) -> SearchSummary {
1149 SearchSummary {
1150 total_architectures_evaluated: self.search_history.len(),
1151 best_architecture: self.best_architectures.first().cloned(),
1152 pareto_front_size: self.pareto_front.len(),
1153 search_generations: self.current_generation,
1154 }
1155 }
1156
1157 pub fn get_pareto_front(&self) -> &[ArchitectureCandidate] {
1159 &self.pareto_front
1160 }
1161}
1162
1163#[derive(Debug, Clone)]
1165pub struct SurrogateModel {
1166 pub mean_prediction: f64,
1167 pub uncertainty: f64,
1168}
1169
1170#[derive(Debug, Clone)]
1172pub struct SearchSummary {
1173 pub total_architectures_evaluated: usize,
1174 pub best_architecture: Option<ArchitectureCandidate>,
1175 pub pareto_front_size: usize,
1176 pub search_generations: usize,
1177}
1178
1179impl fmt::Display for ArchitectureCandidate {
1180 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1181 write!(
1182 f,
1183 "Architecture {} (layers: {}, qubits: {}, accuracy: {:.3})",
1184 self.id,
1185 self.layers.len(),
1186 self.num_qubits,
1187 self.metrics.accuracy.unwrap_or(0.0)
1188 )
1189 }
1190}
1191
1192pub fn create_default_search_space() -> SearchSpace {
1194 SearchSpace {
1195 layer_types: vec![
1196 QNNLayerType::VariationalLayer { num_params: 6 },
1197 QNNLayerType::VariationalLayer { num_params: 9 },
1198 QNNLayerType::VariationalLayer { num_params: 12 },
1199 QNNLayerType::EntanglementLayer {
1200 connectivity: "circular".to_string(),
1201 },
1202 QNNLayerType::EntanglementLayer {
1203 connectivity: "full".to_string(),
1204 },
1205 ],
1206 depth_range: (2, 8),
1207 qubit_constraints: QubitConstraints {
1208 min_qubits: 3,
1209 max_qubits: 8,
1210 topology: Some(QuantumTopology::Complete),
1211 },
1212 param_ranges: vec![
1213 ("variational_params".to_string(), (3, 15)),
1214 ("encoding_features".to_string(), (2, 8)),
1215 ]
1216 .into_iter()
1217 .collect(),
1218 connectivity_patterns: vec![
1219 "linear".to_string(),
1220 "circular".to_string(),
1221 "full".to_string(),
1222 ],
1223 measurement_bases: vec![
1224 "computational".to_string(),
1225 "Pauli-Z".to_string(),
1226 "Pauli-X".to_string(),
1227 "Pauli-Y".to_string(),
1228 ],
1229 }
1230}
1231
1232#[cfg(test)]
1233mod tests {
1234 use super::*;
1235
1236 #[test]
1237 fn test_search_space_creation() {
1238 let search_space = create_default_search_space();
1239 assert!(search_space.layer_types.len() > 0);
1240 assert!(search_space.depth_range.0 < search_space.depth_range.1);
1241 assert!(
1242 search_space.qubit_constraints.min_qubits <= search_space.qubit_constraints.max_qubits
1243 );
1244 }
1245
1246 #[test]
1247 fn test_nas_initialization() {
1248 let search_space = create_default_search_space();
1249 let strategy = SearchStrategy::Random { num_samples: 10 };
1250 let nas = QuantumNAS::new(strategy, search_space);
1251
1252 assert_eq!(nas.current_generation, 0);
1253 assert_eq!(nas.best_architectures.len(), 0);
1254 }
1255
1256 #[test]
1257 fn test_random_architecture_sampling() {
1258 let search_space = create_default_search_space();
1259 let strategy = SearchStrategy::Random { num_samples: 10 };
1260 let nas = QuantumNAS::new(strategy, search_space);
1261
1262 let arch = nas.sample_random_architecture().unwrap();
1263 assert!(arch.layers.len() >= 2); assert!(arch.num_qubits >= nas.search_space.qubit_constraints.min_qubits);
1265 assert!(arch.num_qubits <= nas.search_space.qubit_constraints.max_qubits);
1266 }
1267
1268 #[test]
1269 fn test_fitness_computation() {
1270 let search_space = create_default_search_space();
1271 let strategy = SearchStrategy::Random { num_samples: 10 };
1272 let nas = QuantumNAS::new(strategy, search_space);
1273
1274 let mut arch = nas.sample_random_architecture().unwrap();
1275 arch.metrics.accuracy = Some(0.8);
1276 arch.metrics.parameter_count = 50;
1277 arch.metrics.circuit_depth = 10;
1278
1279 let fitness = nas.compute_fitness(&arch);
1280 assert!(fitness > 0.0);
1281 }
1282
1283 #[test]
1284 fn test_architecture_mutation() {
1285 let search_space = create_default_search_space();
1286 let strategy = SearchStrategy::Evolutionary {
1287 population_size: 10,
1288 mutation_rate: 0.1,
1289 crossover_rate: 0.7,
1290 elitism_ratio: 0.1,
1291 };
1292 let nas = QuantumNAS::new(strategy, search_space);
1293
1294 let mut arch = nas.sample_random_architecture().unwrap();
1295 let original_layers = arch.layers.len();
1296
1297 nas.mutate(&mut arch).unwrap();
1298
1299 assert!(arch.layers.len() >= 2);
1301 assert!(arch.num_qubits >= nas.search_space.qubit_constraints.min_qubits);
1302 }
1303}