#!/usr/bin/env python3
"""Produce 10 fully deterministic, illustrative scientific SVGs and CSV tables for COMPLEXA C161–C170.

All curves follow explicitly stated equations in the respective manuscripts; no clinical,
observational or empirical dataset is asserted. Python 3.10+ standard library only.

python make_figures.py --out DIR
python make_figures.py --check DIR  # reproducibility validation against checked-in output
"""
from __future__ import annotations

import argparse
import csv
import io
import math
import pathlib
import tempfile
from html import escape

INK, SUB, GRID, TEAL, ORANGE, BLUE, MUTED = '#173039', '#465f69', '#dfe9e7', '#187f76', '#cf7655', '#546fbb', '#64777b'
WIDTH, HEIGHT = 1060, 610
PI = math.pi


def f(v):
    return f'{v:.2f}'


def fmt_value(v):
    return f'{v:.8g}'


def xml(s):
    return escape(str(s), quote=True)


class Svg:
    def __init__(self, number, title, description, footer):
        self.n = number
        self.raw_title = title
        self.parts = [f'<svg xmlns="http://www.w3.org/2000/svg" width="{WIDTH}" height="{HEIGHT}" viewBox="0 0 {WIDTH} {HEIGHT}" role="img" aria-labelledby="title desc">',
            f'<title id="title">C{number} · {xml(title)}</title>',
            f'<desc id="desc">{xml(description)}</desc>',
            f'<rect width="{WIDTH}" height="{HEIGHT}" fill="#f8faf9"/>',
            '<rect width="1060" height="8" fill="#187f76"/>',
            '<text x="56" y="48" font-family="sans-serif" font-weight="700" font-size="15" fill="#187f76" letter-spacing="1.5">COMPLEXA  /  FIGURA C'+str(number)+'.1</text>',
            f'<text x="56" y="95" font-family="sans-serif" font-size="30" font-weight="650" fill="{INK}">{xml(title)}</text>',
            f'<text x="56" y="125" font-family="sans-serif" font-size="16" fill="{SUB}">{xml(description)}</text>',
            '<line x1="56" y1="556" x2="1004" y2="556" stroke="#d5e1df"/>',
            f'<text x="56" y="583" font-family="sans-serif" font-size="13" fill="{SUB}">{xml(footer)}</text>',
            f'<text x="1004" y="583" text-anchor="end" font-family="sans-serif" font-size="13" fill="{MUTED}">C{number} · gráfico vectorial verificable</text>',
        ]
    def add(self, s): self.parts.append(s)
    def text(self,x,y,s,size=14,fill=INK,anchor='start',weight=400):
        self.add(f'<text x="{f(x)}" y="{f(y)}" text-anchor="{anchor}" font-family="sans-serif" font-size="{size}" font-weight="{weight}" fill="{fill}">{xml(s)}</text>')
    def line(self,x1,y1,x2,y2,color=GRID,w=1,dash=None):
        s=f'<line x1="{f(x1)}" y1="{f(y1)}" x2="{f(x2)}" y2="{f(y2)}" stroke="{color}" stroke-width="{w}"'
        if dash:s+=f' stroke-dasharray="{dash}"'
        self.add(s+'/>')
    def rect(self,x,y,w,h,fill='#ffffff',rx=0,stroke='none'):
        self.add(f'<rect x="{f(x)}" y="{f(y)}" width="{f(w)}" height="{f(h)}" rx="{rx}" fill="{fill}" stroke="{stroke}"/>')
    def circle(self,x,y,r,fill=TEAL,stroke='none',sw=1):
        self.add(f'<circle cx="{f(x)}" cy="{f(y)}" r="{f(r)}" fill="{fill}" stroke="{stroke}" stroke-width="{sw}"/>')
    def output(self): return ('\n'.join(self.parts+['</svg>',''])).encode('utf-8')


