1pub 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
34pub 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 #[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 #[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 #[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 #[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(®ion).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 #[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 #[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 #[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 #[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 #[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}