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

Use readonly morphio Morphology #979

Merged
merged 16 commits into from
May 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 45 additions & 70 deletions neurom/core/morphology.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,34 +46,26 @@ class Section:

def __init__(self, morphio_section):
"""The section constructor."""
self.morphio_section = morphio_section
self._morphio_section = morphio_section

def to_morphio(self):
"""Returns the morphio section."""
return self._morphio_section

@property
def id(self):
"""Returns the section ID."""
return self.morphio_section.id
return self._morphio_section.id

@property
def parent(self):
"""Returns the parent section if non root section else None."""
if self.morphio_section.is_root:
return None
return Section(self.morphio_section.parent)
return None if self.is_root() else Section(self._morphio_section.parent)

@property
def children(self):
"""Returns a list of child section."""
return [Section(child) for child in self.morphio_section.children]

def append_section(self, section):
"""Appends a section to the current section object.

Args:
section (morphio.Section|morphio.mut.Section|Section|morphio.PointLevel): a section
"""
if isinstance(section, Section):
return self.morphio_section.append_section(section.morphio_section)
return self.morphio_section.append_section(section)
return [Section(child) for child in self._morphio_section.children]

def is_homogeneous_point(self):
"""A section is homogeneous if it has the same type with its children."""
Expand All @@ -93,7 +85,7 @@ def is_leaf(self):

def is_root(self):
"""Is tree the root node?"""
return self.parent is None
return self._morphio_section.is_root

def ipreorder(self):
"""Depth-first pre-order iteration of tree nodes."""
Expand Down Expand Up @@ -124,10 +116,10 @@ def iupstream(self, stop_node=None):
"""
if stop_node is None:
def stop_condition(section):
return section.parent is None
return section.is_root()
else:
def stop_condition(section):
return section == stop_node
return section.is_root() or section == stop_node

current_section = self
while not stop_condition(current_section):
Expand Down Expand Up @@ -157,35 +149,23 @@ def ibifurcation_point(self, iter_mode=ipreorder):

def __eq__(self, other):
"""Equal when its morphio section is equal."""
return self.morphio_section == other.morphio_section
return self.to_morphio().has_same_shape(other.to_morphio())

def __hash__(self):
"""Hash of its id."""
return self.id

def __nonzero__(self):
"""If has children."""
return self.morphio_section is not None

__bool__ = __nonzero__

@property
def points(self):
"""Returns the section list of points the NeuroM way (points + radius)."""
return np.concatenate((self.morphio_section.points,
self.morphio_section.diameters[:, np.newaxis] / 2.),
return np.concatenate((self._morphio_section.points,
self._morphio_section.diameters[:, np.newaxis] / 2.),
axis=1)

@points.setter
def points(self, value):
"""Set the points."""
self.morphio_section.points = np.copy(value[:, COLS.XYZ])
self.morphio_section.diameters = np.copy(value[:, COLS.R]) * 2

@property
def type(self):
"""Returns the section type."""
return NeuriteType(int(self.morphio_section.type))
return NeuriteType(int(self._morphio_section.type))

@property
def length(self):
Expand Down Expand Up @@ -234,11 +214,11 @@ def _homogeneous_subtrees(neurite):
sub-tree.
"""
it = neurite.root_node.ipreorder()
homogeneous_neurites = [Neurite(next(it).morphio_section)]
homogeneous_neurites = [Neurite(next(it).to_morphio())]

for section in it:
if section.type != section.parent.type:
homogeneous_neurites.append(Neurite(section.morphio_section))
homogeneous_neurites.append(Neurite(section.to_morphio()))

homogeneous_types = [neurite.type for neurite in homogeneous_neurites]

Expand Down Expand Up @@ -416,17 +396,10 @@ def graft_morphology(section):
"""Returns a morphology starting at section."""
assert isinstance(section, Section)
m = morphio.mut.Morphology()
m.append_root_section(section.morphio_section)
m.append_root_section(section.to_morphio())
return Morphology(m)


def graft_neuron(section):
"""Deprecated in favor of ``graft_morphology``."""
warn_deprecated('`neurom.core.neuron.graft_neuron` is deprecated in favor of '
'`neurom.core.morphology.graft_morphology`') # pragma: no cover
return graft_morphology(section) # pragma: no cover


