Spaces:
Sleeping
Sleeping
| """ | |
| arXivis pipeline β project papers onto the brain-university concept manifold. | |
| For each paper (arXiv slice + defense corpus): | |
| 1. Extract activated concepts via case-insensitive concept-name match. | |
| 2. Score activations by TF-IDF over the concept vocabulary. | |
| 3. Compute information-theoretic novelty: | |
| novelty_concept_bits = sum(-log p(c)) over activated concepts | |
| novelty_edge_bits = #activated pairs never co-occurring before | |
| in chronological-prior corpus | |
| 4. Emit `design/arxivis_data.js` with PAPERS array ranked by total novelty. | |
| Output schema (window.ARXIVIS): | |
| { | |
| papers: [{ | |
| id, title, source, year, url, | |
| concepts: [{id, weight}], # top-k activated wiki concept ids | |
| new_terms: [str], # high-tfidf terms not in vocab | |
| pair_edges: [[id_a, id_b], ...], # activated concept pairs | |
| novelty_bits: float, | |
| pitch: str, # 30-sec elevator pitch | |
| category_hint: str, # arxiv cat or defense topic | |
| }], | |
| concept_freq: {wiki_id: count}, | |
| edge_freq: {"a|b": count}, | |
| generated_at: iso8601 | |
| } | |
| Run: | |
| python3 scripts/build_arxivis_data.py | |
| python3 scripts/build_arxivis_data.py --max-papers 800 --top-k 12 | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import math | |
| import re | |
| import sys | |
| import time | |
| from collections import Counter, defaultdict | |
| from datetime import datetime, timezone | |
| from pathlib import Path | |
| PROJECT_ROOT = Path(__file__).parent.parent | |
| sys.path.insert(0, str(PROJECT_ROOT)) | |
| from agents.arxivis_innovations import extract_innovations, classify_against_vocab | |
| from agents.arxivis_rigor import score_paper as score_rigor | |
| WIKI_DIR = PROJECT_ROOT / "wiki" / "concepts" | |
| ARXIV_DIR = PROJECT_ROOT / "data" / "raw" / "arxiv" | |
| DEFENSE_MANI = PROJECT_ROOT / "data" / "defense_corpus" / "MANIFEST.jsonl" | |
| OUT_PATH = PROJECT_ROOT / "design" / "arxivis_data.js" | |
| TOKEN_RE = re.compile(r"[a-zA-Z][a-zA-Z0-9_-]{2,}") | |
| SENT_SPLIT = re.compile(r"(?<=[.!?])\s+(?=[A-Z])") | |
| STOP = { | |
| "the","and","for","from","with","this","that","via","into","based","using", | |
| "study","studies","approach","method","methods","model","models","system", | |
| "systems","analysis","data","paper","novel","new","review","survey", | |
| "introduction","framework","general","applied","applications","such","also", | |
| "show","shown","first","work","works","results","result","case","cases", | |
| "problem","problems","number","time","function","functions","theorem", | |
| "section","figure","table","note","notes","proof","proofs","corollary", | |
| "lemma","appendix","section","arxiv","abstract","keywords","conclusion", | |
| "however","therefore","hence","while","since","still","both","either", | |
| "many","most","more","much","very","each","some","when","where","what", | |
| "which","than","then","there","these","those","upon","over","under","above", | |
| "below","between","across","through","within","without","along","around", | |
| "another","others","may","can","could","would","should","might","must", | |
| } | |
| # ββ Concept vocabulary βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def load_concept_vocab() -> tuple[dict[str, dict], dict[str, str]]: | |
| """ | |
| Return (concepts, name_index). | |
| concepts[wiki_id] = { | |
| id, display, slug, tokens, name_lc, page_terms (Counter) | |
| } | |
| name_index[name_lc] = wiki_id | |
| """ | |
| concepts: dict[str, dict] = {} | |
| name_index: dict[str, str] = {} | |
| for p in sorted(WIKI_DIR.glob("*.md")): | |
| slug = p.stem # raw filename stem | |
| display = slug.replace("_", " ") | |
| name_lc = display.lower() | |
| wiki_id = f"wiki:{slug}" | |
| # page body terms | |
| text = p.read_text(errors="ignore") | |
| page_terms = Counter( | |
| t.lower() for t in TOKEN_RE.findall(text) | |
| if t.lower() not in STOP and len(t) > 3 | |
| ) | |
| # synonyms = parenthetical aliases in name e.g. "(MST)" | |
| aliases: list[str] = [] | |
| if "(" in display and ")" in display: | |
| inner = display[display.find("(") + 1: display.rfind(")")] | |
| aliases.append(inner.lower().strip()) | |
| concepts[wiki_id] = { | |
| "id": wiki_id, | |
| "display": display, | |
| "slug": slug, | |
| "name_lc": name_lc, | |
| "aliases": aliases, | |
| "page_terms": page_terms, | |
| } | |
| name_index[name_lc] = wiki_id | |
| for a in aliases: | |
| if a and a not in name_index: | |
| name_index[a] = wiki_id | |
| return concepts, name_index | |
| # ββ Paper loaders ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def load_arxiv_papers(max_per_cat: int) -> list[dict]: | |
| """arXiv text papers; use first ~2000 chars.""" | |
| out = [] | |
| if not ARXIV_DIR.exists(): | |
| return out | |
| for cat_dir in sorted(ARXIV_DIR.iterdir()): | |
| if not cat_dir.is_dir(): | |
| continue | |
| n = 0 | |
| for p in sorted(cat_dir.glob("*.txt")): | |
| if n >= max_per_cat: | |
| break | |
| try: | |
| text = p.read_text(errors="ignore") | |
| except Exception: | |
| continue | |
| # first line ~ title (until blank), rest = body | |
| head = text[:200].strip() | |
| title = (head.splitlines()[0] or p.stem).strip() | |
| out.append({ | |
| "id": f"arx:{p.stem}", | |
| "title": title, | |
| "source": "arXiv", | |
| "year": _parse_year_from_arxiv(p.stem), | |
| "url": f"https://arxiv.org/abs/{p.stem}", | |
| "category_hint": cat_dir.name, | |
| "text": text[:2500], | |
| }) | |
| n += 1 | |
| return out | |
| def _parse_year_from_arxiv(stem: str) -> int | None: | |
| m = re.match(r"^(\d{2})(\d{2})", stem) | |
| if not m: | |
| return None | |
| yy = int(m.group(1)) | |
| # arXiv yyMM: 25 β 2025, 95 β 1995 (heuristic; arXiv runs from 2007) | |
| return 2000 + yy if yy < 90 else 1900 + yy | |
| def load_defense_papers() -> list[dict]: | |
| if not DEFENSE_MANI.exists(): | |
| return [] | |
| out = [] | |
| for line in DEFENSE_MANI.read_text().splitlines(): | |
| line = line.strip() | |
| if not line: | |
| continue | |
| try: | |
| o = json.loads(line) | |
| except json.JSONDecodeError: | |
| continue | |
| title = (o.get("title") or "").strip() | |
| abstract = (o.get("abstract") or "").strip() | |
| if not title and not abstract: | |
| continue | |
| year = None | |
| if o.get("date"): | |
| m = re.match(r"(\d{4})", str(o["date"])) | |
| if m: | |
| year = int(m.group(1)) | |
| src = o.get("source", "defense") | |
| # Normalize: arxiv-defense β "arXiv"; everything else uppercase | |
| src_label = "arXiv" if src.lower() == "arxiv" else src.upper() | |
| out.append({ | |
| "id": f"def:{o.get('id')}", | |
| "title": title, | |
| "source": src_label, | |
| "year": year, | |
| "url": o.get("url"), | |
| "category_hint": o.get("topic", ""), | |
| "text": f"{title}\n\n{abstract}"[:2500], | |
| }) | |
| return out | |
| # ββ Activation extractor βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def build_term_idf(papers: list[dict]) -> dict[str, float]: | |
| df = Counter() | |
| for p in papers: | |
| seen = {t.lower() for t in TOKEN_RE.findall(p["text"]) | |
| if t.lower() not in STOP and len(t) > 3} | |
| df.update(seen) | |
| N = len(papers) or 1 | |
| return {t: math.log((N + 1) / (c + 1)) + 1.0 for t, c in df.items()} | |
| def activations_for_paper( | |
| paper: dict, | |
| concepts: dict[str, dict], | |
| name_index: dict[str, str], | |
| idf: dict[str, float], | |
| top_k: int, | |
| ) -> tuple[list[tuple[str, float]], list[dict]]: | |
| """ | |
| Return (top concepts with weight, innovations). | |
| """ | |
| text = paper["text"] | |
| text_lc = text.lower() | |
| title_lc = paper["title"].lower() | |
| # 1. concept-name match (multiword: phrase substring search; | |
| # single-word: word-boundary regex on the whole text) | |
| hits: dict[str, float] = {} | |
| for name_lc, wid in name_index.items(): | |
| if len(name_lc) < 4 or name_lc in STOP: | |
| continue | |
| if " " in name_lc: | |
| n = text_lc.count(name_lc) | |
| if n: | |
| hits[wid] = hits.get(wid, 0.0) + n * 2.0 # phrase weight | |
| if name_lc in title_lc: | |
| hits[wid] += 3.0 | |
| else: | |
| # word-boundary regex (cached cheaply via str.split) | |
| tokens = TOKEN_RE.findall(text_lc) | |
| if not tokens: | |
| continue | |
| n = sum(1 for t in tokens if t == name_lc) | |
| if n: | |
| hits[wid] = hits.get(wid, 0.0) + n * 1.0 | |
| if name_lc in title_lc.split(): | |
| hits[wid] += 2.0 | |
| # 2. boost by IDF of concept slug (rare concepts β higher weight) | |
| scored = [] | |
| for wid, raw in hits.items(): | |
| slug_tokens = concepts[wid]["name_lc"].split() | |
| idf_score = sum(idf.get(tok, 1.0) for tok in slug_tokens) / max(len(slug_tokens), 1) | |
| scored.append((wid, raw * idf_score)) | |
| scored.sort(key=lambda t: -t[1]) | |
| top = scored[:top_k] | |
| # 3. Innovation extraction (named, claim-pattern, hyphen, k-prefix) | |
| raw_innov = extract_innovations(text, max_n=8) | |
| innovations = classify_against_vocab(raw_innov, name_index) | |
| return top, innovations | |
| # ββ Novelty metric βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def compute_novelty( | |
| papers_with_acts: list[tuple[dict, list[tuple[str, float]]]], | |
| ) -> tuple[Counter, Counter]: | |
| """Compute global concept_freq + co-occurrence edge_freq over all papers.""" | |
| concept_freq: Counter = Counter() | |
| edge_freq: Counter = Counter() | |
| for _, acts in papers_with_acts: | |
| ids = [a[0] for a in acts] | |
| for a in ids: | |
| concept_freq[a] += 1 | |
| for i in range(len(ids)): | |
| for j in range(i + 1, len(ids)): | |
| a, b = sorted([ids[i], ids[j]]) | |
| edge_freq[f"{a}|{b}"] += 1 | |
| return concept_freq, edge_freq | |
| def novelty_bits( | |
| acts: list[tuple[str, float]], | |
| innovations: list[dict], | |
| concept_freq: Counter, | |
| edge_freq: Counter, | |
| total_papers: int, | |
| ) -> tuple[float, dict]: | |
| if not acts: | |
| return 0.0, {"concept_bits": 0.0, "edge_bits": 0.0, "innovation_bonus": 0.0} | |
| ids = [a[0] for a in acts] | |
| # Concept rarity bits: -log p(c) summed, normalised | |
| concept_bits = 0.0 | |
| for c in ids: | |
| p = (concept_freq[c] + 1) / (total_papers + len(concept_freq)) | |
| concept_bits += -math.log2(p) | |
| concept_bits /= len(ids) | |
| # Edge novelty bits: pairs that have NEVER co-occurred elsewhere | |
| new_edges = 0 | |
| rare_edges = 0 | |
| for i in range(len(ids)): | |
| for j in range(i + 1, len(ids)): | |
| a, b = sorted([ids[i], ids[j]]) | |
| f = edge_freq.get(f"{a}|{b}", 0) | |
| if f <= 1: | |
| new_edges += 1 | |
| elif f <= 3: | |
| rare_edges += 1 | |
| edge_bits = new_edges * 1.5 + rare_edges * 0.5 | |
| # Innovation bonus: weight "proposes" higher than "connects" | |
| innov_bonus = 0.0 | |
| for i in innovations: | |
| innov_bonus += 0.8 if i["kind"] == "proposes" else 0.3 | |
| total = concept_bits + edge_bits + innov_bonus | |
| return total, { | |
| "concept_bits": round(concept_bits, 2), | |
| "edge_bits": round(edge_bits, 2), | |
| "innovation_bonus": round(innov_bonus, 2), | |
| } | |
| # ββ Elevator pitch βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| _RE_EMAIL = re.compile(r"\S+@\S+\.\S+") | |
| _RE_AFFIL = re.compile(r"\b(?:university|institute|college|laboratory|laboratoire|inc\.?|llc|gmbh|ltd\.?|corp\.?|hospital|department|Γ©cole|universitΓ€t|universitΓ |universidad)\b", re.I) | |
| _RE_DAGGER = re.compile(r"[\*β β‘Β§ΒΆ#0-9]{1,3}\s*$") | |
| _RE_NAME_BIB = re.compile(r"^[A-Z][a-zA-Z'.-]+(?:\s+[A-Z][a-zA-Z'.-]+){0,3}$") | |
| _RE_TOC_LINE = re.compile(r"^\s*\d+(\.\d+)*(\s+[A-Z][\w\s-]+)?\s*$") | |
| _RE_PAGE_NUM = re.compile(r"^\s*\d{1,3}\s*$") | |
| _RE_SECTION_H = re.compile(r"^\s*\d+(\.\d+)*\s+[A-Z]") # "1 Introduction" / "2.3 Method" | |
| _TOC_KEYWORDS = { | |
| "contents", "abstract", "introduction", "references", "bibliography", | |
| "acknowledgments", "acknowledgements", "appendix", "notation", | |
| "preliminaries", "background", "related work", "conclusion", | |
| "discussion", "results", "methods", "method", "table of contents", | |
| "future work", | |
| } | |
| def _clean_body(text: str) -> str: | |
| """Drop TOC entries, page numbers, lone section headers / keywords.""" | |
| out = [] | |
| for line in text.splitlines(): | |
| s = line.strip() | |
| if not s: | |
| out.append("") | |
| continue | |
| if _RE_TOC_LINE.match(s) or _RE_PAGE_NUM.match(s): | |
| continue | |
| if len(s) < 30 and _RE_SECTION_H.match(s): | |
| continue | |
| # Drop lone TOC keywords | |
| if s.lower().rstrip(".:") in _TOC_KEYWORDS: | |
| continue | |
| out.append(line) | |
| return "\n".join(out) | |
| def _strip_author_block(text: str) -> str: | |
| """Strip leading title/author/affil lines until we hit the abstract.""" | |
| text = _clean_body(text) | |
| lines = text.splitlines() | |
| # Find "Abstract" line and start after it | |
| for i, line in enumerate(lines): | |
| if line.strip().lower() in ("abstract", "abstract.", "abstract:"): | |
| return "\n".join(lines[i+1:]).strip() | |
| # Otherwise drop the first ~12 lines if they look like author block | |
| out_start = 0 | |
| for i, line in enumerate(lines[:25]): | |
| s = line.strip() | |
| if not s: | |
| continue | |
| if _RE_EMAIL.search(s) or _RE_AFFIL.search(s) or _RE_DAGGER.search(s): | |
| out_start = i + 1 | |
| continue | |
| if _RE_NAME_BIB.match(s): | |
| out_start = i + 1 | |
| continue | |
| # First substantive line β keep | |
| break | |
| return "\n".join(lines[out_start:]).strip() | |
| def make_pitch(paper: dict, acts: list[tuple[str, float]], | |
| concepts: dict[str, dict], innovations: list[dict]) -> str: | |
| body = _strip_author_block(paper["text"]) | |
| # First sentence that starts w/ "We ", "This paper", "Our ", "In this", | |
| # otherwise first 1-2 sentences >=40 chars | |
| sents_all = SENT_SPLIT.split(body) | |
| sents_clean = [] | |
| for s in sents_all: | |
| s = re.sub(r"\s+", " ", s).strip(" ,.;:()[]") | |
| if 40 < len(s) < 280: | |
| sents_clean.append(s) | |
| # Prioritize sentences that look authorial | |
| authorial = [s for s in sents_clean if re.match(r"^(We |This (?:paper|work|study|note) |Our |In this |The (?:main|key|primary))", s)] | |
| picks = (authorial + sents_clean)[:2] | |
| pitch_core = " ".join(picks).strip() | |
| if not pitch_core: | |
| pitch_core = body[:240].strip() | |
| return pitch_core | |
| def make_key_finding(innovations: list[dict], concepts: dict[str, dict], | |
| acts: list[tuple[str, float]]) -> str: | |
| """One-line headline for video + UI β the single most-important claim.""" | |
| # Prefer first "proposes" innovation, then any innovation, then top concept | |
| for innov in innovations: | |
| if innov.get("kind") == "proposes": | |
| return innov["phrase"] | |
| if innovations: | |
| return innovations[0]["phrase"] | |
| if acts: | |
| return concepts[acts[0][0]]["display"] | |
| return "" | |
| # ββ Main βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--max-per-cat", type=int, default=80, | |
| help="arXiv papers per category (default 80)") | |
| ap.add_argument("--top-k", type=int, default=12, | |
| help="top activated concepts per paper") | |
| ap.add_argument("--keep-top", type=int, default=600, | |
| help="papers to emit (ranked by novelty)") | |
| args = ap.parse_args() | |
| t0 = time.time() | |
| print("β load concept vocab", flush=True) | |
| concepts, name_index = load_concept_vocab() | |
| print(f" {len(concepts)} concepts, {len(name_index)} name aliases") | |
| print("β load papers", flush=True) | |
| arx = load_arxiv_papers(args.max_per_cat) | |
| deff = load_defense_papers() | |
| papers = arx + deff | |
| print(f" {len(arx)} arxiv + {len(deff)} defense = {len(papers)}") | |
| print("β build idf", flush=True) | |
| idf = build_term_idf(papers) | |
| print("β extract activations + innovations", flush=True) | |
| acts_per_paper: list[tuple[dict, list[tuple[str, float]], list[dict]]] = [] | |
| for i, p in enumerate(papers): | |
| acts, innovations = activations_for_paper(p, concepts, name_index, idf, args.top_k) | |
| acts_per_paper.append((p, acts, innovations)) | |
| if (i + 1) % 500 == 0: | |
| print(f" {i+1}/{len(papers)}") | |
| print("β compute corpus frequencies", flush=True) | |
| concept_freq, edge_freq = compute_novelty([(p, a) for p, a, _ in acts_per_paper]) | |
| print("β score novelty per paper", flush=True) | |
| enriched = [] | |
| for p, acts, innovations in acts_per_paper: | |
| if not acts: | |
| continue | |
| score, breakdown = novelty_bits(acts, innovations, concept_freq, edge_freq, len(papers)) | |
| pair_edges = [] | |
| ids = [a[0] for a in acts] | |
| for i in range(len(ids)): | |
| for j in range(i + 1, len(ids)): | |
| a, b = sorted([ids[i], ids[j]]) | |
| pair_edges.append([a, b]) | |
| rigor = score_rigor(p) | |
| composite = round(score + rigor["total"], 2) | |
| # foundations vs introduced β depends on universe paper-degree | |
| # which isn't yet computed. We'll patch them in a second pass below. | |
| enriched.append({ | |
| "id": p["id"], | |
| "title": p["title"][:240], | |
| "source": p["source"], | |
| "year": p.get("year"), | |
| "url": p.get("url"), | |
| "category_hint": p.get("category_hint", ""), | |
| "concepts": [ | |
| {"id": a, "weight": round(w, 3), | |
| "name": concepts[a]["display"]} | |
| for a, w in acts | |
| ], | |
| "innovations": innovations, | |
| "pair_edges": pair_edges, | |
| "novelty_bits": round(score, 2), | |
| "novelty_breakdown": breakdown, | |
| "rigor": rigor, | |
| "composite": composite, | |
| "pitch": make_pitch(p, acts, concepts, innovations), | |
| "key_finding": make_key_finding(innovations, concepts, acts), | |
| }) | |
| enriched.sort(key=lambda x: -x["composite"]) | |
| keep = enriched[: args.keep_top] | |
| print(f"β kept top {len(keep)}/{len(enriched)} by novelty") | |
| # ββ Foundations / introduced per paper βββββββββββββββββββββββββββββββ | |
| # Universe paper-degree = how many of the kept papers activate each | |
| # concept. High β foundational; β€ 2 β introduced. | |
| paper_degree_of_concept = Counter() | |
| for p in keep: | |
| for c in p["concepts"]: | |
| paper_degree_of_concept[c["id"]] += 1 | |
| for p in keep: | |
| title_lc = p["title"].lower() | |
| def echoes_title(name): | |
| return any(t for t in name.lower().split() | |
| if len(t) > 3 and t in title_lc) | |
| scored = [] | |
| for c in p["concepts"]: | |
| deg = paper_degree_of_concept[c["id"]] | |
| scored.append({ | |
| "id": c["id"], | |
| "name": c["name"], | |
| "weight": c["weight"], | |
| "degree": deg, | |
| "echoes_title": echoes_title(c["name"]), | |
| }) | |
| foundations = [c for c in scored | |
| if not c["echoes_title"] and c["degree"] >= 3] | |
| foundations.sort(key=lambda c: -c["degree"]) | |
| introduced = [c for c in scored if c["degree"] <= 2] | |
| introduced.sort(key=lambda c: c["degree"]) | |
| p["foundations"] = foundations[:4] | |
| p["introduced"] = introduced[:4] | |
| # Concept universe: every concept referenced by ANY kept paper. | |
| # Each node gets a degree (count of papers activating it) and a cluster | |
| # (cluster_id = id within first 12 clusters by paper count, else 11). | |
| universe_ids: set[str] = set() | |
| for p in keep: | |
| for c in p["concepts"]: | |
| universe_ids.add(c["id"]) | |
| # Cluster-by-cooccurrence: simple greedy β pick top-12 most-frequent | |
| # concepts as cluster seeds; assign each remaining concept to the | |
| # seed it co-occurs most with. | |
| cf = Counter() | |
| for p in keep: | |
| for c in p["concepts"]: | |
| if c["id"] in universe_ids: | |
| cf[c["id"]] += 1 | |
| seeds = [c for c, _ in cf.most_common(12)] | |
| seed_idx = {c: i for i, c in enumerate(seeds)} | |
| # Build co-occurrence | |
| cooc: dict[tuple[str,str], int] = defaultdict(int) | |
| for p in keep: | |
| ids = [c["id"] for c in p["concepts"]] | |
| for a in ids: | |
| for b in ids: | |
| if a != b: | |
| cooc[(a, b)] += 1 | |
| cluster_of: dict[str, int] = dict(seed_idx) | |
| for cid in universe_ids: | |
| if cid in cluster_of: | |
| continue | |
| best, best_count = 0, -1 | |
| for s in seeds: | |
| c = cooc.get((cid, s), 0) | |
| if c > best_count: | |
| best, best_count = seed_idx[s], c | |
| cluster_of[cid] = best | |
| universe_nodes = [] | |
| for cid in universe_ids: | |
| slug = cid.replace("wiki:", "") | |
| universe_nodes.append({ | |
| "id": slug, | |
| "name": concepts.get(cid, {}).get("display") or slug.replace("_", " "), | |
| "cluster": cluster_of.get(cid, 0), | |
| "degree": cf.get(cid, 1), | |
| "kind": "wiki", | |
| }) | |
| # Concept edges: every pair_edge across kept papers, deduplicated + weighted | |
| edge_weight: Counter = Counter() | |
| for p in keep: | |
| for a, b in p["pair_edges"]: | |
| key = tuple(sorted([a.replace("wiki:", ""), b.replace("wiki:", "")])) | |
| edge_weight[key] += 1 | |
| universe_edges = [list(k) + [v] for k, v in edge_weight.items()] | |
| # Emit JS | |
| out = { | |
| "papers": keep, | |
| "concept_universe": universe_nodes, | |
| "concept_edges": universe_edges, | |
| "concept_freq": dict(concept_freq.most_common(400)), | |
| "edge_freq": dict(edge_freq.most_common(400)), | |
| "generated_at": datetime.now(timezone.utc).isoformat(), | |
| "stats": { | |
| "total_papers_scanned": len(papers), | |
| "papers_with_acts": len(enriched), | |
| "concepts_in_vocab": len(concepts), | |
| "concept_universe_size": len(universe_nodes), | |
| "concept_edges_count": len(universe_edges), | |
| "elapsed_s": round(time.time() - t0, 1), | |
| }, | |
| } | |
| js = "window.ARXIVIS = " + json.dumps(out, indent=2, ensure_ascii=False) + ";\n" | |
| OUT_PATH.write_text(js) | |
| sz_kb = OUT_PATH.stat().st_size / 1024 | |
| print(f"β wrote {OUT_PATH} ({sz_kb:.0f} KB) in {time.time()-t0:.1f}s") | |
| if __name__ == "__main__": | |
| main() | |