"""Genera las fichas ES_52 de circuitos Q/V. Codigo: HL3.

Diagramas trazados de forma determinista y textos editables en Markdown.
No publica ni modifica los documentos fuente.
"""
from pathlib import Path
import hashlib
import itertools
import json
import math
import sys
import textwrap

from PIL import Image, ImageDraw, ImageFont

ROOT = Path(__file__).resolve().parents[1]
SRC = ROOT.parent
sys.dont_write_bytecode = True
sys.path.insert(0, str(SRC/'UNIHOLOG/Traductores_V'))
from operaciones_senales import Bloque, fourier_inversa

CARDS = [
 dict(title='X / NOT', kind='x', wires=['a'], q='X', v='NOT',
      eq=[r'X|0\rangle=|1\rangle,\quad X|1\rangle=|0\rangle.', r"a'=1-a."],
      qtext=['X intercambia las componentes de base.', '|0> pasa a |1>; |1> pasa a |0>.'],
      vtext=["a' = 1 - a", 'En caminos: (s0, s1) pasa a (s1, s0).'],
      scope='Equivalencia de la inversión binaria; intercambio de caminos para la ley local de X.',
      check='Las dos entradas binarias y la identidad NOT-NOT se verifican exhaustivamente.'),
 dict(title='CNOT / XOR controlada', kind='cnot', wires=['a','b'], q='CNOT', v='XOR',
      eq=[r"(a,b)\mapsto(a,b\oplus a).", r"(s_{00},s_{01},s_{10},s_{11})\mapsto(s_{00},s_{01},s_{11},s_{10})."],
      qtext=['Control a conservado.', 'El objetivo b cambia cuando a = 1.'],
      vtext=["a' = a;  b' = b XOR a", 'La derivación del control conserva su señal.'],
      scope='Tabla binaria coincidente. La ley conjunta de cuatro caminos se conserva como relación local; su composición coherente requiere el acoplamiento correspondiente.',
      check='Las cuatro entradas binarias y la identidad CNOT-CNOT se verifican exhaustivamente.'),
 dict(title='Toffoli / AND + XOR', kind='toffoli', wires=['a','b','c'], q='CCX', v='AND + XOR',
      eq=[r"(a,b,c)\mapsto(a,b,c\oplus(a\land b)).", r"d=a\land b,\qquad c'=c\oplus d."],
      qtext=['Dos controles: a y b.', 'El objetivo c cambia solo si a = b = 1.'],
      vtext=['d = a AND b', "a' = a;  b' = b;  c' = c XOR d"],
      scope='Transformación reversible sobre tres bits. AND es una señal auxiliar interna; no sustituye por sí sola a Toffoli.',
      check='Las ocho entradas y la restitución por doble aplicación se verifican exhaustivamente.'),
 dict(title='OR reversible / OR + XOR', kind='or', wires=['a','b','c'], q='CX, CX, CCX', v='OR + XOR',
      eq=[r"(a,b,c)\mapsto(a,b,c\oplus(a\lor b)).", r"a\lor b=a\oplus b\oplus(a\land b)."],
      qtext=['Orden: CX(a,c), CX(b,c), CCX(a,b,c).', 'Conserva los dos controles.'],
      vtext=['d = a OR b', "a' = a;  b' = b;  c' = c XOR d"],
      scope='Equivalencia de la transformación reversible de tres bits; OR destructiva no tiene las mismas entradas y salidas.',
      check='Las ocho entradas se contrastan entre la descomposición CX-CX-CCX y OR-XOR.'),
 dict(title='Hadamard H / suma y diferencia', kind='h', wires=['q'], q='H', v='H',
      eq=[r"H=\frac{1}{\sqrt2}\begin{pmatrix}1&1\\1&-1\end{pmatrix}.", r"s'_0=(s_0+s_1)/\sqrt2,\qquad s'_1=(s_0-s_1)/\sqrt2."],
      qtext=['Una unidad: mezcla de dos componentes.', 'H seguida de H recupera la entrada.'],
      vtext=["s0' = (s0 + s1) / sqrt(2)", "s1' = (s0 - s1) / sqrt(2)"],
      scope='Identidad algebraica de la transformación local. Los dos caminos forman una unidad y no son dos bits ya medidos.',
      check='Se contrasta con la matriz H para variables simbólicas y se demuestra H-H = identidad.',
      extra='La construcción por acoplamiento del documento base produce $-iH$ con $\\Omega=\\chi=0$, $\\Delta=\\kappa=g/\\sqrt2$ y $g\\tau=\\pi/2$. La fase global debe conservarse al controlar la operación.'),
 dict(title='Fase / desfase de un camino', kind='phase', wires=['q'], q='P(phi)', v='FASE',
      eq=[r"P(\varphi)=\operatorname{diag}(1,e^{i\varphi}).", r"s'_0=s_0,\qquad s'_1=e^{i\varphi}s_1.", r"T=P(\pi/4),\quad S=P(\pi/2),\quad Z=P(\pi)."],
      qtext=['P(phi) cambia la fase relativa.', 'T: pi/4; S: pi/2; Z: pi.'],
      vtext=["s0' = s0", "s1' = exp(i phi) s1"],
      scope='Ley local de fase. El módulo de cada camino se conserva; la fase afecta a una recombinación posterior.',
      check='Relación simbólica y composición con la fase inversa.'),
 dict(title='Rotaciones Rx y Rz', kind='rot', wires=['q'], q='Rx / Rz', v='Rx / Rz',
      eq=[r"R_x(\theta)=\begin{pmatrix}u&-iv\\-iv&u\end{pmatrix},\quad u=\cos(\theta/2),\ v=\sin(\theta/2).", r"R_z(\varphi)=\operatorname{diag}(e^{-i\varphi/2},e^{i\varphi/2}).", r"R_x(\theta)=H R_z(\theta)H."],
      qtext=['Rx mezcla las dos componentes.', 'Rz aplica fases de signo opuesto.'],
      vtext=["Rx: (u s0 - i v s1, -i v s0 + u s1)", 'Rz: (exp(-i phi/2)s0, exp(i phi/2)s1)'],
      scope='Identidades de operadores locales; ángulos declarados antes de la ejecución.',
      check='Se comprueban las dos matrices simbólicas, sus inversas y Rx = H Rz H.'),
 dict(title='CZ / fase controlada', kind='cz', wires=['a','b'], q='CZ', v='CP(pi)',
      eq=[r"\operatorname{CP}(\varphi)|a,b\rangle=e^{i\varphi ab}|a,b\rangle,\quad CZ=CP(\pi).", r"(s_{00},s_{01},s_{10},s_{11})\mapsto(s_{00},s_{01},s_{10},-s_{11})."],
      qtext=['CZ aplica signo menos a |11>.', 'Descomposición: H(b), CX(a,b), H(b).'],
      vtext=['CP(phi): s11 pasa a exp(i phi) s11.', 'Los otros tres caminos no cambian.'],
      scope='Ley conjunta local de fase. Una AND sobre bits leídos no reproduce esta transformación.',
      check='Se verifica CP(pi) = CZ, CZ-CZ = identidad y la inversa de CP.',
      extra='Una interacción ZZ aislada no es CZ. Las correcciones del documento son $CZ=e^{-i\\pi/4}[R_z(-\\pi/2)\\otimes R_z(-\\pi/2)]e^{-i\\pi Z\\otimes Z/4}$. La ejecución conjunta seguida de H mantiene los límites del componente instalado.'),
 dict(title='SWAP / intercambio de puertos', kind='swap', wires=['a','b'], q='SWAP', v='SWAP',
      eq=[r"(a,b)\mapsto(b,a).", r"b\leftarrow b\oplus a,\quad a\leftarrow a\oplus b,\quad b\leftarrow b\oplus a."],
      qtext=['Tres CNOT: a->b, b->a, a->b.', 'Se intercambian las dos unidades completas.'],
      vtext=['Intercambio de puertos a y b.', 'O tres XOR sucesivas con valores actualizados.'],
      scope='Permutación de unidades; conserva las conexiones y no requiere leer para decidir el intercambio.',
      check='Las cuatro entradas se verifican mediante tres XOR; también se verifica la permutación simbólica de los cuatro caminos.'),
 dict(title='Multiplicación modular controlada', kind='mod', wires=['c','y'], q='MOD M,N', v='MOD M,N',
      eq=[r"(c,y)\mapsto(c,y'),\qquad y'=\begin{cases}My\bmod N,&c=1\land y<N,\\y,&\text{en otro caso}.\end{cases}", r"\gcd(M,N)=1,\quad M_j=a^{2^j}\bmod N."],
      qtext=['Bloque de aritmética reversible.', 'M y N se conocen antes de ejecutar.'],
      vtext=["c' = c; y' = M y mod N si c = 1 e y < N.", 'Fuera de ese caso, y se conserva.'],
      scope='Permutación binaria modular. La caja representa un bloque funcional, no una descomposición completa de puertas. La conexión coherente con H requiere su realización conjunta.',
      check='Ejemplo N = 15, M = 2: se verifican los 32 pares (c,y), incluidos y = 15, y la inversión modular.'),
 dict(title='Fourier inversa / secuencia de bloques', kind='fourier', wires=['q0','q1','q2'], q='QFT inversa', v='H + CP + SWAP',
      eq=[r"F^{-1}|x\rangle=\frac1{\sqrt D}\sum_{y=0}^{D-1}e^{-2\pi ixy/D}|y\rangle,\quad D=2^q."],
      qtext=['Ejemplo de tres unidades; q0 es el bit LSB.', 'CP aplica P controlada, con ángulo negativo.'],
      vtext=['Misma secuencia: SWAP, H y CP.', 'Conexiones preparadas antes de la lectura.'],
      scope='Coincidencia de la secuencia compilada. No certifica la ejecución coherente del circuito completo ni exige almacenar una tabla de D amplitudes.',
      check='Se contrasta el compilador existente con los siete bloques y sus ángulos en el ejemplo de tres unidades.'),
 dict(title='Interferencia / H, fase, H', kind='interf', wires=['q'], q='H - P - H', v='H - FASE - H',
      eq=[r"H P(\varphi) H|0\rangle=s'_0|0\rangle+s'_1|1\rangle.", r"s'_0=(1+e^{i\varphi})/2,\quad s'_1=(1-e^{i\varphi})/2.", r"|s'_0|^2=\cos^2(\varphi/2),\quad |s'_1|^2=\sin^2(\varphi/2)."],
      qtext=['Dividir, desfasar y recombinar.', 'Entrada |0>; una medida después de H final.'],
      vtext=['Dos caminos de la misma unidad.', 'Una captura en t = t0 después de recombinar.'],
      scope='Identidad de interferencia de una unidad. En V la salida usa el comparador determinista declarado; estas identidades no prueban una distribución de lecturas equivalente a Q.',
      check='Identidad simbólica de los caminos de salida. Los 40 ensayos locales de V4 verifican cinco fases, dos entradas y cuatro instantes de disparo.'),
 dict(title='Q4 / V4 — cifrado y descifrado QOTP', kind='qotp', wires=['m'], q='QOTP', v='QOTP',
      eq=[r"E_{a,b}=Z^bX^a,\qquad D_{a,b}=X^aZ^b,\qquad D_{a,b}E_{a,b}=I.", r"(s_0,s_1)\xrightarrow{X}(s_1,s_0),\qquad(s_0,s_1)\xrightarrow{Z}(s_0,-s_1)."],
      qtext=['Clave clásica (a,b) por unidad.', 'Cifrar: X, Z. Descifrar: Z, X.', 'Transmitir sin medir entre ambos bloques.'],
      vtext=['NOT si a = 1; fase pi si b = 1.', 'Inversa: fase -pi y después NOT.', 'Una captura después del descifrado.'],
      scope='Cálculo local contrastado con Qiskit y coste de puertas O(n). Entradas producto verificadas; entrada Bell no implementada. Fiabilidad física no medida.',
      check='9.936 comparaciones y 1.656 recuperaciones correctas; 46 diferencias de umbral en fronteras numéricas. Ver el informe para el alcance exacto.',
      extra='Uso: transformar y recuperar un mensaje conservando sus caminos entre ambos bloques. Q4 es la etiqueta de esta instancia QOTP del proyecto. Fuente del circuito: [Leung, sección 2.2](https://arxiv.org/pdf/quant-ph/0012077). [Informe de la prueba Q4/V4](../UNIHOLOG/Traductores_V/COMPARACION_Q4_V4_QOTP.md). La implementación actual conserva el mensaje en memoria; no implementa un canal físico ni un formato de transmisión de señales.'),
]

