diff --git a/docs/guides/controllers_and_update_rules.ipynb b/docs/guides/controllers_and_update_rules.ipynb index c4f9f73..4b0cbdf 100644 --- a/docs/guides/controllers_and_update_rules.ipynb +++ b/docs/guides/controllers_and_update_rules.ipynb @@ -2,11 +2,232 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "493170b9-97c0-439c-a272-20261b6b0adf", "metadata": {}, "outputs": [], - "source": [] + "source": [ + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "markdown", + "id": "f5e2e73b-370b-4989-99dd-ee37c05b11fb", + "metadata": {}, + "source": [ + "# Controllers and Update Rules\n", + "\n", + "In `pylattica`, the rule for evolving a simulation is defined in a subclass of `BasicController` which is implemented by the user. This is an extremely lightweight class which is given to the `Runner` in order to evolve a simulation (see the Runners guide for more details on running a simulation).\n", + "\n", + "In this guide, we will illustrate the following:\n", + "\n", + "- Implementing a simple update rule\n", + "- Returning updates for multiple sites\n", + "- Returning updates for the general simulation state\n", + "\n", + "## A simple example\n", + "\n", + "In our toy simulation, the state stored at each state will be a single integer. We will represent this state like this:\n", + "\n", + "```\n", + "{ \"value\": 1 }\n", + "```\n", + "\n", + "Where `\"value\"` is an arbitrarily chosen key for our state value, and `1` is the current integer value.\n", + "\n", + "Additionally, in this toy simulation, the value of the state at each site is incremented by 1 during each simulation step. Here's how we would use a `BasicController` subclass to implement this." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "75b16f2c-8e39-4554-9c15-9e9d310c89ce", + "metadata": {}, + "outputs": [], + "source": [ + "from pylattica.core import BasicController, SimulationState\n", + "\n", + "class ToyController(BasicController):\n", + " \n", + " def get_state_update(self, site_id: int, prev_state: SimulationState):\n", + " # retrieve the previous state for the site that needs to be updated\n", + " previous_site_state = prev_state.get_site_state(site_id)\n", + " old_value = previous_state.get(\"value\")\n", + " \n", + " # compute the new value\n", + " new_value = old_value + 1\n", + " \n", + " # return a dictionary of updates that should be applied to the state at that site\n", + " return {\n", + " \"value\": new_value\n", + " }" + ] + }, + { + "cell_type": "markdown", + "id": "0b158e0e-c69f-4051-b563-59884952c636", + "metadata": {}, + "source": [ + "The key observation to make here is that the update rule is implemented in a method called `get_state_update` that takes two arguments - the ID of the state whose updates should be computed and returned, and the previous state of the simulation.\n", + "\n", + "There are two salient features of this system:\n", + "\n", + "1. `pylattica` makes no assumptions about how you will use the previous `SimulationState` to compute the updates. For example, you could ignore it or you could use it in conjunction with a `Neighborhood`.\n", + "2. The return value of this function is a dictionary that represents the updates that should be applied to the state. If each site had a state dictionary with multiple keys (i.e. something in addition to `\"value\"`,), those keys would remain unchanged by this update rule." + ] + }, + { + "cell_type": "markdown", + "id": "7589c114-32a3-4ff6-9a95-17a7cc949f43", + "metadata": {}, + "source": [ + "## Updating Multiple Site States at Once\n", + "\n", + "Sometimes a simulation update rule will need to update the values of multiple site states during each application of the update rule (note that this will require use of the `AsynchronousRunner` - as described in the Runners guide).\n", + "\n", + "For example, you might need to implement an update rule that updates multiple sites at once to achieve conservation of some quantity in your simulation state.\n", + "\n", + "To do this, you can return a more complex form of the update dictionary, shown below." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "c8d91d12-29b0-4297-b11d-61bb8d95e35a", + "metadata": {}, + "outputs": [], + "source": [ + "from pylattica.core import BasicController, SimulationState\n", + "\n", + "class MultiSiteUpdate(BasicController):\n", + " \n", + " def get_state_update(self, site_id: int, prev_state: SimulationState):\n", + " # rename the first site for readability\n", + " site_1_id = site_id\n", + " \n", + " # determine the other site that will be updated in conjunction with the one identified by site_id\n", + " site_2_id = site_id + 1\n", + " \n", + " # retrieve the two states\n", + " site_1_prev_state = prev_state.get_site_state(site_1_id)\n", + " site_1_old_val = site_1_prev_state.get(\"value\")\n", + " \n", + " site_2_prev_state = prev_state.get_site_state(site_2_id)\n", + " site_2_old_val = site_2_prev_state.get(\"value\")\n", + " \n", + " # compute the new value\n", + " new_site_1_val = site_1_old_val + 1\n", + " new_site_2_val = site_2_old_val - 1\n", + " \n", + " # return a dictionary of updates that should be applied to the state at that site\n", + " return {\n", + " site_1_id: {\n", + " \"value\": new_site_1_val\n", + " },\n", + " site_2_id: {\n", + " \"value\": new_site_2_val\n", + " }\n", + " }" + ] + }, + { + "cell_type": "markdown", + "id": "8f5e1b9a-44ed-45e7-b871-e478ae327ce8", + "metadata": {}, + "source": [ + "Let's highlight the form of the return value for this controller:\n", + "\n", + "```\n", + "{\n", + " site_1_id: {\n", + " \"value\": new_site_1_val\n", + " },\n", + " site_2_id: {\n", + " \"value\": new_site_2_val\n", + " }\n", + "}\n", + "```\n", + "\n", + "In this case, the updates dictionary being returned has a top level of keys specifying the IDs of sites to which the updates should be applied. This format lets us express that this single invocation of the update rule should yield changes to both site 1 and site 2 (given by `new_site_1_val` and `new_site_2_val`)." + ] + }, + { + "cell_type": "markdown", + "id": "fb3c9ca7-2143-4c42-a7be-5dfa7c8948b2", + "metadata": {}, + "source": [ + "## Updating Multiple Sites and the General State\n", + "\n", + "In addition to per-site state values, `pylattica` also supports accounting for an additional state dictionary that applies to the entire simulation. This is called the *general state*.\n", + "\n", + "> **NOTE**: If you use the *general state*, you should also use the `AsynchronousRunner` to avoid applying conflicting updates to the general state at the same time.\n", + "\n", + "For example, below we implement a toy controller that increments both the site specific state, and a similar value in the general state called `\"general_value\"`." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "a4152e4d-cc30-4ba7-b9fd-1b244b8f6623", + "metadata": {}, + "outputs": [], + "source": [ + "from pylattica.core import BasicController, SimulationState\n", + "from pylattica.core.simulation_state import SITES, GENERAL\n", + "\n", + "class GeneralStateController(BasicController):\n", + " \n", + " def get_state_update(self, site_id: int, prev_state: SimulationState):\n", + " # retrieve the previous state for the site that needs to be updated\n", + " previous_site_state = prev_state.get_site_state(site_id)\n", + " old_value = previous_state.get(\"value\")\n", + " \n", + " previous_general_value = prev_state.get_general_state().get(\"general_value\")\n", + " \n", + " # compute the new value\n", + " new_value = old_value + 1\n", + " \n", + " new_general_value = previous_general_value + 1\n", + " \n", + " # return a dictionary of updates that should be applied to the state at that site\n", + " return {\n", + " SITES: {\n", + " site_id: {\n", + " \"value\": new_value\n", + " }\n", + " },\n", + " GENERAL: {\n", + " \"general_value\": new_general_value\n", + " }\n", + " }" + ] + }, + { + "cell_type": "markdown", + "id": "8bee24d0-3e6f-4bee-9e58-62602457b30e", + "metadata": {}, + "source": [ + "We had to import the `SITES` and `GENERAL` constants from `pylattica` in order to specify the form of this dictionary.\n", + "\n", + "Note that the dictionary under the `SITES` key is a mapping of site IDs to state updates. If we wanted to update multiple sites in addition to the general state, we could use a return value of something like this:\n", + "\n", + "```\n", + "{\n", + " SITES: {\n", + " site_id_1: {\n", + " \"value\": new_value\n", + " },\n", + " site_id_2: {\n", + " \"value\": new_value_2\n", + " }\n", + " },\n", + " GENERAL: {\n", + " \"general_value\": new_general_value\n", + " }\n", + "}\n", + "```" + ] } ], "metadata": { diff --git a/docs/guides/index.md b/docs/guides/index.md index 9ae28b9..466a7e9 100644 --- a/docs/guides/index.md +++ b/docs/guides/index.md @@ -10,6 +10,8 @@ It's likely that this demonstration does not illustrate everything you need for - [Periodic Structures](./periodic_structures.ipynb) - [Building Neighborhoods](./neighborhoods.ipynb) - [Representing Simulation State](./neighborhoods.ipynb) +- [Controllers and Update Rules](./controllers_and_update_rules.ipynb) +- [Runners](./runners.ipynb) - [Working With Square Grids](./square_grids.ipynb) Finally, if all else fails, the [Reference](../reference/core/lattice.md) provides individual method-level detail for the use of this library. \ No newline at end of file diff --git a/docs/guides/runners.ipynb b/docs/guides/runners.ipynb index 88e4566..7709501 100644 --- a/docs/guides/runners.ipynb +++ b/docs/guides/runners.ipynb @@ -2,11 +2,632 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "1879810a-4af4-4601-bd03-df77dae0adc9", "metadata": {}, "outputs": [], - "source": [] + "source": [ + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "markdown", + "id": "9ef21048-b0f2-468b-bdca-3b298b91eecb", + "metadata": { + "tags": [] + }, + "source": [ + "# Runners\n", + "\n", + "After you have defined the form of your `SimulationState`, the shape of the `Lattice`, `PeriodicStructure` and `Neighborhood` and the `Controller` that implements your update rule, you are ready to run your simulation! The entity responsible for running the simulation is called a `Runner`. In `pylattica` there are two types of runners which run the simulation differently.\n", + "\n", + "1. `SynchronousRunner` - This runner calculates and applies updates to _every site at once_ during each simulation step.\n", + "2. `AsynchronousRunner` - This runner calculates and applies updates to _a single site at a time_ during each simulation step.\n", + "\n", + "The `SynchronousRunner` behaves like a typical cellular automaton would. It's updates can be parallelized. The `AsynchronousRunner` is useful for implementing Monte Carlo type simulations, or \"asynchronous automata\", in which multiple cell states are updated during a single invocation of the update rule.\n", + "\n", + "## Set up\n", + "Before we dive into these distinctions, let's set up a small 2D square grid structure using some of the helper classes provided by `pylattica`." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "6169399a-4866-40f2-9812-588f5f8ba07a", + "metadata": {}, + "outputs": [], + "source": [ + "from pylattica.structures.square_grid import SimpleSquare2DStructureBuilder\n", + "\n", + "structure = SimpleSquare2DStructureBuilder().build(10)" + ] + }, + { + "cell_type": "markdown", + "id": "9e2f9b27-3aab-4a92-8aeb-8badb1effd2b", + "metadata": {}, + "source": [ + "Let's also set up a [Von Neumann neighborhood](https://en.wikipedia.org/wiki/Von_Neumann_neighborhood) to use here (also using a `pylattica` helper)." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "750d01c4-01c9-442a-b0c0-eb73e5b786ad", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|████████████████████████████████████| 100/100 [00:00<00:00, 18416.26it/s]\n" + ] + } + ], + "source": [ + "from pylattica.structures.square_grid.neighborhoods import VonNeumannNbHood2DBuilder\n", + "\n", + "nbhood = VonNeumannNbHood2DBuilder().get(structure)" + ] + }, + { + "cell_type": "markdown", + "id": "bef94743-486a-472e-8d0e-7a3f9751646b", + "metadata": {}, + "source": [ + "And finally, let's agree on a simulation state where the state at each cell looks like this:\n", + "```\n", + "{\n", + " \"value\": %some integer value%\n", + "}\n", + "```\n", + "\n", + "Let's also agree that the values will fall within the range 1 - 10.\n", + "\n", + "With that, we can construct a starting state for our simulation." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "d8d32894-1def-4a1e-a825-ac26df8605aa", + "metadata": {}, + "outputs": [], + "source": [ + "from pylattica.core import SimulationState\n", + "\n", + "starting_state = SimulationState.from_struct(structure)" + ] + }, + { + "cell_type": "markdown", + "id": "60d8583d-28ae-4b19-8a67-8d090a58d087", + "metadata": {}, + "source": [ + "And assign random values between 1 and 10 to each site in the structure. I'm putting this in a function so we can reuse it later in the guide." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "b2d4a539-8f21-4ddf-b1f1-e22941857a3d", + "metadata": {}, + "outputs": [], + "source": [ + "import random\n", + "\n", + "def set_random_state(state):\n", + " for site_id in structure.site_ids:\n", + " state.set_site_state(site_id, {\n", + " \"value\": random.randint(1,10)\n", + " })\n", + "\n", + "set_random_state(starting_state)" + ] + }, + { + "cell_type": "markdown", + "id": "602bbc5a-71fa-411b-9a85-7c9d5eac78f2", + "metadata": {}, + "source": [ + "We can visualize this state by creating a color scale corresponding to the sites 1 through 10:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "705db01a-11f5-4cdc-bc5f-3a3a075a46fb", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAUUAAADcCAIAAABoCCQ1AAAFg0lEQVR4nO3d0U0cSRSGUWY1SYAlJ7EOwyYNwlgTBg4DnAZBGGmXMHofWiqVYJFWXKzS/fucJ/uhNMNYn6qn2n05fb78fPFuD+9fenFTWHtZWHtVWPtXYe2nwtq7wto/C2u/vH/pr+3X/NfT6VR4H/xff6x+A8CH0TPk0DPk0DPkOK9+AxxR5RT26fnpA99JGPsz5NAz5NAz5NAz5NAz5NAz5HC/iva2bdv/4H+J25/pbY95L3mEfVj2Z3qzJ8/szySYd+kj0zPtiXnQM72NmLdt8/1ZzyRQ8s55GL25zJ6dS7OpCvOlSrO4Km4Laytzy+4Layszz34U1lZmj7GC623IoWfIoWfIoWfI4XybFSqnsNcf9i7y2J8hh54hh54hh54hh54hh54hh/tVtGd+2GB/pjfzw2Z6hhx6prd5Z3a9rWd6c7090zPk0DO9ud6euV9FezIezhfPhdWVp94qs6keCmv/Lqy9KaytzACrrP2nsPZrYS0ruN6GHHqGHHqGHHqGHM63WaFyCsvb7M+QQ8+QQ8+QQ8+QQ8+QQ8+Qw/0qenvxzPPBn82wP5Pg4BkP9md6U/LM/kwC8wx2eoYceqY9m/OgZ8jhPIz27MzDuTRf6rGw9nthbUXlSb3KHK/KZ1VZW5nTVnldVnC9DTn0DDn0DDn0DDmcb7NC5TcT8Db7M+TQM+TQM+TQM+TQM+TQM+TQMwm2bXsxSOyY3H+mNxnP7M/0djqdPC856Bly6Bly6BlyOA+jt/k8zGBAPdPbket97VyaL3VbWHtfWFt5zxWVn7cyp63y834rrK3MWmMF358hh54hh54hh54hh/NtVjDl//ewP0MOPUMOPUMOPUMOPUMOPUMO96tobzxi5dkM+zO9zc9ImiVmf6Y3e/JMz7TnentwvU17Y8Sn620905tJ+jM909u8M7ve9v2Z9mQ8nC+uCqsrT73dFNbeFdZeL3rdh8Layuf8pbC2ovJZ8V6utyGHniGHniGHniGH821WqJyWrTrh68D+DDn0DDn0DDn0DDn0DDn0DDncr6I98wwG+zO9mR820zPk0DO92Zlneqa9MT8MPdOb+WEzPdOb+WEz96toT8bDuTTH676w9rmw9nLR2u+FtZUnBCvzwx4Layv/Rqzgehty6Bly6Bly6BlyON9mhcqpJG+zP0MOPUMOPUMOPUMOPUMOPUMO96tI8OL5qsNOFLM/09vr55+PPFFMz/RmOMlMz5BDz5BDz5DD+Ta9zSde4yRsHJId7au1nuntP4s9WsbDuTQD7Gdh7dfC2k+LXrcyT+u2sLYyP+xbYW3ls2IF358hh54hh54hh54hh/NtVqj8FgfeZn+GHHqGHHqGHHqGHHqGHHqGHHomxOvBQwekZxIoeadn2tu27bAPSL6gZ3qzM8/0TIJR9cHz9v+36e3FDP2DX3jbnyGH/ZkQB9+Zd+fSTKwfhbWV2VSVtZV5WpeFtZXPqjI/rPJZVWae3RXW8l6utyGHniGHniGHniGH821WuFr9BkLZnyGHniGHniGHniGHniGHniGH+1X09uKB54M/laFnEhw848H1NgkM99zpmQT7/ixp19v05kp7Zn+mN3vyzP5MeyNpe7We6U3Ds3NpnlbF46K1lZ+3Mmut8p5/FtbeF9ZW3jMr+P4MOfQMOfQMOfQMOZxvs0LlNxPwNvsz5NAz5NAz5NAz5NAz5NAz5HC/ivY8XzXYn+ltj9l8kp39mRA25wv7MxnMA9zpmQSut3d6hhx6prd5Z/YV2nkY7cl4OD89P6155es1L3txs+h1V1n1ObOC623IoWfIoWfIoWfI4WCQ38KvWV/C/gw59Aw59Aw59Aw59Aw59Aw59Aw59Aw59Aw59Aw59Aw59Aw59Aw59Aw5/gWfSBOGQuYdkwAAAABJRU5ErkJggg==\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from pylattica.visualization import SquareGridArtist2D, DiscreteCellArtist\n", + "\n", + "color_scale = {}\n", + "\n", + "for i in range(1,11):\n", + " color_scale[i] = (30, int((i - 1) * 255/10), 30)\n", + " \n", + "cell_artist = DiscreteCellArtist(color_map = color_scale, state_key=\"value\")\n", + "step_artist = SquareGridArtist2D(structure, cell_artist)\n", + "\n", + "step_artist.jupyter_show(starting_state, cell_size=20)" + ] + }, + { + "cell_type": "markdown", + "id": "8c3a2028-6fdb-4670-b242-95ff6261ec94", + "metadata": {}, + "source": [ + "## Synchronous Controller and Evolution\n", + "\n", + "To demonstrate the `SynchronousRunner`, we need an update rule that only affects the state of a single site at a time. A good simple example would be: the future value at a cell is given by incrementing the previous value, unless the value is at a maximum (10).\n", + "\n", + "Let's implement this rule in a controller." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "8d0037ba-75d0-4aba-acb7-c3fe848287b2", + "metadata": {}, + "outputs": [], + "source": [ + "from pylattica.core import BasicController\n", + "\n", + "class SyncController(BasicController):\n", + " \n", + " def get_state_update(self, site_id: int, prev_state: SimulationState):\n", + " prev_site_val = prev_state.get_site_state(site_id).get(\"value\")\n", + " if prev_site_val < 10:\n", + " new_site_val = prev_site_val + 1\n", + " else:\n", + " new_site_val = prev_site_val\n", + " \n", + " return {\n", + " \"value\": new_site_val\n", + " }" + ] + }, + { + "cell_type": "markdown", + "id": "0ddeb0be-a592-4215-aae1-aaac60acc755", + "metadata": {}, + "source": [ + "And now we can instantiate a `SynchronousRunner` and run our simulation for 10 steps. \n", + "\n", + "> **NOTE**: Since every site is updated during each step, we expect every site to have a value of 10 by the end.\n", + "\n", + "Let's generate a new starting state and look at it so we know where we are starting from?" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "3ef49f92-0070-4a82-9f59-06674c0d1dc7", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAUUAAADcCAIAAABoCCQ1AAAFg0lEQVR4nO3d0U0cSRSGUWY1SYAlJ7EOwyYNwlgTBg4DnAZBGGmXMHofWiqVYJFWXKzS/fucJ/uhNMNYn6qn2n05fb78fPFuD+9fenFTWHtZWHtVWPtXYe2nwtq7wto/C2u/vH/pr+3X/NfT6VR4H/xff6x+A8CH0TPk0DPk0DPkOK9+AxxR5RT26fnpA99JGPsz5NAz5NAz5NAz5NAz5NAz5HC/iva2bdv/4H+J25/pbY95L3mEfVj2Z3qzJ8/szySYd+kj0zPtiXnQM72NmLdt8/1ZzyRQ8s55GL25zJ6dS7OpCvOlSrO4Km4Laytzy+4Layszz34U1lZmj7GC623IoWfIoWfIoWfI4XybFSqnsNcf9i7y2J8hh54hh54hh54hh54hh54hh/tVtGd+2GB/pjfzw2Z6hhx6prd5Z3a9rWd6c7090zPk0DO9ud6euV9FezIezhfPhdWVp94qs6keCmv/Lqy9KaytzACrrP2nsPZrYS0ruN6GHHqGHHqGHHqGHM63WaFyCsvb7M+QQ8+QQ8+QQ8+QQ8+QQ8+Qw/0qenvxzPPBn82wP5Pg4BkP9md6U/LM/kwC8wx2eoYceqY9m/OgZ8jhPIz27MzDuTRf6rGw9nthbUXlSb3KHK/KZ1VZW5nTVnldVnC9DTn0DDn0DDn0DDmcb7NC5TcT8Db7M+TQM+TQM+TQM+TQM+TQM+TQMwm2bXsxSOyY3H+mNxnP7M/0djqdPC856Bly6Bly6BlyOA+jt/k8zGBAPdPbket97VyaL3VbWHtfWFt5zxWVn7cyp63y834rrK3MWmMF358hh54hh54hh54hh/NtVjDl//ewP0MOPUMOPUMOPUMOPUMOPUMO96tobzxi5dkM+zO9zc9ImiVmf6Y3e/JMz7TnentwvU17Y8Sn620905tJ+jM909u8M7ve9v2Z9mQ8nC+uCqsrT73dFNbeFdZeL3rdh8Layuf8pbC2ovJZ8V6utyGHniGHniGHniGH821WqJyWrTrh68D+DDn0DDn0DDn0DDn0DDn0DDncr6I98wwG+zO9mR820zPk0DO92Zlneqa9MT8MPdOb+WEzPdOb+WEz96toT8bDuTTH676w9rmw9nLR2u+FtZUnBCvzwx4Layv/Rqzgehty6Bly6Bly6BlyON9mhcqpJG+zP0MOPUMOPUMOPUMOPUMOPUMO96tI8OL5qsNOFLM/09vr55+PPFFMz/RmOMlMz5BDz5BDz5DD+Ta9zSde4yRsHJId7au1nuntP4s9WsbDuTQD7Gdh7dfC2k+LXrcyT+u2sLYyP+xbYW3ls2IF358hh54hh54hh54hh/NtVqj8FgfeZn+GHHqGHHqGHHqGHHqGHHqGHHomxOvBQwekZxIoeadn2tu27bAPSL6gZ3qzM8/0TIJR9cHz9v+36e3FDP2DX3jbnyGH/ZkQB9+Zd+fSTKwfhbWV2VSVtZV5WpeFtZXPqjI/rPJZVWae3RXW8l6utyGHniGHniGHniGH821WuFr9BkLZnyGHniGHniGHniGHniGHniGH+1X09uKB54M/laFnEhw848H1NgkM99zpmQT7/ixp19v05kp7Zn+mN3vyzP5MeyNpe7We6U3Ds3NpnlbF46K1lZ+3Mmut8p5/FtbeF9ZW3jMr+P4MOfQMOfQMOfQMOZxvs0LlNxPwNvsz5NAz5NAz5NAz5NAz5NAz5HC/ivY8XzXYn+ltj9l8kp39mRA25wv7MxnMA9zpmQSut3d6hhx6prd5Z/YV2nkY7cl4OD89P6155es1L3txs+h1V1n1ObOC623IoWfIoWfIoWfI4WCQ38KvWV/C/gw59Aw59Aw59Aw59Aw59Aw59Aw59Aw59Aw59Aw59Aw59Aw59Aw59Aw5/gWfSBOGQuYdkwAAAABJRU5ErkJggg==\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "sync_starting_state = SimulationState.from_struct(structure)\n", + "set_random_state(sync_starting_state)\n", + "\n", + "step_artist.jupyter_show(starting_state, cell_size=20)" + ] + }, + { + "cell_type": "markdown", + "id": "c6c6582a-1384-4423-b3a9-4bffcd01cb17", + "metadata": {}, + "source": [ + "And now let's run for 10 steps:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "56920b0e-2989-4af1-99d7-b14a37550dde", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|███████████████████████████████████████| 10/10 [00:00<00:00, 9754.20it/s]\n" + ] + } + ], + "source": [ + "from pylattica.core.runner import SynchronousRunner\n", + "\n", + "sync_runner = SynchronousRunner()\n", + "sync_cont = SyncController()\n", + "num_steps = 10\n", + "\n", + "sync_result = sync_runner.run(starting_state, sync_cont, num_steps)" + ] + }, + { + "cell_type": "markdown", + "id": "3972f35e-323f-4700-bb98-06e68a033b56", + "metadata": {}, + "source": [ + "Finally, we can visualize the last step of this simulation:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "8dab4b48-1259-4cce-a6ef-ddded7d76c57", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAUUAAADICAIAAADwT2S4AAACwklEQVR4nO3cQWqEQBRAQTvkFAnM/a81AzlHZxEiMoRsYhCfVTt105tH61cct4/bAnu7v923h2OMo1ZyKS9HLwDYjZ6hQ8/QoWfoeD16AVzRX6awj/fHjiuJsT9Dh56hQ8/QoWfo0DN06Bk6vK+iYM65bL4S/zpcrvfduP2Zc5tzrvWuZ5bvkp8u5emZcxtjXG0T/oWeoUPP0KFn6DDf5ty2E691ErYOya72aK1nzu3HYq+W8cr9NnToGTr0DB16hg49Q4f5NgfwD7B/Yn+GDj1Dh56hQ8/QoWfo0DN06Bk69AwdeoYOPUOHnqFDz9ChZ+jQM3ToGTr0DB16hg49Q4eeoUPP0KFn6NAzdOgZOvQMHXqGDj1Dh56hQ8/QoWfo0DN06Bk69AwdeoYOPUOHnqFDz9ChZ+jQM3ToGTr0DB16hg49Q4eeoUPP0KFn6NAzdOgZOvQMHXqGDj1Dh56hQ8/QoWfo0DN06Bk69AwdeoYOPUOHnqFDz9ChZ+jQM3ToGTr0DB16hg49Q4eeoUPP0KFn6NAzdOgZOvQMHXqGDj1Dh56hQ8/QoWfo0DN06Bk69AwdeoYOPUOHnqFDz9ChZ+jQM3ToGTr0DB16hg49Q4eeoUPP0KFn6NAzdOgZOvQMHXqGDj1Dh56hQ8/QoWfo0DN06Bk69AwdeoYOPUOHnqFDz9ChZ+jQM3ToGTr0DB16hg49Q4eeoUPP0KFn6NAzdOgZOvQMHXqGDj1Dh56hQ8/QoWfo0DN06Bk69AwdeoYOPUOHnqFDz9ChZ+jQM3ToGTr0DB16hg49Q4eeoUPP0KFn6NAzdOgZOvQMHXqGDj1Dh56hQ8/QoWfo0DN06Bk69AwdeoYOPUOHnqFDz9ChZ+jQM3ToGTr0DB16hg49Q4eeoUPP0KFn6NAzdOgZOvQMHXqGDj1Dh56hQ8/QoWfo0DN0fAIGoCd2xaN6qQAAAABJRU5ErkJggg==\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "step_artist.jupyter_show(sync_result.last_step)" + ] + }, + { + "cell_type": "markdown", + "id": "7ded3348-52e0-4a77-834e-e1ab8cb3e8b4", + "metadata": {}, + "source": [ + "As expected, every cell has been incremented to 10.\n", + "\n", + "We can also look at an intermediate step in the result:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "2a3c26d7-8c9b-4920-b7ee-8baa2c8e3cab", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAUUAAADICAIAAADwT2S4AAAEOElEQVR4nO3d0W0VRxSAYd/ITdgPNEHKcNpw2qCNuI3QBkXgByjj5gFptYIQgY/Jav79vjcLjTwW+jV7Z/Hh8uavNzcv9vDypTcfBmvvB2vvBmsnngZrH19tFz9nsOeP7z7uv7xcLtPN8AN+O3oDwKvRM3ToGTr0DB23R2+AM5rcwj7/+fyKO4lxPkOHnqFDz9ChZ+jQM3ToGTq8r2Jt1+t1/+XJ/6G485mCk2e8cT6zNiXvOZ8p+PLUrW09Q4eeWZ7DeaNn6HAfxvKczJvb0Qywk83EGu35qJ934u3RG+Aned6GDj1Dh56hQ8/Q4X6bI0xuYfk+5zN06Bk69AwdeoYOPUOHnqHD+yoKvvqVyW2o2Nl+VcP5zNqu1+tXIwH3bX/1R3l6Zm2Xy+Vsh/B/0DN06Bk69Awd7rdZ2/7Ga7sJ2y7JzvbRWs+s7V+LPVvGm1nPK87Emuz582Dt3WDtxGTP5oetxudn6NAzdOgZOvQMHe63OcLkfzXg+5zP0KFn6NAzdOgZOvQMHXqGDu+rWN5pp4V9y/nM2s48LexbzmciHM43zmcavp3yeU56psDz9hd6hg49s7b9yewjtPswlifjzaznyWyqD6Pv/HIPg7VHzQCbmOx58vd7P1jLS3nehg49Q4eeoUPP0OF+myOY1P9rOJ+hQ8/QoWfo0DN06Bk69Awd3lexPPPDNs5n1mZ+2J6eoUPPrM08gz09szbP23t6hg49szbP23veV7E8GW9mPf89WPs4+s7rmcziOmpu2Yrz0s7N8zZ06Bk69AwdeoYO99scwbT9X8P5DB16hg49Q4eeoUPP0KFn6PC+iuWZH7ZxPrM28wz29AwdemZtTuY9PbO8y+Xik/MXemZt1+vV4bzRM2szP2zP+yqWJ+PNrOe3g7XvB2sfBmufBmsnM88+DNYe9fNOvDvo+56b523o0DN06Bk69Awd7rc5wPPvz0dvocn5DB16hg49Q4eeoUPP0KFn6NAzdOgZOvQMHXqGDj1Dh56hQ8/QoWfouB3NtZrMDzvK/WDtUTPPJv4YrP30arvg/+F8hg49Q4eeoUPP0KFn6NAzdOgZOvQMHXqGDj1Dh56hQ8/QoWfo0DN06Bk69AwdeoYOPUOHnqHjdskZYE+DtUfN0zpqTtvdQWs5gvMZOvQMHXqGDj1Dh56hQ8/QoWfo0DN06Bk69AwdeoYOPUOHnqFDz9ChZ+jQM3ToGTr0DB16ho7b0eqjZmJNZoAdNU/r82At/BjnM3ToGTr0DB16hg49Q4eeoUPP0KFn6NAzdOgZOvQMHXqGDj1Dh56hQ8/QoWfo0DN06Bk69Awdtzfvj97CC0zmlj282i5+zmTPnwZrHwdrJ3u+H6zlpZzP0KFn6NAzdOgZOvQMHXqGDj1Dh56hQ8/QoWfo0DN06Bk69AwdeoYOPUOHnqFDz9ChZ+jQM3T8A+W+kUerZraQAAAAAElFTkSuQmCC\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "step_artist.jupyter_show(sync_result.get_step(5))" + ] + }, + { + "cell_type": "markdown", + "id": "429d324b-695c-420b-831c-3adb24bdb7d0", + "metadata": {}, + "source": [ + "As we expect, there are no cell states below 5 since every cell has been updated at least 5 times by this point." + ] + }, + { + "cell_type": "markdown", + "id": "a3d6c986-2fb8-422a-a173-c8742d94b3af", + "metadata": {}, + "source": [ + "## Asynchronous Controller and Evolution\n", + "\n", + "We will continue our demonstration of `Runner` objects using the same structure and simulation state seen above, but we will tweak our update rule slightly.\n", + "\n", + "First, instead of incrementing by one during every update, let's say each update will _randomly_ increment or decrement the cell it's applied to.\n", + "\n", + "_Additionally,_ assume we want to conserve the sum of the values present in our simulation. In other words, if at any simulation step, we added up the values held by every cell, we would expect to see the same total.\n", + "\n", + "This would be impossible to achieve with a `SynchronousController` because the total value is a _conserved quantity_. The `SynchronousController` updates every site at once and therefore cannot coordinate site changes. There is no way to conserve a quantity unless we know all the changes that will take place during a given simulation step. This situation is encountered frequently - for instance, in the case of an adsorbed atom moving around on the surface of a crystal, the movement can only be represented by updating neighboring site states simultaneously.\n", + "\n", + "Note that we are removing the bounds on possible values, so our color scale may not be relevant.\n", + "\n", + "Let's make this concrete by implementing the `Controller` that does this." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "8b9233ea-cdff-4a56-bcd3-064f5ac2fceb", + "metadata": {}, + "outputs": [], + "source": [ + "class AsyncController(BasicController):\n", + " \n", + " def get_state_update(self, site_id: int, prev_state: SimulationState):\n", + " prev_site_val = prev_state.get_site_state(site_id).get(\"value\")\n", + " \n", + " neighbors = nbhood.neighbors_of(site_id)\n", + " rand_nb_id = random.choice(neighbors)\n", + " \n", + " prev_nb_val = prev_state.get_site_state(rand_nb_id).get(\"value\")\n", + " \n", + " direction = random.choice([-1, 1])\n", + " new_site_val = prev_site_val + direction\n", + " new_nb_val = prev_nb_val - direction\n", + " \n", + " return {\n", + " site_id: {\n", + " \"value\": new_site_val\n", + " },\n", + " rand_nb_id: {\n", + " \"value\": new_nb_val\n", + " } \n", + " }" + ] + }, + { + "cell_type": "markdown", + "id": "57765b82-3f4c-4b02-be05-37bfc25336eb", + "metadata": {}, + "source": [ + "We also need a new color scale for this. Here's a color scale where the intensity of the green is related to the absolute value of the cell's state." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "665a2aee-a8e5-4321-bf32-be3246d55857", + "metadata": {}, + "outputs": [], + "source": [ + "import math\n", + "\n", + "color_scale = {}\n", + "\n", + "b = 30\n", + "\n", + "for i in range(-b, b + 1):\n", + " color_scale[i] = (30, int(math.fabs(i - 1) * 255/(b)), 30)\n", + " \n", + "legend = {}\n", + "\n", + "for i in range(-b, b + 1, int((b+b)/ 10)):\n", + " legend[str(i)] = color_scale[i]\n", + "\n", + "cell_artist = DiscreteCellArtist(color_map = color_scale, state_key=\"value\", legend = legend)\n", + "step_artist = SquareGridArtist2D(structure, cell_artist)" + ] + }, + { + "cell_type": "markdown", + "id": "d6cf1315-6d88-4e6d-82b1-085932d3c443", + "metadata": {}, + "source": [ + "Additionally, let's create a starting state where every cell value is 0." + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "c8626f11-ef30-43f7-90a3-e09449a09f3b", + "metadata": {}, + "outputs": [], + "source": [ + "async_starting_state = SimulationState.from_struct(structure)\n", + "\n", + "for site_id in structure.site_ids:\n", + " async_starting_state.set_site_state(site_id, { \"value\": 0 })" + ] + }, + { + "cell_type": "markdown", + "id": "9683df0a-f601-49ef-8003-b821aa50b7bf", + "metadata": {}, + "source": [ + "We can see by visualization that every site has a value of 0:" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "0bd46974-c93d-48a1-8fe7-508b1359330b", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAUUAAADwCAIAAAAYZuF4AAAFUUlEQVR4nO3cMY7bVhRAUSlwkVVMkS1kyVlPyqT3bCLumEIAwYiiYGQ0Q/z7z6lkwQbYXLxP0nrXt1/fLvBq3//5vv3j9Xo960qm8svZFwC8jJ6hQ8/QoWfo+Hb2BTCjt+X/P4V9v76/8EpizGfo0DN06Bk69AwdeoYOPUOH91UELcty+7D+v/H9N0nmMzW3dG/d3j7vv6kyn6lpT+Dn9MzY7ubt3RCerW09M7aHxa5VL8syVdLun6m5ZXz2VZxDz9Rsj9y3z/tvqpy3CdpH2854ZT5Dh56hQ8/QoWfo0DN0eL7NGf46+wKizGfo0DN06Bk69AwdeoYOPUOH91UEHe0zyP/Eynym5mhb2Aw/itYzNdfrdT+BJ1lU4rzN2B7uD7vMtNNzS8+M7eHUfXifvFYdntV6pmaNef3wcGgnuX+maZ4z9pb5TM2T8RuezDfmM3ToGTr0DB16hg49Q4fn25zhz7MvIMp8hg49Q4eeoUPP0KFn6NAzdHhfRdPdTyOPNorFmM/ULMty92PJeXaV6Jmah/vDJuG8zdiO9ofdfbkO7XbqemZsP9PndgNReHnYxXkbSsxnavY79J23YVRHt9BffyVfz3kbOvQMHXqGDj1Dh56hw/NtzmB/2Ocwn6FDz9ChZ+jQM3ToGTr0DB3eV9FkfxgUzLw/zHymZt9teyZv6Zmx/cz+sO3fbLetZ8b2k33OEPPF/TMz2G8dqjKfqdnvD9t/rtIzNflD9RPO29ChZ+jQM3ToGTr0DB2eb3MG+8M+h/kMHXqGDj1Dh56hQ8/QoWfo8L6KoEm2he2Zz9TMsy1sz3wma7bhfNEzozvaHzbJgqE7emZsR8Wu24WmStr9M3TomZrtk7CphvPFeZuk2TJemc/QoWfo0DN06Bk69Awdnm9zBvvDPof5DB16hg49Q4eeoUPP0KFn6PC+iuFNuy1sz3xmbDNvC9vTM3ToGTr0DB16hg49M7aZt4XteV/F8GS8Mp+hQ8/QoWfo0DN06Bk6PN/mDL994N/+/bKr6DGfoUPP0KFn6NAzdOgZOvQMHd5XMTz7w1bmM2OzP2zLfCbCcL6YzzQsy2I4X/RMg/P2jZ6hQ8+Mzf6wLc/DGJ6MV+YzdOgZOvQMHXqGDj1Dh+fbnOH3D/xb+8OOmc/QoWfo0DN06Bk69AwdeoYO76souPt91bQbxcxnxrbfTDLzRjHzmbHtu51tJm+ZzzTNueFAzwTNGfNFz/SsMU+49NP9M2PbFnv0eR56ZmwTHqqfcN6GDj1Dh56hQ8/QoWfo8HybM3xkf9gfL7uKHvMZOvQMHXqGDj1Dh56hQ8/Q4X0VBfaH3ZjPjM3+sC09M7br9TrbEH5Cz9RsJ/NsqeuZGudtoMDzbca23xm23QQ423lbz4ztYbGzZbxy3oYOPUOHnqFDz9ChZ+jwfJszfGR/GMfMZ+jQM3ToGTr0DB16hg49Q4f3VQzvaFvYhD+xMp8Z29H2gtk2GdzombE93B+2LMtUY3mlZwpm3jG0pWeG9/A+ea16qrw9D2Ns2x1Dl/8evz0PgyFNNYSfMJ8Z25PxO9Vkvvn2/uP97GsAXsN5Gzr0DB16hg49Q4fn25zB/rDPYT5Dh56hQ8/QoWfo0DN06Bk6vK9iePv9YUcbxfLMZ8a230wy864S85mxzTaBn9Mzw5v2dL3nvM3w1h1Ds52u9/TM2JZlkfFKz4xtO5lvn/ffzMP9M8PbRztbxivzGTr0DB16hg49Q4eeoWPSx4B8trt3wtM+cP5i5jN06Bk69AwdeoYOPUOHnqFDz9ChZ+jQM3ToGTr0DB16hg49Q4eeoeNfEV2tU0EvnvEAAAAASUVORK5CYII=\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "step_artist.jupyter_show(async_starting_state, cell_size=20)" + ] + }, + { + "cell_type": "markdown", + "id": "151a2274-eaab-4b38-81d3-18ad21880e4a", + "metadata": {}, + "source": [ + "But we can also make a helper function that adds the state values up to see that the total is 0:" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "38c85dd1-805a-4ebd-b909-026137a1dc97", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The total of all sites for the starting state is: 0\n" + ] + } + ], + "source": [ + "def get_total(state: SimulationState):\n", + " total = 0\n", + " for state in state.all_site_states():\n", + " total += state.get(\"value\")\n", + " return total\n", + "\n", + "print(\"The total of all sites for the starting state is:\", get_total(sync_starting_state))" + ] + }, + { + "cell_type": "markdown", + "id": "34205b54-30d4-4e50-8652-ab3622f1e635", + "metadata": {}, + "source": [ + "Great, as expected, the total is zero. Now let's run our simulation and see how this changes." + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "a5344521-b01b-455e-a86d-fed0b15616e2", + "metadata": {}, + "outputs": [], + "source": [ + "from pylattica.core.runner import AsynchronousRunner\n", + "\n", + "runner = AsynchronousRunner()\n", + "controller = AsyncController()\n", + "num_steps = 100\n", + "\n", + "async_result = runner.run(async_starting_state, controller, num_steps)" + ] + }, + { + "cell_type": "markdown", + "id": "7fedcbad-a5b3-44ee-930f-cc602b56f1b6", + "metadata": {}, + "source": [ + "After 100 steps, we see that values have started to slightly diverge from 0:" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "6667e657-c9fa-4988-bb67-7d0059d792ef", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAUUAAADwCAIAAAAYZuF4AAAGR0lEQVR4nO3cvW4bZxCGUTJgYZcunUKFbyGXnOtJGZcGrCIqXcrdpiCw2HBJQvFI+vC9c07FCFloneTB7E84x4fDw2GID4VjP77aWbyfz4VjnwrHPheOLfj+/H37l8fjccx5NPPb6BMAXo2eIYeeIYeeIcdp9AnQ0cPy609hH4+Pr3gmYcxnyKFnyKFnyKFnyKFnyKFnyOF9FYGWZTl/WP+/8f1PIpnPpDmne+72/Hn/k1TmM2myJ/B9emZuF/P2Ygh3a1vPzO1qsWvVy7K0Str9M2nOGY8+izH0TJrtJff58/4nqVxvE2gfbXbGq9OwPV4/CseOUvnzfhv0e38Wjq38t8EIrrchh54hh54hh54hh+fbjPD36BMIZT5DDj1DDj1DDj1DDj1DDj1DDu+rCHRrn0H8V6zMZ9Lc2hbW4UvReibN8XjcT+Ami0pcbzO3q/vDDp12em7pmbldnbpX75PXqoNntZ5Js8a8frg6tCO5fyZTn2vsLfOZNHfGb/BkPjuV9kuN2k31qXBs5ZyfC8dWzvmpcKwdYJ243oYceoYceoYceoYcnm8zwl+jTyCU+Qw59Aw59Aw59Aw59Aw59Aw5vK8i08VXI29tFAtjPpNmWZaLL0v22VWiZ9Jc3R/WhOtt5nZrf9jFD9ehnZ26npnbS/rcbiAKXh52cL0NScxn0ux36LvehlnduoV+/zN5f6fD58LRP17tPP6fUfu0PhaOrewtm/HfESO4f4YceoYceoYceoYcnm8zgv1hb8N8hhx6hhx6hhx6hhx6hhx6hhzeV5HJ/jBI0Hl/mPlMmn232TN5S8/M7SX7w7Z/Z3bbemZuL+yzQ8wH9890sN86lMp8Js1+f9j+cyo9kyb+ovqOU2mv1SifJvy9lT1eo3aAVfalMYL7Z8ihZ8ihZ8ihZ8jh+TYj2B/2NsxnyKFnyKFnyKFnyKFnyKFnyOF9FYGabAvbM59J02db2J75TKxuw/mgZ2Z3a39YkwVDF/TM3G4Vu24XapW0+2fIoWfSbJ+EtRrOB9fbROqW8arWc2W/1Iy7uD5M+HtH/XNmBNfbkEPPkEPPkEPPkMPzbUawP+xtmM+QQ8+QQ8+QQ8+QQ8+QQ8+Qw/sqptd2W9ie+czcOm8L29Mz5NAz5NAz5NAz5NAzc+u8LWzP+yqmJ+PV6fBcOPpn4djKbqrKTqyKp8Kxo8658s+5sh+OEVxvQw49Qw49Qw49Qw7PtxnhS+HYr692FnnMZ8ihZ8ihZ8ihZ8ihZ8ihZ8jhfRXTsz9sZT4zN/vDtsxnQhjOB/OZDMuyGM4HPZPB9faZniGHnpmb/WFbnocxPRmvTqUdUZVjR+0Pq+xLqxj1eys73uwPm43rbcihZ8ihZ8ihZ8jh+TYj/FE41v6w28xnyKFnyKFnyKFnyKFnyKFnyOF9FQkuvl/VdqOY+czc9ptJOm8UM5+Z277bbjN5y3wmU88NB3omUM+YD3omzxpzw6Wf7p+Z27bYW5/70DNza3hRfceptNeqspvqS+HYb4VjPxSOrfx5K7+3si/tqXDsqJ1n/Cr3z5BDz5BDz5BDz5DD821GqOwP+/PVziKP+Qw59Aw59Aw59Aw59Aw59Aw5vK8igf1hZ+Yzc7M/bEvPzO14PHYbwnfomTTbydwtdT2TxvU2kMDzbea23xm23QTY7Xpbz8ztarHdMl6dhu3x+qdwbGUX1++FYyvnXFHZATZqXxojuH+GHHqGHHqGHHqGHJ5vM0Jlfxi3mc+QQ8+QQ8+QQ8+QQ8+QQ8+Qw/sqpndrW1jDr1iZz8zt1vaCbpsMzvTM3K7uD1uWpdVYXumZBJ13DG3pmeldvU9eq26Vt+dhzG27Y+jw38tvz8NgSq2G8B3mM3O7M35bTeaz0+PXx9Hn8L4qO89mZAdYJ663IYeeIYeeIYeeIYfn24xgf9jbMJ8hh54hh54hh54hh54hh54hh/dVTG+/P+zWRrF45jNz228m6byrxHxmbt0m8H16Znptr673XG8zvXXHULer6z09M7dlWWS80jNz207m8+f9T/pw/8z09tF2y3hlPkMOPUMOPUMOPUMOPUOOpo8BeWsX74TbPnB+Z+Yz5NAz5NAz5NAz5NAz5NAz5NAz5NAz5NAz5NAz5NAz5NAz5NAz5NAz5PgXyHTXlEOHdbQAAAAASUVORK5CYII=\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "step_artist.jupyter_show(async_result.last_step, cell_size=20)" + ] + }, + { + "cell_type": "markdown", + "id": "d839fa31-7566-4926-81e0-bef1e61559e5", + "metadata": {}, + "source": [ + "But the total is still zero!" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "c7621ceb-ba35-4575-9454-f2521c24973d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Total after 100 steps: 0\n" + ] + } + ], + "source": [ + "print(\"Total after 100 steps:\", get_total(async_result.last_step))" + ] + }, + { + "cell_type": "markdown", + "id": "0a4fd121-fa83-43d2-bf89-b55d6388db03", + "metadata": {}, + "source": [ + "Let's run for 10000 steps and see what the result is:" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "id": "81064558-903f-491e-9dee-4e0acc7e8628", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Total after 10000 steps: 0\n" + ] + } + ], + "source": [ + "async_result = runner.run(async_starting_state, controller, 10000)\n", + "step_artist.jupyter_show(async_result.last_step, cell_size=20)\n", + "print(\"Total after 10000 steps:\", get_total(async_result.last_step))" + ] + }, + { + "cell_type": "markdown", + "id": "e407c0b7-2e8f-4e2d-a230-5df52e7eb475", + "metadata": {}, + "source": [ + "The sum is still 0 even though the value of many of these cells has moved significantly away from 0.\n", + "\n", + "This illustrates how an `AsynchronousRunner` can be used to implement quantity conservation." + ] } ], "metadata": { diff --git a/mkdocs.yml b/mkdocs.yml index 58bbd03..f30aa6d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -17,8 +17,8 @@ nav: - Periodic Structures: ./guides/periodic_structures.ipynb - Neighborhoods: ./guides/neighborhoods.ipynb - Simulation States: ./guides/simulation_state.ipynb - # - Controllers and Update Rules: ./guides/controllers_and_update_rules.ipynb - # - Runners: ./guides/runners.ipynb + - Controllers and Update Rules: ./guides/controllers_and_update_rules.ipynb + - Runners: ./guides/runners.ipynb - Square Grids: ./guides/square_grids.ipynb - Examples: - Life Like Demos: ./examples/life_like_demos.ipynb diff --git a/src/pylattica/visualization/cell_artist.py b/src/pylattica/visualization/cell_artist.py index eb59ca4..4352d99 100644 --- a/src/pylattica/visualization/cell_artist.py +++ b/src/pylattica/visualization/cell_artist.py @@ -33,19 +33,19 @@ def get_legend(self, simulation_state: SimulationState): class DiscreteCellArtist(CellArtist): @classmethod - def from_phase_list(cls, phases): + def from_phase_list(cls, phases, **kwargs): color_map = cls.build_color_map_from_phase_list(phases) - return cls(color_map) + return cls(color_map, **kwargs) @classmethod - def from_discrete_state(cls, state: SimulationState): + def from_discrete_state(cls, state: SimulationState, **kwargs): analyzer = DiscreteStepAnalyzer() phases = analyzer.phases_present(state) - return cls.from_phase_list(phases) + return cls.from_phase_list(phases, **kwargs) @classmethod def from_discrete_result( - cls, result: SimulationResult + cls, result: SimulationResult, **kwargs ) -> typing.Dict[str, typing.Tuple[int, int, int]]: """Returns a map of phases to colors that can be used to visualize the phases @@ -54,7 +54,7 @@ def from_discrete_result( color values """ analyzer = DiscreteResultAnalyzer(result) - return cls.from_phase_list(analyzer.all_phases()) + return cls.from_phase_list(analyzer.all_phases(), **kwargs) @classmethod def build_color_map_from_phase_list(cls, phases): @@ -73,12 +73,23 @@ def build_color_map_from_phase_list(cls, phases): return display_phases - def __init__(self, color_map=None): + def __init__(self, color_map=None, state_key = DISCRETE_OCCUPANCY, legend = None): self.color_map = color_map + self._state_key = state_key + self.legend = legend + + def get_legend(self, simulation_state: SimulationState): + if self.legend is None: + return super().get_legend(simulation_state) + else: + return self.legend def get_color_from_cell_state(self, cell_state: Dict): - phase_name = cell_state[DISCRETE_OCCUPANCY] - return self.color_map[phase_name] + phase_name = cell_state[self._state_key] + if phase_name not in self.color_map: + return (0,0,0) + else: + return self.color_map[phase_name] def get_cell_legend_label(self, cell_state: Dict): - return cell_state[DISCRETE_OCCUPANCY] + return str(cell_state[self._state_key])