Skip to content

Motor de diseño de la neurona LIF: leyes, solver, generador de layout y verificación - #27

Open
carloscl03 wants to merge 68 commits into
AlejandroJuarezLora:mainfrom
carloscl03:lif-mim-option-b
Open

Motor de diseño de la neurona LIF: leyes, solver, generador de layout y verificación#27
carloscl03 wants to merge 68 commits into
AlejandroJuarezLora:mainfrom
carloscl03:lif-mim-option-b

Conversation

@carloscl03

Copy link
Copy Markdown
Contributor

Motor de diseño de la neurona LIF en GF180MCU: se le pide un comportamiento eléctrico y devuelve las dimensiones, el layout y el netlist de referencia, contando qué tuvo que cambiar y por qué.

from lif_design import NeuronSpec, design
d = design(NeuronSpec(freq_range=(200, 1500), iex_range=(20, 200)))
print(d.report())

Qué trae

  • designs/scripts/lif_design/ — el paquete: leyes empíricas, solver, generador de layout con gLayout, y la capa de verificación (DRC, comprobación de redes, netlist de referencia para LVS).
  • designs/libs/tb_analog/tb_lif/ — los testbenches de ngspice con los que se ajustaron las leyes.
  • designs/notebooks/3_test_lif_engine.ipynb — recorrido completo del motor, con los casos de robustez.

Las seis leyes salen de barridos de ngspice (.tran 1n), no de fórmulas: frecuencia 2.03% RMS, ganancia 2.18%, umbral 1.32%, excursión 1.68%, y C_in = 0.945 + 0.865·W al 0.67%. Validadas fuera de la rejilla de ajuste en 18 puntos: −0.00% de media, 1.23% RMS.

Estado

Sobre el diseño de 500 kHz a 100 nA, celda de 40.73 × 25.95 µm:

  • DRC 0 con el deck de gf180, mim_option=B, metal_level=5LM
  • LVS Netlists match contra el netlist que emite el propio motor
  • 14 casos de la suite en verde — siete juegos de dimensiones a mano y siete especificaciones recorriendo el camino completo del solver, cada uno con su DRC y sus seis redes

El netlist de referencia se emite desde el mismo NeuronDesign que genera el layout, así que un fallo de LVS solo puede significar un bug del generador. No existe la clase de fallo "alguien tecleó distinto en dos sitios".

Qué NO toca

88 ficheros nuevos, 0 borrados, y solo dos .gitignore modificados (reglas para artefactos de simulación propios). El pin del submódulo designs/libs/gLayout se queda en 4662db4, exactamente donde está en main. Ninguna celda, script ni flujo existente cambia.

Requisitos — importante para probarlo

El motor no construye contra el gLayout que el submódulo fija hoy. Le faltan arreglos que están en revisión en upstream. Medido sobre 4662db4:

gLayout resultado
el pin tal cual no construye — via_array, dispositivos estrechos
+ #100 no construye — mimcap con extensiones
+ #100 #102 #113 construye, 2583 violaciones (todas de rejilla)
+ #100 #102 #103 #104 #113 #114 construye, DRC 0

Por eso van dos lanzadores: run_GL.sh para el flujo oficial del equipo y run_GL_tmp.sh apuntando a un gLayout con esos PRs aplicados. Si se prueba con el oficial van a salir 2583 violaciones, y no son de la celda: es el pin.

De esos PRs, el #104 lleva aprobado desde el 14 de agosto y el #100 abierto desde el 3. En cuanto entren, el pin del submódulo puede avanzar y esto corre con el flujo normal.

Nota sobre el resto del repo

