aegis_sim.submodels.genetics.ploider
1import numpy as np 2from numba import njit, prange 3 4 5@njit(parallel=True) 6def _diploid_to_haploid_numba(c0, c1, dominance_per_locus): 7 """Parallel numba kernel for diploid-to-haploid conversion. 8 9 Operates on uint8 views of bool arrays to avoid numba's bool limitations. 10 Returns float32 output: 1.0 for homozygous true, 0.0 for homozygous false, 11 dominance_per_locus[j] for heterozygous (j = locus index along axis 1). 12 """ 13 out = np.empty(c0.shape, dtype=np.float32) 14 n0, n1, n2 = c0.shape 15 for i in prange(n0): 16 for j in range(n1): 17 h = dominance_per_locus[j] 18 for k in range(n2): 19 a = c0[i, j, k] 20 b = c1[i, j, k] 21 if a == b: 22 out[i, j, k] = np.float32(a) 23 else: 24 out[i, j, k] = h 25 return out 26 27 28class Ploider: 29 """ """ 30 31 def init(self, REPRODUCTION_MODE, DOMINANCE_FACTOR, PLOIDY): 32 self.REPRODUCTION_MODE = REPRODUCTION_MODE 33 # Kept for backward-compatible config parsing; no longer used by the 34 # collapse kernel. See G_<trait>_dominance for per-trait control. 35 self.DOMINANCE_FACTOR = DOMINANCE_FACTOR 36 self.y = PLOIDY 37 38 if REPRODUCTION_MODE == "sexual": 39 assert PLOIDY == 2, f"If reproduction is sexual, ploidy can only be 2, not {PLOIDY}." 40 41 def diploid_to_haploid(self, loci, dominance_per_locus=None): 42 """Merge two arrays encoding two chromatids into one array. 43 44 Arguments: 45 loci: A bool numpy array with shape (population size, ploidy, genome length, BITS_PER_LOCUS) 46 dominance_per_locus: float32 array of shape (genome length,) giving h per locus. 47 If None, a uniform array of 0.5 (codominant) is used. 48 49 Returns: 50 A float numpy array with shape (population size, genome length, BITS_PER_LOCUS) 51 """ 52 53 assert len(loci.shape) == 4, len(loci.shape) # e.g. (45, 2, 250, 8) 54 assert loci.shape[1] == 2, loci.shape[1] # ploidy 55 56 n_loci = loci.shape[2] 57 if dominance_per_locus is None: 58 # Backward-compatible fallback: callers that don't pass per-locus 59 # dominance (e.g. the modifying architecture, or unit tests) get a 60 # uniform array filled with self.DOMINANCE_FACTOR (the legacy global). 61 # The composite architecture passes a per-trait array that takes precedence. 62 fallback = getattr(self, "DOMINANCE_FACTOR", 0.5) 63 dominance_per_locus = np.full(n_loci, fallback, dtype=np.float32) 64 else: 65 assert dominance_per_locus.shape == (n_loci,), ( 66 f"dominance_per_locus shape {dominance_per_locus.shape} != ({n_loci},)" 67 ) 68 if dominance_per_locus.dtype != np.float32: 69 dominance_per_locus = dominance_per_locus.astype(np.float32) 70 71 arr = _diploid_to_haploid_numba( 72 loci[:, 0].view(np.uint8), 73 loci[:, 1].view(np.uint8), 74 dominance_per_locus, 75 ) 76 77 assert len(arr.shape) == 3, len(arr.shape) 78 79 return arr 80 81 82ploider = Ploider()
class
Ploider:
29class Ploider: 30 """ """ 31 32 def init(self, REPRODUCTION_MODE, DOMINANCE_FACTOR, PLOIDY): 33 self.REPRODUCTION_MODE = REPRODUCTION_MODE 34 # Kept for backward-compatible config parsing; no longer used by the 35 # collapse kernel. See G_<trait>_dominance for per-trait control. 36 self.DOMINANCE_FACTOR = DOMINANCE_FACTOR 37 self.y = PLOIDY 38 39 if REPRODUCTION_MODE == "sexual": 40 assert PLOIDY == 2, f"If reproduction is sexual, ploidy can only be 2, not {PLOIDY}." 41 42 def diploid_to_haploid(self, loci, dominance_per_locus=None): 43 """Merge two arrays encoding two chromatids into one array. 44 45 Arguments: 46 loci: A bool numpy array with shape (population size, ploidy, genome length, BITS_PER_LOCUS) 47 dominance_per_locus: float32 array of shape (genome length,) giving h per locus. 48 If None, a uniform array of 0.5 (codominant) is used. 49 50 Returns: 51 A float numpy array with shape (population size, genome length, BITS_PER_LOCUS) 52 """ 53 54 assert len(loci.shape) == 4, len(loci.shape) # e.g. (45, 2, 250, 8) 55 assert loci.shape[1] == 2, loci.shape[1] # ploidy 56 57 n_loci = loci.shape[2] 58 if dominance_per_locus is None: 59 # Backward-compatible fallback: callers that don't pass per-locus 60 # dominance (e.g. the modifying architecture, or unit tests) get a 61 # uniform array filled with self.DOMINANCE_FACTOR (the legacy global). 62 # The composite architecture passes a per-trait array that takes precedence. 63 fallback = getattr(self, "DOMINANCE_FACTOR", 0.5) 64 dominance_per_locus = np.full(n_loci, fallback, dtype=np.float32) 65 else: 66 assert dominance_per_locus.shape == (n_loci,), ( 67 f"dominance_per_locus shape {dominance_per_locus.shape} != ({n_loci},)" 68 ) 69 if dominance_per_locus.dtype != np.float32: 70 dominance_per_locus = dominance_per_locus.astype(np.float32) 71 72 arr = _diploid_to_haploid_numba( 73 loci[:, 0].view(np.uint8), 74 loci[:, 1].view(np.uint8), 75 dominance_per_locus, 76 ) 77 78 assert len(arr.shape) == 3, len(arr.shape) 79 80 return arr
def
init(self, REPRODUCTION_MODE, DOMINANCE_FACTOR, PLOIDY):
32 def init(self, REPRODUCTION_MODE, DOMINANCE_FACTOR, PLOIDY): 33 self.REPRODUCTION_MODE = REPRODUCTION_MODE 34 # Kept for backward-compatible config parsing; no longer used by the 35 # collapse kernel. See G_<trait>_dominance for per-trait control. 36 self.DOMINANCE_FACTOR = DOMINANCE_FACTOR 37 self.y = PLOIDY 38 39 if REPRODUCTION_MODE == "sexual": 40 assert PLOIDY == 2, f"If reproduction is sexual, ploidy can only be 2, not {PLOIDY}."
def
diploid_to_haploid(self, loci, dominance_per_locus=None):
42 def diploid_to_haploid(self, loci, dominance_per_locus=None): 43 """Merge two arrays encoding two chromatids into one array. 44 45 Arguments: 46 loci: A bool numpy array with shape (population size, ploidy, genome length, BITS_PER_LOCUS) 47 dominance_per_locus: float32 array of shape (genome length,) giving h per locus. 48 If None, a uniform array of 0.5 (codominant) is used. 49 50 Returns: 51 A float numpy array with shape (population size, genome length, BITS_PER_LOCUS) 52 """ 53 54 assert len(loci.shape) == 4, len(loci.shape) # e.g. (45, 2, 250, 8) 55 assert loci.shape[1] == 2, loci.shape[1] # ploidy 56 57 n_loci = loci.shape[2] 58 if dominance_per_locus is None: 59 # Backward-compatible fallback: callers that don't pass per-locus 60 # dominance (e.g. the modifying architecture, or unit tests) get a 61 # uniform array filled with self.DOMINANCE_FACTOR (the legacy global). 62 # The composite architecture passes a per-trait array that takes precedence. 63 fallback = getattr(self, "DOMINANCE_FACTOR", 0.5) 64 dominance_per_locus = np.full(n_loci, fallback, dtype=np.float32) 65 else: 66 assert dominance_per_locus.shape == (n_loci,), ( 67 f"dominance_per_locus shape {dominance_per_locus.shape} != ({n_loci},)" 68 ) 69 if dominance_per_locus.dtype != np.float32: 70 dominance_per_locus = dominance_per_locus.astype(np.float32) 71 72 arr = _diploid_to_haploid_numba( 73 loci[:, 0].view(np.uint8), 74 loci[:, 1].view(np.uint8), 75 dominance_per_locus, 76 ) 77 78 assert len(arr.shape) == 3, len(arr.shape) 79 80 return arr
Merge two arrays encoding two chromatids into one array.
Arguments: loci: A bool numpy array with shape (population size, ploidy, genome length, BITS_PER_LOCUS) dominance_per_locus: float32 array of shape (genome length,) giving h per locus. If None, a uniform array of 0.5 (codominant) is used.
Returns: A float numpy array with shape (population size, genome length, BITS_PER_LOCUS)
ploider =
<Ploider object>