mirror of
https://github.com/gsi-upm/soil
synced 2024-12-22 08:18:13 +00:00
Add rescheduling for received
This commit is contained in:
parent
ee0c4517cb
commit
189836408f
@ -11,6 +11,7 @@ def run_sim(model, **kwargs):
|
||||
dump=False,
|
||||
num_processes=1,
|
||||
parameters={'num_agents': NUM_AGENTS},
|
||||
seed="",
|
||||
max_steps=MAX_STEPS,
|
||||
iterations=NUM_ITERS)
|
||||
opts.update(kwargs)
|
||||
|
@ -8,7 +8,6 @@ class NoopAgent(Agent):
|
||||
self.num_calls = 0
|
||||
|
||||
def step(self):
|
||||
# import pdb;pdb.set_trace()
|
||||
self.num_calls += 1
|
||||
|
||||
|
||||
|
@ -10,7 +10,6 @@ class NoopAgent(Agent):
|
||||
self.num_calls = 0
|
||||
|
||||
def step(self):
|
||||
# import pdb;pdb.set_trace()
|
||||
self.num_calls += 1
|
||||
|
||||
|
||||
|
21
benchmarks/noop/soil_state.py
Normal file
21
benchmarks/noop/soil_state.py
Normal file
@ -0,0 +1,21 @@
|
||||
from soil import Agent, Environment, Simulation, state
|
||||
|
||||
|
||||
class NoopAgent(Agent):
|
||||
num_calls = 0
|
||||
|
||||
@state(default=True)
|
||||
def unique(self):
|
||||
self.num_calls += 1
|
||||
|
||||
class NoopEnvironment(Environment):
|
||||
num_agents = 100
|
||||
|
||||
def init(self):
|
||||
self.add_agents(NoopAgent, k=self.num_agents)
|
||||
self.add_agent_reporter("num_calls")
|
||||
|
||||
|
||||
from _config import *
|
||||
|
||||
run_sim(model=NoopEnvironment)
|
@ -1,7 +1,7 @@
|
||||
from soil import BaseAgent, Environment, Simulation
|
||||
from soil import Agent, Environment, Simulation
|
||||
|
||||
|
||||
class NoopAgent(BaseAgent):
|
||||
class NoopAgent(Agent):
|
||||
num_calls = 0
|
||||
|
||||
def step(self):
|
||||
@ -15,7 +15,6 @@ class NoopEnvironment(Environment):
|
||||
self.add_agent_reporter("num_calls")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from _config import *
|
||||
from _config import *
|
||||
|
||||
run_sim(model=NoopEnvironment)
|
||||
run_sim(model=NoopEnvironment)
|
||||
|
@ -1,5 +1,5 @@
|
||||
from soil import Agent, Environment, Simulation
|
||||
from soilent import Scheduler
|
||||
from soil.time import SoilentActivation
|
||||
|
||||
|
||||
class NoopAgent(Agent):
|
||||
@ -14,7 +14,7 @@ class NoopAgent(Agent):
|
||||
|
||||
class NoopEnvironment(Environment):
|
||||
num_agents = 100
|
||||
schedule_class = Scheduler
|
||||
schedule_class = SoilentActivation
|
||||
|
||||
def init(self):
|
||||
self.add_agents(NoopAgent, k=self.num_agents)
|
||||
@ -26,4 +26,4 @@ if __name__ == "__main__":
|
||||
|
||||
res = run_sim(model=NoopEnvironment)
|
||||
for r in res:
|
||||
assert isinstance(r.schedule, Scheduler)
|
||||
assert isinstance(r.schedule, SoilentActivation)
|
||||
|
@ -1,5 +1,5 @@
|
||||
from soil import Agent, Environment
|
||||
from soilent import PQueueScheduler
|
||||
from soil.time import SoilentPQueueActivation
|
||||
|
||||
|
||||
class NoopAgent(Agent):
|
||||
@ -12,7 +12,7 @@ class NoopAgent(Agent):
|
||||
|
||||
class NoopEnvironment(Environment):
|
||||
num_agents = 100
|
||||
schedule_class = PQueueScheduler
|
||||
schedule_class = SoilentPQueueActivation
|
||||
|
||||
def init(self):
|
||||
self.add_agents(NoopAgent, k=self.num_agents)
|
||||
@ -24,4 +24,4 @@ if __name__ == "__main__":
|
||||
|
||||
res = run_sim(model=NoopEnvironment)
|
||||
for r in res:
|
||||
assert isinstance(r.schedule, PQueueScheduler)
|
||||
assert isinstance(r.schedule, SoilentPQueueActivation)
|
||||
|
@ -1,5 +1,5 @@
|
||||
from soil import Agent, Environment, Simulation
|
||||
from soilent import Scheduler
|
||||
from soil.time import SoilentActivation
|
||||
|
||||
|
||||
class NoopAgent(Agent):
|
||||
@ -13,7 +13,7 @@ class NoopAgent(Agent):
|
||||
|
||||
class NoopEnvironment(Environment):
|
||||
num_agents = 100
|
||||
schedule_class = Scheduler
|
||||
schedule_class = SoilentActivation
|
||||
|
||||
def init(self):
|
||||
self.add_agents(NoopAgent, k=self.num_agents)
|
||||
@ -25,4 +25,4 @@ if __name__ == "__main__":
|
||||
|
||||
res = run_sim(model=NoopEnvironment)
|
||||
for r in res:
|
||||
assert isinstance(r.schedule, Scheduler)
|
||||
assert isinstance(r.schedule, SoilentActivation)
|
||||
|
@ -1,5 +1,5 @@
|
||||
from soil import Agent, Environment
|
||||
from soilent import PQueueScheduler
|
||||
from soil.time import SoilentPQueueActivation
|
||||
|
||||
|
||||
class NoopAgent(Agent):
|
||||
@ -13,7 +13,7 @@ class NoopAgent(Agent):
|
||||
|
||||
class NoopEnvironment(Environment):
|
||||
num_agents = 100
|
||||
schedule_class = PQueueScheduler
|
||||
schedule_class = SoilentPQueueActivation
|
||||
|
||||
def init(self):
|
||||
self.add_agents(NoopAgent, k=self.num_agents)
|
||||
@ -25,4 +25,4 @@ if __name__ == "__main__":
|
||||
|
||||
res = run_sim(model=NoopEnvironment)
|
||||
for r in res:
|
||||
assert isinstance(r.schedule, PQueueScheduler)
|
||||
assert isinstance(r.schedule, SoilentPQueueActivation)
|
||||
|
30
benchmarks/noop/soilent_state.py
Normal file
30
benchmarks/noop/soilent_state.py
Normal file
@ -0,0 +1,30 @@
|
||||
from soil import Agent, Environment, Simulation, state
|
||||
from soil.time import SoilentActivation
|
||||
|
||||
|
||||
class NoopAgent(Agent):
|
||||
num_calls = 0
|
||||
|
||||
@state(default=True)
|
||||
async def unique(self):
|
||||
while True:
|
||||
self.num_calls += 1
|
||||
# yield self.delay(1)
|
||||
await self.delay()
|
||||
|
||||
|
||||
class NoopEnvironment(Environment):
|
||||
num_agents = 100
|
||||
schedule_class = SoilentActivation
|
||||
|
||||
def init(self):
|
||||
self.add_agents(NoopAgent, k=self.num_agents)
|
||||
self.add_agent_reporter("num_calls")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from _config import *
|
||||
|
||||
res = run_sim(model=NoopEnvironment)
|
||||
for r in res:
|
||||
assert isinstance(r.schedule, SoilentActivation)
|
@ -1,5 +1,5 @@
|
||||
from soil import BaseAgent, Environment, Simulation
|
||||
from soilent import Scheduler
|
||||
from soil.time import SoilentActivation
|
||||
|
||||
|
||||
class NoopAgent(BaseAgent):
|
||||
@ -10,7 +10,7 @@ class NoopAgent(BaseAgent):
|
||||
|
||||
class NoopEnvironment(Environment):
|
||||
num_agents = 100
|
||||
schedule_class = Scheduler
|
||||
schedule_class = SoilentActivation
|
||||
|
||||
def init(self):
|
||||
self.add_agents(NoopAgent, k=self.num_agents)
|
||||
@ -21,4 +21,4 @@ if __name__ == "__main__":
|
||||
from _config import *
|
||||
res = run_sim(model=NoopEnvironment)
|
||||
for r in res:
|
||||
assert isinstance(r.schedule, Scheduler)
|
||||
assert isinstance(r.schedule, SoilentActivation)
|
||||
|
@ -1,5 +1,5 @@
|
||||
from soil import BaseAgent, Environment, Simulation
|
||||
from soilent import PQueueScheduler
|
||||
from soil.time import SoilentPQueueActivation
|
||||
|
||||
|
||||
class NoopAgent(BaseAgent):
|
||||
@ -10,7 +10,7 @@ class NoopAgent(BaseAgent):
|
||||
|
||||
class NoopEnvironment(Environment):
|
||||
num_agents = 100
|
||||
schedule_class = PQueueScheduler
|
||||
schedule_class = SoilentPQueueActivation
|
||||
|
||||
def init(self):
|
||||
self.add_agents(NoopAgent, k=self.num_agents)
|
||||
@ -21,4 +21,4 @@ if __name__ == "__main__":
|
||||
from _config import *
|
||||
res = run_sim(model=NoopEnvironment)
|
||||
for r in res:
|
||||
assert isinstance(r.schedule, PQueueScheduler)
|
||||
assert isinstance(r.schedule, SoilentPqueueActivation)
|
||||
|
@ -1,8 +1,9 @@
|
||||
import os
|
||||
from soil import simulation
|
||||
|
||||
NUM_AGENTS = int(os.environ.get('NUM_AGENTS', 100))
|
||||
NUM_ITERS = int(os.environ.get('NUM_ITERS', 10))
|
||||
MAX_STEPS = int(os.environ.get('MAX_STEPS', 1000))
|
||||
MAX_STEPS = int(os.environ.get('MAX_STEPS', 500))
|
||||
|
||||
|
||||
def run_sim(model, **kwargs):
|
||||
@ -22,11 +23,16 @@ def run_sim(model, **kwargs):
|
||||
iterations=NUM_ITERS)
|
||||
opts.update(kwargs)
|
||||
its = Simulation(**opts).run()
|
||||
assert len(its) == NUM_ITERS
|
||||
|
||||
assert all(it.schedule.steps == MAX_STEPS for it in its)
|
||||
ratios = list(it.resistant_susceptible_ratio() for it in its)
|
||||
print("Max - Avg - Min ratio:", max(ratios), sum(ratios)/len(ratios), min(ratios))
|
||||
assert all(sum([it.number_susceptible,
|
||||
it.number_infected,
|
||||
it.number_resistant]) == NUM_AGENTS for it in its)
|
||||
return its
|
||||
if not simulation._AVOID_RUNNING:
|
||||
ratios = list(it.resistant_susceptible_ratio for it in its)
|
||||
print("Max - Avg - Min ratio:", max(ratios), sum(ratios)/len(ratios), min(ratios))
|
||||
infected = list(it.number_infected for it in its)
|
||||
print("Max - Avg - Min infected:", max(infected), sum(infected)/len(infected), min(infected))
|
||||
|
||||
assert all((it.schedule.steps == MAX_STEPS or it.number_infected == 0) for it in its)
|
||||
assert all(sum([it.number_susceptible,
|
||||
it.number_infected,
|
||||
it.number_resistant]) == NUM_AGENTS for it in its)
|
||||
return its
|
||||
|
@ -100,6 +100,7 @@ class VirusOnNetwork(mesa.Model):
|
||||
def number_infected(self):
|
||||
return number_infected(self)
|
||||
|
||||
@property
|
||||
def resistant_susceptible_ratio(self):
|
||||
try:
|
||||
return number_state(self, State.RESISTANT) / number_state(
|
||||
@ -176,5 +177,4 @@ class VirusAgent(mesa.Agent):
|
||||
|
||||
|
||||
from _config import run_sim
|
||||
|
||||
run_sim(model=VirusOnNetwork)
|
@ -30,8 +30,12 @@ class VirusOnNetwork(Environment):
|
||||
for a in self.agents(node_id=infected_nodes):
|
||||
a.set_state(VirusAgent.infected)
|
||||
assert self.number_infected == self.initial_outbreak_size
|
||||
|
||||
def step(self):
|
||||
super().step()
|
||||
|
||||
@report
|
||||
@property
|
||||
def resistant_susceptible_ratio(self):
|
||||
try:
|
||||
return self.number_resistant / self.number_susceptible
|
||||
@ -59,34 +63,29 @@ class VirusAgent(Agent):
|
||||
virus_check_frequency = None # Inherit from model
|
||||
recovery_chance = None # Inherit from model
|
||||
gain_resistance_chance = None # Inherit from model
|
||||
just_been_infected = False
|
||||
|
||||
@state(default=True)
|
||||
def susceptible(self):
|
||||
if self.just_been_infected:
|
||||
self.just_been_infected = False
|
||||
return self.infected
|
||||
async def susceptible(self):
|
||||
await self.received()
|
||||
return self.infected
|
||||
|
||||
@state
|
||||
def infected(self):
|
||||
susceptible_neighbors = self.get_neighbors(state_id=self.susceptible.id)
|
||||
for a in susceptible_neighbors:
|
||||
if self.prob(self.virus_spread_chance):
|
||||
a.just_been_infected = True
|
||||
a.tell(True, sender=self)
|
||||
if self.prob(self.virus_check_frequency):
|
||||
if self.prob(self.recovery_chance):
|
||||
if self.prob(self.gain_resistance_chance):
|
||||
return self.resistant
|
||||
else:
|
||||
return self.susceptible
|
||||
else:
|
||||
return self.infected
|
||||
|
||||
@state
|
||||
def resistant(self):
|
||||
return self.at(INFINITY)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from _config import run_sim
|
||||
run_sim(model=VirusOnNetwork)
|
||||
from _config import run_sim
|
||||
run_sim(model=VirusOnNetwork)
|
@ -38,6 +38,7 @@ class VirusOnNetwork(Environment):
|
||||
assert self.number_infected == self.initial_outbreak_size
|
||||
|
||||
@report
|
||||
@property
|
||||
def resistant_susceptible_ratio(self):
|
||||
try:
|
||||
return self.number_resistant / self.number_susceptible
|
||||
@ -99,6 +100,5 @@ class VirusAgent(Agent):
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from _config import run_sim
|
||||
run_sim(model=VirusOnNetwork)
|
||||
from _config import run_sim
|
||||
run_sim(model=VirusOnNetwork)
|
File diff suppressed because one or more lines are too long
@ -167,7 +167,7 @@ class RandomAccident(BaseAgent):
|
||||
if self.prob(prob_death):
|
||||
self.debug("I killed a rabbit: {}".format(i.unique_id))
|
||||
num_alive -= 1
|
||||
i.die()
|
||||
self.model.remove_agent(i)
|
||||
self.debug("Rabbits alive: {}".format(num_alive))
|
||||
|
||||
|
||||
|
@ -142,13 +142,15 @@ class RandomAccident(BaseAgent):
|
||||
prob_death = min(1, self.prob_death * num_alive/10)
|
||||
self.debug("Killing some rabbits with prob={}!".format(prob_death))
|
||||
|
||||
for i in self.get_agents(agent_class=Rabbit):
|
||||
for i in alive:
|
||||
if i.state_id == i.dead.id:
|
||||
continue
|
||||
if self.prob(prob_death):
|
||||
self.debug("I killed a rabbit: {}".format(i.unique_id))
|
||||
num_alive -= 1
|
||||
i.die()
|
||||
self.model.remove_agent(i)
|
||||
i.alive = False
|
||||
i.killed = True
|
||||
self.debug("Rabbits alive: {}".format(num_alive))
|
||||
|
||||
|
||||
|
@ -259,7 +259,6 @@ def main(
|
||||
except Exception as ex:
|
||||
if args.pdb:
|
||||
from .debugging import post_mortem
|
||||
|
||||
print(traceback.format_exc())
|
||||
post_mortem()
|
||||
else:
|
||||
|
@ -30,8 +30,11 @@ 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, **kwargs):
|
||||
assert isinstance(unique_id, int)
|
||||
def __init__(self, unique_id=None, model=None, name=None, init=True, **kwargs):
|
||||
# Ideally, model should be the first argument, but Mesa's Agent class has unique_id first
|
||||
assert not (model is None), "Must provide a model"
|
||||
if unique_id is None:
|
||||
unique_id = model.next_id()
|
||||
super().__init__(unique_id=unique_id, model=model)
|
||||
|
||||
self.name = (
|
||||
@ -191,25 +194,25 @@ 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)
|
||||
|
||||
|
||||
class Noop(BaseAgent):
|
||||
def step(self):
|
||||
return
|
||||
|
||||
|
||||
from .network_agents import *
|
||||
from .fsm import *
|
||||
from .evented import *
|
||||
from .view import *
|
||||
|
||||
|
||||
class Noop(EventedAgent, BaseAgent):
|
||||
def step(self):
|
||||
return
|
||||
|
||||
|
||||
class Agent(FSM, EventedAgent, NetworkAgent):
|
||||
"""Default agent class, has network, FSM and event capabilities"""
|
||||
|
||||
|
@ -16,7 +16,7 @@ class EventedAgent(BaseAgent):
|
||||
self.model.register(self)
|
||||
|
||||
def received(self, **kwargs):
|
||||
return self.model.received(self, **kwargs)
|
||||
return self.model.received(agent=self, **kwargs)
|
||||
|
||||
def tell(self, msg, **kwargs):
|
||||
return self.model.tell(msg, recipient=self, **kwargs)
|
||||
|
@ -6,39 +6,38 @@ import inspect
|
||||
|
||||
|
||||
class State:
|
||||
__slots__ = ("awaitable", "f", "generator", "name", "default")
|
||||
__slots__ = ("awaitable", "f", "attribute", "generator", "name", "default")
|
||||
|
||||
def __init__(self, f, name, default, generator, awaitable):
|
||||
self.f = f
|
||||
self.name = name
|
||||
self.attribute = "_{}".format(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 __get__(self, obj, owner=None):
|
||||
if obj is None:
|
||||
return self
|
||||
try:
|
||||
return getattr(obj, self.attribute)
|
||||
except AttributeError:
|
||||
b = self.bind(obj)
|
||||
setattr(obj, self.attribute, b)
|
||||
return b
|
||||
|
||||
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
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
raise Exception("States should not be called directly")
|
||||
|
||||
|
||||
class BoundState(State):
|
||||
__slots__ = ("obj", )
|
||||
@ -46,10 +45,21 @@ class BoundState(State):
|
||||
def __init__(self, *args, obj):
|
||||
super().__init__(*args)
|
||||
self.obj = obj
|
||||
|
||||
|
||||
@coroutine
|
||||
def __call__(self):
|
||||
if self.generator or self.awaitable:
|
||||
f = self.f
|
||||
next_state = yield from f(self.obj)
|
||||
return next_state
|
||||
|
||||
else:
|
||||
return self.f(self.obj)
|
||||
|
||||
|
||||
def delay(self, delta=0):
|
||||
return self, self.obj.delay(delta)
|
||||
|
||||
|
||||
def at(self, when):
|
||||
return self, self.obj.at(when)
|
||||
|
||||
@ -63,7 +73,7 @@ def state(name=None, default=False):
|
||||
name = name or func.__name__
|
||||
generator = inspect.isgeneratorfunction(func)
|
||||
awaitable = inspect.iscoroutinefunction(func) or inspect.isasyncgen(func)
|
||||
return UnboundState(func, name, default, generator, awaitable)
|
||||
return State(func, name, default, generator, awaitable)
|
||||
|
||||
if callable(name):
|
||||
return decorator(name)
|
||||
@ -113,15 +123,24 @@ class MetaFSM(MetaAgent):
|
||||
class FSM(BaseAgent, metaclass=MetaFSM):
|
||||
def __init__(self, init=True, state_id=None, **kwargs):
|
||||
super().__init__(**kwargs, init=False)
|
||||
bound_states = {}
|
||||
for (k, v) in list(self._states.items()):
|
||||
if isinstance(v, State):
|
||||
v = v.bind(self)
|
||||
bound_states[k] = v
|
||||
setattr(self, k, v)
|
||||
|
||||
self._states = bound_states
|
||||
|
||||
if state_id is not None:
|
||||
self._set_state(state_id)
|
||||
else:
|
||||
self._set_state(self._state)
|
||||
# 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))
|
||||
|
||||
if init:
|
||||
self.init()
|
||||
@ -139,6 +158,7 @@ class FSM(BaseAgent, metaclass=MetaFSM):
|
||||
raise ValueError("Cannot change state after init")
|
||||
self._set_state(value)
|
||||
|
||||
@coroutine
|
||||
def step(self):
|
||||
if self._state is None:
|
||||
if len(self._states) == 1:
|
||||
@ -146,8 +166,7 @@ class FSM(BaseAgent, metaclass=MetaFSM):
|
||||
else:
|
||||
raise Exception("Invalid state (None) for agent {}".format(self))
|
||||
|
||||
self._check_alive()
|
||||
next_state = yield from self._state.step(self)
|
||||
next_state = yield from self._state()
|
||||
|
||||
try:
|
||||
next_state, when = next_state
|
||||
@ -167,7 +186,9 @@ class FSM(BaseAgent, metaclass=MetaFSM):
|
||||
if state not in self._states:
|
||||
raise ValueError("{} is not a valid state".format(state))
|
||||
state = self._states[state]
|
||||
if not isinstance(state, State):
|
||||
if isinstance(state, State):
|
||||
state = state.bind(self)
|
||||
elif not isinstance(state, BoundState):
|
||||
raise ValueError("{} is not a valid state".format(state))
|
||||
self._state = state
|
||||
|
||||
@ -177,4 +198,4 @@ class FSM(BaseAgent, metaclass=MetaFSM):
|
||||
|
||||
@state
|
||||
def dead(self):
|
||||
return time.INFINITY
|
||||
return time.INFINITY
|
||||
|
@ -2,44 +2,14 @@ from abc import ABCMeta
|
||||
from copy import copy
|
||||
from functools import wraps
|
||||
from .. import time
|
||||
from ..decorators import syncify, while_alive
|
||||
|
||||
import types
|
||||
import inspect
|
||||
|
||||
def decorate_generator_step(func, name):
|
||||
@wraps(func)
|
||||
def decorated(self):
|
||||
if not self.alive:
|
||||
return time.INFINITY
|
||||
|
||||
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_step(func, name):
|
||||
@wraps(func)
|
||||
def decorated(self):
|
||||
# if not self.alive:
|
||||
# return time.INFINITY
|
||||
val = func(self)
|
||||
return float(val) if val is not None else val
|
||||
return decorated
|
||||
|
||||
|
||||
class MetaAgent(ABCMeta):
|
||||
class MetaAnnotations(ABCMeta):
|
||||
"""This metaclass sets default values for agents based on class attributes"""
|
||||
def __new__(mcls, name, bases, namespace):
|
||||
defaults = {}
|
||||
|
||||
@ -53,22 +23,7 @@ class MetaAgent(ABCMeta):
|
||||
}
|
||||
|
||||
for attr, func in namespace.items():
|
||||
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_step(func, attr)
|
||||
else:
|
||||
raise ValueError("Illegal step function: {}".format(func))
|
||||
new_nmspc[attr] = func
|
||||
elif (
|
||||
if (
|
||||
isinstance(func, types.FunctionType)
|
||||
or isinstance(func, property)
|
||||
or isinstance(func, classmethod)
|
||||
@ -82,6 +37,28 @@ class MetaAgent(ABCMeta):
|
||||
else:
|
||||
defaults[attr] = copy(func)
|
||||
|
||||
return super().__new__(mcls, name, bases, new_nmspc)
|
||||
|
||||
|
||||
class AutoAgent(ABCMeta):
|
||||
def __new__(mcls, name, bases, namespace):
|
||||
if "step" in namespace:
|
||||
func = namespace["step"]
|
||||
namespace["_orig_step"] = func
|
||||
if inspect.isfunction(func):
|
||||
if inspect.isgeneratorfunction(func) or inspect.iscoroutinefunction(func):
|
||||
func = syncify(func, method=True)
|
||||
namespace["step"] = while_alive(func)
|
||||
elif inspect.isasyncgenfunction(func):
|
||||
raise ValueError("Illegal step function: {}. It probably mixes both async/await and yield".format(func))
|
||||
else:
|
||||
raise ValueError("Illegal step function: {}".format(func))
|
||||
|
||||
# Add attributes for their use in the decorated functions
|
||||
return super().__new__(mcls, name, bases, new_nmspc)
|
||||
return super().__new__(mcls, name, bases, namespace)
|
||||
|
||||
|
||||
class MetaAgent(AutoAgent, MetaAnnotations):
|
||||
"""This metaclass sets default values for agents based on class attributes"""
|
||||
pass
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
from collections.abc import Mapping, Set
|
||||
from itertools import islice
|
||||
from mesa import Agent
|
||||
|
||||
|
||||
class AgentView(Mapping, Set):
|
||||
@ -55,6 +56,8 @@ class AgentView(Mapping, Set):
|
||||
return list(self.filter(*args, **kwargs))
|
||||
|
||||
def __contains__(self, agent_id):
|
||||
if isinstance(agent_id, Agent):
|
||||
agent_id = agent_id.unique_id
|
||||
return agent_id in self._agents
|
||||
|
||||
def __str__(self):
|
||||
|
@ -19,7 +19,8 @@ def plot(env, agent_df=None, model_df=None, steps=False, ignore=["agent_count",
|
||||
try:
|
||||
agent_df = env.agent_df()
|
||||
except UserWarning:
|
||||
print("No agent dataframe provided and no agent reporters found. Skipping agent plot.", file=sys.stderr)
|
||||
print("No agent dataframe provided and no agent reporters found. "
|
||||
"Skipping agent plot.", file=sys.stderr)
|
||||
return
|
||||
if not agent_df.empty:
|
||||
agent_df.unstack().apply(lambda x: x.value_counts(),
|
||||
@ -48,9 +49,5 @@ def read_sql(fpath=None, name=None, include_agents=False):
|
||||
agents = pd.read_sql_table("agents", con=conn, index_col=["params_id", "iteration_id", "step", "agent_id"])
|
||||
config = pd.read_sql_table("configuration", con=conn, index_col="simulation_id")
|
||||
parameters = pd.read_sql_table("parameters", con=conn, index_col=["simulation_id", "params_id", "iteration_id"])
|
||||
# try:
|
||||
# parameters = parameters.pivot(columns="key", values="value")
|
||||
# except Exception as e:
|
||||
# print(f"warning: coult not pivot parameters: {e}")
|
||||
|
||||
return Results(config, parameters, env, agents)
|
||||
|
@ -1,6 +1,42 @@
|
||||
from functools import wraps
|
||||
from .time import INFINITY
|
||||
|
||||
def report(f: property):
|
||||
if isinstance(f, property):
|
||||
setattr(f.fget, "add_to_report", True)
|
||||
else:
|
||||
setattr(f, "add_to_report", True)
|
||||
return f
|
||||
return f
|
||||
|
||||
|
||||
def syncify(func, method=True):
|
||||
_coroutine = None
|
||||
|
||||
@wraps(func)
|
||||
def wrapped(*args, **kwargs):
|
||||
if not method:
|
||||
nonlocal _coroutine
|
||||
else:
|
||||
_coroutine = getattr(args[0], "_coroutine", None)
|
||||
_coroutine = _coroutine or func(*args, **kwargs)
|
||||
try:
|
||||
val = _coroutine.send(None)
|
||||
except StopIteration as ex:
|
||||
_coroutine = None
|
||||
val = ex.value
|
||||
finally:
|
||||
if method:
|
||||
args[0]._coroutine = _coroutine
|
||||
return val
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
def while_alive(func):
|
||||
@wraps(func)
|
||||
def wrapped(self, *args, **kwargs):
|
||||
if self.alive:
|
||||
return func(self, *args, **kwargs)
|
||||
return INFINITY
|
||||
|
||||
return wrapped
|
@ -11,6 +11,7 @@ import networkx as nx
|
||||
|
||||
|
||||
from mesa import Model
|
||||
from time import time as current_time
|
||||
|
||||
from . import agents as agentmod, datacollection, utils, time, network, events
|
||||
|
||||
@ -43,6 +44,7 @@ class BaseEnvironment(Model):
|
||||
tables: Optional[Any] = None,
|
||||
**kwargs: Any) -> Any:
|
||||
"""Create a new model with a default seed value"""
|
||||
seed = seed or str(current_time())
|
||||
self = super().__new__(cls, *args, seed=seed, **kwargs)
|
||||
self.dir_path = dir_path or os.getcwd()
|
||||
collector_class = collector_class or cls.collector_class
|
||||
@ -136,7 +138,7 @@ class BaseEnvironment(Model):
|
||||
|
||||
@property
|
||||
def now(self):
|
||||
if self.schedule:
|
||||
if self.schedule is not None:
|
||||
return self.schedule.time
|
||||
raise Exception(
|
||||
"The environment has not been scheduled, so it has no sense of time"
|
||||
@ -160,6 +162,10 @@ class BaseEnvironment(Model):
|
||||
self.schedule.add(a)
|
||||
return a
|
||||
|
||||
def remove_agent(self, agent):
|
||||
agent.alive = False
|
||||
self.schedule.remove(agent)
|
||||
|
||||
def add_agents(self, agent_classes: List[type], k, weights: Optional[List[float]] = None, **kwargs):
|
||||
if isinstance(agent_classes, type):
|
||||
agent_classes = [agent_classes]
|
||||
@ -188,12 +194,15 @@ class BaseEnvironment(Model):
|
||||
super().step()
|
||||
self.schedule.step()
|
||||
self.datacollector.collect(self)
|
||||
if self.now == time.INFINITY:
|
||||
self.running = False
|
||||
|
||||
if self.logger.isEnabledFor(logging.DEBUG):
|
||||
msg = "Model data:\n"
|
||||
max_width = max(len(k) for k in self.datacollector.model_vars.keys())
|
||||
for (k, v) in self.datacollector.model_vars.items():
|
||||
msg += f"\t{k:<{max_width}}: {v[-1]:>6}\n"
|
||||
# msg += f"\t{k:<{max_width}}"
|
||||
msg += f"\t{k:<{max_width}}: {v[-1]}\n"
|
||||
self.logger.debug(f"--- Steps: {self.schedule.steps:^5} - Time: {self.now:^5} --- " + msg)
|
||||
|
||||
def add_model_reporter(self, name, func=None):
|
||||
@ -297,6 +306,11 @@ class NetworkEnvironment(BaseEnvironment):
|
||||
self.G.nodes[node_id]["agent"] = a
|
||||
return a
|
||||
|
||||
def remove_agent(self, agent, remove_node=True):
|
||||
super().remove_agent(agent)
|
||||
if remove_node and hasattr(agent, "remove_node"):
|
||||
agent.remove_node()
|
||||
|
||||
def add_agents(self, *args, k=None, **kwargs):
|
||||
if not k and not self.G:
|
||||
raise ValueError("Cannot add agents to an empty network")
|
||||
@ -344,6 +358,7 @@ class NetworkEnvironment(BaseEnvironment):
|
||||
)
|
||||
if node_id is None:
|
||||
node_id = f"Node_for_agent_{unique_id}"
|
||||
assert node_id not in self.G.nodes
|
||||
|
||||
if node_id not in self.G.nodes:
|
||||
self.G.add_node(node_id)
|
||||
@ -417,7 +432,10 @@ class EventedEnvironment(BaseEnvironment):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._inbox = dict()
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._can_reschedule = hasattr(self.schedule, "add_callback") and hasattr(self.schedule, "remove_callback")
|
||||
self._can_reschedule = True
|
||||
self._callbacks = {}
|
||||
|
||||
def register(self, agent):
|
||||
self._inbox[agent.unique_id] = []
|
||||
|
||||
@ -429,24 +447,47 @@ class EventedEnvironment(BaseEnvironment):
|
||||
"Make sure your agent is of type EventedAgent and it is registered with the environment.")
|
||||
|
||||
@coroutine
|
||||
def received(self, agent, expiration=None, timeout=60, delay=1):
|
||||
if not expiration:
|
||||
expiration = self.now + timeout
|
||||
def _polling_callback(self, agent, expiration, delay):
|
||||
# this wakes the agent up at every step. It is better to wait until timeout (or inf)
|
||||
# and if a message is received before that, reschedule the agent
|
||||
# (That is implemented in the `received` method)
|
||||
inbox = self.inbox_for(agent)
|
||||
if inbox:
|
||||
return self.process_messages(inbox)
|
||||
while self.now < expiration:
|
||||
# TODO: this wakes the agent up at every step. It would be better to wait until timeout (or inf)
|
||||
# and if a message is received before that, reschedule the agent when
|
||||
if inbox:
|
||||
return self.process_messages(inbox)
|
||||
yield time.Delay(delay)
|
||||
raise events.TimedOut("No message received")
|
||||
|
||||
def tell(self, msg, sender, recipient, expiration=None, timeout=None, **kwargs):
|
||||
@coroutine
|
||||
def received(self, agent, expiration=None, timeout=None, delay=1):
|
||||
if not expiration:
|
||||
if timeout:
|
||||
expiration = self.now + timeout
|
||||
else:
|
||||
expiration = float("inf")
|
||||
inbox = self.inbox_for(agent)
|
||||
if inbox:
|
||||
return self.process_messages(inbox)
|
||||
|
||||
if self._can_reschedule:
|
||||
checked = False
|
||||
def cb():
|
||||
nonlocal checked
|
||||
if checked:
|
||||
return time.INFINITY
|
||||
checked = True
|
||||
self.schedule.add_callback(self.now, agent.step)
|
||||
self.schedule.add_callback(expiration, cb)
|
||||
self._callbacks[agent.unique_id] = cb
|
||||
yield time.INFINITY
|
||||
res = yield from self._polling_callback(agent, expiration, delay)
|
||||
return res
|
||||
|
||||
|
||||
def tell(self, msg, recipient, sender=None, expiration=None, timeout=None, **kwargs):
|
||||
if expiration is None:
|
||||
expiration = float("inf") if timeout is None else self.now + timeout
|
||||
self.inbox_for(recipient).append(
|
||||
self._add_to_inbox(recipient.unique_id,
|
||||
events.Tell(timestamp=self.now,
|
||||
payload=msg,
|
||||
sender=sender,
|
||||
@ -463,18 +504,23 @@ class EventedEnvironment(BaseEnvironment):
|
||||
if agent_class and not isinstance(self.agents(unique_id=agent_id), agent_class):
|
||||
continue
|
||||
self.logger.debug(f"Telling {agent_id}: {msg} ttl={ttl}")
|
||||
inbox.append(
|
||||
self._add_to_inbox(agent_id,
|
||||
events.Tell(
|
||||
payload=msg,
|
||||
sender=sender,
|
||||
expiration=expiration,
|
||||
)
|
||||
)
|
||||
def _add_to_inbox(self, inbox_id, msg):
|
||||
self._inbox[inbox_id].append(msg)
|
||||
if inbox_id in self._callbacks:
|
||||
cb = self._callbacks.pop(inbox_id)
|
||||
cb()
|
||||
|
||||
@coroutine
|
||||
def ask(self, msg, recipient, sender=None, expiration=None, timeout=None, delay=1):
|
||||
ask = events.Ask(timestamp=self.now, payload=msg, sender=sender)
|
||||
self.inbox_for(recipient).append(ask)
|
||||
self._add_to_inbox(recipient.unique_id, ask)
|
||||
expiration = float("inf") if timeout is None else self.now + timeout
|
||||
while self.now < expiration:
|
||||
if ask.reply:
|
||||
@ -493,4 +539,4 @@ class EventedEnvironment(BaseEnvironment):
|
||||
|
||||
|
||||
class Environment(EventedEnvironment, NetworkEnvironment):
|
||||
pass
|
||||
pass
|
||||
|
@ -75,6 +75,13 @@ class Exporter:
|
||||
def iteration_end(self, env, params, params_id):
|
||||
"""Method to call when a iteration ends"""
|
||||
pass
|
||||
|
||||
def env_id(self, env):
|
||||
try:
|
||||
return env.id
|
||||
except AttributeError:
|
||||
return f"{env.__class__.__name__}_{current_time()}"
|
||||
|
||||
|
||||
def output(self, f, mode="w", **kwargs):
|
||||
if not self.dump:
|
||||
@ -90,7 +97,7 @@ class Exporter:
|
||||
def get_dfs(self, env, params_id, **kwargs):
|
||||
yield from get_dc_dfs(env.datacollector,
|
||||
params_id,
|
||||
iteration_id=env.id,
|
||||
iteration_id=self.env_id(env),
|
||||
**kwargs)
|
||||
|
||||
|
||||
@ -157,11 +164,11 @@ class SQLite(Exporter):
|
||||
return
|
||||
|
||||
with timer(
|
||||
"Dumping simulation {} iteration {}".format(self.simulation.name, env.id)
|
||||
"Dumping simulation {} iteration {}".format(self.simulation.name, self.env_id(env))
|
||||
):
|
||||
d = {"simulation_id": self.simulation.id,
|
||||
"params_id": params_id,
|
||||
"iteration_id": env.id,
|
||||
"iteration_id": self.env_id(env),
|
||||
}
|
||||
for (k,v) in params.items():
|
||||
d[k] = serialize(v)[0]
|
||||
@ -173,7 +180,7 @@ class SQLite(Exporter):
|
||||
pd.DataFrame([{
|
||||
"simulation_id": self.simulation.id,
|
||||
"params_id": params_id,
|
||||
"iteration_id": env.id,
|
||||
"iteration_id": self.env_id(env),
|
||||
}]).reset_index().to_sql("iterations",
|
||||
con=self.engine,
|
||||
if_exists="append",
|
||||
@ -191,11 +198,11 @@ class csv(Exporter):
|
||||
def iteration_end(self, env, params, params_id, *args, **kwargs):
|
||||
with timer(
|
||||
"[CSV] Dumping simulation {} iteration {} @ dir {}".format(
|
||||
self.simulation.name, env.id, self.outdir
|
||||
self.simulation.name, self.env_id(env), self.outdir
|
||||
)
|
||||
):
|
||||
for (df_name, df) in self.get_dfs(env, params_id=params_id):
|
||||
with self.output("{}.{}.csv".format(env.id, df_name), mode="a") as f:
|
||||
with self.output("{}.{}.csv".format(self.env_id(env), df_name), mode="a") as f:
|
||||
df.to_csv(f)
|
||||
|
||||
|
||||
@ -206,9 +213,9 @@ class gexf(Exporter):
|
||||
return
|
||||
|
||||
with timer(
|
||||
"[GEXF] Dumping simulation {} iteration {}".format(self.simulation.name, env.id)
|
||||
"[GEXF] Dumping simulation {} iteration {}".format(self.simulation.name, self.env_id(env))
|
||||
):
|
||||
with self.output("{}.gexf".format(env.id), mode="wb") as f:
|
||||
with self.output("{}.gexf".format(self.env_id(env)), mode="wb") as f:
|
||||
nx.write_gexf(env.G, f)
|
||||
|
||||
|
||||
@ -242,7 +249,7 @@ class graphdrawing(Exporter):
|
||||
pos=nx.spring_layout(env.G, scale=100),
|
||||
ax=f.add_subplot(111),
|
||||
)
|
||||
with open("graph-{}.png".format(env.id)) as f:
|
||||
with open("graph-{}.png".format(self.env_id(env))) as f:
|
||||
f.savefig(f)
|
||||
|
||||
|
||||
|
@ -44,8 +44,8 @@ def do_not_run():
|
||||
|
||||
def _iter_queued():
|
||||
while _QUEUED:
|
||||
(cls, params) = _QUEUED.pop(0)
|
||||
yield replace(cls, parameters=params)
|
||||
slf = _QUEUED.pop(0)
|
||||
yield slf
|
||||
|
||||
|
||||
# TODO: change documentation for simulation
|
||||
@ -130,11 +130,11 @@ class Simulation:
|
||||
def run(self, **kwargs):
|
||||
"""Run the simulation and return the list of resulting environments"""
|
||||
if kwargs:
|
||||
return replace(self, **kwargs).run()
|
||||
res = replace(self, **kwargs)
|
||||
return res.run()
|
||||
|
||||
param_combinations = self._collect_params(**kwargs)
|
||||
if _AVOID_RUNNING:
|
||||
_QUEUED.extend((self, param) for param in param_combinations)
|
||||
_QUEUED.append(self)
|
||||
return []
|
||||
|
||||
self.logger.debug("Using exporters: %s", self.exporters or [])
|
||||
@ -154,6 +154,8 @@ class Simulation:
|
||||
for exporter in exporters:
|
||||
exporter.sim_start()
|
||||
|
||||
param_combinations = self._collect_params(**kwargs)
|
||||
|
||||
for params in tqdm(param_combinations, desc=self.name, unit="configuration"):
|
||||
for (k, v) in params.items():
|
||||
tqdm.write(f"{k} = {v}")
|
||||
@ -204,6 +206,7 @@ class Simulation:
|
||||
for env in tqdm(utils.run_parallel(
|
||||
func=func,
|
||||
iterable=range(self.iterations),
|
||||
num_processes=self.num_processes,
|
||||
**params,
|
||||
), total=self.iterations, leave=False):
|
||||
if env is None and self.dry_run:
|
||||
@ -338,12 +341,13 @@ def iter_from_py(pyfile, module_name='imported_file', **kwargs):
|
||||
sims.append(sim)
|
||||
for sim in _iter_queued():
|
||||
sims.append(sim)
|
||||
# Try to find environments to run, because we did not import a script that ran simulations
|
||||
if not sims:
|
||||
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))
|
||||
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:
|
||||
|
53
soil/time.py
53
soil/time.py
@ -24,7 +24,10 @@ class Delay:
|
||||
|
||||
def __float__(self):
|
||||
return self.delta
|
||||
|
||||
|
||||
def __eq__(self, other):
|
||||
return float(self) == float(other)
|
||||
|
||||
def __await__(self):
|
||||
return (yield self.delta)
|
||||
|
||||
@ -87,6 +90,9 @@ class PQueueSchedule:
|
||||
del self._queue[i]
|
||||
break
|
||||
|
||||
def __len__(self):
|
||||
return len(self._queue)
|
||||
|
||||
def step(self) -> None:
|
||||
"""
|
||||
Executes events in order, one at a time. After each step,
|
||||
@ -107,7 +113,8 @@ class PQueueSchedule:
|
||||
next_time = when
|
||||
break
|
||||
|
||||
when = event.func() or 1
|
||||
when = event.func()
|
||||
when = float(when) if when is not None else 1.0
|
||||
|
||||
if when == INFINITY:
|
||||
heappop(self._queue)
|
||||
@ -153,12 +160,18 @@ class Schedule:
|
||||
return lst
|
||||
|
||||
def insert(self, when, func, replace=False):
|
||||
if when == INFINITY:
|
||||
return
|
||||
lst = self._find_loc(when)
|
||||
lst.append(func)
|
||||
|
||||
def add_bulk(self, funcs, when=None):
|
||||
lst = self._find_loc(when)
|
||||
n = len(funcs)
|
||||
#TODO: remove for performance
|
||||
before = len(self)
|
||||
lst.extend(funcs)
|
||||
assert len(self) == before + n
|
||||
|
||||
def remove(self, func):
|
||||
for bucket in self._queue:
|
||||
@ -167,6 +180,9 @@ class Schedule:
|
||||
bucket.remove(ix)
|
||||
return
|
||||
|
||||
def __len__(self):
|
||||
return sum(len(bucket[1]) for bucket in self._queue)
|
||||
|
||||
def step(self) -> None:
|
||||
"""
|
||||
Executes events in order, one at a time. After each step,
|
||||
@ -188,11 +204,14 @@ class Schedule:
|
||||
self.random.shuffle(bucket)
|
||||
next_batch = defaultdict(list)
|
||||
for func in bucket:
|
||||
when = func() or 1
|
||||
when = func()
|
||||
when = float(when) if when is not None else 1
|
||||
|
||||
if when != INFINITY:
|
||||
when += now
|
||||
next_batch[when].append(func)
|
||||
if when == INFINITY:
|
||||
continue
|
||||
|
||||
when += now
|
||||
next_batch[when].append(func)
|
||||
|
||||
for (when, bucket) in next_batch.items():
|
||||
self.add_bulk(bucket, when)
|
||||
@ -229,6 +248,12 @@ class InnerActivation(BaseScheduler):
|
||||
self.agents_by_type[agent_class][agent.unique_id] = agent
|
||||
super().add(agent)
|
||||
|
||||
def add_callback(self, when, cb):
|
||||
self.inner.insert(when, cb)
|
||||
|
||||
def remove_callback(self, when, cb):
|
||||
self.inner.remove(cb)
|
||||
|
||||
def remove(self, agent):
|
||||
del self._agents[agent.unique_id]
|
||||
del self.agents_by_type[type(agent)][agent.unique_id]
|
||||
@ -241,6 +266,9 @@ class InnerActivation(BaseScheduler):
|
||||
"""
|
||||
self.inner.step()
|
||||
|
||||
def __len__(self):
|
||||
return len(self.inner)
|
||||
|
||||
|
||||
class BucketTimedActivation(InnerActivation):
|
||||
inner_class = Schedule
|
||||
@ -250,16 +278,19 @@ class PQueueActivation(InnerActivation):
|
||||
inner_class = PQueueSchedule
|
||||
|
||||
|
||||
# Set the bucket implementation as default
|
||||
#Set the bucket implementation as default
|
||||
TimedActivation = BucketTimedActivation
|
||||
|
||||
try:
|
||||
from soilent.soilent import BucketScheduler
|
||||
from soilent.soilent import BucketScheduler, PQueueScheduler
|
||||
|
||||
class SoilBucketActivation(InnerActivation):
|
||||
class SoilentActivation(InnerActivation):
|
||||
inner_class = BucketScheduler
|
||||
class SoilentPQueueActivation(InnerActivation):
|
||||
inner_class = PQueueScheduler
|
||||
|
||||
TimedActivation = SoilBucketActivation
|
||||
# TimedActivation = SoilentBucketActivation
|
||||
except ImportError:
|
||||
TimedActivation = BucketTimedActivation
|
||||
pass
|
||||
|
||||
|
||||
|
@ -93,15 +93,12 @@ def flatten_dict(d):
|
||||
|
||||
def _flatten_dict(d, prefix=""):
|
||||
if not isinstance(d, dict):
|
||||
# print('END:', prefix, d)
|
||||
yield prefix, d
|
||||
return
|
||||
if prefix:
|
||||
prefix = prefix + "."
|
||||
for k, v in d.items():
|
||||
# print(k, v)
|
||||
res = list(_flatten_dict(v, prefix="{}{}".format(prefix, k)))
|
||||
# print('RES:', res)
|
||||
yield from res
|
||||
|
||||
|
||||
@ -142,6 +139,7 @@ def run_and_return_exceptions(func, *args, **kwargs):
|
||||
|
||||
def run_parallel(func, iterable, num_processes=1, **kwargs):
|
||||
if num_processes > 1 and not os.environ.get("SOIL_DEBUG", None):
|
||||
logger.info("Running simulations in {} processes".format(num_processes))
|
||||
if num_processes < 1:
|
||||
num_processes = cpu_count() - num_processes
|
||||
p = Pool(processes=num_processes)
|
||||
|
@ -1,7 +1,7 @@
|
||||
from unittest import TestCase
|
||||
import pytest
|
||||
|
||||
from soil import agents, environment
|
||||
from soil import agents, events, environment
|
||||
from soil import time as stime
|
||||
|
||||
|
||||
@ -25,7 +25,7 @@ class TestAgents(TestCase):
|
||||
assert d.alive
|
||||
d.step()
|
||||
assert not d.alive
|
||||
when = d.step()
|
||||
when = float(d.step())
|
||||
assert not d.alive
|
||||
assert when == stime.INFINITY
|
||||
|
||||
@ -63,6 +63,7 @@ class TestAgents(TestCase):
|
||||
def other(self):
|
||||
self.times_run += 1
|
||||
|
||||
assert MyAgent.other.id == "other"
|
||||
e = environment.Environment()
|
||||
a = e.add_agent(MyAgent)
|
||||
e.step()
|
||||
@ -73,6 +74,53 @@ class TestAgents(TestCase):
|
||||
a.step()
|
||||
assert a.times_run == 2
|
||||
|
||||
def test_state_decorator_multiple(self):
|
||||
class MyAgent(agents.FSM):
|
||||
times_run = 0
|
||||
|
||||
@agents.state(default=True)
|
||||
def one(self):
|
||||
return self.two
|
||||
|
||||
@agents.state
|
||||
def two(self):
|
||||
return self.one
|
||||
|
||||
e = environment.Environment()
|
||||
first = e.add_agent(MyAgent, state_id=MyAgent.one)
|
||||
second = e.add_agent(MyAgent, state_id=MyAgent.two)
|
||||
assert first.state_id == MyAgent.one.id
|
||||
assert second.state_id == MyAgent.two.id
|
||||
e.step()
|
||||
assert first.state_id == MyAgent.two.id
|
||||
assert second.state_id == MyAgent.one.id
|
||||
|
||||
def test_state_decorator_multiple_async(self):
|
||||
class MyAgent(agents.FSM):
|
||||
times_run = 0
|
||||
|
||||
@agents.state(default=True)
|
||||
def one(self):
|
||||
yield self.delay(1)
|
||||
return self.two
|
||||
|
||||
@agents.state
|
||||
def two(self):
|
||||
yield self.delay(1)
|
||||
return self.one
|
||||
|
||||
e = environment.Environment()
|
||||
first = e.add_agent(MyAgent, state_id=MyAgent.one)
|
||||
second = e.add_agent(MyAgent, state_id=MyAgent.two)
|
||||
for i in range(2):
|
||||
assert first.state_id == MyAgent.one.id
|
||||
assert second.state_id == MyAgent.two.id
|
||||
e.step()
|
||||
for i in range(2):
|
||||
assert first.state_id == MyAgent.two.id
|
||||
assert second.state_id == MyAgent.one.id
|
||||
e.step()
|
||||
|
||||
def test_broadcast(self):
|
||||
"""
|
||||
An agent should be able to broadcast messages to every other agent, AND each receiver should be able
|
||||
@ -372,22 +420,105 @@ class TestAgents(TestCase):
|
||||
assert a.now == 17
|
||||
assert a.my_state == 5
|
||||
|
||||
def test_send_nonevent(self):
|
||||
def test_receive(self):
|
||||
'''
|
||||
Sending a non-event should raise an error.
|
||||
An agent should be able to receive a message after waiting
|
||||
'''
|
||||
model = environment.Environment()
|
||||
a = model.add_agent(agents.Noop)
|
||||
class TestAgent(agents.Agent):
|
||||
sent = False
|
||||
woken = 0
|
||||
def step(self):
|
||||
self.woken += 1
|
||||
return super().step()
|
||||
|
||||
@agents.state(default=True)
|
||||
async def one(self):
|
||||
try:
|
||||
self.sent = await self.received(timeout=15)
|
||||
return self.two.at(20)
|
||||
except events.TimedOut:
|
||||
pass
|
||||
@agents.state
|
||||
def two(self):
|
||||
return self.die()
|
||||
|
||||
a = model.add_agent(TestAgent)
|
||||
|
||||
class Sender(agents.Agent):
|
||||
async def step(self):
|
||||
await self.delay(10)
|
||||
a.tell(1)
|
||||
return stime.INFINITY
|
||||
|
||||
b = model.add_agent(Sender)
|
||||
|
||||
# Start and wait
|
||||
model.step()
|
||||
assert model.now == 10
|
||||
assert a.woken == 1
|
||||
assert not a.sent
|
||||
|
||||
# Sending the message
|
||||
model.step()
|
||||
assert model.now == 10
|
||||
assert a.woken == 1
|
||||
assert not a.sent
|
||||
|
||||
# The receiver callback
|
||||
model.step()
|
||||
assert model.now == 15
|
||||
assert a.woken == 2
|
||||
assert a.sent[0].payload == 1
|
||||
|
||||
# The timeout
|
||||
model.step()
|
||||
assert model.now == 20
|
||||
assert a.woken == 2
|
||||
|
||||
# The last state of the agent
|
||||
model.step()
|
||||
assert a.woken == 3
|
||||
assert model.now == float('inf')
|
||||
|
||||
def test_receive_timeout(self):
|
||||
'''
|
||||
A timeout should be raised if no messages are received after an expiration time
|
||||
'''
|
||||
model = environment.Environment()
|
||||
timedout = False
|
||||
class TestAgent(agents.Agent):
|
||||
@agents.state(default=True)
|
||||
def one(self):
|
||||
try:
|
||||
a.tell(b, 1)
|
||||
yield from self.received(timeout=10)
|
||||
raise AssertionError('Should have raised an error.')
|
||||
except AttributeError:
|
||||
self.model.tell(1, sender=self, recipient=a)
|
||||
except events.TimedOut:
|
||||
nonlocal timedout
|
||||
timedout = True
|
||||
|
||||
model.add_agent(TestAgent)
|
||||
a = model.add_agent(TestAgent)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
model.step()
|
||||
model.step()
|
||||
assert model.now == 10
|
||||
model.step()
|
||||
# Wake up the callback
|
||||
assert model.now == 10
|
||||
assert not timedout
|
||||
# The actual timeout
|
||||
model.step()
|
||||
assert model.now == 11
|
||||
assert timedout
|
||||
|
||||
def test_attributes(self):
|
||||
"""Attributes should be individual per agent"""
|
||||
|
||||
class MyAgent(agents.Agent):
|
||||
my_attribute = 0
|
||||
|
||||
model = environment.Environment()
|
||||
a = MyAgent(model=model)
|
||||
assert a.my_attribute == 0
|
||||
b = MyAgent(model=model, my_attribute=1)
|
||||
assert b.my_attribute == 1
|
||||
assert a.my_attribute == 0
|
||||
|
@ -6,7 +6,7 @@ import networkx as nx
|
||||
from functools import partial
|
||||
|
||||
from os.path import join
|
||||
from soil import simulation, Environment, agents, network, serialization, utils, config, from_file
|
||||
from soil import simulation, Environment, agents, serialization, from_file, time
|
||||
from mesa import Agent as MesaAgent
|
||||
|
||||
ROOT = os.path.abspath(os.path.dirname(__file__))
|
||||
@ -194,7 +194,7 @@ class TestMain(TestCase):
|
||||
return self.ping
|
||||
|
||||
a = ToggleAgent(unique_id=1, model=Environment())
|
||||
when = a.step()
|
||||
when = float(a.step())
|
||||
assert when == 2
|
||||
when = a.step()
|
||||
assert when == None
|
||||
@ -252,4 +252,34 @@ class TestMain(TestCase):
|
||||
assert df["base"][(0,0)] == "base"
|
||||
assert df["base"][(0,1)] == "base"
|
||||
assert df["subclass"][(0,0)] is None
|
||||
assert df["subclass"][(0,1)] == "subclass"
|
||||
assert df["subclass"][(0,1)] == "subclass"
|
||||
|
||||
def test_remove_agent(self):
|
||||
"""An agent that is scheduled should be removed from the schedule"""
|
||||
model = Environment()
|
||||
model.add_agent(agents.Noop)
|
||||
model.step()
|
||||
model.remove_agent(model.agents[0])
|
||||
assert not model.agents
|
||||
when = model.step()
|
||||
assert when == None
|
||||
assert not model.running
|
||||
|
||||
def test_remove_agent(self):
|
||||
"""An agent that is scheduled should be removed from the schedule"""
|
||||
|
||||
allagents = []
|
||||
class Removed(agents.BaseAgent):
|
||||
def step(self):
|
||||
nonlocal allagents
|
||||
assert self.alive
|
||||
assert self in self.model.agents
|
||||
for agent in allagents:
|
||||
self.model.remove_agent(agent)
|
||||
|
||||
model = Environment()
|
||||
a1 = model.add_agent(Removed)
|
||||
a2 = model.add_agent(Removed)
|
||||
allagents = [a1, a2]
|
||||
model.step()
|
||||
assert not model.agents
|
Loading…
Reference in New Issue
Block a user