BG='#f1f5f9'; NAVY='#10263c'; INK='#193247'; BLUE='#2468bb'; TEAL='#087d75'
fontroot=Path('C:/Windows/Fonts')
def font(size,bold=False):
    return ImageFont.truetype(str(fontroot/('arialbd.ttf' if bold else 'arial.ttf')),size)

def wrap(draw,text,width,size=25,bold=False):
    f=font(size,bold); lines=[]; line=''
    for word in text.split():
        nxt=(line+' '+word).strip()
        if draw.textlength(nxt,font=f)>width and line:
            lines.append(line); line=word
        else: line=nxt
    if line: lines.append(line)
    return lines

def para(d,text,xy,width,size=25,fill=INK,bold=False):
    x,y=xy
    for line in wrap(d,text,width,size,bold):
        d.text((x,y),line,font=font(size,bold),fill=fill); y+=size+10
    return y

def gate(d,x,y,label,color,width=140):
    d.rounded_rectangle((x-width/2,y-30,x+width/2,y+30),radius=9,fill='white',outline=color,width=3)
    d.text((x,y),label,anchor='mm',font=font(23,True),fill=color)

def controlled(d,x,ys,controls,target,color,phase=False,label=None):
    selected=[ys[i] for i in (*controls,target)]
    d.line((x,min(selected),x,max(selected)),fill=color,width=4)
    for i in controls:
        d.ellipse((x-8,ys[i]-8,x+8,ys[i]+8),fill=color)
    y=ys[target]
    if phase:
        d.ellipse((x-8,y-8,x+8,y+8),fill=color)
    else:
        d.ellipse((x-16,y-16,x+16,y+16),outline=color,width=3)
        d.line((x-15,y,x+15,y),fill=color,width=3)
        d.line((x,y-15,x,y+15),fill=color,width=3)
    if label: d.text((x+16,(min(selected)+max(selected))/2),label,font=font(19),fill=color)

