-
Notifications
You must be signed in to change notification settings - Fork 532
ENH: Add an activation count map interface #2522
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
nipype/algorithms/stats.py
Outdated
|
||
def _run_interface(self, runtime): | ||
allmaps = nb.concat_images( | ||
[nb.load(f) for f in self.inputs.in_files]) |
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 assume you want to .get_data()
somewhere here, before making numeric comparisons.
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.
Looks good. One comment.
Also make specs
.
nipype/algorithms/stats.py
Outdated
allmaps = nb.concat_images( | ||
[nb.load(f) for f in self.inputs.in_files]).get_data() | ||
acm_pos = (allmaps > self.inputs.threshold).astype( | ||
float).mean(3) |
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.
What about:
acm_pos = np.mean(allmaps > self.inputs.threshold, axis=3, dtype=np.float32)
I think it reads more cleanly, and it'll make any line breaks cleaner looking, too. (I also prefer to use explicit axis
and dtype
keywords, just to avoid having to look up the order when I read. And I prefer to use actual dtypes, just to avoid depending on numpy magic that they can change at any time.)
nipype/algorithms/stats.py
Outdated
|
||
def _run_interface(self, runtime): | ||
allmaps = nb.concat_images( | ||
[nb.load(f) for f in self.inputs.in_files]).get_data() |
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.
Just realizing you can drop the load step here. concat_images will take file names.
LGTM. Have you tested it with some data, just to be sure? |
I guess, really, we could write a test that randomly generates some small (e.g. 10x10x10) images and runs the interface. Shouldn't add significant run time. Your call if it seems worth the effort. |
I'll add the test 👍 |
…e into enh/activations-count-map
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 another look at this, and got to thinking that we could probably make it a little friendlier to non-NIFTI-1 file formats, which should help preserve header information. Two comments are suggestions on how to achieve this.
Also, I think a longer name might be a good idea, here.
nipype/algorithms/stats.py
Outdated
self._results['acm_pos'] = os.path.join( | ||
runtime.cwd, 'acm_pos.nii.gz') | ||
self._results['acm_neg'] = os.path.join( | ||
runtime.cwd, 'acm_neg.nii.gz') |
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.
Maybe let's make this:
template_fname = self.inputs.in_files[0]
ext = split_filename(template_fname)[2]
fname_fmt = os.path.join(runtime.cwd, 'acm_{}' + ext).format
self._results['out_file'] = fname_fmt('diff')
self._results['acm_pos'] = fname_fmt('pos')
self._results['acm_neg'] = fname_fmt('neg')
img = nb.load(template_fname)
nipype/algorithms/stats.py
Outdated
nb.Nifti1Image( | ||
acm_pos, nii.affine, nii.header).to_filename(self._results['acm_pos']) | ||
nb.Nifti1Image( | ||
acm_neg, nii.affine, nii.header).to_filename(self._results['acm_neg']) |
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.
And then we would make these:
img.__class__(acm_diff, img.affine, img.header).to_filename(self._results['out_file'])
img.__class__(acm_pos, img.affine, img.header).to_filename(self._results['acm_pos'])
img.__class__(acm_neg, img.affine, img.header).to_filename(self._results['acm_neg'])
nipype/algorithms/stats.py
Outdated
acm_neg = File(exists=True, desc='negative activation count map') | ||
|
||
|
||
class ACM(SimpleInterface): |
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 think I'd prefer a more descriptive name like ActivationCount
.
See also: oesteban#7 |
TEST: Activation count map smoke test
Some second opinion about this addition to nipype? @djarecka, @mgxd, @satra or @chrisfilo ? Maybe a quick double check on the correctness from @jokedurnez ? |
nipype/algorithms/stats.py
Outdated
threshold = traits.Float(1.65, usedefault=True, | ||
desc='binarization threshold. A z-value of 1.65 ' | ||
'corresponds to a two-sided test of p<.10') | ||
|
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'm wondering if it's better to leave threshold
without default value, so users have to really choose one....
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 have an opinion here.
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.
Makes sense, I'll update accordingly.
diff = nb.load(res.outputs.out_file) | ||
pos = nb.load(res.outputs.acm_pos) | ||
neg = nb.load(res.outputs.acm_neg) | ||
assert np.allclose(diff.get_data(), pos.get_data() - neg.get_data()) |
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.
we could also check some statistics pos.get_data
itself?
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 main things I can think of that should be true of performing this on some random noise would be np.all(pos.get_data() > 0)
and np.all(neg.get_data() > 0)
.
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.
If I understand correctly, the interface just checks how many point are above threshold
or below -threshold
. If we draw points from random distribution and we slightly increase the size of the array, we should be able to compare to theoretical value.
Let me finish something and I'll add a simple example to show what do I mean.
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.
here is an example: oesteban@6e53f01
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.
That looks valid, but I'm not fully clear on what assurances this gets us for the interface. I guess, roughly, that Gaussian noise is being filtered as we'd expect?
I'm okay if this test goes in, but my overall feeling is that a useful test asserts properties that should be true given any real or simulated inputs, and this seems tied to the specific distribution of simulated random variates.
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.
you usually use specific cases for tests, and often some simple cases, so you know the answer.
The interface should work for any real or simulated inputs, but I'm checking here for a specific distribution (exactly the same as @oesteban used in the original test). Obviously I'm not able to predict answers for completely random set of numbers, but for the normal distribution I can.
I just thought that this adds extra checks to the original @oesteban test, that was only checking pos-neg == diff
, but I won't insist to include it.
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.
we can also just generate a small array and calculate the expected output of the interface.
…an/nipype into oesteban-enh/activations-count-map
@oesteban I think you said you're going to drop the threshold. Anything else before merging? |
Oesteban enh/activations count map
…e into enh/activations-count-map
Provided that the tests pass, this should be good to go. |
nipype/algorithms/stats.py
Outdated
desc='input file, generally a list of z-stat maps') | ||
threshold = traits.Float( | ||
mandatory=True, desc='binarization threshold. E.g. a threshold of 1.65 ' | ||
'corresponds to a two-sided test of p<.10') |
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.
"two-sided Z-test"?
A simple calculation of ACMs, adapted from @jokedurnez's code: https://github.com/poldracklab/CNP_task_analysis/blob/61c27f5992db9d8800884f8ffceb73e6957db8af/CNP_2nd_level_ACM.py