…esto no es un subtítulo…
2013-08-31
Hace varios artículos, planteamos un interesante proyecto: una pequeña biblioteca para construir autómatas celulares. Los autómatas celulares son unas estructuras matemáticas muy curiosas: retículos de celdas que van cambiando de un estado a otro y que pueden, a partir de reglas sencillas, exhibir complejísimos comportamientos emergentes. Como práctica, nuestra biblioteca estará hecha en Scheme R5RS y en Python 2. El enfoque es funcional porque el problema se presta mucho a ello. No nos preocuparemos tanto por hacer un código especialmente rápido como por hacerlo claro y conciso.
Seguimos construyendo funciones para hacer una biblioteca de autómatas celulares razonablemente general y práctica. Hoy vamos a crear funciones para trabajar con autómatas elementales. Ya estamos a punto de generalizar aquel trabajito que hicimos sobre la regla 90. Un autómata celular elemental es unidimensional (extendido de izquierda a derecha, por ejemplo), sus celdas solamente tienen dos estados (0 y 1, digamos) y sus vecindarios consisten en la celda inmdiatamente a la izquierda, la propia celda y la celda inmediatamente a la derecha. Las normas quedan escritas de forma muy compacta mediante lo que se conoce como la notación de Wolfram. Parece que lo único que tenemos que preparar es la extracción de los vecindarios (algo fácil gracias a la función cyclic-cartesian- que creamos en el anterior artículo) y la generación de normas (también fácil gracias a la función deterministic-rule que creamos hace unos pocos artículos). Antes de hacer eso, con el fin de facilitarnos nun poco la vida, prepararemos unas pocas funciones auxiliares más.
En Python 2, podemos construir cómodamente una lista de tamaño
n cuyos elementos son todos un cierto objeto o
de la siguiente manera:
n * [o]
Scheme R5RS no tiene el equivalente a esto, pero nos convendrá
disponer de ello. Podemos improvisarlo de varias maneras; aquí
haremos uso de nuestra función unfold para crear una
función repeat que acepta un objeto object y el
número times de veces que aparecerá repetido en una lista:
(define (repeat object times)
(unfold (lambda (n) (< n 1))
(lambda (n) object)
(lambda (n) (- n 1))
times))
Resulta que la operación de extracción de bits es similar a la
operación de paso de índices planos a índices multidimensionales
en un espacio en el que todas las dimensiones tienen tamaño 2 y el
número de dimensiones es igual al número de bits. En general, la
extracción de dígitos es igual que el paso de índices planos a índices
multidimensionales en un espacio en el que todas las dimensiones
tienen como tamaño la base y el número de dimensiones es igual al
número de dígitos a extraer. Podemos definir una función de
extracción de dígitos general integer->digits que acepta un
número number, una base base y el número de
dígitos a extraer number-of-digits; y devuelve la lista de
dígitos:
(define (integer->digits number base number-of-digits)
(flat->multidimensional (repeat base number-of-digits) number))
La extracción de bits es un caso particular:
(define (integer->bits number number-of-bits)
(integer->digits number 2 number-of-bits))
El número de dígitos a extraer es algo importante porque los ceros
a la izquierda sí son significativos en la especificación de las
normas de nuestros autómatas elementales.
El código anterior de Scheme tiene una traducción inmediata a
Python:
def integer_to_digits(number, base, number_of_digits):
return flat_to_multidimensional(number_of_digits * [base], number)
def integer_to_bits(number, number_of_bits):
return integer_to_digits(number, 2, number_of_bits)
La operación inversa a la anterior puede ser útil. Es análoga al
paso de índices multidimensionales a índices planos. Definimos la
función digits->integer que funciona a la inversa de
integer->digits, pero no necesita que le pasemos el número
de dígitos:
(define (digits->integer digits base)
(multidimensional->flat (repeat base (length digits))
digits))
Como antes, el paso de bits a entero bits->integer es
una particularización:
(define (bits->integer bits)
(digits->integer bits 2))
La versión en Python de todo esto es similar:
def digits_to_integer(digits, base):
return multidimensional_to_flat(len(digits) * [base],
digits)
def bits_to_integer(bits):
return digits_to_integer(bits, 2)
Vamos a construir la función extractora de vecindarios. Nuestros
vecindarios contendrán la celda situada a la izquierda, la celda
actual y la celda situada a la derecha. Esto es muy fácil de
conseguir gracias a la función
cyclic-cartesian-neighbourhoods que creamos en el
anterior artículo:
(define (cyclic-elementary-neighbourhoods cells)
(cyclic-cartesian-neighbourhoods cells
(list (length cells))
'((-1) (0) (1))))
Es igual en Python:
def cyclic_elementary_neighbourhoods(cells):
return cyclic_cartesian_neighbourhoods(cells,
[len(cells)],
[[-1], [0], [1]])
Los parámetros de la función
cyclic-cartesian-neighbourhoods son un poco engorrosos de
pasar cuando trabajamos con autómatas unidimensionales: la dimensión
va en una lista y cada posición relativa de los vecinos también va en
una lista. Es así como hicimos la función y hay que quererla igual,
pero este aspecto es mejorable con poco esfuerzo.
La función cyclic-elementary-neighbourhoods acepta como argumento una lista de celdas cells y devuelve los vecindarios de autómatas elementales correspondientes con la restricción de que se supone que el autómata se repite periódicamente y la lista de celdas contiene un periodo.
El código de Wolfram establece una forma muy compacta para describir reglas de autómatas celulares elementales. Cada norma está caracterizada por un número entero entre 0 y 255. Si asociamos cada uno de los 8 posibles estados de los vecindarios elementales de una celda (dispuestos en un determinado orden que veremos más adelante) con los bits de la expansión binaria de 8 bits del número de norma, nos sale una tabla de correspondencias entre el estado del vecindario y el próximo estado de la celda, es decir, la descripción de una norma determinista. Podemos crear las normas de esta manera:
((1 1 1) (1 1 0) (1 0 1) (1 0 0) (0 1 1) (0 1 0) (0 0 1) (0 0 0))
.
Todo esto queda plasmado en el siguiente código de Scheme:
(define (wolfram-rule rule-number)
(define patterns (map (lambda (integer) (integer->bits integer 3))
(reverse (range 0 8 1))))
(define states (integer->bits rule-number 8))
(define table (zip patterns states))
(deterministic-rule table))
La versión en Python es igual.
def wolfram_rule(rule_number):
patterns = map(lambda integer: tuple(integer_to_bits(integer, 3)),
range(0, 8, 1)[::-1])
states = integer_to_bits(rule_number, 8)
table = dict(zip(patterns, states))
return deterministic_rule(table)
Esta función acepta un número de regla rule-number y
devuelve la regla correspondiente según el código de Wolfram.
Categorías: Informática
Permalink: https://sgcg.es/articulos/2013/08/31/jugando-con-automatas-celulares-9/