33from pathlib import Path
44from typing import Any
55
6+ import numpy as np
67import typer
78from rich .console import Console
89from rich .progress import Progress , SpinnerColumn , TextColumn
910
1011from accelperm .backends .factory import BackendFactory
12+ from accelperm .core .corrections import (
13+ BonferroniCorrection ,
14+ FDRCorrection ,
15+ )
1116from accelperm .io .contrast import ContrastLoader
1217from accelperm .io .design import DesignMatrixLoader
1318from accelperm .io .nifti import NiftiLoader
@@ -104,6 +109,26 @@ def glm(
104109 "-v" ,
105110 help = "Enable verbose output" ,
106111 ),
112+ n_permutations : int = typer .Option (
113+ 1000 ,
114+ "--n-perm" ,
115+ "-n" ,
116+ help = "Number of permutations for statistical testing" ,
117+ min = 100 ,
118+ ),
119+ correction : str = typer .Option (
120+ "fdr" ,
121+ "--correction" ,
122+ help = "Multiple comparison correction method (none, bonferroni, fdr, fwer, cluster)" ,
123+ case_sensitive = False ,
124+ ),
125+ alpha : float = typer .Option (
126+ 0.05 ,
127+ "--alpha" ,
128+ help = "Significance level for correction" ,
129+ min = 0.001 ,
130+ max = 0.5 ,
131+ ),
107132) -> None :
108133 """
109134 Run General Linear Model analysis with permutation testing.
@@ -122,6 +147,14 @@ def glm(
122147 )
123148 raise typer .Exit (1 )
124149
150+ # Validate correction method
151+ valid_corrections = ["none" , "bonferroni" , "fdr" , "fwer" , "cluster" ]
152+ if correction .lower () not in valid_corrections :
153+ console .print (
154+ f"[red]Error: Invalid correction method '{ correction } '. Valid options: { ', ' .join (valid_corrections )} [/red]"
155+ )
156+ raise typer .Exit (1 )
157+
125158 # Create output directory if it doesn't exist
126159 output_dir .mkdir (parents = True , exist_ok = True )
127160
@@ -133,6 +166,9 @@ def glm(
133166 "output_dir" : output_dir ,
134167 "backend" : backend .lower (),
135168 "verbose" : verbose ,
169+ "n_permutations" : n_permutations ,
170+ "correction" : correction .lower (),
171+ "alpha" : alpha ,
136172 }
137173
138174 if verbose :
@@ -142,6 +178,9 @@ def glm(
142178 console .print (f"Contrasts: { contrast_file } " )
143179 console .print (f"Output: { output_dir } " )
144180 console .print (f"Backend: { backend } " )
181+ console .print (f"Permutations: { n_permutations } " )
182+ console .print (f"Correction: { correction } " )
183+ console .print (f"Alpha: { alpha } " )
145184
146185 try :
147186 # Run the analysis
@@ -176,6 +215,71 @@ def glm(
176215 raise typer .Exit (1 )
177216
178217
218+ def apply_corrections (glm_result : dict [str , Any ], correction_method : str , alpha : float , verbose : bool = False ) -> dict [str , Any ]:
219+ """
220+ Apply multiple comparison corrections to GLM results.
221+
222+ Parameters
223+ ----------
224+ glm_result : dict[str, Any]
225+ GLM results containing t_stat and p_values
226+ correction_method : str
227+ Correction method to apply
228+ alpha : float
229+ Significance level
230+ verbose : bool
231+ Verbose output flag
232+
233+ Returns
234+ -------
235+ dict[str, Any]
236+ Correction results for each contrast
237+ """
238+
239+ p_values = glm_result ["p_values" ] # Shape: (n_voxels, n_contrasts)
240+ n_voxels , n_contrasts = p_values .shape
241+
242+ correction_results = {}
243+
244+ for i in range (n_contrasts ):
245+ contrast_p = p_values [:, i ]
246+
247+ if correction_method == "bonferroni" :
248+ corrector = BonferroniCorrection ()
249+ result = corrector .correct (contrast_p , alpha = alpha )
250+
251+ elif correction_method == "fdr" :
252+ corrector = FDRCorrection ()
253+ result = corrector .correct (contrast_p , alpha = alpha )
254+
255+ elif correction_method == "fwer" :
256+ # For FWER, we would need a null distribution from permutations
257+ # For now, fall back to Bonferroni (conservative)
258+ if verbose :
259+ console .print ("[yellow]Warning: FWER requires permutation testing. Using Bonferroni correction.[/yellow]" )
260+ corrector = BonferroniCorrection ()
261+ result = corrector .correct (contrast_p , alpha = alpha )
262+
263+ elif correction_method == "cluster" :
264+ # For cluster correction, we would need spatial information and null distribution
265+ # For now, fall back to FDR
266+ if verbose :
267+ console .print ("[yellow]Warning: Cluster correction requires permutation testing. Using FDR correction.[/yellow]" )
268+ corrector = FDRCorrection ()
269+ result = corrector .correct (contrast_p , alpha = alpha )
270+
271+ else :
272+ raise ValueError (f"Unknown correction method: { correction_method } " )
273+
274+ correction_results [f"contrast_{ i } " ] = result
275+
276+ if verbose :
277+ n_significant = np .sum (result .significant_mask )
278+ console .print (f" Contrast { i + 1 } : { n_significant } /{ n_voxels } significant voxels" )
279+
280+ return correction_results
281+
282+
179283def run_glm (config : dict [str , Any ]) -> dict [str , Any ]:
180284 """
181285 Run GLM analysis with the given configuration.
@@ -274,6 +378,16 @@ def run_glm(config: dict[str, Any]) -> dict[str, Any]:
274378
275379 glm_result = backend .compute_glm (Y , X , contrasts )
276380
381+ # Apply multiple comparison corrections if requested
382+ correction_results = None
383+ if config ["correction" ] != "none" :
384+ if config ["verbose" ]:
385+ console .print (f"Applying { config ['correction' ].upper ()} correction..." )
386+
387+ correction_results = apply_corrections (
388+ glm_result , config ["correction" ], config ["alpha" ], config ["verbose" ]
389+ )
390+
277391 # Save results
278392 if config ["verbose" ]:
279393 console .print ("Saving results..." )
@@ -291,10 +405,24 @@ def run_glm(config: dict[str, Any]) -> dict[str, Any]:
291405 t_output = config ["output_dir" ] / f"tstat_{ contrast_name } .nii.gz"
292406 output_writer .save_statistical_map (t_map , affine , t_output , "t-stat" )
293407
294- # Save p-value map
408+ # Save uncorrected p-value map
295409 p_output = config ["output_dir" ] / f"pvals_{ contrast_name } .nii.gz"
296410 output_writer .save_p_value_map (p_map , affine , p_output )
297411
412+ # Save corrected p-values if correction was applied
413+ if correction_results is not None :
414+ corrected_result = correction_results [f"contrast_{ i } " ]
415+ corrected_p_map = corrected_result .corrected_p_values .reshape (- 1 , 1 , 1 )
416+
417+ # Save corrected p-values
418+ corrected_output = config ["output_dir" ] / f"corrp_{ contrast_name } _{ config ['correction' ]} .nii.gz"
419+ output_writer .save_p_value_map (corrected_p_map , affine , corrected_output )
420+
421+ # Save significance mask
422+ sig_map = corrected_result .significant_mask .astype (float ).reshape (- 1 , 1 , 1 )
423+ sig_output = config ["output_dir" ] / f"sig_{ contrast_name } _{ config ['correction' ]} .nii.gz"
424+ output_writer .save_statistical_map (sig_map , affine , sig_output , "significance" )
425+
298426 # Create summary
299427 summary_output = config ["output_dir" ] / "results_summary.txt"
300428 results_summary = {
@@ -303,10 +431,20 @@ def run_glm(config: dict[str, Any]) -> dict[str, Any]:
303431 "n_regressors" : X .shape [1 ],
304432 "n_contrasts" : contrasts .shape [0 ],
305433 "backend" : config ["backend" ],
434+ "correction_method" : config ["correction" ],
435+ "alpha" : config ["alpha" ],
306436 "max_t_stat" : float (glm_result ["t_stat" ].max ()),
307437 "min_p_value" : float (glm_result ["p_values" ].min ()),
308438 }
309439
440+ # Add correction summary if applied
441+ if correction_results is not None :
442+ for i in range (contrasts .shape [0 ]):
443+ result = correction_results [f"contrast_{ i } " ]
444+ n_significant = int (np .sum (result .significant_mask ))
445+ results_summary [f"contrast_{ i } _significant_voxels" ] = n_significant
446+ results_summary [f"contrast_{ i } _min_corrected_p" ] = float (result .corrected_p_values .min ())
447+
310448 from accelperm .io .output import create_results_summary
311449
312450 create_results_summary (results_summary , summary_output )
0 commit comments