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')
genomes
ages
births
birthdays
phenotypes
infection
sizes
sexes
generations
ancestry
@staticmethod
def load_pickle_from(path: pathlib.Path):
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_)
def save_pickle_to(self, path):
147    def save_pickle_to(self, path):
148        with open(path, "wb") as file_:
149            pickle.dump(self, file_)
@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