Skip to main content

optirs_learned/transformer_based_optimizer/
mod.rs

1// Transformer-Based Meta-Learning for Optimization
2//
3// This module implements transformer architectures specifically designed for
4// meta-learning in optimization tasks. It includes attention mechanisms,
5// sequence modeling for optimization trajectories, and advanced transformer
6// architectures tailored for learning optimization strategies.
7
8pub mod architecture;
9pub mod attention;
10pub mod config;
11pub mod feedforward;
12pub mod layers;
13pub mod memory_manager;
14pub mod meta_learning;
15pub mod performance_tracker;
16pub mod positional_encoding;
17pub mod sequence_processor;
18pub mod state;
19
20// Re-export main types for backward compatibility
21pub use architecture::{ArchitectureCache, TransformerArchitecture, TransformerLayer};
22pub use attention::{AttentionMechanism, MultiHeadAttention};
23pub use config::{TransformerArchConfig, TransformerBasedOptimizerConfig};
24pub use feedforward::{ActivationFunction, FeedForwardNetwork};
25pub use layers::{
26    DropoutLayer, EmbeddingLayer, LayerNormalization, OutputProjection, ResidualConnections,
27};
28pub use memory_manager::{MemoryManagementStrategy, TransformerMemoryManager};
29pub use meta_learning::{MetaLearningStrategy, TransformerMetaLearning};
30pub use performance_tracker::{PerformanceMetrics, TransformerPerformanceTracker};
31pub use positional_encoding::{PositionalEncoding, PositionalEncodingType};
32pub use sequence_processor::{OptimizationSequenceProcessor, SequenceProcessingStrategy};
33pub use state::{OptimizerStateSnapshot, TransformerOptimizerState};
34
35// Re-export for backward compatibility - create alias for the old name
36pub use TransformerBasedOptimizerConfig as TransformerOptimizerConfig;
37
38use scirs2_core::ndarray::{Array1, Array2};
39use scirs2_core::numeric::Float;
40use std::fmt::Debug;
41use std::time::{Duration, Instant};
42
43use crate::error::{OptimError, Result};
44
45/// Transformer-based meta-learning optimizer
46pub struct TransformerOptimizer<
47    T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
48> {
49    /// Core transformer architecture
50    transformer: TransformerArchitecture<T>,
51
52    /// Positional encoding for sequence modeling
53    positional_encoding: PositionalEncoding<T>,
54
55    // NOTE: this struct used to also own a standalone `MultiHeadAttention` and
56    // one `FeedForwardNetwork` per layer. Nothing ever read them: every
57    // attention and feed-forward computation happens inside
58    // `TransformerArchitecture`'s own `TransformerLayer`s. At the default
59    // configuration (model_dimension 512, feedforward_dimension 2048, 6 layers)
60    // those duplicates allocated ~12.6M dead parameters, which is why
61    // constructing this optimizer took ~13s and its construction test was
62    // `#[ignore]`d. They are gone.
63    /// Meta-learning components
64    meta_learning: TransformerMetaLearning<T>,
65
66    /// Sequence processor for optimization trajectories
67    sequence_processor: OptimizationSequenceProcessor<T>,
68
69    /// Memory management for long sequences
70    memory_manager: TransformerMemoryManager<T>,
71
72    /// Configuration
73    config: TransformerBasedOptimizerConfig<T>,
74
75    /// Performance tracking
76    performance_tracker: TransformerPerformanceTracker<T>,
77
78    /// State management
79    state: TransformerOptimizerState<T>,
80}
81
82impl<
83        T: Float
84            + Debug
85            + Send
86            + Sync
87            + 'static
88            + scirs2_core::ndarray::ScalarOperand
89            + scirs2_core::numeric::FromPrimitive,
90    > TransformerOptimizer<T>
91{
92    /// Create new transformer optimizer.
93    ///
94    /// # Errors
95    ///
96    /// Returns [`OptimError::InvalidConfig`] if `config` (or any of its three
97    /// sub-configurations — memory, meta-learning, performance) is inconsistent.
98    ///
99    /// F46: `TransformerBasedOptimizerConfig::validate` and the three sub-config
100    /// validators it delegates to existed and were correct, but *nothing in the
101    /// crate ever called them*. A `model_dimension` of 0, an
102    /// `attention_head_dimension · num_attention_heads` that did not equal
103    /// `model_dimension`, a `dropout_rate` of 2.0 or a zero `batch_size` were all
104    /// accepted here and then produced either a wrong answer or a panic several
105    /// layers down. The check now runs before anything is allocated.
106    pub fn new(config: TransformerBasedOptimizerConfig<T>) -> Result<Self> {
107        config.validate().map_err(OptimError::InvalidConfig)?;
108
109        let transformer_config = TransformerArchConfig::from_optimizer_config(&config);
110        let transformer = TransformerArchitecture::new(transformer_config)?;
111
112        let positional_encoding = PositionalEncoding::new(
113            config.sequence_length,
114            config.model_dimension,
115            config.positional_encoding_type,
116        )?;
117
118        let meta_learning = TransformerMetaLearning::new(&config)?;
119        let sequence_processor = OptimizationSequenceProcessor::new(&config)?;
120        let memory_manager = TransformerMemoryManager::new(&config)?;
121        let performance_tracker = TransformerPerformanceTracker::new();
122        let state = TransformerOptimizerState::new(&config)?;
123
124        Ok(Self {
125            transformer,
126            positional_encoding,
127            meta_learning,
128            sequence_processor,
129            memory_manager,
130            config,
131            performance_tracker,
132            state,
133        })
134    }
135
136    /// Generate optimization step using transformer
137    pub fn generate_optimization_step(
138        &mut self,
139        gradient_history: &Array2<T>,
140        parameter_history: &Array2<T>,
141        loss_history: &Array1<T>,
142    ) -> Result<Array1<T>> {
143        let start_time = Instant::now();
144
145        // Process input sequences
146        let processed_sequence = self.sequence_processor.process_optimization_sequence(
147            gradient_history,
148            parameter_history,
149            loss_history,
150        )?;
151
152        // Apply positional encoding
153        let encoded_sequence = self.positional_encoding.encode(&processed_sequence)?;
154
155        // Forward pass through transformer
156        let transformer_output = self.transformer.forward(&encoded_sequence)?;
157
158        // Generate optimization step
159        let optimization_step = self
160            .meta_learning
161            .generate_update(&transformer_output, &self.state.current_parameters)?;
162
163        // Update state
164        self.state
165            .update_with_step(&optimization_step, loss_history.last().copied())?;
166
167        // Track performance
168        let elapsed = start_time.elapsed();
169        self.performance_tracker
170            .record_optimization_step(elapsed, &optimization_step);
171
172        Ok(optimization_step)
173    }
174
175    /// Train the transformer on optimization trajectories
176    pub fn train_on_trajectories(
177        &mut self,
178        trajectories: &[OptimizationTrajectory<T>],
179    ) -> Result<TrainingMetrics> {
180        let start_time = Instant::now();
181        let mut total_loss = T::zero();
182        let mut batch_count = 0;
183        let mut applied_gradient_energy = T::zero();
184
185        for trajectory in trajectories {
186            // Process trajectory into sequences
187            let sequences = self
188                .sequence_processor
189                .trajectory_to_sequences(trajectory)?;
190
191            for sequence in sequences {
192                // Forward pass, recording the activations the backward pass needs
193                let encoded = self.positional_encoding.encode(&sequence.input)?;
194                let (prediction, cache) = self.transformer.forward_with_cache(&encoded)?;
195
196                // Calculate loss
197                let loss = self.calculate_sequence_loss(&prediction, &sequence.target)?;
198                total_loss = total_loss + loss;
199
200                // Backward pass: real gradients, real parameter update
201                applied_gradient_energy = applied_gradient_energy
202                    + self.backward_pass(&prediction, &sequence.target, &cache, loss)?;
203
204                batch_count += 1;
205            }
206        }
207
208        let avg_loss = if batch_count > 0 {
209            total_loss
210                / scirs2_core::numeric::NumCast::from(batch_count).unwrap_or_else(|| T::zero())
211        } else {
212            T::zero()
213        };
214
215        let training_time = start_time.elapsed();
216
217        let metrics = TrainingMetrics {
218            loss: avg_loss.to_f64().unwrap_or(0.0),
219            training_time,
220            num_sequences: batch_count,
221            convergence_rate: self.calculate_convergence_rate()?,
222            applied_gradient_energy: applied_gradient_energy.to_f64().unwrap_or(0.0),
223        };
224
225        self.performance_tracker
226            .record_training_epoch(metrics.clone());
227
228        Ok(metrics)
229    }
230
231    /// Forward pass through the transformer for a sequence (inference path).
232    pub fn forward_sequence(&mut self, sequence: &Array2<T>) -> Result<Array2<T>> {
233        // Apply positional encoding
234        let encoded_sequence = self.positional_encoding.encode(sequence)?;
235
236        // Forward through transformer
237        self.transformer.forward(&encoded_sequence)
238    }
239
240    /// Calculate loss for sequence prediction
241    fn calculate_sequence_loss(&self, prediction: &Array2<T>, target: &Array2<T>) -> Result<T> {
242        if prediction.shape() != target.shape() {
243            return Err(OptimError::Other(
244                "Shape mismatch in loss calculation".to_string(),
245            ));
246        }
247
248        if prediction.is_empty() {
249            return Err(OptimError::InvalidConfig(
250                "cannot take the mean squared error of an empty prediction".to_string(),
251            ));
252        }
253
254        // Mean squared error
255        let diff = prediction - target;
256        let squared_diff = &diff * &diff;
257        let sum = squared_diff.sum();
258        let count = T::from(prediction.len()).ok_or_else(|| {
259            OptimError::ComputationError(format!(
260                "cannot represent the element count {} in the value type",
261                prediction.len()
262            ))
263        })?;
264
265        Ok(sum / count)
266    }
267
268    /// Backward pass: differentiate the sequence MSE and update the transformer.
269    ///
270    /// The loss is `L = mean((pred - target)²)` over all `N` cells, so the
271    /// gradient handed to the network is `dL/dpred = 2·(pred - target)/N`. That
272    /// gradient is propagated by [`TransformerArchitecture::backward`], which
273    /// updates the output projection, the final layer norm, every layer's
274    /// feed-forward sub-layer (with its layer norms) and the input embedding.
275    /// The attention sub-layers ride their residual identity branch and stay
276    /// frozen — the first-order approximation documented on that method.
277    ///
278    /// `enable_gradient_clipping` / `gradient_clip_value` are honoured by
279    /// rescaling `dL/dpred` when its Frobenius norm exceeds the threshold.
280    ///
281    /// Returns `Err` on a shape mismatch instead of silently doing nothing.
282    fn backward_pass(
283        &mut self,
284        prediction: &Array2<T>,
285        target: &Array2<T>,
286        cache: &ArchitectureCache<T>,
287        loss: T,
288    ) -> Result<T> {
289        if prediction.shape() != target.shape() {
290            return Err(OptimError::Other(format!(
291                "Shape mismatch in backward pass: prediction {:?} vs target {:?}",
292                prediction.shape(),
293                target.shape()
294            )));
295        }
296
297        let count = T::from(prediction.len()).unwrap_or_else(|| T::one());
298        let two = T::one() + T::one();
299        let mut grad_output = (prediction - target).mapv(|d| two * d / count);
300
301        if self.config.enable_gradient_clipping {
302            let norm = grad_output
303                .iter()
304                .map(|&g| g * g)
305                .fold(T::zero(), |a, b| a + b)
306                .sqrt();
307            let threshold = self.config.gradient_clip_value;
308            if threshold > T::zero() && norm > threshold {
309                let scale = threshold / norm;
310                grad_output.mapv_inplace(|g| g * scale);
311            }
312        }
313
314        let applied = self
315            .transformer
316            .backward(cache, &grad_output, self.config.learning_rate)?;
317
318        self.meta_learning.update_from_loss(loss)?;
319        Ok(applied)
320    }
321
322    /// Calculate convergence rate
323    fn calculate_convergence_rate(&self) -> Result<f64> {
324        let loss_history = self.performance_tracker.get_loss_history();
325        if loss_history.len() < 2 {
326            return Ok(0.0);
327        }
328
329        // Most recent first; the window is the last 10 recorded losses.
330        let recent_losses: Vec<_> = loss_history.iter().rev().take(10).collect();
331        // `first`/`last` cannot fail after this guard, but pattern-matching says so
332        // to the compiler instead of asserting it at runtime.
333        let (Some(final_loss), Some(initial_loss)) = (recent_losses.first(), recent_losses.last())
334        else {
335            return Ok(0.0);
336        };
337        let (initial_loss, final_loss) = (**initial_loss, **final_loss);
338        if initial_loss == 0.0 {
339            // Already at zero loss: no relative improvement is definable.
340            return Ok(0.0);
341        }
342
343        let improvement = (initial_loss - final_loss) / initial_loss;
344        Ok(improvement.clamp(0.0, 1.0))
345    }
346
347    /// Adopt a new architecture configuration.
348    ///
349    /// The return value says exactly what happened, because the two cases differ
350    /// in a way callers must not be left guessing about:
351    ///
352    /// * [`ArchitectureUpdate::Unchanged`] — the proposal matched the live
353    ///   configuration; nothing was touched.
354    /// * [`ArchitectureUpdate::InPlace`] — only non-structural fields differed
355    ///   (`learning_rate`, `dropout_rate`, clipping, weight decay). They were
356    ///   adopted and **all learned weights were kept**.
357    /// * [`ArchitectureUpdate::Rebuilt`] — a structural dimension differed
358    ///   (`model_dimension`, `num_transformer_layers`, `num_attention_heads`,
359    ///   `attention_head_dimension`, `feedforward_dimension`, `sequence_length`,
360    ///   `activation_function`, `positional_encoding_type`). Weights of one shape
361    ///   cannot be reinterpreted at another, so the transformer stack, positional
362    ///   encoding, meta-learning networks and optimizer state were **rebuilt from
363    ///   a fresh initialization — every learned parameter was discarded.**
364    ///
365    /// That last case is destructive, which is why it is reported rather than
366    /// signalled by a bare `true`. `AdaptiveTransformerEnhancement`'s architecture
367    /// proposal is a function of the measured landscape rather than of the current
368    /// architecture, so repeated enhancement converges and stops rebuilding
369    /// instead of wiping the weights on every call.
370    ///
371    /// This is also what makes
372    /// `AdaptiveTransformerEnhancement::enhance_optimizer` actually enhance the
373    /// optimizer it is given; before, its `&mut TransformerOptimizer` argument
374    /// was never touched.
375    ///
376    /// # Errors
377    /// Propagates construction errors from the new configuration (e.g. a head
378    /// count that does not divide the model dimension), leaving `self` unchanged.
379    pub fn apply_architecture_config(
380        &mut self,
381        config: &TransformerBasedOptimizerConfig<T>,
382    ) -> Result<ArchitectureUpdate> {
383        let structural_change = config.model_dimension != self.config.model_dimension
384            || config.num_transformer_layers != self.config.num_transformer_layers
385            || config.num_attention_heads != self.config.num_attention_heads
386            || config.attention_head_dimension != self.config.attention_head_dimension
387            || config.feedforward_dimension != self.config.feedforward_dimension
388            || config.sequence_length != self.config.sequence_length
389            || config.activation_function != self.config.activation_function
390            || config.positional_encoding_type != self.config.positional_encoding_type;
391
392        if structural_change {
393            // Build everything first so a failure leaves `self` untouched.
394            let transformer_config = TransformerArchConfig::from_optimizer_config(config);
395            let transformer = TransformerArchitecture::new(transformer_config)?;
396            let positional_encoding = PositionalEncoding::new(
397                config.sequence_length,
398                config.model_dimension,
399                config.positional_encoding_type,
400            )?;
401            let meta_learning = TransformerMetaLearning::new(config)?;
402            let sequence_processor = OptimizationSequenceProcessor::new(config)?;
403            let memory_manager = TransformerMemoryManager::new(config)?;
404            let state = TransformerOptimizerState::new(config)?;
405
406            self.transformer = transformer;
407            self.positional_encoding = positional_encoding;
408            self.meta_learning = meta_learning;
409            self.sequence_processor = sequence_processor;
410            self.memory_manager = memory_manager;
411            self.state = state;
412            self.config = config.clone();
413            return Ok(ArchitectureUpdate::Rebuilt);
414        }
415
416        let non_structural_change = config.learning_rate != self.config.learning_rate
417            || config.dropout_rate != self.config.dropout_rate
418            || config.enable_gradient_clipping != self.config.enable_gradient_clipping
419            || config.gradient_clip_value != self.config.gradient_clip_value
420            || config.weight_decay != self.config.weight_decay;
421        if non_structural_change {
422            self.config = config.clone();
423            return Ok(ArchitectureUpdate::InPlace);
424        }
425
426        Ok(ArchitectureUpdate::Unchanged)
427    }
428
429    /// The configuration currently in force.
430    pub fn config(&self) -> &TransformerBasedOptimizerConfig<T> {
431        &self.config
432    }
433
434    /// Total number of learnable parameters this optimizer owns.
435    ///
436    /// Exactly the transformer architecture plus the meta-learning adaptation
437    /// networks — the only two subsystems that hold weights.
438    pub fn parameter_count(&self) -> usize {
439        self.transformer.parameter_count() + self.meta_learning.parameter_count()
440    }
441
442    /// Get current state
443    pub fn get_state(&self) -> &TransformerOptimizerState<T> {
444        &self.state
445    }
446
447    /// Get performance metrics
448    pub fn get_performance_metrics(&self) -> &TransformerPerformanceTracker<T> {
449        &self.performance_tracker
450    }
451
452    /// Reset optimizer state
453    pub fn reset_state(&mut self) -> Result<()> {
454        self.state = TransformerOptimizerState::new(&self.config)?;
455        self.performance_tracker.reset();
456        Ok(())
457    }
458}
459
460/// Optimization trajectory for training
461#[derive(Debug, Clone)]
462pub struct OptimizationTrajectory<
463    T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
464> {
465    pub gradient_sequence: Array2<T>,
466    pub parameter_sequence: Array2<T>,
467    pub loss_sequence: Array1<T>,
468    pub metadata: TrajectoryMetadata,
469}
470
471/// Trajectory metadata
472#[derive(Debug, Clone)]
473pub struct TrajectoryMetadata {
474    pub task_id: String,
475    pub optimizer_type: String,
476    pub convergence_achieved: bool,
477    pub total_steps: usize,
478}
479
480/// Training sequence
481#[derive(Debug, Clone)]
482pub struct TrainingSequence<
483    T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
484> {
485    pub input: Array2<T>,
486    pub target: Array2<T>,
487    pub sequence_length: usize,
488}
489
490/// Outcome of [`TransformerOptimizer::apply_architecture_config`].
491///
492/// Distinguishes a no-op from a weight-preserving update from a **destructive**
493/// rebuild, so a caller can never mistake the third for the second.
494#[derive(Debug, Clone, Copy, PartialEq, Eq)]
495pub enum ArchitectureUpdate {
496    /// The proposal already matched the live configuration.
497    Unchanged,
498    /// Only non-structural fields changed; all learned weights were kept.
499    InPlace,
500    /// A structural dimension changed, so every learned parameter was discarded
501    /// and re-initialized.
502    Rebuilt,
503}
504
505impl ArchitectureUpdate {
506    /// Whether anything changed at all.
507    pub fn changed(self) -> bool {
508        !matches!(self, ArchitectureUpdate::Unchanged)
509    }
510
511    /// Whether learned parameters were discarded.
512    pub fn discarded_weights(self) -> bool {
513        matches!(self, ArchitectureUpdate::Rebuilt)
514    }
515}
516
517/// Training metrics
518#[derive(Debug, Clone)]
519pub struct TrainingMetrics {
520    pub loss: f64,
521    pub training_time: Duration,
522    pub num_sequences: usize,
523    pub convergence_rate: f64,
524    /// Total squared magnitude of the gradient that reached the input embedding
525    /// across every sequence in this epoch.
526    ///
527    /// This is strictly positive whenever a backward pass actually propagated a
528    /// non-zero gradient, and exactly zero when nothing was propagated — which
529    /// is what distinguishes the real backward pass from the previous no-op
530    /// that only recorded the loss.
531    pub applied_gradient_energy: f64,
532}
533
534#[cfg(test)]
535mod tests {
536    use super::*;
537
538    /// Small but structurally complete configuration for the training tests.
539    fn small_config() -> TransformerBasedOptimizerConfig<f64> {
540        TransformerBasedOptimizerConfig::<f64> {
541            model_dimension: 8,
542            num_transformer_layers: 2,
543            num_attention_heads: 2,
544            attention_head_dimension: 4,
545            feedforward_dimension: 16,
546            sequence_length: 8,
547            learning_rate: 1e-2,
548            dropout_rate: 0.0,
549            ..Default::default()
550        }
551    }
552
553    fn trajectory(steps: usize, width: usize, scale: f64) -> OptimizationTrajectory<f64> {
554        let gradient_sequence = Array2::from_shape_fn((steps, width), |(i, j)| {
555            scale * ((i * width + j) as f64 * 0.017).sin()
556        });
557        let parameter_sequence = Array2::from_shape_fn((steps, width), |(i, j)| {
558            scale * ((i * width + j) as f64 * 0.031).cos()
559        });
560        let loss_sequence = Array1::from_shape_fn(steps, |i| scale / (1.0 + i as f64));
561        OptimizationTrajectory {
562            gradient_sequence,
563            parameter_sequence,
564            loss_sequence,
565            metadata: TrajectoryMetadata {
566                task_id: "regression".to_string(),
567                optimizer_type: "adam".to_string(),
568                convergence_achieved: false,
569                total_steps: steps,
570            },
571        }
572    }
573
574    #[test]
575    fn test_transformer_optimizer_creation() {
576        let config = TransformerBasedOptimizerConfig::default();
577        let optimizer = TransformerOptimizer::<f32>::new(config);
578        assert!(optimizer.is_ok());
579    }
580
581    /// F46: `TransformerBasedOptimizerConfig::validate` (and the memory /
582    /// meta-learning / performance validators it delegates to) had **zero call
583    /// sites in the crate**. Every one of the configurations below used to be
584    /// accepted by `TransformerOptimizer::new`, which then either produced a
585    /// silently wrong answer or panicked somewhere downstream.
586    ///
587    /// The list deliberately covers the top-level validator *and* all three
588    /// sub-config validators, because delegation is the part that is easy to
589    /// drop.
590    #[test]
591    fn construction_rejects_every_invalid_configuration() {
592        // A small but valid baseline, so each case below differs in exactly one
593        // field and the default's own consistency is not what is under test.
594        let base = || TransformerBasedOptimizerConfig::<f32> {
595            model_dimension: 8,
596            num_transformer_layers: 1,
597            num_attention_heads: 2,
598            attention_head_dimension: 4,
599            feedforward_dimension: 16,
600            sequence_length: 4,
601            batch_size: 2,
602            ..Default::default()
603        };
604        assert!(
605            TransformerOptimizer::<f32>::new(base()).is_ok(),
606            "the baseline configuration must itself be valid"
607        );
608
609        let mut cases: Vec<(&str, TransformerBasedOptimizerConfig<f32>)> = Vec::new();
610
611        // --- top-level validator ---
612        cases.push(("model_dimension = 0", {
613            let mut c = base();
614            c.model_dimension = 0;
615            c
616        }));
617        cases.push(("num_transformer_layers = 0", {
618            let mut c = base();
619            c.num_transformer_layers = 0;
620            c
621        }));
622        cases.push(("num_attention_heads = 0", {
623            let mut c = base();
624            c.num_attention_heads = 0;
625            c
626        }));
627        cases.push(("heads do not divide model_dimension", {
628            let mut c = base();
629            c.num_attention_heads = 3;
630            c
631        }));
632        cases.push(("head_dim * heads != model_dimension", {
633            let mut c = base();
634            c.attention_head_dimension = 3;
635            c
636        }));
637        cases.push(("sequence_length = 0", {
638            let mut c = base();
639            c.sequence_length = 0;
640            c
641        }));
642        cases.push(("dropout_rate above 1", {
643            let mut c = base();
644            c.dropout_rate = 2.0;
645            c
646        }));
647        cases.push(("negative learning_rate", {
648            let mut c = base();
649            c.learning_rate = -1.0;
650            c
651        }));
652        cases.push(("batch_size = 0", {
653            let mut c = base();
654            c.batch_size = 0;
655            c
656        }));
657
658        // --- delegated: MemoryConfig ---
659        cases.push(("max_cache_size = 0", {
660            let mut c = base();
661            c.memory_config.max_cache_size = 0;
662            c
663        }));
664        cases.push(("allocation_block_size = 0", {
665            let mut c = base();
666            c.memory_config.allocation_block_size = 0;
667            c
668        }));
669
670        // --- delegated: MetaLearningConfig ---
671        cases.push(("meta_learning_rate = 0", {
672            let mut c = base();
673            c.meta_learning_config.meta_learning_rate = 0.0;
674            c
675        }));
676        cases.push(("inner_steps = 0", {
677            let mut c = base();
678            c.meta_learning_config.inner_steps = 0;
679            c
680        }));
681        cases.push(("num_support = 0", {
682            let mut c = base();
683            c.meta_learning_config.num_support = 0;
684            c
685        }));
686
687        // --- delegated: PerformanceConfig ---
688        cases.push(("metrics_interval = 0", {
689            let mut c = base();
690            c.performance_config.metrics_interval = 0;
691            c
692        }));
693        cases.push(("max_history_size = 0", {
694            let mut c = base();
695            c.performance_config.max_history_size = 0;
696            c
697        }));
698
699        for (label, config) in cases {
700            let outcome = TransformerOptimizer::<f32>::new(config);
701            assert!(
702                outcome.is_err(),
703                "`{label}` was accepted by TransformerOptimizer::new"
704            );
705        }
706    }
707
708    /// F62: this struct used to also own a standalone `MultiHeadAttention`
709    /// (4·512² = 1.05M) plus one `FeedForwardNetwork` per layer
710    /// (6 · 2·512·2048 = 12.6M) that nothing ever read. Those duplicates are
711    /// gone, so the optimizer's live parameter count must now equal exactly the
712    /// sum of the two subsystems that actually compute — no more, no less.
713    #[test]
714    fn no_dead_duplicate_parameter_blocks() {
715        let config = TransformerBasedOptimizerConfig::<f32>::default();
716        let model_dim = config.model_dimension;
717        let ff_dim = config.feedforward_dimension;
718        let layers = config.num_transformer_layers;
719        let optimizer = TransformerOptimizer::<f32>::new(config).expect("default construction");
720
721        let live = optimizer.parameter_count();
722        let dead_attention = 4 * model_dim * model_dim;
723        let dead_feedforward = layers * 2 * model_dim * ff_dim;
724
725        assert!(
726            live > 0,
727            "the optimizer must own some parameters ({live} found)"
728        );
729        // The removed duplicates were >13M parameters; if they came back the
730        // live count would exceed the architecture+meta total by that much.
731        let expected =
732            optimizer.transformer.parameter_count() + optimizer.meta_learning.parameter_count();
733        assert_eq!(
734            live,
735            expected,
736            "live parameter count {live} does not match \
737             transformer+meta {expected}; something allocated {} extra \
738             (dead attention {dead_attention}, dead feed-forward {dead_feedforward})",
739            live.saturating_sub(expected)
740        );
741    }
742
743    /// F26: `backward_pass` used to only call `update_from_loss` — no gradients,
744    /// no parameter update. A real backward pass must (a) report non-zero
745    /// applied gradient energy and (b) actually move the network's outputs.
746    #[test]
747    fn train_on_trajectories_propagates_real_gradients() {
748        let config = small_config();
749        let width = config.model_dimension;
750        let mut optimizer =
751            TransformerOptimizer::<f64>::new(config).expect("optimizer construction");
752
753        let probe = Array2::from_shape_fn((4, width), |(i, j)| ((i + j) as f64 * 0.05).tanh());
754        let before = optimizer
755            .forward_sequence(&probe)
756            .expect("forward before training");
757
758        let trajectories = vec![trajectory(12, width, 1.0)];
759        let metrics = optimizer
760            .train_on_trajectories(&trajectories)
761            .expect("training");
762
763        assert!(
764            metrics.num_sequences > 0,
765            "the trajectory must produce at least one training sequence"
766        );
767        assert!(
768            metrics.applied_gradient_energy > 0.0,
769            "backward pass propagated no gradient (energy {})",
770            metrics.applied_gradient_energy
771        );
772
773        let after = optimizer
774            .forward_sequence(&probe)
775            .expect("forward after training");
776        let moved = (&after - &before)
777            .iter()
778            .map(|d| d.abs())
779            .fold(0.0, f64::max);
780        assert!(
781            moved > 1e-12,
782            "training left the network unchanged (max output delta {moved})"
783        );
784    }
785
786    /// F26: repeated epochs on the same data must reduce the training loss —
787    /// the old no-op backward pass produced a flat loss curve forever.
788    #[test]
789    fn repeated_training_epochs_reduce_the_loss() {
790        let config = small_config();
791        let width = config.model_dimension;
792        let mut optimizer =
793            TransformerOptimizer::<f64>::new(config).expect("optimizer construction");
794
795        let trajectories = vec![trajectory(12, width, 1.0)];
796        let first = optimizer
797            .train_on_trajectories(&trajectories)
798            .expect("epoch 1")
799            .loss;
800        let mut last = first;
801        for _ in 0..12 {
802            last = optimizer
803                .train_on_trajectories(&trajectories)
804                .expect("later epoch")
805                .loss;
806        }
807
808        assert!(
809            last < first,
810            "loss did not decrease across epochs: {first} -> {last}"
811        );
812    }
813
814    /// F26: a shape mismatch must be an error, not a silently ignored no-op.
815    #[test]
816    fn sequence_loss_rejects_shape_mismatch() {
817        let config = small_config();
818        let optimizer = TransformerOptimizer::<f64>::new(config).expect("construction");
819        let prediction = Array2::<f64>::zeros((3, 8));
820        let target = Array2::<f64>::zeros((4, 8));
821        assert!(optimizer
822            .calculate_sequence_loss(&prediction, &target)
823            .is_err());
824    }
825
826    #[test]
827    fn test_trajectory_creation() {
828        let trajectory = OptimizationTrajectory::<f32> {
829            gradient_sequence: Array2::zeros((10, 5)),
830            parameter_sequence: Array2::zeros((10, 5)),
831            loss_sequence: Array1::zeros(10),
832            metadata: TrajectoryMetadata {
833                task_id: "test".to_string(),
834                optimizer_type: "adam".to_string(),
835                convergence_achieved: true,
836                total_steps: 10,
837            },
838        };
839
840        assert_eq!(trajectory.gradient_sequence.shape(), &[10, 5]);
841        assert_eq!(trajectory.loss_sequence.len(), 10);
842    }
843}