├── LICENSE ├── README.md ├── bin ├── plot_history ├── plot_history_full.py ├── profile.sh └── text_to_map.py ├── ecosim.sh ├── ecosim ├── __init__.py ├── collisiongrid │ ├── __init__.py │ ├── __init__.pyx │ ├── collision_grid.py │ ├── collision_gridx.pxd │ └── collision_gridx.pyx ├── draw.py ├── history.py ├── individual.pxd ├── individual.pyx ├── main.py ├── simulation.py └── utils.py ├── examples ├── default.config ├── disturbance.config ├── large_four.config ├── profile.config ├── seed_cost.config └── two_regions.config ├── setup.py ├── tests ├── test_collisiongrid.py └── test_main.py └── time_collisions.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Joel Simon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ecology-modeling 2 | -------------------------------------------------------------------------------- /bin/plot_history: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import print_function, division 3 | import sys, os 4 | import pickle 5 | import numpy as np 6 | import matplotlib.pyplot as plt 7 | from collections import Counter 8 | from os.path import join as pjoin 9 | import argparse 10 | import shutil 11 | sys.path.append(os.path.abspath('.')) 12 | 13 | def format_color(rgb): 14 | r, g, b = rgb 15 | return float(r)/255, float(g)/255, float(b)/255 16 | 17 | def n_most_common_genomes(n): 18 | max_total_area = Counter() 19 | for g in history[::step_size]: 20 | for gid, count, area in g: 21 | max_total_area[gid] = max(area*count, max_total_area.get(gid, 0)) 22 | top_genomes = list(zip(*max_total_area.most_common(n)))[0] 23 | return top_genomes 24 | 25 | def plot(x, X_area, X_count, colors): 26 | f, axarr = plt.subplots(2, sharex=True, figsize=(12, 8)) 27 | axarr[0].stackplot(x, X_area, colors=colors) 28 | axarr[0].set_title('Species Total Area') 29 | axarr[0].set_xlabel('Generations') 30 | axarr[0].set_ylabel('Total Area of Species') 31 | axarr[0].set_ylim([0, 1]) 32 | 33 | axarr[1].stackplot(x, X_count, colors=colors) 34 | axarr[1].set_title('Species Abundance') 35 | axarr[1].set_xlabel('Generations') 36 | axarr[1].set_ylabel('Abundance of Species') 37 | 38 | def histogram_animate(x, X_area, colors, out_dir, width, height): 39 | dpi = 96 40 | step = 20 41 | f, axarr = plt.subplots(1, figsize=(width/dpi, height/dpi), dpi=dpi) 42 | 43 | plt.subplots_adjust(left=.1, bottom=.1, right=.9, top=.9, wspace=.2, hspace=0) 44 | 45 | axarr.stackplot(x, X_area, colors=colors) 46 | 47 | dir = pjoin(out_dir, 'hist_imgs') 48 | 49 | if os.path.exists(dir): 50 | shutil.rmtree(dir) 51 | os.makedirs(dir) 52 | 53 | axarr.set_ylim([0, 1]) 54 | axarr.set_xlim([0, x.max()]) 55 | axarr.get_xaxis().set_visible(False) 56 | axarr.get_yaxis().set_visible(False) 57 | 58 | line = axarr.axvline(x=0, color='black') 59 | 60 | for _x in range(0, x.max(), step): 61 | line.set_xdata(_x) 62 | plt.savefig(pjoin(dir, '%06d.jpg'%_x)) 63 | 64 | if __name__ == '__main__': 65 | parser = argparse.ArgumentParser() 66 | parser.add_argument('archive_path', help='Path to archive') 67 | parser.add_argument('--n_color', type=int, default=50, help='Number of genomes to color individually, default=50') 68 | parser.add_argument('--n_steps', type=int, default=1000, help='Graphing resolution, default=100') 69 | parser.add_argument('--animate', action='store_true', default=False) 70 | parser.add_argument('--animate_width', type=int, default=1800, help='Graphing resolution, default=100') 71 | parser.add_argument('--animate_height', type=int, default=300, help='Graphing resolution, default=100') 72 | 73 | # parser.add_argument('--n_steps', type=int, default=1000, help='Graphing resolution, default=100') 74 | 75 | args = parser.parse_args() 76 | 77 | history, genomes = pickle.load(open(args.archive_path, 'rb')) 78 | print('Loaded History.') 79 | 80 | # Calculate generation skip size. 81 | n_gens = len(history) 82 | step_size = max(1, int(n_gens/args.n_steps)) 83 | print('step_size:', step_size) 84 | 85 | # Get genomes we will plot seperately. 86 | top_genomes = n_most_common_genomes(args.n_color) 87 | 88 | # Map these from genome_id to index. 89 | top_genome_ordering = dict(zip(top_genomes, range(len(top_genomes)))) 90 | 91 | colors = [format_color(genomes[gid].color) for gid in top_genomes ] 92 | colors.append((.3, .3, .3)) 93 | 94 | # Create array to store plotting. 95 | X_area = np.zeros((args.n_color+1, args.n_steps)) 96 | X_count = np.zeros((args.n_color+1, args.n_steps)) 97 | 98 | for i in range(args.n_steps): 99 | g = i * step_size 100 | for genome_id, count, area in history[g]: 101 | if genome_id in top_genome_ordering: 102 | X_area[top_genome_ordering[genome_id], i] = area*count 103 | X_count[top_genome_ordering[genome_id], i] = count 104 | else: 105 | X_area[-1, i] = area*count 106 | X_count[-1, i] = count 107 | 108 | for i in range(args.n_steps): 109 | X_area[:, i] /= X_area[:,i].sum() 110 | 111 | x = np.arange(args.n_steps) * step_size 112 | print('Structured data for plotting.') 113 | 114 | plot(x, X_area, X_count, colors) 115 | 116 | out_dir = os.path.dirname(args.archive_path) 117 | 118 | outpath = os.path.join(out_dir, 'history.png') 119 | 120 | plt.savefig(outpath) 121 | 122 | if args.animate: 123 | histogram_animate(x, X_area, colors, out_dir, args.animate_width, args.animate_height) 124 | -------------------------------------------------------------------------------- /bin/plot_history_full.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function, division 2 | import sys 3 | import pickle 4 | 5 | import numpy as np 6 | import matplotlib.pyplot as plt 7 | from collections import Counter 8 | 9 | from os.path import join as pjoin 10 | 11 | n = 50 # how many genomes to color individually 12 | 13 | npzfile = np.load(sys.argv[1]) 14 | 15 | step_breaks = npzfile['arr_0'] 16 | history_ints = npzfile['arr_1'] 17 | history_floats = npzfile['arr_2'] 18 | genome_ints = npzfile['arr_3'] 19 | genome_floats = npzfile['arr_4'] 20 | 21 | print('Loaded History.') 22 | 23 | n_gens = len(step_breaks) 24 | n_steps = 1000 25 | step_size = max(1, int(n_gens/n_steps)) 26 | 27 | print('step_size:', step_size) 28 | 29 | genome_colors = {} 30 | for i in range(genome_ints.shape[0]): 31 | g_id = genome_ints[i, 0] 32 | r, g, b = genome_ints[i, 2:5] 33 | genome_colors[g_id] = (float(r)/255, float(g)/255, float(b)/255) 34 | 35 | def iterate_timesteps_individuals(g): 36 | start = step_breaks[g-1] if g > 0 else 0 37 | for i in range(start, step_breaks[g]): 38 | id, genome_id = history_ints[i] 39 | x, y, area = history_floats[i] 40 | yield id, genome_id, x, y, area 41 | 42 | def n_most_common_genomes(): 43 | max_total_area = Counter() 44 | 45 | for g in range(0, n_gens, step_size): 46 | 47 | generation_total_area = Counter() 48 | 49 | for id, genome_id, x, y, area in iterate_timesteps_individuals(g): 50 | generation_total_area[genome_id] += area 51 | 52 | for id, area in generation_total_area.items(): 53 | max_total_area[id] = max(area, max_total_area.get(id, 0)) 54 | 55 | top_genomes = zip(*max_total_area.most_common(n))[0] 56 | return top_genomes 57 | 58 | # Get genomes we will plot seperately. 59 | top_genomes = n_most_common_genomes() 60 | 61 | # Map these from genome_id to index. 62 | top_genome_ordering = dict(zip(top_genomes, range(len(top_genomes)))) 63 | 64 | top_genome_colors = [genome_colors[gid] for gid in top_genomes] 65 | top_genome_colors.append((.3, .3, .3)) 66 | 67 | # Create array to store plotting. 68 | X_area = np.zeros((n+1, n_steps)) 69 | X_count = np.zeros((n+1, n_steps)) 70 | 71 | for i in range(n_steps): 72 | g = i * step_size 73 | for id, genome_id, x, y, area in iterate_timesteps_individuals(g): 74 | if area == 0: 75 | continue 76 | if genome_id in top_genome_ordering: 77 | X_area[top_genome_ordering[genome_id], i] += area 78 | X_count[top_genome_ordering[genome_id], i] += 1 79 | else: 80 | X_area[-1, i] += area 81 | X_count[-1, i] += 1 82 | 83 | for i in range(n_steps): 84 | X_area[:, i] /= X_area[:,i].sum() 85 | 86 | print('Structured data for plotting.') 87 | 88 | x = np.arange(n_steps) * step_size 89 | 90 | print(x.shape) 91 | print(X_area.shape) 92 | 93 | f, axarr = plt.subplots(2, sharex=True, figsize=(20, 12)) 94 | axarr[0].stackplot(x, X_area, colors=top_genome_colors) 95 | axarr[0].set_title('Species Total Area') 96 | axarr[0].set_xlabel('Generations') 97 | axarr[0].set_ylabel('Total Area of Species') 98 | axarr[0].set_ylim([0, 1]) 99 | 100 | axarr[1].stackplot(x, X_count, colors=top_genome_colors) 101 | axarr[1].set_title('Species Abundance') 102 | axarr[1].set_xlabel('Generations') 103 | axarr[1].set_ylabel('Abundance of Species') 104 | 105 | plt.show() 106 | -------------------------------------------------------------------------------- /bin/profile.sh: -------------------------------------------------------------------------------- 1 | python -m cProfile -o run.prof ecosim.sh 200 examples/profile.config 2 | snakeviz run.prof 3 | -------------------------------------------------------------------------------- /bin/text_to_map.py: -------------------------------------------------------------------------------- 1 | from draw import PygameDraw 2 | import numpy as np 3 | import pygame 4 | 5 | w, h = (350, int(350 / (8.5/11.0))) 6 | 7 | view = PygameDraw(w, h, flip_y=False) 8 | view.start_draw() 9 | 10 | # view.draw_text((w//2, h//2), 'F I G U R A T I O N', 75//2, center=True, fontfamily='Oxygen') 11 | view.draw_rect((10, 10, w-20, h-20), (0,0,0), width=10) 12 | view.draw_text((w//2, 50), 'Chimera Corp.', 60, center=True, fontfamily='Oxygen') 13 | view.draw_text((w//2, 200), 'Become your', 40, center=True, fontfamily='Oxygen') 14 | view.draw_text((w//2, 250), 'better self.', 40, center=True, fontfamily='Oxygen') 15 | 16 | view.end_draw() 17 | pxarray = pygame.surfarray.array2d(view.surface) 18 | pxarray[pxarray == -256] = 1 19 | # pxarray[10:40, :] = 0 20 | 21 | view.hold() 22 | # np.save('figuration.npy', pxarray.astype('uint8')) 23 | # np.save('chimeracorp.npy', pxarray.astype('uint8')) -------------------------------------------------------------------------------- /ecosim.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import print_function, division 3 | from os import path 4 | import argparse 5 | 6 | # 2/3 compatability 7 | try: 8 | import ConfigParser as configparser 9 | except ImportError: 10 | import configparser 11 | 12 | from ecosim.main import main 13 | 14 | if __name__ == '__main__': 15 | parser = argparse.ArgumentParser() 16 | parser.add_argument("steps", help="Number of steps to run for.", type=int) 17 | parser.add_argument("out", help="Path for output files.") 18 | parser.add_argument("config", help="Path to config file.") 19 | 20 | # parser.add_argument('--bias_map', help='path to a binary array in .npy format.') 21 | parser.add_argument('--draw_scale', type=float, default=3.0, 22 | help='world to pixel scale, default=3') 23 | parser.add_argument('--log_interval', type=int, default=100, 24 | help='number of iterations between print, default=10') 25 | parser.add_argument('--archive_interval', type=int, default=10000, 26 | help='number of iterations between each archive, default=1000') 27 | parser.add_argument('--img_interval', type=int, default=100, 28 | help='number of iterations between each image, default=100') 29 | args = parser.parse_args() 30 | 31 | config = configparser.ConfigParser(allow_no_value=True) 32 | 33 | if not path.exists(args.config): 34 | raise ValueError('That is not a valid to a config file:'+args.config) 35 | 36 | config.read(args.config) 37 | 38 | run_config = { 39 | 'width': config.getint('simulation', 'width'), 40 | 'height': config.getint('simulation', 'height'), 41 | 'n_start': config.getint('simulation', 'n_start'), 42 | 'p_death': config.getfloat('simulation', 'p_death'), 43 | 'n_randseed': config.getint('simulation', 'n_randseed'), 44 | # 'bias_areas': parse_bias_areas(config.get('simulation', 'bias_areas')), 45 | 'bias_map': config.get('simulation', 'bias_map'), 46 | 'p_disturbance': config.getfloat('simulation', 'p_disturbance'), 47 | 'disturbance_power': config.getfloat('simulation', 'disturbance_power'), 48 | 'seed_cost_multiplier': config.getfloat('simulation', 'seed_cost_multiplier'), 49 | 'growth_cost_multiplier': config.getfloat('simulation', 'growth_cost_multiplier'), 50 | 'n_attributes': config.getint('genome', 'n_attributes'), 51 | 'seed_size_range': (config.getfloat('genome', 'min_seed_size'), 52 | config.getfloat('genome', 'max_seed_size')), 53 | } 54 | main(run_config, args.steps, args.out, args.log_interval, args.img_interval,\ 55 | args.draw_scale, args.archive_interval) 56 | -------------------------------------------------------------------------------- /ecosim/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joel-simon/ecology-modeling/5466bbeeb8e8f23af5423131b5170ee8dc67fce0/ecosim/__init__.py -------------------------------------------------------------------------------- /ecosim/collisiongrid/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joel-simon/ecology-modeling/5466bbeeb8e8f23af5423131b5170ee8dc67fce0/ecosim/collisiongrid/__init__.py -------------------------------------------------------------------------------- /ecosim/collisiongrid/__init__.pyx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joel-simon/ecology-modeling/5466bbeeb8e8f23af5423131b5170ee8dc67fce0/ecosim/collisiongrid/__init__.pyx -------------------------------------------------------------------------------- /ecosim/collisiongrid/collision_grid.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from math import pi, sqrt, hypot, floor 3 | 4 | # import numpy as np 5 | class CollisionGrid(object): 6 | """docstring for CollisionGrid""" 7 | def __init__(self, width, height, blocksize): 8 | assert width % blocksize == 0 9 | assert height % blocksize == 0 10 | 11 | self.width = width 12 | self.height = height 13 | self.blocksize = blocksize 14 | 15 | self.shape = (int(width / blocksize), int(height / blocksize)) 16 | self.grid = [[set() for _ in range(self.shape[0])] for _ in range(self.shape[1])] 17 | self.particles = dict() 18 | 19 | def isEmpty(self, x, y, r): 20 | cx = max(0, int(floor((x-r) / self.blocksize))) 21 | while (cx * self.blocksize) <= min( self.width-1, x+r): 22 | 23 | cy = max(0, int(floor((y-r) / self.blocksize))) 24 | while (cy * self.blocksize) <= min( self.height-1, y+r): 25 | block = self.grid[cx][cy] 26 | 27 | for id0 in block: 28 | x2, y2, r2 = self.particles[id0] 29 | overlap = hypot(x - x2, y-y2) < r + r2 30 | if overlap: 31 | return False 32 | 33 | cy += 1 34 | cx += 1 35 | 36 | return True 37 | 38 | def insertParticle(self, id, x, y, r): 39 | """ Return True if add was accepted and False otherwise. 40 | """ 41 | assert id not in self.particles 42 | assert x >= 0 and x < self.width 43 | assert y >= 0 and y < self.height 44 | assert r > 0 45 | 46 | self.particles[id] = (x, y, r) 47 | 48 | cx = max(0, int(floor((x-r) / self.blocksize))) 49 | while (cx * self.blocksize) <= min( self.width-1, x+r): 50 | 51 | cy = max(0, int(floor((y-r) / self.blocksize))) 52 | while (cy * self.blocksize) <= min( self.height-1, y+r): 53 | 54 | block = self.grid[cx][cy] 55 | block.add(id) 56 | 57 | cy += 1 58 | cx += 1 59 | 60 | return True 61 | 62 | def removeParticle(self, id): 63 | x, y, r = self.particles[id] 64 | del self.particles[id] 65 | 66 | cx = max(0, int(floor((x-r) / self.blocksize))) 67 | while (cx * self.blocksize) <= min( self.width-1, x+r): 68 | 69 | cy = max(0, int(floor((y-r) / self.blocksize))) 70 | while (cy * self.blocksize) <= min( self.height-1, y+r): 71 | 72 | block = self.grid[cx][cy] 73 | block.remove(id) 74 | 75 | cy += 1 76 | cx += 1 77 | 78 | def updateRadius(self, id, r): 79 | """ Return True if update was accepted and False otherwise. 80 | """ 81 | assert r > 0 82 | x, y, _ = self.particles[id] 83 | self.removeParticle(id) 84 | self.insertParticle(id, x, y, r) 85 | 86 | return True 87 | 88 | def query(self, x0, y0, x1, y1): 89 | seen = set() 90 | 91 | assert x0 < x1 92 | assert y0 < y1 93 | 94 | cx = max(0, int(floor(x0 / self.blocksize))) 95 | while (cx * self.blocksize) <= min( self.width-1, x1): 96 | 97 | cy = max(0, int(floor(y0 / self.blocksize))) 98 | while (cy * self.blocksize) <= min( self.height-1, y1): 99 | seen.update(self.grid[cx][cy]) 100 | cy += 1 101 | 102 | cx += 1 103 | 104 | return seen 105 | -------------------------------------------------------------------------------- /ecosim/collisiongrid/collision_gridx.pxd: -------------------------------------------------------------------------------- 1 | from cymem.cymem cimport Pool 2 | 3 | # Singly linked list structure. 4 | cdef struct Entry: 5 | int value 6 | Entry *next 7 | 8 | cdef class CollisionGrid: 9 | cdef Pool mem 10 | cdef Entry **grid 11 | cdef public double blocksize 12 | cdef public int width, height, nx, ny 13 | cdef public dict particles 14 | 15 | # Internal helpers. 16 | cpdef list get_block(self, int ix, int iy) 17 | cpdef void grid_add(self, int id, int ix, int iy) except * 18 | cpdef void grid_remove(self, int id, int ix, int iy) except * 19 | 20 | # Public methods. 21 | cpdef bint isEmpty(self, double x, double y, double r) except * 22 | cpdef void insertParticle(self, int id, double x, double y, double r) except * 23 | cpdef void removeParticle(self, int id) except * 24 | cpdef void updateRadius(self, int id, double r) except * 25 | cpdef set query(self, double x0, double y0, double x1, double y1) 26 | cpdef set queryCircle(self, double x, double y, double r) 27 | -------------------------------------------------------------------------------- /ecosim/collisiongrid/collision_gridx.pyx: -------------------------------------------------------------------------------- 1 | # cython: boundscheck=False 2 | # cython: wraparound=False 3 | # cython: initializedcheck=False 4 | # cython: nonecheck=False 5 | # cython: cdivision=True 6 | 7 | from __future__ import print_function 8 | 9 | from cpython.mem cimport PyMem_Malloc, PyMem_Free 10 | from libc.math cimport abs, floor, sqrt 11 | # from cymem.cymem cimport Pool 12 | 13 | cdef class CollisionGrid: 14 | """ Fast collision detection in mass particle system. 15 | """ 16 | def __cinit__(self, width, height, blocksize): 17 | assert width % blocksize == 0 18 | assert height % blocksize == 0 19 | 20 | self.width = width 21 | self.height = height 22 | self.blocksize = blocksize 23 | 24 | self.nx = int(width / blocksize) 25 | self.ny = int(height / blocksize) 26 | 27 | self.grid = PyMem_Malloc(self.nx * self.ny * sizeof(Entry *)) 28 | 29 | if not self.grid: 30 | raise MemoryError() 31 | 32 | cdef int i = 0 33 | for i in range(self.nx * self.ny): 34 | self.grid[i] = NULL 35 | 36 | self.particles = dict() 37 | 38 | print('Created CollisionGrid', (self.nx, self.ny)) 39 | 40 | def __dealloc__(self): 41 | cdef Entry *head 42 | cdef Entry *temp 43 | cdef int i = 0 44 | 45 | for i in range(self.nx * self.ny): 46 | head = self.grid[i] 47 | 48 | while head != NULL: 49 | temp = head 50 | head = head.next 51 | PyMem_Free(temp) 52 | 53 | PyMem_Free(self.grid) 54 | 55 | cpdef list get_block(self, int ix, int iy): 56 | """ Debug function to return linked list as python list. 57 | """ 58 | cdef Entry *p = self.grid[ix + iy*self.nx] 59 | cdef list result = [] 60 | 61 | while p != NULL: 62 | result.append(p.value) 63 | p = p.next 64 | 65 | return result 66 | 67 | cpdef void grid_add(self, int id, int ix, int iy) except *: 68 | """ Helper function to prepend value to linked list. 69 | """ 70 | cdef int i = ix + iy*self.nx 71 | 72 | if ix < 0 or ix >= self.nx: 73 | raise ValueError() 74 | 75 | if iy < 0 or iy >= self.ny: 76 | raise ValueError() 77 | 78 | cdef Entry *e = PyMem_Malloc(sizeof(Entry)) 79 | 80 | if not e: 81 | raise MemoryError() 82 | 83 | e.value = id 84 | e.next = self.grid[i] 85 | 86 | self.grid[i] = e 87 | 88 | cpdef void grid_remove(self, int id, int ix, int iy) except *: 89 | """ Helper function to remove value from linked list. 90 | Assumes it only occures once. 91 | """ 92 | cdef int i = ix + iy*self.nx 93 | cdef Entry *temp = self.grid[i] 94 | cdef Entry *prev 95 | 96 | if temp == NULL: 97 | raise KeyError(id) 98 | 99 | elif temp.value == id: 100 | # Need to remove head of list. 101 | self.grid[i] = temp.next 102 | PyMem_Free(temp) 103 | return 104 | 105 | while temp is not NULL and temp.value != id: 106 | prev = temp 107 | temp = temp.next 108 | 109 | if temp is NULL: 110 | raise KeyError(id) 111 | 112 | prev.next = temp.next 113 | PyMem_Free(temp) 114 | 115 | cpdef bint isEmpty(self, double x, double y, double r) except *: 116 | cdef int cx, cy, id0 117 | cdef Entry *p 118 | cdef double x2, y2, r2, dx, dy 119 | 120 | cy = max(0, floor((y-r) / self.blocksize)) 121 | while (cy * self.blocksize) <= min( self.height-1, y+r): 122 | 123 | cx = max(0, floor((x-r) / self.blocksize)) 124 | while (cx * self.blocksize) <= min( self.width-1, x+r): 125 | 126 | p = self.grid[cx + cy*self.nx] 127 | 128 | while p != NULL: 129 | id0 = p.value 130 | x2, y2, r2 = self.particles[id0] 131 | dx = x - x2 132 | dy = y - y2 133 | 134 | if sqrt(dx*dx + dy*dy) < r+r2: 135 | return False 136 | 137 | p = p.next 138 | 139 | cx += 1 140 | cy += 1 141 | 142 | return True 143 | 144 | cpdef void insertParticle(self, int id, double x, double y, double r) except *: 145 | cdef int cx, cy 146 | 147 | if x < 0 or x >= self.width: 148 | raise ValueError() 149 | 150 | if y < 0 or y >= self.height: 151 | raise ValueError() 152 | 153 | if id in self.particles: 154 | raise ValueError() 155 | 156 | if r < 0: 157 | raise ValueError() 158 | 159 | self.particles[id] = (x, y, r) 160 | 161 | cy = max(0, floor((y-r) / self.blocksize)) 162 | while (cy * self.blocksize) <= min( self.height-1, y+r): 163 | cx = max(0, floor((x-r) / self.blocksize)) 164 | while (cx * self.blocksize) <= min( self.width-1, x+r): 165 | 166 | self.grid_add(id, cx, cy) 167 | 168 | cx += 1 169 | cy += 1 170 | 171 | cpdef void removeParticle(self, int id) except *: 172 | cdef double x, y, r 173 | cdef int cx, cy 174 | 175 | x, y, r = self.particles[id] 176 | del self.particles[id] 177 | 178 | cy = max(0, floor((y-r) / self.blocksize)) 179 | while (cy * self.blocksize) <= min( self.height-1, y+r): 180 | 181 | cx = max(0, floor((x-r) / self.blocksize)) 182 | while (cx * self.blocksize) <= min( self.width-1, x+r): 183 | 184 | self.grid_remove(id, cx, cy) 185 | 186 | cx += 1 187 | cy += 1 188 | 189 | cpdef void updateRadius(self, int id, double r) except *: 190 | 191 | if r < 0: 192 | raise ValueError() 193 | 194 | cdef double x, y, r0 195 | x, y, r0 = self.particles[id] 196 | 197 | cdef int cy = floor((y-r) / self.blocksize) 198 | cdef int cy0 = floor((y-r0) / self.blocksize) 199 | cdef int cx = floor((x-r) / self.blocksize) 200 | cdef int cx0 = floor((x-r0) / self.blocksize) 201 | 202 | cdef int ey = floor((y+r) / self.blocksize) 203 | cdef int ey0 = floor((y+r0) / self.blocksize) 204 | cdef int ex = floor((x+r) / self.blocksize) 205 | cdef int ex0 = floor((x+r0) / self.blocksize) 206 | 207 | if cy == cy0 and cx == cx0 and ey == ey0 and ex == ex0: 208 | self.particles[id] = (x, y, r) 209 | else: 210 | self.removeParticle(id) 211 | self.insertParticle(id, x, y, r) 212 | 213 | cpdef set query(self, double x0, double y0, double x1, double y1): 214 | cdef set seen = set() 215 | cdef int cx, cy 216 | cdef Entry *p 217 | 218 | cy = max(0, floor(y0 / self.blocksize)) 219 | while (cy * self.blocksize) <= min( self.height-1, y1): 220 | 221 | cx = max(0, floor(x0 / self.blocksize)) 222 | while (cx * self.blocksize) <= min( self.width-1, x1): 223 | 224 | p = self.grid[cx + cy*self.nx] 225 | while p != NULL: 226 | seen.add(p.value) 227 | p = p.next 228 | 229 | cx += 1 230 | cy += 1 231 | 232 | return seen 233 | 234 | cpdef set queryCircle(self, double x, double y, double r): 235 | """ Return set of all particle that overlap circle. 236 | """ 237 | cdef int cx, cy, id 238 | cdef double x0, y0, r0, dx, dy 239 | cdef set seen = set() 240 | cdef Entry *p 241 | 242 | cy = max(0, floor((y-r) / self.blocksize)) 243 | while (cy * self.blocksize) <= min( self.height-1, y+r): 244 | 245 | cx = max(0, floor((x-r) / self.blocksize)) 246 | while (cx * self.blocksize) <= min( self.width-1, x+r): 247 | p = self.grid[cx + cy*self.nx] 248 | 249 | while p is not NULL: 250 | id = p.value 251 | 252 | x0, y0, r0 = self.particles[id] 253 | dx = x - x0 254 | dy = y - y0 255 | 256 | if sqrt(dx*dx + dy*dy) < r+r0: 257 | seen.add(id) 258 | 259 | p = p.next 260 | 261 | cx += 1 262 | cy += 1 263 | return seen 264 | -------------------------------------------------------------------------------- /ecosim/draw.py: -------------------------------------------------------------------------------- 1 | """ provide a consistent api around pygame draw functions 2 | using gfx draw if available. 3 | """ 4 | import sys 5 | import math 6 | import pygame 7 | import pygame.gfxdraw 8 | 9 | pygame.init() 10 | pygame.mixer.init() 11 | pygame.font.init() 12 | 13 | BLACK = (0,0,0) 14 | 15 | class PygameDraw(object): 16 | """docstring for Draw""" 17 | def __init__(self, w, h, scale=1, flip_y=True): 18 | self.fonts = dict() 19 | self.w = w 20 | self.h = h 21 | self.scale = scale 22 | self.flip_y = flip_y 23 | 24 | self.surface = pygame.display.set_mode((w, h)) 25 | self.images = dict() 26 | 27 | self.font = pygame.font.SysFont("monospace", 16) 28 | 29 | def map_point(self, p): 30 | x, y = p 31 | if self.flip_y: 32 | return (int(x*self.scale), int(self.h - y*self.scale)) 33 | else: 34 | return (int(x*self.scale), int(y*self.scale)) 35 | 36 | def start_draw(self): 37 | self.surface.fill((255, 255, 255)) 38 | 39 | def end_draw(self): 40 | pygame.display.flip() 41 | 42 | def save(self, path): 43 | pygame.image.save(self.surface, path) 44 | 45 | def hold(self): 46 | while True: 47 | for event in pygame.event.get(): 48 | if event.type == pygame.QUIT: 49 | sys.exit() 50 | 51 | def draw_pixel(self, point, color): 52 | x, y = self.map_point(point) 53 | pygame.gfxdraw.pixel(self.surface, x, y, color) 54 | 55 | def draw_polygon(self, points, color, t=0): 56 | points = [self.map_point(p) for p in points] 57 | if t == 0: 58 | pygame.gfxdraw.filled_polygon(self.surface, points, color) 59 | elif t == 1: 60 | pygame.gfxdraw.aapolygon(self.surface, points, color) 61 | else: 62 | pygame.draw.polygon(self.surface, color, points, t) 63 | 64 | def draw_circle(self, position, radius, color, width=0): 65 | position = self.map_point(position) 66 | r = int(radius*self.scale) 67 | width = int(width*self.scale) 68 | x, y = position 69 | 70 | if width == 0: 71 | pygame.gfxdraw.filled_circle(self.surface, x, y, r, color) 72 | 73 | if r > 1: 74 | pygame.gfxdraw.aacircle(self.surface, x, y, r, color) 75 | 76 | def draw_line(self, positionA, positionB, color, width=1): 77 | a = self.map_point(positionA) 78 | b = self.map_point(positionB) 79 | width = int(width*self.scale) 80 | center = ((a[0] + b[0])/2, (a[1] + b[1])/2) 81 | angle = math.atan2(a[1] - b[1], a[0] - b[0]) 82 | length = math.sqrt((a[0]-b[0])**2 + (a[1]-b[1])**2) 83 | 84 | if width == 1: 85 | pygame.draw.line(self.surface, color, a, b, width) 86 | else: 87 | UL = (center[0] + (length / 2.) * math.cos(angle) - (width / 2.) * math.sin(angle), 88 | center[1] + (width / 2.) * math.cos(angle) + (length / 2.) * math.sin(angle)) 89 | UR = (center[0] - (length / 2.) * math.cos(angle) - (width / 2.) * math.sin(angle), 90 | center[1] + (width / 2.) * math.cos(angle) - (length / 2.) * math.sin(angle)) 91 | BL = (center[0] + (length / 2.) * math.cos(angle) + (width / 2.) * math.sin(angle), 92 | center[1] - (width / 2.) * math.cos(angle) + (length / 2.) * math.sin(angle)) 93 | BR = (center[0] - (length / 2.) * math.cos(angle) + (width / 2.) * math.sin(angle), 94 | center[1] - (width / 2.) * math.cos(angle) - (length / 2.) * math.sin(angle)) 95 | 96 | pygame.gfxdraw.aapolygon(self.surface, (UL, UR, BR, BL), color) 97 | pygame.gfxdraw.filled_polygon(self.surface, (UL, UR, BR, BL), color) 98 | 99 | # if width > 1: 100 | # pygame.draw.line(self.surface, color, a, b, width) 101 | # else: 102 | # pygame.gfxdraw.line(self.surface, a[0], a[1], b[0], b[1], color) 103 | 104 | def draw_lines(self, points, color, width=1): 105 | points = [self.map_point(p) for p in points] 106 | pygame.draw.lines(self.surface, color, False, points, width) 107 | 108 | def draw_rect(self, rect, color, width=1): 109 | x, y = self.map_point((rect[0], rect[1])) 110 | w, h = int(self.scale*rect[2]), int(self.scale*rect[3]) 111 | rect = (x, y, w, h) 112 | 113 | if width == 0: 114 | points = [(x, y), (x+w, y), (x+w, y-h), (x, y-h)] 115 | pygame.gfxdraw.filled_polygon(self.surface, points, color) 116 | 117 | elif width == 1: 118 | pygame.gfxdraw.rectangle(self.surface, rect, color) 119 | 120 | else: 121 | pygame.draw.rect(self.surface, color, rect, width) 122 | 123 | def draw_alpha_rect(self, rect, color, alpha): 124 | x, y = self.map_point((rect[0], rect[1])) 125 | w, h = int(self.scale*rect[2]), int(self.scale*rect[3]) 126 | s = pygame.Surface((w, h), pygame.SRCALPHA) # per-pixel alpha 127 | s.fill(color + (alpha,)) 128 | # print(y, self.h) 129 | self.surface.blit(s, (x, y-h)) 130 | 131 | def draw_text(self, position, string, font=8, color=BLACK, center=False, fontfamily='monospace'): 132 | font = int(self.scale * font) 133 | # x, y = self.map_point(position) 134 | x, y = position 135 | 136 | key = (font, fontfamily) 137 | if key not in self.fonts: 138 | self.fonts[font] = pygame.font.SysFont(fontfamily, font) 139 | 140 | text = self.fonts[font].render(string, 0, color) 141 | # text = self.font.render(string, 1, color) 142 | 143 | if center: 144 | w = text.get_rect().width 145 | h = text.get_rect().height 146 | self.surface.blit(text, (int(x-w/2.), int(y-h/2.))) 147 | 148 | else: 149 | 150 | self.surface.blit(text, (x, y)) 151 | -------------------------------------------------------------------------------- /ecosim/history.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function, division 2 | import pickle 3 | import numpy as np 4 | from os.path import join as pjoin 5 | from collections import Counter 6 | 7 | class HistoryFull(object): 8 | """ Store a complete representation of the run including every individual 9 | at each generation. Useful if sections want to generaed later and 10 | animated. Takes a large amount of space for longer runs. 11 | """ 12 | def __init__(self): 13 | self._init() 14 | 15 | def _init(self): 16 | self._genomes = dict() 17 | self._stepBreaks = [] 18 | self._history_ints = [] 19 | self._history_floats = [] 20 | 21 | def addGeneration(self, sim): 22 | for ind in sim.individuals.values(): 23 | self._history_ints.append((ind.id, ind.genome.id)) 24 | self._history_floats.append((ind.x, ind.y, ind.area())) 25 | self._genomes[ind.genome.id] = ind.genome 26 | 27 | self._stepBreaks.append(len(self._history_ints)) 28 | 29 | def save(self, filepath, config): 30 | print('Saving the history to:', filepath) 31 | 32 | step_breaks = np.array(self._stepBreaks, dtype='uint32') 33 | history_ints = np.array(self._history_ints, dtype='uint32') 34 | history_flaots = np.array(self._history_floats, dtype='float16') 35 | 36 | n_ints = 5 + config['n_attributes'] 37 | genome_ints = np.empty((len(self._genomes), n_ints), dtype='uint32') 38 | genome_floats = np.empty((len(self._genomes), 4), dtype='float32') 39 | 40 | for i, genome in enumerate(self._genomes.values()): 41 | genome_ints[i, 0] = genome.id 42 | genome_ints[i, 1] = genome.parent 43 | genome_ints[i, 2:5] = genome.color 44 | genome_ints[i, 5:] = genome.attributes 45 | 46 | genome_floats[i, 0] = genome.fight 47 | genome_floats[i, 1] = genome.grow 48 | genome_floats[i, 2] = genome.seed 49 | genome_floats[i, 3] = genome.seed_size 50 | 51 | np.savez(filepath, step_breaks, history_ints, history_flaots, \ 52 | genome_ints, genome_floats) 53 | self._init() 54 | 55 | class History(object): 56 | def __init__(self): 57 | self.data = [] 58 | self.genomes = {} 59 | 60 | def addGeneration(self, sim): 61 | genome_area = Counter() 62 | genome_count = Counter() 63 | 64 | for ind in sim.individuals.values(): 65 | genome_area[ind.genome.id] += ind.area() 66 | genome_count[ind.genome.id] += 1 67 | self.genomes[ind.genome.id] = ind.genome 68 | 69 | gen_data = [(id, n, genome_area[id]/n) for id,n in genome_count.items()] 70 | self.data.append(gen_data) 71 | 72 | def save(self, filepath, config): 73 | print('Saving the history to:', filepath) 74 | pickle.dump((self.data, self.genomes), open(filepath, 'wb'), protocol=-1) 75 | -------------------------------------------------------------------------------- /ecosim/individual.pxd: -------------------------------------------------------------------------------- 1 | cdef class Individual: 2 | cdef public int id, next_seeds, seed_size 3 | cdef public object genome 4 | cdef public double x, y, radius, start_radius, next_radius, energy, grow, \ 5 | seed, growth_cost_multiplier, seed_cost_multiplier 6 | cdef public bint alive, blocked, has_bias 7 | cdef public double[:] attributes 8 | 9 | cpdef double area(self) 10 | cpdef void update(self) 11 | cpdef double combatWinProbability(self, other) 12 | cpdef tuple combat(self, other) 13 | -------------------------------------------------------------------------------- /ecosim/individual.pyx: -------------------------------------------------------------------------------- 1 | # cython: boundscheck=False 2 | # cython: wraparound=False 3 | # cython: initializedcheck=False 4 | # cython: nonecheck=False 5 | # cython: cdivision=True 6 | 7 | from __future__ import print_function, division 8 | from libc.math cimport abs, floor, sqrt 9 | from libc.math cimport M_PI as pi 10 | 11 | from .utils import area_to_radius 12 | from random import random 13 | 14 | cdef class Individual(object): 15 | def __init__(self, id, genome, attributes, x, y, radius, energy,\ 16 | growth_cost_multiplier, seed_cost_multiplier, has_bias): 17 | self.id = id 18 | self.genome = genome 19 | self.x = x 20 | self.y = y 21 | self.attributes = attributes 22 | self.start_radius = radius 23 | self.radius = radius 24 | self.next_radius = radius 25 | self.energy = energy 26 | self.next_seeds = 0 27 | self.alive = True 28 | self.has_bias = has_bias 29 | 30 | self.grow = self.genome.grow 31 | self.seed = self.genome.seed 32 | self.seed_size = self.genome.seed_size 33 | 34 | self.growth_cost_multiplier = growth_cost_multiplier 35 | self.seed_cost_multiplier = seed_cost_multiplier 36 | 37 | cpdef double area(self): 38 | return pi * self.radius * self.radius 39 | 40 | cpdef void update(self): 41 | cdef double ind_area = self.area() 42 | 43 | # energy is based on it and 3/4 power rule. 44 | # https://www.ncbi.nlm.nih.gov/pmc/articles/PMC33381/ 45 | cdef double new_energy = ind_area**.75 46 | cdef double grow_energy = sqrt(new_energy * self.grow) 47 | cdef double seed_energy = sqrt(new_energy * self.seed) 48 | 49 | grow_energy /= self.growth_cost_multiplier 50 | seed_energy /= self.seed_cost_multiplier 51 | self.energy += seed_energy 52 | 53 | cdef double new_area = ind_area + grow_energy 54 | self.radius = sqrt(new_area/pi) 55 | 56 | # Number of seeds. 57 | cdef int num_seeds = int((self.energy) / self.seed_size) 58 | 59 | self.energy -= num_seeds * self.seed_size 60 | self.next_seeds = num_seeds 61 | 62 | cpdef double combatWinProbability(self, other): 63 | cdef double max_a = self.attributes[0] 64 | cdef double max_b = other.attributes[0] 65 | cdef double max_diff = abs(max_a - max_b) 66 | cdef double a1, a2, w1, w2 67 | 68 | for a1, a2 in zip(self.attributes, other.attributes): 69 | if abs(a1 - a2) > max_diff: 70 | max_a = a1 71 | max_b = a2 72 | max_diff = abs(a1 - a2) 73 | 74 | w1, w2 = max_a, max_b 75 | 76 | w1 *= self.area() * self.genome.fight 77 | w2 *= other.area() * other.genome.fight 78 | 79 | if w1 == w2: # handle 0 case too. 80 | return .5 81 | else: 82 | return w1 / (w1 + w2) 83 | 84 | cpdef tuple combat(self, other): 85 | """ Return who outcompetes whom. (winner, loser) tuple. 86 | """ 87 | cdef double p = self.combatWinProbability(other) 88 | return (self, other) if random() < p else (other, self) 89 | -------------------------------------------------------------------------------- /ecosim/main.py: -------------------------------------------------------------------------------- 1 | import time 2 | from os import path, makedirs 3 | import shutil 4 | 5 | from ecosim.draw import PygameDraw 6 | from ecosim.simulation import Simulation 7 | from ecosim.history import History, HistoryFull 8 | 9 | ################################################################################ 10 | # Util functions. 11 | 12 | def draw_sim(view, sim): 13 | view.start_draw() 14 | for ind in sim.individuals.values(): 15 | view.draw_circle((ind.x, ind.y), ind.radius, ind.genome.color, 0) 16 | 17 | view.end_draw() 18 | 19 | def prepare_dir(dir): 20 | if path.exists(dir): 21 | shutil.rmtree(dir) 22 | makedirs(dir) 23 | 24 | ################################################################################ 25 | 26 | def main(config, timesteps, out_dir, log_interval, img_interval, draw_scale, \ 27 | archive_interval): 28 | 29 | if out_dir is not None: 30 | assert not path.exists(out_dir) 31 | 32 | assert timesteps > 0 33 | 34 | logfull = False 35 | 36 | start = time.time() 37 | 38 | sim = Simulation(config) 39 | 40 | if logfull: 41 | log = HistoryFull() 42 | else: 43 | log = History() 44 | 45 | if out_dir is not None: 46 | scale = draw_scale 47 | width = int(config['width']*scale) 48 | height = int(config['height']*scale) 49 | view = PygameDraw(width, height, scale=scale) 50 | prepare_dir(path.join(out_dir, 'imgs')) 51 | draw_sim(view, sim) 52 | 53 | with open(path.join(out_dir, 'config.txt'), 'w+') as fconfig: 54 | for key, value in config.items(): 55 | fconfig.write(key+'\t'+str(value)+'\n') 56 | 57 | for i in range(timesteps): 58 | sim.step() 59 | 60 | if out_dir is not None: 61 | log.addGeneration(sim) 62 | 63 | if log_interval != -1 and i % log_interval == 0: 64 | print('Step:', i) 65 | genomes = set(ind.genome.id for ind in sim.individuals.values()) 66 | print('n_individuals:', len(sim.individuals)) 67 | print('n_genomes:', len(genomes)) 68 | print() 69 | 70 | if out_dir is not None: 71 | if img_interval != -1 and i % img_interval == 0: 72 | if out_dir is not None: 73 | draw_sim(view, sim) 74 | view.save(path.join(out_dir, 'imgs/%06d.jpg'%i)) 75 | 76 | if archive_interval != -1 and i % archive_interval == 0 and i > 0: 77 | log.save(path.join(out_dir, 'archive_%i'%i), config) 78 | 79 | print('Done in:', time.time() - start) 80 | 81 | if out_dir is not None: 82 | log.save(path.join(out_dir, 'archive_final'), config) 83 | 84 | -------------------------------------------------------------------------------- /ecosim/simulation.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function, division 2 | from math import pi, sqrt, hypot 3 | from random import random, randint, uniform, shuffle 4 | from collections import namedtuple 5 | import numpy as np 6 | 7 | from .collisiongrid.collision_gridx import CollisionGrid 8 | 9 | from .individual import Individual 10 | from .utils import area_to_radius, random_color 11 | 12 | Genome = namedtuple('Genome', ['id', 'parent', 'fight', 'grow', 'seed', 13 | 'seed_size', 'attributes', 'color']) 14 | 15 | class Simulation(object): 16 | def __init__(self, config): 17 | ######################################################################## 18 | # Configuration. 19 | 20 | self.width = config['width'] 21 | self.height = config['height'] 22 | self.n_start = config['n_start'] 23 | self.p_death = config['p_death'] 24 | self.n_randseed = config['n_randseed'] 25 | # self.bias_areas = config['bias_areas'] 26 | self.n_attributes = config['n_attributes'] 27 | self.seed_size_range = config['seed_size_range'] # Starting seed size (IN AREA) 28 | self.p_disturbance = config['p_disturbance'] 29 | self.disturbance_power = config['disturbance_power'] 30 | self.seed_cost_multiplier = config['seed_cost_multiplier'] 31 | self.growth_cost_multiplier = config['growth_cost_multiplier'] 32 | self.max_radius = min(self.width, self.height) / 2.0 33 | 34 | self.start_grid = np.load('../data/circle.npy') 35 | # self.start_grid = np.load('../data/circle_gradient.npy') 36 | self.grid_width = self.width / self.start_grid.shape[1] 37 | self.grid_height = self.height / self.start_grid.shape[0] 38 | 39 | 40 | 41 | self.bias_map = None 42 | if config['bias_map']: 43 | self.bias_map = np.load(config['bias_map']) 44 | 45 | # # print(self.bias_areas) 46 | 47 | self.bias_vectors = [] 48 | # for _, _, _, strength in self.bias_areas: 49 | # bias_vector = np.random.rand(self.n_attributes) * strength 50 | # self.bias_vectors.append(bias_vector) 51 | 52 | ######################################################################## 53 | # Counters. 54 | self.next_ind_id = 0 55 | self.next_gen_id = 0 56 | self.step_count = 0 57 | 58 | ######################################################################## 59 | # Core objects. 60 | self.individuals = dict() 61 | self.genomes = dict() 62 | self.world = CollisionGrid(self.width, self.height, 2) 63 | 64 | ######################################################################## 65 | # Create intitial population. 66 | n_copies = 5 67 | for _ in range(self.n_start//n_copies): 68 | g = self.randomGenome() 69 | for _ in range(n_copies): 70 | x = random() * self.width 71 | y = random() * self.height 72 | self.createIndividual(g, x, y, g.seed_size) 73 | 74 | ######################################################################## 75 | 76 | def randomGenome(self): 77 | seed_size = uniform(*self.seed_size_range) 78 | fight, grow, seed = random(), random(), random() 79 | s = fight+grow+seed 80 | 81 | attributes = np.random.rand(self.n_attributes) 82 | attributes /= (attributes.sum() / self.n_attributes) 83 | 84 | color = random_color()#saturation=1.0, brightness=.7) 85 | id = self.next_gen_id 86 | genome = Genome(id, id, fight/s, grow/s, seed/s, seed_size, attributes, color) 87 | self.genomes[genome.id] = genome 88 | self.next_gen_id += 1 89 | 90 | return genome 91 | 92 | def createIndividual(self, genome, x, y, seed_size): 93 | """ Returns the individuals id. 94 | """ 95 | radius = area_to_radius(seed_size) 96 | 97 | if not self.world.isEmpty(x, y, radius): 98 | return None 99 | 100 | row, col = int(y//self.grid_height), int(x//self.grid_width) 101 | 102 | prob_starting = self.start_grid[row, col] 103 | if random() > prob_starting: 104 | return None 105 | 106 | energy = pi * radius * radius 107 | has_bias = False 108 | attributes = genome.attributes.copy() 109 | 110 | if self.bias_map is not None: 111 | 112 | attributes *= self.bias_map[row, col] 113 | 114 | # for (bx, by, br, _), vector in zip(self.bias_areas, self.bias_vectors): 115 | # bx *= self.width 116 | # by *= self.height 117 | # br *= min(self.width, self.height) 118 | 119 | # if self.distance(x, y, bx, by) < radius + br: 120 | # has_bias = True 121 | # attributes *= vector 122 | 123 | ind = Individual(self.next_ind_id, genome, attributes, x, y, radius, \ 124 | energy, self.growth_cost_multiplier, \ 125 | self.seed_cost_multiplier, has_bias) 126 | 127 | self.next_ind_id += 1 128 | self.individuals[ind.id] = ind 129 | 130 | # Add to spatial grid. 131 | self.world.insertParticle(ind.id, x, y, radius) 132 | 133 | return ind 134 | 135 | def destroyIndividual(self, individual): 136 | individual.alive = False 137 | del self.individuals[individual.id] 138 | self.world.removeParticle(individual.id) 139 | 140 | def disturbRectangle(self): 141 | x0 = random()*(self.width * (1 - self.disturbance_power)) 142 | y0 = random()*(self.height * (1 - self.disturbance_power)) 143 | x1 = x0 + self.width*self.disturbance_power 144 | y1 = y0 + self.height*self.disturbance_power 145 | 146 | for id in self.world.query(x0, y0, x1, y1): 147 | self.destroyIndividual(self.individuals[id]) 148 | 149 | def stepSpreadSeeds(self): 150 | """ Spread seeds. 151 | """ 152 | n_randseed = int(self.n_randseed * max(0, (5000 - self.step_count) / 5000)) 153 | # if self.step_count < 5000: 154 | for _ in range(n_randseed): 155 | genome = self.randomGenome() 156 | x = random() * (self.width) 157 | y = random() * self.height 158 | self.createIndividual(genome, x, y, genome.seed_size) 159 | 160 | to_seed = [] 161 | for individual in self.individuals.values(): 162 | to_seed.append((individual.next_seeds, individual.genome)) 163 | 164 | for n, genome in to_seed: 165 | for _ in range(n): 166 | x, y = random() * self.width, random() * self.height 167 | self.createIndividual(genome, x, y, genome.seed_size) 168 | 169 | def distance(self, x1, y1, x2, y2): 170 | """ Distance function with periodic boundaries #TODO. 171 | """ 172 | dx = x1 - x2 173 | # if abs(dx) > self.width*0.5: 174 | # dx = self.width - dx 175 | dy = y1 - y2 176 | # if abs(dy) > self.height*0.5: 177 | # dy = self.height - dy 178 | return hypot(dx, dy) 179 | 180 | def updateRadius(self, individual): 181 | self.world.updateRadius(individual.id, individual.radius) 182 | 183 | def individualOverlap(self, individual): 184 | x0 = individual.x - individual.radius 185 | y0 = individual.y - individual.radius 186 | x1 = individual.x + individual.radius 187 | y1 = individual.y + individual.radius 188 | return self.world.query(x0, y0, x1, y1) 189 | # return self.world.queryCircle(individual.x, individual.y, individual.radius) 190 | 191 | def step(self): 192 | for individual in self.individuals.values(): 193 | individual.blocked = False 194 | 195 | # Store in seperate list to avoid editing dict during iteration. 196 | to_kill = [] 197 | 198 | for id, individual in self.individuals.items(): 199 | 200 | # Died from combat this turn or already lost a fight 201 | if not individual.alive or individual.blocked: 202 | continue 203 | 204 | # Chance of random death. 205 | if random() < self.p_death: 206 | individual.alive = False 207 | to_kill.append(individual) 208 | continue 209 | 210 | # Update individuals. 211 | individual.update() 212 | 213 | # Query by individuals new size. 214 | for id_other in self.world.queryCircle(individual.x, individual.y, \ 215 | individual.radius): 216 | if id_other == id: 217 | continue 218 | 219 | ind_other = self.individuals[id_other] 220 | 221 | if not ind_other.alive: 222 | continue 223 | 224 | dist = self.distance(individual.x, individual.y, ind_other.x, \ 225 | ind_other.y) 226 | 227 | winner, loser = individual.combat(ind_other) 228 | 229 | # The loser shrinks. 230 | loser.blocked = True 231 | 232 | loser.radius = (dist - winner.radius) * .95 233 | 234 | killed = loser.radius <= loser.start_radius 235 | 236 | if killed: 237 | loser.alive = False 238 | to_kill.append(loser) 239 | 240 | # Stop checking others if this individual lost. 241 | if loser is individual: 242 | break 243 | 244 | if loser is not individual and not killed: 245 | self.updateRadius(loser) 246 | 247 | if individual.alive: 248 | self.updateRadius(individual) 249 | 250 | for individual in to_kill: 251 | self.destroyIndividual(individual) 252 | 253 | self.stepSpreadSeeds() 254 | 255 | self.step_count += 1 256 | 257 | def isValid(self): 258 | for id1, ind1 in self.individuals.items(): 259 | assert ind1.radius == self.world.particles[id1][2] 260 | 261 | for id1, ind1 in self.individuals.items(): 262 | for id2, ind2 in self.individuals.items(): 263 | if id1 == id2: 264 | continue 265 | d = self.distance(ind1.x, ind1.y, ind2.x, ind2.y) 266 | if d < ind1.radius + ind2.radius: 267 | print(d, ind1.radius+ind2.radius,(ind1.x, ind1.y, ind1.radius), (ind2.x, ind2.y, ind2.radius)) 268 | print(id1, id2) 269 | return False 270 | return True 271 | -------------------------------------------------------------------------------- /ecosim/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function, division 2 | from math import pi, sqrt 3 | from random import random, randint 4 | import numpy as np 5 | import colorsys 6 | 7 | ################################################################################ 8 | """ Utils 9 | """ 10 | def area_to_radius(area): 11 | return sqrt(area/pi) 12 | 13 | def random_color(base=None, saturation=.50, brightness=.90): 14 | """ Returns a 3-tuple of integers in range [0, 255] 15 | """ 16 | if base is None: 17 | r, g, b = colorsys.hsv_to_rgb(random(), saturation, brightness) 18 | return int(r*255), int(g*255), int(b*255) 19 | else: 20 | r2, g2, b2 = base 21 | return int((r+r2)/2), int((g+g2)/2), int((b+b2)/2) 22 | 23 | 24 | -------------------------------------------------------------------------------- /examples/default.config: -------------------------------------------------------------------------------- 1 | [simulation] 2 | width = 600 3 | height = 600 4 | n_start = 300 5 | p_death = .0078125 6 | n_randseed = 12 7 | p_disturbance = 0.0 8 | disturbance_power = 0 9 | seed_cost_multiplier = 40 10 | growth_cost_multiplier = 20 11 | bias_map = 12 | 13 | [genome] 14 | n_attributes = 5 15 | min_seed_size = 1.0 16 | max_seed_size = 3.0 17 | -------------------------------------------------------------------------------- /examples/disturbance.config: -------------------------------------------------------------------------------- 1 | [simulation] 2 | width = 300 3 | height = 300 4 | n_start = 300 5 | p_death = .000 6 | n_randseed = 6 7 | p_disturbance = 0.125 8 | disturbance_power = .25 9 | seed_cost_multiplier = 20 10 | growth_cost_multiplier = 10 11 | 12 | [genome] 13 | n_attributes = 5 14 | min_seed_size = 1.0 15 | max_seed_size = 5.0 16 | -------------------------------------------------------------------------------- /examples/large_four.config: -------------------------------------------------------------------------------- 1 | [simulation] 2 | width = 800 3 | height = 800 4 | n_start = 300 5 | p_death = .0078125 6 | n_randseed = 16 7 | p_disturbance = 0.0 8 | disturbance_power = 0 9 | seed_cost_multiplier = 20 10 | growth_cost_multiplier = 20 11 | bias_map = ../data/bias_grid_four.npy 12 | 13 | [genome] 14 | n_attributes = 5 15 | min_seed_size = 1.0 16 | max_seed_size = 2.0 17 | -------------------------------------------------------------------------------- /examples/profile.config: -------------------------------------------------------------------------------- 1 | [simulation] 2 | width = 200 3 | height = 200 4 | n_start = 300 5 | p_death = .0078125 6 | n_randseed = 6 7 | p_disturbance = 0.0 8 | disturbance_power = 0 9 | seed_cost_multiplier = 20 10 | growth_cost_multiplier = 20 11 | 12 | [genome] 13 | n_attributes = 5 14 | min_seed_size = 1.0 15 | max_seed_size = 2.0 16 | -------------------------------------------------------------------------------- /examples/seed_cost.config: -------------------------------------------------------------------------------- 1 | [simulation] 2 | width = 400 3 | height = 400 4 | n_start = 300 5 | p_death = .0078125 6 | bias_power = 1.0 7 | n_randseed = 8 8 | p_disturbance = 0.0 9 | disturbance_power = 0 10 | seed_cost_multiplier = 60 11 | growth_cost_multiplier = 20 12 | 13 | [genome] 14 | n_attributes = 5 15 | min_seed_size = 1.0 16 | max_seed_size = 2.0 17 | -------------------------------------------------------------------------------- /examples/two_regions.config: -------------------------------------------------------------------------------- 1 | [simulation] 2 | width = 400 3 | height = 400 4 | n_start = 300 5 | p_death = .0078125 6 | n_randseed = 8 7 | p_disturbance = 0.0 8 | disturbance_power = 0 9 | seed_cost_multiplier = 30 10 | growth_cost_multiplier = 20 11 | bias_areas = .2929 .2929 .2929 4.0 .7071 .7071 .2929 4.0 12 | 13 | [genome] 14 | n_attributes = 5 15 | min_seed_size = 1.0 16 | max_seed_size = 2.0 17 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | try: 4 | from setuptools import setup 5 | from setuptools.extension import Extension 6 | except Exception: 7 | from distutils.core import setup 8 | from distutils.extension import Extension 9 | 10 | from Cython.Build import cythonize 11 | from Cython.Distutils import build_ext 12 | import numpy 13 | 14 | extensions = [ 15 | Extension('ecosim.collisiongrid.collision_gridx', 16 | ['ecosim/collisiongrid/collision_gridx.pyx']), 17 | Extension('ecosim.individual', ['ecosim/individual.pyx']), 18 | ] 19 | 20 | setup( 21 | name = "ecosim", 22 | version = '0.1.0', 23 | author = 'Joel Simon (joelsimon.net)', 24 | install_requires = ['numpy', 'cython'], 25 | license = 'MIT', 26 | cmdclass={'build_ext' : build_ext}, 27 | include_dirs = [numpy.get_include()], 28 | 29 | ext_modules = cythonize( 30 | extensions, 31 | include_path = [numpy.get_include()], 32 | ) 33 | ) 34 | -------------------------------------------------------------------------------- /tests/test_collisiongrid.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from ecosim.collisiongrid.collision_gridx import CollisionGrid 3 | 4 | class TestRandomlyDistribute(unittest.TestCase): 5 | def test_init(self): 6 | world = CollisionGrid(20, 20, 1) 7 | self.assertEqual(world.nx, 20) 8 | self.assertEqual(world.ny, 20) 9 | self.assertEqual(world.blocksize, 1) 10 | 11 | def test_grid_add(self): 12 | world = CollisionGrid(20, 20, 1) 13 | world.grid_add(0, 0, 0) 14 | world.grid_add(1, 0, 0) 15 | self.assertEqual(world.get_block(0, 0), [1, 0]) 16 | 17 | def test_grid_remove(self): 18 | world = CollisionGrid(20, 20, 1) 19 | world.grid_add(0, 0, 0) 20 | world.grid_add(1, 0, 0) 21 | self.assertEqual(world.get_block(0, 0), [1, 0]) 22 | 23 | world.grid_remove(0, 0, 0) 24 | self.assertEqual(world.get_block(0, 0), [1]) 25 | 26 | world.grid_remove(1, 0, 0) 27 | self.assertEqual(world.get_block(0, 0), []) 28 | 29 | def test_add_particle(self): 30 | world = CollisionGrid(20, 20, 1) 31 | world.insertParticle(id=0, x=10, y=10, r=1) 32 | 33 | def test_remove_particle(self): 34 | world = CollisionGrid(20, 20, 1) 35 | world.insertParticle(id=0, x=10.1, y=10.1, r=.9) 36 | world.insertParticle(id=1, x=10.1, y=10.1, r=.9) 37 | world.insertParticle(id=2, x=10.1, y=10.1, r=.9) 38 | 39 | world.removeParticle(1) 40 | 41 | # for y in range(20): 42 | # print([world.get_block(x, y) for x in range(20)]) 43 | 44 | print(world.isEmpty(10, 10, 1)) 45 | print(world.isEmpty(5, 5, 1)) 46 | print(world.query(9, 9, 11, 11)) 47 | 48 | 49 | if __name__ == '__main__': 50 | unittest.main() 51 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | # import unittest 2 | # from main import randomly_distribute 3 | 4 | # class TestRandomlyDistribute(unittest.TestCase): 5 | # def test_random_distribute(self): 6 | # tokens = 10 7 | # for i in range(10): 8 | # groups = 5 + i 9 | # result = randomly_distribute(tokens, groups) 10 | # self.assertEqual(sum(result), tokens) 11 | # self.assertEqual(len(result), groups) 12 | 13 | # if __name__ == '__main__': 14 | # unittest.main() 15 | -------------------------------------------------------------------------------- /time_collisions.py: -------------------------------------------------------------------------------- 1 | from math import sqrt 2 | import time 3 | import random 4 | # import aabb 5 | from ecosim.collisiongrid import collision_gridx, collision_grid 6 | 7 | worldx = collision_gridx.CollisionGrid(20, 20, 1) 8 | world = collision_grid.CollisionGrid(20, 20, 1) 9 | 10 | def overlaps(x1, y1, r1, x2, y2, r2): 11 | return sqrt((x1 - x2)**2 + (y1 - y2)**2) < r1 + r2 12 | 13 | def collisions_brute(points): 14 | c = set() 15 | for i1, (x1, y1, r1) in points.items(): 16 | for i2, (x2, y2, r2) in points.items(): 17 | if i1 < i2 and overlaps(x1, y1, r1, x2, y2, r2): 18 | c.add((i1, i2)) 19 | return c 20 | 21 | def collisions_grid(points, grid): 22 | c = set() 23 | for i1, (x1, y1, r1) in points.items(): 24 | for i2 in grid.query(x1-r1, y1-r1, x1+r1, y1+r1): 25 | x2, y2, r2 = points[i2] 26 | if i1 < i2 and overlaps(x1, y1, r1, x2, y2, r2): 27 | c.add((i1, i2)) 28 | return c 29 | 30 | def time_world(world): 31 | points = dict() 32 | start = time.time() 33 | random.seed(123) 34 | idx = 0 35 | 36 | for _ in range(500): 37 | x = random.random() * 20 38 | y = random.random() * 20 39 | r = random.random() * 2 40 | 41 | world.insertParticle(idx, x, y, r) 42 | points[idx] = (x, y, r) 43 | idx += 1 44 | 45 | for _ in range(10): 46 | 47 | for _ in range(5): 48 | kid = random.choice(list(points.keys())) 49 | del points[kid] 50 | world.removeParticle(kid) 51 | 52 | x = random.random() * 20 53 | y = random.random() * 20 54 | r = random.random() * 2 55 | 56 | world.insertParticle(idx, x, y, r) 57 | points[idx] = (x, y, r) 58 | idx += 1 59 | 60 | 61 | for i, (id, (x, y, r)) in enumerate(list(points.items())): 62 | r *= 1.1 63 | world.updateRadius(id, r) 64 | 65 | collisions = collisions_grid(points, world) 66 | print(len(collisions), time.time() - start) 67 | assert(collisions == collisions_brute(points)) 68 | 69 | time_world(worldx) 70 | time_world(world) 71 | 72 | # for _ in range(100): 73 | # w = random.random() * 2 74 | # x = random.random() * 18 75 | # y = random.random() * 10 76 | # # q1 = world.query(x, y, x+2, y+w) 77 | # # q2 = worldx.query(x, y, x+2, y+w) 78 | 79 | # q1 = world.isEmpty(x, y, w) 80 | # q2 = worldx.isEmpty(x, y, w) 81 | # assert q1 == q2, (q1, q2) 82 | --------------------------------------------------------------------------------