Objetivos de aprendizaje
Estimar con una simulación de Monte Carlo cómo afectan las tolerancias de los componentes a la salida de un circuito.
Contrastar la simulación con el peor caso y con la sensibilidad de primer orden.
Interpretar el número de condición de una matriz y reconocer un sistema mal condicionado.
Explicar cómo el desapareamiento de las resistencias limita el rechazo en modo común (CMRR) de un amplificador diferencial.
Teoría: Volumen I, capítulo 5 (divisor de tensión) 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
import schemdraw
import schemdraw.elements as elm
import sympy as sp
from circuitmath.plotting import use_style
use_style()Idea clave¶
Hasta ahora cada circuito tenía una solución. En el laboratorio no es así: una resistencia «de 10 kΩ al 5 %» puede valer cualquier cosa entre 9,5 kΩ y 10,5 kΩ, y el resultado que has calculado es solo uno de los posibles. Este cuaderno plantea dos preguntas distintas, que conviene no confundir:
Sensibilidad: ¿cuánto cambia la salida de un circuito cuando cambian sus componentes? Es una propiedad del circuito, y decide si un diseño funcionará con componentes reales.
Condicionamiento: ¿cuánto cambia la solución de un sistema lineal cuando cambian ligeramente sus datos? Es una propiedad de la matriz, y decide si el ordenador puede resolver el sistema con precisión.
Para la primera usaremos el método de Monte Carlo: repetir el cálculo miles de veces con valores de componentes tomados al azar dentro de su tolerancia y mirar la distribución del resultado. Para la segunda, el número de condición.
Primer circuito: un divisor de tensión¶
El circuito más sencillo posible: dos resistencias nominalmente iguales que dividen 5 V por la mitad.
V_ENTRADA = 5.0 # voltios
R_NOMINAL = 10e3 # ohmios, las dos resistencias
def divisor(R1, R2):
"""Tensión de salida de un divisor: R1 arriba, R2 abajo."""
return V_ENTRADA * R2 / (R1 + R2)
print(f"Salida nominal: {divisor(R_NOMINAL, R_NOMINAL):.3f} V")Salida nominal: 2.500 V
1. Predice¶
Si las dos resistencias tienen una tolerancia del 5 %, ¿cuál es el mayor error relativo posible en la tensión de salida: 2,5 %, 5 % o 10 %?
Si el valor de cada resistencia puede caer con igual probabilidad en cualquier punto de su margen de tolerancia, ¿qué forma tendrá el histograma de la tensión de salida: plana, triangular o de campana?
Un circuito tiene dos nodos unidos por un cable de 1 mΩ. ¿Esperas que eso sea un problema para resolverlo numéricamente?
Tu predicción:
2. Ejecuta y explora¶
2.1 Monte Carlo¶
Fabricamos cien mil divisores. Suponemos que el valor de cada resistencia se reparte uniformemente dentro de su tolerancia, una hipótesis conservadora y habitual cuando del componente solo se conoce la tolerancia. La semilla del generador se fija para que el cuaderno dé siempre el mismo resultado.
rng = np.random.default_rng(2026)
N = 100_000 # número de circuitos simulados
def fabricar(nominal, tolerancia, n=N):
"""Valores de n resistencias repartidos uniformemente dentro de la tolerancia."""
return nominal * (1 + rng.uniform(-tolerancia, tolerancia, n))
salidas = {}
for tolerancia in (0.05, 0.01):
R1 = fabricar(R_NOMINAL, tolerancia)
R2 = fabricar(R_NOMINAL, tolerancia)
salidas[tolerancia] = divisor(R1, R2) # NumPy calcula los cien mil divisores de una vez
fig, ax = plt.subplots()
for tolerancia, v in salidas.items():
ax.hist(v, bins=80, density=True, alpha=0.6, label=f"Tolerancia del {tolerancia:.0%}")
ax.set(xlabel="Tensión de salida (V)", ylabel="Densidad", title="Cien mil divisores de tensión")
ax.legend()
plt.show()
for tolerancia, v in salidas.items():
print(
f"Tolerancia {tolerancia:.0%}: media = {v.mean():.4f} V, "
f"desviación típica = {v.std() * 1e3:.2f} mV, "
f"mínimo = {v.min():.4f} V, máximo = {v.max():.4f} V"
)Tolerancia 5%: media = 2.5000 V, desviación típica = 51.15 mV, mínimo = 2.3756 V, máximo = 2.6241 V
Tolerancia 1%: media = 2.5000 V, desviación típica = 10.25 mV, mínimo = 2.4751 V, máximo = 2.5250 V
2.2 Sensibilidad: predecir sin simular¶
El histograma se puede anticipar con derivadas. La sensibilidad normalizada de la salida respecto a un componente dice qué variación relativa de la salida produce una variación relativa del componente:
R1s, R2s, Vin = sp.symbols("R1 R2 V_in", positive=True)
salida_sim = Vin * R2s / (R1s + R2s)
S1 = sp.simplify(sp.diff(salida_sim, R1s) * R1s / salida_sim)
S2 = sp.simplify(sp.diff(salida_sim, R2s) * R2s / salida_sim)
print("Sensibilidad respecto a R1:", S1)
print("Sensibilidad respecto a R2:", S2)
iguales = {R1s: R_NOMINAL, R2s: R_NOMINAL}
s1, s2 = float(S1.subs(iguales)), float(S2.subs(iguales))
print(f"Con R1 = R2: S1 = {s1}, S2 = {s2}")Sensibilidad respecto a R1: -R1/(R1 + R2)
Sensibilidad respecto a R2: R1/(R1 + R2)
Con R1 = R2: S1 = -0.5, S2 = 0.5
Con resistencias iguales, un 1 % más en sube la salida un 0,5 %, y un 1 % más en la baja un 0,5 %. De aquí salen dos predicciones para una tolerancia :
Peor caso: las dos resistencias se desvían al máximo y en sentidos opuestos: .
Desviación típica: las desviaciones son independientes y se suman en cuadratura. Una distribución uniforme de semianchura tiene una desviación típica , de modo que la de la salida es .
nominal = divisor(R_NOMINAL, R_NOMINAL)
print(f"{'Tolerancia':<12}{'Peor caso':>26}{'Desviación típica':>28}")
print(f"{'':<12}{'predicho':>13}{'simulado':>13}{'predicha':>14}{'simulada':>14}")
for tolerancia, v in salidas.items():
peor_predicho = (abs(s1) + abs(s2)) * tolerancia * nominal
peor_simulado = np.max(np.abs(v - nominal))
sigma_predicha = np.hypot(s1, s2) * tolerancia / np.sqrt(3) * nominal
print(
f"{tolerancia:<12.0%}{peor_predicho * 1e3:>10.2f} mV{peor_simulado * 1e3:>10.2f} mV"
f"{sigma_predicha * 1e3:>11.2f} mV{v.std() * 1e3:>11.2f} mV"
)Tolerancia Peor caso Desviación típica
predicho simulado predicha simulada
5% 125.00 mV 124.43 mV 51.03 mV 51.15 mV
1% 25.00 mV 24.97 mV 10.21 mV 10.25 mV
La sensibilidad de primer orden y la simulación coinciden. La derivada es más rápida y da una fórmula; Monte Carlo no necesita derivar nada y funciona igual con un circuito de cien componentes o con una salida que no sea lineal, como verás al final del cuaderno.
2.3 Segundo circuito: un sistema mal condicionado¶
Cambiamos de pregunta. Dos nodos, cada uno con una resistencia MΩ a referencia, unidos por una resistencia pequeña . Una fuente inyecta 1 µA en el nodo 1.
with schemdraw.Drawing() as d:
d.config(unit=2.5, fontsize=12)
fuente = elm.SourceI().up().label("1 µA")
elm.Line().right().length(1.5)
elm.Dot().label("1", loc="top")
d.push()
elm.Resistor().down().label("$R$", loc="bottom")
d.pop()
elm.Resistor().right().label("$r$")
elm.Dot().label("2", loc="top")
elm.Resistor().down().label("$R$", loc="bottom")
elm.Line().left().tox(fuente.start)
elm.Ground()Este circuito tiene solución exacta sencilla, así que sirve de banco de pruebas: se puede comparar lo que calcula el ordenador con lo que debería salir.
El número de condición mide cuánto puede amplificar una matriz los errores relativos de sus datos: si los datos tienen un error relativo , la solución puede tener un error relativo de hasta . Una matriz con cercano a 1 es ideal; con se pueden perder seis cifras significativas.
R_GRANDE = 1e6 # ohmios
I_FUENTE = 1e-6 # amperios
def sistema(r):
"""Matriz G, vector de fuentes y solución exacta del circuito de dos nodos."""
G = np.array(
[
[1 / R_GRANDE + 1 / r, -1 / r],
[-1 / r, 1 / R_GRANDE + 1 / r],
]
)
i = np.array([I_FUENTE, 0.0])
exacta = I_FUENTE * R_GRANDE / (2 * R_GRANDE + r) * np.array([R_GRANDE + r, R_GRANDE])
return G, i, exacta
for r in (1e6, 1e3, 1.0, 1e-3):
G, i, exacta = sistema(r)
print(f"r = {r:8.0e} Ω número de condición = {np.linalg.cond(G):.2e} v exacta = {exacta}")r = 1e+06 Ω número de condición = 3.00e+00 v exacta = [0.66666667 0.33333333]
r = 1e+03 Ω número de condición = 2.00e+03 v exacta = [0.50024988 0.49975012]
r = 1e+00 Ω número de condición = 2.00e+06 v exacta = [0.50000025 0.49999975]
r = 1e-03 Ω número de condición = 2.00e+09 v exacta = [0.5 0.5]
Cuanto menor es , peor condicionada está la matriz. ¿Qué significa en la práctica? Tomamos mΩ y cambiamos un solo elemento de la matriz en una parte por millón.
G, i, exacta = sistema(1e-3)
print("Solución con la matriz original: ", np.linalg.solve(G, i), "V")
G_alterada = G.copy()
G_alterada[0, 0] *= 1 + 1e-6
print("Solución con G11 alterado 1 ppm: ", np.linalg.solve(G_alterada, i), "V")Solución con la matriz original: [0.5 0.5] V
Solución con G11 alterado 1 ppm: [0.000998 0.000998] V
Un cambio de una millonésima en un dato ha hundido la solución de 0,5 V a 1 mV. Tiene una lectura física: S, y una millonésima de 1000 S es 1 mS, es decir, el equivalente a conectar una resistencia de 1 kΩ entre el nodo 1 y la referencia, en paralelo con la de 1 MΩ.
Ahora cambiamos el componente en lugar del elemento de la matriz: pasa de 1 mΩ a 1,01 mΩ (un 1 %, diez mil veces más que antes).
_, _, exacta_101 = sistema(1.01e-3)
cambio = np.linalg.norm(exacta_101 - exacta) / np.linalg.norm(exacta)
print(f"Cambio relativo de la solución al aumentar r un 1 %: {cambio:.1e}")Cambio relativo de la solución al aumentar r un 1 %: 5.0e-12
La solución ni se entera. El circuito no es sensible: es un cable entre dos nodos, y da igual que mida 1 mΩ o 1,01 mΩ. Lo delicado es el planteamiento: la matriz guarda la información importante (las resistencias de 1 MΩ) como una diferencia minúscula entre números enormes, y cualquier error en esos números la destruye.
2.4 El ordenador también redondea¶
Nadie altera la matriz a propósito, pero el ordenador sí lo hace: cada número se guarda con un número finito de cifras. En doble precisión (float64, la habitual) son unas 16 cifras decimales; en precisión simple (float32), unas 7. Resolvemos el mismo circuito con las dos, para valores de cada vez menores, y medimos el error frente a la solución exacta.
def error_relativo(r, tipo):
"""Error relativo de np.linalg.solve trabajando con el tipo de dato indicado."""
G, i, exacta = sistema(r)
try:
v = np.linalg.solve(G.astype(tipo), i.astype(tipo))
except np.linalg.LinAlgError:
return np.nan # tras el redondeo, la matriz es exactamente singular
return np.linalg.norm(v - exacta) / np.linalg.norm(exacta)
valores_r = np.logspace(6, -6, 25)
condicion = np.array([np.linalg.cond(sistema(r)[0]) for r in valores_r])
fig, ax = plt.subplots()
for tipo in (np.float32, np.float64):
errores = np.array([error_relativo(r, tipo) for r in valores_r])
(linea,) = ax.loglog(condicion, errores, "o", markersize=4, label=tipo.__name__)
ax.loglog(
condicion, condicion * np.finfo(tipo).eps, "--", color=linea.get_color(), linewidth=1
)
fallos = np.isnan(errores).sum()
print(f"{tipo.__name__}: {fallos} casos de {len(valores_r)} con matriz singular")
ax.set(
xlabel="Número de condición, κ",
ylabel="Error relativo de la solución",
title="Precisión de la solución (líneas: κ · ε de la máquina)",
ylim=(1e-17, 10),
)
ax.legend()
plt.show()float32: 9 casos de 25 con matriz singular
float64: 0 casos de 25 con matriz singular

El error crece en proporción al número de condición y se mantiene por debajo de la línea discontinua , donde es la precisión relativa de cada tipo de dato. Es la regla práctica: al resolver un sistema se pierden unas cifras significativas.
En precisión simple, los últimos puntos no aparecen: al sumar 10-6 S a una conductancia de 100 S o más con solo siete cifras, la conductancia de 1 MΩ desaparece, y lo que el ordenador ve es un circuito sin camino a referencia. Es la matriz singular del cuaderno B1, producida esta vez por el redondeo.
La lección es de modelado, no de programación: un número de condición enorme avisa de que el modelo mezcla escalas muy distintas. Aquí la solución es no modelar el cable: si es despreciable frente a , los nodos 1 y 2 son el mismo nodo.
2.5 Hilo biomédico: resistencias desapareadas y rechazo en modo común¶
Un electrocardiograma mide una diferencia de potencial de alrededor de 1 mV entre dos electrodos. Los dos electrodos, además, recogen por igual una interferencia de 50 Hz de la red eléctrica que puede ser cien o mil veces mayor. El amplificador debe amplificar la diferencia entre sus entradas y rechazar lo que tienen en común.
El amplificador diferencial básico usa un amplificador operacional y cuatro resistencias. En teoría rechaza por completo el modo común si . ¿Y con resistencias reales?
with schemdraw.Drawing() as d:
d.config(unit=2.5, fontsize=12)
operacional = elm.Opamp(leads=True)
elm.Line().left().at(operacional.in1).length(0.75)
inversora = elm.Dot()
elm.Resistor().left().label("$R_1$")
elm.Dot(open=True).label("$V_1$", loc="left")
elm.Line().up().at(inversora.center).length(1.75)
elm.Resistor().right().tox(operacional.out).label("$R_2$")
elm.Line().down().toy(operacional.out)
elm.Dot()
elm.Line().right().at(operacional.out).length(1)
elm.Dot(open=True).label("$V_o$", loc="right")
elm.Line().left().at(operacional.in2).length(0.75)
no_inversora = elm.Dot()
elm.Resistor().left().label("$R_3$")
elm.Dot(open=True).label("$V_2$", loc="left")
elm.Resistor().down().at(no_inversora.center).label("$R_4$")
elm.Ground()Para analizarlo basta el modelo ideal del amplificador operacional: sus entradas no toman corriente y, con realimentación negativa, están a la misma tensión. La entrada no inversora es un divisor (, ) y en la inversora se aplica KCL. SymPy despeja la salida.
Después descomponemos las entradas en su parte común y su parte diferencial, y , para obtener la ganancia de cada una.
V1, V2, Vo, Vc, Vd = sp.symbols("V1 V2 V_o V_c V_d")
R1, R2, R3, R4 = sp.symbols("R1 R2 R3 R4", positive=True)
v_entradas = V2 * R4 / (R3 + R4) # tensión de las dos entradas del operacional
kcl = sp.Eq((V1 - v_entradas) / R1 + (Vo - v_entradas) / R2, 0)
salida = sp.solve(kcl, Vo)[0]
salida = salida.subs({V1: Vc - Vd / 2, V2: Vc + Vd / 2})
A_d = sp.simplify(sp.diff(salida, Vd)) # ganancia diferencial
A_c = sp.factor(sp.diff(salida, Vc)) # ganancia en modo común
A_cLa ganancia en modo común se anula exactamente cuando , y solo entonces. El CMRR (common-mode rejection ratio) es el cociente entre la ganancia diferencial y la de modo común, en decibelios: cuanto mayor, mejor.
Simulamos cien mil amplificadores de ganancia 10 ( kΩ, kΩ) para tres calidades de resistencia.
ganancias = sp.lambdify((R1, R2, R3, R4), (A_d, A_c))
GANANCIA = 10
fig, ax = plt.subplots()
cmrr = {}
for tolerancia in (0.05, 0.01, 0.001):
Ad, Ac = ganancias(
fabricar(10e3, tolerancia),
fabricar(10e3 * GANANCIA, tolerancia),
fabricar(10e3, tolerancia),
fabricar(10e3 * GANANCIA, tolerancia),
)
cmrr[tolerancia] = 20 * np.log10(np.abs(Ad / Ac))
ax.hist(
cmrr[tolerancia], bins=120, range=(30, 130), density=True, alpha=0.6,
label=f"Tolerancia del {tolerancia:.1%}",
)
ax.set(xlabel="CMRR (dB)", ylabel="Densidad", title="CMRR de cien mil amplificadores diferenciales")
ax.legend()
plt.show()
V_COMUN = 100e-3 # voltios, interferencia de 50 Hz común a los dos electrodos
V_ECG = 1e-3 # voltios, amplitud de la señal
print(f"{'Tolerancia':<12}{'Mediana':>10}{'Percentil 1':>14}{'Peor caso':>12}{'Interferencia':>16}")
for tolerancia, valores in cmrr.items():
peor_caso = 20 * np.log10((1 + GANANCIA) / (4 * tolerancia))
garantizado = np.percentile(valores, 1)
interferencia = V_COMUN / 10 ** (garantizado / 20) # equivalente a la entrada
print(
f"{tolerancia:<12.1%}{np.median(valores):>7.1f} dB{garantizado:>11.1f} dB"
f"{peor_caso:>9.1f} dB{interferencia / V_ECG:>12.0%} del ECG"
)Tolerancia Mediana Percentil 1 Peor caso Interferencia
5.0% 48.7 dB 37.9 dB 34.8 dB 128% del ECG
1.0% 62.7 dB 51.8 dB 48.8 dB 26% del ECG
0.1% 82.7 dB 71.8 dB 68.8 dB 3% del ECG
La columna «peor caso» es la fórmula aproximada para una ganancia y una tolerancia . La última columna traduce el CMRR que cumple el 99 % de las unidades a algo tangible: qué tamaño tendría, comparada con un ECG de 1 mV, una interferencia de 100 mV común a los dos electrodos.
Con resistencias del 1 %, la interferencia que se cuela es una fracción apreciable de la señal. Para que quede por debajo del 1 % del ECG hace falta un CMRR de 80 dB, es decir, resistencias apareadas mejor que el 0,03 %. Por eso los amplificadores de biopotenciales no se construyen con resistencias sueltas, sino con amplificadores de instrumentación integrados cuyas resistencias se ajustan por láser en fábrica.
Fíjate en que aquí la sensibilidad de primer orden del apartado 2.2 no habría bastado: el CMRR depende de una diferencia que en el caso nominal es cero. Monte Carlo no necesita ninguna hipótesis.
3. Explica¶
El valor de cada resistencia se reparte de forma uniforme, pero el histograma de la salida del divisor no es plano. ¿Por qué? ¿Qué forma tendría con diez resistencias en lugar de dos?
Para decidir si un diseño es aceptable se puede usar el peor caso o la desviación típica. ¿Cuál usarías para un equipo médico del que se fabrican diez unidades? ¿Y para estimar cuántas unidades de un lote de cien mil habrá que rechazar?
Explica con tus palabras la diferencia entre un circuito sensible y un sistema mal condicionado, usando el circuito de los dos nodos como ejemplo.
En precisión simple, el circuito de los dos nodos acaba dando «matriz singular». Relaciónalo con el circuito sin resistencias a referencia del cuaderno B1.
Según la fórmula del peor caso, el CMRR mejora al aumentar la ganancia . ¿Por qué? Piensa en qué le ocurre a cada una de las dos ganancias.
Cambia
Na 100 y ejecuta varias veces la simulación del divisor con semillas distintas. ¿Cuánto varía la desviación típica estimada? ¿Cómo esperas que dependa esa variación deN?
4. Ejercicios¶
Básico. Repite el análisis para un divisor 10:1 ( kΩ, kΩ) con resistencias del 1 %. Calcula las sensibilidades, predice el peor caso y la desviación típica de la salida y compáralos con una simulación. ¿Es este divisor más o menos sensible a las tolerancias que el de resistencias iguales?
Aplicación. En el puente de Wheatstone de B2 ( V, cuatro ramas de 1 kΩ, sin carga), un cambio del 1 % en el sensor daba unos 12 mV de salida. Simula el puente con las cuatro resistencias al 1 % y el sensor en su valor nominal.
Representa el histograma de la tensión de salida «en reposo» (el offset).
¿Qué fracción de los puentes tiene un offset mayor que la señal que se quiere medir?
¿Qué tolerancia haría falta para que el offset fuera, en el 99 % de los casos, menor que el 10 % de la señal? ¿Qué alternativa hay a comprar resistencias mejores?
Reto biomédico. Las resistencias del amplificador pueden ser perfectas y el CMRR, aun así, malo. La impedancia de contacto de cada electrodo queda en serie con la entrada correspondiente: se suma a en una rama y a en la otra.
Con las cuatro resistencias en su valor nominal, calcula el CMRR cuando los dos electrodos tienen 10 kΩ y cuando uno tiene 10 kΩ y el otro 20 kΩ.
Simula cien mil casos con impedancias de electrodo repartidas uniformemente entre 5 kΩ y 50 kΩ, independientes entre sí. ¿Qué CMRR se garantiza en el 99 % de los casos?
Los amplificadores de instrumentación anteponen a esta etapa dos seguidores de tensión, cuya impedancia de entrada es de gigaohmios. Explica por qué eso resuelve el problema y qué relación tiene con el efecto de carga del cuaderno B4.
Resumen¶
Los componentes reales tienen tolerancias: cada resultado calculado es uno de una distribución. Monte Carlo la obtiene repitiendo el cálculo con valores al azar.
La sensibilidad de primer orden predice el peor caso y la desviación típica cuando la salida depende suavemente de los componentes; Monte Carlo sirve siempre.
El número de condición mide cuánto amplifica una matriz los errores de sus datos: al resolver se pierden unas cifras. Un valor enorme avisa de un modelo que mezcla escalas muy distintas.
El CMRR de un amplificador diferencial lo limita el apareamiento de sus resistencias, no su valor: con una tolerancia y una ganancia , el peor caso es .
→ Siguiente: B6 · Mínimos cuadrados: ajustar modelos a medidas
- Miguel-Medina, T. (2026). Electrónica en Ingeniería Biomédica — Volumen I. Autopublicación. 10.5281/zenodo.22913382