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        "lineage_id",
 28        "parent_lineage_id",
 29        "positions",
 30    )
 31
 32    def __init__(
 33        self,
 34        genomes: Genomes,
 35        ages,
 36        births,
 37        birthdays,
 38        phenotypes: Phenotypes,
 39        infection,
 40        sizes,
 41        sexes,
 42        generations=None,
 43        ancestry=None,
 44        lineage_id=None,
 45        parent_lineage_id=None,
 46        positions=None,
 47    ):
 48        self.genomes = genomes
 49        self.ages = ages
 50        self.births = births
 51        self.birthdays = birthdays
 52        self.phenotypes = phenotypes
 53        self.infection = infection
 54        self.sizes = sizes
 55        self.sexes = sexes
 56        self.generations = generations
 57        # ancestry: bool array, same shape as genomes.array; True = introgressed from source pop.
 58        # None when introgression tracking is disabled (standard runs).
 59        self.ancestry = ancestry
 60        # lineage_id, parent_lineage_id: int64 arrays of length len(genomes).
 61        # Both None when LINEAGE_TRACING is disabled (standard runs).
 62        self.lineage_id = lineage_id
 63        self.parent_lineage_id = parent_lineage_id
 64        # positions: int32 array of shape (n, 2) holding (q, r) axial hex coordinates
 65        # on the toroidal lattice. None when LATTICE_MODE is disabled (standard runs).
 66        self.positions = positions
 67
 68        assert isinstance(phenotypes, Phenotypes)
 69
 70        if not (
 71            len(genomes)
 72            == len(ages)
 73            == len(births)
 74            == len(birthdays)
 75            == len(phenotypes)
 76            == len(infection)
 77            == len(sizes)
 78            == len(sexes)
 79            # == len(generations)
 80        ):
 81            raise ValueError("Population attributes must have equal length")
 82
 83    def __setstate__(self, state):
 84        # Backward compat: pickles saved before ancestry/lineage/positions fields were added
 85        self.__dict__.update(state)
 86        if "ancestry" not in self.__dict__:
 87            self.ancestry = None
 88        if "lineage_id" not in self.__dict__:
 89            self.lineage_id = None
 90        if "parent_lineage_id" not in self.__dict__:
 91            self.parent_lineage_id = None
 92        if "positions" not in self.__dict__:
 93            self.positions = None
 94
 95    def __len__(self):
 96        """Return the number of living individuals."""
 97        return len(self.genomes)
 98
 99    def __getitem__(self, index):
