Skip to content
cfd-lab:~/es/posts/2026-05-04-implicit-diff…● online

Traducción del original en coreano escrito por el autor. Leer el original (한국어)

NOTE #031DAY MON CFD기법DATE 2026.05.04UPDATED 2026.10.19READ 6 min read#Numerical-Analysis#Implicit#Diffusion#Thomas-algorithm#stability

Difusión implícita y algoritmo de Thomas — comprar estabilidad con un solo barrido tridiagonal

Partir la malla a la mitad divide el paso explícito entre cuatro. En una dimensión la salida es un barrido tridiagonal, no un paso menor.

El cálculo dejado a las 11 de la noche amaneció en NaN. La malla acababa de partirse a la mitad. El paso de tiempo, no. Olvidar la línea de la CFL (Courant–Friedrichs–Lewy) costó una hora de clúster.

Si el espaciado se reduce a la mitad, el paso explícito de la difusión queda en un cuarto. Una malla de 1024 puntos igual camina cientos de miles de pasos, solo para no explotar. La salida es corta. El futuro pasa al lado derecho y Thomas factoriza la tridiagonal.

El deslizador de r va de 0.05 a 5 y superpone el mismo pulso gaussiano en ambos esquemas, así que el punto de ruptura queda en una sola figura.

Explicit: stable (r ≤ 0.5)Implicit: unconditionally stable

Por debajo de 0.5 las curvas casi coinciden. Al cruzar 0.5 la traza naranja (explícita) empieza a vibrar en diente de sierra; pasada la unidad sale del marco, y la traza cian (implícita) se queda.

La CFL ata las mallas finas#

La ecuación de difusión es

∂q∂t=D∂2q∂x2\frac{\partial q}{\partial t} = D \frac{\partial^2 q}{\partial x^2}

donde qq es concentración (o temperatura) y DD el coeficiente de difusión. El esquema explícito más simple (Euler hacia adelante con diferencias centrales) se escribe

qin+1=qin+r (qi+1n−2qin+qi−1n),r=D ΔtΔx2q_i^{n+1} = q_i^{n} + r\,(q_{i+1}^{n} - 2 q_i^{n} + q_{i-1}^{n}),\quad r = \frac{D\,\Delta t}{\Delta x^2}

Para que sea estable hace falta r≤1/2r \le 1/2. Análisis de von Neumann en una línea: la longitud de onda más corta (longitud =2Δx= 2\Delta x) tiene factor de amplificación ∣1−4r∣|1 - 4r|, y forzarlo ≤1\le 1 obliga a r≤1/2r \le 1/2.

Pasar de 1024→20481024 \to 2048 deja Δt\Delta t en 1/41/4. Un corte más, hasta 40964096, deja Δt\Delta t en 1/161/16 respecto del paso de 1024 puntos, así que el número de pasos se multiplica por 16. Ocho veces más puntos significan 64 veces más pasos para el mismo instante. La factura es ese cuadrado, no el número de celdas.

Con r=0.6r = 0.6 el factor de la onda más corta es ∣1−4r∣=1.4|1 - 4r| = 1.4. Sesenta pasos son ese factor elevado a 60. El 1.33×10121.33\times 10^{12} impreso también arrastra la parte del pulso inicial que cayó sobre ese modo. Con r=0.4r = 0.4 el factor es 0.60.6 y el pico explícito se queda en 4.21×10−14.21\times 10^{-1}.

Mover el futuro al lado derecho#

Una sola sustitución. qnq^n del lado derecho pasa a ser qn+1q^{n+1}.

qin+1=qin+r (qi+1n+1−2qin+1+qi−1n+1)q_i^{n+1} = q_i^{n} + r\,(q_{i+1}^{n+1} - 2 q_i^{n+1} + q_{i-1}^{n+1})

Con el valor futuro a la derecha ya no se resuelve celda a celda. Todas las incógnitas quedan acopladas y el paso es una ecuación matricial.

A qn+1=qnA\,\mathbf{q}^{n+1} = \mathbf{q}^{n}

El mismo análisis da un factor 1/(1+4r)1/(1 + 4r), siempre menor que 1 si rr es positivo. Aumentar Δt\Delta t no hace divergir el esquema. Es incondicionalmente estable.