class Neurite:
"""Class representing a neurite tree."""

Expand All @@ -436,7 +409,12 @@ def __init__(self, root_node):
Args:
root_node (morphio.Section): root section
"""
self.morphio_root_node = root_node
self._root_node = root_node

@property
def morphio_root_node(self):
"""Returns the morphio root section."""
return self._root_node

@property
def root_node(self):
Expand Down Expand Up @@ -511,10 +489,6 @@ def iter_sections(self, order=Section.ipreorder, neurite_order=NeuriteIter.FileO
"""
return iter_sections(self, iterator_type=order, neurite_order=neurite_order)

def __nonzero__(self):
"""If has root node."""
return bool(self.morphio_root_node)

Comment on lines -514 to -517
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mgeplf you mentioned you don't agree with removing the section booleaness. Is this still true?

Copy link
Collaborator

Choose a reason for hiding this comment

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

no; I can see how it's confusing; one can check if there are children more explicitly.

def __eq__(self, other):
"""If root node ids and types are equal."""
return self.type == other.type and self.morphio_root_node.id == other.morphio_root_node.id
Expand All @@ -523,37 +497,37 @@ def __hash__(self):
"""Hash is made of tuple of type and root_node."""
return hash((self.type, self.root_node))

__bool__ = __nonzero__

def __repr__(self):
"""Return a string representation."""
return 'Neurite <type: %s>' % self.type


class Morphology(morphio.mut.Morphology):
class Morphology:
"""Class representing a simple morphology."""

def __init__(self, filename, name=None):
"""Morphology constructor.

Args:
filename (str|Path): a filename
name (str): a option morphology name
filename (str|Path): a filename or morphio.{mut}.Morphology object
name (str): an optional morphology name
"""
super().__init__(filename)
self._morphio_morph = morphio.mut.Morphology(filename).as_immutable()
self.name = name if name else 'Morphology'
self.morphio_soma = super().soma
self.neurom_soma = make_soma(self.morphio_soma)
self.soma = make_soma(self._morphio_morph.soma)

@property
def soma(self):
"""Corresponding soma."""
return self.neurom_soma
def to_morphio(self):
"""Returns the morphio morphology object."""
return self._morphio_morph

@property
def neurites(self):
"""The list of neurites."""
return [Neurite(root_section) for root_section in self.root_sections]
return [Neurite(root_section) for root_section in self._morphio_morph.root_sections]

def section(self, section_id):
"""Returns the section with the given id."""
return Section(self._morphio_morph.section(section_id))

@property
def sections(self):
Expand All @@ -568,21 +542,22 @@ def points(self):

def transform(self, trans):
"""Return a copy of this morphology with a 3D transformation applied."""
obj = Morphology(self)
obj.morphio_soma.points = trans(obj.morphio_soma.points)
mut = self._morphio_morph.as_mutable()
mut.soma.points = trans(mut.soma.points)

for section in mut.iter():
section.points = trans(section.points)

for section in obj.sections:
section.morphio_section.points = trans(section.morphio_section.points)
return obj
return Morphology(mut)

def __copy__(self):
"""Creates a deep copy of Morphology instance."""
return Morphology(self, self.name)
return Morphology(self.to_morphio(), self.name)

def __deepcopy__(self, memodict={}):
"""Creates a deep copy of Morphology instance."""
# pylint: disable=dangerous-default-value
return Morphology(self, self.name)
return Morphology(self.to_morphio(), self.name)

def __repr__(self):
"""Return a string representation."""
Expand Down
7 changes: 0 additions & 7 deletions neurom/core/soma.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,6 @@ def points(self):
self._morphio_soma.diameters[:, np.newaxis] / 2.),
axis=1)

@points.setter
def points(self, values):
"""Set the points."""
values = np.asarray(values)
self._morphio_soma.points = np.copy(values[:, COLS.XYZ])
self._morphio_soma.diameters = np.copy(values[:, COLS.R]) * 2

