mirror of
				https://github.com/gsi-upm/soil
				synced 2025-10-31 15:48:16 +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_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 |             yield Delta(30)  # Wait at least 30 seconds before checking again | ||||||
|  |  | ||||||
|         try: |         try: | ||||||
| @@ -204,6 +205,8 @@ class Passenger(Evented, FSM): | |||||||
|         while not self.journey: |         while not self.journey: | ||||||
|             self.info(f"Passenger at: { self.pos }. Checking for responses.") |             self.info(f"Passenger at: { self.pos }. Checking for responses.") | ||||||
|             try: |             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) |                 yield self.received(expiration=expiration) | ||||||
|             except events.TimedOut: |             except events.TimedOut: | ||||||
|                 self.info(f"Passenger at: { self.pos }. Asking for journey.") |                 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 |                     journey, ttl=timeout, sender=self, agent_class=Driver | ||||||
|                 ) |                 ) | ||||||
|                 expiration = self.now + timeout |                 expiration = self.now + timeout | ||||||
|             self.check_messages() |  | ||||||
|         return self.driving_home |         return self.driving_home | ||||||
|  |  | ||||||
|     @state |     @state | ||||||
| @@ -220,13 +222,20 @@ class Passenger(Evented, FSM): | |||||||
|             self.pos[0] != self.journey.destination[0] |             self.pos[0] != self.journey.destination[0] | ||||||
|             or self.pos[1] != self.journey.destination[1] |             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.info("Got home safe!") | ||||||
|         self.die() |         self.die() | ||||||
|  |  | ||||||
|  |  | ||||||
| simulation = Simulation( | 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__": | if __name__ == "__main__": | ||||||
|   | |||||||
| @@ -133,7 +133,7 @@ class RandomAccident(BaseAgent): | |||||||
|             math.log10(max(1, rabbits_alive)) |             math.log10(max(1, rabbits_alive)) | ||||||
|         ) |         ) | ||||||
|         self.debug("Killing some rabbits with prob={}!".format(prob_death)) |         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: |             if i.state_id == i.dead.id: | ||||||
|                 continue |                 continue | ||||||
|             if self.prob(prob_death): |             if self.prob(prob_death): | ||||||
|   | |||||||
| @@ -258,9 +258,7 @@ class TerroristNetworkModel(TerroristSpreadModel): | |||||||
|             ) |             ) | ||||||
|             neighbours = set( |             neighbours = set( | ||||||
|                 agent.id |                 agent.id | ||||||
|                 for agent in self.get_neighbors( |                 for agent in self.get_neighbors(agent_class=TerroristNetworkModel) | ||||||
|                     agent_class=TerroristNetworkModel |  | ||||||
|                 ) |  | ||||||
|             ) |             ) | ||||||
|             search = (close_ups | step_neighbours) - neighbours |             search = (close_ups | step_neighbours) - neighbours | ||||||
|             for agent in self.get_agents(search): |             for agent in self.get_agents(search): | ||||||
|   | |||||||
| @@ -1 +1 @@ | |||||||
| 0.30.0rc2 | 0.30.0rc3 | ||||||
| @@ -47,7 +47,7 @@ def main( | |||||||
|         "file", |         "file", | ||||||
|         type=str, |         type=str, | ||||||
|         nargs="?", |         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)", |         help="Configuration file for the simulation (e.g., YAML or JSON)", | ||||||
|     ) |     ) | ||||||
|     parser.add_argument( |     parser.add_argument( | ||||||
| @@ -169,22 +169,26 @@ def main( | |||||||
|             sim.exporters = exporters |             sim.exporters = exporters | ||||||
|             sim.parallel = parallel |             sim.parallel = parallel | ||||||
|             sim.outdir = output |             sim.outdir = output | ||||||
|             sims = [sim, ] |             sims = [ | ||||||
|  |                 sim, | ||||||
|  |             ] | ||||||
|         else: |         else: | ||||||
|             logger.info("Loading config file: {}".format(args.file)) |             logger.info("Loading config file: {}".format(args.file)) | ||||||
|             if not os.path.exists(args.file): |             if not os.path.exists(args.file): | ||||||
|                 logger.error("Please, input a valid file") |                 logger.error("Please, input a valid file") | ||||||
|                 return |                 return | ||||||
|  |  | ||||||
|             sims = list(simulation.iter_from_config( |             sims = list( | ||||||
|                 args.file, |                 simulation.iter_from_config( | ||||||
|                 dry_run=args.dry_run, |                     args.file, | ||||||
|                 exporters=exporters, |                     dry_run=args.dry_run, | ||||||
|                 parallel=parallel, |                     exporters=exporters, | ||||||
|                 outdir=output, |                     parallel=parallel, | ||||||
|                 exporter_params=exp_params, |                     outdir=output, | ||||||
|                 **kwargs, |                     exporter_params=exp_params, | ||||||
|             )) |                     **kwargs, | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |  | ||||||
|         for sim in sims: |         for sim in sims: | ||||||
|  |  | ||||||
|   | |||||||
| @@ -20,12 +20,6 @@ from typing import Dict, List | |||||||
| from .. import serialization, utils, time, config | from .. import serialization, utils, time, config | ||||||
|  |  | ||||||
|  |  | ||||||
| def as_node(agent): |  | ||||||
|     if isinstance(agent, BaseAgent): |  | ||||||
|         return agent.id |  | ||||||
|     return agent |  | ||||||
|  |  | ||||||
|  |  | ||||||
| IGNORED_FIELDS = ("model", "logger") | 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): |     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) |         assert isinstance(unique_id, int) | ||||||
|         super().__init__(unique_id=unique_id, model=model) |         super().__init__(unique_id=unique_id, model=model) | ||||||
|  |  | ||||||
| @@ -207,7 +197,8 @@ class BaseAgent(MesaAgent, MutableMapping, metaclass=MetaAgent): | |||||||
|     def step(self): |     def step(self): | ||||||
|         if not self.alive: |         if not self.alive: | ||||||
|             raise time.DeadAgent(self.unique_id) |             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): |     def log(self, message, *args, level=logging.INFO, **kwargs): | ||||||
|         if not self.logger.isEnabledFor(level): |         if not self.logger.isEnabledFor(level): | ||||||
| @@ -414,7 +405,7 @@ def filter_agents( | |||||||
|     if ids: |     if ids: | ||||||
|         f = (agents[aid] for aid in ids if aid in agents) |         f = (agents[aid] for aid in ids if aid in agents) | ||||||
|     else: |     else: | ||||||
|         f = (a for a in agents.values()) |         f = agents.values() | ||||||
|  |  | ||||||
|     if state_id is not None and not isinstance(state_id, (tuple, list)): |     if state_id is not None and not isinstance(state_id, (tuple, list)): | ||||||
|         state_id = tuple([state_id]) |         state_id = tuple([state_id]) | ||||||
| @@ -638,6 +629,11 @@ from .SentimentCorrelationModel import * | |||||||
| from .SISaModel import * | from .SISaModel import * | ||||||
| from .CounterModel import * | from .CounterModel import * | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Agent(NetworkAgent, EventedAgent): | ||||||
|  |     """Default agent class, has both network and event capabilities""" | ||||||
|  |  | ||||||
|  |  | ||||||
| try: | try: | ||||||
|     import scipy |     import scipy | ||||||
|     from .Geo import Geo |     from .Geo import Geo | ||||||
|   | |||||||
| @@ -1,57 +1,77 @@ | |||||||
| from . import BaseAgent | from . import BaseAgent | ||||||
| from ..events import Message, Tell, Ask, Reply, TimedOut | from ..events import Message, Tell, Ask, TimedOut | ||||||
| from ..time import Cond | from ..time import BaseCond | ||||||
| from functools import partial | from functools import partial | ||||||
| from collections import deque | 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): |     def __init__(self, *args, **kwargs): | ||||||
|         super().__init__(*args, **kwargs) |         super().__init__(*args, **kwargs) | ||||||
|         self._inbox = deque() |         self._inbox = deque() | ||||||
|         self._received = 0 |  | ||||||
|         self._processed = 0 |         self._processed = 0 | ||||||
|  |  | ||||||
|  |  | ||||||
|     def on_receive(self, *args, **kwargs): |     def on_receive(self, *args, **kwargs): | ||||||
|         pass |         pass | ||||||
|  |  | ||||||
|     def received(self, expiration=None, timeout=None): |     def received(self, *args, **kwargs): | ||||||
|         current = self._received |         return ReceivedOrTimeout(self, *args, **kwargs) | ||||||
|         if expiration is None: |  | ||||||
|             expiration = float('inf') if timeout is None else self.now + timeout |  | ||||||
|  |  | ||||||
|         if expiration < self.now: |     def tell(self, msg, sender=None): | ||||||
|             raise ValueError("Invalid expiration time") |         self._inbox.append(Tell(timestamp=self.now, payload=msg, sender=sender)) | ||||||
|  |  | ||||||
|         def ready(agent): |     def ask(self, msg, timeout=None, **kwargs): | ||||||
|             return agent._received > current or agent.now >= expiration |         ask = Ask(timestamp=self.now, payload=msg, sender=self) | ||||||
|  |  | ||||||
|         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) |  | ||||||
|         self._inbox.append(ask) |         self._inbox.append(ask) | ||||||
|         expiration = float('inf') if timeout is None else self.now + timeout |         expiration = float("inf") if timeout is None else self.now + timeout | ||||||
|         return ask.replied(expiration=expiration) |         return ask.replied(expiration=expiration, **kwargs) | ||||||
|  |  | ||||||
|     def check_messages(self): |     def check_messages(self): | ||||||
|  |         changed = False | ||||||
|         while self._inbox: |         while self._inbox: | ||||||
|             msg = self._inbox.popleft() |             msg = self._inbox.popleft() | ||||||
|             self._processed += 1 |             self._processed += 1 | ||||||
|             if msg.expired(self.now): |             if msg.expired(self.now): | ||||||
|                 continue |                 continue | ||||||
|  |             changed = True | ||||||
|             reply = self.on_receive(msg.payload, sender=msg.sender) |             reply = self.on_receive(msg.payload, sender=msg.sender) | ||||||
|             if isinstance(msg, Ask): |             if isinstance(msg, Ask): | ||||||
|                 msg.reply = reply |                 msg.reply = reply | ||||||
|  |         return changed | ||||||
|  |  | ||||||
|  |  | ||||||
|  | Evented = EventedAgent | ||||||
|   | |||||||
| @@ -38,8 +38,6 @@ def state(name=None): | |||||||
|                         self._last_return = None |                         self._last_return = None | ||||||
|                         self._last_except = None |                         self._last_except = None | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|         func.id = name or func.__name__ |         func.id = name or func.__name__ | ||||||
|         func.is_default = False |         func.is_default = False | ||||||
|         return func |         return func | ||||||
|   | |||||||
| @@ -54,7 +54,7 @@ class NetworkAgent(BaseAgent): | |||||||
|         return G |         return G | ||||||
|  |  | ||||||
|     def remove_node(self): |     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.G.remove_node(self.node_id) | ||||||
|         self.node_id = None |         self.node_id = None | ||||||
|  |  | ||||||
| @@ -80,3 +80,6 @@ class NetworkAgent(BaseAgent): | |||||||
|         if remove: |         if remove: | ||||||
|             self.remove_node() |             self.remove_node() | ||||||
|         return super().die() |         return super().die() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | NetAgent = NetworkAgent | ||||||
|   | |||||||
| @@ -38,7 +38,7 @@ class BaseEnvironment(Model): | |||||||
|         self, |         self, | ||||||
|         id="unnamed_env", |         id="unnamed_env", | ||||||
|         seed="default", |         seed="default", | ||||||
|         schedule=None, |         schedule_class=time.TimedActivation, | ||||||
|         dir_path=None, |         dir_path=None, | ||||||
|         interval=1, |         interval=1, | ||||||
|         agent_class=None, |         agent_class=None, | ||||||
| @@ -58,9 +58,11 @@ class BaseEnvironment(Model): | |||||||
|  |  | ||||||
|         self.dir_path = dir_path or os.getcwd() |         self.dir_path = dir_path or os.getcwd() | ||||||
|  |  | ||||||
|         if schedule is None: |         if schedule_class is None: | ||||||
|             schedule = time.TimedActivation(self) |             schedule_class = time.TimedActivation | ||||||
|         self.schedule = schedule |         else: | ||||||
|  |             schedule_class = serialization.deserialize(schedule_class) | ||||||
|  |         self.schedule = schedule_class(self) | ||||||
|  |  | ||||||
|         self.agent_class = agent_class or agentmod.BaseAgent |         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) |             self.add_agent(node_id=node_id, agent_class=a_class, **agent_params) | ||||||
|  |  | ||||||
|  |  | ||||||
| Environment = NetworkEnvironment | class EventedEnvironment(BaseEnvironment): | ||||||
|  |     def broadcast(self, msg, sender=None, expiration=None, ttl=None, **kwargs): | ||||||
|  |  | ||||||
| class EventedEnvironment(Environment): |  | ||||||
|     def broadcast(self, msg,  sender, expiration=None, ttl=None, **kwargs): |  | ||||||
|         for agent in self.agents(**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: |             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: |             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 dataclasses import dataclass, field | ||||||
| from typing import Any | from typing import Any | ||||||
| from uuid import uuid4 | from uuid import uuid4 | ||||||
|  |  | ||||||
|  |  | ||||||
| class Event: | class Event: | ||||||
|     pass |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
| @dataclass | @dataclass | ||||||
| class Message: | class Message: | ||||||
|     payload: Any |     payload: Any | ||||||
|     sender: Any = None |     sender: Any = None | ||||||
|     expiration: float = None |     expiration: float = None | ||||||
|  |     timestamp: float = None | ||||||
|     id: int = field(default_factory=uuid4) |     id: int = field(default_factory=uuid4) | ||||||
|  |  | ||||||
|     def expired(self, when): |     def expired(self, when): | ||||||
|         return self.expiration is not None and self.expiration < when |         return self.expiration is not None and self.expiration < when | ||||||
|  |  | ||||||
|  |  | ||||||
| class Reply(Message): | class Reply(Message): | ||||||
|     source: 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): | class Ask(Message): | ||||||
|     reply: Message = None |     reply: Message = None | ||||||
|  |  | ||||||
|     def replied(self, expiration=None): |     def replied(self, expiration=None): | ||||||
|         def ready(agent): |         return ReplyCond(self) | ||||||
|             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) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Tell(Message): | 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. |     If node_id is None, a node without an agent_id will be found. | ||||||
|     """ |     """ | ||||||
|     # TODO: test |  | ||||||
|     candidates = list(G.nodes(data=True)) |     candidates = list(G.nodes(data=True)) | ||||||
|     if shuffle: |     if shuffle: | ||||||
|         random.shuffle(candidates) |         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): | def deserialize_all(names, *args, known_modules=KNOWN_MODULES, **kwargs): | ||||||
|     """Return the list of deserialized objects""" |     """Return the list of deserialized objects""" | ||||||
|     # TODO: remove |  | ||||||
|     print("SERIALIZATION", kwargs) |  | ||||||
|     objects = [] |     objects = [] | ||||||
|     for name in names: |     for name in names: | ||||||
|         mod = deserialize(name, known_modules=known_modules) |         mod = deserialize(name, known_modules=known_modules) | ||||||
|   | |||||||
| @@ -66,7 +66,7 @@ class Simulation: | |||||||
|         if ignored: |         if ignored: | ||||||
|             d.setdefault("extra", {}).update(ignored) |             d.setdefault("extra", {}).update(ignored) | ||||||
|         if 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) |         d.update(kwargs) | ||||||
|  |  | ||||||
|         return cls(**d) |         return cls(**d) | ||||||
|   | |||||||
							
								
								
									
										227
									
								
								soil/time.py
									
									
									
									
									
								
							
							
						
						
									
										227
									
								
								soil/time.py
									
									
									
									
									
								
							| @@ -1,10 +1,11 @@ | |||||||
