#!/usr/bin/env python3
"""COMPLEXA C261-C270: deterministic model-derived scientific SVG and CSV.

No external Python dependencies. No empirical data or stochastic draws are used.
To generate: python make_figures.py --generate content/supplementary
To verify:   python make_figures.py --check content/supplementary
"""
from __future__ import annotations
import argparse
import base64
import csv
import hashlib
import io
import math
import tempfile
import zipfile
from pathlib import Path
from xml.etree import ElementTree as ET

W,H = 1120,680
INK="#173047"; MID="#526477"; LIGHT="#d8e3eb"; PAPER="#ffffff"
TEAL="#0b8585"; BLUE="#3b68a7"; ORANGE="#d37834"; PURPLE="#7a59a6"
DARK="#0d2034"; GREEN="#388269"; RED="#be4d4d"
SVG_NS="http://www.w3.org/2000/svg"
ET.register_namespace("",SVG_NS)

def fmt(x):
    return f"{x:.3f}".rstrip('0').rstrip('.') if isinstance(x,(float,int)) else str(x)

def elt(node,tag,**props):
    return ET.SubElement(node,tag,{k.replace('_','-'):str(v) for k,v in props.items() if v is not None})

def txt(parent,x,y,content,size=18,color=INK,weight=400,anchor='start',**kwargs):
    node=elt(parent,"text",x=fmt(x),y=fmt(y),fill=color,font_family="DejaVu Sans, Arial, sans-serif",font_size=size,font_weight=weight,text_anchor=anchor,**kwargs)
    node.text=str(content);return node

def line(parent,x1,y1,x2,y2,color=LIGHT,width=1.2,dash=None):
    return elt(parent,"line",x1=fmt(x1),y1=fmt(y1),x2=fmt(x2),y2=fmt(y2),stroke=color,stroke_width=width,stroke_dasharray=dash)

def rect(parent,x,y,w,h,fill,rx=0,stroke=None,stroke_width=1):
    return elt(parent,"rect",x=fmt(x),y=fmt(y),width=fmt(w),height=fmt(h),fill=fill,rx=rx,stroke=stroke,stroke_width=stroke_width if stroke else None)

def path(parent,pts,color,width=3,dash=None,opacity=None):
    if len(pts)<2:return
    d='M '+' L '.join(f'{fmt(x)} {fmt(y)}' for x,y in pts)
    return elt(parent,"path",d=d,fill="none",stroke=color,stroke_width=width,stroke_linejoin="round",stroke_linecap="round",stroke_dasharray=dash,opacity=opacity)

def circ(parent,x,y,r,fill,stroke=None,sw=1.5):
    return elt(parent,"circle",cx=fmt(x),cy=fmt(y),r=fmt(r),fill=fill,stroke=stroke,stroke_width=sw if stroke else None)

def new(number,title,subtitle,description):
    root=ET.Element("svg",{"xmlns":SVG_NS,"width":str(W),"height":str(H),"viewBox":f"0 0 {W} {H}","role":"img","aria-labelledby":f"t{number} d{number}"})
    ET.SubElement(root,"title",{"id":f"t{number}"}).text=f"Figura {number}.1. {title}"
    ET.SubElement(root,"desc",{"id":f"d{number}"}).text=description
    rect(root,0,0,W,H,PAPER)
    rect(root,0,0,W,8,TEAL)
    txt(root,55,62,f"C{number}  /  COMPLEXA",15,TEAL,700)
    txt(root,55,105,title,28,INK,700)
    txt(root,55,137,subtitle,16,MID)
    line(root,55,160,1065,160,LIGHT,1.6)
    return root

def footer(root,message):
    line(root,55,618,1065,618,LIGHT,1)
    txt(root,55,643,message,14,MID)
    txt(root,1065,643,"Modelo didáctico  ·  Cálculo reproducible",12,MID,anchor="end")

