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.
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dtree-encoding
3
- Version: 1.5.0
4
- Summary: DTree: incremental encoding of directed trees by two projections (Q1, Q2) with O(1) reachability and set queries
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
- - 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 [set queries](https://github.com/EnisOlgac/DTree/blob/main/docs/set-queries.md).
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, set 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`.
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
- - 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 [set queries](https://github.com/EnisOlgac/DTree/blob/main/docs/set-queries.md).
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, set 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`.
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
 
@@ -0,0 +1,6 @@
1
+ """DTree: directed tree library with dual projections Q1 and Q2."""
2
+
3
+ from .dtree import SINK, SOURCE, DList, DNode, DTree, UMax, UMin, VMax, VMin
4
+
5
+ __all__ = ["DTree", "DNode", "DList", "VMin", "VMax", "UMin", "UMax", "SOURCE", "SINK"]
6
+ __version__ = "1.6.0"
@@ -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._parent: Dict[str, str] = {} # child -> parent (root -> SOURCE)
305
- self._children: Dict[str, List[str]] = {} # parent -> children, insertion order
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: str, child: str) -> None:
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. Afterwards ``parent`` must
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 parent in (SOURCE, SINK) or child in (SOURCE, SINK):
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 not self._parent:
340
- self._attach(SOURCE, parent)
341
- elif parent not in self._parent:
342
- raise KeyError(f"parent {parent!r} is not in the tree")
343
- if child in self._parent:
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._attach(parent, child)
346
- self.edge_log.append(f"({parent}, {child})")
347
-
348
- def _attach(self, parent: str, child: str) -> None:
349
- end = self._subtree_end_q1(parent)
350
- self._q2.insert_after(child, parent)
351
- self._q1.insert_before(child, end)
352
- self._parent[child] = parent
353
- self._children.setdefault(parent, []).append(child)
354
-
355
- def _subtree_end_q1(self, u: str) -> str:
356
- """First node after u in Q1 that u cannot reach (may be SINK)."""
357
- return self._q1.next(self._subtree_last(u, self._q1))
358
-
359
- def _subtree_last(self, u: str, q: DList) -> str:
360
- """Last node of u's subtree in projection q, found in O(depth).
361
-
362
- Q1 lists children in insertion order, so its last node is reached by
363
- following last children; Q2 reverses siblings, so by following first
364
- children.
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
- pick = -1 if q is self._q1 else 0
367
- w = u
368
- while w in self._children:
369
- w = self._children[w][pick]
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: str) -> None:
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._require(node)
379
- if self.is_root(node):
480
+ n = self._node(node)
481
+ if n is self._root:
380
482
  raise ValueError("the root cannot be deleted")
381
- parent = self._parent.pop(node)
382
- kids = self._children.pop(node, [])
383
- siblings = self._children[parent]
384
- i = siblings.index(node)
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
- self._parent[k] = parent
388
- if not siblings:
389
- del self._children[parent]
390
- self._q1.remove(node)
391
- self._q2.remove(node)
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: str) -> List[str]:
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._require(node)
401
- if self.is_root(node):
502
+ n = self._node(node)
503
+ if n is self._root:
402
504
  raise ValueError("the root cannot be deleted")
403
- last1 = self._subtree_last(node, self._q1)
404
- last2 = self._subtree_last(node, self._q2)
405
- removed = self._q1.cut_block(node, last1)
406
- self._q2.cut_block(node, last2)
407
- parent = self._parent[node]
408
- self._children[parent].remove(node)
409
- if not self._children[parent]:
410
- del self._children[parent]
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
- del self._parent[v]
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: str, new_parent: str) -> None:
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._require(subtree_root)
425
- self._require(new_parent)
426
- if self.is_root(subtree_root):
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 new_parent == subtree_root or self.can_reach(subtree_root, new_parent):
529
+ if p is u or self._in(u, p):
429
530
  raise ValueError("new_parent lies inside the subtree being moved")
430
- u = subtree_root
431
- last1 = self._subtree_last(u, self._q1)
432
- last2 = self._subtree_last(u, self._q2)
433
- block1 = self._q1.cut_block(u, last1)
434
- block2 = self._q2.cut_block(u, last2)
435
- old = self._parent[u]
436
- self._children[old].remove(u)
437
- if not self._children[old]:
438
- del self._children[old]
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._subtree_last(new_parent, self._q1)
442
- self._q2.paste_block_after(block2, new_parent)
443
- self._q1.paste_block_after(block1, last)
444
- self._parent[u] = new_parent
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
- self._children.setdefault(new_parent, []).append(u)
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
- kids = self._children.get(SOURCE)
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._parent)
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: str) -> bool:
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: str) -> bool:
503
- return node in self._parent
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: str) -> Optional[str]:
652
+ def get_parent(self, node) -> Optional[str]:
506
653
  """Parent of ``node``; ``None`` for the root. Raises KeyError if absent."""
507
- self._require(node)
508
- p = self._parent[node]
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: str) -> List[str]:
512
- self._require(node)
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: str, v: str) -> bool:
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 not in self._q1 or v not in self._q1:
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
- return self._q1.precedes(u, v) and self._q2.precedes(u, v)
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: str, v: str) -> bool:
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: str) -> bool:
528
- return node == self.root
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: str) -> bool:
531
- self._require(node)
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: str, v: str) -> bool:
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.contains(u) and self.contains(v) and self.can_reach(u, v)
696
+ return self.can_reach(u, v)
537
697
 
538
- def is_descendant(self, u: str, v: str) -> bool:
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.is_ancestor(v, u)
700
+ return self.can_reach(v, u)
541
701
 
542
- def same_subtree(self, u: str, v: str) -> bool:
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: str) -> List[str]:
706
+ def subtree(self, node) -> List[str]:
547
707
  """Nodes of the subtree rooted at ``node``, in Q1 order."""
548
- self._require(node)
549
- out, v = [node], self._q1.next(node)
550
- while v != SINK and self.can_reach(node, v):
551
- out.append(v)
552
- v = self._q1.next(v)
553
- return out
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: str) -> int:
719
+ def subtree_size(self, node) -> int:
556
720
  return len(self.subtree(node))
557
721
 
558
- def path_to_root(self, node: str) -> List[str]:
722
+ def path_to_root(self, node) -> List[str]:
559
723
  """[node, parent, ..., root]."""
560
- self._require(node)
561
- out, v = [], node
562
- while v != SOURCE:
563
- out.append(v)
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: str) -> List[str]:
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: str, v: str) -> List[str]:
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
- if u != v and not self.can_reach(u, v):
577
- raise ValueError(f"{u!r} cannot reach {v!r}")
578
- out, w = [v], v
579
- while w != u:
580
- w = self._parent[w]
581
- out.append(w)
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: str) -> int:
748
+ def depth(self, node) -> int:
585
749
  """Distance from the root; the root has depth 0."""
586
- return len(self.path_to_root(node)) - 1
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
- # -- set queries ------------------------------------------------------
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: str) -> None:
607
- if node not in self._parent:
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: str) -> bool:
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: str) -> bool:
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: str) -> bool:
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: str) -> bool:
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: str) -> bool:
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: str) -> bool:
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.5.0
4
- Summary: DTree: incremental encoding of directed trees by two projections (Q1, Q2) with O(1) reachability and set queries
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
- - 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 [set queries](https://github.com/EnisOlgac/DTree/blob/main/docs/set-queries.md).
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, set 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`.
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.5.0"
8
- description = "DTree: incremental encoding of directed trees by two projections (Q1, Q2) with O(1) reachability and set queries"
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
- """Set queries with constructed nodes (Olgac 2021), checked against brute force."""
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):
@@ -1,6 +0,0 @@
1
- """DTree: directed tree library with dual projections Q1 and Q2."""
2
-
3
- from .dtree import SINK, SOURCE, DList, DTree, UMax, UMin, VMax, VMin
4
-
5
- __all__ = ["DTree", "DList", "VMin", "VMax", "UMin", "UMax", "SOURCE", "SINK"]
6
- __version__ = "1.5.0"
File without changes