mirror of https://github.com/gsi-upm/soil
parent
f49be3af68
commit
25d042f16c
File diff suppressed because one or more lines are too long
@ -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
@ -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()
|
@ -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)
|
@ -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)
|
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
|
@ -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
|
Loading…
Reference in New Issue