def t2smap(data, tes, combmode='t2s', label=None): """ Estimate T2 and S0, and optimally combine data across TEs. Parameters ---------- data : :obj:`list` of :obj:`str` Either a single z-concatenated file (single-entry list) or a list of echo-specific files, in ascending order. tes : :obj:`list` List of echo times associated with data in milliseconds. combmode : {'t2s', 'ste'}, optional Combination scheme for TEs: 't2s' (Posse 1999, default), 'ste' (Poser). label : :obj:`str` or :obj:`None`, optional Label for output directory. Default is None. """ if label is not None: suf = '_%s' % str(label) else: suf = '' tes, data, combmode = tes, data, combmode tes = [float(te) for te in tes] n_echos = len(tes) catd = utils.load_data(data, n_echos=n_echos) _, n_echos, _ = catd.shape ref_img = data[0] if isinstance(data, list) else data LGR.info('Computing adaptive mask') mask, masksum = utils.make_adaptive_mask(catd, minimum=False, getsum=True) utils.filewrite(masksum, 'masksum%s' % suf, ref_img, copy_header=False) LGR.info('Computing adaptive T2* map') t2s, s0, t2ss, s0vs, _, _ = model.t2sadmap(catd, tes, mask, masksum, 2) utils.filewrite(t2ss, 't2ss%s' % suf, ref_img, copy_header=False) utils.filewrite(s0vs, 's0vs%s' % suf, ref_img, copy_header=False) LGR.info('Computing optimal combination') tsoc = np.array(model.make_optcom(catd, t2s, tes, mask, combmode), dtype=float) # Clean up numerical errors t2sm = t2s.copy() for n in (tsoc, s0, t2s, t2sm): np.nan_to_num(n, copy=False) s0[s0 < 0] = 0 t2s[t2s < 0] = 0 t2sm[t2sm < 0] = 0 utils.filewrite(tsoc, 'ocv%s' % suf, ref_img, copy_header=False) utils.filewrite(s0, 's0v%s' % suf, ref_img, copy_header=False) utils.filewrite(t2s, 't2sv%s' % suf, ref_img, copy_header=False) utils.filewrite(t2sm, 't2svm%s' % suf, ref_img, copy_header=False)
def fitmodels_direct(catd, mmix, mask, t2s, t2sG, tes, combmode, ref_img, fout=None, reindex=False, mmixN=None, full_sel=True): """ Fit models directly. Parameters ---------- catd : (S x E x T) array_like Input data, where `S` is samples, `E` is echos, 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 `catd` mask : (S,) array_like Boolean mask array t2s : (S,) array_like t2sG : (S,) array_like tes : list List of echo times associated with `catd`, in milliseconds combmode : {'t2s', 'ste'} str How optimal combination of echos should be made, where 't2s' indicates using the method of Posse 1999 and 'ste' indicates using the method of Poser 2006 ref_img : str or img_like Reference image to dictate how outputs are saved to disk fout : bool Whether to output per-component TE-dependence maps. Default: None reindex : bool, optional Default: False mmixN : array_like, optional Default: None full_sel : bool, optional Whether to perform selection of components based on Rho/Kappa scores. Default: True Returns ------- seldict : dict comptab : (N x 5) :obj:`numpy.ndarray` Array with columns denoting (1) index of component, (2) Kappa score of component, (3) Rho score of component, (4) variance explained by component, and (5) normalized variance explained bycomponent betas : :obj:`numpy.ndarray` mmix_new : :obj:`numpy.ndarray` """ # compute optimal combination of raw data tsoc = model.make_optcom(catd, t2sG, tes, mask, combmode, verbose=False).astype(float)[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(utils.unmask(tsoc, mask), mmixN, mask, normalize=False) # compute PSC dataset - shouldn't have to refit data tsoc_B = get_coeffs(utils.unmask(tsoc_dm, mask), mask, mmix)[mask] 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(catd, np.repeat(mask[:, np.newaxis], len(tes), axis=1), mmix) n_samp, n_echos, n_components = betas.shape n_voxels = mask.sum() n_data_voxels = (t2s != 0).sum() mu = catd.mean(axis=-1, dtype=float) tes = np.reshape(tes, (n_echos, 1)) fmin, fmid, fmax = utils.getfbounds(n_echos) # mask arrays mumask = mu[t2s != 0] t2smask = t2s[t2s != 0] betamask = betas[t2s != 0] # set up Xmats X1 = mumask.T # Model 1 X2 = np.tile(tes, (1, n_data_voxels)) * mumask.T / t2smask.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_data_voxels, n_components]) F_S0_maps = np.zeros([n_data_voxels, n_components]) Z_clmaps = np.zeros([n_voxels, n_components]) F_R2_clmaps = np.zeros([n_data_voxels, n_components]) F_S0_clmaps = np.zeros([n_data_voxels, n_components]) Br_clmaps_R2 = np.zeros([n_voxels, n_components]) Br_clmaps_S0 = np.zeros([n_voxels, n_components]) LGR.info('Fitting TE- and S0-dependent models to components') for i in range(n_components): # size of B is (n_components, nx*ny*nz) B = np.atleast_3d(betamask)[:, :, i].T alpha = (np.abs(B)**2).sum(axis=0) varex[i] = (tsoc_B[:, i]**2).sum() / totvar * 100. varex_norm[i] = (utils.unmask(WTS, mask)[t2s != 0][:, i]** 2).sum() / totvar_norm * 100. # S0 Model coeffs_S0 = (B * X1).sum(axis=0) / (X1**2).sum(axis=0) SSE_S0 = (B - X1 * np.tile(coeffs_S0, (n_echos, 1)))**2 SSE_S0 = SSE_S0.sum(axis=0) F_S0 = (alpha - SSE_S0) * 2 / (SSE_S0) F_S0_maps[:, i] = F_S0 # R2 Model coeffs_R2 = (B * X2).sum(axis=0) / (X2**2).sum(axis=0) SSE_R2 = (B - X2 * np.tile(coeffs_R2, (n_echos, 1)))**2 SSE_R2 = SSE_R2.sum(axis=0) F_R2 = (alpha - SSE_R2) * 2 / (SSE_R2) F_R2_maps[:, i] = F_R2 # compute weights as Z-values wtsZ = (WTS[:, i] - WTS[:, i].mean()) / WTS[:, i].std() wtsZ[np.abs(wtsZ) > Z_MAX] = ( Z_MAX * (np.abs(wtsZ) / wtsZ))[np.abs(wtsZ) > Z_MAX] Z_maps[:, i] = 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( np.squeeze(utils.unmask(wtsZ, mask)[t2s != 0]**2.)) Kappas[i] = np.average(F_R2, weights=norm_weights) Rhos[i] = np.average(F_S0, weights=norm_weights) # tabulate component values comptab_pre = np.vstack( [np.arange(n_components), Kappas, Rhos, varex, varex_norm]).T if reindex: # re-index all components in Kappa order comptab = comptab_pre[comptab_pre[:, 1].argsort()[::-1], :] Kappas = comptab[:, 1] Rhos = comptab[:, 2] varex = comptab[:, 3] varex_norm = comptab[:, 4] nnc = np.array(comptab[:, 0], dtype=np.int) mmix_new = mmix[:, nnc] F_S0_maps = F_S0_maps[:, nnc] F_R2_maps = F_R2_maps[:, nnc] Z_maps = Z_maps[:, nnc] WTS = WTS[:, nnc] PSC = PSC[:, nnc] tsoc_B = tsoc_B[:, nnc] tsoc_Babs = tsoc_Babs[:, nnc] comptab[:, 0] = np.arange(comptab.shape[0]) else: comptab = comptab_pre mmix_new = mmix # full selection including clustering criteria seldict = None if full_sel: 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 in range(n_components): # save out files out = np.zeros((n_samp, 4)) out[:, 0] = np.squeeze(utils.unmask(PSC[:, i], mask)) out[:, 1] = np.squeeze(utils.unmask(F_R2_maps[:, i], t2s != 0)) out[:, 2] = np.squeeze(utils.unmask(F_S0_maps[:, i], t2s != 0)) out[:, 3] = np.squeeze(utils.unmask(Z_maps[:, i], mask)) if utils.get_dtype(ref_img) == 'GIFTI': continue # TODO: pass through GIFTI file data as below ccimg = utils.new_nii_like(ref_img, out) # Do simple clustering on F sel = spatclust(ccimg, min_cluster_size=csize, threshold=int(fmin), index=[1, 2], mask=(t2s != 0)) F_R2_clmaps[:, i] = sel[:, 0] F_S0_clmaps[:, i] = sel[:, 1] countsigFR2 = F_R2_clmaps[:, i].sum() countsigFS0 = F_S0_clmaps[:, i].sum() # Do simple clustering on Z at p<0.05 sel = spatclust(ccimg, min_cluster_size=csize, threshold=1.95, index=3, mask=mask) Z_clmaps[:, i] = sel # Do simple clustering on ranked signal-change map spclust_input = utils.unmask(stats.rankdata(tsoc_Babs[:, i]), mask) spclust_input = utils.new_nii_like(ref_img, spclust_input) Br_clmaps_R2[:, i] = spatclust(spclust_input, min_cluster_size=csize, threshold=max(tsoc_Babs.shape) - countsigFR2, mask=mask) Br_clmaps_S0[:, i] = spatclust(spclust_input, min_cluster_size=csize, threshold=max(tsoc_Babs.shape) - countsigFS0, mask=mask) seldict = {} selvars = [ 'Kappas', 'Rhos', 'WTS', 'varex', 'Z_maps', 'F_R2_maps', 'F_S0_maps', 'Z_clmaps', 'F_R2_clmaps', 'F_S0_clmaps', 'tsoc_B', 'Br_clmaps_R2', 'Br_clmaps_S0', 'PSC' ] for vv in selvars: seldict[vv] = eval(vv) return seldict, comptab, betas, mmix_new
def tedana(data, tes, mixm=None, ctab=None, manacc=None, strict=False, gscontrol=True, kdaw=10., rdaw=1., conv=2.5e-5, ste=-1, combmode='t2s', dne=False, initcost='tanh', finalcost='tanh', stabilize=False, fout=False, filecsdata=False, label=None, fixed_seed=42, debug=False, quiet=False): """ Run the "canonical" TE-Dependent ANAlysis workflow. Parameters ---------- data : :obj:`list` of :obj:`str` Either a single z-concatenated file (single-entry list) or a list of echo-specific files, in ascending order. tes : :obj:`list` List of echo times associated with data in milliseconds. mixm : :obj:`str`, optional File containing mixing matrix. If not provided, ME-PCA and ME-ICA are done. ctab : :obj:`str`, optional File containing component table from which to extract pre-computed classifications. manacc : :obj:`str`, optional Comma separated list of manually accepted components in string form. Default is None. strict : :obj:`bool`, optional Ignore low-variance ambiguous components. Default is False. gscontrol : :obj:`bool`, optional Control global signal using spatial approach. Default is True. kdaw : :obj:`float`, optional Dimensionality augmentation weight (Kappa). Default is 10. -1 for low-dimensional ICA. rdaw : :obj:`float`, optional Dimensionality augmentation weight (Rho). Default is 1. -1 for low-dimensional ICA. conv : :obj:`float`, optional Convergence limit. Default is 2.5e-5. ste : :obj:`int`, optional Source TEs for models. 0 for all, -1 for optimal combination. Default is -1. combmode : {'t2s', 'ste'}, optional Combination scheme for TEs: 't2s' (Posse 1999, default), 'ste' (Poser). dne : :obj:`bool`, optional Denoise each TE dataset separately. Default is False. initcost : {'tanh', 'pow3', 'gaus', 'skew'}, optional Initial cost function for ICA. Default is 'tanh'. finalcost : {'tanh', 'pow3', 'gaus', 'skew'}, optional Final cost function. Default is 'tanh'. stabilize : :obj:`bool`, optional Stabilize convergence by reducing dimensionality, for low quality data. Default is False. fout : :obj:`bool`, optional Save output TE-dependence Kappa/Rho SPMs. Default is False. filecsdata : :obj:`bool`, optional Save component selection data to file. Default is False. label : :obj:`str` or :obj:`None`, optional Label for output directory. Default is None. fixed_seed : :obj:`int`, optional Seeded value for ICA, for reproducibility. """ # ensure tes are in appropriate format tes = [float(te) for te in tes] n_echos = len(tes) # coerce data to samples x echos x time array LGR.info('Loading input data: {}'.format([op.abspath(f) for f in data])) catd, ref_img = utils.load_data(data, n_echos=n_echos) n_samp, n_echos, n_vols = catd.shape LGR.debug('Resulting data shape: {}'.format(catd.shape)) if fout: fout = ref_img else: fout = None kdaw, rdaw = float(kdaw), float(rdaw) if label is not None: out_dir = 'TED.{0}'.format(label) else: out_dir = 'TED' out_dir = op.abspath(out_dir) if not op.isdir(out_dir): LGR.info('Creating output directory: {}'.format(out_dir)) os.mkdir(out_dir) else: LGR.info('Using output directory: {}'.format(out_dir)) if mixm is not None and op.isfile(mixm): shutil.copyfile(mixm, op.join(out_dir, 'meica_mix.1D')) shutil.copyfile(mixm, op.join(out_dir, op.basename(mixm))) elif mixm is not None: raise IOError('Argument "mixm" must be an existing file.') if ctab is not None and op.isfile(ctab): shutil.copyfile(ctab, op.join(out_dir, 'comp_table.txt')) shutil.copyfile(ctab, op.join(out_dir, op.basename(ctab))) elif ctab is not None: raise IOError('Argument "ctab" must be an existing file.') os.chdir(out_dir) LGR.info('Computing adapative mask') mask, masksum = utils.make_adaptive_mask(catd, minimum=False, getsum=True) LGR.debug('Retaining {}/{} samples'.format(mask.sum(), n_samp)) LGR.info('Computing T2* map') t2s, s0, t2ss, s0s, t2sG, s0G = model.fit_decay(catd, tes, mask, masksum, start_echo=1) # set a hard cap for the T2* map # anything that is 10x higher than the 99.5 %ile will be reset to 99.5 %ile cap_t2s = stats.scoreatpercentile(t2s.flatten(), 99.5, interpolation_method='lower') LGR.debug('Setting cap on T2* map at {:.5f}'.format(cap_t2s * 10)) t2s[t2s > cap_t2s * 10] = cap_t2s utils.filewrite(t2s, op.join(out_dir, 't2sv'), ref_img) utils.filewrite(s0, op.join(out_dir, 's0v'), ref_img) utils.filewrite(t2ss, op.join(out_dir, 't2ss'), ref_img) utils.filewrite(s0s, op.join(out_dir, 's0vs'), ref_img) utils.filewrite(t2sG, op.join(out_dir, 't2svG'), ref_img) utils.filewrite(s0G, op.join(out_dir, 's0vG'), ref_img) # optimally combine data OCcatd = model.make_optcom(catd, t2sG, tes, mask, combmode) # regress out global signal unless explicitly not desired if gscontrol: catd, OCcatd = model.gscontrol_raw(catd, OCcatd, n_echos, ref_img) if mixm is None: n_components, dd = decomposition.tedpca(catd, OCcatd, combmode, mask, t2s, t2sG, stabilize, ref_img, tes=tes, kdaw=kdaw, rdaw=rdaw, ste=ste) mmix_orig = decomposition.tedica(n_components, dd, conv, fixed_seed, cost=initcost, final_cost=finalcost, verbose=debug) np.savetxt(op.join(out_dir, '__meica_mix.1D'), mmix_orig) LGR.info('Making second component selection guess from ICA results') seldict, comptable, betas, mmix = model.fitmodels_direct(catd, mmix_orig, mask, t2s, t2sG, tes, combmode, ref_img, fout=fout, reindex=True) np.savetxt(op.join(out_dir, 'meica_mix.1D'), mmix) acc, rej, midk, empty = selection.selcomps(seldict, mmix, mask, ref_img, manacc, n_echos, t2s, s0, strict_mode=strict, filecsdata=filecsdata) else: LGR.info('Using supplied mixing matrix from ICA') mmix_orig = np.loadtxt(op.join(out_dir, 'meica_mix.1D')) seldict, comptable, betas, mmix = model.fitmodels_direct(catd, mmix_orig, mask, t2s, t2sG, tes, combmode, ref_img, fout=fout) if ctab is None: acc, rej, midk, empty = selection.selcomps(seldict, mmix, mask, ref_img, manacc, n_echos, t2s, s0, filecsdata=filecsdata, strict_mode=strict) else: acc, rej, midk, empty = utils.ctabsel(ctab) if len(acc) == 0: LGR.warning( 'No BOLD components detected! Please check data and results!') utils.writeresults(OCcatd, mask, comptable, mmix, n_vols, acc, rej, midk, empty, ref_img) utils.gscontrol_mmix(OCcatd, mmix, mask, acc, rej, midk, ref_img) if dne: utils.writeresults_echoes(catd, mmix, mask, acc, rej, midk, ref_img)
def t2smap_workflow(data, tes, fitmode='all', combmode='t2s', label=None): """ Estimate T2 and S0, and optimally combine data across TEs. Parameters ---------- data : :obj:`str` or :obj:`list` of :obj:`str` Either a single z-concatenated file (single-entry list or str) or a list of echo-specific files, in ascending order. tes : :obj:`list` List of echo times associated with data in milliseconds. fitmode : {'all', 'ts'}, optional Monoexponential model fitting scheme. 'all' means that the model is fit, per voxel, across all timepoints. 'ts' means that the model is fit, per voxel and per timepoint. Default is 'all'. combmode : {'t2s', 'ste'}, optional Combination scheme for TEs: 't2s' (Posse 1999, default), 'ste' (Poser). label : :obj:`str` or :obj:`None`, optional Label for output directory. Default is None. Notes ----- This workflow writes out several files, which are written out to a folder named TED.[ref_label].[label] if ``label`` is provided and TED.[ref_label] if not. ``ref_label`` is determined based on the name of the first ``data`` file. Files are listed below: ====================== ================================================= Filename Content ====================== ================================================= t2sv.nii Limited estimated T2* 3D map or 4D timeseries. Will be a 3D map if ``fitmode`` is 'all' and a 4D timeseries if it is 'ts'. s0v.nii Limited S0 3D map or 4D timeseries. t2svG.nii Full T2* map/timeseries. The difference between the limited and full maps is that, for voxels affected by dropout where only one echo contains good data, the full map uses the single echo's value while the limited map has a NaN. s0vG.nii Full S0 map/timeseries. ts_OC.nii Optimally combined timeseries. ====================== ================================================= """ # ensure tes are in appropriate format tes = [float(te) for te in tes] n_echos = len(tes) # coerce data to samples x echos x time array if isinstance(data, str): data = [data] LGR.info('Loading input data: {}'.format([f for f in data])) catd, ref_img = utils.load_data(data, n_echos=n_echos) n_samp, n_echos, n_vols = catd.shape LGR.debug('Resulting data shape: {}'.format(catd.shape)) try: ref_label = os.path.basename(ref_img).split('.')[0] except TypeError: ref_label = os.path.basename(str(data[0])).split('.')[0] if label is not None: out_dir = 'TED.{0}.{1}'.format(ref_label, label) else: out_dir = 'TED.{0}'.format(ref_label) out_dir = op.abspath(out_dir) if not op.isdir(out_dir): LGR.info('Creating output directory: {}'.format(out_dir)) os.mkdir(out_dir) else: LGR.info('Using output directory: {}'.format(out_dir)) LGR.info('Computing adaptive mask') mask, masksum = utils.make_adaptive_mask(catd, minimum=False, getsum=True) LGR.info('Computing adaptive T2* map') if fitmode == 'all': (t2s_limited, s0_limited, t2ss, s0s, t2s_full, s0_full) = model.fit_decay(catd, tes, mask, masksum, start_echo=1) else: (t2s_limited, s0_limited, t2s_full, s0_full) = model.fit_decay_ts(catd, tes, mask, masksum, start_echo=1) # set a hard cap for the T2* map/timeseries # anything that is 10x higher than the 99.5 %ile will be reset to 99.5 %ile cap_t2s = stats.scoreatpercentile(t2s_limited.flatten(), 99.5, interpolation_method='lower') LGR.debug('Setting cap on T2* map at {:.5f}'.format(cap_t2s * 10)) t2s_limited[t2s_limited > cap_t2s * 10] = cap_t2s LGR.info('Computing optimal combination') # optimally combine data OCcatd = model.make_optcom(catd, tes, mask, t2s=t2s_full, combmode=combmode) # clean up numerical errors for arr in (OCcatd, s0_limited, t2s_limited): np.nan_to_num(arr, copy=False) s0_limited[s0_limited < 0] = 0 t2s_limited[t2s_limited < 0] = 0 utils.filewrite(t2s_limited, op.join(out_dir, 't2sv.nii'), ref_img) utils.filewrite(s0_limited, op.join(out_dir, 's0v.nii'), ref_img) utils.filewrite(t2s_full, op.join(out_dir, 't2svG.nii'), ref_img) utils.filewrite(s0_full, op.join(out_dir, 's0vG.nii'), ref_img) utils.filewrite(OCcatd, op.join(out_dir, 'ts_OC.nii'), ref_img)
def tedana(data, tes, mixm=None, ctab=None, manacc=None, strict=False, gscontrol=True, kdaw=10., rdaw=1., conv=2.5e-5, ste=-1, combmode='t2s', dne=False, initcost='tanh', finalcost='tanh', stabilize=False, fout=False, filecsdata=False, label=None, fixed_seed=42, debug=False, quiet=False): """ Run the "canonical" TE-Dependent ANAlysis workflow. Parameters ---------- data : :obj:`list` of :obj:`str` Either a single z-concatenated file (single-entry list) or a list of echo-specific files, in ascending order. tes : :obj:`list` List of echo times associated with data in milliseconds. mixm : :obj:`str`, optional File containing mixing matrix. If not provided, ME-PCA and ME-ICA are done. ctab : :obj:`str`, optional File containing component table from which to extract pre-computed classifications. manacc : :obj:`str`, optional Comma separated list of manually accepted components in string form. Default is None. strict : :obj:`bool`, optional Ignore low-variance ambiguous components. Default is False. gscontrol : :obj:`bool`, optional Control global signal using spatial approach. Default is True. kdaw : :obj:`float`, optional Dimensionality augmentation weight (Kappa). Default is 10. -1 for low-dimensional ICA. rdaw : :obj:`float`, optional Dimensionality augmentation weight (Rho). Default is 1. -1 for low-dimensional ICA. conv : :obj:`float`, optional Convergence limit. Default is 2.5e-5. ste : :obj:`int`, optional Source TEs for models. 0 for all, -1 for optimal combination. Default is -1. combmode : {'t2s', 'ste'}, optional Combination scheme for TEs: 't2s' (Posse 1999, default), 'ste' (Poser). dne : :obj:`bool`, optional Denoise each TE dataset separately. Default is False. initcost : {'tanh', 'pow3', 'gaus', 'skew'}, optional Initial cost function for ICA. Default is 'tanh'. finalcost : {'tanh', 'pow3', 'gaus', 'skew'}, optional Final cost function. Default is 'tanh'. stabilize : :obj:`bool`, optional Stabilize convergence by reducing dimensionality, for low quality data. Default is False. fout : :obj:`bool`, optional Save output TE-dependence Kappa/Rho SPMs. Default is False. filecsdata : :obj:`bool`, optional Save component selection data to file. Default is False. label : :obj:`str` or :obj:`None`, optional Label for output directory. Default is None. fixed_seed : :obj:`int`, optional Seeded value for ICA, for reproducibility. """ # ensure tes are in appropriate format tes = [float(te) for te in tes] n_echos = len(tes) # coerce data to samples x echos x time array LGR.info('Loading input data: {}'.format([op.abspath(f) for f in data])) catd, ref_img = utils.load_data(data, n_echos=n_echos) n_samp, n_echos, n_vols = catd.shape LGR.debug('Resulting data shape: {}'.format(catd.shape)) if fout: fout = ref_img else: fout = None kdaw, rdaw = float(kdaw), float(rdaw) if label is not None: out_dir = 'TED.{0}'.format(label) else: out_dir = 'TED' out_dir = op.abspath(out_dir) if not op.isdir(out_dir): LGR.info('Creating output directory: {}'.format(out_dir)) os.mkdir(out_dir) else: LGR.info('Using output directory: {}'.format(out_dir)) if mixm is not None and op.isfile(mixm): shutil.copyfile(mixm, op.join(out_dir, 'meica_mix.1D')) shutil.copyfile(mixm, op.join(out_dir, op.basename(mixm))) elif mixm is not None: raise IOError('Argument "mixm" must be an existing file.') if ctab is not None and op.isfile(ctab): shutil.copyfile(ctab, op.join(out_dir, 'comp_table.txt')) shutil.copyfile(ctab, op.join(out_dir, op.basename(ctab))) elif ctab is not None: raise IOError('Argument "ctab" must be an existing file.') os.chdir(out_dir) LGR.info('Computing adapative mask') mask, masksum = utils.make_adaptive_mask(catd, minimum=False, getsum=True) LGR.debug('Retaining {}/{} samples'.format(mask.sum(), n_samp)) LGR.info('Computing T2* map') t2s, s0, t2ss, s0s, t2sG, s0G = model.fit_decay(catd, tes, mask, masksum, start_echo=1) # set a hard cap for the T2* map # anything that is 10x higher than the 99.5 %ile will be reset to 99.5 %ile cap_t2s = stats.scoreatpercentile(t2s.flatten(), 99.5, interpolation_method='lower') LGR.debug('Setting cap on T2* map at {:.5f}'.format(cap_t2s * 10)) t2s[t2s > cap_t2s * 10] = cap_t2s utils.filewrite(t2s, op.join(out_dir, 't2sv'), ref_img) utils.filewrite(s0, op.join(out_dir, 's0v'), ref_img) utils.filewrite(t2ss, op.join(out_dir, 't2ss'), ref_img) utils.filewrite(s0s, op.join(out_dir, 's0vs'), ref_img) utils.filewrite(t2sG, op.join(out_dir, 't2svG'), ref_img) utils.filewrite(s0G, op.join(out_dir, 's0vG'), ref_img) # optimally combine data OCcatd = model.make_optcom(catd, t2sG, tes, mask, combmode) # regress out global signal unless explicitly not desired if gscontrol: catd, OCcatd = model.gscontrol_raw(catd, OCcatd, n_echos, ref_img) if mixm is None: n_components, dd = decomposition.tedpca(catd, OCcatd, combmode, mask, t2s, t2sG, stabilize, ref_img, tes=tes, kdaw=kdaw, rdaw=rdaw, ste=ste) mmix_orig = decomposition.tedica(n_components, dd, conv, fixed_seed, cost=initcost, final_cost=finalcost, verbose=debug) np.savetxt(op.join(out_dir, '__meica_mix.1D'), mmix_orig) LGR.info('Making second component selection guess from ICA results') seldict, comptable, betas, mmix = model.fitmodels_direct(catd, mmix_orig, mask, t2s, t2sG, tes, combmode, ref_img, fout=fout, reindex=True) np.savetxt(op.join(out_dir, 'meica_mix.1D'), mmix) acc, rej, midk, empty = selection.selcomps(seldict, mmix, mask, ref_img, manacc, n_echos, t2s, s0, strict_mode=strict, filecsdata=filecsdata) else: LGR.info('Using supplied mixing matrix from ICA') mmix_orig = np.loadtxt(op.join(out_dir, 'meica_mix.1D')) seldict, comptable, betas, mmix = model.fitmodels_direct(catd, mmix_orig, mask, t2s, t2sG, tes, combmode, ref_img, fout=fout) if ctab is None: acc, rej, midk, empty = selection.selcomps(seldict, mmix, mask, ref_img, manacc, n_echos, t2s, s0, filecsdata=filecsdata, strict_mode=strict) else: acc, rej, midk, empty = utils.ctabsel(ctab) if len(acc) == 0: LGR.warning('No BOLD components detected! Please check data and results!') utils.writeresults(OCcatd, mask, comptable, mmix, n_vols, acc, rej, midk, empty, ref_img) utils.gscontrol_mmix(OCcatd, mmix, mask, acc, rej, midk, ref_img) if dne: utils.writeresults_echoes(catd, mmix, mask, acc, rej, midk, ref_img)
def fitmodels_direct(catd, mmix, mask, t2s, t2sG, tes, combmode, ref_img, fout=None, reindex=False, mmixN=None, full_sel=True): """ Fit models directly. Parameters ---------- catd : (S x E x T) array_like Input data, where `S` is samples, `E` is echos, 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 `catd` mask : (S,) array_like Boolean mask array t2s : (S,) array_like t2sG : (S,) array_like tes : list List of echo times associated with `catd`, in milliseconds combmode : {'t2s', 'ste'} str How optimal combination of echos should be made, where 't2s' indicates using the method of Posse 1999 and 'ste' indicates using the method of Poser 2006 ref_img : str or img_like Reference image to dictate how outputs are saved to disk fout : bool Whether to output per-component TE-dependence maps. Default: None reindex : bool, optional Default: False mmixN : array_like, optional Default: None full_sel : bool, optional Whether to perform selection of components based on Rho/Kappa scores. Default: True Returns ------- seldict : dict comptab : (N x 5) :obj:`numpy.ndarray` Array with columns denoting (1) index of component, (2) Kappa score of component, (3) Rho score of component, (4) variance explained by component, and (5) normalized variance explained bycomponent betas : :obj:`numpy.ndarray` mmix_new : :obj:`numpy.ndarray` """ # compute optimal combination of raw data tsoc = model.make_optcom(catd, t2sG, tes, mask, combmode, verbose=False).astype(float)[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(utils.unmask(tsoc, mask), mmixN, mask, normalize=False) # compute PSC dataset - shouldn't have to refit data tsoc_B = get_coeffs(utils.unmask(tsoc_dm, mask), mask, mmix)[mask] 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(catd, np.repeat(mask[:, np.newaxis], len(tes), axis=1), mmix) n_samp, n_echos, n_components = betas.shape n_voxels = mask.sum() n_data_voxels = (t2s != 0).sum() mu = catd.mean(axis=-1, dtype=float) tes = np.reshape(tes, (n_echos, 1)) fmin, fmid, fmax = utils.getfbounds(n_echos) # mask arrays mumask = mu[t2s != 0] t2smask = t2s[t2s != 0] betamask = betas[t2s != 0] # set up Xmats X1 = mumask.T # Model 1 X2 = np.tile(tes, (1, n_data_voxels)) * mumask.T / t2smask.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_data_voxels, n_components]) F_S0_maps = np.zeros([n_data_voxels, n_components]) Z_clmaps = np.zeros([n_voxels, n_components]) F_R2_clmaps = np.zeros([n_data_voxels, n_components]) F_S0_clmaps = np.zeros([n_data_voxels, n_components]) Br_clmaps_R2 = np.zeros([n_voxels, n_components]) Br_clmaps_S0 = np.zeros([n_voxels, n_components]) LGR.info('Fitting TE- and S0-dependent models to components') for i in range(n_components): # size of B is (n_components, nx*ny*nz) B = np.atleast_3d(betamask)[:, :, i].T alpha = (np.abs(B)**2).sum(axis=0) varex[i] = (tsoc_B[:, i]**2).sum() / totvar * 100. varex_norm[i] = (utils.unmask(WTS, mask)[t2s != 0][:, i]**2).sum() / totvar_norm * 100. # S0 Model coeffs_S0 = (B * X1).sum(axis=0) / (X1**2).sum(axis=0) SSE_S0 = (B - X1 * np.tile(coeffs_S0, (n_echos, 1)))**2 SSE_S0 = SSE_S0.sum(axis=0) F_S0 = (alpha - SSE_S0) * 2 / (SSE_S0) F_S0_maps[:, i] = F_S0 # R2 Model coeffs_R2 = (B * X2).sum(axis=0) / (X2**2).sum(axis=0) SSE_R2 = (B - X2 * np.tile(coeffs_R2, (n_echos, 1)))**2 SSE_R2 = SSE_R2.sum(axis=0) F_R2 = (alpha - SSE_R2) * 2 / (SSE_R2) F_R2_maps[:, i] = F_R2 # compute weights as Z-values wtsZ = (WTS[:, i] - WTS[:, i].mean()) / WTS[:, i].std() wtsZ[np.abs(wtsZ) > Z_MAX] = (Z_MAX * (np.abs(wtsZ) / wtsZ))[np.abs(wtsZ) > Z_MAX] Z_maps[:, i] = 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(np.squeeze(utils.unmask(wtsZ, mask)[t2s != 0]**2.)) Kappas[i] = np.average(F_R2, weights=norm_weights) Rhos[i] = np.average(F_S0, weights=norm_weights) # tabulate component values comptab_pre = np.vstack([np.arange(n_components), Kappas, Rhos, varex, varex_norm]).T if reindex: # re-index all components in Kappa order comptab = comptab_pre[comptab_pre[:, 1].argsort()[::-1], :] Kappas = comptab[:, 1] Rhos = comptab[:, 2] varex = comptab[:, 3] varex_norm = comptab[:, 4] nnc = np.array(comptab[:, 0], dtype=np.int) mmix_new = mmix[:, nnc] F_S0_maps = F_S0_maps[:, nnc] F_R2_maps = F_R2_maps[:, nnc] Z_maps = Z_maps[:, nnc] WTS = WTS[:, nnc] PSC = PSC[:, nnc] tsoc_B = tsoc_B[:, nnc] tsoc_Babs = tsoc_Babs[:, nnc] comptab[:, 0] = np.arange(comptab.shape[0]) else: comptab = comptab_pre mmix_new = mmix # full selection including clustering criteria seldict = None if full_sel: 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 in range(n_components): # save out files out = np.zeros((n_samp, 4)) out[:, 0] = np.squeeze(utils.unmask(PSC[:, i], mask)) out[:, 1] = np.squeeze(utils.unmask(F_R2_maps[:, i], t2s != 0)) out[:, 2] = np.squeeze(utils.unmask(F_S0_maps[:, i], t2s != 0)) out[:, 3] = np.squeeze(utils.unmask(Z_maps[:, i], mask)) if utils.get_dtype(ref_img) == 'GIFTI': continue # TODO: pass through GIFTI file data as below ccimg = utils.new_nii_like(ref_img, out) # Do simple clustering on F sel = spatclust(ccimg, min_cluster_size=csize, threshold=int(fmin), index=[1, 2], mask=(t2s != 0)) F_R2_clmaps[:, i] = sel[:, 0] F_S0_clmaps[:, i] = sel[:, 1] countsigFR2 = F_R2_clmaps[:, i].sum() countsigFS0 = F_S0_clmaps[:, i].sum() # Do simple clustering on Z at p<0.05 sel = spatclust(ccimg, min_cluster_size=csize, threshold=1.95, index=3, mask=mask) Z_clmaps[:, i] = sel # Do simple clustering on ranked signal-change map spclust_input = utils.unmask(stats.rankdata(tsoc_Babs[:, i]), mask) spclust_input = utils.new_nii_like(ref_img, spclust_input) Br_clmaps_R2[:, i] = spatclust(spclust_input, min_cluster_size=csize, threshold=max(tsoc_Babs.shape)-countsigFR2, mask=mask) Br_clmaps_S0[:, i] = spatclust(spclust_input, min_cluster_size=csize, threshold=max(tsoc_Babs.shape)-countsigFS0, mask=mask) seldict = {} selvars = ['Kappas', 'Rhos', 'WTS', 'varex', 'Z_maps', 'F_R2_maps', 'F_S0_maps', 'Z_clmaps', 'F_R2_clmaps', 'F_S0_clmaps', 'tsoc_B', 'Br_clmaps_R2', 'Br_clmaps_S0', 'PSC'] for vv in selvars: seldict[vv] = eval(vv) return seldict, comptab, betas, mmix_new
def tedana_workflow(data, tes, mask=None, mixm=None, ctab=None, manacc=None, strict=False, gscontrol=True, kdaw=10., rdaw=1., conv=2.5e-5, ste=-1, combmode='t2s', dne=False, initcost='tanh', finalcost='tanh', stabilize=False, filecsdata=False, wvpca=False, label=None, fixed_seed=42, debug=False, quiet=False): """ Run the "canonical" TE-Dependent ANAlysis workflow. Parameters ---------- data : :obj:`str` or :obj:`list` of :obj:`str` Either a single z-concatenated file (single-entry list or str) or a list of echo-specific files, in ascending order. tes : :obj:`list` List of echo times associated with data in milliseconds. mask : :obj:`str`, optional Binary mask of voxels to include in TE Dependent ANAlysis. Must be spatially aligned with `data`. mixm : :obj:`str`, optional File containing mixing matrix. If not provided, ME-PCA and ME-ICA are done. ctab : :obj:`str`, optional File containing component table from which to extract pre-computed classifications. manacc : :obj:`str`, optional Comma separated list of manually accepted components in string form. Default is None. strict : :obj:`bool`, optional Ignore low-variance ambiguous components. Default is False. gscontrol : :obj:`bool`, optional Control global signal using spatial approach. Default is True. kdaw : :obj:`float`, optional Dimensionality augmentation weight (Kappa). Default is 10. -1 for low-dimensional ICA. rdaw : :obj:`float`, optional Dimensionality augmentation weight (Rho). Default is 1. -1 for low-dimensional ICA. conv : :obj:`float`, optional Convergence limit. Default is 2.5e-5. ste : :obj:`int`, optional Source TEs for models. 0 for all, -1 for optimal combination. Default is -1. combmode : {'t2s', 'ste'}, optional Combination scheme for TEs: 't2s' (Posse 1999, default), 'ste' (Poser). dne : :obj:`bool`, optional Denoise each TE dataset separately. Default is False. initcost : {'tanh', 'pow3', 'gaus', 'skew'}, optional Initial cost function for ICA. Default is 'tanh'. finalcost : {'tanh', 'pow3', 'gaus', 'skew'}, optional Final cost function. Default is 'tanh'. stabilize : :obj:`bool`, optional Stabilize convergence by reducing dimensionality, for low quality data. Default is False. filecsdata : :obj:`bool`, optional Save component selection data to file. Default is False. wvpca : :obj:`bool`, optional Whether or not to perform PCA on wavelet-transformed data. Default is False. label : :obj:`str` or :obj:`None`, optional Label for output directory. Default is None. Other Parameters ---------------- fixed_seed : :obj:`int`, optional Value passed to ``mdp.numx_rand.seed()``. Set to a positive integer value for reproducible ICA results; otherwise, set to -1 for varying results across calls. debug : :obj:`bool`, optional Whether to run in debugging mode or not. Default is False. quiet : :obj:`bool`, optional If True, suppresses logging/printing of messages. Default is False. Notes ----- PROCEDURE 2 : Computes ME-PCA and ME-ICA - Computes T2* map - Computes PCA of concatenated ME data, then computes TE-dependence of PCs - Computes ICA of TE-dependence PCs - Identifies TE-dependent ICs, outputs high-\kappa (BOLD) component and denoised time series or computes TE-dependence of each component of a general linear model specified by input (includes MELODIC FastICA mixing matrix) PROCEDURE 2a: Model fitting and component selection routines This workflow writes out several files, which are written out to a folder named TED.[ref_label].[label] if ``label`` is provided and TED.[ref_label] if not. ``ref_label`` is determined based on the name of the first ``data`` file. Files are listed below: ====================== ================================================= Filename Content ====================== ================================================= t2sv.nii Limited estimated T2* 3D map. The difference between the limited and full maps is that, for voxels affected by dropout where only one echo contains good data, the full map uses the single echo's value while the limited map has a NaN. s0v.nii Limited S0 3D map. The difference between the limited and full maps is that, for voxels affected by dropout where only one echo contains good data, the full map uses the single echo's value while the limited map has a NaN. t2ss.nii ??? s0vs.nii ??? t2svG.nii Full T2* map/timeseries. The difference between the limited and full maps is that, for voxels affected by dropout where only one echo contains good data, the full map uses the single echo's value while the limited map has a NaN. s0vG.nii Full S0 map/timeseries. __meica_mix.1D A mixing matrix meica_mix.1D Another mixing matrix ts_OC.nii Optimally combined timeseries. 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 comp_table.txt Component table sphis_hik.nii T1-like effect hik_ts_OC_T1c.nii T1 corrected time series by regression dn_ts_OC_T1c.nii ME-DN version of T1 corrected time series betas_hik_OC_T1c.nii T1-GS corrected components meica_mix_T1c.1D T1-GS corrected mixing matrix ====================== ================================================= If ``dne`` is set to True: ====================== ================================================= Filename Content ====================== ================================================= hik_ts_e[echo].nii High-Kappa timeseries for echo number ``echo`` midk_ts_e[echo].nii Mid-Kappa timeseries for echo number ``echo`` lowk_ts_e[echo].nii Low-Kappa timeseries for echo number ``echo`` dn_ts_e[echo].nii Denoised timeseries for echo number ``echo`` ====================== ================================================= """ # ensure tes are in appropriate format tes = [float(te) for te in tes] n_echos = len(tes) # coerce data to samples x echos x time array if isinstance(data, str): data = [data] LGR.info('Loading input data: {}'.format([f for f in data])) catd, ref_img = utils.load_data(data, n_echos=n_echos) n_samp, n_echos, n_vols = catd.shape LGR.debug('Resulting data shape: {}'.format(catd.shape)) kdaw, rdaw = float(kdaw), float(rdaw) try: ref_label = op.basename(ref_img).split('.')[0] except TypeError: ref_label = op.basename(str(data[0])).split('.')[0] if label is not None: out_dir = 'TED.{0}.{1}'.format(ref_label, label) else: out_dir = 'TED.{0}'.format(ref_label) out_dir = op.abspath(out_dir) if not op.isdir(out_dir): LGR.info('Creating output directory: {}'.format(out_dir)) os.mkdir(out_dir) else: LGR.info('Using output directory: {}'.format(out_dir)) if mixm is not None and op.isfile(mixm): shutil.copyfile(mixm, op.join(out_dir, 'meica_mix.1D')) shutil.copyfile(mixm, op.join(out_dir, op.basename(mixm))) elif mixm is not None: raise IOError('Argument "mixm" must be an existing file.') if ctab is not None and op.isfile(ctab): shutil.copyfile(ctab, op.join(out_dir, 'comp_table.txt')) shutil.copyfile(ctab, op.join(out_dir, op.basename(ctab))) elif ctab is not None: raise IOError('Argument "ctab" must be an existing file.') os.chdir(out_dir) if mask is None: LGR.info('Computing adaptive mask') else: # TODO: add affine check LGR.info('Using user-defined mask') mask, masksum = utils.make_adaptive_mask(catd, mask=mask, minimum=False, getsum=True) LGR.debug('Retaining {}/{} samples'.format(mask.sum(), n_samp)) LGR.info('Computing T2* map') t2s, s0, t2ss, s0s, t2sG, s0G = model.fit_decay(catd, tes, mask, masksum) # set a hard cap for the T2* map # anything that is 10x higher than the 99.5 %ile will be reset to 99.5 %ile cap_t2s = stats.scoreatpercentile(t2s.flatten(), 99.5, interpolation_method='lower') LGR.debug('Setting cap on T2* map at {:.5f}'.format(cap_t2s * 10)) t2s[t2s > cap_t2s * 10] = cap_t2s utils.filewrite(t2s, op.join(out_dir, 't2sv.nii'), ref_img) utils.filewrite(s0, op.join(out_dir, 's0v.nii'), ref_img) utils.filewrite(t2ss, op.join(out_dir, 't2ss.nii'), ref_img) utils.filewrite(s0s, op.join(out_dir, 's0vs.nii'), ref_img) utils.filewrite(t2sG, op.join(out_dir, 't2svG.nii'), ref_img) utils.filewrite(s0G, op.join(out_dir, 's0vG.nii'), ref_img) # optimally combine data OCcatd = model.make_optcom(catd, tes, mask, t2s=t2sG, combmode=combmode) # regress out global signal unless explicitly not desired if gscontrol: catd, OCcatd = model.gscontrol_raw(catd, OCcatd, n_echos, ref_img) if mixm is None: n_components, dd = decomposition.tedpca(catd, OCcatd, combmode, mask, t2s, t2sG, stabilize, ref_img, tes=tes, kdaw=kdaw, rdaw=rdaw, ste=ste, wvpca=wvpca) mmix_orig, fixed_seed = decomposition.tedica(n_components, dd, conv, fixed_seed, cost=initcost, final_cost=finalcost, verbose=debug) np.savetxt(op.join(out_dir, '__meica_mix.1D'), mmix_orig) LGR.info('Making second component selection guess from ICA results') seldict, comptable, betas, mmix = model.fitmodels_direct(catd, mmix_orig, mask, t2s, t2sG, tes, combmode, ref_img, reindex=True) np.savetxt(op.join(out_dir, 'meica_mix.1D'), mmix) acc, rej, midk, empty = selection.selcomps(seldict, mmix, mask, ref_img, manacc, n_echos, t2s, s0, strict_mode=strict, filecsdata=filecsdata) else: LGR.info('Using supplied mixing matrix from ICA') mmix_orig = np.loadtxt(op.join(out_dir, 'meica_mix.1D')) seldict, comptable, betas, mmix = model.fitmodels_direct(catd, mmix_orig, mask, t2s, t2sG, tes, combmode, ref_img) if ctab is None: acc, rej, midk, empty = selection.selcomps(seldict, mmix, mask, ref_img, manacc, n_echos, t2s, s0, filecsdata=filecsdata, strict_mode=strict) else: acc, rej, midk, empty = utils.ctabsel(ctab) if len(acc) == 0: LGR.warning('No BOLD components detected! Please check data and ' 'results!') utils.writeresults(OCcatd, mask, comptable, mmix, fixed_seed, n_vols, acc, rej, midk, empty, ref_img) utils.gscontrol_mmix(OCcatd, mmix, mask, acc, ref_img) if dne: utils.writeresults_echoes(catd, mmix, mask, acc, rej, midk, ref_img)