aegis_sim.submodels.reproduction.matingmanager

 1import numpy as np
 2
 3
 4class MatingManager:
 5    def __init__(self):
 6        pass
 7
 8    def pair_up_polygamously(self, sexes, parent_positions=None, max_search_radius=0):
 9        """
10        Return slot indices (into the reproducing pool) of paired males and females.
11
12        When `parent_positions` is None: classical well-mixed pairing — males and
13        females are shuffled and paired up to min(n_males, n_females) pairs.
14
15        When `parent_positions` is provided (LATTICE_MODE=True): expanding-ring
16        spatial pairing — for each female, search out from her cell up to
17        `max_search_radius` hex rings; pair her with a random male found in the
18        closest ring that contains any male. Males can mate with multiple
19        females (polygamous), so a single male slot may be paired multiple times.
20        Females who find no male within the search radius are not paired.
21        """
22        indices_male = (sexes == 0).nonzero()[0]
23        indices_female = (sexes == 1).nonzero()[0]
24
25        if parent_positions is None:
26            # Classical well-mixed pairing (unchanged behaviour)
27            n_pairs = min(len(indices_male), len(indices_female))
28            np.random.shuffle(indices_male)
29            np.random.shuffle(indices_female)
30            males = indices_male[:n_pairs]
31            females = indices_female[:n_pairs]
32            return males, females
33
34        # Lattice-aware pairing
35        if len(indices_male) == 0 or len(indices_female) == 0:
36            return np.array([], dtype=np.int64), np.array([], dtype=np.int64)
37
38        from aegis_sim.submodels import lattice
39
40        # Build a lookup: (q, r) -> male slot index. If multiple males share a
41        # cell (shouldn't happen with one-per-cell, but defensive), the last wins.
42        male_pos_to_slot = {}
43        for slot in indices_male:
44            q, r = parent_positions[slot]
45            male_pos_to_slot[(int(q), int(r))] = int(slot)
46
47        paired_males = []
48        paired_females = []
49        # Shuffle female search order so early females don't monopolise nearby males
50        female_order = indices_female.copy()
51        np.random.shuffle(female_order)
52
53        for f_slot in female_order:
54            f_q, f_r = parent_positions[f_slot]
55            found = -1
56            for radius in range(1, max_search_radius + 1):
57                ring_cells = lattice.ring(int(f_q), int(f_r), radius)
58                candidates = []
59                for c in ring_cells:
60                    key = (int(c[0]), int(c[1]))
61                    if key in male_pos_to_slot:
62                        candidates.append(male_pos_to_slot[key])
63                if candidates:
64                    found = int(candidates[np.random.randint(len(candidates))])
65                    break
66            if found != -1:
67                paired_males.append(found)
68                paired_females.append(int(f_slot))
69
70        return (
71            np.asarray(paired_males, dtype=np.int64),
72            np.asarray(paired_females, dtype=np.int64),
73        )
74
75    def pair_up_monogamously(self, sexes):
76        return
class MatingManager:
 5class MatingManager:
 6    def __init__(self):
 7        pass
 8
 9    def pair_up_polygamously(self, sexes, parent_positions=None, max_search_radius=0):
10        """
11        Return slot indices (into the reproducing pool) of paired males and females.
12
13        When `parent_positions` is None: classical well-mixed pairing — males and
14        females are shuffled and paired up to min(n_males, n_females) pairs.
15
16        When `parent_positions` is provided (LATTICE_MODE=True): expanding-ring
17        spatial pairing — for each female, search out from her cell up to
18        `max_search_radius` hex rings; pair her with a random male found in the
19        closest ring that contains any male. Males can mate with multiple
20        females (polygamous), so a single male slot may be paired multiple times.
21        Females who find no male within the search radius are not paired.
22        """
23        indices_male = (sexes == 0).nonzero()[0]
24        indices_female = (sexes == 1).nonzero()[0]
25
26        if parent_positions is None:
27            # Classical well-mixed pairing (unchanged behaviour)
28            n_pairs = min(len(indices_male), len(indices_female))
29            np.random.shuffle(indices_male)
30            np.random.shuffle(indices_female)
31            males = indices_male[:n_pairs]
32            females = indices_female[:n_pairs]
33            return males, females
34
35        # Lattice-aware pairing
36        if len(indices_male) == 0 or len(indices_female) == 0:
37            return np.array([], dtype=np.int64), np.array([], dtype=np.int64)
38
39        from aegis_sim.submodels import lattice
40
41        # Build a lookup: (q, r) -> male slot index. If multiple males share a
42        # cell (shouldn't happen with one-per-cell, but defensive), the last wins.
43        male_pos_to_slot = {}
44        for slot in indices_male:
45            q, r = parent_positions[slot]
46            male_pos_to_slot[(int(q), int(r))] = int(slot)
47
48        paired_males = []
49        paired_females = []
50        # Shuffle female search order so early females don't monopolise nearby males
51        female_order = indices_female.copy()
52        np.random.shuffle(female_order)
53
54        for f_slot in female_order:
55            f_q, f_r = parent_positions[f_slot]
56            found = -1
57            for radius in range(1, max_search_radius + 1):
58                ring_cells = lattice.ring(int(f_q), int(f_r), radius)
59                candidates = []
60                for c in ring_cells:
61                    key = (int(c[0]), int(c[1]))
62                    if key in male_pos_to_slot:
63                        candidates.append(male_pos_to_slot[key])
64                if candidates:
65                    found = int(candidates[np.random.randint(len(candidates))])
66                    break
67            if found != -1:
68                paired_males.append(found)
69                paired_females.append(int(f_slot))
70
71        return (
72            np.asarray(paired_males, dtype=np.int64),
73            np.asarray(paired_females, dtype=np.int64),
74        )
75
76    def pair_up_monogamously(self, sexes):
77        return
def pair_up_polygamously(self, sexes, parent_positions=None, max_search_radius=0):
 9    def pair_up_polygamously(self, sexes, parent_positions=None, max_search_radius=0):
