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.
Files changed (56) hide show
  1. dendrotweaks/__init__.py +10 -0
  2. dendrotweaks/analysis/__init__.py +11 -0
  3. dendrotweaks/analysis/ephys_analysis.py +482 -0
  4. dendrotweaks/analysis/morphometric_analysis.py +106 -0
  5. dendrotweaks/membrane/__init__.py +6 -0
  6. dendrotweaks/membrane/default_mod/AMPA.mod +65 -0
  7. dendrotweaks/membrane/default_mod/AMPA_NMDA.mod +100 -0
  8. dendrotweaks/membrane/default_mod/CaDyn.mod +54 -0
  9. dendrotweaks/membrane/default_mod/GABAa.mod +65 -0
  10. dendrotweaks/membrane/default_mod/Leak.mod +27 -0
  11. dendrotweaks/membrane/default_mod/NMDA.mod +72 -0
  12. dendrotweaks/membrane/default_mod/vecstim.mod +76 -0
  13. dendrotweaks/membrane/default_templates/NEURON_template.py +354 -0
  14. dendrotweaks/membrane/default_templates/default.py +73 -0
  15. dendrotweaks/membrane/default_templates/standard_channel.mod +87 -0
  16. dendrotweaks/membrane/default_templates/template_jaxley.py +108 -0
  17. dendrotweaks/membrane/default_templates/template_jaxley_new.py +108 -0
  18. dendrotweaks/membrane/distributions.py +324 -0
  19. dendrotweaks/membrane/groups.py +103 -0
  20. dendrotweaks/membrane/io/__init__.py +11 -0
  21. dendrotweaks/membrane/io/ast.py +201 -0
  22. dendrotweaks/membrane/io/code_generators.py +312 -0
  23. dendrotweaks/membrane/io/converter.py +108 -0
  24. dendrotweaks/membrane/io/factories.py +144 -0
  25. dendrotweaks/membrane/io/grammar.py +417 -0
  26. dendrotweaks/membrane/io/loader.py +90 -0
  27. dendrotweaks/membrane/io/parser.py +499 -0
  28. dendrotweaks/membrane/io/reader.py +212 -0
  29. dendrotweaks/membrane/mechanisms.py +574 -0
  30. dendrotweaks/model.py +1916 -0
  31. dendrotweaks/model_io.py +75 -0
  32. dendrotweaks/morphology/__init__.py +5 -0
  33. dendrotweaks/morphology/domains.py +100 -0
  34. dendrotweaks/morphology/io/__init__.py +5 -0
  35. dendrotweaks/morphology/io/factories.py +212 -0
  36. dendrotweaks/morphology/io/reader.py +66 -0
  37. dendrotweaks/morphology/io/validation.py +212 -0
  38. dendrotweaks/morphology/point_trees.py +681 -0
  39. dendrotweaks/morphology/reduce/__init__.py +16 -0
  40. dendrotweaks/morphology/reduce/reduce.py +155 -0
  41. dendrotweaks/morphology/reduce/reduced_cylinder.py +129 -0
  42. dendrotweaks/morphology/sec_trees.py +1112 -0
  43. dendrotweaks/morphology/seg_trees.py +157 -0
  44. dendrotweaks/morphology/trees.py +567 -0
  45. dendrotweaks/path_manager.py +261 -0
  46. dendrotweaks/simulators.py +235 -0
  47. dendrotweaks/stimuli/__init__.py +3 -0
  48. dendrotweaks/stimuli/iclamps.py +73 -0
  49. dendrotweaks/stimuli/populations.py +265 -0
  50. dendrotweaks/stimuli/synapses.py +203 -0
  51. dendrotweaks/utils.py +239 -0
  52. dendrotweaks-0.3.1.dist-info/METADATA +70 -0
  53. dendrotweaks-0.3.1.dist-info/RECORD +56 -0
  54. dendrotweaks-0.3.1.dist-info/WHEEL +5 -0
  55. dendrotweaks-0.3.1.dist-info/licenses/LICENSE +674 -0
  56. 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]