Skip to content

Commit 4664551

Browse files
add Tree implementation
1 parent 07df47f commit 4664551

2 files changed

Lines changed: 319 additions & 0 deletions

File tree

loopy/schedule/tree.py

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
__copyright__ = "Copyright (C) 2022 Kaushik Kulkarni"
2+
3+
__license__ = """
4+
Permission is hereby granted, free of charge, to any person obtaining a copy
5+
of this software and associated documentation files (the "Software"), to deal
6+
in the Software without restriction, including without limitation the rights
7+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
copies of the Software, and to permit persons to whom the Software is
9+
furnished to do so, subject to the following conditions:
10+
11+
The above copyright notice and this permission notice shall be included in
12+
all copies or substantial portions of the Software.
13+
14+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20+
THE SOFTWARE.
21+
"""
22+
23+
from immutables import Map
24+
from typing import Generic, Hashable, Tuple, TypeVar, Iterator, Optional, List
25+
from dataclasses import dataclass
26+
27+
# {{{ tree data structure
28+
29+
NodeT = TypeVar("NodeT", bound=Hashable)
30+
31+
32+
@dataclass(frozen=True)
33+
class Tree(Generic[NodeT]):
34+
"""
35+
An immutable n-ary tree containing nodes of type :class:`NodeT`.
36+
37+
.. automethod:: ancestors
38+
.. automethod:: parent
39+
.. automethod:: children
40+
.. automethod:: add_node
41+
.. automethod:: depth
42+
.. automethod:: replace_node
43+
.. automethod:: move_node
44+
45+
.. note::
46+
47+
Almost all the operations are implemented recursively. NOT suitable for
48+
deep trees. At the very least if the Python implementation is CPython
49+
this allocates a new stack frame for each iteration of the operation.
50+
"""
51+
_parent_to_children: Map[NodeT, Tuple[NodeT, ...]]
52+
_child_to_parent: Map[NodeT, Optional[NodeT]]
53+
54+
@staticmethod
55+
def from_root(root: NodeT) -> "Tree[NodeT]":
56+
return Tree(Map({root: tuple()}),
57+
Map({root: None}))
58+
59+
@property
60+
def root(self) -> NodeT:
61+
guess = set(self._child_to_parent).pop()
62+
parent_of_guess = self.parent(guess)
63+
while parent_of_guess is not None:
64+
guess = parent_of_guess
65+
parent_of_guess = self.parent(guess)
66+
67+
return guess
68+
69+
def ancestors(self, node: NodeT) -> Tuple[NodeT, ...]:
70+
"""
71+
Returns a :class:`tuple` of nodes that are ancestors of *node*.
72+
"""
73+
if not self.is_a_node(node):
74+
raise ValueError(f"'{node}' not in tree.")
75+
76+
if self.is_root(node):
77+
# => root
78+
return tuple()
79+
80+
parent = self._child_to_parent[node]
81+
assert parent is not None
82+
83+
return (parent,) + self.ancestors(parent)
84+
85+
def parent(self, node: NodeT) -> Optional[NodeT]:
86+
"""
87+
Returns the parent of *node*.
88+
"""
89+
if not self.is_a_node(node):
90+
raise ValueError(f"'{node}' not in tree.")
91+
92+
return self._child_to_parent[node]
93+
94+
def children(self, node: NodeT) -> Tuple[NodeT, ...]:
95+
"""
96+
Returns the children of *node*.
97+
"""
98+
if not self.is_a_node(node):
99+
raise ValueError(f"'{node}' not in tree.")
100+
101+
return self._parent_to_children[node]
102+
103+
def depth(self, node: NodeT) -> int:
104+
"""
105+
Returns the depth of *node*.
106+
"""
107+
if not self.is_a_node(node):
108+
raise ValueError(f"'{node}' not in tree.")
109+
110+
if self.is_root(node):
111+
# => None
112+
return 0
113+
114+
parent_of_node = self.parent(node)
115+
assert parent_of_node is not None
116+
117+
return 1 + self.depth(parent_of_node)
118+
119+
def is_root(self, node: NodeT) -> bool:
120+
if not self.is_a_node(node):
121+
raise ValueError(f"'{node}' not in tree.")
122+
123+
return self.parent(node) is None
124+
125+
def is_leaf(self, node: NodeT) -> bool:
126+
if not self.is_a_node(node):
127+
raise ValueError(f"'{node}' not in tree.")
128+
129+
return len(self.children(node)) == 0
130+
131+
def is_a_node(self, node: NodeT) -> bool:
132+
return node in self._child_to_parent
133+
134+
def add_node(self, node: NodeT, parent: NodeT) -> "Tree[NodeT]":
135+
"""
136+
Returns a :class:`Tree` with added node *node* having a parent
137+
*parent*.
138+
"""
139+
if self.is_a_node(node):
140+
raise ValueError(f"'{node}' already present in tree.")
141+
142+
siblings = self._parent_to_children[parent]
143+
144+
return Tree((self._parent_to_children
145+
.set(parent, siblings + (node,))
146+
.set(node, tuple())),
147+
self._child_to_parent.set(node, parent))
148+
149+
def replace_node(self, node: NodeT, new_id: NodeT) -> "Tree[NodeT]":
150+
"""
151+
Returns a copy of *self* with *node* replaced with *new_id*.
152+
"""
153+
if not self.is_a_node(node):
154+
raise ValueError(f"'{node}' not present in tree.")
155+
156+
if self.is_a_node(new_id):
157+
raise ValueError(f"cannot rename to '{new_id}', as its already a part"
158+
" of the tree.")
159+
160+
parent = self.parent(node)
161+
children = self.children(node)
162+
163+
# {{{ update child to parent
164+
165+
new_child_to_parent = (self._child_to_parent.delete(node)
166+
.set(new_id, parent))
167+
168+
for child in children:
169+
new_child_to_parent = (new_child_to_parent
170+
.set(child, new_id))
171+
172+
# }}}
173+
174+
# {{{ update parent_to_children
175+
176+
new_parent_to_children = (self._parent_to_children
177+
.delete(node)
178+
.set(new_id, self.children(node)))
179+
180+
if parent is not None:
181+
# update the child's name in the parent's children
182+
new_parent_to_children = (new_parent_to_children
183+
.delete(parent)
184+
.set(parent, tuple(
185+
frozenset(self.children(parent))
186+
- frozenset([node]))
187+
+ (new_id,)))
188+
189+
# }}}
190+
191+
return Tree(new_parent_to_children,
192+
new_child_to_parent)
193+
194+
def move_node(self, node: NodeT, new_parent: Optional[NodeT]) -> "Tree[NodeT]":
195+
"""
196+
Returns a copy of *self* with node *node* as a child of *new_parent*.
197+
"""
198+
if not self.is_a_node(node):
199+
raise ValueError(f"'{node}' not a part of the tree => cannot move.")
200+
201+
if self.is_root(node):
202+
if new_parent is None:
203+
return self
204+
else:
205+
raise ValueError("Moving root not allowed.")
206+
207+
if new_parent is None:
208+
raise ValueError("Making multiple roots not allowed")
209+
210+
if not self.is_a_node(new_parent):
211+
raise ValueError(f"Cannot move to '{new_parent}' as it's not in tree.")
212+
213+
parent = self.parent(node)
214+
assert parent is not None # parent=root handled as a special case
215+
siblings = self.children(parent)
216+
parents_new_children = tuple(frozenset(siblings) - frozenset([node]))
217+
new_parents_children = self.children(new_parent) + (node,)
218+
219+
new_child_to_parent = self._child_to_parent.set(node, new_parent)
220+
new_parent_to_children = (self._parent_to_children
221+
.set(parent, parents_new_children)
222+
.set(new_parent, new_parents_children))
223+
224+
return Tree(new_parent_to_children,
225+
new_child_to_parent)
226+
227+
def __str__(self) -> str:
228+
"""
229+
Stringifies the tree by using the box-drawing unicode characters.
230+
231+
.. doctest::
232+
233+
>>> from loopy.schedule.tree import Tree
234+
>>> tree = (Tree.from_root("Root")
235+
... .add_node("A", "Root")
236+
... .add_node("B", "Root")
237+
... .add_node("D", "B")
238+
... .add_node("E", "B")
239+
... .add_node("C", "A"))
240+
241+
>>> print(tree)
242+
Root
243+
├── A
244+
│ └── C
245+
└── B
246+
├── D
247+
└── E
248+
"""
249+
def rec(node: NodeT) -> List[str]:
250+
children_result = [rec(c) for c in self.children(node)]
251+
252+
def post_process_non_last_child(child):
253+
return ["├── " + child[0]] + [f"│ {c}" for c in child[1:]]
254+
255+
def post_process_last_child(child):
256+
return ["└── " + child[0]] + [f" {c}" for c in child[1:]]
257+
258+
children_result = ([post_process_non_last_child(c)
259+
for c in children_result[:-1]]
260+
+ [post_process_last_child(c)
261+
for c in children_result[-1:]])
262+
return [str(node)] + sum(children_result, start=[])
263+
264+
return "\n".join(rec(self.root))
265+
266+
def nodes(self) -> Iterator[NodeT]:
267+
return iter(self._child_to_parent.keys())
268+
269+
# }}}

