mirror of
https://github.com/gsi-upm/soil
synced 2024-11-21 18:52:28 +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.
|
||||
* Ability to run simulations using mesa models
|
||||
* The `soil.exporters` module to export the results of datacollectors (`model.datacollector`) into files at the end of trials/simulations
|
||||
* Agents can now have generators as a step function or a state. They work similar to normal functions, with one caveat in the case of `FSM`: only `time` values (or None) can be yielded, not a state. This is because the state will not change, it will be resumed after the yield, at the appropriate time. The return value *can* be a state, or a `(state, time)` tuple, just like in normal states.
|
||||
* Agents can now have generators or async functions as their step or as states. They work similar to normal functions, with one caveat in the case of `FSM`: only time values (a float, int or None) can be awaited or yielded, not a state. This is because the state will not change, it will be resumed after the yield, at the appropriate time. To return to a different state, use the `delay` and `at` functions of the state.
|
||||
* Simulations can now specify a `matrix` with possible values for every simulation parameter. The final parameters will be calculated based on the `parameters` used and a cartesian product (i.e., all possible combinations) of each parameter.
|
||||
* Simple debugging capabilities in `soil.debugging`, with a custom `pdb.Debugger` subclass that exposes commands to list agents and their status and set breakpoints on states (for FSM agents). Try it with `soil --debug <simulation file>`
|
||||
* The `agent.after` and `agent.at` methods, to avoid having to return a time manually.
|
||||
### Changed
|
||||
* Configuration schema (`Simulation`) is very simplified. All simulations should be checked
|
||||
* 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
|
||||
* `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`
|
||||
@ -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
|
||||
|
||||
### Removed
|
||||
* The `time.When` and `time.Cond` classes are removed
|
||||
* Any `tsih` and `History` integration in the main classes. To record the state of environments/agents, just use a datacollector. In some cases this may be slower or consume more memory than the previous system. However, few cases actually used the full potential of the history, and it came at the cost of unnecessary complexity and worse performance for the majority of cases.
|
||||
|
||||
## [0.20.8]
|
||||
|
@ -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.
|
||||
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):.
|
||||
|
||||
@ -49,6 +49,7 @@ If you use Soil in your research, do not forget to cite this paper:
|
||||
installation
|
||||
Tutorial <tutorial/soil_tutorial>
|
||||
notes_v1.0
|
||||
soil-vs
|
||||
|
||||
..
|
||||
|
||||
|
@ -1,22 +0,0 @@
|
||||
Mesa compatibility
|
||||
------------------
|
||||
|
||||
Soil is in the process of becoming fully compatible with MESA.
|
||||
The idea is to provide a set of modular classes and functions that extend the functionality of mesa, whilst staying compatible.
|
||||
In the end, it should be possible to add regular mesa agents to a soil simulation, or use a soil agent within a mesa simulation/model.
|
||||
|
||||
This is a non-exhaustive list of tasks to achieve compatibility:
|
||||
|
||||
- [ ] Integrate `soil.Simulation` with mesa's runners:
|
||||
- [ ] `soil.Simulation` could mimic/become a `mesa.batchrunner`
|
||||
- [ ] Integrate `soil.Environment` with `mesa.Model`:
|
||||
- [x] `Soil.Environment` inherits from `mesa.Model`
|
||||
- [x] `Soil.Environment` includes a Mesa-like Scheduler (see the `soil.time` module.
|
||||
- [ ] Allow for `mesa.Model` to be used in a simulation.
|
||||
- [ ] Integrate `soil.Agent` with `mesa.Agent`:
|
||||
- [x] Rename agent.id to unique_id?
|
||||
- [x] mesa agents can be used in soil simulations (see `examples/mesa`)
|
||||
- [ ] Provide examples
|
||||
- [ ] Using mesa modules in a soil simulation
|
||||
- [ ] Using soil modules in a mesa simulation
|
||||
- [ ] Document the new APIs and usage
|
@ -1,4 +1,8 @@
|
||||
### MESA
|
||||
Soil vs other ABM frameworks
|
||||
============================
|
||||
|
||||
MESA
|
||||
----
|
||||
|
||||
Starting with version 0.3, Soil has been redesigned to complement Mesa, while remaining compatible with it.
|
||||
That means that every component in Soil (i.e., Models, Environments, etc.) can be mixed with existing mesa components.
|
||||
@ -10,3 +14,42 @@ Here are some reasons to use Soil instead of plain mesa:
|
||||
- Functions to automatically populate a topology with an agent distribution (i.e., different ratios of agent class and state)
|
||||
- The `soil.Simulation` class allows you to run multiple instances of the same experiment (i.e., multiple trials with the same parameters but a different randomness seed)
|
||||
- Reporting functions that aggregate multiple
|
||||
|
||||
Mesa compatibility
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Soil is in the process of becoming fully compatible with MESA.
|
||||
The idea is to provide a set of modular classes and functions that extend the functionality of mesa, whilst staying compatible.
|
||||
In the end, it should be possible to add regular mesa agents to a soil simulation, or use a soil agent within a mesa simulation/model.
|
||||
|
||||
This is a non-exhaustive list of tasks to achieve compatibility:
|
||||
|
||||
.. |check| raw:: html
|
||||
|
||||
☑
|
||||
|
||||
.. |uncheck| raw:: html
|
||||
|
||||
☐
|
||||
|
||||
- |check| Integrate `soil.Simulation` with mesa's runners:
|
||||
|
||||
- |check| `soil.Simulation` can replace `mesa.batchrunner`
|
||||
|
||||
- |check| Integrate `soil.Environment` with `mesa.Model`:
|
||||
|
||||
- |check| `Soil.Environment` inherits from `mesa.Model`
|
||||
- |check| `Soil.Environment` includes a Mesa-like Scheduler (see the `soil.time` module.
|
||||
- |check| Allow for `mesa.Model` to be used in a simulation.
|
||||
|
||||
- |check| Integrate `soil.Agent` with `mesa.Agent`:
|
||||
|
||||
- |check| Rename agent.id to unique_id
|
||||
- |check| mesa agents can be used in soil simulations (see `examples/mesa`)
|
||||
|
||||
- |check| Provide examples
|
||||
|
||||
- |check| Using mesa modules in a soil simulation (see `examples/mesa`)
|
||||
- |uncheck| Using soil modules in a mesa simulation (see `examples/mesa`)
|
||||
|
||||
- |uncheck| Document the new APIs and usage
|
File diff suppressed because 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
|
||||
|
||||
def on_receive(self, msg, sender):
|
||||
"""This is not a state. It will run (and block) every time check_messages is invoked"""
|
||||
"""This is not a state. It will run (and block) every time process_messages is invoked"""
|
||||
if self.journey is None and isinstance(msg, Journey) and msg.driver is None:
|
||||
msg.driver = self
|
||||
self.journey = msg
|
||||
@ -95,15 +95,14 @@ class Driver(Evented, FSM):
|
||||
"""If there are no more passengers, stop forever"""
|
||||
c = self.count_agents(agent_class=Passenger)
|
||||
self.debug(f"Passengers left {c}")
|
||||
if not c:
|
||||
self.die("No more passengers")
|
||||
return c
|
||||
|
||||
@default_state
|
||||
@state
|
||||
def wandering(self):
|
||||
@state(default=True)
|
||||
async def wandering(self):
|
||||
"""Move around the city until a journey is accepted"""
|
||||
target = None
|
||||
self.check_passengers()
|
||||
if not self.check_passengers():
|
||||
return self.die("No passengers left")
|
||||
self.journey = None
|
||||
while self.journey is None: # No potential journeys detected (see on_receive)
|
||||
if target is None or not self.move_towards(target):
|
||||
@ -111,14 +110,15 @@ class Driver(Evented, FSM):
|
||||
self.model.grid.get_neighborhood(self.pos, moore=False)
|
||||
)
|
||||
|
||||
self.check_passengers()
|
||||
if not self.check_passengers():
|
||||
return self.die("No passengers left")
|
||||
# This will call on_receive behind the scenes, and the agent's status will be updated
|
||||
self.check_messages()
|
||||
yield Delta(30) # Wait at least 30 seconds before checking again
|
||||
self.process_messages()
|
||||
await self.delay(30) # Wait at least 30 seconds before checking again
|
||||
|
||||
try:
|
||||
# Re-send the journey to the passenger, to confirm that we have been selected
|
||||
self.journey = yield self.journey.passenger.ask(self.journey, timeout=60)
|
||||
self.journey = await self.journey.passenger.ask(self.journey, timeout=60, delay=5)
|
||||
except events.TimedOut:
|
||||
# No journey has been accepted. Try again
|
||||
self.journey = None
|
||||
@ -127,18 +127,19 @@ class Driver(Evented, FSM):
|
||||
return self.driving
|
||||
|
||||
@state
|
||||
def driving(self):
|
||||
async def driving(self):
|
||||
"""The journey has been accepted. Pick them up and take them to their destination"""
|
||||
self.info(f"Driving towards Passenger {self.journey.passenger.unique_id}")
|
||||
while self.move_towards(self.journey.origin):
|
||||
yield
|
||||
await self.delay()
|
||||
self.info(f"Driving {self.journey.passenger.unique_id} from {self.journey.origin} to {self.journey.destination}")
|
||||
while self.move_towards(self.journey.destination, with_passenger=True):
|
||||
yield
|
||||
await self.delay()
|
||||
self.info("Arrived at destination")
|
||||
self.earnings += self.journey.tip
|
||||
self.model.total_earnings += self.journey.tip
|
||||
self.check_passengers()
|
||||
if not self.check_passengers():
|
||||
return self.die("No passengers left")
|
||||
return self.wandering
|
||||
|
||||
def move_towards(self, target, with_passenger=False):
|
||||
@ -167,7 +168,7 @@ class Passenger(Evented, FSM):
|
||||
pos = None
|
||||
|
||||
def on_receive(self, msg, sender):
|
||||
"""This is not a state. It will be run synchronously every time `check_messages` is run"""
|
||||
"""This is not a state. It will be run synchronously every time `process_messages` is run"""
|
||||
|
||||
if isinstance(msg, Journey):
|
||||
self.journey = msg
|
||||
@ -175,7 +176,7 @@ class Passenger(Evented, FSM):
|
||||
|
||||
@default_state
|
||||
@state
|
||||
def asking(self):
|
||||
async def asking(self):
|
||||
destination = (
|
||||
self.random.randint(0, self.model.grid.height-1),
|
||||
self.random.randint(0, self.model.grid.width-1),
|
||||
@ -195,9 +196,9 @@ class Passenger(Evented, FSM):
|
||||
while not self.journey:
|
||||
self.debug(f"Waiting for responses at: { self.pos }")
|
||||
try:
|
||||
# This will call check_messages behind the scenes, and the agent's status will be updated
|
||||
# This will call process_messages behind the scenes, and the agent's status will be updated
|
||||
# If you want to avoid that, you can call it with: check=False
|
||||
yield self.received(expiration=expiration)
|
||||
await self.received(expiration=expiration, delay=10)
|
||||
except events.TimedOut:
|
||||
self.info(f"Still no response. Waiting at: { self.pos }")
|
||||
self.model.broadcast(
|
||||
@ -208,13 +209,13 @@ class Passenger(Evented, FSM):
|
||||
return self.driving_home
|
||||
|
||||
@state
|
||||
def driving_home(self):
|
||||
async def driving_home(self):
|
||||
while (
|
||||
self.pos[0] != self.journey.destination[0]
|
||||
or self.pos[1] != self.journey.destination[1]
|
||||
):
|
||||
try:
|
||||
yield self.received(timeout=60)
|
||||
await self.received(timeout=60)
|
||||
except events.TimedOut:
|
||||
pass
|
||||
|
||||
@ -228,4 +229,4 @@ simulation = Simulation(name="RideHailing",
|
||||
parameters=dict(n_passengers=2))
|
||||
|
||||
if __name__ == "__main__":
|
||||
easy(simulation)
|
||||
easy(simulation)
|
@ -33,7 +33,7 @@ class GeneratorEnv(Environment):
|
||||
self.add_agents(CounterModel)
|
||||
|
||||
|
||||
sim = Simulation(model=GeneratorEnv, max_steps=10, interval=1)
|
||||
sim = Simulation(model=GeneratorEnv, max_steps=10)
|
||||
|
||||
if __name__ == '__main__':
|
||||
sim.run(dump=False)
|
0
examples/custom_timeouts/custom_timeouts.py
Normal file
0
examples/custom_timeouts/custom_timeouts.py
Normal file
@ -1,5 +1,4 @@
|
||||
from soil.agents import FSM, state, default_state
|
||||
from soil.time import Delta
|
||||
|
||||
|
||||
class Fibonacci(FSM):
|
||||
@ -11,17 +10,17 @@ class Fibonacci(FSM):
|
||||
def counting(self):
|
||||
self.log("Stopping at {}".format(self.now))
|
||||
prev, self["prev"] = self["prev"], max([self.now, self["prev"]])
|
||||
return None, Delta(prev)
|
||||
return self.delay(prev)
|
||||
|
||||
|
||||
|
||||
class Odds(FSM):
|
||||
"""Agent that only executes in odd t_steps"""
|
||||
|
||||
@default_state
|
||||
@state
|
||||
@state(default=True)
|
||||
def odds(self):
|
||||
self.log("Stopping at {}".format(self.now))
|
||||
return None, Delta(1 + self.now % 2)
|
||||
return self.delay(1 + (self.now % 2))
|
||||
|
||||
|
||||
from soil import Environment, Simulation
|
||||
@ -35,7 +34,7 @@ class TimeoutsEnv(Environment):
|
||||
self.add_agent(agent_class=Odds, node_id=1)
|
||||
|
||||
|
||||
sim = Simulation(model=TimeoutsEnv, max_steps=10, interval=1)
|
||||
sim = Simulation(model=TimeoutsEnv, max_steps=10)
|
||||
|
||||
if __name__ == "__main__":
|
||||
sim.run(dump=False)
|
||||
sim.run(dump=False)
|
||||
|
@ -1,7 +1,7 @@
|
||||
from soil import Simulation
|
||||
from social_wealth import MoneyEnv, graph_generator
|
||||
|
||||
sim = Simulation(name="mesa_sim", dump=False, max_steps=10, interval=2, model=MoneyEnv, parameters=dict(generator=graph_generator, N=10, width=50, height=50))
|
||||
sim = Simulation(name="mesa_sim", dump=False, max_steps=10, model=MoneyEnv, parameters=dict(generator=graph_generator, N=10, width=50, height=50))
|
||||
|
||||
if __name__ == "__main__":
|
||||
sim.run()
|
||||
|
@ -1,5 +1,4 @@
|
||||
from soil import FSM, state, default_state, BaseAgent, NetworkAgent, Environment, Simulation
|
||||
from soil.time import Delta
|
||||
from enum import Enum
|
||||
from collections import Counter
|
||||
import logging
|
||||
@ -35,7 +34,7 @@ class Rabbit(FSM, NetworkAgent):
|
||||
self.info("I am a newborn.")
|
||||
self.birth = self.now
|
||||
self.offspring = 0
|
||||
return self.youngling, Delta(self.sexual_maturity - self.age)
|
||||
return self.youngling.delay(self.sexual_maturity - self.age)
|
||||
|
||||
@state
|
||||
def youngling(self):
|
||||
|
@ -1,9 +1,7 @@
|
||||
"""
|
||||
Example of setting a
|
||||
Example of a fully programmatic simulation, without definition files.
|
||||
"""
|
||||
from soil import Simulation, agents, Environment
|
||||
from soil.time import Delta
|
||||
|
||||
|
||||
class MyAgent(agents.FSM):
|
||||
@ -11,22 +9,22 @@ class MyAgent(agents.FSM):
|
||||
An agent that first does a ping
|
||||
"""
|
||||
|
||||
defaults = {"pong_counts": 2}
|
||||
max_pongs = 2
|
||||
|
||||
@agents.default_state
|
||||
@agents.state
|
||||
def ping(self):
|
||||
self.info("Ping")
|
||||
return self.pong, Delta(self.random.expovariate(1 / 16))
|
||||
return self.pong.delay(self.random.expovariate(1 / 16))
|
||||
|
||||
@agents.state
|
||||
def pong(self):
|
||||
self.info("Pong")
|
||||
self.pong_counts -= 1
|
||||
self.info(str(self.pong_counts))
|
||||
if self.pong_counts < 1:
|
||||
self.max_pongs -= 1
|
||||
self.info(str(self.max_pongs), "pongs remaining")
|
||||
if self.max_pongs < 1:
|
||||
return self.die()
|
||||
return None, Delta(self.random.expovariate(1 / 16))
|
||||
return self.delay(self.random.expovariate(1 / 16))
|
||||
|
||||
|
||||
class RandomEnv(Environment):
|
||||
|
@ -1,5 +1,6 @@
|
||||
import networkx as nx
|
||||
from soil.agents import Geo, NetworkAgent, FSM, custom, state, default_state
|
||||
from soil.agents import NetworkAgent, FSM, custom, state, default_state
|
||||
from soil.agents.geo import Geo
|
||||
from soil import Environment, Simulation
|
||||
from soil.parameters import *
|
||||
from soil.utils import int_seed
|
||||
@ -39,8 +40,8 @@ class TerroristEnvironment(Environment):
|
||||
HavenModel
|
||||
], [self.ratio_civil, self.ratio_leader, self.ratio_training, self.ratio_haven])
|
||||
|
||||
def generator(self, *args, **kwargs):
|
||||
return nx.random_geometric_graph(*args, **kwargs, seed=int_seed(self._seed))
|
||||
def generator(self, *args, seed=None, **kwargs):
|
||||
return nx.random_geometric_graph(*args, **kwargs, seed=seed or int_seed(self._seed))
|
||||
|
||||
class TerroristSpreadModel(FSM, Geo):
|
||||
"""
|
||||
|
@ -21,5 +21,4 @@ class TorvaldsEnv(Environment):
|
||||
|
||||
sim = Simulation(name='torvalds_example',
|
||||
max_steps=10,
|
||||
interval=2,
|
||||
model=TorvaldsEnv)
|
@ -1 +1 @@
|
||||
1.0.0rc2
|
||||
1.0.0rc3
|
||||
|
@ -16,7 +16,6 @@ except NameError:
|
||||
basestring = str
|
||||
|
||||
from pathlib import Path
|
||||
from .analysis import *
|
||||
from .agents import *
|
||||
from . import agents
|
||||
from .simulation import *
|
||||
|
@ -25,6 +25,35 @@ from .. import serialization, network, utils, time, config
|
||||
IGNORED_FIELDS = ("model", "logger")
|
||||
|
||||
|
||||
def decorate_generator_step(func, name):
|
||||
@wraps(func)
|
||||
def decorated(self):
|
||||
while True:
|
||||
if self._coroutine is None:
|
||||
self._coroutine = func(self)
|
||||
try:
|
||||
if self._last_except:
|
||||
val = self._coroutine.throw(self._last_except)
|
||||
else:
|
||||
val = self._coroutine.send(self._last_return)
|
||||
except StopIteration as ex:
|
||||
self._coroutine = None
|
||||
val = ex.value
|
||||
finally:
|
||||
self._last_return = None
|
||||
self._last_except = None
|
||||
return float(val) if val is not None else val
|
||||
return decorated
|
||||
|
||||
|
||||
def decorate_normal_func(func, name):
|
||||
@wraps(func)
|
||||
def decorated(self):
|
||||
val = func(self)
|
||||
return float(val) if val is not None else val
|
||||
return decorated
|
||||
|
||||
|
||||
class MetaAgent(ABCMeta):
|
||||
def __new__(mcls, name, bases, namespace):
|
||||
defaults = {}
|
||||
@ -36,34 +65,23 @@ class MetaAgent(ABCMeta):
|
||||
|
||||
new_nmspc = {
|
||||
"_defaults": defaults,
|
||||
"_last_return": None,
|
||||
"_last_except": None,
|
||||
}
|
||||
|
||||
for attr, func in namespace.items():
|
||||
if attr == "step" and inspect.isgeneratorfunction(func):
|
||||
orig_func = func
|
||||
new_nmspc["_coroutine"] = None
|
||||
|
||||
@wraps(func)
|
||||
def func(self):
|
||||
while True:
|
||||
if not self._coroutine:
|
||||
self._coroutine = orig_func(self)
|
||||
try:
|
||||
if self._last_except:
|
||||
return self._coroutine.throw(self._last_except)
|
||||
else:
|
||||
return self._coroutine.send(self._last_return)
|
||||
except StopIteration as ex:
|
||||
self._coroutine = None
|
||||
return ex.value
|
||||
finally:
|
||||
self._last_return = None
|
||||
self._last_except = None
|
||||
|
||||
func.id = name or func.__name__
|
||||
func.is_default = False
|
||||
if attr == "step":
|
||||
if inspect.isgeneratorfunction(func) or inspect.iscoroutinefunction(func):
|
||||
func = decorate_generator_step(func, attr)
|
||||
new_nmspc.update({
|
||||
"_last_return": None,
|
||||
"_last_except": None,
|
||||
"_coroutine": None,
|
||||
})
|
||||
elif inspect.isasyncgenfunction(func):
|
||||
raise ValueError("Illegal step function: {}. It probably mixes both async/await and yield".format(func))
|
||||
elif inspect.isfunction(func):
|
||||
func = decorate_normal_func(func, attr)
|
||||
else:
|
||||
raise ValueError("Illegal step function: {}".format(func))
|
||||
new_nmspc[attr] = func
|
||||
elif (
|
||||
isinstance(func, types.FunctionType)
|
||||
@ -74,9 +92,13 @@ class MetaAgent(ABCMeta):
|
||||
new_nmspc[attr] = func
|
||||
elif attr == "defaults":
|
||||
defaults.update(func)
|
||||
elif inspect.isfunction(func):
|
||||
new_nmspc[attr] = func
|
||||
else:
|
||||
defaults[attr] = copy(func)
|
||||
|
||||
|
||||
# Add attributes for their use in the decorated functions
|
||||
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.
|
||||
"""
|
||||
|
||||
def __init__(self, unique_id, model, name=None, init=True, interval=None, **kwargs):
|
||||
def __init__(self, unique_id, model, name=None, init=True, **kwargs):
|
||||
assert isinstance(unique_id, int)
|
||||
super().__init__(unique_id=unique_id, model=model)
|
||||
|
||||
@ -102,7 +124,6 @@ class BaseAgent(MesaAgent, MutableMapping, metaclass=MetaAgent):
|
||||
|
||||
self.alive = True
|
||||
|
||||
self.interval = interval or self.get("interval", 1)
|
||||
logger = utils.logger.getChild(getattr(self.model, "id", self.model)).getChild(
|
||||
self.name
|
||||
)
|
||||
@ -111,13 +132,18 @@ class BaseAgent(MesaAgent, MutableMapping, metaclass=MetaAgent):
|
||||
if hasattr(self, "level"):
|
||||
self.logger.setLevel(self.level)
|
||||
|
||||
for k in self._defaults:
|
||||
v = getattr(model, k, None)
|
||||
if v is not None:
|
||||
setattr(self, k, v)
|
||||
|
||||
for (k, v) in self._defaults.items():
|
||||
if not hasattr(self, k) or getattr(self, k) is None:
|
||||
setattr(self, k, deepcopy(v))
|
||||
|
||||
for (k, v) in kwargs.items():
|
||||
|
||||
setattr(self, k, v)
|
||||
|
||||
if init:
|
||||
self.init()
|
||||
|
||||
@ -189,11 +215,13 @@ class BaseAgent(MesaAgent, MutableMapping, metaclass=MetaAgent):
|
||||
return it
|
||||
|
||||
def get(self, key, default=None):
|
||||
if key in self:
|
||||
return self[key]
|
||||
elif key in self.model:
|
||||
return self.model[key]
|
||||
return default
|
||||
try:
|
||||
return getattr(self, key)
|
||||
except AttributeError:
|
||||
try:
|
||||
return getattr(self.model, key)
|
||||
except AttributeError:
|
||||
return default
|
||||
|
||||
@property
|
||||
def now(self):
|
||||
@ -206,17 +234,18 @@ class BaseAgent(MesaAgent, MutableMapping, metaclass=MetaAgent):
|
||||
def die(self, msg=None):
|
||||
if msg:
|
||||
self.info("Agent dying:", msg)
|
||||
self.debug(f"agent dying")
|
||||
else:
|
||||
self.debug(f"agent dying")
|
||||
self.alive = False
|
||||
try:
|
||||
self.model.schedule.remove(self)
|
||||
except KeyError:
|
||||
pass
|
||||
return time.NEVER
|
||||
return time.Delay(time.INFINITY)
|
||||
|
||||
def step(self):
|
||||
raise NotImplementedError("Agent must implement step method")
|
||||
|
||||
|
||||
def _check_alive(self):
|
||||
if not self.alive:
|
||||
raise time.DeadAgent(self.unique_id)
|
||||
@ -265,6 +294,12 @@ class BaseAgent(MesaAgent, MutableMapping, metaclass=MetaAgent):
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__}({self.unique_id})"
|
||||
|
||||
def at(self, at):
|
||||
return time.Delay(float(at) - self.now)
|
||||
|
||||
def delay(self, delay=1):
|
||||
return time.Delay(delay)
|
||||
|
||||
|
||||
def prob(prob, random):
|
||||
@ -450,8 +485,10 @@ def filter_agents(
|
||||
state = state or dict()
|
||||
state.update(kwargs)
|
||||
|
||||
for k, v in state.items():
|
||||
f = filter(lambda agent: getattr(agent, k, None) == v, f)
|
||||
for k, vs in state.items():
|
||||
if not isinstance(vs, list):
|
||||
vs = [vs]
|
||||
f = filter(lambda agent: any(getattr(agent, k, None) == v for v in vs), f)
|
||||
|
||||
if limit is not None:
|
||||
f = islice(f, limit)
|
||||
@ -658,15 +695,6 @@ from .SISaModel import *
|
||||
from .CounterModel import *
|
||||
|
||||
|
||||
try:
|
||||
import scipy
|
||||
from .Geo import Geo
|
||||
except ImportError:
|
||||
import sys
|
||||
|
||||
print("Could not load the Geo Agent, scipy is not installed", file=sys.stderr)
|
||||
|
||||
|
||||
def custom(cls, **kwargs):
|
||||
"""Create a new class from a template class and keyword arguments"""
|
||||
return type(cls.__name__, (cls,), kwargs)
|
||||
|
@ -1,42 +1,8 @@
|
||||
from . import BaseAgent
|
||||
from ..events import Message, Tell, Ask, TimedOut
|
||||
from ..time import BaseCond
|
||||
from functools import partial
|
||||
from collections import deque
|
||||
|
||||
|
||||
class ReceivedOrTimeout(BaseCond):
|
||||
def __init__(
|
||||
self, agent, expiration=None, timeout=None, check=True, ignore=False, **kwargs
|
||||
):
|
||||
if expiration is None:
|
||||
if timeout is not None:
|
||||
expiration = agent.now + timeout
|
||||
self.expiration = expiration
|
||||
self.ignore = ignore
|
||||
self.check = check
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def expired(self, time):
|
||||
return self.expiration and self.expiration < time
|
||||
|
||||
def ready(self, agent, time):
|
||||
return len(agent._inbox) or self.expired(time)
|
||||
|
||||
def return_value(self, agent):
|
||||
if not self.ignore and self.expired(agent.now):
|
||||
raise TimedOut("No messages received")
|
||||
if self.check:
|
||||
agent.check_messages()
|
||||
return None
|
||||
|
||||
def schedule_next(self, time, delta, first=False):
|
||||
if self._delta is not None:
|
||||
delta = self._delta
|
||||
return (time + delta, self)
|
||||
|
||||
def __repr__(self):
|
||||
return f"ReceivedOrTimeout(expires={self.expiration})"
|
||||
from types import coroutine
|
||||
|
||||
|
||||
class EventedAgent(BaseAgent):
|
||||
@ -48,30 +14,45 @@ class EventedAgent(BaseAgent):
|
||||
def on_receive(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def received(self, *args, **kwargs):
|
||||
return ReceivedOrTimeout(self, *args, **kwargs)
|
||||
@coroutine
|
||||
def received(self, expiration=None, timeout=60, delay=1, process=True):
|
||||
if not expiration:
|
||||
expiration = self.now + timeout
|
||||
while self.now < expiration:
|
||||
if self._inbox:
|
||||
msgs = self._inbox
|
||||
if process:
|
||||
self.process_messages()
|
||||
return msgs
|
||||
yield self.delay(delay)
|
||||
raise TimedOut("No message received")
|
||||
|
||||
def tell(self, msg, sender=None):
|
||||
self._inbox.append(Tell(timestamp=self.now, payload=msg, sender=sender))
|
||||
|
||||
def ask(self, msg, timeout=None, **kwargs):
|
||||
@coroutine
|
||||
def ask(self, msg, expiration=None, timeout=None, delay=1):
|
||||
ask = Ask(timestamp=self.now, payload=msg, sender=self)
|
||||
self._inbox.append(ask)
|
||||
expiration = float("inf") if timeout is None else self.now + timeout
|
||||
return ask.replied(expiration=expiration, **kwargs)
|
||||
while self.now < expiration:
|
||||
if ask.reply:
|
||||
return ask.reply
|
||||
yield self.delay(delay)
|
||||
raise TimedOut("No reply received")
|
||||
|
||||
def check_messages(self):
|
||||
changed = False
|
||||
while self._inbox:
|
||||
msg = self._inbox.popleft()
|
||||
def process_messages(self):
|
||||
valid = list()
|
||||
for msg in self._inbox:
|
||||
self._processed += 1
|
||||
if msg.expired(self.now):
|
||||
continue
|
||||
changed = True
|
||||
valid.append(msg)
|
||||
reply = self.on_receive(msg.payload, sender=msg.sender)
|
||||
if isinstance(msg, Ask):
|
||||
msg.reply = reply
|
||||
return changed
|
||||
self._inbox.clear()
|
||||
return valid
|
||||
|
||||
|
||||
Evented = EventedAgent
|
||||
|
@ -1,47 +1,69 @@
|
||||
from . import MetaAgent, BaseAgent
|
||||
from ..time import Delta
|
||||
|
||||
from .. import time
|
||||
from types import coroutine
|
||||
from functools import partial, wraps
|
||||
import inspect
|
||||
|
||||
|
||||
class State:
|
||||
__slots__ = ("awaitable", "f", "generator", "name", "default")
|
||||
|
||||
def __init__(self, f, name, default, generator, awaitable):
|
||||
self.f = f
|
||||
self.name = name
|
||||
self.generator = generator
|
||||
self.awaitable = awaitable
|
||||
self.default = default
|
||||
|
||||
@coroutine
|
||||
def step(self, obj):
|
||||
if self.generator or self.awaitable:
|
||||
f = self.f
|
||||
next_state = yield from f(obj)
|
||||
return next_state
|
||||
|
||||
else:
|
||||
return self.f(obj)
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self.name
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
raise Exception("States should not be called directly")
|
||||
|
||||
class UnboundState(State):
|
||||
|
||||
def bind(self, obj):
|
||||
bs = BoundState(self.f, self.name, self.default, self.generator, self.awaitable, obj=obj)
|
||||
setattr(obj, self.name, bs)
|
||||
return bs
|
||||
|
||||
|
||||
class BoundState(State):
|
||||
__slots__ = ("obj", )
|
||||
|
||||
def __init__(self, *args, obj):
|
||||
super().__init__(*args)
|
||||
self.obj = obj
|
||||
|
||||
def delay(self, delta=0):
|
||||
return self, self.obj.delay(delta)
|
||||
|
||||
def at(self, when):
|
||||
return self, self.obj.at(when)
|
||||
|
||||
|
||||
def state(name=None, default=False):
|
||||
def decorator(func, name=None):
|
||||
"""
|
||||
A state function should return either a state id, or a tuple (state_id, when)
|
||||
The default value for state_id is the current state id.
|
||||
The default value for when is the interval defined in the environment.
|
||||
"""
|
||||
if inspect.isgeneratorfunction(func):
|
||||
orig_func = func
|
||||
|
||||
@wraps(func)
|
||||
def func(self):
|
||||
while True:
|
||||
if not self._coroutine:
|
||||
self._coroutine = orig_func(self)
|
||||
|
||||
try:
|
||||
if self._last_except:
|
||||
n = self._coroutine.throw(self._last_except)
|
||||
else:
|
||||
n = self._coroutine.send(self._last_return)
|
||||
if n:
|
||||
return None, n
|
||||
return n
|
||||
except StopIteration as ex:
|
||||
self._coroutine = None
|
||||
next_state = ex.value
|
||||
if next_state is not None:
|
||||
self._set_state(next_state)
|
||||
return next_state
|
||||
finally:
|
||||
self._last_return = None
|
||||
self._last_except = None
|
||||
|
||||
func.id = name or func.__name__
|
||||
func.is_default = default
|
||||
return func
|
||||
name = name or func.__name__
|
||||
generator = inspect.isgeneratorfunction(func)
|
||||
awaitable = inspect.iscoroutinefunction(func) or inspect.isasyncgen(func)
|
||||
return UnboundState(func, name, default, generator, awaitable)
|
||||
|
||||
if callable(name):
|
||||
return decorator(name)
|
||||
@ -50,7 +72,7 @@ def state(name=None, default=False):
|
||||
|
||||
|
||||
def default_state(func):
|
||||
func.is_default = True
|
||||
func.default = True
|
||||
return func
|
||||
|
||||
|
||||
@ -62,42 +84,45 @@ class MetaFSM(MetaAgent):
|
||||
for i in bases:
|
||||
if isinstance(i, MetaFSM):
|
||||
for state_id, state in i._states.items():
|
||||
if state.is_default:
|
||||
if state.default:
|
||||
default_state = state
|
||||
states[state_id] = state
|
||||
|
||||
# Add new states
|
||||
for attr, func in namespace.items():
|
||||
if hasattr(func, "id"):
|
||||
if func.is_default:
|
||||
if isinstance(func, State):
|
||||
if func.default:
|
||||
default_state = func
|
||||
states[func.id] = func
|
||||
states[func.name] = func
|
||||
|
||||
namespace.update(
|
||||
{
|
||||
"_default_state": default_state,
|
||||
"_state": default_state,
|
||||
"_states": states,
|
||||
}
|
||||
)
|
||||
|
||||
return super(MetaFSM, mcls).__new__(
|
||||
cls = super(MetaFSM, mcls).__new__(
|
||||
mcls=mcls, name=name, bases=bases, namespace=namespace
|
||||
)
|
||||
for (k, v) in states.items():
|
||||
setattr(cls, k, v)
|
||||
return cls
|
||||
|
||||
|
||||
class FSM(BaseAgent, metaclass=MetaFSM):
|
||||
def __init__(self, init=True, **kwargs):
|
||||
def __init__(self, init=True, state_id=None, **kwargs):
|
||||
super().__init__(**kwargs, init=False)
|
||||
if not hasattr(self, "state_id"):
|
||||
if not self._default_state:
|
||||
raise ValueError(
|
||||
"No default state specified for {}".format(self.unique_id)
|
||||
)
|
||||
self.state_id = self._default_state.id
|
||||
if state_id is not None:
|
||||
self._set_state(state_id)
|
||||
# If more than "dead" state is defined, but no default state
|
||||
if len(self._states) > 1 and not self._state:
|
||||
raise ValueError(
|
||||
f"No default state specified for {type(self)}({self.unique_id})"
|
||||
)
|
||||
for (k, v) in self._states.items():
|
||||
setattr(self, k, v.bind(self))
|
||||
|
||||
self._coroutine = None
|
||||
self.default_interval = Delta(self.model.interval)
|
||||
self._set_state(self.state_id)
|
||||
if init:
|
||||
self.init()
|
||||
|
||||
@ -105,44 +130,46 @@ class FSM(BaseAgent, metaclass=MetaFSM):
|
||||
def states(cls):
|
||||
return list(cls._states.keys())
|
||||
|
||||
@property
|
||||
def state_id(self):
|
||||
return self._state.name
|
||||
|
||||
def set_state(self, value):
|
||||
if self.now > 0:
|
||||
raise ValueError("Cannot change state after init")
|
||||
self._set_state(value)
|
||||
|
||||
def step(self):
|
||||
self.debug(f"Agent {self.unique_id} @ state {self.state_id}")
|
||||
|
||||
self._check_alive()
|
||||
next_state = self._states[self.state_id](self)
|
||||
next_state = yield from self._state.step(self)
|
||||
|
||||
when = None
|
||||
try:
|
||||
next_state, *when = next_state
|
||||
if not when:
|
||||
when = None
|
||||
elif len(when) == 1:
|
||||
when = when[0]
|
||||
else:
|
||||
raise ValueError(
|
||||
"Too many values returned. Only state (and time) allowed"
|
||||
)
|
||||
except TypeError:
|
||||
pass
|
||||
next_state, when = next_state
|
||||
except (TypeError, ValueError) as ex:
|
||||
try:
|
||||
self._set_state(next_state)
|
||||
return None
|
||||
except ValueError:
|
||||
return next_state
|
||||
|
||||
if next_state is not None:
|
||||
self._set_state(next_state)
|
||||
self._set_state(next_state)
|
||||
return when
|
||||
|
||||
return when or self.default_interval
|
||||
|
||||
def _set_state(self, state, when=None):
|
||||
if hasattr(state, "id"):
|
||||
state = state.id
|
||||
if state not in self._states:
|
||||
def _set_state(self, state):
|
||||
if state is None:
|
||||
return
|
||||
if isinstance(state, str):
|
||||
if state not in self._states:
|
||||
raise ValueError("{} is not a valid state".format(state))
|
||||
state = self._states[state]
|
||||
if not isinstance(state, State):
|
||||
raise ValueError("{} is not a valid state".format(state))
|
||||
self.state_id = state
|
||||
if when is not None:
|
||||
self.model.schedule.add(self, when=when)
|
||||
return state
|
||||
self._state = state
|
||||
|
||||
def die(self, *args, **kwargs):
|
||||
return self.dead, super().die(*args, **kwargs)
|
||||
super().die(*args, **kwargs)
|
||||
return self.dead.at(time.INFINITY)
|
||||
|
||||
@state
|
||||
def dead(self):
|
||||
return self.die()
|
||||
return time.INFINITY
|
@ -6,7 +6,7 @@ class NetworkAgent(BaseAgent):
|
||||
super().__init__(*args, init=False, **kwargs)
|
||||
|
||||
self.G = topology or self.model.G
|
||||
assert self.G
|
||||
assert self.G is not None, "Network agents should have a network"
|
||||
if node_id is None:
|
||||
nodes = self.random.choices(list(self.G.nodes), k=len(self.G))
|
||||
for n_id in nodes:
|
||||
@ -25,8 +25,6 @@ class NetworkAgent(BaseAgent):
|
||||
|
||||
def count_neighbors(self, state_id=None, **kwargs):
|
||||
return len(self.get_neighbors(state_id=state_id, **kwargs))
|
||||
if init:
|
||||
self.init()
|
||||
|
||||
def iter_neighbors(self, **kwargs):
|
||||
return self.iter_agents(limit_neighbors=True, **kwargs)
|
||||
|
@ -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"])
|
||||
#TODO implement reading from CSV and SQLITE
|
||||
#TODO implement reading from CSV
|
||||
|
||||
def read_sql(fpath=None, name=None, include_agents=False):
|
||||
if not (fpath is None) ^ (name is None):
|
||||
raise ValueError("Specify either a path or a simulation name")
|
||||
|
@ -9,7 +9,7 @@ class SoilCollector(MDC):
|
||||
if 'agent_count' not in model_reporters:
|
||||
model_reporters['agent_count'] = lambda m: m.schedule.get_agent_count()
|
||||
if 'time' not in model_reporters:
|
||||
model_reporters['time'] = lambda m: m.now
|
||||
model_reporters['time'] = lambda m: m.schedule.time
|
||||
# if 'state_id' not in agent_reporters:
|
||||
# agent_reporters['state_id'] = lambda agent: getattr(agent, 'state_id', None)
|
||||
|
||||
|
@ -16,7 +16,7 @@ import networkx as nx
|
||||
|
||||
from mesa import Model, Agent
|
||||
|
||||
from . import agents as agentmod, datacollection, serialization, utils, time, network, events
|
||||
from . import agents as agentmod, datacollection, utils, time, network, events
|
||||
|
||||
|
||||
# TODO: maybe add metaclass to read attributes of a model
|
||||
@ -35,6 +35,7 @@ class BaseEnvironment(Model):
|
||||
"""
|
||||
|
||||
collector_class = datacollection.SoilCollector
|
||||
schedule_class = time.TimedActivation
|
||||
|
||||
def __new__(cls,
|
||||
*args: Any,
|
||||
@ -49,7 +50,6 @@ class BaseEnvironment(Model):
|
||||
self = super().__new__(cls, *args, seed=seed, **kwargs)
|
||||
self.dir_path = dir_path or os.getcwd()
|
||||
collector_class = collector_class or cls.collector_class
|
||||
collector_class = serialization.deserialize(collector_class)
|
||||
self.datacollector = collector_class(
|
||||
model_reporters=model_reporters,
|
||||
agent_reporters=agent_reporters,
|
||||
@ -60,7 +60,7 @@ class BaseEnvironment(Model):
|
||||
if isinstance(v, property):
|
||||
v = v.fget
|
||||
if getattr(v, "add_to_report", False):
|
||||
self.add_model_reporter(k, v)
|
||||
self.add_model_reporter(k, k)
|
||||
|
||||
return self
|
||||
|
||||
@ -70,8 +70,8 @@ class BaseEnvironment(Model):
|
||||
id="unnamed_env",
|
||||
seed="default",
|
||||
dir_path=None,
|
||||
schedule_class=time.TimedActivation,
|
||||
interval=1,
|
||||
schedule=None,
|
||||
schedule_class=None,
|
||||
logger = None,
|
||||
agents: Optional[Dict] = None,
|
||||
collector_class: type = datacollection.SoilCollector,
|
||||
@ -94,13 +94,11 @@ class BaseEnvironment(Model):
|
||||
else:
|
||||
self.logger = utils.logger.getChild(self.id)
|
||||
|
||||
if schedule_class is None:
|
||||
schedule_class = time.TimedActivation
|
||||
else:
|
||||
schedule_class = serialization.deserialize(schedule_class)
|
||||
|
||||
self.interval = interval
|
||||
self.schedule = schedule_class(self)
|
||||
self.schedule = schedule
|
||||
if schedule is None:
|
||||
if schedule_class is None:
|
||||
schedule_class = self.schedule_class
|
||||
self.schedule = schedule_class(self)
|
||||
|
||||
for (k, v) in env_params.items():
|
||||
self[k] = v
|
||||
@ -161,7 +159,7 @@ class BaseEnvironment(Model):
|
||||
if unique_id is None:
|
||||
unique_id = self.next_id()
|
||||
|
||||
a = serialization.deserialize(agent_class)(unique_id=unique_id, model=self, **agent)
|
||||
a = agent_class(unique_id=unique_id, model=self, **agent)
|
||||
|
||||
self.schedule.add(a)
|
||||
return a
|
||||
@ -204,14 +202,14 @@ class BaseEnvironment(Model):
|
||||
|
||||
def add_model_reporter(self, name, func=None):
|
||||
if not func:
|
||||
func = lambda env: getattr(env, name)
|
||||
func = name
|
||||
self.datacollector._new_model_reporter(name, func)
|
||||
|
||||
def add_agent_reporter(self, name, agent_type=None):
|
||||
if agent_type:
|
||||
reporter = lambda a: getattr(a, name) if isinstance(a, agent_type) else None
|
||||
else:
|
||||
reporter = lambda a: getattr(a, name, None)
|
||||
def add_agent_reporter(self, name, reporter=None, agent_type=None):
|
||||
if not agent_type and not reporter:
|
||||
reporter = name
|
||||
elif agent_type:
|
||||
reporter = lambda a: reporter(a) if isinstance(a, agent_type) else None
|
||||
self.datacollector._new_agent_reporter(name, reporter)
|
||||
|
||||
@classmethod
|
||||
@ -278,8 +276,6 @@ class NetworkEnvironment(BaseEnvironment):
|
||||
super().__init__(*args, **kwargs, init=False)
|
||||
|
||||
self.agent_class = agent_class
|
||||
if agent_class:
|
||||
self.agent_class = serialization.deserialize(agent_class)
|
||||
if self.agent_class:
|
||||
self.populate_network(self.agent_class)
|
||||
self._check_agent_nodes()
|
||||
@ -309,7 +305,15 @@ class NetworkEnvironment(BaseEnvironment):
|
||||
elif path is not None:
|
||||
topology = network.from_topology(path, dir_path=self.dir_path)
|
||||
elif generator is not None:
|
||||
topology = network.from_params(generator=generator, dir_path=self.dir_path, **network_params)
|
||||
params = dict(generator=generator,
|
||||
dir_path=self.dir_path,
|
||||
seed=self.random,
|
||||
**network_params)
|
||||
try:
|
||||
topology = network.from_params(**params)
|
||||
except TypeError:
|
||||
del params["seed"]
|
||||
topology = network.from_params(**params)
|
||||
else:
|
||||
raise ValueError("topology must be a networkx.Graph or a string, or network_generator must be provided")
|
||||
self.G = topology
|
||||
|
@ -1,4 +1,3 @@
|
||||
from .time import BaseCond
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
@ -24,29 +23,9 @@ class Reply(Message):
|
||||
source: Message
|
||||
|
||||
|
||||
class ReplyCond(BaseCond):
|
||||
def __init__(self, ask, *args, **kwargs):
|
||||
self._ask = ask
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def ready(self, agent, time):
|
||||
return self._ask.reply is not None or self._ask.expired(time)
|
||||
|
||||
def return_value(self, agent):
|
||||
if self._ask.expired(agent.now):
|
||||
raise TimedOut()
|
||||
return self._ask.reply
|
||||
|
||||
def __repr__(self):
|
||||
return f"ReplyCond({self._ask.id})"
|
||||
|
||||
|
||||
class Ask(Message):
|
||||
reply: Message = None
|
||||
|
||||
def replied(self, expiration=None):
|
||||
return ReplyCond(self)
|
||||
|
||||
|
||||
class Tell(Message):
|
||||
pass
|
||||
|
@ -2,11 +2,9 @@ import os
|
||||
import sys
|
||||
from time import time as current_time
|
||||
from io import BytesIO
|
||||
from sqlalchemy import create_engine
|
||||
from textwrap import dedent, indent
|
||||
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import networkx as nx
|
||||
import pandas as pd
|
||||
|
||||
@ -124,6 +122,9 @@ class SQLite(Exporter):
|
||||
if not self.dump:
|
||||
logger.debug("NOT dumping results")
|
||||
return
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
|
||||
self.dbpath = os.path.join(self.outdir, f"{self.simulation.name}.sqlite")
|
||||
logger.info("Dumping results to %s", self.dbpath)
|
||||
if self.simulation.backup:
|
||||
@ -175,7 +176,6 @@ class csv(Exporter):
|
||||
df.to_csv(f)
|
||||
|
||||
|
||||
# TODO: reimplement GEXF exporting without history
|
||||
class gexf(Exporter):
|
||||
def iteration_end(self, env, *args, **kwargs):
|
||||
if not self.dump:
|
||||
@ -186,8 +186,7 @@ class gexf(Exporter):
|
||||
"[GEXF] Dumping simulation {} iteration {}".format(self.simulation.name, env.id)
|
||||
):
|
||||
with self.output("{}.gexf".format(env.id), mode="wb") as f:
|
||||
network.dump_gexf(env.history_to_graph(), f)
|
||||
self.dump_gexf(env, f)
|
||||
nx.write_gexf(env.G, f)
|
||||
|
||||
|
||||
class dummy(Exporter):
|
||||
@ -210,6 +209,7 @@ class dummy(Exporter):
|
||||
|
||||
class graphdrawing(Exporter):
|
||||
def iteration_end(self, env, *args, **kwargs):
|
||||
import matplotlib.pyplot as plt
|
||||
# Outside effects
|
||||
f = plt.figure()
|
||||
nx.draw(
|
||||
|
@ -103,7 +103,13 @@ def load_config(cfg):
|
||||
yield from load_files(cfg)
|
||||
|
||||
|
||||
builtins = importlib.import_module("builtins")
|
||||
_BUILTINS = None
|
||||
|
||||
def builtins():
|
||||
global _BUILTINS
|
||||
if not _BUILTINS:
|
||||
_BUILTINS = importlib.import_module("builtins")
|
||||
return _BUILTINS
|
||||
|
||||
KNOWN_MODULES = {
|
||||
'soil': None,
|
||||
@ -163,7 +169,7 @@ def name(value, known_modules=KNOWN_MODULES):
|
||||
if not isinstance(value, type): # Get the class name first
|
||||
value = type(value)
|
||||
tname = value.__name__
|
||||
if hasattr(builtins, tname):
|
||||
if hasattr(builtins(), tname):
|
||||
return tname
|
||||
modname = value.__module__
|
||||
if modname == "__main__":
|
||||
@ -178,7 +184,7 @@ def name(value, known_modules=KNOWN_MODULES):
|
||||
|
||||
|
||||
def serializer(type_):
|
||||
if type_ != "str" and hasattr(builtins, type_):
|
||||
if type_ != "str":
|
||||
return repr
|
||||
return lambda x: x
|
||||
|
||||
@ -216,8 +222,8 @@ def deserializer(type_, known_modules=KNOWN_MODULES):
|
||||
return lambda x="": x
|
||||
if type_ == "None":
|
||||
return lambda x=None: None
|
||||
if hasattr(builtins, type_): # Check if it's a builtin type
|
||||
cls = getattr(builtins, type_)
|
||||
if hasattr(builtins(), type_): # Check if it's a builtin type
|
||||
cls = getattr(builtins(), type_)
|
||||
return lambda x=None: ast.literal_eval(x) if x is not None else cls()
|
||||
match = IS_CLASS.match(type_)
|
||||
if match:
|
||||
|
@ -23,7 +23,7 @@ import json
|
||||
|
||||
|
||||
from . import serialization, exporters, utils, basestring, agents
|
||||
from .environment import Environment
|
||||
from . import environment
|
||||
from .utils import logger, run_and_return_exceptions
|
||||
from .debugging import set_trace
|
||||
|
||||
@ -49,8 +49,6 @@ def _iter_queued():
|
||||
|
||||
|
||||
# TODO: change documentation for simulation
|
||||
# TODO: rename iterations to iterations
|
||||
# TODO: make parameters a dict of iterable/any
|
||||
@dataclass
|
||||
class Simulation:
|
||||
"""
|
||||
@ -68,7 +66,6 @@ class Simulation:
|
||||
dir_path: The directory path to use for the simulation.
|
||||
max_time: The maximum time to run the simulation.
|
||||
max_steps: The maximum number of steps to run the simulation.
|
||||
interval: The interval to use for the simulation.
|
||||
iterations: The number of iterations (times) to run the simulation.
|
||||
num_processes: The number of processes to use for the simulation. If greater than one, simulations will be performed in parallel. This may make debugging and error handling difficult.
|
||||
tables: The tables to use in the simulation datacollector
|
||||
@ -96,7 +93,6 @@ class Simulation:
|
||||
dir_path: str = field(default_factory=lambda: os.getcwd())
|
||||
max_time: float = None
|
||||
max_steps: int = None
|
||||
interval: int = 1
|
||||
iterations: int = 1
|
||||
num_processes: Optional[int] = 1
|
||||
exporters: Optional[List[str]] = field(default_factory=lambda: [exporters.default])
|
||||
@ -126,15 +122,9 @@ class Simulation:
|
||||
if isinstance(self.model, str):
|
||||
self.model = serialization.deserialize(self.model)
|
||||
|
||||
def deserialize_reporters(reporters):
|
||||
for (k, v) in reporters.items():
|
||||
if isinstance(v, str) and v.startswith("py:"):
|
||||
reporters[k] = serialization.deserialize(v.split(":", 1)[1])
|
||||
return reporters
|
||||
|
||||
self.agent_reporters = deserialize_reporters(self.agent_reporters)
|
||||
self.model_reporters = deserialize_reporters(self.model_reporters)
|
||||
self.tables = deserialize_reporters(self.tables)
|
||||
self.agent_reporters = self.agent_reporters
|
||||
self.model_reporters = self.model_reporters
|
||||
self.tables = self.tables
|
||||
self.id = f"{self.name}_{current_time()}"
|
||||
|
||||
def run(self, **kwargs):
|
||||
@ -142,15 +132,6 @@ class Simulation:
|
||||
if kwargs:
|
||||
return replace(self, **kwargs).run()
|
||||
|
||||
self.logger.debug(
|
||||
dedent(
|
||||
"""
|
||||
Simulation:
|
||||
---
|
||||
"""
|
||||
)
|
||||
+ self.to_yaml()
|
||||
)
|
||||
param_combinations = self._collect_params(**kwargs)
|
||||
if _AVOID_RUNNING:
|
||||
_QUEUED.extend((self, param) for param in param_combinations)
|
||||
@ -244,7 +225,6 @@ class Simulation:
|
||||
id=iteration_id,
|
||||
seed=f"{self.seed}_iteration_{iteration_id}",
|
||||
dir_path=self.dir_path,
|
||||
interval=self.interval,
|
||||
logger=self.logger.getChild(iteration_id),
|
||||
agent_reporters=agent_reporters,
|
||||
model_reporters=model_reporters,
|
||||
@ -359,8 +339,11 @@ def iter_from_py(pyfile, module_name='imported_file', **kwargs):
|
||||
for sim in _iter_queued():
|
||||
sims.append(sim)
|
||||
if not sims:
|
||||
for (_name, sim) in inspect.getmembers(module, lambda x: inspect.isclass(x) and issubclass(x, Simulation)):
|
||||
sims.append(sim(**kwargs))
|
||||
for (_name, env) in inspect.getmembers(module,
|
||||
lambda x: inspect.isclass(x) and
|
||||
issubclass(x, environment.Environment) and
|
||||
(getattr(x, "__module__", None) != environment.__name__)):
|
||||
sims.append(Simulation(model=env, **kwargs))
|
||||
del sys.modules[module_name]
|
||||
assert not _AVOID_RUNNING
|
||||
if not sims:
|
||||
|
233
soil/time.py
233
soil/time.py
@ -1,7 +1,9 @@
|
||||
from mesa.time import BaseScheduler
|
||||
from queue import Empty
|
||||
from heapq import heappush, heappop, heapreplace
|
||||
from collections import deque
|
||||
import math
|
||||
import logging
|
||||
|
||||
from inspect import getsource
|
||||
from numbers import Number
|
||||
@ -13,119 +15,54 @@ from mesa import Agent as MesaAgent
|
||||
|
||||
INFINITY = float("inf")
|
||||
|
||||
class Delay:
|
||||
"""A delay object which can be used both as a return value and as an awaitable (in async code)."""
|
||||
__slots__ = ("delta", )
|
||||
def __init__(self, delta):
|
||||
self.delta = float(delta)
|
||||
|
||||
def __float__(self):
|
||||
return self.delta
|
||||
|
||||
def __await__(self):
|
||||
return (yield self.delta)
|
||||
|
||||
|
||||
class DeadAgent(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class When:
|
||||
def __init__(self, time):
|
||||
if isinstance(time, When):
|
||||
return time
|
||||
self._time = time
|
||||
|
||||
def abs(self, time):
|
||||
return self._time
|
||||
|
||||
def schedule_next(self, time, delta, first=False):
|
||||
return (self._time, None)
|
||||
|
||||
|
||||
NEVER = When(INFINITY)
|
||||
|
||||
|
||||
class Delta(When):
|
||||
def __init__(self, delta):
|
||||
self._delta = delta
|
||||
|
||||
def abs(self, time):
|
||||
return self._time + self._delta
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, Delta):
|
||||
return self._delta == other._delta
|
||||
return False
|
||||
|
||||
def schedule_next(self, time, delta, first=False):
|
||||
return (time + self._delta, None)
|
||||
|
||||
def __repr__(self):
|
||||
return str(f"Delta({self._delta})")
|
||||
|
||||
|
||||
class BaseCond:
|
||||
def __init__(self, msg=None, delta=None, eager=False):
|
||||
self._msg = msg
|
||||
self._delta = delta
|
||||
self.eager = eager
|
||||
|
||||
def schedule_next(self, time, delta, first=False):
|
||||
if first and self.eager:
|
||||
return (time, self)
|
||||
if self._delta:
|
||||
delta = self._delta
|
||||
return (time + delta, self)
|
||||
|
||||
def return_value(self, agent):
|
||||
return None
|
||||
|
||||
def __repr__(self):
|
||||
return self._msg or self.__class__.__name__
|
||||
|
||||
|
||||
class Cond(BaseCond):
|
||||
def __init__(self, func, *args, **kwargs):
|
||||
self._func = func
|
||||
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.
|
||||
class PQueueActivation(BaseScheduler):
|
||||
"""
|
||||
A scheduler which activates each agent with a delay returned by the agent's step method.
|
||||
If no delay is returned, a default of 1 is used.
|
||||
|
||||
In each activation, each agent will update its 'next_time'.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, shuffle=True, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._next = {}
|
||||
self._queue = []
|
||||
self._shuffle = shuffle
|
||||
# self.step_interval = getattr(self.model, "interval", 1)
|
||||
self.step_interval = self.model.interval
|
||||
self.logger = getattr(self.model, "logger", logger).getChild(f"time_{ self.model }")
|
||||
self.next_time = self.time
|
||||
|
||||
def add(self, agent: MesaAgent, when=None):
|
||||
if when is None:
|
||||
when = self.time
|
||||
elif isinstance(when, When):
|
||||
when = when.abs()
|
||||
else:
|
||||
when = float(when)
|
||||
|
||||
self._schedule(agent, None, when)
|
||||
super().add(agent)
|
||||
|
||||
def _schedule(self, agent, condition=None, when=None, replace=False):
|
||||
if condition:
|
||||
if not when:
|
||||
when, condition = condition.schedule_next(
|
||||
when or self.time, self.step_interval
|
||||
)
|
||||
else:
|
||||
if when is None:
|
||||
when = self.time + self.step_interval
|
||||
condition = None
|
||||
def _schedule(self, agent, when=None, replace=False):
|
||||
if when is None:
|
||||
when = self.time
|
||||
if self._shuffle:
|
||||
key = (when, self.model.random.random(), condition)
|
||||
key = (when, self.model.random.random())
|
||||
else:
|
||||
key = (when, agent.unique_id, condition)
|
||||
self._next[agent.unique_id] = key
|
||||
key = (when, agent.unique_id)
|
||||
if replace:
|
||||
heapreplace(self._queue, (key, agent))
|
||||
else:
|
||||
@ -137,70 +74,104 @@ class TimedActivation(BaseScheduler):
|
||||
an agent will signal when it wants to be scheduled next.
|
||||
"""
|
||||
|
||||
self.logger.debug(f"Simulation step {self.time}")
|
||||
if not self.model.running or self.time == INFINITY:
|
||||
if self.time == INFINITY:
|
||||
return
|
||||
|
||||
self.logger.debug(f"Queue length: %s", len(self._queue))
|
||||
next_time = INFINITY
|
||||
|
||||
now = self.time
|
||||
|
||||
while self._queue:
|
||||
((when, _id, cond), agent) = self._queue[0]
|
||||
if when > self.time:
|
||||
((when, _id), agent) = self._queue[0]
|
||||
if when > now:
|
||||
next_time = when
|
||||
break
|
||||
|
||||
if cond:
|
||||
if not cond.ready(agent, self.time):
|
||||
self._schedule(agent, cond, replace=True)
|
||||
continue
|
||||
try:
|
||||
agent._last_return = cond.return_value(agent)
|
||||
except Exception as ex:
|
||||
agent._last_except = ex
|
||||
else:
|
||||
agent._last_return = None
|
||||
agent._last_except = None
|
||||
|
||||
self.logger.debug("Stepping agent %s", agent)
|
||||
self._next.pop(agent.unique_id, None)
|
||||
|
||||
try:
|
||||
returned = agent.step()
|
||||
when = agent.step() or 1
|
||||
when += now
|
||||
except DeadAgent:
|
||||
agent.alive = False
|
||||
heappop(self._queue)
|
||||
continue
|
||||
|
||||
# Check status for MESA agents
|
||||
if not getattr(agent, "alive", True):
|
||||
if when == INFINITY:
|
||||
heappop(self._queue)
|
||||
continue
|
||||
|
||||
if returned:
|
||||
next_check = returned.schedule_next(
|
||||
self.time, self.step_interval, first=True
|
||||
)
|
||||
self._schedule(agent, when=next_check[0], condition=next_check[1], replace=True)
|
||||
else:
|
||||
next_check = (self.time + self.step_interval, None)
|
||||
|
||||
self._schedule(agent, replace=True)
|
||||
self._schedule(agent, when, replace=True)
|
||||
|
||||
self.steps += 1
|
||||
|
||||
if not self._queue:
|
||||
self.time = next_time
|
||||
|
||||
if next_time == INFINITY:
|
||||
self.model.running = False
|
||||
self.time = INFINITY
|
||||
return
|
||||
|
||||
next_time = self._queue[0][0][0]
|
||||
|
||||
if next_time < self.time:
|
||||
raise Exception(
|
||||
f"An agent has been scheduled for a time in the past, there is probably an error ({when} < {self.time})"
|
||||
)
|
||||
self.logger.debug("Updating time step: %s -> %s ", self.time, next_time)
|
||||
class TimedActivation(BaseScheduler):
|
||||
def __init__(self, *args, shuffle=True, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._queue = deque()
|
||||
self._shuffle = shuffle
|
||||
self.logger = getattr(self.model, "logger", logger).getChild(f"time_{ self.model }")
|
||||
self.next_time = self.time
|
||||
|
||||
self.time = next_time
|
||||
def add(self, agent: MesaAgent, when=None):
|
||||
if when is None:
|
||||
when = self.time
|
||||
else:
|
||||
when = float(when)
|
||||
self._schedule(agent, None, when)
|
||||
super().add(agent)
|
||||
|
||||
def _schedule(self, agent, when=None, replace=False):
|
||||
when = when or self.time
|
||||
pos = len(self._queue)
|
||||
for (ix, l) in enumerate(self._queue):
|
||||
if l[0] == when:
|
||||
l[1].append(agent)
|
||||
return
|
||||
if l[0] > when:
|
||||
pos = ix
|
||||
break
|
||||
self._queue.insert(pos, (when, [agent]))
|
||||
|
||||
def step(self) -> None:
|
||||
"""
|
||||
Executes agents in order, one at a time. After each step,
|
||||
an agent will signal when it wants to be scheduled next.
|
||||
"""
|
||||
if not self._queue:
|
||||
return
|
||||
|
||||
now = self.time
|
||||
|
||||
next_time = self._queue[0][0]
|
||||
|
||||
if next_time > now:
|
||||
self.time = next_time
|
||||
return
|
||||
|
||||
bucket = self._queue.popleft()[1]
|
||||
if self._shuffle:
|
||||
self.model.random.shuffle(bucket)
|
||||
for agent in bucket:
|
||||
try:
|
||||
when = agent.step() or 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):
|
||||
@ -211,3 +182,5 @@ class ShuffledTimedActivation(TimedActivation):
|
||||
class OrderedTimedActivation(TimedActivation):
|
||||
def __init__(self, *args, **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"""
|
||||
d = Dead(unique_id=0, model=environment.Environment())
|
||||
ret = d.step()
|
||||
assert ret == stime.NEVER
|
||||
assert ret == stime.INFINITY
|
||||
|
||||
def test_die_raises_exception(self):
|
||||
"""A dead agent should raise an exception if it is stepped after death"""
|
||||
@ -52,23 +52,25 @@ class TestAgents(TestCase):
|
||||
|
||||
def test_state_decorator(self):
|
||||
class MyAgent(agents.FSM):
|
||||
run = 0
|
||||
times_run = 0
|
||||
|
||||
@agents.state("original", default=True)
|
||||
def root(self):
|
||||
self.run += 1
|
||||
return self.other
|
||||
|
||||
@agents.state
|
||||
def other(self):
|
||||
self.run += 1
|
||||
self.times_run += 1
|
||||
|
||||
e = environment.Environment()
|
||||
a = e.add_agent(MyAgent)
|
||||
e.step()
|
||||
assert a.run == 1
|
||||
assert a.times_run == 0
|
||||
a.step()
|
||||
print("DONE")
|
||||
assert a.times_run == 1
|
||||
assert a.state_id == MyAgent.other.id
|
||||
a.step()
|
||||
assert a.times_run == 2
|
||||
|
||||
def test_broadcast(self):
|
||||
"""
|
||||
@ -86,7 +88,7 @@ class TestAgents(TestCase):
|
||||
except Exception as ex:
|
||||
print(ex)
|
||||
while True:
|
||||
self.check_messages()
|
||||
self.process_messages()
|
||||
yield
|
||||
|
||||
def on_receive(self, msg, sender=None):
|
||||
@ -132,14 +134,14 @@ class TestAgents(TestCase):
|
||||
while True:
|
||||
if pongs or not pings: # First agent, or anyone after that
|
||||
pings.append(self.now)
|
||||
response = yield target.ask("PING")
|
||||
response = yield from target.ask("PING")
|
||||
responses.append(response)
|
||||
else:
|
||||
print("NOT sending ping")
|
||||
print("Checking msgs")
|
||||
# Do not block if we have already received a PING
|
||||
if not self.check_messages():
|
||||
yield self.received()
|
||||
if not self.process_messages():
|
||||
yield from self.received()
|
||||
print("done")
|
||||
|
||||
def on_receive(self, msg, sender=None):
|
||||
@ -174,4 +176,200 @@ class TestAgents(TestCase):
|
||||
assert len(ev) == 1
|
||||
assert ev[0].unique_id == 1
|
||||
null = list(e.agents(unique_ids=[0, 1], agent_class=agents.NetworkAgent))
|
||||
assert not null
|
||||
assert not null
|
||||
|
||||
def test_agent_return(self):
|
||||
'''
|
||||
An agent should be able to cycle through different states and control when it
|
||||
should be awaken.
|
||||
'''
|
||||
class TestAgent(agents.Agent):
|
||||
@agents.state(default=True)
|
||||
def one(self):
|
||||
return self.two
|
||||
|
||||
@agents.state
|
||||
def two(self):
|
||||
return self.three.at(10)
|
||||
|
||||
@agents.state
|
||||
def three(self):
|
||||
return self.four.delay(1)
|
||||
|
||||
@agents.state
|
||||
def four(self):
|
||||
yield self.delay(2)
|
||||
return self.five.delay(3)
|
||||
|
||||
@agents.state
|
||||
def five(self):
|
||||
return self.delay(1)
|
||||
|
||||
model = environment.Environment()
|
||||
a = model.add_agent(TestAgent)
|
||||
assert a.state_id == TestAgent.one.id
|
||||
assert a.now == 0
|
||||
model.step()
|
||||
assert a.state_id == TestAgent.two.id
|
||||
assert a.now == 1
|
||||
model.step()
|
||||
assert a.state_id == TestAgent.three.id
|
||||
assert a.now == 10
|
||||
model.step()
|
||||
assert a.state_id == TestAgent.four.id
|
||||
assert a.now == 11
|
||||
model.step()
|
||||
assert a.state_id == TestAgent.four.id
|
||||
assert a.now == 13
|
||||
model.step()
|
||||
assert a.state_id == TestAgent.five.id
|
||||
assert a.now == 16
|
||||
model.step()
|
||||
assert a.state_id == TestAgent.five.id
|
||||
assert a.now == 17
|
||||
|
||||
def test_agent_async(self):
|
||||
'''
|
||||
Async functions should also be valid states.
|
||||
'''
|
||||
|
||||
class TestAgent(agents.Agent):
|
||||
@agents.state(default=True)
|
||||
def one(self):
|
||||
return self.two
|
||||
|
||||
@agents.state
|
||||
def two(self):
|
||||
return self.three.at(10)
|
||||
|
||||
@agents.state
|
||||
def three(self):
|
||||
return self.four.delay(1)
|
||||
|
||||
@agents.state
|
||||
async def four(self):
|
||||
await self.delay(2)
|
||||
return self.five.delay(3)
|
||||
|
||||
@agents.state
|
||||
def five(self):
|
||||
return self.delay(1)
|
||||
|
||||
model = environment.Environment()
|
||||
a = model.add_agent(TestAgent)
|
||||
assert a.now == 0
|
||||
assert a.state_id == TestAgent.one.id
|
||||
model.step()
|
||||
assert a.now == 1
|
||||
assert a.state_id == TestAgent.two.id
|
||||
model.step()
|
||||
assert a.now == 10
|
||||
assert a.state_id == TestAgent.three.id
|
||||
model.step()
|
||||
assert a.state_id == TestAgent.four.id
|
||||
assert a.now == 11
|
||||
model.step()
|
||||
assert a.state_id == TestAgent.four.id
|
||||
assert a.now == 13
|
||||
model.step()
|
||||
assert a.state_id == TestAgent.five.id
|
||||
assert a.now == 16
|
||||
model.step()
|
||||
assert a.state_id == TestAgent.five.id
|
||||
assert a.now == 17
|
||||
|
||||
def test_agent_return_step(self):
|
||||
'''
|
||||
The same result as the previous test should be achievable by manually
|
||||
handling the agent state.
|
||||
'''
|
||||
class TestAgent(agents.Agent):
|
||||
my_state = 1
|
||||
my_count = 0
|
||||
|
||||
def step(self):
|
||||
if self.my_state == 1:
|
||||
self.my_state = 2
|
||||
return None
|
||||
elif self.my_state == 2:
|
||||
self.my_state = 3
|
||||
return self.at(10)
|
||||
elif self.my_state == 3:
|
||||
self.my_state = 4
|
||||
self.my_count = 0
|
||||
return self.delay(1)
|
||||
elif self.my_state == 4:
|
||||
self.my_count += 1
|
||||
if self.my_count == 1:
|
||||
return self.delay(2)
|
||||
self.my_state = 5
|
||||
return self.delay(3)
|
||||
elif self.my_state == 5:
|
||||
return self.delay(1)
|
||||
|
||||
model = environment.Environment()
|
||||
a = model.add_agent(TestAgent)
|
||||
assert a.my_state == 1
|
||||
assert a.now == 0
|
||||
model.step()
|
||||
assert a.now == 1
|
||||
assert a.my_state == 2
|
||||
model.step()
|
||||
assert a.now == 10
|
||||
assert a.my_state == 3
|
||||
model.step()
|
||||
assert a.now == 11
|
||||
assert a.my_state == 4
|
||||
model.step()
|
||||
assert a.now == 13
|
||||
assert a.my_state == 4
|
||||
model.step()
|
||||
assert a.now == 16
|
||||
assert a.my_state == 5
|
||||
model.step()
|
||||
assert a.now == 17
|
||||
assert a.my_state == 5
|
||||
|
||||
def test_agent_return_step_async(self):
|
||||
'''
|
||||
The same result as the previous test should be achievable by manually
|
||||
handling the agent state.
|
||||
'''
|
||||
class TestAgent(agents.Agent):
|
||||
my_state = 1
|
||||
|
||||
async def step(self):
|
||||
self.my_state = 2
|
||||
await self.delay()
|
||||
self.my_state = 3
|
||||
await self.at(10)
|
||||
self.my_state = 4
|
||||
await self.delay(1)
|
||||
await self.delay(2)
|
||||
self.my_state = 5
|
||||
await self.delay(3)
|
||||
while True:
|
||||
await self.delay(1)
|
||||
|
||||
model = environment.Environment()
|
||||
a = model.add_agent(TestAgent)
|
||||
assert a.my_state == 1
|
||||
assert a.now == 0
|
||||
model.step()
|
||||
assert a.now == 1
|
||||
assert a.my_state == 2
|
||||
model.step()
|
||||
assert a.now == 10
|
||||
assert a.my_state == 3
|
||||
model.step()
|
||||
assert a.now == 11
|
||||
assert a.my_state == 4
|
||||
model.step()
|
||||
assert a.now == 13
|
||||
assert a.my_state == 4
|
||||
model.step()
|
||||
assert a.now == 16
|
||||
assert a.my_state == 5
|
||||
model.step()
|
||||
assert a.now == 17
|
||||
assert a.my_state == 5
|
@ -29,15 +29,12 @@ class TestConfig(TestCase):
|
||||
def test_torvalds_config(self):
|
||||
sim = simulation.from_config(os.path.join(ROOT, "test_config.yml"))
|
||||
MAX_STEPS = 10
|
||||
INTERVAL = 2
|
||||
assert sim.interval == INTERVAL
|
||||
assert sim.max_steps == MAX_STEPS
|
||||
envs = sim.run()
|
||||
assert len(envs) == 1
|
||||
env = envs[0]
|
||||
assert env.interval == 2
|
||||
assert env.count_agents() == 3
|
||||
assert env.now == INTERVAL * MAX_STEPS
|
||||
assert env.now == MAX_STEPS
|
||||
|
||||
|
||||
def make_example_test(path, cfg):
|
||||
|
@ -1,5 +1,4 @@
|
||||
---
|
||||
source_file: "../examples/torvalds_sim.py"
|
||||
model: "TorvaldsEnv"
|
||||
max_steps: 10
|
||||
interval: 2
|
||||
max_steps: 10
|
@ -88,7 +88,7 @@ class Exporters(TestCase):
|
||||
parameters=dict(
|
||||
network_generator="complete_graph",
|
||||
network_params={"n": n_nodes},
|
||||
agent_class="CounterModel",
|
||||
agent_class=agents.CounterModel,
|
||||
agent_reporters={"times": "times"},
|
||||
),
|
||||
max_time=max_time,
|
||||
|
@ -7,8 +7,6 @@ from functools import partial
|
||||
|
||||
from os.path import join
|
||||
from soil import simulation, Environment, agents, network, serialization, utils, config, from_file
|
||||
from soil.time import Delta
|
||||
|
||||
from mesa import Agent as MesaAgent
|
||||
|
||||
ROOT = os.path.abspath(os.path.dirname(__file__))
|
||||
@ -114,7 +112,6 @@ class TestMain(TestCase):
|
||||
def test_serialize_class(self):
|
||||
ser, name = serialization.serialize(agents.BaseAgent, known_modules=[])
|
||||
assert name == "soil.agents.BaseAgent"
|
||||
assert ser == agents.BaseAgent
|
||||
|
||||
ser, name = serialization.serialize(
|
||||
agents.BaseAgent,
|
||||
@ -123,11 +120,9 @@ class TestMain(TestCase):
|
||||
],
|
||||
)
|
||||
assert name == "BaseAgent"
|
||||
assert ser == agents.BaseAgent
|
||||
|
||||
ser, name = serialization.serialize(CustomAgent)
|
||||
assert name == "test_main.CustomAgent"
|
||||
assert ser == CustomAgent
|
||||
pickle.dumps(ser)
|
||||
|
||||
def test_serialize_builtin_types(self):
|
||||
@ -168,7 +163,6 @@ class TestMain(TestCase):
|
||||
|
||||
def test_fsm(self):
|
||||
"""Basic state change"""
|
||||
|
||||
class ToggleAgent(agents.FSM):
|
||||
@agents.default_state
|
||||
@agents.state
|
||||
@ -193,7 +187,7 @@ class TestMain(TestCase):
|
||||
@agents.default_state
|
||||
@agents.state
|
||||
def ping(self):
|
||||
return self.pong, 2
|
||||
return self.pong.delay(2)
|
||||
|
||||
@agents.state
|
||||
def pong(self):
|
||||
@ -203,7 +197,7 @@ class TestMain(TestCase):
|
||||
when = a.step()
|
||||
assert when == 2
|
||||
when = a.step()
|
||||
assert when == Delta(a.interval)
|
||||
assert when == None
|
||||
|
||||
def test_load_sim(self):
|
||||
"""Make sure at least one of the examples can be loaded"""
|
||||
@ -232,4 +226,4 @@ class TestMain(TestCase):
|
||||
assert len(configs) == len(a) * len(b)
|
||||
for i in a:
|
||||
for j in b:
|
||||
assert {"a": i, "b": j} in configs
|
||||
assert {"a": i, "b": j} in configs
|
||||
|
@ -4,26 +4,6 @@ from soil import time, agents, environment
|
||||
|
||||
|
||||
class TestMain(TestCase):
|
||||
def test_cond(self):
|
||||
"""
|
||||
A condition should match a When if the concition is True
|
||||
"""
|
||||
|
||||
t = time.Cond(lambda t: True)
|
||||
f = time.Cond(lambda t: False)
|
||||
for i in range(10):
|
||||
w = time.When(i)
|
||||
assert w == t
|
||||
assert w is not f
|
||||
|
||||
def test_cond(self):
|
||||
"""
|
||||
Comparing a Cond to a Delta should always return False
|
||||
"""
|
||||
|
||||
c = time.Cond(lambda t: False)
|
||||
d = time.Delta(1)
|
||||
assert c is not d
|
||||
|
||||
def test_cond_env(self):
|
||||
""" """
|
||||
@ -36,11 +16,12 @@ class TestMain(TestCase):
|
||||
|
||||
class CondAgent(agents.BaseAgent):
|
||||
def step(self):
|
||||
nonlocal done
|
||||
nonlocal done, times_started, times_asleep, times_awakened
|
||||
times_started.append(self.now)
|
||||
while True:
|
||||
times_asleep.append(self.now)
|
||||
yield time.Cond(lambda agent: agent.now >= 10, delta=2)
|
||||
while self.now < 10:
|
||||
yield self.delay(2)
|
||||
times_awakened.append(self.now)
|
||||
if self.now >= 10:
|
||||
break
|
||||
@ -57,7 +38,6 @@ class TestMain(TestCase):
|
||||
assert times_started == [0]
|
||||
assert times_awakened == [10]
|
||||
assert done == [10]
|
||||
# The first time will produce the Cond.
|
||||
assert env.schedule.steps == 6
|
||||
assert len(times) == 6
|
||||
|
||||
@ -65,11 +45,10 @@ class TestMain(TestCase):
|
||||
times.append(env.now)
|
||||
env.step()
|
||||
|
||||
assert times == [0, 2, 4, 6, 8, 10, 11]
|
||||
assert times == [0, 2, 4, 6, 8, 10, 11, 12]
|
||||
assert env.schedule.time == 13
|
||||
assert times_started == [0, 11]
|
||||
assert times_awakened == [10]
|
||||
assert done == [10]
|
||||
# Once more to yield the cond, another one to continue
|
||||
assert env.schedule.steps == 7
|
||||
assert len(times) == 7
|
||||
assert times_started == [0, 11, 12]
|
||||
assert times_awakened == [10, 11, 12]
|
||||
assert done == [10, 11, 12]
|
||||
assert env.schedule.steps == 8
|
||||
assert len(times) == 8
|
||||
|
Loading…
Reference in New Issue
Block a user