Skip to main content

scirs2_cluster/serialization/
core.rs

1//! Core serialization traits and metadata structures
2//!
3//! This module provides the fundamental traits and metadata structures
4//! for model serialization and deserialization.
5
6use crate::error::{ClusteringError, Result};
7use oxiarc_deflate::{gzip_compress, gzip_decompress};
8use serde::{Deserialize, Serialize};
9use std::fs::File;
10use std::io::{Read, Write};
11use std::path::Path;
12use std::time::{SystemTime, UNIX_EPOCH};
13
14/// Trait for clustering models that can be serialized
15pub trait SerializableModel: Serialize + for<'de> Deserialize<'de> {
16    /// Save the model to a file
17    fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
18        let file = File::create(path)
19            .map_err(|e| ClusteringError::InvalidInput(format!("Failed to create file: {}", e)))?;
20        self.save_to_writer(file)
21    }
22
23    /// Save the model to a writer
24    fn save_to_writer<W: Write>(&self, writer: W) -> Result<()> {
25        serde_json::to_writer_pretty(writer, self)
26            .map_err(|e| ClusteringError::InvalidInput(format!("Failed to serialize model: {}", e)))
27    }
28
29    /// Save the model to a file with compression
30    fn save_to_file_compressed<P: AsRef<Path>>(&self, path: P) -> Result<()> {
31        // Serialize to JSON bytes first
32        let json_bytes = serde_json::to_vec_pretty(self).map_err(|e| {
33            ClusteringError::InvalidInput(format!("Failed to serialize model: {}", e))
34        })?;
35        // Compress with oxiarc-deflate gzip (level 6 = default)
36        let compressed = gzip_compress(&json_bytes, 6)
37            .map_err(|e| ClusteringError::InvalidInput(format!("Failed to compress: {}", e)))?;
38        std::fs::write(path, compressed)
39            .map_err(|e| ClusteringError::InvalidInput(format!("Failed to write file: {}", e)))
40    }
41
42    /// Load the model from a compressed file
43    fn load_from_file_compressed<P: AsRef<Path>>(path: P) -> Result<Self> {
44        let compressed = std::fs::read(path.as_ref())
45            .map_err(|e| ClusteringError::InvalidInput(format!("Failed to read file: {}", e)))?;
46        let decompressed = gzip_decompress(&compressed)
47            .map_err(|e| ClusteringError::InvalidInput(format!("Failed to decompress: {}", e)))?;
48        serde_json::from_slice(&decompressed).map_err(|e| {
49            ClusteringError::InvalidInput(format!("Failed to deserialize model: {}", e))
50        })
51    }
52
53    /// Load the model from a file
54    fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
55        let mut file = File::open(path)
56            .map_err(|e| ClusteringError::InvalidInput(format!("Failed to open file: {}", e)))?;
57        Self::load_from_reader(&mut file)
58    }
59
60    /// Load the model from a reader
61    fn load_from_reader<R: Read>(reader: R) -> Result<Self> {
62        serde_json::from_reader(reader).map_err(|e| {
63            ClusteringError::InvalidInput(format!("Failed to deserialize model: {}", e))
64        })
65    }
66}
67
68/// Enhanced model metadata with versioning and performance metrics
69#[derive(Serialize, Deserialize, Debug, Clone)]
70pub struct EnhancedModelMetadata {
71    /// Model format version for backward compatibility
72    pub format_version: String,
73    /// scirs2-cluster library version
74    pub library_version: String,
75    /// Timestamp when model was created (Unix epoch)
76    pub created_timestamp: u64,
77    /// Algorithm name and configuration hash
78    pub algorithm_signature: String,
79    /// Performance metrics during training
80    pub training_metrics: TrainingMetrics,
81    /// Data characteristics
82    pub data_characteristics: DataCharacteristics,
83    /// Model integrity hash
84    pub integrity_hash: String,
85    /// Platform information
86    pub platform_info: PlatformInfo,
87}
88
89/// Training performance metrics
90#[derive(Serialize, Deserialize, Debug, Clone)]
91pub struct TrainingMetrics {
92    /// Total training time in milliseconds
93    pub training_time_ms: u64,
94    /// Number of iterations/epochs
95    pub iterations: usize,
96    /// Final convergence metric (e.g., inertia, log-likelihood)
97    pub final_convergence_metric: f64,
98    /// Peak memory usage in bytes
99    pub peak_memory_bytes: usize,
100    /// CPU utilization during training (0.0 to 100.0)
101    pub avg_cpu_utilization: f64,
102}
103
104/// Data characteristics for validation
105#[derive(Serialize, Deserialize, Debug, Clone)]
106pub struct DataCharacteristics {
107    /// Number of samples in training data
108    pub n_samples: usize,
109    /// Number of features
110    pub n_features: usize,
111    /// Data type fingerprint
112    pub data_type_fingerprint: String,
113    /// Feature range summaries (min, max for each feature)
114    pub feature_ranges: Option<Vec<(f64, f64)>>,
115    /// Whether data was normalized/standardized
116    pub preprocessing_applied: Vec<String>,
117}
118
119/// Platform information for cross-platform compatibility
120#[derive(Serialize, Deserialize, Debug, Clone)]
121pub struct PlatformInfo {
122    /// Operating system
123    pub os: String,
124    /// Architecture (x86_64, aarch64, etc.)
125    pub arch: String,
126    /// Rust compiler version
127    pub rust_version: String,
128    /// CPU features used (SIMD, etc.)
129    pub cpu_features: Vec<String>,
130}
131
132impl Default for EnhancedModelMetadata {
133    fn default() -> Self {
134        Self {
135            format_version: "1.0.0".to_string(),
136            library_version: env!("CARGO_PKG_VERSION").to_string(),
137            created_timestamp: SystemTime::now()
138                .duration_since(UNIX_EPOCH)
139                .unwrap_or_default()
140                .as_secs(),
141            algorithm_signature: "unknown".to_string(),
142            training_metrics: TrainingMetrics::default(),
143            data_characteristics: DataCharacteristics::default(),
144            integrity_hash: String::new(),
145            platform_info: PlatformInfo::detect(),
146        }
147    }
148}
149
150impl Default for TrainingMetrics {
151    fn default() -> Self {
152        Self {
153            training_time_ms: 0,
154            iterations: 0,
155            final_convergence_metric: 0.0,
156            peak_memory_bytes: 0,
157            avg_cpu_utilization: 0.0,
158        }
159    }
160}
161
162impl Default for DataCharacteristics {
163    fn default() -> Self {
164        Self {
165            n_samples: 0,
166            n_features: 0,
167            data_type_fingerprint: "unknown".to_string(),
168            feature_ranges: None,
169            preprocessing_applied: Vec::new(),
170        }
171    }
172}
173
174impl PlatformInfo {
175    /// Detect current platform information
176    pub fn detect() -> Self {
177        Self {
178            os: std::env::consts::OS.to_string(),
179            arch: std::env::consts::ARCH.to_string(),
180            rust_version: option_env!("CARGO_PKG_RUST_VERSION")
181                .filter(|s| !s.is_empty())
182                .unwrap_or("unknown")
183                .to_string(),
184            cpu_features: Self::detect_cpu_features(),
185        }
186    }
187
188    /// Detect available CPU features
189    fn detect_cpu_features() -> Vec<String> {
190        let mut features = Vec::new();
191
192        #[cfg(target_arch = "x86_64")]
193        {
194            if std::arch::is_x86_feature_detected!("avx2") {
195                features.push("avx2".to_string());
196            }
197            if std::arch::is_x86_feature_detected!("sse4.1") {
198                features.push("sse4.1".to_string());
199            }
200            if std::arch::is_x86_feature_detected!("fma") {
201                features.push("fma".to_string());
202            }
203        }
204
205        #[cfg(target_arch = "aarch64")]
206        {
207            if std::arch::is_aarch64_feature_detected!("neon") {
208                features.push("neon".to_string());
209            }
210        }
211
212        features
213    }
214}
215
216/// Enhanced model wrapper with metadata
217#[derive(Serialize, Debug, Clone)]
218pub struct EnhancedModel<T: SerializableModel> {
219    /// The actual model data
220    pub model: T,
221    /// Enhanced metadata
222    pub metadata: EnhancedModelMetadata,
223}
224
225impl<'de, T: SerializableModel> Deserialize<'de> for EnhancedModel<T> {
226    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
227    where
228        D: serde::Deserializer<'de>,
229    {
230        #[derive(Deserialize)]
231        struct EnhancedModelHelper<U> {
232            model: U,
233            metadata: EnhancedModelMetadata,
234        }
235
236        let helper = EnhancedModelHelper::deserialize(deserializer)?;
237        Ok(EnhancedModel {
238            model: helper.model,
239            metadata: helper.metadata,
240        })
241    }
242}
243
244impl<T: SerializableModel> EnhancedModel<T> {
245    /// Create a new enhanced model with metadata
246    pub fn new(model: T, metadata: EnhancedModelMetadata) -> Self {
247        Self { model, metadata }
248    }
249
250    /// Create an enhanced model with automatic metadata generation
251    pub fn with_auto_metadata(model: T, algorithm_name: &str) -> Self {
252        let mut metadata = EnhancedModelMetadata::default();
253        metadata.algorithm_signature = algorithm_name.to_string();
254        Self { model, metadata }
255    }
256
257    /// Validate model integrity
258    pub fn validate_integrity(&self) -> Result<bool> {
259        // Simple validation - in practice this would check the hash
260        Ok(!self.metadata.integrity_hash.is_empty())
261    }
262
263    /// Get model format version
264    pub fn format_version(&self) -> &str {
265        &self.metadata.format_version
266    }
267
268    /// Check if model is compatible with current library version
269    pub fn is_compatible(&self) -> bool {
270        // Simple compatibility check based on major version
271        let model_version = &self.metadata.library_version;
272        let current_version = env!("CARGO_PKG_VERSION");
273
274        let model_major = model_version.split('.').next().unwrap_or("0");
275        let current_major = current_version.split('.').next().unwrap_or("0");
276
277        model_major == current_major
278    }
279
280    /// Get training duration in seconds
281    pub fn training_duration_seconds(&self) -> f64 {
282        self.metadata.training_metrics.training_time_ms as f64 / 1000.0
283    }
284
285    /// Get memory usage in MB
286    pub fn peak_memory_mb(&self) -> f64 {
287        self.metadata.training_metrics.peak_memory_bytes as f64 / (1024.0 * 1024.0)
288    }
289}
290
291impl<T: SerializableModel> SerializableModel for EnhancedModel<T> {}
292
293/// Format a timestamp for display
294pub fn format_timestamp(timestamp: u64) -> String {
295    match SystemTime::UNIX_EPOCH.checked_add(std::time::Duration::from_secs(timestamp)) {
296        Some(_datetime) => {
297            // Simple conversion: Unix timestamp to year
298            // 1640995200 is 2022-01-01 00:00:00 UTC
299            let years_since_1970 = timestamp / (365 * 24 * 3600); // Approximate
300            let year = 1970 + years_since_1970;
301            format!("Timestamp: {} (approx year {})", timestamp, year)
302        }
303        None => "Invalid timestamp".to_string(),
304    }
305}
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310
311    #[derive(Serialize, Deserialize, Debug, Clone)]
312    struct TestModel {
313        value: i32,
314    }
315
316    impl SerializableModel for TestModel {}
317
318    #[test]
319    fn test_enhanced_model_creation() {
320        let model = TestModel { value: 42 };
321        let enhanced = EnhancedModel::with_auto_metadata(model, "test_algorithm");
322
323        assert_eq!(enhanced.metadata.algorithm_signature, "test_algorithm");
324        assert_eq!(enhanced.model.value, 42);
325    }
326
327    #[test]
328    fn test_platform_info_detection() {
329        let platform = PlatformInfo::detect();
330        assert!(!platform.os.is_empty());
331        assert!(!platform.arch.is_empty());
332    }
333
334    #[test]
335    fn test_format_timestamp() {
336        let timestamp = 1640995200; // 2022-01-01 00:00:00 UTC
337        let formatted = format_timestamp(timestamp);
338        assert!(formatted.contains("2022"));
339    }
340}