Skip to main content

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)]
186#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
187mod tests {
188    use super::*;
189    use presentar_yaml::formats::{DType, ModelLayer, Tensor};
190
191    #[test]
192    fn test_apr_to_model_card() {
193        let mut model = AprModel::new("LinearRegression");
194        model.layers.push(ModelLayer {
195            layer_type: "dense".to_string(),
196            parameters: vec![Tensor::from_f32("weights", vec![10, 5], &[0.0; 50])],
197        });
198        model
199            .metadata
200            .insert("accuracy".to_string(), "0.95".to_string());
201        model
202            .metadata
203            .insert("task".to_string(), "classification".to_string());
204
205        let card = model.to_model_card();
206
207        assert_eq!(card.get_name(), "LinearRegression");
208        assert_eq!(card.get_framework(), Some("Aprender"));
209        assert_eq!(card.get_parameters(), Some(50));
210        assert!(card.get_tags().contains(&"apr".to_string()));
211    }
212
213    #[test]
214    fn test_ald_to_data_card() {
215        let mut dataset = AldDataset::new();
216        dataset.add_tensor(Tensor::from_f32("features", vec![100, 10], &[0.0; 1000]));
217        dataset.add_tensor(Tensor::from_f32("labels", vec![100], &[0.0; 100]));
218
219        let card = dataset.to_data_card("mnist_sample");
220
221        assert_eq!(card.get_name(), "mnist_sample");
222        assert_eq!(card.column_count(), 2);
223        assert!(card.get_tags().contains(&"ald".to_string()));
224    }
225
226    #[test]
227    fn test_format_bytes() {
228        assert_eq!(format_bytes(500), "500 B");
229        assert_eq!(format_bytes(1024), "1.0 KB");
230        assert_eq!(format_bytes(1536), "1.5 KB");
231        assert_eq!(format_bytes(1_048_576), "1.0 MB");
232        assert_eq!(format_bytes(1_073_741_824), "1.0 GB");
233    }
234
235    #[test]
236    fn test_load_apr_roundtrip() {
237        let mut model = AprModel::new("MLP");
238        model.layers.push(ModelLayer {
239            layer_type: "dense".to_string(),
240            parameters: vec![Tensor::from_f32("w", vec![4, 4], &[1.0; 16])],
241        });
242
243        let bytes = model.save();
244        let card = load_apr_as_card(&bytes).unwrap();
245
246        assert_eq!(card.get_name(), "MLP");
247    }
248
249    #[test]
250    fn test_load_ald_roundtrip() {
251        let mut dataset = AldDataset::new();
252        dataset.add_tensor(Tensor::from_f32("data", vec![10], &[1.0; 10]));
253
254        let bytes = dataset.save();
255        let card = load_ald_as_card(&bytes, "test_data").unwrap();
256
257        assert_eq!(card.get_name(), "test_data");
258    }
259
260    // =========================================================================
261    // ModelCard Metadata Tests
262    // =========================================================================
263
264    #[test]
265    fn test_apr_model_with_all_metrics() {
266        let mut model = AprModel::new("Classifier");
267        let kernel_data = vec![0.0_f32; 73_728];
268        model.layers.push(ModelLayer {
269            layer_type: "conv2d".to_string(),
270            parameters: vec![Tensor::from_f32(
271                "kernel",
272                vec![3, 3, 64, 128],
273                &kernel_data,
274            )],
275        });
276        model
277            .metadata
278            .insert("accuracy".to_string(), "0.972".to_string());
279        model
280            .metadata
281            .insert("loss".to_string(), "0.083".to_string());
282        model
283            .metadata
284            .insert("f1_score".to_string(), "0.968".to_string());
285
286        let card = model.to_model_card();
287
288        assert_eq!(card.get_name(), "Classifier");
289        assert_eq!(card.get_parameters(), Some(73728));
290    }
291
292    #[test]
293    fn test_apr_model_with_task_and_dataset() {
294        let mut model = AprModel::new("BERT");
295        model
296            .metadata
297            .insert("task".to_string(), "text-classification".to_string());
298        model
299            .metadata
300            .insert("dataset".to_string(), "IMDB".to_string());
301        model
302            .metadata
303            .insert("author".to_string(), "research-team".to_string());
304
305        let card = model.to_model_card();
306
307        assert_eq!(card.get_task(), Some("text-classification"));
308        assert_eq!(card.get_dataset(), Some("IMDB"));
309        assert_eq!(card.get_author(), Some("research-team"));
310    }
311
312    #[test]
313    fn test_apr_model_with_custom_metadata() {
314        let mut model = AprModel::new("CustomModel");
315        model
316            .metadata
317            .insert("training_time".to_string(), "2h30m".to_string());
318        model
319            .metadata
320            .insert("epochs".to_string(), "50".to_string());
321        model
322            .metadata
323            .insert("learning_rate".to_string(), "0.001".to_string());
324
325        let card = model.to_model_card();
326
327        // Custom metadata is added as metadata entries
328        assert_eq!(card.get_name(), "CustomModel");
329    }
330
331    #[test]
332    fn test_apr_model_multiple_layers() {
333        let mut model = AprModel::new("DeepNet");
334        let w1_data = vec![0.0_f32; 200_704];
335        model.layers.push(ModelLayer {
336            layer_type: "dense".to_string(),
337            parameters: vec![
338                Tensor::from_f32("w1", vec![784, 256], &w1_data),
339                Tensor::from_f32("b1", vec![256], &[0.0; 256]),
340            ],
341        });
342        model.layers.push(ModelLayer {
343            layer_type: "relu".to_string(),
344            parameters: vec![],
345        });
346        model.layers.push(ModelLayer {
347            layer_type: "dense".to_string(),
348            parameters: vec![
349                Tensor::from_f32("w2", vec![256, 10], &[0.0; 2560]),
350                Tensor::from_f32("b2", vec![10], &[0.0; 10]),
351            ],
352        });
353
354        let card = model.to_model_card();
355
356        // 200704 + 256 + 2560 + 10 = 203530
357        assert_eq!(card.get_parameters(), Some(203_530));
358    }
359
360    #[test]
361    fn test_apr_model_empty_layers() {
362        let model = AprModel::new("EmptyModel");
363
364        let card = model.to_model_card();
365
366        assert_eq!(card.get_name(), "EmptyModel");
367        assert_eq!(card.get_parameters(), Some(0));
368    }
369
370    #[test]
371    fn test_apr_model_invalid_metric_values() {
372        let mut model = AprModel::new("BadMetrics");
373        model
374            .metadata
375            .insert("accuracy".to_string(), "not-a-number".to_string());
376        model
377            .metadata
378            .insert("loss".to_string(), "invalid".to_string());
379
380        // Should not panic, just skip invalid metrics
381        let card = model.to_model_card();
382        assert_eq!(card.get_name(), "BadMetrics");
383    }
384
385    // =========================================================================
386    // DataCard Tests
387    // =========================================================================
388
389    #[test]
390    fn test_ald_empty_dataset() {
391        let dataset = AldDataset::new();
392        let card = dataset.to_data_card("empty");
393
394        assert_eq!(card.get_name(), "empty");
395        assert_eq!(card.column_count(), 0);
396    }
397
398    #[test]
399    fn test_ald_multiple_tensors() {
400        let mut dataset = AldDataset::new();
401        let train_x = vec![0.0_f32; 784_000];
402        let test_x = vec![0.0_f32; 78_400];
403        dataset.add_tensor(Tensor::from_f32("train_x", vec![1000, 784], &train_x));
404        dataset.add_tensor(Tensor::from_f32("train_y", vec![1000], &[0.0; 1000]));
405        dataset.add_tensor(Tensor::from_f32("test_x", vec![100, 784], &test_x));
406        dataset.add_tensor(Tensor::from_f32("test_y", vec![100], &[0.0; 100]));
407
408        let card = dataset.to_data_card("mnist");
409
410        assert_eq!(card.get_name(), "mnist");
411        assert_eq!(card.column_count(), 4);
412    }
413
414    #[test]
415    fn test_ald_different_dtypes() {
416        let mut dataset = AldDataset::new();
417
418        // Test float32 dtype (only one available via from_f32)
419        dataset.add_tensor(Tensor::from_f32("float32_tensor", vec![10], &[0.0; 10]));
420        dataset.add_tensor(Tensor::from_f32("float32_tensor2", vec![5, 2], &[0.0; 10]));
421
422        // Use Tensor::new for other dtypes
423        dataset.add_tensor(Tensor::new(
424            "float64_tensor",
425            DType::F64,
426            vec![10],
427            vec![0u8; 80],
428        ));
429        dataset.add_tensor(Tensor::new(
430            "int32_tensor",
431            DType::I32,
432            vec![10],
433            vec![0u8; 40],
434        ));
435        dataset.add_tensor(Tensor::new(
436            "uint8_tensor",
437            DType::U8,
438            vec![10],
439            vec![0u8; 10],
440        ));
441
442        let card = dataset.to_data_card("multi_dtype");
443
444        assert_eq!(card.column_count(), 5);
445    }
446
447    // =========================================================================
448    // format_bytes Edge Cases
449    // =========================================================================
450
451    #[test]
452    fn test_format_bytes_boundaries() {
453        // Exact boundaries
454        assert_eq!(format_bytes(0), "0 B");
455        assert_eq!(format_bytes(1023), "1023 B");
456        assert_eq!(format_bytes(1024), "1.0 KB");
457        assert_eq!(format_bytes(1024 * 1024 - 1), "1024.0 KB");
458        assert_eq!(format_bytes(1024 * 1024), "1.0 MB");
459        assert_eq!(format_bytes(1024 * 1024 * 1024 - 1), "1024.0 MB");
460        assert_eq!(format_bytes(1024 * 1024 * 1024), "1.0 GB");
461    }
462
463    #[test]
464    fn test_format_bytes_large_values() {
465        // 10 GB
466        assert_eq!(format_bytes(10 * 1024 * 1024 * 1024), "10.0 GB");
467        // 100 GB
468        assert_eq!(format_bytes(100 * 1024 * 1024 * 1024), "100.0 GB");
469    }
470
471    #[test]
472    fn test_format_bytes_fractional() {
473        // 1.5 KB = 1536 bytes
474        assert_eq!(format_bytes(1536), "1.5 KB");
475        // 2.5 MB
476        assert_eq!(format_bytes(2_621_440), "2.5 MB");
477        // 3.25 GB
478        assert_eq!(format_bytes(3_489_660_928), "3.2 GB");
479    }
480
481    // =========================================================================
482    // Error Handling Tests
483    // =========================================================================
484
485    #[test]
486    fn test_load_apr_invalid_data() {
487        let invalid_data = b"not a valid apr file";
488        let result = load_apr_as_card(invalid_data);
489        assert!(result.is_err());
490    }
491
492    #[test]
493    fn test_load_ald_invalid_data() {
494        let invalid_data = b"not a valid ald file";
495        let result = load_ald_as_card(invalid_data, "test");
496        assert!(result.is_err());
497    }
498
499    #[test]
500    fn test_load_apr_empty_data() {
501        let empty_data = b"";
502        let result = load_apr_as_card(empty_data);
503        assert!(result.is_err());
504    }
505
506    #[test]
507    fn test_load_ald_empty_data() {
508        let empty_data = b"";
509        let result = load_ald_as_card(empty_data, "test");
510        assert!(result.is_err());
511    }
512
513    // =========================================================================
514    // Tags Test
515    // =========================================================================
516
517    #[test]
518    fn test_model_card_has_sovereign_ai_tag() {
519        let model = AprModel::new("SovereignModel");
520        let card = model.to_model_card();
521
522        assert!(card.get_tags().contains(&"sovereign-ai".to_string()));
523        assert!(card.get_tags().contains(&"apr".to_string()));
524    }
525
526    #[test]
527    fn test_data_card_has_sovereign_ai_tag() {
528        let dataset = AldDataset::new();
529        let card = dataset.to_data_card("SovereignData");
530
531        assert!(card.get_tags().contains(&"sovereign-ai".to_string()));
532        assert!(card.get_tags().contains(&"ald".to_string()));
533    }
534}