El precio es la precisión. Un Δt\Delta t enorme sigue estable y deja de ser preciso. Aun así, "no va a explotar" pesa cuando el trabajo queda corriendo de noche.

Con el mismo r=0.4r = 0.4 y los mismos 60 pasos, los picos son 4.21×10−14.21\times 10^{-1} explícito y 0.44560.4456 implícito. Ambos estables, y ya discrepan en el tercer dígito. Mismo tiempo físico. La diferencia es error de truncamiento.

Matrices tridiagonales y el barrido único de Thomas#

Al escribir AA, en 1D la celda ii solo se acopla con i−1i-1 e i+1i+1.

A=(1+r−r−r1+2r−r−r1+2r−r⋱⋱⋱−r1+r)A = \begin{pmatrix} 1+r & -r & & & \\ -r & 1+2r & -r & & \\ & -r & 1+2r & -r & \\ & & \ddots & \ddots & \ddots \\ & & & -r & 1+r \end{pmatrix}

Es una matriz tridiagonal. Solo tres diagonales son distintas de cero. La eliminación gaussiana general es O(N3)\mathcal{O}(N^3); una tridiagonal es O(N)\mathcal{O}(N). El algoritmo es el de Thomas. Llewellyn Thomas lo dejó en una nota interna de IBM en 1949.

Thomas da dos pasadas.

  1. Eliminación hacia adelante — barrido de arriba abajo, eliminando la subdiagonal y calculando una nueva diagonal b′b' y un nuevo lado derecho d′d'.
  2. Sustitución hacia atrás — barrido de abajo arriba, sustituyendo los valores ya conocidos.

Cada pasada son cuatro o cinco operaciones por celda, y la memoria son tres vectores. La matriz completa no tiene que residir en memoria.

Solo la diagonal de frontera vale 1+r1+r. Las filas interiores valen 1+2r1+2r. Flujo nulo iguala la celda fantasma al valor de frontera, así que el vecino exterior desaparece. Un rr sale del 2r2r diagonal, y el −r-r que apuntaba a ese vecino se va con él. Por eso el código fija b[0] = 1 + r. Copiar la fila interior en la frontera hace que el total se escape.

Comparación en NumPy#

import numpy as np
 
def explicit_diffusion_step(q, r):
    """Euler hacia adelante + diferencias centrales."""
    next_q = q.copy()
    next_q[1:-1] = q[1:-1] + r * (q[2:] - 2 * q[1:-1] + q[:-2])
    return next_q
 
def thomas_solver(a, b, c, d):
    """Resuelve el sistema tridiagonal A x = d en O(N).
    a: subdiagonal (a[0] no se usa)
    b: diagonal principal
    c: superdiagonal (c[-1] no se usa)
    d: lado derecho
    """
    n = len(d)
    cp = np.zeros(n)
    dp = np.zeros(n)
    cp[0] = c[0] / b[0]
    dp[0] = d[0] / b[0]
    for i in range(1, n):
        m = b[i] - a[i] * cp[i - 1]
        cp[i] = c[i] / m
        dp[i] = (d[i] - a[i] * dp[i - 1]) / m
    x = np.zeros(n)
    x[-1] = dp[-1]
    for i in range(n - 2, -1, -1):
        x[i] = dp[i] - cp[i] * x[i + 1]
    return x
 
def implicit_diffusion_step(q, r):
    """Euler hacia atrás — Thomas resuelve (I - r·L) q^{n+1} = q^n."""
    n = len(q)
    a = np.full(n, -r)
    b = np.full(n, 1 + 2 * r)
    c = np.full(n, -r)
    b[0] = 1 + r        # frontera de flujo cero
    b[-1] = 1 + r
    return thomas_solver(a, b, c, q.copy())
 