def coord_axes(root,x0=104,y0=231,w=905,h=285,xmin=0,xmax=1,ymin=0,ymax=1,xticks=5,yticks=5,xlabel='',ylabel='',yfmt=lambda x:fmt(x),xfmt=lambda x:fmt(x)):
    x=lambda v:x0+(v-xmin)/(xmax-xmin)*w
    y=lambda v:y0+h-(v-ymin)/(ymax-ymin)*h
    for i in range(yticks+1):
        v=ymin+(ymax-ymin)*i/yticks;yy=y(v)
        line(root,x0,yy,x0+w,yy,LIGHT,1)
        txt(root,x0-15,yy+5,yfmt(v),13,MID,anchor='end')
    for i in range(xticks+1):
        v=xmin+(xmax-xmin)*i/xticks;xx=x(v)
        line(root,xx,y0,xx,y0+h,LIGHT,0.8)
        txt(root,xx,y0+h+25,xfmt(v),13,MID,anchor='middle')
    line(root,x0,y0,x0,y0+h,INK,1.6)
    line(root,x0,y0+h,x0+w,y0+h,INK,1.6)
    if xlabel:txt(root,x0+w/2,y0+h+65,xlabel,16,INK,500,anchor="middle")
    if ylabel:txt(root,x0,y0-21,ylabel,15,INK,500)
    return x,y

def legend(root,items,x=710,y=189,step=175):
    for i,(label,color) in enumerate(items):
        x0=x+i*step
        rect(root,x0,y-12,24,6,color,rx=3);txt(root,x0+31,y-3,label,13,MID)

def serialize(root):
    ET.indent(root,space="  ")
    return b'<?xml version="1.0" encoding="UTF-8"?>\n'+ET.tostring(root,encoding="utf-8",xml_declaration=False)+b'\n'

def csv_bytes(headers,rows):
    out=io.StringIO(newline='')
    w=csv.writer(out,lineterminator='\n');w.writerow(headers)
    for row in rows:w.writerow([f"{v:.10f}" if isinstance(v,float) else v for v in row])
    return out.getvalue().encode('utf-8')

def put(root,n,headers,rows,results):
    svg=serialize(root)
    if not all(q in svg for q in [b'<svg',b'<title',b'<desc']):raise AssertionError("SVG metadata absent")
    ET.fromstring(svg)
    results[f"figures/complexa-c{n}-fig01.svg"]=svg
    results[f"repro/c261-270/c{n}.csv"]=csv_bytes(headers,rows)

def h2(p):
    return -p*math.log2(p)-(1-p)*math.log2(1-p) if 0<p<1 else 0.

def render261(results):
    n=261;root=new(n,"Selección y fidelidad de transmisión","La reproducción diferencial no basta: el cambio acumulado requiere transmisión.","Frecuencia inicial de tipo A de 0,25 y posterior a la selección de 0,4. La descendencia final depende linealmente de la fidelidad h entre cero y uno.")
    rows=[]
    for j in range(101):
        h=j/100.;selected=.4;final=.25+.15*h
        rows.append([h,.25,selected,final,selected-.25,final-.25])
    x,y=coord_axes(root,xmin=0,xmax=1,ymin=.2,ymax=.45,xticks=5,yticks=5,xlabel="Probabilidad h de conservar el tipo del progenitor",ylabel="Frecuencia esperada de A",yfmt=lambda v:f'{v:.2f}')
    path(root,[(x(r[0]),y(r[3])) for r in rows],TEAL,5)
    path(root,[(x(0),y(.4)),(x(1),y(.4))],ORANGE,2.5,'9 6')
    path(root,[(x(0),y(.25)),(x(1),y(.25))],BLUE,2.5,'4 5')
    circ(root,x(.5),y(.325),6,TEAL)
    txt(root,x(.5)+17,y(.325)-14,"h=0,5 → 0,325",15,INK,600)
    legend(root,[("Después de transmisión",TEAL),("Selección fiel",ORANGE),("Frecuencia inicial",BLUE)],x=310,y=189,step=240)
    footer(root,"p₀=0,25 · wA=2 · wB=1 · pselección=0,40 · h∈[0,1]")
    assert abs(rows[50][3]-.325)<1e-12
    put(root,n,["fidelidad_h","frecuencia_inicial_A","frecuencia_tras_seleccion","frecuencia_siguiente_A","ganancia_seleccion","ganancia_transmitida"],rows,results)

