Initial commit
This commit is contained in:
@@ -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, ¬ch_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,
|
||||
}
|
||||
Reference in New Issue
Block a user