import uuid
import copy
import imp
import sys
import numpy as np
from . import default_fields
from .utils import cast_to_list
from .organism_behavior import movement, reproduction, drinking, eating, action
[docs]
class Organism(object):
"""
A basic organism structure for all species.
"""
def __init__(self, init_dict={}, seed=None):
"""
Create a new organism from a dictary of parameters. The dictionary
is specified in blossom.default_fields.
"""
# Set up defaults based on organism parameters
for (field, default) in default_fields.organism_fields.items():
setattr(self, field, init_dict.get(field, default))
# Set up custom fields provided in initialization dictionary
init_keys = set(init_dict.keys())
default_keys = set(default_fields.organism_fields.keys())
for custom_field in (init_keys - default_keys):
setattr(self, custom_field, init_dict[custom_field])
# Set unique id for organism
if self.organism_id is None:
self.organism_id = self.get_new_id(seed=seed)
# Set current water level for uninitialized organism
if self.drinking_type is not None and self.water_current is None:
self.water_current = self.water_initial
# Set current food level for uninitialized organism
if self.eating_type is not None and self.food_current is None:
self.food_current = self.food_initial
# Import custom modules / paths
if self.custom_module_fns is not None:
self._custom_modules = []
for i, path in enumerate(cast_to_list(self.custom_module_fns)):
temp_module = imp.load_source('%s' % i, path)
self._custom_modules.append(temp_module)
[docs]
def to_dict(self):
"""
Convert Organism to dict.
"""
organism_vars = vars(self)
public_vars = {key: val
for key, val in organism_vars.items()
if not key.startswith('_')}
return public_vars
[docs]
def get_new_id(self, seed=None):
"""
Generates pseudo-random ID for the organism, seeded by the universe.
"""
rng = np.random.default_rng(seed)
return str(uuid.UUID(bytes=rng.bytes(16)))
[docs]
@classmethod
def clone(cls, organism):
"""
Makes a new Organism object identical to the current one.
Parameters
----------
organism : Organism
Organism to copy.
Returns
-------
new_organism : Organism
Copied organism.
"""
new_organism = cls(organism.to_dict())
# Use copy module to properly handle mutable lists
new_organism.ancestry = copy.copy(new_organism.ancestry)
new_organism.location = copy.copy(new_organism.location)
return new_organism
[docs]
def clone_self(self):
"""
Clone this organism.
"""
return self.clone(self)
[docs]
def get_child(self, other_parent=None, seed=None):
"""
Creates an Organism object with similar properties to self, and can
add another parent if it exists. Note that this doesn't assume
anything about how much food / water the child is left with, so these
should be set with custom / default reproduction methods.
Parameters
----------
other_parent : Organism
Parent that reproduces with self to produce the child.
Returns
-------
child : Organism
Generated child.
"""
child = self.clone_self()
child.age = 0
child.organism_id = child.get_new_id(seed=seed)
if other_parent is None:
child.ancestry.append(self.organism_id)
else:
child.ancestry.append([self.organism_id, other_parent.organism_id])
child.last_action = None
return child
[docs]
def update_parameter(self, parameter, value, method='set', in_place=False, original=None):
"""
Update a specific parameter of the organism.
Parameters
----------
parameter : string
Parameter to update.
value
Value with which to update.
method : string
Method types are: 'set', 'add', 'subtract', 'append'.
in_place : bool
If True, modifies self, otherwise, copy organism and return new
Organism object.
original : Organism or None
Original organism we are changing. If it is the original,
clone organism so that we aren't editing the original.
Returns
-------
updated_organism : Organism
Organism object with updated parameter.
"""
if self is original or not in_place:
updated_organism = self.clone_self()
else:
updated_organism = self
attribute = getattr(updated_organism, parameter)
if method == 'set':
attribute = value
elif method == 'add':
attribute += value
elif method == 'subtract':
attribute -= value
elif method == 'append':
attribute.append(value)
else:
raise ValueError('Invalid update method!')
setattr(updated_organism, parameter, attribute)
return updated_organism
[docs]
def move(self, universe):
"""
Method for handling movement. Searches through custom methods and
built-in movement methods.
Parameters
----------
universe : Universe
Universe containing organism
Returns
-------
affected_organisms : Organisms, or list of Organisms
Organism or list of organisms affected by this organism's movement.
"""
try:
if self.movement_type is None:
raise ValueError('No movement type defined!')
elif self.custom_module_fns is not None:
for custom_module in self._custom_modules:
if hasattr(custom_module, self.movement_type):
return getattr(custom_module, self.movement_type)(
self,
universe
)
return getattr(movement, self.movement_type)(
self,
universe
)
except AttributeError as e:
raise AttributeError(
str(e)
+ '. Check that \'%s\' method is indeed ' % self.movement_type
+ 'contained within the provided custom modules and that the '
+ 'method is written correctly.'
).with_traceback(sys.exc_info()[2])
[docs]
def reproduce(self, universe):
"""
Method for handling reproduction. Searches through custom methods
and built-in reproduction methods.
Parameters
----------
universe : Universe
Universe containing organism
Returns
-------
affected_organisms : Organisms, or list of Organisms
Organism or list of organisms affected by this organism's
reproduction. For example, this would include both parent and
child organisms.
"""
try:
if self.reproduction_type is None:
raise ValueError('No reproduction type defined!')
elif self.custom_module_fns is not None:
for custom_module in self._custom_modules:
if hasattr(custom_module, self.reproduction_type):
return getattr(custom_module, self.reproduction_type)(
self,
universe
)
return getattr(reproduction, self.reproduction_type)(
self,
universe
)
except AttributeError as e:
raise AttributeError(
str(e) + '. Check '
+ 'that \'%s\' method is indeed ' % self.reproduction_type
+ 'contained within the provided custom modules and that the '
+ 'method is written correctly.'
).with_traceback(sys.exc_info()[2])
[docs]
def drink(self, universe):
"""
Method for handling drinking. Searches through custom methods and
built-in drinking methods.
Parameters
----------
universe : Universe
Universe containing organism
Returns
-------
affected_organisms : Organisms, or list of Organisms
Organism or list of organisms affected by this organism's drinking.
"""
try:
if self.drinking_type is None:
raise ValueError('No drinking type defined!')
elif self.custom_module_fns is not None:
for custom_module in self._custom_modules:
if hasattr(custom_module, self.drinking_type):
return getattr(custom_module, self.drinking_type)(
self,
universe
)
return getattr(drinking, self.drinking_type)(
self,
universe
)
except AttributeError as e:
raise AttributeError(
str(e)
+ '. Check that \'%s\' method is indeed ' % self.drinking_type
+ 'contained within the provided custom modules and that the '
+ 'method is written correctly.'
).with_traceback(sys.exc_info()[2])
[docs]
def eat(self, universe):
"""
Method for handling eating. Searches through custom methods and
built-in eating methods.
Parameters
----------
universe : Universe
Universe containing organism
Returns
-------
affected_organisms : Organisms, or list of Organisms
Organism or list of organisms affected by this organism's eating.
"""
try:
if self.eating_type is None:
raise ValueError('No eating type defined!')
elif self.custom_module_fns is not None:
for custom_module in self._custom_modules:
if hasattr(custom_module, self.eating_type):
return getattr(custom_module, self.eating_type)(
self,
universe
)
return getattr(eating, self.eating_type)(
self,
universe
)
except AttributeError as e:
raise AttributeError(
str(e)
+ '. Check that \'%s\' method is indeed ' % self.eating_type
+ 'contained within the provided custom modules and that the '
+ 'method is written correctly.'
).with_traceback(sys.exc_info()[2])
[docs]
def act(self, universe):
"""
Method that decides and calls an action for the current timestep.
Searches through custom methods and built-in movement methods.
The action method specifically selects an action to take, from "move",
"reproduce", "drink", and "eat". Then the appropriate instance method
from this class is executed to yield the final list of affect
organisms.
Parameters
----------
universe : Universe
Universe containing organism
Returns
-------
affected_organisms : Organisms, or list of Organisms
Organism or list of organisms affected by this organism's action.
"""
action_name = None
try:
if self.custom_module_fns is not None:
for custom_module in self._custom_modules:
if hasattr(custom_module, self.action_type):
action_name = getattr(custom_module, self.action_type)(
self,
universe
)
if action_name is None:
action_name = getattr(action, self.action_type)(
self,
universe
)
except AttributeError as e:
raise AttributeError(
str(e)
+ '. Check that \'%s\' method is indeed ' % self.action_type
+ 'contained within the provided custom modules and that the '
+ 'method is written correctly.'
).with_traceback(sys.exc_info()[2])
self.last_action = action_name
affected_organisms = cast_to_list(getattr(self, action_name)(
universe
))
# Ensure this organism is included in affected_organisms
already_included = False
for org in affected_organisms:
if org.organism_id == self.organism_id:
already_included = True
if not already_included:
# e.g. unknown reason for exlusion, so default to death
self.die('unknown', in_place=True)
affected_organisms.append(self)
return self, affected_organisms
def _update_age(self):
"""
Increments age by 1.
"""
self.age += 1
return self
def _update_water(self):
"""
Updates health parameters relevant to water consumption.
Decreases current water level based on metabolism, and increments
time without water accordingly. Note that organisms die of thirst if
this reaches the maximum time without water.
"""
self.water_current -= self.water_metabolism
if self.water_current > self.water_capacity:
self.water_current = self.water_capacity
elif self.water_current <= 0:
self.water_current = 0
self.time_without_water += 1
elif self.time_without_water > 0:
self.time_without_water = 0
return self
def _update_food(self):
"""
Updates health parameters relevant to food consumption.
Decreases current food level based on metabolism, and increments
time without food accordingly. Note that organisms die of hunger if
this reaches the maximum time without food.
"""
self.food_current -= self.food_metabolism
if self.food_current > self.food_capacity:
self.food_current = self.food_capacity
elif self.food_current <= 0:
self.food_current = 0
self.time_without_food += 1
elif self.time_without_food > 0:
self.time_without_food = 0
return self
[docs]
def is_at_death(self, cause):
"""
Check various conditions for death.
Parameters
----------
cause : str
Potential cause of this organism's death.
Returns
-------
is_dead : bool
Returns True if organism is dead from the specified cause, False
otherwise.
"""
if cause == 'old_age':
return self.age > self.max_age
elif cause == 'thirst':
return (self.drinking_type is not None
and self.time_without_water > self.max_time_without_water)
elif cause == 'hunger':
return (self.eating_type is not None
and self.time_without_food > self.max_time_without_food)
else:
raise ValueError('Invalid cause!')
[docs]
def die(self, cause, in_place=False, original=None):
"""
Method that "kills" organism.
Parameters
----------
cause : str
Cause of this organism's death.
in_place : bool
If True, modifies self, otherwise, copy organism and return new
Organism object.
Returns
-------
dead_organism : Organism
New "dead" state of this organism.
"""
updated_organism = self.update_parameter('alive',
False,
in_place=in_place,
original=original)
updated_organism = updated_organism.update_parameter('age_at_death',
self.age,
in_place=in_place,
original=original)
updated_organism = updated_organism.update_parameter('cause_of_death',
cause,
in_place=in_place,
original=original)
return updated_organism
[docs]
def step(self, universe, do_action=True):
"""
Steps through one time step for this organism. Reflects changes
based on actions / behaviors and updates to health parameters.
Returns a list of organisms that the action produced (either new or
altered organisms).
Parameters
----------
universe : Universe
Universe containing organism
do_action: bool
If True, this organism will act, otherwise, it will not.
Returns
-------
affected_organisms : list of Organisms
List of organisms affected by this organism's actions
or health. This could be an updated version of this organism,
especially if the organism dies during the time step, but could
also be multiple other organisms affected by actions (i.e. children
from reproduction).
"""
# Create new Organism object / reference and update age
organism = self.clone_self()._update_age()
if organism.is_at_death('old_age'):
# This organism is at death from old age
# Design choice to prevent action if organism is at old age limit
return [organism.die('old_age')]
if do_action:
organism, affected_organisms = organism.act(universe)
else:
affected_organisms = [organism]
for organism in affected_organisms:
# Checks whether alive after action
if organism.alive:
if organism.is_at_death('old_age'):
organism.die('old_age', in_place=True)
# Update and check water / food status
if organism.drinking_type is not None:
organism._update_water()
if organism.is_at_death('thirst'):
organism.die('thirst', in_place=True)
if organism.eating_type is not None:
organism._update_food()
if organism.is_at_death('hunger'):
organism.die('hunger', in_place=True)
return affected_organisms
[docs]
def step_without_acting(self):
"""
Steps through one time step without acting for this organism.
Returns
-------
organism
Note that this returns an Organism object, not a list.
"""
return self.step(universe=None, do_action=False)[0]