def test_load_motpars_break(motion_parameters): """Break aroma.utils.load_motpars.""" with pytest.raises(Exception): utils.load_motpars("dog.dog", source="auto") with pytest.raises(ValueError): utils.load_motpars(motion_parameters["FSL"], source="dog")
def test_load_motpars_auto(motion_parameters): """Test aroma.utils.load_motpars with automatic source determination.""" fsl = utils.load_motpars(motion_parameters["FSL"], source="auto") afni = utils.load_motpars(motion_parameters["AfNI"], source="auto") spm = utils.load_motpars(motion_parameters["SPM"], source="auto") fmriprep = utils.load_motpars(motion_parameters["fMRIPrep"], source="auto") assert np.allclose(fsl, afni) assert np.allclose(fsl, spm) assert np.allclose(fsl, fmriprep)
def test_motpars_afni2fsl(motion_parameters): """Test aroma.utils.motpars_afni2fsl.""" fsl = utils.load_motpars(motion_parameters["FSL"], source="fsl") afni = utils.motpars_afni2fsl(motion_parameters["AfNI"]) assert np.allclose(fsl, afni) with pytest.raises(ValueError): utils.motpars_afni2fsl(5) bad_data = np.random.random((200, 7)) with pytest.raises(ValueError): utils.motpars_afni2fsl(bad_data)
def test_motpars_fmriprep2fsl(motion_parameters): """Test aroma.utils.motpars_fmriprep2fsl.""" fsl = utils.load_motpars(motion_parameters["FSL"], source="fsl") fmriprep = utils.motpars_fmriprep2fsl(motion_parameters["fMRIPrep"]) assert np.allclose(fsl, fmriprep) with pytest.raises(ValueError): utils.motpars_fmriprep2fsl(5) bad_data = np.random.random((200, 7)) with pytest.raises(ValueError): utils.motpars_fmriprep2fsl(bad_data)
def feature_time_series(mel_mix, mc, metric_metadata=None): """Extract maximum motion parameter correlation scores from components. This function determines the maximum robust correlation of each component time series with a model of 72 realignment parameters. Parameters ---------- mel_mix : numpy.ndarray of shape (T, C) Mixing matrix in shape T (time) by C (component). mc : str or array_like Full path of the text file containing the realignment parameters. Motion parameters are (time x 6), with the first three columns being rotation parameters (in radians) and the final three being translation parameters (in mm). metric_metadata : None or dict, optional A dictionary containing metadata about the AROMA metrics. If provided, metadata for the ``max_RP_corr`` metric will be added. Otherwise, no operations will be performed on this parameter. Returns ------- max_RP_corr : array_like Array of the maximum RP correlation feature scores for the components of the melodic_mix file. metric_metadata : None or dict If the ``metric_metadata`` input was None, then None will be returned. Otherwise, this will be a dictionary containing existing information, as well as new metadata for the ``max_RP_corr`` metric. """ if isinstance(metric_metadata, dict): metric_metadata["max_RP_corr"] = { "LongName": "Maximum motion parameter correlation", "Description": ("The maximum correlation coefficient between each component and " "a set of 36 regressors derived from the motion parameters. " "The derived regressors are the raw six motion parameters (6), " "their derivatives (6), " "the parameters and their derivatives time-shifted one TR forward (12), and " "the parameters and their derivatives time-shifted one TR backward (12). " "The correlations are performed on a series of 1000 permutations, " "in which 90 percent of the volumes are selected from both the " "component time series and the motion parameters. " "The correlation is performed between each permuted component time series and " "each permuted regressor in the motion parameter model, " "as well as the squared versions of both. " "The maximum correlation coefficient from each permutation is retained and these " "correlation coefficients are averaged across permutations for the final metric." ), "Units": "arbitrary", } if isinstance(mc, str): rp6 = utils.load_motpars(mc, source="auto") else: rp6 = mc if (rp6.ndim != 2) or (rp6.shape[1] != 6): raise ValueError( f"Motion parameters must of shape (n_trs, 6), not {rp6.shape}") if rp6.shape[0] != mel_mix.shape[0]: raise ValueError( f"Number of rows in mixing matrix ({mel_mix.shape[0]}) does not match " f"number of rows in motion parameters ({rp6.shape[0]}).") # Determine the derivatives of the RPs (add zeros at time-point zero) _, nparams = rp6.shape rp6_der = np.vstack((np.zeros(nparams), np.diff(rp6, axis=0))) # Create an RP-model including the RPs and its derivatives rp12 = np.hstack((rp6, rp6_der)) # add the fw and bw shifted versions rp12_1fw = np.vstack((np.zeros(2 * nparams), rp12[:-1])) rp12_1bw = np.vstack((rp12[1:], np.zeros(2 * nparams))) rp_model = np.hstack((rp12, rp12_1fw, rp12_1bw)) # Determine the maximum correlation between RPs and IC time-series nsplits = 1000 nmixrows, nmixcols = mel_mix.shape nrows_to_choose = int(round(0.9 * nmixrows)) # Max correlations for multiple splits of the dataset (for a robust # estimate) max_correls = np.empty((nsplits, nmixcols)) for i in range(nsplits): # Select a random subset of 90% of the dataset rows # (*without* replacement) chosen_rows = np.random.choice(a=range(nmixrows), size=nrows_to_choose, replace=False) # Combined correlations between RP and IC time-series, squared and # non squared correl_nonsquared = utils.cross_correlation(mel_mix[chosen_rows], rp_model[chosen_rows]) correl_squared = utils.cross_correlation(mel_mix[chosen_rows]**2, rp_model[chosen_rows]**2) correl_both = np.hstack((correl_squared, correl_nonsquared)) # Maximum absolute temporal correlation for every IC max_correls[i] = np.abs(correl_both).max(axis=1) # Feature score is the mean of the maximum correlation over all the random # splits # Avoid propagating occasional nans that arise in artificial test cases max_RP_corr = np.nanmean(max_correls, axis=0) return max_RP_corr, metric_metadata
def aroma_workflow( in_file, mc, mixing, component_maps, out_dir, den_type="nonaggr", TR=None, overwrite=False, generate_plots=True, debug=False, quiet=False, mc_source="auto", ): """Run the AROMA workflow. Parameters ---------- in_file : str Path to MNI-space functional run to denoise. mc : str Path to motion parameters. mixing : str Path to mixing matrix. component_maps : str Path to thresholded z-statistic component maps. out_dir : str Output directory. den_type : {"nonaggr", "aggr", "both", "no"}, optional Denoising approach to use. TR : float or None, optional Repetition time of data in in_file and mixing. If None, this will be extracted from the header of in_file. overwrite : bool generate_plots : bool debug : bool quiet : bool mc_source : {"auto"}, optional What format is the mc file in? """ if not op.isfile(in_file): raise FileNotFoundError(f"Input file does not exist: {in_file}") if not op.isfile(mc): raise FileNotFoundError(f"Motion parameters file does not exist: {mc}") if not op.isfile(mixing): raise FileNotFoundError(f"Mixing matrix file does not exist: {mixing}") if not op.isfile(component_maps): raise FileNotFoundError( f"Component maps file does not exist: {component_maps}") # Create output directory if needed if op.isdir(out_dir) and not overwrite: LGR.info( f"Output directory {out_dir}," """already exists. AROMA will not continue. Rerun with the -overwrite option to explicitly overwrite existing output.""", ) return elif op.isdir(out_dir) and overwrite: LGR.warning("Output directory {} exists and will be overwritten." "\n".format(out_dir)) shutil.rmtree(out_dir) os.makedirs(out_dir) else: os.makedirs(out_dir) # Create logfile name basename = 'aroma_' extension = 'tsv' isotime = datetime.datetime.now().strftime('%Y-%m-%dT%H%M%S') logname = os.path.join(out_dir, (basename + isotime + '.' + extension)) # Set logging format log_formatter = logging.Formatter( '%(asctime)s\t%(name)-12s\t%(levelname)-8s\t%(message)s', datefmt='%Y-%m-%dT%H:%M:%S') # Set up logging file and open it for writing log_handler = logging.FileHandler(logname) log_handler.setFormatter(log_formatter) sh = logging.StreamHandler() # add logger mode options if quiet: logging.basicConfig(level=logging.WARNING, handlers=[log_handler, sh], format='%(levelname)-10s %(message)s') elif debug: logging.basicConfig(level=logging.DEBUG, handlers=[log_handler, sh], format='%(levelname)-10s %(message)s') else: logging.basicConfig(level=logging.INFO, handlers=[log_handler, sh], format='%(levelname)-10s %(message)s') version_number = _version.get_versions()['version'] LGR.info(f'Currently running ICA-AROMA version {version_number}') # Check if the type of denoising is correctly specified, when specified if den_type not in ("nonaggr", "aggr", "both", "no"): LGR.warning( "Type of denoising was not correctly specified. Non-aggressive " "denoising will be run.") den_type = "nonaggr" # Prepare # Get TR of the fMRI data, if not specified if not TR: in_img = nib.load(in_file) TR = in_img.header.get_zooms()[3] # Check TR if TR == 1: LGR.warning("Please check whether the determined TR (of " + str(TR) + "s) is correct!\n") elif TR == 0: raise Exception( "TR is zero. ICA-AROMA requires a valid TR and will therefore " "exit. Please check the header, or define the TR as an additional " "argument.\n" "-------------- ICA-AROMA IS CANCELED ------------\n") # Load more inputs motion_params = utils.load_motpars(mc, source=mc_source) # T x 6 mixing = np.loadtxt(mixing) # T x C component_maps = nib.load(component_maps) # X x Y x Z x C if mixing.shape[1] != component_maps.shape[3]: raise ValueError( f"Number of columns in mixing matrix ({mixing.shape[1]}) does not match " f"fourth dimension of component maps file ({component_maps.shape[3]})." ) if mixing.shape[0] != motion_params.shape[0]: raise ValueError( f"Number of rows in mixing matrix ({mixing.shape[0]}) does not match " f"number of rows in motion parameters ({motion_params.shape[0]}).") LGR.info(" - extracting the CSF & Edge fraction features") metric_metadata = {} features_df = pd.DataFrame() (features_df["edge_fract"], features_df["csf_fract"], metric_metadata) = features.feature_spatial(component_maps, metric_metadata) LGR.info(" - extracting the Maximum RP correlation feature") features_df["max_RP_corr"], metric_metadata = features.feature_time_series( mixing, motion_params, metric_metadata, ) LGR.info(" - extracting the High-frequency content feature") # Should probably check that the frequencies match up with MELODIC's outputs mel_FT_mix, FT_freqs = utils.get_spectrum(mixing, TR) features_df["HFC"], metric_metadata = features.feature_frequency( mel_FT_mix, TR, metric_metadata, ) LGR.info(" - classification") features_df, metric_metadata = utils.classification( features_df, metric_metadata) motion_ICs = utils.write_metrics(features_df, out_dir, metric_metadata) if generate_plots: from . import plotting plotting.classification_plot( op.join(out_dir, "desc-AROMA_metrics.tsv"), out_dir) if den_type != "no": LGR.info("Step 3) Data denoising") utils.denoising(in_file, out_dir, mixing, den_type, motion_ICs) LGR.info("Finished")