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

move_to method #320

Merged
merged 32 commits into from
Apr 24, 2024
Merged

move_to method #320

merged 32 commits into from
Apr 24, 2024

Conversation

kyralianaka
Copy link
Contributor

This PR adds a method to base.py that allows one to move cells or networks to specific x, y, z coordinates.

It takes a float, such that the first compartment of the first branch of the first cell is moved to that float coordinate, and everything else is shifted accordingly.

However, it can also take an array where the x, y, z dimensions all have to be the same (and the same as the number of cells), and this allows moving cells to specific x, y, z locations all at once. I wanted to write this so that one doesn't have to loop over all of the x, y, z coordinates of each cell to assign their locations, which would be very cumbersome for a large network.

I also wrote a file with tests for move() (which were missing before as far as I could tell) and move_to().

I look forward to hearing feedback and thoughts, because I'm sure there is some room for improvement.

@kyralianaka
Copy link
Contributor Author

I just realized that I vectorized the part that takes the coordinate arrays but not the part that takes floats, so I will vectorize that "half" of the method too now:)

Copy link
Contributor

@michaeldeistler michaeldeistler left a comment

Choose a reason for hiding this comment

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

Awesome, thanks!

Please write a docstring that explains exactly what this method is doing.

jaxley/modules/base.py Outdated Show resolved Hide resolved
jaxley/modules/base.py Outdated Show resolved Hide resolved
jaxley/modules/base.py Outdated Show resolved Hide resolved
jaxley/modules/base.py Outdated Show resolved Hide resolved
# Compute the within cell branch offsets
tup_indices = np.array([view.cell_index, view.branch_index])
branches_per_cell = np.unique(tup_indices, axis=1)[0]
_, first_branches = np.unique(branches_per_cell, return_index=True)
Copy link
Contributor

Choose a reason for hiding this comment

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

to get the first branches, can you not do net[:,0,0].view.index?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not really because what I need is the index into xyzr which has length equal to the number of branches, not the number of compartments like view

tests/test_moving.py Show resolved Hide resolved
@michaeldeistler
Copy link
Contributor

I think you can almost exactly copy the PR description into the docstring :)

@kyralianaka
Copy link
Contributor Author

Thanks for the feedback @michaeldeistler!

I added lots of comments, so you can let me know now if it's too much or too little.

Unfortunately I still cannot use move_to() with array inputs and groups, because group.xyzr doesn't exist (like network.xyzr), but we can talk about whether that's worth implementing.

Copy link
Contributor

@michaeldeistler michaeldeistler left a comment

Choose a reason for hiding this comment

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

Thanks for pointing out that this is not yet possible :) But I think this can be done without much code, see me comment below. Let me know if you want to have a quick zoom.

tests/test_moving.py Show resolved Hide resolved
@@ -1021,6 +1020,73 @@ def _move(self, x: float, y: float, z: float, view):
self.xyzr[i][:, 1] += y
self.xyzr[i][:, 2] += z

