This is an old revision of the document!
Table of Contents
ULAM demos: Coevolution
Evolution can sometimes be seen as occurring between a population of creatures and a fixed environment. Some creatures do better than others and proliferate, and over time a population emerges that is increasingly well-adapted to that environment. Then further innovations typically become smaller and rarer and the system's dynamics tends toward a fixed point.
But just as often, the “environment” is a moving target, because it contains other populations of creatures that are also evolving. In such circustances the resulting coevolutionary system dynamics can quite complex, as the evolutionary innovations of one species are countered by evolutionary counter-innovations by another species.
In 2002, Dave Ackley developed a project called Huegene for a C++ programming course. The coevolutionary model demonstrated here is a variation of the original Huegene, using RGB color encoding while the original Huegene used HSV. (Also, perhaps ironically, the primary pedagogical purpose of Huegene was to learn about discrete event simulation, but that is all part of the fundamental machine architecture in ULAM and the MFM, Huegene in ULAM is a much simpler project than it was in C++!)
The Huegene model
Suppose there are two species of creatures, the Plants and the Herbivores, existing in a two-dimension grid of sites that each can hold one creature. Plants are immobile, but they spontaneously grow over time and will split into empty adjacent sites if they are large enough. The Herbivores move randomly and attempt to eat any Plants they stumble over. Herbivores must eat enough Plants to avoid starving to death, and if they accumulate enough energy they can produce an offspring Herbivore in a neighboring empty site. Plant vs Herbivore
The Plants have genetic information that allows them to appear as any (24 bit RGB) color. Any given Plant has a fixed color for its whole lifetime, but if it manages to produce an offspring Plant, the child's color will be slightly modified – “mutated”, completely at random – compared to the parent. Regardless of their color, all Plants are equally efficient at growing and having offspring plants, and so on – in a world consisting only of Plants their colors are completely irrelevant to their evolutionary success.
For their part, the Herbivores also have a genetically-specified color, that determines what color Plants they are most likely to eat successfully. So a “well-adapted” Herbivore will have a color closely matching that of the nearby Plants, while a “well-adapted” Plant will have a color that's far (in the absolute distance in RGB) from the nearby Herbivores.
If we put a single Plant into an empty grid, it will gradually produce a population of Plants around it, with increasingly mutated colors the further we get from the original “Ancestor”. What will happen if we then start dropping Herbivores near the Plant population, until we get one that “catches” – that can eat the available Plants successfully enough to produce offspring?
The video at right shows one possible result, run for several hours of real time and displayed so that about 90 events happen at each site during each displayed second.
ULAM source code
The ULAM source, not surprisingly, consists of two files: Plant.ulam
and Herbivore.ulam
. The code was not explicitly designed for teaching or to be as simple as possible, but it is presented here in its entirety so people can play around with it as they like.
- Plant.ulam
/** \symmetries all */ element Plant { typedef Plant Self; typedef Unsigned(8) Channel; typedef Channel ARGB[4]; typedef Channel RGB[3]; typedef Unsigned(3) Energy; EventWindow ew; Random random; Once once; RGB color; Energy energy; constant Int cMUTATION_DISTANCE = 7; Channel mutate(Channel cur) { return (Channel) (cur + random.between(-cMUTATION_DISTANCE,cMUTATION_DISTANCE)); } Void behave() { if (once.new()) { for (Int i = 0; i < 3; ++i) color[i] = (Channel) random.between(Channel.minof,Channel.maxof); } ++energy; if (!ew.isLive(1)) return; if (energy == energy.maxof && ew[1] is Empty) { energy = Energy.maxof / 2 - 1; Self kid = self; for (Int i = 0; i < 3; ++i) { kid.color[i] = mutate(kid.color[i]); } ew[1] = kid; } } ARGB getColor(Unsigned selector) { ARGB ret; ret[0] = 0xff; ret[1] = color[0]; ret[2] = color[1]; ret[3] = color[2]; return ret; } }
- Herbivore.ulam
/** \symmetries all */ element Herbivore { typedef Herbivore Self; typedef Unsigned(8) Channel; typedef Channel ARGB[4]; typedef Channel RGB[3]; typedef Unsigned(7) Hunger; EventWindow ew; Random random; Once once; RGB color; Hunger hunger; constant Hunger cEAT_PER_ENERGY = 6; constant Int cMUTATION_DISTANCE = 10; Channel mutate(Channel cur) { Int mutOdds = cMUTATION_DISTANCE; return (Channel) (cur + random.between(-mutOdds,mutOdds)); } Bool isEdible(RGB us, RGB them) { Unsigned sad; for (Int i = 0; i < 3; ++i) { Int diff = (Int) us[i] - (Int) them[i]; if (diff < 0) diff = -diff; sad += (Unsigned) diff; } return random.oneIn(sad/50u); } Void behave() { if (once.new()) { hunger = hunger.maxof / 2 + 1; for (Int i = 0; i < 3; ++i) color[i] = (Channel) random.between(Channel.minof,Channel.maxof); } if (++hunger == hunger.maxof) { Empty e; ew[0] = e; return; } if (!ew.isLive(1)) return; if (ew[1] is Plant) { Plant p = (Plant) ew[1]; if (isEdible(color, p.color)) { hunger -= (Hunger) (p.energy * cEAT_PER_ENERGY); if (hunger == hunger.minof) { Hunger give = (Hunger) (3 * hunger.maxof / 4 + 1); hunger = give; Self kid = self; hunger = (Hunger) (Hunger.maxof - give); for (Int i = 0; i < 3; ++i) { kid.color[i] = mutate(kid.color[i]); } ew[0] = kid; } } else { Empty e; ew[0] = e; } ew[1] = self; } else if (ew[1] is Empty || ew[1] is Self) { ew.swap(0,1); } } ARGB getColor(Unsigned selector) { ARGB ret; ret[0] = 0xff; if (selector==2u) { ret[1] = color[0]; ret[2] = color[1]; ret[3] = color[2]; } else { ret[1] = 0; ret[2] = 0; ret[3] = 0; } return ret; } }
How the video was made
«TO COME BY REQUEST»