100        """Return a subpopulation."""
101        return Population(
102            genomes=self.genomes.get(individuals=index),
103            ages=self.ages[index],
104            births=self.births[index],
105            birthdays=self.birthdays[index],
106            phenotypes=self.phenotypes.get(individuals=index),
107            infection=self.infection[index],
108            sizes=self.sizes[index],
109            sexes=self.sexes[index],
110            generations=self.generations[index] if self.generations is not None else None,
111            ancestry=self.ancestry[index] if self.ancestry is not None else None,
112            lineage_id=self.lineage_id[index] if self.lineage_id is not None else None,
113            parent_lineage_id=self.parent_lineage_id[index] if self.parent_lineage_id is not None else None,
114            positions=self.positions[index] if self.positions is not None else None,
115        )
116
117    def __imul__(self, index):
118        """Redefine itself as its own subpopulation."""
119        for attr in self.attrs:
120            if attr == "genomes":
121                self.genomes.keep(individuals=index)
122            elif attr == "phenotypes":
123                self.phenotypes.keep(individuals=index)
124            elif attr == "generations":
125                self.generations = None
126            elif attr == "ancestry":
127                if self.ancestry is not None:
128                    self.ancestry = self.ancestry[index]
129            elif attr in ("lineage_id", "parent_lineage_id", "positions"):
130                current = getattr(self, attr)
131                if current is not None:
132                    setattr(self, attr, current[index])
133            else:
134                setattr(self, attr, getattr(self, attr)[index])
135        return self
136
137    def __iadd__(self, population):
138        """Merge with another population."""
139
140        for attr in self.attrs:
141            if attr == "genomes":
142                self.genomes.add(population.genomes)
143            elif attr == "phenotypes":
144                assert isinstance(population.phenotypes, Phenotypes)
145                self.phenotypes.add(population.phenotypes)
146            elif attr == "generations":
147                self.generations = None
148            elif attr == "ancestry":
149                if self.ancestry is not None and population.ancestry is not None:
150                    self.ancestry = np.concatenate([self.ancestry, population.ancestry])
151                elif self.ancestry is not None or population.ancestry is not None:
152                    # one side has ancestry tracking, the other doesn't — treat missing as all-native
153                    a = self.ancestry if self.ancestry is not None else np.zeros(self.genomes.array.shape, dtype=np.bool_)
154                    b = population.ancestry if population.ancestry is not None else np.zeros(population.genomes.array.shape, dtype=np.bool_)
155                    self.ancestry = np.concatenate([a, b])
156            elif attr in ("lineage_id", "parent_lineage_id"):
157                self_val = getattr(self, attr)
158                other_val = getattr(population, attr)
159                if self_val is None and other_val is None:
160                    continue
161                # If only one side has lineage IDs, treat the missing side as -1 sentinels.
162                # (No lineage tracking on one side means we cannot reconstruct ancestry — keep what we have.)
163                if self_val is None:
164                    self_val = np.full(len(self.genomes), -1, dtype=np.int64)
165                if other_val is None:
166                    other_val = np.full(len(population.genomes), -1, dtype=np.int64)
167                setattr(self, attr, np.concatenate([self_val, other_val]))
168            elif attr == "positions":
169                self_val = self.positions
170                other_val = population.positions
171                if self_val is None and other_val is None:
172                    continue
173                # Merging spatial + non-spatial populations is unusual. If one side has
174                # positions and the other doesn't, sentinel-fill (-1, -1) for the missing side.
175                # Lattice submodel must claim cells for these once the merge is realized.
176                if self_val is None:
177                    self_val = np.full((len(self.genomes), 2), -1, dtype=np.int32)
178                if other_val is None:
179                    other_val = np.full((len(population.genomes), 2), -1, dtype=np.int32)
180                self.positions = np.concatenate([self_val, other_val])
181            else:
182                val = np.concatenate([getattr(self, attr), getattr(population, attr)])
183                setattr(self, attr, val)
184        return self
185
186    # def shuffle(self):
187    #     order = np.random.arange(len(self))
188    #     np.random.shuffle(order)
189    #     self *= order
190
191    @staticmethod
192    def load_pickle_from(path: pathlib.Path):
193        assert path.exists(), f"pickle_path {path} does not exist"
194        with open(path, "rb") as file_:
195            return pickle.load(file_)
196
197    def save_pickle_to(self, path):
198        with open(path, "wb") as file_:
199            pickle.dump(self, file_)
200
201    @staticmethod
202    def initialize(n, AGE_LIMIT):
203        from aegis_sim.parameterization import parametermanager
204        from aegis_sim import variables
205
206        genomes = Genomes(submodels.architect.architecture.init_genome_array(n))
207        ages = np.random.randint(low=0, high=AGE_LIMIT, size=n, dtype=np.int32)
208        births = np.zeros(n, dtype=np.int32)
209        birthdays = np.zeros(n, dtype=np.int32)
210        # generations = np.zeros(n, dtype=np.int32)
211        generations = None
212
213        phenotypes = submodels.architect.__call__(genomes)
214        assert isinstance(phenotypes, Phenotypes)
215
216        infection = np.zeros(n, dtype=np.int32)
217        sizes = np.zeros(n, dtype=np.float32)
218        sexes = submodels.sexsystem.get_sex(n)
219
220        if parametermanager.parameters.LINEAGE_TRACING:
221            lineage_id = variables.next_lineage_ids(n)
222            parent_lineage_id = np.full(n, -1, dtype=np.int64)
223        else:
224            lineage_id = None
225            parent_lineage_id = None
226
227        # Spatial-model: when LATTICE_MODE is on, assign each individual a unique cell
228        # on the toroidal hex lattice. The lattice submodel owns the cell-occupancy
229        # bookkeeping; here we just record each individual's (q, r) coords. When
230        # LATTICE_MODE is off (default), positions stays None and behavior is unchanged.
231        if parametermanager.parameters.LATTICE_MODE:
232            positions = submodels.lattice.assign_initial_positions(n)
233        else:
234            positions = None
235
236        return Population(
237            genomes=genomes,
238            ages=ages,
239            births=births,
240            birthdays=birthdays,
241            generations=generations,
242            phenotypes=phenotypes,
243            infection=infection,
244            sizes=sizes,
245            sexes=sexes,
246            ancestry=None,
247            lineage_id=lineage_id,
248            parent_lineage_id=parent_lineage_id,
249            positions=positions,
250        )
251
252    @staticmethod
253    def make_eggs(
254        offspring_genomes: Genomes,
255        step,
256        offspring_sexes,
257        parental_generations,
258        offspring_ancestry=None,
259        offspring_lineage_id=None,
260        offspring_parent_lineage_id=None,
261        offspring_positions=None,
262    ):
263        n = len(offspring_genomes)
264        eggs = Population(
265            genomes=offspring_genomes,
266            ages=np.zeros(n, dtype=np.int32),
267            births=np.zeros(n, dtype=np.int32),
268            birthdays=np.zeros(n, dtype=np.int32) + step,
269            generations=None,
270            phenotypes=Phenotypes.init_phenotype_array(n),
271            infection=np.zeros(n, dtype=np.int32),
272            sizes=np.zeros(n, dtype=np.float32),
273            sexes=offspring_sexes,
274            ancestry=offspring_ancestry,
275            lineage_id=offspring_lineage_id,
276            parent_lineage_id=offspring_parent_lineage_id,
277            positions=offspring_positions,
278        )
279        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        "lineage_id",
 29        "parent_lineage_id",
 30        "positions",
 31    )
 32
 33    def __init__(
 34        self,
 35        genomes: Genomes,
 36        ages,
 37        births,
 38        birthdays,
 39        phenotypes: Phenotypes,
 40        infection,
 41        sizes,
 42        sexes,
 43        generations=None,
 44        ancestry=None,
 45        lineage_id=None,
 46        parent_lineage_id=None,
 47        positions=None,
 48    ):
 49        self.genomes = genomes
 50        self.ages = ages
 51        self.births = births
 52        self.birthdays = birthdays
 53        self.phenotypes = phenotypes
 54        self.infection = infection
 55        self.sizes = sizes
 56        self.sexes = sexes
 57        self.generations = generations
 58        # ancestry: bool array, same shape as genomes.array; True = introgressed from source pop.
 59        # None when introgression tracking is disabled (standard runs).
 60        self.ancestry = ancestry
 61        # lineage_id, parent_lineage_id: int64 arrays of length len(genomes).
 62        # Both None when LINEAGE_TRACING is disabled (standard runs).
 63        self.lineage_id = lineage_id
 64        self.parent_lineage_id = parent_lineage_id
 65        # positions: int32 array of shape (n, 2) holding (q, r) axial hex coordinates
 66        # on the toroidal lattice. None when LATTICE_MODE is disabled (standard runs).
 67        self.positions = positions
 68
 69        assert isinstance(phenotypes, Phenotypes)
 70
 71        if not (
 72            len(genomes)
 73            == len(ages)
 74            == len(births)
 75            == len(birthdays)
 76            == len(phenotypes)
 77            == len(infection)
 78            == len(sizes)
 79            == len(sexes)
 80            # == len(generations)
 81        ):
 82            raise ValueError("Population attributes must have equal length")
 83
 84    def __setstate__(self, state):
 85        # Backward compat: pickles saved before ancestry/lineage/positions fields were added
 86        self.__dict__.update(state)
 87        if "ancestry" not in self.__dict__:
 88            self.ancestry = None
 89        if "lineage_id" not in self.__dict__:
 90            self.lineage_id = None
 91        if "parent_lineage_id" not in self.__dict__:
 92            self.parent_lineage_id = None
 93        if "positions" not in self.__dict__:
 94            self.positions = None
 95
 96    def __len__(self):
 97        """Return the number of living individuals."""
 98        return len(self.genomes)
 99