class Panel:
    def __init__(self, g:Svg, x,y,w,h, xmin,xmax,ymin,ymax, xlabel='',ylabel='',xticks=None,yticks=None, logy=False):
        self.g,self.x,self.y,self.w,self.h,self.xmin,self.xmax = g,x,y,w,h,xmin,xmax
        self.ymin,self.ymax,self.logy=ymin,ymax,logy
        self.plotx,self.ploty=x+67,y+23
        self.plotw,self.ploth=w-92,h-70
        g.rect(x,y,w,h,'#ffffff',10,'#e4eeeb')
        if logy:
            if ymin<=0: raise ValueError('log y requires positive range')
            self.ymin=math.log10(ymin);self.ymax=math.log10(ymax)
        xticks=xticks if xticks is not None else [xmin,(xmin+xmax)/2,xmax]
        yticks=yticks if yticks is not None else [ymin,(ymin+ymax)/2,ymax]
        for v in yticks:
            if v<=0 and logy:continue
            yy=self.mapy(v)
            g.line(self.plotx,yy,self.plotx+self.plotw,yy,'#e4ebe9')
            g.text(self.plotx-10,yy+4,f'{v:g}',12,MUTED,anchor='end')
        for v in xticks:
            xx=self.mapx(v)
            g.line(xx,self.ploty,xx,self.ploty+self.ploth,'#f1f4f3')
            g.text(xx,self.ploty+self.ploth+20,f'{v:g}',12,MUTED,anchor='middle')
        g.line(self.plotx,self.ploty,self.plotx,self.ploty+self.ploth,'#92a9aa',1.6)
        g.line(self.plotx,self.ploty+self.ploth,self.plotx+self.plotw,self.ploty+self.ploth,'#92a9aa',1.6)
        if xlabel:g.text(self.plotx+self.plotw/2,y+h-10,xlabel,13,SUB,anchor='middle')
        if ylabel:g.text(x+7,y+20,ylabel,13,SUB)
    def mapx(self,v): return self.plotx+(v-self.xmin)/(self.xmax-self.xmin)*self.plotw
    def mapy(self,v):
        vv=math.log10(v) if self.logy else v
        return self.ploty+self.ploth-(vv-self.ymin)/(self.ymax-self.ymin)*self.ploth
    def plot(self,pairs,color=TEAL,width=3,dash=None,opacity=1):
        pts=' '.join(f'{f(self.mapx(x))},{f(self.mapy(y))}' for x,y in pairs)
        tag=f'<polyline points="{pts}" fill="none" stroke="{color}" stroke-width="{width}" stroke-linecap="round" stroke-linejoin="round" opacity="{opacity}"'
        if dash: tag+=f' stroke-dasharray="{dash}"'
        self.g.add(tag+'/>')
    def points(self,pairs,color=ORANGE,r=4.5,open=False):
        for x,y in pairs:self.g.circle(self.mapx(x),self.mapy(y),r,'#ffffff' if open else color,color if open else 'none',2)
    def horizontal(self,y,color=ORANGE,dash='6 4'):
        self.g.line(self.mapx(self.xmin),self.mapy(y),self.mapx(self.xmax),self.mapy(y),color,1.7,dash)
    def vertical(self,x,color=ORANGE,dash='6 4'):
        self.g.line(self.mapx(x),self.mapy(self.ymin if not self.logy else 10**self.ymin),self.mapx(x),self.mapy(self.ymax if not self.logy else 10**self.ymax),color,1.5,dash)


def linspace(lo,hi,n): return [lo+(hi-lo)*k/(n-1) for k in range(n)]

def legend(g:Svg,items,x=135,y=540,step=244):
    for i,(label,color,dashed) in enumerate(items):
        xx=x+i*step
        g.line(xx,y,xx+29,y,color,3,'7 5' if dashed else None)
        g.text(xx+38,y+5,label,14,SUB)


