dendrotweaks 0.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dendrotweaks/__init__.py +10 -0
- dendrotweaks/analysis/__init__.py +11 -0
- dendrotweaks/analysis/ephys_analysis.py +482 -0
- dendrotweaks/analysis/morphometric_analysis.py +106 -0
- dendrotweaks/membrane/__init__.py +6 -0
- dendrotweaks/membrane/default_mod/AMPA.mod +65 -0
- dendrotweaks/membrane/default_mod/AMPA_NMDA.mod +100 -0
- dendrotweaks/membrane/default_mod/CaDyn.mod +54 -0
- dendrotweaks/membrane/default_mod/GABAa.mod +65 -0
- dendrotweaks/membrane/default_mod/Leak.mod +27 -0
- dendrotweaks/membrane/default_mod/NMDA.mod +72 -0
- dendrotweaks/membrane/default_mod/vecstim.mod +76 -0
- dendrotweaks/membrane/default_templates/NEURON_template.py +354 -0
- dendrotweaks/membrane/default_templates/default.py +73 -0
- dendrotweaks/membrane/default_templates/standard_channel.mod +87 -0
- dendrotweaks/membrane/default_templates/template_jaxley.py +108 -0
- dendrotweaks/membrane/default_templates/template_jaxley_new.py +108 -0
- dendrotweaks/membrane/distributions.py +324 -0
- dendrotweaks/membrane/groups.py +103 -0
- dendrotweaks/membrane/io/__init__.py +11 -0
- dendrotweaks/membrane/io/ast.py +201 -0
- dendrotweaks/membrane/io/code_generators.py +312 -0
- dendrotweaks/membrane/io/converter.py +108 -0
- dendrotweaks/membrane/io/factories.py +144 -0
- dendrotweaks/membrane/io/grammar.py +417 -0
- dendrotweaks/membrane/io/loader.py +90 -0
- dendrotweaks/membrane/io/parser.py +499 -0
- dendrotweaks/membrane/io/reader.py +212 -0
- dendrotweaks/membrane/mechanisms.py +574 -0
- dendrotweaks/model.py +1916 -0
- dendrotweaks/model_io.py +75 -0
- dendrotweaks/morphology/__init__.py +5 -0
- dendrotweaks/morphology/domains.py +100 -0
- dendrotweaks/morphology/io/__init__.py +5 -0
- dendrotweaks/morphology/io/factories.py +212 -0
- dendrotweaks/morphology/io/reader.py +66 -0
- dendrotweaks/morphology/io/validation.py +212 -0
- dendrotweaks/morphology/point_trees.py +681 -0
- dendrotweaks/morphology/reduce/__init__.py +16 -0
- dendrotweaks/morphology/reduce/reduce.py +155 -0
- dendrotweaks/morphology/reduce/reduced_cylinder.py +129 -0
- dendrotweaks/morphology/sec_trees.py +1112 -0
- dendrotweaks/morphology/seg_trees.py +157 -0
- dendrotweaks/morphology/trees.py +567 -0
- dendrotweaks/path_manager.py +261 -0
- dendrotweaks/simulators.py +235 -0
- dendrotweaks/stimuli/__init__.py +3 -0
- dendrotweaks/stimuli/iclamps.py +73 -0
- dendrotweaks/stimuli/populations.py +265 -0
- dendrotweaks/stimuli/synapses.py +203 -0
- dendrotweaks/utils.py +239 -0
- dendrotweaks-0.3.1.dist-info/METADATA +70 -0
- dendrotweaks-0.3.1.dist-info/RECORD +56 -0
- dendrotweaks-0.3.1.dist-info/WHEEL +5 -0
- dendrotweaks-0.3.1.dist-info/licenses/LICENSE +674 -0
- dendrotweaks-0.3.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,157 @@
|
|
1
|
+
from dendrotweaks.morphology.trees import Node, Tree
|
2
|
+
import numpy as np
|
3
|
+
|
4
|
+
class Segment(Node):
|
5
|
+
"""
|
6
|
+
A class representing a segment.
|
7
|
+
|
8
|
+
A segment is a part of a section used to discretize the section
|
9
|
+
in space for the purpose of numerical simulations.
|
10
|
+
|
11
|
+
Parameters
|
12
|
+
----------
|
13
|
+
idx : int
|
14
|
+
The index of the segment.
|
15
|
+
parent_idx : int
|
16
|
+
The index of the parent segment.
|
17
|
+
neuron_seg : h.Segment
|
18
|
+
The NEURON segment.
|
19
|
+
section : Section
|
20
|
+
The section to which the segment belongs.
|
21
|
+
|
22
|
+
Attributes
|
23
|
+
----------
|
24
|
+
_section : Section
|
25
|
+
The section to which the segment belongs.
|
26
|
+
_ref : h.Segment
|
27
|
+
The NEURON segment.
|
28
|
+
"""
|
29
|
+
|
30
|
+
def __init__(self, idx, parent_idx, neuron_seg, section) -> None:
|
31
|
+
super().__init__(idx, parent_idx)
|
32
|
+
self._section = section
|
33
|
+
self._ref = neuron_seg
|
34
|
+
|
35
|
+
|
36
|
+
# PROPERTIES
|
37
|
+
|
38
|
+
@property
|
39
|
+
def domain(self):
|
40
|
+
"""
|
41
|
+
The morphological or functional domain of the segment.
|
42
|
+
"""
|
43
|
+
return self._section.domain
|
44
|
+
|
45
|
+
|
46
|
+
@property
|
47
|
+
def x(self):
|
48
|
+
"""
|
49
|
+
The position of the segment along the normalized section length (from NEURON).
|
50
|
+
"""
|
51
|
+
return self._ref.x
|
52
|
+
|
53
|
+
|
54
|
+
@property
|
55
|
+
def area(self):
|
56
|
+
"""
|
57
|
+
The area of the segment (from NEURON).
|
58
|
+
"""
|
59
|
+
return self._ref.area()
|
60
|
+
|
61
|
+
|
62
|
+
@property
|
63
|
+
def diam(self):
|
64
|
+
"""
|
65
|
+
The diameter of the segment (from NEURON).
|
66
|
+
"""
|
67
|
+
return self._ref.diam
|
68
|
+
|
69
|
+
|
70
|
+
@property
|
71
|
+
def subtree_size(self):
|
72
|
+
"""
|
73
|
+
The number of sections in the subtree rooted at the segment.
|
74
|
+
"""
|
75
|
+
return self._section.subtree_size
|
76
|
+
|
77
|
+
|
78
|
+
@property
|
79
|
+
def Ra(self):
|
80
|
+
"""
|
81
|
+
The axial resistance of the segment (from NEURON).
|
82
|
+
"""
|
83
|
+
return self._section.Ra
|
84
|
+
|
85
|
+
|
86
|
+
def path_distance(self, within_domain=False):
|
87
|
+
return self._section.path_distance(self.x,
|
88
|
+
within_domain=within_domain)
|
89
|
+
|
90
|
+
|
91
|
+
@property
|
92
|
+
def distance(self):
|
93
|
+
return self.path_distance(within_domain=False)
|
94
|
+
|
95
|
+
|
96
|
+
@property
|
97
|
+
def domain_distance(self):
|
98
|
+
return self.path_distance(within_domain=True)
|
99
|
+
|
100
|
+
|
101
|
+
# PARAMETER SETTERS AND GETTERS
|
102
|
+
|
103
|
+
def set_param_value(self, param_name, value):
|
104
|
+
"""
|
105
|
+
Set the value of a parameter of the segment.
|
106
|
+
|
107
|
+
Parameters
|
108
|
+
----------
|
109
|
+
param_name : str
|
110
|
+
The name of the parameter to set.
|
111
|
+
value : float
|
112
|
+
The value to set the parameter to.
|
113
|
+
"""
|
114
|
+
if hasattr(self._ref, param_name):
|
115
|
+
setattr(self._ref, param_name, value)
|
116
|
+
|
117
|
+
|
118
|
+
def get_param_value(self, param_name):
|
119
|
+
"""
|
120
|
+
Get the value of a parameter of the segment.
|
121
|
+
|
122
|
+
Parameters
|
123
|
+
----------
|
124
|
+
param_name : str
|
125
|
+
The name of the parameter to get.
|
126
|
+
|
127
|
+
Returns
|
128
|
+
-------
|
129
|
+
float
|
130
|
+
The value of the parameter.
|
131
|
+
"""
|
132
|
+
if hasattr(self, param_name):
|
133
|
+
return getattr(self, param_name)
|
134
|
+
elif hasattr(self._ref, param_name):
|
135
|
+
return getattr(self._ref, param_name)
|
136
|
+
else:
|
137
|
+
return np.nan
|
138
|
+
|
139
|
+
|
140
|
+
class SegmentTree(Tree):
|
141
|
+
"""
|
142
|
+
A class representing a tree graph of segments.
|
143
|
+
"""
|
144
|
+
|
145
|
+
def __init__(self, segments: list[Segment]) -> None:
|
146
|
+
super().__init__(segments)
|
147
|
+
|
148
|
+
def __repr__(self):
|
149
|
+
return f"SegmentTree(root={self.root!r}, num_nodes={len(self._nodes)})"
|
150
|
+
|
151
|
+
|
152
|
+
@property
|
153
|
+
def segments(self):
|
154
|
+
"""
|
155
|
+
The segments in the segment tree. Alias for self._nodes
|
156
|
+
"""
|
157
|
+
return self._nodes
|
@@ -0,0 +1,567 @@
|
|
1
|
+
from collections import defaultdict
|
2
|
+
import matplotlib.pyplot as plt
|
3
|
+
import numpy as np
|
4
|
+
import pandas as pd
|
5
|
+
|
6
|
+
from dendrotweaks.utils import timeit
|
7
|
+
from typing import Union
|
8
|
+
|
9
|
+
class Node():
|
10
|
+
"""
|
11
|
+
Represents a node in a tree.
|
12
|
+
|
13
|
+
A node can be a 3D point in a neuron morphology,
|
14
|
+
a segment, a section, or even a tree.
|
15
|
+
|
16
|
+
Parameters
|
17
|
+
----------
|
18
|
+
idx : Union[int, str]
|
19
|
+
The index of the node.
|
20
|
+
parent_idx : Union[int, str]
|
21
|
+
The index of the parent node.
|
22
|
+
|
23
|
+
Examples:
|
24
|
+
>>> node = Node(0, -1)
|
25
|
+
>>> node
|
26
|
+
•0
|
27
|
+
"""
|
28
|
+
|
29
|
+
def __init__(self, idx: Union[int, str], parent_idx: Union[int, str]) -> None:
|
30
|
+
"""
|
31
|
+
Creates a node in a tree.
|
32
|
+
|
33
|
+
Args:
|
34
|
+
idx (Union[int, str]): The index of the node.
|
35
|
+
parent_idx (Union[int, str]): The index of the parent node.
|
36
|
+
"""
|
37
|
+
self.idx = int(idx)
|
38
|
+
self.parent_idx = int(parent_idx)
|
39
|
+
self._parent = None
|
40
|
+
self.children = []
|
41
|
+
self._tree = None
|
42
|
+
|
43
|
+
def __repr__(self):
|
44
|
+
class_name = type(self).__name__
|
45
|
+
return f"{class_name}(idx={self.idx!r})"
|
46
|
+
|
47
|
+
@property
|
48
|
+
def parent(self):
|
49
|
+
return self._parent
|
50
|
+
|
51
|
+
@parent.setter
|
52
|
+
def parent(self, parent):
|
53
|
+
self._parent = parent
|
54
|
+
self.parent_idx = parent.idx if parent else -1
|
55
|
+
|
56
|
+
|
57
|
+
@property
|
58
|
+
def topological_type(self) -> str:
|
59
|
+
"""The topological type of the node based on the number of children.
|
60
|
+
|
61
|
+
Returns:
|
62
|
+
str: The topological type of the node: 'continuation', 'bifurcation', or 'termination'.
|
63
|
+
"""
|
64
|
+
types = {0: 'termination', 1: 'continuation'}
|
65
|
+
return types.get(len(self.children), 'bifurcation')
|
66
|
+
|
67
|
+
# @property
|
68
|
+
# def subtree(self) -> list:
|
69
|
+
# """
|
70
|
+
# Gets the subtree of the node (including the node itself).
|
71
|
+
|
72
|
+
# Returns:
|
73
|
+
# list: A list of nodes in the subtree.
|
74
|
+
# """
|
75
|
+
# subtree = [self]
|
76
|
+
# for child in self.children:
|
77
|
+
# subtree += child.subtree
|
78
|
+
# return subtree
|
79
|
+
|
80
|
+
@property
|
81
|
+
def subtree(self) -> list:
|
82
|
+
"""
|
83
|
+
Gets the subtree of the node (including the node itself) using
|
84
|
+
an iterative depth-first traversal.
|
85
|
+
|
86
|
+
Returns:
|
87
|
+
list: A list of nodes in the subtree.
|
88
|
+
"""
|
89
|
+
subtree = []
|
90
|
+
stack = [self] # Start from the current node
|
91
|
+
|
92
|
+
while stack:
|
93
|
+
node = stack.pop()
|
94
|
+
subtree.append(node)
|
95
|
+
stack.extend(node.children) # Push children to stack
|
96
|
+
|
97
|
+
return subtree
|
98
|
+
|
99
|
+
@property
|
100
|
+
def subtree_size(self):
|
101
|
+
"""
|
102
|
+
Gets the size of the subtree of the node.
|
103
|
+
|
104
|
+
Returns:
|
105
|
+
int: The size of the subtree of the node.
|
106
|
+
"""
|
107
|
+
return len(self.subtree)
|
108
|
+
|
109
|
+
@property
|
110
|
+
def depth(self):
|
111
|
+
"""
|
112
|
+
Computes the depth of the node in the tree iteratively.
|
113
|
+
"""
|
114
|
+
depth = 0
|
115
|
+
node = self
|
116
|
+
while node.parent: # Traverse up to the root
|
117
|
+
depth += 1
|
118
|
+
node = node.parent
|
119
|
+
return depth
|
120
|
+
|
121
|
+
|
122
|
+
@property
|
123
|
+
def siblings(self):
|
124
|
+
"""
|
125
|
+
Gets the siblings of the node.
|
126
|
+
|
127
|
+
Returns:
|
128
|
+
list: A list of nodes that share the same parent as the node.
|
129
|
+
"""
|
130
|
+
if self.parent is None:
|
131
|
+
return []
|
132
|
+
return [child for child in self.parent.children if child is not self]
|
133
|
+
|
134
|
+
@property
|
135
|
+
def nearest_neighbours(self):
|
136
|
+
"""
|
137
|
+
Gets the nearest neighbours of the node.
|
138
|
+
|
139
|
+
Returns:
|
140
|
+
list: A list of nodes that share the same parent or children as the node.
|
141
|
+
"""
|
142
|
+
return [self.parent] + self.children
|
143
|
+
|
144
|
+
def connect_to_parent(self, parent):
|
145
|
+
"""
|
146
|
+
Attach the node to a parent node.
|
147
|
+
|
148
|
+
Warning
|
149
|
+
-------
|
150
|
+
This method should not be used directly when working with trees
|
151
|
+
as it doesn't add the node to the tree's list of nodes.
|
152
|
+
Use the `Tree` class to insert nodes into the tree.
|
153
|
+
|
154
|
+
Args:
|
155
|
+
parent (Node): The parent node to attach the node to.
|
156
|
+
"""
|
157
|
+
if parent in self.subtree:
|
158
|
+
raise ValueError(f'Attaching a node will create a loop in the tree: {self} -> {parent}')
|
159
|
+
self.parent = parent
|
160
|
+
if self not in parent.children:
|
161
|
+
parent.children.append(self)
|
162
|
+
# parent.childrensorted(parent.children + [node], key=lambda x: x.idx)
|
163
|
+
|
164
|
+
def disconnect_from_parent(self):
|
165
|
+
"""
|
166
|
+
Detach the node from its parent.
|
167
|
+
|
168
|
+
Examples
|
169
|
+
--------
|
170
|
+
for child in node.children: child.disconnect_from_parent()
|
171
|
+
|
172
|
+
"""
|
173
|
+
if self.parent:
|
174
|
+
self.parent.children.remove(self)
|
175
|
+
self.parent = None
|
176
|
+
|
177
|
+
class Tree:
|
178
|
+
"""
|
179
|
+
A class to represent a tree data structure.
|
180
|
+
|
181
|
+
A tree graph is a hierarchical data structure that consists of nodes connected by edges.
|
182
|
+
|
183
|
+
Parameters
|
184
|
+
----------
|
185
|
+
nodes : list
|
186
|
+
A list of nodes in the tree.
|
187
|
+
|
188
|
+
Attributes
|
189
|
+
----------
|
190
|
+
root : Node
|
191
|
+
The root node of the tree.
|
192
|
+
"""
|
193
|
+
|
194
|
+
def __init__(self, nodes: list) -> None:
|
195
|
+
|
196
|
+
for node in nodes: node._tree = self
|
197
|
+
self._nodes = nodes
|
198
|
+
self.root = self._find_root()
|
199
|
+
|
200
|
+
if not self.is_connected:
|
201
|
+
self._connect_nodes()
|
202
|
+
|
203
|
+
# MAGIC METHODS
|
204
|
+
|
205
|
+
def __repr__(self):
|
206
|
+
return f"Tree(root={self.root!r}, num_nodes={len(self._nodes)})"
|
207
|
+
|
208
|
+
def __getitem__(self, idx):
|
209
|
+
return self._nodes[idx]
|
210
|
+
|
211
|
+
def __len__(self):
|
212
|
+
return len(self._nodes)
|
213
|
+
|
214
|
+
def __iter__(self):
|
215
|
+
for node in self._nodes:
|
216
|
+
yield node
|
217
|
+
|
218
|
+
def __contains__(self, node):
|
219
|
+
return node in self._nodes
|
220
|
+
|
221
|
+
# PROPERTIES
|
222
|
+
|
223
|
+
@property
|
224
|
+
def is_connected(self):
|
225
|
+
"""
|
226
|
+
Whether the tree is connected i.e. each node can be reached from the root.
|
227
|
+
|
228
|
+
Returns
|
229
|
+
-------
|
230
|
+
bool
|
231
|
+
True if the root node's subtree contains exactly the same nodes
|
232
|
+
as the entire tree. False otherwise.
|
233
|
+
"""
|
234
|
+
nodes_set = set(self._nodes)
|
235
|
+
subtree_set = set(self.root.subtree)
|
236
|
+
return nodes_set == subtree_set
|
237
|
+
|
238
|
+
@property
|
239
|
+
def is_sorted(self):
|
240
|
+
"""
|
241
|
+
Whether the nodes in the tree are sorted by index.
|
242
|
+
|
243
|
+
Returns
|
244
|
+
-------
|
245
|
+
bool
|
246
|
+
True if the nodes are sorted by index. False otherwise.
|
247
|
+
"""
|
248
|
+
if not all([node.idx == i for i, node in enumerate(self._nodes, start=0)]):
|
249
|
+
return False
|
250
|
+
traversal_indices = [node.idx for node in self.traverse()]
|
251
|
+
return traversal_indices == sorted(traversal_indices)
|
252
|
+
|
253
|
+
@property
|
254
|
+
def bifurcations(self):
|
255
|
+
"""
|
256
|
+
The bifurcation nodes in the tree.
|
257
|
+
|
258
|
+
Returns
|
259
|
+
-------
|
260
|
+
list
|
261
|
+
A list of bifurcation nodes in the tree.
|
262
|
+
"""
|
263
|
+
return [node for node in self._nodes if len(node.children) > 1]
|
264
|
+
|
265
|
+
@property
|
266
|
+
def terminations(self):
|
267
|
+
return [node for node in self._nodes if len(node.children) == 0]
|
268
|
+
|
269
|
+
@property
|
270
|
+
def edges(self) -> list:
|
271
|
+
"""
|
272
|
+
Returns a list of edges in the tree.
|
273
|
+
|
274
|
+
Returns:
|
275
|
+
list[tuple[Node, Node]]: A list of edges in the tree.
|
276
|
+
"""
|
277
|
+
edges = []
|
278
|
+
for node in self._nodes:
|
279
|
+
if node.parent is not None:
|
280
|
+
edges.append((node.parent, node))
|
281
|
+
return edges
|
282
|
+
|
283
|
+
# TREE CONSTRUCTION METHODS
|
284
|
+
|
285
|
+
def _find_root(self):
|
286
|
+
"""
|
287
|
+
Find the root node.
|
288
|
+
|
289
|
+
Returns
|
290
|
+
-------
|
291
|
+
Node
|
292
|
+
The root node of the tree.
|
293
|
+
"""
|
294
|
+
ROOT_PARENT = {None, -1, '-1'}
|
295
|
+
root_nodes = [node for node in self._nodes if node.parent_idx in ROOT_PARENT]
|
296
|
+
|
297
|
+
if len(root_nodes) != 1:
|
298
|
+
print('Root nodes:', root_nodes)
|
299
|
+
raise ValueError(f'Tree must have exactly one root node. Found: {root_nodes}')
|
300
|
+
|
301
|
+
return root_nodes[0]
|
302
|
+
|
303
|
+
def _connect_nodes(self):
|
304
|
+
"""
|
305
|
+
Efficiently builds the hierarchical tree structure for the nodes
|
306
|
+
using a dictionary for fast parent lookups.
|
307
|
+
"""
|
308
|
+
if self.is_connected:
|
309
|
+
print(' Tree already connected.')
|
310
|
+
return
|
311
|
+
|
312
|
+
# Step 1: Create a dictionary for O(1) lookups
|
313
|
+
node_map = {node.idx: node for node in self._nodes}
|
314
|
+
|
315
|
+
# Step 2: Assign parent-child relationships in O(N) time
|
316
|
+
for node in self._nodes:
|
317
|
+
if node is not self.root and node.parent_idx in node_map:
|
318
|
+
node.connect_to_parent(node_map[node.parent_idx])
|
319
|
+
|
320
|
+
# Step 3: Ensure tree is fully connected
|
321
|
+
if not self.is_connected:
|
322
|
+
raise ValueError('Tree is not connected.')
|
323
|
+
|
324
|
+
|
325
|
+
# TRAVERSAL METHODS
|
326
|
+
|
327
|
+
def traverse(self, root=None):
|
328
|
+
"""
|
329
|
+
Iterate over the nodes in the tree using a stack-based
|
330
|
+
depth-first traversal.
|
331
|
+
|
332
|
+
Parameters
|
333
|
+
----------
|
334
|
+
root : Node, optional
|
335
|
+
The root node to start the traversal from. Defaults to None.
|
336
|
+
"""
|
337
|
+
root = root or self.root
|
338
|
+
stack = [root]
|
339
|
+
visited = set()
|
340
|
+
|
341
|
+
while stack:
|
342
|
+
node = stack.pop()
|
343
|
+
if node in visited:
|
344
|
+
continue
|
345
|
+
|
346
|
+
yield node
|
347
|
+
visited.add(node)
|
348
|
+
for child in reversed(node.children):
|
349
|
+
stack.append(child)
|
350
|
+
|
351
|
+
|
352
|
+
|
353
|
+
# SORTIONG METHODS
|
354
|
+
|
355
|
+
def _sort_children(self):
|
356
|
+
"""
|
357
|
+
Iterate through all nodes in the tree and sort their children based on
|
358
|
+
the number of bifurcations (nodes with more than one child) in each child's
|
359
|
+
subtree. Nodes with fewer bifurcations in their subtrees are placed earlier in the list
|
360
|
+
of the node's children, ensuring that the shortest paths are traversed first.
|
361
|
+
"""
|
362
|
+
|
363
|
+
# subtree_size_map = {node: len(self.get_subtree(node)) for node in self._nodes}
|
364
|
+
|
365
|
+
for node in self._nodes:
|
366
|
+
node.children = sorted(
|
367
|
+
node.children,
|
368
|
+
key=lambda x: sum(1 for n in x.subtree if len(n.children) > 1),
|
369
|
+
reverse=False
|
370
|
+
)
|
371
|
+
|
372
|
+
# @timeit
|
373
|
+
def sort(self, sort_children=True, force=False):
|
374
|
+
"""
|
375
|
+
Sort the nodes in the tree using a stack-based depth-first traversal.
|
376
|
+
|
377
|
+
Parameters
|
378
|
+
----------
|
379
|
+
sort_children : bool, optional
|
380
|
+
Whether to sort the children of each node
|
381
|
+
based on the number of bifurcations in their subtrees. Defaults to True.
|
382
|
+
force : bool, optional
|
383
|
+
Whether to force the sorting of the tree even if it is already sorted. Defaults to False.
|
384
|
+
"""
|
385
|
+
if self.is_sorted and not force:
|
386
|
+
return
|
387
|
+
|
388
|
+
if sort_children:
|
389
|
+
self._sort_children()
|
390
|
+
|
391
|
+
count = 0
|
392
|
+
for node in self.traverse():
|
393
|
+
node.idx = count
|
394
|
+
node.parent_idx = node.parent.idx if node.parent else -1
|
395
|
+
count += 1
|
396
|
+
|
397
|
+
self._nodes = sorted(self._nodes, key=lambda x: x.idx)
|
398
|
+
|
399
|
+
if not self.is_sorted:
|
400
|
+
raise ValueError('Tree is not sorted.')
|
401
|
+
print(f'Sorted {self}.')
|
402
|
+
|
403
|
+
# INSERTION AND REMOVAL METHODS
|
404
|
+
|
405
|
+
def remove_node(self, node):
|
406
|
+
"""
|
407
|
+
Remove a node from the tree.
|
408
|
+
|
409
|
+
Parameters
|
410
|
+
----------
|
411
|
+
node : Node
|
412
|
+
The node to remove.
|
413
|
+
|
414
|
+
Raises
|
415
|
+
------
|
416
|
+
ValueError
|
417
|
+
If the tree is not sorted.
|
418
|
+
"""
|
419
|
+
if node.parent is None:
|
420
|
+
raise ValueError('Cannot remove the root node.')
|
421
|
+
parent = node.parent
|
422
|
+
children = node.children[:]
|
423
|
+
for child in children:
|
424
|
+
child.disconnect_from_parent()
|
425
|
+
child.connect_to_parent(parent)
|
426
|
+
node.disconnect_from_parent()
|
427
|
+
self._nodes.remove(node)
|
428
|
+
|
429
|
+
|
430
|
+
def remove_subtree(self, node):
|
431
|
+
"""
|
432
|
+
Remove a subtree from the tree.
|
433
|
+
|
434
|
+
Parameters
|
435
|
+
----------
|
436
|
+
node : Node
|
437
|
+
The root node of the subtree to remove.
|
438
|
+
"""
|
439
|
+
node.disconnect_from_parent()
|
440
|
+
for n in node.subtree:
|
441
|
+
self._nodes.remove(n)
|
442
|
+
|
443
|
+
|
444
|
+
def add_subtree(self, node, parent):
|
445
|
+
"""
|
446
|
+
Add a subtree to the tree.
|
447
|
+
|
448
|
+
Parameters
|
449
|
+
----------
|
450
|
+
node : Node
|
451
|
+
The root node of the subtree to add.
|
452
|
+
parent : Node
|
453
|
+
The parent node to attach the subtree to.
|
454
|
+
"""
|
455
|
+
node.connect_to_parent(parent)
|
456
|
+
self._nodes.extend(node.subtree)
|
457
|
+
|
458
|
+
|
459
|
+
def insert_node_after(self, new_node, existing_node):
|
460
|
+
"""
|
461
|
+
Insert a node after a given node in the tree.
|
462
|
+
|
463
|
+
Parameters
|
464
|
+
----------
|
465
|
+
new_node : Node
|
466
|
+
The new node to insert.
|
467
|
+
existing_node : Node
|
468
|
+
The existing node after which to insert the new node.
|
469
|
+
"""
|
470
|
+
if new_node in self._nodes:
|
471
|
+
raise ValueError('Node already exists in the tree.')
|
472
|
+
|
473
|
+
for child in existing_node.children:
|
474
|
+
child.disconnect_from_parent()
|
475
|
+
child.connect_to_parent(new_node)
|
476
|
+
new_node.connect_to_parent(existing_node)
|
477
|
+
|
478
|
+
self._nodes.append(new_node)
|
479
|
+
|
480
|
+
|
481
|
+
def insert_node_before(self, new_node, existing_node):
|
482
|
+
"""
|
483
|
+
Insert a node before a given node in the tree.
|
484
|
+
|
485
|
+
Parameters
|
486
|
+
----------
|
487
|
+
new_node : Node
|
488
|
+
The new node to insert.
|
489
|
+
existing_node : Node
|
490
|
+
The existing node before which to insert the new node.
|
491
|
+
"""
|
492
|
+
if new_node in self._nodes:
|
493
|
+
raise ValueError('Node already exists in the tree.')
|
494
|
+
new_node.connect_to_parent(existing_node.parent)
|
495
|
+
existing_node.disconnect_from_parent()
|
496
|
+
existing_node.connect_to_parent(new_node)
|
497
|
+
|
498
|
+
self._nodes.append(new_node)
|
499
|
+
|
500
|
+
|
501
|
+
def reposition_subtree(self, node, new_parent_node, origin=None):
|
502
|
+
"""
|
503
|
+
Reposition a subtree in the tree.
|
504
|
+
|
505
|
+
Parameters
|
506
|
+
----------
|
507
|
+
node : Node
|
508
|
+
The root node of the subtree to reposition.
|
509
|
+
new_parent_node : Node
|
510
|
+
The new parent node of the subtree.
|
511
|
+
origin : Node, optional
|
512
|
+
The origin node to use as the reference point for the repositioning.
|
513
|
+
Defaults to None.
|
514
|
+
|
515
|
+
Note
|
516
|
+
-----
|
517
|
+
Treats differently the children of the root node.
|
518
|
+
"""
|
519
|
+
if node.parent is None:
|
520
|
+
raise ValueError('Cannot reposition the root node.')
|
521
|
+
origin = origin or node.parent
|
522
|
+
self.remove_subtree(node)
|
523
|
+
shift_coordinates(node.subtree,
|
524
|
+
origin=origin,
|
525
|
+
target=new_parent_node)
|
526
|
+
self.add_subtree(node, new_parent_node)
|
527
|
+
|
528
|
+
|
529
|
+
# VISUALIZATION METHODS
|
530
|
+
|
531
|
+
|
532
|
+
def topology(self):
|
533
|
+
"""
|
534
|
+
Print the topology of the tree with a visual tree structure.
|
535
|
+
"""
|
536
|
+
def print_node(node, prefix="", is_last=True):
|
537
|
+
"""Recursive function to print the node with branches."""
|
538
|
+
# Print the current
|
539
|
+
root_str = f"{node.parent_idx:6} | "
|
540
|
+
prefix = root_str + prefix
|
541
|
+
print(prefix + '•' + str(node.idx))
|
542
|
+
|
543
|
+
# Handle the children nodes
|
544
|
+
num_children = len(node.children)
|
545
|
+
for i, child in enumerate(node.children):
|
546
|
+
is_last_child = (i == num_children - 1)
|
547
|
+
branch = "└─" if is_last_child else "├─"
|
548
|
+
prefix = prefix.replace("└─", " ").replace("├─", "│ ")
|
549
|
+
prefix = prefix.replace(root_str, "")
|
550
|
+
print_node(child, prefix + branch, is_last_child)
|
551
|
+
|
552
|
+
print('parent | idx')
|
553
|
+
print('-'*15)
|
554
|
+
print_node(self.root)
|
555
|
+
|
556
|
+
|
557
|
+
|
558
|
+
def shift_coordinates(points, origin, target):
|
559
|
+
"""
|
560
|
+
Shift the coordinates of a list of points from an origin to a target.
|
561
|
+
"""
|
562
|
+
origin_vector = (origin.x, origin.y, origin.z)
|
563
|
+
target_vector = (target.x, target.y, target.z)
|
564
|
+
for pt in points:
|
565
|
+
pt.x = pt.x - origin_vector[0] + target_vector[0]
|
566
|
+
pt.y = pt.y - origin_vector[1] + target_vector[1]
|
567
|
+
pt.z = pt.z - origin_vector[2] + target_vector[2]
|