Ejemplo n.º 1
0
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
Ejemplo n.º 2
0
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
Ejemplo n.º 3
0
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}')