Skip to main content

sklears_neural/
versioning.rs

1use scirs2_core::ndarray::{Array1, Array2};
2use sklears_core::error::SklearsError;
3use sklears_core::types::FloatBounds;
4use std::collections::HashMap;
5use std::time::{SystemTime, UNIX_EPOCH};
6
7/// Result type for versioning operations
8pub type VersioningResult<T> = Result<T, SklearsError>;
9
10/// Model version identifier
11#[derive(Debug, Clone, PartialEq, Eq, Hash)]
12#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
13pub struct ModelVersion {
14    /// Major version (breaking changes)
15    pub major: u32,
16    /// Minor version (new features, backward compatible)
17    pub minor: u32,
18    /// Patch version (bug fixes, backward compatible)
19    pub patch: u32,
20    /// Optional pre-release identifier
21    pub pre_release: Option<String>,
22    /// Build metadata
23    pub build: Option<String>,
24}
25
26impl ModelVersion {
27    /// Create a new model version
28    pub fn new(major: u32, minor: u32, patch: u32) -> Self {
29        Self {
30            major,
31            minor,
32            patch,
33            pre_release: None,
34            build: None,
35        }
36    }
37
38    /// Create a version with pre-release identifier
39    pub fn with_pre_release(mut self, pre_release: String) -> Self {
40        self.pre_release = Some(pre_release);
41        self
42    }
43
44    /// Create a version with build metadata
45    pub fn with_build(mut self, build: String) -> Self {
46        self.build = Some(build);
47        self
48    }
49
50    /// Check if this version is compatible with another version
51    pub fn is_compatible_with(&self, other: &ModelVersion) -> bool {
52        // Same major version means compatible
53        self.major == other.major
54    }
55
56    /// Check if this version is newer than another
57    pub fn is_newer_than(&self, other: &ModelVersion) -> bool {
58        if self.major != other.major {
59            return self.major > other.major;
60        }
61        if self.minor != other.minor {
62            return self.minor > other.minor;
63        }
64        self.patch > other.patch
65    }
66
67    /// Check if this version requires migration from another
68    pub fn requires_migration_from(&self, other: &ModelVersion) -> bool {
69        self.major != other.major || (self.major == other.major && self.minor > other.minor)
70    }
71}
72
73impl std::fmt::Display for ModelVersion {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
76        if let Some(ref pre) = self.pre_release {
77            write!(f, "-{}", pre)?;
78        }
79        if let Some(ref build) = self.build {
80            write!(f, "+{}", build)?;
81        }
82        Ok(())
83    }
84}
85
86impl std::str::FromStr for ModelVersion {
87    type Err = SklearsError;
88
89    fn from_str(s: &str) -> Result<Self, Self::Err> {
90        let parts: Vec<&str> = s.split('.').collect();
91        if parts.len() < 3 {
92            return Err(SklearsError::InvalidParameter {
93                name: "version".to_string(),
94                reason: "Version must have at least major.minor.patch".to_string(),
95            });
96        }
97
98        let major = parts[0]
99            .parse()
100            .map_err(|_| SklearsError::InvalidParameter {
101                name: "major_version".to_string(),
102                reason: "Major version must be a number".to_string(),
103            })?;
104
105        let minor = parts[1]
106            .parse()
107            .map_err(|_| SklearsError::InvalidParameter {
108                name: "minor_version".to_string(),
109                reason: "Minor version must be a number".to_string(),
110            })?;
111
112        let patch_part = parts[2];
113        let (patch_str, pre_release, build) = if let Some(build_pos) = patch_part.find('+') {
114            let (patch_pre, build_str) = patch_part.split_at(build_pos);
115            let build_str = &build_str[1..]; // Remove '+'
116
117            if let Some(pre_pos) = patch_pre.find('-') {
118                let (patch_str, pre_str) = patch_pre.split_at(pre_pos);
119                let pre_str = &pre_str[1..]; // Remove '-'
120                (
121                    patch_str,
122                    Some(pre_str.to_string()),
123                    Some(build_str.to_string()),
124                )
125            } else {
126                (patch_pre, None, Some(build_str.to_string()))
127            }
128        } else if let Some(pre_pos) = patch_part.find('-') {
129            let (patch_str, pre_str) = patch_part.split_at(pre_pos);
130            let pre_str = &pre_str[1..]; // Remove '-'
131            (patch_str, Some(pre_str.to_string()), None)
132        } else {
133            (patch_part, None, None)
134        };
135
136        let patch = patch_str
137            .parse()
138            .map_err(|_| SklearsError::InvalidParameter {
139                name: "patch_version".to_string(),
140                reason: "Patch version must be a number".to_string(),
141            })?;
142
143        Ok(ModelVersion {
144            major,
145            minor,
146            patch,
147            pre_release,
148            build,
149        })
150    }
151}
152
153/// Migration strategy for model updates
154#[derive(Debug, Clone)]
155pub enum MigrationStrategy {
156    /// No migration needed
157    None,
158    /// Automatic migration with parameter mapping
159    Automatic {
160        /// Mapping from old parameter names to new parameter names
161        parameter_mapping: HashMap<String, String>,
162        /// Default values for any newly introduced parameters
163        default_values: HashMap<String, f64>,
164    },
165    /// Custom migration function
166    Custom {
167        /// Identifier for the custom migration procedure
168        migration_name: String,
169        /// Human-readable description of what the custom migration does
170        description: String,
171    },
172    /// Manual migration required
173    Manual {
174        /// Step-by-step instructions for the human operator to follow
175        instructions: String,
176    },
177}
178
179/// Model metadata for versioning
180#[derive(Debug, Clone)]
181#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
182pub struct ModelMetadata {
183    /// Model version
184    pub version: ModelVersion,
185    /// Timestamp of creation
186    pub created_at: u64,
187    /// Model architecture description
188    pub architecture: String,
189    /// Training configuration hash
190    pub config_hash: Option<String>,
191    /// Performance metrics at time of saving
192    pub performance_metrics: HashMap<String, f64>,
193    /// Model size in parameters
194    pub parameter_count: usize,
195    /// Framework version used
196    pub framework_version: String,
197    /// Custom tags
198    pub tags: Vec<String>,
199    /// Model description
200    pub description: Option<String>,
201}
202
203impl ModelMetadata {
204    /// Create a new `ModelMetadata` record for the given version and architecture description
205    pub fn new(version: ModelVersion, architecture: String) -> Self {
206        let created_at = SystemTime::now()
207            .duration_since(UNIX_EPOCH)
208            .expect("value should be present")
209            .as_secs();
210
211        Self {
212            version,
213            created_at,
214            architecture,
215            config_hash: None,
216            performance_metrics: HashMap::new(),
217            parameter_count: 0,
218            framework_version: env!("CARGO_PKG_VERSION").to_string(),
219            tags: Vec::new(),
220            description: None,
221        }
222    }
223
224    /// Attach a configuration hash string for reproducibility verification
225    pub fn with_config_hash(mut self, hash: String) -> Self {
226        self.config_hash = Some(hash);
227        self
228    }
229
230    /// Record the final evaluation metrics (e.g., accuracy, F1) for this model version
231    pub fn with_performance_metrics(mut self, metrics: HashMap<String, f64>) -> Self {
232        self.performance_metrics = metrics;
233        self
234    }
235
236    /// Record the total number of trainable parameters in the model
237    pub fn with_parameter_count(mut self, count: usize) -> Self {
238        self.parameter_count = count;
239        self
240    }
241
242    /// Attach a human-readable description to this model version
243    pub fn with_description(mut self, description: String) -> Self {
244        self.description = Some(description);
245        self
246    }
247
248    /// Append a searchable tag to this model version record
249    pub fn add_tag(mut self, tag: String) -> Self {
250        self.tags.push(tag);
251        self
252    }
253}
254
255/// Version compatibility checker
256#[derive(Debug, Clone)]
257pub struct CompatibilityChecker {
258    /// Migration strategies for different version transitions
259    migration_strategies: HashMap<(ModelVersion, ModelVersion), MigrationStrategy>,
260    /// Deprecated features by version
261    deprecated_features: HashMap<ModelVersion, Vec<String>>,
262    /// Breaking changes by version
263    breaking_changes: HashMap<ModelVersion, Vec<String>>,
264}
265
266impl CompatibilityChecker {
267    /// Create a new compatibility checker with no registered strategies or deprecations
268    pub fn new() -> Self {
269        Self {
270            migration_strategies: HashMap::new(),
271            deprecated_features: HashMap::new(),
272            breaking_changes: HashMap::new(),
273        }
274    }
275
276    /// Register a migration strategy for version transition
277    pub fn register_migration(
278        &mut self,
279        from: ModelVersion,
280        to: ModelVersion,
281        strategy: MigrationStrategy,
282    ) {
283        self.migration_strategies.insert((from, to), strategy);
284    }
285
286    /// Register deprecated features for a version
287    pub fn register_deprecated_features(&mut self, version: ModelVersion, features: Vec<String>) {
288        self.deprecated_features.insert(version, features);
289    }
290
291    /// Register breaking changes for a version
292    pub fn register_breaking_changes(&mut self, version: ModelVersion, changes: Vec<String>) {
293        self.breaking_changes.insert(version, changes);
294    }
295
296    /// Check compatibility between two versions
297    pub fn check_compatibility(
298        &self,
299        from: &ModelVersion,
300        to: &ModelVersion,
301    ) -> VersioningResult<CompatibilityReport> {
302        let mut report = CompatibilityReport {
303            compatible: from.is_compatible_with(to),
304            migration_required: to.requires_migration_from(from),
305            migration_strategy: MigrationStrategy::None,
306            warnings: Vec::new(),
307            errors: Vec::new(),
308        };
309
310        // Check for breaking changes
311        if to.major > from.major {
312            if let Some(changes) = self.breaking_changes.get(to) {
313                report.errors.extend(changes.iter().cloned());
314                report.compatible = false;
315            }
316        }
317
318        // Check for deprecated features
319        if let Some(deprecated) = self.deprecated_features.get(from) {
320            for feature in deprecated {
321                report
322                    .warnings
323                    .push(format!("Feature '{}' is deprecated", feature));
324            }
325        }
326
327        // Find migration strategy
328        if let Some(strategy) = self.migration_strategies.get(&(from.clone(), to.clone())) {
329            report.migration_strategy = strategy.clone();
330        } else if report.migration_required {
331            report.migration_strategy = MigrationStrategy::Manual {
332                instructions: "Manual migration required - no automatic strategy available"
333                    .to_string(),
334            };
335        }
336
337        Ok(report)
338    }
339}
340
341impl Default for CompatibilityChecker {
342    fn default() -> Self {
343        Self::new()
344    }
345}
346
347/// Compatibility report between model versions
348#[derive(Debug, Clone)]
349pub struct CompatibilityReport {
350    /// Whether versions are compatible
351    pub compatible: bool,
352    /// Whether migration is required
353    pub migration_required: bool,
354    /// Migration strategy to use
355    pub migration_strategy: MigrationStrategy,
356    /// Compatibility warnings
357    pub warnings: Vec<String>,
358    /// Compatibility errors
359    pub errors: Vec<String>,
360}
361
362/// Model version manager
363#[derive(Debug, Clone)]
364pub struct ModelVersionManager {
365    compatibility_checker: CompatibilityChecker,
366    version_history: Vec<ModelMetadata>,
367    current_version: Option<ModelVersion>,
368}
369
370impl ModelVersionManager {
371    /// Create a new version manager pre-loaded with default migration strategies
372    pub fn new() -> Self {
373        let mut compatibility_checker = CompatibilityChecker::new();
374
375        // Register some default migration strategies
376        Self::register_default_migrations(&mut compatibility_checker);
377
378        Self {
379            compatibility_checker,
380            version_history: Vec::new(),
381            current_version: None,
382        }
383    }
384
385    /// Register default migration strategies for common version transitions
386    fn register_default_migrations(checker: &mut CompatibilityChecker) {
387        // Example: Migration from v1.0.0 to v1.1.0 (minor version bump)
388        let from_v1_0 = ModelVersion::new(1, 0, 0);
389        let to_v1_1 = ModelVersion::new(1, 1, 0);
390
391        checker.register_migration(
392            from_v1_0.clone(),
393            to_v1_1.clone(),
394            MigrationStrategy::Automatic {
395                parameter_mapping: HashMap::new(),
396                default_values: HashMap::new(),
397            },
398        );
399
400        // Example: Major version migration (breaking changes)
401        let from_v1_x = ModelVersion::new(1, 9, 0);
402        let to_v2_0 = ModelVersion::new(2, 0, 0);
403
404        checker.register_migration(
405            from_v1_x,
406            to_v2_0.clone(),
407            MigrationStrategy::Manual {
408                instructions: "Major version upgrade requires manual review of model architecture"
409                    .to_string(),
410            },
411        );
412
413        // Register deprecated features
414        checker.register_deprecated_features(
415            ModelVersion::new(1, 5, 0),
416            vec![
417                "old_activation_function".to_string(),
418                "legacy_optimizer".to_string(),
419            ],
420        );
421
422        // Register breaking changes
423        checker.register_breaking_changes(
424            to_v2_0,
425            vec![
426                "Changed default activation from sigmoid to relu".to_string(),
427                "Removed support for legacy file format".to_string(),
428            ],
429        );
430    }
431
432    /// Set the current model version
433    pub fn set_current_version(&mut self, version: ModelVersion, metadata: ModelMetadata) {
434        self.current_version = Some(version);
435        self.version_history.push(metadata);
436    }
437
438    /// Get the current model version
439    pub fn get_current_version(&self) -> Option<&ModelVersion> {
440        self.current_version.as_ref()
441    }
442
443    /// Get version history
444    pub fn get_version_history(&self) -> &[ModelMetadata] {
445        &self.version_history
446    }
447
448    /// Check if a version can be loaded
449    pub fn can_load_version(
450        &self,
451        version: &ModelVersion,
452    ) -> VersioningResult<CompatibilityReport> {
453        if let Some(current) = &self.current_version {
454            self.compatibility_checker
455                .check_compatibility(version, current)
456        } else {
457            Ok(CompatibilityReport {
458                compatible: true,
459                migration_required: false,
460                migration_strategy: MigrationStrategy::None,
461                warnings: Vec::new(),
462                errors: Vec::new(),
463            })
464        }
465    }
466
467    /// Apply automatic migration between versions
468    pub fn apply_migration<T: FloatBounds>(
469        &self,
470        from_version: &ModelVersion,
471        to_version: &ModelVersion,
472        parameters: &mut HashMap<String, Array2<T>>,
473        biases: &mut HashMap<String, Array1<T>>,
474    ) -> VersioningResult<()> {
475        let report = self
476            .compatibility_checker
477            .check_compatibility(from_version, to_version)?;
478
479        if !report.compatible {
480            return Err(SklearsError::InvalidParameter {
481                name: "version_compatibility".to_string(),
482                reason: format!(
483                    "Versions {} and {} are not compatible",
484                    from_version, to_version
485                ),
486            });
487        }
488
489        match report.migration_strategy {
490            MigrationStrategy::None => {
491                // No migration needed
492            }
493            MigrationStrategy::Automatic {
494                parameter_mapping,
495                default_values,
496            } => {
497                // Apply parameter mapping
498                for (old_name, new_name) in parameter_mapping {
499                    if let Some(param) = parameters.remove(&old_name) {
500                        parameters.insert(new_name.clone(), param);
501                    }
502                    if let Some(bias) = biases.remove(&old_name) {
503                        biases.insert(new_name, bias);
504                    }
505                }
506
507                // Add default values for new parameters
508                for (name, default_val) in default_values {
509                    if !parameters.contains_key(&name) {
510                        // Create default parameter with appropriate shape
511                        // This is a simplified example - real implementation would need shape info
512                        let default_param = Array2::from_elem(
513                            (1, 1),
514                            T::from(default_val).unwrap_or_else(|| T::zero()),
515                        );
516                        parameters.insert(name.clone(), default_param);
517                    }
518                }
519            }
520            MigrationStrategy::Custom { migration_name, .. } => {
521                return Err(SklearsError::InvalidParameter {
522                    name: "migration".to_string(),
523                    reason: format!("Custom migration '{}' not implemented", migration_name),
524                });
525            }
526            MigrationStrategy::Manual { instructions } => {
527                return Err(SklearsError::InvalidParameter {
528                    name: "migration".to_string(),
529                    reason: format!("Manual migration required: {}", instructions),
530                });
531            }
532        }
533
534        Ok(())
535    }
536
537    /// Get migration path between versions
538    pub fn get_migration_path(&self, from: &ModelVersion, to: &ModelVersion) -> Vec<ModelVersion> {
539        // For simplicity, this returns direct path
540        // A more sophisticated implementation would find optimal migration path
541        if from.is_compatible_with(to) {
542            vec![from.clone(), to.clone()]
543        } else {
544            // Would need to implement pathfinding through compatible versions
545            vec![]
546        }
547    }
548
549    /// Validate model metadata
550    pub fn validate_metadata(&self, metadata: &ModelMetadata) -> VersioningResult<()> {
551        if metadata.architecture.is_empty() {
552            return Err(SklearsError::InvalidParameter {
553                name: "architecture".to_string(),
554                reason: "Architecture description cannot be empty".to_string(),
555            });
556        }
557
558        if metadata.parameter_count == 0 {
559            return Err(SklearsError::InvalidParameter {
560                name: "parameter_count".to_string(),
561                reason: "Parameter count must be greater than 0".to_string(),
562            });
563        }
564
565        Ok(())
566    }
567}
568
569impl Default for ModelVersionManager {
570    fn default() -> Self {
571        Self::new()
572    }
573}
574
575/// Trait for versioned models
576pub trait VersionedModel {
577    /// Get model version
578    fn get_version(&self) -> &ModelVersion;
579
580    /// Get model metadata
581    fn get_metadata(&self) -> &ModelMetadata;
582
583    /// Check if model is compatible with a version
584    fn is_compatible_with(&self, version: &ModelVersion) -> bool;
585
586    /// Migrate model to a new version
587    fn migrate_to_version(&mut self, version: ModelVersion) -> VersioningResult<()>;
588}
589
590#[allow(non_snake_case)]
591#[cfg(test)]
592mod tests {
593    use super::*;
594
595    #[test]
596    fn test_model_version_creation() {
597        let version = ModelVersion::new(1, 2, 3);
598        assert_eq!(version.major, 1);
599        assert_eq!(version.minor, 2);
600        assert_eq!(version.patch, 3);
601        assert_eq!(version.to_string(), "1.2.3");
602    }
603
604    #[test]
605    fn test_version_parsing() {
606        let version: ModelVersion = "1.2.3".parse().expect("operation should succeed");
607        assert_eq!(version.major, 1);
608        assert_eq!(version.minor, 2);
609        assert_eq!(version.patch, 3);
610
611        let version_with_pre: ModelVersion =
612            "1.2.3-alpha".parse().expect("operation should succeed");
613        assert_eq!(version_with_pre.pre_release, Some("alpha".to_string()));
614
615        let version_with_build: ModelVersion =
616            "1.2.3+20240101".parse().expect("operation should succeed");
617        assert_eq!(version_with_build.build, Some("20240101".to_string()));
618    }
619
620    #[test]
621    fn test_version_compatibility() {
622        let v1_0_0 = ModelVersion::new(1, 0, 0);
623        let v1_1_0 = ModelVersion::new(1, 1, 0);
624        let v2_0_0 = ModelVersion::new(2, 0, 0);
625
626        assert!(v1_0_0.is_compatible_with(&v1_1_0));
627        assert!(!v1_0_0.is_compatible_with(&v2_0_0));
628        assert!(v1_1_0.is_newer_than(&v1_0_0));
629        assert!(v1_1_0.requires_migration_from(&v1_0_0));
630    }
631
632    #[test]
633    fn test_model_metadata() {
634        let version = ModelVersion::new(1, 0, 0);
635        let metadata = ModelMetadata::new(version.clone(), "MLP".to_string())
636            .with_parameter_count(1000)
637            .with_description("Test model".to_string())
638            .add_tag("test".to_string());
639
640        assert_eq!(metadata.version, version);
641        assert_eq!(metadata.architecture, "MLP");
642        assert_eq!(metadata.parameter_count, 1000);
643        assert_eq!(metadata.description, Some("Test model".to_string()));
644        assert!(metadata.tags.contains(&"test".to_string()));
645    }
646
647    #[test]
648    fn test_compatibility_checker() {
649        let mut checker = CompatibilityChecker::new();
650        let v1_0 = ModelVersion::new(1, 0, 0);
651        let v1_1 = ModelVersion::new(1, 1, 0);
652
653        checker.register_migration(
654            v1_0.clone(),
655            v1_1.clone(),
656            MigrationStrategy::Automatic {
657                parameter_mapping: HashMap::new(),
658                default_values: HashMap::new(),
659            },
660        );
661
662        let report = checker
663            .check_compatibility(&v1_0, &v1_1)
664            .expect("operation should succeed");
665        assert!(report.compatible);
666        assert!(report.migration_required);
667        assert!(matches!(
668            report.migration_strategy,
669            MigrationStrategy::Automatic { .. }
670        ));
671    }
672
673    #[test]
674    fn test_version_manager() {
675        let mut manager = ModelVersionManager::new();
676        let version = ModelVersion::new(1, 0, 0);
677        let metadata = ModelMetadata::new(version.clone(), "MLP".to_string());
678
679        manager.set_current_version(version.clone(), metadata);
680        assert_eq!(manager.get_current_version(), Some(&version));
681        assert_eq!(manager.get_version_history().len(), 1);
682    }
683
684    #[test]
685    fn test_migration_application() {
686        let manager = ModelVersionManager::new();
687        let from_version = ModelVersion::new(1, 0, 0);
688        let to_version = ModelVersion::new(1, 0, 1);
689
690        let mut parameters: HashMap<String, Array2<f64>> = HashMap::new();
691        let mut biases: HashMap<String, Array1<f64>> = HashMap::new();
692
693        // Should succeed with no migration needed
694        let result =
695            manager.apply_migration(&from_version, &to_version, &mut parameters, &mut biases);
696        assert!(result.is_ok());
697    }
698}