Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
46c5da2
Merge branch 'development' of https://github.com/NeuroML/pyNeuroML in…
AdityaPandeyCN Jun 3, 2024
31bc91d
Merge branch 'development' of https://github.com/NeuroML/pyNeuroML in…
AdityaPandeyCN Jun 5, 2024
68385fa
added SWCnode class
AdityaPandeyCN Jun 5, 2024
703de88
Include type and parent attribute
AdityaPandeyCN Jun 5, 2024
ba883c3
added SWCgraph class
AdityaPandeyCN Jun 5, 2024
c62cdb5
restructured to SWCtree
AdityaPandeyCN Jun 5, 2024
c0ebcec
extended SWCTree class and added load_swc
AdityaPandeyCN Jun 5, 2024
5265094
removed unused imports
AdityaPandeyCN Jun 5, 2024
8093370
added docstrings
AdityaPandeyCN Jun 7, 2024
f389344
replaced with SWCGraph class
AdityaPandeyCN Jun 9, 2024
debf533
added validation check
AdityaPandeyCN Jun 9, 2024
8eb5a9a
added validation check
AdityaPandeyCN Jun 9, 2024
e36a532
added load_swc function
AdityaPandeyCN Jun 9, 2024
ac46824
added unit test file & TestSWCNode class
AdityaPandeyCN Jun 10, 2024
779f19c
removed unused errors
AdityaPandeyCN Jun 10, 2024
8b4bded
removed unused variable
AdityaPandeyCN Jun 10, 2024
e9a6742
added TestSWCGraph class
AdityaPandeyCN Jun 10, 2024
e5dbc88
added __init__.py file
AdityaPandeyCN Jun 10, 2024
b4d0b69
added test_get_graph function
AdityaPandeyCN Jun 10, 2024
01914db
added test_get_parent
AdityaPandeyCN Jun 10, 2024
8fa1b05
added more tests
AdityaPandeyCN Jun 11, 2024
ab9a8eb
replaced the test file for updated LoadSWC.py func
AdityaPandeyCN Jun 18, 2024
bbac284
updated the LoadSWC.py function
AdityaPandeyCN Jun 18, 2024
67f7319
Merge branch 'development' of https://github.com/NeuroML/pyNeuroML in…
AdityaPandeyCN Jun 18, 2024
d930f76
Merge branch 'development' into feat/SWCLoaderfile
AdityaPandeyCN Jun 18, 2024
046dbda
updated the docstring
AdityaPandeyCN Jun 18, 2024
c98a0f7
updated
AdityaPandeyCN Jun 18, 2024
bf31f28
removed get_descendants
AdityaPandeyCN Jun 18, 2024
6056b2f
removed test_get_descendants
AdityaPandeyCN Jun 18, 2024
b118eea
changed the doctrings
AdityaPandeyCN Jun 19, 2024
763ce34
removed unused imports
AdityaPandeyCN Jun 19, 2024
a1f1f76
changes making the function correct
AdityaPandeyCN Jun 20, 2024
374e029
changes making the function correct
AdityaPandeyCN Jun 20, 2024
3f4e9e4
added more test
AdityaPandeyCN Jun 20, 2024
f2225c7
added get_nodes_by_type test
AdityaPandeyCN Jun 20, 2024
c39b7e5
removed unused function
AdityaPandeyCN Jun 21, 2024
6d07eeb
changes
AdityaPandeyCN Jun 21, 2024
d9ccfd5
changes
AdityaPandeyCN Jun 21, 2024
877042c
added docstrings
AdityaPandeyCN Jun 25, 2024
b4df5d1
added docstrings in parse header function
AdityaPandeyCN Jun 25, 2024
35b820e
added docstrings and refactored the functions
AdityaPandeyCN Jun 25, 2024
416f0d5
changed the docstring format
AdityaPandeyCN Jun 25, 2024
4d934ea
changed the docstring format
AdityaPandeyCN Jun 26, 2024
aa291d8
changed the string represenation function
AdityaPandeyCN Jun 26, 2024
9641853
Merge branch 'development' into feat/SWCLoaderfile
sanjayankur31 Jun 26, 2024
5093b2a
Merge branch 'development' into feat/SWCLoaderfile
sanjayankur31 Jun 26, 2024
7d4705d
Merge branch 'development' into feat/SWCLoaderfile
sanjayankur31 Jul 3, 2024
f3381be
chore(loadswc): set logger level
sanjayankur31 Jul 3, 2024
d5e5e60
feat(swcgraph): add docstring
sanjayankur31 Jul 3, 2024
9316cb9
feat(swcloader): upgrade error log to a value error
sanjayankur31 Jul 3, 2024
9cc464a
feat(swcloader): inform user that soma node does not have parent
sanjayankur31 Jul 3, 2024
6baa176
feat(swcloade): do not update node when getting children
sanjayankur31 Jul 3, 2024
db6d0a2
feat(swcloader): tweak log messages
sanjayankur31 Jul 3, 2024
c7f2c11
feat(swcloader): add type hints, allow exceptions to error program
sanjayankur31 Jul 3, 2024
985fed9
docs(loadswc): update docstring, add to docs
sanjayankur31 Jul 3, 2024
41380e7
chore(exportswc): remove circular dependency while generating docs
sanjayankur31 Jul 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/source/pyneuroml.swc.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ pyneuroml.swc package
:undoc-members:
:show-inheritance:

pyneuroml.swc.LoadSWC module
------------------------------

.. automodule:: pyneuroml.swc.LoadSWC
:members:
:undoc-members:
:show-inheritance:

pyneuroml.swc.ExportSWC module
------------------------------

Expand Down
9 changes: 5 additions & 4 deletions pyneuroml/swc/ExportSWC.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
"""
A script to export SWC files from NeuroML <cell>s
A script to export SWC files from NeuroML <cell>s

"""

import logging
import os
import sys
import logging
from pyneuroml import pynml

from pyneuroml import __version__ as pynmlv
from pyneuroml.io import read_neuroml2_file

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -117,7 +118,7 @@ def convert_to_swc(nml_file_name, add_comments=False, target_dir=None):
if target_dir is None:
base_dir = os.path.dirname(os.path.realpath(nml_file_name))
target_dir = base_dir
nml_doc = pynml.read_neuroml2_file(
nml_doc = read_neuroml2_file(
nml_file_name, include_includes=True, verbose=False, optimized=True
)

Expand Down
336 changes: 336 additions & 0 deletions pyneuroml/swc/LoadSWC.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,336 @@
"""
Module for loading SWC files

.. versionadded:: 1.3.4

"""

import logging
import re
import typing

logging.basicConfig(level=logging.WARNING)
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)


class SWCNode:
"""
Represents a single node in an SWC (Standardized Morphology Data Format) file.

The SWC format is a widely used standard for representing neuronal morphology data.
It consists of a series of lines, each representing a single node or sample point
along the neuronal structure. For more information on the SWC format, see:
https://swc-specification.readthedocs.io/en/latest/swc.html

:param UNDEFINED: ID representing an undefined node type
:type UNDEFINED: int
:param SOMA: ID representing a soma node
:type SOMA: int
:param AXON: ID representing an axon node
:type AXON: int
:param BASAL_DENDRITE: ID representing a basal dendrite node
:type BASAL_DENDRITE: int
:param APICAL_DENDRITE: ID representing an apical dendrite node
:type APICAL_DENDRITE: int
:param CUSTOM: ID representing a custom node type
:type CUSTOM: int
:param UNSPECIFIED_NEURITE: ID representing an unspecified neurite node
:type UNSPECIFIED_NEURITE: int
:param GLIA_PROCESSES: ID representing a glia process node
:type GLIA_PROCESSES: int
:param TYPE_NAMES: A mapping of node type IDs to their string representations
:type TYPE_NAMES: dict
"""

UNDEFINED = 0
SOMA = 1
AXON = 2
BASAL_DENDRITE = 3
APICAL_DENDRITE = 4
CUSTOM = 5
UNSPECIFIED_NEURITE = 6
GLIA_PROCESSES = 7

