Skip to content

Commit 9fdafad

Browse files
committed
Added mutation rates
1 parent 5e7e5c3 commit 9fdafad

6 files changed

Lines changed: 533 additions & 153 deletions

File tree

docs/source/overview.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Currently, each individual's culture is represented by a set of {math}`N` featur
1818

1919
### Neutral model
2020

21-
The Neutral model follows loosely the set up selected by other authors working on the spread of cultural traits (_e.g._ *Patterns in space and time: simulating cultural transmission in archaeology*, Marko Porčić). While in most works the Neutral Model assumes some sort of mutation rate in the traits, in the form of errors in the process of copying traits, we assume no copying error and instead use the simplest model where one focal individual at random in each subpopulation and each generation copies one trait at random from another individual in the same sub-population. Because of the absence of mutations, given enough time, this process is expected to lead to uniformity in an isolated subpopulation.
21+
The Neutral model follows loosely the set up selected by other authors working on the spread of cultural traits (_e.g._ *Patterns in space and time: simulating cultural transmission in archaeology*, Marko Porčić). While in most works the Neutral Model assumes some sort of mutation rate in the traits, in the form of errors in the process of copying traits, we assume no copying error and instead use the simplest model where one focal individual at random in each subpopulation and each generation copies one trait at random from another individual in the same subpopulation. Because of the absence of mutations, given enough time, this process is expected to lead to uniformity in an isolated subpopulation.
2222

2323
### Axelrod model
2424

exploration.ipynb

Lines changed: 450 additions & 93 deletions
Large diffs are not rendered by default.