Aprovecho para dejar dicho, por si es útil: construyendo vuestro integrator y vuestro encoder contra ese mismo pin, el integrator no compila (Component.add: unsupported type Component, lo arregla el #102) y el encoder da 2893 violaciones de rejilla que el #104 elimina. No he tocado nada de eso — solo lo medí al comprobar que lo mío no os rompiera nada.

Barridos de Iex, Cm, L_M5 y W de salida con medicion multi-ciclo, mas
validacion cruzada de cada ecuacion en configuraciones distintas al
nominal. Resultados en results/lif_knowledge_base.md.

Ecuaciones validadas:
- Iex = 169.1*(2.571-Vin)^2         robusta (<0.03% entre configs)
- I_drive = 85*W                    robusta (<1%)
- f = (363.6/L_M5 - 2.08)*Iex       general, reemplaza la del nominal
- Vth = 2.893*L_M5/Cm - 21.28/Cm + 1.2606
- Cm_min = 2.166*L_M5 + 52.06       limite por overshoot capacitivo

Se descarto f ~ 1/L_M5: con 6 puntos el producto f*L varia 31%, la
dependencia es indirecta via el threshold.
W_M5 resulta parametro de primer orden (nunca se habia variado). A
Iex=100nA, Cm=150f, L_M5=50u: W de 0.5 a 5u cambia la frecuencia de 1359
a 278 kHz y el threshold de 1.68 a 3.25 V (este ultimo casi en Vdd).

Hallazgos:
- Vth = 0.3387*W_M5 + 1.6114  (R2=0.981, a L_M5=50u)
- f ~ 1/W_M5 (R2=0.972) pero satura arriba de 2.5u
- W_M5 tambien mueve el limite de Cm: la ley Cm_min=2.166*L_M5+52 esta
  incompleta, le falta el termino de W
- El parametro NO es W/L: mismo ratio 0.025 da 545 vs 2531 kHz segun las
  dimensiones absolutas. Domina el acoplamiento capacitivo (area W*L),
  no la resistencia de fuga
- Rango util W_M5 <= 1.25u; el nominal esta en el borde
Barrido 4x4 (W 0.5-2.5u, L 25-50u) a Cm=200f. Solo 11 de 16 puntos en
regimen valido: con W y L grandes 200f ya no evita el overshoot.

- freq ~ 1/(W*L)  R2=0.978; ley de potencia W^-1.008 * L^-1.304 R2=0.981.
  Los modelos lineales en (W,L) fallan (R2<=0.83): la relacion es inversa.
- Vth = -0.11481*W +0.00280*L +0.008966*W*L +1.3001  R2=0.994, LOO=0.025V.
  El termino de interaccion W*L es imprescindible (sin el, R2<=0.92).
- El area no predice el overshoot: W=2.5/L=25 (area 62.5) queda OK mientras
  W=1.75/L=41 (area 71.8) falla. Para el overshoot L pesa mas que W.
- La ley Cm_min=2.166*L+52 subestima el limite para W>1.25u.
Barrido de 30 puntos (6 combinaciones W,L x 5 valores de Cm) interpolando
donde Vm_min cruza cero.

La ley anterior Cm_min=2.166*L+52 solo acierta en W=1.25u (su punto de
medicion). Fuera de ahi subestima hasta 105 fF, lo que llevaria a disenos
en regimen anomalo.

Modelo aproximado: Cm_min = 6.230*(W*L) - 208.7  (R2=0.826)

Con reservas: solo 4 puntos tienen frontera medible (los de W=0.5u caen
por debajo de la grilla) y el LOO RMSE es 42 fF. Modelos con mas terminos
dan R2=1.0 pero es sobreajuste trivial (4 parametros, 4 puntos). Usar como
estimacion gruesa y verificar por simulacion el punto concreto.
Cm_min = 8.94 * W_M5^1.038 * L_M5^0.700   R2=0.980, LOO=15.4 fF

Busqueda binaria del limite (rango 30-500 fF) en 12 combinaciones, lo que
permitio capturar las fronteras por debajo de 100 fF que la grilla anterior
no alcanzaba. Errores dentro de +-11%, la mayoria bajo 5%.

Validada por extrapolacion: W=2.0u/L=33u predicho 207 fF vs 206 medido
(0.6%); W=2.5u/L=41u predicho 301 vs 337 (10.7%).

Nota metodologica: con los primeros 9 puntos (todos W<=1.5u) el modelo
W^2*L parecia el mejor (LOO 10.3 fF), pero al anadir W=1.75u fallo por 31%
y su LOO subio a 28.3. Un LOO bajo no protege contra extrapolacion cuando
el rango de datos esta sesgado.
Se fusionaron las 85 mediciones de Vm_min de los 4 barridos y se ajusto el
modelo fisico Vm_min = A - k*W^a*L^b/Cm (R2=0.837). Despejando el limite da
Cm_min = 3.48*W^1.453*L^0.847, con RMSE 33.6 fF contra las fronteras
medidas, frente a 11.5 fF de la ley ajustada directamente a ellas.

La razon: las 85 mediciones cubren todo el rango de Vm_min, la mayoria
lejos de cero. El ajuste global optimiza el error medio en todo el espacio
y pierde precision justo en Vm_min~0, que es lo que define el limite. Se
conserva la ley de las 12 fronteras.
El paso .tran 20n sobreestima la frecuencia +41% en promedio y hasta +193%:
el integrador salta ciclos y los cuenta como disparos. test_tstep.sh lo
verifica re-simulando 5 configuraciones a 20/5/1 ns.

Consecuencias:
- el jitter de 30-55% que atribuiamos al circuito era resolucion temporal,
  desaparece con paso fino (<0.5% real). No hay frontera de jitter.
- Cm no controla la frecuencia: varia <4% al triplicarse. Lo que veiamos
  como +79% era el artefacto.

Leyes nuevas sobre 32 puntos, todos convergidos:
  f     = 24837*W^-1.076*L^-0.940              LOO 3.1%
  swing = 4.114*W^0.951*L^1.065*Cm^-1.006      LOO 12 mV
  Vth   = 1.2792 + (-16.83W+0.4884L+1.766WL)/Cm  LOO 16 mV

Como Cm sale de la ecuacion de frecuencia, el algoritmo de diseno deja de
necesitar iteracion: W,L fijan f y luego Cm ajusta Vth sin realimentar.

Los scripts nuevos marcan NOCONV los puntos con jitter >2% para que un dato
mal convergido no entre en un ajuste sin avisar.
Puntos nuevos W en {0.75,1.4,2.1} x L en {33,50}: ninguna combinacion esta
en la grilla con que se ajustaron las leyes, y L=50 queda fuera del rango
ajustado (25-41), asi que ademas prueba extrapolacion.

  f      error medio -0.00%, RMS 1.23%
  Vth    error medio -0.63%, RMS 0.90%
  swing  error medio -0.58%, RMS 1.11%

Extrapolando a L=50 el error se mantiene ~1%. El LOO de 3.1% resulto
conservador.

Iex medida a paso 1n: RMS 0.07%, y el reajuste da k=169.19 vs 169.1 (+0.05%).
Queda comprobado por medicion lo que antes solo se argumentaba.

Limites de validez que faltaban:
  Cm >= 50f    bajo eso Vth diverge (a 25f predice 5.83V, imposible con
               VDD=3.3V). Todo diseno debe comprobar Vth < 3.3.
  W <= 2.5u    sobre eso f satura (a 5u predice 111 kHz, medido 278)
  Iex >= 50nA  para que f sea lineal en Iex (a 23nA el error es +7.4%)

test_tstop.sh: acortar el transitorio de 100u a 30u da resultados identicos
a 4 cifras, 3.3x mas rapido.

bench_par.sh: ngspice ya usa 7.3 de 8 nucleos con un solo proceso, asi que
correr varios en paralelo no aporta y degrada por contencion de cache. Los
barridos van en serie.
36 puntos en los bordes del espacio de diseno. Los estados no-OK son el dato:
marcan donde deja de funcionar el circuito.

  Iex minimo   ~12.5 nA   (Vin>=2.3 no oscila)
  L_M5 maximo  50-60 um   (L=60 no converge)
  W_M5 maximo  ~3.8 um    a Cm=400f, depende de Cm
  Cm minimo    80-100 f   para W=1,L=41

Correccion: se documentaba que sobre W=2.5um la ley fallaba porque f satura.
Es falso. El barrido fino muestra que la membrana se sale del riel: W=3.5 da
Vm_min +0.087 (OK) y W=4.0 da -0.058 (ANOMALO). El punto de W=5um que se uso
como referencia de -60% era anomalo. Es la misma frontera Cm_min(W,L) vista
desde el eje de W.

Cm_min resulta conservadora en +34%: predice 120f donde la frontera real esta
entre 80 y 100f. Del lado seguro, pero desperdicia area.

Reajuste con los 69 puntos limpios (13 niveles de W, 8 de L): las leyes no
cambian. f pasa de RMS 2.07% a 2.03%, Vth y swing igual. Se conservan las
actuales; cambiar coeficientes por 0.04 pp invalidaria la validacion externa
a cambio de nada.

Nota metodologica: un corte a W=1.0 fijo daba exponente de L -0.871 frente al
-0.940 de la ley, y parecia un error del 7.3%. Con los 69 puntos el exponente
global es -0.9285. Un exponente de una seccion 1-D no es comparable con el de
un ajuste multivariable.

La ley pierde precision bajo L=25um: 5-7% frente al ~1% habitual.
La celda usa entrada de corriente (se conecta a distintas etapas), asi que se
rehizo el tb sin depender de Vin. Comprobado que la impedancia de M6 era lo
bastante alta como para no afectar: 501 vs 494 kHz, 1.4%. Las leyes previas
siguen valiendo.

Ganancia de modulacion (45 pts, 9 configuraciones):
  k = 1063*W^-0.936*L^-0.940  kHz/nA    LOO 4.4%
  f = k*Iex + f0, lineal con R2 0.998-0.9999
  Cm no interviene: anadirlo empeora el ajuste.
  Iex es ortogonal a Vth: <1.2% de variacion con la corriente x16.

Carga de salida (24 pts):
  C_load_max ~= 600*W_M7M8 fF  (criterio tf <= 5ns)
  La carga NO afecta la frecuencia: <0.7% con C_load de 0 a 1600 fF.

Impedancia de fuente (28 pts) -- el requisito mas restrictivo:
  ro >= 190/Iex[nA] GOhm para 1% de error.
  A 100 nA son 1.9 GOhm. Un espejo simple da 1-10 MOhm y desvia la
  frecuencia 20-200%. Con 3 MOhm el circuito deja de oscilar.

Ventana de Iex:
  techo  f_max ~4500 kHz en 3 configuraciones con corrientes distintas
         (350/500/600 nA) -> es limite de periodo, no de corriente.
  piso   NO EXISTE. Verificado hasta 5 nA con k y swing constantes.

El piso que se habia reportado antes era artefacto: con tstop=30us una
neurona a 15 kHz (periodo 67us) no completa un ciclo y el criterio lo
marcaba NO_OSCILA. Segundo artefacto instrumental de la serie, tras el del
paso de .tran.
Resuelve intencion -> parametros (W_M5, L_M5, Cm, W_M7M8) con las leyes
medidas. Herramienta para que la use una IA: entrada determinista y salida
estructurada, incluido el detalle de por que algo no se puede.

Todo por despeje cerrado, sin iteracion: las leyes son potencias o lineales
en 1/Cm, y como Cm no interviene en la frecuencia la cadena es unidireccional
(W,L -> f ; W,L,Cm -> Vth) y no hay punto fijo. 149 us por diseño completo.
Lo unico no analitico es elegir sobre la curva de iso-frecuencia cuando W y L
quedan libres, y ahi es un barrido de 60 pasos con criterio explicito.

Solo stdlib (math, dataclasses, enum). Sin numpy: son ~15 pow() por diseño y
la importacion costaria 100 ms para un calculo de 50 us.

Politica de prioridades: los objetivos mandan sobre las dimensiones fijadas.
Si contradicen, se ajusta la dimension con WARNING; si no hay salida, ERROR
con la cadena causal. Nunca lanza excepcion.

Orden de ajuste por coste de cambio: Cm, luego L_M5, luego W_M5. W_M7M8 queda
fuera de la cadena porque depende solo del fan-out y la carga no afecta a f.

La ganancia se reajusto al probar el motor: la constante estaba transcrita
como 1063 cuando el ajuste da 280.22, y los exponentes tambien estaban
desviados. Queda k = 280.22*W^-1.0447*L^-0.9923 con RMS 2.18%. Derivarla de
f(100)/100 daria RMS 7.9% con sesgo +6% porque la recta tiene intercepto.
verify.py genera el netlist del diseño, lo simula y compara lo medido con lo
predicho. En primera instancia el return del sistema es SPICE; cuando gLayout
avance sera el layout y esto quedara como verificacion previa.

Probado en tres diseños, dos de ellos con geometrias que el sistema invento y
que no estaban en ninguna grilla de caracterizacion:

  nominal  W=1.25  L=50.0   Cm=150   f 494 -> 501   -1.4%
  rapida   W=0.602 L=35.42  Cm=77    f 1500 -> 1492 +0.5%
  lenta    W=2.835 L=40.42  Cm=422   f 250 -> 253   -1.0%

Peor error de los tres: 2.5%. Valida la cadena completa: leyes, despejes,
eleccion sobre la curva de iso-frecuencia, netlist y simulacion.

Dos salvaguardas contra los artefactos que ya nos costaron caro: el paso queda
fijo en 1 ns (con 20n la frecuencia se infla hasta +193%) y el transitorio se
calcula por diseño para dejar 8 ciclos utiles tras el arranque (con tstop fijo
una neurona lenta no completa ni un ciclo).

f0 resulta ser CERO. El 14-144 kHz que salia de ajustar rectas sobre Iex
25-400 nA era artefacto: un intercepto lejos del origen absorbe la curvatura
de la zona alta. Midiendo a 5 y 10 nA sale f0=+0.57 kHz. Extrapolar la recta
con intercepto hacia abajo falla +56%; el anclaje proporcional da -3.4%.
Se aborto el barrido de f0 (habria sido 18 h) porque los 2 primeros puntos ya
respondian la pregunta.

Queda documentado un sesgo real: k tiene curvatura y a corriente baja la
pendiente es ~11.6% mayor que la ley.
Hay dos testbenches y nada explicaba la diferencia. El vigente es
tb_charac_isrc.spice (entrada de corriente, sin M6); tb_charac.spice queda
como historico.

El README de tb/ recoge las dos trampas que costaron caro durante la
caracterizacion: el paso de .tran (con 20n la frecuencia se infla hasta +193%)
y el transitorio corto (una neurona a 15 kHz no completa un ciclo en 30us y
parece que no oscila).

Se borra verify_laws_par.sh: bench_par.sh midio que ngspice ya usa 7.3 de 8
nucleos con un solo proceso, asi que la version paralela no aplica.

.gitignore cubre ahora netlists_*/ y los verify_*.spice que genera el modulo
de verificacion.
La documentacion tecnica del repo esta en ingles (README principal, stdp), asi
que estos tres archivos se alinean. Los mensajes de commit siguen en español
como el resto del equipo.

La knowledge base habia crecido por acumulacion: 1141 lineas donde las leyes
vigentes estaban en la linea 26 y luego venian 700 lineas de secciones
historicas con ecuaciones ya invalidadas, y un segundo resumen en la 1049. Un
lector no podia saber que creer.

Queda en 320 lineas con estructura por temas: las 5 leyes, la estructura del
espacio de diseño, los limites de operacion, las correcciones a conclusiones
previas, la validacion y la metodologia. No se pierde informacion: lo que se
fue eran ecuaciones sustituidas y tablas duplicadas.
example.py es ejecutable y cubre los seis casos: default, rangos, hibrido,
conflicto resoluble, imposible fisico y verificacion por simulacion.

Al escribirlo salio un bug: con una sola dimension fija (W o L) y un objetivo
de frecuencia, si la otra se salia de rango el motor la acotaba y devolvia una
frecuencia distinta a la pedida (+26% en el ejemplo). Solo entraba a ajustar
ambas cuando las dos estaban fijas.

Ahora libera tambien la que el usuario habia fijado, que es la politica
acordada: los objetivos mandan. Verificado en cuatro casos, la frecuencia
resultante queda a <=0.1% de la pedida.
Antes habia que hacer sys.path.insert con una ruta relativa distinta segun
desde donde se ejecutara el script. Con pyproject.toml basta:

    cd sch/lif && pip install -e .

y luego 'from design import NeuronSpec, design' funciona desde cualquier ruta.
Modo editable, asi que editar laws.py se refleja sin reinstalar.

Sin dependencias: solo stdlib.

.gitignore cubre ahora los *.egg-info/ que deja pip install -e.
El motor vivia en sch/lif/design/, dentro de una carpeta que se creo para
trabajo personal ("creando espacios para que cada quien trabaje"). Lo que
se entrega es designs/, asi que ahi debe estar.

  designs/scripts/lif_design/       leyes, spec y solver
  designs/libs/tb_analog/tb_lif/    testbench, siguiendo tb_ota_5t

verify.py pasa a ser el fixture.py del testbench: hace lo mismo que el de
tb_ota_5t -- componer netlist, correr ngspice, medir -- y ahora se invoca
con pytest como el resto.

De paso, freq_range e iex_range aceptan un escalar ademas de un par:
freq_range=500 es lo mismo que freq_range=(500, 500). Pedir ambos fijos
antes reventaba con ZeroDivisionError al calcular la ganancia (0/0);
ahora se resuelve como punto de operacion.
plan_row() calcula la separacion entre bloques a partir de las rutas que
cruzan cada hueco, en vez de un factor fijo. El orden de los bloques se
respeta tal cual se da; esto solo decide cuanto separarlos.

Sobre la neurona de layout/lif/neurona.ipynb, los tres huecos dejan de ser
iguales: 0.58 / 1.16 / 0.58 um segun crucen una o dos rutas.

OVERLAP_NOTES.md recoge lo que se midio para llegar ahi:

  * un fet de glayout lleva ~2.6 um de bbox vacio por lado, asi que solapar
    los bbox no es de por si un error -- el limite util es 5.5 um, donde las
    difusiones se acercan a 0.20 um y salta DF.3a
  * el DRC no avisa de esto: a 6 um las dos difusiones se fusionan y los dos
    transistores pasan a ser uno, y el deck lo da por bueno
  * with_tie/with_dummy/with_substrate_tap explican las 31 violaciones de la
    celda actual. Con los defaults de glayout el DRC queda limpio, pero el
    area pasa de 1168 a 4204 um2
  * fingers mueve la frecuencia <=0.40% (dentro del ruido de la propia
    simulacion) pero el ancho del bloque +85% con 2 dedos y +255% con 4:
    es decision de layout, no del motor

Ojo con el deck: el que trae glayout en src/glayout/pdk/gf180_mapped/ es el
que evalua. El de $PDK_ROOT/libs.tech/klayout/tech/drc/ devuelve un reporte
vacio incluso para un GDS con violaciones conocidas.
place.py calcula donde va cada bloque. Ninguna distancia es un factor de
ajuste: salen de las reglas del pdk y de las capas que dos bloques
comparten. Lee tamano, pozo y capas ocupadas del componente ya generado,
via evaluate_bbox y los puertos well_*, asi que no hay que intuir nada
del generador. El pitch entre inversores le sale 5.840 um contra 5.820
medidos en la celda de Abrahan, sin barrer.

Las rutas no entran en el hueco: cruzan por encima de los bloques, no
entre ellos. clearances() dice por que capa vuela cada red, o si no le
queda ninguna y toca rodear.

build.py convierte el plan en geometria. Los rieles se dibujan aqui y no
se dejan al llamante: una celda que expone VDD y VSS y no los conecta
parece terminada y no lo esta, que es como la neurona LIF llego a DRC
limpio sin alimentacion.

Los rieles van una capa por encima de los bloques. Un fet de glayout
ocupa met2 hasta su anillo de tie y el puerto de source cae dentro del
anillo, asi que una bajada en met2 lo cruza al salir y cortocircuita los
dos. En met3 la pletina pasa por encima y solo baja, con via, sobre el
source.

preview.py dibuja la celda con sus violaciones marcadas. Los numeros
dicen que regla fallo; el dibujo dice que lado del dispositivo ya esta
ocupado.

Barrido de 10 casos: topologia correcta en los diez y DRC 0 en nueve.
Multipliers > 1 avisa: conecta bien pero deja las bajadas a 0.10 um del
ruteo interno.
La fila unica desperdiciaba el 45% del area: la altura la fijaba el
inversor mientras M5, que mide 24-54 um de largo, dejaba vacio todo lo
que tenia encima y debajo. En bandas -- pfets arriba, M5 cruzando,
nfets y condensadores abajo -- el hueco de M5 se llena y la celda pasa
de 84x14 a 54x21, con la forma que el equipo ya usaba en su layout.

place.py gana Band y plan_bands. El hueco entre bandas sale de la regla
de pozo, y son distintos por eso: 1.4 um entre dos pwell, 0.4 entre
pwell y nwell. Los rieles se calculan sobre el conjunto ya planificado,
asi que redimensionar los condensadores los aparta solo.

Rails.above pasa a mirar toda la fila, no un bloque: el que decide la
CAPA no es el que decide la ALTURA. En la LIF el inversor es lo mas
alto pero el mimcap es lo unico que llega a met3, asi que mirando solo
al mas alto los rieles salian en met3, atravesando el cap.

build.py gana lif_cell. Dos cosas costaron seis intentos y conviene
dejarlas escritas:

Puerta y drenador no pueden bajar por la misma columna. gate_S y
drain_S estan los dos en x=0 del dispositivo, asi que rutear ambos
desde ahi los superpone y deja cada inversor en diodo -- con puerta y
drenador en una sola red, que a un verificador descuidado le sigue
pareciendo un par bien formado.

Y no se puede suponer donde esta el metal. La columna met3 del drenador
no cae sobre su puerto, porque c_route sale al este antes de subir; su
x se toma del bbox de la ruta. Sobre la puerta si coincide, porque esa
es una recta por el centro del bloque.

Verificado por extraccion en 5 juegos de dimensiones -- inversores 4.5x
mas anchos, M5 con la mitad de longitud, M5 casi 3x mas ancho y
condensadores un 60% mayores: DRC 0 y topologia correcta en los cinco.
Los condensadores se comprueban por geometria, no por extraccion: el
techfile de magic situa el MIM entre metal4 y metal5 y nuestro stack lo
tiene en met2/met3, asi que lo que reporta de ellos no corresponde.

Falta la realimentacion de inv1 a la puerta de M5 y los rieles.
Las fuentes van al anillo que tienen justo al lado y el anillo baja al
riel, en vez de tirar cada fuente al riel cruzando la celda entera, que
es lo que se llevaba por delante las pistas de fan-out y membrana.
Rieles y bajadas van gruesos porque por ahi pasa la corriente.

M5 baja al VSS por el hueco libre que dejen los caps, calculado, y si no
hay hueco salta al anillo del nfet que tiene debajo. La realimentacion a
la puerta de M5 sale con un solo via: la columna met3 del drenador de
inv1 ya cruza la tira de puerta.

Anade check.py y netcheck.py. El extractor de magic no sirve aqui porque
su techfile pone el MIM en metal4/metal5 y el nuestro esta en met2/met3,
asi que la conectividad se camina sobre el metal. Con eso salta que el
deck se saltaba todas las reglas MIM (mim_option por defecto es "Nan") y
que faltaba MIM.1 en el placement: 1.2 um de la placa inferior a
cualquier met2, no la separacion normal.

Seis casos de dimensiones, DRC 0 y las seis redes del LIF exactas.
from_design() toma lo que devuelve el solver y saca la celda. Faltaban dos
piezas: convertir Cm a geometria, y poder dimensionar el inversor de salida
aparte, que es el unico que el solver dimensiona por carga.

La conversion no es area por densidad. El modelo del PDK es
C = c_cox*area + c_capsw*perimetro, y a nuestro tamaño el perimetro pesa un
21%. Ver mim.py, con los parametros copiados de sm141064.ngspice.

Al dejar mandar a la caracterizacion salieron dos cosas que los casos de
dimensiones fijas no tocaban: MIM.8a no deja un FuseTop de menos de 25 um2,
o sea 5 um de lado, asi que el numero de MIM baja solo cuando la membrana es
pequeña; y el salto anillo a anillo topaba con los dos anillos en vez de
solaparlos y el redondeo dejaba 7 nm de hueco.

Elegida la opcion A con receta 2f0: A deja libres las capas altas para el
ruteo entre neuronas, y de las tres recetas la densa es la que menos area
pide, entre 8% y 25% de celda. Falta confirmarlo con el equipo, que es una
decision de todo el chip.

check.py corre ahora tambien desde la especificacion. Diez casos, DRC 0 con
reglas MIM y las seis redes exactas.
Las dos capas por separado, las dos verificaciones y las dos tandas de
robustez, con las salidas dentro para poder leerlo sin ejecutar nada.

Deja escritas las dos trampas que nos costaron dias: el deck se salta
todas las reglas MIM si no le pasas mim_option, y el extractor de magic
no vale aqui porque su techfile pone el MIM en otras capas.

check.py se vuelve portable de paso. Importaba un nombre de paquete que
solo existia en mi contenedor y tenia /tmp escrito a mano, asi que ahora
resuelve el paquete por donde esta el fichero y toma el deck y la salida
del entorno.
La imagen ya trae python 3.12, gdstk y klayout, asi que basta un venv y el
glayout de la rama. Sin descargas grandes.

Hace falta GLAYOUT_BACKEND=gdstk y no es opcional: el gdsfactory 9.40 de la
imagen ya no expone component_reference, asi que glayout de upstream ni
arranca aqui, cae a un DummyPdk. Con nuestro backend si.

Instala desde capimagics-base, no desde PyPI: comprueba con un assert que
capmet apunte a fusetop, porque si apunta al marcador los MIM salen con las
placas en corto y eso no da error en ningun sitio.

Verificado corriendo el notebook entero en un contenedor limpio: mismos
resultados que en el de desarrollo.
…y PR

Junto a los otros dos y con su misma forma, pero desde capimagics-base y
sobre el python de la imagen. Sin conda ni pyenv, sin descargas grandes.

Los dos que hay llevan a un glayout con tres problemas: no arranca siquiera
en esta imagen (cae a un DummyPdk sin fallar del todo), el mimcap de gf180
sale con las placas en corto, y los fets estrechos no se construyen.
run_GL_conda.sh instala de PyPI y run_GL_pyenv.sh instala libs/gLayout, que
es un clon de upstream main, asi que los dos caminos acaban ahi.

Este no toca sus entornos ni su kernel: registra uno aparte llamado 'lif'.
Y si el puerto esta ocupado avisa en vez de lanzar un segundo Jupyter.

Comprobado en un contenedor limpio, con asserts que revientan si el glayout
instalado generaria los MIM en corto. Sustituye a setup_container.sh, que
estaba escondido dentro de lif_design.
Los notebooks declaran ese kernel, asi que en un contenedor recien creado
abren sin kernel valido. Y si uno elige el equivocado, el error que da no
apunta al kernel por ningun lado: sale un ValueError dentro de pmos, que es
el bug de los fets estrechos del glayout sin parchear.

Solo lo crea si no existe. Si ya hay uno es porque lo puso run_GL_conda.sh o
run_GL_pyenv.sh, y ese no se toca: avisa de que hay que cambiar a mano.
C_in = 0.945 + 0.865*W_M5, la capacidad que la fuente ve en el nodo aparte
de Cm. Medida como C_total - Cm con C_total = Iex/(dV/dt) sobre la rampa: la
propia fuente cargando el nodo es la medida, sin necesidad de .ac (el
circuito oscila y no hay punto de operacion que linealizar).

LOO 0.67% RMS, validacion externa 0.56% RMS sobre rejilla disjunta, con dos
puntos en W=0.30 y W=0.22, por debajo del borde de ajuste. Afin y no
potencia, por la misma razon que Vth: el 0.945 son las puertas de M1/M2, que
cuelgan del nodo aunque M5 sea minimo. Una potencia pura pasa por el origen
y falla -21% en W=0.22.

Entra como salida predicha, nunca como objetivo: depende solo de W_M5, y
1.1-4.0 fF en todo el envolvente es un rango demasiado estrecho para
restringir nada. c_in_max va en contexto y se comprueba en _validate; no
compite por W_M5, que es el ultimo eslabon de la cadena de ajuste.

NO se suma a freq(): la ley de frecuencia se ajusto sobre simulaciones del
circuito completo que ya la contienen.
Cm era la unica incoherencia viva de la convencion: mayuscula en el spec y
en las claves de predicted, minuscula como parametro en laws.py y en los
bancos de prueba. La regla es que el guion bajo es el subindice del simbolo
del esquematico (W_M5, c_load, C_in), y Cm va pegado porque asi viene del
esquematico y del paper.

Arrastra cm_min -> Cm_min y solve_cm_for_vth -> solve_Cm_for_vth. Mayuscula
en medio de un nombre de funcion incomoda a PEP 8, pero laws.py ya lo rompe
a proposito con solve_L_for_freq y solve_W_for_freq, asi que esto es
consistente dentro del fichero en vez de abrir una excepcion nueva.

Renombrado mecanico: ningun llamador pasaba cm= por palabra clave.
La suite pasa de 6+4 a 7+7 casos de layout. Los tres nuevos de
especificacion entran por rutas distintas Y salen con geometrias que ninguna
de las anteriores producia, que es el criterio para estar ahi porque cada
caso cuesta un DRC completo:

  umbral 2.0V   Vth arrastra Cm, o sea el numero de MIM
  Iex 5 nA      saca W_M5 a 0.222, casi el minimo; la celda mas estrecha
  carga 800fF   unica ruta que dimensiona W_M7M8 (1.333)

Un objetivo de ganancia NO esta: recorre codigo distinto en el solver pero
resuelve por (f_hi, iex_hi), asi que da el mismo GDS que el caso de 800 kHz.
Comprobado antes de descartarlo, y anotado para que no vuelva a colarse.

En CASOS entra "todo minimo" (W_M5=0.22 con L_M5=20), la esquina donde el
anillo de M5 se queda mas estrecho que el paso de las pistas.

El notebook gana la seccion de C_in -- colocada junto a source_ro, porque
las dos son el interfaz con la fuente: una es lo que le exigimos y la otra
lo que le ofrecemos -- y "el solver nunca lanza", ocho entradas hostiles de
las que dos hacen saltar ValueError dentro de las leyes. Ese contrato
importa porque quien llama a design() es un generador: si lanzara, un
barrido de mil neuronas se caeria en la primera absurda.

Los 14 casos de layout pasan con DRC 0 y las seis redes.
…ierta

Seis leyes en vez de cinco. C_in entra con su derivacion, la fila de
validacion externa (0.56% RMS) y la salvedad de que ese numero mide la ley
sobre la superficie Cm = 2*Cm_min, donde viven las dos rejillas; fuera de
ella se desvia hasta el 15%.

En "Not characterized" queda la pista que el dato pide y que no modelamos a
proposito. A geometria fija el exponente de Cm sale +0.098 en W=0.5 y -0.023
en W=2.5: signos opuestos, asi que no hay forma separable. Ordenando los
diez puntos por excursion en vez de por geometria colapsan sobre una sola
curva, y ahi se ve que lo que parecian dependencias de Cm y de L son una
sola variable vista dos veces, porque swing lleva las dos dentro. Predijo
dos puntos nuevos a 0.1% y 0.2%. Uno de los diez se desvia un 26% y queda
sin explicar; esta marcado, no escondido.

No se afina mas porque el efecto se traduce en menos de 0.15% de desviacion
en frecuencia sobre todo el espacio que el solver alcanza -- ocho veces por
debajo del 1.23% RMS de la propia ley de frecuencia. Afinar una correccion
muy por debajo del error de lo que corrige no compra nada.
El equipo se mueve a opcion B: glayout fusiono el PR 106, que reescribio
mimcap() y movio capmet a met4/met5. A y B no pueden convivir en el mismo
proceso -- la DRM lo dice y hay una sola capa FuseTop, asi que un poligono
no puede declarar a que altura pertenece -- de modo que es una decision de
chip, no por celda.

NO ESTA TERMINADO. La celda construye y encoge de 44.53x25.64 a
40.73x24.14, pero las seis redes NO salen: la membrana queda en corto con
VSS y las placas inferiores sin conectar. Ver abajo.

Lo que cambia, y el hilo es siempre el mismo: dejar de suponer las capas
y preguntarselas al PDK.

  _cap_glayers()   lee capmettop/capmetbottom del PDK. Suponerlas deja el
                   ruteo dos niveles por debajo de la placa, sin via que lo
                   salve y sin error: el condensador flota en silencio.

  _CAP_TOP/_CAP_BOT  el mimcap reescrito ya no expone bottom_met_*. La placa
                   inferior sale por la extension sur, que la sube a la capa
                   de la superior.

  la membrana corre por capmettop, asi que llega al banco y se funde con las
  placas sin una sola via -- que es lo que hacia opcion A cuando esa capa era
  met3. De paso sale de met2, el carril mas poblado, y la red sensible de la
  celda pierde vecinos que le acoplen.

  drop() sabe bajar ademas de subir: con el MIM arriba la placa inferior
  queda por encima del riel y la pila se invierte. Y no desplaza "hacia
  dentro del metal" en el mimcap: alli eso empuja la correa contra la placa
  superior, que esta a 0.6 um.

  check.py deriva la opcion del deck del PDK. Con la equivocada, mim_virtual
  sale vacio y MIM.3 acusa al condensador de no tener placa -- un falso
  positivo que parece un fallo de layout. Y sus sondas leen las capas de las
  placas en vez de llevar 42 y 36 escritos.

DRC: 176 violaciones. De esas, ~90 son de mimcap() de glayout: la primitiva
sola, sin nada alrededor, ya da 30 (MIMTM.1, MIMTM.3, MIMTM.10). Reportarlo
aguas arriba antes de seguir puliendo las nuestras, porque si cambian la
geometria de la primitiva parte de este ajuste se rehace.

Lo que falta: la correa de la placa inferior a VSS. Entre el pad de la
extension y la placa superior hay 0.6 um y hay que entrar ahi sin tocar
arriba ni quedarse corto abajo.
Dos errores de esta migracion, y el primero es del instrumento.

netcheck.py tenia la pila cableada hasta met4:

    STACK = [met1, via1, met2, via2, met3, via3, met4]

Con el MIM en met4/met5 eso deja INVISIBLES la placa superior, el puente de
membrana y las correas de VSS. El verificador reportaba las tres conexiones
del condensador como abiertas mientras el layout las tenia. Varias rondas de
depuracion se fueron persiguiendo eso: el mismo error que ya nos costo un dia
con magic y su techfile, medir con una herramienta que busca el condensador
donde ya no esta.

Ahora la pila llega a met5 y la via del MIM se DETECTA en vez de fijarse:
se parte por FuseTop la capa de via que de verdad lo solapa, y se suelda con
el metal de arriba que le corresponda. Partir la que no es deja las placas
soldadas y las conexiones reales fuera del grafo.

El segundo era mio. Habia metido un margen vertical para que la pista de
membrana guardase MIM.1 a la placa inferior, y eso la empujo de 8.4 a 9.7 --
justo encima del anillo de met2 de M5, que es su tap de bulk, o sea VSS. La
pila de la esquina lo tocaba. El margen sobraba: la pista corre por la capa
de la placa SUPERIOR y MIM.1 se mide contra la INFERIOR, que es otra capa.
Quien tiene que apartarse es la pila, y por eso va donde no hay cap debajo.

Estado: VSS y las placas de arriba y abajo conectan. Queda UN corto, entre
membrana y salida, y esta medido: la pila de la esquina no cabe en el canal
entre el ultimo inversor y el banco.

    columna met3 del inversor, borde   15.64
    + separacion met3 + medio pad      x >= 16.19
    cap met4 empieza                   17.11
    - MIM.1 - medio pad                x <= 15.66

No hay x valido: faltan 0.53 um de canal. Toca en floorplan.py, que ya
dimensiona cada hueco por las redes que lo cruzan -- hay que decirle que ese
ademas aloja una pila con margen de MIM.1.

DRC 172, de las que ~90 son de mimcap() de glayout.
Las seis redes pasan. La migracion a opcion B funciona.

Dos restos de un estado anterior del codigo, y los dos hacian daño:

La esquina del camino de membrana llevaba un via_stack de met2 a met5. Venia
de cuando el horizontal era met2 y el vertical met3, y necesitaba una. Con
los dos tramos en la capa de la placa la esquina es un doblez y nada mas; la
pila solo servia para atravesar met1..met4 y tocar la columna del ultimo
inversor. De ahi el corto entre membrana y salida.

Y la bajada se habia ido al lado del banco para que esa pila no cayera sobre
el FuseTop -- el deck de LVS descarta de la conectividad las vias que lo
solapan. Sin pila esa razon desaparece: la bajada vuelve a caer sobre el
primer condensador y se funde con la placa, que es lo que hacia opcion A
cuando la placa era met3. Ni una via en todo el tramo del cap.

Cuesta 9 violaciones mas que rodear por el lado (180 contra 171), todas
MIMTM.1 y MIMTM.3 sobre los condensadores: nuestro met5 se funde con la
placa -- que es justo lo que se busca -- y la placa superior efectiva crece
mas alla del FuseTop, asi que el met4 deja de envolverla los 0.6 um. Se
prefiere el camino directo porque es mas corto y porque ~90 de las
violaciones ya son de mimcap() de glayout: hasta que esa geometria se
arregle no sabemos si esas reglas mediran lo mismo.

Caja 40.73 x 24.14 contra 44.53 x 25.64 en opcion A: la celda encoge un 9%
de ancho porque el MIM deja de competir por met2/met3.
El deck del PDK asume 5LM si no se le dice, pero la copia que trae glayout
asume 6LM. Con 6LM el deck cree que la cima de la pila es metaltop, asi que
`topmin1_via` pasa a ser via4 y `topmin1_metal` met5 -- que es justo el
sandwich del MIM en opcion B. MIMTM.10 entonces acusa a las vias del propio
condensador de ser vias prohibidas dentro de si mismo.

    antes   180   MIMTM.1: 11   MIMTM.3: 21   MIMTM.10: 148
    ahora     3   MIMTM.1: 3

Y el mimcap de glayout, que yo habia dado por roto, da CERO en aislamiento.
Las 30 que le atribui eran del mismo error. Estuve a punto de reportarle
aguas arriba un fallo que no existe.

Es la tercera vez en esta migracion que el instrumento estaba configurado
para otro proceso: el deck de LVS con mim_option, netcheck con la pila hasta
met4, y ahora el DRC con metal_level. Las tres se ven igual -- el layout
parece roto y lo que esta mal es lo que mide. Por eso los tres se derivan
ahora del PDK en vez de escribirse.

Quedan 3 violaciones, una por condensador, y estan medidas: la pila de la
correa de VSS crea un pad de met4 al subir de met3 a met5, y queda a 0.85 um
de la placa inferior cuando MIMTM.1 pide 1.2. Faltan 0.35 um entre el banco
y el riel.

Estado: caja 40.73 x 24.14, las seis redes OK, DRC 3.
Migracion a opcion B terminada: DRC 0 y las seis redes.

Rails.minimum calculaba la separacion con la regla de la capa del riel, y
eso basta mientras lo unico que se le acerque sea metal de esa capa. No basta
cuando algo aterriza en el riel viniendo de arriba: la pila deja un pad en
cada capa que atraviesa, y uno de esos puede caer bajo una regla mas dura.

Aqui la correa de la placa inferior baja desde met5 y su pila deja un pad de
met4 justo bajo el banco. Contra la placa, que tambien es met4, manda MIM.1
con 1.2 um -- cuatro veces la separacion de met3, que era la que se estaba
usando. De ahi una violacion por condensador.

`clearance` es ahora un argumento de Rails.minimum, y build.py lo eleva a
MIM.1 cuando hay caps y su placa inferior no comparte capa con el riel.

    caja    40.73 x 25.95   (opcion A: 44.53 x 25.64)
    area    1057 um2 contra 1141: 7% menos
    DRC     0
    redes   las seis

La holgura cuesta 1.8 um de alto. Sale a cuenta igual, y la celda sigue
siendo mas pequeña que en opcion A pese a pagarla.
…tomatica

check.py pasaba rail_layer=RIEL, que sin la variable de entorno vale None y
pide la eleccion automatica. Esa mira la capa mas alta de la fila y sube un
piso: con el MIM en met4/met5 la mas alta es la placa superior y no queda
piso encima, asi que levanta ValueError y el barrido entero no arranca.

La regla es conservadora, no incorrecta -- los rieles corren por los extremos
y no cruzan el banco en ningun punto -- pero no lo sabe. Sin la variable no
se pasa rail_layer y vale el defecto del paquete, que es met3.

Barrido completo en opcion B, 14 casos:

    7 juegos de dimensiones a mano   DRC 0 en todos
    200 kHz        1  M5.2a
    300 kHz        0
    800 kHz        0
    2000 kHz       0
    umbral 2.0V   48  MIMTM.4
    Iex 5 nA       0
    carga 800fF    0

Topologia correcta en LOS CATORCE: las seis redes salen en todo el rango.

Las 48 de MIMTM.4 no son nuestras. El mimcap solo, sin nada alrededor, da 24
al tamaño que pide ese caso (5.047 um) y CERO a 5.0, 5.1, 5.5, 6.0, 6.5 y
7.0. La regla pide que el FuseTop solape la via 0.4 um, y #106 monta la
matriz con minus1=False -- "maximum via coverage" -- asi que la fila del
borde queda pegada al canto cuando la division del tamaño cae de cierta
forma. Antes de #106 se usaba minus1=True, que dejaba ese margen. Va aguas
arriba.

Queda una M5.2a, separacion de met5, en el caso de 200 kHz. Esa si es
nuestra.
Rails.above elegia la capa subiendo un piso sobre el bloque mas alto de la
fila. Con el MIM en met4/met5 el mas alto es la placa superior del mimcap,
met5, y no queda piso: levantaba ValueError y la eleccion automatica quedaba
inservible en opcion B.

La regla no puede aplicarse a un bloque que ya esta en la cima -- no hay
sitio al que subir -- asi que ese bloque no participa. Solo levanta si NINGUNO
deja capa libre.

Excluirlo traslada una responsabilidad al llamante: los rieles no pueden
cruzar ese bloque. En la celda LIF se cumple, los rieles corren por los
extremos y el banco esta dentro, y queda escrito en el docstring para que
quien reutilice esto lo sepa.

Y `clearance` llega ahora tambien por ese camino, via plan_bands. Sin eso el
automatico daba las 3 violaciones de MIMTM.1 que el forzado ya no tiene: la
holgura del riel se calculaba bien en una rama y no en la otra.

Los dos caminos coinciden: 40.73 x 25.95, riel met3, DRC 0, las seis redes.
Y coinciden porque la regla lo deduce, no porque el defecto este escrito dos
veces.
Con Cm grande el banco crece mas que la banda de nfet. El caso de 200 kHz
pide 511 fF, la placa superior sube hasta 10.52 y la pista de membrana caia
a 10.65: 0.13 um cuando M5.2a pide 0.28.

Son la misma red -- las dos son la membrana -- asi que no habia corto. Pero
la separacion se mide igual entre poligonos del mismo nodo, y el suelo de la
ventana de pistas salia solo de los anillos de los nfet. Ahora sale del mas
alto de los dos.

Es el mismo patron que ya aparecio en la holgura del riel y en la eleccion
de capa: una parte del floorplan que no sabe hasta donde llega el
condensador. Con el MIM en met2/met3 no importaba porque nunca era el bloque
mas alto; con el en met4/met5 lo es a menudo.

Barrido completo, 14 casos: 13 con DRC 0 y topologia correcta en los 14. La
que falla es umbral 2.0V con 48 MIMTM.4, y esas son de mimcap() -- la
primitiva sola da 24 al tamaño que pide ese caso.
El equipo sigue lo que dicta glayout, y glayout esta en opcion B desde el
PR 106. Con el MIM en met4/met5 la placa inferior ocupa met4 y la superior
met5, asi que tres comentarios que justificaban decisiones "para dejar met4
libre al ruteo entre neuronas" describian un plano de capas que ya no
existe.

met3 sigue siendo la capa de los rieles, pero por otro motivo: es la mas
alta que los transistores dejan libre, no un hueco que reservamos. Fuera de
la huella del banco met4 y met5 siguen disponibles para el ruteo entre
celdas -- el banco solo las ocupa donde esta.

Y el comentario sobre Rails.above ya no aplica: decia que la eleccion
automatica sube un piso sobre el mimcap y por eso habia que forzar met3.
Ahora el mimcap esta en la cima y no participa en esa regla, asi que la
eleccion automatica llega a met3 sola.
mim.py llevaba PAR_METALES = "m2m3" fijo. Con la celda construida en
met4/met5 eso emitiria cap_mim_2f0_m2m3_noshield: el VALOR seria correcto
--los coeficientes son identicos en las cuatro parejas, c_cox 1.99e-3 y
c_capsw 2.383e-10, porque el sandwich es el mismo nitruro y solo cambia a
que altura se inserta-- pero el netlist declararia un dispositivo de otro
apilamiento, y el LVS lo compararia contra una extraccion que no coincide.

Ahora par_metales() y drc_option() lo derivan de donde el PDK ponga capmet.
Con opcion B sale cap_mim_2f0_m4m5_noshield, que existe en el PDK.

Es la quinta capa escrita a mano que aparece en esta migracion, despues del
mim_option del deck de LVS, la pila de netcheck, el metal_level del DRC y la
holgura del riel. Todas se veian igual: algo parecia roto y lo que estaba
mal era lo que lo describia.

Y confirma que la migracion fue de layout y no de caracterizacion: Cm no se
recalcula, asi que las cinco leyes y C_in siguen en pie sin tocar nada.

Celda verificada tras el cambio: DRC 0 y las seis redes.
Era el hueco declarado del flujo desde hace semanas: el motor generaba
layout pero no esquematico, asi que no habia contra que comparar y el LVS
nunca se habia ejecutado sobre nuestra celda.

netlist.de_diseño() lo emite desde la misma NeuronDesign que produce el
layout. Eso cambia lo que significa un fallo de LVS: en un flujo a mano es
un DESCUBRIMIENTO -- dos artefactos independientes que pueden divergir --
y aqui es una COMPROBACION, porque las dos salidas vienen de la misma
descripcion. "Alguien tecleo distinto en dos sitios" deja de ser posible.

Y el LVS, la primera vez que corrio, encontro algo que netcheck no puede
ver: la marca de pin de Iin estaba en met3 escrita a mano. Con el MIM en
met4/met5 la placa superior es met5, asi que la marca caia sobre una capa
vacia; el extractor no encuentra conductor bajo el texto y la red sale sin
nombre. netcheck camina el metal y no mira etiquetas, asi que daba las seis
redes por buenas. Sexta capa escrita a mano de esta migracion.

Estado: el LVS ejecuta y falla con "extra top-level pin(s): Iin|spike_neg",
o sea que klayout ve las dos etiquetas sobre la MISMA red. netcheck dice que
estan separadas, y las etiquetas caen en capas distintas a 21 um una de
otra. Los dos instrumentos discrepan y hay que averiguar cual miente antes
de tocar el layout -- hoy ya me ha pasado cuatro veces que el roto fuera el
medidor.
LVS was failing on three counts, all of them the netlist describing
something slightly different from the GDS:

- the MIM device class. The deck extracts cap_mim_2f0fF, without the
  metal-pair suffix that the simulation model carries. modelo_lvs()
  now gives that name and mim.modelo() documents that it is not it.
- the drawn dimensions. The GDS is written on a 0.005 um grid and both
  the MIM plate and a FET channel are centred, so a 5.864 um plate comes
  out 5.87 and a 1.671 um M5 comes out 1.68. en_rejilla() rounds each
  dimension up to that grid before building, and handles['dims'] carries
  what was drawn so the netlist stops declaring what was asked for.
- the capacitor parameters. The gf180 reader translates W*L*M into area
  and (W+L)*M*2 into perimeter; c_width/c_length are ignored in silence
  and the capacitor enters with zero area.

Netlists match.
157 commits desde el 30 de julio. Sin conflictos: el equipo no ha tocado
designs/scripts/lif_design, que es todo lo nuestro.

Entra el submodulo designs/libs/gLayout, fijado por el equipo en 4662db4.
Ese pin es upstream main pelado y NO lleva el arreglo de MIMTM.4 en mimcap
ni la opcion de MIM del runner de LVS, asi que la celda no pasa DRC ni LVS
construida contra el. Se deja donde el equipo lo puso; moverlo es una
decision que hay que hablar con ellos.
La regla se escribio cuando designs/libs/gLayout era un clon manual para
probar. Desde que el equipo lo fijo como submodulo esa ruta si forma parte
del proyecto, y la regla dice lo contrario.
Todo lo escrito describia la opcion A, que es donde estabamos hasta la
migracion. Cuatro pasajes decian lo contrario de la realidad:

- el DRC pasaba `mim_option=A` a mano; ahora `check` lo deriva del PDK, y
  con el sale B: met4 / FuseTop / met5, contacto por via4. De paso se
  cuenta la segunda trampa del deck, el METAL_LEVEL a 6LM que hace que
  MIMTM.10 acuse al condensador de sus propias vias.
- netcheck modelaba via2 sobre FuseTop; es via4.
- "met4 queda entero libre para el ruteo" -- la placa inferior vive ahi.
- "esta celda no pasa LVS, no hay netlist de referencia" -- lo hay, y pasa.

Entra una seccion de LVS con los dos hallazgos que costaron: los dos
nombres del condensador (cap_mim_2f0fF al extraer, cap_mim_2f0_m4m5_noshield
al simular) y el ajuste a rejilla, que hacia que lo declarado no fuese lo
dibujado.

En "lo que falta", primero la pregunta que no tiene respuesta escrita en
ningun sitio: quien fija la opcion de MIM para la oblea. Las dos dibujan la
misma capa FuseTop, asi que el proceso solo puede tener una, y a nosotros B
nos llego por el defecto de mimcap() desde el PR #106 de glayout, no porque
nadie lo decidiera.

Salidas regeneradas: 14/14 casos con DRC 0.
…pio script

La rama por defecto seguia siendo capimagics-base, anterior a la migracion.
El mimcap de esa rama no crea los puertos de la extension, asi que
`_rails_bands` pide `bot_via_S_top_met_S` y el motor muere con un KeyError
en cuanto se construye una celda. Y como el script hace `reset --hard`,
deshacia cualquier checkout manual y devolvia el fallo.

Dos cosas mas que impedian que corriese tal cual:

- La comprobacion de arranque exigia `capmet == "fusetop"`. Desde el PR #106
  de glayout el valor correcto es CAP_MK -- es el marcador -- y el
  dielectrico lo define el FuseTop que dibuja el propio mimcap. La asercion
  abortaba por una condicion que ya no aplica. Ahora se comprueba el
  resultado: se construye un MIM y se mira que la capa (75,0) este dibujada,
  que vale para las dos opciones.

- `git fetch origin <rama>` no creaba `origin/<rama>`: el clon es --depth 1
  de una sola rama y su refspec no cubre las demas, asi que el commit se
  quedaba en FETCH_HEAD y el reset moria con "ambiguous argument". Con el
  destino escrito a mano la referencia existe siempre. Afectaba a cualquier
  rama distinta de la clonada, tambien via GLAYOUT_RAMA.

Corrido de punta a punta: las dos comprobaciones pasan y la celda se
construye, 40.73 x 25.95 um.
Un macro se integra por su LEF, que declara un origen. Si la geometria
vive en (0, -2.21) y el LEF dice (0, 0), la herramienta coloca el bloque
creyendo una cosa y el metal aparece desplazado: pistas que no conectan y
violaciones en el borde, y no al colocar sino despues de rutear.

glayout compone centrado en el origen, que es lo natural para disenar. La
convencion cambia cuando la celda deja de ser un diseno suelto y pasa a
ser un bloque que LibreLane coloca, y el equipo la acaba de fijar para
todas las celdas del D14_topcell.

Va despues de los rieles y antes de los puertos y las etiquetas. Las
referencias arrastran sus puertos al moverse, pero `rails_y` lleva
coordenadas absolutas en float que no se enteran, asi que se corrigen a
mano: sin eso las sondas de netcheck pinchan 2.21um por debajo de los
rieles y reportan redes rotas que no lo estan.

    antes  (0.000, -2.210) -> (40.735, 23.740)
    ahora  (0.000,  0.000) -> (40.735, 25.950)

Verificado: los 14 casos de la suite con DRC 0 y topologia ok, y el LVS
sigue en Netlists match.
Las dos vistas que le faltaban a la celda para entrar en LibreLane como un
bloque, siguiendo la convencion que el equipo acaba de fijar para todas las
celdas del D14_topcell.

El contorno va en la capa (0,0), que no se fabrica: es la marca que la
herramienta lee para saber donde acaba el bloque, porque no lo deduce
mirando el metal. Se dibuja despues de llevar la celda al origen, asi que
arranca en (0,0) y coincide al micrometro con lo que declarara el LEF --
al reves quedaria desplazado respecto al contenido.

El .lib es un stub: sin tiempos ni potencia, solo la declaracion de los
cinco pines con su direccion. Los nombres y las direcciones salen de la
misma fuente que el netlist de referencia, para que un pin no pueda
llamarse de una forma en el LVS y de otra en la integracion.

Lleva ademas `pg_type : primary_power` y `primary_ground` en Vdd y Vss,
sin lo cual la herramienta las trata como senales en vez de como red de
potencia.

Verificado: los 14 casos de la suite siguen con DRC 0 -- la capa (0,0) no
dispara ninguna regla -- y el contorno mide 0,0 -> 40.735,25.950, igual
que la celda.

PENDIENTE de hablar con el equipo: el anillo de PDN, que ellos han puesto
en neurona, STDP y current_mirror y aqui son rieles horizontales, y si sus
nombres avdd/avss tienen que sustituir a Vdd/Vss para que el top_module.v
conecte.
La celda que construye `lif_demo` heredaba `d`, que para ese punto ya habia
pasado por siete reasignaciones: la ultima, del barrido de c_in, la dejaba
en 1500 kHz con tope de capacidad de entrada. Asi que el dibujo era esa
celda -- un solo MIM, 982 um2 -- mientras el texto describe el ejemplo
canonico de 500 kHz, que lleva tres MIM y mide 1057.

No fallaba: dibujaba una celda correcta que no era la que decia el titulo,
y cualquier conclusion que se sacara de la imagen -- numero de
condensadores, area, disposicion -- quedaba mal sin que nada lo indicase.
Ahora resuelve su propio diseno en vez de heredarlo.

Imprime tambien la esquina inferior izquierda, para que el (0,0) se vea en
la salida y no haya que fiarse.

Salidas regeneradas con el codigo de hoy, que ya lleva el origen y el
contorno:

    caja: 40.73 x 25.95 um = 1057 um2
    esquina inferior izquierda: (0.000, 0.000)
    bloques: nfets 3, pfets 3, caps 3, m5 1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant