Spaces:
Running
Running
| """ | |
| Open-Problems scorer β find gaps in the concept graph that no paper has | |
| addressed yet, and rank them by how much novelty + value they'd unlock. | |
| Three kinds of gap: | |
| 1. missing_bridge: two high-paper-degree concepts that *should* connect | |
| (both are foundational, both appear together as activations in | |
| some paper's foundation set, yet have zero edges between them in | |
| the corpus). A new paper could earn high novelty bits by linking | |
| them. | |
| 2. underexplored: a wiki concept that exists in the vocabulary and | |
| is referenced as a foundation by multiple other papers, but only | |
| 1-2 papers in the corpus actively activate it as a primary | |
| concept. A focused paper on it would close a key gap. | |
| 3. stale_cluster: a Louvain-style cluster (computed by greedy | |
| co-occurrence seeding) whose newest activating paper is older | |
| than the cluster's median paper year. The community has gone | |
| quiet β open territory for a fresh contribution. | |
| For each problem we attach: | |
| - closest_papers: top-3 arXivis papers most likely to be relevant | |
| (concept overlap w/ the problem's involved nodes). | |
| - suggested_foundations: high-paper-degree concepts the new work | |
| could build on. | |
| Output: design/open_problems.js (window.OPEN_PROBLEMS). | |
| Run: | |
| python3 scripts/build_open_problems.py | |
| python3 scripts/build_open_problems.py --max 60 | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import sys | |
| from collections import Counter, defaultdict | |
| from datetime import datetime, timezone | |
| from pathlib import Path | |
| PROJECT_ROOT = Path(__file__).parent.parent | |
| ARXIVIS_JS = PROJECT_ROOT / "design" / "arxivis_data.js" | |
| OUT_PATH = PROJECT_ROOT / "design" / "open_problems.js" | |
| def load_arxivis() -> dict: | |
| if not ARXIVIS_JS.exists(): | |
| sys.exit(f"missing {ARXIVIS_JS} β run scripts/build_arxivis_data.py first") | |
| t = ARXIVIS_JS.read_text() | |
| return json.loads(t[t.find("{"): t.rfind("}") + 1]) | |
| GENERIC_STOP = { | |
| "filter", "reduce", "partition", "match", "map", "graph", "tree", | |
| "set", "node", "edge", "function", "vector", "matrix", "value", | |
| "norm", "rate", "loss", "rule", "step", "task", "limit", "row", | |
| "column", "operator", "label", | |
| "convert", "split", "merge", "build", "init", "scan", "test", | |
| } | |
| def _is_real_concept(name: str) -> bool: | |
| """Skip noisy single-verb tokens that aren't really concepts.""" | |
| if not name: return False | |
| if len(name) < 5: return False | |
| if name.lower().strip() in GENERIC_STOP: return False | |
| return True | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--max", type=int, default=50, help="problems to keep") | |
| args = ap.parse_args() | |
| data = load_arxivis() | |
| papers = data["papers"] | |
| universe = data.get("concept_universe", []) | |
| edges = data.get("concept_edges", []) # [a, b, weight] | |
| # Concept lookup tables | |
| concept_by_id = {c["id"]: c for c in universe} | |
| # Paper-degree (count of papers activating this concept) = universe.degree | |
| # Edge weight (count of papers in which both co-activate) = edges[i][2] | |
| edge_weight: dict[tuple[str, str], int] = {} | |
| for e in edges: | |
| a, b, w = e[0], e[1], (e[2] if len(e) > 2 else 1) | |
| edge_weight[(a, b)] = w | |
| edge_weight[(b, a)] = w | |
| # Per-paper concept set (slug form) for relevance lookup | |
| paper_concept_set: dict[str, set[str]] = {} | |
| paper_year: dict[str, int | None] = {} | |
| for p in papers: | |
| ids = {c["id"].replace("wiki:", "") for c in p.get("concepts", [])} | |
| paper_concept_set[p["id"]] = ids | |
| paper_year[p["id"]] = p.get("year") | |
| def closest_papers(involved_ids: set[str], k: int = 3) -> list[dict]: | |
| scored = [] | |
| for p in papers: | |
| inter = paper_concept_set[p["id"]] & involved_ids | |
| if not inter: | |
| continue | |
| scored.append({ | |
| "id": p["id"], | |
| "title": p["title"], | |
| "overlap": len(inter), | |
| "novelty": p.get("novelty_bits", 0), | |
| }) | |
| scored.sort(key=lambda x: (-x["overlap"], -x["novelty"])) | |
| return scored[:k] | |
| def name(slug: str) -> str: | |
| c = concept_by_id.get(slug) | |
| if c: | |
| return c["name"] | |
| return slug.replace("_", " ") | |
| problems = [] | |
| # βββ 1. MISSING BRIDGES βββββββββββββββββββββββββββββββββββββββββββββ | |
| # Concepts with paper-degree β₯ 3 that have no direct edge despite | |
| # appearing together as activations in some paper's pair_edges. | |
| # We approximate this with: cluster of high-degree concepts whose | |
| # mutual edge_weight is 0. | |
| high_deg = [c for c in universe | |
| if c.get("degree", 0) >= 3 and _is_real_concept(c.get("name", ""))] | |
| # Sort by degree descending, cap to keep N^2 manageable | |
| high_deg.sort(key=lambda c: -c["degree"]) | |
| high_deg = high_deg[:60] | |
| # Cluster co-membership = same cluster id from build_arxivis_data | |
| seen_pairs: set[tuple[str, str]] = set() | |
| for i, a in enumerate(high_deg): | |
| for b in high_deg[i + 1:]: | |
| key = tuple(sorted([a["id"], b["id"]])) | |
| if key in seen_pairs: | |
| continue | |
| seen_pairs.add(key) | |
| w = edge_weight.get(key, 0) | |
| if w > 0: | |
| continue | |
| # Higher score for pairs across different clusters | |
| cross_cluster = a.get("cluster") != b.get("cluster") | |
| score = (a["degree"] + b["degree"]) * (1.6 if cross_cluster else 1.0) | |
| problems.append({ | |
| "kind": "missing_bridge", | |
| "title": f"No paper has bridged {name(a['id'])} and {name(b['id'])}", | |
| "concepts": [a["id"], b["id"]], | |
| "score": round(score, 2), | |
| "why": ( | |
| f"{name(a['id'])} appears in {a['degree']} papers, " | |
| f"{name(b['id'])} in {b['degree']}, but the corpus " | |
| f"contains zero papers that activate both." | |
| + (" Bridges two distinct clusters." | |
| if cross_cluster else "") | |
| ), | |
| "closest_papers": closest_papers({a["id"], b["id"]}), | |
| "suggested_foundations": [ | |
| {"id": a["id"], "name": name(a["id"]), "papers": a["degree"]}, | |
| {"id": b["id"], "name": name(b["id"]), "papers": b["degree"]}, | |
| ], | |
| }) | |
| # βββ 2. UNDEREXPLORED ANCHORS βββββββββββββββββββββββββββββββββββββββ | |
| # Concepts mentioned as `foundations` by β₯ 3 papers, but themselves | |
| # activated as a primary concept in β€ 2 papers. Foundation pull | |
| # without active research = open territory. | |
| foundation_pull: Counter = Counter() | |
| for p in papers: | |
| for f in p.get("foundations", []) or []: | |
| # foundations were stored w/ raw id (e.g. wiki:slug); strip | |
| slug = f["id"].replace("wiki:", "") if "id" in f else None | |
| if slug: | |
| foundation_pull[slug] += 1 | |
| for slug, pull in foundation_pull.items(): | |
| c = concept_by_id.get(slug) | |
| if not c: | |
| continue | |
| if not _is_real_concept(c.get("name", "")): | |
| continue | |
| deg = c.get("degree", 0) | |
| if pull >= 2 and deg <= 2: | |
| score = pull * 4 + (3 - deg) | |
| problems.append({ | |
| "kind": "underexplored", | |
| "title": f"{name(slug)} is foundational but barely studied", | |
| "concepts": [slug], | |
| "score": round(score, 2), | |
| "why": ( | |
| f"{pull} papers in the corpus build ON {name(slug)} as a " | |
| f"foundation, yet only {deg} paper{'s' if deg != 1 else ''} " | |
| f"focuses on it as a primary subject." | |
| ), | |
| "closest_papers": closest_papers({slug}), | |
| "suggested_foundations": [ | |
| {"id": slug, "name": name(slug), "papers": deg}, | |
| ], | |
| }) | |
| # βββ 3. STALE CLUSTERS ββββββββββββββββββββββββββββββββββββββββββββββ | |
| # For each cluster id, find median year of activating papers + newest. | |
| # Stale = newest is older than median (community has slowed). | |
| cluster_papers: dict[int, list[int]] = defaultdict(list) | |
| cluster_concepts: dict[int, list[str]] = defaultdict(list) | |
| for c in universe: | |
| cluster_concepts[c.get("cluster", 0)].append(c["id"]) | |
| for p in papers: | |
| y = p.get("year") | |
| if not y: | |
| continue | |
| clusters_touched = set() | |
| for c in p.get("concepts", []): | |
| slug = c["id"].replace("wiki:", "") | |
| cid = concept_by_id.get(slug, {}).get("cluster") | |
| if cid is not None: | |
| clusters_touched.add(cid) | |
| for cid in clusters_touched: | |
| cluster_papers[cid].append(y) | |
| for cid, years in cluster_papers.items(): | |
| if len(years) < 4: | |
| continue | |
| years_sorted = sorted(years) | |
| median = years_sorted[len(years_sorted) // 2] | |
| newest = years_sorted[-1] | |
| # If the newest paper isn't notably newer than the median, cluster's stale | |
| if newest - median <= 0: | |
| seed_concepts = [name(s) for s in cluster_concepts[cid][:3]] | |
| score = len(years) * 0.5 + (median - 2000) | |
| problems.append({ | |
| "kind": "stale_cluster", | |
| "title": f"Cluster around {', '.join(seed_concepts)} has gone quiet", | |
| "concepts": cluster_concepts[cid][:5], | |
| "score": round(score, 2), | |
| "why": ( | |
| f"{len(years)} papers in this cluster, median year " | |
| f"{median}, newest paper {newest}. No recent activity β " | |
| "open territory for a fresh contribution." | |
| ), | |
| "closest_papers": closest_papers(set(cluster_concepts[cid][:8])), | |
| "suggested_foundations": [ | |
| {"id": s, "name": name(s), "papers": concept_by_id.get(s, {}).get("degree", 1)} | |
| for s in cluster_concepts[cid][:3] | |
| ], | |
| }) | |
| # Bucket-balance: ensure variety across kinds (avoid 60 missing_bridge dominating) | |
| by_kind: dict[str, list[dict]] = defaultdict(list) | |
| for p in problems: | |
| by_kind[p["kind"]].append(p) | |
| for k in by_kind: | |
| by_kind[k].sort(key=lambda p: -p["score"]) | |
| balanced: list[dict] = [] | |
| quota = max(args.max // 3, 5) | |
| for k in ("missing_bridge", "underexplored", "stale_cluster"): | |
| balanced.extend(by_kind.get(k, [])[:quota]) | |
| # Top up w/ remaining missing_bridges if room | |
| while len(balanced) < args.max: | |
| rest = [p for k in by_kind for p in by_kind[k] if p not in balanced] | |
| if not rest: | |
| break | |
| rest.sort(key=lambda p: -p["score"]) | |
| balanced.append(rest[0]) | |
| balanced.sort(key=lambda p: -p["score"]) | |
| problems = balanced[: args.max] | |
| out = { | |
| "problems": problems, | |
| "generated_at": datetime.now(timezone.utc).isoformat(), | |
| "stats": { | |
| "corpus_papers": len(papers), | |
| "universe_concepts": len(universe), | |
| "missing_bridges": sum(1 for p in problems if p["kind"] == "missing_bridge"), | |
| "underexplored": sum(1 for p in problems if p["kind"] == "underexplored"), | |
| "stale_clusters": sum(1 for p in problems if p["kind"] == "stale_cluster"), | |
| }, | |
| } | |
| js = "window.OPEN_PROBLEMS = " + json.dumps(out, indent=2, ensure_ascii=False) + ";\n" | |
| OUT_PATH.write_text(js) | |
| print(f"β wrote {OUT_PATH} ({OUT_PATH.stat().st_size // 1024} KB)") | |
| print(f" {len(problems)} problems: {out['stats']}") | |
| for p in problems[:5]: | |
| print(f" [{p['kind']:14s} {p['score']:6.1f}] {p['title']}") | |
| if __name__ == "__main__": | |
| main() | |