Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
ecca9b3
lif: caracterizacion parametros-comportamiento
carloscl03 Aug 2, 2026
5c420ea
lif: caracterizacion de W de M5
carloscl03 Aug 2, 2026
b8d0a21
lif: superficies 2D W_M5 x L_M5
carloscl03 Aug 2, 2026
eb23870
lif: limite de Cm en funcion de W y L de M5
carloscl03 Aug 2, 2026
1b04c45
lif: ley definitiva del limite de Cm (12 puntos, busqueda binaria)
carloscl03 Aug 2, 2026
7a53283
lif: documenta por que el ajuste global de Cm_min predice peor
carloscl03 Aug 2, 2026
e76cd14
lif: recaracteriza con paso 1ns, el barrido a 20ns tenia sesgo
carloscl03 Aug 2, 2026
7c2c3d2
lif: valida las leyes con 18 puntos fuera de la grilla de ajuste
carloscl03 Aug 2, 2026
182a0bb
lif: mide las fronteras de operacion en los extremos
carloscl03 Aug 2, 2026
5c3b7b7
lif: caracteriza el contrato con entrada de corriente
carloscl03 Aug 3, 2026
a6c086d
lif: motor de calculo del sistema de diseño
carloscl03 Aug 3, 2026
dfc7227
lif: cierra el lazo del sistema con verificacion por simulacion
carloscl03 Aug 3, 2026
8661cf4
lif: ordena tb/ y documenta cual testbench usar
carloscl03 Aug 3, 2026
b45409f
lif: reescribe la documentacion en ingles y reorganiza la knowledge base
carloscl03 Aug 3, 2026
89e0e40
lif: ejemplos de uso del motor y corrige el caso de una dimension fija
carloscl03 Aug 3, 2026
d3dd30c
lif: hace instalable el paquete de diseño
carloscl03 Aug 3, 2026
87d3bd7
lif: mueve el motor de diseño a la estructura de la plantilla
carloscl03 Aug 9, 2026
c44d1f9
lif: capa de espaciado entre bloques y lo medido sobre el layout
carloscl03 Aug 10, 2026
7ded382
motor: capas de placement y generacion, con rieles
carloscl03 Aug 10, 2026
af1fec5
motor: celda LIF en bandas, ruteada y verificada por extraccion
carloscl03 Aug 11, 2026
e543460
motor: conecta VDD/VSS por los anillos de guarda y cierra la celda
carloscl03 Aug 11, 2026
1dc93ba
motor: une la capa de caracterizacion con la de layout
carloscl03 Aug 11, 2026
69e21d3
notebook: recorrido del motor de la neurona, de la spec al layout
carloscl03 Aug 11, 2026
c3d0da7
check: descubre el deck de DRC desde glayout, no una ruta del contenedor
carloscl03 Aug 11, 2026
52a5bca
scripts: deja un contenedor listo para el motor sin conda ni pyenv
carloscl03 Aug 11, 2026
e451a2b
scripts: el setup deja tambien el kernel de jupyter listo
carloscl03 Aug 11, 2026
c6c4b95
scripts: aisla el kernel del PYTHONPATH de la imagen
carloscl03 Aug 11, 2026
bdf77b7
scripts: run_GL_tmp.sh, la instalacion que si funciona mientras no ha…
carloscl03 Aug 11, 2026
e7b79b6
run_GL_tmp: abre la ventana de jupyter en el escritorio del contenedor
carloscl03 Aug 11, 2026
365d3c9
run_GL_tmp: registra tambien gldev cuando no hay ninguno
carloscl03 Aug 11, 2026
3419590
run_GL_tmp: fuerza el clon a la rama en vez de intentar un checkout
carloscl03 Aug 13, 2026
4d67e30
run_GL_tmp: el bin del venv primero en el PATH del kernel, para que !…
carloscl03 Aug 13, 2026
76da6ea
motor: la celda pasa por el extractor, con etiquetas y sin vias sobre…
carloscl03 Aug 14, 2026
32439d2
check: LIF_RAIL_LAYER para correr el barrido con los rieles en otra capa
carloscl03 Aug 15, 2026
9210341
scripts: visor de gds con zoom por zona, filtro de capas y trazado de…
carloscl03 Aug 15, 2026
52e383b
build: from_design tambien tiene que aceptar la capa de los rieles
carloscl03 Aug 15, 2026
a156d63
notebook 3: reejecutado entero, el barrido enseñaba solo 3 de 10 casos
carloscl03 Aug 15, 2026
1d2d5b0
solver: avisar cuando la frecuencia pedida pasa del maximo medido
carloscl03 Aug 15, 2026
6cf4445
notebook 3: ejemplos de la flexibilidad del spec y de la fuente no ideal
carloscl03 Aug 15, 2026
ef55914
lif: bajar el ruteo entre bandas a met2/met3 y dejar met4 libre
carloscl03 Aug 16, 2026
1aa6d1b
place: la separacion se debe entre el metal, no entre contornos de bl…
carloscl03 Aug 16, 2026
a0340a2
gdsview: celda de nivel superior, met1 y met2 distinguibles, trazado …
carloscl03 Aug 16, 2026
166ceb6
lif: el salto entra por met1 al anillo, sin via propia que choque con…
carloscl03 Aug 16, 2026
0dc9a5a
notebook 3: lo que falta, al dia con met4 libre, el LVS que no hay y M6
carloscl03 Aug 16, 2026
279d13a
lif: rieles en met3 por defecto, met4 libre de serie
carloscl03 Aug 16, 2026
1b0d10c
lif: C_in como sexta ley, medida sobre la rampa de integracion
carloscl03 Aug 22, 2026
fced531
lif: cm -> Cm, para que la magnitud se escriba igual en todo el paquete
carloscl03 Aug 22, 2026
231ae51
lif: ampliar la robustez a rutas del solver que nadie ejercitaba
carloscl03 Aug 22, 2026
c3fec62
lif knowledge base: C_in, su validacion, y la excursion como pista ab…
carloscl03 Aug 22, 2026
fe6922b
lif: avance de la migracion a MIM opcion B (INCOMPLETO)
carloscl03 Aug 22, 2026
c870cb8
lif: netcheck no veia met5, y el margen del MIM creaba un corto
carloscl03 Aug 22, 2026
aefb58c
lif: la conexion al cap es directa, asi que la esquina no lleva pila
carloscl03 Aug 22, 2026
0d264f6
lif: pasar metal_level al deck; 177 de las 180 violaciones no existian
carloscl03 Aug 22, 2026
96257b1
lif: la holgura del riel la decide MIM.1, no la capa del riel. DRC 0
carloscl03 Aug 22, 2026
1f5332f
lif: el barrido usa el defecto de riel del paquete, no la eleccion au…
carloscl03 Aug 22, 2026
6340147
place: un bloque en la cima de la pila no decide la capa de los rieles
carloscl03 Aug 22, 2026
0d01d0e
lif: el suelo de las pistas lo marca tambien la placa superior
carloscl03 Aug 22, 2026
b229bc3
lif: los comentarios ya no dicen que met4 queda libre
carloscl03 Aug 22, 2026
d8764f5
lif: el nombre del modelo del MIM sale del PDK, no escrito a mano
carloscl03 Aug 22, 2026
9a76829
lif: emitir el netlist de referencia, y el LVS ya corre
carloscl03 Aug 22, 2026
1eeab31
lif: make the reference netlist say what the layout actually draws
carloscl03 Aug 22, 2026
1447bf8
merge main del equipo en la linea de opcion B
carloscl03 Aug 23, 2026
5db883e
gitignore: gLayout ya no es un clon local, es el submodulo del equipo
carloscl03 Aug 24, 2026
7f1b29f
notebook 3: al dia con la opcion B y con el LVS que ya existe
carloscl03 Aug 24, 2026
15c3d13
run_GL_tmp: apuntar a la rama con la opcion B, y dos arreglos del pro…
carloscl03 Aug 25, 2026
a923334
lif: la celda arranca en (0,0), no centrada en el origen
carloscl03 Aug 27, 2026
69fcd35
lif: contorno en la capa (0,0) y stub de Liberty para el macro
carloscl03 Aug 27, 2026
4a40bee
notebook 3: el dibujo era de otro diseno, y las salidas al dia
carloscl03 Aug 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,9 @@ tools/
context/

