# -*- coding: utf-8 -*-
"""
"""
import numpy as np
import math
from numba import jit
import random
__author__ = "Jon-Mikkel Korsvik & Petter Bøe Hørtvedt"
__email__ = "jonkors@nmbu.no & petterho@nmbu.no"
[docs]@jit
def fitness_calculation(
phi_age, age, a_half,
phi_weight, weight, w_half
):
r"""
Calculates fitness by sigmoid multiplication
.. math::
\Phi =\left\{\begin{matrix}0
& , w\leq 0 & \\
q^-_{weight}*q^+_{age}&, else &
\end{matrix}\right.
.. math::
q^\pm(x, x_{\frac{1}{2}},\phi)=\frac{1}{1+e^{\pm\phi(x-x_\frac{1}{2})}}
Parameters
----------
phi_age : float
age : int
a_half : float
phi_weight : float
weight : float
w_half : float
Returns
-------
float
Value between 0 and 1 representing fitness
"""
pos_q_age = phi_age * (age - a_half)
neg_q_weight = - (phi_weight * (weight - w_half))
return 1/(1 + math.exp(pos_q_age)) * 1/(1 + math.exp(neg_q_weight))
class BaseAnimal:
"""
Baseclass for all animals
Methods
-------
set_parameters: class method
__init__
__repr__
age_one_year
reset_has_moved
will_migrate
birth
death
feed
lose_weight
"""
w_birth = 8.0
sigma_birth = 1.5
beta = 0.9
eta = 0.05
a_half = 40
phi_age = 0.2
w_half = 10
phi_weight = 0.1
mu = 0.25
lambda_ = 1.0
gamma = 0.2
zeta = 3.5
xi = 1.2
omega = 0.4
F = 10.0
@classmethod
def set_parameters(cls, w_birth=None, sigma_birth=None, beta=None,
eta=None, a_half=None, phi_age=None, w_half=None,
phi_weight=None, mu=None, lambda_=None, gamma=None,
zeta=None, xi=None, omega=None, F=None,
DeltaPhiMax=None):
"""
Method for changing one or all parameters with a dictionary for
subclass of BaseAnimal class
Does not change any parameters before it is sure that all
parameters are valid.
Parameters
----------
w_birth : float
Average birth weight
sigma_birth : float
STD of birth weight
beta : float
Fodder to weight conversion
eta : float
Weight loss scalar
a_half : float
Half age of Animals
phi_age : float
Scalar of age for fitness
w_half : float
Half weight of Animals
phi_weight : float
Scalar of weight for fitness
mu : float
Scalar for moving is multiplied with fitness
lambda_ : float
Scalar for propensity calculation
gamma : float
Scalar for birth
zeta : float
Scalar if birth will happen
xi : float
Scalar for weight loss after birth
omega : float
Scalar for death
F : float
Appetite of Animal
DeltaPhiMax : float
Parameter used by Carnivore when calculating if they can kill
an Animal
Returns
-------
"""
# By checking all parameters first, set parameters does not change
# any parameters before it is sure that all parameters are valid
# If I can, I should make this smaller.
bool_w_birth = False
bool_sigma_birth = False
bool_beta = False
bool_eta = False
bool_a_half = False
bool_phi_age = False
bool_w_half = False
bool_phi_weight = False
bool_mu = False
bool_lambda_ = False
bool_gamma = False
bool_zeta = False
bool_xi = False
bool_omega = False
bool_F = False
bool_DeltaPhiMax = False
if w_birth:
if w_birth >= 0:
bool_w_birth = True
else:
raise ValueError('w_birth takes positive int or float '
'arguments only')
if sigma_birth:
if sigma_birth >= 0:
bool_sigma_birth = True
else:
raise ValueError('sigma_birth takes positive int or float '
'arguments only')
if beta:
if beta >= 0:
bool_beta = True
else:
raise ValueError('beta takes positive int or float '
'arguments only')
if eta:
if 1 >= eta >= 0:
bool_eta = True
else:
raise ValueError('eta takes int or float '
'arguments 0 <= eta <= 1 only')
if a_half:
if a_half >= 0:
bool_a_half = True
else:
raise ValueError('a_half takes positive int or float '
'arguments only')
if phi_age:
if phi_age >= 0:
bool_phi_age = True
else:
raise ValueError('phi_age takes positive int or float '
'arguments only')
if w_half:
if w_half >= 0:
bool_w_half = True
else:
raise ValueError('w_half takes positive int or float '
'arguments only')
if phi_weight:
if phi_weight >= 0:
bool_phi_weight = True
else:
raise ValueError('phi_weight takes positive int or float '
'arguments only')
if mu:
if mu >= 0:
bool_mu = True
else:
raise ValueError('mu takes positive int or float '
'arguments only')
if lambda_:
if lambda_ >= 0:
bool_lambda_ = True
else:
raise ValueError('lambda_ takes positive int or float '
'arguments only')
if gamma:
if gamma >= 0:
bool_gamma = True
else:
raise ValueError('gamma takes positive int or float '
'arguments only')
if zeta:
if zeta >= 0:
bool_zeta = True
else:
raise ValueError('zeta takes positive int or float '
'arguments only')
if xi:
if xi >= 0:
bool_xi = True
else:
raise ValueError('xi takes positive int or float '
'arguments only')
if omega:
if omega >= 0:
bool_omega = True
else:
raise ValueError('omega takes positive int or float '
'arguments only')
if F:
if F >= 0:
bool_F = True
else:
raise ValueError('F takes positive int or float '
'arguments only')
if DeltaPhiMax:
if DeltaPhiMax > 0:
bool_DeltaPhiMax = True
else:
raise ValueError('DeltaPhiMax takes strictly positive int or '
'float arguments only')
if bool_w_birth is True:
cls.w_birth = w_birth
if bool_sigma_birth is True:
cls.sigma_birth = sigma_birth
if bool_beta is True:
cls.beta = beta
if bool_eta is True:
cls.eta = eta
if bool_a_half is True:
cls.a_half = a_half
if bool_phi_age is True:
cls.phi_age = phi_age
if bool_w_half is True:
cls.w_half = w_half
if bool_phi_weight is True:
cls.phi_weight = phi_weight
if bool_mu is True:
cls.mu = mu
if bool_lambda_ is True:
cls.lambda_ = lambda_
if bool_gamma is True:
cls.gamma = gamma
if bool_zeta is True:
cls.zeta = zeta
if bool_xi is True:
cls.xi = xi
if bool_omega is True:
cls.omega = omega
if bool_F is True:
cls.F = F
if bool_DeltaPhiMax is True:
cls.DeltaPhiMax = DeltaPhiMax
def __init__(self, age=0, weight=None):
"""
Initialises instance, calculates weight with a gaussian
distribution if weight is not specified.
Parameters
----------
age : int
weight : float
Attributes
--------
self._age : int
self._weight : float
self._compute_fitness : bool
self._fitness : float
Between 0 and 1
self._has_moved : bool
"""
self._age = age
self._weight = weight
self._compute_fitness = True
self._fitness = None
self._has_moved = False
if weight is None:
normal = random.gauss(self.w_birth, self.sigma_birth)
self.weight = normal
if normal < 0:
self.weight = 0 # newborns with <= 0 will die end of year
def __repr__(self):
"""How the instance presents itself if called"""
string = f"Animal Type: {type(self).__name__}\n" \
f"Age: {self.age}\n" \
f"Weight: {self.weight}\n" \
f"Fitness: {self.fitness}\n"
return string
@property
def fitness(self):
"""
Calculates fitness if weight or age is changed, else return old value
Returns
-------
self._fitness : float
"""
if self._compute_fitness is True:
if self.weight <= 0:
return 0
self._compute_fitness = False
self._fitness = fitness_calculation(
self.phi_age, self.age, self.a_half,
self.phi_weight, self.weight, self.w_half)
return self._fitness
return self._fitness
def age_one_year(self):
"""Adds an increment of 1 to age"""
self.age += 1
@property
def age(self):
"""Getter for age"""
return self._age
@age.setter
def age(self, new_age):
"""Sets age to new value and compute fitness to true"""
self._compute_fitness = True
self._age = new_age
@property
def weight(self):
"""Getter for weight"""
return self._weight
@weight.setter
def weight(self, new_weight):
"""Sets weight to new value and compute fitness to true"""
self._compute_fitness = True
self._weight = new_weight
@property
def has_moved(self):
"""
Gives back bool value and sets the moved statement to True
Returns
-------
moved : bool
"""
moved = self._has_moved
self._has_moved = True
return moved
def reset_has_moved(self):
"""Set has moved to False"""
self._has_moved = False
def will_migrate(self):
"""
Animals that has not moved yet will calculate if they will move
Returns
-------
bool
True if the animal wil move
"""
if not self.has_moved:
prob_to_move = self.fitness * self.mu
return bool(random.random() < prob_to_move)
return False
def birth(self, num_same_species):
"""
Whether or not an animal will give birth, weight loss updated, returns
offspring
Parameters
----------
num_same_species : int
Number of same animals of age >= 1 in the same cell
Returns
-------
offspring : object
Instance of a new animal of same type as "mother"
with default age 0 and default weight None.
"""
if self.age <= 0:
return 0
mates = num_same_species - 1
prob_to_birth = np.minimum(1, (self.gamma * self.fitness * mates))
if self.weight < self.zeta*(self.w_birth + self.phi_weight):
return 0
if random.random() < prob_to_birth:
offspring = type(self)()
weight_loss = self.xi * offspring.weight
if self.weight >= weight_loss:
self.weight -= weight_loss
return offspring
return 0
def death(self):
"""
Calculates if animal dies by probability.
If fitness = 0, the animal will die regardless
Returns
-------
bool
True if animal dies
"""
prob_to_die = self.omega*(1-self.fitness)
dies = random.random() < prob_to_die
return bool(dies) or self.fitness <= 0
def feed(self, available_food): # Overwritten by carnivores
"""
Eats food in cell, updates weight and returns new amount of fodder
left
Parameters
----------
available_food : float
Food in current cell
Returns
-------
float
Remaining fodder in the cell
"""
if self.F <= available_food:
self.weight += self.beta * self.F
return available_food - self.F
if 0 < available_food:
self.weight += self.beta * available_food
return 0
def lose_weight(self):
"""Yearly passive weight loss"""
self.weight -= self.eta*self.weight
[docs]class Herbivore(BaseAnimal):
w_birth = 8.0
sigma_birth = 1.5
beta = 0.9
eta = 0.05
a_half = 40
phi_age = 0.2
w_half = 10
phi_weight = 0.1
mu = 0.25
lambda_ = 1.0
gamma = 0.2
zeta = 3.5
xi = 1.2
omega = 0.4
F = 10.0
def __init__(self, age=0, weight=None):
"""
Subclass of BaseAnimal, has it's own set of class parameters.
Parameters
----------
age : int
weight : float
"""
super().__init__(age, weight)
[docs]class Carnivore(BaseAnimal):
w_birth = 6.0
sigma_birth = 1.0
beta = 0.75
eta = 0.125
a_half = 60.0
phi_age = 0.4
w_half = 4.0
phi_weight = 0.4
mu = 0.4
lambda_ = 1.0
gamma = 0.8
zeta = 3.5
xi = 1.1
omega = 0.9
F = 50.0
DeltaPhiMax = 10.0
def __init__(self, age=0, weight=None):
"""
Subclass of BaseAnimal, has it's own set of class parameters.
Overwrites feed (eats other animals)
Parameters
----------
age: int
weight: float
Methods
-------
kill_or_not
eat
feed
"""
super().__init__(age, weight)
[docs] def kill_or_not(self, herbivore):
"""
Calculates if carnivore will kill herbivore
Parameters
----------
herbivore : object
Returns
-------
bool
whether or not the herbivore was killed
"""
probability_to_kill = ((self.fitness - herbivore.fitness) /
self.DeltaPhiMax)
return bool(random.random() < probability_to_kill)
[docs] def eat(self, meat, eaten):
"""Consumes herbivore, updates weight, will not eat more than F"""
if meat + eaten < self.F:
self.weight += self.beta * meat
else:
self.weight += self.beta*(self.F - eaten)
[docs] def feed(self, list_herbivores_least_fit):
"""
Iterates list of herbivores then tries to kill them.
Cannot eat animals with greater fitness than themselves.
Stops feeding when F(appetite) is met.
Creates deletion list with Herbivores, then removes them
and returns updated list of herbivores.
Parameters
----------
list_herbivores_least_fit : list
Herbivores in ascending order by fitness
Returns
-------
list_herbivores_least_fit : list
The same list as input, killed herbivores removed
"""
eaten = 0
deletion_list = []
for herbivore in list_herbivores_least_fit:
if eaten >= self.F:
break
if self.fitness <= herbivore.fitness:
break # kunne breaka
if self.DeltaPhiMax < self.fitness - herbivore.fitness:
self.eat(herbivore.weight, eaten)
eaten += herbivore.weight
deletion_list.append(herbivore)
else:
if self.kill_or_not(herbivore):
self.eat(herbivore.weight, eaten)
eaten += herbivore.weight
deletion_list.append(herbivore)
for herbivore in deletion_list:
list_herbivores_least_fit.remove(herbivore)
return list_herbivores_least_fit
if __name__ == '__main__':
pass