├── README.md ├── diff_game_of_life.ipynb └── game_of_life.ipynb /README.md: -------------------------------------------------------------------------------- 1 | # ca-experiments 2 | experiments with cellular automata 3 | 4 | You'll need the following packages: pytorch, opencv, gtk2, tqdm, numpy and scipy, jupyter. I recommend you to install them using the Anaconda python distribution. 5 | 6 | Personally, I use the Miniconda distribution available here: https://conda.io/miniconda.html. To install the packages after miniconda is installed I ran the following commands: 7 | 8 | conda install numpy scipy jupyter tqdm 9 | conda install gtk2 opencv -c conda-forge 10 | conda install pytorch -c pytorch 11 | 12 | Optionally, if you have an NVIDIA graphics card, you can try to install pytorch with 13 | 14 | conda install pytorch cuda90 cudnn -c pytorch 15 | 16 | If you have any questions, don't hesitate to write a message. 17 | -------------------------------------------------------------------------------- /diff_game_of_life.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "# convolution based attempt of the game of life\n", 10 | "import torch\n", 11 | "import numpy as np\n", 12 | "import cv2\n", 13 | "import os\n", 14 | "import gc\n", 15 | "import tqdm\n", 16 | "from functools import partial\n", 17 | "torch.set_num_threads(2)\n", 18 | "video_output_path = 'ca-experiment.mp4'\n", 19 | "in_channels = 3\n", 20 | "out_channels = 3\n", 21 | "groups = 1\n", 22 | "batchsize = 1\n", 23 | "fps = 30\n", 24 | "length_seconds = 60\n", 25 | "grid_resolution = (384, 216)\n", 26 | "output_resolution = (1280, 720)\n", 27 | "rule = [np.exp(1)/3,np.exp(1),0.98*np.exp(1),1.02*np.exp(1)]\n", 28 | "\n", 29 | "conv_out = None" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": 2, 35 | "metadata": {}, 36 | "outputs": [], 37 | "source": [ 38 | "def tocv2_img(src, shape):\n", 39 | " img = src.permute(0,2,3,1).mean(0).squeeze().cpu().numpy()\n", 40 | " img /= img.max()\n", 41 | " img = (255*img).astype('uint8')\n", 42 | " img = cv2.resize(img, shape, interpolation=cv2.INTER_NEAREST)\n", 43 | " return img\n", 44 | "\n", 45 | "def set_seed(grid, seed, loc, index=None):\n", 46 | " shp = seed.shape[-2:]\n", 47 | " if index is None:\n", 48 | " grid[:, :, loc[0]:shp[0]+loc[0], loc[1]:shp[1]+loc[1]] = seed\n", 49 | " else:\n", 50 | " grid[index[0], index[1], loc[0]:shp[0]+loc[0], log[1]:shp[1]+loc[1]] = seed\n", 51 | "\n", 52 | "def step(grid, kernels, rules, detach=True, circular_padding=True):\n", 53 | " global conv_out\n", 54 | " # pad input \n", 55 | " pad = (kernels.shape[-2]/2, kernels.shape[-2]/2,\n", 56 | " kernels.shape[-1]/2, kernels.shape[-1]/2)\n", 57 | " inps = torch.nn.functional.pad(grid, pad)\n", 58 | " if circular_padding:\n", 59 | " # sides\n", 60 | " inps[:, :, 0:pad[0], pad[2]:-pad[3]] = grid[:, :, -pad[0]:, :]\n", 61 | " inps[:, :, -pad[1]:, pad[2]:-pad[3]] = grid[:, :, 0:pad[1]:, :]\n", 62 | " inps[:, :, pad[0]:-pad[1], 0:pad[2]] = grid[:, :, :, -pad[2]:]\n", 63 | " inps[:, :, pad[0]:-pad[1], -pad[3]:] = grid[:, :, :, 0:pad[3]:]\n", 64 | " # corners\n", 65 | " inps[:, :, 0:pad[0], 0:pad[2]] = grid[:, :, -pad[0]:, -pad[2]:]\n", 66 | " inps[:, :, -pad[1]:, 0:pad[2]] = grid[:, :, 0:pad[1]:, -pad[2]:]\n", 67 | " inps[:, :, 0:pad[0], -pad[3]:] = grid[:, :, -pad[0]:, 0:pad[3]]\n", 68 | " inps[:, :, -pad[1]:, -pad[3]:] = grid[:, :, 0:pad[1], 0:pad[3]:]\n", 69 | "\n", 70 | " # convolve grid with kernels\n", 71 | " conv_out = torch.conv1d(inps, kernels, groups=groups)\n", 72 | " \n", 73 | " # update grid from rules. this will do a logical or between the list of rules\n", 74 | " update = -grid\n", 75 | " for rule in rules:\n", 76 | " update = update + rule(conv_out, grid)\n", 77 | " grid = grid + update\n", 78 | " return grid\n", 79 | "\n", 80 | "def soft_ge(x, value, scale=16, bias=4):\n", 81 | " return ((x-value)*scale+bias).sigmoid()\n", 82 | "\n", 83 | "def soft_le(x, value, scale=16, bias=4):\n", 84 | " return (-(x-value)*scale+bias).sigmoid()\n", 85 | "\n", 86 | "def soft_or(x, y, scale=16, bias=4):\n", 87 | " return ((x+y)*scale-bias).sigmoid()\n", 88 | "\n", 89 | "def game_of_life_rule(sum_grid, grid, rule=[2,3,3,3]):\n", 90 | " diff_grid = sum_grid - grid\n", 91 | " g_img = tocv2_img(grid, output_resolution)\n", 92 | " dg_img = tocv2_img(diff_grid, output_resolution)\n", 93 | " population_cond = grid*(soft_ge(diff_grid, rule[0])*(soft_le(diff_grid, rule[1])))\n", 94 | " reprod_cond = (1-grid)*(soft_ge(diff_grid, rule[2])*(soft_le(diff_grid, rule[3])))\n", 95 | " surviving_cells = soft_or(population_cond, reprod_cond)\n", 96 | " return surviving_cells\n" 97 | ] 98 | }, 99 | { 100 | "cell_type": "code", 101 | "execution_count": 3, 102 | "metadata": {}, 103 | "outputs": [], 104 | "source": [ 105 | "# the format is a bit weird, to conform to the minibatch processing API of the conv2d function. \n", 106 | "grid = torch.zeros((batchsize, in_channels, grid_resolution[-1], grid_resolution[-2]))\n", 107 | "# convolution with this filter results in adding up the number of ones in a 3x3 neighborhood\n", 108 | "# dimensions are (input channels, output channels, height width)\n", 109 | "def build_filters():\n", 110 | " sum_filter_3_3 = torch.ones(in_channels, out_channels, 3, 3)\n", 111 | " ind_sum_filter_3_3 = torch.stack([(i==j)*torch.ones(3, 3)\n", 112 | " for i in range(1, in_channels+1)\n", 113 | " for j in range(1, out_channels+1)]).reshape(in_channels, out_channels, 3, 3)\n", 114 | " weighted_channel_filter_3_3 = ind_sum_filter_3_3.clone().uniform_()\n", 115 | " weighted_channel_filter_3_3 /= weighted_channel_filter_3_3.sum(0) \n", 116 | " return [sum_filter_3_3, ind_sum_filter_3_3, weighted_channel_filter_3_3]\n", 117 | "\n", 118 | "# the seed as a sub image\n", 119 | "seed = torch.tensor([[[0,1,1,0],\n", 120 | " [1,1,0,0],\n", 121 | " [0,1,0,0],\n", 122 | " [0,0,0,0]],\n", 123 | " [[0,0,0,0],\n", 124 | " [0,1,1,0],\n", 125 | " [0,0,1,1],\n", 126 | " [0,0,1,0]],\n", 127 | " [[0,0,0,0],\n", 128 | " [0,0,1,0],\n", 129 | " [0,0,1,1],\n", 130 | " [0,1,1,0]]]).float()\n", 131 | "seed = seed.repeat(batchsize, 1, 1, 1)\n", 132 | "\n", 133 | "# initialize grid\n", 134 | "set_seed(grid, seed, ((grid.shape[-2]-seed.shape[-2])/2, (grid.shape[-1]-seed.shape[-1])/2))\n", 135 | "\n", 136 | "locs = np.random.uniform([8,8], [grid.shape[-2]-8, grid.shape[-1]-8], size=(10,2)).astype('uint32')\n", 137 | "for loc in locs:\n", 138 | " set_seed(grid, (torch.rand_like(seed) > 0.5).float(), loc)\n", 139 | " \n", 140 | "\n", 141 | "kernels_list = build_filters()\n", 142 | "\n", 143 | "gol_rule = partial(game_of_life_rule, rule=rule)" 144 | ] 145 | }, 146 | { 147 | "cell_type": "code", 148 | "execution_count": null, 149 | "metadata": {}, 150 | "outputs": [ 151 | { 152 | "name": "stderr", 153 | "output_type": "stream", 154 | "text": [ 155 | " 26%|██▌ | 459/1800 [00:32<01:34, 14.22it/s]" 156 | ] 157 | } 158 | ], 159 | "source": [ 160 | "cv2.destroyAllWindows()\n", 161 | "try:\n", 162 | " fourcc = cv2.VideoWriter_fourcc(*'H264')\n", 163 | " vw = cv2.VideoWriter(video_output_path, fourcc, fps, output_resolution)\n", 164 | "except Exception as e:\n", 165 | " print e\n", 166 | " vw = None\n", 167 | " \n", 168 | "if torch.cuda.is_available():\n", 169 | " grid = grid.cuda()\n", 170 | " kernels_list = [kernels.cuda() for kernels in kernels_list]\n", 171 | " \n", 172 | "conv_out = grid.clone()\n", 173 | "for i in tqdm.tqdm(range(int(fps*length_seconds))):\n", 174 | " # every once in a while, change the way neighbours are counted\n", 175 | " if i%61 == 1:\n", 176 | " kernels_list = build_filters()\n", 177 | " if torch.cuda.is_available():\n", 178 | " kernels_list = [kernels.cuda() for kernels in kernels_list]\n", 179 | " # every once in a while, drop random seeds at random locations\n", 180 | " if i%31 == 1:\n", 181 | " locs = np.random.uniform([8,8], [grid.shape[-2]-8, grid.shape[-1]-8], size=(3,2)).astype('uint32')\n", 182 | " for loc in locs:\n", 183 | " set_seed(grid, (torch.rand_like(seed) > 0.5).float(), loc)\n", 184 | " kernels = kernels_list[2]\n", 185 | " # convert to image with channels as last dimension, and desired output resolution\n", 186 | " grid_img = tocv2_img(grid, output_resolution)\n", 187 | " \n", 188 | " # show current grid state\n", 189 | " cv2.imshow('big bang orig', grid_img)\n", 190 | " cv2.waitKey(int(1000.0/fps))\n", 191 | " \n", 192 | " # write output\n", 193 | " if vw is not None:\n", 194 | " vw.write(grid_img)\n", 195 | " \n", 196 | " # step the cellular automaton\n", 197 | " grid = step(grid, kernels, (gol_rule,))\n", 198 | " \n", 199 | " # clean up memory\n", 200 | " gc.collect()\n", 201 | " torch.cuda.empty_cache()\n", 202 | "\n", 203 | "vw.release()\n", 204 | "cv2.destroyAllWindows()" 205 | ] 206 | }, 207 | { 208 | "cell_type": "code", 209 | "execution_count": null, 210 | "metadata": {}, 211 | "outputs": [], 212 | "source": [] 213 | }, 214 | { 215 | "cell_type": "code", 216 | "execution_count": null, 217 | "metadata": {}, 218 | "outputs": [], 219 | "source": [] 220 | } 221 | ], 222 | "metadata": { 223 | "kernelspec": { 224 | "display_name": "Python 2", 225 | "language": "python", 226 | "name": "python2" 227 | }, 228 | "language_info": { 229 | "codemirror_mode": { 230 | "name": "ipython", 231 | "version": 2 232 | }, 233 | "file_extension": ".py", 234 | "mimetype": "text/x-python", 235 | "name": "python", 236 | "nbconvert_exporter": "python", 237 | "pygments_lexer": "ipython2", 238 | "version": "2.7.15" 239 | } 240 | }, 241 | "nbformat": 4, 242 | "nbformat_minor": 2 243 | } 244 | -------------------------------------------------------------------------------- /game_of_life.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "# convolution based attempt of the game of life\n", 10 | "import torch\n", 11 | "import numpy as np\n", 12 | "import cv2\n", 13 | "import os\n", 14 | "import gc\n", 15 | "import tqdm\n", 16 | "from functools import partial\n", 17 | "torch.set_num_threads(2)\n", 18 | "video_output_path = None #'ca-experiment.mp4'\n", 19 | "in_channels = 3\n", 20 | "out_channels = 3\n", 21 | "groups = 1\n", 22 | "batchsize = 1\n", 23 | "fps = 30\n", 24 | "length_seconds = 60\n", 25 | "grid_resolution = (480, 270)\n", 26 | "output_resolution = (1920, 1080)\n", 27 | "rule = [2,3,3,3]\n", 28 | "\n", 29 | "conv_out = None" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": 2, 35 | "metadata": {}, 36 | "outputs": [], 37 | "source": [ 38 | "def tocv2_img(src, shape):\n", 39 | " img = src.permute(0,2,3,1).mean(0).squeeze().cpu().numpy()\n", 40 | " img /= img.max()\n", 41 | " img = (255*img).astype('uint8')\n", 42 | " img = cv2.resize(img, shape, interpolation=cv2.INTER_NEAREST)\n", 43 | " return img\n", 44 | "\n", 45 | "def set_seed(grid, seed, loc, index=None):\n", 46 | " shp = seed.shape[-2:]\n", 47 | " if index is None:\n", 48 | " grid[:, :, loc[0]:shp[0]+loc[0], loc[1]:shp[1]+loc[1]] = seed\n", 49 | " else:\n", 50 | " grid[index[0], index[1], loc[0]:shp[0]+loc[0], log[1]:shp[1]+loc[1]] = seed\n", 51 | "\n", 52 | "def step(grid, kernels, rules, detach=True, circular_padding=True):\n", 53 | " global conv_out\n", 54 | " # pad input \n", 55 | " pad = (kernels.shape[-2]/2, kernels.shape[-2]/2,\n", 56 | " kernels.shape[-1]/2, kernels.shape[-1]/2)\n", 57 | " inps = torch.nn.functional.pad(grid, pad)\n", 58 | " if circular_padding:\n", 59 | " # sides\n", 60 | " inps[:, :, 0:pad[0], pad[2]:-pad[3]] = grid[:, :, -pad[0]:, :]\n", 61 | " inps[:, :, -pad[1]:, pad[2]:-pad[3]] = grid[:, :, 0:pad[1]:, :]\n", 62 | " inps[:, :, pad[0]:-pad[1], 0:pad[2]] = grid[:, :, :, -pad[2]:]\n", 63 | " inps[:, :, pad[0]:-pad[1], -pad[3]:] = grid[:, :, :, 0:pad[3]:]\n", 64 | " # corners\n", 65 | " inps[:, :, 0:pad[0], 0:pad[2]] = grid[:, :, -pad[0]:, -pad[2]:]\n", 66 | " inps[:, :, -pad[1]:, 0:pad[2]] = grid[:, :, 0:pad[1]:, -pad[2]:]\n", 67 | " inps[:, :, 0:pad[0], -pad[3]:] = grid[:, :, -pad[0]:, 0:pad[3]]\n", 68 | " inps[:, :, -pad[1]:, -pad[3]:] = grid[:, :, 0:pad[1], 0:pad[3]:]\n", 69 | "\n", 70 | " # convolve grid with kernels\n", 71 | " conv_out = torch.conv2d(inps, kernels, groups=groups)\n", 72 | " \n", 73 | " # update grid from rules. this will do a logical or between the list of rules\n", 74 | " update = -grid\n", 75 | " for rule in rules:\n", 76 | " update = update + rule(conv_out, grid).float()\n", 77 | " grid = grid + update\n", 78 | " return grid\n", 79 | "\n", 80 | "def game_of_life_rule(sum_grid, grid, rule=[2,3,3,3]):\n", 81 | " diff_grid = sum_grid - grid\n", 82 | " population_cond = (grid == 1)*((diff_grid >= rule[0])*(diff_grid <= rule[1]))\n", 83 | " reprod_cond = (grid == 0)*((sum_grid >= rule[2])*(sum_grid <= rule[3]))\n", 84 | " surviving_cells = population_cond + reprod_cond\n", 85 | " return surviving_cells\n", 86 | "\n", 87 | "def buggy_game_of_life_rule(sum_grid, grid, rule=[2,3,3,3]):\n", 88 | " neighbours = sum_grid - grid\n", 89 | " return [((neighbours < rule[0]) + (neighbours > rule[1]), 0),\n", 90 | " ((neighbours == rule[0]) + (neighbours == rule[3]), 1)]\n" 91 | ] 92 | }, 93 | { 94 | "cell_type": "code", 95 | "execution_count": 3, 96 | "metadata": {}, 97 | "outputs": [], 98 | "source": [ 99 | "# the format is a bit weird, to conform to the minibatch processing API of the conv2d function. \n", 100 | "grid = torch.zeros((batchsize, in_channels, grid_resolution[-1], grid_resolution[-2]))\n", 101 | "# convolution with this filter results in adding up the number of ones in a 3x3 neighborhood\n", 102 | "# dimensions are (input channels, output channels, height width)\n", 103 | "def build_filters():\n", 104 | " sum_filter_3_3 = torch.ones(in_channels, out_channels, 3, 3)\n", 105 | " ind_sum_filter_3_3 = torch.stack([(i==j)*torch.ones(3, 3)\n", 106 | " for i in range(1, in_channels+1)\n", 107 | " for j in range(1, out_channels+1)]).reshape(in_channels, out_channels, 3, 3)\n", 108 | " two_channel_sum_filter_3_3 = ind_sum_filter_3_3.clone()\n", 109 | " two_channel_sum_filter_3_3[0, 1] = two_channel_sum_filter_3_3[0, 0].clone().bernoulli_(1.0/9.0)\n", 110 | " #two_channel_sum_filter_3_3[0, 0] = 1 - two_channel_sum_filter_3_3[0, 1]\n", 111 | " two_channel_sum_filter_3_3[1, 2] = two_channel_sum_filter_3_3[1, 1].clone().bernoulli_(1.0/9.0)\n", 112 | " #two_channel_sum_filter_3_3[1, 1] = 1 - two_channel_sum_filter_3_3[1, 2]\n", 113 | " two_channel_sum_filter_3_3[2, 0] = two_channel_sum_filter_3_3[2, 2].clone().bernoulli_(1.0/9.0)\n", 114 | " #two_channel_sum_filter_3_3[2, 2] = 1 - two_channel_sum_filter_3_3[2, 0]\n", 115 | " return [sum_filter_3_3, ind_sum_filter_3_3, two_channel_sum_filter_3_3]\n", 116 | "\n", 117 | "# the seed as a sub image\n", 118 | "seed = torch.tensor([[[0,1,1,0],\n", 119 | " [1,1,0,0],\n", 120 | " [0,1,0,0],\n", 121 | " [0,0,0,0]],\n", 122 | " [[0,0,0,0],\n", 123 | " [0,1,1,0],\n", 124 | " [0,0,1,1],\n", 125 | " [0,0,1,0]],\n", 126 | " [[0,0,0,0],\n", 127 | " [0,0,1,0],\n", 128 | " [0,0,1,1],\n", 129 | " [0,1,1,0]]]).float()\n", 130 | "seed = seed.repeat(batchsize, 1, 1, 1)\n", 131 | "\n", 132 | "# initialize grid\n", 133 | "set_seed(grid, seed, ((grid.shape[-2]-seed.shape[-2])/2, (grid.shape[-1]-seed.shape[-1])/2))\n", 134 | "\n", 135 | "locs = np.random.uniform([8,8], [grid.shape[-2]-8, grid.shape[-1]-8], size=(1000,2)).astype('uint32')\n", 136 | "for loc in locs:\n", 137 | " set_seed(grid, (torch.rand_like(seed) > 0.5).float(), loc)\n", 138 | " \n", 139 | "\n", 140 | "kernels_list = build_filters()\n", 141 | "\n", 142 | "gol_rule = partial(game_of_life_rule, rule=rule)" 143 | ] 144 | }, 145 | { 146 | "cell_type": "code", 147 | "execution_count": null, 148 | "metadata": { 149 | "scrolled": true 150 | }, 151 | "outputs": [ 152 | { 153 | "name": "stderr", 154 | "output_type": "stream", 155 | "text": [ 156 | " 6%|▌ | 109/1800 [00:07<01:49, 15.45it/s]" 157 | ] 158 | } 159 | ], 160 | "source": [ 161 | "cv2.destroyAllWindows()\n", 162 | "try:\n", 163 | " fourcc = cv2.VideoWriter_fourcc(*'H264')\n", 164 | " vw = cv2.VideoWriter(video_output_path, fourcc, fps, output_resolution)\n", 165 | "except Exception as e:\n", 166 | " print e\n", 167 | " vw = None\n", 168 | " \n", 169 | "if torch.cuda.is_available():\n", 170 | " grid = grid.cuda()\n", 171 | " kernels_list = [kernels.cuda() for kernels in kernels_list]\n", 172 | " \n", 173 | "conv_out = grid.clone()\n", 174 | "for i in tqdm.tqdm(range(int(fps*length_seconds))):\n", 175 | " # every once in a while, change the way neighbours are counted\n", 176 | " if i%47 == 1:\n", 177 | " kernels_list = build_filters()\n", 178 | " if torch.cuda.is_available():\n", 179 | " kernels_list = [kernels.cuda() for kernels in kernels_list]\n", 180 | " # every once in a while, drop random seeds at random locations\n", 181 | " if i%31 == 1:\n", 182 | " locs = np.random.uniform([8,8], [grid.shape[-2]-8, grid.shape[-1]-8], size=(3,2)).astype('uint32')\n", 183 | " for loc in locs:\n", 184 | " set_seed(grid, (torch.rand_like(seed) > 0.5).float(), loc)\n", 185 | " kernels = kernels_list[2]\n", 186 | " # convert to image with channels as last dimension, and desired output resolution\n", 187 | " grid_img = tocv2_img(grid, output_resolution)\n", 188 | " \n", 189 | " # show current grid state\n", 190 | " cv2.imshow('big bang orig', grid_img)\n", 191 | " cv2.waitKey(int(1000.0/fps))\n", 192 | " \n", 193 | " # write output\n", 194 | " if vw is not None:\n", 195 | " vw.write(grid_img)\n", 196 | " \n", 197 | " # step the cellular automaton\n", 198 | " grid = step(grid, kernels, (gol_rule,))\n", 199 | " \n", 200 | " # clean up memory\n", 201 | " gc.collect()\n", 202 | " torch.cuda.empty_cache()\n", 203 | "\n", 204 | "vw.release()\n", 205 | "cv2.destroyAllWindows()" 206 | ] 207 | }, 208 | { 209 | "cell_type": "code", 210 | "execution_count": null, 211 | "metadata": {}, 212 | "outputs": [], 213 | "source": [] 214 | } 215 | ], 216 | "metadata": { 217 | "kernelspec": { 218 | "display_name": "Python 2", 219 | "language": "python", 220 | "name": "python2" 221 | }, 222 | "language_info": { 223 | "codemirror_mode": { 224 | "name": "ipython", 225 | "version": 2 226 | }, 227 | "file_extension": ".py", 228 | "mimetype": "text/x-python", 229 | "name": "python", 230 | "nbconvert_exporter": "python", 231 | "pygments_lexer": "ipython2", 232 | "version": "2.7.15" 233 | } 234 | }, 235 | "nbformat": 4, 236 | "nbformat_minor": 2 237 | } 238 | --------------------------------------------------------------------------------