« NetLogo: Fundamentos « || Inicio || » NetLogo: Estructuras … »

Usando Patches de NetLogo para modelar Autómatas Celulares

Última modificación: 30 de Octubre de 2016, y ha tenido 2999 vistas

Etiquetas utilizadas: || ||

Gracias a la similitud existente entre la distribución topológica de los patches que forman el mundo bidimensional de NetLogo y la distribución de las celdas de un autómata celular 2D, nos centraremos en esta entrada en analizar la forma más sencilla y directa de modelar un autómata celular 2D haciendo uso de patches.

Como el resto de los agentes que proporciona NetLogo, los patches tienen una serie de propiedades inherentes que los definen (más adelante añadiremos propiedades a esta lista que NetLogo trae por defecto). Para acceder a esta lista de propiedades por medio del interfaz basta hacer clic con el botón derecho sobre el agente en cuestión, y seleccionar la inspección del agente. Si no hemos definido más propiedades en los patches, debemos obtener algo similar a la imagen que se muestra a continuación:

Donde se muestran las propiedades básicas que tienen los patches, que son:

  • pxcor: que determina su coordenada \(x\) (horizontal).
  • pycor: que determina su coordenada \(y\) (vertical).
  • pcolor: que determina su color.
  • plabel: si es no vacía, determina el texto que usará como etiqueta.
  • plabel-color: que determina el color usado para la etiqueta.

Debido a que estamos trabajando con un mundo bidimensional, y que los patches son estáticos, bastan dos coordenadas para poder identificar cada patch en el mundo. Para tener una idea más exacta del sistema coordenado que usa NetLogo, así como de la posibilidad de cambiar las características del mundo, se aconseja la lectura de los fundamentos de programación de NetLogo, donde se puede encontrar información adicional sobre temas puntuales del lenguaje.

En la siguiente imagen podemos ver los patches de un posible mundo (centrado en el origen) y en el que se han dibujado los ejer coordenados y se han etiqueta los patches con sus coordenadas.

Como hemos indicado anteriormente, los patches están completamente determinados si conocemos sus dos coordenadas. Por ello, una de las formas que hay para trabajar con ellos es precisamente indicando ambas coordenadas. Esto es, patch x y hace referencia al patch que ocupa las coordenadas \((x,y)\) del mundo. Si queremos que ese patch realice alguna acción, basta con pedírselo de la siguiente forma:

ask patch x y [ lista-de-acciones ]

El comando ask de NetLogo es uno de los comandos más importantes del lenguaje, ya que es el que nos permite (como observadores) pedirle a un conjunto de agentes que realicen una sucesión de acciones:

ask conjunto-de-agentes [ lista-de-acciones ]

Por ello, conviene entender adecuadamente cómo funciona este comando. Idealmente, la ejecución de este comando provocaría que todos los agentes referenciados realicen las acciones de manera simultánea, pero la realidad es ligeramente diferente. Debido a que el paralelismo es un problema computacional de una envergadura considerable (no solo por los problemas técnicos que conlleva, sino por los problemas teóricos de resolver cómo se resuelven factores como la concurrencia, es decir, ¿qué hacer si dos acciones se intentan realizar simultáneamente y son incompatibles entre si?) y a que NetLogo no es realmente paralelo, no entraremos en detalle acerca de qué soluciones se han propuesto de manera general, sino que analizaremos únicamente qué solución han tomado los diseñadores de este lenguaje para evitarlo. En realidad, lo que hace NetLogo es evitar la ejecución simultánea y realizar una ejecución ordenada de la lista-de-acciones yendo agente por agente, pero como lo habitual es que estas acciones se repitan una y otra vez, y para no dar preferencia a ninguno de los agentes frente al resto, en cada ejecución del comando ask se reordenan los agentes de forma aleatoria y se considera ese orden en la ejecución.

Así, por ejemplo, si quisieramos que el patch que está en las coordenadas \((0,0)\) mostrara como etiqueta su coordenada \(x\) bastaría escribir algo así:

ask patch 0 0
[ set plabel pxcor ]

Además, en el código anterior también vemos cómo usar el comando set para asignar un nuevo valor a una propiedad. En general, por medio de una instrucción del tipo:

set var val

asignamos el valor \(val\) a la variable \(var\) (que debe existir), perdiendo el valor anterior que dicha variable tuviera. Por supuesto, todas las propiedades de los agentes se comportan como variables particulares de cada uno de los agentes, por lo que podemos modificarlas a nuestro antojo, y aunque se llamen igual, modificar el valor de una propiedad de uno de los agentes no afecta al valor de esa misma propiedad en el resto de los agentes existentes.

Evidentemente, lo habitual será pedirle a todo un conjunto de patches que hagan algo. Para referenciar el conjunto de todos los patches, NetLogo proporciona la palabra reservada patches, de forma que si quisieramos que la acción anterior la realizaran todos los patches del mundo bastaría escribir:

ask patches
[ set plabel pxcor ]

