Animates the evolution of an Abelian Sandpile on a finite two-dimensional integer lattice over discrete timesteps.
The sandpile is only one instance of an entire class of cellular automata which this project supports.
The rendering and control logic is defined such that it correctly handles a generic two-dimensional cellular automaton of arbitrary finite size, so long as its transition function does not modify anything but the cell states (see Structure and CellularAutomaton.ts
for more details).
Project is live at: https://jamais-vu.github.io/sandpile/. Check it out!
I became interested in the Abelian Sandpile Model and other cellular automata after finishing my first JavaScript project, an interactive Game of Life. The sandpile has a more complex transition function, and I wanted to see how much better I could do now that I had some experience with JavaScript.
I chose TypeScript because I had never used it before, and learning it felt like a natural next step after becoming comfortable with JavaScript. I'm very happy with that decision; TypeScript is great, it forces me to write clearer code and prevented many errors that would have otherwise occurred had I used JavaScript.
I also wanted to familiarize myself with mocha
and chai
, and get in the habit of unit testing my project code as I write it.
This section may be skipped if you're not interested in the specific dynamics of the sandpile. My implementation of the transition function is in /rules/sandpile.ts
.
The Abelian Sandpile Model is a cellular automaton consisting of an n x m grid of cells. Each cell has an associated non-negative integer (the state of the cell) representing how many grains of sand are on that cell.
Each cell has up to four neighbors: the cells directly above, left, right, and below it.
Neighbors of the cell (x, y):
+------------+------------+------------+
| | (x, y + 1) | |
+------------+------------+------------+
| (x - 1, y) | (x, y) | (x + 1, y) |
+------------+------------+------------+
| | (x, y - 1) | |
+------------+------------+------------+
A cell on a side of the grid has three neighbors, and a cell on a corner of the grid has two neighbors. (This is in contrast to Game of Life, which defines the neighbors of a given cell to be all eight adjacent cells.)
In addition to the grid of cells, the Sandpile also has a transition function. The transition function uses the current (time t) state of each cell in the grid to determine the next state of the cell.
-
We add one grain to an arbitrary cell of the grid.
-
If every cell in the grid is stable (has at most three grains), then the grid as a whole is stable. In this case, the state transition is complete, and we are in the resultant state.
-
If at least one cell in the grid is unstable (has four or more grains), then the grid is unstable.1
-
We pick an arbitrary unstable cell2 and topple it: reduce its grain count by four, and add one grain to each of its neighbors. For cells which have four neighbors, this process does not alter the total count of grains in the grid; however, for cells on the sides or corners of the grid this process decreases the total count of grains in the grid. They "fall off" the edges.
-
When we topple an unstable cell, the grain added to each neighbor may make one or more of those neighbors also unstable. We topple these newly-unstable cells (an avalanche), which in turn may produce even more unstable cells, and so on.
-
After all that toppling, the grid will eventually be stable3. Then the transition function has finished.
Click to expand transition function footnotes
1: Technically, when we reach part (3) for the first time, there is at most one cell which could be unstable: the cell we added a grain to.
2: The final stable state of the grid is not dependent on the order in which we topple unstable cells; i.e. toppling of cells is commutative (see Theorem 2.1 of ref [1] if you want a proof of this). In other words, a grid has only one possible final stable state, and this state is uniquely-determined as soon as the transition function adds one grain to the grid, before even a single unstable cell has been toppled.
3: (TODO: It's intuitive to me but I'd like a simple proof to include here.)
So the grid is stable at each timestep — even though it may have contained an extremely-large number of unstable cells during the transition, the states it moves through during the transition are not "at" a timestep.
The application's entry point, script.ts
, sets up the canvas and a Controller
. The Controller
then handles all of the application's logic, according to a Presentation-Abstraction-Control (PAC) framework:
User Interface
|
| UIEvents trigger EventListeners.
| EventListeners trigger Controller method
| calls.
|
v
------- Controller -------
Calls step methods. | | Passes CellularAutomaton
Gets grid/cells | | state to Drawing, to
states. | | draw on canvas.
v v
CellularAutomaton Drawing
(Abstraction) (Presentation)
- Presentation:
Drawing
class- Renders gridlines and cell states on the canvas.
- Only thing it "touches" outside itself is its given canvas rendering context.
- Abstraction:
CellularAutomaton
class- Handles transition logic of the cellular automaton.
- Manages the complete history of the cellular automaton's state.
- Control:
Controller
class- Interface between the UI and the Presentation+Abstraction.
- Class methods are called by
EventListeners
added inscript.ts
. - Calls
CellularAutomaton
class methods and getsCellularAutomaton
state data - Passes
CellularAutomaton
state data toDrawing
class methods.
The /rules
directory contains rules for specific cellular automata. Each /rules
module exports a Rules
object (defined in /util/types.ts
, which contains the information necessary to define and draw a specific cellular automaton:
transitionFunction
- its transition functioncellColors
- a Map defining which cell states correspond to which colorsmaxInitialCellValue
- whether cell states are initially 0 or randomly-populated from a range of integers
At creation, the static method Controller.createControllerFromRules()
is passed a Rules
object, as well as the canvas and size of cells to draw, and uses these to create a Controller
which has:
- a
CellularAutomaton
on a grid fitted to the canvas, with the given transition function and initial cell states. - a
Drawing
which uses the coloring rules of the specific cellular automaton.
This means changing which cellular automaton this application uses is as simple as changing which Rules
we pass the Controller
at initialization — the Controller
automatically handles everything else.
You can view the animation on the web, or clone this repository and host your own HTTP server.
-
By default,
script.ts
initializes the sandpileCellularAutomaton
with a transition function which adds new grains to the center of the grid (rather than to a random cell). -
The demo starts paused.
-
Jumping forward several thousand steps at once may take a few seconds, and jumping forward more than 10,000 steps at once may cause it to stop responding (I hope to make that faster in the future).
- View the sandpile animation at this project's GitHub Pages deployment.
- The application is also live at Replit, but that one is not guaranteed to be up-to-date.
Run this simulation on your computer by cloning the repository and hosting index.html
on a local server.
Expand this if you want to host it locally, but are unfamiliar with how to do so.
I use the NodeJS package http-server
, because it's very simple.
# In the directory where you want to save this repository
$ git clone https://github.com/jamais-vu/sandpile
# If you don't have the http-server package.
$ npm install http-server
# Navigate to the sandpile directory.
$ cd sandpile
e
# Start the HTTP server. It automatically uses 'index.html'
# The '-c-1' option disables caching (useful if you are editing the web page).
$ http-server -c-1
# When you are done, you can stop the HTTP sever by pressing Ctrl+C.
After starting the server, navigate your web browser to http://localhost:8080/index.html and you're done.
Some cool ideas I may implement in my spare time.
Since toppling a cell in the grid only affects that cell's immediate neighbors, we can partition the grid into several subsets of cells, where any two cells in the same subset are independent (do not share neighbors). This means that, for each subset, we can simultaneously topple all cells in the subset without worrying about potential conflicts from concurrent topple processes accessing or modifying the same neighbor.
Partition of a 5 x 5 grid. No two cells of the same color share a neighbor.
(Image credit: P. Vahagn, P. Suren, and N. Hayk [2])
I want to do this because it's a neat solution, and I'd like to become more familiar with parallel programming.
-
createVertexGroups
, the function for creating independent subsets, passes tests. Have been experimenting with breaking up the transition functions to accept subsets of a grid, but I'll need to figure out how to call them in parallel to test them. -
Plan to check out
Promise
. I'm not clear on whether that's truly "parallel" but it may suffice. May also play around with Web Workers.
Symmetries of the Abelian Sandpile
When adding grains to the center, the sandpile exhibits reflection symmetry over the axes, and (on a square grid) fourfold rotational symmetry. This means we only need to consider the origin, positive x and y axes, and one quadrant; then we can transform the quadrant to create the remainder of the sandpile. This would effectively cut down the drawing iteration by 3/4, and possibly offer optimizations for computing transitions.
- Not started.
Instead of a two-dimensional integer lattice, where each cell has 4 neighbors, the Sandpile is represented by an undirected finite multigraph, where each vertex has an arbitrary number of neighbors. A vertex in the graph topples if its grain count is equal to or greater than its number of neighbors.
Unlike the grid, there is no "edge" for grains to fall off and be lost from the sandpile, so a single vertex is chosen to be the "sink" and does not topple no matter how many grains are placed on it.
-
Basic recursive implementation seems to work for very small graphs. For any graphs of an interesting size, it quickly exceeds the maximum call stack size. I like how concise the recursive implementation is, but I'm not sure there's a nice solution to avoid hitting the call stack limit, so I'm working on an iterative implementation.
-
After I test that it's working, I'd like to visualize the vertices and edges, and animate transitions. That sounds like a good challenge; more complex than drawing a bunch of rectangles.
[1] N. Doman, "The Identity of the Abelian Sandpile Group", Bachelor Thesis in Mathematics, Faculty of Science and Engineering, University of Groningen, Groningen, Netherlands, 2020. Available: https://fse.studenttheses.ub.rug.nl/21391/.
[2] P. Vahagn, P. Suren, and N. Hayk, "The Investigation of Models of Self-Organized Systems by Parallel Programming Methods Based on the Example of an Abelian Sandpile Model", in Proceedings of CSIT 2013: 9th International Conference on Computer Science and Information Technologies, 23-27 September, 2013, Yerevan, Armenia. Available: https://csit.am/2013/proceedings.php.