Skip to content

Commit

Permalink
Use readonly morphio Morphology (#979)
Browse files Browse the repository at this point in the history
Switch to using the morphio readonly morphology instead of mut

Co-authored-by: Mike Gevaert <[email protected]>
  • Loading branch information
eleftherioszisis and mgeplf authored May 3, 2022
1 parent ef56cbc commit d66d037
Show file tree
Hide file tree
Showing 8 changed files with 89 additions and 125 deletions.
108 changes: 45 additions & 63 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,7 +396,7 @@ 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)


Expand All @@ -429,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 @@ -504,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)

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 @@ -516,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 @@ -561,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 @@ -155,7 +155,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

0 comments on commit d66d037

Please sign in to comment.