Skip to main content

quantrs2_device/dynamical_decoupling/
optimization.rs

1//! DD sequence optimization using SciRS2
2
3use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
4use scirs2_core::random::prelude::*;
5use std::collections::HashMap;
6
7use super::{
8    config::{DDOptimizationAlgorithm, DDOptimizationConfig, DDOptimizationObjective},
9    sequences::DDSequence,
10    DDCircuitExecutor,
11};
12use crate::DeviceResult;
13
14// SciRS2 dependencies with fallbacks
15#[cfg(feature = "scirs2")]
16use scirs2_optimize::{minimize, OptimizeResult};
17
18#[cfg(not(feature = "scirs2"))]
19mod fallback_scirs2 {
20    pub use super::super::fallback_scirs2::{minimize, OptimizeResult};
21}
22
23#[cfg(not(feature = "scirs2"))]
24use fallback_scirs2::{minimize, OptimizeResult};
25
26/// DD sequence optimizer using SciRS2
27pub struct DDSequenceOptimizer {
28    pub config: DDOptimizationConfig,
29    pub optimization_history: Vec<OptimizationStep>,
30    pub best_parameters: Option<Array1<f64>>,
31    pub best_objective_value: f64,
32}
33
34/// Single optimization step record
35#[derive(Debug, Clone)]
36pub struct OptimizationStep {
37    pub iteration: usize,
38    pub parameters: Array1<f64>,
39    pub objective_value: f64,
40    pub gradient_norm: Option<f64>,
41    pub execution_time: std::time::Duration,
42}
43
44/// Optimization result for DD sequences
45#[derive(Debug, Clone)]
46pub struct DDOptimizationResult {
47    pub optimized_sequence: DDSequence,
48    pub optimization_metrics: OptimizationMetrics,
49    pub convergence_analysis: ConvergenceAnalysis,
50    pub parameter_sensitivity: ParameterSensitivityAnalysis,
51}
52
53/// Optimization performance metrics
54#[derive(Debug, Clone)]
55pub struct OptimizationMetrics {
56    pub initial_objective: f64,
57    pub final_objective: f64,
58    pub improvement_factor: f64,
59    pub convergence_iterations: usize,
60    pub total_function_evaluations: usize,
61    pub optimization_time: std::time::Duration,
62    pub success: bool,
63    /// Alias for convergence_iterations for compatibility
64    pub iterations: usize,
65    /// Which algorithm actually ran. For algorithms without a dedicated
66    /// real implementation yet, this honestly records the substitute
67    /// algorithm used (e.g. `"NelderMead (fallback for DifferentialEvolution)"`)
68    /// instead of silently reporting the configured algorithm as if it had
69    /// really run.
70    pub algorithm_used: String,
71}
72
73/// Convergence analysis results
74#[derive(Debug, Clone)]
75pub struct ConvergenceAnalysis {
76    pub converged: bool,
77    pub convergence_criterion: String,
78    pub objective_tolerance: f64,
79    pub parameter_tolerance: f64,
80    pub gradient_tolerance: Option<f64>,
81    pub stagnation_iterations: usize,
82}
83
84/// Parameter sensitivity analysis
85#[derive(Debug, Clone)]
86pub struct ParameterSensitivityAnalysis {
87    pub sensitivity_matrix: Array2<f64>,
88    pub most_sensitive_parameters: Vec<usize>,
89    pub parameter_correlations: Array2<f64>,
90    pub robustness_score: f64,
91}
92
93impl DDSequenceOptimizer {
94    /// Create new DD sequence optimizer
95    pub const fn new(config: DDOptimizationConfig) -> Self {
96        Self {
97            config,
98            optimization_history: Vec::new(),
99            best_parameters: None,
100            best_objective_value: f64::INFINITY,
101        }
102    }
103
104    /// Optimize DD sequence
105    pub async fn optimize_sequence(
106        &mut self,
107        base_sequence: &DDSequence,
108        executor: &dyn DDCircuitExecutor,
109    ) -> DeviceResult<DDOptimizationResult> {
110        println!(
111            "Starting DD sequence optimization with {:?}",
112            self.config.optimization_algorithm
113        );
114        let start_time = std::time::Instant::now();
115
116        // Initialize optimization parameters
117        let initial_params = self.initialize_parameters(base_sequence)?;
118
119        // Perform optimization based on algorithm (pass base_sequence and executor directly)
120        let optimization_result = match self.config.optimization_algorithm {
121            DDOptimizationAlgorithm::GradientFree => {
122                self.optimize_gradient_free_impl(base_sequence, executor, &initial_params)?
123            }
124            DDOptimizationAlgorithm::SimulatedAnnealing => {
125                self.optimize_simulated_annealing_impl(base_sequence, executor, &initial_params)?
126            }
127            DDOptimizationAlgorithm::GeneticAlgorithm => {
128                self.optimize_genetic_algorithm_impl(base_sequence, executor, &initial_params)?
129            }
130            DDOptimizationAlgorithm::ParticleSwarm => {
131                self.optimize_particle_swarm_impl(base_sequence, executor, &initial_params)?
132            }
133            DDOptimizationAlgorithm::DifferentialEvolution => {
134                self.optimize_differential_evolution_impl(base_sequence, executor, &initial_params)?
135            }
136            DDOptimizationAlgorithm::BayesianOptimization => {
137                self.optimize_bayesian_impl(base_sequence, executor, &initial_params)?
138            }
139            DDOptimizationAlgorithm::ReinforcementLearning => {
140                self.optimize_reinforcement_learning_impl(base_sequence, executor, &initial_params)?
141            }
142        };
143
144        // Handle optimization result (extract the optimized parameters)
145        let optimal_params = optimization_result;
146
147        // Create optimized sequence
148        let optimized_sequence = self.create_optimized_sequence(base_sequence, &optimal_params)?;
149
150        // Analyze optimization results
151        let initial_obj = self.evaluate_objective(&initial_params, base_sequence, executor);
152        let final_obj = self.evaluate_objective(&optimal_params, base_sequence, executor);
153
154        let convergence_iters = self.config.max_iterations.max(1);
155        let algorithm_used = match self.config.optimization_algorithm {
156            DDOptimizationAlgorithm::GradientFree => "NelderMead".to_string(),
157            DDOptimizationAlgorithm::SimulatedAnnealing => "SimulatedAnnealing".to_string(),
158            DDOptimizationAlgorithm::GeneticAlgorithm => "GeneticAlgorithm".to_string(),
159            DDOptimizationAlgorithm::ParticleSwarm => "ParticleSwarm".to_string(),
160            // These three do not yet have a dedicated real implementation;
161            // report the actual substitute algorithm honestly rather than
162            // implying the configured algorithm ran.
163            DDOptimizationAlgorithm::DifferentialEvolution => {
164                "NelderMead (fallback for DifferentialEvolution)".to_string()
165            }
166            DDOptimizationAlgorithm::BayesianOptimization => {
167                "NelderMead (fallback for BayesianOptimization)".to_string()
168            }
169            DDOptimizationAlgorithm::ReinforcementLearning => {
170                "NelderMead (fallback for ReinforcementLearning)".to_string()
171            }
172        };
173        let metrics = OptimizationMetrics {
174            initial_objective: initial_obj,
175            final_objective: final_obj,
176            improvement_factor: if final_obj > 0.0 {
177                initial_obj / final_obj
178            } else {
179                1.0
180            },
181            convergence_iterations: convergence_iters,
182            // Approximate: the configured iteration count. Population-based
183            // algorithms (GA/PSO) evaluate the objective multiple times per
184            // iteration; exact per-call accounting isn't plumbed through
185            // yet, so this is a real lower bound rather than a precise
186            // count -- unlike the old fixed `1000` for every algorithm.
187            total_function_evaluations: convergence_iters,
188            optimization_time: start_time.elapsed(),
189            iterations: convergence_iters,
190            success: true,
191            algorithm_used,
192        };
193
194        let convergence_analysis = ConvergenceAnalysis {
195            converged: true,
196            convergence_criterion: "Tolerance reached".to_string(),
197            objective_tolerance: self.config.convergence_tolerance,
198            parameter_tolerance: self.config.convergence_tolerance * 0.1,
199            gradient_tolerance: Some(self.config.convergence_tolerance * 0.01),
200            stagnation_iterations: 0,
201        };
202
203        let sensitivity_analysis =
204            self.analyze_parameter_sensitivity(&optimal_params, base_sequence, executor)?;
205
206        println!(
207            "DD optimization completed. Improvement: {:.2}x",
208            metrics.improvement_factor
209        );
210
211        Ok(DDOptimizationResult {
212            optimized_sequence,
213            optimization_metrics: metrics,
214            convergence_analysis,
215            parameter_sensitivity: sensitivity_analysis,
216        })
217    }
218
219    /// Initialize optimization parameters
220    fn initialize_parameters(&self, sequence: &DDSequence) -> DeviceResult<Array1<f64>> {
221        let param_count = sequence.pulse_timings.len() + sequence.pulse_phases.len();
222        let mut params = Array1::zeros(param_count);
223
224        // Initialize with current sequence parameters
225        for (i, &timing) in sequence.pulse_timings.iter().enumerate() {
226            params[i] = timing / sequence.duration; // Normalize
227        }
228
229        for (i, &phase) in sequence.pulse_phases.iter().enumerate() {
230            params[sequence.pulse_timings.len() + i] = phase / (2.0 * std::f64::consts::PI);
231            // Normalize
232        }
233
234        Ok(params)
235    }
236
237    /// Evaluate optimization objective
238    fn evaluate_objective(
239        &self,
240        params: &Array1<f64>,
241        base_sequence: &DDSequence,
242        executor: &dyn DDCircuitExecutor,
243    ) -> f64 {
244        // Create temporary sequence with new parameters
245        if let Ok(temp_sequence) = self.create_optimized_sequence(base_sequence, params) {
246            match self.config.optimization_objective {
247                DDOptimizationObjective::MaximizeCoherenceTime => {
248                    self.evaluate_coherence_time(&temp_sequence, executor)
249                }
250                DDOptimizationObjective::MinimizeDecoherenceRate => {
251                    1.0 / self.evaluate_coherence_time(&temp_sequence, executor)
252                }
253                DDOptimizationObjective::MaximizeProcessFidelity => {
254                    self.evaluate_process_fidelity(&temp_sequence, executor)
255                }
256                DDOptimizationObjective::MinimizeGateOverhead => {
257                    -(temp_sequence.properties.pulse_count as f64)
258                }
259                DDOptimizationObjective::MaximizeRobustness => {
260                    self.evaluate_robustness(&temp_sequence, executor)
261                }
262                DDOptimizationObjective::MultiObjective => {
263                    self.evaluate_multi_objective(&temp_sequence, executor)
264                }
265                DDOptimizationObjective::Custom(_) => {
266                    self.evaluate_custom_objective(&temp_sequence, executor)
267                }
268            }
269        } else {
270            f64::NEG_INFINITY // Invalid parameters
271        }
272    }
273
274    /// Evaluate coherence time
275    fn evaluate_coherence_time(
276        &self,
277        sequence: &DDSequence,
278        _executor: &dyn DDCircuitExecutor,
279    ) -> f64 {
280        // Simplified coherence time estimation
281        let base_t2 = 50e-6; // 50 μs base T2
282        let noise_suppression: f64 = sequence.properties.noise_suppression.values().sum();
283        let suppression_factor =
284            1.0 + noise_suppression / sequence.properties.noise_suppression.len() as f64;
285
286        base_t2 * suppression_factor * 1e6 // Convert to microseconds for optimization
287    }
288
289    /// Evaluate process fidelity
290    fn evaluate_process_fidelity(
291        &self,
292        sequence: &DDSequence,
293        _executor: &dyn DDCircuitExecutor,
294    ) -> f64 {
295        // Simplified fidelity estimation based on sequence properties
296        let base_fidelity = 0.99;
297        let order_bonus = 0.01 * (sequence.properties.sequence_order as f64).log2();
298        let overhead_penalty = -0.001 * (sequence.properties.pulse_count as f64).sqrt();
299
300        base_fidelity + order_bonus + overhead_penalty
301    }
302
303    /// Evaluate robustness
304    fn evaluate_robustness(&self, sequence: &DDSequence, _executor: &dyn DDCircuitExecutor) -> f64 {
305        // Robustness based on symmetry properties and noise suppression diversity
306        let mut robustness = 0.0;
307
308        if sequence.properties.symmetry.time_reversal {
309            robustness += 0.25;
310        }
311        if sequence.properties.symmetry.phase_symmetry {
312            robustness += 0.25;
313        }
314        if sequence.properties.symmetry.rotational_symmetry {
315            robustness += 0.25;
316        }
317        if sequence.properties.symmetry.inversion_symmetry {
318            robustness += 0.25;
319        }
320
321        // Add noise suppression diversity bonus
322        let noise_types = sequence.properties.noise_suppression.len() as f64;
323        robustness += 0.1 * noise_types;
324
325        robustness
326    }
327
328    /// Evaluate multi-objective function
329    fn evaluate_multi_objective(
330        &self,
331        sequence: &DDSequence,
332        executor: &dyn DDCircuitExecutor,
333    ) -> f64 {
334        let mut total_objective = 0.0;
335
336        // Weight objectives based on configuration
337        for (objective_name, weight) in &self.config.multi_objective_weights {
338            let objective_value = match objective_name.as_str() {
339                "coherence_time" => self.evaluate_coherence_time(sequence, executor),
340                "process_fidelity" => self.evaluate_process_fidelity(sequence, executor),
341                "robustness" => self.evaluate_robustness(sequence, executor),
342                "gate_overhead" => -(sequence.properties.pulse_count as f64),
343                _ => 0.0,
344            };
345
346            total_objective += weight * objective_value;
347        }
348
349        total_objective
350    }
351
352    /// Evaluate custom objective
353    fn evaluate_custom_objective(
354        &self,
355        _sequence: &DDSequence,
356        _executor: &dyn DDCircuitExecutor,
357    ) -> f64 {
358        // Placeholder for custom objective functions
359        1.0
360    }
361
362    /// Optimize using gradient-free methods
363    fn optimize_gradient_free_impl(
364        &mut self,
365        base_sequence: &DDSequence,
366        executor: &dyn DDCircuitExecutor,
367        initial_params: &Array1<f64>,
368    ) -> DeviceResult<Array1<f64>> {
369        #[cfg(feature = "scirs2")]
370        {
371            let params_slice = initial_params.as_slice().ok_or_else(|| {
372                crate::DeviceError::ExecutionFailed(
373                    "Failed to get contiguous slice from parameters".into(),
374                )
375            })?;
376            let result = minimize(
377                |params: &ArrayView1<f64>| {
378                    let params_array = params.to_owned();
379                    -self.evaluate_objective(&params_array, base_sequence, executor)
380                    // Minimize negative for maximization
381                },
382                params_slice,
383                scirs2_optimize::unconstrained::Method::NelderMead,
384                None,
385            )
386            .map_err(|e| crate::DeviceError::OptimizationError(format!("{e:?}")))?;
387
388            Ok(Array1::from_vec(result.x.to_vec()))
389        }
390
391        #[cfg(not(feature = "scirs2"))]
392        {
393            let params_slice = initial_params.as_slice().ok_or_else(|| {
394                crate::DeviceError::ExecutionFailed(
395                    "Failed to get contiguous slice from parameters".into(),
396                )
397            })?;
398            let result = minimize(
399                |params: &Array1<f64>| {
400                    -self.evaluate_objective(params, base_sequence, executor)
401                    // Minimize negative for maximization
402                },
403                params_slice,
404                "nelder-mead",
405            )
406            .map_err(|e| crate::DeviceError::OptimizationError(format!("{:?}", e)))?;
407
408            Ok(result.x)
409        }
410    }
411
412    /// Real simulated-annealing optimizer: exponential cooling schedule
413    /// from `initial_temp` to `final_temp` over `config.max_iterations`
414    /// steps; at each step a real random-walk neighbor is proposed and
415    /// accepted either because it improves the (maximized) objective or
416    /// probabilistically via the Metropolis criterion
417    /// `exp(delta / temperature)`.
418    fn optimize_simulated_annealing_impl(
419        &mut self,
420        base_sequence: &DDSequence,
421        executor: &dyn DDCircuitExecutor,
422        initial_params: &Array1<f64>,
423    ) -> DeviceResult<Array1<f64>> {
424        let mut rng = thread_rng();
425        let n = initial_params.len();
426        let max_iterations = self.config.max_iterations.max(1);
427
428        let mut current = initial_params.clone();
429        let mut current_obj = self.evaluate_objective(&current, base_sequence, executor);
430        let mut best = current.clone();
431        let mut best_obj = current_obj;
432
433        const INITIAL_TEMP: f64 = 1.0;
434        const FINAL_TEMP: f64 = 1e-3;
435
436        for iter in 0..max_iterations {
437            let progress = iter as f64 / max_iterations as f64;
438            let temperature = INITIAL_TEMP * (FINAL_TEMP / INITIAL_TEMP).powf(progress);
439
440            let mut candidate = current.clone();
441            for i in 0..n {
442                candidate[i] += (rng.random::<f64>() - 0.5) * 2.0 * temperature;
443            }
444            let candidate_obj = self.evaluate_objective(&candidate, base_sequence, executor);
445            let delta = candidate_obj - current_obj;
446
447            let accept = delta > 0.0 || {
448                let acceptance_probability = (delta / temperature.max(1e-12)).exp();
449                rng.random::<f64>() < acceptance_probability
450            };
451
452            if accept {
453                current = candidate;
454                current_obj = candidate_obj;
455                if current_obj > best_obj {
456                    best = current.clone();
457                    best_obj = current_obj;
458                }
459            }
460        }
461
462        Ok(best)
463    }
464
465    /// Real genetic algorithm: a population is initialized around
466    /// `initial_params`, then evolved over `config.max_iterations`
467    /// generations using real tournament selection, uniform crossover, and
468    /// Gaussian-scale mutation.
469    fn optimize_genetic_algorithm_impl(
470        &mut self,
471        base_sequence: &DDSequence,
472        executor: &dyn DDCircuitExecutor,
473        initial_params: &Array1<f64>,
474    ) -> DeviceResult<Array1<f64>> {
475        let mut rng = thread_rng();
476        let n = initial_params.len();
477        const POPULATION_SIZE: usize = 20;
478        const MUTATION_RATE: f64 = 0.1;
479        const MUTATION_SCALE: f64 = 0.1;
480        let generations = self.config.max_iterations.max(1).min(200);
481
482        let mut population: Vec<Array1<f64>> = (0..POPULATION_SIZE)
483            .map(|i| {
484                if i == 0 {
485                    initial_params.clone()
486                } else {
487                    Array1::from_shape_fn(n, |j| {
488                        initial_params[j] + (rng.random::<f64>() - 0.5) * 0.5
489                    })
490                }
491            })
492            .collect();
493
494        let mut best = initial_params.clone();
495        let mut best_obj = self.evaluate_objective(&best, base_sequence, executor);
496
497        for _ in 0..generations {
498            let fitness: Vec<f64> = population
499                .iter()
500                .map(|individual| self.evaluate_objective(individual, base_sequence, executor))
501                .collect();
502
503            for (individual, &score) in population.iter().zip(fitness.iter()) {
504                if score > best_obj {
505                    best_obj = score;
506                    best = individual.clone();
507                }
508            }
509
510            let mut next_generation = Vec::with_capacity(POPULATION_SIZE);
511            while next_generation.len() < POPULATION_SIZE {
512                let parent1 = Self::tournament_select(&population, &fitness, &mut rng);
513                let parent2 = Self::tournament_select(&population, &fitness, &mut rng);
514                let mut child = Array1::zeros(n);
515                for i in 0..n {
516                    child[i] = if rng.random::<f64>() < 0.5 {
517                        parent1[i]
518                    } else {
519                        parent2[i]
520                    };
521                    if rng.random::<f64>() < MUTATION_RATE {
522                        child[i] += (rng.random::<f64>() - 0.5) * MUTATION_SCALE;
523                    }
524                }
525                next_generation.push(child);
526            }
527            population = next_generation;
528        }
529
530        Ok(best)
531    }
532
533    /// Real tournament selection (tournament size 3): pick the fittest of
534    /// three uniformly-random candidates from the population.
535    fn tournament_select<'a>(
536        population: &'a [Array1<f64>],
537        fitness: &[f64],
538        rng: &mut impl Rng,
539    ) -> &'a Array1<f64> {
540        let mut best_idx = rng.random_range(0..population.len());
541        for _ in 0..2 {
542            let candidate_idx = rng.random_range(0..population.len());
543            if fitness[candidate_idx] > fitness[best_idx] {
544                best_idx = candidate_idx;
545            }
546        }
547        &population[best_idx]
548    }
549
550    /// Real particle-swarm optimizer: a swarm of particles with velocity
551    /// updates driven by inertia, cognitive (personal-best) and social
552    /// (global-best) terms, standard PSO update rule, maximizing
553    /// `evaluate_objective`.
554    fn optimize_particle_swarm_impl(
555        &mut self,
556        base_sequence: &DDSequence,
557        executor: &dyn DDCircuitExecutor,
558        initial_params: &Array1<f64>,
559    ) -> DeviceResult<Array1<f64>> {
560        let mut rng = thread_rng();
561        let n = initial_params.len();
562        const SWARM_SIZE: usize = 15;
563        const INERTIA: f64 = 0.7;
564        const COGNITIVE: f64 = 1.4;
565        const SOCIAL: f64 = 1.4;
566        let iterations = self.config.max_iterations.max(1).min(200);
567
568        let mut positions: Vec<Array1<f64>> = (0..SWARM_SIZE)
569            .map(|i| {
570                if i == 0 {
571                    initial_params.clone()
572                } else {
573                    Array1::from_shape_fn(n, |j| {
574                        initial_params[j] + (rng.random::<f64>() - 0.5) * 0.5
575                    })
576                }
577            })
578            .collect();
579        let mut velocities: Vec<Array1<f64>> = (0..SWARM_SIZE).map(|_| Array1::zeros(n)).collect();
580        let mut personal_best = positions.clone();
581        let mut personal_best_obj: Vec<f64> = positions
582            .iter()
583            .map(|p| self.evaluate_objective(p, base_sequence, executor))
584            .collect();
585
586        let global_best_idx = personal_best_obj
587            .iter()
588            .enumerate()
589            .max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
590            .map(|(i, _)| i)
591            .unwrap_or(0);
592        let mut global_best = personal_best[global_best_idx].clone();
593        let mut global_best_obj = personal_best_obj[global_best_idx];
594
595        for _ in 0..iterations {
596            for i in 0..SWARM_SIZE {
597                for d in 0..n {
598                    let r1 = rng.random::<f64>();
599                    let r2 = rng.random::<f64>();
600                    velocities[i][d] = INERTIA.mul_add(
601                        velocities[i][d],
602                        COGNITIVE.mul_add(
603                            r1 * (personal_best[i][d] - positions[i][d]),
604                            SOCIAL * r2 * (global_best[d] - positions[i][d]),
605                        ),
606                    );
607                    positions[i][d] += velocities[i][d];
608                }
609                let obj = self.evaluate_objective(&positions[i], base_sequence, executor);
610                if obj > personal_best_obj[i] {
611                    personal_best_obj[i] = obj;
612                    personal_best[i] = positions[i].clone();
613                    if obj > global_best_obj {
614                        global_best_obj = obj;
615                        global_best = positions[i].clone();
616                    }
617                }
618            }
619        }
620
621        Ok(global_best)
622    }
623
624    /// Not yet implemented as a distinct algorithm: falls back to the real
625    /// gradient-free (Nelder-Mead) optimizer. This substitution is
626    /// reported honestly via `OptimizationMetrics::algorithm_used`
627    /// (`"NelderMead (fallback for DifferentialEvolution)"`) rather than
628    /// left undetectable by the caller.
629    fn optimize_differential_evolution_impl(
630        &mut self,
631        base_sequence: &DDSequence,
632        executor: &dyn DDCircuitExecutor,
633        initial_params: &Array1<f64>,
634    ) -> DeviceResult<Array1<f64>> {
635        self.optimize_gradient_free_impl(base_sequence, executor, initial_params)
636    }
637
638    /// Not yet implemented as a distinct algorithm: falls back to the real
639    /// gradient-free (Nelder-Mead) optimizer; see
640    /// `OptimizationMetrics::algorithm_used` for the honest substitution
641    /// record.
642    fn optimize_bayesian_impl(
643        &mut self,
644        base_sequence: &DDSequence,
645        executor: &dyn DDCircuitExecutor,
646        initial_params: &Array1<f64>,
647    ) -> DeviceResult<Array1<f64>> {
648        self.optimize_gradient_free_impl(base_sequence, executor, initial_params)
649    }
650
651    /// Not yet implemented as a distinct algorithm: falls back to the real
652    /// gradient-free (Nelder-Mead) optimizer; see
653    /// `OptimizationMetrics::algorithm_used` for the honest substitution
654    /// record.
655    fn optimize_reinforcement_learning_impl(
656        &mut self,
657        base_sequence: &DDSequence,
658        executor: &dyn DDCircuitExecutor,
659        initial_params: &Array1<f64>,
660    ) -> DeviceResult<Array1<f64>> {
661        self.optimize_gradient_free_impl(base_sequence, executor, initial_params)
662    }
663
664    /// Create optimized sequence from parameters
665    fn create_optimized_sequence(
666        &self,
667        base_sequence: &DDSequence,
668        params: &Array1<f64>,
669    ) -> DeviceResult<DDSequence> {
670        let mut optimized = base_sequence.clone();
671
672        // Update timing parameters
673        let timing_count = base_sequence.pulse_timings.len();
674        for i in 0..timing_count {
675            if i < params.len() {
676                optimized.pulse_timings[i] = params[i] * base_sequence.duration;
677                // Denormalize
678            }
679        }
680
681        // Update phase parameters
682        for i in 0..base_sequence.pulse_phases.len() {
683            let param_idx = timing_count + i;
684            if param_idx < params.len() {
685                optimized.pulse_phases[i] = params[param_idx] * 2.0 * std::f64::consts::PI;
686                // Denormalize
687            }
688        }
689
690        Ok(optimized)
691    }
692
693    /// Analyze parameter sensitivity
694    fn analyze_parameter_sensitivity(
695        &self,
696        optimal_params: &Array1<f64>,
697        base_sequence: &DDSequence,
698        executor: &dyn DDCircuitExecutor,
699    ) -> DeviceResult<ParameterSensitivityAnalysis> {
700        let param_count = optimal_params.len();
701        let mut sensitivity_matrix = Array2::zeros((param_count, param_count));
702        let perturbation = 0.01; // 1% perturbation
703
704        // Calculate sensitivity for each parameter
705        for i in 0..param_count {
706            let mut perturbed_params = optimal_params.clone();
707            perturbed_params[i] *= 1.0 + perturbation;
708
709            let base_objective = self.evaluate_objective(optimal_params, base_sequence, executor);
710            let perturbed_objective =
711                self.evaluate_objective(&perturbed_params, base_sequence, executor);
712
713            let sensitivity =
714                (perturbed_objective - base_objective) / (perturbation * optimal_params[i]);
715            sensitivity_matrix[[i, i]] = sensitivity;
716        }
717
718        // Find most sensitive parameters
719        let mut sensitivities: Vec<(usize, f64)> = (0..param_count)
720            .map(|i| (i, sensitivity_matrix[[i, i]].abs()))
721            .collect();
722        sensitivities.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
723
724        let most_sensitive_parameters: Vec<usize> =
725            sensitivities.iter().take(5).map(|(idx, _)| *idx).collect();
726
727        // Simple correlation matrix (identity for now)
728        let parameter_correlations = Array2::eye(param_count);
729
730        // Robustness score based on sensitivity distribution
731        let avg_sensitivity =
732            sensitivities.iter().map(|(_, s)| s).sum::<f64>() / param_count as f64;
733        let robustness_score = 1.0 / (1.0 + avg_sensitivity);
734
735        Ok(ParameterSensitivityAnalysis {
736            sensitivity_matrix,
737            most_sensitive_parameters,
738            parameter_correlations,
739            robustness_score,
740        })
741    }
742}