mirror of
https://github.com/gsi-upm/soil
synced 2024-11-24 11:52:29 +00:00
Add conditional time values
This commit is contained in:
parent
77d08fc592
commit
5d759d0072
@ -169,7 +169,7 @@ class BaseEnvironment(Model):
|
|||||||
Advance one step in the simulation, and update the data collection and scheduler appropriately
|
Advance one step in the simulation, and update the data collection and scheduler appropriately
|
||||||
"""
|
"""
|
||||||
super().step()
|
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.schedule.step()
|
||||||
self.datacollector.collect(self)
|
self.datacollector.collect(self)
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import sys
|
|||||||
from time import time as current_time
|
from time import time as current_time
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
|
from textwrap import dedent, indent
|
||||||
|
|
||||||
|
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
@ -86,34 +87,8 @@ class Exporter:
|
|||||||
pass
|
pass
|
||||||
return open_or_reuse(f, mode=mode, **kwargs)
|
return open_or_reuse(f, mode=mode, **kwargs)
|
||||||
|
|
||||||
|
def get_dfs(self, env):
|
||||||
class default(Exporter):
|
yield from get_dc_dfs(env.datacollector, trial_id=env.id)
|
||||||
"""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_dc_dfs(dc, trial_id=None):
|
def get_dc_dfs(dc, trial_id=None):
|
||||||
@ -129,6 +104,34 @@ def get_dc_dfs(dc, trial_id=None):
|
|||||||
yield from dfs.items()
|
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):
|
class csv(Exporter):
|
||||||
|
|
||||||
"""Export the state of each environment (and its agents) in a separate CSV file"""
|
"""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
|
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:
|
with self.output("{}.{}.csv".format(env.id, df_name)) as f:
|
||||||
df.to_csv(f)
|
df.to_csv(f)
|
||||||
|
|
||||||
@ -192,52 +195,14 @@ class graphdrawing(Exporter):
|
|||||||
f.savefig(f)
|
f.savefig(f)
|
||||||
|
|
||||||
|
|
||||||
"""
|
class summary(Exporter):
|
||||||
Convert an environment into a NetworkX graph
|
"""Print a summary of each trial to sys.stdout"""
|
||||||
"""
|
|
||||||
|
|
||||||
|
def trial_end(self, env):
|
||||||
def env_to_graph(env, history=None):
|
for (t, df) in self.get_dfs(env):
|
||||||
G = nx.Graph(env.G)
|
if not len(df):
|
||||||
|
|
||||||
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
|
|
||||||
continue
|
continue
|
||||||
key = "attr_" + attribute
|
msg = indent(str(df.describe()), ' ')
|
||||||
if key not in attributes:
|
logger.info(dedent(f'''
|
||||||
attributes[key] = list()
|
Dataframe {t}:
|
||||||
if key not in lastattributes:
|
''') + msg)
|
||||||
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
|
|
||||||
|
@ -21,7 +21,6 @@ import pickle
|
|||||||
from . import serialization, exporters, utils, basestring, agents
|
from . import serialization, exporters, utils, basestring, agents
|
||||||
from .environment import Environment
|
from .environment import Environment
|
||||||
from .utils import logger, run_and_return_exceptions
|
from .utils import logger, run_and_return_exceptions
|
||||||
from .time import INFINITY
|
|
||||||
from .config import Config, convert_old
|
from .config import Config, convert_old
|
||||||
|
|
||||||
|
|
||||||
@ -194,7 +193,7 @@ class Simulation:
|
|||||||
|
|
||||||
# Set up agents on nodes
|
# Set up agents on nodes
|
||||||
def is_done():
|
def is_done():
|
||||||
return False
|
return not model.running
|
||||||
|
|
||||||
if until and hasattr(model.schedule, "time"):
|
if until and hasattr(model.schedule, "time"):
|
||||||
prev = is_done
|
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)}'
|
f'Simulation time {model.schedule.time}/{until}. Next: {getattr(model.schedule, "next_time", model.schedule.time + self.interval)}'
|
||||||
)
|
)
|
||||||
model.step()
|
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
|
return model
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
|
134
soil/time.py
134
soil/time.py
@ -2,6 +2,10 @@ from mesa.time import BaseScheduler
|
|||||||
from queue import Empty
|
from queue import Empty
|
||||||
from heapq import heappush, heappop, heapify
|
from heapq import heappush, heappop, heapify
|
||||||
import math
|
import math
|
||||||
|
|
||||||
|
from inspect import getsource
|
||||||
|
from numbers import Number
|
||||||
|
|
||||||
from .utils import logger
|
from .utils import logger
|
||||||
from mesa import Agent as MesaAgent
|
from mesa import Agent as MesaAgent
|
||||||
|
|
||||||
@ -15,9 +19,55 @@ class When:
|
|||||||
return time
|
return time
|
||||||
self._time = time
|
self._time = time
|
||||||
|
|
||||||
def abs(self, time):
|
def next(self, time):
|
||||||
return 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)
|
NEVER = When(INFINITY)
|
||||||
|
|
||||||
@ -27,11 +77,19 @@ class Delta(When):
|
|||||||
self._delta = delta
|
self._delta = delta
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
|
if isinstance(other, Delta):
|
||||||
return self._delta == other._delta
|
return self._delta == other._delta
|
||||||
|
return False
|
||||||
|
|
||||||
def abs(self, time):
|
def abs(self, time):
|
||||||
|
return When(self._delta + time)
|
||||||
|
|
||||||
|
def next(self, time):
|
||||||
return time + self._delta
|
return time + self._delta
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return str(f"Delta({self._delta})")
|
||||||
|
|
||||||
|
|
||||||
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.
|
||||||
@ -47,14 +105,15 @@ class TimedActivation(BaseScheduler):
|
|||||||
|
|
||||||
def add(self, agent: MesaAgent, when=None):
|
def add(self, agent: MesaAgent, when=None):
|
||||||
if when is 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:
|
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]
|
del self._agents[agent.unique_id]
|
||||||
heapify(self._queue)
|
heapify(self._queue)
|
||||||
|
|
||||||
heappush(self._queue, (when, agent.unique_id))
|
heappush(self._queue, (when, agent))
|
||||||
self._next[agent.unique_id] = when
|
|
||||||
super().add(agent)
|
super().add(agent)
|
||||||
|
|
||||||
def step(self) -> None:
|
def step(self) -> None:
|
||||||
@ -63,42 +122,61 @@ class TimedActivation(BaseScheduler):
|
|||||||
an agent will signal when it wants to be scheduled next.
|
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:
|
if not self.model.running:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.time = self.next_time
|
when = NEVER
|
||||||
when = self.time
|
|
||||||
|
|
||||||
while self._queue and self._queue[0][0] == self.time:
|
to_process = []
|
||||||
(when, agent_id) = heappop(self._queue)
|
skipped = []
|
||||||
self.logger.debug(f"Stepping agent {agent_id}")
|
next_time = INFINITY
|
||||||
|
|
||||||
agent = self._agents[agent_id]
|
ix = 0
|
||||||
returned = agent.step()
|
|
||||||
|
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):
|
if not getattr(agent, "alive", True):
|
||||||
self.remove(agent)
|
self.remove(agent)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
when = (returned or Delta(1)).abs(self.time)
|
value = when.next(self.time)
|
||||||
if when < self.time:
|
|
||||||
raise Exception(
|
|
||||||
"Cannot schedule an agent for a time in the past ({} < {})".format(
|
|
||||||
when, self.time
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
self._next[agent_id] = when
|
if value < self.time:
|
||||||
heappush(self._queue, (when, agent_id))
|
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.steps += 1
|
||||||
|
self.logger.debug(f"Updating time step: {self.time} -> {next_time}")
|
||||||
|
self.time = next_time
|
||||||
|
|
||||||
if not self._queue:
|
if not self._queue or next_time == INFINITY:
|
||||||
self.time = INFINITY
|
|
||||||
self.next_time = INFINITY
|
|
||||||
self.model.running = False
|
self.model.running = False
|
||||||
return self.time
|
return self.time
|
||||||
|
|
||||||
self.next_time = self._queue[0][0]
|
|
||||||
self.logger.debug(f"Next step: {self.next_time}")
|
|
||||||
|
@ -47,7 +47,7 @@ def timer(name="task", pre="", function=logger.info, to_object=None):
|
|||||||
to_object.end = end
|
to_object.end = end
|
||||||
|
|
||||||
|
|
||||||
def try_backup(path, move=False):
|
def try_backup(path, remove=False):
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
return None
|
return None
|
||||||
outdir = os.path.dirname(path)
|
outdir = os.path.dirname(path)
|
||||||
|
@ -18,7 +18,7 @@ class TestMain(TestCase):
|
|||||||
d = Dead(unique_id=0, model=environment.Environment())
|
d = Dead(unique_id=0, model=environment.Environment())
|
||||||
ret = d.step().abs(0)
|
ret = d.step().abs(0)
|
||||||
print(ret, "next")
|
print(ret, "next")
|
||||||
assert ret == stime.INFINITY
|
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'''
|
||||||
|
@ -50,7 +50,6 @@ class Exporters(TestCase):
|
|||||||
|
|
||||||
for env in s.run_simulation(exporters=[Dummy], dry_run=True):
|
for env in s.run_simulation(exporters=[Dummy], dry_run=True):
|
||||||
assert len(env.agents) == 1
|
assert len(env.agents) == 1
|
||||||
assert env.now == max_time
|
|
||||||
|
|
||||||
assert Dummy.started
|
assert Dummy.started
|
||||||
assert Dummy.ended
|
assert Dummy.ended
|
||||||
|
Loading…
Reference in New Issue
Block a user