#!/usr/bin/env python3
"""COMPLEXA C181-C190: ten deterministic, source-derived scientific SVG figures.

No external dependencies. Source: chapter equations and examples in published Markdown.
Generate: python make_figures.py --out PATH_TO_SUPPLEMENTARY
Verify:   python make_figures.py --check PATH_TO_SUPPLEMENTARY
All numerical inputs are explicit and are not presented as empirical observations.
"""
import argparse
import csv
import io
import itertools
import math
import hashlib
import html
from pathlib import Path

INK='#183247'; MUTED='#536a78'; BLUE='#326e91'; TEAL='#197f80'; ORANGE='#bd733b'
PLUM='#7b6297'; GRID='#e4eaf0'; BG='#ffffff'; LIGHT='#f4f8fa'; RED='#b0525a'


def fmt(v, n=4):
    return f'{v:.{n}g}'

class Figure:
    def __init__(self, n, title, subtitle):
        self.n=n; self.parts=[]
        self.raw('<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1080" height="650" viewBox="0 0 1080 650" role="img" aria-labelledby="svgtitle svgdesc">')
        self.raw(f'<title id="svgtitle">{html.escape(title)}</title><desc id="svgdesc">{html.escape(subtitle)}</desc>')
        self.raw('<rect width="1080" height="650" fill="#ffffff"/>')
        self.raw('<rect width="1080" height="9" fill="#197f80"/>')
        self.text(64,48,f'COMPLEXA / FIGURA {n}.1',14,TEAL,bold=True)
        self.text(64,91,title,28,INK,bold=True)
        self.text(64,123,subtitle,15,MUTED)
        self.line(64,143,1016,143,GRID,1.5)
    def raw(self,s): self.parts.append(s)
    def rect(self,x,y,w,h,color,stroke='none',radius=0,opacity=1):
        self.raw(f'<rect x="{x:.2f}" y="{y:.2f}" width="{w:.2f}" height="{h:.2f}" rx="{radius}" fill="{color}" stroke="{stroke}" opacity="{opacity}"/>')
    def line(self,x1,y1,x2,y2,color=INK,width=2,dash=None,opacity=1):
        d=f' stroke-dasharray="{dash}"' if dash else ''
        self.raw(f'<line x1="{x1:.2f}" y1="{y1:.2f}" x2="{x2:.2f}" y2="{y2:.2f}" stroke="{color}" stroke-width="{width}" opacity="{opacity}"{d}/>' )
    def poly(self,pts,color=TEAL,width=3,dash=None,opacity=1):
        p=' '.join(f'{x:.2f},{y:.2f}' for x,y in pts)
        d=f' stroke-dasharray="{dash}"' if dash else ''
        self.raw(f'<polyline points="{p}" fill="none" stroke="{color}" stroke-width="{width}" stroke-linejoin="round" stroke-linecap="round" opacity="{opacity}"{d}/>' )
    def circle(self,x,y,r,fill=TEAL,stroke='white',sw=2):
        self.raw(f'<circle cx="{x:.2f}" cy="{y:.2f}" r="{r:.2f}" fill="{fill}" stroke="{stroke}" stroke-width="{sw}"/>')
    def text(self,x,y,s,size=15,color=INK,bold=False,anchor='start'):
        self.raw(f'<text x="{x:.2f}" y="{y:.2f}" text-anchor="{anchor}" font-family="Arial,Helvetica,sans-serif" font-size="{size}" font-weight="{700 if bold else 400}" fill="{color}">{html.escape(str(s))}</text>')
    def arrow(self,x1,y1,x2,y2,color=TEAL,width=4,endgap=28):
        theta=math.atan2(y2-y1,x2-x1)
        xa=x2-endgap*math.cos(theta);ya=y2-endgap*math.sin(theta)
        xb=x1+24*math.cos(theta);yb=y1+24*math.sin(theta)
        self.line(xb,yb,xa,ya,color,width)
        sz=12
        left=(xa-sz*math.cos(theta)+sz*.56*math.sin(theta),ya-sz*math.sin(theta)-sz*.56*math.cos(theta))
        right=(xa-sz*math.cos(theta)-sz*.56*math.sin(theta),ya-sz*math.sin(theta)+sz*.56*math.cos(theta))
        p=' '.join(f'{v[0]:.2f},{v[1]:.2f}' for v in [(xa,ya),left,right])
        self.raw(f'<polygon points="{p}" fill="{color}"/>')
    def note(self,title,s):
        self.rect(64,572,952,53,LIGHT,GRID,8)
        self.text(82,594,title,14,TEAL,bold=True)
        self.text(82,615,s,13,MUTED)
    def output(self):
        self.raw('</svg>')
        return ('\n'.join(self.parts)+'\n').encode('utf-8')