*.gds.backup

# paquetes instalados en modo editable (pip install -e)
*.egg-info/
build/
dist/

240 changes: 240 additions & 0 deletions designs/libs/tb_analog/tb_lif/fixture.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
"""Verificacion por simulacion: cierra el lazo diseño -> netlist -> medida.

En primera instancia el "return" del sistema es un netlist SPICE que se simula
para comprobar que el diseño cumple lo que las leyes predicen. Cuando gLayout
avance, el return sera el layout y esto quedara como verificacion previa.

Genera el netlist a partir de tb_charac_isrc.spice (entrada de corriente, sin
M6) sustituyendo W_M5, L_M5, Cm y W_M7M8.
"""
from __future__ import annotations

import re
import subprocess
from dataclasses import dataclass, field
from pathlib import Path

# lif_design lives in designs/scripts/; add it to the path so this fixture
# can be run straight from the testbench directory, like tb_ota_5t's.
import sys as _sys
from pathlib import Path as _Path
_sys.path.insert(0, str(_Path(__file__).resolve().parents[3] / "scripts"))

from lif_design import laws as L
from lif_design.spec import NeuronDesign

# .tran 1n es obligatorio: con 20n la frecuencia se infla +41% de media y hasta
# +193% (el integrador salta ciclos y los cuenta como disparos).
TSTEP_NS = 1