def render262(results):
    n=262;root=new(n,"Wright–Fisher: muestreo de cuatro copias","Una generación neutral puede perder la variante aunque su frecuencia esperada se conserve.","Distribución binomial exacta de j copias A tras una generación con M=4 e i=1. Probabilidades 81,108,54,12,1 sobre 256.")
    ps=[math.comb(4,j)*(.25**j)*(.75**(4-j)) for j in range(5)]
    rows=[[j,math.comb(4,j),ps[j],ps[j]*256,j*ps[j]] for j in range(5)]
    x,y=coord_axes(root,xmin=-.5,xmax=4.5,ymin=0,ymax=.46,xticks=10,yticks=4,xlabel="Copias de A en la siguiente generación",ylabel="Probabilidad exacta P(j)",xfmt=lambda v:fmt(v) if abs(v-round(v))<1e-8 else "")
    for j,p in enumerate(ps):
        left=x(j-.29);rect(root,left,y(p),x(j+.29)-left,y(0)-y(p),ORANGE if j==0 else TEAL,rx=4)
        txt(root,x(j),y(p)-13,f"{int(round(p*256))}/256",16,INK,600,anchor="middle")
    txt(root,x(2.6),y(.405),"P(pérdida)=81/256",17,ORANGE,700)
    txt(root,x(2.6),y(.364),"E[j]=1 · Var(j)=3/4",16,INK)
    footer(root,"M=4 copias · i=1 · p=1/4 · actualización generacional binomial")
    assert abs(sum(ps)-1)<1e-12 and abs(sum(j*p for j,p in enumerate(ps))-1)<1e-12
    put(root,n,["copias_A_siguiente","combinaciones_binomiales","probabilidad","numerador_sobre_256","contribucion_a_esperanza"],rows,results)

def moran_fix(i,M,r):
    return i/M if r==1 else (1-r**(-i))/(1-r**(-M))

def render263(results):
    n=263;root=new(n,"Moran: selección y fijación eventual","La ventaja reproductiva cambia probabilidades de absorción, no garantiza la fijación.","Fijación eventual de A desde i=0..4 copias en el proceso Moran de M=4 para ventajas relativas r=0,5, 1 y 2.")
    rows=[]
    for i in range(5):rows.append([i,i/4,moran_fix(i,4,.5),moran_fix(i,4,1),moran_fix(i,4,2)])
    x,y=coord_axes(root,xmin=0,xmax=4,ymin=0,ymax=1,xticks=4,yticks=5,xlabel="Número inicial i de copias A (M=4)",ylabel="Probabilidad de fijación eventual")
    for c,idx in [(ORANGE,2),(MID,3),(TEAL,4)]:
        pts=[(x(r[0]),y(r[idx])) for r in rows];path(root,pts,c,3.8)
        for xx,yy in pts:circ(root,xx,yy,5,c)
    legend(root,[("r=0,5 desfavorable",ORANGE),("r=1 neutral",MID),("r=2 favorable",TEAL)],x=220,y=184,step=264)
    txt(root,x(1)+13,y(8/15)-16,"r=2: 8/15",16,TEAL,600)
    footer(root,"Reproducción proporcional a r y reemplazo uniforme · estados 0 y 4 absorbentes")
    assert abs(moran_fix(1,4,2)-8/15)<1e-12
    assert abs(moran_fix(1,4,.5)-1/15)<1e-12
    put(root,n,["copias_iniciales_A","frecuencia_inicial_A","fijacion_r_0_5","fijacion_r_1","fijacion_r_2"],rows,results)

def mutsel(q,s=.2,u=.01):
    qsel=(1-s)*q/(1-s*q)
    return qsel+u*(1-qsel)

def render264(results):
    n=264;root=new(n,"Equilibrio mutación–selección","El punto fijo interior q*=u/s no es idéntico a la frecuencia tras cada etapa.","Trayectorias deterministas para alelo desfavorecido a con s=0,2, mutación directa u=0,01 y q inicial cero, 0,2 o 0,5.")
    q=[0,.2,.5];rows=[]
    for t in range(36):
        rows.append([t,*q,.05])
        q=[mutsel(v) for v in q]
    x,y=coord_axes(root,xmin=0,xmax=35,ymin=0,ymax=.51,xticks=7,yticks=5,xlabel="Generaciones",ylabel="Frecuencia de la variante a")
    for idx,c in [(1,TEAL),(2,BLUE),(3,ORANGE)]:path(root,[(x(r[0]),y(r[idx])) for r in rows],c,4)
    line(root,x(0),y(.05),x(35),y(.05),MID,2,"8 6")
    legend(root,[("Inicio q=0",TEAL),("q=0,2",BLUE),("q=0,5",ORANGE)],x=272,y=187,step=230)
    txt(root,x(27),y(.05)-14,"q*=0,05",16,MID,600)
    footer(root,"Selección antes de mutación · s=0,2 · u=0,01 · q*=u/s")
    assert abs(mutsel(.05)-.05)<1e-12 and abs(mutsel(.5)-.45)<1e-12
    put(root,n,["generacion","frecuencia_inicial_0","frecuencia_inicial_0_2","frecuencia_inicial_0_5","equilibrio_interior"],rows,results)

