r"""Build the manuscript-draft PDF from main.md (Markdown stays the source of truth). Adapted from paper/arxiv/md2tex.py (same Markdown subset + pipe tables), with one addition: standalone `*(FIG:name)*` markers place the publication figures produced by make_figs.py (unified, lettered, codename-free panels re-plotted from the committed artifacts). Run make_figs.py before building when results change. Usage: python paper/pnas/build.py && (cd paper/pnas && tectonic main.tex) """ from __future__ import annotations import re import shutil from pathlib import Path ROOT = Path(__file__).resolve().parents[2] HERE = Path(__file__).resolve().parent SRC = HERE / "main.md" OUT = HERE / "body.tex" SI_FIG_DIR = HERE / "figs" / "si" import sys # noqa: E402 sys.path.insert(0, str(HERE)) from si_figures import SI_FIGURES # noqa: E402 (supplementary figures, numbered by order in si.md) # figure name -> (single publication PDF from make_figs.py, caption) FIGURES: dict[str, tuple[list[str], str]] = { "fig1": (["paper/pnas/figs/fig1a.pdf", "paper/pnas/figs/fig1b.pdf"], "A map of the study. (A) Each row is a biological mechanism the paper borrows, each column a level " "of realism at which it is tested: an inheritance model (an exact simulation of knowledge " "transmission, green), trained neural networks measured against exact oracles (blue), and language " "models (blue). Filled cells name the experiments run at each level and, in the corner, the figure " "or table reporting them; grey cells were not run, either because the result is " "established in prior work (23, 34) or because that level adds no new test for that question. The " "inheritance model is the reference column: it sets the expectation the real-model experiments are " "read against. (B) The change of viewpoint the transfer rests on. A group of models is usually " "pictured as a society in space, contemporaries exchanging messages. The couplings studied here run " "between generations: training on model output (inheritance), weight-space merging (recombination), " "and verified real data entering each generation (immigration from reality). That is a society in " "time, which is what population genetics describes. Dots are capabilities: the rare one (gold) is " "lost under single-parent inheritance, reassembled by merging complementary parents, and re-supplied " "by grounding."), "fig2": (["paper/pnas/figs/fig2.pdf"], "How much real data stops model collapse. (A) An image-generating network (a variational " "autoencoder) is trained on handwritten digits, then a fresh copy is trained only on the digits the " "previous one drew, for fifteen generations, with no real data added. Each row is a later generation " "(0, 4, 8, 12, 15) and each column a randomly chosen drawing. The thirty kinds of digit (ten digits " "$\\times$ three stroke thicknesses, some kinds rare) collapse to one blurred shape; an independent " "classifier confirms that the number of kinds still drawn falls from 30 to 1, while adding 10\\% real " "digits each generation keeps all 30 (Fig.~S7; 4 replicates). (B) The same question in the " "inheritance model, the exact simulation: 1,000 knowledge items, 200 samples drawn per generation, " "and a fraction $g$ of fresh real samples mixed in. Points are the diversity the population settles " "at after 500 generations (mean and 95\\% CI over 100 lineages), the dashed line the exact prediction " "(the immigration--drift equilibrium), the dotted line the diversity of the real data itself. The " "curve is smooth, so any threshold is a choice: the red line marks the $g$ at which 95\\% of the real " "data's diversity is kept, about 0.05 (bootstrap CI shaded). The hollow point at $g = 0$ has not yet " "reached its equilibrium of zero. The trained image model needed about twice this fraction, because " "a trained network is not the exact copier the simulation assumes (Fig.~S2)."), "fig4": (["paper/pnas/figs/fig4.pdf"], "A population of language models over six generations. (A) The set-up. Three lineages start " "from one frozen 1.5-billion-parameter base (Qwen2.5-1.5B). Each generation, every lineage learns " "one new skill from a public dataset by continuing to train its parent's adapter (300 new examples " "plus 150 replayed from earlier skills), may merge with a partner according to its arm's rule " "(weights averaged at a ratio chosen on validation data), and is tested on all six skills by a " "verifier; the child becomes the next parent. The six skills are taken in rotated order, so a " "partner knows things a lineage lacks early on (complementarity 1.0) and nothing it lacks by the " "end (0.0). Three training seeds. (B) Accuracy over all six skills of the best lineage (mean and " "95\\% CI). Never merging and merging only when it beats keeping the parent finish level (0.80 and " "0.79); merging with a contemporary every generation collapses to 0.27, beginning when partners " "stop being complementary; a control that merges through generation 2 and then stops (dashed) " "matches the declinable arm in every seed, and a single model taught the whole syllabus alone " "(diamond) matches the population. (C) How often the declinable lineages refused a merge (bars) " "against partner complementarity (lines), under the rotated syllabus and under a second syllabus in " "which complementarity is zero at the start, peaks mid-way and returns to zero. Refusals rise with " "generation under both; with generation held fixed they do not track complementarity (partial " "Spearman $\\rho = -0.07$, 95\\% CI $-0.21$ to $0.09$, $n = 36$). (D--F) The simulation that " "motivated the design: 60 agents evolving on a rugged fitness landscape with all four mechanisms " "(grounded evaluation, recombination, diversity preservation, mutation) and one removed per arm (12 " "replicates; mean and 95\\% CI). Removing grounded evaluation, so that agents are scored on " "agreement with the crowd instead of on the truth, collapses the population onto a confident but " "wrong consensus (D, F); removing recombination or diversity preservation strands it below the " "optimum (D) and drains diversity fastest (E). Each removal fails in its own way."), "fig5": (["paper/pnas/figs/fig5.pdf"], "Model speciation: when two lineages can no longer merge. (A, B) Small image classifiers " "(multilayer perceptrons) forked from one trained base. Two networks that compute the same function " "can still differ in their weights, because hidden units can be renumbered and rescaled without " "changing the output; alignment undoes this before averaging. The merge error barrier is how much " "worse the average of two networks is than the networks themselves. (A) Two copies trained from " "different random starts on the same task have a barrier that alignment removes almost entirely " "(0.04 to 0.001); two trained on conflicting labels (the same images, some classes relabelled) keep " "theirs (0.50), and their average is useless (3 replicates). (B) Sweeping the share of classes in " "conflict moves the merged model's accuracy from 0.97 to 0.03. (C) Language models: two specialists " "share a set of ambiguous questions (``sort this list'', direction unstated) and are taught opposite " "conventions. As the share of conflicting training grows, each parent stays good under its own " "convention while the merged model falls below both, in all three seeds (95\\% CI shaded). (D) The " "control: specialists trained longer and longer on different tasks, with no conflict, merge better, " "not worse, in every seed. (E, F) The simulation: 20-position genotypes carrying incompatibilities " "of the Bateson--Dobzhansky--Muller kind. Hybrid fitness tracks the parents while lineages are " "compatible, then crashes, sooner the denser the incompatibilities (E), and the probability of a " "non-viable hybrid rises with divergence (F). What breaks merging is conflicting conventions on " "shared machinery, not distance or specialisation as such."), "fig3": (["paper/pnas/figs/fig3.pdf"], "Merging language-model specialists: when it helps, and predicting when it will hurt. All models " "are built from one frozen base (Qwen2.5) plus a LoRA adapter, a small set of extra weights trained " "on one family of tasks (list puzzles, string puzzles or arithmetic); a verifier marks every answer " "right or wrong, and accuracy is the share marked right on held-out questions. (A) The models " "compared: the base alone; three specialists (one adapter each); their merge by averaging the " "adapters; their merge after dropping the changes on which the parents conflict (TIES); and routing, " "which keeps the specialists separate and sends each question to the one that owns it. (B) Easy " "tasks, 0.5-billion-parameter base, five training seeds (fixed test sets; mean and 95\\% CI). Both " "merges beat the best single specialist on the weakest task family (paired t-test over seeds, " "$p < 10^{-4}$), and the interference-aware merge beats it overall ($p = 0.006$; the plain average " "$p = 0.09$, ahead in 4 of 5 seeds); the two merges do not differ from each other. Only merged models " "are competent on every family. (C) Deliberately hard tasks, 7-billion-parameter base, three seeds. " "Averaging only matches the best specialist overall ($p = 0.96$) although it lifts the weakest " "family ($p = 0.009$); routing beats averaging overall ($p = 0.007$, ahead in every seed) and beats " "the best specialist on both measures ($p = 0.018$ and $0.014$). With three seeds, some comparisons " "that hold in every seed are not significant (ns). (D) Predicting merge damage before merging: 39 " "pairs of specialists built along three axes, parents taught contradictory conventions (red), " "parents merely trained longer on different tasks (blue), and parents sharing training data without " "conflict (green). The horizontal axis is how often the two parents confidently disagree when asked " "the same questions before merging; the vertical axis is the merge penalty, the accuracy the merged " "model loses relative to answering each task with the parent that owns it. Damage concentrates in " "the conflicting pairs. (E) Six pre-merge measures ranked by how strongly they track the penalty " "(absolute Spearman correlation): measures taken by asking the parents questions carry the signal, " "measures taken on the parents' weights do not; differences between individual predictors are not " "significant at this sample size (Table~S2)."), } UNICODE = { # Multi-character sequences first: esc() applies these in insertion order. "⁽ᵏ⁾": r"\(^{(k)}\)", "⁻³": r"\(^{-3}\)", "⁻⁴": r"\(^{-4}\)", "—": "---", "–": "--", "→": r"\(\rightarrow\)", "≈": r"\(\approx\)", "≥": r"\(\geq\)", "≳": r"\(\gtrsim\)", "×": r"\(\times\)", "·": r"\(\cdot\)", "μ": r"\(\mu\)", "ρ": r"\(\rho\)", "≤": r"\(\leq\)", "≪": r"\(\ll\)", "∝": r"\(\propto\)", # Greek and math symbols: the typewriter font has no Greek, so these must become math. "α": r"\(\alpha\)", "β": r"\(\beta\)", "ε": r"\(\varepsilon\)", "Δ": r"\(\Delta\)", "Σ": r"\(\Sigma\)", "∈": r"\(\in\)", "≠": r"\(\neq\)", "±": r"\(\pm\)", "∼": r"\(\sim\)", "−": r"\(-\)", "²": r"\(^{2}\)", "³": r"\(^{3}\)", "⁴": r"\(^{4}\)", } SPECIALS = {"&": r"\&", "%": r"\%", "#": r"\#", "_": r"\_", "$": r"\$", "~": r"\textasciitilde{}", "^": r"\textasciicircum{}"} def esc(s: str) -> str: s = s.replace("\\", r"\textbackslash{}") for k, v in SPECIALS.items(): s = s.replace(k, v) for k, v in UNICODE.items(): s = s.replace(k, v) return s def inline(s: str) -> str: # Code spans are stashed behind sentinels *before* emphasis is applied, so that an italic or # bold span containing `code` is still matched as one span (splitting first would break it). codes: list[str] = [] def stash(m: re.Match) -> str: codes.append(r"\texttt{" + esc(m.group(0)[1:-1]) + "}") return f"\x00{len(codes) - 1}\x00" s = re.sub(r"`[^`]*`", stash, s) s = esc(s) s = re.sub(r"\[([^\]]+)\]\((https?://[^)]+)\)", r"\\href{\2}{\1}", s) s = re.sub(r"\*\*([^*]+)\*\*", r"\\textbf{\1}", s) s = re.sub(r"\*([^*]+)\*", r"\\emph{\1}", s) s = re.sub(r'"([^"]+)"', r"``\1''", s) return re.sub(r"\x00(\d+)\x00", lambda m: codes[int(m.group(1))], s) def figure_env(name: str) -> str: if name in SI_FIGURES: # Supplementary figure: copy the committed per-experiment PDF beside the SI build, so the SI # stays a pure function of the results artifacts; numbered by the LaTeX counter (S-prefixed). src, caption = SI_FIGURES[name] SI_FIG_DIR.mkdir(parents=True, exist_ok=True) dst = SI_FIG_DIR / f"{name}_{(ROOT / src).name}" shutil.copyfile(ROOT / src, dst) return "\n".join([f"\\begin{{figure}}[p]\\centering % {name}", f"\\includegraphics[width=\\textwidth]{{figs/si/{dst.name}}}", f"\\caption{{{caption}}}\\label{{{name}}}", "\\end{figure}"]) pdfs, caption = FIGURES[name] incl = "\\\\[6pt]\n".join(f"\\includegraphics[width=\\textwidth]{{figs/{(ROOT / p).name}}}" for p in pdfs) lines = [f"\\begin{{figure*}}[p]\\centering % {name}", incl, f"\\caption{{{caption}}}\\label{{{name}}}", "\\end{figure*}"] return "\n".join(lines) def convert(text: str) -> str: lines = text.split("\n") # Skip the title block only when the document opens with one (main.md separates it with a rule # in the first few lines); si.md has no such block, so nothing is dropped there. i = 0 head = [n for n, ln in enumerate(lines[:10]) if ln.strip() == "---"] if head: i = head[0] + 1 blocks: list[list[str]] = [] cur: list[str] = [] for line in lines[i:]: if line.strip() == "": if cur: blocks.append(cur); cur = [] else: cur.append(line) if cur: blocks.append(cur) def emit_table(block, out): rows = [[c.strip() for c in line.strip().strip("|").split("|")] for line in block] header, body = rows[0], rows[2:] n = len(header) widths = " ".join([f"p{{{0.92 / n:.3f}\\textwidth}}"] * n) out += ["\\medskip\\noindent\\begin{center}\\footnotesize", f"\\begin{{tabular}}{{{widths}}}", "\\hline", " & ".join(inline(c) for c in header) + " \\\\ \\hline"] for r in body: r = (r + [""] * n)[:n] out.append(" & ".join(inline(c) for c in r) + " \\\\[3pt]") out += ["\\hline\\end{tabular}\\end{center}\\medskip", ""] out: list[str] = [] for block in blocks: first = block[0].strip() m = re.match(r"^\*?\(FIG:(\w+)\)\*?$", first) if m: out.append(figure_env(m.group(1))); out.append("") elif first.startswith("|") and len(block) >= 2 and set(block[1].strip()) <= set("|-: "): emit_table(block, out) elif first == "---" and len(block) == 1: out.append("\\medskip\\hrule\\medskip"); out.append("") elif first.startswith("# ") and not first.startswith("## "): continue # document title: the wrapper supplies it elif first.startswith("## "): out.append(f"\\section*{{{inline(first[3:])}}}"); out.append("") elif first.startswith("### "): out.append(f"\\subsection*{{{inline(first[4:])}}}"); out.append("") elif re.match(r"^(- |\d+\. )", first): env = "itemize" if first.startswith("- ") else "enumerate" out.append(f"\\begin{{{env}}}") items: list[str] = [] for l in block: s = l.strip() if re.match(r"^(- |\d+\. )", s): items.append(re.sub(r"^(- |\d+\. )", "", s)) else: items[-1] += " " + s for it in items: out.append("\\item " + inline(it.strip())) out.append(f"\\end{{{env}}}"); out.append("") else: joined = re.sub(r"\s{2,}", " ", " ".join(l.strip() for l in block)).strip() out.append(inline(joined)); out.append("") return "\n".join(out) + "\n" if __name__ == "__main__": import sys doc = sys.argv[1] if len(sys.argv) > 1 else "main" src = HERE / f"{doc}.md" out = HERE / ("body.tex" if doc == "main" else f"{doc}_body.tex") out.write_text(convert(src.read_text())) print(f"wrote {out}")