Con el objetivo de representar gráficamente el proceso de criba de Eratóstenes para obtener numeros primos, utilizamos PyGame, Imageio y NumPy para generar la siguiente animación.
Código completo al final del artículo.
Generar la animación
Para comenzar, recordemos la función a la que habíamos llegado en el artículo anterior.
def get_prime_numbers(max_number):
numbers = [True, True] + [True] * (max_number-1)
last_prime_number = 2
i = last_prime_number
while last_prime_number**2 <= max_number:
i += last_prime_number
while i <= max_number:
numbers[i] = False
i += last_prime_number
j = last_prime_number + 1
while j < max_number:
if numbers[j]:
last_prime_number = j
break
j += 1
i = last_prime_number
return [i + 2 for i, not_crossed in enumerate(numbers[2:]) if not_crossed]
La idea es, a medida que la función selecciona y descarta números, representarlo en la pantalla utilizando PyGame. Para evitar que el código anterior bloquee el bucle principal de PyGame, haremos las modificaciones a continuación.
def get_prime_numbers(max_number):
numbers = [True, True] + [True] * (max_number-1)
last_prime_number = 2
i = last_prime_number
while last_prime_number**2 <= max_number:
yield ACTION_SELECTED, i
i += last_prime_number
while i <= max_number:
numbers[i] = False
yield ACTION_CROSSED_OUT, i
i += last_prime_number
j = last_prime_number + 1
while j < max_number:
if numbers[j]:
last_prime_number = j
break
j += 1
i = last_prime_number
for i, not_crossed in enumerate(numbers[2:]):
if not_crossed:
yield ACTION_PRIME_NUMBER, i + 2
Previamente definiendo las siguientes constantes.
ACTION_SELECTED = 0 ACTION_CROSSED_OUT = 1 ACTION_PRIME_NUMBER = 2
Convertimos la función en un generador para dar lugar a la ejecución del bucle principal a medida que se realiza el proceso de criba. Las constantes anteriores indicarán qué tipo recuadro se debe dibujar (verde para números primos o rojo para números descartados) y cuánto tiempo debe esperarse. Volveremos a esto más adelante.
Seguimos definiendo otras tres constantes que representan la cantidad de números en pantalla, las columnas y las filas, respectivamente.
NUMBERS = 120 COLS = 10 ROWS = NUMBERS / COLS
Para evitar el uso de objetos globales, implementamos una clase principal, Animation, y una estructura básica de PyGame.
import pygame
# ...
class Animation(object):
def start(self):
pygame.init()
self.screen = pygame.display.set_mode((515, 500))
pygame.display.set_caption("Criba de Eratóstenes")
run = True
# Pintar pantalla de blanco.
self.screen.fill((255, 255, 255))
# Fuente para dibujar los números.
self.font = pygame.font.Font(None, 20)
while run:
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
break
pygame.display.flip()
if __name__ == "__main__":
Animation().start()
Hasta el momento tenemos simplemente una pantalla en blanco. Continuaremos por colocar la cantidad de numeros definidos (120). Antes de hacerlo, definimos la siguiente función que retorna la fila y columna correspondiente para un determinado número dentro del rango (1-120).
def get_number_position(number):
col = number % COLS
if col == 0:
col = 10
row = (number - col) / COLS
return row, col
Luego, dentro de la clase Animation, incluimos la función draw_rect, que será la encargada de dibujar un número con su respectivo recuadro de color en la posición determinada por get_number_position.
def draw_rect(self, number, color):
row, col = get_number_position(number)
self.screen.fill(color, (15 + col*40, 15 + row*40, 30, 30))
text = self.font.render(str(number), 1, (240, 240, 240))
w, h = text.get_size()
self.screen.blit(text,
(15 + col*40 + (30 - w)/2, 15 + row*40 + (30 - h)/2))
Por último, antes de while run:, procedemos a dibujar los números en pantalla (exceptuando el 1).
numbers = range(1, NUMBERS + 1)
for number in numbers:
if number == 1:
continue
self.draw_rect(number, (190, 190, 190))
Ahora bien, luego del código anterior, obtenemos la lista de números primos y creamos un par de variables para el control del tiempo en la animación.
prime_numbers = get_prime_numbers(NUMBERS)
prev_ticks = 0
wait = 100
Finalmente, antes de pygame.display.flip(), ubicamos la siguiente lógica.
ticks = pygame.time.get_ticks()
dif = ticks - prev_ticks
if dif > wait:
try:
action, number = next(prime_numbers)
except StopIteration:
run = False
else:
if action in (ACTION_SELECTED, ACTION_PRIME_NUMBER):
self.draw_rect(number, (0, 190, 70))
elif action == ACTION_CROSSED_OUT:
self.draw_rect(number, (255, 130, 130))
if action in (ACTION_CROSSED_OUT, ACTION_PRIME_NUMBER):
wait = 75
else:
wait = 750
prev_ticks = ticks
Hacemos uso de la función get_ticks para controlar la duración de cada proceso de selección en pantalla. A medida que obtenemos instrucciones del generador prime_numbers (vía next(prime_numbers)), dibujamos en pantalla los recuadros con su correspondiente color (self.draw_rect). Finalmente, cuando la criba ha finalizado (se lanza StopIteration), terminamos el bucle principal (run = False).
Exportar como imagen GIF
Los módulos y funciones necesarias:
import imageio from numpy import fliplr, rot90
Luego de wait = 100, creamos una lista para cada fragmento de la animación y otra para su duración.
frames = []
duration = []
Las cuales procedemos a completar luego de prev_ticks = ticks.
# Obtener vector de NumPy de la imagen actual.
frames.append(pygame.surfarray.array3d(self.screen))
# Conversión a segundos para Imageio.
duration.append(dif / 1000)
Por último, generamos la imagen GIF al final de la función start.
print("Generando archivo GIF...")
# Esperar 2 segundos al comenzar.
duration[0] = 2
# Esperar 5 segundos al finalizar.
duration[-1] = 5
# Rotar e invertir el vector para satisfacer la lectura
# de Imageio.
imageio.mimwrite("sieve.gif",
(fliplr(rot90(f, 3)) for f in frames), duration=duration)
print("Listo.")
Código completo
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import imageio
import pygame
from numpy import fliplr, rot90
NUMBERS = 120
COLS = 10
ROWS = NUMBERS / COLS
ACTION_SELECTED = 0
ACTION_CROSSED_OUT = 1
ACTION_PRIME_NUMBER = 2
def get_prime_numbers(max_number):
numbers = [True, True] + [True] * (max_number-1)
last_prime_number = 2
i = last_prime_number
while last_prime_number**2 <= max_number:
yield ACTION_SELECTED, i
i += last_prime_number
while i <= max_number:
numbers[i] = False
yield ACTION_CROSSED_OUT, i
i += last_prime_number
j = last_prime_number + 1
while j < max_number:
if numbers[j]:
last_prime_number = j
break
j += 1
i = last_prime_number
for i, not_crossed in enumerate(numbers[2:]):
if not_crossed:
yield ACTION_PRIME_NUMBER, i + 2
def get_number_position(number):
col = number % COLS
if col == 0:
col = 10
row = (number - col) / COLS
return row, col
class Animation(object):
def draw_rect(self, number, color):
row, col = get_number_position(number)
self.screen.fill(color, (15 + col*40, 15 + row*40, 30, 30))
text = self.font.render(str(number), 1, (240, 240, 240))
w, h = text.get_size()
self.screen.blit(text,
(15 + col*40 + (30 - w)/2, 15 + row*40 + (30 - h)/2))
def start(self):
pygame.init()
self.screen = pygame.display.set_mode((515, 500))
pygame.display.set_caption("Criba de Eratóstenes")
run = True
self.screen.fill((255, 255, 255))
self.font = pygame.font.Font(None, 20)
numbers = range(1, NUMBERS + 1)
for number in numbers:
if number == 1:
continue
self.draw_rect(number, (190, 190, 190))
prime_numbers = get_prime_numbers(NUMBERS)
prev_ticks = 0
wait = 100
frames = []
duration = []
while run:
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
break
ticks = pygame.time.get_ticks()
dif = ticks - prev_ticks
if dif > wait:
try:
action, number = next(prime_numbers)
except StopIteration:
run = False
else:
if action in (ACTION_SELECTED, ACTION_PRIME_NUMBER):
self.draw_rect(number, (0, 190, 70))
elif action == ACTION_CROSSED_OUT:
self.draw_rect(number, (255, 130, 130))
if action in (ACTION_CROSSED_OUT, ACTION_PRIME_NUMBER):
wait = 75
else:
wait = 750
prev_ticks = ticks
frames.append(pygame.surfarray.array3d(self.screen))
duration.append(dif / 1000)
pygame.display.flip()
print("Generando archivo GIF...")
duration[0] = 2
duration[-1] = 5
imageio.mimwrite("sieve.gif",
(fliplr(rot90(f, 3)) for f in frames), duration=duration)
print("Listo.")
if __name__ == "__main__":
Animation().start()
Curso online 👨💻
¡Ya lanzamos el curso oficial de Recursos Python en Udemy!
Un curso moderno para aprender Python desde cero con programación orientada a objetos, SQL y tkinter en 2024.
Consultoría 💡
Ofrecemos servicios profesionales de desarrollo y capacitación en Python a personas y empresas. Consultanos por tu proyecto.

