swcgeom 0.21.0__cp313-cp313-win_amd64.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 (72) hide show
  1. swcgeom/__init__.py +21 -0
  2. swcgeom/analysis/__init__.py +13 -0
  3. swcgeom/analysis/feature_extractor.py +454 -0
  4. swcgeom/analysis/features.py +218 -0
  5. swcgeom/analysis/lmeasure.py +750 -0
  6. swcgeom/analysis/sholl.py +201 -0
  7. swcgeom/analysis/trunk.py +183 -0
  8. swcgeom/analysis/visualization.py +191 -0
  9. swcgeom/analysis/visualization3d.py +81 -0
  10. swcgeom/analysis/volume.py +143 -0
  11. swcgeom/core/__init__.py +19 -0
  12. swcgeom/core/branch.py +129 -0
  13. swcgeom/core/branch_tree.py +65 -0
  14. swcgeom/core/compartment.py +107 -0
  15. swcgeom/core/node.py +130 -0
  16. swcgeom/core/path.py +155 -0
  17. swcgeom/core/population.py +398 -0
  18. swcgeom/core/swc.py +247 -0
  19. swcgeom/core/swc_utils/__init__.py +19 -0
  20. swcgeom/core/swc_utils/assembler.py +35 -0
  21. swcgeom/core/swc_utils/base.py +180 -0
  22. swcgeom/core/swc_utils/checker.py +112 -0
  23. swcgeom/core/swc_utils/io.py +335 -0
  24. swcgeom/core/swc_utils/normalizer.py +163 -0
  25. swcgeom/core/swc_utils/subtree.py +70 -0
  26. swcgeom/core/tree.py +387 -0
  27. swcgeom/core/tree_utils.py +277 -0
  28. swcgeom/core/tree_utils_impl.py +58 -0
  29. swcgeom/images/__init__.py +9 -0
  30. swcgeom/images/augmentation.py +149 -0
  31. swcgeom/images/contrast.py +87 -0
  32. swcgeom/images/folder.py +217 -0
  33. swcgeom/images/io.py +604 -0
  34. swcgeom/images/loaders/__init__.py +8 -0
  35. swcgeom/images/loaders/pbd.c +38785 -0
  36. swcgeom/images/loaders/pbd.cp313-win_amd64.pyd +0 -0
  37. swcgeom/images/loaders/raw.c +17408 -0
  38. swcgeom/images/loaders/raw.cp313-win_amd64.pyd +0 -0
  39. swcgeom/transforms/__init__.py +20 -0
  40. swcgeom/transforms/base.py +136 -0
  41. swcgeom/transforms/branch.py +223 -0
  42. swcgeom/transforms/branch_tree.py +74 -0
  43. swcgeom/transforms/geometry.py +270 -0
  44. swcgeom/transforms/image_preprocess.py +107 -0
  45. swcgeom/transforms/image_stack.py +219 -0
  46. swcgeom/transforms/images.py +206 -0
  47. swcgeom/transforms/mst.py +183 -0
  48. swcgeom/transforms/neurolucida_asc.py +498 -0
  49. swcgeom/transforms/path.py +56 -0
  50. swcgeom/transforms/population.py +36 -0
  51. swcgeom/transforms/tree.py +298 -0
  52. swcgeom/transforms/tree_assembler.py +160 -0
  53. swcgeom/utils/__init__.py +18 -0
  54. swcgeom/utils/debug.py +23 -0
  55. swcgeom/utils/download.py +119 -0
  56. swcgeom/utils/dsu.py +58 -0
  57. swcgeom/utils/ellipse.py +131 -0
  58. swcgeom/utils/file.py +90 -0
  59. swcgeom/utils/neuromorpho.py +581 -0
  60. swcgeom/utils/numpy_helper.py +70 -0
  61. swcgeom/utils/plotter_2d.py +134 -0
  62. swcgeom/utils/plotter_3d.py +35 -0
  63. swcgeom/utils/renderer.py +145 -0
  64. swcgeom/utils/sdf.py +324 -0
  65. swcgeom/utils/solid_geometry.py +154 -0
  66. swcgeom/utils/transforms.py +367 -0
  67. swcgeom/utils/volumetric_object.py +483 -0
  68. swcgeom-0.21.0.dist-info/METADATA +86 -0
  69. swcgeom-0.21.0.dist-info/RECORD +72 -0
  70. swcgeom-0.21.0.dist-info/WHEEL +5 -0
  71. swcgeom-0.21.0.dist-info/licenses/LICENSE +201 -0
  72. swcgeom-0.21.0.dist-info/top_level.txt +1 -0
