aegis_sim.dataclasses.population
1import numpy as np 2import pickle 3import pathlib 4 5from aegis_sim.dataclasses.genomes import Genomes 6from aegis_sim.dataclasses.phenotypes import Phenotypes 7from aegis_sim import submodels 8 9 10class Population: 11 """Population data 12 13 Contains demographic, genetic and phenotypic data of living individuals. 14 """ 15 16 attrs = ( 17 "genomes", 18 "ages", 19 "births", 20 "birthdays", 21 "generations", 22 "phenotypes", 23 "infection", 24 "sizes", 25 "sexes", 26 "ancestry", 27 ) 28 29 def __init__( 30 self, 31 genomes: Genomes, 32 ages, 33 births, 34 birthdays, 35 phenotypes: Phenotypes, 36 infection, 37 sizes, 38 sexes, 39 generations=None, 40 ancestry=None, 41 ): 42 self.genomes = genomes 43 self.ages = ages 44 self.births = births 45 self.birthdays = birthdays 46 self.phenotypes = phenotypes 47 self.infection = infection 48 self.sizes = sizes 49 self.sexes = sexes 50 self.generations = generations 51 # ancestry: bool array, same shape as genomes.array; True = introgressed from source pop. 52 # None when introgression tracking is disabled (standard runs). 53 self.ancestry = ancestry 54 55 assert isinstance(phenotypes, Phenotypes) 56 57 if not ( 58 len(genomes) 59 == len(ages) 60 == len(births) 61 == len(birthdays) 62 == len(phenotypes) 63 == len(infection) 64 == len(sizes) 65 == len(sexes) 66 # == len(generations) 67 ): 68 raise ValueError("Population attributes must have equal length") 69 70 def __setstate__(self, state): 71 # Backward compat: pickles saved before ancestry field was added 72 self.__dict__.update(state) 73 if "ancestry" not in self.__dict__: 74 self.ancestry = None 75 76 def __len__(self): 77 """Return the number of living individuals.""" 78 return len(self.genomes) 79 80 def __getitem__(self, index): 81 """Return a subpopulation.""" 82 return Population( 83 genomes=self.genomes.get(individuals=index), 84 ages=self.ages[index], 85 births=self.births[index], 86 birthdays=self.birthdays[index], 87 phenotypes=self.phenotypes.get(individuals=index), 88 infection=self.infection[index], 89 sizes=self.sizes[index], 90 sexes=self.sexes[index], 91 generations=self.generations[index] if self.generations is not None else None, 92 ancestry=self.ancestry[index] if self.ancestry is not None else None, 93 ) 94 95 def __imul__(self, index): 96 """Redefine itself as its own subpopulation.""" 97 for attr in self.attrs: 98 if attr == "genomes": 99 self.genomes.keep(individuals=index) 100 elif attr == "phenotypes": 101 self.phenotypes.keep(individuals=index) 102 elif attr == "generations": 103 self.generations = None 104 elif attr == "ancestry": 105 if self.ancestry is not None: 106 self.ancestry = self.ancestry[index] 107 else: 108 setattr(self, attr, getattr(self, attr)[index]) 109 return self 110 111 def __iadd__(self, population): 112 """Merge with another population.""" 113 114 for attr in self.attrs: 115 if attr == "genomes": 116 self.genomes.add(population.genomes) 117 elif attr == "phenotypes": 118 assert isinstance(population.phenotypes, Phenotypes) 119 self.phenotypes.add(population.phenotypes) 120 elif attr == "generations": 121 self.generations = None 122 elif attr == "ancestry": 123 if self.ancestry is not None and population.ancestry is not None: 124 self.ancestry = np.concatenate([self.ancestry, population.ancestry]) 125 elif self.ancestry is not None or population.ancestry is not None: 126 # one side has ancestry tracking, the other doesn't — treat missing as all-native 127 a = self.ancestry if self.ancestry is not None else np.zeros(self.genomes.array.shape, dtype=np.bool_) 128 b = population.ancestry if population.ancestry is not None else np.zeros(population.genomes.array.shape, dtype=np.bool_) 129 self.ancestry = np.concatenate([a, b]) 130 else: 131 val = np.concatenate([getattr(self, attr), getattr(population, attr)]) 132 setattr(self, attr, val) 133 return self 134 135 # def shuffle(self): 136 # order = np.random.arange(len(self)) 137 # np.random.shuffle(order) 138 # self *= order 139 140 @staticmethod 141 def load_pickle_from(path: pathlib.Path): 142 assert path.exists(), f"pickle_path {path} does not exist" 143 with open(path, "rb") as file_: 144 return pickle.load(file_) 145 146 def save_pickle_to(self, path): 147 with open(path, "wb") as file_: 148 pickle.dump(self, file_) 149 150 @staticmethod 151 def initialize(n, AGE_LIMIT): 152 genomes = Genomes(submodels.architect.architecture.init_genome_array(n)) 153 ages = np.random.randint(low=0, high=AGE_LIMIT, size=n, dtype=np.int32) 154 births = np.zeros(n, dtype=np.int32) 155 birthdays = np.zeros(n, dtype=np.int32) 156 # generations = np.zeros(n, dtype=np.int32) 157 generations = None 158 159 phenotypes = submodels.architect.__call__(genomes) 160 assert isinstance(phenotypes, Phenotypes) 161 162 infection = np.zeros(n, dtype=np.int32) 163 sizes = np.zeros(n, dtype=np.float32) 164 sexes = submodels.sexsystem.get_sex(n) 165 return Population( 166 genomes=genomes, 167 ages=ages, 168 births=births, 169 birthdays=birthdays, 170 generations=generations, 171 phenotypes=phenotypes, 172 infection=infection, 173 sizes=sizes, 174 sexes=sexes, 175 ancestry=None, 176 ) 177 178 @staticmethod 179 def make_eggs(offspring_genomes: Genomes, step, offspring_sexes, parental_generations, offspring_ancestry=None): 180 n = len(offspring_genomes) 181 eggs = Population( 182 genomes=offspring_genomes, 183 ages=np.zeros(n, dtype=np.int32), 184 births=np.zeros(n, dtype=np.int32), 185 birthdays=np.zeros(n, dtype=np.int32) + step, 186 generations=None, 187 phenotypes=Phenotypes.init_phenotype_array(n), 188 infection=np.zeros(n, dtype=np.int32), 189 sizes=np.zeros(n, dtype=np.float32), 190 sexes=offspring_sexes, 191 ancestry=offspring_ancestry, 192 ) 193 return eggs
class
Population:
11class Population: 12 """Population data 13 14 Contains demographic, genetic and phenotypic data of living individuals. 15 """ 16 17 attrs = ( 18 "genomes", 19 "ages", 20 "births", 21 "birthdays", 22 "generations", 23 "phenotypes", 24 "infection", 25 "sizes", 26 "sexes", 27 "ancestry", 28 ) 29 30 def __init__( 31 self, 32 genomes: Genomes, 33 ages, 34 births, 35 birthdays, 36 phenotypes: Phenotypes, 37 infection, 38 sizes, 39 sexes, 40 generations=None, 41 ancestry=None, 42 ): 43 self.genomes = genomes 44 self.ages = ages 45 self.births = births 46 self.birthdays = birthdays 47 self.phenotypes = phenotypes 48 self.infection = infection 49 self.sizes = sizes 50 self.sexes = sexes 51 self.generations = generations 52 # ancestry: bool array, same shape as genomes.array; True = introgressed from source pop. 53 # None when introgression tracking is disabled (standard runs). 54 self.ancestry = ancestry 55 56 assert isinstance(phenotypes, Phenotypes) 57 58 if not ( 59 len(genomes) 60 == len(ages) 61 == len(births) 62 == len(birthdays) 63 == len(phenotypes) 64 == len(infection) 65 == len(sizes) 66 == len(sexes) 67 # == len(generations) 68 ): 69 raise ValueError("Population attributes must have equal length") 70 71 def __setstate__(self, state): 72 # Backward compat: pickles saved before ancestry field was added 73 self.__dict__.update(state) 74 if "ancestry" not in self.__dict__: 75 self.ancestry = None 76 77 def __len__(self): 78 """Return the number of living individuals.""" 79 return len(self.genomes) 80 81 def __getitem__(self, index): 82 """Return a subpopulation.""" 83 return Population( 84 genomes=self.genomes.get(individuals=index), 85 ages=self.ages[index], 86 births=self.births[index], 87 birthdays=self.birthdays[index], 88 phenotypes=self.phenotypes.get(individuals=index), 89 infection=self.infection[index], 90 sizes=self.sizes[index], 91 sexes=self.sexes[index], 92 generations=self.generations[index] if self.generations is not None else None, 93 ancestry=self.ancestry[index] if self.ancestry is not None else None, 94 ) 95 96 def __imul__(self, index): 97 """Redefine itself as its own subpopulation.""" 98 for attr in self.attrs: 99 if attr == "genomes": 100 self.genomes.keep(individuals=index) 101 elif attr == "phenotypes": 102 self.phenotypes.keep(individuals=index) 103 elif attr == "generations": 104 self.generations = None 105 elif attr == "ancestry": 106 if self.ancestry is not None: 107 self.ancestry = self.ancestry[index] 108 else: 109 setattr(self, attr, getattr(self, attr)[index]) 110 return self 111 112 def __iadd__(self, population): 113 """Merge with another population.""" 114 115 for attr in self.attrs: 116 if attr == "genomes": 117 self.genomes.add(population.genomes) 118 elif attr == "phenotypes": 119 assert isinstance(population.phenotypes, Phenotypes) 120 self.phenotypes.add(population.phenotypes) 121 elif attr == "generations": 122 self.generations = None 123 elif attr == "ancestry": 124 if self.ancestry is not None and population.ancestry is not None: 125 self.ancestry = np.concatenate([self.ancestry, population.ancestry]) 126 elif self.ancestry is not None or population.ancestry is not None: 127 # one side has ancestry tracking, the other doesn't — treat missing as all-native 128 a = self.ancestry if self.ancestry is not None else np.zeros(self.genomes.array.shape, dtype=np.bool_) 129 b = population.ancestry if population.ancestry is not None else np.zeros(population.genomes.array.shape, dtype=np.bool_) 130 self.ancestry = np.concatenate([a, b]) 131 else: 132 val = np.concatenate([getattr(self, attr), getattr(population, attr)]) 133 setattr(self, attr, val) 134 return self 135 136 # def shuffle(self): 137 # order = np.random.arange(len(self)) 138 # np.random.shuffle(order) 139 # self *= order 140 141 @staticmethod 142 def load_pickle_from(path: pathlib.Path): 143 assert path.exists(), f"pickle_path {path} does not exist" 144 with open(path, "rb") as file_: 145 return pickle.load(file_) 146 147 def save_pickle_to(self, path): 148 with open(path, "wb") as file_: 149 pickle.dump(self, file_) 150 151 @staticmethod 152 def initialize(n, AGE_LIMIT): 153 genomes = Genomes(submodels.architect.architecture.init_genome_array(n)) 154 ages = np.random.randint(low=0, high=AGE_LIMIT, size=n, dtype=np.int32) 155 births = np.zeros(n, dtype=np.int32) 156 birthdays = np.zeros(n, dtype=np.int32) 157 # generations = np.zeros(n, dtype=np.int32) 158 generations = None 159 160 phenotypes = submodels.architect.__call__(genomes) 161 assert isinstance(phenotypes, Phenotypes) 162 163 infection = np.zeros(n, dtype=np.int32) 164 sizes = np.zeros(n, dtype=np.float32) 165 sexes = submodels.sexsystem.get_sex(n) 166 return Population( 167 genomes=genomes, 168 ages=ages, 169 births=births, 170 birthdays=birthdays, 171 generations=generations, 172 phenotypes=phenotypes, 173 infection=infection, 174 sizes=sizes, 175 sexes=sexes, 176 ancestry=None, 177 ) 178 179 @staticmethod 180 def make_eggs(offspring_genomes: Genomes, step, offspring_sexes, parental_generations, offspring_ancestry=None): 181 n = len(offspring_genomes) 182 eggs = Population( 183 genomes=offspring_genomes, 184 ages=np.zeros(n, dtype=np.int32), 185 births=np.zeros(n, dtype=np.int32), 186 birthdays=np.zeros(n, dtype=np.int32) + step, 187 generations=None, 188 phenotypes=Phenotypes.init_phenotype_array(n), 189 infection=np.zeros(n, dtype=np.int32), 190 sizes=np.zeros(n, dtype=np.float32), 191 sexes=offspring_sexes, 192 ancestry=offspring_ancestry, 193 ) 194 return eggs
Population data
Contains demographic, genetic and phenotypic data of living individuals.
Population( genomes: aegis_sim.dataclasses.genomes.Genomes, ages, births, birthdays, phenotypes: aegis_sim.dataclasses.phenotypes.Phenotypes, infection, sizes, sexes, generations=None, ancestry=None)
30 def __init__( 31 self, 32 genomes: Genomes, 33 ages, 34 births, 35 birthdays, 36 phenotypes: Phenotypes, 37 infection, 38 sizes, 39 sexes, 40 generations=None, 41 ancestry=None, 42 ): 43 self.genomes = genomes 44 self.ages = ages 45 self.births = births 46 self.birthdays = birthdays 47 self.phenotypes = phenotypes 48 self.infection = infection 49 self.sizes = sizes 50 self.sexes = sexes 51 self.generations = generations 52 # ancestry: bool array, same shape as genomes.array; True = introgressed from source pop. 53 # None when introgression tracking is disabled (standard runs). 54 self.ancestry = ancestry 55 56 assert isinstance(phenotypes, Phenotypes) 57 58 if not ( 59 len(genomes) 60 == len(ages) 61 == len(births) 62 == len(birthdays) 63 == len(phenotypes) 64 == len(infection) 65 == len(sizes) 66 == len(sexes) 67 # == len(generations) 68 ): 69 raise ValueError("Population attributes must have equal length")
attrs =
('genomes', 'ages', 'births', 'birthdays', 'generations', 'phenotypes', 'infection', 'sizes', 'sexes', 'ancestry')
@staticmethod
def
initialize(n, AGE_LIMIT):
151 @staticmethod 152 def initialize(n, AGE_LIMIT): 153 genomes = Genomes(submodels.architect.architecture.init_genome_array(n)) 154 ages = np.random.randint(low=0, high=AGE_LIMIT, size=n, dtype=np.int32) 155 births = np.zeros(n, dtype=np.int32) 156 birthdays = np.zeros(n, dtype=np.int32) 157 # generations = np.zeros(n, dtype=np.int32) 158 generations = None 159 160 phenotypes = submodels.architect.__call__(genomes) 161 assert isinstance(phenotypes, Phenotypes) 162 163 infection = np.zeros(n, dtype=np.int32) 164 sizes = np.zeros(n, dtype=np.float32) 165 sexes = submodels.sexsystem.get_sex(n) 166 return Population( 167 genomes=genomes, 168 ages=ages, 169 births=births, 170 birthdays=birthdays, 171 generations=generations, 172 phenotypes=phenotypes, 173 infection=infection, 174 sizes=sizes, 175 sexes=sexes, 176 ancestry=None, 177 )
@staticmethod
def
make_eggs( offspring_genomes: aegis_sim.dataclasses.genomes.Genomes, step, offspring_sexes, parental_generations, offspring_ancestry=None):
179 @staticmethod 180 def make_eggs(offspring_genomes: Genomes, step, offspring_sexes, parental_generations, offspring_ancestry=None): 181 n = len(offspring_genomes) 182 eggs = Population( 183 genomes=offspring_genomes, 184 ages=np.zeros(n, dtype=np.int32), 185 births=np.zeros(n, dtype=np.int32), 186 birthdays=np.zeros(n, dtype=np.int32) + step, 187 generations=None, 188 phenotypes=Phenotypes.init_phenotype_array(n), 189 infection=np.zeros(n, dtype=np.int32), 190 sizes=np.zeros(n, dtype=np.float32), 191 sexes=offspring_sexes, 192 ancestry=offspring_ancestry, 193 ) 194 return eggs