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

Add conditional time values

This commit is contained in:
J. Fernando Sánchez 2022-10-17 13:58:14 +02:00
parent 77d08fc592
commit 5d759d0072
7 changed files with 155 additions and 111 deletions

View File

@ -169,7 +169,7 @@ class BaseEnvironment(Model):
Advance one step in the simulation, and update the data collection and scheduler appropriately
"""
super().step()
self.logger.info(f"--- Step {self.now:^5} ---")
self.logger.info(f"--- Step: {self.schedule.steps:^5} - Time: {self.now:^5} ---")
self.schedule.step()
self.datacollector.collect(self)

View File

@ -3,6 +3,7 @@ import sys
from time import time as current_time
from io import BytesIO
from sqlalchemy import create_engine
from textwrap import dedent, indent
import matplotlib.pyplot as plt
@ -86,34 +87,8 @@ class Exporter:
pass
return open_or_reuse(f, mode=mode, **kwargs)
class default(Exporter):
"""Default exporter. Writes sqlite results, as well as the simulation YAML"""
def sim_start(self):
if self.dry_run:
logger.info("NOT dumping results")
return
logger.info("Dumping results to %s", self.outdir)
with self.output(self.simulation.name + ".dumped.yml") as f:
f.write(self.simulation.to_yaml())
self.dbpath = os.path.join(self.outdir, f"{self.simulation.name}.sqlite")
try_backup(self.dbpath, move=True)
def trial_end(self, env):
if self.dry_run:
logger.info("Running in DRY_RUN mode, the database will NOT be created")
return
with timer(
"Dumping simulation {} trial {}".format(self.simulation.name, env.id)
):
engine = create_engine(f"sqlite:///{self.dbpath}", echo=False)
dc = env.datacollector
for (t, df) in get_dc_dfs(dc, trial_id=env.id):
df.to_sql(t, con=engine, if_exists="append")
def get_dfs(self, env):
yield from get_dc_dfs(env.datacollector, trial_id=env.id)
def get_dc_dfs(dc, trial_id=None):
@ -129,6 +104,34 @@ def get_dc_dfs(dc, trial_id=None):
yield from dfs.items()
class default(Exporter):
"""Default exporter. Writes sqlite results, as well as the simulation YAML"""
def sim_start(self):
if self.dry_run:
logger.info("NOT dumping results")
return
logger.info("Dumping results to %s", self.outdir)
with self.output(self.simulation.name + ".dumped.yml") as f:
f.write(self.simulation.to_yaml())
self.dbpath = os.path.join(self.outdir, f"{self.simulation.name}.sqlite")
try_backup(self.dbpath, remove=True)
def trial_end(self, env):
if self.dry_run:
logger.info("Running in DRY_RUN mode, the database will NOT be created")
return
with timer(
"Dumping simulation {} trial {}".format(self.simulation.name, env.id)
):
engine = create_engine(f"sqlite:///{self.dbpath}", echo=False)
for (t, df) in self.get_dfs(env):
df.to_sql(t, con=engine, if_exists="append")
class csv(Exporter):
"""Export the state of each environment (and its agents) in a separate CSV file"""
@ -139,7 +142,7 @@ class csv(Exporter):
self.simulation.name, env.id, self.outdir
)
):
for (df_name, df) in get_dc_dfs(env.datacollector, trial_id=env.id):
for (df_name, df) in self.get_dfs(env):
with self.output("{}.{}.csv".format(env.id, df_name)) as f:
df.to_csv(f)
@ -192,52 +195,14 @@ class graphdrawing(Exporter):
f.savefig(f)
"""
Convert an environment into a NetworkX graph
"""
class summary(Exporter):
"""Print a summary of each trial to sys.stdout"""
def env_to_graph(env, history=None):
G = nx.Graph(env.G)
for agent in env.network_agents:
attributes = {"agent": str(agent.__class__)}
lastattributes = {}
spells = []
lastvisible = False
laststep = None
if not history:
history = sorted(list(env.state_to_tuples()))
for _, t_step, attribute, value in history:
if attribute == "visible":
nowvisible = value
if nowvisible and not lastvisible:
laststep = t_step
if not nowvisible and lastvisible:
spells.append((laststep, t_step))
lastvisible = nowvisible
def trial_end(self, env):
for (t, df) in self.get_dfs(env):
if not len(df):
continue
key = "attr_" + attribute
if key not in attributes:
attributes[key] = list()
if key not in lastattributes:
lastattributes[key] = (value, t_step)
elif lastattributes[key][0] != value:
last_value, laststep = lastattributes[key]
commit_value = (last_value, laststep, t_step)
if key not in attributes:
attributes[key] = list()
attributes[key].append(commit_value)
lastattributes[key] = (value, t_step)
for k, v in lastattributes.items():
attributes[k].append((v[0], v[1], None))
if lastvisible:
spells.append((laststep, None))
if spells:
G.add_node(agent.id, spells=spells, **attributes)
else:
G.add_node(agent.id, **attributes)
return G
msg = indent(str(df.describe()), ' ')
logger.info(dedent(f'''
Dataframe {t}:
''') + msg)

View File

@ -21,7 +21,6 @@ import pickle
from . import serialization, exporters, utils, basestring, agents
from .environment import Environment
from .utils import logger, run_and_return_exceptions
from .time import INFINITY
from .config import Config, convert_old
@ -194,7 +193,7 @@ class Simulation:
# Set up agents on nodes
def is_done():
return False
return not model.running
if until and hasattr(model.schedule, "time"):
prev = is_done
@ -226,6 +225,9 @@ Model stats:
f'Simulation time {model.schedule.time}/{until}. Next: {getattr(model.schedule, "next_time", model.schedule.time + self.interval)}'
)
model.step()
if model.schedule.time < until: # Simulation ended (no more steps) before until (i.e., no changes expected)
model.schedule.time = until
return model
def to_dict(self):

View File

@ -2,6 +2,10 @@ from mesa.time import BaseScheduler
from queue import Empty
from heapq import heappush, heappop, heapify
import math
from inspect import getsource
from numbers import Number
from .utils import logger
from mesa import Agent as MesaAgent
@ -15,9 +19,55 @@ class When:
return time
self._time = time
def abs(self, time):
def next(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, time):
return self._time <= time
class Cond(When):
def __init__(self, func, delta=1):
self._func = func
self._delta = delta
def next(self, time):
return time + self._delta
def abs(self, time):
return self
def ready(self, time):
return self._func(time)
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)
@ -27,11 +77,19 @@ class Delta(When):
self._delta = delta
def __eq__(self, other):
return self._delta == other._delta
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 __repr__(self):
return str(f"Delta({self._delta})")
class TimedActivation(BaseScheduler):
"""A scheduler which activates each agent when the agent requests.
@ -47,14 +105,15 @@ class TimedActivation(BaseScheduler):
def add(self, agent: MesaAgent, when=None):
if when is None:
when = self.time
when = When(self.time)
elif not isinstance(when, When):
when = When(when)
if agent.unique_id in self._agents:
self._queue.remove((self._next[agent.unique_id], agent.unique_id))
self._queue.remove((self._next[agent.unique_id], agent))
del self._agents[agent.unique_id]
heapify(self._queue)
heappush(self._queue, (when, agent.unique_id))
self._next[agent.unique_id] = when
heappush(self._queue, (when, agent))
super().add(agent)
def step(self) -> None:
@ -63,42 +122,61 @@ class TimedActivation(BaseScheduler):
an agent will signal when it wants to be scheduled next.
"""
self.logger.debug(f"Simulation step {self.next_time}")
self.logger.debug(f"Simulation step {self.time}")
if not self.model.running:
return
self.time = self.next_time
when = self.time
when = NEVER
while self._queue and self._queue[0][0] == self.time:
(when, agent_id) = heappop(self._queue)
self.logger.debug(f"Stepping agent {agent_id}")
to_process = []
skipped = []
next_time = INFINITY
agent = self._agents[agent_id]
returned = agent.step()
ix = 0
while self._queue:
(when, agent) = self._queue[0]
if when > self.time:
break
heappop(self._queue)
if when.ready(self.time):
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}")
returned = ((agent.step() or Delta(1))).abs(self.time)
if not getattr(agent, "alive", True):
self.remove(agent)
continue
when = (returned or Delta(1)).abs(self.time)
if when < self.time:
raise Exception(
"Cannot schedule an agent for a time in the past ({} < {})".format(
when, self.time
)
)
value = when.next(self.time)
self._next[agent_id] = when
heappush(self._queue, (when, agent_id))
if value < self.time:
raise Exception(
f"Cannot schedule an agent for a time in the past ({when} < {self.time})"
)
if value < INFINITY:
next_time = min(value, next_time)
self._next[agent.unique_id] = returned
heappush(self._queue, (returned, agent))
self.steps += 1
self.logger.debug(f"Updating time step: {self.time} -> {next_time}")
self.time = next_time
if not self._queue:
self.time = INFINITY
self.next_time = INFINITY
if not self._queue or next_time == INFINITY:
self.model.running = False
return self.time
self.next_time = self._queue[0][0]
self.logger.debug(f"Next step: {self.next_time}")

View File

@ -47,7 +47,7 @@ def timer(name="task", pre="", function=logger.info, to_object=None):
to_object.end = end
def try_backup(path, move=False):
def try_backup(path, remove=False):
if not os.path.exists(path):
return None
outdir = os.path.dirname(path)

View File

@ -18,7 +18,7 @@ class TestMain(TestCase):
d = Dead(unique_id=0, model=environment.Environment())
ret = d.step().abs(0)
print(ret, "next")
assert ret == stime.INFINITY
assert ret == stime.NEVER
def test_die_raises_exception(self):
'''A dead agent should raise an exception if it is stepped after death'''

View File

@ -50,7 +50,6 @@ class Exporters(TestCase):
for env in s.run_simulation(exporters=[Dummy], dry_run=True):
assert len(env.agents) == 1
assert env.now == max_time
assert Dummy.started
assert Dummy.ended