TYPE_NAMES = {
UNDEFINED: "Undefined",
SOMA: "Soma",
AXON: "Axon",
BASAL_DENDRITE: "Basal Dendrite",
APICAL_DENDRITE: "Apical Dendrite",
CUSTOM: "Custom",
UNSPECIFIED_NEURITE: "Unspecified Neurite",
GLIA_PROCESSES: "Glia Processes",
}

def __init__(
self,
node_id: typing.Union[str, int],
type_id: typing.Union[str, int],
x: typing.Union[str, float],
y: typing.Union[str, float],
z: typing.Union[str, float],
radius: typing.Union[str, float],
parent_id: typing.Union[str, int],
):
try:
self.id = int(node_id)
self.type = int(type_id)
self.x = float(x)
self.y = float(y)
self.z = float(z)
self.radius = float(radius)
self.parent_id = int(parent_id)
self.children: typing.List[SWCNode] = []
except (ValueError, TypeError) as e:
raise ValueError(f"Invalid data types in SWC line: {e}")

def __str__(self) -> str:
"""
Returns a human-readable string representation of the node.

:return: A string representation of the node
:rtype: str
"""
type_name = self.TYPE_NAMES.get(self.type, f"Custom_{self.type}")
return f"Node ID: {self.id}, Type: {type_name}, Coordinates: ({self.x:.2f}, {self.y:.2f}, {self.z:.2f}), Radius: {self.radius:.2f}, Parent ID: {self.parent_id}"


class SWCGraph:
"""Graph data structure holding SWCNode objects"""

HEADER_FIELDS = [
"ORIGINAL_SOURCE",
"CREATURE",
"REGION",
"FIELD/LAYER",
"TYPE",
"CONTRIBUTOR",
"REFERENCE",
"RAW",
"EXTRAS",
"SOMA_AREA",
"SHRINKAGE_CORRECTION",
"VERSION_NUMBER",
"VERSION_DATE",
"SCALE",
]

def __init__(self) -> None:
self.nodes: typing.List[SWCNode] = []
self.root: typing.Optional[SWCNode] = None
self.metadata: typing.Dict[str, str] = {}

def add_node(self, node: SWCNode):
"""
Add a node to the SWC graph.

:param node: The node to be added
:type node: SWCNode
:raises ValueError: If a node with the same ID already exists in the graph or if multiple root nodes are detected
"""
if any(existing_node.id == node.id for existing_node in self.nodes):
logger.error(f"Duplicate node ID: {node.id}")
raise ValueError(f"Duplicate node ID: {node.id}")

if node.parent_id == -1:
if self.root is not None:
raise ValueError(
"Attempted to add multiple root nodes. Only one root node is allowed."
)
self.root = node
logger.debug(f"Root node set: {node}")
else:
parent = next((n for n in self.nodes if n.id == node.parent_id), None)
if parent:
parent.children.append(node)
logger.debug(f"Node {node.id} added as child to node {parent.id}")
else:
raise ValueError(
f"Parent node {node.parent_id} not found for node {node.id}"
)

self.nodes.append(node)
logger.debug(f"New node added: {node}")

def get_node(self, node_id: int) -> SWCNode:
"""
Get a node from the graph by its ID.

:param node_id: The ID of the node to retrieve
:type node_id: int
:return: The node with the specified ID
:rtype: SWCNode
:raises ValueError: If the specified node_id is not found in the SWC tree
"""
node = next((n for n in self.nodes if n.id == node_id), None)
if node is None:
raise ValueError(f"Node {node_id} not found in the SWC tree")
return node

def add_metadata(self, key: str, value: str):
"""
Add metadata to the SWC graph.

:param key: The key for the metadata
:type key: str
:param value: The value for the metadata
:type value: str
"""

if key in self.HEADER_FIELDS:
self.metadata[key] = value
logger.debug(f"Added metadata: {key}: {value}")
else:
logger.warning(f"Ignoring unrecognized header field: {key}: {value}")

def get_parent(self, node_id: int) -> typing.Optional[SWCNode]:
"""
Get the parent node of a given node in the SWC tree.

:param node_id: The ID of the node for which to retrieve the parent
:type node_id: int
:return: The parent node if the node has a parent, otherwise None
:rtype: SWCNode or None
:raises ValueError: If the specified node_id is not found in the SWC tree

"""

