1
0
mirror of https://github.com/gsi-upm/soil synced 2025-08-24 03:52:20 +00:00

WIP: all tests pass

This commit is contained in:
J. Fernando Sánchez
2022-10-13 22:43:16 +02:00
parent f811ee18c5
commit cd62c23cb9
46 changed files with 1720 additions and 1434 deletions

View File

@@ -0,0 +1,4 @@
There are two similar implementations of this simulation.
- `basic`. Using simple primites
- `improved`. Using more advanced features such as the `time` module to avoid unnecessary computations (i.e., skip steps), and generator functions.

View File

@@ -0,0 +1,130 @@
from soil.agents import FSM, state, default_state, BaseAgent, NetworkAgent
from soil.time import Delta
from enum import Enum
from collections import Counter
import logging
import math
class RabbitModel(FSM, NetworkAgent):
sexual_maturity = 30
life_expectancy = 300
@default_state
@state
def newborn(self):
self.info('I am a newborn.')
self.age = 0
self.offspring = 0
return self.youngling
@state
def youngling(self):
self.age += 1
if self.age >= self.sexual_maturity:
self.info(f'I am fertile! My age is {self.age}')
return self.fertile
@state
def fertile(self):
raise Exception("Each subclass should define its fertile state")
@state
def dead(self):
self.die()
class Male(RabbitModel):
max_females = 5
mating_prob = 0.001
@state
def fertile(self):
self.age += 1
if self.age > self.life_expectancy:
return self.dead
# Males try to mate
for f in self.model.agents(agent_class=Female,
state_id=Female.fertile.id,
limit=self.max_females):
self.debug('FOUND A FEMALE: ', repr(f), self.mating_prob)
if self.prob(self['mating_prob']):
f.impregnate(self)
break # Take a break
class Female(RabbitModel):
gestation = 100
@state
def fertile(self):
# Just wait for a Male
self.age += 1
if self.age > self.life_expectancy:
return self.dead
def impregnate(self, male):
self.info(f'{repr(male)} impregnating female {repr(self)}')
self.mate = male
self.pregnancy = -1
self.set_state(self.pregnant, when=self.now)
self.number_of_babies = int(8+4*self.random.random())
self.debug('I am pregnant')
@state
def pregnant(self):
self.age += 1
self.pregnancy += 1
if self.prob(self.age / self.life_expectancy):
return self.die()
if self.pregnancy >= self.gestation:
self.info('Having {} babies'.format(self.number_of_babies))
for i in range(self.number_of_babies):
state = {}
agent_class = self.random.choice([Male, Female])
child = self.model.add_node(agent_class=agent_class,
topology=self.topology,
**state)
child.add_edge(self)
try:
child.add_edge(self.mate)
self.model.agents[self.mate].offspring += 1
except ValueError:
self.debug('The father has passed away')
self.offspring += 1
self.mate = None
return self.fertile
@state
def dead(self):
super().dead()
if 'pregnancy' in self and self['pregnancy'] > -1:
self.info('A mother has died carrying a baby!!')
class RandomAccident(BaseAgent):
level = logging.INFO
def step(self):
rabbits_alive = self.model.topology.number_of_nodes()
if not rabbits_alive:
return self.die()
prob_death = self.model.get('prob_death', 1e-100)*math.floor(math.log10(max(1, rabbits_alive)))
self.debug('Killing some rabbits with prob={}!'.format(prob_death))
for i in self.iter_agents(agent_class=RabbitModel):
if i.state.id == i.dead.id:
continue
if self.prob(prob_death):
self.info('I killed a rabbit: {}'.format(i.id))
rabbits_alive -= 1
i.set_state(i.dead)
self.debug('Rabbits alive: {}'.format(rabbits_alive))

View File

@@ -0,0 +1,41 @@
---
version: '2'
name: rabbits_basic
num_trials: 1
seed: MySeed
description: null
group: null
interval: 1.0
max_time: 100
model_class: soil.environment.Environment
model_params:
agents:
topology: default
agent_class: rabbit_agents.RabbitModel
distribution:
- agent_class: rabbit_agents.Male
topology: default
weight: 1
- agent_class: rabbit_agents.Female
topology: default
weight: 1
fixed:
- agent_class: rabbit_agents.RandomAccident
topology: null
hidden: true
state:
group: environment
state:
group: network
mating_prob: 0.1
prob_death: 0.001
topologies:
default:
topology:
directed: true
links: []
nodes:
- id: 1
- id: 0
extra:
visualization_params: {}

