Implementación de los Autómatas Celulares de Wolfram en R

En este artículo, presento una exploración práctica de los ACW, utilizando R y {ggplot2} para la visualización. Esta implementación busca ilustrar cómo reglas sencillas pueden dar lugar a dinámicas, a veces, impredecibles.

R
Tutorial
Visualización
Autómatas Celulares
Fecha de publicación

23 de enero de 2024

Introducción a los Autómatas Celulares (de Wolfram)

Los Autómatas Celulares (AC) son sistemas dinámicos discretos y abstractos con aplicaciones en numerosos campos científicos. Se componen de una red de células que cambian de estado según reglas determinadas, a menudo con base en el estado de las células vecinas. Esta sencilla premisa puede generar una sorprendente variedad de comportamientos: desde patrones estáticos, hasta dinámicas complejas y caóticas.

Los AC fueron introducidos por primera vez por el matemático John von Neumann y el físico Stanislaw Ulam en la década de 1960, pero fue el físico Stephen Wolfram quien popularizó su estudio en la década de 1980. Wolfram propuso una estructura unidimensional y un conjunto de reglas binarias simples para los AC, que se terminaron conociendo como los Autómatas Celulares de Wolfram (ACW). Este tipo de autómatas se han convertido en un modelo de referencia para el estudio de la complejidad y la emergencia de patrones en sistemas dinámicos.

Fundamentos Teóricos

En general, los AC basan su comportamiento en tres componentes principales:

  • Espacio de Células: Una matriz (en nuestro caso, un vector) donde cada elemento representa una célula.
  • Estados: Cada célula puede adoptar un estado de un conjunto finito de estados posibles. En este ejemplo, las células pueden estar activas (“\(1\)”) o inactivas (“\(0\)”).
  • Reglas de Evolución: Las células cambian de estado según reglas determinadas, habitualmente con base en el estado de las células vecinas. Para los ACW, las células cambian de estado basandose en su propio estado y el de las células que están a la izquierda y derecha de cada una de ellas mediante una regla binaria de 8 bits. Esta última se representa como un número entero entre 0 y 255.

Preparación del Entorno en R

Para comenzar nuestra exploración de los Autómatas Celulares de Wolfram en R, primero necesitamos configurar nuestro entorno de trabajo cargando algunas bibliotecas esenciales. Estas bibliotecas nos ayudarán en la manipulación de datos y en la visualización gráfica de los resultados.

# Instalalas si aún no las tienes utilizando:
# install.packages(c("dplyr", "ggplot2", "ggview"))

library(dplyr) # para la manipulación de datos
library(ggplot2) # para la creación de gráficos
library(ggview) # para visualizar los gráficos en el Viewer sin guardarlos

Definición de la Función para Reglas Binarias

Después de haber preparado nuestro entorno en R, el siguiente paso es definir cómo nuestro ACW interpretará las reglas que dictarán la evolución de sus células. Recordemos que en los ACW, estas reglas se aplican basándose en el estado actual de una célula y sus vecinos inmediatos a la izquierda y a la derecha.

Cada combinación de estos tres estados (el propio y los de sus dos vecinos) puede ser representada como un número binario de 3 dígitos, y para cada combinación posible, la regla del autómata define si la célula estará activa o inactiva en la siguiente generación. Dado que hay \(2^{3} = 8\) combinaciones posibles, una regla de autómata celular se puede representar como una secuencia de 8 bits, donde cada bit indica el estado resultante para una combinación específica de vecinos. Por lo tanto, hay un total de \(2^{8} = 256\) reglas posibles, numeradas del 0 al 255.

La función getBinaryRule que definiremos a continuación, toma un número entre 0 y 255 y lo convierte en su versión binaria de 8 bits. Aquí nos estamos aprovechando de que los números del 0 al 255 se representan en binario con 8 dígitos y a partir del 256 con 9 o más. Esto nos permitirá aplicar fácilmente la regla elegida a nuestro autómata:

getBinaryRule <- function(ruleNumber) {
  # Convierte el número en una secuencia de bits:
  ruleBinary <- intToBits(ruleNumber)[1:8]
  # Convierte la secuencia de bits en un vector de enteros:
  ruleBinary <- as.integer(rev(ruleBinary))
  return(ruleBinary)
}

Tras definir la función getBinaryRule, nuestro siguiente paso es establecer el escenario para la simulación de nuestro ACW. Esto implica inicializar las reglas que determinarán el comportamiento de las células y preparar la población inicial en nuestro modelo.

Inicialización de las Reglas y las Células

Comenzaremos eligiendo una regla específica para nuestro autómata. Recordemos que las reglas varían de 0 a 255, cada una produciendo patrones interesantes. Para este ejemplo, utilizaremos la regla 135:

