Hola amigx cuánticx! Seguro que si trabajás o sos un entusiasta de la Física algo de osciladores armónicos viste en tu vida. Y sí… sabemos resolver tan pocas cosas de manera analítica que las exprimimos hasta el fin. Hoy voy a contarte sobre el oscilador armónico cuántico, pero desde un punto de vista completamente numérico. Me parece que es algo que si bien está en muchos textos, puede ser interesante para tener una primera visión de los problemas numéricos que aparecen en las simulaciones del quehacer científico. Pasemos a definir nuestro problema: El Hamiltoniano de un oscilador unidimensional tiene la siguiente pinta:
(1)
Si utilizamos unidades atómicas, en la representación de coordenadas la Ecuación de Schrödinger independiente del tiempo será
(2)
que es un problema de autovalores, y que es bien conocido. Si consideramos que la partícula es un electrón , las energías toman los valores
, y las autofunciones correspondientes son:
(3)
Las soluciones analíticas se pueden encontrar en la mayoría de los libros de texto [1,2] inclusive en Wikipedia en la versión en inglés donde está desarrollado de manera bastante completa y una versión reducida en español. En este artículo exploraremos un método para resolverlo de manera numérica en Python 3, utilizando la librería NumPy.
Como todos los métodos numéricos, lo que se hace es discretizar todo el problema en una grilla, que llamaremos , con
. En este caso utilizaremos una grilla equi-espaciada. Esto nos permite utilizar el método de Numerov para realizar la integración. Este método nos dice que en una ecuación con la forma
, evaluada sobre una grilla, se puede integrar de manera iterativa de la siguiente manera:
(4)
donde:
(5)
En las ecuaciones (4) y (5), es el espaciado de la grilla y
es la función evaluada en el punto
de la grilla. Esto nos permitirá integrar a partir de dos puntos previos el punto siguiente. Este procedimiento se puede repetir de manera iterativa partiendo de los dos puntos del borde izquierdo (o derecho) de la grilla hasta llegar al otro extremo. En el siguiente snippet les muestro las implementaciones hacia adelante y hacia atrás.
def numerov_fwd(u, f, delta, n_cut): #u es la función, n_cut es el punto sobre la grilla hasta donde integrar #forward fi_0 = 0 fi_1 = u[1]*(1-delta**2/12*f[1]) for i in range(1,n_cut+1): fi_2 = 2*fi_1 - fi_0 + delta**2*f[i]*u[i] u[i+1] = fi_2/(1-delta**2/12*f[i+1]) fi_0 = fi_1 fi_1 = fi_2 return u def numerov_bwd(u, f, delta, n_cut): #backward fi_0 = u[n_grid - 1]*(1-delta**2/12*f[n_grid-1]) fi_1 = u[n_grid - 2]*(1-delta**2/12*f[n_grid-2]) for i in range(n_grid-2,n_cut-1,-1): fi_2 = 2*fi_1 - fi_0 + delta**2*f[i]*u[i] u[i-1] = fi_2/(1-delta**2/12*f[i-1]) fi_0 = fi_1 fi_1 = fi_2 return u
Es claro que para que la solución encontrada sea la solución correspondiente a una autofunción de nuestro problema, el autovalor provisto en y las condiciones de contorno deben ser las exactas. Para resolver un problema de autovalores como el que describe la Ecuación (2), donde no se conocen los autovalores, conviene explotar la continuidad de la función de onda y su derivada, utilizando un método de disparo como veremos en el apartado que sigue.
Método de disparo y el problema de autovalores y autovectores
Para resolver este problema, integraremos la Ecuación (2) hasta un punto intermedio, partiendo tanto por la derecha, como por la izquierda. Como las soluciones, si existen son únicas, para encontrarlas seguiremos el siguiente esquema (voy a utilizar la notación de los arrays de NumPy para los índices donde los valores negativos significa contar desde el último):
- Dadas dos condiciones de borde por izquierda:
, y otras por derecha:
, integraremos hasta un punto intermedio para un valor propuesto de la energía
, que determina la integración de la ecuación como parte de
- En ese punto intermedio igualamos las soluciones encontradas por izquierda y por derecha, y calculamos la derivada en ese punto mediante alguna aproximación. Si la derivada no es igual por derecha e izquierda dentro de un error determinado, quiere decir que la energía no corresponde a un autovalor. Esto se puede asegurar porque la derivada logarítmica de la función de onda debe ser continua, es decir:
- Este esquema se puede repetir, variando el valor de la energía de prueba hasta conseguir que la diferencia en los valores de la derivada obtenidos por izquierda y derecha difieran en un valor menor a una tolerancia determinada. Una vez que se alcanza este valor, podemos decir que hemos encontrado el autovalor, y la autofunción correspondiente (no olvidar que se deben normalizar).
Primero construyamos nuestra grilla radial
#parametros de grilla n_grid = 1024 #numero de puntos de grilla x_max = 15 #distancia del origen donde se considera el infinito x_grid = np.linspace(-x_max,x_max,n_grid) #grilla espacial delta = x_grid[1]-x_grid[0] #paso de la grilla
Resolvamos para un sistema de masa y frecuencia
. Para esto veamos qué sucede para una energía
a.u.
E_try=0.3 f = -2*(E_try-0.5*x_grid**2) #funcional de la energía del integrador de Numerov n_corte=n_grid//2 #lugar donde se deben intersectar las funciones psi_fwd=np.zeros(n_grid) #defino el array de la función psi_fwd[0] = 1e-4 psi_fwd[1] = 1 #defino las condiciones de borde psi_fwd = numerov_fwd(psi_fwd.copy(), f, delta, n_corte+1) #evoluciono un paso mas para derivar por el punto medio psi_fwd = psi_fwd/psi_fwd[n_corte] #normalizo a 1 el punto donde corto

