Exemple #1
0
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")
Exemple #2
0
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)
Exemple #3
0
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)
Exemple #4
0
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)
Exemple #5
0
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
Exemple #6
0
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")