dtree-encoding 1.7.0__tar.gz → 1.8.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.7.0 → dtree_encoding-1.8.0}/PKG-INFO +4 -2
- {dtree_encoding-1.7.0 → dtree_encoding-1.8.0}/README.md +3 -1
- {dtree_encoding-1.7.0 → dtree_encoding-1.8.0}/dtree/__init__.py +1 -1
- {dtree_encoding-1.7.0 → dtree_encoding-1.8.0}/dtree/dtree.py +75 -5
- {dtree_encoding-1.7.0 → dtree_encoding-1.8.0}/dtree_encoding.egg-info/PKG-INFO +4 -2
- {dtree_encoding-1.7.0 → dtree_encoding-1.8.0}/pyproject.toml +1 -1
- {dtree_encoding-1.7.0 → dtree_encoding-1.8.0}/tests/test_dtree.py +142 -0
- {dtree_encoding-1.7.0 → dtree_encoding-1.8.0}/dtree/bench.py +0 -0
- {dtree_encoding-1.7.0 → dtree_encoding-1.8.0}/dtree_encoding.egg-info/SOURCES.txt +0 -0
- {dtree_encoding-1.7.0 → dtree_encoding-1.8.0}/dtree_encoding.egg-info/dependency_links.txt +0 -0
- {dtree_encoding-1.7.0 → dtree_encoding-1.8.0}/dtree_encoding.egg-info/top_level.txt +0 -0
- {dtree_encoding-1.7.0 → dtree_encoding-1.8.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dtree-encoding
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.8.0
|
|
4
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
|
|
@@ -59,7 +59,9 @@ t = DTree.from_file("Test.csx") # e.g. examples/Test.csx from the repository
|
|
|
59
59
|
| C# (docs) | Python |
|
|
60
60
|
|---|---|
|
|
61
61
|
| `DTree(id)` | `DTree(tree_id="P1")` |
|
|
62
|
-
| `Insert(parent, child)` | `insert(parent, child)` |
|
|
62
|
+
| `Insert(parent, child)` | `insert(parent, child)` (last child) |
|
|
63
|
+
| `InsertAfter`, `InsertBefore` | `insert_after(sibling, child)`, `insert_before(sibling, child)` |
|
|
64
|
+
| `AddLine` | `add_line(line)` (`p: c`, `s > c`, `s < c`; also in `from_file`) |
|
|
63
65
|
| `Contains(node)` | `contains(node)` / `node in t` |
|
|
64
66
|
| `GetParent(node)` | `get_parent(node)` (`None` for the root) |
|
|
65
67
|
| `CutNPaste(u, p)` | `cut_n_paste(subtree_root, new_parent)` |
|
|
@@ -36,7 +36,9 @@ t = DTree.from_file("Test.csx") # e.g. examples/Test.csx from the repository
|
|
|
36
36
|
| C# (docs) | Python |
|
|
37
37
|
|---|---|
|
|
38
38
|
| `DTree(id)` | `DTree(tree_id="P1")` |
|
|
39
|
-
| `Insert(parent, child)` | `insert(parent, child)` |
|
|
39
|
+
| `Insert(parent, child)` | `insert(parent, child)` (last child) |
|
|
40
|
+
| `InsertAfter`, `InsertBefore` | `insert_after(sibling, child)`, `insert_before(sibling, child)` |
|
|
41
|
+
| `AddLine` | `add_line(line)` (`p: c`, `s > c`, `s < c`; also in `from_file`) |
|
|
40
42
|
| `Contains(node)` | `contains(node)` / `node in t` |
|
|
41
43
|
| `GetParent(node)` | `get_parent(node)` (`None` for the root) |
|
|
42
44
|
| `CutNPaste(u, p)` | `cut_n_paste(subtree_root, new_parent)` |
|
|
@@ -351,7 +351,12 @@ class DTree:
|
|
|
351
351
|
# -- construction ------------------------------------------------------
|
|
352
352
|
@classmethod
|
|
353
353
|
def from_file(cls, path: str, tree_id: Optional[str] = None) -> "DTree":
|
|
354
|
-
"""Read the
|
|
354
|
+
"""Read the format used by the .csx examples.
|
|
355
|
+
|
|
356
|
+
``parent: child1; child2`` adds children as last children of ``parent``;
|
|
357
|
+
``sibling > child1; child2`` inserts them as siblings directly after ``sibling``
|
|
358
|
+
(in that order), ``sibling < child1; child2`` directly before it.
|
|
359
|
+
"""
|
|
355
360
|
import os
|
|
356
361
|
|
|
357
362
|
tree = cls(tree_id or os.path.splitext(os.path.basename(path))[0])
|
|
@@ -362,12 +367,27 @@ class DTree:
|
|
|
362
367
|
continue
|
|
363
368
|
if line.startswith("@@"):
|
|
364
369
|
break
|
|
365
|
-
|
|
366
|
-
for child in (c.strip() for c in rest.split(";")):
|
|
367
|
-
if child:
|
|
368
|
-
tree.insert(parent.strip(), child)
|
|
370
|
+
tree.add_line(line)
|
|
369
371
|
return tree
|
|
370
372
|
|
|
373
|
+
def add_line(self, line: str) -> List[DNode]:
|
|
374
|
+
"""Apply one line of the description format (see from_file); returns the new handles."""
|
|
375
|
+
at = min((i for i in (line.find(":"), line.find(">"), line.find("<")) if i >= 0), default=-1)
|
|
376
|
+
if at < 0:
|
|
377
|
+
raise ValueError(f"no ':', '>' or '<' in {line!r}")
|
|
378
|
+
op, anchor = line[at], line[:at].strip()
|
|
379
|
+
children = [c.strip() for c in line[at + 1:].split(";") if c.strip()]
|
|
380
|
+
added = []
|
|
381
|
+
for child in children:
|
|
382
|
+
if op == ":":
|
|
383
|
+
added.append(self.insert(anchor, child))
|
|
384
|
+
elif op == ">":
|
|
385
|
+
added.append(self.insert_after(anchor, child))
|
|
386
|
+
anchor = child # the next one goes after this one
|
|
387
|
+
else:
|
|
388
|
+
added.append(self.insert_before(anchor, child))
|
|
389
|
+
return added
|
|
390
|
+
|
|
371
391
|
def insert(self, parent, child: str) -> DNode:
|
|
372
392
|
"""Add the edge parent -> child and return the handle of ``child``.
|
|
373
393
|
|
|
@@ -387,6 +407,56 @@ class DTree:
|
|
|
387
407
|
self.edge_log.append(f"({p.name}, {child})")
|
|
388
408
|
return n
|
|
389
409
|
|
|
410
|
+
def insert_after(self, sibling, child: str) -> DNode:
|
|
411
|
+
"""Insert ``child`` as a sibling directly after ``sibling`` (same parent) and return its
|
|
412
|
+
handle. In Q1 it goes after the end of the sibling's subtree, in Q2 directly before the
|
|
413
|
+
sibling; no other node moves. The root has no siblings."""
|
|
414
|
+
s = self._sibling(sibling, child)
|
|
415
|
+
p = s.up
|
|
416
|
+
i = p.kids.index(s)
|
|
417
|
+
w = self._subtree_last_node(s)
|
|
418
|
+
n = self._link_node(child, p, i + 1, w.c1, s.c2.prev)
|
|
419
|
+
if i + 1 == len(p.kids) - 1:
|
|
420
|
+
p.last = n # n is now the last node of p's subtree
|
|
421
|
+
self.edge_log.append(f"({s.name} > {child})")
|
|
422
|
+
return n
|
|
423
|
+
|
|
424
|
+
def insert_before(self, sibling, child: str) -> DNode:
|
|
425
|
+
"""Insert ``child`` as a sibling directly before ``sibling`` (same parent) and return its
|
|
426
|
+
handle. In Q1 it goes directly before the sibling, in Q2 after the end of the sibling's
|
|
427
|
+
subtree there (directly before the previous sibling; for a first child, after the end
|
|
428
|
+
of the parent's subtree in Q2, found by following first children); no other node moves."""
|
|
429
|
+
s = self._sibling(sibling, child)
|
|
430
|
+
p = s.up
|
|
431
|
+
i = p.kids.index(s)
|
|
432
|
+
q2_after = p.kids[i - 1].c2.prev if i > 0 else self._subtree_first_last_q2(s).c2
|
|
433
|
+
n = self._link_node(child, p, i, s.c1.prev, q2_after)
|
|
434
|
+
self.edge_log.append(f"({s.name} < {child})")
|
|
435
|
+
return n
|
|
436
|
+
|
|
437
|
+
def _sibling(self, sibling, child: str) -> DNode:
|
|
438
|
+
if child in (SOURCE, SINK):
|
|
439
|
+
raise ValueError("SOURCE/SINK markers cannot be tree nodes")
|
|
440
|
+
s = self._node(sibling)
|
|
441
|
+
if s.up is None:
|
|
442
|
+
raise ValueError("the root has no siblings")
|
|
443
|
+
if child in self._nodes:
|
|
444
|
+
raise ValueError(f"child {child!r} is already in the tree")
|
|
445
|
+
return s
|
|
446
|
+
|
|
447
|
+
def _link_node(self, name: str, p: DNode, index: int, q1_after, q2_after) -> DNode:
|
|
448
|
+
"""Create a child of p at position ``index`` among p's children, linked after the given cells."""
|
|
449
|
+
n = DNode(name, self)
|
|
450
|
+
c1, c2 = self._q1._new(name), self._q2._new(name)
|
|
451
|
+
self._q2._link_after(c2, q2_after)
|
|
452
|
+
self._q1._link_after(c1, q1_after)
|
|
453
|
+
n.up = p
|
|
454
|
+
p.kids.insert(index, n)
|
|
455
|
+
c1.node = c2.node = n
|
|
456
|
+
n.c1, n.c2 = c1, c2
|
|
457
|
+
self._nodes[name] = n
|
|
458
|
+
return n
|
|
459
|
+
|
|
390
460
|
def _new_node(self, name: str, p: Optional[DNode]) -> DNode:
|
|
391
461
|
n = DNode(name, self)
|
|
392
462
|
q1, q2 = self._q1, self._q2
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dtree-encoding
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.8.0
|
|
4
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
|
|
@@ -59,7 +59,9 @@ t = DTree.from_file("Test.csx") # e.g. examples/Test.csx from the repository
|
|
|
59
59
|
| C# (docs) | Python |
|
|
60
60
|
|---|---|
|
|
61
61
|
| `DTree(id)` | `DTree(tree_id="P1")` |
|
|
62
|
-
| `Insert(parent, child)` | `insert(parent, child)` |
|
|
62
|
+
| `Insert(parent, child)` | `insert(parent, child)` (last child) |
|
|
63
|
+
| `InsertAfter`, `InsertBefore` | `insert_after(sibling, child)`, `insert_before(sibling, child)` |
|
|
64
|
+
| `AddLine` | `add_line(line)` (`p: c`, `s > c`, `s < c`; also in `from_file`) |
|
|
63
65
|
| `Contains(node)` | `contains(node)` / `node in t` |
|
|
64
66
|
| `GetParent(node)` | `get_parent(node)` (`None` for the root) |
|
|
65
67
|
| `CutNPaste(u, p)` | `cut_n_paste(subtree_root, new_parent)` |
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dtree-encoding"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.8.0"
|
|
8
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"
|
|
@@ -68,6 +68,18 @@ class Reference:
|
|
|
68
68
|
for n in gone:
|
|
69
69
|
del self.parent[n]
|
|
70
70
|
|
|
71
|
+
def insert_after(self, s, v):
|
|
72
|
+
p = self.parent[s]
|
|
73
|
+
sib = self.children[p]
|
|
74
|
+
sib.insert(sib.index(s) + 1, v)
|
|
75
|
+
self.parent[v] = p
|
|
76
|
+
|
|
77
|
+
def insert_before(self, s, v):
|
|
78
|
+
p = self.parent[s]
|
|
79
|
+
sib = self.children[p]
|
|
80
|
+
sib.insert(sib.index(s), v)
|
|
81
|
+
self.parent[v] = p
|
|
82
|
+
|
|
71
83
|
def move(self, u, p):
|
|
72
84
|
old = self.parent[u]
|
|
73
85
|
self.children[old].remove(u)
|
|
@@ -559,6 +571,136 @@ class TestAgainstReference(unittest.TestCase):
|
|
|
559
571
|
check(self, t, ref)
|
|
560
572
|
|
|
561
573
|
|
|
574
|
+
class TestSiblingInsert(unittest.TestCase):
|
|
575
|
+
def test_example(self):
|
|
576
|
+
t = DTree()
|
|
577
|
+
t.insert("A", "B")
|
|
578
|
+
t.insert("A", "D")
|
|
579
|
+
t.insert("B", "C")
|
|
580
|
+
t.insert_after("B", "X") # A: B, X, D
|
|
581
|
+
t.insert_before("B", "W") # A: W, B, X, D
|
|
582
|
+
t.insert_before("C", "C0") # B: C0, C
|
|
583
|
+
self.assertEqual(t.children("A"), ["W", "B", "X", "D"])
|
|
584
|
+
self.assertEqual(t.children("B"), ["C0", "C"])
|
|
585
|
+
self.assertEqual(t.q1, ["A", "W", "B", "C0", "C", "X", "D"])
|
|
586
|
+
self.assertEqual(t.q2, ["A", "D", "X", "B", "C", "C0", "W"])
|
|
587
|
+
self.assertTrue(t.can_reach("A", "X") and not t.can_reach("B", "X"))
|
|
588
|
+
self.assertEqual(t.subtree_last("A"), "D")
|
|
589
|
+
|
|
590
|
+
def test_rules(self):
|
|
591
|
+
t = DTree()
|
|
592
|
+
t.insert("A", "B")
|
|
593
|
+
with self.assertRaises(ValueError):
|
|
594
|
+
t.insert_after("A", "Z") # the root has no siblings
|
|
595
|
+
with self.assertRaises(ValueError):
|
|
596
|
+
t.insert_before("A", "Z")
|
|
597
|
+
with self.assertRaises(ValueError):
|
|
598
|
+
t.insert_after("B", "A") # already in the tree
|
|
599
|
+
with self.assertRaises(KeyError):
|
|
600
|
+
t.insert_after("nobody", "Z")
|
|
601
|
+
n = t.insert_after(t.get_node("B"), "C") # handles work too
|
|
602
|
+
self.assertIsInstance(n, DNode)
|
|
603
|
+
self.assertEqual(t.children("A"), ["B", "C"])
|
|
604
|
+
|
|
605
|
+
def test_example_file(self):
|
|
606
|
+
t = DTree.from_file(os.path.join(EXAMPLES, "siblings.csx"))
|
|
607
|
+
self.assertEqual(t.children("01"), ["02", "04", "06", "05"])
|
|
608
|
+
self.assertEqual(t.q1, ["01", "02", "03a", "03", "04", "06", "05"])
|
|
609
|
+
self.assertEqual(t.q2, ["01", "05", "06", "04", "02", "03", "03a"])
|
|
610
|
+
|
|
611
|
+
def test_description_lines(self):
|
|
612
|
+
t = DTree()
|
|
613
|
+
t.add_line("A: B; D")
|
|
614
|
+
t.add_line("B > X; Y") # X after B, Y after X
|
|
615
|
+
t.add_line("D < W1; W2") # W1 before D, W2 before D
|
|
616
|
+
t.add_line("B: C")
|
|
617
|
+
self.assertEqual(t.children("A"), ["B", "X", "Y", "W1", "W2", "D"])
|
|
618
|
+
self.assertEqual(t.children("B"), ["C"])
|
|
619
|
+
|
|
620
|
+
def test_random_mix(self):
|
|
621
|
+
# all three kinds of insertion, mixed with deletes and moves, against the reference
|
|
622
|
+
import dtree.dtree as impl
|
|
623
|
+
for budget in (impl._BUDGET, 0):
|
|
624
|
+
saved, impl._BUDGET = impl._BUDGET, budget
|
|
625
|
+
try:
|
|
626
|
+
for seed in range(12):
|
|
627
|
+
t, ref, rnd = random_tree(20, seed)
|
|
628
|
+
k = 0
|
|
629
|
+
for _ in range(100):
|
|
630
|
+
nodes = ref.nodes()
|
|
631
|
+
inner = [n for n in nodes if n != ref.root]
|
|
632
|
+
r = rnd.random()
|
|
633
|
+
name = f"x{k}"
|
|
634
|
+
if r < 0.25:
|
|
635
|
+
p = rnd.choice(nodes)
|
|
636
|
+
t.insert(p, name)
|
|
637
|
+
ref.insert(p, name)
|
|
638
|
+
k += 1
|
|
639
|
+
elif r < 0.5 and inner:
|
|
640
|
+
s = rnd.choice(inner)
|
|
641
|
+
t.insert_after(s, name)
|
|
642
|
+
ref.insert_after(s, name)
|
|
643
|
+
k += 1
|
|
644
|
+
elif r < 0.75 and inner:
|
|
645
|
+
s = rnd.choice(inner)
|
|
646
|
+
t.insert_before(s, name)
|
|
647
|
+
ref.insert_before(s, name)
|
|
648
|
+
k += 1
|
|
649
|
+
elif r < 0.85 and len(inner) > 10:
|
|
650
|
+
v = rnd.choice(inner)
|
|
651
|
+
t.delete(v)
|
|
652
|
+
ref.delete(v)
|
|
653
|
+
elif len(inner) > 10:
|
|
654
|
+
u = rnd.choice(inner)
|
|
655
|
+
banned = set(t.subtree(u))
|
|
656
|
+
p = rnd.choice([n for n in nodes if n not in banned])
|
|
657
|
+
t.cut_n_paste(u, p)
|
|
658
|
+
ref.move(u, p)
|
|
659
|
+
check(self, t, ref)
|
|
660
|
+
for v in rnd.sample(ref.nodes(), 3):
|
|
661
|
+
sub_v = [w for w in ref.nodes() if w == v or v == ref.root or v in ref_ancestors(ref.parent, w)]
|
|
662
|
+
self.assertEqual(t.subtree_last(v), [w for w in t.q1 if w in sub_v][-1])
|
|
663
|
+
finally:
|
|
664
|
+
impl._BUDGET = saved
|
|
665
|
+
|
|
666
|
+
def test_same_gap_many_times(self):
|
|
667
|
+
# repeated insertion into the same gap from both sides: positions stay strictly increasing
|
|
668
|
+
t, ref = DTree(), Reference()
|
|
669
|
+
t.insert("r", "a")
|
|
670
|
+
ref.insert("r", "a")
|
|
671
|
+
t.insert("r", "b")
|
|
672
|
+
ref.insert("r", "b")
|
|
673
|
+
left, right = "a", "b"
|
|
674
|
+
for i in range(400):
|
|
675
|
+
if i % 2:
|
|
676
|
+
t.insert_after(left, f"l{i}")
|
|
677
|
+
ref.insert_after(left, f"l{i}")
|
|
678
|
+
left = f"l{i}"
|
|
679
|
+
else:
|
|
680
|
+
t.insert_before(right, f"r{i}")
|
|
681
|
+
ref.insert_before(right, f"r{i}")
|
|
682
|
+
right = f"r{i}"
|
|
683
|
+
check(self, t, ref)
|
|
684
|
+
|
|
685
|
+
def test_constructed_nodes_stay_valid(self):
|
|
686
|
+
t, ref, rnd = random_tree(60, 3)
|
|
687
|
+
S = rnd.sample(ref.nodes(), 6)
|
|
688
|
+
um, vm = t.umin(S), t.vmax(S)
|
|
689
|
+
for i in range(60):
|
|
690
|
+
s = rnd.choice([n for n in ref.nodes() if n != ref.root])
|
|
691
|
+
if i % 2:
|
|
692
|
+
t.insert_after(s, f"y{i}")
|
|
693
|
+
ref.insert_after(s, f"y{i}")
|
|
694
|
+
else:
|
|
695
|
+
t.insert_before(s, f"y{i}")
|
|
696
|
+
ref.insert_before(s, f"y{i}")
|
|
697
|
+
for v in ref.nodes():
|
|
698
|
+
anc = set(ref_ancestors(ref.parent, v)[1:]) | ({ref.root} if v != ref.root else set())
|
|
699
|
+
self.assertEqual(um.reached_by_some(v), any(m in anc for m in S), v)
|
|
700
|
+
below = {w for w in ref.nodes() if w != v and (v == ref.root or v in ref_ancestors(ref.parent, w))}
|
|
701
|
+
self.assertEqual(vm.reaches_some(v), any(m in below for m in S), v)
|
|
702
|
+
|
|
703
|
+
|
|
562
704
|
class TestNoSizeLimit(unittest.TestCase):
|
|
563
705
|
def test_deep_chain(self):
|
|
564
706
|
t = DTree()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|