use bids_core::entities::StringEntities;
use bids_variables::collections::VariableCollection;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone)]
pub struct StatsModelsEdge {
pub source: String,
pub destination: String,
pub filter: StringEntities,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContrastInfo {
pub name: String,
pub conditions: Vec<String>,
pub weights: Vec<f64>,
pub test: Option<String>,
pub entities: StringEntities,
}
#[derive(Debug, Clone)]
pub struct StatsModelsNode {
pub level: String,
pub name: String,
pub model: serde_json::Value,
pub group_by: Vec<String>,
pub transformations: Option<crate::transformations::TransformSpec>,
pub contrasts: Vec<serde_json::Value>,
pub dummy_contrasts: Option<serde_json::Value>,
pub children: Vec<StatsModelsEdge>,
pub parents: Vec<StatsModelsEdge>,
collections: Vec<VariableCollection>,
}
impl StatsModelsNode {
pub fn new(
level: &str,
name: &str,
model: serde_json::Value,
group_by: Vec<String>,
transformations: Option<crate::transformations::TransformSpec>,
contrasts: Vec<serde_json::Value>,
dummy_contrasts: Option<serde_json::Value>,
) -> Self {
Self {
level: level.to_lowercase(),
name: name.into(),
model,
group_by,
transformations,
contrasts,
dummy_contrasts,
children: Vec::new(),
parents: Vec::new(),
collections: Vec::new(),
}
}
pub fn add_child(&mut self, edge: StatsModelsEdge) {
self.children.push(edge);
}
pub fn add_parent(&mut self, edge: StatsModelsEdge) {
self.parents.push(edge);
}
pub fn add_collections(&mut self, collections: Vec<VariableCollection>) {
self.collections.extend(collections);
}
pub fn get_collections(&self) -> &[VariableCollection] {
&self.collections
}
pub fn run(
&self,
inputs: &[ContrastInfo],
_force_dense: bool,
_sampling_rate: &str,
) -> Vec<StatsModelsNodeOutput> {
let mut results = Vec::new();
if self.collections.is_empty() && inputs.is_empty() {
return results;
}
for collection in &self.collections {
let mut coll = collection.clone();
if let Some(ref spec) = self.transformations {
crate::transformations::apply_transformations(&mut coll, spec);
}
let x_vars: Vec<String> = self
.model
.get("x")
.or_else(|| self.model.get("X"))
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| {
if v.is_number() {
Some("intercept".into())
} else {
v.as_str().map(String::from)
}
})
.collect()
})
.unwrap_or_default();
let mut contrasts = Vec::new();
if let Some(ref dc) = self.dummy_contrasts {
let test = dc
.get("test")
.or(dc.get("Test"))
.and_then(|v| v.as_str())
.unwrap_or("t")
.to_string();
for var_name in &x_vars {
if var_name == "intercept" {
continue;
}
contrasts.push(ContrastInfo {
name: var_name.clone(),
conditions: vec![var_name.clone()],
weights: vec![1.0],
test: Some(test.clone()),
entities: collection.entities.clone(),
});
}
}
for con_spec in &self.contrasts {
let name = con_spec
.get("name")
.or(con_spec.get("Name"))
.and_then(|v| v.as_str())
.unwrap_or("unnamed");
let conditions: Vec<String> = con_spec
.get("condition_list")
.or(con_spec.get("ConditionList"))
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let weights: Vec<f64> = con_spec
.get("weights")
.or(con_spec.get("Weights"))
.and_then(|v| v.as_array())
.map(|arr| arr.iter().filter_map(serde_json::Value::as_f64).collect())
.unwrap_or_default();
let test = con_spec
.get("test")
.or(con_spec.get("Test"))
.and_then(|v| v.as_str())
.map(String::from);
let mut entities = collection.entities.clone();
entities.insert("contrast".into(), name.into());
contrasts.push(ContrastInfo {
name: name.into(),
conditions,
weights,
test,
entities,
});
}
let dm = if !x_vars.is_empty() {
let mut cols = Vec::new();
let mut col_names = Vec::new();
for var_name in &x_vars {
if var_name == "intercept" {
let n = coll
.variables
.values()
.next()
.map_or(0, bids_variables::SimpleVariable::len);
cols.push(vec![1.0; n]);
col_names.push("intercept".into());
} else if let Some(var) = coll.variables.get(var_name) {
cols.push(var.values.clone());
col_names.push(var_name.clone());
}
}
if !cols.is_empty() {
Some((col_names, cols))
} else {
None
}
} else {
None
};
results.push(StatsModelsNodeOutput {
node_name: self.name.clone(),
entities: collection.entities.clone(),
x_variables: x_vars.clone(),
contrasts,
design_matrix: dm,
});
}
results
}
}
#[derive(Debug, Clone)]
pub struct StatsModelsNodeOutput {
pub node_name: String,
pub entities: StringEntities,
pub x_variables: Vec<String>,
pub contrasts: Vec<ContrastInfo>,
pub design_matrix: Option<(Vec<String>, Vec<Vec<f64>>)>,
}
impl StatsModelsNodeOutput {
pub fn x_columns(&self) -> &[String] {
&self.x_variables
}
}
pub fn build_groups(
entity_maps: &[StringEntities],
group_by: &[String],
) -> std::collections::HashMap<Vec<(String, String)>, Vec<usize>> {
let mut groups: std::collections::HashMap<Vec<(String, String)>, Vec<usize>> =
std::collections::HashMap::new();
if group_by.is_empty() {
groups.insert(vec![], (0..entity_maps.len()).collect());
return groups;
}
let mut unique_vals: std::collections::HashMap<&str, Vec<String>> =
std::collections::HashMap::new();
for col in group_by {
let vals: std::collections::BTreeSet<String> = entity_maps
.iter()
.filter_map(|e| e.get(col.as_str()).cloned())
.collect();
unique_vals.insert(col.as_str(), vals.into_iter().collect());
}
for (i, ents) in entity_maps.iter().enumerate() {
let mut base: Vec<(String, String)> = Vec::new();
let mut missing: Vec<&str> = Vec::new();
for col in group_by {
if let Some(val) = ents.get(col.as_str()) {
base.push((col.clone(), val.clone()));
} else {
missing.push(col.as_str());
}
}
if missing.is_empty() {
base.sort();
groups.entry(base).or_default().push(i);
} else {
let mut combos = vec![base.clone()];
for col in &missing {
if let Some(vals) = unique_vals.get(col) {
let mut new_combos = Vec::new();
for combo in &combos {
for val in vals {
let mut c = combo.clone();
c.push((col.to_string(), val.clone()));
new_combos.push(c);
}
}
combos = new_combos;
}
}
for mut combo in combos {
combo.sort();
groups.entry(combo).or_default().push(i);
}
}
}
groups
}