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] Make cell morphology plots reflect actual cell size #481

Merged
merged 11 commits into from Jul 18, 2022
6 changes: 6 additions & 0 deletions doc/whats_new.rst
Expand Up @@ -18,6 +18,9 @@ Changelog
- Changed ``conn_seed`` default to ``None`` (from ``3``) in :func:`~hnn_core.network.add_connection`,
by `Mattan Pelah`_ in :gh:`492`.

- Add interface to modify attributes of sections in
:func:`~hnn_core.Cell.modify_section`, by `Nick Tolley`_ in :gh:`481`

Bug
~~~
- Fix bugs in drives API to enable: rate constant argument as float; evoked drive with
Expand All @@ -42,6 +45,9 @@ Bug
- Add option to plot the averaged dipole in `~hnn_core.viz.plot_dipole` when `dpl`
is a list of dipoles, by `Huzi Cheng`_ in :gh:`475`.

- Fix bug where :func:`~hnn_core.viz.plot_morphology` did not accurately
rythorpe marked this conversation as resolved.
Show resolved Hide resolved
reflect the shape of the cell being simulated, by `Nick Tolley`_ in :gh:`481`

API
~~~
- Optimization of the evoked drives can be conducted on any :class:`~hnn_core.Network`
Expand Down
91 changes: 80 additions & 11 deletions hnn_core/cell.py
Expand Up @@ -11,6 +11,7 @@
from neuron import h, nrn

from .viz import plot_cell_morphology
from .externals.mne import _validate_type, _check_option

# Units for e: mV
# Units for gbar: S/cm^2
Expand Down Expand Up @@ -159,11 +160,11 @@ class Section:
Parameters
----------
L : float
length of a section in microns
length of a section in microns.
diam : float
diameter of a section in microns
diameter of a section in microns.
cm : float
membrane capacitance in micro-Farads
membrane capacitance in micro-Farads.
Ra : float
axial resistivity in ohm-cm
end_pts : list of [x, y, z]
Expand All @@ -180,20 +181,20 @@ class Section:
end_pts : list of [x, y, z]
The start and stop points of the section. Cannot be changed.
L : float
length of a section in microns. Cannot be changed.
length of a section in microns.
diam : float
diameter of a section in microns. Cannot be changed.
diameter of a section in microns.
cm : float
membrane capacitance in micro-Farads
membrane capacitance in micro-Farads.
Ra : float
axial resistivity in ohm-cm
axial resistivity in ohm-cm.
"""
def __init__(self, L, diam, Ra, cm, end_pts=None):

self._L = L
self._diam = diam
self.Ra = Ra
self.cm = cm
self._Ra = Ra
self._cm = cm
if end_pts is None:
end_pts = list()
self._end_pts = end_pts
Expand All @@ -212,6 +213,14 @@ def L(self):
def diam(self):
return self._diam

@property
def cm(self):
return self._cm

@property
def Ra(self):
return self._Ra

@property
def end_pts(self):
return self._end_pts
Expand Down Expand Up @@ -311,6 +320,8 @@ def __init__(self, name, pos, sections, synapses, topology, sect_loc,
if gid is not None:
self.gid = gid # use setter method to check input argument gid

self._update_end_pts()

def __repr__(self):
class_name = self.__class__.__name__
return f'<{class_name} | gid={self._gid}>'
Expand Down Expand Up @@ -374,6 +385,8 @@ def _create_sections(self, sections, topology):
for height and xz plane for depth. This is opposite for model as a
whole, but convention is followed in this function ease use of gui.
"""
if 'soma' not in self.sections:
raise KeyError('soma must be defined for cell')
# shift cell to self.pos and reorient apical dendrite
# along z direction of self.pos
dx = self.pos[0] - self.sections['soma'].end_pts[0][0]
Expand Down Expand Up @@ -428,8 +441,6 @@ def build(self, sec_name_apical=None):
with this section. The section should belong to the apical dendrite
of a pyramidal neuron.
"""
if 'soma' not in self.sections:
raise KeyError('soma must be defined for cell')
self._create_sections(self.sections, self.topology)
self._create_synapses(self.sections, self.synapses)
self._set_biophysics(self.sections)
Expand Down Expand Up @@ -640,3 +651,61 @@ def plot_morphology(self, ax=None, cell_types=None, show=True):
The matplotlib 3D axis handle.
"""
return plot_cell_morphology(self, ax=ax, show=show)