class Plot:
    def __init__(self,fig,x0=118,y0=188,w=836,h=322,xmin=0,xmax=1,ymin=0,ymax=1,xlabel='',ylabel='',xlog=False,ylog=False):
        self.f=fig;self.x0=x0;self.y0=y0;self.w=w;self.h=h
        self.xmin=xmin;self.xmax=xmax;self.ymin=ymin;self.ymax=ymax;self.xlog=xlog;self.ylog=ylog
        assert xmin<xmax and ymin<ymax
        fig.rect(x0,y0,w,h,'#ffffff',GRID,3)
        fig.line(x0,y0+h,x0+w,y0+h,INK,1.7);fig.line(x0,y0,x0,y0+h,INK,1.7)
        fig.text(x0+w/2,y0+h+55,xlabel,15,INK,anchor='middle')
        fig.text(x0-15,y0-18,ylabel,14,MUTED)
    def xy(self,x,y):
        if self.xlog: x=math.log10(x);lo=math.log10(self.xmin);hi=math.log10(self.xmax)
        else:lo=self.xmin;hi=self.xmax
        if self.ylog: y=math.log10(y);ly=math.log10(self.ymin);hy=math.log10(self.ymax)
        else:ly=self.ymin;hy=self.ymax
        return self.x0+(x-lo)/(hi-lo)*self.w,self.y0+self.h-(y-ly)/(hy-ly)*self.h
    def ticks(self,xticks=(),yticks=()):
        for x,label in xticks:
            px,_=self.xy(x,self.ymin)
            self.f.line(px,self.y0,px,self.y0+self.h,GRID,1)
            self.f.text(px,self.y0+self.h+23,label,12,MUTED,anchor='middle')
        for y,label in yticks:
            _,py=self.xy(self.xmin,y)
            self.f.line(self.x0,py,self.x0+self.w,py,GRID,1)
            self.f.text(self.x0-10,py+4,label,12,MUTED,anchor='end')
    def curve(self,xy,color=TEAL,width=3,dash=None):self.f.poly([self.xy(x,y) for x,y in xy],color,width,dash)
    def dots(self,xy,color=TEAL,r=5):
        for x,y in xy:self.f.circle(*self.xy(x,y),r,color,'white',1.3)
    def hline(self,y,color=MUTED,dash='5,6'):self.f.line(*self.xy(self.xmin,y),*self.xy(self.xmax,y),color,1.5,dash)


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


