tree.network
1from __future__ import annotations 2 3from typing import List, Iterable 4from varname.core import varname 5 6class _SliceMixin: 7 """ 8 **Private mix‑in** that equips a container with NumPy‑style multi‑axis 9 slicing and broadcasting. 10 11 It is mixed into :class:`Tree`, :class:`Network` and :class:`_Mapper` 12 so they can all share one robust implementation of ``__getitem__``. 13 14 -------------------- 15 Supported selectors 16 -------------------- 17 18 ╔═══════════════════════╦══════════════════════════════════════════════════╗ 19 ║ Selector type ║ Behaviour ║ 20 ╠═══════════════════════╬══════════════════════════════════════════════════╣ 21 ║ ``int`` ║ Pick a single element, *collapse* the axis. ║ 22 ║ ``Tree`` instance ║ Same as ``int`` but selects the exact object. ║ 23 ║ ``str`` ║ • If it matches a child’s ``name`` → pick it. ║ 24 ║ ║ • Otherwise treat as attribute broadcast. ║ 25 ║ ``slice`` ║ Keep the axis; later keys decide whether to ║ 26 ║ ║ collapse (mimics NumPy’s “slice‑then‑scalar”). ║ 27 ║ ``list`` / ``_Mapper``║ Advanced (a.k.a. *fancy*) indexing; keeps axis. ║ 28 ╚═══════════════════════╩══════════════════════════════════════════════════╝ 29 30 Attribute broadcasting lets you ask, for example:: 31 32 tree[:, 'mass'] # list of each child.mass 33 tree[['a', 'b'], :, 'color'] 34 35 ------------------ 36 Implementation 37 ------------------ 38 The heavy lifting is done in the private 39 :py:meth:`_SliceMixin.__array_slice` helper; the public entry point 40 is ``__getitem__`` which forwards to it. 41 42 The class is **deliberately prefixed with an underscore** because 43 user code should never inherit from it directly; consume it only via 44 the public classes that already depend on it. 45 """ 46 47 # ‑‑ internal implementation pulled from your working array_slice() ‑‑ 48 def __array_slice(self, *keys): 49 if len(keys) == 1 and isinstance(keys[0], tuple): 50 keys = keys[0] 51 if not keys: 52 return _Mapper(self) 53 54 key, *rest = keys 55 children = list(self) 56 57 # normalise first selector ------------------------------------------------ 58 if isinstance(key, Tree): 59 selectors, grouping = [key], False 60 elif isinstance(key, slice): 61 selectors, grouping = children[key], True 62 elif isinstance(key, (list, _Mapper)): 63 selectors, grouping = list(key), True 64 else: 65 selectors, grouping = [key], False 66 67 resolved = [] 68 name_map = {c.name: c for c in children} 69 70 for sel in selectors: 71 if sel is None: 72 continue 73 if isinstance(sel, Tree): 74 resolved.append(sel) 75 elif isinstance(sel, int): 76 resolved.append(children[sel]) 77 elif isinstance(sel, slice): 78 resolved.append(children[sel]) 79 elif isinstance(sel, _Mapper): 80 resolved.append(sel) 81 elif isinstance(sel, str): 82 if sel in name_map: 83 resolved.append(name_map[sel]) 84 else: 85 resolved.append( 86 _Mapper([getattr(ch, sel, None) for ch in children]) 87 ) 88 else: 89 raise ValueError(f"Unsupported key type: {type(sel)}") 90 91 # recurse ---------------------------------------------------------------- 92 if rest: 93 resolved = [item.__array_slice(*rest) for item in resolved] 94 95 collapse = False 96 if not grouping: 97 collapse = True # scalar pick 98 elif isinstance(key, slice): 99 if not isinstance(rest[0], slice): 100 collapse = True # slice → non‑slice 101 102 if collapse: 103 flat = [] 104 for item in resolved: 105 flat.extend(item if isinstance(item, (list, _Mapper)) else [item]) 106 resolved = flat 107 108 return _Mapper(resolved) 109 110 # public entry point -------------------------------------------------------- 111 def __getitem__(self, *keys): 112 return self.__array_slice(*keys) 113 114class Tree(_SliceMixin, list): 115 """ 116 Efficient and dynamic tree structure with cascading attributes. More complex structure than a binary tree, 117 and more flexible, resembling a network. 118 119 .. note:: Computations: efficient and dynamic 120 - [x] `parent` attribute is set to child nodes as they are added. 121 - [x] All other attributes are computed *recursively*, with *vectorisation*. 122 - [x] Attributes are set in a cascade down the descendants. 123 - [x] Attributes are accessed via Python's own hashing routine. 124 125 Examples 126 -------- 127 128 Build a tree branch with two `leaves` 129 >>> leaf = Tree() 130 >>> branch = Tree(leaf, leaf) 131 >>> print(branch) 132 branch: ['leaf', 'leaf'] 133 >>> len(branch) 134 2 135 136 Attributes cascade down the `descendants` 137 >>> branch.color = 'green' 138 >>> leaf.color 139 'green' 140 >>> leaf.color = 'crimson' 141 >>> leaf.color 142 'crimson' 143 144 We can climb the tree with the `parent` attribute 145 >>> leaf.parent.color 146 'green' 147 148 Consider a more complicated tree: 149 >>> trunk = Tree(branch, branch, leaf) 150 >>> tree = Tree(trunk) 151 152 We can access the `ancestors`': 153 >>> print(leaf.ancestors) 154 ancestors: ['leaf', 'trunk', 'tree'] 155 156 Note the different ways to access the `root`. 157 >>> leaf.root == leaf.ancestors[-1] 158 True 159 160 >>> print(tree.descendants) 161 descendants: ['tree', 'trunk', 'branch', 'leaf', 'leaf', 'branch', 'leaf', 'leaf', 'leaf'] 162 163 Extracting variables from the leaves: 164 >>> tree.a = 1.0 165 >>> print(tree.a) 166 1.0 167 >>> print(tree.leaves.a) 168 [1.0, 1.0, 1.0, 1.0, 1.0] 169 >>> print(extrinsic(tree.leaves.a)) 170 5.0 171 >>> print(intrinsic(tree.leaves.a)) 172 1.0 173 """ 174 175 def __init__(self, *child: Tree) -> None: 176 """ 177 A subclass of list. 178 """ 179 super().__init__(_child for _child in child) 180 181 self.__dict__["parent"] = None 182 """ 183 Parent attribute set by parent node. 184 """ 185 186 try: 187 self.__dict__["__name__"] = varname() 188 except: 189 self.__dict__["__name__"] = str(list(self)) 190 191 self.__setparent__(*child) 192 193 def __eq__(self, other): 194 return (id(self) == id(other)) 195 196 def __hash__(self): 197 return id(self) 198 199 @property 200 def name(self): 201 return self.__name__ 202 203 def __str__(self): 204 if len(self)>0: 205 return self.__name__ + ': ' + str([child.__name__ for child in self]) 206 else: 207 return self.__name__ 208 209 @property 210 def root(self) -> Tree: 211 """ 212 The last ancestor. 213 """ 214 return self.ancestors[-1] 215 216 @property 217 def __ancestors(self) -> Tree: 218 """ 219 This node prepended to its parent's ancestors. 220 221 .. note:: The first ancestor is self. 222 """ 223 if self.parent == None: 224 return [self] 225 else: 226 return [self] + self.parent.__ancestors 227 228 @property 229 def ancestors(self) -> "Tree": 230 """ 231 This node prepended to its parent’s ancestors. 232 The first element is *self*. 233 """ 234 # 1. raw list (self … root) 235 chain = self.__ancestors # type: List[Tree] 236 237 # 2. remember current parent pointers 238 old_parent = {node: node.parent for node in chain} 239 240 # 3. create a *display* object without breaking the real graph 241 ancestors = Tree(*chain) # pretty wrapper 242 ancestors.__dict__["__name__"] = "ancestors" 243 244 # 4. restore parents exactly as they were 245 for node, par in old_parent.items(): 246 node.__dict__["parent"] = par 247 248 return ancestors 249 250 @property 251 def descendants(self) -> Tree: 252 """ 253 254 .. note:: The first descendant is self. 255 256 Lists all descendant nodes 257 """ 258 descendants = Tree(self) 259 for child in self: 260 descendants += (child.descendants) 261 return descendants 262 263 @property 264 def generations(self) -> List[Tree]: 265 """ 266 Descendants ordered by generation. 267 268 """ 269 descendants = self.descendants 270 index = [descendant.height for descendant in descendants] 271 generations = [Tree() for _ in range(max(index)+1)] 272 for index, descendant in zip(index, descendants): 273 generations[index].append(descendant) 274 for i, generation in enumerate(generations): 275 generation.__dict__["__name__"] = "generation " + str(i) 276 generations = Tree(*generations) 277 return generations 278 279 @property 280 def leaves(self) -> List[Tree]: 281 """ 282 Elements of the tree without descendants. 283 """ 284 leaves = _Mapper([descendant for child in self for descendant in child.leaves] if len(self)>0 else [self]) 285 return leaves 286 287 @property 288 def n_leaves(self): 289 return len(self.leaves) 290 291 @property 292 def size(self) -> int: 293 """ 294 Number of `descendants` of current node, including itself. 295 """ 296 return len(self.descendants) 297 298 @property 299 def depth(self) -> int: 300 """ 301 Number of ancestors of current node excluding itself. 302 """ 303 return len(self.ancestors) - 1 304 305 @property 306 def height(self) -> int: 307 """ 308 Number of generations of descendants. 309 """ 310 if len(self)>0: 311 return 1 + max([child.height for child in self]) 312 else: 313 return 0 314 315 def insert(self, index, child): 316 """Insert child before index and link to it as its parent.""" 317 super().insert(index, child) 318 self.__setparent__(*child) 319 # try: 320 # self.__dict__[child.__name__] = child 321 # except: 322 # pass 323 324 def append(self, child: Tree): 325 """Append child and link to it as its parent.""" 326 super().append(child) 327 self.__setparent__(*child) 328 # try: 329 # self.__dict__[child.__name__] = child 330 # except: 331 # pass 332 333 def extend(self, children: Iterable[Tree]): 334 """Extend children with an Iterable of Trees, linking 335 to the children as a parent. 336 """ 337 self.__setparent__(*children) 338 super().extend(children) 339 # for child in children: 340 # try: 341 # self.__dict__[child.__name__] = child 342 # except: 343 # pass 344 345 def __getattr__(self, name: str): 346 try: 347 return self.__dict__[f"{name}"] 348 except KeyError: 349 raise AttributeError(name + f" not defined!") 350 351 def __setattr__(self, name, value): 352 """set attributes down the lineage""" 353 self.__dict__[f"{name}"] = value 354 [item.__setattr__(name, value) for item in self] 355 356 def __setparent__(self, *children): 357 """ 358 Set childs' parent 359 """ 360 def f(child): 361 child.__dict__["parent"] = self 362 child.__setparent__(*child) 363 list(map(f, children)) 364 365class Graph(Tree): 366 """ 367 Add `add` and `hop` functions to Tree to turn the leaves into a Graph 368 369 Examples 370 -------- 371 372 Consider a complicated graph: 373 >>> A = Graph() 374 >>> B = Graph() 375 >>> graph = Graph(A, B) 376 377 Add onsite term: 378 >>> graph.add(μ=0.2) 379 380 Add hopping terms: 381 >>> A.hop(B, t=1) 382 >>> B.hop(A, t=-1) 383 384 Parameter describing diagonal elements are given by the following set 385 >>> A.diagonal 386 {'μ'} 387 388 Parameter describing off-diagonal elements are given by the following 389 dictionary, where the dictionary value is the Tree node specified by `hop`: 390 >>> A.offdiagonal 391 {'t': {[]}} 392 393 The parameter itself is accessed by 394 >>> A.μ 395 0.2 396 397 >>> graph.leaves.t 398 [1, -1] 399 >>> graph.leaves.t *= 2 400 >>> graph.leaves.t 401 [2, -2] 402 """ 403 _diagonal = {} 404 _offdiagonal = {} 405 406 def __init__(self, *child: Tree) -> None: 407 """ 408 A subclass of Tree. 409 """ 410 super().__init__(*child) 411 412 try: 413 self.__dict__["__name__"] = varname() 414 except: 415 self.__dict__["__name__"] 416 417 def __reset__(self): 418 self._diagonal = {} 419 self._offdiagonal = {} 420 421 @property 422 def diagonal(self): 423 _diagonal = set() 424 for key, items in self._diagonal.items(): 425 for node, item in items.items(): 426 if self in node.descendants: 427 _diagonal.add(key) 428 return _diagonal 429 430 @property 431 def offdiagonal(self): 432 _offdiagonal = dict() 433 for key, items in self._offdiagonal.items(): 434 for node, neighbours in items.items(): 435 for neighbour, items in neighbours.items(): 436 if self in node.descendants: 437 try: 438 _offdiagonal[key].add(neighbour) 439 except: 440 _offdiagonal[key] = set([neighbour]) 441 return _offdiagonal 442 443 def __setattr__(self, name, value, update = True): 444 """ 445 Set attributes down the lineage, and add attribute to list of 446 attributes to be updates 447 """ 448 if update: 449 self.__cache__(name, value) 450 451 self.__dict__[f"{name}"] = value 452 453 [item.__setattr__(name, value, update = False) for item in self] 454 455 def __cache__(self, key, value): 456 457 try: 458 self._diagonal[key][self]["value"] = value 459 except: 460 pass 461 462 try: 463 neighbours = self._offdiagonal[key][self] 464 for neighbour, items in neighbours.items(): 465 self._offdiagonal[key][self][neighbour]["value"] = value 466 except: 467 pass 468 469 def add(self, **kwarg): 470 """Add vertex term as a parameter described by a key-value pair""" 471 472 if len(kwarg)==0: 473 raise ValueError("Requires a kwarg!") 474 475 if len(kwarg)>1: 476 raise ValueError("Accepts only one kwarg!") 477 478 key, value = list(kwarg.items())[0] 479 480 if not key in self._diagonal: 481 self._diagonal[key] = {} 482 483 if not self in self._diagonal[key]: 484 self._diagonal[key][self] = {} 485 486 self.__setattr__(key, value) 487 488 def hop(self, neighbour: Tree, **kwarg): 489 """Add directed edge as a neighbour and a parameter (described by a 490 key-value pair) 491 """ 492 493 if len(kwarg)==0: 494 raise ValueError("Requires a kwarg!") 495 496 if len(kwarg)>1: 497 raise ValueError("Accepts only one kwarg!") 498 499 if self.n_leaves!=neighbour.n_leaves: 500 raise ValueError("Self and neighbour have different number of leaves. Hopping is not well-defined!") 501 502 key, value = list(kwarg.items())[0] 503 504 if not key in self._offdiagonal: 505 self._offdiagonal[key] = {} 506 507 if not self in self._offdiagonal[key]: 508 self._offdiagonal[key][self] = {} 509 510 if not neighbour in self._offdiagonal[key][self]: 511 self._offdiagonal[key][self][neighbour] = {} 512 513 self.__setattr__(key, value) 514 515class _Mapper(_SliceMixin, list): 516 """ 517 **Private list‑like wrapper** returned by all slicing operations and by 518 :pyattr:`Tree.leaves`. 519 520 It behaves exactly like a Python ``list`` *plus* a few tricks: 521 522 * **Vectorised attribute access / assignment** 523 524 >>> tree = Tree(Tree(Tree()),Tree(Tree())) 525 >>> branches = tree[:, 0] 526 >>> print(branches) 527 [[], []] 528 >>> branches.color = "green" # propagated to every branch 529 >>> print(branches.color) 530 ['green', 'green'] 531 532 * **Simple numeric broadcasting** 533 534 ``__iadd__``, ``__imul__`` … modify numbers (or NumPy / torch arrays) 535 in place across the collection. 536 537 * **Recursive slicing** 538 539 Because ``_Mapper`` also inherits :class:`_SliceMixin`, you can keep 540 chaining subscripts indefinitely:: 541 542 leaf_masses = tree[:, :, :, 'mass'] 543 544 The class is private because library users never need to create it 545 manually; it is only surfaced as a return value. 546 """ 547 548 def __init__(self, *items) -> None: 549 """ 550 A subclass of list. 551 """ 552 if type(items)==tuple and len(items)==1: 553 items = items[0] 554 if type(items) == list: 555 items = [_Mapper(item) if type(item)==list else item for item in items] 556 super().__init__(items) 557 558 def __getattr__(self, name: str): 559 return _Mapper([getattr(item, name) if type(item)!=type(None) else None for item in self]) 560 561 def __setattr__(self, name, value): 562 """Set attributes down the lineage""" 563 if type(value) is list: 564 if len(value) == len(self): 565 [setattr(self[i], name, value[i]) for i in range(len(self)) if self[i] is not None] 566 else: 567 [setattr(item, name, value) for item in self if item is not None] 568 569 def __imul__(self, scaler): 570 self = [n*scaler for n in self] 571 return self 572 573 def __itruediv__(self, scaler): 574 self = [n/scaler for n in self] 575 return self 576 577 def __iadd__(self, scaler): 578 self = [n+scaler for n in self] 579 return self 580 581 def __isub__(self, scaler): 582 self = [n-scaler for n in self] 583 return self 584 585 @property 586 def names(self): 587 """Returns name of each element""" 588 return self.name 589 590def extrinsic(items): 591 r""" 592 $$\sum_{i\in \text{items}} i$$ 593 594 If items is a numpy/torch object, optimisations are employed. 595 """ 596 try: 597 return items.sum() 598 except: 599 return sum(items) 600 601def intrinsic(items): 602 r""" 603 $$\frac{1}{\\#\text{items}}\sum_{i\in \text{items}} i$$ 604 605 If items is a numpy/torch object, optimisations are employed. 606 """ 607 try: 608 return items.mean() 609 except: 610 return extrinsic(items) / len(items) 611 612class Network(Graph): 613 """ 614 Access Network nodes via a powerful slicing method 615 616 Examples 617 -------- 618 619 >>> up = Network() 620 >>> dn = Network() 621 >>> branch = Network(up, dn) 622 >>> trunk_1 = Network(branch, branch) 623 >>> trunk_2 = Network(branch, branch) 624 >>> tree = Network(trunk_1, trunk_2) 625 626 >>> items = tree[['trunk_1', 'trunk_2']] 627 >>> items 628 [[[[], []], [[], []]], [[[], []], [[], []]]] 629 >>> items.__name__ 630 ['trunk_1', 'trunk_2'] 631 >>> items.root.name 632 [['tree'], ['tree']] 633 634 >>> items = tree[[trunk_1, trunk_2], :] 635 >>> items.name 636 [['branch', 'branch'], ['branch', 'branch']] 637 638 >>> items = tree[[trunk_1, trunk_2], :, up] 639 >>> items.name 640 [['up', 'up'], ['up', 'up']] 641 >>> items = tree[trunk_2, :, up] 642 >>> items.name 643 ['up', 'up'] 644 645 >>> items = tree[:, :, up] 646 >>> items.name 647 [['up', 'up'], ['up', 'up']] 648 649 >>> items = tree[:, :] 650 >>> items.name 651 [['branch', 'branch'], ['branch', 'branch']] 652 653 >>> items = tree[:] 654 >>> items.name 655 ['trunk_1', 'trunk_2'] 656 657 >>> tree[['trunk_2', 'trunk_2']].a = 1.2 658 >>> up.a 659 1.2 660 >>> tree.leaves.a 661 [1.2, 1.2, 1.2, 1.2, 1.2, 1.2, 1.2, 1.2] 662 663 Many ways to slice: 664 >>> tree[['trunk_1', 'trunk_2']].names 665 ['trunk_1', 'trunk_2'] 666 >>> tree[['trunk_1', 'trunk_2'], 0].names 667 [['branch'], ['branch']] 668 >>> tree[['trunk_1', 'trunk_2'], 0,0].names 669 [['up'], ['up']] 670 >>> tree[:, :, 1].names 671 [['dn', 'dn'], ['dn', 'dn']] 672 >>> tree[:, :, :].names 673 [[['up', 'dn'], ['up', 'dn']], [['up', 'dn'], ['up', 'dn']]] 674 >>> tree[['trunk_1', trunk_2], :, :].names 675 [[['up', 'dn'], ['up', 'dn']], [['up', 'dn'], ['up', 'dn']]] 676 >>> tree['trunk_1', :].names 677 ['branch', 'branch'] 678 >>> tree[('trunk_1')].names 679 ['trunk_1'] 680 >>> tree[['trunk_1']].names 681 ['trunk_1'] 682 >>> tree[(trunk_1)].names 683 ['trunk_1'] 684 >>> tree['trunk_1'].names 685 ['trunk_1'] 686 >>> tree[trunk_1].names 687 ['trunk_1'] 688 >>> tree[1].names 689 ['trunk_2'] 690 >>> tree[:].names 691 ['trunk_1', 'trunk_2'] 692 >>> tree[0:2].names 693 ['trunk_1', 'trunk_2'] 694 """ 695 def __init__(self, *child: Network) -> None: 696 """ 697 A subclass of Tree. 698 """ 699 super().__init__(*child) 700 701 try: 702 self.__dict__["__name__"] = varname() 703 except: 704 self.__dict__["__name__"] 705 706 for child in child: 707 self.__dict__[child.__name__] = child 708 709 def __set_indices__(self): 710 """ 711 Consider: 712 >>> A = Network() 713 >>> B = Network() 714 >>> tree = Network(A, B) 715 716 Set the indices 717 >>> tree.__set_indices__() 718 719 The indices are then given by 720 >>> A.index 721 0 722 >>> B.index 723 1 724 725 """ 726 self.initialised = True 727 def f(l, i): l.__dict__["index"] = i 728 [f(leaf, i) for i, leaf in enumerate(self.leaves)] 729 730 @property 731 def indices(self): 732 if not hasattr(self, "_indices"): 733 self.__dict__["_indices"] = [leaf.index for leaf in self.leaves] 734 return self._indices
115class Tree(_SliceMixin, list): 116 """ 117 Efficient and dynamic tree structure with cascading attributes. More complex structure than a binary tree, 118 and more flexible, resembling a network. 119 120 .. note:: Computations: efficient and dynamic 121 - [x] `parent` attribute is set to child nodes as they are added. 122 - [x] All other attributes are computed *recursively*, with *vectorisation*. 123 - [x] Attributes are set in a cascade down the descendants. 124 - [x] Attributes are accessed via Python's own hashing routine. 125 126 Examples 127 -------- 128 129 Build a tree branch with two `leaves` 130 >>> leaf = Tree() 131 >>> branch = Tree(leaf, leaf) 132 >>> print(branch) 133 branch: ['leaf', 'leaf'] 134 >>> len(branch) 135 2 136 137 Attributes cascade down the `descendants` 138 >>> branch.color = 'green' 139 >>> leaf.color 140 'green' 141 >>> leaf.color = 'crimson' 142 >>> leaf.color 143 'crimson' 144 145 We can climb the tree with the `parent` attribute 146 >>> leaf.parent.color 147 'green' 148 149 Consider a more complicated tree: 150 >>> trunk = Tree(branch, branch, leaf) 151 >>> tree = Tree(trunk) 152 153 We can access the `ancestors`': 154 >>> print(leaf.ancestors) 155 ancestors: ['leaf', 'trunk', 'tree'] 156 157 Note the different ways to access the `root`. 158 >>> leaf.root == leaf.ancestors[-1] 159 True 160 161 >>> print(tree.descendants) 162 descendants: ['tree', 'trunk', 'branch', 'leaf', 'leaf', 'branch', 'leaf', 'leaf', 'leaf'] 163 164 Extracting variables from the leaves: 165 >>> tree.a = 1.0 166 >>> print(tree.a) 167 1.0 168 >>> print(tree.leaves.a) 169 [1.0, 1.0, 1.0, 1.0, 1.0] 170 >>> print(extrinsic(tree.leaves.a)) 171 5.0 172 >>> print(intrinsic(tree.leaves.a)) 173 1.0 174 """ 175 176 def __init__(self, *child: Tree) -> None: 177 """ 178 A subclass of list. 179 """ 180 super().__init__(_child for _child in child) 181 182 self.__dict__["parent"] = None 183 """ 184 Parent attribute set by parent node. 185 """ 186 187 try: 188 self.__dict__["__name__"] = varname() 189 except: 190 self.__dict__["__name__"] = str(list(self)) 191 192 self.__setparent__(*child) 193 194 def __eq__(self, other): 195 return (id(self) == id(other)) 196 197 def __hash__(self): 198 return id(self) 199 200 @property 201 def name(self): 202 return self.__name__ 203 204 def __str__(self): 205 if len(self)>0: 206 return self.__name__ + ': ' + str([child.__name__ for child in self]) 207 else: 208 return self.__name__ 209 210 @property 211 def root(self) -> Tree: 212 """ 213 The last ancestor. 214 """ 215 return self.ancestors[-1] 216 217 @property 218 def __ancestors(self) -> Tree: 219 """ 220 This node prepended to its parent's ancestors. 221 222 .. note:: The first ancestor is self. 223 """ 224 if self.parent == None: 225 return [self] 226 else: 227 return [self] + self.parent.__ancestors 228 229 @property 230 def ancestors(self) -> "Tree": 231 """ 232 This node prepended to its parent’s ancestors. 233 The first element is *self*. 234 """ 235 # 1. raw list (self … root) 236 chain = self.__ancestors # type: List[Tree] 237 238 # 2. remember current parent pointers 239 old_parent = {node: node.parent for node in chain} 240 241 # 3. create a *display* object without breaking the real graph 242 ancestors = Tree(*chain) # pretty wrapper 243 ancestors.__dict__["__name__"] = "ancestors" 244 245 # 4. restore parents exactly as they were 246 for node, par in old_parent.items(): 247 node.__dict__["parent"] = par 248 249 return ancestors 250 251 @property 252 def descendants(self) -> Tree: 253 """ 254 255 .. note:: The first descendant is self. 256 257 Lists all descendant nodes 258 """ 259 descendants = Tree(self) 260 for child in self: 261 descendants += (child.descendants) 262 return descendants 263 264 @property 265 def generations(self) -> List[Tree]: 266 """ 267 Descendants ordered by generation. 268 269 """ 270 descendants = self.descendants 271 index = [descendant.height for descendant in descendants] 272 generations = [Tree() for _ in range(max(index)+1)] 273 for index, descendant in zip(index, descendants): 274 generations[index].append(descendant) 275 for i, generation in enumerate(generations): 276 generation.__dict__["__name__"] = "generation " + str(i) 277 generations = Tree(*generations) 278 return generations 279 280 @property 281 def leaves(self) -> List[Tree]: 282 """ 283 Elements of the tree without descendants. 284 """ 285 leaves = _Mapper([descendant for child in self for descendant in child.leaves] if len(self)>0 else [self]) 286 return leaves 287 288 @property 289 def n_leaves(self): 290 return len(self.leaves) 291 292 @property 293 def size(self) -> int: 294 """ 295 Number of `descendants` of current node, including itself. 296 """ 297 return len(self.descendants) 298 299 @property 300 def depth(self) -> int: 301 """ 302 Number of ancestors of current node excluding itself. 303 """ 304 return len(self.ancestors) - 1 305 306 @property 307 def height(self) -> int: 308 """ 309 Number of generations of descendants. 310 """ 311 if len(self)>0: 312 return 1 + max([child.height for child in self]) 313 else: 314 return 0 315 316 def insert(self, index, child): 317 """Insert child before index and link to it as its parent.""" 318 super().insert(index, child) 319 self.__setparent__(*child) 320 # try: 321 # self.__dict__[child.__name__] = child 322 # except: 323 # pass 324 325 def append(self, child: Tree): 326 """Append child and link to it as its parent.""" 327 super().append(child) 328 self.__setparent__(*child) 329 # try: 330 # self.__dict__[child.__name__] = child 331 # except: 332 # pass 333 334 def extend(self, children: Iterable[Tree]): 335 """Extend children with an Iterable of Trees, linking 336 to the children as a parent. 337 """ 338 self.__setparent__(*children) 339 super().extend(children) 340 # for child in children: 341 # try: 342 # self.__dict__[child.__name__] = child 343 # except: 344 # pass 345 346 def __getattr__(self, name: str): 347 try: 348 return self.__dict__[f"{name}"] 349 except KeyError: 350 raise AttributeError(name + f" not defined!") 351 352 def __setattr__(self, name, value): 353 """set attributes down the lineage""" 354 self.__dict__[f"{name}"] = value 355 [item.__setattr__(name, value) for item in self] 356 357 def __setparent__(self, *children): 358 """ 359 Set childs' parent 360 """ 361 def f(child): 362 child.__dict__["parent"] = self 363 child.__setparent__(*child) 364 list(map(f, children))
Efficient and dynamic tree structure with cascading attributes. More complex structure than a binary tree, and more flexible, resembling a network.
Computations: efficient and dynamic
-
parent
attribute is set to child nodes as they are added. - All other attributes are computed recursively, with vectorisation.
- Attributes are set in a cascade down the descendants.
- Attributes are accessed via Python's own hashing routine.
Examples
Build a tree branch with two leaves
>>> leaf = Tree()
>>> branch = Tree(leaf, leaf)
>>> print(branch)
branch: ['leaf', 'leaf']
>>> len(branch)
2
Attributes cascade down the descendants
>>> branch.color = 'green'
>>> leaf.color
'green'
>>> leaf.color = 'crimson'
>>> leaf.color
'crimson'
We can climb the tree with the parent
attribute
>>> leaf.parent.color
'green'
Consider a more complicated tree:
>>> trunk = Tree(branch, branch, leaf)
>>> tree = Tree(trunk)
We can access the ancestors
':
>>> print(leaf.ancestors)
ancestors: ['leaf', 'trunk', 'tree']
Note the different ways to access the root
.
>>> leaf.root == leaf.ancestors[-1]
True
>>> print(tree.descendants)
descendants: ['tree', 'trunk', 'branch', 'leaf', 'leaf', 'branch', 'leaf', 'leaf', 'leaf']
Extracting variables from the leaves:
>>> tree.a = 1.0
>>> print(tree.a)
1.0
>>> print(tree.leaves.a)
[1.0, 1.0, 1.0, 1.0, 1.0]
>>> print(extrinsic(tree.leaves.a))
5.0
>>> print(intrinsic(tree.leaves.a))
1.0
176 def __init__(self, *child: Tree) -> None: 177 """ 178 A subclass of list. 179 """ 180 super().__init__(_child for _child in child) 181 182 self.__dict__["parent"] = None 183 """ 184 Parent attribute set by parent node. 185 """ 186 187 try: 188 self.__dict__["__name__"] = varname() 189 except: 190 self.__dict__["__name__"] = str(list(self)) 191 192 self.__setparent__(*child)
A subclass of list.
210 @property 211 def root(self) -> Tree: 212 """ 213 The last ancestor. 214 """ 215 return self.ancestors[-1]
The last ancestor.
229 @property 230 def ancestors(self) -> "Tree": 231 """ 232 This node prepended to its parent’s ancestors. 233 The first element is *self*. 234 """ 235 # 1. raw list (self … root) 236 chain = self.__ancestors # type: List[Tree] 237 238 # 2. remember current parent pointers 239 old_parent = {node: node.parent for node in chain} 240 241 # 3. create a *display* object without breaking the real graph 242 ancestors = Tree(*chain) # pretty wrapper 243 ancestors.__dict__["__name__"] = "ancestors" 244 245 # 4. restore parents exactly as they were 246 for node, par in old_parent.items(): 247 node.__dict__["parent"] = par 248 249 return ancestors
This node prepended to its parent’s ancestors. The first element is self.
251 @property 252 def descendants(self) -> Tree: 253 """ 254 255 .. note:: The first descendant is self. 256 257 Lists all descendant nodes 258 """ 259 descendants = Tree(self) 260 for child in self: 261 descendants += (child.descendants) 262 return descendants
The first descendant is self.
Lists all descendant nodes
264 @property 265 def generations(self) -> List[Tree]: 266 """ 267 Descendants ordered by generation. 268 269 """ 270 descendants = self.descendants 271 index = [descendant.height for descendant in descendants] 272 generations = [Tree() for _ in range(max(index)+1)] 273 for index, descendant in zip(index, descendants): 274 generations[index].append(descendant) 275 for i, generation in enumerate(generations): 276 generation.__dict__["__name__"] = "generation " + str(i) 277 generations = Tree(*generations) 278 return generations
Descendants ordered by generation.
280 @property 281 def leaves(self) -> List[Tree]: 282 """ 283 Elements of the tree without descendants. 284 """ 285 leaves = _Mapper([descendant for child in self for descendant in child.leaves] if len(self)>0 else [self]) 286 return leaves
Elements of the tree without descendants.
292 @property 293 def size(self) -> int: 294 """ 295 Number of `descendants` of current node, including itself. 296 """ 297 return len(self.descendants)
Number of descendants
of current node, including itself.
299 @property 300 def depth(self) -> int: 301 """ 302 Number of ancestors of current node excluding itself. 303 """ 304 return len(self.ancestors) - 1
Number of ancestors of current node excluding itself.
306 @property 307 def height(self) -> int: 308 """ 309 Number of generations of descendants. 310 """ 311 if len(self)>0: 312 return 1 + max([child.height for child in self]) 313 else: 314 return 0
Number of generations of descendants.
316 def insert(self, index, child): 317 """Insert child before index and link to it as its parent.""" 318 super().insert(index, child) 319 self.__setparent__(*child) 320 # try: 321 # self.__dict__[child.__name__] = child 322 # except: 323 # pass
Insert child before index and link to it as its parent.
325 def append(self, child: Tree): 326 """Append child and link to it as its parent.""" 327 super().append(child) 328 self.__setparent__(*child) 329 # try: 330 # self.__dict__[child.__name__] = child 331 # except: 332 # pass
Append child and link to it as its parent.
334 def extend(self, children: Iterable[Tree]): 335 """Extend children with an Iterable of Trees, linking 336 to the children as a parent. 337 """ 338 self.__setparent__(*children) 339 super().extend(children) 340 # for child in children: 341 # try: 342 # self.__dict__[child.__name__] = child 343 # except: 344 # pass
Extend children with an Iterable of Trees, linking to the children as a parent.
366class Graph(Tree): 367 """ 368 Add `add` and `hop` functions to Tree to turn the leaves into a Graph 369 370 Examples 371 -------- 372 373 Consider a complicated graph: 374 >>> A = Graph() 375 >>> B = Graph() 376 >>> graph = Graph(A, B) 377 378 Add onsite term: 379 >>> graph.add(μ=0.2) 380 381 Add hopping terms: 382 >>> A.hop(B, t=1) 383 >>> B.hop(A, t=-1) 384 385 Parameter describing diagonal elements are given by the following set 386 >>> A.diagonal 387 {'μ'} 388 389 Parameter describing off-diagonal elements are given by the following 390 dictionary, where the dictionary value is the Tree node specified by `hop`: 391 >>> A.offdiagonal 392 {'t': {[]}} 393 394 The parameter itself is accessed by 395 >>> A.μ 396 0.2 397 398 >>> graph.leaves.t 399 [1, -1] 400 >>> graph.leaves.t *= 2 401 >>> graph.leaves.t 402 [2, -2] 403 """ 404 _diagonal = {} 405 _offdiagonal = {} 406 407 def __init__(self, *child: Tree) -> None: 408 """ 409 A subclass of Tree. 410 """ 411 super().__init__(*child) 412 413 try: 414 self.__dict__["__name__"] = varname() 415 except: 416 self.__dict__["__name__"] 417 418 def __reset__(self): 419 self._diagonal = {} 420 self._offdiagonal = {} 421 422 @property 423 def diagonal(self): 424 _diagonal = set() 425 for key, items in self._diagonal.items(): 426 for node, item in items.items(): 427 if self in node.descendants: 428 _diagonal.add(key) 429 return _diagonal 430 431 @property 432 def offdiagonal(self): 433 _offdiagonal = dict() 434 for key, items in self._offdiagonal.items(): 435 for node, neighbours in items.items(): 436 for neighbour, items in neighbours.items(): 437 if self in node.descendants: 438 try: 439 _offdiagonal[key].add(neighbour) 440 except: 441 _offdiagonal[key] = set([neighbour]) 442 return _offdiagonal 443 444 def __setattr__(self, name, value, update = True): 445 """ 446 Set attributes down the lineage, and add attribute to list of 447 attributes to be updates 448 """ 449 if update: 450 self.__cache__(name, value) 451 452 self.__dict__[f"{name}"] = value 453 454 [item.__setattr__(name, value, update = False) for item in self] 455 456 def __cache__(self, key, value): 457 458 try: 459 self._diagonal[key][self]["value"] = value 460 except: 461 pass 462 463 try: 464 neighbours = self._offdiagonal[key][self] 465 for neighbour, items in neighbours.items(): 466 self._offdiagonal[key][self][neighbour]["value"] = value 467 except: 468 pass 469 470 def add(self, **kwarg): 471 """Add vertex term as a parameter described by a key-value pair""" 472 473 if len(kwarg)==0: 474 raise ValueError("Requires a kwarg!") 475 476 if len(kwarg)>1: 477 raise ValueError("Accepts only one kwarg!") 478 479 key, value = list(kwarg.items())[0] 480 481 if not key in self._diagonal: 482 self._diagonal[key] = {} 483 484 if not self in self._diagonal[key]: 485 self._diagonal[key][self] = {} 486 487 self.__setattr__(key, value) 488 489 def hop(self, neighbour: Tree, **kwarg): 490 """Add directed edge as a neighbour and a parameter (described by a 491 key-value pair) 492 """ 493 494 if len(kwarg)==0: 495 raise ValueError("Requires a kwarg!") 496 497 if len(kwarg)>1: 498 raise ValueError("Accepts only one kwarg!") 499 500 if self.n_leaves!=neighbour.n_leaves: 501 raise ValueError("Self and neighbour have different number of leaves. Hopping is not well-defined!") 502 503 key, value = list(kwarg.items())[0] 504 505 if not key in self._offdiagonal: 506 self._offdiagonal[key] = {} 507 508 if not self in self._offdiagonal[key]: 509 self._offdiagonal[key][self] = {} 510 511 if not neighbour in self._offdiagonal[key][self]: 512 self._offdiagonal[key][self][neighbour] = {} 513 514 self.__setattr__(key, value)
Add add
and hop
functions to Tree to turn the leaves into a Graph
Examples
Consider a complicated graph:
>>> A = Graph()
>>> B = Graph()
>>> graph = Graph(A, B)
Add onsite term:
>>> graph.add(μ=0.2)
Add hopping terms:
>>> A.hop(B, t=1)
>>> B.hop(A, t=-1)
Parameter describing diagonal elements are given by the following set
>>> A.diagonal
{'μ'}
Parameter describing off-diagonal elements are given by the following
dictionary, where the dictionary value is the Tree node specified by hop
:
>>> A.offdiagonal
{'t': {[]}}
The parameter itself is accessed by
>>> A.μ
0.2
>>> graph.leaves.t
[1, -1]
>>> graph.leaves.t *= 2
>>> graph.leaves.t
[2, -2]
407 def __init__(self, *child: Tree) -> None: 408 """ 409 A subclass of Tree. 410 """ 411 super().__init__(*child) 412 413 try: 414 self.__dict__["__name__"] = varname() 415 except: 416 self.__dict__["__name__"]
A subclass of Tree.
431 @property 432 def offdiagonal(self): 433 _offdiagonal = dict() 434 for key, items in self._offdiagonal.items(): 435 for node, neighbours in items.items(): 436 for neighbour, items in neighbours.items(): 437 if self in node.descendants: 438 try: 439 _offdiagonal[key].add(neighbour) 440 except: 441 _offdiagonal[key] = set([neighbour]) 442 return _offdiagonal
470 def add(self, **kwarg): 471 """Add vertex term as a parameter described by a key-value pair""" 472 473 if len(kwarg)==0: 474 raise ValueError("Requires a kwarg!") 475 476 if len(kwarg)>1: 477 raise ValueError("Accepts only one kwarg!") 478 479 key, value = list(kwarg.items())[0] 480 481 if not key in self._diagonal: 482 self._diagonal[key] = {} 483 484 if not self in self._diagonal[key]: 485 self._diagonal[key][self] = {} 486 487 self.__setattr__(key, value)
Add vertex term as a parameter described by a key-value pair
489 def hop(self, neighbour: Tree, **kwarg): 490 """Add directed edge as a neighbour and a parameter (described by a 491 key-value pair) 492 """ 493 494 if len(kwarg)==0: 495 raise ValueError("Requires a kwarg!") 496 497 if len(kwarg)>1: 498 raise ValueError("Accepts only one kwarg!") 499 500 if self.n_leaves!=neighbour.n_leaves: 501 raise ValueError("Self and neighbour have different number of leaves. Hopping is not well-defined!") 502 503 key, value = list(kwarg.items())[0] 504 505 if not key in self._offdiagonal: 506 self._offdiagonal[key] = {} 507 508 if not self in self._offdiagonal[key]: 509 self._offdiagonal[key][self] = {} 510 511 if not neighbour in self._offdiagonal[key][self]: 512 self._offdiagonal[key][self][neighbour] = {} 513 514 self.__setattr__(key, value)
Add directed edge as a neighbour and a parameter (described by a key-value pair)
591def extrinsic(items): 592 r""" 593 $$\sum_{i\in \text{items}} i$$ 594 595 If items is a numpy/torch object, optimisations are employed. 596 """ 597 try: 598 return items.sum() 599 except: 600 return sum(items)
$$\sum_{i\in \text{items}} i$$
If items is a numpy/torch object, optimisations are employed.
602def intrinsic(items): 603 r""" 604 $$\frac{1}{\\#\text{items}}\sum_{i\in \text{items}} i$$ 605 606 If items is a numpy/torch object, optimisations are employed. 607 """ 608 try: 609 return items.mean() 610 except: 611 return extrinsic(items) / len(items)
$$\frac{1}{\#\text{items}}\sum_{i\in \text{items}} i$$
If items is a numpy/torch object, optimisations are employed.
613class Network(Graph): 614 """ 615 Access Network nodes via a powerful slicing method 616 617 Examples 618 -------- 619 620 >>> up = Network() 621 >>> dn = Network() 622 >>> branch = Network(up, dn) 623 >>> trunk_1 = Network(branch, branch) 624 >>> trunk_2 = Network(branch, branch) 625 >>> tree = Network(trunk_1, trunk_2) 626 627 >>> items = tree[['trunk_1', 'trunk_2']] 628 >>> items 629 [[[[], []], [[], []]], [[[], []], [[], []]]] 630 >>> items.__name__ 631 ['trunk_1', 'trunk_2'] 632 >>> items.root.name 633 [['tree'], ['tree']] 634 635 >>> items = tree[[trunk_1, trunk_2], :] 636 >>> items.name 637 [['branch', 'branch'], ['branch', 'branch']] 638 639 >>> items = tree[[trunk_1, trunk_2], :, up] 640 >>> items.name 641 [['up', 'up'], ['up', 'up']] 642 >>> items = tree[trunk_2, :, up] 643 >>> items.name 644 ['up', 'up'] 645 646 >>> items = tree[:, :, up] 647 >>> items.name 648 [['up', 'up'], ['up', 'up']] 649 650 >>> items = tree[:, :] 651 >>> items.name 652 [['branch', 'branch'], ['branch', 'branch']] 653 654 >>> items = tree[:] 655 >>> items.name 656 ['trunk_1', 'trunk_2'] 657 658 >>> tree[['trunk_2', 'trunk_2']].a = 1.2 659 >>> up.a 660 1.2 661 >>> tree.leaves.a 662 [1.2, 1.2, 1.2, 1.2, 1.2, 1.2, 1.2, 1.2] 663 664 Many ways to slice: 665 >>> tree[['trunk_1', 'trunk_2']].names 666 ['trunk_1', 'trunk_2'] 667 >>> tree[['trunk_1', 'trunk_2'], 0].names 668 [['branch'], ['branch']] 669 >>> tree[['trunk_1', 'trunk_2'], 0,0].names 670 [['up'], ['up']] 671 >>> tree[:, :, 1].names 672 [['dn', 'dn'], ['dn', 'dn']] 673 >>> tree[:, :, :].names 674 [[['up', 'dn'], ['up', 'dn']], [['up', 'dn'], ['up', 'dn']]] 675 >>> tree[['trunk_1', trunk_2], :, :].names 676 [[['up', 'dn'], ['up', 'dn']], [['up', 'dn'], ['up', 'dn']]] 677 >>> tree['trunk_1', :].names 678 ['branch', 'branch'] 679 >>> tree[('trunk_1')].names 680 ['trunk_1'] 681 >>> tree[['trunk_1']].names 682 ['trunk_1'] 683 >>> tree[(trunk_1)].names 684 ['trunk_1'] 685 >>> tree['trunk_1'].names 686 ['trunk_1'] 687 >>> tree[trunk_1].names 688 ['trunk_1'] 689 >>> tree[1].names 690 ['trunk_2'] 691 >>> tree[:].names 692 ['trunk_1', 'trunk_2'] 693 >>> tree[0:2].names 694 ['trunk_1', 'trunk_2'] 695 """ 696 def __init__(self, *child: Network) -> None: 697 """ 698 A subclass of Tree. 699 """ 700 super().__init__(*child) 701 702 try: 703 self.__dict__["__name__"] = varname() 704 except: 705 self.__dict__["__name__"] 706 707 for child in child: 708 self.__dict__[child.__name__] = child 709 710 def __set_indices__(self): 711 """ 712 Consider: 713 >>> A = Network() 714 >>> B = Network() 715 >>> tree = Network(A, B) 716 717 Set the indices 718 >>> tree.__set_indices__() 719 720 The indices are then given by 721 >>> A.index 722 0 723 >>> B.index 724 1 725 726 """ 727 self.initialised = True 728 def f(l, i): l.__dict__["index"] = i 729 [f(leaf, i) for i, leaf in enumerate(self.leaves)] 730 731 @property 732 def indices(self): 733 if not hasattr(self, "_indices"): 734 self.__dict__["_indices"] = [leaf.index for leaf in self.leaves] 735 return self._indices
Access Network nodes via a powerful slicing method
Examples
>>> up = Network()
>>> dn = Network()
>>> branch = Network(up, dn)
>>> trunk_1 = Network(branch, branch)
>>> trunk_2 = Network(branch, branch)
>>> tree = Network(trunk_1, trunk_2)
>>> items = tree[['trunk_1', 'trunk_2']]
>>> items
[[[[], []], [[], []]], [[[], []], [[], []]]]
>>> items.__name__
['trunk_1', 'trunk_2']
>>> items.root.name
[['tree'], ['tree']]
>>> items = tree[[trunk_1, trunk_2], :]
>>> items.name
[['branch', 'branch'], ['branch', 'branch']]
>>> items = tree[[trunk_1, trunk_2], :, up]
>>> items.name
[['up', 'up'], ['up', 'up']]
>>> items = tree[trunk_2, :, up]
>>> items.name
['up', 'up']
>>> items = tree[:, :, up]
>>> items.name
[['up', 'up'], ['up', 'up']]
>>> items = tree[:, :]
>>> items.name
[['branch', 'branch'], ['branch', 'branch']]
>>> items = tree[:]
>>> items.name
['trunk_1', 'trunk_2']
>>> tree[['trunk_2', 'trunk_2']].a = 1.2
>>> up.a
1.2
>>> tree.leaves.a
[1.2, 1.2, 1.2, 1.2, 1.2, 1.2, 1.2, 1.2]
Many ways to slice:
>>> tree[['trunk_1', 'trunk_2']].names
['trunk_1', 'trunk_2']
>>> tree[['trunk_1', 'trunk_2'], 0].names
[['branch'], ['branch']]
>>> tree[['trunk_1', 'trunk_2'], 0,0].names
[['up'], ['up']]
>>> tree[:, :, 1].names
[['dn', 'dn'], ['dn', 'dn']]
>>> tree[:, :, :].names
[[['up', 'dn'], ['up', 'dn']], [['up', 'dn'], ['up', 'dn']]]
>>> tree[['trunk_1', trunk_2], :, :].names
[[['up', 'dn'], ['up', 'dn']], [['up', 'dn'], ['up', 'dn']]]
>>> tree['trunk_1', :].names
['branch', 'branch']
>>> tree[('trunk_1')].names
['trunk_1']
>>> tree[['trunk_1']].names
['trunk_1']
>>> tree[(trunk_1)].names
['trunk_1']
>>> tree['trunk_1'].names
['trunk_1']
>>> tree[trunk_1].names
['trunk_1']
>>> tree[1].names
['trunk_2']
>>> tree[:].names
['trunk_1', 'trunk_2']
>>> tree[0:2].names
['trunk_1', 'trunk_2']
696 def __init__(self, *child: Network) -> None: 697 """ 698 A subclass of Tree. 699 """ 700 super().__init__(*child) 701 702 try: 703 self.__dict__["__name__"] = varname() 704 except: 705 self.__dict__["__name__"] 706 707 for child in child: 708 self.__dict__[child.__name__] = child
A subclass of Tree.