mirror of
https://github.com/gsi-upm/soil
synced 2025-01-06 23:01:27 +00:00
Compare commits
6 Commits
2f5e5d0a74
...
d3cee18635
Author | SHA1 | Date | |
---|---|---|---|
|
d3cee18635 | ||
|
9a7b62e88e | ||
|
c09e480d37 | ||
|
b2d48cb4df | ||
|
a1262edd2a | ||
|
cbbaf73538 |
@ -127,7 +127,8 @@ class Driver(Evented, FSM):
|
||||
)
|
||||
|
||||
self.check_passengers()
|
||||
self.check_messages() # This will call on_receive behind the scenes, and the agent's status will be updated
|
||||
# This will call on_receive behind the scenes, and the agent's status will be updated
|
||||
self.check_messages()
|
||||
yield Delta(30) # Wait at least 30 seconds before checking again
|
||||
|
||||
try:
|
||||
@ -204,6 +205,8 @@ class Passenger(Evented, FSM):
|
||||
while not self.journey:
|
||||
self.info(f"Passenger at: { self.pos }. Checking for responses.")
|
||||
try:
|
||||
# This will call check_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)
|
||||
except events.TimedOut:
|
||||
self.info(f"Passenger at: { self.pos }. Asking for journey.")
|
||||
@ -211,7 +214,6 @@ class Passenger(Evented, FSM):
|
||||
journey, ttl=timeout, sender=self, agent_class=Driver
|
||||
)
|
||||
expiration = self.now + timeout
|
||||
self.check_messages()
|
||||
return self.driving_home
|
||||
|
||||
@state
|
||||
@ -220,13 +222,20 @@ class Passenger(Evented, FSM):
|
||||
self.pos[0] != self.journey.destination[0]
|
||||
or self.pos[1] != self.journey.destination[1]
|
||||
):
|
||||
yield self.received(timeout=60)
|
||||
try:
|
||||
yield self.received(timeout=60)
|
||||
except events.TimedOut:
|
||||
pass
|
||||
|
||||
self.info("Got home safe!")
|
||||
self.die()
|
||||
|
||||
|
||||
simulation = Simulation(
|
||||
name="RideHailing", model_class=City, model_params={"n_passengers": 2}
|
||||
name="RideHailing",
|
||||
model_class=City,
|
||||
model_params={"n_passengers": 2},
|
||||
seed="carsSeed",
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
@ -133,7 +133,7 @@ class RandomAccident(BaseAgent):
|
||||
math.log10(max(1, rabbits_alive))
|
||||
)
|
||||
self.debug("Killing some rabbits with prob={}!".format(prob_death))
|
||||
for i in self.iter_agents(agent_class=Rabbit):
|
||||
for i in self.get_agents(agent_class=Rabbit):
|
||||
if i.state_id == i.dead.id:
|
||||
continue
|
||||
if self.prob(prob_death):
|
||||
|
@ -258,9 +258,7 @@ class TerroristNetworkModel(TerroristSpreadModel):
|
||||
)
|
||||
neighbours = set(
|
||||
agent.id
|
||||
for agent in self.get_neighbors(
|
||||
agent_class=TerroristNetworkModel
|
||||
)
|
||||
for agent in self.get_neighbors(agent_class=TerroristNetworkModel)
|
||||
)
|
||||
search = (close_ups | step_neighbours) - neighbours
|
||||
for agent in self.get_agents(search):
|
||||
|
@ -1 +1 @@
|
||||
0.30.0rc2
|
||||
0.30.0rc3
|
@ -47,7 +47,7 @@ def main(
|
||||
"file",
|
||||
type=str,
|
||||
nargs="?",
|
||||
default=cfg if sim is None else '',
|
||||
default=cfg if sim is None else "",
|
||||
help="Configuration file for the simulation (e.g., YAML or JSON)",
|
||||
)
|
||||
parser.add_argument(
|
||||
@ -169,22 +169,26 @@ def main(
|
||||
sim.exporters = exporters
|
||||
sim.parallel = parallel
|
||||
sim.outdir = output
|
||||
sims = [sim, ]
|
||||
sims = [
|
||||
sim,
|
||||
]
|
||||
else:
|
||||
logger.info("Loading config file: {}".format(args.file))
|
||||
if not os.path.exists(args.file):
|
||||
logger.error("Please, input a valid file")
|
||||
return
|
||||
|
||||
sims = list(simulation.iter_from_config(
|
||||
args.file,
|
||||
dry_run=args.dry_run,
|
||||
exporters=exporters,
|
||||
parallel=parallel,
|
||||
outdir=output,
|
||||
exporter_params=exp_params,
|
||||
**kwargs,
|
||||
))
|
||||
sims = list(
|
||||
simulation.iter_from_config(
|
||||
args.file,
|
||||
dry_run=args.dry_run,
|
||||
exporters=exporters,
|
||||
parallel=parallel,
|
||||
outdir=output,
|
||||
exporter_params=exp_params,
|
||||
**kwargs,
|
||||
)
|
||||
)
|
||||
|
||||
for sim in sims:
|
||||
|
||||
|
@ -20,12 +20,6 @@ from typing import Dict, List
|
||||
from .. import serialization, utils, time, config
|
||||
|
||||
|
||||
def as_node(agent):
|
||||
if isinstance(agent, BaseAgent):
|
||||
return agent.id
|
||||
return agent
|
||||
|
||||
|
||||
IGNORED_FIELDS = ("model", "logger")
|
||||
|
||||
|
||||
@ -97,10 +91,6 @@ class BaseAgent(MesaAgent, MutableMapping, metaclass=MetaAgent):
|
||||
"""
|
||||
|
||||
def __init__(self, unique_id, model, name=None, interval=None, **kwargs):
|
||||
# Check for REQUIRED arguments
|
||||
# Initialize agent parameters
|
||||
if isinstance(unique_id, MesaAgent):
|
||||
raise Exception()
|
||||
assert isinstance(unique_id, int)
|
||||
super().__init__(unique_id=unique_id, model=model)
|
||||
|
||||
@ -207,7 +197,8 @@ class BaseAgent(MesaAgent, MutableMapping, metaclass=MetaAgent):
|
||||
def step(self):
|
||||
if not self.alive:
|
||||
raise time.DeadAgent(self.unique_id)
|
||||
return super().step() or time.Delta(self.interval)
|
||||
super().step()
|
||||
return time.Delta(self.interval)
|
||||
|
||||
def log(self, message, *args, level=logging.INFO, **kwargs):
|
||||
if not self.logger.isEnabledFor(level):
|
||||
@ -414,7 +405,7 @@ def filter_agents(
|
||||
if ids:
|
||||
f = (agents[aid] for aid in ids if aid in agents)
|
||||
else:
|
||||
f = (a for a in agents.values())
|
||||
f = agents.values()
|
||||
|
||||
if state_id is not None and not isinstance(state_id, (tuple, list)):
|
||||
state_id = tuple([state_id])
|
||||
@ -638,6 +629,11 @@ from .SentimentCorrelationModel import *
|
||||
from .SISaModel import *
|
||||
from .CounterModel import *
|
||||
|
||||
|
||||
class Agent(NetworkAgent, EventedAgent):
|
||||
"""Default agent class, has both network and event capabilities"""
|
||||
|
||||
|
||||
try:
|
||||
import scipy
|
||||
from .Geo import Geo
|
||||
|
@ -1,57 +1,77 @@
|
||||
from . import BaseAgent
|
||||
from ..events import Message, Tell, Ask, Reply, TimedOut
|
||||
from ..time import Cond
|
||||
from ..events import Message, Tell, Ask, TimedOut
|
||||
from ..time import BaseCond
|
||||
from functools import partial
|
||||
from collections import deque
|
||||
|
||||
|
||||
class Evented(BaseAgent):
|
||||
class ReceivedOrTimeout(BaseCond):
|
||||
def __init__(
|
||||
self, agent, expiration=None, timeout=None, check=True, ignore=False, **kwargs
|
||||
):
|
||||
if expiration is None:
|
||||
if timeout is not None:
|
||||
expiration = agent.now + timeout
|
||||
self.expiration = expiration
|
||||
self.ignore = ignore
|
||||
self.check = check
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def expired(self, time):
|
||||
return self.expiration and self.expiration < time
|
||||
|
||||
def ready(self, agent, time):
|
||||
return len(agent._inbox) or self.expired(time)
|
||||
|
||||
def return_value(self, agent):
|
||||
if not self.ignore and self.expired(agent.now):
|
||||
raise TimedOut("No messages received")
|
||||
if self.check:
|
||||
agent.check_messages()
|
||||
return None
|
||||
|
||||
def schedule_next(self, time, delta, first=False):
|
||||
if self._delta is not None:
|
||||
delta = self._delta
|
||||
return (time + delta, self)
|
||||
|
||||
def __repr__(self):
|
||||
return f"ReceivedOrTimeout(expires={self.expiration})"
|
||||
|
||||
|
||||
class EventedAgent(BaseAgent):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._inbox = deque()
|
||||
self._received = 0
|
||||
self._processed = 0
|
||||
|
||||
|
||||
def on_receive(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def received(self, expiration=None, timeout=None):
|
||||
current = self._received
|
||||
if expiration is None:
|
||||
expiration = float('inf') if timeout is None else self.now + timeout
|
||||
def received(self, *args, **kwargs):
|
||||
return ReceivedOrTimeout(self, *args, **kwargs)
|
||||
|
||||
if expiration < self.now:
|
||||
raise ValueError("Invalid expiration time")
|
||||
def tell(self, msg, sender=None):
|
||||
self._inbox.append(Tell(timestamp=self.now, payload=msg, sender=sender))
|
||||
|
||||
def ready(agent):
|
||||
return agent._received > current or agent.now >= expiration
|
||||
|
||||
def value(agent):
|
||||
if agent.now > expiration:
|
||||
raise TimedOut("No message received")
|
||||
|
||||
c = Cond(func=ready, return_func=value)
|
||||
c._checked = True
|
||||
return c
|
||||
|
||||
def tell(self, msg, sender):
|
||||
self._received += 1
|
||||
self._inbox.append(Tell(payload=msg, sender=sender))
|
||||
|
||||
def ask(self, msg, timeout=None):
|
||||
self._received += 1
|
||||
ask = Ask(payload=msg)
|
||||
def ask(self, msg, timeout=None, **kwargs):
|
||||
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)
|
||||
expiration = float("inf") if timeout is None else self.now + timeout
|
||||
return ask.replied(expiration=expiration, **kwargs)
|
||||
|
||||
def check_messages(self):
|
||||
changed = False
|
||||
while self._inbox:
|
||||
msg = self._inbox.popleft()
|
||||
self._processed += 1
|
||||
if msg.expired(self.now):
|
||||
continue
|
||||
changed = True
|
||||
reply = self.on_receive(msg.payload, sender=msg.sender)
|
||||
if isinstance(msg, Ask):
|
||||
msg.reply = reply
|
||||
return changed
|
||||
|
||||
|
||||
Evented = EventedAgent
|
||||
|
@ -38,8 +38,6 @@ def state(name=None):
|
||||
self._last_return = None
|
||||
self._last_except = None
|
||||
|
||||
|
||||
|
||||
func.id = name or func.__name__
|
||||
func.is_default = False
|
||||
return func
|
||||
|
@ -54,7 +54,7 @@ class NetworkAgent(BaseAgent):
|
||||
return G
|
||||
|
||||
def remove_node(self):
|
||||
print(f"Removing node for {self.unique_id}: {self.node_id}")
|
||||
self.debug(f"Removing node for {self.unique_id}: {self.node_id}")
|
||||
self.G.remove_node(self.node_id)
|
||||
self.node_id = None
|
||||
|
||||
@ -80,3 +80,6 @@ class NetworkAgent(BaseAgent):
|
||||
if remove:
|
||||
self.remove_node()
|
||||
return super().die()
|
||||
|
||||
|
||||
NetAgent = NetworkAgent
|
||||
|
@ -38,7 +38,7 @@ class BaseEnvironment(Model):
|
||||
self,
|
||||
id="unnamed_env",
|
||||
seed="default",
|
||||
schedule=None,
|
||||
schedule_class=time.TimedActivation,
|
||||
dir_path=None,
|
||||
interval=1,
|
||||
agent_class=None,
|
||||
@ -58,9 +58,11 @@ class BaseEnvironment(Model):
|
||||
|
||||
self.dir_path = dir_path or os.getcwd()
|
||||
|
||||
if schedule is None:
|
||||
schedule = time.TimedActivation(self)
|
||||
self.schedule = schedule
|
||||
if schedule_class is None:
|
||||
schedule_class = time.TimedActivation
|
||||
else:
|
||||
schedule_class = serialization.deserialize(schedule_class)
|
||||
self.schedule = schedule_class(self)
|
||||
|
||||
self.agent_class = agent_class or agentmod.BaseAgent
|
||||
|
||||
@ -310,15 +312,28 @@ class NetworkEnvironment(BaseEnvironment):
|
||||
self.add_agent(node_id=node_id, agent_class=a_class, **agent_params)
|
||||
|
||||
|
||||
Environment = NetworkEnvironment
|
||||
|
||||
|
||||
class EventedEnvironment(Environment):
|
||||
def broadcast(self, msg, sender, expiration=None, ttl=None, **kwargs):
|
||||
class EventedEnvironment(BaseEnvironment):
|
||||
def broadcast(self, msg, sender=None, expiration=None, ttl=None, **kwargs):
|
||||
for agent in self.agents(**kwargs):
|
||||
self.logger.info(f'Telling {repr(agent)}: {msg} ttl={ttl}')
|
||||
if agent == sender:
|
||||
continue
|
||||
self.logger.info(f"Telling {repr(agent)}: {msg} ttl={ttl}")
|
||||
try:
|
||||
agent._inbox.append(events.Tell(payload=msg, sender=sender, expiration=expiration if ttl is None else self.now+ttl))
|
||||
inbox = agent._inbox
|
||||
except AttributeError:
|
||||
self.info(f'Agent {agent.unique_id} cannot receive events')
|
||||
self.logger.info(
|
||||
f"Agent {agent.unique_id} cannot receive events because it does not have an inbox"
|
||||
)
|
||||
continue
|
||||
# Allow for AttributeError exceptions in this part of the code
|
||||
inbox.append(
|
||||
events.Tell(
|
||||
payload=msg,
|
||||
sender=sender,
|
||||
expiration=expiration if ttl is None else self.now + ttl,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class Environment(NetworkEnvironment, EventedEnvironment):
|
||||
"""Default environment class, has both network and event capabilities"""
|
||||
|
@ -1,38 +1,51 @@
|
||||
from .time import Cond
|
||||
from .time import BaseCond
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
class Event:
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Message:
|
||||
payload: Any
|
||||
sender: Any = None
|
||||
expiration: float = None
|
||||
timestamp: float = None
|
||||
id: int = field(default_factory=uuid4)
|
||||
|
||||
def expired(self, when):
|
||||
return self.expiration is not None and self.expiration < when
|
||||
|
||||
|
||||
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):
|
||||
def ready(agent):
|
||||
return self.reply is not None or agent.now > expiration
|
||||
|
||||
def value(agent):
|
||||
if agent.now > expiration:
|
||||
raise TimedOut(f'No answer received for {self}')
|
||||
return self.reply
|
||||
|
||||
return Cond(func=ready, return_func=value)
|
||||
return ReplyCond(self)
|
||||
|
||||
|
||||
class Tell(Message):
|
||||
|
@ -59,7 +59,6 @@ def find_unassigned(G, shuffle=False, random=random):
|
||||
|
||||
If node_id is None, a node without an agent_id will be found.
|
||||
"""
|
||||
# TODO: test
|
||||
candidates = list(G.nodes(data=True))
|
||||
if shuffle:
|
||||
random.shuffle(candidates)
|
||||
|
@ -221,8 +221,6 @@ def deserialize(type_, value=None, globs=None, **kwargs):
|
||||
|
||||
def deserialize_all(names, *args, known_modules=KNOWN_MODULES, **kwargs):
|
||||
"""Return the list of deserialized objects"""
|
||||
# TODO: remove
|
||||
print("SERIALIZATION", kwargs)
|
||||
objects = []
|
||||
for name in names:
|
||||
mod = deserialize(name, known_modules=known_modules)
|
||||
|
@ -66,7 +66,7 @@ class Simulation:
|
||||
if ignored:
|
||||
d.setdefault("extra", {}).update(ignored)
|
||||
if ignored:
|
||||
print(f'Warning: Ignoring these parameters (added to "extra"): { ignored }')
|
||||
logger.warning(f'Ignoring these parameters (added to "extra"): { ignored }')
|
||||
d.update(kwargs)
|
||||
|
||||
return cls(**d)
|
||||
|
227
soil/time.py
227
soil/time.py
@ -1,10 +1,11 @@
|
||||
from mesa.time import BaseScheduler
|
||||
from queue import Empty
|
||||
from heapq import heappush, heappop, heapify
|
||||
from heapq import heappush, heappop
|
||||
import math
|
||||
|
||||
from inspect import getsource
|
||||
from numbers import Number
|
||||
from textwrap import dedent
|
||||
|
||||
from .utils import logger
|
||||
from mesa import Agent as MesaAgent
|
||||
@ -23,65 +24,11 @@ class When:
|
||||
return time
|
||||
self._time = time
|
||||
|
||||
def next(self, time):
|
||||
def abs(self, time):
|
||||
return self._time
|
||||
|
||||
def abs(self, time):
|
||||
return self
|
||||
|
||||
def __repr__(self):
|
||||
return str(f"When({self._time})")
|
||||
|
||||
def __lt__(self, other):
|
||||
if isinstance(other, Number):
|
||||
return self._time < other
|
||||
return self._time < other.next(self._time)
|
||||
|
||||
def __gt__(self, other):
|
||||
if isinstance(other, Number):
|
||||
return self._time > other
|
||||
return self._time > other.next(self._time)
|
||||
|
||||
def ready(self, agent):
|
||||
return self._time <= agent.model.schedule.time
|
||||
|
||||
def return_value(self, agent):
|
||||
return None
|
||||
|
||||
|
||||
class Cond(When):
|
||||
def __init__(self, func, delta=1, return_func=lambda agent: None):
|
||||
self._func = func
|
||||
self._delta = delta
|
||||
self._checked = False
|
||||
self._return_func = return_func
|
||||
|
||||
def next(self, time):
|
||||
if self._checked:
|
||||
return time + self._delta
|
||||
return time
|
||||
|
||||
def abs(self, time):
|
||||
return self
|
||||
|
||||
def ready(self, agent):
|
||||
self._checked = True
|
||||
return self._func(agent)
|
||||
|
||||
def return_value(self, agent):
|
||||
return self._return_func(agent)
|
||||
|
||||
def __eq__(self, other):
|
||||
return False
|
||||
|
||||
def __lt__(self, other):
|
||||
return True
|
||||
|
||||
def __gt__(self, other):
|
||||
return False
|
||||
|
||||
def __repr__(self):
|
||||
return str(f'Cond("{getsource(self._func)}")')
|
||||
def schedule_next(self, time, delta, first=False):
|
||||
return (self._time, None)
|
||||
|
||||
|
||||
NEVER = When(INFINITY)
|
||||
@ -91,48 +38,94 @@ 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 abs(self, time):
|
||||
return When(self._delta + time)
|
||||
|
||||
def next(self, time):
|
||||
return time + self._delta
|
||||
def schedule_next(self, time, delta, first=False):
|
||||
return (time + self._delta, None)
|
||||
|
||||
def __repr__(self):
|
||||
return str(f"Delta({self._delta})")
|
||||
|
||||
|
||||
class BaseCond:
|
||||
def __init__(self, msg=None, delta=None, eager=False):
|
||||
self._msg = msg
|
||||
self._delta = delta
|
||||
self.eager = eager
|
||||
|
||||
def schedule_next(self, time, delta, first=False):
|
||||
if first and self.eager:
|
||||
return (time, self)
|
||||
if self._delta:
|
||||
delta = self._delta
|
||||
return (time + delta, self)
|
||||
|
||||
def return_value(self, agent):
|
||||
return None
|
||||
|
||||
def __repr__(self):
|
||||
return self._msg or self.__class__.__name__
|
||||
|
||||
|
||||
class Cond(BaseCond):
|
||||
def __init__(self, func, *args, **kwargs):
|
||||
self._func = func
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def ready(self, agent, time):
|
||||
return self._func(agent)
|
||||
|
||||
def __repr__(self):
|
||||
if self._msg:
|
||||
return self._msg
|
||||
return str(f'Cond("{dedent(getsource(self._func)).strip()}")')
|
||||
|
||||
|
||||
class TimedActivation(BaseScheduler):
|
||||
"""A scheduler which activates each agent when the agent requests.
|
||||
In each activation, each agent will update its 'next_time'.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, *args, shuffle=True, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._next = {}
|
||||
self._queue = []
|
||||
self.next_time = 0
|
||||
self._shuffle = shuffle
|
||||
self.step_interval = 1
|
||||
self.logger = logger.getChild(f"time_{ self.model }")
|
||||
|
||||
def add(self, agent: MesaAgent, when=None):
|
||||
if when is None:
|
||||
when = When(self.time)
|
||||
elif not isinstance(when, When):
|
||||
when = When(when)
|
||||
if agent.unique_id in self._agents:
|
||||
del self._agents[agent.unique_id]
|
||||
if agent.unique_id in self._next:
|
||||
self._queue.remove((self._next[agent.unique_id], agent))
|
||||
heapify(self._queue)
|
||||
when = self.time
|
||||
elif isinstance(when, When):
|
||||
when = when.abs()
|
||||
|
||||
self._next[agent.unique_id] = when
|
||||
heappush(self._queue, (when, agent))
|
||||
self._schedule(agent, None, when)
|
||||
super().add(agent)
|
||||
|
||||
def _schedule(self, agent, condition=None, when=None):
|
||||
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
|
||||
if self._shuffle:
|
||||
key = (when, self.model.random.random(), condition)
|
||||
else:
|
||||
key = (when, agent.unique_id, condition)
|
||||
self._next[agent.unique_id] = key
|
||||
heappush(self._queue, (key, agent))
|
||||
|
||||
def step(self) -> None:
|
||||
"""
|
||||
Executes agents in order, one at a time. After each step,
|
||||
@ -143,73 +136,71 @@ class TimedActivation(BaseScheduler):
|
||||
if not self.model.running:
|
||||
return
|
||||
|
||||
when = NEVER
|
||||
|
||||
to_process = []
|
||||
skipped = []
|
||||
next_time = INFINITY
|
||||
|
||||
ix = 0
|
||||
|
||||
self.logger.debug(f"Queue length: {len(self._queue)}")
|
||||
|
||||
while self._queue:
|
||||
(when, agent) = self._queue[0]
|
||||
((when, _id, cond), agent) = self._queue[0]
|
||||
if when > self.time:
|
||||
break
|
||||
|
||||
heappop(self._queue)
|
||||
if when.ready(agent):
|
||||
if cond:
|
||||
if not cond.ready(agent, self.time):
|
||||
self._schedule(agent, cond)
|
||||
continue
|
||||
try:
|
||||
agent._last_return = when.return_value(agent)
|
||||
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._next.pop(agent.unique_id, None)
|
||||
to_process.append(agent)
|
||||
continue
|
||||
|
||||
next_time = min(next_time, when.next(self.time))
|
||||
self._next[agent.unique_id] = next_time
|
||||
skipped.append((when, agent))
|
||||
|
||||
if self._queue:
|
||||
next_time = min(next_time, self._queue[0][0].next(self.time))
|
||||
|
||||
self._queue = [*skipped, *self._queue]
|
||||
|
||||
for agent in to_process:
|
||||
self.logger.debug(f"Stepping agent {agent}")
|
||||
self._next.pop(agent.unique_id, None)
|
||||
|
||||
try:
|
||||
returned = ((agent.step() or Delta(1))).abs(self.time)
|
||||
returned = agent.step()
|
||||
except DeadAgent:
|
||||
if agent.unique_id in self._next:
|
||||
del self._next[agent.unique_id]
|
||||
agent.alive = False
|
||||
continue
|
||||
|
||||
# Check status for MESA agents
|
||||
if not getattr(agent, "alive", True):
|
||||
continue
|
||||
|
||||
value = returned.next(self.time)
|
||||
agent._last_return = value
|
||||
|
||||
if value < self.time:
|
||||
raise Exception(
|
||||
f"Cannot schedule an agent for a time in the past ({when} < {self.time})"
|
||||
if returned:
|
||||
next_check = returned.schedule_next(
|
||||
self.time, self.step_interval, first=True
|
||||
)
|
||||
if value < INFINITY:
|
||||
next_time = min(value, next_time)
|
||||
|
||||
self._next[agent.unique_id] = returned
|
||||
heappush(self._queue, (returned, agent))
|
||||
self._schedule(agent, when=next_check[0], condition=next_check[1])
|
||||
else:
|
||||
assert not self._next[agent.unique_id]
|
||||
next_check = (self.time + self.step_interval, None)
|
||||
|
||||
self._schedule(agent)
|
||||
|
||||
self.steps += 1
|
||||
self.logger.debug(f"Updating time step: {self.time} -> {next_time}")
|
||||
self.time = next_time
|
||||
|
||||
if not self._queue or next_time == INFINITY:
|
||||
if not self._queue:
|
||||
self.time = INFINITY
|
||||
self.model.running = False
|
||||
return self.time
|
||||
|
||||
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(f"Updating time step: {self.time} -> {next_time}")
|
||||
|
||||
self.time = next_time
|
||||
|
||||
|
||||
class ShuffledTimedActivation(TimedActivation):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, shuffle=True, **kwargs)
|
||||
|
||||
|
||||
class OrderedTimedActivation(TimedActivation):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, shuffle=False, **kwargs)
|
||||
|
@ -12,34 +12,34 @@ class Dead(agents.FSM):
|
||||
return self.die()
|
||||
|
||||
|
||||
class TestMain(TestCase):
|
||||
class TestAgents(TestCase):
|
||||
def test_die_returns_infinity(self):
|
||||
'''The last step of a dead agent should return time.INFINITY'''
|
||||
"""The last step of a dead agent should return time.INFINITY"""
|
||||
d = Dead(unique_id=0, model=environment.Environment())
|
||||
ret = d.step().abs(0)
|
||||
print(ret, "next")
|
||||
ret = d.step()
|
||||
assert ret == stime.NEVER
|
||||
|
||||
def test_die_raises_exception(self):
|
||||
'''A dead agent should raise an exception if it is stepped after death'''
|
||||
"""A dead agent should raise an exception if it is stepped after death"""
|
||||
d = Dead(unique_id=0, model=environment.Environment())
|
||||
d.step()
|
||||
with pytest.raises(stime.DeadAgent):
|
||||
d.step()
|
||||
|
||||
|
||||
def test_agent_generator(self):
|
||||
'''
|
||||
"""
|
||||
The step function of an agent could be a generator. In that case, the state of the
|
||||
agent will be resumed after every call to step.
|
||||
'''
|
||||
"""
|
||||
a = 0
|
||||
|
||||
class Gen(agents.BaseAgent):
|
||||
def step(self):
|
||||
nonlocal a
|
||||
for i in range(5):
|
||||
yield
|
||||
a += 1
|
||||
|
||||
e = environment.Environment()
|
||||
g = Gen(model=e, unique_id=e.next_id())
|
||||
e.schedule.add(g)
|
||||
@ -51,8 +51,9 @@ class TestMain(TestCase):
|
||||
def test_state_decorator(self):
|
||||
class MyAgent(agents.FSM):
|
||||
run = 0
|
||||
|
||||
@agents.default_state
|
||||
@agents.state('original')
|
||||
@agents.state("original")
|
||||
def root(self):
|
||||
self.run += 1
|
||||
return self.other
|
||||
@ -66,4 +67,97 @@ class TestMain(TestCase):
|
||||
a.step()
|
||||
assert a.run == 1
|
||||
a.step()
|
||||
assert a.run == 2
|
||||
|
||||
def test_broadcast(self):
|
||||
"""
|
||||
An agent should be able to broadcast messages to every other agent, AND each receiver should be able
|
||||
to process it
|
||||
"""
|
||||
|
||||
class BCast(agents.Evented):
|
||||
pings_received = 0
|
||||
|
||||
def step(self):
|
||||
print(self.model.broadcast)
|
||||
try:
|
||||
self.model.broadcast("PING")
|
||||
except Exception as ex:
|
||||
print(ex)
|
||||
while True:
|
||||
self.check_messages()
|
||||
yield
|
||||
|
||||
def on_receive(self, msg, sender=None):
|
||||
self.pings_received += 1
|
||||
|
||||
e = environment.EventedEnvironment()
|
||||
|
||||
for i in range(10):
|
||||
e.add_agent(agent_class=BCast)
|
||||
e.step()
|
||||
pings_received = lambda: [a.pings_received for a in e.agents]
|
||||
assert sorted(pings_received()) == list(range(1, 11))
|
||||
e.step()
|
||||
assert all(x == 10 for x in pings_received())
|
||||
|
||||
def test_ask_messages(self):
|
||||
"""
|
||||
An agent should be able to ask another agent, and wait for a response.
|
||||
"""
|
||||
|
||||
# There are two agents, they try to send pings
|
||||
# This is arguably a very contrived example. In practice, the or
|
||||
# There should be a delay of one step between agent 0 and 1
|
||||
# On the first step:
|
||||
# Agent 0 sends a PING, but blocks before a PONG
|
||||
# Agent 1 detects the PING, responds with a PONG, and blocks after its own PING
|
||||
# After that step, every agent can both receive (there are pending messages) and send.
|
||||
# In each step, for each agent, one message is sent, and another one is received
|
||||
# (although not necessarily in that order).
|
||||
|
||||
# Results depend on ordering (agents are normally shuffled)
|
||||
# so we force the timedactivation not to be shuffled
|
||||
|
||||
pings = []
|
||||
pongs = []
|
||||
responses = []
|
||||
|
||||
class Ping(agents.EventedAgent):
|
||||
def step(self):
|
||||
target_id = (self.unique_id + 1) % self.count_agents()
|
||||
target = self.model.agents[target_id]
|
||||
print("starting")
|
||||
while True:
|
||||
if pongs or not pings: # First agent, or anyone after that
|
||||
pings.append(self.now)
|
||||
response = yield 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()
|
||||
print("done")
|
||||
|
||||
def on_receive(self, msg, sender=None):
|
||||
if msg == "PING":
|
||||
pongs.append(self.now)
|
||||
return "PONG"
|
||||
raise Exception("This should never happen")
|
||||
|
||||
e = environment.EventedEnvironment(schedule_class=stime.OrderedTimedActivation)
|
||||
for i in range(2):
|
||||
e.add_agent(agent_class=Ping)
|
||||
assert e.now == 0
|
||||
|
||||
for i in range(5):
|
||||
e.step()
|
||||
time = i + 1
|
||||
assert e.now == time
|
||||
assert len(pings) == 2 * time
|
||||
assert len(pongs) == (2 * time) - 1
|
||||
# Every step between 0 and t appears twice
|
||||
assert sum(pings) == sum(range(time)) * 2
|
||||
# It is the same as pings, without the leading 0
|
||||
assert sum(pongs) == sum(range(time)) * 2
|
||||
|
@ -44,6 +44,8 @@ def add_example_tests():
|
||||
for cfg, path in serialization.load_files(
|
||||
join(EXAMPLES, "**", "*.yml"),
|
||||
):
|
||||
if "soil_output" in path:
|
||||
continue
|
||||
p = make_example_test(path=path, cfg=config.Config.from_raw(cfg))
|
||||
fname = os.path.basename(path)
|
||||
p.__name__ = "test_example_file_%s" % fname
|
||||
|
@ -172,25 +172,24 @@ class TestMain(TestCase):
|
||||
assert len(configs) > 0
|
||||
|
||||
def test_until(self):
|
||||
config = {
|
||||
"name": "until_sim",
|
||||
"model_params": {
|
||||
"network_params": {},
|
||||
"agents": {
|
||||
"fixed": [
|
||||
{
|
||||
"agent_class": agents.BaseAgent,
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
"max_time": 2,
|
||||
"num_trials": 50,
|
||||
}
|
||||
s = simulation.from_config(config)
|
||||
n_runs = 0
|
||||
|
||||
class CheckRun(agents.BaseAgent):
|
||||
def step(self):
|
||||
nonlocal n_runs
|
||||
n_runs += 1
|
||||
return super().step()
|
||||
|
||||
n_trials = 50
|
||||
max_time = 2
|
||||
s = simulation.Simulation(
|
||||
model_params={"agents": [{"agent_class": CheckRun}]},
|
||||
num_trials=n_trials,
|
||||
max_time=max_time,
|
||||
)
|
||||
runs = list(s.run_simulation(dry_run=True))
|
||||
over = list(x.now for x in runs if x.now > 2)
|
||||
assert len(runs) == config["num_trials"]
|
||||
assert len(runs) == n_trials
|
||||
assert len(over) == 0
|
||||
|
||||
def test_fsm(self):
|
||||
|
@ -72,7 +72,7 @@ class TestNetwork(TestCase):
|
||||
assert len(env.agents) == 2
|
||||
assert env.agents[1].count_agents(state_id="normal") == 2
|
||||
assert env.agents[1].count_agents(state_id="normal", limit_neighbors=True) == 1
|
||||
assert env.agents[0].neighbors == 1
|
||||
assert env.agents[0].count_neighbors() == 1
|
||||
|
||||
def test_custom_agent_neighbors(self):
|
||||
"""Allow for search of neighbors with a certain state_id"""
|
||||
@ -90,7 +90,7 @@ class TestNetwork(TestCase):
|
||||
env = s.run_simulation(dry_run=True)[0]
|
||||
assert env.agents[1].count_agents(state_id="normal") == 2
|
||||
assert env.agents[1].count_agents(state_id="normal", limit_neighbors=True) == 1
|
||||
assert env.agents[0].neighbors == 1
|
||||
assert env.agents[0].count_neighbors() == 1
|
||||
|
||||
def test_subgraph(self):
|
||||
"""An agent should be able to subgraph the global topology"""
|
||||
|
@ -2,11 +2,12 @@ from unittest import TestCase
|
||||
|
||||
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)
|
||||
@ -16,59 +17,58 @@ class TestMain(TestCase):
|
||||
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):
|
||||
'''
|
||||
'''
|
||||
""" """
|
||||
|
||||
times_started = []
|
||||
times_awakened = []
|
||||
times_asleep = []
|
||||
times = []
|
||||
done = 0
|
||||
done = []
|
||||
|
||||
class CondAgent(agents.BaseAgent):
|
||||
|
||||
def step(self):
|
||||
nonlocal done
|
||||
times_started.append(self.now)
|
||||
while True:
|
||||
yield time.Cond(lambda agent: agent.model.schedule.time >= 10)
|
||||
times_asleep.append(self.now)
|
||||
yield time.Cond(lambda agent: agent.now >= 10, delta=2)
|
||||
times_awakened.append(self.now)
|
||||
if self.now >= 10:
|
||||
break
|
||||
done += 1
|
||||
|
||||
env = environment.Environment(agents=[{'agent_class': CondAgent}])
|
||||
done.append(self.now)
|
||||
|
||||
env = environment.Environment(agents=[{"agent_class": CondAgent}])
|
||||
|
||||
while env.schedule.time < 11:
|
||||
env.step()
|
||||
times.append(env.now)
|
||||
env.step()
|
||||
|
||||
assert env.schedule.time == 11
|
||||
assert times_started == [0]
|
||||
assert times_awakened == [10]
|
||||
assert done == 1
|
||||
assert done == [10]
|
||||
# The first time will produce the Cond.
|
||||
# Since there are no other agents, time will not advance, but the number
|
||||
# of steps will.
|
||||
assert env.schedule.steps == 12
|
||||
assert len(times) == 12
|
||||
assert env.schedule.steps == 6
|
||||
assert len(times) == 6
|
||||
|
||||
while env.schedule.time < 12:
|
||||
env.step()
|
||||
while env.schedule.time < 13:
|
||||
times.append(env.now)
|
||||
env.step()
|
||||
|
||||
assert env.schedule.time == 12
|
||||
assert times == [0, 2, 4, 6, 8, 10, 11]
|
||||
assert env.schedule.time == 13
|
||||
assert times_started == [0, 11]
|
||||
assert times_awakened == [10, 11]
|
||||
assert done == 2
|
||||
assert times_awakened == [10]
|
||||
assert done == [10]
|
||||
# Once more to yield the cond, another one to continue
|
||||
assert env.schedule.steps == 14
|
||||
assert len(times) == 14
|
||||
assert env.schedule.steps == 7
|
||||
assert len(times) == 7
|
||||
|
Loading…
Reference in New Issue
Block a user