swcgeom/core/path.py ADDED
@@ -0,0 +1,155 @@
1
+
2
+ # SPDX-FileCopyrightText: 2022 - 2025 Zexin Yuan <pypi@yzx9.xyz>
3
+ #
4
+ # SPDX-License-Identifier: Apache-2.0
5
+
6
+ """Nueron path."""
7
+
8
+ from collections.abc import Iterable, Iterator
9
+ from typing import Generic, overload
10
+
11
+ import numpy as np
12
+ import numpy.typing as npt
13
+ from typing_extensions import deprecated
14
+
15
+ from swcgeom.core.node import Node
16
+ from swcgeom.core.swc import DictSWC, SWCLike, SWCTypeVar
17
+
18
+ __all__ = ["Path"]
19
+
20
+
21
+ class Path(SWCLike, Generic[SWCTypeVar]):
22
+ """Neuron path.
23
+
24
+ A path is a linear set of points without furcations.
25
+ """
26
+
27
+ attach: SWCTypeVar
28
+ idx: npt.NDArray[np.int32]
29
+
30
+ class Node(Node["Path"]):
31
+ """Node of neuron tree."""
32
+
33
+ def __init__(self, attach: SWCTypeVar, idx: npt.ArrayLike) -> None:
34
+ super().__init__()
35
+ self.attach = attach
36
+ self.names = attach.names
37
+ self.idx = np.array(idx, dtype=np.int32)
38
+ self.source = self.attach.source
39
+
40
+ def __iter__(self) -> Iterator[Node]:
41
+ return (self.node(i) for i in range(len(self)))
42
+
43
+ def __len__(self) -> int:
44
+ return self.id().shape[0]
45
+
46
+ def __repr__(self) -> str:
47
+ return f"Neuron path with {len(self)} nodes."
48
+
49
+ @overload
50
+ def __getitem__(self, key: int) -> Node: ...
51
+ @overload
52
+ def __getitem__(self, key: slice) -> list[Node]: ...
53
+ @overload
54
+ def __getitem__(self, key: str) -> npt.NDArray: ...
55
+ def __getitem__(self, key):
56
+ if isinstance(key, slice):
57
+ return [self.node(i) for i in range(*key.indices(len(self)))]
58
+
59
+ if isinstance(key, (int, np.integer)):
60
+ length = len(self)
61
+ if key < -length or key >= length:
62
+ raise IndexError(f"The index ({key}) is out of range.")
63
+
64
+ if key < 0: # Handle negative indices
65
+ key += length
66
+
67
+ return self.node(key)
68
+
69
+ if isinstance(key, str):
70
+ return self.get_ndata(key)
71
+
72
+ raise TypeError("Invalid argument type.")
73
+
74
+ def keys(self) -> Iterable[str]:
75
+ return self.attach.keys()
76
+
77
+ def get_ndata(self, key: str) -> npt.NDArray:
78
+ return self.attach.get_ndata(key)[self.idx]
79
+
80
+ @deprecated("Use `path.node` instead.")
81
+ def get_node(self, idx: int | np.integer) -> Node:
82
+ """Get the count of intersection.
83
+
84
+ .. deprecated:: 0.16.0
85
+ Use :meth:`path.node` instead.
86
+ """
87
+
88
+ return self.node(idx)
89
+
90
+ def node(self, idx: int | np.integer) -> Node:
91
+ return self.Node(self, idx)
92
+
93
+ def detach(self) -> "Path[DictSWC]":
94
+ """Detach from current attached object."""
95
+ # pylint: disable-next=consider-using-dict-items
96
+ attact = DictSWC(
97
+ **{k: self.get_ndata(k) for k in self.keys()},
98
+ source=self.source,
99
+ names=self.names,
100
+ )
101
+ attact.ndata[self.names.id] = self.id()
102
+ attact.ndata[self.names.pid] = self.pid()
103
+ return Path(attact, self.id())
104
+
105
+ def id(self) -> npt.NDArray[np.int32]: # pylint: disable=invalid-name
106
+ """Get the ids of shape (n_sample,).
107
+
108
+ Returns:
109
+ a consecutively incremented id.
110
+
111
+ See Also:
112
+ self.origin_id
113
+ """
114
+ return np.arange(len(self.origin_id()), dtype=np.int32)
115
+
116
+ def pid(self) -> npt.NDArray[np.int32]:
117
+ """Get the ids of shape (n_sample,).
118
+
119
+ Returns:
120
+ a consecutively incremented pid.
121
+
122
+ See Also:
123
+ self.origin_pid
124
+ """
125
+ return np.arange(-1, len(self.origin_id()) - 1, dtype=np.int32)
126
+
127
+ def origin_id(self) -> npt.NDArray[np.int32]:
128
+ """Get the original id."""
129
+ return self.get_ndata(self.names.id)
130
+
131
+ def origin_pid(self) -> npt.NDArray[np.int32]:
132
+ """Get the original pid."""
133
+ return self.get_ndata(self.names.pid)
134
+
135
+ def length(self) -> float:
136
+ """Sum of length of stems."""
137
+ xyz = self.xyz()
138
+ return np.sum(np.linalg.norm(xyz[1:] - xyz[:-1], axis=1)).item()
139
+
140
+ def straight_line_distance(self) -> float:
141
+ """Straight-line distance of path.
142
+
143
+ The end-to-end straight-line distance between start point and end point.
144
+ """
145
+ return np.linalg.norm(self.node(-1).xyz() - self.node(0).xyz()).item()
146
+
147
+ def tortuosity(self) -> float:
148
+ """Tortuosity of path.
149
+
150
+ The straight-line distance between two consecutive branch points divided by the
151
+ length of the neuronal path between those points.
152
+ """
153
+ if (length := self.length()) == 0:
154
+ return 1
155
+ return self.straight_line_distance() / length
@@ -0,0 +1,398 @@
1
+ # SPDX-FileCopyrightText: 2022 - 2025 Zexin Yuan <pypi@yzx9.xyz>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ """Neuron population is a set of tree."""
6
+
7
+ import os
8
+ import warnings
9
+ from collections.abc import Callable, Iterable, Iterator
10
+ from concurrent.futures import ProcessPoolExecutor
11
+ from functools import reduce
12
+ from typing import Any, Literal, Protocol, TypeVar, cast, overload
13
+
14
+ import numpy as np
15
+ import numpy.typing as npt
16
+ from tqdm.contrib.concurrent import process_map
17
+ from typing_extensions import Self
18
+
19
+ from swcgeom.core.swc import eswc_cols
20
+ from swcgeom.core.swc_utils.base import SWCNames
21
+ from swcgeom.core.swc_utils.io import read_swc_components
22
+ from swcgeom.core.tree import Tree
23
+ from swcgeom.utils import PathOrIO
24
+
25
+ __all__ = ["LazyLoadingTrees", "ChainTrees", "Population", "Populations"]
26
+
27
+
28
+ T = TypeVar("T")
29
+
30
+
31
+ class Trees(Protocol):
32
+ """Trees protocol support index and len."""
33
+
34
+ def __getitem__(self, key: int, /) -> Tree: ...
35
+ def __len__(self) -> int: ...
36
+
37
+
38
+ class LazyLoadingTrees:
39
+ """Lazy loading trees."""
40
+
41
+ swcs: list[str]
42
+ trees: list[Tree | None]
43
+ kwargs: dict[str, Any]
44
+
45
+ def __init__(self, swcs: Iterable[str], **kwargs) -> None:
46
+ """
47
+ Args:
48
+ swcs: List of str
49
+ kwargs: Forwarding to `Tree.from_swc`
50
+ """
51
+
52
+ super().__init__()
53
+ self.swcs = list(swcs)
54
+ self.trees = [None for _ in swcs]
55
+ self.kwargs = kwargs
56
+
57
+ def __getitem__(self, key: int, /) -> Tree:
58
+ idx = _get_idx(key, len(self))
59
+ self.load(idx)
60
+ return cast(Tree, self.trees[idx])
61
+
62
+ def __len__(self) -> int:
63
+ return len(self.swcs)
64
+
65
+ def __iter__(self) -> Iterator[Tree]:
66
+ return (self[i] for i in range(self.__len__()))
67
+
68
+ def load(self, key: int) -> None:
69
+ if self.trees[key] is None:
70
+ self.trees[key] = Tree.from_swc(self.swcs[key], **self.kwargs)
71
+
72
+
73
+ class ChainTrees:
74
+ """Chain trees."""
75
+
76
+ trees: list[Trees]
77
+ cumsum: npt.NDArray[np.int64]
78
+
79
+ def __init__(self, trees: Iterable[Trees]) -> None:
80
+ super().__init__()
81
+ self.trees = list(trees)
82
+ self.cumsum = np.cumsum([0] + [len(ts) for ts in trees])
83
+
84
+ def __getitem__(self, key: int, /) -> Tree:
85
+ i, j = 1, len(self.trees) # cumsum[0] === 0
86
+ idx = _get_idx(key, len(self))
87
+ while i < j:
88
+ mid = (i + j) // 2
89
+ if self.cumsum[mid] <= idx:
90
+ i = mid + 1
91
+ else:
92
+ j = mid
93
+
94
+ return self.trees[i - 1][idx - self.cumsum[i - 1]]
95
+
96
+ def __len__(self) -> int:
97
+ return self.cumsum[-1].item()
98
+
99
+ def __iter__(self) -> Iterator[Tree]:
100
+ return (self[i] for i in range(self.__len__()))
101
+
102
+
103
+ class NestTrees:
104
+ def __init__(self, trees: Trees, idx: Iterable[int], /) -> None:
105
+ super().__init__()
106
+ self.trees = trees
107
+ self.idx = list(idx)
108
+
109
+ def __getitem__(self, key: int, /) -> Tree:
110
+ return self.trees[self.idx[key]]
111
+
112
+ def __len__(self) -> int:
113
+ return len(self.idx)
114
+
115
+
116
+ class Population:
117
+ """Neuron population."""
118
+
119
+ trees: Trees
120
+
121
+ @overload
122
+ def __init__(
123
+ self, swcs: Iterable[str], lazy_loading: bool = ..., root: str = ..., **kwargs
124
+ ) -> None: ...
125
+ @overload
126
+ def __init__(self, trees: Trees, /, *, root: str = "") -> None: ...
127
+ def __init__(self, swcs, lazy_loading=True, root="", **kwargs) -> None:
128
+ super().__init__()
129
+ if len(swcs) > 0 and isinstance(swcs[0], str):
130
+ warnings.warn(
131
+ "`Population(swcs)` has been replaced by "
132
+ "`Population(LazyLoadingTrees(swcs))` since v0.8.0 thus we can create "
133
+ "a population from a group of trees, and this will be removed in next "
134
+ "version",
135
+ DeprecationWarning,
136
+ )
137
+
138
+ trees = LazyLoadingTrees(swcs, **kwargs)
139
+ if not lazy_loading:
140
+ for i in range(len(swcs)):
141
+ trees.load(i)
142
+ else:
143
+ trees = swcs
144
+
145
+ self.trees = trees
146
+ self.root = root
147
+
148
+ if len(swcs) == 0:
149
+ warnings.warn(f"no trees in population from '{root}'")
150
+
151
+ @overload
152
+ def __getitem__(self, key: slice) -> Trees: ...
153
+ @overload
154
+ def __getitem__(self, key: int) -> Tree: ...
155
+ def __getitem__(self, key: int | slice):
156
+ if isinstance(key, slice):
157
+ trees = NestTrees(self.trees, range(*key.indices(len(self))))
158
+ return cast(Trees, trees)
159
+
160
+ if isinstance(key, (int, np.integer)):
161
+ return cast(Tree, self.trees[int(key)])
162
+
163
+ raise TypeError("Invalid argument type.")
164
+
165
+ def __len__(self) -> int:
166
+ return len(self.trees)
167
+
168
+ def __iter__(self) -> Iterator[Tree]:
169
+ return (self[i] for i in range(self.__len__()))
170
+
171
+ def __repr__(self) -> str:
172
+ return f"Neuron population in '{self.root}'"
173
+
174
+ def map(
175
+ self,
176
+ fn: Callable[[Tree], T],
177
+ *,
178
+ max_worker: int | None = None,
179
+ verbose: bool = False,
180
+ ) -> Iterator[T]:
181
+ """Map a function to all trees in the population.
182
+
183
+ This is a straightforward interface for parallelizing computations. The
184
+ parameters are intentionally kept simple and user-friendly. For more advanced
185
+ control, consider using `concurrent.futures` directly.
186
+ """
187
+
188
+ trees = (t for t in self.trees)
189
+
190
+ if verbose:
191
+ results = process_map(fn, trees, max_workers=max_worker)
192
+ else:
193
+ with ProcessPoolExecutor(max_worker) as p:
194
+ results = p.map(fn, trees)
195
+
196
+ return results
197
+
198
+ @classmethod
199
+ def from_swc(cls, root: str, ext: str = ".swc", **kwargs) -> Self:
200
+ if not os.path.exists(root):
201
+ raise FileNotFoundError(
202
+ f"the root does not refers to an existing directory: {root}"
203
+ )
204
+
205
+ swcs = cls.find_swcs(root, ext)
206
+ return cls(LazyLoadingTrees(swcs, **kwargs), root=root)
207
+
208
+ @classmethod
209
+ def from_eswc(
210
+ cls,
211
+ root: str,
212
+ ext: str = ".eswc",
213
+ extra_cols: Iterable[str] | None = None,
214
+ **kwargs,
215
+ ) -> Self:
216
+ extra_cols = list(extra_cols) if extra_cols is not None else []
217
+ extra_cols.extend(k for k, _ in eswc_cols)
218
+ return cls.from_swc(root, ext, extra_cols=extra_cols, **kwargs)
219
+
220
+ @staticmethod
221
+ def find_swcs(root: str, ext: str = ".swc", relpath: bool = False) -> list[str]:
222
+ """Find all swc files."""
223
+ swcs: list[str] = []
224
+ for r, _, files in os.walk(root):
225
+ rr = os.path.relpath(r, root) if relpath else r
226
+ fs = filter(lambda f: os.path.splitext(f)[-1] == ext, files)
227
+ swcs.extend(os.path.join(rr, f) for f in fs)
228
+
229
+ return swcs
230
+
231
+ @classmethod
232
+ def from_multi_roots_swc(
233
+ cls,
234
+ swc_file: PathOrIO,
235
+ *,
236
+ extra_cols: Iterable[str] | None = None,
237
+ encoding: Literal["detect"] | str = "utf-8",
238
+ names: SWCNames | None = None,
239
+ reset_index_per_subtree: bool = True,
240
+ **kwargs,
241
+ ) -> Self:
242
+ """Create a population from an SWC file containing multiple roots.
243
+
244
+ Each root in the SWC file will be treated as the start of a
245
+ separate tree in the resulting population.
246
+
247
+ Args:
248
+ swc_file: Path to the SWC file.
249
+ extra_cols: Read more cols in swc file. Passed to
250
+ `read_swc_components`.
251
+ encoding: The name of the encoding used to decode the file.
252
+ Passed to `read_swc_components`.
253
+ names: SWCNames configuration. Passed to `read_swc_components`.
254
+ reset_index_per_subtree: Reset node index for each subtree
255
+ to start with zero. Passed to `read_swc_components`.
256
+ **kwargs: Additional keyword arguments passed to `Tree.from_data_frame`
257
+ for each component tree.
258
+
259
+ Returns:
260
+ A Population object where each tree corresponds to a connected
261
+ component from the input SWC file.
262
+ """
263
+ dfs, comments = read_swc_components(
264
+ swc_file,
265
+ extra_cols=extra_cols,
266
+ encoding=encoding,
267
+ names=names,
268
+ reset_index_per_subtree=reset_index_per_subtree,
269
+ )
270
+
271
+ trees = [
272
+ Tree.from_data_frame(
273
+ df,
274
+ source=f"{swc_file}#component_{i}",
275
+ comments=comments,
276
+ names=names,
277
+ **kwargs,
278
+ )
279
+ for i, df in enumerate(dfs)
280
+ ]
281
+
282
+ # Use the file path as the 'root' for the population representation
283
+ root_repr = str(swc_file) if not hasattr(swc_file, "name") else swc_file.name
284
+ return cls(trees, root=root_repr)
285
+
286
+
287
+ class Populations:
288
+ """A set of population."""
289
+
290
+ len: int
291
+ populations: list[Population]
292
+ labels: list[str]
293
+
294
+ def __init__(
295
+ self, populations: Iterable[Population], labels: Iterable[str] | None = None
296
+ ) -> None:
297
+ self.len = min(len(p) for p in populations)
298
+ self.populations = list(populations)
299
+
300
+ labels = list(labels) if labels is not None else ["" for i in populations]
301
+ assert len(labels) == len(self.populations), (
302
+ f"got {len(self.populations)} populations, but has {len(labels)} labels"
303
+ )
304
+ self.labels = labels
305
+
306
+ @overload
307
+ def __getitem__(self, key: slice) -> list[list[Tree]]: ...
308
+ @overload
309
+ def __getitem__(self, key: int) -> list[Tree]: ...
310
+ def __getitem__(self, key):
311
+ return [p[key] for p in self.populations]
312
+
313
+ def __len__(self) -> int:
314
+ """Miniumn length of populations."""
315
+ return self.len
316
+
317
+ def __iter__(self) -> Iterator[list[Tree]]:
318
+ return (self[i] for i in range(self.len))
319
+
320
+ def __repr__(self) -> str:
321
+ return (
322
+ f"A cluster of {self.num_of_populations()} neuron populations, "
323
+ f"each containing at least {self.len} trees"
324
+ )
325
+
326
+ def num_of_populations(self) -> int:
327
+ return len(self.populations)
328
+
329
+ def to_population(self) -> Population:
330
+ return Population(ChainTrees(p.trees for p in self.populations))
331
+
332
+ @classmethod
333
+ def from_swc(
334
+ cls,
335
+ roots: Iterable[str],
336
+ ext: str = ".swc",
337
+ intersect: bool = True,
338
+ check_same: bool = False,
339
+ labels: Iterable[str] | None = None,
340
+ **kwargs,
341
+ ) -> Self:
342
+ """Get population from dirs.
343
+
344
+ Args:
345
+ roots: List of str
346
+ intersect: Take the intersection of these populations.
347
+ check_same: Check if the directories contains the same swc.
348
+ labels: Label of populations.
349
+ **kwargs: Forwarding to `Population`.
350
+ """
351
+ fs = [Population.find_swcs(d, ext=ext, relpath=True) for d in roots]
352
+ if intersect:
353
+ inter = list(reduce(lambda a, b: set(a).intersection(set(b)), fs))
354
+ if len(inter) == 0:
355
+ warnings.warn("no intersection among populations")
356
+
357
+ fs = [inter for _ in roots]
358
+ elif check_same:
359
+ assert [fs[0] == a for a in fs[1:]], "not the same among populations"
360
+
361
+ populations = [
362
+ Population(
363
+ LazyLoadingTrees([os.path.join(d, p) for p in fs[i]], **kwargs), root=d
364
+ )
365
+ for i, d in enumerate(roots)
366
+ ]
367
+ return cls(populations, labels=labels)
368
+
369
+ @classmethod
370
+ def from_eswc(
371
+ cls,
372
+ roots: Iterable[str],
373
+ extra_cols: Iterable[str] | None = None,
374
+ *,
375
+ ext: str = ".eswc",
376
+ **kwargs,
377
+ ) -> Self:
378
+ extra_cols = list(extra_cols) if extra_cols is not None else []
379
+ extra_cols.extend(k for k, _ in eswc_cols)
380
+ return cls.from_swc(roots, extra_cols=extra_cols, ext=ext, **kwargs)
381
+
382
+
383
+ def _get_idx(key: int, length: int) -> int:
384
+ if key < -length or key >= length:
385
+ raise IndexError(f"The index ({key}) is out of range.")
386
+
387
+ if key < 0: # Handle negative indices
388
+ key += length
389
+
390
+ return key
391
+
392
+
393
+ # experimental
394
+ def filter_population(pop: Population, predicate: Callable[[Tree], bool]) -> Population:
395
+ """Filter trees in the population."""
396
+ # TODO: how to avoid load trees
397
+ idx = [i for i, t in enumerate(pop) if predicate(t)]
398
+ return Population(NestTrees(pop.trees, idx), root=pop.root)