presentar_widgets/
formats.rs

1//! Conversion from `.apr`/`.ald` file formats to display widgets.
2//!
3//! Bridges `presentar-yaml` format loaders to `ModelCard` and `DataCard` widgets.
4#![allow(clippy::doc_markdown)]
5#![allow(clippy::uninlined_format_args)]
6#![allow(clippy::missing_panics_doc)]
7#![allow(clippy::missing_errors_doc)]
8#![allow(clippy::redundant_closure_for_method_calls)]
9
10use crate::data_card::{DataCard, DataColumn};
11use crate::model_card::{ModelCard, ModelMetric, ModelStatus};
12use presentar_yaml::formats::{AldDataset, AprModel, DType};
13
14/// Extension trait for converting `AprModel` to `ModelCard`.
15pub trait AprModelExt {
16    /// Convert to a display-ready `ModelCard` widget.
17    fn to_model_card(&self) -> ModelCard;
18}
19
20impl AprModelExt for AprModel {
21    fn to_model_card(&self) -> ModelCard {
22        // Count total parameters
23        let total_params: u64 = self
24            .layers
25            .iter()
26            .flat_map(|l| l.parameters.iter())
27            .map(|t| t.numel() as u64)
28            .sum();
29
30        // Build layer summary for description
31        let layer_summary: String = self
32            .layers
33            .iter()
34            .map(|l| format!("{}({})", l.layer_type, l.parameters.len()))
35            .collect::<Vec<_>>()
36            .join(" → ");
37
38        let mut card = ModelCard::new(&self.model_type)
39            .version(format!("v{}", self.version))
40            .framework("Aprender")
41            .parameters(total_params)
42            .status(ModelStatus::Published);
43
44        if !layer_summary.is_empty() {
45            card = card.description(format!("Architecture: {}", layer_summary));
46        }
47
48        // Add metrics from metadata
49        if let Some(acc) = self.metadata.get("accuracy") {
50            if let Ok(v) = acc.parse::<f64>() {
51                card = card.metric(ModelMetric::new("accuracy", v));
52            }
53        }
54        if let Some(loss) = self.metadata.get("loss") {
55            if let Ok(v) = loss.parse::<f64>() {
56                card = card.metric(ModelMetric::new("loss", v).lower_is_better());
57            }
58        }
59        if let Some(f1) = self.metadata.get("f1_score") {
60            if let Ok(v) = f1.parse::<f64>() {
61                card = card.metric(ModelMetric::new("F1", v));
62            }
63        }
64
65        // Add task if in metadata
66        if let Some(task) = self.metadata.get("task") {
67            card = card.task(task);
68        }
69
70        // Add dataset if in metadata
71        if let Some(dataset) = self.metadata.get("dataset") {
72            card = card.dataset(dataset);
73        }
74
75        // Add author if in metadata
76        if let Some(author) = self.metadata.get("author") {
77            card = card.author(author);
78        }
79
80        // Add remaining metadata
81        for (k, v) in &self.metadata {
82            if !["accuracy", "loss", "f1_score", "task", "dataset", "author"].contains(&k.as_str())
83            {
84                card = card.metadata_entry(k, v);
85            }
86        }
87
88        card.tag("apr").tag("sovereign-ai")
89    }
90}
91
92/// Extension trait for converting `AldDataset` to `DataCard`.
93pub trait AldDatasetExt {
94    /// Convert to a display-ready `DataCard` widget.
95    fn to_data_card(&self, name: &str) -> DataCard;
96}
97
98impl AldDatasetExt for AldDataset {
99    fn to_data_card(&self, name: &str) -> DataCard {
100        // Calculate total size
101        let total_bytes: usize = self.tensors.iter().map(|t| t.data.len()).sum();
102
103        // Count total elements
104        let total_elements: usize = self.tensors.iter().map(|t| t.numel()).sum();
105
106        // Build columns from tensors
107        let columns: Vec<DataColumn> = self
108            .tensors
109            .iter()
110            .map(|t| {
111                let dtype_str = match t.dtype {
112                    DType::F32 => "float32",
113                    DType::F64 => "float64",
114                    DType::I32 => "int32",
115                    DType::I64 => "int64",
116                    DType::U8 => "uint8",
117                };
118                let shape_str = t
119                    .shape
120                    .iter()
121                    .map(|d| d.to_string())
122                    .collect::<Vec<_>>()
123                    .join("×");
124
125                DataColumn::new(&t.name, dtype_str).description(format!("Shape: [{}]", shape_str))
126            })
127            .collect();
128
129        let mut card = DataCard::new(name)
130            .description(format!(
131                "{} elements, {} tensors, {}",
132                total_elements,
133                self.tensors.len(),
134                format_bytes(total_bytes)
135            ))
136            .source("Alimentar (.ald)")
137            .tag("ald")
138            .tag("sovereign-ai");
139
140        for col in columns {
141            card = card.column(col);
142        }
143
144        card
145    }
146}
147
148/// Format bytes as human-readable string.
149fn format_bytes(bytes: usize) -> String {
150    const KB: usize = 1024;
151    const MB: usize = KB * 1024;
152    const GB: usize = MB * 1024;
153
154    if bytes >= GB {
155        format!("{:.1} GB", bytes as f64 / GB as f64)
156    } else if bytes >= MB {
157        format!("{:.1} MB", bytes as f64 / MB as f64)
158    } else if bytes >= KB {
159        format!("{:.1} KB", bytes as f64 / KB as f64)
160    } else {
161        format!("{} B", bytes)
162    }
163}
164
165/// Load an .apr file and convert to ModelCard.
166///
167/// # Errors
168///
169/// Returns error if file cannot be loaded.
170pub fn load_apr_as_card(data: &[u8]) -> Result<ModelCard, presentar_yaml::FormatError> {
171    let model = AprModel::load(data)?;
172    Ok(model.to_model_card())
173}
174
175/// Load an .ald file and convert to DataCard.
176///
177/// # Errors
178///
179/// Returns error if file cannot be loaded.
180pub fn load_ald_as_card(data: &[u8], name: &str) -> Result<DataCard, presentar_yaml::FormatError> {
181    let dataset = AldDataset::load(data)?;
182    Ok(dataset.to_data_card(name))
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188    use presentar_yaml::formats::{DType, ModelLayer, Tensor};
189
190    #[test]
191    fn test_apr_to_model_card() {
192        let mut model = AprModel::new("LinearRegression");
193        model.layers.push(ModelLayer {
194            layer_type: "dense".to_string(),
195            parameters: vec![Tensor::from_f32("weights", vec![10, 5], &[0.0; 50])],
196        });
197        model
198            .metadata
199            .insert("accuracy".to_string(), "0.95".to_string());
200        model
201            .metadata
202            .insert("task".to_string(), "classification".to_string());
203
204        let card = model.to_model_card();
205
206        assert_eq!(card.get_name(), "LinearRegression");
207        assert_eq!(card.get_framework(), Some("Aprender"));
208        assert_eq!(card.get_parameters(), Some(50));
209        assert!(card.get_tags().contains(&"apr".to_string()));
210    }
211
212    #[test]
213    fn test_ald_to_data_card() {
214        let mut dataset = AldDataset::new();
215        dataset.add_tensor(Tensor::from_f32("features", vec![100, 10], &[0.0; 1000]));
216        dataset.add_tensor(Tensor::from_f32("labels", vec![100], &[0.0; 100]));
217
218        let card = dataset.to_data_card("mnist_sample");
219
220        assert_eq!(card.get_name(), "mnist_sample");
221        assert_eq!(card.column_count(), 2);
222        assert!(card.get_tags().contains(&"ald".to_string()));
223    }
224
225    #[test]
226    fn test_format_bytes() {
227        assert_eq!(format_bytes(500), "500 B");
228        assert_eq!(format_bytes(1024), "1.0 KB");
229        assert_eq!(format_bytes(1536), "1.5 KB");
230        assert_eq!(format_bytes(1048576), "1.0 MB");
231        assert_eq!(format_bytes(1073741824), "1.0 GB");
232    }
233
234    #[test]
235    fn test_load_apr_roundtrip() {
236        let mut model = AprModel::new("MLP");
237        model.layers.push(ModelLayer {
238            layer_type: "dense".to_string(),
239            parameters: vec![Tensor::from_f32("w", vec![4, 4], &[1.0; 16])],
240        });
241
242        let bytes = model.save();
243        let card = load_apr_as_card(&bytes).unwrap();
244
245        assert_eq!(card.get_name(), "MLP");
246    }
247
248    #[test]
249    fn test_load_ald_roundtrip() {
250        let mut dataset = AldDataset::new();
251        dataset.add_tensor(Tensor::from_f32("data", vec![10], &[1.0; 10]));
252
253        let bytes = dataset.save();
254        let card = load_ald_as_card(&bytes, "test_data").unwrap();
255
256        assert_eq!(card.get_name(), "test_data");
257    }
258
259    // =========================================================================
260    // ModelCard Metadata Tests
261    // =========================================================================
262
263    #[test]
264    fn test_apr_model_with_all_metrics() {
265        let mut model = AprModel::new("Classifier");
266        model.layers.push(ModelLayer {
267            layer_type: "conv2d".to_string(),
268            parameters: vec![Tensor::from_f32(
269                "kernel",
270                vec![3, 3, 64, 128],
271                &[0.0; 73728],
272            )],
273        });
274        model
275            .metadata
276            .insert("accuracy".to_string(), "0.972".to_string());
277        model
278            .metadata
279            .insert("loss".to_string(), "0.083".to_string());
280        model
281            .metadata
282            .insert("f1_score".to_string(), "0.968".to_string());
283
284        let card = model.to_model_card();
285
286        assert_eq!(card.get_name(), "Classifier");
287        assert_eq!(card.get_parameters(), Some(73728));
288    }
289
290    #[test]
291    fn test_apr_model_with_task_and_dataset() {
292        let mut model = AprModel::new("BERT");
293        model
294            .metadata
295            .insert("task".to_string(), "text-classification".to_string());
296        model
297            .metadata
298            .insert("dataset".to_string(), "IMDB".to_string());
299        model
300            .metadata
301            .insert("author".to_string(), "research-team".to_string());
302
303        let card = model.to_model_card();
304
305        assert_eq!(card.get_task(), Some("text-classification"));
306        assert_eq!(card.get_dataset(), Some("IMDB"));
307        assert_eq!(card.get_author(), Some("research-team"));
308    }
309
310    #[test]
311    fn test_apr_model_with_custom_metadata() {
312        let mut model = AprModel::new("CustomModel");
313        model
314            .metadata
315            .insert("training_time".to_string(), "2h30m".to_string());
316        model
317            .metadata
318            .insert("epochs".to_string(), "50".to_string());
319        model
320            .metadata
321            .insert("learning_rate".to_string(), "0.001".to_string());
322
323        let card = model.to_model_card();
324
325        // Custom metadata is added as metadata entries
326        assert_eq!(card.get_name(), "CustomModel");
327    }
328
329    #[test]
330    fn test_apr_model_multiple_layers() {
331        let mut model = AprModel::new("DeepNet");
332        model.layers.push(ModelLayer {
333            layer_type: "dense".to_string(),
334            parameters: vec![
335                Tensor::from_f32("w1", vec![784, 256], &[0.0; 200704]),
336                Tensor::from_f32("b1", vec![256], &[0.0; 256]),
337            ],
338        });
339        model.layers.push(ModelLayer {
340            layer_type: "relu".to_string(),
341            parameters: vec![],
342        });
343        model.layers.push(ModelLayer {
344            layer_type: "dense".to_string(),
345            parameters: vec![
346                Tensor::from_f32("w2", vec![256, 10], &[0.0; 2560]),
347                Tensor::from_f32("b2", vec![10], &[0.0; 10]),
348            ],
349        });
350
351        let card = model.to_model_card();
352
353        // 200704 + 256 + 2560 + 10 = 203530
354        assert_eq!(card.get_parameters(), Some(203530));
355    }
356
357    #[test]
358    fn test_apr_model_empty_layers() {
359        let model = AprModel::new("EmptyModel");
360
361        let card = model.to_model_card();
362
363        assert_eq!(card.get_name(), "EmptyModel");
364        assert_eq!(card.get_parameters(), Some(0));
365    }
366
367    #[test]
368    fn test_apr_model_invalid_metric_values() {
369        let mut model = AprModel::new("BadMetrics");
370        model
371            .metadata
372            .insert("accuracy".to_string(), "not-a-number".to_string());
373        model
374            .metadata
375            .insert("loss".to_string(), "invalid".to_string());
376
377        // Should not panic, just skip invalid metrics
378        let card = model.to_model_card();
379        assert_eq!(card.get_name(), "BadMetrics");
380    }
381
382    // =========================================================================
383    // DataCard Tests
384    // =========================================================================
385
386    #[test]
387    fn test_ald_empty_dataset() {
388        let dataset = AldDataset::new();
389        let card = dataset.to_data_card("empty");
390
391        assert_eq!(card.get_name(), "empty");
392        assert_eq!(card.column_count(), 0);
393    }
394
395    #[test]
396    fn test_ald_multiple_tensors() {
397        let mut dataset = AldDataset::new();
398        dataset.add_tensor(Tensor::from_f32("train_x", vec![1000, 784], &[0.0; 784000]));
399        dataset.add_tensor(Tensor::from_f32("train_y", vec![1000], &[0.0; 1000]));
400        dataset.add_tensor(Tensor::from_f32("test_x", vec![100, 784], &[0.0; 78400]));
401        dataset.add_tensor(Tensor::from_f32("test_y", vec![100], &[0.0; 100]));
402
403        let card = dataset.to_data_card("mnist");
404
405        assert_eq!(card.get_name(), "mnist");
406        assert_eq!(card.column_count(), 4);
407    }
408
409    #[test]
410    fn test_ald_different_dtypes() {
411        let mut dataset = AldDataset::new();
412
413        // Test float32 dtype (only one available via from_f32)
414        dataset.add_tensor(Tensor::from_f32("float32_tensor", vec![10], &[0.0; 10]));
415        dataset.add_tensor(Tensor::from_f32("float32_tensor2", vec![5, 2], &[0.0; 10]));
416
417        // Use Tensor::new for other dtypes
418        dataset.add_tensor(Tensor::new(
419            "float64_tensor",
420            DType::F64,
421            vec![10],
422            vec![0u8; 80],
423        ));
424        dataset.add_tensor(Tensor::new(
425            "int32_tensor",
426            DType::I32,
427            vec![10],
428            vec![0u8; 40],
429        ));
430        dataset.add_tensor(Tensor::new(
431            "uint8_tensor",
432            DType::U8,
433            vec![10],
434            vec![0u8; 10],
435        ));
436
437        let card = dataset.to_data_card("multi_dtype");
438
439        assert_eq!(card.column_count(), 5);
440    }
441
442    // =========================================================================
443    // format_bytes Edge Cases
444    // =========================================================================
445
446    #[test]
447    fn test_format_bytes_boundaries() {
448        // Exact boundaries
449        assert_eq!(format_bytes(0), "0 B");
450        assert_eq!(format_bytes(1023), "1023 B");
451        assert_eq!(format_bytes(1024), "1.0 KB");
452        assert_eq!(format_bytes(1024 * 1024 - 1), "1024.0 KB");
453        assert_eq!(format_bytes(1024 * 1024), "1.0 MB");
454        assert_eq!(format_bytes(1024 * 1024 * 1024 - 1), "1024.0 MB");
455        assert_eq!(format_bytes(1024 * 1024 * 1024), "1.0 GB");
456    }
457
458    #[test]
459    fn test_format_bytes_large_values() {
460        // 10 GB
461        assert_eq!(format_bytes(10 * 1024 * 1024 * 1024), "10.0 GB");
462        // 100 GB
463        assert_eq!(format_bytes(100 * 1024 * 1024 * 1024), "100.0 GB");
464    }
465
466    #[test]
467    fn test_format_bytes_fractional() {
468        // 1.5 KB = 1536 bytes
469        assert_eq!(format_bytes(1536), "1.5 KB");
470        // 2.5 MB
471        assert_eq!(format_bytes(2621440), "2.5 MB");
472        // 3.25 GB
473        assert_eq!(format_bytes(3489660928), "3.2 GB");
474    }
475
476    // =========================================================================
477    // Error Handling Tests
478    // =========================================================================
479
480    #[test]
481    fn test_load_apr_invalid_data() {
482        let invalid_data = b"not a valid apr file";
483        let result = load_apr_as_card(invalid_data);
484        assert!(result.is_err());
485    }
486
487    #[test]
488    fn test_load_ald_invalid_data() {
489        let invalid_data = b"not a valid ald file";
490        let result = load_ald_as_card(invalid_data, "test");
491        assert!(result.is_err());
492    }
493
494    #[test]
495    fn test_load_apr_empty_data() {
496        let empty_data = b"";
497        let result = load_apr_as_card(empty_data);
498        assert!(result.is_err());
499    }
500
501    #[test]
502    fn test_load_ald_empty_data() {
503        let empty_data = b"";
504        let result = load_ald_as_card(empty_data, "test");
505        assert!(result.is_err());
506    }
507
508    // =========================================================================
509    // Tags Test
510    // =========================================================================
511
512    #[test]
513    fn test_model_card_has_sovereign_ai_tag() {
514        let model = AprModel::new("SovereignModel");
515        let card = model.to_model_card();
516
517        assert!(card.get_tags().contains(&"sovereign-ai".to_string()));
518        assert!(card.get_tags().contains(&"apr".to_string()));
519    }
520
521    #[test]
522    fn test_data_card_has_sovereign_ai_tag() {
523        let dataset = AldDataset::new();
524        let card = dataset.to_data_card("SovereignData");
525
526        assert!(card.get_tags().contains(&"sovereign-ai".to_string()));
527        assert!(card.get_tags().contains(&"ald".to_string()));
528    }
529}