mirror of
https://github.com/gsi-upm/soil
synced 2024-10-31 17:11:42 +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).
|
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`.
|
For an explanation of the general changes in version 1.0, please refer to the file `docs/notes_v1.0.rst`.
|
||||||
|
|
||||||
### Added
|
### 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.
|
* The `agent.after` and `agent.at` methods, to avoid having to return a time manually.
|
||||||
### Changed
|
### Changed
|
||||||
* Configuration schema (`Simulation`) is very simplified. All simulations should be checked
|
* 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
|
* 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.
|
* `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`
|
* `num_trials` renamed to `iterations`
|
||||||
|
@ -27,7 +27,7 @@ class VirusOnNetwork(Environment):
|
|||||||
|
|
||||||
# Infect some nodes
|
# Infect some nodes
|
||||||
infected_nodes = self.random.sample(list(self.G), self.initial_outbreak_size)
|
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)
|
a.set_state(VirusAgent.infected)
|
||||||
assert self.number_infected == self.initial_outbreak_size
|
assert self.number_infected == self.initial_outbreak_size
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ class VirusOnNetwork(Environment):
|
|||||||
|
|
||||||
# Infect some nodes
|
# Infect some nodes
|
||||||
infected_nodes = self.random.sample(list(self.G), self.initial_outbreak_size)
|
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
|
a.status = State.INFECTED
|
||||||
assert self.number_infected == self.initial_outbreak_size
|
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):
|
def init(self):
|
||||||
self.grid = MultiGrid(width=self.width, height=self.height, torus=False)
|
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(Driver, k=self.n_cars)
|
||||||
self.add_agents(Passenger, k=self.n_passengers)
|
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.place_agent(agent, (0, 0))
|
||||||
self.grid.move_to_empty(agent)
|
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.time import RandomActivation
|
||||||
from mesa.datacollection import DataCollector
|
from mesa.datacollection import DataCollector
|
||||||
from mesa.batchrunner import BatchRunner
|
from mesa.batchrunner import batch_run
|
||||||
|
|
||||||
import networkx as nx
|
import networkx as nx
|
||||||
|
|
||||||
@ -101,7 +101,7 @@ class MoneyEnv(Environment):
|
|||||||
self.populate_network(agent_class=agent_class)
|
self.populate_network(agent_class=agent_class)
|
||||||
|
|
||||||
# Create agents
|
# Create agents
|
||||||
for agent in self.agents:
|
for agent in self.get_agents():
|
||||||
x = self.random.randrange(self.grid.width)
|
x = self.random.randrange(self.grid.width)
|
||||||
y = self.random.randrange(self.grid.height)
|
y = self.random.randrange(self.grid.height)
|
||||||
self.grid.place_agent(agent, (x, y))
|
self.grid.place_agent(agent, (x, y))
|
||||||
@ -122,16 +122,14 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
variable_params = {"N": range(10, 100, 10)}
|
variable_params = {"N": range(10, 100, 10)}
|
||||||
|
|
||||||
batch_run = BatchRunner(
|
results = batch_run(
|
||||||
MoneyEnv,
|
MoneyEnv,
|
||||||
variable_parameters=variable_params,
|
variable_parameters=variable_params,
|
||||||
fixed_parameters=fixed_params,
|
fixed_parameters=fixed_params,
|
||||||
iterations=5,
|
iterations=5,
|
||||||
max_steps=100,
|
max_steps=100
|
||||||
model_reporters={"Gini": compute_gini},
|
|
||||||
)
|
)
|
||||||
batch_run.run_all()
|
|
||||||
|
|
||||||
run_data = batch_run.get_model_vars_dataframe()
|
run_data = pd.DataFrame(results)
|
||||||
run_data.head()
|
print(run_data.head())
|
||||||
print(run_data.Gini)
|
print(run_data.Gini)
|
||||||
|
@ -61,7 +61,7 @@ class Male(Rabbit):
|
|||||||
return self.dead
|
return self.dead
|
||||||
|
|
||||||
# Males try to mate
|
# 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
|
agent_class=Female, state_id=Female.fertile.id, limit=self.max_females
|
||||||
):
|
):
|
||||||
self.debug("FOUND A FEMALE: ", repr(f), self.mating_prob)
|
self.debug("FOUND A FEMALE: ", repr(f), self.mating_prob)
|
||||||
|
@ -70,7 +70,7 @@ class Male(Rabbit):
|
|||||||
return self.dead
|
return self.dead
|
||||||
|
|
||||||
# Males try to mate
|
# 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
|
agent_class=Female, state_id=Female.fertile.id, limit=self.max_females
|
||||||
):
|
):
|
||||||
self.debug("FOUND A FEMALE: ", repr(f), self.mating_prob)
|
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
|
self.alive = True
|
||||||
|
|
||||||
logger = utils.logger.getChild(getattr(self.model, "id", self.model)).getChild(
|
logger = model.logger.getChild(self.name)
|
||||||
self.name
|
|
||||||
)
|
|
||||||
self.logger = logging.LoggerAdapter(logger, {"agent_name": self.name})
|
self.logger = logging.LoggerAdapter(logger, {"agent_name": self.name})
|
||||||
|
|
||||||
if hasattr(self, "level"):
|
if hasattr(self, "level"):
|
||||||
@ -233,7 +231,7 @@ class BaseAgent(MesaAgent, MutableMapping, metaclass=MetaAgent):
|
|||||||
|
|
||||||
def die(self, msg=None):
|
def die(self, msg=None):
|
||||||
if msg:
|
if msg:
|
||||||
self.info("Agent dying:", msg)
|
self.debug("Agent dying:", msg)
|
||||||
else:
|
else:
|
||||||
self.debug(f"agent dying")
|
self.debug(f"agent dying")
|
||||||
self.alive = False
|
self.alive = False
|
||||||
@ -437,7 +435,6 @@ def filter_agents(
|
|||||||
unique_id=None,
|
unique_id=None,
|
||||||
state_id=None,
|
state_id=None,
|
||||||
agent_class=None,
|
agent_class=None,
|
||||||
ignore=None,
|
|
||||||
state=None,
|
state=None,
|
||||||
limit=None,
|
limit=None,
|
||||||
**kwargs,
|
**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).
|
Filter agents given as a dict, by the criteria given as arguments (e.g., certain type or state id).
|
||||||
"""
|
"""
|
||||||
assert isinstance(agents, dict)
|
|
||||||
|
|
||||||
ids = []
|
ids = []
|
||||||
|
|
||||||
@ -459,9 +455,9 @@ def filter_agents(
|
|||||||
ids += id_args
|
ids += id_args
|
||||||
|
|
||||||
if ids:
|
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:
|
else:
|
||||||
f = agents.values()
|
f = agents
|
||||||
|
|
||||||
if state_id is not None and not isinstance(state_id, (tuple, list)):
|
if state_id is not None and not isinstance(state_id, (tuple, list)):
|
||||||
state_id = tuple([state_id])
|
state_id = tuple([state_id])
|
||||||
@ -473,9 +469,6 @@ def filter_agents(
|
|||||||
except TypeError:
|
except TypeError:
|
||||||
agent_class = tuple([agent_class])
|
agent_class = tuple([agent_class])
|
||||||
|
|
||||||
if ignore:
|
|
||||||
f = filter(lambda x: x not in ignore, f)
|
|
||||||
|
|
||||||
if state_id is not None:
|
if state_id is not None:
|
||||||
f = filter(lambda agent: agent.get("state_id", None) in state_id, f)
|
f = filter(lambda agent: agent.get("state_id", None) in state_id, f)
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ class Debug(pdb.Pdb):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _soil_agents(model, attrs=None, pretty=True, **kwargs):
|
def _soil_agents(model, attrs=None, pretty=True, **kwargs):
|
||||||
for agent in model.agents(**kwargs):
|
for agent in model.get_agents(**kwargs):
|
||||||
d = agent
|
d = agent
|
||||||
print(" - " + indent(agent.to_str(keys=attrs, pretty=pretty), " "))
|
print(" - " + indent(agent.to_str(keys=attrs, pretty=pretty), " "))
|
||||||
|
|
||||||
|
@ -113,14 +113,14 @@ class BaseEnvironment(Model):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def agents(self):
|
def get_agents(self):
|
||||||
return agentmod.AgentView(self.schedule._agents)
|
return agentmod.AgentView(self.schedule.agents)
|
||||||
|
|
||||||
def agent(self, *args, **kwargs):
|
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):
|
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):
|
def agent_df(self, steps=False):
|
||||||
df = self.datacollector.get_agent_vars_dataframe()
|
df = self.datacollector.get_agent_vars_dataframe()
|
||||||
@ -145,6 +145,7 @@ class BaseEnvironment(Model):
|
|||||||
raise Exception(
|
raise Exception(
|
||||||
"The environment has not been scheduled, so it has no sense of time"
|
"The environment has not been scheduled, so it has no sense of time"
|
||||||
)
|
)
|
||||||
|
|
||||||
def init_agents(self):
|
def init_agents(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -244,7 +245,7 @@ class BaseEnvironment(Model):
|
|||||||
return sum(1 for n in self.keys())
|
return sum(1 for n in self.keys())
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
return iter(self.agents())
|
return iter(self.get_agents())
|
||||||
|
|
||||||
def get(self, key, default=None):
|
def get(self, key, default=None):
|
||||||
return self[key] if key in self else default
|
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):
|
for (id, data) in self.G.nodes(data=True):
|
||||||
if "agent_id" in data:
|
if "agent_id" in data:
|
||||||
agent = self.agents(data["agent_id"])
|
agent = self.get_agents(data["agent_id"])
|
||||||
self.G.nodes[id]["agent"] = agent
|
self.G.nodes[id]["agent"] = agent
|
||||||
assert not getattr(agent, "node_id", None) or agent.node_id == id
|
assert not getattr(agent, "node_id", None) or agent.node_id == id
|
||||||
agent.node_id = id
|
agent.node_id = id
|
||||||
for agent in self.agents():
|
for agent in self.get_agents():
|
||||||
if hasattr(agent, "node_id"):
|
if hasattr(agent, "node_id"):
|
||||||
node_id = agent["node_id"]
|
node_id = agent["node_id"]
|
||||||
if node_id not in self.G.nodes:
|
if node_id not in self.G.nodes:
|
||||||
@ -410,7 +411,7 @@ class NetworkEnvironment(BaseEnvironment):
|
|||||||
|
|
||||||
class EventedEnvironment(BaseEnvironment):
|
class EventedEnvironment(BaseEnvironment):
|
||||||
def broadcast(self, msg, sender=None, expiration=None, ttl=None, **kwargs):
|
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:
|
if agent == sender:
|
||||||
continue
|
continue
|
||||||
self.logger.debug(f"Telling {repr(agent)}: {msg} ttl={ttl}")
|
self.logger.debug(f"Telling {repr(agent)}: {msg} ttl={ttl}")
|
||||||
|
@ -155,8 +155,9 @@ class Simulation:
|
|||||||
exporter.sim_start()
|
exporter.sim_start()
|
||||||
|
|
||||||
for params in tqdm(param_combinations, desc=self.name, unit="configuration"):
|
for params in tqdm(param_combinations, desc=self.name, unit="configuration"):
|
||||||
|
tqdm.write("- Running for parameters: ")
|
||||||
for (k, v) in params.items():
|
for (k, v) in params.items():
|
||||||
tqdm.write(f"{k} = {v}")
|
tqdm.write(f" {k} = {v}")
|
||||||
sha = hashlib.sha256()
|
sha = hashlib.sha256()
|
||||||
sha.update(repr(sorted(params.items())).encode())
|
sha.update(repr(sorted(params.items())).encode())
|
||||||
params_id = sha.hexdigest()[:7]
|
params_id = sha.hexdigest()[:7]
|
||||||
|
50
soil/time.py
50
soil/time.py
@ -32,7 +32,15 @@ class DeadAgent(Exception):
|
|||||||
pass
|
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.
|
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.
|
If no delay is returned, a default of 1 is used.
|
||||||
@ -88,7 +96,7 @@ class PQueueActivation(BaseScheduler):
|
|||||||
break
|
break
|
||||||
|
|
||||||
try:
|
try:
|
||||||
when = agent.step() or 1
|
when = agent.step() or self.default_interval
|
||||||
when += now
|
when += now
|
||||||
except DeadAgent:
|
except DeadAgent:
|
||||||
heappop(self._queue)
|
heappop(self._queue)
|
||||||
@ -110,7 +118,8 @@ class PQueueActivation(BaseScheduler):
|
|||||||
return
|
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):
|
def __init__(self, *args, shuffle=True, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self._queue = deque()
|
self._queue = deque()
|
||||||
@ -159,7 +168,7 @@ class TimedActivation(BaseScheduler):
|
|||||||
self.model.random.shuffle(bucket)
|
self.model.random.shuffle(bucket)
|
||||||
for agent in bucket:
|
for agent in bucket:
|
||||||
try:
|
try:
|
||||||
when = agent.step() or 1
|
when = agent.step() or self.default_interval
|
||||||
when += now
|
when += now
|
||||||
except DeadAgent:
|
except DeadAgent:
|
||||||
continue
|
continue
|
||||||
@ -175,12 +184,45 @@ class TimedActivation(BaseScheduler):
|
|||||||
|
|
||||||
|
|
||||||
class ShuffledTimedActivation(TimedActivation):
|
class ShuffledTimedActivation(TimedActivation):
|
||||||
|
'''
|
||||||
|
A TimedActivation scheduler that processes events in random order.
|
||||||
|
'''
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, shuffle=True, **kwargs)
|
super().__init__(*args, shuffle=True, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class OrderedTimedActivation(TimedActivation):
|
class OrderedTimedActivation(TimedActivation):
|
||||||
|
'''
|
||||||
|
A TimedActivation scheduler that always processes events in
|
||||||
|
the same order.
|
||||||
|
'''
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, shuffle=False, **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)
|
consoleHandler.setFormatter(logFormatter)
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.WARNING,
|
||||||
handlers=[
|
handlers=[
|
||||||
consoleHandler,
|
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 = environment.Environment()
|
||||||
e.add_agent(agent_class=agents.BaseAgent)
|
e.add_agent(agent_class=agents.BaseAgent)
|
||||||
e.add_agent(agent_class=agents.Evented)
|
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
|
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 len(ev) == 1
|
||||||
assert ev[0].unique_id == 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
|
assert not null
|
||||||
|
|
||||||
def test_agent_return(self):
|
def test_agent_return(self):
|
||||||
|
Loading…
Reference in New Issue
Block a user