| from mesa.time import BaseScheduler | from mesa.time import BaseScheduler | ||||||
| from queue import Empty | from queue import Empty | ||||||
| from heapq import heappush, heappop, heapify | from heapq import heappush, heappop | ||||||
| import math | import math | ||||||
|  |  | ||||||
| from inspect import getsource | from inspect import getsource | ||||||
| from numbers import Number | from numbers import Number | ||||||
|  | from textwrap import dedent | ||||||
|  |  | ||||||
| from .utils import logger | from .utils import logger | ||||||
| from mesa import Agent as MesaAgent | from mesa import Agent as MesaAgent | ||||||
| @@ -23,65 +24,11 @@ class When: | |||||||
|             return time |             return time | ||||||
|         self._time = time |         self._time = time | ||||||
|  |  | ||||||
|     def next(self, time): |     def abs(self, time): | ||||||
|         return self._time |         return self._time | ||||||
|  |  | ||||||
|     def abs(self, time): |     def schedule_next(self, time, delta, first=False): | ||||||
|         return self |         return (self._time, None) | ||||||
|  |  | ||||||
|     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)}")') |  | ||||||
|  |  | ||||||
|  |  | ||||||
| NEVER = When(INFINITY) | NEVER = When(INFINITY) | ||||||
| @@ -91,48 +38,94 @@ class Delta(When): | |||||||
|     def __init__(self, delta): |     def __init__(self, delta): | ||||||
|         self._delta = delta |         self._delta = delta | ||||||
|  |  | ||||||
|  |     def abs(self, time): | ||||||
|  |         return self._time + self._delta | ||||||
|  |  | ||||||
|     def __eq__(self, other): |     def __eq__(self, other): | ||||||
|         if isinstance(other, Delta): |         if isinstance(other, Delta): | ||||||
|             return self._delta == other._delta |             return self._delta == other._delta | ||||||
|         return False |         return False | ||||||
|  |  | ||||||
|     def abs(self, time): |     def schedule_next(self, time, delta, first=False): | ||||||
|         return When(self._delta + time) |         return (time + self._delta, None) | ||||||
|  |  | ||||||
|     def next(self, time): |  | ||||||
|         return time + self._delta |  | ||||||
|  |  | ||||||
|     def __repr__(self): |     def __repr__(self): | ||||||
|         return str(f"Delta({self._delta})") |         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): | class TimedActivation(BaseScheduler): | ||||||
|     """A scheduler which activates each agent when the agent requests. |     """A scheduler which activates each agent when the agent requests. | ||||||
|     In each activation, each agent will update its 'next_time'. |     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) |         super().__init__(*args, **kwargs) | ||||||
|         self._next = {} |         self._next = {} | ||||||
|         self._queue = [] |         self._queue = [] | ||||||
|         self.next_time = 0 |         self._shuffle = shuffle | ||||||
|  |         self.step_interval = 1 | ||||||
|         self.logger = logger.getChild(f"time_{ self.model }") |         self.logger = logger.getChild(f"time_{ self.model }") | ||||||
|  |  | ||||||
|     def add(self, agent: MesaAgent, when=None): |     def add(self, agent: MesaAgent, when=None): | ||||||
|         if when is None: |         if when is None: | ||||||
|             when = When(self.time) |             when = self.time | ||||||
|         elif not isinstance(when, When): |         elif isinstance(when, When): | ||||||
|             when = When(when) |             when = when.abs() | ||||||
|         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) |  | ||||||
|  |  | ||||||
|         self._next[agent.unique_id] = when |         self._schedule(agent, None, when) | ||||||
|         heappush(self._queue, (when, agent)) |  | ||||||
|         super().add(agent) |         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: |     def step(self) -> None: | ||||||
|         """ |         """ | ||||||
|         Executes agents in order, one at a time. After each step, |         Executes agents in order, one at a time. After each step, | ||||||
| @@ -143,73 +136,71 @@ class TimedActivation(BaseScheduler): | |||||||
|         if not self.model.running: |         if not self.model.running: | ||||||
|             return |             return | ||||||
|  |  | ||||||
|         when = NEVER |  | ||||||
|  |  | ||||||
|         to_process = [] |  | ||||||
|         skipped = [] |  | ||||||
|         next_time = INFINITY |  | ||||||
|  |  | ||||||
|         ix = 0 |  | ||||||
|  |  | ||||||
|         self.logger.debug(f"Queue length: {len(self._queue)}") |         self.logger.debug(f"Queue length: {len(self._queue)}") | ||||||
|  |  | ||||||
|         while self._queue: |         while self._queue: | ||||||
|             (when, agent) = self._queue[0] |             ((when, _id, cond), agent) = self._queue[0] | ||||||
|             if when > self.time: |             if when > self.time: | ||||||
|                 break |                 break | ||||||
|  |  | ||||||
|             heappop(self._queue) |             heappop(self._queue) | ||||||
|             if when.ready(agent): |             if cond: | ||||||
|  |                 if not cond.ready(agent, self.time): | ||||||
|  |                     self._schedule(agent, cond) | ||||||
|  |                     continue | ||||||
|                 try: |                 try: | ||||||
|                     agent._last_return = when.return_value(agent) |                     agent._last_return = cond.return_value(agent) | ||||||
|                 except Exception as ex: |                 except Exception as ex: | ||||||
|                     agent._last_except = 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.logger.debug(f"Stepping agent {agent}") | ||||||
|  |             self._next.pop(agent.unique_id, None) | ||||||
|  |  | ||||||
|             try: |             try: | ||||||
|                 returned = ((agent.step() or Delta(1))).abs(self.time) |                 returned = agent.step() | ||||||
|             except DeadAgent: |             except DeadAgent: | ||||||
|                 if agent.unique_id in self._next: |  | ||||||
|                     del self._next[agent.unique_id] |  | ||||||
|                 agent.alive = False |                 agent.alive = False | ||||||
|                 continue |                 continue | ||||||
|  |  | ||||||
|  |             # Check status for MESA agents | ||||||
|             if not getattr(agent, "alive", True): |             if not getattr(agent, "alive", True): | ||||||
|                 continue |                 continue | ||||||
|  |  | ||||||
|             value = returned.next(self.time) |             if returned: | ||||||
|             agent._last_return = value |                 next_check = returned.schedule_next( | ||||||
|  |                     self.time, self.step_interval, first=True | ||||||
|             if value < self.time: |  | ||||||
|                 raise Exception( |  | ||||||
|                     f"Cannot schedule an agent for a time in the past ({when} < {self.time})" |  | ||||||
|                 ) |                 ) | ||||||
|             if value < INFINITY: |                 self._schedule(agent, when=next_check[0], condition=next_check[1]) | ||||||
|                 next_time = min(value, next_time) |  | ||||||
|  |  | ||||||
|                 self._next[agent.unique_id] = returned |  | ||||||
|                 heappush(self._queue, (returned, agent)) |  | ||||||
|             else: |             else: | ||||||
|                 assert not self._next[agent.unique_id] |                 next_check = (self.time + self.step_interval, None) | ||||||
|  |  | ||||||
|  |                 self._schedule(agent) | ||||||
|  |  | ||||||
|         self.steps += 1 |         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 |             self.model.running = False | ||||||
|             return self.time |             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() |         return self.die() | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestMain(TestCase): | class TestAgents(TestCase): | ||||||
|     def test_die_returns_infinity(self): |     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()) |         d = Dead(unique_id=0, model=environment.Environment()) | ||||||
|         ret = d.step().abs(0) |         ret = d.step() | ||||||
|         print(ret, "next") |  | ||||||
|         assert ret == stime.NEVER |         assert ret == stime.NEVER | ||||||
|  |  | ||||||
|     def test_die_raises_exception(self): |     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 = Dead(unique_id=0, model=environment.Environment()) | ||||||
|         d.step() |         d.step() | ||||||
|         with pytest.raises(stime.DeadAgent): |         with pytest.raises(stime.DeadAgent): | ||||||
|             d.step() |             d.step() | ||||||
|  |  | ||||||
|  |  | ||||||
|     def test_agent_generator(self): |     def test_agent_generator(self): | ||||||
|         ''' |         """ | ||||||
|         The step function of an agent could be a generator. In that case, the state of the |         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. |         agent will be resumed after every call to step. | ||||||
|         ''' |         """ | ||||||
|         a = 0 |         a = 0 | ||||||
|  |  | ||||||
|         class Gen(agents.BaseAgent): |         class Gen(agents.BaseAgent): | ||||||
|             def step(self): |             def step(self): | ||||||
|                 nonlocal a |                 nonlocal a | ||||||
|                 for i in range(5): |                 for i in range(5): | ||||||
|                     yield |                     yield | ||||||
|                     a += 1 |                     a += 1 | ||||||
|  |  | ||||||
|         e = environment.Environment() |         e = environment.Environment() | ||||||
|         g = Gen(model=e, unique_id=e.next_id()) |         g = Gen(model=e, unique_id=e.next_id()) | ||||||
|         e.schedule.add(g) |         e.schedule.add(g) | ||||||
| @@ -51,8 +51,9 @@ class TestMain(TestCase): | |||||||
|     def test_state_decorator(self): |     def test_state_decorator(self): | ||||||
|         class MyAgent(agents.FSM): |         class MyAgent(agents.FSM): | ||||||
|             run = 0 |             run = 0 | ||||||
|  |  | ||||||
|             @agents.default_state |             @agents.default_state | ||||||
|             @agents.state('original') |             @agents.state("original") | ||||||
|             def root(self): |             def root(self): | ||||||
|                 self.run += 1 |                 self.run += 1 | ||||||
|                 return self.other |                 return self.other | ||||||
| @@ -66,4 +67,97 @@ class TestMain(TestCase): | |||||||
|         a.step() |         a.step() | ||||||
|         assert a.run == 1 |         assert a.run == 1 | ||||||
|         a.step() |         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( |     for cfg, path in serialization.load_files( | ||||||
|         join(EXAMPLES, "**", "*.yml"), |         join(EXAMPLES, "**", "*.yml"), | ||||||
|     ): |     ): | ||||||
|  |         if "soil_output" in path: | ||||||
|  |             continue | ||||||
|         p = make_example_test(path=path, cfg=config.Config.from_raw(cfg)) |         p = make_example_test(path=path, cfg=config.Config.from_raw(cfg)) | ||||||
|         fname = os.path.basename(path) |         fname = os.path.basename(path) | ||||||
|         p.__name__ = "test_example_file_%s" % fname |         p.__name__ = "test_example_file_%s" % fname | ||||||
|   | |||||||
| @@ -172,25 +172,24 @@ class TestMain(TestCase): | |||||||
|         assert len(configs) > 0 |         assert len(configs) > 0 | ||||||
|  |  | ||||||
|     def test_until(self): |     def test_until(self): | ||||||
|         config = { |         n_runs = 0 | ||||||
|             "name": "until_sim", |  | ||||||
|             "model_params": { |         class CheckRun(agents.BaseAgent): | ||||||
|                 "network_params": {}, |             def step(self): | ||||||
|                 "agents": { |                 nonlocal n_runs | ||||||
|                     "fixed": [ |                 n_runs += 1 | ||||||
|                         { |                 return super().step() | ||||||
|                             "agent_class": agents.BaseAgent, |  | ||||||
|                         } |         n_trials = 50 | ||||||
|                     ] |         max_time = 2 | ||||||
|                 }, |         s = simulation.Simulation( | ||||||
|             }, |             model_params={"agents": [{"agent_class": CheckRun}]}, | ||||||
|             "max_time": 2, |             num_trials=n_trials, | ||||||
|             "num_trials": 50, |             max_time=max_time, | ||||||
|         } |         ) | ||||||
|         s = simulation.from_config(config) |  | ||||||
|         runs = list(s.run_simulation(dry_run=True)) |         runs = list(s.run_simulation(dry_run=True)) | ||||||
|         over = list(x.now for x in runs if x.now > 2) |         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 |         assert len(over) == 0 | ||||||
|  |  | ||||||
|     def test_fsm(self): |     def test_fsm(self): | ||||||
|   | |||||||
| @@ -72,7 +72,7 @@ class TestNetwork(TestCase): | |||||||
|         assert len(env.agents) == 2 |         assert len(env.agents) == 2 | ||||||
|         assert env.agents[1].count_agents(state_id="normal") == 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[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): |     def test_custom_agent_neighbors(self): | ||||||
|         """Allow for search of neighbors with a certain state_id""" |         """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] |         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") == 2 | ||||||
|         assert env.agents[1].count_agents(state_id="normal", limit_neighbors=True) == 1 |         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): |     def test_subgraph(self): | ||||||
|         """An agent should be able to subgraph the global topology""" |         """An agent should be able to subgraph the global topology""" | ||||||
|   | |||||||
| @@ -2,11 +2,12 @@ from unittest import TestCase | |||||||
|  |  | ||||||
| from soil import time, agents, environment | from soil import time, agents, environment | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestMain(TestCase): | class TestMain(TestCase): | ||||||
|     def test_cond(self): |     def test_cond(self): | ||||||
|         ''' |         """ | ||||||
|         A condition should match a When if the concition is True |         A condition should match a When if the concition is True | ||||||
|         ''' |         """ | ||||||
|  |  | ||||||
|         t = time.Cond(lambda t: True) |         t = time.Cond(lambda t: True) | ||||||
|         f = time.Cond(lambda t: False) |         f = time.Cond(lambda t: False) | ||||||
| @@ -16,59 +17,58 @@ class TestMain(TestCase): | |||||||
|             assert w is not f |             assert w is not f | ||||||
|  |  | ||||||
|     def test_cond(self): |     def test_cond(self): | ||||||
|         ''' |         """ | ||||||
|         Comparing a Cond to a Delta should always return False |         Comparing a Cond to a Delta should always return False | ||||||
|         ''' |         """ | ||||||
|  |  | ||||||
|         c = time.Cond(lambda t: False) |         c = time.Cond(lambda t: False) | ||||||
|         d = time.Delta(1) |         d = time.Delta(1) | ||||||
|         assert c is not d |         assert c is not d | ||||||
|  |  | ||||||
|     def test_cond_env(self): |     def test_cond_env(self): | ||||||
|         ''' |         """ """ | ||||||
|         ''' |  | ||||||
|  |  | ||||||
|         times_started = [] |         times_started = [] | ||||||
|         times_awakened = [] |         times_awakened = [] | ||||||
|  |         times_asleep = [] | ||||||
|         times = [] |         times = [] | ||||||
|         done = 0 |         done = [] | ||||||
|  |  | ||||||
|         class CondAgent(agents.BaseAgent): |         class CondAgent(agents.BaseAgent): | ||||||
|  |  | ||||||
|             def step(self): |             def step(self): | ||||||
|                 nonlocal done |                 nonlocal done | ||||||
|                 times_started.append(self.now) |                 times_started.append(self.now) | ||||||
|                 while True: |                 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) |                     times_awakened.append(self.now) | ||||||
|                     if self.now >= 10: |                     if self.now >= 10: | ||||||
|                         break |                         break | ||||||
|                 done += 1 |                 done.append(self.now) | ||||||
|  |  | ||||||
|         env = environment.Environment(agents=[{'agent_class': CondAgent}]) |  | ||||||
|  |  | ||||||
|  |         env = environment.Environment(agents=[{"agent_class": CondAgent}]) | ||||||
|  |  | ||||||
|         while env.schedule.time < 11: |         while env.schedule.time < 11: | ||||||
|             env.step() |  | ||||||
|             times.append(env.now) |             times.append(env.now) | ||||||
|  |             env.step() | ||||||
|  |  | ||||||
|         assert env.schedule.time == 11 |         assert env.schedule.time == 11 | ||||||
|         assert times_started == [0] |         assert times_started == [0] | ||||||
|         assert times_awakened == [10] |         assert times_awakened == [10] | ||||||
|         assert done == 1 |         assert done == [10] | ||||||
|         # The first time will produce the Cond. |         # The first time will produce the Cond. | ||||||
|         # Since there are no other agents, time will not advance, but the number |         assert env.schedule.steps == 6 | ||||||
|         # of steps will. |         assert len(times) == 6 | ||||||
|         assert env.schedule.steps == 12 |  | ||||||
|         assert len(times) == 12 |  | ||||||
|  |  | ||||||
|         while env.schedule.time < 12: |         while env.schedule.time < 13: | ||||||
|             env.step() |  | ||||||
|             times.append(env.now) |             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_started == [0, 11] | ||||||
|         assert times_awakened == [10, 11] |         assert times_awakened == [10] | ||||||
|         assert done == 2 |         assert done == [10] | ||||||
|         # Once more to yield the cond, another one to continue |         # Once more to yield the cond, another one to continue | ||||||
|         assert env.schedule.steps == 14 |         assert env.schedule.steps == 7 | ||||||
|         assert len(times) == 14 |         assert len(times) == 7 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user