brain-university-api / scripts /build_arxivis_data.py
jang0294's picture
Upload folder using huggingface_hub
84ffaa0 verified
Raw
History Blame Contribute Delete
23.9 kB
"""
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()