Source code for src.biosim.simulation

# -*- coding: utf-8 -*-

"""
"""

__author__ = "Jon-Mikkel Korsvik & Petter Bøe Hørtvedt"
__email__ = "jonkors@nmbu.no & petterho@nmbu.no"


from .island import Island
from .visualization import Visuals
from .landscape import (
    Jungle, Ocean, Savanna, Mountain, Desert
)
from .animals import Herbivore, Carnivore
import textwrap
import pandas as pd
import numpy as np
import subprocess
import random
import pickle
import os


FFMPEG = os.path.join(os.path.dirname(__file__), '../../FFMPEG/ffmpeg.exe')

# Retrieved from:
# https://stackoverflow.com/questions/19201290/how-to-save-a-dictionary-to-a-file/32216025


[docs]def save_sim(simulation, name): """ Save a state of Simulation Parameters ---------- simulation : object Instance of Simulation name : str Save name Returns ------- """ with open(name + '.pkl', 'wb') as f: pickle.dump(simulation, f, pickle.HIGHEST_PROTOCOL)
[docs]def load_sim(name): """ Loads state of Simulation Parameters ---------- name : file Name of file Returns ------- Loaded file of Simulation """ with open(name + '.pkl', 'rb') as f: return pickle.load(f)
[docs]class BioSim: default_map = """ OOOOOOOOOOOOOOOOOOOOO OSSSSSJJJJMMJJJJJJJOO OSSSSSJJJJMMJJJJJJJOO OSSSSSJJJJMMJJJJJJJOO OOSSJJJJJJJMMJJJJJJJO OOSSJJJJJJJMMJJJJJJJO OOOOOOOOSMMMMJJJJJJJO OSSSSSJJJJMMJJJJJJJOO OSSSSSSSSSMMJJJJJJOOO OSSSSSDDDDDJJJJJJJOOO OSSSSSDDDDDJJJJJJJOOO OSSSSSDDDDDJJJJJJJOOO OSSSSSDDDDDMMJJJJJOOO OSSSSDDDDDDJJJJOOOOOO OOSSSSDDDDDDJOOOOOOOO OOSSSSDDDDDJJJOOOOOOO OSSSSSDDDDDJJJJJJJOOO OSSSSDDDDDDJJJJOOOOOO OOSSSSDDDDDJJJOOOOOOO OOOSSSSJJJJJJJOOOOOOO OOOSSSSSSOOOOOOOOOOOO OOOOOOOOOOOOOOOOOOOOO """ default_map = textwrap.dedent(default_map) default_population = [ { "loc": (10, 10), "pop": [ {"species": "Herbivore", "age": 5, "weight": 20} for _ in range(150) ], }, { "loc": (10, 10), "pop": [ {"species": "Carnivore", "age": 5, "weight": 20} for _ in range(40) ], } ] def __init__( self, island_map=None, ini_pop=None, seed=None, ymax_animals=None, cmax_animals=None, img_base=None, img_fmt="png", movie_fmt="mp4", island_save_name=None, store_stats=False ): """ :param island_map: Multi-line string specifying island geography :param ini_pop: List of dictionaries specifying initial population :param seed: Integer used as random number seed :param ymax_animals: Number specifying y-axis limit for graph showing animal numbers :param cmax_animals: Dict specifying color-code limits for animal densities :param img_base: String with beginning of file name for figures, including path :param img_fmt: String with file type for figures, e.g. 'png' :param island_save_name: Name of previously saved game, directory is already defined within the project. :param store_stats: boolean statement, wether to store all dead and born animals overtime for analysis If ymax_animals is None, the y-axis limit should be adjusted automatically. If cmax_animals is None, sensible, fixed default values should be used. cmax_animals is a dict mapping species names to numbers, e.g., {'Herbivore': 50, 'Carnivore': 20} If img_base is None, no figures are written to file. Filenames are formed as '{}_{:05d}.{}'.format(img_base, img_no, img_fmt) where img_no are consecutive image numbers starting from 0. img_base should contain a path and beginning of a file name. """ if ini_pop is None: ini_pop = self.default_population if island_save_name is None: if island_map is None: self.island = Island(self.default_map, ini_pop, store_stats) else: self.island = Island(island_map, ini_pop, store_stats) else: self.island = load_sim(island_save_name) if seed is not None: np.random.seed(seed) random.seed(seed) self.ymax_animals = ymax_animals self.cmax_animals = cmax_animals self.img_base = img_base self.img_fmt = img_fmt self.movie_fmt = movie_fmt
[docs] @staticmethod def set_animal_parameters(species, params): """ Set parameters for animal species. :param species: String, name of animal species :param params: Dict with valid parameter specification for species """ animal_species = {'Herbivore': Herbivore, 'Carnivore': Carnivore} animal_species[species].set_parameters(**params)
[docs] @staticmethod def set_landscape_parameters(landscape, params): """ Set parameters for landscape type. :param landscape: String, code letter for landscape :param params: Dict with valid parameter specification for landscape """ map_params = {'O': Ocean, 'M': Mountain, 'D': Desert, 'S': Savanna, 'J': Jungle} map_params[landscape].set_parameters(**params)
[docs] def clean_simulation(self, num_years): """ A simulation without any visualization. Parameters ---------- num_years : int Returns ------- """ index = 1 while index <= num_years: self.island.simulate_one_year() index += 1
[docs] def simulate(self, num_years, vis_years=1, img_years=None): """ Run simulation while visualizing the result. :param num_years: number of years to simulate :param vis_years: years between visualization updates :param img_years: years between visualizations saved to files (default: vis_years) Image files will be numbered consecutively. """ visuals = Visuals(self.island, num_years, self.ymax_animals, self.cmax_animals, self.img_base, self.img_fmt) if img_years is None: img_years = vis_years if img_years % vis_years != 0: raise ValueError('img_years must be a multiple of vis_years') if self.img_base is not None: visuals.save_fig() index = 1 while index <= num_years: self.island.simulate_one_year() if index % vis_years == 0: visuals.update_fig(self.island) if self.img_base is not None: if index % img_years == 0: visuals.save_fig() index += 1
[docs] def add_population(self, population): """ Add a population to the island Calls function from Island.py :param population: List of dictionaries specifying population """ self.island.add_population(population)
@property def year(self): """Last year simulated.""" return self.island.year @property def num_animals(self): """Total number of animals on island.""" return self.island.num_animals @property def num_animals_per_species(self): """Number of animals per species in island, as dictionary.""" return self.island.num_animals_per_species @property def animal_distribution(self, save_name=None): """Pandas DataFrame with animal count per species for each cell on island.""" dict_for_df = {"Row": [], "Col": [], "Herbivore": [], "Carnivore": []} for pos, cell in self.island.map.items(): row, col = pos dict_for_df["Row"].append(row) dict_for_df["Col"].append(col) dict_for_df["Herbivore"].append(cell.num_herbivores) dict_for_df["Carnivore"].append(cell.num_carnivores) df_sim = pd.DataFrame.from_dict(dict_for_df) if save_name is not None: df_sim.to_csv(save_name + '.csv') return df_sim
[docs] def island_stats(self): """ Creates dictionary for birth and death rate. Can be used as an example for extracting data from stats. Returns ------- death_rate_per_year_herb : dict key : year, value : float death_rate_per_year_carn : dict key : year, value : float birth_rate_per_year_herb : dict key : year, value : float birth_rate_per_year_carn : dict key : year, value : float """ death_rate_per_year_herb = {} death_rate_per_year_carn = {} birth_rate_per_year_herb = {} birth_rate_per_year_carn = {} for year, value in self.island.stats.items(): deaths_herb = 0 deaths_carn = 0 born_herb = 0 born_carn = 0 death_per_pos_herb = value['Herbivore']['death'] death_per_pos_carn = value['Carnivore']['death'] birth_per_pos_herb = value['Herbivore']['birth'] birth_per_pos_carn = value['Carnivore']['birth'] N_herbivores = value['Herbivore']['alive'] for dead in death_per_pos_herb.values(): deaths_herb += len(dead) death_rate_per_year_herb[year] = deaths_herb / N_herbivores for born in birth_per_pos_herb.values(): born_herb += len(born) birth_rate_per_year_herb[year] = born_herb / N_herbivores N_carnivores = value['Carnivore']['alive'] if N_carnivores <= 0: continue for dead in death_per_pos_carn.values(): deaths_carn += len(dead) death_rate_per_year_carn[year] = deaths_carn / N_carnivores for born in birth_per_pos_carn.values(): born_carn += len(born) birth_rate_per_year_carn[year] = born_carn / N_carnivores return ( death_rate_per_year_herb, death_rate_per_year_carn, birth_rate_per_year_herb, birth_rate_per_year_carn )
[docs] def make_movie(self): """Create MPEG4 movie from visualization images saved.""" if self.img_base is None: raise RuntimeError("No filename defined.") if self.movie_fmt == 'mp4': try: subprocess.check_call(f'{FFMPEG} -y -r 8 -i ' f'{self.img_base}_%05d.{self.img_fmt}' f' -c:v libx264 -vf fps=25 -pix_fmt ' f'yuv420p ' f'-start_number 0 ' f'{self.img_base}.{self.movie_fmt}') except subprocess.CalledProcessError as err: raise RuntimeError(f'ERROR: ffmpeg failed with: {err}') else: raise ValueError('Unknown movie format: ' + self.movie_fmt)
[docs] def save_sim(self, name): """Calls function: save_sim outside Simulation""" save_sim(self.island, name)
if __name__ == '__main__': pass