Skip to content

Commit

Permalink
Decouple Morphology constructor from io (#1120)
Browse files Browse the repository at this point in the history
  • Loading branch information
eleftherioszisis authored Apr 25, 2024
1 parent 985a4dd commit 98b5326
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 36 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Changelog
Version 4.0.0
-------------

- Morphology class accepts only morphio objects, not files anymore. (#1120)
- Replace ``iter_*`` methods by properties in core objects and improve ``iter_segments``. (#1054)
- NeuriteType extended to allow mixed type declarations as tuple of ints. (#1071)
- All features return built-in types (#1064)
Expand Down
10 changes: 7 additions & 3 deletions doc/source/migration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,19 @@ Breaking changes in Morphology class
The Morphology class has changed in two major ways:

* Does not derive from morphio.mut.Morphology
* By default an immutable morphio Morphology is instantiated
* It accepts a morphio object as an argument

The morphio Morphology is stored as a protected attribute in neurom Morphology object turning
the latter into a wrapper around morphio Morphology.

.. warning::
Morphology class will raise a NeuroMerror if a filepath is passed as an argument. Please
use `neurom.load_morphology()` to load from file or a stream.

However, it is still accessible via the ``to_morphio()`` method:

.. testcode:: [v4-migration]

from neurom import load_morphology
neurom_morphology = load_morphology('tests/data/swc/Neuron.swc')
ref_morph = neurom_morphology.to_morphio()
Expand All @@ -101,7 +105,7 @@ which means that the default morphio Morphology is immutable. It is however poss
neurom_morphology = load_morphology(morphio_morphology)
ref_morph = neurom_morphology.to_morphio()

print(type(ref_morph).__module__, type(ref_morph).__name__)
print(type(ref_morph).__module__, type(ref_morph).__name__)

.. testoutput:: [v4-migration]

Expand Down
15 changes: 9 additions & 6 deletions neurom/core/morphology.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@

import warnings
from collections import deque
from pathlib import Path

import morphio
import numpy as np
Expand All @@ -41,6 +40,7 @@
from neurom.core.population import Population
from neurom.core.soma import make_soma
from neurom.core.types import NeuriteIter, NeuriteType
from neurom.exceptions import NeuroMError
from neurom.utils import flatten


Expand Down Expand Up @@ -538,18 +538,21 @@ def __repr__(self):
class Morphology:
"""Class representing a simple morphology."""

def __init__(self, filename, name=None, process_subtrees=False):
def __init__(self, morphio_morph, name=None, process_subtrees=False):
"""Morphology constructor.
Args:
filename (str|Path): a filename or morphio.{mut}.Morphology object
morphio_morph (morphio.Morphology|morphio.mut.Morphology): a morphio object
name (str): an optional morphology name
process_subtrees (bool): enable mixed tree processing if set to True
"""
self._morphio_morph = morphio.mut.Morphology(filename)
if not isinstance(morphio_morph, (morphio.Morphology, morphio.mut.Morphology)):
raise NeuroMError(
f"Expected morphio Morphology object but got: {morphio_morph}.\n"
f"Use neurom.load_morphology() to load from file."
)

if isinstance(filename, (str, Path, morphio.Morphology)):
self._morphio_morph = self._morphio_morph.as_immutable()
self._morphio_morph = morphio_morph

self.name = name if name else 'Morphology'
self.soma = make_soma(self._morphio_morph.soma)
Expand Down
32 changes: 22 additions & 10 deletions neurom/io/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def _get_file(stream, extension):
return temp_file


def load_morphology(morph, reader=None, process_subtrees=False):
def load_morphology(morph, reader=None, mutable=None, process_subtrees=False):
"""Build section trees from a morphology or a h5, swc or asc file.
Args:
Expand All @@ -132,6 +132,9 @@ def load_morphology(morph, reader=None, process_subtrees=False):
- a morphio mutable or immutable Morphology object
- a stream that can be put into a io.StreamIO object. In this case, the READER argument
must be passed with the corresponding file format (asc, swc and h5)
mutable (bool|None): Whether to enforce mutability. If None and a morphio/neurom object is
passed, the initial mutability will be maintained. If None and the
morphology is loaded, then it will be immutable by default.
reader (str): Optional, must be provided if morphology is a stream to
specify the file format (asc, swc, h5)
Expand All @@ -157,15 +160,24 @@ def load_morphology(morph, reader=None, process_subtrees=False):
)'''), reader='asc')
"""
if isinstance(morph, Morphology):
return Morphology(morph.to_morphio(), process_subtrees=process_subtrees)

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

if reader:
return Morphology(_get_file(morph, reader), process_subtrees=process_subtrees)

return Morphology(morph, Path(morph).name, process_subtrees=process_subtrees)
name = morph.name
morphio_morph = morph.to_morphio()
elif isinstance(morph, (morphio.Morphology, morphio.mut.Morphology)):
name = "Morphology"
morphio_morph = morph
else:
filepath = _get_file(morph, reader) if reader else morph
name = os.path.basename(filepath)
morphio_morph = morphio.Morphology(filepath)

# None does not modify existing mutability
if mutable is not None:
if mutable and isinstance(morphio_morph, morphio.Morphology):
morphio_morph = morphio_morph.as_mutable()
elif not mutable and isinstance(morphio_morph, morphio.mut.Morphology):
morphio_morph = morphio_morph.as_immutable()

return Morphology(morphio_morph, name=name, process_subtrees=process_subtrees)


def load_morphologies(
Expand Down
22 changes: 5 additions & 17 deletions tests/core/test_neuron.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@
from copy import copy, deepcopy
from pathlib import Path

import pytest
import neurom as nm
import numpy as np
import morphio
from neurom.core.morphology import Morphology, graft_morphology, iter_segments
from numpy.testing import assert_array_equal
from neurom.exceptions import NeuroMError

SWC_PATH = Path(__file__).parent.parent / 'data/swc/'

Expand Down Expand Up @@ -65,9 +67,6 @@ def test_load_morphology_from_other_morphologies():
]

assert_array_equal(nm.load_morphology(nm.load_morphology(filename)).points, expected_points)

assert_array_equal(nm.load_morphology(Morphology(filename)).points, expected_points)

assert_array_equal(nm.load_morphology(morphio.Morphology(filename)).points, expected_points)


Expand Down Expand Up @@ -142,17 +141,6 @@ def test_str():
assert 'Section' in str(n.neurites[0].root_node)


def test_mut_nonmut_constructor():
path = SWC_PATH / 'simple.swc'

m = Morphology(path)
assert isinstance(m.to_morphio(), morphio.Morphology)

m = Morphology(str(path))
assert isinstance(m.to_morphio(), morphio.Morphology)

m = Morphology(morphio.Morphology(path))
assert isinstance(m.to_morphio(), morphio.Morphology)

m = Morphology(morphio.mut.Morphology(path))
assert isinstance(m.to_morphio(), morphio.mut.Morphology)
def test_morphology_raises_wrong_argument():
with pytest.raises(NeuroMError, match="Expected morphio Morphology object but got: my-path"):
Morphology("my-path")
59 changes: 59 additions & 0 deletions tests/io/test_io_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from pathlib import Path

import numpy as np
import morphio
from morphio import (
MissingParentError,
RawDataError,
Expand Down Expand Up @@ -183,6 +184,64 @@ def test_load_morphology():
utils.load_morphology(StringIO(morphology_str), reader='swc')


def test_load_morphology__conversions():

morphology_str = u""" 1 1 0 0 0 1. -1
2 3 0 0 0 1. 1
3 3 0 5 0 1. 2
4 3 -5 5 0 0. 3
5 3 6 5 0 0. 3
6 2 0 0 0 1. 1
7 2 0 -4 0 1. 6
8 2 6 -4 0 0. 7
9 2 -5 -4 0 0. 7
"""
filepath = FILENAMES[0]
morphio_mut = morphio.mut.Morphology(filepath)
morphio_immut = morphio_mut.as_immutable()

# default readonly
morph = utils.load_morphology(filepath)
assert isinstance(morph.to_morphio(), morphio.Morphology)

# should be same with mutable=False
morph = utils.load_morphology(filepath, mutable=False)
assert isinstance(morph.to_morphio(), morphio.Morphology)

morph = utils.load_morphology(filepath, mutable=True)
assert isinstance(morph.to_morphio(), morphio.mut.Morphology)

# default mutable=None maintains mutability
morph = utils.load_morphology(morphio_mut)
assert isinstance(morph.to_morphio(), morphio.mut.Morphology)

morph = utils.load_morphology(morphio_mut, mutable=False)
assert isinstance(morph.to_morphio(), morphio.Morphology)

morph = utils.load_morphology(morphio_mut, mutable=True)
assert isinstance(morph.to_morphio(), morphio.mut.Morphology)

# default mutable=None maintains mutability
morph = utils.load_morphology(morphio_immut)
assert isinstance(morph.to_morphio(), morphio.Morphology)

morph = utils.load_morphology(morphio_immut, mutable=False)
assert isinstance(morph.to_morphio(), morphio.Morphology)

morph = utils.load_morphology(morphio_immut, mutable=True)
assert isinstance(morph.to_morphio(), morphio.mut.Morphology)

# default mutable=None is readaonly
morph = utils.load_morphology(morphology_str, reader="swc")
assert isinstance(morph.to_morphio(), morphio.Morphology)

morph = utils.load_morphology(morphology_str, mutable=False, reader="swc")
assert isinstance(morph.to_morphio(), morphio.Morphology)

morph = utils.load_morphology(morphology_str, mutable=True, reader="swc")
assert isinstance(morph.to_morphio(), morphio.mut.Morphology)


def test_morphology_name():
for fn, nn in zip(FILENAMES, NRN_NAMES):
m = utils.load_morphology(fn)
Expand Down

0 comments on commit 98b5326

Please sign in to comment.