#!/usr/bin/env python3 """WP44 — generate manuscript/tables/numbers.tex: every number quoted in the prose is a LaTeX macro produced here from result files (never typed by hand). Macro names use letters only (no digits: `\\nCoreEligible`, not `\\n2026`). Sources: data/metadata/prisma_counts.json, linkage_report.json, results/bibliometrics/*.csv, results/econometrics/*.json, taxonomy/validation/metrics.json, results/emerging/metrics_h12.json (each optional: missing → macro undefined → compile error, which is intended: the prose cannot cite a number that does not exist).""" import json, os, sys, csv, re R = sys.argv[1] if len(sys.argv) > 1 else os.path.dirname(os.path.dirname(os.path.abspath(__file__))) out = [] WORDS = {"0": "Zero", "1": "One", "2": "Two", "3": "Three", "4": "Four", "5": "Five", "6": "Six", "7": "Seven", "8": "Eight", "9": "Nine"} def name(s): s = re.sub(r"[^A-Za-z0-9]+", " ", s).title().replace(" ", "") return "".join(WORDS[ch] if ch.isdigit() else ch for ch in s) def fmt(v): if isinstance(v, bool): return str(v) if isinstance(v, int): return f"{v:,}".replace(",", "{,}") if isinstance(v, float): return f"{v:,.3f}".replace(",", "{,}") if abs(v) < 100 else f"{v:,.1f}".replace(",", "{,}") return str(v).replace("%", "\\%").replace("_", "\\_") def emit(k, v): out.append(f"\\newcommand{{\\{name(k)}}}{{{fmt(v)}}}") def walk(prefix, obj): if isinstance(obj, dict): for k, v in obj.items(): walk(f"{prefix} {k}", v) elif isinstance(obj, (int, float, str, bool)) and obj is not None: emit(prefix, obj) p = os.path.join(R, "data/metadata/prisma_counts.json") if os.path.exists(p): walk("prisma", json.load(open(p))) p = os.path.join(R, "data/metadata/linkage_report.json") if os.path.exists(p): walk("link", json.load(open(p))) for f in ["volume_yearly", "citation_windows_yearly", "international_collab_yearly", "industry_share_yearly", "venue_hhi_yearly"]: p = os.path.join(R, "results/bibliometrics", f + ".csv") if os.path.exists(p): for row in csv.DictReader(open(p)): y = row.get("year") or row.get("publication_year") for k, v in row.items(): if k in ("year", "publication_year") or v in ("", None): continue try: v = int(v) if re.fullmatch(r"-?\d+", v) else float(v) except ValueError: pass emit(f"{f} {y} {k}", v) for f, key in [("results/econometrics/its_results.json", "its"), ("taxonomy/validation/metrics.json", "val"), ("results/emerging/metrics_h12.json", "emerg"), ("results/emerging/metrics_h24.json", "emergTwoFour")]: p = os.path.join(R, f) if os.path.exists(p): d = json.load(open(p)) walk(key, {k: v for k, v in d.items() if not isinstance(v, (list,))}) for f, key in [("data/metadata/dblp_parse_report.json", "dblp"), ("data/metadata/acl_parse_report.json", "acl"), ("data/metadata/api_increment_ledger.json", "increment"), ("data/metadata/increment_report.json", "incrementIngest")]: p = os.path.join(R, f) if os.path.exists(p): d = json.load(open(p)) if key == "increment": sl = d.get("slices", {}); emit("increment api calls", d.get("calls", 0)); emit("increment records retrieved", sum(v.get("records", 0) for v in sl.values())) emit("increment truncated slices", sum(1 for v in sl.values() if v.get("truncated"))); emit("increment slices done", sum(1 for v in sl.values() if v.get("done"))); emit("increment cost usd", round(d.get("cost_usd_total", 0), 4)) elif key == "incrementIngest": emit("increment new works", d.get("new_works", 0)); emit("increment already in snapshot", d.get("already_in_snapshot", 0)) elif key == "dblp": emit("dblp parsed records", d.get("rows_written", 0)); [emit(f"dblp type {k}", v) for k, v in d.get("counts_by_type", {}).items()] elif key == "acl": emit("acl parsed entries", d.get("entries", 0)) for f, key in [("reports/classification_report.json", "classification"), ("literature_synthesis/landmarks/selection_report.json", "selection"), ("data/metadata/linkage_sample_summary.json", "linkagesample")]: p = os.path.join(R, f) if os.path.exists(p): walk(key, json.load(open(p))) p = os.path.join(R, "results/emerging/known_set_lead_times.csv") if os.path.exists(p): for row in csv.DictReader(open(p)): for k in ("total_works", "detection_month", "peak_growth_month", "lead_months"): v = row.get(k) if v in ("", None): continue try: v = int(float(v)) if re.fullmatch(r"-?\d+(\.0)?", v) else v except ValueError: pass emit(f"known {row['concept']} {k}", v) leads = [int(float(r["lead_months"])) for r in csv.DictReader(open(p)) if r.get("lead_months") not in ("", None)] if leads: import statistics; emit("known set median lead months", int(statistics.median(leads))); emit("known set n detected", len(leads)); emit("known set n positive lead", sum(1 for l in leads if l > 0)) # ---- per-category growth and yearly primary counts (appended by Agent F, 2026-11-03; used by Parts V–VII) ---- p = os.path.join(R, "results/bibliometrics/cagr_by_category.csv") if os.path.exists(p): for row in csv.DictReader(open(p)): for k, v in row.items(): if k == "category" or v in ("", None): continue try: v = int(v) if re.fullmatch(r"-?\d+", v) else float(v) except ValueError: pass emit(f"cagr {row['category']} {k}", v) p = os.path.join(R, "results/graph/category_counts_yearly.csv") if os.path.exists(p): agg = {} for row in csv.DictReader(open(p)): # one row per (year, period, category): sum the periods k = (row["category_id"], row["year"]); agg[k] = agg.get(k, 0) + int(float(row["n_primary"])) for (c, y), v in sorted(agg.items()): emit(f"catcount {c} {y} primary", v) post = {}; base = {} for (c, y), v in agg.items(): (post if int(y) >= 2023 else base).setdefault(c, 0); (post if int(y) >= 2023 else base)[c] += v for c in sorted(set(base) | set(post)): emit(f"catcount {c} baseline primary", base.get(c, 0)); emit(f"catcount {c} post primary", post.get(c, 0)) # de-duplicate macro names (last definition dropped) so that independent appends cannot break the compile _seen = set(); _out = [] for line in out: n = line.split("}")[0] if n in _seen: continue _seen.add(n); _out.append(line) out = _out open(os.path.join(R, "manuscript/tables/numbers.tex"), "w").write("% machine-generated by tables/make_numbers.py — do not edit\n" + "\n".join(out) + "\n") print(len(out), "macros") # ---- Part VII-b (security half of Part VII; Agent H, 2026-11-03): category-level macros for the five security_privacy ---- # categories, appended to numbers.tex. Sources: results/graph/category_counts_yearly.csv (eligible works, multi-label and # primary counts), results/bibliometrics/cagr_by_category.csv, results/econometrics/breaks.csv, # results/graph/category_flows_yearly.csv, results/graph/category_cooccurrence_yearly.csv, # literature_synthesis/landmarks/security_privacy_landmarks.csv. Percentages are pre-formatted strings (1 or 2 decimals). SEC = ["cybersecurity", "applied_cryptography", "blockchain", "privacy_preserving_computation", "federated_learning"] extra = [] def emit2(k, v): extra.append(f"\\newcommand{{\\{name(k)}}}{{{fmt(v)}}}") agg = {}; tot_primary = {} p = os.path.join(R, "results/graph/category_counts_yearly.csv") if os.path.exists(p): for row in csv.DictReader(open(p)): tot_primary[row["year"]] = tot_primary.get(row["year"], 0) + int(row["n_primary"]) if row["category_id"] in SEC: a = agg.setdefault((row["category_id"], row["year"]), [0, 0]); a[0] += int(row["n_multilabel"]); a[1] += int(row["n_primary"]) for (c, y), (m, pr) in sorted(agg.items()): emit2(f"sec count {c} {y} multilabel", m); emit2(f"sec count {c} {y} primary", pr) if tot_primary.get(y): emit2(f"sec share primary {c} {y} pct", f"{100 * pr / tot_primary[y]:.2f}") for c in SEC: for y0, y1, tag in [("2018", "2022", "baseline"), ("2023", "2025", "post"), ("2022", "2025", "span")]: m0, m1 = agg.get((c, y0), [0, 0])[0], agg.get((c, y1), [0, 0])[0] if m0 > 0: emit2(f"sec cagr multilabel {c} {tag} pct", f"{100 * ((m1 / m0) ** (1 / (int(y1) - int(y0))) - 1):.1f}"); emit2(f"sec ratio multilabel {c} {tag}", f"{m1 / m0:.2f}") p0, p1 = agg.get((c, y0), [0, 0])[1], agg.get((c, y1), [0, 0])[1] if p0 > 0: emit2(f"sec cagr primary {c} {tag} pct", f"{100 * ((p1 / p0) ** (1 / (int(y1) - int(y0))) - 1):.1f}") s = sum(v[0] for (cc, y), v in agg.items() if cc == c); emit2(f"sec count {c} all multilabel", s) s = sum(v[1] for (cc, y), v in agg.items() if cc == c); emit2(f"sec count {c} all primary", s) p = os.path.join(R, "results/bibliometrics/cagr_by_category.csv") if os.path.exists(p): for row in csv.DictReader(open(p)): if row["category"] in SEC: c = row["category"] for k in ("n_2018", "n_2022", "n_2023", "n_2025"): emit2(f"sec cagr {c} {k}", int(row[k])) for k in ("cagr_baseline", "cagr_post"): emit2(f"sec cagr {c} {k} pct", f"{100 * float(row[k]):.1f}") for k in ("share_2018", "share_2022", "share_2025"): emit2(f"sec cagr {c} {k} pct", f"{100 * float(row[k]):.2f}") p = os.path.join(R, "results/econometrics/breaks.csv") if os.path.exists(p): for row in csv.DictReader(open(p)): c = row["series"].replace("cat_", "") if c in SEC: for k in ("bp_breaks_bic", "pelt", "bocpd_resets"): emit2(f"sec break {c} {k}", (row[k] or "none").replace(";", ", ")) for k in ("chatgpt_level", "gpt3_level", "dalle2_level", "gpt4_level", "o1_level"): emit2(f"sec break {c} {k}", float(row[k])); emit2(f"sec break {c} {k} p", float(row[k + "_p"])) p = os.path.join(R, "results/graph/category_flows_yearly.csv") if os.path.exists(p): want = SEC + ["computer_networks", "computer_vision", "ml_theory", "quantum_computing", "llm", "ai_safety", "ai_agents", "generative_ai", "distributed_computing", "edge_computing", "software_engineering", "database_systems", "hci"] YRS = ("2019", "2022", "2025", "2026"); rows = list(csv.DictReader(open(p))); tot = {} for row in rows: if row["citing_cat"] in SEC: tot[(row["citing_cat"], row["citing_year"])] = tot.get((row["citing_cat"], row["citing_year"]), 0) + int(row["n"]) for row in rows: if row["citing_year"] not in YRS: continue if (row["citing_cat"] in SEC and row["cited_cat"] in want) or (row["cited_cat"] in SEC and row["citing_cat"] in ("llm", "ai_agents", "ai_safety", "generative_ai", "computer_networks", "distributed_computing", "edge_computing")): emit2(f"sec flow {row['citing_cat']} to {row['cited_cat']} {row['citing_year']} pct", f"{100 * float(row['row_share']):.1f}") for (c, y), n in sorted(tot.items()): if y in YRS: emit2(f"sec flow {c} total {y}", n) p = os.path.join(R, "results/graph/category_cooccurrence_yearly.csv") if os.path.exists(p): partners = ["llm", "ai_agents", "ai_safety", "generative_ai", "foundation_models"] for row in csv.DictReader(open(p)): a, b, y = row["cat_a"], row["cat_b"], row["year"] if a in SEC and b in partners: c, q = a, b elif b in SEC and a in partners: c, q = b, a elif a in SEC and b in SEC: c, q = a, b else: continue n = int(row["n"]); emit2(f"sec cooc {c} {q} {y}", n) m = agg.get((c, y), [0, 0])[0] if m: emit2(f"sec cooc {c} {q} {y} pct", f"{100 * n / m:.1f}") p = os.path.join(R, "literature_synthesis/landmarks/security_privacy_landmarks.csv") if os.path.exists(p): from collections import Counter L = list(csv.DictReader(open(p))); n = len(L); emit2("sec land n", n) wa = sum(r["has_abstract"] == "True" for r in L); emit2("sec land with abstract", wa); emit2("sec land with abstract pct", f"{100 * wa / n:.0f}") emit2("sec land oa pct", f"{100 * sum(r['is_oa'] == 'True' for r in L) / n:.0f}") emit2("sec land post", sum(r["period"] == "post" for r in L)); emit2("sec land baseline", sum(r["period"] == "baseline" for r in L)); emit2("sec land year 2026", sum(r["year"] == "2026" for r in L)) emit2("sec land no doi", sum(not r["doi"] for r in L)) for t, k in Counter(r["type"] for r in L).items(): emit2(f"sec land type {t}", k) for c in SEC: g = [r for r in L if r["primary_category"] == c]; emit2(f"sec land {c} n", len(g)); emit2(f"sec land {c} post", sum(r["period"] == "post" for r in g)) emit2(f"sec land {c} with abstract", sum(r["has_abstract"] == "True" for r in g)); emit2(f"sec land {c} year 2026", sum(r["year"] == "2026" for r in g)) emit2(f"sec land {c} preprints", sum(r["type"] == "preprint" for r in g)); emit2(f"sec land {c} articles", sum(r["type"] in ("article", "review") for r in g)) emit2(f"sec land {c} conference", sum(r["type"] == "conference-paper" for r in g)); emit2(f"sec land {c} grey", sum(r["type"] in ("dissertation", "report", "book-chapter", "book") for r in g)) open(os.path.join(R, "manuscript/tables/numbers.tex"), "a").write("% ---- Part VII-b security macros (Agent H) ----\n" + "\n".join(extra) + "\n") print(len(extra), "Part VII-b macros appended")