def quasi(x):return (.1+1.7*x)/(1+x)

def render265(results):
    n=265;root=new(n,"Cuasiespecies: intercambio entre dos tipos","Reproducción 2:1 y mutación recíproca 0,1 sostienen una mezcla no trivial.","Convergencia determinista de la fracción de tipo A según matriz W de reproducción y mutación; valor estacionario (0,7+raíz de 0,89)/2.")
    star=(.7+math.sqrt(.89))/2
    p=[0,1];rows=[]
    for t in range(26):
        rows.append([t,p[0],p[1],star,1+star]);p=[quasi(q) for q in p]
    x,y=coord_axes(root,xmin=0,xmax=25,ymin=0,ymax=1,xticks=5,yticks=5,xlabel="Generaciones de reproducción y mutación",ylabel="Frecuencia del tipo A")
    for idx,c in [(1,TEAL),(2,ORANGE)]:path(root,[(x(r[0]),y(r[idx])) for r in rows],c,4)
    line(root,x(0),y(star),x(25),y(star),BLUE,2,"8 5")
    legend(root,[("Inicio 100% B",TEAL),("Inicio 100% A",ORANGE),("Equilibrio",BLUE)],x=240,y=189,step=240)
    txt(root,x(9),y(star)-13,f"x*={star:.6f}",16,BLUE,600)
    footer(root,"W=[[1,8;0,1],[0,2;0,9]] (por columnas) · λ₁=1,821699")
    assert abs(quasi(star)-star)<1e-12
    put(root,n,["generacion","frecuencia_A_inicio_B","frecuencia_A_inicio_A","equilibrio_A","produccion_media_equilibrio"],rows,results)

def render266(results):
    n=266;root=new(n,"Epistasis: invertir el efecto de un locus","El efecto de modificar g₁ depende del fondo genético g₂.","Mapa fenotípico exacto y=g1+g2−2*g1*g2; cambiar g1 produce efecto +1 si g2=0 y −1 si g2=1.")
    rows=[]
    for g2 in (0,1):
        for g1 in (0,1):rows.append([g1,g2,g1+g2-2*g1*g2,1 if g2==0 else -1])
    x,y=coord_axes(root,xmin=0,xmax=1,ymin=-.12,ymax=1.12,xticks=1,yticks=4,xlabel="Estado del locus g₁",ylabel="Fenotipo y",yfmt=lambda v:f'{v:.2f}')
    for g2,c in [(0,TEAL),(1,ORANGE)]:
        arr=[r for r in rows if r[1]==g2];path(root,[(x(r[0]),y(r[2])) for r in arr],c,4)
        for r in arr:circ(root,x(r[0]),y(r[2]),7,c)
    legend(root,[("Fondo g₂=0 (+1)",TEAL),("Fondo g₂=1 (−1)",ORANGE)],x=390,y=185,step=275)
    txt(root,x(.5),y(.58)-46,"Contraste de interacción = −2",19,INK,650,anchor="middle")
    footer(root,"Genotipos 00 → 0, 10 → 1, 01 → 1, 11 → 0 · sin interpretación molecular atribuida")
    assert sum(r[2] for r in rows)==2
    put(root,n,["g1","g2","fenotipo_y","efecto_de_modificar_g1"],rows,results)