10        """
11        Return slot indices (into the reproducing pool) of paired males and females.
12
13        When `parent_positions` is None: classical well-mixed pairing — males and
14        females are shuffled and paired up to min(n_males, n_females) pairs.
15
16        When `parent_positions` is provided (LATTICE_MODE=True): expanding-ring
17        spatial pairing — for each female, search out from her cell up to
18        `max_search_radius` hex rings; pair her with a random male found in the
19        closest ring that contains any male. Males can mate with multiple
20        females (polygamous), so a single male slot may be paired multiple times.
21        Females who find no male within the search radius are not paired.
22        """
23        indices_male = (sexes == 0).nonzero()[0]
24        indices_female = (sexes == 1).nonzero()[0]
25
26        if parent_positions is None:
27            # Classical well-mixed pairing (unchanged behaviour)
28            n_pairs = min(len(indices_male), len(indices_female))
29            np.random.shuffle(indices_male)
30            np.random.shuffle(indices_female)
31            males = indices_male[:n_pairs]
32            females = indices_female[:n_pairs]
33            return males, females
34
35        # Lattice-aware pairing
36        if len(indices_male) == 0 or len(indices_female) == 0:
37            return np.array([], dtype=np.int64), np.array([], dtype=np.int64)
38
39        from aegis_sim.submodels import lattice
40
41        # Build a lookup: (q, r) -> male slot index. If multiple males share a
42        # cell (shouldn't happen with one-per-cell, but defensive), the last wins.
43        male_pos_to_slot = {}
44        for slot in indices_male:
45            q, r = parent_positions[slot]
46            male_pos_to_slot[(int(q), int(r))] = int(slot)
47
48        paired_males = []
49        paired_females = []
50        # Shuffle female search order so early females don't monopolise nearby males
51        female_order = indices_female.copy()
52        np.random.shuffle(female_order)
53
54        for f_slot in female_order:
55            f_q, f_r = parent_positions[f_slot]
56            found = -1
57            for radius in range(1, max_search_radius + 1):
58                ring_cells = lattice.ring(int(f_q), int(f_r), radius)
59                candidates = []
60                for c in ring_cells:
61                    key = (int(c[0]), int(c[1]))
62                    if key in male_pos_to_slot:
63                        candidates.append(male_pos_to_slot[key])
64                if candidates:
65                    found = int(candidates[np.random.randint(len(candidates))])
66                    break
67            if found != -1:
68                paired_males.append(found)
69                paired_females.append(int(f_slot))
70
71        return (
72            np.asarray(paired_males, dtype=np.int64),
73            np.asarray(paired_females, dtype=np.int64),
74        )

Return slot indices (into the reproducing pool) of paired males and females.

When parent_positions is None: classical well-mixed pairing — males and females are shuffled and paired up to min(n_males, n_females) pairs.

When parent_positions is provided (LATTICE_MODE=True): expanding-ring spatial pairing — for each female, search out from her cell up to max_search_radius hex rings; pair her with a random male found in the closest ring that contains any male. Males can mate with multiple females (polygamous), so a single male slot may be paired multiple times. Females who find no male within the search radius are not paired.

def pair_up_monogamously(self, sexes):
76    def pair_up_monogamously(self, sexes):
77        return