mirror of
https://github.com/gsi-upm/soil
synced 2025-09-13 19:52:20 +00:00
Compare commits
14 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
25d042f16c | ||
|
f49be3af68 | ||
|
5e93399d58 | ||
|
eca4cae298 | ||
|
47a67f6665 | ||
|
c13550cf83 | ||
|
55bbc76b2a | ||
|
d13e4eb4b9 | ||
|
93d23e4cab | ||
|
3802578ad5 | ||
|
4e296e0cf1 | ||
|
302075a65d | ||
|
fba379c97c | ||
|
bf481f0f88 |
@@ -1,5 +1,7 @@
|
||||
**/soil_output
|
||||
.*
|
||||
**/.*
|
||||
**/__pycache__
|
||||
__pycache__
|
||||
*.pyc
|
||||
**/backup
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -8,4 +8,5 @@ soil_output
|
||||
docs/_build*
|
||||
build/*
|
||||
dist/*
|
||||
prof
|
||||
prof
|
||||
backup
|
15
CHANGELOG.md
15
CHANGELOG.md
@@ -3,9 +3,9 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.0 UNRELEASED]
|
||||
## [1.0.0 UNRELEASED]
|
||||
|
||||
Version 1.0 introduced multiple changes, especially on the `Simulation` class and anything related to how configuration is handled.
|
||||
Version 1.0 will introduce multiple changes, especially on the `Simulation` class and anything related to how configuration is handled.
|
||||
For an explanation of the general changes in version 1.0, please refer to the file `docs/notes_v1.0.rst`.
|
||||
|
||||
### Added
|
||||
@@ -13,9 +13,10 @@ For an explanation of the general changes in version 1.0, please refer to the fi
|
||||
* Environments now have a class method to make them easier to use without a simulation`.run`. Notice that this is different from `run_model`, which is an instance method.
|
||||
* Ability to run simulations using mesa models
|
||||
* The `soil.exporters` module to export the results of datacollectors (`model.datacollector`) into files at the end of trials/simulations
|
||||
* Agents can now have generators as a step function or a state. They work similar to normal functions, with one caveat in the case of `FSM`: only `time` values (or None) can be yielded, not a state. This is because the state will not change, it will be resumed after the yield, at the appropriate time. The return value *can* be a state, or a `(state, time)` tuple, just like in normal states.
|
||||
* Agents can now have generators or async functions as their step or as states. They work similar to normal functions, with one caveat in the case of `FSM`: only time values (a float, int or None) can be awaited or yielded, not a state. This is because the state will not change, it will be resumed after the yield, at the appropriate time. To return to a different state, use the `delay` and `at` functions of the state.
|
||||
* Simulations can now specify a `matrix` with possible values for every simulation parameter. The final parameters will be calculated based on the `parameters` used and a cartesian product (i.e., all possible combinations) of each parameter.
|
||||
* Simple debugging capabilities in `soil.debugging`, with a custom `pdb.Debugger` subclass that exposes commands to list agents and their status and set breakpoints on states (for FSM agents). Try it with `soil --debug <simulation file>`
|
||||
* The `agent.after` and `agent.at` methods, to avoid having to return a time manually.
|
||||
### Changed
|
||||
* Configuration schema (`Simulation`) is very simplified. All simulations should be checked
|
||||
* Model / environment variables are expected (but not enforced) to be a single value. This is done to more closely align with mesa
|
||||
@@ -26,8 +27,16 @@ For an explanation of the general changes in version 1.0, please refer to the fi
|
||||
* Simulation results for every iteration of a simulation with the same name are stored in a single `sqlite` database
|
||||
|
||||
### Removed
|
||||
* The `time.When` and `time.Cond` classes are removed
|
||||
* Any `tsih` and `History` integration in the main classes. To record the state of environments/agents, just use a datacollector. In some cases this may be slower or consume more memory than the previous system. However, few cases actually used the full potential of the history, and it came at the cost of unnecessary complexity and worse performance for the majority of cases.
|
||||
|
||||
## [0.20.8]
|
||||
### Changed
|
||||
* Tsih bumped to version 0.1.8
|
||||
### Fixed
|
||||
* Mentions to `id` in docs. It should be `state_id` now.
|
||||
* Fixed bug: environment agents were not being added to the simulation
|
||||
|
||||
## [0.20.7]
|
||||
### Changed
|
||||
* Creating a `time.When` from another `time.When` does not nest them anymore (it returns the argument)
|
||||
|
@@ -4,7 +4,7 @@
|
||||
Soil is an extensible and user-friendly Agent-based Social Simulator for Social Networks.
|
||||
Learn how to run your own simulations with our [documentation](http://soilsim.readthedocs.io).
|
||||
|
||||
Follow our [tutorial](examples/tutorial/soil_tutorial.ipynb) to develop your own agent models.
|
||||
Follow our [tutorial](docs/tutorial/soil_tutorial.ipynb) to develop your own agent models.
|
||||
|
||||
> **Warning**
|
||||
> Soil 1.0 introduced many fundamental changes. Check the [documention on how to update your simulations to work with newer versions](docs/notes_v1.0.rst)
|
||||
@@ -36,7 +36,6 @@ Follow our [tutorial](examples/tutorial/soil_tutorial.ipynb) to develop your own
|
||||
* A command line interface (`soil`), to quickly run simulations with different parameters
|
||||
* An integrated debugger (`soil --debug`) with custom functions to print agent states and break at specific states
|
||||
|
||||
|
||||
## Mesa compatibility
|
||||
|
||||
SOIL has been redesigned to integrate well with [Mesa](https://github.com/projectmesa/mesa).
|
||||
|
12
benchmarks/noop-bench-async.csv
Normal file
12
benchmarks/noop-bench-async.csv
Normal file
@@ -0,0 +1,12 @@
|
||||
command,mean,stddev,median,user,system,min,max,parameter_sim
|
||||
python noop/mesa_batchrunner.py,1.3258325165599998,0.05822826666377271,1.31279976286,1.2978164199999997,0.25767558,1.2780627573599999,1.46763559736,mesa_batchrunner
|
||||
python noop/mesa_simulation.py,1.3915081544599999,0.07311646048704976,1.37166811936,1.35267662,0.29222067999999995,1.32746067836,1.58495303336,mesa_simulation
|
||||
python noop/soil_step.py,1.9859962588599998,0.12143759641749913,1.93586195486,2.0000750199999997,0.54126188,1.9061700903599998,2.2532835533599997,soil_step
|
||||
python noop/soil_step_pqueue.py,2.1347049971600005,0.01336179424666973,2.13492341986,2.1368160200000004,0.56862948,2.11810132936,2.16042739636,soil_step_pqueue
|
||||
python noop/soil_gens.py,2.1284937893599998,0.03030587681163665,2.13585231586,2.14158812,0.54900038,2.0768625143599997,2.19043625236,soil_gens
|
||||
python noop/soil_gens_pqueue.py,2.3469003942599995,0.019461346004472344,2.3486906343599996,2.36505852,0.54629858,2.31766326036,2.37998102136,soil_gens_pqueue
|
||||
python noop/soil_async.py,2.85755484126,0.0314955571121844,2.84774029536,2.86388112,0.55261338,2.81428668936,2.90567961636,soil_async
|
||||
python noop/soil_async_pqueue.py,3.1999731134600005,0.04432336803797717,3.20255954186,3.2162337199999995,0.5501872800000001,3.1406816913599997,3.26137401936,soil_async_pqueue
|
||||
python noop/soilent_step.py,1.30038977816,0.017973958957989845,1.30187804986,1.3231730199999998,0.5452653799999999,1.27058263436,1.31902240836,soilent_step
|
||||
python noop/soilent_step_pqueue.py,1.4708435788599998,0.027193290392962755,1.4707784423599999,1.4900387199999998,0.54749428,1.43498127536,1.53065598436,soilent_step_pqueue
|
||||
python noop/soilent_gens.py,1.6338810973599998,0.05752539125688073,1.63513330036,1.65216122,0.51846678,1.54135944036,1.7038832853599999,soilent_gens
|
|
11
benchmarks/noop-bench.csv
Normal file
11
benchmarks/noop-bench.csv
Normal file
@@ -0,0 +1,11 @@
|
||||
command,mean,stddev,median,user,system,min,max,parameter_sim
|
||||
python noop/mesa1_batchrunner.py,1.2559917394000002,0.012031173494887278,1.2572688413000002,1.2168630799999998,0.31825289999999995,1.2346063853,1.2735512493,mesa1_batchrunner
|
||||
python noop/mesa1_simulation.py,1.3024417227,0.022498874113931668,1.2994157323,1.2595484799999999,0.3087897,1.2697029703,1.3350640403,mesa1_simulation
|
||||
python noop/soil1.py,1.8789492443,0.18023367899835044,1.8186795393000001,1.86076288,0.5309521,1.7326687413000001,2.2928370642999996,soil1
|
||||
python noop/soil1_pqueue.py,1.9841675890000001,0.01735524088843906,1.9884363323,2.01830338,0.5787977999999999,1.9592171483,2.0076169282999996,soil1_pqueue
|
||||
python noop/soil2.py,2.0135188921999996,0.02869307129649681,2.0184709453,2.03951308,0.5885591,1.9680417823,2.0567112592999997,soil2
|
||||
python noop/soil2_pqueue.py,2.2367320454999997,0.024339667344486046,2.2357249777999995,2.2515216799999997,0.5978869,2.1957917303,2.2688685033,soil2_pqueue
|
||||
python noop/soilent1.py,1.1309301329,0.015133005948737871,1.1276461497999999,1.14056688,0.6027519,1.1135821423,1.1625753893,soilent1
|
||||
python noop/soilent1_pqueue.py,1.3097537665000003,0.018821977712258842,1.3073709358,1.3270259799999997,0.6000067999999998,1.2874580013,1.3381646823,soilent1_pqueue
|
||||
python noop/soilent2.py,1.5055360476,0.05166674417574119,1.4883118568,1.5121205799999997,0.5817363999999999,1.4490918363,1.6005909333000001,soilent2
|
||||
python noop/soilent2_pqueue.py,1.6622598218,0.031130739036296016,1.6588702603,1.6862567799999997,0.5854159,1.6289724583,1.7330545383,soilent2_pqueue
|
|
25
benchmarks/noop/_config.py
Normal file
25
benchmarks/noop/_config.py
Normal file
@@ -0,0 +1,25 @@
|
||||
import os
|
||||
|
||||
NUM_AGENTS = int(os.environ.get('NUM_AGENTS', 100))
|
||||
NUM_ITERS = int(os.environ.get('NUM_ITERS', 10))
|
||||
MAX_STEPS = int(os.environ.get('MAX_STEPS', 1000))
|
||||
|
||||
|
||||
def run_sim(model, **kwargs):
|
||||
from soil import Simulation
|
||||
opts = dict(model=model,
|
||||
dump=False,
|
||||
num_processes=1,
|
||||
parameters={'num_agents': NUM_AGENTS},
|
||||
max_steps=MAX_STEPS,
|
||||
iterations=NUM_ITERS)
|
||||
opts.update(kwargs)
|
||||
res = Simulation(**opts).run()
|
||||
|
||||
total = sum(a.num_calls for e in res for a in e.schedule.agents)
|
||||
expected = NUM_AGENTS * NUM_ITERS * MAX_STEPS
|
||||
print(total)
|
||||
print(expected)
|
||||
|
||||
assert total == expected
|
||||
return res
|
44
benchmarks/noop/mesa_batchrunner.py
Normal file
44
benchmarks/noop/mesa_batchrunner.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from mesa import batch_run, DataCollector, Agent, Model
|
||||
from mesa.time import RandomActivation
|
||||
|
||||
|
||||
class NoopAgent(Agent):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.num_calls = 0
|
||||
|
||||
def step(self):
|
||||
# import pdb;pdb.set_trace()
|
||||
self.num_calls += 1
|
||||
|
||||
|
||||
class NoopModel(Model):
|
||||
def __init__(self, N):
|
||||
super().__init__()
|
||||
self.schedule = RandomActivation(self)
|
||||
for i in range(N):
|
||||
self.schedule.add(NoopAgent(self.next_id(), self))
|
||||
self.datacollector = DataCollector(model_reporters={"num_agents": lambda m: m.schedule.get_agent_count(),
|
||||
"time": lambda m: m.schedule.time},
|
||||
agent_reporters={"num_calls": "num_calls"})
|
||||
self.datacollector.collect(self)
|
||||
|
||||
def step(self):
|
||||
self.schedule.step()
|
||||
self.datacollector.collect(self)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from _config import *
|
||||
|
||||
res = batch_run(model_cls=NoopModel,
|
||||
max_steps=MAX_STEPS,
|
||||
iterations=NUM_ITERS,
|
||||
number_processes=1,
|
||||
parameters={'N': NUM_AGENTS})
|
||||
total = sum(s["num_calls"] for s in res)
|
||||
total_agents = sum(s["num_agents"] for s in res)
|
||||
assert len(res) == NUM_AGENTS * NUM_ITERS
|
||||
assert total == NUM_AGENTS * NUM_ITERS * MAX_STEPS
|
||||
assert total_agents == NUM_AGENTS * NUM_AGENTS * NUM_ITERS
|
||||
|
38
benchmarks/noop/mesa_simulation.py
Normal file
38
benchmarks/noop/mesa_simulation.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from mesa import batch_run, DataCollector, Agent, Model
|
||||
from mesa.time import RandomActivation
|
||||
from soil import Simulation
|
||||
from _config import *
|
||||
|
||||
|
||||
class NoopAgent(Agent):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.num_calls = 0
|
||||
|
||||
def step(self):
|
||||
# import pdb;pdb.set_trace()
|
||||
self.num_calls += 1
|
||||
|
||||
|
||||
class NoopModel(Model):
|
||||
def __init__(self, num_agents, *args, **kwargs):
|
||||
super().__init__()
|
||||
self.schedule = RandomActivation(self)
|
||||
for i in range(num_agents):
|
||||
self.schedule.add(NoopAgent(self.next_id(), self))
|
||||
self.datacollector = DataCollector(model_reporters={"num_agents": lambda m: m.schedule.get_agent_count(),
|
||||
"time": lambda m: m.schedule.time},
|
||||
agent_reporters={"num_calls": "num_calls"})
|
||||
self.datacollector.collect(self)
|
||||
|
||||
def step(self):
|
||||
self.schedule.step()
|
||||
self.datacollector.collect(self)
|
||||
|
||||
|
||||
def run():
|
||||
run_sim(model=NoopModel)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
3
benchmarks/noop/noop-bench.csv
Normal file
3
benchmarks/noop/noop-bench.csv
Normal file
@@ -0,0 +1,3 @@
|
||||
command,mean,stddev,median,user,system,min,max,parameter_sim
|
||||
python mesa1_batchrunner.py,1.2932078178200002,0.05649377020829272,1.2705532802200001,1.25902256,0.27242284,1.22210926572,1.40867459172,mesa1_batchrunner
|
||||
python mesa1_simulation.py,1.81112963812,0.015491072368938567,1.81342524572,1.8594407599999996,0.8005329399999999,1.78538603972,1.84176361172,mesa1_simulation
|
|
24
benchmarks/noop/soil_async.py
Normal file
24
benchmarks/noop/soil_async.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from soil import BaseAgent, Environment, Simulation
|
||||
|
||||
|
||||
class NoopAgent(BaseAgent):
|
||||
num_calls = 0
|
||||
|
||||
async def step(self):
|
||||
while True:
|
||||
self.num_calls += 1
|
||||
await self.delay()
|
||||
|
||||
|
||||
class NoopEnvironment(Environment):
|
||||
num_agents = 100
|
||||
|
||||
def init(self):
|
||||
self.add_agents(NoopAgent, k=self.num_agents)
|
||||
self.add_agent_reporter("num_calls")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from _config import *
|
||||
|
||||
run_sim(model=NoopEnvironment)
|
25
benchmarks/noop/soil_async_pqueue.py
Normal file
25
benchmarks/noop/soil_async_pqueue.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from soil import BaseAgent, Environment, Simulation, PQueueActivation
|
||||
|
||||
|
||||
class NoopAgent(BaseAgent):
|
||||
num_calls = 0
|
||||
|
||||
async def step(self):
|
||||
while True:
|
||||
self.num_calls += 1
|
||||
await self.delay()
|
||||
|
||||
|
||||
class NoopEnvironment(Environment):
|
||||
num_agents = 100
|
||||
schedule_class = PQueueActivation
|
||||
|
||||
def init(self):
|
||||
self.add_agents(NoopAgent, k=self.num_agents)
|
||||
self.add_agent_reporter("num_calls")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from _config import *
|
||||
|
||||
run_sim(model=NoopEnvironment)
|
24
benchmarks/noop/soil_gens.py
Normal file
24
benchmarks/noop/soil_gens.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from soil import BaseAgent, Environment, Simulation
|
||||
|
||||
|
||||
class NoopAgent(BaseAgent):
|
||||
num_calls = 0
|
||||
|
||||
def step(self):
|
||||
while True:
|
||||
self.num_calls += 1
|
||||
yield self.delay()
|
||||
|
||||
|
||||
class NoopEnvironment(Environment):
|
||||
num_agents = 100
|
||||
|
||||
def init(self):
|
||||
self.add_agents(NoopAgent, k=self.num_agents)
|
||||
self.add_agent_reporter("num_calls")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from _config import *
|
||||
|
||||
run_sim(model=NoopEnvironment)
|
25
benchmarks/noop/soil_gens_pqueue.py
Normal file
25
benchmarks/noop/soil_gens_pqueue.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from soil import BaseAgent, Environment, Simulation, PQueueActivation
|
||||
|
||||
|
||||
class NoopAgent(BaseAgent):
|
||||
num_calls = 0
|
||||
|
||||
def step(self):
|
||||
while True:
|
||||
self.num_calls += 1
|
||||
yield self.delay()
|
||||
|
||||
|
||||
class NoopEnvironment(Environment):
|
||||
num_agents = 100
|
||||
schedule_class = PQueueActivation
|
||||
|
||||
def init(self):
|
||||
self.add_agents(NoopAgent, k=self.num_agents)
|
||||
self.add_agent_reporter("num_calls")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from _config import *
|
||||
|
||||
run_sim(model=NoopEnvironment)
|
21
benchmarks/noop/soil_step.py
Normal file
21
benchmarks/noop/soil_step.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from soil import BaseAgent, Environment, Simulation
|
||||
|
||||
|
||||
class NoopAgent(BaseAgent):
|
||||
num_calls = 0
|
||||
|
||||
def step(self):
|
||||
self.num_calls += 1
|
||||
|
||||
class NoopEnvironment(Environment):
|
||||
num_agents = 100
|
||||
|
||||
def init(self):
|
||||
self.add_agents(NoopAgent, k=self.num_agents)
|
||||
self.add_agent_reporter("num_calls")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from _config import *
|
||||
|
||||
run_sim(model=NoopEnvironment)
|
22
benchmarks/noop/soil_step_pqueue.py
Normal file
22
benchmarks/noop/soil_step_pqueue.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from soil import BaseAgent, Environment, Simulation, PQueueActivation
|
||||
|
||||
|
||||
class NoopAgent(BaseAgent):
|
||||
num_calls = 0
|
||||
|
||||
def step(self):
|
||||
self.num_calls += 1
|
||||
|
||||
class NoopEnvironment(Environment):
|
||||
num_agents = 100
|
||||
schedule_class = PQueueActivation
|
||||
|
||||
def init(self):
|
||||
self.add_agents(NoopAgent, k=self.num_agents)
|
||||
self.add_agent_reporter("num_calls")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from _config import *
|
||||
|
||||
run_sim(model=NoopEnvironment)
|
29
benchmarks/noop/soilent_async.py
Normal file
29
benchmarks/noop/soilent_async.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from soil import Agent, Environment, Simulation
|
||||
from soilent import Scheduler
|
||||
|
||||
|
||||
class NoopAgent(Agent):
|
||||
num_calls = 0
|
||||
|
||||
async def step(self):
|
||||
while True:
|
||||
self.num_calls += 1
|
||||
# yield self.delay(1)
|
||||
await self.delay()
|
||||
|
||||
|
||||
class NoopEnvironment(Environment):
|
||||
num_agents = 100
|
||||
schedule_class = Scheduler
|
||||
|
||||
def init(self):
|
||||
self.add_agents(NoopAgent, k=self.num_agents)
|
||||
self.add_agent_reporter("num_calls")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from _config import *
|
||||
|
||||
res = run_sim(model=NoopEnvironment)
|
||||
for r in res:
|
||||
assert isinstance(r.schedule, Scheduler)
|
27
benchmarks/noop/soilent_async_pqueue.py
Normal file
27
benchmarks/noop/soilent_async_pqueue.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from soil import Agent, Environment
|
||||
from soilent import PQueueScheduler
|
||||
|
||||
|
||||
class NoopAgent(Agent):
|
||||
num_calls = 0
|
||||
|
||||
async def step(self):
|
||||
while True:
|
||||
self.num_calls += 1
|
||||
await self.delay()
|
||||
|
||||
class NoopEnvironment(Environment):
|
||||
num_agents = 100
|
||||
schedule_class = PQueueScheduler
|
||||
|
||||
def init(self):
|
||||
self.add_agents(NoopAgent, k=self.num_agents)
|
||||
self.add_agent_reporter("num_calls")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from _config import *
|
||||
|
||||
res = run_sim(model=NoopEnvironment)
|
||||
for r in res:
|
||||
assert isinstance(r.schedule, PQueueScheduler)
|
28
benchmarks/noop/soilent_gens.py
Normal file
28
benchmarks/noop/soilent_gens.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from soil import Agent, Environment, Simulation
|
||||
from soilent import Scheduler
|
||||
|
||||
|
||||
class NoopAgent(Agent):
|
||||
num_calls = 0
|
||||
|
||||
def step(self):
|
||||
while True:
|
||||
self.num_calls += 1
|
||||
# yield self.delay(1)
|
||||
yield self.delay()
|
||||
|
||||
class NoopEnvironment(Environment):
|
||||
num_agents = 100
|
||||
schedule_class = Scheduler
|
||||
|
||||
def init(self):
|
||||
self.add_agents(NoopAgent, k=self.num_agents)
|
||||
self.add_agent_reporter("num_calls")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from _config import *
|
||||
|
||||
res = run_sim(model=NoopEnvironment)
|
||||
for r in res:
|
||||
assert isinstance(r.schedule, Scheduler)
|
28
benchmarks/noop/soilent_gens_pqueue.py
Normal file
28
benchmarks/noop/soilent_gens_pqueue.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from soil import Agent, Environment
|
||||
from soilent import PQueueScheduler
|
||||
|
||||
|
||||
class NoopAgent(Agent):
|
||||
num_calls = 0
|
||||
|
||||
def step(self):
|
||||
while True:
|
||||
self.num_calls += 1
|
||||
# yield self.delay(1)
|
||||
yield
|
||||
|
||||
class NoopEnvironment(Environment):
|
||||
num_agents = 100
|
||||
schedule_class = PQueueScheduler
|
||||
|
||||
def init(self):
|
||||
self.add_agents(NoopAgent, k=self.num_agents)
|
||||
self.add_agent_reporter("num_calls")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from _config import *
|
||||
|
||||
res = run_sim(model=NoopEnvironment)
|
||||
for r in res:
|
||||
assert isinstance(r.schedule, PQueueScheduler)
|
24
benchmarks/noop/soilent_step.py
Normal file
24
benchmarks/noop/soilent_step.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from soil import BaseAgent, Environment, Simulation
|
||||
from soilent import Scheduler
|
||||
|
||||
|
||||
class NoopAgent(BaseAgent):
|
||||
num_calls = 0
|
||||
|
||||
def step(self):
|
||||
self.num_calls += 1
|
||||
|
||||
class NoopEnvironment(Environment):
|
||||
num_agents = 100
|
||||
schedule_class = Scheduler
|
||||
|
||||
def init(self):
|
||||
self.add_agents(NoopAgent, k=self.num_agents)
|
||||
self.add_agent_reporter("num_calls")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from _config import *
|
||||
res = run_sim(model=NoopEnvironment)
|
||||
for r in res:
|
||||
assert isinstance(r.schedule, Scheduler)
|
24
benchmarks/noop/soilent_step_pqueue.py
Normal file
24
benchmarks/noop/soilent_step_pqueue.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from soil import BaseAgent, Environment, Simulation
|
||||
from soilent import PQueueScheduler
|
||||
|
||||
|
||||
class NoopAgent(BaseAgent):
|
||||
num_calls = 0
|
||||
|
||||
def step(self):
|
||||
self.num_calls += 1
|
||||
|
||||
class NoopEnvironment(Environment):
|
||||
num_agents = 100
|
||||
schedule_class = PQueueScheduler
|
||||
|
||||
def init(self):
|
||||
self.add_agents(NoopAgent, k=self.num_agents)
|
||||
self.add_agent_reporter("num_calls")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from _config import *
|
||||
res = run_sim(model=NoopEnvironment)
|
||||
for r in res:
|
||||
assert isinstance(r.schedule, PQueueScheduler)
|
19
benchmarks/run.py
Executable file
19
benchmarks/run.py
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/bin/env python
|
||||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser(
|
||||
prog='Profiler for soil')
|
||||
parser.add_argument('--suffix', default=None)
|
||||
parser.add_argument('files', nargs="+")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
for fname in args.files:
|
||||
suffix = ("_" + args.suffix) if args.suffix else ""
|
||||
simname = f"{fname.replace('/', '-')}{suffix}"
|
||||
profpath = os.path.join("profs", simname + ".prof")
|
||||
|
||||
print(f"Running {fname} and saving profile to {profpath}")
|
||||
subprocess.call(["python", "-m", "cProfile", "-o", profpath, fname])
|
4
benchmarks/virusonnetwork.csv
Normal file
4
benchmarks/virusonnetwork.csv
Normal file
@@ -0,0 +1,4 @@
|
||||
command,mean,stddev,median,user,system,min,max,parameter_sim
|
||||
python virusonnetwork/mesa_basic.py,3.8381473157,0.0518143371442526,3.8475315791,3.873109219999999,0.55102658,3.7523016936,3.9095182436,mesa_basic.py
|
||||
python virusonnetwork/soil_step.py,3.2167258977000004,0.02337131987357665,3.2257620261,3.28374132,0.51343958,3.1792271306,3.2511521286000002,soil_step.py
|
||||
python virusonnetwork/soil_states.py,3.4908183217,0.03726734070349347,3.4912775086,3.5684004200000006,0.50416068,3.4272087936,3.5529207346000002,soil_states.py
|
|
32
benchmarks/virusonnetwork/_config.py
Normal file
32
benchmarks/virusonnetwork/_config.py
Normal file
@@ -0,0 +1,32 @@
|
||||
import os
|
||||
|
||||
NUM_AGENTS = int(os.environ.get('NUM_AGENTS', 100))
|
||||
NUM_ITERS = int(os.environ.get('NUM_ITERS', 10))
|
||||
MAX_STEPS = int(os.environ.get('MAX_STEPS', 1000))
|
||||
|
||||
|
||||
def run_sim(model, **kwargs):
|
||||
from soil import Simulation
|
||||
opts = dict(model=model,
|
||||
dump=False,
|
||||
num_processes=1,
|
||||
parameters={'num_nodes': NUM_AGENTS,
|
||||
"avg_node_degree": 3,
|
||||
"initial_outbreak_size": 5,
|
||||
"virus_spread_chance": 0.25,
|
||||
"virus_check_frequency": 0.25,
|
||||
"recovery_chance": 0.3,
|
||||
"gain_resistance_chance": 0.1,
|
||||
},
|
||||
max_steps=MAX_STEPS,
|
||||
iterations=NUM_ITERS)
|
||||
opts.update(kwargs)
|
||||
its = Simulation(**opts).run()
|
||||
|
||||
assert all(it.schedule.steps == MAX_STEPS for it in its)
|
||||
ratios = list(it.resistant_susceptible_ratio() for it in its)
|
||||
print("Max - Avg - Min ratio:", max(ratios), sum(ratios)/len(ratios), min(ratios))
|
||||
assert all(sum([it.number_susceptible,
|
||||
it.number_infected,
|
||||
it.number_resistant]) == NUM_AGENTS for it in its)
|
||||
return its
|
180
benchmarks/virusonnetwork/mesa_basic.py
Normal file
180
benchmarks/virusonnetwork/mesa_basic.py
Normal file
@@ -0,0 +1,180 @@
|
||||
# Verbatim copy from mesa
|
||||
# https://github.com/projectmesa/mesa/blob/976ddfc8a1e5feaaf8007a7abaa9abc7093881a0/examples/virus_on_network/virus_on_network/model.py
|
||||
import math
|
||||
from enum import Enum
|
||||
import networkx as nx
|
||||
|
||||
import mesa
|
||||
|
||||
|
||||
class State(Enum):
|
||||
SUSCEPTIBLE = 0
|
||||
INFECTED = 1
|
||||
RESISTANT = 2
|
||||
|
||||
|
||||
def number_state(model, state):
|
||||
return sum(1 for a in model.grid.get_all_cell_contents() if a.state is state)
|
||||
|
||||
|
||||
def number_infected(model):
|
||||
return number_state(model, State.INFECTED)
|
||||
|
||||
|
||||
def number_susceptible(model):
|
||||
return number_state(model, State.SUSCEPTIBLE)
|
||||
|
||||
|
||||
def number_resistant(model):
|
||||
return number_state(model, State.RESISTANT)
|
||||
|
||||
|
||||
class VirusOnNetwork(mesa.Model):
|
||||
"""A virus model with some number of agents"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
num_nodes=10,
|
||||
avg_node_degree=3,
|
||||
initial_outbreak_size=1,
|
||||
virus_spread_chance=0.4,
|
||||
virus_check_frequency=0.4,
|
||||
recovery_chance=0.3,
|
||||
gain_resistance_chance=0.5,
|
||||
**kwargs,
|
||||
):
|
||||
|
||||
self.num_nodes = num_nodes
|
||||
prob = avg_node_degree / self.num_nodes
|
||||
self.G = nx.erdos_renyi_graph(n=self.num_nodes, p=prob)
|
||||
self.grid = mesa.space.NetworkGrid(self.G)
|
||||
self.schedule = mesa.time.RandomActivation(self)
|
||||
self.initial_outbreak_size = (
|
||||
initial_outbreak_size if initial_outbreak_size <= num_nodes else num_nodes
|
||||
)
|
||||
self.virus_spread_chance = virus_spread_chance
|
||||
self.virus_check_frequency = virus_check_frequency
|
||||
self.recovery_chance = recovery_chance
|
||||
self.gain_resistance_chance = gain_resistance_chance
|
||||
|
||||
self.datacollector = mesa.DataCollector(
|
||||
{
|
||||
"Ratio": "resistant_susceptible_ratio",
|
||||
"Infected": number_infected,
|
||||
"Susceptible": number_susceptible,
|
||||
"Resistant": number_resistant,
|
||||
}
|
||||
)
|
||||
|
||||
# Create agents
|
||||
for i, node in enumerate(self.G.nodes()):
|
||||
a = VirusAgent(
|
||||
i,
|
||||
self,
|
||||
State.SUSCEPTIBLE,
|
||||
self.virus_spread_chance,
|
||||
self.virus_check_frequency,
|
||||
self.recovery_chance,
|
||||
self.gain_resistance_chance,
|
||||
)
|
||||
self.schedule.add(a)
|
||||
# Add the agent to the node
|
||||
self.grid.place_agent(a, node)
|
||||
|
||||
# Infect some nodes
|
||||
infected_nodes = self.random.sample(list(self.G), self.initial_outbreak_size)
|
||||
for a in self.grid.get_cell_list_contents(infected_nodes):
|
||||
a.state = State.INFECTED
|
||||
|
||||
self.running = True
|
||||
self.datacollector.collect(self)
|
||||
|
||||
@property
|
||||
def number_susceptible(self):
|
||||
return number_susceptible(self)
|
||||
@property
|
||||
def number_resistant(self):
|
||||
return number_resistant(self)
|
||||
@property
|
||||
def number_infected(self):
|
||||
return number_infected(self)
|
||||
|
||||
def resistant_susceptible_ratio(self):
|
||||
try:
|
||||
return number_state(self, State.RESISTANT) / number_state(
|
||||
self, State.SUSCEPTIBLE
|
||||
)
|
||||
except ZeroDivisionError:
|
||||
return math.inf
|
||||
|
||||
def step(self):
|
||||
self.schedule.step()
|
||||
# collect data
|
||||
self.datacollector.collect(self)
|
||||
|
||||
def run_model(self, n):
|
||||
for i in range(n):
|
||||
self.step()
|
||||
|
||||
|
||||
class VirusAgent(mesa.Agent):
|
||||
def __init__(
|
||||
self,
|
||||
unique_id,
|
||||
model,
|
||||
initial_state,
|
||||
virus_spread_chance,
|
||||
virus_check_frequency,
|
||||
recovery_chance,
|
||||
gain_resistance_chance,
|
||||
):
|
||||
super().__init__(unique_id, model)
|
||||
|
||||
self.state = initial_state
|
||||
|
||||
self.virus_spread_chance = virus_spread_chance
|
||||
self.virus_check_frequency = virus_check_frequency
|
||||
self.recovery_chance = recovery_chance
|
||||
self.gain_resistance_chance = gain_resistance_chance
|
||||
|
||||
def try_to_infect_neighbors(self):
|
||||
neighbors_nodes = self.model.grid.get_neighbors(self.pos, include_center=False)
|
||||
susceptible_neighbors = [
|
||||
agent
|
||||
for agent in self.model.grid.get_cell_list_contents(neighbors_nodes)
|
||||
if agent.state is State.SUSCEPTIBLE
|
||||
]
|
||||
for a in susceptible_neighbors:
|
||||
if self.random.random() < self.virus_spread_chance:
|
||||
a.state = State.INFECTED
|
||||
|
||||
def try_gain_resistance(self):
|
||||
if self.random.random() < self.gain_resistance_chance:
|
||||
self.state = State.RESISTANT
|
||||
|
||||
def try_remove_infection(self):
|
||||
# Try to remove
|
||||
if self.random.random() < self.recovery_chance:
|
||||
# Success
|
||||
self.state = State.SUSCEPTIBLE
|
||||
self.try_gain_resistance()
|
||||
else:
|
||||
# Failed
|
||||
self.state = State.INFECTED
|
||||
|
||||
def try_check_situation(self):
|
||||
if self.random.random() < self.virus_check_frequency:
|
||||
# Checking...
|
||||
if self.state is State.INFECTED:
|
||||
self.try_remove_infection()
|
||||
|
||||
def step(self):
|
||||
if self.state is State.INFECTED:
|
||||
self.try_to_infect_neighbors()
|
||||
self.try_check_situation()
|
||||
|
||||
|
||||
from _config import run_sim
|
||||
|
||||
run_sim(model=VirusOnNetwork)
|
92
benchmarks/virusonnetwork/soil_states.py
Normal file
92
benchmarks/virusonnetwork/soil_states.py
Normal file
@@ -0,0 +1,92 @@
|
||||
# Verbatim copy from mesa
|
||||
# https://github.com/projectmesa/mesa/blob/976ddfc8a1e5feaaf8007a7abaa9abc7093881a0/examples/virus_on_network/virus_on_network/model.py
|
||||
import math
|
||||
from enum import Enum
|
||||
import networkx as nx
|
||||
|
||||
from soil import *
|
||||
|
||||
|
||||
class VirusOnNetwork(Environment):
|
||||
"""A virus model with some number of agents"""
|
||||
num_nodes = 10
|
||||
avg_node_degree = 3
|
||||
initial_outbreak_size = 1
|
||||
virus_spread_chance = 0.4
|
||||
virus_check_frequency = 0.4
|
||||
recovery_chance = 0
|
||||
gain_resistance_chance = 0
|
||||
|
||||
def init(self):
|
||||
prob = self.avg_node_degree / self.num_nodes
|
||||
# Use internal seed with the networkx generator
|
||||
self.create_network(generator=nx.erdos_renyi_graph, n=self.num_nodes, p=prob)
|
||||
|
||||
self.initial_outbreak_size = min(self.initial_outbreak_size, self.num_nodes)
|
||||
self.populate_network(VirusAgent)
|
||||
|
||||
# Infect some nodes
|
||||
infected_nodes = self.random.sample(list(self.G), self.initial_outbreak_size)
|
||||
for a in self.get_agents(node_id=infected_nodes):
|
||||
a.set_state(VirusAgent.infected)
|
||||
assert self.number_infected == self.initial_outbreak_size
|
||||
|
||||
@report
|
||||
def resistant_susceptible_ratio(self):
|
||||
try:
|
||||
return self.number_resistant / self.number_susceptible
|
||||
except ZeroDivisionError:
|
||||
return math.inf
|
||||
|
||||
@report
|
||||
@property
|
||||
def number_infected(self):
|
||||
return self.count_agents(state_id=VirusAgent.infected.id)
|
||||
|
||||
@report
|
||||
@property
|
||||
def number_susceptible(self):
|
||||
return self.count_agents(state_id=VirusAgent.susceptible.id)
|
||||
|
||||
@report
|
||||
@property
|
||||
def number_resistant(self):
|
||||
return self.count_agents(state_id=VirusAgent.resistant.id)
|
||||
|
||||
|
||||
class VirusAgent(Agent):
|
||||
virus_spread_chance = None # Inherit from model
|
||||
virus_check_frequency = None # Inherit from model
|
||||
recovery_chance = None # Inherit from model
|
||||
gain_resistance_chance = None # Inherit from model
|
||||
just_been_infected = False
|
||||
|
||||
@state(default=True)
|
||||
def susceptible(self):
|
||||
if self.just_been_infected:
|
||||
self.just_been_infected = False
|
||||
return self.infected
|
||||
|
||||
@state
|
||||
def infected(self):
|
||||
susceptible_neighbors = self.get_neighbors(state_id=self.susceptible.id)
|
||||
for a in susceptible_neighbors:
|
||||
if self.prob(self.virus_spread_chance):
|
||||
a.just_been_infected = True
|
||||
if self.prob(self.virus_check_frequency):
|
||||
if self.prob(self.recovery_chance):
|
||||
if self.prob(self.gain_resistance_chance):
|
||||
return self.resistant
|
||||
else:
|
||||
return self.susceptible
|
||||
else:
|
||||
return self.infected
|
||||
|
||||
@state
|
||||
def resistant(self):
|
||||
return self.at(INFINITY)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from _config import run_sim
|
||||
run_sim(model=VirusOnNetwork)
|
104
benchmarks/virusonnetwork/soil_step.py
Normal file
104
benchmarks/virusonnetwork/soil_step.py
Normal file
@@ -0,0 +1,104 @@
|
||||
# Verbatim copy from mesa
|
||||
# https://github.com/projectmesa/mesa/blob/976ddfc8a1e5feaaf8007a7abaa9abc7093881a0/examples/virus_on_network/virus_on_network/model.py
|
||||
import math
|
||||
from enum import Enum
|
||||
import networkx as nx
|
||||
|
||||
from soil import *
|
||||
|
||||
|
||||
class State(Enum):
|
||||
SUSCEPTIBLE = 0
|
||||
INFECTED = 1
|
||||
RESISTANT = 2
|
||||
|
||||
|
||||
class VirusOnNetwork(Environment):
|
||||
"""A virus model with some number of agents"""
|
||||
num_nodes = 10
|
||||
avg_node_degree = 3
|
||||
initial_outbreak_size = 1
|
||||
virus_spread_chance = 0.4
|
||||
virus_check_frequency = 0.4
|
||||
recovery_chance = 0
|
||||
gain_resistance_chance = 0
|
||||
|
||||
def init(self):
|
||||
prob = self.avg_node_degree / self.num_nodes
|
||||
# Use internal seed with the networkx generator
|
||||
self.create_network(generator=nx.erdos_renyi_graph, n=self.num_nodes, p=prob)
|
||||
|
||||
self.initial_outbreak_size = min(self.initial_outbreak_size, self.num_nodes)
|
||||
self.populate_network(VirusAgent)
|
||||
|
||||
# Infect some nodes
|
||||
infected_nodes = self.random.sample(list(self.G), self.initial_outbreak_size)
|
||||
for a in self.get_agents(node_id=infected_nodes):
|
||||
a.status = State.INFECTED
|
||||
assert self.number_infected == self.initial_outbreak_size
|
||||
|
||||
@report
|
||||
def resistant_susceptible_ratio(self):
|
||||
try:
|
||||
return self.number_resistant / self.number_susceptible
|
||||
except ZeroDivisionError:
|
||||
return math.inf
|
||||
|
||||
@report
|
||||
@property
|
||||
def number_infected(self):
|
||||
return self.count_agents(status=State.INFECTED)
|
||||
|
||||
@report
|
||||
@property
|
||||
def number_susceptible(self):
|
||||
return self.count_agents(status=State.SUSCEPTIBLE)
|
||||
|
||||
@report
|
||||
@property
|
||||
def number_resistant(self):
|
||||
return self.count_agents(status=State.RESISTANT)
|
||||
|
||||
|
||||
|
||||
class VirusAgent(Agent):
|
||||
status = State.SUSCEPTIBLE
|
||||
virus_spread_chance = None # Inherit from model
|
||||
virus_check_frequency = None # Inherit from model
|
||||
recovery_chance = None # Inherit from model
|
||||
gain_resistance_chance = None # Inherit from model
|
||||
|
||||
def try_to_infect_neighbors(self):
|
||||
susceptible_neighbors = self.get_neighbors(status=State.SUSCEPTIBLE)
|
||||
for a in susceptible_neighbors:
|
||||
if self.prob(self.virus_spread_chance):
|
||||
a.status = State.INFECTED
|
||||
|
||||
def try_gain_resistance(self):
|
||||
if self.prob(self.gain_resistance_chance):
|
||||
self.status = State.RESISTANT
|
||||
return self.at(INFINITY)
|
||||
|
||||
def try_remove_infection(self):
|
||||
# Try to remove
|
||||
if self.prob(self.recovery_chance):
|
||||
# Success
|
||||
self.status = State.SUSCEPTIBLE
|
||||
return self.try_gain_resistance()
|
||||
|
||||
def try_check_situation(self):
|
||||
if self.prob(self.virus_check_frequency):
|
||||
# Checking...
|
||||
if self.status is State.INFECTED:
|
||||
return self.try_remove_infection()
|
||||
|
||||
def step(self):
|
||||
if self.status is State.INFECTED:
|
||||
self.try_to_infect_neighbors()
|
||||
return self.try_check_situation()
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from _config import run_sim
|
||||
run_sim(model=VirusOnNetwork)
|
10
docs/conf.py
10
docs/conf.py
@@ -31,7 +31,10 @@
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = ['IPython.sphinxext.ipython_console_highlighting']
|
||||
extensions = [
|
||||
"IPython.sphinxext.ipython_console_highlighting",
|
||||
"nbsphinx",
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
@@ -64,7 +67,7 @@ release = '0.1'
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = None
|
||||
language = "en"
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
@@ -152,6 +155,3 @@ texinfo_documents = [
|
||||
author, 'Soil', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
|
||||
|
||||
|
@@ -1,40 +0,0 @@
|
||||
---
|
||||
name: MyExampleSimulation
|
||||
max_time: 50
|
||||
num_trials: 3
|
||||
interval: 2
|
||||
model_params:
|
||||
topology:
|
||||
params:
|
||||
generator: barabasi_albert_graph
|
||||
n: 100
|
||||
m: 2
|
||||
agents:
|
||||
distribution:
|
||||
- agent_class: SISaModel
|
||||
topology: True
|
||||
ratio: 0.1
|
||||
state:
|
||||
state_id: content
|
||||
- agent_class: SISaModel
|
||||
topology: True
|
||||
ratio: .1
|
||||
state:
|
||||
state_id: discontent
|
||||
- agent_class: SISaModel
|
||||
topology: True
|
||||
ratio: 0.8
|
||||
state:
|
||||
state_id: neutral
|
||||
prob_infect: 0.075
|
||||
neutral_discontent_spon_prob: 0.1
|
||||
neutral_discontent_infected_prob: 0.3
|
||||
neutral_content_spon_prob: 0.3
|
||||
neutral_content_infected_prob: 0.4
|
||||
discontent_neutral: 0.5
|
||||
discontent_content: 0.5
|
||||
variance_d_c: 0.2
|
||||
content_discontent: 0.2
|
||||
variance_c_d: 0.2
|
||||
content_neutral: 0.2
|
||||
standard_variance: 1
|
@@ -2,19 +2,20 @@ Welcome to Soil's documentation!
|
||||
================================
|
||||
|
||||
Soil is an opinionated Agent-based Social Simulator in Python focused on Social Networks.
|
||||
To get started developing your own simulations and agent behaviors, check out our :doc:`Tutorial <tutorial/soil_tutorial>` and the `examples on GitHub <https://github.com/gsi-upm/soil/tree/master/examples>`.
|
||||
|
||||
Soil can be installed through pip (see more details in the :doc:`installation` page):.
|
||||
|
||||
.. image:: soil.png
|
||||
:width: 80%
|
||||
:align: center
|
||||
|
||||
Soil can be installed through pip (see more details in the :doc:`installation` page):
|
||||
|
||||
.. code:: bash
|
||||
|
||||
pip install soil
|
||||
|
||||
|
||||
To get started developing your own simulations and agent behaviors, check out our :doc:`Tutorial <soil_tutorial>` and the `examples on GitHub <https://github.com/gsi-upm/soil/tree/master/examples>.
|
||||
|
||||
If you use Soil in your research, do not forget to cite this paper:
|
||||
|
||||
@@ -46,7 +47,9 @@ If you use Soil in your research, do not forget to cite this paper:
|
||||
:caption: Learn more about soil:
|
||||
|
||||
installation
|
||||
Tutorial <soil_tutorial>
|
||||
Tutorial <tutorial/soil_tutorial>
|
||||
notes_v1.0
|
||||
soil-vs
|
||||
|
||||
..
|
||||
|
||||
|
@@ -1,22 +0,0 @@
|
||||
Mesa compatibility
|
||||
------------------
|
||||
|
||||
Soil is in the process of becoming fully compatible with MESA.
|
||||
The idea is to provide a set of modular classes and functions that extend the functionality of mesa, whilst staying compatible.
|
||||
In the end, it should be possible to add regular mesa agents to a soil simulation, or use a soil agent within a mesa simulation/model.
|
||||
|
||||
This is a non-exhaustive list of tasks to achieve compatibility:
|
||||
|
||||
- [ ] Integrate `soil.Simulation` with mesa's runners:
|
||||
- [ ] `soil.Simulation` could mimic/become a `mesa.batchrunner`
|
||||
- [ ] Integrate `soil.Environment` with `mesa.Model`:
|
||||
- [x] `Soil.Environment` inherits from `mesa.Model`
|
||||
- [x] `Soil.Environment` includes a Mesa-like Scheduler (see the `soil.time` module.
|
||||
- [ ] Allow for `mesa.Model` to be used in a simulation.
|
||||
- [ ] Integrate `soil.Agent` with `mesa.Agent`:
|
||||
- [x] Rename agent.id to unique_id?
|
||||
- [x] mesa agents can be used in soil simulations (see `examples/mesa`)
|
||||
- [ ] Provide examples
|
||||
- [ ] Using mesa modules in a soil simulation
|
||||
- [ ] Using soil modules in a mesa simulation
|
||||
- [ ] Document the new APIs and usage
|
@@ -1,3 +1,6 @@
|
||||
Upgrading to Soil 1.0
|
||||
---------------------
|
||||
|
||||
What are the main changes in version 1.0?
|
||||
#########################################
|
||||
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 4.7 KiB |
Binary file not shown.
Before Width: | Height: | Size: 11 KiB |
Binary file not shown.
Before Width: | Height: | Size: 26 KiB |
Binary file not shown.
Before Width: | Height: | Size: 31 KiB |
@@ -1 +1,2 @@
|
||||
ipython>=7.31.1
|
||||
nbsphinx==0.9.1
|
||||
|
@@ -1,4 +1,8 @@
|
||||
### MESA
|
||||
Soil vs other ABM frameworks
|
||||
============================
|
||||
|
||||
MESA
|
||||
----
|
||||
|
||||
Starting with version 0.3, Soil has been redesigned to complement Mesa, while remaining compatible with it.
|
||||
That means that every component in Soil (i.e., Models, Environments, etc.) can be mixed with existing mesa components.
|
||||
@@ -10,3 +14,42 @@ Here are some reasons to use Soil instead of plain mesa:
|
||||
- Functions to automatically populate a topology with an agent distribution (i.e., different ratios of agent class and state)
|
||||
- The `soil.Simulation` class allows you to run multiple instances of the same experiment (i.e., multiple trials with the same parameters but a different randomness seed)
|
||||
- Reporting functions that aggregate multiple
|
||||
|
||||
Mesa compatibility
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Soil is in the process of becoming fully compatible with MESA.
|
||||
The idea is to provide a set of modular classes and functions that extend the functionality of mesa, whilst staying compatible.
|
||||
In the end, it should be possible to add regular mesa agents to a soil simulation, or use a soil agent within a mesa simulation/model.
|
||||
|
||||
This is a non-exhaustive list of tasks to achieve compatibility:
|
||||
|
||||
.. |check| raw:: html
|
||||
|
||||
☑
|
||||
|
||||
.. |uncheck| raw:: html
|
||||
|
||||
☐
|
||||
|
||||
- |check| Integrate `soil.Simulation` with mesa's runners:
|
||||
|
||||
- |check| `soil.Simulation` can replace `mesa.batchrunner`
|
||||
|
||||
- |check| Integrate `soil.Environment` with `mesa.Model`:
|
||||
|
||||
- |check| `Soil.Environment` inherits from `mesa.Model`
|
||||
- |check| `Soil.Environment` includes a Mesa-like Scheduler (see the `soil.time` module.
|
||||
- |check| Allow for `mesa.Model` to be used in a simulation.
|
||||
|
||||
- |check| Integrate `soil.Agent` with `mesa.Agent`:
|
||||
|
||||
- |check| Rename agent.id to unique_id
|
||||
- |check| mesa agents can be used in soil simulations (see `examples/mesa`)
|
||||
|
||||
- |check| Provide examples
|
||||
|
||||
- |check| Using mesa modules in a soil simulation (see `examples/mesa`)
|
||||
- |uncheck| Using soil modules in a mesa simulation (see `examples/mesa`)
|
||||
|
||||
- |uncheck| Document the new APIs and usage
|
File diff suppressed because it is too large
Load Diff
2853
docs/tutorial/soil_tutorial.ipynb
Normal file
2853
docs/tutorial/soil_tutorial.ipynb
Normal file
File diff suppressed because one or more lines are too long
1
examples/README.md
Normal file
1
examples/README.md
Normal file
@@ -0,0 +1 @@
|
||||
Some of these examples are close to real life simulations, whereas some others are only a demonstration of Soil's capatibilities.
|
@@ -63,11 +63,11 @@ class City(EventedEnvironment):
|
||||
|
||||
def init(self):
|
||||
self.grid = MultiGrid(width=self.width, height=self.height, torus=False)
|
||||
if not self.agents:
|
||||
if not self.get_agents():
|
||||
self.add_agents(Driver, k=self.n_cars)
|
||||
self.add_agents(Passenger, k=self.n_passengers)
|
||||
|
||||
for agent in self.agents:
|
||||
for agent in self.get_agents():
|
||||
self.grid.place_agent(agent, (0, 0))
|
||||
self.grid.move_to_empty(agent)
|
||||
|
||||
@@ -86,7 +86,7 @@ class Driver(Evented, FSM):
|
||||
earnings = 0
|
||||
|
||||
def on_receive(self, msg, sender):
|
||||
"""This is not a state. It will run (and block) every time check_messages is invoked"""
|
||||
"""This is not a state. It will run (and block) every time process_messages is invoked"""
|
||||
if self.journey is None and isinstance(msg, Journey) and msg.driver is None:
|
||||
msg.driver = self
|
||||
self.journey = msg
|
||||
@@ -95,15 +95,14 @@ class Driver(Evented, FSM):
|
||||
"""If there are no more passengers, stop forever"""
|
||||
c = self.count_agents(agent_class=Passenger)
|
||||
self.debug(f"Passengers left {c}")
|
||||
if not c:
|
||||
self.die("No more passengers")
|
||||
return c
|
||||
|
||||
@default_state
|
||||
@state
|
||||
def wandering(self):
|
||||
@state(default=True)
|
||||
async def wandering(self):
|
||||
"""Move around the city until a journey is accepted"""
|
||||
target = None
|
||||
self.check_passengers()
|
||||
if not self.check_passengers():
|
||||
return self.die("No passengers left")
|
||||
self.journey = None
|
||||
while self.journey is None: # No potential journeys detected (see on_receive)
|
||||
if target is None or not self.move_towards(target):
|
||||
@@ -111,14 +110,15 @@ class Driver(Evented, FSM):
|
||||
self.model.grid.get_neighborhood(self.pos, moore=False)
|
||||
)
|
||||
|
||||
self.check_passengers()
|
||||
if not self.check_passengers():
|
||||
return self.die("No passengers left")
|
||||
# This will call on_receive behind the scenes, and the agent's status will be updated
|
||||
self.check_messages()
|
||||
yield Delta(30) # Wait at least 30 seconds before checking again
|
||||
self.process_messages()
|
||||
await self.delay(30) # Wait at least 30 seconds before checking again
|
||||
|
||||
try:
|
||||
# Re-send the journey to the passenger, to confirm that we have been selected
|
||||
self.journey = yield self.journey.passenger.ask(self.journey, timeout=60)
|
||||
self.journey = await self.journey.passenger.ask(self.journey, timeout=60, delay=5)
|
||||
except events.TimedOut:
|
||||
# No journey has been accepted. Try again
|
||||
self.journey = None
|
||||
@@ -127,18 +127,19 @@ class Driver(Evented, FSM):
|
||||
return self.driving
|
||||
|
||||
@state
|
||||
def driving(self):
|
||||
async def driving(self):
|
||||
"""The journey has been accepted. Pick them up and take them to their destination"""
|
||||
self.info(f"Driving towards Passenger {self.journey.passenger.unique_id}")
|
||||
while self.move_towards(self.journey.origin):
|
||||
yield
|
||||
await self.delay()
|
||||
self.info(f"Driving {self.journey.passenger.unique_id} from {self.journey.origin} to {self.journey.destination}")
|
||||
while self.move_towards(self.journey.destination, with_passenger=True):
|
||||
yield
|
||||
await self.delay()
|
||||
self.info("Arrived at destination")
|
||||
self.earnings += self.journey.tip
|
||||
self.model.total_earnings += self.journey.tip
|
||||
self.check_passengers()
|
||||
if not self.check_passengers():
|
||||
return self.die("No passengers left")
|
||||
return self.wandering
|
||||
|
||||
def move_towards(self, target, with_passenger=False):
|
||||
@@ -167,7 +168,7 @@ class Passenger(Evented, FSM):
|
||||
pos = None
|
||||
|
||||
def on_receive(self, msg, sender):
|
||||
"""This is not a state. It will be run synchronously every time `check_messages` is run"""
|
||||
"""This is not a state. It will be run synchronously every time `process_messages` is run"""
|
||||
|
||||
if isinstance(msg, Journey):
|
||||
self.journey = msg
|
||||
@@ -175,7 +176,7 @@ class Passenger(Evented, FSM):
|
||||
|
||||
@default_state
|
||||
@state
|
||||
def asking(self):
|
||||
async def asking(self):
|
||||
destination = (
|
||||
self.random.randint(0, self.model.grid.height-1),
|
||||
self.random.randint(0, self.model.grid.width-1),
|
||||
@@ -195,9 +196,9 @@ class Passenger(Evented, FSM):
|
||||
while not self.journey:
|
||||
self.debug(f"Waiting for responses at: { self.pos }")
|
||||
try:
|
||||
# This will call check_messages behind the scenes, and the agent's status will be updated
|
||||
# This will call process_messages behind the scenes, and the agent's status will be updated
|
||||
# If you want to avoid that, you can call it with: check=False
|
||||
yield self.received(expiration=expiration)
|
||||
await self.received(expiration=expiration, delay=10)
|
||||
except events.TimedOut:
|
||||
self.info(f"Still no response. Waiting at: { self.pos }")
|
||||
self.model.broadcast(
|
||||
@@ -208,13 +209,13 @@ class Passenger(Evented, FSM):
|
||||
return self.driving_home
|
||||
|
||||
@state
|
||||
def driving_home(self):
|
||||
async def driving_home(self):
|
||||
while (
|
||||
self.pos[0] != self.journey.destination[0]
|
||||
or self.pos[1] != self.journey.destination[1]
|
||||
):
|
||||
try:
|
||||
yield self.received(timeout=60)
|
||||
await self.received(timeout=60)
|
||||
except events.TimedOut:
|
||||
pass
|
||||
|
||||
@@ -228,4 +229,4 @@ simulation = Simulation(name="RideHailing",
|
||||
parameters=dict(n_passengers=2))
|
||||
|
||||
if __name__ == "__main__":
|
||||
easy(simulation)
|
||||
easy(simulation)
|
@@ -33,7 +33,7 @@ class GeneratorEnv(Environment):
|
||||
self.add_agents(CounterModel)
|
||||
|
||||
|
||||
sim = Simulation(model=GeneratorEnv, max_steps=10, interval=1)
|
||||
sim = Simulation(model=GeneratorEnv, max_steps=10)
|
||||
|
||||
if __name__ == '__main__':
|
||||
sim.run(dump=False)
|
0
examples/custom_timeouts/custom_timeouts.py
Normal file
0
examples/custom_timeouts/custom_timeouts.py
Normal file
@@ -1,5 +1,4 @@
|
||||
from soil.agents import FSM, state, default_state
|
||||
from soil.time import Delta
|
||||
|
||||
|
||||
class Fibonacci(FSM):
|
||||
@@ -11,17 +10,17 @@ class Fibonacci(FSM):
|
||||
def counting(self):
|
||||
self.log("Stopping at {}".format(self.now))
|
||||
prev, self["prev"] = self["prev"], max([self.now, self["prev"]])
|
||||
return None, Delta(prev)
|
||||
return self.delay(prev)
|
||||
|
||||
|
||||
|
||||
class Odds(FSM):
|
||||
"""Agent that only executes in odd t_steps"""
|
||||
|
||||
@default_state
|
||||
@state
|
||||
@state(default=True)
|
||||
def odds(self):
|
||||
self.log("Stopping at {}".format(self.now))
|
||||
return None, Delta(1 + self.now % 2)
|
||||
return self.delay(1 + (self.now % 2))
|
||||
|
||||
|
||||
from soil import Environment, Simulation
|
||||
@@ -35,7 +34,7 @@ class TimeoutsEnv(Environment):
|
||||
self.add_agent(agent_class=Odds, node_id=1)
|
||||
|
||||
|
||||
sim = Simulation(model=TimeoutsEnv, max_steps=10, interval=1)
|
||||
sim = Simulation(model=TimeoutsEnv, max_steps=10)
|
||||
|
||||
if __name__ == "__main__":
|
||||
sim.run(dump=False)
|
||||
sim.run(dump=False)
|
||||
|
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)
|
@@ -1,7 +1,7 @@
|
||||
from soil import Simulation
|
||||
from social_wealth import MoneyEnv, graph_generator
|
||||
|
||||
sim = Simulation(name="mesa_sim", dump=False, max_steps=10, interval=2, model=MoneyEnv, parameters=dict(generator=graph_generator, N=10, width=50, height=50))
|
||||
sim = Simulation(name="mesa_sim", dump=False, max_steps=10, model=MoneyEnv, parameters=dict(generator=graph_generator, N=10, width=50, height=50))
|
||||
|
||||
if __name__ == "__main__":
|
||||
sim.run()
|
||||
|
@@ -7,7 +7,7 @@ from mesa.space import MultiGrid
|
||||
|
||||
# from mesa.time import RandomActivation
|
||||
from mesa.datacollection import DataCollector
|
||||
from mesa.batchrunner import BatchRunner
|
||||
from mesa.batchrunner import batch_run
|
||||
|
||||
import networkx as nx
|
||||
|
||||
@@ -101,7 +101,7 @@ class MoneyEnv(Environment):
|
||||
self.populate_network(agent_class=agent_class)
|
||||
|
||||
# Create agents
|
||||
for agent in self.agents:
|
||||
for agent in self.get_agents():
|
||||
x = self.random.randrange(self.grid.width)
|
||||
y = self.random.randrange(self.grid.height)
|
||||
self.grid.place_agent(agent, (x, y))
|
||||
@@ -122,16 +122,14 @@ if __name__ == "__main__":
|
||||
|
||||
variable_params = {"N": range(10, 100, 10)}
|
||||
|
||||
batch_run = BatchRunner(
|
||||
results = batch_run(
|
||||
MoneyEnv,
|
||||
variable_parameters=variable_params,
|
||||
fixed_parameters=fixed_params,
|
||||
iterations=5,
|
||||
max_steps=100,
|
||||
model_reporters={"Gini": compute_gini},
|
||||
)
|
||||
batch_run.run_all()
|
||||
max_steps=100
|
||||
)
|
||||
|
||||
run_data = batch_run.get_model_vars_dataframe()
|
||||
run_data.head()
|
||||
run_data = pd.DataFrame(results)
|
||||
print(run_data.head())
|
||||
print(run_data.Gini)
|
||||
|
@@ -2,13 +2,12 @@
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"execution_count": 1,
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2017-11-08T16:22:30.732107Z",
|
||||
"start_time": "2017-11-08T17:22:30.059855+01:00"
|
||||
},
|
||||
"collapsed": true
|
||||
}
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
@@ -28,24 +27,16 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"execution_count": 2,
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2017-11-08T16:22:35.580593Z",
|
||||
"start_time": "2017-11-08T17:22:35.542745+01:00"
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Populating the interactive namespace from numpy and matplotlib\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%pylab inline\n",
|
||||
"%matplotlib inline\n",
|
||||
"\n",
|
||||
"from soil import *"
|
||||
]
|
||||
@@ -66,7 +57,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"execution_count": 3,
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2017-11-08T16:22:37.242327Z",
|
||||
@@ -86,7 +77,7 @@
|
||||
" prob_neighbor_spread: 0.0\r\n",
|
||||
" prob_tv_spread: 0.01\r\n",
|
||||
"interval: 1\r\n",
|
||||
"max_time: 30\r\n",
|
||||
"max_time: 300\r\n",
|
||||
"name: Sim_all_dumb\r\n",
|
||||
"network_agents:\r\n",
|
||||
"- agent_class: DumbViewer\r\n",
|
||||
@@ -110,7 +101,7 @@
|
||||
" prob_neighbor_spread: 0.0\r\n",
|
||||
" prob_tv_spread: 0.01\r\n",
|
||||
"interval: 1\r\n",
|
||||
"max_time: 30\r\n",
|
||||
"max_time: 300\r\n",
|
||||
"name: Sim_half_herd\r\n",
|
||||
"network_agents:\r\n",
|
||||
"- agent_class: DumbViewer\r\n",
|
||||
@@ -142,18 +133,18 @@
|
||||
" prob_neighbor_spread: 0.0\r\n",
|
||||
" prob_tv_spread: 0.01\r\n",
|
||||
"interval: 1\r\n",
|
||||
"max_time: 30\r\n",
|
||||
"max_time: 300\r\n",
|
||||
"name: Sim_all_herd\r\n",
|
||||
"network_agents:\r\n",
|
||||
"- agent_class: HerdViewer\r\n",
|
||||
" state:\r\n",
|
||||
" has_tv: true\r\n",
|
||||
" id: neutral\r\n",
|
||||
" state_id: neutral\r\n",
|
||||
" weight: 1\r\n",
|
||||
"- agent_class: HerdViewer\r\n",
|
||||
" state:\r\n",
|
||||
" has_tv: true\r\n",
|
||||
" id: neutral\r\n",
|
||||
" state_id: neutral\r\n",
|
||||
" weight: 1\r\n",
|
||||
"network_params:\r\n",
|
||||
" generator: barabasi_albert_graph\r\n",
|
||||
@@ -169,13 +160,13 @@
|
||||
" prob_tv_spread: 0.01\r\n",
|
||||
" prob_neighbor_cure: 0.1\r\n",
|
||||
"interval: 1\r\n",
|
||||
"max_time: 30\r\n",
|
||||
"max_time: 300\r\n",
|
||||
"name: Sim_wise_herd\r\n",
|
||||
"network_agents:\r\n",
|
||||
"- agent_class: HerdViewer\r\n",
|
||||
" state:\r\n",
|
||||
" has_tv: true\r\n",
|
||||
" id: neutral\r\n",
|
||||
" state_id: neutral\r\n",
|
||||
" weight: 1\r\n",
|
||||
"- agent_class: WiseViewer\r\n",
|
||||
" state:\r\n",
|
||||
@@ -195,13 +186,13 @@
|
||||
" prob_tv_spread: 0.01\r\n",
|
||||
" prob_neighbor_cure: 0.1\r\n",
|
||||
"interval: 1\r\n",
|
||||
"max_time: 30\r\n",
|
||||
"max_time: 300\r\n",
|
||||
"name: Sim_all_wise\r\n",
|
||||
"network_agents:\r\n",
|
||||
"- agent_class: WiseViewer\r\n",
|
||||
" state:\r\n",
|
||||
" has_tv: true\r\n",
|
||||
" id: neutral\r\n",
|
||||
" state_id: neutral\r\n",
|
||||
" weight: 1\r\n",
|
||||
"- agent_class: WiseViewer\r\n",
|
||||
" state:\r\n",
|
||||
@@ -225,7 +216,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 22,
|
||||
"execution_count": 4,
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2017-11-08T18:07:46.781745Z",
|
||||
@@ -233,7 +224,24 @@
|
||||
},
|
||||
"scrolled": true
|
||||
},
|
||||
"outputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"ename": "ValueError",
|
||||
"evalue": "No objects to concatenate",
|
||||
"output_type": "error",
|
||||
"traceback": [
|
||||
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
|
||||
"\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)",
|
||||
"Cell \u001b[0;32mIn[4], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m evodumb \u001b[38;5;241m=\u001b[39m \u001b[43manalysis\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mread_data\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43msoil_output/Sim_all_dumb/\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mprocess\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43manalysis\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget_count\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mgroup\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mTrue\u001b[39;49;00m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mkeys\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mid\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m)\u001b[49m;\n",
|
||||
"File \u001b[0;32m/mnt/data/home/j/git/lab.gsi/soil/soil/soil/analysis.py:14\u001b[0m, in \u001b[0;36mread_data\u001b[0;34m(group, *args, **kwargs)\u001b[0m\n\u001b[1;32m 12\u001b[0m iterable \u001b[38;5;241m=\u001b[39m _read_data(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)\n\u001b[1;32m 13\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m group:\n\u001b[0;32m---> 14\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mgroup_trials\u001b[49m\u001b[43m(\u001b[49m\u001b[43miterable\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 15\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m 16\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mlist\u001b[39m(iterable)\n",
|
||||
"File \u001b[0;32m/mnt/data/home/j/git/lab.gsi/soil/soil/soil/analysis.py:201\u001b[0m, in \u001b[0;36mgroup_trials\u001b[0;34m(trials, aggfunc)\u001b[0m\n\u001b[1;32m 199\u001b[0m trials \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mlist\u001b[39m(trials)\n\u001b[1;32m 200\u001b[0m trials \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mlist\u001b[39m(\u001b[38;5;28mmap\u001b[39m(\u001b[38;5;28;01mlambda\u001b[39;00m x: x[\u001b[38;5;241m1\u001b[39m] \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(x, \u001b[38;5;28mtuple\u001b[39m) \u001b[38;5;28;01melse\u001b[39;00m x, trials))\n\u001b[0;32m--> 201\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mpd\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mconcat\u001b[49m\u001b[43m(\u001b[49m\u001b[43mtrials\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241m.\u001b[39mgroupby(level\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m0\u001b[39m)\u001b[38;5;241m.\u001b[39magg(aggfunc)\u001b[38;5;241m.\u001b[39mreorder_levels([\u001b[38;5;241m2\u001b[39m, \u001b[38;5;241m0\u001b[39m,\u001b[38;5;241m1\u001b[39m] ,axis\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m1\u001b[39m)\n",
|
||||
"File \u001b[0;32m/mnt/data/home/j/git/lab.gsi/soil/soil/.env-v0.20/lib/python3.8/site-packages/pandas/util/_decorators.py:331\u001b[0m, in \u001b[0;36mdeprecate_nonkeyword_arguments.<locals>.decorate.<locals>.wrapper\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 325\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(args) \u001b[38;5;241m>\u001b[39m num_allow_args:\n\u001b[1;32m 326\u001b[0m warnings\u001b[38;5;241m.\u001b[39mwarn(\n\u001b[1;32m 327\u001b[0m msg\u001b[38;5;241m.\u001b[39mformat(arguments\u001b[38;5;241m=\u001b[39m_format_argument_list(allow_args)),\n\u001b[1;32m 328\u001b[0m \u001b[38;5;167;01mFutureWarning\u001b[39;00m,\n\u001b[1;32m 329\u001b[0m stacklevel\u001b[38;5;241m=\u001b[39mfind_stack_level(),\n\u001b[1;32m 330\u001b[0m )\n\u001b[0;32m--> 331\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n",
|
||||
"File \u001b[0;32m/mnt/data/home/j/git/lab.gsi/soil/soil/.env-v0.20/lib/python3.8/site-packages/pandas/core/reshape/concat.py:368\u001b[0m, in \u001b[0;36mconcat\u001b[0;34m(objs, axis, join, ignore_index, keys, levels, names, verify_integrity, sort, copy)\u001b[0m\n\u001b[1;32m 146\u001b[0m \u001b[38;5;129m@deprecate_nonkeyword_arguments\u001b[39m(version\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m, allowed_args\u001b[38;5;241m=\u001b[39m[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mobjs\u001b[39m\u001b[38;5;124m\"\u001b[39m])\n\u001b[1;32m 147\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mconcat\u001b[39m(\n\u001b[1;32m 148\u001b[0m objs: Iterable[NDFrame] \u001b[38;5;241m|\u001b[39m Mapping[HashableT, NDFrame],\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 157\u001b[0m copy: \u001b[38;5;28mbool\u001b[39m \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mTrue\u001b[39;00m,\n\u001b[1;32m 158\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m DataFrame \u001b[38;5;241m|\u001b[39m Series:\n\u001b[1;32m 159\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 160\u001b[0m \u001b[38;5;124;03m Concatenate pandas objects along a particular axis.\u001b[39;00m\n\u001b[1;32m 161\u001b[0m \n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 366\u001b[0m \u001b[38;5;124;03m 1 3 4\u001b[39;00m\n\u001b[1;32m 367\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[0;32m--> 368\u001b[0m op \u001b[38;5;241m=\u001b[39m \u001b[43m_Concatenator\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 369\u001b[0m \u001b[43m \u001b[49m\u001b[43mobjs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 370\u001b[0m \u001b[43m \u001b[49m\u001b[43maxis\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43maxis\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 371\u001b[0m \u001b[43m \u001b[49m\u001b[43mignore_index\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mignore_index\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 372\u001b[0m \u001b[43m \u001b[49m\u001b[43mjoin\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mjoin\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 373\u001b[0m \u001b[43m \u001b[49m\u001b[43mkeys\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mkeys\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 374\u001b[0m \u001b[43m \u001b[49m\u001b[43mlevels\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mlevels\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 375\u001b[0m \u001b[43m \u001b[49m\u001b[43mnames\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mnames\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 376\u001b[0m \u001b[43m \u001b[49m\u001b[43mverify_integrity\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mverify_integrity\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 377\u001b[0m \u001b[43m \u001b[49m\u001b[43mcopy\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcopy\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 378\u001b[0m \u001b[43m \u001b[49m\u001b[43msort\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43msort\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 379\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 381\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m op\u001b[38;5;241m.\u001b[39mget_result()\n",
|
||||
"File \u001b[0;32m/mnt/data/home/j/git/lab.gsi/soil/soil/.env-v0.20/lib/python3.8/site-packages/pandas/core/reshape/concat.py:425\u001b[0m, in \u001b[0;36m_Concatenator.__init__\u001b[0;34m(self, objs, axis, join, keys, levels, names, ignore_index, verify_integrity, copy, sort)\u001b[0m\n\u001b[1;32m 422\u001b[0m objs \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mlist\u001b[39m(objs)\n\u001b[1;32m 424\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(objs) \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m0\u001b[39m:\n\u001b[0;32m--> 425\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mNo objects to concatenate\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 427\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m keys \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[1;32m 428\u001b[0m objs \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mlist\u001b[39m(com\u001b[38;5;241m.\u001b[39mnot_none(\u001b[38;5;241m*\u001b[39mobjs))\n",
|
||||
"\u001b[0;31mValueError\u001b[0m: No objects to concatenate"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"evodumb = analysis.read_data('soil_output/Sim_all_dumb/', process=analysis.get_count, group=True, keys=['id']);"
|
||||
]
|
||||
@@ -721,9 +729,9 @@
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"display_name": "venv-soil",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
"name": "venv-soil"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
@@ -735,7 +743,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.6.2"
|
||||
"version": "3.8.10"
|
||||
},
|
||||
"toc": {
|
||||
"colors": {
|
||||
|
@@ -1,5 +1,4 @@
|
||||
from soil import FSM, state, default_state, BaseAgent, NetworkAgent, Environment, Simulation
|
||||
from soil.time import Delta
|
||||
from enum import Enum
|
||||
from collections import Counter
|
||||
import logging
|
||||
@@ -35,7 +34,7 @@ class Rabbit(FSM, NetworkAgent):
|
||||
self.info("I am a newborn.")
|
||||
self.birth = self.now
|
||||
self.offspring = 0
|
||||
return self.youngling, Delta(self.sexual_maturity - self.age)
|
||||
return self.youngling.delay(self.sexual_maturity - self.age)
|
||||
|
||||
@state
|
||||
def youngling(self):
|
||||
@@ -62,7 +61,7 @@ class Male(Rabbit):
|
||||
return self.dead
|
||||
|
||||
# Males try to mate
|
||||
for f in self.model.agents(
|
||||
for f in self.model.get_agents(
|
||||
agent_class=Female, state_id=Female.fertile.id, limit=self.max_females
|
||||
):
|
||||
self.debug("FOUND A FEMALE: ", repr(f), self.mating_prob)
|
||||
|
@@ -70,7 +70,7 @@ class Male(Rabbit):
|
||||
return self.dead
|
||||
|
||||
# Males try to mate
|
||||
for f in self.model.agents(
|
||||
for f in self.model.get_agents(
|
||||
agent_class=Female, state_id=Female.fertile.id, limit=self.max_females
|
||||
):
|
||||
self.debug("FOUND A FEMALE: ", repr(f), self.mating_prob)
|
||||
|
@@ -1,9 +1,7 @@
|
||||
"""
|
||||
Example of setting a
|
||||
Example of a fully programmatic simulation, without definition files.
|
||||
"""
|
||||
from soil import Simulation, agents, Environment
|
||||
from soil.time import Delta
|
||||
|
||||
|
||||
class MyAgent(agents.FSM):
|
||||
@@ -11,22 +9,22 @@ class MyAgent(agents.FSM):
|
||||
An agent that first does a ping
|
||||
"""
|
||||
|
||||
defaults = {"pong_counts": 2}
|
||||
max_pongs = 2
|
||||
|
||||
@agents.default_state
|
||||
@agents.state
|
||||
def ping(self):
|
||||
self.info("Ping")
|
||||
return self.pong, Delta(self.random.expovariate(1 / 16))
|
||||
return self.pong.delay(self.random.expovariate(1 / 16))
|
||||
|
||||
@agents.state
|
||||
def pong(self):
|
||||
self.info("Pong")
|
||||
self.pong_counts -= 1
|
||||
self.info(str(self.pong_counts))
|
||||
if self.pong_counts < 1:
|
||||
self.max_pongs -= 1
|
||||
self.info(str(self.max_pongs), "pongs remaining")
|
||||
if self.max_pongs < 1:
|
||||
return self.die()
|
||||
return None, Delta(self.random.expovariate(1 / 16))
|
||||
return self.delay(self.random.expovariate(1 / 16))
|
||||
|
||||
|
||||
class RandomEnv(Environment):
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import networkx as nx
|
||||
from soil.agents import Geo, NetworkAgent, FSM, custom, state, default_state
|
||||
from soil.agents import NetworkAgent, FSM, custom, state, default_state
|
||||
from soil.agents.geo import Geo
|
||||
from soil import Environment, Simulation
|
||||
from soil.parameters import *
|
||||
from soil.utils import int_seed
|
||||
@@ -39,8 +40,8 @@ class TerroristEnvironment(Environment):
|
||||
HavenModel
|
||||
], [self.ratio_civil, self.ratio_leader, self.ratio_training, self.ratio_haven])
|
||||
|
||||
def generator(self, *args, **kwargs):
|
||||
return nx.random_geometric_graph(*args, **kwargs, seed=int_seed(self._seed))
|
||||
def generator(self, *args, seed=None, **kwargs):
|
||||
return nx.random_geometric_graph(*args, **kwargs, seed=seed or int_seed(self._seed))
|
||||
|
||||
class TerroristSpreadModel(FSM, Geo):
|
||||
"""
|
||||
|
@@ -21,5 +21,4 @@ class TorvaldsEnv(Environment):
|
||||
|
||||
sim = Simulation(name='torvalds_example',
|
||||
max_steps=10,
|
||||
interval=2,
|
||||
model=TorvaldsEnv)
|
File diff suppressed because one or more lines are too long
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"
|
4
setup.py
4
setup.py
@@ -17,9 +17,9 @@ def parse_requirements(filename):
|
||||
install_reqs = parse_requirements("requirements.txt")
|
||||
test_reqs = parse_requirements("test-requirements.txt")
|
||||
extras_require={
|
||||
'mesa': ['mesa>=0.8.9'],
|
||||
'geo': ['scipy>=1.3'],
|
||||
'web': ['tornado']
|
||||
'web': ['tornado'],
|
||||
'ipython': ['ipython==8.12', 'nbformat==5.8'],
|
||||
}
|
||||
extras_require['all'] = [dep for package in extras_require.values() for dep in package]
|
||||
|
||||
|
@@ -1 +1 @@
|
||||
1.0.0rc1
|
||||
1.0.0rc11
|
||||
|
@@ -16,7 +16,6 @@ except NameError:
|
||||
basestring = str
|
||||
|
||||
from pathlib import Path
|
||||
from .analysis import *
|
||||
from .agents import *
|
||||
from . import agents
|
||||
from .simulation import *
|
||||
|
@@ -25,6 +25,35 @@ from .. import serialization, network, utils, time, config
|
||||
IGNORED_FIELDS = ("model", "logger")
|
||||
|
||||
|
||||
def decorate_generator_step(func, name):
|
||||
@wraps(func)
|
||||
def decorated(self):
|
||||
while True:
|
||||
if self._coroutine is None:
|
||||
self._coroutine = func(self)
|
||||
try:
|
||||
if self._last_except:
|
||||
val = self._coroutine.throw(self._last_except)
|
||||
else:
|
||||
val = self._coroutine.send(self._last_return)
|
||||
except StopIteration as ex:
|
||||
self._coroutine = None
|
||||
val = ex.value
|
||||
finally:
|
||||
self._last_return = None
|
||||
self._last_except = None
|
||||
return float(val) if val is not None else val
|
||||
return decorated
|
||||
|
||||
|
||||
def decorate_normal_func(func, name):
|
||||
@wraps(func)
|
||||
def decorated(self):
|
||||
val = func(self)
|
||||
return float(val) if val is not None else val
|
||||
return decorated
|
||||
|
||||
|
||||
class MetaAgent(ABCMeta):
|
||||
def __new__(mcls, name, bases, namespace):
|
||||
defaults = {}
|
||||
@@ -36,34 +65,23 @@ class MetaAgent(ABCMeta):
|
||||
|
||||
new_nmspc = {
|
||||
"_defaults": defaults,
|
||||
"_last_return": None,
|
||||
"_last_except": None,
|
||||
}
|
||||
|
||||
for attr, func in namespace.items():
|
||||
if attr == "step" and inspect.isgeneratorfunction(func):
|
||||
orig_func = func
|
||||
new_nmspc["_coroutine"] = None
|
||||
|
||||
@wraps(func)
|
||||
def func(self):
|
||||
while True:
|
||||
if not self._coroutine:
|
||||
self._coroutine = orig_func(self)
|
||||
try:
|
||||
if self._last_except:
|
||||
return self._coroutine.throw(self._last_except)
|
||||
else:
|
||||
return self._coroutine.send(self._last_return)
|
||||
except StopIteration as ex:
|
||||
self._coroutine = None
|
||||
return ex.value
|
||||
finally:
|
||||
self._last_return = None
|
||||
self._last_except = None
|
||||
|
||||
func.id = name or func.__name__
|
||||
func.is_default = False
|
||||
if attr == "step":
|
||||
if inspect.isgeneratorfunction(func) or inspect.iscoroutinefunction(func):
|
||||
func = decorate_generator_step(func, attr)
|
||||
new_nmspc.update({
|
||||
"_last_return": None,
|
||||
"_last_except": None,
|
||||
"_coroutine": None,
|
||||
})
|
||||
elif inspect.isasyncgenfunction(func):
|
||||
raise ValueError("Illegal step function: {}. It probably mixes both async/await and yield".format(func))
|
||||
elif inspect.isfunction(func):
|
||||
func = decorate_normal_func(func, attr)
|
||||
else:
|
||||
raise ValueError("Illegal step function: {}".format(func))
|
||||
new_nmspc[attr] = func
|
||||
elif (
|
||||
isinstance(func, types.FunctionType)
|
||||
@@ -74,10 +92,14 @@ class MetaAgent(ABCMeta):
|
||||
new_nmspc[attr] = func
|
||||
elif attr == "defaults":
|
||||
defaults.update(func)
|
||||
elif inspect.isfunction(func):
|
||||
new_nmspc[attr] = func
|
||||
else:
|
||||
defaults[attr] = copy(func)
|
||||
|
||||
return super().__new__(mcls=mcls, name=name, bases=bases, namespace=new_nmspc)
|
||||
|
||||
# Add attributes for their use in the decorated functions
|
||||
return super().__new__(mcls, name, bases, new_nmspc)
|
||||
|
||||
|
||||
class BaseAgent(MesaAgent, MutableMapping, metaclass=MetaAgent):
|
||||
@@ -92,7 +114,7 @@ class BaseAgent(MesaAgent, MutableMapping, metaclass=MetaAgent):
|
||||
Any attribute that is not preceded by an underscore (`_`) will also be added to its state.
|
||||
"""
|
||||
|
||||
def __init__(self, unique_id, model, name=None, init=True, interval=None, **kwargs):
|
||||
def __init__(self, unique_id, model, name=None, init=True, **kwargs):
|
||||
assert isinstance(unique_id, int)
|
||||
super().__init__(unique_id=unique_id, model=model)
|
||||
|
||||
@@ -102,22 +124,24 @@ class BaseAgent(MesaAgent, MutableMapping, metaclass=MetaAgent):
|
||||
|
||||
self.alive = True
|
||||
|
||||
self.interval = interval or self.get("interval", 1)
|
||||
logger = utils.logger.getChild(getattr(self.model, "id", self.model)).getChild(
|
||||
self.name
|
||||
)
|
||||
logger = model.logger.getChild(self.name)
|
||||
self.logger = logging.LoggerAdapter(logger, {"agent_name": self.name})
|
||||
|
||||
if hasattr(self, "level"):
|
||||
self.logger.setLevel(self.level)
|
||||
|
||||
for k in self._defaults:
|
||||
v = getattr(model, k, None)
|
||||
if v is not None:
|
||||
setattr(self, k, v)
|
||||
|
||||
for (k, v) in self._defaults.items():
|
||||
if not hasattr(self, k) or getattr(self, k) is None:
|
||||
setattr(self, k, deepcopy(v))
|
||||
|
||||
for (k, v) in kwargs.items():
|
||||
|
||||
setattr(self, k, v)
|
||||
|
||||
if init:
|
||||
self.init()
|
||||
|
||||
@@ -189,11 +213,13 @@ class BaseAgent(MesaAgent, MutableMapping, metaclass=MetaAgent):
|
||||
return it
|
||||
|
||||
def get(self, key, default=None):
|
||||
if key in self:
|
||||
return self[key]
|
||||
elif key in self.model:
|
||||
return self.model[key]
|
||||
return default
|
||||
try:
|
||||
return getattr(self, key)
|
||||
except AttributeError:
|
||||
try:
|
||||
return getattr(self.model, key)
|
||||
except AttributeError:
|
||||
return default
|
||||
|
||||
@property
|
||||
def now(self):
|
||||
@@ -205,18 +231,19 @@ class BaseAgent(MesaAgent, MutableMapping, metaclass=MetaAgent):
|
||||
|
||||
def die(self, msg=None):
|
||||
if msg:
|
||||
self.info("Agent dying:", msg)
|
||||
self.debug(f"agent dying")
|
||||
self.debug("Agent dying:", msg)
|
||||
else:
|
||||
self.debug(f"agent dying")
|
||||
self.alive = False
|
||||
try:
|
||||
self.model.schedule.remove(self)
|
||||
except KeyError:
|
||||
pass
|
||||
return time.NEVER
|
||||
return time.Delay(time.INFINITY)
|
||||
|
||||
def step(self):
|
||||
raise NotImplementedError("Agent must implement step method")
|
||||
|
||||
|
||||
def _check_alive(self):
|
||||
if not self.alive:
|
||||
raise time.DeadAgent(self.unique_id)
|
||||
@@ -265,6 +292,12 @@ class BaseAgent(MesaAgent, MutableMapping, metaclass=MetaAgent):
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__}({self.unique_id})"
|
||||
|
||||
def at(self, at):
|
||||
return time.Delay(float(at) - self.now)
|
||||
|
||||
def delay(self, delay=1):
|
||||
return time.Delay(delay)
|
||||
|
||||
|
||||
def prob(prob, random):
|
||||
@@ -402,7 +435,6 @@ def filter_agents(
|
||||
unique_id=None,
|
||||
state_id=None,
|
||||
agent_class=None,
|
||||
ignore=None,
|
||||
state=None,
|
||||
limit=None,
|
||||
**kwargs,
|
||||
@@ -410,7 +442,6 @@ def filter_agents(
|
||||
"""
|
||||
Filter agents given as a dict, by the criteria given as arguments (e.g., certain type or state id).
|
||||
"""
|
||||
assert isinstance(agents, dict)
|
||||
|
||||
ids = []
|
||||
|
||||
@@ -424,9 +455,9 @@ def filter_agents(
|
||||
ids += id_args
|
||||
|
||||
if ids:
|
||||
f = (agents[aid] for aid in ids if aid in agents)
|
||||
f = (agent for agent in agents if agent.unique_id in ids)
|
||||
else:
|
||||
f = agents.values()
|
||||
f = agents
|
||||
|
||||
if state_id is not None and not isinstance(state_id, (tuple, list)):
|
||||
state_id = tuple([state_id])
|
||||
@@ -438,9 +469,6 @@ def filter_agents(
|
||||
except TypeError:
|
||||
agent_class = tuple([agent_class])
|
||||
|
||||
if ignore:
|
||||
f = filter(lambda x: x not in ignore, f)
|
||||
|
||||
if state_id is not None:
|
||||
f = filter(lambda agent: agent.get("state_id", None) in state_id, f)
|
||||
|
||||
@@ -450,8 +478,10 @@ def filter_agents(
|
||||
state = state or dict()
|
||||
state.update(kwargs)
|
||||
|
||||
for k, v in state.items():
|
||||
f = filter(lambda agent: getattr(agent, k, None) == v, f)
|
||||
for k, vs in state.items():
|
||||
if not isinstance(vs, list):
|
||||
vs = [vs]
|
||||
f = filter(lambda agent: any(getattr(agent, k, None) == v for v in vs), f)
|
||||
|
||||
if limit is not None:
|
||||
f = islice(f, limit)
|
||||
@@ -658,15 +688,6 @@ from .SISaModel import *
|
||||
from .CounterModel import *
|
||||
|
||||
|
||||
try:
|
||||
import scipy
|
||||
from .Geo import Geo
|
||||
except ImportError:
|
||||
import sys
|
||||
|
||||
print("Could not load the Geo Agent, scipy is not installed", file=sys.stderr)
|
||||
|
||||
|
||||
def custom(cls, **kwargs):
|
||||
"""Create a new class from a template class and keyword arguments"""
|
||||
return type(cls.__name__, (cls,), kwargs)
|
||||
|
@@ -1,42 +1,8 @@
|
||||
from . import BaseAgent
|
||||
from ..events import Message, Tell, Ask, TimedOut
|
||||
from ..time import BaseCond
|
||||
from functools import partial
|
||||
from collections import deque
|
||||
|
||||
|
||||
class ReceivedOrTimeout(BaseCond):
|
||||
def __init__(
|
||||
self, agent, expiration=None, timeout=None, check=True, ignore=False, **kwargs
|
||||
):
|
||||
if expiration is None:
|
||||
if timeout is not None:
|
||||
expiration = agent.now + timeout
|
||||
self.expiration = expiration
|
||||
self.ignore = ignore
|
||||
self.check = check
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def expired(self, time):
|
||||
return self.expiration and self.expiration < time
|
||||
|
||||
def ready(self, agent, time):
|
||||
return len(agent._inbox) or self.expired(time)
|
||||
|
||||
def return_value(self, agent):
|
||||
if not self.ignore and self.expired(agent.now):
|
||||
raise TimedOut("No messages received")
|
||||
if self.check:
|
||||
agent.check_messages()
|
||||
return None
|
||||
|
||||
def schedule_next(self, time, delta, first=False):
|
||||
if self._delta is not None:
|
||||
delta = self._delta
|
||||
return (time + delta, self)
|
||||
|
||||
def __repr__(self):
|
||||
return f"ReceivedOrTimeout(expires={self.expiration})"
|
||||
from types import coroutine
|
||||
|
||||
|
||||
class EventedAgent(BaseAgent):
|
||||
@@ -48,30 +14,45 @@ class EventedAgent(BaseAgent):
|
||||
def on_receive(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def received(self, *args, **kwargs):
|
||||
return ReceivedOrTimeout(self, *args, **kwargs)
|
||||
@coroutine
|
||||
def received(self, expiration=None, timeout=60, delay=1, process=True):
|
||||
if not expiration:
|
||||
expiration = self.now + timeout
|
||||
while self.now < expiration:
|
||||
if self._inbox:
|
||||
msgs = self._inbox
|
||||
if process:
|
||||
self.process_messages()
|
||||
return msgs
|
||||
yield self.delay(delay)
|
||||
raise TimedOut("No message received")
|
||||
|
||||
def tell(self, msg, sender=None):
|
||||
self._inbox.append(Tell(timestamp=self.now, payload=msg, sender=sender))
|
||||
|
||||
def ask(self, msg, timeout=None, **kwargs):
|
||||
@coroutine
|
||||
def ask(self, msg, expiration=None, timeout=None, delay=1):
|
||||
ask = Ask(timestamp=self.now, payload=msg, sender=self)
|
||||
self._inbox.append(ask)
|
||||
expiration = float("inf") if timeout is None else self.now + timeout
|
||||
return ask.replied(expiration=expiration, **kwargs)
|
||||
while self.now < expiration:
|
||||
if ask.reply:
|
||||
return ask.reply
|
||||
yield self.delay(delay)
|
||||
raise TimedOut("No reply received")
|
||||
|
||||
def check_messages(self):
|
||||
changed = False
|
||||
while self._inbox:
|
||||
msg = self._inbox.popleft()
|
||||
def process_messages(self):
|
||||
valid = list()
|
||||
for msg in self._inbox:
|
||||
self._processed += 1
|
||||
if msg.expired(self.now):
|
||||
continue
|
||||
changed = True
|
||||
valid.append(msg)
|
||||
reply = self.on_receive(msg.payload, sender=msg.sender)
|
||||
if isinstance(msg, Ask):
|
||||
msg.reply = reply
|
||||
return changed
|
||||
self._inbox.clear()
|
||||
return valid
|
||||
|
||||
|
||||
Evented = EventedAgent
|
||||
|
@@ -1,47 +1,69 @@
|
||||
from . import MetaAgent, BaseAgent
|
||||
from ..time import Delta
|
||||
|
||||
from .. import time
|
||||
from types import coroutine
|
||||
from functools import partial, wraps
|
||||
import inspect
|
||||
|
||||
|
||||
class State:
|
||||
__slots__ = ("awaitable", "f", "generator", "name", "default")
|
||||
|
||||
def __init__(self, f, name, default, generator, awaitable):
|
||||
self.f = f
|
||||
self.name = name
|
||||
self.generator = generator
|
||||
self.awaitable = awaitable
|
||||
self.default = default
|
||||
|
||||
@coroutine
|
||||
def step(self, obj):
|
||||
if self.generator or self.awaitable:
|
||||
f = self.f
|
||||
next_state = yield from f(obj)
|
||||
return next_state
|
||||
|
||||
else:
|
||||
return self.f(obj)
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self.name
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
raise Exception("States should not be called directly")
|
||||
|
||||
class UnboundState(State):
|
||||
|
||||
def bind(self, obj):
|
||||
bs = BoundState(self.f, self.name, self.default, self.generator, self.awaitable, obj=obj)
|
||||
setattr(obj, self.name, bs)
|
||||
return bs
|
||||
|
||||
|
||||
class BoundState(State):
|
||||
__slots__ = ("obj", )
|
||||
|
||||
def __init__(self, *args, obj):
|
||||
super().__init__(*args)
|
||||
self.obj = obj
|
||||
|
||||
def delay(self, delta=0):
|
||||
return self, self.obj.delay(delta)
|
||||
|
||||
def at(self, when):
|
||||
return self, self.obj.at(when)
|
||||
|
||||
|
||||
def state(name=None, default=False):
|
||||
def decorator(func, name=None):
|
||||
"""
|
||||
A state function should return either a state id, or a tuple (state_id, when)
|
||||
The default value for state_id is the current state id.
|
||||
The default value for when is the interval defined in the environment.
|
||||
"""
|
||||
if inspect.isgeneratorfunction(func):
|
||||
orig_func = func
|
||||
|
||||
@wraps(func)
|
||||
def func(self):
|
||||
while True:
|
||||
if not self._coroutine:
|
||||
self._coroutine = orig_func(self)
|
||||
|
||||
try:
|
||||
if self._last_except:
|
||||
n = self._coroutine.throw(self._last_except)
|
||||
else:
|
||||
n = self._coroutine.send(self._last_return)
|
||||
if n:
|
||||
return None, n
|
||||
return n
|
||||
except StopIteration as ex:
|
||||
self._coroutine = None
|
||||
next_state = ex.value
|
||||
if next_state is not None:
|
||||
self._set_state(next_state)
|
||||
return next_state
|
||||
finally:
|
||||
self._last_return = None
|
||||
self._last_except = None
|
||||
|
||||
func.id = name or func.__name__
|
||||
func.is_default = default
|
||||
return func
|
||||
name = name or func.__name__
|
||||
generator = inspect.isgeneratorfunction(func)
|
||||
awaitable = inspect.iscoroutinefunction(func) or inspect.isasyncgen(func)
|
||||
return UnboundState(func, name, default, generator, awaitable)
|
||||
|
||||
if callable(name):
|
||||
return decorator(name)
|
||||
@@ -50,7 +72,7 @@ def state(name=None, default=False):
|
||||
|
||||
|
||||
def default_state(func):
|
||||
func.is_default = True
|
||||
func.default = True
|
||||
return func
|
||||
|
||||
|
||||
@@ -62,42 +84,45 @@ class MetaFSM(MetaAgent):
|
||||
for i in bases:
|
||||
if isinstance(i, MetaFSM):
|
||||
for state_id, state in i._states.items():
|
||||
if state.is_default:
|
||||
if state.default:
|
||||
default_state = state
|
||||
states[state_id] = state
|
||||
|
||||
# Add new states
|
||||
for attr, func in namespace.items():
|
||||
if hasattr(func, "id"):
|
||||
if func.is_default:
|
||||
if isinstance(func, State):
|
||||
if func.default:
|
||||
default_state = func
|
||||
states[func.id] = func
|
||||
states[func.name] = func
|
||||
|
||||
namespace.update(
|
||||
{
|
||||
"_default_state": default_state,
|
||||
"_state": default_state,
|
||||
"_states": states,
|
||||
}
|
||||
)
|
||||
|
||||
return super(MetaFSM, mcls).__new__(
|
||||
cls = super(MetaFSM, mcls).__new__(
|
||||
mcls=mcls, name=name, bases=bases, namespace=namespace
|
||||
)
|
||||
for (k, v) in states.items():
|
||||
setattr(cls, k, v)
|
||||
return cls
|
||||
|
||||
|
||||
class FSM(BaseAgent, metaclass=MetaFSM):
|
||||
def __init__(self, init=True, **kwargs):
|
||||
def __init__(self, init=True, state_id=None, **kwargs):
|
||||
super().__init__(**kwargs, init=False)
|
||||
if not hasattr(self, "state_id"):
|
||||
if not self._default_state:
|
||||
raise ValueError(
|
||||
"No default state specified for {}".format(self.unique_id)
|
||||
)
|
||||
self.state_id = self._default_state.id
|
||||
if state_id is not None:
|
||||
self._set_state(state_id)
|
||||
# If more than "dead" state is defined, but no default state
|
||||
if len(self._states) > 1 and not self._state:
|
||||
raise ValueError(
|
||||
f"No default state specified for {type(self)}({self.unique_id})"
|
||||
)
|
||||
for (k, v) in self._states.items():
|
||||
setattr(self, k, v.bind(self))
|
||||
|
||||
self._coroutine = None
|
||||
self.default_interval = Delta(self.model.interval)
|
||||
self._set_state(self.state_id)
|
||||
if init:
|
||||
self.init()
|
||||
|
||||
@@ -105,44 +130,46 @@ class FSM(BaseAgent, metaclass=MetaFSM):
|
||||
def states(cls):
|
||||
return list(cls._states.keys())
|
||||
|
||||
@property
|
||||
def state_id(self):
|
||||
return self._state.name
|
||||
|
||||
def set_state(self, value):
|
||||
if self.now > 0:
|
||||
raise ValueError("Cannot change state after init")
|
||||
self._set_state(value)
|
||||
|
||||
def step(self):
|
||||
self.debug(f"Agent {self.unique_id} @ state {self.state_id}")
|
||||
|
||||
self._check_alive()
|
||||
next_state = self._states[self.state_id](self)
|
||||
next_state = yield from self._state.step(self)
|
||||
|
||||
when = None
|
||||
try:
|
||||
next_state, *when = next_state
|
||||
if not when:
|
||||
when = None
|
||||
elif len(when) == 1:
|
||||
when = when[0]
|
||||
else:
|
||||
raise ValueError(
|
||||
"Too many values returned. Only state (and time) allowed"
|
||||
)
|
||||
except TypeError:
|
||||
pass
|
||||
next_state, when = next_state
|
||||
except (TypeError, ValueError) as ex:
|
||||
try:
|
||||
self._set_state(next_state)
|
||||
return None
|
||||
except ValueError:
|
||||
return next_state
|
||||
|
||||
if next_state is not None:
|
||||
self._set_state(next_state)
|
||||
self._set_state(next_state)
|
||||
return when
|
||||
|
||||
return when or self.default_interval
|
||||
|
||||
def _set_state(self, state, when=None):
|
||||
if hasattr(state, "id"):
|
||||
state = state.id
|
||||
if state not in self._states:
|
||||
def _set_state(self, state):
|
||||
if state is None:
|
||||
return
|
||||
if isinstance(state, str):
|
||||
if state not in self._states:
|
||||
raise ValueError("{} is not a valid state".format(state))
|
||||
state = self._states[state]
|
||||
if not isinstance(state, State):
|
||||
raise ValueError("{} is not a valid state".format(state))
|
||||
self.state_id = state
|
||||
if when is not None:
|
||||
self.model.schedule.add(self, when=when)
|
||||
return state
|
||||
self._state = state
|
||||
|
||||
def die(self, *args, **kwargs):
|
||||
return self.dead, super().die(*args, **kwargs)
|
||||
super().die(*args, **kwargs)
|
||||
return self.dead.at(time.INFINITY)
|
||||
|
||||
@state
|
||||
def dead(self):
|
||||
return self.die()
|
||||
return time.INFINITY
|
@@ -6,7 +6,7 @@ class NetworkAgent(BaseAgent):
|
||||
super().__init__(*args, init=False, **kwargs)
|
||||
|
||||
self.G = topology or self.model.G
|
||||
assert self.G
|
||||
assert self.G is not None, "Network agents should have a network"
|
||||
if node_id is None:
|
||||
nodes = self.random.choices(list(self.G.nodes), k=len(self.G))
|
||||
for n_id in nodes:
|
||||
@@ -25,8 +25,6 @@ class NetworkAgent(BaseAgent):
|
||||
|
||||
def count_neighbors(self, state_id=None, **kwargs):
|
||||
return len(self.get_neighbors(state_id=state_id, **kwargs))
|
||||
if init:
|
||||
self.init()
|
||||
|
||||
def iter_neighbors(self, **kwargs):
|
||||
return self.iter_agents(limit_neighbors=True, **kwargs)
|
||||
|
@@ -1,12 +1,11 @@
|
||||
import os
|
||||
import sys
|
||||
import sqlalchemy
|
||||
import pandas as pd
|
||||
from collections import namedtuple
|
||||
|
||||
def plot(env, agent_df=None, model_df=None, steps=False, ignore=["agent_count", ]):
|
||||
"""Plot the model dataframe and agent dataframe together."""
|
||||
if agent_df is None:
|
||||
agent_df = env.agent_df()
|
||||
if model_df is None:
|
||||
model_df = env.model_df()
|
||||
ignore = list(ignore)
|
||||
@@ -16,12 +15,21 @@ def plot(env, agent_df=None, model_df=None, steps=False, ignore=["agent_count",
|
||||
ignore.append("time")
|
||||
|
||||
ax = model_df.drop(ignore, axis='columns').plot();
|
||||
if agent_df is None:
|
||||
try:
|
||||
agent_df = env.agent_df()
|
||||
except UserWarning:
|
||||
print("No agent dataframe provided and no agent reporters found. Skipping agent plot.", file=sys.stderr)
|
||||
return
|
||||
if not agent_df.empty:
|
||||
agent_df.unstack().apply(lambda x: x.value_counts(),
|
||||
axis=1).fillna(0).plot(ax=ax, secondary_y=True);
|
||||
axis=1).fillna(0).plot(ax=ax,
|
||||
secondary_y=True)
|
||||
|
||||
|
||||
Results = namedtuple("Results", ["config", "parameters", "env", "agents"])
|
||||
#TODO implement reading from CSV and SQLITE
|
||||
#TODO implement reading from CSV
|
||||
|
||||
def read_sql(fpath=None, name=None, include_agents=False):
|
||||
if not (fpath is None) ^ (name is None):
|
||||
raise ValueError("Specify either a path or a simulation name")
|
||||
|
@@ -9,7 +9,7 @@ class SoilCollector(MDC):
|
||||
if 'agent_count' not in model_reporters:
|
||||
model_reporters['agent_count'] = lambda m: m.schedule.get_agent_count()
|
||||
if 'time' not in model_reporters:
|
||||
model_reporters['time'] = lambda m: m.now
|
||||
model_reporters['time'] = lambda m: m.schedule.time
|
||||
# if 'state_id' not in agent_reporters:
|
||||
# agent_reporters['state_id'] = lambda agent: getattr(agent, 'state_id', None)
|
||||
|
||||
|
@@ -49,7 +49,7 @@ class Debug(pdb.Pdb):
|
||||
|
||||
@staticmethod
|
||||
def _soil_agents(model, attrs=None, pretty=True, **kwargs):
|
||||
for agent in model.agents(**kwargs):
|
||||
for agent in model.get_agents(**kwargs):
|
||||
d = agent
|
||||
print(" - " + indent(agent.to_str(keys=attrs, pretty=pretty), " "))
|
||||
|
||||
|
@@ -16,7 +16,7 @@ import networkx as nx
|
||||
|
||||
from mesa import Model, Agent
|
||||
|
||||
from . import agents as agentmod, datacollection, serialization, utils, time, network, events
|
||||
from . import agents as agentmod, datacollection, utils, time, network, events
|
||||
|
||||
|
||||
# TODO: maybe add metaclass to read attributes of a model
|
||||
@@ -35,6 +35,7 @@ class BaseEnvironment(Model):
|
||||
"""
|
||||
|
||||
collector_class = datacollection.SoilCollector
|
||||
schedule_class = time.TimedActivation
|
||||
|
||||
def __new__(cls,
|
||||
*args: Any,
|
||||
@@ -49,7 +50,6 @@ class BaseEnvironment(Model):
|
||||
self = super().__new__(cls, *args, seed=seed, **kwargs)
|
||||
self.dir_path = dir_path or os.getcwd()
|
||||
collector_class = collector_class or cls.collector_class
|
||||
collector_class = serialization.deserialize(collector_class)
|
||||
self.datacollector = collector_class(
|
||||
model_reporters=model_reporters,
|
||||
agent_reporters=agent_reporters,
|
||||
@@ -60,7 +60,7 @@ class BaseEnvironment(Model):
|
||||
if isinstance(v, property):
|
||||
v = v.fget
|
||||
if getattr(v, "add_to_report", False):
|
||||
self.add_model_reporter(k, v)
|
||||
self.add_model_reporter(k, k)
|
||||
|
||||
return self
|
||||
|
||||
@@ -70,8 +70,8 @@ class BaseEnvironment(Model):
|
||||
id="unnamed_env",
|
||||
seed="default",
|
||||
dir_path=None,
|
||||
schedule_class=time.TimedActivation,
|
||||
interval=1,
|
||||
schedule=None,
|
||||
schedule_class=None,
|
||||
logger = None,
|
||||
agents: Optional[Dict] = None,
|
||||
collector_class: type = datacollection.SoilCollector,
|
||||
@@ -94,13 +94,11 @@ class BaseEnvironment(Model):
|
||||
else:
|
||||
self.logger = utils.logger.getChild(self.id)
|
||||
|
||||
if schedule_class is None:
|
||||
schedule_class = time.TimedActivation
|
||||
else:
|
||||
schedule_class = serialization.deserialize(schedule_class)
|
||||
|
||||
self.interval = interval
|
||||
self.schedule = schedule_class(self)
|
||||
self.schedule = schedule
|
||||
if schedule is None:
|
||||
if schedule_class is None:
|
||||
schedule_class = self.schedule_class
|
||||
self.schedule = schedule_class(self)
|
||||
|
||||
for (k, v) in env_params.items():
|
||||
self[k] = v
|
||||
@@ -115,14 +113,14 @@ class BaseEnvironment(Model):
|
||||
pass
|
||||
|
||||
@property
|
||||
def agents(self):
|
||||
return agentmod.AgentView(self.schedule._agents)
|
||||
def get_agents(self):
|
||||
return agentmod.AgentView(self.schedule.agents)
|
||||
|
||||
def agent(self, *args, **kwargs):
|
||||
return agentmod.AgentView(self.schedule._agents).one(*args, **kwargs)
|
||||
return agentmod.AgentView(self.schedule.agents).one(*args, **kwargs)
|
||||
|
||||
def count_agents(self, *args, **kwargs):
|
||||
return sum(1 for i in self.agents(*args, **kwargs))
|
||||
return sum(1 for i in self.get_agents(*args, **kwargs))
|
||||
|
||||
def agent_df(self, steps=False):
|
||||
df = self.datacollector.get_agent_vars_dataframe()
|
||||
@@ -147,6 +145,7 @@ class BaseEnvironment(Model):
|
||||
raise Exception(
|
||||
"The environment has not been scheduled, so it has no sense of time"
|
||||
)
|
||||
|
||||
def init_agents(self):
|
||||
pass
|
||||
|
||||
@@ -161,7 +160,7 @@ class BaseEnvironment(Model):
|
||||
if unique_id is None:
|
||||
unique_id = self.next_id()
|
||||
|
||||
a = serialization.deserialize(agent_class)(unique_id=unique_id, model=self, **agent)
|
||||
a = agent_class(unique_id=unique_id, model=self, **agent)
|
||||
|
||||
self.schedule.add(a)
|
||||
return a
|
||||
@@ -204,22 +203,23 @@ class BaseEnvironment(Model):
|
||||
|
||||
def add_model_reporter(self, name, func=None):
|
||||
if not func:
|
||||
func = lambda env: getattr(env, name)
|
||||
func = name
|
||||
self.datacollector._new_model_reporter(name, func)
|
||||
|
||||
def add_agent_reporter(self, name, agent_type=None):
|
||||
if agent_type:
|
||||
reporter = lambda a: getattr(a, name) if isinstance(a, agent_type) else None
|
||||
else:
|
||||
reporter = lambda a: getattr(a, name, None)
|
||||
def add_agent_reporter(self, name, reporter=None, agent_type=None):
|
||||
if not agent_type and not reporter:
|
||||
reporter = name
|
||||
elif agent_type:
|
||||
reporter = lambda a: reporter(a) if isinstance(a, agent_type) else None
|
||||
self.datacollector._new_agent_reporter(name, reporter)
|
||||
|
||||
@classmethod
|
||||
def run(cls, *,
|
||||
name=None,
|
||||
iterations=1,
|
||||
num_processes=1, **kwargs):
|
||||
from .simulation import Simulation
|
||||
return Simulation(name=cls.__name__,
|
||||
return Simulation(name=name or cls.__name__,
|
||||
model=cls, iterations=iterations,
|
||||
num_processes=num_processes, **kwargs).run()
|
||||
|
||||
@@ -245,7 +245,7 @@ class BaseEnvironment(Model):
|
||||
return sum(1 for n in self.keys())
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.agents())
|
||||
return iter(self.get_agents())
|
||||
|
||||
def get(self, key, default=None):
|
||||
return self[key] if key in self else default
|
||||
@@ -277,8 +277,6 @@ class NetworkEnvironment(BaseEnvironment):
|
||||
super().__init__(*args, **kwargs, init=False)
|
||||
|
||||
self.agent_class = agent_class
|
||||
if agent_class:
|
||||
self.agent_class = serialization.deserialize(agent_class)
|
||||
if self.agent_class:
|
||||
self.populate_network(self.agent_class)
|
||||
self._check_agent_nodes()
|
||||
@@ -308,7 +306,15 @@ class NetworkEnvironment(BaseEnvironment):
|
||||
elif path is not None:
|
||||
topology = network.from_topology(path, dir_path=self.dir_path)
|
||||
elif generator is not None:
|
||||
topology = network.from_params(generator=generator, dir_path=self.dir_path, **network_params)
|
||||
params = dict(generator=generator,
|
||||
dir_path=self.dir_path,
|
||||
seed=self.random,
|
||||
**network_params)
|
||||
try:
|
||||
topology = network.from_params(**params)
|
||||
except TypeError:
|
||||
del params["seed"]
|
||||
topology = network.from_params(**params)
|
||||
else:
|
||||
raise ValueError("topology must be a networkx.Graph or a string, or network_generator must be provided")
|
||||
self.G = topology
|
||||
@@ -357,11 +363,11 @@ class NetworkEnvironment(BaseEnvironment):
|
||||
"""
|
||||
for (id, data) in self.G.nodes(data=True):
|
||||
if "agent_id" in data:
|
||||
agent = self.agents(data["agent_id"])
|
||||
agent = self.get_agents(data["agent_id"])
|
||||
self.G.nodes[id]["agent"] = agent
|
||||
assert not getattr(agent, "node_id", None) or agent.node_id == id
|
||||
agent.node_id = id
|
||||
for agent in self.agents():
|
||||
for agent in self.get_agents():
|
||||
if hasattr(agent, "node_id"):
|
||||
node_id = agent["node_id"]
|
||||
if node_id not in self.G.nodes:
|
||||
@@ -405,7 +411,7 @@ class NetworkEnvironment(BaseEnvironment):
|
||||
|
||||
class EventedEnvironment(BaseEnvironment):
|
||||
def broadcast(self, msg, sender=None, expiration=None, ttl=None, **kwargs):
|
||||
for agent in self.agents(**kwargs):
|
||||
for agent in self.get_agents(**kwargs):
|
||||
if agent == sender:
|
||||
continue
|
||||
self.logger.debug(f"Telling {repr(agent)}: {msg} ttl={ttl}")
|
||||
|
@@ -1,4 +1,3 @@
|
||||
from .time import BaseCond
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
@@ -24,29 +23,9 @@ class Reply(Message):
|
||||
source: Message
|
||||
|
||||
|
||||
class ReplyCond(BaseCond):
|
||||
def __init__(self, ask, *args, **kwargs):
|
||||
self._ask = ask
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def ready(self, agent, time):
|
||||
return self._ask.reply is not None or self._ask.expired(time)
|
||||
|
||||
def return_value(self, agent):
|
||||
if self._ask.expired(agent.now):
|
||||
raise TimedOut()
|
||||
return self._ask.reply
|
||||
|
||||
def __repr__(self):
|
||||
return f"ReplyCond({self._ask.id})"
|
||||
|
||||
|
||||
class Ask(Message):
|
||||
reply: Message = None
|
||||
|
||||
def replied(self, expiration=None):
|
||||
return ReplyCond(self)
|
||||
|
||||
|
||||
class Tell(Message):
|
||||
pass
|
||||
|
@@ -2,11 +2,9 @@ import os
|
||||
import sys
|
||||
from time import time as current_time
|
||||
from io import BytesIO
|
||||
from sqlalchemy import create_engine
|
||||
from textwrap import dedent, indent
|
||||
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import networkx as nx
|
||||
import pandas as pd
|
||||
|
||||
@@ -124,6 +122,9 @@ class SQLite(Exporter):
|
||||
if not self.dump:
|
||||
logger.debug("NOT dumping results")
|
||||
return
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
|
||||
self.dbpath = os.path.join(self.outdir, f"{self.simulation.name}.sqlite")
|
||||
logger.info("Dumping results to %s", self.dbpath)
|
||||
if self.simulation.backup:
|
||||
@@ -175,7 +176,6 @@ class csv(Exporter):
|
||||
df.to_csv(f)
|
||||
|
||||
|
||||
# TODO: reimplement GEXF exporting without history
|
||||
class gexf(Exporter):
|
||||
def iteration_end(self, env, *args, **kwargs):
|
||||
if not self.dump:
|
||||
@@ -186,8 +186,7 @@ class gexf(Exporter):
|
||||
"[GEXF] Dumping simulation {} iteration {}".format(self.simulation.name, env.id)
|
||||
):
|
||||
with self.output("{}.gexf".format(env.id), mode="wb") as f:
|
||||
network.dump_gexf(env.history_to_graph(), f)
|
||||
self.dump_gexf(env, f)
|
||||
nx.write_gexf(env.G, f)
|
||||
|
||||
|
||||
class dummy(Exporter):
|
||||
@@ -210,6 +209,7 @@ class dummy(Exporter):
|
||||
|
||||
class graphdrawing(Exporter):
|
||||
def iteration_end(self, env, *args, **kwargs):
|
||||
import matplotlib.pyplot as plt
|
||||
# Outside effects
|
||||
f = plt.figure()
|
||||
nx.draw(
|
||||
|
@@ -8,6 +8,8 @@ import importlib.machinery, importlib.util
|
||||
from glob import glob
|
||||
from itertools import product, chain
|
||||
|
||||
from contextlib import contextmanager
|
||||
|
||||
import yaml
|
||||
import networkx as nx
|
||||
|
||||
@@ -101,7 +103,13 @@ def load_config(cfg):
|
||||
yield from load_files(cfg)
|
||||
|
||||
|
||||
builtins = importlib.import_module("builtins")
|
||||
_BUILTINS = None
|
||||
|
||||
def builtins():
|
||||
global _BUILTINS
|
||||
if not _BUILTINS:
|
||||
_BUILTINS = importlib.import_module("builtins")
|
||||
return _BUILTINS
|
||||
|
||||
KNOWN_MODULES = {
|
||||
'soil': None,
|
||||
@@ -110,12 +118,12 @@ KNOWN_MODULES = {
|
||||
|
||||
MODULE_FILES = {}
|
||||
|
||||
def add_source_file(file):
|
||||
def _add_source_file(file):
|
||||
"""Add a file to the list of known modules"""
|
||||
file = os.path.abspath(file)
|
||||
if file in MODULE_FILES:
|
||||
logger.warning(f"File {file} already added as module {MODULE_FILES[file]}. Reloading")
|
||||
remove_source_file(file)
|
||||
_remove_source_file(file)
|
||||
modname = f"imported_module_{len(MODULE_FILES)}"
|
||||
loader = importlib.machinery.SourceFileLoader(modname, file)
|
||||
spec = importlib.util.spec_from_loader(loader.name, loader)
|
||||
@@ -124,7 +132,7 @@ def add_source_file(file):
|
||||
MODULE_FILES[file] = modname
|
||||
KNOWN_MODULES[modname] = my_module
|
||||
|
||||
def remove_source_file(file):
|
||||
def _remove_source_file(file):
|
||||
"""Remove a file from the list of known modules"""
|
||||
file = os.path.abspath(file)
|
||||
modname = None
|
||||
@@ -134,6 +142,18 @@ def remove_source_file(file):
|
||||
except KeyError as ex:
|
||||
raise ValueError(f"File {file} had not been added as a module: {ex}")
|
||||
|
||||
|
||||
@contextmanager
|
||||
def with_source(file=None):
|
||||
"""Add a file to the list of known modules, and remove it afterwards"""
|
||||
if file:
|
||||
_add_source_file(file)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if file:
|
||||
_remove_source_file(file)
|
||||
|
||||
def get_module(modname):
|
||||
"""Get a module from the list of known modules"""
|
||||
if modname not in KNOWN_MODULES or KNOWN_MODULES[modname] is None:
|
||||
@@ -149,7 +169,7 @@ def name(value, known_modules=KNOWN_MODULES):
|
||||
if not isinstance(value, type): # Get the class name first
|
||||
value = type(value)
|
||||
tname = value.__name__
|
||||
if hasattr(builtins, tname):
|
||||
if hasattr(builtins(), tname):
|
||||
return tname
|
||||
modname = value.__module__
|
||||
if modname == "__main__":
|
||||
@@ -164,7 +184,7 @@ def name(value, known_modules=KNOWN_MODULES):
|
||||
|
||||
|
||||
def serializer(type_):
|
||||
if type_ != "str" and hasattr(builtins, type_):
|
||||
if type_ != "str":
|
||||
return repr
|
||||
return lambda x: x
|
||||
|
||||
@@ -202,8 +222,8 @@ def deserializer(type_, known_modules=KNOWN_MODULES):
|
||||
return lambda x="": x
|
||||
if type_ == "None":
|
||||
return lambda x=None: None
|
||||
if hasattr(builtins, type_): # Check if it's a builtin type
|
||||
cls = getattr(builtins, type_)
|
||||
if hasattr(builtins(), type_): # Check if it's a builtin type
|
||||
cls = getattr(builtins(), type_)
|
||||
return lambda x=None: ast.literal_eval(x) if x is not None else cls()
|
||||
match = IS_CLASS.match(type_)
|
||||
if match:
|
||||
|
@@ -23,7 +23,7 @@ import json
|
||||
|
||||
|
||||
from . import serialization, exporters, utils, basestring, agents
|
||||
from .environment import Environment
|
||||
from . import environment
|
||||
from .utils import logger, run_and_return_exceptions
|
||||
from .debugging import set_trace
|
||||
|
||||
@@ -49,8 +49,6 @@ def _iter_queued():
|
||||
|
||||
|
||||
# TODO: change documentation for simulation
|
||||
# TODO: rename iterations to iterations
|
||||
# TODO: make parameters a dict of iterable/any
|
||||
@dataclass
|
||||
class Simulation:
|
||||
"""
|
||||
@@ -68,7 +66,6 @@ class Simulation:
|
||||
dir_path: The directory path to use for the simulation.
|
||||
max_time: The maximum time to run the simulation.
|
||||
max_steps: The maximum number of steps to run the simulation.
|
||||
interval: The interval to use for the simulation.
|
||||
iterations: The number of iterations (times) to run the simulation.
|
||||
num_processes: The number of processes to use for the simulation. If greater than one, simulations will be performed in parallel. This may make debugging and error handling difficult.
|
||||
tables: The tables to use in the simulation datacollector
|
||||
@@ -96,7 +93,6 @@ class Simulation:
|
||||
dir_path: str = field(default_factory=lambda: os.getcwd())
|
||||
max_time: float = None
|
||||
max_steps: int = None
|
||||
interval: int = 1
|
||||
iterations: int = 1
|
||||
num_processes: Optional[int] = 1
|
||||
exporters: Optional[List[str]] = field(default_factory=lambda: [exporters.default])
|
||||
@@ -119,43 +115,23 @@ class Simulation:
|
||||
self.logger = logger.getChild(self.name)
|
||||
self.logger.setLevel(self.level)
|
||||
|
||||
if self.source_file:
|
||||
source_file = self.source_file
|
||||
if not os.path.isabs(source_file):
|
||||
source_file = os.path.abspath(os.path.join(self.dir_path, source_file))
|
||||
serialization.add_source_file(source_file)
|
||||
self.source_file = source_file
|
||||
if self.source_file and (not os.path.isabs(self.source_file)):
|
||||
self.source_file = os.path.abspath(os.path.join(self.dir_path, self.source_file))
|
||||
with serialization.with_source(self.source_file):
|
||||
|
||||
if isinstance(self.model, str):
|
||||
self.model = serialization.deserialize(self.model)
|
||||
if isinstance(self.model, str):
|
||||
self.model = serialization.deserialize(self.model)
|
||||
|
||||
def deserialize_reporters(reporters):
|
||||
for (k, v) in reporters.items():
|
||||
if isinstance(v, str) and v.startswith("py:"):
|
||||
reporters[k] = serialization.deserialize(v.split(":", 1)[1])
|
||||
return reporters
|
||||
|
||||
self.agent_reporters = deserialize_reporters(self.agent_reporters)
|
||||
self.model_reporters = deserialize_reporters(self.model_reporters)
|
||||
self.tables = deserialize_reporters(self.tables)
|
||||
if self.source_file:
|
||||
serialization.remove_source_file(self.source_file)
|
||||
self.id = f"{self.name}_{current_time()}"
|
||||
self.agent_reporters = self.agent_reporters
|
||||
self.model_reporters = self.model_reporters
|
||||
self.tables = self.tables
|
||||
self.id = f"{self.name}_{current_time()}"
|
||||
|
||||
def run(self, **kwargs):
|
||||
"""Run the simulation and return the list of resulting environments"""
|
||||
if kwargs:
|
||||
return replace(self, **kwargs).run()
|
||||
|
||||
self.logger.debug(
|
||||
dedent(
|
||||
"""
|
||||
Simulation:
|
||||
---
|
||||
"""
|
||||
)
|
||||
+ self.to_yaml()
|
||||
)
|
||||
param_combinations = self._collect_params(**kwargs)
|
||||
if _AVOID_RUNNING:
|
||||
_QUEUED.extend((self, param) for param in param_combinations)
|
||||
@@ -179,8 +155,9 @@ class Simulation:
|
||||
exporter.sim_start()
|
||||
|
||||
for params in tqdm(param_combinations, desc=self.name, unit="configuration"):
|
||||
tqdm.write("- Running for parameters: ")
|
||||
for (k, v) in params.items():
|
||||
tqdm.write(f"{k} = {v}")
|
||||
tqdm.write(f" {k} = {v}")
|
||||
sha = hashlib.sha256()
|
||||
sha.update(repr(sorted(params.items())).encode())
|
||||
params_id = sha.hexdigest()[:7]
|
||||
@@ -217,10 +194,7 @@ class Simulation:
|
||||
):
|
||||
"""Run the simulation and yield the resulting environments."""
|
||||
|
||||
try:
|
||||
if self.source_file:
|
||||
serialization.add_source_file(self.source_file)
|
||||
|
||||
with serialization.with_source(self.source_file):
|
||||
with utils.timer(f"running for config {params}"):
|
||||
if self.dry_run:
|
||||
def func(*args, **kwargs):
|
||||
@@ -237,9 +211,6 @@ class Simulation:
|
||||
continue
|
||||
|
||||
yield env
|
||||
finally:
|
||||
if self.source_file:
|
||||
serialization.remove_source_file(self.source_file)
|
||||
|
||||
def _get_env(self, iteration_id, params):
|
||||
"""Create an environment for a iteration of the simulation"""
|
||||
@@ -255,7 +226,6 @@ class Simulation:
|
||||
id=iteration_id,
|
||||
seed=f"{self.seed}_iteration_{iteration_id}",
|
||||
dir_path=self.dir_path,
|
||||
interval=self.interval,
|
||||
logger=self.logger.getChild(iteration_id),
|
||||
agent_reporters=agent_reporters,
|
||||
model_reporters=model_reporters,
|
||||
@@ -370,8 +340,11 @@ def iter_from_py(pyfile, module_name='imported_file', **kwargs):
|
||||
for sim in _iter_queued():
|
||||
sims.append(sim)
|
||||
if not sims:
|
||||
for (_name, sim) in inspect.getmembers(module, lambda x: inspect.isclass(x) and issubclass(x, Simulation)):
|
||||
sims.append(sim(**kwargs))
|
||||
for (_name, env) in inspect.getmembers(module,
|
||||
lambda x: inspect.isclass(x) and
|
||||
issubclass(x, environment.Environment) and
|
||||
(getattr(x, "__module__", None) != environment.__name__)):
|
||||
sims.append(Simulation(model=env, **kwargs))
|
||||
del sys.modules[module_name]
|
||||
assert not _AVOID_RUNNING
|
||||
if not sims:
|
||||
|
269
soil/time.py
269
soil/time.py
@@ -1,7 +1,9 @@
|
||||
from mesa.time import BaseScheduler
|
||||
from queue import Empty
|
||||
from heapq import heappush, heappop, heapreplace
|
||||
from collections import deque
|
||||
import math
|
||||
import logging
|
||||
|
||||
from inspect import getsource
|
||||
from numbers import Number
|
||||
@@ -13,119 +15,62 @@ from mesa import Agent as MesaAgent
|
||||
|
||||
INFINITY = float("inf")
|
||||
|
||||
class Delay:
|
||||
"""A delay object which can be used both as a return value and as an awaitable (in async code)."""
|
||||
__slots__ = ("delta", )
|
||||
def __init__(self, delta):
|
||||
self.delta = float(delta)
|
||||
|
||||
def __float__(self):
|
||||
return self.delta
|
||||
|
||||
def __await__(self):
|
||||
return (yield self.delta)
|
||||
|
||||
|
||||
class DeadAgent(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class When:
|
||||
def __init__(self, time):
|
||||
if isinstance(time, When):
|
||||
return time
|
||||
self._time = time
|
||||
|
||||
def abs(self, time):
|
||||
return self._time
|
||||
|
||||
def schedule_next(self, time, delta, first=False):
|
||||
return (self._time, None)
|
||||
|
||||
|
||||
NEVER = When(INFINITY)
|
||||
|
||||
|
||||
class Delta(When):
|
||||
def __init__(self, delta):
|
||||
self._delta = delta
|
||||
|
||||
def abs(self, time):
|
||||
return self._time + self._delta
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, Delta):
|
||||
return self._delta == other._delta
|
||||
return False
|
||||
|
||||
def schedule_next(self, time, delta, first=False):
|
||||
return (time + self._delta, None)
|
||||
|
||||
def __repr__(self):
|
||||
return str(f"Delta({self._delta})")
|
||||
|
||||
|
||||
class BaseCond:
|
||||
def __init__(self, msg=None, delta=None, eager=False):
|
||||
self._msg = msg
|
||||
self._delta = delta
|
||||
self.eager = eager
|
||||
|
||||
def schedule_next(self, time, delta, first=False):
|
||||
if first and self.eager:
|
||||
return (time, self)
|
||||
if self._delta:
|
||||
delta = self._delta
|
||||
return (time + delta, self)
|
||||
|
||||
def return_value(self, agent):
|
||||
return None
|
||||
|
||||
def __repr__(self):
|
||||
return self._msg or self.__class__.__name__
|
||||
|
||||
|
||||
class Cond(BaseCond):
|
||||
def __init__(self, func, *args, **kwargs):
|
||||
self._func = func
|
||||
class DiscreteActivation(BaseScheduler):
|
||||
default_interval = 1
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def ready(self, agent, time):
|
||||
return self._func(agent)
|
||||
|
||||
def __repr__(self):
|
||||
if self._msg:
|
||||
return self._msg
|
||||
return str(f'Cond("{dedent(getsource(self._func)).strip()}")')
|
||||
if hasattr(self.model, 'default_interval'):
|
||||
self.default_interval = self.model.interval
|
||||
|
||||
|
||||
class TimedActivation(BaseScheduler):
|
||||
"""A scheduler which activates each agent when the agent requests.
|
||||
class PQueueActivation(DiscreteActivation):
|
||||
"""
|
||||
A scheduler which activates each agent with a delay returned by the agent's step method.
|
||||
If no delay is returned, a default of 1 is used.
|
||||
|
||||
In each activation, each agent will update its 'next_time'.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, shuffle=True, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._next = {}
|
||||
self._queue = []
|
||||
self._shuffle = shuffle
|
||||
# self.step_interval = getattr(self.model, "interval", 1)
|
||||
self.step_interval = self.model.interval
|
||||
self.logger = getattr(self.model, "logger", logger).getChild(f"time_{ self.model }")
|
||||
self.next_time = self.time
|
||||
|
||||
def add(self, agent: MesaAgent, when=None):
|
||||
if when is None:
|
||||
when = self.time
|
||||
elif isinstance(when, When):
|
||||
when = when.abs()
|
||||
else:
|
||||
when = float(when)
|
||||
|
||||
self._schedule(agent, None, when)
|
||||
super().add(agent)
|
||||
|
||||
def _schedule(self, agent, condition=None, when=None, replace=False):
|
||||
if condition:
|
||||
if not when:
|
||||
when, condition = condition.schedule_next(
|
||||
when or self.time, self.step_interval
|
||||
)
|
||||
else:
|
||||
if when is None:
|
||||
when = self.time + self.step_interval
|
||||
condition = None
|
||||
def _schedule(self, agent, when=None, replace=False):
|
||||
if when is None:
|
||||
when = self.time
|
||||
if self._shuffle:
|
||||
key = (when, self.model.random.random(), condition)
|
||||
key = (when, self.model.random.random())
|
||||
else:
|
||||
key = (when, agent.unique_id, condition)
|
||||
self._next[agent.unique_id] = key
|
||||
key = (when, agent.unique_id)
|
||||
if replace:
|
||||
heapreplace(self._queue, (key, agent))
|
||||
else:
|
||||
@@ -137,77 +82,147 @@ class TimedActivation(BaseScheduler):
|
||||
an agent will signal when it wants to be scheduled next.
|
||||
"""
|
||||
|
||||
self.logger.debug(f"Simulation step {self.time}")
|
||||
if not self.model.running or self.time == INFINITY:
|
||||
if self.time == INFINITY:
|
||||
return
|
||||
|
||||
self.logger.debug(f"Queue length: %s", len(self._queue))
|
||||
next_time = INFINITY
|
||||
|
||||
now = self.time
|
||||
|
||||
while self._queue:
|
||||
((when, _id, cond), agent) = self._queue[0]
|
||||
if when > self.time:
|
||||
((when, _id), agent) = self._queue[0]
|
||||
if when > now:
|
||||
next_time = when
|
||||
break
|
||||
|
||||
if cond:
|
||||
if not cond.ready(agent, self.time):
|
||||
self._schedule(agent, cond, replace=True)
|
||||
continue
|
||||
try:
|
||||
agent._last_return = cond.return_value(agent)
|
||||
except Exception as ex:
|
||||
agent._last_except = ex
|
||||
else:
|
||||
agent._last_return = None
|
||||
agent._last_except = None
|
||||
|
||||
self.logger.debug("Stepping agent %s", agent)
|
||||
self._next.pop(agent.unique_id, None)
|
||||
|
||||
try:
|
||||
returned = agent.step()
|
||||
when = agent.step() or self.default_interval
|
||||
when += now
|
||||
except DeadAgent:
|
||||
agent.alive = False
|
||||
heappop(self._queue)
|
||||
continue
|
||||
|
||||
# Check status for MESA agents
|
||||
if not getattr(agent, "alive", True):
|
||||
if when == INFINITY:
|
||||
heappop(self._queue)
|
||||
continue
|
||||
|
||||
if returned:
|
||||
next_check = returned.schedule_next(
|
||||
self.time, self.step_interval, first=True
|
||||
)
|
||||
self._schedule(agent, when=next_check[0], condition=next_check[1], replace=True)
|
||||
else:
|
||||
next_check = (self.time + self.step_interval, None)
|
||||
|
||||
self._schedule(agent, replace=True)
|
||||
self._schedule(agent, when, replace=True)
|
||||
|
||||
self.steps += 1
|
||||
|
||||
if not self._queue:
|
||||
self.time = next_time
|
||||
|
||||
if next_time == INFINITY:
|
||||
self.model.running = False
|
||||
self.time = INFINITY
|
||||
return
|
||||
|
||||
next_time = self._queue[0][0][0]
|
||||
|
||||
if next_time < self.time:
|
||||
raise Exception(
|
||||
f"An agent has been scheduled for a time in the past, there is probably an error ({when} < {self.time})"
|
||||
)
|
||||
self.logger.debug("Updating time step: %s -> %s ", self.time, next_time)
|
||||
class TimedActivation(DiscreteActivation):
|
||||
'''A discrete-time scheduler that has time buckets with agents that should be woken at the same time instant.'''
|
||||
def __init__(self, *args, shuffle=True, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._queue = deque()
|
||||
self._shuffle = shuffle
|
||||
self.logger = getattr(self.model, "logger", logger).getChild(f"time_{ self.model }")
|
||||
self.next_time = self.time
|
||||
|
||||
self.time = next_time
|
||||
def add(self, agent: MesaAgent, when=None):
|
||||
if when is None:
|
||||
when = self.time
|
||||
else:
|
||||
when = float(when)
|
||||
self._schedule(agent, None, when)
|
||||
super().add(agent)
|
||||
|
||||
def _schedule(self, agent, when=None, replace=False):
|
||||
when = when or self.time
|
||||
pos = len(self._queue)
|
||||
for (ix, l) in enumerate(self._queue):
|
||||
if l[0] == when:
|
||||
l[1].append(agent)
|
||||
return
|
||||
if l[0] > when:
|
||||
pos = ix
|
||||
break
|
||||
self._queue.insert(pos, (when, [agent]))
|
||||
|
||||
def step(self) -> None:
|
||||
"""
|
||||
Executes agents in order, one at a time. After each step,
|
||||
an agent will signal when it wants to be scheduled next.
|
||||
"""
|
||||
if not self._queue:
|
||||
return
|
||||
|
||||
now = self.time
|
||||
|
||||
next_time = self._queue[0][0]
|
||||
|
||||
if next_time > now:
|
||||
self.time = next_time
|
||||
return
|
||||
|
||||
bucket = self._queue.popleft()[1]
|
||||
if self._shuffle:
|
||||
self.model.random.shuffle(bucket)
|
||||
for agent in bucket:
|
||||
try:
|
||||
when = agent.step() or self.default_interval
|
||||
when += now
|
||||
except DeadAgent:
|
||||
continue
|
||||
|
||||
if when != INFINITY:
|
||||
self._schedule(agent, when, replace=True)
|
||||
|
||||
self.steps += 1
|
||||
if self._queue:
|
||||
self.time = self._queue[0][0]
|
||||
else:
|
||||
self.time = INFINITY
|
||||
|
||||
|
||||
class ShuffledTimedActivation(TimedActivation):
|
||||
'''
|
||||
A TimedActivation scheduler that processes events in random order.
|
||||
'''
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, shuffle=True, **kwargs)
|
||||
|
||||
|
||||
class OrderedTimedActivation(TimedActivation):
|
||||
'''
|
||||
A TimedActivation scheduler that always processes events in
|
||||
the same order.
|
||||
'''
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, shuffle=False, **kwargs)
|
||||
|
||||
|
||||
Scheduler = TimedActivation
|
||||
|
||||
|
||||
class Lockstepper:
|
||||
'''
|
||||
A wrapper class to produce discrete-event schedulers that behave like
|
||||
fixed-time schedulers.
|
||||
'''
|
||||
|
||||
def __init__(self, scheduler: BaseScheduler, interval=1):
|
||||
self.scheduler = scheduler
|
||||
self.default_interval = interval
|
||||
self.time = scheduler.time
|
||||
self.steps = 0
|
||||
|
||||
def step(self):
|
||||
end_time = self.time + self.default_interval
|
||||
res = None
|
||||
while self.scheduler.time < end_time:
|
||||
res = self.scheduler.step()
|
||||
self.time = end_time
|
||||
self.steps += 1
|
||||
return res
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.scheduler, name)
|
@@ -24,7 +24,7 @@ consoleHandler = logging.StreamHandler()
|
||||
consoleHandler.setFormatter(logFormatter)
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
level=logging.WARNING,
|
||||
handlers=[
|
||||
consoleHandler,
|
||||
],
|
||||
|
141
soil/visualization.py
Normal file
141
soil/visualization.py
Normal file
@@ -0,0 +1,141 @@
|
||||
from typing import Optional
|
||||
|
||||
import sys
|
||||
import threading
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import reacton.ipywidgets as widgets
|
||||
import solara
|
||||
from solara.alias import rv
|
||||
|
||||
import mesa.experimental.components.matplotlib as components_matplotlib
|
||||
from mesa.experimental.jupyter_viz import *
|
||||
from matplotlib.figure import Figure
|
||||
import networkx as nx
|
||||
|
||||
|
||||
class Controller:
|
||||
'''
|
||||
A visualization controller that holds a reference to a model so that it can be modified or queried while the simulation is still running.
|
||||
'''
|
||||
def __init__(self):
|
||||
self.model = None
|
||||
|
||||
|
||||
def JupyterViz(*args, **kwargs):
|
||||
c = Controller()
|
||||
page = JupyterPage(*args, controller=c, **kwargs)
|
||||
page.controller = c
|
||||
return page
|
||||
|
||||
|
||||
@solara.component
|
||||
def JupyterPage(
|
||||
model_class,
|
||||
model_params,
|
||||
controller=None,
|
||||
measures=None,
|
||||
name="Mesa Model",
|
||||
agent_portrayal=None,
|
||||
space_drawer="default",
|
||||
play_interval=150,
|
||||
columns=2,
|
||||
):
|
||||
"""Initialize a component to visualize a model.
|
||||
Args:
|
||||
model_class: class of the model to instantiate
|
||||
model_params: parameters for initializing the model
|
||||
measures: list of callables or data attributes to plot
|
||||
name: name for display
|
||||
agent_portrayal: options for rendering agents (dictionary)
|
||||
space_drawer: method to render the agent space for
|
||||
the model; default implementation is the `SpaceMatplotlib` component;
|
||||
simulations with no space to visualize should
|
||||
specify `space_drawer=False`
|
||||
play_interval: play interval (default: 150)
|
||||
"""
|
||||
if controller is None:
|
||||
controller = Controller()
|
||||
|
||||
current_step = solara.use_reactive(0)
|
||||
|
||||
# 1. Set up model parameters
|
||||
user_params, fixed_params = split_model_params(model_params)
|
||||
model_parameters, set_model_parameters = solara.use_state(
|
||||
{**fixed_params, **{k: v["value"] for k, v in user_params.items()}}
|
||||
)
|
||||
|
||||
# 2. Set up Model
|
||||
def make_model():
|
||||
model = model_class(**model_parameters)
|
||||
current_step.value = 0
|
||||
controller.model = model
|
||||
return model
|
||||
|
||||
reset_counter = solara.use_reactive(0)
|
||||
model = solara.use_memo(
|
||||
make_model, dependencies=[*list(model_parameters.values()), reset_counter.value]
|
||||
)
|
||||
|
||||
def handle_change_model_params(name: str, value: any):
|
||||
set_model_parameters({**model_parameters, name: value})
|
||||
|
||||
# 3. Set up UI
|
||||
with solara.AppBar():
|
||||
solara.AppBarTitle(name)
|
||||
|
||||
with solara.GridFixed(columns=2):
|
||||
UserInputs(user_params, on_change=handle_change_model_params)
|
||||
ModelController(model, play_interval, current_step, reset_counter)
|
||||
solara.Markdown(md_text=f"###Step: {current_step} - Time: {model.schedule.time } ")
|
||||
|
||||
with solara.GridFixed(columns=columns):
|
||||
# 4. Space
|
||||
if space_drawer == "default":
|
||||
# draw with the default implementation
|
||||
components_matplotlib.SpaceMatplotlib(
|
||||
model, agent_portrayal, dependencies=[current_step.value]
|
||||
)
|
||||
elif space_drawer:
|
||||
# if specified, draw agent space with an alternate renderer
|
||||
space_drawer(model, agent_portrayal, dependencies=[current_step.value])
|
||||
# otherwise, do nothing (do not draw space)
|
||||
|
||||
# 5. Plots
|
||||
for measure in measures:
|
||||
if callable(measure):
|
||||
# Is a custom object
|
||||
measure(model)
|
||||
else:
|
||||
components_matplotlib.make_plot(model, measure)
|
||||
|
||||
|
||||
@solara.component
|
||||
def NetworkDrawer(model, network_portrayal, dependencies: Optional[list[any]] = None):
|
||||
space_fig = Figure()
|
||||
space_ax = space_fig.subplots()
|
||||
graph = model.grid.G
|
||||
nx.draw(
|
||||
graph,
|
||||
ax=space_ax,
|
||||
**network_portrayal(graph),
|
||||
)
|
||||
solara.FigureMatplotlib(space_fig, format="png", dependencies=dependencies)
|
||||
|
||||
|
||||
try:
|
||||
import osmnx as ox
|
||||
|
||||
@solara.component
|
||||
def GeoNetworkDrawer(model, network_portrayal, dependencies: Optional[list[any]] = None):
|
||||
space_fig = Figure()
|
||||
space_ax = space_fig.subplots()
|
||||
graph = model.grid.G
|
||||
ox.plot_graph(
|
||||
graph,
|
||||
ax=space_ax,
|
||||
**network_portrayal(graph),
|
||||
)
|
||||
solara.FigureMatplotlib(space_fig, format="png", dependencies=dependencies)
|
||||
except ImportError:
|
||||
pass
|
@@ -2,3 +2,6 @@ pytest
|
||||
pytest-profiling
|
||||
scipy>=1.3
|
||||
tornado
|
||||
nbconvert==7.3.1
|
||||
nbformat==5.8.0
|
||||
jupyter==1.0.0
|
@@ -17,7 +17,7 @@ class TestAgents(TestCase):
|
||||
"""The last step of a dead agent should return time.INFINITY"""
|
||||
d = Dead(unique_id=0, model=environment.Environment())
|
||||
ret = d.step()
|
||||
assert ret == stime.NEVER
|
||||
assert ret == stime.INFINITY
|
||||
|
||||
def test_die_raises_exception(self):
|
||||
"""A dead agent should raise an exception if it is stepped after death"""
|
||||
@@ -52,23 +52,25 @@ class TestAgents(TestCase):
|
||||
|
||||
def test_state_decorator(self):
|
||||
class MyAgent(agents.FSM):
|
||||
run = 0
|
||||
times_run = 0
|
||||
|
||||
@agents.state("original", default=True)
|
||||
def root(self):
|
||||
self.run += 1
|
||||
return self.other
|
||||
|
||||
@agents.state
|
||||
def other(self):
|
||||
self.run += 1
|
||||
self.times_run += 1
|
||||
|
||||
e = environment.Environment()
|
||||
a = e.add_agent(MyAgent)
|
||||
e.step()
|
||||
assert a.run == 1
|
||||
assert a.times_run == 0
|
||||
a.step()
|
||||
print("DONE")
|
||||
assert a.times_run == 1
|
||||
assert a.state_id == MyAgent.other.id
|
||||
a.step()
|
||||
assert a.times_run == 2
|
||||
|
||||
def test_broadcast(self):
|
||||
"""
|
||||
@@ -86,7 +88,7 @@ class TestAgents(TestCase):
|
||||
except Exception as ex:
|
||||
print(ex)
|
||||
while True:
|
||||
self.check_messages()
|
||||
self.process_messages()
|
||||
yield
|
||||
|
||||
def on_receive(self, msg, sender=None):
|
||||
@@ -132,14 +134,14 @@ class TestAgents(TestCase):
|
||||
while True:
|
||||
if pongs or not pings: # First agent, or anyone after that
|
||||
pings.append(self.now)
|
||||
response = yield target.ask("PING")
|
||||
response = yield from target.ask("PING")
|
||||
responses.append(response)
|
||||
else:
|
||||
print("NOT sending ping")
|
||||
print("Checking msgs")
|
||||
# Do not block if we have already received a PING
|
||||
if not self.check_messages():
|
||||
yield self.received()
|
||||
if not self.process_messages():
|
||||
yield from self.received()
|
||||
print("done")
|
||||
|
||||
def on_receive(self, msg, sender=None):
|
||||
@@ -168,10 +170,206 @@ class TestAgents(TestCase):
|
||||
e = environment.Environment()
|
||||
e.add_agent(agent_class=agents.BaseAgent)
|
||||
e.add_agent(agent_class=agents.Evented)
|
||||
base = list(e.agents(agent_class=agents.BaseAgent))
|
||||
base = list(e.get_agents(agent_class=agents.BaseAgent))
|
||||
assert len(base) == 2
|
||||
ev = list(e.agents(agent_class=agents.Evented))
|
||||
ev = list(e.get_agents(agent_class=agents.Evented))
|
||||
assert len(ev) == 1
|
||||
assert ev[0].unique_id == 1
|
||||
null = list(e.agents(unique_ids=[0, 1], agent_class=agents.NetworkAgent))
|
||||
assert not null
|
||||
null = list(e.get_agents(unique_ids=[0, 1], agent_class=agents.NetworkAgent))
|
||||
assert not null
|
||||
|
||||
def test_agent_return(self):
|
||||
'''
|
||||
An agent should be able to cycle through different states and control when it
|
||||
should be awaken.
|
||||
'''
|
||||
class TestAgent(agents.Agent):
|
||||
@agents.state(default=True)
|
||||
def one(self):
|
||||
return self.two
|
||||
|
||||
@agents.state
|
||||
def two(self):
|
||||
return self.three.at(10)
|
||||
|
||||
@agents.state
|
||||
def three(self):
|
||||
return self.four.delay(1)
|
||||
|
||||
@agents.state
|
||||
def four(self):
|
||||
yield self.delay(2)
|
||||
return self.five.delay(3)
|
||||
|
||||
@agents.state
|
||||
def five(self):
|
||||
return self.delay(1)
|
||||
|
||||
model = environment.Environment()
|
||||
a = model.add_agent(TestAgent)
|
||||
assert a.state_id == TestAgent.one.id
|
||||
assert a.now == 0
|
||||
model.step()
|
||||
assert a.state_id == TestAgent.two.id
|
||||
assert a.now == 1
|
||||
model.step()
|
||||
assert a.state_id == TestAgent.three.id
|
||||
assert a.now == 10
|
||||
model.step()
|
||||
assert a.state_id == TestAgent.four.id
|
||||
assert a.now == 11
|
||||
model.step()
|
||||
assert a.state_id == TestAgent.four.id
|
||||
assert a.now == 13
|
||||
model.step()
|
||||
assert a.state_id == TestAgent.five.id
|
||||
assert a.now == 16
|
||||
model.step()
|
||||
assert a.state_id == TestAgent.five.id
|
||||
assert a.now == 17
|
||||
|
||||
def test_agent_async(self):
|
||||
'''
|
||||
Async functions should also be valid states.
|
||||
'''
|
||||
|
||||
class TestAgent(agents.Agent):
|
||||
@agents.state(default=True)
|
||||
def one(self):
|
||||
return self.two
|
||||
|
||||
@agents.state
|
||||
def two(self):
|
||||
return self.three.at(10)
|
||||
|
||||
@agents.state
|
||||
def three(self):
|
||||
return self.four.delay(1)
|
||||
|
||||
@agents.state
|
||||
async def four(self):
|
||||
await self.delay(2)
|
||||
return self.five.delay(3)
|
||||
|
||||
@agents.state
|
||||
def five(self):
|
||||
return self.delay(1)
|
||||
|
||||
model = environment.Environment()
|
||||
a = model.add_agent(TestAgent)
|
||||
assert a.now == 0
|
||||
assert a.state_id == TestAgent.one.id
|
||||
model.step()
|
||||
assert a.now == 1
|
||||
assert a.state_id == TestAgent.two.id
|
||||
model.step()
|
||||
assert a.now == 10
|
||||
assert a.state_id == TestAgent.three.id
|
||||
model.step()
|
||||
assert a.state_id == TestAgent.four.id
|
||||
assert a.now == 11
|
||||
model.step()
|
||||
assert a.state_id == TestAgent.four.id
|
||||
assert a.now == 13
|
||||
model.step()
|
||||
assert a.state_id == TestAgent.five.id
|
||||
assert a.now == 16
|
||||
model.step()
|
||||
assert a.state_id == TestAgent.five.id
|
||||
assert a.now == 17
|
||||
|
||||
def test_agent_return_step(self):
|
||||
'''
|
||||
The same result as the previous test should be achievable by manually
|
||||
handling the agent state.
|
||||
'''
|
||||
class TestAgent(agents.Agent):
|
||||
my_state = 1
|
||||
my_count = 0
|
||||
|
||||
def step(self):
|
||||
if self.my_state == 1:
|
||||
self.my_state = 2
|
||||
return None
|
||||
elif self.my_state == 2:
|
||||
self.my_state = 3
|
||||
return self.at(10)
|
||||
elif self.my_state == 3:
|
||||
self.my_state = 4
|
||||
self.my_count = 0
|
||||
return self.delay(1)
|
||||
elif self.my_state == 4:
|
||||
self.my_count += 1
|
||||
if self.my_count == 1:
|
||||
return self.delay(2)
|
||||
self.my_state = 5
|
||||
return self.delay(3)
|
||||
elif self.my_state == 5:
|
||||
return self.delay(1)
|
||||
|
||||
model = environment.Environment()
|
||||
a = model.add_agent(TestAgent)
|
||||
assert a.my_state == 1
|
||||
assert a.now == 0
|
||||
model.step()
|
||||
assert a.now == 1
|
||||
assert a.my_state == 2
|
||||
model.step()
|
||||
assert a.now == 10
|
||||
assert a.my_state == 3
|
||||
model.step()
|
||||
assert a.now == 11
|
||||
assert a.my_state == 4
|
||||
model.step()
|
||||
assert a.now == 13
|
||||
assert a.my_state == 4
|
||||
model.step()
|
||||
assert a.now == 16
|
||||
assert a.my_state == 5
|
||||
model.step()
|
||||
assert a.now == 17
|
||||
assert a.my_state == 5
|
||||
|
||||
def test_agent_return_step_async(self):
|
||||
'''
|
||||
The same result as the previous test should be achievable by manually
|
||||
handling the agent state.
|
||||
'''
|
||||
class TestAgent(agents.Agent):
|
||||
my_state = 1
|
||||
|
||||
async def step(self):
|
||||
self.my_state = 2
|
||||
await self.delay()
|
||||
self.my_state = 3
|
||||
await self.at(10)
|
||||
self.my_state = 4
|
||||
await self.delay(1)
|
||||
await self.delay(2)
|
||||
self.my_state = 5
|
||||
await self.delay(3)
|
||||
while True:
|
||||
await self.delay(1)
|
||||
|
||||
model = environment.Environment()
|
||||
a = model.add_agent(TestAgent)
|
||||
assert a.my_state == 1
|
||||
assert a.now == 0
|
||||
model.step()
|
||||
assert a.now == 1
|
||||
assert a.my_state == 2
|
||||
model.step()
|
||||
assert a.now == 10
|
||||
assert a.my_state == 3
|
||||
model.step()
|
||||
assert a.now == 11
|
||||
assert a.my_state == 4
|
||||
model.step()
|
||||
assert a.now == 13
|
||||
assert a.my_state == 4
|
||||
model.step()
|
||||
assert a.now == 16
|
||||
assert a.my_state == 5
|
||||
model.step()
|
||||
assert a.now == 17
|
||||
assert a.my_state == 5
|
@@ -29,15 +29,12 @@ class TestConfig(TestCase):
|
||||
def test_torvalds_config(self):
|
||||
sim = simulation.from_config(os.path.join(ROOT, "test_config.yml"))
|
||||
MAX_STEPS = 10
|
||||
INTERVAL = 2
|
||||
assert sim.interval == INTERVAL
|
||||
assert sim.max_steps == MAX_STEPS
|
||||
envs = sim.run()
|
||||
assert len(envs) == 1
|
||||
env = envs[0]
|
||||
assert env.interval == 2
|
||||
assert env.count_agents() == 3
|
||||
assert env.now == INTERVAL * MAX_STEPS
|
||||
assert env.now == MAX_STEPS
|
||||
|
||||
|
||||
def make_example_test(path, cfg):
|
||||
|
@@ -1,5 +1,4 @@
|
||||
---
|
||||
source_file: "../examples/torvalds_sim.py"
|
||||
model: "TorvaldsEnv"
|
||||
max_steps: 10
|
||||
interval: 2
|
||||
max_steps: 10
|
@@ -88,7 +88,7 @@ class Exporters(TestCase):
|
||||
parameters=dict(
|
||||
network_generator="complete_graph",
|
||||
network_params={"n": n_nodes},
|
||||
agent_class="CounterModel",
|
||||
agent_class=agents.CounterModel,
|
||||
agent_reporters={"times": "times"},
|
||||
),
|
||||
max_time=max_time,
|
||||
|
18
tests/test_ipython.py
Normal file
18
tests/test_ipython.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from unittest import TestCase
|
||||
import os
|
||||
import nbformat
|
||||
from nbconvert.preprocessors import ExecutePreprocessor
|
||||
|
||||
ROOT = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
class TestNotebooks(TestCase):
|
||||
def test_tutorial(self):
|
||||
notebook = os.path.join(ROOT, "../docs/tutorial/soil_tutorial.ipynb")
|
||||
with open(notebook) as f:
|
||||
nb = nbformat.read(f, as_version=4)
|
||||
ep = ExecutePreprocessor(timeout=60000)
|
||||
try:
|
||||
assert ep.preprocess(nb) is not None, f"Got empty notebook for {notebook}"
|
||||
except Exception:
|
||||
assert False, f"Failed executing {notebook}"
|
||||
|
@@ -7,8 +7,6 @@ from functools import partial
|
||||
|
||||
from os.path import join
|
||||
from soil import simulation, Environment, agents, network, serialization, utils, config, from_file
|
||||
from soil.time import Delta
|
||||
|
||||
from mesa import Agent as MesaAgent
|
||||
|
||||
ROOT = os.path.abspath(os.path.dirname(__file__))
|
||||
@@ -114,7 +112,6 @@ class TestMain(TestCase):
|
||||
def test_serialize_class(self):
|
||||
ser, name = serialization.serialize(agents.BaseAgent, known_modules=[])
|
||||
assert name == "soil.agents.BaseAgent"
|
||||
assert ser == agents.BaseAgent
|
||||
|
||||
ser, name = serialization.serialize(
|
||||
agents.BaseAgent,
|
||||
@@ -123,11 +120,9 @@ class TestMain(TestCase):
|
||||
],
|
||||
)
|
||||
assert name == "BaseAgent"
|
||||
assert ser == agents.BaseAgent
|
||||
|
||||
ser, name = serialization.serialize(CustomAgent)
|
||||
assert name == "test_main.CustomAgent"
|
||||
assert ser == CustomAgent
|
||||
pickle.dumps(ser)
|
||||
|
||||
def test_serialize_builtin_types(self):
|
||||
@@ -168,7 +163,6 @@ class TestMain(TestCase):
|
||||
|
||||
def test_fsm(self):
|
||||
"""Basic state change"""
|
||||
|
||||
class ToggleAgent(agents.FSM):
|
||||
@agents.default_state
|
||||
@agents.state
|
||||
@@ -193,7 +187,7 @@ class TestMain(TestCase):
|
||||
@agents.default_state
|
||||
@agents.state
|
||||
def ping(self):
|
||||
return self.pong, 2
|
||||
return self.pong.delay(2)
|
||||
|
||||
@agents.state
|
||||
def pong(self):
|
||||
@@ -203,7 +197,7 @@ class TestMain(TestCase):
|
||||
when = a.step()
|
||||
assert when == 2
|
||||
when = a.step()
|
||||
assert when == Delta(a.interval)
|
||||
assert when == None
|
||||
|
||||
def test_load_sim(self):
|
||||
"""Make sure at least one of the examples can be loaded"""
|
||||
@@ -232,4 +226,4 @@ class TestMain(TestCase):
|
||||
assert len(configs) == len(a) * len(b)
|
||||
for i in a:
|
||||
for j in b:
|
||||
assert {"a": i, "b": j} in configs
|
||||
assert {"a": i, "b": j} in configs
|
||||
|
@@ -4,26 +4,6 @@ from soil import time, agents, environment
|
||||
|
||||
|
||||
class TestMain(TestCase):
|
||||
def test_cond(self):
|
||||
"""
|
||||
A condition should match a When if the concition is True
|
||||
"""
|
||||
|
||||
t = time.Cond(lambda t: True)
|
||||
f = time.Cond(lambda t: False)
|
||||
for i in range(10):
|
||||
w = time.When(i)
|
||||
assert w == t
|
||||
assert w is not f
|
||||
|
||||
def test_cond(self):
|
||||
"""
|
||||
Comparing a Cond to a Delta should always return False
|
||||
"""
|
||||
|
||||
c = time.Cond(lambda t: False)
|
||||
d = time.Delta(1)
|
||||
assert c is not d
|
||||
|
||||
def test_cond_env(self):
|
||||
""" """
|
||||
@@ -36,11 +16,12 @@ class TestMain(TestCase):
|
||||
|
||||
class CondAgent(agents.BaseAgent):
|
||||
def step(self):
|
||||
nonlocal done
|
||||
nonlocal done, times_started, times_asleep, times_awakened
|
||||
times_started.append(self.now)
|
||||
while True:
|
||||
times_asleep.append(self.now)
|
||||
yield time.Cond(lambda agent: agent.now >= 10, delta=2)
|
||||
while self.now < 10:
|
||||
yield self.delay(2)
|
||||
times_awakened.append(self.now)
|
||||
if self.now >= 10:
|
||||
break
|
||||
@@ -57,7 +38,6 @@ class TestMain(TestCase):
|
||||
assert times_started == [0]
|
||||
assert times_awakened == [10]
|
||||
assert done == [10]
|
||||
# The first time will produce the Cond.
|
||||
assert env.schedule.steps == 6
|
||||
assert len(times) == 6
|
||||
|
||||
@@ -65,11 +45,10 @@ class TestMain(TestCase):
|
||||
times.append(env.now)
|
||||
env.step()
|
||||
|
||||
assert times == [0, 2, 4, 6, 8, 10, 11]
|
||||
assert times == [0, 2, 4, 6, 8, 10, 11, 12]
|
||||
assert env.schedule.time == 13
|
||||
assert times_started == [0, 11]
|
||||
assert times_awakened == [10]
|
||||
assert done == [10]
|
||||
# Once more to yield the cond, another one to continue
|
||||
assert env.schedule.steps == 7
|
||||
assert len(times) == 7
|
||||
assert times_started == [0, 11, 12]
|
||||
assert times_awakened == [10, 11, 12]
|
||||
assert done == [10, 11, 12]
|
||||
assert env.schedule.steps == 8
|
||||
assert len(times) == 8
|
||||
|
Reference in New Issue
Block a user