You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
soil/soil/time.py

214 lines
6.0 KiB
Python

from mesa.time import BaseScheduler
from queue import Empty
from heapq import heappush, heappop, heapreplace
import math
from inspect import getsource
from numbers import Number
from textwrap import dedent
from .utils import logger
2 years ago
from mesa import Agent as MesaAgent
INFINITY = float("inf")
2 years ago
class DeadAgent(Exception):
pass
class When:
def __init__(self, time):
2 years ago
if isinstance(time, When):
return time
self._time = time
def abs(self, time):
return self._time
def schedule_next(self, time, delta, first=False):
return (self._time, None)
NEVER = When(INFINITY)
2 years ago
class Delta(When):
def __init__(self, delta):
self._delta = delta
def abs(self, time):
return self._time + self._delta
2 years ago
def __eq__(self, other):
if isinstance(other, Delta):
return self._delta == other._delta
return False
def schedule_next(self, time, delta, first=False):
return (time + self._delta, None)
def __repr__(self):
return str(f"Delta({self._delta})")
class BaseCond:
def __init__(self, msg=None, delta=None, eager=False):
self._msg = msg
self._delta = delta
self.eager = eager
2 years ago
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, shuffle=True, **kwargs):
2 years ago
super().__init__(*args, **kwargs)
self._next = {}
self._queue = []
self._shuffle = shuffle
# self.step_interval = getattr(self.model, "interval", 1)
self.step_interval = self.model.interval
self.logger = getattr(self.model, "logger", logger).getChild(f"time_{ self.model }")
self.next_time = self.time
def add(self, agent: MesaAgent, when=None):
if when is None:
when = self.time
elif isinstance(when, When):
when = when.abs()
2 years ago
self._schedule(agent, None, when)
super().add(agent)
def _schedule(self, agent, condition=None, when=None, replace=False):
if condition:
if not when:
2 years ago
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
if replace:
heapreplace(self._queue, (key, agent))
else:
heappush(self._queue, (key, agent))
def step(self) -> None:
"""
Executes agents in order, one at a time. After each step,
an agent will signal when it wants to be scheduled next.
"""
self.logger.debug(f"Simulation step {self.time}")
if not self.model.running or self.time == INFINITY:
return
self.logger.debug(f"Queue length: %s", len(self._queue))
2 years ago
while self._queue:
((when, _id, cond), agent) = self._queue[0]
if when > self.time:
break
if cond:
if not cond.ready(agent, self.time):
self._schedule(agent, cond, replace=True)
continue
2 years ago
try:
agent._last_return = cond.return_value(agent)
2 years ago
except Exception as ex:
agent._last_except = ex
else:
agent._last_return = None
agent._last_except = None
2 years ago
self.logger.debug("Stepping agent %s", agent)
self._next.pop(agent.unique_id, None)
2 years ago
try:
returned = agent.step()
2 years ago
except DeadAgent:
agent.alive = False
heappop(self._queue)
2 years ago
continue
# Check status for MESA agents
if not getattr(agent, "alive", True):
heappop(self._queue)
continue
if returned:
2 years ago
next_check = returned.schedule_next(
self.time, self.step_interval, first=True
)
self._schedule(agent, when=next_check[0], condition=next_check[1], replace=True)
2 years ago
else:
next_check = (self.time + self.step_interval, None)
self._schedule(agent, replace=True)
self.steps += 1
if not self._queue:
2 years ago
self.model.running = False
self.time = INFINITY
return
next_time = self._queue[0][0][0]
if next_time < self.time:
raise Exception(
f"An agent has been scheduled for a time in the past, there is probably an error ({when} < {self.time})"
)
self.logger.debug("Updating time step: %s -> %s ", self.time, next_time)
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)