def build181():
    f=Figure(181,'Orientación y peso de los enlaces','Tres estaciones: transferencias en un intervalo común, no tiempos de viaje')
    pos={1:(260,290),2:(730,290),3:(495,482)}
    flows=[(1,2,2),(2,1,1),(2,3,3),(3,1,4)]
    for a,b,w in flows:
        x1,y1=pos[a];x2,y2=pos[b]
        if {a,b}=={1,2}:y1+=(-13 if a==1 else 13);y2+=(-13 if a==1 else 13)
        f.arrow(x1,y1,x2,y2,color=TEAL if a!=2 or b!=1 else ORANGE,width=2+w*.85)
        mx=(x1+x2)/2;my=(y1+y2)/2
        offset= -18 if (a,b)==(1,2) else 22 if (a,b)==(2,1) else 15
        f.text(mx,my+offset,f'{w}',19,INK,bold=True,anchor='middle')
    for n,(x,y) in pos.items():
        f.circle(x,y,35,BLUE);f.text(x,y+6,str(n),22,'#ffffff',bold=True,anchor='middle')
    f.rect(812,234,188,235,LIGHT,GRID,6)
    for i,(k,st,si) in enumerate([(1,2,5),(2,4,2),(3,4,3)]):
        f.text(830,268+i*57,f'Estación {k}',14,INK,bold=True)
        f.text(830,291+i*57,f'Salida {st} / entrada {si}',12,MUTED)
    f.note('Balance comprobado','Fuerza total saliente = fuerza total entrante = 10 unidades.')
    return f.output(),csvbytes(['origen','destino','peso'],flows)


def triangles(edges):
    ed={tuple(sorted(x)) for x in edges}
    return sum(all((min(a,b),max(a,b)) in ed for a,b in [(i,j),(i,k),(j,k)]) for i,j,k in itertools.combinations(range(1,7),3))

def build182():
    deg=[3,3,2,2,2,2];possible=list(itertools.combinations(range(1,7),2));counts={}
    for subset in itertools.combinations(possible,7):
        d=[0]*6
        for a,b in subset:d[a-1]+=1;d[b-1]+=1
        if d==deg:
            t=triangles(subset);counts[t]=counts.get(t,0)+1
    assert counts=={2:6,1:24,0:24},counts
    f=Figure(182,'Los grados no fijan los triángulos','Enumeración exacta de todos los grafos simples etiquetados con grados (3,3,2,2,2,2)')
    f.text(78,182,'54 realizaciones equiprobables',18,INK,bold=True)
    for t in range(3):
        x=156+t*295;h=counts[t]/24*260
        f.rect(x,492-h,170,h,[BLUE,TEAL,ORANGE][t],radius=5)
        f.text(x+85,480-h,str(counts[t])+' grafos',20,INK,bold=True,anchor='middle')
        f.text(x+85,525,str(t)+' triángulos',16,INK,anchor='middle')
        f.text(x+85,552,fmt(100*counts[t]/54,3)+' %',13,MUTED,anchor='middle')
    f.note('Distribución exacta','El promedio es 2/3 triángulos; observar dos sucede en 6/54 = 1/9 grafos.')
    return f.output(),csvbytes(['triangulos','grafos','probabilidad'],[(t,counts[t],counts[t]/54) for t in range(3)])


def build183():
    r2=math.sqrt(2);rows=[('Árbol',3,5/3),('Cuadrado',4,4/3),('Un atajo',4+r2,(6+r2)/6),('Dos atajos',4+2*r2,(2+r2)/3)]
    f=Figure(183,'Longitud construida frente a accesibilidad','Cuatro vértices de un cuadrado: caminos físicos y costo en unidades de longitud')
    p=Plot(f,xmin=2.5,xmax=7.3,ymin=1.06,ymax=1.83,xlabel='Longitud total de enlaces',ylabel='Distancia media por la red')
    p.ticks([(3,'3'),(4,'4'),(5,'5'),(6,'6'),(7,'7')],[(1.1,'1,1'),(1.3,'1,3'),(1.5,'1,5'),(1.7,'1,7')])
    p.curve([(x,y) for _,x,y in rows],TEAL,3)
    for label,x,y in rows:
        f.circle(*p.xy(x,y),7,ORANGE)
        dx,dy={'Árbol':(15,-13),'Cuadrado':(0,-18),'Un atajo':(10,-22),'Dos atajos':(-12,-18)}[label]
        f.text(p.xy(x,y)[0]+dx,p.xy(x,y)[1]+dy,label,14,INK,bold=True,anchor='end' if label=='Dos atajos' else 'start')
    f.note('Compromiso geométrico','La menor longitud construida no minimiza el recorrido promedio.')
    return f.output(),csvbytes(['diseno','longitud_construida','distancia_media'],rows)


