-
Notifications
You must be signed in to change notification settings - Fork 34
Add AFNI 3dREMLfit for first-level estimation #171
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
Conversation
Holy shit, this fantastic, John! Had no idea you were working on this. Will read through the comments and try to answer any questions I can. Excited to use this! (@adelavega, feel free to experiment—it would be awesome to be able to give NeuroScout users the option to use AFNI for estimation). |
Excellent. Thanks. I’ll work through the feedback next week.
… On Aug 30, 2019, at 16:02, Tal Yarkoni ***@***.***> wrote:
Holy shit, this fantastic, John! Had no idea you were working on this. Will read through the comments and try to answer any questions I can. Excited to use this! ***@***.***, feel free to experiment—it would be awesome to be able to give NeuroScout users the option to use AFNI for estimation).
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub, or mute the thread.
|
Very awesome. I guess in theory as far as NeuroScout is concerned the API should be plug and play interchangeable, correct? I'll take a closer look at the code next week! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your patience. I've had a general look through, and made hopefully high level comments and questions. There are a lot of details to get to still, so I'll make more time on Monday.
(24, "log10 p value", "Log10Pval"), | ||
), | ||
fields=("code", "label", "stat_code"), | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pretty sure this is just nibabel.nifti1.intent_codes
, restricted to the first 24?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The intent codes in nifti mirror the afni brik stat codes (they were copied from them). I don't know what's more intuitive to describe it as a stat code mapping or to add the brik labels as aliases to the niftit intent code mapping. I think moving this to nibabel would be good though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since they're in NIFTI, I would probably just stick with that, as it's all official and whatnot. But if you wanted to add them to nibabel.brikhead
, you're welcome to.
fitlins/interfaces/afni.py
Outdated
nistats_flm.labels_, nistats_flm.results_, self.design_matrices_ = ([], [], []) | ||
n_runs = len(run_imgs) | ||
t0 = time.time() | ||
for run_idx, run_img in enumerate(run_imgs): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it important to do these in series, or are they independent? If they're independent, we may be able to rewrite this to run just one, and use a MapNode
to iterate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
map node sounds good
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The workflow already uses boldfile as an iterfield in a map node. So the iteration is already done intelligently. the remlfit function (just like the nistats.fit function) has the ability to take a list of images though. I can remove that if you wish in the interest of clarity and brevity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay. Let me have another read through before we refactor.
fitlins/interfaces/afni.py
Outdated
else: | ||
params = [x for x in val[1].split(",")] | ||
intent_info.append( | ||
(STAT_CODES.label[val[0]], tuple([int(x) for x in params if x])) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(STAT_CODES.label[val[0]], tuple([int(x) for x in params if x])) | |
(nb.nifti1.intent_codes.label[val[0]], tuple(int(x) for x in params if x)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this will work. The AFNI stats code labels are very slightly different from the nifti intent codes.
fitlins/interfaces/afni.py
Outdated
pval_bucket, pval_labels = load_bucket_by_prefix(tmpdir, pvals) | ||
zscore_bucket, z_labels = load_bucket_by_prefix(tmpdir, zscores) | ||
extra_bucket, extra_labels = load_bucket_by_prefix(tmpdir, extra_vars) | ||
stat_bucket, stat_labels = load_bucket_by_prefix(tmpdir, glt) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
L332-L361: Is this stuff not already done in Remlfit? I assumed all of the parsing work should already be done. If it's not, it would make more sense to fix up that interface than to have a separate implementation here.
I guess we need to add a 3dPval interface. We can build that here or in nipype. (It'll get upstreamed eventually.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good. That does seem like 30 lines of superfluous code. I didn't want to complicate things before a refactor. Would you have just defined and executed the single interface node within the class method?
I can take a hack at writing the 3dPval interface
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My thought here was to just use the interface, not wrapped in a Node()
, which means it would just use the working directory of the Node
that the whole interface is being run in.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great, will do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have implemented this now. Let me know what you think
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps some of this code could be moved into the remlfit interface. The main problem is that remlfit is writing out images with different intent codes to the same file. These files need to be parsed and the results rewritten according to the categories required by FitLins.
And at some point the 3dPval interface can be moved to Nipype.
So I began to make code suggestions, but I think it makes more sense to step back and sketch out an overall API. Your Let's consider some set of abstract interfaces: class DesignMatrixInterface(BaseInterface):
class input_spec(BaseInterfaceInputSpec):
session_info = traits.Dict(mandatory=True)
bold_file = File(exists=True, mandatory=True)
class output_spec(TraitedSpec):
design_matrix = File()
class FirstLevelEstimatorInterface(BaseInterface):
class input_spec(BaseInterfaceInputSpec):
bold_file = File(exists=True, mandatory=True)
mask_file = File(exists=True)
smoothing_fwhm = traits.Float(desc='Full-width half max (FWHM) in mm for smoothing in mask')
design_matrix = File(exists=True, mandatory=True)
contrast_info = traits.List(traits.Dict)
class output_spec(TraitedSpec):
effect_maps = traits.List(File)
variance_maps = traits.List(File)
stat_maps = traits.List(File)
zscore_maps = traits.List(File)
pvalue_maps = traits.List(File)
contrast_metadata = traits.List(traits.Dict) Then we could simply say: class nistats.DesignMatrix(DesignMatrixInterface):
def _run_interface(self, runtime):
import nibabel as nb
from nistats import design_matrix as dm
info = self.inputs.session_info
img = nb.load(self.inputs.bold_file)
vols = img.shape[3]
if info['sparse'] not in (None, 'None'):
sparse = pd.read_hdf(info['sparse'], key='sparse').rename(
columns={'condition': 'trial_type',
'amplitude': 'modulation'})
sparse = sparse.dropna(subset=['modulation']) # Drop NAs
else:
sparse = None
if info['dense'] not in (None, 'None'):
dense = pd.read_hdf(info['dense'], key='dense')
column_names = dense.columns.tolist()
drift_model = None if 'cosine_00' in column_names else 'cosine'
else:
dense = None
column_names = None
drift_model = 'cosine'
mat = dm.make_first_level_design_matrix(
frame_times=np.arange(vols) * info['repetition_time'],
events=sparse,
add_regs=dense,
add_reg_names=column_names,
drift_model=drift_model,
)
mat.to_csv('design.tsv', sep='\t')
self._results['design_matrix'] = os.path.join(runtime.cwd,
'design.tsv')
return runtime
class nistats.FirstLevelEstimator(FirstLevelEstimatorInterface):
def _run_interface(self, runtime):
import nibabel as nb
mat = pd.read_csv(self.inputs.design_matrix, sep='\t')
img = nb.load(self.inputs.bold_file)
mask_file = self.inputs.mask_file
if not isdefined(mask_file):
mask_file = None
smoothing_fwhm = self.inputs.smoothing_fwhm
if not isdefined(smoothing_fwhm):
smoothing_fwhm = None
flm = level1.FirstLevelModel(
mask_img=mask_file, smoothing_fwhm=smoothing_fwhm)
flm.fit(img, design_matrices=mat)
effect_maps = []
variance_maps = []
stat_maps = []
zscore_maps = []
pvalue_maps = []
contrast_metadata = []
out_ents = self.inputs.contrast_info[0]['entities']
fname_fmt = os.path.join(runtime.cwd, '{}_{}.nii.gz').format
for name, weights, contrast_type in prepare_contrasts(
self.inputs.contrast_info, mat.columns.tolist()):
maps = flm.compute_contrast(weights, contrast_type, output_type='all')
contrast_metadata.append(
{'contrast': name,
'stat': contrast_type,
**out_ents}
)
for map_type, map_list in (('effect_size', effect_maps),
('effect_variance', variance_maps),
('z_score', zscore_maps),
('p_value', pvalue_maps),
('stat', stat_maps)):
fname = fname_fmt(name, map_type)
maps[map_type].to_filename(fname)
map_list.append(fname)
self._results['effect_maps'] = effect_maps
self._results['variance_maps'] = variance_maps
self._results['stat_maps'] = stat_maps
self._results['zscore_maps'] = zscore_maps
self._results['pvalue_maps'] = pvalue_maps
self._results['contrast_metadata'] = contrast_metadata
return runtime And then your changes boil down roughly to your I would recommend against using Does that seem like a reasonable approach to you? |
Yes, it looks good. Shall I make a first pass at it or do you want to do it and I can rebase and fix? A problem I have noticed is that a variable number of output files are required. For example the number of stats_maps, effect_maps etc depend on the number of contrasts specified. This will get slightly more confusing when multi-row F tests (as defined above) are used because the count for each type of map written to disk will also differ. I don't know whether the length of traited lists can be defined dynamically (based on parsing the model json). Or alternatively contrast could be an iterfield. Making it an iterfield would get the expected map count right (except for the case of multi-row F tests) but doesn't work with how the nistats prepare_contrasts function works. I'm not under the impression this would be better but thought I should flag it at this point rather than later... |
I can do a first pass on Monday, but if you're planning on working the weekend, you're welcome to give it a shot. |
b3e9ccf
to
ec9f118
Compare
Hello @leej3, Thank you for updating!
To test for issues locally, Comment last updated at 2020-11-18 21:50:03 UTC |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Had a couple minor wording/typo suggestions here, and I'm too lazy to cancel the review and make the main comment by itself.
fitlins/workflows/base.py
Outdated
from ..interfaces.visualizations import ( | ||
DesignPlot, DesignCorrelationPlot, ContrastMatrixPlot, GlassBrainPlot) | ||
from ..interfaces.utils import MergeAll, CollateWithMetadata | ||
from ..interfaces.afni import AFNIMergeAll |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are now being imported inside init_fitlins_wf
. This works around some annoying new nipype behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good to know
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was what was causing the packaging test failure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aha. That confused me.
Codecov Report
@@ Coverage Diff @@
## master #171 +/- ##
==========================================
+ Coverage 72.43% 75.16% +2.73%
==========================================
Files 20 21 +1
Lines 1208 1506 +298
Branches 217 263 +46
==========================================
+ Hits 875 1132 +257
- Misses 234 261 +27
- Partials 99 113 +14
Flags with carried forward coverage won't be shown. Click here to find out more.
Continue to review full report at Codecov.
|
The nistats estimator automatically applies scaling. I have done the same albeit with a tweak to accommodate the 2d -> 4d switch in data shape. I don't know whether it would be preferable to specify this a transform in the model json instead. I have assumed that the Tr (required for the afni design matrix) is in the design matrix index |
also I can't figure out why test_packaging fails |
I don't know
Assuming But if you have the nistats-built design-matrix, why do you need the TR again? |
The tr in ds_00003 is 2.0, so I assume the unit is seconds.
I have to add it to the AFNI design matrix. session_info is an input to the design matrix node but not to the l1_model node. Rather than add it to the input spec I just wanted to extract it from that design matrix. I'll use the diff just in case. so
|
Ah, right, you need to translate to a new format. I think that's fine. The only potential issue is that there are experimental designs where there is no constant TR, e.g., clustered volume acquisition. We don't support this well yet, but it's a thing to keep in mind. Does AFNI handle situations like that? |
@leej3 Do we want to discuss this today? Sorry that it's taken so long to get around to it. |
sounds good. Discuss what/if any bits of functionality are addressed before
merging. Also, its worth flagging that @tyarkoni mentioned that a refactor
might be ideal before merging this...
…On Thu, Nov 14, 2019 at 10:05 AM Chris Markiewicz ***@***.***> wrote:
@leej3 <https://github.com/leej3> Do we want to discuss this today? Sorry
that it's taken so long to get around to it.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#171?email_source=notifications&email_token=ABJKZKEVSIPBO6Y4KZQD44LQTVSMDA5CNFSM4ISQVIAKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEECEIPQ#issuecomment-553927742>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ABJKZKCQQQ5MP3DOACO6AMLQTVSMDANCNFSM4ISQVIAA>
.
|
946e64e
to
ca219a6
Compare
Rerunning the failed test... Hopefully it was just a hiccup. |
Can you merge/rebase master? |
Sure
…On Wed, Jan 8, 2020 at 2:12 PM Chris Markiewicz ***@***.***> wrote:
Can you merge/rebase master?
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#171?email_source=notifications&email_token=ABJKZKGMR7S6J5HVVYAWJUDQ4YQTHA5CNFSM4ISQVIAKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEINUVBQ#issuecomment-572213894>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ABJKZKEWXG536WGCPFTOLDTQ4YQTHANCNFSM4ISQVIAA>
.
|
adf0e37
to
d7c0e89
Compare
sorry I hadn't spotted your commit. You'll have to push that again. |
@effigies, are there any other changes you would like for this? |
Thanks for this! I will look tomorrow. |
I will look today. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Marking my progress. Meeting now, so will get back to it later. Feel free to respond now or wait for me to finish.
changes error ts arg/var name explicitly uses runtime.cwd instead of tmpdir Co-authored-by: Chris Markiewicz <effigies@gmail.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Small fixes here. I think this can go in and I'll make some issues for future improvements.
fitlins/interfaces/afni.py
Outdated
# modifying function as required and then append it to the | ||
# appropriate output list type represented by map_list and write | ||
# map_list to disk | ||
for map_type, map_list, idx_list, mod_func in ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mod_func doesn't appear to be used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmmm... that would be an error and mean that the effect variance is currently incorrectly the stdev. I'll check this.
fitlins/interfaces/afni.py
Outdated
outmap.to_filename(fname) | ||
|
||
|
||
class AFNIMergeAll(MergeAll): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A docstring explaining why this is happening would be helpful (I'm sure I knew, but now I don't), but this can be a new issue.
adapt FirstLevelModel init for common api with AFNI estimator tidy some code improve nibabel usage Co-authored-by: Chris Markiewicz <effigies@gmail.com>
Ok thanks for taking a look. I'll take a pass through and push remaining suggestions:
|
So I admit the documentation is needed! I am pretty sure that my previous comment refers to this issue. In this subclass I overwrite the _list_outputs method to avoid the check of equal list lengths across fields. I don't have a sense of whether this violates the stats models IO spec or if there is an alternative solution that is more elegant. Do you have any suggestions?
This looks fine. Nothing blew up.
This isn't required, the code handles either img or imgs fine. I'll leave get_afni_intent_info as is (and the full series is now passed to get_afni_intent_info_for_subvol as you suggested.
@Shotgunosine and I spotted this a while back and then forgot to make the fix. He's working through that now. |
I think let's just note in a new issue there's an inconsistent API. It may be that there's something in the nistats vs AFNI implementations, or it may be that the models you're using would require a similar change in nistats. |
Sounds good.
…On Tue, Nov 17, 2020 at 4:36 PM Chris Markiewicz ***@***.***> wrote:
Document MergeAll subclass
So I admit the documentation is needed! I am pretty sure that my previous
comment
<#171 (comment)>
refers to this issue. In this subclass I overwrite the _list_outputs method
to avoid the check of equal list lengths across fields. I don't have a
sense of whether this violates the stats models IO spec or if there is an
alternative solution that is more elegant. Do you have any suggestions?
I think let's just note in a new issue there's an inconsistent API. It may
be that there's something in the nistats vs AFNI implementations, or it may
be that the models you're using would require a similar change in nistats.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#171 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ABJKZKCF4EUICVCXEOD67RTSQLUEVANCNFSM4ISQVIAA>
.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's do it. Thanks so much for pushing on this!
Seconded—this is awesome! |
Summary
Addresses #115
This is still a work in progress. I've created a FirstLevelModel nipype node that can be swapped in for the default fitlins one. To my knowledge, the estimator, which uses AFNI's 3dREMLfit program, can compute any F-tests and t-tests, with voxelwise REML estimation of the ARMA(1,1) temporal correlations (e.g., https://doi.org/10.1038/s41467-019-09230-w. However, there are some things that need to be considered for it to fully integrate with the fitlins workflow. Also, breaking it into separate nipype nodes and using the 3dREMLfit nipype interface rather than a subprocess call would probably be good.
For now. I have overwritten the
fitlins.interfaces.nistats.FirstLevelModel _run_interface
method and instead of usingnistats.first_level_model.FirstLevelModel.fit
I have written my ownreml_fit
as a method offitlins.interfaces.afni.FirstLevelModel
. These can be integrated as desired (I'm happy to help with some guidance) or persist as its own node/sub-workflow that can be swapped into the main fitlins workflow as requested by the user (likely by using the json rather than by the CLI change I’ve implemented here).I overwrote
fitlins.interfaces.utils.MergeAll
because it checks for an equal number of contrast metadata elements and maps of each category. This allows me to make my way through a lot of the workflow but it just kicks the can down the road to theCollateWithMetadata
class. The expectation that all elements have the same number is not met when one considers multirow F-tests (by multirow I am referring to F-tests specified with multiple condition weight vectors as part of the BIDS Stats Models specification json). I'm not sure what you want to do with this...Other notes on the implementation
The main component of translation implemented here is to rewrite the design matrix and metadata from fitlins into a form that is consumable by 3dREMLfit. This form (described here, along with other useful features of the program) embeds the information in the NIML (XML-like) header of the AFNI design matrix file to guide the model fitting performed. It drops the need to specify many of the options of the 3dREMLfit CLI. I have hacked this together using string interpolation: it works but representing the metadata as XML and writing it to the file may be more elegant. I will look into that.
I'm not sure of the desired scope of this estimator. Would you prefer all results computed or a minimal output of the regression fit? As implemented, all statistical maps are computed using AFNI tools (3dREMLfit and then 3dPval). An alternative would be to tweak the data fed into 3dREMLfit (generate the 1d 'Y' object computed by nilearn) so that the output can be unmasked and the results maps can instead be computed using
nistats.contrasts.compute_contrast
. Regarding AFNI's computation of the maps...I couldn't spot whether t-tests should be interpreted as 1- or 2-sided in the stat models specification (in the p-value computation). The AFNI team feels strongly about the fact that it should be 2-sided unless well-justified (and by far most FMRI-related cases are really 2-sided analyses)! I have used 3dPval to compute the p-value statistical maps, with the default being 2-sided. (For a 1-sided interpretation, these can be divided by 2)
z-score maps (the z statistic) are computed using a recent option added to 3dPval (afni/afni@7b41df7).
From the nistats source I think Bonferroni correction (for number of contrasts) is being applied during result map computation. This has not been added but can be within the reml_fit method in python if desired. Though perhaps this should not be a default and should instead be explicit in the stats model specification?
Possible additional features
In the stats models spec there is a description of how it attempts to expand model computation beyond the typical summary statistics paradigm. Are there currently implementations of this in fitlins or elsewhere? We (@afni-gangc) know of an approach that incorporates both effect estimate and the associated standard error as input for group analysis (3dMEMA in AFNI, FLAME is FSL, and also something in SPM along the same line).
Notes on neuroimaging processing
Queries about where the appropriate place for certain code-chunks.
__iter__
for iterating through each element accessible by slicer[...,idx] might be nice...