Objetivos de aprendizaje
Plantear la ecuación diferencial de un circuito RC o RL y resolverla de forma analítica (SymPy) y numérica (SciPy).
Interpretar la constante de tiempo y usarla para estimar cuánto dura un transitorio.
Programar el método de Euler y explicar por qué se vuelve inestable con pasos grandes.
Aplicar el transitorio de primer orden a la recuperación de la línea de base de un amplificador de ECG.
Teoría: Volumen I, Anexo C (corriente en un condensador, tensión en un inductor) (Miguel-Medina, 2026). Los circuitos dinámicos se desarrollan en el Volumen II. 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
import schemdraw
import schemdraw.elements as elm
import sympy as sp
from scipy import integrate
from circuitmath.plotting import use_style
use_style()Idea clave¶
Un circuito con resistencias y un condensador (o un inductor) no responde al instante a un cambio: necesita tiempo. La ley de Kirchhoff de tensiones, junto con , ya no da una ecuación algebraica, sino una ecuación diferencial: una relación entre una magnitud y su ritmo de cambio.
Para el circuito RC de la figura, al cerrar el interruptor:
La segunda forma se lee así: la tensión cambia tanto más deprisa cuanto más lejos está de su valor final. Todo el comportamiento del circuito queda resumido en un número con unidades de tiempo, la constante de tiempo .
with schemdraw.Drawing() as d:
d.config(unit=2.5, fontsize=12)
fuente = elm.SourceV().up().label("$V_s$")
elm.Switch().right().label("$t = 0$")
elm.Resistor().right().label("$R$")
elm.Capacitor().down().label("$C$", loc="bottom").label("$v(t)$", loc="top")
elm.Line().left().tox(fuente.start)
elm.Ground()R = 10e3 # ohmios <- cámbialos cuando quieras experimentar
C = 10e-6 # faradios
VS = 5.0 # voltios
TAU = R * C
print(f"Constante de tiempo: {TAU * 1e3:.0f} ms")Constante de tiempo: 100 ms
1. Predice¶
El condensador está descargado y el interruptor se cierra en .
¿Qué tensión tendrá el condensador cuando haya pasado un tiempo : la mitad de , dos tercios o toda?
Si se duplica , ¿cuánto tarda ahora en alcanzar el 99 % de : lo mismo, el doble o cuatro veces más?
Un método numérico avanza la solución a saltos de duración . ¿Qué crees que ocurre si es mayor que ?
Tu predicción:
2. Ejecuta y explora¶
2.1 Solución analítica¶
sp.dsolve resuelve la ecuación diferencial con la condición inicial .
t = sp.symbols("t", nonnegative=True)
Rs, Cs, Vs = sp.symbols("R C V_s", positive=True)
v = sp.Function("v")
ecuacion = sp.Eq(Rs * Cs * v(t).diff(t) + v(t), Vs)
solucion = sp.dsolve(ecuacion, v(t), ics={v(0): 0})
solucionEs la curva de carga exponencial, . La dibujamos con el tiempo medido en constantes de tiempo.
def carga(t, tau, v_final):
"""Carga exponencial desde cero: solución analítica del circuito RC."""
return v_final * (1 - np.exp(-t / tau))
tiempo = np.linspace(0, 6 * TAU, 400)
fig, ax = plt.subplots()
ax.plot(tiempo / TAU, carga(tiempo, TAU, VS))
ax.plot([0, 1], [0, VS], "k--", linewidth=1, label="Tangente en el origen")
ax.axhline(VS, color="gray", linestyle=":", linewidth=1)
for k in (1, 3, 5):
ax.plot(k, carga(k * TAU, TAU, VS), "o", color="C1")
ax.set(xlabel="Tiempo ($t/\\tau$)", ylabel="Tensión del condensador (V)")
ax.set_title("Carga de un circuito RC")
ax.legend()
plt.show()
for k in range(1, 6):
print(f"t = {k}τ: {carga(k * TAU, TAU, VS) / VS:.1%} del valor final")
t = 1τ: 63.2% del valor final
t = 2τ: 86.5% del valor final
t = 3τ: 95.0% del valor final
t = 4τ: 98.2% del valor final
t = 5τ: 99.3% del valor final
Tres lecturas de que conviene recordar:
Tras un tiempo se ha recorrido el 63 % del camino; tras , más del 99 %. En la práctica, un transitorio «dura» .
Si la tensión siguiera subiendo al ritmo inicial, llegaría al valor final exactamente en (la tangente de la figura).
La curva dibujada en unidades de es siempre la misma, valgan lo que valgan y .
2.2 Solución numérica: el método de Euler¶
La mayoría de las ecuaciones diferenciales no tienen fórmula. El método numérico más simple se deduce de la propia ecuación: si se conoce en un instante, la ecuación da su pendiente, y con la pendiente se estima el valor un poco después.
Es la diferencia hacia delante del cuaderno C2, despejada al revés.
def circuito_rc(t, v):
"""Pendiente dv/dt del circuito RC: lo que dice la ecuación diferencial."""
return (VS - v) / TAU
def euler(pendiente, v0, h, t_final):
"""Método de Euler explícito con paso h."""
n = int(round(t_final / h))
t = np.arange(n + 1) * h
v = np.empty(n + 1)
v[0] = v0
for k in range(n):
v[k + 1] = v[k] + h * pendiente(t[k], v[k])
return t, v
fig, (ax_bien, ax_mal) = plt.subplots(1, 2, figsize=(9, 3.8))
for ax, pasos in ((ax_bien, (0.1, 0.5)), (ax_mal, (1.5, 2.5))):
ax.plot(tiempo / TAU, carga(tiempo, TAU, VS), "k--", linewidth=1, label="Analítica")
for paso in pasos:
t_e, v_e = euler(circuito_rc, 0.0, paso * TAU, 6 * TAU)
ax.plot(t_e / TAU, v_e, "o-", markersize=3, label=f"Euler, $h = {paso}\\,\\tau$")
ax.set(xlabel="Tiempo ($t/\\tau$)")
ax.legend(fontsize="small")
ax_bien.set(ylabel="Tensión (V)", title="Pasos pequeños")
ax_mal.set(title="Pasos grandes")
fig.tight_layout()
plt.show()
Con pasos pequeños, Euler sigue a la solución. Con se pasa de largo y oscila alrededor del valor final, aunque acaba llegando. Con cada salto lo aleja más: el método es inestable, y el resultado no tiene nada que ver con el circuito.
Se puede ver por qué. Restando a los dos lados de la fórmula de Euler:
En cada paso, la distancia al valor final se multiplica por . Si ese factor es negativo, la solución oscila; si su valor absoluto es mayor que 1, crece. La condición de estabilidad es .
¿Y cuánto se equivoca cuando es estable? Medimos el error en para pasos cada vez menores.
divisiones = 2 ** np.arange(2, 12) # pasos por constante de tiempo
errores = []
for n in divisiones:
_, v_e = euler(circuito_rc, 0.0, TAU / n, TAU)
errores.append(abs(v_e[-1] - carga(TAU, TAU, VS)) / VS)
fig, ax = plt.subplots()
ax.loglog(1 / divisiones, errores, "o-", markersize=4)
ax.set(xlabel="Paso, $h/\\tau$", ylabel="Error relativo en $t = \\tau$")
ax.set_title("Error del método de Euler")
plt.show()
El error es proporcional al paso: Euler es de orden 1, como la suma de rectángulos de C1 y la diferencia hacia delante de C2. Para ganar una cifra hay que dar diez veces más pasos.
2.3 Un método mejor: solve_ivp¶
En la práctica se usan métodos de orden superior que, además, eligen el paso sobre la marcha: pequeño donde la solución cambia deprisa y grande donde apenas cambia. scipy.integrate.solve_ivp es la puerta de entrada.
resultado = integrate.solve_ivp(circuito_rc, (0, 6 * TAU), [0.0], rtol=1e-6, atol=1e-9)
error = np.max(np.abs(resultado.y[0] - carga(resultado.t, TAU, VS)))
print(f"Pasos dados: {len(resultado.t) - 1}")
print(f"Error máximo: {error:.1e} V")
fig, ax = plt.subplots()
ax.plot(tiempo / TAU, carga(tiempo, TAU, VS), "k--", linewidth=1, label="Analítica")
ax.plot(resultado.t / TAU, resultado.y[0], "o", markersize=4, label="Instantes de solve_ivp")
ax.set(xlabel="Tiempo ($t/\\tau$)", ylabel="Tensión (V)", title="Paso adaptativo")
ax.legend()
plt.show()Pasos dados: 21
Error máximo: 8.8e-07 V