# Selecciona la regla número 135:
ruleNumber <- 135
# Convierte la regla a su representación binaria:
ruleSet <- getBinaryRule(ruleNumber)
# Establece el número de células y generaciones para la simulación:
numCells <- 101
generations <- 101
# Crea una matriz para representar cada generación de células:
grid <- matrix(nrow = generations, ncol = numCells)

Configuración de la Población Inicial

Ahora, definiremos la población inicial de células. Para simplificar y visualizar claramente el efecto de la regla seleccionada, iniciaremos con una única célula activa en el centro de nuestra cuadrícula:

# Inicializa todas las células en estado inactivo (0):
cells <- rep(0, numCells)
# Activa la célula central:
cells[ceiling(numCells / 2)] <- 1
# Establece la configuración previa como la primera generación en nuestra matriz:
grid[1, ] <- cells

Así, estamos en posición de simular cómo estas células evolucionarán a lo largo de las generaciones bajo la regla seleccionada. Este es el núcleo de los ACW, donde las simples reglas locales conducen a patrones complejos y a menudo sorprendentes a lo largo del tiempo.

Aplicación de las Reglas para Dar Paso a Nuevas Generaciones

Para observar la evolución de nuestro autómata celular, aplicaremos las reglas definidas en cada célula y en cada generación. Recordemos que las reglas consideran el estado de una célula y sus vecinas inmediatas (izquierda y derecha) para determinar su nuevo estado. Este proceso se repite para cada célula en cada nueva generación:

# Bucle a través de cada generación, empezando por la segunda:
for (gen in 2:generations) {
  # Bucle a través de cada célula en la generación:
  for (i in 1:numCells) {
    # Calcula los índices de las vecinas izquierda y derecha, 
    # considerando condiciones de frontera periódicas
    # (si la célula está en el borde, la vecina es la primera o la última):
    left_index <- ifelse(i == 1, numCells, i - 1)
    right_index <- ifelse(i == numCells, 1, i + 1)

    # Obtiene los estados de las vecinas y de la célula central:
    left <- grid[gen - 1, left_index]
    center <- grid[gen - 1, i]
    right <- grid[gen - 1, right_index]

    # Calcula el patrón de vecindario en versión entera (nota más adelante):
    neighborhood <- left * 4 + center * 2 + right
    # Determina el nuevo estado basado en la respectiva regla:
    grid[gen, i] <- ruleSet[8 - neighborhood]
  }
}
Nota

El vecindario de una célula es una combinación de sus estados y los de sus dos vecinas. En los ACW, este vecindario se representa como un número binario de 3 dígitos. Por ejemplo, si la célula izquierda, la célula central y la célula derecha están en los estados 1 (activo), 0 (inactivo), y 1 (activo), respectivamente, el vecindario se representa como \(101\) en binario.

Para convertir esta representación binaria del vecindario en algo que podamos usar para acceder a nuestra regla en versión binaria, necesitamos convertirla a un número entero que vaya del 0 al 7. Aquí es donde entran en juego las multiplicaciones por 4 y por 2:

  • El bit más a la izquierda (la vecina izquierda) se multiplica por 4 porque, en la representación binaria, este bit es equivalente a \(2^{2} = 4\). Si este bit es 1, su contribución al número decimal total es 4.
  • El bit del medio (la célula misma) se multiplica por 2 porque corresponde a \(2^{1} = 2\) en binario. Si esta célula está activa, añade 2 al número decimal.
  • El bit más a la derecha (la vecina derecha) no se multiplica porque representa \(2^{0} = 1\). Así, su estado (activo o inactivo) se añade tal cual al número decimal.

Este doble bucle, primero a través de las generaciones y luego a través de cada célula, es el corazón de nuestra simulación. En cada paso, estamos aplicando cada regla binaria para determinar el estado futuro de cada célula, basándonos en su estado actual y el de sus vecinas. Al final de este proceso, tendremos una representación completa de cómo las células evolucionan a lo largo del tiempo bajo la influencia de nuestra regla seleccionada.

Visualización de la Evolución de las Células

Para visualizar la evolución de nuestro ACW, primero necesitamos transformar nuestros datos de la matriz grid en un formato adecuado para {ggplot2}. Lo hacemos creando un data frame que represente cada célula y su estado en cada generación:

# Crea un data frame con todas las combinaciones de células y generaciones:
df <- expand.grid(x = 1:numCells, y = 1:generations)
# Asigna los valores de la cuadrícula al data frame, ajustando la estructura.
# La transposición cambia las filas por columnas y viceversa para que cada
# columna represente una generación y cada fila una célula.
# La función as.vector() convierte la matriz en un vector para que podamos ir
# asignando valores de generación en generación, como esta creado el data frame:
df$value <- as.vector(t(grid))
# Ajusta el eje y para que las generaciones se muestren en el orden correcto
# (de arriba a abajo):
df$y <- generations - df$y + 1

