Mongoosetross commited on
Commit
7d7cdbe
·
verified ·
1 Parent(s): b7fcb98

Fast deploy (index c3ce686a86bd)

Browse files
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
- # Weak binary edges (relation ``other``, including ``{{root}}`` category shortcuts)
588
- # are deferred until after strong etymons + formation hyperedges are expanded.
589
- # Otherwise a page-level ``{{root|en|ine-pro|*wed-}}`` edge marks *wed- visited at
590
- # depth 1, and the later *wódr̥ = *wed- + *r̥ hyperedge can only attach orphan *r̥.
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
- weak_anc_rels = {REL_TO_CODE["other"]}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
597
 
598
  if include_ancestors and max_anc_depth > 0 and fwd is not None:
599
  aq: deque[int] = deque([root])
600
 
601
- def consider_anc(etymon: int, via: int, rel_code: int, conf_code: int) -> bool:
 
602
  nonlocal anc_visited, visited, truncated
603
- if etymon == root or etymon in anc_depth:
604
  return False
605
  # Prefer descendant placement if already reached going down.
606
  if etymon in depth_of:
607
  return False
608
- anc_depth[etymon] = anc_depth[via] + 1
609
- anc_parent[etymon] = via
610
- anc_rel_code[etymon] = rel_code
611
- anc_conf_code[etymon] = conf_code
612
- anc_visited += 1
613
- visited += 1
614
- if visited >= max_visit:
615
- truncated = True
616
- aq.clear()
617
- return True
618
- aq.append(etymon)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
619
  return False
620
 
621
- def expand_strong_and_formations(node: int) -> bool:
622
- """Follow inherited/derived/... edges and hyperedge parts. Skip weak ``other``."""
623
  d = anc_depth[node]
624
  if d >= max_anc_depth:
625
- return False
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
- if consider_anc(etymon, node, rel_c, int(fwd.conf[e])):
636
- return True
637
- # Formation parts of this word (compositional etymons), one hop.
 
 
 
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
- if consider_anc(int(part), node, rel_c, conf_c):
654
- return True
655
- return False
656
-
657
- def expand_weak_others(node: int) -> bool:
658
- """Attach ``other``/root shortcuts only if not already claimed by a formation."""
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-BCsMQuux.js"></script>
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>