File size: 11,076 Bytes
421f9b7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
"""
Full Quranic Corpus Parser β€” builds the complete root graph from real data.

Data sources:
  1. roots_lane.json β€” 1,651 roots with Lane's Lexicon definitions,
     frequencies, and morphological forms
  2. quran-morphology.txt β€” Quranic Arabic Corpus v0.4, per-word
     morphological annotation for all 77,430 words

Output:
  - Complete NetworkX graph with:
    - ALL root nodes (with frequency, semantic field, confidence)
    - ALL lemma/form nodes (with pattern, POS, frequency)
    - Derivation edges (root β†’ lemma)
    - Semantic co-occurrence edges (root ↔ root, weighted by verse co-occurrence)
"""

import json
import re
from collections import defaultdict, Counter
from pathlib import Path
import networkx as nx

CORPUS_DIR = Path(__file__).parent / "corpus"


def parse_roots_lane():
    """Parse roots_lane.json β€” all 1,651 roots with morphological forms."""
    path = CORPUS_DIR / "roots_lane.json"
    with open(path, "r", encoding="utf-8") as f:
        data = json.load(f)

    roots = {}
    for entry in data["roots"]:
        root_ar = entry["root"]
        roots[root_ar] = {
            "buckwalter": entry.get("root_buckwalter", ""),
            "frequency": entry.get("quran_frequency", 0),
            "summary_en": entry.get("summary_en", ""),
            "confidence": entry.get("confidence", ""),
            "source": entry.get("source", ""),
            "forms": [],
        }
        for form in entry.get("morphological_forms", []):
            roots[root_ar]["forms"].append({
                "pattern": form.get("form_pattern", ""),
                "arabic": form.get("form_arabic", ""),
                "name": form.get("form_name", ""),
                "category": form.get("form_category", ""),
                "example": form.get("example_word", ""),
                "occurrences": form.get("occurrences", 0),
            })

    return roots, data.get("metadata", {})