# El transitorio debe dar >= 5 ciclos. A frecuencias bajas 30u no basta: una
# neurona a 15 kHz tiene periodo 67us y no completa ni un ciclo.
SKIP_US = 20.0 # arranque que se descarta al medir


def _tstop_for(freq_khz: float, min_cycles: int = 8) -> float:
"""Transitorio [us] que deja min_cycles DESPUES del arranque.

Los primeros SKIP_US no cuentan (transitorio de encendido) y measure()
ademas descarta el primer periodo, asi que se piden 2 ciclos de mas.
"""
if freq_khz <= 0:
return 200.0
return max(30.0, SKIP_US + (min_cycles + 2) * 1000.0 / freq_khz)


@dataclass
class VerifyResult:
"""Lo medido en la simulacion, frente a lo predicho."""
ok: bool
measured: dict[str, float] = field(default_factory=dict)
predicted: dict[str, float] = field(default_factory=dict)
errors_pct: dict[str, float] = field(default_factory=dict)
status: str = ""
netlist: str = ""
raw_path: str = ""

def report(self) -> str:
lines = ["=" * 58,
f"VERIFICACION: {self.status}",
"=" * 58,
f"{'magnitud':<16}{'predicho':>11}{'medido':>11}{'error':>9}"]
for k in self.predicted:
p = self.predicted[k]
m = self.measured.get(k)
if m is None:
lines.append(f"{k:<16}{p:>11.3f}{'--':>11}{'--':>9}")
continue
e = self.errors_pct.get(k, 0.0)
lines.append(f"{k:<16}{p:>11.3f}{m:>11.3f}{e:>8.1f}%")
return "\n".join(lines)


def build_netlist(params: dict[str, float], iex_na: float,
template: str | Path, tstop_us: float | None = None,
freq_hint_khz: float = 0.0) -> str:
"""Netlist SPICE del diseño, listo para ngspice -b."""
text = Path(template).read_text(encoding="utf-8", errors="ignore")
W = params["W_M5"]
Lg = params["L_M5"]
Cm = params["Cm"]
w78 = params.get("W_M7M8", 0.22)

if tstop_us is None:
tstop_us = _tstop_for(freq_hint_khz)