View File

@@ -0,0 +1,130 @@
from soil.agents import FSM, state, default_state, BaseAgent, NetworkAgent
from soil.time import Delta, When, NEVER
from enum import Enum
import logging
import math
class RabbitModel(FSM, NetworkAgent):
mating_prob = 0.005
offspring = 0
birth = None
sexual_maturity = 3
life_expectancy = 30
@default_state
@state
def newborn(self):
self.birth = self.now
self.info(f'I am a newborn.')
self.model['rabbits_alive'] = self.model.get('rabbits_alive', 0) + 1
# Here we can skip the `youngling` state by using a coroutine/generator.
while self.age < self.sexual_maturity:
interval = self.sexual_maturity - self.age
yield Delta(interval)
self.info(f'I am fertile! My age is {self.age}')
return self.fertile
@property
def age(self):
return self.now - self.birth
@state
def fertile(self):
raise Exception("Each subclass should define its fertile state")
def step(self):
super().step()
if self.prob(self.age / self.life_expectancy):
return self.die()
class Male(RabbitModel):
max_females = 5
@state
def fertile(self):
# Males try to mate
for f in self.model.agents(agent_class=Female,
state_id=Female.fertile.id,
limit=self.max_females):
self.debug('Found a female:', repr(f))
if self.prob(self['mating_prob']):
f.impregnate(self)
break # Take a break, don't try to impregnate the rest
class Female(RabbitModel):
due_date = None
age_of_pregnancy = None
gestation = 10
mate = None
@state
def fertile(self):
return self.fertile, NEVER
@state
def pregnant(self):
self.info('I am pregnant')
if self.age > self.life_expectancy:
return self.dead
self.due_date = self.now + self.gestation
number_of_babies = int(8+4*self.random.random())
while self.now < self.due_date:
yield When(self.due_date)
self.info('Having {} babies'.format(number_of_babies))
for i in range(number_of_babies):
agent_class = self.random.choice([Male, Female])
child = self.model.add_node(agent_class=agent_class,
topology=self.topology)
self.model.add_edge(self, child)
self.model.add_edge(self.mate, child)
self.offspring += 1
self.model.agents[self.mate].offspring += 1
self.mate = None
self.due_date = None
return self.fertile
@state
def dead(self):
super().dead()
if self.due_date is not None:
self.info('A mother has died carrying a baby!!')
def impregnate(self, male):
self.info(f'{repr(male)} impregnating female {repr(self)}')
self.mate = male
self.set_state(self.pregnant, when=self.now)
class RandomAccident(BaseAgent):
level = logging.INFO
def step(self):
rabbits_total = self.model.topology.number_of_nodes()
if 'rabbits_alive' not in self.model:
self.model['rabbits_alive'] = 0
rabbits_alive = self.model.get('rabbits_alive', rabbits_total)
prob_death = self.model.get('prob_death', 1e-100)*math.floor(math.log10(max(1, rabbits_alive)))
self.debug('Killing some rabbits with prob={}!'.format(prob_death))
for i in self.model.network_agents:
if i.state.id == i.dead.id:
continue
if self.prob(prob_death):
self.info('I killed a rabbit: {}'.format(i.id))
rabbits_alive = self.model['rabbits_alive'] = rabbits_alive -1
i.set_state(i.dead)
self.debug('Rabbits alive: {}/{}'.format(rabbits_alive, rabbits_total))
if self.model.count_agents(state_id=RabbitModel.dead.id) == self.model.topology.number_of_nodes():
self.die()

View File