100    def __getitem__(self, index):
101        """Return a subpopulation."""
102        return Population(
103            genomes=self.genomes.get(individuals=index),
104            ages=self.ages[index],
105            births=self.births[index],
106            birthdays=self.birthdays[index],
107            phenotypes=self.phenotypes.get(individuals=index),
108            infection=self.infection[index],
109            sizes=self.sizes[index],
110            sexes=self.sexes[index],
111            generations=self.generations[index] if self.generations is not None else None,
112            ancestry=self.ancestry[index] if self.ancestry is not None else None,
113            lineage_id=self.lineage_id[index] if self.lineage_id is not None else None,
114            parent_lineage_id=self.parent_lineage_id[index] if self.parent_lineage_id is not None else None,
115            positions=self.positions[index] if self.positions is not None else None,
116        )
117
118    def __imul__(self, index):
119        """Redefine itself as its own subpopulation."""
120        for attr in self.attrs:
121            if attr == "genomes":
122                self.genomes.keep(individuals=index)
123            elif attr == "phenotypes":
124                self.phenotypes.keep(individuals=index)
125            elif attr == "generations":
126                self.generations = None
127            elif attr == "ancestry":
128                if self.ancestry is not None:
129                    self.ancestry = self.ancestry[index]
130            elif attr in ("lineage_id", "parent_lineage_id", "positions"):
131                current = getattr(self, attr)
132                if current is not None:
133                    setattr(self, attr, current[index])
134            else:
135                setattr(self, attr, getattr(self, attr)[index])
136        return self
137
138    def __iadd__(self, population):
139        """Merge with another population."""
140
141        for attr in self.attrs:
142            if attr == "genomes":
143                self.genomes.add(population.genomes)
144            elif attr == "phenotypes":
145                assert isinstance(population.phenotypes, Phenotypes)
146                self.phenotypes.add(population.phenotypes)
147            elif attr == "generations":
148                self.generations = None
149            elif attr == "ancestry":
150                if self.ancestry is not None and population.ancestry is not None:
151                    self.ancestry = np.concatenate([self.ancestry, population.ancestry])
152                elif self.ancestry is not None or population.ancestry is not None:
153                    # one side has ancestry tracking, the other doesn't — treat missing as all-native
154                    a = self.ancestry if self.ancestry is not None else np.zeros(self.genomes.array.shape, dtype=np.bool_)
155                    b = population.ancestry if population.ancestry is not None else np.zeros(population.genomes.array.shape, dtype=np.bool_)
156                    self.ancestry = np.concatenate([a, b])
157            elif attr in ("lineage_id", "parent_lineage_id"):
158                self_val = getattr(self, attr)
159                other_val = getattr(population, attr)
160                if self_val is None and other_val is None:
161                    continue
162                # If only one side has lineage IDs, treat the missing side as -1 sentinels.
163                # (No lineage tracking on one side means we cannot reconstruct ancestry — keep what we have.)
164                if self_val is None:
165                    self_val = np.full(len(self.genomes), -1, dtype=np.int64)
166                if other_val is None:
167                    other_val = np.full(len(population.genomes), -1, dtype=np.int64)
168                setattr(self, attr, np.concatenate([self_val, other_val]))
169            elif attr == "positions":
170                self_val = self.positions
171                other_val = population.positions
172                if self_val is None and other_val is None:
173                    continue
174                # Merging spatial + non-spatial populations is unusual. If one side has
175                # positions and the other doesn't, sentinel-fill (-1, -1) for the missing side.
176                # Lattice submodel must claim cells for these once the merge is realized.
177                if self_val is None:
178                    self_val = np.full((len(self.genomes), 2), -1, dtype=np.int32)
179                if other_val is None:
180                    other_val = np.full((len(population.genomes), 2), -1, dtype=np.int32)
181                self.positions = np.concatenate([self_val, other_val])
182            else:
183                val = np.concatenate([getattr(self, attr), getattr(population, attr)])
184                setattr(self, attr, val)
185        return self
186
187    # def shuffle(self):
188    #     order = np.random.arange(len(self))
189    #     np.random.shuffle(order)
190    #     self *= order
191
192    @staticmethod
193    def load_pickle_from(path: pathlib.Path):
194        assert path.exists(), f"pickle_path {path} does not exist"
195        with open(path, "rb") as file_:
196            return pickle.load(file_)
197
198    def save_pickle_to(self, path):
199        with open(path, "wb") as file_:
200            pickle.dump(self, file_)
201
202    @staticmethod
203    def initialize(n, AGE_LIMIT):
204        from aegis_sim.parameterization import parametermanager
205        from aegis_sim import variables
206
207        genomes = Genomes(submodels.architect.architecture.init_genome_array(n))
208        ages = np.random.randint(low=0, high=AGE_LIMIT, size=n, dtype=np.int32)
209        births = np.zeros(n, dtype=np.int32)
210        birthdays = np.zeros(n, dtype=np.int32)
211        # generations = np.zeros(n, dtype=np.int32)
212        generations = None
213
214        phenotypes = submodels.architect.__call__(genomes)
215        assert isinstance(phenotypes, Phenotypes)
216
217        infection = np.zeros(n, dtype=np.int32)
218        sizes = np.zeros(n, dtype=np.float32)
219        sexes = submodels.sexsystem.get_sex(n)
220
221        if parametermanager.parameters.LINEAGE_TRACING:
222            lineage_id = variables.next_lineage_ids(n)
223            parent_lineage_id = np.full(n, -1, dtype=np.int64)
224        else:
225            lineage_id = None
226            parent_lineage_id = None
227
228        # Spatial-model: when LATTICE_MODE is on, assign each individual a unique cell
229        # on the toroidal hex lattice. The lattice submodel owns the cell-occupancy
230        # bookkeeping; here we just record each individual's (q, r) coords. When
231        # LATTICE_MODE is off (default), positions stays None and behavior is unchanged.
232        if parametermanager.parameters.LATTICE_MODE:
233            positions = submodels.lattice.assign_initial_positions(n)
234        else:
235            positions = None
236
237        return Population(
238            genomes=genomes,
239            ages=ages,
240            births=births,
241            birthdays=birthdays,
242            generations=generations,
243            phenotypes=phenotypes,
244            infection=infection,
245            sizes=sizes,
246            sexes=sexes,
247            ancestry=None,
248            lineage_id=lineage_id,
249            parent_lineage_id=parent_lineage_id,
250            positions=positions,
251        )
252
253    @staticmethod
254    def make_eggs(
255        offspring_genomes: Genomes,
256        step,
257        offspring_sexes,
258        parental_generations,
259        offspring_ancestry=None,
260        offspring_lineage_id=None,
261        offspring_parent_lineage_id=None,
262        offspring_positions=None,
263    ):
264        n = len(offspring_genomes)
265        eggs = Population(
266            genomes=offspring_genomes,
267            ages=np.zeros(n, dtype=np.int32),
268            births=np.zeros(n, dtype=np.int32),
269            birthdays=np.zeros(n, dtype=np.int32) + step,
270            generations=None,
271            phenotypes=Phenotypes.init_phenotype_array(n),
272            infection=np.zeros(n, dtype=np.int32),
273            sizes=np.zeros(n, dtype=np.float32),
274            sexes=offspring_sexes,
275            ancestry=offspring_ancestry,
276            lineage_id=offspring_lineage_id,
277            parent_lineage_id=offspring_parent_lineage_id,
278            positions=offspring_positions,
279        )
280        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, lineage_id=None, parent_lineage_id=None, positions=None)
33    def __init__(
34        self,
35        genomes: Genomes,
36        ages,
37        births,
38        birthdays,
39        phenotypes: Phenotypes,
40        infection,
41        sizes,
42        sexes,
43        generations=None,
44        ancestry=None,
45        lineage_id=None,
46        parent_lineage_id=None,
47        positions=None,
48    ):
49        self.genomes = genomes
50        self.ages = ages
51        self.births = births
52        self.birthdays = birthdays
53        self.phenotypes = phenotypes
54        self.infection = infection
55        self.sizes = sizes
56        self.sexes = sexes
57        self.generations = generations
58        # ancestry: bool array, same shape as genomes.array; True = introgressed from source pop.
59        # None when introgression tracking is disabled (standard runs).
60        self.ancestry = ancestry
61        # lineage_id, parent_lineage_id: int64 arrays of length len(genomes).
62        # Both None when LINEAGE_TRACING is disabled (standard runs).
63        self.lineage_id = lineage_id
64        self.parent_lineage_id = parent_lineage_id
65        # positions: int32 array of shape (n, 2) holding (q, r) axial hex coordinates
66        # on the toroidal lattice. None when LATTICE_MODE is disabled (standard runs).
67        self.positions = positions
68
69        assert isinstance(phenotypes, Phenotypes)
70
71        if not (
72            len(genomes)
73            == len(ages)
74            == len(births)
75            == len(birthdays)
76            == len(phenotypes)
77            == len(infection)
78            == len(sizes)
79            == len(sexes)
80            # == len(generations)
81        ):
82            raise ValueError("Population attributes must have equal length")
attrs = ('genomes', 'ages', 'births', 'birthdays', 'generations', 'phenotypes', 'infection', 'sizes', 'sexes', 'ancestry', 'lineage_id', 'parent_lineage_id', 'positions')
genomes
ages
births
birthdays
phenotypes
infection
sizes
sexes
generations
ancestry
lineage_id
parent_lineage_id
positions
@staticmethod
def load_pickle_from(path: pathlib.Path):
192    @staticmethod
193    def load_pickle_from(path: pathlib.Path):
194        assert path.exists(), f"pickle_path {path} does not exist"
195        with open(path, "rb") as file_:
196            return pickle.load(file_)
def save_pickle_to(self, path):
198    def save_pickle_to(self, path):
199        with open(path, "wb") as file_:
200            pickle.dump(self, file_)
@staticmethod
def initialize(n, AGE_LIMIT):
202    @staticmethod
203    def initialize(n, AGE_LIMIT):
204        from aegis_sim.parameterization import parametermanager
205        from aegis_sim import variables
206
207        genomes = Genomes(submodels.architect.architecture.init_genome_array(n))
208        ages = np.random.randint(low=0, high=AGE_LIMIT, size=n, dtype=np.int32)
209        births = np.zeros(n, dtype=np.int32)
210        birthdays = np.zeros(n, dtype=np.int32)
211        # generations = np.zeros(n, dtype=np.int32)
212        generations = None
213
214        phenotypes = submodels.architect.__call__(genomes)
215        assert isinstance(phenotypes, Phenotypes)
216
217        infection = np.zeros(n, dtype=np.int32)
218        sizes = np.zeros(n, dtype=np.float32)
219        sexes = submodels.sexsystem.get_sex(n)
220
221        if parametermanager.parameters.LINEAGE_TRACING:
222            lineage_id = variables.next_lineage_ids(n)
223            parent_lineage_id = np.full(n, -1, dtype=np.int64)
224        else:
225            lineage_id = None
226            parent_lineage_id = None
227
228        # Spatial-model: when LATTICE_MODE is on, assign each individual a unique cell
229        # on the toroidal hex lattice. The lattice submodel owns the cell-occupancy
230        # bookkeeping; here we just record each individual's (q, r) coords. When
231        # LATTICE_MODE is off (default), positions stays None and behavior is unchanged.
232        if parametermanager.parameters.LATTICE_MODE:
233            positions = submodels.lattice.assign_initial_positions(n)
234        else:
235            positions = None
236
237        return Population(
238            genomes=genomes,
239            ages=ages,
240            births=births,
241            birthdays=birthdays,
242            generations=generations,
243            phenotypes=phenotypes,
244            infection=infection,
245            sizes=sizes,
246            sexes=sexes,
247            ancestry=None,
248            lineage_id=lineage_id,
249            parent_lineage_id=parent_lineage_id,
250            positions=positions,
251        )
@staticmethod
def make_eggs( offspring_genomes: aegis_sim.dataclasses.genomes.Genomes, step, offspring_sexes, parental_generations, offspring_ancestry=None, offspring_lineage_id=None, offspring_parent_lineage_id=None, offspring_positions=None):
253    @staticmethod
254    def make_eggs(
255        offspring_genomes: Genomes,
256        step,
257        offspring_sexes,
258        parental_generations,
259        offspring_ancestry=None,
260        offspring_lineage_id=None,
261        offspring_parent_lineage_id=None,
262        offspring_positions=None,
263    ):
264        n = len(offspring_genomes)
265        eggs = Population(
266            genomes=offspring_genomes,
267            ages=np.zeros(n, dtype=np.int32),
268            births=np.zeros(n, dtype=np.int32),
269            birthdays=np.zeros(n, dtype=np.int32) + step,
270            generations=None,
271            phenotypes=Phenotypes.init_phenotype_array(n),
272            infection=np.zeros(n, dtype=np.int32),
273            sizes=np.zeros(n, dtype=np.float32),
274            sexes=offspring_sexes,
275            ancestry=offspring_ancestry,
276            lineage_id=offspring_lineage_id,
277            parent_lineage_id=offspring_parent_lineage_id,
278            positions=offspring_positions,
279        )
280        return eggs