Objetivos de aprendizaje
Explicar cómo el análisis nodal modificado (MNA) convierte una lista de componentes en un sistema lineal.
«Estampar» resistencias, fuentes de corriente y fuentes de tensión en la matriz del sistema.
Programar un resolvedor de continua de unas cincuenta líneas y validarlo con circuitos resueltos a mano.
Usar el resolvedor en un circuito grande generado por código.
Teoría: Volumen I, capítulo 6 (análisis nodal modificado) y Anexo B (Miguel-Medina, 2026). Tiempo: ~2 h
import sys
if "google.colab" in sys.modules:
%pip install -q git+https://github.com/telmomm/math-tools-biomedical-electronicsimport matplotlib.pyplot as plt
import numpy as np
from circuitmath.plotting import use_style
use_style()Idea clave¶
En B1 y B2 escribiste las matrices mirando el circuito. Un simulador no puede mirar: recibe una lista de componentes y tiene que construir el sistema él solo. El método que usan SPICE y sus derivados es el análisis nodal modificado (MNA) Ho et al., 1975, y se apoya en dos ideas:
Cada componente aporta su parte por separado. Una resistencia entre dos nodos suma siempre los mismos cuatro números en las mismas cuatro posiciones de la matriz, esté donde esté. A esa aportación se la llama sello (stamp), y montar la matriz es recorrer la lista estampando.
Las fuentes de tensión añaden una incógnita y una ecuación. Su corriente no se puede escribir en función de las tensiones de nodo, así que se añade como incógnita; a cambio, la fuente aporta la ecuación .
El sistema resultante tiene la estructura por bloques del capítulo 6:
con las tensiones de nodo y las corrientes de las fuentes de tensión. En este cuaderno lo vas a programar.
La netlist¶
Un circuito se describe con una línea por componente: nombre, los dos nodos a los que se conecta y su valor. La primera letra del nombre dice qué es (R, I o V) y el nodo 0 es la referencia. Es el formato de SPICE, simplificado.
El primer circuito es el ejemplo de análisis nodal modificado del capítulo 6 del Volumen I: una fuente de 10 V y dos resistencias de 5 Ω en serie.
EJEMPLO_LIBRO = """
* Volumen I, capítulo 6: ejemplo de análisis nodal modificado
V1 1 0 10 ; fuente de tensión, con el + en el nodo 1
R1 1 2 5
R2 2 0 5
"""1. Predice¶
¿Cuántas incógnitas tendrá el sistema MNA de este circuito? ¿Cuáles son?
La fuente entrega 1 A al circuito. ¿Qué signo esperas para la incógnita «corriente de la fuente» si se define como la que entra por el terminal + y atraviesa la fuente hacia el −?
El circuito del cuaderno B1 tenía tres nodos y una fuente de corriente. ¿De qué tamaño será su matriz MNA?
Tu predicción:
2. Ejecuta y explora¶
2.1 Leer la netlist¶
El primer paso no tiene nada de circuitos: convertir el texto en una lista de datos. Se ignoran las líneas vacías, las que empiezan por * y lo que haya detrás de un ;.
def leer_netlist(texto):
"""Convierte una netlist en una lista de tuplas (nombre, nodo+, nodo-, valor)."""
elementos = []
for linea in texto.splitlines():
linea = linea.split(";")[0].strip()
if not linea or linea.startswith("*"):
continue
nombre, nodo_mas, nodo_menos, valor = linea.split()
elementos.append((nombre, nodo_mas, nodo_menos, float(valor)))
return elementos
leer_netlist(EJEMPLO_LIBRO)[('V1', '1', '0', 10.0), ('R1', '1', '2', 5.0), ('R2', '2', '0', 5.0)]2.2 Numerar las incógnitas¶
Cada nodo y cada fuente de tensión necesitan una posición en el vector de incógnitas. Reservamos el índice 0 para el nodo de referencia; enseguida verás por qué conviene incluirlo.
def numerar(elementos):
"""Asigna un índice a cada nodo (el 0 es la referencia) y a cada fuente de tensión."""
indice = {"0": 0}
for _, nodo_mas, nodo_menos, _ in elementos:
for nodo in (nodo_mas, nodo_menos):
if nodo not in indice:
indice[nodo] = len(indice)
for nombre, *_ in elementos:
if nombre[0] == "V":
indice[nombre] = len(indice)
return indice
numerar(leer_netlist(EJEMPLO_LIBRO)){'0': 0, '1': 1, '2': 2, 'V1': 3}2.3 El sello de una resistencia¶
Una resistencia de conductancia entre los nodos y lleva una corriente que sale de y entra en . En la ecuación de KCL del nodo aparece , y en la del nodo , . Son cuatro sumas:
Es la regla de inspección de B1, aplicada componente a componente.
def estampar_resistencia(A, a, b, R):
"""Suma el sello de una resistencia entre los nodos de índices a y b."""
g = 1 / R
A[a, a] += g
A[b, b] += g
A[a, b] -= g
A[b, a] -= g
A = np.zeros((3, 3))
estampar_resistencia(A, 1, 2, 5) # R1, entre los nodos 1 y 2
print("Después de R1:")
print(A)
estampar_resistencia(A, 2, 0, 5) # R2, entre el nodo 2 y la referencia
print("Después de R2:")
print(A)Después de R1:
[[ 0. 0. 0. ]
[ 0. 0.2 -0.2]
[ 0. -0.2 0.2]]
Después de R2:
[[ 0.2 0. -0.2]
[ 0. 0.2 -0.2]
[-0.2 -0.2 0.4]]
Fíjate en que los sellos se suman: el elemento acumula las dos resistencias conectadas al nodo 2. Por eso el orden de los componentes en la netlist no importa.
2.4 Los sellos de las fuentes¶
Una fuente de corriente no toca la matriz: solo el segundo miembro. Seguimos el convenio de SPICE: la corriente va del primer nodo al segundo por dentro de la fuente, de modo que sale del primer nodo y se inyecta en el segundo.
Una fuente de tensión añade una incógnita, su corriente , en la posición . Esa corriente sale del nodo + y entra en el nodo − (columna ), y la fuente aporta su propia ecuación, (fila ).
def estampar_fuente_corriente(z, a, b, valor):
"""La fuente saca corriente del nodo a y la inyecta en el nodo b."""
z[a] -= valor
z[b] += valor
def estampar_fuente_tension(A, z, a, b, k, valor):
"""Fuente de tensión con el + en a: una incógnita nueva (k) y una ecuación nueva."""
A[a, k] += 1 # su corriente sale del nodo a...
A[b, k] -= 1 # ...y entra en el nodo b
A[k, a] += 1 # ecuación de la fuente: v_a - v_b = valor
A[k, b] -= 1
z[k] = valor2.5 Montar el sistema¶
Con los tres sellos, montar el sistema es un bucle sobre la lista de componentes.
def montar(elementos):
"""Construye el sistema MNA, incluidas la fila y la columna del nodo de referencia."""
indice = numerar(elementos)
A = np.zeros((len(indice), len(indice)))
z = np.zeros(len(indice))
for nombre, nodo_mas, nodo_menos, valor in elementos:
a, b = indice[nodo_mas], indice[nodo_menos]
if nombre[0] == "R":
estampar_resistencia(A, a, b, valor)
elif nombre[0] == "I":
estampar_fuente_corriente(z, a, b, valor)
elif nombre[0] == "V":
estampar_fuente_tension(A, z, a, b, indice[nombre], valor)
else:
raise ValueError(f"Elemento desconocido: {nombre}")
return A, z, indice
A, z, indice = montar(leer_netlist(EJEMPLO_LIBRO))
print("Incógnitas:", list(indice))
print(A)
print("Segundo miembro:", z)Incógnitas: ['0', '1', '2', 'V1']
[[ 0.2 0. -0.2 -1. ]
[ 0. 0.2 -0.2 1. ]
[-0.2 -0.2 0.4 0. ]
[-1. 1. 0. 0. ]]
Segundo miembro: [ 0. 0. 0. 10.]
La matriz tiene cuatro filas: la primera corresponde al nodo de referencia y la última, a la fuente. Suma las tres primeras filas, las de los nodos (incluida la referencia): el resultado es cero en todas las columnas. Es la misma situación del apartado 2.5 de B1: la ecuación de KCL del nodo de referencia no aporta información nueva (es la suma de las demás cambiada de signo), y las tensiones solo están definidas salvo una constante.
La solución es la de siempre: fijar y descartar esa ecuación, es decir, eliminar la fila y la columna de la referencia. Haberlas incluido hasta ahora tiene una ventaja: los sellos no necesitan tratar la referencia como un caso especial.
print(A[1:, 1:])
print("Segundo miembro:", z[1:])[[ 0.2 -0.2 1. ]
[-0.2 0.4 0. ]
[ 1. 0. 0. ]]
Segundo miembro: [ 0. 0. 10.]
Esta es exactamente la matriz del ejemplo del libro, con sus bloques (arriba a la izquierda), y (la columna y la fila de la fuente) y .
2.6 Resolver¶
Solo queda resolver el sistema y devolver cada resultado con su nombre.
def resolver_dc(texto):
"""Resuelve una netlist. Devuelve las tensiones de nodo y las corrientes de las fuentes V."""
A, z, indice = montar(leer_netlist(texto))
x = np.zeros(len(indice))
x[1:] = np.linalg.solve(A[1:, 1:], z[1:])
return {nombre: float(x[k]) for nombre, k in indice.items()}
resolver_dc(EJEMPLO_LIBRO){'0': 0.0, '1': 10.0, '2': 5.0, 'V1': -1.0}V, V y A, como en el libro. El signo negativo indica que la corriente real sale por el terminal + de la fuente: la fuente entrega potencia.
Cuenta las líneas de leer_netlist, numerar, los tres sellos, montar y resolver_dc: tu simulador ocupa unas cincuenta.
2.7 Validación¶
Un programa que acierta un ejemplo no está validado. Lo probamos con los circuitos de los cuadernos anteriores, cuyos resultados ya conoces.
CIRCUITO_B1 = """
I1 0 1 2e-3 ; inyecta 2 mA en el nodo 1
R1 1 0 1e3
R2 1 2 2.2e3
R3 2 0 4.7e3
R4 2 3 1e3
R5 3 0 3.3e3
"""
PUENTE_B2 = """
V1 s 0 5
R1 s a 1e3
R2 a 0 1e3
R3 s b 1e3
R4 b 0 1.01e3 ; sensor con x = 1 %
Rd a b 10e3
"""
b1 = resolver_dc(CIRCUITO_B1)
print("B1, tensiones (V):", [round(b1[nodo], 4) for nodo in ("1", "2", "3")])
print(" esperado: [1.6327, 0.8247, 0.6329]")
b2 = resolver_dc(PUENTE_B2)
print(f"B2, salida del puente: {(b2['b'] - b2['a']) * 1e3:.4f} mV (esperado: 11.3045 mV)")B1, tensiones (V): [1.6327, 0.8247, 0.6329]
esperado: [1.6327, 0.8247, 0.6329]
B2, salida del puente: 11.3045 mV (esperado: 11.3045 mV)
Los nodos pueden tener nombres en lugar de números: para el programa son solo etiquetas.
El caso que complica el análisis a mano es una fuente de tensión entre dos nodos que no son la referencia: obliga a plantear un supernodo (capítulo 6). Para el MNA no es un caso especial.
SUPERNODO = """
I1 0 1 1 ; 1 A hacia el nodo 1
V1 1 2 2 ; fuente flotante de 2 V entre los nodos 1 y 2
R1 1 0 1
R2 2 0 1
"""
s = resolver_dc(SUPERNODO)
print(s)
print("v1 - v2 =", s["1"] - s["2"], "V"){'0': 0.0, '1': 1.5, '2': -0.5, 'V1': -0.5}
v1 - v2 = 2.0 V
2.8 Hilo biomédico: un cable con fugas¶
La ventaja de un resolvedor que lee netlists es que el circuito lo puede escribir otro programa. Un ejemplo clásico de la bioingeniería es el modelo de cable de una fibra nerviosa en reposo: el interior de la fibra conduce a lo largo (resistencia axial por tramo) y la membrana deja escapar corriente hacia el exterior (resistencia de membrana por tramo). Es una escalera de muchas secciones iguales.
Si se fija una tensión en un extremo, ¿hasta dónde llega? La teoría del cable predice una caída aproximadamente exponencial, , con una constante de espacio de tramos. Los valores siguientes son ilustrativos.
def netlist_cable(N, r_axial, r_membrana, v_inicio):
"""Netlist de una escalera de N tramos con una fuente de tensión en el nodo 1."""
lineas = [f"V1 1 0 {v_inicio}"]
for k in range(1, N + 1):
lineas.append(f"Ra{k} {k} {k + 1} {r_axial}")
lineas.append(f"Rm{k} {k + 1} 0 {r_membrana}")
return "\n".join(lineas)
N = 40 # número de tramos <- cámbialo
R_AXIAL = 1e6 # ohmios por tramo <- cámbialo
R_MEMBRANA = 25e6 # ohmios por tramo <- cámbialo
V0 = 10e-3 # voltios
cable = netlist_cable(N, R_AXIAL, R_MEMBRANA, V0)
print("\n".join(cable.splitlines()[:5]))
print(f"... ({len(cable.splitlines())} componentes)")V1 1 0 0.01
Ra1 1 2 1000000.0
Rm1 2 0 25000000.0
Ra2 2 3 1000000.0
Rm2 3 0 25000000.0
... (81 componentes)
solucion = resolver_dc(cable)
tramo = np.arange(N + 1)
v_cable = np.array([solucion[str(k + 1)] for k in tramo])
lambda_teorica = np.sqrt(R_MEMBRANA / R_AXIAL)
fig, ax = plt.subplots()
ax.semilogy(tramo, v_cable * 1e3, "o", markersize=4, label="mini-SPICE")
ax.semilogy(
tramo,
V0 * np.exp(-tramo / lambda_teorica) * 1e3,
"k--",
linewidth=1,
label=f"$V_0\\,e^{{-k/\\lambda}}$, λ = {lambda_teorica:.0f} tramos",
)
ax.set(xlabel="Tramo, $k$", ylabel="Tensión (mV)", title="Caída de tensión a lo largo del cable")
ax.legend()
plt.show()
Has resuelto un sistema de 42 incógnitas sin escribir una sola ecuación. En escala logarítmica una exponencial es una recta, y los puntos la siguen salvo en los últimos tramos. La tensión cae a un 37 % () cada tramos: una señal que se propagara solo así no llegaría lejos. La mielina, que aumenta la resistencia de membrana, es una de las soluciones de la naturaleza para alargar .
2.9 Así ve SPICE tu circuito¶
¿Qué aspecto tiene la matriz de un circuito grande? plt.spy dibuja un punto en cada elemento distinto de cero.
A, z, indice = montar(leer_netlist(cable))
A_reducida = A[1:, 1:]
fig, ax = plt.subplots(figsize=(4.5, 4.5))
ax.spy(A_reducida, markersize=3)
ax.grid(False)
ax.set_title("Elementos no nulos de la matriz MNA del cable")
plt.show()
no_nulos = np.count_nonzero(A_reducida)
print(f"Tamaño: {A_reducida.shape}, elementos no nulos: {no_nulos} de {A_reducida.size}")
Tamaño: (42, 42), elementos no nulos: 123 de 1764
Casi toda la matriz está vacía: cada nodo solo está conectado con sus vecinos. La banda diagonal es el bloque , y los dos puntos sueltos del borde son la fuente de tensión ( y ). Las matrices de los circuitos reales son así, dispersas, y los simuladores profesionales lo aprovechan para resolver circuitos con millones de nodos.
2.10 Comparación con la solución de referencia¶
El paquete circuitmath de este módulo incluye una implementación de referencia del mismo algoritmo. Sirve para contrastar la tuya.
from circuitmath import mna
tensiones_ref, corrientes_ref = mna.solve_dc(cable)
mias = [solucion[nodo] for nodo in tensiones_ref]
mismas_tensiones = np.allclose(mias, list(tensiones_ref.values()))
print("¿Mismas tensiones que la referencia?", mismas_tensiones)
print("Corriente de la fuente:", solucion["V1"], "A; referencia:", corrientes_ref["V1"], "A")¿Mismas tensiones que la referencia? True
Corriente de la fuente: -1.8099747437514538e-09 A; referencia: -1.8099747437514538e-09 A
3. Explica¶
¿Por qué una fuente de tensión añade una fila y una columna a la matriz? ¿Qué representa cada una?
Al montar la matriz con la fila del nodo de referencia, las filas de los nodos suman cero. ¿Qué ley física expresa ese hecho?
En el cable, la tensión de los últimos tramos queda por encima de la exponencial teórica. La fórmula supone un cable infinito; ¿por qué un cable que termina se comporta así? Aumenta
Ny observa.¿Qué ocurre si en la netlist hay dos fuentes de tensión distintas en paralelo? ¿Y si hay un nodo conectado a un solo componente que no es la referencia? Pruébalo y relaciona el mensaje de error con lo aprendido en B1.
La matriz MNA del ejemplo del libro es simétrica, pero tiene un cero en la diagonal. ¿Sigue siendo de diagonal dominante? ¿Qué propiedad de las de B1 se ha perdido al añadir la fuente de tensión?
4. Ejercicios¶
Básico. Escribe la netlist de la escalera del cuaderno B2 ( V; kΩ; kΩ, kΩ, kΩ) y comprueba que obtienes las tensiones de entonces (5,6732 V, 3,9252 V y 3,0124 V). ¿Con qué corriente de malla de B2 coincide la corriente de la fuente, y con qué signo?
Aplicación. Mejora tu simulador en dos pasos:
Haz que
leer_netlistacepte los sufijos de SPICE (k,meg,m,u,n), de modo que se pueda escribirR1 1 2 4.7k.Escribe una función que, a partir de la solución, calcule la corriente y la potencia de cada resistencia, y comprueba que la potencia total disipada coincide con la que entregan las fuentes.
Reto biomédico. En una enfermedad desmielinizante, un tramo de la fibra pierde su aislamiento y la resistencia de membrana cae.
Modifica
netlist_cablepara que acepte una lista con la resistencia de membrana de cada tramo.Simula una fibra de 60 tramos en la que los tramos 20 a 30 tienen una resistencia de membrana diez veces menor y compara la caída de tensión con la de la fibra sana.
¿Cuánto se reduce la tensión que llega al tramo 40? ¿Importa en qué posición esté la lesión?
Para ir más lejos. El capítulo 6 del Volumen I explica cómo se estampa una fuente de tensión controlada por tensión (VCVS). Añade a tu simulador un elemento
Econ cuatro nodos y una ganancia, y úsalo para modelar un amplificador de ganancia 100 conectado a la salida del puente de Wheatstone.
Resumen¶
El MNA construye el sistema recorriendo la lista de componentes: cada uno estampa su aportación en posiciones fijas, sin mirar el resto del circuito.
Una resistencia suma cuatro términos en la matriz; una fuente de corriente, dos en el segundo miembro; una fuente de tensión añade una incógnita y una ecuación.
Eliminar la fila y la columna del nodo de referencia es lo que hace que el sistema tenga solución única.
Un simulador de continua cabe en cincuenta líneas, y sus matrices son dispersas: casi todo son ceros.
→ Siguiente: B4 · Matrices complejas: análisis en alterna
- Miguel-Medina, T. (2026). Electrónica en Ingeniería Biomédica — Volumen I. Autopublicación. 10.5281/zenodo.22913382
- Ho, C.-W., Ruehli, A. E., & Brennan, P. A. (1975). The modified nodal approach to network analysis. IEEE Transactions on Circuits and Systems, 22(6), 504–509. 10.1109/TCS.1975.1084079