Skip to main content

ruv_neural_core/
lib.rs

1//! # ruv-neural-core
2//!
3//! Core types, traits, and error types for the ruv-neural brain topology
4//! analysis system.
5//!
6//! This crate is the foundation of the ruv-neural workspace. It has **zero**
7//! internal dependencies — all other ruv-neural crates depend on this one.
8//!
9//! ## Modules
10//!
11//! | Module      | Contents                                          |
12//! |-------------|---------------------------------------------------|
13//! | `error`     | `RuvNeuralError` enum, `Result<T>` alias           |
14//! | `sensor`    | `SensorType`, `SensorChannel`, `SensorArray`       |
15//! | `signal`    | `MultiChannelTimeSeries`, `FrequencyBand`, spectra |
16//! | `brain`     | `Atlas`, `BrainRegion`, `Parcellation`             |
17//! | `graph`     | `BrainGraph`, `BrainEdge`, `ConnectivityMetric`    |
18//! | `topology`  | `MincutResult`, `CognitiveState`, `TopologyMetrics`|
19//! | `embedding` | `NeuralEmbedding`, `EmbeddingTrajectory`           |
20//! | `rvf`       | RuVector File format header and I/O                |
21//! | `traits`    | Pipeline trait definitions for all crates          |
22
23pub mod brain;
24pub mod embedding;
25pub mod error;
26pub mod graph;
27pub mod rvf;
28pub mod sensor;
29pub mod signal;
30pub mod topology;
31pub mod traits;
32pub mod witness;
33
34// Re-export the most commonly used types at crate root.
35pub use brain::{Atlas, BrainRegion, Hemisphere, Lobe, Parcellation};
36pub use embedding::{EmbeddingMetadata, EmbeddingTrajectory, NeuralEmbedding};
37pub use error::{Result, RuvNeuralError};
38pub use graph::{BrainEdge, BrainGraph, BrainGraphSequence, ConnectivityMetric};
39pub use rvf::{RvfDataType, RvfFile, RvfHeader};
40pub use sensor::{SensorArray, SensorChannel, SensorType};
41pub use signal::{FrequencyBand, MultiChannelTimeSeries, SpectralFeatures, TimeFrequencyMap};
42pub use topology::{
43    CognitiveState, MincutResult, MultiPartition, SleepStage, TopologyMetrics,
44};
45pub use traits::{
46    EmbeddingGenerator, GraphConstructor, NeuralMemory, RvfSerializable, SensorSource,
47    SignalProcessor, StateDecoder, TopologyAnalyzer,
48};
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    // ── Error tests ─────────────────────────────────────────────────
55
56    #[test]
57    fn error_display_formatting() {
58        let err = RuvNeuralError::Sensor("calibration failed".into());
59        assert!(err.to_string().contains("Sensor error"));
60        assert!(err.to_string().contains("calibration failed"));
61
62        let err = RuvNeuralError::DimensionMismatch {
63            expected: 68,
64            got: 100,
65        };
66        assert!(err.to_string().contains("68"));
67        assert!(err.to_string().contains("100"));
68
69        let err = RuvNeuralError::ChannelOutOfRange {
70            channel: 5,
71            max: 3,
72        };
73        assert!(err.to_string().contains("5"));
74        assert!(err.to_string().contains("3"));
75
76        let err = RuvNeuralError::InsufficientData {
77            needed: 1000,
78            have: 500,
79        };
80        assert!(err.to_string().contains("1000"));
81        assert!(err.to_string().contains("500"));
82    }
83
84    // ── Sensor tests ────────────────────────────────────────────────
85
86    #[test]
87    fn sensor_type_sensitivity() {
88        assert!(SensorType::SquidMeg.typical_sensitivity_ft_sqrt_hz() < 5.0);
89        assert!(SensorType::Eeg.typical_sensitivity_ft_sqrt_hz() > 100.0);
90    }
91
92    #[test]
93    fn sensor_array_operations() {
94        let array = SensorArray {
95            channels: vec![
96                SensorChannel {
97                    id: 0,
98                    sensor_type: SensorType::Opm,
99                    position: [0.0, 0.0, 0.1],
100                    orientation: [0.0, 0.0, 1.0],
101                    sensitivity_ft_sqrt_hz: 7.0,
102                    sample_rate_hz: 1000.0,
103                    label: "OPM-001".into(),
104                },
105                SensorChannel {
106                    id: 1,
107                    sensor_type: SensorType::Opm,
108                    position: [0.05, 0.0, 0.12],
109                    orientation: [0.0, 0.0, 1.0],
110                    sensitivity_ft_sqrt_hz: 7.0,
111                    sample_rate_hz: 1000.0,
112                    label: "OPM-002".into(),
113                },
114            ],
115            sensor_type: SensorType::Opm,
116            name: "OPM array".into(),
117        };
118
119        assert_eq!(array.num_channels(), 2);
120        assert!(!array.is_empty());
121        assert_eq!(array.get_channel(0).unwrap().label, "OPM-001");
122        assert!(array.get_channel(5).is_none());
123
124        let (min, max) = array.bounding_box().unwrap();
125        assert_eq!(min[0], 0.0);
126        assert_eq!(max[0], 0.05);
127    }
128
129    #[test]
130    fn sensor_serialize_roundtrip() {
131        let ch = SensorChannel {
132            id: 0,
133            sensor_type: SensorType::NvDiamond,
134            position: [1.0, 2.0, 3.0],
135            orientation: [0.0, 0.0, 1.0],
136            sensitivity_ft_sqrt_hz: 10.0,
137            sample_rate_hz: 2000.0,
138            label: "NV-001".into(),
139        };
140        let json = serde_json::to_string(&ch).unwrap();
141        let ch2: SensorChannel = serde_json::from_str(&json).unwrap();
142        assert_eq!(ch2.id, 0);
143        assert_eq!(ch2.sensor_type, SensorType::NvDiamond);
144    }
145
146    // ── Signal tests ────────────────────────────────────────────────
147
148    #[test]
149    fn frequency_band_ranges() {
150        assert_eq!(FrequencyBand::Delta.range_hz(), (1.0, 4.0));
151        assert_eq!(FrequencyBand::Alpha.range_hz(), (8.0, 13.0));
152        assert_eq!(FrequencyBand::Gamma.range_hz(), (30.0, 100.0));
153        assert_eq!(
154            FrequencyBand::Custom {
155                low_hz: 50.0,
156                high_hz: 70.0
157            }
158            .range_hz(),
159            (50.0, 70.0)
160        );
161    }
162
163    #[test]
164    fn frequency_band_center_and_bandwidth() {
165        assert!((FrequencyBand::Alpha.center_hz() - 10.5).abs() < 1e-10);
166        assert!((FrequencyBand::Alpha.bandwidth_hz() - 5.0).abs() < 1e-10);
167    }
168
169    #[test]
170    fn time_series_creation_valid() {
171        let data = vec![vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]];
172        let ts = MultiChannelTimeSeries::new(data, 100.0, 1000.0).unwrap();
173        assert_eq!(ts.num_channels, 2);
174        assert_eq!(ts.num_samples, 3);
175        assert!((ts.duration_s() - 0.03).abs() < 1e-10);
176    }
177
178    #[test]
179    fn time_series_dimension_mismatch() {
180        let data = vec![vec![1.0, 2.0], vec![3.0]];
181        let result = MultiChannelTimeSeries::new(data, 100.0, 0.0);
182        assert!(result.is_err());
183    }
184
185    #[test]
186    fn time_series_channel_access() {
187        let data = vec![vec![10.0, 20.0], vec![30.0, 40.0]];
188        let ts = MultiChannelTimeSeries::new(data, 100.0, 0.0).unwrap();
189        assert_eq!(ts.channel(0).unwrap(), &[10.0, 20.0]);
190        assert!(ts.channel(5).is_err());
191    }
192
193    // ── Brain / Atlas tests ─────────────────────────────────────────
194
195    #[test]
196    fn atlas_region_counts() {
197        assert_eq!(Atlas::DesikanKilliany68.num_regions(), 68);
198        assert_eq!(Atlas::Destrieux148.num_regions(), 148);
199        assert_eq!(Atlas::Schaefer100.num_regions(), 100);
200        assert_eq!(Atlas::Schaefer200.num_regions(), 200);
201        assert_eq!(Atlas::Schaefer400.num_regions(), 400);
202        assert_eq!(Atlas::Custom(42).num_regions(), 42);
203    }
204
205    #[test]
206    fn parcellation_query() {
207        let parcellation = Parcellation {
208            atlas: Atlas::Custom(3),
209            regions: vec![
210                BrainRegion {
211                    id: 0,
212                    name: "left_frontal".into(),
213                    hemisphere: Hemisphere::Left,
214                    lobe: Lobe::Frontal,
215                    centroid: [-30.0, 20.0, 40.0],
216                },
217                BrainRegion {
218                    id: 1,
219                    name: "right_frontal".into(),
220                    hemisphere: Hemisphere::Right,
221                    lobe: Lobe::Frontal,
222                    centroid: [30.0, 20.0, 40.0],
223                },
224                BrainRegion {
225                    id: 2,
226                    name: "left_temporal".into(),
227                    hemisphere: Hemisphere::Left,
228                    lobe: Lobe::Temporal,
229                    centroid: [-50.0, -10.0, 0.0],
230                },
231            ],
232        };
233
234        assert_eq!(parcellation.num_regions(), 3);
235        assert_eq!(
236            parcellation.regions_in_hemisphere(Hemisphere::Left).len(),
237            2
238        );
239        assert_eq!(parcellation.regions_in_lobe(Lobe::Frontal).len(), 2);
240        assert_eq!(parcellation.regions_in_lobe(Lobe::Temporal).len(), 1);
241        assert!(parcellation.get_region(1).is_some());
242        assert!(parcellation.get_region(99).is_none());
243    }
244
245    #[test]
246    fn brain_region_serialize_roundtrip() {
247        let region = BrainRegion {
248            id: 42,
249            name: "postcentral".into(),
250            hemisphere: Hemisphere::Left,
251            lobe: Lobe::Parietal,
252            centroid: [-40.0, -25.0, 55.0],
253        };
254        let json = serde_json::to_string(&region).unwrap();
255        let r2: BrainRegion = serde_json::from_str(&json).unwrap();
256        assert_eq!(r2.id, 42);
257        assert_eq!(r2.hemisphere, Hemisphere::Left);
258    }
259
260    // ── Graph tests ─────────────────────────────────────────────────
261
262    #[test]
263    fn brain_graph_adjacency_matrix() {
264        let graph = BrainGraph {
265            num_nodes: 3,
266            edges: vec![
267                BrainEdge {
268                    source: 0,
269                    target: 1,
270                    weight: 0.8,
271                    metric: ConnectivityMetric::PhaseLockingValue,
272                    frequency_band: FrequencyBand::Alpha,
273                },
274                BrainEdge {
275                    source: 1,
276                    target: 2,
277                    weight: 0.5,
278                    metric: ConnectivityMetric::Coherence,
279                    frequency_band: FrequencyBand::Beta,
280                },
281            ],
282            timestamp: 100.0,
283            window_duration_s: 1.0,
284            atlas: Atlas::Custom(3),
285        };
286
287        let mat = graph.adjacency_matrix();
288        assert_eq!(mat.len(), 3);
289        assert!((mat[0][1] - 0.8).abs() < 1e-10);
290        assert!((mat[1][0] - 0.8).abs() < 1e-10);
291        assert!((mat[1][2] - 0.5).abs() < 1e-10);
292        assert!((mat[0][2] - 0.0).abs() < 1e-10);
293    }
294
295    #[test]
296    fn brain_graph_edge_weight_lookup() {
297        let graph = BrainGraph {
298            num_nodes: 2,
299            edges: vec![BrainEdge {
300                source: 0,
301                target: 1,
302                weight: 0.9,
303                metric: ConnectivityMetric::MutualInformation,
304                frequency_band: FrequencyBand::Gamma,
305            }],
306            timestamp: 0.0,
307            window_duration_s: 0.5,
308            atlas: Atlas::Custom(2),
309        };
310
311        assert!((graph.edge_weight(0, 1).unwrap() - 0.9).abs() < 1e-10);
312        assert!((graph.edge_weight(1, 0).unwrap() - 0.9).abs() < 1e-10);
313        assert!(graph.edge_weight(0, 0).is_none());
314    }
315
316    #[test]
317    fn brain_graph_node_degree() {
318        let graph = BrainGraph {
319            num_nodes: 3,
320            edges: vec![
321                BrainEdge {
322                    source: 0,
323                    target: 1,
324                    weight: 0.3,
325                    metric: ConnectivityMetric::Coherence,
326                    frequency_band: FrequencyBand::Alpha,
327                },
328                BrainEdge {
329                    source: 0,
330                    target: 2,
331                    weight: 0.7,
332                    metric: ConnectivityMetric::Coherence,
333                    frequency_band: FrequencyBand::Alpha,
334                },
335            ],
336            timestamp: 0.0,
337            window_duration_s: 1.0,
338            atlas: Atlas::Custom(3),
339        };
340
341        assert!((graph.node_degree(0) - 1.0).abs() < 1e-10);
342        assert!((graph.node_degree(1) - 0.3).abs() < 1e-10);
343        assert!((graph.node_degree(2) - 0.7).abs() < 1e-10);
344    }
345
346    #[test]
347    fn brain_graph_density() {
348        let graph = BrainGraph {
349            num_nodes: 4,
350            edges: vec![
351                BrainEdge {
352                    source: 0,
353                    target: 1,
354                    weight: 1.0,
355                    metric: ConnectivityMetric::PhaseLockingValue,
356                    frequency_band: FrequencyBand::Alpha,
357                },
358                BrainEdge {
359                    source: 2,
360                    target: 3,
361                    weight: 1.0,
362                    metric: ConnectivityMetric::PhaseLockingValue,
363                    frequency_band: FrequencyBand::Alpha,
364                },
365                BrainEdge {
366                    source: 0,
367                    target: 3,
368                    weight: 1.0,
369                    metric: ConnectivityMetric::PhaseLockingValue,
370                    frequency_band: FrequencyBand::Alpha,
371                },
372            ],
373            timestamp: 0.0,
374            window_duration_s: 1.0,
375            atlas: Atlas::Custom(4),
376        };
377
378        assert!((graph.density() - 0.5).abs() < 1e-10);
379    }
380
381    #[test]
382    fn graph_sequence_duration() {
383        let seq = BrainGraphSequence {
384            graphs: vec![
385                BrainGraph {
386                    num_nodes: 2,
387                    edges: vec![],
388                    timestamp: 0.0,
389                    window_duration_s: 1.0,
390                    atlas: Atlas::Custom(2),
391                },
392                BrainGraph {
393                    num_nodes: 2,
394                    edges: vec![],
395                    timestamp: 0.5,
396                    window_duration_s: 1.0,
397                    atlas: Atlas::Custom(2),
398                },
399                BrainGraph {
400                    num_nodes: 2,
401                    edges: vec![],
402                    timestamp: 1.0,
403                    window_duration_s: 1.0,
404                    atlas: Atlas::Custom(2),
405                },
406            ],
407            window_step_s: 0.5,
408        };
409
410        assert_eq!(seq.len(), 3);
411        assert!(!seq.is_empty());
412        assert!((seq.duration_s() - 2.0).abs() < 1e-10);
413    }
414
415    // ── Topology tests ──────────────────────────────────────────────
416
417    #[test]
418    fn mincut_result_properties() {
419        let result = MincutResult {
420            cut_value: 1.5,
421            partition_a: vec![0, 1],
422            partition_b: vec![2, 3, 4],
423            cut_edges: vec![(1, 2, 0.8), (0, 3, 0.7)],
424            timestamp: 100.0,
425        };
426
427        assert_eq!(result.num_nodes(), 5);
428        assert_eq!(result.num_cut_edges(), 2);
429        assert!((result.balance_ratio() - 2.0 / 3.0).abs() < 1e-10);
430    }
431
432    #[test]
433    fn multi_partition_properties() {
434        let mp = MultiPartition {
435            partitions: vec![vec![0, 1], vec![2, 3], vec![4]],
436            cut_value: 2.0,
437            modularity: 0.4,
438        };
439        assert_eq!(mp.num_partitions(), 3);
440        assert_eq!(mp.num_nodes(), 5);
441    }
442
443    #[test]
444    fn cognitive_state_serialize_roundtrip() {
445        let states = vec![
446            CognitiveState::Rest,
447            CognitiveState::Focused,
448            CognitiveState::Sleep(SleepStage::Rem),
449            CognitiveState::Unknown,
450        ];
451        let json = serde_json::to_string(&states).unwrap();
452        let deserialized: Vec<CognitiveState> = serde_json::from_str(&json).unwrap();
453        assert_eq!(states, deserialized);
454    }
455
456    // ── Embedding tests ─────────────────────────────────────────────
457
458    #[test]
459    fn embedding_creation_and_norm() {
460        let meta = EmbeddingMetadata {
461            subject_id: Some("sub-01".into()),
462            session_id: Some("ses-01".into()),
463            cognitive_state: Some(CognitiveState::Focused),
464            source_atlas: Atlas::Schaefer100,
465            embedding_method: "spectral".into(),
466        };
467        let emb = NeuralEmbedding::new(vec![3.0, 4.0], 1000.0, meta).unwrap();
468        assert_eq!(emb.dimension, 2);
469        assert!((emb.norm() - 5.0).abs() < 1e-10);
470    }
471
472    #[test]
473    fn embedding_cosine_similarity() {
474        let meta = || EmbeddingMetadata {
475            subject_id: None,
476            session_id: None,
477            cognitive_state: None,
478            source_atlas: Atlas::Custom(2),
479            embedding_method: "test".into(),
480        };
481
482        let a = NeuralEmbedding::new(vec![1.0, 0.0], 0.0, meta()).unwrap();
483        let b = NeuralEmbedding::new(vec![1.0, 0.0], 0.0, meta()).unwrap();
484        let c = NeuralEmbedding::new(vec![0.0, 1.0], 0.0, meta()).unwrap();
485
486        assert!((a.cosine_similarity(&b).unwrap() - 1.0).abs() < 1e-10);
487        assert!((a.cosine_similarity(&c).unwrap() - 0.0).abs() < 1e-10);
488    }
489
490    #[test]
491    fn embedding_euclidean_distance() {
492        let meta = || EmbeddingMetadata {
493            subject_id: None,
494            session_id: None,
495            cognitive_state: None,
496            source_atlas: Atlas::Custom(2),
497            embedding_method: "test".into(),
498        };
499
500        let a = NeuralEmbedding::new(vec![0.0, 0.0], 0.0, meta()).unwrap();
501        let b = NeuralEmbedding::new(vec![3.0, 4.0], 0.0, meta()).unwrap();
502        assert!((a.euclidean_distance(&b).unwrap() - 5.0).abs() < 1e-10);
503    }
504
505    #[test]
506    fn embedding_dimension_mismatch() {
507        let meta = || EmbeddingMetadata {
508            subject_id: None,
509            session_id: None,
510            cognitive_state: None,
511            source_atlas: Atlas::Custom(2),
512            embedding_method: "test".into(),
513        };
514
515        let a = NeuralEmbedding::new(vec![1.0, 2.0], 0.0, meta()).unwrap();
516        let b = NeuralEmbedding::new(vec![1.0, 2.0, 3.0], 0.0, meta()).unwrap();
517        assert!(a.cosine_similarity(&b).is_err());
518        assert!(a.euclidean_distance(&b).is_err());
519    }
520
521    #[test]
522    fn embedding_trajectory() {
523        let meta = || EmbeddingMetadata {
524            subject_id: None,
525            session_id: None,
526            cognitive_state: None,
527            source_atlas: Atlas::Custom(2),
528            embedding_method: "test".into(),
529        };
530
531        let traj = EmbeddingTrajectory {
532            embeddings: vec![
533                NeuralEmbedding::new(vec![1.0], 0.0, meta()).unwrap(),
534                NeuralEmbedding::new(vec![2.0], 1.0, meta()).unwrap(),
535                NeuralEmbedding::new(vec![3.0], 2.0, meta()).unwrap(),
536            ],
537            timestamps: vec![0.0, 1.0, 2.0],
538        };
539
540        assert_eq!(traj.len(), 3);
541        assert!(!traj.is_empty());
542        assert!((traj.duration_s() - 2.0).abs() < 1e-10);
543    }
544
545    // ── RVF tests ───────────────────────────────────────────────────
546
547    #[test]
548    fn rvf_data_type_tag_roundtrip() {
549        for dt in [
550            RvfDataType::BrainGraph,
551            RvfDataType::NeuralEmbedding,
552            RvfDataType::TopologyMetrics,
553            RvfDataType::MincutResult,
554            RvfDataType::TimeSeriesChunk,
555        ] {
556            let tag = dt.to_tag();
557            let recovered = RvfDataType::from_tag(tag).unwrap();
558            assert_eq!(dt, recovered);
559        }
560        assert!(RvfDataType::from_tag(255).is_err());
561    }
562
563    #[test]
564    fn rvf_header_encode_decode() {
565        let header = RvfHeader::new(RvfDataType::NeuralEmbedding, 42, 128);
566        let bytes = header.to_bytes();
567        assert_eq!(bytes.len(), 22);
568
569        let decoded = RvfHeader::from_bytes(&bytes).unwrap();
570        assert_eq!(decoded.magic, rvf::RVF_MAGIC);
571        assert_eq!(decoded.version, rvf::RVF_VERSION);
572        assert_eq!(decoded.data_type, RvfDataType::NeuralEmbedding);
573        assert_eq!(decoded.num_entries, 42);
574        assert_eq!(decoded.embedding_dim, 128);
575    }
576
577    #[test]
578    fn rvf_header_validation() {
579        let mut header = RvfHeader::new(RvfDataType::BrainGraph, 1, 0);
580        assert!(header.validate().is_ok());
581
582        header.magic = [0, 0, 0, 0];
583        assert!(header.validate().is_err());
584    }
585
586    #[test]
587    fn rvf_file_write_read_roundtrip() {
588        let mut file = RvfFile::new(RvfDataType::TopologyMetrics);
589        file.header.num_entries = 1;
590        file.metadata = serde_json::json!({ "subject": "sub-01" });
591        file.data = vec![1, 2, 3, 4, 5];
592
593        let mut buf = Vec::new();
594        file.write_to(&mut buf).unwrap();
595
596        let mut cursor = std::io::Cursor::new(buf);
597        let recovered = RvfFile::read_from(&mut cursor).unwrap();
598
599        assert_eq!(recovered.header.data_type, RvfDataType::TopologyMetrics);
600        assert_eq!(recovered.header.num_entries, 1);
601        assert_eq!(recovered.metadata["subject"], "sub-01");
602        assert_eq!(recovered.data, vec![1, 2, 3, 4, 5]);
603    }
604
605    // ── Serialization roundtrip tests ───────────────────────────────
606
607    #[test]
608    fn graph_serialize_roundtrip() {
609        let graph = BrainGraph {
610            num_nodes: 2,
611            edges: vec![BrainEdge {
612                source: 0,
613                target: 1,
614                weight: 0.42,
615                metric: ConnectivityMetric::TransferEntropy,
616                frequency_band: FrequencyBand::Theta,
617            }],
618            timestamp: 999.0,
619            window_duration_s: 2.0,
620            atlas: Atlas::Schaefer200,
621        };
622        let json = serde_json::to_string(&graph).unwrap();
623        let g2: BrainGraph = serde_json::from_str(&json).unwrap();
624        assert_eq!(g2.num_nodes, 2);
625        assert_eq!(g2.edges.len(), 1);
626        assert!((g2.edges[0].weight - 0.42).abs() < 1e-10);
627    }
628
629    #[test]
630    fn topology_metrics_serialize_roundtrip() {
631        let metrics = TopologyMetrics {
632            global_mincut: 3.14,
633            modularity: 0.55,
634            global_efficiency: 0.72,
635            local_efficiency: 0.68,
636            graph_entropy: 2.3,
637            fiedler_value: 0.12,
638            num_modules: 4,
639            timestamp: 500.0,
640        };
641        let json = serde_json::to_string(&metrics).unwrap();
642        let m2: TopologyMetrics = serde_json::from_str(&json).unwrap();
643        assert!((m2.global_mincut - 3.14).abs() < 1e-10);
644        assert_eq!(m2.num_modules, 4);
645    }
646}