Wave Function Collapse Algorithm in J
I have heard of the J programming language for a while, but I haven’t written any non-trivial program in J or any array languages. Recently I came across the wave function collapse algorithm that generates sophisticated image without any machine learning model. There are lots of arrays and parallelism in the algorithm, so I decided to implement it to learn a bit of J.
I’ll assume that the reader have read the J primer, just like me when I started this blog. I’ll explain everything in the code in the beginning and gradually reduce the explanation. Also, I’ll intentionally ignore advanced subjects like tacit programming in this post.
The Simplified Algorithm
The original algorithm is too complex to fit in a blog, therefore in this post I’ll implement a special case of the algorithm called ESTM by Robert Heaton. You should read his article and understand the algorithm first.
To make our life as J programmer easier, let me describe the problem and the algorithm with some math notations. 1
The problem: We have an input matrix . The elements of are from a finite set . From we can obtain some sets of pairs
The goal is to create a output matrix , where for every ,
The ESTM algorithm:
- Create a matrix whose elements are subsets of . Initially every element is .
- Repeat the following until every element of only has one element or some of them are empty:
- Choose an element in . We’ll discuss how to choose it in the following sections.
- Choose an element .
- The collapse step: set .
- The propagation step: set where and so on.
- If every element of has only one element (I’ll call such element as a singleton), then we get a valid by setting the element of as .
Theoretically, the algorithm doesn’t solve the problem because it will generate matrix that violates constraints in (1) under rare circumstances (see appendix A). A more serious issue is that in the propagation step, you have to think in individual elements and their neighbors, not the matrix as a whole, which makes it hard to implement in J. These two problems can be addressed by a slightly different algorithm where the propagation step is applied to all indexes, not just .
The algorithm to implement:
- Create a matrix whose elements are .
- For in :
- Choose an element in that is not singleton.
- Choose an element .
- The collapse step: set .
- The propagation step: create a new matrix where the element at index is
- If every element of is singleton, then we return a valid by setting the element of as . If there is empty set, then we reset to .
Some Intuition
Before we move on to implement the algorithm, I’d like to provide some intuition on these math symbols.
The rules of ESTM can be encoded into a graph. The node set is , and there is an edge from to if and only if a tile with type can be placed next to . are the edge set of 4 graphs that corresponds to 4 different directions. The following is an example of the downward graph extracted from a matrix:

During the propagation step, we look at the neighbors at the chosen index and use the graph to determine which type of tile can be placed there. If the chosen tile has type and is not in , then the tile below it can not have type . So is the set of valid types of tiles that can be place below the tile with type .
From Math to J
It’s fairly easy to write a J program once you have a clean representation of your problem, described in finite sets and matrixes.
The input image can be a text file that contains an integer matrix:
0 0 0 0 0
0 1 1 1 0
0 1 2 1 0
0 1 1 1 0
0 0 0 0 0
We can read the text file and get a matrix in J:
load 'files'
image =: 0 ". freadr 'image.txt'
NB. freadr reads a file
NB. 0 ". convert the string to an array with default value 0The subset of is represented by a binary list of elements, where is in the set iff the element is 1. The matrix can be a 3D array.
NB. Get `|T|`
max =: 1 + >./ , image
NB. , Convert the input image to a list
NB. >./ Gets the maximum value of a list
NB. 1 + and plus one
NB. Create the matrix `B'` given the shape of output image
NB. The new state is 1 everywhere since every tile can be in every state
new_state =: 3 : '(y, max) $ 1'
NB. , Concatenate the shape and maximum value
NB. $ 1 Create a matrix with 1We can use edge lists 2 to store the graph:
NB. Get the edge list of the input matrix given a direction
NB. Usage: x edge_list direction
edge_list =: 4 : ',/ |: > x; y|.x'
NB. |. Rotate x so the neighboring element moves to
NB. the original position
NB. > ; Creates an array whose first element is x and
NB. second element is y|.x
NB. |: Transpose the array. When seen as matrix, its
NB. element are the pair (value before rotation,
NB. value after rotation)
NB. ,/ Flatten the matrixBut an adjacency matrix is handy when we need since the row of the matrix is equivalent to . We can use matrix unit to construct the adjacency matrix. For example, if the edge set is , then the adjacency matrix is the sum of matrix units .
NB. Generate a matrix unit E_{ij} given index and shape
NB. Usage: (i,j) unit (shape_x,shape_y)
unit =: 4 : '(i.y) = x +/ . * (1{y), 1'
NB. , Creates a list (x, 1)
NB. +/ . * Use matrix product to calculate i*x+j
NB. (i.y) Creates a range matrix with shape (x,y).
NB. Its element at (i,j)-th is i*x+y
NB. = This sets the (i,j)-th element as 1 and
NB. others as 0
NB. Get adjacency matrix of a picture given a direction
adj_mat =: 3 : '+./ (image edge_list y) (unit"1) 2 $ max'
NB. 2 $ max Is the size of matrix unit
NB. (image edge_list y) Creates the edge list which
NB. is a list of pairs
NB. (unit"1) Calculate `unit` for each
NB. pair of the left, generating
NB. a list of matrix unit
NB. +./ Add all of the matrix unitsEntropy
The algorithm says that we need to choose the element that has the smallest entropy, but we haven’t defined what the entropy of a subset of means.
For a discrete probability distribution where , its entropy is
Which can be directly translated into a J verb:
NB. Calculate entropy assuming that input is a probability mass function (PDF)
entropy =: 3 : '+/ - y * 2 ^. y'Let a subset of , we can define its entropy as the entropy of sampling from , given by Bayes’ rule
We can have a good guess of by counting the frequency of each element in the input image. We will not normalize the probability until it is used.
NB. Count the presence of a number in given matrix
NB. Usage: matrix count number
count =: 4 : '+/ y = , x'
NB. The PDF of the prior distribution
prior =: image count"_ 0 i. maxThe posterior distribution is trivial since is equivalent to the element of the list that represent the subset :
NB. The PDF of posterior distribution
posterior =: 3 : '(% +/) y * prior'
NB. % +/ is a hook. it is equivalent to `y % +/ y`Now we have a definition of the entropy of the matrix that can be transformed into J sentences, but there is a small gap between it and the function we use to determine the element to collapse: the elements that are already collapsed has the lowest entropy, while we need to choose an element that hasn’t been collapsed. The gap can be filled by replacing zeros in the entropy by infinity because an uncollapsed element always have a positive entropy. 3
NB. Replace zero with infinity
replace_zero =: 3 : 'y + _ * 0 =/ y'
NB. This can't be explained here. See appendix B if you are interested.
NB. The matrix we use to choose the element to collapse
entropy_mat =: 3 : 'replace_zero entropy"1 posterior"1 y'Collapsing an Element
Unlike Numpy, it is hard to write J sentences that returns or uses the index of the element in an array. A more idiomatic way is to use masks (i.e. the array that is 1 at the element you want to choose and 0 everywhere else).
To get the element to collapse in step 2.1, we can use the mask that indicates the minimal values:
min_mask =: 3 : 'y = <./ , y'This will give us all elements that equal to the minimal value. Let’s take the first element that matches, so only one element will be collapsed at a time:
NB. Keeps the first element that is 1 and drop others
first =: 3 : '(i.$y) = (,y) i. 1'
NB. (,y) i. 1 Get the index of the first 1 in `(,y)`
NB. Indicates the element to collapse
collapse_mask =: 3 : 'first min_mask entropy_mat y'We use roulette wheel selection to choose the element to collapse to in step 2.2.
NB. Roulette wheel selection assuming that input is a PMF
roulette =: 3 : '(((? 2147483647) % 2147483647) < +/\ y) I. 1'
NB. ? 2147483647 Generates a random integer
NB. % 2147483647 Divide them by maximum value
NB. +/\ y Calculate the running sum of the PMF
NB. < Create a list that is 1 after some
NB. random element
NB. I. 1 Take the index of 1It’s easier to calculate roulette for the whole even though we only use the result of one element.
NB. Run `roulette` and return a mask for every element in the matrix
elem_mask =: 3 : '(i.max) ="_ 0 roulette"1 posterior"1 y'Now we are able to write the J verb that corresponds to step 2.1 in the algorithm:
collapse =: 3 : '(y * -. collapse_mask y) + (elem_mask y) * collapse_mask y'
NB. Here we use the trick introduced in appendix B againPropagation
In this section we will translate the equation (2) into J sentenses. is just a matrix product given that is just the row of the adjacency matrix:
NB. Get that union of things given a direction
NB. Usage: B' union_of_things direction
unions =: 4 : '((-y) |. x) (+./ . *)"1 _ adj_mat y'
NB. +./ . * Is the matrix product where `+`
NB. is replaced by `+.` (union)
NB. _y The adjacency matrix's direction
NB. is reversed in equation (2)Then the propagetion step can be done by taking the intersections of these unions:
NB. Neighbors on the right, left, down, up
neighbors =: 4 2 $ 0 1 0 _1 1 0 _1 0
propagation =: 3 : '*./ y , y unions"_ 1 neighbors'
NB. unions"_ 1 Get the unions for all neighbors
NB. and stack the results
NB. y , Add B' to the results
NB. *./ Intersects among the resultsControl Flows
The control flow of our algorithm consists of three component:
- There is a infinite loop
- If there is an empty set, reset the matrix
- If every element of is singleton, break the loop
The second part can be placed in the body of the loop:
NB. Reset B' if there is a zero row
valid_or_reset =: 3 : 'y >: *./ , +./"1 y'
NB. +./"1 If there is an 1, then it won't be 0
NB. *./ , Calculate the conjunction among them
NB. >: If rhs is 1, then it returns y,
NB. otherwise returns an array full of 1s
NB. The body of the loop
iter =: 3 : 'valid_or_reset propagation collapse y'The loop can be written as the power of a verb whose input has the same “type” as output. It stops when the condition is not true:
NB. Continue the loop if there are uncollapsed element
continue =: 3 : '-. *./ , 1 = +/"1 y'
NB. +/"1 Count the number of elements in a row
NB. 1 = Check whether the count is 1
NB. ( ) The result of this part stands for
NB. "All elements are collapsed"
NB. The loop
loop =: iter ^: continue ^: _
NB. _ Can be an integer to indicate the number of loopsWhen the algorithm returns, we need to get the output matrix from :
get_image =: 3 : 'y (I."1) 1'Putting All Pieces Together
That’s it! A simplified wave function collapse algorithm within a page of J code:
load 'files'
image =: 0 ". freadr 'image.txt'
max =: 1 + >./ , image
new_state =: 3 : '(y, max) $ 1'
edge_list =: 4 : ',/ |: > x; y|.x'
unit =: 4 : '(i.y) = x +/ . * (1{y), 1'
adj_mat =: 3 : '+./ (image edge_list y) (unit"1) 2 $ max'
entropy =: 3 : '+/ - y * 2 ^. y'
count =: 4 : '+/ y = , x'
prior =: image count"_ 0 i. max
posterior =: 3 : '(% +/) y * prior'
replace_zero =: 3 : 'y + _ * 0 =/ y'
entropy_mat =: 3 : 'replace_zero entropy"1 posterior"1 y'
min_mask =: 3 : 'y = <./ , y'
first =: 3 : '(i.$y) = (,y) i. 1'
collapse_mask =: 3 : 'first min_mask entropy_mat y'
roulette =: 3 : '(((? 2147483647) % 2147483647) < +/\ y) I. 1'
elem_mask =: 3 : '(i.max) ="_ 0 roulette"1 posterior"1 y'
collapse =: 3 : '(y * -. collapse_mask y) + (elem_mask y) * collapse_mask y'
unions =: 4 : '((-y) |. x) (+./ . *)"1 _ adj_mat y'
neighbors =: 4 2 $ 0 1 0 _1 1 0 _1 0
propagation =: 3 : '*./ y , y unions"_ 1 neighbors'
valid_or_reset =: 3 : 'y >: *./ , +./"1 y'
iter =: 3 : 'valid_or_reset propagation collapse y'
continue =: 3 : '-. *./ , 1 = +/"1 y'
loop =: iter ^: continue ^: _
get_image =: 3 : 'y (I."1) 1'
(": get_image loop new_state 10 10) fwrites 'output.txt'I won’t bother writing a GUI for it. A simple vibe-coded webapp is good enough.

Conclusion
Things I like in J (or array languages in general):
- Programming becomes math. If there is a simple way to describe the problem in matrixes and sets of integers, then writing a J program is trivial.
- You get the benefit of functional programming. Arrays are immutable, you are thinking in functions (or verbs), there are some kinds of type constraint (through array shape and rank) and higher order functions (through combinators).
- It’s not that hard to read. The semantics are simple, and once you understand the basics and know what the words mean, it’s quite readable (I may be wrong because I haven’t learned much about tacit programming).
What’s next: I’ll probably learn some tacit programming and rewrite (golf?) the program, or try other things in J.
Appendix A: The Algorithm is Imperfect
Here’s an example where the original algorithm fails: consider the input matrix and a output matrix . By collapsing , we create a invalid pair which doesn’t appear in .
Our algorithm also fails because the propagation steps is based on stale data. In the example above, it is still possible to generate if
Appendix B: How to Modify Elements in J
We need to write a J verb replace_zero that set the zero elements in an array to infinity (or any number). For example:
1 2 0 1 2 _
0 0 1 --> _ _ 1
2 3 4 2 3 4
y replace_zero y
The example above can be written as from the linear combination of matrix units to another linear combination of matrix units
So a valid replace_zero
- Finds the matrix units that corresponds to
y’s 0 - Multiply them by infinity
- Adds them to
y
We can directly translate these steps to J:
replace_zero =: 3 : 'y + _ * 0 =/ y'In general, we can modify the matrix by:
- Create some masks
- Decompose the input matrix according to these masks (which is always possible because matrix units are a set of independent basis)
- Multiply something and add them back
Suppose that we want to set the element of a matrix at index to 1 and element at index to 2. First we can decompose into these three matrix:
Then the result can be written as