Si disminuimos la velocidad de ejecución del comando anterior lo suficiente (en la pestaña de interfaz la herramienta proporciona un control deslizante para modificar la velocidad de ejecución), veremos cómo no es una acción simultánea para todos los patches, sino que se van realizando uno a uno siguiendo un orden aleatorio (para comprobar que es aleatorio, además de creer a los diseñadores del sistema, basta ejecutarla varias veces y ver que el orden cambia).

¿Cómo podemos conseguir el resultado de ajedrez mostrado en la figura anterior? Teniendo en cuenta que lo que cambia en los patches de un color respecto del otro es simplemente la paridad de (pxcor + pycor), lo que necesitamos es una estructura condicional que nos permita diferenciar en qué caso se encuentra el patch actual. Como es habitual en la mayoría de los lenguajes de programación, NetLogo proporciona estructuras de tipo if que permiten realizar distintas acciones según la verificación de una condición. Su formato es:

if-else condicion
  [ acciones-Sí ]
  [ acciones-No ]

y disponemos de una versión reducida en caso de que no haya nada que ejecutar si la condición no se verifica:

if condicion
  [ acciones-Sí ]

En consecuencia, una posible solución al problema planteado vendría dada por el siguiente código:

ask patches
  [
    ifelse (pxcor + pycor) mod 2 = 0
    [ set pcolor black ]
    [ set pcolor white ]
  ]

Como veremos en breve, el hecho de que la ejecución de los comandos no sea simultánea, sino secuencial recorriendo la familia de agentes involucrados, hará que haya que tomar algunas precauciones a la hora de diseñar nuestras simulaciones de autómatas celulares, ya que tenemos que simular el paralelismo "real" que éstos tienen.

Al igual que los distintos tipos de agentes disponene de propiedades específicas que los definen e individualizan, también disponen de una colección de métodos que les permiten obtener información de su entorno o interactuar con los elementos que le rodean. A continuación veremos los métodos más habituales que NetLogo trae predefinidos para los patches.

¿Cómo puede un patch saber lo que ocurre a su alrededor?

Los patches que rodean a un patch dado es lo que se denomina su vecindad. En 2D es habitual considerar dos tipos de vecindades: los 4 patches que lindan con él siguiendo las direcciones principales (llamada vecindad de von Neumann), o bien los 8 patches que lo rodean (vecindad de Moore). En la siguiente figura se muestra la vecindad de von Neumann del patch \((-2,2)\), y la vecindad de Moore del patch \((2,2)\) destacadas de azul.

NetLogo proporciona dos procedimientos de patch (es decir, que los pueden ejecutar los agentes de este tipo) para obtener los vecindarios anteriores, que se denominan, respectivamente, neighbors4 y neighbors. Cuando un patch ejecuta uno de estos procedmientos recibe como respuesta el conjunto de patches que pertenecen a su vecindario, por tanto, para conseguir el efecto de la figura anterior deberíamos ejecutar un código como el siguiente:

ask patch -2 2 
  [
  ask neighbors4 [set pcolor blue]
  ]

Ya estamos en condiciones de construir nuestro primer autómata celular, y vamos a comenzar por construir el autómata del Juego de la Vida de Conway.

Recordemos que en este autómata las reglas que rigen la evolución de las celdas son:

  • Una celda viva con dos o tres vecinas vivas, sobrevive (supervivencia).
  • Una celda muerta con exactamente tres vecinas vivas, recupera su estatus de celda viva (nacimiento).
  • En todos los demás casos una celda muere o permanece muerta.

Vamos a representar las celdas vivas de color negro, y las celdas muertas de color blanco. Para poder modelar este autómata hemos de ser capaces de contar el número de celdas vecinas alrededor de cada patch que están vivas (es decir, que tienen color negro). Para ello hemos de realizar dos operaciones: por una parte hemos de ser capaces de diferenciar aquellos vecinos que tienen color negro, y por otra parte contar el conjunto de agentes que lo verifican. Para la primera operación, filtrar de un conjunto de agentes aquellos que verifican una condición, NetLogo proporciona el modificador with que se usa de la siguiente forma:

conjunto-agentes with [ condición ]

que devuelve el subconjunto de agentes que verifican la condición. Por tanto, en nuestro caso, los vecinos que nos interesa contar son:

neighbors with [pcolor = black]

Para contarlos, basta aplicar el operador count que, sobre un conjunto de agentes, devuelve el tamaño del conjunto:

count neighbors with [pcolor = black]

Veamos una posible solución a cómo sería un paso de ejecución del autómata, y después analizaremos las novedades que se hayan introducido y la características de la solución dada:

ask patches
  [
    let aux count neighbors with [pcolor = black]
    ifelse pcolor = white and aux = 3
    [ set pcolor black]
    [
      ifelse pcolor = black and aux = 2 or aux = 3
      [ set pcolor black]
      [ set pcolor white]
    ]
  ]