def swap(d,x,ys,a,b,color):
    d.line((x,ys[a],x,ys[b]),fill=color,width=3)
    for i in (a,b):
        y=ys[i]; d.line((x-10,y-10,x+10,y+10),fill=color,width=3)
        d.line((x-10,y+10,x+10,y-10),fill=color,width=3)

def diagram(d,card,y,is_v):
    color=TEAL if is_v else BLUE
    n=len(card['wires']); ys=[y+120+(i-(n-1)/2)*65 for i in range(n)]
    left,right=175,1100
    for label,yy in zip(card['wires'],ys):
        d.text((120,yy),label,anchor='rm',font=font(25),fill=INK)
        d.line((left,yy,right,yy),fill=color,width=3)
    k=card['kind']
    if k in ('x','phase','rot'):
        gate(d,610,ys[0],card['v' if is_v else 'q'],color,210)
    elif k=='cnot': controlled(d,610,ys,(0,),1,color)
    elif k=='toffoli':
        if not is_v: controlled(d,610,ys,(0,1),2,color)
        else:
            d.rounded_rectangle((400,ys[0]-29,860,ys[-1]+29),radius=12,fill='white',outline=color,width=3)
            for i,txt in enumerate(("a' = a", "b' = b", "c' = c XOR (a AND b)")):
                d.text((630,ys[i]),txt,anchor='mm',font=font(24),fill=color)
    elif k=='or':
        if not is_v:
            controlled(d,420,ys,(0,),2,color); controlled(d,620,ys,(1,),2,color)
            controlled(d,820,ys,(0,1),2,color)
        else:
            d.rounded_rectangle((400,ys[0]-29,860,ys[-1]+29),radius=12,fill='white',outline=color,width=3)
            for i,txt in enumerate(("a' = a", "b' = b", "c' = c XOR (a OR b)")):
                d.text((630,ys[i]),txt,anchor='mm',font=font(24),fill=color)
    elif k=='swap':
        if is_v: swap(d,610,ys,0,1,color)
        else:
            controlled(d,420,ys,(0,),1,color); controlled(d,620,ys,(1,),0,color)
            controlled(d,820,ys,(0,),1,color)
    elif k=='cz':
        if is_v: controlled(d,610,ys,(0,),1,color,phase=True,label='pi')
        else:
            gate(d,410,ys[1],'H',color,85); controlled(d,610,ys,(0,),1,color)
            gate(d,810,ys[1],'H',color,85)
    elif k=='mod':
        d.line((610,ys[0],610,ys[1]),fill=color,width=3)
        d.ellipse((602,ys[0]-8,618,ys[0]+8),fill=color)
        gate(d,610,ys[1],'M x y mod N',color,260)
        d.text((1100,ys[1]+27),'registro y: varios bits',anchor='ra',font=font(19),fill=INK)
    elif k=='fourier':
        swap(d,260,ys,0,2,color); gate(d,365,ys[0],'H',color,65)
        controlled(d,465,ys,(1,),0,color,True,'-pi/2')
        gate(d,570,ys[1],'H',color,65)
        controlled(d,680,ys,(2,),0,color,True,'-pi/4')
        controlled(d,850,ys,(2,),1,color,True,'-pi/2')
        gate(d,1020,ys[2],'H',color,65)
    elif k=='interf':
        for x,label in ((360,'H'),(610,'FASE' if is_v else 'P(phi)'),(860,'H')):
            gate(d,x,ys[0],label,color,150)
    elif k=='qotp':
        labels = ('NOT^a', 'P(b pi)', 'P(-b pi)', 'NOT^a') if is_v else ('X^a', 'Z^b', 'Z^b', 'X^a')
        for x, label in zip((305, 505, 785, 985), labels):
            gate(d,x,ys[0],label,color,165)
        d.line((645,ys[0]-55,645,ys[0]+55),fill='#9baebd',width=2)
        d.text((410,ys[0]-62),'CIFRADO',anchor='mm',font=font(21,True),fill=color)
        d.text((885,ys[0]-62),'DESCIFRADO',anchor='mm',font=font(21,True),fill=color)
    elif k=='h':
        if not is_v: gate(d,610,ys[0],'H',color,140)
        else:
            yy=ys[0]
            d.rectangle((175,yy-20,1100,yy+20),fill='white')
            for offset in (-38,38):
                d.line((175,yy+offset,1100,yy+offset),fill=color,width=3)
            d.rounded_rectangle((395,yy-72,865,yy+72),radius=12,fill='white',outline=color,width=3)
            for offset,txt in ((-38,"s0' = (s0 + s1) / sqrt(2)"),(38,"s1' = (s0 - s1) / sqrt(2)")):
                d.text((630,yy+offset),txt,anchor='mm',font=font(23,True),fill=color)
            d.text((175,yy-64),'s0',font=font(21),fill=INK)
            d.text((175,yy+48),'s1',font=font(21),fill=INK)
    output_ys=[ys[0]-38,ys[0]+38] if k=='h' and is_v else ys
    for yy in output_ys:
        d.line((right-14,yy-7,right,yy),fill=color,width=3)
        d.line((right-14,yy+7,right,yy),fill=color,width=3)