@@ -0,0 +1,41 @@
---
version: '2'
name: rabbits_improved
num_trials: 1
seed: MySeed
description: null
group: null
interval: 1.0
max_time: 100
model_class: soil.environment.Environment
model_params:
agents:
topology: default
agent_class: rabbit_agents.RabbitModel
distribution:
- agent_class: rabbit_agents.Male
topology: default
weight: 1
- agent_class: rabbit_agents.Female
topology: default
weight: 1
fixed:
- agent_class: rabbit_agents.RandomAccident
topology: null
hidden: true
state:
group: environment
state:
group: network
mating_prob: 0.1
prob_death: 0.001
topologies:
default:
topology:
directed: true
links: []
nodes:
- id: 1
- id: 0
extra:
visualization_params: {}

View File

@@ -1,133 +0,0 @@
from soil.agents import FSM, state, default_state, BaseAgent, NetworkAgent
from enum import Enum
import logging
import math
class Genders(Enum):
male = 'male'
female = 'female'
class RabbitModel(FSM, NetworkAgent):
defaults = {
'age': 0,
'gender': Genders.male.value,
'mating_prob': 0.001,
'offspring': 0,
}
sexual_maturity = 3 #4*30
life_expectancy = 365 * 3
gestation = 33
pregnancy = -1
max_females = 5
@default_state
@state
def newborn(self):
self.debug(f'I am a newborn at age {self["age"]}')
self['age'] += 1
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):
self['age'] += 1
if self['age'] > self.life_expectancy:
return self.dead
if self['gender'] == Genders.female.value:
return
# Males try to mate
for f in self.get_agents(state_id=Female.fertile.id,
agent_class=Female,
limit_neighbors=False,
limit=self.max_females):
r = self.random.random()
if r < self['mating_prob']:
self.impregnate(f)
break # Take a break
def impregnate(self, whom):
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
if self['age'] > self.life_expectancy:
return self.dead
self['pregnancy'] += 1
self.debug('Pregnancy: {}'.format(self['pregnancy']))
if self['pregnancy'] >= self.gestation:
number_of_babies = int(8+4*self.random.random())
self.info('Having {} babies'.format(number_of_babies))
for i in range(number_of_babies):
state = {}
state['gender'] = self.random.choice(list(Genders)).value
child = self.env.add_node(self.__class__, state)
self.env.add_edge(self.id, child.id)
self.env.add_edge(self['mate'], child.id)
# self.add_edge()
self.debug('A BABY IS COMING TO LIFE')
self.env['rabbits_alive'] = self.env.get('rabbits_alive', self.topology.number_of_nodes())+1
self.debug('Rabbits alive: {}'.format(self.env['rabbits_alive']))
self['offspring'] += 1
self.env.get_agent(self['mate'])['offspring'] += 1
del self['mate']
self['pregnancy'] = -1
return self.fertile
@state
def dead(self):
super().dead()
if 'pregnancy' in self and self['pregnancy'] > -1:
self.info('A mother has died carrying a baby!!')
class RandomAccident(BaseAgent):
level = logging.DEBUG
def step(self):
rabbits_total = self.env.topology.number_of_nodes()
if 'rabbits_alive' not in self.env:
self.env['rabbits_alive'] = 0
rabbits_alive = self.env.get('rabbits_alive', rabbits_total)
prob_death = self.env.get('prob_death', 1e-100)*math.floor(math.log10(max(1, rabbits_alive)))
self.debug('Killing some rabbits with prob={}!'.format(prob_death))
for i in self.env.network_agents:
if i.state['id'] == i.dead.id:
continue
if self.prob(prob_death):
self.debug('I killed a rabbit: {}'.format(i.id))
rabbits_alive = self.env['rabbits_alive'] = rabbits_alive -1
self.log('Rabbits alive: {}'.format(self.env['rabbits_alive']))
i.set_state(i.dead)
self.log('Rabbits alive: {}/{}'.format(rabbits_alive, rabbits_total))
if self.env.count_agents(state_id=RabbitModel.dead.id) == self.env.topology.number_of_nodes():
self.die()

View File

@@ -1,20 +0,0 @@
---
name: rabbits_example
max_time: 100
interval: 1
seed: MySeed
agent_class: rabbit_agents.RabbitModel
environment_agents:
- agent_class: rabbit_agents.RandomAccident
environment_params:
prob_death: 0.001
default_state:
mating_prob: 0.1
topology:
nodes:
- id: 1
agent_class: rabbit_agents.Male
- id: 0
agent_class: rabbit_agents.Female
directed: true
links: []