test/test_tree.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
__copyright__ = "Copyright (C) 2022 University of Illinois Board of Trustees"
2+
3+
__license__ = """
4+
Permission is hereby granted, free of charge, to any person obtaining a copy
5+
of this software and associated documentation files (the "Software"), to deal
6+
in the Software without restriction, including without limitation the rights
7+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
copies of the Software, and to permit persons to whom the Software is
9+
furnished to do so, subject to the following conditions:
10+
11+
The above copyright notice and this permission notice shall be included in
12+
all copies or substantial portions of the Software.
13+
14+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20+
THE SOFTWARE.
21+
"""
22+
23+
from pyopencl.tools import ( # noqa: F401
24+
pytest_generate_tests_for_pyopencl
25+
as pytest_generate_tests)
26+
27+
from loopy.schedule.tree import Tree
28+
29+
30+
def test_tree_simple():
31+
tree = Tree.from_root("")
32+
33+
tree = tree.add_node("bar", parent="")
34+
tree = tree.add_node("baz", parent="bar")
35+
36+
assert tree.depth("") == 0
37+
assert tree.depth("bar") == 1
38+
assert tree.depth("baz") == 2
39+
40+
assert tree.is_a_node("")
41+
assert tree.is_a_node("bar")
42+
assert tree.is_a_node("baz")
43+
assert not tree.is_a_node("foo")
44+
45+
tree = tree.replace_node("bar", "foo")
46+
assert not tree.is_a_node("bar")
47+
assert tree.is_a_node("foo")
48+
49+
tree = tree.move_node("baz", new_parent="")
50+
assert tree.depth("baz") == 1

0 commit comments

Comments
 (0)