def render267(results):
    n=267;root=new(n,"Robustez neutral y novedades accesibles","Una red de cuatro genotipos conserva el fenotipo P y expone cuatro resultados nuevos.","Cadena neutral de cuatro genotipos con cuatro mutaciones equiprobables por estado; robusteces 1/4 en extremos, 1/2 en nodos centrales y cuatro fenotipos alternativos distintos X,Y,Z,W.")
    rows=[];robust=[.25,.5,.5,.25]
    for i,v in enumerate(robust):rows.append([i+1,1 if i in (0,3) else 2,4,v,.25,['X','Y','Z','W'][i]])
    txt(root,105,216,"Cadena de conexiones neutras",18,INK,700)
    pts=[(205+226*i,295) for i in range(4)]
    for i in range(3):line(root,pts[i][0]+34,295,pts[i+1][0]-34,295,TEAL,5)
    for i,(px,py) in enumerate(pts):
        circ(root,px,py,36,BLUE)
        txt(root,px,py+6,f"g{i+1}",19,PAPER,700,anchor="middle")
        txt(root,px,358,"Fenotipo P",13,MID,anchor="middle")
        txt(root,px,389,f"R={fmt(robust[i])}",16,TEAL,700,anchor="middle")
        line(root,px,330,px,453,ORANGE,2,"5 5")
        txt(root,px,480,"→ "+rows[i][5],19,ORANGE,700,anchor="middle")
    txt(root,560,555,"Una novedad por genotipo; cuatro novedades en la población",18,INK,550,anchor="middle")
    footer(root,"Mutaciones equiprobables · R media=3/8 · destinos nuevos X, Y, Z, W")
    assert abs(sum(robust)/4-.375)<1e-12
    put(root,n,["genotipo_numero","vecinos_neutrales","mutaciones_totales","robustez_local","frecuencia_genotipo","fenotipo_nuevo_externo"],rows,results)

def render268(results):
    n=268;root=new(n,"La misma red, tres ocupaciones estacionarias","Caminata, propuestas rechazadas y reproducción–mutación producen distribuciones diferentes.","Comparación de tres ocupaciones de la cadena neutral 1−2−3−4: proporcional al grado, uniforme con rechazos, y vector propio principal de la matriz de reproducción viable.")
    ph=(1+math.sqrt(5))/2
    a=1/(2*(1+ph));b=ph*a
    pi_walk=[1/6,1/3,1/3,1/6];pi_uniform=[.25]*4;pi_repro=[a,b,b,a]
    rows=[[i+1,pi_walk[i],pi_uniform[i],pi_repro[i]] for i in range(4)]
    x,y=coord_axes(root,xmin=.5,xmax=4.5,ymin=0,ymax=.39,xticks=8,yticks=4,xlabel="Nodo de la cadena neutral",ylabel="Frecuencia estacionaria",xfmt=lambda v:fmt(v) if abs(v-round(v))<1e-8 else "")
    for r in rows:
        center=x(r[0]);full=x(.2)-x(0)
        for k,(idx,color) in enumerate([(1,BLUE),(2,ORANGE),(3,TEAL)]):
            left=center+(k-1)*full*.7-full*.32
            rect(root,left,y(r[idx]),full*.56,y(0)-y(r[idx]),color,rx=4)
    legend(root,[("Solo vecinos",BLUE),("Rechazos",ORANGE),("Reproducción",TEAL)],x=260,y=192,step=235)
    footer(root,"Caminata π∝grado · propuesta uniforme · vector Perron de la red 1–2–3–4")
    assert abs(sum(pi_repro)-1)<1e-12 and abs(a-.190983005625)<1e-10
    put(root,n,["nodo","caminata_vecinos_validos","caminata_con_rechazos","reproduccion_mutacion"],rows,results)

def render269(results):
    n=269;root=new(n,"Selección indirecta: pigmento y enzima","La pigmentación crece por asociación, pese a tener un costo directo constante.","Aptitudes para cuatro combinaciones binarias de enzima E y pigmento P según w=1+0,20E−0,05P.")
    rows=[[E,P,1+.2*E-.05*P] for E in (0,1) for P in (0,1)]
    x,y=coord_axes(root,xmin=0,xmax=1,ymin=.88,ymax=1.26,xticks=1,yticks=4,xlabel="Presencia del pigmento P",ylabel="Aptitud reproductiva relativa",yfmt=lambda v:f'{v:.2f}')
    for e,color in [(0,ORANGE),(1,TEAL)]:
        rr=[r for r in rows if r[0]==e];path(root,[(x(z[1]),y(z[2])) for z in rr],color,4)
        for z in rr:circ(root,x(z[1]),y(z[2]),7,color)
    legend(root,[("E=0, sin enzima",ORANGE),("E=1, con enzima",TEAL)],x=320,y=190,step=300)
    txt(root,x(.47),y(1.15)-27,"Efecto directo de P: −0,05",18,INK,650,anchor="middle")
    footer(root,"w=1+0,20E−0,05P · efecto de la enzima +0,20 en ambos fondos")
    assert abs(rows[-1][-1]-1.15)<1e-12
    put(root,n,["enzima_E","pigmento_P","aptitud_relativa"],rows,results)