def build184():
    obs=[[.4,.1],[.1,.4]];expect=[[.25,.25],[.25,.25]]
    f=Figure(184,'Matriz de mezcla y referencia independiente','50 aristas no dirigidas: 20 A–A, 20 B–B y 10 A–B')
    for x0,dat,title in [(205,obs,'Mezcla observada'),(663,expect,'Extremos independientes')]:
        f.text(x0+115,195,title,18,INK,bold=True,anchor='middle')
        for i in range(2):
            for j in range(2):
                v=dat[i][j];t=v/.4
                col='#e4f3ef' if t<.4 else '#9cd1c8' if t<.8 else '#368a82'
                x=x0+j*121;y=254+i*116
                f.rect(x,y,106,100,col,'#ffffff',8)
                f.text(x+53,y+58,fmt(v,2).replace('.',','),28,INK,bold=True,anchor='middle')
        f.text(x0-24,316,'A',15,MUTED,anchor='middle');f.text(x0-24,431,'B',15,MUTED,anchor='middle')
        f.text(x0+53,241,'A',15,MUTED,anchor='middle');f.text(x0+174,241,'B',15,MUTED,anchor='middle')
    f.note('Homofilia relativa','Traza 0,80; referencia 0,50; r = (0,80 − 0,50)/(1 − 0,50) = 0,60.')
    return f.output(),csvbytes(['origen','destino','observada','independencia'],[(i,j,obs[k][l],expect[k][l]) for k,i in enumerate(['A','B']) for l,j in enumerate(['A','B'])])


def graph_edges(f,pos,edges):
    for a,b in edges:f.line(*pos[a],*pos[b],'#95aebb',3)

def build185():
    edges=list(itertools.combinations(range(1,5),2))+[(1,5),(2,5),(5,6),(1,7),(7,8)]
    degrees={i:sum(i in e for e in edges) for i in range(1,9)}
    cores={i:3 if i<=4 else 2 if i==5 else 1 for i in range(1,9)}
    assert [degrees[i] for i in range(1,9)]==[5,4,3,3,3,1,2,1]
    f=Figure(185,'El k-core depende del soporte colectivo','Ocho nodos: un clique de cuatro sostiene el 3-core; las hojas desaparecen en la poda')
    pos={1:(325,276),2:(545,276),3:(545,459),4:(325,459),5:(738,328),6:(891,328),7:(203,392),8:(97,468)}
    graph_edges(f,pos,edges)
    for n,(x,y) in pos.items():
        f.circle(x,y,30,{3:TEAL,2:ORANGE,1:BLUE}[cores[n]])
        f.text(x,y+5,str(n),18,'white',bold=True,anchor='middle')
    for i,(label,col) in enumerate([('3-core',TEAL),('2-core',ORANGE),('1-core',BLUE)]):
        f.circle(800,202+i*39,9,col);f.text(820,207+i*39,label,15,INK)
    f.note('Poda verificable','El grado inicial 3 del nodo 5 no le permite sobrevivir dentro del 3-core.')
    return f.output(),csvbytes(['nodo','grado_inicial','numero_core'],[(n,degrees[n],cores[n]) for n in range(1,9)])


