Skip to main content

optirs_core/neural_integration/
mod.rs

1// Neural network integration for optimizers
2//
3// This module provides interfaces and utilities for integrating optimizers with neural networks,
4// including generic parameter optimization, lazy registration, and architecture-aware optimizations.
5
6use crate::error::{OptimError, Result};
7use crate::utils::{scalar_opt, scalar_or, try_scalar};
8use scirs2_core::ndarray::{Array, Dimension, ScalarOperand};
9use scirs2_core::numeric::Float;
10use std::collections::HashMap;
11use std::fmt::Debug;
12// use statrs::statistics::Statistics; // statrs not available
13
14/// Type alias for layer identifiers
15pub type LayerId = String;
16
17/// Type alias for parameter identifiers
18pub type ParamId = String;
19
20/// Parameter metadata for neural network parameters
21#[derive(Debug, Clone)]
22pub struct ParameterMetadata {
23    /// Layer name this parameter belongs to
24    pub layername: LayerId,
25    /// Parameter name within the layer
26    pub param_name: ParamId,
27    /// Parameter shape
28    pub shape: Vec<usize>,
29    /// Whether parameter requires gradients
30    pub requires_grad: bool,
31    /// Parameter type (weights, bias, etc.)
32    pub paramtype: ParameterType,
33    /// Sharing group for parameter sharing
34    pub sharing_group: Option<String>,
35    /// Custom tags for architecture-specific optimizations
36    pub tags: Vec<String>,
37}
38
39/// Types of neural network parameters
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum ParameterType {
42    /// Weight matrices
43    Weight,
44    /// Bias vectors
45    Bias,
46    /// Normalization parameters (scale/shift)
47    Normalization,
48    /// Embedding parameters
49    Embedding,
50    /// Attention parameters
51    Attention,
52    /// Custom parameter type
53    Custom,
54}
55
56/// Layer architecture information
57#[derive(Debug, Clone)]
58pub struct LayerArchitecture {
59    /// Layer type name
60    pub layer_type: String,
61    /// Input dimensions
62    pub input_dims: Vec<usize>,
63    /// Output dimensions
64    pub output_dims: Vec<usize>,
65    /// Layer-specific configuration
66    pub config: HashMap<String, LayerConfig>,
67    /// Whether layer is trainable
68    pub trainable: bool,
69}
70
71/// Layer configuration values
72#[derive(Debug, Clone)]
73pub enum LayerConfig {
74    /// Integer value
75    Int(i64),
76    /// Float value
77    Float(f64),
78    /// String value
79    String(String),
80    /// Boolean value
81    Bool(bool),
82    /// List of values
83    List(Vec<LayerConfig>),
84}
85
86/// Generic parameter optimization interface
87pub trait ParameterOptimizer<A: Float, D: Dimension> {
88    /// Register a parameter for optimization
89    fn register_parameter(
90        &mut self,
91        paramid: ParamId,
92        parameter: &Array<A, D>,
93        metadata: ParameterMetadata,
94    ) -> Result<()>;
95
96    /// Update registered parameters with gradients
97    fn step(
98        &mut self,
99        gradients: HashMap<ParamId, Array<A, D>>,
100        parameters: &mut HashMap<ParamId, Array<A, D>>,
101    ) -> Result<()>;
102
103    /// Get parameter-specific learning rate
104    fn get_learning_rate(&self, paramid: &ParamId) -> Option<A>;
105
106    /// Set parameter-specific learning rate
107    fn set_learning_rate(&mut self, paramid: &ParamId, lr: A) -> Result<()>;
108
109    /// Get optimizer state for a parameter
110    fn get_parameter_state(&self, paramid: &ParamId) -> Option<&HashMap<String, Array<A, D>>>;
111
112    /// Reset optimizer state
113    fn reset_state(&mut self);
114
115    /// Get all registered parameter IDs
116    fn registered_parameters(&self) -> Vec<ParamId>;
117}
118
119/// Neural network parameter manager with lazy registration
120#[derive(Debug)]
121pub struct ParameterManager<A: Float, D: Dimension> {
122    /// Registered parameters with metadata
123    parameters: HashMap<ParamId, ParameterMetadata>,
124    /// Parameter optimizer states
125    optimizer_states: HashMap<ParamId, HashMap<String, Array<A, D>>>,
126    /// Layer architectures
127    layer_architectures: HashMap<LayerId, LayerArchitecture>,
128    /// Parameter sharing groups
129    sharing_groups: HashMap<String, Vec<ParamId>>,
130    /// Layer-specific optimization rules
131    layer_rules: HashMap<LayerId, LayerOptimizationRule<A>>,
132    /// Global optimization configuration
133    global_config: OptimizationConfig<A>,
134    /// Lazy registration mode
135    lazy_mode: bool,
136    /// Pending registrations (for lazy mode)
137    pending_registrations: Vec<(ParamId, ParameterMetadata)>,
138}
139
140/// Layer-specific optimization rules
141#[derive(Debug, Clone)]
142pub struct LayerOptimizationRule<A: Float> {
143    /// Learning rate multiplier for this layer
144    pub lr_multiplier: A,
145    /// Weight decay multiplier
146    pub weight_decay_multiplier: A,
147    /// Whether to freeze this layer
148    pub frozen: bool,
149    /// Custom optimizer settings
150    pub custom_settings: HashMap<String, LayerConfig>,
151}
152
153/// Global optimization configuration
154#[derive(Debug, Clone)]
155pub struct OptimizationConfig<A: Float> {
156    /// Base learning rate
157    pub base_learning_rate: A,
158    /// Global weight decay
159    pub weight_decay: A,
160    /// Gradient clipping threshold
161    pub gradient_clip: Option<A>,
162    /// Whether to use mixed precision
163    pub mixed_precision: bool,
164    /// Architecture-specific optimizations
165    pub architecture_optimizations: HashMap<String, bool>,
166}
167
168impl<A: Float + ScalarOperand + Debug + Send + Sync, D: Dimension + Send + Sync>
169    ParameterManager<A, D>
170{
171    /// Create a new parameter manager
172    pub fn new(config: OptimizationConfig<A>) -> Self {
173        Self {
174            parameters: HashMap::new(),
175            optimizer_states: HashMap::new(),
176            layer_architectures: HashMap::new(),
177            sharing_groups: HashMap::new(),
178            layer_rules: HashMap::new(),
179            global_config: config,
180            lazy_mode: false,
181            pending_registrations: Vec::new(),
182        }
183    }
184
185    /// Enable lazy registration mode
186    pub fn enable_lazy_mode(&mut self) {
187        self.lazy_mode = true;
188    }
189
190    /// Disable lazy registration mode and process pending registrations
191    pub fn disable_lazy_mode(&mut self) -> Result<()> {
192        self.lazy_mode = false;
193
194        // Process all pending registrations
195        let pending = std::mem::take(&mut self.pending_registrations);
196        for (paramid, metadata) in pending {
197            self.register_parameter_impl(paramid, metadata)?;
198        }
199
200        Ok(())
201    }
202
203    /// Register a layer architecture
204    pub fn register_layer(&mut self, layerid: LayerId, architecture: LayerArchitecture) {
205        self.layer_architectures.insert(layerid, architecture);
206    }
207
208    /// Set layer-specific optimization rule
209    pub fn set_layer_rule(&mut self, layerid: LayerId, rule: LayerOptimizationRule<A>) {
210        self.layer_rules.insert(layerid, rule);
211    }
212
213    /// Register a parameter
214    pub fn register_parameter(
215        &mut self,
216        paramid: ParamId,
217        metadata: ParameterMetadata,
218    ) -> Result<()> {
219        if self.lazy_mode {
220            self.pending_registrations.push((paramid, metadata));
221            Ok(())
222        } else {
223            self.register_parameter_impl(paramid, metadata)
224        }
225    }
226
227    /// Internal parameter registration implementation
228    fn register_parameter_impl(
229        &mut self,
230        paramid: ParamId,
231        metadata: ParameterMetadata,
232    ) -> Result<()> {
233        // Handle parameter sharing
234        if let Some(sharing_group) = &metadata.sharing_group {
235            self.sharing_groups
236                .entry(sharing_group.clone())
237                .or_default()
238                .push(paramid.clone());
239        }
240
241        // Initialize optimizer state for this parameter
242        self.optimizer_states
243            .insert(paramid.clone(), HashMap::new());
244
245        // Store parameter metadata
246        self.parameters.insert(paramid, metadata);
247
248        Ok(())
249    }
250
251    /// Get effective learning rate for a parameter
252    pub fn get_effective_learning_rate(&self, paramid: &ParamId) -> A {
253        let base_lr = self.global_config.base_learning_rate;
254
255        if let Some(metadata) = self.parameters.get(paramid) {
256            if let Some(rule) = self.layer_rules.get(&metadata.layername) {
257                return base_lr * rule.lr_multiplier;
258            }
259        }
260
261        base_lr
262    }
263
264    /// Get effective weight decay for a parameter
265    pub fn get_effective_weight_decay(&self, paramid: &ParamId) -> A {
266        let base_decay = self.global_config.weight_decay;
267
268        if let Some(metadata) = self.parameters.get(paramid) {
269            if let Some(rule) = self.layer_rules.get(&metadata.layername) {
270                return base_decay * rule.weight_decay_multiplier;
271            }
272        }
273
274        base_decay
275    }
276
277    /// Check if parameter is frozen
278    pub fn is_parameter_frozen(&self, paramid: &ParamId) -> bool {
279        if let Some(metadata) = self.parameters.get(paramid) {
280            if let Some(rule) = self.layer_rules.get(&metadata.layername) {
281                return rule.frozen;
282            }
283        }
284        false
285    }
286
287    /// Get parameters in a sharing group
288    pub fn get_sharing_group(&self, groupname: &str) -> Option<&[ParamId]> {
289        self.sharing_groups.get(groupname).map(|v| v.as_slice())
290    }
291
292    /// Get all registered parameters
293    pub fn get_all_parameters(&self) -> &HashMap<ParamId, ParameterMetadata> {
294        &self.parameters
295    }
296
297    /// Get layer architecture
298    pub fn get_layer_architecture(&self, layerid: &LayerId) -> Option<&LayerArchitecture> {
299        self.layer_architectures.get(layerid)
300    }
301
302    /// Get parameter metadata
303    pub fn get_parameter_metadata(&self, paramid: &ParamId) -> Option<&ParameterMetadata> {
304        self.parameters.get(paramid)
305    }
306
307    /// Update global configuration
308    pub fn update_config(&mut self, config: OptimizationConfig<A>) {
309        self.global_config = config;
310    }
311
312    /// Get optimizer state for parameter
313    pub fn get_optimizer_state(&self, paramid: &ParamId) -> Option<&HashMap<String, Array<A, D>>> {
314        self.optimizer_states.get(paramid)
315    }
316
317    /// Get mutable optimizer state for parameter
318    pub fn get_optimizer_state_mut(
319        &mut self,
320        paramid: &ParamId,
321    ) -> Option<&mut HashMap<String, Array<A, D>>> {
322        self.optimizer_states.get_mut(paramid)
323    }
324
325    /// Initialize optimizer state for parameter
326    pub fn init_optimizer_state(
327        &mut self,
328        paramid: &ParamId,
329        state_name: &str,
330        state: Array<A, D>,
331    ) -> Result<()> {
332        if let Some(states) = self.optimizer_states.get_mut(paramid) {
333            states.insert(state_name.to_string(), state);
334            Ok(())
335        } else {
336            Err(OptimError::InvalidConfig(format!(
337                "Parameter {} not registered",
338                paramid
339            )))
340        }
341    }
342
343    /// Reset all optimizer states
344    pub fn reset_optimizer_states(&mut self) {
345        for states in self.optimizer_states.values_mut() {
346            states.clear();
347        }
348    }
349
350    /// Get parameters by layer
351    pub fn get_parameters_by_layer(&self, layerid: &LayerId) -> Vec<&ParamId> {
352        self.parameters
353            .iter()
354            .filter(|(_, metadata)| &metadata.layername == layerid)
355            .map(|(paramid, _)| paramid)
356            .collect()
357    }
358
359    /// Get parameters by type
360    pub fn get_parameters_by_type(&self, paramtype: ParameterType) -> Vec<&ParamId> {
361        self.parameters
362            .iter()
363            .filter(|(_, metadata)| metadata.paramtype == paramtype)
364            .map(|(paramid, _)| paramid)
365            .collect()
366    }
367
368    /// Get trainable parameters
369    pub fn get_trainable_parameters(&self) -> Vec<&ParamId> {
370        self.parameters
371            .iter()
372            .filter(|(paramid, metadata)| {
373                metadata.requires_grad && !self.is_parameter_frozen(paramid)
374            })
375            .map(|(paramid, _)| paramid)
376            .collect()
377    }
378}
379
380impl<A: Float + Send + Sync> Default for OptimizationConfig<A> {
381    fn default() -> Self {
382        Self {
383            base_learning_rate: scalar_or(0.001, A::zero()),
384            weight_decay: A::zero(),
385            gradient_clip: None,
386            mixed_precision: false,
387            architecture_optimizations: HashMap::new(),
388        }
389    }
390}
391
392impl<A: Float + Send + Sync> Default for LayerOptimizationRule<A> {
393    fn default() -> Self {
394        Self {
395            lr_multiplier: A::one(),
396            weight_decay_multiplier: A::one(),
397            frozen: false,
398            custom_settings: HashMap::new(),
399        }
400    }
401}
402
403/// Forward/backward pass integration
404pub mod forward_backward {
405    use super::*;
406
407    /// Forward pass hook for parameter tracking
408    pub trait ForwardHook<A: Float, D: Dimension> {
409        /// Called before layer forward pass
410        fn pre_forward(&mut self, layerid: &LayerId, inputs: &[Array<A, D>]) -> Result<()>;
411
412        /// Called after layer forward pass
413        fn post_forward(&mut self, layerid: &LayerId, outputs: &[Array<A, D>]) -> Result<()>;
414    }
415
416    /// Backward pass hook for gradient processing
417    pub trait BackwardHook<A: Float, D: Dimension> {
418        /// Called before layer backward pass
419        fn pre_backward(&mut self, layerid: &LayerId, gradoutputs: &[Array<A, D>]) -> Result<()>;
420
421        /// Called after layer backward pass
422        fn post_backward(&mut self, layerid: &LayerId, gradinputs: &[Array<A, D>]) -> Result<()>;
423    }
424
425    /// Neural network integration manager
426    pub struct NeuralIntegration<A: Float, D: Dimension> {
427        /// Parameter manager
428        param_manager: ParameterManager<A, D>,
429        /// Forward hooks
430        forward_hooks: HashMap<LayerId, Box<dyn ForwardHook<A, D>>>,
431        /// Backward hooks
432        backward_hooks: HashMap<LayerId, Box<dyn BackwardHook<A, D>>>,
433        /// Gradient accumulation mode
434        gradient_accumulation: bool,
435        /// Accumulated gradients
436        accumulated_gradients: HashMap<ParamId, Array<A, D>>,
437        /// Accumulation count
438        accumulation_count: usize,
439    }
440
441    impl<
442            A: Float
443                + ScalarOperand
444                + Debug
445                + 'static
446                + scirs2_core::numeric::FromPrimitive
447                + std::iter::Sum
448                + Send
449                + Sync,
450            D: Dimension + 'static,
451        > NeuralIntegration<A, D>
452    {
453        /// Create a new neural integration manager
454        pub fn new(config: OptimizationConfig<A>) -> Self {
455            Self {
456                param_manager: ParameterManager::new(config),
457                forward_hooks: HashMap::new(),
458                backward_hooks: HashMap::new(),
459                gradient_accumulation: false,
460                accumulated_gradients: HashMap::new(),
461                accumulation_count: 0,
462            }
463        }
464
465        /// Register a forward hook for a layer
466        pub fn register_forward_hook<H>(&mut self, layerid: LayerId, hook: H)
467        where
468            H: ForwardHook<A, D> + 'static,
469        {
470            self.forward_hooks.insert(layerid, Box::new(hook));
471        }
472
473        /// Register a backward hook for a layer
474        pub fn register_backward_hook<H>(&mut self, layerid: LayerId, hook: H)
475        where
476            H: BackwardHook<A, D> + 'static,
477        {
478            self.backward_hooks.insert(layerid, Box::new(hook));
479        }
480
481        /// Enable gradient accumulation
482        pub fn enable_gradient_accumulation(&mut self) {
483            self.gradient_accumulation = true;
484        }
485
486        /// Disable gradient accumulation and return accumulated gradients
487        pub fn disable_gradient_accumulation(&mut self) -> HashMap<ParamId, Array<A, D>> {
488            self.gradient_accumulation = false;
489            let result = std::mem::take(&mut self.accumulated_gradients);
490            self.accumulation_count = 0;
491            result
492        }
493
494        /// Execute forward pass with hooks
495        pub fn forward_pass(
496            &mut self,
497            layerid: &LayerId,
498            inputs: &[Array<A, D>],
499        ) -> Result<Vec<Array<A, D>>> {
500            // Execute pre-forward hook
501            if let Some(hook) = self.forward_hooks.get_mut(layerid) {
502                hook.pre_forward(layerid, inputs)?;
503            }
504
505            // Get layer architecture and parameters
506            let layer_arch = self
507                .param_manager
508                .get_layer_architecture(layerid)
509                .ok_or_else(|| {
510                    OptimError::InvalidConfig(format!("Layer {} not registered", layerid))
511                })?
512                .clone();
513
514            // Compute outputs based on layer type
515            let outputs = match layer_arch.layer_type.as_str() {
516                "linear" | "dense" | "fc" => {
517                    // Linear layer: output = input @ weight^T + bias
518                    self.compute_linear_forward(layerid, inputs)?
519                }
520                "conv" | "conv2d" => {
521                    // Convolutional layer: simplified computation
522                    self.compute_conv_forward(layerid, inputs)?
523                }
524                "activation" => {
525                    // Activation layer: apply activation function
526                    self.compute_activation_forward(layerid, inputs, &layer_arch)?
527                }
528                "normalization" | "batchnorm" | "layernorm" => {
529                    // Normalization layer
530                    self.compute_normalization_forward(layerid, inputs)?
531                }
532                "dropout" => {
533                    // Dropout layer: apply dropout mask
534                    self.compute_dropout_forward(layerid, inputs, &layer_arch)?
535                }
536                "pooling" | "maxpool" | "avgpool" => {
537                    // Pooling layer
538                    self.compute_pooling_forward(layerid, inputs, &layer_arch)?
539                }
540                _ => {
541                    // Default: pass through for unknown layer types
542                    inputs.to_vec()
543                }
544            };
545
546            // Execute post-forward hook
547            if let Some(hook) = self.forward_hooks.get_mut(layerid) {
548                hook.post_forward(layerid, &outputs)?;
549            }
550
551            Ok(outputs)
552        }
553
554        /// Compute linear layer forward pass.
555        ///
556        /// Not implemented (F80): a real linear forward pass requires the
557        /// layer's weight matrix and bias, but `ParameterManager` stores
558        /// only optimizer metadata/state, not parameter *values*. The
559        /// previous body multiplied the input by the layer's learning rate
560        /// and returned it as if it were `input @ Wáµ€ + b`, i.e. silently
561        /// meaningless numbers. Returning an honest error is preferable to
562        /// fabricating an output.
563        fn compute_linear_forward(
564            &self,
565            _layer_id: &LayerId,
566            _inputs: &[Array<A, D>],
567        ) -> Result<Vec<Array<A, D>>> {
568            Err(OptimError::UnsupportedOperation(
569                "linear layer forward pass is not implemented: this module tracks \
570                 optimizer state, not weight values, so it cannot compute input @ Wáµ€ + b"
571                    .to_string(),
572            ))
573        }
574
575        /// Compute convolutional layer forward pass.
576        ///
577        /// Not implemented (F80): the previous body simply passed the input
578        /// through unchanged while claiming to convolve. Convolution needs
579        /// stored kernels, which this module does not hold.
580        fn compute_conv_forward(
581            &self,
582            _layer_id: &LayerId,
583            _inputs: &[Array<A, D>],
584        ) -> Result<Vec<Array<A, D>>> {
585            Err(OptimError::UnsupportedOperation(
586                "convolution forward pass is not implemented: no convolution kernels \
587                 are stored in this module"
588                    .to_string(),
589            ))
590        }
591
592        /// Compute activation forward pass
593        fn compute_activation_forward(
594            &self,
595            _layer_id: &LayerId,
596            inputs: &[Array<A, D>],
597            layer_arch: &LayerArchitecture,
598        ) -> Result<Vec<Array<A, D>>> {
599            let activation_type = layer_arch
600                .config
601                .get("activation")
602                .and_then(|v| match v {
603                    LayerConfig::String(s) => Some(s.as_str()),
604                    _ => None,
605                })
606                .unwrap_or("relu");
607
608            let outputs: Vec<Array<A, D>> = inputs
609                .iter()
610                .map(|input| {
611                    match activation_type {
612                        "relu" => input.mapv(|x| if x > A::zero() { x } else { A::zero() }),
613                        "sigmoid" => input.mapv(|x| A::one() / (A::one() + (-x).exp())),
614                        "tanh" => input.mapv(|x| x.tanh()),
615                        "leaky_relu" => {
616                            let alpha = scalar_or(0.01, A::zero());
617                            input.mapv(|x| if x > A::zero() { x } else { alpha * x })
618                        }
619                        _ => input.clone(), // Unknown activation, pass through
620                    }
621                })
622                .collect();
623
624            Ok(outputs)
625        }
626
627        /// Compute normalization forward pass
628        fn compute_normalization_forward(
629            &self,
630            _layer_id: &LayerId,
631            inputs: &[Array<A, D>],
632        ) -> Result<Vec<Array<A, D>>> {
633            // Simplified normalization: normalize to zero mean and unit variance
634            let outputs: Vec<Array<A, D>> = inputs
635                .iter()
636                .map(|input| {
637                    let mean = input.iter().copied().sum::<A>()
638                        / A::from(input.len()).unwrap_or(A::zero());
639                    let variance = input
640                        .mapv(|x| (x - mean).powi(2))
641                        .mean()
642                        .unwrap_or(A::one());
643                    let std_dev = variance.sqrt();
644                    let epsilon = scalar_or(1e-5, A::one());
645
646                    input.mapv(|x| (x - mean) / (std_dev + epsilon))
647                })
648                .collect();
649
650            Ok(outputs)
651        }
652
653        /// Compute dropout forward pass
654        fn compute_dropout_forward(
655            &self,
656            _layer_id: &LayerId,
657            inputs: &[Array<A, D>],
658            layer_arch: &LayerArchitecture,
659        ) -> Result<Vec<Array<A, D>>> {
660            let dropout_rate = layer_arch
661                .config
662                .get("dropout_rate")
663                .and_then(|v| match v {
664                    LayerConfig::Float(f) => scalar_opt(*f),
665                    _ => None,
666                })
667                .unwrap_or(try_scalar::<A, _>(0.5)?);
668
669            // During training, we would apply dropout mask
670            // For now, scale by (1 - dropout_rate) to maintain expected value
671            let scale = A::one() - dropout_rate;
672            let outputs: Vec<Array<A, D>> = inputs
673                .iter()
674                .map(|input| input.mapv(|x| x * scale))
675                .collect();
676
677            Ok(outputs)
678        }
679
680        /// Compute pooling forward pass.
681        ///
682        /// Not implemented (F80): the previous body passed the input through
683        /// unchanged while claiming to pool. Real pooling downsamples using a
684        /// window/stride that is not modeled here.
685        fn compute_pooling_forward(
686            &self,
687            _layer_id: &LayerId,
688            _inputs: &[Array<A, D>],
689            _layer_arch: &LayerArchitecture,
690        ) -> Result<Vec<Array<A, D>>> {
691            Err(OptimError::UnsupportedOperation(
692                "pooling forward pass is not implemented: downsampling window/stride \
693                 are not modeled in this module"
694                    .to_string(),
695            ))
696        }
697
698        /// Execute backward pass with hooks
699        pub fn backward_pass(
700            &mut self,
701            layerid: &LayerId,
702            grad_outputs: &[Array<A, D>],
703        ) -> Result<Vec<Array<A, D>>> {
704            // Execute pre-backward hook
705            if let Some(hook) = self.backward_hooks.get_mut(layerid) {
706                hook.pre_backward(layerid, grad_outputs)?;
707            }
708
709            // Get layer architecture
710            let layer_arch = self
711                .param_manager
712                .get_layer_architecture(layerid)
713                .ok_or_else(|| {
714                    OptimError::InvalidConfig(format!("Layer {} not registered", layerid))
715                })?
716                .clone();
717
718            // Compute gradients based on layer type
719            let grad_inputs = match layer_arch.layer_type.as_str() {
720                "linear" | "dense" | "fc" => {
721                    // Linear layer gradient computation
722                    self.compute_linear_backward(layerid, grad_outputs)?
723                }
724                "conv" | "conv2d" => {
725                    // Convolutional layer gradient computation
726                    self.compute_conv_backward(layerid, grad_outputs)?
727                }
728                "activation" => {
729                    // Activation gradient computation
730                    self.compute_activation_backward(layerid, grad_outputs, &layer_arch)?
731                }
732                "normalization" | "batchnorm" | "layernorm" => {
733                    // Normalization gradient computation
734                    self.compute_normalization_backward(layerid, grad_outputs)?
735                }
736                "dropout" => {
737                    // Dropout gradient computation
738                    self.compute_dropout_backward(layerid, grad_outputs, &layer_arch)?
739                }
740                "pooling" | "maxpool" | "avgpool" => {
741                    // Pooling gradient computation
742                    self.compute_pooling_backward(layerid, grad_outputs, &layer_arch)?
743                }
744                _ => {
745                    // Default: pass through gradients for unknown layer types
746                    grad_outputs.to_vec()
747                }
748            };
749
750            // Apply gradient clipping if configured
751            let clipped_grads =
752                if let Some(clipvalue) = self.param_manager.global_config.gradient_clip {
753                    self.apply_gradient_clipping(grad_inputs, clipvalue)?
754                } else {
755                    grad_inputs
756                };
757
758            // Execute post-backward hook
759            if let Some(hook) = self.backward_hooks.get_mut(layerid) {
760                hook.post_backward(layerid, &clipped_grads)?;
761            }
762
763            Ok(clipped_grads)
764        }
765
766        /// Compute linear layer backward pass.
767        ///
768        /// Not implemented (F80): the true input gradient is `grad_output @
769        /// W`, which needs the weight matrix this module does not store. The
770        /// previous body scaled the gradient by a constant `0.9` and
771        /// returned it as if it were the real backprop. To accumulate
772        /// gradients for a parameter update, call
773        /// [`Self::accumulate_gradients`] directly.
774        fn compute_linear_backward(
775            &mut self,
776            _layer_id: &LayerId,
777            _grad_outputs: &[Array<A, D>],
778        ) -> Result<Vec<Array<A, D>>> {
779            Err(OptimError::UnsupportedOperation(
780                "linear layer backward pass is not implemented: computing grad_output @ W \
781                 requires stored weight values this module does not hold"
782                    .to_string(),
783            ))
784        }
785
786        /// Compute convolutional layer backward pass.
787        ///
788        /// Not implemented (F80): the previous body passed gradients through
789        /// unchanged. Real backprop needs the stored kernels.
790        fn compute_conv_backward(
791            &self,
792            _layer_id: &LayerId,
793            _grad_outputs: &[Array<A, D>],
794        ) -> Result<Vec<Array<A, D>>> {
795            Err(OptimError::UnsupportedOperation(
796                "convolution backward pass is not implemented: no convolution kernels \
797                 are stored in this module"
798                    .to_string(),
799            ))
800        }
801
802        /// Compute activation backward pass
803        fn compute_activation_backward(
804            &self,
805            _layer_id: &LayerId,
806            grad_outputs: &[Array<A, D>],
807            layer_arch: &LayerArchitecture,
808        ) -> Result<Vec<Array<A, D>>> {
809            let activation_type = layer_arch
810                .config
811                .get("activation")
812                .and_then(|v| match v {
813                    LayerConfig::String(s) => Some(s.as_str()),
814                    _ => None,
815                })
816                .unwrap_or("relu");
817
818            // Note: This is simplified - real implementation would need the forward pass inputs
819            let grad_inputs: Vec<Array<A, D>> = grad_outputs
820                .iter()
821                .map(|grad| {
822                    match activation_type {
823                        "relu" => {
824                            // ReLU gradient: 1 if x > 0, 0 otherwise
825                            // Since we don't have the original input, we approximate
826                            grad.mapv(|g| if g > A::zero() { g } else { A::zero() })
827                        }
828                        "sigmoid" => {
829                            // Sigmoid gradient: sigmoid(x) * (1 - sigmoid(x))
830                            // Approximation without original input
831                            let factor = scalar_or(0.25, A::one()); // Max gradient of sigmoid
832                            grad.mapv(|g| g * factor)
833                        }
834                        "tanh" => {
835                            // Tanh gradient: 1 - tanh(x)^2
836                            // Approximation without original input
837                            let factor = scalar_or(0.5, A::one());
838                            grad.mapv(|g| g * factor)
839                        }
840                        "leaky_relu" => {
841                            let alpha = scalar_or(0.01, A::zero());
842                            grad.mapv(|g| if g > A::zero() { g } else { alpha * g })
843                        }
844                        _ => grad.clone(), // Unknown activation, pass through
845                    }
846                })
847                .collect();
848
849            Ok(grad_inputs)
850        }
851
852        /// Compute normalization backward pass
853        fn compute_normalization_backward(
854            &self,
855            _layer_id: &LayerId,
856            grad_outputs: &[Array<A, D>],
857        ) -> Result<Vec<Array<A, D>>> {
858            // Simplified normalization backward
859            // Real implementation would compute gradients considering mean and variance
860            let scale_factor = try_scalar::<A, _>(0.9)?;
861            let grad_inputs: Vec<Array<A, D>> = grad_outputs
862                .iter()
863                .map(|grad| grad.mapv(|g| g * scale_factor))
864                .collect();
865
866            Ok(grad_inputs)
867        }
868
869        /// Compute dropout backward pass
870        fn compute_dropout_backward(
871            &self,
872            _layer_id: &LayerId,
873            grad_outputs: &[Array<A, D>],
874            layer_arch: &LayerArchitecture,
875        ) -> Result<Vec<Array<A, D>>> {
876            let dropout_rate = layer_arch
877                .config
878                .get("dropout_rate")
879                .and_then(|v| match v {
880                    LayerConfig::Float(f) => scalar_opt(*f),
881                    _ => None,
882                })
883                .unwrap_or(try_scalar::<A, _>(0.5)?);
884
885            // Scale gradients by (1 - dropout_rate) to match forward pass
886            let scale = A::one() - dropout_rate;
887            let grad_inputs: Vec<Array<A, D>> = grad_outputs
888                .iter()
889                .map(|grad| grad.mapv(|g| g * scale))
890                .collect();
891
892            Ok(grad_inputs)
893        }
894
895        /// Compute pooling backward pass.
896        ///
897        /// Not implemented (F80): the previous body passed gradients through
898        /// unchanged. Real pooling backprop routes gradients through the
899        /// stored pooling argmax/averaging indices, which are not modeled.
900        fn compute_pooling_backward(
901            &self,
902            _layer_id: &LayerId,
903            _grad_outputs: &[Array<A, D>],
904            _layer_arch: &LayerArchitecture,
905        ) -> Result<Vec<Array<A, D>>> {
906            Err(OptimError::UnsupportedOperation(
907                "pooling backward pass is not implemented: pooling indices are not \
908                 modeled in this module"
909                    .to_string(),
910            ))
911        }
912
913        /// Apply gradient clipping
914        fn apply_gradient_clipping(
915            &self,
916            gradients: Vec<Array<A, D>>,
917            clipvalue: A,
918        ) -> Result<Vec<Array<A, D>>> {
919            let clipped: Vec<Array<A, D>> = gradients
920                .into_iter()
921                .map(|grad| {
922                    // Compute L2 norm of gradient
923                    let norm = grad.mapv(|x| x * x).sum().sqrt();
924
925                    if norm > clipvalue {
926                        // Scale gradient to have norm = clipvalue
927                        let scale = clipvalue / norm;
928                        grad.mapv(|x| x * scale)
929                    } else {
930                        grad
931                    }
932                })
933                .collect();
934
935            Ok(clipped)
936        }
937
938        /// Accumulate gradients for parameters
939        pub fn accumulate_gradients(
940            &mut self,
941            gradients: HashMap<ParamId, Array<A, D>>,
942        ) -> Result<()> {
943            if !self.gradient_accumulation {
944                return Err(OptimError::InvalidConfig(
945                    "Gradient accumulation not enabled".to_string(),
946                ));
947            }
948
949            self.accumulation_count += 1;
950
951            for (paramid, grad) in gradients {
952                if let Some(acc_grad) = self.accumulated_gradients.get_mut(&paramid) {
953                    // Add to existing accumulated gradient
954                    *acc_grad = acc_grad.clone() + grad;
955                } else {
956                    // First gradient for this parameter
957                    self.accumulated_gradients.insert(paramid, grad);
958                }
959            }
960
961            Ok(())
962        }
963
964        /// Get parameter manager
965        pub fn parameter_manager(&self) -> &ParameterManager<A, D> {
966            &self.param_manager
967        }
968
969        /// Get mutable parameter manager
970        pub fn parameter_manager_mut(&mut self) -> &mut ParameterManager<A, D> {
971            &mut self.param_manager
972        }
973
974        /// Get accumulation count
975        pub fn accumulation_count(&self) -> usize {
976            self.accumulation_count
977        }
978    }
979}
980
981/// Architecture-aware optimization utilities
982pub mod architecture_aware {
983    use super::*;
984
985    /// Architecture-specific optimization strategy
986    #[derive(Debug, Clone)]
987    pub enum ArchitectureStrategy {
988        /// Transformer-specific optimizations
989        Transformer {
990            /// Use different learning rates for different components
991            component_specific_lr: bool,
992            /// Apply layer-wise learning rate decay
993            layer_wise_decay: bool,
994            /// Warmup steps for attention parameters
995            attention_warmup: usize,
996        },
997        /// CNN-specific optimizations
998        ConvolutionalNet {
999            /// Use different learning rates for conv vs fc layers
1000            layer_type_lr: bool,
1001            /// Apply depth-wise learning rate scaling
1002            depth_scaling: bool,
1003            /// Batch norm parameter handling
1004            bn_special_handling: bool,
1005        },
1006        /// RNN-specific optimizations
1007        RecurrentNet {
1008            /// Gradient clipping specifically for RNNs
1009            rnn_gradient_clip: Option<f64>,
1010            /// Different learning rates for recurrent vs linear weights
1011            weight_type_lr: bool,
1012        },
1013        /// Custom architecture strategy
1014        Custom {
1015            /// Custom optimization rules
1016            rules: HashMap<String, LayerConfig>,
1017        },
1018    }
1019
1020    /// Architecture-aware optimizer
1021    #[derive(Debug)]
1022    pub struct ArchitectureAwareOptimizer<A: Float, D: Dimension> {
1023        /// Parameter manager
1024        param_manager: ParameterManager<A, D>,
1025        /// Architecture strategy
1026        strategy: ArchitectureStrategy,
1027        /// Step count
1028        step_count: usize,
1029    }
1030
1031    impl<A: Float + ScalarOperand + Debug + Send + Sync, D: Dimension + Send + Sync>
1032        ArchitectureAwareOptimizer<A, D>
1033    {
1034        /// Create a new architecture-aware optimizer
1035        pub fn new(config: OptimizationConfig<A>, strategy: ArchitectureStrategy) -> Self {
1036            Self {
1037                param_manager: ParameterManager::new(config),
1038                strategy,
1039                step_count: 0,
1040            }
1041        }
1042
1043        /// Apply architecture-specific optimizations
1044        pub fn apply_architecture_optimizations(&mut self) -> Result<()> {
1045            // Clone the strategy to avoid borrowing conflicts
1046            let strategy = self.strategy.clone();
1047            match strategy {
1048                ArchitectureStrategy::Transformer {
1049                    component_specific_lr,
1050                    layer_wise_decay,
1051                    attention_warmup,
1052                } => {
1053                    self.apply_transformer_optimizations(
1054                        component_specific_lr,
1055                        layer_wise_decay,
1056                        attention_warmup,
1057                    )?;
1058                }
1059                ArchitectureStrategy::ConvolutionalNet {
1060                    layer_type_lr,
1061                    depth_scaling,
1062                    bn_special_handling,
1063                } => {
1064                    self.apply_cnn_optimizations(
1065                        layer_type_lr,
1066                        depth_scaling,
1067                        bn_special_handling,
1068                    )?;
1069                }
1070                ArchitectureStrategy::RecurrentNet {
1071                    rnn_gradient_clip,
1072                    weight_type_lr,
1073                } => {
1074                    self.apply_rnn_optimizations(rnn_gradient_clip, weight_type_lr)?;
1075                }
1076                ArchitectureStrategy::Custom { rules } => {
1077                    self.apply_custom_optimizations(&rules)?;
1078                }
1079            }
1080            Ok(())
1081        }
1082
1083        /// Apply Transformer-specific optimizations
1084        fn apply_transformer_optimizations(
1085            &mut self,
1086            component_specific_lr: bool,
1087            layer_wise_decay: bool,
1088            attention_warmup: usize,
1089        ) -> Result<()> {
1090            if component_specific_lr {
1091                // Different learning rates for attention, ffn, and normalization
1092                self.set_component_learning_rates()?;
1093            }
1094
1095            if layer_wise_decay {
1096                // Apply layer-wise learning rate _decay
1097                self.apply_layer_wise_decay()?;
1098            }
1099
1100            if attention_warmup > 0 && self.step_count < attention_warmup {
1101                // Apply _warmup to attention parameters
1102                self.apply_attention_warmup(attention_warmup)?;
1103            }
1104
1105            Ok(())
1106        }
1107
1108        /// Apply CNN-specific optimizations
1109        fn apply_cnn_optimizations(
1110            &mut self,
1111            layer_type_lr: bool,
1112            depth_scaling: bool,
1113            bn_special_handling: bool,
1114        ) -> Result<()> {
1115            if layer_type_lr {
1116                // Different learning rates for conv vs fully connected layers
1117                self.set_layer_type_learning_rates()?;
1118            }
1119
1120            if depth_scaling {
1121                // Scale learning rates based on network depth
1122                self.apply_depth_scaling()?;
1123            }
1124
1125            if bn_special_handling {
1126                // Special _handling for batch normalization parameters
1127                self.apply_bn_optimizations()?;
1128            }
1129
1130            Ok(())
1131        }
1132
1133        /// Apply RNN-specific optimizations
1134        fn apply_rnn_optimizations(
1135            &mut self,
1136            rnn_gradient_clip: Option<f64>,
1137            weight_type_lr: bool,
1138        ) -> Result<()> {
1139            if let Some(clipvalue) = rnn_gradient_clip {
1140                // Apply RNN-specific gradient clipping (F80). Convert the
1141                // clip threshold without panicking: an unrepresentable value
1142                // is an honest configuration error, not a crash.
1143                let clip = A::from(clipvalue).ok_or_else(|| {
1144                    OptimError::InvalidConfig(format!(
1145                        "RNN gradient clip value {clipvalue} is not representable \
1146                         in the optimizer's float type"
1147                    ))
1148                })?;
1149                self.apply_rnn_gradient_clipping(clip)?;
1150            }
1151
1152            if weight_type_lr {
1153                // Different learning rates for recurrent vs linear weights
1154                self.set_weight_type_learning_rates()?;
1155            }
1156
1157            Ok(())
1158        }
1159
1160        /// Apply custom optimizations
1161        fn apply_custom_optimizations(
1162            &mut self,
1163            rules: &HashMap<String, LayerConfig>,
1164        ) -> Result<()> {
1165            // Collect rules first to avoid borrowing conflicts
1166            let rule_entries: Vec<(String, LayerConfig)> = rules
1167                .iter()
1168                .map(|(name, config)| (name.clone(), config.clone()))
1169                .collect();
1170
1171            for (rule_name, config) in rule_entries {
1172                self.apply_custom_rule(&rule_name, &config)?;
1173            }
1174            Ok(())
1175        }
1176
1177        /// Set component-specific learning rates for Transformers
1178        fn set_component_learning_rates(&mut self) -> Result<()> {
1179            // Collect the data first to avoid borrowing conflicts
1180            let layer_rules: Vec<(LayerId, LayerOptimizationRule<A>)> = self
1181                .param_manager
1182                .get_all_parameters()
1183                .values()
1184                .map(|metadata| {
1185                    let mut rule = LayerOptimizationRule::default();
1186
1187                    // Determine learning rate multiplier based on parameter tags
1188                    if metadata.tags.contains(&"attention".to_string()) {
1189                        rule.lr_multiplier = scalar_or(1.2, A::one());
1190                    // Higher LR for attention
1191                    } else if metadata.tags.contains(&"ffn".to_string()) {
1192                        rule.lr_multiplier = scalar_or(1.0, A::one());
1193                    // Standard LR for FFN
1194                    } else if metadata.tags.contains(&"normalization".to_string()) {
1195                        rule.lr_multiplier = scalar_or(0.8, A::one());
1196                        // Lower LR for normalization
1197                    }
1198
1199                    (metadata.layername.clone(), rule)
1200                })
1201                .collect();
1202
1203            // Now apply the rules
1204            for (layername, rule) in layer_rules {
1205                self.param_manager.set_layer_rule(layername, rule);
1206            }
1207            Ok(())
1208        }
1209
1210        /// Apply layer-wise learning rate decay
1211        fn apply_layer_wise_decay(&mut self) -> Result<()> {
1212            // Extract layer numbers from layer names and apply decay
1213            for (layerid, _) in self.param_manager.layer_architectures.clone() {
1214                if let Some(layer_num) = self.extract_layer_number(&layerid) {
1215                    let decay_factor = try_scalar::<A, _>(0.95_f64.powi(layer_num as i32))?;
1216                    let mut rule = self
1217                        .param_manager
1218                        .layer_rules
1219                        .get(&layerid)
1220                        .cloned()
1221                        .unwrap_or_default();
1222                    rule.lr_multiplier = rule.lr_multiplier * decay_factor;
1223                    self.param_manager.set_layer_rule(layerid, rule);
1224                }
1225            }
1226            Ok(())
1227        }
1228
1229        /// Apply attention parameter warmup
1230        fn apply_attention_warmup(&mut self, warmupsteps: usize) -> Result<()> {
1231            let warmup_factor = try_scalar::<A, _>(self.step_count as f64 / warmupsteps as f64)?;
1232
1233            // Collect attention layers first
1234            let attention_layers: Vec<LayerId> = self
1235                .param_manager
1236                .get_all_parameters()
1237                .values()
1238                .filter_map(|metadata| {
1239                    if metadata.tags.contains(&"attention".to_string()) {
1240                        Some(metadata.layername.clone())
1241                    } else {
1242                        None
1243                    }
1244                })
1245                .collect();
1246
1247            // Apply warmup to attention layers
1248            for layername in attention_layers {
1249                let mut rule = self
1250                    .param_manager
1251                    .layer_rules
1252                    .get(&layername)
1253                    .cloned()
1254                    .unwrap_or_default();
1255                rule.lr_multiplier = rule.lr_multiplier * warmup_factor;
1256                self.param_manager.set_layer_rule(layername, rule);
1257            }
1258            Ok(())
1259        }
1260
1261        /// Set learning rates based on layer type (conv vs fc)
1262        fn set_layer_type_learning_rates(&mut self) -> Result<()> {
1263            for (layerid, architecture) in self.param_manager.layer_architectures.clone() {
1264                let mut rule = LayerOptimizationRule::default();
1265
1266                match architecture.layer_type.as_str() {
1267                    "conv" | "conv2d" | "conv3d" => {
1268                        rule.lr_multiplier = scalar_or(1.0, A::one());
1269                        // Standard LR for conv
1270                    }
1271                    "linear" | "dense" | "fc" => {
1272                        rule.lr_multiplier = scalar_or(0.8, A::one());
1273                        // Lower LR for FC
1274                    }
1275                    _ => {
1276                        rule.lr_multiplier = scalar_or(1.0, A::one());
1277                        // Default
1278                    }
1279                }
1280
1281                self.param_manager.set_layer_rule(layerid, rule);
1282            }
1283            Ok(())
1284        }
1285
1286        /// Apply depth-based scaling
1287        fn apply_depth_scaling(&mut self) -> Result<()> {
1288            // Count total layers
1289            let total_layers = self.param_manager.layer_architectures.len();
1290
1291            for (i, (layerid, _)) in self
1292                .param_manager
1293                .layer_architectures
1294                .clone()
1295                .iter()
1296                .enumerate()
1297            {
1298                let depth_factor =
1299                    try_scalar::<A, _>(1.0 - 0.1 * (i as f64 / total_layers as f64))?;
1300                let mut rule = self
1301                    .param_manager
1302                    .layer_rules
1303                    .get(layerid)
1304                    .cloned()
1305                    .unwrap_or_default();
1306                rule.lr_multiplier = rule.lr_multiplier * depth_factor;
1307                self.param_manager.set_layer_rule(layerid.clone(), rule);
1308            }
1309            Ok(())
1310        }
1311
1312        /// Apply batch normalization optimizations
1313        fn apply_bn_optimizations(&mut self) -> Result<()> {
1314            // Collect normalization layers first
1315            let norm_layers: Vec<LayerId> = self
1316                .param_manager
1317                .get_all_parameters()
1318                .values()
1319                .filter_map(|metadata| {
1320                    if metadata.paramtype == ParameterType::Normalization {
1321                        Some(metadata.layername.clone())
1322                    } else {
1323                        None
1324                    }
1325                })
1326                .collect();
1327
1328            // Apply optimization to normalization layers
1329            for layername in norm_layers {
1330                let mut rule = self
1331                    .param_manager
1332                    .layer_rules
1333                    .get(&layername)
1334                    .cloned()
1335                    .unwrap_or_default();
1336                // Higher learning rate and no weight decay for BN parameters
1337                rule.lr_multiplier = try_scalar::<A, _>(2.0)?;
1338                rule.weight_decay_multiplier = A::zero();
1339                self.param_manager.set_layer_rule(layername, rule);
1340            }
1341            Ok(())
1342        }
1343
1344        /// Apply RNN-specific gradient clipping
1345        fn apply_rnn_gradient_clipping(&mut self, clipvalue: A) -> Result<()> {
1346            // This would be implemented in coordination with the gradient processing system
1347            // For now, we'll store the clip _value in the global config
1348            self.param_manager.global_config.gradient_clip = Some(clipvalue);
1349            Ok(())
1350        }
1351
1352        /// Set learning rates based on weight type (recurrent vs linear)
1353        fn set_weight_type_learning_rates(&mut self) -> Result<()> {
1354            // Collect weight type layers first
1355            let layer_rules: Vec<(LayerId, LayerOptimizationRule<A>)> = self
1356                .param_manager
1357                .get_all_parameters()
1358                .values()
1359                .map(|metadata| {
1360                    let mut rule = LayerOptimizationRule::default();
1361
1362                    if metadata.tags.contains(&"recurrent".to_string()) {
1363                        rule.lr_multiplier = scalar_or(0.5, A::one());
1364                    // Lower LR for recurrent weights
1365                    } else if metadata.tags.contains(&"linear".to_string()) {
1366                        rule.lr_multiplier = scalar_or(1.0, A::one());
1367                        // Standard LR for linear weights
1368                    }
1369
1370                    (metadata.layername.clone(), rule)
1371                })
1372                .collect();
1373
1374            // Apply the rules
1375            for (layername, rule) in layer_rules {
1376                self.param_manager.set_layer_rule(layername, rule);
1377            }
1378            Ok(())
1379        }
1380
1381        /// Apply a single custom optimization rule from
1382        /// [`ArchitectureStrategy::Custom`].
1383        ///
1384        /// A rule name is either a bare setting, applied to every registered
1385        /// layer, or `"<layer>.<setting>"`, applied only to that layer. The
1386        /// recognised settings map onto [`LayerOptimizationRule`]:
1387        ///
1388        /// | setting                     | expected value            |
1389        /// |-----------------------------|---------------------------|
1390        /// | `lr_multiplier`             | [`LayerConfig::Float`]/[`LayerConfig::Int`] |
1391        /// | `weight_decay_multiplier`   | [`LayerConfig::Float`]/[`LayerConfig::Int`] |
1392        /// | `frozen`                    | [`LayerConfig::Bool`]     |
1393        ///
1394        /// An unrecognised setting, an unknown layer, or a value of the wrong
1395        /// variant is an honest `Err`: this used to be a no-op that ignored
1396        /// both arguments and returned `Ok(())`, so a misspelled or
1397        /// unsupported rule silently did nothing while reporting success.
1398        fn apply_custom_rule(&mut self, rule_name: &str, config: &LayerConfig) -> Result<()> {
1399            let (target_layer, setting) = match rule_name.rsplit_once('.') {
1400                Some((layer, setting)) => (Some(layer.to_string()), setting),
1401                None => (None, rule_name),
1402            };
1403
1404            // Resolve the layers this rule applies to before mutating, so the
1405            // borrow of `param_manager` ends first.
1406            let layers: Vec<LayerId> = {
1407                let all: Vec<LayerId> = self
1408                    .param_manager
1409                    .get_all_parameters()
1410                    .values()
1411                    .map(|metadata| metadata.layername.clone())
1412                    .collect();
1413                match &target_layer {
1414                    Some(name) => {
1415                        if !all.iter().any(|layer| layer == name) {
1416                            return Err(OptimError::InvalidConfig(format!(
1417                                "custom rule `{rule_name}` targets layer `{name}`, \
1418                                 which has no registered parameters"
1419                            )));
1420                        }
1421                        vec![name.clone()]
1422                    }
1423                    None => {
1424                        let mut unique = all;
1425                        unique.sort();
1426                        unique.dedup();
1427                        unique
1428                    }
1429                }
1430            };
1431
1432            let as_float = |config: &LayerConfig| -> Result<A> {
1433                let raw = match config {
1434                    LayerConfig::Float(value) => *value,
1435                    LayerConfig::Int(value) => *value as f64,
1436                    other => {
1437                        return Err(OptimError::InvalidConfig(format!(
1438                            "custom rule `{rule_name}` expects a numeric value, got {other:?}"
1439                        )))
1440                    }
1441                };
1442                A::from(raw).ok_or_else(|| {
1443                    OptimError::InvalidConfig(format!(
1444                        "custom rule `{rule_name}` value {raw} is not representable \
1445                         in the optimizer's float type"
1446                    ))
1447                })
1448            };
1449
1450            for layer in layers {
1451                let mut rule = self
1452                    .param_manager
1453                    .layer_rules
1454                    .get(&layer)
1455                    .cloned()
1456                    .unwrap_or_default();
1457                match setting {
1458                    "lr_multiplier" => rule.lr_multiplier = as_float(config)?,
1459                    "weight_decay_multiplier" => rule.weight_decay_multiplier = as_float(config)?,
1460                    "frozen" => match config {
1461                        LayerConfig::Bool(value) => rule.frozen = *value,
1462                        other => {
1463                            return Err(OptimError::InvalidConfig(format!(
1464                                "custom rule `{rule_name}` expects a boolean value, got {other:?}"
1465                            )))
1466                        }
1467                    },
1468                    unknown => {
1469                        return Err(OptimError::InvalidConfig(format!(
1470                            "unknown custom optimization rule `{unknown}`; supported \
1471                             settings are `lr_multiplier`, `weight_decay_multiplier` \
1472                             and `frozen`, optionally prefixed with `<layer>.`"
1473                        )))
1474                    }
1475                }
1476                rule.custom_settings
1477                    .insert(rule_name.to_string(), config.clone());
1478                self.param_manager.set_layer_rule(layer, rule);
1479            }
1480
1481            Ok(())
1482        }
1483
1484        /// Extract layer number from layer name (e.g., "layer_12" -> 12)
1485        fn extract_layer_number(&self, layername: &str) -> Option<usize> {
1486            layername.split('_').next_back()?.parse().ok()
1487        }
1488
1489        /// Step the optimizer
1490        pub fn step(&mut self) -> Result<()> {
1491            self.step_count += 1;
1492            self.apply_architecture_optimizations()
1493        }
1494
1495        /// Get parameter manager
1496        pub fn parameter_manager(&self) -> &ParameterManager<A, D> {
1497            &self.param_manager
1498        }
1499
1500        /// Get mutable parameter manager
1501        pub fn parameter_manager_mut(&mut self) -> &mut ParameterManager<A, D> {
1502            &mut self.param_manager
1503        }
1504    }
1505}
1506
1507#[cfg(test)]
1508mod tests {
1509    use super::*;
1510    use approx::assert_relative_eq;
1511
1512    #[test]
1513    fn test_parameter_manager_basic() {
1514        let config = OptimizationConfig::default();
1515        let mut manager = ParameterManager::<f64, scirs2_core::ndarray::Ix1>::new(config);
1516
1517        let metadata = ParameterMetadata {
1518            layername: "layer1".to_string(),
1519            param_name: "weight".to_string(),
1520            shape: vec![10, 5],
1521            requires_grad: true,
1522            paramtype: ParameterType::Weight,
1523            sharing_group: None,
1524            tags: vec!["dense".to_string()],
1525        };
1526
1527        manager
1528            .register_parameter("param1".to_string(), metadata)
1529            .expect("unwrap failed");
1530
1531        assert!(manager
1532            .get_parameter_metadata(&"param1".to_string())
1533            .is_some());
1534        assert_eq!(manager.get_all_parameters().len(), 1);
1535        assert!(!manager.is_parameter_frozen(&"param1".to_string()));
1536    }
1537
1538    #[test]
1539    fn test_lazy_registration() {
1540        let config = OptimizationConfig::default();
1541        let mut manager = ParameterManager::<f64, scirs2_core::ndarray::Ix1>::new(config);
1542
1543        manager.enable_lazy_mode();
1544
1545        let metadata = ParameterMetadata {
1546            layername: "layer1".to_string(),
1547            param_name: "weight".to_string(),
1548            shape: vec![10, 5],
1549            requires_grad: true,
1550            paramtype: ParameterType::Weight,
1551            sharing_group: None,
1552            tags: vec![],
1553        };
1554
1555        manager
1556            .register_parameter("param1".to_string(), metadata)
1557            .expect("unwrap failed");
1558
1559        // Parameter should not be registered yet
1560        assert_eq!(manager.get_all_parameters().len(), 0);
1561
1562        // Disable lazy mode to process pending registrations
1563        manager.disable_lazy_mode().expect("unwrap failed");
1564
1565        // Now parameter should be registered
1566        assert_eq!(manager.get_all_parameters().len(), 1);
1567    }
1568
1569    #[test]
1570    fn test_layer_specific_rules() {
1571        let config = OptimizationConfig {
1572            base_learning_rate: 0.01,
1573            weight_decay: 0.001,
1574            gradient_clip: None,
1575            mixed_precision: false,
1576            architecture_optimizations: HashMap::new(),
1577        };
1578        let mut manager = ParameterManager::<f64, scirs2_core::ndarray::Ix1>::new(config);
1579
1580        let rule = LayerOptimizationRule {
1581            lr_multiplier: 2.0,
1582            weight_decay_multiplier: 0.5,
1583            frozen: false,
1584            custom_settings: HashMap::new(),
1585        };
1586
1587        manager.set_layer_rule("layer1".to_string(), rule);
1588
1589        let metadata = ParameterMetadata {
1590            layername: "layer1".to_string(),
1591            param_name: "weight".to_string(),
1592            shape: vec![10, 5],
1593            requires_grad: true,
1594            paramtype: ParameterType::Weight,
1595            sharing_group: None,
1596            tags: vec![],
1597        };
1598
1599        manager
1600            .register_parameter("param1".to_string(), metadata)
1601            .expect("unwrap failed");
1602
1603        // Test effective learning rate
1604        let effective_lr = manager.get_effective_learning_rate(&"param1".to_string());
1605        assert_relative_eq!(effective_lr, 0.02, epsilon = 1e-6); // 0.01 * 2.0
1606
1607        // Test effective weight decay
1608        let effective_decay = manager.get_effective_weight_decay(&"param1".to_string());
1609        assert_relative_eq!(effective_decay, 0.0005, epsilon = 1e-6); // 0.001 * 0.5
1610    }
1611
1612    #[test]
1613    fn test_parameter_sharing() {
1614        let config = OptimizationConfig::default();
1615        let mut manager = ParameterManager::<f64, scirs2_core::ndarray::Ix1>::new(config);
1616
1617        let metadata1 = ParameterMetadata {
1618            layername: "layer1".to_string(),
1619            param_name: "weight".to_string(),
1620            shape: vec![10, 5],
1621            requires_grad: true,
1622            paramtype: ParameterType::Weight,
1623            sharing_group: Some("shared_weights".to_string()),
1624            tags: vec![],
1625        };
1626
1627        let metadata2 = ParameterMetadata {
1628            layername: "layer2".to_string(),
1629            param_name: "weight".to_string(),
1630            shape: vec![10, 5],
1631            requires_grad: true,
1632            paramtype: ParameterType::Weight,
1633            sharing_group: Some("shared_weights".to_string()),
1634            tags: vec![],
1635        };
1636
1637        manager
1638            .register_parameter("param1".to_string(), metadata1)
1639            .expect("unwrap failed");
1640        manager
1641            .register_parameter("param2".to_string(), metadata2)
1642            .expect("unwrap failed");
1643
1644        let sharing_group = manager
1645            .get_sharing_group("shared_weights")
1646            .expect("unwrap failed");
1647        assert_eq!(sharing_group.len(), 2);
1648        assert!(sharing_group.contains(&"param1".to_string()));
1649        assert!(sharing_group.contains(&"param2".to_string()));
1650    }
1651
1652    #[test]
1653    fn test_parameter_filtering() {
1654        let config = OptimizationConfig::default();
1655        let mut manager = ParameterManager::<f64, scirs2_core::ndarray::Ix1>::new(config);
1656
1657        let weight_metadata = ParameterMetadata {
1658            layername: "layer1".to_string(),
1659            param_name: "weight".to_string(),
1660            shape: vec![10, 5],
1661            requires_grad: true,
1662            paramtype: ParameterType::Weight,
1663            sharing_group: None,
1664            tags: vec![],
1665        };
1666
1667        let bias_metadata = ParameterMetadata {
1668            layername: "layer1".to_string(),
1669            param_name: "bias".to_string(),
1670            shape: vec![5],
1671            requires_grad: true,
1672            paramtype: ParameterType::Bias,
1673            sharing_group: None,
1674            tags: vec![],
1675        };
1676
1677        manager
1678            .register_parameter("weight".to_string(), weight_metadata)
1679            .expect("unwrap failed");
1680        manager
1681            .register_parameter("bias".to_string(), bias_metadata)
1682            .expect("unwrap failed");
1683
1684        // Test filtering by type
1685        let weights = manager.get_parameters_by_type(ParameterType::Weight);
1686        assert_eq!(weights.len(), 1);
1687        assert_eq!(weights[0], &"weight".to_string());
1688
1689        let biases = manager.get_parameters_by_type(ParameterType::Bias);
1690        assert_eq!(biases.len(), 1);
1691        assert_eq!(biases[0], &"bias".to_string());
1692
1693        // Test filtering by layer
1694        let layer_params = manager.get_parameters_by_layer(&"layer1".to_string());
1695        assert_eq!(layer_params.len(), 2);
1696
1697        // Test trainable parameters
1698        let trainable = manager.get_trainable_parameters();
1699        assert_eq!(trainable.len(), 2);
1700    }
1701
1702    #[test]
1703    fn test_architecture_aware_transformer() {
1704        use crate::neural_integration::architecture_aware::*;
1705
1706        let config = OptimizationConfig::default();
1707        let strategy = ArchitectureStrategy::Transformer {
1708            component_specific_lr: true,
1709            layer_wise_decay: true,
1710            attention_warmup: 1000,
1711        };
1712
1713        let mut optimizer =
1714            ArchitectureAwareOptimizer::<f64, scirs2_core::ndarray::Ix1>::new(config, strategy);
1715
1716        // Register a layer architecture
1717        let layer_arch = LayerArchitecture {
1718            layer_type: "transformer_block".to_string(),
1719            input_dims: vec![512],
1720            output_dims: vec![512],
1721            config: HashMap::new(),
1722            trainable: true,
1723        };
1724
1725        optimizer
1726            .parameter_manager_mut()
1727            .register_layer("layer_0".to_string(), layer_arch);
1728
1729        // Register parameters with different tags
1730        let attention_metadata = ParameterMetadata {
1731            layername: "layer_0".to_string(),
1732            param_name: "attention_weight".to_string(),
1733            shape: vec![512, 512],
1734            requires_grad: true,
1735            paramtype: ParameterType::Attention,
1736            sharing_group: None,
1737            tags: vec!["attention".to_string()],
1738        };
1739
1740        optimizer
1741            .parameter_manager_mut()
1742            .register_parameter("attn_param".to_string(), attention_metadata)
1743            .expect("unwrap failed");
1744
1745        // Apply optimizations
1746        optimizer
1747            .apply_architecture_optimizations()
1748            .expect("unwrap failed");
1749
1750        // Verify that attention parameters get special treatment
1751        assert!(optimizer
1752            .parameter_manager()
1753            .get_parameter_metadata(&"attn_param".to_string())
1754            .is_some());
1755    }
1756}