Ahora que tenemos nuestros datos en el formato correcto, podemos proceder a visualizarlos utilizando {ggplot2}. Esta herramienta nos permite crear una representación gráfica clara y atractiva de cómo cambian las células a lo largo del tiempo:

# Crea un gráfico con ggplot2:
plot_acw <- ggplot(df, aes(x = x, y = y, fill = factor(value))) +
  geom_tile() + # Crea un gráfico de mosaico
  scale_fill_manual(values = c("white", "black")) + # Define los colores
  theme(legend.position = "none", # Elimina la leyenda
        panel.grid = element_blank(), # Elimina las líneas de la cuadrícula
        axis.title = element_blank(), # Elimina los títulos de los ejes
        axis.text = element_blank(), # Elimina las etiquetas de los ejes
        axis.ticks = element_blank(), # Elimina las marcas de los ejes
        panel.background = element_blank(), # Elimina el fondo del panel
        panel.border = element_blank()) # Elimina el borde del panel

# Visualiza el gráfico en el Viewer de RStudio:
#ggview(plot_acw, width=10, height = 10, dpi = 500, units = "in")

# Guarda el gráfico como un archivo PNG:
#ggsave("acw_135.png", width=10, height = 10, dpi = 1000, units = "in")

El resultado de todo lo anterior es una visualización de la evolución de nuestro ACW bajo la regla 135:

Explorando el Espacio de Reglas

Ya que hemos visto cómo funciona un ACW bajo una regla específica, podemos explorar cómo cambia el comportamiento de nuestro sistema cuando cambiamos la regla. Para ello, a continuación y gracias a quarto-webr, podemos modificar y correr el siguiente chunk de código para explorar el espacio de reglas (0 a 255): solamente debemos cambiar el número de la regla dentro de la función ACW y darle clic a Run Code:

Conclusión y Reflexiones

Al concluir este viaje a través de los ACW en R, hemos explorado no solo un fascinante modelo matemático, sino también la profunda complejidad que puede surgir de reglas aparentemente simples. Los ACW nos ofrecen una ventana única hacia la naturaleza emergente de los sistemas complejos, un concepto que encuentra resonancia en campos tan variados como la biología, la informática, la física e incluso la economía, la sociología y demás ciencias sociales.

Lecciones Aprendidas

  • Simplicidad y Complejidad: Hemos visto cómo estructuras simples pueden dar lugar a patrones increíblemente complejos y variados. Esta es una poderosa metáfora de cómo sistemas simples pueden generar fenómenos emergentes en el mundo real. Por ejemplo, la complejidad de un individuo surge de la interacción y coevolución con otros individuos, su entorno socioeconómico-cultural y factores psicológicos y biológicos.
  • Exploración y Experimentación: La capacidad de experimentar con diferentes reglas y configuraciones nos recuerda la importancia de la exploración en la ciencia y la tecnología. Cada cambio en la regla puede revelar nuevos patrones y comportamientos, alentando una mentalidad de descubrimiento continuo. Además, logramos aprender cómo graficar celdas producto de una simulación, lo cual es una habilidad útil para trabajar con datos en general.

Referencias e Inspiración

Este artículo se basa en el trabajo de tres personas. Primero, el físico Stephen Wolfram, quien en 2002 publicó un libro titulado A New Kind of Science (Un Nuevo Tipo de Ciencia), en el cual introduce y explora los ACW y sus implicaciones en la ciencia y la tecnología. En segundo lugar, el economista y científico de la complejidad Gonzalo Castañeda, quien en 2021 publicó The Paradigm of Social Complexity (El Paradigma de la Complejidad Social): un libro que, en dos volúmenes, construye un marco teórico y práctico para el estudio de fenómenos socioeconómicos a través de conceptos como los Sistemas Adaptables Complejos, Autómatas Celulares y Modelos Basados en Agentes. Finalmente, al maestro Daniel Shiffman, quien con su canal de Youtube The Coding Train y su libro The Nature of Code (La Naturaleza del Código) me inspiró y ayudó a implementar este proyecto en R.

Espero sinceramente que hayas encontrado tanto valor como inspiración en el fascinante mundo de los Autómatas Celulares de Wolfram. Mi objetivo al compartir esta exploración en R no es solo presentar un tema interesante y complejo de manera accesible, sino también encender tu curiosidad y el deseo de experimentar más allá de lo que hemos explorado aquí. Animo a cada uno de ustedes a tomar lo que hemos aprendido y expandirlo, ya sea creando nuevas versiones, aplicando estos conceptos en diferentes contextos o explorando nuevas áreas que aún estén por descubrir. La belleza de la programación y la ciencia de datos radica en su naturaleza exploratoria, en cómo nos permite dar forma a nuestras ideas y teorías.