# Comparar sobre un pulso gaussiano
N = 80
x = np.linspace(0, 1, N)
q0 = np.exp(-((x - 0.5) ** 2) / 0.005)
D, dx = 1.0, 1.0 / N
r_values = [0.4, 0.6, 2.0]
for r in r_values:
    dt = r * dx**2 / D
    qe, qi = q0.copy(), q0.copy()
    for _ in range(60):
        qe = explicit_diffusion_step(qe, r)
        qi = implicit_diffusion_step(qi, r)
    print(f"r={r:.2f} | explicit max={np.max(np.abs(qe)):.2e} | implicit max={np.max(np.abs(qi)):.4f}")
# r=0.40 | explicit max=4.21e-01 | implicit max=0.4456
# r=0.60 | explicit max=1.33e+12 | implicit max=0.3873
# r=2.00 | explicit max=2.78e+38 | implicit max=0.2517

Con r=0.6r=0.6 el método explícito ha llegado a 101210^{12} en 60 pasos. El implícito, con el mismo paso, sigue difundiendo, con un pico de 0.38730.3873. Con r=2r=2 el máximo explícito es 2.78×10382.78\times 10^{38} y el pico implícito ha bajado a 0.25170.2517.

Las tres filas impresas no son el mismo instante#

Con Δt=r Δx2/D\Delta t = r\,\Delta x^2 / D, N=80N = 80 y D=1D = 1, sesenta pasos de tiempo físico escalan con rr. La fila r=2r = 2 está cinco veces más adelante que la fila r=0.4r = 0.4. Leer 0.44560.4456 junto a 0.25170.2517 y decir que el implícito "murió más" es una comparación falsa. Igualar el tiempo exige igualar rr por el número de pasos. Doce pasos con r=2r = 2 caen en el mismo instante que sesenta pasos con r=0.4r = 0.4. La salida impresa no hizo ese ajuste.

Por qué el implícito no siempre es la respuesta#

La estabilidad incondicional no es un pase libre. El coste se parte en tres.

1. Precisión vs estabilidad — La estabilidad solo dice que la amplitud no explota. No dice que la respuesta sea correcta. Euler hacia atrás es O(Δt)\mathcal{O}(\Delta t). Si el paso grande también tiene que ser preciso, conviene pasar a Crank–Nicolson (O(Δt2)\mathcal{O}(\Delta t^2), también incondicionalmente estable).

El factor de la onda más corta en Crank–Nicolson es (1−2r)/(1+2r)(1-2r)/(1+2r). Con r=2r = 2 vale −0.6-0.6. La magnitud queda por debajo de 1, así que no explota, pero el signo es negativo y la fase se invierte en cada paso. Si rr sube más, el factor se pega a −1-1.

2. No linealidad — Si DD depende de la solución (D=D(q)D = D(q)), la matriz cambia en cada paso y hace falta un solucionador no lineal (Picard o Newton) encima de Thomas.

3. Más dimensiones — En 2D/3D la matriz deja de ser tridiagonal. Un stencil de cinco o siete puntos crea bandas laterales. El paso se separa con ADI (implícito en direcciones alternas) en dos o tres barridos tridiagonales 1D, o se pasa a los métodos iterativos de Jacobi a BiCGSTAB.

Ese tercer caso reaparece al pasar de flujo compresible a incompresible. La ecuación de Poisson para la presión ∇2P=−ρ∇ ⁣⋅(u ⁣⋅ ⁣∇u)\nabla^2 P = -\rho \nabla\!\cdot(\mathbf{u}\!\cdot\!\nabla\mathbf{u}) tiene la misma forma dispersa y grande. Quien ya resolvió difusión implícita usa las mismas herramientas en el paso de presión.

Anotar un r grande como respuesta correcta es un fallo#

El pico implícito 0.25170.2517 con r=2r = 2 no es un registro de haberse acercado a la solución. El tiempo físico es cinco veces mayor, y el error temporal escala con Δt\Delta t. Guardar la curva sin anotar el tiempo físico de esos pasos vuelve falsa la comparación.

En una malla bidimensional este código tridiagonal no se reutiliza tal cual. Cuando un barrido que nunca forma la matriz parece más barato, ¿hasta dónde lleva la misma promesa de estabilidad LU-SGS, que avanza de forma implícita sin construir la matriz? Si la dirección del barrido está mal, ¿esa promesa se rompe con ella? Esa parte sigue abierta.

Comparte si te resultó útil.