def parse_morphology_corpus():
    """
    Parse quran-morphology.txt β€” every word in the Quran with root + lemma.

    Returns:
      word_data: list of (chapter, verse, word_pos, arabic, pos, root, lemma, features_dict)
      verse_roots: dict mapping (chapter, verse) β†’ set of roots in that verse
      root_lemmas: dict mapping root β†’ Counter of lemma occurrences
      root_forms: dict mapping root β†’ Counter of verb form (VF:N) occurrences
    """
    path = CORPUS_DIR / "quran-morphology.txt"

    word_data = []
    verse_roots = defaultdict(set)
    root_lemmas = defaultdict(Counter)
    root_forms = defaultdict(Counter)
    root_pos_counts = defaultdict(Counter)

    with open(path, "r", encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if not line or line.startswith("#"):
                continue

            parts = line.split("\t")
            if len(parts) < 4:
                continue

            position = parts[0]
            arabic = parts[1]
            pos = parts[2]
            features_str = parts[3]

            pos_parts = position.split(":")
            if len(pos_parts) < 3:
                continue
            chapter = int(pos_parts[0])
            verse = int(pos_parts[1])
            word_pos = int(pos_parts[2])

            features = {}
            for feat in features_str.split("|"):
                if ":" in feat:
                    key, val = feat.split(":", 1)
                    features[key] = val
                else:
                    features[feat] = True

            root = features.get("ROOT", None)
            lemma = features.get("LEM", None)
            verb_form = features.get("VF", None)

            if root:
                verse_roots[(chapter, verse)].add(root)
                if lemma:
                    root_lemmas[root][lemma] += 1
                if verb_form:
                    root_forms[root][verb_form] += 1
                root_pos_counts[root][pos] += 1

            word_data.append((chapter, verse, word_pos, arabic, pos, root, lemma, features))

    return word_data, dict(verse_roots), dict(root_lemmas), dict(root_forms), dict(root_pos_counts)


def build_cooccurrence_edges(verse_roots, min_cooccurrence=3):
    """
    Build semantic co-occurrence edges from verse-level root co-occurrence.

    Two roots that appear in the same verse are semantically related.
    Weight = number of verses they co-occur in.
    """
    cooccurrence = Counter()

    for (chapter, verse), roots in verse_roots.items():
        roots_list = sorted(roots)
        for i in range(len(roots_list)):
            for j in range(i + 1, len(roots_list)):
                pair = (roots_list[i], roots_list[j])
                cooccurrence[pair] += 1

    edges = []
    for (root_a, root_b), count in cooccurrence.items():
        if count >= min_cooccurrence:
            edges.append((root_a, root_b, count))

    edges.sort(key=lambda x: -x[2])
    return edges


def build_full_graph(min_cooccurrence=3):
    """Build the complete Quranic root graph from all data sources."""

    print("Parsing roots_lane.json...")
    roots_data, metadata = parse_roots_lane()
    print(f"  {len(roots_data)} roots loaded")

    print("Parsing quran-morphology.txt...")
    word_data, verse_roots, root_lemmas, root_forms, root_pos = parse_morphology_corpus()
    print(f"  {len(word_data)} word tokens parsed")
    print(f"  {len(verse_roots)} verses processed")
    print(f"  {len(root_lemmas)} unique roots found in morphology corpus")

    print("Building co-occurrence edges...")
    cooc_edges = build_cooccurrence_edges(verse_roots, min_cooccurrence)
    print(f"  {len(cooc_edges)} co-occurrence edges (min {min_cooccurrence} shared verses)")

    G = nx.DiGraph()

    all_roots_in_corpus = set(root_lemmas.keys())
    all_roots_in_lane = set(roots_data.keys())
    corpus_root_freq = Counter()
    for (ch, v), roots in verse_roots.items():
        for r in roots:
            corpus_root_freq[r] += 1

    for root_ar, data in roots_data.items():
        G.add_node(root_ar, type="root", depth=0,
                   frequency=data["frequency"],
                   summary=data["summary_en"],
                   buckwalter=data["buckwalter"],
                   confidence=data["confidence"],
                   num_forms=len(data["forms"]),
                   num_lemmas=len(root_lemmas.get(root_ar, {})))

    for root_ar in all_roots_in_corpus - all_roots_in_lane:
        freq = corpus_root_freq.get(root_ar, 0)
        if freq >= 2:
            G.add_node(root_ar, type="root", depth=0,
                       frequency=freq, summary="", buckwalter="",
                       confidence="corpus_only", num_forms=0,
                       num_lemmas=len(root_lemmas.get(root_ar, {})))

    for root_ar, data in roots_data.items():
        for form in data["forms"]:
            form_name = form["example"] or form["arabic"]
            if not form_name:
                continue
            node_id = f"{root_ar}:{form_name}"
            depth = 1 if form["pattern"].startswith("form_") else 2
            G.add_node(node_id, type="derived", depth=depth,
                       arabic=form_name, pattern=form["pattern"],
                       pattern_name=form["name"],
                       category=form["category"],
                       frequency=form["occurrences"],
                       source_root=root_ar)
            G.add_edge(root_ar, node_id, relation="derivation",
                       pattern=form["pattern"])

    for root_ar, lemmas in root_lemmas.items():
        if not G.has_node(root_ar):
            continue
        for lemma, count in lemmas.most_common():
            node_id = f"{root_ar}:{lemma}"
            if G.has_node(node_id):
                continue
            G.add_node(node_id, type="derived", depth=2,
                       arabic=lemma, pattern="corpus",
                       frequency=count, source_root=root_ar)
            G.add_edge(root_ar, node_id, relation="derivation",
                       pattern="corpus", weight=count)

    for root_a, root_b, count in cooc_edges:
        if G.has_node(root_a) and G.has_node(root_b):
            G.add_edge(root_a, root_b, relation="semantic",
                       co_occurrence=count)
            G.add_edge(root_b, root_a, relation="semantic",
                       co_occurrence=count)

    return G, metadata


def print_stats(G):
    """Print comprehensive graph statistics."""
    roots = [n for n, d in G.nodes(data=True) if d.get("type") == "root"]
    derived = [n for n, d in G.nodes(data=True) if d.get("type") == "derived"]

    deriv_edges = [(u, v) for u, v, d in G.edges(data=True) if d.get("relation") == "derivation"]
    sem_edges = [(u, v) for u, v, d in G.edges(data=True) if d.get("relation") == "semantic"]

    root_freqs = sorted([G.nodes[n].get("frequency", 0) for n in roots], reverse=True)

    print(f"\n{'='*60}")
    print(f"QURANIC ROOT GRAPH β€” COMPLETE")
    print(f"{'='*60}")
    print(f"  Root nodes:        {len(roots)}")
    print(f"  Derived nodes:     {len(derived)}")
    print(f"  Total nodes:       {G.number_of_nodes()}")
    print(f"  Derivation edges:  {len(deriv_edges)}")
    print(f"  Semantic edges:    {len(sem_edges)} (co-occurrence)")
    print(f"  Total edges:       {G.number_of_edges()}")
    print(f"\n  Top 20 roots by Quranic frequency:")
    top_roots = sorted(roots, key=lambda n: G.nodes[n].get("frequency", 0), reverse=True)[:20]
    for i, r in enumerate(top_roots, 1):
        d = G.nodes[r]
        summ = d.get("summary", "")[:60]
        print(f"    {i:2d}. {r:8s}  freq={d['frequency']:5d}  forms={d.get('num_forms',0):2d}  {summ}")

    print(f"\n  Frequency distribution:")
    print(f"    Roots with freq > 100: {sum(1 for f in root_freqs if f > 100)}")
    print(f"    Roots with freq 10-100: {sum(1 for f in root_freqs if 10 <= f <= 100)}")
    print(f"    Roots with freq 1-9:    {sum(1 for f in root_freqs if 1 <= f < 10)}")

    top_cooc = sorted(
        [(u, v, d["co_occurrence"]) for u, v, d in G.edges(data=True)
         if d.get("relation") == "semantic" and u < v],
        key=lambda x: -x[2]
    )[:15]
    print(f"\n  Top 15 co-occurring root pairs:")
    for r1, r2, count in top_cooc:
        s1 = G.nodes[r1].get("summary", "")[:25]
        s2 = G.nodes[r2].get("summary", "")[:25]
        print(f"    {r1:8s} <-> {r2:8s}  co_occ={count:4d}  ({s1} / {s2})")


def save_graph(G, path):
    """Save graph as JSON for reloading."""
    nodes = {}
    for n, d in G.nodes(data=True):
        nodes[n] = dict(d)

    edges = []
    for u, v, d in G.edges(data=True):
        edges.append({"source": u, "target": v, **d})

    with open(path, "w", encoding="utf-8") as f:
        json.dump({"nodes": nodes, "edges": edges}, f, ensure_ascii=False, indent=1)
    print(f"\n  Graph saved to {path}")


if __name__ == "__main__":
    import sys
    min_co = int(sys.argv[1]) if len(sys.argv) > 1 else 3
    G, meta = build_full_graph(min_cooccurrence=min_co)
    print_stats(G)
    save_graph(G, "E:/ABU/NEOCORTEX/data/full_graph.json")