Skip to main content

optirs_learned/
lstm.rs

1// LSTM-based Neural Optimizer
2//
3// This module implements a learned optimizer using LSTM networks to adaptively
4// update optimization parameters. The LSTM learns optimization strategies through
5// meta-learning, enabling automatic discovery of effective optimization patterns.
6
7use scirs2_core::ndarray::{s, Array, Array1, Array2, ArrayBase, Data, Dimension};
8use scirs2_core::numeric::Float;
9use scirs2_core::random::{rngs::StdRng, seeded_rng, thread_rng, CoreRandom};
10use std::collections::{HashMap, VecDeque};
11use std::fmt::Debug;
12
13use super::LearnedOptimizerConfig;
14use crate::common::cast_scalar;
15use crate::error::{OptimError, Result};
16
17pub mod bptt;
18pub mod components;
19pub mod features;
20pub mod introspection;
21pub mod trainer;
22
23pub use bptt::{
24    BpttConfig, DiagonalQuadraticTask, FrozenRollout, MetaTrainingTask, NetworkGradients,
25};
26pub use features::build_lstm_features;
27pub use trainer::MetaTrainer;
28
29/// LSTM-based neural optimizer with meta-learning capabilities
30#[derive(Debug)]
31pub struct LSTMOptimizer<T: Float + Debug + Send + Sync + 'static> {
32    /// Configuration for the LSTM optimizer
33    config: LearnedOptimizerConfig,
34
35    /// LSTM network architecture
36    lstm_network: LSTMNetwork<T>,
37
38    /// Gradient and parameter history for context
39    history_buffer: HistoryBuffer<T>,
40
41    /// Meta-learning components
42    meta_learner: MetaLearner<T>,
43
44    /// Adaptive learning rate controller
45    lr_controller: AdaptiveLearningRateController<T>,
46
47    /// Optimization state tracker
48    state_tracker: OptimizationStateTracker<T>,
49
50    /// Performance metrics
51    metrics: LSTMOptimizerMetrics,
52
53    /// Current optimization step
54    step_count: usize,
55}
56
57/// LSTM network architecture for optimization
58#[derive(Debug, Clone)]
59pub struct LSTMNetwork<T: Float + Debug + Send + Sync + 'static> {
60    /// LSTM layers
61    layers: Vec<LSTMLayer<T>>,
62
63    /// Output projection layer
64    output_projection: OutputProjection<T>,
65
66    /// Attention mechanism (optional)
67    attention: Option<AttentionMechanism<T>>,
68
69    /// Normalization layers
70    layer_norms: Vec<LayerNormalization<T>>,
71
72    /// Dropout for regularization
73    dropout_rate: f64,
74
75    /// Whether dropout is active.
76    ///
77    /// Defaults to `false` (evaluation mode). Dropout used to be applied
78    /// unconditionally whenever `dropout_rate > 0.0`, which made
79    /// `LSTMOptimizer::lstm_step` non-deterministic and injected training-time
80    /// noise into deployed updates. It also made meta-training impossible to
81    /// verify: the truncated-BPTT gradient in [`bptt`] is the gradient of a
82    /// *deterministic* rollout. Call [`LSTMNetwork::set_training`] to turn it on
83    /// deliberately.
84    training: bool,
85}
86
87/// Individual LSTM layer
88#[derive(Debug, Clone)]
89pub struct LSTMLayer<T: Float + Debug + Send + Sync + 'static> {
90    /// Input-to-hidden weights (for i, f, g, o gates)
91    weight_ih: Array2<T>,
92
93    /// Hidden-to-hidden weights (for i, f, g, o gates)
94    weight_hh: Array2<T>,
95
96    /// Input biases
97    bias_ih: Array1<T>,
98
99    /// Hidden biases
100    bias_hh: Array1<T>,
101
102    /// Hidden state
103    hidden_state: Array1<T>,
104
105    /// Cell state
106    cell_state: Array1<T>,
107
108    /// Hidden size
109    hiddensize: usize,
110}
111
112/// Output projection for generating parameter updates
113#[derive(Debug, Clone)]
114pub struct OutputProjection<T: Float + Debug + Send + Sync + 'static> {
115    /// Projection weights
116    weights: Array2<T>,
117
118    /// Projection biases
119    bias: Array1<T>,
120
121    /// Output transformation
122    output_transform: OutputTransform,
123}
124
125impl<T: Float + Debug + Send + Sync + 'static + Default + Clone> OutputProjection<T> {
126    /// Create a new output projection with Xavier-initialized weights.
127    ///
128    /// The output-side transform (tanh / scaling / ...) named by
129    /// `output_transform` is applied by the caller (see
130    /// `LSTMOptimizer::generate_updates`); this projection itself computes the
131    /// underlying linear map `W x + b`.
132    pub fn new(
133        input_size: usize,
134        output_size: usize,
135        output_transform: OutputTransform,
136    ) -> Result<Self> {
137        Self::new_with_rng(
138            input_size,
139            output_size,
140            output_transform,
141            &mut seeded_rng(thread_rng().random::<u64>()),
142        )
143    }
144
145    /// [`Self::new`] with the caller's generator, for reproducible construction
146    /// (see [`LSTMNetwork::new_seeded`]).
147    pub(crate) fn new_with_rng(
148        input_size: usize,
149        output_size: usize,
150        output_transform: OutputTransform,
151        rng: &mut CoreRandom<StdRng>,
152    ) -> Result<Self> {
153        // Xavier/Glorot *uniform* limit is sqrt(6 / (fan_in + fan_out)); the
154        // sqrt(2 / ...) that used to be here is the limit for a Xavier *normal*
155        // draw, and `xavier_init` samples uniformly. Uniform[-b, b] has variance
156        // b^2/3, so the old constant gave exactly one third of the intended
157        // variance in every LSTM weight matrix in this file (finding F64).
158        let limit = LSTMLayer::<T>::xavier_limit(input_size, output_size);
159        let weights = LSTMLayer::<T>::xavier_init(rng, output_size, input_size, limit);
160        let bias = Array1::zeros(output_size);
161
162        Ok(Self {
163            weights,
164            bias,
165            output_transform,
166        })
167    }
168
169    /// Forward pass through output projection: `output = W * input + b`.
170    pub fn forward(&self, input: &Array1<T>) -> Result<Array1<T>> {
171        if input.len() != self.weights.ncols() {
172            return Err(OptimError::InvalidConfig(format!(
173                "OutputProjection expected {} input features, got {}",
174                self.weights.ncols(),
175                input.len()
176            )));
177        }
178        Ok(self.weights.dot(input) + &self.bias)
179    }
180
181    /// Re-randomize the projection weights and zero the bias.
182    ///
183    /// Used when the target output dimension changes at runtime (e.g. the
184    /// flattened parameter count the optimizer is asked to update does not
185    /// match this projection's configured `output_features`): a fresh Xavier
186    /// draw at the new shape is the honest response, since there is no
187    /// meaningful way to reuse weights trained for a different output size.
188    pub fn reset(&mut self, input_size: usize, output_size: usize) {
189        let limit = LSTMLayer::<T>::xavier_limit(input_size, output_size);
190        // A runtime reshape is inherently a fresh draw; entropy seeding is the
191        // honest choice here (reproducible runs should avoid triggering it).
192        let mut rng = seeded_rng(thread_rng().random::<u64>());
193        self.weights = LSTMLayer::<T>::xavier_init(&mut rng, output_size, input_size, limit);
194        self.bias = Array1::zeros(output_size);
195    }
196
197    /// Current output dimension.
198    pub fn output_size(&self) -> usize {
199        self.weights.nrows()
200    }
201}
202
203/// Output transformation types
204#[derive(Debug, Clone, Copy)]
205pub enum OutputTransform {
206    /// Direct output (no transformation)
207    Identity,
208
209    /// Tanh activation
210    Tanh,
211
212    /// Scaled tanh for bounded updates
213    ScaledTanh { scale: f64 },
214
215    /// Adaptive scaling based on gradient norms
216    AdaptiveScale,
217
218    /// Learned nonlinear transformation
219    LearnedNonlinear,
220}
221
222/// Attention mechanism for focusing on relevant history
223#[derive(Debug, Clone)]
224pub struct AttentionMechanism<T: Float + Debug + Send + Sync + 'static> {
225    /// Query projection
226    query_proj: Array2<T>,
227
228    /// Key projection
229    key_proj: Array2<T>,
230
231    /// Value projection
232    value_proj: Array2<T>,
233
234    /// Output projection
235    output_proj: Array2<T>,
236
237    /// Number of attention heads
238    num_heads: usize,
239
240    /// Attention head size
241    head_size: usize,
242
243    /// Attention weights from last forward pass
244    attentionweights: Option<Array2<T>>,
245
246    /// Raw pre-softmax logits from the last forward pass, one per head.
247    ///
248    /// At sequence length 1 the softmax weights are identically `1.0` for every
249    /// head, so they carry no information about how the heads differ. The scaled
250    /// dot products `qₕ·kₕ / √d` do, which is what
251    /// [`LSTMOptimizer::compute_attention_stats`] measures head diversity from.
252    head_logits: Option<Array1<T>>,
253}
254
255impl<T: Float + Debug + Send + Sync + 'static + Default + Clone> AttentionMechanism<T> {
256    /// Create a new attention mechanism with Xavier-initialized projections.
257    pub fn new(config: &LearnedOptimizerConfig) -> Result<Self> {
258        Self::new_with_rng(config, &mut seeded_rng(thread_rng().random::<u64>()))
259    }
260
261    /// [`Self::new`] with the caller's generator, for reproducible construction
262    /// (see [`LSTMNetwork::new_seeded`]).
263    pub(crate) fn new_with_rng(
264        config: &LearnedOptimizerConfig,
265        rng: &mut CoreRandom<StdRng>,
266    ) -> Result<Self> {
267        let hiddensize = config.hidden_size;
268        let num_heads = config.attention_heads.max(1);
269        if !hiddensize.is_multiple_of(num_heads) {
270            return Err(OptimError::InvalidConfig(format!(
271                "attention_heads ({num_heads}) must evenly divide hidden_size ({hiddensize})"
272            )));
273        }
274        let head_size = hiddensize / num_heads;
275        // Square projections: fan_in == fan_out == hiddensize.
276        let limit = LSTMLayer::<T>::xavier_limit(hiddensize, hiddensize);
277
278        Ok(Self {
279            query_proj: LSTMLayer::<T>::xavier_init(rng, hiddensize, hiddensize, limit),
280            key_proj: LSTMLayer::<T>::xavier_init(rng, hiddensize, hiddensize, limit),
281            value_proj: LSTMLayer::<T>::xavier_init(rng, hiddensize, hiddensize, limit),
282            output_proj: LSTMLayer::<T>::xavier_init(rng, hiddensize, hiddensize, limit),
283            num_heads,
284            head_size,
285            attentionweights: None,
286            head_logits: None,
287        })
288    }
289
290    /// Scaled dot-product self-attention.
291    ///
292    /// The optimizer calls this once per optimization step with a *single*
293    /// hidden-state vector (there is no sequence axis at this call site), so
294    /// per head the attention distribution is always over exactly one key and
295    /// therefore softmaxes to the constant `1.0`: the attended value degenerates
296    /// to `V` itself. The formula is exactly standard SDPA at sequence length 1
297    /// rather than an ad hoc shortcut.
298    ///
299    /// The degenerate weights are recorded in the private `attentionweights`
300    /// field, and the *pre-softmax* scaled dot products — which, unlike the
301    /// weights, do vary with the input and between heads — in `head_logits`, so
302    /// the optimizer's attention statistics (surfaced through
303    /// [`LSTMOptimizer::get_metrics`]) have something informative to report for
304    /// head diversity.
305    pub fn forward(&mut self, input: &Array1<T>) -> Result<Array1<T>> {
306        let dim = self.query_proj.nrows();
307        if input.len() != dim {
308            return Err(OptimError::InvalidConfig(format!(
309                "AttentionMechanism expected {dim} features, got {}",
310                input.len()
311            )));
312        }
313
314        let query = self.query_proj.dot(input);
315        let key = self.key_proj.dot(input);
316        let value = self.value_proj.dot(input);
317
318        let head_scale = T::one()
319            / scirs2_core::numeric::NumCast::from(self.head_size)
320                .unwrap_or_else(T::one)
321                .sqrt();
322        let mut weights = Array2::zeros((self.num_heads, 1));
323        let mut logits = Array1::zeros(self.num_heads);
324        for h in 0..self.num_heads {
325            let start = h * self.head_size;
326            let end = start + self.head_size;
327            let score = query
328                .slice(s![start..end])
329                .iter()
330                .zip(key.slice(s![start..end]).iter())
331                .fold(T::zero(), |acc, (&q, &k)| acc + q * k)
332                * head_scale;
333            logits[h] = score;
334            // softmax over a single logit: exp(score) / exp(score) == 1,
335            // independent of `score`'s value. The score itself is kept in
336            // `head_logits` because it is the only per-head quantity that
337            // survives the degenerate softmax.
338            weights[[h, 0]] = T::one();
339        }
340        self.attentionweights = Some(weights);
341        self.head_logits = Some(logits);
342
343        Ok(self.output_proj.dot(&value))
344    }
345}
346
347/// Layer normalization for stable training
348#[derive(Debug, Clone)]
349pub struct LayerNormalization<T: Float + Debug + Send + Sync + 'static> {
350    /// Scale parameters
351    gamma: Array1<T>,
352
353    /// Shift parameters
354    beta: Array1<T>,
355
356    /// Epsilon for numerical stability
357    epsilon: T,
358}
359
360impl<T: Float + Debug + Send + Sync + 'static + Default + Clone> LayerNormalization<T> {
361    /// Create a new layer normalization
362    pub fn new(features: usize) -> Result<Self> {
363        Ok(Self {
364            gamma: Array1::ones(features),
365            beta: Array1::zeros(features),
366            epsilon: scirs2_core::numeric::NumCast::from(1e-5).unwrap_or_else(|| T::zero()),
367        })
368    }
369
370    /// Forward pass through layer normalization: standardise `input` to zero
371    /// mean / unit variance across its features, then apply the learned
372    /// affine transform `gamma * x_hat + beta`.
373    pub fn forward(&self, input: &Array1<T>) -> Result<Array1<T>> {
374        if input.len() != self.gamma.len() {
375            return Err(OptimError::InvalidConfig(format!(
376                "LayerNormalization expected {} features, got {}",
377                self.gamma.len(),
378                input.len()
379            )));
380        }
381        if input.is_empty() {
382            return Ok(input.clone());
383        }
384        let n = scirs2_core::numeric::NumCast::from(input.len()).unwrap_or_else(T::one);
385        let mean = input.iter().copied().fold(T::zero(), |a, b| a + b) / n;
386        let variance = input
387            .iter()
388            .map(|&x| (x - mean) * (x - mean))
389            .fold(T::zero(), |a, b| a + b)
390            / n;
391        let inv_std = T::one() / (variance + self.epsilon).sqrt();
392
393        let mut output = Array1::zeros(input.len());
394        for i in 0..input.len() {
395            output[i] = (input[i] - mean) * inv_std * self.gamma[i] + self.beta[i];
396        }
397        Ok(output)
398    }
399}
400
401/// History buffer for maintaining context
402#[derive(Debug, Clone)]
403pub struct HistoryBuffer<T: Float + Debug + Send + Sync + 'static> {
404    /// Gradient history
405    gradients: VecDeque<Array1<T>>,
406
407    /// Parameter history
408    parameters: VecDeque<Array1<T>>,
409
410    /// Loss history
411    losses: VecDeque<T>,
412
413    /// Maximum history length
414    _maxlength: usize,
415
416    /// Preprocessed features cache
417    feature_cache: Option<Array2<T>>,
418}
419
420/// Meta-learning component for optimizer adaptation.
421///
422/// `MetaLearner::step` implements one fixed rule: a truncated-BPTT unroll of
423/// the controller over each task, i.e. the second-order / MAML-style inner loop.
424/// It carried a `strategy: MetaOptimizationStrategy` field that was hard-wired to
425/// `MAML` at construction and never consulted, which advertised a choice the
426/// implementation does not offer; the alternatives (Reptile, first-order,
427/// custom) are not implemented for this controller.
428#[derive(Debug, Clone)]
429pub struct MetaLearner<T: Float + Debug + Send + Sync + 'static> {
430    /// Meta-parameters (optimizer parameters)
431    meta_parameters: HashMap<String, Array1<T>>,
432
433    /// Meta-gradients accumulator
434    meta_gradients: HashMap<String, Array1<T>>,
435
436    /// Task history for meta-learning
437    task_history: VecDeque<MetaTask<T>>,
438
439    /// Meta-learning state
440    meta_state: MetaLearningState<T>,
441
442    /// Transfer learning capabilities
443    transfer_learner: TransferLearner<T>,
444}
445
446/// Meta-learning task
447#[derive(Debug, Clone)]
448pub struct MetaTask<T: Float + Debug + Send + Sync + 'static> {
449    /// Task identifier
450    pub id: String,
451
452    /// Task type
453    pub task_type: TaskType,
454
455    /// Training trajectory
456    pub training_trajectory: Vec<TrajectoryPoint<T>>,
457
458    /// Final performance
459    pub final_performance: T,
460
461    /// Task characteristics
462    pub characteristics: TaskCharacteristics<T>,
463
464    /// Task weight for meta-learning
465    pub weight: T,
466}
467
468/// Types of optimization tasks
469#[derive(Debug, Clone, Copy)]
470pub enum TaskType {
471    /// Standard supervised learning
472    SupervisedLearning,
473
474    /// Reinforcement learning
475    ReinforcementLearning,
476
477    /// Unsupervised learning
478    UnsupervisedLearning,
479
480    /// Few-shot learning
481    FewShotLearning,
482
483    /// Online learning
484    OnlineLearning,
485
486    /// Adversarial training
487    AdversarialTraining,
488}
489
490/// Point in optimization trajectory
491#[derive(Debug, Clone)]
492pub struct TrajectoryPoint<T: Float + Debug + Send + Sync + 'static> {
493    /// Step number
494    pub step: usize,
495
496    /// Gradient at this step
497    pub gradient: Array1<T>,
498
499    /// Parameters at this step
500    pub parameters: Array1<T>,
501
502    /// Loss at this step
503    pub loss: T,
504
505    /// Learning rate used
506    pub learning_rate: T,
507
508    /// Update direction
509    pub update: Array1<T>,
510}
511
512/// Task characteristics for meta-learning
513#[derive(Debug, Clone)]
514pub struct TaskCharacteristics<T: Float + Debug + Send + Sync + 'static> {
515    /// Problem dimensionality
516    pub dimensionality: usize,
517
518    /// Loss landscape curvature estimate
519    pub curvature: T,
520
521    /// Noise level estimate
522    pub noise_level: T,
523
524    /// Conditioning number estimate
525    pub conditioning: T,
526
527    /// Convergence difficulty
528    pub difficulty: T,
529
530    /// Task domain features
531    pub domain_features: Array1<T>,
532}
533
534/// Meta-learning state
535#[derive(Debug, Clone)]
536pub struct MetaLearningState<T: Float + Debug + Send + Sync + 'static> {
537    /// Current meta-learning step
538    pub meta_step: usize,
539
540    /// Meta-learning rate
541    pub meta_lr: T,
542
543    /// Adaptation rate
544    pub adaptation_rate: T,
545
546    /// Meta-validation performance
547    pub meta_validation_performance: T,
548
549    /// Task adaptation history
550    pub adaptation_history: VecDeque<AdaptationEvent<T>>,
551
552    /// Inner loop state
553    pub inner_loop_state: InnerLoopState<T>,
554}
555
556/// Adaptation event tracking
557#[derive(Debug, Clone)]
558pub struct AdaptationEvent<T: Float + Debug + Send + Sync + 'static> {
559    /// Source task
560    pub source_task: String,
561
562    /// Target task
563    pub target_task: String,
564
565    /// Adaptation steps required
566    pub adaptation_steps: usize,
567
568    /// Transfer efficiency
569    pub transfer_efficiency: T,
570
571    /// Final performance improvement
572    pub performance_improvement: T,
573}
574
575/// Inner loop optimization state
576#[derive(Debug, Clone)]
577pub struct InnerLoopState<T: Float + Debug + Send + Sync + 'static> {
578    /// Current inner step
579    pub inner_step: usize,
580
581    /// Inner loop parameters
582    pub inner_parameters: Array1<T>,
583
584    /// Inner loop optimizer state
585    pub inner_optimizer_state: HashMap<String, Array1<T>>,
586
587    /// Inner loop performance
588    pub inner_performance: T,
589}
590
591/// Transfer learning component
592#[derive(Debug, Clone)]
593pub struct TransferLearner<T: Float + Debug + Send + Sync + 'static> {
594    /// Source domain knowledge
595    pub source_knowledge: HashMap<String, Array1<T>>,
596
597    /// Domain adaptation parameters
598    pub adaptation_parameters: Array1<T>,
599
600    /// Transfer efficiency metrics
601    pub transfer_metrics: TransferMetrics<T>,
602
603    /// Domain similarity estimator
604    pub similarity_estimator: DomainSimilarityEstimator<T>,
605}
606
607/// Transfer learning metrics
608#[derive(Debug, Clone)]
609pub struct TransferMetrics<T: Float + Debug + Send + Sync + 'static> {
610    /// Transfer efficiency
611    pub efficiency: T,
612
613    /// Adaptation speed
614    pub adaptation_speed: T,
615
616    /// Knowledge retention
617    pub knowledge_retention: T,
618
619    /// Negative transfer detection
620    pub negative_transfer_score: T,
621}
622
623/// Domain similarity estimator
624#[derive(Debug, Clone)]
625pub struct DomainSimilarityEstimator<T: Float + Debug + Send + Sync + 'static> {
626    /// Domain embeddings
627    pub domain_embeddings: HashMap<String, Array1<T>>,
628
629    /// Similarity metric parameters
630    pub similarity_params: Array1<T>,
631
632    /// Learned similarity function
633    pub similarity_function: SimilarityFunction,
634}
635
636/// Similarity function types
637#[derive(Debug, Clone, Copy)]
638pub enum SimilarityFunction {
639    /// Cosine similarity
640    Cosine,
641
642    /// Euclidean distance
643    Euclidean,
644
645    /// Learned metric
646    LearnedMetric,
647
648    /// Task-specific similarity
649    TaskSpecific,
650}
651
652/// Adaptive learning rate controller
653#[derive(Debug, Clone)]
654pub struct AdaptiveLearningRateController<T: Float + Debug + Send + Sync + 'static> {
655    /// Base learning rate
656    base_lr: T,
657
658    /// Current learning rate
659    current_lr: T,
660
661    /// Learning rate adaptation parameters
662    adaptation_params: LRAdaptationParams<T>,
663
664    /// Learning rate history
665    lr_history: VecDeque<T>,
666
667    /// Performance-based adaptation
668    performance_tracker: PerformanceTracker<T>,
669}
670
671/// Learning rate adaptation parameters
672#[derive(Debug, Clone)]
673pub struct LRAdaptationParams<T: Float + Debug + Send + Sync + 'static> {
674    /// Momentum for LR adaptation
675    pub momentum: T,
676
677    /// Sensitivity to gradient changes
678    pub gradient_sensitivity: T,
679
680    /// Sensitivity to loss changes
681    pub loss_sensitivity: T,
682
683    /// Minimum learning rate
684    pub min_lr: T,
685
686    /// Maximum learning rate
687    pub max_lr: T,
688
689    /// Adaptation rate
690    pub adaptation_rate: T,
691}
692
693/// Performance tracker for adaptive learning rate
694#[derive(Debug, Clone)]
695pub struct PerformanceTracker<T: Float + Debug + Send + Sync + 'static> {
696    /// Recent loss values
697    recent_losses: VecDeque<T>,
698
699    /// Performance trend
700    trend: PerformanceTrend,
701
702    /// Stagnation detection
703    stagnation_counter: usize,
704
705    /// Best performance seen
706    best_performance: T,
707
708    /// Performance improvement rate
709    improvement_rate: T,
710}
711
712/// Performance trend indicators
713#[derive(Debug, Clone, Copy)]
714pub enum PerformanceTrend {
715    /// Performance is improving
716    Improving,
717
718    /// Performance is stagnating
719    Stagnating,
720
721    /// Performance is degrading
722    Degrading,
723
724    /// Performance is oscillating
725    Oscillating,
726
727    /// Insufficient data
728    Unknown,
729}
730
731/// Optimization state tracker
732#[derive(Debug, Clone)]
733pub struct OptimizationStateTracker<T: Float + Debug + Send + Sync + 'static> {
734    /// Current optimization phase
735    phase: OptimizationPhase,
736
737    /// Convergence indicators
738    convergence_indicators: ConvergenceIndicators<T>,
739
740    /// Gradient analysis
741    gradient_analyzer: GradientAnalyzer<T>,
742
743    /// Loss landscape analysis
744    landscape_analyzer: LossLandscapeAnalyzer<T>,
745
746    /// Stability metrics
747    stability_metrics: StabilityMetrics<T>,
748
749    /// Previous step's gradient, for the direction-consistency, noise and
750    /// secant-curvature estimates computed by
751    /// [`OptimizationStateTracker::update`].
752    previous_gradient: Option<Array1<T>>,
753
754    /// Previous step's loss, for the loss-change trend.
755    previous_loss: Option<T>,
756
757    /// Number of observed steps, for the running moments.
758    step_count: usize,
759}
760
761/// Optimization phases
762#[derive(Debug, Clone, Copy)]
763pub enum OptimizationPhase {
764    /// Initial rapid descent
765    InitialDescent,
766
767    /// Steady progress
768    SteadyProgress,
769
770    /// Fine-tuning
771    FineTuning,
772
773    /// Converged
774    Converged,
775
776    /// Stuck/Plateau
777    Plateau,
778
779    /// Diverging
780    Diverging,
781}
782
783/// Convergence indicators
784#[derive(Debug, Clone)]
785pub struct ConvergenceIndicators<T: Float + Debug + Send + Sync + 'static> {
786    /// Gradient norm trend
787    pub gradient_norm_trend: Vec<T>,
788
789    /// Loss change trend
790    pub loss_change_trend: Vec<T>,
791
792    /// Parameter change magnitude
793    pub parameter_change_magnitude: T,
794
795    /// Convergence probability
796    pub convergence_probability: T,
797
798    /// Estimated steps to convergence
799    pub estimated_steps_to_convergence: Option<usize>,
800}
801
802/// Gradient analysis component
803#[derive(Debug, Clone)]
804pub struct GradientAnalyzer<T: Float + Debug + Send + Sync + 'static> {
805    /// Gradient statistics
806    pub gradient_stats: GradientStatistics<T>,
807
808    /// Gradient correlation tracking
809    pub correlation_tracker: GradientCorrelationTracker<T>,
810
811    /// Gradient noise estimation
812    pub noise_estimator: GradientNoiseEstimator<T>,
813
814    /// Gradient flow analysis
815    pub flow_analyzer: GradientFlowAnalyzer<T>,
816}
817
818/// Gradient statistics
819#[derive(Debug, Clone)]
820pub struct GradientStatistics<T: Float + Debug + Send + Sync + 'static> {
821    /// Mean gradient norm
822    pub mean_norm: T,
823
824    /// Gradient norm variance
825    pub norm_variance: T,
826
827    /// Gradient direction consistency
828    pub direction_consistency: T,
829
830    /// Gradient magnitude distribution
831    pub magnitude_distribution: Vec<T>,
832
833    /// Component-wise statistics
834    pub component_stats: Array1<T>,
835}
836
837/// Gradient correlation tracker
838#[derive(Debug, Clone)]
839pub struct GradientCorrelationTracker<T: Float + Debug + Send + Sync + 'static> {
840    /// Correlation matrix
841    pub correlation_matrix: Array2<T>,
842
843    /// Temporal correlations
844    pub temporal_correlations: VecDeque<T>,
845
846    /// Cross-parameter correlations
847    pub cross_correlations: HashMap<String, T>,
848}
849
850/// Gradient noise estimator
851#[derive(Debug, Clone)]
852pub struct GradientNoiseEstimator<T: Float + Debug + Send + Sync + 'static> {
853    /// Estimated noise level
854    pub noise_level: T,
855
856    /// Signal-to-noise ratio
857    pub signal_to_noise_ratio: T,
858
859    /// Noise characteristics
860    pub noise_characteristics: NoiseCharacteristics<T>,
861}
862
863/// Noise characteristics
864#[derive(Debug, Clone)]
865pub struct NoiseCharacteristics<T: Float + Debug + Send + Sync + 'static> {
866    /// Noise type
867    pub noise_type: NoiseType,
868
869    /// Noise scale
870    pub scale: T,
871
872    /// Temporal correlation
873    pub temporal_correlation: T,
874
875    /// Spatial correlation
876    pub spatial_correlation: T,
877}
878
879/// Types of gradient noise
880#[derive(Debug, Clone, Copy)]
881pub enum NoiseType {
882    /// White noise (uncorrelated)
883    White,
884
885    /// Colored noise (correlated)
886    Colored,
887
888    /// Structured noise
889    Structured,
890
891    /// Adaptive noise
892    Adaptive,
893}
894
895/// Gradient flow analyzer
896#[derive(Debug, Clone)]
897pub struct GradientFlowAnalyzer<T: Float + Debug + Send + Sync + 'static> {
898    /// Flow field estimation
899    pub flow_field: Array2<T>,
900
901    /// Critical points
902    pub critical_points: Vec<Array1<T>>,
903
904    /// Flow stability
905    pub stability: FlowStability,
906
907    /// Attractors and repellers
908    pub attractors: Vec<Array1<T>>,
909    pub repellers: Vec<Array1<T>>,
910}
911
912/// Flow stability indicators
913#[derive(Debug, Clone, Copy)]
914pub enum FlowStability {
915    /// Stable flow
916    Stable,
917
918    /// Unstable flow
919    Unstable,
920
921    /// Chaotic flow
922    Chaotic,
923
924    /// Unknown stability
925    Unknown,
926}
927
928/// Loss landscape analyzer
929#[derive(Debug, Clone)]
930pub struct LossLandscapeAnalyzer<T: Float + Debug + Send + Sync + 'static> {
931    /// Local curvature estimation
932    pub local_curvature: T,
933
934    /// Hessian eigenvalue estimates
935    pub hessian_eigenvalues: Option<Array1<T>>,
936
937    /// Landscape roughness
938    pub roughness: T,
939
940    /// Basin of attraction size
941    pub basin_size: T,
942
943    /// Barrier heights
944    pub barrier_heights: Vec<T>,
945}
946
947/// Stability metrics
948#[derive(Debug, Clone)]
949pub struct StabilityMetrics<T: Float + Debug + Send + Sync + 'static> {
950    /// Lyapunov exponents
951    pub lyapunov_exponents: Array1<T>,
952
953    /// Stability margin
954    pub stability_margin: T,
955
956    /// Perturbation sensitivity
957    pub perturbation_sensitivity: T,
958
959    /// Robustness score
960    pub robustness_score: T,
961}
962
963/// Performance metrics for LSTM optimizer
964#[derive(Debug, Clone)]
965pub struct LSTMOptimizerMetrics {
966    /// Meta-learning performance
967    pub meta_learning_loss: f64,
968
969    /// Average convergence speed
970    pub avg_convergence_speed: f64,
971
972    /// Generalization performance
973    pub generalization_performance: f64,
974
975    /// Adaptation efficiency
976    pub adaptation_efficiency: f64,
977
978    /// Transfer learning success rate
979    pub transfer_success_rate: f64,
980
981    /// Memory usage
982    pub memory_usage_mb: f64,
983
984    /// Computational overhead
985    pub computational_overhead: f64,
986
987    /// LSTM network statistics
988    pub lstm_stats: LSTMNetworkStats,
989
990    /// Attention statistics (if using attention)
991    pub attention_stats: Option<AttentionStats>,
992}
993
994/// LSTM network statistics
995#[derive(Debug, Clone)]
996pub struct LSTMNetworkStats {
997    /// Gate activation statistics
998    pub gate_activations: GateActivationStats,
999
1000    /// Hidden state statistics
1001    pub hidden_state_stats: StateStatistics,
1002
1003    /// Cell state statistics
1004    pub cell_state_stats: StateStatistics,
1005
1006    /// Gradient flow statistics
1007    pub gradient_flow_stats: GradientFlowStats,
1008}
1009
1010/// Gate activation statistics
1011#[derive(Debug, Clone)]
1012pub struct GateActivationStats {
1013    /// Input gate activations
1014    pub input_gate: StateStatistics,
1015
1016    /// Forget gate activations
1017    pub forget_gate: StateStatistics,
1018
1019    /// Output gate activations
1020    pub output_gate: StateStatistics,
1021
1022    /// Cell gate activations
1023    pub cell_gate: StateStatistics,
1024}
1025
1026/// State statistics
1027#[derive(Debug, Clone)]
1028pub struct StateStatistics {
1029    /// Mean activation
1030    pub mean: f64,
1031
1032    /// Standard deviation
1033    pub std: f64,
1034
1035    /// Minimum value
1036    pub min: f64,
1037
1038    /// Maximum value
1039    pub max: f64,
1040
1041    /// Saturation percentage
1042    pub saturation_percent: f64,
1043}
1044
1045/// Gradient flow statistics
1046#[derive(Debug, Clone)]
1047pub struct GradientFlowStats {
1048    /// Gradient norm through layers
1049    pub layer_gradient_norms: Vec<f64>,
1050
1051    /// Gradient correlation between layers
1052    pub layer_correlations: Vec<f64>,
1053
1054    /// Vanishing gradient indicator
1055    pub vanishing_gradient_score: f64,
1056
1057    /// Exploding gradient indicator
1058    pub exploding_gradient_score: f64,
1059}
1060
1061/// Attention mechanism statistics
1062#[derive(Debug, Clone)]
1063pub struct AttentionStats {
1064    /// Attention entropy
1065    pub attention_entropy: f64,
1066
1067    /// Attention concentration
1068    pub attention_concentration: f64,
1069
1070    /// Head diversity
1071    pub head_diversity: f64,
1072
1073    /// Temporal attention patterns
1074    pub temporal_patterns: Vec<f64>,
1075}
1076
1077impl<
1078        T: Float
1079            + Default
1080            + Clone
1081            + Send
1082            + Sync
1083            + std::iter::Sum
1084            + for<'a> std::iter::Sum<&'a T>
1085            + scirs2_core::ndarray::ScalarOperand
1086            + std::fmt::Debug,
1087    > LSTMOptimizer<T>
1088{
1089    /// Create a new LSTM optimizer
1090    pub fn new(config: LearnedOptimizerConfig) -> Result<Self> {
1091        // Validate configuration
1092        Self::validate_config(&config)?;
1093
1094        // Initialize LSTM network
1095        let lstm_network = LSTMNetwork::new(&config)?;
1096
1097        // Initialize history buffer
1098        let history_buffer = HistoryBuffer::new(config.gradient_history_size);
1099
1100        // Initialize meta-learner
1101        let meta_learner = MetaLearner::new(&config)?;
1102
1103        // Initialize learning rate controller
1104        let lr_controller = AdaptiveLearningRateController::new(&config)?;
1105
1106        // Initialize state tracker
1107        let state_tracker = OptimizationStateTracker::new();
1108
1109        // Initialize metrics
1110        let metrics = LSTMOptimizerMetrics::new();
1111
1112        Ok(Self {
1113            config,
1114            lstm_network,
1115            history_buffer,
1116            meta_learner,
1117            lr_controller,
1118            state_tracker,
1119            metrics,
1120            step_count: 0,
1121        })
1122    }
1123
1124    /// Perform LSTM-based optimization step
1125    pub fn lstm_step<S, D>(
1126        &mut self,
1127        parameters: &ArrayBase<S, D>,
1128        gradients: &ArrayBase<S, D>,
1129        loss: Option<T>,
1130    ) -> Result<Array<T, D>>
1131    where
1132        S: Data<Elem = T>,
1133        D: Dimension + Clone,
1134    {
1135        // Convert to flat arrays for processing
1136        let flat_params = self.flatten_to_1d(parameters)?;
1137        let flat_gradients = self.flatten_to_1d(gradients)?;
1138
1139        // The output projection must produce exactly one update per
1140        // flattened parameter, or the `flat_params - updates` subtraction
1141        // below panics on mismatched shapes. Re-initialize it at the correct
1142        // size the moment the optimizer sees a parameter count different from
1143        // its `output_features` configuration, rather than requiring every
1144        // caller to pre-size their model to match a fixed constant.
1145        if self.lstm_network.output_projection.output_size() != flat_params.len() {
1146            self.lstm_network
1147                .output_projection
1148                .reset(self.config.hidden_size, flat_params.len());
1149        }
1150
1151        // Update history buffer
1152        self.history_buffer
1153            .update(&flat_params, &flat_gradients, loss);
1154
1155        // Prepare LSTM input features
1156        let lstm_input = self.prepare_lstm_input(&flat_gradients)?;
1157
1158        // Forward pass through LSTM
1159        let lstm_output = self.lstm_network.forward(&lstm_input)?;
1160
1161        // Compute adaptive learning rate
1162        let learning_rate =
1163            self.lr_controller
1164                .compute_lr(&flat_gradients, loss, &self.history_buffer)?;
1165
1166        // Generate parameter updates
1167        let updates = self.generate_updates(&lstm_output, &flat_gradients, learning_rate)?;
1168
1169        // Apply updates to parameters
1170        let updated_flat = &flat_params - &updates;
1171
1172        // Update state tracking
1173        self.state_tracker.update(&flat_gradients, &updates, loss);
1174
1175        // Update metrics
1176        self.update_metrics(&flat_gradients, &updates, learning_rate);
1177
1178        // Reshape back to original dimensions
1179        let updated_params = self.reshape_from_1d(&updated_flat, parameters.raw_dim())?;
1180
1181        self.step_count += 1;
1182
1183        Ok(updated_params)
1184    }
1185
1186    /// Meta-learning step for optimizer adaptation
1187    pub fn meta_learning_step(&mut self, tasks: &[MetaTask<T>]) -> Result<T> {
1188        // Perform meta-learning update
1189        let meta_loss = self.meta_learner.step(tasks, &mut self.lstm_network)?;
1190
1191        // Update meta-learning metrics
1192        self.metrics.meta_learning_loss = meta_loss.to_f64().unwrap_or(0.0);
1193
1194        Ok(meta_loss)
1195    }
1196
1197    /// Transfer learning to new optimization domain
1198    pub fn transfer_to_domain(
1199        &mut self,
1200        target_tasks: &[MetaTask<T>],
1201    ) -> Result<TransferResults<T>> {
1202        self.meta_learner
1203            .transfer_learner
1204            .transfer_to_domain(target_tasks, &mut self.lstm_network)
1205    }
1206
1207    /// Get current performance metrics
1208    pub fn get_metrics(&self) -> &LSTMOptimizerMetrics {
1209        &self.metrics
1210    }
1211
1212    /// Read-only access to the LSTM controller network.
1213    pub fn network(&self) -> &LSTMNetwork<T> {
1214        &self.lstm_network
1215    }
1216
1217    /// Read-only access to the meta-learner, whose `adaptation_history` records
1218    /// what each [`Self::meta_learning_step`] achieved.
1219    pub fn meta_learner(&self) -> &MetaLearner<T> {
1220        &self.meta_learner
1221    }
1222
1223    /// Mutable access to the LSTM controller network.
1224    ///
1225    /// This is the handle a caller needs to meta-train the controller directly
1226    /// with [`trainer::MetaTrainer`], rather than going through
1227    /// [`Self::meta_learning_step`]'s trajectory-surrogate path.
1228    pub fn network_mut(&mut self) -> &mut LSTMNetwork<T> {
1229        &mut self.lstm_network
1230    }
1231
1232    /// The learning rate the adaptive controller last produced.
1233    ///
1234    /// Meaningful only after at least one [`Self::lstm_step`]; before that it is
1235    /// the configured base rate.
1236    pub fn current_learning_rate(&self) -> T {
1237        self.lr_controller.current_lr()
1238    }
1239
1240    /// Consecutive steps without a new best loss, as tracked by the adaptive
1241    /// learning-rate controller.
1242    pub fn stagnation_counter(&self) -> usize {
1243        self.lr_controller.stagnation_counter()
1244    }
1245
1246    /// Get optimization state analysis
1247    pub fn get_state_analysis(&self) -> OptimizationStateAnalysis<T> {
1248        OptimizationStateAnalysis {
1249            current_phase: self.state_tracker.phase,
1250            convergence_indicators: self.state_tracker.convergence_indicators.clone(),
1251            gradient_analysis: self.state_tracker.gradient_analyzer.clone(),
1252            landscape_analysis: self.state_tracker.landscape_analyzer.clone(),
1253            stability_metrics: self.state_tracker.stability_metrics.clone(),
1254        }
1255    }
1256
1257    /// Prepare input features for the LSTM.
1258    ///
1259    /// Delegates to [`features::build_lstm_features`] so that inference and
1260    /// truncated-BPTT meta-training consume byte-identical features — training a
1261    /// different function than the one that ships would make the meta-training
1262    /// worthless. That shared builder also removed the
1263    /// `gradients.as_slice().expect(...)` in the previous body, which panicked on
1264    /// any gradient that was not in contiguous standard layout.
1265    fn prepare_lstm_input(&self, gradients: &Array1<T>) -> Result<Array1<T>> {
1266        let recent = self
1267            .history_buffer
1268            .get_recent_gradients(5)
1269            .unwrap_or_default();
1270        let loss_features = self.history_buffer.get_loss_features();
1271        features::build_lstm_features(
1272            gradients,
1273            &recent,
1274            loss_features.as_deref(),
1275            self.config.input_features,
1276        )
1277    }
1278
1279    /// Generate parameter updates from LSTM output
1280    fn generate_updates(
1281        &self,
1282        lstm_output: &Array1<T>,
1283        gradients: &Array1<T>,
1284        learning_rate: T,
1285    ) -> Result<Array1<T>> {
1286        // Apply _output transformation
1287        let transformed_output = match self.lstm_network.output_projection.output_transform {
1288            OutputTransform::Identity => lstm_output.clone(),
1289            OutputTransform::Tanh => lstm_output.mapv(|x| x.tanh()),
1290            OutputTransform::ScaledTanh { scale } => {
1291                let scale_t =
1292                    scirs2_core::numeric::NumCast::from(scale).unwrap_or_else(|| T::zero());
1293                lstm_output.mapv(|x| x.tanh() * scale_t)
1294            }
1295            OutputTransform::AdaptiveScale => {
1296                let grad_norm = gradients.iter().map(|&g| g * g).sum::<T>().sqrt();
1297                let adaptive_scale = T::one() / (T::one() + grad_norm);
1298                lstm_output.mapv(|x| x * adaptive_scale)
1299            }
1300            OutputTransform::LearnedNonlinear => {
1301                // Apply learned nonlinear transformation
1302                lstm_output.mapv(|x| {
1303                    let exp_x = x.exp();
1304                    (exp_x - (-x).exp()) / (exp_x + (-x).exp()) // tanh via exp
1305                })
1306            }
1307        };
1308
1309        // Combine with gradient information
1310        let updates = &transformed_output * learning_rate;
1311
1312        Ok(updates)
1313    }
1314
1315    /// Update performance metrics.
1316    ///
1317    /// `adaptation_efficiency` is the *effective step-size ratio*
1318    /// `‖Δθ‖ / (lr · ‖g‖)`: the learned optimizer's step measured against the
1319    /// plain SGD step of the same learning rate that it replaces. `1.0` means
1320    /// "the same size as SGD would have taken", above `1.0` means the learned
1321    /// rule is more aggressive than its nominal learning rate, below `1.0` more
1322    /// conservative. Without dividing by `lr` the figure just tracked the
1323    /// learning rate itself and was not comparable across schedules.
1324    fn update_metrics(&mut self, gradients: &Array1<T>, updates: &Array1<T>, lr: T) {
1325        // Compute gradient statistics
1326        let grad_norm = gradients.iter().map(|&g| g * g).sum::<T>().sqrt();
1327        let update_norm = updates.iter().map(|&u| u * u).sum::<T>().sqrt();
1328
1329        // Update LSTM statistics
1330        self.update_lstm_stats();
1331
1332        // Update efficiency metrics. A zero gradient (or a zero learning rate)
1333        // makes the ratio undefined rather than 0/0-shaped, so report the
1334        // neutral 1.0 instead of dividing.
1335        let sgd_step_norm = lr.abs() * grad_norm;
1336        self.metrics.adaptation_efficiency = if sgd_step_norm > T::zero() {
1337            (update_norm / sgd_step_norm).to_f64().unwrap_or(1.0)
1338        } else {
1339            1.0
1340        };
1341
1342        // Update computational overhead
1343        self.metrics.computational_overhead = self.estimate_computational_overhead();
1344
1345        // Update memory usage
1346        self.metrics.memory_usage_mb = self.estimate_memory_usage();
1347    }
1348
1349    /// Update LSTM network statistics
1350    fn update_lstm_stats(&mut self) {
1351        // Update gate activation statistics
1352        for layer in self.lstm_network.layers.iter() {
1353            let hidden_stats = self.compute_state_stats(&layer.hidden_state);
1354            let cell_stats = self.compute_state_stats(&layer.cell_state);
1355
1356            // Update statistics (simplified)
1357            self.metrics.lstm_stats.hidden_state_stats = hidden_stats;
1358            self.metrics.lstm_stats.cell_state_stats = cell_stats;
1359        }
1360
1361        // Update attention statistics if available
1362        if let Some(ref attention) = self.lstm_network.attention {
1363            if let Some(ref attentionweights) = attention.attentionweights {
1364                self.metrics.attention_stats = Some(
1365                    self.compute_attention_stats(attentionweights, attention.head_logits.as_ref()),
1366                );
1367            }
1368        }
1369    }
1370
1371    /// Compute state statistics
1372    fn compute_state_stats(&self, state: &Array1<T>) -> StateStatistics {
1373        let values: Vec<f64> = state.iter().map(|&x| x.to_f64().unwrap_or(0.0)).collect();
1374
1375        let mean = values.iter().sum::<f64>() / values.len() as f64;
1376        let variance =
1377            values.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / values.len() as f64;
1378        let std = variance.sqrt();
1379        let min = values.iter().cloned().fold(f64::INFINITY, f64::min);
1380        let max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
1381
1382        let saturation_count = values.iter().filter(|&&x| x.abs() > 0.95).count();
1383        let saturation_percent = saturation_count as f64 / values.len() as f64 * 100.0;
1384
1385        StateStatistics {
1386            mean,
1387            std,
1388            min,
1389            max,
1390            saturation_percent,
1391        }
1392    }
1393
1394    /// Compute attention statistics.
1395    ///
1396    /// `head_logits`, when present, holds the pre-softmax scaled dot product per
1397    /// head. Head diversity is the population standard deviation of those
1398    /// logits: at sequence length 1 the softmax weights are all `1.0`, so any
1399    /// diversity measure computed from the *weights* is a constant and tells the
1400    /// caller nothing. The logits are the quantity that actually varies with the
1401    /// input and between heads.
1402    ///
1403    /// `temporal_patterns` is the per-head logit sequence itself, which is the
1404    /// full temporal record available from one forward pass; an empty vector when
1405    /// no logits were recorded, rather than a zero-filled placeholder.
1406    fn compute_attention_stats(
1407        &self,
1408        attentionweights: &Array2<T>,
1409        head_logits: Option<&Array1<T>>,
1410    ) -> AttentionStats {
1411        let weights: Vec<f64> = attentionweights
1412            .iter()
1413            .map(|&w| w.to_f64().unwrap_or(0.0))
1414            .collect();
1415
1416        // Compute entropy
1417        let entropy = weights
1418            .iter()
1419            .filter(|&&w| w > 0.0)
1420            .map(|&w| -w * w.ln())
1421            .sum::<f64>();
1422
1423        // Compute concentration (inverse of entropy)
1424        let concentration = 1.0 / (1.0 + entropy);
1425
1426        let logits: Vec<f64> = head_logits
1427            .map(|l| l.iter().map(|&x| x.to_f64().unwrap_or(0.0)).collect())
1428            .unwrap_or_default();
1429        let head_diversity = if logits.is_empty() {
1430            0.0
1431        } else {
1432            let mean = logits.iter().sum::<f64>() / logits.len() as f64;
1433            (logits.iter().map(|&x| (x - mean) * (x - mean)).sum::<f64>() / logits.len() as f64)
1434                .sqrt()
1435        };
1436
1437        AttentionStats {
1438            attention_entropy: entropy,
1439            attention_concentration: concentration,
1440            head_diversity,
1441            temporal_patterns: logits,
1442        }
1443    }
1444
1445    /// Estimate computational overhead
1446    fn estimate_computational_overhead(&self) -> f64 {
1447        // Simplified overhead estimation
1448        let lstm_overhead = self.config.num_layers as f64 * 0.1;
1449        let attention_overhead = if self.config.use_attention { 0.2 } else { 0.0 };
1450        let meta_learning_overhead = 0.1;
1451
1452        1.0 + lstm_overhead + attention_overhead + meta_learning_overhead
1453    }
1454
1455    /// Estimate memory usage
1456    fn estimate_memory_usage(&self) -> f64 {
1457        // Simplified memory estimation in MB
1458        let parameter_memory =
1459            self.config.hidden_size as f64 * self.config.num_layers as f64 * 8.0 / 1024.0 / 1024.0;
1460        let history_memory =
1461            self.config.gradient_history_size as f64 * self.config.input_features as f64 * 8.0
1462                / 1024.0
1463                / 1024.0;
1464        let lstm_state_memory =
1465            self.config.hidden_size as f64 * self.config.num_layers as f64 * 2.0 * 8.0
1466                / 1024.0
1467                / 1024.0;
1468
1469        parameter_memory + history_memory + lstm_state_memory
1470    }
1471
1472    /// Validate configuration
1473    fn validate_config(config: &LearnedOptimizerConfig) -> Result<()> {
1474        if config.hidden_size == 0 {
1475            return Err(OptimError::InvalidConfig(
1476                "Hidden size must be positive".to_string(),
1477            ));
1478        }
1479
1480        if config.num_layers == 0 {
1481            return Err(OptimError::InvalidConfig(
1482                "Number of layers must be positive".to_string(),
1483            ));
1484        }
1485
1486        if config.input_features == 0 {
1487            return Err(OptimError::InvalidConfig(
1488                "Input features must be positive".to_string(),
1489            ));
1490        }
1491
1492        if config.meta_learning_rate <= 0.0 {
1493            return Err(OptimError::InvalidConfig(
1494                "Meta learning rate must be positive".to_string(),
1495            ));
1496        }
1497
1498        Ok(())
1499    }
1500
1501    /// Utility functions for array manipulation
1502    fn flatten_to_1d<S, D>(&self, array: &ArrayBase<S, D>) -> Result<Array1<T>>
1503    where
1504        S: Data<Elem = T>,
1505        D: Dimension,
1506    {
1507        Ok(Array1::from_iter(array.iter().cloned()))
1508    }
1509
1510    fn reshape_from_1d<D>(&self, flat: &Array1<T>, shape: D) -> Result<Array<T, D>>
1511    where
1512        D: Dimension + Clone,
1513    {
1514        Array::from_shape_vec(shape, flat.to_vec())
1515            .map_err(|e| OptimError::InvalidConfig(format!("Reshape error: {}", e)))
1516    }
1517}
1518
1519// Implementation of major components
1520
1521impl<T: Float + Debug + Default + Clone + 'static + Send + Sync> LSTMNetwork<T> {
1522    /// Create a new LSTM controller network from a configuration, with weights
1523    /// drawn from thread-local entropy.
1524    ///
1525    /// Public because meta-training operates on a controller directly: see
1526    /// [`trainer::MetaTrainer`] and [`LSTMOptimizer::network_mut`]. For
1527    /// reproducible construction (tests, checkpoint-comparable experiments) use
1528    /// [`Self::new_seeded`].
1529    pub fn new(config: &LearnedOptimizerConfig) -> Result<Self> {
1530        Self::new_seeded(config, thread_rng().random::<u64>())
1531    }
1532
1533    /// Create a controller whose initial weights are a deterministic function of
1534    /// `seed` (and the configuration).
1535    ///
1536    /// Everything downstream of construction — rollout capture, truncated BPTT,
1537    /// meta-training — is already deterministic with dropout off, so seeding the
1538    /// initialization makes an entire meta-training run reproducible.
1539    pub fn new_seeded(config: &LearnedOptimizerConfig, seed: u64) -> Result<Self> {
1540        let mut rng = seeded_rng(seed);
1541        let mut layers = Vec::new();
1542
1543        // Create LSTM layers
1544        for i in 0..config.num_layers {
1545            let input_size = if i == 0 {
1546                config.input_features
1547            } else {
1548                config.hidden_size
1549            };
1550            let layer = LSTMLayer::new(input_size, config.hidden_size, &mut rng)?;
1551            layers.push(layer);
1552        }
1553
1554        // Create output projection
1555        let output_projection = OutputProjection::new_with_rng(
1556            config.hidden_size,
1557            config.output_features,
1558            OutputTransform::ScaledTanh { scale: 0.1 },
1559            &mut rng,
1560        )?;
1561
1562        // Create attention mechanism if enabled
1563        let attention = if config.use_attention {
1564            Some(AttentionMechanism::new_with_rng(config, &mut rng)?)
1565        } else {
1566            None
1567        };
1568
1569        // Create layer normalization
1570        let layer_norms = (0..config.num_layers)
1571            .map(|_| LayerNormalization::new(config.hidden_size))
1572            .collect::<Result<Vec<_>>>()?;
1573
1574        Ok(Self {
1575            layers,
1576            output_projection,
1577            attention,
1578            layer_norms,
1579            dropout_rate: config.dropout_rate,
1580            training: false,
1581        })
1582    }
1583
1584    /// Enable or disable dropout.
1585    ///
1586    /// Meta-training and inference both run with dropout **off**; turn it on only
1587    /// for a training regime that wants the regularization and can tolerate the
1588    /// nondeterminism.
1589    pub fn set_training(&mut self, training: bool) {
1590        self.training = training;
1591    }
1592
1593    /// Whether dropout is currently active.
1594    pub fn is_training(&self) -> bool {
1595        self.training
1596    }
1597
1598    /// Zero every layer's hidden and cell state.
1599    ///
1600    /// Required before a meta-training rollout: the recurrent state is what makes
1601    /// two rollouts of the same controller differ, so a reproducible rollout must
1602    /// start from a known state.
1603    pub fn reset_state(&mut self) {
1604        for layer in self.layers.iter_mut() {
1605            layer.hidden_state.fill(T::zero());
1606            layer.cell_state.fill(T::zero());
1607        }
1608    }
1609
1610    /// Forward pass through the LSTM controller network.
1611    ///
1612    /// Public because the truncated-BPTT engine's taped forward
1613    /// ([`bptt`]) must be checkable against it — see
1614    /// `taped_forward_matches_the_deployed_forward` in
1615    /// `tests/lstm_meta_training.rs`. If the two ever drift apart,
1616    /// meta-training would optimize a different function than the one that runs
1617    /// at inference time, and the finite-difference check alone would not notice
1618    /// (it is self-consistent by construction).
1619    pub fn forward(&mut self, input: &Array1<T>) -> Result<Array1<T>> {
1620        let mut current_input = input.clone();
1621
1622        // Forward through LSTM layers
1623        for i in 0..self.layers.len() {
1624            current_input = self.layers[i].forward(&current_input)?;
1625
1626            // Apply layer normalization
1627            current_input = self.layer_norms[i].forward(&current_input)?;
1628
1629            // Apply dropout during training only.
1630            if self.training && self.dropout_rate > 0.0 {
1631                current_input = self.apply_dropout(&current_input)?;
1632            }
1633        }
1634
1635        // Apply attention if enabled
1636        if let Some(ref mut attention) = self.attention {
1637            current_input = attention.forward(&current_input)?;
1638        }
1639
1640        // Final output projection
1641        let output = self.output_projection.forward(&current_input)?;
1642
1643        Ok(output)
1644    }
1645
1646    /// Apply inverted dropout for regularization.
1647    ///
1648    /// Each element is zeroed with probability `dropout_rate` and the survivors
1649    /// are divided by `1 - dropout_rate`, so the expected activation is
1650    /// unchanged. A rate of `0` is a no-op; a rate of `1` would divide by zero
1651    /// and is rejected.
1652    ///
1653    /// # Errors
1654    /// Returns `Err` when `dropout_rate` is not in `[0, 1)` or when the drawn
1655    /// uniform sample cannot be represented in `T`.
1656    fn apply_dropout(&self, input: &Array1<T>) -> Result<Array1<T>> {
1657        if !(0.0..1.0).contains(&self.dropout_rate) {
1658            return Err(OptimError::InvalidConfig(format!(
1659                "dropout_rate must be in [0, 1), got {}",
1660                self.dropout_rate
1661            )));
1662        }
1663        if self.dropout_rate == 0.0 {
1664            return Ok(input.clone());
1665        }
1666        let threshold: T = cast_scalar(self.dropout_rate)?;
1667        let keep_scale: T = cast_scalar(1.0 - self.dropout_rate)?;
1668        let mut rng = scirs2_core::random::thread_rng();
1669        let mut out = input.clone();
1670        for value in out.iter_mut() {
1671            let sample: T = cast_scalar(rng.gen_range(0.0..1.0))?;
1672            *value = if sample < threshold {
1673                T::zero()
1674            } else {
1675                *value / keep_scale
1676            };
1677        }
1678        Ok(out)
1679    }
1680}
1681
1682impl<T: Float + Debug + Default + Clone + 'static + Send + Sync> LSTMLayer<T> {
1683    /// Create new LSTM layer
1684    fn new(_input_size: usize, hiddensize: usize, rng: &mut CoreRandom<StdRng>) -> Result<Self> {
1685        // Xavier/Glorot uniform, with the fan pair taken *per matrix*: the four
1686        // gates are independent maps stacked along the rows, so the fan-out of
1687        // each is `hiddensize`, not `4 * hiddensize`. The input-to-hidden and
1688        // hidden-to-hidden matrices therefore have different fan-ins and must not
1689        // share one limit (they did, computed from `_input_size + hiddensize` for
1690        // both).
1691        let limit_ih = Self::xavier_limit(_input_size, hiddensize);
1692        let limit_hh = Self::xavier_limit(hiddensize, hiddensize);
1693
1694        Ok(Self {
1695            weight_ih: Self::xavier_init(rng, 4 * hiddensize, _input_size, limit_ih),
1696            weight_hh: Self::xavier_init(rng, 4 * hiddensize, hiddensize, limit_hh),
1697            bias_ih: Array1::zeros(4 * hiddensize),
1698            bias_hh: Array1::zeros(4 * hiddensize),
1699            hidden_state: Array1::zeros(hiddensize),
1700            cell_state: Array1::zeros(hiddensize),
1701            hiddensize,
1702        })
1703    }
1704
1705    /// Forward pass through LSTM layer
1706    fn forward(&mut self, input: &Array1<T>) -> Result<Array1<T>> {
1707        // LSTM computation: i, f, g, o = σ(W_ih @ x + W_hh @ h + b)
1708        let ih_linear = self.weight_ih.dot(input) + &self.bias_ih;
1709        let hh_linear = self.weight_hh.dot(&self.hidden_state) + &self.bias_hh;
1710        let gates = ih_linear + hh_linear;
1711
1712        // Split into gates
1713        let input_gate = Self::sigmoid(&gates.slice(s![0..self.hiddensize]).to_owned());
1714        let forget_gate = Self::sigmoid(
1715            &gates
1716                .slice(s![self.hiddensize..2 * self.hiddensize])
1717                .to_owned(),
1718        );
1719        let cell_gate = Self::tanh(
1720            &gates
1721                .slice(s![2 * self.hiddensize..3 * self.hiddensize])
1722                .to_owned(),
1723        );
1724        let output_gate = Self::sigmoid(
1725            &gates
1726                .slice(s![3 * self.hiddensize..4 * self.hiddensize])
1727                .to_owned(),
1728        );
1729
1730        // Update cell state
1731        self.cell_state = &forget_gate * &self.cell_state + &input_gate * &cell_gate;
1732
1733        // Update hidden state
1734        self.hidden_state = &output_gate * &Self::tanh(&self.cell_state);
1735
1736        Ok(self.hidden_state.clone())
1737    }
1738
1739    /// Xavier/Glorot **uniform** limit for a layer with the given fans:
1740    /// `sqrt(6 / (fan_in + fan_out))`.
1741    ///
1742    /// `Uniform[-b, b]` has variance `b^2 / 3`, so reaching Glorot's target
1743    /// variance `2 / (fan_in + fan_out)` needs `b = sqrt(6 / (fan_in + fan_out))`.
1744    /// Every call site in this file previously passed `sqrt(2 / (fan_in +
1745    /// fan_out))` — the limit for a *normal* draw — to the uniform sampler
1746    /// below, so every LSTM weight matrix started with one third of the intended
1747    /// variance and correspondingly attenuated signal and gradient (finding F64).
1748    pub(crate) fn xavier_limit(fan_in: usize, fan_out: usize) -> f64 {
1749        (6.0 / (fan_in + fan_out).max(1) as f64).sqrt()
1750    }
1751
1752    /// Draw a matrix from `Uniform[-limit, limit]` using the caller's generator.
1753    ///
1754    /// `limit` is the *uniform half-width*, not a standard deviation — use
1755    /// [`Self::xavier_limit`] to compute it from the layer's fans. Threading the
1756    /// generator through the constructors (rather than grabbing `thread_rng`
1757    /// here) is what makes [`LSTMNetwork::new_seeded`] reproducible.
1758    fn xavier_init(
1759        rng: &mut CoreRandom<StdRng>,
1760        rows: usize,
1761        cols: usize,
1762        limit: f64,
1763    ) -> Array2<T> {
1764        Array2::from_shape_fn((rows, cols), |_| {
1765            let val = (rng.gen_range(0.0..1.0) - 0.5) * 2.0 * limit;
1766            scirs2_core::numeric::NumCast::from(val).unwrap_or_else(|| T::zero())
1767        })
1768    }
1769
1770    /// Sigmoid activation
1771    fn sigmoid(x: &Array1<T>) -> Array1<T> {
1772        x.mapv(|xi| T::one() / (T::one() + (-xi).exp()))
1773    }
1774
1775    /// Tanh activation
1776    fn tanh(x: &Array1<T>) -> Array1<T> {
1777        x.mapv(|xi| xi.tanh())
1778    }
1779}
1780
1781impl<T: Float + Debug + Send + Sync + 'static + Default + Clone> HistoryBuffer<T> {
1782    /// Create new history buffer
1783    fn new(_maxlength: usize) -> Self {
1784        Self {
1785            gradients: VecDeque::with_capacity(_maxlength),
1786            parameters: VecDeque::with_capacity(_maxlength),
1787            losses: VecDeque::with_capacity(_maxlength),
1788            _maxlength,
1789            feature_cache: None,
1790        }
1791    }
1792
1793    /// Update history with new data
1794    fn update(&mut self, params: &Array1<T>, grads: &Array1<T>, loss: Option<T>) {
1795        // Add new entries
1796        self.parameters.push_back(params.clone());
1797        self.gradients.push_back(grads.clone());
1798
1799        if let Some(l) = loss {
1800            self.losses.push_back(l);
1801        }
1802
1803        // Maintain size limits
1804        while self.parameters.len() > self._maxlength {
1805            self.parameters.pop_front();
1806        }
1807        while self.gradients.len() > self._maxlength {
1808            self.gradients.pop_front();
1809        }
1810        while self.losses.len() > self._maxlength {
1811            self.losses.pop_front();
1812        }
1813
1814        // Invalidate cache
1815        self.feature_cache = None;
1816    }
1817
1818    /// Get recent gradients
1819    fn get_recent_gradients(&self, count: usize) -> Option<Vec<&Array1<T>>> {
1820        if self.gradients.len() < count {
1821            return None;
1822        }
1823
1824        Some(self.gradients.iter().rev().take(count).collect())
1825    }
1826
1827    /// Get loss-based features
1828    fn get_loss_features(&self) -> Option<Vec<T>> {
1829        if self.losses.len() < 2 {
1830            return None;
1831        }
1832
1833        // `len() >= 2` was checked above, so both reads are in range; using the
1834        // fallible accessor keeps that fact local instead of relying on an
1835        // `expect` five lines away from its guard.
1836        let current_loss = *self.losses.back()?;
1837        let prev_loss = *self.losses.get(self.losses.len() - 2)?;
1838
1839        let loss_change = current_loss - prev_loss;
1840        let loss_ratio = if prev_loss.abs()
1841            > scirs2_core::numeric::NumCast::from(1e-8).unwrap_or_else(|| T::zero())
1842        {
1843            current_loss / prev_loss
1844        } else {
1845            T::one()
1846        };
1847
1848        Some(vec![loss_change, loss_ratio])
1849    }
1850}
1851
1852/// Additional implementations for other components...
1853/// Results from optimization state analysis
1854#[derive(Debug, Clone)]
1855pub struct OptimizationStateAnalysis<T: Float + Debug + Send + Sync + 'static> {
1856    pub current_phase: OptimizationPhase,
1857    pub convergence_indicators: ConvergenceIndicators<T>,
1858    pub gradient_analysis: GradientAnalyzer<T>,
1859    pub landscape_analysis: LossLandscapeAnalyzer<T>,
1860    pub stability_metrics: StabilityMetrics<T>,
1861}
1862
1863/// Transfer learning results
1864#[derive(Debug, Clone)]
1865pub struct TransferResults<T: Float + Debug + Send + Sync + 'static> {
1866    pub initial_performance: T,
1867    pub final_performance: T,
1868    pub adaptation_steps: usize,
1869    pub transfer_efficiency: T,
1870}
1871
1872// Additional default implementations and stubs for remaining components...
1873
1874impl Default for LSTMOptimizerMetrics {
1875    fn default() -> Self {
1876        Self::new()
1877    }
1878}
1879
1880impl LSTMOptimizerMetrics {
1881    fn new() -> Self {
1882        Self {
1883            meta_learning_loss: 0.0,
1884            avg_convergence_speed: 0.0,
1885            generalization_performance: 0.0,
1886            adaptation_efficiency: 0.0,
1887            transfer_success_rate: 0.0,
1888            memory_usage_mb: 0.0,
1889            computational_overhead: 1.0,
1890            lstm_stats: LSTMNetworkStats {
1891                gate_activations: GateActivationStats {
1892                    input_gate: StateStatistics::default(),
1893                    forget_gate: StateStatistics::default(),
1894                    output_gate: StateStatistics::default(),
1895                    cell_gate: StateStatistics::default(),
1896                },
1897                hidden_state_stats: StateStatistics::default(),
1898                cell_state_stats: StateStatistics::default(),
1899                gradient_flow_stats: GradientFlowStats {
1900                    layer_gradient_norms: Vec::new(),
1901                    layer_correlations: Vec::new(),
1902                    vanishing_gradient_score: 0.0,
1903                    exploding_gradient_score: 0.0,
1904                },
1905            },
1906            attention_stats: None,
1907        }
1908    }
1909}
1910
1911impl Default for StateStatistics {
1912    fn default() -> Self {
1913        Self {
1914            mean: 0.0,
1915            std: 0.0,
1916            min: 0.0,
1917            max: 0.0,
1918            saturation_percent: 0.0,
1919        }
1920    }
1921}
1922
1923#[cfg(test)]
1924mod tests {
1925    use super::*;
1926
1927    #[test]
1928    fn test_lstm_optimizer_creation() {
1929        let config = LearnedOptimizerConfig::default();
1930        let optimizer = LSTMOptimizer::<f64>::new(config);
1931        assert!(optimizer.is_ok());
1932    }
1933
1934    #[test]
1935    fn test_lstm_layer_creation() {
1936        let layer = LSTMLayer::<f64>::new(10, 20, &mut seeded_rng(7));
1937        assert!(layer.is_ok());
1938
1939        let layer = layer.expect("LSTMLayer::new should succeed");
1940        assert_eq!(layer.hiddensize, 20);
1941        assert_eq!(layer.weight_ih.shape(), &[80, 10]); // 4 * hiddensize, input_size
1942        assert_eq!(layer.weight_hh.shape(), &[80, 20]); // 4 * hiddensize, hiddensize
1943    }
1944
1945    #[test]
1946    fn test_history_buffer() {
1947        let mut buffer = HistoryBuffer::<f64>::new(5);
1948
1949        let params = Array1::from_vec(vec![1.0, 2.0, 3.0]);
1950        let grads = Array1::from_vec(vec![0.1, 0.2, 0.3]);
1951
1952        buffer.update(&params, &grads, Some(0.5));
1953
1954        assert_eq!(buffer.gradients.len(), 1);
1955        assert_eq!(buffer.parameters.len(), 1);
1956        assert_eq!(buffer.losses.len(), 1);
1957    }
1958
1959    #[test]
1960    fn test_config_validation() {
1961        let mut config = LearnedOptimizerConfig::default();
1962        assert!(LSTMOptimizer::<f64>::validate_config(&config).is_ok());
1963
1964        config.hidden_size = 0;
1965        assert!(LSTMOptimizer::<f64>::validate_config(&config).is_err());
1966    }
1967
1968    #[test]
1969    fn test_lstm_network_creation() {
1970        let config = LearnedOptimizerConfig::default();
1971        let network = LSTMNetwork::<f64>::new(&config);
1972        assert!(network.is_ok());
1973
1974        let network = network.expect("LSTMNetwork::new should succeed");
1975        assert_eq!(network.layers.len(), config.num_layers);
1976        assert!(network.attention.is_some()); // attention enabled by default
1977    }
1978
1979    #[test]
1980    fn test_metrics_initialization() {
1981        let metrics = LSTMOptimizerMetrics::new();
1982        assert_eq!(metrics.meta_learning_loss, 0.0);
1983        assert_eq!(metrics.computational_overhead, 1.0);
1984        assert!(metrics.attention_stats.is_none());
1985    }
1986}