En la Figura (1) vemos la integración numérica de izquierda a derecha utilizando el algoritmo de Numerov. Hemos elegido integrar hasta la mitad de los puntos más uno, y forzamos a la solución para que tome el valor 1 en la mitad de la grilla. Es importante aclarar que las condiciones de borde no tienen que ser exactas, ya que el proceso de normalización lleva a un cambio de escala en la totalidad de la función. Esta es una de las grandes ventajas de este método donde no se conocen exactamente las condiciones asintóticas de las soluciones. Por otra parte siempre hay que tener una estimación adecuada debido a que la solución puede tomar valores que escapan de los 64 bits que tenemos para representar números de punto flotante.
Se puede hacer lo mismo partiendo desde el lado derecho, y fijarnos en la diferencia entre las derivadas por derecha y por izquierda para el punto elegido para hacer el encuentro entre las funciones.
psi_bwd=np.zeros(n_grid) psi_bwd[n_grid-1] = 1e-4 psi_bwd[n_grid-2] = 1 psi_bwd = numerov_bwd(psi_bwd.copy(), f, delta, n_corte-1) psi_bwd = psi_bwd/psi_bwd[n_corte]

En este caso vemos que si bien la función está forzada a ser continua, porque le hemos exigido que en el punto medio de la grilla tome el valor unidad, las derivadas por derecha e izquierda no son las mismas. Para ver los valores que toma la derivada podemos hacer el cálculo por diferencias finitas en el punto medio (por esto es que integramos un paso extra del punto de encuentro):
der_fwd = (psi_fwd[n_corte+1]-psi_fwd[n_corte-1])/2/delta der_bwd = -(psi_bwd[n_corte-1]-psi_bwd[n_corte+1])/2/delta print('deriv por izquierda:',der_fwd) print('deriv por derecha:',der_bwd) #out: deriv por izquierda: 0.3028597909587141 deriv por derecha: -0.32331840338242146
Donde vemos que la diferencia entre los valores de la derivada es notable. Veamos qué sucede cuando calculamos para el valor exacto de la energía del estado fundamental siguiendo el mismo procedimiento. En la Figura (3) vemos la función integrada por derecha e izquierda, donde se aprecia que por derecha e izquierda ambas soluciones se acercan de modo suave. Los valores que se obtienen para la derivada son los siguientes:
derivada por izq: -0.014656459323199651
derivada por der: -0.014656449080109192.

¡La diferencia en el error absoluto de las derivadas se encuentra recién en la octava cifra decimal! Lo que nos dice que nos encontramos ante un método sumamente potente para resolver este tipo de problemas. Para completar el procedimiento basta con normalizar la función. Recordemos que si:
(6)
La función se puede normalizar dividiendo por , como vemos a continuación mediante el método del trapecio:
psi = np.hstack([psi_fwd[:n_corte],psi_bwd[n_corte:]]) #unifico las soluciones psi = psi/np.sqrt(np.sum(psi[1:]**2+psi[:-1]**2)*delta/2) #normalizo integrando por trapecio
Y así hemos encontrado de un modo completamente numérico el estado fundamental del oscilador armónico. No está de más que verifiquen que se corresponde con la solución analítica, y lo recomiendo enfáticamente.
Hasta aquí llega este artículo, en uno próximo veremos qué sucede con los errores como función de la energía, y cómo sistematizar la búsqueda de autovalores y autofunciones de los estados excitados. Dejen comentarios, compartan y esas cosas ¡Hasta pronto!
[1] Eisberg, R. M., & Resnick, R. (1985). Quantum physics of atoms, molecules, solids, nuclei, and particles (2nd ed.). New York: Wiley.
[2] Cohen-Tannoudji, Claude, Bernard Diu, and Franck Laloë. 2005. Quantum mechanics. New York: Wiley