@property
def volume(self):
"""Gets soma volume assuming it is a sphere."""
Expand Down
5 changes: 4 additions & 1 deletion neurom/io/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,10 @@ def load_morphology(morph, reader=None):
)
)'''), reader='asc')
"""
if isinstance(morph, (Morphology, morphio.Morphology, morphio.mut.Morphology)):
if isinstance(morph, Morphology):
return Morphology(morph.to_morphio())

if isinstance(morph, (morphio.Morphology, morphio.mut.Morphology)):
return Morphology(morph)

if reader:
Expand Down
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,8 @@ requires = [
"wheel",
]
build-backend = "setuptools.build_meta"

[tool.pytest.ini_options]
testpaths = [
"tests",
]
6 changes: 2 additions & 4 deletions tests/core/test_iter.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,8 @@ def test_iter_population():


def test_iter_sections_default():

ref = [s for n in POP.neurites for s in n.iter_sections()]
assert (ref ==
[n for n in iter_sections(POP)])
ref = [s.id for n in POP.neurites for s in n.iter_sections()]
assert (ref == [n.id for n in iter_sections(POP)])

def test_iter_sections_default_pop():
ref = [s.id for n in POP.neurites for s in n.iter_sections()]
Expand Down
10 changes: 0 additions & 10 deletions tests/core/test_neuron.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,6 @@ def test_for_morphio():
[1., 1., 1., 0.5],
[2., 2., 2., 0.5]])

neurom_m.soma.points = [[1, 1, 1, 1],
[2, 2, 2, 2]]
assert_array_equal(neurom_m.soma.points,
[[1, 1, 1, 1],
[2, 2, 2, 2]])


def _check_cloned_morphology(m, m2):
# check if two morphs are identical
Expand All @@ -116,10 +110,6 @@ def _check_cloned_morphology(m, m2):
for neu1, neu2 in zip(m.neurites, m2.neurites):
assert neu1 is not neu2

# check if changes are propagated between morphs
m2.soma.radius = 10.
assert m.soma.radius != m2.soma.radius


def test_copy():
m = nm.load_morphology(SWC_PATH / 'simple.swc')
Expand Down
37 changes: 9 additions & 28 deletions tests/core/test_section.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,14 @@ def test_section_base_func():
# __nonzero__
assert section


def test_section_tree():
m = nm.load_morphology(str(SWC_PATH / 'simple.swc'))

assert m.sections[0].parent is None

assert m.sections[0] == m.sections[0]

assert m.sections[0] == m.sections[0].children[0].parent

assert_array_equal([s.is_root() for s in m.sections],
Expand All @@ -70,6 +74,11 @@ def test_section_tree():
[0])
assert_array_equal([s.id for s in m.sections[2].iupstream()],
[2, 0])
assert_array_equal([s.id for s in m.sections[2].iupstream(stop_node=m.sections[2])],
[2])
# if a stop node that is not upstream is given, it should stop at root
assert_array_equal([s.id for s in m.sections[2].iupstream(stop_node=m.sections[1])],
[2, 0])
assert_array_equal([s.id for s in m.neurites[0].root_node.ileaf()],
[1, 2])
assert_array_equal([s.id for s in m.sections[2].ileaf()],
Expand All @@ -78,31 +87,3 @@ def test_section_tree():
[0])
assert_array_equal([s.id for s in m.neurites[0].root_node.ibifurcation_point()],
[0])


def test_append_section():
n = nm.load_morphology(SWC_PATH / 'simple.swc')
s = n.sections[0]

s.append_section(n.sections[-1])
assert len(s.children) == 3
assert s.children[-1].id == 6
assert s.children[-1].type == n.sections[-1].type

s.append_section(n.sections[-1].morphio_section)
assert len(s.children) == 4
assert s.children[-1].id == 7
assert s.children[-1].type == n.sections[-1].type


def test_set_points():
n = nm.load_morphology(SWC_PATH / 'simple.swc')
s = n.sections[0]
s.points = np.array([
[0, 5, 0, 2],
[0, 7, 0, 2],
])
assert_array_equal(s.points, np.array([
[0, 5, 0, 2],
[0, 7, 0, 2],
]))
Loading