Objetivos de aprendizaje
Extender el análisis nodal modificado al régimen sinusoidal sustituyendo las conductancias por admitancias complejas.
Hacer un barrido en frecuencia resolviendo un sistema lineal por cada frecuencia.
Explicar por qué varias etapas RC en cascada no se comportan como el producto de las etapas aisladas.
Analizar un puente de alterna y justificar su doble condición de equilibrio.
Teoría: Volumen I, Anexo A (fasores e impedancia compleja) y capítulo 6 (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 régimen sinusoidal permanente, cada tensión y cada corriente se representa con un fasor: un número complejo cuyo módulo es la amplitud y cuyo argumento es la fase (Anexo A del Volumen I). Con fasores, condensadores e inductores cumplen una «ley de Ohm» con una admitancia compleja que depende de la pulsación :
| Componente | Admitancia |
|---|---|
| Resistencia | |
| Condensador | |
| Inductor |
Las leyes de Kirchhoff siguen siendo lineales, así que todo lo construido en B3 sigue valiendo: los mismos sellos, la misma matriz, el mismo np.linalg.solve. Solo cambian dos cosas: los números son complejos y la matriz depende de la frecuencia, de modo que hay que montar y resolver un sistema para cada frecuencia.
El resolvedor en alterna¶
Partimos del mini-SPICE de B3. Las funciones leer_netlist y numerar son las mismas. En resolver_ac, busca los tres cambios respecto a resolver_dc: las matrices son de tipo complejo, el sello de resistencia usa una admitancia y aparece la pulsación.
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
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
def admitancia(nombre, valor, omega):
"""Admitancia compleja de una resistencia, un condensador o un inductor."""
if nombre[0] == "R":
return 1 / valor
if nombre[0] == "C":
return 1j * omega * valor
if nombre[0] == "L":
return 1 / (1j * omega * valor)
raise ValueError(f"Elemento desconocido: {nombre}")
def resolver_ac(texto, f):
"""Resuelve una netlist en régimen sinusoidal a la frecuencia f (Hz). Devuelve fasores."""
elementos = leer_netlist(texto)
indice = numerar(elementos)
A = np.zeros((len(indice), len(indice)), dtype=complex)
z = np.zeros(len(indice), dtype=complex)
omega = 2 * np.pi * f
for nombre, nodo_mas, nodo_menos, valor in elementos:
a, b = indice[nodo_mas], indice[nodo_menos]
if nombre[0] == "I":
z[a] -= valor
z[b] += valor
elif nombre[0] == "V":
k = indice[nombre]
A[a, k] += 1
A[b, k] -= 1
A[k, a] += 1
A[k, b] -= 1
z[k] = valor
else:
y = admitancia(nombre, valor, omega)
A[a, a] += y
A[b, b] += y
A[a, b] -= y
A[b, a] -= y
x = np.zeros(len(indice), dtype=complex)
x[1:] = np.linalg.solve(A[1:, 1:], z[1:])
return {nombre: x[k] for nombre, k in indice.items()}1. Predice¶
Más abajo vas a simular un filtro RC paso bajo y, después, tres etapas RC idénticas conectadas una detrás de otra. Antes de ejecutar:
En la frecuencia de corte de una etapa, ¿cuánto valen el módulo y la fase de ?
Si una etapa tiene una función de transferencia , ¿será la de las tres etapas en cascada?
La frecuencia a la que la salida de las tres etapas cae 3 dB, ¿será igual, mayor o menor que ?
Tu predicción:
2. Ejecuta y explora¶
2.1 Un filtro RC, para validar¶
Una resistencia de 10 kΩ y un condensador de 100 nF. La fuente tiene amplitud 1 V y fase 0, de modo que el fasor de la salida es directamente la función de transferencia.
R_ETAPA = 10e3 # ohmios
C_ETAPA = 100e-9 # faradios
F_CORTE = 1 / (2 * np.pi * R_ETAPA * C_ETAPA)
UNA_ETAPA = f"""
V1 1 0 1
R1 1 2 {R_ETAPA}
C1 2 0 {C_ETAPA}
"""
salida = resolver_ac(UNA_ETAPA, F_CORTE)["2"]
print(f"Frecuencia de corte: {F_CORTE:.2f} Hz")
print(f"Fasor de la salida: {salida:.4f}")
print(f"Módulo: {abs(salida):.4f} Fase: {np.degrees(np.angle(salida)):.2f}°")Frecuencia de corte: 159.15 Hz
Fasor de la salida: 0.5000-0.5000j
Módulo: 0.7071 Fase: -45.00°
El resultado es un número complejo: parte real y parte imaginaria, o módulo y fase. Para ver el comportamiento del filtro hay que repetir el cálculo a muchas frecuencias.
def barrido(texto, frecuencias, nodo):
"""Fasor de la tensión de un nodo a cada frecuencia: un sistema lineal por frecuencia."""
return np.array([resolver_ac(texto, f)[nodo] for f in frecuencias])
frecuencias = np.logspace(0, 4, 400) # de 1 Hz a 10 kHz
H1 = barrido(UNA_ETAPA, frecuencias, "2")
H1_teorica = 1 / (1 + 2j * np.pi * frecuencias * R_ETAPA * C_ETAPA)
print("¿Coincide con 1 / (1 + j·2πf·RC)?", np.allclose(H1, H1_teorica))¿Coincide con 1 / (1 + j·2πf·RC)? True
2.2 Tres etapas en cascada¶
Ahora conectamos tres etapas iguales, la salida de cada una a la entrada de la siguiente. La netlist se genera con un bucle.
def netlist_cascada(etapas, R, C):
"""Netlist de varias etapas RC paso bajo conectadas en cascada."""
lineas = ["V1 1 0 1"]
for k in range(1, etapas + 1):
lineas.append(f"R{k} {k} {k + 1} {R}")
lineas.append(f"C{k} {k + 1} 0 {C}")
return "\n".join(lineas)
TRES_ETAPAS = netlist_cascada(3, R_ETAPA, C_ETAPA)
print(TRES_ETAPAS)
H3 = barrido(TRES_ETAPAS, frecuencias, "4")
H3_ideal = H1**3V1 1 0 1
R1 1 2 10000.0
C1 2 0 1e-07
R2 2 3 10000.0
C2 3 0 1e-07
R3 3 4 10000.0
C3 4 0 1e-07
def a_db(H):
return 20 * np.log10(np.abs(H))
def fase_grados(H):
return np.degrees(np.unwrap(np.angle(H)))
fig, (ax_modulo, ax_fase) = plt.subplots(2, 1, sharex=True, figsize=(6.4, 5.6))
curvas = [
(H1, "Una etapa", "-"),
(H3, "Tres etapas (simulación)", "-"),
(H3_ideal, "Tres etapas ideales, $H_1^3$", "--"),
]
for H, etiqueta, estilo in curvas:
ax_modulo.semilogx(frecuencias, a_db(H), estilo, label=etiqueta)
ax_fase.semilogx(frecuencias, fase_grados(H), estilo)
ax_modulo.axhline(-3, color="gray", linestyle=":", linewidth=1)
ax_modulo.set(ylabel="Módulo (dB)", title="Diagrama de Bode")
ax_modulo.legend()
ax_fase.set(xlabel="Frecuencia (Hz)", ylabel="Fase (°)")
plt.show()
Las tres etapas reales no se comportan como . Para cuantificar la diferencia, buscamos numéricamente la frecuencia a la que cada curva cae 3 dB.
def frecuencia_3db(frecuencias, H):
"""Frecuencia a la que |H| cae a 1/√2, por interpolación (|H| debe ser decreciente)."""
modulo = np.abs(H)
return np.interp(1 / np.sqrt(2), modulo[::-1], frecuencias[::-1])
casos = [(H1, "Una etapa"), (H3, "Tres etapas (simulación)"), (H3_ideal, "Tres etapas ideales")]
for H, etiqueta in casos:
f3 = frecuencia_3db(frecuencias, H)
print(f"{etiqueta:<32} f(-3 dB) = {f3:7.2f} Hz = {f3 / F_CORTE:.3f} · fc")Una etapa f(-3 dB) = 159.16 Hz = 1.000 · fc
Tres etapas (simulación) f(-3 dB) = 30.92 Hz = 0.194 · fc
Tres etapas ideales f(-3 dB) = 81.14 Hz = 0.510 · fc
La cascada real corta a una frecuencia mucho más baja que la ideal. La razón es el efecto de carga: la segunda etapa toma corriente de la primera, de modo que la primera ya no es un RC aislado. El producto solo sería correcto si entre etapa y etapa hubiera un amplificador separador (buffer) que no cargara a la anterior.
Con la matriz completa no hace falta ninguna hipótesis: el efecto de carga está incluido porque todas las ecuaciones se resuelven a la vez.
2.3 Hilo biomédico: un puente de alterna para medir impedancias¶
La interfaz entre un electrodo y la piel no se comporta como una resistencia: a primera aproximación es una resistencia en paralelo con una capacidad . Una forma clásica de medir una impedancia desconocida es el puente de Wheatstone de B2, alimentado en alterna.
En una rama del puente está la impedancia desconocida () y en la otra, una resistencia y un condensador ajustables (). Las dos ramas superiores son resistencias iguales. Cuando el detector marca cero, las dos ramas inferiores son iguales.
RX, CX = 100e3, 47e-9 # la impedancia «desconocida» (valores ilustrativos)
F_MEDIDA = 100.0 # Hz
def detector(Rv, Cv, f=F_MEDIDA):
"""Módulo de la tensión del detector del puente para un ajuste (Rv, Cv)."""
netlist = f"""
V1 s 0 1
R1 s a 10e3
R2 s b 10e3
Rv a 0 {Rv}
Cv a 0 {Cv}
Rx b 0 {RX}
Cx b 0 {CX}
Rd a b 1e6
"""
v = resolver_ac(netlist, f)
return abs(v["a"] - v["b"])
print(f"Detector con Rv = Rx y Cv = Cx: {detector(RX, CX):.2e} V")
print(f"Detector con Rv = Rx y Cv = 0: {detector(RX, 0):.2e} V")
print(f"Detector con Rv = 0,8·Rx y Cv = Cx: {detector(0.8 * RX, CX):.2e} V")Detector con Rv = Rx y Cv = Cx: 2.78e-17 V
Detector con Rv = Rx y Cv = 0: 2.32e-01 V
Detector con Rv = 0,8·Rx y Cv = Cx: 1.86e-02 V
Supón que no conoces y giras el mando del condensador ajustable buscando el mínimo del detector. Lo hacemos dos veces: con la resistencia ya bien ajustada y con la resistencia un 20 % por debajo.
valores_Cv = np.linspace(10e-9, 100e-9, 181)
fig, ax = plt.subplots()
for Rv, etiqueta in [(RX, "$R_v = R_x$"), (0.8 * RX, "$R_v = 0{,}8\\,R_x$")]:
lectura = np.array([detector(Rv, Cv) for Cv in valores_Cv])
ax.semilogy(valores_Cv * 1e9, lectura * 1e3, label=etiqueta)
ax.axvline(CX * 1e9, color="gray", linestyle=":", linewidth=1)
ax.set(xlabel="$C_v$ (nF)", ylabel="Detector (mV)", title="Búsqueda del equilibrio girando $C_v$")
ax.legend()
plt.show()
Con la resistencia mal ajustada hay un mínimo, pero no un cero: ningún valor de equilibra el puente. Para verlo completo, calculamos la lectura del detector para todas las combinaciones de los dos mandos.
valores_Rv = np.linspace(50e3, 150e3, 61)
valores_Cv = np.linspace(20e-9, 80e-9, 61)
lectura = np.array([[detector(Rv, Cv) for Rv in valores_Rv] for Cv in valores_Cv])
fig, ax = plt.subplots()
lectura_db = 20 * np.log10(np.maximum(lectura, 1e-3)) # se recorta en -60 dB
mapa = ax.pcolormesh(valores_Rv / 1e3, valores_Cv * 1e9, lectura_db, shading="auto")
ax.plot(RX / 1e3, CX * 1e9, "w+", markersize=12)
ax.grid(False)
ax.set(xlabel="$R_v$ (kΩ)", ylabel="$C_v$ (nF)", title="Lectura del detector (dB respecto a 1 V)")
fig.colorbar(mapa, ax=ax)
plt.show()
El equilibrio es un punto, no una línea: hay que acertar a la vez con y con . En continua bastaba una condición porque la ecuación de equilibrio era real; en alterna la ecuación es compleja y equivale a dos ecuaciones reales (parte real y parte imaginaria, o módulo y fase). Por eso los puentes de alterna tienen dos mandos.
En el cuaderno B6 estimarás los mismos dos parámetros de otra manera: midiendo la impedancia a varias frecuencias y ajustando el modelo por mínimos cuadrados.
2.4 Comparación con la solución de referencia¶
Como en B3, contrastamos el resolvedor con la implementación de referencia del paquete del módulo.
from circuitmath import mna
tensiones_ref, _ = mna.solve_ac(TRES_ETAPAS, frecuencias)
print("¿Mismo resultado que la referencia?", np.allclose(H3, tensiones_ref["4"]))¿Mismo resultado que la referencia? True
3. Explica¶
Enumera los cambios entre
resolver_dc(B3) yresolver_ac. ¿Por qué hay que volver a montar la matriz en cada frecuencia y no basta con resolver una vez?A frecuencias altas, las dos curvas de tres etapas caen con la misma pendiente. ¿Cuál es, en dB por década, y por qué es el triple que la de una etapa?
La cascada real corta a una frecuencia más baja que la ideal. Explica el efecto de carga con tus palabras. ¿Qué cambiaría si la segunda y la tercera etapa usaran una resistencia cien veces mayor y un condensador cien veces menor? Compruébalo.
En el puente, con el detector nunca llega a cero. Relaciónalo con el hecho de que una ecuación compleja son dos ecuaciones reales.
El equilibrio del puente con y , ¿depende de la frecuencia de medida? Compruébalo llamando a
detectorcon otro valor defy explica el resultado.¿Qué ocurre si llamas a
resolver_accon y la netlist contiene un inductor? Con lo aprendido en B3, propón cómo tratar el inductor para evitarlo.
4. Ejercicios¶
Básico. Intercambia la resistencia y el condensador de
UNA_ETAPApara obtener un filtro paso alto. Representa su diagrama de Bode y comprueba que en el módulo es y la fase, +45°.Aplicación. Simula un circuito RLC serie ( Ω, mH, nF) alimentado con 1 V y representa el módulo de la corriente entre 1 kHz y 20 kHz. Localiza numéricamente la frecuencia de resonancia y el ancho de banda a −3 dB, y compáralos con y con .
Reto biomédico. Un electrodo de ECG se modela como una resistencia Ω en serie con el paralelo de kΩ y nF. El electrodo se conecta a un amplificador cuya entrada es una resistencia a referencia.
Escribe la netlist con una fuente de 1 mV que represente el biopotencial y calcula qué fracción de la señal llega al amplificador entre 0,05 Hz y 150 Hz, para MΩ y para kΩ.
Un electrodo seco puede tener MΩ. Repite el cálculo.
¿Qué resistencia de entrada mínima garantiza que se pierde menos del 1 % de la señal en toda la banda con el electrodo seco? ¿Por qué la atenuación depende de la frecuencia?
Resumen¶
En régimen sinusoidal el MNA no cambia: las conductancias pasan a ser admitancias complejas y las incógnitas, fasores.
La matriz depende de la frecuencia: un barrido es un bucle que monta y resuelve un sistema lineal por frecuencia.
Resolver el circuito completo incluye el efecto de carga entre etapas, que el producto de funciones de transferencia aisladas ignora.
Una ecuación compleja equivale a dos ecuaciones reales: un puente de alterna necesita dos ajustes para equilibrarse.
→ Siguiente: B5 · Sensibilidad, tolerancias y condicionamiento
- Miguel-Medina, T. (2026). Electrónica en Ingeniería Biomédica — Volumen I. Autopublicación. 10.5281/zenodo.22913382