From f5f92d2ee349948e171310f3613dc087de7ef538 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Thu, 25 Apr 2024 14:28:05 +0200 Subject: [PATCH 1/4] Decouple Morphology constructor from io --- neurom/core/morphology.py | 9 ++---- neurom/io/utils.py | 29 ++++++++++++------- tests/core/test_neuron.py | 19 ------------- tests/io/test_io_utils.py | 59 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 81 insertions(+), 35 deletions(-) diff --git a/neurom/core/morphology.py b/neurom/core/morphology.py index 79b49c8f..fd99e3f3 100644 --- a/neurom/core/morphology.py +++ b/neurom/core/morphology.py @@ -538,18 +538,15 @@ 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 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) diff --git a/neurom/io/utils.py b/neurom/io/utils.py index 7b8f4edb..7ca85db7 100644 --- a/neurom/io/utils.py +++ b/neurom/io/utils.py @@ -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: @@ -157,15 +157,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( diff --git a/tests/core/test_neuron.py b/tests/core/test_neuron.py index fa52a14d..110aa0f0 100644 --- a/tests/core/test_neuron.py +++ b/tests/core/test_neuron.py @@ -65,9 +65,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) @@ -140,19 +137,3 @@ def test_str(): n = nm.load_morphology(SWC_PATH / 'simple.swc') assert 'Morphology' in str(n) 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) diff --git a/tests/io/test_io_utils.py b/tests/io/test_io_utils.py index c87ead5b..1a211223 100644 --- a/tests/io/test_io_utils.py +++ b/tests/io/test_io_utils.py @@ -34,6 +34,7 @@ from pathlib import Path import numpy as np +import morphio from morphio import ( MissingParentError, RawDataError, @@ -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) From 2132da8f97eedf0c7f6d8ad7b5fa20b2eb05a17d Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Thu, 25 Apr 2024 14:41:28 +0200 Subject: [PATCH 2/4] Add check --- neurom/core/morphology.py | 7 +++++++ tests/core/test_neuron.py | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/neurom/core/morphology.py b/neurom/core/morphology.py index fd99e3f3..d4f35ee8 100644 --- a/neurom/core/morphology.py +++ b/neurom/core/morphology.py @@ -42,6 +42,7 @@ from neurom.core.soma import make_soma from neurom.core.types import NeuriteIter, NeuriteType from neurom.utils import flatten +from neurom.exceptions import NeuroMError class Section: @@ -546,6 +547,12 @@ def __init__(self, morphio_morph, name=None, process_subtrees=False): name (str): an optional morphology name process_subtrees (bool): enable mixed tree processing if set to True """ + 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." + ) + self._morphio_morph = morphio_morph self.name = name if name else 'Morphology' diff --git a/tests/core/test_neuron.py b/tests/core/test_neuron.py index 110aa0f0..6fc9efe4 100644 --- a/tests/core/test_neuron.py +++ b/tests/core/test_neuron.py @@ -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/' @@ -137,3 +139,8 @@ def test_str(): n = nm.load_morphology(SWC_PATH / 'simple.swc') assert 'Morphology' in str(n) assert 'Section' in str(n.neurites[0].root_node) + + +def test_morphology_raises_wrong_argument(): + with pytest.raises(NeuroMError, match="Expected morphio Morphology object but got: my-path"): + Morphology("my-path") From abdd5b40efc26c503a162a2d26646c4699f7a42c Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Thu, 25 Apr 2024 14:53:08 +0200 Subject: [PATCH 3/4] Fix docstring & lint --- neurom/core/morphology.py | 3 +-- neurom/io/utils.py | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/neurom/core/morphology.py b/neurom/core/morphology.py index d4f35ee8..95183824 100644 --- a/neurom/core/morphology.py +++ b/neurom/core/morphology.py @@ -30,7 +30,6 @@ import warnings from collections import deque -from pathlib import Path import morphio import numpy as np @@ -41,8 +40,8 @@ from neurom.core.population import Population from neurom.core.soma import make_soma from neurom.core.types import NeuriteIter, NeuriteType -from neurom.utils import flatten from neurom.exceptions import NeuroMError +from neurom.utils import flatten class Section: diff --git a/neurom/io/utils.py b/neurom/io/utils.py index 7ca85db7..4e420c7a 100644 --- a/neurom/io/utils.py +++ b/neurom/io/utils.py @@ -132,6 +132,9 @@ def load_morphology(morph, reader=None, mutable=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) From a6cc523464c72703fd98ea986618feb843e59713 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Thu, 25 Apr 2024 15:15:34 +0200 Subject: [PATCH 4/4] Update CHANGELOG and docs --- CHANGELOG.rst | 1 + doc/source/migration.rst | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b0fdc2a1..fb7e675d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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) diff --git a/doc/source/migration.rst b/doc/source/migration.rst index e643a54a..ad681d77 100644 --- a/doc/source/migration.rst +++ b/doc/source/migration.rst @@ -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() @@ -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]