node = self.get_node(node_id)
if node.parent_id == -1:
logger.info("Root node given, does not have a parent. Returning None")
return None
return self.get_node(node.parent_id)

def get_children(self, node_id: int) -> typing.List[SWCNode]:
"""
Get a list of child nodes for a given node.

:param node_id: The ID of the node for which to get the children
:type node_id: int
:return: A list of SWCNode objects representing the children of the given node
:rtype: list
:raises ValueError: If the provided node_id is not found in the graph

"""

children = [node for node in self.nodes if node.parent_id == node_id]
return children

def get_nodes_with_multiple_children(
self, type_id: typing.Optional[int] = None
) -> typing.List[SWCNode]:
"""
Get nodes with multiple children, optionally filtered by type.

:param type_id: The type ID to filter nodes by (optional)
:type type_id: int or None
:return: A list of SWCNode objects that have multiple children and match the specified type (if provided)
:rtype: list
"""
nodes = []
for node in self.nodes:
children = self.get_children(node.id)
if len(children) > 1 and (type_id is None or node.type == type_id):
nodes.append(node)

if type_id is not None:
logger.debug(
f"Found {len(nodes)} nodes of type {type_id} with multiple children."
)
else:
logger.debug(f"Found {len(nodes)} nodes with multiple children.")

return nodes

def get_nodes_by_type(self, type_id: int) -> typing.List[SWCNode]:
"""
Get a list of nodes of a specific type.

:param type_id: The type ID of the nodes to retrieve
:type type_id: int
:return: A list of SWCNode objects that have the specified type ID
:rtype: list
"""
return [node for node in self.nodes if node.type == type_id]

def get_branch_points(
self, types: typing.Optional[typing.List[int]]
) -> typing.Union[typing.List[SWCNode], typing.Dict[int, typing.List[SWCNode]]]:
"""
Get all branch points (nodes with multiple children) of the given types.

:param types: One or more node type IDs to filter the branch points by
:type types: int
:return: if node types are given, a dictionary with keys as the node
type and lists of nodes as values; otherwise a list of all nodes
:rtype: list or dict
"""

if not types:
# If no types are specified, return all branch points
return self.get_nodes_with_multiple_children()
else:
branch_points = {}
for type_id in types:
branch_points[type_id] = self.get_nodes_with_multiple_children(type_id)
return branch_points


def parse_header(line: str) -> typing.Optional[typing.Tuple[str, str]]:
"""
Parse a header line from an SWC file.

:param line: A single line from the SWC file header
:type line: str
:return: A tuple containing the matched header field name and corresponding value (or None if no match)
:rtype: tuple

"""

for field in SWCGraph.HEADER_FIELDS:
match = re.match(rf"{field}\s+(.+)", line, re.IGNORECASE)
if match:
return field, match.group(1).strip()
else:
logger.warn(f"Line beginning with '#' does not match header format: {line}")
return None


def load_swc(filename: str) -> SWCGraph:
"""
Load an SWC file and create an SWCGraph object.

:param filename: The path to the SWC file to be loaded
:type filename: str
:return: An SWCGraph object representing the loaded SWC file
:rtype: SWCGraph
:raises ValueError: If a non header line with more than the required number
of fields is found
"""

tree = SWCGraph()
with open(filename, "r") as file:
for line_number, line in enumerate(file, 1):
line = line.strip()
if not line:
continue
if line.startswith("#"):
header = parse_header(line[1:].strip())
if header:
tree.add_metadata(header[0], header[1])
continue

parts = line.split()
if len(parts) != 7:
raise ValueError(
f"Line {line_number}: Invalid number of fields. Expected 7, got {len(parts)}. Skipping line: {line}"
)

# the add_node bit throws errors if things don't work out as
# expected
node_id, type_id, x, y, z, radius, parent_id = parts
node = SWCNode(node_id, type_id, x, y, z, radius, parent_id)
tree.add_node(node)

return tree
Empty file added tests/swc/__init__.py
Empty file.
Loading