Spaces:
Sleeping
Sleeping
Fast deploy (index c3ce686a86bd)
Browse files- backend/atlas.py +127 -64
- frontend/dist/assets/index-DIxRZy10.js +0 -0
- frontend/dist/index.html +1 -1
backend/atlas.py
CHANGED
|
@@ -584,57 +584,100 @@ def walk_tree(atlas: Atlas, query: TreeQuery, root: int, t0: float) -> dict:
|
|
| 584 |
# --- Ancestors (forward CSR): etymons of the selected word ---
|
| 585 |
# Separate maps so a node that is also a descendant keeps its down-tree identity.
|
| 586 |
#
|
| 587 |
-
#
|
| 588 |
-
#
|
| 589 |
-
#
|
| 590 |
-
#
|
| 591 |
anc_parent: dict[int, int] = {}
|
| 592 |
anc_rel_code: dict[int, int] = {}
|
| 593 |
anc_conf_code: dict[int, int] = {}
|
| 594 |
anc_depth: dict[int, int] = {root: 0}
|
|
|
|
|
|
|
|
|
|
| 595 |
anc_visited = 0
|
| 596 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 597 |
|
| 598 |
if include_ancestors and max_anc_depth > 0 and fwd is not None:
|
| 599 |
aq: deque[int] = deque([root])
|
| 600 |
|
| 601 |
-
def
|
|
|
|
| 602 |
nonlocal anc_visited, visited, truncated
|
| 603 |
-
if etymon == root
|
| 604 |
return False
|
| 605 |
# Prefer descendant placement if already reached going down.
|
| 606 |
if etymon in depth_of:
|
| 607 |
return False
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 619 |
return False
|
| 620 |
|
| 621 |
-
|
| 622 |
-
|
| 623 |
d = anc_depth[node]
|
| 624 |
if d >= max_anc_depth:
|
| 625 |
-
|
| 626 |
start = int(fwd.offsets[node])
|
| 627 |
end = int(fwd.offsets[node + 1])
|
|
|
|
| 628 |
# Etymon walk is unfiltered by descendant edge settings — users still
|
| 629 |
# want to see where a word comes from even when browsing only borrowings etc.
|
| 630 |
for e in range(start, end):
|
| 631 |
-
rel_c = int(fwd.rel[e])
|
| 632 |
-
if rel_c in weak_anc_rels:
|
| 633 |
-
continue
|
| 634 |
etymon = int(fwd.targets[e])
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
|
|
|
|
|
|
|
|
|
| 638 |
if (
|
| 639 |
getattr(atlas, "par_off", None) is not None
|
| 640 |
and getattr(atlas, "par_hyper", None) is not None
|
|
@@ -650,44 +693,13 @@ def walk_tree(atlas: Atlas, query: TreeQuery, root: int, t0: float) -> dict:
|
|
| 650 |
ha = int(atlas.hyper_part_off[h])
|
| 651 |
hb = int(atlas.hyper_part_off[h + 1])
|
| 652 |
for part in atlas.hyper_part_tgt[ha:hb]:
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
d = anc_depth[node]
|
| 660 |
-
if d >= max_anc_depth:
|
| 661 |
-
return False
|
| 662 |
-
start = int(fwd.offsets[node])
|
| 663 |
-
end = int(fwd.offsets[node + 1])
|
| 664 |
-
for e in range(start, end):
|
| 665 |
-
rel_c = int(fwd.rel[e])
|
| 666 |
-
if rel_c not in weak_anc_rels:
|
| 667 |
-
continue
|
| 668 |
-
etymon = int(fwd.targets[e])
|
| 669 |
-
if consider_anc(etymon, node, rel_c, int(fwd.conf[e])):
|
| 670 |
-
return True
|
| 671 |
-
return False
|
| 672 |
-
|
| 673 |
-
# Phase 1: strong binary etymons + formation hyperedges (full BFS).
|
| 674 |
-
while aq:
|
| 675 |
-
if expand_strong_and_formations(aq.popleft()):
|
| 676 |
-
break
|
| 677 |
-
|
| 678 |
-
# Phase 2: weak ``other`` edges (e.g. {{root}}) for nodes already reached;
|
| 679 |
-
# newly attached targets then get their own strong/formation expansion.
|
| 680 |
-
# Repeat until no new weak targets appear (rare multi-hop other chains).
|
| 681 |
-
while not truncated:
|
| 682 |
-
before = anc_visited
|
| 683 |
-
for node in list(anc_depth.keys()):
|
| 684 |
-
if expand_weak_others(node):
|
| 685 |
-
break
|
| 686 |
-
while aq and not truncated:
|
| 687 |
-
if expand_strong_and_formations(aq.popleft()):
|
| 688 |
break
|
| 689 |
-
if truncated or anc_visited == before:
|
| 690 |
-
break
|
| 691 |
|
| 692 |
has_child: set[int] = set()
|
| 693 |
for nid, p in parent.items():
|
|
@@ -919,6 +931,56 @@ def walk_tree(atlas: Atlas, query: TreeQuery, root: int, t0: float) -> dict:
|
|
| 919 |
}
|
| 920 |
)
|
| 921 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 922 |
hydrate_ids = list(word_ids) + list(anc_ids)
|
| 923 |
extra = atlas.hydrate(hydrate_ids)
|
| 924 |
for node in payload_nodes:
|
|
@@ -962,6 +1024,7 @@ def walk_tree(atlas: Atlas, query: TreeQuery, root: int, t0: float) -> dict:
|
|
| 962 |
**{k: root_info.get(k) for k in ("lang_display", "family_name", "macroarea", "latitude", "longitude")},
|
| 963 |
},
|
| 964 |
"nodes": payload_nodes,
|
|
|
|
| 965 |
"stats": {
|
| 966 |
"visited": int(visited),
|
| 967 |
"kept_leaves": int(kept_leaves),
|
|
|
|
| 584 |
# --- Ancestors (forward CSR): etymons of the selected word ---
|
| 585 |
# Separate maps so a node that is also a descendant keeps its down-tree identity.
|
| 586 |
#
|
| 587 |
+
# The etymon graph is a DAG: one word can have many outbound etymon edges, and the
|
| 588 |
+
# same etymon can be reached more than once (e.g. water → *wed- via {{root}} and
|
| 589 |
+
# *wódr̥ → *wed- as a formation part). We record every edge. ``anc_parent`` is only
|
| 590 |
+
# the layout spine for tidy-tree placement (prefer formation/descent over ``other``).
|
| 591 |
anc_parent: dict[int, int] = {}
|
| 592 |
anc_rel_code: dict[int, int] = {}
|
| 593 |
anc_conf_code: dict[int, int] = {}
|
| 594 |
anc_depth: dict[int, int] = {root: 0}
|
| 595 |
+
# (via, etymon, rel_code, conf_code) — may include multiple edges into the same etymon.
|
| 596 |
+
anc_edges: list[tuple[int, int, int, int]] = []
|
| 597 |
+
anc_edge_seen: set[tuple[int, int, int]] = set()
|
| 598 |
anc_visited = 0
|
| 599 |
+
|
| 600 |
+
def _anc_layout_rank(rel_code: int, via_depth: int) -> tuple[int, int]:
|
| 601 |
+
"""Higher rank wins as the single tidy-tree parent (layout only)."""
|
| 602 |
+
name = RELATIONS[rel_code] if 0 <= rel_code < len(RELATIONS) else "other"
|
| 603 |
+
strength = {
|
| 604 |
+
"compound": 5,
|
| 605 |
+
"blend": 5,
|
| 606 |
+
"inherited": 4,
|
| 607 |
+
"derived": 4,
|
| 608 |
+
"borrowed": 4,
|
| 609 |
+
"calque": 4,
|
| 610 |
+
"clipping": 3,
|
| 611 |
+
"back_formation": 3,
|
| 612 |
+
"abbreviation": 3,
|
| 613 |
+
"other": 1,
|
| 614 |
+
}.get(name, 2)
|
| 615 |
+
# Prefer a deeper via so formation parts hang under the stem, not the page lemma.
|
| 616 |
+
return (strength, via_depth)
|
| 617 |
|
| 618 |
if include_ancestors and max_anc_depth > 0 and fwd is not None:
|
| 619 |
aq: deque[int] = deque([root])
|
| 620 |
|
| 621 |
+
def record_anc_edge(etymon: int, via: int, rel_code: int, conf_code: int) -> bool:
|
| 622 |
+
"""Record an etymon edge. Enqueue on first visit. Never drop duplicate edges."""
|
| 623 |
nonlocal anc_visited, visited, truncated
|
| 624 |
+
if etymon == root:
|
| 625 |
return False
|
| 626 |
# Prefer descendant placement if already reached going down.
|
| 627 |
if etymon in depth_of:
|
| 628 |
return False
|
| 629 |
+
if anc_depth.get(via, 0) >= max_anc_depth:
|
| 630 |
+
return False
|
| 631 |
+
|
| 632 |
+
key = (via, etymon, rel_code)
|
| 633 |
+
if key not in anc_edge_seen:
|
| 634 |
+
anc_edge_seen.add(key)
|
| 635 |
+
anc_edges.append((via, etymon, rel_code, conf_code))
|
| 636 |
+
|
| 637 |
+
first = etymon not in anc_depth
|
| 638 |
+
if first:
|
| 639 |
+
anc_depth[etymon] = anc_depth[via] + 1
|
| 640 |
+
anc_parent[etymon] = via
|
| 641 |
+
anc_rel_code[etymon] = rel_code
|
| 642 |
+
anc_conf_code[etymon] = conf_code
|
| 643 |
+
anc_visited += 1
|
| 644 |
+
visited += 1
|
| 645 |
+
if visited >= max_visit:
|
| 646 |
+
truncated = True
|
| 647 |
+
aq.clear()
|
| 648 |
+
return True
|
| 649 |
+
aq.append(etymon)
|
| 650 |
+
else:
|
| 651 |
+
# Upgrade layout parent if this inbound edge is a better spine choice.
|
| 652 |
+
cur_via = anc_parent[etymon]
|
| 653 |
+
cur_rel = anc_rel_code[etymon]
|
| 654 |
+
if _anc_layout_rank(rel_code, anc_depth[via]) > _anc_layout_rank(
|
| 655 |
+
cur_rel, anc_depth.get(cur_via, 0)
|
| 656 |
+
):
|
| 657 |
+
anc_parent[etymon] = via
|
| 658 |
+
anc_rel_code[etymon] = rel_code
|
| 659 |
+
anc_conf_code[etymon] = conf_code
|
| 660 |
+
anc_depth[etymon] = anc_depth[via] + 1
|
| 661 |
return False
|
| 662 |
|
| 663 |
+
while aq:
|
| 664 |
+
node = aq.popleft()
|
| 665 |
d = anc_depth[node]
|
| 666 |
if d >= max_anc_depth:
|
| 667 |
+
continue
|
| 668 |
start = int(fwd.offsets[node])
|
| 669 |
end = int(fwd.offsets[node + 1])
|
| 670 |
+
stop = False
|
| 671 |
# Etymon walk is unfiltered by descendant edge settings — users still
|
| 672 |
# want to see where a word comes from even when browsing only borrowings etc.
|
| 673 |
for e in range(start, end):
|
|
|
|
|
|
|
|
|
|
| 674 |
etymon = int(fwd.targets[e])
|
| 675 |
+
stop = record_anc_edge(etymon, node, int(fwd.rel[e]), int(fwd.conf[e]))
|
| 676 |
+
if stop:
|
| 677 |
+
break
|
| 678 |
+
if stop:
|
| 679 |
+
break
|
| 680 |
+
# Formation parts of this word (compositional etymons).
|
| 681 |
if (
|
| 682 |
getattr(atlas, "par_off", None) is not None
|
| 683 |
and getattr(atlas, "par_hyper", None) is not None
|
|
|
|
| 693 |
ha = int(atlas.hyper_part_off[h])
|
| 694 |
hb = int(atlas.hyper_part_off[h + 1])
|
| 695 |
for part in atlas.hyper_part_tgt[ha:hb]:
|
| 696 |
+
stop = record_anc_edge(int(part), node, rel_c, conf_c)
|
| 697 |
+
if stop:
|
| 698 |
+
break
|
| 699 |
+
if stop:
|
| 700 |
+
break
|
| 701 |
+
if stop:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 702 |
break
|
|
|
|
|
|
|
| 703 |
|
| 704 |
has_child: set[int] = set()
|
| 705 |
for nid, p in parent.items():
|
|
|
|
| 931 |
}
|
| 932 |
)
|
| 933 |
|
| 934 |
+
# Explicit edge list: tidy-tree ``parent`` is layout-only; DAG multi-edges live here.
|
| 935 |
+
# Descendants currently have one parent each; ancestors may have several inbound edges.
|
| 936 |
+
payload_edges: list[dict] = []
|
| 937 |
+
edge_keys: set[tuple[str, str, str]] = set()
|
| 938 |
+
|
| 939 |
+
def push_edge(
|
| 940 |
+
source: int | str,
|
| 941 |
+
target: int | str,
|
| 942 |
+
relation: str | None,
|
| 943 |
+
confidence: float | None,
|
| 944 |
+
branch: str,
|
| 945 |
+
) -> None:
|
| 946 |
+
rel = relation or ""
|
| 947 |
+
key = (str(source), str(target), rel)
|
| 948 |
+
if key in edge_keys:
|
| 949 |
+
return
|
| 950 |
+
edge_keys.add(key)
|
| 951 |
+
payload_edges.append(
|
| 952 |
+
{
|
| 953 |
+
"source": source,
|
| 954 |
+
"target": target,
|
| 955 |
+
"relation": relation,
|
| 956 |
+
"confidence": confidence,
|
| 957 |
+
"branch": branch,
|
| 958 |
+
}
|
| 959 |
+
)
|
| 960 |
+
|
| 961 |
+
for node in payload_nodes:
|
| 962 |
+
if node.get("parent") is None:
|
| 963 |
+
continue
|
| 964 |
+
push_edge(
|
| 965 |
+
node["parent"],
|
| 966 |
+
node["id"],
|
| 967 |
+
node.get("relation"),
|
| 968 |
+
node.get("confidence"),
|
| 969 |
+
node.get("branch") or "down",
|
| 970 |
+
)
|
| 971 |
+
|
| 972 |
+
anc_keep_ids = set(anc_ids) | {root}
|
| 973 |
+
for via, etymon, rel_c, conf_c in anc_edges:
|
| 974 |
+
if etymon not in anc_keep_ids or via not in anc_keep_ids:
|
| 975 |
+
continue
|
| 976 |
+
push_edge(
|
| 977 |
+
anc_payload_id(via),
|
| 978 |
+
anc_payload_id(etymon),
|
| 979 |
+
RELATIONS[int(rel_c)] if 0 <= rel_c < len(RELATIONS) else None,
|
| 980 |
+
int(conf_c) / 100.0,
|
| 981 |
+
"up",
|
| 982 |
+
)
|
| 983 |
+
|
| 984 |
hydrate_ids = list(word_ids) + list(anc_ids)
|
| 985 |
extra = atlas.hydrate(hydrate_ids)
|
| 986 |
for node in payload_nodes:
|
|
|
|
| 1024 |
**{k: root_info.get(k) for k in ("lang_display", "family_name", "macroarea", "latitude", "longitude")},
|
| 1025 |
},
|
| 1026 |
"nodes": payload_nodes,
|
| 1027 |
+
"edges": payload_edges,
|
| 1028 |
"stats": {
|
| 1029 |
"visited": int(visited),
|
| 1030 |
"kept_leaves": int(kept_leaves),
|
frontend/dist/assets/index-DIxRZy10.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontend/dist/index.html
CHANGED
|
@@ -9,7 +9,7 @@
|
|
| 9 |
<link rel="icon" href="/icon.svg" type="image/svg+xml" />
|
| 10 |
<link rel="apple-touch-icon" href="/icon.svg" />
|
| 11 |
<title>Reverse Etymology Atlas</title>
|
| 12 |
-
<script type="module" crossorigin src="/assets/index-
|
| 13 |
<link rel="stylesheet" crossorigin href="/assets/index-CL00p6UV.css">
|
| 14 |
</head>
|
| 15 |
<body>
|
|
|
|
| 9 |
<link rel="icon" href="/icon.svg" type="image/svg+xml" />
|
| 10 |
<link rel="apple-touch-icon" href="/icon.svg" />
|
| 11 |
<title>Reverse Etymology Atlas</title>
|
| 12 |
+
<script type="module" crossorigin src="/assets/index-DIxRZy10.js"></script>
|
| 13 |
<link rel="stylesheet" crossorigin href="/assets/index-CL00p6UV.css">
|
| 14 |
</head>
|
| 15 |
<body>
|