Source code for blossom.simulation.organism

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]