1
0
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:
J. Fernando Sánchez 2024-04-11 17:46:45 +02:00
parent f49be3af68
commit 25d042f16c
30 changed files with 5896 additions and 404 deletions

View File

@ -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`

View File

@ -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

View File

@ -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

View File

@ -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)

View 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
}

View 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>"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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()

View 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)

View 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)

View File

@ -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)

View File

@ -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)

View File

@ -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

File diff suppressed because it is too large Load Diff

38
pyproject.toml Normal file
View 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"

View File

@ -1 +1 @@
1.0.0rc3 1.0.0rc11

View File

@ -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)

View File

@ -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), " "))

View File

@ -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}")

View File

@ -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]

View File

@ -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)

View File

@ -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
View 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

View File

@ -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):