{ "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import json\n", "import numpy as np\n", "import pandas as pd\n", "import panel as pn\n", "\n", "pn.extension('deckgl', sizing_mode=\"stretch_width\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This demo was adapted from [PyDeck's Conway Game of Life example](https://github.com/uber/deck.gl/blob/66c75051d5b385db31f0a4322dff054779824783/bindings/pydeck/examples/06%20-%20Conway's%20Game%20of%20Life.ipynb), full copyright lies with the original authors.\n", "\n", "This modified example demonstrates how to display and update a `DeckGL` pane with a periodic callback by modifying the JSON representation and triggering an update." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Declare Game of Life logic" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import random\n", "\n", "def new_board(x, y, num_live_cells=2, num_dead_cells=3):\n", " \"\"\"Initializes a board for Conway's Game of Life\"\"\"\n", " board = []\n", " for i in range(0, y):\n", " # Defaults to a 3:2 dead cell:live cell ratio\n", " board.append([random.choice([0] * num_dead_cells + [1] * num_live_cells) for _ in range(0, x)])\n", " return board\n", "\n", " \n", "def get(board, x, y):\n", " \"\"\"Return the value at location (x, y) on a board, wrapping around if out-of-bounds\"\"\"\n", " return board[y % len(board)][x % len(board[0])]\n", "\n", "\n", "def assign(board, x, y, value):\n", " \"\"\"Assigns a value at location (x, y) on a board, wrapping around if out-of-bounds\"\"\"\n", " board[y % len(board)][x % len(board[0])] = value\n", "\n", "\n", "def count_neighbors(board, x, y):\n", " \"\"\"Counts the number of living neighbors a cell at (x, y) on a board has\"\"\"\n", " return sum([\n", " get(board, x - 1, y),\n", " get(board, x + 1, y),\n", " get(board, x, y - 1),\n", " get(board, x, y + 1),\n", " get(board, x + 1, y + 1),\n", " get(board, x + 1, y - 1),\n", " get(board, x - 1, y + 1),\n", " get(board, x - 1, y - 1)])\n", "\n", "\n", "def process_life(board):\n", " \"\"\"Creates the next iteration from a passed state of Conway's Game of Life\"\"\"\n", " next_board = new_board(len(board[0]), len(board))\n", " for y in range(0, len(board)):\n", " for x in range(0, len(board[y])):\n", " num_neighbors = count_neighbors(board, x, y)\n", " is_alive = get(board, x, y) == 1\n", " if num_neighbors < 2 and is_alive:\n", " assign(next_board, x, y, 0)\n", " elif 2 <= num_neighbors <= 3 and is_alive:\n", " assign(next_board, x, y, 1)\n", " elif num_neighbors > 3 and is_alive:\n", " assign(next_board, x, y, 0)\n", " elif num_neighbors == 3 and not is_alive:\n", " assign(next_board, x, y, 1)\n", " else:\n", " assign(next_board, x, y, 0)\n", " return next_board" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Set up DeckGL JSON" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "points = {\n", " '@@type': 'PointCloudLayer',\n", " 'data': [],\n", " 'getColor': '@@=color',\n", " 'getPosition': '@@=position',\n", " 'getRadius': 40,\n", " 'id': '0558257e-1a5c-43d7-bd98-1fba69981c6c'\n", "}\n", "\n", "board_json = {\n", " \"initialViewState\": {\n", " \"bearing\": 44,\n", " \"latitude\": 0.01,\n", " \"longitude\": 0.01,\n", " \"pitch\": 45,\n", " \"zoom\": 13\n", " },\n", " \"layers\": [points],\n", " \"mapStyle\": \"\",\n", " \"views\": [\n", " {\n", " \"@@type\": \"MapView\",\n", " \"controller\": True\n", " }\n", " ]\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Declare callbacks to periodically update the board" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "PINK = [155, 155, 255, 245]\n", "PURPLE = [255, 155, 255, 245]\n", "\n", "SCALING_FACTOR = 1000.0\n", "\n", "def convert_board_to_df(board):\n", " \"\"\"Makes the board matrix into a list for easier processing\"\"\"\n", " rows = []\n", " for x in range(0, len(board[0])):\n", " for y in range(0, len(board)):\n", " rows.append([[x / SCALING_FACTOR, y / SCALING_FACTOR], PURPLE if board[y][x] else PINK])\n", " return pd.DataFrame(rows, columns=['position', 'color'])\n", "\n", "def run_gol(event=None):\n", " global board\n", " board = process_life(board)\n", " records = convert_board_to_df(board)\n", " points['data'] = records\n", " gol.param.trigger('object')\n", " \n", "def reset_board(event):\n", " global board\n", " board = new_board(30, 30)\n", " run_gol()\n", "\n", "def toggle_periodic_callback(event):\n", " if event.new:\n", " periodic_toggle.name = 'Stop'\n", " periodic_toggle.button_type = 'warning'\n", " periodic_cb.start()\n", " else:\n", " periodic_toggle.name = 'Run'\n", " periodic_toggle.button_type = 'primary'\n", " periodic_cb.stop()\n", " \n", "def update_period(event):\n", " periodic_cb.period = event.new" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Set up Panel and callbacks" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "board = new_board(30, 30)\n", "\n", "gol = pn.pane.DeckGL(board_json, height=400)\n", "\n", "run_gol()\n", "\n", "periodic_toggle = pn.widgets.Toggle(\n", " name='Run', value=False, button_type='primary', align='end', width=50\n", ")\n", "periodic_toggle.param.watch(toggle_periodic_callback, 'value')\n", "\n", "period = pn.widgets.Spinner(name=\"Period (ms)\", value=500, step=50, start=50,\n", " align='end', width=100)\n", "period.param.watch(update_period, 'value')\n", "\n", "reset = pn.widgets.Button(name='Reset', button_type='warning', width=60, align='end')\n", "reset.on_click(reset_board)\n", "\n", "periodic_cb = pn.state.add_periodic_callback(run_gol, start=False, period=period.value)\n", "\n", "settings = pn.Row(period, periodic_toggle, reset, width=400, sizing_mode=\"fixed\")\n", "\n", "pn.Column(\n", " '## Game of Life (using Deck.GL)',\n", " gol,\n", " settings,\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## App\n", "\n", "Lets wrap it into nice template that can be served via `panel serve deckgl_game_of_life.ipynb`" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "description = \"\"\"\n", "**Conway's Game of Life** is a classic demonstration of *emergence*, where higher level patterns form from a few simple rules. Fantastic patterns emerge when the game is let to run long enough.\n", "\n", "The **rules** here, to borrow from [Wikipedia](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life), are as follows:\n", "\n", "- Any live cell with fewer than two live neighbours dies, as if by underpopulation.\n", "- Any live cell with two or three live neighbours lives on to the next generation.\n", "- Any live cell with more than three live neighbours dies, as if by overpopulation.\n", "- Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.\n", "\n", "This demo was **adapted from [PyDeck's Conway Game of Life example](https://github.com/uber/deck.gl/blob/66c75051d5b385db31f0a4322dff054779824783/bindings/pydeck/examples/06%20-%20Conway's%20Game%20of%20Life.ipynb)**, full copyright lies with the original authors.\n", "\n", "This modified example demonstrates **how to display and update a `DeckGL` pane with a periodic callback** by modifying the JSON representation and triggering an update.\"\"\"" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "pn.template.FastListTemplate(\n", " site=\"Panel\", \n", " title=\"Deck.gl - Game of Life\", \n", " main=[ pn.Column(description, settings), gol, ], main_max_width=\"768px\",\n", ").servable();" ] } ], "metadata": { "language_info": { "name": "python", "pygments_lexer": "ipython3" } }, "nbformat": 4, "nbformat_minor": 4 }