mirror of
https://github.com/gsi-upm/soil
synced 2025-09-14 20:22:22 +00:00
Compare commits
3 Commits
0.20.2.pos
...
0.20.4
Author | SHA1 | Date | |
---|---|---|---|
|
5559d37e57 | ||
|
2116fe6f38 | ||
|
affeeb9643 |
17
CHANGELOG.md
17
CHANGELOG.md
@@ -3,6 +3,23 @@ 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.4]
|
||||
### Added
|
||||
* Agents can now be given any kwargs, which will be used to set their state
|
||||
* Environments have a default logger `self.logger` and a log method, just like agents
|
||||
## [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
|
||||
|
@@ -1 +1 @@
|
||||
ipython==7.23
|
||||
ipython==7.31.1
|
||||
|
@@ -17,7 +17,7 @@ class DumbViewer(FSM):
|
||||
def neutral(self):
|
||||
if self['has_tv']:
|
||||
if prob(self.env['prob_tv_spread']):
|
||||
self.set_state(self.infected)
|
||||
return self.infected
|
||||
|
||||
@state
|
||||
def infected(self):
|
||||
@@ -26,6 +26,12 @@ class DumbViewer(FSM):
|
||||
neighbor.infect()
|
||||
|
||||
def infect(self):
|
||||
'''
|
||||
This is not a state. It is a function that other agents can use to try to
|
||||
infect this agent. DumbViewer always gets infected, but other agents like
|
||||
HerdViewer might not become infected right away
|
||||
'''
|
||||
|
||||
self.set_state(self.infected)
|
||||
|
||||
|
||||
@@ -35,12 +41,13 @@ class HerdViewer(DumbViewer):
|
||||
'''
|
||||
|
||||
def infect(self):
|
||||
'''Notice again that this is NOT a state. See DumbViewer.infect for reference'''
|
||||
infected = self.count_neighboring_agents(state_id=self.infected.id)
|
||||
total = self.count_neighboring_agents()
|
||||
prob_infect = self.env['prob_neighbor_spread'] * infected/total
|
||||
self.debug('prob_infect', prob_infect)
|
||||
if prob(prob_infect):
|
||||
self.set_state(self.infected.id)
|
||||
self.set_state(self.infected)
|
||||
|
||||
|
||||
class WiseViewer(HerdViewer):
|
||||
@@ -75,5 +82,5 @@ class WiseViewer(HerdViewer):
|
||||
1.0)
|
||||
prob_cure = self.env['prob_neighbor_cure'] * (cured/infected)
|
||||
if prob(prob_cure):
|
||||
return self.cure()
|
||||
return self.cured
|
||||
return self.set_state(super().infected)
|
||||
|
@@ -18,7 +18,9 @@ class MyAgent(agents.FSM):
|
||||
@agents.default_state
|
||||
@agents.state
|
||||
def neutral(self):
|
||||
self.info('I am running')
|
||||
self.debug('I am running')
|
||||
if agents.prob(0.2):
|
||||
self.info('This runs 2/10 times on average')
|
||||
|
||||
|
||||
s = Simulation(name='Programmatic',
|
||||
@@ -29,10 +31,10 @@ s = Simulation(name='Programmatic',
|
||||
dry_run=True)
|
||||
|
||||
|
||||
# By default, logging will only print WARNING logs (and above).
|
||||
# You need to choose a lower logging level to get INFO/DEBUG traces
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
envs = s.run()
|
||||
|
||||
s.dump_yaml()
|
||||
|
||||
for env in envs:
|
||||
env.dump_csv()
|
||||
# Uncomment this to output the simulation to a YAML file
|
||||
# s.dump_yaml('simulation.yaml')
|
||||
|
@@ -12,8 +12,6 @@ class Genders(Enum):
|
||||
|
||||
class RabbitModel(FSM):
|
||||
|
||||
level = logging.INFO
|
||||
|
||||
defaults = {
|
||||
'age': 0,
|
||||
'gender': Genders.male.value,
|
||||
@@ -36,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):
|
||||
@@ -47,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
|
||||
@@ -90,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):
|
||||
|
@@ -1,23 +1,21 @@
|
||||
---
|
||||
load_module: rabbit_agents
|
||||
name: rabbits_example
|
||||
max_time: 150
|
||||
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:
|
||||
mating_prob: 0.01
|
||||
mating_prob: 0.1
|
||||
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: []
|
||||
|
@@ -1 +1 @@
|
||||
0.20.1
|
||||
0.20.4
|
@@ -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:
|
||||
|
@@ -35,7 +35,9 @@ class BaseAgent(Agent):
|
||||
unique_id,
|
||||
model,
|
||||
name=None,
|
||||
interval=None):
|
||||
interval=None,
|
||||
**kwargs
|
||||
):
|
||||
# Check for REQUIRED arguments
|
||||
# Initialize agent parameters
|
||||
if isinstance(unique_id, Agent):
|
||||
@@ -52,7 +54,8 @@ class BaseAgent(Agent):
|
||||
|
||||
if hasattr(self, 'level'):
|
||||
self.logger.setLevel(self.level)
|
||||
|
||||
for (k, v) in kwargs.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
# TODO: refactor to clean up mesa compatibility
|
||||
@property
|
||||
|
@@ -5,6 +5,7 @@ import math
|
||||
import random
|
||||
import yaml
|
||||
import tempfile
|
||||
import logging
|
||||
import pandas as pd
|
||||
from time import time as current_time
|
||||
from copy import deepcopy
|
||||
@@ -101,6 +102,8 @@ class Environment(Model):
|
||||
environment_agents = agents._convert_agent_types(distro)
|
||||
self.environment_agents = environment_agents
|
||||
|
||||
self.logger = utils.logger.getChild(self.name)
|
||||
|
||||
@property
|
||||
def now(self):
|
||||
if self.schedule:
|
||||
@@ -169,11 +172,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)
|
||||
@@ -197,17 +201,29 @@ class Environment(Model):
|
||||
start = start or self.now
|
||||
return self.G.add_edge(agent1, agent2, **attrs)
|
||||
|
||||
def log(self, message, *args, level=logging.INFO, **kwargs):
|
||||
if not self.logger.isEnabledFor(level):
|
||||
return
|
||||
message = message + " ".join(str(i) for i in args)
|
||||
message = " @{:>3}: {}".format(self.now, message)
|
||||
for k, v in kwargs:
|
||||
message += " {k}={v} ".format(k, v)
|
||||
extra = {}
|
||||
extra['now'] = self.now
|
||||
extra['unique_id'] = self.name
|
||||
return self.logger.log(level, message, extra=extra)
|
||||
|
||||
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):
|
||||
|
@@ -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
|
||||
|
60
soil/time.py
60
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
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user