def render(card,number,path):
    im=Image.new('RGB',(1800,1320),BG); d=ImageDraw.Draw(im)
    d.rectangle((0,0,1800,175),fill=NAVY)
    d.text((65,30),f'ES_52  /  FICHA DE COMPONENTE {number:02d}',font=font(24,True),fill='#8bd9da')
    d.text((65,78),card['title'],font=font(46,True),fill='white')
    d.text((65,195),'Circuitos y leyes de transformación Q–V',font=font(27),fill=INK)
    for is_v,y in ((False,255),(True,590)):
        color=TEAL if is_v else BLUE
        d.rounded_rectangle((55,y,1745,y+310),radius=18,fill='white',outline='#d8e2ec',width=2)
        d.rounded_rectangle((78,y+18,155,y+64),radius=8,fill=color)
        d.text((116,y+41),'V' if is_v else 'Q',anchor='mm',font=font(28,True),fill='white')
        d.text((178,y+26),'Construcción V' if is_v else 'Circuito Q',font=font(26,True),fill=color)
        diagram(d,card,y+30,is_v)
        d.line((1160,y+25,1160,y+285),fill='#dbe5ee',width=2)
        ty=y+55
        for text in card['vtext' if is_v else 'qtext']:
            ty=para(d,text,(1190,ty),515,size=25)+18
        d.text((90,y+273),'Conexiones preparadas; lectura al finalizar en t = t0.',font=font(22),fill='#576e80')
    d.text((70,937),'ALCANCE DE LA EQUIVALENCIA',font=font(24,True),fill=TEAL)
    end=para(d,card['scope'],(70,980),1650,size=27)
    para(d,card['check'],(70,end+14),1650,size=24,fill='#476075')
    d.line((65,1215,1735,1215),fill='#c7d4e0',width=2)
    d.text((65,1240),'Víctor Estrada Díaz · UNIHOLOG · 23/10/2026',font=font(23),fill=INK)
    d.text((65,1276),'Texto e imagen: CC BY-NC-SA 4.0 · Código: HL3 · estradad.es/licencia.php',font=font(21),fill='#576e80')
    im.save(path)