def render270(results):
    n=270;root=new(n,"Defensas: la preferencia depende del costo","Dos señales de detección fijas intercambian su orden según el costo de responder.","Pérdida total en diez mil situaciones de dos políticas fijas: señal amplia con 1070 respuestas y veinte amenazas omitidas, señal estricta con 149 respuestas y cincuenta amenazas omitidas.")
    rows=[]
    for i in range(81):
        c=i/10
        rows.append([c,1070*c+20*100,149*c+50*100,100*100,10000*c])
    x,y=coord_axes(root,xmin=0,xmax=8,ymin=0,ymax=12500,xticks=8,yticks=5,xlabel="Costo C por respuesta defensiva (unidades)",ylabel="Pérdida total esperada en 10.000 situaciones",yfmt=lambda v:f'{int(v):,}'.replace(',','.'))
    for idx,col in [(1,TEAL),(2,ORANGE),(3,BLUE)]:path(root,[(x(r[0]),y(r[idx])) for r in rows],col,3.8,dash="7 5" if idx==3 else None)
    cross=3000/921
    line(root,x(cross),y(0),x(cross),y(1070*cross+2000),PURPLE,2,"5 5")
    txt(root,x(cross)+18,y(6000)-30,f"Cruce C≈{cross:.3f}",16,PURPLE,700)
    legend(root,[("Señal amplia",TEAL),("Señal estricta",ORANGE),("Nunca defender",BLUE)],x=230,y=189,step=250)
    footer(root,"Escenario ficticio, no modelo clínico · 100 amenazas · daño D=100")
    assert (1070+2000)<(149+5000) and (1070*5+2000)>(149*5+5000)
    put(root,n,["costo_defensa_C","perdida_senal_amplia","perdida_senal_estricta","perdida_no_responder","perdida_responder_siempre"],rows,results)

GENERATORS=[render261,render262,render263,render264,render265,render266,render267,render268,render269,render270]

def files_to_emit():
    results={}
    for fun in GENERATORS:fun(results)
    return results

def readme():
    return '''# COMPLEXA C261–C270 — Figuras y datos exactos\n\nSuplementos **científicos, didácticos y reproducibles**, añadidos sin editar ninguno de los diez manuscritos originales. Los archivos no contienen observaciones empíricas y no permiten inferir mecanismos biológicos o clínicos sin evidencia adicional.\n\n## Reproducción\n\nRequiere Python 3.12+ y exclusivamente su biblioteca estándar. Desde la raíz del repositorio:\n\n```bash\npython content/supplementary/repro/c261-270/make_figures.py --check content/supplementary\n```\n\nPara regenerar en una carpeta separada:\n\n```bash\npython content/supplementary/repro/c261-270/make_figures.py --generate /tmp/complexa-c261-270\n```\n\nEl generador crea diez SVG con metadatos accesibles, diez CSV UTF-8 con punto decimal y un ZIP determinista. El archivo ZIP tiene orden, fechas, compresión y atributos de entradas fijados. La modalidad `--check` compara **bytes**, no solo números: cualquier modificación de los recursos, del código o de la documentación falla. El manifiesto independiente del sitio añade tamaños y SHA-256 para impedir cambios inadvertidos.\n\n## Modelos y supuestos\n\n- **C261:** población inicial p(A)=0,25, aportes de descendencia wA=2, wB=1 y fidelidad h; respuesta p'(A)=0,25+0,15h. No es heredabilidad cuantitativa.\n- **C262:** Wright–Fisher neutral con M=4 e i=1, binomial(4, 1/4); transiciones de una generación y no fijación eventual.\n- **C263:** proceso Moran de tamaño 4, reproducción relativa r en 0,5, 1 y 2; reemplazo uniforme. Fijación eventual, sin mutaciones.\n- **C264:** selección contra a con s=0,2 seguida de mutación unidireccional A→a con u=0,01; equilibrio interior q*=0,05, además del extremo absorbente q=1. El orden del ciclo está fijado.\n- **C265:** mutación recíproca de 0,1 y reproducción 2:1 entre A y B; frecuencias normalizadas, no extinción ni crecimiento absoluto.\n- **C266:** mapa binario no aditivo y=g1+g2−2g1g2; no se atribuye una interacción molecular concreta.\n- **C267:** cuatro genotipos neutrales en cadena, cada cual con cuatro destinos equiprobables; fenotipos nuevos externos X,Y,Z,W. Robustez depende de la definición de mutación.\n- **C268:** misma red de cuatro genotipos, pero tres actualizaciones diferentes: paseo entre vecinos, propuestas rechazadas y reproducción viable. No se equiparan sus relojes.\n- **C269:** enzima E y pigmento P con w=1+0,20E−0,05P. La aptitud aumenta con E y disminuye con P a igualdad de E; es un ejemplo de confusión entre asociación y efecto directo.\n- **C270:** 10.000 situaciones ficticias, 100 amenazas, daño 100; señales con 80 verdaderos positivos / 990 falsas alarmas / 20 omisiones frente a 50 / 99 / 50. Varía solo el costo de responder para comparar **reglas fijas**. No hay pacientes ni recomendaciones clínicas ni estimación de riesgos de trastornos mentales.\n\n## Precisión y límites\n\nLos cálculos usan aritmética estándar `math` de Python para funciones analíticas y muestras de resolución fija. El formato CSV imprime diez decimales para resultados reales, por lo que las cantidades mostradas están redondeadas aunque sus fórmulas se evalúen sin muestreo. Las figuras no llevan intervalos de confianza empíricos. Los resultados están condicionados a parámetros conocidos y simplificaciones declaradas.\n\nCada CSV conserva encabezados, especificación de columnas y referencias a la correspondiente figura. Se recomienda contrastar las ecuaciones, sensibilidad a parámetros y la elección del observable antes de interpretar un resultado en un sistema natural.\n'''.encode("utf-8")