text = re.sub(r"^IEX 0 iin DC [\d.]+n", f"IEX 0 iin DC {iex_na:g}n",
text, flags=re.M)
text = text.replace("L=50u W=1.25u", f"L={Lg:g}u W={W:g}u")
text = re.sub(r"^C1 integration Vss [\d.]+f",
f"C1 integration Vss {Cm:g}f", text, flags=re.M)
# buffer de salida (M7/M8), solo si se pidio distinto del minimo
if abs(w78 - 0.22) > 1e-9:
text = text.replace(
"XM7 spike spike_neg Vdd Vdd pfet_03v3 L=0.28u W=0.22u",
f"XM7 spike spike_neg Vdd Vdd pfet_03v3 L=0.28u W={w78:g}u")
text = text.replace(
"XM8 spike spike_neg GND GND nfet_03v3 L=0.28u W=0.22u",
f"XM8 spike spike_neg GND GND nfet_03v3 L=0.28u W={w78:g}u")
text = re.sub(r"^\.tran [\d.]+n [\d.]+u",
f".tran {TSTEP_NS}n {tstop_us:g}u", text, flags=re.M)
return text


# --- medida sobre el .raw --------------------------------------------------
def _read_raw(path: str | Path) -> tuple[list[float], list[float], list[float]]:
"""(t, v_spike, v_membrana) de un .raw binario de ngspice.

Sin numpy a proposito: son ~3 columnas y struct.unpack basta. El paquete
de diseño no arrastra dependencias.
"""
import struct

data = Path(path).read_bytes()
marker = b"Binary:\n"
k = data.find(marker)
if k < 0:
raise ValueError("el .raw no tiene seccion Binary")
header = data[:k].decode("ascii", "ignore")
npts = int([x for x in header.splitlines()
if "No. Points" in x][0].split(":")[1])
nvar = int([x for x in header.splitlines()
if "No. Variables" in x][0].split(":")[1])
body = data[k + len(marker):]
vals = struct.unpack(f"<{npts * nvar}d", body[:npts * nvar * 8])
t = list(vals[0::nvar])
sp = list(vals[1::nvar])
vm = list(vals[2::nvar]) if nvar > 2 else []
return t, sp, vm


def measure(raw_path: str | Path, skip_us: float = SKIP_US) -> dict[str, float]:
"""Frecuencia, Vth, swing y jitter desde el .raw."""
t, sp, vm = _read_raw(raw_path)
i0 = next((i for i, x in enumerate(t) if x > skip_us * 1e-6), 0)
t, sp = t[i0:], sp[i0:]
vm = vm[i0:] if vm else []
if len(t) < 10:
return {"n_cyc": 0}

# flancos de subida del spike, con umbral a media escala
hi, lo = max(sp), min(sp)
mid = (hi + lo) / 2.0
edges = [i for i in range(1, len(sp))
if sp[i - 1] <= mid < sp[i]]
periods = [t[edges[i + 1]] - t[edges[i]] for i in range(len(edges) - 1)]
periods = [p for p in periods if p > 0.2e-6]
if len(periods) > 1:
periods = periods[1:] # descartar el primero, aun transitorio
out: dict[str, float] = {"n_cyc": float(len(periods))}
if periods:
mean = sum(periods) / len(periods)
out["f"] = 1.0 / mean / 1e3 # kHz
if len(periods) > 1:
var = sum((p - mean) ** 2 for p in periods) / len(periods)
out["jitter_pct"] = 100.0 * (var ** 0.5) / mean
else:
out["jitter_pct"] = 0.0
if vm:
out["Vth"] = max(vm)
out["Vm_min"] = min(vm)
out["swing"] = max(vm) - min(vm)
out["v_out_swing"] = hi - lo
return out


# --- verificacion completa -------------------------------------------------
def verify(design_result: NeuronDesign, iex_na: float = L.IEX_REF,
workdir: str | Path = ".", template: str | Path | None = None,
ngspice: str = "ngspice", timeout_s: int = 900,
keep_files: bool = True) -> VerifyResult:
"""Simula el diseño y compara lo medido con lo predicho.

workdir debe ser la carpeta del testbench (tb/), porque el netlist usa
rutas relativas para el .raw.
"""
workdir = Path(workdir)
if template is None:
template = workdir / "tb_lif.spice"

p = design_result.params
W, Lg, Cm = p["W_M5"], p["L_M5"], p["Cm"]
f_pred = L.freq(W, Lg, iex_na)