def render161():
    # Figure directly demonstrates exact aliasing for cosines with phase zero at fs=1 Hz.
    tt=linspace(0,10,1001)
    high=[(t,math.cos(2*PI*.8*t)) for t in tt]
    low=[(t,math.cos(2*PI*.2*t)) for t in tt]
    sample=[(n,math.cos(2*PI*.8*n)) for n in range(11)]
    g=Svg(161,'La misma muestra, dos frecuencias','Muestreo a 1 Hz: 0,8 Hz y 0,2 Hz dan lecturas idénticas.','Señales ideales sin ruido · ecuación y condición de fase de C161, sección 161.5')
    p=Panel(g,55,150,950,357,0,10,-1.2,1.2,'Tiempo (s)','Amplitud relativa',xticks=[0,2,4,6,8,10],yticks=[-1,0,1])
    p.plot(high,BLUE,2.7);p.plot(low,TEAL,2.7,'7 5');p.points(sample,ORANGE,5.1)
    legend(g,[('Señal real: 0,8 Hz',BLUE,False),('Alternativa: 0,2 Hz',TEAL,True),('Lecturas cada 1 s',ORANGE,False)],x=120,y=532,step=295)
    return g,{'t':tt,'senal_0_8':[y for _,y in high],'senal_0_2':[y for _,y in low]}


def render162():
    nn=list(range(13));q0=[1/3-(1/3)*(.4**n) for n in nn];q1=[1/3+(2/3)*(.4**n) for n in nn]
    g=Svg(162,'La agregación conserva la dinámica','Probabilidad del bloque B bajo dos preparaciones microscópicas compatibles.','Cadena agregable exacta de C162 · P(B→B)=0,6 y P(A→B)=0,2')
    p=Panel(g,55,150,950,357,0,12,0,1.05,'Paso de la cadena','Probabilidad de B',xticks=[0,2,4,6,8,10,12],yticks=[0,.25,.5,.75,1])
    p.plot(list(zip(nn,q0)),TEAL,3);p.plot(list(zip(nn,q1)),ORANGE,3);p.horizontal(1/3,BLUE,'7 6');p.points(list(zip(nn,q0)),TEAL,3);p.points(list(zip(nn,q1)),ORANGE,3)
    legend(g,[('Inicio en A',TEAL,False),('Inicio en B',ORANGE,False),('Equilibrio: 1/3',BLUE,True)],x=115,y=532,step=295)
    return g,{'paso':nn,'desde_A':q0,'desde_B':q1,'equilibrio':[1/3]*len(nn)}


def rg(k):return .5*math.log(math.cosh(2*k))

def render163():
    ks=list(range(10));a=[.5];b=[1.]
    for _ in range(9):a.append(rg(a[-1]));b.append(rg(b[-1]))
    g=Svg(163,'Decimación exacta: el acoplamiento fluye','Cadena de Ising 1D, sin campo: K siguiente = ½ ln cosh(2K).','Identidad exacta del manuscrito C163 · K inicial 0,5 y 1,0 · sin datos empíricos')
    p=Panel(g,55,150,950,357,0,9,0,1.05,'Número de decimaciones','Acoplamiento K',xticks=[0,1,3,5,7,9],yticks=[0,.25,.5,.75,1])
    p.plot(list(zip(ks,a)),TEAL,3);p.plot(list(zip(ks,b)),ORANGE,3);p.points(list(zip(ks,a)),TEAL,3.3);p.points(list(zip(ks,b)),ORANGE,3.3)
    legend(g,[('K inicial = 0,5',TEAL,False),('K inicial = 1',ORANGE,False)],x=160,y=532,step=350)
    return g,{'paso':ks,'K_inicial_0_5':a,'K_inicial_1':b}


def render164():
    nn=list(range(6));u=[.01*2**n for n in nn];v=[.8*2**(-n) for n in nn]
    g=Svg(164,'Dos direcciones cerca del punto fijo','El acoplamiento relevante crece; el irrelevante se contrae al reescalar.','Mapa didáctico C164: u siguiente = 2u; v siguiente = v/2')
    p=Panel(g,55,150,950,357,0,5,.005,1,'Iteraciones de escala n','Magnitud (escala log)',xticks=nn,yticks=[.01,.03,.1,.3,1],logy=True)
    p.plot(list(zip(nn,u)),ORANGE,3);p.plot(list(zip(nn,v)),TEAL,3);p.points(list(zip(nn,u)),ORANGE,4);p.points(list(zip(nn,v)),TEAL,4)
    legend(g,[('u relevante (0,01 → 0,32)',ORANGE,False),('v irrelevante (0,8 → 0,025)',TEAL,False)],x=110,y=532,step=425)
    return g,{'paso':nn,'u_relevante':u,'v_irrelevante':v}


