diff --git a/examples/boid_flockers/Flocker Test.ipynb b/examples/boid_flockers/Flocker Test.ipynb new file mode 100644 index 00000000000..c757f3a88ed --- /dev/null +++ b/examples/boid_flockers/Flocker Test.ipynb @@ -0,0 +1,113 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "from boid_flockers.model import BoidFlockers\n", + "\n", + "%matplotlib inline" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def draw_boids(model):\n", + " x_vals = []\n", + " y_vals = []\n", + " for boid in model.agents:\n", + " x, y = boid.pos\n", + " x_vals.append(x)\n", + " y_vals.append(y)\n", + " fig = plt.figure(figsize=(10, 10))\n", + " ax = fig.add_subplot(111)\n", + " ax.scatter(x_vals, y_vals)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "model = BoidFlockers(100, 100, 100, speed=5, vision=5, separation=1)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "for i in range(50):\n", + " model.step()" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAlwAAAJPCAYAAACpXgqFAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJzt3W+snNd9H/jvT1LMUnEVmQwg+Y9iB22MxEbqVt1N04Kt\nuGtLVI3WirCA0wAu1LTJInAXN1rSrSUnqPUi68ZuyPVqF4bRJnaJoPZWTaPYKdwV2TRMs9ggzsZx\n7Ur22img1rIhuiHtMHEU1TbPvpih7tXVveS9d+bcZ56ZzwcYaJ5n5rlz9PDOne+c8zvnqdZaAADo\n57qhGwAAsOwELgCAzgQuAIDOBC4AgM4ELgCAzgQuAIDOdhS4quoDVXW+qj69Yd8/qqrPVNW/r6pf\nrKpv2/DYg1X1+ar6bFXd1aPhAABjsdMerg8muXvTvjNJXttae12SzyV5MEmq6jVJfjDJa6bHvK+q\n9KQBACtrR0GotfbrSb6yad/Z1trl6eZvJnnF9P49ST7cWvt6a+3JJL+b5Pvm01wAgPGZV8/T307y\nsen9lyV5asNjTyV5+ZxeBwBgdGYOXFX1E0n+a2vtQ1d5musHAQAr64ZZDq6qv5XkjUlev2H3F5Pc\ntmH7FdN9m48VwgCA0Wit1V6P3XPgqqq7k/y9JHe01v54w0MfTfKhqjqVyVDidyX5+FY/Y5aGr7qq\neqi19tDQ7Rgr5282zt/eOXezcf5m4/zt3awdRTsKXFX14SR3JPn2qvpCkndmMivxRUnOVlWS/EZr\n7a2ttSeq6pEkTyT5RpK3ttb0ZgEAK2tHgau19kNb7P7AVZ7/riTv2mujAACWifWxxuvc0A0YuXND\nN2Dkzg3dgBE7N3QDRu7c0A0YuXNDN2BV1VCjfVXV1HABAGMwa27RwwUA0JnABQDQmcAFANCZwAUA\n0JnABQDQmcAFANCZwAUA0JnABQDQmcAFANCZwAUA0JnABQDQmcAFANCZwAUA0JnABQDQmcAFANCZ\nwAUA0JnABQDQmcAFANCZwAUA0JnABQDQmcAFANCZwAUA0JnABQDQmcAFANCZwAUA0JnABQDQmcAF\nANCZwAUA0JnABQDQmcAFANCZwAUA0JnABQDQmcAFANCZwAUA0JnABQDQmcAFANCZwAUA0JnABQDQ\nmcAFANCZwAUA0JnABQDQmcAFANCZwAUA0JnABQDQmcAFANCZwAUA0JnABQDQmcAFANCZwAUA0JnA\nBQDQmcAFANCZwAUA0JnABQDQmcAFANCZwAUA0JnABQDQmcAFANCZwAUA0JnABQDQmcAFANCZwAUA\n0JnABQDQmcAFANCZwAUA0JnABQDQmcAFANCZwAUA0JnABQDQmcAFANDZjgJXVX2gqs5X1ac37DtU\nVWer6nNVdaaqbt7w2INV9fmq+mxV3dWj4QAAY7HTHq4PJrl7074Hkpxtrb06ya9Mt1NVr0nyg0le\nMz3mfVWlJw0AWFk7CkKttV9P8pVNu9+U5PT0/ukkPzC9f0+SD7fWvt5aezLJ7yb5vtmbCgAwTrP0\nPN3SWjs/vX8+yS3T+y9L8tSG5z2V5OUzvA4AwKjNZaivtdaStKs9ZR6vAwAwRjfMcOz5qrq1tfZ0\nVb00yZen+7+Y5LYNz3vFdN8LVNVDGzbPtdbOzdAedqiqjiWHTky2Lp5srT02bIsAYLFU1dEkR+f2\n8yadUzt64Vcl+eXW2vdOt9+T5EJr7d1V9UCSm1trD0yL5j+USd3Wy5P8myR/um16oapqrbWa1/8I\nOzMJWzc9mjx8cLJn7XLyzU8mX3uH4AUAW5s1t+yoh6uqPpzkjiTfXlVfSPIPkvx0kkeq6u8keTLJ\nm5OktfZEVT2S5Ikk30jy1s1hiyEdOpGcOpjcd2XHdcn7b08+9ZGqlzyeXHdBrxcAzNeOAldr7Ye2\neegN2zz/XUnetddGsd+uT3LjgeRnbp9srx2pqnuFLgCYj1lquBiliyeTtSNJpkOKb0/y3Ul+Jht6\nvQ4mx08kEbgAYA4sSLpiJr1Wl+5N7v9Ecv/l5C1Jnh26WQCw1HZcND/3F1Y0P7j12YrPHk6uf23y\n8IHJI2vPJJcMKQLA1Ky5ReAiiaUiAObJ39TlI3CxLW94gP23xfI7Rg2WwL4sC8H4rL/hT115w5t5\nCLAvXrD8jolICFzLyxseABaFwAUAc7V5+Z21Z5JLJwdtEoNTw7WkJkOKN34k+TPTmYefejb5o3sM\nKQL0p4Z2+ajh4ipuSPJj0/trQzYEYKVMA5aQxXMEriXy/G9UNx9O3ntgQw3XATVcADAMgWtJvHBW\n4v2Xh20RAHCFwLU0Ns9K/PR1ydrlPHf5JkWbADAUgWtpfW+Sb34yOX5hsn1J0SYADMQsxSWx15WN\nJ8fd/K7kulcmz/6n5GvvmDxidg0AXOHSPjxnt9OQpyHtI+sXrX5bkj/4enLgsgtZA8A6y0LwnN1P\nQz50Ijm1cSZjkp/8luSnYoV6AJif64ZuAADAstPDtdIunkzW/kqSTUOKa5fX95ndCACzUsO1gjbV\nep1Lbv4fFM0DwPYUzbMre53NCACrbNbcooZrhKrqWNXhM5NbHdvd0YdOTMLWfZncHj643psFAPSg\nhmtkXngJn7UjVbWLHqrLh/u1DgDYisA1Opsv4bPzZRsmYe3G106K469Ye1ZRPAD0JXCtlCvrbt2a\n5B8n+VKSbz6ufgsA+hK4RufiyWTtSJKNRe+77KE6Nr2dzvq1FgGAXsxSHKHdXsLn+ceZoQgAu2VZ\nCHZlr2ENAFaZwAUA0Jl1uAAAFpzAtQJmWygVAJiVIcUlp1AeAGY3a26xLMTS2/tCqQDAfBhSBADo\nTA/X0pvHQqkAwCzUcK0Aa28BwGyswwUA0Jl1uAAAFpzABQDQmcAFANCZwAUA0JnABQDQmcAFANCZ\nwAUA0JnABQDQmcAFANCZwAUA0JnABQDQmcAFANCZwAUA0JnABQDQmcAFANCZwAUA0JnABQDQmcAF\nANCZwAUA0JnABQDQmcAFANCZwAUA0JnABQDQmcAFANCZwAUA0JnABQDQmcAFANCZwAUA0JnABQDQ\nmcAFANCZwAUA0JnABQDQmcAFANCZwAUA0NnMgauqHqyqx6vq01X1oao6UFWHqupsVX2uqs5U1c3z\naCwAzENVHas6fGZyq2NDt4flV621vR9c9aok/zbJ97TWnq2qf57kY0lem+T3Wmvvqaq3J3lJa+2B\nTce21lrt+cUBWGmToHToxGTr4snW2mM7P+6mR5OHD072rD2TXLp3p8ezmmbNLTfM+PqXknw9yY1V\n9c0kNyb5UpIHk9wxfc7pJOeSPLDVDwCA3VoPTaeuhKYjVbXD0HToxOS4+67sOJgcP5FE4KKbmYYU\nW2sXk5xM8p8zCVpfba2dTXJLa+389Gnnk9wyUysB4HkOnZj0UN2Xye3hg+u9XbB4Zurhqqo/leT+\nJK9K8vtJ/kVVvWXjc1prraq2HLesqoc2bJ5rrZ2bpT0AcG0XTyZrR5JsHFI8OWiTWDhVdTTJ0bn9\nvBlruH4wyZ2ttR+Zbv/NJN+f5L9P8t+11p6uqpcm+dXW2ndvOlYNFwB7Mmsd1l7rv1hds+aWWQPX\n65L8syT/bZI/TvJPk3w8ySuTXGitvbuqHkhys6J5AOZJaGI/DRq4pg34+5kMoF9O8okkP5LkTyZ5\n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\nMinr9tba9ya5PsnfiHN3NR/M5LNhoy3Pl8/cLW11/uaWWfb95FowdWbfl+R3W2tPtta+nuT/zOTc\nsYXW2tOttU9O7/9hks9kMpnjTUlOT592OskPDNPCxVdVr0jyxiQ/m+TKDB3nbwem34b/cmvtA0nS\nWvtGa+334/ztxKVMvjDdWFU3JLkxyZfi3G2rtfbrSb6yafd258tn7iZbnb95Zpah06wFU3fv5Um+\nsGHbedqh6TfmP5fJm+aW1tr56UPnk9wyULPG4H9N8veSXN6wz/nbme9M8l+q6oNV9Ymq+idV9a1x\n/q6ptXYxk5nw/zmToPXV1trZOHe7td358pm7ezNlli6Bazpe/Oktbn99w3MsmLo3zskeVNWLk/zL\nJD/eWvuDjY9NZ9E6r1uoqr+W5Muttd/Jeu/W8zh/V3VDktuTvK+1dnuSr2XTEJjzt7Wq+lNJ7k/y\nqkw+3F5cVW/Z+Bznbnd2cL6cy23MI7PMsizE9q/Y2p1Xe3y6YOobk7x+w+4vJrltw/Yrpvt4vs3n\n6bY8P2WzSVV9SyZh6+dba7803X2+qm5trT1dVS9N8uXhWrjQ/lKSN1XVG5P8iSQ3VdXPx/nbqaeS\nPNVa+63p9i9kUgPytPN3Tf9Nkv+ntXYhSarqF5P8xTh3u7Xde9Vn7g7NK7MMMUvx7kyGJ+65sjr9\n1EeT/I2qelFVfWeS70ry8f1u3wj8v0m+q6peVVUvyqRo76MDt2lhVVUl+bkkT7TW3rvhoY8muW96\n/74kv7T5WJLW2jtaa7e11r4zk4Llf9ta+5tx/naktfZ0ki9U1aunu96Q5PEkvxzn71o+m+T7q+rg\n9H38hkwmbjh3u7Pde9Vn7g7MM7Ps+zpcVfX5TBZMvTjd9RuttbdOH3tHJmOk38hk6OexfW3cSFTV\nX03y3kxm7fxca+0fDtykhVVVR5L8uySfynp374OZvDEeSfIdSZ5M8ubW2leHaONYVNUdSU601t5U\nVYfi/O1IVb0ukwkHL0ryH5P8cCbvXefvGqrq72cSEi4n+USSH0nyJ+PcbamqPpzkjiTfnkm91j9I\n8pFsc7585j7fFufvnZl8Xswls1j4FACgs6FnKQIALD2BCwCgM4ELAKAzgQsAoDOBCwCgM4ELAKAz\ngQsAoDOBCwCgs/8fICoqGcqtXKgAAAAASUVORK5CYII=\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "draw_boids(model)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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.4.2" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/examples/boid_flockers/Readme.md b/examples/boid_flockers/Readme.md new file mode 100644 index 00000000000..d1f4a987399 --- /dev/null +++ b/examples/boid_flockers/Readme.md @@ -0,0 +1,47 @@ +# Boids Flockers + +## Summary + +An implementation of Craig Reynolds's Boids flocker model. Agents (simulated birds) try to fly towards the average position of their neighbors and in the same direction as them, while maintaining a minimum distance. This produces flocking behavior. + +This model tests Mesa's continuous space feature, and uses numpy arrays to represent vectors. It also demonstrates how to create custom visualization components. + +## Installation + +To install the dependencies use pip and the requirements.txt in this directory. e.g. + +``` + $ pip install -r requirements.txt +``` + +## How to Run + +* To launch the visualization interactively, run ``mesa runserver`` in this directory. e.g. + +``` +$ mesa runserver +``` + +or + +Directly run the file ``run.py`` in the terminal. e.g. + +``` + $ python run.py +``` + +* Then open your browser to [http://127.0.0.1:8521/](http://127.0.0.1:8521/) and press Reset, then Run. + +## Files + +* [boid_flockers/model.py](boid_flockers/model.py): Core model file; contains the Boid Model and Boid Agent class. +* [boid_flockers/SimpleContinuousModule.py](boid_flockers/SimpleContinuousModule.py): Defines ``SimpleCanvas``, the Python side of a custom visualization module for drawing agents with continuous positions. +* [boid_flockers/simple_continuous_canvas.js](boid_flockers/simple_continuous_canvas.js): JavaScript side of the ``SimpleCanvas`` visualization module; takes the output generated by the Python ``SimpleCanvas`` element and draws it in the browser window via HTML5 canvas. +* [boid_flockers/server.py](boid_flockers/server.py): Sets up the visualization; uses the SimpleCanvas element defined above +* [run.py](run.py) Launches the visualization. +* [Flocker_Test.ipynb](Flocker_Test.ipynb): Tests the model in a Jupyter notebook. + +## Further Reading + +The following link can be visited for more information on the boid flockers model: +https://cs.stanford.edu/people/eroberts/courses/soco/projects/2008-09/modeling-natural-systems/boids.html diff --git a/examples/boid_flockers/app.py b/examples/boid_flockers/app.py new file mode 100644 index 00000000000..ade2deb2616 --- /dev/null +++ b/examples/boid_flockers/app.py @@ -0,0 +1,27 @@ +from boid_flockers.model import BoidFlockers + +from mesa.visualization import SolaraViz, make_space_matplotlib + + +def boid_draw(agent): + return {"color": "tab:red"} + + +model_params = { + "population": 100, + "width": 100, + "height": 100, + "speed": 5, + "vision": 10, + "separation": 2, +} + +model = BoidFlockers(100, 100, 100, 5, 10, 2) + +page = SolaraViz( + model, + [make_space_matplotlib(agent_portrayal=boid_draw)], + model_params=model_params, + name="BoidFlockers", +) +page # noqa diff --git a/examples/boid_flockers/boid_flockers/SimpleContinuousModule.py b/examples/boid_flockers/boid_flockers/SimpleContinuousModule.py new file mode 100644 index 00000000000..ef26ddf7b80 --- /dev/null +++ b/examples/boid_flockers/boid_flockers/SimpleContinuousModule.py @@ -0,0 +1,27 @@ +import mesa + + +class SimpleCanvas(mesa.visualization.VisualizationElement): + local_includes = ["boid_flockers/simple_continuous_canvas.js"] + + def __init__(self, portrayal_method=None, canvas_height=500, canvas_width=500): + """Instantiate a new SimpleCanvas""" + self.portrayal_method = portrayal_method + self.canvas_height = canvas_height + self.canvas_width = canvas_width + new_element = ( + f"new Simple_Continuous_Module({self.canvas_width}, {self.canvas_height})" + ) + self.js_code = "elements.push(" + new_element + ");" + + def render(self, model): + space_state = [] + for obj in model.agents: + portrayal = self.portrayal_method(obj) + x, y = obj.pos + x = (x - model.space.x_min) / (model.space.x_max - model.space.x_min) + y = (y - model.space.y_min) / (model.space.y_max - model.space.y_min) + portrayal["x"] = x + portrayal["y"] = y + space_state.append(portrayal) + return space_state diff --git a/examples/boid_flockers/boid_flockers/model.py b/examples/boid_flockers/boid_flockers/model.py new file mode 100644 index 00000000000..d293a6bdc5f --- /dev/null +++ b/examples/boid_flockers/boid_flockers/model.py @@ -0,0 +1,136 @@ +"""Flockers +============================================================= +A Mesa implementation of Craig Reynolds's Boids flocker model. +Uses numpy arrays to represent vectors. +""" + +import numpy as np + +import mesa + + +class Boid(mesa.Agent): + """A Boid-style flocker agent. + + The agent follows three behaviors to flock: + - Cohesion: steering towards neighboring agents. + - Separation: avoiding getting too close to any other agent. + - Alignment: try to fly in the same direction as the neighbors. + + Boids have a vision that defines the radius in which they look for their + neighbors to flock with. Their speed (a scalar) and direction (a vector) + define their movement. Separation is their desired minimum distance from + any other Boid. + """ + + def __init__( + self, + model, + speed, + direction, + vision, + separation, + cohere=0.03, + separate=0.015, + match=0.05, + ): + """Create a new Boid flocker agent. + + Args: + speed: Distance to move per step. + direction: numpy vector for the Boid's direction of movement. + vision: Radius to look around for nearby Boids. + separation: Minimum distance to maintain from other Boids. + cohere: the relative importance of matching neighbors' positions + separate: the relative importance of avoiding close neighbors + match: the relative importance of matching neighbors' headings + """ + super().__init__(model) + self.speed = speed + self.direction = direction + self.vision = vision + self.separation = separation + self.cohere_factor = cohere + self.separate_factor = separate + self.match_factor = match + self.neighbors = None + + def step(self): + """Get the Boid's neighbors, compute the new vector, and move accordingly.""" + self.neighbors = self.model.space.get_neighbors(self.pos, self.vision, False) + n = 0 + match_vector, separation_vector, cohere = np.zeros((3, 2)) + for neighbor in self.neighbors: + n += 1 + heading = self.model.space.get_heading(self.pos, neighbor.pos) + cohere += heading + if self.model.space.get_distance(self.pos, neighbor.pos) < self.separation: + separation_vector -= heading + match_vector += neighbor.direction + n = max(n, 1) + cohere = cohere * self.cohere_factor + separation_vector = separation_vector * self.separate_factor + match_vector = match_vector * self.match_factor + self.direction += (cohere + separation_vector + match_vector) / n + self.direction /= np.linalg.norm(self.direction) + new_pos = self.pos + self.direction * self.speed + self.model.space.move_agent(self, new_pos) + + +class BoidFlockers(mesa.Model): + """Flocker model class. Handles agent creation, placement and scheduling.""" + + def __init__( + self, + seed=None, + population=100, + width=100, + height=100, + vision=10, + speed=1, + separation=1, + cohere=0.03, + separate=0.015, + match=0.05, + ): + """Create a new Flockers model. + + Args: + population: Number of Boids + width, height: Size of the space. + speed: How fast should the Boids move. + vision: How far around should each Boid look for its neighbors + separation: What's the minimum distance each Boid will attempt to + keep from any other + cohere, separate, match: factors for the relative importance of + the three drives. + """ + super().__init__(seed=seed) + self.population = population + self.vision = vision + self.speed = speed + self.separation = separation + + self.space = mesa.space.ContinuousSpace(width, height, True) + self.factors = {"cohere": cohere, "separate": separate, "match": match} + self.make_agents() + + def make_agents(self): + """Create self.population agents, with random positions and starting headings.""" + for _ in range(self.population): + x = self.random.random() * self.space.x_max + y = self.random.random() * self.space.y_max + pos = np.array((x, y)) + direction = np.random.random(2) * 2 - 1 + boid = Boid( + model=self, + speed=self.speed, + direction=direction, + vision=self.vision, + separation=self.separation, + **self.factors, + ) + self.space.place_agent(boid, pos) + + def step(self): + self.agents.shuffle_do("step") diff --git a/examples/boid_flockers/boid_flockers/server.py b/examples/boid_flockers/boid_flockers/server.py new file mode 100644 index 00000000000..190c6533abb --- /dev/null +++ b/examples/boid_flockers/boid_flockers/server.py @@ -0,0 +1,64 @@ +import mesa + +from .model import BoidFlockers +from .SimpleContinuousModule import SimpleCanvas + + +def boid_draw(agent): + if not agent.neighbors: # Only for the first Frame + neighbors = len(agent.model.space.get_neighbors(agent.pos, agent.vision, False)) + else: + neighbors = len(agent.neighbors) + + if neighbors <= 1: + return {"Shape": "circle", "r": 2, "Filled": "true", "Color": "Red"} + elif neighbors >= 2: + return {"Shape": "circle", "r": 2, "Filled": "true", "Color": "Green"} + + +boid_canvas = SimpleCanvas( + portrayal_method=boid_draw, canvas_height=500, canvas_width=500 +) +model_params = { + "population": mesa.visualization.Slider( + name="Number of boids", + value=100, + min_value=10, + max_value=200, + step=10, + description="Choose how many agents to include in the model", + ), + "width": 100, + "height": 100, + "speed": mesa.visualization.Slider( + name="Speed of Boids", + value=5, + min_value=1, + max_value=20, + step=1, + description="How fast should the Boids move", + ), + "vision": mesa.visualization.Slider( + name="Vision of Bird (radius)", + value=10, + min_value=1, + max_value=50, + step=1, + description="How far around should each Boid look for its neighbors", + ), + "separation": mesa.visualization.Slider( + name="Minimum Separation", + value=2, + min_value=1, + max_value=20, + step=1, + description="What is the minimum distance each Boid will attempt to keep from any other", + ), +} + +server = mesa.visualization.ModularServer( + model_cls=BoidFlockers, + visualization_elements=[boid_canvas], + name="Boid Flocking Model", + model_params=model_params, +) diff --git a/examples/boid_flockers/boid_flockers/simple_continuous_canvas.js b/examples/boid_flockers/boid_flockers/simple_continuous_canvas.js new file mode 100644 index 00000000000..812cadced8b --- /dev/null +++ b/examples/boid_flockers/boid_flockers/simple_continuous_canvas.js @@ -0,0 +1,78 @@ +const ContinuousVisualization = function(width, height, context) { + this.draw = function(objects) { + for (const p of objects) { + if (p.Shape == "rect") + this.drawRectange(p.x, p.y, p.w, p.h, p.Color, p.Filled); + if (p.Shape == "circle") + this.drawCircle(p.x, p.y, p.r, p.Color, p.Filled); + }; + }; + + this.drawCircle = function(x, y, radius, color, fill) { + const cx = x * width; + const cy = y * height; + const r = radius; + + context.beginPath(); + context.arc(cx, cy, r, 0, Math.PI * 2, false); + context.closePath(); + + context.strokeStyle = color; + context.stroke(); + + if (fill) { + context.fillStyle = color; + context.fill(); + } + + }; + + this.drawRectange = function(x, y, w, h, color, fill) { + context.beginPath(); + const dx = w * width; + const dy = h * height; + + // Keep the drawing centered: + const x0 = (x*width) - 0.5*dx; + const y0 = (y*height) - 0.5*dy; + + context.strokeStyle = color; + context.fillStyle = color; + if (fill) + context.fillRect(x0, y0, dx, dy); + else + context.strokeRect(x0, y0, dx, dy); + }; + + this.resetCanvas = function() { + context.clearRect(0, 0, width, height); + context.beginPath(); + }; +}; + +const Simple_Continuous_Module = function(canvas_width, canvas_height) { + // Create the element + // ------------------ + + const canvas = document.createElement("canvas"); + Object.assign(canvas, { + width: canvas_width, + height: canvas_height, + style: 'border:1px dotted' + }); + // Append it to body: + document.getElementById("elements").appendChild(canvas); + + // Create the context and the drawing controller: + const context = canvas.getContext("2d"); + const canvasDraw = new ContinuousVisualization(canvas_width, canvas_height, context); + + this.render = function(data) { + canvasDraw.resetCanvas(); + canvasDraw.draw(data); + }; + + this.reset = function() { + canvasDraw.resetCanvas(); + }; +}; diff --git a/examples/boid_flockers/requirements.txt b/examples/boid_flockers/requirements.txt new file mode 100644 index 00000000000..da2b9972efd --- /dev/null +++ b/examples/boid_flockers/requirements.txt @@ -0,0 +1,3 @@ +jupyter +matplotlib +mesa~=2.0 diff --git a/examples/boid_flockers/run.py b/examples/boid_flockers/run.py new file mode 100644 index 00000000000..0d9ca624248 --- /dev/null +++ b/examples/boid_flockers/run.py @@ -0,0 +1,3 @@ +from boid_flockers.server import server + +server.launch(open_browser=True) diff --git a/examples/boltzmann_wealth_model/Readme.md b/examples/boltzmann_wealth_model/Readme.md new file mode 100644 index 00000000000..8f7f7c817c2 --- /dev/null +++ b/examples/boltzmann_wealth_model/Readme.md @@ -0,0 +1,60 @@ +# Boltzmann Wealth Model (Tutorial) + +## Summary + +A simple model of agents exchanging wealth. All agents start with the same amount of money. Every step, each agent with one unit of money or more gives one unit of wealth to another random agent. This is the model described in the [Intro Tutorial](https://mesa.readthedocs.io/en/latest/tutorials/intro_tutorial.html), with the completed code. + +If you want to go over the step-by-step tutorial, please go and run the [Jupyter Notebook](https://github.com/projectmesa/mesa/blob/main/docs/tutorials/intro_tutorial.ipynb). The code here runs the finalized code in the last cells directly. + +As the model runs, the distribution of wealth among agents goes from being perfectly uniform (all agents have the same starting wealth), to highly skewed -- a small number have high wealth, more have none at all. + +## How to Run + +To follow the tutorial example, launch the Jupyter Notebook and run the code in ``Introduction to Mesa Tutorial Code.ipynb`` which you can find in the main mesa repo [here](https://github.com/projectmesa/mesa/blob/main/docs/tutorials/intro_tutorial.ipynb) + +Make sure to install the requirements first: + +``` + $ pip install -r requirements.txt +``` + +To launch the interactive server, as described in the [last section of the tutorial](https://mesa.readthedocs.io/en/latest/tutorials/intro_tutorial.html#adding-visualization), run: + +``` + $ solara run app.py +``` + +If your browser doesn't open automatically, point it to [http://127.0.0.1:8765/](http://127.0.0.1:8765/). When the visualization loads, click on the Play button. + + +## Files + +* ``model.py``: Final version of the model. +* ``app.py``: Code for the interactive visualization. + +## Optional + +An optional visualization is also provided using Streamlit, which is another popular Python library for creating interactive web applications. + +To run the Streamlit app, you will need to install the `streamlit` and `altair` libraries: + +``` + $ pip install streamlit altair +``` + +Then, you can run the Streamlit app using the following command: + +``` + $ streamlit run st_app.py +``` + +## Further Reading + +The full tutorial describing how the model is built can be found at: +https://mesa.readthedocs.io/en/latest/tutorials/intro_tutorial.html + +This model is drawn from econophysics and presents a statistical mechanics approach to wealth distribution. Some examples of further reading on the topic can be found at: + +[Milakovic, M. A Statistical Equilibrium Model of Wealth Distribution. February, 2001.](https://editorialexpress.com/cgi-bin/conference/download.cgi?db_name=SCE2001&paper_id=214) + +[Dragulescu, A and Yakovenko, V. Statistical Mechanics of Money, Income, and Wealth: A Short Survey. November, 2002](http://arxiv.org/pdf/cond-mat/0211175v1.pdf) diff --git a/examples/boltzmann_wealth_model/app.py b/examples/boltzmann_wealth_model/app.py new file mode 100644 index 00000000000..0c38c742500 --- /dev/null +++ b/examples/boltzmann_wealth_model/app.py @@ -0,0 +1,66 @@ +from model import BoltzmannWealthModel + +from mesa.visualization import ( + SolaraViz, + make_plot_measure, + make_space_matplotlib, +) + + +def agent_portrayal(agent): + size = 10 + color = "tab:red" + if agent.wealth > 0: + size = 50 + color = "tab:blue" + return {"size": size, "color": color} + + +model_params = { + "N": { + "type": "SliderInt", + "value": 50, + "label": "Number of agents:", + "min": 10, + "max": 100, + "step": 1, + }, + "width": 10, + "height": 10, +} + +# Create initial model instance +model1 = BoltzmannWealthModel(50, 10, 10) + +# Create visualization elements. The visualization elements are solara components +# that receive the model instance as a "prop" and display it in a certain way. +# Under the hood these are just classes that receive the model instance. +# You can also author your own visualization elements, which can also be functions +# that receive the model instance and return a valid solara component. +SpaceGraph = make_space_matplotlib(agent_portrayal) +GiniPlot = make_plot_measure("Gini") + +# Create the SolaraViz page. This will automatically create a server and display the +# visualization elements in a web browser. +# Display it using the following command in the example directory: +# solara run app.py +# It will automatically update and display any changes made to this file +page = SolaraViz( + model1, + components=[SpaceGraph, GiniPlot], + model_params=model_params, + name="Boltzmann Wealth Model", +) +page # noqa + + +# In a notebook environment, we can also display the visualization elements directly +# SpaceGraph(model1) +# GiniPlot(model1) + +# The plots will be static. If you want to pick up model steps, +# you have to make the model reactive first +# reactive_model = solara.reactive(model1) +# SpaceGraph(reactive_model) +# In a different notebook block: +# reactive_model.value.step() diff --git a/examples/boltzmann_wealth_model/model.py b/examples/boltzmann_wealth_model/model.py new file mode 100644 index 00000000000..ac091a6ce34 --- /dev/null +++ b/examples/boltzmann_wealth_model/model.py @@ -0,0 +1,77 @@ +import mesa + + +def compute_gini(model): + agent_wealths = [agent.wealth for agent in model.agents] + x = sorted(agent_wealths) + N = model.num_agents + B = sum(xi * (N - i) for i, xi in enumerate(x)) / (N * sum(x)) + return 1 + (1 / N) - 2 * B + + +class BoltzmannWealthModel(mesa.Model): + """A simple model of an economy where agents exchange currency at random. + + All the agents begin with one unit of currency, and each time step can give + a unit of currency to another agent. Note how, over time, this produces a + highly skewed distribution of wealth. + """ + + def __init__(self, N=100, width=10, height=10): + super().__init__() + self.num_agents = N + self.grid = mesa.space.MultiGrid(width, height, True) + + self.datacollector = mesa.DataCollector( + model_reporters={"Gini": compute_gini}, agent_reporters={"Wealth": "wealth"} + ) + # Create agents + for _ in range(self.num_agents): + a = MoneyAgent(self) + + # Add the agent to a random grid cell + x = self.random.randrange(self.grid.width) + y = self.random.randrange(self.grid.height) + self.grid.place_agent(a, (x, y)) + + self.running = True + self.datacollector.collect(self) + + def step(self): + self.agents.shuffle_do("step") + # collect data + self.datacollector.collect(self) + + def run_model(self, n): + for i in range(n): + self.step() + + +class MoneyAgent(mesa.Agent): + """An agent with fixed initial wealth.""" + + def __init__(self, model): + super().__init__(model) + self.wealth = 1 + + def move(self): + possible_steps = self.model.grid.get_neighborhood( + self.pos, moore=True, include_center=False + ) + new_position = self.random.choice(possible_steps) + self.model.grid.move_agent(self, new_position) + + def give_money(self): + cellmates = self.model.grid.get_cell_list_contents([self.pos]) + cellmates.pop( + cellmates.index(self) + ) # Ensure agent is not giving money to itself + if len(cellmates) > 0: + other = self.random.choice(cellmates) + other.wealth += 1 + self.wealth -= 1 + + def step(self): + self.move() + if self.wealth > 0: + self.give_money() diff --git a/examples/boltzmann_wealth_model/requirements.txt b/examples/boltzmann_wealth_model/requirements.txt new file mode 100644 index 00000000000..95044bedf78 --- /dev/null +++ b/examples/boltzmann_wealth_model/requirements.txt @@ -0,0 +1 @@ +mesa[viz]>=3.0.0b0 diff --git a/examples/boltzmann_wealth_model/st_app.py b/examples/boltzmann_wealth_model/st_app.py new file mode 100644 index 00000000000..665f8067a6a --- /dev/null +++ b/examples/boltzmann_wealth_model/st_app.py @@ -0,0 +1,113 @@ +import time + +import altair as alt +import pandas as pd +import streamlit as st +from model import BoltzmannWealthModel + +model = st.title("Boltzman Wealth Model") +num_agents = st.slider( + "Choose how many agents to include in the model", + min_value=1, + max_value=100, + value=50, +) +num_ticks = st.slider( + "Select number of Simulation Runs", min_value=1, max_value=100, value=50 +) +height = st.slider("Select Grid Height", min_value=10, max_value=100, step=10, value=15) +width = st.slider("Select Grid Width", min_value=10, max_value=100, step=10, value=20) +model = BoltzmannWealthModel(num_agents, height, width) + + +status_text = st.empty() +run = st.button("Run Simulation") + + +if run: + tick = time.time() + step = 0 + # init grid + df_grid = pd.DataFrame() + df_gini = pd.DataFrame({"step": [0], "gini": [-1]}) + for x in range(width): + for y in range(height): + df_grid = pd.concat( + [df_grid, pd.DataFrame({"x": [x], "y": [y], "agent_count": 0})], + ignore_index=True, + ) + + heatmap = ( + alt.Chart(df_grid) + .mark_point(size=100) + .encode(x="x", y="y", color=alt.Color("agent_count")) + .interactive() + .properties(width=800, height=600) + ) + + line = ( + alt.Chart(df_gini) + .mark_line(point=True) + .encode(x="step", y="gini") + .properties(width=800, height=600) + ) + + # init progress bar + my_bar = st.progress(0, text="Simulation Progress") # progress + placeholder = st.empty() + st.subheader("Agent Grid") + chart = st.altair_chart(heatmap) + st.subheader("Gini Values") + line_chart = st.altair_chart(line) + + color_scale = alt.Scale( + domain=[0, 1, 2, 3, 4], range=["red", "cyan", "white", "white", "blue"] + ) + for i in range(num_ticks): + model.step() + my_bar.progress((i / num_ticks), text="Simulation progress") + placeholder.text("Step = %d" % i) + for cell in model.grid.coord_iter(): + cell_content, (x, y) = cell + agent_count = len(cell_content) + selected_row = df_grid[(df_grid["x"] == x) & (df_grid["y"] == y)] + df_grid.loc[selected_row.index, "agent_count"] = ( + agent_count # random.choice([1,2]) + ) + + df_gini = pd.concat( + [ + df_gini, + pd.DataFrame( + {"step": [i], "gini": [model.datacollector.model_vars["Gini"][i]]} + ), + ] + ) + # st.table(df_grid) + heatmap = ( + alt.Chart(df_grid) + .mark_circle(size=100) + .encode(x="x", y="y", color=alt.Color("agent_count", scale=color_scale)) + .interactive() + .properties(width=800, height=600) + ) + chart.altair_chart(heatmap) + + line = ( + alt.Chart(df_gini) + .mark_line(point=True) + .encode(x="step", y="gini") + .properties(width=800, height=600) + ) + line_chart.altair_chart(line) + + time.sleep(0.01) + + tock = time.time() + st.success(f"Simulation completed in {tock - tick:.2f} secs") + + # st.subheader('Agent Grid') + # fig = px.imshow(agent_counts,labels={'color':'Agent Count'}) + # st.plotly_chart(fig) + # st.subheader('Gini value over sim ticks (Plotly)') + # chart = st.line_chart(model.datacollector.model_vars['Gini']) diff --git a/examples/conways_game_of_life/Readme.md b/examples/conways_game_of_life/Readme.md new file mode 100644 index 00000000000..85b591aa713 --- /dev/null +++ b/examples/conways_game_of_life/Readme.md @@ -0,0 +1,37 @@ +# Conway's Game Of "Life" + +## Summary + +[The Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life), also known simply as "Life", is a cellular automaton devised by the British mathematician John Horton Conway in 1970. + +The "game" is a zero-player game, meaning that its evolution is determined by its initial state, requiring no further input by a human. One interacts with the Game of "Life" by creating an initial configuration and observing how it evolves, or, for advanced "players", by creating patterns with particular properties. + + +## How to Run + +To run the model interactively, run ``mesa runserver`` in this directory. e.g. + +``` + $ mesa runserver +``` + +Then open your browser to [http://127.0.0.1:8521/](http://127.0.0.1:8521/) and press ``run``. + +## Files + +* ``conways_game_of_life/cell.py``: Defines the behavior of an individual cell, which can be in two states: DEAD or ALIVE. +* ``conways_game_of_life/model.py``: Defines the model itself, initialized with a random configuration of alive and dead cells. +* ``conways_game_of_life/portrayal.py``: Describes for the front end how to render a cell. +* ``conways_game_of_life/server.py``: Defines an interactive visualization. +* ``run.py``: Launches the visualization + +## Optional + +* ``conways_game_of_life/app.py``: can be used to run the simulation via the streamlit interface. +* For this some additional packages like ``streamlit`` and ``altair`` needs to be installed. +* Once installed, the app can be opened in the browser using : ``streamlit run app.py`` + + +## Further Reading +[Conway's Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life) + diff --git a/examples/conways_game_of_life/app.py b/examples/conways_game_of_life/app.py new file mode 100644 index 00000000000..5be8327a35b --- /dev/null +++ b/examples/conways_game_of_life/app.py @@ -0,0 +1,71 @@ +import time + +import altair as alt +import numpy as np +import pandas as pd +import streamlit as st +from conways_game_of_life.model import ConwaysGameOfLife + +model = st.title("Boltzman Wealth Model") +num_ticks = st.slider("Select number of Steps", min_value=1, max_value=100, value=50) +height = st.slider("Select Grid Height", min_value=10, max_value=100, step=10, value=15) +width = st.slider("Select Grid Width", min_value=10, max_value=100, step=10, value=20) +model = ConwaysGameOfLife(height, width) + +col1, col2, col3 = st.columns(3) +status_text = st.empty() +# step_mode = st.checkbox('Run Step-by-Step') +run = st.button("Run Simulation") + + +if run: + tick = time.time() + step = 0 + # init grid + df_grid = pd.DataFrame() + agent_counts = np.zeros((model.grid.width, model.grid.height)) + for x in range(width): + for y in range(height): + df_grid = pd.concat( + [df_grid, pd.DataFrame({"x": [x], "y": [y], "state": [0]})], + ignore_index=True, + ) + + heatmap = ( + alt.Chart(df_grid) + .mark_point(size=100) + .encode(x="x", y="y", color=alt.Color("state")) + .interactive() + .properties(width=800, height=600) + ) + + # init progress bar + my_bar = st.progress(0, text="Simulation Progress") # progress + placeholder = st.empty() + st.subheader("Agent Grid") + chart = st.altair_chart(heatmap, use_container_width=True) + color_scale = alt.Scale(domain=[0, 1], range=["red", "yellow"]) + for i in range(num_ticks): + model.step() + my_bar.progress((i / num_ticks), text="Simulation progress") + placeholder.text("Step = %d" % i) + for contents, (x, y) in model.grid.coord_iter(): + # print('x:',x,'y:',y, 'state:',contents) + selected_row = df_grid[(df_grid["x"] == x) & (df_grid["y"] == y)] + df_grid.loc[selected_row.index, "state"] = ( + contents.state + ) # random.choice([1,2]) + + heatmap = ( + alt.Chart(df_grid) + .mark_circle(size=100) + .encode(x="x", y="y", color=alt.Color("state", scale=color_scale)) + .interactive() + .properties(width=800, height=600) + ) + chart.altair_chart(heatmap) + + time.sleep(0.1) + + tock = time.time() + st.success(f"Simulation completed in {tock - tick:.2f} secs") diff --git a/examples/conways_game_of_life/conways_game_of_life/cell.py b/examples/conways_game_of_life/conways_game_of_life/cell.py new file mode 100644 index 00000000000..9d286d925cd --- /dev/null +++ b/examples/conways_game_of_life/conways_game_of_life/cell.py @@ -0,0 +1,47 @@ +import mesa + + +class Cell(mesa.Agent): + """Represents a single ALIVE or DEAD cell in the simulation.""" + + DEAD = 0 + ALIVE = 1 + + def __init__(self, pos, model, init_state=DEAD): + """Create a cell, in the given state, at the given x, y position.""" + super().__init__(model) + self.x, self.y = pos + self.state = init_state + self._nextState = None + + @property + def isAlive(self): + return self.state == self.ALIVE + + @property + def neighbors(self): + return self.model.grid.iter_neighbors((self.x, self.y), True) + + def determine_state(self): + """Compute if the cell will be dead or alive at the next tick. This is + based on the number of alive or dead neighbors. The state is not + changed here, but is just computed and stored in self._nextState, + because our current state may still be necessary for our neighbors + to calculate their next state. + """ + # Get the neighbors and apply the rules on whether to be alive or dead + # at the next tick. + live_neighbors = sum(neighbor.isAlive for neighbor in self.neighbors) + + # Assume nextState is unchanged, unless changed below. + self._nextState = self.state + if self.isAlive: + if live_neighbors < 2 or live_neighbors > 3: + self._nextState = self.DEAD + else: + if live_neighbors == 3: + self._nextState = self.ALIVE + + def assume_state(self): + """Set the state to the new computed state -- computed in step().""" + self.state = self._nextState diff --git a/examples/conways_game_of_life/conways_game_of_life/model.py b/examples/conways_game_of_life/conways_game_of_life/model.py new file mode 100644 index 00000000000..d00261c90c7 --- /dev/null +++ b/examples/conways_game_of_life/conways_game_of_life/model.py @@ -0,0 +1,33 @@ +import mesa + +from .cell import Cell + + +class ConwaysGameOfLife(mesa.Model): + """Represents the 2-dimensional array of cells in Conway's + Game of Life. + """ + + def __init__(self, width=50, height=50): + """Create a new playing area of (width, height) cells.""" + super().__init__() + # Use a simple grid, where edges wrap around. + self.grid = mesa.space.SingleGrid(width, height, torus=True) + + # Place a cell at each location, with some initialized to + # ALIVE and some to DEAD. + for contents, (x, y) in self.grid.coord_iter(): + cell = Cell((x, y), self) + if self.random.random() < 0.1: + cell.state = cell.ALIVE + self.grid.place_agent(cell, (x, y)) + + self.running = True + + def step(self): + """Perform the model step in two stages: + - First, all cells assume their next state (whether they will be dead or alive) + - Then, all cells change state to their next state + """ + self.agents.do("determine_state") + self.agents.do("assume_state") diff --git a/examples/conways_game_of_life/conways_game_of_life/portrayal.py b/examples/conways_game_of_life/conways_game_of_life/portrayal.py new file mode 100644 index 00000000000..1c8d6d0fadd --- /dev/null +++ b/examples/conways_game_of_life/conways_game_of_life/portrayal.py @@ -0,0 +1,18 @@ +def portrayCell(cell): + """This function is registered with the visualization server to be called + each tick to indicate how to draw the cell in its current state. + :param cell: the cell in the simulation + :return: the portrayal dictionary. + """ + if cell is None: + raise AssertionError + return { + "Shape": "rect", + "w": 1, + "h": 1, + "Filled": "true", + "Layer": 0, + "x": cell.x, + "y": cell.y, + "Color": "black" if cell.isAlive else "white", + } diff --git a/examples/conways_game_of_life/conways_game_of_life/server.py b/examples/conways_game_of_life/conways_game_of_life/server.py new file mode 100644 index 00000000000..6da932f3ee6 --- /dev/null +++ b/examples/conways_game_of_life/conways_game_of_life/server.py @@ -0,0 +1,11 @@ +import mesa + +from .model import ConwaysGameOfLife +from .portrayal import portrayCell + +# Make a world that is 50x50, on a 250x250 display. +canvas_element = mesa.visualization.CanvasGrid(portrayCell, 50, 50, 250, 250) + +server = mesa.visualization.ModularServer( + ConwaysGameOfLife, [canvas_element], "Game of Life", {"height": 50, "width": 50} +) diff --git a/examples/conways_game_of_life/requirements.txt b/examples/conways_game_of_life/requirements.txt new file mode 100644 index 00000000000..ecd07eafe6f --- /dev/null +++ b/examples/conways_game_of_life/requirements.txt @@ -0,0 +1 @@ +mesa~=2.0 \ No newline at end of file diff --git a/examples/conways_game_of_life/run.py b/examples/conways_game_of_life/run.py new file mode 100644 index 00000000000..7095816577c --- /dev/null +++ b/examples/conways_game_of_life/run.py @@ -0,0 +1,3 @@ +from conways_game_of_life.server import server + +server.launch(open_browser=True) diff --git a/examples/wolf_sheep/Readme.md b/examples/wolf_sheep/Readme.md new file mode 100644 index 00000000000..30794a6ee67 --- /dev/null +++ b/examples/wolf_sheep/Readme.md @@ -0,0 +1,57 @@ +# Wolf-Sheep Predation Model + +## Summary + +A simple ecological model, consisting of three agent types: wolves, sheep, and grass. The wolves and the sheep wander around the grid at random. Wolves and sheep both expend energy moving around, and replenish it by eating. Sheep eat grass, and wolves eat sheep if they end up on the same grid cell. + +If wolves and sheep have enough energy, they reproduce, creating a new wolf or sheep (in this simplified model, only one parent is needed for reproduction). The grass on each cell regrows at a constant rate. If any wolves and sheep run out of energy, they die. + +The model is tests and demonstrates several Mesa concepts and features: + - MultiGrid + - Multiple agent types (wolves, sheep, grass) + - Overlay arbitrary text (wolf's energy) on agent's shapes while drawing on CanvasGrid + - Agents inheriting a behavior (random movement) from an abstract parent + - Writing a model composed of multiple files. + - Dynamically adding and removing agents from the schedule + +## Installation + +To install the dependencies use pip and the requirements.txt in this directory. e.g. + +``` + # First, we clone the Mesa repo + $ git clone https://github.com/projectmesa/mesa.git + $ cd mesa + # Then we cd to the example directory + $ cd examples/wolf_sheep + $ pip install -r requirements.txt +``` + +## How to Run + +To run the model interactively, run ``mesa runserver`` in this directory. e.g. + +``` + $ mesa runserver +``` + +Then open your browser to [http://127.0.0.1:8521/](http://127.0.0.1:8521/) and press Reset, then Run. + +## Files + +* ``wolf_sheep/random_walk.py``: This defines the ``RandomWalker`` agent, which implements the behavior of moving randomly across a grid, one cell at a time. Both the Wolf and Sheep agents will inherit from it. +* ``wolf_sheep/test_random_walk.py``: Defines a simple model and a text-only visualization intended to make sure the RandomWalk class was working as expected. This doesn't actually model anything, but serves as an ad-hoc unit test. To run it, ``cd`` into the ``wolf_sheep`` directory and run ``python test_random_walk.py``. You'll see a series of ASCII grids, one per model step, with each cell showing a count of the number of agents in it. +* ``wolf_sheep/agents.py``: Defines the Wolf, Sheep, and GrassPatch agent classes. +* ``wolf_sheep/scheduler.py``: Defines a custom variant on the RandomActivationByType scheduler, where we can define filters for the `get_type_count` function. +* ``wolf_sheep/model.py``: Defines the Wolf-Sheep Predation model itself +* ``wolf_sheep/server.py``: Sets up the interactive visualization server +* ``run.py``: Launches a model visualization server. + +## Further Reading + +This model is closely based on the NetLogo Wolf-Sheep Predation Model: + +Wilensky, U. (1997). NetLogo Wolf Sheep Predation model. http://ccl.northwestern.edu/netlogo/models/WolfSheepPredation. Center for Connected Learning and Computer-Based Modeling, Northwestern University, Evanston, IL. + +See also the [Lotka–Volterra equations +](https://en.wikipedia.org/wiki/Lotka%E2%80%93Volterra_equations) for an example of a classic differential-equation model with similar dynamics. diff --git a/examples/wolf_sheep/__init__.py b/examples/wolf_sheep/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/wolf_sheep/requirements.txt b/examples/wolf_sheep/requirements.txt new file mode 100644 index 00000000000..25d263f4e84 --- /dev/null +++ b/examples/wolf_sheep/requirements.txt @@ -0,0 +1 @@ +mesa~=2.0 diff --git a/examples/wolf_sheep/run.py b/examples/wolf_sheep/run.py new file mode 100644 index 00000000000..89e3b5488df --- /dev/null +++ b/examples/wolf_sheep/run.py @@ -0,0 +1,3 @@ +from wolf_sheep.server import server + +server.launch(open_browser=True) diff --git a/examples/wolf_sheep/wolf_sheep/__init__.py b/examples/wolf_sheep/wolf_sheep/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/wolf_sheep/wolf_sheep/agents.py b/examples/wolf_sheep/wolf_sheep/agents.py new file mode 100644 index 00000000000..fd6a8561bf9 --- /dev/null +++ b/examples/wolf_sheep/wolf_sheep/agents.py @@ -0,0 +1,106 @@ +import mesa + +from .random_walk import RandomWalker + + +class Sheep(RandomWalker): + """A sheep that walks around, reproduces (asexually) and gets eaten. + + The init is the same as the RandomWalker. + """ + + energy = None + + def __init__(self, model, moore, energy=None): + super().__init__(model, moore=moore) + self.energy = energy + + def step(self): + """A model step. Move, then eat grass and reproduce.""" + self.random_move() + living = True + + if self.model.grass: + # Reduce energy + self.energy -= 1 + + # If there is grass available, eat it + this_cell = self.model.grid.get_cell_list_contents([self.pos]) + grass_patch = next(obj for obj in this_cell if isinstance(obj, GrassPatch)) + if grass_patch.fully_grown: + self.energy += self.model.sheep_gain_from_food + grass_patch.fully_grown = False + + # Death + if self.energy < 0: + self.model.grid.remove_agent(self) + self.remove() + living = False + + if living and self.random.random() < self.model.sheep_reproduce: + # Create a new sheep: + if self.model.grass: + self.energy /= 2 + lamb = Sheep(self.model, self.moore, self.energy) + self.model.grid.place_agent(lamb, self.pos) + + +class Wolf(RandomWalker): + """A wolf that walks around, reproduces (asexually) and eats sheep.""" + + energy = None + + def __init__(self, model, moore, energy=None): + super().__init__(model, moore=moore) + self.energy = energy + + def step(self): + self.random_move() + self.energy -= 1 + + # If there are sheep present, eat one + x, y = self.pos + this_cell = self.model.grid.get_cell_list_contents([self.pos]) + sheep = [obj for obj in this_cell if isinstance(obj, Sheep)] + if len(sheep) > 0: + sheep_to_eat = self.random.choice(sheep) + self.energy += self.model.wolf_gain_from_food + + # Kill the sheep + self.model.grid.remove_agent(sheep_to_eat) + sheep_to_eat.remove() + + # Death or reproduction + if self.energy < 0: + self.model.grid.remove_agent(self) + self.remove() + else: + if self.random.random() < self.model.wolf_reproduce: + # Create a new wolf cub + self.energy /= 2 + cub = Wolf(self.model, self.moore, self.energy) + self.model.grid.place_agent(cub, self.pos) + + +class GrassPatch(mesa.Agent): + """A patch of grass that grows at a fixed rate and it is eaten by sheep""" + + def __init__(self, model, fully_grown, countdown): + """Creates a new patch of grass + + Args: + grown: (boolean) Whether the patch of grass is fully grown or not + countdown: Time for the patch of grass to be fully grown again + """ + super().__init__(model) + self.fully_grown = fully_grown + self.countdown = countdown + + def step(self): + if not self.fully_grown: + if self.countdown <= 0: + # Set as fully grown + self.fully_grown = True + self.countdown = self.model.grass_regrowth_time + else: + self.countdown -= 1 diff --git a/examples/wolf_sheep/wolf_sheep/model.py b/examples/wolf_sheep/wolf_sheep/model.py new file mode 100644 index 00000000000..e97775c7746 --- /dev/null +++ b/examples/wolf_sheep/wolf_sheep/model.py @@ -0,0 +1,134 @@ +"""Wolf-Sheep Predation Model +================================ + +Replication of the model found in NetLogo: + Wilensky, U. (1997). NetLogo Wolf Sheep Predation model. + http://ccl.northwestern.edu/netlogo/models/WolfSheepPredation. + Center for Connected Learning and Computer-Based Modeling, + Northwestern University, Evanston, IL. +""" + +import mesa + +from .agents import GrassPatch, Sheep, Wolf + + +class WolfSheep(mesa.Model): + """Wolf-Sheep Predation Model""" + + height = 20 + width = 20 + + initial_sheep = 100 + initial_wolves = 50 + + sheep_reproduce = 0.04 + wolf_reproduce = 0.05 + + wolf_gain_from_food = 20 + + grass = False + grass_regrowth_time = 30 + sheep_gain_from_food = 4 + + description = ( + "A model for simulating wolf and sheep (predator-prey) ecosystem modelling." + ) + + def __init__( + self, + width=20, + height=20, + initial_sheep=100, + initial_wolves=50, + sheep_reproduce=0.04, + wolf_reproduce=0.05, + wolf_gain_from_food=20, + grass=False, + grass_regrowth_time=30, + sheep_gain_from_food=4, + ): + """Create a new Wolf-Sheep model with the given parameters. + + Args: + initial_sheep: Number of sheep to start with + initial_wolves: Number of wolves to start with + sheep_reproduce: Probability of each sheep reproducing each step + wolf_reproduce: Probability of each wolf reproducing each step + wolf_gain_from_food: Energy a wolf gains from eating a sheep + grass: Whether to have the sheep eat grass for energy + grass_regrowth_time: How long it takes for a grass patch to regrow + once it is eaten + sheep_gain_from_food: Energy sheep gain from grass, if enabled. + """ + super().__init__() + # Set parameters + self.width = width + self.height = height + self.initial_sheep = initial_sheep + self.initial_wolves = initial_wolves + self.sheep_reproduce = sheep_reproduce + self.wolf_reproduce = wolf_reproduce + self.wolf_gain_from_food = wolf_gain_from_food + self.grass = grass + self.grass_regrowth_time = grass_regrowth_time + self.sheep_gain_from_food = sheep_gain_from_food + + self.grid = mesa.space.MultiGrid(self.width, self.height, torus=True) + + collectors = { + "Wolves": lambda m: len(m.agents_by_type[Wolf]), + "Sheep": lambda m: len(m.agents_by_type[Sheep]), + } + + if grass: + collectors["Grass"] = lambda m: len(m.agents_by_type[GrassPatch]) + + self.datacollector = mesa.DataCollector(collectors) + + # Create sheep: + for i in range(self.initial_sheep): + x = self.random.randrange(self.width) + y = self.random.randrange(self.height) + energy = self.random.randrange(2 * self.sheep_gain_from_food) + sheep = Sheep(self, True, energy) + self.grid.place_agent(sheep, (x, y)) + + # Create wolves + for _ in range(self.initial_wolves): + x = self.random.randrange(self.width) + y = self.random.randrange(self.height) + energy = self.random.randrange(2 * self.wolf_gain_from_food) + wolf = Wolf(self, True, energy) + self.grid.place_agent(wolf, (x, y)) + + # Create grass patches + if self.grass: + for agent, (x, y) in self.grid.coord_iter(): + fully_grown = self.random.choice([True, False]) + + if fully_grown: + countdown = self.grass_regrowth_time + else: + countdown = self.random.randrange(self.grass_regrowth_time) + + patch = GrassPatch(self, fully_grown, countdown) + self.grid.place_agent(patch, (x, y)) + + self.running = True + self.datacollector.collect(self) + + def step(self): + # This replicated the behavior of the old RandomActivationByType scheduler + # when using step(shuffle_types=True, shuffle_agents=True). + # Conceptually, it can be argued that this should be modelled differently. + self.random.shuffle(self.agent_types) + for agent_type in self.agent_types: + self.agents_by_type[agent_type].do("step") + + # collect data + self.datacollector.collect(self) + + def run_model(self, step_count=200): + for i in range(step_count): + self.step() diff --git a/examples/wolf_sheep/wolf_sheep/random_walk.py b/examples/wolf_sheep/wolf_sheep/random_walk.py new file mode 100644 index 00000000000..51b0a72b8f2 --- /dev/null +++ b/examples/wolf_sheep/wolf_sheep/random_walk.py @@ -0,0 +1,34 @@ +"""Generalized behavior for random walking, one grid cell at a time.""" + +import mesa + + +class RandomWalker(mesa.Agent): + """Class implementing random walker methods in a generalized manner. + + Not intended to be used on its own, but to inherit its methods to multiple + other agents. + """ + + grid = None + x = None + y = None + moore = True + + def __init__(self, model, moore=True): + """grid: The MultiGrid object in which the agent lives. + x: The agent's current x coordinate + y: The agent's current y coordinate + moore: If True, may move in all 8 directions. + Otherwise, only up, down, left, right. + """ + super().__init__(model) + self.moore = moore + + def random_move(self): + """Step one cell in any allowable direction.""" + # Pick the next cell from the adjacent cells. + next_moves = self.model.grid.get_neighborhood(self.pos, self.moore, True) + next_move = self.random.choice(next_moves) + # Now move: + self.model.grid.move_agent(self, next_move) diff --git a/examples/wolf_sheep/wolf_sheep/resources/sheep.png b/examples/wolf_sheep/wolf_sheep/resources/sheep.png new file mode 100644 index 00000000000..dfb81b0e5d7 Binary files /dev/null and b/examples/wolf_sheep/wolf_sheep/resources/sheep.png differ diff --git a/examples/wolf_sheep/wolf_sheep/resources/wolf.png b/examples/wolf_sheep/wolf_sheep/resources/wolf.png new file mode 100644 index 00000000000..5357b855197 Binary files /dev/null and b/examples/wolf_sheep/wolf_sheep/resources/wolf.png differ diff --git a/examples/wolf_sheep/wolf_sheep/server.py b/examples/wolf_sheep/wolf_sheep/server.py new file mode 100644 index 00000000000..112c1a2dfda --- /dev/null +++ b/examples/wolf_sheep/wolf_sheep/server.py @@ -0,0 +1,78 @@ +import mesa +from wolf_sheep.agents import GrassPatch, Sheep, Wolf +from wolf_sheep.model import WolfSheep + + +def wolf_sheep_portrayal(agent): + if agent is None: + return + + portrayal = {} + + if type(agent) is Sheep: + portrayal["Shape"] = "wolf_sheep/resources/sheep.png" + # https://icons8.com/web-app/433/sheep + portrayal["scale"] = 0.9 + portrayal["Layer"] = 1 + + elif type(agent) is Wolf: + portrayal["Shape"] = "wolf_sheep/resources/wolf.png" + # https://icons8.com/web-app/36821/German-Shepherd + portrayal["scale"] = 0.9 + portrayal["Layer"] = 2 + portrayal["text"] = round(agent.energy, 1) + portrayal["text_color"] = "White" + + elif type(agent) is GrassPatch: + if agent.fully_grown: + portrayal["Color"] = ["#00FF00", "#00CC00", "#009900"] + else: + portrayal["Color"] = ["#84e184", "#adebad", "#d6f5d6"] + portrayal["Shape"] = "rect" + portrayal["Filled"] = "true" + portrayal["Layer"] = 0 + portrayal["w"] = 1 + portrayal["h"] = 1 + + return portrayal + + +canvas_element = mesa.visualization.CanvasGrid(wolf_sheep_portrayal, 20, 20, 500, 500) +chart_element = mesa.visualization.ChartModule( + [ + {"Label": "Wolves", "Color": "#AA0000"}, + {"Label": "Sheep", "Color": "#666666"}, + {"Label": "Grass", "Color": "#00AA00"}, + ] +) + +model_params = { + # The following line is an example to showcase StaticText. + "title": mesa.visualization.StaticText("Parameters:"), + "grass": mesa.visualization.Checkbox("Grass Enabled", True), + "grass_regrowth_time": mesa.visualization.Slider("Grass Regrowth Time", 20, 1, 50), + "initial_sheep": mesa.visualization.Slider( + "Initial Sheep Population", 100, 10, 300 + ), + "sheep_reproduce": mesa.visualization.Slider( + "Sheep Reproduction Rate", 0.04, 0.01, 1.0, 0.01 + ), + "initial_wolves": mesa.visualization.Slider("Initial Wolf Population", 50, 10, 300), + "wolf_reproduce": mesa.visualization.Slider( + "Wolf Reproduction Rate", + 0.05, + 0.01, + 1.0, + 0.01, + description="The rate at which wolf agents reproduce.", + ), + "wolf_gain_from_food": mesa.visualization.Slider( + "Wolf Gain From Food Rate", 20, 1, 50 + ), + "sheep_gain_from_food": mesa.visualization.Slider("Sheep Gain From Food", 4, 1, 10), +} + +server = mesa.visualization.ModularServer( + WolfSheep, [canvas_element, chart_element], "Wolf Sheep Predation", model_params +) +server.port = 8521 diff --git a/examples/wolf_sheep/wolf_sheep/test_random_walk.py b/examples/wolf_sheep/wolf_sheep/test_random_walk.py new file mode 100644 index 00000000000..14284f98300 --- /dev/null +++ b/examples/wolf_sheep/wolf_sheep/test_random_walk.py @@ -0,0 +1,70 @@ +"""Testing the RandomWalker by having an ABM composed only of random walker +agents. +""" + +from mesa import Model +from mesa.space import MultiGrid +from mesa.visualization.TextVisualization import TextGrid, TextVisualization +from wolf_sheep.random_walk import RandomWalker + + +class WalkerAgent(RandomWalker): + """Agent which only walks around.""" + + def step(self): + self.random_move() + + +class WalkerWorld(Model): + """Random walker world.""" + + height = 10 + width = 10 + + def __init__(self, width, height, agent_count): + """Create a new WalkerWorld. + + Args: + width, height: World size. + agent_count: How many agents to create. + """ + self.height = height + self.width = width + self.grid = MultiGrid(self.width, self.height, torus=True) + self.agent_count = agent_count + + # Create agents + for i in range(self.agent_count): + x = self.random.randrange(self.width) + y = self.random.randrange(self.height) + a = WalkerAgent(i, (x, y), self, True) + self.grid.place_agent(a, (x, y)) + + def step(self): + self.agents.shuffle_do("step") + + +class WalkerWorldViz(TextVisualization): + """ASCII Visualization for a WalkerWorld agent. + Each cell is displayed as the number of agents currently in that cell. + """ + + def __init__(self, model): + """Create a new visualization for a WalkerWorld instance. + + Args: + model: An instance of a WalkerWorld model. + """ + self.model = model + grid_viz = TextGrid(self.model.grid, None) + grid_viz.converter = lambda x: str(len(x)) + self.elements = [grid_viz] + + +if __name__ == "__main__": + print("Testing 10x10 world, with 50 random walkers, for 10 steps.") + model = WalkerWorld(10, 10, 50) + viz = WalkerWorldViz(model) + for i in range(10): + print("Step:", str(i)) + viz.step()