Códigos de espirales y fracciones continuas
En el último vídeo sobre espirales en el mundo vegetal y fracciones continuas utilicé algunos códigos en Python para crear las animaciones y calcular las fracciones continuas de algunos números. He recopilado algunos por si son de interés.

Fracciones continuas
Todo número real se puede escribir en forma de fracción continua simple, una torre de fracciones que tiene este aspecto:
\pi \,=\, 3 + \cfrac{1}{7 + \cfrac{1}{15 + \cfrac{1}{1 + \cfrac{1}{\cdots}}}} \,=\, [3, 7, 15, 1, ...]Aunque la torre de fracciones es muy visual, a veces es preferible la notación en forma de lista. Los elementos que forman la lista, los a_n, son los coeficientes que encontramos en cada piso de la fracción continua:
[a_0, a_1, a_2, a_3, ...] \,=\, a_0 + \cfrac{1}{a_1 + \cfrac{1}{a_2 + \cfrac{1}{a_3 + \cfrac{1}{\cdots}}}} Por otro lado, truncar o cortar una fracción continua simple por el n-ésimo piso nos devuelve la n-ésima convergente. Las convergentes son las fracciones que mejor se aproximan al número original. Cada convergente se suele escribir como p_n/q_n. Cuantos más pisos consideres al truncar la fracción continua, más precisa será la aproximación que consigas.
El siguiente código devuelve la fracción continua de un número, así como sus primeras convergentes.
import numpy as np
def fraccion_continua(num, pisos):
# Calcula la fracción continua de un número.
# num = número que convertir en fracción.
# pisos = número de pisos de la fracción.
fraccion = []
for piso in range(pisos):
pe = np.floor(num)
num = num - pe
fraccion.append(int(pe))
if num < 1e-8:
break
num =1.0/num
# Esta parte evita duplicidades del tipo [2]=[1,1]
if len(fraccion)>1 and fraccion[-1]==1:
fraccion.pop()
fraccion[-1] += 1
return fraccion
def convergentes(fraccion):
p0, q0 = 1, 0
p1, q1 = fraccion[0], 1
print('Piso 0:')
print(str(p1) + '/1 = ' + str(p1))
print(' ')
pisos = len(fraccion)
for n in range(1, pisos):
print('Piso ' + str(n) + ':')
p2, q2 = fraccion[n]*p1 + p0, fraccion[n]*q1 + q0
p0, q0 = p1, q1
p1, q1 = p2, q2
print(str(p2) + '/' + str(q2) + ' = ' + str(p2/q2))
print(' ')
return None
# Ejemplo numérico:
num = 0.12345
pisos = 10
f = fraccion_continua(num, pisos)
print(str(f) + "\n")
convergentes(f)La función fraccion_continua calcula la fracción de un número dado con el método que expliqué en el vídeo: calcular el inverso, separar la parte entera y repetir. El resultado es una lista con los elementos de la fracción continua. Por cuestiones numéricas, todo lo que sea inferior a 10^{-8} se considera cero.
He añadido una sección en el código para eliminar redundancias. Hay ocasiones en las que un número tiene varias fracciones continuas equivalentes cuando el último piso es un 1. Por ejemplo, las dos fracciones siguientes representan el mismo número:
[3,1,1]=3+\cfrac{1}{1+\cfrac{1}{1}} \qquad\qquad [3,2] =3+\cfrac{1}{2} .La función convergentes transforma una fracción continua en las aproximaciones que resultan de truncarla por cada piso. Para calcular qué fracción resulta de cortar la fracción continua por un determinado piso he utilizado la recurrencia de las convergentes,
\begin{aligned}
p_n &=a_n p_{n-1} + p_{n-2}\,, \\
q_n &=a_n q_{n-1} + q_{n-2}\,,
\end{aligned}para n\geq 1. Se puede tomar (p_0, q_0)=(a_0, 1) y (p_{-1}, q_{-1})=(1, 0).
Espirales
La construcción de una espiral sigue la metodología explicada en el vídeo. En general, una espiral es un conjunto de puntos distribuidos en el plano. Cada punto tiene coordenadas
p_n=\left(\sqrt{n}\cos(2\pi r n), \sqrt{n}\sin(2\pi r n)\right) .En esta expresión, n es el natural que indica n-ésimo punto de la espiral y r\in\mathbb{R} es el ángulo que separa los puntos, medido como un porcentaje de vuelta (es decir, que r=1/4 es un cuarto de vuelta o 90º).
Las espirales visualmente más llamativas dependen del valor del ángulo r y la distancia de los puntos al centro. En esta descripción he utilizado la distancia de un punto al centro como \sqrt{n}, aunque también es habitual utilizar simplemente n. Lo puedes modificar en la función distancia del siguiente código.
import numpy as np
import matplotlib.pyplot as plt
def distancia(d):
# Devuelve la disancia de un punto al centro.
# Puede ser sqrt(d) o simplemente d.
return np.sqrt(d)
def dibujar_espiral(n, r):
# Dibuja una espiral de puntos.
# n = número de puntos.
# r = ángulo de separación entre puntos.
# nombre = nombre con el que se guarda la imagen
fig = plt.figure()
ax = fig.add_subplot(111)
size = distancia(n+1)
# Dibujar ejes
plt.plot([-size,size], [0,0], linewidth=1, color='#C0C0C0', solid_capstyle='round')
plt.plot([0,0], [-size,size], linewidth=1, color='#C0C0C0', solid_capstyle='round')
lx = []
ly = []
for k in range(0, n + 1):
# Dibujar puntos
x = distancia(k) * np.cos(2 * np.pi * r * k)
y = distancia(k) * np.sin(2 * np.pi * r * k)
lx.append(x)
ly.append(y)
# Añadir un letrero con el número en cada punto
plt.text(x + 0.2, y + 0.2, str(k), fontsize=6, ha='right', va='bottom')
plt.plot(lx, ly, '.', markersize=8, c='#f47d00')
plt.xlim([-size*1.05, size*1.05])
plt.ylim([-size*1.05, size*1.05])
plt.axis('off')
ax.set_aspect('equal')
# Guardar imagen
nombre = 'prueba'
plt.savefig(nombre + '.png', dpi=400, transparent=True)
plt.show()
plt.close()
# Ejemplo
dibujar_espiral(100, np.e-2)
El código anterior genera una espiral como esta, compuesta por cien puntos distribuidos con el ángulo que utiliza los decimales del número e.