tag = f"verify_{W:g}_{Lg:g}_{Cm:g}_{iex_na:g}".replace(".", "p")
raw_rel = f"{tag}.raw"
netlist = build_netlist(p, iex_na, template, freq_hint_khz=f_pred)
netlist = netlist.replace("tb_charac_isrc.raw", raw_rel)

spice_path = workdir / f"{tag}.spice"
spice_path.write_text(netlist, encoding="utf-8", newline="\n")

res = VerifyResult(ok=False, netlist=netlist,
raw_path=str(workdir / raw_rel))
res.predicted = {
"f": round(f_pred, 1),
"Vth": round(L.vth(W, Lg, Cm), 3),
"swing": round(L.swing(W, Lg, Cm), 3),
}

try:
subprocess.run([ngspice, "-b", spice_path.name], cwd=str(workdir),
capture_output=True, timeout=timeout_s, check=False)
except FileNotFoundError:
res.status = f"ngspice no encontrado ({ngspice})"
return res
except subprocess.TimeoutExpired:
res.status = f"la simulacion supero {timeout_s} s"
return res

raw_file = workdir / raw_rel
if not raw_file.exists():
res.status = "la simulacion no genero .raw"
return res

res.measured = measure(raw_file)
if not keep_files:
spice_path.unlink(missing_ok=True)
raw_file.unlink(missing_ok=True)

if res.measured.get("n_cyc", 0) < 3:
res.status = "NO OSCILA (menos de 3 ciclos)"
return res

for k, pv in res.predicted.items():
mv = res.measured.get(k)
if mv:
res.errors_pct[k] = 100.0 * (pv - mv) / mv

worst = max((abs(v) for v in res.errors_pct.values()), default=0.0)
vmin = res.measured.get("Vm_min", 0.0)
if vmin < -0.05:
res.status = f"ANOMALO: la membrana baja a {vmin:.3f} V"
elif res.measured.get("jitter_pct", 0) > 2.0:
res.status = f"NO CONVERGIO (jitter {res.measured['jitter_pct']:.1f}%)"
elif worst <= 10.0:
res.ok = True
res.status = f"OK (peor error {worst:.1f}%)"
else:
res.status = f"DESVIACION ALTA (peor error {worst:.1f}%)"
return res
120 changes: 120 additions & 0 deletions designs/libs/tb_analog/tb_lif/sweep_m5_switch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"""Recaracterizacion con M5 como interruptor.

Las leyes actuales se ajustaron con M5 largo (L=50um), donde la carga que el
interruptor inyecta al abrirse domina el umbral: Vth = 1.2792 + Q/Cm con
Q proporcional a W*L. Eso convierte a M5 en el mando de frecuencia y obliga a
canales de 20-50um, que en layout son una barra de 24-54um de ancho.

Con M5 al minimo la carga inyectada cae de ~114 fC a ~0.4 fC, el umbral
colapsa al punto de conmutacion del inversor y la frecuencia deberia quedar

f = Iex / (Cm * dV) con dV fijo

o sea proporcional a Iex e inversa a Cm, sin que W ni L intervengan. Este
barrido comprueba exactamente eso, y de paso mide cuanta frecuencia mueve M5
todavia -- si mueve poco, deja de ser variable de diseño.

Uso: python sweep_m5_switch.py [salida.csv]
"""
from __future__ import annotations

import subprocess
import sys
import tempfile
from pathlib import Path

import fixture as fx

AQUI = Path(__file__).resolve().parent
PLANTILLA = AQUI / "tb_lif.spice"

# dV esperado si la inyeccion es despreciable: el punto de conmutacion del
# inversor, que ya sale como termino independiente de la ley de Vth.
DV_ESPERADO = 1.2792

M5_MIN = (0.22, 0.28) # W, L en um -- minimo construible (dogbone)
M5_ABE = (10.0, 0.28) # lo que puso Abrahan
M5_VIEJO = (1.25, 50.0) # el del barrido original