def m_ising(t):
    if t>=1:return 0.
    Kc=math.log(1+math.sqrt(2))/2
    a=2*Kc/t
    if a>700:return 1.
    value=1-1/math.sinh(a)**4
    return max(0,value)**(1/8)


def render165():
    tt=linspace(.07,1.15,350);m=[m_ising(t) for t in tt]
    g=Svg(165,'Orden espontáneo y temperatura crítica','Magnetización exacta del Ising cuadrado infinito, en la rama positiva.','Ley exacta de Yang (1952), C165 · campo externo nulo · orden termodinámico')
    p=Panel(g,55,150,950,357,0,1.2,-.05,1.1,'Temperatura T / Tc','Magnetización',xticks=[0,.2,.4,.6,.8,1,1.2],yticks=[0,.25,.5,.75,1])
    p.plot(list(zip(tt,m)),TEAL,3.2);p.vertical(1,ORANGE,'6 5');g.text(p.mapx(1)+10,p.mapy(.97),'Tc',14,ORANGE)
    legend(g,[('m espontánea (rama +)',TEAL,False),('Umbral T / Tc = 1',ORANGE,True)],x=185,y=532,step=420)
    return g,{'T_sobre_Tc':tt,'m_spontanea':m}


def render166():
    # Intentionally synthetic family from C166: chi_L(t)=L^7/4/[1+(tL-1)^2].
    ll=[16,32,64];colors=[TEAL,ORANGE,BLUE];xs=linspace(-2,5,141)
    g=Svg(166,'El colapso de curvas está impuesto','Tres tamaños sintéticos coinciden al normalizar la susceptibilidad.','Familia didáctica de C166 · χ = L^(7/4) / [1 + (tL−1)²] · no es simulación')
    p=Panel(g,55,150,950,357,-2,5,0,1.05,'Variable reescalada x = t·L','χ · L^(-7/4)',xticks=[-2,-1,0,1,2,3,4,5],yticks=[0,.25,.5,.75,1])
    for l,c,step in zip(ll,colors,[7,11,15]):
        values=[1/(1+(x-1)**2) for x in xs]
        p.plot(list(zip(xs,values)),c,1.9)
        p.points([(xs[i],values[i]) for i in range(0,len(xs),step)],c,2.9,True)
    legend(g,[('L = 16',TEAL,False),('L = 32',ORANGE,False),('L = 64',BLUE,False)],x=210,y=532,step=270)
    return g,{'x_tL':xs,'L16_normalizada':[1/(1+(x-1)**2) for x in xs],'L32_normalizada':[1/(1+(x-1)**2) for x in xs],'L64_normalizada':[1/(1+(x-1)**2) for x in xs]}