def build186():
    p11=5/6;p22=4/6;p12=2/16
    mat=[[p11,p12],[p12,p22]]
    f=Figure(186,'Probabilidades de conexión entre bloques','Ocho nodos en dos grupos de cuatro: probabilidades máximoverosímiles por pares')
    f.text(155,194,'P(arista | tipos de los extremos)',18,INK,bold=True)
    for i in range(2):
        for j in range(2):
            v=mat[i][j];x=215+160*j;y=235+130*i
            f.rect(x,y,144,114,'#c5e2dd' if v>.6 else '#e5eef2',GRID,8)
            f.text(x+72,y+51,f'{100*v:.1f} %'.replace('.',','),24,INK,bold=True,anchor='middle')
            f.text(x+72,y+81,['A–A','A–B','B–A','B–B'][i*2+j],15,MUTED,anchor='middle')
    f.rect(649,240,335,238,LIGHT,GRID,7)
    f.text(673,276,'Estimaciones del manuscrito',18,INK,bold=True)
    f.text(673,321,'A–A: 5 enlaces / 6 posibles',15,MUTED)
    f.text(673,361,'B–B: 4 enlaces / 6 posibles',15,MUTED)
    f.text(673,401,'A–B: 2 enlaces / 16 posibles',15,MUTED)
    f.text(673,448,'E[triángulos] = 65/16 = 4,0625',16,TEAL,bold=True)
    f.note('Distinción inferencial','La tabla muestra parámetros ajustados; no identifica por sí sola las aristas ausentes.')
    return f.output(),csvbytes(['grupo_1','grupo_2','observadas','pares_posibles','probabilidad'],[('A','A',5,6,p11),('B','B',4,6,p22),('A','B',2,16,p12)])


def build187():
    sep=10/11-1/30;pair=21/22-1/15
    assert abs(pair-sep-2/165)<1e-12
    f=Figure(187,'Modularidad y límite de resolución','Anillo de 30 cliques de cinco nodos; 330 aristas no dirigidas')
    for x,label,v,col in [(260,'30 cliques individuales',sep,BLUE),(635,'15 pares de cliques',pair,TEAL)]:
        y=495-300*v/.94
        f.rect(x,y,212,495-y,col,radius=5)
        f.text(x+106,y-19,f'{v:.6f}'.replace('.',','),24,INK,bold=True,anchor='middle')
        f.text(x+106,529,label,16,INK,anchor='middle')
    f.line(157,495,898,495,INK,1.5)
    f.note('Advertencia','La fusión en pares mejora Q en 0,012121, aunque cada clique esté densamente unida.')
    return f.output(),csvbytes(['particion','grupos','modularidad'],[('individuales',30,sep),('pares',15,pair)])


def build188():
    rows=[(k,4/(k*(k+1)*(k+2)),2**(-k)) for k in range(1,21)]
    f=Figure(188,'Dos mecanismos, dos colas de grados','Modelos límite con un enlace por llegada: conexión preferencial frente a uniforme')
    p=Plot(f,xmin=1,xmax=20,ymin=.000001,ymax=1.0,ylog=True,xlabel='Grado k (valor entero)',ylabel='Probabilidad límite p(k), escala log')
    p.ticks([(1,'1'),(5,'5'),(10,'10'),(15,'15'),(20,'20')],[(1,'1'),(.01,'10⁻²'),(.0001,'10⁻⁴'),(.000001,'10⁻⁶')])
    p.curve([(k,a) for k,a,b in rows],TEAL,3.2)
    p.curve([(k,b) for k,a,b in rows],ORANGE,3.2)
    p.dots([(k,a) for k,a,b in rows],TEAL,3.3)
    f.line(616,166,670,166,TEAL,3);f.text(679,172,'Preferencial: 4/[k(k+1)(k+2)]',14,MUTED)
    f.line(616,192,670,192,ORANGE,3);f.text(679,198,'Uniforme: 2⁻ᵏ',14,MUTED)
    f.note('Comparación de cola','P(K ≥ 10): preferencial 1,82 % frente a uniforme 0,195 % (límites teóricos).')
    return f.output(),csvbytes(['grado_k','prob_preferencial','prob_uniforme'],rows)


