Commit 884d11e1 authored by Jacob Winters's avatar Jacob Winters

Initial commit

parents
Pipeline #33342874 passed with stages
in 10 minutes and 49 seconds
*.hi
*.o
bin
results
image: ubuntu:18.04
stages:
- run
- deploy
run:
stage: run
script:
- apt-get update > /dev/null && apt-get install -y clang ghc netpbm optipng python zopfli > /dev/null
- ./runall
artifacts:
paths:
- results
pages:
stage: deploy
script:
- mv results public
artifacts:
paths:
- public
This diff is collapsed.
# Tile pattern enumeration and counting... thingy
This code was built to answer the question, "how many distinct patterns can you make by tiling a 4x4 monochrome bitmap?" Since then it's grown a few more options. There are two implementations, one in C and one in Haskell. They produce identical results. The C implementation is significantly faster.
You can [see the results in your browser](#results) or [download the raw data](https://gitlab.com/jacobwinters/tiles/-/jobs/artifacts/master/download?job=run).
## What do you mean by "distinct patterns?"
An image with one black pixel in the bottom right is distinct from an image with one black pixel in the top left, but they produce identical results when tiled:
<img src="https://jacobwinters.gitlab.io/tiles/4x4_2/1111111111111110.png">
In general, if one image is a translation of another, the two images will produce identical tilings.
The first versions of this program only considered images that were translations of each other to be equivalent, but this produced too many patterns to look through. The current version considers images that can be turned into each other by some sequence of translations, 90° rotations, transpositions, reflections, and color permutations to produce equivalent tiled patterns. This reduces the number of pattern equivalence classes enough for us to look through examples of all of them.
## Options
The program can list tilings of square bitmaps of any size with either 2 or 3 colors. (The Haskell version supports any number of colors; the C program needs manual adjustment to support more.) It can also restrict its investigation to patterns with a given symmetry.
The options are compile-time constants. They're defined as preprocessor macros so you can specify them on the compiler command line:
```
clang tiles.c -DSIZE=<size> -DSTATES=<color count> -D<symmetry type-{brick|none}>
ghc tiles.hs -cpp -DSIZE=<size> -DSTATES=<color count> -D<symmetry type-{brick|none}>
```
### Symmetries
#### Brick
A pattern has brick-like symmetry when the bottom half of a tile is the top half shifted horizontally by half its width.
These patterns have brick-like symmetry:
<img src="https://jacobwinters.gitlab.io/tiles/4x4_2_brick/1111100011110010.png">
<img src="https://jacobwinters.gitlab.io/tiles/4x4_2_brick/1100110000110011.png">
<img src="https://jacobwinters.gitlab.io/tiles/4x4_2_brick/1100100100110110.png">
These patterns don't:
<img src="https://jacobwinters.gitlab.io/tiles/4x4_2/1111110011110010.png">
<img src="https://jacobwinters.gitlab.io/tiles/4x4_2/1111110000000011.png">
<img src="https://jacobwinters.gitlab.io/tiles/4x4_2/1110110101111011.png">
## Running
The project comes with a shell script that will:
1. Compile one or both of the C and Haskell programs with a given set of options and run them
2. If you ran both programs, compare their results and alert you if they are different (which would indicate that there's a bug somewhere)
3. Optionally produce an example of each pattern equivalence class in PNG format and write an HTML file that displays all of the PNGs it generated
It depends on Clang, GHC, Netpbm, Optipng, Python, and ZopfliPNG.
```
./run [-H] [-C] -x<size> [-i<size of png files>] [-c<color count>] [-s<symmetry type-{brick|none}>]
-H: Run Haskell
-C: Run C
-i: Create images and HTML
```
There is also a `runall` script that tries a variety of parameters. This is the script that produces the results published on GitLab. Increases in tile size beyond those that `runall` tries aren't feasible on any hardware I have access to. Increases in state count are possible but I don't think the patterns will be as interesting as they are with low state counts.
## Results
A pattern from each equivalence class is displayed. The equivalence classes are sorted by the number of distinct images that produce patterns belonging to them.
Listed from most to least interesting:
* [6x6 2-state brick](https://jacobwinters.gitlab.io/tiles/6x6_2_brick/)
* [4x4 2-state](https://jacobwinters.gitlab.io/tiles/4x4_2/)
* [4x4 2-state brick](https://jacobwinters.gitlab.io/tiles/4x4_2_brick/)
* [4x4 3-state brick](https://jacobwinters.gitlab.io/tiles/4x4_3_brick/)
* [3x3 2-state](https://jacobwinters.gitlab.io/tiles/3x3_2/)
* [3x3 3-state](https://jacobwinters.gitlab.io/tiles/3x3_3/)
* [2x2 2-state](https://jacobwinters.gitlab.io/tiles/2x2_2/)
* [2x2 2-state brick](https://jacobwinters.gitlab.io/tiles/2x2_2_brick/)
* [2x2 3-state](https://jacobwinters.gitlab.io/tiles/2x2_3/)
* [2x2 3-state brick](https://jacobwinters.gitlab.io/tiles/2x2_3_brick/)
There are also some runs that produce too many images to make a gallery, but you can download the raw data if you want to:
* [5x5 2-state](https://jacobwinters.gitlab.io/tiles/5x5_2/results)
* [4x4 3-state](https://jacobwinters.gitlab.io/tiles/4x4_3/results)
#!/usr/bin/env python
import sys
print('''<!doctype html>
<html lang="en">
<head>
<title>''' + sys.argv[1] + '''</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
img {
-ms-interpolation-mode: nearest-neighbor;
image-rendering: -moz-crisp-edges;
image-rendering: pixelated;
width: ''' + sys.argv[2] + '''px;
height: ''' + sys.argv[2] + '''px;
}
</style>
</head>
<body>
<a href="results">View raw results</a>''')
lastVariationCount = ''
for line in sys.stdin:
[variationCount, file] = line.split()
if lastVariationCount != variationCount:
if lastVariationCount != '': print(' </section>')
print(' <section>')
print(' <h1>' + variationCount + '</h1>')
lastVariationCount = variationCount
print(' <img src="' + file + '.png">')
print(''' </section>
</body>
</html>''')
#!/bin/bash
states='2'
symmetry='none'
while getopts HCx:i:c:s: option; do
case $option in
H) runhs='true';;
C) runc='true';;
x) size=$OPTARG;;
i) tilesize=$OPTARG; runimages='true';;
c) states=$OPTARG;;
s) symmetry=$OPTARG;;
esac
done
runname=${size}x${size}_${states}
if [ $symmetry != none ]; then runname+=_${symmetry}; fi
echo
echo
echo $runname
mkdir -p bin
mkdir -p results/$runname
if [ "$runc" ]; then
clang tiles.c -O2 -flto -o bin/${runname}_c -DSIZE=$size -DSTATES=$states -D$symmetry
echo Running C...
time ./bin/${runname}_c > results/$runname/results_c
resultsfile="results/$runname/results_c"
fi
if [ "$runhs" ]; then
ghc tiles.hs -v0 -O2 -o bin/${runname}_hs -cpp -DSIZE=$size -DSTATES=$states -D$symmetry
echo Running Haskell...
time ./bin/${runname}_hs > results/$runname/results_hs
resultsfile="results/$runname/results_hs"
fi
if [ "$runc" -a "$runhs" ]; then
diff results/$runname/results_c results/$runname/results_hs -q
fi
cp $resultsfile results/$runname/results
if [ "$runimages" ]; then
echo Rendering images...
time (cat results/$runname/results | while read count tile; do
echo P2 $size $size $((states-1)) $(echo $tile | sed 's/./& /g') | pnmtile $tilesize $tilesize > "results/$runname/$tile.pgm"
optipng -o7 -strip all -fix -quiet results/$runname/$tile.pgm
zopflipng results/$runname/$tile.png results/$runname/$tile.png -y > /dev/null
rm results/$runname/$tile.pgm
done)
title="${size}x${size} "
if [ $states != 2 ]; then title+="${states}-state "; fi
if [ $symmetry != none ]; then title+="$symmetry "; fi
title+="patterns"
cat results/$runname/results | sort -k 2 -r | sort -k 1,1 -n -s | ./makehtml.py "$title" $((tilesize*2)) > results/$runname/index.html
fi
#!/bin/bash
# Regular
./run -HC -x2 -i32
./run -HC -x3 -i30
./run -HC -x4 -i32
./run -C -x5
# 3-state
./run -HC -x2 -i32 -c3
./run -HC -x3 -i30 -c3
./run -C -x4 -c3
# Brick
./run -HC -x2 -i32 -sbrick
./run -HC -x4 -i32 -sbrick
./run -H -x6 -i36 -sbrick
# 3-state brick
./run -HC -x2 -i32 -c3 -sbrick
./run -HC -x4 -i32 -c3 -sbrick
#include <stdio.h>
#include <inttypes.h>
#include <stdbool.h>
#include <assert.h>
#include <stdlib.h>
typedef uint8_t u8;
typedef uint64_t u64;
u64 powi(u64 base, u64 exponent) {
u64 result = 1;
for(u64 i = 0; i < exponent; i++) result *= base;
return result;
}
typedef struct {
u8 data[SIZE][SIZE];
} pattern;
#if STATES == 2
const int permutationCount = 2;
const u8 permutations[permutationCount][STATES] = {
{0, 1},
{1, 0}
};
#elif STATES == 3
const int permutationCount = 6;
const u8 permutations[permutationCount][STATES] = {
{0, 1, 2},
{0, 2, 1},
{1, 0, 2},
{2, 0, 1},
{1, 2, 0},
{2, 1, 0}
};
#else
#error Unsupported state count
#endif
pattern patternFromNumber(u64 patternNumber) {
pattern result;
for(int i = SIZE - 1; i >= 0; i--) {
for(int j = SIZE - 1; j >= 0; j--) {
result.data[i][j] = patternNumber % STATES;
patternNumber /= STATES;
}
}
return result;
}
u64 patternNumber(pattern pattern) {
u64 result = 0;
for(int i = 0; i < SIZE; i++) {
for(int j = 0; j < SIZE; j++) {
result *= STATES;
result += pattern.data[i][j];
}
}
return result;
}
bool patternEqual(pattern x, pattern y) {
for(int i = 0; i < SIZE; i++)
for(int j = 0; j < SIZE; j++)
if(x.data[i][j] != y.data[i][j])
return false;
return true;
}
pattern right(pattern x) {
pattern result;
for(int i = 0; i < SIZE; i++)
for(int j = 0; j < SIZE; j++)
result.data[(i + 1) % SIZE][j] = x.data[i][j];
return result;
}
pattern down(pattern x) {
pattern result;
for(int i = 0; i < SIZE; i++)
for(int j = 0; j < SIZE; j++)
result.data[i][(j + 1) % SIZE] = x.data[i][j];
return result;
}
pattern transpose(pattern x) {
pattern result;
for(int i = 0; i < SIZE; i++)
for(int j = 0; j < SIZE; j++)
result.data[j][i] = x.data[i][j];
return result;
}
pattern reflect(pattern x) {
pattern result;
for(int i = 0; i < SIZE; i++)
for(int j = 0; j < SIZE; j++)
result.data[SIZE - 1 - i][j] = x.data[i][j];
return result;
}
pattern rotate(pattern x) {
return reflect(transpose(x));
}
pattern permute(pattern x, int permutationNumber) {
pattern result;
for(int i = 0; i < SIZE; i++)
for(int j = 0; j < SIZE; j++)
result.data[i][j] = permutations[permutationNumber][x.data[i][j]];
return result;
}
void printPattern(pattern x) {
for(int i = 0; i < SIZE; i++)
for(int j = 0; j < SIZE; j++)
putchar('0' + (STATES - 1 - x.data[i][j]));
putchar('\n');
}
#ifdef brick
#if (SIZE & 1) == 1
#error Brick symmetry only works with even sizes
#endif
u64 initialPatternCount() {
return powi(STATES, SIZE * SIZE / 2);
}
pattern initialPatternNumber(u64 initialPatternNumber) {
pattern result;
for(int i = (SIZE / 2) - 1; i >= 0; i--) {
for(int j = SIZE - 1; j >= 0; j--) {
result.data[i][j] = result.data[SIZE / 2 + i][(j + SIZE / 2) % SIZE] = initialPatternNumber % STATES;
initialPatternNumber /= STATES;
}
}
return result;
}
#else
u64 initialPatternCount() {
return powi(STATES, SIZE * SIZE);
}
pattern initialPatternNumber(u64 initialPatternNumber) {
return patternFromNumber(initialPatternNumber);
}
#endif
const bool doRight = true;
const bool doDown = true;
const bool doRotate = true;
const bool doTranspose = true;
const bool doReflect = true;
const bool doPermutations = true;
const int rightIterations = doRight ? SIZE : 1;
const int downIterations = doDown ? SIZE : 1;
const int rotateIterations = doRotate ? 4 : 1;
const int transposeIterations = doTranspose ? 2 : 1;
const int reflectIterations = doReflect ? 2 : 1;
const int permutationIterations = doPermutations ? permutationCount : 1;
const int iterations = rightIterations * downIterations * rotateIterations * transposeIterations * reflectIterations * permutationIterations;
u8 *alreadySeen;
bool hasAlreadySeen(pattern pattern) {
u64 thisPatternNumber = patternNumber(pattern);
return alreadySeen[thisPatternNumber / 8] & (1 << (thisPatternNumber % 8));
}
void recordSeen(pattern pattern) {
u64 thisPatternNumber = patternNumber(pattern);
alreadySeen[thisPatternNumber / 8] |= (1 << (thisPatternNumber % 8));
}
int main() {
u64 patternCount = powi(STATES, SIZE * SIZE);
alreadySeen = calloc(patternCount / 8 + 1 /* Round up */, sizeof(u8));
int j, k, l, m, n, o;
pattern a, b, c, d, e, f;
pattern equivalenceClass[iterations];
for(u64 i = 0; i < initialPatternCount(); i++) {
int equivalenceClassCounter = 0;
for(j = 0, a = initialPatternNumber(i); j < rotateIterations; j++, a = doRotate ? rotate(a) : a) {
for(k = 0, b = a; k < transposeIterations; k++, b = doTranspose ? transpose(b) : b) {
for(l = 0, c = b; l < reflectIterations; l++, c = doReflect ? reflect(c) : c) {
for(m = 0, d = c; m < rightIterations; m++, d = doRight ? right(d) : d) {
for(n = 0, e = d; n < downIterations; n++, e = doDown ? down(e) : e) {
for(o = 0, f = e; o < permutationIterations; o++, f = doPermutations ? permute(e, o % permutationIterations) : e) {
if(hasAlreadySeen(f)) {
assert(patternEqual(f, initialPatternNumber(i)));
goto nextNumber;
}
equivalenceClass[equivalenceClassCounter++] = f;
}
assert(patternEqual(f, e));
}
assert(patternEqual(e, d));
}
assert(patternEqual(d, c));
}
assert(patternEqual(c, b));
}
assert(patternEqual(b, a));
}
assert(patternEqual(a, initialPatternNumber(i)));
int uniqueCount = 0;
for(j = 0; j < iterations; j++) {
recordSeen(equivalenceClass[j]);
for(k = 0; k < j; k++) {
if(patternEqual(equivalenceClass[j], equivalenceClass[k])) goto isRepeated;
}
uniqueCount++;
isRepeated:;
}
printf("%d ", uniqueCount);
printPattern(equivalenceClass[0]);
nextNumber:;
}
return 0;
}
import Data.List
size = SIZE
states = [0.. STATES-1]
baseMatrices =
#ifdef brick
allBrickMatrices
#else
allMatrices
#endif
allCombinations :: [[a]] -> [[a]]
allCombinations [] = [[]]
allCombinations (x:rest) = (:) <$> x <*> allCombinations rest
allColumns :: [[Int]]
allColumns = allCombinations $ take size $ repeat states
allMatrices :: [[[Int]]]
allMatrices = allCombinations $ take size $ repeat allColumns
-- This only makes sense when size is even
allBrickMatrices :: [[[Int]]]
allBrickMatrices = map makeBrick $ allCombinations $ take (size `quot` 2) $ repeat allColumns where
makeBrick halfMatrix = halfMatrix ++ map (rotateList $ size `quot` 2) halfMatrix
rotateList :: Int -> [a] -> [a]
rotateList n list = drop n list ++ take n list
rotateRows :: [[a]] -> [[a]]
rotateRows = map rotateRow where
rotateRow (first:rest) = rest ++ [first]
rotateRow [] = []
rotateColumns :: [[a]] -> [[a]]
rotateColumns = transpose . rotateRows . transpose
reflect :: [[a]] -> [[a]]
reflect = map reverse
rotate :: [[a]] -> [[a]]
rotate = reflect . transpose
permuteStates :: [[Int]] -> [[[Int]]]
permuteStates matrix = map (permuteStatesInMatrix matrix) (permutations states)
permuteStatesInMatrix :: [[Int]] -> [Int] -> [[Int]]
permuteStatesInMatrix matrix permutation = map (map (permutation !!)) matrix
allVariations :: [[Int]] -> [[[Int]]]
allVariations matrix = do
rotations <- take 4 $ iterate rotate matrix
-- The other transformations end up producing all variations of the patterns, so we don't need transpositions
-- transpositions <- [rotations, transpose rotations]
-- reflections <- [transpositions, reflect transpositions]
reflections <- [rotations, reflect rotations]
rowRotations <- take size $ iterate rotateRows reflections
columnRotations <- take size $ iterate rotateColumns rowRotations
statePermutations <- permuteStates columnRotations
return statePermutations
filteredMatrices :: [[[Int]]]
filteredMatrices = nub $ map (maximum . allVariations) baseMatrices
result :: [(Int, [[Int]])]
result = map matrixResult filteredMatrices where
matrixResult matrix = (length $ nub $ allVariations matrix, matrix)
-- Assumes 1 digit integers
matrixToString :: [[Int]] -> String
matrixToString = concatMap $ concatMap show
resultToString :: (Int, [[Int]]) -> String
resultToString (variations, matrix) = show variations ++ " " ++ matrixToString matrix
main = mapM (putStrLn . resultToString) result
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment