#!/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)) p = os.path.join(R, "results/graph/category_flows_yearly.csv") if os.path.exists(p): # Agent F: citation flows between primary categories by citing period (calendar years: baseline <= 2022, post >= 2023) fl = {}; ft = {} for row in csv.DictReader(open(p)): per = "post" if int(row["citing_year"]) >= 2023 else "baseline"; k = (row["citing_cat"], per) fl[(k, row["cited_cat"])] = fl.get((k, row["cited_cat"]), 0) + int(float(row["n"])); ft[k] = ft.get(k, 0) + int(float(row["n"])) for k, n in sorted(ft.items()): emit(f"sysflow {k[0]} {k[1]} n", n) for (k, cited), n in sorted(fl.items()): emit(f"sysflow {k[0]} {k[1]} to {cited} share", f"{100.0 * n / ft[k]:.1f}") # 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") # ---- Part VII-a (theory half of Part VII; Agent G, 2026-11-03): category-level macros for the six theory/software_pl ---- # categories (algorithms, computational_complexity, programming_languages, compilers, formal_verification, # software_engineering), appended to numbers.tex with prefix "thy". Sources: results/graph/category_counts_yearly.csv, # results/bibliometrics/cagr_by_category.csv, results/econometrics/breaks.csv, results/graph/category_flows_yearly.csv # (aggregated by period: citing_year <= 2022 baseline, >= 2023 post), results/graph/category_cooccurrence_yearly.csv, # literature_synthesis/landmarks/{theory,software_pl}_landmarks.csv and literature_synthesis/agent_G/contributions.csv # (the specialist's audit of the candidate landmarks). Percentages are pre-formatted strings. THY = ["algorithms", "computational_complexity", "programming_languages", "compilers", "formal_verification", "software_engineering"] extraG = [] def emitG(k, v): extraG.append(f"\\newcommand{{\\{name(k)}}}{{{fmt(v)}}}") aggG = {}; totG = {}; totmG = {} p = os.path.join(R, "results/graph/category_counts_yearly.csv") if os.path.exists(p): for row in csv.DictReader(open(p)): totG[row["year"]] = totG.get(row["year"], 0) + int(row["n_primary"]); totmG[row["year"]] = totmG.get(row["year"], 0) + int(row["n_multilabel"]) if row["category_id"] in THY: a = aggG.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(aggG.items()): emitG(f"thy count {c} {y} multilabel", m); emitG(f"thy count {c} {y} primary", pr) if totG.get(y): emitG(f"thy share primary {c} {y} pct", f"{100 * pr / totG[y]:.2f}") if totmG.get(y): emitG(f"thy share multilabel {c} {y} pct", f"{100 * m / totmG[y]:.2f}") for c in THY: for y0, y1, tag in [("2018", "2022", "baseline"), ("2023", "2025", "post"), ("2022", "2025", "span")]: m0, m1 = aggG.get((c, y0), [0, 0])[0], aggG.get((c, y1), [0, 0])[0] if m0 > 0: emitG(f"thy cagr multilabel {c} {tag} pct", f"{100 * ((m1 / m0) ** (1 / (int(y1) - int(y0))) - 1):.1f}"); emitG(f"thy ratio multilabel {c} {tag}", f"{m1 / m0:.2f}") p0, p1 = aggG.get((c, y0), [0, 0])[1], aggG.get((c, y1), [0, 0])[1] if p0 > 0: emitG(f"thy cagr primary {c} {tag} pct", f"{100 * ((p1 / p0) ** (1 / (int(y1) - int(y0))) - 1):.1f}"); emitG(f"thy ratio primary {c} {tag}", f"{p1 / p0:.2f}") emitG(f"thy count {c} all multilabel", sum(v[0] for (cc, y), v in aggG.items() if cc == c)); emitG(f"thy count {c} all primary", sum(v[1] for (cc, y), v in aggG.items() if cc == c)) for y0, y1, tag in [("2018", "2022", "baseline"), ("2023", "2025", "post")]: if totG.get(y0): emitG(f"thy cagr primary total {tag} pct", f"{100 * ((totG[y1] / totG[y0]) ** (1 / (int(y1) - int(y0))) - 1):.1f}") if totmG.get(y0): emitG(f"thy cagr multilabel total {tag} pct", f"{100 * ((totmG[y1] / totmG[y0]) ** (1 / (int(y1) - int(y0))) - 1):.1f}") 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 THY: c = row["category"] for k in ("n_2018", "n_2022", "n_2023", "n_2025"): emitG(f"thy cagrfile {c} {k}", int(row[k])) for k in ("cagr_baseline", "cagr_post"): emitG(f"thy cagrfile {c} {k} pct", f"{100 * float(row[k]):.1f}") for k in ("share_2018", "share_2022", "share_2025"): emitG(f"thy cagrfile {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 THY: for k in ("bp_breaks_bic", "pelt", "bocpd_resets"): emitG(f"thy break {c} {k}", (row[k] or "none").replace(";", ", ")) for k in ("chatgpt_level", "gpt3_level", "dalle2_level", "gpt4_level", "o1_level", "chatgpt_slope"): emitG(f"thy break {c} {k}", float(row[k])); emitG(f"thy break {c} {k} p", float(row[k + "_p"])) for k in ("placebo_level_p", "placebo_slope_p"): emitG(f"thy break {c} {k}", float(row[k])) emitG(f"thy break {c} chatgpt slope pct", f"{100 * float(row['chatgpt_slope']):.1f}"); emitG(f"thy break {c} chatgpt level pct", f"{100 * float(row['chatgpt_level']):.1f}") p = os.path.join(R, "results/graph/category_flows_yearly.csv") if os.path.exists(p): rows = list(csv.DictReader(open(p))); per = lambda y: "baseline" if int(y) <= 2022 else "post" outf = {}; inf = {}; tout = {}; tin = {} for row in rows: pd_ = per(row["citing_year"]); n = int(row["n"]) if row["citing_cat"] in THY: outf[(row["citing_cat"], row["cited_cat"], pd_)] = outf.get((row["citing_cat"], row["cited_cat"], pd_), 0) + n; tout[(row["citing_cat"], pd_)] = tout.get((row["citing_cat"], pd_), 0) + n if row["cited_cat"] in THY: inf[(row["cited_cat"], row["citing_cat"], pd_)] = inf.get((row["cited_cat"], row["citing_cat"], pd_), 0) + n; tin[(row["cited_cat"], pd_)] = tin.get((row["cited_cat"], pd_), 0) + n for (c, d, pd_), n in sorted(outf.items()): if tout[(c, pd_)] and (n / tout[(c, pd_)] >= 0.01 or d in ("llm", "ai_agents", "generative_ai", "foundation_models", "ai_safety", "quantum_computing")): emitG(f"thy flow {c} to {d} {pd_} pct", f"{100 * n / tout[(c, pd_)]:.1f}") for (c, s, pd_), n in sorted(inf.items()): if tin[(c, pd_)] and (n / tin[(c, pd_)] >= 0.01 or s in ("llm", "ai_agents", "generative_ai", "foundation_models", "ai_safety", "quantum_computing")): emitG(f"thy inflow {c} from {s} {pd_} pct", f"{100 * n / tin[(c, pd_)]:.1f}") for (c, pd_), n in sorted(tout.items()): emitG(f"thy flow {c} total {pd_}", n) for (c, pd_), n in sorted(tin.items()): emitG(f"thy inflow {c} total {pd_}", 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", "ml_theory", "deep_learning_architectures", "quantum_computing", "cybersecurity"] for row in csv.DictReader(open(p)): a, b, y = row["cat_a"], row["cat_b"], row["year"] if a in THY and (b in partners or b in THY): c, q = a, b elif b in THY and (a in partners or a in THY): c, q = b, a else: continue n = int(row["n"]); emitG(f"thy cooc {c} {q} {y}", n) m = aggG.get((c, y), [0, 0])[0] if m: emitG(f"thy cooc {c} {q} {y} pct", f"{100 * n / m:.1f}") if q in THY: m2 = aggG.get((q, y), [0, 0])[0] if m2: emitG(f"thy cooc {q} {c} {y} pct", f"{100 * n / m2:.1f}") from collections import Counter as _C for ar in ("theory", "software_pl"): p = os.path.join(R, f"literature_synthesis/landmarks/{ar}_landmarks.csv") if os.path.exists(p): L = list(csv.DictReader(open(p))); emitG(f"thy land {ar} n", len(L)) emitG(f"thy land {ar} post", sum(r["period"] == "post" for r in L)); emitG(f"thy land {ar} dated twentysix", sum(r["year"] == "2026" for r in L)) emitG(f"thy land {ar} abstract", sum(r["has_abstract"] == "True" for r in L)); emitG(f"thy land {ar} oa", sum(r["is_oa"] == "True" for r in L)) for t, n in _C(r["type"] for r in L).items(): emitG(f"thy land {ar} type {t}", n) for c, n in _C(r["primary_category"] for r in L).items(): emitG(f"thy land {ar} cat {c}", n) p = os.path.join(R, "literature_synthesis/agent_G/contributions.csv") if os.path.exists(p): L = list(csv.DictReader(open(p))); emitG("thy audit n", len(L)) for v, n in _C(r["verdict"] for r in L).items(): emitG(f"thy audit verdict {v}", n) for v, n in _C(r["contribution_type"] for r in L).items(): emitG(f"thy audit type {v}", n) emitG("thy audit id collision", sum(r["id_resolves_to_other_work"] == "yes" for r in L)) emitG("thy audit in scope research post", sum(r["verdict"] == "in_scope_research" and r["period"] == "post" for r in L)) emitG("thy audit in scope research", sum(r["verdict"] == "in_scope_research" for r in L)) for c in THY: g = [r for r in L if r["primary_category"] == c]; emitG(f"thy audit {c} n", len(g)); emitG(f"thy audit {c} in scope research", sum(r["verdict"] == "in_scope_research" for r in g)); emitG(f"thy audit {c} misclassified", sum(r["verdict"] == "misclassified" for r in g)) _seenG = set(); _outG = [] for line in extraG: n = line.split("}")[0] if n in _seenG: continue _seenG.add(n); _outG.append(line) open(os.path.join(R, "manuscript/tables/numbers.tex"), "a").write("% ---- Part VII-a theory macros (Agent G) ----\n" + "\n".join(_outG) + "\n") print(len(_outG), "Part VII-a macros appended")