Skip to content

Commit

Permalink
Add delete_point for morphology branch. (#751)
Browse files Browse the repository at this point in the history
* Add delete_point for morphology branch.
Add tests and update documentation.

* Simplify test_delete_point.

* Update bsb/morphologies/__init__.py

Co-authored-by: Robin De Schepper <robin.deschepper93@gmail.com>

* Fix test properties.

---------

Co-authored-by: Robin De Schepper <robin.deschepper93@gmail.com>
  • Loading branch information
drodarie and Helveg committed Oct 2, 2023
1 parent 577f060 commit 57492bb
Show file tree
Hide file tree
Showing 2 changed files with 64 additions and 6 deletions.
46 changes: 40 additions & 6 deletions bsb/morphologies/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@
from scipy.spatial.transform import Rotation
from .._encoding import EncodedLabels
from ..voxels import VoxelSet
from ..exceptions import MorphologyDataError, EmptyBranchError, MorphologyWarning
from ..exceptions import (
MorphologyError,
MorphologyDataError,
EmptyBranchError,
MorphologyWarning,
)
from ..reporting import warn
from .. import _util as _gutil

Expand Down Expand Up @@ -562,7 +567,7 @@ def root_rotate(self, rot, downstream_of=0):
:param downstream_of: index of the point in the subtree from which the rotation should be
applied. This feature works only when the subtree has only one root branch.
:returns: rotated Morphology
:rtype: bsb.morphologies.Morphology
:rtype: bsb.morphologies.SubTree
"""

if downstream_of != 0:
Expand All @@ -585,6 +590,13 @@ def root_rotate(self, rot, downstream_of=0):
return self

def translate(self, point):
"""
Translate the subtree by a 3D vector.
:param numpy.ndarray point: 3D vector to translate the subtree.
:returns: the translated subtree
:rtype: bsb.morphologies.SubTree
"""
if len(point) != 3:
raise ValueError("Point must be a sequence of x, y and z coordinates")
if self._is_shared:
Expand Down Expand Up @@ -618,7 +630,7 @@ def close_gaps(self):

def collapse(self, on=None):
"""
Collapse all of the roots of the morphology or subtree onto a single point.
Collapse all the roots of the morphology or subtree onto a single point.
:param on: Index of the root to collapse on. Collapses onto the origin by default.
:type on: int
Expand Down Expand Up @@ -1085,10 +1097,12 @@ def __init__(self, points, radii, labels=None, properties=None, children=None):
:type radii: list | numpy.ndarray
:param labels: Array of labels to associate to each point
:type labels: EncodedLabels | List[str] | set | numpy.ndarray
:param properties: dictionary of metadata to store in the branch
:param properties: dictionary of per-point data to store in the branch
:type properties: dict
:param children: list of child branches to attach to the branch
:type children: List[bsb.morphologies.Branch]
:raises bsb.exceptions.MorphologyError: if a property of the branch does not have the same
size as its points
"""

self._points = _gutil.sanitize_ndarray(points, (-1, 3), float)
Expand All @@ -1102,6 +1116,11 @@ def __init__(self, points, radii, labels=None, properties=None, children=None):
self._labels = labels
if properties is None:
properties = {}
mismatched = [str(k) for k, v in properties.items() if len(v) != len(points)]
if mismatched:
raise MorphologyError(
f"Morphology properties {', '.join(mismatched)} are not length {len(points)}"
)
self._properties = {
k: v if isinstance(v, np.ndarray) else np.array(v)
for k, v in properties.items()
Expand Down Expand Up @@ -1379,7 +1398,7 @@ def label(self, labels, points=None):
Add labels to the branch.
:param labels: Label(s) for the branch
:type labels: str
:type labels: List[str]
:param points: An integer or boolean mask to select the points to label.
"""
if points is None:
Expand Down Expand Up @@ -1492,7 +1511,7 @@ def walk(self):
self.points[:, 2],
self.radii,
self.labels.walk(),
*self.properties.values(),
*self._properties.values(),
)

def contains_labels(self, labels):
Expand Down Expand Up @@ -1647,6 +1666,21 @@ def get_axial_distances(self, idx_start=0, idx_end=-1, return_max=False):
else:
return displacements

def delete_point(self, index):
"""
Remove a point from the branch
:param int index: index position of the point to remove
:returns: the branch where the point has been removed
:rtype: bsb.morphologies.Branch
"""
self._points = np.delete(self._points, index, axis=0)
self._labels = np.delete(self._labels, index, axis=0)
self._radii = np.delete(self._radii, index, axis=0)
for k, v in self._properties.items():
self._properties[k] = np.delete(v, index, axis=0)
return self

def simplify(self, epsilon, idx_start=0, idx_end=-1):
"""
Apply Ramer–Douglas–Peucker algorithm to all points or a subset of points of the branch.
Expand Down
24 changes: 24 additions & 0 deletions tests/test_morphologies.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,18 @@ def test_properties(self):
self.assertTrue(branch.is_terminal)
branch.attach_child(branch)
self.assertFalse(branch.is_terminal)
with self.assertRaises(MorphologyError):
_ = Branch(
np.array(
[
[0, 1, 2],
[0, 1, 2],
[0, 1, 2],
]
),
np.array([0, 1, 2]),
properties={"a": np.array([0, 1])}, # not one value per point
)

def test_optimize(self):
b1 = _branch(3)
Expand Down Expand Up @@ -342,6 +354,18 @@ def test_adjacency(self):

self.assertEqual(m.adjacency_dictionary, target)

def test_delete_point(self):
points = np.arange(9).reshape(3, 3)
labels = set(["test1", "test2"])
branch = Branch(
points, np.array([0, 1, 2]), properties={"a": np.array([0, 1, 2])}
)
branch.label(labels, [0, 2])
branch.delete_point(0)
self.assertClose(branch.points, points[1:])
self.assertClose(branch.radii, np.array([1, 2]))
self.assertClose([0, 1], branch.labels)


class TestMorphologyLabels(NumpyTestCase, unittest.TestCase):
def test_labels(self):
Expand Down

0 comments on commit 57492bb

Please sign in to comment.