The world’s leading publication for data science, AI, and ML professionals.

Dancing With the Stars

Exploring Emergent Behavior From Simple Computational Rules

Photo by Allef Vinicius on Unsplash
Photo by Allef Vinicius on Unsplash

To humans, stars are among the most reliable facts of life. Our main star, the Sun, was there on the day we were born and will be there on the day we die. And the same is true for almost every other star we can observe in the evening sky. Yes, the occasional star may go "poof" as a supernova, but by and large, the stars in the sky do not change much during our lifetimes. You can set your clocks by them, literally.

On longer timescales, stars get around though. Every star is constantly nudged gravitationally by a tiny amount by all the stars around it. Stars that are close have a large effect, but huge amounts of stars farther away also play a role. And even mysterious "dark matter" affects how stars move relative to one another.

A few years ago, astrophysicist & research programmer at Wolfram|Alpha, Jeffrey Bryant, wrote a blog post about a really interesting simulation of colliding galaxies with each galaxy containing billions of stars. That post includes the complete Wolfram Language code to recreate the full simulation shown here:

Colliding galaxies are an interesting example of a type of emergent behavior, where tiny individual objects respond to typically small outside influences. Another good example of this behavior is a flock of birds, where each bird individually responds to changes in the flight direction of the birds around itself.

Photo by Yasong Zhou on Unsplash
Photo by Yasong Zhou on Unsplash

Simulating some of these emergent behaviors can be done with surprisingly simple code in the Wolfram Language. My Wolfram Language example is loosely based on a Wolfram Community post by Simon Woods. It considers a number of objects on a flat surface, let’s call them particles, which interact with each other. Every particle has one "friend" and it always wants to move in the direction of this friend wherever it is on the plane. Every particle also has one "enemy" and it always wants to move away from this enemy. Then, every so often, a particle may randomly get a new friend and a new enemy. This helps to keep the emergent behavior interesting and dynamic. Finally, each particle wants to slowly drift to the center of the plane. This rule keeps the particles slowly drifting away from the center.

That’s a lot of rules, but simple to program in the Wolfram Language once you break it down. Let’s start by picking the number of particles and start with 1,000:

n = 1000

Next, we generate n random particles on the plane with coordinates in the range (-1,1) x ( -1,1):

x = RandomReal[{-1, 1}, {n, 2}]

Each particle has a friend and an enemy. We generate them in one go with the following line of code. The list p contains the list of friends for each particle and the list q contains the list of enemies:

{p, q} = RandomInteger[{1, n}, {2, n}]

For example, p may start with {621, 47, 874, …} which means that the first particle in x is friends with the 621st particle in x and so on.

The magnitude of the force of attraction or repulsion depends on the distance between a particle and its friend or enemy. The following function f is using a very compact Wolfram Language notation to calculate the forces for all particles in one go:

f = Function[{x, i}, (#/(.01 + Sqrt[#.#])) & /@ (x[[i]] - x)]

To update each particle for one time step, we use the following code. The first term 0.995 x represents the drift of the particles towards the center of the plane. The second term represents the attracting force, with a parameter of 0.02 specifying how strong the overall attraction is. The last term represents the repulsive force. It works the same as the attractive force, but in the opposite direction

x = 0.995 x + 0.02 f[x, p] - 0.01 f[x, q]

Finally, every time step runs the following conditional statement. It is True about 10% of the time and when that happens a single particle is assigned a new friend and enemy:

If[ RandomReal[] < 0.5,
 With[{r1 = RandomInteger[{1, n}]}, 
  p[[r1]] = RandomInteger[{1, n}];
  q[[r1]] = RandomInteger[{1, n}]
]]

To visualize the particles we use a basic Graphics object to display the particles as white points. The Dynamic expression detects if any variables change inside its scope and it re-evaluates if it does. It is essentially a smart while-true loop that you can place anywhere in rendered typeset code:

Graphics[{ White, PointSize[0.007],
 Dynamic[
  If[RandomReal[] < 0.1, 
   With[{r1 = RandomInteger[{1, n}]}, p[[r1]] = r; q[[r1]] = r]
  ];
  Point[x = 0.995 x + 0.02 f[x, p] - 0.01 f[x, q]]
 ]}, 
 PlotRange -> {{-1, 1}, {-1, 1}}, 
 Background -> Black,
 ImageSize -> 600
]

The end result can be seen directly in a Wolfram Notebook (it will play until you choose to stop it) or in the following one minute video that I created for YouTube:

It’s always quite amazing how a tiny amount of code can yield such intricate behavior. There are lots of interesting ways to create variations, which I hope to explore in a future post. For example, each particle can have multiple friends and enemies and, instead of rendering with simple white points, one could use arbitrary graphics (circles, polygons) to make things even more bizarre-looking.


Related Articles