From fe9f4cd20f14c2de512b0e2efbc728f5ac1ee68a Mon Sep 17 00:00:00 2001 From: Sanggyu Lee <8325289+gyusang@users.noreply.github.com> Date: Wed, 11 Oct 2023 07:17:39 +0000 Subject: [PATCH] Add dependencies --- .devcontainer/Dockerfile | 3 +- .devcontainer/devcontainer.json | 2 +- pyproject.toml | 23 +- tutorial.ipynb | 1752 +++++++++++++++++++++++++++++++ vec_noise-1.1.4/CHANGES.txt | 23 + vec_noise-1.1.4/LICENSE.txt | 20 + vec_noise-1.1.4/MANIFEST.in | 3 + vec_noise-1.1.4/PKG-INFO | 37 + vec_noise-1.1.4/README.rst | 65 ++ vec_noise-1.1.4/__init__.py | 20 + vec_noise-1.1.4/_noise.h | 108 ++ vec_noise-1.1.4/_perlin.c | 307 ++++++ vec_noise-1.1.4/_simplex.c | 774 ++++++++++++++ vec_noise-1.1.4/perlin.py | 354 +++++++ vec_noise-1.1.4/setup.cfg | 7 + vec_noise-1.1.4/setup.py | 75 ++ vec_noise-1.1.4/shader.py | 257 +++++ vec_noise-1.1.4/shader_noise.py | 194 ++++ vec_noise-1.1.4/test.py | 139 +++ 19 files changed, 4160 insertions(+), 3 deletions(-) create mode 100644 tutorial.ipynb create mode 100644 vec_noise-1.1.4/CHANGES.txt create mode 100644 vec_noise-1.1.4/LICENSE.txt create mode 100644 vec_noise-1.1.4/MANIFEST.in create mode 100644 vec_noise-1.1.4/PKG-INFO create mode 100644 vec_noise-1.1.4/README.rst create mode 100644 vec_noise-1.1.4/__init__.py create mode 100644 vec_noise-1.1.4/_noise.h create mode 100644 vec_noise-1.1.4/_perlin.c create mode 100644 vec_noise-1.1.4/_simplex.c create mode 100644 vec_noise-1.1.4/perlin.py create mode 100644 vec_noise-1.1.4/setup.cfg create mode 100644 vec_noise-1.1.4/setup.py create mode 100644 vec_noise-1.1.4/shader.py create mode 100644 vec_noise-1.1.4/shader_noise.py create mode 100644 vec_noise-1.1.4/test.py diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 2828fd1..31a0e2b 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -28,4 +28,5 @@ RUN curl -sSL https://pdm.fming.dev/install-pdm.py | python - \ && mkdir ${ZSH_CUSTOM}/plugins/pdm \ && pdm completion zsh > ${ZSH_CUSTOM}/plugins/pdm/_pdm \ && sed -i "s|^plugins=(|&pdm |" ${HOME}/.zshrc \ - && echo 'eval $(pdm venv activate)' >>${HOME}/.zshrc + && echo 'eval $(pdm venv activate in-project)' >>${HOME}/.zshrc \ + && echo 'export COMPUTE_PLATFORM=cpu' diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 7ed2e4f..bc39424 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -9,7 +9,7 @@ // 👇 Use 'forwardPorts' to make a list of ports inside the container available locally. // "forwardPorts": [], // 👇 Use 'postCreateCommand' to run commands after the container is created. - // "postCreateCommand": "", + "postCreateCommand": "cd /workspaces/LuX-AI-Season-2 && pdm use -f && pdm venv create && eval $(pdm venv activate in-project) && pdm update", // 👇 Configure tool-specific properties. "customizations": { "vscode": { diff --git a/pyproject.toml b/pyproject.toml index 8b534fb..39f6965 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,9 +8,30 @@ authors = [ ] dependencies = [ "jaxlib @ https://storage.googleapis.com/jax-releases/cuda11/jaxlib-0.4.7+cuda11.cudnn82-cp310-cp310-manylinux2014_x86_64.whl", - "jax==0.4.7", + "jax[cuda11_cudnn82]==0.4.7", + "rich>=13.6.0", + "juxai-s2>=3.0.0", + "vec-noise @ file:///${PROJECT_ROOT}/vec_noise-1.1.4", + "torch>=2.1.0", + "torchvision>=0.16.0", + "torchaudio>=2.1.0", ] requires-python = ">=3.10,<3.11" readme = "README.md" license = {text = "NOLICENSE"} +[tool.pdm.dev-dependencies] +dev = [ + "jupyter>=1.0.0", + "opencv-python-headless>=4.8.1.78", +] + +[[tool.pdm.source]] +name = "torch" +url = "https://download.pytorch.org/whl/${COMPUTE_PLATFORM}" +verify_ssl = true + +[[tool.pdm.source]] +name = "jaxlib" +url = "https://storage.googleapis.com/jax-releases/jax_cuda_releases.html" +verify_ssl = true diff --git a/tutorial.ipynb b/tutorial.ipynb new file mode 100644 index 0000000..ca37495 --- /dev/null +++ b/tutorial.ipynb @@ -0,0 +1,1752 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Jux Tutorial" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "JUX is a jax-accelerated game core for Lux AI Challenge Season 2. This tutorial will guide you through the usage of JUX. Make sure you have fully understand [game rules](https://www.lux-ai.org/specs-2022-beta) of Lux AI Challenge Season 2 before going through this tutorial. This tutorial requires you to have basic knowledge of JAX." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# The Art of Dancing with Shackles" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Programming in JAX is an art of dancing with shackles.\n", + "\n", + "JAX in nature is a library aimed for array manipulation, not for general purpose programming. Programming in JAX is quite different from normal programming paradigm, and sometimes painful. Nearly all weird APIs you encountered in JUX are due to some limitation JAX exerts on us. Here are some of them:\n", + "\n", + "1. We need to map all if-else, for-loop, and other normal statements to JAX's **array operators**. Good news is that this part is done in JUX, and you don't need to worry about it.\n", + "\n", + "2. We need to adopt a **functional programming style**, which means we need to avoid mutating variables, and use immutable data structures. Every time you want to change a field of a class, you need to create a new instance of that class. This explains why you must pass in current game state to every function in JUX, and receive a new game state from them.\n", + "\n", + "3. We have **no advanced data structures** like list, dict, set, etc. All we have is just array, and we need to imitate all kinds of data structures with array.\n", + "\n", + "4. We must adopt a strange **attribute-first memory layout**, which is counter-intuitive for most programmers at first glance, but it is the key to understand the game state object.\n", + "\n", + "5. To make functions jittable, all arrays must be of **static shape and dtype**. In other word, we have no stuff like lists with dynamic length. Every list must be of fixed length. If not, pad it to a fixed length, and use another variable to record the actual length.\n", + "\n", + "But, what can we get from JAX? The answer is the massive parallel computation power and perfect portability. We need not to write any CUDA kernels or even any C++ code to enjoy the power of GPU. The whole project is implemented in pure python, so it can run on Linux/Windows/MacOS, and on CPU/GPU/TPU or any other XLA-compatible devices. This is cool, isn't it?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create a New Environment\n", + "\n", + "We first create a new `JuxEnv` object. This object is analogous to a `LuxAI_S2`." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "No GPU/TPU found, falling back to CPU. (Set TF_CPP_MIN_LOG_LEVEL=0 and rerun for more info.)\n" + ] + }, + { + "data": { + "text/plain": [ + "JuxBufferConfig(MAX_N_UNITS=200, MAX_GLOBAL_ID=2000, MAX_N_FACTORIES=11, MAP_SIZE=64)" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from jux.env import JuxEnv\n", + "from jux.config import JuxBufferConfig, EnvConfig\n", + "\n", + "jux_env = JuxEnv(\n", + " env_cfg=EnvConfig(),\n", + " buf_cfg=JuxBufferConfig(MAX_N_UNITS=200),\n", + ")\n", + "\n", + "jux_env.buf_cfg" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Beside the `EnvConfig` configuration, we also need to specify the `JuxBufferConfig`. The most important parameter is the `MAX_N_UNITS`, which greatly affects the performance and memory usage. The default value is 1000. See the performance section in [README.md](README.md#performance) for more details.\n", + "\n", + "Then, we reset the environment." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "state = jux_env.reset(seed=0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `reset()` function returns a `State` object, which stores the the current state of the game. Since LuxAI_S2 is a perfect information game, the `State` object is also our initial observation. The first time we call `reset()` takes several seconds for jax to compile it, and subsequent calls will be finished on the fly. Please run above cell again to see the difference." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can visualize the game state by following code snippet. " + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "img = jux_env.render(state, \"rgb_array\")\n", + "plt.axis('off')\n", + "plt.tight_layout()\n", + "plt.imshow(img)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step the Environment" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's first load a replay, and create our `JuxEnv` and `State` from it." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "import jux.utils\n", + "\n", + "lux_env, lux_actions = jux.utils.load_replay(f'https://www.kaggleusercontent.com/episodes/{46215591}.json')\n", + "\n", + "jux_env, state = JuxEnv.from_lux(lux_env, buf_cfg=JuxBufferConfig(MAX_N_UNITS=200))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This section we show how to step the environment. LuxAI_S2 has three phases:\n", + "1. the initial bid step\n", + "2. the factory placement steps\n", + "3. the normal steps\n", + "\n", + "To deal with three different phases, the `LuxAI_S2.step()` function is replaced with three phase-specific step functions:\n", + "1. `JuxEnv.step_bid()`\n", + "2. `JuxEnv.step_factory_placement()`\n", + "3. `JuxEnv.step_late_game()`\n", + "\n", + "Each of them has unique signature and deal a specific game phase." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Bid Step\n", + "\n", + "The first step is bid step, which is handled by `JuxEnv.step_bid()`. Jax adopts a functional programming style, so `JuxEnv` stores nothing about the game state. Every time we call `step_bid()`, the current game state shall be passed in, and a new game state is passed out. The same rule also applies to `step_factory_placement()` and `step_late_game()`." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "bid action in Lux:\n", + "{'player_0': {'bid': 0, 'faction': 'AlphaStrike'},\n", + " 'player_1': {'bid': 0, 'faction': 'AlphaStrike'}}\n", + "\n", + "bid action in Jux:\n", + "bid = [0 0], faction = [0 0]\n" + ] + } + ], + "source": [ + "import jux.actions\n", + "from pprint import pprint\n", + "\n", + "lux_act = next(lux_actions)\n", + "\n", + "print(\"bid action in Lux:\")\n", + "pprint(lux_act, compact=False)\n", + "\n", + "bid, faction = jux.actions.bid_action_from_lux(lux_act)\n", + "print()\n", + "print(\"bid action in Jux:\")\n", + "print(f\"{bid = !s}, {faction = !s}\")\n", + "\n", + "state, (observations, rewards, dones, infos) = jux_env.step_bid(state, bid, faction)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The argument `bid` and `faction` shall have following shapes." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "bid: ShapedArray(int32[2])\n", + "faction: ShapedArray(int8[2])\n" + ] + } + ], + "source": [ + "print(f\"bid: {bid.aval}\")\n", + "print(f\"faction: {faction.aval}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There are four factions, numbered from 0 to 3." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{0: ,\n", + " 1: ,\n", + " 2: ,\n", + " 3: }" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from jux.team import FactionTypes\n", + "\n", + "{int(f): f for f in FactionTypes}" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Beside new game state, there are also other four return values, `observations`, `rewards`, `dones`, and `infos`.\n", + "1. `observations` contains the observations for each player. Since LuxAI_S2 is a perfect information game, the observations are simply the same `state` object, i.e., \n", + " ```python\n", + " observations['player_0'] == observations['player_1'] == state\n", + " ```\n", + "2. `rewards` contains the sum of lichens each player owns.\n", + "3. `dones` is a dictionary of boolean values, which indicates whether the game is over for each player. It is gaurenteed that \n", + " ```python\n", + " dones['player_0'] == dones['player_1']\n", + " ```\n", + "4. `infos` for each player is an empty dictionary, because there is no other infos to provide. \n", + " ```python\n", + " infos['player_0'] == infos['player_1'] == {}\n", + " ```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Factory Placement Steps\n", + "\n", + "The factory placement step is handled by `JuxEnv.step_factory_placement()`. The next player to place a factory is indicated by `state.next_player`." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "lux_act = {'player_0': {'metal': 150, 'spawn': [27, 25], 'water': 150}, 'player_1': {}}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "player_0 place a factory at [27 25] with water=150, metal=150.\n", + "lux_act = {'player_0': {}, 'player_1': {'metal': 150, 'spawn': [19, 31], 'water': 150}}\n", + "player_1 place a factory at [19 31] with water=150, metal=150.\n", + "lux_act = {'player_0': {'metal': 150, 'spawn': [7, 10], 'water': 150}, 'player_1': {}}\n", + "player_0 place a factory at [ 7 10] with water=150, metal=150.\n", + "lux_act = {'player_0': {}, 'player_1': {'metal': 150, 'spawn': [7, 3], 'water': 150}}\n", + "player_1 place a factory at [7 3] with water=150, metal=150.\n" + ] + } + ], + "source": [ + "while state.real_env_steps < 0:\n", + " lux_act = next(lux_actions)\n", + " print(f\"{lux_act = }\")\n", + " spawn, water, metal = jux.actions.factory_placement_action_from_lux(lux_act)\n", + "\n", + " print(f\"player_{state.next_player!s} place a factory at {spawn[state.next_player]}\",\n", + " f\"with water={water[state.next_player]}, metal={metal[state.next_player]}.\")\n", + " state, (observations, rewards, dones, infos) = jux_env.step_factory_placement(state, spawn, water, metal)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The argument `spawn`, `water`, and `metal` have a leading dimension 2, but only \n", + "- `spawn[state.next_player]`\n", + "- `water[state.next_player]`\n", + "- `metal[state.next_player]`\n", + "\n", + "are used." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "spawn: ShapedArray(int8[2,2])\n", + "water: ShapedArray(int32[2])\n", + "metal: ShapedArray(int32[2])\n" + ] + } + ], + "source": [ + "print(f\"spawn: {spawn.aval}\")\n", + "print(f\"water: {water.aval}\")\n", + "print(f\"metal: {metal.aval}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This is the current game state." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/workspaces/LuX-AI-Season-2/.venv/lib/python3.10/site-packages/pygame/sysfont.py:223: UserWarning: 'fc-list' is missing, system fonts cannot be loaded on your platform\n", + " warnings.warn(\n" + ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "img = jux_env.render(state, \"rgb_array\")\n", + "plt.axis('off')\n", + "plt.tight_layout()\n", + "plt.imshow(img)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Normal Steps\n", + "\n", + "The normal steps are handled by `JuxEnv.step_late_game()`." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from jux.actions import JuxAction\n", + "# prepare actions\n", + "lux_act = next(lux_actions)\n", + "jux_act = JuxAction.from_lux(state, lux_act)\n", + "\n", + "# step\n", + "state, (observations, rewards, dones, infos) = jux_env.step_late_game(state, jux_act)\n", + "\n", + "# render\n", + "img = jux_env.render(state, \"rgb_array\")\n", + "plt.axis('off')\n", + "plt.tight_layout()\n", + "plt.imshow(img)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`step_late_game()` takes an `JuxAction` object as input, which will be elaborated lately. The first time we call `step_late_game()` triggers the jax jit compilation, which is slow, but subsequent calls will be finished on the fly. You may run above cell again to see the difference." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Understanding the `State`\n", + "\n", + "`State` object is a nested `NamedTuple`, with all leaves being `jax.numpy.ndarray`. It has following fields. " + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "('env_cfg',\n", + " 'seed',\n", + " 'rng_state',\n", + " 'env_steps',\n", + " 'board',\n", + " 'units',\n", + " 'unit_id2idx',\n", + " 'n_units',\n", + " 'factories',\n", + " 'factory_id2idx',\n", + " 'n_factories',\n", + " 'teams',\n", + " 'global_id',\n", + " 'place_first')" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "state._fields" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Board Information\n", + "\n", + "Information about the board, including rubble, ice, ore, and lichen, are stored in `state.board`." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
Board(\n",
+       "    seed=ShapedArray(int32[]),\n",
+       "    factories_per_team=ShapedArray(int8[]),\n",
+       "    map=GameMap(\n",
+       "        rubble=ShapedArray(int8[48,48]),\n",
+       "        ice=ShapedArray(bool[48,48]),\n",
+       "        ore=ShapedArray(bool[48,48]),\n",
+       "        symmetry=ShapedArray(int8[])\n",
+       "    ),\n",
+       "    lichen=ShapedArray(int32[48,48]),\n",
+       "    lichen_strains=ShapedArray(int8[48,48]),\n",
+       "    units_map=ShapedArray(int16[48,48]),\n",
+       "    factory_map=ShapedArray(int8[48,48]),\n",
+       "    factory_occupancy_map=ShapedArray(int8[48,48]),\n",
+       "    factory_pos=ShapedArray(int8[22,2])\n",
+       ")\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[1;35mBoard\u001b[0m\u001b[1m(\u001b[0m\n", + " \u001b[33mseed\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint32\u001b[1m[\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mfactories_per_team\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mmap\u001b[0m=\u001b[1;35mGameMap\u001b[0m\u001b[1m(\u001b[0m\n", + " \u001b[33mrubble\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m48\u001b[0m,\u001b[1;36m48\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mice\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mbool\u001b[1m[\u001b[0m\u001b[1;36m48\u001b[0m,\u001b[1;36m48\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33more\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mbool\u001b[1m[\u001b[0m\u001b[1;36m48\u001b[0m,\u001b[1;36m48\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33msymmetry\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m\n", + " \u001b[1m)\u001b[0m,\n", + " \u001b[33mlichen\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint32\u001b[1m[\u001b[0m\u001b[1;36m48\u001b[0m,\u001b[1;36m48\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mlichen_strains\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m48\u001b[0m,\u001b[1;36m48\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33munits_map\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint16\u001b[1m[\u001b[0m\u001b[1;36m48\u001b[0m,\u001b[1;36m48\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mfactory_map\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m48\u001b[0m,\u001b[1;36m48\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mfactory_occupancy_map\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m48\u001b[0m,\u001b[1;36m48\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mfactory_pos\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m22\u001b[0m,\u001b[1;36m2\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m\n", + "\u001b[1m)\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import jux.tree_util\n", + "import rich\n", + "rich.print(jux.tree_util.map_to_aval(state.board))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Factory Information\n", + "\n", + "All information about factories, including their position, cargo, and power, are stored in `state.factories`. `state.n_factories` indicates the number of factories each player has. Because we have just reset the environment, both players have 0 factory. The leaves of `state.factories` have shapes shown as below. " + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "state.n_factories = Array([2, 2], dtype=int8)\n" + ] + }, + { + "data": { + "text/html": [ + "
Factory(\n",
+       "    team_id=ShapedArray(int8[2,11]),\n",
+       "    unit_id=ShapedArray(int8[2,11]),\n",
+       "    pos=Position(pos=ShapedArray(int8[2,11,2])),\n",
+       "    power=ShapedArray(int32[2,11]),\n",
+       "    cargo=UnitCargo(stock=ShapedArray(int32[2,11,4]))\n",
+       ")\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[1;35mFactory\u001b[0m\u001b[1m(\u001b[0m\n", + " \u001b[33mteam_id\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m11\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33munit_id\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m11\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mpos\u001b[0m=\u001b[1;35mPosition\u001b[0m\u001b[1m(\u001b[0m\u001b[33mpos\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m11\u001b[0m,\u001b[1;36m2\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mpower\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint32\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m11\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mcargo\u001b[0m=\u001b[1;35mUnitCargo\u001b[0m\u001b[1m(\u001b[0m\u001b[33mstock\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint32\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m11\u001b[0m,\u001b[1;36m4\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m\u001b[1m)\u001b[0m\n", + "\u001b[1m)\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "print(f\"{state.n_factories = }\")\n", + "rich.print(jux.tree_util.map_to_aval(state.factories))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "All leaves of `state.factories` have leading dimensions of `(2, MAX_N_FACTORIES)`. Data of player 0 are stored in `state.factories.xxx[0]` and data of player 1 are stored in `state.factories.xxx[1]`.\n", + "\n", + "Assume `state.n_factories` is `[2, 3]`, then player 0 has 2 factories, and player 1 has 3 factories. For player 0, the 2 factories he/she owns have power `state.factories.power[0, 0]` and `state.units.factories[0, 1]`, respectively. All values in `state.factories.power[0, 2:]` are invalid and undefined, because player 0 only has 2 factories. The same rule applies to other fields in `state.factories`. " + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Memory layout: Object-frist vs Attribute-first\n", + "The memory layout of `state.factories` is different from the classic object-oriented programming style. Take power as an example, the power of all units are stored in a single array. We call such kind of memory layout **attribute-first**, because it arranges the same attributes of all objects into consecutive memory space. \n", + "\n", + "In classic object-oriented programming, if we have a list of objects, we usually stores all attributes of an object in a single memory block, and maintain a list of such memory block. It usually looks like:\n", + "```python\n", + "[\n", + " Factory(unit_id=0, power=0, ... ),\n", + " Factory( ... ),\n", + " Factory( ... ),\n", + " Factory( ... ),\n", + " ...\n", + "]\n", + "```\n", + "This is called **object-first** memory layout, because it arranges the same object into consecutive memory space.\n", + "\n", + "In **attribute-first** layout, we only have one `Factory` object, but its attributes have a leading batch dimension. It looks like:\n", + "```python\n", + "Factory(\n", + " unit_id=[0, 1, 2, 3, ...],\n", + " power=[0, 0, 0, 0, ...],\n", + " ...\n", + ")\n", + "```\n", + "\n", + "The biggest advantage of attribute-first layout is that it is array-friendly. We can use array operations to manipulate the data. For example, we can implement factory power gain by a simple vector addition. \n", + "```python\n", + "new_power = state.factories.power + state.env_cfg.FACTORY_CHARGE\n", + "```" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Unit Information\n", + "\n", + "Information about units, including their unit type, position, cargo, and power, are stored in `state.units`. `state.n_units` indicates the number of units each player has. The leaves of `state.units` have shapes shown as below. Similar as factories, units are also stored in attribute-first layout. All leaves of `state.units` have leading dimensions of `(2, MAX_N_UNITS)`. More info about `state.units.action_queue` will be elaborated in next section." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "state.n_units = Array([2, 2], dtype=int16)\n" + ] + }, + { + "data": { + "text/html": [ + "
Unit(\n",
+       "    unit_type=ShapedArray(int8[2,200]),\n",
+       "    action_queue=ActionQueue(\n",
+       "        data=UnitAction(\n",
+       "            action_type=ShapedArray(int8[2,200,20]),\n",
+       "            direction=ShapedArray(int8[2,200,20]),\n",
+       "            resource_type=ShapedArray(int8[2,200,20]),\n",
+       "            amount=ShapedArray(int16[2,200,20]),\n",
+       "            repeat=ShapedArray(int16[2,200,20]),\n",
+       "            n=ShapedArray(int16[2,200,20])\n",
+       "        ),\n",
+       "        front=ShapedArray(int8[2,200]),\n",
+       "        rear=ShapedArray(int8[2,200]),\n",
+       "        count=ShapedArray(int8[2,200])\n",
+       "    ),\n",
+       "    team_id=ShapedArray(int8[2,200]),\n",
+       "    unit_id=ShapedArray(int16[2,200]),\n",
+       "    pos=Position(pos=ShapedArray(int8[2,200,2])),\n",
+       "    cargo=UnitCargo(stock=ShapedArray(int32[2,200,4])),\n",
+       "    power=ShapedArray(int32[2,200])\n",
+       ")\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[1;35mUnit\u001b[0m\u001b[1m(\u001b[0m\n", + " \u001b[33munit_type\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33maction_queue\u001b[0m=\u001b[1;35mActionQueue\u001b[0m\u001b[1m(\u001b[0m\n", + " \u001b[33mdata\u001b[0m=\u001b[1;35mUnitAction\u001b[0m\u001b[1m(\u001b[0m\n", + " \u001b[33maction_type\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m,\u001b[1;36m20\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mdirection\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m,\u001b[1;36m20\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mresource_type\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m,\u001b[1;36m20\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mamount\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint16\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m,\u001b[1;36m20\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mrepeat\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint16\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m,\u001b[1;36m20\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mn\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint16\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m,\u001b[1;36m20\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m\n", + " \u001b[1m)\u001b[0m,\n", + " \u001b[33mfront\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mrear\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mcount\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m\n", + " \u001b[1m)\u001b[0m,\n", + " \u001b[33mteam_id\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33munit_id\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint16\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mpos\u001b[0m=\u001b[1;35mPosition\u001b[0m\u001b[1m(\u001b[0m\u001b[33mpos\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m,\u001b[1;36m2\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mcargo\u001b[0m=\u001b[1;35mUnitCargo\u001b[0m\u001b[1m(\u001b[0m\u001b[33mstock\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint32\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m,\u001b[1;36m4\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mpower\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint32\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m\n", + "\u001b[1m)\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "print(f\"{state.n_units = }\")\n", + "rich.print(jux.tree_util.map_to_aval(state.units))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Understanding Robot Actions\n", + "\n", + "The whole actions system is designed upon three classes `UnitAction`, `ActionQueue` and `JuxActions`.\n", + "1. `UnitAction`: the class representing single robot action.\n", + "2. `ActionQueue`: a bi-directional queue storing a sequence of actions.\n", + "3. `JuxActions`: a NamedTuple containing both actions for factories and robots, mainly used as input to `JuxEnv.step_late_game()`.\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### UnitAction\n", + "\n", + "Actions of robots are represented by `UnitAction` objects. There are five fields in `UnitAction`:\n", + "\n", + "| fields | dtype | description |\n", + "|:--------------- |:----- |:---------------------------------------------------- |\n", + "| `action_type` | int8 | indicates the action type. |\n", + "| `direction` | int8 | used in `MOVE` and `TRANSFER` actions. |\n", + "| `resource_type` | int8 | used in `PICKUP` and `TRANSFER` actions. |\n", + "| `amount` | int16 | used in `PICKUP`, `TRANSFER` and `RECHARGE` actions. |\n", + "| `repeat` | int8 | used in all actions. |\n", + "| `n` | int16 | used in all actions. |" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
UnitAction(\n",
+       "    action_type=Array(0, dtype=int8),\n",
+       "    direction=Array(1, dtype=int8),\n",
+       "    resource_type=Array(0, dtype=int8),\n",
+       "    amount=Array(0, dtype=int16),\n",
+       "    repeat=Array(0, dtype=int16),\n",
+       "    n=Array(1, dtype=int16)\n",
+       ")\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[1;35mUnitAction\u001b[0m\u001b[1m(\u001b[0m\n", + " \u001b[33maction_type\u001b[0m=\u001b[1;35mArray\u001b[0m\u001b[1m(\u001b[0m\u001b[1;36m0\u001b[0m, \u001b[33mdtype\u001b[0m=\u001b[35mint8\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mdirection\u001b[0m=\u001b[1;35mArray\u001b[0m\u001b[1m(\u001b[0m\u001b[1;36m1\u001b[0m, \u001b[33mdtype\u001b[0m=\u001b[35mint8\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mresource_type\u001b[0m=\u001b[1;35mArray\u001b[0m\u001b[1m(\u001b[0m\u001b[1;36m0\u001b[0m, \u001b[33mdtype\u001b[0m=\u001b[35mint8\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mamount\u001b[0m=\u001b[1;35mArray\u001b[0m\u001b[1m(\u001b[0m\u001b[1;36m0\u001b[0m, \u001b[33mdtype\u001b[0m=\u001b[35mint16\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mrepeat\u001b[0m=\u001b[1;35mArray\u001b[0m\u001b[1m(\u001b[0m\u001b[1;36m0\u001b[0m, \u001b[33mdtype\u001b[0m=\u001b[35mint16\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mn\u001b[0m=\u001b[1;35mArray\u001b[0m\u001b[1m(\u001b[0m\u001b[1;36m1\u001b[0m, \u001b[33mdtype\u001b[0m=\u001b[35mint16\u001b[0m\u001b[1m)\u001b[0m\n", + "\u001b[1m)\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from jux.map.position import Direction\n", + "from jux.actions import UnitAction\n", + "import rich\n", + "\n", + "move_action = UnitAction.move(Direction.UP, repeat=0)\n", + "rich.print(move_action)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`action_type` must be one of `UnitActionType`. There are 6 types of actions, numbered from 0 to 5. Note: `DO_NOTHING` (-1) is a special one, designed for internal use only. Users should not use it. " + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{-1: ,\n", + " 0: ,\n", + " 1: ,\n", + " 2: ,\n", + " 3: ,\n", + " 4: ,\n", + " 5: }" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from jux.actions import UnitActionType\n", + "\n", + "{int(a): a for a in UnitActionType}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`direction` must be one of `Direction`. There are 5 directions, numbered from 0 to 4." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{0: ,\n", + " 1: ,\n", + " 2: ,\n", + " 3: ,\n", + " 4: }" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from jux.map.position import Direction\n", + "\n", + "{int(a): a for a in Direction}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`resource_type` must be one of `ResourceType`. There are 4 types of resources, numbered from 0 to 3." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{0: ,\n", + " 1: ,\n", + " 2: ,\n", + " 3: ,\n", + " 4: }" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from jux.unit_cargo import ResourceType\n", + "\n", + "{int(a): a for a in ResourceType}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Action Queue" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The most complicated part about `units` is its action queue. The action queue for single unit has following structure." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
ActionQueue(\n",
+       "    data=UnitAction(\n",
+       "        action_type=ShapedArray(int8[20]),\n",
+       "        direction=ShapedArray(int8[20]),\n",
+       "        resource_type=ShapedArray(int8[20]),\n",
+       "        amount=ShapedArray(int16[20]),\n",
+       "        repeat=ShapedArray(int16[20]),\n",
+       "        n=ShapedArray(int16[20])\n",
+       "    ),\n",
+       "    front=ShapedArray(int8[]),\n",
+       "    rear=ShapedArray(int8[]),\n",
+       "    count=ShapedArray(int8[])\n",
+       ")\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[1;35mActionQueue\u001b[0m\u001b[1m(\u001b[0m\n", + " \u001b[33mdata\u001b[0m=\u001b[1;35mUnitAction\u001b[0m\u001b[1m(\u001b[0m\n", + " \u001b[33maction_type\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m20\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mdirection\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m20\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mresource_type\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m20\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mamount\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint16\u001b[1m[\u001b[0m\u001b[1;36m20\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mrepeat\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint16\u001b[1m[\u001b[0m\u001b[1;36m20\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mn\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint16\u001b[1m[\u001b[0m\u001b[1;36m20\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m\n", + " \u001b[1m)\u001b[0m,\n", + " \u001b[33mfront\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mrear\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mcount\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m\n", + "\u001b[1m)\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from jux.actions import ActionQueue\n", + "action_queue = ActionQueue.empty(capacity=state.env_cfg.UNIT_ACTION_QUEUE_SIZE)\n", + "rich.print(jux.tree_util.map_to_aval(action_queue))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Units' actions are stored in `state.units.action_queue`, which is a bi-directional queue implemented in jax. `ActionQueue` has four fields: `data`, `count`, `front`, and `rear`. \n", + "1. `data` is a `UnitAction` object, containing `UNIT_ACTION_QUEUE_SIZE` actions in attribute-first layout, so its attributes have a leading dimension of `UNIT_ACTION_QUEUE_SIZE`.\n", + "2. `count` indicates the number of actions in the queue.\n", + "3. `front` is the index of the first action in the queue. \n", + "4. `rear` is the index of the first empty slot in the queue, which is also the position for next push back. `(rear - 1) % capacity` is the index of the last action in the queue." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Above is the structure of action queue for a single unit. Now, let's have a look at `state.units.action_queue`, which contains the action queue for all units. The main difference is that all leaves are added with two leading dimensions of `(2, MAX_N_UNITS)`. Again, this is because we use attribute-first layout." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
ActionQueue(\n",
+       "    data=UnitAction(\n",
+       "        action_type=ShapedArray(int8[2,200,20]),\n",
+       "        direction=ShapedArray(int8[2,200,20]),\n",
+       "        resource_type=ShapedArray(int8[2,200,20]),\n",
+       "        amount=ShapedArray(int16[2,200,20]),\n",
+       "        repeat=ShapedArray(int16[2,200,20]),\n",
+       "        n=ShapedArray(int16[2,200,20])\n",
+       "    ),\n",
+       "    front=ShapedArray(int8[2,200]),\n",
+       "    rear=ShapedArray(int8[2,200]),\n",
+       "    count=ShapedArray(int8[2,200])\n",
+       ")\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[1;35mActionQueue\u001b[0m\u001b[1m(\u001b[0m\n", + " \u001b[33mdata\u001b[0m=\u001b[1;35mUnitAction\u001b[0m\u001b[1m(\u001b[0m\n", + " \u001b[33maction_type\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m,\u001b[1;36m20\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mdirection\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m,\u001b[1;36m20\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mresource_type\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m,\u001b[1;36m20\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mamount\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint16\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m,\u001b[1;36m20\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mrepeat\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint16\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m,\u001b[1;36m20\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mn\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint16\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m,\u001b[1;36m20\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m\n", + " \u001b[1m)\u001b[0m,\n", + " \u001b[33mfront\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mrear\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mcount\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m\n", + "\u001b[1m)\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "rich.print(jux.tree_util.map_to_aval(state.units.action_queue))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`ActionQueue` provides several useful methods, such as `push_back()`, `push_front()`, `pop()` and `peek()`. However, all these methods can only be applied to the queues of a single unit. `state.units.action_queue` has two batch dimension, one for player, one for units, so we need to `vmap` these methods two times. The following code shows how to peek the next actions of all units. The returned `act` contains the next actions of all units, again, in attribute-first layout." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
UnitAction(\n",
+       "    action_type=ShapedArray(int8[2,200]),\n",
+       "    direction=ShapedArray(int8[2,200]),\n",
+       "    resource_type=ShapedArray(int8[2,200]),\n",
+       "    amount=ShapedArray(int16[2,200]),\n",
+       "    repeat=ShapedArray(int16[2,200]),\n",
+       "    n=ShapedArray(int16[2,200])\n",
+       ")\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[1;35mUnitAction\u001b[0m\u001b[1m(\u001b[0m\n", + " \u001b[33maction_type\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mdirection\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mresource_type\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mamount\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint16\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mrepeat\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint16\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mn\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint16\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m\n", + "\u001b[1m)\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from jux.actions import ActionQueue\n", + "import jax\n", + "peek_vmap_vmap = jax.vmap(jax.vmap(ActionQueue.peek))\n", + "act = peek_vmap_vmap(state.units.action_queue)\n", + "rich.print(jux.tree_util.map_to_aval(act))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### JuxAction\n", + "\n", + "`JuxAction` is a NamedTuple containing both actions for factories and robots, mainly used as input to `JuxEnv.step_late_game()`. It has four fields: `factory_actions`, `unit_action_queue`, `unit_action_queue_count` and `unit_action_queue_update`.\n", + "1. `factory_actions`: the actions for each factories.\n", + "2. `unit_action_queue_update`: a boolean array indicating whether a unit's action queue should be updated or not in this turn.\n", + "3. `unit_action_queue`: What actions will be in the new action queue, If update.\n", + "4. `unit_action_queue_count`: How many actions will there be in the new action queue, if update." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
JuxAction(\n",
+       "    factory_action=ShapedArray(int8[2,11]),\n",
+       "    unit_action_queue=UnitAction(\n",
+       "        action_type=ShapedArray(int8[2,200,20]),\n",
+       "        direction=ShapedArray(int8[2,200,20]),\n",
+       "        resource_type=ShapedArray(int8[2,200,20]),\n",
+       "        amount=ShapedArray(int16[2,200,20]),\n",
+       "        repeat=ShapedArray(int16[2,200,20]),\n",
+       "        n=ShapedArray(int16[2,200,20])\n",
+       "    ),\n",
+       "    unit_action_queue_count=ShapedArray(int8[2,200]),\n",
+       "    unit_action_queue_update=ShapedArray(bool[2,200])\n",
+       ")\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[1;35mJuxAction\u001b[0m\u001b[1m(\u001b[0m\n", + " \u001b[33mfactory_action\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m11\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33munit_action_queue\u001b[0m=\u001b[1;35mUnitAction\u001b[0m\u001b[1m(\u001b[0m\n", + " \u001b[33maction_type\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m,\u001b[1;36m20\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mdirection\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m,\u001b[1;36m20\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mresource_type\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m,\u001b[1;36m20\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mamount\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint16\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m,\u001b[1;36m20\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mrepeat\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint16\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m,\u001b[1;36m20\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mn\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint16\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m,\u001b[1;36m20\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m\n", + " \u001b[1m)\u001b[0m,\n", + " \u001b[33munit_action_queue_count\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33munit_action_queue_update\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mbool\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m\n", + "\u001b[1m)\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "jux_act = JuxAction.from_lux(state, lux_act)\n", + "rich.print(jux.tree_util.map_to_aval(jux_act))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There are four types of actions for factories: `DO_NOTHING`, `BUILD_LIGHT`, `BUILD_HEAVY` and `WATER`, numbered from -1 to 2." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{-1: ,\n", + " 0: ,\n", + " 1: ,\n", + " 2: }" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from jux.actions import FactoryAction\n", + "\n", + "{int(a): a for a in FactoryAction}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Run a Batch of Environments\n", + "Running a single environment cannot take full advantage of the power of modern GPU. This section will show you how to run a batch of environments at once. We first create a `JuxEnvBatch` object." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [], + "source": [ + "from jux.env import JuxEnvBatch\n", + "\n", + "jux_env_batch = JuxEnvBatch(buf_cfg=JuxBufferConfig(MAX_N_UNITS=200))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then, reset the environment to get initial states. Instead of passing in a single seed, we pass in a batch of seeds. The batch size is determined by the size of the seed array." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [], + "source": [ + "import jax.numpy as jnp\n", + "\n", + "batch_size = 2\n", + "seeds = jnp.arange(batch_size)\n", + "\n", + "states = jux_env_batch.reset(seeds)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that the default env rendering function is CPU based and only works for non batched environments. In order to render the environments, we recommend you construct the images you need yourself when needed, especially since generating evaluation videos of 1000s of parallel environments is slow due to I/O of video creationg becoming a bottleneck. Regardless, visualizations are helpful and an example showing the distribution of resources on each batched environment is shown below." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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cjA3qh7XK3OjL2JuVBVzo6qYG1Xly9yoUWxbAkI2evRo+f3vf1/p79jqqwsuuEDeffddOXTokGzatEkGDRok27dvr+1h1Zp9+/bJuHHj5NVXX5UmTZrU9nASo0uXLhX/+9JLL5X27dvLeeedJxs2bJDTTjutFkdWe8rLy+Wqq66Sxx9/XERErrjiCvn9738vCxYskEGDBtXy6AAgPcyN/hdzo8qYG6XG3KgqX+ZGifxExllnnSUNGzas8o2wxcXFkpubW0ujSpbP90N93UdjxoyR559/Xl577TU599xzK17Pzc2V48ePy8GDByv9+7q+Xxo3bizf+MY3pF27djJlyhS57LLLZM6cOfV2f+zevVs+/vhjufLKK6VRo0bSqFEj2b59u8ydO1caNWokOTk59XK/fFnz5s3ln/7pn+SDDz6ot+fKOeecIxdffHGl1y666KKKj5XW93stkoO5UfXq+/XK3Kgy5kaVMTeqGeZG/syNErmQ0bhxY2nXrp1s27at4rXy8nLZtm2bdOjQoRZHlhxt27aV3NzcSvuopKREdu3aVaf3URAEMmbMGNm8ebP86le/krZt21aqt2vXTjIzMyvtlz179sjevXvr9H75svLyciktLa23++Omm26S9957T959992Kn6uuukr69etX8b/r4375siNHjsh//dd/yTnnnFNvz5WOHTtWiSn8z//8TznvvPNEpP7ea5E8zI2qV1+vV+ZGNcPciLlRTTA38mhuFNvXiqZp3bp1QVZWVrB8+fLgj3/8YzB8+PCgefPmQVFRUW0PLTaHDx8O3nnnneCdd94JRCSYOXNm8M477wT//d//HQRBEEydOjVo3rx5sGXLluA//uM/gm7dugVt27YN/ud//qeWRx6dkSNHBtnZ2cHrr78efPTRRxU///jHPyr+zV133RW0adMm+NWvfhW89dZbQYcOHYIOHTrU4qij9cADDwTbt28PPvzww+A//uM/ggceeCDIyMgIXnnllSAI6t/+cPniN3MHQf3cLxMnTgxef/314MMPPwx+85vfBJ06dQrOOuus4OOPPw6CoH7uk9/97ndBo0aNgsceeyx4//33g9WrVwdf+cpXglWrVlX8m/p4r0UyMTdibpQKc6OqmBvVDHMj5kap+DI3SuxCRhAEwbx584I2bdoEjRs3Dq655ppg586dtT2kWL322muBiFT5GTRoUBAEn0XfPPjgg0FOTk6QlZUV3HTTTcGePXtqd9ARS7U/RCRYtmxZxb/5n//5n2DUqFHBmWeeGXzlK18Jbr311uCjjz6qvUFHbMiQIcF5550XNG7cOGjZsmVw0003VTyog6D+7Q+XLz+s6+N+6d27d3DOOecEjRs3Dlq1ahX07t07+OCDDyrq9XGfBEEQbN26NbjkkkuCrKys4MILLwwWLVpUqV4f77VILuZGzI2+jLlRVcyNaoa5EXMjFx/mRhlBEATxff4DAAAAAADALpHfkQEAAAAAAJAKCxkAAAAAAMAbLGQAAAAAAABvsJABAAAAAAC8wUIGAAAAAADwBgsZAAAAAADAGyxkAAAAAAAAb7CQAQAAAAAAvMFCBgAAAAAA8AYLGQAAAAAAwBssZAAAAAAAAG/8/3sUzCYyAr3SAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import cv2\n", + "import numpy as np\n", + "# create images of each episode from board state data in a vectorized fashion. These will generate colors close to the real visualizer, \n", + "# serves as good starting point for building your own visualizations\n", + "# it's a bit more complex to further generate some other details in the game for visualization e.g. units, text etc., in addition to trying to generate \n", + "# 1000s of videos of episodes at a time, making it no faster than simply just evaluating on CPU (I/O of creating videos becomes a bottleneck)\n", + "# we leave that to you the user to decide what to do with visualization with some useful vectorized code below\n", + "\n", + "# rubble = states.board.map.rubble[i]\n", + "imgs = np.zeros((batch_size, 64, 64, 3))\n", + "for c in [0, 1, 2]:\n", + " imgs[:, :, :, c] = ((np.array(states.board.map.rubble) / 100)*(1-0.411) + 0.411)\n", + "# color in ice and ore\n", + "imgs[states.board.map.ice == True] = np.array([72, 219, 251]) / 255.0\n", + "imgs[states.board.map.ore == True] = np.array([44, 62, 80]) / 255.0\n", + "# imgs[states.board.factory_occupancy_map\n", + "\n", + "# for sake of space, we will just plot the first 5 environments\n", + "fig,axs = plt.subplots(1, 2, figsize=(12,5))\n", + "axs = axs.flatten()\n", + "for i in range(len(axs)):\n", + " ax = axs[i] \n", + " ax.imshow(imgs[i])\n", + "plt.tight_layout()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "According to our attribute-first memory layout, states is still a `State` object, but all leaves are added with a leading dimension of `batch_size` as shown below. E.g. `unit_id` transforms from shape `(2, 11)` to `(batch_size, 2, 11)`" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
Factory(\n",
+       "    team_id=ShapedArray(int8[2,2,11]),\n",
+       "    unit_id=ShapedArray(int8[2,2,11]),\n",
+       "    pos=Position(pos=ShapedArray(int8[2,2,11,2])),\n",
+       "    power=ShapedArray(int32[2,2,11]),\n",
+       "    cargo=UnitCargo(stock=ShapedArray(int32[2,2,11,4]))\n",
+       ")\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[1;35mFactory\u001b[0m\u001b[1m(\u001b[0m\n", + " \u001b[33mteam_id\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m2\u001b[0m,\u001b[1;36m11\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33munit_id\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m2\u001b[0m,\u001b[1;36m11\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mpos\u001b[0m=\u001b[1;35mPosition\u001b[0m\u001b[1m(\u001b[0m\u001b[33mpos\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m2\u001b[0m,\u001b[1;36m11\u001b[0m,\u001b[1;36m2\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mpower\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint32\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m2\u001b[0m,\u001b[1;36m11\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mcargo\u001b[0m=\u001b[1;35mUnitCargo\u001b[0m\u001b[1m(\u001b[0m\u001b[33mstock\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint32\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m2\u001b[0m,\u001b[1;36m11\u001b[0m,\u001b[1;36m4\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m\u001b[1m)\u001b[0m\n", + "\u001b[1m)\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "rich.print(jux.tree_util.map_to_aval(states.factories))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The states returned by `JuxEnvBatch.reset()` shares the same environmental config and jux buffer config. In addition, they are also guaranteed to have the same `states.board.factories_per_team`, so they take the same number of turns to place factories." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Array([6, 6], dtype=int8)" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "states.board.factories_per_team" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Similarly, other arguments of `JuxEnvBatch.step_*()` are also batched. All arrays must have a leading dimension of `batch_size`." + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "bid: ShapedArray(int32[2,2])\n", + "faction: ShapedArray(int8[2,2])\n" + ] + } + ], + "source": [ + "# bid step\n", + "bid = jnp.zeros((batch_size, 2), dtype=jnp.int32)\n", + "faction = jnp.zeros((batch_size, 2), dtype=jnp.int8)\n", + "\n", + "states, (observations, rewards, dones, infos) = jux_env_batch.step_bid(states, bid, faction)\n", + "\n", + "print(f\"bid: {bid.aval}\")\n", + "print(f\"faction: {faction.aval}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "spawn: ShapedArray(int8[2,2,2])\n", + "water: ShapedArray(int32[2,2])\n", + "metal: ShapedArray(int32[2,2])\n" + ] + } + ], + "source": [ + "key = jax.random.PRNGKey(0)\n", + "water = jnp.ones((batch_size, 2), dtype=jnp.int32) * 150\n", + "metal = jnp.ones((batch_size, 2), dtype=jnp.int32) * 150\n", + "\n", + "# factory placement step\n", + "factories_per_team = states.board.factories_per_team[0]\n", + "for i in range(factories_per_team * 2):\n", + " # random spawn position\n", + " key, subkey = jax.random.split(key)\n", + " spawn = jax.random.randint(subkey, (batch_size, 2, 2), 0, jux_env_batch.env_cfg.map_size, dtype=jnp.int8)\n", + " states, (observations, rewards, dones, infos) = jux_env_batch.step_factory_placement(states, spawn, water, metal)\n", + "\n", + "print(f\"spawn: {spawn.aval}\")\n", + "print(f\"water: {water.aval}\")\n", + "print(f\"metal: {metal.aval}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
JuxAction(\n",
+       "    factory_action=ShapedArray(int8[2,2,11]),\n",
+       "    unit_action_queue=UnitAction(\n",
+       "        action_type=ShapedArray(int8[2,2,200,20]),\n",
+       "        direction=ShapedArray(int8[2,2,200,20]),\n",
+       "        resource_type=ShapedArray(int8[2,2,200,20]),\n",
+       "        amount=ShapedArray(int16[2,2,200,20]),\n",
+       "        repeat=ShapedArray(int16[2,2,200,20]),\n",
+       "        n=ShapedArray(int16[2,2,200,20])\n",
+       "    ),\n",
+       "    unit_action_queue_count=ShapedArray(int8[2,2,200]),\n",
+       "    unit_action_queue_update=ShapedArray(bool[2,2,200])\n",
+       ")\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[1;35mJuxAction\u001b[0m\u001b[1m(\u001b[0m\n", + " \u001b[33mfactory_action\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m2\u001b[0m,\u001b[1;36m11\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33munit_action_queue\u001b[0m=\u001b[1;35mUnitAction\u001b[0m\u001b[1m(\u001b[0m\n", + " \u001b[33maction_type\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m,\u001b[1;36m20\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mdirection\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m,\u001b[1;36m20\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mresource_type\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m,\u001b[1;36m20\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mamount\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint16\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m,\u001b[1;36m20\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mrepeat\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint16\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m,\u001b[1;36m20\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mn\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint16\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m,\u001b[1;36m20\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m\n", + " \u001b[1m)\u001b[0m,\n", + " \u001b[33munit_action_queue_count\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mint8\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33munit_action_queue_update\u001b[0m=\u001b[1;35mShapedArray\u001b[0m\u001b[1m(\u001b[0mbool\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m,\u001b[1;36m2\u001b[0m,\u001b[1;36m200\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m\n", + "\u001b[1m)\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# a dummy action\n", + "jux_action = JuxAction.empty(\n", + " jux_env_batch.env_cfg, \n", + " jux_env_batch.buf_cfg\n", + ")\n", + "jux_action = jax.tree_map(lambda x: x[None].repeat(batch_size, axis=0), jux_action)\n", + "rich.print(jux.tree_util.map_to_aval(jux_action))\n", + "\n", + "# late game step\n", + "states, (observations, rewards, dones, infos) = jux_env_batch.step_late_game(states, jux_action)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Convert between PyTorch and JAX" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Since PyTorch is widely used in the community, we provide a simple way to convert between PyTorch and JAX. Following code shows how to convert observations (state) from JAX array to PyTorch tensor, so it can be feed into PyTorch models." + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
State(\n",
+       "    env_cfg=None,\n",
+       "    seed=torch.Size([2]),\n",
+       "    rng_state=torch.Size([2, 2]),\n",
+       "    env_steps=torch.Size([2]),\n",
+       "    board=Board(\n",
+       "        seed=torch.Size([2]),\n",
+       "        factories_per_team=torch.Size([2]),\n",
+       "        map=GameMap(\n",
+       "            rubble=torch.Size([2, 64, 64]),\n",
+       "            ice=torch.Size([2, 64, 64]),\n",
+       "            ore=torch.Size([2, 64, 64]),\n",
+       "            symmetry=torch.Size([2])\n",
+       "        ),\n",
+       "        lichen=torch.Size([2, 64, 64]),\n",
+       "        lichen_strains=torch.Size([2, 64, 64]),\n",
+       "        units_map=torch.Size([2, 64, 64]),\n",
+       "        factory_map=torch.Size([2, 64, 64]),\n",
+       "        factory_occupancy_map=torch.Size([2, 64, 64]),\n",
+       "        factory_pos=torch.Size([2, 22, 2])\n",
+       "    ),\n",
+       "    units=Unit(\n",
+       "        unit_type=torch.Size([2, 2, 200]),\n",
+       "        action_queue=ActionQueue(\n",
+       "            data=UnitAction(\n",
+       "                action_type=torch.Size([2, 2, 200, 20]),\n",
+       "                direction=torch.Size([2, 2, 200, 20]),\n",
+       "                resource_type=torch.Size([2, 2, 200, 20]),\n",
+       "                amount=torch.Size([2, 2, 200, 20]),\n",
+       "                repeat=torch.Size([2, 2, 200, 20]),\n",
+       "                n=torch.Size([2, 2, 200, 20])\n",
+       "            ),\n",
+       "            front=torch.Size([2, 2, 200]),\n",
+       "            rear=torch.Size([2, 2, 200]),\n",
+       "            count=torch.Size([2, 2, 200])\n",
+       "        ),\n",
+       "        team_id=torch.Size([2, 2, 200]),\n",
+       "        unit_id=torch.Size([2, 2, 200]),\n",
+       "        pos=Position(pos=torch.Size([2, 2, 200, 2])),\n",
+       "        cargo=UnitCargo(stock=torch.Size([2, 2, 200, 4])),\n",
+       "        power=torch.Size([2, 2, 200])\n",
+       "    ),\n",
+       "    unit_id2idx=torch.Size([2, 2000, 2]),\n",
+       "    n_units=torch.Size([2, 2]),\n",
+       "    factories=Factory(\n",
+       "        team_id=torch.Size([2, 2, 11]),\n",
+       "        unit_id=torch.Size([2, 2, 11]),\n",
+       "        pos=Position(pos=torch.Size([2, 2, 11, 2])),\n",
+       "        power=torch.Size([2, 2, 11]),\n",
+       "        cargo=UnitCargo(stock=torch.Size([2, 2, 11, 4]))\n",
+       "    ),\n",
+       "    factory_id2idx=torch.Size([2, 22, 2]),\n",
+       "    n_factories=torch.Size([2, 2]),\n",
+       "    teams=Team(\n",
+       "        team_id=torch.Size([2, 2]),\n",
+       "        faction=torch.Size([2, 2]),\n",
+       "        init_water=torch.Size([2, 2]),\n",
+       "        init_metal=torch.Size([2, 2]),\n",
+       "        factories_to_place=torch.Size([2, 2]),\n",
+       "        factory_strains=torch.Size([2, 2, 11]),\n",
+       "        n_factory=torch.Size([2, 2]),\n",
+       "        bid=torch.Size([2, 2])\n",
+       "    ),\n",
+       "    global_id=torch.Size([2]),\n",
+       "    place_first=torch.Size([2])\n",
+       ")\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[1;35mState\u001b[0m\u001b[1m(\u001b[0m\n", + " \u001b[33menv_cfg\u001b[0m=\u001b[3;35mNone\u001b[0m,\n", + " \u001b[33mseed\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mrng_state\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m2\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33menv_steps\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mboard\u001b[0m=\u001b[1;35mBoard\u001b[0m\u001b[1m(\u001b[0m\n", + " \u001b[33mseed\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mfactories_per_team\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mmap\u001b[0m=\u001b[1;35mGameMap\u001b[0m\u001b[1m(\u001b[0m\n", + " \u001b[33mrubble\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m64\u001b[0m, \u001b[1;36m64\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mice\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m64\u001b[0m, \u001b[1;36m64\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33more\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m64\u001b[0m, \u001b[1;36m64\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33msymmetry\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m\n", + " \u001b[1m)\u001b[0m,\n", + " \u001b[33mlichen\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m64\u001b[0m, \u001b[1;36m64\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mlichen_strains\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m64\u001b[0m, \u001b[1;36m64\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33munits_map\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m64\u001b[0m, \u001b[1;36m64\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mfactory_map\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m64\u001b[0m, \u001b[1;36m64\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mfactory_occupancy_map\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m64\u001b[0m, \u001b[1;36m64\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mfactory_pos\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m22\u001b[0m, \u001b[1;36m2\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m\n", + " \u001b[1m)\u001b[0m,\n", + " \u001b[33munits\u001b[0m=\u001b[1;35mUnit\u001b[0m\u001b[1m(\u001b[0m\n", + " \u001b[33munit_type\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m2\u001b[0m, \u001b[1;36m200\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33maction_queue\u001b[0m=\u001b[1;35mActionQueue\u001b[0m\u001b[1m(\u001b[0m\n", + " \u001b[33mdata\u001b[0m=\u001b[1;35mUnitAction\u001b[0m\u001b[1m(\u001b[0m\n", + " \u001b[33maction_type\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m2\u001b[0m, \u001b[1;36m200\u001b[0m, \u001b[1;36m20\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mdirection\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m2\u001b[0m, \u001b[1;36m200\u001b[0m, \u001b[1;36m20\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mresource_type\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m2\u001b[0m, \u001b[1;36m200\u001b[0m, \u001b[1;36m20\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mamount\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m2\u001b[0m, \u001b[1;36m200\u001b[0m, \u001b[1;36m20\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mrepeat\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m2\u001b[0m, \u001b[1;36m200\u001b[0m, \u001b[1;36m20\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mn\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m2\u001b[0m, \u001b[1;36m200\u001b[0m, \u001b[1;36m20\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m\n", + " \u001b[1m)\u001b[0m,\n", + " \u001b[33mfront\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m2\u001b[0m, \u001b[1;36m200\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mrear\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m2\u001b[0m, \u001b[1;36m200\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mcount\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m2\u001b[0m, \u001b[1;36m200\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m\n", + " \u001b[1m)\u001b[0m,\n", + " \u001b[33mteam_id\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m2\u001b[0m, \u001b[1;36m200\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33munit_id\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m2\u001b[0m, \u001b[1;36m200\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mpos\u001b[0m=\u001b[1;35mPosition\u001b[0m\u001b[1m(\u001b[0m\u001b[33mpos\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m2\u001b[0m, \u001b[1;36m200\u001b[0m, \u001b[1;36m2\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mcargo\u001b[0m=\u001b[1;35mUnitCargo\u001b[0m\u001b[1m(\u001b[0m\u001b[33mstock\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m2\u001b[0m, \u001b[1;36m200\u001b[0m, \u001b[1;36m4\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mpower\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m2\u001b[0m, \u001b[1;36m200\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m\n", + " \u001b[1m)\u001b[0m,\n", + " \u001b[33munit_id2idx\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m2000\u001b[0m, \u001b[1;36m2\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mn_units\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m2\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mfactories\u001b[0m=\u001b[1;35mFactory\u001b[0m\u001b[1m(\u001b[0m\n", + " \u001b[33mteam_id\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m2\u001b[0m, \u001b[1;36m11\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33munit_id\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m2\u001b[0m, \u001b[1;36m11\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mpos\u001b[0m=\u001b[1;35mPosition\u001b[0m\u001b[1m(\u001b[0m\u001b[33mpos\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m2\u001b[0m, \u001b[1;36m11\u001b[0m, \u001b[1;36m2\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mpower\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m2\u001b[0m, \u001b[1;36m11\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mcargo\u001b[0m=\u001b[1;35mUnitCargo\u001b[0m\u001b[1m(\u001b[0m\u001b[33mstock\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m2\u001b[0m, \u001b[1;36m11\u001b[0m, \u001b[1;36m4\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m\u001b[1m)\u001b[0m\n", + " \u001b[1m)\u001b[0m,\n", + " \u001b[33mfactory_id2idx\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m22\u001b[0m, \u001b[1;36m2\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mn_factories\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m2\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mteams\u001b[0m=\u001b[1;35mTeam\u001b[0m\u001b[1m(\u001b[0m\n", + " \u001b[33mteam_id\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m2\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mfaction\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m2\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33minit_water\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m2\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33minit_metal\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m2\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mfactories_to_place\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m2\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mfactory_strains\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m2\u001b[0m, \u001b[1;36m11\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mn_factory\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m2\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mbid\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m, \u001b[1;36m2\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m\n", + " \u001b[1m)\u001b[0m,\n", + " \u001b[33mglobal_id\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[33mplace_first\u001b[0m=\u001b[1;35mtorch\u001b[0m\u001b[1;35m.Size\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m\n", + "\u001b[1m)\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# remove env_cfg to reduce output size\n", + "torch_state = states._replace(env_cfg=None).to_torch()\n", + "rich.print(jax.tree_map(lambda x: x.shape, torch_state))" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can also convert between PyTorch tensors and JAX arrays by `jux.torch.from_torch` and `jux.torch.to_torch`. Typical usage is to convert actions generated by PyTorch models to JAX arrays, so they can be feed into `JuxEnv.step_*()`." + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=int32)" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import torch\n", + "import jux.torch\n", + "\n", + "jux.torch.from_torch(torch.arange(10))" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "tensor([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=torch.int32)" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "jux.torch.to_torch(jnp.arange(10))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There is also a utility function `JuxAction.from_torch()` for build a valid `JuxAction` object from PyTorch tensors." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.0" + }, + "vscode": { + "interpreter": { + "hash": "d4d1e4263499bec80672ea0156c357c1ee493ec2b1c70f0acce89fc37c4a6abe" + } + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/vec_noise-1.1.4/CHANGES.txt b/vec_noise-1.1.4/CHANGES.txt new file mode 100644 index 0000000..d025c6c --- /dev/null +++ b/vec_noise-1.1.4/CHANGES.txt @@ -0,0 +1,23 @@ +1.1.4 -- August 1, 2017 + + - Fix Python 3 crash + +1.1.3 -- June 8, 2017 + + - Add build-time dependency on numpy + +1.1.2 -- May 8, 2017 + + - Fix scalar fallback detection + +1.1.1 -- May 8, 2017 + + - Dummy version for re-upload to PyPI + +1.1.0 -- May 8, 2017 + + - Add vectorized versions of the simplex noise functions + +1.0.0 -- April 11, 2017 + + - Initial fork from https://github.com/caseman/noise diff --git a/vec_noise-1.1.4/LICENSE.txt b/vec_noise-1.1.4/LICENSE.txt new file mode 100644 index 0000000..88b5af3 --- /dev/null +++ b/vec_noise-1.1.4/LICENSE.txt @@ -0,0 +1,20 @@ +Original work (noise library) Copyright (c) 2008 Casey Duncan +Modified work (vec_noise library) Copyright (c) 2017 Zev Benjamin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vec_noise-1.1.4/MANIFEST.in b/vec_noise-1.1.4/MANIFEST.in new file mode 100644 index 0000000..daef09f --- /dev/null +++ b/vec_noise-1.1.4/MANIFEST.in @@ -0,0 +1,3 @@ +global-exclude *.pyc *.pyo *.o +include *.py *.c *.h *.txt MANIFEST.in MANIFEST +recursive-include examples *.txt *.py *.png *.jpg diff --git a/vec_noise-1.1.4/PKG-INFO b/vec_noise-1.1.4/PKG-INFO new file mode 100644 index 0000000..e9d6d4f --- /dev/null +++ b/vec_noise-1.1.4/PKG-INFO @@ -0,0 +1,37 @@ +Metadata-Version: 1.1 +Name: vec_noise +Version: 1.1.4 +Summary: Vectorized Perlin noise for Python +Home-page: https://github.com/zbenjamin/vec_noise +Author: Zev Benjamin +Author-email: zev@strangersgate.com +License: UNKNOWN +Description: This is a fork of Casey Duncan's noise library that vectorizes all of the noise + functions using NumPy. It is much faster than the original for computing noise + values at many coordinates. + + Perlin noise is ubiquitous in modern CGI. Used for procedural texturing, + animation, and enhancing realism, Perlin noise has been called the "salt" of + procedural content. Perlin noise is a type of gradient noise, smoothly + interpolating across a pseudo-random matrix of values. + + The vec_noise library includes native-code implementations of Perlin "improved" + noise and Perlin simplex noise. It also includes a fast implementation of + Perlin noise in GLSL, for use in OpenGL shaders. The shader code and many of + the included examples require Pyglet (http://www.pyglet.org), the native-code + noise functions themselves do not, however. + + The Perlin improved noise functions can also generate fBm (fractal Brownian + motion) noise by combining multiple octaves of Perlin noise. Shader functions + for convenient generation of turbulent noise are also included. + +Platform: UNKNOWN +Classifier: Development Status :: 4 - Beta +Classifier: Topic :: Multimedia :: Graphics +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: MacOS :: MacOS X +Classifier: Operating System :: Microsoft :: Windows +Classifier: Operating System :: POSIX +Classifier: Programming Language :: C +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 3 diff --git a/vec_noise-1.1.4/README.rst b/vec_noise-1.1.4/README.rst new file mode 100644 index 0000000..8744770 --- /dev/null +++ b/vec_noise-1.1.4/README.rst @@ -0,0 +1,65 @@ +Vectorized native-code and shader implementations of Perlin noise for Python +============================================================================ + +This is a fork of Casey Duncan's `noise +library `_ that vectorizes all of the noise +functions using `NumPy `_. It is much faster than the +original for computing noise values at many coordinates. + +This package is designed to give you simple to use, fast functions for +generating Perlin noise in your Python programs. Perlin noise is famously +called the "salt" of procedural generation, as it adds considerable flavor in +its application. Noise is commonly used for imparting realism in textures, +animation and other procedural content generation -- placement of hairs, +heights of mountains, density of forests, waving of a flag, etc. etc.. + +Background +========== + +Ken Perlin invented the technique implemented in these algorithms following +his work on the CGI for the movie Tron. Over time Perlin noise has become +ubiquitous in CGI, and greatly contributed to the huge leap in realism that +followed. + +An excellent "from the horse's mouth" overview of Perlin noise can be found +here: http://www.noisemachine.com/talk1/ + +An excellent discussion of simplex noise can be found here: +http://zach.in.tu-clausthal.de/teaching/cg2_08/literatur/simplexnoise.pdf + +The vec_noise library includes native-code implementations of Perlin "improved" +noise and Perlin simplex noise. It also includes a fast implementation of +Perlin noise in GLSL, for use in OpenGL shaders. The shader code and many of +the included examples require Pyglet (http://www.pyglet.org), the native-code +noise functions themselves do not, however. + +The Perlin improved noise functions can also generate fBm (fractal Brownian +motion) noise by combining multiple octaves of Perlin noise. Functions for +convenient generation of turbulent noise in shaders are also included. + +Installation +============ + +Installation uses the standard Python distutils regime:: + + python setup.py install + + +This will compile and install the noise package into your Python site +packages. + +Usage +===== + +The functions and their signatures are documented in their respective +docstrings. Use the Python help() function to read them:: + + >>> import vec_noise + >>> help(vec_noise) + +The examples directory contains sample programs using the noise functions. + +Thanks +====== + +Blue planet texture used for atmosphere example courtesy NASA diff --git a/vec_noise-1.1.4/__init__.py b/vec_noise-1.1.4/__init__.py new file mode 100644 index 0000000..faa075c --- /dev/null +++ b/vec_noise-1.1.4/__init__.py @@ -0,0 +1,20 @@ +"""Noise functions for procedural generation of content + +Contains native code implementations of Perlin improved noise (with +fBm capabilities) and Perlin simplex noise. Also contains a fast +"fake noise" implementation in GLSL for execution in shaders. + +Copyright (c) 2008, Casey Duncan (casey dot duncan at gmail dot com) +Copyright (c) 2017, Zev Benjamin +""" + +__version__ = "1.1.4" + +from . import _perlin, _simplex + +snoise2 = _simplex.noise2 +snoise3 = _simplex.noise3 +snoise4 = _simplex.noise4 +pnoise1 = _perlin.noise1 +pnoise2 = _perlin.noise2 +pnoise3 = _perlin.noise3 diff --git a/vec_noise-1.1.4/_noise.h b/vec_noise-1.1.4/_noise.h new file mode 100644 index 0000000..0f38323 --- /dev/null +++ b/vec_noise-1.1.4/_noise.h @@ -0,0 +1,108 @@ +// -*- mode: c; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; -*- +// +// Copyright (c) 2008, Casey Duncan (casey dot duncan at gmail dot com) +// see LICENSE.txt for details + +#include + +#if defined(_MSC_VER) +#define inline _inline +#define M_1_PI 0.31830988618379067154 +#endif + +const float GRAD3[][3] = { + {1,1,0},{-1,1,0},{1,-1,0},{-1,-1,0}, + {1,0,1},{-1,0,1},{1,0,-1},{-1,0,-1}, + {0,1,1},{0,-1,1},{0,1,-1},{0,-1,-1}, + {1,0,-1},{-1,0,-1},{0,-1,1},{0,1,1}}; + +const float GRAD4[][4] = { + {0,1,1,1}, {0,1,1,-1}, {0,1,-1,1}, {0,1,-1,-1}, + {0,-1,1,1}, {0,-1,1,-1}, {0,-1,-1,1}, {0,-1,-1,-1}, + {1,0,1,1}, {1,0,1,-1}, {1,0,-1,1}, {1,0,-1,-1}, + {-1,0,1,1}, {-1,0,1,-1}, {-1,0,-1,1}, {-1,0,-1,-1}, + {1,1,0,1}, {1,1,0,-1}, {1,-1,0,1}, {1,-1,0,-1}, + {-1,1,0,1}, {-1,1,0,-1}, {-1,-1,0,1}, {-1,-1,0,-1}, + {1,1,1,0}, {1,1,-1,0}, {1,-1,1,0}, {1,-1,-1,0}, + {-1,1,1,0}, {-1,1,-1,0}, {-1,-1,1,0}, {-1,-1,-1,0}}; + +// At the possible cost of unaligned access, we use char instead of +// int here to try to ensure that this table fits in L1 cache +const unsigned char PERM[] = { + 151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, + 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247, 120, + 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33, + 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, + 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, + 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161, + 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196, 135, 130, + 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250, + 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, + 47, 16, 58, 17, 182, 189, 28, 42, 223, 183, 170, 213, 119, 248, 152, 2, 44, + 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9, 129, 22, 39, 253, 19, 98, + 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, 218, 246, 97, 228, 251, 34, + 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14, + 239, 107, 49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121, + 50, 45, 127, 4, 150, 254, 138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, + 141, 128, 195, 78, 66, 215, 61, 156, 180, 151, 160, 137, 91, 90, 15, 131, + 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, + 240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, + 219, 203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125, + 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, + 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, + 40, 244, 102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, + 208, 89, 18, 169, 200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, + 173, 186, 3, 64, 52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, + 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, 223, + 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, + 43, 172, 9, 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, + 112, 104, 218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, + 162, 241, 81, 51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181, 199, + 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, + 205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, + 180}; + +const unsigned char SIMPLEX[][4] = { + {0,1,2,3},{0,1,3,2},{0,0,0,0},{0,2,3,1},{0,0,0,0},{0,0,0,0},{0,0,0,0}, + {1,2,3,0},{0,2,1,3},{0,0,0,0},{0,3,1,2},{0,3,2,1},{0,0,0,0},{0,0,0,0}, + {0,0,0,0},{1,3,2,0},{0,0,0,0},{0,0,0,0},{0,0,0,0},{0,0,0,0},{0,0,0,0}, + {0,0,0,0},{0,0,0,0},{0,0,0,0},{1,2,0,3},{0,0,0,0},{1,3,0,2},{0,0,0,0}, + {0,0,0,0},{0,0,0,0},{2,3,0,1},{2,3,1,0},{1,0,2,3},{1,0,3,2},{0,0,0,0}, + {0,0,0,0},{0,0,0,0},{2,0,3,1},{0,0,0,0},{2,1,3,0},{0,0,0,0},{0,0,0,0}, + {0,0,0,0},{0,0,0,0},{0,0,0,0},{0,0,0,0},{0,0,0,0},{0,0,0,0},{2,0,1,3}, + {0,0,0,0},{0,0,0,0},{0,0,0,0},{3,0,1,2},{3,0,2,1},{0,0,0,0},{3,1,2,0}, + {2,1,0,3},{0,0,0,0},{0,0,0,0},{0,0,0,0},{3,1,0,2},{0,0,0,0},{3,2,0,1}, + {3,2,1,0}}; + +#define fastfloor(n) (int)(n) - (((n) < 0.0f) & ((n) != (int)(n))) + +// Fast sine/cosine functions from +// http://devmaster.net/forums/topic/4648-fast-and-accurate-sinecosine/page__st__80 +// Note the input to these functions is not radians +// instead x = [0, 2] for r = [0, 2*PI] + +inline float fast_sin(float x) +{ + // Convert the input value to a range of -1 to 1 + // x = x * (1.0f / PI); + + // Wrap around + volatile float z = (x + 25165824.0f); + x = x - (z - 25165824.0f); + + #if LOW_SINE_PRECISION + return 4.0f * (x - x * fabsf(x)); + #else + { + float y = x - x * fabsf(x); + const float Q = 3.1f; + const float P = 3.6f; + return y * (Q + P * fabsf(y)); + } + #endif +} + +inline float fast_cos(float x) +{ + return fast_sin(x + 0.5f); +} diff --git a/vec_noise-1.1.4/_perlin.c b/vec_noise-1.1.4/_perlin.c new file mode 100644 index 0000000..b871b11 --- /dev/null +++ b/vec_noise-1.1.4/_perlin.c @@ -0,0 +1,307 @@ +// -*- mode: c; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; -*- +// +// Copyright (c) 2008, Casey Duncan (casey dot duncan at gmail dot com) +// Copyright (c) 2017, Zev Benjamin +// see LICENSE.txt for details + +#include "Python.h" +#include +#include +#include "_noise.h" + +#ifdef _MSC_VER +#define inline __inline +#endif + +#define lerp(t, a, b) ((a) + (t) * ((b) - (a))) + +float inline +grad1(const int hash, const float x) +{ + float g = (hash & 7) + 1.0f; + if (hash & 8) + g = -1; + return (g * x); +} + +float +noise1(float x, const int repeat, const int base) +{ + float fx; + int i = (int)floorf(x) % repeat; + int ii = (i + 1) % repeat; + i = (i & 255) + base; + ii = (ii & 255) + base; + + x -= floorf(x); + fx = x*x*x * (x * (x * 6 - 15) + 10); + + return lerp(fx, grad1(PERM[i], x), grad1(PERM[ii], x - 1)) * 0.4f; +} + +static PyObject * +py_noise1(PyObject *self, PyObject *args, PyObject *kwargs) +{ + float x; + int octaves = 1; + float persistence = 0.5f; + float lacunarity = 2.0f; + int repeat = 1024; // arbitrary + int base = 0; + + static char *kwlist[] = {"x", "octaves", "persistence", "lacunarity", "repeat", "base", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "f|iffii:noise1", kwlist, + &x, &octaves, &persistence, &lacunarity, &repeat, &base)) + return NULL; + + if (octaves == 1) { + // Single octave, return simple noise + return (PyObject *) PyFloat_FromDouble((double) noise1(x, repeat, base)); + } else if (octaves > 1) { + int i; + float freq = 1.0f; + float amp = 1.0f; + float max = 0.0f; + float total = 0.0f; + + for (i = 0; i < octaves; i++) { + total += noise1(x * freq, (const int)(repeat * freq), base) * amp; + max += amp; + freq *= lacunarity; + amp *= persistence; + } + return (PyObject *) PyFloat_FromDouble((double) (total / max)); + } else { + PyErr_SetString(PyExc_ValueError, "Expected octaves value > 0"); + return NULL; + } +} + +float inline +grad2(const int hash, const float x, const float y) +{ + const int h = hash & 15; + return x * GRAD3[h][0] + y * GRAD3[h][1]; +} + +float +noise2(float x, float y, const float repeatx, const float repeaty, const int base) +{ + float fx, fy; + int A, AA, AB, B, BA, BB; + int i = (int)floorf(fmodf(x, repeatx)); + int j = (int)floorf(fmodf(y, repeaty)); + int ii = (int)fmodf(i + 1, repeatx); + int jj = (int)fmodf(j + 1, repeaty); + i = (i & 255) + base; + j = (j & 255) + base; + ii = (ii & 255) + base; + jj = (jj & 255) + base; + + x -= floorf(x); y -= floorf(y); + fx = x*x*x * (x * (x * 6 - 15) + 10); + fy = y*y*y * (y * (y * 6 - 15) + 10); + + A = PERM[i]; + AA = PERM[A + j]; + AB = PERM[A + jj]; + B = PERM[ii]; + BA = PERM[B + j]; + BB = PERM[B + jj]; + + return lerp(fy, lerp(fx, grad2(PERM[AA], x, y), + grad2(PERM[BA], x - 1, y)), + lerp(fx, grad2(PERM[AB], x, y - 1), + grad2(PERM[BB], x - 1, y - 1))); +} + +static PyObject * +py_noise2(PyObject *self, PyObject *args, PyObject *kwargs) +{ + float x, y; + int octaves = 1; + float persistence = 0.5f; + float lacunarity = 2.0f; + float repeatx = 1024; // arbitrary + float repeaty = 1024; // arbitrary + int base = 0; + + static char *kwlist[] = {"x", "y", "octaves", "persistence", "lacunarity", "repeatx", "repeaty", "base", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ff|iffffi:noise2", kwlist, + &x, &y, &octaves, &persistence, &lacunarity, &repeatx, &repeaty, &base)) + return NULL; + + if (octaves == 1) { + // Single octave, return simple noise + return (PyObject *) PyFloat_FromDouble((double) noise2(x, y, repeatx, repeaty, base)); + } else if (octaves > 1) { + int i; + float freq = 1.0f; + float amp = 1.0f; + float max = 0.0f; + float total = 0.0f; + + for (i = 0; i < octaves; i++) { + total += noise2(x * freq, y * freq, repeatx * freq, repeaty * freq, base) * amp; + max += amp; + freq *= lacunarity; + amp *= persistence; + } + return (PyObject *) PyFloat_FromDouble((double) (total / max)); + } else { + PyErr_SetString(PyExc_ValueError, "Expected octaves value > 0"); + return NULL; + } +} + +float inline +grad3(const int hash, const float x, const float y, const float z) +{ + const int h = hash & 15; + return x * GRAD3[h][0] + y * GRAD3[h][1] + z * GRAD3[h][2]; +} + +float +noise3(float x, float y, float z, const int repeatx, const int repeaty, const int repeatz, + const int base) +{ + float fx, fy, fz; + int A, AA, AB, B, BA, BB; + int i = (int)floorf(fmodf(x, repeatx)); + int j = (int)floorf(fmodf(y, repeaty)); + int k = (int)floorf(fmodf(z, repeatz)); + int ii = (int)fmodf(i + 1, repeatx); + int jj = (int)fmodf(j + 1, repeaty); + int kk = (int)fmodf(k + 1, repeatz); + i = (i & 255) + base; + j = (j & 255) + base; + k = (k & 255) + base; + ii = (ii & 255) + base; + jj = (jj & 255) + base; + kk = (kk & 255) + base; + + x -= floorf(x); y -= floorf(y); z -= floorf(z); + fx = x*x*x * (x * (x * 6 - 15) + 10); + fy = y*y*y * (y * (y * 6 - 15) + 10); + fz = z*z*z * (z * (z * 6 - 15) + 10); + + A = PERM[i]; + AA = PERM[A + j]; + AB = PERM[A + jj]; + B = PERM[ii]; + BA = PERM[B + j]; + BB = PERM[B + jj]; + + return lerp(fz, lerp(fy, lerp(fx, grad3(PERM[AA + k], x, y, z), + grad3(PERM[BA + k], x - 1, y, z)), + lerp(fx, grad3(PERM[AB + k], x, y - 1, z), + grad3(PERM[BB + k], x - 1, y - 1, z))), + lerp(fy, lerp(fx, grad3(PERM[AA + kk], x, y, z - 1), + grad3(PERM[BA + kk], x - 1, y, z - 1)), + lerp(fx, grad3(PERM[AB + kk], x, y - 1, z - 1), + grad3(PERM[BB + kk], x - 1, y - 1, z - 1)))); +} + +static PyObject * +py_noise3(PyObject *self, PyObject *args, PyObject *kwargs) +{ + float x, y, z; + int octaves = 1; + float persistence = 0.5f; + float lacunarity = 2.0f; + int repeatx = 1024; // arbitrary + int repeaty = 1024; // arbitrary + int repeatz = 1024; // arbitrary + int base = 0; + + static char *kwlist[] = {"x", "y", "z", "octaves", "persistence", "lacunarity", + "repeatx", "repeaty", "repeatz", "base", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "fff|iffiiii:noise3", kwlist, + &x, &y, &z, &octaves, &persistence, &lacunarity, &repeatx, &repeaty, &repeatz, &base)) + return NULL; + + if (octaves == 1) { + // Single octave, return simple noise + return (PyObject *) PyFloat_FromDouble((double) noise3(x, y, z, + repeatx, repeaty, repeatz, base)); + } else if (octaves > 1) { + int i; + float freq = 1.0f; + float amp = 1.0f; + float max = 0.0f; + float total = 0.0f; + + for (i = 0; i < octaves; i++) { + total += noise3(x * freq, y * freq, z * freq, + (const int)(repeatx*freq), (const int)(repeaty*freq), (const int)(repeatz*freq), base) * amp; + max += amp; + freq *= lacunarity; + amp *= persistence; + } + return (PyObject *) PyFloat_FromDouble((double) (total / max)); + } else { + PyErr_SetString(PyExc_ValueError, "Expected octaves value > 0"); + return NULL; + } +} + +static PyMethodDef perlin_functions[] = { + {"noise1", (PyCFunction) py_noise1, METH_VARARGS | METH_KEYWORDS, + "noise1(x, octaves=1, persistence=0.5, lacunarity=2.0, repeat=1024, base=0.0)\n\n" + "1 dimensional perlin improved noise function (see noise3 for more info)"}, + {"noise2", (PyCFunction) py_noise2, METH_VARARGS | METH_KEYWORDS, + "noise2(x, y, octaves=1, persistence=0.5, lacunarity=2.0, repeatx=1024, repeaty=1024, base=0.0)\n\n" + "2 dimensional perlin improved noise function (see noise3 for more info)"}, + {"noise3", (PyCFunction) py_noise3, METH_VARARGS | METH_KEYWORDS, + "noise3(x, y, z, octaves=1, persistence=0.5, lacunarity=2.0, " + "repeatx=1024, repeaty=1024, repeatz=1024, base=0.0)\n\n" + "return perlin \"improved\" noise value for specified coordinate\n\n" + "octaves -- specifies the number of passes for generating fBm noise,\n" + "defaults to 1 (simple noise).\n\n" + "persistence -- specifies the amplitude of each successive octave relative\n" + "to the one below it. Defaults to 0.5 (each higher octave's amplitude\n" + "is halved). Note the amplitude of the first pass is always 1.0.\n\n" + "lacunarity -- specifies the frequency of each successive octave relative\n" + "to the one below it, similar to persistence. Defaults to 2.0.\n\n" + "repeatx, repeaty, repeatz -- specifies the interval along each axis when \n" + "the noise values repeat. This can be used as the tile size for creating \n" + "tileable textures\n\n" + "base -- specifies a fixed offset for the input coordinates. Useful for\n" + "generating different noise textures with the same repeat interval"}, + {NULL} +}; + +PyDoc_STRVAR(module_doc, "Native-code tileable Perlin \"improved\" noise functions"); + +#if PY_MAJOR_VERSION >= 3 + +static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "_perlin", + module_doc, + -1, /* m_size */ + perlin_functions, /* m_methods */ + NULL, /* m_reload (unused) */ + NULL, /* m_traverse */ + NULL, /* m_clear */ + NULL /* m_free */ +}; + +PyObject * +PyInit__perlin(void) +{ + return PyModule_Create(&moduledef); +} + +#else + +void +init_perlin(void) +{ + Py_InitModule3("_perlin", perlin_functions, module_doc); +} + +#endif diff --git a/vec_noise-1.1.4/_simplex.c b/vec_noise-1.1.4/_simplex.c new file mode 100644 index 0000000..418e833 --- /dev/null +++ b/vec_noise-1.1.4/_simplex.c @@ -0,0 +1,774 @@ +// -*- mode: c; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; -*- +// +// Copyright (c) 2008, Casey Duncan (casey dot duncan at gmail dot com) +// Copyright (c) 2017, Zev Benjamin +// see LICENSE.txt for details + +#include "Python.h" +#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION +#include +#include +#include +#include "_noise.h" + +typedef struct _NoiseArgs NoiseArgs; + +typedef PyObject* (*ScalarFunc)(NoiseArgs* args); +typedef float (*DispatchFunc)(NoiseArgs* args, float **coord); + +typedef struct _NoiseArgs { + int ndims; + int nops; + // dim_vals is an array of the form [xs, ys, ...] + PyObject **dim_vals; + PyArrayObject **op; + npy_uint32 *op_flags; + PyArray_Descr **op_dtypes; + + ScalarFunc scalar_func; + DispatchFunc dispatch_func; + + int octaves; + float persistence; + float lacunarity; + + // Only used for noise2 + float repeatx; + float repeaty; + float z; +} NoiseArgs; + +// 2D simplex skew factors +#define F2 0.3660254037844386f // 0.5 * (sqrt(3.0) - 1.0) +#define G2 0.21132486540518713f // (3.0 - sqrt(3.0)) / 6.0 + +float +noise2(float x, float y) +{ + int i1, j1, I, J, c; + float s = (x + y) * F2; + float i = floorf(x + s); + float j = floorf(y + s); + float t = (i + j) * G2; + + float xx[3], yy[3], f[3]; + float noise[3] = {0.0f, 0.0f, 0.0f}; + int g[3]; + + xx[0] = x - (i - t); + yy[0] = y - (j - t); + + i1 = xx[0] > yy[0]; + j1 = xx[0] <= yy[0]; + + xx[2] = xx[0] + G2 * 2.0f - 1.0f; + yy[2] = yy[0] + G2 * 2.0f - 1.0f; + xx[1] = xx[0] - i1 + G2; + yy[1] = yy[0] - j1 + G2; + + I = (int) i & 255; + J = (int) j & 255; + g[0] = PERM[I + PERM[J]] % 12; + g[1] = PERM[I + i1 + PERM[J + j1]] % 12; + g[2] = PERM[I + 1 + PERM[J + 1]] % 12; + + for (c = 0; c <= 2; c++) + f[c] = 0.5f - xx[c]*xx[c] - yy[c]*yy[c]; + + for (c = 0; c <= 2; c++) + if (f[c] > 0) + noise[c] = f[c]*f[c]*f[c]*f[c] * (GRAD3[g[c]][0]*xx[c] + GRAD3[g[c]][1]*yy[c]); + + return (noise[0] + noise[1] + noise[2]) * 70.0f; +} + +#define dot3(v1, v2) ((v1)[0]*(v2)[0] + (v1)[1]*(v2)[1] + (v1)[2]*(v2)[2]) + +#define ASSIGN(a, v0, v1, v2) (a)[0] = v0; (a)[1] = v1; (a)[2] = v2; + +#define F3 (1.0f / 3.0f) +#define G3 (1.0f / 6.0f) + +float +noise3(float x, float y, float z) +{ + int c, o1[3], o2[3], g[4], I, J, K; + float f[4], noise[4] = {0.0f, 0.0f, 0.0f, 0.0f}; + float s = (x + y + z) * F3; + float i = floorf(x + s); + float j = floorf(y + s); + float k = floorf(z + s); + float t = (i + j + k) * G3; + + float pos[4][3]; + + pos[0][0] = x - (i - t); + pos[0][1] = y - (j - t); + pos[0][2] = z - (k - t); + + if (pos[0][0] >= pos[0][1]) { + if (pos[0][1] >= pos[0][2]) { + ASSIGN(o1, 1, 0, 0); + ASSIGN(o2, 1, 1, 0); + } else if (pos[0][0] >= pos[0][2]) { + ASSIGN(o1, 1, 0, 0); + ASSIGN(o2, 1, 0, 1); + } else { + ASSIGN(o1, 0, 0, 1); + ASSIGN(o2, 1, 0, 1); + } + } else { + if (pos[0][1] < pos[0][2]) { + ASSIGN(o1, 0, 0, 1); + ASSIGN(o2, 0, 1, 1); + } else if (pos[0][0] < pos[0][2]) { + ASSIGN(o1, 0, 1, 0); + ASSIGN(o2, 0, 1, 1); + } else { + ASSIGN(o1, 0, 1, 0); + ASSIGN(o2, 1, 1, 0); + } + } + + for (c = 0; c <= 2; c++) { + pos[3][c] = pos[0][c] - 1.0f + 3.0f * G3; + pos[2][c] = pos[0][c] - o2[c] + 2.0f * G3; + pos[1][c] = pos[0][c] - o1[c] + G3; + } + + I = (int) i & 255; + J = (int) j & 255; + K = (int) k & 255; + g[0] = PERM[I + PERM[J + PERM[K]]] % 12; + g[1] = PERM[I + o1[0] + PERM[J + o1[1] + PERM[o1[2] + K]]] % 12; + g[2] = PERM[I + o2[0] + PERM[J + o2[1] + PERM[o2[2] + K]]] % 12; + g[3] = PERM[I + 1 + PERM[J + 1 + PERM[K + 1]]] % 12; + + for (c = 0; c <= 3; c++) { + f[c] = 0.6f - pos[c][0]*pos[c][0] - pos[c][1]*pos[c][1] - pos[c][2]*pos[c][2]; + } + + for (c = 0; c <= 3; c++) { + if (f[c] > 0) { + noise[c] = f[c]*f[c]*f[c]*f[c] * dot3(pos[c], GRAD3[g[c]]); + } + } + + return (noise[0] + noise[1] + noise[2] + noise[3]) * 32.0f; +} + +inline float +fbm_noise3(float x, float y, float z, int octaves, float persistence, float lacunarity) { + float freq = 1.0f; + float amp = 1.0f; + float max = 1.0f; + float total = noise3(x, y, z); + int i; + + for (i = 1; i < octaves; ++i) { + freq *= lacunarity; + amp *= persistence; + max += amp; + total += noise3(x * freq, y * freq, z * freq) * amp; + } + return total / max; +} + +#define dot4(v1, x, y, z, w) ((v1)[0]*(x) + (v1)[1]*(y) + (v1)[2]*(z) + (v1)[3]*(w)) + +#define F4 0.30901699437494745f /* (sqrt(5.0) - 1.0) / 4.0 */ +#define G4 0.1381966011250105f /* (5.0 - sqrt(5.0)) / 20.0 */ + +float +noise4(float x, float y, float z, float w) { + float noise[5] = {0.0f, 0.0f, 0.0f, 0.0f, 0.0f}; + + float s = (x + y + z + w) * F4; + float i = floorf(x + s); + float j = floorf(y + s); + float k = floorf(z + s); + float l = floorf(w + s); + float t = (i + j + k + l) * G4; + + float x0 = x - (i - t); + float y0 = y - (j - t); + float z0 = z - (k - t); + float w0 = w - (l - t); + + int c = (x0 > y0)*32 + (x0 > z0)*16 + (y0 > z0)*8 + (x0 > w0)*4 + (y0 > w0)*2 + (z0 > w0); + int i1 = SIMPLEX[c][0]>=3; + int j1 = SIMPLEX[c][1]>=3; + int k1 = SIMPLEX[c][2]>=3; + int l1 = SIMPLEX[c][3]>=3; + int i2 = SIMPLEX[c][0]>=2; + int j2 = SIMPLEX[c][1]>=2; + int k2 = SIMPLEX[c][2]>=2; + int l2 = SIMPLEX[c][3]>=2; + int i3 = SIMPLEX[c][0]>=1; + int j3 = SIMPLEX[c][1]>=1; + int k3 = SIMPLEX[c][2]>=1; + int l3 = SIMPLEX[c][3]>=1; + + float x1 = x0 - i1 + G4; + float y1 = y0 - j1 + G4; + float z1 = z0 - k1 + G4; + float w1 = w0 - l1 + G4; + float x2 = x0 - i2 + 2.0f*G4; + float y2 = y0 - j2 + 2.0f*G4; + float z2 = z0 - k2 + 2.0f*G4; + float w2 = w0 - l2 + 2.0f*G4; + float x3 = x0 - i3 + 3.0f*G4; + float y3 = y0 - j3 + 3.0f*G4; + float z3 = z0 - k3 + 3.0f*G4; + float w3 = w0 - l3 + 3.0f*G4; + float x4 = x0 - 1.0f + 4.0f*G4; + float y4 = y0 - 1.0f + 4.0f*G4; + float z4 = z0 - 1.0f + 4.0f*G4; + float w4 = w0 - 1.0f + 4.0f*G4; + + int I = (int)i & 255; + int J = (int)j & 255; + int K = (int)k & 255; + int L = (int)l & 255; + int gi0 = PERM[I + PERM[J + PERM[K + PERM[L]]]] & 0x1f; + int gi1 = PERM[I + i1 + PERM[J + j1 + PERM[K + k1 + PERM[L + l1]]]] & 0x1f; + int gi2 = PERM[I + i2 + PERM[J + j2 + PERM[K + k2 + PERM[L + l2]]]] & 0x1f; + int gi3 = PERM[I + i3 + PERM[J + j3 + PERM[K + k3 + PERM[L + l3]]]] & 0x1f; + int gi4 = PERM[I + 1 + PERM[J + 1 + PERM[K + 1 + PERM[L + 1]]]] & 0x1f; + float t0, t1, t2, t3, t4; + + t0 = 0.6f - x0*x0 - y0*y0 - z0*z0 - w0*w0; + if (t0 >= 0.0f) { + t0 *= t0; + noise[0] = t0 * t0 * dot4(GRAD4[gi0], x0, y0, z0, w0); + } + t1 = 0.6f - x1*x1 - y1*y1 - z1*z1 - w1*w1; + if (t1 >= 0.0f) { + t1 *= t1; + noise[1] = t1 * t1 * dot4(GRAD4[gi1], x1, y1, z1, w1); + } + t2 = 0.6f - x2*x2 - y2*y2 - z2*z2 - w2*w2; + if (t2 >= 0.0f) { + t2 *= t2; + noise[2] = t2 * t2 * dot4(GRAD4[gi2], x2, y2, z2, w2); + } + t3 = 0.6f - x3*x3 - y3*y3 - z3*z3 - w3*w3; + if (t3 >= 0.0f) { + t3 *= t3; + noise[3] = t3 * t3 * dot4(GRAD4[gi3], x3, y3, z3, w3); + } + t4 = 0.6f - x4*x4 - y4*y4 - z4*z4 - w4*w4; + if (t4 >= 0.0f) { + t4 *= t4; + noise[4] = t4 * t4 * dot4(GRAD4[gi4], x4, y4, z4, w4); + } + + return 27.0 * (noise[0] + noise[1] + noise[2] + noise[3] + noise[4]); +} + +inline float +fbm_noise4(float x, float y, float z, float w, int octaves, float persistence, float lacunarity) { + float freq = 1.0f; + float amp = 1.0f; + float max = 1.0f; + float total = noise4(x, y, z, w); + int i; + + for (i = 1; i < octaves; ++i) { + freq *= lacunarity; + amp *= persistence; + max += amp; + total += noise4(x * freq, y * freq, z * freq, w * freq) * amp; + } + return total / max; +} + +static float +dispatch_noise2(float x, float y, int octaves, float persistence, + float lacunarity, float repeatx, float repeaty, float z) +{ + if (repeatx == FLT_MAX && repeaty == FLT_MAX) { + // Flat noise, no tiling + float freq = 1.0f; + float amp = 1.0f; + float max = 1.0f; + float total = noise2(x + z, y + z); + int i; + + for (i = 1; i < octaves; i++) { + freq *= lacunarity; + amp *= persistence; + max += amp; + total += noise2(x * freq + z, y * freq + z) * amp; + } + return total / max; + } else { // Tiled noise + float w = z; + if (repeaty != FLT_MAX) { + float yf = y * 2.0 / repeaty; + float yr = repeaty * M_1_PI * 0.5; + float vy = fast_sin(yf); + float vyz = fast_cos(yf); + y = vy * yr; + w += vyz * yr; + if (repeatx == FLT_MAX) { + return fbm_noise3(x, y, w, octaves, persistence, lacunarity); + } + } + if (repeatx != FLT_MAX) { + float xf = x * 2.0 / repeatx; + float xr = repeatx * M_1_PI * 0.5; + float vx = fast_sin(xf); + float vxz = fast_cos(xf); + x = vx * xr; + z += vxz * xr; + if (repeaty == FLT_MAX) { + return fbm_noise3(x, y, z, octaves, persistence, lacunarity); + } + } + return fbm_noise4(x, y, z, w, octaves, persistence, lacunarity); + } +} + +static float +dispatch_noise3(float x, float y, float z, int octaves, float persistence, + float lacunarity) +{ + if (octaves == 1) { + // Single octave, return simple noise + return noise3(x, y, z); + } else { + // octaves > 1, since we already checked for <= 0 + return fbm_noise3(x, y, z, octaves, persistence, lacunarity); + } +} + +static float +dispatch_noise4(float x, float y, float z, float w, int octaves, + float persistence, float lacunarity) +{ + if (octaves == 1) { + // Single octave, return simple noise + return noise4(x, y, z, w); + } else { + // octaves > 1, since we already checked for <= 0 + return fbm_noise4(x, y, z, w, octaves, persistence, lacunarity); + } +} + +static float +dispatch_noise2_args(NoiseArgs *args, float **coord) +{ + return dispatch_noise2(*coord[0], + *coord[1], + args->octaves, args->persistence, args->lacunarity, + args->repeatx, args->repeaty, args->z); +} + +static float +dispatch_noise3_args(NoiseArgs *args, float **coord) +{ + return dispatch_noise3(*coord[0], + *coord[1], + *coord[2], + args->octaves, args->persistence, args->lacunarity); +} + +static float +dispatch_noise4_args(NoiseArgs *args, float **coord) +{ + return dispatch_noise4(*coord[0], + *coord[1], + *coord[2], + *coord[3], + args->octaves, args->persistence, args->lacunarity); +} + +static PyObject * +noise2_scalar(NoiseArgs *args) +{ + PyObject* fx = NULL; + PyObject* fy = NULL; + PyObject* result = NULL; + float fresult; + + fx = PyNumber_Float(args->dim_vals[0]); + if (fx == NULL) + goto fail_x; + + fy = PyNumber_Float(args->dim_vals[1]); + if (fy == NULL) + goto fail_y; + + fresult = dispatch_noise2((float) PyFloat_AsDouble(fx), + (float) PyFloat_AsDouble(fy), + args->octaves, args->persistence, + args->lacunarity, + args->repeatx, args->repeaty, args->z); + result = (PyObject*) PyFloat_FromDouble(fresult); + + Py_DECREF(fy); +fail_y: + Py_DECREF(fx); +fail_x: + return result; +} + +static PyObject * +noise3_scalar(NoiseArgs *args) +{ + PyObject* fx = NULL; + PyObject* fy = NULL; + PyObject* fz = NULL; + PyObject* result = NULL; + float fresult; + + fx = PyNumber_Float(args->dim_vals[0]); + if (fx == NULL) + goto fail_x; + + fy = PyNumber_Float(args->dim_vals[1]); + if (fy == NULL) + goto fail_y; + + fz = PyNumber_Float(args->dim_vals[2]); + if (fz == NULL) + goto fail_z; + + fresult = dispatch_noise3((float) PyFloat_AsDouble(fx), + (float) PyFloat_AsDouble(fy), + (float) PyFloat_AsDouble(fz), + args->octaves, args->persistence, + args->lacunarity); + result = (PyObject*) PyFloat_FromDouble(fresult); + + Py_DECREF(fz); +fail_z: + Py_DECREF(fy); +fail_y: + Py_DECREF(fx); +fail_x: + return result; +} + +static PyObject * +noise4_scalar(NoiseArgs *args) +{ + PyObject* fx = NULL; + PyObject* fy = NULL; + PyObject* fz = NULL; + PyObject* fw = NULL; + PyObject* result = NULL; + float fresult; + + fx = PyNumber_Float(args->dim_vals[0]); + if (fx == NULL) + goto fail_x; + + fy = PyNumber_Float(args->dim_vals[1]); + if (fy == NULL) + goto fail_y; + + fz = PyNumber_Float(args->dim_vals[2]); + if (fz == NULL) + goto fail_z; + + fw = PyNumber_Float(args->dim_vals[3]); + if (fw == NULL) + goto fail_w; + + fresult = dispatch_noise4((float) PyFloat_AsDouble(fx), + (float) PyFloat_AsDouble(fy), + (float) PyFloat_AsDouble(fz), + (float) PyFloat_AsDouble(fw), + args->octaves, args->persistence, + args->lacunarity); + result = (PyObject*) PyFloat_FromDouble(fresult); + + Py_DECREF(fw); +fail_w: + Py_DECREF(fz); +fail_z: + Py_DECREF(fy); +fail_y: + Py_DECREF(fx); +fail_x: + return result; +} + +static inline PyObject * +py_noise_common(NoiseArgs* args) +{ + static char *var_names[4] = {"xs", "ys", "zs", "ws"}; + + int i; + int all_scalars = 1; + NpyIter *iter; + NpyIter_IterNextFunc *iternext; + PyArrayObject *ret = NULL; + PyArray_Descr *float_type = NULL; + npy_intp *sizeptr, *strides; + char **dataptrarray; + + if (args->octaves <= 0) { + PyErr_SetString(PyExc_ValueError, "Expected octaves value > 0"); + return NULL; + } + + for (i = 0; i < args->ndims; i++) { + all_scalars &= PyArray_IsPythonScalar(args->dim_vals[i]); + } + if (all_scalars) + return args->scalar_func(args); + + float_type = PyArray_DescrFromType(NPY_FLOAT); + + for (i = 0; i < args->ndims; i++) { + args->op[i] = (PyArrayObject*) PyArray_FROM_O(args->dim_vals[i]); + if (args->op[i] == NULL) { + PyErr_Format(PyExc_ValueError, + "Could not convert argument `%s` to an array of floats", + var_names[i]); + goto fail; + } + args->op_flags[i] = NPY_ITER_READONLY; + args->op_dtypes[i] = float_type; + } + args->op[args->nops - 1] = NULL; + args->op_flags[args->nops - 1] = NPY_ITER_WRITEONLY | NPY_ITER_ALLOCATE; + args->op_dtypes[args->nops - 1] = float_type; + + iter = NpyIter_MultiNew(args->nops, args->op, + NPY_ITER_EXTERNAL_LOOP | NPY_ITER_BUFFERED, + NPY_KEEPORDER, NPY_SAME_KIND_CASTING, + args->op_flags, args->op_dtypes); + + if (iter == NULL) { + goto fail; + } + + iternext = NpyIter_GetIterNext(iter, NULL); + strides = NpyIter_GetInnerStrideArray(iter); + sizeptr = NpyIter_GetInnerLoopSizePtr(iter); + dataptrarray = NpyIter_GetDataPtrArray(iter); + + do { + npy_intp size = *sizeptr; + float *result; + int iop; + + while (size--) { + result = (float*) dataptrarray[args->nops - 1]; + *result = args->dispatch_func(args, (float**) dataptrarray); + for (iop = 0; iop < args->nops; ++iop) { + dataptrarray[iop] += strides[iop]; + } + } + } while (iternext(iter)); + + ret = NpyIter_GetOperandArray(iter)[args->nops - 1]; + Py_INCREF(ret); + + if (NpyIter_Deallocate(iter) != NPY_SUCCEED) { + goto fail; + } + + Py_DECREF(float_type); + // Don't deallocate the output array + for (i = 0; i < args->ndims; i++) { + Py_DECREF(args->op[i]); + } + + return PyArray_Return(ret); + +fail: + Py_XDECREF(float_type); + // Do deallocate the output array + for (i = 0; i < args->nops; i++) { + Py_XDECREF(args->op[i]); + } + Py_XDECREF(ret); + return NULL; +} + +static PyObject * +py_noise2(PyObject *self, PyObject *args, PyObject *kwargs) +{ + PyObject* dim_vals[2] = {NULL, NULL}; + PyArrayObject *op[3] = {NULL, NULL, NULL}; + npy_uint32 op_flags[3]; + PyArray_Descr *op_dtypes[3]; + + NoiseArgs nargs = { + 2, + 3, + dim_vals, + op, + op_flags, + op_dtypes, + noise2_scalar, + dispatch_noise2_args, + 1, + 0.5f, + 2.0f, + FLT_MAX, + FLT_MAX, + 0.0f + }; + + static char *kwlist[] = {"x", "y", "octaves", "persistence", "lacunarity", + "repeatx", "repeaty", "base", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "OO|ifffff:snoise2", + kwlist, + &dim_vals[0], &dim_vals[1], + &nargs.octaves, &nargs.persistence, + &nargs.lacunarity, &nargs.repeatx, + &nargs.repeaty, &nargs.z)) + return NULL; + + return py_noise_common(&nargs); +} + +static PyObject * +py_noise3(PyObject *self, PyObject *args, PyObject *kwargs) +{ + PyObject* dim_vals[3] = {NULL, NULL, NULL}; + PyArrayObject *op[4] = {NULL, NULL, NULL, NULL}; + npy_uint32 op_flags[4]; + PyArray_Descr *op_dtypes[4]; + + NoiseArgs nargs = { + 3, + 4, + dim_vals, + op, + op_flags, + op_dtypes, + noise3_scalar, + dispatch_noise3_args, + 1, + 0.5f, + 2.0f + }; + + static char *kwlist[] = {"x", "y", "z", "octaves", "persistence", + "lacunarity", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "OOO|iff:snoise3", + kwlist, + &dim_vals[0], &dim_vals[1], &dim_vals[2], + &nargs.octaves, &nargs.persistence, + &nargs.lacunarity)) + return NULL; + + return py_noise_common(&nargs); +} + +static PyObject * +py_noise4(PyObject *self, PyObject *args, PyObject *kwargs) +{ + PyObject* dim_vals[4] = {NULL, NULL, NULL, NULL}; + PyArrayObject *op[5] = {NULL, NULL, NULL, NULL, NULL}; + npy_uint32 op_flags[5]; + PyArray_Descr *op_dtypes[5]; + + NoiseArgs nargs = { + 4, + 5, + dim_vals, + op, + op_flags, + op_dtypes, + noise4_scalar, + dispatch_noise4_args, + 1, + 0.5f, + 2.0f + }; + + static char *kwlist[] = {"x", "y", "z", "w", "octaves", "persistence", + "lacunarity", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "OOOO|iff:snoise4", + kwlist, + &dim_vals[0], &dim_vals[1], &dim_vals[2], + &dim_vals[3], + &nargs.octaves, &nargs.persistence, + &nargs.lacunarity)) + return NULL; + + return py_noise_common(&nargs); +} + +#define SIMPLEX_COMMON_DOCS \ + "octaves -- specifies the number of passes, defaults to 1 (simple noise).\n\n" \ + "persistence -- specifies the amplitude of each successive octave relative\n" \ + "to the one below it. Defaults to 0.5 (each higher octave's amplitude\n" \ + "is halved). Note the amplitude of the first pass is always 1.0.\n\n" \ + "lacunarity -- specifies the frequency of each successive octave relative\n" \ + "to the one below it, similar to persistence. Defaults to 2.0." + +static PyMethodDef simplex_functions[] = { + {"noise2", (PyCFunction)py_noise2, METH_VARARGS | METH_KEYWORDS, + "noise2(x, y, octaves=1, persistence=0.5, lacunarity=2.0, repeatx=None, repeaty=None, base=0.0) " + "return simplex noise value for specified 2D coordinate.\n\n" + "repeatx, repeaty -- specifies the interval along each axis when \n" + "the noise values repeat. This can be used as the tile size for creating \n" + "tileable textures\n\n" + SIMPLEX_COMMON_DOCS "\n\n" + "base -- specifies a fixed offset for the noise coordinates. Useful for\n" + "generating different noise textures with the same repeat interval"}, + {"noise3", (PyCFunction)py_noise3, METH_VARARGS | METH_KEYWORDS, + "noise3(x, y, z, octaves=1, persistence=0.5, lacunarity=2.0) return simplex noise value for " + "specified 3D coordinate\n\n" + SIMPLEX_COMMON_DOCS + }, + {"noise4", (PyCFunction)py_noise4, METH_VARARGS | METH_KEYWORDS, + "noise4(x, y, z, w, octaves=1, persistence=0.5, lacunarity=2.0) return simplex noise value for " + "specified 4D coordinate\n\n" + SIMPLEX_COMMON_DOCS + }, + {NULL} +}; + +#undef SIMPLEX_COMMON_DOCS + +PyDoc_STRVAR(module_doc, "Native-code simplex noise functions"); + +#if PY_MAJOR_VERSION >= 3 + +static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "_simplex", + module_doc, + -1, /* m_size */ + simplex_functions, /* m_methods */ + NULL, /* m_reload (unused) */ + NULL, /* m_traverse */ + NULL, /* m_clear */ + NULL /* m_free */ +}; + +PyMODINIT_FUNC +PyInit__simplex(void) +{ + PyObject* ret = PyModule_Create(&moduledef); + if (! ret) { + return NULL; + } + + import_array(); + return ret; +} + +#else + +PyMODINIT_FUNC +init_simplex(void) +{ + Py_InitModule3("_simplex", simplex_functions, module_doc); + import_array(); +} + +#endif diff --git a/vec_noise-1.1.4/perlin.py b/vec_noise-1.1.4/perlin.py new file mode 100644 index 0000000..74b425c --- /dev/null +++ b/vec_noise-1.1.4/perlin.py @@ -0,0 +1,354 @@ +# Copyright (c) 2008, Casey Duncan (casey dot duncan at gmail dot com) +# see LICENSE.txt for details + +"""Perlin noise -- pure python implementation""" + +__version__ = '$Id: perlin.py 521 2008-12-15 03:03:52Z casey.duncan $' + +from math import floor, fmod, sqrt +from random import randint + +# 3D Gradient vectors +_GRAD3 = ((1,1,0),(-1,1,0),(1,-1,0),(-1,-1,0), + (1,0,1),(-1,0,1),(1,0,-1),(-1,0,-1), + (0,1,1),(0,-1,1),(0,1,-1),(0,-1,-1), + (1,1,0),(0,-1,1),(-1,1,0),(0,-1,-1), +) + +# 4D Gradient vectors +_GRAD4 = ((0,1,1,1), (0,1,1,-1), (0,1,-1,1), (0,1,-1,-1), + (0,-1,1,1), (0,-1,1,-1), (0,-1,-1,1), (0,-1,-1,-1), + (1,0,1,1), (1,0,1,-1), (1,0,-1,1), (1,0,-1,-1), + (-1,0,1,1), (-1,0,1,-1), (-1,0,-1,1), (-1,0,-1,-1), + (1,1,0,1), (1,1,0,-1), (1,-1,0,1), (1,-1,0,-1), + (-1,1,0,1), (-1,1,0,-1), (-1,-1,0,1), (-1,-1,0,-1), + (1,1,1,0), (1,1,-1,0), (1,-1,1,0), (1,-1,-1,0), + (-1,1,1,0), (-1,1,-1,0), (-1,-1,1,0), (-1,-1,-1,0)) + +# A lookup table to traverse the simplex around a given point in 4D. +# Details can be found where this table is used, in the 4D noise method. +_SIMPLEX = ( + (0,1,2,3),(0,1,3,2),(0,0,0,0),(0,2,3,1),(0,0,0,0),(0,0,0,0),(0,0,0,0),(1,2,3,0), + (0,2,1,3),(0,0,0,0),(0,3,1,2),(0,3,2,1),(0,0,0,0),(0,0,0,0),(0,0,0,0),(1,3,2,0), + (0,0,0,0),(0,0,0,0),(0,0,0,0),(0,0,0,0),(0,0,0,0),(0,0,0,0),(0,0,0,0),(0,0,0,0), + (1,2,0,3),(0,0,0,0),(1,3,0,2),(0,0,0,0),(0,0,0,0),(0,0,0,0),(2,3,0,1),(2,3,1,0), + (1,0,2,3),(1,0,3,2),(0,0,0,0),(0,0,0,0),(0,0,0,0),(2,0,3,1),(0,0,0,0),(2,1,3,0), + (0,0,0,0),(0,0,0,0),(0,0,0,0),(0,0,0,0),(0,0,0,0),(0,0,0,0),(0,0,0,0),(0,0,0,0), + (2,0,1,3),(0,0,0,0),(0,0,0,0),(0,0,0,0),(3,0,1,2),(3,0,2,1),(0,0,0,0),(3,1,2,0), + (2,1,0,3),(0,0,0,0),(0,0,0,0),(0,0,0,0),(3,1,0,2),(0,0,0,0),(3,2,0,1),(3,2,1,0)) + +# Simplex skew constants +_F2 = 0.5 * (sqrt(3.0) - 1.0) +_G2 = (3.0 - sqrt(3.0)) / 6.0 +_F3 = 1.0 / 3.0 +_G3 = 1.0 / 6.0 + + +class BaseNoise: + """Noise abstract base class""" + + permutation = (151,160,137,91,90,15, + 131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23, + 190,6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33, + 88,237,149,56,87,174,20,125,136,171,168,68,175,74,165,71,134,139,48,27,166, + 77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244, + 102,143,54,65,25,63,161,1,216,80,73,209,76,132,187,208,89,18,169,200,196, + 135,130,116,188,159,86,164,100,109,198,173,186,3,64,52,217,226,250,124,123, + 5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42, + 223,183,170,213,119,248,152,2,44,154,163,70,221,153,101,155,167,43,172,9, + 129,22,39,253,9,98,108,110,79,113,224,232,178,185,112,104,218,246,97,228, + 251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107, + 49,192,214,31,181,199,106,157,184,84,204,176,115,121,50,45,127,4,150,254, + 138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180) + + period = len(permutation) + + # Double permutation array so we don't need to wrap + permutation = permutation * 2 + + randint_function = randint + + def __init__(self, period=None, permutation_table=None, randint_function=None): + """Initialize the noise generator. With no arguments, the default + period and permutation table are used (256). The default permutation + table generates the exact same noise pattern each time. + + An integer period can be specified, to generate a random permutation + table with period elements. The period determines the (integer) + interval that the noise repeats, which is useful for creating tiled + textures. period should be a power-of-two, though this is not + enforced. Note that the speed of the noise algorithm is indpendent of + the period size, though larger periods mean a larger table, which + consume more memory. + + A permutation table consisting of an iterable sequence of whole + numbers can be specified directly. This should have a power-of-two + length. Typical permutation tables are a sequnce of unique integers in + the range [0,period) in random order, though other arrangements could + prove useful, they will not be "pure" simplex noise. The largest + element in the sequence must be no larger than period-1. + + period and permutation_table may not be specified together. + + A substitute for the method random.randint(a, b) can be chosen. The + method must take two integer parameters a and b and return an integer N + such that a <= N <= b. + """ + if randint_function is not None: # do this before calling randomize() + if not hasattr(randint_function, '__call__'): + raise TypeError( + 'randint_function has to be a function') + self.randint_function = randint_function + if period is None: + period = self.period # enforce actually calling randomize() + if period is not None and permutation_table is not None: + raise ValueError( + 'Can specify either period or permutation_table, not both') + if period is not None: + self.randomize(period) + elif permutation_table is not None: + self.permutation = tuple(permutation_table) * 2 + self.period = len(permutation_table) + + def randomize(self, period=None): + """Randomize the permutation table used by the noise functions. This + makes them generate a different noise pattern for the same inputs. + """ + if period is not None: + self.period = period + perm = list(range(self.period)) + perm_right = self.period - 1 + for i in list(perm): + j = self.randint_function(0, perm_right) + perm[i], perm[j] = perm[j], perm[i] + self.permutation = tuple(perm) * 2 + + +class SimplexNoise(BaseNoise): + """Perlin simplex noise generator + + Adapted from Stefan Gustavson's Java implementation described here: + + http://staffwww.itn.liu.se/~stegu/simplexnoise/simplexnoise.pdf + + To summarize: + + "In 2001, Ken Perlin presented 'simplex noise', a replacement for his classic + noise algorithm. Classic 'Perlin noise' won him an academy award and has + become an ubiquitous procedural primitive for computer graphics over the + years, but in hindsight it has quite a few limitations. Ken Perlin himself + designed simplex noise specifically to overcome those limitations, and he + spent a lot of good thinking on it. Therefore, it is a better idea than his + original algorithm. A few of the more prominent advantages are: + + * Simplex noise has a lower computational complexity and requires fewer + multiplications. + * Simplex noise scales to higher dimensions (4D, 5D and up) with much less + computational cost, the complexity is O(N) for N dimensions instead of + the O(2^N) of classic Noise. + * Simplex noise has no noticeable directional artifacts. Simplex noise has + a well-defined and continuous gradient everywhere that can be computed + quite cheaply. + * Simplex noise is easy to implement in hardware." + """ + + def noise2(self, x, y): + """2D Perlin simplex noise. + + Return a floating point value from -1 to 1 for the given x, y coordinate. + The same value is always returned for a given x, y pair unless the + permutation table changes (see randomize above). + """ + # Skew input space to determine which simplex (triangle) we are in + s = (x + y) * _F2 + i = floor(x + s) + j = floor(y + s) + t = (i + j) * _G2 + x0 = x - (i - t) # "Unskewed" distances from cell origin + y0 = y - (j - t) + + if x0 > y0: + i1 = 1; j1 = 0 # Lower triangle, XY order: (0,0)->(1,0)->(1,1) + else: + i1 = 0; j1 = 1 # Upper triangle, YX order: (0,0)->(0,1)->(1,1) + + x1 = x0 - i1 + _G2 # Offsets for middle corner in (x,y) unskewed coords + y1 = y0 - j1 + _G2 + x2 = x0 + _G2 * 2.0 - 1.0 # Offsets for last corner in (x,y) unskewed coords + y2 = y0 + _G2 * 2.0 - 1.0 + + # Determine hashed gradient indices of the three simplex corners + perm = self.permutation + ii = int(i) % self.period + jj = int(j) % self.period + gi0 = perm[ii + perm[jj]] % 12 + gi1 = perm[ii + i1 + perm[jj + j1]] % 12 + gi2 = perm[ii + 1 + perm[jj + 1]] % 12 + + # Calculate the contribution from the three corners + tt = 0.5 - x0**2 - y0**2 + if tt > 0: + g = _GRAD3[gi0] + noise = tt**4 * (g[0] * x0 + g[1] * y0) + else: + noise = 0.0 + + tt = 0.5 - x1**2 - y1**2 + if tt > 0: + g = _GRAD3[gi1] + noise += tt**4 * (g[0] * x1 + g[1] * y1) + + tt = 0.5 - x2**2 - y2**2 + if tt > 0: + g = _GRAD3[gi2] + noise += tt**4 * (g[0] * x2 + g[1] * y2) + + return noise * 70.0 # scale noise to [-1, 1] + + def noise3(self, x, y, z): + """3D Perlin simplex noise. + + Return a floating point value from -1 to 1 for the given x, y, z coordinate. + The same value is always returned for a given x, y, z pair unless the + permutation table changes (see randomize above). + """ + # Skew the input space to determine which simplex cell we're in + s = (x + y + z) * _F3 + i = floor(x + s) + j = floor(y + s) + k = floor(z + s) + t = (i + j + k) * _G3 + x0 = x - (i - t) # "Unskewed" distances from cell origin + y0 = y - (j - t) + z0 = z - (k - t) + + # For the 3D case, the simplex shape is a slightly irregular tetrahedron. + # Determine which simplex we are in. + if x0 >= y0: + if y0 >= z0: + i1 = 1; j1 = 0; k1 = 0 + i2 = 1; j2 = 1; k2 = 0 + elif x0 >= z0: + i1 = 1; j1 = 0; k1 = 0 + i2 = 1; j2 = 0; k2 = 1 + else: + i1 = 0; j1 = 0; k1 = 1 + i2 = 1; j2 = 0; k2 = 1 + else: # x0 < y0 + if y0 < z0: + i1 = 0; j1 = 0; k1 = 1 + i2 = 0; j2 = 1; k2 = 1 + elif x0 < z0: + i1 = 0; j1 = 1; k1 = 0 + i2 = 0; j2 = 1; k2 = 1 + else: + i1 = 0; j1 = 1; k1 = 0 + i2 = 1; j2 = 1; k2 = 0 + + # Offsets for remaining corners + x1 = x0 - i1 + _G3 + y1 = y0 - j1 + _G3 + z1 = z0 - k1 + _G3 + x2 = x0 - i2 + 2.0 * _G3 + y2 = y0 - j2 + 2.0 * _G3 + z2 = z0 - k2 + 2.0 * _G3 + x3 = x0 - 1.0 + 3.0 * _G3 + y3 = y0 - 1.0 + 3.0 * _G3 + z3 = z0 - 1.0 + 3.0 * _G3 + + # Calculate the hashed gradient indices of the four simplex corners + perm = self.permutation + ii = int(i) % self.period + jj = int(j) % self.period + kk = int(k) % self.period + gi0 = perm[ii + perm[jj + perm[kk]]] % 12 + gi1 = perm[ii + i1 + perm[jj + j1 + perm[kk + k1]]] % 12 + gi2 = perm[ii + i2 + perm[jj + j2 + perm[kk + k2]]] % 12 + gi3 = perm[ii + 1 + perm[jj + 1 + perm[kk + 1]]] % 12 + + # Calculate the contribution from the four corners + noise = 0.0 + tt = 0.6 - x0**2 - y0**2 - z0**2 + if tt > 0: + g = _GRAD3[gi0] + noise = tt**4 * (g[0] * x0 + g[1] * y0 + g[2] * z0) + else: + noise = 0.0 + + tt = 0.6 - x1**2 - y1**2 - z1**2 + if tt > 0: + g = _GRAD3[gi1] + noise += tt**4 * (g[0] * x1 + g[1] * y1 + g[2] * z1) + + tt = 0.6 - x2**2 - y2**2 - z2**2 + if tt > 0: + g = _GRAD3[gi2] + noise += tt**4 * (g[0] * x2 + g[1] * y2 + g[2] * z2) + + tt = 0.6 - x3**2 - y3**2 - z3**2 + if tt > 0: + g = _GRAD3[gi3] + noise += tt**4 * (g[0] * x3 + g[1] * y3 + g[2] * z3) + + return noise * 32.0 + + +def lerp(t, a, b): + return a + t * (b - a) + +def grad3(hash, x, y, z): + g = _GRAD3[hash % 16] + return x*g[0] + y*g[1] + z*g[2] + + +class TileableNoise(BaseNoise): + """Tileable implemention of Perlin "improved" noise. This + is based on the reference implementation published here: + + http://mrl.nyu.edu/~perlin/noise/ + """ + + def noise3(self, x, y, z, repeat, base=0.0): + """Tileable 3D noise. + + repeat specifies the integer interval in each dimension + when the noise pattern repeats. + + base allows a different texture to be generated for + the same repeat interval. + """ + i = int(fmod(floor(x), repeat)) + j = int(fmod(floor(y), repeat)) + k = int(fmod(floor(z), repeat)) + ii = (i + 1) % repeat + jj = (j + 1) % repeat + kk = (k + 1) % repeat + if base: + i += base; j += base; k += base + ii += base; jj += base; kk += base + + x -= floor(x); y -= floor(y); z -= floor(z) + fx = x**3 * (x * (x * 6 - 15) + 10) + fy = y**3 * (y * (y * 6 - 15) + 10) + fz = z**3 * (z * (z * 6 - 15) + 10) + + perm = self.permutation + A = perm[i] + AA = perm[A + j] + AB = perm[A + jj] + B = perm[ii] + BA = perm[B + j] + BB = perm[B + jj] + + return lerp(fz, lerp(fy, lerp(fx, grad3(perm[AA + k], x, y, z), + grad3(perm[BA + k], x - 1, y, z)), + lerp(fx, grad3(perm[AB + k], x, y - 1, z), + grad3(perm[BB + k], x - 1, y - 1, z))), + lerp(fy, lerp(fx, grad3(perm[AA + kk], x, y, z - 1), + grad3(perm[BA + kk], x - 1, y, z - 1)), + lerp(fx, grad3(perm[AB + kk], x, y - 1, z - 1), + grad3(perm[BB + kk], x - 1, y - 1, z - 1)))) + + + + diff --git a/vec_noise-1.1.4/setup.cfg b/vec_noise-1.1.4/setup.cfg new file mode 100644 index 0000000..2ec3fc1 --- /dev/null +++ b/vec_noise-1.1.4/setup.cfg @@ -0,0 +1,7 @@ +[sdist] +formats = gztar,zip + +[egg_info] +tag_build = +tag_date = 0 + diff --git a/vec_noise-1.1.4/setup.py b/vec_noise-1.1.4/setup.py new file mode 100644 index 0000000..3dff3c5 --- /dev/null +++ b/vec_noise-1.1.4/setup.py @@ -0,0 +1,75 @@ +import sys + +from setuptools import setup, Extension +from setuptools.command.build_ext import build_ext as _build_ext + +class build_ext(_build_ext): + def finalize_options(self): + _build_ext.finalize_options(self) + # Prevent numpy from thinking it is still in its setup process: + try: + __builtins__.__NUMPY_SETUP__ = False + except AttributeError: + print("Cannot set '__builtins__.__NUMPY_SETUP__ = False' This is not needed if numpy is already installed.") + import numpy + self.include_dirs.append(numpy.get_include()) + +if sys.platform != 'win32': + compile_args = ['-funroll-loops'] +else: + # XXX insert win32 flag to unroll loops here + compile_args = [] + +setup( + name='vec_noise', + version='1.1.4', + description='Vectorized Perlin noise for Python', + long_description='''\ +This is a fork of Casey Duncan's noise library that vectorizes all of the noise +functions using NumPy. It is much faster than the original for computing noise +values at many coordinates. + +Perlin noise is ubiquitous in modern CGI. Used for procedural texturing, +animation, and enhancing realism, Perlin noise has been called the "salt" of +procedural content. Perlin noise is a type of gradient noise, smoothly +interpolating across a pseudo-random matrix of values. + +The vec_noise library includes native-code implementations of Perlin "improved" +noise and Perlin simplex noise. It also includes a fast implementation of +Perlin noise in GLSL, for use in OpenGL shaders. The shader code and many of +the included examples require Pyglet (http://www.pyglet.org), the native-code +noise functions themselves do not, however. + +The Perlin improved noise functions can also generate fBm (fractal Brownian +motion) noise by combining multiple octaves of Perlin noise. Shader functions +for convenient generation of turbulent noise are also included. +''', + author='Zev Benjamin', + author_email='zev@strangersgate.com', + url='https://github.com/zbenjamin/vec_noise', + classifiers = [ + 'Development Status :: 4 - Beta', + 'Topic :: Multimedia :: Graphics', + 'License :: OSI Approved :: MIT License', + 'Operating System :: MacOS :: MacOS X', + 'Operating System :: Microsoft :: Windows', + 'Operating System :: POSIX', + 'Programming Language :: C', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 3', + ], + + package_dir={'vec_noise': ''}, + packages=['vec_noise'], + setup_requires=['numpy'], + install_requires=['numpy'], + cmdclass={'build_ext': build_ext}, + ext_modules=[ + Extension('vec_noise._simplex', ['_simplex.c'], + extra_compile_args=compile_args, + ), + Extension('vec_noise._perlin', ['_perlin.c'], + extra_compile_args=compile_args, + ) + ], +) diff --git a/vec_noise-1.1.4/shader.py b/vec_noise-1.1.4/shader.py new file mode 100644 index 0000000..18d8e0f --- /dev/null +++ b/vec_noise-1.1.4/shader.py @@ -0,0 +1,257 @@ +# Shader module from pyglet/experimental + +from ctypes import * + +from pyglet.gl import * + +class GLSLException(Exception): pass + + + +def glsl_log(handle): + if handle == 0: + return '' + log_len = c_int(0) + + glGetObjectParameterivARB(handle, GL_OBJECT_INFO_LOG_LENGTH_ARB, + byref(log_len)) + if log_len.value == 0: + return '' + + log = create_string_buffer(log_len.value) # does log_len include the NUL? + + chars_written = c_int(0) + glGetInfoLogARB(handle, log_len.value, byref(chars_written), log) + + return log.value + + + +class Shader(object): + s_tag = 0 + + def __init__(self, name, prog): + self.name = name + self.prog = prog + self.shader = 0 + self.compiling = False + self.tag = -1 + self.dependencies = [] + + def __del__(self): + self.destroy() + + def _source(self): + if self.tag == Shader.s_tag: return [] + self.tag = Shader.s_tag + + r = [] + for d in self.dependencies: + r.extend(d._source()) + r.append(self.prog) + + return r + + def _compile(self): + if self.shader: return + if self.compiling : return + self.compiling = True + + self.shader = glCreateShaderObjectARB(self.shaderType()) + if self.shader == 0: + raise GLSLException('faled to create shader object') + + prog = c_char_p(self.prog) + length = c_int(-1) + glShaderSourceARB(self.shader, + 1, + cast(byref(prog), POINTER(POINTER(c_char))), + byref(length)) + glCompileShaderARB(self.shader) + + self.compiling = False + + compile_status = c_int(0) + glGetObjectParameterivARB(self.shader, GL_OBJECT_COMPILE_STATUS_ARB, byref(compile_status)) + + if not compile_status.value: + err = glsl_log(self.shader) + glDeleteObjectARB(self.shader) + self.shader = 0 + raise GLSLException('failed to compile shader', err) + + def _attachTo(self, program): + if self.tag == Shader.s_tag: return + + self.tag = Shader.s_tag + + for d in self.dependencies: + d._attachTo(program) + + if self.isCompiled(): + glAttachObjectARB(program, self.shader) + + def addDependency(self, shader): + self.dependencies.append(shader) + return self + + def destroy(self): + if self.shader != 0: glDeleteObjectARB(self.shader) + + + def shaderType(self): + raise NotImplementedError() + + def isCompiled(self): + return self.shader != 0 + + def attachTo(self, program): + Shader.s_tag = Shader.s_tag + 1 + self._attachTo(program) + + # ATI/apple's glsl compiler is broken. + def attachFlat(self, program): + if self.isCompiled(): + glAttachObjectARB(program, self.shader) + + def compileFlat(self): + if self.isCompiled(): return + + self.shader = glCreateShaderObjectARB(self.shaderType()) + if self.shader == 0: + raise GLSLException('faled to create shader object') + + all_source = ['\n'.join(self._source()).encode('utf-8')] + prog = (c_char_p * len(all_source))(*all_source) + length = (c_int * len(all_source))(-1) + glShaderSourceARB(self.shader, + len(all_source), + cast(prog, POINTER(POINTER(c_char))), + length) + glCompileShaderARB(self.shader) + + compile_status = c_int(0) + glGetObjectParameterivARB(self.shader, GL_OBJECT_COMPILE_STATUS_ARB, byref(compile_status)) + + if not compile_status.value: + err = glsl_log(self.shader) + glDeleteObjectARB(self.shader) + self.shader = 0 + raise GLSLException('failed to compile shader', err) + + + def compile(self): + if self.isCompiled(): return + + for d in self.dependencies: + d.compile() + + self._compile() + + + +class VertexShader(Shader): + def shaderType(self): return GL_VERTEX_SHADER_ARB + + + +class FragmentShader(Shader): + def shaderType(self): return GL_FRAGMENT_SHADER_ARB + + + +class ShaderProgram(object): + def __init__(self, vertex_shader=None, fragment_shader=None): + self.vertex_shader = vertex_shader + self.fragment_shader = fragment_shader + self.program = 0 + + def __del__(self): + self.destroy() + + def destroy(self): + if self.program != 0: glDeleteObjectARB(self.program) + + def setShader(self, shader): + if isinstance(shader, FragmentShader): + self.fragment_shader = shader + if isinstance(shader, VertexShader): + self.vertex_shader = shader + if self.program != 0: glDeleteObjectARB(self.program) + + def link(self): + if self.vertex_shader is not None: self.vertex_shader.compileFlat() + if self.fragment_shader is not None: self.fragment_shader.compileFlat() + + self.program = glCreateProgramObjectARB() + if self.program == 0: + raise GLSLException('failed to create program object') + + if self.vertex_shader is not None: self.vertex_shader.attachFlat(self.program) + if self.fragment_shader is not None: self.fragment_shader.attachFlat(self.program) + + glLinkProgramARB(self.program) + + link_status = c_int(0) + glGetObjectParameterivARB(self.program, GL_OBJECT_LINK_STATUS_ARB, byref(link_status)) + if link_status.value == 0: + err = glsl_log(self.program) + glDeleteObjectARB(self.program) + self.program = 0 + raise GLSLException('failed to link shader', err) + + self.__class__._uloc_ = {} + self.__class__._vloc_ = {} + + return self.program + + def prog(self): + if self.program: return self.program + return self.link() + + def install(self): + p = self.prog() + if p != 0: + glUseProgramObjectARB(p) + + def uninstall(self): + glUseProgramObjectARB(0) + + def uniformLoc(self, var): + try: + return self.__class__._uloc_[var] + except: + if self.program == 0: + self.link() + self.__class__._uloc_[var] = v = glGetUniformLocationARB( + self.program, var.encode('utf-8')) + return v + + def uset1F(self, var, x): + glUniform1fARB(self.uniformLoc(var), x) + + def uset2F(self, var, x, y): + glUniform2fARB(self.uniformLoc(var), x, y) + + def uset3F(self, var, x, y, z): + glUniform3fARB(self.uniformLoc(var), x, y, z) + + def uset4F(self, var, x, y, z, w): + glUniform4fARB(self.uniformLoc(var), x, y, z, w) + + def uset1I(self, var, x): + glUniform1iARB(self.uniformLoc(var), x) + + def uset3I(self, var, x, y, z): + glUniform1iARB(self.uniformLoc(var), x, y, z) + + def usetM4F(self, var, m): + pass + # glUniform1iARB(self.uniformLoc(var), x, y, z) + + def usetTex(self, var, u, v): + glUniform1iARB(self.uniformLoc(var), u) + glActiveTexture(GL_TEXTURE0 + u) + glBindTexture(v.gl_tgt, v.gl_id) + +__all__ = ['VertexShader', 'FragmentShader', 'ShaderProgram', 'GLSLException'] diff --git a/vec_noise-1.1.4/shader_noise.py b/vec_noise-1.1.4/shader_noise.py new file mode 100644 index 0000000..5ce742b --- /dev/null +++ b/vec_noise-1.1.4/shader_noise.py @@ -0,0 +1,194 @@ +"""shader_noise shader function and texture generator +as described in "GPU Gems" chapter 5: + +http://http.developer.nvidia.com/GPUGems/gpugems_ch05.html +""" + +__version__ = "$Id: shader_noise.py 37 2008-06-27 22:25:39Z casey.duncan $" + +from vec_noise import pnoise3 +import ctypes +from pyglet.gl import * + +class ShaderNoiseTexture: + """tiling 3D noise texture with two channels for use by the + shader noise functions. + """ + + def __init__(self, freq=8, width=32): + """Generate the 3D noise texture. + + freq -- frequency of generated noise over the width of the + texture. + + width -- Width of the texture in texels. The texture is cubic, + thus all sides are the same width. Must be a power of two. + Using a larger width can reduce artifacts caused by linear + interpolation of the noise texture, at the cost of video + memory, and possibly slower texture access. + """ + self.freq = freq + self.width = width + scale = float(freq) / width + width2 = width**2 + texel = (ctypes.c_ushort * (2 * width**3))() + for z in range(width): + for y in range(width): + for x in range(width): + texel[(x + (y * width) + (z * width2)) * 2] = int((pnoise3( + x * scale, y * scale, z * scale, + repeatx=freq, repeaty=freq, repeatz=freq) + 1.0) * 32767) + texel[(x + (y * width) + (z * width2)) * 2 + 1] = int((pnoise3( + x * scale, y * scale, z * scale, + repeatx=freq, repeaty=freq, repeatz=freq, base=freq + 1) + 1.0) * 32767) + self.data = texel + + def load(self): + """Load the noise texture data into the current texture unit""" + glTexImage3D(GL_TEXTURE_3D, 0, GL_LUMINANCE16_ALPHA16, + self.width, self.width, self.width, 0, GL_LUMINANCE_ALPHA, + GL_UNSIGNED_SHORT, ctypes.byref(self.data)) + + def enable(self): + """Convenience method to enable 3D texturing state so the texture may be used by the + ffpnoise shader function + """ + glEnable(GL_TEXTURE_3D) + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_REPEAT) + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_REPEAT) + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_REPEAT) + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) + + +shader_noise_glsl = ''' +/* + * GLSL Shader functions for fast fake Perlin 3D noise + * + * The required shader_noise_tex texture can be generated using the + * ShaderNoiseTexture class. It is a toroidal tiling 3D texture with each texel + * containing two 16-bit noise source channels. The shader permutes the source + * texture values by combining the channels such that the noise repeats at a + * much larger interval than the input texture. + */ + +uniform sampler3D shader_noise_tex; +const float twopi = 3.1415926 * 2.0; + +/* Simple perlin noise work-alike */ +float +pnoise(vec3 position) +{ + vec4 hi = 2.0 * texture3D(shader_noise_tex, position.xyz) - 1.0; + vec4 lo = 2.0 * texture3D(shader_noise_tex, position.xyz / 9.0) - 1.0; + return hi.r * cos(twopi * lo.r) + hi.a * sin(twopi * lo.r); +} + +/* Multi-octave fractal brownian motion perlin noise */ +float +fbmnoise(vec3 position, int octaves) +{ + float m = 1.0; + vec3 p = position; + vec4 hi = vec4(0.0); + /* XXX Loops may not work correctly on all video cards */ + for (int x = 0; x < octaves; x++) { + hi += (2.0 * texture3D(shader_noise_tex, p.xyz) - 1.0) * m; + p *= 2.0; + m *= 0.5; + } + vec4 lo = 2.0 * texture3D(shader_noise_tex, position.xyz / 9.0) - 1.0; + return hi.r * cos(twopi * lo.r) + hi.a * sin(twopi * lo.r); +} + +/* Multi-octave turbulent noise */ +float +fbmturbulence(vec3 position, int octaves) +{ + float m = 1.0; + vec3 p = position; + vec4 hi = vec4(0.0); + /* XXX Loops may not work correctly on all video cards */ + for (int x = 0; x < octaves; x++) { + hi += abs(2.0 * texture3D(shader_noise_tex, p.xyz) - 1.0) * m; + p *= 2.0; + m *= 0.5; + } + vec4 lo = texture3D(shader_noise_tex, position.xyz / 9.0); + return 2.0 * mix(hi.r, hi.a, cos(twopi * lo.r) * 0.5 + 0.5) - 1.0; +} + +''' + +if __name__ == '__main__': + # Demo using a simple noise-textured rotating sphere + import shader + win = pyglet.window.Window(width=640, height=640, resizable=True, visible=False) + vert_shader = shader.VertexShader('stupid', ''' + /* simple vertex shader that stores the vertex position in a varying + * for easy access by the frag shader + */ + varying vec3 position; + + void main(void) { + position = gl_Vertex.xyz * 5.0; + gl_Position = ftransform(); + } + ''') + frag_shader = shader.FragmentShader('noise_test', shader_noise_glsl + ''' + varying vec3 position; + + void main(void) { + float v; + float a = atan(position.y, position.x); + float arc = 3.14159 / 3.0; + if (a > -arc && a < arc) { + v = pnoise(position) * 0.5 + 0.5; + } else if (a > arc && a < arc * 4.0) { + v = fbmnoise(position, 4) * 0.5 + 0.5; + } else { + v = fbmturbulence(position, 4) * 0.5 + 0.5; + } + gl_FragColor = vec4(v, v, v, 1.0); + } + ''') + shader_prog = shader.ShaderProgram(vert_shader, frag_shader) + shader_prog.install() + tex = ShaderNoiseTexture() + tex.load() + tex.enable() + shader_prog.uset1I('shader_noise_tex', 0) + + quadratic = gluNewQuadric() + gluQuadricNormals(quadratic, GLU_SMOOTH) + gluQuadricTexture(quadratic, GL_TRUE) + glEnable(GL_CULL_FACE) + global spin + spin = 0 + + def on_resize(width, height): + glViewport(0, 0, width, height) + glMatrixMode(GL_PROJECTION) + glLoadIdentity() + gluPerspective(70, 1.0*width/height, 0.1, 1000.0) + glMatrixMode(GL_MODELVIEW) + glLoadIdentity() + win.on_resize = on_resize + + @win.event + def on_draw(): + global spin + win.clear() + glLoadIdentity() + glTranslatef(0, 0, -1.5) + glRotatef(spin, 1.0, 1.0, 1.0) + gluSphere(quadratic, 0.65, 60, 60) + + def update(dt): + global spin + spin += dt * 10.0 + pyglet.clock.schedule_interval(update, 1.0/30.0) + + win.set_visible() + pyglet.app.run() + diff --git a/vec_noise-1.1.4/test.py b/vec_noise-1.1.4/test.py new file mode 100644 index 0000000..8c7f6c0 --- /dev/null +++ b/vec_noise-1.1.4/test.py @@ -0,0 +1,139 @@ +import unittest + + +class PerlinTestCase(unittest.TestCase): + + def test_perlin_1d_range(self): + from vec_noise import pnoise1 + for i in range(-10000, 10000): + x = i * 0.49 + n = pnoise1(x) + self.assertTrue(-1.0 <= n <= 1.0, (x, n)) + + def test_perlin_1d_octaves_range(self): + from vec_noise import pnoise1 + for i in range(-1000, 1000): + for o in range(10): + x = i * 0.49 + n = pnoise1(x, octaves=o + 1) + self.assertTrue(-1.0 <= n <= 1.0, (x, n)) + + def test_perlin_1d_base(self): + from vec_noise import pnoise1 + self.assertEqual(pnoise1(0.5), pnoise1(0.5, base=0)) + self.assertNotEqual(pnoise1(0.5), pnoise1(0.5, base=5)) + self.assertNotEqual(pnoise1(0.5, base=5), pnoise1(0.5, base=1)) + + def test_perlin_2d_range(self): + from vec_noise import pnoise2 + for i in range(-10000, 10000): + x = i * 0.49 + y = -i * 0.67 + n = pnoise2(x, y) + self.assertTrue(-1.0 <= n <= 1.0, (x, y, n)) + + def test_perlin_2d_octaves_range(self): + from vec_noise import pnoise2 + for i in range(-1000, 1000): + for o in range(10): + x = -i * 0.49 + y = i * 0.67 + n = pnoise2(x, y, octaves=o + 1) + self.assertTrue(-1.0 <= n <= 1.0, (x, n)) + + def test_perlin_2d_base(self): + from vec_noise import pnoise2 + x, y = 0.73, 0.27 + self.assertEqual(pnoise2(x, y), pnoise2(x, y, base=0)) + self.assertNotEqual(pnoise2(x, y), pnoise2(x, y, base=5)) + self.assertNotEqual(pnoise2(x, y, base=5), pnoise2(x, y, base=1)) + + def test_perlin_3d_range(self): + from vec_noise import pnoise3 + for i in range(-10000, 10000): + x = -i * 0.49 + y = i * 0.67 + z = -i * 0.727 + n = pnoise3(x, y, z) + self.assertTrue(-1.0 <= n <= 1.0, (x, y, z, n)) + + def test_perlin_3d_octaves_range(self): + from vec_noise import pnoise3 + for i in range(-1000, 1000): + x = i * 0.22 + y = -i * 0.77 + z = -i * 0.17 + for o in range(10): + n = pnoise3(x, y, z, octaves=o + 1) + self.assertTrue(-1.0 <= n <= 1.0, (x, y, z, n)) + + def test_perlin_3d_base(self): + from vec_noise import pnoise3 + x, y, z = 0.1, 0.7, 0.33 + self.assertEqual(pnoise3(x, y, z), pnoise3(x, y, z, base=0)) + self.assertNotEqual(pnoise3(x, y, z), pnoise3(x, y, z, base=5)) + self.assertNotEqual(pnoise3(x, y, z, base=5), pnoise3(x, y, z, base=1)) + + +class SimplexTestCase(unittest.TestCase): + + def test_simplex_2d_range(self): + from vec_noise import snoise2 + for i in range(-10000, 10000): + x = i * 0.49 + y = -i * 0.67 + n = snoise2(x, y) + self.assertTrue(-1.0 <= n <= 1.0, (x, y, n)) + + def test_simplex_2d_octaves_range(self): + from vec_noise import snoise2 + for i in range(-1000, 1000): + for o in range(10): + x = -i * 0.49 + y = i * 0.67 + n = snoise2(x, y, octaves=o + 1) + self.assertTrue(-1.0 <= n <= 1.0, (x, n)) + + def test_simplex_3d_range(self): + from vec_noise import snoise3 + for i in range(-10000, 10000): + x = i * 0.31 + y = -i * 0.7 + z = i * 0.19 + n = snoise3(x, y, z) + self.assertTrue(-1.0 <= n <= 1.0, (x, y, z, n)) + + def test_simplex_3d_octaves_range(self): + from vec_noise import snoise3 + for i in range(-1000, 1000): + x = -i * 0.12 + y = i * 0.55 + z = i * 0.34 + for o in range(10): + n = snoise3(x, y, z, octaves=o + 1) + self.assertTrue(-1.0 <= n <= 1.0, (x, y, z, o+1, n)) + + def test_simplex_4d_range(self): + from vec_noise import snoise4 + for i in range(-10000, 10000): + x = i * 0.88 + y = -i * 0.11 + z = -i * 0.57 + w = i * 0.666 + n = snoise4(x, y, z, w) + self.assertTrue(-1.0 <= n <= 1.0, (x, y, z, w, n)) + + def test_simplex_4d_octaves_range(self): + from vec_noise import snoise4 + for i in range(-1000, 1000): + x = -i * 0.12 + y = i * 0.55 + z = i * 0.34 + w = i * 0.21 + for o in range(10): + n = snoise4(x, y, z, w, octaves=o + 1) + self.assertTrue(-1.0 <= n <= 1.0, (x, y, z, w, o+1, n)) + + +if __name__ == '__main__': + unittest.main()