use serde::{Deserialize, Serialize};
use crate::brain::Atlas;
use crate::error::{Result, RuvNeuralError};
use crate::signal::FrequencyBand;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ConnectivityMetric {
PhaseLockingValue,
AmplitudeEnvelopeCorrelation,
WeightedPhaseLagIndex,
Coherence,
GrangerCausality,
TransferEntropy,
MutualInformation,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BrainEdge {
pub source: usize,
pub target: usize,
pub weight: f64,
pub metric: ConnectivityMetric,
pub frequency_band: FrequencyBand,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BrainGraph {
pub num_nodes: usize,
pub edges: Vec<BrainEdge>,
pub timestamp: f64,
pub window_duration_s: f64,
pub atlas: Atlas,
}
impl BrainGraph {
pub fn validate(&self) -> Result<()> {
for (i, edge) in self.edges.iter().enumerate() {
if edge.source >= self.num_nodes {
return Err(RuvNeuralError::Graph(format!(
"Edge {i}: source {} out of bounds (num_nodes={})",
edge.source, self.num_nodes
)));
}
if edge.target >= self.num_nodes {
return Err(RuvNeuralError::Graph(format!(
"Edge {i}: target {} out of bounds (num_nodes={})",
edge.target, self.num_nodes
)));
}
if edge.source == edge.target {
return Err(RuvNeuralError::Graph(format!(
"Edge {i}: self-loop on node {}",
edge.source
)));
}
if !edge.weight.is_finite() {
return Err(RuvNeuralError::Graph(format!(
"Edge {i}: non-finite weight {}",
edge.weight
)));
}
}
Ok(())
}
pub fn adjacency_matrix(&self) -> Vec<Vec<f64>> {
let n = self.num_nodes;
let mut mat = vec![vec![0.0; n]; n];
for edge in &self.edges {
if edge.source < n && edge.target < n {
mat[edge.source][edge.target] = edge.weight;
mat[edge.target][edge.source] = edge.weight;
}
}
mat
}
pub fn edge_weight(&self, source: usize, target: usize) -> Option<f64> {
self.edges
.iter()
.find(|e| {
(e.source == source && e.target == target)
|| (e.source == target && e.target == source)
})
.map(|e| e.weight)
}
pub fn node_degree(&self, node: usize) -> f64 {
self.edges
.iter()
.filter(|e| e.source == node || e.target == node)
.map(|e| e.weight)
.sum()
}
pub fn density(&self) -> f64 {
if self.num_nodes < 2 {
return 0.0;
}
let max_edges = self.num_nodes * (self.num_nodes - 1) / 2;
if max_edges == 0 {
return 0.0;
}
self.edges.len() as f64 / max_edges as f64
}
pub fn total_weight(&self) -> f64 {
self.edges.iter().map(|e| e.weight).sum()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BrainGraphSequence {
pub graphs: Vec<BrainGraph>,
pub window_step_s: f64,
}
impl BrainGraphSequence {
pub fn len(&self) -> usize {
self.graphs.len()
}
pub fn is_empty(&self) -> bool {
self.graphs.is_empty()
}
pub fn duration_s(&self) -> f64 {
if self.graphs.is_empty() {
return 0.0;
}
let first = self.graphs.first().unwrap();
let last = self.graphs.last().unwrap();
(last.timestamp - first.timestamp) + last.window_duration_s
}
}