def move_to(
Copy link
Contributor

Choose a reason for hiding this comment

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

Okay I just saw that this cannot move submodules, right? I.e.

net.cell(0).move_to(x, y, z)

does not work, right?

I think this should definitely be possible. The way to do this is to pass self.nodes to the ._move_to() method in Module and to add a move_to method to View and pass self.view to _move_to() in the View. You can take inspiration from other methods: For example the .show() method passes self.nodes here and self.view in the View here

Copy link
Contributor Author

@kyralianaka kyralianaka Apr 9, 2024

Choose a reason for hiding this comment

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

That actually does work, I wrote the function just like move() so it is also in view (these lines). Hope this is alright :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well sorry, this works to update the coordinates in the view, but then I guess it doesn't update the module's xyzr, so I will try to fix that

Copy link
Contributor

Choose a reason for hiding this comment

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

you probably need self.pointer.xyzr

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok I made it so that it can take cell view input now. It is not very pretty underneath, but when one does net.cell(0).move_to(x, y, z), one is really operating on net.xyzr, and it makes sense that this is distinct from operating on a cell.xyzr, which would only be the case if one does cell.move_to(x, y, z). I am totally open to prettier ways of doing this, but for now it works, and it is even possible to do net.cell([0, 1, ...]).move_to(x, y, z) where x, y, and z are arrays the length of the number of cells!

I also added ._update_nodes_with_xyz() to both the move and move_to methods at the end so that the results are shown in the view.

Another thing I added is that if someone tries to call move() or move_to() with float input and xyz is not calculated yet, then it will print something and run .compute_xyz(). If this isn't done, then xyzr will just stay entirely nans, and the user might be confused as to why.

@jnsbck
Copy link
Contributor

jnsbck commented Apr 9, 2024

This adds xyzr to Views.

@property
def xyzr(self):
    idxs = self.view.global_comp_index
    return np.array(self.pointer.xyzr)[idxs]

Works until .comp but for .loc one would need some kind of interpolation.


@property
def xyzr(self):
idxs = self.view.global_comp_index
Copy link
Contributor

Choose a reason for hiding this comment

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

We might wanna raise on .loc or add `interpolation of coordinates like in

def interpolate_xyz(loc: float, coords: np.ndarray):

@kyralianaka
Copy link
Contributor Author

I was getting some weird behavior trying to use the xyzr property before, so I removed it, but I will add it back in when I make sure that it works alright. Then I'll see if it can be used to clean up the method a bit. Thanks for your patience :)

@kyralianaka
Copy link
Contributor Author

kyralianaka commented Apr 16, 2024

A couple of updates on this pull request:

  1. I added the xyzr property to View, but this cannot then be called within the module methods, because they don't actually seem to get the cell view passed to them. One could access the full module view in these methods with self.show(), and then index into it using the "view" dataframe as I am already doing, but it doesn't seem like one can simply call view.xyzr in these module methods (because view is just a dataframe then as created by set_global_index_and_index).

  2. The move() method can take inhomogeneously shaped xyzr input created by the functions that read swc files. Allowing these inhomogeneously shaped coordinate arrays means that the xyzr property of the network cannot be operated on as an array, and this is a big limitation in terms of computational time. I adjusted the code so that the xyzr property is agnostic to the form of the self.xyzr entries, but move_to() requires the form of xyzr to be [# branches, [branch start, branch end], [x, y, z, r]] (this is checked). I have some ideas about how to deal with this situation moving forward, but for now, move_to() does what I want it to do and works on cell views.

Looking forward to hearing your thoughts :)

Edit: I guess I will work next on getting move_to to work with the swc coordinate arrays, hopefully vectorized but we'll see (right now it just raises and says to use move().

Copy link
Contributor

@michaeldeistler michaeldeistler left a comment

Choose a reason for hiding this comment

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

Thanks a lot!

I think we definitely have to support SWC files though. I think an easy way (that will also save a ton of code) would be to have a function that modifies one coordinate and jit that. Would that not work? We can also discuss on Thu.


NOTE: This method works when module.xyzr is not of the form
(n branches, [start_branch, end_branch], 4) but rather constructed by the code
that reads swc files."""
Copy link
Contributor

Choose a reason for hiding this comment

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

So it does not work if the xzy is constructed with self.compute_xyz()?

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay I only now (after having seen code below) understand this sentence. So, IIUC, it does work for coordinates generated from self.compute_xyz() and it does not work for those generated from read_swc, right?

Anyways, I think this is a pretty big limitation. Can we not jit-compile the for loop across branches and make it fast without having to rely on matrix operations?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, sorry that was not worded correctly.

I should definitely get something working for xyzrs from SWCs -- the only reason I didn't before is just because I didn't realize they were different in shape from xyzr generated by compute_xyz().

The one thing that I find a bit strange about xyzr from SWC files though is that some branches have a (4, ) shape array as the xyzr, so not even the start and end of the branch. I guess this then means that those branches have length zero or something. I will have to look into this more though.

As for the jit compiled loop, jitting would require static argument shapes, and different networks will have different numbers of branches. If we jit the moving of one branch, I'm not terribly convinced there would be a great speed-up because the function is then just one variable assignment, but I can test it. vmap might be really helpful though.

@@ -1024,6 +1028,102 @@ def _move(self, x: float, y: float, z: float, view):
self.xyzr[i][:, 0] += x
self.xyzr[i][:, 1] += y
self.xyzr[i][:, 2] += z
self._update_nodes_with_xyz()
Copy link
Contributor

Choose a reason for hiding this comment

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

why are you modifying ._move()?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Without this line, the xyz coords don't update in the view, so calling network.show() after calling .move() would not update the xyz. This is run at the end of move_to() as well. We could require that the user call _update_notes_with_xyz() after calling move() and move_to(), but I thought that was a bit of a hassle

If x, y, and z are arrays, then they must each have a length equal to the number
of cells being moved. Then the first compartment of the first branch of each
cell is moved to the specified location. This avoids the need to move cells in
loops.
Copy link
Contributor

Choose a reason for hiding this comment

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

awesome!

x: Union[float, np.ndarray] = 0.0,
y: Union[float, np.ndarray] = 0.0,
z: Union[float, np.ndarray] = 0.0,
):
Copy link
Contributor

Choose a reason for hiding this comment

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

type hint for return type: -> None.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is not done for any of the other methods in base.py though as far as I see, should I add that everywhere? :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Or I could just do it here and then I guess that would be a separate PR

jaxley/modules/base.py Outdated Show resolved Hide resolved
jaxley/modules/base.py Outdated Show resolved Hide resolved
jaxley/modules/base.py Outdated Show resolved Hide resolved
jaxley/modules/base.py Outdated Show resolved Hide resolved
# Find the indices in xyzr of the first branch of each cell
_, first_branches = np.unique(cell_inds, return_index=True)
# Select the xyz coordinates of the first branch of each cell
xyz_first_branches = xyzr_arr[first_branches, :, :3]
Copy link
Contributor

Choose a reason for hiding this comment

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

can you not do pointer[cell_inds, 0, 0].xyzr?

# Compute the distance between first branches and their connected branches
branch_offsets = xyzr_arr[:, :, :3] - xyz_firsts_expanded

# Compute new coord arr [# branches, [branch start, branch end], [x, y, z]]
Copy link
Contributor

Choose a reason for hiding this comment

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

I think if we just use a jitted for-loop we can get rid of almost all of this, no?

Copy link
Contributor

@michaeldeistler michaeldeistler left a comment

Choose a reason for hiding this comment

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

This is looking great now! Thanks a ton!

@jnsbck jnsbck mentioned this pull request Apr 24, 2024
9 tasks
@kyralianaka kyralianaka merged commit c6d81cc into main Apr 24, 2024
1 check passed
@kyralianaka kyralianaka deleted the move_to branch April 24, 2024 13:37
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