Skip to content

ENH: Define Cifti2 Axes describing the rows/columns of the Cifti2 data #641

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 60 commits into from
Mar 31, 2019
Merged

ENH: Define Cifti2 Axes describing the rows/columns of the Cifti2 data #641

merged 60 commits into from
Mar 31, 2019

Conversation

MichielCottaar
Copy link
Contributor

This pull request is designed to add semantic understanding of the Cifti2 file format to nibabel. This is essentially a port from https://github.com/MichielCottaar/cifti as recently suggested to me by @demianw .

In short, the Cifti2 file format allows for the storage off matrices (or tensors) with a header describing what each row/column means. This meaning can be assigning to every row/column either a voxel or vertex (BrainModel), a group of voxels and/or vertices (Parcel), simply assigning a name (Scalar), a name and a color map (Label), or a starting point and step (Series). All these different options are stored in the same XML structure and hence have the same class representation in the current Cifti2Header file.

This pull request defines a new cifti2_axes.Axis class, which is a superclass of all the options defined above (BrainModel, Parcel, Scalar, Label, and Series). These new class all support:

  1. they can be read from Cifti2 header
  2. they can be used to write a Cifti2 header
  3. they can be easily generated from scratch (either using the init method or using some from_* class methods)
  4. they can be easily introspected
  5. they support operations like slicing and can be concatenated through the '+' operator

There is quite a lot going on here. I would welcome any feedback on the current feature set, the API, and whether people think it is a good idea to include this in nibabel at all.

@demianw
Copy link
Contributor

demianw commented Jun 25, 2018

@MichielCottaar great that you did this! Let's start working on this!

@demianw
Copy link
Contributor

demianw commented Jun 25, 2018

I think we can also merge this utils.py from my branch which allows for a better manipulation of indices. What do you think?

@MichielCottaar
Copy link
Contributor Author

Thanks, Demian. Your MatrixIndexMap seems quite similar to my Axis class, although rather than using sub-classing to support the various Cifti2 axes types you handle the various types through an if-statement in the init. So I would view it as an alternative, simpler implementation for if people think that my implementation is too complex.

@demianw
Copy link
Contributor

demianw commented Jun 25, 2018

You are right! I didn't look in detail your code yet. I will go over the interfaces more than the implementation and check their simplicity. You code looks very nice at first sight!

@coveralls
Copy link

coveralls commented Jun 25, 2018

Coverage Status

Coverage decreased (-0.01%) to 91.799% when pulling 95b571a on MichielCottaar:enh/cifti2_axes into ad6b890 on nipy:master.

@effigies
Copy link
Member

effigies commented Oct 1, 2018

Hi @MichielCottaar @demianw, what's the status of this? I'm inclined to try to release soon, so if this isn't too far away, I can put it in the queue.

@demianw
Copy link
Contributor

demianw commented Oct 1, 2018

Hey! I meant to do a code review months ago and I was buried in work. I expect to be able to review by nov 1st the latest. Does that sound good?

@effigies
Copy link
Member

effigies commented Oct 1, 2018

Sounds good, but I'll plan to release before that. I think it would be good to establish a consistent release cycle (e.g., every 3 months no matter what), so that we don't have these weird, aperiodic merge windows that are too short for serious work to get done in.

Copy link
Contributor

@demianw demianw left a comment

Choose a reason for hiding this comment

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

It's great work! Sorry for the very very late response. First round of comments included.

(N, ) structured array with for every element a tuple with 3 elements:
- vertex index (-1 for voxels)
- 3 voxel indices (-1 for vertices)
- string (name of brain structure)
Copy link
Contributor

Choose a reason for hiding this comment

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

@MichielCottaar I think that passing the voxel/vertex/structure elements separated as in the attributes presented on line 130 might be an easier to understand interface. The typed arrays with -1 harcoded for an ignored value seems suboptimal as interface design. @effigies , you have more the hand on what's the interface style in nibabel, what do you think?

Alternatively, this constructor could be hidden and just creating the objects from the factory methods, such as from_mask, which have very comfy and clear interfaces, might be a good option.

@codecov-io
Copy link

codecov-io commented Jan 28, 2019

Codecov Report

Merging #641 into master will decrease coverage by 1.46%.
The diff coverage is 98.12%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #641      +/-   ##
==========================================
- Coverage   89.57%   88.11%   -1.47%     
==========================================
  Files          93      188      +95     
  Lines       11470    23998   +12528     
  Branches     1991     4258    +2267     
==========================================
+ Hits        10274    21145   +10871     
- Misses        856     2158    +1302     
- Partials      340      695     +355
Impacted Files Coverage Δ
nibabel/cifti2/parse_cifti2.py 83.76% <ø> (ø) ⬆️
nibabel/cifti2/__init__.py 100% <100%> (ø) ⬆️
nibabel/info.py 100% <100%> (ø) ⬆️
nibabel/cifti2/cifti2.py 96.08% <90%> (-0.31%) ⬇️
nibabel/cifti2/cifti2_axes.py 98.26% <98.26%> (ø)
nibabel/pydicom_compat.py 65% <0%> (-35%) ⬇️
nibabel/py3k.py 44.64% <0%> (-28.58%) ⬇️
nibabel/environment.py 75% <0%> (-20%) ⬇️
nibabel/pkg_info.py 65.51% <0%> (-13.8%) ⬇️
nibabel/casting.py 84.74% <0%> (-2.55%) ⬇️
... and 103 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update abc2f43...0927424. Read the comment docs.

