MachineSex/paper/arxiv/md2tex.py
Giorgio Gilestro ab3dc10587 Restructure: descriptive tier and experiment names, paper/manuscript
- paper/pnas -> paper/manuscript (venue-neutral)
- configs/layer1 -> configs/inheritance, src/knowledge -> src/inheritance
  (imported as `inheritance`), make layer1 -> make inheritance; layer2 alias dropped
- inheritance and trained-network bundles named after the manuscript figure
  they feed (fig2_grounding_sweep, figS3_rebaselining, ...), or descriptively
  where they feed none; configs keep their `experiment:` value so parquet
  hashes are unchanged, only output.dir moves
- figure scripts, SI figure sources, notebooks, REPRODUCING.md, README and the
  SI Methods/tables updated; make clean no longer deletes tracked manifests;
  reproduce.sh hashes the s{seed}/ layouts too

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y64o8FKP7rCuXzC48pxpMm
2026-09-13 17:00:40 +01:00

177 lines
7.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

r"""Deterministic Markdown -> LaTeX converter for the arXiv preprint (paper-specific, not general).
Converts `paper/the-evolution-of-sex-for-ai.md` into `body.tex`, which `main.tex` inputs. Kept
deliberately dumb and auditable: the paper uses a small Markdown subset (##/### headings, bold,
italics, inline code, links, bullet/numbered lists, one blockquote, horizontal rules, and
`(Figure: \`path\`.)` figure references), and this script handles exactly that subset. Re-run after
editing the Markdown; the Markdown remains the source of truth.
Usage: python paper/arxiv/md2tex.py
"""
from __future__ import annotations
import re
from pathlib import Path
SRC = Path(__file__).resolve().parents[1] / "the-evolution-of-sex-for-ai.md"
OUT = Path(__file__).resolve().parent / "body.tex"
# Figure references in the text -> (graphics file under figs/, caption).
FIGURES = {
"results/figS13_mating_breadth/E14.png": ("figs/E14.pdf",
"Mating systems (E14): the best mate-pool breadth shrinks as skills get more entangled. "
"(A) best fitness peaks at intermediate breadth on rugged landscapes; (B) the population mean "
"is monotonically favoured by promiscuity; (C) diversity is monotonically destroyed by it."),
"results/fig5_speciation_bdm/E12.png": ("figs/E12.pdf",
"Model speciation, analytic (E12): hybrid fitness vs divergence traces compatible $\\rightarrow$ "
"outbreeding depression $\\rightarrow$ inviability; the isolation cliff arrives earlier the "
"denser the incompatibilities (epistasis), and damage grows super-linearly (the Orr--Turelli "
"snowball)."),
"results/speciation_real/speciation_real.png": ("figs/speciation_real.pdf",
"Model speciation in real weights (E13). (A) the merge barrier decomposed by alignment "
"strength: the independent-init barrier is a coordinate artefact (removed by alignment); the "
"conflict barrier survives even the full function-preserving symmetry group. (B) the isolation "
"cliff: residual barrier rises and hybrid accuracy falls ($0.97 \\rightarrow 0.03$) with "
"functional conflict. (C) the pre-registered emergent test: divergent-but-compatible "
"specialists develop no isolation at any divergence --- the merge instead rescues them "
"(Fisher--Muller)."),
}
UNICODE = {
"": "---", "": "--", "": r"\(\rightarrow\)", "": r"\(\approx\)",
"": r"\(\geq\)", "×": r"\(\times\)", "·": r"\(\cdot\)", "μ": r"\(\mu\)",
}
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:
"""Escape + convert inline markup. Code spans are protected, then bold, italic, links."""
parts = re.split(r"(`[^`]*`)", s)
out = []
for p in parts:
if p.startswith("`") and p.endswith("`") and len(p) >= 2:
out.append(r"\texttt{" + esc(p[1:-1]) + "}")
else:
p = esc(p)
p = re.sub(r"\[([^\]]+)\]\((https?://[^)]+)\)", r"\\href{\2}{\1}", p)
p = re.sub(r"\*\*([^*]+)\*\*", r"\\textbf{\1}", p)
p = re.sub(r"\*([^*]+)\*", r"\\emph{\1}", p)
p = re.sub(r'"([^"]+)"', r"``\1''", p) # straight quotes -> LaTeX quotes
out.append(p)
return "".join(out)
def figure_block(md_path: str) -> str:
gfx, caption = FIGURES[md_path]
return ("\\begin{figure*}[t]\\centering\n"
f"\\includegraphics[width=\\textwidth]{{{gfx}}}\n"
f"\\caption{{{caption}}}\n\\end{{figure*}}\n")
def convert(text: str) -> str:
"""Block-based conversion: soft-wrapped lines are joined per paragraph/item BEFORE inline
conversion, so bold/italic/code spans and figure pointers crossing a line break work."""
fig_queue: list[str] = []
def fig_sub(m):
path = m.group(1)
if path in FIGURES:
fig_queue.append(figure_block(path))
return ""
return m.group(0)
lines = text.split("\n")
i = 0
# Skip the header block (title/subtitle/author) up to and including the first horizontal rule:
# main.tex composes the title page itself.
while i < len(lines) and lines[i].strip() != "---":
i += 1
i += 1
# Group into blocks separated by blank lines; a block is a heading, rule, quote, list, or paragraph.
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_para(joined: str, out: list[str]) -> None:
joined = re.sub(r"\(Figure: `([^`]+)`\.?\)", fig_sub, joined)
joined = re.sub(r"\s{2,}", " ", joined).strip()
if joined:
out.append(inline(joined))
out.append("")
while fig_queue:
out.append(fig_queue.pop(0)); out.append("")
def emit_table(block: list[str], out: list[str]) -> None:
"""Pipe table -> small-font tabular with wrapped paragraph columns (full text width)."""
rows = [[c.strip() for c in line.strip().strip("|").split("|")] for line in block]
header, body = rows[0], [r for r in rows[2:]] # rows[1] is the |---| separator
n = len(header)
widths = " ".join([f"p{{{0.92 / n:.3f}\\textwidth}}"] * n)
out.append("\\medskip\\noindent\\begin{center}\\footnotesize") # non-floating: stays in place
out.append(f"\\begin{{tabular}}{{{widths}}}")
out.append("\\hline")
out.append(" & ".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.append("\\hline\\end{tabular}\\end{center}\\medskip")
out.append("")
out: list[str] = []
for block in blocks:
first = block[0].strip()
if 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("## "):
out.append(f"\\section*{{{inline(first[3:])}}}"); out.append("")
elif first.startswith("### "):
out.append(f"\\subsection*{{{inline(first[4:])}}}"); out.append("")
elif first.startswith("> "):
joined = " ".join(l.strip().lstrip("> ").strip() for l in block)
out.append("\\begin{quote}" + inline(joined) + "\\end{quote}"); 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] = items[-1] + " " + s # soft-wrapped continuation of the item
for it in items:
it = re.sub(r"\(Figure: `([^`]+)`\.?\)", fig_sub, it)
out.append("\\item " + inline(it.strip()))
out.append(f"\\end{{{env}}}"); out.append("")
while fig_queue:
out.append(fig_queue.pop(0)); out.append("")
else:
emit_para(" ".join(l.strip() for l in block), out)
return "\n".join(out) + "\n"
if __name__ == "__main__":
OUT.write_text(convert(SRC.read_text()))
print(f"wrote {OUT}")