Unas pocas decenas de pasos dan un error de microvoltios; Euler necesitaría millones. Los instantes se amontonan al principio, donde la pendiente es grande, y se espacian al final.
El método numérico muestra su valor cuando la entrada no es constante. Aplicamos al circuito un pulso de 5 V que empieza en s y dura 0,1 s, una constante de tiempo. Solo cambia la función de la pendiente.
def entrada(t):
"""Pulso de tensión de 0,1 s que empieza en t = 1 s."""
return VS if 1.0 <= t < 1.1 else 0.0
def rc_con_entrada(t, v):
return (entrada(t) - v) / TAU
confiado = integrate.solve_ivp(rc_con_entrada, (0, 3), [0.0])
cuidadoso = integrate.solve_ivp(rc_con_entrada, (0, 3), [0.0], max_step=TAU / 10)
t_fino = np.linspace(0, 3, 1500)
fig, ax = plt.subplots()
ax.plot(t_fino, [entrada(x) for x in t_fino], color="gray", linewidth=1, label="Entrada")
ax.plot(confiado.t, confiado.y[0], "o-", markersize=4, label="solve_ivp sin límite de paso")
ax.plot(cuidadoso.t, cuidadoso.y[0], label="solve_ivp con max_step = τ/10")
ax.set(xlabel="Tiempo (s)", ylabel="Tensión (V)", title="Respuesta a un pulso")
ax.legend(fontsize="small")
plt.show()
print(f"Tensión máxima sin límite de paso: {confiado.y[0].max():.2f} V")
print(f"Tensión máxima con max_step: {cuidadoso.y[0].max():.2f} V")
Tensión máxima sin límite de paso: 0.00 V
Tensión máxima con max_step: 3.08 V
Sin límite de paso, la solución es una línea plana: el método no ha visto el pulso. Durante el primer segundo no pasa nada, el algoritmo alarga el paso confiado y salta por encima. No da ningún aviso. Es el fallo opuesto al de Euler: no es inestable, simplemente no mira donde debe. Que lo vea o no depende de dónde caigan sus pasos: con un pulso más largo o en otro instante puede verlo entero, verlo a medias o no verlo.
Ningún método numérico exime de saber qué se espera. Cuando la entrada tiene cambios bruscos, hay que limitar el paso (max_step) a una fracción de lo que dura el detalle más corto.
2.4 El circuito RL es el mismo problema¶
En un circuito con una resistencia y un inductor en serie, . Dividiendo por :
Es la misma ecuación, con la corriente en el papel de la tensión, y valor final . No hace falta resolver nada nuevo.
L_BOBINA = 100e-3 # henrios
R_BOBINA = 100.0 # ohmios
tau_rl = L_BOBINA / R_BOBINA
t_rc = np.linspace(0, 6 * TAU, 200)
t_rl = np.linspace(0, 6 * tau_rl, 200)
v_rc = carga(t_rc, TAU, VS)
i_rl = carga(t_rl, tau_rl, VS / R_BOBINA)
fig, ax = plt.subplots()
ax.plot(t_rc / TAU, v_rc / VS, label=f"RC: tensión, τ = {TAU * 1e3:.0f} ms")
i_rl_normalizada = i_rl / (VS / R_BOBINA)
ax.plot(t_rl / tau_rl, i_rl_normalizada, "--", label=f"RL: corriente, τ = {tau_rl * 1e3:.0f} ms")
ax.set(xlabel="Tiempo ($t/\\tau$)", ylabel="Fracción del valor final")
ax.set_title("Dos circuitos, una curva")
ax.legend()
plt.show()
2.5 Hilo biomédico: la línea de base de un ECG¶
Entre un electrodo y la piel hay un potencial de contacto de continua que puede ser cientos de veces mayor que el ECG, y que cambia de golpe cuando el electrodo se conecta o el paciente se mueve. Para que no sature el amplificador, la entrada lleva un filtro paso alto: un condensador en serie y una resistencia a referencia.
Ese filtro es el circuito RC de este cuaderno, mirado desde otro sitio. El condensador se carga hacia la tensión de entrada con constante de tiempo , y la salida es lo que queda: la entrada menos la tensión del condensador.
Los electrocardiógrafos ofrecen dos frecuencias de corte: 0,05 Hz (modo diagnóstico) y 0,5 Hz (modo monitor). Simulamos qué ocurre en cada una cuando el potencial de contacto da un salto de 5 mV. La señal de ECG es sintética.
from circuitmath.signals import ecg
t_ecg, v_ecg = ecg(duration=16.0, fs=500.0)
dt = t_ecg[1] - t_ecg[0]
SALTO = 5e-3 # voltios
v_entrada = v_ecg + SALTO * (t_ecg >= 2.0)
def paso_alto(v_in, dt, f_corte):
"""Filtro paso alto RC resuelto con Euler: la salida es la entrada menos v_C."""
tau = 1 / (2 * np.pi * f_corte)
v_c = np.empty_like(v_in)
v_c[0] = v_in[0]
for k in range(len(v_in) - 1):
v_c[k + 1] = v_c[k] + dt * (v_in[k] - v_c[k]) / tau
return v_in - v_c
fig, axes = plt.subplots(3, 1, sharex=True, figsize=(6.4, 6.6))
axes[0].plot(t_ecg, v_entrada * 1e3, linewidth=0.8)
axes[0].set(ylabel="Entrada (mV)", title="Un salto del potencial de contacto en un ECG")
for ax, f_corte in zip(axes[1:], (0.05, 0.5), strict=True):
tau = 1 / (2 * np.pi * f_corte)
ax.plot(t_ecg, paso_alto(v_entrada, dt, f_corte) * 1e3, linewidth=0.8)
ax.set(ylabel="Salida (mV)", ylim=(-1, 6))
titulo = f"Corte en {f_corte} Hz (τ = {tau:.2f} s, paso de Euler = {dt / tau:.4f} τ)"
ax.set_title(titulo, fontsize="small")
axes[2].set_xlabel("Tiempo (s)")
fig.tight_layout()
plt.show()
El salto desplaza la señal y el filtro la devuelve a su sitio con una exponencial de constante de tiempo . Con el corte en 0,05 Hz, es de más de tres segundos y el trazado tarda unos quince en recuperarse (); con 0,5 Hz, segundo y medio.
¿Por qué no usar siempre 0,5 Hz? Porque el filtro no distingue el potencial de contacto de las partes lentas del propio ECG, como el segmento ST, y las deforma. El modo diagnóstico conserva la forma de onda a cambio de una línea de base lenta; el modo monitor da un trazado estable a cambio de fidelidad. Es una decisión de diseño que se reduce a elegir una constante de tiempo.
Fíjate, además, en que el filtro está resuelto con el método de Euler, con un paso cientos de veces menor que : un filtro digital es, en el fondo, una ecuación diferencial integrada paso a paso.
3. Explica¶
Explica con tus palabras qué es la constante de tiempo, usando la tangente en el origen de la primera figura. ¿Por qué se dice que un transitorio dura si, en rigor, nunca termina?
Con , Euler oscila pero converge; con , diverge. Explícalo con el factor . ¿Qué ocurre exactamente con ? ¿Y con ? Compruébalo.
El error de Euler, el de la suma de rectángulos (C1) y el de la diferencia hacia delante (C2) son todos de orden 1. ¿Qué tienen en común los tres métodos?
solve_ivpeligió pasos pequeños al principio y grandes al final. ¿Con qué criterio? ¿Por qué ese mismo criterio le hizo saltarse el pulso?El circuito RC y el RL dan la misma curva normalizada. ¿Qué tienen en común sus ecuaciones? Si se duplica la resistencia, ¿qué le ocurre a en cada uno?
En el ECG, ¿qué pasaría con la línea de base y con la forma de onda si la frecuencia de corte se subiera a 5 Hz? Pruébalo.
4. Ejercicios¶
Básico. Un condensador de 10 µF cargado a 5 V se descarga a través de una resistencia de 10 kΩ.
Plantea la ecuación diferencial y resuélvela con
sp.dsolve.Resuélvela con tu función
eulery consolve_ivp, y compara con la solución analítica.¿Cuánto tarda la tensión en caer al 10 % del valor inicial? Compruébalo con la fórmula .
Aplicación. Mides la descarga de un circuito RC cuya constante de tiempo no conoces. Genera datos sintéticos: 50 muestras de entre 0 y 0,5 s, con s y un ruido de 20 mV.
Tomando logaritmos, es una recta. Ajústala por mínimos cuadrados (cuaderno B6) y estima .
Representa los residuos. ¿Tienen todos la misma dispersión? ¿Por qué los últimos puntos son menos fiables?
Repite el ajuste usando solo las muestras con V, y luego con pesos proporcionales a . ¿Cuál se acerca más al valor real?
Reto biomédico. La membrana de una célula excitable se comporta, por debajo del umbral, como un circuito RC en paralelo. Un pulso de corriente de amplitud y duración la despolariza hasta , y la célula se dispara si se supera un umbral . Usa MΩ, ms y mV.
Simula con
solve_ivpla respuesta a un pulso de 2 nA y 0,5 ms, y a uno de 2 nA y 3 ms. ¿Cuál alcanza el umbral? (Recuerda limitar el paso.)Deduce la corriente mínima que dispara la célula en función de la duración del pulso, , y represéntala para entre 0,05 ms y 20 ms en ejes logarítmicos. Es la curva intensidad-duración.
Identifica en la curva la reobase (corriente umbral para pulsos muy largos) y la cronaxia (duración a la que el umbral es el doble de la reobase), y comprueba que la cronaxia vale .
La carga de cada pulso es . ¿Con qué duraciones se estimula con menos carga? ¿Por qué interesa en un marcapasos?
Resumen¶
Un circuito con un condensador o un inductor obedece a una ecuación diferencial de primer orden, y su respuesta a un escalón es una exponencial con constante de tiempo ( o ): 63 % en , más del 99 % en .
El método de Euler avanza con la pendiente que da la propia ecuación. Es de orden 1 y solo es estable si .
solve_ivpadapta el paso y es mucho más preciso, pero puede saltarse los cambios bruscos de la entrada: hay que limitar el paso.El filtro paso alto de un amplificador de ECG es un circuito de primer orden: su constante de tiempo decide cuánto tarda en recuperarse la línea de base.
→ Siguiente: C4 · Transitorios de segundo orden (RLC)
- Miguel-Medina, T. (2026). Electrónica en Ingeniería Biomédica — Volumen I. Autopublicación. 10.5281/zenodo.22913382