Skip to content

Commit 31f10aa

Browse files
Merge pull request #404 from AdityaBITMESRA/feat/ExportSWC
Feat/export swc-Adds function to export SWC graph to a new SWC file
2 parents eadd188 + 8e8d696 commit 31f10aa

File tree

8 files changed

+159
-1
lines changed

8 files changed

+159
-1
lines changed

pyneuroml/swc/LoadSWC.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,24 @@ def get_branch_points(
276276
branch_points[type_id] = self.get_nodes_with_multiple_children(type_id)
277277
return branch_points
278278

279+
def export_to_swc_file(self, filename: str) -> None:
280+
"""
281+
Export the SWCGraph to a new SWC file.
282+
283+
:param filename: The path to the output SWC file
284+
:type filename: str
285+
"""
286+
with open(filename, "w") as file:
287+
# Write metadata
288+
for key, value in self.metadata.items():
289+
file.write(f"# {key} {value}\n")
290+
291+
# Write node data
292+
for node in sorted(self.nodes, key=lambda n: n.id):
293+
file.write(
294+
f"{node.id} {node.type} {node.x:.4f} {node.y:.4f} {node.z:.4f} {node.radius:.4f} {node.parent_id}\n"
295+
)
296+
279297

280298
def parse_header(line: str) -> typing.Optional[typing.Tuple[str, str]]:
281299
"""

tests/swc/Case1_new.swc

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Case 1: Single contour soma case
2+
# Fixed: 3 point soma centred at origin, length 20um, radius 10um
3+
4+
1 1 0 0 0 10 -1
5+
2 1 0 -10 0 10 1
6+
3 1 0 10 0 10 1
7+
8+
# Branching dendrite starting at "edge" of soma
9+
10+
4 3 10 0 0 2 1
11+
5 3 30 0 0 2 4
12+
6 3 40 10 0 2 5
13+
7 3 40 -10 0 2 5

tests/swc/Case2_new.swc

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Case 2: No soma case
2+
# NO CHANGE FROM ORIGINAL FORMAT
3+
4+
# Soma not reconstructed...
5+
# 3 axons emanate from the same point
6+
7+
1 2 0 0 0 2 -1
8+
2 2 20 0 0 2 1
9+
10+
3 2 0 20 0 2 1
11+
4 2 0 30 0 2 3
12+
13+
5 2 0 -20 0 2 1
14+
6 2 0 -30 0 2 5

tests/swc/Case3_new.swc

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Case 3: Multiple contours soma case
2+
# Fixed: 3 point soma centred at origin, length 20um, radius 10um
3+
4+
1 1 0 0 0 10 -1
5+
2 1 0 -10 0 10 1
6+
3 1 0 10 0 10 1
7+
8+
# Single dendrite starting at "edge" of soma
9+
10+
4 3 10 0 0 2 1
11+
5 3 20 0 0 2 4
12+
6 3 30 0 0 2 5

tests/swc/Case4_new.swc

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Case 4: Multiple cylinder soma case
2+
# NO CHANGE FROM ORIGINAL FORMAT
3+
4+
# 4 real soma points, i.e. 3 real segments starting at origin
5+
6+
1 1 0 0 0 5 -1
7+
2 1 0 5 0 10 1
8+
3 1 0 10 0 10 2
9+
4 1 0 15 0 5 3
10+
11+
# One dendrite starting at top of soma, one starting at the bottom, one from side
12+
13+
5 3 0 20 0 5 4
14+
6 3 0 30 0 5 5
15+
16+
7 3 0 -5 0 5 1
17+
8 3 0 -15 0 2.5 7
18+
19+
9 3 10 10 0 5 2
20+
10 3 20 10 0 5 9

tests/swc/Case5_new.swc

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Case 5: Spherical soma case
2+
# Fixed: 3 point soma centred at origin, length 20um, radius 10um
3+
4+
1 1 0 0 0 10 -1
5+
2 1 0 -10 0 10 1
6+
3 1 0 10 0 10 1
7+
8+
# 3 dendrites emanate from the soma, starting at a distance of 10um from the origin
9+
10+
4 3 10 0 0 2 1
11+
5 3 30 0 0 2 4
12+
13+
6 3 0 10 0 2 1
14+
7 3 0 30 0 2 6
15+
16+
8 3 0 -10 0 2 1
17+
9 3 0 -30 0 2 8

tests/swc/__init__.py

Whitespace-only changes.

tests/swc/test_LoadSWC.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
import os
12
import unittest
23

3-
from pyneuroml.swc.LoadSWC import SWCGraph, SWCNode
4+
from pyneuroml.swc.LoadSWC import SWCGraph, SWCNode, load_swc
45

56

67
class TestSWCNode(unittest.TestCase):
8+
"""Test cases for the SWCNode class."""
9+
710
def test_init(self):
11+
"""Test the initialization of an SWCNode object."""
812
node = SWCNode(1, 1, 0.0, 0.0, 0.0, 1.0, -1)
913
self.assertEqual(node.id, 1)
1014
self.assertEqual(node.type, 1)
@@ -15,12 +19,16 @@ def test_init(self):
1519
self.assertEqual(node.parent_id, -1)
1620

1721
def test_invalid_init(self):
22+
"""Test that initializing an SWCNode with invalid data raises a ValueError."""
1823
with self.assertRaises(ValueError):
1924
SWCNode("a", 1, 0.0, 0.0, 0.0, 1.0, -1)
2025

2126

2227
class TestSWCGraph(unittest.TestCase):
28+
"""Test cases for the SWCGraph class."""
29+
2330
def setUp(self):
31+
"""Set up a sample SWCGraph for testing."""
2432
self.tree = SWCGraph()
2533
self.node1 = SWCNode(1, 1, 0.0, 0.0, 0.0, 1.0, -1)
2634
self.node2 = SWCNode(2, 3, 1.0, 0.0, 0.0, 0.5, 1)
@@ -30,39 +38,95 @@ def setUp(self):
3038
self.tree.add_node(self.node3)
3139

3240
def test_duplicate_node(self):
41+
"""Test that adding a duplicate node raises a ValueError."""
3342
with self.assertRaises(ValueError):
3443
self.tree.add_node(SWCNode(1, 1, 0.0, 0.0, 0.0, 1.0, -1))
3544

3645
def test_add_metadata(self):
46+
"""Test adding valid metadata to the SWCGraph."""
3747
self.tree.add_metadata("ORIGINAL_SOURCE", "file.swc")
3848
self.assertEqual(self.tree.metadata["ORIGINAL_SOURCE"], "file.swc")
3949

4050
def test_invalid_metadata(self):
51+
"""Test that adding invalid metadata does not modify the metadata dictionary."""
4152
self.tree.add_metadata("INVALID_FIELD", "value")
4253
self.assertEqual(self.tree.metadata, {})
4354

4455
def test_get_parent(self):
56+
"""Test getting the parent node of a given node."""
4557
self.assertIsNone(self.tree.get_parent(self.node1.id))
4658
self.assertEqual(self.tree.get_parent(self.node2.id), self.node1)
4759
self.assertEqual(self.tree.get_parent(self.node3.id), self.node2)
4860
with self.assertRaises(ValueError):
4961
self.tree.get_parent(4)
5062

5163
def test_get_children(self):
64+
"""Test getting the children of a given node."""
5265
self.assertEqual(self.tree.get_children(self.node1.id), [self.node2])
5366
self.assertEqual(self.tree.get_children(self.node2.id), [self.node3])
5467
with self.assertRaises(ValueError):
5568
self.tree.get_parent(4)
5669

5770
def test_get_nodes_with_multiple_children(self):
71+
"""Test getting nodes with multiple children."""
5872
node4 = SWCNode(4, 3, 3.0, 0.0, 0.0, 0.5, 2)
5973
self.tree.add_node(node4)
6074
self.assertEqual(self.tree.get_nodes_with_multiple_children(), [self.node2])
6175

6276
def test_get_nodes_by_type(self):
77+
"""Test getting nodes by their type."""
6378
self.assertEqual(self.tree.get_nodes_by_type(1), [self.node1])
6479
self.assertEqual(self.tree.get_nodes_by_type(3), [self.node2, self.node3])
6580

6681

82+
class TestSWCExport(unittest.TestCase):
83+
"""Test cases for exporting SWC files."""
84+
85+
def setUp(self):
86+
"""Set up file paths for testing."""
87+
current_dir = os.path.dirname(os.path.abspath(__file__))
88+
self.input_file = os.path.join(current_dir, "Case1_new.swc")
89+
self.output_file = os.path.join(current_dir, "Case1_exported.swc")
90+
91+
def test_load_export_compare(self):
92+
"""Test loading an SWC file, exporting it, and comparing the results."""
93+
# Load the original file
94+
original_tree = load_swc(self.input_file)
95+
96+
# Export the loaded tree
97+
original_tree.export_to_swc_file(self.output_file)
98+
99+
# Check if the exported file was created
100+
self.assertTrue(os.path.exists(self.output_file))
101+
102+
# Load the exported file
103+
exported_tree = load_swc(self.output_file)
104+
105+
# Compare the original and exported trees
106+
self.assertEqual(len(original_tree.nodes), len(exported_tree.nodes))
107+
108+
# Compare a few key properties of the first and last nodes
109+
self.compare_nodes(original_tree.nodes[0], exported_tree.nodes[0])
110+
self.compare_nodes(original_tree.nodes[-1], exported_tree.nodes[-1])
111+
112+
# Compare metadata
113+
self.assertEqual(original_tree.metadata, exported_tree.metadata)
114+
115+
def compare_nodes(self, node1, node2):
116+
"""Compare two SWCNode objects for equality."""
117+
self.assertEqual(node1.id, node2.id)
118+
self.assertEqual(node1.type, node2.type)
119+
self.assertEqual(node1.parent_id, node2.parent_id)
120+
self.assertAlmostEqual(node1.x, node2.x, places=4)
121+
self.assertAlmostEqual(node1.y, node2.y, places=4)
122+
self.assertAlmostEqual(node1.z, node2.z, places=4)
123+
self.assertAlmostEqual(node1.radius, node2.radius, places=4)
124+
125+
def tearDown(self):
126+
"""Clean up by removing the exported file if it exists."""
127+
if os.path.exists(self.output_file):
128+
os.remove(self.output_file)
129+
130+
67131
if __name__ == "__main__":
68132
unittest.main()

0 commit comments

Comments
 (0)