def test_get_coeffs(): """ Check least squares coefficients. """ # Simulate one voxel with 40 TRs data = np.empty((2, 40)) data[0, :] = np.arange(0, 200, 5) data[1, :] = np.arange(0, 200, 5) X = np.arange(0, 40)[:, np.newaxis] mask = np.array([True, False]) betas = get_coeffs(data, X, mask=None, add_const=False) betas = np.squeeze(betas) assert np.allclose(betas, np.array([5., 5.])) betas = get_coeffs(data, X, mask=None, add_const=True) betas = np.squeeze(betas) assert np.allclose(betas, np.array([5., 5.])) betas = get_coeffs(data, X, mask=mask, add_const=False) betas = np.squeeze(betas) assert np.allclose(betas, np.array([5, 0])) betas = get_coeffs(data, X, mask=mask, add_const=True) betas = np.squeeze(betas) assert np.allclose(betas, np.array([5, 0]))
def test_break_get_coeffs(): """ Ensure that get_coeffs fails when input data do not have the right shapes. """ n_samples, n_echos, n_vols, n_comps = 10000, 5, 100, 50 data = np.empty((n_samples, n_vols)) X = np.empty((n_vols, n_comps)) mask = np.empty((n_samples)) data = np.empty((n_samples)) with pytest.raises(ValueError): get_coeffs(data, X, mask, add_const=False) data = np.empty((n_samples, n_vols)) X = np.empty((n_vols)) with pytest.raises(ValueError): get_coeffs(data, X, mask, add_const=False) data = np.empty((n_samples, n_echos, n_vols + 1)) X = np.empty((n_vols, n_comps)) with pytest.raises(ValueError): get_coeffs(data, X, mask, add_const=False) data = np.empty((n_samples, n_echos, n_vols)) mask = np.empty((n_samples, n_echos, n_vols)) with pytest.raises(ValueError): get_coeffs(data, X, mask, add_const=False) mask = np.empty((n_samples + 1, n_echos)) with pytest.raises(ValueError): get_coeffs(data, X, mask, add_const=False)
def calculate_betas(data, mixing): """Calculate unstandardized parameter estimates between data and mixing matrix. Parameters ---------- data : (M x [E] x T) array_like Data to calculate betas for mixing : (T x C) array_like Mixing matrix Returns ------- betas : (M x [E] x C) array_like Unstandardized parameter estimates """ if len(data.shape) == 2: data_optcom = data assert data_optcom.shape[1] == mixing.shape[0] # mean-center optimally-combined data data_optcom_dm = data_optcom - data_optcom.mean(axis=-1, keepdims=True) # betas are the result of a normal OLS fit of the mixing matrix # against the mean-center data betas = get_coeffs(data_optcom_dm, mixing) return betas else: betas = np.zeros([data.shape[0], data.shape[1], mixing.shape[1]]) for n_echo in range(data.shape[1]): betas[:, n_echo, :] = get_coeffs(data[:, n_echo, :], mixing) return betas
def test_smoke_get_coeffs(): """ Ensure that get_coeffs returns outputs with different inputs and optional paramters """ n_samples, _, n_times, n_components = 100, 5, 20, 6 data_2d = np.random.random((n_samples, n_times)) x = np.random.random((n_times, n_components)) mask = np.random.randint(2, size=n_samples) assert get_coeffs(data_2d, x) is not None # assert get_coeffs(data_3d, x) is not None TODO: submit an issue for the bug assert get_coeffs(data_2d, x, mask=mask) is not None assert get_coeffs(data_2d, x, add_const=True) is not None
def test_break_get_coeffs(): """ Ensure that get_coeffs fails when input data do not have the right shapes. """ n_samples, n_echos, n_vols, n_comps = 10000, 5, 100, 50 data = np.empty((n_samples, n_vols)) X = np.empty((n_vols, n_comps)) mask = np.empty((n_samples)) data = np.empty((n_samples)) with pytest.raises(ValueError) as e_info: get_coeffs(data, X, mask, add_const=False) assert str( e_info.value) == ('Parameter data should be 2d or 3d, not {0}d'.format( data.ndim)) data = np.empty((n_samples, n_vols)) X = np.empty((n_vols)) with pytest.raises(ValueError) as e_info: get_coeffs(data, X, mask, add_const=False) assert str(e_info.value) == ('Parameter X should be 2d, not {0}d'.format( X.ndim)) data = np.empty((n_samples, n_echos, n_vols + 1)) X = np.empty((n_vols, n_comps)) with pytest.raises(ValueError) as e_info: get_coeffs(data, X, mask, add_const=False) assert str(e_info.value) == ( 'Last dimension (dimension {0}) of data ({1}) does not ' 'match first dimension of ' 'X ({2})'.format(data.ndim, data.shape[-1], X.shape[0])) data = np.empty((n_samples, n_echos, n_vols)) mask = np.empty((n_samples, n_echos, n_vols)) with pytest.raises(ValueError) as e_info: get_coeffs(data, X, mask, add_const=False) assert str( e_info.value) == ('Parameter data should be 1d or 2d, not {0}d'.format( mask.ndim)) mask = np.empty((n_samples + 1, n_echos)) with pytest.raises(ValueError) as e_info: get_coeffs(data, X, mask, add_const=False) assert str(e_info.value) == ( 'First dimensions of data ({0}) and mask ({1}) do not ' 'match'.format(data.shape[0], mask.shape[0]))
def denoise_ts(data, mmix, mask, comptable): """Apply component classifications to data for denoising. Parameters ---------- data : (S x T) array_like Input time series mmix : (C x T) array_like Mixing matrix for converting input data to component space, where `C` is components and `T` is the same as in `data` mask : (S,) array_like Boolean mask array comptable : (C x X) :obj:`pandas.DataFrame` Component metric table. One row for each component, with a column for each metric. Requires at least one column: "classification". Returns ------- dnts : (S x T) array_like Denoised data (i.e., data with rejected components removed). hikts : (S x T) array_like High-Kappa data (i.e., data composed only of accepted components). lowkts : (S x T) array_like Low-Kappa data (i.e., data composed only of rejected components). """ acc = comptable[comptable.classification == "accepted"].index.values rej = comptable[comptable.classification == "rejected"].index.values # mask and de-mean data mdata = data[mask] dmdata = mdata.T - mdata.T.mean(axis=0) # get variance explained by retained components betas = get_coeffs(dmdata.T, mmix, mask=None) varexpl = (1 - ((dmdata.T - betas.dot(mmix.T))**2.0).sum() / (dmdata**2.0).sum()) * 100 LGR.info("Variance explained by decomposition: {:.02f}%".format(varexpl)) # create component-based data hikts = utils.unmask(betas[:, acc].dot(mmix.T[acc, :]), mask) lowkts = utils.unmask(betas[:, rej].dot(mmix.T[rej, :]), mask) dnts = utils.unmask(data[mask] - lowkts[mask], mask) return dnts, hikts, lowkts
def split_ts(data, mmix, mask, comptable): """ Splits `data` time series into accepted component time series and remainder Parameters ---------- data : (S x T) array_like Input data, where `S` is samples and `T` is time mmix : (T x C) array_like Mixing matrix for converting input data to component space, where `C` is components and `T` is the same as in `data` mask : (S,) array_like Boolean mask array comptable : (C x X) :obj:`pandas.DataFrame` Component metric table. One row for each component, with a column for each metric. Requires at least two columns: "component" and "classification". Returns ------- hikts : (S x T) :obj:`numpy.ndarray` Time series reconstructed using only components in `acc` rest : (S x T) :obj:`numpy.ndarray` Original data with `hikts` removed """ acc = comptable[comptable.classification == 'accepted'].index.values cbetas = get_coeffs(data - data.mean(axis=-1, keepdims=True), mmix, mask) betas = cbetas[mask] if len(acc) != 0: hikts = utils.unmask(betas[:, acc].dot(mmix.T[acc, :]), mask) else: hikts = None resid = data - hikts return hikts, resid
def dependence_metrics(catd, tsoc, mmix, t2s, tes, ref_img, reindex=False, mmixN=None, algorithm=None, label=None, out_dir='.', verbose=False): """ Fit TE-dependence and -independence models to components. Parameters ---------- catd : (S x E x T) array_like Input data, where `S` is samples, `E` is echos, and `T` is time tsoc : (S x T) array_like Optimally combined data mmix : (T x C) array_like Mixing matrix for converting input data to component space, where `C` is components and `T` is the same as in `catd` t2s : (S [x T]) array_like Limited T2* map or timeseries. tes : list List of echo times associated with `catd`, in milliseconds ref_img : str or img_like Reference image to dictate how outputs are saved to disk reindex : bool, optional Whether to sort components in descending order by Kappa. Default: False mmixN : (T x C) array_like, optional Z-scored mixing matrix. Default: None algorithm : {'kundu_v2', 'kundu_v3', None}, optional Decision tree to be applied to metrics. Determines which maps will be generated and stored in seldict. Default: None label : :obj:`str` or None, optional Prefix to apply to generated files. Default is None. out_dir : :obj:`str`, optional Output directory for generated files. Default is current working directory. verbose : :obj:`bool`, optional Whether or not to generate additional files. Default is False. Returns ------- comptable : (C x X) :obj:`pandas.DataFrame` Component metric table. One row for each component, with a column for each metric. The index is the component number. seldict : :obj:`dict` or None Dictionary containing component-specific metric maps to be used for component selection. If `algorithm` is None, then seldict will be None as well. betas : :obj:`numpy.ndarray` mmix_new : :obj:`numpy.ndarray` """ # Use t2s as mask mask = t2s != 0 if not (catd.shape[0] == t2s.shape[0] == mask.shape[0] == tsoc.shape[0]): raise ValueError('First dimensions (number of samples) of catd ({0}), ' 'tsoc ({1}), and t2s ({2}) do not ' 'match'.format(catd.shape[0], tsoc.shape[0], t2s.shape[0])) elif catd.shape[1] != len(tes): raise ValueError('Second dimension of catd ({0}) does not match ' 'number of echoes provided (tes; ' '{1})'.format(catd.shape[1], len(tes))) elif not (catd.shape[2] == tsoc.shape[1] == mmix.shape[0]): raise ValueError('Number of volumes in catd ({0}), ' 'tsoc ({1}), and mmix ({2}) do not ' 'match.'.format(catd.shape[2], tsoc.shape[1], mmix.shape[0])) elif t2s.ndim == 2: if catd.shape[2] != t2s.shape[1]: raise ValueError('Number of volumes in catd ' '({0}) does not match number of volumes in ' 't2s ({1})'.format(catd.shape[2], t2s.shape[1])) # mask everything we can tsoc = tsoc[mask, :] catd = catd[mask, ...] t2s = t2s[mask] # demean optimal combination tsoc_dm = tsoc - tsoc.mean(axis=-1, keepdims=True) # compute un-normalized weight dataset (features) if mmixN is None: mmixN = mmix WTS = computefeats2(tsoc, mmixN, mask=None, normalize=False) # compute PSC dataset - shouldn't have to refit data tsoc_B = get_coeffs(tsoc_dm, mmix, mask=None) del tsoc_dm tsoc_Babs = np.abs(tsoc_B) PSC = tsoc_B / tsoc.mean(axis=-1, keepdims=True) * 100 # compute skews to determine signs based on unnormalized weights, # correct mmix & WTS signs based on spatial distribution tails signs = stats.skew(WTS, axis=0) signs /= np.abs(signs) mmix = mmix.copy() mmix *= signs WTS *= signs PSC *= signs totvar = (tsoc_B**2).sum() totvar_norm = (WTS**2).sum() # compute Betas and means over TEs for TE-dependence analysis betas = get_coeffs(utils.unmask(catd, mask), mmix, np.repeat(mask[:, np.newaxis], len(tes), axis=1)) betas = betas[mask, ...] n_voxels, n_echos, n_components = betas.shape mu = catd.mean(axis=-1, dtype=float) tes = np.reshape(tes, (n_echos, 1)) fmin, _, _ = getfbounds(n_echos) # set up Xmats X1 = mu.T # Model 1 X2 = np.tile(tes, (1, n_voxels)) * mu.T / t2s.T # Model 2 # tables for component selection kappas = np.zeros([n_components]) rhos = np.zeros([n_components]) varex = np.zeros([n_components]) varex_norm = np.zeros([n_components]) Z_maps = np.zeros([n_voxels, n_components]) F_R2_maps = np.zeros([n_voxels, n_components]) F_S0_maps = np.zeros([n_voxels, n_components]) pred_R2_maps = np.zeros([n_voxels, n_echos, n_components]) pred_S0_maps = np.zeros([n_voxels, n_echos, n_components]) LGR.info('Fitting TE- and S0-dependent models to components') for i_comp in range(n_components): # size of comp_betas is (n_echoes, n_samples) comp_betas = np.atleast_3d(betas)[:, :, i_comp].T alpha = (np.abs(comp_betas)**2).sum(axis=0) varex[i_comp] = (tsoc_B[:, i_comp]**2).sum() / totvar * 100. varex_norm[i_comp] = (WTS[:, i_comp]**2).sum() / totvar_norm # S0 Model # (S,) model coefficient map coeffs_S0 = (comp_betas * X1).sum(axis=0) / (X1**2).sum(axis=0) pred_S0 = X1 * np.tile(coeffs_S0, (n_echos, 1)) pred_S0_maps[:, :, i_comp] = pred_S0.T SSE_S0 = (comp_betas - pred_S0)**2 SSE_S0 = SSE_S0.sum(axis=0) # (S,) prediction error map F_S0 = (alpha - SSE_S0) * (n_echos - 1) / (SSE_S0) F_S0_maps[:, i_comp] = F_S0 # R2 Model coeffs_R2 = (comp_betas * X2).sum(axis=0) / (X2**2).sum(axis=0) pred_R2 = X2 * np.tile(coeffs_R2, (n_echos, 1)) pred_R2_maps[:, :, i_comp] = pred_R2.T SSE_R2 = (comp_betas - pred_R2)**2 SSE_R2 = SSE_R2.sum(axis=0) F_R2 = (alpha - SSE_R2) * (n_echos - 1) / (SSE_R2) F_R2_maps[:, i_comp] = F_R2 # compute weights as Z-values wtsZ = (WTS[:, i_comp] - WTS[:, i_comp].mean()) / WTS[:, i_comp].std() wtsZ[np.abs(wtsZ) > Z_MAX] = ( Z_MAX * (np.abs(wtsZ) / wtsZ))[np.abs(wtsZ) > Z_MAX] Z_maps[:, i_comp] = wtsZ # compute Kappa and Rho F_S0[F_S0 > F_MAX] = F_MAX F_R2[F_R2 > F_MAX] = F_MAX norm_weights = np.abs(wtsZ**2.) kappas[i_comp] = np.average(F_R2, weights=norm_weights) rhos[i_comp] = np.average(F_S0, weights=norm_weights) del SSE_S0, SSE_R2, wtsZ, F_S0, F_R2, norm_weights, comp_betas if algorithm != 'kundu_v3': del WTS, PSC, tsoc_B # tabulate component values comptable = np.vstack([kappas, rhos, varex, varex_norm]).T if reindex: # re-index all components in descending Kappa order sort_idx = comptable[:, 0].argsort()[::-1] comptable = comptable[sort_idx, :] mmix_new = mmix[:, sort_idx] betas = betas[..., sort_idx] pred_R2_maps = pred_R2_maps[:, :, sort_idx] pred_S0_maps = pred_S0_maps[:, :, sort_idx] F_R2_maps = F_R2_maps[:, sort_idx] F_S0_maps = F_S0_maps[:, sort_idx] Z_maps = Z_maps[:, sort_idx] tsoc_Babs = tsoc_Babs[:, sort_idx] if algorithm == 'kundu_v3': WTS = WTS[:, sort_idx] PSC = PSC[:, sort_idx] tsoc_B = tsoc_B[:, sort_idx] else: mmix_new = mmix del mmix if verbose: # Echo-specific weight maps for each of the ICA components. io.filewrite(utils.unmask(betas, mask), op.join(out_dir, '{0}betas_catd.nii'.format(label)), ref_img) # Echo-specific maps of predicted values for R2 and S0 models for each # component. io.filewrite(utils.unmask(pred_R2_maps, mask), op.join(out_dir, '{0}R2_pred.nii'.format(label)), ref_img) io.filewrite(utils.unmask(pred_S0_maps, mask), op.join(out_dir, '{0}S0_pred.nii'.format(label)), ref_img) # Weight maps used to average metrics across voxels io.filewrite(utils.unmask(Z_maps**2., mask), op.join(out_dir, '{0}metric_weights.nii'.format(label)), ref_img) del pred_R2_maps, pred_S0_maps comptable = pd.DataFrame(comptable, columns=[ 'kappa', 'rho', 'variance explained', 'normalized variance explained' ]) comptable.index.name = 'component' # Generate clustering criteria for component selection if algorithm in ['kundu_v2', 'kundu_v3']: Z_clmaps = np.zeros([n_voxels, n_components], bool) F_R2_clmaps = np.zeros([n_voxels, n_components], bool) F_S0_clmaps = np.zeros([n_voxels, n_components], bool) Br_R2_clmaps = np.zeros([n_voxels, n_components], bool) Br_S0_clmaps = np.zeros([n_voxels, n_components], bool) LGR.info('Performing spatial clustering of components') csize = np.max([int(n_voxels * 0.0005) + 5, 20]) LGR.debug('Using minimum cluster size: {}'.format(csize)) for i_comp in range(n_components): # Cluster-extent threshold and binarize F-maps ccimg = io.new_nii_like( ref_img, np.squeeze(utils.unmask(F_R2_maps[:, i_comp], mask))) F_R2_clmaps[:, i_comp] = utils.threshold_map(ccimg, min_cluster_size=csize, threshold=fmin, mask=mask, binarize=True) countsigFR2 = F_R2_clmaps[:, i_comp].sum() ccimg = io.new_nii_like( ref_img, np.squeeze(utils.unmask(F_S0_maps[:, i_comp], mask))) F_S0_clmaps[:, i_comp] = utils.threshold_map(ccimg, min_cluster_size=csize, threshold=fmin, mask=mask, binarize=True) countsigFS0 = F_S0_clmaps[:, i_comp].sum() # Cluster-extent threshold and binarize Z-maps with CDT of p < 0.05 ccimg = io.new_nii_like( ref_img, np.squeeze(utils.unmask(Z_maps[:, i_comp], mask))) Z_clmaps[:, i_comp] = utils.threshold_map(ccimg, min_cluster_size=csize, threshold=1.95, mask=mask, binarize=True) # Cluster-extent threshold and binarize ranked signal-change map ccimg = io.new_nii_like( ref_img, utils.unmask(stats.rankdata(tsoc_Babs[:, i_comp]), mask)) Br_R2_clmaps[:, i_comp] = utils.threshold_map( ccimg, min_cluster_size=csize, threshold=(max(tsoc_Babs.shape) - countsigFR2), mask=mask, binarize=True) Br_S0_clmaps[:, i_comp] = utils.threshold_map( ccimg, min_cluster_size=csize, threshold=(max(tsoc_Babs.shape) - countsigFS0), mask=mask, binarize=True) del ccimg, tsoc_Babs if algorithm == 'kundu_v2': # WTS, tsoc_B, PSC, and F_S0_maps are not used by Kundu v2.5 selvars = [ 'Z_maps', 'F_R2_maps', 'Z_clmaps', 'F_R2_clmaps', 'F_S0_clmaps', 'Br_R2_clmaps', 'Br_S0_clmaps' ] elif algorithm == 'kundu_v3': selvars = [ 'WTS', 'tsoc_B', 'PSC', 'Z_maps', 'F_R2_maps', 'F_S0_maps', 'Z_clmaps', 'F_R2_clmaps', 'F_S0_clmaps', 'Br_R2_clmaps', 'Br_S0_clmaps' ] elif algorithm is None: selvars = [] else: raise ValueError( 'Algorithm "{0}" not recognized.'.format(algorithm)) seldict = {} for vv in selvars: seldict[vv] = eval(vv) else: seldict = None return comptable, seldict, betas, mmix_new
def write_split_ts(data, mmix, mask, comptable, ref_img, suffix=''): """ Splits `data` into denoised / noise / ignored time series and saves to disk Parameters ---------- data : (S x T) array_like Input time series mmix : (C x T) array_like Mixing matrix for converting input data to component space, where `C` is components and `T` is the same as in `data` mask : (S,) array_like Boolean mask array ref_img : :obj:`str` or img_like Reference image to dictate how outputs are saved to disk suffix : :obj:`str`, optional Appended to name of saved files (before extension). Default: '' Returns ------- varexpl : :obj:`float` Percent variance of data explained by extracted + retained components Notes ----- This function writes out several files: ====================== ================================================= Filename Content ====================== ================================================= hik_ts_[suffix].nii High-Kappa time series. midk_ts_[suffix].nii Mid-Kappa time series. low_ts_[suffix].nii Low-Kappa time series. dn_ts_[suffix].nii Denoised time series. ====================== ================================================= """ acc = comptable[comptable.classification == 'accepted'].index.values rej = comptable[comptable.classification == 'rejected'].index.values # mask and de-mean data mdata = data[mask] dmdata = mdata.T - mdata.T.mean(axis=0) # get variance explained by retained components betas = get_coeffs(dmdata.T, mmix, mask=None) varexpl = (1 - ((dmdata.T - betas.dot(mmix.T))**2.).sum() / (dmdata**2.).sum()) * 100 LGR.info('Variance explained by ICA decomposition: {:.02f}%'.format(varexpl)) # create component and de-noised time series and save to files hikts = betas[:, acc].dot(mmix.T[acc, :]) lowkts = betas[:, rej].dot(mmix.T[rej, :]) dnts = data[mask] - lowkts if len(acc) != 0: fout = filewrite(utils.unmask(hikts, mask), 'hik_ts_{0}'.format(suffix), ref_img) LGR.info('Writing high-Kappa time series: {}'.format(op.abspath(fout))) if len(rej) != 0: fout = filewrite(utils.unmask(lowkts, mask), 'lowk_ts_{0}'.format(suffix), ref_img) LGR.info('Writing low-Kappa time series: {}'.format(op.abspath(fout))) fout = filewrite(utils.unmask(dnts, mask), 'dn_ts_{0}'.format(suffix), ref_img) LGR.info('Writing denoised time series: {}'.format(op.abspath(fout))) return varexpl
def writeresults(ts, mask, comptable, mmix, n_vols, ref_img): """ Denoises `ts` and saves all resulting files to disk Parameters ---------- ts : (S x T) array_like Time series to denoise and save to disk mask : (S,) array_like Boolean mask array comptable : (C x X) :obj:`pandas.DataFrame` Component metric table. One row for each component, with a column for each metric. Requires at least two columns: "component" and "classification". mmix : (C x T) array_like Mixing matrix for converting input data to component space, where `C` is components and `T` is the same as in `data` n_vols : :obj:`int` Number of volumes in original time series ref_img : :obj:`str` or img_like Reference image to dictate how outputs are saved to disk Notes ----- This function writes out several files: ====================== ================================================= Filename Content ====================== ================================================= ts_OC.nii Optimally combined 4D time series. hik_ts_OC.nii High-Kappa time series. Generated by :py:func:`tedana.utils.io.write_split_ts`. midk_ts_OC.nii Mid-Kappa time series. Generated by :py:func:`tedana.utils.io.write_split_ts`. low_ts_OC.nii Low-Kappa time series. Generated by :py:func:`tedana.utils.io.write_split_ts`. dn_ts_OC.nii Denoised time series. Generated by :py:func:`tedana.utils.io.write_split_ts`. betas_OC.nii Full ICA coefficient feature set. betas_hik_OC.nii Denoised ICA coefficient feature set. feats_OC2.nii Z-normalized spatial component maps. Generated by :py:func:`tedana.utils.io.writefeats`. comp_table.txt Component table. Generated by :py:func:`tedana.utils.io.writect`. ====================== ================================================= """ acc = comptable[comptable.classification == 'accepted'].index.values fout = filewrite(ts, 'ts_OC', ref_img) LGR.info('Writing optimally combined time series: {}'.format(op.abspath(fout))) write_split_ts(ts, mmix, mask, comptable, ref_img, suffix='OC') ts_B = get_coeffs(ts, mmix, mask) fout = filewrite(ts_B, 'betas_OC', ref_img) LGR.info('Writing full ICA coefficient feature set: {}'.format(op.abspath(fout))) if len(acc) != 0: fout = filewrite(ts_B[:, acc], 'betas_hik_OC', ref_img) LGR.info('Writing denoised ICA coefficient feature set: {}'.format(op.abspath(fout))) fout = writefeats(split_ts(ts, mmix, mask, comptable)[0], mmix[:, acc], mask, ref_img, suffix='OC2') LGR.info('Writing Z-normalized spatial component maps: {}'.format(op.abspath(fout)))
def comp_figures(ts, mask, comptable, mmix, io_generator, png_cmap): """ Creates static figures that highlight certain aspects of tedana processing This includes a figure for each component showing the component time course, the spatial weight map and a fast Fourier transform of the time course Parameters ---------- ts : (S x T) array_like Time series from which to derive ICA betas mask : (S,) array_like Boolean mask array comptable : (C x X) :obj:`pandas.DataFrame` Component metric table. One row for each component, with a column for each metric. The index should be the component number. mmix : (C x T) array_like Mixing matrix for converting input data to component space, where `C` is components and `T` is the same as in `data` io_generator : :obj:`tedana.io.OutputGenerator` Output Generator object to use for this workflow """ # Get the lenght of the timeseries n_vols = len(mmix) # Flip signs of mixing matrix as needed mmix = mmix * comptable["optimal sign"].values # regenerate the beta images ts_B = stats.get_coeffs(ts, mmix, mask) ts_B = ts_B.reshape(io_generator.reference_img.shape[:3] + ts_B.shape[1:]) # trim edges from ts_B array ts_B = _trim_edge_zeros(ts_B) # Mask out remaining zeros ts_B = np.ma.masked_where(ts_B == 0, ts_B) # Get repetition time from reference image tr = io_generator.reference_img.header.get_zooms()[-1] # Create indices for 6 cuts, based on dimensions cuts = [ts_B.shape[dim] // 6 for dim in range(3)] expl_text = "" # Remove trailing ';' from rationale column comptable["rationale"] = comptable["rationale"].str.rstrip(";") for compnum in comptable.index.values: if comptable.loc[compnum, "classification"] == "accepted": line_color = "g" expl_text = "accepted" elif comptable.loc[compnum, "classification"] == "rejected": line_color = "r" expl_text = "rejection reason(s): " + comptable.loc[compnum, "rationale"] elif comptable.loc[compnum, "classification"] == "ignored": line_color = "k" expl_text = "ignored reason(s): " + comptable.loc[compnum, "rationale"] else: # Classification not added # If new, this will keep code running line_color = "0.75" expl_text = "other classification" allplot = plt.figure(figsize=(10, 9)) ax_ts = plt.subplot2grid((5, 6), (0, 0), rowspan=1, colspan=6, fig=allplot) ax_ts.set_xlabel("TRs") ax_ts.set_xlim(0, n_vols) plt.yticks([]) # Make a second axis with units of time (s) max_xticks = 10 xloc = plt.MaxNLocator(max_xticks) ax_ts.xaxis.set_major_locator(xloc) ax_ts2 = ax_ts.twiny() ax1Xs = ax_ts.get_xticks() ax2Xs = [] for X in ax1Xs: # Limit to 2 decimal places seconds_val = round(X * tr, 2) ax2Xs.append(seconds_val) ax_ts2.set_xticks(ax1Xs) ax_ts2.set_xlim(ax_ts.get_xbound()) ax_ts2.set_xticklabels(ax2Xs) ax_ts2.set_xlabel("seconds") ax_ts.plot(mmix[:, compnum], color=line_color) # Title will include variance from comptable comp_var = "{0:.2f}".format(comptable.loc[compnum, "variance explained"]) comp_kappa = "{0:.2f}".format(comptable.loc[compnum, "kappa"]) comp_rho = "{0:.2f}".format(comptable.loc[compnum, "rho"]) plt_title = "Comp. {}: variance: {}%, kappa: {}, rho: {}, {}".format( compnum, comp_var, comp_kappa, comp_rho, expl_text) title = ax_ts.set_title(plt_title) title.set_y(1.5) # Set range to ~1/10th of max positive or negative beta imgmax = 0.1 * np.abs(ts_B[:, :, :, compnum]).max() imgmin = imgmax * -1 for idx, _ in enumerate(cuts): for imgslice in range(1, 6): ax = plt.subplot2grid((5, 6), (idx + 1, imgslice - 1), rowspan=1, colspan=1) ax.axis("off") if idx == 0: to_plot = np.rot90(ts_B[imgslice * cuts[idx], :, :, compnum]) if idx == 1: to_plot = np.rot90(ts_B[:, imgslice * cuts[idx], :, compnum]) if idx == 2: to_plot = ts_B[:, :, imgslice * cuts[idx], compnum] ax_im = ax.imshow(to_plot, vmin=imgmin, vmax=imgmax, aspect="equal", cmap=png_cmap) # Add a color bar to the plot. ax_cbar = allplot.add_axes([0.8, 0.3, 0.03, 0.37]) cbar = allplot.colorbar(ax_im, ax_cbar) cbar.set_label("Component Beta", rotation=90) cbar.ax.yaxis.set_label_position("left") # Get fft and freqs for this subject # adapted from @dangom spectrum, freqs = utils.get_spectrum(mmix[:, compnum], tr) # Plot it ax_fft = plt.subplot2grid((5, 6), (4, 0), rowspan=1, colspan=6) ax_fft.plot(freqs, spectrum) ax_fft.set_title("One Sided fft") ax_fft.set_xlabel("Hz") ax_fft.set_xlim(freqs[0], freqs[-1]) plt.yticks([]) # Fix spacing so TR label does overlap with other plots allplot.subplots_adjust(hspace=0.4) plot_name = "comp_{}.png".format(str(compnum).zfill(3)) compplot_name = os.path.join(io_generator.out_dir, "figures", plot_name) plt.savefig(compplot_name) plt.close()
def calculate_f_maps(data_cat, Z_maps, mixing, adaptive_mask, tes, f_max=500): """Calculate pseudo-F-statistic maps for TE-dependence and -independence models. Parameters ---------- data_cat : (M x E x T) array_like Multi-echo data, already masked. Z_maps : (M x C) array_like Z-statistic maps for components, reflecting voxel-wise component loadings. mixing : (T x C) array_like Mixing matrix adaptive_mask : (M) array_like Adaptive mask, where each voxel's value is the number of echoes with "good signal". Limited to masked voxels. tes : (E) array_like Echo times in milliseconds, in the same order as the echoes in data_cat. f_max : float, optional Maximum F-statistic, used to crop extreme values. Values in the F-statistic maps greater than this value are set to it. Returns ------- F_T2_maps, F_S0_maps, pred_T2_maps, pred_S0_maps : (M x C) array_like Pseudo-F-statistic maps for TE-dependence and -independence models, respectively. """ assert data_cat.shape[0] == Z_maps.shape[0] == adaptive_mask.shape[0] assert data_cat.shape[1] == tes.shape[0] assert data_cat.shape[2] == mixing.shape[0] assert Z_maps.shape[1] == mixing.shape[1] # TODO: Remove mask arg from get_coeffs me_betas = get_coeffs(data_cat, mixing, mask=np.ones(data_cat.shape[:2], bool), add_const=True) n_voxels, n_echos, n_components = me_betas.shape mu = data_cat.mean(axis=-1, dtype=float) tes = np.reshape(tes, (n_echos, 1)) # set up Xmats X1 = mu.T # Model 1 X2 = np.tile(tes, (1, n_voxels)) * mu.T # Model 2 F_T2_maps = np.zeros([n_voxels, n_components]) F_S0_maps = np.zeros([n_voxels, n_components]) pred_T2_maps = np.zeros([n_voxels, len(tes), n_components]) pred_S0_maps = np.zeros([n_voxels, len(tes), n_components]) for i_comp in range(n_components): # size of comp_betas is (n_echoes, n_samples) comp_betas = np.atleast_3d(me_betas)[:, :, i_comp].T alpha = (np.abs(comp_betas) ** 2).sum(axis=0) # Only analyze good echoes at each voxel for j_echo in np.unique(adaptive_mask[adaptive_mask >= 3]): mask_idx = adaptive_mask == j_echo alpha = (np.abs(comp_betas[:j_echo]) ** 2).sum(axis=0) # S0 Model # (S,) model coefficient map coeffs_S0 = (comp_betas[:j_echo] * X1[:j_echo, :]).sum(axis=0) / ( X1[:j_echo, :] ** 2 ).sum(axis=0) pred_S0 = X1[:j_echo, :] * np.tile(coeffs_S0, (j_echo, 1)) SSE_S0 = (comp_betas[:j_echo] - pred_S0) ** 2 SSE_S0 = SSE_S0.sum(axis=0) # (S,) prediction error map F_S0 = (alpha - SSE_S0) * (j_echo - 1) / (SSE_S0) F_S0[F_S0 > f_max] = f_max F_S0_maps[mask_idx, i_comp] = F_S0[mask_idx] # T2 Model coeffs_T2 = (comp_betas[:j_echo] * X2[:j_echo, :]).sum(axis=0) / ( X2[:j_echo, :] ** 2 ).sum(axis=0) pred_T2 = X2[:j_echo] * np.tile(coeffs_T2, (j_echo, 1)) SSE_T2 = (comp_betas[:j_echo] - pred_T2) ** 2 SSE_T2 = SSE_T2.sum(axis=0) F_T2 = (alpha - SSE_T2) * (j_echo - 1) / (SSE_T2) F_T2[F_T2 > f_max] = f_max F_T2_maps[mask_idx, i_comp] = F_T2[mask_idx] pred_S0_maps[mask_idx, :j_echo, i_comp] = pred_S0.T[mask_idx, :] pred_T2_maps[mask_idx, :j_echo, i_comp] = pred_T2.T[mask_idx, :] return F_T2_maps, F_S0_maps, pred_T2_maps, pred_S0_maps
def writeresults(ts, mask, comptable, mmix, n_vols, io_generator): """ Denoises `ts` and saves all resulting files to disk Parameters ---------- ts : (S x T) array_like Time series to denoise and save to disk mask : (S,) array_like Boolean mask array comptable : (C x X) :obj:`pandas.DataFrame` Component metric table. One row for each component, with a column for each metric. Requires at least two columns: "component" and "classification". mmix : (C x T) array_like Mixing matrix for converting input data to component space, where `C` is components and `T` is the same as in `data` n_vols : :obj:`int` Number of volumes in original time series ref_img : :obj:`str` or img_like Reference image to dictate how outputs are saved to disk Notes ----- This function writes out several files: ========================================= ===================================== Filename Content ========================================= ===================================== desc-optcomAccepted_bold.nii.gz High-Kappa time series. desc-optcomRejected_bold.nii.gz Low-Kappa time series. desc-optcomDenoised_bold.nii.gz Denoised time series. desc-ICA_components.nii.gz Spatial component maps for all components. desc-ICAAccepted_components.nii.gz Spatial component maps for accepted components. desc-ICAAccepted_stat-z_components.nii.gz Z-normalized spatial component maps for accepted components. ========================================= ===================================== See Also -------- tedana.io.write_split_ts: Writes out time series files """ acc = comptable[comptable.classification == "accepted"].index.values write_split_ts(ts, mmix, mask, comptable, io_generator) ts_B = get_coeffs(ts, mmix, mask) fout = io_generator.save_file(ts_B, "ICA components img") LGR.info("Writing full ICA coefficient feature set: {}".format(fout)) if len(acc) != 0: fout = io_generator.save_file(ts_B[:, acc], "ICA accepted components img") LGR.info( "Writing denoised ICA coefficient feature set: {}".format(fout)) # write feature versions of components feats = computefeats2( split_ts(ts, mmix, mask, comptable)[0], mmix[:, acc], mask) feats = utils.unmask(feats, mask) fname = io_generator.save_file(feats, "z-scored ICA accepted components img") LGR.info( "Writing Z-normalized spatial component maps: {}".format(fname))
def write_comp_figs(ts, mask, comptable, mmix, ref_img, out_dir, png_cmap): """ Creates static figures that highlight certain aspects of tedana processing This includes a figure for each component showing the component time course, the spatial weight map and a fast Fourier transform of the time course Parameters ---------- ts : (S x T) array_like Time series from which to derive ICA betas mask : (S,) array_like Boolean mask array comptable : (C x X) :obj:`pandas.DataFrame` Component metric table. One row for each component, with a column for each metric. The index should be the component number. mmix : (C x T) array_like Mixing matrix for converting input data to component space, where `C` is components and `T` is the same as in `data` ref_img : :obj:`str` or img_like Reference image to dictate how outputs are saved to disk out_dir : :obj:`str` Figures folder within output directory png_cmap : :obj:`str` The name of a matplotlib colormap to use when making figures. Optional. Default colormap is 'coolwarm' """ # Get the lenght of the timeseries n_vols = len(mmix) # Check that colormap provided exists if png_cmap not in plt.colormaps(): LGR.warning( 'Provided colormap is not recognized, proceeding with default') png_cmap = 'coolwarm' # regenerate the beta images ts_B = stats.get_coeffs(ts, mmix, mask) ts_B = ts_B.reshape(ref_img.shape[:3] + ts_B.shape[1:]) # trim edges from ts_B array ts_B = trim_edge_zeros(ts_B) # Mask out remaining zeros ts_B = np.ma.masked_where(ts_B == 0, ts_B) # Get repetition time from ref_img tr = ref_img.header.get_zooms()[-1] # Create indices for 6 cuts, based on dimensions cuts = [ts_B.shape[dim] // 6 for dim in range(3)] expl_text = '' # Remove trailing ';' from rationale column comptable['rationale'] = comptable['rationale'].str.rstrip(';') for compnum in comptable.index.values: if comptable.loc[compnum, "classification"] == 'accepted': line_color = 'g' expl_text = 'accepted' elif comptable.loc[compnum, "classification"] == 'rejected': line_color = 'r' expl_text = 'rejection reason(s): ' + comptable.loc[compnum, "rationale"] elif comptable.loc[compnum, "classification"] == 'ignored': line_color = 'k' expl_text = 'ignored reason(s): ' + comptable.loc[compnum, "rationale"] else: # Classification not added # If new, this will keep code running line_color = '0.75' expl_text = 'other classification' allplot = plt.figure(figsize=(10, 9)) ax_ts = plt.subplot2grid((5, 6), (0, 0), rowspan=1, colspan=6, fig=allplot) ax_ts.set_xlabel('TRs') ax_ts.set_xlim(0, n_vols) plt.yticks([]) # Make a second axis with units of time (s) max_xticks = 10 xloc = plt.MaxNLocator(max_xticks) ax_ts.xaxis.set_major_locator(xloc) ax_ts2 = ax_ts.twiny() ax1Xs = ax_ts.get_xticks() ax2Xs = [] for X in ax1Xs: # Limit to 2 decimal places seconds_val = round(X * tr, 2) ax2Xs.append(seconds_val) ax_ts2.set_xticks(ax1Xs) ax_ts2.set_xlim(ax_ts.get_xbound()) ax_ts2.set_xticklabels(ax2Xs) ax_ts2.set_xlabel('seconds') ax_ts.plot(mmix[:, compnum], color=line_color) # Title will include variance from comptable comp_var = "{0:.2f}".format(comptable.loc[compnum, "variance explained"]) comp_kappa = "{0:.2f}".format(comptable.loc[compnum, "kappa"]) comp_rho = "{0:.2f}".format(comptable.loc[compnum, "rho"]) plt_title = ('Comp. {}: variance: {}%, kappa: {}, rho: {}, ' '{}'.format(compnum, comp_var, comp_kappa, comp_rho, expl_text)) title = ax_ts.set_title(plt_title) title.set_y(1.5) # Set range to ~1/10th of max positive or negative beta imgmax = 0.1 * np.abs(ts_B[:, :, :, compnum]).max() imgmin = imgmax * -1 for idx, cut in enumerate(cuts): for imgslice in range(1, 6): ax = plt.subplot2grid((5, 6), (idx + 1, imgslice - 1), rowspan=1, colspan=1) ax.axis('off') if idx == 0: to_plot = np.rot90(ts_B[imgslice * cuts[idx], :, :, compnum]) if idx == 1: to_plot = np.rot90(ts_B[:, imgslice * cuts[idx], :, compnum]) if idx == 2: to_plot = ts_B[:, :, imgslice * cuts[idx], compnum] ax_im = ax.imshow(to_plot, vmin=imgmin, vmax=imgmax, aspect='equal', cmap=png_cmap) # Add a color bar to the plot. ax_cbar = allplot.add_axes([0.8, 0.3, 0.03, 0.37]) cbar = allplot.colorbar(ax_im, ax_cbar) cbar.set_label('Component Beta', rotation=90) cbar.ax.yaxis.set_label_position('left') # Get fft and freqs for this subject # adapted from @dangom spectrum, freqs = get_spectrum(mmix[:, compnum], tr) # Plot it ax_fft = plt.subplot2grid((5, 6), (4, 0), rowspan=1, colspan=6) ax_fft.plot(freqs, spectrum) ax_fft.set_title('One Sided fft') ax_fft.set_xlabel('Hz') ax_fft.set_xlim(freqs[0], freqs[-1]) plt.yticks([]) # Fix spacing so TR label does overlap with other plots allplot.subplots_adjust(hspace=0.4) plot_name = 'comp_{}.png'.format(str(compnum).zfill(3)) compplot_name = os.path.join(out_dir, plot_name) plt.savefig(compplot_name) plt.close()