Skip to main content

bids_modeling/
auto_model.rs

1//! Automatic BIDS Stats Model generation.
2//!
3//! Analyzes a BIDS dataset's task structure and event files to automatically
4//! create a complete BIDS-StatsModels JSON specification. This is useful
5//! as a starting point that can be manually refined.
6
7use bids_core::error::Result;
8use bids_layout::BidsLayout;
9use serde_json::{Value, json};
10
11/// Auto-generate a BIDS Stats Model for each task in a dataset.
12///
13/// For each task found in the layout, creates a model with:
14///
15/// - **Run level** — Factor(trial_type) transformation, GLM with factored
16///   trial type columns, and t-test dummy contrasts
17/// - **Session level** (if multiple sessions) — Meta-analysis pass-through
18/// - **Subject level** (if multiple subjects) — Meta-analysis pass-through
19/// - **Dataset level** — Group-level GLM with intercept
20///
21/// Trial types are automatically extracted from `_events.tsv` files.
22///
23/// # Example
24///
25/// ```no_run
26/// # use bids_layout::BidsLayout;
27/// # let layout = BidsLayout::new("/path").unwrap();
28/// let models = bids_modeling::auto_model(&layout).unwrap();
29/// for model in &models {
30///     println!("{}", serde_json::to_string_pretty(model).unwrap());
31/// }
32/// ```
33pub fn auto_model(layout: &BidsLayout) -> Result<Vec<Value>> {
34    let tasks = layout.get_tasks()?;
35    let subjects = layout.get_subjects()?;
36    let sessions = layout.get_sessions()?;
37    let root_name = layout
38        .root()
39        .file_name()
40        .and_then(|n| n.to_str())
41        .unwrap_or("dataset");
42
43    let mut models = Vec::new();
44
45    for task_name in &tasks {
46        let model_name = format!("{root_name}_{task_name}");
47
48        // Get trial types from events files for this task
49        let event_files = layout
50            .get()
51            .suffix("events")
52            .extension("tsv")
53            .task(task_name)
54            .collect()?;
55
56        let mut trial_types = std::collections::BTreeSet::new();
57        for ef in &event_files {
58            if let Ok(rows) = ef.get_df() {
59                for row in &rows {
60                    if let Some(tt) = row.get("trial_type")
61                        && !tt.is_empty()
62                    {
63                        trial_types.insert(tt.clone());
64                    }
65                }
66            }
67        }
68
69        let trial_type_factors: Vec<String> = trial_types
70            .iter()
71            .map(|tt| format!("trial_type.{tt}"))
72            .collect();
73
74        // Build run node
75        let run_node = json!({
76            "Level": "Run",
77            "Name": "run",
78            "GroupBy": ["run", "subject"],
79            "Transformations": {
80                "Transformer": "pybids-transforms-v1",
81                "Instructions": [{"Name": "Factor", "Input": ["trial_type"]}]
82            },
83            "Model": {"Type": "glm", "X": trial_type_factors},
84            "DummyContrasts": {"Test": "t"}
85        });
86
87        let mut nodes = vec![run_node];
88
89        // Add higher-level pass-through nodes
90        if sessions.len() > 1 {
91            nodes.push(json!({
92                "Level": "Session",
93                "Name": "session",
94                "GroupBy": ["session", "contrast"],
95                "Model": {"Type": "meta", "X": [1]},
96                "DummyContrasts": {"Test": "t"}
97            }));
98        }
99
100        if subjects.len() > 1 {
101            nodes.push(json!({
102                "Level": "Subject",
103                "Name": "subject",
104                "GroupBy": ["subject", "contrast"],
105                "Model": {"Type": "meta", "X": [1]},
106                "DummyContrasts": {"Test": "t"}
107            }));
108        }
109
110        nodes.push(json!({
111            "Level": "Dataset",
112            "Name": "dataset",
113            "GroupBy": ["contrast"],
114            "Model": {"Type": "glm", "X": [1]},
115            "DummyContrasts": {"Test": "t"}
116        }));
117
118        let model = json!({
119            "Name": model_name,
120            "Description": format!("Autogenerated model for {} task from {}", task_name, root_name),
121            "BIDSModelVersion": "1.0.0",
122            "Input": {"task": [task_name]},
123            "Nodes": nodes
124        });
125
126        models.push(model);
127    }
128
129    Ok(models)
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    // Integration test would require a real layout on disk
137    // Unit test just verifies the function signature compiles
138    #[test]
139    fn test_auto_model_signature() {
140        // Just verify types compile
141        let _: fn(&BidsLayout) -> Result<Vec<Value>> = auto_model;
142    }
143}