def write_zip(entries):
    b=io.BytesIO()
    with zipfile.ZipFile(b,'w',compression=zipfile.ZIP_DEFLATED,compresslevel=9,strict_timestamps=True) as z:
        for name in sorted(entries):
            info=zipfile.ZipInfo(name,(2026,1,1,0,0,0))
            info.compress_type=zipfile.ZIP_DEFLATED
            info.external_attr=(0o100644<<16)
            info.create_system=3
            z.writestr(info,entries[name],compress_type=zipfile.ZIP_DEFLATED,compresslevel=9)
    return b.getvalue()

def all_files():
    data=files_to_emit()
    data['repro/c261-270/README.md']=readme()
    data['repro/c261-270/make_figures.py']=Path(__file__).read_bytes()
    archive={}
    for key,value in data.items():
        if key.startswith('figures/'):
            archive['figures/'+Path(key).name]=value
        elif key.startswith('repro/c261-270/'):
            archive['repro/'+Path(key).name]=value
    zip_bytes=write_zip(archive)
    data['zip-data/complexa-c261-c270-reproducible.zip.b64']=base64.b64encode(zip_bytes)+b'\n'
    return data,zip_bytes

def main():
    p=argparse.ArgumentParser(description=__doc__)
    group=p.add_mutually_exclusive_group(required=True)
    group.add_argument('--generate',metavar='SUPPLEMENT_DIR')
    group.add_argument('--check',metavar='SUPPLEMENT_DIR')
    args=p.parse_args();root=Path(args.generate or args.check)
    files,archive=all_files()
    assert len(files)==23,(len(files),sorted(files))
    if args.generate:
        for rel,b in files.items():
            dest=root/rel;dest.parent.mkdir(parents=True,exist_ok=True);dest.write_bytes(b)
        print('PASS: generated 10 SVG, 10 CSV, generator, README and base64 ZIP')
    else:
        for rel,b in files.items():
            target=root/rel
            if not target.exists():raise AssertionError(f'Missing {target}')
            given=target.read_bytes()
            if given!=b:raise AssertionError(f'Not bit-identical: {target}')
        print('PASS: bit-identical 10 SVG, 10 CSV, README, generator, ZIP')
    print('ZIP bytes:',len(archive),' SHA-256:',hashlib.sha256(archive).hexdigest())
    print('FILES:',len(files),'CSV data rows:',sum(x.count(b'\n')-1 for k,x in files.items() if k.endswith('.csv')))

if __name__=='__main__':main()