def validate():
    import sympy as sp
    checked=[]
    def equal(name,actual,expected):
        assert len(actual)==len(expected),name
        assert all(sp.simplify(sp.expand((a-b).rewrite(sp.cos)))==0 for a,b in zip(actual,expected)),name
        checked.append(name)
    for a in (0,1): assert 1-(1-a)==a
    checked.append('NOT: 2 entradas y doble inversion')
    for a,b in itertools.product((0,1),repeat=2):
        assert ((b^a)^a)==b
        aa,bb=a,b; bb^=aa; aa^=bb; bb^=aa
        assert (aa,bb)==(b,a)
    checked.extend(['CNOT: 4 entradas e inversa','SWAP: 4 entradas con tres XOR'])
    for a,b,c in itertools.product((0,1),repeat=3):
        assert (c^(a&b))^(a&b)==c
        assert (c^a^b^(a&b))==(c^(a|b))
    checked.extend(['Toffoli: 8 entradas e inversa','OR reversible: 8 entradas'])
    x,y=sp.symbols('s0 s1'); phi=sp.Symbol('phi',real=True)
    h=Bloque('h',(0,)); hp=h.transformar_par(x,y,simbolico=True)
    equal('H: matriz local',hp,((x+y)/sp.sqrt(2),(x-y)/sp.sqrt(2)))
    equal('H-H: identidad',h.transformar_par(*hp,simbolico=True),(x,y))
    phase=Bloque('fase',(0,),phi).transformar_par(x,y,simbolico=True)
    equal('Fase: ley local',phase,(x,sp.exp(sp.I*phi)*y))
    for name in ('fase','rx','rz'):
        first=Bloque(name,(0,),phi).transformar_par(x,y,simbolico=True)
        equal(name+': inversa',Bloque(name,(0,),-phi).transformar_par(*first,simbolico=True),(x,y))
    u,v=sp.cos(phi/2),sp.sin(phi/2)
    equal('Rx: matriz',Bloque('rx',(0,),phi).transformar_par(x,y,simbolico=True),(u*x-sp.I*v*y,-sp.I*v*x+u*y))
    equal('Rz: matriz',Bloque('rz',(0,),phi).transformar_par(x,y,simbolico=True),(sp.exp(-sp.I*phi/2)*x,sp.exp(sp.I*phi/2)*y))
    hz=Bloque('rz',(0,),phi).transformar_par(*hp,simbolico=True)
    equal('Rx = H Rz H',h.transformar_par(*hz,simbolico=True),Bloque('rx',(0,),phi).transformar_par(x,y,simbolico=True))
    joint=sp.symbols('s00 s01 s10 s11')
    for name,expected in (('cnot',(joint[0],joint[1],joint[3],joint[2])),('swap',(joint[0],joint[2],joint[1],joint[3])),('cz',(*joint[:3],-joint[3]))):
        block=Bloque(name,(0,1)); actual=block.transformar_conexion_doble(*joint,simbolico=True)
        equal(name+': caminos conjuntos',actual,expected)
        equal(name+': identidad doble',block.transformar_conexion_doble(*actual,simbolico=True),joint)
    equal('CP(pi) = CZ',Bloque('cp',(0,1),sp.pi).transformar_conexion_doble(*joint,simbolico=True),(*joint[:3],-joint[3]))
    cp=Bloque('cp',(0,1),phi).transformar_conexion_doble(*joint,simbolico=True)
    equal('CP: inversa',Bloque('cp',(0,1),-phi).transformar_conexion_doble(*cp,simbolico=True),joint)
    for c in (0,1):
        seen=[]
        for word in range(16):
            out=2*word%15 if c and word<15 else word
            back=8*out%15 if c and out<15 else out
            assert back==word
            seen.append(out)
        assert len(set(seen))==16
    checked.append('Modular: 32 pares, dominio extendido e inversa')
    blocks=fourier_inversa((0,1,2))
    expected=[('swap',(0,2),None),('h',(0,),None),('cp',(1,0),-math.pi/2),('h',(1,),None),('cp',(2,0),-math.pi/4),('cp',(2,1),-math.pi/2),('h',(2,),None)]
    assert [(b.nombre,b.puertos,b.angulo) for b in blocks]==expected
    checked.append('Fourier inversa: 7 bloques y angulos')
    divided=h.transformar_par(1,0,simbolico=True)
    delayed=Bloque('fase',(0,),phi).transformar_par(*divided,simbolico=True)
    equal('Interferencia: H fase H',h.transformar_par(*delayed,simbolico=True),((1+sp.exp(sp.I*phi))/2,(1-sp.exp(sp.I*phi))/2))
    return checked

