mirror of
https://github.com/gsi-upm/soil
synced 2024-11-23 03:32:28 +00:00
v1.0.0rc11
This commit is contained in:
parent
f49be3af68
commit
25d042f16c
@ -3,9 +3,9 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.0 UNRELEASED]
|
||||
## [1.0.0 UNRELEASED]
|
||||
|
||||
Version 1.0 introduced multiple changes, especially on the `Simulation` class and anything related to how configuration is handled.
|
||||
Version 1.0 will introduce multiple changes, especially on the `Simulation` class and anything related to how configuration is handled.
|
||||
For an explanation of the general changes in version 1.0, please refer to the file `docs/notes_v1.0.rst`.
|
||||
|
||||
### Added
|
||||
@ -19,7 +19,6 @@ For an explanation of the general changes in version 1.0, please refer to the fi
|
||||
* The `agent.after` and `agent.at` methods, to avoid having to return a time manually.
|
||||
### Changed
|
||||
* Configuration schema (`Simulation`) is very simplified. All simulations should be checked
|
||||
* Agents that wish to
|
||||
* Model / environment variables are expected (but not enforced) to be a single value. This is done to more closely align with mesa
|
||||
* `Exporter.iteration_end` now takes two parameters: `env` (same as before) and `params` (specific parameters for this environment). We considered including a `parameters` attribute in the environment, but this would not be compatible with mesa.
|
||||
* `num_trials` renamed to `iterations`
|
||||
|
@ -27,7 +27,7 @@ class VirusOnNetwork(Environment):
|
||||
|
||||
# Infect some nodes
|
||||
infected_nodes = self.random.sample(list(self.G), self.initial_outbreak_size)
|
||||
for a in self.agents(node_id=infected_nodes):
|
||||
for a in self.get_agents(node_id=infected_nodes):
|
||||
a.set_state(VirusAgent.infected)
|
||||
assert self.number_infected == self.initial_outbreak_size
|
||||
|
||||
|
@ -33,7 +33,7 @@ class VirusOnNetwork(Environment):
|
||||
|
||||
# Infect some nodes
|
||||
infected_nodes = self.random.sample(list(self.G), self.initial_outbreak_size)
|
||||
for a in self.agents(node_id=infected_nodes):
|
||||
for a in self.get_agents(node_id=infected_nodes):
|
||||
a.status = State.INFECTED
|
||||
assert self.number_infected == self.initial_outbreak_size
|
||||
|
||||
|
File diff suppressed because one or more lines are too long
@ -63,11 +63,11 @@ class City(EventedEnvironment):
|
||||
|
||||
def init(self):
|
||||
self.grid = MultiGrid(width=self.width, height=self.height, torus=False)
|
||||
if not self.agents:
|
||||
if not self.get_agents():
|
||||
self.add_agents(Driver, k=self.n_cars)
|
||||
self.add_agents(Passenger, k=self.n_passengers)
|
||||
|
||||
for agent in self.agents:
|
||||
for agent in self.get_agents():
|
||||
self.grid.place_agent(agent, (0, 0))
|
||||
self.grid.move_to_empty(agent)
|
||||
|
||||
|
355
examples/markov_chains/MarkovChains.ipynb
Normal file
355
examples/markov_chains/MarkovChains.ipynb
Normal file
@ -0,0 +1,355 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "7641396c-a602-477e-bf03-09e1191ff549",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%load_ext autoreload"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "4f12285c-78db-4ee8-b9c6-7799d34f10f5",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%autoreload 1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "7710bb03-0cb9-413a-a407-fe48855ff917",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%aimport markov_sim"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"id": "2dffca0f-da9e-4f69-ac43-7afe52ad2d32",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%aimport soil\n",
|
||||
"%aimport soil.visualization"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 11,
|
||||
"id": "12871006-70ca-4c6f-8a3e-0aae1d0bce31",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"G = markov_sim.load_city_graph(\"Chamberi, Madrid\", network_type=\"drive\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 12,
|
||||
"id": "31e96cc5-b703-4d2a-a006-7b9a2cedc365",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# env = markov_sim.CityEnv(G=G, n_assets=20, side=10, max_weight=1, seed=10)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 13,
|
||||
"id": "5e070b36-0ba6-4780-8fd4-3c72fa3bb240",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# for i in range(2):\n",
|
||||
"# env.step()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 35,
|
||||
"id": "56f8b997-65b0-431d-9517-b93edb1cfcd8",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"/home/j/.cache/pypoetry/virtualenvs/soil-cCX5yKRx-py3.10/lib/python3.10/site-packages/osmnx/plot.py:955: UserWarning: FigureCanvasAgg is non-interactive, and thus cannot be shown\n",
|
||||
" plt.show()\n",
|
||||
"/home/j/.cache/pypoetry/virtualenvs/soil-cCX5yKRx-py3.10/lib/python3.10/site-packages/osmnx/plot.py:955: UserWarning: FigureCanvasAgg is non-interactive, and thus cannot be shown\n",
|
||||
" plt.show()\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"application/vnd.jupyter.widget-view+json": {
|
||||
"model_id": "86e45bd44e434674b11805fd94e98414",
|
||||
"version_major": 2,
|
||||
"version_minor": 0
|
||||
},
|
||||
"text/html": [
|
||||
"Cannot show widget. You probably want to rerun the code cell above (<i>Click in the code cell, and press Shift+Enter <kbd>⇧</kbd>+<kbd>↩</kbd></i>)."
|
||||
],
|
||||
"text/plain": [
|
||||
"Cannot show ipywidgets in text"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"from soil.visualization import JupyterViz, GeoNetworkDrawer, Controller\n",
|
||||
"from soil import visualization\n",
|
||||
"from matplotlib import colors\n",
|
||||
"from matplotlib import colormaps\n",
|
||||
"plasma = colormaps.get_cmap('plasma')\n",
|
||||
"model_params = {\n",
|
||||
" \"n_assets\": {\n",
|
||||
" \"type\": \"SliderInt\",\n",
|
||||
" \"value\": 100,\n",
|
||||
" \"label\": \"Number of assets:\",\n",
|
||||
" \"min\": 1,\n",
|
||||
" \"max\": 1000,\n",
|
||||
" \"step\": 1,\n",
|
||||
" },\n",
|
||||
" \"max_weight\": {\n",
|
||||
" \"type\": \"SliderInt\",\n",
|
||||
" \"value\": 3,\n",
|
||||
" \"label\": \"Maximum edge weight:\",\n",
|
||||
" \"min\": 1,\n",
|
||||
" \"max\": 20,\n",
|
||||
" \"step\": 1,\n",
|
||||
" },\n",
|
||||
" \"ratio_lazy\": {\n",
|
||||
" \"type\": \"SliderFloat\",\n",
|
||||
" \"value\": 0,\n",
|
||||
" \"label\": \"Ratio of lazy agents (they prefer shorter streets):\",\n",
|
||||
" \"min\": 0,\n",
|
||||
" \"max\": 1,\n",
|
||||
" \"step\": 0.05,\n",
|
||||
" },\n",
|
||||
" \"side\": {\n",
|
||||
" \"type\": \"SliderInt\",\n",
|
||||
" \"value\": 10,\n",
|
||||
" \"label\": \"Size of the side:\",\n",
|
||||
" \"min\": 2,\n",
|
||||
" \"max\": 20,\n",
|
||||
" \"step\": 1,\n",
|
||||
" },\n",
|
||||
" \"gradual_move\": {\n",
|
||||
" \"type\": \"Checkbox\",\n",
|
||||
" \"value\": True,\n",
|
||||
" \"label\": \"Use gradual movement\",\n",
|
||||
" }, \n",
|
||||
" \"lockstep\": {\n",
|
||||
" \"type\": \"Checkbox\",\n",
|
||||
" \"value\": True,\n",
|
||||
" \"label\": \"Run in locksteps\",\n",
|
||||
" },\n",
|
||||
" \"G\": G,\n",
|
||||
" # \"width\": 10,\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"def colorize(d):\n",
|
||||
" # print(d)\n",
|
||||
" if any(a.waiting for a in d):\n",
|
||||
" return 'red'\n",
|
||||
" else:\n",
|
||||
" return 'blue'\n",
|
||||
"\n",
|
||||
"def network_portrayal(graph, spring=True):\n",
|
||||
" global pos, l\n",
|
||||
" node_size = [10*(len(node[1][\"agent\"])) for node in graph.nodes(data=True)]\n",
|
||||
" node_color = [colorize(d[\"agent\"]) for (k, d) in graph.nodes(data=True)]\n",
|
||||
" # pos = {node: (d[\"x\"], d[\"y\"]) for node, d in graph.nodes(data=True)}\n",
|
||||
" edge_width = [graph.edges[k]['travel_time']/100 for k in graph.edges]\n",
|
||||
" # print(edge_width)\n",
|
||||
" weights = [graph.edges[k]['occupation'] for k in graph.edges]\n",
|
||||
" norm = colors.Normalize(vmin=0, vmax=max(weights))\n",
|
||||
" color = plasma(norm(weights))\n",
|
||||
" # print(color)\n",
|
||||
" return dict(node_size=node_size, node_color=node_color, edge_linewidth=edge_width, edge_color=color)\n",
|
||||
"\n",
|
||||
"page = visualization.JupyterViz(\n",
|
||||
" markov_sim.CityEnv,\n",
|
||||
" model_params,\n",
|
||||
" measures=[\"NodeGini\", \"EdgeGini\", \"EdgeOccupation\"],\n",
|
||||
" name=\"City Environment\",\n",
|
||||
" space_drawer=GeoNetworkDrawer,\n",
|
||||
" agent_portrayal=network_portrayal,\n",
|
||||
" columns=3,\n",
|
||||
")\n",
|
||||
"# This is required to render the visualization in the Jupyter notebook\n",
|
||||
"page"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 39,
|
||||
"id": "70da18d7-66bd-4710-89a6-aca14707c56e",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"<div>\n",
|
||||
"<style scoped>\n",
|
||||
" .dataframe tbody tr th:only-of-type {\n",
|
||||
" vertical-align: middle;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" .dataframe tbody tr th {\n",
|
||||
" vertical-align: top;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" .dataframe thead th {\n",
|
||||
" text-align: right;\n",
|
||||
" }\n",
|
||||
"</style>\n",
|
||||
"<table border=\"1\" class=\"dataframe\">\n",
|
||||
" <thead>\n",
|
||||
" <tr style=\"text-align: right;\">\n",
|
||||
" <th></th>\n",
|
||||
" <th>NodeGini</th>\n",
|
||||
" <th>EdgeGini</th>\n",
|
||||
" <th>EdgeOccupation</th>\n",
|
||||
" </tr>\n",
|
||||
" </thead>\n",
|
||||
" <tbody>\n",
|
||||
" <tr>\n",
|
||||
" <th>0</th>\n",
|
||||
" <td>0.866567</td>\n",
|
||||
" <td>0.927276</td>\n",
|
||||
" <td>0.087624</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>1</th>\n",
|
||||
" <td>0.866567</td>\n",
|
||||
" <td>0.933494</td>\n",
|
||||
" <td>0.081301</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>2</th>\n",
|
||||
" <td>0.863867</td>\n",
|
||||
" <td>0.933163</td>\n",
|
||||
" <td>0.078591</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>3</th>\n",
|
||||
" <td>0.866567</td>\n",
|
||||
" <td>0.929943</td>\n",
|
||||
" <td>0.084914</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>4</th>\n",
|
||||
" <td>0.869433</td>\n",
|
||||
" <td>0.934949</td>\n",
|
||||
" <td>0.076784</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>...</th>\n",
|
||||
" <td>...</td>\n",
|
||||
" <td>...</td>\n",
|
||||
" <td>...</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>127</th>\n",
|
||||
" <td>0.880367</td>\n",
|
||||
" <td>0.934185</td>\n",
|
||||
" <td>0.075881</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>128</th>\n",
|
||||
" <td>0.881400</td>\n",
|
||||
" <td>0.933038</td>\n",
|
||||
" <td>0.078591</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>129</th>\n",
|
||||
" <td>0.881400</td>\n",
|
||||
" <td>0.936299</td>\n",
|
||||
" <td>0.078591</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>130</th>\n",
|
||||
" <td>0.881400</td>\n",
|
||||
" <td>0.929784</td>\n",
|
||||
" <td>0.086721</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>131</th>\n",
|
||||
" <td>0.876733</td>\n",
|
||||
" <td>0.932746</td>\n",
|
||||
" <td>0.082204</td>\n",
|
||||
" </tr>\n",
|
||||
" </tbody>\n",
|
||||
"</table>\n",
|
||||
"<p>132 rows × 3 columns</p>\n",
|
||||
"</div>"
|
||||
],
|
||||
"text/plain": [
|
||||
" NodeGini EdgeGini EdgeOccupation\n",
|
||||
"0 0.866567 0.927276 0.087624\n",
|
||||
"1 0.866567 0.933494 0.081301\n",
|
||||
"2 0.863867 0.933163 0.078591\n",
|
||||
"3 0.866567 0.929943 0.084914\n",
|
||||
"4 0.869433 0.934949 0.076784\n",
|
||||
".. ... ... ...\n",
|
||||
"127 0.880367 0.934185 0.075881\n",
|
||||
"128 0.881400 0.933038 0.078591\n",
|
||||
"129 0.881400 0.936299 0.078591\n",
|
||||
"130 0.881400 0.929784 0.086721\n",
|
||||
"131 0.876733 0.932746 0.082204\n",
|
||||
"\n",
|
||||
"[132 rows x 3 columns]"
|
||||
]
|
||||
},
|
||||
"execution_count": 39,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"page.controller.model.datacollector.get_model_vars_dataframe()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "d9a7d3c8-2f87-47d5-8d27-a7387ea3457d",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.10.12"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
11
examples/markov_chains/app.py
Normal file
11
examples/markov_chains/app.py
Normal file
@ -0,0 +1,11 @@
|
||||
from flask import Flask
|
||||
import solara.server.flask
|
||||
|
||||
app = Flask(__name__)
|
||||
app.register_blueprint(solara.server.flask.blueprint, url_prefix="/solara/")
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def hello_world():
|
||||
return "<p>Hello, World!</p>"
|
||||
|
1
examples/markov_chains/cache/09a7a68a80018222f1664b90343892049e1aa11c.json
vendored
Normal file
1
examples/markov_chains/cache/09a7a68a80018222f1664b90343892049e1aa11c.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
examples/markov_chains/cache/197dbd44d019bd1193a35cd7a026cc804ebd1050.json
vendored
Normal file
1
examples/markov_chains/cache/197dbd44d019bd1193a35cd7a026cc804ebd1050.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
examples/markov_chains/cache/22d10b0cf32c036918f028c264b91bf130ec62f1.json
vendored
Normal file
1
examples/markov_chains/cache/22d10b0cf32c036918f028c264b91bf130ec62f1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
examples/markov_chains/cache/b64244838e9beadda30d0d2a72a54353258b5c83.json
vendored
Normal file
1
examples/markov_chains/cache/b64244838e9beadda30d0d2a72a54353258b5c83.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
examples/markov_chains/cache/b83091b27a005b409e64054a3cad9807ecce636c.json
vendored
Normal file
1
examples/markov_chains/cache/b83091b27a005b409e64054a3cad9807ecce636c.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
examples/markov_chains/cache/cd74ca98335920ce3055676b8729521ca6f6767a.json
vendored
Normal file
1
examples/markov_chains/cache/cd74ca98335920ce3055676b8729521ca6f6767a.json
vendored
Normal file
File diff suppressed because one or more lines are too long
159
examples/markov_chains/markov_sim.py
Normal file
159
examples/markov_chains/markov_sim.py
Normal file
@ -0,0 +1,159 @@
|
||||
'''
|
||||
This scenario has drivers driving around a city.
|
||||
In this model, drivers can only be at intersections, which are treated as nodes in the City Graph (grid).
|
||||
|
||||
At the start of the simulation, drivers are randomly positioned in the city grid.
|
||||
|
||||
The following models for agent behavior are included:
|
||||
|
||||
* DummyDriver: In each simulation step, this type of driver can instantly move to any of the neighboring nodes in the grid, or stay in its place.
|
||||
|
||||
'''
|
||||
|
||||
import networkx as nx
|
||||
from soil import Environment, BaseAgent, state, time
|
||||
from mesa.space import NetworkGrid
|
||||
import mesa
|
||||
import statistics
|
||||
|
||||
|
||||
class CityGrid(NetworkGrid):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
for (u, v, d) in self.G.edges(data=True):
|
||||
d["occupation"] = 0
|
||||
# self.dijkstras = dict(nx.all_pairs_dijkstra(self.G, weight="length"))
|
||||
|
||||
# def eta(self, pos1, pos2):
|
||||
# return self.dijkstras[pos1][0][pos2]
|
||||
|
||||
def travel_time(self, pos1, pos2):
|
||||
return float(min(d["travel_time"] for d in self.G.adj[pos1][pos2].values()))
|
||||
|
||||
|
||||
def node_occupation(self):
|
||||
return {k: len(v.get("agent", [])) for (k, v) in self.G.nodes(data=True)}
|
||||
|
||||
def edge_occupation(self):
|
||||
return {(u,v): d.get('occupation', 1) for (u, v, d) in self.G.edges(data=True)}
|
||||
|
||||
|
||||
class Roamer(BaseAgent):
|
||||
waiting = False
|
||||
|
||||
def step(self):
|
||||
'''
|
||||
A simple driver that just moves to a neighboring cell in the city
|
||||
'''
|
||||
yield from self.move_to(None)
|
||||
return self.delay(0)
|
||||
|
||||
def choose_next(self):
|
||||
opts = self.model.grid.get_neighborhood(self.pos, include_center=False)
|
||||
pos = self.random.choice(opts)
|
||||
delay = self.model.grid.travel_time(self.pos, pos)
|
||||
return pos, delay
|
||||
|
||||
def move_to(self, pos=None):
|
||||
self.waiting = True
|
||||
if pos is None:
|
||||
pos, delay = self.choose_next()
|
||||
if self.model.gradual_move:
|
||||
# Calculate how long it will take, and wait for that long
|
||||
if pos != self.pos:
|
||||
self.model.grid.G.edges[self.pos,pos,0]["occupation"] += 1
|
||||
yield delay
|
||||
if self.model.gradual_move and pos != self.pos:
|
||||
w1 = self.model.grid.G.edges[self.pos,pos,0]["occupation"]
|
||||
oldpos = self.pos
|
||||
self.model.grid.G.edges[self.pos,pos,0]["occupation"] = w1 - 1
|
||||
assert self.model.grid.G.edges[self.pos,pos,0]["occupation"] == w1-1
|
||||
self.model.grid.move_agent(self, pos)
|
||||
self.waiting = False
|
||||
|
||||
|
||||
class LazyRoamer(Roamer):
|
||||
waiting = False
|
||||
def choose_next(self):
|
||||
opts = self.model.grid.get_neighborhood(self.pos, include_center=False)
|
||||
times = [self.model.grid.travel_time(self.pos, other) for other in opts]
|
||||
idx = self.random.choices(range(len(times)), k=1, weights=[1/time for time in times])[0]
|
||||
return opts[idx], times[idx]
|
||||
|
||||
|
||||
|
||||
def gini(values):
|
||||
s = sum(values)
|
||||
|
||||
N = len(values)
|
||||
if s == 0:
|
||||
return 0
|
||||
x = sorted(values)
|
||||
|
||||
B = sum(xi * (N - i) for i, xi in enumerate(x)) / (N * s)
|
||||
return 1 + (1 / N) - 2 * B
|
||||
|
||||
|
||||
class CityEnv(Environment):
|
||||
def __init__(self, *, G, side=20, n_assets=100, ratio_lazy=1, lockstep=True, gradual_move=True, max_weight=1, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
if lockstep:
|
||||
self.schedule = time.Lockstepper(self.schedule)
|
||||
self.n_assets = n_assets
|
||||
self.side = side
|
||||
self.max_weight = max_weight
|
||||
self.gradual_move = gradual_move
|
||||
self.grid = CityGrid(g=G)
|
||||
|
||||
n_lazy = round(self.n_assets * ratio_lazy)
|
||||
n_other = self.n_assets - n_lazy
|
||||
self.add_agents(Roamer, k=n_other)
|
||||
self.add_agents(LazyRoamer, k=n_lazy)
|
||||
|
||||
positions = list(self.grid.G.nodes)
|
||||
for agent in self.get_agents():
|
||||
pos = self.random.choice(positions)
|
||||
self.grid.place_agent(agent, pos)
|
||||
|
||||
self.datacollector = mesa.DataCollector(
|
||||
model_reporters={
|
||||
"NodeGini": lambda model: gini(model.grid.node_occupation().values()),
|
||||
"EdgeGini": lambda model: gini(model.grid.edge_occupation().values()),
|
||||
"EdgeOccupation": lambda model: statistics.mean(model.grid.edge_occupation().values()),
|
||||
}#, agent_reporters={"Wealth": "wealth"}
|
||||
)
|
||||
|
||||
class SquareCityEnv(CityEnv):
|
||||
def __init__(self, *, side=20, **kwargs):
|
||||
self.side = side
|
||||
G = nx.grid_graph(dim=[side, side])
|
||||
for (_, _, d) in G.edges(data=True):
|
||||
d["travel_time"] = self.random.randint(1, self.max_weight)
|
||||
|
||||
for (k, d) in G.nodes(data=True):
|
||||
d["pos"] = k
|
||||
super().__init__(**kwargs, G=G)
|
||||
|
||||
import osmnx as ox
|
||||
|
||||
|
||||
class NamedCityEnv(CityEnv):
|
||||
def __init__(self, *, location="Chamberi, Madrid", **kwargs):
|
||||
self.location = location
|
||||
super().__init__(**kwargs, G=load_city_graph(location))
|
||||
|
||||
|
||||
def load_city_graph(location='Chamberi, Madrid', **kwargs):
|
||||
G = ox.graph.graph_from_place(location, **kwargs)
|
||||
G = ox.add_edge_speeds(G)
|
||||
G = ox.add_edge_travel_times(G)
|
||||
largest = sorted(nx.strongly_connected_components(G), key=lambda x: len(x))[-1]
|
||||
G = G.subgraph(largest)
|
||||
return G
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
env = CityEnv()
|
||||
for i in range(100):
|
||||
env.step()
|
26
examples/markov_chains/sol.py
Normal file
26
examples/markov_chains/sol.py
Normal file
@ -0,0 +1,26 @@
|
||||
import solara
|
||||
|
||||
@solara.component
|
||||
def MainPage(clicks):
|
||||
color = "green"
|
||||
if clicks.value >= 5:
|
||||
color = "red"
|
||||
|
||||
def increment():
|
||||
clicks.value += 1
|
||||
print("clicks", clicks) # noqa
|
||||
|
||||
solara.Button(label=f"Clicked: {clicks}", on_click=increment, color=color)
|
||||
|
||||
@solara.component
|
||||
def Page():
|
||||
v = Visualization()
|
||||
v.viz()
|
||||
|
||||
class Visualization:
|
||||
def __init__(self):
|
||||
self.clicks = solara.reactive(0)
|
||||
|
||||
def viz(self):
|
||||
from sol_lib import MainPage
|
||||
return MainPage(self.clicks)
|
13
examples/markov_chains/sol_lib.py
Normal file
13
examples/markov_chains/sol_lib.py
Normal file
@ -0,0 +1,13 @@
|
||||
import solara
|
||||
|
||||
@solara.component
|
||||
def MainPage(clicks):
|
||||
color = "green"
|
||||
if clicks.value >= 5:
|
||||
color = "red"
|
||||
|
||||
def increment():
|
||||
clicks.value += 1
|
||||
print("clicks", clicks) # noqa
|
||||
|
||||
solara.Button(label=f"Clicked: {clicks}", on_click=increment, color=color)
|
@ -7,7 +7,7 @@ from mesa.space import MultiGrid
|
||||
|
||||
# from mesa.time import RandomActivation
|
||||
from mesa.datacollection import DataCollector
|
||||
from mesa.batchrunner import BatchRunner
|
||||
from mesa.batchrunner import batch_run
|
||||
|
||||
import networkx as nx
|
||||
|
||||
@ -101,7 +101,7 @@ class MoneyEnv(Environment):
|
||||
self.populate_network(agent_class=agent_class)
|
||||
|
||||
# Create agents
|
||||
for agent in self.agents:
|
||||
for agent in self.get_agents():
|
||||
x = self.random.randrange(self.grid.width)
|
||||
y = self.random.randrange(self.grid.height)
|
||||
self.grid.place_agent(agent, (x, y))
|
||||
@ -122,16 +122,14 @@ if __name__ == "__main__":
|
||||
|
||||
variable_params = {"N": range(10, 100, 10)}
|
||||
|
||||
batch_run = BatchRunner(
|
||||
results = batch_run(
|
||||
MoneyEnv,
|
||||
variable_parameters=variable_params,
|
||||
fixed_parameters=fixed_params,
|
||||
iterations=5,
|
||||
max_steps=100,
|
||||
model_reporters={"Gini": compute_gini},
|
||||
max_steps=100
|
||||
)
|
||||
batch_run.run_all()
|
||||
|
||||
run_data = batch_run.get_model_vars_dataframe()
|
||||
run_data.head()
|
||||
run_data = pd.DataFrame(results)
|
||||
print(run_data.head())
|
||||
print(run_data.Gini)
|
||||
|
@ -61,7 +61,7 @@ class Male(Rabbit):
|
||||
return self.dead
|
||||
|
||||
# Males try to mate
|
||||
for f in self.model.agents(
|
||||
for f in self.model.get_agents(
|
||||
agent_class=Female, state_id=Female.fertile.id, limit=self.max_females
|
||||
):
|
||||
self.debug("FOUND A FEMALE: ", repr(f), self.mating_prob)
|
||||
|
@ -70,7 +70,7 @@ class Male(Rabbit):
|
||||
return self.dead
|
||||
|
||||
# Males try to mate
|
||||
for f in self.model.agents(
|
||||
for f in self.model.get_agents(
|
||||
agent_class=Female, state_id=Female.fertile.id, limit=self.max_females
|
||||
):
|
||||
self.debug("FOUND A FEMALE: ", repr(f), self.mating_prob)
|
||||
|
4061
poetry.lock
generated
Normal file
4061
poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
38
pyproject.toml
Normal file
38
pyproject.toml
Normal file
@ -0,0 +1,38 @@
|
||||
[tool.poetry]
|
||||
name = "soil"
|
||||
version = "1.0.0rc11"
|
||||
description = "An Agent-Based Social Simulator for Social Networks"
|
||||
authors = ["J. Fernando Sánchez"]
|
||||
license = "Apache 2.0"
|
||||
readme = "README.md"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
networkx = ">=2.5"
|
||||
numpy = "^1.26.4"
|
||||
matplotlib = "^3.8.3"
|
||||
pyyaml = ">=5.1"
|
||||
pandas = ">=1"
|
||||
salib = ">=1.3"
|
||||
jinja2 = "^3.1.3"
|
||||
mesa = ">=1.2"
|
||||
pydantic = ">=1.9"
|
||||
sqlalchemy = ">=1.4"
|
||||
typing-extensions = ">=4.4"
|
||||
annotated-types = ">=0.4"
|
||||
tqdm = ">=4.64"
|
||||
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "^8.1.1"
|
||||
pytest-profiling = "^1.7.0"
|
||||
scipy = ">=1.3"
|
||||
tornado = "^6.4"
|
||||
nbconvert = "7.3.1"
|
||||
nbformat = "5.8.0"
|
||||
jupyter = "1.0.0"
|
||||
osmnx = "^1.9.2"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
@ -1 +1 @@
|
||||
1.0.0rc3
|
||||
1.0.0rc11
|
||||
|
@ -124,9 +124,7 @@ class BaseAgent(MesaAgent, MutableMapping, metaclass=MetaAgent):
|
||||
|
||||
self.alive = True
|
||||
|
||||
logger = utils.logger.getChild(getattr(self.model, "id", self.model)).getChild(
|
||||
self.name
|
||||
)
|
||||
logger = model.logger.getChild(self.name)
|
||||
self.logger = logging.LoggerAdapter(logger, {"agent_name": self.name})
|
||||
|
||||
if hasattr(self, "level"):
|
||||
@ -233,7 +231,7 @@ class BaseAgent(MesaAgent, MutableMapping, metaclass=MetaAgent):
|
||||
|
||||
def die(self, msg=None):
|
||||
if msg:
|
||||
self.info("Agent dying:", msg)
|
||||
self.debug("Agent dying:", msg)
|
||||
else:
|
||||
self.debug(f"agent dying")
|
||||
self.alive = False
|
||||
@ -437,7 +435,6 @@ def filter_agents(
|
||||
unique_id=None,
|
||||
state_id=None,
|
||||
agent_class=None,
|
||||
ignore=None,
|
||||
state=None,
|
||||
limit=None,
|
||||
**kwargs,
|
||||
@ -445,7 +442,6 @@ def filter_agents(
|
||||
"""
|
||||
Filter agents given as a dict, by the criteria given as arguments (e.g., certain type or state id).
|
||||
"""
|
||||
assert isinstance(agents, dict)
|
||||
|
||||
ids = []
|
||||
|
||||
@ -459,9 +455,9 @@ def filter_agents(
|
||||
ids += id_args
|
||||
|
||||
if ids:
|
||||
f = (agents[aid] for aid in ids if aid in agents)
|
||||
f = (agent for agent in agents if agent.unique_id in ids)
|
||||
else:
|
||||
f = agents.values()
|
||||
f = agents
|
||||
|
||||
if state_id is not None and not isinstance(state_id, (tuple, list)):
|
||||
state_id = tuple([state_id])
|
||||
@ -473,9 +469,6 @@ def filter_agents(
|
||||
except TypeError:
|
||||
agent_class = tuple([agent_class])
|
||||
|
||||
if ignore:
|
||||
f = filter(lambda x: x not in ignore, f)
|
||||
|
||||
if state_id is not None:
|
||||
f = filter(lambda agent: agent.get("state_id", None) in state_id, f)
|
||||
|
||||
|
@ -49,7 +49,7 @@ class Debug(pdb.Pdb):
|
||||
|
||||
@staticmethod
|
||||
def _soil_agents(model, attrs=None, pretty=True, **kwargs):
|
||||
for agent in model.agents(**kwargs):
|
||||
for agent in model.get_agents(**kwargs):
|
||||
d = agent
|
||||
print(" - " + indent(agent.to_str(keys=attrs, pretty=pretty), " "))
|
||||
|
||||
|
@ -113,14 +113,14 @@ class BaseEnvironment(Model):
|
||||
pass
|
||||
|
||||
@property
|
||||
def agents(self):
|
||||
return agentmod.AgentView(self.schedule._agents)
|
||||
def get_agents(self):
|
||||
return agentmod.AgentView(self.schedule.agents)
|
||||
|
||||
def agent(self, *args, **kwargs):
|
||||
return agentmod.AgentView(self.schedule._agents).one(*args, **kwargs)
|
||||
return agentmod.AgentView(self.schedule.agents).one(*args, **kwargs)
|
||||
|
||||
def count_agents(self, *args, **kwargs):
|
||||
return sum(1 for i in self.agents(*args, **kwargs))
|
||||
return sum(1 for i in self.get_agents(*args, **kwargs))
|
||||
|
||||
def agent_df(self, steps=False):
|
||||
df = self.datacollector.get_agent_vars_dataframe()
|
||||
@ -145,6 +145,7 @@ class BaseEnvironment(Model):
|
||||
raise Exception(
|
||||
"The environment has not been scheduled, so it has no sense of time"
|
||||
)
|
||||
|
||||
def init_agents(self):
|
||||
pass
|
||||
|
||||
@ -244,7 +245,7 @@ class BaseEnvironment(Model):
|
||||
return sum(1 for n in self.keys())
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.agents())
|
||||
return iter(self.get_agents())
|
||||
|
||||
def get(self, key, default=None):
|
||||
return self[key] if key in self else default
|
||||
@ -362,11 +363,11 @@ class NetworkEnvironment(BaseEnvironment):
|
||||
"""
|
||||
for (id, data) in self.G.nodes(data=True):
|
||||
if "agent_id" in data:
|
||||
agent = self.agents(data["agent_id"])
|
||||
agent = self.get_agents(data["agent_id"])
|
||||
self.G.nodes[id]["agent"] = agent
|
||||
assert not getattr(agent, "node_id", None) or agent.node_id == id
|
||||
agent.node_id = id
|
||||
for agent in self.agents():
|
||||
for agent in self.get_agents():
|
||||
if hasattr(agent, "node_id"):
|
||||
node_id = agent["node_id"]
|
||||
if node_id not in self.G.nodes:
|
||||
@ -410,7 +411,7 @@ class NetworkEnvironment(BaseEnvironment):
|
||||
|
||||
class EventedEnvironment(BaseEnvironment):
|
||||
def broadcast(self, msg, sender=None, expiration=None, ttl=None, **kwargs):
|
||||
for agent in self.agents(**kwargs):
|
||||
for agent in self.get_agents(**kwargs):
|
||||
if agent == sender:
|
||||
continue
|
||||
self.logger.debug(f"Telling {repr(agent)}: {msg} ttl={ttl}")
|
||||
|
@ -155,8 +155,9 @@ class Simulation:
|
||||
exporter.sim_start()
|
||||
|
||||
for params in tqdm(param_combinations, desc=self.name, unit="configuration"):
|
||||
tqdm.write("- Running for parameters: ")
|
||||
for (k, v) in params.items():
|
||||
tqdm.write(f"{k} = {v}")
|
||||
tqdm.write(f" {k} = {v}")
|
||||
sha = hashlib.sha256()
|
||||
sha.update(repr(sorted(params.items())).encode())
|
||||
params_id = sha.hexdigest()[:7]
|
||||
|
50
soil/time.py
50
soil/time.py
@ -32,7 +32,15 @@ class DeadAgent(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class PQueueActivation(BaseScheduler):
|
||||
class DiscreteActivation(BaseScheduler):
|
||||
default_interval = 1
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if hasattr(self.model, 'default_interval'):
|
||||
self.default_interval = self.model.interval
|
||||
|
||||
|
||||
class PQueueActivation(DiscreteActivation):
|
||||
"""
|
||||
A scheduler which activates each agent with a delay returned by the agent's step method.
|
||||
If no delay is returned, a default of 1 is used.
|
||||
@ -88,7 +96,7 @@ class PQueueActivation(BaseScheduler):
|
||||
break
|
||||
|
||||
try:
|
||||
when = agent.step() or 1
|
||||
when = agent.step() or self.default_interval
|
||||
when += now
|
||||
except DeadAgent:
|
||||
heappop(self._queue)
|
||||
@ -110,7 +118,8 @@ class PQueueActivation(BaseScheduler):
|
||||
return
|
||||
|
||||
|
||||
class TimedActivation(BaseScheduler):
|
||||
class TimedActivation(DiscreteActivation):
|
||||
'''A discrete-time scheduler that has time buckets with agents that should be woken at the same time instant.'''
|
||||
def __init__(self, *args, shuffle=True, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._queue = deque()
|
||||
@ -159,7 +168,7 @@ class TimedActivation(BaseScheduler):
|
||||
self.model.random.shuffle(bucket)
|
||||
for agent in bucket:
|
||||
try:
|
||||
when = agent.step() or 1
|
||||
when = agent.step() or self.default_interval
|
||||
when += now
|
||||
except DeadAgent:
|
||||
continue
|
||||
@ -175,12 +184,45 @@ class TimedActivation(BaseScheduler):
|
||||
|
||||
|
||||
class ShuffledTimedActivation(TimedActivation):
|
||||
'''
|
||||
A TimedActivation scheduler that processes events in random order.
|
||||
'''
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, shuffle=True, **kwargs)
|
||||
|
||||
|
||||
class OrderedTimedActivation(TimedActivation):
|
||||
'''
|
||||
A TimedActivation scheduler that always processes events in
|
||||
the same order.
|
||||
'''
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, shuffle=False, **kwargs)
|
||||
|
||||
|
||||
Scheduler = TimedActivation
|
||||
|
||||
|
||||
class Lockstepper:
|
||||
'''
|
||||
A wrapper class to produce discrete-event schedulers that behave like
|
||||
fixed-time schedulers.
|
||||
'''
|
||||
|
||||
def __init__(self, scheduler: BaseScheduler, interval=1):
|
||||
self.scheduler = scheduler
|
||||
self.default_interval = interval
|
||||
self.time = scheduler.time
|
||||
self.steps = 0
|
||||
|
||||
def step(self):
|
||||
end_time = self.time + self.default_interval
|
||||
res = None
|
||||
while self.scheduler.time < end_time:
|
||||
res = self.scheduler.step()
|
||||
self.time = end_time
|
||||
self.steps += 1
|
||||
return res
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.scheduler, name)
|
@ -24,7 +24,7 @@ consoleHandler = logging.StreamHandler()
|
||||
consoleHandler.setFormatter(logFormatter)
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
level=logging.WARNING,
|
||||
handlers=[
|
||||
consoleHandler,
|
||||
],
|
||||
|
141
soil/visualization.py
Normal file
141
soil/visualization.py
Normal file
@ -0,0 +1,141 @@
|
||||
from typing import Optional
|
||||
|
||||
import sys
|
||||
import threading
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import reacton.ipywidgets as widgets
|
||||
import solara
|
||||
from solara.alias import rv
|
||||
|
||||
import mesa.experimental.components.matplotlib as components_matplotlib
|
||||
from mesa.experimental.jupyter_viz import *
|
||||
from matplotlib.figure import Figure
|
||||
import networkx as nx
|
||||
|
||||
|
||||
class Controller:
|
||||
'''
|
||||
A visualization controller that holds a reference to a model so that it can be modified or queried while the simulation is still running.
|
||||
'''
|
||||
def __init__(self):
|
||||
self.model = None
|
||||
|
||||
|
||||
def JupyterViz(*args, **kwargs):
|
||||
c = Controller()
|
||||
page = JupyterPage(*args, controller=c, **kwargs)
|
||||
page.controller = c
|
||||
return page
|
||||
|
||||
|
||||
@solara.component
|
||||
def JupyterPage(
|
||||
model_class,
|
||||
model_params,
|
||||
controller=None,
|
||||
measures=None,
|
||||
name="Mesa Model",
|
||||
agent_portrayal=None,
|
||||
space_drawer="default",
|
||||
play_interval=150,
|
||||
columns=2,
|
||||
):
|
||||
"""Initialize a component to visualize a model.
|
||||
Args:
|
||||
model_class: class of the model to instantiate
|
||||
model_params: parameters for initializing the model
|
||||
measures: list of callables or data attributes to plot
|
||||
name: name for display
|
||||
agent_portrayal: options for rendering agents (dictionary)
|
||||
space_drawer: method to render the agent space for
|
||||
the model; default implementation is the `SpaceMatplotlib` component;
|
||||
simulations with no space to visualize should
|
||||
specify `space_drawer=False`
|
||||
play_interval: play interval (default: 150)
|
||||
"""
|
||||
if controller is None:
|
||||
controller = Controller()
|
||||
|
||||
current_step = solara.use_reactive(0)
|
||||
|
||||
# 1. Set up model parameters
|
||||
user_params, fixed_params = split_model_params(model_params)
|
||||
model_parameters, set_model_parameters = solara.use_state(
|
||||
{**fixed_params, **{k: v["value"] for k, v in user_params.items()}}
|
||||
)
|
||||
|
||||
# 2. Set up Model
|
||||
def make_model():
|
||||
model = model_class(**model_parameters)
|
||||
current_step.value = 0
|
||||
controller.model = model
|
||||
return model
|
||||
|
||||
reset_counter = solara.use_reactive(0)
|
||||
model = solara.use_memo(
|
||||
make_model, dependencies=[*list(model_parameters.values()), reset_counter.value]
|
||||
)
|
||||
|
||||
def handle_change_model_params(name: str, value: any):
|
||||
set_model_parameters({**model_parameters, name: value})
|
||||
|
||||
# 3. Set up UI
|
||||
with solara.AppBar():
|
||||
solara.AppBarTitle(name)
|
||||
|
||||
with solara.GridFixed(columns=2):
|
||||
UserInputs(user_params, on_change=handle_change_model_params)
|
||||
ModelController(model, play_interval, current_step, reset_counter)
|
||||
solara.Markdown(md_text=f"###Step: {current_step} - Time: {model.schedule.time } ")
|
||||
|
||||
with solara.GridFixed(columns=columns):
|
||||
# 4. Space
|
||||
if space_drawer == "default":
|
||||
# draw with the default implementation
|
||||
components_matplotlib.SpaceMatplotlib(
|
||||
model, agent_portrayal, dependencies=[current_step.value]
|
||||
)
|
||||
elif space_drawer:
|
||||
# if specified, draw agent space with an alternate renderer
|
||||
space_drawer(model, agent_portrayal, dependencies=[current_step.value])
|
||||
# otherwise, do nothing (do not draw space)
|
||||
|
||||
# 5. Plots
|
||||
for measure in measures:
|
||||
if callable(measure):
|
||||
# Is a custom object
|
||||
measure(model)
|
||||
else:
|
||||
components_matplotlib.make_plot(model, measure)
|
||||
|
||||
|
||||
@solara.component
|
||||
def NetworkDrawer(model, network_portrayal, dependencies: Optional[list[any]] = None):
|
||||
space_fig = Figure()
|
||||
space_ax = space_fig.subplots()
|
||||
graph = model.grid.G
|
||||
nx.draw(
|
||||
graph,
|
||||
ax=space_ax,
|
||||
**network_portrayal(graph),
|
||||
)
|
||||
solara.FigureMatplotlib(space_fig, format="png", dependencies=dependencies)
|
||||
|
||||
|
||||
try:
|
||||
import osmnx as ox
|
||||
|
||||
@solara.component
|
||||
def GeoNetworkDrawer(model, network_portrayal, dependencies: Optional[list[any]] = None):
|
||||
space_fig = Figure()
|
||||
space_ax = space_fig.subplots()
|
||||
graph = model.grid.G
|
||||
ox.plot_graph(
|
||||
graph,
|
||||
ax=space_ax,
|
||||
**network_portrayal(graph),
|
||||
)
|
||||
solara.FigureMatplotlib(space_fig, format="png", dependencies=dependencies)
|
||||
except ImportError:
|
||||
pass
|
@ -170,12 +170,12 @@ class TestAgents(TestCase):
|
||||
e = environment.Environment()
|
||||
e.add_agent(agent_class=agents.BaseAgent)
|
||||
e.add_agent(agent_class=agents.Evented)
|
||||
base = list(e.agents(agent_class=agents.BaseAgent))
|
||||
base = list(e.get_agents(agent_class=agents.BaseAgent))
|
||||
assert len(base) == 2
|
||||
ev = list(e.agents(agent_class=agents.Evented))
|
||||
ev = list(e.get_agents(agent_class=agents.Evented))
|
||||
assert len(ev) == 1
|
||||
assert ev[0].unique_id == 1
|
||||
null = list(e.agents(unique_ids=[0, 1], agent_class=agents.NetworkAgent))
|
||||
null = list(e.get_agents(unique_ids=[0, 1], agent_class=agents.NetworkAgent))
|
||||
assert not null
|
||||
|
||||
def test_agent_return(self):
|
||||
|
Loading…
Reference in New Issue
Block a user