# Malla principal: M5 minimo, se mueven Cm e Iex. La corriente sube con Cm
# para que el periodo no se dispare y el transitorio quede acotado.
MALLA = [(M5_MIN, Cm, iex)
for Cm in (50.0, 100.0, 150.0, 300.0)
for iex in (100.0, 200.0, 400.0)]

# Control del mecanismo: mismo Cm e Iex, tres tamaños de M5. Si la frecuencia
# apenas se mueve entre el minimo y el de Abrahan, la inyeccion ya no manda.
CONTROL = [(m5, 150.0, 100.0) for m5 in (M5_MIN, M5_ABE, M5_VIEJO)]


def periodo_esperado_us(cm_ff: float, iex_na: float) -> float:
"""T = Cm*dV/Iex, en microsegundos."""
return cm_ff * 1e-15 * DV_ESPERADO / (iex_na * 1e-9) * 1e6


def corre(m5, Cm, iex, etiqueta):
w, l = m5
t_per = periodo_esperado_us(Cm, iex)
# 6 periodos utiles mas el arranque que measure() descarta
tstop = max(8.0, fx.SKIP_US + 8 * t_per)
params = {"W_M5": w, "L_M5": l, "Cm": Cm}
netlist = fx.build_netlist(params, iex, PLANTILLA, tstop_us=tstop)

with tempfile.TemporaryDirectory() as d:
deck = Path(d) / "tb.spice"
raw = Path(d) / "tb_charac_isrc.raw"
deck.write_text(netlist, encoding="utf-8")
proc = subprocess.run(["ngspice", "-b", str(deck)], cwd=d,
capture_output=True, text=True, timeout=7200)
if not raw.exists():
cola = (proc.stderr or proc.stdout or "").strip().splitlines()[-1:]
return {"error": " ".join(cola) or "sin raw"}
m = fx.measure(str(raw))

f_ideal = iex / (Cm * DV_ESPERADO) * 1e3 # kHz
m.update({"W_M5": w, "L_M5": l, "Cm": Cm, "iex": iex,
"tstop_us": tstop, "f_ideal": f_ideal, "etiqueta": etiqueta})
return m


def linea(r):
if "error" in r:
return ("%-9s W=%-5s L=%-5s Cm=%-6s Iex=%-5s FALLO: %s"
% (r.get("etiqueta", ""), r.get("W_M5"), r.get("L_M5"),
r.get("Cm"), r.get("iex"), r["error"][:60]))
f = r.get("f", 0.0)
err = 100 * (f - r["f_ideal"]) / r["f_ideal"] if r["f_ideal"] else 0.0
return ("%-9s W=%-5.2f L=%-5.2f Cm=%-6.0f Iex=%-5.0f f=%8.1f "
"(ideal %8.1f, %+6.1f%%) Vth=%5.2f swing=%5.2f ciclos=%2.0f"
% (r["etiqueta"], r["W_M5"], r["L_M5"], r["Cm"], r["iex"],
f, r["f_ideal"], err, r.get("Vth", 0.0), r.get("swing", 0.0),
r.get("n_cyc", 0)))


def main(destino=None):
filas = []
print("CONTROL DEL MECANISMO -- mismo Cm e Iex, distinto M5")
for m5, Cm, iex in CONTROL:
r = corre(m5, Cm, iex, "control")
filas.append(r)
print(" " + linea(r), flush=True)

print("\nMALLA -- M5 minimo, se mueven Cm e Iex")
for m5, Cm, iex in MALLA:
r = corre(m5, Cm, iex, "malla")
filas.append(r)
print(" " + linea(r), flush=True)

if destino:
campos = ["etiqueta", "W_M5", "L_M5", "Cm", "iex", "f", "f_ideal",
"Vth", "Vm_min", "swing", "jitter_pct", "n_cyc", "tstop_us"]
with open(destino, "w", encoding="utf-8") as fh:
fh.write(",".join(campos) + "\n")
for r in filas:
fh.write(",".join(str(r.get(c, "")) for c in campos) + "\n")
print(f"\nescrito {destino}")
return filas


if __name__ == "__main__":
main(sys.argv[1] if len(sys.argv) > 1 else None)
Loading