Skip to content

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

Merged
merged 23 commits into from
Apr 25, 2018

Conversation

oesteban
Copy link
Contributor

@oesteban oesteban requested a review from effigies March 30, 2018 22:54

def _run_interface(self, runtime):
allmaps = nb.concat_images(
[nb.load(f) for f in self.inputs.in_files])
Copy link
Member

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.

effigies
effigies previously approved these changes Mar 31, 2018
Copy link
Member

@effigies effigies left a 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.

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)
Copy link
Member

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.)


def _run_interface(self, runtime):
allmaps = nb.concat_images(
[nb.load(f) for f in self.inputs.in_files]).get_data()
Copy link
Member

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.

@oesteban oesteban changed the title [ENH] Add a activation count map interface [ENH] Add an activation count map interface Apr 1, 2018
@oesteban oesteban changed the title [ENH] Add an activation count map interface ENH: Add an activation count map interface Apr 1, 2018
@effigies
Copy link
Member

effigies commented Apr 1, 2018

LGTM. Have you tested it with some data, just to be sure?

@effigies
Copy link
Member

effigies commented Apr 1, 2018

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.

@oesteban
Copy link
Contributor Author

oesteban commented Apr 1, 2018

I'll add the test 👍

@effigies effigies added this to the 1.0.3 milestone Apr 1, 2018
@effigies effigies dismissed their stale review April 2, 2018 14:08

Pending test

Copy link
Member

@effigies effigies left a 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.

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')
Copy link
Member

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)

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'])
Copy link
Member

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'])

acm_neg = File(exists=True, desc='negative activation count map')


class ACM(SimpleInterface):
Copy link
Member

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.

@effigies
Copy link
Member

See also: oesteban#7

@oesteban
Copy link
Contributor Author

Some second opinion about this addition to nipype? @djarecka, @mgxd, @satra or @chrisfilo ?

Maybe a quick double check on the correctness from @jokedurnez ?

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')

Copy link
Collaborator

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....

Copy link
Member

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.

Copy link
Contributor Author

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())
Copy link
Collaborator

@djarecka djarecka Apr 13, 2018

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?

Copy link
Member

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).

Copy link
Collaborator

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.

Copy link
Collaborator

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

Copy link
Member

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.

Copy link
Collaborator

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.

Copy link
Collaborator

@djarecka djarecka Apr 17, 2018

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.

@effigies
Copy link
Member

@oesteban I think you said you're going to drop the threshold. Anything else before merging?

@oesteban
Copy link
Contributor Author

Provided that the tests pass, this should be good to go.

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')
Copy link
Member

Choose a reason for hiding this comment

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

"two-sided Z-test"?

@effigies effigies merged commit a2d0f57 into nipy:master Apr 25, 2018
@oesteban oesteban deleted the enh/activations-count-map branch October 26, 2018 06:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants