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
7pub type VersioningResult<T> = Result<T, SklearsError>;
9
10#[derive(Debug, Clone, PartialEq, Eq, Hash)]
12#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
13pub struct ModelVersion {
14 pub major: u32,
16 pub minor: u32,
18 pub patch: u32,
20 pub pre_release: Option<String>,
22 pub build: Option<String>,
24}
25
26impl ModelVersion {
27 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 pub fn with_pre_release(mut self, pre_release: String) -> Self {
40 self.pre_release = Some(pre_release);
41 self
42 }
43
44 pub fn with_build(mut self, build: String) -> Self {
46 self.build = Some(build);
47 self
48 }
49
50 pub fn is_compatible_with(&self, other: &ModelVersion) -> bool {
52 self.major == other.major
54 }
55
56 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 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..]; 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..]; (
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..]; (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#[derive(Debug, Clone)]
155pub enum MigrationStrategy {
156 None,
158 Automatic {
160 parameter_mapping: HashMap<String, String>,
162 default_values: HashMap<String, f64>,
164 },
165 Custom {
167 migration_name: String,
169 description: String,
171 },
172 Manual {
174 instructions: String,
176 },
177}
178
179#[derive(Debug, Clone)]
181#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
182pub struct ModelMetadata {
183 pub version: ModelVersion,
185 pub created_at: u64,
187 pub architecture: String,
189 pub config_hash: Option<String>,
191 pub performance_metrics: HashMap<String, f64>,
193 pub parameter_count: usize,
195 pub framework_version: String,
197 pub tags: Vec<String>,
199 pub description: Option<String>,
201}
202
203impl ModelMetadata {
204 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 pub fn with_config_hash(mut self, hash: String) -> Self {
226 self.config_hash = Some(hash);
227 self
228 }
229
230 pub fn with_performance_metrics(mut self, metrics: HashMap<String, f64>) -> Self {
232 self.performance_metrics = metrics;
233 self
234 }
235
236 pub fn with_parameter_count(mut self, count: usize) -> Self {
238 self.parameter_count = count;
239 self
240 }
241
242 pub fn with_description(mut self, description: String) -> Self {
244 self.description = Some(description);
245 self
246 }
247
248 pub fn add_tag(mut self, tag: String) -> Self {
250 self.tags.push(tag);
251 self
252 }
253}
254
255#[derive(Debug, Clone)]
257pub struct CompatibilityChecker {
258 migration_strategies: HashMap<(ModelVersion, ModelVersion), MigrationStrategy>,
260 deprecated_features: HashMap<ModelVersion, Vec<String>>,
262 breaking_changes: HashMap<ModelVersion, Vec<String>>,
264}
265
266impl CompatibilityChecker {
267 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 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 pub fn register_deprecated_features(&mut self, version: ModelVersion, features: Vec<String>) {
288 self.deprecated_features.insert(version, features);
289 }
290
291 pub fn register_breaking_changes(&mut self, version: ModelVersion, changes: Vec<String>) {
293 self.breaking_changes.insert(version, changes);
294 }
295
296 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 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 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 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#[derive(Debug, Clone)]
349pub struct CompatibilityReport {
350 pub compatible: bool,
352 pub migration_required: bool,
354 pub migration_strategy: MigrationStrategy,
356 pub warnings: Vec<String>,
358 pub errors: Vec<String>,
360}
361
362#[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 pub fn new() -> Self {
373 let mut compatibility_checker = CompatibilityChecker::new();
374
375 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 fn register_default_migrations(checker: &mut CompatibilityChecker) {
387 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 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 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 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 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 pub fn get_current_version(&self) -> Option<&ModelVersion> {
440 self.current_version.as_ref()
441 }
442
443 pub fn get_version_history(&self) -> &[ModelMetadata] {
445 &self.version_history
446 }
447
448 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 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 }
493 MigrationStrategy::Automatic {
494 parameter_mapping,
495 default_values,
496 } => {
497 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 for (name, default_val) in default_values {
509 if !parameters.contains_key(&name) {
510 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 pub fn get_migration_path(&self, from: &ModelVersion, to: &ModelVersion) -> Vec<ModelVersion> {
539 if from.is_compatible_with(to) {
542 vec![from.clone(), to.clone()]
543 } else {
544 vec![]
546 }
547 }
548
549 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
575pub trait VersionedModel {
577 fn get_version(&self) -> &ModelVersion;
579
580 fn get_metadata(&self) -> &ModelMetadata;
582
583 fn is_compatible_with(&self, version: &ModelVersion) -> bool;
585
586 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 let result =
695 manager.apply_migration(&from_version, &to_version, &mut parameters, &mut biases);
696 assert!(result.is_ok());
697 }
698}