Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ENH] Add ability to re-run ICA when no BOLD components are found #663

Merged
merged 9 commits into from
Jan 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions tedana/decomposition/ica.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ def tedica(data, n_components, fixed_seed, maxit=500, maxrestart=10):
mmix : (T x C) :obj:`numpy.ndarray`
Z-scored mixing matrix for converting input data to component space,
where `C` is components and `T` is the same as in `data`
fixed_seed : :obj:`int`
Random seed from final decomposition.

Notes
-----
Expand All @@ -64,16 +66,16 @@ def tedica(data, n_components, fixed_seed, maxit=500, maxrestart=10):

w = list(filter(lambda i: issubclass(i.category, UserWarning), w))
if len(w):
LGR.warning('ICA attempt {0} failed to converge after {1} '
'iterations'.format(i_attempt + 1, ica.n_iter_))
LGR.warning('ICA with random seed {0} failed to converge after {1} '
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're updating this user-facing behavior, we should make sure it's noted somewhere (maybe in the release notes ?).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think release notes should be okay for this. It's not a significant change.

Copy link
Member

@emdupre emdupre Jan 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know when I used to run tedana more I would watch the logs, so I worry it could scare those users (is why I do want it mentioned somewhere). I know it would have scared me ! 😅

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's completely fair. Any thoughts on where to put it aside from release notes? When we do next release we could package it with the newsletter, too.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could downgrade this from a warning to a debug in a separate PR?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general I was thinking that we don't have a descriptive whatsnew. Maybe we could consider starting one ?

For now, I think the release notes are fine ! And I always like the newsletter's summaries :)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually disagree with downgrading to a debug. I've noticed that failure to converge tends to go along with lower-quality data, so I think it's good to let the user know that the software is struggling a bit.
Would you mind opening an issue for a whatsnew @emdupre ? That sounds like a good idea to me, especially if we're planning on increasing the release speed.

'iterations'.format(fixed_seed, ica.n_iter_))
if i_attempt < maxrestart - 1:
fixed_seed += 1
LGR.warning('Random seed updated to {0}'.format(fixed_seed))
else:
LGR.info('ICA attempt {0} converged in {1} '
'iterations'.format(i_attempt + 1, ica.n_iter_))
LGR.info('ICA with random seed {0} converged in {1} '
'iterations'.format(fixed_seed, ica.n_iter_))
break

mmix = ica.mixing_
mmix = stats.zscore(mmix, axis=0)
return mmix
return mmix, fixed_seed
7 changes: 7 additions & 0 deletions tedana/tests/data/fiu_four_echo_outputs.txt
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
T1gs.nii.gz
adaptive_mask.nii.gz
betas_OC.nii.gz
betas_hik_OC.nii.gz
betas_hik_OC_MIR.nii.gz
dn_ts_OC.nii.gz
dn_ts_OC_MIR.nii.gz
dn_ts_e1.nii.gz
dn_ts_e2.nii.gz
dn_ts_e3.nii.gz
dn_ts_e4.nii.gz
feats_OC2.nii.gz
glsig.1D
hik_ts_OC.nii.gz
hik_ts_OC_MIR.nii.gz
hik_ts_e1.nii.gz
hik_ts_e2.nii.gz
hik_ts_e3.nii.gz
hik_ts_e4.nii.gz
ica_components.nii.gz
ica_decomposition.json
ica_mixing.tsv
Expand Down
51 changes: 36 additions & 15 deletions tedana/workflows/tedana.py
Original file line number Diff line number Diff line change
Expand Up @@ -514,21 +514,45 @@ def tedana_workflow(data, tes, out_dir='.', mask=None,
out_dir=out_dir,
verbose=verbose,
low_mem=low_mem)
mmix_orig = decomposition.tedica(dd, n_components, fixed_seed,
maxit, maxrestart)

if verbose:
io.filewrite(utils.unmask(dd, mask),
op.join(out_dir, 'ts_OC_whitened.nii.gz'), ref_img)

LGR.info('Making second component selection guess from ICA results')
# Estimate betas and compute selection metrics for mixing matrix
# generated from dimensionally reduced data using full data (i.e., data
# with thermal noise)
comptable, metric_maps, betas, mmix = metrics.dependence_metrics(
catd, data_oc, mmix_orig, masksum, tes,
ref_img, reindex=True, label='meica_', out_dir=out_dir,
algorithm='kundu_v2', verbose=verbose)
# Perform ICA, calculate metrics, and apply decision tree
# Restart when ICA fails to converge or too few BOLD components found
keep_restarting = True
n_restarts = 0
seed = fixed_seed
while keep_restarting:
mmix_orig, seed = decomposition.tedica(
dd, n_components, seed,
maxit, maxrestart=(maxrestart - n_restarts)
)
seed += 1
n_restarts = seed - fixed_seed

# Estimate betas and compute selection metrics for mixing matrix
# generated from dimensionally reduced data using full data (i.e., data
# with thermal noise)
LGR.info('Making second component selection guess from ICA results')
comptable, metric_maps, betas, mmix = metrics.dependence_metrics(
catd, data_oc, mmix_orig, masksum, tes,
ref_img, reindex=True, label='meica_', out_dir=out_dir,
algorithm='kundu_v2', verbose=verbose
)
comptable = metrics.kundu_metrics(comptable, metric_maps)
comptable = selection.kundu_selection_v2(comptable, n_echos, n_vols)

n_bold_comps = comptable[comptable.classification == 'accepted'].shape[0]
if (n_restarts < maxrestart) and (n_bold_comps == 0):
LGR.warning("No BOLD components found. Re-attempting ICA.")
elif (n_bold_comps == 0):
LGR.warning("No BOLD components found, but maximum number of restarts reached.")
tsalo marked this conversation as resolved.
Show resolved Hide resolved
keep_restarting = False
tsalo marked this conversation as resolved.
Show resolved Hide resolved
else:
keep_restarting = False

# Write out ICA files.
comp_names = [io.add_decomp_prefix(comp, prefix='ica', max_value=comptable.index.max())
for comp in comptable.index.values]
mixing_df = pd.DataFrame(data=mmix, columns=comp_names)
Expand All @@ -537,9 +561,6 @@ def tedana_workflow(data, tes, out_dir='.', mask=None,
io.filewrite(betas_oc,
op.join(out_dir, 'ica_components.nii.gz'),
ref_img)

comptable = metrics.kundu_metrics(comptable, metric_maps)
comptable = selection.kundu_selection_v2(comptable, n_echos, n_vols)
else:
LGR.info('Using supplied mixing matrix from ICA')
mmix_orig = pd.read_table(op.join(out_dir, 'ica_mixing.tsv')).values
Expand All @@ -561,7 +582,7 @@ def tedana_workflow(data, tes, out_dir='.', mask=None,
op.join(out_dir, 'ica_components.nii.gz'),
ref_img)

# Save decomposition
# Save component table
comptable['Description'] = 'ICA fit to dimensionally-reduced optimally combined data.'
mmix_dict = {}
mmix_dict['Method'] = ('Independent components analysis with FastICA '
Expand Down