def build189():
    xs=[1,2,4,8,16];a=1+1/(2*math.log(2));obs=[(x,(len(xs)-i)/len(xs)) for i,x in enumerate(xs)]
    theory=[(2**(4*j/160),2**(4*j/160*(1-a))) for j in range(161)]
    f=Figure(189,'Una recta logarítmica no demuestra una ley','Cinco valores didácticos: 1, 2, 4, 8 y 16; umbral xₘᵢₙ = 1')
    p=Plot(f,xmin=1,xmax=16,ymin=.1,ymax=1,xlog=True,ylog=True,xlabel='Observación x (escala log)',ylabel='Supervivencia P(X ≥ x), escala log')
    p.ticks([(1,'1'),(2,'2'),(4,'4'),(8,'8'),(16,'16')],[(1,'1'),(.5,'0,5'),(.2,'0,2'),(.1,'0,1')])
    p.curve(theory,TEAL,3)
    p.dots(obs,ORANGE,7)
    f.line(612,174,664,174,TEAL,3);f.text(677,179,'Pareto continuo ajustado',14,MUTED)
    f.circle(638,201,6,ORANGE);f.text(677,206,'Supervivencia empírica, n=5',14,MUTED)
    f.note('Estimación descriptiva',f'α = 1 + 1/(2 ln 2) = {a:.5f}; no se ha comprobado el ajuste estadístico.')
    return f.output(),csvbytes(['x','supervivencia_empirica','supervivencia_pareto'],[(x,p,a0) for (x,p),(_,a0) in zip(obs,[ (x,x**(1-a)) for x in xs])])


def build190():
    ed=list(itertools.combinations(range(1,5),2))+[(1,5),(2,5),(5,6),(1,7),(7,8)]
    def tri8(sub):
        es=set(map(lambda e:tuple(sorted(e)),sub))
        return sum(all((a,b) in es for a,b in itertools.combinations(t,2)) for t in itertools.combinations(range(1,9),3))
    rows=[]
    for e in ed:
        t=tri8([v for v in ed if v!=e]);rows.append((f'{e[0]}–{e[1]}',t,1 if e in [(5,6),(1,7),(7,8)] else 0,1 if e not in list(itertools.combinations(range(1,5),2)) else 0))
    counts={t:sum(x[1]==t for x in rows) for t in range(2,6)}
    assert counts=={2:1,3:5,4:2,5:3},counts
    assert sum(t for _,t,*z in rows)==40
    f=Figure(190,'Sensibilidad a perder una sola arista','Auditoría exhaustiva: se elimina una de las 11 conexiones por turno')
    for t in range(2,6):
        x=160+(t-2)*222;h=counts[t]/5*260
        f.rect(x,493-h,139,h,[RED,ORANGE,TEAL,BLUE][t-2],radius=5)
        f.text(x+70,480-h,str(counts[t])+' casos',18,INK,bold=True,anchor='middle')
        f.text(x+70,528,str(t)+' triángulos',15,INK,anchor='middle')
    f.note('Cota exacta del diseño','Media observada: 40/11 triángulos; 3-core persiste en 5/11 omisiones.')
    return f.output(),csvbytes(['enlace_eliminado','triangulos_restantes','desconecta','tres_core_presente'],rows)

BUILDERS={181:build181,182:build182,183:build183,184:build184,185:build185,186:build186,187:build187,188:build188,189:build189,190:build190}

def main():
    a=argparse.ArgumentParser();g=a.add_mutually_exclusive_group(required=True);g.add_argument('--out',type=Path);g.add_argument('--check',type=Path);args=a.parse_args()
    root=args.out if args.out else args.check
    for n,builder in BUILDERS.items():
        svg,data=builder();sf=root/f'figures/complexa-c{n}-fig01.svg';cf=root/f'repro/c181-190/c{n}.csv'
        for p,c in [(sf,svg),(cf,data)]:
            if args.check:
                assert p.read_bytes()==c,(n,str(p),'byte mismatch')
            else:
                p.parent.mkdir(parents=True,exist_ok=True);p.write_bytes(c)
        print(f'C{n}: {len(svg):6d} bytes SVG; {len(data):5d} bytes CSV; sha256 {hashlib.sha256(svg).hexdigest()[:12]}')
    print('PASS: 10 figuras SVG + 10 tablas CSV; inferencia derivada y pruebas numéricas deterministas.')

if __name__=='__main__':main()