Los números son útiles para observar algunas propiedades de las fracciones continuas y las espirales. Por ejemplo, los puntos más cercanos al eje x (por la parte positiva) son los que coinciden con los denominadores de las fracciones convergentes. En este caso, el primer punto que más se acerca al eje es el 3, después el 4, después el 7, el 32, el 39 y el 71.
Animación
El código anterior se puede modificar para crear las animaciones de mi vídeo. Una modificación sencilla sería que aparecieran los puntos uno por uno, construyendo poco a poco la espiral. Otra modificación (la que ofrece el código de abajo) es la transición de la espiral cuando modificas el ángulo de separación entre los puntos.
Para ello, solo tienes que indicar el ángulo inicial, el ángulo final, la cantidad de fotogramas que quieres generar y el número de puntos que tendrá la espiral. Como he comentado alguna vez, este código genera los fotogramas del vídeo, pero no el vídeo en sí. Para mí es más cómodo así, por cuestiones de producción. Con el programa de edición genero la animación con 30 fotogramas por segundo.
def angulo(ang_ini, ang_fin, frames, j):
# Hace que el movimiento del ángulo sea suave
# (que se acelere y frene en el comienzo y el final).
# ang_ini = ángulo inicial de la animación, en grados.
# ang_fin = ángulo final de la animación, en grados.
# frames = número de frames de la animación.
# j = número del frame.
ang = ang_ini + 0.5*(ang_fin - ang_ini)*(1 + np.sin(np.pi*(j/(frames-1)-0.5)))
return ang/180*np.pi
def crear_fotogramas(ang_ini, ang_fin, frames, n):
# Crea y guarda los fotogramas de la animación de una
# espiral que cambia el ángulo de ang_ini hasta ang_fin.
# ang_ini = ángulo inicial de la animación, en grados.
# ang_fin = ángulo final de la animación, en grados.
# frames = número de frames de la animación.
# n = puntos de la espiral.
for j in range(frames):
angle = angulo(ang_ini, ang_fin, frames, j)
lx = []
ly = []
for i in range(n):
lx += [distancia(i)*np.cos(i*angle)]
ly += [distancia(i)*np.sin(i*angle)]
fig = plt.figure()
ax = fig.add_subplot(111)
ax.set_aspect('equal')
plt.axis('off')
plt.plot(lx, ly, '.', markersize=4, c='steelblue')
size = distancia(n+1)
plt.xlim([-size*1.05, size*1.05])
plt.ylim([-size*1.05, size*1.05])
plt.savefig('Espiral'+str(j).zfill(5)+'.png', dpi=400,transparent=True)
plt.close()
return None
# Ejemplo
n = 200
frames = 150
ang_ini = 83.5
ang_fin = 84.5
crear_fotogramas(ang_ini, ang_fin, frames, n)La función angulo está diseñada para que el movimiento sea suave: que se acelere lentamente en el comienzo de la animación y se frene poco a poco al final. Si te interesa que el movimiento sea a velocidad constante, puedes cambiar la función por esta:
def angulo(ang_ini, ang_fin, frames, j):
# Hace que el movimiento del ángulo sea a velocidad constante.
# ang_ini = ángulo inicial de la animación, en grados.
# ang_fin = ángulo final de la animación, en grados.
# frames = número de frames de la animación.
# j = número del frame.
ang = ang_ini + (ang_fin - ang_ini)*j/(frames-1)
return ang/180*np.pi