def _update_end_pts(self):
""""Create cell and copy coordinates to Section.end_pts"""
self._create_sections(self.sections, self.topology)
section_names = list(self.sections.keys())

for name in section_names:
nrn_pts = self._nrn_sections[name].psection()['morphology'][
'pts3d']

del self._nrn_sections[name]

x0, y0, z0 = nrn_pts[0][0], nrn_pts[0][1], nrn_pts[0][2]
x1, y1, z1 = nrn_pts[1][0], nrn_pts[1][1], nrn_pts[1][2]
self.sections[name]._end_pts = [[x0, y0, z0], [x1, y1, z1]]

self._nrn_sections = dict()

def modify_section(self, sec_name, L=None, diam=None, cm=None, Ra=None):
"""Change attributes of section specified by `sec_name`

Parameters
----------
sec_name : str
Name of section to be modified. Must be a key of Cell.sections
L : float | int | None
length of a section in microns. Default None.
diam : float | int | None
diameter of a section in microns.
cm : float | int | None
membrane capacitance in micro-Farads.
Ra : float | int | None
axial resistivity in ohm-cm.

Notes
-----
Leaving default of None produces no change.
"""
valid_sec_names = list(self.sections.keys())
_check_option('sec_name', sec_name, valid_sec_names)

if L is not None:
_validate_type(L, (float, int), 'L')
self.sections[sec_name]._L = L

if diam is not None:
_validate_type(diam, (float, int), 'diam')
self.sections[sec_name]._diam = diam

if cm is not None:
_validate_type(cm, (float, int), 'cm')
self.sections[sec_name]._cm = cm

if Ra is not None:
_validate_type(Ra, (float, int), 'Ra')
self.sections[sec_name]._Ra = Ra

self._update_end_pts()
28 changes: 26 additions & 2 deletions hnn_core/tests/test_cell.py
@@ -1,5 +1,6 @@
import pytest
import pickle
import numpy as np

import matplotlib

Expand All @@ -15,7 +16,8 @@ def test_cell():

name = 'test'
pos = (0., 0., 0.)
sections = {'blah': Section(L=1, diam=5, Ra=3, cm=100)}
sections = {'soma': Section(L=1, diam=5, Ra=3, cm=100,
end_pts=[[0, 0, 0], [0, 39., 0]])}
synapses = {'ampa': dict(e=0, tau1=0.5, tau2=5.)}
topology = None
sect_loc = {'proximal': 'soma'}
Expand Down Expand Up @@ -45,8 +47,12 @@ def test_cell():
cell.syn_create(0.5, e=0., tau1=0.5, tau2=5.)

pickle.dumps(cell) # check cell object is picklable until built

bad_sections = {'blah': Section(L=1, diam=5, Ra=3, cm=100,
end_pts=[[0, 0, 0], [0, 39., 0]])}
# Check soma must be included in sections
with pytest.raises(KeyError, match='soma must be defined'):
cell.build()
cell = Cell(name, pos, bad_sections, synapses, topology, sect_loc)

sections = {
'soma': Section(
Expand Down Expand Up @@ -79,6 +85,24 @@ def test_cell():
with pytest.raises(ValueError, match='sec_name_apical must be an'):
cell.build(sec_name_apical='blah')

# Test section modification
sec_name = 'soma'
new_L = 1.0
new_diam = 2.0
new_cm = 3.0
new_Ra = 4.0
cell.modify_section(sec_name, L=new_L, diam=new_diam, cm=new_cm, Ra=new_Ra)

# Make sure distance betweeen `Section.end_pts` matches `Section.L`
new_pts = np.array(cell.sections[sec_name].end_pts)
new_dist = np.linalg.norm(new_pts[0, :] - new_pts[1, :])
np.isclose(new_L, new_dist)

assert cell.sections[sec_name].L == new_L
assert cell.sections[sec_name].diam == new_diam
assert cell.sections[sec_name].cm == new_cm
assert cell.sections[sec_name].Ra == new_Ra


def test_artificial_cell():
"""Test artificial cell object."""
Expand Down