def render167():
    # Exact spectrum of A=[[-2,1],[10,-20]]; new illustrated IC x0=1, y0=0.
    slow=(-22+math.sqrt(364))/2;fast=(-22-math.sqrt(364))/2
    cs=(-2-fast)/(slow-fast);cf=1-cs
    tt=linspace(0,4,220);actual=[cs*math.exp(slow*t)+cf*math.exp(fast*t) for t in tt];simple=[math.exp(-1.5*t) for t in tt];corrected=[math.exp((-1.5/1.025)*t) for t in tt]
    g=Svg(167,'Eliminar el estado rápido no borra su memoria','Comparación desde una preparación ilustrativa x(0)=1, y(0)=0.','Parámetros del sistema C167 · ε = 0,1; condiciones iniciales declaradas aquí')
    p=Panel(g,55,150,554,357,0,4,0,1.05,'Tiempo','Estado x(t)',xticks=[0,1,2,3,4],yticks=[0,.25,.5,.75,1])
    p.plot(list(zip(tt,actual)),BLUE,3.4);p.plot(list(zip(tt,simple)),ORANGE,2.5,'7 5');p.plot(list(zip(tt,corrected)),TEAL,2.5,'3 4')
    q=Panel(g,622,150,384,357,0,2,-.01,.03,'Tiempo','Error vs exacta',xticks=[0,.5,1,1.5,2],yticks=[-.01,0,.01,.02,.03])
    q.horizontal(0,BLUE,'2 5')
    q.plot([(t,b-a) for t,a,b in zip(tt,actual,simple) if t<=2],ORANGE,2.4,'7 5')
    q.plot([(t,b-a) for t,a,b in zip(tt,actual,corrected) if t<=2],TEAL,2.4,'3 4')
    legend(g,[('Sistema completo',BLUE,False),('Cierre algebraico',ORANGE,True),('Cierre corregido',TEAL,True)],x=92,y=532,step=295)
    return g,{'tiempo':tt,'x_completo':actual,'x_cierre_algebraico':simple,'x_cierre_corregido':corrected}


def render168():
    xx=linspace(0,1,201)
    hetero=[1-1.8*x if x<=.5 else .2*(1-x) for x in xx]
    homog=[1-x for x in xx]
    g=Svg(168,'Capas en serie: el gradiente cambia','D₁ = 1, D₂ = 9, espesores iguales; el flujo permanece constante.','Caso analítico C168 · D efectivo en serie = 1,8; referencia homogénea')
    p=Panel(g,55,150,950,357,0,1,0,1.05,'Posición / longitud total','Concentración',xticks=[0,.25,.5,.75,1],yticks=[0,.25,.5,.75,1])
    # Layers: unobtrusive drawing below curves (transparent via opacity)
    g.rect(p.mapx(0),p.ploty,p.plotw/2,p.ploth,'#edf7f5')
    g.rect(p.mapx(.5),p.ploty,p.plotw/2,p.ploth,'#fff2eb')
    # Re-grid as layer backgrounds are above initial grids
    for value in [0,.25,.5,.75,1]:g.line(p.mapx(0),p.mapy(value),p.mapx(1),p.mapy(value),'#e1e8e6',.8)
    p.plot(list(zip(xx,hetero)),TEAL,3.5);p.plot(list(zip(xx,homog)),ORANGE,2.3,'7 6');p.vertical(.5,BLUE,'4 5')
    g.text(p.mapx(.18),p.ploty+27,'Capa D₁ = 1',14,SUB,anchor='middle')
    g.text(p.mapx(.78),p.ploty+27,'Capa D₂ = 9',14,SUB,anchor='middle')
    legend(g,[('Perfil en capas (flujo 1,8)',TEAL,False),('Perfil homogéneo',ORANGE,True)],x=180,y=532,step=450)
    return g,{'x':xx,'concentracion_serie':hetero,'referencia_homogenea':homog}


def render169():
    nn=list(range(1,301));ind=[4/n for n in nn];corr=[4*(.1+.9/n) for n in nn]
    g=Svg(169,'Una fluctuación común resiste al promedio','La correlación ρ = 0,1 mantiene una varianza colectiva positiva.','Identidad exacta C169 · varianza individual σ² = 4; N de 1 a 300')
    p=Panel(g,55,150,950,357,1,300,.008,5,'Número de unidades N','Varianza de la media (log)',xticks=[1,50,100,150,200,250,300],yticks=[.01,.04,.1,.4,1,4],logy=True)
    p.plot(list(zip(nn,ind)),TEAL,3);p.plot(list(zip(nn,corr)),ORANGE,3);p.horizontal(.4,BLUE,'6 4')
    g.circle(p.mapx(100),p.mapy(4/100),5,TEAL);g.circle(p.mapx(100),p.mapy(4*(.1+.9/100)),5,ORANGE)
    legend(g,[('ρ = 0: varianza 4/N',TEAL,False),('ρ = 0,1: suelo 0,4',ORANGE,False)],x=155,y=532,step=435)
    return g,{'N':nn,'varianza_independencia':ind,'varianza_rho_0_1':corr}