def main():
    checked=validate()
    (ROOT/'img').mkdir(exist_ok=True)
    files=[]; links=[]
    for i,c in enumerate(CARDS,1):
        stem=f'ES_52_fichas-comonentes-{i:02d}'
        png=ROOT/'img'/f'{stem}.png'; md=ROOT/f'{stem}.md'
        render(c,i,png)
        equations='\n\n'.join('$$\n'+eq+'\n$$' for eq in c['eq'])
        text=f'''# ES_52 — Ficha {i:02d}: {c['title']}

Autor: Víctor Estrada Díaz  
Proyecto: UNIHOLOG / V-COMPUTACION  
Fecha: 23 de octubre de 2026

![Ficha {i:02d}: {c['title']}](./img/{stem}.png)

## Circuitos y relaciones

**Q:** {' '.join(c['qtext'])}

**V:** {' '.join(c['vtext'])}

{equations}

{c.get('extra','')}

## Preparación y lectura

Las conexiones y los parámetros se establecen antes de ejecutar. En V, el
resultado individual permanece sin lectura pública hasta el disparo común
en $t=t_0$. Las relaciones se conservan al propagar la captura. Dos caminos
de una unidad no son dos bits medidos. No se generan salidas mediante sorteos.

## Alcance comprobado

{c['scope']}

{c['check']}

La ficha distingue una identidad de operadores o de tabla binaria de la
equivalencia operacional de un circuito compuesto. El registro de controles
locales figura en [validación](./datos/fichas_componentes_validacion.json).

## Fuentes locales

- [Láminas de construcción Q–V](../DEMUESTRA/Proyectos/ES_52/ES_52_Laminas_Q_V.md).
- [Documento base de la máquina V](../UNIHOLOG/ES_52_Maquina_V_UNIHOLOG.md).
- [Operaciones de señales](../UNIHOLOG/Traductores_V/operaciones_senales.py).
- [Circuito de señales y captura](../UNIHOLOG/Traductores_V/circuito_senales_v.py).
- [Índice de fichas](./ES_52_fichas-comonentes.md).

## Licencia dual — Ética y uso

Texto e imagen: [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/),
con atribución a Víctor Estrada Díaz, uso no comercial y compartir igual.
Código: [HL3](https://firstdonoharm.dev/version/3/0/), con uso comercial sujeto
a sus condiciones éticas. Prohibidos los usos para guerras, violaciones de
los derechos humanos, daño ambiental, explotación, vigilancia masiva y
discriminación. Principios del autor: respeto a la dignidad humana, no uso
lucrativo sin permiso y prohibición de usos dañinos.
[Condiciones del autor](https://estradad.es/licencia.php).
ORCID: [0000-0001-9942-1021](https://orcid.org/0000-0001-9942-1021).
'''
        md.write_text(text,encoding='utf-8'); files.extend((png,md))
        links.append(f'| {i:02d} | {c["title"]} | [Markdown](./{stem}.md) | [PNG](./img/{stem}.png) |')
    index='''# ES_52 — Fichas de equivalencia de componentes Q–V

Autor: Víctor Estrada Díaz · 23 de octubre de 2026.

Trece fichas con circuitos, leyes de transformación, preparación, lectura y
alcance comprobado. La numeración utiliza el nombre solicitado
`ES_52_fichas-comonentes-<numero>`.

| Ficha | Componente | Documento | Imagen |
| --- | --- | --- | --- |
'''+ '\n'.join(links)+'''

Las equivalencias locales y binarias no certifican por sí solas la ejecución
coherente de todas sus composiciones. Cada ficha identifica su alcance.

[Controles algebraicos y binarios](./datos/fichas_componentes_validacion.json).
[Generador editable](./datos/generar_fichas_componentes.py).

Textos e imágenes: CC BY-NC-SA 4.0. Código: HL3. Se aplican la atribución y
las condiciones éticas del autor detalladas en cada ficha.
'''
    (ROOT/'ES_52_fichas-comonentes.md').write_text(index,encoding='utf-8')
    report={'fecha':'2026-10-23','fichas':len(CARDS),'controles_correctos':checked,
            'alcance':'algebra local, tablas binarias y secuencia compilada; no equivalencia general Q4/V4',
            'archivos':{str(p.relative_to(ROOT)):hashlib.sha256(p.read_bytes()).hexdigest() for p in files}}
    (ROOT/'datos/fichas_componentes_validacion.json').write_text(json.dumps(report,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
    print(f'{len(CARDS)} PNG + {len(CARDS)} Markdown + indice; {len(checked)} controles correctos')

if __name__=='__main__': main()
