mirror of
https://github.com/gsi-upm/soil
synced 2024-11-24 03:52:27 +00:00
1.0pre4
This commit is contained in:
parent
5e93399d58
commit
f49be3af68
@ -13,11 +13,13 @@ 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.
|
* 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
|
* 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
|
* 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.
|
* 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>`
|
* 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
|
### Changed
|
||||||
* Configuration schema (`Simulation`) is very simplified. All simulations should be checked
|
* Configuration schema (`Simulation`) is very simplified. All simulations should be checked
|
||||||
|
* Agents that wish to
|
||||||
* Model / environment variables are expected (but not enforced) to be a single value. This is done to more closely align with mesa
|
* Model / environment variables are expected (but not enforced) to be a single value. This is done to more closely align with mesa
|
||||||
* `Exporter.iteration_end` now takes two parameters: `env` (same as before) and `params` (specific parameters for this environment). We considered including a `parameters` attribute in the environment, but this would not be compatible with mesa.
|
* `Exporter.iteration_end` now takes two parameters: `env` (same as before) and `params` (specific parameters for this environment). We considered including a `parameters` attribute in the environment, but this would not be compatible with mesa.
|
||||||
* `num_trials` renamed to `iterations`
|
* `num_trials` renamed to `iterations`
|
||||||
@ -26,6 +28,7 @@ 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
|
* Simulation results for every iteration of a simulation with the same name are stored in a single `sqlite` database
|
||||||
|
|
||||||
### Removed
|
### 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.
|
* 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]
|
## [0.20.8]
|
||||||
|
@ -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,7 +2,7 @@ Welcome to Soil's documentation!
|
|||||||
================================
|
================================
|
||||||
|
|
||||||
Soil is an opinionated Agent-based Social Simulator in Python focused on Social Networks.
|
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 <soil_tutorial>` and the `examples on GitHub <https://github.com/gsi-upm/soil/tree/master/examples>`.
|
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):.
|
Soil can be installed through pip (see more details in the :doc:`installation` page):.
|
||||||
|
|
||||||
@ -49,6 +49,7 @@ If you use Soil in your research, do not forget to cite this paper:
|
|||||||
installation
|
installation
|
||||||
Tutorial <tutorial/soil_tutorial>
|
Tutorial <tutorial/soil_tutorial>
|
||||||
notes_v1.0
|
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,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.
|
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.
|
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)
|
- 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)
|
- 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
|
- 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 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.
|
@ -86,7 +86,7 @@ class Driver(Evented, FSM):
|
|||||||
earnings = 0
|
earnings = 0
|
||||||
|
|
||||||
def on_receive(self, msg, sender):
|
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:
|
if self.journey is None and isinstance(msg, Journey) and msg.driver is None:
|
||||||
msg.driver = self
|
msg.driver = self
|
||||||
self.journey = msg
|
self.journey = msg
|
||||||
@ -95,15 +95,14 @@ class Driver(Evented, FSM):
|
|||||||
"""If there are no more passengers, stop forever"""
|
"""If there are no more passengers, stop forever"""
|
||||||
c = self.count_agents(agent_class=Passenger)
|
c = self.count_agents(agent_class=Passenger)
|
||||||
self.debug(f"Passengers left {c}")
|
self.debug(f"Passengers left {c}")
|
||||||
if not c:
|
return c
|
||||||
self.die("No more passengers")
|
|
||||||
|
|
||||||
@default_state
|
@state(default=True)
|
||||||
@state
|
async def wandering(self):
|
||||||
def wandering(self):
|
|
||||||
"""Move around the city until a journey is accepted"""
|
"""Move around the city until a journey is accepted"""
|
||||||
target = None
|
target = None
|
||||||
self.check_passengers()
|
if not self.check_passengers():
|
||||||
|
return self.die("No passengers left")
|
||||||
self.journey = None
|
self.journey = None
|
||||||
while self.journey is None: # No potential journeys detected (see on_receive)
|
while self.journey is None: # No potential journeys detected (see on_receive)
|
||||||
if target is None or not self.move_towards(target):
|
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.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
|
# This will call on_receive behind the scenes, and the agent's status will be updated
|
||||||
self.check_messages()
|
self.process_messages()
|
||||||
yield Delta(30) # Wait at least 30 seconds before checking again
|
await self.delay(30) # Wait at least 30 seconds before checking again
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Re-send the journey to the passenger, to confirm that we have been selected
|
# 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:
|
except events.TimedOut:
|
||||||
# No journey has been accepted. Try again
|
# No journey has been accepted. Try again
|
||||||
self.journey = None
|
self.journey = None
|
||||||
@ -127,18 +127,19 @@ class Driver(Evented, FSM):
|
|||||||
return self.driving
|
return self.driving
|
||||||
|
|
||||||
@state
|
@state
|
||||||
def driving(self):
|
async def driving(self):
|
||||||
"""The journey has been accepted. Pick them up and take them to their destination"""
|
"""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}")
|
self.info(f"Driving towards Passenger {self.journey.passenger.unique_id}")
|
||||||
while self.move_towards(self.journey.origin):
|
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}")
|
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):
|
while self.move_towards(self.journey.destination, with_passenger=True):
|
||||||
yield
|
await self.delay()
|
||||||
self.info("Arrived at destination")
|
self.info("Arrived at destination")
|
||||||
self.earnings += self.journey.tip
|
self.earnings += self.journey.tip
|
||||||
self.model.total_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
|
return self.wandering
|
||||||
|
|
||||||
def move_towards(self, target, with_passenger=False):
|
def move_towards(self, target, with_passenger=False):
|
||||||
@ -167,7 +168,7 @@ class Passenger(Evented, FSM):
|
|||||||
pos = None
|
pos = None
|
||||||
|
|
||||||
def on_receive(self, msg, sender):
|
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):
|
if isinstance(msg, Journey):
|
||||||
self.journey = msg
|
self.journey = msg
|
||||||
@ -175,7 +176,7 @@ class Passenger(Evented, FSM):
|
|||||||
|
|
||||||
@default_state
|
@default_state
|
||||||
@state
|
@state
|
||||||
def asking(self):
|
async def asking(self):
|
||||||
destination = (
|
destination = (
|
||||||
self.random.randint(0, self.model.grid.height-1),
|
self.random.randint(0, self.model.grid.height-1),
|
||||||
self.random.randint(0, self.model.grid.width-1),
|
self.random.randint(0, self.model.grid.width-1),
|
||||||
@ -195,9 +196,9 @@ class Passenger(Evented, FSM):
|
|||||||
while not self.journey:
|
while not self.journey:
|
||||||
self.debug(f"Waiting for responses at: { self.pos }")
|
self.debug(f"Waiting for responses at: { self.pos }")
|
||||||
try:
|
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
|
# 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:
|
except events.TimedOut:
|
||||||
self.info(f"Still no response. Waiting at: { self.pos }")
|
self.info(f"Still no response. Waiting at: { self.pos }")
|
||||||
self.model.broadcast(
|
self.model.broadcast(
|
||||||
@ -208,13 +209,13 @@ class Passenger(Evented, FSM):
|
|||||||
return self.driving_home
|
return self.driving_home
|
||||||
|
|
||||||
@state
|
@state
|
||||||
def driving_home(self):
|
async def driving_home(self):
|
||||||
while (
|
while (
|
||||||
self.pos[0] != self.journey.destination[0]
|
self.pos[0] != self.journey.destination[0]
|
||||||
or self.pos[1] != self.journey.destination[1]
|
or self.pos[1] != self.journey.destination[1]
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
yield self.received(timeout=60)
|
await self.received(timeout=60)
|
||||||
except events.TimedOut:
|
except events.TimedOut:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -228,4 +229,4 @@ simulation = Simulation(name="RideHailing",
|
|||||||
parameters=dict(n_passengers=2))
|
parameters=dict(n_passengers=2))
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
easy(simulation)
|
easy(simulation)
|
@ -33,7 +33,7 @@ class GeneratorEnv(Environment):
|
|||||||
self.add_agents(CounterModel)
|
self.add_agents(CounterModel)
|
||||||
|
|
||||||
|
|
||||||
sim = Simulation(model=GeneratorEnv, max_steps=10, interval=1)
|
sim = Simulation(model=GeneratorEnv, max_steps=10)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
sim.run(dump=False)
|
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.agents import FSM, state, default_state
|
||||||
from soil.time import Delta
|
|
||||||
|
|
||||||
|
|
||||||
class Fibonacci(FSM):
|
class Fibonacci(FSM):
|
||||||
@ -11,17 +10,17 @@ class Fibonacci(FSM):
|
|||||||
def counting(self):
|
def counting(self):
|
||||||
self.log("Stopping at {}".format(self.now))
|
self.log("Stopping at {}".format(self.now))
|
||||||
prev, self["prev"] = self["prev"], max([self.now, self["prev"]])
|
prev, self["prev"] = self["prev"], max([self.now, self["prev"]])
|
||||||
return None, Delta(prev)
|
return self.delay(prev)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Odds(FSM):
|
class Odds(FSM):
|
||||||
"""Agent that only executes in odd t_steps"""
|
"""Agent that only executes in odd t_steps"""
|
||||||
|
|
||||||
@default_state
|
@state(default=True)
|
||||||
@state
|
|
||||||
def odds(self):
|
def odds(self):
|
||||||
self.log("Stopping at {}".format(self.now))
|
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
|
from soil import Environment, Simulation
|
||||||
@ -35,7 +34,7 @@ class TimeoutsEnv(Environment):
|
|||||||
self.add_agent(agent_class=Odds, node_id=1)
|
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__":
|
if __name__ == "__main__":
|
||||||
sim.run(dump=False)
|
sim.run(dump=False)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from soil import Simulation
|
from soil import Simulation
|
||||||
from social_wealth import MoneyEnv, graph_generator
|
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__":
|
if __name__ == "__main__":
|
||||||
sim.run()
|
sim.run()
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
from soil import FSM, state, default_state, BaseAgent, NetworkAgent, Environment, Simulation
|
from soil import FSM, state, default_state, BaseAgent, NetworkAgent, Environment, Simulation
|
||||||
from soil.time import Delta
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
import logging
|
import logging
|
||||||
@ -35,7 +34,7 @@ class Rabbit(FSM, NetworkAgent):
|
|||||||
self.info("I am a newborn.")
|
self.info("I am a newborn.")
|
||||||
self.birth = self.now
|
self.birth = self.now
|
||||||
self.offspring = 0
|
self.offspring = 0
|
||||||
return self.youngling, Delta(self.sexual_maturity - self.age)
|
return self.youngling.delay(self.sexual_maturity - self.age)
|
||||||
|
|
||||||
@state
|
@state
|
||||||
def youngling(self):
|
def youngling(self):
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Example of setting a
|
|
||||||
Example of a fully programmatic simulation, without definition files.
|
Example of a fully programmatic simulation, without definition files.
|
||||||
"""
|
"""
|
||||||
from soil import Simulation, agents, Environment
|
from soil import Simulation, agents, Environment
|
||||||
from soil.time import Delta
|
|
||||||
|
|
||||||
|
|
||||||
class MyAgent(agents.FSM):
|
class MyAgent(agents.FSM):
|
||||||
@ -11,22 +9,22 @@ class MyAgent(agents.FSM):
|
|||||||
An agent that first does a ping
|
An agent that first does a ping
|
||||||
"""
|
"""
|
||||||
|
|
||||||
defaults = {"pong_counts": 2}
|
max_pongs = 2
|
||||||
|
|
||||||
@agents.default_state
|
@agents.default_state
|
||||||
@agents.state
|
@agents.state
|
||||||
def ping(self):
|
def ping(self):
|
||||||
self.info("Ping")
|
self.info("Ping")
|
||||||
return self.pong, Delta(self.random.expovariate(1 / 16))
|
return self.pong.delay(self.random.expovariate(1 / 16))
|
||||||
|
|
||||||
@agents.state
|
@agents.state
|
||||||
def pong(self):
|
def pong(self):
|
||||||
self.info("Pong")
|
self.info("Pong")
|
||||||
self.pong_counts -= 1
|
self.max_pongs -= 1
|
||||||
self.info(str(self.pong_counts))
|
self.info(str(self.max_pongs), "pongs remaining")
|
||||||
if self.pong_counts < 1:
|
if self.max_pongs < 1:
|
||||||
return self.die()
|
return self.die()
|
||||||
return None, Delta(self.random.expovariate(1 / 16))
|
return self.delay(self.random.expovariate(1 / 16))
|
||||||
|
|
||||||
|
|
||||||
class RandomEnv(Environment):
|
class RandomEnv(Environment):
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import networkx as nx
|
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 import Environment, Simulation
|
||||||
from soil.parameters import *
|
from soil.parameters import *
|
||||||
from soil.utils import int_seed
|
from soil.utils import int_seed
|
||||||
@ -39,8 +40,8 @@ class TerroristEnvironment(Environment):
|
|||||||
HavenModel
|
HavenModel
|
||||||
], [self.ratio_civil, self.ratio_leader, self.ratio_training, self.ratio_haven])
|
], [self.ratio_civil, self.ratio_leader, self.ratio_training, self.ratio_haven])
|
||||||
|
|
||||||
def generator(self, *args, **kwargs):
|
def generator(self, *args, seed=None, **kwargs):
|
||||||
return nx.random_geometric_graph(*args, **kwargs, seed=int_seed(self._seed))
|
return nx.random_geometric_graph(*args, **kwargs, seed=seed or int_seed(self._seed))
|
||||||
|
|
||||||
class TerroristSpreadModel(FSM, Geo):
|
class TerroristSpreadModel(FSM, Geo):
|
||||||
"""
|
"""
|
||||||
|
@ -21,5 +21,4 @@ class TorvaldsEnv(Environment):
|
|||||||
|
|
||||||
sim = Simulation(name='torvalds_example',
|
sim = Simulation(name='torvalds_example',
|
||||||
max_steps=10,
|
max_steps=10,
|
||||||
interval=2,
|
|
||||||
model=TorvaldsEnv)
|
model=TorvaldsEnv)
|
@ -1 +1 @@
|
|||||||
1.0.0rc2
|
1.0.0rc3
|
||||||
|
@ -16,7 +16,6 @@ except NameError:
|
|||||||
basestring = str
|
basestring = str
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from .analysis import *
|
|
||||||
from .agents import *
|
from .agents import *
|
||||||
from . import agents
|
from . import agents
|
||||||
from .simulation import *
|
from .simulation import *
|
||||||
|
@ -25,6 +25,35 @@ from .. import serialization, network, utils, time, config
|
|||||||
IGNORED_FIELDS = ("model", "logger")
|
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):
|
class MetaAgent(ABCMeta):
|
||||||
def __new__(mcls, name, bases, namespace):
|
def __new__(mcls, name, bases, namespace):
|
||||||
defaults = {}
|
defaults = {}
|
||||||
@ -36,34 +65,23 @@ class MetaAgent(ABCMeta):
|
|||||||
|
|
||||||
new_nmspc = {
|
new_nmspc = {
|
||||||
"_defaults": defaults,
|
"_defaults": defaults,
|
||||||
"_last_return": None,
|
|
||||||
"_last_except": None,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for attr, func in namespace.items():
|
for attr, func in namespace.items():
|
||||||
if attr == "step" and inspect.isgeneratorfunction(func):
|
if attr == "step":
|
||||||
orig_func = func
|
if inspect.isgeneratorfunction(func) or inspect.iscoroutinefunction(func):
|
||||||
new_nmspc["_coroutine"] = None
|
func = decorate_generator_step(func, attr)
|
||||||
|
new_nmspc.update({
|
||||||
@wraps(func)
|
"_last_return": None,
|
||||||
def func(self):
|
"_last_except": None,
|
||||||
while True:
|
"_coroutine": None,
|
||||||
if not self._coroutine:
|
})
|
||||||
self._coroutine = orig_func(self)
|
elif inspect.isasyncgenfunction(func):
|
||||||
try:
|
raise ValueError("Illegal step function: {}. It probably mixes both async/await and yield".format(func))
|
||||||
if self._last_except:
|
elif inspect.isfunction(func):
|
||||||
return self._coroutine.throw(self._last_except)
|
func = decorate_normal_func(func, attr)
|
||||||
else:
|
else:
|
||||||
return self._coroutine.send(self._last_return)
|
raise ValueError("Illegal step function: {}".format(func))
|
||||||
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
|
|
||||||
new_nmspc[attr] = func
|
new_nmspc[attr] = func
|
||||||
elif (
|
elif (
|
||||||
isinstance(func, types.FunctionType)
|
isinstance(func, types.FunctionType)
|
||||||
@ -74,9 +92,13 @@ class MetaAgent(ABCMeta):
|
|||||||
new_nmspc[attr] = func
|
new_nmspc[attr] = func
|
||||||
elif attr == "defaults":
|
elif attr == "defaults":
|
||||||
defaults.update(func)
|
defaults.update(func)
|
||||||
|
elif inspect.isfunction(func):
|
||||||
|
new_nmspc[attr] = func
|
||||||
else:
|
else:
|
||||||
defaults[attr] = copy(func)
|
defaults[attr] = copy(func)
|
||||||
|
|
||||||
|
|
||||||
|
# Add attributes for their use in the decorated functions
|
||||||
return super().__new__(mcls, name, bases, new_nmspc)
|
return super().__new__(mcls, name, bases, new_nmspc)
|
||||||
|
|
||||||
|
|
||||||
@ -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.
|
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)
|
assert isinstance(unique_id, int)
|
||||||
super().__init__(unique_id=unique_id, model=model)
|
super().__init__(unique_id=unique_id, model=model)
|
||||||
|
|
||||||
@ -102,7 +124,6 @@ class BaseAgent(MesaAgent, MutableMapping, metaclass=MetaAgent):
|
|||||||
|
|
||||||
self.alive = True
|
self.alive = True
|
||||||
|
|
||||||
self.interval = interval or self.get("interval", 1)
|
|
||||||
logger = utils.logger.getChild(getattr(self.model, "id", self.model)).getChild(
|
logger = utils.logger.getChild(getattr(self.model, "id", self.model)).getChild(
|
||||||
self.name
|
self.name
|
||||||
)
|
)
|
||||||
@ -111,13 +132,18 @@ class BaseAgent(MesaAgent, MutableMapping, metaclass=MetaAgent):
|
|||||||
if hasattr(self, "level"):
|
if hasattr(self, "level"):
|
||||||
self.logger.setLevel(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():
|
for (k, v) in self._defaults.items():
|
||||||
if not hasattr(self, k) or getattr(self, k) is None:
|
if not hasattr(self, k) or getattr(self, k) is None:
|
||||||
setattr(self, k, deepcopy(v))
|
setattr(self, k, deepcopy(v))
|
||||||
|
|
||||||
for (k, v) in kwargs.items():
|
for (k, v) in kwargs.items():
|
||||||
|
|
||||||
setattr(self, k, v)
|
setattr(self, k, v)
|
||||||
|
|
||||||
if init:
|
if init:
|
||||||
self.init()
|
self.init()
|
||||||
|
|
||||||
@ -189,11 +215,13 @@ class BaseAgent(MesaAgent, MutableMapping, metaclass=MetaAgent):
|
|||||||
return it
|
return it
|
||||||
|
|
||||||
def get(self, key, default=None):
|
def get(self, key, default=None):
|
||||||
if key in self:
|
try:
|
||||||
return self[key]
|
return getattr(self, key)
|
||||||
elif key in self.model:
|
except AttributeError:
|
||||||
return self.model[key]
|
try:
|
||||||
return default
|
return getattr(self.model, key)
|
||||||
|
except AttributeError:
|
||||||
|
return default
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def now(self):
|
def now(self):
|
||||||
@ -206,17 +234,18 @@ class BaseAgent(MesaAgent, MutableMapping, metaclass=MetaAgent):
|
|||||||
def die(self, msg=None):
|
def die(self, msg=None):
|
||||||
if msg:
|
if msg:
|
||||||
self.info("Agent dying:", msg)
|
self.info("Agent dying:", msg)
|
||||||
self.debug(f"agent dying")
|
else:
|
||||||
|
self.debug(f"agent dying")
|
||||||
self.alive = False
|
self.alive = False
|
||||||
try:
|
try:
|
||||||
self.model.schedule.remove(self)
|
self.model.schedule.remove(self)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
return time.NEVER
|
return time.Delay(time.INFINITY)
|
||||||
|
|
||||||
def step(self):
|
def step(self):
|
||||||
raise NotImplementedError("Agent must implement step method")
|
raise NotImplementedError("Agent must implement step method")
|
||||||
|
|
||||||
def _check_alive(self):
|
def _check_alive(self):
|
||||||
if not self.alive:
|
if not self.alive:
|
||||||
raise time.DeadAgent(self.unique_id)
|
raise time.DeadAgent(self.unique_id)
|
||||||
@ -265,6 +294,12 @@ class BaseAgent(MesaAgent, MutableMapping, metaclass=MetaAgent):
|
|||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"{self.__class__.__name__}({self.unique_id})"
|
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):
|
def prob(prob, random):
|
||||||
@ -450,8 +485,10 @@ def filter_agents(
|
|||||||
state = state or dict()
|
state = state or dict()
|
||||||
state.update(kwargs)
|
state.update(kwargs)
|
||||||
|
|
||||||
for k, v in state.items():
|
for k, vs in state.items():
|
||||||
f = filter(lambda agent: getattr(agent, k, None) == v, f)
|
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:
|
if limit is not None:
|
||||||
f = islice(f, limit)
|
f = islice(f, limit)
|
||||||
@ -658,15 +695,6 @@ from .SISaModel import *
|
|||||||
from .CounterModel 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):
|
def custom(cls, **kwargs):
|
||||||
"""Create a new class from a template class and keyword arguments"""
|
"""Create a new class from a template class and keyword arguments"""
|
||||||
return type(cls.__name__, (cls,), kwargs)
|
return type(cls.__name__, (cls,), kwargs)
|
||||||
|
@ -1,42 +1,8 @@
|
|||||||
from . import BaseAgent
|
from . import BaseAgent
|
||||||
from ..events import Message, Tell, Ask, TimedOut
|
from ..events import Message, Tell, Ask, TimedOut
|
||||||
from ..time import BaseCond
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
from types import coroutine
|
||||||
|
|
||||||
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})"
|
|
||||||
|
|
||||||
|
|
||||||
class EventedAgent(BaseAgent):
|
class EventedAgent(BaseAgent):
|
||||||
@ -48,30 +14,45 @@ class EventedAgent(BaseAgent):
|
|||||||
def on_receive(self, *args, **kwargs):
|
def on_receive(self, *args, **kwargs):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def received(self, *args, **kwargs):
|
@coroutine
|
||||||
return ReceivedOrTimeout(self, *args, **kwargs)
|
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):
|
def tell(self, msg, sender=None):
|
||||||
self._inbox.append(Tell(timestamp=self.now, payload=msg, sender=sender))
|
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)
|
ask = Ask(timestamp=self.now, payload=msg, sender=self)
|
||||||
self._inbox.append(ask)
|
self._inbox.append(ask)
|
||||||
expiration = float("inf") if timeout is None else self.now + timeout
|
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):
|
def process_messages(self):
|
||||||
changed = False
|
valid = list()
|
||||||
while self._inbox:
|
for msg in self._inbox:
|
||||||
msg = self._inbox.popleft()
|
|
||||||
self._processed += 1
|
self._processed += 1
|
||||||
if msg.expired(self.now):
|
if msg.expired(self.now):
|
||||||
continue
|
continue
|
||||||
changed = True
|
valid.append(msg)
|
||||||
reply = self.on_receive(msg.payload, sender=msg.sender)
|
reply = self.on_receive(msg.payload, sender=msg.sender)
|
||||||
if isinstance(msg, Ask):
|
if isinstance(msg, Ask):
|
||||||
msg.reply = reply
|
msg.reply = reply
|
||||||
return changed
|
self._inbox.clear()
|
||||||
|
return valid
|
||||||
|
|
||||||
|
|
||||||
Evented = EventedAgent
|
Evented = EventedAgent
|
||||||
|
@ -1,47 +1,69 @@
|
|||||||
from . import MetaAgent, BaseAgent
|
from . import MetaAgent, BaseAgent
|
||||||
from ..time import Delta
|
from .. import time
|
||||||
|
from types import coroutine
|
||||||
from functools import partial, wraps
|
from functools import partial, wraps
|
||||||
import inspect
|
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 state(name=None, default=False):
|
||||||
def decorator(func, name=None):
|
def decorator(func, name=None):
|
||||||
"""
|
"""
|
||||||
A state function should return either a state id, or a tuple (state_id, when)
|
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 state_id is the current state id.
|
||||||
The default value for when is the interval defined in the environment.
|
|
||||||
"""
|
"""
|
||||||
if inspect.isgeneratorfunction(func):
|
name = name or func.__name__
|
||||||
orig_func = func
|
generator = inspect.isgeneratorfunction(func)
|
||||||
|
awaitable = inspect.iscoroutinefunction(func) or inspect.isasyncgen(func)
|
||||||
@wraps(func)
|
return UnboundState(func, name, default, generator, awaitable)
|
||||||
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
|
|
||||||
|
|
||||||
if callable(name):
|
if callable(name):
|
||||||
return decorator(name)
|
return decorator(name)
|
||||||
@ -50,7 +72,7 @@ def state(name=None, default=False):
|
|||||||
|
|
||||||
|
|
||||||
def default_state(func):
|
def default_state(func):
|
||||||
func.is_default = True
|
func.default = True
|
||||||
return func
|
return func
|
||||||
|
|
||||||
|
|
||||||
@ -62,42 +84,45 @@ class MetaFSM(MetaAgent):
|
|||||||
for i in bases:
|
for i in bases:
|
||||||
if isinstance(i, MetaFSM):
|
if isinstance(i, MetaFSM):
|
||||||
for state_id, state in i._states.items():
|
for state_id, state in i._states.items():
|
||||||
if state.is_default:
|
if state.default:
|
||||||
default_state = state
|
default_state = state
|
||||||
states[state_id] = state
|
states[state_id] = state
|
||||||
|
|
||||||
# Add new states
|
# Add new states
|
||||||
for attr, func in namespace.items():
|
for attr, func in namespace.items():
|
||||||
if hasattr(func, "id"):
|
if isinstance(func, State):
|
||||||
if func.is_default:
|
if func.default:
|
||||||
default_state = func
|
default_state = func
|
||||||
states[func.id] = func
|
states[func.name] = func
|
||||||
|
|
||||||
namespace.update(
|
namespace.update(
|
||||||
{
|
{
|
||||||
"_default_state": default_state,
|
"_state": default_state,
|
||||||
"_states": states,
|
"_states": states,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return super(MetaFSM, mcls).__new__(
|
cls = super(MetaFSM, mcls).__new__(
|
||||||
mcls=mcls, name=name, bases=bases, namespace=namespace
|
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):
|
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)
|
super().__init__(**kwargs, init=False)
|
||||||
if not hasattr(self, "state_id"):
|
if state_id is not None:
|
||||||
if not self._default_state:
|
self._set_state(state_id)
|
||||||
raise ValueError(
|
# If more than "dead" state is defined, but no default state
|
||||||
"No default state specified for {}".format(self.unique_id)
|
if len(self._states) > 1 and not self._state:
|
||||||
)
|
raise ValueError(
|
||||||
self.state_id = self._default_state.id
|
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:
|
if init:
|
||||||
self.init()
|
self.init()
|
||||||
|
|
||||||
@ -105,44 +130,46 @@ class FSM(BaseAgent, metaclass=MetaFSM):
|
|||||||
def states(cls):
|
def states(cls):
|
||||||
return list(cls._states.keys())
|
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):
|
def step(self):
|
||||||
self.debug(f"Agent {self.unique_id} @ state {self.state_id}")
|
|
||||||
|
|
||||||
self._check_alive()
|
self._check_alive()
|
||||||
next_state = self._states[self.state_id](self)
|
next_state = yield from self._state.step(self)
|
||||||
|
|
||||||
when = None
|
|
||||||
try:
|
try:
|
||||||
next_state, *when = next_state
|
next_state, when = next_state
|
||||||
if not when:
|
except (TypeError, ValueError) as ex:
|
||||||
when = None
|
try:
|
||||||
elif len(when) == 1:
|
self._set_state(next_state)
|
||||||
when = when[0]
|
return None
|
||||||
else:
|
except ValueError:
|
||||||
raise ValueError(
|
return next_state
|
||||||
"Too many values returned. Only state (and time) allowed"
|
|
||||||
)
|
|
||||||
except TypeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
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):
|
||||||
|
if state is None:
|
||||||
def _set_state(self, state, when=None):
|
return
|
||||||
if hasattr(state, "id"):
|
if isinstance(state, str):
|
||||||
state = state.id
|
if state not in self._states:
|
||||||
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))
|
raise ValueError("{} is not a valid state".format(state))
|
||||||
self.state_id = state
|
self._state = state
|
||||||
if when is not None:
|
|
||||||
self.model.schedule.add(self, when=when)
|
|
||||||
return state
|
|
||||||
|
|
||||||
def die(self, *args, **kwargs):
|
def die(self, *args, **kwargs):
|
||||||
return self.dead, super().die(*args, **kwargs)
|
super().die(*args, **kwargs)
|
||||||
|
return self.dead.at(time.INFINITY)
|
||||||
|
|
||||||
@state
|
@state
|
||||||
def dead(self):
|
def dead(self):
|
||||||
return self.die()
|
return time.INFINITY
|
@ -6,7 +6,7 @@ class NetworkAgent(BaseAgent):
|
|||||||
super().__init__(*args, init=False, **kwargs)
|
super().__init__(*args, init=False, **kwargs)
|
||||||
|
|
||||||
self.G = topology or self.model.G
|
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:
|
if node_id is None:
|
||||||
nodes = self.random.choices(list(self.G.nodes), k=len(self.G))
|
nodes = self.random.choices(list(self.G.nodes), k=len(self.G))
|
||||||
for n_id in nodes:
|
for n_id in nodes:
|
||||||
@ -25,8 +25,6 @@ class NetworkAgent(BaseAgent):
|
|||||||
|
|
||||||
def count_neighbors(self, state_id=None, **kwargs):
|
def count_neighbors(self, state_id=None, **kwargs):
|
||||||
return len(self.get_neighbors(state_id=state_id, **kwargs))
|
return len(self.get_neighbors(state_id=state_id, **kwargs))
|
||||||
if init:
|
|
||||||
self.init()
|
|
||||||
|
|
||||||
def iter_neighbors(self, **kwargs):
|
def iter_neighbors(self, **kwargs):
|
||||||
return self.iter_agents(limit_neighbors=True, **kwargs)
|
return self.iter_agents(limit_neighbors=True, **kwargs)
|
||||||
|
@ -28,7 +28,8 @@ def plot(env, agent_df=None, model_df=None, steps=False, ignore=["agent_count",
|
|||||||
|
|
||||||
|
|
||||||
Results = namedtuple("Results", ["config", "parameters", "env", "agents"])
|
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):
|
def read_sql(fpath=None, name=None, include_agents=False):
|
||||||
if not (fpath is None) ^ (name is None):
|
if not (fpath is None) ^ (name is None):
|
||||||
raise ValueError("Specify either a path or a simulation name")
|
raise ValueError("Specify either a path or a simulation name")
|
||||||
|
@ -9,7 +9,7 @@ class SoilCollector(MDC):
|
|||||||
if 'agent_count' not in model_reporters:
|
if 'agent_count' not in model_reporters:
|
||||||
model_reporters['agent_count'] = lambda m: m.schedule.get_agent_count()
|
model_reporters['agent_count'] = lambda m: m.schedule.get_agent_count()
|
||||||
if 'time' not in model_reporters:
|
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:
|
# if 'state_id' not in agent_reporters:
|
||||||
# agent_reporters['state_id'] = lambda agent: getattr(agent, 'state_id', None)
|
# agent_reporters['state_id'] = lambda agent: getattr(agent, 'state_id', None)
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ import networkx as nx
|
|||||||
|
|
||||||
from mesa import Model, Agent
|
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
|
# TODO: maybe add metaclass to read attributes of a model
|
||||||
@ -35,6 +35,7 @@ class BaseEnvironment(Model):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
collector_class = datacollection.SoilCollector
|
collector_class = datacollection.SoilCollector
|
||||||
|
schedule_class = time.TimedActivation
|
||||||
|
|
||||||
def __new__(cls,
|
def __new__(cls,
|
||||||
*args: Any,
|
*args: Any,
|
||||||
@ -49,7 +50,6 @@ class BaseEnvironment(Model):
|
|||||||
self = super().__new__(cls, *args, seed=seed, **kwargs)
|
self = super().__new__(cls, *args, seed=seed, **kwargs)
|
||||||
self.dir_path = dir_path or os.getcwd()
|
self.dir_path = dir_path or os.getcwd()
|
||||||
collector_class = collector_class or cls.collector_class
|
collector_class = collector_class or cls.collector_class
|
||||||
collector_class = serialization.deserialize(collector_class)
|
|
||||||
self.datacollector = collector_class(
|
self.datacollector = collector_class(
|
||||||
model_reporters=model_reporters,
|
model_reporters=model_reporters,
|
||||||
agent_reporters=agent_reporters,
|
agent_reporters=agent_reporters,
|
||||||
@ -60,7 +60,7 @@ class BaseEnvironment(Model):
|
|||||||
if isinstance(v, property):
|
if isinstance(v, property):
|
||||||
v = v.fget
|
v = v.fget
|
||||||
if getattr(v, "add_to_report", False):
|
if getattr(v, "add_to_report", False):
|
||||||
self.add_model_reporter(k, v)
|
self.add_model_reporter(k, k)
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@ -70,8 +70,8 @@ class BaseEnvironment(Model):
|
|||||||
id="unnamed_env",
|
id="unnamed_env",
|
||||||
seed="default",
|
seed="default",
|
||||||
dir_path=None,
|
dir_path=None,
|
||||||
schedule_class=time.TimedActivation,
|
schedule=None,
|
||||||
interval=1,
|
schedule_class=None,
|
||||||
logger = None,
|
logger = None,
|
||||||
agents: Optional[Dict] = None,
|
agents: Optional[Dict] = None,
|
||||||
collector_class: type = datacollection.SoilCollector,
|
collector_class: type = datacollection.SoilCollector,
|
||||||
@ -94,13 +94,11 @@ class BaseEnvironment(Model):
|
|||||||
else:
|
else:
|
||||||
self.logger = utils.logger.getChild(self.id)
|
self.logger = utils.logger.getChild(self.id)
|
||||||
|
|
||||||
if schedule_class is None:
|
self.schedule = schedule
|
||||||
schedule_class = time.TimedActivation
|
if schedule is None:
|
||||||
else:
|
if schedule_class is None:
|
||||||
schedule_class = serialization.deserialize(schedule_class)
|
schedule_class = self.schedule_class
|
||||||
|
self.schedule = schedule_class(self)
|
||||||
self.interval = interval
|
|
||||||
self.schedule = schedule_class(self)
|
|
||||||
|
|
||||||
for (k, v) in env_params.items():
|
for (k, v) in env_params.items():
|
||||||
self[k] = v
|
self[k] = v
|
||||||
@ -161,7 +159,7 @@ class BaseEnvironment(Model):
|
|||||||
if unique_id is None:
|
if unique_id is None:
|
||||||
unique_id = self.next_id()
|
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)
|
self.schedule.add(a)
|
||||||
return a
|
return a
|
||||||
@ -204,14 +202,14 @@ class BaseEnvironment(Model):
|
|||||||
|
|
||||||
def add_model_reporter(self, name, func=None):
|
def add_model_reporter(self, name, func=None):
|
||||||
if not func:
|
if not func:
|
||||||
func = lambda env: getattr(env, name)
|
func = name
|
||||||
self.datacollector._new_model_reporter(name, func)
|
self.datacollector._new_model_reporter(name, func)
|
||||||
|
|
||||||
def add_agent_reporter(self, name, agent_type=None):
|
def add_agent_reporter(self, name, reporter=None, agent_type=None):
|
||||||
if agent_type:
|
if not agent_type and not reporter:
|
||||||
reporter = lambda a: getattr(a, name) if isinstance(a, agent_type) else None
|
reporter = name
|
||||||
else:
|
elif agent_type:
|
||||||
reporter = lambda a: getattr(a, name, None)
|
reporter = lambda a: reporter(a) if isinstance(a, agent_type) else None
|
||||||
self.datacollector._new_agent_reporter(name, reporter)
|
self.datacollector._new_agent_reporter(name, reporter)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -278,8 +276,6 @@ class NetworkEnvironment(BaseEnvironment):
|
|||||||
super().__init__(*args, **kwargs, init=False)
|
super().__init__(*args, **kwargs, init=False)
|
||||||
|
|
||||||
self.agent_class = agent_class
|
self.agent_class = agent_class
|
||||||
if agent_class:
|
|
||||||
self.agent_class = serialization.deserialize(agent_class)
|
|
||||||
if self.agent_class:
|
if self.agent_class:
|
||||||
self.populate_network(self.agent_class)
|
self.populate_network(self.agent_class)
|
||||||
self._check_agent_nodes()
|
self._check_agent_nodes()
|
||||||
@ -309,7 +305,15 @@ class NetworkEnvironment(BaseEnvironment):
|
|||||||
elif path is not None:
|
elif path is not None:
|
||||||
topology = network.from_topology(path, dir_path=self.dir_path)
|
topology = network.from_topology(path, dir_path=self.dir_path)
|
||||||
elif generator is not None:
|
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:
|
else:
|
||||||
raise ValueError("topology must be a networkx.Graph or a string, or network_generator must be provided")
|
raise ValueError("topology must be a networkx.Graph or a string, or network_generator must be provided")
|
||||||
self.G = topology
|
self.G = topology
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
from .time import BaseCond
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
@ -24,29 +23,9 @@ class Reply(Message):
|
|||||||
source: 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):
|
class Ask(Message):
|
||||||
reply: Message = None
|
reply: Message = None
|
||||||
|
|
||||||
def replied(self, expiration=None):
|
|
||||||
return ReplyCond(self)
|
|
||||||
|
|
||||||
|
|
||||||
class Tell(Message):
|
class Tell(Message):
|
||||||
pass
|
pass
|
||||||
|
@ -2,11 +2,9 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
from time import time as current_time
|
from time import time as current_time
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from sqlalchemy import create_engine
|
|
||||||
from textwrap import dedent, indent
|
from textwrap import dedent, indent
|
||||||
|
|
||||||
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
import networkx as nx
|
import networkx as nx
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
@ -124,6 +122,9 @@ class SQLite(Exporter):
|
|||||||
if not self.dump:
|
if not self.dump:
|
||||||
logger.debug("NOT dumping results")
|
logger.debug("NOT dumping results")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
|
||||||
self.dbpath = os.path.join(self.outdir, f"{self.simulation.name}.sqlite")
|
self.dbpath = os.path.join(self.outdir, f"{self.simulation.name}.sqlite")
|
||||||
logger.info("Dumping results to %s", self.dbpath)
|
logger.info("Dumping results to %s", self.dbpath)
|
||||||
if self.simulation.backup:
|
if self.simulation.backup:
|
||||||
@ -175,7 +176,6 @@ class csv(Exporter):
|
|||||||
df.to_csv(f)
|
df.to_csv(f)
|
||||||
|
|
||||||
|
|
||||||
# TODO: reimplement GEXF exporting without history
|
|
||||||
class gexf(Exporter):
|
class gexf(Exporter):
|
||||||
def iteration_end(self, env, *args, **kwargs):
|
def iteration_end(self, env, *args, **kwargs):
|
||||||
if not self.dump:
|
if not self.dump:
|
||||||
@ -186,8 +186,7 @@ class gexf(Exporter):
|
|||||||
"[GEXF] Dumping simulation {} iteration {}".format(self.simulation.name, env.id)
|
"[GEXF] Dumping simulation {} iteration {}".format(self.simulation.name, env.id)
|
||||||
):
|
):
|
||||||
with self.output("{}.gexf".format(env.id), mode="wb") as f:
|
with self.output("{}.gexf".format(env.id), mode="wb") as f:
|
||||||
network.dump_gexf(env.history_to_graph(), f)
|
nx.write_gexf(env.G, f)
|
||||||
self.dump_gexf(env, f)
|
|
||||||
|
|
||||||
|
|
||||||
class dummy(Exporter):
|
class dummy(Exporter):
|
||||||
@ -210,6 +209,7 @@ class dummy(Exporter):
|
|||||||
|
|
||||||
class graphdrawing(Exporter):
|
class graphdrawing(Exporter):
|
||||||
def iteration_end(self, env, *args, **kwargs):
|
def iteration_end(self, env, *args, **kwargs):
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
# Outside effects
|
# Outside effects
|
||||||
f = plt.figure()
|
f = plt.figure()
|
||||||
nx.draw(
|
nx.draw(
|
||||||
|
@ -103,7 +103,13 @@ def load_config(cfg):
|
|||||||
yield from load_files(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 = {
|
KNOWN_MODULES = {
|
||||||
'soil': None,
|
'soil': None,
|
||||||
@ -163,7 +169,7 @@ def name(value, known_modules=KNOWN_MODULES):
|
|||||||
if not isinstance(value, type): # Get the class name first
|
if not isinstance(value, type): # Get the class name first
|
||||||
value = type(value)
|
value = type(value)
|
||||||
tname = value.__name__
|
tname = value.__name__
|
||||||
if hasattr(builtins, tname):
|
if hasattr(builtins(), tname):
|
||||||
return tname
|
return tname
|
||||||
modname = value.__module__
|
modname = value.__module__
|
||||||
if modname == "__main__":
|
if modname == "__main__":
|
||||||
@ -178,7 +184,7 @@ def name(value, known_modules=KNOWN_MODULES):
|
|||||||
|
|
||||||
|
|
||||||
def serializer(type_):
|
def serializer(type_):
|
||||||
if type_ != "str" and hasattr(builtins, type_):
|
if type_ != "str":
|
||||||
return repr
|
return repr
|
||||||
return lambda x: x
|
return lambda x: x
|
||||||
|
|
||||||
@ -216,8 +222,8 @@ def deserializer(type_, known_modules=KNOWN_MODULES):
|
|||||||
return lambda x="": x
|
return lambda x="": x
|
||||||
if type_ == "None":
|
if type_ == "None":
|
||||||
return lambda x=None: None
|
return lambda x=None: None
|
||||||
if hasattr(builtins, type_): # Check if it's a builtin type
|
if hasattr(builtins(), type_): # Check if it's a builtin type
|
||||||
cls = getattr(builtins, type_)
|
cls = getattr(builtins(), type_)
|
||||||
return lambda x=None: ast.literal_eval(x) if x is not None else cls()
|
return lambda x=None: ast.literal_eval(x) if x is not None else cls()
|
||||||
match = IS_CLASS.match(type_)
|
match = IS_CLASS.match(type_)
|
||||||
if match:
|
if match:
|
||||||
|
@ -23,7 +23,7 @@ import json
|
|||||||
|
|
||||||
|
|
||||||
from . import serialization, exporters, utils, basestring, agents
|
from . import serialization, exporters, utils, basestring, agents
|
||||||
from .environment import Environment
|
from . import environment
|
||||||
from .utils import logger, run_and_return_exceptions
|
from .utils import logger, run_and_return_exceptions
|
||||||
from .debugging import set_trace
|
from .debugging import set_trace
|
||||||
|
|
||||||
@ -49,8 +49,6 @@ def _iter_queued():
|
|||||||
|
|
||||||
|
|
||||||
# TODO: change documentation for simulation
|
# TODO: change documentation for simulation
|
||||||
# TODO: rename iterations to iterations
|
|
||||||
# TODO: make parameters a dict of iterable/any
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Simulation:
|
class Simulation:
|
||||||
"""
|
"""
|
||||||
@ -68,7 +66,6 @@ class Simulation:
|
|||||||
dir_path: The directory path to use for the simulation.
|
dir_path: The directory path to use for the simulation.
|
||||||
max_time: The maximum time to run the simulation.
|
max_time: The maximum time to run the simulation.
|
||||||
max_steps: The maximum number of steps 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.
|
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.
|
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
|
tables: The tables to use in the simulation datacollector
|
||||||
@ -96,7 +93,6 @@ class Simulation:
|
|||||||
dir_path: str = field(default_factory=lambda: os.getcwd())
|
dir_path: str = field(default_factory=lambda: os.getcwd())
|
||||||
max_time: float = None
|
max_time: float = None
|
||||||
max_steps: int = None
|
max_steps: int = None
|
||||||
interval: int = 1
|
|
||||||
iterations: int = 1
|
iterations: int = 1
|
||||||
num_processes: Optional[int] = 1
|
num_processes: Optional[int] = 1
|
||||||
exporters: Optional[List[str]] = field(default_factory=lambda: [exporters.default])
|
exporters: Optional[List[str]] = field(default_factory=lambda: [exporters.default])
|
||||||
@ -126,15 +122,9 @@ class Simulation:
|
|||||||
if isinstance(self.model, str):
|
if isinstance(self.model, str):
|
||||||
self.model = serialization.deserialize(self.model)
|
self.model = serialization.deserialize(self.model)
|
||||||
|
|
||||||
def deserialize_reporters(reporters):
|
self.agent_reporters = self.agent_reporters
|
||||||
for (k, v) in reporters.items():
|
self.model_reporters = self.model_reporters
|
||||||
if isinstance(v, str) and v.startswith("py:"):
|
self.tables = self.tables
|
||||||
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)
|
|
||||||
self.id = f"{self.name}_{current_time()}"
|
self.id = f"{self.name}_{current_time()}"
|
||||||
|
|
||||||
def run(self, **kwargs):
|
def run(self, **kwargs):
|
||||||
@ -142,15 +132,6 @@ class Simulation:
|
|||||||
if kwargs:
|
if kwargs:
|
||||||
return replace(self, **kwargs).run()
|
return replace(self, **kwargs).run()
|
||||||
|
|
||||||
self.logger.debug(
|
|
||||||
dedent(
|
|
||||||
"""
|
|
||||||
Simulation:
|
|
||||||
---
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
+ self.to_yaml()
|
|
||||||
)
|
|
||||||
param_combinations = self._collect_params(**kwargs)
|
param_combinations = self._collect_params(**kwargs)
|
||||||
if _AVOID_RUNNING:
|
if _AVOID_RUNNING:
|
||||||
_QUEUED.extend((self, param) for param in param_combinations)
|
_QUEUED.extend((self, param) for param in param_combinations)
|
||||||
@ -244,7 +225,6 @@ class Simulation:
|
|||||||
id=iteration_id,
|
id=iteration_id,
|
||||||
seed=f"{self.seed}_iteration_{iteration_id}",
|
seed=f"{self.seed}_iteration_{iteration_id}",
|
||||||
dir_path=self.dir_path,
|
dir_path=self.dir_path,
|
||||||
interval=self.interval,
|
|
||||||
logger=self.logger.getChild(iteration_id),
|
logger=self.logger.getChild(iteration_id),
|
||||||
agent_reporters=agent_reporters,
|
agent_reporters=agent_reporters,
|
||||||
model_reporters=model_reporters,
|
model_reporters=model_reporters,
|
||||||
@ -359,8 +339,11 @@ def iter_from_py(pyfile, module_name='imported_file', **kwargs):
|
|||||||
for sim in _iter_queued():
|
for sim in _iter_queued():
|
||||||
sims.append(sim)
|
sims.append(sim)
|
||||||
if not sims:
|
if not sims:
|
||||||
for (_name, sim) in inspect.getmembers(module, lambda x: inspect.isclass(x) and issubclass(x, Simulation)):
|
for (_name, env) in inspect.getmembers(module,
|
||||||
sims.append(sim(**kwargs))
|
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]
|
del sys.modules[module_name]
|
||||||
assert not _AVOID_RUNNING
|
assert not _AVOID_RUNNING
|
||||||
if not sims:
|
if not sims:
|
||||||
|
233
soil/time.py
233
soil/time.py
@ -1,7 +1,9 @@
|
|||||||
from mesa.time import BaseScheduler
|
from mesa.time import BaseScheduler
|
||||||
from queue import Empty
|
from queue import Empty
|
||||||
from heapq import heappush, heappop, heapreplace
|
from heapq import heappush, heappop, heapreplace
|
||||||
|
from collections import deque
|
||||||
import math
|
import math
|
||||||
|
import logging
|
||||||
|
|
||||||
from inspect import getsource
|
from inspect import getsource
|
||||||
from numbers import Number
|
from numbers import Number
|
||||||
@ -13,119 +15,54 @@ from mesa import Agent as MesaAgent
|
|||||||
|
|
||||||
INFINITY = float("inf")
|
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):
|
class DeadAgent(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class When:
|
class PQueueActivation(BaseScheduler):
|
||||||
def __init__(self, time):
|
"""
|
||||||
if isinstance(time, When):
|
A scheduler which activates each agent with a delay returned by the agent's step method.
|
||||||
return time
|
If no delay is returned, a default of 1 is used.
|
||||||
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
|
|
||||||
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()}")')
|
|
||||||
|
|
||||||
|
|
||||||
class TimedActivation(BaseScheduler):
|
|
||||||
"""A scheduler which activates each agent when the agent requests.
|
|
||||||
In each activation, each agent will update its 'next_time'.
|
In each activation, each agent will update its 'next_time'.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, shuffle=True, **kwargs):
|
def __init__(self, *args, shuffle=True, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self._next = {}
|
|
||||||
self._queue = []
|
self._queue = []
|
||||||
self._shuffle = shuffle
|
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.logger = getattr(self.model, "logger", logger).getChild(f"time_{ self.model }")
|
||||||
self.next_time = self.time
|
self.next_time = self.time
|
||||||
|
|
||||||
def add(self, agent: MesaAgent, when=None):
|
def add(self, agent: MesaAgent, when=None):
|
||||||
if when is None:
|
if when is None:
|
||||||
when = self.time
|
when = self.time
|
||||||
elif isinstance(when, When):
|
else:
|
||||||
when = when.abs()
|
when = float(when)
|
||||||
|
|
||||||
self._schedule(agent, None, when)
|
self._schedule(agent, None, when)
|
||||||
super().add(agent)
|
super().add(agent)
|
||||||
|
|
||||||
def _schedule(self, agent, condition=None, when=None, replace=False):
|
def _schedule(self, agent, when=None, replace=False):
|
||||||
if condition:
|
if when is None:
|
||||||
if not when:
|
when = self.time
|
||||||
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
|
|
||||||
if self._shuffle:
|
if self._shuffle:
|
||||||
key = (when, self.model.random.random(), condition)
|
key = (when, self.model.random.random())
|
||||||
else:
|
else:
|
||||||
key = (when, agent.unique_id, condition)
|
key = (when, agent.unique_id)
|
||||||
self._next[agent.unique_id] = key
|
|
||||||
if replace:
|
if replace:
|
||||||
heapreplace(self._queue, (key, agent))
|
heapreplace(self._queue, (key, agent))
|
||||||
else:
|
else:
|
||||||
@ -137,70 +74,104 @@ class TimedActivation(BaseScheduler):
|
|||||||
an agent will signal when it wants to be scheduled next.
|
an agent will signal when it wants to be scheduled next.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.logger.debug(f"Simulation step {self.time}")
|
if self.time == INFINITY:
|
||||||
if not self.model.running or self.time == INFINITY:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
self.logger.debug(f"Queue length: %s", len(self._queue))
|
next_time = INFINITY
|
||||||
|
|
||||||
|
now = self.time
|
||||||
|
|
||||||
while self._queue:
|
while self._queue:
|
||||||
((when, _id, cond), agent) = self._queue[0]
|
((when, _id), agent) = self._queue[0]
|
||||||
if when > self.time:
|
if when > now:
|
||||||
|
next_time = when
|
||||||
break
|
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:
|
try:
|
||||||
returned = agent.step()
|
when = agent.step() or 1
|
||||||
|
when += now
|
||||||
except DeadAgent:
|
except DeadAgent:
|
||||||
agent.alive = False
|
|
||||||
heappop(self._queue)
|
heappop(self._queue)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check status for MESA agents
|
if when == INFINITY:
|
||||||
if not getattr(agent, "alive", True):
|
|
||||||
heappop(self._queue)
|
heappop(self._queue)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if returned:
|
self._schedule(agent, when, replace=True)
|
||||||
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.steps += 1
|
self.steps += 1
|
||||||
|
|
||||||
if not self._queue:
|
self.time = next_time
|
||||||
|
|
||||||
|
if next_time == INFINITY:
|
||||||
self.model.running = False
|
self.model.running = False
|
||||||
self.time = INFINITY
|
self.time = INFINITY
|
||||||
return
|
return
|
||||||
|
|
||||||
next_time = self._queue[0][0][0]
|
|
||||||
|
|
||||||
if next_time < self.time:
|
class TimedActivation(BaseScheduler):
|
||||||
raise Exception(
|
def __init__(self, *args, shuffle=True, **kwargs):
|
||||||
f"An agent has been scheduled for a time in the past, there is probably an error ({when} < {self.time})"
|
super().__init__(*args, **kwargs)
|
||||||
)
|
self._queue = deque()
|
||||||
self.logger.debug("Updating time step: %s -> %s ", self.time, next_time)
|
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 1
|
||||||
|
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):
|
class ShuffledTimedActivation(TimedActivation):
|
||||||
@ -211,3 +182,5 @@ class ShuffledTimedActivation(TimedActivation):
|
|||||||
class OrderedTimedActivation(TimedActivation):
|
class OrderedTimedActivation(TimedActivation):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, shuffle=False, **kwargs)
|
super().__init__(*args, shuffle=False, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ class TestAgents(TestCase):
|
|||||||
"""The last step of a dead agent should return time.INFINITY"""
|
"""The last step of a dead agent should return time.INFINITY"""
|
||||||
d = Dead(unique_id=0, model=environment.Environment())
|
d = Dead(unique_id=0, model=environment.Environment())
|
||||||
ret = d.step()
|
ret = d.step()
|
||||||
assert ret == stime.NEVER
|
assert ret == stime.INFINITY
|
||||||
|
|
||||||
def test_die_raises_exception(self):
|
def test_die_raises_exception(self):
|
||||||
"""A dead agent should raise an exception if it is stepped after death"""
|
"""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):
|
def test_state_decorator(self):
|
||||||
class MyAgent(agents.FSM):
|
class MyAgent(agents.FSM):
|
||||||
run = 0
|
times_run = 0
|
||||||
|
|
||||||
@agents.state("original", default=True)
|
@agents.state("original", default=True)
|
||||||
def root(self):
|
def root(self):
|
||||||
self.run += 1
|
|
||||||
return self.other
|
return self.other
|
||||||
|
|
||||||
@agents.state
|
@agents.state
|
||||||
def other(self):
|
def other(self):
|
||||||
self.run += 1
|
self.times_run += 1
|
||||||
|
|
||||||
e = environment.Environment()
|
e = environment.Environment()
|
||||||
a = e.add_agent(MyAgent)
|
a = e.add_agent(MyAgent)
|
||||||
e.step()
|
e.step()
|
||||||
assert a.run == 1
|
assert a.times_run == 0
|
||||||
a.step()
|
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):
|
def test_broadcast(self):
|
||||||
"""
|
"""
|
||||||
@ -86,7 +88,7 @@ class TestAgents(TestCase):
|
|||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print(ex)
|
print(ex)
|
||||||
while True:
|
while True:
|
||||||
self.check_messages()
|
self.process_messages()
|
||||||
yield
|
yield
|
||||||
|
|
||||||
def on_receive(self, msg, sender=None):
|
def on_receive(self, msg, sender=None):
|
||||||
@ -132,14 +134,14 @@ class TestAgents(TestCase):
|
|||||||
while True:
|
while True:
|
||||||
if pongs or not pings: # First agent, or anyone after that
|
if pongs or not pings: # First agent, or anyone after that
|
||||||
pings.append(self.now)
|
pings.append(self.now)
|
||||||
response = yield target.ask("PING")
|
response = yield from target.ask("PING")
|
||||||
responses.append(response)
|
responses.append(response)
|
||||||
else:
|
else:
|
||||||
print("NOT sending ping")
|
print("NOT sending ping")
|
||||||
print("Checking msgs")
|
print("Checking msgs")
|
||||||
# Do not block if we have already received a PING
|
# Do not block if we have already received a PING
|
||||||
if not self.check_messages():
|
if not self.process_messages():
|
||||||
yield self.received()
|
yield from self.received()
|
||||||
print("done")
|
print("done")
|
||||||
|
|
||||||
def on_receive(self, msg, sender=None):
|
def on_receive(self, msg, sender=None):
|
||||||
@ -174,4 +176,200 @@ class TestAgents(TestCase):
|
|||||||
assert len(ev) == 1
|
assert len(ev) == 1
|
||||||
assert ev[0].unique_id == 1
|
assert ev[0].unique_id == 1
|
||||||
null = list(e.agents(unique_ids=[0, 1], agent_class=agents.NetworkAgent))
|
null = list(e.agents(unique_ids=[0, 1], agent_class=agents.NetworkAgent))
|
||||||
assert not null
|
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):
|
def test_torvalds_config(self):
|
||||||
sim = simulation.from_config(os.path.join(ROOT, "test_config.yml"))
|
sim = simulation.from_config(os.path.join(ROOT, "test_config.yml"))
|
||||||
MAX_STEPS = 10
|
MAX_STEPS = 10
|
||||||
INTERVAL = 2
|
|
||||||
assert sim.interval == INTERVAL
|
|
||||||
assert sim.max_steps == MAX_STEPS
|
assert sim.max_steps == MAX_STEPS
|
||||||
envs = sim.run()
|
envs = sim.run()
|
||||||
assert len(envs) == 1
|
assert len(envs) == 1
|
||||||
env = envs[0]
|
env = envs[0]
|
||||||
assert env.interval == 2
|
|
||||||
assert env.count_agents() == 3
|
assert env.count_agents() == 3
|
||||||
assert env.now == INTERVAL * MAX_STEPS
|
assert env.now == MAX_STEPS
|
||||||
|
|
||||||
|
|
||||||
def make_example_test(path, cfg):
|
def make_example_test(path, cfg):
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
---
|
---
|
||||||
source_file: "../examples/torvalds_sim.py"
|
source_file: "../examples/torvalds_sim.py"
|
||||||
model: "TorvaldsEnv"
|
model: "TorvaldsEnv"
|
||||||
max_steps: 10
|
max_steps: 10
|
||||||
interval: 2
|
|
@ -88,7 +88,7 @@ class Exporters(TestCase):
|
|||||||
parameters=dict(
|
parameters=dict(
|
||||||
network_generator="complete_graph",
|
network_generator="complete_graph",
|
||||||
network_params={"n": n_nodes},
|
network_params={"n": n_nodes},
|
||||||
agent_class="CounterModel",
|
agent_class=agents.CounterModel,
|
||||||
agent_reporters={"times": "times"},
|
agent_reporters={"times": "times"},
|
||||||
),
|
),
|
||||||
max_time=max_time,
|
max_time=max_time,
|
||||||
|
@ -7,8 +7,6 @@ from functools import partial
|
|||||||
|
|
||||||
from os.path import join
|
from os.path import join
|
||||||
from soil import simulation, Environment, agents, network, serialization, utils, config, from_file
|
from soil import simulation, Environment, agents, network, serialization, utils, config, from_file
|
||||||
from soil.time import Delta
|
|
||||||
|
|
||||||
from mesa import Agent as MesaAgent
|
from mesa import Agent as MesaAgent
|
||||||
|
|
||||||
ROOT = os.path.abspath(os.path.dirname(__file__))
|
ROOT = os.path.abspath(os.path.dirname(__file__))
|
||||||
@ -114,7 +112,6 @@ class TestMain(TestCase):
|
|||||||
def test_serialize_class(self):
|
def test_serialize_class(self):
|
||||||
ser, name = serialization.serialize(agents.BaseAgent, known_modules=[])
|
ser, name = serialization.serialize(agents.BaseAgent, known_modules=[])
|
||||||
assert name == "soil.agents.BaseAgent"
|
assert name == "soil.agents.BaseAgent"
|
||||||
assert ser == agents.BaseAgent
|
|
||||||
|
|
||||||
ser, name = serialization.serialize(
|
ser, name = serialization.serialize(
|
||||||
agents.BaseAgent,
|
agents.BaseAgent,
|
||||||
@ -123,11 +120,9 @@ class TestMain(TestCase):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
assert name == "BaseAgent"
|
assert name == "BaseAgent"
|
||||||
assert ser == agents.BaseAgent
|
|
||||||
|
|
||||||
ser, name = serialization.serialize(CustomAgent)
|
ser, name = serialization.serialize(CustomAgent)
|
||||||
assert name == "test_main.CustomAgent"
|
assert name == "test_main.CustomAgent"
|
||||||
assert ser == CustomAgent
|
|
||||||
pickle.dumps(ser)
|
pickle.dumps(ser)
|
||||||
|
|
||||||
def test_serialize_builtin_types(self):
|
def test_serialize_builtin_types(self):
|
||||||
@ -168,7 +163,6 @@ class TestMain(TestCase):
|
|||||||
|
|
||||||
def test_fsm(self):
|
def test_fsm(self):
|
||||||
"""Basic state change"""
|
"""Basic state change"""
|
||||||
|
|
||||||
class ToggleAgent(agents.FSM):
|
class ToggleAgent(agents.FSM):
|
||||||
@agents.default_state
|
@agents.default_state
|
||||||
@agents.state
|
@agents.state
|
||||||
@ -193,7 +187,7 @@ class TestMain(TestCase):
|
|||||||
@agents.default_state
|
@agents.default_state
|
||||||
@agents.state
|
@agents.state
|
||||||
def ping(self):
|
def ping(self):
|
||||||
return self.pong, 2
|
return self.pong.delay(2)
|
||||||
|
|
||||||
@agents.state
|
@agents.state
|
||||||
def pong(self):
|
def pong(self):
|
||||||
@ -203,7 +197,7 @@ class TestMain(TestCase):
|
|||||||
when = a.step()
|
when = a.step()
|
||||||
assert when == 2
|
assert when == 2
|
||||||
when = a.step()
|
when = a.step()
|
||||||
assert when == Delta(a.interval)
|
assert when == None
|
||||||
|
|
||||||
def test_load_sim(self):
|
def test_load_sim(self):
|
||||||
"""Make sure at least one of the examples can be loaded"""
|
"""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)
|
assert len(configs) == len(a) * len(b)
|
||||||
for i in a:
|
for i in a:
|
||||||
for j in b:
|
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):
|
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):
|
def test_cond_env(self):
|
||||||
""" """
|
""" """
|
||||||
@ -36,11 +16,12 @@ class TestMain(TestCase):
|
|||||||
|
|
||||||
class CondAgent(agents.BaseAgent):
|
class CondAgent(agents.BaseAgent):
|
||||||
def step(self):
|
def step(self):
|
||||||
nonlocal done
|
nonlocal done, times_started, times_asleep, times_awakened
|
||||||
times_started.append(self.now)
|
times_started.append(self.now)
|
||||||
while True:
|
while True:
|
||||||
times_asleep.append(self.now)
|
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)
|
times_awakened.append(self.now)
|
||||||
if self.now >= 10:
|
if self.now >= 10:
|
||||||
break
|
break
|
||||||
@ -57,7 +38,6 @@ class TestMain(TestCase):
|
|||||||
assert times_started == [0]
|
assert times_started == [0]
|
||||||
assert times_awakened == [10]
|
assert times_awakened == [10]
|
||||||
assert done == [10]
|
assert done == [10]
|
||||||
# The first time will produce the Cond.
|
|
||||||
assert env.schedule.steps == 6
|
assert env.schedule.steps == 6
|
||||||
assert len(times) == 6
|
assert len(times) == 6
|
||||||
|
|
||||||
@ -65,11 +45,10 @@ class TestMain(TestCase):
|
|||||||
times.append(env.now)
|
times.append(env.now)
|
||||||
env.step()
|
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 env.schedule.time == 13
|
||||||
assert times_started == [0, 11]
|
assert times_started == [0, 11, 12]
|
||||||
assert times_awakened == [10]
|
assert times_awakened == [10, 11, 12]
|
||||||
assert done == [10]
|
assert done == [10, 11, 12]
|
||||||
# Once more to yield the cond, another one to continue
|
assert env.schedule.steps == 8
|
||||||
assert env.schedule.steps == 7
|
assert len(times) == 8
|
||||||
assert len(times) == 7
|
|
||||||
|
Loading…
Reference in New Issue
Block a user