def render170():
    tt=linspace(0,60,241)
    initial=[20+40*math.exp(-.1*t) for t in tt]
    new=[100/3-40/3*math.exp(-.12*t) for t in tt]
    g=Svg(170,'Dos experimentos con la misma población','N = 100: relajación desde 60 activos e intervención desde equilibrio.','C170 · k+ original 0,02; intervenido 0,04; k− = 0,08 (s⁻¹)')
    p=Panel(g,55,150,950,357,0,60,0,65,'Tiempo (s)','Activos esperados E[n]',xticks=[0,10,20,30,40,50,60],yticks=[0,20,40,60])
    p.plot(list(zip(tt,initial)),TEAL,3);p.plot(list(zip(tt,new)),ORANGE,3);p.horizontal(20,BLUE,'6 5');p.horizontal(100/3,ORANGE,'4 6')
    legend(g,[('Desde n(0) = 60 (sin intervenir)',TEAL,False),('Intervención desde E[n(0)] = 20',ORANGE,False)],x=104,y=532,step=445)
    return g,{'tiempo_s':tt,'relajacion_desde_60':initial,'intervencion_desde_20':new}


RENDERS={161:render161,162:render162,163:render163,164:render164,165:render165,166:render166,167:render167,168:render168,169:render169,170:render170}


def build(out):
    figures=out/'figures';data=out/'repro'/'c161-170'
    figures.mkdir(parents=True,exist_ok=True);data.mkdir(parents=True,exist_ok=True)
    for n,gen in RENDERS.items():
        drawing,table=gen()
        fields=list(table)
        widths={len(v) for v in table.values()}
        assert len(widths)==1 and next(iter(widths))>0
        figure=figures/f'complexa-c{n}-fig01.svg'
        figure.write_bytes(drawing.output())
        buf=io.StringIO(newline='');writer=csv.writer(buf,lineterminator='\n')
        writer.writerow(fields)
        for row in zip(*table.values()):writer.writerow([fmt_value(v) for v in row])
        (data/f'c{n}.csv').write_text(buf.getvalue(),encoding='utf-8')
    # Control calculations repeated independently of graphics.
    assert abs(math.cos(2*PI*.8*5)-math.cos(2*PI*.2*5))<1e-12
    assert abs(render162()[1]['desde_A'][1]-.2)<1e-12
    assert abs(rg(.5)-.2168904)<1e-6
    assert abs(render164()[1]['u_relevante'][-1]-.32)<1e-12
    assert m_ising(1)==0
    assert abs(render166()[1]['L16_normalizada'][60]-render166()[1]['L64_normalizada'][60])<1e-12
    assert abs((-22+math.sqrt(364))/2-(-1.46060799))<1e-7
    assert abs(render168()[1]['concentracion_serie'][100]-.1)<1e-12
    assert abs(4*(.1+.9/100)-.436)<1e-12
    assert abs(20+40*math.exp(-1)-34.7151776)<1e-6
    print('PASS: 10 SVG and 10 numerical CSV files; 10 independent analytic checks')


def main():
    p=argparse.ArgumentParser(description=__doc__)
    ex=p.add_mutually_exclusive_group(required=True)
    ex.add_argument('--out',type=pathlib.Path)
    ex.add_argument('--check',type=pathlib.Path)
    args=p.parse_args()
    if args.out:
        build(args.out)
    else:
        with tempfile.TemporaryDirectory() as tmp:
            generated=pathlib.Path(tmp)
            build(generated)
            for sub in ['figures','repro/c161-170']:
                for file in (generated/sub).iterdir():
                    target=args.check/sub/file.name
                    assert target.exists(),f'Missing tracked figure/data: {target}'
                    assert file.read_bytes()==target.read_bytes(),f'Non-reproducible figure/data: {target}'
        print('PASS: byte-for-byte scientific graphics and CSV regeneration')


if __name__=='__main__':main()
