diff --git a/CHANGELOG.md b/CHANGELOG.md index eefbb44..bb7a34f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.20.3] +### Fixed +* Default state values are now deepcopied again. +* Seeds for environments only concatenate the trial id (i.e., a number), to provide repeatable results. +* `Environment.run` now calls `Environment.step`, to allow for easy overloading of the environment step +### Removed +* Datacollectors are not being used for now. +* `time.TimedActivation.step` does not use an `until` parameter anymore. +### Changed +* Simulations now run right up to `until` (open interval) +* Time instants (`time.When`) don't need to be floats anymore. Now we can avoid precision issues with big numbers by using ints. +* Rabbits simulation is more idiomatic (using subclasses) + ## [0.20.2] ### Fixed * CI/CD testing issues diff --git a/examples/rabbits/rabbit_agents.py b/examples/rabbits/rabbit_agents.py index 056be93..a8e6028 100644 --- a/examples/rabbits/rabbit_agents.py +++ b/examples/rabbits/rabbit_agents.py @@ -34,6 +34,17 @@ class RabbitModel(FSM): if self['age'] >= self.sexual_maturity: self.debug('I am fertile!') return self.fertile + @state + def fertile(self): + raise Exception("Each subclass should define its fertile state") + + @state + def dead(self): + self.info('Agent {} is dying'.format(self.id)) + self.die() + + +class Male(RabbitModel): @state def fertile(self): @@ -45,20 +56,26 @@ class RabbitModel(FSM): return # Males try to mate - for f in self.get_agents(state_id=self.fertile.id, gender=Genders.female.value, limit_neighbors=False, limit=self.max_females): + for f in self.get_agents(state_id=Female.fertile.id, + agent_type=Female, + limit_neighbors=False, + limit=self.max_females): r = random() if r < self['mating_prob']: self.impregnate(f) break # Take a break - def impregnate(self, whom): - if self['gender'] == Genders.female.value: - raise NotImplementedError('Females cannot impregnate') whom['pregnancy'] = 0 whom['mate'] = self.id whom.set_state(whom.pregnant) self.debug('{} impregnating: {}. {}'.format(self.id, whom.id, whom.state)) +class Female(RabbitModel): + @state + def fertile(self): + # Just wait for a Male + pass + @state def pregnant(self): self['age'] += 1 @@ -88,11 +105,9 @@ class RabbitModel(FSM): @state def dead(self): - self.info('Agent {} is dying'.format(self.id)) + super().dead() if 'pregnancy' in self and self['pregnancy'] > -1: self.info('A mother has died carrying a baby!!') - self.die() - return class RandomAccident(NetworkAgent): diff --git a/examples/rabbits/rabbits.yml b/examples/rabbits/rabbits.yml index 16fd125..a78a5db 100644 --- a/examples/rabbits/rabbits.yml +++ b/examples/rabbits/rabbits.yml @@ -4,9 +4,9 @@ name: rabbits_example max_time: 100 interval: 1 seed: MySeed -agent_type: RabbitModel +agent_type: rabbit_agents.RabbitModel environment_agents: - - agent_type: RandomAccident + - agent_type: rabbit_agents.RandomAccident environment_params: prob_death: 0.001 default_state: @@ -14,10 +14,8 @@ default_state: topology: nodes: - id: 1 - state: - gender: female + agent_type: rabbit_agents.Male - id: 0 - state: - gender: male + agent_type: rabbit_agents.Female directed: true links: [] diff --git a/soil/VERSION b/soil/VERSION index 9d26321..3428dd4 100644 --- a/soil/VERSION +++ b/soil/VERSION @@ -1 +1 @@ -0.20.1 \ No newline at end of file +0.20.3 \ No newline at end of file diff --git a/soil/__init__.py b/soil/__init__.py index c02d744..dc79354 100644 --- a/soil/__init__.py +++ b/soil/__init__.py @@ -65,6 +65,10 @@ def main(): logger.info('Loading config file: {}'.format(args.file)) + if args.pdb: + args.synchronous = True + + try: exporters = list(args.exporter or ['default', ]) if args.csv: diff --git a/soil/environment.py b/soil/environment.py index cead20b..6df44be 100644 --- a/soil/environment.py +++ b/soil/environment.py @@ -169,11 +169,12 @@ class Environment(Model): if agent_type: state = defstate a = agent_type(model=self, - unique_id=agent_id) + unique_id=agent_id + ) for (k, v) in getattr(a, 'defaults', {}).items(): if not hasattr(a, k) or getattr(a, k) is None: - setattr(a, k, v) + setattr(a, k, deepcopy(v)) for (k, v) in state.items(): setattr(a, k, v) @@ -199,15 +200,15 @@ class Environment(Model): def step(self): super().step() - self.datacollector.collect(self) self.schedule.step() def run(self, until, *args, **kwargs): self._save_state() - while self.schedule.next_time <= until and not math.isinf(self.schedule.next_time): - self.schedule.step(until=until) + while self.schedule.next_time < until: + self.step() utils.logger.debug(f'Simulation step {self.schedule.time}/{until}. Next: {self.schedule.next_time}') + self.schedule.time = until self._history.flush_cache() def _save_state(self, now=None): diff --git a/soil/simulation.py b/soil/simulation.py index 5aa6374..427adb8 100644 --- a/soil/simulation.py +++ b/soil/simulation.py @@ -145,9 +145,7 @@ class Simulation: def _run_sync_or_async(self, parallel=False, *args, **kwargs): if parallel and not os.environ.get('SENPY_DEBUG', None): p = Pool() - func = partial(self.run_trial_exceptions, - *args, - **kwargs) + func = lambda x: self.run_trial_exceptions(trial_id=x, *args, **kwargs) for i in p.imap_unordered(func, range(self.num_trials)): if isinstance(i, Exception): logger.error('Trial failed:\n\t%s', i.message) @@ -155,7 +153,8 @@ class Simulation: yield i else: for i in range(self.num_trials): - yield self.run_trial(*args, + yield self.run_trial(trial_id=i, + *args, **kwargs) def run_gen(self, *args, parallel=False, dry_run=False, @@ -224,7 +223,7 @@ class Simulation: '''Create an environment for a trial of the simulation''' opts = self.environment_params.copy() opts.update({ - 'name': trial_id, + 'name': '{}_trial_{}'.format(self.name, trial_id), 'topology': self.topology.copy(), 'network_params': self.network_params, 'seed': '{}_trial_{}'.format(self.seed, trial_id), @@ -241,12 +240,11 @@ class Simulation: env = self.environment_class(**opts) return env - def run_trial(self, until=None, log_level=logging.INFO, **opts): + def run_trial(self, trial_id=0, until=None, log_level=logging.INFO, **opts): """ Run a single trial of the simulation """ - trial_id = '{}_trial_{}'.format(self.name, time.time()).replace('.', '-') if log_level: logger.setLevel(log_level) # Set-up trial environment and graph diff --git a/soil/time.py b/soil/time.py index 52ed2eb..889c7c8 100644 --- a/soil/time.py +++ b/soil/time.py @@ -6,9 +6,11 @@ from .utils import logger from mesa import Agent +INFINITY = float('inf') + class When: def __init__(self, time): - self._time = float(time) + self._time = time def abs(self, time): return self._time @@ -40,48 +42,34 @@ class TimedActivation(BaseScheduler): heappush(self._queue, (self.time, agent.unique_id)) super().add(agent) - def step(self, until: float =float('inf')) -> None: + 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. """ - when = None - agent_id = None - unsched = [] - until = until or float('inf') + if self.next_time == INFINITY: + return + + self.time = self.next_time + when = self.time + + while self._queue and self._queue[0][0] == self.time: + (when, agent_id) = heappop(self._queue) + logger.debug(f'Stepping agent {agent_id}') + + when = (self._agents[agent_id].step() 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)) + + heappush(self._queue, (when, agent_id)) + + self.steps += 1 if not self._queue: - self.time = until - self.next_time = float('inf') + self.time = INFINITY + self.next_time = INFINITY return - (when, agent_id) = self._queue[0] + self.next_time = self._queue[0][0] - if until and when > until: - self.time = until - self.next_time = when - return - - self.time = when - next_time = float("inf") - - while when == self.time: - heappop(self._queue) - logger.debug(f'Stepping agent {agent_id}') - when = (self._agents[agent_id].step() or Delta(1)).abs(self.time) - heappush(self._queue, (when, agent_id)) - if when < next_time: - next_time = when - - if not self._queue or self._queue[0][0] > self.time: - agent_id = None - break - else: - (when, agent_id) = self._queue[0] - - if when and when < self.time: - raise Exception("Invalid scheduling time") - - self.next_time = next_time - self.steps += 1 diff --git a/tests/test_main.py b/tests/test_main.py index 349c3e3..d7dc58c 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -127,7 +127,7 @@ class TestMain(TestCase): env = s.run_simulation(dry_run=True)[0] for agent in env.network_agents: last = 0 - assert len(agent[None, None]) == 11 + assert len(agent[None, None]) == 10 for step, total in sorted(agent['total', None]): assert total == last + 2 last = total