def test_fit_decay_ts(testdata1): """ fit_decay_ts should return data in samples x time shape. """ t2sv, s0v, t2svG, s0vG = me.fit_decay_ts(testdata1['data'], testdata1['tes'], testdata1['mask'], testdata1['mask_sum']) assert t2sv.ndim == 2 assert s0v.ndim == 2 assert t2svG.ndim == 2 assert s0vG.ndim == 2
def test_fit_decay_ts(testdata1): """ fit_decay_ts should return data in samples x time shape. """ t2sv, s0v, t2svG, s0vG = me.fit_decay_ts( testdata1["data"], testdata1["tes"], testdata1["mask"], testdata1["adaptive_mask"], testdata1["fittype"], ) assert t2sv.ndim == 2 assert s0v.ndim == 2 assert t2svG.ndim == 2 assert s0vG.ndim == 2
def test_smoke_fit_decay_ts(): """ test_smoke_fit_decay_ts tests that the function fit_decay_ts returns reasonable objects with random inputs in the correct format """ n_samples = 100 n_echos = 5 n_times = 20 data = np.random.random((n_samples, n_echos, n_times)) tes = np.random.random((n_echos)).tolist() mask = np.ones(n_samples, dtype=int) mask[n_samples // 2:] = 0 adaptive_mask = np.random.randint(2, n_echos, size=(n_samples)) * mask fittype = 'loglin' t2s_limited_ts, s0_limited_ts, t2s_full_ts, s0_full_ts = me.fit_decay_ts( data, tes, mask, adaptive_mask, fittype) assert t2s_limited_ts is not None assert s0_limited_ts is not None assert t2s_full_ts is not None assert s0_full_ts is not None
def test_smoke_fit_decay_ts(): """ test_smoke_fit_decay_ts tests that the function fit_decay_ts returns reasonable objects with random inputs in the correct format """ n_samples = 100 n_echos = 5 n_times = 20 data = np.random.random((n_samples, n_echos, n_times)) tes = np.random.random((n_echos)).tolist() mask = np.random.randint( 2, size=n_samples) # generate binary mask of random 0s and 1s masksum = np.random.random((n_samples)) fittype = 'loglin' t2s_limited_ts, s0_limited_ts, t2s_full_ts, s0_full_ts = me.fit_decay_ts( data, tes, mask, masksum, fittype) assert t2s_limited_ts is not None assert s0_limited_ts is not None assert t2s_full_ts is not None assert s0_full_ts is not None
def test_smoke_fit_decay_curvefit_ts(): """ test_smoke_fit_decay_ts tests that the function fit_decay_ts returns reasonable objects with random inputs in the correct format when using the direct monoexponetial approach """ n_samples = 100 n_echos = 5 n_times = 20 data = np.random.random((n_samples, n_echos, n_times)) tes = np.random.random((n_echos)).tolist() mask = np.random.randint( 2, size=n_samples) # generate binary mask of random 0s and 1s masksum = np.random.random((n_samples)) fittype = 'curvefit' t2s_limited_ts, s0_limited_ts, t2s_full_ts, s0_full_ts = me.fit_decay_ts( data, tes, mask, masksum, fittype) assert t2s_limited_ts is not None assert s0_limited_ts is not None assert t2s_full_ts is not None assert s0_full_ts is not None # TODO: BREAK AND UNIT TESTS
def t2smap_workflow(data, tes, out_dir='.', mask=None, fittype='loglin', fitmode='all', combmode='t2s', debug=False, quiet=False): """ 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. out_dir : :obj:`str`, optional Output directory. mask : :obj:`str`, optional Binary mask of voxels to include in TE Dependent ANAlysis. Must be spatially aligned with `data`. fittype : {'loglin', 'curvefit'}, optional Monoexponential fitting method. 'loglin' means to use the the default linear fit to the log of the data. 'curvefit' means to use a monoexponential fit to the raw data, which is slightly slower but may be more accurate. 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', 'paid'}, optional Combination scheme for TEs: 't2s' (Posse 1999, default), 'paid' (Poser). Other Parameters ---------------- debug : :obj:`bool`, optional Whether to run in debugging mode or not. Default is False. quiet : :obj:`bool`, optional If True, suppress logging/printing of messages. Default is False. Notes ----- This workflow writes out several files, which are described below: ========================== ================================================= Filename Content ========================== ================================================= T2starmap.nii.gz 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'. S0map.nii.gz Limited S0 3D map or 4D timeseries. desc-full_T2starmap.nii.gz 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. desc-full_S0map.nii.gz Full S0 map/timeseries. desc-optcom_bold.nii.gz Optimally combined timeseries. ========================== ================================================= """ out_dir = op.abspath(out_dir) if not op.isdir(out_dir): os.mkdir(out_dir) if debug and not quiet: logging.basicConfig(level=logging.DEBUG) elif quiet: logging.basicConfig(level=logging.WARNING) else: logging.basicConfig(level=logging.INFO) LGR.info('Using output directory: {}'.format(out_dir)) # 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 = io.load_data(data, n_echos=n_echos) n_samp, n_echos, n_vols = catd.shape LGR.debug('Resulting data shape: {}'.format(catd.shape)) if mask is None: LGR.info('Computing adaptive mask') else: LGR.info('Using user-defined mask') mask, masksum = utils.make_adaptive_mask(catd, mask=mask, getsum=True, threshold=1) LGR.info('Computing adaptive T2* map') if fitmode == 'all': (t2s_limited, s0_limited, t2s_full, s0_full) = decay.fit_decay(catd, tes, mask, masksum, fittype) else: (t2s_limited, s0_limited, t2s_full, s0_full) = decay.fit_decay_ts(catd, tes, mask, masksum, fittype) # 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') cap_t2s_sec = utils.millisec2sec(cap_t2s * 10.) LGR.debug('Setting cap on T2* map at {:.5f}s'.format(cap_t2s_sec)) t2s_limited[t2s_limited > cap_t2s * 10] = cap_t2s LGR.info('Computing optimal combination') # optimally combine data OCcatd = combine.make_optcom(catd, tes, masksum, 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 io.filewrite(utils.millisec2sec(t2s_limited), op.join(out_dir, 'T2starmap.nii.gz'), ref_img) io.filewrite(s0_limited, op.join(out_dir, 'S0map.nii.gz'), ref_img) io.filewrite(utils.millisec2sec(t2s_full), op.join(out_dir, 'desc-full_T2starmap.nii.gz'), ref_img) io.filewrite(s0_full, op.join(out_dir, 'desc-full_S0map.nii.gz'), ref_img) io.filewrite(OCcatd, op.join(out_dir, 'desc-optcom_bold.nii.gz'), ref_img)
def t2smap_workflow(data, tes, mask=None, fitmode='all', combmode='t2s', label=None, debug=False, fittype='loglin', quiet=False): """ 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. mask : :obj:`str`, optional Binary mask of voxels to include in TE Dependent ANAlysis. Must be spatially aligned with `data`. 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', 'paid'}, optional Combination scheme for TEs: 't2s' (Posse 1999, default), 'paid' (Poser). label : :obj:`str` or :obj:`None`, optional Label for output directory. Default is None. fittype : {'loglin', 'curvefit'}, optional Monoexponential fitting method. 'loglin' means to use the the default linear fit to the log of the data. 'curvefit' means to use a monoexponential fit to the raw data, which is slightly slower but may be more accurate. Other Parameters ---------------- 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 ----- 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 = io.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 = op.basename(ref_img).split('.')[0] except (TypeError, AttributeError): 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 mask is None: LGR.info('Computing adaptive mask') else: LGR.info('Using user-defined mask') mask, masksum = utils.make_adaptive_mask(catd, getsum=True) LGR.info('Computing adaptive T2* map') if fitmode == 'all': (t2s_limited, s0_limited, t2ss, s0s, t2s_full, s0_full) = decay.fit_decay(catd, tes, mask, masksum, fittype) else: (t2s_limited, s0_limited, t2s_full, s0_full) = decay.fit_decay_ts(catd, tes, mask, masksum, fittype) # 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 = combine.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 io.filewrite(t2s_limited, op.join(out_dir, 't2sv.nii'), ref_img) io.filewrite(s0_limited, op.join(out_dir, 's0v.nii'), ref_img) io.filewrite(t2s_full, op.join(out_dir, 't2svG.nii'), ref_img) io.filewrite(s0_full, op.join(out_dir, 's0vG.nii'), ref_img) io.filewrite(OCcatd, op.join(out_dir, 'ts_OC.nii'), ref_img)
def t2smap_workflow( data, tes, out_dir=".", mask=None, prefix="", convention="bids", fittype="loglin", fitmode="all", combmode="t2s", debug=False, quiet=False, ): """ Estimate T2 and S0, and optimally combine data across TEs. Please remember to cite [1]_. 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. out_dir : :obj:`str`, optional Output directory. mask : :obj:`str`, optional Binary mask of voxels to include in TE Dependent ANAlysis. Must be spatially aligned with `data`. fittype : {'loglin', 'curvefit'}, optional Monoexponential fitting method. 'loglin' means to use the the default linear fit to the log of the data. 'curvefit' means to use a monoexponential fit to the raw data, which is slightly slower but may be more accurate. 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', 'paid'}, optional Combination scheme for TEs: 't2s' (Posse 1999, default), 'paid' (Poser). Other Parameters ---------------- debug : :obj:`bool`, optional Whether to run in debugging mode or not. Default is False. quiet : :obj:`bool`, optional If True, suppress logging/printing of messages. Default is False. Notes ----- This workflow writes out several files, which are described below: ============================= ================================================= Filename Content ============================= ================================================= T2starmap.nii.gz Estimated T2* 3D map or 4D timeseries. Will be a 3D map if ``fitmode`` is 'all' and a 4D timeseries if it is 'ts'. S0map.nii.gz S0 3D map or 4D timeseries. desc-limited_T2starmap.nii.gz Limited 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 T2* estimate from the first two echos, while the limited map will have a NaN. desc-limited_S0map.nii.gz Limited S0 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 S0 estimate from the first two echos, while the limited map will have a NaN. desc-optcom_bold.nii.gz Optimally combined timeseries. ============================= ================================================= References ---------- .. [1] DuPre, E. M., Salo, T., Ahmed, Z., Bandettini, P. A., Bottenhorn, K. L., Caballero-Gaudes, C., Dowdle, L. T., Gonzalez-Castillo, J., Heunis, S., Kundu, P., Laird, A. R., Markello, R., Markiewicz, C. J., Moia, S., Staden, I., Teves, J. B., Uruñuela, E., Vaziri-Pashkam, M., Whitaker, K., & Handwerker, D. A. (2021). TE-dependent analysis of multi-echo fMRI with tedana. Journal of Open Source Software, 6(66), 3669. doi:10.21105/joss.03669. """ out_dir = op.abspath(out_dir) if not op.isdir(out_dir): os.mkdir(out_dir) utils.setup_loggers(quiet=quiet, debug=debug) LGR.info("Using output directory: {}".format(out_dir)) # 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 = io.load_data(data, n_echos=n_echos) io_generator = io.OutputGenerator( ref_img, convention=convention, out_dir=out_dir, prefix=prefix, config="auto", make_figures=False, ) n_samp, n_echos, n_vols = catd.shape LGR.debug("Resulting data shape: {}".format(catd.shape)) if mask is None: LGR.info("Computing adaptive mask") else: LGR.info("Using user-defined mask") mask, masksum = utils.make_adaptive_mask(catd, mask=mask, getsum=True, threshold=1) LGR.info("Computing adaptive T2* map") if fitmode == "all": (t2s_limited, s0_limited, t2s_full, s0_full) = decay.fit_decay(catd, tes, mask, masksum, fittype) else: (t2s_limited, s0_limited, t2s_full, s0_full) = decay.fit_decay_ts(catd, tes, mask, masksum, fittype) # 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_full.flatten(), 99.5, interpolation_method="lower") cap_t2s_sec = utils.millisec2sec(cap_t2s * 10.0) LGR.debug("Setting cap on T2* map at {:.5f}s".format(cap_t2s_sec)) t2s_full[t2s_full > cap_t2s * 10] = cap_t2s LGR.info("Computing optimal combination") # optimally combine data OCcatd = combine.make_optcom(catd, tes, masksum, t2s=t2s_full, combmode=combmode) # clean up numerical errors for arr in (OCcatd, s0_full, t2s_full): np.nan_to_num(arr, copy=False) s0_full[s0_full < 0] = 0 t2s_full[t2s_full < 0] = 0 io_generator.save_file( utils.millisec2sec(t2s_full), "t2star img", ) io_generator.save_file(s0_full, "s0 img") io_generator.save_file( utils.millisec2sec(t2s_limited), "limited t2star img", ) io_generator.save_file( s0_limited, "limited s0 img", ) io_generator.save_file(OCcatd, "combined img") # Write out BIDS-compatible description file derivative_metadata = { "Name": "t2smap Outputs", "BIDSVersion": "1.5.0", "DatasetType": "derivative", "GeneratedBy": [{ "Name": "tedana", "Version": __version__, "Description": ("A pipeline estimating T2* from multi-echo fMRI data and " "combining data across echoes."), "CodeURL": "https://github.com/ME-ICA/tedana", }], } io_generator.save_file(derivative_metadata, "data description json") LGR.info("Workflow completed") utils.teardown_loggers()
def t2smap_workflow(data, tes, out_dir='.', mask=None, prefix='', convention='bids', fittype='loglin', fitmode='all', combmode='t2s', debug=False, quiet=False): """ 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. out_dir : :obj:`str`, optional Output directory. mask : :obj:`str`, optional Binary mask of voxels to include in TE Dependent ANAlysis. Must be spatially aligned with `data`. fittype : {'loglin', 'curvefit'}, optional Monoexponential fitting method. 'loglin' means to use the the default linear fit to the log of the data. 'curvefit' means to use a monoexponential fit to the raw data, which is slightly slower but may be more accurate. 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', 'paid'}, optional Combination scheme for TEs: 't2s' (Posse 1999, default), 'paid' (Poser). Other Parameters ---------------- debug : :obj:`bool`, optional Whether to run in debugging mode or not. Default is False. quiet : :obj:`bool`, optional If True, suppress logging/printing of messages. Default is False. Notes ----- This workflow writes out several files, which are described below: ============================= ================================================= Filename Content ============================= ================================================= T2starmap.nii.gz Estimated T2* 3D map or 4D timeseries. Will be a 3D map if ``fitmode`` is 'all' and a 4D timeseries if it is 'ts'. S0map.nii.gz S0 3D map or 4D timeseries. desc-limited_T2starmap.nii.gz Limited 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 T2* estimate from the first two echos, while the limited map will have a NaN. desc-limited_S0map.nii.gz Limited S0 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 S0 estimate from the first two echos, while the limited map will have a NaN. desc-optcom_bold.nii.gz Optimally combined timeseries. ============================= ================================================= """ out_dir = op.abspath(out_dir) if not op.isdir(out_dir): os.mkdir(out_dir) utils.setup_loggers(quiet=quiet, debug=debug) LGR.info('Using output directory: {}'.format(out_dir)) # 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 = io.load_data(data, n_echos=n_echos) io_generator = io.OutputGenerator( ref_img, convention=convention, out_dir=out_dir, prefix=prefix, config="auto", make_figures=False, ) n_samp, n_echos, n_vols = catd.shape LGR.debug('Resulting data shape: {}'.format(catd.shape)) if mask is None: LGR.info('Computing adaptive mask') else: LGR.info('Using user-defined mask') mask, masksum = utils.make_adaptive_mask(catd, mask=mask, getsum=True, threshold=1) LGR.info('Computing adaptive T2* map') if fitmode == 'all': (t2s_limited, s0_limited, t2s_full, s0_full) = decay.fit_decay(catd, tes, mask, masksum, fittype) else: (t2s_limited, s0_limited, t2s_full, s0_full) = decay.fit_decay_ts(catd, tes, mask, masksum, fittype) # 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_full.flatten(), 99.5, interpolation_method='lower') cap_t2s_sec = utils.millisec2sec(cap_t2s * 10.) LGR.debug('Setting cap on T2* map at {:.5f}s'.format(cap_t2s_sec)) t2s_full[t2s_full > cap_t2s * 10] = cap_t2s LGR.info('Computing optimal combination') # optimally combine data OCcatd = combine.make_optcom(catd, tes, masksum, t2s=t2s_full, combmode=combmode) # clean up numerical errors for arr in (OCcatd, s0_full, t2s_full): np.nan_to_num(arr, copy=False) s0_full[s0_full < 0] = 0 t2s_full[t2s_full < 0] = 0 io_generator.save_file( utils.millisec2sec(t2s_full), 't2star img', ) io_generator.save_file(s0_full, 's0 img') io_generator.save_file( utils.millisec2sec(t2s_limited), 'limited t2star img', ) io_generator.save_file( s0_limited, 'limited s0 img', ) io_generator.save_file(OCcatd, 'combined img') # Write out BIDS-compatible description file derivative_metadata = { "Name": "t2smap Outputs", "BIDSVersion": "1.5.0", "DatasetType": "derivative", "GeneratedBy": [{ "Name": "tedana", "Version": __version__, "Description": ("A pipeline estimating T2* from multi-echo fMRI data and " "combining data across echoes."), "CodeURL": "https://github.com/ME-ICA/tedana" }] } io_generator.save_file(derivative_metadata, 'data description json') LGR.info("Workflow completed") utils.teardown_loggers()