dtree-encoding 1.5.0__tar.gz → 1.6.0__tar.gz
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.
- {dtree_encoding-1.5.0 → dtree_encoding-1.6.0}/PKG-INFO +10 -6
- {dtree_encoding-1.5.0 → dtree_encoding-1.6.0}/README.md +7 -3
- dtree_encoding-1.6.0/dtree/__init__.py +6 -0
- {dtree_encoding-1.5.0 → dtree_encoding-1.6.0}/dtree/dtree.py +320 -146
- {dtree_encoding-1.5.0 → dtree_encoding-1.6.0}/dtree_encoding.egg-info/PKG-INFO +10 -6
- {dtree_encoding-1.5.0 → dtree_encoding-1.6.0}/pyproject.toml +3 -3
- {dtree_encoding-1.5.0 → dtree_encoding-1.6.0}/tests/test_dtree.py +131 -2
- dtree_encoding-1.5.0/dtree/__init__.py +0 -6
- {dtree_encoding-1.5.0 → dtree_encoding-1.6.0}/dtree_encoding.egg-info/SOURCES.txt +0 -0
- {dtree_encoding-1.5.0 → dtree_encoding-1.6.0}/dtree_encoding.egg-info/dependency_links.txt +0 -0
- {dtree_encoding-1.5.0 → dtree_encoding-1.6.0}/dtree_encoding.egg-info/top_level.txt +0 -0
- {dtree_encoding-1.5.0 → dtree_encoding-1.6.0}/setup.cfg +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dtree-encoding
|
|
3
|
-
Version: 1.
|
|
4
|
-
Summary: DTree: incremental encoding of directed trees by two projections (Q1, Q2)
|
|
3
|
+
Version: 1.6.0
|
|
4
|
+
Summary: DTree: incremental encoding of the topology of directed trees by two projections (Q1, Q2): O(1) reachability, and/or queries on the topology of the tree, dynamic (incremental) updates
|
|
5
5
|
Author-email: Enis Olgac <enis@digraphs.info>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/EnisOlgac/DTree
|
|
@@ -9,7 +9,7 @@ Project-URL: Documentation, https://github.com/EnisOlgac/DTree/blob/main/docs/AP
|
|
|
9
9
|
Project-URL: Changelog, https://github.com/EnisOlgac/DTree/blob/main/CHANGELOG.md
|
|
10
10
|
Project-URL: Paper, https://doi.org/10.5772/intechopen.1013408
|
|
11
11
|
Project-URL: Archive, https://doi.org/10.5281/zenodo.23395710
|
|
12
|
-
Keywords: directed tree,reachability,incremental encoding,dynamic tree,graph theory,data structure
|
|
12
|
+
Keywords: directed tree,tree topology,reachability,and/or queries,incremental encoding,dynamic tree,graph theory,data structure
|
|
13
13
|
Classifier: Development Status :: 5 - Production/Stable
|
|
14
14
|
Classifier: Intended Audience :: Science/Research
|
|
15
15
|
Classifier: Intended Audience :: Developers
|
|
@@ -23,7 +23,7 @@ Description-Content-Type: text/markdown
|
|
|
23
23
|
|
|
24
24
|
# DTree for Python
|
|
25
25
|
|
|
26
|
-
Python implementation of `DTree`, the incremental encoding of directed trees by two projections Q1 and Q2 (see the [API reference](https://github.com/EnisOlgac/DTree/blob/main/docs/API.md) and the [paper](https://doi.org/10.5772/intechopen.1013408)). No dependencies; Python 3.8+.
|
|
26
|
+
Python implementation of `DTree`, the incremental encoding of the topology of directed trees by two projections Q1 and Q2 (see the [API reference](https://github.com/EnisOlgac/DTree/blob/main/docs/API.md) and the [paper](https://doi.org/10.5772/intechopen.1013408)). No dependencies; Python 3.8+.
|
|
27
27
|
|
|
28
28
|
```
|
|
29
29
|
pip install dtree-encoding
|
|
@@ -73,18 +73,22 @@ t = DTree.from_file("Test.csx") # e.g. examples/Test.csx from the repository
|
|
|
73
73
|
| `SameSubtree`, `SubtreeSize` | `same_subtree`, `subtree_size` (plus `subtree`) |
|
|
74
74
|
| `Depth` | `depth` (root = 0) |
|
|
75
75
|
| `PathToRoot`, `RootToPath` | `path_to_root`, `root_to_path` (plus `path(u, v)`) |
|
|
76
|
+
| `GetNode`, `TryGetNode`, `DNode` | `get_node`, `try_get_node`, `DNode` (node handles; `insert` returns one) |
|
|
77
|
+
| `SubtreeLast` | `subtree_last` (last node of a subtree in Q1) |
|
|
76
78
|
| `VMin`, `VMax`, `UMin`, `UMax` (of nodes) | `vmin`, `vmax`, `umin`, `umax` |
|
|
77
79
|
| `ReachesAll`, `MayReachSome`, `ReachesSome` | `reaches_all`, `may_reach_some`, `reaches_some` |
|
|
78
80
|
| `ReachedByAll`, `MayBeReachedBySome`, `ReachedBySome` | `reached_by_all`, `may_be_reached_by_some`, `reached_by_some` |
|
|
79
81
|
|
|
80
82
|
## Semantics
|
|
81
83
|
|
|
84
|
+
- Every method accepts a node name or a node handle (`DNode`). `insert` returns the handle of the new node, `get_node(name)` gives the handle of an existing one. A handle refers directly to the node's positions in Q1 and Q2, so `can_reach` on handles is two position comparisons without any lookup by name. Handles of deleted nodes, or of another tree, are rejected (KeyError) or reported as unreachable.
|
|
85
|
+
|
|
82
86
|
- `can_reach(u, v)` is irreflexive: no node reaches itself, so `can_reach(u, u)` is `False`. (The C# prototype is reflexive.)
|
|
83
87
|
- `is_ancestor(u, v)` equals `can_reach(u, v)` and is likewise proper.
|
|
84
88
|
- `delete(v)` re-attaches v's children to v's parent, in v's place among its siblings.
|
|
85
89
|
- `delete_subtree(v)` removes v with all its descendants and returns them in Q1 order.
|
|
86
90
|
- `cut_n_paste(u, p)` moves the subtree rooted at u under p as p's last child; p must not lie inside u's subtree.
|
|
87
|
-
-
|
|
91
|
+
- And/or queries on the topology: the constructed nodes `vmin(S)`, `vmax(S)` (S as target) and `umin(S)`, `umax(S)` (S as source) take the minimum or maximum of each index over S, separately in Q1 and Q2. `reaches_all` and `reached_by_all` are exact O(1) tests; `may_reach_some` and `may_be_reached_by_some` are only filters, and `reaches_some` and `reached_by_some` are exact in O(log k). A constructed node stays valid under insertions and is recomputed automatically after `delete`, `delete_subtree` or `cut_n_paste`. See [and/or queries on the topology](https://github.com/EnisOlgac/DTree/blob/main/docs/and-or-queries.md).
|
|
88
92
|
- Projection positions are fractions: a new node gets the mediant (a+c)/(b+d) of its neighbors, so normally no other node moves. Only rare alternating insertion patterns trigger a local renumbering. Tree depth and width are unbounded. `t.Q1.index(v)` returns a position as a `Fraction`.
|
|
89
93
|
|
|
90
94
|
## Tests
|
|
@@ -104,7 +108,7 @@ python benchmarks/bench.py --dynamic # only insertions and queries alternating
|
|
|
104
108
|
python benchmarks/bench.py 1000000 # custom sizes
|
|
105
109
|
```
|
|
106
110
|
|
|
107
|
-
The script prints Markdown tables: build time and renumbering counts per tree shape (random, chain, star at the root, star at an inner node, alternating worst case), list growth at either end, and timings for `can_reach`, `depth`, `delete`, `cut_n_paste` and `delete_subtree`, and comparisons with a classical parent walk: reachability by tree depth,
|
|
111
|
+
The script prints Markdown tables: build time and renumbering counts per tree shape (random, chain, star at the root, star at an inner node, alternating worst case), list growth at either end, and timings for `can_reach`, `depth`, `delete`, `cut_n_paste` and `delete_subtree`, and comparisons with a classical parent walk: reachability by tree depth, and/or queries with constructed nodes, and dynamic updates (insertions and queries alternating). Each `DList` keeps the counters `renumber_count` and `renumber_moved`, so the same figures are available in your own code via `t.Q1` and `t.Q2`.
|
|
108
112
|
|
|
109
113
|
## Provenance
|
|
110
114
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# DTree for Python
|
|
2
2
|
|
|
3
|
-
Python implementation of `DTree`, the incremental encoding of directed trees by two projections Q1 and Q2 (see the [API reference](https://github.com/EnisOlgac/DTree/blob/main/docs/API.md) and the [paper](https://doi.org/10.5772/intechopen.1013408)). No dependencies; Python 3.8+.
|
|
3
|
+
Python implementation of `DTree`, the incremental encoding of the topology of directed trees by two projections Q1 and Q2 (see the [API reference](https://github.com/EnisOlgac/DTree/blob/main/docs/API.md) and the [paper](https://doi.org/10.5772/intechopen.1013408)). No dependencies; Python 3.8+.
|
|
4
4
|
|
|
5
5
|
```
|
|
6
6
|
pip install dtree-encoding
|
|
@@ -50,18 +50,22 @@ t = DTree.from_file("Test.csx") # e.g. examples/Test.csx from the repository
|
|
|
50
50
|
| `SameSubtree`, `SubtreeSize` | `same_subtree`, `subtree_size` (plus `subtree`) |
|
|
51
51
|
| `Depth` | `depth` (root = 0) |
|
|
52
52
|
| `PathToRoot`, `RootToPath` | `path_to_root`, `root_to_path` (plus `path(u, v)`) |
|
|
53
|
+
| `GetNode`, `TryGetNode`, `DNode` | `get_node`, `try_get_node`, `DNode` (node handles; `insert` returns one) |
|
|
54
|
+
| `SubtreeLast` | `subtree_last` (last node of a subtree in Q1) |
|
|
53
55
|
| `VMin`, `VMax`, `UMin`, `UMax` (of nodes) | `vmin`, `vmax`, `umin`, `umax` |
|
|
54
56
|
| `ReachesAll`, `MayReachSome`, `ReachesSome` | `reaches_all`, `may_reach_some`, `reaches_some` |
|
|
55
57
|
| `ReachedByAll`, `MayBeReachedBySome`, `ReachedBySome` | `reached_by_all`, `may_be_reached_by_some`, `reached_by_some` |
|
|
56
58
|
|
|
57
59
|
## Semantics
|
|
58
60
|
|
|
61
|
+
- Every method accepts a node name or a node handle (`DNode`). `insert` returns the handle of the new node, `get_node(name)` gives the handle of an existing one. A handle refers directly to the node's positions in Q1 and Q2, so `can_reach` on handles is two position comparisons without any lookup by name. Handles of deleted nodes, or of another tree, are rejected (KeyError) or reported as unreachable.
|
|
62
|
+
|
|
59
63
|
- `can_reach(u, v)` is irreflexive: no node reaches itself, so `can_reach(u, u)` is `False`. (The C# prototype is reflexive.)
|
|
60
64
|
- `is_ancestor(u, v)` equals `can_reach(u, v)` and is likewise proper.
|
|
61
65
|
- `delete(v)` re-attaches v's children to v's parent, in v's place among its siblings.
|
|
62
66
|
- `delete_subtree(v)` removes v with all its descendants and returns them in Q1 order.
|
|
63
67
|
- `cut_n_paste(u, p)` moves the subtree rooted at u under p as p's last child; p must not lie inside u's subtree.
|
|
64
|
-
-
|
|
68
|
+
- And/or queries on the topology: the constructed nodes `vmin(S)`, `vmax(S)` (S as target) and `umin(S)`, `umax(S)` (S as source) take the minimum or maximum of each index over S, separately in Q1 and Q2. `reaches_all` and `reached_by_all` are exact O(1) tests; `may_reach_some` and `may_be_reached_by_some` are only filters, and `reaches_some` and `reached_by_some` are exact in O(log k). A constructed node stays valid under insertions and is recomputed automatically after `delete`, `delete_subtree` or `cut_n_paste`. See [and/or queries on the topology](https://github.com/EnisOlgac/DTree/blob/main/docs/and-or-queries.md).
|
|
65
69
|
- Projection positions are fractions: a new node gets the mediant (a+c)/(b+d) of its neighbors, so normally no other node moves. Only rare alternating insertion patterns trigger a local renumbering. Tree depth and width are unbounded. `t.Q1.index(v)` returns a position as a `Fraction`.
|
|
66
70
|
|
|
67
71
|
## Tests
|
|
@@ -81,7 +85,7 @@ python benchmarks/bench.py --dynamic # only insertions and queries alternating
|
|
|
81
85
|
python benchmarks/bench.py 1000000 # custom sizes
|
|
82
86
|
```
|
|
83
87
|
|
|
84
|
-
The script prints Markdown tables: build time and renumbering counts per tree shape (random, chain, star at the root, star at an inner node, alternating worst case), list growth at either end, and timings for `can_reach`, `depth`, `delete`, `cut_n_paste` and `delete_subtree`, and comparisons with a classical parent walk: reachability by tree depth,
|
|
88
|
+
The script prints Markdown tables: build time and renumbering counts per tree shape (random, chain, star at the root, star at an inner node, alternating worst case), list growth at either end, and timings for `can_reach`, `depth`, `delete`, `cut_n_paste` and `delete_subtree`, and comparisons with a classical parent walk: reachability by tree depth, and/or queries with constructed nodes, and dynamic updates (insertions and queries alternating). Each `DList` keeps the counters `renumber_count` and `renumber_moved`, so the same figures are available in your own code via `t.Q1` and `t.Q2`.
|
|
85
89
|
|
|
86
90
|
## Provenance
|
|
87
91
|
|
|
@@ -52,7 +52,7 @@ _GROWTH = 2 # each doubling of the region raises the required spacing by this f
|
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
class _Cell:
|
|
55
|
-
__slots__ = ("name", "prev", "next", "num", "den")
|
|
55
|
+
__slots__ = ("name", "prev", "next", "num", "den", "node")
|
|
56
56
|
|
|
57
57
|
def __init__(self, name: str) -> None:
|
|
58
58
|
self.name = name
|
|
@@ -60,6 +60,7 @@ class _Cell:
|
|
|
60
60
|
self.next: Optional[_Cell] = None
|
|
61
61
|
self.num = 0
|
|
62
62
|
self.den = 1
|
|
63
|
+
self.node: Optional["DNode"] = None # the tree node owning this cell (DTree only)
|
|
63
64
|
|
|
64
65
|
|
|
65
66
|
class DList:
|
|
@@ -296,13 +297,52 @@ class DList:
|
|
|
296
297
|
return f"{self.name}: [{', '.join(self)}]"
|
|
297
298
|
|
|
298
299
|
|
|
300
|
+
class DNode:
|
|
301
|
+
"""Handle of a tree node: refers directly to its positions in Q1 and Q2.
|
|
302
|
+
|
|
303
|
+
Returned by ``DTree.insert`` and ``DTree.get_node``. Every DTree method accepts a
|
|
304
|
+
handle wherever it accepts a node name; with handles, ``can_reach`` is two position
|
|
305
|
+
comparisons without any lookup by name. The idea of using addresses as ids comes from
|
|
306
|
+
Enis Olgac's original implementation.
|
|
307
|
+
"""
|
|
308
|
+
|
|
309
|
+
__slots__ = ("name", "up", "kids", "c1", "c2", "last", "owner")
|
|
310
|
+
|
|
311
|
+
def __init__(self, name: str, owner: "DTree") -> None:
|
|
312
|
+
self.name = name
|
|
313
|
+
self.up: Optional[DNode] = None # parent (None for the root)
|
|
314
|
+
self.kids: Optional[List[DNode]] = None
|
|
315
|
+
self.c1: Optional[_Cell] = None # position in Q1
|
|
316
|
+
self.c2: Optional[_Cell] = None # position in Q2
|
|
317
|
+
self.last: Optional[DNode] = None # hint: last node of the subtree in Q1 (validated before use)
|
|
318
|
+
self.owner: Optional[DTree] = owner # None once the node has been removed
|
|
319
|
+
|
|
320
|
+
@property
|
|
321
|
+
def parent(self) -> Optional["DNode"]:
|
|
322
|
+
return self.up
|
|
323
|
+
|
|
324
|
+
@property
|
|
325
|
+
def children(self) -> List["DNode"]:
|
|
326
|
+
return list(self.kids or ())
|
|
327
|
+
|
|
328
|
+
@property
|
|
329
|
+
def is_in_tree(self) -> bool:
|
|
330
|
+
return self.owner is not None
|
|
331
|
+
|
|
332
|
+
def __repr__(self) -> str:
|
|
333
|
+
return f"DNode({self.name!r})" if self.owner is not None else f"DNode({self.name!r}, removed)"
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
_BUDGET = 8 # steps of the plain walk to the end of a subtree before the validated hint is used
|
|
337
|
+
|
|
338
|
+
|
|
299
339
|
class DTree:
|
|
300
340
|
"""Single-rooted directed tree with dual projections Q1 and Q2."""
|
|
301
341
|
|
|
302
342
|
def __init__(self, tree_id: str = "P1") -> None:
|
|
303
343
|
self.tree_id = tree_id
|
|
304
|
-
self.
|
|
305
|
-
self.
|
|
344
|
+
self._nodes: Dict[str, DNode] = {}
|
|
345
|
+
self._root: Optional[DNode] = None
|
|
306
346
|
self._q1 = DList(f"{tree_id}.Q1")
|
|
307
347
|
self._q2 = DList(f"{tree_id}.Q2")
|
|
308
348
|
self.edge_log: List[str] = []
|
|
@@ -328,128 +368,228 @@ class DTree:
|
|
|
328
368
|
tree.insert(parent.strip(), child)
|
|
329
369
|
return tree
|
|
330
370
|
|
|
331
|
-
def insert(self, parent
|
|
332
|
-
"""Add the edge parent -> child
|
|
371
|
+
def insert(self, parent, child: str) -> DNode:
|
|
372
|
+
"""Add the edge parent -> child and return the handle of ``child``.
|
|
333
373
|
|
|
334
|
-
The first call makes ``parent`` the root.
|
|
335
|
-
already exist and ``child`` must not.
|
|
374
|
+
``parent`` is a name or a handle. The first call makes ``parent`` the root.
|
|
375
|
+
Afterwards ``parent`` must already exist and ``child`` must not.
|
|
336
376
|
"""
|
|
337
|
-
if
|
|
377
|
+
if child in (SOURCE, SINK) or (isinstance(parent, str) and parent in (SOURCE, SINK)):
|
|
338
378
|
raise ValueError("SOURCE/SINK markers cannot be tree nodes")
|
|
339
|
-
if
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
379
|
+
if self._root is None:
|
|
380
|
+
if isinstance(parent, DNode):
|
|
381
|
+
raise ValueError("the first insert names the root by its name")
|
|
382
|
+
self._root = self._new_node(parent, None)
|
|
383
|
+
p = self._node(parent)
|
|
384
|
+
if child in self._nodes:
|
|
344
385
|
raise ValueError(f"child {child!r} is already in the tree")
|
|
345
|
-
self.
|
|
346
|
-
self.edge_log.append(f"({
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
self._q1
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
386
|
+
n = self._new_node(child, p)
|
|
387
|
+
self.edge_log.append(f"({p.name}, {child})")
|
|
388
|
+
return n
|
|
389
|
+
|
|
390
|
+
def _new_node(self, name: str, p: Optional[DNode]) -> DNode:
|
|
391
|
+
n = DNode(name, self)
|
|
392
|
+
q1, q2 = self._q1, self._q2
|
|
393
|
+
if p is None: # the root
|
|
394
|
+
c1, c2 = q1._new(name), q2._new(name)
|
|
395
|
+
q1._link_after(c1, q1.first)
|
|
396
|
+
q2._link_after(c2, q2.first)
|
|
397
|
+
else:
|
|
398
|
+
w = self._subtree_last_node(p) # Q1: after the end of p's subtree
|
|
399
|
+
c2 = q2._new(name) # Q2: right after p
|
|
400
|
+
q2._link_after(c2, p.c2)
|
|
401
|
+
c1 = q1._new(name)
|
|
402
|
+
q1._link_after(c1, w.c1)
|
|
403
|
+
n.up = p
|
|
404
|
+
if p.kids is None:
|
|
405
|
+
p.kids = [n]
|
|
406
|
+
else:
|
|
407
|
+
p.kids.append(n)
|
|
408
|
+
p.last = n # n is now the last node of p's subtree
|
|
409
|
+
c1.node = c2.node = n
|
|
410
|
+
n.c1, n.c2 = c1, c2
|
|
411
|
+
self._nodes[name] = n
|
|
412
|
+
return n
|
|
413
|
+
|
|
414
|
+
# -- the end of a subtree in Q1 ---------------------------------------
|
|
415
|
+
@staticmethod
|
|
416
|
+
def _in(p: DNode, x: DNode) -> bool:
|
|
417
|
+
"""x is p or lies in p's subtree."""
|
|
418
|
+
if x is p:
|
|
419
|
+
return True
|
|
420
|
+
a, b = p.c1, x.c1
|
|
421
|
+
if not a.num * b.den < b.num * a.den:
|
|
422
|
+
return False
|
|
423
|
+
a, b = p.c2, x.c2
|
|
424
|
+
return a.num * b.den < b.num * a.den
|
|
425
|
+
|
|
426
|
+
def _is_last(self, p: DNode, x: Optional[DNode]) -> bool:
|
|
427
|
+
"""x is the last node of p's subtree in Q1: x lies in it and x's Q1 successor does not."""
|
|
428
|
+
if x is None or x.owner is not self or not self._in(p, x):
|
|
429
|
+
return False
|
|
430
|
+
nxt = x.c1.next
|
|
431
|
+
return nxt.den == 0 or not self._in(p, nxt.node)
|
|
432
|
+
|
|
433
|
+
def _subtree_last_node(self, p: DNode) -> DNode:
|
|
434
|
+
"""Last node of p's subtree in Q1.
|
|
435
|
+
|
|
436
|
+
A short walk down the last children finds it on ordinary trees. Beyond _BUDGET steps,
|
|
437
|
+
the end remembered last time (p.last) is used if it validates in O(1); otherwise the
|
|
438
|
+
walk continues, stops at a node whose own hint validates, and every node passed gets
|
|
439
|
+
the found end as its new hint (path compression). Stale hints are never used, so the
|
|
440
|
+
hints of ancestors need no update when a subtree grows.
|
|
365
441
|
"""
|
|
366
|
-
|
|
367
|
-
w
|
|
368
|
-
|
|
369
|
-
|
|
442
|
+
w, k = p, 0
|
|
443
|
+
while w.kids and k < _BUDGET:
|
|
444
|
+
w = w.kids[-1]
|
|
445
|
+
k += 1
|
|
446
|
+
if not w.kids:
|
|
447
|
+
return w
|
|
448
|
+
if self._is_last(p, p.last):
|
|
449
|
+
return p.last
|
|
450
|
+
path, w = [p], p
|
|
451
|
+
while w.kids:
|
|
452
|
+
w = w.kids[-1]
|
|
453
|
+
if self._is_last(w, w.last):
|
|
454
|
+
w = w.last
|
|
455
|
+
break
|
|
456
|
+
path.append(w)
|
|
457
|
+
for v in path:
|
|
458
|
+
v.last = w
|
|
459
|
+
return w
|
|
460
|
+
|
|
461
|
+
@staticmethod
|
|
462
|
+
def _subtree_first_last_q2(p: DNode) -> DNode:
|
|
463
|
+
"""Last node of p's subtree in Q2 (siblings reversed): follow first children."""
|
|
464
|
+
w = p
|
|
465
|
+
while w.kids:
|
|
466
|
+
w = w.kids[0]
|
|
370
467
|
return w
|
|
371
468
|
|
|
469
|
+
def subtree_last(self, node):
|
|
470
|
+
"""Last node of the subtree of ``node`` in Q1, as a name (the subtree is the block from
|
|
471
|
+
``node`` to it). Usually O(1) when asked again for the same node."""
|
|
472
|
+
return self._subtree_last_node(self._node(node)).name
|
|
473
|
+
|
|
372
474
|
# -- structural reconfiguration ---------------------------------------
|
|
373
|
-
def delete(self, node
|
|
475
|
+
def delete(self, node) -> None:
|
|
374
476
|
"""Remove ``node``; its children are re-attached to its parent.
|
|
375
477
|
|
|
376
478
|
Deleting the root is disallowed.
|
|
377
479
|
"""
|
|
378
|
-
self.
|
|
379
|
-
if self.
|
|
480
|
+
n = self._node(node)
|
|
481
|
+
if n is self._root:
|
|
380
482
|
raise ValueError("the root cannot be deleted")
|
|
381
|
-
|
|
382
|
-
kids =
|
|
383
|
-
|
|
384
|
-
i =
|
|
385
|
-
siblings[i : i + 1] = kids
|
|
483
|
+
p = n.up
|
|
484
|
+
kids = n.kids or []
|
|
485
|
+
i = p.kids.index(n)
|
|
486
|
+
p.kids[i : i + 1] = kids
|
|
386
487
|
for k in kids:
|
|
387
|
-
|
|
388
|
-
if not
|
|
389
|
-
|
|
390
|
-
self._q1.remove(
|
|
391
|
-
self._q2.remove(
|
|
488
|
+
k.up = p
|
|
489
|
+
if not p.kids:
|
|
490
|
+
p.kids = None
|
|
491
|
+
self._q1.remove(n.name)
|
|
492
|
+
self._q2.remove(n.name)
|
|
493
|
+
self._forget(n)
|
|
392
494
|
self._reshapes += 1
|
|
393
495
|
|
|
394
|
-
def delete_subtree(self, node
|
|
496
|
+
def delete_subtree(self, node) -> List[str]:
|
|
395
497
|
"""Remove ``node`` together with all its descendants.
|
|
396
498
|
|
|
397
499
|
Returns the removed nodes in Q1 order. Deleting the root's subtree
|
|
398
500
|
(the whole tree) is disallowed, as for delete.
|
|
399
501
|
"""
|
|
400
|
-
self.
|
|
401
|
-
if self.
|
|
502
|
+
n = self._node(node)
|
|
503
|
+
if n is self._root:
|
|
402
504
|
raise ValueError("the root cannot be deleted")
|
|
403
|
-
last1 = self.
|
|
404
|
-
last2 = self.
|
|
405
|
-
removed = self._q1.cut_block(
|
|
406
|
-
self._q2.cut_block(
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
if not
|
|
410
|
-
|
|
505
|
+
last1 = self._subtree_last_node(n)
|
|
506
|
+
last2 = self._subtree_first_last_q2(n)
|
|
507
|
+
removed = self._q1.cut_block(n.name, last1.name)
|
|
508
|
+
self._q2.cut_block(n.name, last2.name)
|
|
509
|
+
p = n.up
|
|
510
|
+
p.kids.remove(n)
|
|
511
|
+
if not p.kids:
|
|
512
|
+
p.kids = None
|
|
411
513
|
for v in removed:
|
|
412
|
-
|
|
413
|
-
self._children.pop(v, None)
|
|
514
|
+
self._forget(self._nodes[v])
|
|
414
515
|
self._reshapes += 1
|
|
415
516
|
return removed
|
|
416
517
|
|
|
417
|
-
def cut_n_paste(self, subtree_root
|
|
518
|
+
def cut_n_paste(self, subtree_root, new_parent) -> None:
|
|
418
519
|
"""Detach the subtree rooted at ``subtree_root`` and attach it under ``new_parent``.
|
|
419
520
|
|
|
420
521
|
The subtree keeps its internal shape and becomes the last child of
|
|
421
522
|
``new_parent``. ``new_parent`` must not lie inside the moved subtree,
|
|
422
523
|
and the root cannot be moved.
|
|
423
524
|
"""
|
|
424
|
-
self.
|
|
425
|
-
self.
|
|
426
|
-
if self.
|
|
525
|
+
u = self._node(subtree_root)
|
|
526
|
+
p = self._node(new_parent)
|
|
527
|
+
if u is self._root:
|
|
427
528
|
raise ValueError("the root cannot be moved")
|
|
428
|
-
if
|
|
529
|
+
if p is u or self._in(u, p):
|
|
429
530
|
raise ValueError("new_parent lies inside the subtree being moved")
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
531
|
+
last1 = self._subtree_last_node(u)
|
|
532
|
+
last2 = self._subtree_first_last_q2(u)
|
|
533
|
+
block1 = self._q1.cut_block(u.name, last1.name)
|
|
534
|
+
block2 = self._q2.cut_block(u.name, last2.name)
|
|
535
|
+
moved = [self._nodes[v] for v in block1]
|
|
536
|
+
for v in moved:
|
|
537
|
+
v.owner = None # detached for a moment: their hints and cells are not valid
|
|
538
|
+
old = u.up
|
|
539
|
+
old.kids.remove(u)
|
|
540
|
+
if not old.kids:
|
|
541
|
+
old.kids = None
|
|
439
542
|
# Same placement rule as insert: Q2 right after the parent,
|
|
440
543
|
# Q1 right after the current last node of the parent's subtree.
|
|
441
|
-
last = self.
|
|
442
|
-
self._q2.paste_block_after(block2,
|
|
443
|
-
self._q1.paste_block_after(block1, last)
|
|
444
|
-
|
|
544
|
+
last = self._subtree_last_node(p)
|
|
545
|
+
self._q2.paste_block_after(block2, p.name)
|
|
546
|
+
self._q1.paste_block_after(block1, last.name)
|
|
547
|
+
for v in moved: # the pasted cells are new objects
|
|
548
|
+
v.owner = self
|
|
549
|
+
v.c1, v.c2 = self._q1._cells[v.name], self._q2._cells[v.name]
|
|
550
|
+
v.c1.node = v.c2.node = v
|
|
551
|
+
u.up = p
|
|
552
|
+
if p.kids is None:
|
|
553
|
+
p.kids = [u]
|
|
554
|
+
else:
|
|
555
|
+
p.kids.append(u)
|
|
556
|
+
p.last = last1
|
|
445
557
|
self._reshapes += 1
|
|
446
|
-
|
|
558
|
+
|
|
559
|
+
def _forget(self, n: DNode) -> None:
|
|
560
|
+
del self._nodes[n.name]
|
|
561
|
+
n.owner = None
|
|
562
|
+
n.up = n.kids = n.last = None
|
|
563
|
+
|
|
564
|
+
# -- handles and names -------------------------------------------------
|
|
565
|
+
def get_node(self, name: str) -> DNode:
|
|
566
|
+
"""Handle of the node called ``name``. Raises KeyError if absent."""
|
|
567
|
+
n = self._nodes.get(name)
|
|
568
|
+
if n is None:
|
|
569
|
+
raise KeyError(f"node {name!r} is not in the tree")
|
|
570
|
+
return n
|
|
571
|
+
|
|
572
|
+
def try_get_node(self, name: str) -> Optional[DNode]:
|
|
573
|
+
return self._nodes.get(name)
|
|
574
|
+
|
|
575
|
+
def _node(self, x) -> DNode:
|
|
576
|
+
"""Resolve a name or a handle of this tree; raises KeyError otherwise."""
|
|
577
|
+
if x.__class__ is DNode:
|
|
578
|
+
if x.owner is not self:
|
|
579
|
+
raise KeyError(f"node {x.name!r} is not in this tree")
|
|
580
|
+
return x
|
|
581
|
+
n = self._nodes.get(x)
|
|
582
|
+
if n is None:
|
|
583
|
+
raise KeyError(f"node {x!r} is not in the tree")
|
|
584
|
+
return n
|
|
585
|
+
|
|
586
|
+
def _name(self, x) -> str:
|
|
587
|
+
return x.name if x.__class__ is DNode else x
|
|
447
588
|
|
|
448
589
|
# -- accessors --------------------------------------------------------
|
|
449
590
|
@property
|
|
450
591
|
def root(self) -> Optional[str]:
|
|
451
|
-
|
|
452
|
-
return kids[0] if kids else None
|
|
592
|
+
return self._root.name if self._root is not None else None
|
|
453
593
|
|
|
454
594
|
@property
|
|
455
595
|
def source(self) -> str:
|
|
@@ -483,110 +623,138 @@ class DTree:
|
|
|
483
623
|
|
|
484
624
|
@property
|
|
485
625
|
def size(self) -> int:
|
|
486
|
-
return len(self.
|
|
626
|
+
return len(self._nodes)
|
|
487
627
|
|
|
488
628
|
@property
|
|
489
629
|
def nodes(self) -> List[str]:
|
|
490
630
|
return self.q1
|
|
491
631
|
|
|
632
|
+
@property
|
|
633
|
+
def _parent(self) -> Dict[str, str]:
|
|
634
|
+
"""child name -> parent name (root -> SOURCE); a snapshot, e.g. for comparisons."""
|
|
635
|
+
return {n.name: (n.up.name if n.up is not None else SOURCE) for n in self._nodes.values()}
|
|
636
|
+
|
|
492
637
|
def __len__(self) -> int:
|
|
493
638
|
return self.size
|
|
494
639
|
|
|
495
|
-
def __contains__(self, node
|
|
640
|
+
def __contains__(self, node) -> bool:
|
|
496
641
|
return self.contains(node)
|
|
497
642
|
|
|
498
643
|
def __repr__(self) -> str:
|
|
499
644
|
return f"DTree({self.tree_id!r}, size={self.size})\n {self._q1!r}\n {self._q2!r}"
|
|
500
645
|
|
|
501
646
|
# -- queries ----------------------------------------------------------
|
|
502
|
-
def contains(self, node
|
|
503
|
-
|
|
647
|
+
def contains(self, node) -> bool:
|
|
648
|
+
if node.__class__ is DNode:
|
|
649
|
+
return node.owner is self
|
|
650
|
+
return node in self._nodes
|
|
504
651
|
|
|
505
|
-
def get_parent(self, node
|
|
652
|
+
def get_parent(self, node) -> Optional[str]:
|
|
506
653
|
"""Parent of ``node``; ``None`` for the root. Raises KeyError if absent."""
|
|
507
|
-
self.
|
|
508
|
-
p
|
|
509
|
-
return None if p == SOURCE else p
|
|
654
|
+
p = self._node(node).up
|
|
655
|
+
return p.name if p is not None else None
|
|
510
656
|
|
|
511
|
-
def children(self, node
|
|
512
|
-
self.
|
|
513
|
-
return list(self._children.get(node, []))
|
|
657
|
+
def children(self, node) -> List[str]:
|
|
658
|
+
return [k.name for k in (self._node(node).kids or ())]
|
|
514
659
|
|
|
515
|
-
def can_reach(self, u
|
|
516
|
-
"""True if there is a directed path of length >= 1 from u to v.
|
|
660
|
+
def can_reach(self, u, v) -> bool:
|
|
661
|
+
"""True if there is a directed path of length >= 1 from u to v (names or handles).
|
|
517
662
|
|
|
518
|
-
Irreflexive: can_reach(u, u) is False.
|
|
663
|
+
Irreflexive: can_reach(u, u) is False. With handles: two position comparisons.
|
|
519
664
|
"""
|
|
520
|
-
if u
|
|
665
|
+
if u.__class__ is DNode:
|
|
666
|
+
if u.owner is not self:
|
|
667
|
+
return False
|
|
668
|
+
else:
|
|
669
|
+
u = self._nodes.get(u)
|
|
670
|
+
if u is None:
|
|
671
|
+
return False
|
|
672
|
+
if v.__class__ is DNode:
|
|
673
|
+
if v.owner is not self:
|
|
674
|
+
return False
|
|
675
|
+
else:
|
|
676
|
+
v = self._nodes.get(v)
|
|
677
|
+
if v is None:
|
|
678
|
+
return False
|
|
679
|
+
a, b = u.c1, v.c1
|
|
680
|
+
if not a.num * b.den < b.num * a.den:
|
|
521
681
|
return False
|
|
522
|
-
|
|
682
|
+
a, b = u.c2, v.c2
|
|
683
|
+
return a.num * b.den < b.num * a.den
|
|
523
684
|
|
|
524
|
-
def can_not_reach(self, u
|
|
685
|
+
def can_not_reach(self, u, v) -> bool:
|
|
525
686
|
return not self.can_reach(u, v)
|
|
526
687
|
|
|
527
|
-
def is_root(self, node
|
|
528
|
-
return node
|
|
688
|
+
def is_root(self, node) -> bool:
|
|
689
|
+
return self.contains(node) and self._node(node) is self._root
|
|
529
690
|
|
|
530
|
-
def is_leaf(self, node
|
|
531
|
-
self.
|
|
532
|
-
return node not in self._children
|
|
691
|
+
def is_leaf(self, node) -> bool:
|
|
692
|
+
return not self._node(node).kids
|
|
533
693
|
|
|
534
|
-
def is_ancestor(self, u
|
|
694
|
+
def is_ancestor(self, u, v) -> bool:
|
|
535
695
|
"""True if u is a proper ancestor of v (u lies above v on v's root path)."""
|
|
536
|
-
return self.
|
|
696
|
+
return self.can_reach(u, v)
|
|
537
697
|
|
|
538
|
-
def is_descendant(self, u
|
|
698
|
+
def is_descendant(self, u, v) -> bool:
|
|
539
699
|
"""True if u lies below v. Equivalent to is_ancestor(v, u)."""
|
|
540
|
-
return self.
|
|
700
|
+
return self.can_reach(v, u)
|
|
541
701
|
|
|
542
|
-
def same_subtree(self, u
|
|
702
|
+
def same_subtree(self, u, v) -> bool:
|
|
543
703
|
"""True if both nodes are in this tree (and so share its root)."""
|
|
544
704
|
return self.contains(u) and self.contains(v)
|
|
545
705
|
|
|
546
|
-
def subtree(self, node
|
|
706
|
+
def subtree(self, node) -> List[str]:
|
|
547
707
|
"""Nodes of the subtree rooted at ``node``, in Q1 order."""
|
|
548
|
-
self.
|
|
549
|
-
out,
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
708
|
+
n = self._node(node)
|
|
709
|
+
out, c = [n.name], n.c1.next
|
|
710
|
+
end = self._subtree_last_node(n).c1
|
|
711
|
+
if end is n.c1:
|
|
712
|
+
return out
|
|
713
|
+
while True:
|
|
714
|
+
out.append(c.name)
|
|
715
|
+
if c is end:
|
|
716
|
+
return out
|
|
717
|
+
c = c.next
|
|
554
718
|
|
|
555
|
-
def subtree_size(self, node
|
|
719
|
+
def subtree_size(self, node) -> int:
|
|
556
720
|
return len(self.subtree(node))
|
|
557
721
|
|
|
558
|
-
def path_to_root(self, node
|
|
722
|
+
def path_to_root(self, node) -> List[str]:
|
|
559
723
|
"""[node, parent, ..., root]."""
|
|
560
|
-
self.
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
v = self._parent[v]
|
|
724
|
+
out, w = [], self._node(node)
|
|
725
|
+
while w is not None:
|
|
726
|
+
out.append(w.name)
|
|
727
|
+
w = w.up
|
|
565
728
|
return out
|
|
566
729
|
|
|
567
|
-
def root_to_path(self, node
|
|
730
|
+
def root_to_path(self, node) -> List[str]:
|
|
568
731
|
"""[root, ..., parent, node]."""
|
|
569
732
|
return self.path_to_root(node)[::-1]
|
|
570
733
|
|
|
571
|
-
def path(self, u
|
|
734
|
+
def path(self, u, v) -> List[str]:
|
|
572
735
|
"""Nodes from u down to v, both included; [u] when u == v.
|
|
573
736
|
|
|
574
737
|
Raises if u != v and u cannot reach v.
|
|
575
738
|
"""
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
739
|
+
a, b = self._node(u), self._node(v)
|
|
740
|
+
if a is not b and not self.can_reach(a, b):
|
|
741
|
+
raise ValueError(f"{a.name!r} cannot reach {b.name!r}")
|
|
742
|
+
out, w = [b.name], b
|
|
743
|
+
while w is not a:
|
|
744
|
+
w = w.up
|
|
745
|
+
out.append(w.name)
|
|
582
746
|
return out[::-1]
|
|
583
747
|
|
|
584
|
-
def depth(self, node
|
|
748
|
+
def depth(self, node) -> int:
|
|
585
749
|
"""Distance from the root; the root has depth 0."""
|
|
586
|
-
|
|
750
|
+
d, w = 0, self._node(node).up
|
|
751
|
+
while w is not None:
|
|
752
|
+
d += 1
|
|
753
|
+
w = w.up
|
|
754
|
+
return d
|
|
587
755
|
|
|
588
756
|
# -- helpers ----------------------------------------------------------
|
|
589
|
-
# --
|
|
757
|
+
# -- and/or queries on the topology -----------------------------------
|
|
590
758
|
def vmin(self, nodes: Iterable[str] = ()) -> "VMin":
|
|
591
759
|
"""Constructed node VMin: the minimum of each index over the nodes (see VMin)."""
|
|
592
760
|
return VMin(self, nodes)
|
|
@@ -603,9 +771,8 @@ class DTree:
|
|
|
603
771
|
"""Constructed node UMax: the maximum of each index over the nodes (see UMax)."""
|
|
604
772
|
return UMax(self, nodes)
|
|
605
773
|
|
|
606
|
-
def _require(self, node
|
|
607
|
-
|
|
608
|
-
raise KeyError(f"node {node!r} is not in the tree")
|
|
774
|
+
def _require(self, node) -> None:
|
|
775
|
+
self._node(node)
|
|
609
776
|
|
|
610
777
|
|
|
611
778
|
class _VNode:
|
|
@@ -639,7 +806,8 @@ class _VNode:
|
|
|
639
806
|
return q.precedes(v, c) if self._lowest else q.precedes(c, v)
|
|
640
807
|
|
|
641
808
|
def add(self, node: str) -> "_VNode":
|
|
642
|
-
"""Add a node of the tree to the set; returns the constructed node itself."""
|
|
809
|
+
"""Add a node of the tree (name or handle) to the set; returns the constructed node itself."""
|
|
810
|
+
node = self._t._name(node)
|
|
643
811
|
self._t._require(node)
|
|
644
812
|
if node in self._seen:
|
|
645
813
|
return self
|
|
@@ -710,8 +878,9 @@ class VMin(_VNode):
|
|
|
710
878
|
|
|
711
879
|
_lowest = True
|
|
712
880
|
|
|
713
|
-
def reaches_all(self, u
|
|
881
|
+
def reaches_all(self, u) -> bool:
|
|
714
882
|
"""True if u reaches every node of the set (vacuously True for an empty set)."""
|
|
883
|
+
u = self._t._name(u)
|
|
715
884
|
self._check()
|
|
716
885
|
t = self._t
|
|
717
886
|
if not t.contains(u):
|
|
@@ -728,21 +897,23 @@ class VMax(_VNode):
|
|
|
728
897
|
|
|
729
898
|
_lowest = False
|
|
730
899
|
|
|
731
|
-
def may_reach_some(self, u
|
|
900
|
+
def may_reach_some(self, u) -> bool:
|
|
732
901
|
"""Fast rejection: False means u reaches no node of the set. True is not a confirmation."""
|
|
902
|
+
u = self._t._name(u)
|
|
733
903
|
self._check()
|
|
734
904
|
t = self._t
|
|
735
905
|
if self._c1 is None or not t.contains(u):
|
|
736
906
|
return False
|
|
737
907
|
return t._q1.precedes(u, self._c1) and t._q2.precedes(u, self._c2)
|
|
738
908
|
|
|
739
|
-
def reaches_some(self, u
|
|
909
|
+
def reaches_some(self, u) -> bool:
|
|
740
910
|
"""True if u reaches at least one node of the set (exact).
|
|
741
911
|
|
|
742
912
|
The subtree of u is the block directly after u in Q1, so u reaches some member
|
|
743
913
|
exactly when the first member after u in Q1 lies in it: a binary search over the
|
|
744
914
|
members (kept in Q1 order) and one reachability test.
|
|
745
915
|
"""
|
|
916
|
+
u = self._t._name(u)
|
|
746
917
|
if not self.may_reach_some(u):
|
|
747
918
|
return False
|
|
748
919
|
t, m = self._t, self._members
|
|
@@ -763,8 +934,9 @@ class UMax(_VNode):
|
|
|
763
934
|
|
|
764
935
|
_lowest = False
|
|
765
936
|
|
|
766
|
-
def reached_by_all(self, v
|
|
937
|
+
def reached_by_all(self, v) -> bool:
|
|
767
938
|
"""True if every node of the set reaches v (vacuously True for an empty set)."""
|
|
939
|
+
v = self._t._name(v)
|
|
768
940
|
self._check()
|
|
769
941
|
t = self._t
|
|
770
942
|
if not t.contains(v):
|
|
@@ -824,16 +996,18 @@ class UMin(_VNode):
|
|
|
824
996
|
if not (self._tops and t.can_reach(self._tops[-1], v)):
|
|
825
997
|
self._tops.append(v)
|
|
826
998
|
|
|
827
|
-
def may_be_reached_by_some(self, v
|
|
999
|
+
def may_be_reached_by_some(self, v) -> bool:
|
|
828
1000
|
"""Fast rejection: False means no node of the set reaches v; True is not a confirmation."""
|
|
1001
|
+
v = self._t._name(v)
|
|
829
1002
|
self._check()
|
|
830
1003
|
t = self._t
|
|
831
1004
|
if self._c1 is None or not t.contains(v):
|
|
832
1005
|
return False
|
|
833
1006
|
return t._q1.precedes(self._c1, v) and t._q2.precedes(self._c2, v)
|
|
834
1007
|
|
|
835
|
-
def reached_by_some(self, v
|
|
1008
|
+
def reached_by_some(self, v) -> bool:
|
|
836
1009
|
"""True if at least one node of the set reaches v (exact, O(log k))."""
|
|
1010
|
+
v = self._t._name(v)
|
|
837
1011
|
if not self.may_be_reached_by_some(v):
|
|
838
1012
|
return False
|
|
839
1013
|
return self._covered(v)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dtree-encoding
|
|
3
|
-
Version: 1.
|
|
4
|
-
Summary: DTree: incremental encoding of directed trees by two projections (Q1, Q2)
|
|
3
|
+
Version: 1.6.0
|
|
4
|
+
Summary: DTree: incremental encoding of the topology of directed trees by two projections (Q1, Q2): O(1) reachability, and/or queries on the topology of the tree, dynamic (incremental) updates
|
|
5
5
|
Author-email: Enis Olgac <enis@digraphs.info>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/EnisOlgac/DTree
|
|
@@ -9,7 +9,7 @@ Project-URL: Documentation, https://github.com/EnisOlgac/DTree/blob/main/docs/AP
|
|
|
9
9
|
Project-URL: Changelog, https://github.com/EnisOlgac/DTree/blob/main/CHANGELOG.md
|
|
10
10
|
Project-URL: Paper, https://doi.org/10.5772/intechopen.1013408
|
|
11
11
|
Project-URL: Archive, https://doi.org/10.5281/zenodo.23395710
|
|
12
|
-
Keywords: directed tree,reachability,incremental encoding,dynamic tree,graph theory,data structure
|
|
12
|
+
Keywords: directed tree,tree topology,reachability,and/or queries,incremental encoding,dynamic tree,graph theory,data structure
|
|
13
13
|
Classifier: Development Status :: 5 - Production/Stable
|
|
14
14
|
Classifier: Intended Audience :: Science/Research
|
|
15
15
|
Classifier: Intended Audience :: Developers
|
|
@@ -23,7 +23,7 @@ Description-Content-Type: text/markdown
|
|
|
23
23
|
|
|
24
24
|
# DTree for Python
|
|
25
25
|
|
|
26
|
-
Python implementation of `DTree`, the incremental encoding of directed trees by two projections Q1 and Q2 (see the [API reference](https://github.com/EnisOlgac/DTree/blob/main/docs/API.md) and the [paper](https://doi.org/10.5772/intechopen.1013408)). No dependencies; Python 3.8+.
|
|
26
|
+
Python implementation of `DTree`, the incremental encoding of the topology of directed trees by two projections Q1 and Q2 (see the [API reference](https://github.com/EnisOlgac/DTree/blob/main/docs/API.md) and the [paper](https://doi.org/10.5772/intechopen.1013408)). No dependencies; Python 3.8+.
|
|
27
27
|
|
|
28
28
|
```
|
|
29
29
|
pip install dtree-encoding
|
|
@@ -73,18 +73,22 @@ t = DTree.from_file("Test.csx") # e.g. examples/Test.csx from the repository
|
|
|
73
73
|
| `SameSubtree`, `SubtreeSize` | `same_subtree`, `subtree_size` (plus `subtree`) |
|
|
74
74
|
| `Depth` | `depth` (root = 0) |
|
|
75
75
|
| `PathToRoot`, `RootToPath` | `path_to_root`, `root_to_path` (plus `path(u, v)`) |
|
|
76
|
+
| `GetNode`, `TryGetNode`, `DNode` | `get_node`, `try_get_node`, `DNode` (node handles; `insert` returns one) |
|
|
77
|
+
| `SubtreeLast` | `subtree_last` (last node of a subtree in Q1) |
|
|
76
78
|
| `VMin`, `VMax`, `UMin`, `UMax` (of nodes) | `vmin`, `vmax`, `umin`, `umax` |
|
|
77
79
|
| `ReachesAll`, `MayReachSome`, `ReachesSome` | `reaches_all`, `may_reach_some`, `reaches_some` |
|
|
78
80
|
| `ReachedByAll`, `MayBeReachedBySome`, `ReachedBySome` | `reached_by_all`, `may_be_reached_by_some`, `reached_by_some` |
|
|
79
81
|
|
|
80
82
|
## Semantics
|
|
81
83
|
|
|
84
|
+
- Every method accepts a node name or a node handle (`DNode`). `insert` returns the handle of the new node, `get_node(name)` gives the handle of an existing one. A handle refers directly to the node's positions in Q1 and Q2, so `can_reach` on handles is two position comparisons without any lookup by name. Handles of deleted nodes, or of another tree, are rejected (KeyError) or reported as unreachable.
|
|
85
|
+
|
|
82
86
|
- `can_reach(u, v)` is irreflexive: no node reaches itself, so `can_reach(u, u)` is `False`. (The C# prototype is reflexive.)
|
|
83
87
|
- `is_ancestor(u, v)` equals `can_reach(u, v)` and is likewise proper.
|
|
84
88
|
- `delete(v)` re-attaches v's children to v's parent, in v's place among its siblings.
|
|
85
89
|
- `delete_subtree(v)` removes v with all its descendants and returns them in Q1 order.
|
|
86
90
|
- `cut_n_paste(u, p)` moves the subtree rooted at u under p as p's last child; p must not lie inside u's subtree.
|
|
87
|
-
-
|
|
91
|
+
- And/or queries on the topology: the constructed nodes `vmin(S)`, `vmax(S)` (S as target) and `umin(S)`, `umax(S)` (S as source) take the minimum or maximum of each index over S, separately in Q1 and Q2. `reaches_all` and `reached_by_all` are exact O(1) tests; `may_reach_some` and `may_be_reached_by_some` are only filters, and `reaches_some` and `reached_by_some` are exact in O(log k). A constructed node stays valid under insertions and is recomputed automatically after `delete`, `delete_subtree` or `cut_n_paste`. See [and/or queries on the topology](https://github.com/EnisOlgac/DTree/blob/main/docs/and-or-queries.md).
|
|
88
92
|
- Projection positions are fractions: a new node gets the mediant (a+c)/(b+d) of its neighbors, so normally no other node moves. Only rare alternating insertion patterns trigger a local renumbering. Tree depth and width are unbounded. `t.Q1.index(v)` returns a position as a `Fraction`.
|
|
89
93
|
|
|
90
94
|
## Tests
|
|
@@ -104,7 +108,7 @@ python benchmarks/bench.py --dynamic # only insertions and queries alternating
|
|
|
104
108
|
python benchmarks/bench.py 1000000 # custom sizes
|
|
105
109
|
```
|
|
106
110
|
|
|
107
|
-
The script prints Markdown tables: build time and renumbering counts per tree shape (random, chain, star at the root, star at an inner node, alternating worst case), list growth at either end, and timings for `can_reach`, `depth`, `delete`, `cut_n_paste` and `delete_subtree`, and comparisons with a classical parent walk: reachability by tree depth,
|
|
111
|
+
The script prints Markdown tables: build time and renumbering counts per tree shape (random, chain, star at the root, star at an inner node, alternating worst case), list growth at either end, and timings for `can_reach`, `depth`, `delete`, `cut_n_paste` and `delete_subtree`, and comparisons with a classical parent walk: reachability by tree depth, and/or queries with constructed nodes, and dynamic updates (insertions and queries alternating). Each `DList` keeps the counters `renumber_count` and `renumber_moved`, so the same figures are available in your own code via `t.Q1` and `t.Q2`.
|
|
108
112
|
|
|
109
113
|
## Provenance
|
|
110
114
|
|
|
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dtree-encoding"
|
|
7
|
-
version = "1.
|
|
8
|
-
description = "DTree: incremental encoding of directed trees by two projections (Q1, Q2)
|
|
7
|
+
version = "1.6.0"
|
|
8
|
+
description = "DTree: incremental encoding of the topology of directed trees by two projections (Q1, Q2): O(1) reachability, and/or queries on the topology of the tree, dynamic (incremental) updates"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
11
11
|
license = { text = "MIT" }
|
|
12
12
|
authors = [{ name = "Enis Olgac", email = "enis@digraphs.info" }]
|
|
13
|
-
keywords = ["directed tree", "reachability", "incremental encoding", "dynamic tree", "graph theory", "data structure"]
|
|
13
|
+
keywords = ["directed tree", "tree topology", "reachability", "and/or queries", "incremental encoding", "dynamic tree", "graph theory", "data structure"]
|
|
14
14
|
classifiers = [
|
|
15
15
|
"Development Status :: 5 - Production/Stable",
|
|
16
16
|
"Intended Audience :: Science/Research",
|
|
@@ -9,7 +9,7 @@ import unittest
|
|
|
9
9
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
10
10
|
sys.path.insert(0, os.path.dirname(HERE))
|
|
11
11
|
|
|
12
|
-
from dtree import SINK, SOURCE, DTree # noqa: E402
|
|
12
|
+
from dtree import SINK, SOURCE, DNode, DTree # noqa: E402
|
|
13
13
|
|
|
14
14
|
EXAMPLES = os.path.join(HERE, "..", "..", "examples")
|
|
15
15
|
|
|
@@ -242,7 +242,7 @@ class TestDList(unittest.TestCase):
|
|
|
242
242
|
|
|
243
243
|
|
|
244
244
|
class TestVNodes(unittest.TestCase):
|
|
245
|
-
"""
|
|
245
|
+
"""And/or queries on the topology with constructed nodes (Olgac 2021), checked against brute force."""
|
|
246
246
|
|
|
247
247
|
def check_vnodes(self, t, S, vmin=None, vmax=None, umin=None, umax=None):
|
|
248
248
|
vmin = vmin or t.vmin(S)
|
|
@@ -343,6 +343,135 @@ class TestVNodes(unittest.TestCase):
|
|
|
343
343
|
self.check_vnodes(t, S, **v_s)
|
|
344
344
|
|
|
345
345
|
|
|
346
|
+
class TestNodeHandles(unittest.TestCase):
|
|
347
|
+
"""Handles refer directly to a node's positions; names keep working."""
|
|
348
|
+
|
|
349
|
+
def test_handles(self):
|
|
350
|
+
t = DTree()
|
|
351
|
+
b = t.insert("A", "B")
|
|
352
|
+
c = t.insert(b, "C")
|
|
353
|
+
d = t.insert("A", "D")
|
|
354
|
+
a = t.get_node("A")
|
|
355
|
+
self.assertIsInstance(b, DNode)
|
|
356
|
+
self.assertEqual(b.name, "B")
|
|
357
|
+
self.assertTrue(t.can_reach(a, c) and t.can_reach(b, c) and t.can_reach("A", c))
|
|
358
|
+
self.assertFalse(t.can_reach(c, a) or t.can_reach(b, d) or t.can_reach(c, c))
|
|
359
|
+
self.assertTrue(t.is_root(a) and not t.is_root(b))
|
|
360
|
+
self.assertEqual(t.depth(c), 2)
|
|
361
|
+
self.assertIs(c.parent, b)
|
|
362
|
+
self.assertIsNone(a.parent)
|
|
363
|
+
self.assertEqual([k.name for k in a.children], ["B", "D"])
|
|
364
|
+
self.assertIs(t.try_get_node("D"), d)
|
|
365
|
+
self.assertIsNone(t.try_get_node("X"))
|
|
366
|
+
t.cut_n_paste(c, d)
|
|
367
|
+
self.assertTrue(t.can_reach(d, c) and not t.can_reach(b, c))
|
|
368
|
+
self.assertTrue(t.can_reach(t.get_node("A"), t.get_node("C"))) # handles stay valid after a move
|
|
369
|
+
t.delete(b)
|
|
370
|
+
self.assertFalse(b.is_in_tree or t.can_reach(a, b))
|
|
371
|
+
with self.assertRaises(KeyError):
|
|
372
|
+
t.insert(b, "E")
|
|
373
|
+
other = DTree("other")
|
|
374
|
+
x = other.insert("X", "Y")
|
|
375
|
+
self.assertFalse(t.can_reach(a, x))
|
|
376
|
+
with self.assertRaises(KeyError):
|
|
377
|
+
t.delete(x)
|
|
378
|
+
self.assertEqual(t.delete_subtree(d), ["D", "C"])
|
|
379
|
+
self.assertFalse(c.is_in_tree)
|
|
380
|
+
self.assertEqual(t.size, 1)
|
|
381
|
+
|
|
382
|
+
def test_and_or_queries_accept_handles(self):
|
|
383
|
+
t = DTree()
|
|
384
|
+
b = t.insert("A", "B")
|
|
385
|
+
c = t.insert(b, "C")
|
|
386
|
+
d = t.insert("A", "D")
|
|
387
|
+
a = t.get_node("A")
|
|
388
|
+
self.assertTrue(t.vmin([c, d]).reaches_all(a))
|
|
389
|
+
self.assertFalse(t.vmin([c, "D"]).reaches_all(b))
|
|
390
|
+
self.assertTrue(t.vmax([c, d]).reaches_some(b))
|
|
391
|
+
self.assertTrue(t.umax([a, b]).reached_by_all(c))
|
|
392
|
+
self.assertTrue(t.umin([b, d]).reached_by_some(c))
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
class TestSubtreeEnd(unittest.TestCase):
|
|
396
|
+
"""The end of a subtree in Q1 (walk with a budget, validated hint, path compression)."""
|
|
397
|
+
|
|
398
|
+
@staticmethod
|
|
399
|
+
def _ref_last(t, v):
|
|
400
|
+
w = v
|
|
401
|
+
while t.children(w):
|
|
402
|
+
w = t.children(w)[-1]
|
|
403
|
+
return w
|
|
404
|
+
|
|
405
|
+
def test_top_down_comb(self):
|
|
406
|
+
# a chain, then new leaves under the spine nodes from the top down: the plain walk
|
|
407
|
+
# would be O(depth) per insertion here
|
|
408
|
+
t, ref = DTree(), Reference()
|
|
409
|
+
d = 300
|
|
410
|
+
names = ["n0"]
|
|
411
|
+
for i in range(1, d):
|
|
412
|
+
t.insert(names[-1], f"n{i}")
|
|
413
|
+
ref.insert(names[-1], f"n{i}")
|
|
414
|
+
names.append(f"n{i}")
|
|
415
|
+
k = d
|
|
416
|
+
for rnd_ in range(3):
|
|
417
|
+
for i in range(d):
|
|
418
|
+
t.insert(f"n{i}", f"x{k}")
|
|
419
|
+
ref.insert(f"n{i}", f"x{k}")
|
|
420
|
+
k += 1
|
|
421
|
+
check(self, t, ref)
|
|
422
|
+
for v in random.Random(1).sample(t.nodes, 50):
|
|
423
|
+
self.assertEqual(t.subtree_last(v), self._ref_last(t, v))
|
|
424
|
+
|
|
425
|
+
def test_inserts_mixed_with_deletes_and_moves(self):
|
|
426
|
+
self._mixed()
|
|
427
|
+
|
|
428
|
+
def test_hint_path_always(self):
|
|
429
|
+
# budget 0: every lookup of a subtree's end goes through the validated hint
|
|
430
|
+
import dtree.dtree as mod
|
|
431
|
+
saved = mod._BUDGET
|
|
432
|
+
mod._BUDGET = 0
|
|
433
|
+
try:
|
|
434
|
+
self._mixed()
|
|
435
|
+
self.test_top_down_comb()
|
|
436
|
+
finally:
|
|
437
|
+
mod._BUDGET = saved
|
|
438
|
+
|
|
439
|
+
def _mixed(self):
|
|
440
|
+
for seed in range(25):
|
|
441
|
+
t, ref, rnd = random_tree(40, seed)
|
|
442
|
+
k = 40
|
|
443
|
+
for step in range(150):
|
|
444
|
+
nodes = ref.nodes()
|
|
445
|
+
inner = [v for v in nodes if v != ref.root]
|
|
446
|
+
r = rnd.random()
|
|
447
|
+
if r < 0.55 or len(inner) < 8:
|
|
448
|
+
# favor deep parents and the spine of last children
|
|
449
|
+
p = rnd.choice(nodes) if rnd.random() < 0.5 else self._ref_last(t, rnd.choice(nodes))
|
|
450
|
+
if rnd.random() < 0.3:
|
|
451
|
+
p = ref.root
|
|
452
|
+
t.insert(p, f"x{k}")
|
|
453
|
+
ref.insert(p, f"x{k}")
|
|
454
|
+
k += 1
|
|
455
|
+
elif r < 0.7:
|
|
456
|
+
v = rnd.choice(inner)
|
|
457
|
+
t.delete(v)
|
|
458
|
+
ref.delete(v)
|
|
459
|
+
elif r < 0.8:
|
|
460
|
+
v = rnd.choice(inner)
|
|
461
|
+
t.delete_subtree(v)
|
|
462
|
+
ref.delete_subtree(v)
|
|
463
|
+
else:
|
|
464
|
+
u = rnd.choice(inner)
|
|
465
|
+
banned = set(t.subtree(u))
|
|
466
|
+
p = rnd.choice([w for w in nodes if w not in banned])
|
|
467
|
+
t.cut_n_paste(u, p)
|
|
468
|
+
ref.move(u, p)
|
|
469
|
+
if step % 10 == 0:
|
|
470
|
+
for v in rnd.sample(ref.nodes(), min(10, len(ref.nodes()))):
|
|
471
|
+
self.assertEqual(t.subtree_last(v), self._ref_last(t, v))
|
|
472
|
+
check(self, t, ref)
|
|
473
|
+
|
|
474
|
+
|
|
346
475
|
class TestAgainstReference(unittest.TestCase):
|
|
347
476
|
def test_random_inserts(self):
|
|
348
477
|
for seed in range(30):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|