metapypulation/individual.py

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@ class Individual():
1414
original_deme_id (int): Identifier of the deme where the individual originated.
1515
number_of_features (int): Number of cultural features of the individual.
1616
number_of_traits (int): Number of traits per feature of the individual.
17+
mutation_rate (float): Probability of a random mutation to occur during cultural transmission.
1718
features (List[int]): List of features of the individual.
1819
number_of_changes (int): The number of times this individual has changed set of features following an interaction.
1920
"""
20-
def __init__(self, id: int, original_deme_id: int, number_of_features: int, number_of_traits: int, features: List = None):
21+
def __init__(self, id: int, original_deme_id: int, number_of_features: int, number_of_traits: int, mutation_rate: float = 0.0, features: List = None):
2122
"""
2223
Create a new individual with a random set of features.
2324
@@ -26,12 +27,14 @@ def __init__(self, id: int, original_deme_id: int, number_of_features: int, numb
2627
original_deme_id (int): Identifier of the deme where the individual originated.
2728
number_of_features (int): Number of cultural features of the individual.
2829
number_of_traits (int): Number of traits per feature of the individual.
30+
mutation_rate (float, optional): Probability of a random mutation to occur during cultural transmission.
2931
features (List, optional): Preset set of features of the individual. Default is None.
3032
"""
3133
self.id = id
3234
self.original_deme_id = original_deme_id
3335
self.number_of_features = number_of_features
3436
self.number_of_traits = number_of_traits
37+
self.mutation_rate = mutation_rate
3538

3639
if features is None:
3740
# number of traits is +1 as the argument high is exclusive
@@ -43,6 +46,7 @@ def __init__(self, id: int, original_deme_id: int, number_of_features: int, numb
4346
raise ValueError("The input number of features does not match the input set of features!")
4447

4548
self.number_of_changes = 0
49+
self.number_of_mutations = 0
4650

4751

4852
def axelrod_interaction(self, interacting_individual: "Individual") -> None:
@@ -55,21 +59,34 @@ def axelrod_interaction(self, interacting_individual: "Individual") -> None:
5559
interacting_individual (Individual): Individual with which the self individual interacts. Currently accepts only "axelrod_interaction".
5660
"""
5761
probability_of_interaction = 1 - np.count_nonzero(self.features - interacting_individual.features)/self.number_of_features
58-
random_number = np.random.rand()
59-
if (random_number <= probability_of_interaction) and (probability_of_interaction < 1.0):
60-
index = np.random.choice(np.nonzero(self.features - interacting_individual.features)[0])
61-
self.features[index] = interacting_individual.features[index]
62-
self.number_of_changes += 1
62+
[interaction_random_number, mutation_random_number] = np.random.rand(2) # generates two random numbers
63+
if (interaction_random_number <= probability_of_interaction) and (probability_of_interaction < 1.0):
64+
index_to_copy = np.random.choice(np.nonzero(self.features - interacting_individual.features)[0])
65+
if mutation_random_number <= self.mutation_rate:
66+
# if mutation is occurring, just chose a random trait from possible traits
67+
self.features[index_to_copy] = np.random.randint(low = 1, high = self.number_of_traits+1, size=1)
68+
self.number_of_mutations += 1
69+
self.number_of_changes += 1
70+
else:
71+
self.features[index_to_copy] = interacting_individual.features[index_to_copy]
72+
self.number_of_changes += 1
73+
6374

6475
def neutral_interaction(self, interacting_individual: "Individual") -> None:
6576
"""
6677
Interaction following a neutral model, where replication of a trait is purely based on frequency in the population. The focal indivdual changes one
6778
trait at random copying from the source individual.
6879
"""
69-
index = np.random.choice(range(0, self.number_of_features))
70-
self.features[index] = interacting_individual.features[index]
71-
self.number_of_changes += 1
72-
80+
mutation_random_number = np.random.rand()
81+
index_to_copy = np.random.choice(range(0, self.number_of_features))
82+
if mutation_random_number <= self.mutation_rate:
83+
self.features[index_to_copy] = np.random.randint(low = 1, high = self.number_of_trait+1, size=1)
84+
self.number_of_mutations += 1
85+
self.number_of_changes += 1
86+
else:
87+
self.features[index_to_copy] = interacting_individual.features[index_to_copy]
88+
self.number_of_changes += 1
89+
7390

7491
def interact(self, interacting_individual: "Individual", interaction_function: str) -> None:
7592
"""

metapypulation/metapopulation.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class Metapopulation():
2424
carrying_capacities (List[int] | int): A list of carrying capacities (one for each subpopulation) or an integer (same carrying capacity for each subpopulation).
2525
number_of_features (int): Total number of cultural features per individual.
2626
number_of_traits (int, optional): Number of different possible traits for each cultural feature.
27+
mutation_rate (float, optional): Probability of a mutation to occur.
2728
min_trait (int, optional): Minimum value for a trait in each feature.
2829
max_trait (int, optional): Maximum value for a trait in each feature.
2930
"""
@@ -33,6 +34,7 @@ def __init__(self, number_of_subpopulations: int,
3334
carrying_capacities: List[int] | int = 100,
3435
number_of_features: int = 5,
3536
number_of_traits: int = 10,
37+
mutation_rate: float = 0.0,
3638
min_trait: int = 1,
3739
max_trait: int = 10
3840
):
@@ -45,6 +47,7 @@ def __init__(self, number_of_subpopulations: int,
4547
carrying_capacities (List[int] | int, optional): Either a list of carrying capacities (of which the `len()` is the same as `number_of_subpopulations`) or single integer determining the same carrying capacity for all subpopulations. Defaults to 100.
4648
number_of_features (int, optional): Total number of cultural features per individual. Defaults to 5.
4749
number_of_traits (int, optional): Number of different possible traits for each cultural feature. Defaults to 10.
50+
mutation_rate (float, optional): Probability of a mutation to occur. Defaults to 0.0.
4851
min_trait (int, optional): Minimum value for a trait in each feature. Defaults to 1.
4952
max_trait (int, optional): Maximum value for a trait in each feature. Deafults to 10.
5053
"""
@@ -55,6 +58,7 @@ def __init__(self, number_of_subpopulations: int,
5558
self.carrying_capacities = carrying_capacities
5659
self.number_of_features = number_of_features
5760
self.number_of_traits = number_of_traits
61+
self.mutation_rate = mutation_rate
5862
self.min_trait = min_trait
5963
self.max_trait = max_trait
6064

@@ -72,14 +76,14 @@ def populate(self) -> None:
7276
for i in range(self.carrying_capacities[subpopulation.id]):
7377
derived_number_of_traits = self.max_trait - self.min_trait + 1 # e.g. if smaller trait is 1 and largest is 10, there are 10 traits: 10 - 1 + 1
7478
set_of_features = np.random.randint(low = self.min_trait, high = self.max_trait + 1, size = self.number_of_features)
75-
new_individual = Individual(i, subpopulation.id, self.number_of_features, derived_number_of_traits, set_of_features)
79+
new_individual = Individual(i, subpopulation.id, self.number_of_features, derived_number_of_traits, self.mutation_rate, set_of_features)
7680
subpopulation.add_individual(new_individual)
7781
case int():
7882
for subpopulation in self.subpopulations:
7983
for i in range(self.carrying_capacities):
8084
derived_number_of_traits = self.max_trait - self.min_trait + 1 # e.g. if smaller trait is 1 and largest is 10, there are 10 traits: 10 - 1 + 1
8185
set_of_features = np.random.randint(low = self.min_trait, high = self.max_trait + 1, size = self.number_of_features)
82-
new_individual = Individual(i, subpopulation.id, self.number_of_features, derived_number_of_traits, set_of_features)
86+
new_individual = Individual(i, subpopulation.id, self.number_of_features, derived_number_of_traits, self.mutation_rate, set_of_features)
8387
subpopulation.add_individual(new_individual)
8488

8589

metapypulation/simulation.py

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -142,53 +142,6 @@ def run_single_replicate(self, replicate_id: int) -> None:
142142

143143
print(f"{t} generations ran in {total_time}.")
144144

145-
146-
# def run_replicate_with_burn_in(self, replicate_id: int) -> None:
147-
# """
148-
# Run one replicate of the simulation.
149-
150-
# Args:
151-
# replicate_id (int): The number of the current replicate (for the output data columns).
152-
# """
153-
# metapopulation = Metapopulation(self.number_of_subpopulations, self.interaction_type, self.migration_matrix, self.carrying_capacities)
154-
# metapopulation.populate()
155-
156-
# set_counts = []
157-
# shannon = []
158-
# metapop_counts = []
159-
# metapop_shannon = []
160-
161-
# start_time = time.time()
162-
163-
# for t in range(self.generations + 1):
164-
# if self.verbose:
165-
# if t%self.verbose_timing == 0:
166-
# print(f"Replicate {replicate_id}, gen {t}!")
167-
# # TODO print other fun stuff
168-
169-
# if t%self.measure_timing == 0:
170-
# set_counts.append(np.mean(metapopulation.traits_sets_per_subpopulation()))
171-
# shannon.append(np.mean(metapopulation.shannon_diversity_per_subpopulation()))
172-
# metapop_counts.append(metapopulation.metapopulation_test_sets())
173-
# metapop_shannon.append(metapopulation.metapopulation_shannon_diversity())
174-
175-
# if t > self.burn_in:
176-
# metapopulation.migrate()
177-
178-
# metapopulation.make_interact()
179-
180-
# self.subpop_set_counts = pd.concat([self.subpop_set_counts, pd.Series(set_counts, name=replicate_id)], axis=1)
181-
# self.subpop_shannon = pd.concat([self.subpop_shannon, pd.Series(shannon, name=replicate_id)], axis=1)
182-
# self.metapop_set_counts = pd.concat([self.metapop_set_counts, pd.Series(metapop_counts, name=replicate_id)], axis=1)
183-
# self.metapop_shannoneturn = pd.concat([self.metapop_shannon, pd.Series(metapop_shannon, name=replicate_id)], axis=1)
184-
185-
# if self.verbose:
186-
# end_time = time.time()
187-
# total_time = end_time - start_time
188-
# total_time = time.strftime("%H:%M:%S", time.gmtime(total_time))
189-
190-
# print(f"{t} generations ran in {total_time}.")
191-
192145

193146
def run_simulation(self) -> None:
194147
"""

script_Kiel2025.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import matplotlib.pyplot as plt
2+
import numpy as np
3+
import time
4+
5+
from metapypulation.simulation import Simulation
6+
7+
total_population = 200
8+
replicates = 1
9+
#migrations = ['island'] # 'stepping_stone'
10+
interactions = ['neutral_interaction'] # 'axelrod_interaction'
11+
#number_of_subpopulations = [4]
12+
subpopulations = 4
13+
population_sizes = [[50, 50, 50, 50]]
14+
15+
start_time = time.time()
16+
17+
count = 0
18+
19+
for interaction in interactions:
20+
for rate_of_migration in [0.001, 0.1]:
21+
# rate_of_migration = 0.001
22+
23+
carrying_capacity = int(np.ceil(total_population / subpopulations)) # [283, 39, 39, 39]#
24+
generations = 200000 #350000
25+
burn_in = 0
26+
27+
# create one big simulation
28+
simulation = Simulation(generations = generations,
29+
number_of_subpopulations=subpopulations,
30+
migration_matrix = migration,
31+
interaction = interaction,
32+
carrying_capacities = carrying_capacity,
33+
replicates = replicates,
34+
output_path = f"./Outputs/{subpopulations}subpop_popsize{population_sizes[0]}_{migration}_{interaction}_{rate_of_migration}_noburnin", #TAG2024/04-migration-rates/{subpopulations}subpop_{migration}_{interaction}_m{rate_of_migration}",
35+
burn_in = burn_in,
36+
migration_rate = rate_of_migration)
37+
38+
# modify individuals in simulation
39+
40+
41+
simulation.run_simulation()
42+
count = count + 1
43+
#f"./Outputs/SourceSink/pop{total_population}/{migrations}/{subpopulations}subpop_m1e-3_burnin_{carrying_capacity[0]}",
44+
45+
end_time = time.time() - start_time
46+
hours = round(end_time//3600)
47+
minutes = round(end_time//60) - hours*60
48+
seconds = round(end_time) - hours*3600 - minutes*60
49+
print(f"Simulation of {count} sets of parameters, {replicates} replicates each, finished in {hours}h, {minutes}m and {seconds}s")

0 commit comments

Comments
 (0)