Desde el punto de vista de programación, la única novedad introducida es la creación de una variable auxiliar, usando el comando let, para almacenar el número de vecinos vivos del patch. A diferencia del comando de asignación que ya vimos (set), el comando let le indica al NetLogo que estamos definiendo una nueva variable y el valor inicial que queremos que tenga. Si posteriormente, en este mismo trozo de código, quisiéramos modificar su valor, deberíamos hacer uso de la instrucción set.

El resto del código refleja, haciendo uso de lo que hemos aprendido a lo largo de esta entrada, las condiciones de evolución del autómata de manera fiel. La repetición continua del código anterior producirá la evolución del autómata en los diversos pasos por los que queramos simularlo.

Pero... ¿estamos seguros de que el código anterior hace lo que queremos hacer?

La primera sensación cuando uno lo ve es que el código es correcto, pero hay un importante detalle que hemos pasado por alto... y es que el método elegido sería correcto si todos los patches (que son nuestra celdas) trabajaran en paralelo, simultáneamente, y lo que vieran en sus vecinas fuese su "estado" anterior congelado en el tiempo. Pero recordemos que no es ese el caso, sino que NetLogo simula la ejecución paralela por medio de una ejecución secuencial de orden aleatorio y, en consecuencia, lo que un patch puede estar mirando en sus vecinas en el momento en que le toca ejecutarse puede ser ya el "nuevo" estado de algunas de ellas, dependiendo del orden que hayan ocupado en esa ordenación aleatoria.

¿Cómo podemos resolver este problema?

La solución a este problema es sencilla, basta definir una propiedad más en cada patch que permita mantener el estado en que se encuentra y que no se modifique hasta que todos los patches hayan acabado de realizar la tarea. De nuevo, NetLogo viene a ayudarnos con esta tarea y nos permite crear propiedades nuevas con muy poco esfuerzo, para ello basta poner el principio del código una instrucción como la siguiente:

patches-own
  [
  propiedad1
  propiedad2
  ...
  ]

que nos permite añadir todas las propiedades adicionales que queramos a los patches, y con las que se trabaja de igual forma a las propiedades que por defecto trae el sistema para ellos.

En nuestro caso, añadiremos una propiedad llamada estado_nuevo que nos permitirá almacenar el estado nuevo que debe tomar cada patch sin necesidad de modificar su estado actual. Posteriormente, por medio de otra petición al conjunto de todos los patches, podremos actualizarlos sin miedo a que afecte al cálculo real de los estados de los demás. El código sería entonces:

patches-own [estado_nuevo]

to life
  ask patches
  [
    let aux count neighbors with [pcolor = black]
    ifelse pcolor = white and aux = 3
    [ set estado_nuevo black]
    [
      ifelse pcolor = black and aux = 2 or aux = 3
      [ set estado_nuevo black]
      [ set estado_nuevo white]
    ]
  ]
  ask patches
  [
    set pcolor estado_nuevo
  ]
end

Observa las dos fases que hemos programado: en la primera fase, que está determinada por la primera petición a todos los patches, se calcula el nuevo estado que cada patch debe tener, mirando el estado actual que tienen (y que almacenamos en el color de cada patch); y en la segunda fase, correspondiente a la segunda petición, se actualizan todos los estados actuales siendo sustituidos por los estados nuevos que se han calculado en la fase anterior.

Con esto hemos desarrollado todo el núcleo de un motor que sea capaz de simular el juego de la vida, pero para que sea realmente útil debemos crear un estado inicial (una configuración del autómata celular con la que empezar) y proporcionar los botones en el interfaz para que sea más cómo de usar para el usuario, para que no haya que ir ejecutando el procedimiento life paso a paso.

Para resolver el problema del estado inicial vamos a crear un procedimiento que genere una configuración aleatoria en la que cada patch tenga la misma probabilidad de ester viva o muerta. Casi todos los lenguajes de programación proporcionan funciones para generar números aleatorios (pseudo-aleatorios), y entre las que NetLogo proporciona encontramos random, que recibe como entrada un número natural, n, y devuelve de manera aleatoria y uniforme un número natural en \(\{0,\dots,n-1\}\). El procedimiento que buscamos podría ser:

to configuracion-inicial
  ask patches
  [
    ifelse random 2 = 1
    [ set pcolor black ]
    [ set pcolor white ]
  ]
end

Para conseguir la interfaz de usuario adecuada, añadiremos un botón que ejecute el procedimiento configuracion_inicial y otro, con la opción Forever marcada, que ejecute el procedimiento life, tal y como vemos en la figura siguiente:

En este caso hemos implementado las reglas de un autómata celular 2D concreto, el Juego de la Vida, que tiene unas reglas muy sencillas al ser totalístico (solo depende del número total de vecinas vivas, no de dónde están situadas concretamente) y proporciona comportamientos muy interesantes. Modificando las reglas locales podemos implementar en muy pocas líneas y fácilmente cualquier autómata celular bidimensional (o 3D si usamos NetLogo3D, o 1D si únicamente consideramos vecindades en una dirección.

« NetLogo: Fundamentos « || Inicio || » NetLogo: Estructuras … »