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

[MRG] Add ability to record synaptic currents #523

Merged
merged 7 commits into from
Nov 28, 2024

Conversation

ntolley
Copy link
Contributor

@ntolley ntolley commented Nov 20, 2024

Closes #363 and #533

As discussed with @jnsbck in #515, this PR adds synapse_current_names as an attribute to the base module, and then appends this to the synapse states in _get_state_names(). The end goal is for currents to be recordable via:

net.record('IonotropicSynapse_current')

Some lingering thoughts:

  1. Is it worth changing "current" to "i" for consistency with the channel naming conventions?
  2. It took me awhile to figure out that I needed to modify the View class. Will it be necessary to add a self._synapses_in_view() property for this to work properly?
  3. Add some tests that check basic assumptions of the current recording?

@ntolley
Copy link
Contributor Author

ntolley commented Nov 20, 2024

FYI here's the currents and voltages from the tutorial network

image

Seems reasonable to me? I'll admit I'm just guessing on the units for the synaptic current 😄

@jnsbck
Copy link
Contributor

jnsbck commented Nov 21, 2024

Nice! Just had a superficial look and it looks like not a lot of changes had to be made for the above to work. Units should be the standard current units if I am not mistaken (see here) so nA seems right. Regarding your lingerning thoughts:

  1. I'd also be in favor of changing it to "i"
  2. The View v Module has a small learning curve indeeed! You might not have to add a _synapses_in_view, but we should make sure the membrane currents are filtered by the synapses in view. sth like [i for i, syn in zip(self.base.synapse_current_names, self.synapses) if syn._name in i] should do the trick. Also for more intuition on View v Module see jaxley api tutorial, (I assume you already read the docstrings :) )
  3. Yes, we should definitely make sure this is tested well. (which I just noticed we don't do for channel_currents at the moment), maybe you can add a quick test for i_HH along with the synapse current to
    def test_record_synaptic_and_membrane_states(SimpleNet):
    other than testing that it works, I don't know what would be sensible to check though. @michaeldeistler any ideas?

Lemme me know when you want a review :)

@michaeldeistler
Copy link
Contributor

Awesome, thanks a lot for getting this started @ntolley!

I think the following would be important to test:

  1. It plays nicely with advanced indexing of synapses. E.g., to record only synaptic currents of for pre-synaptic neuron 3 and 4:
fully_connect(net.cell("all"), net.cell("all"), IonotropicSynapse())
net.copy_node_property_to_edges("global_cell_index")
df = net.edges
df = df.query("pre_global_cell_index in [3, 4]")
net.select(edges=df.index).record("Ionotropic_s")
  1. If there are multiple kinds of synapses (e.g. IonotropicSynapse and TanhSynapse), then there is no mixup between them. E.g.:
fully_connect(net.cell("all"), net.cell("all"), IonotropicSynapse())
fully_connect(net.cell("all"), net.cell("all"), TanhSynapse())
df = net.edges
df = df.query("SynapseType == 'IonotropicSynapse'")
net.select(edges=df.index).record("Ionotropic_s")

Let me know if anything is unclear!

@@ -2422,7 +2426,8 @@ def __init__(
)

self.channels = self._channels_in_view(pointer)
self.membrane_current_names = [c._name for c in self.channels]
self.membrane_current_names = [c.current_name for c in self.channels]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Think this was a bug, it wasn't actually possible to record channels currents from a View because of the naming

Copy link
Contributor

Choose a reason for hiding this comment

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

good catch!

@ntolley
Copy link
Contributor Author

ntolley commented Nov 26, 2024

Got sent down a rabbit hole that led to #533, unfortunately edges aren't being updated in the View so that's going to need to be fixed to actually have the correct synpases be filtered out.

@@ -2490,7 +2495,7 @@ def _set_inds_in_view(
].unique()
pre = base_edges["pre_global_comp_index"].isin(incl_comps).to_numpy()
post = base_edges["post_global_comp_index"].isin(incl_comps).to_numpy()
possible_edges_in_view = base_edges.index.to_numpy()[(pre & post).flatten()]
possible_edges_in_view = base_edges.index.to_numpy()[(pre | post).flatten()]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@michaeldeistler @jnsbck this is the culprit for #533 ! Essentially if you're indexing net.cell(0), then it's impossible for a view of that cell to include the compartments of another cell (unless it's an autapse)

I think this is a question of how you want this to behave. I've changed it to an "or" condition, so net.cell(0).edges will include both synapses onto this cell, as well as synapses it makes with other cells

For the purposes of this PR, it would make sense to only include the edges of the post synaptic cell, however you could argue for different expected behaviors.

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 decided to just go forward with only appending the post synaptic target so that I could finish the tasks for this PR. We can discuss if that's actually the desired behavior for the View

@@ -1207,9 +1208,14 @@ def _get_state_names(self) -> Tuple[List, List]:

Returns states seperated by comps and edges."""
channel_states = [name for c in self.channels for name in c.channel_states]
synapse_states = [name for s in self.synapses for name in s.synapse_states]
synapse_states = [
name for s in self.synapses if s is not None for name in s.synapse_states
Copy link
Contributor Author

Choose a reason for hiding this comment

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

the not None filter is necessary since self.synapses is populated with [None] under certain conditions

I'll admit I don't totally understand why it's necessary just yet, although I saw the comment about it preserving indexing 😄

@ntolley ntolley changed the title WIP: Add ability to record synaptic currents [MRG] Add ability to record synaptic currents Nov 26, 2024
@ntolley
Copy link
Contributor Author

ntolley commented Nov 26, 2024

I think I've fixed most of the issues! The new tests are passing so this is ready for a review

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 awesome, thanks a ton!

A few comments below, mostly minor things. I suggest we stick to pre & post for now, we could change this behavior in another PR if we want to. Also, please add an entry in the changelog describing the new feature and the bug fixes :)

Good to go once changes are addressed! Thanks again!

@@ -2422,7 +2426,8 @@ def __init__(
)

self.channels = self._channels_in_view(pointer)
self.membrane_current_names = [c._name for c in self.channels]
self.membrane_current_names = [c.current_name for c in self.channels]
Copy link
Contributor

Choose a reason for hiding this comment

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

good catch!

post = base_edges["post_global_comp_index"].isin(incl_comps).to_numpy()
possible_edges_in_view = base_edges.index.to_numpy()[(pre & post).flatten()]
possible_edges_in_view = base_edges.index.to_numpy()[(post).flatten()]
Copy link
Contributor

Choose a reason for hiding this comment

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

let's revert this to pre & post, see my response in #533

net.cell(2).branch(0).loc(0.0).record("v")
net.IonotropicSynapse.edge(1).record("IonotropicSynapse_s")
net.cell(2).branch(0).loc(0.0).record("HH_m")
net.cell(1).branch(0).loc(0.0).record("v")
net.TestSynapse.edge(0).record("TestSynapse_c")
net.cell(1).branch(0).loc(0.0).record("HH_m")
net.cell(1).branch(0).loc(0.0).record("i_HH")
Copy link
Contributor

Choose a reason for hiding this comment

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

thanks!

@ntolley
Copy link
Contributor Author

ntolley commented Nov 27, 2024

Think all the comments have been addressed, thanks @michaeldeistler @jnsbck for the help with this PR 🙌

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.

LGTM, thanks a lot!

@michaeldeistler michaeldeistler merged commit d89fe7b into jaxleyverse:main Nov 28, 2024
1 check passed
@jnsbck
Copy link
Contributor

jnsbck commented Dec 2, 2024

Awesome! Thanks for tackling this! 🚀

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.

record synaptic currents
3 participants