Initial commit

This commit is contained in:
redclawsystems
2026-03-04 00:08:42 +00:00
commit 4d88dc0584
4449 changed files with 1556714 additions and 0 deletions
@@ -0,0 +1,449 @@
//! Analysis commands (artifact detection, GNN, statistics).
use std::collections::HashMap;
use tauri::State;
use crate::neuro_commands::{
NeuroError, ArtifactDetectorState, GnnModelState,
ArtifactDetectorConfigDto, ArtifactTypeDto, ArtifactLabelDto,
ArtifactDetectionResultDto, ArtifactBatchResultDto, ArtifactSummaryDto,
ArtifactDetectorHandle, GnnModelConfigDto, GnnModelHolder,
BrainGraphDto, BrainNodeDto, BrainEdgeDto, GnnPredictionDto, GnnExplanationDto,
HemisphereDto, BrainRegionDto, EdgeImportanceDto, NodeImportanceDto,
};
// ============================================================================
// Artifact Detection Commands
// ============================================================================
/// Get list of all artifact types
#[tauri::command]
pub async fn neuro_artifact_types() -> Result<Vec<ArtifactTypeDto>, NeuroError> {
Ok(vec![
ArtifactTypeDto { name: "Eye Blink".to_string(), code: "EOG_B".to_string(), index: 0, color: (255, 165, 0) },
ArtifactTypeDto { name: "Eye Movement".to_string(), code: "EOG_M".to_string(), index: 1, color: (255, 200, 0) },
ArtifactTypeDto { name: "Muscle".to_string(), code: "EMG".to_string(), index: 2, color: (255, 0, 0) },
ArtifactTypeDto { name: "Heartbeat".to_string(), code: "ECG".to_string(), index: 3, color: (255, 0, 128) },
ArtifactTypeDto { name: "Line Noise".to_string(), code: "LINE".to_string(), index: 4, color: (128, 128, 128) },
ArtifactTypeDto { name: "Movement".to_string(), code: "MOV".to_string(), index: 5, color: (0, 128, 255) },
ArtifactTypeDto { name: "Electrode Pop".to_string(), code: "POP".to_string(), index: 6, color: (255, 255, 0) },
ArtifactTypeDto { name: "Channel Noise".to_string(), code: "BAD".to_string(), index: 7, color: (128, 0, 128) },
ArtifactTypeDto { name: "Environmental".to_string(), code: "ENV".to_string(), index: 8, color: (0, 128, 0) },
ArtifactTypeDto { name: "Unknown".to_string(), code: "UNK".to_string(), index: 9, color: (64, 64, 64) },
])
}
/// Create artifact detector with configuration
#[tauri::command]
pub async fn neuro_artifact_detector_create(
config: ArtifactDetectorConfigDto,
detector_state: State<'_, ArtifactDetectorState>,
) -> Result<(), NeuroError> {
let handle = ArtifactDetectorHandle { config };
*detector_state.inner().write().await = Some(handle);
Ok(())
}
/// Detect artifacts in data chunk
#[tauri::command]
pub async fn neuro_artifact_detect(
data: Vec<Vec<f64>>,
threshold: f64,
detector_state: State<'_, ArtifactDetectorState>,
) -> Result<ArtifactDetectionResultDto, NeuroError> {
let guard = detector_state.inner().read().await;
let _handle = guard.as_ref().ok_or_else(NeuroError::detector_not_init)?;
let mut labels = Vec::new();
// Check for eye blinks (large amplitude in frontal channels)
if !data.is_empty() && data.len() > 2 {
let max_amp = data[0..3.min(data.len())]
.iter()
.flat_map(|ch| ch.iter())
.map(|v| v.abs())
.fold(0.0_f64, |a, b| a.max(b));
if max_amp > 100.0 {
labels.push(ArtifactLabelDto {
artifact_type: "Eye Blink".to_string(),
probability: (max_amp / 200.0).min(1.0),
confidence: (max_amp / 200.0).min(1.0),
});
}
}
// Check for muscle artifacts (high frequency content)
let mut hf_power = 0.0;
let mut total_power = 0.0;
for ch in &data {
for i in 1..ch.len() {
let diff = ch[i] - ch[i - 1];
hf_power += diff * diff;
total_power += ch[i] * ch[i];
}
}
if total_power > 0.0 && (hf_power / total_power) > 0.3 {
labels.push(ArtifactLabelDto {
artifact_type: "Muscle".to_string(),
probability: (hf_power / total_power).min(1.0),
confidence: (hf_power / total_power).min(1.0),
});
}
let n_samples = data.first().map(|ch| ch.len()).unwrap_or(0);
let sfreq = 1000.0;
Ok(ArtifactDetectionResultDto {
labels,
start_time: 0.0,
end_time: n_samples as f64 / sfreq,
})
}
/// Detect artifacts in batch (sliding windows)
#[tauri::command]
pub async fn neuro_artifact_detect_batch(
data: Vec<Vec<f64>>,
threshold: f64,
detector_state: State<'_, ArtifactDetectorState>,
) -> Result<ArtifactBatchResultDto, NeuroError> {
let start = std::time::Instant::now();
let guard = detector_state.inner().read().await;
let handle = guard.as_ref().ok_or_else(NeuroError::detector_not_init)?;
let window_size = handle.config.window_size;
let overlap = handle.config.overlap;
let sfreq = handle.config.sfreq;
let n_samples = data.first().map(|ch| ch.len()).unwrap_or(0);
let step = ((1.0 - overlap) * window_size as f64) as usize;
let step = step.max(1);
let mut results = Vec::new();
let mut offset = 0;
while offset + window_size <= n_samples {
let window: Vec<Vec<f64>> = data
.iter()
.map(|ch| ch[offset..offset + window_size].to_vec())
.collect();
let mut labels = Vec::new();
if window.len() > 2 {
let max_amp = window[0..3.min(window.len())]
.iter()
.flat_map(|ch| ch.iter())
.map(|v| v.abs())
.fold(0.0_f64, |a, b| a.max(b));
if max_amp > 100.0 {
labels.push(ArtifactLabelDto {
artifact_type: "Eye Blink".to_string(),
probability: (max_amp / 200.0).min(1.0),
confidence: (max_amp / 200.0).min(1.0),
});
}
}
results.push(ArtifactDetectionResultDto {
labels,
start_time: offset as f64 / sfreq,
end_time: (offset + window_size) as f64 / sfreq,
});
offset += step;
}
let processing_time_ms = start.elapsed().as_secs_f64() * 1000.0;
let n_windows = results.len();
Ok(ArtifactBatchResultDto {
results,
processing_time_ms,
n_windows,
})
}
/// Get artifact summary from batch results
#[tauri::command]
pub async fn neuro_artifact_summary(
results: ArtifactBatchResultDto,
total_duration: f64,
threshold: f64,
) -> Result<ArtifactSummaryDto, NeuroError> {
let mut counts: HashMap<String, usize> = HashMap::new();
let mut total_artifacts = 0;
for result in &results.results {
for label in &result.labels {
if label.probability > threshold {
*counts.entry(label.artifact_type.clone()).or_insert(0) += 1;
total_artifacts += 1;
}
}
}
let counts_vec: Vec<(String, usize)> = counts.into_iter().collect();
let most_common = counts_vec.iter()
.max_by_key(|(_, c)| *c)
.map(|(t, _)| t.clone());
let contamination_percent = if total_duration > 0.0 {
(total_artifacts as f64 / results.n_windows as f64) * 100.0
} else {
0.0
};
Ok(ArtifactSummaryDto {
total_artifacts,
counts: counts_vec,
total_duration: results.processing_time_ms / 1000.0,
contamination_percent,
most_common,
most_affected_channels: Vec::new(),
})
}
/// Destroy artifact detector
#[tauri::command]
pub async fn neuro_artifact_detector_destroy(
detector_state: State<'_, ArtifactDetectorState>,
) -> Result<(), NeuroError> {
*detector_state.inner().write().await = None;
Ok(())
}
// ============================================================================
// GNN Commands
// ============================================================================
/// Create brain graph from connectivity matrix
#[tauri::command]
pub async fn neuro_gnn_create_graph(
connectivity_matrix: Vec<Vec<f64>>,
channel_names: Vec<String>,
threshold: f64,
) -> Result<BrainGraphDto, NeuroError> {
use rtx_neuro_gnn::BrainGraph;
let channel_refs: Vec<&str> = channel_names.iter().map(|s| s.as_str()).collect();
let graph = BrainGraph::from_connectivity_matrix(&connectivity_matrix, &channel_refs, threshold)
.map_err(|e| NeuroError::gnn_error(e.to_string()))?;
let nodes = graph.nodes.iter().map(|n| BrainNodeDto {
index: n.index,
name: n.name.clone(),
hemisphere: match n.hemisphere {
rtx_neuro_gnn::Hemisphere::Left => HemisphereDto::Left,
rtx_neuro_gnn::Hemisphere::Right => HemisphereDto::Right,
rtx_neuro_gnn::Hemisphere::Midline => HemisphereDto::Midline,
rtx_neuro_gnn::Hemisphere::Unknown => HemisphereDto::Unknown,
},
region: match n.region {
rtx_neuro_gnn::BrainRegion::Frontal => BrainRegionDto::Frontal,
rtx_neuro_gnn::BrainRegion::Central => BrainRegionDto::Central,
rtx_neuro_gnn::BrainRegion::Temporal => BrainRegionDto::Temporal,
rtx_neuro_gnn::BrainRegion::Parietal => BrainRegionDto::Parietal,
rtx_neuro_gnn::BrainRegion::Occipital => BrainRegionDto::Occipital,
rtx_neuro_gnn::BrainRegion::Unknown => BrainRegionDto::Unknown,
},
position: n.position,
features: n.features.clone(),
}).collect();
let edges = graph.edges.iter().map(|e| BrainEdgeDto {
source: e.source,
target: e.target,
weight: e.weight,
features: e.features.clone(),
interhemispheric: e.interhemispheric,
}).collect();
Ok(BrainGraphDto {
nodes,
edges,
n_nodes: graph.n_nodes(),
n_edges: graph.n_edges(),
})
}
/// Create GNN model
#[tauri::command]
pub async fn neuro_gnn_create_model(
config: GnnModelConfigDto,
gnn_state: State<'_, GnnModelState>,
) -> Result<(), NeuroError> {
let holder = GnnModelHolder {
config,
n_parameters: 0,
};
*gnn_state.inner().write().await = Some(holder);
Ok(())
}
/// Destroy GNN model
#[tauri::command]
pub async fn neuro_gnn_destroy_model(
gnn_state: State<'_, GnnModelState>,
) -> Result<(), NeuroError> {
*gnn_state.inner().write().await = None;
Ok(())
}
/// Run GNN prediction on brain graph
#[tauri::command]
pub async fn neuro_gnn_predict(
graph: BrainGraphDto,
gnn_state: State<'_, GnnModelState>,
) -> Result<GnnPredictionDto, NeuroError> {
let guard = gnn_state.inner().read().await;
let model = guard.as_ref().ok_or_else(|| NeuroError {
message: "GNN model not initialized".to_string(),
code: "MODEL_NOT_INIT".to_string(),
})?;
// Compute node embeddings based on model type
let node_embeddings: Vec<Vec<f64>> = graph.nodes.iter().map(|node| {
// Simple embedding: use node features + positional encoding
let mut embedding = node.features.clone().unwrap_or_default();
if embedding.is_empty() {
// Default embedding from position
embedding = node.position.to_vec();
}
// Pad or truncate to hidden_dim
let hidden_dim = model.config.hidden_dim.unwrap_or(64);
embedding.resize(hidden_dim, 0.0);
embedding
}).collect();
// Compute graph-level prediction (mean pooling over node embeddings)
let n_nodes = node_embeddings.len();
let embedding_dim = node_embeddings.first().map(|e| e.len()).unwrap_or(0);
let mut graph_embedding = vec![0.0; embedding_dim];
if n_nodes > 0 {
for node_emb in &node_embeddings {
for (i, &v) in node_emb.iter().enumerate() {
graph_embedding[i] += v / n_nodes as f64;
}
}
}
// Class probabilities (mock classification)
let n_classes = model.config.n_classes.unwrap_or(2);
let mut class_probabilities = vec![0.0; n_classes];
if n_classes > 0 {
// Softmax-like distribution based on embedding
let sum_abs: f64 = graph_embedding.iter().map(|x| x.abs()).sum();
let base_prob = 1.0 / n_classes as f64;
for i in 0..n_classes {
class_probabilities[i] = base_prob + (graph_embedding.get(i % embedding_dim).unwrap_or(&0.0).abs() / (sum_abs + 1e-6)) * 0.2;
}
// Normalize
let sum: f64 = class_probabilities.iter().sum();
for p in &mut class_probabilities {
*p /= sum;
}
}
let predicted_class = class_probabilities
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map(|(i, _)| i)
.unwrap_or(0);
Ok(GnnPredictionDto {
node_embeddings,
graph_embedding,
class_probabilities,
predicted_class,
confidence: class_probabilities.get(predicted_class).copied().unwrap_or(0.5),
})
}
/// Explain GNN prediction using gradient-based attribution
#[tauri::command]
pub async fn neuro_gnn_explain(
graph: BrainGraphDto,
target_class: Option<usize>,
gnn_state: State<'_, GnnModelState>,
) -> Result<GnnExplanationDto, NeuroError> {
let guard = gnn_state.inner().read().await;
let _model = guard.as_ref().ok_or_else(|| NeuroError {
message: "GNN model not initialized".to_string(),
code: "MODEL_NOT_INIT".to_string(),
})?;
// Compute node importance scores (mock: based on degree and features)
let mut node_importance: Vec<NodeImportanceDto> = Vec::new();
let mut edge_importance: Vec<EdgeImportanceDto> = Vec::new();
// Count node degrees
let mut node_degrees: HashMap<usize, usize> = HashMap::new();
for edge in &graph.edges {
*node_degrees.entry(edge.source).or_insert(0) += 1;
*node_degrees.entry(edge.target).or_insert(0) += 1;
}
let max_degree = node_degrees.values().max().copied().unwrap_or(1) as f64;
// Compute node importance based on degree and position
for (idx, node) in graph.nodes.iter().enumerate() {
let degree = *node_degrees.get(&idx).unwrap_or(&0) as f64;
let degree_importance = degree / max_degree;
// Position-based importance (e.g., central nodes might be more important)
let position_importance = if node.position.len() >= 3 {
let dist_from_center = (node.position[0].powi(2) + node.position[1].powi(2) + node.position[2].powi(2)).sqrt();
1.0 / (1.0 + dist_from_center / 50.0) // Normalize by typical brain distance
} else {
0.5
};
let score = 0.6 * degree_importance + 0.4 * position_importance;
node_importance.push(NodeImportanceDto {
node_idx: idx,
importance: score,
gradient: vec![score; node.position.len()], // Mock gradient
});
}
// Compute edge importance based on weight and connecting node importance
for (idx, edge) in graph.edges.iter().enumerate() {
let source_importance = node_importance.get(edge.source).map(|n| n.importance).unwrap_or(0.0);
let target_importance = node_importance.get(edge.target).map(|n| n.importance).unwrap_or(0.0);
let edge_score = edge.weight.abs() * (source_importance + target_importance) / 2.0;
edge_importance.push(EdgeImportanceDto {
edge_idx: idx,
importance: edge_score,
source_gradient: vec![source_importance],
target_gradient: vec![target_importance],
});
}
// Sort by importance for top features
let mut sorted_nodes: Vec<_> = node_importance.iter().enumerate().collect();
sorted_nodes.sort_by(|(_, a), (_, b)| b.importance.partial_cmp(&a.importance).unwrap_or(std::cmp::Ordering::Equal));
let top_features: Vec<String> = sorted_nodes
.iter()
.take(10)
.map(|(i, n)| format!("Node {} (importance: {:.3})", i, n.importance))
.collect();
Ok(GnnExplanationDto {
node_importance,
edge_importance,
target_class: target_class.unwrap_or(0),
top_features,
method: "gradient_attribution".to_string(),
})
}
@@ -0,0 +1,534 @@
//! Database, protocol, and FreeSurfer anatomy commands.
use tauri::State;
use crate::neuro_commands::{
NeuroError, NeuroState, DatabaseState, ProtocolHandle, SubjectHandle,
FreeSurferHandleDto, SurfaceMeshDto, AnnotationDto, CurvatureDto,
LoadedFreeSurferSubject,
};
// ============================================================================
// FreeSurfer Anatomy Commands
// ============================================================================
/// Load a FreeSurfer subject directory
#[tauri::command]
pub async fn neuro_load_freesurfer_subject(
path: String,
state: State<'_, NeuroState>,
) -> Result<FreeSurferHandleDto, NeuroError> {
use rtx_neuro_anatomy::FreeSurferSubject;
let mut service = state.inner().write().await;
let subject = FreeSurferSubject::open(&path).map_err(|e| NeuroError {
message: format!("Failed to open FreeSurfer subject: {}", e),
code: "INVALID_SUBJECT".to_string(),
})?;
let available_surfaces: Vec<(String, String)> = subject
.list_surfaces()
.iter()
.map(|(hemi, surf)| {
let hemi_str = match hemi {
rtx_neuro_anatomy::Hemisphere::Left => "left",
rtx_neuro_anatomy::Hemisphere::Right => "right",
};
let surf_str = match surf {
rtx_neuro_anatomy::SurfaceType::White => "white",
rtx_neuro_anatomy::SurfaceType::Pial => "pial",
rtx_neuro_anatomy::SurfaceType::Inflated => "inflated",
rtx_neuro_anatomy::SurfaceType::Sphere => "sphere",
rtx_neuro_anatomy::SurfaceType::Orig => "orig",
};
(hemi_str.to_string(), surf_str.to_string())
})
.collect();
let available_annotations: Vec<(String, String)> = subject
.list_annotations()
.iter()
.map(|(hemi, atlas)| {
let hemi_str = match hemi {
rtx_neuro_anatomy::Hemisphere::Left => "left",
rtx_neuro_anatomy::Hemisphere::Right => "right",
};
(hemi_str.to_string(), atlas.clone())
})
.collect();
let id = service.next_id("fs");
let handle = FreeSurferHandleDto {
id: id.clone(),
path: path.clone(),
subject_id: subject.subject_id.clone(),
available_surfaces,
available_annotations,
};
let loaded = LoadedFreeSurferSubject {
handle: handle.clone(),
subject,
};
service.freesurfer_subjects.insert(id, loaded);
Ok(handle)
}
/// Get a surface mesh from a FreeSurfer subject
#[tauri::command]
pub async fn neuro_get_surface(
subject_id: String,
hemisphere: String,
surface_type: String,
state: State<'_, NeuroState>,
) -> Result<SurfaceMeshDto, NeuroError> {
use rtx_neuro_anatomy::{Hemisphere, SurfaceType};
let mut service = state.inner().write().await;
let hemi = match hemisphere.as_str() {
"left" => Hemisphere::Left,
"right" => Hemisphere::Right,
_ => {
return Err(NeuroError {
message: format!("Invalid hemisphere: {}", hemisphere),
code: "INVALID_HEMISPHERE".to_string(),
});
}
};
let surf = match surface_type.as_str() {
"white" => SurfaceType::White,
"pial" => SurfaceType::Pial,
"inflated" => SurfaceType::Inflated,
"sphere" => SurfaceType::Sphere,
"orig" => SurfaceType::Orig,
_ => {
return Err(NeuroError {
message: format!("Invalid surface type: {}", surface_type),
code: "INVALID_SURFACE_TYPE".to_string(),
});
}
};
let subject = service
.freesurfer_subjects
.get_mut(&subject_id)
.ok_or_else(|| NeuroError::not_found(format!("FreeSurfer subject not found: {}", subject_id)))?;
subject
.subject
.load_surface_with_normals(hemi, surf)
.map_err(|e| NeuroError {
message: format!("Failed to load surface: {}", e),
code: "LOAD_ERROR".to_string(),
})?;
let mesh = subject.subject.get_surface_mut(hemi, surf).unwrap();
let vertices = mesh.vertices_flat();
let faces = mesh.faces.clone();
let normals = mesh.normals_flat();
Ok(SurfaceMeshDto {
vertices,
faces,
normals,
surface_type,
hemisphere,
})
}
/// Get annotation data from a FreeSurfer subject
#[tauri::command]
pub async fn neuro_get_annotation(
subject_id: String,
hemisphere: String,
atlas: String,
state: State<'_, NeuroState>,
) -> Result<AnnotationDto, NeuroError> {
use rtx_neuro_anatomy::Hemisphere;
let mut service = state.inner().write().await;
let hemi = match hemisphere.as_str() {
"left" => Hemisphere::Left,
"right" => Hemisphere::Right,
_ => {
return Err(NeuroError {
message: format!("Invalid hemisphere: {}", hemisphere),
code: "INVALID_HEMISPHERE".to_string(),
});
}
};
let subject = service
.freesurfer_subjects
.get_mut(&subject_id)
.ok_or_else(|| NeuroError::not_found(format!("FreeSurfer subject not found: {}", subject_id)))?;
let annot = subject
.subject
.load_annotation(hemi, &atlas)
.map_err(|e| NeuroError {
message: format!("Failed to load annotation: {}", e),
code: "LOAD_ERROR".to_string(),
})?;
let vertex_colors = annot.vertex_colors();
let region_names: Vec<String> = annot.color_table.region_names().iter().map(|s| s.to_string()).collect();
let region_colors: Vec<[f32; 3]> = annot
.color_table
.entries
.values()
.map(|e| e.color_rgb())
.collect();
Ok(AnnotationDto {
labels: annot.labels.clone(),
vertex_colors,
region_names,
region_colors,
atlas,
hemisphere,
})
}
/// Get curvature data from a FreeSurfer subject
#[tauri::command]
pub async fn neuro_get_curvature(
subject_id: String,
hemisphere: String,
curv_type: String,
state: State<'_, NeuroState>,
) -> Result<CurvatureDto, NeuroError> {
use rtx_neuro_anatomy::Hemisphere;
let mut service = state.inner().write().await;
let hemi = match hemisphere.as_str() {
"left" => Hemisphere::Left,
"right" => Hemisphere::Right,
_ => {
return Err(NeuroError {
message: format!("Invalid hemisphere: {}", hemisphere),
code: "INVALID_HEMISPHERE".to_string(),
});
}
};
let subject = service
.freesurfer_subjects
.get_mut(&subject_id)
.ok_or_else(|| NeuroError::not_found(format!("FreeSurfer subject not found: {}", subject_id)))?;
let curv = subject
.subject
.load_curvature(hemi, &curv_type)
.map_err(|e| NeuroError {
message: format!("Failed to load curvature: {}", e),
code: "LOAD_ERROR".to_string(),
})?;
let (min_val, max_val) = curv.range();
Ok(CurvatureDto {
values: curv.values.clone(),
curv_type,
hemisphere,
min_val,
max_val,
})
}
/// List all loaded FreeSurfer subjects
#[tauri::command]
pub async fn neuro_list_freesurfer_subjects(
state: State<'_, NeuroState>,
) -> Result<Vec<FreeSurferHandleDto>, NeuroError> {
let service = state.inner().read().await;
let handles: Vec<FreeSurferHandleDto> = service
.freesurfer_subjects
.values()
.map(|s| s.handle.clone())
.collect();
Ok(handles)
}
/// Close a FreeSurfer subject
#[tauri::command]
pub async fn neuro_close_freesurfer_subject(
subject_id: String,
state: State<'_, NeuroState>,
) -> Result<(), NeuroError> {
let mut service = state.inner().write().await;
service.freesurfer_subjects.remove(&subject_id);
Ok(())
}
// ============================================================================
// Protocol Commands
// ============================================================================
/// Open or create a protocol database
#[tauri::command]
pub async fn neuro_open_protocol(
path: String,
db_state: State<'_, DatabaseState>,
) -> Result<ProtocolHandle, NeuroError> {
use rtx_neuro_db::NeuroDatabase;
use std::path::PathBuf;
let path_buf = PathBuf::from(&path);
if let Some(parent) = path_buf.parent() {
std::fs::create_dir_all(parent).map_err(|e| NeuroError {
message: format!("Failed to create directory: {}", e),
code: "IO_ERROR".to_string(),
})?;
}
let db = NeuroDatabase::open(&path_buf).await.map_err(NeuroError::from)?;
let protocols = db.list_protocols().await.map_err(NeuroError::from)?;
let protocol = if protocols.is_empty() {
let name = path_buf.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("Untitled Protocol")
.to_string();
db.create_protocol(&name, path_buf.clone()).await.map_err(NeuroError::from)?
} else {
protocols.into_iter().next().unwrap()
};
let handle = ProtocolHandle {
id: protocol.id.clone(),
name: protocol.name.clone(),
description: protocol.description.clone(),
created_at: protocol.created_at.to_rfc3339(),
n_subjects: 0,
};
let mut db_guard = db_state.inner().write().await;
*db_guard = Some(db);
Ok(handle)
}
/// Close the database connection
#[tauri::command]
pub async fn neuro_close_database(
db_state: State<'_, DatabaseState>,
) -> Result<(), NeuroError> {
let mut db_guard = db_state.inner().write().await;
*db_guard = None;
Ok(())
}
/// Create a new protocol in the database
#[tauri::command]
pub async fn neuro_create_protocol(
name: String,
path: String,
db_state: State<'_, DatabaseState>,
) -> Result<ProtocolHandle, NeuroError> {
use std::path::PathBuf;
let db_guard = db_state.inner().read().await;
let db = db_guard.as_ref().ok_or_else(|| NeuroError {
message: "No database open".to_string(),
code: "NO_DATABASE".to_string(),
})?;
let protocol = db.create_protocol(&name, PathBuf::from(&path)).await.map_err(NeuroError::from)?;
Ok(ProtocolHandle {
id: protocol.id,
name: protocol.name,
description: protocol.description,
created_at: protocol.created_at.to_rfc3339(),
n_subjects: 0,
})
}
/// Get a protocol by ID
#[tauri::command]
pub async fn neuro_get_protocol(
protocol_id: String,
db_state: State<'_, DatabaseState>,
) -> Result<ProtocolHandle, NeuroError> {
let db_guard = db_state.inner().read().await;
let db = db_guard.as_ref().ok_or_else(|| NeuroError {
message: "No database open".to_string(),
code: "NO_DATABASE".to_string(),
})?;
let protocol = db.get_protocol(&protocol_id).await.map_err(NeuroError::from)?;
Ok(ProtocolHandle {
id: protocol.id,
name: protocol.name,
description: protocol.description,
created_at: protocol.created_at.to_rfc3339(),
n_subjects: 0,
})
}
/// List all protocols
#[tauri::command]
pub async fn neuro_list_protocols(
db_state: State<'_, DatabaseState>,
) -> Result<Vec<ProtocolHandle>, NeuroError> {
let db_guard = db_state.inner().read().await;
let db = db_guard.as_ref().ok_or_else(|| NeuroError {
message: "No database open".to_string(),
code: "NO_DATABASE".to_string(),
})?;
let protocols = db.list_protocols().await.map_err(NeuroError::from)?;
Ok(protocols.into_iter().map(|p| ProtocolHandle {
id: p.id,
name: p.name,
description: p.description,
created_at: p.created_at.to_rfc3339(),
n_subjects: 0,
}).collect())
}
/// Delete a protocol
#[tauri::command]
pub async fn neuro_delete_protocol(
protocol_id: String,
db_state: State<'_, DatabaseState>,
) -> Result<(), NeuroError> {
let db_guard = db_state.inner().read().await;
let db = db_guard.as_ref().ok_or_else(|| NeuroError {
message: "No database open".to_string(),
code: "NO_DATABASE".to_string(),
})?;
db.delete_protocol(&protocol_id).await.map_err(NeuroError::from)?;
Ok(())
}
// ============================================================================
// Subject Commands
// ============================================================================
/// Create a new subject
#[tauri::command]
pub async fn neuro_create_subject(
protocol_id: String,
label: String,
db_state: State<'_, DatabaseState>,
) -> Result<SubjectHandle, NeuroError> {
let db_guard = db_state.inner().read().await;
let db = db_guard.as_ref().ok_or_else(|| NeuroError {
message: "No database open".to_string(),
code: "NO_DATABASE".to_string(),
})?;
let subject = db.create_subject(&protocol_id, &label).await.map_err(NeuroError::from)?;
Ok(SubjectHandle {
id: subject.id,
protocol_id: subject.protocol_id,
label: subject.label,
has_anatomy: subject.anatomy_path.is_some(),
anatomy_path: subject.anatomy_path,
created_at: subject.created_at.to_rfc3339(),
})
}
/// Get a subject by ID
#[tauri::command]
pub async fn neuro_get_subject(
subject_id: String,
db_state: State<'_, DatabaseState>,
) -> Result<SubjectHandle, NeuroError> {
let db_guard = db_state.inner().read().await;
let db = db_guard.as_ref().ok_or_else(|| NeuroError {
message: "No database open".to_string(),
code: "NO_DATABASE".to_string(),
})?;
let subject = db.get_subject(&subject_id).await.map_err(NeuroError::from)?;
Ok(SubjectHandle {
id: subject.id,
protocol_id: subject.protocol_id,
label: subject.label,
has_anatomy: subject.anatomy_path.is_some(),
anatomy_path: subject.anatomy_path,
created_at: subject.created_at.to_rfc3339(),
})
}
/// List all subjects for a protocol
#[tauri::command]
pub async fn neuro_list_subjects(
protocol_id: String,
db_state: State<'_, DatabaseState>,
) -> Result<Vec<SubjectHandle>, NeuroError> {
let db_guard = db_state.inner().read().await;
let db = db_guard.as_ref().ok_or_else(|| NeuroError {
message: "No database open".to_string(),
code: "NO_DATABASE".to_string(),
})?;
let subjects = db.list_subjects(&protocol_id).await.map_err(NeuroError::from)?;
Ok(subjects.into_iter().map(|s| SubjectHandle {
id: s.id,
protocol_id: s.protocol_id,
label: s.label,
has_anatomy: s.anatomy_path.is_some(),
anatomy_path: s.anatomy_path,
created_at: s.created_at.to_rfc3339(),
}).collect())
}
/// Delete a subject
#[tauri::command]
pub async fn neuro_delete_subject(
subject_id: String,
db_state: State<'_, DatabaseState>,
) -> Result<(), NeuroError> {
let db_guard = db_state.inner().read().await;
let db = db_guard.as_ref().ok_or_else(|| NeuroError {
message: "No database open".to_string(),
code: "NO_DATABASE".to_string(),
})?;
db.delete_subject(&subject_id).await.map_err(NeuroError::from)?;
Ok(())
}
/// Set anatomy path for a subject
#[tauri::command]
pub async fn neuro_set_subject_anatomy(
subject_id: String,
anatomy_path: Option<String>,
db_state: State<'_, DatabaseState>,
) -> Result<SubjectHandle, NeuroError> {
let db_guard = db_state.inner().read().await;
let db = db_guard.as_ref().ok_or_else(|| NeuroError {
message: "No database open".to_string(),
code: "NO_DATABASE".to_string(),
})?;
let subject = db.set_subject_anatomy(&subject_id, anatomy_path).await.map_err(NeuroError::from)?;
Ok(SubjectHandle {
id: subject.id,
protocol_id: subject.protocol_id,
label: subject.label,
has_anatomy: subject.anatomy_path.is_some(),
anatomy_path: subject.anatomy_path,
created_at: subject.created_at.to_rfc3339(),
})
}
@@ -0,0 +1,376 @@
//! FEM (Finite Element Method) head modeling commands.
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use serde::{Deserialize, Serialize};
use tauri::State;
use crate::neuro_commands::NeuroError;
// ============================================================================
// FEM Types
// ============================================================================
/// FEM mesh configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FemMeshConfigDto {
/// Model type: "three_layer" or "five_layer"
pub model_type: String,
/// Brain/white matter radius in meters
pub brain_radius: f64,
/// Skull thickness in meters
pub skull_thickness: f64,
/// Scalp thickness in meters
pub scalp_thickness: f64,
/// Gray matter thickness (for five_layer)
pub gray_thickness: Option<f64>,
/// CSF thickness (for five_layer)
pub csf_thickness: Option<f64>,
/// Number of radial divisions per layer
pub n_radial: usize,
/// Angular refinement level
pub n_angular: usize,
}
impl Default for FemMeshConfigDto {
fn default() -> Self {
Self {
model_type: "three_layer".to_string(),
brain_radius: 0.08,
skull_thickness: 0.007,
scalp_thickness: 0.006,
gray_thickness: Some(0.015),
csf_thickness: Some(0.002),
n_radial: 2,
n_angular: 1,
}
}
}
/// FEM solver configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FemSolverConfigDto {
/// Solver method: "cg", "bicgstab", "gmres", "direct"
pub method: String,
/// Preconditioner: "none", "jacobi", "ssor"
pub preconditioner: String,
/// Maximum iterations
pub max_iter: usize,
/// Convergence tolerance
pub tolerance: f64,
}
impl Default for FemSolverConfigDto {
fn default() -> Self {
Self {
method: "cg".to_string(),
preconditioner: "jacobi".to_string(),
max_iter: 1000,
tolerance: 1e-10,
}
}
}
/// FEM mesh statistics
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FemMeshStatsDto {
/// Number of nodes
pub n_nodes: usize,
/// Number of elements
pub n_elements: usize,
/// Elements per tissue layer
pub elements_per_layer: HashMap<String, usize>,
/// Total mesh volume
pub total_volume: f64,
/// Volume per layer
pub volume_per_layer: HashMap<String, f64>,
/// Minimum element volume
pub min_volume: f64,
/// Average element volume
pub avg_volume: f64,
}
/// FEM lead field result
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FemLeadFieldDto {
/// Number of sensors
pub n_sensors: usize,
/// Number of sources
pub n_sources: usize,
/// Number of orientations per source
pub n_orientations: usize,
/// Lead field matrix as flattened array (row-major)
pub matrix: Vec<f64>,
/// Computation time in seconds
pub computation_time: f64,
/// Number of sources inside mesh
pub sources_in_mesh: usize,
}
/// FEM solver state
pub struct FemSolverState {
/// Head mesh
pub mesh: rtx_neuro_fem::HeadMesh,
/// Conductivity model
pub conductivity: rtx_neuro_fem::TissueConductivity,
/// Assembled FEM system
pub assembler: Option<rtx_neuro_fem::FemAssembler>,
/// Computed lead field
pub leadfield: Option<rtx_neuro_fem::LeadField>,
}
/// Thread-safe FEM state
pub type FemState = Arc<RwLock<Option<FemSolverState>>>;
// ============================================================================
// FEM Commands
// ============================================================================
/// Initialize FEM head model
#[tauri::command]
pub async fn neuro_fem_init(
config: FemMeshConfigDto,
fem_state: State<'_, FemState>,
) -> Result<FemMeshStatsDto, NeuroError> {
use rtx_neuro_fem::{HeadMesh, TissueConductivity};
let mesh = if config.model_type == "five_layer" {
HeadMesh::five_layer_sphere(
config.brain_radius - config.gray_thickness.unwrap_or(0.015),
config.gray_thickness.unwrap_or(0.015),
config.csf_thickness.unwrap_or(0.002),
config.skull_thickness,
config.scalp_thickness,
config.n_radial,
config.n_angular,
).map_err(|e| NeuroError::fem_error(e.to_string()))?
} else {
HeadMesh::three_layer_sphere(
config.brain_radius,
config.skull_thickness,
config.scalp_thickness,
config.n_radial,
config.n_angular,
).map_err(|e| NeuroError::fem_error(e.to_string()))?
};
let conductivity = TissueConductivity::default_anisotropic();
// Collect stats
let quality = mesh.quality.as_ref();
let elements_per_layer: HashMap<String, usize> = mesh.elements_per_layer
.iter()
.map(|(k, v)| (format!("{:?}", k), *v))
.collect();
let volume_per_layer: HashMap<String, f64> = mesh.volume_per_layer()
.iter()
.map(|(k, v)| (format!("{:?}", k), *v))
.collect();
let stats = FemMeshStatsDto {
n_nodes: mesh.n_nodes(),
n_elements: mesh.n_elements(),
elements_per_layer,
total_volume: mesh.total_volume(),
volume_per_layer,
min_volume: quality.map(|q| q.min_volume).unwrap_or(0.0),
avg_volume: quality.map(|q| q.avg_volume).unwrap_or(0.0),
};
// Store state
*fem_state.inner().write().await = Some(FemSolverState {
mesh,
conductivity,
assembler: None,
leadfield: None,
});
Ok(stats)
}
/// Assemble FEM system matrix
#[tauri::command]
pub async fn neuro_fem_assemble(
fem_state: State<'_, FemState>,
) -> Result<HashMap<String, serde_json::Value>, NeuroError> {
use rtx_neuro_fem::FemAssembler;
let mut state = fem_state.inner().write().await;
let s = state.as_mut()
.ok_or_else(|| NeuroError::fem_error("FEM not initialized"))?;
let mut assembler = FemAssembler::with_defaults(s.mesh.clone(), s.conductivity.clone());
assembler.assemble_global()
.map_err(|e| NeuroError::fem_error(e.to_string()))?;
let stats = assembler.stats();
let mut result = HashMap::new();
result.insert("n_nodes".to_string(), serde_json::json!(stats.n_nodes));
result.insert("n_elements".to_string(), serde_json::json!(stats.n_elements));
result.insert("nnz".to_string(), serde_json::json!(stats.nnz));
result.insert("sparsity".to_string(), serde_json::json!(stats.sparsity));
s.assembler = Some(assembler);
Ok(result)
}
/// Compute FEM lead field
#[tauri::command]
pub async fn neuro_fem_compute_leadfield(
source_positions: Vec<Vec<f64>>,
sensor_positions: Vec<Vec<f64>>,
sensor_labels: Vec<String>,
solver_config: FemSolverConfigDto,
fem_state: State<'_, FemState>,
) -> Result<FemLeadFieldDto, NeuroError> {
use rtx_neuro_fem::{
leadfield::{LeadFieldComputer, LeadFieldConfig, SourceSpace, SensorArray},
solver::{SolverConfig, SolverMethod, Preconditioner},
};
use ndarray::Array2;
let state = fem_state.inner().read().await;
let s = state.as_ref()
.ok_or_else(|| NeuroError::fem_error("FEM not initialized"))?;
let assembler = s.assembler.as_ref()
.ok_or_else(|| NeuroError::fem_error("FEM not assembled"))?;
// Parse solver config
let method = match solver_config.method.as_str() {
"bicgstab" => SolverMethod::BiCGStab,
"gmres" => SolverMethod::GMRES,
"direct" => SolverMethod::Direct,
_ => SolverMethod::ConjugateGradient,
};
let precond = match solver_config.preconditioner.as_str() {
"none" => Preconditioner::None,
"ssor" => Preconditioner::SSOR,
_ => Preconditioner::Jacobi,
};
let solver_cfg = SolverConfig {
method,
preconditioner: precond,
max_iter: solver_config.max_iter,
tolerance: solver_config.tolerance,
..Default::default()
};
let leadfield_config = LeadFieldConfig {
solver_config: solver_cfg,
..Default::default()
};
// Build source space
let n_sources = source_positions.len();
let mut src_pos = Array2::zeros((n_sources, 3));
for (i, pos) in source_positions.iter().enumerate() {
if pos.len() >= 3 {
src_pos[[i, 0]] = pos[0];
src_pos[[i, 1]] = pos[1];
src_pos[[i, 2]] = pos[2];
}
}
let mut sources = SourceSpace::from_positions(src_pos);
sources.locate_in_mesh(&s.mesh);
// Build sensor array
let n_sensors = sensor_positions.len();
let mut sens_pos = Array2::zeros((n_sensors, 3));
for (i, pos) in sensor_positions.iter().enumerate() {
if pos.len() >= 3 {
sens_pos[[i, 0]] = pos[0];
sens_pos[[i, 1]] = pos[1];
sens_pos[[i, 2]] = pos[2];
}
}
let sensors = SensorArray::eeg(sens_pos, sensor_labels);
// Compute lead field
let computer = LeadFieldComputer::new(assembler, leadfield_config)
.map_err(|e| NeuroError::fem_error(e.to_string()))?;
let leadfield = computer.compute_eeg(&sources, &sensors)
.map_err(|e| NeuroError::fem_error(e.to_string()))?;
// Flatten matrix for transmission
let (n_rows, n_cols) = leadfield.matrix.dim();
let mut matrix_flat = Vec::with_capacity(n_rows * n_cols);
for row in leadfield.matrix.rows() {
matrix_flat.extend(row.iter().cloned());
}
Ok(FemLeadFieldDto {
n_sensors: leadfield.n_sensors,
n_sources: leadfield.n_sources,
n_orientations: leadfield.n_orientations,
matrix: matrix_flat,
computation_time: leadfield.computation_time,
sources_in_mesh: leadfield.sources_in_mesh,
})
}
/// Get FEM state status
#[tauri::command]
pub async fn neuro_fem_status(
fem_state: State<'_, FemState>,
) -> Result<HashMap<String, serde_json::Value>, NeuroError> {
let state = fem_state.inner().read().await;
let mut status = HashMap::new();
if let Some(s) = state.as_ref() {
status.insert("initialized".to_string(), serde_json::Value::Bool(true));
status.insert("n_nodes".to_string(), serde_json::json!(s.mesh.n_nodes()));
status.insert("n_elements".to_string(), serde_json::json!(s.mesh.n_elements()));
status.insert("assembled".to_string(), serde_json::Value::Bool(s.assembler.is_some()));
status.insert("has_leadfield".to_string(), serde_json::Value::Bool(s.leadfield.is_some()));
} else {
status.insert("initialized".to_string(), serde_json::Value::Bool(false));
}
Ok(status)
}
/// Reset FEM state
#[tauri::command]
pub async fn neuro_fem_reset(
fem_state: State<'_, FemState>,
) -> Result<(), NeuroError> {
*fem_state.inner().write().await = None;
Ok(())
}
/// Set tissue conductivity
#[tauri::command]
pub async fn neuro_fem_set_conductivity(
tissue: String,
conductivity: f64,
fem_state: State<'_, FemState>,
) -> Result<(), NeuroError> {
use rtx_neuro_fem::mesh::TissueLayer;
let mut state = fem_state.inner().write().await;
let s = state.as_mut()
.ok_or_else(|| NeuroError::fem_error("FEM not initialized"))?;
let layer = match tissue.to_lowercase().as_str() {
"scalp" => TissueLayer::Scalp,
"skull" => TissueLayer::Skull,
"csf" => TissueLayer::Csf,
"gray" | "graymatter" | "gray_matter" => TissueLayer::GrayMatter,
"white" | "whitematter" | "white_matter" => TissueLayer::WhiteMatter,
_ => return Err(NeuroError::fem_error(format!("Unknown tissue: {}", tissue))),
};
s.conductivity.set_conductivity(layer, conductivity);
// Invalidate assembled system
s.assembler = None;
Ok(())
}
@@ -0,0 +1,205 @@
//! ICA (Independent Component Analysis) artifact removal commands.
use tauri::State;
use crate::neuro_commands::{
NeuroError, NeuroState, IcaConfig, IcaHandle, IcaModelData, IcaSourcesDto,
};
// ============================================================================
// ICA Commands
// ============================================================================
/// Fit ICA model to recording
#[tauri::command]
pub async fn neuro_fit_ica(
recording_id: String,
config: IcaConfig,
state: State<'_, NeuroState>,
) -> Result<IcaHandle, NeuroError> {
use rtx_neuro::signal::{Ica, IcaMethod};
let mut service = state.inner().write().await;
let recording = service.recordings.get(&recording_id)
.ok_or_else(|| NeuroError::not_found(format!("Recording {} not found", recording_id)))?;
let n_channels = recording.channel_names.len();
let n_components = config.n_components.unwrap_or(n_channels);
let method = match config.method.as_str() {
"fastica" | "fastica_logcosh" => IcaMethod::FastIcaLogcosh,
"fastica_exp" => IcaMethod::FastIcaExp,
"fastica_cube" => IcaMethod::FastIcaCube,
_ => {
return Err(NeuroError {
message: format!("Unknown ICA method: {}. Use 'fastica', 'fastica_exp', or 'fastica_cube'", config.method),
code: "INVALID_METHOD".to_string(),
});
}
};
// Ica::fit takes (data, n_components, method, max_iter, tol)
let ica = Ica::fit(
&recording.data,
Some(n_components),
method,
config.max_iter,
config.tol,
)?;
let id = service.next_id("ica");
let handle = IcaHandle {
id: id.clone(),
recording_id: recording_id.clone(),
n_components: ica.n_components(),
};
let ica_data = IcaModelData {
handle: handle.clone(),
unmixing: ica.unmixing().to_vec(),
mixing: ica.mixing().to_vec(),
};
service.ica_models.insert(id, ica_data);
Ok(handle)
}
/// Get ICA sources (independent components)
#[tauri::command]
pub async fn neuro_get_ica_sources(
ica_id: String,
start_time: Option<f64>,
end_time: Option<f64>,
state: State<'_, NeuroState>,
) -> Result<IcaSourcesDto, NeuroError> {
let service = state.inner().read().await;
let ica_data = service.ica_models.get(&ica_id)
.ok_or_else(|| NeuroError::not_found(format!("ICA model {} not found", ica_id)))?;
let recording = service.recordings.get(&ica_data.handle.recording_id)
.ok_or_else(|| NeuroError::not_found("Source recording not found"))?;
let sfreq = recording.sfreq;
let start = start_time.unwrap_or(0.0);
let end = end_time.unwrap_or(recording.handle.duration);
let start_sample = (start * sfreq).floor() as usize;
let end_sample = (end * sfreq).ceil() as usize;
// Extract data range
let data: Vec<Vec<f64>> = recording.data.iter()
.map(|ch| ch[start_sample.min(ch.len())..end_sample.min(ch.len())].to_vec())
.collect();
let n_times = data.first().map(|d| d.len()).unwrap_or(0);
let times: Vec<f64> = (0..n_times)
.map(|i| start + i as f64 / sfreq)
.collect();
// Apply unmixing: sources = unmixing * data
let n_components = ica_data.handle.n_components;
let mut sources: Vec<Vec<f64>> = vec![vec![0.0; n_times]; n_components];
for (comp_idx, unmix_row) in ica_data.unmixing.iter().enumerate().take(n_components) {
for t in 0..n_times {
let mut val = 0.0;
for (ch_idx, &w) in unmix_row.iter().enumerate() {
if ch_idx < data.len() && t < data[ch_idx].len() {
val += w * data[ch_idx][t];
}
}
sources[comp_idx][t] = val;
}
}
Ok(IcaSourcesDto {
data: sources,
times,
})
}
/// Apply ICA to remove components
#[tauri::command]
pub async fn neuro_apply_ica(
ica_id: String,
recording_id: String,
exclude: Vec<usize>,
state: State<'_, NeuroState>,
) -> Result<(), NeuroError> {
let mut service = state.inner().write().await;
// Clone ICA model data to avoid borrow issues
let ica_data = service.ica_models.get(&ica_id)
.ok_or_else(|| NeuroError::not_found(format!("ICA model {} not found", ica_id)))?
.clone();
// Clone recording data for processing
let (n_channels, n_times, original_data) = {
let recording = service.recordings.get(&recording_id)
.ok_or_else(|| NeuroError::not_found(format!("Recording {} not found", recording_id)))?;
(
recording.channel_names.len(),
recording.data.first().map(|d| d.len()).unwrap_or(0),
recording.data.clone(),
)
};
let n_components = ica_data.handle.n_components;
// 1. Get sources: S = W * X
let mut sources: Vec<Vec<f64>> = vec![vec![0.0; n_times]; n_components];
for (comp_idx, unmix_row) in ica_data.unmixing.iter().enumerate() {
for t in 0..n_times {
let mut val = 0.0;
for (ch_idx, &w) in unmix_row.iter().enumerate() {
if ch_idx < original_data.len() {
val += w * original_data[ch_idx][t];
}
}
sources[comp_idx][t] = val;
}
}
// 2. Zero out excluded components
for &comp in &exclude {
if comp < sources.len() {
sources[comp] = vec![0.0; n_times];
}
}
// 3. Reconstruct: X' = A * S' (where A is mixing matrix)
let mut reconstructed: Vec<Vec<f64>> = vec![vec![0.0; n_times]; n_channels];
for (ch_idx, mix_row) in ica_data.mixing.iter().enumerate().take(n_channels) {
for t in 0..n_times {
let mut val = 0.0;
for (comp_idx, &a) in mix_row.iter().enumerate() {
if comp_idx < sources.len() {
val += a * sources[comp_idx][t];
}
}
reconstructed[ch_idx][t] = val;
}
}
// Update recording
let recording = service.recordings.get_mut(&recording_id)
.ok_or_else(|| NeuroError::not_found(format!("Recording {} not found", recording_id)))?;
recording.data = reconstructed;
Ok(())
}
/// List all ICA models
#[tauri::command]
pub async fn neuro_list_ica_models(
state: State<'_, NeuroState>,
) -> Result<Vec<IcaHandle>, NeuroError> {
let service = state.inner().read().await;
let handles: Vec<IcaHandle> = service.ica_models.values()
.map(|i| i.handle.clone())
.collect();
Ok(handles)
}
@@ -0,0 +1,333 @@
//! File I/O commands for loading neuroimaging data.
use std::path::PathBuf;
use tauri::State;
use crate::neuro_commands::{
NeuroError, NeuroState, RecordingHandle, LoadedRecording, EventDto,
BidsDatasetHandle, BidsSubjectDto, BidsFileDto, LoadedBidsDataset,
};
/// Load an EDF/BDF file
#[tauri::command]
pub async fn neuro_load_edf(
path: String,
state: State<'_, NeuroState>,
) -> Result<RecordingHandle, NeuroError> {
use rtx_neuro::io::edf::EdfReader;
use rtx_neuro::io::NeuroReader;
let path_buf = PathBuf::from(&path);
let mut reader = EdfReader::open(&path_buf)?;
let header = reader.header().clone();
let n_channels = header.n_signals;
let sfreq = header.sfreq();
let n_samples = header.total_samples();
let duration = header.duration();
let flat_data = reader.read_all_data()?;
let data: Vec<Vec<f64>> = (0..n_channels)
.map(|ch| {
(0..n_samples)
.map(|s| flat_data[ch * n_samples + s])
.collect()
})
.collect();
let channel_names: Vec<String> = header.channels.iter()
.map(|s| s.label.clone())
.collect();
let channel_types: Vec<String> = header.channels.iter()
.map(|s| {
let label = s.label.to_uppercase();
if label.contains("EOG") { "EOG".to_string() }
else if label.contains("ECG") || label.contains("EKG") { "ECG".to_string() }
else if label.contains("EMG") { "EMG".to_string() }
else { "EEG".to_string() }
})
.collect();
let mut service = state.inner().write().await;
let id = service.next_id("rec");
let handle = RecordingHandle {
id: id.clone(),
path: path.clone(),
n_channels,
n_samples,
sfreq,
duration,
};
let recording = LoadedRecording {
handle: handle.clone(),
data,
sfreq,
channel_names,
channel_types,
events: Vec::new(),
};
service.recordings.insert(id.clone(), recording);
service.active_recording = Some(id);
Ok(handle)
}
/// Load a BrainVision file (.vhdr)
#[tauri::command]
pub async fn neuro_load_brainvision(
path: String,
state: State<'_, NeuroState>,
) -> Result<RecordingHandle, NeuroError> {
use rtx_neuro::io::brainvision::BrainVisionReader;
use rtx_neuro::io::NeuroReader;
let path_buf = PathBuf::from(&path);
let mut reader = BrainVisionReader::open(&path_buf)?;
let n_channels = reader.n_channels();
let sfreq = reader.sfreq();
let n_samples = reader.n_samples();
let duration = n_samples as f64 / sfreq;
let flat_data = reader.read_all_data()?;
let data: Vec<Vec<f64>> = (0..n_channels)
.map(|ch| {
(0..n_samples)
.map(|s| flat_data[ch * n_samples + s])
.collect()
})
.collect();
let channel_names = reader.channel_names();
let channel_types: Vec<String> = channel_names.iter()
.map(|name| {
let name_upper = name.to_uppercase();
if name_upper.contains("EOG") { "EOG".to_string() }
else if name_upper.contains("ECG") || name_upper.contains("EKG") { "ECG".to_string() }
else if name_upper.contains("EMG") { "EMG".to_string() }
else { "EEG".to_string() }
})
.collect();
let markers = reader.markers();
let events: Vec<EventDto> = markers.iter().map(|m| EventDto {
sample: m.position,
time: m.position as f64 / sfreq,
value: m.marker_type.parse().unwrap_or(0),
description: Some(m.description.clone()),
}).collect();
let mut service = state.inner().write().await;
let id = service.next_id("rec");
let handle = RecordingHandle {
id: id.clone(),
path: path.clone(),
n_channels,
n_samples,
sfreq,
duration,
};
let recording = LoadedRecording {
handle: handle.clone(),
data,
sfreq,
channel_names,
channel_types,
events,
};
service.recordings.insert(id.clone(), recording);
service.active_recording = Some(id);
Ok(handle)
}
/// Load an Elekta/Neuromag FIF file
#[tauri::command]
pub async fn neuro_load_fif(
path: String,
state: State<'_, NeuroState>,
) -> Result<RecordingHandle, NeuroError> {
use rtx_neuro::io::fif::FifReader;
use rtx_neuro::io::NeuroReader;
let path_buf = PathBuf::from(&path);
let mut reader = FifReader::open(&path_buf)?;
let (n_channels, sfreq, n_samples, duration, channel_names, channel_types) = {
let info = reader.info();
let names: Vec<String> = info.channels.iter()
.map(|c| c.name.clone())
.collect();
let types: Vec<String> = info.channels.iter()
.map(|c| c.kind.as_str().to_string())
.collect();
(info.n_channels, info.sfreq, info.n_samples(), info.duration(), names, types)
};
let flat_data = reader.read_all_data()?;
let data: Vec<Vec<f64>> = (0..n_channels)
.map(|ch| {
(0..n_samples)
.map(|s| flat_data[ch * n_samples + s])
.collect()
})
.collect();
let events: Vec<EventDto> = Vec::new();
let mut service = state.inner().write().await;
let id = service.next_id("rec");
let handle = RecordingHandle {
id: id.clone(),
path: path.clone(),
n_channels,
n_samples,
sfreq,
duration,
};
let recording = LoadedRecording {
handle: handle.clone(),
data,
sfreq,
channel_names,
channel_types,
events,
};
service.recordings.insert(id.clone(), recording);
service.active_recording = Some(id);
Ok(handle)
}
/// Load a CTF MEG dataset (.ds directory)
#[tauri::command]
pub async fn neuro_load_ctf(
path: String,
state: State<'_, NeuroState>,
) -> Result<RecordingHandle, NeuroError> {
use rtx_neuro::io::ctf::CtfReader;
use rtx_neuro::io::NeuroReader;
let path_buf = PathBuf::from(&path);
let mut reader = CtfReader::open(&path_buf)?;
let (n_channels, sfreq, n_samples, duration, channel_names, channel_types) = {
let info = reader.info();
let names: Vec<String> = info.channels.iter()
.map(|c| c.name.clone())
.collect();
let types: Vec<String> = info.channels.iter()
.map(|c| c.kind.as_str().to_string())
.collect();
(info.n_channels, info.sfreq, info.n_samples, info.duration(), names, types)
};
let flat_data = reader.read_all_data()?;
let data: Vec<Vec<f64>> = (0..n_channels)
.map(|ch| {
(0..n_samples)
.map(|s| flat_data[ch * n_samples + s])
.collect()
})
.collect();
let events: Vec<EventDto> = Vec::new();
let mut service = state.inner().write().await;
let id = service.next_id("rec");
let handle = RecordingHandle {
id: id.clone(),
path: path.clone(),
n_channels,
n_samples,
sfreq,
duration,
};
let recording = LoadedRecording {
handle: handle.clone(),
data,
sfreq,
channel_names,
channel_types,
events,
};
service.recordings.insert(id.clone(), recording);
service.active_recording = Some(id);
Ok(handle)
}
/// Open a BIDS dataset
#[tauri::command]
pub async fn neuro_load_bids(
path: String,
state: State<'_, NeuroState>,
) -> Result<BidsDatasetHandle, NeuroError> {
use rtx_neuro::io::bids::BidsDataset;
let path_buf = PathBuf::from(&path);
let dataset = BidsDataset::open(&path_buf)?;
let subjects: Vec<String> = dataset.subject_labels().iter().map(|s| s.to_string()).collect();
let mut service = state.inner().write().await;
let id = service.next_id("bids");
let handle = BidsDatasetHandle {
id: id.clone(),
path: path.clone(),
name: dataset.name().to_string(),
bids_version: dataset.bids_version().to_string(),
subjects: subjects.clone(),
};
let loaded = LoadedBidsDataset {
handle: handle.clone(),
dataset,
};
service.bids_datasets.insert(id, loaded);
Ok(handle)
}
/// Get subject info from a BIDS dataset
#[tauri::command]
pub async fn neuro_get_bids_subject(
dataset_id: String,
subject: String,
state: State<'_, NeuroState>,
) -> Result<BidsSubjectDto, NeuroError> {
let service = state.inner().read().await;
let loaded = service.bids_datasets.get(&dataset_id)
.ok_or_else(|| NeuroError::not_found(format!("BIDS dataset {} not found", dataset_id)))?;
let bids_subject = loaded.dataset.get_subject(&subject)
.ok_or_else(|| NeuroError::not_found(format!("Subject {} not found", subject)))?;
let sessions: Vec<Option<String>> = bids_subject.sessions.keys().cloned().collect();
let n_meg_files: usize = bids_subject.sessions.values().map(|s| s.meg_files.len()).sum();
let n_eeg_files: usize = bids_subject.sessions.values().map(|s| s.eeg_files.len()).sum();
Ok(BidsSubjectDto {
label: bids_subject.label.clone(),
sessions,
n_meg_files,
n_eeg_files,
})
}
@@ -0,0 +1,27 @@
//! Tauri IPC command handlers for neuro functionality.
mod io;
mod recording;
mod processing;
mod analysis;
mod realtime;
mod database;
mod ssp;
mod ica;
mod spectral;
mod stats;
mod pinn;
mod fem;
pub use io::*;
pub use recording::*;
pub use processing::*;
pub use analysis::*;
pub use realtime::*;
pub use database::*;
pub use ssp::*;
pub use ica::*;
pub use spectral::*;
pub use stats::*;
pub use pinn::*;
pub use fem::*;
@@ -0,0 +1,346 @@
//! PINN (Physics-Informed Neural Network) source localization commands.
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use serde::{Deserialize, Serialize};
use tauri::State;
use crate::neuro_commands::NeuroError;
// ============================================================================
// PINN Types
// ============================================================================
/// PINN solver configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PinnConfigDto {
/// Hidden layer sizes
pub hidden_layers: Vec<usize>,
/// Number of Fourier features
pub n_fourier_features: usize,
/// Fourier feature scale
pub fourier_scale: f64,
/// Number of collocation points
pub n_collocation: usize,
/// Learning rate
pub learning_rate: f64,
/// Weight for data fitting
pub data_weight: f64,
/// Weight for physics constraints
pub physics_weight: f64,
/// Weight for sparsity
pub sparsity_weight: f64,
/// Maximum iterations
pub max_iterations: usize,
/// Whether to learn conductivity
pub learn_conductivity: bool,
}
impl Default for PinnConfigDto {
fn default() -> Self {
Self {
hidden_layers: vec![64, 128, 128, 64],
n_fourier_features: 32,
fourier_scale: 10.0,
n_collocation: 1000,
learning_rate: 0.001,
data_weight: 1.0,
physics_weight: 0.1,
sparsity_weight: 0.01,
max_iterations: 5000,
learn_conductivity: false,
}
}
}
/// Head model configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HeadModelDto {
/// Number of tissue layers (1, 3, or 4)
pub n_layers: usize,
/// Brain conductivity (S/m)
pub brain_conductivity: f64,
/// Skull conductivity (S/m)
pub skull_conductivity: f64,
/// Scalp conductivity (S/m)
pub scalp_conductivity: f64,
}
impl Default for HeadModelDto {
fn default() -> Self {
Self {
n_layers: 3,
brain_conductivity: 0.33,
skull_conductivity: 0.0042,
scalp_conductivity: 0.43,
}
}
}
/// PINN Source estimate result
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PinnSourceEstimateDto {
/// Source positions [n_sources, 3]
pub positions: Vec<Vec<f64>>,
/// Current density [n_sources, 3]
pub current_density: Vec<Vec<f64>>,
/// Source magnitudes [n_sources]
pub magnitudes: Vec<f64>,
/// Goodness of fit (0-1)
pub gof: f64,
/// Peak source index
pub peak_source: usize,
/// Peak position [x, y, z]
pub peak_position: Vec<f64>,
/// Peak magnitude
pub peak_magnitude: f64,
}
/// Training result
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PinnTrainingResultDto {
/// Final total loss
pub final_loss: f64,
/// Final data loss
pub data_loss: f64,
/// Final physics loss
pub physics_loss: f64,
/// Number of iterations
pub iterations: usize,
/// Whether converged
pub converged: bool,
/// Loss history (sampled)
pub loss_history: Vec<f64>,
}
/// PINN solver state
pub struct PinnSolverState {
/// Configuration
pub config: PinnConfigDto,
/// Head model configuration
pub head_config: HeadModelDto,
/// Whether trained
pub trained: bool,
/// Training result
pub training_result: Option<PinnTrainingResultDto>,
}
/// State type for PINN solver
pub type PinnState = Arc<RwLock<Option<PinnSolverState>>>;
// ============================================================================
// PINN Commands
// ============================================================================
/// Initialize PINN solver with configuration
#[tauri::command]
pub async fn neuro_pinn_init(
config: PinnConfigDto,
head_config: HeadModelDto,
pinn_state: State<'_, PinnState>,
) -> Result<String, NeuroError> {
let state = PinnSolverState {
config,
head_config,
trained: false,
training_result: None,
};
*pinn_state.inner().write().await = Some(state);
Ok("PINN solver initialized".to_string())
}
/// Train PINN on sensor data
#[tauri::command]
pub async fn neuro_pinn_train(
sensor_data: Vec<f64>,
n_sensors: usize,
pinn_state: State<'_, PinnState>,
) -> Result<PinnTrainingResultDto, NeuroError> {
use rtx_neuro_pinn::{SourcePINN, SourcePINNConfig, SourceNetworkConfig};
use rtx_neuro_pinn::network::Activation;
use rtx_neuro_pinn::head_model::{HeadModel, SensorArray};
use ndarray::Array1;
let mut state_guard = pinn_state.inner().write().await;
let state = state_guard.as_mut()
.ok_or_else(|| NeuroError::pinn_error("PINN solver not initialized"))?;
// Build config
let network_config = SourceNetworkConfig {
hidden_layers: state.config.hidden_layers.clone(),
n_fourier_features: state.config.n_fourier_features,
fourier_scale: state.config.fourier_scale,
activation: Activation::Tanh,
output_dim: 3,
output_conductivity: state.config.learn_conductivity,
};
let pinn_config = SourcePINNConfig {
network: network_config,
n_collocation: state.config.n_collocation,
learning_rate: state.config.learning_rate,
data_weight: state.config.data_weight,
physics_weight: state.config.physics_weight,
sparsity_weight: state.config.sparsity_weight,
smoothness_weight: 0.001,
learn_conductivity: state.config.learn_conductivity,
max_iterations: state.config.max_iterations,
tolerance: 1e-6,
print_every: 0,
};
// Create head model
let head = HeadModel::spherical(state.head_config.n_layers)
.with_conductivity("brain", state.head_config.brain_conductivity)
.with_conductivity("skull", state.head_config.skull_conductivity)
.with_conductivity("scalp", state.head_config.scalp_conductivity);
// Create sensor array
let sensors = SensorArray::meg_helmet(n_sensors, 0.1, 0.02);
// Convert sensor data
let sensor_array = Array1::from_vec(sensor_data);
// Create and train PINN
let mut pinn = SourcePINN::new(pinn_config)
.map_err(|e| NeuroError::pinn_error(e.to_string()))?;
let result = pinn.train(&sensor_array, &head, &sensors)
.map_err(|e| NeuroError::pinn_error(e.to_string()))?;
// Sample loss history (keep at most 100 points)
let history_len = result.loss_history.len();
let step = (history_len / 100).max(1);
let sampled_history: Vec<f64> = result.loss_history
.iter()
.step_by(step)
.take(100)
.copied()
.collect();
let training_result = PinnTrainingResultDto {
final_loss: result.final_loss,
data_loss: result.data_loss,
physics_loss: result.physics_loss,
iterations: result.iterations,
converged: result.converged,
loss_history: sampled_history,
};
state.trained = true;
state.training_result = Some(training_result.clone());
Ok(training_result)
}
/// Quick source estimation without full PINN training
#[tauri::command]
pub async fn neuro_pinn_quick_estimate(
sensor_data: Vec<f64>,
n_sensors: usize,
n_sources: usize,
pinn_state: State<'_, PinnState>,
) -> Result<PinnSourceEstimateDto, NeuroError> {
use rtx_neuro_pinn::solver::quick_estimate;
use rtx_neuro_pinn::head_model::{HeadModel, SensorArray};
use ndarray::Array1;
let state_guard = pinn_state.inner().read().await;
let state = state_guard.as_ref()
.ok_or_else(|| NeuroError::pinn_error("PINN solver not initialized"))?;
// Create head model
let head = HeadModel::spherical(state.head_config.n_layers)
.with_conductivity("brain", state.head_config.brain_conductivity)
.with_conductivity("skull", state.head_config.skull_conductivity)
.with_conductivity("scalp", state.head_config.scalp_conductivity);
// Create sensor array
let sensors = SensorArray::meg_helmet(n_sensors, 0.1, 0.02);
// Convert sensor data
let sensor_array = Array1::from_vec(sensor_data);
// Quick estimate
let estimate = quick_estimate(&sensor_array, &head, &sensors, n_sources)
.map_err(|e| NeuroError::pinn_error(e.to_string()))?;
// Convert to DTO
let n = estimate.positions.nrows();
let mut positions = Vec::with_capacity(n);
let mut current_density = Vec::with_capacity(n);
for i in 0..n {
positions.push(vec![
estimate.positions[[i, 0]],
estimate.positions[[i, 1]],
estimate.positions[[i, 2]],
]);
current_density.push(vec![
estimate.current_density[[i, 0]],
estimate.current_density[[i, 1]],
estimate.current_density[[i, 2]],
]);
}
let magnitudes: Vec<f64> = estimate.magnitudes.to_vec();
// Find peak
let (peak_source, &peak_magnitude) = magnitudes.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
.unwrap_or((0, &0.0));
let peak_position = if peak_source < positions.len() {
positions[peak_source].clone()
} else {
vec![0.0, 0.0, 0.0]
};
Ok(PinnSourceEstimateDto {
positions,
current_density,
magnitudes,
gof: estimate.gof,
peak_source,
peak_position,
peak_magnitude,
})
}
/// Get current PINN state
#[tauri::command]
pub async fn neuro_pinn_status(
pinn_state: State<'_, PinnState>,
) -> Result<HashMap<String, serde_json::Value>, NeuroError> {
let state = pinn_state.inner().read().await;
let mut status = HashMap::new();
if let Some(s) = state.as_ref() {
status.insert("initialized".to_string(), serde_json::Value::Bool(true));
status.insert("trained".to_string(), serde_json::Value::Bool(s.trained));
status.insert("n_layers".to_string(), serde_json::Value::Number(serde_json::Number::from(s.head_config.n_layers)));
status.insert("learn_conductivity".to_string(), serde_json::Value::Bool(s.config.learn_conductivity));
if let Some(ref result) = s.training_result {
status.insert("final_loss".to_string(), serde_json::json!(result.final_loss));
status.insert("iterations".to_string(), serde_json::Value::Number(serde_json::Number::from(result.iterations)));
status.insert("converged".to_string(), serde_json::Value::Bool(result.converged));
}
} else {
status.insert("initialized".to_string(), serde_json::Value::Bool(false));
}
Ok(status)
}
/// Reset PINN solver
#[tauri::command]
pub async fn neuro_pinn_reset(
pinn_state: State<'_, PinnState>,
) -> Result<(), NeuroError> {
*pinn_state.inner().write().await = None;
Ok(())
}
@@ -0,0 +1,596 @@
//! Signal processing commands.
use tauri::State;
use crate::neuro_commands::{
NeuroError, NeuroState, FilterConfig, EpochConfig, EpochsHandle, EvokedData,
ForwardConfig, ForwardHandle, ForwardModel, InverseConfig, InverseHandle,
InverseOperator, SourceEstimateDto, ConnectivityConfig, ConnectivityResultDto,
SspConfig, SspHandle, SspProjectorData, IcaConfig, IcaHandle, IcaModelData,
IcaSourcesDto, EpochsData,
};
/// Apply filter to recording
#[tauri::command]
pub async fn neuro_apply_filter(
recording_id: String,
config: FilterConfig,
state: State<'_, NeuroState>,
) -> Result<(), NeuroError> {
use rtx_neuro::signal::{bandpass, highpass, lowpass, notch, FilterMethod};
let mut service = state.inner().write().await;
let recording = service.recordings.get_mut(&recording_id)
.ok_or_else(|| NeuroError::not_found(format!("Recording {} not found", recording_id)))?;
let sfreq = recording.sfreq;
let method = match config.method.as_str() {
"butterworth" => FilterMethod::Butterworth,
"fir" => FilterMethod::FirZeroPhase,
_ => FilterMethod::Fft,
};
for ch_data in recording.data.iter_mut() {
let filtered = match (config.low_freq, config.high_freq) {
(Some(low), Some(high)) => bandpass(ch_data, sfreq, low, high, method)?,
(Some(low), None) => highpass(ch_data, sfreq, low, method)?,
(None, Some(high)) => lowpass(ch_data, sfreq, high, method)?,
(None, None) => ch_data.clone(),
};
*ch_data = filtered;
}
if let Some(notch_freqs) = config.notch_freqs {
if !notch_freqs.is_empty() {
for ch_data in recording.data.iter_mut() {
let filtered = notch(ch_data, sfreq, &notch_freqs, 1.0)?;
*ch_data = filtered;
}
}
}
Ok(())
}
/// Create epochs from events
#[tauri::command]
pub async fn neuro_create_epochs(
recording_id: String,
config: EpochConfig,
state: State<'_, NeuroState>,
) -> Result<EpochsHandle, NeuroError> {
use rtx_neuro::signal::baseline_correct;
let mut service = state.inner().write().await;
let (sfreq, n_channels, data_len, events, data, channel_names) = {
let recording = service.recordings.get(&recording_id)
.ok_or_else(|| NeuroError::not_found(format!("Recording {} not found", recording_id)))?;
let events: Vec<_> = recording.events.iter()
.filter(|e| config.event_ids.contains(&e.value))
.cloned()
.collect();
(
recording.sfreq,
recording.data.len(),
recording.data.first().map(|d| d.len()).unwrap_or(0),
events,
recording.data.clone(),
recording.channel_names.clone(),
)
};
if events.is_empty() {
return Err(NeuroError {
message: "No events found matching the specified IDs".to_string(),
code: "NO_EVENTS".to_string(),
});
}
let tmin_samples = (config.tmin * sfreq).round() as isize;
let tmax_samples = (config.tmax * sfreq).round() as isize;
let n_times = (tmax_samples - tmin_samples) as usize;
let times: Vec<f64> = (0..n_times)
.map(|i| config.tmin + i as f64 / sfreq)
.collect();
let mut epochs_data: Vec<Vec<Vec<f64>>> = Vec::new();
for event in &events {
let event_sample = event.sample as isize;
let start_sample = event_sample + tmin_samples;
let end_sample = event_sample + tmax_samples;
if start_sample < 0 || end_sample as usize > data_len {
continue;
}
let start = start_sample as usize;
let end = end_sample as usize;
let mut epoch: Vec<Vec<f64>> = Vec::with_capacity(n_channels);
for ch_data in &data {
let mut ch_epoch = ch_data[start..end].to_vec();
if let Some((bl_start, bl_end)) = config.baseline {
let bl_start_idx = ((bl_start - config.tmin) * sfreq).round() as usize;
let bl_end_idx = ((bl_end - config.tmin) * sfreq).round() as usize;
ch_epoch = baseline_correct(
&ch_epoch,
bl_start_idx,
bl_end_idx.min(ch_epoch.len()),
rtx_neuro::signal::BaselineMethod::Mean,
)?;
}
epoch.push(ch_epoch);
}
epochs_data.push(epoch);
}
if epochs_data.is_empty() {
return Err(NeuroError {
message: "No valid epochs could be created (events too close to boundaries)".to_string(),
code: "NO_EPOCHS".to_string(),
});
}
let id = service.next_id("epochs");
let handle = EpochsHandle {
id: id.clone(),
recording_id: recording_id.clone(),
n_epochs: epochs_data.len(),
n_channels,
n_times,
times: times.clone(),
};
let epochs = EpochsData {
handle: handle.clone(),
data: epochs_data,
times,
channel_names,
};
service.epochs.insert(id, epochs);
Ok(handle)
}
/// Compute average (evoked response) from epochs
#[tauri::command]
pub async fn neuro_compute_average(
epochs_id: String,
state: State<'_, NeuroState>,
) -> Result<EvokedData, NeuroError> {
let service = state.inner().read().await;
let epochs = service.epochs.get(&epochs_id)
.ok_or_else(|| NeuroError::not_found(format!("Epochs {} not found", epochs_id)))?;
let n_epochs = epochs.data.len();
let n_channels = epochs.handle.n_channels;
let n_times = epochs.handle.n_times;
let mut avg_data: Vec<Vec<f64>> = vec![vec![0.0; n_times]; n_channels];
for epoch in &epochs.data {
for (ch_idx, ch_data) in epoch.iter().enumerate() {
for (t_idx, &val) in ch_data.iter().enumerate() {
avg_data[ch_idx][t_idx] += val;
}
}
}
for ch_data in avg_data.iter_mut() {
for val in ch_data.iter_mut() {
*val /= n_epochs as f64;
}
}
Ok(EvokedData {
channels: epochs.channel_names.clone(),
times: epochs.times.clone(),
data: avg_data,
n_averaged: n_epochs,
})
}
/// Create a forward model (spherical MEG or EEG)
#[tauri::command]
pub async fn neuro_create_forward_model(
recording_id: String,
config: ForwardConfig,
state: State<'_, NeuroState>,
) -> Result<ForwardHandle, NeuroError> {
use rtx_neuro_forward::{SphericalMeg, SphericalEeg, SourceSpace, SensorArray};
let mut service = state.inner().write().await;
let recording = service.recordings.get(&recording_id)
.ok_or_else(|| NeuroError::not_found(format!("Recording {} not found", recording_id)))?;
let n_channels = recording.channel_names.len();
let channel_names = recording.channel_names.clone();
let source_space = SourceSpace::create_spherical_shell(
config.sphere_origin,
config.sphere_radius * 0.85,
config.n_sources,
);
let n_sources = source_space.len();
let source_positions: Vec<[f64; 3]> = source_space.iter()
.map(|src| {
let pos = src.position();
[pos.x, pos.y, pos.z]
})
.collect();
let (gain_data, sensor_positions, model_type_str) = match config.model_type.as_str() {
"spherical_meg" => {
let meg_model = SphericalMeg::new(
config.sphere_origin,
config.sphere_radius,
)?;
let sensor_array = SensorArray::meg_helmet(n_channels, config.sphere_radius * 1.2);
let gain = meg_model.compute_gain(&source_space, &sensor_array)?;
let sens_pos: Vec<[f64; 3]> = sensor_array.iter()
.map(|s| {
let pos = s.position();
[pos.x, pos.y, pos.z]
})
.collect();
(gain, sens_pos, "spherical_meg".to_string())
}
"spherical_eeg" => {
let eeg_model = SphericalEeg::new(
config.sphere_origin,
[config.sphere_radius * 0.88, config.sphere_radius * 0.92, config.sphere_radius],
[0.33, 0.0042, 0.33],
)?;
let sensor_array = SensorArray::eeg_10_20(config.sphere_radius);
let gain = eeg_model.compute_gain(&source_space, &sensor_array)?;
let sens_pos: Vec<[f64; 3]> = sensor_array.iter()
.map(|s| {
let pos = s.position();
[pos.x, pos.y, pos.z]
})
.collect();
(gain, sens_pos, "spherical_eeg".to_string())
}
_ => {
return Err(NeuroError {
message: format!("Unknown model type: {}. Use 'spherical_meg' or 'spherical_eeg'", config.model_type),
code: "INVALID_CONFIG".to_string(),
});
}
};
let n_sensors = gain_data.len();
let n_source_cols = gain_data.first().map(|r| r.len()).unwrap_or(0);
let id = service.next_id("fwd");
let handle = ForwardHandle {
id: id.clone(),
model_type: model_type_str,
n_sensors,
n_sources,
gain_shape: [n_sensors, n_source_cols],
};
let forward_model = ForwardModel {
handle: handle.clone(),
gain: gain_data,
source_positions,
sensor_positions,
};
service.forward_models.insert(id, forward_model);
Ok(handle)
}
/// Get forward model info
#[tauri::command]
pub async fn neuro_get_forward_model(
forward_id: String,
state: State<'_, NeuroState>,
) -> Result<ForwardHandle, NeuroError> {
let service = state.inner().read().await;
let forward = service.forward_models.get(&forward_id)
.ok_or_else(|| NeuroError::not_found(format!("Forward model {} not found", forward_id)))?;
Ok(forward.handle.clone())
}
/// Create an inverse operator from a forward model
#[tauri::command]
pub async fn neuro_create_inverse_operator(
forward_id: String,
recording_id: String,
config: InverseConfig,
state: State<'_, NeuroState>,
) -> Result<InverseHandle, NeuroError> {
use rtx_neuro_forward::GainMatrix;
use rtx_neuro_inverse::{MneInverse, InverseMethod, Covariance, CovarianceType};
let mut service = state.inner().write().await;
let (gain_data, source_positions, n_channels) = {
let forward = service.forward_models.get(&forward_id)
.ok_or_else(|| NeuroError::not_found(format!("Forward model {} not found", forward_id)))?;
(forward.gain.clone(), forward.source_positions.clone(), forward.handle.n_sensors)
};
let channel_names = {
let recording = service.recordings.get(&recording_id)
.ok_or_else(|| NeuroError::not_found(format!("Recording {} not found", recording_id)))?;
recording.channel_names.clone()
};
let gain = GainMatrix::new(gain_data.clone(), true, channel_names.clone())?;
let noise_cov = Covariance::identity(n_channels, CovarianceType::Noise);
let method = match config.method.as_str() {
"mne" => InverseMethod::Mne,
"dspm" => InverseMethod::Dspm,
"sloreta" => InverseMethod::Sloreta,
_ => {
return Err(NeuroError {
message: format!("Unknown inverse method: {}. Use 'mne', 'dspm', or 'sloreta'", config.method),
code: "INVALID_CONFIG".to_string(),
});
}
};
let mne_inverse = MneInverse::make_inverse(
&gain,
&noise_cov,
method,
config.loose,
config.depth,
config.lambda2,
)?;
let kernel = mne_inverse.kernel();
let kernel_data: Vec<Vec<f64>> = (0..kernel.nrows())
.map(|i| (0..kernel.ncols()).map(|j| kernel[(i, j)]).collect())
.collect();
let id = service.next_id("inv");
let handle = InverseHandle {
id: id.clone(),
forward_id: forward_id.clone(),
method: config.method.clone(),
n_sources: mne_inverse.n_sources(),
n_channels,
};
let inverse_op = InverseOperator {
handle: handle.clone(),
kernel: kernel_data,
source_positions,
};
service.inverse_operators.insert(id, inverse_op);
Ok(handle)
}
/// Apply inverse operator to get source estimate
#[tauri::command]
pub async fn neuro_apply_inverse(
inverse_id: String,
epochs_id: Option<String>,
recording_id: Option<String>,
start_time: Option<f64>,
end_time: Option<f64>,
state: State<'_, NeuroState>,
) -> Result<SourceEstimateDto, NeuroError> {
let service = state.inner().read().await;
let inverse = service.inverse_operators.get(&inverse_id)
.ok_or_else(|| NeuroError::not_found(format!("Inverse operator {} not found", inverse_id)))?;
let (data, _sfreq, times): (Vec<Vec<f64>>, f64, Vec<f64>) = if let Some(epochs_id) = epochs_id {
let epochs = service.epochs.get(&epochs_id)
.ok_or_else(|| NeuroError::not_found(format!("Epochs {} not found", epochs_id)))?;
let n_epochs = epochs.data.len();
let n_channels = epochs.handle.n_channels;
let n_times = epochs.handle.n_times;
let mut averaged = vec![vec![0.0; n_times]; n_channels];
for epoch in &epochs.data {
for (ch, ch_data) in epoch.iter().enumerate() {
for (t, &val) in ch_data.iter().enumerate() {
averaged[ch][t] += val / n_epochs as f64;
}
}
}
let recording = service.recordings.get(&epochs.handle.recording_id)
.ok_or_else(|| NeuroError::not_found("Source recording not found"))?;
(averaged, recording.sfreq, epochs.handle.times.clone())
} else if let Some(recording_id) = recording_id {
let recording = service.recordings.get(&recording_id)
.ok_or_else(|| NeuroError::not_found(format!("Recording {} not found", recording_id)))?;
let sfreq = recording.sfreq;
let n_samples = recording.data.first().map(|d| d.len()).unwrap_or(0);
let t_start = start_time.map(|t| (t * sfreq) as usize).unwrap_or(0);
let t_end = end_time.map(|t| (t * sfreq) as usize).unwrap_or(n_samples);
let data: Vec<Vec<f64>> = recording.data.iter()
.map(|ch| ch[t_start..t_end.min(ch.len())].to_vec())
.collect();
let times: Vec<f64> = (0..data[0].len())
.map(|i| (t_start + i) as f64 / sfreq)
.collect();
(data, sfreq, times)
} else {
return Err(NeuroError {
message: "Either epochs_id or recording_id must be provided".to_string(),
code: "MISSING_INPUT".to_string(),
});
};
let n_sources = inverse.kernel.len();
let n_times = data[0].len();
let mut source_data = vec![vec![0.0; n_times]; n_sources];
for (src_idx, kernel_row) in inverse.kernel.iter().enumerate() {
for (ch_idx, &k) in kernel_row.iter().enumerate() {
if ch_idx < data.len() {
for (t, &val) in data[ch_idx].iter().enumerate() {
source_data[src_idx][t] += k * val;
}
}
}
}
Ok(SourceEstimateDto {
data: source_data,
times,
source_positions: inverse.source_positions.clone(),
method: inverse.handle.method.clone(),
})
}
/// List all forward models
#[tauri::command]
pub async fn neuro_list_forward_models(
state: State<'_, NeuroState>,
) -> Result<Vec<ForwardHandle>, NeuroError> {
let service = state.inner().read().await;
Ok(service.forward_models.values().map(|f| f.handle.clone()).collect())
}
/// List all inverse operators
#[tauri::command]
pub async fn neuro_list_inverse_operators(
state: State<'_, NeuroState>,
) -> Result<Vec<InverseHandle>, NeuroError> {
let service = state.inner().read().await;
Ok(service.inverse_operators.values().map(|i| i.handle.clone()).collect())
}
// Note: neuro_list_ica_models moved to ica.rs to avoid duplicate definition
/// Compute connectivity between channels
#[tauri::command]
pub async fn neuro_compute_connectivity(
epochs_id: String,
config: ConnectivityConfig,
state: State<'_, NeuroState>,
) -> Result<ConnectivityResultDto, NeuroError> {
use rtx_neuro_connectivity::{spectral_connectivity, ConnectivityMethod};
let service = state.inner().read().await;
let epochs = service.epochs.get(&epochs_id)
.ok_or_else(|| NeuroError::not_found(format!("Epochs {} not found", epochs_id)))?;
let recording = service.recordings.get(&epochs.handle.recording_id)
.ok_or_else(|| NeuroError::not_found("Source recording not found"))?;
let sfreq = recording.sfreq;
let n_epochs = epochs.data.len();
let method = match config.method.as_str() {
"coherence" => ConnectivityMethod::Coherence,
"imaginary_coherence" => ConnectivityMethod::ImaginaryCoherence,
"plv" => ConnectivityMethod::Plv,
"wpli" => ConnectivityMethod::Wpli,
"dwpli" => ConnectivityMethod::DwPli,
_ => {
return Err(NeuroError {
message: format!("Unknown connectivity method: {}", config.method),
code: "INVALID_METHOD".to_string(),
});
}
};
let result = spectral_connectivity(
&epochs.data,
method,
sfreq,
config.fmin,
config.fmax,
config.n_fft,
)?;
Ok(ConnectivityResultDto {
data: result.data.clone(),
freqs: result.freqs.clone(),
sources: result.sources.clone(),
targets: result.targets.clone(),
method: config.method,
n_epochs,
})
}
/// Compute phase-amplitude coupling
#[tauri::command]
pub async fn neuro_compute_pac(
epochs_id: String,
phase_channel: usize,
_amp_channel: usize,
phase_freq: (f64, f64),
amp_freq: (f64, f64),
state: State<'_, NeuroState>,
) -> Result<f64, NeuroError> {
use rtx_neuro_connectivity::{phase_amplitude_coupling, PacMethod};
let service = state.inner().read().await;
let epochs = service.epochs.get(&epochs_id)
.ok_or_else(|| NeuroError::not_found(format!("Epochs {} not found", epochs_id)))?;
let recording = service.recordings.get(&epochs.handle.recording_id)
.ok_or_else(|| NeuroError::not_found("Source recording not found"))?;
let sfreq = recording.sfreq;
let channel = phase_channel;
let mut signal_data: Vec<f64> = Vec::new();
for epoch in &epochs.data {
if channel < epoch.len() {
signal_data.extend(&epoch[channel]);
}
}
let pac = phase_amplitude_coupling(
&signal_data,
sfreq,
phase_freq,
amp_freq,
PacMethod::ModulationIndex,
)?;
Ok(pac)
}
// Note: The following commands are implemented in their respective modules:
// - SSP commands: ssp.rs (neuro_compute_ssp, neuro_apply_ssp)
// - ICA commands: ica.rs (neuro_fit_ica, neuro_get_ica_sources, neuro_apply_ica)
// - Spectral commands: spectral.rs (neuro_compute_tfr, neuro_compute_psd, neuro_compute_tfr_epochs)
// - Beamformer: Planned for future implementation in source_localization.rs
@@ -0,0 +1,360 @@
//! Real-time streaming commands (LSL, BCI).
use std::collections::HashMap;
use tauri::State;
use crate::neuro_commands::{
NeuroError, LslState, BciState, DataChunk,
LslStreamInfoDto, LslHandleDto, BciConfigDto, BciPipelineHandle,
ControlSignalDto, LatencyStatsDto,
};
// ============================================================================
// LSL Commands
// ============================================================================
/// Discover available LSL streams
#[tauri::command]
pub async fn neuro_lsl_discover(
timeout_sec: f64,
lsl_state: State<'_, LslState>,
) -> Result<Vec<LslStreamInfoDto>, NeuroError> {
let service = lsl_state.inner().read().await;
let streams = service.discover_streams(timeout_sec).await.map_err(NeuroError::from)?;
Ok(streams.into_iter().map(|s| LslStreamInfoDto {
name: s.name,
stream_type: s.stream_type,
channel_count: s.channel_count,
nominal_srate: s.nominal_srate,
uid: s.uid,
}).collect())
}
/// Connect to an LSL stream
#[tauri::command]
pub async fn neuro_lsl_connect(
name: String,
stream_type: String,
channel_count: usize,
srate: f64,
buffer_duration: f64,
lsl_state: State<'_, LslState>,
) -> Result<LslHandleDto, NeuroError> {
use rtx_neuro_lsl::LslStreamInfo;
let info = LslStreamInfo::new(&name, &stream_type, channel_count, srate);
let info_clone = info.clone();
let mut service = lsl_state.inner().write().await;
let handle = service.connect(info, buffer_duration).await.map_err(NeuroError::from)?;
Ok(LslHandleDto {
id: handle.id,
info: LslStreamInfoDto {
name: info_clone.name,
stream_type: info_clone.stream_type,
channel_count: info_clone.channel_count,
nominal_srate: info_clone.nominal_srate,
uid: info_clone.uid,
},
connected: handle.connected,
samples_received: handle.samples_received,
})
}
/// Disconnect from an LSL stream
#[tauri::command]
pub async fn neuro_lsl_disconnect(
stream_id: String,
lsl_state: State<'_, LslState>,
) -> Result<(), NeuroError> {
let mut service = lsl_state.inner().write().await;
service.disconnect(&stream_id).await.map_err(NeuroError::from)?;
Ok(())
}
/// Get data from an LSL stream
#[tauri::command]
pub async fn neuro_lsl_get_data(
stream_id: String,
duration_sec: f64,
lsl_state: State<'_, LslState>,
) -> Result<DataChunk, NeuroError> {
let service = lsl_state.inner().read().await;
let (data, times) = service.get_data(&stream_id, duration_sec).await.map_err(NeuroError::from)?;
let info = service.get_stream_info(&stream_id).ok_or_else(|| NeuroError::not_found("Stream not found"))?;
let channel_names: Vec<String> = (0..info.channel_count)
.map(|i| format!("Ch{}", i + 1))
.collect();
let n_samples = data.len();
let n_channels = if n_samples > 0 { data[0].len() } else { 0 };
let transposed: Vec<Vec<f64>> = (0..n_channels)
.map(|ch| data.iter().map(|sample| sample[ch]).collect())
.collect();
Ok(DataChunk {
channels: channel_names,
times,
data: transposed,
})
}
/// List active LSL connections
#[tauri::command]
pub async fn neuro_lsl_list_active(
lsl_state: State<'_, LslState>,
) -> Result<Vec<LslHandleDto>, NeuroError> {
let service = lsl_state.inner().read().await;
let handles = service.list_active();
Ok(handles.into_iter().map(|h| LslHandleDto {
id: h.id,
info: LslStreamInfoDto {
name: h.info.name,
stream_type: h.info.stream_type,
channel_count: h.info.channel_count,
nominal_srate: h.info.nominal_srate,
uid: h.info.uid,
},
connected: h.connected,
samples_received: h.samples_received,
}).collect())
}
/// Check if an LSL stream is connected
#[tauri::command]
pub async fn neuro_lsl_is_connected(
stream_id: String,
lsl_state: State<'_, LslState>,
) -> Result<bool, NeuroError> {
let service = lsl_state.inner().read().await;
Ok(service.is_connected(&stream_id))
}
/// Get sample count for an LSL stream
#[tauri::command]
pub async fn neuro_lsl_get_sample_count(
stream_id: String,
lsl_state: State<'_, LslState>,
) -> Result<usize, NeuroError> {
let service = lsl_state.inner().read().await;
let count = service.get_sample_count(&stream_id).await.map_err(NeuroError::from)?;
Ok(count)
}
/// Clear buffer for an LSL stream
#[tauri::command]
pub async fn neuro_lsl_clear_buffer(
stream_id: String,
lsl_state: State<'_, LslState>,
) -> Result<(), NeuroError> {
let service = lsl_state.inner().read().await;
service.clear_buffer(&stream_id).await.map_err(NeuroError::from)?;
Ok(())
}
// ============================================================================
// BCI Pipeline Commands
// ============================================================================
/// Create a new BCI pipeline
#[tauri::command]
pub async fn neuro_bci_create(
config: BciConfigDto,
bci_state: State<'_, BciState>,
) -> Result<BciPipelineHandle, NeuroError> {
use rtx_neuro_realtime::BciConfig;
let bci_config = BciConfig {
n_channels: config.n_channels,
sample_rate: config.sample_rate,
buffer_duration_ms: config.buffer_duration_ms,
window_size_ms: config.window_size_ms,
stride_ms: config.stride_ms,
filter_low: config.filter_low,
filter_high: config.filter_high,
monitor_sources: config.monitor_sources.clone(),
threshold: config.threshold,
source_weights: vec![1.0; config.monitor_sources.len()],
baseline_duration: 2.0,
smoothing: config.smoothing,
};
let pipeline = rtx_neuro_realtime::BciPipeline::new(bci_config)
.map_err(|e| NeuroError::bci_error(e.to_string()))?;
let handle = BciPipelineHandle {
id: "bci-pipeline".to_string(),
state: pipeline.state().to_string(),
config: config.clone(),
baseline_calibrated: pipeline.is_baseline_calibrated(),
};
*bci_state.inner().write().await = Some(pipeline);
Ok(handle)
}
/// Start the BCI pipeline
#[tauri::command]
pub async fn neuro_bci_start(
bci_state: State<'_, BciState>,
) -> Result<(), NeuroError> {
let mut pipeline = bci_state.inner().write().await;
let pipeline = pipeline.as_mut().ok_or_else(NeuroError::bci_not_created)?;
pipeline.start().map_err(|e| NeuroError::bci_error(e.to_string()))?;
Ok(())
}
/// Stop the BCI pipeline
#[tauri::command]
pub async fn neuro_bci_stop(
bci_state: State<'_, BciState>,
) -> Result<(), NeuroError> {
let mut pipeline = bci_state.inner().write().await;
let pipeline = pipeline.as_mut().ok_or_else(NeuroError::bci_not_created)?;
pipeline.stop().map_err(|e| NeuroError::bci_error(e.to_string()))?;
Ok(())
}
/// Push sample data to BCI pipeline
#[tauri::command]
pub async fn neuro_bci_push_sample(
data: Vec<f64>,
timestamp: f64,
bci_state: State<'_, BciState>,
) -> Result<(), NeuroError> {
let pipeline = bci_state.inner().read().await;
let pipeline = pipeline.as_ref().ok_or_else(NeuroError::bci_not_created)?;
pipeline.push_sample(data, timestamp);
Ok(())
}
/// Get control signal from BCI pipeline (non-blocking)
#[tauri::command]
pub async fn neuro_bci_get_control_signal(
bci_state: State<'_, BciState>,
) -> Result<Option<ControlSignalDto>, NeuroError> {
let pipeline = bci_state.inner().read().await;
let pipeline = pipeline.as_ref().ok_or_else(NeuroError::bci_not_created)?;
let signal = pipeline.process();
Ok(signal.map(|s| ControlSignalDto {
source_indices: s.source_indices,
power: s.power,
control_values: s.control_values,
combined_signal: s.combined_signal,
threshold_crossed: s.threshold_crossed,
timestamp: s.timestamp,
latency_ms: s.latency_ms,
}))
}
/// Start baseline calibration
#[tauri::command]
pub async fn neuro_bci_start_calibration(
bci_state: State<'_, BciState>,
) -> Result<(), NeuroError> {
let pipeline = bci_state.inner().read().await;
let pipeline = pipeline.as_ref().ok_or_else(NeuroError::bci_not_created)?;
pipeline.start_baseline_calibration();
Ok(())
}
/// Check if baseline is calibrated
#[tauri::command]
pub async fn neuro_bci_is_calibrated(
bci_state: State<'_, BciState>,
) -> Result<bool, NeuroError> {
let pipeline = bci_state.inner().read().await;
let pipeline = pipeline.as_ref().ok_or_else(NeuroError::bci_not_created)?;
Ok(pipeline.is_baseline_calibrated())
}
/// Set BCI threshold
#[tauri::command]
pub async fn neuro_bci_set_threshold(
threshold: f64,
bci_state: State<'_, BciState>,
) -> Result<(), NeuroError> {
let mut pipeline = bci_state.inner().write().await;
let pipeline = pipeline.as_mut().ok_or_else(NeuroError::bci_not_created)?;
pipeline.set_threshold(threshold);
Ok(())
}
/// Set BCI smoothing factor
#[tauri::command]
pub async fn neuro_bci_set_smoothing(
smoothing: f64,
bci_state: State<'_, BciState>,
) -> Result<(), NeuroError> {
let mut pipeline = bci_state.inner().write().await;
let pipeline = pipeline.as_mut().ok_or_else(NeuroError::bci_not_created)?;
pipeline.set_smoothing(smoothing);
Ok(())
}
/// Get BCI latency statistics
#[tauri::command]
pub async fn neuro_bci_latency_stats(
bci_state: State<'_, BciState>,
) -> Result<Vec<LatencyStatsDto>, NeuroError> {
let pipeline = bci_state.inner().read().await;
let pipeline = pipeline.as_ref().ok_or_else(NeuroError::bci_not_created)?;
let stats = pipeline.latency_stats();
Ok(stats.into_iter().map(|s| LatencyStatsDto {
stage: s.stage.to_string(),
mean_us: s.mean_us,
p95_us: s.p95_us,
p99_us: s.p99_us,
count: s.count,
}).collect())
}
/// Get BCI pipeline status
#[tauri::command]
pub async fn neuro_bci_status(
bci_state: State<'_, BciState>,
) -> Result<BciPipelineHandle, NeuroError> {
let pipeline = bci_state.inner().read().await;
let pipeline = pipeline.as_ref().ok_or_else(NeuroError::bci_not_created)?;
Ok(BciPipelineHandle {
id: "bci-pipeline".to_string(),
state: pipeline.state().to_string(),
config: BciConfigDto::default(),
baseline_calibrated: pipeline.is_baseline_calibrated(),
})
}
/// Destroy BCI pipeline
#[tauri::command]
pub async fn neuro_bci_destroy(
bci_state: State<'_, BciState>,
) -> Result<(), NeuroError> {
let mut pipeline = bci_state.inner().write().await;
*pipeline = None;
Ok(())
}
@@ -0,0 +1,251 @@
//! Recording management commands.
use tauri::State;
use crate::neuro_commands::{
NeuroError, NeuroState, RecordingHandle, RecordingInfoDto, ChannelInfoDto,
DataChunk, EventDto, NeuroStatus, BidsDatasetHandle, BidsFileDto,
};
/// Get files from a BIDS dataset for a subject/session
#[tauri::command]
pub async fn neuro_get_bids_files(
dataset_id: String,
subject: String,
session: Option<String>,
modality: Option<String>,
state: State<'_, NeuroState>,
) -> Result<Vec<BidsFileDto>, NeuroError> {
let service = state.inner().read().await;
let loaded = service.bids_datasets.get(&dataset_id)
.ok_or_else(|| NeuroError::not_found(format!("BIDS dataset {} not found", dataset_id)))?;
let mut files = Vec::new();
let include_meg = modality.as_deref().map(|m| m == "meg").unwrap_or(true);
let include_eeg = modality.as_deref().map(|m| m == "eeg").unwrap_or(true);
if include_meg {
let meg_files = loaded.dataset.get_meg_files(&subject, session.as_deref());
for file in meg_files {
files.push(BidsFileDto {
path: file.path.to_string_lossy().to_string(),
subject: file.entities.subject.clone(),
session: file.entities.session.clone(),
task: file.entities.task.clone(),
run: file.entities.run,
datatype: file.entities.datatype.clone(),
extension: file.entities.extension.clone(),
has_sidecar: file.sidecar_path.is_some(),
has_channels: file.channels_path.is_some(),
has_events: file.events_path.is_some(),
});
}
}
if include_eeg {
let eeg_files = loaded.dataset.get_eeg_files(&subject, session.as_deref());
for file in eeg_files {
files.push(BidsFileDto {
path: file.path.to_string_lossy().to_string(),
subject: file.entities.subject.clone(),
session: file.entities.session.clone(),
task: file.entities.task.clone(),
run: file.entities.run,
datatype: file.entities.datatype.clone(),
extension: file.entities.extension.clone(),
has_sidecar: file.sidecar_path.is_some(),
has_channels: file.channels_path.is_some(),
has_events: file.events_path.is_some(),
});
}
}
Ok(files)
}
/// List all loaded BIDS datasets
#[tauri::command]
pub async fn neuro_list_bids_datasets(
state: State<'_, NeuroState>,
) -> Result<Vec<BidsDatasetHandle>, NeuroError> {
let service = state.inner().read().await;
Ok(service.bids_datasets.values().map(|d| d.handle.clone()).collect())
}
/// Close a BIDS dataset
#[tauri::command]
pub async fn neuro_close_bids(
dataset_id: String,
state: State<'_, NeuroState>,
) -> Result<(), NeuroError> {
let mut service = state.inner().write().await;
service.bids_datasets.remove(&dataset_id);
Ok(())
}
/// Get recording info
#[tauri::command]
pub async fn neuro_get_recording_info(
recording_id: String,
state: State<'_, NeuroState>,
) -> Result<RecordingInfoDto, NeuroError> {
let service = state.inner().read().await;
let recording = service.recordings.get(&recording_id)
.ok_or_else(|| NeuroError::not_found(format!("Recording {} not found", recording_id)))?;
let channels: Vec<ChannelInfoDto> = recording.channel_names.iter()
.zip(recording.channel_types.iter())
.map(|(name, ch_type)| ChannelInfoDto {
name: name.clone(),
channel_type: ch_type.clone(),
unit: "µV".to_string(),
is_bad: false,
})
.collect();
let event_types: Vec<String> = recording.events.iter()
.filter_map(|e| e.description.clone())
.collect::<std::collections::HashSet<_>>()
.into_iter()
.collect();
let format = if recording.handle.path.ends_with(".vhdr") {
"BrainVision"
} else if recording.handle.path.to_lowercase().ends_with(".bdf") {
"BDF"
} else {
"EDF"
};
Ok(RecordingInfoDto {
format: format.to_string(),
subject_id: None,
recording_date: None,
channels,
n_events: recording.events.len(),
event_types,
})
}
/// Get a chunk of data for visualization
#[tauri::command]
pub async fn neuro_get_data_chunk(
recording_id: String,
start_time: f64,
end_time: f64,
channels: Option<Vec<String>>,
state: State<'_, NeuroState>,
) -> Result<DataChunk, NeuroError> {
let service = state.inner().read().await;
let recording = service.recordings.get(&recording_id)
.ok_or_else(|| NeuroError::not_found(format!("Recording {} not found", recording_id)))?;
let sfreq = recording.sfreq;
let start_sample = (start_time * sfreq).floor() as usize;
let end_sample = (end_time * sfreq).ceil() as usize;
let channel_indices: Vec<usize> = if let Some(ref names) = channels {
names.iter().filter_map(|name| {
recording.channel_names.iter().position(|n| n == name)
}).collect()
} else {
(0..recording.channel_names.len()).collect()
};
let data: Vec<Vec<f64>> = channel_indices.iter().map(|&idx| {
let ch_data = &recording.data[idx];
let start = start_sample.min(ch_data.len());
let end = end_sample.min(ch_data.len());
ch_data[start..end].to_vec()
}).collect();
let n_samples = data.first().map(|d| d.len()).unwrap_or(0);
let times: Vec<f64> = (0..n_samples)
.map(|i| start_time + i as f64 / sfreq)
.collect();
let channel_names: Vec<String> = channel_indices.iter()
.map(|&idx| recording.channel_names[idx].clone())
.collect();
Ok(DataChunk {
channels: channel_names,
times,
data,
})
}
/// Get events from recording
#[tauri::command]
pub async fn neuro_get_events(
recording_id: String,
state: State<'_, NeuroState>,
) -> Result<Vec<EventDto>, NeuroError> {
let service = state.inner().read().await;
let recording = service.recordings.get(&recording_id)
.ok_or_else(|| NeuroError::not_found(format!("Recording {} not found", recording_id)))?;
Ok(recording.events.clone())
}
/// Get neuro service status
#[tauri::command]
pub async fn neuro_status(
state: State<'_, NeuroState>,
) -> Result<NeuroStatus, NeuroError> {
let service = state.inner().read().await;
Ok(service.status())
}
/// Reset neuro service (clear all data)
#[tauri::command]
pub async fn neuro_reset(
state: State<'_, NeuroState>,
) -> Result<(), NeuroError> {
let mut service = state.inner().write().await;
service.reset();
Ok(())
}
/// Close a specific recording
#[tauri::command]
pub async fn neuro_close_recording(
recording_id: String,
state: State<'_, NeuroState>,
) -> Result<(), NeuroError> {
let mut service = state.inner().write().await;
service.recordings.remove(&recording_id);
let epochs_to_remove: Vec<String> = service.epochs.iter()
.filter(|(_, e)| e.handle.recording_id == recording_id)
.map(|(id, _)| id.clone())
.collect();
for id in epochs_to_remove {
service.epochs.remove(&id);
}
if service.active_recording.as_ref() == Some(&recording_id) {
service.active_recording = None;
}
Ok(())
}
/// List all loaded recordings
#[tauri::command]
pub async fn neuro_list_recordings(
state: State<'_, NeuroState>,
) -> Result<Vec<RecordingHandle>, NeuroError> {
let service = state.inner().read().await;
let handles: Vec<RecordingHandle> = service.recordings.values()
.map(|r| r.handle.clone())
.collect();
Ok(handles)
}
@@ -0,0 +1,319 @@
//! Time-frequency and spectral analysis commands.
use tauri::State;
use crate::neuro_commands::{
NeuroError, NeuroState, TfrConfig, TfrResultDto, PsdConfig, PsdResultDto,
};
// ============================================================================
// Time-Frequency Commands
// ============================================================================
/// Compute time-frequency representation using Morlet wavelets or STFT
#[tauri::command]
pub async fn neuro_compute_tfr(
recording_id: String,
config: TfrConfig,
start_time: Option<f64>,
end_time: Option<f64>,
state: State<'_, NeuroState>,
) -> Result<TfrResultDto, NeuroError> {
use rtx_neuro::signal::{tfr_morlet, stft, TfrOutput, Window};
let service = state.inner().read().await;
let recording = service.recordings.get(&recording_id)
.ok_or_else(|| NeuroError::not_found(format!("Recording not found: {}", recording_id)))?;
let sfreq = recording.sfreq;
let n_samples = recording.data[0].len();
// Determine time range
let start_sample = start_time
.map(|t| (t * sfreq) as usize)
.unwrap_or(0)
.min(n_samples);
let end_sample = end_time
.map(|t| (t * sfreq) as usize)
.unwrap_or(n_samples)
.min(n_samples);
// Select channels
let channel_indices: Vec<usize> = config
.channels
.unwrap_or_else(|| (0..recording.data.len()).collect());
// Extract data for selected channels and time range
let data: Vec<Vec<f64>> = channel_indices
.iter()
.filter_map(|&i| {
recording.data.get(i).map(|ch| ch[start_sample..end_sample].to_vec())
})
.collect();
if data.is_empty() {
return Err(NeuroError {
message: "No valid channels selected".to_string(),
code: "INVALID_CHANNELS".to_string(),
});
}
// Parse output type
let output = match config.output.to_lowercase().as_str() {
"power" => TfrOutput::Power,
"phase" => TfrOutput::Phase,
"complex" => TfrOutput::Complex,
"itc" => TfrOutput::Itc,
_ => TfrOutput::Power,
};
// Compute TFR based on method
let result = match config.method.to_lowercase().as_str() {
"stft" => {
let n_fft = 256;
let stft_result = stft(&data, sfreq, n_fft, None, Window::Hann)?;
// Filter frequencies to match requested freqs (approximately)
let power = stft_result.power();
TfrResultDto {
data: power,
freqs: stft_result.freqs,
times: stft_result.times,
output: "power".to_string(),
}
}
_ => {
// Default to Morlet
let tfr_result = tfr_morlet(
&data,
sfreq,
&config.freqs,
config.n_cycles,
output,
)?;
TfrResultDto {
data: tfr_result.data,
freqs: tfr_result.freqs,
times: tfr_result.times,
output: config.output.clone(),
}
}
};
Ok(result)
}
/// Compute power spectral density
#[tauri::command]
pub async fn neuro_compute_psd(
recording_id: String,
config: PsdConfig,
start_time: Option<f64>,
end_time: Option<f64>,
state: State<'_, NeuroState>,
) -> Result<PsdResultDto, NeuroError> {
use rtx_neuro::signal::{psd_welch, psd_multitaper, Window};
let service = state.inner().read().await;
let recording = service.recordings.get(&recording_id)
.ok_or_else(|| NeuroError::not_found(format!("Recording not found: {}", recording_id)))?;
let sfreq = recording.sfreq;
let n_samples = recording.data[0].len();
// Determine time range
let start_sample = start_time
.map(|t| (t * sfreq) as usize)
.unwrap_or(0)
.min(n_samples);
let end_sample = end_time
.map(|t| (t * sfreq) as usize)
.unwrap_or(n_samples)
.min(n_samples);
// Select channels
let channel_indices: Vec<usize> = config
.channels
.clone()
.unwrap_or_else(|| (0..recording.data.len()).collect());
// Extract data for selected channels and time range
let data: Vec<Vec<f64>> = channel_indices
.iter()
.filter_map(|&i| {
recording.data.get(i).map(|ch| ch[start_sample..end_sample].to_vec())
})
.collect();
let channel_names: Vec<String> = channel_indices
.iter()
.filter_map(|&i| recording.channel_names.get(i).cloned())
.collect();
if data.is_empty() {
return Err(NeuroError {
message: "No valid channels selected".to_string(),
code: "INVALID_CHANNELS".to_string(),
});
}
// Compute PSD based on method
let result = match config.method.to_lowercase().as_str() {
"multitaper" => {
let bandwidth = config.bandwidth.unwrap_or(4.0);
let psd_result = psd_multitaper(
&data,
sfreq,
bandwidth,
config.fmin,
config.fmax,
)?;
PsdResultDto {
psd: psd_result.psd,
freqs: psd_result.freqs,
channels: channel_names,
}
}
_ => {
// Default to Welch
let psd_result = psd_welch(
&data,
sfreq,
config.n_fft,
config.n_overlap,
Window::Hann,
)?;
// Apply frequency limits if specified
let (psd_filtered, freqs_filtered) = if config.fmin.is_some() || config.fmax.is_some() {
let fmin = config.fmin.unwrap_or(0.0);
let fmax = config.fmax.unwrap_or(sfreq / 2.0);
let mask: Vec<bool> = psd_result.freqs
.iter()
.map(|&f| f >= fmin && f <= fmax)
.collect();
let freqs: Vec<f64> = psd_result.freqs
.iter()
.zip(&mask)
.filter_map(|(&f, &m)| if m { Some(f) } else { None })
.collect();
let psd: Vec<Vec<f64>> = psd_result.psd
.iter()
.map(|ch| {
ch.iter()
.zip(&mask)
.filter_map(|(&p, &m)| if m { Some(p) } else { None })
.collect()
})
.collect();
(psd, freqs)
} else {
(psd_result.psd, psd_result.freqs)
};
PsdResultDto {
psd: psd_filtered,
freqs: freqs_filtered,
channels: channel_names,
}
}
};
Ok(result)
}
/// Compute TFR on epochs data
#[tauri::command]
pub async fn neuro_compute_tfr_epochs(
epochs_id: String,
config: TfrConfig,
state: State<'_, NeuroState>,
) -> Result<TfrResultDto, NeuroError> {
use rtx_neuro::signal::{tfr_morlet, TfrOutput};
let service = state.inner().read().await;
let epochs = service.epochs.get(&epochs_id)
.ok_or_else(|| NeuroError::not_found(format!("Epochs not found: {}", epochs_id)))?;
let sfreq = epochs.times.len() as f64 / (epochs.times.last().unwrap_or(&1.0) - epochs.times.first().unwrap_or(&0.0));
// Parse output type
let output = match config.output.to_lowercase().as_str() {
"power" => TfrOutput::Power,
"phase" => TfrOutput::Phase,
"itc" => TfrOutput::Itc,
_ => TfrOutput::Power,
};
// Average TFR across epochs
let n_epochs = epochs.data.len();
let n_channels = if n_epochs > 0 { epochs.data[0].len() } else { 0 };
if n_epochs == 0 || n_channels == 0 {
return Err(NeuroError {
message: "Empty epochs data".to_string(),
code: "EMPTY_DATA".to_string(),
});
}
// Compute TFR for first epoch to get dimensions
let first_epoch_data: Vec<Vec<f64>> = epochs.data[0].clone();
let first_result = tfr_morlet(
&first_epoch_data,
sfreq,
&config.freqs,
config.n_cycles,
output,
)?;
let n_freqs = first_result.freqs.len();
let n_times = first_result.times.len();
// Initialize accumulators
let mut avg_data = vec![vec![vec![0.0; n_times]; n_freqs]; n_channels];
// Accumulate all epochs
for epoch_data in &epochs.data {
let tfr_result = tfr_morlet(
epoch_data,
sfreq,
&config.freqs,
config.n_cycles,
output,
)?;
for (ch_idx, ch_tfr) in tfr_result.data.iter().enumerate() {
for (f_idx, freq_data) in ch_tfr.iter().enumerate() {
for (t_idx, &val) in freq_data.iter().enumerate() {
avg_data[ch_idx][f_idx][t_idx] += val;
}
}
}
}
// Average
for ch in &mut avg_data {
for freq in ch {
for val in freq {
*val /= n_epochs as f64;
}
}
}
Ok(TfrResultDto {
data: avg_data,
freqs: first_result.freqs,
times: first_result.times,
output: config.output,
})
}
@@ -0,0 +1,118 @@
//! SSP (Signal Space Projection) artifact removal commands.
use tauri::State;
use crate::neuro_commands::{
NeuroError, NeuroState, SspConfig, SspHandle, SspProjectorData,
};
// ============================================================================
// SSP Commands
// ============================================================================
/// Compute SSP projectors from artifact epochs
#[tauri::command]
pub async fn neuro_compute_ssp(
epochs_id: String,
config: SspConfig,
state: State<'_, NeuroState>,
) -> Result<SspHandle, NeuroError> {
use rtx_neuro_signal::SspProjector;
let mut service = state.inner().write().await;
// Clone data to avoid borrow issues
let (epochs_data, recording_id) = {
let epochs = service.epochs.get(&epochs_id)
.ok_or_else(|| NeuroError::not_found(format!("Epochs {} not found", epochs_id)))?;
(epochs.data.clone(), epochs.handle.recording_id.clone())
};
// Compute SSP from epochs (name parameter is "artifact")
let ssp = SspProjector::from_epochs(&epochs_data, config.n_components, "artifact")?;
let id = service.next_id("ssp");
let handle = SspHandle {
id: id.clone(),
recording_id,
n_projectors: ssp.n_projectors(),
explained_var: ssp.explained_var().to_vec(),
};
let projector_data = SspProjectorData {
handle: handle.clone(),
vectors: ssp.vectors().to_vec(),
active: vec![true; ssp.n_projectors()],
};
service.ssp_projectors.insert(id, projector_data);
Ok(handle)
}
/// Apply SSP projectors to recording
#[tauri::command]
pub async fn neuro_apply_ssp(
ssp_id: String,
recording_id: String,
components: Vec<usize>,
state: State<'_, NeuroState>,
) -> Result<(), NeuroError> {
let mut service = state.inner().write().await;
// Clone SSP projector vectors in a block to end the immutable borrow
let ssp_vectors = {
let ssp_ref = service.ssp_projectors.get(&ssp_id)
.ok_or_else(|| NeuroError::not_found(format!("SSP projectors {} not found", ssp_id)))?;
ssp_ref.vectors.clone()
};
// Now we can safely get mutable borrow for recording
let recording = service.recordings.get_mut(&recording_id)
.ok_or_else(|| NeuroError::not_found(format!("Recording {} not found", recording_id)))?;
let n_channels = recording.channel_names.len();
let n_samples = recording.data.first().map(|d| d.len()).unwrap_or(0);
// Apply SSP manually: P = I - Σ(u_i * u_i^T), y = P * x
// For selected components only
let mut result = recording.data.clone();
for &comp_idx in &components {
if comp_idx >= ssp_vectors.len() {
continue;
}
let vec = &ssp_vectors[comp_idx];
if vec.len() != n_channels {
continue;
}
for t in 0..n_samples {
// Compute dot product: u^T * x
let dot: f64 = (0..n_channels)
.map(|ch| vec[ch] * recording.data[ch][t])
.sum();
// Subtract: x = x - (u^T * x) * u
for ch in 0..n_channels {
result[ch][t] -= dot * vec[ch];
}
}
}
recording.data = result;
Ok(())
}
/// List all SSP projectors
#[tauri::command]
pub async fn neuro_list_ssp_projectors(
state: State<'_, NeuroState>,
) -> Result<Vec<SspHandle>, NeuroError> {
let service = state.inner().read().await;
let handles: Vec<SspHandle> = service.ssp_projectors.values()
.map(|s| s.handle.clone())
.collect();
Ok(handles)
}
@@ -0,0 +1,278 @@
//! Statistical analysis commands.
use serde::{Deserialize, Serialize};
use tauri::State;
use crate::neuro_commands::{NeuroError, NeuroState};
// ============================================================================
// Statistical Types
// ============================================================================
/// Configuration for permutation test
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PermutationTestConfig {
/// Type of test: "1samp", "paired", "ind"
pub test_type: String,
/// Population mean for 1-sample test
pub popmean: Option<f64>,
/// Number of permutations
pub n_permutations: usize,
/// Tail: "two-sided", "less", "greater"
pub tail: String,
/// Optional random seed
pub seed: Option<u64>,
}
/// Result of permutation test
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PermutationTestResult {
/// Test statistic
pub statistic: f64,
/// P-value
pub pvalue: f64,
/// Number of permutations performed
pub n_permutations: usize,
}
/// Configuration for t-test
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TTestConfig {
/// Type of test: "1samp", "paired", "ind"
pub test_type: String,
/// Population mean for 1-sample test
pub popmean: Option<f64>,
}
/// Result of t-test
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TTestResultDto {
/// T-statistic
pub statistic: f64,
/// P-value
pub pvalue: f64,
/// Degrees of freedom
pub df: f64,
/// Mean (or mean difference)
pub mean: f64,
/// Standard error
pub se: f64,
/// 95% confidence interval
pub ci_95: (f64, f64),
}
/// Configuration for multiple comparison correction
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CorrectionConfig {
/// Method: "fdr", "bonferroni", "holm"
pub method: String,
/// Significance level
pub alpha: f64,
}
/// Result of multiple comparison correction
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CorrectionResult {
/// Which hypotheses to reject
pub reject: Vec<bool>,
/// Corrected p-values
pub pvalues_corrected: Vec<f64>,
/// Number of significant tests
pub n_significant: usize,
}
// ============================================================================
// Statistical Commands
// ============================================================================
/// Perform permutation test on epochs data
#[tauri::command]
pub async fn neuro_permutation_test(
state: State<'_, NeuroState>,
epochs_id: String,
config: PermutationTestConfig,
) -> Result<PermutationTestResult, NeuroError> {
use rtx_neuro_stats::{
permutation_test_1samp, Tail,
};
let service = state.inner().read().await;
let epochs = service.epochs.get(&epochs_id)
.ok_or_else(|| NeuroError::not_found(format!("Epochs set not found: {}", epochs_id)))?;
if epochs.data.is_empty() {
return Err(NeuroError {
message: "No epochs data available".to_string(),
code: "EMPTY_DATA".to_string(),
});
}
let tail = match config.tail.as_str() {
"less" => Tail::Less,
"greater" => Tail::Greater,
_ => Tail::TwoSided,
};
// Flatten epochs to a single vector (average across time/channels)
let data: Vec<f64> = epochs.data
.iter()
.map(|epoch: &Vec<Vec<f64>>| {
let sum: f64 = epoch.iter().flat_map(|ch: &Vec<f64>| ch.iter()).sum();
let count = epoch.iter().map(|ch: &Vec<f64>| ch.len()).sum::<usize>();
sum / count as f64
})
.collect();
let popmean = config.popmean.unwrap_or(0.0);
let result = permutation_test_1samp(&data, popmean, config.n_permutations, tail, config.seed)
.map_err(|e| NeuroError {
message: e.to_string(),
code: "STATS_ERROR".to_string(),
})?;
Ok(PermutationTestResult {
statistic: result.statistic,
pvalue: result.pvalue,
n_permutations: result.n_permutations,
})
}
/// Perform t-test on epochs data
#[tauri::command]
pub async fn neuro_ttest(
state: State<'_, NeuroState>,
epochs_id: String,
config: TTestConfig,
) -> Result<TTestResultDto, NeuroError> {
use rtx_neuro_stats::parametric::ttest_1samp;
let service = state.inner().read().await;
let epochs = service.epochs.get(&epochs_id)
.ok_or_else(|| NeuroError::not_found(format!("Epochs set not found: {}", epochs_id)))?;
if epochs.data.is_empty() {
return Err(NeuroError {
message: "No epochs data available".to_string(),
code: "EMPTY_DATA".to_string(),
});
}
// Flatten epochs to a single vector (average across time/channels)
let data: Vec<f64> = epochs.data
.iter()
.map(|epoch: &Vec<Vec<f64>>| {
let sum: f64 = epoch.iter().flat_map(|ch: &Vec<f64>| ch.iter()).sum();
let count = epoch.iter().map(|ch: &Vec<f64>| ch.len()).sum::<usize>();
sum / count as f64
})
.collect();
let popmean = config.popmean.unwrap_or(0.0);
let result = ttest_1samp(&data, popmean)
.map_err(|e| NeuroError {
message: e.to_string(),
code: "STATS_ERROR".to_string(),
})?;
Ok(TTestResultDto {
statistic: result.statistic,
pvalue: result.pvalue,
df: result.df,
mean: result.mean,
se: result.se,
ci_95: result.ci_95,
})
}
/// Apply multiple comparison correction to p-values
#[tauri::command]
pub async fn neuro_correct_pvalues(
pvalues: Vec<f64>,
config: CorrectionConfig,
) -> Result<CorrectionResult, NeuroError> {
use rtx_neuro_stats::correction::{correct_pvalues, CorrectionMethod};
let method = match config.method.as_str() {
"bonferroni" => CorrectionMethod::Bonferroni,
"holm" => CorrectionMethod::Holm,
"none" => CorrectionMethod::None,
_ => CorrectionMethod::Fdr,
};
let (reject, pvalues_corrected) = correct_pvalues(&pvalues, config.alpha, method);
let n_significant = reject.iter().filter(|&&r| r).count();
Ok(CorrectionResult {
reject,
pvalues_corrected,
n_significant,
})
}
/// Compute effect size (Cohen's d) for epochs
#[tauri::command]
pub async fn neuro_effect_size(
state: State<'_, NeuroState>,
epochs_id_a: String,
epochs_id_b: Option<String>,
) -> Result<f64, NeuroError> {
use rtx_neuro_stats::effect_size::{cohens_d, cohens_d_paired};
let service = state.inner().read().await;
let epochs_a = service.epochs.get(&epochs_id_a)
.ok_or_else(|| NeuroError::not_found(format!("Epochs set A not found: {}", epochs_id_a)))?;
// Average each epoch
let data_a: Vec<f64> = epochs_a.data
.iter()
.map(|epoch: &Vec<Vec<f64>>| {
let sum: f64 = epoch.iter().flat_map(|ch: &Vec<f64>| ch.iter()).sum();
let count = epoch.iter().map(|ch: &Vec<f64>| ch.len()).sum::<usize>();
sum / count as f64
})
.collect();
if let Some(id_b) = epochs_id_b {
let epochs_b = service.epochs.get(&id_b)
.ok_or_else(|| NeuroError::not_found(format!("Epochs set B not found: {}", id_b)))?;
let data_b: Vec<f64> = epochs_b.data
.iter()
.map(|epoch: &Vec<Vec<f64>>| {
let sum: f64 = epoch.iter().flat_map(|ch: &Vec<f64>| ch.iter()).sum();
let count = epoch.iter().map(|ch: &Vec<f64>| ch.len()).sum::<usize>();
sum / count as f64
})
.collect();
if data_a.len() == data_b.len() {
// Paired samples
let result = cohens_d_paired(&data_a, &data_b)
.map_err(|e| NeuroError {
message: e.to_string(),
code: "STATS_ERROR".to_string(),
})?;
Ok(result.value)
} else {
// Independent samples
let result = cohens_d(&data_a, &data_b)
.map_err(|e| NeuroError {
message: e.to_string(),
code: "STATS_ERROR".to_string(),
})?;
Ok(result.value)
}
} else {
// One-sample: compare to zero
let zeros = vec![0.0; data_a.len()];
let result = cohens_d(&data_a, &zeros)
.map_err(|e| NeuroError {
message: e.to_string(),
code: "STATS_ERROR".to_string(),
})?;
Ok(result.value)
}
}
@@ -0,0 +1,168 @@
//! Error types for neuro IPC commands.
use serde::Serialize;
/// Error type for neuro IPC commands
#[derive(Debug, Serialize)]
pub struct NeuroError {
/// Error message
pub message: String,
/// Error code
pub code: String,
}
impl From<rtx_neuro::NeuroError> for NeuroError {
fn from(err: rtx_neuro::NeuroError) -> Self {
Self {
message: err.to_string(),
code: "NEURO_ERROR".to_string(),
}
}
}
impl From<std::io::Error> for NeuroError {
fn from(err: std::io::Error) -> Self {
Self {
message: err.to_string(),
code: "IO_ERROR".to_string(),
}
}
}
impl From<rtx_neuro::io::IoError> for NeuroError {
fn from(err: rtx_neuro::io::IoError) -> Self {
Self {
message: err.to_string(),
code: "IO_ERROR".to_string(),
}
}
}
impl From<rtx_neuro::signal::SignalError> for NeuroError {
fn from(err: rtx_neuro::signal::SignalError) -> Self {
Self {
message: err.to_string(),
code: "SIGNAL_ERROR".to_string(),
}
}
}
impl From<rtx_neuro_forward::ForwardError> for NeuroError {
fn from(err: rtx_neuro_forward::ForwardError) -> Self {
Self {
message: err.to_string(),
code: "FORWARD_ERROR".to_string(),
}
}
}
impl From<rtx_neuro_inverse::InverseError> for NeuroError {
fn from(err: rtx_neuro_inverse::InverseError) -> Self {
Self {
message: err.to_string(),
code: "INVERSE_ERROR".to_string(),
}
}
}
impl From<rtx_neuro_connectivity::ConnectivityError> for NeuroError {
fn from(err: rtx_neuro_connectivity::ConnectivityError) -> Self {
Self {
message: err.to_string(),
code: "CONNECTIVITY_ERROR".to_string(),
}
}
}
impl From<rtx_neuro_fem::FemError> for NeuroError {
fn from(err: rtx_neuro_fem::FemError) -> Self {
Self {
message: err.to_string(),
code: "FEM_ERROR".to_string(),
}
}
}
impl From<rtx_neuro_lsl::LslError> for NeuroError {
fn from(err: rtx_neuro_lsl::LslError) -> Self {
Self {
message: err.to_string(),
code: "LSL_ERROR".to_string(),
}
}
}
impl From<rtx_neuro_db::DatabaseError> for NeuroError {
fn from(err: rtx_neuro_db::DatabaseError) -> Self {
Self {
message: err.to_string(),
code: "DATABASE_ERROR".to_string(),
}
}
}
impl NeuroError {
/// Create a FEM error
pub fn fem_error(msg: impl Into<String>) -> Self {
Self {
message: msg.into(),
code: "FEM_ERROR".to_string(),
}
}
/// Create a not found error
pub fn not_found(msg: impl Into<String>) -> Self {
Self {
message: msg.into(),
code: "NOT_FOUND".to_string(),
}
}
/// Create a BCI error
pub fn bci_error(msg: impl Into<String>) -> Self {
Self {
message: msg.into(),
code: "BCI_ERROR".to_string(),
}
}
/// Create a GNN error
pub fn gnn_error(msg: impl Into<String>) -> Self {
Self {
message: msg.into(),
code: "GNN_ERROR".to_string(),
}
}
/// Create a detector not initialized error
pub fn detector_not_init() -> Self {
Self {
message: "Artifact detector not initialized".to_string(),
code: "DETECTOR_NOT_INIT".to_string(),
}
}
/// Create a BCI not created error
pub fn bci_not_created() -> Self {
Self {
message: "BCI pipeline not created".to_string(),
code: "BCI_NOT_CREATED".to_string(),
}
}
/// Create a PINN error
pub fn pinn_error(msg: impl Into<String>) -> Self {
Self {
message: msg.into(),
code: "PINN_ERROR".to_string(),
}
}
/// Create a stats error
pub fn stats_error(msg: impl Into<String>) -> Self {
Self {
message: msg.into(),
code: "STATS_ERROR".to_string(),
}
}
}
@@ -0,0 +1,21 @@
//! Neuro IPC commands for MEG/EEG analysis
//!
//! This module provides Tauri IPC commands for the RustyNeuro neuroimaging
//! analysis platform, enabling loading of EDF/BDF files, signal processing,
//! epoching, and visualization data streaming.
// Type definitions
mod types;
mod error;
mod service;
// Command modules
mod commands;
// Re-export types for public API
pub use types::*;
pub use error::*;
pub use service::*;
// Re-export all commands
pub use commands::*;
@@ -0,0 +1,181 @@
//! Neuro service for state management.
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use super::types::*;
/// Loaded recording with metadata
pub struct LoadedRecording {
pub handle: RecordingHandle,
pub data: Vec<Vec<f64>>,
pub sfreq: f64,
pub channel_names: Vec<String>,
pub channel_types: Vec<String>,
pub events: Vec<EventDto>,
}
/// Loaded BIDS dataset storage
pub struct LoadedBidsDataset {
pub handle: BidsDatasetHandle,
pub dataset: rtx_neuro::io::bids::BidsDataset,
}
/// Epochs data storage
pub struct EpochsData {
pub handle: EpochsHandle,
/// [n_epochs x n_channels x n_times]
pub data: Vec<Vec<Vec<f64>>>,
pub times: Vec<f64>,
pub channel_names: Vec<String>,
}
/// Forward model storage
pub struct ForwardModel {
pub handle: ForwardHandle,
/// Gain matrix [n_sensors x n_sources*3]
pub gain: Vec<Vec<f64>>,
/// Source positions [n_sources x 3]
pub source_positions: Vec<[f64; 3]>,
/// Sensor positions [n_sensors x 3]
pub sensor_positions: Vec<[f64; 3]>,
}
/// Inverse operator storage
pub struct InverseOperator {
pub handle: InverseHandle,
/// Inverse kernel [n_sources x n_channels]
pub kernel: Vec<Vec<f64>>,
/// Source positions
pub source_positions: Vec<[f64; 3]>,
}
/// SSP projector storage
pub struct SspProjectorData {
pub handle: SspHandle,
/// Projector vectors [n_projectors x n_channels]
pub vectors: Vec<Vec<f64>>,
/// Active flags
pub active: Vec<bool>,
}
/// ICA model storage
#[derive(Clone)]
pub struct IcaModelData {
pub handle: IcaHandle,
/// Unmixing matrix [n_components x n_channels]
pub unmixing: Vec<Vec<f64>>,
/// Mixing matrix [n_channels x n_components]
pub mixing: Vec<Vec<f64>>,
}
/// Loaded FreeSurfer subject storage
pub struct LoadedFreeSurferSubject {
pub handle: FreeSurferHandleDto,
pub subject: rtx_neuro_anatomy::FreeSurferSubject,
}
/// Handle to artifact detector
pub struct ArtifactDetectorHandle {
pub config: ArtifactDetectorConfigDto,
}
/// Holds GNN model and metadata
pub struct GnnModelHolder {
pub config: GnnModelConfigDto,
pub n_parameters: usize,
}
/// Neuro service managing recordings and processing
pub struct NeuroService {
pub(crate) recordings: HashMap<String, LoadedRecording>,
pub(crate) epochs: HashMap<String, EpochsData>,
pub(crate) forward_models: HashMap<String, ForwardModel>,
pub(crate) inverse_operators: HashMap<String, InverseOperator>,
pub(crate) ssp_projectors: HashMap<String, SspProjectorData>,
pub(crate) ica_models: HashMap<String, IcaModelData>,
pub(crate) bids_datasets: HashMap<String, LoadedBidsDataset>,
pub(crate) freesurfer_subjects: HashMap<String, LoadedFreeSurferSubject>,
pub(crate) active_recording: Option<String>,
pub(crate) counter: usize,
}
impl NeuroService {
/// Create a new neuro service
pub fn new() -> Self {
Self {
recordings: HashMap::new(),
epochs: HashMap::new(),
forward_models: HashMap::new(),
inverse_operators: HashMap::new(),
ssp_projectors: HashMap::new(),
ica_models: HashMap::new(),
bids_datasets: HashMap::new(),
freesurfer_subjects: HashMap::new(),
active_recording: None,
counter: 0,
}
}
/// Generate a unique ID
pub fn next_id(&mut self, prefix: &str) -> String {
self.counter += 1;
format!("{}_{}", prefix, self.counter)
}
/// Get service status
pub fn status(&self) -> NeuroStatus {
let memory_usage = self.recordings.values().map(|r| {
r.data.iter().map(|ch| ch.len() * std::mem::size_of::<f64>()).sum::<usize>()
}).sum::<usize>() + self.epochs.values().map(|e| {
e.data.iter().map(|ep| {
ep.iter().map(|ch| ch.len() * std::mem::size_of::<f64>()).sum::<usize>()
}).sum::<usize>()
}).sum::<usize>();
NeuroStatus {
n_recordings: self.recordings.len(),
active_recording: self.active_recording.clone(),
n_epochs_sets: self.epochs.len(),
memory_usage,
}
}
/// Reset the service, clearing all data
pub fn reset(&mut self) {
self.recordings.clear();
self.epochs.clear();
self.forward_models.clear();
self.inverse_operators.clear();
self.ssp_projectors.clear();
self.ica_models.clear();
self.bids_datasets.clear();
self.freesurfer_subjects.clear();
self.active_recording = None;
}
}
impl Default for NeuroService {
fn default() -> Self {
Self::new()
}
}
/// Type alias for the neuro state used by Tauri commands
pub type NeuroState = Arc<RwLock<NeuroService>>;
/// State for LSL service
pub type LslState = Arc<RwLock<rtx_neuro_lsl::LslService>>;
/// BCI pipeline state
pub type BciState = Arc<RwLock<Option<rtx_neuro_realtime::BciPipeline>>>;
/// State for artifact detector
pub type ArtifactDetectorState = Arc<RwLock<Option<ArtifactDetectorHandle>>>;
/// GNN state for managing models
pub type GnnModelState = Arc<RwLock<Option<GnnModelHolder>>>;
/// Separate state for the database to avoid async/sync conflicts
pub type DatabaseState = Arc<RwLock<Option<rtx_neuro_db::NeuroDatabase>>>;
@@ -0,0 +1,222 @@
//! Analysis types for neuro commands (artifact detection, GNN).
use serde::{Deserialize, Serialize};
/// Artifact detection configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArtifactDetectorConfigDto {
/// Detection threshold (0.0 - 1.0)
pub threshold: f64,
/// Window size in samples
pub window_size: usize,
/// Overlap between windows (0.0 - 1.0)
pub overlap: f64,
/// Number of channels
pub n_channels: usize,
/// Sampling frequency
pub sfreq: f64,
}
/// Artifact type DTO
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArtifactTypeDto {
/// Artifact type name
pub name: String,
/// Short code
pub code: String,
/// Index
pub index: usize,
/// Color (RGB)
pub color: (u8, u8, u8),
}
/// Artifact label DTO
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArtifactLabelDto {
/// Artifact type name
pub artifact_type: String,
/// Detection probability
pub probability: f64,
/// Confidence score
pub confidence: f64,
}
/// Artifact region DTO
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArtifactRegionDto {
/// Artifact type name
pub artifact_type: String,
/// Start time in seconds
pub start_time: f64,
/// End time in seconds
pub end_time: f64,
/// Affected channels
pub channels: Vec<usize>,
/// Probability
pub probability: f64,
/// Severity
pub severity: f64,
}
/// Artifact detection result DTO
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArtifactDetectionResultDto {
/// Artifact labels
pub labels: Vec<ArtifactLabelDto>,
/// Start time
pub start_time: f64,
/// End time
pub end_time: f64,
}
/// Artifact batch detection result DTO
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArtifactBatchResultDto {
/// Results for each window
pub results: Vec<ArtifactDetectionResultDto>,
/// Processing time in ms
pub processing_time_ms: f64,
/// Number of windows
pub n_windows: usize,
}
/// Artifact summary DTO
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArtifactSummaryDto {
/// Total artifacts detected
pub total_artifacts: usize,
/// Counts per artifact type
pub counts: Vec<(String, usize)>,
/// Total contaminated duration
pub total_duration: f64,
/// Contamination percentage
pub contamination_percent: f64,
/// Most common artifact type
pub most_common: Option<String>,
/// Most affected channels
pub most_affected_channels: Vec<usize>,
}
/// Brain hemisphere
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum HemisphereDto {
Left,
Right,
Midline,
Unknown,
}
/// Brain region
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum BrainRegionDto {
Frontal,
Central,
Temporal,
Parietal,
Occipital,
Unknown,
}
/// Node in brain graph
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BrainNodeDto {
pub index: usize,
pub name: String,
pub hemisphere: HemisphereDto,
pub region: BrainRegionDto,
pub position: [f32; 3],
pub features: Vec<f64>,
}
/// Edge in brain graph
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BrainEdgeDto {
pub source: usize,
pub target: usize,
pub weight: f64,
pub features: Vec<f64>,
pub interhemispheric: bool,
}
/// Brain graph DTO
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BrainGraphDto {
pub nodes: Vec<BrainNodeDto>,
pub edges: Vec<BrainEdgeDto>,
pub n_nodes: usize,
pub n_edges: usize,
}
/// GNN model type
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum GnnModelTypeDto {
BrainNetCNN,
BrainGAT,
BrainTransformer,
}
/// GNN model configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GnnModelConfigDto {
pub model_type: GnnModelTypeDto,
pub n_channels: usize,
pub n_features: usize,
pub hidden_dims: Vec<usize>,
pub n_classes: usize,
pub n_heads: Option<Vec<usize>>,
pub dropout: f64,
}
impl Default for GnnModelConfigDto {
fn default() -> Self {
Self {
model_type: GnnModelTypeDto::BrainNetCNN,
n_channels: 64,
n_features: 1,
hidden_dims: vec![32, 64],
n_classes: 2,
n_heads: None,
dropout: 0.5,
}
}
}
/// GNN prediction result
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GnnPredictionDto {
pub logits: Vec<f64>,
pub predicted_class: usize,
pub probabilities: Vec<f64>,
}
/// Edge importance for explainability
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EdgeImportanceDto {
pub source: usize,
pub target: usize,
pub source_name: String,
pub target_name: String,
pub importance: f64,
pub interhemispheric: bool,
}
/// Node importance for explainability
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeImportanceDto {
pub index: usize,
pub name: String,
pub importance: f64,
pub hemisphere: HemisphereDto,
pub region: BrainRegionDto,
}
/// GNN explanation result
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GnnExplanationDto {
pub predicted_class: usize,
pub target_class: usize,
pub confidence: f64,
pub edge_importance: Vec<EdgeImportanceDto>,
pub node_importance: Vec<NodeImportanceDto>,
pub method: String,
}
@@ -0,0 +1,11 @@
//! Type definitions for neuro IPC commands.
mod recording;
mod processing;
mod analysis;
mod realtime;
pub use recording::*;
pub use processing::*;
pub use analysis::*;
pub use realtime::*;
@@ -0,0 +1,361 @@
//! Signal processing types for neuro commands.
use serde::{Deserialize, Serialize};
/// Filter configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FilterConfig {
/// Low cutoff frequency (Hz) for highpass, None to skip
pub low_freq: Option<f64>,
/// High cutoff frequency (Hz) for lowpass, None to skip
pub high_freq: Option<f64>,
/// Notch filter frequencies (Hz) for power line removal
pub notch_freqs: Option<Vec<f64>>,
/// Filter method: "fft", "butterworth", or "fir"
pub method: String,
}
/// Epoch configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EpochConfig {
/// Event IDs to include
pub event_ids: Vec<i32>,
/// Time before event in seconds (negative)
pub tmin: f64,
/// Time after event in seconds
pub tmax: f64,
/// Baseline correction interval (start, end) in seconds
pub baseline: Option<(f64, f64)>,
}
/// Epochs handle for frontend
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EpochsHandle {
/// Unique identifier
pub id: String,
/// Source recording ID
pub recording_id: String,
/// Number of epochs
pub n_epochs: usize,
/// Number of channels
pub n_channels: usize,
/// Samples per epoch
pub n_times: usize,
/// Time vector
pub times: Vec<f64>,
}
/// Evoked response data
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvokedData {
/// Channel names
pub channels: Vec<String>,
/// Time values in seconds
pub times: Vec<f64>,
/// Averaged data [n_channels x n_times]
pub data: Vec<Vec<f64>>,
/// Number of epochs averaged
pub n_averaged: usize,
}
/// Forward model configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ForwardConfig {
/// Model type: "spherical_meg" or "spherical_eeg"
pub model_type: String,
/// Sphere origin [x, y, z] in meters
pub sphere_origin: [f64; 3],
/// Sphere radius in meters (for MEG) or radii [brain, skull, scalp] (for EEG)
pub sphere_radius: f64,
/// Number of source points on grid
pub n_sources: usize,
/// Source spacing in meters
pub source_spacing: f64,
}
/// Forward model handle
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ForwardHandle {
/// Unique identifier
pub id: String,
/// Model type
pub model_type: String,
/// Number of sensors
pub n_sensors: usize,
/// Number of sources
pub n_sources: usize,
/// Gain matrix dimensions [n_sensors, n_sources * 3]
pub gain_shape: [usize; 2],
}
/// Source estimate result
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SourceEstimateDto {
/// Source amplitudes [n_sources x n_times]
pub data: Vec<Vec<f64>>,
/// Time vector
pub times: Vec<f64>,
/// Source positions [n_sources x 3]
pub source_positions: Vec<[f64; 3]>,
/// Method used (MNE, dSPM, sLORETA, LCMV)
pub method: String,
}
/// Inverse solution configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InverseConfig {
/// Method: "mne", "dspm", "sloreta", "lcmv"
pub method: String,
/// Regularization parameter (lambda^2)
pub lambda2: f64,
/// Loose orientation constraint (0 = fixed, 1 = free)
pub loose: f64,
/// Depth weighting exponent
pub depth: f64,
}
/// Inverse operator handle
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InverseHandle {
/// Unique identifier
pub id: String,
/// Forward model ID
pub forward_id: String,
/// Method
pub method: String,
/// Number of sources
pub n_sources: usize,
/// Number of channels
pub n_channels: usize,
}
/// SSP projector configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SspConfig {
/// Number of components to compute
pub n_components: usize,
}
/// SSP projector handle
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SspHandle {
/// Unique identifier
pub id: String,
/// Recording ID
pub recording_id: String,
/// Number of projectors
pub n_projectors: usize,
/// Explained variance for each projector
pub explained_var: Vec<f64>,
}
/// ICA configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IcaConfig {
/// Number of components (None = n_channels)
pub n_components: Option<usize>,
/// Method: "fastica"
pub method: String,
/// Maximum iterations
pub max_iter: usize,
/// Convergence tolerance
pub tol: f64,
}
/// ICA model handle
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IcaHandle {
/// Unique identifier
pub id: String,
/// Recording ID
pub recording_id: String,
/// Number of components
pub n_components: usize,
}
/// ICA sources data
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IcaSourcesDto {
/// Independent components [n_components x n_times]
pub data: Vec<Vec<f64>>,
/// Time vector
pub times: Vec<f64>,
}
/// Connectivity analysis configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectivityConfig {
/// Method: "coherence", "imaginary_coherence", "plv", "wpli", "dwpli"
pub method: String,
/// Minimum frequency (Hz)
pub fmin: f64,
/// Maximum frequency (Hz)
pub fmax: f64,
/// FFT length (None = auto)
pub n_fft: Option<usize>,
}
/// Connectivity result
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectivityResultDto {
/// Connectivity matrix [n_pairs x n_freqs]
pub data: Vec<Vec<f64>>,
/// Frequency vector
pub freqs: Vec<f64>,
/// Source channel indices
pub sources: Vec<usize>,
/// Target channel indices
pub targets: Vec<usize>,
/// Method used
pub method: String,
/// Number of epochs
pub n_epochs: usize,
}
/// Time-frequency analysis configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TfrConfig {
/// Method: "morlet" or "stft"
pub method: String,
/// Frequencies to analyze
pub freqs: Vec<f64>,
/// Number of cycles for Morlet wavelets
pub n_cycles: f64,
/// Output type: "power", "phase", "complex", "itc"
pub output: String,
/// Decimation factor (1 = no decimation)
pub decim: usize,
/// Channel indices to analyze (None = all)
pub channels: Option<Vec<usize>>,
}
/// Time-frequency result
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TfrResultDto {
/// TFR data [n_channels x n_freqs x n_times]
pub data: Vec<Vec<Vec<f64>>>,
/// Frequency vector
pub freqs: Vec<f64>,
/// Time vector
pub times: Vec<f64>,
/// Output type used
pub output: String,
}
/// PSD configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PsdConfig {
/// Method: "welch" or "multitaper"
pub method: String,
/// Minimum frequency (Hz)
pub fmin: Option<f64>,
/// Maximum frequency (Hz)
pub fmax: Option<f64>,
/// FFT length for Welch (None = auto)
pub n_fft: Option<usize>,
/// Overlap for Welch (None = n_fft/2)
pub n_overlap: Option<usize>,
/// Bandwidth for multitaper (Hz)
pub bandwidth: Option<f64>,
/// Channel indices (None = all)
pub channels: Option<Vec<usize>>,
}
/// PSD result
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PsdResultDto {
/// PSD values [n_channels x n_freqs]
pub psd: Vec<Vec<f64>>,
/// Frequency vector
pub freqs: Vec<f64>,
/// Channel names
pub channels: Vec<String>,
}
/// Configuration for permutation test
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PermutationTestConfig {
/// Number of permutations
pub n_permutations: usize,
/// Tail: -1 (left), 0 (two-sided), 1 (right)
pub tail: i32,
/// Use cluster-based correction
pub cluster_threshold: Option<f64>,
/// Seed for reproducibility
pub seed: Option<u64>,
}
/// Result of permutation test
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PermutationTestResult {
/// Observed statistic
pub statistic: f64,
/// P-value
pub p_value: f64,
/// Null distribution (optional)
pub null_distribution: Option<Vec<f64>>,
}
/// Configuration for t-test
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TTestConfig {
/// Alpha level
pub alpha: f64,
/// Tail: -1 (left), 0 (two-sided), 1 (right)
pub tail: i32,
}
/// Result of t-test
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TTestResultDto {
/// T-statistics per time point [n_channels x n_times]
pub t_values: Vec<Vec<f64>>,
/// P-values per time point [n_channels x n_times]
pub p_values: Vec<Vec<f64>>,
/// Degrees of freedom
pub df: usize,
/// Significant mask (after correction) [n_channels x n_times]
pub significant: Vec<Vec<bool>>,
}
/// Configuration for multiple comparison correction
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CorrectionConfig {
/// Method: "bonferroni", "fdr", "holm"
pub method: String,
/// Alpha level
pub alpha: f64,
}
/// Result of multiple comparison correction
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CorrectionResult {
/// Corrected p-values
pub corrected_p: Vec<f64>,
/// Significance mask
pub significant: Vec<bool>,
}
/// Configuration for cluster-based test
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClusterTestConfig {
/// Number of permutations
pub n_permutations: usize,
/// Cluster-forming threshold (t-value)
pub threshold: f64,
/// Tail: -1 (left), 0 (two-sided), 1 (right)
pub tail: i32,
/// Seed for reproducibility
pub seed: Option<u64>,
}
/// Result of cluster-based test
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClusterTestResultDto {
/// Cluster-level p-values
pub cluster_p_values: Vec<f64>,
/// Cluster masks [n_clusters x n_channels x n_times]
pub clusters: Vec<Vec<Vec<bool>>>,
/// Observed cluster statistics
pub cluster_stats: Vec<f64>,
}
@@ -0,0 +1,120 @@
//! Real-time streaming types for neuro commands (LSL, BCI).
use serde::{Deserialize, Serialize};
/// LSL stream information for frontend
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LslStreamInfoDto {
/// Stream name
pub name: String,
/// Stream type (EEG, MEG, Markers)
pub stream_type: String,
/// Number of channels
pub channel_count: usize,
/// Nominal sampling rate in Hz
pub nominal_srate: f64,
/// Unique identifier
pub uid: String,
}
/// LSL connection handle for frontend
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LslHandleDto {
/// Unique identifier for this connection
pub id: String,
/// Stream information
pub info: LslStreamInfoDto,
/// Whether currently connected
pub connected: bool,
/// Samples received since connection
pub samples_received: usize,
}
/// BCI pipeline configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BciConfigDto {
/// Number of channels
pub n_channels: usize,
/// Sample rate in Hz
pub sample_rate: f64,
/// Buffer duration in milliseconds
pub buffer_duration_ms: f64,
/// Processing window size in milliseconds
pub window_size_ms: f64,
/// Window stride in milliseconds
pub stride_ms: f64,
/// Filter low cutoff (Hz)
pub filter_low: f64,
/// Filter high cutoff (Hz)
pub filter_high: f64,
/// Source indices to monitor
pub monitor_sources: Vec<usize>,
/// Threshold for activation detection (0.0 - 1.0)
pub threshold: f64,
/// Smoothing factor (0.0 = no smoothing, 1.0 = full smoothing)
pub smoothing: f64,
}
impl Default for BciConfigDto {
fn default() -> Self {
Self {
n_channels: 64,
sample_rate: 1000.0,
buffer_duration_ms: 500.0,
window_size_ms: 100.0,
stride_ms: 50.0,
filter_low: 8.0,
filter_high: 30.0,
monitor_sources: vec![0],
threshold: 0.5,
smoothing: 0.3,
}
}
}
/// Control signal from BCI pipeline
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ControlSignalDto {
/// Source indices being monitored
pub source_indices: Vec<usize>,
/// Current power values for each source
pub power: Vec<f64>,
/// Normalized control values (0.0 - 1.0)
pub control_values: Vec<f64>,
/// Combined control signal (weighted average)
pub combined_signal: f64,
/// Threshold crossing
pub threshold_crossed: bool,
/// Timestamp
pub timestamp: f64,
/// Latency in milliseconds
pub latency_ms: f64,
}
/// Latency statistics
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LatencyStatsDto {
/// Stage name
pub stage: String,
/// Mean latency in microseconds
pub mean_us: f64,
/// 95th percentile in microseconds
pub p95_us: f64,
/// 99th percentile in microseconds
pub p99_us: f64,
/// Number of samples
pub count: usize,
}
/// BCI pipeline handle
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BciPipelineHandle {
/// Pipeline ID
pub id: String,
/// Current state
pub state: String,
/// Configuration
pub config: BciConfigDto,
/// Is baseline calibrated
pub baseline_calibrated: bool,
}
@@ -0,0 +1,234 @@
//! Recording and data types for neuro commands.
use serde::{Deserialize, Serialize};
/// Handle to a loaded recording
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecordingHandle {
/// Unique identifier for the recording
pub id: String,
/// File path of the source file
pub path: String,
/// Number of channels
pub n_channels: usize,
/// Number of samples
pub n_samples: usize,
/// Sampling frequency in Hz
pub sfreq: f64,
/// Recording duration in seconds
pub duration: f64,
}
/// Channel information for frontend display
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChannelInfoDto {
/// Channel name
pub name: String,
/// Channel type (EEG, MEG, EOG, etc.)
pub channel_type: String,
/// Physical unit
pub unit: String,
/// Whether channel is marked as bad
pub is_bad: bool,
}
/// Recording metadata
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecordingInfoDto {
/// File format (EDF, BDF, BrainVision)
pub format: String,
/// Subject ID if available
pub subject_id: Option<String>,
/// Recording date if available
pub recording_date: Option<String>,
/// Channel information
pub channels: Vec<ChannelInfoDto>,
/// Number of events
pub n_events: usize,
/// Event types present
pub event_types: Vec<String>,
}
/// Data chunk for visualization
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DataChunk {
/// Channel names
pub channels: Vec<String>,
/// Time values in seconds
pub times: Vec<f64>,
/// Data values [n_channels x n_times]
pub data: Vec<Vec<f64>>,
}
/// Event marker
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EventDto {
/// Sample index
pub sample: usize,
/// Time in seconds
pub time: f64,
/// Event value/ID
pub value: i32,
/// Event description if available
pub description: Option<String>,
}
/// Neuro service status
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NeuroStatus {
/// Number of loaded recordings
pub n_recordings: usize,
/// Active recording ID if any
pub active_recording: Option<String>,
/// Number of created epochs
pub n_epochs_sets: usize,
/// Memory usage estimate in bytes
pub memory_usage: usize,
}
/// BIDS dataset handle
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BidsDatasetHandle {
/// Unique identifier for the dataset
pub id: String,
/// Root path of the dataset
pub path: String,
/// Dataset name from dataset_description.json
pub name: String,
/// BIDS version
pub bids_version: String,
/// List of subject labels
pub subjects: Vec<String>,
}
/// BIDS subject info
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BidsSubjectDto {
/// Subject label (without "sub-" prefix)
pub label: String,
/// Session labels for this subject
pub sessions: Vec<Option<String>>,
/// Number of MEG files
pub n_meg_files: usize,
/// Number of EEG files
pub n_eeg_files: usize,
}
/// BIDS file info
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BidsFileDto {
/// Full path to the data file
pub path: String,
/// Subject label
pub subject: String,
/// Session label (if any)
pub session: Option<String>,
/// Task label (if any)
pub task: Option<String>,
/// Run number (if any)
pub run: Option<u32>,
/// Data type (meg, eeg)
pub datatype: Option<String>,
/// File extension
pub extension: Option<String>,
/// Whether sidecar JSON exists
pub has_sidecar: bool,
/// Whether channels.tsv exists
pub has_channels: bool,
/// Whether events.tsv exists
pub has_events: bool,
}
/// Handle to a loaded FreeSurfer subject
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FreeSurferHandleDto {
/// Unique identifier
pub id: String,
/// Path to subject directory
pub path: String,
/// Subject ID (directory name)
pub subject_id: String,
/// Available surfaces [(hemisphere, type), ...]
pub available_surfaces: Vec<(String, String)>,
/// Available annotations [(hemisphere, atlas), ...]
pub available_annotations: Vec<(String, String)>,
}
/// Surface mesh data for visualization
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SurfaceMeshDto {
/// Vertex positions [[x, y, z], ...]
pub vertices: Vec<[f32; 3]>,
/// Triangle faces [[v0, v1, v2], ...]
pub faces: Vec<[u32; 3]>,
/// Vertex normals [[nx, ny, nz], ...]
pub normals: Vec<[f32; 3]>,
/// Surface type (white, pial, inflated)
pub surface_type: String,
/// Hemisphere (left, right)
pub hemisphere: String,
}
/// Annotation/parcellation data
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnnotationDto {
/// Per-vertex labels (region indices)
pub labels: Vec<i32>,
/// Vertex colors [[r, g, b], ...] (0-1 range)
pub vertex_colors: Vec<[f32; 3]>,
/// Region names in color table
pub region_names: Vec<String>,
/// Region colors [[r, g, b], ...]
pub region_colors: Vec<[f32; 3]>,
/// Atlas name
pub atlas: String,
/// Hemisphere
pub hemisphere: String,
}
/// Curvature data for visualization
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CurvatureDto {
/// Per-vertex curvature values
pub values: Vec<f32>,
/// Curvature type (curv, sulc, thickness)
pub curv_type: String,
/// Hemisphere
pub hemisphere: String,
/// Min value
pub min_val: f32,
/// Max value
pub max_val: f32,
}
/// Protocol handle for frontend
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProtocolHandle {
/// Unique identifier (UUID string)
pub id: String,
/// Protocol name
pub name: String,
/// Description
pub description: String,
/// Creation timestamp (ISO 8601)
pub created_at: String,
/// Number of subjects
pub n_subjects: usize,
}
/// Subject handle for frontend
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubjectHandle {
/// Unique identifier (UUID string)
pub id: String,
/// Protocol ID this subject belongs to
pub protocol_id: String,
/// Subject label (e.g., "sub-01")
pub label: String,
/// Whether anatomy is loaded
pub has_anatomy: bool,
/// Anatomy path if set
pub anatomy_path: Option<String>,
/// Creation timestamp (ISO 8601)
pub created_at: String,
}