1
0
mirror of https://github.com/gsi-upm/soil synced 2024-12-22 16:28:11 +00:00

Decouple activation and schedulers

This commit is contained in:
J. Fernando Sánchez 2023-05-16 09:05:23 +02:00
parent 3041156f19
commit ee0c4517cb
5 changed files with 115 additions and 67 deletions

1
.gitignore vendored
View File

@ -10,3 +10,4 @@ build/*
dist/* dist/*
prof prof
backup backup
*.egg-info

View File

@ -1,12 +1,9 @@
from soil import Evented, FSM, state, default_state, BaseAgent, NetworkAgent, Environment, parameters, report, TimedOut from soil import Evented, FSM, state, default_state, BaseAgent, NetworkAgent, Environment, parameters, report, TimedOut
import math import math
from soilent import Scheduler
class RabbitsImprovedEnv(Environment): class RabbitsImprovedEnv(Environment):
prob_death: parameters.probability = 1e-3 prob_death: parameters.probability = 1e-3
schedule_class = Scheduler
def init(self): def init(self):
a1 = self.add_node(Male) a1 = self.add_node(Male)

View File

@ -6,7 +6,6 @@ pandas>=1
SALib>=1.3 SALib>=1.3
Jinja2 Jinja2
Mesa>=1.2 Mesa>=1.2
pydantic>=1.9
sqlalchemy>=1.4 sqlalchemy>=1.4
typing-extensions>=4.4 typing-extensions>=4.4
annotated-types>=0.4 annotated-types>=0.4

View File

@ -138,10 +138,6 @@ class BaseAgent(MesaAgent, MutableMapping, metaclass=MetaAgent):
else: else:
self.debug(f"agent dying") self.debug(f"agent dying")
self.alive = False self.alive = False
try:
self.model.schedule.remove(self)
except KeyError:
pass
return time.Delay(time.INFINITY) return time.Delay(time.INFINITY)
def step(self): def step(self):

View File

@ -8,6 +8,7 @@ import logging
from inspect import getsource from inspect import getsource
from numbers import Number from numbers import Number
from textwrap import dedent from textwrap import dedent
import random as random_std
from .utils import logger from .utils import logger
from mesa import Agent as MesaAgent from mesa import Agent as MesaAgent
@ -30,6 +31,7 @@ class Delay:
class When: class When:
def __init__(self, when): def __init__(self, when):
raise Exception("The use of When is deprecated. Use the `Agent.at` and `Agent.delay` methods instead") raise Exception("The use of When is deprecated. Use the `Agent.at` and `Agent.delay` methods instead")
class Delta: class Delta:
def __init__(self, delta): def __init__(self, delta):
raise Exception("The use of Delay is deprecated. Use the `Agent.at` and `Agent.delay` methods instead") raise Exception("The use of Delay is deprecated. Use the `Agent.at` and `Agent.delay` methods instead")
@ -38,58 +40,57 @@ class DeadAgent(Exception):
pass pass
class PQueueActivation(BaseScheduler): class Event(object):
def __init__(self, when: float, func, order=1):
self.when = when
self.func = func
self.order = order
def __repr__(self):
return f'Event @ {self.when} - Func: {self.func}'
def __lt__(self, other):
return (self.when < other.when) or (self.when == other.when and self.order < other.order)
class PQueueSchedule:
""" """
A scheduler which activates each agent with a delay returned by the agent's step method. A scheduler which activates each function with a delay returned by the function at each step.
If no delay is returned, a default of 1 is used. 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): def __init__(self, shuffle=True, seed=None, **kwargs):
super().__init__(*args, **kwargs)
self._queue = [] self._queue = []
self._shuffle = shuffle self._shuffle = shuffle
self.logger = getattr(self.model, "logger", logger).getChild(f"time_{ self.model }") self.time = 0
self.steps = 0
self.random = random_std.Random(seed)
self.next_time = self.time self.next_time = self.time
self.agents_by_type = defaultdict(dict)
def add(self, agent: MesaAgent, when=None): def insert(self, when, callback, replace=False):
if when is None:
when = self.time
else:
when = float(when)
agent_class = type(agent)
self.agents_by_type[agent_class][agent.unique_id] = agent
super().add(agent)
self.add_callback(agent.step, when)
def add_callback(self, callback, when=None, replace=False):
if when is None: if when is None:
when = self.time when = self.time
else: else:
when = float(when) when = float(when)
order = 1
if self._shuffle: if self._shuffle:
key = (when, self.model.random.random()) order = self.random.random()
else: event = Event(when, callback, order=order)
key = when
if replace: if replace:
heapreplace(self._queue, (key, callback)) heapreplace(self._queue, event)
else: else:
heappush(self._queue, (key, callback)) heappush(self._queue, event)
def remove(self, agent): def remove(self, callback):
del self._agents[agent.unique_id] for i, event in enumerate(self._queue):
del self._agents[type(agent)][agent.unique_id] if callback == event.func:
for i, (key, callback) in enumerate(self._queue):
if callback == agent.step:
del self._queue[i] del self._queue[i]
break break
def step(self) -> None: def step(self) -> None:
""" """
Executes agents in order, one at a time. After each step, Executes events in order, one at a time. After each step,
an agent will signal when it wants to be scheduled next. an event will signal when it wants to be scheduled next.
""" """
if self.time == INFINITY: if self.time == INFINITY:
@ -100,44 +101,39 @@ class PQueueActivation(BaseScheduler):
now = self.time now = self.time
while self._queue: while self._queue:
((when, _id), agent) = self._queue[0] event = self._queue[0]
when = event.when
if when > now: if when > now:
next_time = when next_time = when
break break
when = agent.step() or 1 when = event.func() or 1
if when == INFINITY: if when == INFINITY:
heappop(self._queue) heappop(self._queue)
continue continue
when += now when += now
self.add_callback(agent, when, replace=True) self.insert(when, event.func, replace=True)
self.steps += 1 self.steps += 1
self.time = next_time self.time = next_time
if next_time == INFINITY: if next_time == INFINITY:
self.model.running = False
self.time = INFINITY self.time = INFINITY
return return
class TimedActivation(BaseScheduler): class Schedule:
def __init__(self, *args, shuffle=True, **kwargs): def __init__(self, shuffle=True, seed=None, **kwargs):
super().__init__(*args, **kwargs)
self._queue = deque() self._queue = deque()
self._shuffle = shuffle self._shuffle = shuffle
self.logger = getattr(self.model, "logger", logger).getChild(f"time_{ self.model }") self.time = 0
self.steps = 0
self.random = random_std.Random(seed)
self.next_time = self.time self.next_time = self.time
self.agents_by_type = defaultdict(dict)
def add(self, agent: MesaAgent, when=None):
self.add_callback(agent.step, when)
agent_class = type(agent)
self.agents_by_type[agent_class][agent.unique_id] = agent
super().add(agent)
def _find_loc(self, when=None): def _find_loc(self, when=None):
if when is None: if when is None:
@ -156,7 +152,7 @@ class TimedActivation(BaseScheduler):
self._queue.insert(pos, (when, lst)) self._queue.insert(pos, (when, lst))
return lst return lst
def add_callback(self, func, when=None, replace=False): def insert(self, when, func, replace=False):
lst = self._find_loc(when) lst = self._find_loc(when)
lst.append(func) lst.append(func)
@ -164,14 +160,17 @@ class TimedActivation(BaseScheduler):
lst = self._find_loc(when) lst = self._find_loc(when)
lst.extend(funcs) lst.extend(funcs)
def remove(self, agent): def remove(self, func):
del self._agents[agent.unique_id] for bucket in self._queue:
del self.agents_by_type[type(agent)][agent.unique_id] for (ix, e) in enumerate(bucket):
if e == func:
bucket.remove(ix)
return
def step(self) -> None: def step(self) -> None:
""" """
Executes agents in order, one at a time. After each step, Executes events in order, one at a time. After each step,
an agent will signal when it wants to be scheduled next. an event will signal when it wants to be scheduled next.
""" """
if not self._queue: if not self._queue:
return return
@ -186,7 +185,7 @@ class TimedActivation(BaseScheduler):
bucket = self._queue.popleft()[1] bucket = self._queue.popleft()[1]
if self._shuffle: if self._shuffle:
self.model.random.shuffle(bucket) self.random.shuffle(bucket)
next_batch = defaultdict(list) next_batch = defaultdict(list)
for func in bucket: for func in bucket:
when = func() or 1 when = func() or 1
@ -202,10 +201,68 @@ class TimedActivation(BaseScheduler):
if self._queue: if self._queue:
self.time = self._queue[0][0] self.time = self._queue[0][0]
else: else:
self.model.running = False
self.time = INFINITY self.time = INFINITY
class InnerActivation(BaseScheduler):
inner_class = Schedule
def __init__(self, model, shuffle=True, **kwargs):
self.model = model
self.logger = getattr(self.model, "logger", logger).getChild(f"time_{ self.model }")
self._agents = {}
self.agents_by_type = defaultdict(dict)
self.inner = self.inner_class(shuffle=shuffle, seed=self.model._seed)
@property
def steps(self):
return self.inner.steps
@property
def time(self):
return self.inner.time
def add(self, agent: MesaAgent, when=None):
when = when or self.inner.time
self.inner.insert(when, agent.step)
agent_class = type(agent)
self.agents_by_type[agent_class][agent.unique_id] = agent
super().add(agent)
def remove(self, agent):
del self._agents[agent.unique_id]
del self.agents_by_type[type(agent)][agent.unique_id]
self.inner.remove(agent.step)
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.
"""
self.inner.step()
class BucketTimedActivation(InnerActivation):
inner_class = Schedule
class PQueueActivation(InnerActivation):
inner_class = PQueueSchedule
# Set the bucket implementation as default
try:
from soilent.soilent import BucketScheduler
class SoilBucketActivation(InnerActivation):
inner_class = BucketScheduler
TimedActivation = SoilBucketActivation
except ImportError:
TimedActivation = BucketTimedActivation
pass
class ShuffledTimedActivation(TimedActivation): class ShuffledTimedActivation(TimedActivation):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, shuffle=True, **kwargs) super().__init__(*args, shuffle=True, **kwargs)
@ -214,5 +271,3 @@ class ShuffledTimedActivation(TimedActivation):
class OrderedTimedActivation(TimedActivation): class OrderedTimedActivation(TimedActivation):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, shuffle=False, **kwargs) super().__init__(*args, shuffle=False, **kwargs)