class TestIsland: @pytest.fixture(autouse=True) def create_island(self): """ Creating island object """ self.island = Island("""\ WWWWWWWW WLLLLLWW WHLLLLWW WLDDLLWW WHWWWWWW WWWWWWWW""") def test_get_cells(self): """ To test if the cells are being recognized correctly. In the island object we see that coordinate of (1,1) is Lowland. We'll tests this below """ assert type(self.island._cells[1, 1]).__name__ is 'Lowland' def test_annual_cycle(self, mocker): """ To test during an annual cycle, the animal either eats, procreates, ages, migrates and dies or not """ ini_herbs = [{ 'loc': (2, 3), 'pop': [{ 'species': 'Herbivore', 'age': 5, 'weight': 20 } for _ in range(100)] }] ini_carns = [{ 'loc': (2, 4), 'pop': [{ 'species': 'Carnivore', 'age': 5, 'weight': 20 } for _ in range(50)] }] self.island.place_animals(ini_herbs + ini_carns) mocker.patch('numpy.random.random', return_value=0) # To be assured that migration, death # and procreation happen num_animals_before_cycle = self.island.total_num_animals_per_species('Herbivore') + \ self.island.total_num_animals_per_species('Carnivore') self.island.annual_cycle() num_animals_after_cycle = self.island.total_num_animals_per_species('Herbivore') + \ self.island.total_num_animals_per_species('Carnivore') assert num_animals_after_cycle is not num_animals_before_cycle
class BioSim: """ BioSim Class used interface class for the simulations """ def __init__(self, island_map, seed, ini_pop, ymax_animals=None, cmax_animals=None, hist_specs=None, img_base=None, img_fmt="png"): np.random.seed(seed) self._animal_species = {'Carnivore': Carnivore, 'Herbivore': Herbivore} self._landscapes_with_changeable_parameters = { 'H': Highland, 'L': Lowland } self._island_map = island_map self._island = Island(island_map) self.add_population(ini_pop) self._vis = None self._fig = None self._final_year = None self._year = 0 self.maximum = 0 if ymax_animals is None: # the y-axis limit should be adjusted automatically. # matplotlib.pyplot.autoscale(enable=True, axis='both', tight=None) self.ymax_animals = 17000 # or call a function to get max num animals updated? else: self.ymax_animals = ymax_animals if cmax_animals is None: self._cmax_animals = {'Herbivore': 5, 'Carnivore': 5} else: self._cmax_animals = cmax_animals if img_base is None: self._img_base = _DEFAULT_GRAPHICS_DIR + _DEFAULT_GRAPHICS_NAME else: self._img_base = img_base self._img_ctr = 0 self._img_fmt = img_fmt """ :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 hist_specs: Specifications for histograms, see below :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' 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} hist_specs is a dictionary with one entry per property for which a histogram shall be shown. For each property, a dictionary providing the maximum value and the bin width must be given, e.g., {'weight': {'max': 80, 'delta': 2}, 'fitness': {'max': 1.0, 'delta': 0.05}} Permitted properties are 'weight', 'age', 'fitness'. 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. """ def set_animal_parameters(self, species, params): """ Sets given parameters for corresponding animal species class. Parameters ---------- species: str Name of animal species params: dict With valid parameter specification for species """ if species in self._animal_species: species_class = self._animal_species[species] animal = species_class() animal.set_given_parameters(params) else: raise TypeError(species + ' parameters can\'t be assigned, ' 'there is no such data type') def set_landscape_parameters(self, landscape, params): """ Set parameters for landscape type. Parameters ---------- landscape: String, code letter for landscape params: Dict with valid parameter specification for landscape """ if landscape in self._landscapes_with_changeable_parameters: landscape = self._landscapes_with_changeable_parameters[landscape] land = landscape() land.set_given_parameters(params) else: raise TypeError(landscape + 'parameters can not be assigned') def simulate(self, num_years, vis_years=1, img_years=None): """ Run simulation while visualizing the result. Parameters ---------- num_years: number of years to simulate vis_years: years between visualization updates img_years: years between visualizations saved to files (default: vis_years) Image files will be numbered consecutively. """ if img_years is None: img_years = vis_years self._final_year = self._year + num_years self._setup_graphics() while self._year < self._final_year: if self._year % vis_years == 0: self._update_graphics() if self._year % img_years == 0: self._save_graphics() self._island.annual_cycle() self._year += 1 def _save_to_csv(self): pass # df = self._animal_distribution # df.to_csv('../results/data.csv', sep='\t', encoding='utf-8') """ Saves animal_distribution as CSV file """ df = self._animal_distribution df.to_csv('../results/data.csv', sep='\t', encoding='utf-8') def y_max(self): """ Maximum number of animals """ new_value = self.num_animals if new_value > self.maximum: self.maximum = new_value return self.maximum def _setup_graphics(self): """ Creates subplots. """ map_dims = self._island.cells_dims if self._fig is None: self._fig = plt.figure() self._vis = Visualisation(self._island_map, self._fig, map_dims) self._vis.visualise_map() self._vis.animal_graphs(self._final_year, self.ymax_animals) self._vis.animal_dist_graphs() self._fig.tight_layout() def _update_graphics(self): """ Updates graphics with current data. """ df = self._animal_distribution rows, cols = self._island.cells_dims dist_matrix_carnivore = np.array(df[['Carnivore']]).reshape(rows, cols) dist_matrix_herbivore = np.array(df[['Herbivore']]).reshape(rows, cols) self._update_animals_graph() self._vis.update_herbivore_dist(dist_matrix_herbivore) self._vis.update_carnivore_dist(dist_matrix_carnivore) plt.pause(1e-6) self._fig.suptitle('Year: ' + str(self.year + 1), x=0.5) # shows first year as 1 def _update_animals_graph(self): """ Updating the animal graphs by counted number of animals """ herb_count, carn_count = list(self.num_animals_per_species.values()) self._vis.update_graphs(self._year, herb_count, carn_count) def add_population(self, population): """ Add a population to the island :param population: List of dictionaries specifying population """ self._island.place_animals(population) @property def year(self): """ Simulates last year. """ return self._year @property def num_animals(self): """ Calculates total number of animals on island. Returns ------- total_num: int """ total_num = 0 for species in self._animal_species: total_num += self._island.total_num_animals_per_species(species) return total_num @property def num_animals_per_species(self): """ Calculates number of animals per species in island, as dictionary. Returns ------- num_per_species: dict """ num_per_species = {} for species in self._animal_species: num_per_species[ species] = self._island.total_num_animals_per_species(species) return num_per_species @property def _animal_distribution(self): """ Calculates Pandas DataFrame with animal count per species for each cell on island. Returns ------- pd.DataFrame(count_df): data frame """ count_df = [] rows, cols = self._island.cells_dims for i in range(rows): for j in range(cols): cell = self._island.get_cells()[i, j] animals_count = cell.cell_fauna_count count_df.append({ 'Row': i, 'Col': j, 'Herbivore': animals_count['Herbivore'], 'Carnivore': animals_count['Carnivore'] }) return pd.DataFrame(count_df) def _save_graphics(self): """ Saves graphics to file if file name is given. """ if self._img_base is None: return plt.savefig('{base}_{num:05d}.{type}'.format(base=self._img_base, num=self._img_ctr, type=self._img_fmt)) self._img_ctr += 1 def make_movie(self, movie_fmt=_DEFAULT_MOVIE_FORMAT): """ Creates MPEG4 movie from visualization images saved, requires ffmpeg. The movie is stored as img_base + movie_fmt Parameters ---------- movie_fmt: str """ pass
class BioSim: def __init__( self, island_map, ini_pop, seed, ymax_animals=None, cmax_animals=None, img_base=None, img_fmt="png", ): """ :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' If ymax_animals is None, the y-axis limit will be adjusted dynamically. 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 f'{img_base}_{img_no:05d}.{img_fmt}' where img_no are consecutive image numbers starting from 0. img_base should contain a path and beginning of a file name. """ self.island = Island(island_map) random.seed(seed) self.add_population(ini_pop) self._current_year = 0 self._final_year = None self.ymax_animals = ymax_animals self.cmax_animals = cmax_animals # attributes for saving images self._img_ctr = 0 self._img_fmt = img_fmt self._img_base = img_base # Axes and figures to be instantiated in setup_sim_window self._sim_window_fig = None self._static_map_ax = None self._static_map_obj = None self._pop_plot_ax = None self._heat_herb_ax = None self._heat_herb_obj = None self._heat_carn_ax = None self._heat_carn_obj = None self._pop_pyram_ax = None self._pop_pyram_obj = None self._stack_area_ax = None self._stack_area_obj = None self._year_ax = None self._herb_cbar_ax = None self._carn_cbar_ax = None self.rgb_map = self._create_color_map(island_map) # Data variables for plots to be updated continuously self.y_stack = None self.herb_y = [] self.carn_y = [] def _setup_sim_window(self): """ Instantiates the main figure widow and creates subplots for the different plots and heatmaps. Also does some setup regarding the subplot parameters. """ plt.ion() # setup main window figure if self._sim_window_fig is None: self._sim_window_fig = plt.figure(figsize=(10, 5.63), dpi=150, facecolor="#ccd9ff") # setup static map axis and create the map object if self._static_map_ax is None: self._static_map_ax = self._sim_window_fig.add_subplot(2, 3, 1) self._static_map_obj = self._static_map_ax.imshow(self.rgb_map) # setup population subplot and some parameters for the subplot if self._pop_plot_ax is None: self._pop_plot_ax = self._sim_window_fig.add_subplot(2, 3, 4) self._pop_plot_ax.set_xlabel('Population', fontsize=9) if self.ymax_animals is not None: self._pop_plot_ax.set_ylim(0, self.ymax_animals) self._pop_plot_ax.set_xlim(0, self._final_year + 1) self._pop_plot_ax.tick_params(axis='both', which='major', labelsize=8) # setup herbivore heatmap subplot and accompanying colorbar axes if self._heat_herb_ax is None: self._heat_herb_ax = self._sim_window_fig.add_subplot(2, 3, 3) self._heat_herb_ax.tick_params(axis='both', which='major', labelsize=8) self._heat_herb_ax.set_xlabel('Herbivore heatmap', fontsize=9) self._herb_cbar_ax = self._sim_window_fig.add_axes( [0.715, 0.93, 0.25, 0.006]) # setup carnivore heatmap subplot and accompanying colorbar axes if self._heat_carn_ax is None: self._heat_carn_ax = self._sim_window_fig.add_subplot(2, 3, 6) self._heat_carn_ax.tick_params(axis='both', which='major', labelsize=8) self._heat_carn_ax.set_xlabel('Carnivore heatmap', fontsize=9) self._carn_cbar_ax = self._sim_window_fig.add_axes( [0.715, 0.473, 0.25, 0.006]) # setup population pyramid subplot and some parameters along with # text for labels if self._pop_pyram_ax is None: self._pop_pyram_ax = self._sim_window_fig.add_subplot(2, 3, 2) self._pop_pyram_obj = self._pop_pyram_ax.twiny() self._sim_window_fig.text(0.5, -0.15, 'Population size', fontsize=9, transform=self._pop_pyram_ax.transAxes, horizontalalignment='center') self._pop_pyram_obj.set_xlabel('Average weight', fontsize=9) self._sim_window_fig.text(1.02, 0.5, 'Age groups', fontsize=9, rotation=270, transform=self._pop_pyram_ax.transAxes, verticalalignment='center') self._pop_pyram_ax.tick_params(axis='both', which='major', labelsize=8) self._pop_pyram_obj.tick_params(axis='both', which='major', labelsize=8) # setup stacked area subplot if self._stack_area_ax is None: self._stack_area_ax = self._sim_window_fig.add_subplot(2, 3, 5) self._stack_area_ax.tick_params(axis='both', which='major', labelsize=8) self._stack_area_ax.set_xlabel('Biomass', fontsize=9) self._instantiate_stacked_area() # setup year counter if self._year_ax is None: self._year_ax = self._sim_window_fig.text( 0.04, 0.925, f'Year {self._current_year}', fontsize=18) self._sim_window_fig.tight_layout() def _save_graphics(self): """Saves graphics to file if file name is given.""" if self._img_base is None: return self._sim_window_fig.savefig( f'{self._img_base}_{self._img_ctr:05d}.{self._img_fmt}', facecolor="#ccd9ff") self._img_ctr += 1 def _update_population_plot(self): """ Takes the total number of animal per species for the year it is called and appends them to the population plot y values, then plots the plot. if ymax is not set it adjusts the ymax the biggest value plotted + 500 """ carn_count, herb_count = self.island.total_number_per_species().values( ) self.carn_y.append(carn_count) self.herb_y.append(herb_count) self._pop_plot_ax.plot(range(0, self._current_year + 1), self.herb_y, 'red', self.carn_y, 'lawngreen') if self.ymax_animals is None: self._pop_plot_ax.set_ylim( 0, max(max(self.herb_y), max(self.carn_y)) + 500) def _instantiate_stacked_area(self): """ Starts the stacked area plot "y" with nan values for the for length of num_years (in simulate(num_years)) when simulate is first called and appends to the existing "y" when called subsequently. When first instantiated it also sets some parameters for the plot, and adds a legend. """ if self._stack_area_obj is None: nanstack = np.full(self._final_year, np.nan) self.y_stack = np.vstack([nanstack, nanstack, nanstack]) self._stack_area_obj = self._stack_area_ax.stackplot( np.arange(0, self._final_year), self.y_stack, colors=['red', 'lawngreen', 'green'], labels=["Carnivores", "Herbivores", "Fodder"]) self._stack_area_ax.legend(fontsize='small', borderpad=0.1, loc=2) else: nanstack = np.full(self._final_year - self._current_year, np.nan) new_empty_values = np.vstack([nanstack, nanstack, nanstack]) self.y_stack = np.append(self.y_stack, new_empty_values, axis=1) def _update_stacked_area(self): """ Gets the current years biomass in a dictionary from "biomass_food_chain()" and updates the values form the stacked plot """ biomassdict = self.island.biomass_food_chain() self.y_stack[0][self._current_year] = biomassdict["biomass_carnivores"] self.y_stack[1][self._current_year] = biomassdict["biomass_herbs"] self.y_stack[2][self._current_year] = biomassdict["biomass_fodder"] self._stack_area_ax.stackplot(np.arange(0, self._final_year), self.y_stack, colors=['red', 'lawngreen', 'green']) def _update_heatmap_herb(self, array): """ :param array: A numpy array Takes a numpy array with the same dimensions as the island and with the ammount of herbivores per cell, where row and col corresponds to the x and y of the island. The method sets up the heatmap and colorbar when simulate() is first called and updates the data for subsequent calls. """ if self._heat_herb_obj is None: self._heat_herb_obj = self._heat_herb_ax.imshow( array, interpolation='nearest', vmax=200, cmap='inferno') herb_cbar = plt.colorbar(self._heat_herb_obj, cax=self._herb_cbar_ax, shrink=0.5, orientation='horizontal') herb_cbar.ax.tick_params(labelsize=6) if self.cmax_animals is not None: self._heat_herb_obj.set_clim( vmax=self.cmax_animals['Herbivore']) else: self._heat_herb_obj.set_data(array) def _update_heatmap_carn(self, array): """ :param array: A numpy array Takes a numpy array with the same dimensions as the island and with the ammount of carnivores per cell, where row and col corresponds to the x and y of the island. The method sets up the heatmap and colorbar when simulate() is first called and updates the data for subsequent calls. """ if self._heat_carn_obj is None: self._heat_carn_obj = self._heat_carn_ax.imshow( array, interpolation='nearest', vmax=200, cmap='inferno') carn_cbar = plt.colorbar(self._heat_carn_obj, cax=self._carn_cbar_ax, shrink=0.5, orientation='horizontal') carn_cbar.ax.tick_params(labelsize=6) if self.cmax_animals is not None: self._heat_carn_obj.set_clim( vmax=self.cmax_animals['Carnivore']) else: self._heat_carn_obj.set_data(array) def _update_pop_pyram(self): """ Creates/updates the population and biomass pyramid. This modifies the biomass bars from full bars to a line representing the bars extent it also automatically adjusts the xlimit of the populationnumbers while keeping 0 centered """ herb_pop_per_age, carn_pop_per_age, herb_mean_w, carn_mean_w = \ self.island.population_biomass_age_groups() age = ["0-1", "2-5", "5-10", "10-15", "15+"] [ rectangle.remove() for rectangle in reversed(self._pop_pyram_obj.patches) ] self._pop_pyram_ax.cla() self._pop_pyram_ax.barh(age, herb_pop_per_age, color='lawngreen') self._pop_pyram_ax.barh(age, carn_pop_per_age, color='red') rek1 = self._pop_pyram_obj.barh(age, herb_mean_w, color='black') rek2 = self._pop_pyram_obj.barh(age, carn_mean_w, color='black') maxlim_pop_per_age = max(max(herb_pop_per_age), abs(min(carn_pop_per_age))) + 150 self._pop_pyram_ax.set_xlim(-maxlim_pop_per_age, maxlim_pop_per_age) maxlim_mean_w = max(max(herb_mean_w), abs(min(carn_mean_w))) + 20 self._pop_pyram_obj.set_xlim(-maxlim_mean_w, maxlim_mean_w) for rectangle in rek1: rectangle.set_x(rectangle.get_width() - 1) rectangle.set_width(1) for rectangle in rek2: rectangle.set_x(rectangle.get_width() + 1) rectangle.set_width(1) def _update_sim_window(self): """ This updates the main figure window the current years data. """ herb_array, carn_array = self.island.arrays_for_heatmap() self._update_pop_pyram() self._update_heatmap_herb(herb_array) self._update_heatmap_carn(carn_array) self._update_population_plot() self._update_stacked_area() self._year_ax.set_text(f'Year {self._current_year}') plt.pause(1e-6) @staticmethod def _create_color_map(island_map_string): """ :param island_map_string: Multi-line string specifying island geography :return: map_rgb : Nested list with a color value for each cell type Creates the basis for the static color map. """ island_map_string = island_map_string.replace(" ", "") rgb_value = { 'O': (0.0, 0.0, 1.0), # blue 'M': (0.5, 0.5, 0.5), # grey 'J': (0.0, 0.6, 0.0), # dark green 'S': (0.5, 1.0, 0.5), # light green 'D': (1.0, 1.0, 0.5) } # light yellow map_rgb = [[rgb_value[column] for column in row] for row in island_map_string.splitlines()] return map_rgb @staticmethod def set_animal_parameters(species, params): """ :param species: String, name of animal species :param params: Dict with valid parameter specification for species Set parameters for animal species. """ if species == "Herbivore": Herbivores.set_parameters(params) elif species == "Carnivore": Carnivores.set_parameters(params) else: raise ValueError(f"{species} is not a species in this simulation") @staticmethod def set_landscape_parameters(landscape, params): """ :param landscape: String, code letter for landscape :param params: Dict with valid parameter specification for landscape Set parameters for landscape type. """ if landscape == "J": Jungle.set_parameters(params) elif landscape == "S": Savanna.set_parameters(params) else: raise ValueError(f"{landscape} is not an acceptable" f" landscape code for setting parameters") def simulate(self, num_years, vis_years=1, img_years=None): """ :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) Run simulation while visualizing the result. Image files will be numbered consecutively. """ if img_years is None: img_years = vis_years self._final_year = self._current_year + num_years self._setup_sim_window() while self._current_year < self._final_year: self.island.annual_cycle() if self._current_year % vis_years == 0: self._update_sim_window() self._sim_window_fig.canvas.draw() if self._current_year % img_years == 0: self._save_graphics() self._current_year += 1 def add_population(self, population): """ :param population: List of dictionaries specifying population Add a population to the island """ self.island.populate_island(population) @property def year(self): """Last year simulated.""" return self._current_year @property def num_animals(self): """Total number of animals on island.""" return sum(self.island.total_number_per_species().values()) @property def num_animals_per_species(self): """Number of animals per species in island, as dictionary.""" return self.island.total_number_per_species() @property def animal_distribution(self): """Pandas DataFrame with animal count per species for each cell on island.""" return self.island.per_cell_count_pandas_dataframe() def make_movie(self): """Create MPEG4 movie from visualization images saved.""" if self._img_base is None: raise RuntimeError("No filename defined.") try: # Parameters chosen according to # http://trac.ffmpeg.org/wiki/Encode/H.264, # section "Compatibility" subprocess.check_call([ _FFMPEG_BINARY, '-framerate', '15', '-i', f'{self._img_base}_%05d.png', '-y', '-profile:v', 'baseline', '-level', '3.0', '-pix_fmt', 'yuv420p', f'{self._img_base}.mp4' ]) except subprocess.CalledProcessError as err: raise RuntimeError(f'ERROR: ffmpeg failed with: {err}')