#!/usr/bin/env python3 """Build viewer data for the PhantomWiki viewer. PhantomWiki (https://github.com/kilian-group/phantom-wiki, arXiv:2502.20377) generates synthetic "universes" of people (family + friendship graphs) with Wikipedia-style articles, plus multi-hop reasoning questions over them. The pre-generated HF dataset ``kilian-group/phantom-wiki-v1`` has, for each of 9 universes (``depth_20_size_{50,500,5000}_seed_{1,2,3}``), two relevant configs: text-corpus {title, article (markdown), facts} โ€” one document per person question-answer {id, question, answer (list), intermediate_answers, prolog{query, answer}, template, type, difficulty} Each universe has exactly 500 questions (paper's "500 per instance"); the corpus has one document per person (โ‰ˆ the universe size). This script writes, per universe: corpus_.json list[{title, article}] (documents, inline) eval_.json list[{id, question, answer, supporting_titles, template, prolog, meta{Difficulty, Type}}] plus a ``sets.json`` manifest (universe selector + Difficulty/Type facets). Answers that name people resolve to corpus documents (clickable supporting docs); answers that are attribute values (dates, counts, jobs) have none. Run from the viewer repo root: python scripts/build_data.py --data-dir /mnt/tmp/phantom-wiki/pw-v1 """ import argparse import json import os try: import pyarrow.parquet as pq except ImportError: pq = None ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) DEFAULT_DATA_DIR = "/mnt/tmp/phantom-wiki/pw-v1" SIZES = [50, 500, 5000] SEEDS = [1, 2, 3] def parquet_path(data_dir, cfg, size, seed): return os.path.join(data_dir, cfg, f"depth_20_size_{size}_seed_{seed}-00000-of-00001.parquet") def build_universe(data_dir, size, seed): uid = f"size{size}_seed{seed}" tc_path = parquet_path(data_dir, "text-corpus", size, seed) qa_path = parquet_path(data_dir, "question-answer", size, seed) if not (os.path.exists(tc_path) and os.path.exists(qa_path)): print(f"[{uid}] missing parquet โ€” skipping") return None tc = pq.read_table(tc_path).to_pydict() corpus_rows = [] titles = set() for i in range(len(tc["title"])): t = tc["title"][i] titles.add(t) corpus_rows.append({"title": t, "article": tc["article"][i] or ""}) corpus_rows.sort(key=lambda d: d["title"].lower()) qa = pq.read_table(qa_path).to_pydict() eval_rows = [] diffs, types = set(), set() for i in range(len(qa["id"])): answer = list(qa["answer"][i]) if qa["answer"][i] is not None else [] supporting = [a for a in answer if a in titles] prolog = qa["prolog"][i] or {} template = qa["template"][i] or [] d = int(qa["difficulty"][i]) if qa["difficulty"][i] is not None else None ty = int(qa["type"][i]) if qa["type"][i] is not None else None diffs.add(d) types.add(ty) eval_rows.append({ "id": qa["id"][i], "question": (qa["question"][i] or "").strip(), "answer": answer, "supporting_titles": supporting, "template": " ".join(template), "prolog": list(prolog.get("query") or []), "prolog_answer": prolog.get("answer", ""), "meta": {"Difficulty": str(d), "Type": str(ty)}, }) eval_rows.sort(key=lambda r: (int(r["meta"]["Type"]), r["id"])) corpus_path = os.path.join(ROOT, f"corpus_{uid}.json") eval_path = os.path.join(ROOT, f"eval_{uid}.json") with open(corpus_path, "w", encoding="utf-8") as f: json.dump(corpus_rows, f, ensure_ascii=False) with open(eval_path, "w", encoding="utf-8") as f: json.dump(eval_rows, f, ensure_ascii=False) facets = [ {"key": "Difficulty", "values": [str(x) for x in sorted(v for v in diffs if v is not None)]}, {"key": "Type", "values": [str(x) for x in sorted(v for v in types if v is not None)]}, ] print(f"[{uid}] docs={len(corpus_rows)} questions={len(eval_rows)} " f"corpus={os.path.getsize(corpus_path)/1e6:.2f}MB eval={os.path.getsize(eval_path)/1e6:.2f}MB") return { "set": uid, "label": f"size {size} ยท seed {seed}", "size": size, "seed": seed, "n_docs": len(corpus_rows), "n_questions": len(eval_rows), "corpus_file": f"corpus_{uid}.json", "eval_file": f"eval_{uid}.json", "facets": facets, } def main(): ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("--data-dir", default=DEFAULT_DATA_DIR) args = ap.parse_args() if pq is None: raise SystemExit("pyarrow is required") manifest = [] for size in SIZES: for seed in SEEDS: m = build_universe(args.data_dir, size, seed) if m: manifest.append(m) with open(os.path.join(ROOT, "sets.json"), "w", encoding="utf-8") as f: json.dump(manifest, f, ensure_ascii=False, indent=2) print("wrote sets.json:", [s["set"] for s in manifest]) if __name__ == "__main__": main()