Skip to main content

optirs_core/plugin/
sdk.rs

1// Plugin SDK utilities and helpers for optimizer development
2//
3// This module provides a comprehensive SDK for developing custom optimizer plugins,
4// including base classes, utilities, testing frameworks, and development tools.
5
6#[allow(dead_code)]
7use super::core::*;
8#[cfg(feature = "cross-platform-testing")]
9use crate::benchmarking::cross_platform_tester::{PerformanceBaseline, PlatformTarget};
10use crate::error::{OptimError, Result};
11use scirs2_core::ndarray::Array1;
12use scirs2_core::numeric::Float;
13use std::collections::HashMap;
14use std::fmt::Debug;
15
16/// Base optimizer plugin implementation with common functionality
17pub struct BaseOptimizerPlugin<A: Float + std::fmt::Debug> {
18    /// Plugin information
19    info: PluginInfo,
20    /// Plugin capabilities
21    capabilities: PluginCapabilities,
22    /// Optimizer configuration
23    config: OptimizerConfig,
24    /// Internal state
25    state: BaseOptimizerState<A>,
26    /// Performance metrics
27    metrics: PerformanceMetrics,
28    /// Memory usage tracking
29    memory_usage: MemoryUsage,
30    /// Event handlers
31    event_handlers: Vec<Box<dyn PluginEventHandler>>,
32}
33
34impl<A: Float + std::fmt::Debug + Send + Sync> std::fmt::Debug for BaseOptimizerPlugin<A> {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        f.debug_struct("BaseOptimizerPlugin")
37            .field("info", &self.info)
38            .field("capabilities", &self.capabilities)
39            .field("config", &self.config)
40            .field("state", &self.state)
41            .field("metrics", &self.metrics)
42            .field("memory_usage", &self.memory_usage)
43            .field(
44                "event_handlers",
45                &format!("{} handlers", self.event_handlers.len()),
46            )
47            .finish()
48    }
49}
50
51/// Base optimizer state
52#[derive(Debug, Clone)]
53pub struct BaseOptimizerState<A: Float + std::fmt::Debug> {
54    /// Step count
55    pub step_count: usize,
56    /// Parameter count
57    pub param_count: usize,
58    /// Learning rate history
59    pub lr_history: Vec<A>,
60    /// Gradient norms history
61    pub grad_norm_history: Vec<A>,
62    /// Parameter change norms history
63    pub param_change_history: Vec<A>,
64    /// Momentum buffer (SGD-with-momentum velocity), one entry per
65    /// parameter, lazily sized to the incoming gradient on the first step.
66    pub momentum_buffer: Vec<A>,
67    /// Custom state data
68    pub custom_state: HashMap<String, StateValue>,
69}
70
71/// Plugin development utilities
72pub struct PluginSDK;
73
74/// Test configuration
75#[derive(Debug, Clone)]
76pub struct TestConfig {
77    /// Number of test iterations
78    pub iterations: usize,
79    /// Tolerance for numerical tests
80    pub tolerance: f64,
81    /// Random seed for reproducibility
82    pub random_seed: u64,
83    /// Enable performance testing
84    pub enable_performance_tests: bool,
85    /// Enable memory testing
86    pub enable_memory_tests: bool,
87    /// Enable convergence testing
88    pub enable_convergence_tests: bool,
89}
90
91/// Test suite for plugin validation
92#[derive(Debug)]
93pub struct TestSuite<A: Float> {
94    /// Functionality tests
95    pub functionality_tests: Vec<Box<dyn PluginTest<A>>>,
96    /// Performance tests
97    pub performance_tests: Vec<Box<dyn PerformanceTest<A>>>,
98    /// Convergence tests
99    pub convergence_tests: Vec<Box<dyn ConvergenceTest<A>>>,
100    /// Memory tests
101    pub memory_tests: Vec<Box<dyn MemoryTest<A>>>,
102}
103
104/// Individual plugin test trait
105pub trait PluginTest<A: Float>: Debug {
106    /// Run the test
107    fn run_test(&self, plugin: &mut dyn OptimizerPlugin<A>) -> TestResult;
108
109    /// Get test name
110    fn name(&self) -> &str;
111
112    /// Get test description
113    fn description(&self) -> &str;
114}
115
116/// Performance test trait
117pub trait PerformanceTest<A: Float>: Debug {
118    /// Run performance test
119    fn run_performance_test(&self, plugin: &mut dyn OptimizerPlugin<A>) -> PerformanceTestResult;
120
121    /// Get test name
122    fn name(&self) -> &str;
123
124    /// Get performance baseline
125    #[cfg(feature = "cross-platform-testing")]
126    fn baseline(&self) -> PerformanceBaseline;
127}
128
129/// Convergence test trait
130pub trait ConvergenceTest<A: Float>: Debug {
131    /// Run convergence test
132    fn run_convergence_test(&self, plugin: &mut dyn OptimizerPlugin<A>)
133        -> ConvergenceTestResult<A>;
134
135    /// Get test name
136    fn name(&self) -> &str;
137
138    /// Get convergence criteria
139    fn convergence_criteria(&self) -> ConvergenceCriteria<A>;
140}
141
142/// Memory test trait
143pub trait MemoryTest<A: Float>: Debug {
144    /// Run memory test
145    fn run_memory_test(&self, plugin: &mut dyn OptimizerPlugin<A>) -> MemoryTestResult;
146
147    /// Get test name
148    fn name(&self) -> &str;
149
150    /// Get memory constraints
151    fn memory_constraints(&self) -> MemoryConstraints;
152}
153
154/// Test result
155#[derive(Debug, Clone)]
156pub struct TestResult {
157    /// Test passed
158    pub passed: bool,
159    /// Test message
160    pub message: String,
161    /// Execution time
162    pub execution_time: std::time::Duration,
163    /// Additional data
164    pub data: HashMap<String, serde_json::Value>,
165}
166
167/// Performance test result
168#[derive(Debug, Clone)]
169pub struct PerformanceTestResult {
170    /// Performance metrics
171    pub metrics: PerformanceMetrics,
172    /// Comparison with baseline
173    pub baseline_comparison: BaselineComparison,
174    /// Performance score (0.0 to 1.0)
175    pub performance_score: f64,
176}
177
178/// Convergence test result
179#[derive(Debug, Clone)]
180pub struct ConvergenceTestResult<A: Float> {
181    /// Converged successfully
182    pub converged: bool,
183    /// Number of iterations to convergence
184    pub iterations_to_convergence: Option<usize>,
185    /// Final objective value
186    pub final_objective: A,
187    /// Convergence rate
188    pub convergence_rate: f64,
189    /// Convergence metrics
190    pub metrics: ConvergenceMetrics,
191}
192
193/// Memory test result
194#[derive(Debug, Clone)]
195pub struct MemoryTestResult {
196    /// Memory usage metrics
197    pub memory_metrics: MemoryUsage,
198    /// Memory leak detected
199    pub memory_leak_detected: bool,
200    /// Memory efficiency score
201    pub efficiency_score: f64,
202}
203
204/// Baseline comparison
205#[derive(Debug, Clone)]
206pub struct BaselineComparison {
207    /// Relative performance (baseline = 1.0)
208    pub relative_performance: f64,
209    /// Performance difference (absolute)
210    pub absolute_difference: f64,
211    /// Performance improvement (percentage)
212    pub improvement_percent: f64,
213}
214
215/// Convergence criteria
216#[derive(Debug, Clone)]
217pub struct ConvergenceCriteria<A: Float> {
218    /// Maximum iterations
219    pub max_iterations: usize,
220    /// Gradient norm tolerance
221    pub gradient_tolerance: A,
222    /// Function value tolerance
223    pub function_tolerance: A,
224    /// Parameter change tolerance
225    pub parameter_tolerance: A,
226}
227
228/// Memory constraints for testing
229#[derive(Debug, Clone)]
230pub struct MemoryConstraints {
231    /// Maximum memory usage (bytes)
232    pub max_memory_usage: usize,
233    /// Maximum allocation count
234    pub max_allocations: usize,
235    /// Memory leak tolerance (bytes)
236    pub leak_tolerance: usize,
237}
238
239/// Validation rule trait
240pub trait ValidationRule<A: Float>: Debug {
241    /// Validate plugin
242    fn validate(&self, plugin: &dyn OptimizerPlugin<A>) -> ValidationResult;
243
244    /// Get rule name
245    fn name(&self) -> &str;
246
247    /// Get rule severity
248    fn severity(&self) -> ValidationSeverity;
249}
250
251/// Validation result
252#[derive(Debug, Clone)]
253pub struct ValidationResult {
254    /// Validation passed
255    pub passed: bool,
256    /// Validation message
257    pub message: String,
258    /// Severity level
259    pub severity: ValidationSeverity,
260    /// Suggestions for improvement
261    pub suggestions: Vec<String>,
262}
263
264/// Validation severity levels
265#[derive(Debug, Clone)]
266pub enum ValidationSeverity {
267    Info,
268    Warning,
269    Error,
270    Critical,
271}
272
273/// Benchmark trait
274pub trait Benchmark<A: Float>: Debug {
275    /// Run benchmark
276    fn run_benchmark(&self, plugin: &mut dyn OptimizerPlugin<A>) -> BenchmarkResult<A>;
277
278    /// Get benchmark name
279    fn name(&self) -> &str;
280
281    /// Get benchmark description
282    fn description(&self) -> &str;
283
284    /// Get benchmark category
285    fn category(&self) -> BenchmarkCategory;
286}
287
288/// Benchmark categories
289#[derive(Debug, Clone)]
290pub enum BenchmarkCategory {
291    /// Speed benchmarks
292    Speed,
293    /// Memory benchmarks
294    Memory,
295    /// Accuracy benchmarks
296    Accuracy,
297    /// Scalability benchmarks
298    Scalability,
299    /// Robustness benchmarks
300    Robustness,
301}
302
303/// Benchmark result
304#[derive(Debug, Clone)]
305pub struct BenchmarkResult<A: Float> {
306    /// Benchmark name
307    pub name: String,
308    /// Score, normalized into `[0, 1]` against the benchmark's
309    /// `expected_baseline` (higher is better)
310    pub score: f64,
311    /// Metrics
312    pub metrics: HashMap<String, f64>,
313    /// Execution time
314    pub execution_time: std::time::Duration,
315    /// Memory usage
316    pub memory_usage: usize,
317    /// Additional data
318    pub data: HashMap<String, A>,
319    /// Whether this benchmark actually measured the plugin (e.g. `false`
320    /// when `initialize()` failed before any step could run). Unverified
321    /// results are excluded from the overall score rather than counted.
322    pub verified: bool,
323}
324
325/// Benchmark configuration
326#[derive(Debug, Clone)]
327pub struct BenchmarkConfig {
328    /// Number of benchmark runs
329    pub runs: usize,
330    /// Warmup iterations
331    pub warmup_iterations: usize,
332    /// Problem sizes to test
333    pub problem_sizes: Vec<usize>,
334    /// Random seeds
335    pub random_seeds: Vec<u64>,
336}
337
338/// Plugin development helper macros and utilities
339impl PluginSDK {
340    /// Create a plugin template with common functionality
341    pub fn create_plugin_template(name: &str) -> PluginTemplate {
342        PluginTemplate::new(name)
343    }
344
345    /// Validate plugin configuration schema
346    pub fn validate_config_schema(schema: &ConfigSchema) -> Result<()> {
347        for (field_name, field_schema) in &schema.fields {
348            if field_name.is_empty() {
349                return Err(OptimError::InvalidConfig(
350                    "Field name cannot be empty".to_string(),
351                ));
352            }
353
354            if field_schema.description.is_empty() {
355                return Err(OptimError::InvalidConfig(format!(
356                    "Field '{}' must have a description",
357                    field_name
358                )));
359            }
360        }
361        Ok(())
362    }
363
364    /// Generate plugin manifest template
365    pub fn generate_plugin_manifest(info: &PluginInfo) -> String {
366        format!(
367            r#"[plugin]
368name = "{}"
369version = "{}"
370description = "{}"
371author = "{}"
372license = "{}"
373entry_point = "plugin_main"
374
375[build]
376rust_version = "1.70.0"
377target = "*"
378profile = "release"
379
380[runtime]
381min_rust_version = "1.70.0"
382"#,
383            info.name, info.version, info.description, info.author, info.license
384        )
385    }
386
387    /// Create default test configuration
388    pub fn default_test_config() -> TestConfig {
389        TestConfig {
390            iterations: 100,
391            tolerance: 1e-6,
392            random_seed: 42,
393            enable_performance_tests: true,
394            enable_memory_tests: true,
395            enable_convergence_tests: true,
396        }
397    }
398
399    /// Create performance baseline from existing optimizer
400    #[cfg(feature = "cross-platform-testing")]
401    pub fn create_performance_baseline<A>(
402        optimizer: &mut dyn OptimizerPlugin<A>,
403        test_data: &[(Array1<A>, Array1<A>)],
404    ) -> PerformanceBaseline
405    where
406        A: Float + Debug + Send + Sync + 'static,
407    {
408        let start_time = std::time::Instant::now();
409        let mut total_memory = 0;
410
411        for (params, gradients) in test_data {
412            let _result = optimizer.step(params, gradients);
413            total_memory += optimizer.memory_usage().current_usage;
414        }
415
416        let execution_time = start_time.elapsed();
417        let _avg_memory = total_memory / test_data.len();
418
419        PerformanceBaseline {
420            target: PlatformTarget::CPU,
421            throughput_ops_per_sec: test_data.len() as f64 / execution_time.as_secs_f64(),
422            latency_ms: execution_time.as_secs_f64() * 1000.0 / test_data.len() as f64,
423            memory_usage_mb: total_memory as f64 / (1024.0 * 1024.0),
424            energy_consumption_joules: None,
425            accuracy_metrics: HashMap::new(),
426        }
427    }
428}
429
430/// Plugin template for rapid development
431#[derive(Debug)]
432pub struct PluginTemplate {
433    /// Template name
434    name: String,
435    /// Template structure
436    structure: TemplateStructure,
437}
438
439/// Template structure definition
440#[derive(Debug)]
441pub struct TemplateStructure {
442    /// Source files
443    pub source_files: Vec<TemplateFile>,
444    /// Configuration files
445    pub config_files: Vec<TemplateFile>,
446    /// Test files
447    pub test_files: Vec<TemplateFile>,
448    /// Documentation files
449    pub doc_files: Vec<TemplateFile>,
450}
451
452/// Template file
453#[derive(Debug)]
454pub struct TemplateFile {
455    /// File path
456    pub path: String,
457    /// File content
458    pub content: String,
459    /// File type
460    pub file_type: TemplateFileType,
461}
462
463/// Template file types
464#[derive(Debug)]
465pub enum TemplateFileType {
466    /// Rust source file
467    RustSource,
468    /// TOML configuration
469    TomlConfig,
470    /// Markdown documentation
471    Markdown,
472    /// Test file
473    Test,
474}
475
476impl PluginTemplate {
477    /// The template's name, which is also the generated crate/type prefix.
478    pub fn name(&self) -> &str {
479        &self.name
480    }
481
482    /// Create a new plugin template
483    pub fn new(name: &str) -> Self {
484        let structure = Self::create_default_structure(name);
485        Self {
486            name: name.to_string(),
487            structure,
488        }
489    }
490
491    /// Generate template files to directory
492    pub fn generate_to_directory(&self, outputdir: &std::path::Path) -> Result<()> {
493        std::fs::create_dir_all(outputdir)?;
494
495        for file in &self.structure.source_files {
496            let file_path = outputdir.join(&file.path);
497            if let Some(parent) = file_path.parent() {
498                std::fs::create_dir_all(parent)?;
499            }
500            std::fs::write(&file_path, &file.content)?;
501        }
502
503        for file in &self.structure.config_files {
504            let file_path = outputdir.join(&file.path);
505            std::fs::write(&file_path, &file.content)?;
506        }
507
508        for file in &self.structure.test_files {
509            let file_path = outputdir.join(&file.path);
510            if let Some(parent) = file_path.parent() {
511                std::fs::create_dir_all(parent)?;
512            }
513            std::fs::write(&file_path, &file.content)?;
514        }
515
516        Ok(())
517    }
518
519    fn create_default_structure(name: &str) -> TemplateStructure {
520        let lib_rs_content = format!(
521            r#"//! {} optimizer plugin
522//
523// This is an auto-generated plugin template.
524
525use optirs_core::plugin::*;
526use scirs2_core::ndarray::Array1;
527use scirs2_core::numeric::Float;
528
529#[derive(Debug)]
530pub struct {}Optimizer<A: Float> {{
531    learning_rate: A,
532    // Add your optimizer state here
533}}
534
535impl<A: Float + Send + Sync> {}Optimizer<A> {{
536    pub fn new(_learningrate: A) -> Self {{
537        Self {{
538            learning_rate,
539        }}
540    }}
541}}
542
543impl<A: Float + std::fmt::Debug + Send + Sync + 'static> OptimizerPlugin<A> for {}Optimizer<A> {{
544    fn step(&mut self, params: &Array1<A>, gradients: &Array1<A>) -> Result<Array1<A>> {{
545        // Implement your optimization step here
546        Ok(params - &(gradients * self.learning_rate))
547    }}
548    
549    fn name(&self) -> &str {{
550        "{}"
551    }}
552    
553    fn version(&self) -> &str {{
554        "0.1.0"
555    }}
556    
557    fn plugin_info(&self) -> PluginInfo {{
558        create_plugin_info("{}", "0.1.0", "Plugin Developer")
559    }}
560    
561    fn capabilities(&self) -> PluginCapabilities {{
562        create_basic_capabilities()
563    }}
564    
565    fn initialize(&mut self, paramshape: &[usize]) -> Result<()> {{
566        Ok(())
567    }}
568    
569    fn reset(&mut self) -> Result<()> {{
570        Ok(())
571    }}
572    
573    fn get_config(&self) -> OptimizerConfig {{
574        OptimizerConfig::default()
575    }}
576    
577    fn set_config(&mut self, config: OptimizerConfig) -> Result<()> {{
578        Ok(())
579    }}
580    
581    fn get_state(&self) -> Result<OptimizerState> {{
582        Ok(OptimizerState::default())
583    }}
584    
585    fn set_state(&mut self, state: OptimizerState) -> Result<()> {{
586        Ok(())
587    }}
588    
589    fn clone_plugin(&self) -> Box<dyn OptimizerPlugin<A>> {{
590        Box::new(Self::new(self.learning_rate))
591    }}
592}}
593
594// Plugin factory implementation
595#[derive(Debug)]
596pub struct {}Factory;
597
598impl<A: Float + std::fmt::Debug + Send + Sync + 'static> OptimizerPluginFactory<A> for {}Factory {{
599    fn create_optimizer(&self, config: OptimizerConfig) -> Result<Box<dyn OptimizerPlugin<A>>> {{
600        let learning_rate = A::from(config.learning_rate).ok_or_else(|| {{
601            OptimError::InvalidConfig(format!(
602                "learning_rate {{}} is not representable in this optimizer's element type",
603                config.learning_rate
604            ))
605        }})?;
606        Ok(Box::new({}Optimizer::new(learning_rate)))
607    }}
608    
609    fn factory_info(&self) -> PluginInfo {{
610        create_plugin_info("{}", "0.1.0", "Plugin Developer")
611    }}
612    
613    fn validate_config(&self, config: &OptimizerConfig) -> Result<()> {{
614        if config.learning_rate <= 0.0 {{
615            return Err(OptimError::InvalidConfig(
616                "Learning rate must be positive".to_string(),
617            ));
618        }}
619        Ok(())
620    }}
621    
622    fn default_config(&self) -> OptimizerConfig {{
623        OptimizerConfig {{
624            learning_rate: 0.001,
625            ..Default::default()
626        }}
627    }}
628    
629    fn config_schema(&self) -> ConfigSchema {{
630        let mut schema = ConfigSchema {{
631            fields: std::collections::HashMap::new(),
632            required_fields: vec!["learning_rate".to_string()],
633            version: "1.0".to_string(),
634        }};
635        
636        schema.fields.insert(
637            "learning_rate".to_string(),
638            FieldSchema {{
639                field_type: FieldType::Float {{ min: Some(0.0), max: None }},
640                description: "Learning rate for optimization".to_string(),
641                default_value: Some(ConfigValue::Float(0.001)),
642                constraints: vec![ValidationConstraint::Positive],
643                required: true,
644            }},
645        );
646        
647        schema
648    }}
649}}
650"#,
651            name, name, name, name, name, name, name, name, name, name
652        );
653
654        let plugin_toml_content = format!(
655            r#"[plugin]
656name = "{}"
657version = "0.1.0"
658description = "Custom optimizer plugin"
659author = "Plugin Developer"
660license = "MIT"
661entry_point = "plugin_main"
662
663[build]
664rust_version = "1.70.0"
665target = "*"
666profile = "release"
667
668[runtime]
669min_rust_version = "1.70.0"
670"#,
671            name
672        );
673
674        let test_content = format!(
675            r#"//! Tests for {} optimizer plugin
676
677use super::*;
678use scirs2_core::ndarray::Array1;
679
680#[test]
681#[allow(dead_code)]
682fn test_{}_basic_functionality() {{
683    let mut optimizer = {}Optimizer::new(0.01);
684    let params = Array1::from_vec(vec![1.0, 2.0, 3.0]);
685    let gradients = Array1::from_vec(vec![0.1, 0.2, 0.3]);
686    
687    let result = optimizer.step(&params, &gradients).expect("a step over finite inputs");
688    
689    // Verify the result
690    assert!((result[0] - 0.999).abs() < 1e-6);
691    assert!((result[1] - 1.998).abs() < 1e-6);
692    assert!((result[2] - 2.997).abs() < 1e-6);
693}}
694
695#[test]
696#[allow(dead_code)]
697fn test_{}_convergence() {{
698    let mut optimizer = {}Optimizer::new(0.1);
699    let mut params = Array1::from_vec(vec![1.0, 1.0]);
700    
701    // Optimize towards zero
702    for _ in 0..100 {{
703        let gradients = &params * 2.0; // Gradient of x^2
704        params = optimizer.step(&params, &gradients).expect("a step over finite inputs");
705    }}
706    
707    // Should converge close to zero
708    assert!(params.iter().all(|&x| x.abs() < 0.1));
709}}
710"#,
711            name,
712            name.to_lowercase(),
713            name,
714            name.to_lowercase(),
715            name
716        );
717
718        TemplateStructure {
719            source_files: vec![TemplateFile {
720                path: "src/lib.rs".to_string(),
721                content: lib_rs_content,
722                file_type: TemplateFileType::RustSource,
723            }],
724            config_files: vec![TemplateFile {
725                path: "plugin.toml".to_string(),
726                content: plugin_toml_content,
727                file_type: TemplateFileType::TomlConfig,
728            }],
729            test_files: vec![TemplateFile {
730                path: "tests/integration_tests.rs".to_string(),
731                content: test_content,
732                file_type: TemplateFileType::Test,
733            }],
734            doc_files: vec![],
735        }
736    }
737}
738
739// Implementation for base optimizer plugin
740
741impl<A: Float + Debug + Send + Sync + 'static> BaseOptimizerPlugin<A> {
742    /// Create a new base optimizer plugin
743    pub fn new(info: PluginInfo, capabilities: PluginCapabilities) -> Self {
744        Self {
745            info,
746            capabilities,
747            config: OptimizerConfig::default(),
748            state: BaseOptimizerState::new(),
749            metrics: PerformanceMetrics::default(),
750            memory_usage: MemoryUsage::default(),
751            event_handlers: Vec::new(),
752        }
753    }
754
755    /// Add event handler
756    pub fn add_event_handler(&mut self, handler: Box<dyn PluginEventHandler>) {
757        self.event_handlers.push(handler);
758    }
759
760    /// Update performance metrics
761    pub fn update_metrics(&mut self, steptime: std::time::Duration) {
762        self.metrics.total_steps += 1;
763        self.metrics.avg_step_time = (self.metrics.avg_step_time
764            * (self.metrics.total_steps - 1) as f64
765            + steptime.as_secs_f64())
766            / self.metrics.total_steps as f64;
767        self.metrics.throughput = 1.0 / self.metrics.avg_step_time;
768    }
769}
770
771impl<A: Float + std::fmt::Debug + Send + Sync> BaseOptimizerState<A> {
772    fn new() -> Self {
773        Self {
774            step_count: 0,
775            param_count: 0,
776            lr_history: Vec::new(),
777            grad_norm_history: Vec::new(),
778            param_change_history: Vec::new(),
779            momentum_buffer: Vec::new(),
780            custom_state: HashMap::new(),
781        }
782    }
783}
784
785impl<A: Float + Debug + Send + Sync + 'static> OptimizerPlugin<A> for BaseOptimizerPlugin<A> {
786    /// SGD with momentum, weight decay, and L2 gradient-norm clipping,
787    /// driven entirely by `self.config` (`OptimizerConfig`). This is the
788    /// SDK's documented base class for plugin authors: it must itself be a
789    /// working, registrable optimizer -- not merely a field-storage struct
790    /// -- since `BaseOptimizerPlugin` is re-exported at the crate root and
791    /// used directly wherever a caller needs a plain baseline.
792    fn step(&mut self, params: &Array1<A>, gradients: &Array1<A>) -> Result<Array1<A>> {
793        if params.len() != gradients.len() {
794            return Err(OptimError::DimensionMismatch(format!(
795                "parameter vector has {} elements but gradient vector has {}",
796                params.len(),
797                gradients.len()
798            )));
799        }
800        if params.is_empty() {
801            return Err(OptimError::InvalidConfig(
802                "cannot step an optimizer over zero parameters".to_string(),
803            ));
804        }
805
806        let lr = A::from(self.config.learning_rate).ok_or_else(|| {
807            OptimError::InvalidConfig(format!(
808                "learning rate {} could not be represented in the parameter type",
809                self.config.learning_rate
810            ))
811        })?;
812        let weight_decay = A::from(self.config.weight_decay).ok_or_else(|| {
813            OptimError::InvalidConfig(format!(
814                "weight decay {} could not be represented in the parameter type",
815                self.config.weight_decay
816            ))
817        })?;
818        let momentum = A::from(self.config.momentum).ok_or_else(|| {
819            OptimError::InvalidConfig(format!(
820                "momentum {} could not be represented in the parameter type",
821                self.config.momentum
822            ))
823        })?;
824
825        // Decoupled weight decay (params contribute to the effective
826        // gradient before clipping/momentum, matching standard SGD-with-
827        // weight-decay semantics).
828        let mut effective_grad: Vec<A> = gradients
829            .iter()
830            .zip(params.iter())
831            .map(|(&g, &p)| g + weight_decay * p)
832            .collect();
833
834        // Optional global-norm gradient clipping.
835        let grad_norm = effective_grad
836            .iter()
837            .fold(A::zero(), |acc, &g| acc + g * g)
838            .sqrt();
839        if let Some(clip) = self.config.gradient_clip {
840            let clip_a = A::from(clip).ok_or_else(|| {
841                OptimError::InvalidConfig(format!(
842                    "gradient clip {clip} could not be represented in the parameter type"
843                ))
844            })?;
845            if clip_a > A::zero() && grad_norm > clip_a {
846                let scale = clip_a / grad_norm;
847                for g in &mut effective_grad {
848                    *g = *g * scale;
849                }
850            }
851        }
852
853        // Lazily (re)size the momentum buffer to match the incoming
854        // parameter count -- `initialize()` may have been called with a
855        // different `paramshape`, or never called at all.
856        if self.state.momentum_buffer.len() != params.len() {
857            self.state.momentum_buffer = vec![A::zero(); params.len()];
858        }
859
860        let mut new_params = Array1::<A>::zeros(params.len());
861        let mut change_sq = A::zero();
862        for i in 0..params.len() {
863            let velocity = momentum * self.state.momentum_buffer[i] + effective_grad[i];
864            self.state.momentum_buffer[i] = velocity;
865            let delta = lr * velocity;
866            let updated = params[i] - delta;
867            new_params[i] = updated;
868            change_sq = change_sq + delta * delta;
869        }
870
871        self.state.step_count += 1;
872        self.state.param_count = params.len();
873        self.state.lr_history.push(lr);
874        self.state.grad_norm_history.push(grad_norm);
875        self.state.param_change_history.push(change_sq.sqrt());
876
877        for handler in &mut self.event_handlers {
878            let params_f64 = Array1::from_iter(params.iter().map(|p| p.to_f64().unwrap_or(0.0)));
879            let grad_f64 = Array1::from_iter(gradients.iter().map(|g| g.to_f64().unwrap_or(0.0)));
880            handler.on_step(self.state.step_count, &params_f64, &grad_f64);
881        }
882
883        Ok(new_params)
884    }
885
886    fn name(&self) -> &str {
887        &self.info.name
888    }
889
890    fn version(&self) -> &str {
891        &self.info.version
892    }
893
894    fn plugin_info(&self) -> PluginInfo {
895        self.info.clone()
896    }
897
898    fn capabilities(&self) -> PluginCapabilities {
899        self.capabilities.clone()
900    }
901
902    fn initialize(&mut self, paramshape: &[usize]) -> Result<()> {
903        if paramshape.is_empty() {
904            return Err(OptimError::InvalidConfig(
905                "cannot initialize with zero parameter groups".to_string(),
906            ));
907        }
908        let total: usize = paramshape.iter().product();
909        self.state = BaseOptimizerState::new();
910        self.state.param_count = total;
911        self.state.momentum_buffer = vec![A::zero(); total];
912        Ok(())
913    }
914
915    fn reset(&mut self) -> Result<()> {
916        let param_count = self.state.param_count;
917        self.state = BaseOptimizerState::new();
918        self.state.param_count = param_count;
919        self.state.momentum_buffer = vec![A::zero(); param_count];
920        Ok(())
921    }
922
923    fn get_config(&self) -> OptimizerConfig {
924        self.config.clone()
925    }
926
927    fn set_config(&mut self, config: OptimizerConfig) -> Result<()> {
928        self.config = config;
929        Ok(())
930    }
931
932    fn get_state(&self) -> Result<OptimizerState> {
933        let mut state_vectors = HashMap::new();
934        state_vectors.insert(
935            "momentum_buffer".to_string(),
936            self.state
937                .momentum_buffer
938                .iter()
939                .map(|v| v.to_f64().unwrap_or(0.0))
940                .collect(),
941        );
942        Ok(OptimizerState {
943            state_vectors,
944            step_count: self.state.step_count,
945            custom_state: self.state.custom_state.clone(),
946        })
947    }
948
949    fn set_state(&mut self, state: OptimizerState) -> Result<()> {
950        self.state.step_count = state.step_count;
951        self.state.custom_state = state.custom_state;
952        if let Some(buf) = state.state_vectors.get("momentum_buffer") {
953            self.state.momentum_buffer = buf
954                .iter()
955                .map(|&v| A::from(v).unwrap_or_else(A::zero))
956                .collect();
957            self.state.param_count = self.state.momentum_buffer.len();
958        }
959        Ok(())
960    }
961
962    fn clone_plugin(&self) -> Box<dyn OptimizerPlugin<A>> {
963        Box::new(BaseOptimizerPlugin {
964            info: self.info.clone(),
965            capabilities: self.capabilities.clone(),
966            config: self.config.clone(),
967            state: self.state.clone(),
968            metrics: self.metrics.clone(),
969            memory_usage: self.memory_usage.clone(),
970            // Event handlers are per-instance observers, not cloneable
971            // state; a clone starts with none attached, matching the
972            // semantics of every other plugin's `clone_plugin`.
973            event_handlers: Vec::new(),
974        })
975    }
976
977    fn memory_usage(&self) -> MemoryUsage {
978        self.memory_usage.clone()
979    }
980
981    fn performance_metrics(&self) -> PerformanceMetrics {
982        self.metrics.clone()
983    }
984}
985
986// Default implementations
987
988impl Default for TestConfig {
989    fn default() -> Self {
990        Self {
991            iterations: 100,
992            tolerance: 1e-6,
993            random_seed: 42,
994            enable_performance_tests: true,
995            enable_memory_tests: true,
996            enable_convergence_tests: true,
997        }
998    }
999}
1000
1001impl Default for BenchmarkConfig {
1002    fn default() -> Self {
1003        Self {
1004            runs: 10,
1005            warmup_iterations: 5,
1006            problem_sizes: vec![10, 100, 1000],
1007            random_seeds: vec![42, 123, 456],
1008        }
1009    }
1010}
1011
1012/// Macro for creating a simple optimizer plugin
1013#[macro_export]
1014macro_rules! create_optimizer_plugin {
1015    ($name:ident, $step_fn:expr) => {
1016        #[derive(Debug)]
1017        pub struct $name<A: Float> {
1018            config: OptimizerConfig,
1019            state: OptimizerState,
1020            _phantom: std::marker::PhantomData<A>,
1021        }
1022
1023        impl<A: Float + Send + Sync> $name<A> {
1024            pub fn new() -> Self {
1025                Self {
1026                    config: OptimizerConfig::default(),
1027                    state: OptimizerState::default(),
1028                    _phantom: std::marker::PhantomData,
1029                }
1030            }
1031        }
1032
1033        impl<A: Float + Send + Sync> Default for $name<A> {
1034            fn default() -> Self {
1035                Self::new()
1036            }
1037        }
1038
1039        impl<A: Float + std::fmt::Debug + Send + Sync + 'static> OptimizerPlugin<A> for $name<A> {
1040            fn step(&mut self, params: &Array1<A>, gradients: &Array1<A>) -> Result<Array1<A>> {
1041                $step_fn(self, params, gradients)
1042            }
1043
1044            fn name(&self) -> &str {
1045                stringify!($name)
1046            }
1047
1048            fn version(&self) -> &str {
1049                "0.1.0"
1050            }
1051
1052            fn plugin_info(&self) -> PluginInfo {
1053                create_plugin_info(stringify!($name), "0.1.0", "Auto-generated")
1054            }
1055
1056            fn capabilities(&self) -> PluginCapabilities {
1057                create_basic_capabilities()
1058            }
1059
1060            fn initialize(&mut self, paramshape: &[usize]) -> Result<()> {
1061                if paramshape.is_empty() {
1062                    return Err($crate::error::OptimError::InvalidConfig(
1063                        "cannot initialize a plugin optimizer with zero parameter groups"
1064                            .to_string(),
1065                    ));
1066                }
1067                // A fresh initialization starts a fresh optimization run:
1068                // reset the step counter so re-initializing an existing
1069                // instance for a new run does not inherit a stale count.
1070                self.state.step_count = 0;
1071                Ok(())
1072            }
1073
1074            fn reset(&mut self) -> Result<()> {
1075                self.state = OptimizerState::default();
1076                Ok(())
1077            }
1078
1079            fn get_config(&self) -> OptimizerConfig {
1080                self.config.clone()
1081            }
1082
1083            fn set_config(&mut self, config: OptimizerConfig) -> Result<()> {
1084                self.config = config;
1085                Ok(())
1086            }
1087
1088            fn get_state(&self) -> Result<OptimizerState> {
1089                Ok(self.state.clone())
1090            }
1091
1092            fn set_state(&mut self, state: OptimizerState) -> Result<()> {
1093                self.state = state;
1094                Ok(())
1095            }
1096
1097            fn clone_plugin(&self) -> Box<dyn OptimizerPlugin<A>> {
1098                Box::new(Self {
1099                    config: self.config.clone(),
1100                    state: self.state.clone(),
1101                    _phantom: std::marker::PhantomData,
1102                })
1103            }
1104        }
1105    };
1106}
1107
1108#[cfg(test)]
1109mod tests {
1110    use super::*;
1111
1112    // Compile-time exercise of `create_optimizer_plugin!`, kept inside the
1113    // test module (not at module scope) so the generated `pub struct` never
1114    // becomes part of the shipped public API: the macro was invoked nowhere
1115    // in the workspace, so a field-name typo (`phantom` in the struct vs
1116    // `_phantom` in the constructor) went uncaught until any real user
1117    // tried to expand it. `use super::*` above still brings in the parent
1118    // module's `use super::core::*`, so the expansion resolves normally.
1119    //
1120    // The closure intentionally avoids naming the impl's generic parameter
1121    // `A` (macro hygiene keeps a bare `A` written at this call site in a
1122    // distinct context from the `A` the macro binds in `impl<A: ...>`); its
1123    // parameter and return types are inferred entirely from the immediate
1124    // call `$step_fn(self, params, gradients)` inside a `-> Result<Array1<A>>`
1125    // body.
1126    create_optimizer_plugin!(MacroGeneratedSgd, |_this, params, gradients| {
1127        Ok(params - gradients)
1128    });
1129
1130    #[test]
1131    fn test_macro_generated_plugin_compiles_and_steps() {
1132        // Exercises the `create_optimizer_plugin!` expansion end-to-end:
1133        // construction, `initialize`, `step`, and `clone_plugin` all need
1134        // the struct's field names (`_phantom`, not `phantom`) to agree
1135        // between the struct definition and the constructor for this to
1136        // compile at all.
1137        let mut plugin: MacroGeneratedSgd<f64> = MacroGeneratedSgd::new();
1138        plugin.initialize(&[3]).expect("initialize should succeed");
1139
1140        let params = Array1::from(vec![1.0_f64, 2.0, 3.0]);
1141        let grads = Array1::from(vec![0.1_f64, 0.2, 0.3]);
1142        let updated = plugin.step(&params, &grads).expect("step should succeed");
1143        assert_eq!(updated.len(), 3);
1144
1145        // clone_plugin must preserve config/state, not silently reset to a
1146        // fresh instance.
1147        plugin
1148            .set_config(OptimizerConfig {
1149                learning_rate: 0.5,
1150                ..OptimizerConfig::default()
1151            })
1152            .expect("set_config should succeed");
1153        let cloned = plugin.clone_plugin();
1154        assert_eq!(cloned.get_config().learning_rate, 0.5);
1155    }
1156
1157    #[test]
1158    fn test_macro_generated_plugin_rejects_empty_paramshape() {
1159        let mut plugin: MacroGeneratedSgd<f64> = MacroGeneratedSgd::new();
1160        assert!(plugin.initialize(&[]).is_err());
1161    }
1162
1163    // F73 regression: `BaseOptimizerPlugin` previously implemented only
1164    // `Debug` -- there was no `impl OptimizerPlugin<A> for
1165    // BaseOptimizerPlugin<A>` at all, so the SDK's documented base class
1166    // (re-exported at the crate root) could never be registered or used
1167    // anywhere an `OptimizerPlugin` was expected. These exercise the trait
1168    // impl through the trait itself (`&mut dyn OptimizerPlugin<f64>`), not
1169    // just inherent methods, so a future regression back to "no impl" is a
1170    // compile error here, not a silently-vanished capability.
1171    fn base_plugin() -> BaseOptimizerPlugin<f64> {
1172        BaseOptimizerPlugin::new(
1173            create_plugin_info("TestBase", "0.1.0", "test"),
1174            create_basic_capabilities(),
1175        )
1176    }
1177
1178    #[test]
1179    fn test_base_optimizer_plugin_implements_the_trait() {
1180        let mut owned = base_plugin();
1181        let plugin: &mut dyn OptimizerPlugin<f64> = &mut owned;
1182        plugin.initialize(&[3]).expect("initialize should succeed");
1183
1184        let params = Array1::from(vec![1.0_f64, 2.0, 3.0]);
1185        let grads = Array1::from(vec![0.1_f64, 0.2, 0.3]);
1186        let updated = plugin.step(&params, &grads).expect("step should succeed");
1187        assert_eq!(updated.len(), 3);
1188        // Plain gradient descent moves parameters opposite the gradient.
1189        for (u, p) in updated.iter().zip(params.iter()) {
1190            assert!(u < p, "expected descent step to reduce each parameter");
1191        }
1192    }
1193
1194    #[test]
1195    fn test_base_optimizer_plugin_rejects_dimension_mismatch() {
1196        let mut plugin = base_plugin();
1197        plugin.initialize(&[3]).expect("initialize should succeed");
1198        let params = Array1::from(vec![1.0_f64, 2.0, 3.0]);
1199        let grads = Array1::from(vec![0.1_f64, 0.2]);
1200        assert!(plugin.step(&params, &grads).is_err());
1201    }
1202
1203    #[test]
1204    fn test_base_optimizer_plugin_gradient_clipping_bounds_update_norm() {
1205        let mut plugin = base_plugin();
1206        plugin
1207            .set_config(OptimizerConfig {
1208                learning_rate: 1.0,
1209                weight_decay: 0.0,
1210                momentum: 0.0,
1211                gradient_clip: Some(1.0),
1212                ..OptimizerConfig::default()
1213            })
1214            .expect("set_config should succeed");
1215        plugin.initialize(&[2]).expect("initialize should succeed");
1216
1217        let params = Array1::from(vec![0.0_f64, 0.0]);
1218        // Unclipped gradient norm is 100 * sqrt(2) >> clip of 1.0.
1219        let grads = Array1::from(vec![100.0_f64, 100.0]);
1220        let updated = plugin.step(&params, &grads).expect("step should succeed");
1221        let step_norm =
1222            ((updated[0] - params[0]).powi(2) + (updated[1] - params[1]).powi(2)).sqrt();
1223        assert!(
1224            step_norm <= 1.0 + 1e-9,
1225            "clipped step norm should not exceed the configured clip value, got {step_norm}"
1226        );
1227    }
1228
1229    #[test]
1230    fn test_base_optimizer_plugin_state_round_trips() {
1231        let mut plugin = base_plugin();
1232        plugin.initialize(&[2]).expect("initialize should succeed");
1233        let params = Array1::from(vec![1.0_f64, 1.0]);
1234        let grads = Array1::from(vec![0.5_f64, 0.5]);
1235        plugin.step(&params, &grads).expect("step should succeed");
1236
1237        let saved = plugin.get_state().expect("get_state should succeed");
1238        assert_eq!(saved.step_count, 1);
1239
1240        let mut restored = base_plugin();
1241        restored.set_state(saved).expect("set_state should succeed");
1242        assert_eq!(restored.get_state().unwrap().step_count, 1);
1243    }
1244
1245    #[test]
1246    fn test_base_optimizer_plugin_clone_preserves_config_and_state() {
1247        let mut plugin = base_plugin();
1248        plugin
1249            .set_config(OptimizerConfig {
1250                learning_rate: 0.25,
1251                ..OptimizerConfig::default()
1252            })
1253            .expect("set_config should succeed");
1254        plugin.initialize(&[2]).expect("initialize should succeed");
1255        let params = Array1::from(vec![1.0_f64, 1.0]);
1256        let grads = Array1::from(vec![0.5_f64, 0.5]);
1257        plugin.step(&params, &grads).expect("step should succeed");
1258
1259        let cloned = plugin.clone_plugin();
1260        assert_eq!(cloned.get_config().learning_rate, 0.25);
1261        assert_eq!(cloned.get_state().unwrap().step_count, 1);
1262    }
1263
1264    #[test]
1265    fn test_plugin_template_creation() {
1266        let template = PluginTemplate::new("TestOptimizer");
1267        assert_eq!(template.name, "TestOptimizer");
1268        assert!(!template.structure.source_files.is_empty());
1269    }
1270
1271    #[test]
1272    fn test_test_config_default() {
1273        let config = TestConfig::default();
1274        assert_eq!(config.iterations, 100);
1275        assert!(config.enable_performance_tests);
1276    }
1277
1278    #[test]
1279    fn test_sdk_config_validation() {
1280        let mut schema = ConfigSchema {
1281            fields: HashMap::new(),
1282            required_fields: vec!["test_field".to_string()],
1283            version: "1.0".to_string(),
1284        };
1285
1286        schema.fields.insert(
1287            "test_field".to_string(),
1288            FieldSchema {
1289                field_type: FieldType::Float {
1290                    min: None,
1291                    max: None,
1292                },
1293                description: "Test field".to_string(),
1294                default_value: None,
1295                constraints: Vec::new(),
1296                required: true,
1297            },
1298        );
1299
1300        assert!(PluginSDK::validate_config_schema(&schema).is_ok());
1301
1302        // Test with empty field name
1303        schema.fields.insert(
1304            "".to_string(),
1305            FieldSchema {
1306                field_type: FieldType::Float {
1307                    min: None,
1308                    max: None,
1309                },
1310                description: "Test".to_string(),
1311                default_value: None,
1312                constraints: Vec::new(),
1313                required: false,
1314            },
1315        );
1316
1317        assert!(PluginSDK::validate_config_schema(&schema).is_err());
1318    }
1319}