@MichielCottaar
Copy link
Contributor Author

I just rebased onto master and adjusted the documentation according to your first suggestion.

I agree that the typed array is not the most natural interface. The main reason for doing this was that I was able to move some of the logic to the Axis class. However, in practice I ended up overriding most of these methods anyway. So perhaps I should just use the Axis class as a convenient class for type checking and to define the interface. I could even make it an abstract base class (https://docs.python.org/3.7/library/abc.html) to make this explicit.

@demianw
Copy link
Contributor

demianw commented Jan 28, 2019

Great reactivity!
I think that the Axis-classes could just hide the constructor and work through the factory functions which seem quite easy to use. This just needs updating the documentation of the class to indicate the recommended way of building them and leave the internal representation hidden.

Agreed that if finally you don't use anything of the Axis class, the ABC scenario is good design.

I just rebased onto master and adjusted the documentation according to your first suggestion.

I agree that the typed array is not the most natural interface. The main reason for doing this was that I was able to move some of the logic to the Axis class. However, in practice I ended up overriding most of these methods anyway. So perhaps I should just use the Axis class as a convenient class for type checking and to define the interface. I could even make it an abstract base class (https://docs.python.org/3.7/library/abc.html) to make this explicit.

@effigies effigies self-assigned this Mar 13, 2019
@MichielCottaar
Copy link
Contributor Author

I have removed the underlying typed arrays as I agree that they were mainly confusing. The constructors are now more useful. Next step is to check why the style check in travis is failing.

@effigies
Copy link
Member

@MichielCottaar Might be worth merging master, to make sure it's not something unrelated that we fixed earlier.

@effigies effigies mentioned this pull request Mar 15, 2019
20 tasks
@effigies effigies added this to the 2.4.0 milestone Mar 18, 2019
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.

Hi @MichielCottaar, here's a first pass. I stopped at BrainModel because this is starting to get a bit long. I figured we could establish strategy, you could have another pass, and then I can finish the review. Let me know if that works for you.

I tried to limit overly nit-picky suggestions, but you should definitely feel free to argue against any of them.

other.volume_shape != shape
):
raise ValueError("Trying to concatenate two BrainModels defined " +
"in a different brain volume")
Copy link
Member

Choose a reason for hiding this comment

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

Can simplify these 10 lines:

affine, shape = other.affine, other.volume_shape
if self.affine is not None and (
        not np.allclose(affine, self.affine) or
        self.volume_shape != shape):
    raise ValueError(...)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If the other BrainModel has an affine or volume_shape of None (which is expected if it does not contain any voxels), this change will lead to an error.

The existing code ensures two things:

  1. If only one of the BrainModels has volumetric data (i.e. the affine is not set to None), the local variables affine and volume_shape will be set to that BrainModel.
  2. If both BrainModels have volumetric data, they are checked for consistency of the affine and volume_shape.

Copy link
Member

Choose a reason for hiding this comment

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

Ah, right. Sorry about that.

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.

This looks great! I made a few more comments, mostly in the docstrings.

The :class: linking I suggested may be a little overdone, so feel free not to go too wild in linking everything. I think in general it's good to link to objects that are not being directly documented, so hopefully that's what I suggested.


>>> from nibabel import cifti2
>>> bm_cortex = cifti2.BrainModel.from_mask(cortex_mask,
... brain_structure='cortex_left') # doctest: +SKIP
Copy link
Member

Choose a reason for hiding this comment

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

Are you able to generate these (or dummy) masks easily? Since we run doctests on docs like this, it would help to ensure that the docs are providing usable suggestions. If not, we can add this to the list of things to look into when we do a big sweep through the docs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I created some small, fake masks. I guess a better example would load some atlas and select a single ROI to include in the BrainModelAxis, but I am not aware of one easily availble

@MichielCottaar
Copy link
Contributor Author

Any ideas about the appveyor errors, @effigies ? They only happened in python 3.4 for the 32-bit system and seem unrelated with the changes I made in the code.

@effigies
Copy link
Member

@MichielCottaar Those errors are stochastic but persistent. I'm currently not worrying about them. Thanks for this.

@demianw Any final comments?

@demianw
Copy link
Contributor

demianw commented Mar 31, 2019

Nope! Great job @MichielCottaar!

@effigies effigies merged commit a59f3b0 into nipy:master Mar 31, 2019
@effigies
Copy link
Member

Thanks for your patience.

@MichielCottaar
Copy link
Contributor Author

Thanks for all your great feedback.

@demianw
Copy link
Contributor

demianw commented Mar 31, 2019

Yay!!!!!

@satra
Copy link
Member

satra commented Mar 31, 2019

awesome job @MichielCottaar . this will help us move things forward. i asked @effigies if we can start doing some interactive tutorials for nibabel that would serve as both static tutorials but also something people can immediately start playing with online. we can use the nipype tutorials as an example: https://miykael.github.io/nipype_tutorial/

effigies added a commit to matthew-brett/czi-nibabel that referenced this pull request Aug 2, 2020
For consideration:

* PySurfer : 13 (Alex Gramfort contributed FreeSurfer IO initially used there IIRC)
* MichielCottaar/cifti : 60 (nipy/nibabel#641 deprecated that tool altogether)
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.

6 participants